Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git

Load embedded images for file relative to configured image directory

AuthorDaveJarvis <email>
Date2020-06-18 18:33:45 GMT-0700
Commit62c72610cd80927060991a70b648886494cf1a93
Parent6dcd675
src/main/java/com/scrivenvar/Constants.java
// Prevent double events when updating files on Linux (save and timestamp).
public static final int APP_WATCHDOG_TIMEOUT = get(
- "application.watchdog.timeout", 100 );
+ "application.watchdog.timeout", 200 );
public static final String STYLESHEET_SCENE = get( "file.stylesheet.scene" );
*/
public static final int DEFAULT_MAP_SIZE = 64;
-
- /**
- * Directory to search for images.
- */
- public static final String PERSIST_IMAGES_DIRECTORY = "imagesDirectory";
public static final String PERSIST_IMAGES_DEFAULT =
src/main/java/com/scrivenvar/FileEditorTabPane.java
"Dialog.file.choose.filter";
- private final Options mOptions = Services.load( Options.class );
- private final Settings mSettings = Services.load( Settings.class );
- private final Notifier mNotifyService = Services.load( Notifier.class );
-
- private final ReadOnlyObjectWrapper<Path> openDefinition =
- new ReadOnlyObjectWrapper<>();
- private final ReadOnlyObjectWrapper<FileEditorTab> mActiveFileEditor =
- new ReadOnlyObjectWrapper<>();
- private final ReadOnlyBooleanWrapper anyFileEditorModified =
- new ReadOnlyBooleanWrapper();
- private final Consumer<Double> mScrollEventObserver;
-
- /**
- * Constructs a new file editor tab pane.
- */
- public FileEditorTabPane( final Consumer<Double> scrollEventObserver ) {
- final ObservableList<Tab> tabs = getTabs();
-
- setFocusTraversable( false );
- setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
-
- addTabSelectionListener(
- ( ObservableValue<? extends Tab> tabPane,
- final Tab oldTab, final Tab newTab ) -> {
-
- if( newTab != null ) {
- mActiveFileEditor.set( (FileEditorTab) newTab );
- }
- }
- );
-
- final ChangeListener<Boolean> modifiedListener = ( observable, oldValue,
- newValue ) -> {
- for( final Tab tab : tabs ) {
- if( ((FileEditorTab) tab).isModified() ) {
- this.anyFileEditorModified.set( true );
- break;
- }
- }
- };
-
- tabs.addListener(
- (ListChangeListener<Tab>) change -> {
- while( change.next() ) {
- if( change.wasAdded() ) {
- change.getAddedSubList().forEach(
- ( tab ) -> ((FileEditorTab) tab).modifiedProperty()
- .addListener( modifiedListener ) );
- }
- else if( change.wasRemoved() ) {
- change.getRemoved().forEach(
- ( tab ) -> ((FileEditorTab) tab).modifiedProperty()
- .removeListener(
- modifiedListener ) );
- }
- }
-
- // Changes in the tabs may also change anyFileEditorModified property
- // (e.g. closed modified file)
- modifiedListener.changed( null, null, null );
- }
- );
-
- mScrollEventObserver = scrollEventObserver;
- }
-
- /**
- * Allows observers to be notified when the current file editor tab changes.
- *
- * @param listener The listener to notify of tab change events.
- */
- public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
- // Observe the tab so that when a new tab is opened or selected,
- // a notification is kicked off.
- getSelectionModel().selectedItemProperty().addListener( listener );
- }
-
- /**
- * Returns the tab that has keyboard focus.
- *
- * @return A non-null instance.
- */
- public FileEditorTab getActiveFileEditor() {
- return mActiveFileEditor.get();
- }
-
- /**
- * Returns the property corresponding to the tab that has focus.
- *
- * @return A non-null instance.
- */
- public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
- return mActiveFileEditor.getReadOnlyProperty();
- }
-
- /**
- * Property that can answer whether the text has been modified.
- *
- * @return A non-null instance, true meaning the content has not been saved.
- */
- ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
- return this.anyFileEditorModified.getReadOnlyProperty();
- }
-
- /**
- * Creates a new editor instance from the given path.
- *
- * @param path The file to open.
- * @return A non-null instance.
- */
- private FileEditorTab createFileEditor( final Path path ) {
- assert path != null;
-
- final FileEditorTab tab = new FileEditorTab( path );
-
- tab.getEditorPane().getScrollPane().estimatedScrollYProperty().addObserver(
- mScrollEventObserver
- );
-
- tab.setOnCloseRequest( e -> {
- if( !canCloseEditor( tab ) ) {
- e.consume();
- }
- else if( isActiveFileEditor( tab ) ) {
- // Prevent prompting the user to save when there are no file editor
- // tabs open.
- mActiveFileEditor.set( null );
- }
- } );
-
- 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" );
- }
-
- /**
- * Called when the user selects New from the File menu.
- */
- void newEditor() {
- final Path defaultPath = getDefaultPath();
- final FileEditorTab tab = createFileEditor( defaultPath );
-
- 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 );
- }
- }
-
- /**
- * Opens the files into new editors, unless one of those files was a
- * definition file. The definition file is loaded into the definition pane,
- * but only the first one selected (multiple definition files will result in a
- * warning).
- *
- * @param files The list of non-definition files that the were requested to
- * open.
- */
- private void openFiles( final List<File> files ) {
- final List<String> extensions =
- createExtensionFilter( DEFINITION ).getExtensions();
- final FileTypePredicate predicate =
- new FileTypePredicate( extensions );
-
- // The user might have opened multiple definitions files. These will
- // be discarded from the text editable files.
- final List<File> definitions
- = files.stream().filter( predicate ).collect( Collectors.toList() );
-
- // Create a modifiable list to remove any definition files that were
- // opened.
- final List<File> editors = new ArrayList<>( files );
-
- if( !editors.isEmpty() ) {
- saveLastDirectory( editors.get( 0 ) );
- }
-
- editors.removeAll( definitions );
-
- // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
- 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();
-
- // Close single unmodified "Untitled" tab.
- 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 );
-
- // Only open new files.
- if( fileEditorTab == null ) {
- fileEditorTab = createFileEditor( path );
- getTabs().add( fileEditorTab );
- }
-
- // Select the first file in the list.
- if( i == activeIndex ) {
- getSelectionModel().select( fileEditorTab );
- }
- }
- }
-
- /**
- * Returns a property that changes when a new definition file is opened.
- *
- * @return The path to a definition file that was opened.
- */
- public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
- return getOnOpenDefinitionFile().getReadOnlyProperty();
- }
-
- private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
- return this.openDefinition;
- }
-
- /**
- * Called when the user has opened a definition file (using the file open
- * dialog box). This will replace the current set of definitions for the
- * active tab.
- *
- * @param definition The file to open.
- */
- private void openDefinition( final File definition ) {
- // TODO: Prevent reading this file twice when a new text document is opened.
- // (might be a matter of checking the value first).
- getOnOpenDefinitionFile().set( definition.toPath() );
- }
-
- /**
- * Called when the contents of the editor are to be saved.
- *
- * @param tab The tab containing content to save.
- * @return true The contents were saved (or needn't be saved).
- */
- public boolean saveEditor( final FileEditorTab tab ) {
- if( tab == null || !tab.isModified() ) {
- return true;
- }
-
- return tab.getPath() == null ? saveEditorAs( tab ) : tab.save();
- }
-
- /**
- * Opens the Save As dialog for the user to save the content under a new
- * path.
- *
- * @param tab The tab with contents to save.
- * @return true The contents were saved, or the tab was null.
- */
- 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 );
- }
- }
-
- /**
- * Answers whether the file has had modifications. '
- *
- * @param tab THe tab to check for modifications.
- * @return false The file is unmodified.
- */
- @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();
- }
-
- private Notifier getNotifyService() {
- return this.mNotifyService;
- }
-
- 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();
-
- // try to save active tab first because in case the user decides to cancel,
- // then it stays active
- if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
- return false;
- }
-
- // This should be called any time a tab changes.
- persistPreferences();
-
- // save modified tabs
- for( int i = 0; i < allEditors.length; i++ ) {
- final FileEditorTab fileEditor = allEditors[ i ];
-
- if( fileEditor == activeEditor ) {
- continue;
- }
-
- if( fileEditor.isModified() ) {
- // activate the modified tab to make its modified content visible to
- // the user
- getSelectionModel().select( i );
-
- if( !canCloseEditor( fileEditor ) ) {
- return false;
- }
- }
- }
-
- // Close all tabs.
- 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;
- }
-
- /**
- * Returns the file editor tab that has the given path.
- *
- * @return null No file editor tab for the given path was found.
- */
- 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<>();
-
- // TODO: Return a list of all properties that match the filter prefix.
- // This will allow dynamic filters to be added and removed just by
- // updating the properties file.
- list.add( createExtensionFilter( ALL ) );
- list.add( createExtensionFilter( SOURCE ) );
- list.add( createExtensionFilter( DEFINITION ) );
- list.add( createExtensionFilter( XML ) );
- return list;
- }
-
- /**
- * Returns a filter for file name extensions recognized by the application
- * that can be opened by the user.
- *
- * @param filetype Used to find the globbing pattern for extensions.
- * @return A filename filter suitable for use by a FileDialog instance.
- */
- 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 List<String> getExtensions( final String key ) {
- return getSettings().getStringSettingList( key );
- }
-
- private void saveLastDirectory( final File file ) {
- getPreferences().put( "lastDirectory", file.getParent() );
- }
-
- public void restorePreferences() {
- 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 Settings getSettings() {
- return mSettings;
- }
-
- protected Options getOptions() {
- return mOptions;
+ 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> openDefinition =
+ new ReadOnlyObjectWrapper<>();
+ private final ReadOnlyObjectWrapper<FileEditorTab> mActiveFileEditor =
+ new ReadOnlyObjectWrapper<>();
+ private final ReadOnlyBooleanWrapper anyFileEditorModified =
+ new ReadOnlyBooleanWrapper();
+ private final Consumer<Double> mScrollEventObserver;
+
+ /**
+ * Constructs a new file editor tab pane.
+ */
+ public FileEditorTabPane( final Consumer<Double> scrollEventObserver ) {
+ final ObservableList<Tab> tabs = getTabs();
+
+ setFocusTraversable( false );
+ setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
+
+ addTabSelectionListener(
+ ( ObservableValue<? extends Tab> tabPane,
+ final Tab oldTab, final Tab newTab ) -> {
+
+ if( newTab != null ) {
+ mActiveFileEditor.set( (FileEditorTab) newTab );
+ }
+ }
+ );
+
+ final ChangeListener<Boolean> modifiedListener = ( observable, oldValue,
+ newValue ) -> {
+ for( final Tab tab : tabs ) {
+ if( ((FileEditorTab) tab).isModified() ) {
+ this.anyFileEditorModified.set( true );
+ break;
+ }
+ }
+ };
+
+ tabs.addListener(
+ (ListChangeListener<Tab>) change -> {
+ while( change.next() ) {
+ if( change.wasAdded() ) {
+ change.getAddedSubList().forEach(
+ ( tab ) -> ((FileEditorTab) tab).modifiedProperty()
+ .addListener( modifiedListener ) );
+ }
+ else if( change.wasRemoved() ) {
+ change.getRemoved().forEach(
+ ( tab ) -> ((FileEditorTab) tab).modifiedProperty()
+ .removeListener(
+ modifiedListener ) );
+ }
+ }
+
+ // Changes in the tabs may also change anyFileEditorModified property
+ // (e.g. closed modified file)
+ modifiedListener.changed( null, null, null );
+ }
+ );
+
+ mScrollEventObserver = scrollEventObserver;
+ }
+
+ /**
+ * Allows observers to be notified when the current file editor tab changes.
+ *
+ * @param listener The listener to notify of tab change events.
+ */
+ public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
+ // Observe the tab so that when a new tab is opened or selected,
+ // a notification is kicked off.
+ getSelectionModel().selectedItemProperty().addListener( listener );
+ }
+
+ /**
+ * Returns the tab that has keyboard focus.
+ *
+ * @return A non-null instance.
+ */
+ public FileEditorTab getActiveFileEditor() {
+ return mActiveFileEditor.get();
+ }
+
+ /**
+ * Returns the property corresponding to the tab that has focus.
+ *
+ * @return A non-null instance.
+ */
+ public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
+ return mActiveFileEditor.getReadOnlyProperty();
+ }
+
+ /**
+ * Property that can answer whether the text has been modified.
+ *
+ * @return A non-null instance, true meaning the content has not been saved.
+ */
+ ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
+ return this.anyFileEditorModified.getReadOnlyProperty();
+ }
+
+ /**
+ * Creates a new editor instance from the given path.
+ *
+ * @param path The file to open.
+ * @return A non-null instance.
+ */
+ private FileEditorTab createFileEditor( final Path path ) {
+ assert path != null;
+
+ final FileEditorTab tab = new FileEditorTab( path );
+
+ tab.getEditorPane().getScrollPane().estimatedScrollYProperty().addObserver(
+ mScrollEventObserver
+ );
+
+ tab.setOnCloseRequest( e -> {
+ if( !canCloseEditor( tab ) ) {
+ e.consume();
+ }
+ else if( isActiveFileEditor( tab ) ) {
+ // Prevent prompting the user to save when there are no file editor
+ // tabs open.
+ mActiveFileEditor.set( null );
+ }
+ } );
+
+ 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" );
+ }
+
+ /**
+ * Called when the user selects New from the File menu.
+ */
+ void newEditor() {
+ final Path defaultPath = getDefaultPath();
+ final FileEditorTab tab = createFileEditor( defaultPath );
+
+ 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 );
+ }
+ }
+
+ /**
+ * Opens the files into new editors, unless one of those files was a
+ * definition file. The definition file is loaded into the definition pane,
+ * but only the first one selected (multiple definition files will result in a
+ * warning).
+ *
+ * @param files The list of non-definition files that the were requested to
+ * open.
+ */
+ private void openFiles( final List<File> files ) {
+ final List<String> extensions =
+ createExtensionFilter( DEFINITION ).getExtensions();
+ final FileTypePredicate predicate =
+ new FileTypePredicate( extensions );
+
+ // The user might have opened multiple definitions files. These will
+ // be discarded from the text editable files.
+ final List<File> definitions
+ = files.stream().filter( predicate ).collect( Collectors.toList() );
+
+ // Create a modifiable list to remove any definition files that were
+ // opened.
+ final List<File> editors = new ArrayList<>( files );
+
+ if( !editors.isEmpty() ) {
+ saveLastDirectory( editors.get( 0 ) );
+ }
+
+ editors.removeAll( definitions );
+
+ // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
+ 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();
+
+ // Close single unmodified "Untitled" tab.
+ 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 );
+
+ // Only open new files.
+ if( fileEditorTab == null ) {
+ fileEditorTab = createFileEditor( path );
+ getTabs().add( fileEditorTab );
+ }
+
+ // Select the first file in the list.
+ if( i == activeIndex ) {
+ getSelectionModel().select( fileEditorTab );
+ }
+ }
+ }
+
+ /**
+ * Returns a property that changes when a new definition file is opened.
+ *
+ * @return The path to a definition file that was opened.
+ */
+ public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
+ return getOnOpenDefinitionFile().getReadOnlyProperty();
+ }
+
+ private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
+ return this.openDefinition;
+ }
+
+ /**
+ * Called when the user has opened a definition file (using the file open
+ * dialog box). This will replace the current set of definitions for the
+ * active tab.
+ *
+ * @param definition The file to open.
+ */
+ private void openDefinition( final File definition ) {
+ // TODO: Prevent reading this file twice when a new text document is opened.
+ // (might be a matter of checking the value first).
+ getOnOpenDefinitionFile().set( definition.toPath() );
+ }
+
+ /**
+ * Called when the contents of the editor are to be saved.
+ *
+ * @param tab The tab containing content to save.
+ * @return true The contents were saved (or needn't be saved).
+ */
+ public boolean saveEditor( final FileEditorTab tab ) {
+ if( tab == null || !tab.isModified() ) {
+ return true;
+ }
+
+ return tab.getPath() == null ? saveEditorAs( tab ) : tab.save();
+ }
+
+ /**
+ * Opens the Save As dialog for the user to save the content under a new
+ * path.
+ *
+ * @param tab The tab with contents to save.
+ * @return true The contents were saved, or the tab was null.
+ */
+ 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 );
+ }
+ }
+
+ /**
+ * Answers whether the file has had modifications. '
+ *
+ * @param tab THe tab to check for modifications.
+ * @return false The file is unmodified.
+ */
+ @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();
+
+ // try to save active tab first because in case the user decides to cancel,
+ // then it stays active
+ if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
+ return false;
+ }
+
+ // This should be called any time a tab changes.
+ persistPreferences();
+
+ // save modified tabs
+ for( int i = 0; i < allEditors.length; i++ ) {
+ final FileEditorTab fileEditor = allEditors[ i ];
+
+ if( fileEditor == activeEditor ) {
+ continue;
+ }
+
+ if( fileEditor.isModified() ) {
+ // activate the modified tab to make its modified content visible to
+ // the user
+ getSelectionModel().select( i );
+
+ if( !canCloseEditor( fileEditor ) ) {
+ return false;
+ }
+ }
+ }
+
+ // Close all tabs.
+ 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;
+ }
+
+ /**
+ * Returns the file editor tab that has the given path.
+ *
+ * @return null No file editor tab for the given path was found.
+ */
+ 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<>();
+
+ // TODO: Return a list of all properties that match the filter prefix.
+ // This will allow dynamic filters to be added and removed just by
+ // updating the properties file.
+ list.add( createExtensionFilter( ALL ) );
+ list.add( createExtensionFilter( SOURCE ) );
+ list.add( createExtensionFilter( DEFINITION ) );
+ list.add( createExtensionFilter( XML ) );
+ return list;
+ }
+
+ /**
+ * Returns a filter for file name extensions recognized by the application
+ * that can be opened by the user.
+ *
+ * @param filetype Used to find the globbing pattern for extensions.
+ * @return A filename filter suitable for use by a FileDialog instance.
+ */
+ 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 List<String> getExtensions( final String key ) {
+ return getSettings().getStringSettingList( key );
+ }
+
+ private void saveLastDirectory( final File file ) {
+ getPreferences().put( "lastDirectory", file.getParent() );
+ }
+
+ public void restorePreferences() {
+ 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 Notifier getNotifyService() {
+ return sNotifier;
+ }
+
+ private Settings getSettings() {
+ return sSettings;
+ }
+
+ protected Options getOptions() {
+ return sOptions;
}
src/main/java/com/scrivenvar/MainWindow.java
initTextChangeListener( tab );
- //initCaretParagraphListener( tab );
initKeyboardEventListeners( tab );
// initSyntaxListener( tab );
src/main/java/com/scrivenvar/preview/HTMLPreviewPane.java
}
- private Path getPath() {
+ public Path getPath() {
return mPath;
}
src/main/java/com/scrivenvar/preview/SVGRasterizer.java
}
+ public static BufferedImage rasterize( final String url, final int width )
+ throws IOException, TranscoderException {
+ return rasterize( new URL( url ), width );
+ }
+
public static BufferedImage rasterize( final URL url, final int width )
throws IOException, TranscoderException {
src/main/java/com/scrivenvar/preview/SVGReplacedElementFactory.java
import java.awt.*;
-import java.io.File;
-import java.net.MalformedURLException;
-import java.net.URL;
-
-import static com.scrivenvar.util.ProtocolResolver.getProtocol;
public class SVGReplacedElementFactory
if( SVG_FILE.equalsIgnoreCase( ext ) ) {
try {
- final URL url = getUrl( src );
final int width = box.getContentWidth();
- final Image image = SVGRasterizer.rasterize( url, width );
+ final Image image = SVGRasterizer.rasterize( src, width );
final int w = image.getWidth( null );
@Override
public void setFormSubmissionListener( FormSubmissionListener listener ) {
- }
-
- private URL getUrl( final String src ) throws MalformedURLException {
- return "file".equals( getProtocol( src ) )
- ? new File( src ).toURI().toURL()
- : new URL( src );
}
src/main/java/com/scrivenvar/processors/ProcessorFactory.java
private Processor<String> createMarkdownProcessor() {
final var hpp = createHTMLPreviewProcessor();
- return new MarkdownProcessor( hpp );
+ return new MarkdownProcessor( hpp, getPreviewPane().getPath() );
}
src/main/java/com/scrivenvar/processors/markdown/ImageLinkExtension.java
import com.scrivenvar.service.Options;
import com.scrivenvar.service.events.Notifier;
+import com.scrivenvar.util.ProtocolResolver;
import com.vladsch.flexmark.ast.Image;
import com.vladsch.flexmark.html.HtmlRenderer;
import java.io.File;
+import java.nio.file.Path;
import static java.lang.String.format;
*/
public class ImageLinkExtension implements HtmlRenderer.HtmlRendererExtension {
- private final static Options OPTIONS = Services.load( Options.class );
- private final static Notifier NOTIFIER = Services.load( Notifier.class );
+ private final static Options sOptions = Services.load( Options.class );
+ private final static Notifier sNotifier = Services.load( Notifier.class );
- public static ImageLinkExtension create() {
- return new ImageLinkExtension();
+ /**
+ * Creates an extension capable of using a relative path to embed images.
+ *
+ * @param path The {@link Path} to the file being edited; the parent path
+ * is the starting location of the relative image directory.
+ * @return The new {@link ImageLinkExtension}, never {@code null}.
+ */
+ public static ImageLinkExtension create( final Path path ) {
+ return new ImageLinkExtension( path );
}
private class ImageLinkResolver implements LinkResolver {
private final UserPreferences mUserPref = getUserPreferences();
- private final String mImagePrefix = mUserPref.getImagesDirectory()
- .toString();
- private final String mImageuffixes = mUserPref.getImagesOrder();
+ private final String mImagePrefix =
+ mUserPref.getImagesDirectory().toString();
+ private final String mImageSuffixes = mUserPref.getImagesOrder();
public ImageLinkResolver() {
}
// you can also set/clear/modify attributes through
// ResolvedLink.getAttributes() and
// ResolvedLink.getNonNullAttributes()
@NotNull
+ @Override
public ResolvedLink resolveLink(
@NotNull final Node node,
@NotNull final LinkResolverBasicContext context,
@NotNull final ResolvedLink link ) {
- return node instanceof Image ? resolve( (Image) node, link ) : link;
+ return node instanceof Image ? resolve( link ) : link;
}
@NotNull
- private ResolvedLink resolve(
- @NotNull final Image image, @NotNull final ResolvedLink link ) {
+ private ResolvedLink resolve( @NotNull final ResolvedLink link ) {
String url = link.getUrl();
try {
- final String filename = format( "%s/%s", getImagePrefix(), url );
+ final String imageFile = format( "%s/%s", getImagePrefix(), url );
final String suffixes = getImageSuffixes();
+ final String editDir = getEditDirectory();
for( final String ext : Splitter.on( ' ' ).split( suffixes ) ) {
- final File file = new File( format( "%s.%s", filename, ext ) );
+ final String imagePath = format(
+ "%s/%s.%s", editDir, imageFile, ext );
+ final File file = new File( imagePath );
if( file.exists() ) {
url = file.toString();
break;
}
}
- System.out.println( "URL: " + url );
+ final String protocol = ProtocolResolver.getProtocol( url );
+ if( "file".equals( protocol ) ) {
+ url = "file://" + url;
+ }
return link.withStatus( LinkStatus.VALID ).withUrl( url );
private String getImageSuffixes() {
- return mImageuffixes;
+ return mImageSuffixes;
+ }
+
+ private String getEditDirectory() {
+ return mPath.getParent().toString();
}
}
- private ImageLinkExtension() {
+ private final Path mPath;
+
+ private ImageLinkExtension( final Path path ) {
+ mPath = path;
}
private Options getOptions() {
- return OPTIONS;
+ return sOptions;
}
private Notifier getNotifier() {
- return NOTIFIER;
+ return sNotifier;
}
}
src/main/java/com/scrivenvar/processors/markdown/MarkdownProcessor.java
import com.vladsch.flexmark.util.misc.Extension;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
+
+import static com.scrivenvar.Constants.USER_DIRECTORY;
/**
* Responsible for parsing a Markdown document and rendering it as HTML.
*
* @author White Magic Software, Ltd.
*/
public class MarkdownProcessor extends AbstractProcessor<String> {
-
- private final static HtmlRenderer RENDERER;
- private final static IParse PARSER;
- static {
- final Collection<Extension> extensions = new ArrayList<>();
- extensions.add( TablesExtension.create() );
- extensions.add( SuperscriptExtension.create() );
- extensions.add( StrikethroughSubscriptExtension.create() );
- extensions.add( ImageLinkExtension.create() );
+ private final HtmlRenderer mRenderer;
+ private final IParse mParser;
- RENDERER = HtmlRenderer.builder().extensions( extensions ).build();
- PARSER = Parser.builder().extensions( extensions ).build();
+ public MarkdownProcessor(
+ final Processor<String> successor ) {
+ this( successor, Path.of( USER_DIRECTORY ) );
}
/**
* Constructs a new Markdown processor that can create HTML documents.
*
* @param successor Usually the HTML Preview Processor.
*/
- public MarkdownProcessor( final Processor<String> successor ) {
+ public MarkdownProcessor(
+ final Processor<String> successor, final Path path ) {
super( successor );
+
+ final Collection<Extension> extensions = new ArrayList<>();
+ extensions.add( TablesExtension.create() );
+ extensions.add( SuperscriptExtension.create() );
+ extensions.add( StrikethroughSubscriptExtension.create() );
+ extensions.add( ImageLinkExtension.create( path ) );
+
+ mRenderer = HtmlRenderer.builder().extensions( extensions ).build();
+ mParser = Parser.builder().extensions( extensions ).build();
}
*/
private IParse getParser() {
- return PARSER;
+ return mParser;
}
private HtmlRenderer getRenderer() {
- return RENDERER;
+ return mRenderer;
}
}
Delta630 lines added, 614 lines removed, 16-line increase