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 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.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Tab;
import javafx.scene.control.Tooltip;
import javafx.scene.text.Text;
import javafx.stage.Window;
import org.fxmisc.richtext.StyleClassedTextArea;
import org.fxmisc.richtext.model.TwoDimensional.Position;
import org.fxmisc.undo.UndoManager;
import org.mozilla.universalchardet.UniversalDetector;
import java.io.File;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import static com.scrivenvar.Messages.get;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Locale.ENGLISH;
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
public final class FileEditorTab extends Tab {
private final Notifier mNotifier = Services.load( Notifier.class );
private final EditorPane mEditorPane = new MarkdownEditorPane();
private final ReadOnlyBooleanWrapper mModified = new ReadOnlyBooleanWrapper();
private final BooleanProperty canUndo = new SimpleBooleanProperty();
private final BooleanProperty canRedo = new SimpleBooleanProperty();
private Charset mEncoding = UTF_8;
private Path mPath;
public FileEditorTab( final Path path ) {
setPath( path );
mModified.addListener( ( observable, oldPath, newPath ) -> updateTab() );
setOnSelectionChanged( e -> {
if( isSelected() ) {
Platform.runLater( this::activated );
}
} );
}
private void updateTab() {
setText( getTabTitle() );
setGraphic( getModifiedMark() );
setTooltip( getTabTooltip() );
}
private String getTabTitle() {
return getPath().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();
mModified.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 );
}
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 && filePath.equals( check );
}
private void load() {
final Path path = getPath();
final File file = path.toFile();
try {
if( file.exists() ) {
if( file.canWrite() && file.canRead() ) {
final EditorPane pane = getEditorPane();
pane.setText( asString( Files.readAllBytes( path ) ) );
pane.scrollToTop();
}
else {
final String msg = get(
"FileEditor.loadFailed.message",
file.toString(),
get( "FileEditor.loadFailed.reason.permissions" )
);
getNotifier().notify( msg );
}
}
} catch( final Exception ex ) {
getNotifier().notify( ex );
}
}
public boolean save() {
try {
final EditorPane editor = getEditorPane();
Files.write( getPath(), asBytes( editor.getText() ) );
editor.getUndoManager().mark();
return true;
} catch( final Exception ex ) {
return alert(
"FileEditor.saveFailed.title",
"FileEditor.saveFailed.message",
ex
);
}
}
@SuppressWarnings("SameParameterValue")
private boolean alert(
final String titleKey, final String messageKey, final Exception e ) {
final Notifier service = getNotifier();
final Path filePath = getPath();
final Notification message = service.createNotification(
get( titleKey ),
get( messageKey ),
filePath == null ? "" : filePath,
e.getMessage()
);
try {
service.createError( getWindow(), message ).showAndWait();
} catch( final Exception ex ) {
getNotifier().notify( ex );
}
return false;
}
private Window getWindow() {
final Scene scene = getEditorPane().getScene();
if( scene == null ) {
throw new UnsupportedOperationException( "No scene window available" );
}
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 mPath;
}
public void setPath( final Path path ) {
assert path != null;
mPath = path;
updateTab();
}
public boolean isModified() {
return mModified.get();
}
ReadOnlyBooleanProperty modifiedProperty() {
return mModified.getReadOnlyProperty();
}
BooleanProperty canUndoProperty() {
return this.canUndo;
}
BooleanProperty canRedoProperty() {
return this.canRedo;
}
private UndoManager<?> getUndoManager() {
return getEditorPane().getUndoManager();
}
public void addTextChangeListener( final ChangeListener<String> listener ) {
getEditorPane().addTextChangeListener( listener );
}
public void addCaretParagraphListener(
final ChangeListener<Integer> listener ) {
getEditorPane().addCaretParagraphListener( listener );
}
public <T extends Event> void addEventFilter(
final EventType<T> eventType,
final EventHandler<? super T> eventFilter ) {
getEditorPane().getEditor().addEventFilter( eventType, eventFilter );
}
public String getEditorText() {
return getEditorPane().getText();
}
public EditorPane getEditorPane() {
return mEditorPane;
}
private Charset getEncoding() {
return mEncoding;
}
private void setEncoding( final Charset encoding ) {
assert encoding != null;
mEncoding = encoding;
}
private Notifier getNotifier() {
return mNotifier;
}
@Override
public String toString() {
return getTabTitle();
}
}