Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git

Automatic reloading of external XSL template file.

Authordjarvis <email>
Date2016-12-19 10:28:25 GMT-0800
Commitee49231269f3453ad41ca987d126267e18333ae7
Parent0f9e56c
Delta869 lines added, 492 lines removed, 377-line increase
src/main/resources/com/scrivenvar/settings.properties
application.messages= com.${application.title}.messages
+# Suppress multiple file modified notifications for one logical modification.
+# Given in milliseconds.
+application.watchdog.timeout=50
+
# ########################################################################
#
file.stylesheet.scene=${application.package}/scene.css
-file.stylesheet.markdown=${application.package}/editor/Markdown.css
+file.stylesheet.markdown=${application.package}/editor/markdown.css
file.stylesheet.preview=webview.css
caret.token.base=CARETPOSITION
caret.token.markdown=%${constant.caret.token.base}%
-caret.token.xml=<![CDATA[${constant.caret.token.markdown}]]>
caret.token.html=<span id="${caret.token.base}"></span>
# ########################################################################
#
# Filename Extensions
#
# ########################################################################
# Comma-separated list of definition filename extensions.
-file.ext.definition.json=*.json
-file.ext.definition.toml=*.toml
-file.ext.definition.yaml=*.yml,*.yaml
-file.ext.definition.properties=*.properties,*.props
+definition.file.ext.json=*.json
+definition.file.ext.toml=*.toml
+definition.file.ext.yaml=*.yml,*.yaml
+definition.file.ext.properties=*.properties,*.props
# Comma-separated list of filename extensions.
-filter.file.ext.markdown=*.Rmd,*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt
-filter.file.ext.definition=${file.ext.definition.yaml}
-filter.file.ext.xml=*.xml,*.Rxml
-filter.file.ext.all=*.*
+file.ext.rmarkdown=*.Rmd
+file.ext.rxml=*.Rxml
+file.ext.markdown=*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt,${file.ext.rmarkdown}
+file.ext.definition=${definition.file.ext.yaml}
+file.ext.xml=*.xml,${file.ext.rxml}
+file.ext.all=*.*
# ########################################################################
src/main/resources/com/scrivenvar/editor/markdown.css
+/*
+ * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.markdown-editor {
+ -fx-font-size: 14px;
+}
+
+/*---- headers ----*/
+
+.markdown-editor .h1 { -fx-font-size: 2.25em; }
+.markdown-editor .h2 { -fx-font-size: 1.75em; }
+.markdown-editor .h3 { -fx-font-size: 1.5em; }
+.markdown-editor .h4 { -fx-font-size: 1.25em; }
+.markdown-editor .h5 { -fx-font-size: 1.1em; }
+.markdown-editor .h6 { -fx-font-size: 1em; }
+
+.markdown-editor .h1,
+.markdown-editor .h2,
+.markdown-editor .h3,
+.markdown-editor .h4,
+.markdown-editor .h5,
+.markdown-editor .h6 {
+ -fx-font-weight: bold;
+ -fx-fill: derive(crimson, -20%);
+}
+
+
+/*---- inlines ----*/
+
+.markdown-editor .strong {
+ -fx-font-weight: bold;
+}
+
+.markdown-editor .em {
+ -fx-font-style: italic;
+}
+
+.markdown-editor .del {
+ -fx-strikethrough: true;
+}
+
+.markdown-editor .a {
+ -fx-fill: #4183C4 !important;
+}
+
+.markdown-editor .img {
+ -fx-fill: #4183C4 !important;
+}
+
+.markdown-editor .code {
+ -fx-font-family: monospace;
+ -fx-fill: #090 !important;
+}
+
+
+/*---- blocks ----*/
+
+.markdown-editor .pre {
+ -fx-font-family: monospace;
+ -fx-fill: #060 !important;
+}
+
+.markdown-editor .blockquote {
+ -fx-fill: #777;
+}
+
+
+/*---- lists ----*/
+
+.markdown-editor .ul {
+}
+
+.markdown-editor .ol {
+}
+
+.markdown-editor .li {
+ -fx-fill: #444;
+}
+
+.markdown-editor .dl {
+}
+
+.markdown-editor .dt {
+ -fx-font-weight: bold;
+ -fx-font-style: italic;
+}
+
+.markdown-editor .dd {
+ -fx-fill: #444;
+}
+
+
+/*---- table ----*/
+
+.markdown-editor .table {
+ -fx-font-family: monospace;
+}
+
+.markdown-editor .thead {
+}
+
+.markdown-editor .tbody {
+}
+
+.markdown-editor .caption {
+}
+
+.markdown-editor .th {
+ -fx-font-weight: bold;
+}
+
+.markdown-editor .tr {
+}
+
+.markdown-editor .td {
+}
+
+
+/*---- misc ----*/
+
+.markdown-editor .html {
+ -fx-font-family: monospace;
+ -fx-fill: derive(crimson, -50%);
+}
+.markdown-editor .monospace {
+ -fx-font-family: monospace;
+}
src/main/java/com/scrivenvar/processors/ProcessorFactory.java
+/*
+ * Copyright 2016 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.scrivenvar.processors;
+
+import com.scrivenvar.AbstractFileFactory;
+import com.scrivenvar.Constants;
+import com.scrivenvar.FileEditorTab;
+import com.scrivenvar.FileType;
+import com.scrivenvar.preview.HTMLPreviewPane;
+import java.nio.file.Path;
+import java.util.Map;
+
+/**
+ * Responsible for creating processors capable of parsing, transforming,
+ * interpolating, and rendering known file types.
+ *
+ * @author White Magic Software, Ltd.
+ */
+public class ProcessorFactory extends AbstractFileFactory {
+
+ private final HTMLPreviewPane previewPane;
+ private final Map<String, String> resolvedMap;
+
+ private Processor<String> terminalProcessChain;
+
+ /**
+ * Constructs a factory with the ability to create processors that can perform
+ * text and caret processing to generate a final preview.
+ *
+ * @param previewPane
+ * @param resolvedMap
+ */
+ public ProcessorFactory(
+ final HTMLPreviewPane previewPane,
+ final Map<String, String> resolvedMap ) {
+ this.previewPane = previewPane;
+ this.resolvedMap = resolvedMap;
+ }
+
+ /**
+ * Creates a processor suitable for parsing and rendering the file opened at
+ * the given tab.
+ *
+ * @param tab The tab containing a text editor, path, and caret position.
+ *
+ * @return A processor that can render the given tab's text.
+ */
+ public Processor<String> createProcessor( final FileEditorTab tab ) {
+ final Path path = tab.getPath();
+ final FileType fileType = lookup( path, Constants.GLOB_PREFIX_FILE );
+ Processor<String> processor = null;
+
+ switch( fileType ) {
+ case RMARKDOWN:
+ processor = createRMarkdownProcessor( tab );
+ break;
+
+ case MARKDOWN:
+ processor = createMarkdownProcessor( tab );
+ break;
+
+ case XML:
+ processor = createXMLProcessor( tab );
+ break;
+
+ default:
+ unknownExtension( path );
+ break;
+ }
+
+ return processor;
+ }
+
+ /**
+ * Returns a processor common to all processors: markdown, caret position
+ * token replacer, and an HTML preview renderer.
+ *
+ * @return Processors at the end of the processing chain.
+ */
+ private Processor<String> getTerminalProcessChain() {
+ if( this.terminalProcessChain == null ) {
+ this.terminalProcessChain = createTerminalProcessChain();
+ }
+
+ return this.terminalProcessChain;
+ }
+
+ private Processor<String> createTerminalProcessChain() {
+ final Processor<String> hpp = new HTMLPreviewProcessor( getPreviewPane() );
+ final Processor<String> mcrp = new CaretReplacementProcessor( hpp );
+ final Processor<String> mpp = new MarkdownProcessor( mcrp );
+
+ return mpp;
+ }
+
+ protected Processor<String> createMarkdownProcessor( final FileEditorTab tab ) {
+ final Processor<String> bp = getTerminalProcessChain();
+ final Processor<String> xcip = new MarkdownCaretInsertionProcessor( bp, tab.caretPositionProperty() );
+ final Processor<String> vp = new VariableProcessor( xcip, getResolvedMap() );
+
+ return vp;
+ }
+
+ protected Processor<String> createRMarkdownProcessor( final FileEditorTab tab ) {
+ return createMarkdownProcessor( tab );
+ }
+
+ protected Processor<String> createXMLProcessor( final FileEditorTab tab ) {
+ final Processor<String> bp = getTerminalProcessChain();
+ final Processor<String> xmlp = new XMLProcessor( bp, tab.getPath() );
+ final Processor<String> xcip = new XMLCaretInsertionProcessor( xmlp, tab.caretPositionProperty() );
+ final Processor<String> vp = new VariableProcessor( xcip, getResolvedMap() );
+
+ return vp;
+ }
+
+ private HTMLPreviewPane getPreviewPane() {
+ return this.previewPane;
+ }
+
+ /**
+ * Returns the variable map of interpolated definitions.
+ *
+ * @return A map to help dereference variables.
+ */
+ private Map<String, String> getResolvedMap() {
+ return this.resolvedMap;
+ }
+}
src/main/java/com/scrivenvar/processors/XMLCaretInsertionProcessor.java
package com.scrivenvar.processors;
-import com.scrivenvar.FileEditorTab;
import com.ximpleware.VTDException;
import com.ximpleware.VTDGen;
import static com.ximpleware.VTDGen.TOKEN_CHARACTER_DATA;
import com.ximpleware.VTDNav;
import java.text.ParseException;
+import javafx.beans.value.ObservableValue;
/**
* Inserts a caret position indicator into the document.
*
* @author White Magic Software, Ltd.
*/
public class XMLCaretInsertionProcessor extends CaretInsertionProcessor {
- private FileEditorTab tab;
+ private VTDGen parser;
/**
* Constructs a processor capable of inserting a caret marker into XML.
*
* @param processor The next processor in the chain.
* @param position The caret's current position in the text, cannot be null.
*/
public XMLCaretInsertionProcessor(
- final Processor<String> processor, final int position ) {
+ final Processor<String> processor,
+ final ObservableValue<Integer> position ) {
super( processor, position );
}
/**
* Inserts a caret at a valid position within the XML document.
*
- * @param t The string into which caret position marker text is inserted.
+ * @param text The string into which caret position marker text is inserted.
*
- * @return t with a caret position marker included, or t if no place to insert
- * could be found.
+ * @return The text with a caret position marker included, or the original
+ * text if no insertion point could be found.
*/
@Override
- public String processLink( final String t ) {
+ public String processLink( final String text ) {
final int caret = getCaretPosition();
int insertOffset = -1;
-
- if( t.length() > 0 ) {
+ if( text.length() > 0 ) {
try {
- final VTDNav vn = getNavigator( t );
+ final VTDNav vn = getNavigator( text );
final int tokens = vn.getTokenCount();
final int prevLength = vn.getTokenLength( prevTokenIndex );
- // If the caret falls within the limits of the previous token, then
- // insert the caret position marker at the caret offset.
+ // If the caret falls within the limits of the previous token,
+ // theninsert the caret position marker at the caret offset.
if( isBetween( caret, prevOffset, prevOffset + prevLength ) ) {
insertOffset = caret;
insertOffset = currOffset;
}
-
+
break;
}
}
- return inject( t, insertOffset );
+ return inject( text, insertOffset );
}
}
- private VTDGen getParser() {
+ private synchronized VTDGen getParser() {
+ if( this.parser == null ) {
+ this.parser = createParser();
+ }
+
+ return this.parser;
+ }
+
+ /**
+ * Creates a high-performance XML document parser.
+ *
+ * @return A new XML parser.
+ */
+ protected VTDGen createParser() {
return new VTDGen();
}
src/main/java/com/scrivenvar/processors/XMLProcessor.java
import com.scrivenvar.service.Snitch;
import java.io.File;
+import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
*/
public class XMLProcessor extends AbstractProcessor<String> {
-
+
private final Snitch snitch = Services.load( Snitch.class );
-
+
private XMLInputFactory xmlInputFactory;
private TransformerFactory transformerFactory;
-
+ private Transformer transformer;
+
private Path path;
final String template = getXsltFilename( text );
final Path xsl = getXslPath( template );
-
- // Listen for external file modification events.
- getWatchDog().listen( xsl );
try(
final StringWriter output = new StringWriter( text.length() );
final StringReader input = new StringReader( text ) ) {
-
+
+ // Listen for external file modification events.
+ getSnitch().listen( xsl );
+
getTransformer( xsl ).transform(
new StreamSource( input ),
new StreamResult( output )
);
-
+
return output.toString();
}
}
/**
* Returns an XSL transformer ready to transform an XML document using the
* XSLT file specified by the given path. If the path is already known then
* this will return the associated transformer.
*
- * @param path The path to an XSLT file.
+ * @param xsl The path to an XSLT file.
*
* @return A transformer that will transform XML documents using the given
* XSLT file.
*
* @throws TransformerConfigurationException Could not instantiate the
* transformer.
*/
- private Transformer getTransformer( final Path path )
+ private Transformer getTransformer( final Path xsl )
+ throws TransformerConfigurationException, IOException {
+ if( this.transformer == null ) {
+ this.transformer = createTransformer( xsl );
+ }
+
+ return this.transformer;
+ }
+
+ protected Transformer createTransformer( final Path xsl )
throws TransformerConfigurationException {
-
- final TransformerFactory factory = getTransformerFactory();
- final Source xslt = new StreamSource( path.toFile() );
- return factory.newTransformer( xslt );
+ final Source xslt = new StreamSource( xsl.toFile() );
+ return getTransformerFactory().newTransformer( xslt );
}
-
+
private Path getXslPath( final String filename ) {
final Path xmlPath = getPath();
final File xmlDirectory = xmlPath.toFile().getParentFile();
-
+
return Paths.get( xmlDirectory.getPath(), filename );
}
private String getXsltFilename( final String xml )
throws XMLStreamException, ParseException {
-
+
String result = "";
-
+
try( final StringReader sr = new StringReader( xml ) ) {
boolean found = false;
int count = 0;
final XMLEventReader reader = createXMLEventReader( sr );
// If the processing instruction wasn't found in the first 10 lines,
// fail fast. This should iterate twice through the loop.
while( !found && reader.hasNext() && count++ < 10 ) {
final XMLEvent event = reader.nextEvent();
-
+
if( event.isProcessingInstruction() ) {
final ProcessingInstruction pi = (ProcessingInstruction)event;
final String target = pi.getTarget();
-
+
if( "xml-stylesheet".equalsIgnoreCase( target ) ) {
result = getPseudoAttribute( pi.getData(), "href" );
found = true;
}
}
}
-
+
sr.close();
}
-
+
return result;
}
-
+
private XMLEventReader createXMLEventReader( final Reader reader )
throws XMLStreamException {
return getXMLInputFactory().createXMLEventReader( reader );
}
-
+
private synchronized XMLInputFactory getXMLInputFactory() {
if( this.xmlInputFactory == null ) {
this.xmlInputFactory = createXMLInputFactory();
}
-
+
return this.xmlInputFactory;
}
-
+
private XMLInputFactory createXMLInputFactory() {
return XMLInputFactory.newInstance();
}
-
+
private synchronized TransformerFactory getTransformerFactory() {
if( this.transformerFactory == null ) {
this.transformerFactory = createTransformerFactory();
}
-
+
return this.transformerFactory;
}
return new TransformerFactoryImpl();
}
-
+
private void setPath( final Path path ) {
this.path = path;
}
-
+
private Path getPath() {
return this.path;
}
-
- private Snitch getWatchDog() {
+
+ private Snitch getSnitch() {
return this.snitch;
}
src/main/java/com/scrivenvar/MainWindow.java
package com.scrivenvar;
-import static com.scrivenvar.Constants.FILE_LOGO_32;
-import static com.scrivenvar.Constants.PREFS_DEFINITION_SOURCE;
-import static com.scrivenvar.Constants.STYLESHEET_SCENE;
-import static com.scrivenvar.Messages.get;
-import com.scrivenvar.definition.DefinitionFactory;
-import com.scrivenvar.definition.DefinitionPane;
-import com.scrivenvar.definition.DefinitionSource;
-import com.scrivenvar.definition.EmptyDefinitionSource;
-import com.scrivenvar.editors.EditorPane;
-import com.scrivenvar.editors.VariableNameInjector;
-import com.scrivenvar.editors.markdown.MarkdownEditorPane;
-import com.scrivenvar.preview.HTMLPreviewPane;
-import com.scrivenvar.processors.CaretReplacementProcessor;
-import com.scrivenvar.processors.HTMLPreviewProcessor;
-import com.scrivenvar.processors.MarkdownProcessor;
-import com.scrivenvar.processors.Processor;
-import com.scrivenvar.processors.VariableProcessor;
-import com.scrivenvar.processors.XMLCaretInsertionProcessor;
-import com.scrivenvar.processors.XMLProcessor;
-import com.scrivenvar.service.Options;
-import com.scrivenvar.util.Action;
-import com.scrivenvar.util.ActionUtils;
-import static com.scrivenvar.util.StageState.K_PANE_SPLIT_DEFINITION;
-import static com.scrivenvar.util.StageState.K_PANE_SPLIT_EDITOR;
-import static com.scrivenvar.util.StageState.K_PANE_SPLIT_PREVIEW;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.BOLD;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.CODE;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_ALT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_CODE_ALT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FLOPPY_ALT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FOLDER_OPEN_ALT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.HEADER;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.ITALIC;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LINK;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_OL;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_UL;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.PICTURE_ALT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.QUOTE_LEFT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.REPEAT;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.STRIKETHROUGH;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.UNDO;
-import java.net.MalformedURLException;
-import java.nio.file.Path;
-import java.util.Map;
-import java.util.function.Function;
-import java.util.prefs.Preferences;
-import javafx.beans.binding.Bindings;
-import javafx.beans.binding.BooleanBinding;
-import javafx.beans.property.BooleanProperty;
-import javafx.beans.property.SimpleBooleanProperty;
-import javafx.beans.value.ObservableBooleanValue;
-import javafx.beans.value.ObservableValue;
-import javafx.collections.ListChangeListener.Change;
-import javafx.collections.ObservableList;
-import static javafx.event.Event.fireEvent;
-import javafx.scene.Node;
-import javafx.scene.Scene;
-import javafx.scene.control.Alert;
-import javafx.scene.control.Alert.AlertType;
-import javafx.scene.control.Menu;
-import javafx.scene.control.MenuBar;
-import javafx.scene.control.SplitPane;
-import javafx.scene.control.Tab;
-import javafx.scene.control.ToolBar;
-import javafx.scene.control.TreeView;
-import javafx.scene.image.Image;
-import javafx.scene.image.ImageView;
-import static javafx.scene.input.KeyCode.ESCAPE;
-import javafx.scene.input.KeyEvent;
-import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
-import static javafx.scene.input.KeyEvent.KEY_PRESSED;
-import javafx.scene.layout.BorderPane;
-import javafx.scene.layout.VBox;
-import javafx.stage.Window;
-import javafx.stage.WindowEvent;
-import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
-
-/**
- * Main window containing a tab pane in the center for file editors.
- *
- * @author Karl Tauber and White Magic Software, Ltd.
- */
-public class MainWindow {
-
- private final Options options = Services.load( Options.class );
-
- private Scene scene;
- private MenuBar menuBar;
-
- private DefinitionPane definitionPane;
- private FileEditorTabPane fileEditorPane;
- private HTMLPreviewPane previewPane;
-
- private DefinitionSource definitionSource;
-
- public MainWindow() {
- initLayout();
- initOpenDefinitionListener();
- initTabAddedListener();
- initTabChangedListener();
- initPreferences();
- }
-
- /**
- * Listen for file editor tab pane to receive an open definition source event.
- */
- private void initOpenDefinitionListener() {
- getFileEditorPane().onOpenDefinitionFileProperty().addListener(
- (ObservableValue<? extends Path> definitionFile,
- final Path oldPath, final Path newPath) -> {
- openDefinition( newPath );
- refreshSelectedTab( getActiveFileEditor() );
- } );
- }
-
- /**
- * When tabs are added, hook the various change listeners onto the new tab so
- * that the preview pane refreshes as necessary.
- */
- private void initTabAddedListener() {
- final FileEditorTabPane editorPane = getFileEditorPane();
-
- // Make sure the text processor kicks off when new files are opened.
- final ObservableList<Tab> tabs = editorPane.getTabs();
-
- // Update the preview pane on tab changes.
- tabs.addListener(
- (final Change<? extends Tab> change) -> {
- while( change.next() ) {
- if( change.wasAdded() ) {
- // Multiple tabs can be added simultaneously.
- for( final Tab newTab : change.getAddedSubList() ) {
- final FileEditorTab tab = (FileEditorTab)newTab;
-
- initTextChangeListener( tab );
- initCaretParagraphListener( tab );
- initVariableNameInjector( tab );
- }
- }
- }
- }
- );
- }
-
- /**
- * Reloads the preferences from the previous load.
- */
- private void initPreferences() {
- getFileEditorPane().restorePreferences();
- restoreDefinitionSource();
- }
-
- /**
- * Listen for new tab selection events.
- */
- private void initTabChangedListener() {
- final FileEditorTabPane editorPane = getFileEditorPane();
-
- // Update the preview pane changing tabs.
- editorPane.addTabSelectionListener(
- (ObservableValue<? extends Tab> tabPane,
- final Tab oldTab, final Tab newTab) -> {
-
- // If there was no old tab, then this is a first time load, which
- // can be ignored.
- if( oldTab != null ) {
- if( newTab == null ) {
- closeRemainingTab();
- } else {
- // Update the preview with the edited text.
- refreshSelectedTab( (FileEditorTab)newTab );
- }
- }
- }
- );
- }
-
- private void initTextChangeListener( final FileEditorTab tab ) {
- tab.addTextChangeListener(
- (ObservableValue<? extends String> editor,
- final String oldValue, final String newValue) -> {
- refreshSelectedTab( tab );
- }
- );
- }
-
- private void initCaretParagraphListener( final FileEditorTab tab ) {
- tab.addCaretParagraphListener(
- (ObservableValue<? extends Integer> editor,
- final Integer oldValue, final Integer newValue) -> {
- refreshSelectedTab( tab );
- }
- );
- }
-
- private void initVariableNameInjector( final FileEditorTab tab ) {
- VariableNameInjector.listen( tab, getDefinitionPane() );
- }
-
- /**
- * Called whenever the preview pane becomes out of sync with the file editor
- * tab. This can be called when the text changes, the caret paragraph changes,
- * or the file tab changes.
- *
- * @param tab The file editor tab that has been changed in some fashion.
- */
- private void refreshSelectedTab( final FileEditorTab tab ) {
- final Path path = tab.getPath();
-
- final HTMLPreviewPane preview = getPreviewPane();
- preview.setPath( tab.getPath() );
-
- final Processor<String> hpp = new HTMLPreviewProcessor( preview );
- final Processor<String> mcrp = new CaretReplacementProcessor( hpp );
- final Processor<String> mp = new MarkdownProcessor( mcrp );
-// final Processor<String> mcip = new MarkdownCaretInsertionProcessor( mp, tab.getCaretPosition() );
- final Processor<String> xmlp = new XMLProcessor( mp, tab.getPath() );
- final Processor<String> xcip = new XMLCaretInsertionProcessor( xmlp, tab.getCaretPosition() );
- final Processor<String> vp = new VariableProcessor( xcip, getResolvedMap() );
-
- vp.processChain( tab.getEditorText() );
- }
-
- /**
- * Returns the variable map of interpolated definitions.
- *
- * @return A map to help dereference variables.
- */
- private Map<String, String> getResolvedMap() {
- return getDefinitionSource().getResolvedMap();
- }
-
- /**
- * Returns the root node for the hierarchical definition source.
- *
- * @return Data to display in the definition pane.
- */
- private TreeView<String> getTreeView() {
- try {
- return getDefinitionSource().asTreeView();
- } catch( Exception e ) {
- alert( e );
- }
-
- return new TreeView<>();
- }
-
- private void openDefinition( final Path path ) {
- openDefinition( path.toString() );
- }
-
- private void openDefinition( final String path ) {
- try {
- final DefinitionSource ds = createDefinitionSource( path );
- setDefinitionSource( ds );
- storeDefinitionSource();
-
- getDefinitionPane().setRoot( ds.asTreeView() );
- } catch( Exception e ) {
- alert( e );
- }
- }
-
- private void restoreDefinitionSource() {
- final Preferences preferences = getPreferences();
- final String source = preferences.get( PREFS_DEFINITION_SOURCE, null );
-
- if( source != null ) {
- openDefinition( source );
- }
- }
-
- private void storeDefinitionSource() {
- final Preferences preferences = getPreferences();
- final DefinitionSource ds = getDefinitionSource();
-
- preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() );
- }
-
- /**
- * Called when the last open tab is closed. This clears out the preview pane
- * and the definition pane.
- */
- private void closeRemainingTab() {
- getPreviewPane().clear();
- getDefinitionPane().clear();
- }
-
- /**
- * Called when an exception occurs that warrants the user's attention.
- *
- * @param e The exception with a message that the user should know about.
- */
- private void alert( final Exception e ) {
- // TODO: Raise a notice.
- }
-
- //---- File actions -------------------------------------------------------
- private void fileNew() {
- getFileEditorPane().newEditor();
- }
-
- private void fileOpen() {
- getFileEditorPane().openFileDialog();
- }
-
- private void fileClose() {
- getFileEditorPane().closeEditor( getActiveFileEditor(), true );
- }
-
- private void fileCloseAll() {
- getFileEditorPane().closeAllEditors();
- }
-
- private void fileSave() {
- getFileEditorPane().saveEditor( getActiveFileEditor() );
- }
-
- private void fileSaveAll() {
- getFileEditorPane().saveAllEditors();
- }
-
- private void fileExit() {
- final Window window = getWindow();
- fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
- }
-
- //---- Help actions -------------------------------------------------------
- private void helpAbout() {
- Alert alert = new Alert( AlertType.INFORMATION );
- alert.setTitle( get( "Dialog.about.title" ) );
- alert.setHeaderText( get( "Dialog.about.header" ) );
- alert.setContentText( get( "Dialog.about.content" ) );
- alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
- alert.initOwner( getWindow() );
-
- alert.showAndWait();
- }
-
- //---- Convenience accessors ----------------------------------------------
- private float getFloat( final String key, final float defaultValue ) {
- return getPreferences().getFloat( key, defaultValue );
- }
-
- private Preferences getPreferences() {
- return getOptions().getState();
- }
-
- private Window getWindow() {
- return getScene().getWindow();
- }
-
- private MarkdownEditorPane getActiveEditor() {
- final EditorPane pane = getActiveFileEditor().getEditorPane();
-
- return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null;
- }
-
- private FileEditorTab getActiveFileEditor() {
- return getFileEditorPane().getActiveFileEditor();
- }
-
- //---- Member accessors ---------------------------------------------------
- public Scene getScene() {
- return this.scene;
- }
-
- private void setScene( Scene scene ) {
- this.scene = scene;
- }
-
- private FileEditorTabPane getFileEditorPane() {
- if( this.fileEditorPane == null ) {
- this.fileEditorPane = createFileEditorPane();
- }
-
- return this.fileEditorPane;
- }
-
- private HTMLPreviewPane getPreviewPane() {
- if( this.previewPane == null ) {
- this.previewPane = createPreviewPane();
- }
-
- return this.previewPane;
- }
-
- private void setDefinitionSource( final DefinitionSource definitionSource ) {
- this.definitionSource = definitionSource;
- }
-
- private DefinitionSource getDefinitionSource() {
- if( this.definitionSource == null ) {
- this.definitionSource = new EmptyDefinitionSource();
- }
-
- return this.definitionSource;
- }
-
- private DefinitionPane getDefinitionPane() {
- if( this.definitionPane == null ) {
- this.definitionPane = createDefinitionPane();
- }
-
- return this.definitionPane;
- }
-
- private Options getOptions() {
- return this.options;
- }
-
- public MenuBar getMenuBar() {
- return this.menuBar;
- }
-
- public void setMenuBar( MenuBar menuBar ) {
- this.menuBar = menuBar;
- }
-
- //---- Member creators ----------------------------------------------------
+import static com.scrivenvar.Constants.*;
+import static com.scrivenvar.Messages.get;
+import com.scrivenvar.definition.DefinitionFactory;
+import com.scrivenvar.definition.DefinitionPane;
+import com.scrivenvar.definition.DefinitionSource;
+import com.scrivenvar.definition.EmptyDefinitionSource;
+import com.scrivenvar.editors.EditorPane;
+import com.scrivenvar.editors.VariableNameInjector;
+import com.scrivenvar.editors.markdown.MarkdownEditorPane;
+import com.scrivenvar.preview.HTMLPreviewPane;
+import com.scrivenvar.processors.Processor;
+import com.scrivenvar.processors.ProcessorFactory;
+import com.scrivenvar.service.Options;
+import com.scrivenvar.service.Snitch;
+import com.scrivenvar.util.Action;
+import com.scrivenvar.util.ActionUtils;
+import static com.scrivenvar.util.StageState.*;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
+import java.net.MalformedURLException;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Observable;
+import java.util.Observer;
+import java.util.function.Function;
+import java.util.prefs.Preferences;
+import javafx.application.Platform;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.value.ObservableBooleanValue;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.ListChangeListener.Change;
+import javafx.collections.ObservableList;
+import static javafx.event.Event.fireEvent;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Alert.AlertType;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuBar;
+import javafx.scene.control.SplitPane;
+import javafx.scene.control.Tab;
+import javafx.scene.control.ToolBar;
+import javafx.scene.control.TreeView;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import static javafx.scene.input.KeyCode.ESCAPE;
+import javafx.scene.input.KeyEvent;
+import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
+import static javafx.scene.input.KeyEvent.KEY_PRESSED;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.VBox;
+import javafx.stage.Window;
+import javafx.stage.WindowEvent;
+import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
+
+/**
+ * Main window containing a tab pane in the center for file editors.
+ *
+ * @author Karl Tauber and White Magic Software, Ltd.
+ */
+public class MainWindow implements Observer {
+
+ private final Options options = Services.load( Options.class );
+ private final Snitch snitch = Services.load( Snitch.class );
+
+ /**
+ * Prevent re-instantiation of classes for processing.
+ */
+ private Map<FileEditorTab, Processor<String>> processors;
+ private ProcessorFactory processorFactory;
+
+ private Scene scene;
+ private MenuBar menuBar;
+
+ private DefinitionPane definitionPane;
+ private FileEditorTabPane fileEditorPane;
+ private HTMLPreviewPane previewPane;
+
+ private DefinitionSource definitionSource;
+
+ public MainWindow() {
+ initLayout();
+ initOpenDefinitionListener();
+ initTabAddedListener();
+ initTabChangedListener();
+ initPreferences();
+ initWatchDog();
+ }
+
+ /**
+ * Listen for file editor tab pane to receive an open definition source event.
+ */
+ private void initOpenDefinitionListener() {
+ getFileEditorPane().onOpenDefinitionFileProperty().addListener(
+ (ObservableValue<? extends Path> definitionFile,
+ final Path oldPath, final Path newPath) -> {
+ openDefinition( newPath );
+ refreshSelectedTab( getActiveFileEditor() );
+ } );
+ }
+
+ /**
+ * When tabs are added, hook the various change listeners onto the new tab so
+ * that the preview pane refreshes as necessary.
+ */
+ private void initTabAddedListener() {
+ final FileEditorTabPane editorPane = getFileEditorPane();
+
+ // Make sure the text processor kicks off when new files are opened.
+ final ObservableList<Tab> tabs = editorPane.getTabs();
+
+ // Update the preview pane on tab changes.
+ tabs.addListener(
+ (final Change<? extends Tab> change) -> {
+ while( change.next() ) {
+ if( change.wasAdded() ) {
+ // Multiple tabs can be added simultaneously.
+ for( final Tab newTab : change.getAddedSubList() ) {
+ final FileEditorTab tab = (FileEditorTab)newTab;
+
+ initTextChangeListener( tab );
+ initCaretParagraphListener( tab );
+ initVariableNameInjector( tab );
+ }
+ }
+ }
+ }
+ );
+ }
+
+ /**
+ * Reloads the preferences from the previous load.
+ */
+ private void initPreferences() {
+ getFileEditorPane().restorePreferences();
+ restoreDefinitionSource();
+ }
+
+ /**
+ * Listen for new tab selection events.
+ */
+ private void initTabChangedListener() {
+ final FileEditorTabPane editorPane = getFileEditorPane();
+
+ // Update the preview pane changing tabs.
+ editorPane.addTabSelectionListener(
+ (ObservableValue<? extends Tab> tabPane,
+ final Tab oldTab, final Tab newTab) -> {
+
+ // If there was no old tab, then this is a first time load, which
+ // can be ignored.
+ if( oldTab != null ) {
+ if( newTab == null ) {
+ closeRemainingTab();
+ } else {
+ // Update the preview with the edited text.
+ refreshSelectedTab( (FileEditorTab)newTab );
+ }
+ }
+ }
+ );
+ }
+
+ private void initTextChangeListener( final FileEditorTab tab ) {
+ tab.addTextChangeListener(
+ (ObservableValue<? extends String> editor,
+ final String oldValue, final String newValue) -> {
+ refreshSelectedTab( tab );
+ }
+ );
+ }
+
+ private void initCaretParagraphListener( final FileEditorTab tab ) {
+ tab.addCaretParagraphListener(
+ (ObservableValue<? extends Integer> editor,
+ final Integer oldValue, final Integer newValue) -> {
+ refreshSelectedTab( tab );
+ }
+ );
+ }
+
+ private void initVariableNameInjector( final FileEditorTab tab ) {
+ VariableNameInjector.listen( tab, getDefinitionPane() );
+ }
+
+ private void initWatchDog() {
+ getSnitch().addObserver( this );
+ }
+
+ /**
+ * Called whenever the preview pane becomes out of sync with the file editor
+ * tab. This can be called when the text changes, the caret paragraph changes,
+ * or the file tab changes.
+ *
+ * @param tab The file editor tab that has been changed in some fashion.
+ */
+ private void refreshSelectedTab( final FileEditorTab tab ) {
+ getPreviewPane().setPath( tab.getPath() );
+
+ Processor<String> processor = getProcessors().get( tab );
+
+ if( processor == null ) {
+ processor = createProcessor( tab );
+ getProcessors().put( tab, processor );
+ }
+
+ processor.processChain( tab.getEditorText() );
+ }
+
+ /**
+ * Returns the variable map of interpolated definitions.
+ *
+ * @return A map to help dereference variables.
+ */
+ private Map<String, String> getResolvedMap() {
+ return getDefinitionSource().getResolvedMap();
+ }
+
+ /**
+ * Returns the root node for the hierarchical definition source.
+ *
+ * @return Data to display in the definition pane.
+ */
+ private TreeView<String> getTreeView() {
+ try {
+ return getDefinitionSource().asTreeView();
+ } catch( Exception e ) {
+ alert( e );
+ }
+
+ return new TreeView<>();
+ }
+
+ private void openDefinition( final Path path ) {
+ openDefinition( path.toString() );
+ }
+
+ private void openDefinition( final String path ) {
+ try {
+ final DefinitionSource ds = createDefinitionSource( path );
+ setDefinitionSource( ds );
+ storeDefinitionSource();
+
+ getDefinitionPane().setRoot( ds.asTreeView() );
+ } catch( Exception e ) {
+ alert( e );
+ }
+ }
+
+ private void restoreDefinitionSource() {
+ final Preferences preferences = getPreferences();
+ final String source = preferences.get( PREFS_DEFINITION_SOURCE, null );
+
+ if( source != null ) {
+ openDefinition( source );
+ }
+ }
+
+ private void storeDefinitionSource() {
+ final Preferences preferences = getPreferences();
+ final DefinitionSource ds = getDefinitionSource();
+
+ preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() );
+ }
+
+ /**
+ * Called when the last open tab is closed. This clears out the preview pane
+ * and the definition pane.
+ */
+ private void closeRemainingTab() {
+ getPreviewPane().clear();
+ getDefinitionPane().clear();
+ }
+
+ /**
+ * Called when an exception occurs that warrants the user's attention.
+ *
+ * @param e The exception with a message that the user should know about.
+ */
+ private void alert( final Exception e ) {
+ // TODO: Update the status bar.
+ }
+
+ //---- File actions -------------------------------------------------------
+ /**
+ * Called when a file has been modified.
+ *
+ * @param snitch The watchdog file monitoring instance.
+ * @param file The file that was modified.
+ */
+ @Override
+ public void update( final Observable snitch, final Object file ) {
+ if( file instanceof Path ) {
+ update( (Path)file );
+ }
+ }
+
+ /**
+ * Called when a file has been modified.
+ *
+ * @param file Path to the modified file.
+ */
+ private void update( final Path file ) {
+ // Avoid throwing IllegalStateException by running from a non-JavaFX thread.
+ Platform.runLater(
+ () -> {
+ // Brute-force XSLT file reload by re-instantiating all processors.
+ getProcessors().clear();
+ refreshSelectedTab( getActiveFileEditor() );
+ }
+ );
+ }
+
+ //---- File actions -------------------------------------------------------
+ private void fileNew() {
+ getFileEditorPane().newEditor();
+ }
+
+ private void fileOpen() {
+ getFileEditorPane().openFileDialog();
+ }
+
+ private void fileClose() {
+ getFileEditorPane().closeEditor( getActiveFileEditor(), true );
+ }
+
+ private void fileCloseAll() {
+ getFileEditorPane().closeAllEditors();
+ }
+
+ private void fileSave() {
+ getFileEditorPane().saveEditor( getActiveFileEditor() );
+ }
+
+ private void fileSaveAll() {
+ getFileEditorPane().saveAllEditors();
+ }
+
+ private void fileExit() {
+ final Window window = getWindow();
+ fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
+ }
+
+ //---- Help actions -------------------------------------------------------
+ private void helpAbout() {
+ Alert alert = new Alert( AlertType.INFORMATION );
+ alert.setTitle( get( "Dialog.about.title" ) );
+ alert.setHeaderText( get( "Dialog.about.header" ) );
+ alert.setContentText( get( "Dialog.about.content" ) );
+ alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
+ alert.initOwner( getWindow() );
+
+ alert.showAndWait();
+ }
+
+ //---- Convenience accessors ----------------------------------------------
+ private float getFloat( final String key, final float defaultValue ) {
+ return getPreferences().getFloat( key, defaultValue );
+ }
+
+ private Preferences getPreferences() {
+ return getOptions().getState();
+ }
+
+ private Window getWindow() {
+ return getScene().getWindow();
+ }
+
+ private MarkdownEditorPane getActiveEditor() {
+ final EditorPane pane = getActiveFileEditor().getEditorPane();
+
+ return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null;
+ }
+
+ private FileEditorTab getActiveFileEditor() {
+ return getFileEditorPane().getActiveFileEditor();
+ }
+
+ //---- Member accessors ---------------------------------------------------
+ public Scene getScene() {
+ return this.scene;
+ }
+
+ private void setScene( Scene scene ) {
+ this.scene = scene;
+ }
+
+ private Map<FileEditorTab, Processor<String>> getProcessors() {
+ if( this.processors == null ) {
+ this.processors = new HashMap<>();
+ }
+
+ return this.processors;
+ }
+
+ private ProcessorFactory getProcessorFactory() {
+ if( this.processorFactory == null ) {
+ this.processorFactory = createProcessorFactory();
+ }
+
+ return this.processorFactory;
+ }
+
+ private FileEditorTabPane getFileEditorPane() {
+ if( this.fileEditorPane == null ) {
+ this.fileEditorPane = createFileEditorPane();
+ }
+
+ return this.fileEditorPane;
+ }
+
+ private HTMLPreviewPane getPreviewPane() {
+ if( this.previewPane == null ) {
+ this.previewPane = createPreviewPane();
+ }
+
+ return this.previewPane;
+ }
+
+ private void setDefinitionSource( final DefinitionSource definitionSource ) {
+ this.definitionSource = definitionSource;
+ }
+
+ private DefinitionSource getDefinitionSource() {
+ if( this.definitionSource == null ) {
+ this.definitionSource = new EmptyDefinitionSource();
+ }
+
+ return this.definitionSource;
+ }
+
+ private DefinitionPane getDefinitionPane() {
+ if( this.definitionPane == null ) {
+ this.definitionPane = createDefinitionPane();
+ }
+
+ return this.definitionPane;
+ }
+
+ private Options getOptions() {
+ return this.options;
+ }
+
+ private Snitch getSnitch() {
+ return this.snitch;
+ }
+
+ public MenuBar getMenuBar() {
+ return this.menuBar;
+ }
+
+ public void setMenuBar( MenuBar menuBar ) {
+ this.menuBar = menuBar;
+ }
+
+ //---- Member creators ----------------------------------------------------
+ /**
+ * Factory to create processors that are suited to different file types.
+ *
+ * @param tab The tab that is subjected to processing.
+ *
+ * @return A processor suited to the file type specified by the tab's path.
+ */
+ private Processor<String> createProcessor( final FileEditorTab tab ) {
+ return getProcessorFactory().createProcessor( tab );
+ }
+
+ private ProcessorFactory createProcessorFactory() {
+ return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
+ }
+
private DefinitionSource createDefinitionSource( final String path )
throws MalformedURLException {
src/main/java/com/scrivenvar/Services.java
public class Services {
- private static final Map<Class, Object> SINGLETONS = new HashMap<>( 8 );
+ private static final Map<Class, Object> SINGLETONS = new HashMap<>();
/**
private static <T> T newInstance( final Class<T> api ) {
final ServiceLoader<T> services = ServiceLoader.load( api );
-
- T result = null;
for( final T service : services ) {
- result = service;
-
- if( result != null ) {
- break;
+ if( service != null ) {
+ // Re-use the same instance the next time the class is loaded.
+ put( api, service );
+ return service;
}
- }
-
- if( result == null ) {
- throw new RuntimeException( "No implementation for: " + api );
}
-
- // Re-use the same instance the next time the class is loaded.
- put( api, result );
- return result;
+ throw new RuntimeException( "No implementation for: " + api );
}