| 7 | 7 | .nb-gradle-properties |
| 8 | 8 | scrivenvar.pro |
| 9 | out | |
| 9 | 10 |
| 3 | 3 | <component name="ExternalStorageConfigurationManager" enabled="true" /> |
| 4 | 4 | <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" /> | |
| 6 | 6 | </component> |
| 7 | 7 | </project> |
| 3 | 3 | id 'org.openjfx.javafxplugin' version '0.0.8' |
| 4 | 4 | id 'com.palantir.git-version' version '0.12.3' |
| 5 | id 'org.beryx.jlink' version '2.16.2' | |
| 5 | 6 | } |
| 6 | 7 | |
| ... | ||
| 25 | 26 | implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3' |
| 26 | 27 | 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 | } | |
| 30 | 37 | |
| 31 | 38 | // Markdown |
| ... | ||
| 47 | 54 | implementation 'com.ximpleware:vtd-xml:2.13.4' |
| 48 | 55 | implementation 'net.sf.saxon:Saxon-HE:10.1' |
| 56 | implementation 'xalan:xalan:2.7.2' | |
| 49 | 57 | |
| 50 | 58 | // HTML parsing and rendering |
| ... | ||
| 70 | 78 | implementation 'org.apache.xmlgraphics:batik-util:1.13' |
| 71 | 79 | implementation 'org.apache.xmlgraphics:batik-xml:1.13' |
| 80 | ||
| 81 | implementation 'org.apache.bsf:bsf-api:3.1' | |
| 72 | 82 | |
| 73 | 83 | // Misc. |
| 74 | 84 | implementation 'org.ahocorasick:ahocorasick:0.4.0' |
| 75 | 85 | implementation 'org.apache.commons:commons-configuration2:2.7' |
| 76 | 86 | implementation 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3' |
| 77 | 87 | |
| 78 | 88 | def os = ['win', 'linux', 'mac'] |
| 79 | 89 | def fx = ['controls', 'graphics', 'fxml', 'swing'] |
| 80 | 90 | |
| 91 | // Create cross-platform überjar. | |
| 92 | // | |
| 93 | // Including these runtime dependencies breaks creating cross-platform binaries. | |
| 81 | 94 | fx.each { fxitem -> |
| 82 | 95 | os.each { ositem -> |
| ... | ||
| 91 | 104 | javafx { |
| 92 | 105 | version = "14" |
| 93 | modules = ['javafx.controls', 'javafx.graphics', 'javafx.swing'] | |
| 106 | modules = ['javafx.controls', 'javafx.swing'] | |
| 94 | 107 | } |
| 95 | 108 | |
| 96 | 109 | compileJava { |
| 97 | 110 | options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" |
| 98 | 111 | } |
| 99 | 112 | |
| 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 | ||
| 102 | 124 | version = gitVersion() |
| 103 | mainClassName = "com.${applicationName}.Main" | |
| 125 | sourceCompatibility = JavaVersion.VERSION_11 | |
| 126 | ||
| 104 | 127 | def launcherClassName = "com.${applicationName}.Launcher" |
| 105 | 128 | |
| 106 | 129 | def propertiesFile = new File("src/main/resources/com/${applicationName}/app.properties") |
| 107 | 130 | propertiesFile.write("application.version=${version}") |
| 108 | ||
| 109 | //sourceSets { | |
| 110 | // main { | |
| 111 | // resources { | |
| 112 | // srcDir 'resources' | |
| 113 | // } | |
| 114 | // } | |
| 115 | //} | |
| 116 | 131 | |
| 117 | 132 | jar { |
| ... | ||
| 142 | 157 | } |
| 143 | 158 | } |
| 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 | // } | |
| 144 | 185 | } |
| 145 | 186 | } |
| 42 | 42 | * Provides common behaviours for factories that instantiate classes based on |
| 43 | 43 | * file type. |
| 44 | * | |
| 45 | * @author White Magic Software, Ltd. | |
| 46 | 44 | */ |
| 47 | 45 | public class AbstractFileFactory { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 32 | 32 | /** |
| 33 | 33 | * Hides dependency on {@link MigPane} from subclasses. |
| 34 | * | |
| 35 | * @author White Magic Software, Ltd. | |
| 36 | 34 | */ |
| 37 | 35 | public abstract class AbstractPane extends MigPane { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 32 | 32 | /** |
| 33 | 33 | * Defines application-wide default values. |
| 34 | * | |
| 35 | * @author White Magic Software, Ltd. | |
| 36 | 34 | */ |
| 37 | 35 | public class Constants { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 Karl Tauber and White Magic Software, Ltd. | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * Redistribution and use in source and binary forms, with or without |
| ... | ||
| 62 | 62 | /** |
| 63 | 63 | * Editor for a single file. |
| 64 | * | |
| 65 | * @author Karl Tauber and White Magic Software, Ltd. | |
| 66 | 64 | */ |
| 67 | 65 | public final class FileEditorTab extends Tab { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 Karl Tauber and White Magic Software, Ltd. | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 67 | 67 | /** |
| 68 | 68 | * Tab pane for file editors. |
| 69 | * | |
| 70 | * @author Karl Tauber and White Magic Software, Ltd. | |
| 71 | 69 | */ |
| 72 | 70 | public final class FileEditorTabPane extends TabPane { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 30 | 30 | /** |
| 31 | 31 | * 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}. | |
| 36 | 34 | */ |
| 37 | 35 | public enum FileType { |
| 50 | 50 | */ |
| 51 | 51 | public static void main( final String[] args ) throws IOException { |
| 52 | // Shhh. | |
| 53 | System.err.close(); | |
| 54 | ||
| 55 | 52 | showAppInfo(); |
| 56 | 53 | Main.main( args ); |
| 57 | 54 | } |
| 58 | 55 | |
| 59 | 56 | @SuppressWarnings("RedundantStringFormatCall") |
| 60 | 57 | private static void showAppInfo() throws IOException { |
| 61 | 58 | 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() ) ); | |
| 63 | 60 | out( format( "Portions copyright 2020 Karl Tauber." ) ); |
| 64 | 61 | } |
| 1 | 1 | /* |
| 2 | * Copyright 2016 Karl Tauber and White Magic Software, Ltd. | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 57 | 57 | * Application entry point. The application allows users to edit Markdown |
| 58 | 58 | * files and see a real-time preview of the edits. |
| 59 | * | |
| 60 | * @author Karl Tauber and White Magic Software, Ltd. | |
| 61 | 59 | */ |
| 62 | 60 | public final class Main extends Application { |
| 63 | 61 | |
| 64 | // Suppress standard output logging; the Launcher suppresses stderr output. | |
| 65 | 62 | static { |
| 63 | // Suppress logging to standard output. | |
| 66 | 64 | LogManager.getLogManager().reset(); |
| 65 | ||
| 66 | // Suppress logging to standard error. | |
| 67 | System.err.close(); | |
| 67 | 68 | } |
| 68 | 69 | |
| 1 | 1 | /* |
| 2 | * Copyright 2016 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.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( | |
| 769 | 764 | () -> { |
| 770 | 765 | if( SystemUtils.IS_OS_WINDOWS ) { |
| 1 | 1 | /* |
| 2 | * Copyright (c) 2016 Karl Tauber <karl at jformdesigner dot com> | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | 3 | * All rights reserved. |
| 4 | 4 | * |
| ... | ||
| 37 | 37 | * Recursively resolves message properties. Property values can refer to other |
| 38 | 38 | * properties using a <code>${var}</code> syntax. |
| 39 | * | |
| 40 | * @author Karl Tauber, Dave Jarvis | |
| 41 | 39 | */ |
| 42 | 40 | public class Messages { |
| 177 | 177 | |
| 178 | 178 | 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 | |
| 180 | 180 | // the MouseHandler and ScrollHandler so that events only dispatch to one |
| 181 | 181 | // object (instead of one per editor tab). |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 35 | 35 | * Responsible for loading services. The services are treated as singleton |
| 36 | 36 | * instances. |
| 37 | * | |
| 38 | * @author White Magic Software, Ltd. | |
| 39 | 37 | */ |
| 40 | 38 | public class Services { |
| 1 | 1 | /* |
| 2 | * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com> | |
| 2 | * Copyright 2015 Karl Tauber <karl at jformdesigner dot com> | |
| 3 | 3 | * All rights reserved. |
| 4 | 4 | * |
| ... | ||
| 49 | 49 | * Button that opens a file chooser to select a local file for a URL in |
| 50 | 50 | * markdown. |
| 51 | * | |
| 52 | * @author Karl Tauber | |
| 53 | 51 | */ |
| 54 | 52 | public class BrowseFileButton extends Button { |
| 1 | 1 | /* |
| 2 | * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com> | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | 3 | * All rights reserved. |
| 4 | 4 | * |
| ... | ||
| 35 | 35 | /** |
| 36 | 36 | * TextField that can escape/unescape characters for markdown. |
| 37 | * | |
| 38 | * @author Karl Tauber and White Magic Software, Ltd. | |
| 39 | 37 | */ |
| 40 | 38 | public class EscapeTextField extends TextField { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 30 | 30 | /** |
| 31 | 31 | * Brackets variable names with <code>`r#</code> and <code>`</code>. |
| 32 | * | |
| 33 | * @author White Magic Software, Ltd. | |
| 34 | 32 | */ |
| 35 | 33 | public class RVariableDecorator implements VariableDecorator { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 30 | 30 | /** |
| 31 | 31 | * Brackets variable names with dollar symbols. |
| 32 | * | |
| 33 | * @author White Magic Software, Ltd. | |
| 34 | 32 | */ |
| 35 | 33 | public class YamlVariableDecorator implements VariableDecorator { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 43 | 43 | * sources. The data source could be YAML, TOML, JSON, flat files, or from a |
| 44 | 44 | * database. |
| 45 | * | |
| 46 | * @author White Magic Software, Ltd. | |
| 47 | 45 | */ |
| 48 | 46 | public class DefinitionFactory extends AbstractFileFactory { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 56 | 56 | * allows users to interact with key/value pairs loaded from the |
| 57 | 57 | * {@link DocumentParser} and adapted using a {@link TreeAdapter}. |
| 58 | * | |
| 59 | * @author White Magic Software, Ltd. | |
| 60 | 58 | */ |
| 61 | 59 | public final class DefinitionPane extends TitledPane { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| 36 | 36 | * in a map. The values in the map can use a delimited syntax to refer to |
| 37 | 37 | * keys in the map. |
| 38 | * | |
| 39 | * @author White Magic Software, Ltd. | |
| 40 | 38 | */ |
| 41 | 39 | public class MapInterpolator { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 43 | 43 | * |
| 44 | 44 | * @param <T> The type of {@link TreeItem} to store in the {@link TreeView}. |
| 45 | * @author White Magic Software, Ltd. | |
| 46 | 45 | */ |
| 47 | 46 | public class RootTreeItem<T> extends VariableTreeItem<T> { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 36 | 36 | * Responsible for converting an object hierarchy into a {@link TreeItem} |
| 37 | 37 | * hierarchy. |
| 38 | * | |
| 39 | * @author White Magic Software, Ltd. | |
| 40 | 38 | */ |
| 41 | 39 | public interface TreeAdapter { |
| 58 | 58 | * Reloading the definition file would work, but has a number of drawbacks. |
| 59 | 59 | * </p> |
| 60 | * | |
| 61 | * @author White Magic Software, Ltd. | |
| 62 | 60 | */ |
| 63 | 61 | public class TreeItemAdapter { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 40 | 40 | * |
| 41 | 41 | * @param <T> The type of TreeItem (usually String). |
| 42 | * @author White Magic Software, Ltd. | |
| 43 | 42 | */ |
| 44 | 43 | public class VariableTreeItem<T> extends TreeItem<T> { |
| 35 | 35 | /** |
| 36 | 36 | * Represents a definition data source for YAML files. |
| 37 | * | |
| 38 | * @author White Magic Software, Ltd. | |
| 39 | 37 | */ |
| 40 | 38 | public class YamlDefinitionSource implements DefinitionSource { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 42 | 42 | /** |
| 43 | 43 | * Responsible for reading a YAML document into an object hierarchy. |
| 44 | * | |
| 45 | * @author White Magic Software, Ltd. | |
| 46 | 44 | */ |
| 47 | 45 | public class YamlParser implements DocumentParser<JsonNode> { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 44 | 44 | * Transforms a JsonNode hierarchy into a tree that can be displayed in a user |
| 45 | 45 | * interface and vice-versa. |
| 46 | * | |
| 47 | * @author White Magic Software, Ltd. | |
| 48 | 46 | */ |
| 49 | 47 | public class YamlTreeAdapter implements TreeAdapter { |
| 38 | 38 | * Superclass that abstracts common behaviours for all dialogs. |
| 39 | 39 | * |
| 40 | * @author White Magic Software, Ltd. | |
| 41 | 40 | * @param <T> The type of dialog to create (usually String). |
| 42 | 41 | */ |
| 1 | 1 | /* |
| 2 | * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com> | |
| 2 | * Copyright 2015 Karl Tauber <karl at jformdesigner dot com> | |
| 3 | 3 | * All rights reserved. |
| 4 | 4 | * |
| ... | ||
| 45 | 45 | /** |
| 46 | 46 | * Dialog to enter a markdown image. |
| 47 | * | |
| 48 | * @author Karl Tauber | |
| 49 | 47 | */ |
| 50 | 48 | public class ImageDialog extends AbstractDialog<String> { |
| 45 | 45 | /** |
| 46 | 46 | * Dialog to enter a markdown link. |
| 47 | * | |
| 48 | * @author Karl Tauber | |
| 49 | 47 | */ |
| 50 | 48 | public class LinkDialog extends AbstractDialog<String> { |
| 1 | 1 | /* |
| 2 | * Copyright 2017 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 41 | 41 | * Responsible for managing the R startup script that is run when an R source |
| 42 | 42 | * file is loaded. |
| 43 | * | |
| 44 | * @author White Magic Software, Ltd. | |
| 45 | 43 | */ |
| 46 | 44 | public class RScriptDialog extends AbstractDialog<String> { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 Karl Tauber and White Magic Software, Ltd. | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 28 | 28 | package com.scrivenvar.editors; |
| 29 | 29 | |
| 30 | import javafx.application.Platform; | |
| 31 | 30 | import javafx.beans.property.ObjectProperty; |
| 32 | 31 | import javafx.beans.property.SimpleObjectProperty; |
| ... | ||
| 42 | 41 | |
| 43 | 42 | import java.nio.file.Path; |
| 43 | import java.util.Random; | |
| 44 | 44 | import java.util.function.Consumer; |
| 45 | 45 | |
| 46 | import static javafx.application.Platform.runLater; | |
| 46 | 47 | import static org.fxmisc.wellbehaved.event.InputMap.consume; |
| 47 | 48 | |
| 48 | 49 | /** |
| 49 | 50 | * Represents common editing features for various types of text editors. |
| 50 | * | |
| 51 | * @author White Magic Software, Ltd. | |
| 52 | 51 | */ |
| 53 | 52 | public class EditorPane extends Pane { |
| ... | ||
| 65 | 64 | @Override |
| 66 | 65 | public void requestFocus() { |
| 67 | Platform.runLater( () -> getEditor().requestFocus() ); | |
| 66 | runLater( () -> getEditor().requestFocus() ); | |
| 68 | 67 | } |
| 69 | 68 | |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 37 | 37 | * Responsible for creating a variable name decorator suited to a particular |
| 38 | 38 | * file type. |
| 39 | * | |
| 40 | * @author White Magic Software, Ltd. | |
| 41 | 39 | */ |
| 42 | 40 | public class VariableNameDecoratorFactory extends AbstractFileFactory { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 47 | 47 | /** |
| 48 | 48 | * Provides the logic for injecting variable names within the editor. |
| 49 | * | |
| 50 | * @author White Magic Software, Ltd. | |
| 51 | 49 | */ |
| 52 | 50 | public final class VariableNameInjector { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 32 | 32 | /** |
| 33 | 33 | * Represents the model for a hyperlink: text and url text. |
| 34 | * | |
| 35 | * @author White Magic Software, Ltd. | |
| 36 | 34 | */ |
| 37 | 35 | public class HyperlinkModel { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 34 | 34 | |
| 35 | 35 | /** |
| 36 | * @author White Magic Software, Ltd. | |
| 36 | * Visits hyperlinks in a document so that the user can edit the hyperlink | |
| 37 | * within a dialog. | |
| 37 | 38 | */ |
| 38 | 39 | public class LinkVisitor { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 Karl Tauber and White Magic Software, Ltd. | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 55 | 55 | /** |
| 56 | 56 | * Markdown editor pane. |
| 57 | * | |
| 58 | * @author Karl Tauber and White Magic Software, Ltd. | |
| 59 | 57 | */ |
| 60 | 58 | public class MarkdownEditorPane extends EditorPane { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 37 | 37 | * Responsible for testing whether a given path (to a file) matches one of the |
| 38 | 38 | * filename extension patterns provided during construction. |
| 39 | * | |
| 40 | * @author White Magic Software, Ltd. | |
| 41 | 39 | */ |
| 42 | 40 | public class FileTypePredicate implements Predicate<File> { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 30 | 30 | /** |
| 31 | 31 | * Determines if one string contains another. |
| 32 | * | |
| 33 | * @author White Magic Software, Ltd. | |
| 34 | 32 | */ |
| 35 | 33 | public class ContainsPredicate extends StringPredicate { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 30 | 30 | /** |
| 31 | 31 | * Determines if a string starts with another. |
| 32 | * | |
| 33 | * @author White Magic Software, Ltd. | |
| 34 | 32 | */ |
| 35 | 33 | public class StartsPredicate extends StringPredicate { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 32 | 32 | /** |
| 33 | 33 | * General predicate for different types of string comparisons. |
| 34 | * | |
| 35 | * @author White Magic Software, Ltd. | |
| 36 | 34 | */ |
| 37 | 35 | public abstract class StringPredicate implements Predicate<String> { |
| 1 | 1 | /* |
| 2 | 2 | * {{{ 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 | |
| 5 | 5 | * |
| 6 | 6 | * This program is free software; you can redistribute it and/or |
| 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 | } | |
| 1 | 111 |
| 47 | 47 | import javax.swing.*; |
| 48 | 48 | import java.awt.*; |
| 49 | import java.awt.event.ComponentEvent; | |
| 50 | import java.awt.event.ComponentListener; | |
| 49 | 51 | import java.nio.file.Path; |
| 50 | 52 | |
| 51 | 53 | import static com.scrivenvar.Constants.PARAGRAPH_ID_PREFIX; |
| 52 | 54 | import static com.scrivenvar.Constants.STYLESHEET_PREVIEW; |
| 55 | import static org.xhtmlrenderer.swing.ImageResourceLoader.NO_OP_REPAINT_LISTENER; | |
| 53 | 56 | |
| 54 | 57 | /** |
| 55 | 58 | * HTML preview pane is responsible for rendering an HTML document. |
| 56 | * | |
| 57 | * @author Karl Tauber and White Magic Software, Ltd. | |
| 58 | 59 | */ |
| 59 | 60 | public final class HTMLPreviewPane extends Pane { |
| ... | ||
| 105 | 106 | private final static String HTML_FOOTER = "</body></html>"; |
| 106 | 107 | |
| 107 | private final StringBuilder mHtml = new StringBuilder( 65536 ); | |
| 108 | private final StringBuilder mHtmlDocument = new StringBuilder( 65536 ); | |
| 108 | 109 | private final int mHtmlPrefixLength; |
| 109 | 110 | |
| 110 | 111 | private final W3CDom mW3cDom = new W3CDom(); |
| 111 | 112 | private final XhtmlNamespaceHandler mNamespaceHandler = |
| 112 | 113 | new XhtmlNamespaceHandler(); |
| 113 | private final HTMLPanel mRenderer = new HTMLPanel(); | |
| 114 | private final HTMLPanel mHtmlRenderer = new HTMLPanel(); | |
| 114 | 115 | private final SwingNode mSwingNode = new SwingNode(); |
| 115 | private final JScrollPane mScrollPane = new JScrollPane( mRenderer ); | |
| 116 | private final JScrollPane mScrollPane = new JScrollPane( mHtmlRenderer ); | |
| 116 | 117 | private final DocumentEventHandler mDocumentHandler = |
| 117 | 118 | new DocumentEventHandler(); |
| 119 | private final CustomImageResourceLoader mImageLoader = | |
| 120 | new CustomImageResourceLoader(); | |
| 118 | 121 | |
| 119 | 122 | private Path mPath; |
| 120 | 123 | |
| 121 | 124 | /** |
| 122 | 125 | * Creates a new preview pane that can scroll to the caret position within the |
| 123 | 126 | * document. |
| 124 | 127 | */ |
| 125 | 128 | public HTMLPreviewPane() { |
| 129 | mHtmlDocument.append( HTML_HEADER ); | |
| 130 | mHtmlPrefixLength = mHtmlDocument.length(); | |
| 131 | ||
| 126 | 132 | final var factory = new ChainedReplacedElementFactory(); |
| 127 | 133 | factory.addFactory( new SVGReplacedElementFactory() ); |
| 128 | factory.addFactory( new SwingReplacedElementFactory() ); | |
| 134 | factory.addFactory( new SwingReplacedElementFactory( | |
| 135 | NO_OP_REPAINT_LISTENER, mImageLoader ) ); | |
| 129 | 136 | |
| 130 | 137 | final var context = getSharedContext(); |
| 131 | 138 | context.setReplacedElementFactory( factory ); |
| 132 | 139 | context.getTextRenderer().setSmoothingThreshold( 0 ); |
| 133 | 140 | |
| 134 | 141 | mSwingNode.setContent( mScrollPane ); |
| 135 | 142 | |
| 136 | mHtml.append( HTML_HEADER ); | |
| 137 | mHtmlPrefixLength = mHtml.length(); | |
| 143 | mHtmlRenderer.addDocumentListener( mDocumentHandler ); | |
| 144 | setStyle( "-fx-background-color: white;" ); | |
| 138 | 145 | |
| 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 | } ); | |
| 141 | 167 | } |
| 142 | 168 | |
| ... | ||
| 151 | 177 | final org.w3c.dom.Document w3cDoc = mW3cDom.fromJsoup( jsoupDoc ); |
| 152 | 178 | |
| 153 | mRenderer.setDocument( w3cDoc, getBaseUrl(), mNamespaceHandler ); | |
| 179 | mHtmlRenderer.setDocument( w3cDoc, getBaseUrl(), mNamespaceHandler ); | |
| 154 | 180 | } |
| 155 | 181 | |
| ... | ||
| 226 | 252 | |
| 227 | 253 | private void scrollTo( final Point point ) { |
| 228 | mRenderer.scrollTo( point ); | |
| 254 | mHtmlRenderer.scrollTo( point ); | |
| 229 | 255 | } |
| 230 | 256 | |
| ... | ||
| 242 | 268 | |
| 243 | 269 | private void scrollToBottom() { |
| 244 | scrollToY( mRenderer.getHeight() ); | |
| 270 | scrollToY( mHtmlRenderer.getHeight() ); | |
| 245 | 271 | } |
| 246 | 272 | |
| 247 | 273 | private Box getBoxById( final String id ) { |
| 248 | 274 | return getSharedContext().getBoxById( id ); |
| 249 | 275 | } |
| 250 | 276 | |
| 251 | 277 | private String decorate( final String html ) { |
| 252 | 278 | // Trim the HTML back to the header. |
| 253 | mHtml.setLength( mHtmlPrefixLength ); | |
| 279 | mHtmlDocument.setLength( mHtmlPrefixLength ); | |
| 254 | 280 | |
| 255 | 281 | // 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(); | |
| 259 | 285 | } |
| 260 | 286 | |
| ... | ||
| 307 | 333 | |
| 308 | 334 | if( !box.getStyle().isInline() ) { |
| 309 | final var margin = box.getMargin( mRenderer.getLayoutContext() ); | |
| 335 | final var margin = box.getMargin( mHtmlRenderer.getLayoutContext() ); | |
| 310 | 336 | x += margin.left(); |
| 311 | 337 | y += margin.top(); |
| ... | ||
| 323 | 349 | |
| 324 | 350 | private SharedContext getSharedContext() { |
| 325 | return mRenderer.getSharedContext(); | |
| 351 | return mHtmlRenderer.getSharedContext(); | |
| 326 | 352 | } |
| 327 | 353 | } |
| 28 | 28 | package com.scrivenvar.preview; |
| 29 | 29 | |
| 30 | import com.scrivenvar.Services; | |
| 31 | import com.scrivenvar.service.events.Notifier; | |
| 30 | 32 | import org.apache.batik.anim.dom.SAXSVGDocumentFactory; |
| 31 | 33 | import org.apache.batik.gvt.renderer.ImageRenderer; |
| ... | ||
| 51 | 53 | |
| 52 | 54 | public class SVGRasterizer { |
| 55 | private final static Notifier NOTIFIER = Services.load( Notifier.class ); | |
| 56 | ||
| 53 | 57 | private final static SAXSVGDocumentFactory mFactory = |
| 54 | 58 | new SAXSVGDocumentFactory( getXMLParserClassName() ); |
| 55 | 59 | |
| 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( | |
| 61 | 61 | KEY_ANTIALIASING, |
| 62 | 62 | VALUE_ANTIALIAS_ON, |
| 63 | KEY_ALPHA_INTERPOLATION, | |
| 64 | VALUE_ALPHA_INTERPOLATION_QUALITY, | |
| 63 | 65 | KEY_COLOR_RENDERING, |
| 64 | 66 | VALUE_COLOR_RENDER_QUALITY, |
| 65 | 67 | KEY_DITHERING, |
| 66 | 68 | VALUE_DITHER_DISABLE, |
| 69 | KEY_FRACTIONALMETRICS, | |
| 70 | VALUE_FRACTIONALMETRICS_ON, | |
| 71 | KEY_INTERPOLATION, | |
| 72 | VALUE_INTERPOLATION_BICUBIC, | |
| 67 | 73 | KEY_RENDERING, |
| 68 | 74 | VALUE_RENDER_QUALITY, |
| 69 | 75 | KEY_STROKE_CONTROL, |
| 70 | 76 | VALUE_STROKE_PURE, |
| 71 | KEY_FRACTIONALMETRICS, | |
| 72 | VALUE_FRACTIONALMETRICS_ON, | |
| 73 | 77 | KEY_TEXT_ANTIALIASING, |
| 74 | VALUE_TEXT_ANTIALIAS_OFF | |
| 78 | VALUE_TEXT_ANTIALIAS_ON | |
| 75 | 79 | ); |
| 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 | } | |
| 76 | 101 | |
| 77 | 102 | private static class BufferedImageTranscoder extends ImageTranscoder { |
| ... | ||
| 119 | 144 | return rasterize( new URL( url ), width ); |
| 120 | 145 | } catch( final Exception e ) { |
| 121 | return createPlaceholderImage( width ); | |
| 146 | NOTIFIER.notify( e ); | |
| 147 | return PLACEHOLDER_IMAGE; | |
| 122 | 148 | } |
| 123 | 149 | } |
| ... | ||
| 151 | 177 | |
| 152 | 178 | 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; | |
| 165 | 179 | } |
| 166 | 180 | } |
| 64 | 64 | |
| 65 | 65 | /** |
| 66 | * Where to put document inline evaluated R expressions. | |
| 66 | * Where to put cached image files. | |
| 67 | 67 | */ |
| 68 | 68 | private final Map<String, Image> mImageCache = new LinkedHashMap<>() { |
| 69 | 69 | @Override |
| 70 | 70 | protected boolean removeEldestEntry( |
| 71 | 71 | final Map.Entry<String, Image> eldest ) { |
| 72 | 72 | return size() > MAX_CACHED_IMAGES; |
| 73 | 73 | } |
| 74 | 74 | }; |
| 75 | 75 | |
| 76 | @Override | |
| 76 | 77 | public ReplacedElement createReplacedElement( |
| 77 | 78 | final LayoutContext c, |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 34 | 34 | * sub-chains. |
| 35 | 35 | * |
| 36 | * @author White Magic Software, Ltd. | |
| 37 | 36 | * @param <T> The type of object to process. |
| 38 | 37 | */ |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 36 | 36 | * their values into the post-processed text. The default variable syntax is |
| 37 | 37 | * {@code $variable$}. |
| 38 | * | |
| 39 | * @author White Magic Software, Ltd. | |
| 40 | 38 | */ |
| 41 | 39 | public class DefinitionProcessor extends AbstractProcessor<String> { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 36 | 36 | * final HTML preview is rendered. This should be the last link in the processor |
| 37 | 37 | * chain. |
| 38 | * | |
| 39 | * @author White Magic Software, Ltd. | |
| 40 | 38 | */ |
| 41 | 39 | public class HTMLPreviewProcessor extends AbstractProcessor<String> { |
| 31 | 31 | * This is the default processor used when an unknown filename extension is |
| 32 | 32 | * encountered. |
| 33 | * | |
| 34 | * @author White Magic Software, Ltd. | |
| 35 | 33 | */ |
| 36 | 34 | public class IdentityProcessor extends AbstractProcessor<String> { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 49 | 49 | /** |
| 50 | 50 | * Transforms a document containing R statements into Markdown. |
| 51 | * | |
| 52 | * @author White Magic Software, Ltd. | |
| 53 | 51 | */ |
| 54 | 52 | public final class InlineRProcessor extends DefinitionProcessor { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 32 | 32 | * |
| 33 | 33 | * @param <T> The type of processor to create. |
| 34 | * @author White Magic Software, Ltd. | |
| 35 | 34 | */ |
| 36 | 35 | public interface Processor<T> { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 39 | 39 | * Responsible for creating processors capable of parsing, transforming, |
| 40 | 40 | * interpolating, and rendering known file types. |
| 41 | * | |
| 42 | * @author White Magic Software, Ltd. | |
| 43 | 41 | */ |
| 44 | 42 | public class ProcessorFactory extends AbstractFileFactory { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 35 | 35 | * performs a substitution on the text. The default R variable syntax is |
| 36 | 36 | * {@code v$tree$leaf}. |
| 37 | * | |
| 38 | * @author White Magic Software, Ltd. | |
| 39 | 37 | */ |
| 40 | 38 | public class RVariableProcessor extends DefinitionProcessor { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 58 | 58 | * The XSL must transform the XML document into Markdown, or another format |
| 59 | 59 | * recognized by the next link on the chain. |
| 60 | * | |
| 61 | * @author White Magic Software, Ltd. | |
| 62 | 60 | */ |
| 63 | 61 | public class XMLProcessor extends AbstractProcessor<String> |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 53 | 53 | * Responsible for ensuring that images can be rendered relative to a path. |
| 54 | 54 | * This allows images to be located virtually anywhere. |
| 55 | * | |
| 56 | * @author White Magic Software, Ltd. | |
| 57 | 55 | */ |
| 58 | 56 | public class ImageLinkExtension implements HtmlRenderer.HtmlRendererExtension { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 49 | 49 | /** |
| 50 | 50 | * Responsible for parsing a Markdown document and rendering it as HTML. |
| 51 | * | |
| 52 | * @author White Magic Software, Ltd. | |
| 53 | 51 | */ |
| 54 | 52 | public class MarkdownProcessor extends AbstractProcessor<String> { |
| 1 | 1 | /* |
| 2 | * The MIT License | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | * Copyright 2016 . | |
| 4 | * All rights reserved. | |
| 5 | 5 | * |
| 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: | |
| 12 | 8 | * |
| 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. | |
| 15 | 11 | * |
| 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. | |
| 23 | 27 | */ |
| 24 | 28 | package com.scrivenvar.processors.text; |
| 25 | 29 | |
| 26 | 30 | import java.util.Map; |
| 27 | 31 | |
| 28 | 32 | /** |
| 29 | 33 | * Responsible for common behaviour across all text replacer implementations. |
| 30 | * | |
| 31 | * @author White Magic Software, Ltd. | |
| 32 | 34 | */ |
| 33 | 35 | public abstract class AbstractTextReplacer implements TextReplacer { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 35 | 35 | /** |
| 36 | 36 | * Replaces text using an Aho-Corasick algorithm. |
| 37 | * | |
| 38 | * @author White Magic Software, Ltd. | |
| 39 | 37 | */ |
| 40 | 38 | public class AhoCorasickReplacer extends AbstractTextReplacer { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 34 | 34 | /** |
| 35 | 35 | * Replaces text using Apache's StringUtils.replaceEach method. |
| 36 | * | |
| 37 | * @author White Magic Software, Ltd. | |
| 38 | 36 | */ |
| 39 | 37 | public class StringUtilsReplacer extends AbstractTextReplacer { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 33 | 33 | * Used to generate a class capable of efficiently replacing variable |
| 34 | 34 | * definitions with their values. |
| 35 | * | |
| 36 | * @author White Magic Software, Ltd. | |
| 37 | 35 | */ |
| 38 | 36 | public final class TextReplacementFactory { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 32 | 32 | /** |
| 33 | 33 | * Defines the ability to replace text given a set of keys and values. |
| 34 | * | |
| 35 | * @author White Magic Software, Ltd. | |
| 36 | 34 | */ |
| 37 | 35 | public interface TextReplacer { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 35 | 35 | /** |
| 36 | 36 | * Responsible for persisting options. |
| 37 | * | |
| 38 | * @author White Magic Software, Ltd. | |
| 39 | 37 | */ |
| 40 | 38 | public interface Options extends Service { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 30 | 30 | /** |
| 31 | 31 | * All services inherit from this one. |
| 32 | * | |
| 33 | * @author White Magic Software, Ltd. | |
| 34 | 32 | */ |
| 35 | 33 | public interface Service { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 33 | 33 | /** |
| 34 | 34 | * Defines how settings and options can be retrieved. |
| 35 | * | |
| 36 | * @author White Magic Software, Ltd. | |
| 37 | 35 | */ |
| 38 | 36 | public interface Settings extends Service { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 34 | 34 | /** |
| 35 | 35 | * Listens for changes to file system files and directories. |
| 36 | * | |
| 37 | * @author White Magic Software, Ltd. | |
| 38 | 36 | */ |
| 39 | 37 | public interface Snitch extends Service, Runnable { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 30 | 30 | /** |
| 31 | 31 | * Represents a message that contains a title and content. |
| 32 | * | |
| 33 | * @author White Magic Software, Ltd. | |
| 34 | 32 | */ |
| 35 | 33 | public interface Notification { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 40 | 40 | /** |
| 41 | 41 | * Provides the application with a uniform way to notify the user of events. |
| 42 | * | |
| 43 | * @author White Magic Software, Ltd. | |
| 44 | 42 | */ |
| 45 | 43 | public interface Notifier { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 38 | 38 | * Ensures a consistent button order for alert dialogs across platforms (because |
| 39 | 39 | * the default button order on Linux defies all logic). |
| 40 | * | |
| 41 | * @author White Magic Software, Ltd. | |
| 42 | 40 | */ |
| 43 | 41 | public class ButtonOrderPane extends DialogPane { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 33 | 33 | |
| 34 | 34 | /** |
| 35 | * @author White Magic Software, Ltd. | |
| 35 | * Responsible for alerting the user to prominent information. | |
| 36 | 36 | */ |
| 37 | 37 | public class DefaultNotification implements Notification { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 46 | 46 | /** |
| 47 | 47 | * Provides the ability to notify the user of problems. |
| 48 | * | |
| 49 | * @author White Magic Software, Ltd. | |
| 50 | 48 | */ |
| 51 | 49 | public final class DefaultNotifier extends Observable implements Notifier { |
| 1 | 1 | /* |
| 2 | * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com> | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | 3 | * All rights reserved. |
| 4 | 4 | * |
| ... | ||
| 39 | 39 | /** |
| 40 | 40 | * Persistent options user can change at runtime. |
| 41 | * | |
| 42 | * @author Karl Tauber and White Magic Software, Ltd. | |
| 43 | 41 | */ |
| 44 | 42 | public class DefaultOptions implements Options { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 46 | 46 | /** |
| 47 | 47 | * Responsible for loading settings that help avoid hard-coded assumptions. |
| 48 | * | |
| 49 | * @author White Magic Software, Ltd. | |
| 50 | 48 | */ |
| 51 | 49 | public class DefaultSettings implements Settings { |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 44 | 44 | * Listens for file changes. Other classes can register paths to be monitored |
| 45 | 45 | * and listen for changes to those paths. |
| 46 | * | |
| 47 | * @author White Magic Software, Ltd. | |
| 48 | 46 | */ |
| 49 | 47 | public class DefaultSnitch extends Observable implements Snitch { |
| 1 | 1 | /* |
| 2 | * Copyright (c) 2015 Karl Tauber and White Magic Software, Ltd. | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | 3 | * All rights reserved. |
| 4 | 4 | * |
| ... | ||
| 35 | 35 | /** |
| 36 | 36 | * Simple action class |
| 37 | * | |
| 38 | * @author Karl Tauber | |
| 39 | * @author White Magic Software, Ltd. | |
| 40 | 37 | */ |
| 41 | 38 | public class Action { |
| 1 | 1 | /* |
| 2 | * Copyright (c) 2015 Karl Tauber | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | 3 | * All rights reserved. |
| 4 | 4 | * |
| ... | ||
| 38 | 38 | |
| 39 | 39 | /** |
| 40 | * Action utilities | |
| 41 | * | |
| 42 | * @author Karl Tauber | |
| 43 | * @author White Magic Software, Ltd. | |
| 40 | * Responsible for creating menu items and toolbar buttons. | |
| 44 | 41 | */ |
| 45 | 42 | public class ActionUtils { |
| ... | ||
| 53 | 50 | |
| 54 | 51 | 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 ] ); | |
| 58 | 55 | } |
| 59 | 56 | |
| 1 | 1 | /* |
| 2 | * Copyright 2016 White Magic Software, Ltd. | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | 3 | * |
| 4 | 4 | * All rights reserved. |
| ... | ||
| 32 | 32 | /** |
| 33 | 33 | * Convenience class that provides a clearer API for obtaining list elements. |
| 34 | * | |
| 35 | * @author White Magic Software, Ltd. | |
| 36 | 34 | */ |
| 37 | 35 | public final class Lists { |
| 1 | 1 | /* |
| 2 | * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com> | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | 3 | * All rights reserved. |
| 4 | 4 | * |
| ... | ||
| 36 | 36 | /** |
| 37 | 37 | * Saves and restores Stage state (window bounds, maximized, fullScreen). |
| 38 | * | |
| 39 | * @author Karl Tauber | |
| 40 | 38 | */ |
| 41 | 39 | public class StageState { |
| 1 | 1 | /* |
| 2 | * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com> | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | 3 | * All rights reserved. |
| 4 | 4 | * |
| ... | ||
| 31 | 31 | |
| 32 | 32 | /** |
| 33 | * @author Karl Tauber and White Magic Software, Ltd. | |
| 33 | * Responsible for trimming, storing, and retrieving strings. | |
| 34 | 34 | */ |
| 35 | 35 | public class Utils { |
| 168 | 168 | } |
| 169 | 169 | |
| 170 | dl dd { | |
| 171 | *float: none; | |
| 172 | *width: auto; | |
| 173 | *margin-left: 20%; | |
| 174 | } | |
| 175 | ||
| 176 | 170 | /* CODE |
| 177 | 171 | =============================================================================*/ |
| ... | ||
| 199 | 193 | border: .125em solid #ccc; |
| 200 | 194 | line-height: 1.6; |
| 201 | overflow: auto; | |
| 202 | 195 | padding: .25em .5em; |
| 203 | 196 | border-radius: .25em; |
| ... | ||
| 211 | 204 | kbd { |
| 212 | 205 | background-color: #ccc; |
| 213 | background-image: linear-gradient(#f8f8f8, #DDDDDD); | |
| 214 | 206 | background-repeat: repeat-x; |
| 215 | 207 | border-color: #DDDDDD #CCCCCC #CCCCCC #DDDDDD; |
| 216 | border-image: none; | |
| 217 | 208 | border-radius: 2px; |
| 218 | 209 | border-style: solid; |