package com.scrivenvar;
import com.scrivenvar.predicates.files.FileTypePredicate;
import com.scrivenvar.service.Options;
import com.scrivenvar.service.Settings;
import com.scrivenvar.service.events.Notification;
import com.scrivenvar.service.events.Notifier;
import com.scrivenvar.util.Utils;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.value.ChangeListener;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.stage.FileChooser;
import javafx.stage.FileChooser.ExtensionFilter;
import javafx.stage.Window;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.prefs.Preferences;
import java.util.stream.Collectors;
import static com.scrivenvar.Constants.GLOB_PREFIX_FILE;
import static com.scrivenvar.FileType.*;
import static com.scrivenvar.Messages.get;
import static com.scrivenvar.service.events.Notifier.YES;
public final class FileEditorTabPane extends TabPane {
private final static String FILTER_EXTENSION_TITLES =
"Dialog.file.choose.filter";
private final static Options sOptions = Services.load( Options.class );
private final static Settings sSettings = Services.load( Settings.class );
private final static Notifier sNotifier = Services.load( Notifier.class );
private final ReadOnlyObjectWrapper<Path> mOpenDefinition =
new ReadOnlyObjectWrapper<>();
private final ReadOnlyObjectWrapper<FileEditorTab> mActiveFileEditor =
new ReadOnlyObjectWrapper<>();
private final ReadOnlyBooleanWrapper mAnyFileEditorModified =
new ReadOnlyBooleanWrapper();
private final ChangeListener<Integer> mCaretPositionListener;
private final ChangeListener<Integer> mCaretParagraphListener;
public FileEditorTabPane(
final ChangeListener<Integer> caretPositionListener,
final ChangeListener<Integer> caretParagraphListener ) {
final ObservableList<Tab> tabs = getTabs();
setFocusTraversable( false );
setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
addTabSelectionListener(
( tabPane, oldTab, newTab ) -> {
if( newTab != null ) {
mActiveFileEditor.set( (FileEditorTab) newTab );
}
}
);
final ChangeListener<Boolean> modifiedListener =
( observable, oldValue, newValue ) -> {
for( final Tab tab : tabs ) {
if( ((FileEditorTab) tab).isModified() ) {
mAnyFileEditorModified.set( true );
break;
}
}
};
tabs.addListener(
(ListChangeListener<Tab>) change -> {
while( change.next() ) {
if( change.wasAdded() ) {
change.getAddedSubList().forEach(
( tab ) -> {
final var fet = (FileEditorTab) tab;
fet.modifiedProperty()
.addListener( modifiedListener );
} );
}
else if( change.wasRemoved() ) {
change.getRemoved().forEach(
( tab ) ->
((FileEditorTab) tab).modifiedProperty()
.removeListener( modifiedListener ) );
}
}
modifiedListener.changed( null, null, null );
}
);
mCaretPositionListener = caretPositionListener;
mCaretParagraphListener = caretParagraphListener;
}
public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
getSelectionModel().selectedItemProperty().addListener( listener );
}
public FileEditorTab getActiveFileEditor() {
return mActiveFileEditor.get();
}
public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
return mActiveFileEditor.getReadOnlyProperty();
}
ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
return mAnyFileEditorModified.getReadOnlyProperty();
}
private FileEditorTab createFileEditor( final Path path ) {
assert path != null;
final FileEditorTab tab = new FileEditorTab( path );
tab.setOnCloseRequest( e -> {
if( !canCloseEditor( tab ) ) {
e.consume();
}
else if( isActiveFileEditor( tab ) ) {
mActiveFileEditor.set( null );
}
} );
tab.addCaretPositionListener( mCaretPositionListener );
tab.addCaretParagraphListener( mCaretParagraphListener );
return tab;
}
private boolean isActiveFileEditor( final FileEditorTab tab ) {
return getActiveFileEditor() == tab;
}
private Path getDefaultPath() {
final String filename = getDefaultFilename();
return (new File( filename )).toPath();
}
private String getDefaultFilename() {
return getSettings().getSetting( "file.default", "untitled.md" );
}
void newEditor() {
final FileEditorTab tab = createFileEditor( getDefaultPath() );
getTabs().add( tab );
getSelectionModel().select( tab );
}
void openFileDialog() {
final String title = get( "Dialog.file.choose.open.title" );
final FileChooser dialog = createFileChooser( title );
final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
if( files != null ) {
openFiles( files );
}
}
private void openFiles( final List<File> files ) {
final List<String> extensions =
createExtensionFilter( DEFINITION ).getExtensions();
final FileTypePredicate predicate =
new FileTypePredicate( extensions );
final List<File> definitions
= files.stream().filter( predicate ).collect( Collectors.toList() );
final List<File> editors = new ArrayList<>( files );
if( !editors.isEmpty() ) {
saveLastDirectory( editors.get( 0 ) );
}
editors.removeAll( definitions );
if( !editors.isEmpty() ) {
openEditors( editors, 0 );
}
if( !definitions.isEmpty() ) {
openDefinition( definitions.get( 0 ) );
}
}
private void openEditors( final List<File> files, final int activeIndex ) {
final int fileTally = files.size();
final List<Tab> tabs = getTabs();
if( tabs.size() == 1 ) {
final FileEditorTab fileEditor = (FileEditorTab) (tabs.get( 0 ));
if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
closeEditor( fileEditor, false );
}
}
for( int i = 0; i < fileTally; i++ ) {
final Path path = files.get( i ).toPath();
FileEditorTab fileEditorTab = findEditor( path );
if( fileEditorTab == null ) {
fileEditorTab = createFileEditor( path );
getTabs().add( fileEditorTab );
}
if( i == activeIndex ) {
getSelectionModel().select( fileEditorTab );
}
}
}
public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
return getOnOpenDefinitionFile().getReadOnlyProperty();
}
private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
return mOpenDefinition;
}
private void openDefinition( final File definition ) {
getOnOpenDefinitionFile().set( definition.toPath() );
}
public boolean saveEditor( final FileEditorTab tab ) {
if( tab == null || !tab.isModified() ) {
return true;
}
return tab.getPath() == null ? saveEditorAs( tab ) : tab.save();
}
public boolean saveEditorAs( final FileEditorTab tab ) {
if( tab == null ) {
return true;
}
getSelectionModel().select( tab );
final FileChooser fileChooser = createFileChooser( get(
"Dialog.file.choose.save.title" ) );
final File file = fileChooser.showSaveDialog( getWindow() );
if( file == null ) {
return false;
}
saveLastDirectory( file );
tab.setPath( file.toPath() );
return tab.save();
}
void saveAllEditors() {
for( final FileEditorTab fileEditor : getAllEditors() ) {
saveEditor( fileEditor );
}
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
boolean canCloseEditor( final FileEditorTab tab ) {
final AtomicReference<Boolean> canClose = new AtomicReference<>();
canClose.set( true );
if( tab.isModified() ) {
final Notification message = getNotifyService().createNotification(
Messages.get( "Alert.file.close.title" ),
Messages.get( "Alert.file.close.text" ),
tab.getText()
);
final Alert confirmSave = getNotifyService().createConfirmation(
getWindow(), message );
final Optional<ButtonType> buttonType = confirmSave.showAndWait();
buttonType.ifPresent(
save -> canClose.set(
save == YES ? saveEditor( tab ) : save == ButtonType.NO
)
);
}
return canClose.get();
}
boolean closeEditor( final FileEditorTab tab, final boolean save ) {
if( tab == null ) {
return true;
}
if( save ) {
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() {
final FileEditorTab[] allEditors = getAllEditors();
final FileEditorTab activeEditor = getActiveFileEditor();
if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
return false;
}
persistPreferences();
for( int i = 0; i < allEditors.length; i++ ) {
final FileEditorTab fileEditor = allEditors[ i ];
if( fileEditor == activeEditor ) {
continue;
}
if( fileEditor.isModified() ) {
getSelectionModel().select( i );
if( !canCloseEditor( fileEditor ) ) {
return false;
}
}
}
for( final FileEditorTab fileEditor : allEditors ) {
if( !closeEditor( fileEditor, false ) ) {
return false;
}
}
return getTabs().isEmpty();
}
private FileEditorTab[] getAllEditors() {
final ObservableList<Tab> tabs = getTabs();
final int length = tabs.size();
final FileEditorTab[] allEditors = new FileEditorTab[ length ];
for( int i = 0; i < length; i++ ) {
allEditors[ i ] = (FileEditorTab) tabs.get( i );
}
return allEditors;
}
private FileEditorTab findEditor( final Path path ) {
for( final Tab tab : getTabs() ) {
final FileEditorTab fileEditor = (FileEditorTab) tab;
if( fileEditor.isPath( path ) ) {
return fileEditor;
}
}
return null;
}
private FileChooser createFileChooser( String title ) {
final FileChooser fileChooser = new FileChooser();
fileChooser.setTitle( title );
fileChooser.getExtensionFilters().addAll(
createExtensionFilters() );
final String lastDirectory = getPreferences().get( "lastDirectory", null );
File file = new File( (lastDirectory != null) ? lastDirectory : "." );
if( !file.isDirectory() ) {
file = new File( "." );
}
fileChooser.setInitialDirectory( file );
return fileChooser;
}
private List<ExtensionFilter> createExtensionFilters() {
final List<ExtensionFilter> list = new ArrayList<>();
list.add( createExtensionFilter( ALL ) );
list.add( createExtensionFilter( SOURCE ) );
list.add( createExtensionFilter( DEFINITION ) );
list.add( createExtensionFilter( XML ) );
return list;
}
private ExtensionFilter createExtensionFilter( final FileType filetype ) {
final String tKey = String.format( "%s.title.%s",
FILTER_EXTENSION_TITLES,
filetype );
final String eKey = String.format( "%s.%s", GLOB_PREFIX_FILE, filetype );
return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) );
}
private void saveLastDirectory( final File file ) {
getPreferences().put( "lastDirectory", file.getParent() );
}
public void initPreferences() {
int activeIndex = 0;
final Preferences preferences = getPreferences();
final String[] fileNames = Utils.getPrefsStrings( preferences, "file" );
final String activeFileName = preferences.get( "activeFile", null );
final List<File> files = new ArrayList<>( fileNames.length );
for( final String fileName : fileNames ) {
final File file = new File( fileName );
if( file.exists() ) {
files.add( file );
if( fileName.equals( activeFileName ) ) {
activeIndex = files.size() - 1;
}
}
}
if( files.isEmpty() ) {
newEditor();
}
else {
openEditors( files, activeIndex );
}
}
public void persistPreferences() {
final ObservableList<Tab> allEditors = getTabs();
final List<String> fileNames = new ArrayList<>( allEditors.size() );
for( final Tab tab : allEditors ) {
final FileEditorTab fileEditor = (FileEditorTab) tab;
final Path filePath = fileEditor.getPath();
if( filePath != null ) {
fileNames.add( filePath.toString() );
}
}
final Preferences preferences = getPreferences();
Utils.putPrefsStrings( preferences,
"file",
fileNames.toArray( new String[ 0 ] ) );
final FileEditorTab activeEditor = getActiveFileEditor();
final Path filePath = activeEditor == null ? null : activeEditor.getPath();
if( filePath == null ) {
preferences.remove( "activeFile" );
}
else {
preferences.put( "activeFile", filePath.toString() );
}
}
private List<String> getExtensions( final String key ) {
return getSettings().getStringSettingList( key );
}
private Notifier getNotifyService() {
return sNotifier;
}
private Settings getSettings() {
return sSettings;
}
protected Options getOptions() {
return sOptions;
}
private Window getWindow() {
return getScene().getWindow();
}
private Preferences getPreferences() {
return getOptions().getState();
}
Node getNode() {
return this;
}
}