Dave Jarvis' Repositories

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

Remove incompatible CSS, fix missing image placeholders

AuthorDaveJarvis <email>
Date2020-06-26 00:41:00 GMT-0700
Commit8a3365215e60d0dbd0dc48db221673aab0afcafa
Parent36158f5
.idea/artifacts/scrivenvar.xml
-<component name="ArtifactManager">
- <artifact type="javafx" name="scrivenvar">
- <output-path>$PROJECT_DIR$/out/artifacts/scrivenvar</output-path>
- <properties id="javafx-properties">
- <options>
- <option name="appClass" value="com.scrivenvar.Launcher" />
- <option name="description" value="Markdown editor with live preview and string interpolation." />
- <option name="height" value="768" />
- <option name="htmlPlaceholderId" value="" />
- <option name="nativeBundle" value="all" />
- <option name="title" value="Scrivenvar" />
- <option name="vendor" value="White Magic Software, Ltd." />
- <option name="version" value="1.6.4" />
- <option name="width" value="1024" />
- </options>
- </properties>
- <root id="root">
- <element id="module-output" name="scrivenvar_main" />
- </root>
- </artifact>
-</component>
+
build.gradle
id 'org.openjfx.javafxplugin' version '0.0.8'
id 'com.palantir.git-version' version '0.12.3'
+ id 'org.beryx.jlink' version '2.16.2'
}
implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3'
implementation 'com.miglayout:miglayout-javafx:5.2'
- implementation 'com.dlsc.preferencesfx:preferencesfx-core:11.6.0'
- implementation 'de.jensd:fontawesomefx-commons:11.0'
- implementation 'de.jensd:fontawesomefx-fontawesome:4.7.0-11'
+ implementation('com.dlsc.preferencesfx:preferencesfx-core:11.6.0') {
+ exclude group: 'org.openjfx'
+ }
+ implementation('de.jensd:fontawesomefx-commons:11.0') {
+ exclude group: 'org.openjfx'
+ }
+ implementation('de.jensd:fontawesomefx-fontawesome:4.7.0-11') {
+ exclude group: 'org.openjfx'
+ }
// Markdown
implementation 'com.ximpleware:vtd-xml:2.13.4'
implementation 'net.sf.saxon:Saxon-HE:10.1'
+ implementation 'xalan:xalan:2.7.2'
// HTML parsing and rendering
implementation 'org.apache.xmlgraphics:batik-util:1.13'
implementation 'org.apache.xmlgraphics:batik-xml:1.13'
+
+ implementation 'org.apache.bsf:bsf-api:3.1'
// Misc.
implementation 'org.ahocorasick:ahocorasick:0.4.0'
implementation 'org.apache.commons:commons-configuration2:2.7'
implementation 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3'
def os = ['win', 'linux', 'mac']
def fx = ['controls', 'graphics', 'fxml', 'swing']
+ // Create cross-platform überjar.
+ //
+ // Including these runtime dependencies breaks creating cross-platform binaries.
fx.each { fxitem ->
os.each { ositem ->
javafx {
version = "14"
- modules = ['javafx.controls', 'javafx.graphics', 'javafx.swing']
+ modules = ['javafx.controls', 'javafx.swing']
}
compileJava {
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
}
application {
+ applicationName = 'scrivenvar'
+ mainClassName = "com.${applicationName}.Main"
+
applicationDefaultJvmArgs = [
"--add-opens=javafx.controls/javafx.scene.control=ALL-UNNAMED",
"--add-opens=javafx.controls/javafx.scene.control.skin=ALL-UNNAMED",
"--add-opens=javafx.graphics/com.sun.javafx.css=ALL-UNNAMED",
]
}
-sourceCompatibility = JavaVersion.VERSION_11
-applicationName = 'scrivenvar'
version = gitVersion()
-mainClassName = "com.${applicationName}.Main"
+sourceCompatibility = JavaVersion.VERSION_11
+
def launcherClassName = "com.${applicationName}.Launcher"
def propertiesFile = new File("src/main/resources/com/${applicationName}/app.properties")
propertiesFile.write("application.version=${version}")
-
-//sourceSets {
-// main {
-// resources {
-// srcDir 'resources'
-// }
-// }
-//}
-
jar {
}
}
+ }
+}
+
+
+jlink {
+ options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
+ forceMerge 'jackson'
+
+ launcher {
+ name = 'java-keywords'
+ }
+
+ addExtraDependencies('javafx')
+ jpackage {
+ // Can also set via environment property BADASS_JLINK_JPACKAGE_HOME
+ jpackageHome = '/opt/jdk'
+// jvmArgs = ['-splash:$APPDIR/splash.png']
+// imageOptions = ['--icon', 'src/main/resources/java.ico']
+// installerOptions = [
+// '--file-associations', 'src/main/resources/associations.properties',
+// '--app-version', version,
+// ]
+// if (org.gradle.internal.os.OperatingSystem.current().windows) {
+// installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu']
+// }
+// }
}
}
src/main/java/com/scrivenvar/Launcher.java
private static void showAppInfo() throws IOException {
out( format( "%s version %s", getTitle(), getVersion() ) );
- out( format( "Copyright %s by White Magic Software, Ltd.", getYear() ) );
+ out( format( "Copyright %s White Magic Software, Ltd.", getYear() ) );
out( format( "Portions copyright 2020 Karl Tauber." ) );
}
src/main/java/com/scrivenvar/MainWindow.java
import com.scrivenvar.util.ActionBuilder;
import com.scrivenvar.util.ActionUtils;
-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.ChangeListener;
-import javafx.beans.value.ObservableBooleanValue;
-import javafx.beans.value.ObservableValue;
-import javafx.collections.ListChangeListener.Change;
-import javafx.collections.ObservableList;
-import javafx.event.Event;
-import javafx.event.EventHandler;
-import javafx.geometry.Pos;
-import javafx.scene.Node;
-import javafx.scene.Scene;
-import javafx.scene.control.*;
-import javafx.scene.control.Alert.AlertType;
-import javafx.scene.image.Image;
-import javafx.scene.image.ImageView;
-import javafx.scene.input.KeyEvent;
-import javafx.scene.layout.BorderPane;
-import javafx.scene.layout.VBox;
-import javafx.scene.text.Text;
-import javafx.stage.Window;
-import javafx.stage.WindowEvent;
-import javafx.util.Duration;
-import org.apache.commons.lang3.SystemUtils;
-import org.controlsfx.control.StatusBar;
-import org.fxmisc.richtext.StyleClassedTextArea;
-import org.reactfx.value.Val;
-import org.xhtmlrenderer.util.XRLog;
-
-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 static com.scrivenvar.Constants.*;
-import static com.scrivenvar.Messages.get;
-import static com.scrivenvar.util.StageState.*;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
-import static javafx.event.Event.fireEvent;
-import static javafx.scene.input.KeyCode.ENTER;
-import static javafx.scene.input.KeyCode.TAB;
-import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
-
-/**
- * Main window containing a tab pane in the center for file editors.
- */
-public class MainWindow implements Observer {
- /**
- * The {@code OPTIONS} variable must be declared before all other variables
- * to prevent subsequent initializations from failing due to missing user
- * preferences.
- */
- private final static Options OPTIONS = Services.load( Options.class );
- private final static Snitch SNITCH = Services.load( Snitch.class );
- private final static Notifier NOTIFIER = Services.load( Notifier.class );
-
- private final Scene mScene;
- private final StatusBar mStatusBar;
- private final Text mLineNumberText;
- private final TextField mFindTextField;
-
- private final Object mMutex = new Object();
-
- /**
- * Prevents re-instantiation of processing classes.
- */
- private final Map<FileEditorTab, Processor<String>> mProcessors =
- new HashMap<>();
-
- private final Map<String, String> mResolvedMap =
- new HashMap<>( DEFAULT_MAP_SIZE );
-
- /**
- * Called when the definition data is changed.
- */
- private final EventHandler<TreeItem.TreeModificationEvent<Event>>
- mTreeHandler = event -> {
- exportDefinitions( getDefinitionPath() );
- interpolateResolvedMap();
- renderActiveTab();
- };
-
- /**
- * Called to switch to the definition pane when the user presses the TAB key.
- */
- private final EventHandler<? super KeyEvent> mTabKeyHandler =
- (EventHandler<KeyEvent>) event -> {
- if( event.getCode() == TAB ) {
- getDefinitionPane().requestFocus();
- event.consume();
- }
- };
-
- /**
- * Called to inject the selected item when the user presses ENTER in the
- * definition pane.
- */
- private final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
- event -> {
- if( event.getCode() == ENTER ) {
- getVariableNameInjector().injectSelectedItem();
- }
- };
-
- private final ChangeListener<Integer> mCaretPositionListener =
- ( observable, oldPosition, newPosition ) -> {
- final FileEditorTab tab = getActiveFileEditorTab();
- final EditorPane pane = tab.getEditorPane();
- final StyleClassedTextArea editor = pane.getEditor();
-
- getLineNumberText().setText(
- get( STATUS_BAR_LINE,
- editor.getCurrentParagraph() + 1,
- editor.getParagraphs().size(),
- editor.getCaretPosition()
- )
- );
- };
-
- private final ChangeListener<Integer> mCaretParagraphListener =
- ( observable, oldIndex, newIndex ) ->
- scrollToParagraph( newIndex, true );
-
- private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
- private final DefinitionPane mDefinitionPane = new DefinitionPane();
- private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane();
- private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane(
- mCaretPositionListener,
- mCaretParagraphListener );
-
- /**
- * Listens on the definition pane for double-click events.
- */
- private final VariableNameInjector mVariableNameInjector
- = new VariableNameInjector( mDefinitionPane );
-
- public MainWindow() {
- mStatusBar = createStatusBar();
- mLineNumberText = createLineNumberText();
- mFindTextField = createFindTextField();
- mScene = createScene();
-
- System.getProperties()
- .setProperty( "xr.util-logging.loggingEnabled", "true" );
- XRLog.setLoggingEnabled( true );
-
- initLayout();
- initFindInput();
- initSnitch();
- initDefinitionListener();
- initTabAddedListener();
- initTabChangedListener();
- initPreferences();
- initVariableNameInjector();
-
- NOTIFIER.addObserver( this );
- }
-
- private void initLayout() {
- final Scene appScene = getScene();
-
- appScene.getStylesheets().add( STYLESHEET_SCENE );
-
- // TODO: Apply an XML syntax highlighting for XML files.
-// appScene.getStylesheets().add( STYLESHEET_XML );
- appScene.windowProperty().addListener(
- ( observable, oldWindow, newWindow ) ->
- newWindow.setOnCloseRequest(
- e -> {
- if( !getFileEditorPane().closeAllEditors() ) {
- e.consume();
- }
- }
- )
- );
- }
-
- /**
- * Initialize the find input text field to listen on F3, ENTER, and
- * ESCAPE key presses.
- */
- private void initFindInput() {
- final TextField input = getFindTextField();
-
- input.setOnKeyPressed( ( KeyEvent event ) -> {
- switch( event.getCode() ) {
- case F3:
- case ENTER:
- editFindNext();
- break;
- case F:
- if( !event.isControlDown() ) {
- break;
- }
- case ESCAPE:
- getStatusBar().setGraphic( null );
- getActiveFileEditorTab().getEditorPane().requestFocus();
- break;
- }
- } );
-
- // Remove when the input field loses focus.
- input.focusedProperty().addListener(
- ( focused, oldFocus, newFocus ) -> {
- if( !newFocus ) {
- getStatusBar().setGraphic( null );
- }
- }
- );
- }
-
- /**
- * Watch for changes to external files. In particular, this awaits
- * modifications to any XSL files associated with XML files being edited.
- * When
- * an XSL file is modified (external to the application), the snitch's ears
- * perk up and the file is reloaded. This keeps the XSL transformation up to
- * date with what's on the file system.
- */
- private void initSnitch() {
- SNITCH.addObserver( this );
- }
-
- /**
- * Listen for {@link FileEditorTabPane} to receive open definition file
- * event.
- */
- private void initDefinitionListener() {
- getFileEditorPane().onOpenDefinitionFileProperty().addListener(
- ( final ObservableValue<? extends Path> file,
- final Path oldPath, final Path newPath ) -> {
- // Indirectly refresh the resolved map.
- resetProcessors();
-
- openDefinitions( newPath );
-
- // Will create new processors and therefore a new resolved map.
- renderActiveTab();
- }
- );
- }
-
- /**
- * When tabs are added, hook the various change listeners onto the new
- * tab sothat 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 );
- initTabKeyEventListener( tab );
- initScrollEventListener( tab );
-// initSyntaxListener( tab );
- }
- }
- }
- }
- );
- }
-
- private void initScrollEventListener( final FileEditorTab tab ) {
- final var scrollPane = tab.getEditorPane().getScrollPane();
- final var scrollBar = getPreviewPane().getVerticalScrollBar();
-
- // Before the drag handler can be attached, the scroll bar for the
- // text editor pane must be visible.
- final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) ->
- Platform.runLater( () -> {
- if( newShow ) {
- final var handler = new ScrollEventHandler( scrollPane, scrollBar );
- handler.enabledProperty().bind( tab.selectedProperty() );
- }
- } );
-
- Val.flatMap( scrollPane.sceneProperty(), Scene::windowProperty )
- .flatMap( Window::showingProperty )
- .addListener( listener );
- }
-
- /**
- * Listen for new tab selection events.
- */
- private void initTabChangedListener() {
- final FileEditorTabPane editorPane = getFileEditorPane();
-
- // Update the preview pane changing tabs.
- editorPane.addTabSelectionListener(
- ( tabPane, oldTab, newTab ) -> {
- // If there was no old tab, then this is a first time load, which
- // can be ignored.
- if( oldTab != null ) {
- if( newTab != null ) {
- final FileEditorTab tab = (FileEditorTab) newTab;
- updateVariableNameInjector( tab );
- process( tab );
- }
- }
- }
- );
- }
-
- /**
- * Reloads the preferences from the previous session.
- */
- private void initPreferences() {
- initDefinitionPane();
- getFileEditorPane().initPreferences();
- }
-
- private void initVariableNameInjector() {
- updateVariableNameInjector( getActiveFileEditorTab() );
- }
-
- /**
- * Ensure that the keyboard events are received when a new tab is added
- * to the user interface.
- *
- * @param tab The tab editor that can trigger keyboard events.
- */
- private void initTabKeyEventListener( final FileEditorTab tab ) {
- tab.addEventFilter( KeyEvent.KEY_PRESSED, mTabKeyHandler );
- }
-
- private void initTextChangeListener( final FileEditorTab tab ) {
- tab.addTextChangeListener(
- ( editor, oldValue, newValue ) -> {
- process( tab );
- scrollToParagraph( getCurrentParagraphIndex() );
- }
- );
- }
-
- private int getCurrentParagraphIndex() {
- return getActiveEditorPane().getCurrentParagraphIndex();
- }
-
- private void scrollToParagraph( final int id ) {
- scrollToParagraph( id, false );
- }
-
- /**
- * @param id The paragraph to scroll to, will be approximated if it doesn't
- * exist.
- * @param force {@code true} means to force scrolling immediately, which
- * should only be attempted when it is known that the document
- * has been fully rendered. Otherwise the internal map of ID
- * attributes will be incomplete and scrolling will flounder.
- */
- private void scrollToParagraph( final int id, final boolean force ) {
- synchronized( mMutex ) {
- final var previewPane = getPreviewPane();
- final var scrollPane = previewPane.getScrollPane();
- final int approxId = getActiveEditorPane().approximateParagraphId( id );
-
- if( force ) {
- previewPane.scrollTo( approxId );
- }
- else {
- previewPane.tryScrollTo( approxId );
- }
-
- scrollPane.repaint();
- }
- }
-
- private void updateVariableNameInjector( final FileEditorTab tab ) {
- getVariableNameInjector().addListener( tab );
- }
-
- /**
- * 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 process( final FileEditorTab tab ) {
- if( tab == null ) {
- return;
- }
-
- getPreviewPane().setPath( tab.getPath() );
-
- final Processor<String> processor = getProcessors().computeIfAbsent(
- tab, p -> createProcessor( tab )
- );
-
- try {
- processor.processChain( tab.getEditorText() );
- } catch( final Exception ex ) {
- error( ex );
- }
- }
-
- private void renderActiveTab() {
- process( getActiveFileEditorTab() );
- }
-
- /**
- * Called when a definition source is opened.
- *
- * @param path Path to the definition source that was opened.
- */
- private void openDefinitions( final Path path ) {
- try {
- final DefinitionSource ds = createDefinitionSource( path );
- setDefinitionSource( ds );
- getUserPreferences().definitionPathProperty().setValue( path.toFile() );
- getUserPreferences().save();
-
- final Tooltip tooltipPath = new Tooltip( path.toString() );
- tooltipPath.setShowDelay( Duration.millis( 200 ) );
-
- final DefinitionPane pane = getDefinitionPane();
- pane.update( ds );
- pane.addTreeChangeHandler( mTreeHandler );
- pane.addKeyEventHandler( mDefinitionKeyHandler );
- pane.filenameProperty().setValue( path.getFileName().toString() );
- pane.setTooltip( tooltipPath );
-
- interpolateResolvedMap();
- } catch( final Exception e ) {
- error( e );
- }
- }
-
- private void exportDefinitions( final Path path ) {
- try {
- final DefinitionPane pane = getDefinitionPane();
- final TreeItem<String> root = pane.getTreeView().getRoot();
- final TreeItem<String> problemChild = pane.isTreeWellFormed();
-
- if( problemChild == null ) {
- getDefinitionSource().getTreeAdapter().export( root, path );
- getNotifier().clear();
- }
- else {
- final String msg = get(
- "yaml.error.tree.form", problemChild.getValue() );
- getNotifier().notify( msg );
- }
- } catch( final Exception e ) {
- error( e );
- }
- }
-
- private void interpolateResolvedMap() {
- final Map<String, String> treeMap = getDefinitionPane().toMap();
- final Map<String, String> map = new HashMap<>( treeMap );
- MapInterpolator.interpolate( map );
-
- getResolvedMap().clear();
- getResolvedMap().putAll( map );
- }
-
- private void initDefinitionPane() {
- openDefinitions( getDefinitionPath() );
- }
-
- /**
- * 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 error( final Exception e ) {
- getNotifier().notify( e );
- }
-
- //---- File actions -------------------------------------------------------
-
- /**
- * Called when an {@link Observable} instance has changed. This is called
- * by both the {@link Snitch} service and the notify service. The @link
- * Snitch} service can be called for different file types, including
- * {@link DefinitionSource} instances.
- *
- * @param observable The observed instance.
- * @param value The noteworthy item.
- */
- @Override
- public void update( final Observable observable, final Object value ) {
- if( value != null ) {
- if( observable instanceof Snitch && value instanceof Path ) {
- updateSelectedTab();
- }
- else if( observable instanceof Notifier && value instanceof String ) {
- updateStatusBar( (String) value );
- }
- }
- }
-
- /**
- * Updates the status bar to show the given message.
- *
- * @param s The message to show in the status bar.
- */
- private void updateStatusBar( final String s ) {
- Platform.runLater(
- () -> {
- final int index = s.indexOf( '\n' );
- final String message = s.substring(
- 0, index > 0 ? index : s.length() );
-
- getStatusBar().setText( message );
- }
- );
- }
-
- /**
- * Called when a file has been modified.
- */
- private void updateSelectedTab() {
- Platform.runLater(
- () -> {
- // Brute-force XSLT file reload by re-instantiating all processors.
- resetProcessors();
- renderActiveTab();
- }
- );
- }
-
- /**
- * After resetting the processors, they will refresh anew to be up-to-date
- * with the files (text and definition) currently loaded into the editor.
- */
- private void resetProcessors() {
- getProcessors().clear();
- }
-
- //---- File actions -------------------------------------------------------
-
- private void fileNew() {
- getFileEditorPane().newEditor();
- }
-
- private void fileOpen() {
- getFileEditorPane().openFileDialog();
- }
-
- private void fileClose() {
- getFileEditorPane().closeEditor( getActiveFileEditorTab(), true );
- }
-
- /**
- * TODO: Upon closing, first remove the tab change listeners. (There's no
- * need to re-render each tab when all are being closed.)
- */
- private void fileCloseAll() {
- getFileEditorPane().closeAllEditors();
- }
-
- private void fileSave() {
- getFileEditorPane().saveEditor( getActiveFileEditorTab() );
- }
-
- private void fileSaveAs() {
- final FileEditorTab editor = getActiveFileEditorTab();
- getFileEditorPane().saveEditorAs( editor );
- getProcessors().remove( editor );
-
- try {
- process( editor );
- } catch( final Exception ex ) {
- getNotifier().notify( ex );
- }
- }
-
- private void fileSaveAll() {
- getFileEditorPane().saveAllEditors();
- }
-
- private void fileExit() {
- final Window window = getWindow();
- fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
- }
-
- //---- Edit actions -------------------------------------------------------
-
- /**
- * Used to find text in the active file editor window.
- */
- private void editFind() {
- final TextField input = getFindTextField();
- getStatusBar().setGraphic( input );
- input.requestFocus();
- }
-
- public void editFindNext() {
- getActiveFileEditorTab().searchNext( getFindTextField().getText() );
- }
-
- public void editPreferences() {
- getUserPreferences().show();
- }
-
- //---- Insert actions -----------------------------------------------------
-
- /**
- * Delegates to the active editor to handle wrapping the current text
- * selection with leading and trailing strings.
- *
- * @param leading The string to put before the selection.
- * @param trailing The string to put after the selection.
- */
- private void insertMarkdown(
- final String leading, final String trailing ) {
- getActiveEditorPane().surroundSelection( leading, trailing );
- }
-
- private void insertMarkdown(
- final String leading, final String trailing, final String hint ) {
- getActiveEditorPane().surroundSelection( leading, trailing, hint );
- }
-
- //---- Help actions -------------------------------------------------------
-
- private void helpAbout() {
- final 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();
- }
-
- //---- 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 createProcessorFactory().createProcessor( tab );
- }
-
- private ProcessorFactory createProcessorFactory() {
- return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
- }
-
- private HTMLPreviewPane createHTMLPreviewPane() {
- return new HTMLPreviewPane();
- }
-
- private DefinitionSource createDefaultDefinitionSource() {
- return new YamlDefinitionSource( getDefinitionPath() );
- }
-
- private DefinitionSource createDefinitionSource( final Path path ) {
- try {
- return createDefinitionFactory().createDefinitionSource( path );
- } catch( final Exception ex ) {
- error( ex );
- return createDefaultDefinitionSource();
- }
- }
-
- private TextField createFindTextField() {
- return new TextField();
- }
-
- private DefinitionFactory createDefinitionFactory() {
- return new DefinitionFactory();
- }
-
- private StatusBar createStatusBar() {
- return new StatusBar();
- }
-
- private Scene createScene() {
- final SplitPane splitPane = new SplitPane(
- getDefinitionPane().getNode(),
- getFileEditorPane().getNode(),
- getPreviewPane().getNode() );
-
- splitPane.setDividerPositions(
- getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
- getFloat( K_PANE_SPLIT_EDITOR, .45f ),
- getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
-
- getDefinitionPane().prefHeightProperty()
- .bind( splitPane.heightProperty() );
-
- final BorderPane borderPane = new BorderPane();
- borderPane.setPrefSize( 1024, 800 );
- borderPane.setTop( createMenuBar() );
- borderPane.setBottom( getStatusBar() );
- borderPane.setCenter( splitPane );
-
- final VBox statusBar = new VBox();
- statusBar.setAlignment( Pos.BASELINE_CENTER );
- statusBar.getChildren().add( getLineNumberText() );
- getStatusBar().getRightItems().add( statusBar );
-
- // Force preview pane refresh on Windows.
- splitPane.getDividers().get( 1 ).positionProperty().addListener(
- ( l, oValue, nValue ) -> Platform.runLater(
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableBooleanValue;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.ListChangeListener.Change;
+import javafx.collections.ObservableList;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.*;
+import javafx.scene.control.Alert.AlertType;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Text;
+import javafx.stage.Window;
+import javafx.stage.WindowEvent;
+import javafx.util.Duration;
+import org.apache.commons.lang3.SystemUtils;
+import org.controlsfx.control.StatusBar;
+import org.fxmisc.richtext.StyleClassedTextArea;
+import org.reactfx.value.Val;
+import org.xhtmlrenderer.util.XRLog;
+
+import java.nio.file.Path;
+import java.util.*;
+import java.util.function.Function;
+import java.util.prefs.Preferences;
+
+import static com.scrivenvar.Constants.*;
+import static com.scrivenvar.Messages.get;
+import static com.scrivenvar.util.StageState.*;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
+import static javafx.application.Platform.runLater;
+import static javafx.event.Event.fireEvent;
+import static javafx.scene.input.KeyCode.ENTER;
+import static javafx.scene.input.KeyCode.TAB;
+import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
+
+/**
+ * Main window containing a tab pane in the center for file editors.
+ */
+public class MainWindow implements Observer {
+ /**
+ * The {@code OPTIONS} variable must be declared before all other variables
+ * to prevent subsequent initializations from failing due to missing user
+ * preferences.
+ */
+ private final static Options OPTIONS = Services.load( Options.class );
+ private final static Snitch SNITCH = Services.load( Snitch.class );
+ private final static Notifier NOTIFIER = Services.load( Notifier.class );
+
+ private final Scene mScene;
+ private final StatusBar mStatusBar;
+ private final Text mLineNumberText;
+ private final TextField mFindTextField;
+
+ private final Object mMutex = new Object();
+
+ /**
+ * Prevents re-instantiation of processing classes.
+ */
+ private final Map<FileEditorTab, Processor<String>> mProcessors =
+ new HashMap<>();
+
+ private final Map<String, String> mResolvedMap =
+ new HashMap<>( DEFAULT_MAP_SIZE );
+
+ /**
+ * Called when the definition data is changed.
+ */
+ private final EventHandler<TreeItem.TreeModificationEvent<Event>>
+ mTreeHandler = event -> {
+ exportDefinitions( getDefinitionPath() );
+ interpolateResolvedMap();
+ renderActiveTab();
+ };
+
+ /**
+ * Called to switch to the definition pane when the user presses the TAB key.
+ */
+ private final EventHandler<? super KeyEvent> mTabKeyHandler =
+ (EventHandler<KeyEvent>) event -> {
+ if( event.getCode() == TAB ) {
+ getDefinitionPane().requestFocus();
+ event.consume();
+ }
+ };
+
+ /**
+ * Called to inject the selected item when the user presses ENTER in the
+ * definition pane.
+ */
+ private final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
+ event -> {
+ if( event.getCode() == ENTER ) {
+ getVariableNameInjector().injectSelectedItem();
+ }
+ };
+
+ private final ChangeListener<Integer> mCaretPositionListener =
+ ( observable, oldPosition, newPosition ) -> {
+ final FileEditorTab tab = getActiveFileEditorTab();
+ final EditorPane pane = tab.getEditorPane();
+ final StyleClassedTextArea editor = pane.getEditor();
+
+ getLineNumberText().setText(
+ get( STATUS_BAR_LINE,
+ editor.getCurrentParagraph() + 1,
+ editor.getParagraphs().size(),
+ editor.getCaretPosition()
+ )
+ );
+ };
+
+ private final ChangeListener<Integer> mCaretParagraphListener =
+ ( observable, oldIndex, newIndex ) ->
+ scrollToParagraph( newIndex, true );
+
+ private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
+ private final DefinitionPane mDefinitionPane = new DefinitionPane();
+ private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane();
+ private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane(
+ mCaretPositionListener,
+ mCaretParagraphListener );
+
+ /**
+ * Listens on the definition pane for double-click events.
+ */
+ private final VariableNameInjector mVariableNameInjector
+ = new VariableNameInjector( mDefinitionPane );
+
+ public MainWindow() {
+ mStatusBar = createStatusBar();
+ mLineNumberText = createLineNumberText();
+ mFindTextField = createFindTextField();
+ mScene = createScene();
+
+ System.getProperties()
+ .setProperty( "xr.util-logging.loggingEnabled", "true" );
+ XRLog.setLoggingEnabled( true );
+
+ initLayout();
+ initFindInput();
+ initSnitch();
+ initDefinitionListener();
+ initTabAddedListener();
+ initTabChangedListener();
+ initPreferences();
+ initVariableNameInjector();
+
+ NOTIFIER.addObserver( this );
+ }
+
+ private void initLayout() {
+ final Scene appScene = getScene();
+
+ appScene.getStylesheets().add( STYLESHEET_SCENE );
+
+ // TODO: Apply an XML syntax highlighting for XML files.
+// appScene.getStylesheets().add( STYLESHEET_XML );
+ appScene.windowProperty().addListener(
+ ( observable, oldWindow, newWindow ) ->
+ newWindow.setOnCloseRequest(
+ e -> {
+ if( !getFileEditorPane().closeAllEditors() ) {
+ e.consume();
+ }
+ }
+ )
+ );
+ }
+
+ /**
+ * Initialize the find input text field to listen on F3, ENTER, and
+ * ESCAPE key presses.
+ */
+ private void initFindInput() {
+ final TextField input = getFindTextField();
+
+ input.setOnKeyPressed( ( KeyEvent event ) -> {
+ switch( event.getCode() ) {
+ case F3:
+ case ENTER:
+ editFindNext();
+ break;
+ case F:
+ if( !event.isControlDown() ) {
+ break;
+ }
+ case ESCAPE:
+ getStatusBar().setGraphic( null );
+ getActiveFileEditorTab().getEditorPane().requestFocus();
+ break;
+ }
+ } );
+
+ // Remove when the input field loses focus.
+ input.focusedProperty().addListener(
+ ( focused, oldFocus, newFocus ) -> {
+ if( !newFocus ) {
+ getStatusBar().setGraphic( null );
+ }
+ }
+ );
+ }
+
+ /**
+ * Watch for changes to external files. In particular, this awaits
+ * modifications to any XSL files associated with XML files being edited.
+ * When
+ * an XSL file is modified (external to the application), the snitch's ears
+ * perk up and the file is reloaded. This keeps the XSL transformation up to
+ * date with what's on the file system.
+ */
+ private void initSnitch() {
+ SNITCH.addObserver( this );
+ }
+
+ /**
+ * Listen for {@link FileEditorTabPane} to receive open definition file
+ * event.
+ */
+ private void initDefinitionListener() {
+ getFileEditorPane().onOpenDefinitionFileProperty().addListener(
+ ( final ObservableValue<? extends Path> file,
+ final Path oldPath, final Path newPath ) -> {
+ // Indirectly refresh the resolved map.
+ resetProcessors();
+
+ openDefinitions( newPath );
+
+ // Will create new processors and therefore a new resolved map.
+ renderActiveTab();
+ }
+ );
+ }
+
+ /**
+ * When tabs are added, hook the various change listeners onto the new
+ * tab sothat 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 );
+ initTabKeyEventListener( tab );
+ initScrollEventListener( tab );
+// initSyntaxListener( tab );
+ }
+ }
+ }
+ }
+ );
+ }
+
+ private void initScrollEventListener( final FileEditorTab tab ) {
+ final var scrollPane = tab.getEditorPane().getScrollPane();
+ final var scrollBar = getPreviewPane().getVerticalScrollBar();
+
+ // Before the drag handler can be attached, the scroll bar for the
+ // text editor pane must be visible.
+ final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) ->
+ runLater( () -> {
+ if( newShow ) {
+ final var handler = new ScrollEventHandler( scrollPane, scrollBar );
+ handler.enabledProperty().bind( tab.selectedProperty() );
+ }
+ } );
+
+ Val.flatMap( scrollPane.sceneProperty(), Scene::windowProperty )
+ .flatMap( Window::showingProperty )
+ .addListener( listener );
+ }
+
+ /**
+ * Listen for new tab selection events.
+ */
+ private void initTabChangedListener() {
+ final FileEditorTabPane editorPane = getFileEditorPane();
+
+ // Update the preview pane changing tabs.
+ editorPane.addTabSelectionListener(
+ ( tabPane, oldTab, newTab ) -> {
+ // If there was no old tab, then this is a first time load, which
+ // can be ignored.
+ if( oldTab != null ) {
+ if( newTab != null ) {
+ final FileEditorTab tab = (FileEditorTab) newTab;
+ updateVariableNameInjector( tab );
+ process( tab );
+ }
+ }
+ }
+ );
+ }
+
+ /**
+ * Reloads the preferences from the previous session.
+ */
+ private void initPreferences() {
+ initDefinitionPane();
+ getFileEditorPane().initPreferences();
+ }
+
+ private void initVariableNameInjector() {
+ updateVariableNameInjector( getActiveFileEditorTab() );
+ }
+
+ /**
+ * Ensure that the keyboard events are received when a new tab is added
+ * to the user interface.
+ *
+ * @param tab The tab editor that can trigger keyboard events.
+ */
+ private void initTabKeyEventListener( final FileEditorTab tab ) {
+ tab.addEventFilter( KeyEvent.KEY_PRESSED, mTabKeyHandler );
+ }
+
+ private void initTextChangeListener( final FileEditorTab tab ) {
+ tab.addTextChangeListener(
+ ( editor, oldValue, newValue ) -> {
+ process( tab );
+ scrollToParagraph( getCurrentParagraphIndex() );
+ }
+ );
+ }
+
+ private int getCurrentParagraphIndex() {
+ return getActiveEditorPane().getCurrentParagraphIndex();
+ }
+
+ private void scrollToParagraph( final int id ) {
+ scrollToParagraph( id, false );
+ }
+
+ /**
+ * @param id The paragraph to scroll to, will be approximated if it doesn't
+ * exist.
+ * @param force {@code true} means to force scrolling immediately, which
+ * should only be attempted when it is known that the document
+ * has been fully rendered. Otherwise the internal map of ID
+ * attributes will be incomplete and scrolling will flounder.
+ */
+ private void scrollToParagraph( final int id, final boolean force ) {
+ synchronized( mMutex ) {
+ final var previewPane = getPreviewPane();
+ final var scrollPane = previewPane.getScrollPane();
+ final int approxId = getActiveEditorPane().approximateParagraphId( id );
+
+ if( force ) {
+ previewPane.scrollTo( approxId );
+ }
+ else {
+ previewPane.tryScrollTo( approxId );
+ }
+
+ scrollPane.repaint();
+ }
+ }
+
+ private void updateVariableNameInjector( final FileEditorTab tab ) {
+ getVariableNameInjector().addListener( tab );
+ }
+
+ /**
+ * 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 process( final FileEditorTab tab ) {
+ if( tab == null ) {
+ return;
+ }
+
+ getPreviewPane().setPath( tab.getPath() );
+
+ final Processor<String> processor = getProcessors().computeIfAbsent(
+ tab, p -> createProcessor( tab )
+ );
+
+ try {
+ processor.processChain( tab.getEditorText() );
+ } catch( final Exception ex ) {
+ error( ex );
+ }
+ }
+
+ private void renderActiveTab() {
+ process( getActiveFileEditorTab() );
+ }
+
+ /**
+ * Called when a definition source is opened.
+ *
+ * @param path Path to the definition source that was opened.
+ */
+ private void openDefinitions( final Path path ) {
+ try {
+ final DefinitionSource ds = createDefinitionSource( path );
+ setDefinitionSource( ds );
+ getUserPreferences().definitionPathProperty().setValue( path.toFile() );
+ getUserPreferences().save();
+
+ final Tooltip tooltipPath = new Tooltip( path.toString() );
+ tooltipPath.setShowDelay( Duration.millis( 200 ) );
+
+ final DefinitionPane pane = getDefinitionPane();
+ pane.update( ds );
+ pane.addTreeChangeHandler( mTreeHandler );
+ pane.addKeyEventHandler( mDefinitionKeyHandler );
+ pane.filenameProperty().setValue( path.getFileName().toString() );
+ pane.setTooltip( tooltipPath );
+
+ interpolateResolvedMap();
+ } catch( final Exception e ) {
+ error( e );
+ }
+ }
+
+ private void exportDefinitions( final Path path ) {
+ try {
+ final DefinitionPane pane = getDefinitionPane();
+ final TreeItem<String> root = pane.getTreeView().getRoot();
+ final TreeItem<String> problemChild = pane.isTreeWellFormed();
+
+ if( problemChild == null ) {
+ getDefinitionSource().getTreeAdapter().export( root, path );
+ getNotifier().clear();
+ }
+ else {
+ final String msg = get(
+ "yaml.error.tree.form", problemChild.getValue() );
+ getNotifier().notify( msg );
+ }
+ } catch( final Exception e ) {
+ error( e );
+ }
+ }
+
+ private void interpolateResolvedMap() {
+ final Map<String, String> treeMap = getDefinitionPane().toMap();
+ final Map<String, String> map = new HashMap<>( treeMap );
+ MapInterpolator.interpolate( map );
+
+ getResolvedMap().clear();
+ getResolvedMap().putAll( map );
+ }
+
+ private void initDefinitionPane() {
+ openDefinitions( getDefinitionPath() );
+ }
+
+ /**
+ * 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 error( final Exception e ) {
+ getNotifier().notify( e );
+ }
+
+ //---- File actions -------------------------------------------------------
+
+ /**
+ * Called when an {@link Observable} instance has changed. This is called
+ * by both the {@link Snitch} service and the notify service. The @link
+ * Snitch} service can be called for different file types, including
+ * {@link DefinitionSource} instances.
+ *
+ * @param observable The observed instance.
+ * @param value The noteworthy item.
+ */
+ @Override
+ public void update( final Observable observable, final Object value ) {
+ if( value != null ) {
+ if( observable instanceof Snitch && value instanceof Path ) {
+ updateSelectedTab();
+ }
+ else if( observable instanceof Notifier && value instanceof String ) {
+ updateStatusBar( (String) value );
+ }
+ }
+ }
+
+ /**
+ * Updates the status bar to show the given message.
+ *
+ * @param s The message to show in the status bar.
+ */
+ private void updateStatusBar( final String s ) {
+ runLater(
+ () -> {
+ final int index = s.indexOf( '\n' );
+ final String message = s.substring(
+ 0, index > 0 ? index : s.length() );
+
+ getStatusBar().setText( message );
+ }
+ );
+ }
+
+ /**
+ * Called when a file has been modified.
+ */
+ private void updateSelectedTab() {
+ runLater(
+ () -> {
+ // Brute-force XSLT file reload by re-instantiating all processors.
+ resetProcessors();
+ renderActiveTab();
+ }
+ );
+ }
+
+ /**
+ * After resetting the processors, they will refresh anew to be up-to-date
+ * with the files (text and definition) currently loaded into the editor.
+ */
+ private void resetProcessors() {
+ getProcessors().clear();
+ }
+
+ //---- File actions -------------------------------------------------------
+
+ private void fileNew() {
+ getFileEditorPane().newEditor();
+ }
+
+ private void fileOpen() {
+ getFileEditorPane().openFileDialog();
+ }
+
+ private void fileClose() {
+ getFileEditorPane().closeEditor( getActiveFileEditorTab(), true );
+ }
+
+ /**
+ * TODO: Upon closing, first remove the tab change listeners. (There's no
+ * need to re-render each tab when all are being closed.)
+ */
+ private void fileCloseAll() {
+ getFileEditorPane().closeAllEditors();
+ }
+
+ private void fileSave() {
+ getFileEditorPane().saveEditor( getActiveFileEditorTab() );
+ }
+
+ private void fileSaveAs() {
+ final FileEditorTab editor = getActiveFileEditorTab();
+ getFileEditorPane().saveEditorAs( editor );
+ getProcessors().remove( editor );
+
+ try {
+ process( editor );
+ } catch( final Exception ex ) {
+ getNotifier().notify( ex );
+ }
+ }
+
+ private void fileSaveAll() {
+ getFileEditorPane().saveAllEditors();
+ }
+
+ private void fileExit() {
+ final Window window = getWindow();
+ fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
+ }
+
+ //---- Edit actions -------------------------------------------------------
+
+ /**
+ * Used to find text in the active file editor window.
+ */
+ private void editFind() {
+ final TextField input = getFindTextField();
+ getStatusBar().setGraphic( input );
+ input.requestFocus();
+ }
+
+ public void editFindNext() {
+ getActiveFileEditorTab().searchNext( getFindTextField().getText() );
+ }
+
+ public void editPreferences() {
+ getUserPreferences().show();
+ }
+
+ //---- Insert actions -----------------------------------------------------
+
+ /**
+ * Delegates to the active editor to handle wrapping the current text
+ * selection with leading and trailing strings.
+ *
+ * @param leading The string to put before the selection.
+ * @param trailing The string to put after the selection.
+ */
+ private void insertMarkdown(
+ final String leading, final String trailing ) {
+ getActiveEditorPane().surroundSelection( leading, trailing );
+ }
+
+ private void insertMarkdown(
+ final String leading, final String trailing, final String hint ) {
+ getActiveEditorPane().surroundSelection( leading, trailing, hint );
+ }
+
+ //---- Help actions -------------------------------------------------------
+
+ private void helpAbout() {
+ final 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();
+ }
+
+ //---- 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 createProcessorFactory().createProcessor( tab );
+ }
+
+ private ProcessorFactory createProcessorFactory() {
+ return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
+ }
+
+ private HTMLPreviewPane createHTMLPreviewPane() {
+ return new HTMLPreviewPane();
+ }
+
+ private DefinitionSource createDefaultDefinitionSource() {
+ return new YamlDefinitionSource( getDefinitionPath() );
+ }
+
+ private DefinitionSource createDefinitionSource( final Path path ) {
+ try {
+ return createDefinitionFactory().createDefinitionSource( path );
+ } catch( final Exception ex ) {
+ error( ex );
+ return createDefaultDefinitionSource();
+ }
+ }
+
+ private TextField createFindTextField() {
+ return new TextField();
+ }
+
+ private DefinitionFactory createDefinitionFactory() {
+ return new DefinitionFactory();
+ }
+
+ private StatusBar createStatusBar() {
+ return new StatusBar();
+ }
+
+ private Scene createScene() {
+ final SplitPane splitPane = new SplitPane(
+ getDefinitionPane().getNode(),
+ getFileEditorPane().getNode(),
+ getPreviewPane().getNode() );
+
+ splitPane.setDividerPositions(
+ getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
+ getFloat( K_PANE_SPLIT_EDITOR, .45f ),
+ getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
+
+ getDefinitionPane().prefHeightProperty()
+ .bind( splitPane.heightProperty() );
+
+ final BorderPane borderPane = new BorderPane();
+ borderPane.setPrefSize( 1024, 800 );
+ borderPane.setTop( createMenuBar() );
+ borderPane.setBottom( getStatusBar() );
+ borderPane.setCenter( splitPane );
+
+ final VBox statusBar = new VBox();
+ statusBar.setAlignment( Pos.BASELINE_CENTER );
+ statusBar.getChildren().add( getLineNumberText() );
+ getStatusBar().getRightItems().add( statusBar );
+
+ // Force preview pane refresh on Windows.
+ splitPane.getDividers().get( 1 ).positionProperty().addListener(
+ ( l, oValue, nValue ) -> runLater(
() -> {
if( SystemUtils.IS_OS_WINDOWS ) {
src/main/java/com/scrivenvar/ScrollEventHandler.java
private boolean isEnabled() {
- // As a minor optimization, when this is set to false, it could remove
+ // TODO: As a minor optimization, when this is set to false, it could remove
// the MouseHandler and ScrollHandler so that events only dispatch to one
// object (instead of one per editor tab).
src/main/java/com/scrivenvar/editors/EditorPane.java
package com.scrivenvar.editors;
-import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import java.nio.file.Path;
+import java.util.Random;
import java.util.function.Consumer;
+import static javafx.application.Platform.runLater;
import static org.fxmisc.wellbehaved.event.InputMap.consume;
@Override
public void requestFocus() {
- Platform.runLater( () -> getEditor().requestFocus() );
+ runLater( () -> getEditor().requestFocus() );
}
src/main/java/com/scrivenvar/preview/CustomImageResourceLoader.java
+/*
+ * Copyright 2020 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.preview;
+
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import org.xhtmlrenderer.extend.FSImage;
+import org.xhtmlrenderer.resource.ImageResource;
+import org.xhtmlrenderer.swing.ImageResourceLoader;
+
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+import static com.scrivenvar.preview.SVGRasterizer.PLACEHOLDER_IMAGE;
+import static org.xhtmlrenderer.swing.AWTFSImage.createImage;
+
+/**
+ * Responsible for loading images. If the image cannot be found, a placeholder
+ * is used instead.
+ */
+public class CustomImageResourceLoader extends ImageResourceLoader {
+ /**
+ * Placeholder that's displayed when image cannot be found.
+ */
+ private static final FSImage FS_PLACEHOLDER_IMAGE =
+ createImage( PLACEHOLDER_IMAGE );
+
+ private final IntegerProperty mMaxWidthProperty = new SimpleIntegerProperty();
+
+ public CustomImageResourceLoader() {
+ }
+
+ public IntegerProperty widthProperty() {
+ return mMaxWidthProperty;
+ }
+
+ @Override
+ public synchronized ImageResource get(
+ final String uri, final int width, final int height ) {
+ assert uri != null;
+ assert width >= 0;
+ assert height >= 0;
+
+ boolean exists;
+
+ try {
+ exists = Files.exists( Paths.get( new URI( uri ) ) );
+ } catch( final Exception e ) {
+ exists = false;
+ }
+
+ return exists
+ ? scale( uri, width, height )
+ : new ImageResource( uri, FS_PLACEHOLDER_IMAGE );
+ }
+
+ /**
+ * Scales the image found at the given uri.
+ *
+ * @param uri Path to the image file to load.
+ * @param w Unused (usually -1, which is useless).
+ * @param h Unused (ditto).
+ * @return Resource representing the rendered image and path.
+ */
+ private ImageResource scale( final String uri, final int w, final int h ) {
+ final var ir = super.get( uri, w, h );
+ final var image = ir.getImage();
+ final var imageWidth = image.getWidth();
+ final var imageHeight = image.getHeight();
+
+ int maxWidth = mMaxWidthProperty.get();
+ int newWidth = imageWidth;
+ int newHeight = imageHeight;
+
+ // Maintain aspect ratio while shrinking image to view port bounds.
+ if( imageWidth > maxWidth ) {
+ newWidth = maxWidth;
+ newHeight = (newWidth * imageHeight) / imageWidth;
+ }
+
+ image.scale( newWidth, newHeight );
+ return ir;
+ }
+}
src/main/java/com/scrivenvar/preview/HTMLPreviewPane.java
import javax.swing.*;
import java.awt.*;
+import java.awt.event.ComponentEvent;
+import java.awt.event.ComponentListener;
import java.nio.file.Path;
import static com.scrivenvar.Constants.PARAGRAPH_ID_PREFIX;
import static com.scrivenvar.Constants.STYLESHEET_PREVIEW;
+import static org.xhtmlrenderer.swing.ImageResourceLoader.NO_OP_REPAINT_LISTENER;
/**
private final static String HTML_FOOTER = "</body></html>";
- private final StringBuilder mHtml = new StringBuilder( 65536 );
+ private final StringBuilder mHtmlDocument = new StringBuilder( 65536 );
private final int mHtmlPrefixLength;
private final W3CDom mW3cDom = new W3CDom();
private final XhtmlNamespaceHandler mNamespaceHandler =
new XhtmlNamespaceHandler();
- private final HTMLPanel mRenderer = new HTMLPanel();
+ private final HTMLPanel mHtmlRenderer = new HTMLPanel();
private final SwingNode mSwingNode = new SwingNode();
- private final JScrollPane mScrollPane = new JScrollPane( mRenderer );
+ private final JScrollPane mScrollPane = new JScrollPane( mHtmlRenderer );
private final DocumentEventHandler mDocumentHandler =
new DocumentEventHandler();
+ private final CustomImageResourceLoader mImageLoader =
+ new CustomImageResourceLoader();
private Path mPath;
/**
* Creates a new preview pane that can scroll to the caret position within the
* document.
*/
public HTMLPreviewPane() {
+ mHtmlDocument.append( HTML_HEADER );
+ mHtmlPrefixLength = mHtmlDocument.length();
+
final var factory = new ChainedReplacedElementFactory();
factory.addFactory( new SVGReplacedElementFactory() );
- factory.addFactory( new SwingReplacedElementFactory() );
+ factory.addFactory( new SwingReplacedElementFactory(
+ NO_OP_REPAINT_LISTENER, mImageLoader ) );
final var context = getSharedContext();
context.setReplacedElementFactory( factory );
context.getTextRenderer().setSmoothingThreshold( 0 );
mSwingNode.setContent( mScrollPane );
- mHtml.append( HTML_HEADER );
- mHtmlPrefixLength = mHtml.length();
+ mHtmlRenderer.addDocumentListener( mDocumentHandler );
+ setStyle( "-fx-background-color: white;" );
- mRenderer.addDocumentListener( mDocumentHandler );
- setStyle("-fx-background-color: white;");
+ mHtmlRenderer.addComponentListener( new ComponentListener() {
+ @Override
+ public void componentResized( final ComponentEvent e ) {
+ // Scaling a bit below the full width prevents the horizontal scrollbar
+ // from appearing.
+ final int width = (int) (e.getComponent().getWidth() * .95);
+ mImageLoader.widthProperty().set( width );
+ }
+
+ @Override
+ public void componentMoved( final ComponentEvent e ) {
+ }
+
+ @Override
+ public void componentShown( final ComponentEvent e ) {
+ }
+
+ @Override
+ public void componentHidden( final ComponentEvent e ) {
+ }
+ } );
}
final org.w3c.dom.Document w3cDoc = mW3cDom.fromJsoup( jsoupDoc );
- mRenderer.setDocument( w3cDoc, getBaseUrl(), mNamespaceHandler );
+ mHtmlRenderer.setDocument( w3cDoc, getBaseUrl(), mNamespaceHandler );
}
private void scrollTo( final Point point ) {
- mRenderer.scrollTo( point );
+ mHtmlRenderer.scrollTo( point );
}
private void scrollToBottom() {
- scrollToY( mRenderer.getHeight() );
+ scrollToY( mHtmlRenderer.getHeight() );
}
private Box getBoxById( final String id ) {
return getSharedContext().getBoxById( id );
}
private String decorate( final String html ) {
// Trim the HTML back to the header.
- mHtml.setLength( mHtmlPrefixLength );
+ mHtmlDocument.setLength( mHtmlPrefixLength );
// Write the HTML body element followed by closing tags.
- return mHtml.append( html )
- .append( HTML_FOOTER )
- .toString();
+ return mHtmlDocument.append( html )
+ .append( HTML_FOOTER )
+ .toString();
}
if( !box.getStyle().isInline() ) {
- final var margin = box.getMargin( mRenderer.getLayoutContext() );
+ final var margin = box.getMargin( mHtmlRenderer.getLayoutContext() );
x += margin.left();
y += margin.top();
private SharedContext getSharedContext() {
- return mRenderer.getSharedContext();
+ return mHtmlRenderer.getSharedContext();
}
}
src/main/java/com/scrivenvar/preview/SVGRasterizer.java
package com.scrivenvar.preview;
+import com.scrivenvar.Services;
+import com.scrivenvar.service.events.Notifier;
import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
import org.apache.batik.gvt.renderer.ImageRenderer;
public class SVGRasterizer {
+ private final static Notifier NOTIFIER = Services.load( Notifier.class );
+
private final static SAXSVGDocumentFactory mFactory =
new SAXSVGDocumentFactory( getXMLParserClassName() );
- private final static Map<Object, Object> RENDERING_HINTS = Map.of(
- KEY_ALPHA_INTERPOLATION,
- VALUE_ALPHA_INTERPOLATION_QUALITY,
- KEY_INTERPOLATION,
- VALUE_INTERPOLATION_BICUBIC,
+ public final static Map<Object, Object> RENDERING_HINTS = Map.of(
KEY_ANTIALIASING,
VALUE_ANTIALIAS_ON,
+ KEY_ALPHA_INTERPOLATION,
+ VALUE_ALPHA_INTERPOLATION_QUALITY,
KEY_COLOR_RENDERING,
VALUE_COLOR_RENDER_QUALITY,
KEY_DITHERING,
VALUE_DITHER_DISABLE,
+ KEY_FRACTIONALMETRICS,
+ VALUE_FRACTIONALMETRICS_ON,
+ KEY_INTERPOLATION,
+ VALUE_INTERPOLATION_BICUBIC,
KEY_RENDERING,
VALUE_RENDER_QUALITY,
KEY_STROKE_CONTROL,
VALUE_STROKE_PURE,
- KEY_FRACTIONALMETRICS,
- VALUE_FRACTIONALMETRICS_ON,
KEY_TEXT_ANTIALIASING,
- VALUE_TEXT_ANTIALIAS_OFF
+ VALUE_TEXT_ANTIALIAS_ON
);
+
+ public final static BufferedImage PLACEHOLDER_IMAGE;
+
+ static {
+ final int w = 150;
+ final int h = 150;
+ final var image = new BufferedImage( w, h, TYPE_INT_RGB );
+ final var graphics = (Graphics2D) image.getGraphics();
+ graphics.setRenderingHints( RENDERING_HINTS );
+
+ graphics.setColor( new Color( 204, 204, 204 ) );
+ graphics.fillRect( 0, 0, w, h );
+ graphics.setColor( new Color( 255, 204, 204 ) );
+ graphics.setStroke( new BasicStroke( 4 ) );
+ graphics.drawOval( w / 4, h / 4, w / 2, h / 2 );
+ graphics.drawLine( w / 4 + (int) (w / 4 / Math.PI),
+ h / 4 + (int) (w / 4 / Math.PI),
+ w / 2 + w / 4 - (int) (w / 4 / Math.PI),
+ h / 2 + h / 4 - (int) (w / 4 / Math.PI) );
+ PLACEHOLDER_IMAGE = image;
+ }
private static class BufferedImageTranscoder extends ImageTranscoder {
return rasterize( new URL( url ), width );
} catch( final Exception e ) {
- return createPlaceholderImage( width );
+ NOTIFIER.notify( e );
+ return PLACEHOLDER_IMAGE;
}
}
return transcoder.getImage();
- }
-
- @SuppressWarnings("SuspiciousNameCombination")
- private static Image createPlaceholderImage( final int width ) {
- final var image = new BufferedImage( width, width, TYPE_INT_RGB );
- final var graphics = (Graphics2D) image.getGraphics();
-
- graphics.setColor( RED );
- graphics.setStroke( new BasicStroke( 5 ) );
- graphics.drawOval( 5, 5, width / 2, width / 2 );
-
- return image;
}
}
src/main/java/com/scrivenvar/preview/SVGReplacedElementFactory.java
/**
- * Where to put document inline evaluated R expressions.
+ * Where to put cached image files.
*/
private final Map<String, Image> mImageCache = new LinkedHashMap<>() {
@Override
protected boolean removeEldestEntry(
final Map.Entry<String, Image> eldest ) {
return size() > MAX_CACHED_IMAGES;
}
};
+ @Override
public ReplacedElement createReplacedElement(
final LayoutContext c,
src/main/resources/com/scrivenvar/preview/webview.css
}
-dl dd {
- *float: none;
- *width: auto;
- *margin-left: 20%;
-}
-
/* CODE
=============================================================================*/
border: .125em solid #ccc;
line-height: 1.6;
- overflow: auto;
padding: .25em .5em;
border-radius: .25em;
kbd {
background-color: #ccc;
- background-image: linear-gradient(#f8f8f8, #DDDDDD);
background-repeat: repeat-x;
border-color: #DDDDDD #CCCCCC #CCCCCC #DDDDDD;
- border-image: none;
border-radius: 2px;
border-style: solid;
Delta962 lines added, 808 lines removed, 154-line increase