| Author | djarvis <email> |
|---|---|
| Date | 2016-12-10 14:52:02 GMT-0800 |
| Commit | 64bbaed87a9b18eccd9512c85bd26548435ee651 |
| Parent | eaf59c3 |
| /private | ||
| .nb-gradle-properties | ||
| -src/main/resources/com/scrivenvar/build.sh | ||
| +scrivenvar.pro | ||
| * Vladimir Schneider: [flexmark](https://website.com) | ||
| * Jens Deters: [FontAwesomeFX](https://bitbucket.org/Jerady/fontawesomefx) | ||
| + * Apache Tika Team: [Apache Tika](https://tika.apache.org/) | ||
| * User-defined variables | ||
| -* Preview variable values in real time | ||
| +* Recursive variable definitions | ||
| +* Real-time document preview with variable substitution | ||
| * Platform independent (Windows, Linux, MacOS) | ||
| -* Auto-insert variable names by typing in values followed by `Control+Space` | ||
| +* Auto-insert variable names pressing `Control+Space` | ||
| Future Features | ||
| --- | ||
| * Spell check | ||
| +* Search and replace, with or without variables | ||
| * XML and XSL processing | ||
| -* Search and replace text with variables | ||
| * R integration using [Rserve](https://rforge.net/Rserve/) | ||
| +* Re-organize variable names | ||
| Requirements |
| compile group: 'com.miglayout', name: 'miglayout-javafx', version: '5.0' | ||
| compile group: 'de.jensd', name: 'fontawesomefx-fontawesome', version: '4.5.0' | ||
| - compile group: 'commons-configuration', name: 'commons-configuration', version: '1.10' | ||
| + compile group: 'org.ahocorasick', name: 'ahocorasick', version: '0.3.0' | ||
| compile group: 'com.vladsch.flexmark', name: 'flexmark', version: '0.6.1' | ||
| compile group: 'com.vladsch.flexmark', name: 'flexmark-ext-gfm-tables', version: '0.6.1' | ||
| - compile group: 'org.yaml', name: 'snakeyaml', version: '1.17' | ||
| compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.8.4' | ||
| compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.8.4' | ||
| compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.8.4' | ||
| compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.8.4' | ||
| - compile group: 'org.ahocorasick', name: 'ahocorasick', version: '0.3.0' | ||
| - compile group: 'junit', name: 'junit', version: '4.4' | ||
| + compile group: 'org.yaml', name: 'snakeyaml', version: '1.17' | ||
| + compile group: 'com.googlecode.juniversalchardet', name: 'juniversalchardet', version: '1.0.3' | ||
| + compile group: 'commons-configuration', name: 'commons-configuration', version: '1.10' | ||
| } | ||
| jar { | ||
| baseName = 'scrivenvar' | ||
| + | ||
| + from { | ||
| + (configurations.runtime).collect { | ||
| + it.isDirectory() ? it : zipTree(it) | ||
| + } | ||
| + } | ||
| + | ||
| manifest { | ||
| attributes 'Main-Class': mainClassName, |
| import com.scrivenvar.service.events.AlertMessage; | ||
| import com.scrivenvar.service.events.AlertService; | ||
| -import java.io.IOException; | ||
| +import java.nio.charset.Charset; | ||
| import java.nio.file.Files; | ||
| import java.nio.file.Path; | ||
| +import static java.util.Locale.ENGLISH; | ||
| import java.util.function.Consumer; | ||
| import javafx.application.Platform; | ||
| import javafx.beans.binding.Bindings; | ||
| import javafx.beans.property.BooleanProperty; | ||
| import javafx.beans.property.ReadOnlyBooleanProperty; | ||
| import javafx.beans.property.ReadOnlyBooleanWrapper; | ||
| import javafx.beans.property.SimpleBooleanProperty; | ||
| import javafx.event.Event; | ||
| -import javafx.scene.control.Alert; | ||
| import javafx.scene.control.SplitPane; | ||
| import javafx.scene.control.Tab; | ||
| import org.fxmisc.wellbehaved.event.EventPattern; | ||
| import org.fxmisc.wellbehaved.event.InputMap; | ||
| +import org.mozilla.universalchardet.UniversalDetector; | ||
| /** | ||
| private EditorPane editorPane; | ||
| private HTMLPreviewPane previewPane; | ||
| + | ||
| + /** | ||
| + * Character encoding used by the file (or default encoding if none found). | ||
| + */ | ||
| + private Charset encoding; | ||
| private final ReadOnlyBooleanWrapper modified = new ReadOnlyBooleanWrapper(); | ||
| private void updateTab() { | ||
| - final Path filePath = getPath(); | ||
| - | ||
| - setText( getFilename( filePath ) ); | ||
| + setText( getTabTitle() ); | ||
| setGraphic( getModifiedMark() ); | ||
| - setTooltip( getTooltip( filePath ) ); | ||
| + setTooltip( getTabTooltip() ); | ||
| } | ||
| - private String getFilename( final Path filePath ) { | ||
| + /** | ||
| + * Returns the base filename (without the directory names). | ||
| + * | ||
| + * @return The untitled text if the path hasn't been set. | ||
| + */ | ||
| + private String getTabTitle() { | ||
| + final Path filePath = getPath(); | ||
| + | ||
| return (filePath == null) | ||
| ? Messages.get( "FileEditor.untitled" ) | ||
| : filePath.getFileName().toString(); | ||
| } | ||
| - private Tooltip getTooltip( final Path filePath ) { | ||
| + /** | ||
| + * Returns the full filename represented by the path. | ||
| + * | ||
| + * @return The untitled text if the path hasn't been set. | ||
| + */ | ||
| + private Tooltip getTabTooltip() { | ||
| + final Path filePath = getPath(); | ||
| + | ||
| return (filePath == null) | ||
| ? null | ||
| : new Tooltip( filePath.toString() ); | ||
| } | ||
| + /** | ||
| + * Returns a marker to indicate whether the file has been modified. | ||
| + * | ||
| + * @return "*" when the file has changed; otherwise null. | ||
| + */ | ||
| private Text getModifiedMark() { | ||
| return isModified() ? new Text( "*" ) : null; | ||
| } | ||
| /** | ||
| * Called when the user switches tab. | ||
| */ | ||
| private void activated() { | ||
| + // Tab is closed or no longer active. | ||
| if( getTabPane() == null || !isSelected() ) { | ||
| - // Tab is closed or no longer active. | ||
| return; | ||
| } | ||
| + // Switch to the tab without loading if the contents are already in memory. | ||
| if( getContent() != null ) { | ||
| getEditorPane().requestFocus(); | ||
| return; | ||
| } | ||
| // Load the text and update the preview before the undo manager. | ||
| load(); | ||
| - // Track undo requests (must not be called before load). | ||
| + // Track undo requests (*must* be called after load). | ||
| initUndoManager(); | ||
| initSplitPane(); | ||
| + initFocus(); | ||
| } | ||
| public void initSplitPane() { | ||
| final EditorPane editor = getEditorPane(); | ||
| final HTMLPreviewPane preview = getPreviewPane(); | ||
| - | ||
| final VirtualizedScrollPane<StyleClassedTextArea> editorScrollPane = editor.getScrollPane(); | ||
| // Make the preview pane scroll correspond to the editor pane scroll. | ||
| // Separate the edit and preview panels. | ||
| - final SplitPane splitPane = new SplitPane( | ||
| - editorScrollPane, | ||
| - preview.getWebView() ); | ||
| - setContent( splitPane ); | ||
| + setContent( new SplitPane( editorScrollPane, preview.getNode() ) ); | ||
| + } | ||
| - // Let the user edit. | ||
| - editor.requestFocus(); | ||
| + private void initFocus() { | ||
| + getEditorPane().requestFocus(); | ||
| } | ||
| } | ||
| - void load() { | ||
| + /** | ||
| + * Returns true if the given path exactly matches this tab's path. | ||
| + * | ||
| + * @param check The path to compare against. | ||
| + * | ||
| + * @return true The paths are the same. | ||
| + */ | ||
| + public boolean isPath( final Path check ) { | ||
| final Path filePath = getPath(); | ||
| - if( filePath != null ) { | ||
| - try { | ||
| - final byte[] bytes = Files.readAllBytes( filePath ); | ||
| - String markdown; | ||
| + return filePath == null ? false : filePath.equals( check ); | ||
| + } | ||
| - try { | ||
| - markdown = new String( bytes, getOptions().getEncoding() ); | ||
| - } catch( Exception e ) { | ||
| - // Unsupported encodings and null pointers fallback here. | ||
| - markdown = new String( bytes ); | ||
| - } | ||
| + /** | ||
| + * Reads the entire file contents from the path associated with this tab. | ||
| + */ | ||
| + private void load() { | ||
| + final Path filePath = getPath(); | ||
| - getEditorPane().setText( markdown ); | ||
| - } catch( IOException ex ) { | ||
| - final AlertMessage message = getAlertService().createAlertMessage( | ||
| - Messages.get( "FileEditor.loadFailed.title" ), | ||
| - Messages.get( "FileEditor.loadFailed.message" ), | ||
| - filePath, | ||
| - ex.getMessage() | ||
| + if( filePath != null ) { | ||
| + try { | ||
| + getEditorPane().setText( asString( Files.readAllBytes( filePath ) ) ); | ||
| + } catch( Exception ex ) { | ||
| + alert( | ||
| + "FileEditor.loadFailed.title", "FileEditor.loadFailed.message", ex | ||
| ); | ||
| - | ||
| - final Alert alert = getAlertService().createAlertError( message ); | ||
| - | ||
| - alert.showAndWait(); | ||
| } | ||
| } | ||
| } | ||
| - | ||
| - boolean save() { | ||
| - final String text = getEditorPane().getText(); | ||
| - | ||
| - byte[] bytes; | ||
| - | ||
| - try { | ||
| - bytes = text.getBytes( getOptions().getEncoding() ); | ||
| - } catch( Exception ex ) { | ||
| - bytes = text.getBytes(); | ||
| - } | ||
| + /** | ||
| + * Saves the entire file contents from the path associated with this tab. | ||
| + * | ||
| + * @return true The file has been saved. | ||
| + */ | ||
| + public boolean save() { | ||
| try { | ||
| - Files.write( getPath(), bytes ); | ||
| + Files.write( getPath(), asBytes( getEditorPane().getText() ) ); | ||
| getEditorPane().getUndoManager().mark(); | ||
| return true; | ||
| - } catch( IOException ex ) { | ||
| - final AlertService service = getAlertService(); | ||
| - final AlertMessage message = service.createAlertMessage( | ||
| - Messages.get( "FileEditor.saveFailed.title" ), | ||
| - Messages.get( "FileEditor.saveFailed.message" ), | ||
| - getPath(), | ||
| - ex.getMessage() | ||
| + } catch( Exception ex ) { | ||
| + return alert( | ||
| + "FileEditor.saveFailed.title", "FileEditor.saveFailed.message", ex | ||
| ); | ||
| + } | ||
| + } | ||
| - final Alert alert = service.createAlertError( message ); | ||
| + /** | ||
| + * Creates an alert dialog and waits for it to close. | ||
| + * | ||
| + * @param titleKey Resource bundle key for the alert dialog title. | ||
| + * @param messageKey Resource bundle key for the alert dialog message. | ||
| + * @param e The unexpected happening. | ||
| + * | ||
| + * @return false | ||
| + */ | ||
| + private boolean alert( String titleKey, String messageKey, Exception e ) { | ||
| + final AlertService service = getAlertService(); | ||
| - alert.showAndWait(); | ||
| - return false; | ||
| - } | ||
| + final AlertMessage message = service.createAlertMessage( | ||
| + Messages.get( titleKey ), | ||
| + Messages.get( messageKey ), | ||
| + getPath(), | ||
| + e.getMessage() | ||
| + ); | ||
| + | ||
| + service.createAlertError( message ).showAndWait(); | ||
| + return false; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns a best guess at the file encoding. If the encoding could not be | ||
| + * detected, this will return the default charset for the JVM. | ||
| + * | ||
| + * @param bytes The bytes to perform character encoding detection. | ||
| + * | ||
| + * @return The character encoding. | ||
| + */ | ||
| + private Charset detectEncoding( final byte[] bytes ) { | ||
| + final UniversalDetector detector = new UniversalDetector( null ); | ||
| + detector.handleData( bytes, 0, bytes.length ); | ||
| + detector.dataEnd(); | ||
| + | ||
| + final String charset = detector.getDetectedCharset(); | ||
| + final Charset charEncoding = charset == null | ||
| + ? Charset.defaultCharset() | ||
| + : Charset.forName( charset.toUpperCase( ENGLISH ) ); | ||
| + | ||
| + detector.reset(); | ||
| + | ||
| + return charEncoding; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Converts the given string to an array of bytes using the encoding that was | ||
| + * originally detected (if any) and associated with this file. | ||
| + * | ||
| + * @param text The text to convert into the original file encoding. | ||
| + * | ||
| + * @return A series of bytes ready for writing to a file. | ||
| + */ | ||
| + private byte[] asBytes( final String text ) { | ||
| + return text.getBytes( getEncoding() ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Converts the given bytes into a Java String. This will call setEncoding | ||
| + * with the encoding detected by the CharsetDetector. | ||
| + * | ||
| + * @param text The text of unknown character encoding. | ||
| + * | ||
| + * @return The text, in its auto-detected encoding, as a String. | ||
| + */ | ||
| + private String asString( final byte[] text ) { | ||
| + setEncoding( detectEncoding( text ) ); | ||
| + return new String( text, getEncoding() ); | ||
| } | ||
| } | ||
| - boolean isModified() { | ||
| + public boolean isModified() { | ||
| return this.modified.get(); | ||
| } | ||
| } | ||
| + /** | ||
| + * Forwards the request to the editor pane. | ||
| + * | ||
| + * @param <T> The type of event listener to add. | ||
| + * @param <U> The type of consumer to add. | ||
| + * @param event The event that should trigger updates to the listener. | ||
| + * @param consumer The listener to receive update events. | ||
| + */ | ||
| public <T extends Event, U extends T> void addEventListener( | ||
| final EventPattern<? super T, ? extends U> event, | ||
| final Consumer<? super U> consumer ) { | ||
| getEditorPane().addEventListener( event, consumer ); | ||
| } | ||
| + /** | ||
| + * Forwards to the editor pane's listeners for keyboard events. | ||
| + * | ||
| + * @param map The new input map to replace the existing keyboard listener. | ||
| + */ | ||
| public void addEventListener( final InputMap<InputEvent> map ) { | ||
| getEditorPane().addEventListener( map ); | ||
| } | ||
| + /** | ||
| + * Forwards to the editor pane's listeners for keyboard events. | ||
| + * | ||
| + * @param map The existing input map to remove from the keyboard listeners. | ||
| + */ | ||
| public void removeEventListener( final InputMap<InputEvent> map ) { | ||
| getEditorPane().removeEventListener( map ); | ||
| } | ||
| + /** | ||
| + * Returns the editor pane, or creates one if it doesn't yet exist. | ||
| + * | ||
| + * @return The editor pane, never null. | ||
| + */ | ||
| protected EditorPane getEditorPane() { | ||
| if( this.editorPane == null ) { | ||
| return this.previewPane; | ||
| + } | ||
| + | ||
| + private Charset getEncoding() { | ||
| + return this.encoding; | ||
| + } | ||
| + | ||
| + private void setEncoding( final Charset encoding ) { | ||
| + this.encoding = encoding; | ||
| } | ||
| } | ||
| private List<FileEditorTab> openEditors( final List<File> files, final int activeIndex ) { | ||
| - final List<FileEditorTab> editors = new ArrayList<>(); | ||
| + final int fileTally = files.size(); | ||
| + final List<FileEditorTab> editors = new ArrayList<>( fileTally ); | ||
| final List<Tab> tabs = getTabs(); | ||
| - | ||
| + | ||
| // Close single unmodified "Untitled" tab. | ||
| if( tabs.size() == 1 ) { | ||
| final FileEditorTab fileEditor = (FileEditorTab)(tabs.get( 0 ).getUserData()); | ||
| if( fileEditor.getPath() == null && !fileEditor.isModified() ) { | ||
| closeEditor( fileEditor, false ); | ||
| } | ||
| } | ||
| - for( int i = 0; i < files.size(); i++ ) { | ||
| - Path path = files.get( i ).toPath(); | ||
| + for( int i = 0; i < fileTally; i++ ) { | ||
| + final Path path = files.get( i ).toPath(); | ||
| // Check whether file is already opened. | ||
| boolean saveAllEditors() { | ||
| - final FileEditorTab[] allEditors = getAllEditors(); | ||
| - | ||
| boolean success = true; | ||
| - for( FileEditorTab fileEditor : allEditors ) { | ||
| + | ||
| + for( FileEditorTab fileEditor : getAllEditors() ) { | ||
| if( !saveEditor( fileEditor ) ) { | ||
| success = false; | ||
| Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT ); | ||
| Event.fireEvent( tab, event ); | ||
| + | ||
| if( event.isConsumed() ) { | ||
| return false; | ||
| } | ||
| } | ||
| getTabs().remove( tab ); | ||
| + | ||
| if( tab.getOnClosed() != null ) { | ||
| Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) ); | ||
| } | ||
| return true; | ||
| } | ||
| boolean closeAllEditors() { | ||
| - FileEditorTab[] allEditors = getAllEditors(); | ||
| - FileEditorTab activeEditor = activeFileEditor.get(); | ||
| + final FileEditorTab[] allEditors = getAllEditors(); | ||
| + final FileEditorTab activeEditor = getActiveFileEditor(); | ||
| // try to save active tab first because in case the user decides to cancel, | ||
| // then it stays active | ||
| if( activeEditor != null && !canCloseEditor( activeEditor ) ) { | ||
| return false; | ||
| } | ||
| + | ||
| + // This should be called any time a tab changes. | ||
| + persistPreferences(); | ||
| // save modified tabs | ||
| for( int i = 0; i < allEditors.length; i++ ) { | ||
| - FileEditorTab fileEditor = allEditors[ i ]; | ||
| + final FileEditorTab fileEditor = allEditors[ i ]; | ||
| + | ||
| if( fileEditor == activeEditor ) { | ||
| continue; | ||
| } | ||
| } | ||
| - | ||
| - saveState( allEditors, activeEditor ); | ||
| return getTabs().isEmpty(); | ||
| } | ||
| - private FileEditorTab findEditor( Path path ) { | ||
| + /** | ||
| + * Returns the file editor tab that has the given path. | ||
| + * | ||
| + * @return null No file editor tab for the given path was found. | ||
| + */ | ||
| + private FileEditorTab findEditor( final Path path ) { | ||
| for( final Tab tab : getTabs() ) { | ||
| - final FileEditorTab fileEditor = (FileEditorTab)tab.getUserData(); | ||
| + final FileEditorTab fileEditor = (FileEditorTab)tab; | ||
| - if( path.equals( fileEditor.getPath() ) ) { | ||
| + System.out.println( "path = " + path ); | ||
| + System.out.println( "fileEditor = " + fileEditor.isPath( path ) ); | ||
| + | ||
| + if( fileEditor.isPath( path ) ) { | ||
| return fileEditor; | ||
| } | ||
| } | ||
| return null; | ||
| - } | ||
| - | ||
| - private Settings getSettings() { | ||
| - return this.settings; | ||
| } | ||
| } | ||
| - private void saveLastDirectory( File file ) { | ||
| + private void saveLastDirectory( final File file ) { | ||
| getState().put( "lastDirectory", file.getParent() ); | ||
| } | ||
| - public void restoreState() { | ||
| + public void restorePreferences() { | ||
| int activeIndex = 0; | ||
| - final Preferences state = getState(); | ||
| - final String[] fileNames = Utils.getPrefsStrings( state, "file" ); | ||
| - final String activeFileName = state.get( "activeFile", null ); | ||
| + final Preferences preferences = getState(); | ||
| + final String[] fileNames = Utils.getPrefsStrings( preferences, "file" ); | ||
| + final String activeFileName = preferences.get( "activeFile", null ); | ||
| final ArrayList<File> files = new ArrayList<>( fileNames.length ); | ||
| } | ||
| - private void saveState( final FileEditorTab[] allEditors, final FileEditorTab activeEditor ) { | ||
| - final List<String> fileNames = new ArrayList<>( allEditors.length ); | ||
| + public void persistPreferences() { | ||
| + final ObservableList<Tab> allEditors = getTabs(); | ||
| + final List<String> fileNames = new ArrayList<>( allEditors.size() ); | ||
| - for( final FileEditorTab fileEditor : allEditors ) { | ||
| + for( final Tab tab : allEditors ) { | ||
| + final FileEditorTab fileEditor = (FileEditorTab)tab; | ||
| + | ||
| if( fileEditor.getPath() != null ) { | ||
| fileNames.add( fileEditor.getPath().toString() ); | ||
| } | ||
| } | ||
| - final Preferences state = getState(); | ||
| - Utils.putPrefsStrings( state, "file", fileNames.toArray( new String[ fileNames.size() ] ) ); | ||
| + final Preferences preferences = getState(); | ||
| + Utils.putPrefsStrings( preferences, "file", fileNames.toArray( new String[ fileNames.size() ] ) ); | ||
| + | ||
| + final FileEditorTab activeEditor = getActiveFileEditor(); | ||
| if( activeEditor != null && activeEditor.getPath() != null ) { | ||
| - state.put( "activeFile", activeEditor.getPath().toString() ); | ||
| + preferences.put( "activeFile", activeEditor.getPath().toString() ); | ||
| } else { | ||
| - state.remove( "activeFile" ); | ||
| + preferences.remove( "activeFile" ); | ||
| } | ||
| } | ||
| - private Window getWindow() { | ||
| - return getScene().getWindow(); | ||
| + private Settings getSettings() { | ||
| + return this.settings; | ||
| } | ||
| protected Options getOptions() { | ||
| return this.options; | ||
| + } | ||
| + | ||
| + private Window getWindow() { | ||
| + return getScene().getWindow(); | ||
| } | ||
| protected Preferences getState() { | ||
| return getOptions().getState(); | ||
| } | ||
| - | ||
| } | ||
| import com.scrivenvar.editor.MarkdownEditorPane; | ||
| import com.scrivenvar.editor.VariableNameInjector; | ||
| -import com.scrivenvar.options.OptionsDialog; | ||
| -import com.scrivenvar.preview.HTMLPreviewPane; | ||
| -import com.scrivenvar.processors.HTMLPreviewProcessor; | ||
| -import com.scrivenvar.processors.MarkdownCaretInsertionProcessor; | ||
| -import com.scrivenvar.processors.MarkdownCaretReplacementProcessor; | ||
| -import com.scrivenvar.processors.MarkdownProcessor; | ||
| -import com.scrivenvar.processors.Processor; | ||
| -import com.scrivenvar.processors.TextChangeProcessor; | ||
| -import com.scrivenvar.processors.VariableProcessor; | ||
| -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 com.scrivenvar.yaml.YamlParser; | ||
| -import com.scrivenvar.yaml.YamlTreeAdapter; | ||
| -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.io.IOException; | ||
| -import java.io.InputStream; | ||
| -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 javafx.event.Event; | ||
| -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 org.fxmisc.richtext.StyleClassedTextArea; | ||
| - | ||
| -/** | ||
| - * 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 TreeView<String> treeView; | ||
| - private FileEditorTabPane fileEditorPane; | ||
| - private DefinitionPane definitionPane; | ||
| - | ||
| - private VariableNameInjector variableNameInjector; | ||
| - | ||
| - private YamlTreeAdapter yamlTreeAdapter; | ||
| - private YamlParser yamlParser; | ||
| - | ||
| - private MenuBar menuBar; | ||
| - | ||
| - public MainWindow() { | ||
| - initLayout(); | ||
| - initVariableNameInjector(); | ||
| - } | ||
| - | ||
| - private void initLayout() { | ||
| - final SplitPane splitPane = new SplitPane( | ||
| - getDefinitionPane().getNode(), | ||
| - getFileEditorPane().getNode() ); | ||
| - | ||
| - splitPane.setDividerPositions( | ||
| - getFloat( K_PANE_SPLIT_DEFINITION, .05f ), | ||
| - getFloat( K_PANE_SPLIT_EDITOR, .95f ) ); | ||
| - | ||
| - // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restore-splitpane.html | ||
| - final BorderPane borderPane = new BorderPane(); | ||
| - borderPane.setPrefSize( 1024, 800 ); | ||
| - borderPane.setTop( createMenuBar() ); | ||
| - borderPane.setCenter( splitPane ); | ||
| - | ||
| - final Scene appScene = new Scene( borderPane ); | ||
| - setScene( appScene ); | ||
| - appScene.getStylesheets().add( Constants.STYLESHEET_PREVIEW ); | ||
| - appScene.windowProperty().addListener( | ||
| - (observable, oldWindow, newWindow) -> { | ||
| - newWindow.setOnCloseRequest( e -> { | ||
| - if( !getFileEditorPane().closeAllEditors() ) { | ||
| - e.consume(); | ||
| - } | ||
| - } ); | ||
| - | ||
| - // Workaround JavaFX bug: deselect menubar if window loses focus. | ||
| - newWindow.focusedProperty().addListener( | ||
| - (obs, oldFocused, newFocused) -> { | ||
| - if( !newFocused ) { | ||
| - // Send an ESC key event to the menubar | ||
| - this.menuBar.fireEvent( | ||
| - new KeyEvent( | ||
| - KEY_PRESSED, CHAR_UNDEFINED, "", ESCAPE, | ||
| - false, false, false, false ) ); | ||
| - } | ||
| - } ); | ||
| - } ); | ||
| - } | ||
| - | ||
| - private void initVariableNameInjector() { | ||
| - setVariableNameInjector( new VariableNameInjector( | ||
| - getFileEditorPane(), | ||
| - getDefinitionPane() ) | ||
| - ); | ||
| - } | ||
| - | ||
| - private Window getWindow() { | ||
| - return getScene().getWindow(); | ||
| - } | ||
| - | ||
| - public Scene getScene() { | ||
| - return this.scene; | ||
| - } | ||
| - | ||
| - private void setScene( Scene scene ) { | ||
| - this.scene = scene; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Creates a boolean property that is bound to another boolean value of the | ||
| - * active editor. | ||
| - */ | ||
| - private BooleanProperty createActiveBooleanProperty( | ||
| - final Function<FileEditorTab, ObservableBooleanValue> func ) { | ||
| - | ||
| - final BooleanProperty b = new SimpleBooleanProperty(); | ||
| - final FileEditorTab tab = getActiveFileEditor(); | ||
| - | ||
| - if( tab != null ) { | ||
| - b.bind( func.apply( tab ) ); | ||
| - } | ||
| - | ||
| - getFileEditorPane().activeFileEditorProperty().addListener( | ||
| - (observable, oldFileEditor, newFileEditor) -> { | ||
| - b.unbind(); | ||
| - | ||
| - if( newFileEditor != null ) { | ||
| - b.bind( func.apply( newFileEditor ) ); | ||
| - } else { | ||
| - b.set( false ); | ||
| - } | ||
| - } ); | ||
| - | ||
| - return b; | ||
| - } | ||
| - | ||
| - //---- 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(); | ||
| - Event.fireEvent( window, | ||
| - new WindowEvent( window, WindowEvent.WINDOW_CLOSE_REQUEST ) ); | ||
| - } | ||
| - | ||
| - //---- Tools actions ------------------------------------------------------ | ||
| - private void toolsOptions() { | ||
| - new OptionsDialog( getWindow() ).showAndWait(); | ||
| - } | ||
| - | ||
| - //---- Help actions ------------------------------------------------------- | ||
| - private void helpAbout() { | ||
| - Alert alert = new Alert( AlertType.INFORMATION ); | ||
| - alert.setTitle( Messages.get( "Dialog.about.title" ) ); | ||
| - alert.setHeaderText( Messages.get( "Dialog.about.header" ) ); | ||
| - alert.setContentText( Messages.get( "Dialog.about.content" ) ); | ||
| - alert.setGraphic( new ImageView( new Image( LOGO_32 ) ) ); | ||
| - alert.initOwner( getWindow() ); | ||
| - | ||
| - alert.showAndWait(); | ||
| - } | ||
| - | ||
| - private FileEditorTabPane getFileEditorPane() { | ||
| - if( this.fileEditorPane == null ) { | ||
| - this.fileEditorPane = createFileEditorPane(); | ||
| - } | ||
| - | ||
| - return this.fileEditorPane; | ||
| - } | ||
| - | ||
| - private FileEditorTabPane createFileEditorPane() { | ||
| - // Create an editor pane to hold file editor tabs. | ||
| - final FileEditorTabPane editorPane = new FileEditorTabPane(); | ||
| - | ||
| - // Make sure the text processor kicks off when new files are opened. | ||
| - final ObservableList<Tab> tabs = editorPane.getTabs(); | ||
| - | ||
| - tabs.addListener( (Change<? extends Tab> change) -> { | ||
| - while( change.next() ) { | ||
| - if( change.wasAdded() ) { | ||
| - // Multiple tabs can be added simultaneously. | ||
| - for( final Tab tab : change.getAddedSubList() ) { | ||
| - addListener( (FileEditorTab)tab ); | ||
| - } | ||
| - } | ||
| - } | ||
| - } ); | ||
| - | ||
| - // After the processors are in place, restore the previously closed | ||
| - // tabs. Adding them will trigger the change event, above. | ||
| - editorPane.restoreState(); | ||
| - | ||
| - return editorPane; | ||
| - } | ||
| - | ||
| - private MarkdownEditorPane getActiveEditor() { | ||
| - return (MarkdownEditorPane)(getActiveFileEditor().getEditorPane()); | ||
| - } | ||
| - | ||
| - private FileEditorTab getActiveFileEditor() { | ||
| - return getFileEditorPane().getActiveFileEditor(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Listens for changes to tabs and their text editors. | ||
| - * | ||
| - * @see https://github.com/DaveJarvis/scrivenvar/issues/17 | ||
| - * @see https://github.com/DaveJarvis/scrivenvar/issues/18 | ||
| - * | ||
| - * @param tab The file editor tab that contains a text editor. | ||
| - */ | ||
| - private void addListener( FileEditorTab tab ) { | ||
| - final HTMLPreviewPane previewPane = tab.getPreviewPane(); | ||
| - final EditorPane editorPanel = tab.getEditorPane(); | ||
| - final StyleClassedTextArea editor = editorPanel.getEditor(); | ||
| - | ||
| - // TODO: Use a factory based on the filename extension. The default | ||
| - // extension will be for a markdown file (e.g., on file new). | ||
| - final Processor<String> hpp = new HTMLPreviewProcessor( previewPane ); | ||
| - final Processor<String> mcrp = new MarkdownCaretReplacementProcessor( hpp ); | ||
| - final Processor<String> mp = new MarkdownProcessor( mcrp ); | ||
| - final Processor<String> mcip = new MarkdownCaretInsertionProcessor( mp, editor ); | ||
| - final Processor<String> vnp = new VariableProcessor( mcip, getResolvedMap() ); | ||
| - final TextChangeProcessor tp = new TextChangeProcessor( vnp ); | ||
| - | ||
| - editorPanel.addChangeListener( tp ); | ||
| - editorPanel.addCaretParagraphListener( | ||
| - (final ObservableValue<? extends Integer> observable, | ||
| - final Integer oldValue, final Integer newValue) -> { | ||
| - | ||
| - // Kick off the processing chain at the variable processor when the | ||
| - // cursor changes paragraphs. This might cause some slight duplication | ||
| - // when the Enter key is pressed. | ||
| - vnp.processChain( editor.getText() ); | ||
| - } ); | ||
| - } | ||
| - | ||
| - protected DefinitionPane createDefinitionPane() { | ||
| - return new DefinitionPane( getTreeView() ); | ||
| - } | ||
| - | ||
| - private DefinitionPane getDefinitionPane() { | ||
| - if( this.definitionPane == null ) { | ||
| - this.definitionPane = createDefinitionPane(); | ||
| - } | ||
| - | ||
| - return this.definitionPane; | ||
| - } | ||
| - | ||
| - public MenuBar getMenuBar() { | ||
| - return menuBar; | ||
| - } | ||
| - | ||
| - public void setMenuBar( MenuBar menuBar ) { | ||
| - this.menuBar = menuBar; | ||
| - } | ||
| - | ||
| - public VariableNameInjector getVariableNameInjector() { | ||
| - return this.variableNameInjector; | ||
| - } | ||
| - | ||
| - public void setVariableNameInjector( VariableNameInjector variableNameInjector ) { | ||
| - this.variableNameInjector = variableNameInjector; | ||
| - } | ||
| - | ||
| - private float getFloat( final String key, final float defaultValue ) { | ||
| - return getPreferences().getFloat( key, defaultValue ); | ||
| - } | ||
| - | ||
| - private Preferences getPreferences() { | ||
| - return getOptions().getState(); | ||
| - } | ||
| - | ||
| - private Options getOptions() { | ||
| - return this.options; | ||
| - } | ||
| - | ||
| - private synchronized TreeView<String> getTreeView() throws RuntimeException { | ||
| - if( this.treeView == null ) { | ||
| - try { | ||
| - this.treeView = createTreeView(); | ||
| - } catch( IOException ex ) { | ||
| - | ||
| - // TODO: Pop an error message. | ||
| - throw new RuntimeException( ex ); | ||
| - } | ||
| - } | ||
| - | ||
| - return this.treeView; | ||
| - } | ||
| - | ||
| - private InputStream asStream( final String resource ) { | ||
| - return getClass().getResourceAsStream( resource ); | ||
| - } | ||
| - | ||
| - private TreeView<String> createTreeView() throws IOException { | ||
| - // TODO: Associate variable file with path to current file. | ||
| - return getYamlTreeAdapter().adapt( | ||
| - asStream( "/com/scrivenvar/variables.yaml" ), | ||
| - get( "Pane.defintion.node.root.title" ) | ||
| - ); | ||
| - } | ||
| - | ||
| - private Map<String, String> getResolvedMap() { | ||
| - return getYamlParser().createResolvedMap(); | ||
| - } | ||
| - | ||
| - private YamlTreeAdapter getYamlTreeAdapter() { | ||
| - if( this.yamlTreeAdapter == null ) { | ||
| - setYamlTreeAdapter( new YamlTreeAdapter( getYamlParser() ) ); | ||
| - } | ||
| - | ||
| - return this.yamlTreeAdapter; | ||
| - } | ||
| - | ||
| - private void setYamlTreeAdapter( final YamlTreeAdapter yamlTreeAdapter ) { | ||
| - this.yamlTreeAdapter = yamlTreeAdapter; | ||
| - } | ||
| - | ||
| - private YamlParser getYamlParser() { | ||
| - if( this.yamlParser == null ) { | ||
| - setYamlParser( new YamlParser() ); | ||
| - } | ||
| - | ||
| - return this.yamlParser; | ||
| - } | ||
| - | ||
| - private void setYamlParser( final YamlParser yamlParser ) { | ||
| - this.yamlParser = yamlParser; | ||
| - } | ||
| - | ||
| - private Node createMenuBar() { | ||
| - final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull(); | ||
| - | ||
| - // File actions | ||
| - Action fileNewAction = new Action( Messages.get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() ); | ||
| - Action fileOpenAction = new Action( Messages.get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() ); | ||
| - Action fileCloseAction = new Action( Messages.get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull ); | ||
| - Action fileCloseAllAction = new Action( Messages.get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull ); | ||
| - Action fileSaveAction = new Action( Messages.get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(), | ||
| - createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() ); | ||
| - Action fileSaveAllAction = new Action( Messages.get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(), | ||
| - Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) ); | ||
| - Action fileExitAction = new Action( Messages.get( "Main.menu.file.exit" ), null, null, e -> fileExit() ); | ||
| - | ||
| - // Edit actions | ||
| - Action editUndoAction = new Action( Messages.get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO, | ||
| - e -> getActiveEditor().undo(), | ||
| - createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() ); | ||
| - Action editRedoAction = new Action( Messages.get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT, | ||
| - e -> getActiveEditor().redo(), | ||
| - createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() ); | ||
| - | ||
| - // Insert actions | ||
| - Action insertBoldAction = new Action( Messages.get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD, | ||
| - e -> getActiveEditor().surroundSelection( "**", "**" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertItalicAction = new Action( Messages.get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC, | ||
| - e -> getActiveEditor().surroundSelection( "*", "*" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertStrikethroughAction = new Action( Messages.get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH, | ||
| - e -> getActiveEditor().surroundSelection( "~~", "~~" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertBlockquoteAction = new Action( Messages.get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac | ||
| - e -> getActiveEditor().surroundSelection( "\n\n> ", "" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertCodeAction = new Action( Messages.get( "Main.menu.insert.code" ), "Shortcut+K", CODE, | ||
| - e -> getActiveEditor().surroundSelection( "`", "`" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertFencedCodeBlockAction = new Action( Messages.get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT, | ||
| - e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", Messages.get( "Main.menu.insert.fenced_code_block.prompt" ) ), | ||
| - activeFileEditorIsNull ); | ||
| - | ||
| - Action insertLinkAction = new Action( Messages.get( "Main.menu.insert.link" ), "Shortcut+L", LINK, | ||
| - e -> getActiveEditor().insertLink(), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertImageAction = new Action( Messages.get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT, | ||
| - e -> getActiveEditor().insertImage(), | ||
| - activeFileEditorIsNull ); | ||
| - | ||
| - final Action[] headers = new Action[ 6 ]; | ||
| - | ||
| - // Insert header actions (H1 ... H6) | ||
| - for( int i = 1; i <= 6; i++ ) { | ||
| - final String hashes = new String( new char[ i ] ).replace( "\0", "#" ); | ||
| - final String markup = String.format( "\n\n%s ", hashes ); | ||
| - final String text = Messages.get( "Main.menu.insert.header_" + i ); | ||
| - final String accelerator = "Shortcut+" + i; | ||
| - final String prompt = Messages.get( "Main.menu.insert.header_" + i + ".prompt" ); | ||
| - | ||
| - headers[ i - 1 ] = new Action( text, accelerator, HEADER, | ||
| - e -> getActiveEditor().surroundSelection( markup, "", prompt ), | ||
| - activeFileEditorIsNull ); | ||
| - } | ||
| - | ||
| - Action insertUnorderedListAction = new Action( Messages.get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL, | ||
| - e -> getActiveEditor().surroundSelection( "\n\n* ", "" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertOrderedListAction = new Action( Messages.get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL, | ||
| - e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ), | ||
| - activeFileEditorIsNull ); | ||
| - Action insertHorizontalRuleAction = new Action( Messages.get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null, | ||
| - e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ), | ||
| - activeFileEditorIsNull ); | ||
| - | ||
| - // Tools actions | ||
| - Action toolsOptionsAction = new Action( Messages.get( "Main.menu.tools.options" ), "Shortcut+,", null, e -> toolsOptions() ); | ||
| - | ||
| - // Help actions | ||
| - Action helpAboutAction = new Action( Messages.get( "Main.menu.help.about" ), null, null, e -> helpAbout() ); | ||
| - | ||
| - //---- MenuBar ---- | ||
| - Menu fileMenu = ActionUtils.createMenu( Messages.get( "Main.menu.file" ), | ||
| - fileNewAction, | ||
| - fileOpenAction, | ||
| - null, | ||
| - fileCloseAction, | ||
| - fileCloseAllAction, | ||
| - null, | ||
| - fileSaveAction, | ||
| - fileSaveAllAction, | ||
| - null, | ||
| - fileExitAction ); | ||
| - | ||
| - Menu editMenu = ActionUtils.createMenu( Messages.get( "Main.menu.edit" ), | ||
| - editUndoAction, | ||
| - editRedoAction ); | ||
| - | ||
| - Menu insertMenu = ActionUtils.createMenu( Messages.get( "Main.menu.insert" ), | ||
| - insertBoldAction, | ||
| - insertItalicAction, | ||
| - insertStrikethroughAction, | ||
| - insertBlockquoteAction, | ||
| - insertCodeAction, | ||
| - insertFencedCodeBlockAction, | ||
| - null, | ||
| - insertLinkAction, | ||
| - insertImageAction, | ||
| - null, | ||
| - headers[ 0 ], | ||
| - headers[ 1 ], | ||
| - headers[ 2 ], | ||
| - headers[ 3 ], | ||
| - headers[ 4 ], | ||
| - headers[ 5 ], | ||
| - null, | ||
| - insertUnorderedListAction, | ||
| - insertOrderedListAction, | ||
| - insertHorizontalRuleAction ); | ||
| - | ||
| - Menu toolsMenu = ActionUtils.createMenu( Messages.get( "Main.menu.tools" ), | ||
| - toolsOptionsAction ); | ||
| - | ||
| - Menu helpMenu = ActionUtils.createMenu( Messages.get( "Main.menu.help" ), | ||
| - helpAboutAction ); | ||
| - | ||
| - menuBar = new MenuBar( fileMenu, editMenu, insertMenu, toolsMenu, helpMenu ); | ||
| +import com.scrivenvar.preview.HTMLPreviewPane; | ||
| +import com.scrivenvar.processors.HTMLPreviewProcessor; | ||
| +import com.scrivenvar.processors.MarkdownCaretInsertionProcessor; | ||
| +import com.scrivenvar.processors.MarkdownCaretReplacementProcessor; | ||
| +import com.scrivenvar.processors.MarkdownProcessor; | ||
| +import com.scrivenvar.processors.Processor; | ||
| +import com.scrivenvar.processors.TextChangeProcessor; | ||
| +import com.scrivenvar.processors.VariableProcessor; | ||
| +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 com.scrivenvar.yaml.YamlParser; | ||
| +import com.scrivenvar.yaml.YamlTreeAdapter; | ||
| +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.io.IOException; | ||
| +import java.io.InputStream; | ||
| +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 javafx.event.Event; | ||
| +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 org.fxmisc.richtext.StyleClassedTextArea; | ||
| +import static com.scrivenvar.Messages.get; | ||
| +import static com.scrivenvar.Messages.get; | ||
| +import static com.scrivenvar.Messages.get; | ||
| +import static com.scrivenvar.Messages.get; | ||
| +import static com.scrivenvar.Messages.get; | ||
| +import static com.scrivenvar.Messages.get; | ||
| +import static com.scrivenvar.Messages.get; | ||
| + | ||
| +/** | ||
| + * 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 TreeView<String> treeView; | ||
| + private FileEditorTabPane fileEditorPane; | ||
| + private DefinitionPane definitionPane; | ||
| + | ||
| + private VariableNameInjector variableNameInjector; | ||
| + | ||
| + private YamlTreeAdapter yamlTreeAdapter; | ||
| + private YamlParser yamlParser; | ||
| + | ||
| + private MenuBar menuBar; | ||
| + | ||
| + public MainWindow() { | ||
| + initLayout(); | ||
| + initVariableNameInjector(); | ||
| + } | ||
| + | ||
| + private void initLayout() { | ||
| + final SplitPane splitPane = new SplitPane( | ||
| + getDefinitionPane().getNode(), | ||
| + getFileEditorPane().getNode() ); | ||
| + | ||
| + splitPane.setDividerPositions( | ||
| + getFloat( K_PANE_SPLIT_DEFINITION, .05f ), | ||
| + getFloat( K_PANE_SPLIT_EDITOR, .95f ) ); | ||
| + | ||
| + // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html | ||
| + final BorderPane borderPane = new BorderPane(); | ||
| + borderPane.setPrefSize( 1024, 800 ); | ||
| + borderPane.setTop( createMenuBar() ); | ||
| + borderPane.setCenter( splitPane ); | ||
| + | ||
| + final Scene appScene = new Scene( borderPane ); | ||
| + setScene( appScene ); | ||
| + appScene.getStylesheets().add( Constants.STYLESHEET_PREVIEW ); | ||
| + appScene.windowProperty().addListener( | ||
| + (observable, oldWindow, newWindow) -> { | ||
| + newWindow.setOnCloseRequest( e -> { | ||
| + if( !getFileEditorPane().closeAllEditors() ) { | ||
| + e.consume(); | ||
| + } | ||
| + } ); | ||
| + | ||
| + // Workaround JavaFX bug: deselect menubar if window loses focus. | ||
| + newWindow.focusedProperty().addListener( | ||
| + (obs, oldFocused, newFocused) -> { | ||
| + if( !newFocused ) { | ||
| + // Send an ESC key event to the menubar | ||
| + this.menuBar.fireEvent( | ||
| + new KeyEvent( | ||
| + KEY_PRESSED, CHAR_UNDEFINED, "", ESCAPE, | ||
| + false, false, false, false ) ); | ||
| + } | ||
| + } ); | ||
| + } ); | ||
| + } | ||
| + | ||
| + private void initVariableNameInjector() { | ||
| + setVariableNameInjector( new VariableNameInjector( | ||
| + getFileEditorPane(), | ||
| + getDefinitionPane() ) | ||
| + ); | ||
| + } | ||
| + | ||
| + private Window getWindow() { | ||
| + return getScene().getWindow(); | ||
| + } | ||
| + | ||
| + public Scene getScene() { | ||
| + return this.scene; | ||
| + } | ||
| + | ||
| + private void setScene( Scene scene ) { | ||
| + this.scene = scene; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Creates a boolean property that is bound to another boolean value of the | ||
| + * active editor. | ||
| + */ | ||
| + private BooleanProperty createActiveBooleanProperty( | ||
| + final Function<FileEditorTab, ObservableBooleanValue> func ) { | ||
| + | ||
| + final BooleanProperty b = new SimpleBooleanProperty(); | ||
| + final FileEditorTab tab = getActiveFileEditor(); | ||
| + | ||
| + if( tab != null ) { | ||
| + b.bind( func.apply( tab ) ); | ||
| + } | ||
| + | ||
| + getFileEditorPane().activeFileEditorProperty().addListener( | ||
| + (observable, oldFileEditor, newFileEditor) -> { | ||
| + b.unbind(); | ||
| + | ||
| + if( newFileEditor != null ) { | ||
| + b.bind( func.apply( newFileEditor ) ); | ||
| + } else { | ||
| + b.set( false ); | ||
| + } | ||
| + } ); | ||
| + | ||
| + return b; | ||
| + } | ||
| + | ||
| + //---- 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(); | ||
| + Event.fireEvent( window, | ||
| + new WindowEvent( window, WindowEvent.WINDOW_CLOSE_REQUEST ) ); | ||
| + } | ||
| + | ||
| + //---- Help actions ------------------------------------------------------- | ||
| + private void helpAbout() { | ||
| + Alert alert = new Alert( AlertType.INFORMATION ); | ||
| + alert.setTitle( Messages.get( "Dialog.about.title" ) ); | ||
| + alert.setHeaderText( Messages.get( "Dialog.about.header" ) ); | ||
| + alert.setContentText( Messages.get( "Dialog.about.content" ) ); | ||
| + alert.setGraphic( new ImageView( new Image( LOGO_32 ) ) ); | ||
| + alert.initOwner( getWindow() ); | ||
| + | ||
| + alert.showAndWait(); | ||
| + } | ||
| + | ||
| + private FileEditorTabPane getFileEditorPane() { | ||
| + if( this.fileEditorPane == null ) { | ||
| + this.fileEditorPane = createFileEditorPane(); | ||
| + } | ||
| + | ||
| + return this.fileEditorPane; | ||
| + } | ||
| + | ||
| + private FileEditorTabPane createFileEditorPane() { | ||
| + // Create an editor pane to hold file editor tabs. | ||
| + final FileEditorTabPane editorPane = new FileEditorTabPane(); | ||
| + | ||
| + // Make sure the text processor kicks off when new files are opened. | ||
| + final ObservableList<Tab> tabs = editorPane.getTabs(); | ||
| + | ||
| + tabs.addListener( (Change<? extends Tab> change) -> { | ||
| + while( change.next() ) { | ||
| + if( change.wasAdded() ) { | ||
| + // Multiple tabs can be added simultaneously. | ||
| + for( final Tab tab : change.getAddedSubList() ) { | ||
| + addListener( (FileEditorTab)tab ); | ||
| + } | ||
| + } | ||
| + } | ||
| + } ); | ||
| + | ||
| + // After the processors are in place, restorePreferences the previously closed | ||
| + // tabs. Adding them will trigger the change event, above. | ||
| + editorPane.restorePreferences(); | ||
| + | ||
| + return editorPane; | ||
| + } | ||
| + | ||
| + private MarkdownEditorPane getActiveEditor() { | ||
| + return (MarkdownEditorPane)(getActiveFileEditor().getEditorPane()); | ||
| + } | ||
| + | ||
| + private FileEditorTab getActiveFileEditor() { | ||
| + return getFileEditorPane().getActiveFileEditor(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Listens for changes to tabs and their text editors. | ||
| + * | ||
| + * @see https://github.com/DaveJarvis/scrivenvar/issues/17 | ||
| + * @see https://github.com/DaveJarvis/scrivenvar/issues/18 | ||
| + * | ||
| + * @param tab The file editor tab that contains a text editor. | ||
| + */ | ||
| + private void addListener( FileEditorTab tab ) { | ||
| + final HTMLPreviewPane previewPane = tab.getPreviewPane(); | ||
| + final EditorPane editorPanel = tab.getEditorPane(); | ||
| + final StyleClassedTextArea editor = editorPanel.getEditor(); | ||
| + | ||
| + // TODO: Use a factory based on the filename extension. The default | ||
| + // extension will be for a markdown file (e.g., on file new). | ||
| + final Processor<String> hpp = new HTMLPreviewProcessor( previewPane ); | ||
| + final Processor<String> mcrp = new MarkdownCaretReplacementProcessor( hpp ); | ||
| + final Processor<String> mp = new MarkdownProcessor( mcrp ); | ||
| + final Processor<String> mcip = new MarkdownCaretInsertionProcessor( mp, editor ); | ||
| + final Processor<String> vnp = new VariableProcessor( mcip, getResolvedMap() ); | ||
| + final TextChangeProcessor tp = new TextChangeProcessor( vnp ); | ||
| + | ||
| + editorPanel.addChangeListener( tp ); | ||
| + editorPanel.addCaretParagraphListener( | ||
| + (final ObservableValue<? extends Integer> observable, | ||
| + final Integer oldValue, final Integer newValue) -> { | ||
| + | ||
| + // Kick off the processing chain at the variable processor when the | ||
| + // cursor changes paragraphs. This might cause some slight duplication | ||
| + // when the Enter key is pressed. | ||
| + vnp.processChain( editor.getText() ); | ||
| + } ); | ||
| + } | ||
| + | ||
| + protected DefinitionPane createDefinitionPane() { | ||
| + return new DefinitionPane( getTreeView() ); | ||
| + } | ||
| + | ||
| + private DefinitionPane getDefinitionPane() { | ||
| + if( this.definitionPane == null ) { | ||
| + this.definitionPane = createDefinitionPane(); | ||
| + } | ||
| + | ||
| + return this.definitionPane; | ||
| + } | ||
| + | ||
| + public MenuBar getMenuBar() { | ||
| + return menuBar; | ||
| + } | ||
| + | ||
| + public void setMenuBar( MenuBar menuBar ) { | ||
| + this.menuBar = menuBar; | ||
| + } | ||
| + | ||
| + public VariableNameInjector getVariableNameInjector() { | ||
| + return this.variableNameInjector; | ||
| + } | ||
| + | ||
| + public void setVariableNameInjector( VariableNameInjector variableNameInjector ) { | ||
| + this.variableNameInjector = variableNameInjector; | ||
| + } | ||
| + | ||
| + private float getFloat( final String key, final float defaultValue ) { | ||
| + return getPreferences().getFloat( key, defaultValue ); | ||
| + } | ||
| + | ||
| + private Preferences getPreferences() { | ||
| + return getOptions().getState(); | ||
| + } | ||
| + | ||
| + private Options getOptions() { | ||
| + return this.options; | ||
| + } | ||
| + | ||
| + private synchronized TreeView<String> getTreeView() throws RuntimeException { | ||
| + if( this.treeView == null ) { | ||
| + try { | ||
| + this.treeView = createTreeView(); | ||
| + } catch( IOException ex ) { | ||
| + | ||
| + // TODO: Pop an error message. | ||
| + throw new RuntimeException( ex ); | ||
| + } | ||
| + } | ||
| + | ||
| + return this.treeView; | ||
| + } | ||
| + | ||
| + private InputStream asStream( final String resource ) { | ||
| + return getClass().getResourceAsStream( resource ); | ||
| + } | ||
| + | ||
| + private TreeView<String> createTreeView() throws IOException { | ||
| + // TODO: Associate variable file with path to current file. | ||
| + return getYamlTreeAdapter().adapt( | ||
| + asStream( "/com/scrivenvar/variables.yaml" ), | ||
| + get( "Pane.defintion.node.root.title" ) | ||
| + ); | ||
| + } | ||
| + | ||
| + private Map<String, String> getResolvedMap() { | ||
| + return getYamlParser().createResolvedMap(); | ||
| + } | ||
| + | ||
| + private YamlTreeAdapter getYamlTreeAdapter() { | ||
| + if( this.yamlTreeAdapter == null ) { | ||
| + setYamlTreeAdapter( new YamlTreeAdapter( getYamlParser() ) ); | ||
| + } | ||
| + | ||
| + return this.yamlTreeAdapter; | ||
| + } | ||
| + | ||
| + private void setYamlTreeAdapter( final YamlTreeAdapter yamlTreeAdapter ) { | ||
| + this.yamlTreeAdapter = yamlTreeAdapter; | ||
| + } | ||
| + | ||
| + private YamlParser getYamlParser() { | ||
| + if( this.yamlParser == null ) { | ||
| + setYamlParser( new YamlParser() ); | ||
| + } | ||
| + | ||
| + return this.yamlParser; | ||
| + } | ||
| + | ||
| + private void setYamlParser( final YamlParser yamlParser ) { | ||
| + this.yamlParser = yamlParser; | ||
| + } | ||
| + | ||
| + private Node createMenuBar() { | ||
| + final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull(); | ||
| + | ||
| + // File actions | ||
| + Action fileNewAction = new Action( Messages.get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() ); | ||
| + Action fileOpenAction = new Action( Messages.get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() ); | ||
| + Action fileCloseAction = new Action( Messages.get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull ); | ||
| + Action fileCloseAllAction = new Action( Messages.get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull ); | ||
| + Action fileSaveAction = new Action( Messages.get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(), | ||
| + createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() ); | ||
| + Action fileSaveAllAction = new Action( Messages.get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(), | ||
| + Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) ); | ||
| + Action fileExitAction = new Action( Messages.get( "Main.menu.file.exit" ), null, null, e -> fileExit() ); | ||
| + | ||
| + // Edit actions | ||
| + Action editUndoAction = new Action( Messages.get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO, | ||
| + e -> getActiveEditor().undo(), | ||
| + createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() ); | ||
| + Action editRedoAction = new Action( Messages.get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT, | ||
| + e -> getActiveEditor().redo(), | ||
| + createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() ); | ||
| + | ||
| + // Insert actions | ||
| + Action insertBoldAction = new Action( Messages.get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD, | ||
| + e -> getActiveEditor().surroundSelection( "**", "**" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertItalicAction = new Action( Messages.get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC, | ||
| + e -> getActiveEditor().surroundSelection( "*", "*" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertStrikethroughAction = new Action( Messages.get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH, | ||
| + e -> getActiveEditor().surroundSelection( "~~", "~~" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertBlockquoteAction = new Action( Messages.get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac | ||
| + e -> getActiveEditor().surroundSelection( "\n\n> ", "" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertCodeAction = new Action( Messages.get( "Main.menu.insert.code" ), "Shortcut+K", CODE, | ||
| + e -> getActiveEditor().surroundSelection( "`", "`" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertFencedCodeBlockAction = new Action( Messages.get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT, | ||
| + e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", Messages.get( "Main.menu.insert.fenced_code_block.prompt" ) ), | ||
| + activeFileEditorIsNull ); | ||
| + | ||
| + Action insertLinkAction = new Action( Messages.get( "Main.menu.insert.link" ), "Shortcut+L", LINK, | ||
| + e -> getActiveEditor().insertLink(), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertImageAction = new Action( Messages.get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT, | ||
| + e -> getActiveEditor().insertImage(), | ||
| + activeFileEditorIsNull ); | ||
| + | ||
| + final Action[] headers = new Action[ 6 ]; | ||
| + | ||
| + // Insert header actions (H1 ... H6) | ||
| + for( int i = 1; i <= 6; i++ ) { | ||
| + final String hashes = new String( new char[ i ] ).replace( "\0", "#" ); | ||
| + final String markup = String.format( "\n\n%s ", hashes ); | ||
| + final String text = Messages.get( "Main.menu.insert.header_" + i ); | ||
| + final String accelerator = "Shortcut+" + i; | ||
| + final String prompt = Messages.get( "Main.menu.insert.header_" + i + ".prompt" ); | ||
| + | ||
| + headers[ i - 1 ] = new Action( text, accelerator, HEADER, | ||
| + e -> getActiveEditor().surroundSelection( markup, "", prompt ), | ||
| + activeFileEditorIsNull ); | ||
| + } | ||
| + | ||
| + Action insertUnorderedListAction = new Action( Messages.get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL, | ||
| + e -> getActiveEditor().surroundSelection( "\n\n* ", "" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertOrderedListAction = new Action( Messages.get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL, | ||
| + e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ), | ||
| + activeFileEditorIsNull ); | ||
| + Action insertHorizontalRuleAction = new Action( Messages.get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null, | ||
| + e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ), | ||
| + activeFileEditorIsNull ); | ||
| + | ||
| + // Help actions | ||
| + Action helpAboutAction = new Action( Messages.get( "Main.menu.help.about" ), null, null, e -> helpAbout() ); | ||
| + | ||
| + //---- MenuBar ---- | ||
| + Menu fileMenu = ActionUtils.createMenu( Messages.get( "Main.menu.file" ), | ||
| + fileNewAction, | ||
| + fileOpenAction, | ||
| + null, | ||
| + fileCloseAction, | ||
| + fileCloseAllAction, | ||
| + null, | ||
| + fileSaveAction, | ||
| + fileSaveAllAction, | ||
| + null, | ||
| + fileExitAction ); | ||
| + | ||
| + Menu editMenu = ActionUtils.createMenu( Messages.get( "Main.menu.edit" ), | ||
| + editUndoAction, | ||
| + editRedoAction ); | ||
| + | ||
| + Menu insertMenu = ActionUtils.createMenu( Messages.get( "Main.menu.insert" ), | ||
| + insertBoldAction, | ||
| + insertItalicAction, | ||
| + insertStrikethroughAction, | ||
| + insertBlockquoteAction, | ||
| + insertCodeAction, | ||
| + insertFencedCodeBlockAction, | ||
| + null, | ||
| + insertLinkAction, | ||
| + insertImageAction, | ||
| + null, | ||
| + headers[ 0 ], | ||
| + headers[ 1 ], | ||
| + headers[ 2 ], | ||
| + headers[ 3 ], | ||
| + headers[ 4 ], | ||
| + headers[ 5 ], | ||
| + null, | ||
| + insertUnorderedListAction, | ||
| + insertOrderedListAction, | ||
| + insertHorizontalRuleAction ); | ||
| + | ||
| + Menu helpMenu = ActionUtils.createMenu( Messages.get( "Main.menu.help" ), | ||
| + helpAboutAction ); | ||
| + | ||
| + menuBar = new MenuBar( fileMenu, editMenu, insertMenu, helpMenu ); | ||
| //---- ToolBar ---- |
| import javafx.application.Platform; | ||
| import javafx.beans.property.ObjectProperty; | ||
| -import javafx.beans.property.ReadOnlyDoubleProperty; | ||
| -import javafx.beans.property.ReadOnlyDoubleWrapper; | ||
| import javafx.beans.property.SimpleObjectProperty; | ||
| import javafx.beans.value.ChangeListener; | ||
| private StyleClassedTextArea editor; | ||
| private VirtualizedScrollPane<StyleClassedTextArea> scrollPane; | ||
| - private final ReadOnlyDoubleWrapper scrollY = new ReadOnlyDoubleWrapper(); | ||
| private final ObjectProperty<Path> path = new SimpleObjectProperty<>(); | ||
| - | ||
| - private String lineSeparator = getLineSeparator(); | ||
| /** | ||
| public String getText() { | ||
| - String text = getEditor().getText(); | ||
| - | ||
| - if( !this.lineSeparator.equals( "\n" ) ) { | ||
| - text = text.replace( "\n", this.lineSeparator ); | ||
| - } | ||
| - | ||
| - return text; | ||
| + return getEditor().getText(); | ||
| } | ||
| public void setText( final String text ) { | ||
| - this.lineSeparator = determineLineSeparator( text ); | ||
| getEditor().deselect(); | ||
| getEditor().replaceText( text ); | ||
| protected StyleClassedTextArea createTextArea() { | ||
| return new StyleClassedTextArea( false ); | ||
| - } | ||
| - | ||
| - public double getScrollY() { | ||
| - return this.scrollY.get(); | ||
| - } | ||
| - | ||
| - protected void setScrollY( double scrolled ) { | ||
| - this.scrollY.set( scrolled ); | ||
| - } | ||
| - | ||
| - public ReadOnlyDoubleProperty scrollYProperty() { | ||
| - return this.scrollY.getReadOnlyProperty(); | ||
| } | ||
| public ObjectProperty<Path> pathProperty() { | ||
| return this.path; | ||
| - } | ||
| - | ||
| - private String getLineSeparator() { | ||
| - final String separator = getOptions().getLineSeparator(); | ||
| - | ||
| - return (separator != null) | ||
| - ? separator | ||
| - : System.lineSeparator(); | ||
| - } | ||
| - | ||
| - private String determineLineSeparator( final String s ) { | ||
| - final int length = s.length(); | ||
| - | ||
| - // TODO: Looping backwards will probably detect a newline sooner. | ||
| - for( int i = 0; i < length; i++ ) { | ||
| - char ch = s.charAt( i ); | ||
| - if( ch == '\n' ) { | ||
| - return (i > 0 && s.charAt( i - 1 ) == '\r') ? "\r\n" : "\n"; | ||
| - } | ||
| - } | ||
| - | ||
| - return getLineSeparator(); | ||
| } | ||
| } | ||
| import com.scrivenvar.dialogs.LinkDialog; | ||
| import com.scrivenvar.processors.MarkdownProcessor; | ||
| -import com.scrivenvar.util.Utils; | ||
| +import static com.scrivenvar.util.Utils.ltrim; | ||
| +import static com.scrivenvar.util.Utils.rtrim; | ||
| import com.vladsch.flexmark.ast.Link; | ||
| import com.vladsch.flexmark.ast.Node; | ||
| // remove leading whitespaces from leading text if selection starts at zero | ||
| if( start == 0 ) { | ||
| - leading = Utils.ltrim( leading ); | ||
| + leading = ltrim( leading ); | ||
| } | ||
| // remove trailing whitespaces from trailing text if selection ends at text end | ||
| if( end == textArea.getLength() ) { | ||
| - trailing = Utils.rtrim( trailing ); | ||
| + trailing = rtrim( trailing ); | ||
| } | ||
| import static org.fxmisc.wellbehaved.event.InputMap.consume; | ||
| import static org.fxmisc.wellbehaved.event.InputMap.sequence; | ||
| +import static com.scrivenvar.definition.Lists.getFirst; | ||
| +import static com.scrivenvar.definition.Lists.getLast; | ||
| +import static java.lang.Character.isSpaceChar; | ||
| +import static java.lang.Character.isWhitespace; | ||
| +import static java.lang.Math.min; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped; | ||
| +import static org.fxmisc.wellbehaved.event.InputMap.consume; | ||
| +import static com.scrivenvar.definition.Lists.getFirst; | ||
| +import static com.scrivenvar.definition.Lists.getLast; | ||
| +import static java.lang.Character.isSpaceChar; | ||
| +import static java.lang.Character.isWhitespace; | ||
| +import static java.lang.Math.min; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped; | ||
| +import static org.fxmisc.wellbehaved.event.InputMap.consume; | ||
| +import static com.scrivenvar.definition.Lists.getFirst; | ||
| +import static com.scrivenvar.definition.Lists.getLast; | ||
| +import static java.lang.Character.isSpaceChar; | ||
| +import static java.lang.Character.isWhitespace; | ||
| +import static java.lang.Math.min; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped; | ||
| +import static org.fxmisc.wellbehaved.event.InputMap.consume; | ||
| +import static com.scrivenvar.definition.Lists.getFirst; | ||
| +import static com.scrivenvar.definition.Lists.getLast; | ||
| +import static java.lang.Character.isSpaceChar; | ||
| +import static java.lang.Character.isWhitespace; | ||
| +import static java.lang.Math.min; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped; | ||
| +import static org.fxmisc.wellbehaved.event.InputMap.consume; | ||
| +import static com.scrivenvar.definition.Lists.getFirst; | ||
| +import static com.scrivenvar.definition.Lists.getLast; | ||
| +import static java.lang.Character.isSpaceChar; | ||
| +import static java.lang.Character.isWhitespace; | ||
| +import static java.lang.Math.min; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped; | ||
| +import static org.fxmisc.wellbehaved.event.InputMap.consume; | ||
| +import static com.scrivenvar.definition.Lists.getFirst; | ||
| +import static com.scrivenvar.definition.Lists.getLast; | ||
| +import static java.lang.Character.isSpaceChar; | ||
| +import static java.lang.Character.isWhitespace; | ||
| +import static java.lang.Math.min; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped; | ||
| +import static org.fxmisc.wellbehaved.event.InputMap.consume; | ||
| +import static com.scrivenvar.definition.Lists.getFirst; | ||
| +import static com.scrivenvar.definition.Lists.getLast; | ||
| +import static java.lang.Character.isSpaceChar; | ||
| +import static java.lang.Character.isWhitespace; | ||
| +import static java.lang.Math.min; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | ||
| +import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped; | ||
| +import static org.fxmisc.wellbehaved.event.InputMap.consume; | ||
| /** |
| -/* | ||
| - * 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. | ||
| - */ | ||
| -package com.scrivenvar.options; | ||
| - | ||
| -import com.scrivenvar.Messages; | ||
| -import com.scrivenvar.ui.AbstractPane; | ||
| -import com.scrivenvar.util.Item; | ||
| -import java.nio.charset.Charset; | ||
| -import java.util.ArrayList; | ||
| -import java.util.Collection; | ||
| -import java.util.SortedMap; | ||
| -import javafx.scene.control.ComboBox; | ||
| -import javafx.scene.control.Label; | ||
| - | ||
| -/** | ||
| - * General options pane. | ||
| - * | ||
| - * @author Karl Tauber | ||
| - */ | ||
| -public class GeneralOptionsPane extends AbstractPane { | ||
| - | ||
| - @SuppressWarnings( "unchecked" ) | ||
| - public GeneralOptionsPane() { | ||
| - initComponents(); | ||
| - | ||
| - String defaultLineSeparator = System.getProperty( "line.separator", "\n" ); | ||
| - String defaultLineSeparatorStr = defaultLineSeparator.replace( "\r", "CR" ).replace( "\n", "LF" ); | ||
| - lineSeparatorField.getItems().addAll( | ||
| - new Item<>( Messages.get( "GeneralOptionsPane.platformDefault", defaultLineSeparatorStr ), null ), | ||
| - new Item<>( Messages.get( "GeneralOptionsPane.sepWindows" ), "\r\n" ), | ||
| - new Item<>( Messages.get( "GeneralOptionsPane.sepUnix" ), "\n" ) ); | ||
| - | ||
| - encodingField.getItems().addAll( getAvailableEncodings() ); | ||
| - } | ||
| - | ||
| - private Collection<Item<String>> getAvailableEncodings() { | ||
| - SortedMap<String, Charset> availableCharsets = Charset.availableCharsets(); | ||
| - | ||
| - ArrayList<Item<String>> values = new ArrayList<>( 1 + availableCharsets.size() ); | ||
| - values.add( new Item<>( Messages.get( "GeneralOptionsPane.platformDefault", Charset.defaultCharset().name() ), null ) ); | ||
| - | ||
| - for( String name : availableCharsets.keySet() ) { | ||
| - values.add( new Item<>( name, name ) ); | ||
| - } | ||
| - | ||
| - return values; | ||
| - } | ||
| - | ||
| - void load() { | ||
| - lineSeparatorField.setValue( new Item<>( getOptions().getLineSeparator(), getOptions().getLineSeparator() ) ); | ||
| - encodingField.setValue( new Item<>( getOptions().getEncoding(), getOptions().getEncoding() ) ); | ||
| - | ||
| - } | ||
| - | ||
| - void save() { | ||
| - getOptions().setLineSeparator( lineSeparatorField.getValue().value ); | ||
| - getOptions().setEncoding( encodingField.getValue().value ); | ||
| - | ||
| - } | ||
| - | ||
| - private void initComponents() { | ||
| - // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents | ||
| - Label lineSeparatorLabel = new Label(); | ||
| - lineSeparatorField = new ComboBox<>(); | ||
| - Label lineSeparatorLabel2 = new Label(); | ||
| - Label encodingLabel = new Label(); | ||
| - encodingField = new ComboBox<>(); | ||
| - | ||
| - //======== this ======== | ||
| - setCols( "[fill][fill][fill]" ); | ||
| - setRows( "[][]para[]" ); | ||
| - | ||
| - //---- lineSeparatorLabel ---- | ||
| - lineSeparatorLabel.setText( Messages.get( "GeneralOptionsPane.lineSeparatorLabel.text" ) ); | ||
| - lineSeparatorLabel.setMnemonicParsing( true ); | ||
| - add( lineSeparatorLabel, "cell 0 0" ); | ||
| - add( lineSeparatorField, "cell 1 0" ); | ||
| - | ||
| - //---- lineSeparatorLabel2 ---- | ||
| - lineSeparatorLabel2.setText( Messages.get( "GeneralOptionsPane.lineSeparatorLabel2.text" ) ); | ||
| - add( lineSeparatorLabel2, "cell 2 0" ); | ||
| - | ||
| - //---- encodingLabel ---- | ||
| - encodingLabel.setText( Messages.get( "GeneralOptionsPane.encodingLabel.text" ) ); | ||
| - encodingLabel.setMnemonicParsing( true ); | ||
| - add( encodingLabel, "cell 0 1" ); | ||
| - | ||
| - //---- encodingField ---- | ||
| - encodingField.setVisibleRowCount( 20 ); | ||
| - add( encodingField, "cell 1 1" ); | ||
| - // JFormDesigner - End of component initialization //GEN-END:initComponents | ||
| - | ||
| - // TODO set this in JFormDesigner as soon as it supports labelFor | ||
| - lineSeparatorLabel.setLabelFor( lineSeparatorField ); | ||
| - encodingLabel.setLabelFor( encodingField ); | ||
| - } | ||
| - | ||
| - // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables | ||
| - private ComboBox<Item<String>> lineSeparatorField; | ||
| - private ComboBox<Item<String>> encodingField; | ||
| - // JFormDesigner - End of variables declaration //GEN-END:variables | ||
| -} | ||
| -JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8" | ||
| - | ||
| -new FormModel { | ||
| - "i18n.bundlePackage": "com.scrivendor" | ||
| - "i18n.bundleName": "messages" | ||
| - "i18n.autoExternalize": true | ||
| - "i18n.keyPrefix": "GeneralOptionsPane" | ||
| - contentType: "form/javafx" | ||
| - root: new FormRoot { | ||
| - add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) { | ||
| - "$layoutConstraints": "" | ||
| - "$columnConstraints": "[fill][fill][fill]" | ||
| - "$rowConstraints": "[][]para[]" | ||
| - } ) { | ||
| - name: "this" | ||
| - add( new FormComponent( "javafx.scene.control.Label" ) { | ||
| - name: "lineSeparatorLabel" | ||
| - "text": new FormMessage( null, "GeneralOptionsPane.lineSeparatorLabel.text" ) | ||
| - "mnemonicParsing": true | ||
| - auxiliary() { | ||
| - "JavaCodeGenerator.variableLocal": true | ||
| - } | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 0" | ||
| - } ) | ||
| - add( new FormComponent( "javafx.scene.control.ComboBox" ) { | ||
| - name: "lineSeparatorField" | ||
| - auxiliary() { | ||
| - "JavaCodeGenerator.typeParameters": "Item<String>" | ||
| - } | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 1 0" | ||
| - } ) | ||
| - add( new FormComponent( "javafx.scene.control.Label" ) { | ||
| - name: "lineSeparatorLabel2" | ||
| - "text": new FormMessage( null, "GeneralOptionsPane.lineSeparatorLabel2.text" ) | ||
| - auxiliary() { | ||
| - "JavaCodeGenerator.variableLocal": true | ||
| - } | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 2 0" | ||
| - } ) | ||
| - add( new FormComponent( "javafx.scene.control.Label" ) { | ||
| - name: "encodingLabel" | ||
| - "text": new FormMessage( null, "GeneralOptionsPane.encodingLabel.text" ) | ||
| - "mnemonicParsing": true | ||
| - auxiliary() { | ||
| - "JavaCodeGenerator.variableLocal": true | ||
| - } | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 1" | ||
| - } ) | ||
| - add( new FormComponent( "javafx.scene.control.ComboBox" ) { | ||
| - name: "encodingField" | ||
| - "visibleRowCount": 20 | ||
| - auxiliary() { | ||
| - "JavaCodeGenerator.typeParameters": "Item<String>" | ||
| - } | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 1 1" | ||
| - } ) | ||
| - }, new FormLayoutConstraints( null ) { | ||
| - "location": new javafx.geometry.Point2D( 0.0, 0.0 ) | ||
| - "size": new javafx.geometry.Dimension2D( 400.0, 300.0 ) | ||
| - } ) | ||
| - } | ||
| -} | ||
| -JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8" | ||
| - | ||
| -new FormModel { | ||
| - "i18n.bundlePackage": "com.scrivendor" | ||
| - "i18n.bundleName": "messages" | ||
| - "i18n.autoExternalize": true | ||
| - "i18n.keyPrefix": "MarkdownOptionsPane" | ||
| - contentType: "form/javafx" | ||
| - root: new FormRoot { | ||
| - add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) { | ||
| - "$rowConstraints": "[][][][][][][][][][][][][][][][][][]" | ||
| - "$columnConstraints": "[][fill]" | ||
| - } ) { | ||
| - name: "this" | ||
| - add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) { | ||
| - name: "smartsExtCheckBox" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.smartsExtCheckBox.text" ) | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 0" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) { | ||
| - name: "quotesExtCheckBox" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.quotesExtCheckBox.text" ) | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 1" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) { | ||
| - name: "abbreviationsExtCheckBox" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.abbreviationsExtCheckBox.text" ) | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 2" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.WebHyperlink" ) { | ||
| - name: "abbreviationsExtLink" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.abbreviationsExtLink.text" ) | ||
| - "uri": "http://michelf.com/projects/php-markdown/extra/#abbr" | ||
| - auxiliary() { | ||
| - "JavaCodeGenerator.variableLocal": true | ||
| - } | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 2,gapx 0" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) { | ||
| - name: "hardwrapsExtCheckBox" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.hardwrapsExtCheckBox.text" ) | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 3" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.WebHyperlink" ) { | ||
| - name: "hardwrapsExtLink" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.hardwrapsExtLink.text" ) | ||
| - "uri": "https://help.github.com/articles/writing-on-github/#markup" | ||
| - auxiliary() { | ||
| - "JavaCodeGenerator.variableLocal": true | ||
| - } | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 3,gapx 0" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) { | ||
| - name: "autolinksExtCheckBox" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.autolinksExtCheckBox.text" ) | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 4" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.WebHyperlink" ) { | ||
| - name: "autolinksExtLink" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.autolinksExtLink.text" ) | ||
| - "uri": "https://help.github.com/articles/github-flavored-markdown/#url-autolinking" | ||
| - auxiliary() { | ||
| - "JavaCodeGenerator.variableLocal": true | ||
| - } | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 4,gapx 0" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) { | ||
| - name: "tablesExtCheckBox" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.tablesExtCheckBox.text" ) | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 5" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.WebHyperlink" ) { | ||
| - name: "tablesExtLink" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.tablesExtLink.text" ) | ||
| - "uri": "http://fletcher.github.io/MultiMarkdown-4/syntax.html#tables" | ||
| - auxiliary() { | ||
| - "JavaCodeGenerator.variableLocal": true | ||
| - } | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 5,gapx 0" | ||
| - } ) | ||
| - add( new FormComponent( "javafx.scene.control.Label" ) { | ||
| - name: "tablesExtLabel" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.tablesExtLabel.text" ) | ||
| - auxiliary() { | ||
| - "JavaCodeGenerator.variableLocal": true | ||
| - } | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 5,gapx 3" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.WebHyperlink" ) { | ||
| - name: "tablesExtLink2" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.tablesExtLink2.text" ) | ||
| - "uri": "https://michelf.ca/projects/php-markdown/extra/#table" | ||
| - auxiliary() { | ||
| - "JavaCodeGenerator.variableLocal": true | ||
| - } | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 5,gapx 3 3" | ||
| - } ) | ||
| - add( new FormComponent( "javafx.scene.control.Label" ) { | ||
| - name: "tablesExtLabel2" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.tablesExtLabel2.text" ) | ||
| - auxiliary() { | ||
| - "JavaCodeGenerator.variableLocal": true | ||
| - } | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 5,gapx 0" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) { | ||
| - name: "definitionListsExtCheckBox" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.definitionListsExtCheckBox.text" ) | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 6" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.WebHyperlink" ) { | ||
| - name: "definitionListsExtLink" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.definitionListsExtLink.text" ) | ||
| - "uri": "https://michelf.ca/projects/php-markdown/extra/#def-list" | ||
| - auxiliary() { | ||
| - "JavaCodeGenerator.variableLocal": true | ||
| - } | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 6,gapx 0" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) { | ||
| - name: "fencedCodeBlocksExtCheckBox" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.fencedCodeBlocksExtCheckBox.text" ) | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 7" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.WebHyperlink" ) { | ||
| - name: "fencedCodeBlocksExtLink" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.fencedCodeBlocksExtLink.text" ) | ||
| - "uri": "http://michelf.com/projects/php-markdown/extra/#fenced-code-blocks" | ||
| - auxiliary() { | ||
| - "JavaCodeGenerator.variableLocal": true | ||
| - } | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 7,gapx 0" | ||
| - } ) | ||
| - add( new FormComponent( "javafx.scene.control.Label" ) { | ||
| - name: "fencedCodeBlocksExtLabel" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.fencedCodeBlocksExtLabel.text" ) | ||
| - auxiliary() { | ||
| - "JavaCodeGenerator.variableLocal": true | ||
| - } | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 7,gapx 3" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.WebHyperlink" ) { | ||
| - name: "fencedCodeBlocksExtLink2" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.fencedCodeBlocksExtLink2.text" ) | ||
| - "uri": "https://help.github.com/articles/github-flavored-markdown/#fenced-code-blocks" | ||
| - auxiliary() { | ||
| - "JavaCodeGenerator.variableLocal": true | ||
| - } | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 7,gapx 3" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) { | ||
| - name: "wikilinksExtCheckBox" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.wikilinksExtCheckBox.text" ) | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 8" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) { | ||
| - name: "strikethroughExtCheckBox" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.strikethroughExtCheckBox.text" ) | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 9" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) { | ||
| - name: "anchorlinksExtCheckBox" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.anchorlinksExtCheckBox.text" ) | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 10" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) { | ||
| - name: "suppressHtmlBlocksExtCheckBox" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.suppressHtmlBlocksExtCheckBox.text" ) | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 11" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) { | ||
| - name: "suppressInlineHtmlExtCheckBox" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.suppressInlineHtmlExtCheckBox.text" ) | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 12" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) { | ||
| - name: "atxHeaderSpaceExtCheckBox" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.atxHeaderSpaceExtCheckBox.text" ) | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 13" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) { | ||
| - name: "forceListItemParaExtCheckBox" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.forceListItemParaExtCheckBox.text" ) | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 14" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) { | ||
| - name: "relaxedHrRulesExtCheckBox" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.relaxedHrRulesExtCheckBox.text" ) | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 15" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) { | ||
| - name: "taskListItemsExtCheckBox" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.taskListItemsExtCheckBox.text" ) | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 16" | ||
| - } ) | ||
| - add( new FormComponent( "com.scrivendor.controls.FlagCheckBox" ) { | ||
| - name: "extAnchorLinksExtCheckBox" | ||
| - "text": new FormMessage( null, "MarkdownOptionsPane.extAnchorLinksExtCheckBox.text" ) | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 0 17" | ||
| - } ) | ||
| - }, new FormLayoutConstraints( null ) { | ||
| - "location": new javafx.geometry.Point2D( 0.0, 0.0 ) | ||
| - "size": new javafx.geometry.Dimension2D( 600.0, 531.0 ) | ||
| - } ) | ||
| - } | ||
| -} | ||
| -/* | ||
| - * Copyright 2016 Karl Tauber and 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.options; | ||
| - | ||
| -import com.scrivenvar.Messages; | ||
| -import com.scrivenvar.Services; | ||
| -import com.scrivenvar.service.Options; | ||
| -import com.scrivenvar.service.events.impl.ButtonOrderPane; | ||
| -import java.util.prefs.Preferences; | ||
| -import javafx.event.ActionEvent; | ||
| -import javafx.scene.control.ButtonType; | ||
| -import javafx.scene.control.Dialog; | ||
| -import javafx.scene.control.DialogPane; | ||
| -import javafx.scene.control.Tab; | ||
| -import javafx.scene.control.TabPane; | ||
| -import javafx.stage.Window; | ||
| - | ||
| -/** | ||
| - * Options dialog. | ||
| - * | ||
| - * @author Karl Tauber and White Magic Software, Ltd. | ||
| - */ | ||
| -public class OptionsDialog extends Dialog<Void> { | ||
| - | ||
| - private final Options options = Services.load( Options.class ); | ||
| - | ||
| - public OptionsDialog( Window owner ) { | ||
| - setTitle( Messages.get( "OptionsDialog.title" ) ); | ||
| - initOwner( owner ); | ||
| - | ||
| - initComponents(); | ||
| - | ||
| - tabPane.getStyleClass().add( TabPane.STYLE_CLASS_FLOATING ); | ||
| - | ||
| - setDialogPane( new ButtonOrderPane() ); | ||
| - | ||
| - final DialogPane dialogPane = getDialogPane(); | ||
| - dialogPane.setContent( tabPane ); | ||
| - dialogPane.getButtonTypes().addAll( ButtonType.OK, ButtonType.CANCEL ); | ||
| - | ||
| - dialogPane.lookupButton( ButtonType.OK ).addEventHandler( ActionEvent.ACTION, e -> { | ||
| - save(); | ||
| - e.consume(); | ||
| - } ); | ||
| - | ||
| - // load options | ||
| - load(); | ||
| - | ||
| - // select last tab | ||
| - int tabIndex = getState().getInt( "lastOptionsTab", -1 ); | ||
| - if( tabIndex > 0 ) { | ||
| - tabPane.getSelectionModel().select( tabIndex ); | ||
| - } | ||
| - | ||
| - // remember last selected tab | ||
| - setOnHidden( e -> { | ||
| - getState().putInt( "lastOptionsTab", tabPane.getSelectionModel().getSelectedIndex() ); | ||
| - } ); | ||
| - } | ||
| - | ||
| - private Options getOptions() { | ||
| - return options; | ||
| - } | ||
| - | ||
| - private Preferences getState() { | ||
| - return getOptions().getState(); | ||
| - } | ||
| - | ||
| - private void load() { | ||
| - generalOptionsPane.load(); | ||
| - } | ||
| - | ||
| - private void save() { | ||
| - generalOptionsPane.save(); | ||
| - Services.load( Options.class ).save(); | ||
| - } | ||
| - | ||
| - private void initComponents() { | ||
| - // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents | ||
| - tabPane = new TabPane(); | ||
| - generalTab = new Tab(); | ||
| - generalOptionsPane = new GeneralOptionsPane(); | ||
| - | ||
| - //======== tabPane ======== | ||
| - { | ||
| - tabPane.setTabClosingPolicy( TabPane.TabClosingPolicy.UNAVAILABLE ); | ||
| - | ||
| - //======== generalTab ======== | ||
| - { | ||
| - generalTab.setText( Messages.get( "OptionsDialog.generalTab.text" ) ); | ||
| - generalTab.setContent( generalOptionsPane ); | ||
| - } | ||
| - | ||
| - tabPane.getTabs().addAll( generalTab ); | ||
| - } | ||
| - // JFormDesigner - End of component initialization //GEN-END:initComponents | ||
| - } | ||
| - | ||
| - // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables | ||
| - private TabPane tabPane; | ||
| - private Tab generalTab; | ||
| - private GeneralOptionsPane generalOptionsPane; | ||
| - // JFormDesigner - End of variables declaration //GEN-END:variables | ||
| -} | ||
| -JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8" | ||
| - | ||
| -new FormModel { | ||
| - "i18n.bundlePackage": "com.scrivendor" | ||
| - "i18n.bundleName": "messages" | ||
| - "i18n.keyPrefix": "OptionsDialog" | ||
| - "i18n.autoExternalize": true | ||
| - contentType: "form/javafx" | ||
| - root: new FormRoot { | ||
| - add( new FormContainer( "javafx.scene.control.TabPane", new FormLayoutManager( class javafx.scene.control.TabPane ) ) { | ||
| - name: "tabPane" | ||
| - "tabClosingPolicy": enum javafx.scene.control.TabPane$TabClosingPolicy UNAVAILABLE | ||
| - add( new FormContainer( "javafx.scene.control.Tab", new FormLayoutManager( class javafx.scene.control.Tab ) ) { | ||
| - name: "generalTab" | ||
| - "text": new FormMessage( null, "OptionsDialog.generalTab.text" ) | ||
| - add( new FormComponent( "com.scrivendor.options.GeneralOptionsPane" ) { | ||
| - name: "generalOptionsPane" | ||
| - } ) | ||
| - } ) | ||
| - add( new FormContainer( "javafx.scene.control.Tab", new FormLayoutManager( class javafx.scene.control.Tab ) ) { | ||
| - name: "markdownTab" | ||
| - "text": new FormMessage( null, "OptionsDialog.markdownTab.text" ) | ||
| - add( new FormComponent( "com.scrivendor.options.MarkdownOptionsPane" ) { | ||
| - name: "markdownOptionsPane" | ||
| - } ) | ||
| - } ) | ||
| - }, new FormLayoutConstraints( null ) { | ||
| - "location": new javafx.geometry.Point2D( 0.0, 0.0 ) | ||
| - "size": new javafx.geometry.Dimension2D( 600.0, 560.0 ) | ||
| - } ) | ||
| - } | ||
| -} | ||
| import javafx.concurrent.Worker.State; | ||
| import static javafx.concurrent.Worker.State.SUCCEEDED; | ||
| +import javafx.scene.Node; | ||
| import javafx.scene.layout.Pane; | ||
| import javafx.scene.web.WebEngine; | ||
| setPath( path ); | ||
| initListeners(); | ||
| + initTraversal(); | ||
| + } | ||
| - // Prevent tabbing into the preview pane. | ||
| - getWebView().setFocusTraversable( false ); | ||
| + /** | ||
| + * Initializes observers for document changes. When the document is reloaded | ||
| + * with new HTML, this triggers a scroll event that repositions the document | ||
| + * to the injected caret (that corresponds with the position in the text | ||
| + * editor). | ||
| + */ | ||
| + private void initListeners() { | ||
| + // Scrolls to the caret after the content has been loaded. | ||
| + getEngine().getLoadWorker().stateProperty().addListener( | ||
| + (ObservableValue<? extends State> observable, | ||
| + final State oldValue, final State newValue) -> { | ||
| + if( newValue == SUCCEEDED ) { | ||
| + scrollToCaret(); | ||
| + } | ||
| + } ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Ensures images can be found relative to the document. | ||
| + * | ||
| + * @return The base path element to use for the document, or the empty string | ||
| + * if no path has been set, yet. | ||
| + */ | ||
| + private String getBase() { | ||
| + final Path basePath = getPath(); | ||
| + | ||
| + return basePath == null | ||
| + ? "" | ||
| + : ("<base href='" + basePath.getParent().toUri().toString() + "'>"); | ||
| } | ||
| /** | ||
| * Updates the internal HTML source, loads it into the preview pane, then | ||
| * scrolls to the caret position. | ||
| * | ||
| - * @param html | ||
| + * @param html The new HTML document to display. | ||
| */ | ||
| public void update( final String html ) { | ||
| - setHtml( html ); | ||
| - update(); | ||
| - } | ||
| - | ||
| - private void update() { | ||
| getEngine().loadContent( | ||
| "<!DOCTYPE html>" | ||
| + "<html>" | ||
| + "<head>" | ||
| + "<link rel='stylesheet' href='" + getClass().getResource( "webview.css" ) + "'>" | ||
| + getBase() | ||
| + "</head>" | ||
| + "<body>" | ||
| - + getHtml() | ||
| + + html | ||
| + "</body>" | ||
| + "</html>" ); | ||
| - } | ||
| - | ||
| - private String getBase() { | ||
| - final Path basePath = getPath(); | ||
| - | ||
| - return basePath == null | ||
| - ? "" | ||
| - : ("<base href='" + basePath.getParent().toUri().toString() + "'>"); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Initializes observers for document changes. When the document is reloaded | ||
| - * with new HTML, this triggers a scroll event that repositions the document | ||
| - * to the injected caret (that corresponds with the position in the text | ||
| - * editor). | ||
| - */ | ||
| - private void initListeners() { | ||
| - // Scrolls to the caret after the content has been loaded. | ||
| - getEngine().getLoadWorker().stateProperty().addListener( | ||
| - (ObservableValue<? extends State> observable, | ||
| - State oldValue, State newValue) -> { | ||
| - if( newValue == SUCCEEDED ) { | ||
| - scrollToCaret(); | ||
| - } | ||
| - } ); | ||
| } | ||
| + " window.scrollTo( 0, e.topOffset() - (window.innerHeight / 2 ) );" | ||
| + "}"; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Prevent tabbing into the preview pane. | ||
| + */ | ||
| + private void initTraversal() { | ||
| + getWebView().setFocusTraversable( false ); | ||
| } | ||
| } | ||
| - public WebView getWebView() { | ||
| + private WebView getWebView() { | ||
| return this.webView; | ||
| - } | ||
| - | ||
| - private String getHtml() { | ||
| - return this.html; | ||
| - } | ||
| - | ||
| - private void setHtml( final String html ) { | ||
| - this.html = html; | ||
| } | ||
| private Path getPath() { | ||
| return this.path; | ||
| } | ||
| private void setPath( final Path path ) { | ||
| this.path = path; | ||
| + } | ||
| + | ||
| + public Node getNode() { | ||
| + return getWebView(); | ||
| } | ||
| } | ||
| import java.util.prefs.Preferences; | ||
| -import javafx.beans.property.StringProperty; | ||
| /** | ||
| - * Options | ||
| + * Responsible for persistent options. | ||
| * | ||
| * @author White Magic Software, Ltd. | ||
| */ | ||
| public interface Options { | ||
| - | ||
| - public Preferences getState(); | ||
| - | ||
| - public void load( Preferences options ); | ||
| - | ||
| - public void save(); | ||
| - | ||
| - public String getLineSeparator(); | ||
| - | ||
| - public void setLineSeparator( String lineSeparator ); | ||
| - | ||
| - public StringProperty lineSeparatorProperty(); | ||
| - | ||
| - public String getEncoding(); | ||
| - public void setEncoding( String encoding ); | ||
| + public Preferences getState(); | ||
| + | ||
| + /** | ||
| + * Stores the key and value into the user preferences to be loaded the next | ||
| + * time the application is launched. | ||
| + * | ||
| + * @param key Name of the key to persist along with its value. | ||
| + * @param value Value to associate with the key. | ||
| + */ | ||
| + public void put( String key, String value ); | ||
| - public StringProperty encodingProperty(); | ||
| + /** | ||
| + * Retrieves the value for a key in the user preferences. | ||
| + * | ||
| + * @param key Retrieve the value of this key. | ||
| + * @param defaultValue The value to return in the event that the given key has | ||
| + * no associated value. | ||
| + * | ||
| + * @return The value associated with the key. | ||
| + */ | ||
| + public String get( String key, String defaultValue ); | ||
| } | ||
| import com.scrivenvar.service.Options; | ||
| -import static com.scrivenvar.util.Utils.putPrefs; | ||
| import java.util.prefs.Preferences; | ||
| import static java.util.prefs.Preferences.userRoot; | ||
| -import javafx.beans.property.SimpleStringProperty; | ||
| -import javafx.beans.property.StringProperty; | ||
| /** | ||
| * Persistent options user can change at runtime. | ||
| * | ||
| * @author Karl Tauber and White Magic Software, Ltd. | ||
| */ | ||
| public class DefaultOptions implements Options { | ||
| - private final StringProperty LINE_SEPARATOR = new SimpleStringProperty(); | ||
| - private final StringProperty ENCODING = new SimpleStringProperty(); | ||
| - | ||
| private Preferences preferences; | ||
| public DefaultOptions() { | ||
| setPreferences( getRootPreferences().node( "options" ) ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public void put( final String key, final String value ) { | ||
| + getPreferences().put( key, value ); | ||
| } | ||
| - private void setPreferences( Preferences preferences ) { | ||
| + @Override | ||
| + public String get( final String key, final String defalutValue ) { | ||
| + return getPreferences().get( key, defalutValue ); | ||
| + } | ||
| + | ||
| + private void setPreferences( final Preferences preferences ) { | ||
| this.preferences = preferences; | ||
| } | ||
| } | ||
| - public Preferences getPreferences() { | ||
| + private Preferences getPreferences() { | ||
| return this.preferences; | ||
| - } | ||
| - | ||
| - @Override | ||
| - public void load( Preferences options ) { | ||
| - setLineSeparator( options.get( "lineSeparator", null ) ); | ||
| - setEncoding( options.get( "encoding", null ) ); | ||
| - } | ||
| - | ||
| - @Override | ||
| - public void save() { | ||
| - final Preferences prefs = getPreferences(); | ||
| - | ||
| - putPrefs( prefs, "lineSeparator", getLineSeparator(), null ); | ||
| - putPrefs( prefs, "encoding", getEncoding(), null ); | ||
| - } | ||
| - | ||
| - @Override | ||
| - public String getLineSeparator() { | ||
| - return LINE_SEPARATOR.get(); | ||
| - } | ||
| - | ||
| - @Override | ||
| - public void setLineSeparator( String lineSeparator ) { | ||
| - LINE_SEPARATOR.set( lineSeparator ); | ||
| - } | ||
| - | ||
| - @Override | ||
| - public StringProperty lineSeparatorProperty() { | ||
| - return LINE_SEPARATOR; | ||
| - } | ||
| - | ||
| - @Override | ||
| - public String getEncoding() { | ||
| - return ENCODING.get(); | ||
| - } | ||
| - | ||
| - @Override | ||
| - public void setEncoding( String encoding ) { | ||
| - ENCODING.set( encoding ); | ||
| - } | ||
| - | ||
| - @Override | ||
| - public StringProperty encodingProperty() { | ||
| - return ENCODING; | ||
| } | ||
| } | ||
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
| */ | ||
| - | ||
| package com.scrivenvar.util; | ||
| /** | ||
| - * Simple item for a ChoiceBox, ComboBox or ListView. | ||
| - * Consists of a string name and a value object. | ||
| - * toString() returns the name. | ||
| - * equals() compares the value and hashCode() returns the hash code of the value. | ||
| + * Simple item for a ChoiceBox, ComboBox or ListView. Consists of a string name | ||
| + * and a value object. toString() returns the name. equals() compares the value | ||
| + * and hashCode() returns the hash code of the value. | ||
| * | ||
| * @author Karl Tauber | ||
| + * @param <V> The type of item value. | ||
| */ | ||
| -public class Item<V> | ||
| -{ | ||
| - public final String name; | ||
| - public final V value; | ||
| +public class Item<V> { | ||
| - public Item(String name, V value) { | ||
| - this.name = name; | ||
| - this.value = value; | ||
| - } | ||
| + public final String name; | ||
| + public final V value; | ||
| - @Override | ||
| - public boolean equals(Object obj) { | ||
| - if (this == obj) | ||
| - return true; | ||
| - if (!(obj instanceof Item)) | ||
| - return false; | ||
| - return Utils.safeEquals(value, ((Item<?>)obj).value); | ||
| - } | ||
| + public Item( final String name, final V value ) { | ||
| + this.name = name; | ||
| + this.value = value; | ||
| + } | ||
| - @Override | ||
| - public int hashCode() { | ||
| - return (value != null) ? value.hashCode() : 0; | ||
| - } | ||
| + @Override | ||
| + public boolean equals( final Object obj ) { | ||
| + if( this == obj ) { | ||
| + return true; | ||
| + } | ||
| + if( !(obj instanceof Item) ) { | ||
| + return false; | ||
| + } | ||
| + return Utils.safeEquals( value, ((Item<?>)obj).value ); | ||
| + } | ||
| - @Override | ||
| - public String toString() { | ||
| - return name; | ||
| - } | ||
| + @Override | ||
| + public int hashCode() { | ||
| + return (value != null) ? value.hashCode() : 0; | ||
| + } | ||
| + | ||
| + @Override | ||
| + public String toString() { | ||
| + return name; | ||
| + } | ||
| } | ||
| import java.util.ArrayList; | ||
| -import java.util.Set; | ||
| import java.util.prefs.Preferences; | ||
| -import javafx.geometry.Orientation; | ||
| -import javafx.scene.Node; | ||
| -import javafx.scene.control.ScrollBar; | ||
| /** | ||
| * @author Karl Tauber | ||
| */ | ||
| public class Utils { | ||
| - public static boolean safeEquals( Object o1, Object o2 ) { | ||
| + public static boolean safeEquals( final Object o1, final Object o2 ) { | ||
| if( o1 == o2 ) { | ||
| return true; | ||
| } | ||
| if( o1 == null || o2 == null ) { | ||
| return false; | ||
| } | ||
| return o1.equals( o2 ); | ||
| } | ||
| - public static boolean isNullOrEmpty( String s ) { | ||
| + public static boolean isNullOrEmpty( final String s ) { | ||
| return s == null || s.isEmpty(); | ||
| } | ||
| public static String[] getPrefsStrings( final Preferences prefs, String key ) { | ||
| - final ArrayList<String> arr = new ArrayList<>(); | ||
| + final ArrayList<String> arr = new ArrayList<>( 256 ); | ||
| for( int i = 0; i < 10000; i++ ) { | ||
| final String s = prefs.get( key + (i + 1), null ); | ||
| if( s == null ) { | ||
| break; | ||
| } | ||
| + | ||
| arr.add( s ); | ||
| } | ||
| for( int i = strings.length; prefs.get( key + (i + 1), null ) != null; i++ ) { | ||
| prefs.remove( key + (i + 1) ); | ||
| - } | ||
| - } | ||
| - | ||
| - public static ScrollBar findVScrollBar( Node node ) { | ||
| - final Set<Node> scrollBars = node.lookupAll( ".scroll-bar" ); | ||
| - | ||
| - for( final Node scrollBar : scrollBars ) { | ||
| - if( scrollBar instanceof ScrollBar | ||
| - && ((ScrollBar)scrollBar).getOrientation() == Orientation.VERTICAL ) { | ||
| - return (ScrollBar)scrollBar; | ||
| - } | ||
| } | ||
| - | ||
| - return null; | ||
| } | ||
| } | ||
| +#!/bin/bash | ||
| + | ||
| +INKSCAPE=/usr/bin/inkscape | ||
| + | ||
| +declare -a SIZES=("16" "32" "64" "128" "256" "512") | ||
| + | ||
| +for i in "${SIZES[@]}"; do | ||
| + # -y: export background opacity 0 | ||
| + $INKSCAPE -y 0 -z -f "logo.svg" -w "${i}" -e "logo${i}.png" | ||
| +done | ||
| + | ||
| Delta | 945 lines added, 1431 lines removed, 486-line decrease |
|---|