| | |
| | /** |
| | - * TODO: Load divider positions from exported settings, see bin() comment. |
| | - */ |
| | - private double[] calculateDividerPositions() { |
| | - final var ratio = 100f / getItems().size() / 100; |
| | - final var positions = getDividerPositions(); |
| | - |
| | - for( int i = 0; i < positions.length; i++ ) { |
| | - positions[ i ] = ratio * i; |
| | - } |
| | - |
| | - return positions; |
| | - } |
| | - |
| | - /** |
| | - * Opens all the files into the application, provided the paths are unique. |
| | - * This may only be called for any type of files that a user can edit |
| | - * (i.e., update and persist), such as definitions and text files. |
| | - * |
| | - * @param files The list of files to open. |
| | - */ |
| | - public void open( final List<File> files ) { |
| | - files.forEach( this::open ); |
| | - } |
| | - |
| | - /** |
| | - * This opens the given file. Since the preview pane is not a file that |
| | - * can be opened, it is safe to add a listener to the detachable pane. |
| | - * |
| | - * @param file The file to open. |
| | - */ |
| | - private void open( final File file ) { |
| | - final var tab = createTab( file ); |
| | - final var node = tab.getContent(); |
| | - final var mediaType = MediaType.valueFrom( file ); |
| | - final var tabPane = obtainTabPane( mediaType ); |
| | - |
| | - tab.setTooltip( createTooltip( file ) ); |
| | - tabPane.setFocusTraversable( false ); |
| | - tabPane.setTabClosingPolicy( ALL_TABS ); |
| | - tabPane.getTabs().add( tab ); |
| | - |
| | - // Attach the tab scene factory for new tab panes. |
| | - if( !getItems().contains( tabPane ) ) { |
| | - addTabPane( |
| | - node instanceof TextDefinition ? 0 : getItems().size(), tabPane |
| | - ); |
| | - } |
| | - |
| | - getRecentFiles().add( file.getAbsolutePath() ); |
| | - } |
| | - |
| | - /** |
| | - * Opens a new text editor document using the default document file name. |
| | - */ |
| | - public void newTextEditor() { |
| | - open( DOCUMENT_DEFAULT ); |
| | - } |
| | - |
| | - /** |
| | - * Opens a new definition editor document using the default definition |
| | - * file name. |
| | - */ |
| | - public void newDefinitionEditor() { |
| | - open( DEFINITION_DEFAULT ); |
| | - } |
| | - |
| | - /** |
| | - * Iterates over all tab panes to find all {@link TextEditor}s and request |
| | - * that they save themselves. |
| | - */ |
| | - public void saveAll() { |
| | - mTabPanes.forEach( |
| | - ( tp ) -> tp.getTabs().forEach( ( tab ) -> { |
| | - final var node = tab.getContent(); |
| | - if( node instanceof final TextEditor editor ) { |
| | - save( editor ); |
| | - } |
| | - } ) |
| | - ); |
| | - } |
| | - |
| | - /** |
| | - * Requests that the active {@link TextEditor} saves itself. Don't bother |
| | - * checking if modified first because if the user swaps external media from |
| | - * an external source (e.g., USB thumb drive), save should not second-guess |
| | - * the user: save always re-saves. Also, it's less code. |
| | - */ |
| | - public void save() { |
| | - save( getActiveTextEditor() ); |
| | - } |
| | - |
| | - /** |
| | - * Saves the active {@link TextEditor} under a new name. |
| | - * |
| | - * @param files The new active editor {@link File} reference, must contain |
| | - * at least one element. |
| | - */ |
| | - public void saveAs( final List<File> files ) { |
| | - assert files != null; |
| | - assert !files.isEmpty(); |
| | - final var editor = getActiveTextEditor(); |
| | - final var tab = getTab( editor ); |
| | - final var file = files.get( 0 ); |
| | - |
| | - editor.rename( file ); |
| | - tab.ifPresent( t -> { |
| | - t.setText( editor.getFilename() ); |
| | - t.setTooltip( createTooltip( file ) ); |
| | - } ); |
| | - |
| | - save(); |
| | - } |
| | - |
| | - /** |
| | - * Saves the given {@link TextResource} to a file. This is typically used |
| | - * to save either an instance of {@link TextEditor} or {@link TextDefinition}. |
| | - * |
| | - * @param resource The resource to export. |
| | - */ |
| | - private void save( final TextResource resource ) { |
| | - try { |
| | - resource.save(); |
| | - } catch( final Exception ex ) { |
| | - clue( ex ); |
| | - sNotifier.alert( |
| | - getWindow(), resource.getPath(), "TextResource.saveFailed", ex |
| | - ); |
| | - } |
| | - } |
| | - |
| | - /** |
| | - * Closes all open {@link TextEditor}s; all {@link TextDefinition}s stay open. |
| | - * |
| | - * @return {@code true} when all editors, modified or otherwise, were |
| | - * permitted to close; {@code false} when one or more editors were modified |
| | - * and the user requested no closing. |
| | - */ |
| | - public boolean closeAll() { |
| | - var closable = true; |
| | - |
| | - for( final var tabPane : mTabPanes ) { |
| | - final var tabIterator = tabPane.getTabs().iterator(); |
| | - |
| | - while( tabIterator.hasNext() ) { |
| | - final var tab = tabIterator.next(); |
| | - final var resource = tab.getContent(); |
| | - |
| | - // The definition panes auto-save, so being specific here prevents |
| | - // closing the definitions in the situation where the user wants to |
| | - // continue editing (i.e., possibly save unsaved work). |
| | - if( !(resource instanceof TextEditor) ) { |
| | - continue; |
| | - } |
| | - |
| | - if( canClose( (TextEditor) resource ) ) { |
| | - tabIterator.remove(); |
| | - close( tab ); |
| | - } |
| | - else { |
| | - closable = false; |
| | - } |
| | - } |
| | - } |
| | - |
| | - return closable; |
| | - } |
| | - |
| | - /** |
| | - * Calls the tab's {@link Tab#getOnClosed()} handler to carry out a close |
| | - * event. |
| | - * |
| | - * @param tab The {@link Tab} that was closed. |
| | - */ |
| | - private void close( final Tab tab ) { |
| | - assert tab != null; |
| | - |
| | - final var handler = tab.getOnClosed(); |
| | - |
| | - if( handler != null ) { |
| | - handler.handle( new ActionEvent() ); |
| | - } |
| | - } |
| | - |
| | - /** |
| | - * Closes the active tab; delegates to {@link #canClose(TextResource)}. |
| | - */ |
| | - public void close() { |
| | - final var editor = getActiveTextEditor(); |
| | - |
| | - if( canClose( editor ) ) { |
| | - close( editor ); |
| | - } |
| | - } |
| | - |
| | - /** |
| | - * Closes the given {@link TextResource}. This must not be called from within |
| | - * a loop that iterates over the tab panes using {@code forEach}, lest a |
| | - * concurrent modification exception be thrown. |
| | - * |
| | - * @param resource The {@link TextResource} to close, without confirming with |
| | - * the user. |
| | - */ |
| | - private void close( final TextResource resource ) { |
| | - getTab( resource ).ifPresent( |
| | - ( tab ) -> { |
| | - close( tab ); |
| | - tab.getTabPane().getTabs().remove( tab ); |
| | - } |
| | - ); |
| | - } |
| | - |
| | - /** |
| | - * Answers whether the given {@link TextResource} may be closed. |
| | - * |
| | - * @param editor The {@link TextResource} to try closing. |
| | - * @return {@code true} when the editor may be closed; {@code false} when |
| | - * the user has requested to keep the editor open. |
| | - */ |
| | - private boolean canClose( final TextResource editor ) { |
| | - final var editorTab = getTab( editor ); |
| | - final var canClose = new AtomicBoolean( true ); |
| | - |
| | - if( editor.isModified() ) { |
| | - final var filename = new StringBuilder(); |
| | - editorTab.ifPresent( ( tab ) -> filename.append( tab.getText() ) ); |
| | - |
| | - final var message = sNotifier.createNotification( |
| | - Messages.get( "Alert.file.close.title" ), |
| | - Messages.get( "Alert.file.close.text" ), |
| | - filename.toString() |
| | - ); |
| | - |
| | - final var dialog = sNotifier.createConfirmation( getWindow(), message ); |
| | - |
| | - dialog.showAndWait().ifPresent( |
| | - save -> canClose.set( save == YES ? editor.save() : save == NO ) |
| | - ); |
| | - } |
| | - |
| | - return canClose.get(); |
| | - } |
| | - |
| | - private ObjectProperty<TextEditor> createActiveTextEditor() { |
| | - final var editor = new SimpleObjectProperty<TextEditor>(); |
| | - |
| | - editor.addListener( ( c, o, n ) -> { |
| | - if( n != null ) { |
| | - mPreview.setBaseUri( n.getPath() ); |
| | - process( n ); |
| | - } |
| | - } ); |
| | - |
| | - return editor; |
| | - } |
| | - |
| | - /** |
| | - * Adds the HTML preview tab to its own, singular tab pane. |
| | - */ |
| | - public void viewPreview() { |
| | - viewTab( mPreview, TEXT_HTML, "Pane.preview.title" ); |
| | - } |
| | - |
| | - /** |
| | - * Adds the document outline tab to its own, singular tab pane. |
| | - */ |
| | - public void viewOutline() { |
| | - viewTab( mOutline, APP_DOCUMENT_OUTLINE, "Pane.outline.title" ); |
| | - } |
| | - |
| | - public void viewStatistics() { |
| | - viewTab( mStatistics, APP_DOCUMENT_STATISTICS, "Pane.statistics.title" ); |
| | - } |
| | - |
| | - public void viewFiles() { |
| | - try { |
| | - final var factory = new FilePickerFactory( mWorkspace ); |
| | - final var fileManager = factory.createModeless(); |
| | - viewTab( fileManager, APP_FILE_MANAGER, "Pane.files.title" ); |
| | - } catch( final Exception ex ) { |
| | - clue( ex ); |
| | - } |
| | - } |
| | - |
| | - private void viewTab( |
| | - final Node node, final MediaType mediaType, final String key ) { |
| | - final var tabPane = obtainTabPane( mediaType ); |
| | - |
| | - for( final var tab : tabPane.getTabs() ) { |
| | - if( tab.getContent() == node ) { |
| | - return; |
| | - } |
| | - } |
| | - |
| | - tabPane.getTabs().add( createTab( get( key ), node ) ); |
| | - addTabPane( tabPane ); |
| | - } |
| | - |
| | - public void viewRefresh() { |
| | - mPreview.refresh(); |
| | - } |
| | - |
| | - /** |
| | - * Returns the tab that contains the given {@link TextEditor}. |
| | - * |
| | - * @param editor The {@link TextEditor} instance to find amongst the tabs. |
| | - * @return The first tab having content that matches the given tab. |
| | - */ |
| | - private Optional<Tab> getTab( final TextResource editor ) { |
| | - return mTabPanes.stream() |
| | - .flatMap( pane -> pane.getTabs().stream() ) |
| | - .filter( tab -> editor.equals( tab.getContent() ) ) |
| | - .findFirst(); |
| | - } |
| | - |
| | - /** |
| | - * Creates a new {@link DefinitionEditor} wrapped in a listener that |
| | - * is used to detect when the active {@link DefinitionEditor} has changed. |
| | - * Upon changing, the {@link #mResolvedMap} is updated and the active |
| | - * text editor is refreshed. |
| | - * |
| | - * @param editor Text editor to update with the revised resolved map. |
| | - * @return A newly configured property that represents the active |
| | - * {@link DefinitionEditor}, never null. |
| | - */ |
| | - private ObjectProperty<TextDefinition> createActiveDefinitionEditor( |
| | - final ObjectProperty<TextEditor> editor ) { |
| | - final var definitions = new SimpleObjectProperty<TextDefinition>(); |
| | - definitions.addListener( ( c, o, n ) -> { |
| | - resolve( n == null ? createDefinitionEditor() : n ); |
| | - process( editor.get() ); |
| | - } ); |
| | - |
| | - return definitions; |
| | - } |
| | - |
| | - private Tab createTab( final String filename, final Node node ) { |
| | - return new DetachableTab( filename, node ); |
| | - } |
| | - |
| | - private Tab createTab( final File file ) { |
| | - final var r = createTextResource( file ); |
| | - final var tab = createTab( r.getFilename(), r.getNode() ); |
| | - |
| | - r.modifiedProperty().addListener( |
| | - ( c, o, n ) -> tab.setText( r.getFilename() + (n ? "*" : "") ) |
| | - ); |
| | - |
| | - // This is called when either the tab is closed by the user clicking on |
| | - // the tab's close icon or when closing (all) from the file menu. |
| | - tab.setOnClosed( |
| | - ( __ ) -> getRecentFiles().remove( file.getAbsolutePath() ) |
| | - ); |
| | - |
| | - // When closing a tab, give focus to the newly selected tab. |
| | + * TODO: Load divider positions from exported settings, see |
| | + * {@link #bin(SetProperty)} comment. |
| | + */ |
| | + private double[] calculateDividerPositions() { |
| | + final var ratio = 100f / getItems().size() / 100; |
| | + final var positions = getDividerPositions(); |
| | + |
| | + for( int i = 0; i < positions.length; i++ ) { |
| | + positions[ i ] = ratio * i; |
| | + } |
| | + |
| | + return positions; |
| | + } |
| | + |
| | + /** |
| | + * Opens all the files into the application, provided the paths are unique. |
| | + * This may only be called for any type of files that a user can edit |
| | + * (i.e., update and persist), such as definitions and text files. |
| | + * |
| | + * @param files The list of files to open. |
| | + */ |
| | + public void open( final List<File> files ) { |
| | + files.forEach( this::open ); |
| | + } |
| | + |
| | + /** |
| | + * This opens the given file. Since the preview pane is not a file that |
| | + * can be opened, it is safe to add a listener to the detachable pane. |
| | + * |
| | + * @param file The file to open. |
| | + */ |
| | + private void open( final File file ) { |
| | + final var tab = createTab( file ); |
| | + final var node = tab.getContent(); |
| | + final var mediaType = MediaType.valueFrom( file ); |
| | + final var tabPane = obtainTabPane( mediaType ); |
| | + |
| | + tab.setTooltip( createTooltip( file ) ); |
| | + tabPane.setFocusTraversable( false ); |
| | + tabPane.setTabClosingPolicy( ALL_TABS ); |
| | + tabPane.getTabs().add( tab ); |
| | + |
| | + // Attach the tab scene factory for new tab panes. |
| | + if( !getItems().contains( tabPane ) ) { |
| | + addTabPane( |
| | + node instanceof TextDefinition ? 0 : getItems().size(), tabPane |
| | + ); |
| | + } |
| | + |
| | + getRecentFiles().add( file.getAbsolutePath() ); |
| | + } |
| | + |
| | + /** |
| | + * Opens a new text editor document using the default document file name. |
| | + */ |
| | + public void newTextEditor() { |
| | + open( DOCUMENT_DEFAULT ); |
| | + } |
| | + |
| | + /** |
| | + * Opens a new definition editor document using the default definition |
| | + * file name. |
| | + */ |
| | + public void newDefinitionEditor() { |
| | + open( DEFINITION_DEFAULT ); |
| | + } |
| | + |
| | + /** |
| | + * Iterates over all tab panes to find all {@link TextEditor}s and request |
| | + * that they save themselves. |
| | + */ |
| | + public void saveAll() { |
| | + mTabPanes.forEach( |
| | + ( tp ) -> tp.getTabs().forEach( ( tab ) -> { |
| | + final var node = tab.getContent(); |
| | + if( node instanceof final TextEditor editor ) { |
| | + save( editor ); |
| | + } |
| | + } ) |
| | + ); |
| | + } |
| | + |
| | + /** |
| | + * Requests that the active {@link TextEditor} saves itself. Don't bother |
| | + * checking if modified first because if the user swaps external media from |
| | + * an external source (e.g., USB thumb drive), save should not second-guess |
| | + * the user: save always re-saves. Also, it's less code. |
| | + */ |
| | + public void save() { |
| | + save( getActiveTextEditor() ); |
| | + } |
| | + |
| | + /** |
| | + * Saves the active {@link TextEditor} under a new name. |
| | + * |
| | + * @param files The new active editor {@link File} reference, must contain |
| | + * at least one element. |
| | + */ |
| | + public void saveAs( final List<File> files ) { |
| | + assert files != null; |
| | + assert !files.isEmpty(); |
| | + final var editor = getActiveTextEditor(); |
| | + final var tab = getTab( editor ); |
| | + final var file = files.get( 0 ); |
| | + |
| | + editor.rename( file ); |
| | + tab.ifPresent( t -> { |
| | + t.setText( editor.getFilename() ); |
| | + t.setTooltip( createTooltip( file ) ); |
| | + } ); |
| | + |
| | + save(); |
| | + } |
| | + |
| | + /** |
| | + * Saves the given {@link TextResource} to a file. This is typically used |
| | + * to save either an instance of {@link TextEditor} or {@link TextDefinition}. |
| | + * |
| | + * @param resource The resource to export. |
| | + */ |
| | + private void save( final TextResource resource ) { |
| | + try { |
| | + resource.save(); |
| | + } catch( final Exception ex ) { |
| | + clue( ex ); |
| | + sNotifier.alert( |
| | + getWindow(), resource.getPath(), "TextResource.saveFailed", ex |
| | + ); |
| | + } |
| | + } |
| | + |
| | + /** |
| | + * Closes all open {@link TextEditor}s; all {@link TextDefinition}s stay open. |
| | + * |
| | + * @return {@code true} when all editors, modified or otherwise, were |
| | + * permitted to close; {@code false} when one or more editors were modified |
| | + * and the user requested no closing. |
| | + */ |
| | + public boolean closeAll() { |
| | + var closable = true; |
| | + |
| | + for( final var tabPane : mTabPanes ) { |
| | + final var tabIterator = tabPane.getTabs().iterator(); |
| | + |
| | + while( tabIterator.hasNext() ) { |
| | + final var tab = tabIterator.next(); |
| | + final var resource = tab.getContent(); |
| | + |
| | + // The definition panes auto-save, so being specific here prevents |
| | + // closing the definitions in the situation where the user wants to |
| | + // continue editing (i.e., possibly save unsaved work). |
| | + if( !(resource instanceof TextEditor) ) { |
| | + continue; |
| | + } |
| | + |
| | + if( canClose( (TextEditor) resource ) ) { |
| | + tabIterator.remove(); |
| | + close( tab ); |
| | + } |
| | + else { |
| | + closable = false; |
| | + } |
| | + } |
| | + } |
| | + |
| | + return closable; |
| | + } |
| | + |
| | + /** |
| | + * Calls the tab's {@link Tab#getOnClosed()} handler to carry out a close |
| | + * event. |
| | + * |
| | + * @param tab The {@link Tab} that was closed. |
| | + */ |
| | + private void close( final Tab tab ) { |
| | + assert tab != null; |
| | + |
| | + final var handler = tab.getOnClosed(); |
| | + |
| | + if( handler != null ) { |
| | + handler.handle( new ActionEvent() ); |
| | + } |
| | + } |
| | + |
| | + /** |
| | + * Closes the active tab; delegates to {@link #canClose(TextResource)}. |
| | + */ |
| | + public void close() { |
| | + final var editor = getActiveTextEditor(); |
| | + |
| | + if( canClose( editor ) ) { |
| | + close( editor ); |
| | + } |
| | + } |
| | + |
| | + /** |
| | + * Closes the given {@link TextResource}. This must not be called from within |
| | + * a loop that iterates over the tab panes using {@code forEach}, lest a |
| | + * concurrent modification exception be thrown. |
| | + * |
| | + * @param resource The {@link TextResource} to close, without confirming with |
| | + * the user. |
| | + */ |
| | + private void close( final TextResource resource ) { |
| | + getTab( resource ).ifPresent( |
| | + ( tab ) -> { |
| | + close( tab ); |
| | + tab.getTabPane().getTabs().remove( tab ); |
| | + } |
| | + ); |
| | + } |
| | + |
| | + /** |
| | + * Answers whether the given {@link TextResource} may be closed. |
| | + * |
| | + * @param editor The {@link TextResource} to try closing. |
| | + * @return {@code true} when the editor may be closed; {@code false} when |
| | + * the user has requested to keep the editor open. |
| | + */ |
| | + private boolean canClose( final TextResource editor ) { |
| | + final var editorTab = getTab( editor ); |
| | + final var canClose = new AtomicBoolean( true ); |
| | + |
| | + if( editor.isModified() ) { |
| | + final var filename = new StringBuilder(); |
| | + editorTab.ifPresent( ( tab ) -> filename.append( tab.getText() ) ); |
| | + |
| | + final var message = sNotifier.createNotification( |
| | + Messages.get( "Alert.file.close.title" ), |
| | + Messages.get( "Alert.file.close.text" ), |
| | + filename.toString() |
| | + ); |
| | + |
| | + final var dialog = sNotifier.createConfirmation( getWindow(), message ); |
| | + |
| | + dialog.showAndWait().ifPresent( |
| | + save -> canClose.set( save == YES ? editor.save() : save == NO ) |
| | + ); |
| | + } |
| | + |
| | + return canClose.get(); |
| | + } |
| | + |
| | + private ObjectProperty<TextEditor> createActiveTextEditor() { |
| | + final var editor = new SimpleObjectProperty<TextEditor>(); |
| | + |
| | + editor.addListener( ( c, o, n ) -> { |
| | + if( n != null ) { |
| | + mPreview.setBaseUri( n.getPath() ); |
| | + process( n ); |
| | + } |
| | + } ); |
| | + |
| | + return editor; |
| | + } |
| | + |
| | + /** |
| | + * Adds the HTML preview tab to its own, singular tab pane. |
| | + */ |
| | + public void viewPreview() { |
| | + viewTab( mPreview, TEXT_HTML, "Pane.preview.title" ); |
| | + } |
| | + |
| | + /** |
| | + * Adds the document outline tab to its own, singular tab pane. |
| | + */ |
| | + public void viewOutline() { |
| | + viewTab( mOutline, APP_DOCUMENT_OUTLINE, "Pane.outline.title" ); |
| | + } |
| | + |
| | + public void viewStatistics() { |
| | + viewTab( mStatistics, APP_DOCUMENT_STATISTICS, "Pane.statistics.title" ); |
| | + } |
| | + |
| | + public void viewFiles() { |
| | + try { |
| | + final var factory = new FilePickerFactory( mWorkspace ); |
| | + final var fileManager = factory.createModeless(); |
| | + viewTab( fileManager, APP_FILE_MANAGER, "Pane.files.title" ); |
| | + } catch( final Exception ex ) { |
| | + clue( ex ); |
| | + } |
| | + } |
| | + |
| | + private void viewTab( |
| | + final Node node, final MediaType mediaType, final String key ) { |
| | + final var tabPane = obtainTabPane( mediaType ); |
| | + |
| | + for( final var tab : tabPane.getTabs() ) { |
| | + if( tab.getContent() == node ) { |
| | + return; |
| | + } |
| | + } |
| | + |
| | + tabPane.getTabs().add( createTab( get( key ), node ) ); |
| | + addTabPane( tabPane ); |
| | + } |
| | + |
| | + public void viewRefresh() { |
| | + mPreview.refresh(); |
| | + } |
| | + |
| | + /** |
| | + * Returns the tab that contains the given {@link TextEditor}. |
| | + * |
| | + * @param editor The {@link TextEditor} instance to find amongst the tabs. |
| | + * @return The first tab having content that matches the given tab. |
| | + */ |
| | + private Optional<Tab> getTab( final TextResource editor ) { |
| | + return mTabPanes.stream() |
| | + .flatMap( pane -> pane.getTabs().stream() ) |
| | + .filter( tab -> editor.equals( tab.getContent() ) ) |
| | + .findFirst(); |
| | + } |
| | + |
| | + /** |
| | + * Creates a new {@link DefinitionEditor} wrapped in a listener that |
| | + * is used to detect when the active {@link DefinitionEditor} has changed. |
| | + * Upon changing, the {@link #mResolvedMap} is updated and the active |
| | + * text editor is refreshed. |
| | + * |
| | + * @param editor Text editor to update with the revised resolved map. |
| | + * @return A newly configured property that represents the active |
| | + * {@link DefinitionEditor}, never null. |
| | + */ |
| | + private ObjectProperty<TextDefinition> createActiveDefinitionEditor( |
| | + final ObjectProperty<TextEditor> editor ) { |
| | + final var definitions = new SimpleObjectProperty<TextDefinition>(); |
| | + definitions.addListener( ( c, o, n ) -> { |
| | + resolve( n == null ? createDefinitionEditor() : n ); |
| | + process( editor.get() ); |
| | + } ); |
| | + |
| | + return definitions; |
| | + } |
| | + |
| | + private Tab createTab( final String filename, final Node node ) { |
| | + return new DetachableTab( filename, node ); |
| | + } |
| | + |
| | + private Tab createTab( final File file ) { |
| | + final var r = createTextResource( file ); |
| | + final var tab = createTab( r.getFilename(), r.getNode() ); |
| | + |
| | + r.modifiedProperty().addListener( |
| | + ( c, o, n ) -> tab.setText( r.getFilename() + (n ? "*" : "") ) |
| | + ); |
| | + |
| | + // This is called when either the tab is closed by the user clicking on |
| | + // the tab's close icon or when closing (all) from the file menu. |
| | + tab.setOnClosed( |
| | + ( __ ) -> getRecentFiles().remove( file.getAbsolutePath() ) |
| | + ); |
| | + |
| | + // When closing a tab, give focus to the newly revealed tab. |
| | tab.selectedProperty().addListener( ( c, o, n ) -> { |
| | if( n != null && n ) { |