package com.scrivenvar;
import static com.scrivenvar.Constants.LOGO_32;
import static com.scrivenvar.Messages.get;
import com.scrivenvar.definition.DefinitionPane;
import com.scrivenvar.editor.MarkdownEditorPane;
import com.scrivenvar.editor.VariableNameInjector;
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.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 static com.scrivenvar.util.StageState.K_PANE_SPLIT_PREVIEW;
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;
public class MainWindow {
private final Options options = Services.load( Options.class );
private Scene scene;
private TreeView<String> treeView;
private DefinitionPane definitionPane;
private FileEditorTabPane fileEditorPane;
private HTMLPreviewPane previewPane;
private VariableNameInjector variableNameInjector;
private YamlTreeAdapter yamlTreeAdapter;
private YamlParser yamlParser;
private MenuBar menuBar;
public MainWindow() {
initLayout();
initTabAddedListener();
restorePreferences();
initTabChangeListener();
initVariableNameInjector();
}
private void initLayout() {
final SplitPane splitPane = new SplitPane(
getDefinitionPane().getNode(),
getFileEditorPane().getNode(),
getPreviewPane().getNode() );
splitPane.setDividerPositions(
getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
getFloat( K_PANE_SPLIT_EDITOR, .45f ),
getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
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();
}
} );
newWindow.focusedProperty().addListener(
(obs, oldFocused, newFocused) -> {
if( !newFocused ) {
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;
}
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;
}
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 ) );
}
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() {
return new FileEditorTabPane();
}
private void restorePreferences() {
getFileEditorPane().restorePreferences();
}
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 );
initCaretParagraphListener( tab );
process( tab );
}
}
}
} );
}
private void initTabChangeListener() {
final FileEditorTabPane editorPane = getFileEditorPane();
editorPane.addTabChangeListener(
(ObservableValue<? extends Tab> tabPane,
final Tab oldTab, final Tab newTab) -> {
final FileEditorTab tab = (FileEditorTab)newTab;
if( tab != null ) {
getPreviewPane().setPath( tab.getPath() );
process( tab );
}
} );
}
private void initTextChangeListener( final FileEditorTab tab ) {
tab.addTextChangeListener( (ObservableValue<? extends String> editor,
final String oldValue, final String newValue) -> {
process( tab );
} );
}
private void initCaretParagraphListener( final FileEditorTab tab ) {
tab.addCaretParagraphListener( (ObservableValue<? extends Integer> editor,
final Integer oldValue, final Integer newValue) -> {
process( tab );
} );
}
private void process( final FileEditorTab tab ) {
final Processor<String> hpp = new HTMLPreviewProcessor( getPreviewPane() );
final Processor<String> mcrp = new MarkdownCaretReplacementProcessor( hpp );
final Processor<String> mp = new MarkdownProcessor( mcrp );
final Processor<String> mcip = new MarkdownCaretInsertionProcessor( mp, tab.getCaretPosition() );
final Processor<String> vp = new VariableProcessor( mcip, getResolvedMap() );
vp.processChain( tab.getEditorText() );
}
private MarkdownEditorPane getActiveEditor() {
return (MarkdownEditorPane)(getActiveFileEditor().getEditorPane());
}
private FileEditorTab getActiveFileEditor() {
return getFileEditorPane().getActiveFileEditor();
}
protected DefinitionPane createDefinitionPane() {
return new DefinitionPane( getTreeView() );
}
private DefinitionPane getDefinitionPane() {
if( this.definitionPane == null ) {
this.definitionPane = createDefinitionPane();
}
return this.definitionPane;
}
public MenuBar getMenuBar() {
return this.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 ) {
throw new RuntimeException( ex );
}
}
return this.treeView;
}
private InputStream asStream( final String resource ) {
return getClass().getResourceAsStream( resource );
}
private TreeView<String> createTreeView() throws IOException {
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();
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() );
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() );
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,
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 ];
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 );
Action helpAboutAction = new Action( Messages.get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
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 toolBar = ActionUtils.createToolBar(
fileNewAction,
fileOpenAction,
fileSaveAction,
null,
editUndoAction,
editRedoAction,
null,
insertBoldAction,
insertItalicAction,
insertBlockquoteAction,
insertCodeAction,
insertFencedCodeBlockAction,
null,
insertLinkAction,
insertImageAction,
null,
headers[ 0 ],
null,
insertUnorderedListAction,
insertOrderedListAction );
return new VBox( menuBar, toolBar );
}
private synchronized HTMLPreviewPane getPreviewPane() {
if( this.previewPane == null ) {
this.previewPane = new HTMLPreviewPane();
}
return this.previewPane;
}
}