package com.scrivenvar;
import com.scrivenvar.editors.EditorPane;
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
import com.scrivenvar.service.events.Notification;
import com.scrivenvar.service.events.Notifier;
import java.nio.charset.Charset;
import static java.nio.charset.StandardCharsets.UTF_8;
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.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.Event;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Tab;
import javafx.scene.control.Tooltip;
import javafx.scene.input.InputEvent;
import javafx.scene.text.Text;
import javafx.stage.Window;
import org.fxmisc.richtext.StyleClassedTextArea;
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
import org.fxmisc.richtext.model.TwoDimensional.Position;
import org.fxmisc.undo.UndoManager;
import org.fxmisc.wellbehaved.event.EventPattern;
import org.fxmisc.wellbehaved.event.InputMap;
import org.mozilla.universalchardet.UniversalDetector;
public final class FileEditorTab extends Tab {
private final Notifier alertService = Services.load( Notifier.class );
private EditorPane editorPane;
private Charset encoding;
private final ReadOnlyBooleanWrapper modified = new ReadOnlyBooleanWrapper();
private final BooleanProperty canUndo = new SimpleBooleanProperty();
private final BooleanProperty canRedo = new SimpleBooleanProperty();
private Path path;
FileEditorTab( final Path path ) {
setPath( path );
this.modified.addListener( (observable, oldPath, newPath) -> updateTab() );
setOnSelectionChanged( e -> {
if( isSelected() ) {
Platform.runLater( () -> activated() );
}
} );
}
private void updateTab() {
setText( getTabTitle() );
setGraphic( getModifiedMark() );
setTooltip( getTabTooltip() );
}
private String getTabTitle() {
final Path filePath = getPath();
return (filePath == null)
? Messages.get( "FileEditor.untitled" )
: filePath.getFileName().toString();
}
private Tooltip getTabTooltip() {
final Path filePath = getPath();
return new Tooltip( filePath == null ? "" : filePath.toString() );
}
private Text getModifiedMark() {
return isModified() ? new Text( "*" ) : null;
}
private void activated() {
if( getTabPane() == null || !isSelected() ) {
return;
}
if( getContent() != null ) {
getEditorPane().requestFocus();
return;
}
load();
initUndoManager();
initLayout();
initFocus();
}
private void initLayout() {
setContent( getScrollPane() );
}
private Node getScrollPane() {
return getEditorPane().getScrollPane();
}
private void initFocus() {
getEditorPane().requestFocus();
}
private void initUndoManager() {
final UndoManager undoManager = getUndoManager();
undoManager.forgetHistory();
modified.bind( Bindings.not( undoManager.atMarkedPositionProperty() ) );
canUndo.bind( undoManager.undoAvailableProperty() );
canRedo.bind( undoManager.redoAvailableProperty() );
}
public void searchNext( final String needle ) {
final String haystack = getEditorText();
int index = haystack.indexOf( needle, getCaretPosition() );
if( index == -1 ) {
index = haystack.indexOf( needle, 0 );
}
if( index >= 0 ) {
setCaretPosition( index );
getEditor().selectRange( index, index + needle.length() );
}
}
public int getCaretPosition() {
return getEditor().getCaretPosition();
}
private void setCaretPosition( final int offset ) {
getEditor().moveTo( offset );
getEditor().requestFollowCaret();
}
public Position getCaretOffset() {
return getEditor().offsetToPosition( getCaretPosition(), Forward );
}
public final ObservableValue<Integer> caretPositionProperty() {
return getEditor().caretPositionProperty();
}
private StyleClassedTextArea getEditor() {
return getEditorPane().getEditor();
}
public boolean isPath( final Path check ) {
final Path filePath = getPath();
return filePath == null ? false : filePath.equals( check );
}
private void load() {
final Path filePath = getPath();
if( filePath != null ) {
try {
getEditorPane().setText( asString( Files.readAllBytes( filePath ) ) );
} catch( final Exception ex ) {
getNotifyService().notify( ex );
}
}
}
public boolean save() {
try {
Files.write( getPath(), asBytes( getEditorPane().getText() ) );
getEditorPane().getUndoManager().mark();
return true;
} catch( final Exception ex ) {
return alert(
"FileEditor.saveFailed.title", "FileEditor.saveFailed.message", ex
);
}
}
private boolean alert(
final String titleKey, final String messageKey, final Exception e ) {
final Notifier service = getNotifyService();
final Path filePath = getPath();
final Notification message = service.createNotification(
Messages.get( titleKey ),
Messages.get( messageKey ),
filePath == null ? "" : filePath,
e.getMessage()
);
try {
service.createError( getWindow(), message ).showAndWait();
} catch( final Exception ex ) {
getNotifyService().notify( ex );
}
return false;
}
private Window getWindow() {
final Scene scene = getEditorPane().getScene();
if( scene == null ) {
throw new UnsupportedOperationException( "" );
}
return scene.getWindow();
}
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;
}
private byte[] asBytes( final String text ) {
return text.getBytes( getEncoding() );
}
private String asString( final byte[] text ) {
setEncoding( detectEncoding( text ) );
return new String( text, getEncoding() );
}
public Path getPath() {
return this.path;
}
public void setPath( final Path path ) {
this.path = path;
updateTab();
}
public boolean isFileOpen() {
return this.path != null;
}
public boolean isModified() {
return this.modified.get();
}
ReadOnlyBooleanProperty modifiedProperty() {
return this.modified.getReadOnlyProperty();
}
BooleanProperty canUndoProperty() {
return this.canUndo;
}
BooleanProperty canRedoProperty() {
return this.canRedo;
}
private UndoManager getUndoManager() {
return getEditorPane().getUndoManager();
}
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 );
}
public void addEventListener( final InputMap<InputEvent> map ) {
getEditorPane().addEventListener( map );
}
public void removeEventListener( final InputMap<InputEvent> map ) {
getEditorPane().removeEventListener( map );
}
public void addTextChangeListener( final ChangeListener<String> listener ) {
getEditorPane().addTextChangeListener( listener );
}
public void addCaretParagraphListener( final ChangeListener<Integer> listener ) {
getEditorPane().addCaretParagraphListener( listener );
}
public String getEditorText() {
return getEditorPane().getText();
}
public synchronized EditorPane getEditorPane() {
if( this.editorPane == null ) {
this.editorPane = new MarkdownEditorPane();
}
return this.editorPane;
}
private Notifier getNotifyService() {
return this.alertService;
}
private Charset getEncoding() {
if( this.encoding == null ) {
this.encoding = UTF_8;
}
return this.encoding;
}
private void setEncoding( final Charset encoding ) {
this.encoding = encoding;
}
@Override
public String toString() {
return getTabTitle();
}
}