package com.scrivenvar;
import com.scrivenvar.definition.DefinitionFactory;
import com.scrivenvar.definition.DefinitionPane;
import com.scrivenvar.definition.DefinitionSource;
import com.scrivenvar.definition.MapInterpolator;
import com.scrivenvar.definition.yaml.YamlDefinitionSource;
import com.scrivenvar.editors.EditorPane;
import com.scrivenvar.editors.VariableNameInjector;
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
import com.scrivenvar.preferences.UserPreferences;
import com.scrivenvar.preview.HTMLPreviewPane;
import com.scrivenvar.processors.HtmlPreviewProcessor;
import com.scrivenvar.processors.Processor;
import com.scrivenvar.processors.ProcessorFactory;
import com.scrivenvar.service.Options;
import com.scrivenvar.service.Snitch;
import com.scrivenvar.service.events.Notifier;
import com.scrivenvar.spelling.api.SpellCheckListener;
import com.scrivenvar.spelling.api.SpellChecker;
import com.scrivenvar.spelling.impl.PermissiveSpeller;
import com.scrivenvar.spelling.impl.SymSpellSpeller;
import com.scrivenvar.util.Action;
import com.scrivenvar.util.ActionBuilder;
import com.scrivenvar.util.ActionUtils;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.ast.NodeVisitor;
import com.vladsch.flexmark.util.ast.VisitHandler;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
import javafx.util.Duration;
import org.apache.commons.lang3.SystemUtils;
import org.controlsfx.control.StatusBar;
import org.fxmisc.richtext.StyleClassedTextArea;
import org.fxmisc.richtext.model.StyleSpansBuilder;
import org.reactfx.value.Val;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.prefs.Preferences;
import java.util.stream.Collectors;
import static com.scrivenvar.Constants.*;
import static com.scrivenvar.Messages.get;
import static com.scrivenvar.util.StageState.*;
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.emptyList;
import static java.util.Collections.singleton;
import static javafx.application.Platform.runLater;
import static javafx.event.Event.fireEvent;
import static javafx.scene.input.KeyCode.ENTER;
import static javafx.scene.input.KeyCode.TAB;
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
public class MainWindow implements Observer {
private final static Options sOptions = Services.load( Options.class );
private final static Snitch SNITCH = Services.load( Snitch.class );
private final static Notifier sNotifier = Services.load( Notifier.class );
private final Scene mScene;
private final StatusBar mStatusBar;
private final Text mLineNumberText;
private final TextField mFindTextField;
private final SpellChecker mSpellChecker;
private final Object mMutex = new Object();
private final Map<FileEditorTab, Processor<String>> mProcessors =
new HashMap<>();
private final Map<String, String> mResolvedMap =
new HashMap<>( DEFAULT_MAP_SIZE );
private final EventHandler<TreeItem.TreeModificationEvent<Event>>
mTreeHandler = event -> {
exportDefinitions( getDefinitionPath() );
interpolateResolvedMap();
renderActiveTab();
};
private final EventHandler<? super KeyEvent> mTabKeyHandler =
(EventHandler<KeyEvent>) event -> {
if( event.getCode() == TAB ) {
getDefinitionPane().requestFocus();
event.consume();
}
};
private final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
event -> {
if( event.getCode() == ENTER ) {
getVariableNameInjector().injectSelectedItem();
}
};
private final ChangeListener<Integer> mCaretPositionListener =
( observable, oldPosition, newPosition ) -> {
final FileEditorTab tab = getActiveFileEditorTab();
final EditorPane pane = tab.getEditorPane();
final StyleClassedTextArea editor = pane.getEditor();
getLineNumberText().setText(
get( STATUS_BAR_LINE,
editor.getCurrentParagraph() + 1,
editor.getParagraphs().size(),
editor.getCaretPosition()
)
);
};
private final ChangeListener<Integer> mCaretParagraphListener =
( observable, oldIndex, newIndex ) ->
scrollToParagraph( newIndex, true );
private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
private final DefinitionPane mDefinitionPane = new DefinitionPane();
private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane();
private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane(
mCaretPositionListener,
mCaretParagraphListener );
private final VariableNameInjector mVariableNameInjector
= new VariableNameInjector( mDefinitionPane );
public MainWindow() {
sNotifier.addObserver( this );
mStatusBar = createStatusBar();
mLineNumberText = createLineNumberText();
mFindTextField = createFindTextField();
mScene = createScene();
mSpellChecker = createSpellChecker();
initLayout();
}
public void init() {
initFindInput();
initSnitch();
initDefinitionListener();
initTabAddedListener();
initTabChangedListener();
initPreferences();
initVariableNameInjector();
}
private void initLayout() {
final var appScene = getScene();
appScene.getStylesheets().add( STYLESHEET_SCENE );
appScene.windowProperty().addListener(
( unused, oldWindow, newWindow ) ->
newWindow.setOnCloseRequest(
e -> {
if( !getFileEditorPane().closeAllEditors() ) {
e.consume();
}
}
)
);
}
private void initFindInput() {
final TextField input = getFindTextField();
input.setOnKeyPressed( ( KeyEvent event ) -> {
switch( event.getCode() ) {
case F3:
case ENTER:
editFindNext();
break;
case F:
if( !event.isControlDown() ) {
break;
}
case ESCAPE:
getStatusBar().setGraphic( null );
getActiveFileEditorTab().getEditorPane().requestFocus();
break;
}
} );
input.focusedProperty().addListener(
( focused, oldFocus, newFocus ) -> {
if( !newFocus ) {
getStatusBar().setGraphic( null );
}
}
);
}
private void initSnitch() {
SNITCH.addObserver( this );
}
private void initDefinitionListener() {
getFileEditorPane().onOpenDefinitionFileProperty().addListener(
( final ObservableValue<? extends Path> file,
final Path oldPath, final Path newPath ) -> {
resetProcessors();
openDefinitions( newPath );
renderActiveTab();
}
);
}
private void initTabAddedListener() {
final FileEditorTabPane editorPane = getFileEditorPane();
final ObservableList<Tab> tabs = editorPane.getTabs();
tabs.addListener(
( final Change<? extends Tab> change ) -> {
while( change.next() ) {
if( change.wasAdded() ) {
for( final Tab newTab : change.getAddedSubList() ) {
final FileEditorTab tab = (FileEditorTab) newTab;
initTextChangeListener( tab );
initTabKeyEventListener( tab );
initScrollEventListener( tab );
initSpellCheckListener( tab );
}
}
}
}
);
}
private void initTextChangeListener( final FileEditorTab tab ) {
tab.addTextChangeListener(
( editor, oldValue, newValue ) -> {
process( tab );
scrollToParagraph( getCurrentParagraphIndex() );
}
);
}
private void initTabKeyEventListener( final FileEditorTab tab ) {
tab.addEventFilter( KeyEvent.KEY_PRESSED, mTabKeyHandler );
}
private void initScrollEventListener( final FileEditorTab tab ) {
final var scrollPane = tab.getScrollPane();
final var scrollBar = getPreviewPane().getVerticalScrollBar();
addShowListener( scrollPane, ( __ ) -> {
final var handler = new ScrollEventHandler( scrollPane, scrollBar );
handler.enabledProperty().bind( tab.selectedProperty() );
} );
}
private void initSpellCheckListener( final FileEditorTab tab ) {
final var editor = tab.getEditorPane().getEditor();
addShowListener(
editor, ( __ ) -> spellcheck( editor, editor.getText() )
);
editor.plainTextChanges()
.filter( p -> !p.isIdentity() ).subscribe( change -> {
final var offset = change.getPosition();
final var position = editor.offsetToPosition( offset, Forward );
final var paraId = position.getMajor();
final var paragraph = editor.getParagraph( paraId );
final var text = paragraph.getText();
editor.clearStyle( paraId );
spellcheck( editor, text, paraId );
} );
}
private void initTabChangedListener() {
final FileEditorTabPane editorPane = getFileEditorPane();
editorPane.addTabSelectionListener(
( tabPane, oldTab, newTab ) -> {
if( newTab == null ) {
getPreviewPane().clear();
}
else {
final var tab = (FileEditorTab) newTab;
updateVariableNameInjector( tab );
process( tab );
}
}
);
}
private void initPreferences() {
initDefinitionPane();
getFileEditorPane().initPreferences();
}
private void initVariableNameInjector() {
updateVariableNameInjector( getActiveFileEditorTab() );
}
private void addShowListener(
final Node node, final Consumer<Void> consumer ) {
final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) ->
runLater( () -> {
if( newShow ) {
try {
consumer.accept( null );
} catch( final Exception ex ) {
error( ex );
}
}
} );
Val.flatMap( node.sceneProperty(), Scene::windowProperty )
.flatMap( Window::showingProperty )
.addListener( listener );
}
private void scrollToParagraph( final int id ) {
scrollToParagraph( id, false );
}
private void scrollToParagraph( final int id, final boolean force ) {
synchronized( mMutex ) {
final var previewPane = getPreviewPane();
final var scrollPane = previewPane.getScrollPane();
final int approxId = getActiveEditorPane().approximateParagraphId( id );
if( force ) {
previewPane.scrollTo( approxId );
}
else {
previewPane.tryScrollTo( approxId );
}
scrollPane.repaint();
}
}
private void updateVariableNameInjector( final FileEditorTab tab ) {
getVariableNameInjector().addListener( tab );
}
private void process( final FileEditorTab tab ) {
if( tab != null ) {
getPreviewPane().setPath( tab.getPath() );
final Processor<String> processor = getProcessors().computeIfAbsent(
tab, p -> createProcessors( tab )
);
try {
processChain( processor, tab.getEditorText() );
} catch( final Exception ex ) {
error( ex );
}
}
}
private String processChain( Processor<String> handler, String text ) {
while( handler != null && text != null ) {
text = handler.process( text );
handler = handler.next();
}
return text;
}
private void renderActiveTab() {
process( getActiveFileEditorTab() );
}
private void openDefinitions( final Path path ) {
try {
final var ds = createDefinitionSource( path );
setDefinitionSource( ds );
final var prefs = getUserPreferences();
prefs.definitionPathProperty().setValue( path.toFile() );
prefs.save();
final var tooltipPath = new Tooltip( path.toString() );
tooltipPath.setShowDelay( Duration.millis( 200 ) );
final var pane = getDefinitionPane();
pane.update( ds );
pane.addTreeChangeHandler( mTreeHandler );
pane.addKeyEventHandler( mDefinitionKeyHandler );
pane.filenameProperty().setValue( path.getFileName().toString() );
pane.setTooltip( tooltipPath );
interpolateResolvedMap();
} catch( final Exception ex ) {
error( ex );
}
}
private void exportDefinitions( final Path path ) {
try {
final DefinitionPane pane = getDefinitionPane();
final TreeItem<String> root = pane.getTreeView().getRoot();
final TreeItem<String> problemChild = pane.isTreeWellFormed();
if( problemChild == null ) {
getDefinitionSource().getTreeAdapter().export( root, path );
getNotifier().clear();
}
else {
final String msg = get(
"yaml.error.tree.form", problemChild.getValue() );
error( msg );
}
} catch( final Exception ex ) {
error( ex );
}
}
private void interpolateResolvedMap() {
final Map<String, String> treeMap = getDefinitionPane().toMap();
final Map<String, String> map = new HashMap<>( treeMap );
MapInterpolator.interpolate( map );
getResolvedMap().clear();
getResolvedMap().putAll( map );
}
private void initDefinitionPane() {
openDefinitions( getDefinitionPath() );
}
private void error( final Exception ex ) {
getNotifier().notify( ex );
}
private void error( final String msg ) {
getNotifier().notify( msg );
}
@Override
public void update( final Observable observable, final Object value ) {
if( value != null ) {
if( observable instanceof Snitch && value instanceof Path ) {
updateSelectedTab();
}
else if( observable instanceof Notifier && value instanceof String ) {
updateStatusBar( (String) value );
}
}
}
private void updateStatusBar( final String s ) {
runLater(
() -> {
final int index = s.indexOf( '\n' );
final String message = s.substring(
0, index > 0 ? index : s.length() );
getStatusBar().setText( message );
}
);
}
private void updateSelectedTab() {
runLater(
() -> {
resetProcessors();
renderActiveTab();
}
);
}
private void resetProcessors() {
getProcessors().clear();
}
private void fileNew() {
getFileEditorPane().newEditor();
}
private void fileOpen() {
getFileEditorPane().openFileDialog();
}
private void fileClose() {
getFileEditorPane().closeEditor( getActiveFileEditorTab(), true );
}
private void fileCloseAll() {
getFileEditorPane().closeAllEditors();
}
private void fileSave() {
getFileEditorPane().saveEditor( getActiveFileEditorTab() );
}
private void fileSaveAs() {
final FileEditorTab editor = getActiveFileEditorTab();
getFileEditorPane().saveEditorAs( editor );
getProcessors().remove( editor );
try {
process( editor );
} catch( final Exception ex ) {
error( ex );
}
}
private void fileSaveAll() {
getFileEditorPane().saveAllEditors();
}
private void fileExit() {
final Window window = getWindow();
fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
}
private void copyHtml() {
final var markdown = getActiveEditorPane().getText();
final var processors = createProcessorFactory().createProcessors(
getActiveFileEditorTab()
);
final var chain = processors.remove( HtmlPreviewProcessor.class );
final String html = processChain( chain, markdown );
final Clipboard clipboard = Clipboard.getSystemClipboard();
final ClipboardContent content = new ClipboardContent();
content.putString( html );
clipboard.setContent( content );
}
private void editFind() {
final TextField input = getFindTextField();
getStatusBar().setGraphic( input );
input.requestFocus();
}
public void editFindNext() {
getActiveFileEditorTab().searchNext( getFindTextField().getText() );
}
public void editPreferences() {
getUserPreferences().show();
}
private void insertMarkdown(
final String leading, final String trailing ) {
getActiveEditorPane().surroundSelection( leading, trailing );
}
private void insertMarkdown(
final String leading, final String trailing, final String hint ) {
getActiveEditorPane().surroundSelection( leading, trailing, hint );
}
private void helpAbout() {
final Alert alert = new Alert( AlertType.INFORMATION );
alert.setTitle( get( "Dialog.about.title" ) );
alert.setHeaderText( get( "Dialog.about.header" ) );
alert.setContentText( get( "Dialog.about.content" ) );
alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
alert.initOwner( getWindow() );
alert.showAndWait();
}
private SpellChecker createSpellChecker() {
try {
final Collection<String> lexicon = readLexicon( "en.txt" );
return SymSpellSpeller.forLexicon( lexicon );
} catch( final Exception ex ) {
error( ex );
return new PermissiveSpeller();
}
}
private Processor<String> createProcessors( final FileEditorTab tab ) {
return createProcessorFactory().createProcessors( tab );
}
private ProcessorFactory createProcessorFactory() {
return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
}
private HTMLPreviewPane createHTMLPreviewPane() {
return new HTMLPreviewPane();
}
private DefinitionSource createDefaultDefinitionSource() {
return new YamlDefinitionSource( getDefinitionPath() );
}
private DefinitionSource createDefinitionSource( final Path path ) {
try {
return createDefinitionFactory().createDefinitionSource( path );
} catch( final Exception ex ) {
error( ex );
return createDefaultDefinitionSource();
}
}
private TextField createFindTextField() {
return new TextField();
}
private DefinitionFactory createDefinitionFactory() {
return new DefinitionFactory();
}
private StatusBar createStatusBar() {
return new StatusBar();
}
private Scene createScene() {
final SplitPane splitPane = new SplitPane(
getDefinitionPane().getNode(),
getFileEditorPane().getNode(),
getPreviewPane().getNode() );
splitPane.setDividerPositions(
getFloat( K_PANE_SPLIT_DEFINITION, .22f ),
getFloat( K_PANE_SPLIT_EDITOR, .60f ),
getFloat( K_PANE_SPLIT_PREVIEW, .18f ) );
getDefinitionPane().prefHeightProperty()
.bind( splitPane.heightProperty() );
final BorderPane borderPane = new BorderPane();
borderPane.setPrefSize( 1280, 800 );
borderPane.setTop( createMenuBar() );
borderPane.setBottom( getStatusBar() );
borderPane.setCenter( splitPane );
final VBox statusBar = new VBox();
statusBar.setAlignment( Pos.BASELINE_CENTER );
statusBar.getChildren().add( getLineNumberText() );
getStatusBar().getRightItems().add( statusBar );
if( SystemUtils.IS_OS_WINDOWS ) {
splitPane.getDividers().get( 1 ).positionProperty().addListener(
( l, oValue, nValue ) -> runLater(
() -> getPreviewPane().getScrollPane().repaint()
)
);
}
return new Scene( borderPane );
}
private Text createLineNumberText() {
return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
}
private Node createMenuBar() {
final BooleanBinding activeFileEditorIsNull =
getFileEditorPane().activeFileEditorProperty().isNull();
final Action fileNewAction = new ActionBuilder()
.setText( "Main.menu.file.new" )
.setAccelerator( "Shortcut+N" )
.setIcon( FILE_ALT )
.setAction( e -> fileNew() )
.build();
final Action fileOpenAction = new ActionBuilder()
.setText( "Main.menu.file.open" )
.setAccelerator( "Shortcut+O" )
.setIcon( FOLDER_OPEN_ALT )
.setAction( e -> fileOpen() )
.build();
final Action fileCloseAction = new ActionBuilder()
.setText( "Main.menu.file.close" )
.setAccelerator( "Shortcut+W" )
.setAction( e -> fileClose() )
.setDisable( activeFileEditorIsNull )
.build();
final Action fileCloseAllAction = new ActionBuilder()
.setText( "Main.menu.file.close_all" )
.setAction( e -> fileCloseAll() )
.setDisable( activeFileEditorIsNull )
.build();
final Action fileSaveAction = new ActionBuilder()
.setText( "Main.menu.file.save" )
.setAccelerator( "Shortcut+S" )
.setIcon( FLOPPY_ALT )
.setAction( e -> fileSave() )
.setDisable( createActiveBooleanProperty(
FileEditorTab::modifiedProperty ).not() )
.build();
final Action fileSaveAsAction = new ActionBuilder()
.setText( "Main.menu.file.save_as" )
.setAction( e -> fileSaveAs() )
.setDisable( activeFileEditorIsNull )
.build();
final Action fileSaveAllAction = new ActionBuilder()
.setText( "Main.menu.file.save_all" )
.setAccelerator( "Shortcut+Shift+S" )
.setAction( e -> fileSaveAll() )
.setDisable( Bindings.not(
getFileEditorPane().anyFileEditorModifiedProperty() ) )
.build();
final Action fileExitAction = new ActionBuilder()
.setText( "Main.menu.file.exit" )
.setAction( e -> fileExit() )
.build();
final Action editCopyHtmlAction = new ActionBuilder()
.setText( Messages.get( "Main.menu.edit.copy.html" ) )
.setIcon( HTML5 )
.setAction( e -> copyHtml() )
.setDisable( activeFileEditorIsNull )
.build();
final Action editUndoAction = new ActionBuilder()
.setText( "Main.menu.edit.undo" )
.setAccelerator( "Shortcut+Z" )
.setIcon( UNDO )
.setAction( e -> getActiveEditorPane().undo() )
.setDisable( createActiveBooleanProperty(
FileEditorTab::canUndoProperty ).not() )
.build();
final Action editRedoAction = new ActionBuilder()
.setText( "Main.menu.edit.redo" )
.setAccelerator( "Shortcut+Y" )
.setIcon( REPEAT )
.setAction( e -> getActiveEditorPane().redo() )
.setDisable( createActiveBooleanProperty(
FileEditorTab::canRedoProperty ).not() )
.build();
final Action editCutAction = new ActionBuilder()
.setText( Messages.get( "Main.menu.edit.cut" ) )
.setAccelerator( "Shortcut+X" )
.setIcon( CUT )
.setAction( e -> getActiveEditorPane().cut() )
.setDisable( activeFileEditorIsNull )
.build();
final Action editCopyAction = new ActionBuilder()
.setText( Messages.get( "Main.menu.edit.copy" ) )
.setAccelerator( "Shortcut+C" )
.setIcon( COPY )
.setAction( e -> getActiveEditorPane().copy() )
.setDisable( activeFileEditorIsNull )
.build();
final Action editPasteAction = new ActionBuilder()
.setText( Messages.get( "Main.menu.edit.paste" ) )
.setAccelerator( "Shortcut+V" )
.setIcon( PASTE )
.setAction( e -> getActiveEditorPane().paste() )
.setDisable( activeFileEditorIsNull )
.build();
final Action editSelectAllAction = new ActionBuilder()
.setText( Messages.get( "Main.menu.edit.selectAll" ) )
.setAccelerator( "Shortcut+A" )
.setAction( e -> getActiveEditorPane().selectAll() )
.setDisable( activeFileEditorIsNull )
.build();
final Action editFindAction = new ActionBuilder()
.setText( "Main.menu.edit.find" )
.setAccelerator( "Ctrl+F" )
.setIcon( SEARCH )
.setAction( e -> editFind() )
.setDisable( activeFileEditorIsNull )
.build();
final Action editFindNextAction = new ActionBuilder()
.setText( "Main.menu.edit.find.next" )
.setAccelerator( "F3" )
.setIcon( null )
.setAction( e -> editFindNext() )
.setDisable( activeFileEditorIsNull )
.build();
final Action editPreferencesAction = new ActionBuilder()
.setText( "Main.menu.edit.preferences" )
.setAccelerator( "Ctrl+Alt+S" )
.setAction( e -> editPreferences() )
.build();
final Action insertBoldAction = new ActionBuilder()
.setText( "Main.menu.insert.bold" )
.setAccelerator( "Shortcut+B" )
.setIcon( BOLD )
.setAction( e -> insertMarkdown( "**", "**" ) )
.setDisable( activeFileEditorIsNull )
.build();
final Action insertItalicAction = new ActionBuilder()
.setText( "Main.menu.insert.italic" )
.setAccelerator( "Shortcut+I" )
.setIcon( ITALIC )
.setAction( e -> insertMarkdown( "*", "*" ) )
.setDisable( activeFileEditorIsNull )
.build();
final Action insertSuperscriptAction = new ActionBuilder()
.setText( "Main.menu.insert.superscript" )
.setAccelerator( "Shortcut+[" )
.setIcon( SUPERSCRIPT )
.setAction( e -> insertMarkdown( "^", "^" ) )
.setDisable( activeFileEditorIsNull )
.build();
final Action insertSubscriptAction = new ActionBuilder()
.setText( "Main.menu.insert.subscript" )
.setAccelerator( "Shortcut+]" )
.setIcon( SUBSCRIPT )
.setAction( e -> insertMarkdown( "~", "~" ) )
.setDisable( activeFileEditorIsNull )
.build();
final Action insertStrikethroughAction = new ActionBuilder()
.setText( "Main.menu.insert.strikethrough" )
.setAccelerator( "Shortcut+T" )
.setIcon( STRIKETHROUGH )
.setAction( e -> insertMarkdown( "~~", "~~" ) )
.setDisable( activeFileEditorIsNull )
.build();
final Action insertBlockquoteAction = new ActionBuilder()
.setText( "Main.menu.insert.blockquote" )
.setAccelerator( "Ctrl+Q" )
.setIcon( QUOTE_LEFT )
.setAction( e -> insertMarkdown( "\n\n> ", "" ) )
.setDisable( activeFileEditorIsNull )
.build();
final Action insertCodeAction = new ActionBuilder()
.setText( "Main.menu.insert.code" )
.setAccelerator( "Shortcut+K" )
.setIcon( CODE )
.setAction( e -> insertMarkdown( "`", "`" ) )
.setDisable( activeFileEditorIsNull )
.build();
final Action insertFencedCodeBlockAction = new ActionBuilder()
.setText( "Main.menu.insert.fenced_code_block" )
.setAccelerator( "Shortcut+Shift+K" )
.setIcon( FILE_CODE_ALT )
.setAction( e -> insertMarkdown(
"\n\n```\n",
"\n```\n\n",
get( "Main.menu.insert.fenced_code_block.prompt" ) ) )
.setDisable( activeFileEditorIsNull )
.build();
final Action insertLinkAction = new ActionBuilder()
.setText( "Main.menu.insert.link" )
.setAccelerator( "Shortcut+L" )
.setIcon( LINK )
.setAction( e -> getActiveEditorPane().insertLink() )
.setDisable( activeFileEditorIsNull )
.build();
final Action insertImageAction = new ActionBuilder()
.setText( "Main.menu.insert.image" )
.setAccelerator( "Shortcut+G" )
.setIcon( PICTURE_ALT )
.setAction( e -> getActiveEditorPane().insertImage() )
.setDisable( activeFileEditorIsNull )
.build();
final int HEADERS = 3;
final Action[] headers = new Action[ HEADERS ];
for( int i = 1; i <= HEADERS; i++ ) {
final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
final String markup = String.format( "%n%n%s ", hashes );
final String text = "Main.menu.insert.header." + i;
final String accelerator = "Shortcut+" + i;
final String prompt = text + ".prompt";
headers[ i - 1 ] = new ActionBuilder()
.setText( text )
.setAccelerator( accelerator )
.setIcon( HEADER )
.setAction( e -> insertMarkdown( markup, "", get( prompt ) ) )
.setDisable( activeFileEditorIsNull )
.build();
}
final Action insertUnorderedListAction = new ActionBuilder()
.setText( "Main.menu.insert.unordered_list" )
.setAccelerator( "Shortcut+U" )
.setIcon( LIST_UL )
.setAction( e -> insertMarkdown( "\n\n* ", "" ) )
.setDisable( activeFileEditorIsNull )
.build();
final Action insertOrderedListAction = new ActionBuilder()
.setText( "Main.menu.insert.ordered_list" )
.setAccelerator( "Shortcut+Shift+O" )
.setIcon( LIST_OL )
.setAction( e -> insertMarkdown(
"\n\n1. ", "" ) )
.setDisable( activeFileEditorIsNull )
.build();
final Action insertHorizontalRuleAction = new ActionBuilder()
.setText( "Main.menu.insert.horizontal_rule" )
.setAccelerator( "Shortcut+H" )
.setAction( e -> insertMarkdown(
"\n\n---\n\n", "" ) )
.setDisable( activeFileEditorIsNull )
.build();
final Action helpAboutAction = new ActionBuilder()
.setText( "Main.menu.help.about" )
.setAction( e -> helpAbout() )
.build();
final Menu fileMenu = ActionUtils.createMenu(
get( "Main.menu.file" ),
fileNewAction,
fileOpenAction,
null,
fileCloseAction,
fileCloseAllAction,
null,
fileSaveAction,
fileSaveAsAction,
fileSaveAllAction,
null,
fileExitAction );
final Menu editMenu = ActionUtils.createMenu(
get( "Main.menu.edit" ),
editCopyHtmlAction,
null,
editUndoAction,
editRedoAction,
null,
editCutAction,
editCopyAction,
editPasteAction,
editSelectAllAction,
null,
editFindAction,
editFindNextAction,
null,
editPreferencesAction );
final Menu insertMenu = ActionUtils.createMenu(
get( "Main.menu.insert" ),
insertBoldAction,
insertItalicAction,
insertSuperscriptAction,
insertSubscriptAction,
insertStrikethroughAction,
insertBlockquoteAction,
insertCodeAction,
insertFencedCodeBlockAction,
null,
insertLinkAction,
insertImageAction,
null,
headers[ 0 ],
headers[ 1 ],
headers[ 2 ],
null,
insertUnorderedListAction,
insertOrderedListAction,
insertHorizontalRuleAction
);
final Menu helpMenu = ActionUtils.createMenu(
get( "Main.menu.help" ),
helpAboutAction );
final MenuBar menuBar = new MenuBar(
fileMenu,
editMenu,
insertMenu,
helpMenu );
final ToolBar toolBar = ActionUtils.createToolBar(
fileNewAction,
fileOpenAction,
fileSaveAction,
null,
editUndoAction,
editRedoAction,
editCutAction,
editCopyAction,
editPasteAction,
null,
insertBoldAction,
insertItalicAction,
insertSuperscriptAction,
insertSubscriptAction,
insertBlockquoteAction,
insertCodeAction,
insertFencedCodeBlockAction,
null,
insertLinkAction,
insertImageAction,
null,
headers[ 0 ],
null,
insertUnorderedListAction,
insertOrderedListAction );
return new VBox( menuBar, toolBar );
}
private BooleanProperty createActiveBooleanProperty(
final Function<FileEditorTab, ObservableBooleanValue> func ) {
final BooleanProperty b = new SimpleBooleanProperty();
final FileEditorTab tab = getActiveFileEditorTab();
if( tab != null ) {
b.bind( func.apply( tab ) );
}
getFileEditorPane().activeFileEditorProperty().addListener(
( observable, oldFileEditor, newFileEditor ) -> {
b.unbind();
if( newFileEditor == null ) {
b.set( false );
}
else {
b.bind( func.apply( newFileEditor ) );
}
}
);
return b;
}
private Preferences getPreferences() {
return sOptions.getState();
}
private int getCurrentParagraphIndex() {
return getActiveEditorPane().getCurrentParagraphIndex();
}
private float getFloat( final String key, final float defaultValue ) {
return getPreferences().getFloat( key, defaultValue );
}
public Window getWindow() {
return getScene().getWindow();
}
private MarkdownEditorPane getActiveEditorPane() {
return getActiveFileEditorTab().getEditorPane();
}
private FileEditorTab getActiveFileEditorTab() {
return getFileEditorPane().getActiveFileEditor();
}
protected Scene getScene() {
return mScene;
}
private SpellChecker getSpellChecker() {
return mSpellChecker;
}
private Map<FileEditorTab, Processor<String>> getProcessors() {
return mProcessors;
}
private FileEditorTabPane getFileEditorPane() {
return mFileEditorPane;
}
private HTMLPreviewPane getPreviewPane() {
return mPreviewPane;
}
private void setDefinitionSource(
final DefinitionSource definitionSource ) {
assert definitionSource != null;
mDefinitionSource = definitionSource;
}
private DefinitionSource getDefinitionSource() {
return mDefinitionSource;
}
private DefinitionPane getDefinitionPane() {
return mDefinitionPane;
}
private Text getLineNumberText() {
return mLineNumberText;
}
private StatusBar getStatusBar() {
return mStatusBar;
}
private TextField getFindTextField() {
return mFindTextField;
}
private VariableNameInjector getVariableNameInjector() {
return mVariableNameInjector;
}
private Map<String, String> getResolvedMap() {
return mResolvedMap;
}
private Notifier getNotifier() {
return sNotifier;
}
private UserPreferences getUserPreferences() {
return sOptions.getUserPreferences();
}
private Path getDefinitionPath() {
return getUserPreferences().getDefinitionPath();
}
private void spellcheck(
final StyleClassedTextArea editor, final String text ) {
spellcheck( editor, text, -1 );
}
@SuppressWarnings("CodeBlock2Expr")
private void spellcheck(
final StyleClassedTextArea editor, final String text, final int paraId ) {
final var builder = new StyleSpansBuilder<Collection<String>>();
final var runningIndex = new AtomicInteger( 0 );
final var checker = getSpellChecker();
final var node = mParser.parse( text );
final var visitor = new TextVisitor( ( visited, bIndex, eIndex ) -> {
checker.proofread( visited, ( misspelled, prevIndex, currIndex ) -> {
prevIndex += bIndex;
currIndex += bIndex;
builder.add( emptyList(), prevIndex - runningIndex.get() );
builder.add( singleton( "spelling" ), currIndex - prevIndex );
runningIndex.set( currIndex );
} );
} );
visitor.visit( node );
if( runningIndex.get() > 0 ) {
builder.add( emptyList(), text.length() - runningIndex.get() );
final var spans = builder.create();
if( paraId >= 0 ) {
editor.setStyleSpans( paraId, 0, spans );
}
else {
editor.setStyleSpans( 0, spans );
}
}
}
@SuppressWarnings("SameParameterValue")
private Collection<String> readLexicon( final String filename )
throws Exception {
final var path = Paths.get( LEXICONS_DIRECTORY, filename ).toString();
final var classLoader = MainWindow.class.getClassLoader();
try( final var resource = classLoader.getResourceAsStream( path ) ) {
assert resource != null;
return new BufferedReader( new InputStreamReader( resource, UTF_8 ) )
.lines()
.collect( Collectors.toList() );
}
}
private final Parser mParser = Parser.builder().build();
private final static class TextVisitor {
private final NodeVisitor mVisitor = new NodeVisitor( new VisitHandler<>(
com.vladsch.flexmark.ast.Text.class, this::visit )
);
private final SpellCheckListener mConsumer;
public TextVisitor( final SpellCheckListener consumer ) {
mConsumer = consumer;
}
private void visit( final com.vladsch.flexmark.util.ast.Node node ) {
if( node instanceof com.vladsch.flexmark.ast.Text ) {
mConsumer.accept( node.getChars().toString(),
node.getStartOffset(),
node.getEndOffset() );
}
mVisitor.visitChildren( node );
}
}
}