Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
build.gradle
annotationProcessor "info.picocli:picocli-codegen:${v_picocli}"
- // KeenQuotes, KeenType, KeenSpell, word split.
+ // KeenQuotes, KeenType, KeenSpell, KeenCount.
implementation fileTree( include: ['**/*.jar'], dir: 'libs' )
src/main/java/com/keenwrite/editors/markdown/HyperlinkModel.java
*/
public final class HyperlinkModel {
-
- private String text;
- private String url;
- private String title;
+ private String mText;
+ private String mUrl;
+ private String mTitle;
/**
* Constructs a new hyperlink model in Markdown format by default with no
* title (i.e., tooltip).
*
* @param text The hyperlink text displayed (e.g., displayed to the user).
- * @param url The destination URL (e.g., when clicked).
*/
- public HyperlinkModel( final String text, final String url ) {
- this( text, url, null );
+ public HyperlinkModel( final String text ) {
+ this( text, null, null );
}
setUrl( url );
setTitle( title );
- }
-
- /**
- * Returns the string in Markdown format by default.
- *
- * @return A Markdown version of the hyperlink.
- */
- @Override
- public String toString() {
- String format = "%s%s%s";
-
- if( hasText() ) {
- format = "[%s]" + (hasTitle() ? "(%s \"%s\")" : "(%s%s)");
- }
-
- // Becomes ""+URL+"" if no text is set.
- // Becomes [TITLE]+(URL)+"" if no title is set.
- // Becomes [TITLE]+(URL+ \"TITLE\") if title is set.
- return String.format( format, getText(), getUrl(), getTitle() );
}
public void setText( final String text ) {
- this.text = sanitize( text );
+ mText = sanitize( text );
}
public void setUrl( final String url ) {
- this.url = sanitize( url );
+ mUrl = sanitize( url );
}
public void setTitle( final String title ) {
- this.title = sanitize( title );
+ mTitle = sanitize( title );
}
public String getText() {
- return this.text;
+ return mText;
}
public String getUrl() {
- return this.url;
+ return mUrl;
}
public String getTitle() {
- return this.title;
+ return mTitle;
}
private String sanitize( final String s ) {
return s == null ? "" : s;
+ }
+
+ /**
+ * Returns the string in Markdown format by default.
+ *
+ * @return A Markdown version of the hyperlink.
+ */
+ @Override
+ public String toString() {
+ final String format = hasText()
+ ? STR."[%s]\{hasTitle() ? "(%s \"%s\")" : "(%s%s)"}"
+ : "%s%s%s";
+
+ // Becomes ""+URL+"" if no text is set.
+ // Becomes [TITLE]+(URL)+"" if no title is set.
+ // Becomes [TITLE]+(URL+ \"TITLE\") if title is set.
+ return String.format( format, getText(), getUrl(), getTitle() );
}
}
src/main/java/com/keenwrite/ui/actions/ApplicationBars.java
-/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
+/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
+ *
+ * SPDX-License-Identifier: MIT
+ */
package com.keenwrite.ui.actions;
addAction( "file.new", _ -> actions.file_new() ),
addAction( "file.open", _ -> actions.file_open() ),
+ addAction( "file.open_url", _ -> actions.file_open_url() ),
SEPARATOR,
addAction( "file.close", _ -> actions.file_close() ),
src/main/java/com/keenwrite/ui/actions/GuiCommands.java
-/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
-package com.keenwrite.ui.actions;
-
-import com.keenwrite.ExportFormat;
-import com.keenwrite.MainPane;
-import com.keenwrite.MainScene;
-import com.keenwrite.commands.ConcatenateCommand;
-import com.keenwrite.editors.TextDefinition;
-import com.keenwrite.editors.TextEditor;
-import com.keenwrite.editors.markdown.HyperlinkModel;
-import com.keenwrite.editors.markdown.LinkVisitor;
-import com.keenwrite.events.CaretMovedEvent;
-import com.keenwrite.events.ExportFailedEvent;
-import com.keenwrite.io.SysFile;
-import com.keenwrite.preferences.Key;
-import com.keenwrite.preferences.PreferencesController;
-import com.keenwrite.preferences.Workspace;
-import com.keenwrite.processors.markdown.MarkdownProcessor;
-import com.keenwrite.search.SearchModel;
-import com.keenwrite.typesetting.Typesetter;
-import com.keenwrite.ui.controls.SearchBar;
-import com.keenwrite.ui.dialogs.ExportDialog;
-import com.keenwrite.ui.dialogs.ExportSettings;
-import com.keenwrite.ui.dialogs.ImageDialog;
-import com.keenwrite.ui.dialogs.LinkDialog;
-import com.keenwrite.ui.explorer.FilePicker;
-import com.keenwrite.ui.explorer.FilePickerFactory;
-import com.keenwrite.ui.logging.LogView;
-import com.vladsch.flexmark.ast.Link;
-import javafx.concurrent.Service;
-import javafx.concurrent.Task;
-import javafx.scene.control.Alert;
-import javafx.scene.control.Dialog;
-import javafx.stage.Window;
-import javafx.stage.WindowEvent;
-
-import java.io.File;
-import java.nio.file.Path;
-import java.util.List;
-import java.util.Optional;
-
-import static com.keenwrite.Bootstrap.*;
-import static com.keenwrite.ExportFormat.*;
-import static com.keenwrite.Messages.get;
-import static com.keenwrite.constants.Constants.PDF_DEFAULT;
-import static com.keenwrite.constants.Constants.USER_DIRECTORY;
-import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG_NODE;
-import static com.keenwrite.events.StatusEvent.clue;
-import static com.keenwrite.preferences.AppKeys.*;
-import static com.keenwrite.processors.ProcessorFactory.createProcessors;
-import static com.keenwrite.ui.explorer.FilePickerFactory.SelectionType;
-import static com.keenwrite.ui.explorer.FilePickerFactory.SelectionType.*;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.nio.file.Files.writeString;
-import static javafx.application.Platform.runLater;
-import static javafx.event.Event.fireEvent;
-import static javafx.scene.control.Alert.AlertType.INFORMATION;
-import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
-import static org.apache.commons.io.FilenameUtils.getExtension;
-
-/**
- * Responsible for abstracting how functionality is mapped to the application.
- * This allows users to customize accelerator keys and will provide pluggable
- * functionality so that different text markup languages can change documents
- * using their respective syntax.
- */
-public final class GuiCommands {
- private static final String STYLE_SEARCH = "search";
-
- /**
- * When an action is executed, this is one of the recipients.
- */
- private final MainPane mMainPane;
-
- private final MainScene mMainScene;
-
- private final LogView mLogView;
-
- /**
- * Tracks finding text in the active document.
- */
- private final SearchModel mSearchModel;
-
- private boolean mCanTypeset;
-
- /**
- * A {@link Task} can only be run once, so wrap it in a {@link Service} to
- * allow re-launching the typesetting task repeatedly.
- */
- private Service<Path> mTypesetService;
-
- /**
- * Prevent a race-condition between checking to see if the typesetting task
- * is running and restarting the task itself.
- */
- private final Object mMutex = new Object();
-
- public GuiCommands( final MainScene scene, final MainPane pane ) {
- mMainScene = scene;
- mMainPane = pane;
- mLogView = new LogView();
- mSearchModel = new SearchModel();
- mSearchModel.matchOffsetProperty().addListener( ( c, o, n ) -> {
- final var editor = getActiveTextEditor();
-
- // Clear highlighted areas before highlighting a new region.
- if( o != null ) {
- editor.unstylize( STYLE_SEARCH );
- }
-
- if( n != null ) {
- editor.moveTo( n.getStart() );
- editor.stylize( n, STYLE_SEARCH );
- }
- } );
-
- // When the active text editor changes ...
- mMainPane.textEditorProperty().addListener(
- ( c, o, n ) -> {
- // ... update the haystack.
- mSearchModel.search( getActiveTextEditor().getText() );
-
- // ... update the status bar with the current caret position.
- if( n != null ) {
- final var w = getWorkspace();
- final var recentDoc = w.fileProperty( KEY_UI_RECENT_DOCUMENT );
-
- // ... preserve the most recent document.
- recentDoc.setValue( n.getFile() );
- CaretMovedEvent.fire( n.getCaret() );
- }
- }
- );
- }
-
- public void file_new() {
- getMainPane().newTextEditor();
- }
-
- public void file_open() {
- pickFiles( FILE_OPEN_MULTIPLE ).ifPresent( l -> getMainPane().open( l ) );
- }
-
- public void file_close() {
- getMainPane().close();
- }
-
- public void file_close_all() {
- getMainPane().closeAll();
- }
-
- public void file_save() {
- getMainPane().save();
- }
-
- public void file_save_as() {
- pickFiles( FILE_SAVE_AS ).ifPresent( l -> getMainPane().saveAs( l ) );
- }
-
- public void file_save_all() {
- getMainPane().saveAll();
- }
-
- /**
- * Converts the actively edited file in the given file format.
- *
- * @param format The destination file format.
- */
- private void file_export( final ExportFormat format ) {
- file_export( format, false );
- }
-
- /**
- * Converts one or more files into the given file format. If {@code dir}
- * is set to true, this will first append all files in the same directory
- * as the actively edited file.
- *
- * @param format The destination file format.
- * @param dir Export all files in the actively edited file's directory.
- */
- private void file_export( final ExportFormat format, final boolean dir ) {
- final var editor = getMainPane().getTextEditor();
- final var exported = getWorkspace().fileProperty( KEY_UI_RECENT_EXPORT );
- final var exportParent = exported.get().toPath().getParent();
- final var editorParent = editor.getPath().getParent();
- final var userHomeParent = USER_DIRECTORY.toPath();
- final var exportPath = exportParent != null
- ? exportParent
- : editorParent != null
- ? editorParent
- : userHomeParent;
-
- final var filename = format.toExportFilename( editor.getPath() );
- final var selected = PDF_DEFAULT
- .getName()
- .equals( exported.get().getName() );
- final var selection = pickFile(
- selected
- ? filename
- : exported.get(),
- exportPath,
- FILE_EXPORT
- );
-
- selection.ifPresent( files -> file_export( editor, format, files, dir ) );
- }
-
- private void file_export(
- final TextEditor editor,
- final ExportFormat format,
- final List<File> files,
- final boolean dir ) {
- editor.save();
- final var main = getMainPane();
- final var exported = getWorkspace().fileProperty( KEY_UI_RECENT_EXPORT );
-
- final var sourceFile = files.get( 0 );
- final var sourcePath = sourceFile.toPath();
- final var document = dir ? append( editor ) : editor.getText();
- final var context = main.createProcessorContext( sourcePath, format );
-
- final var service = new Service<Path>() {
- @Override
- protected Task<Path> createTask() {
- final var task = new Task<Path>() {
- @Override
- protected Path call() throws Exception {
- final var chain = createProcessors( context );
- final var export = chain.apply( document );
-
- // Processors can export binary files. In such cases, processors
- // return null to prevent further processing.
- return export == null
- ? null
- : writeString( sourcePath, export, UTF_8 );
- }
- };
-
- task.setOnSucceeded(
- e -> {
- // Remember the exported file name for next time.
- exported.setValue( sourceFile );
-
- final var result = task.getValue();
-
- // Binary formats must notify users of success independently.
- if( result != null ) {
- clue( "Main.status.export.success", result );
- }
- }
- );
-
- task.setOnFailed( e -> {
- final var ex = task.getException();
- clue( ex );
-
- if( ex instanceof TypeNotPresentException ) {
- fireExportFailedEvent();
- }
- } );
-
- return task;
- }
- };
-
- mTypesetService = service;
- typeset( service );
- }
-
- /**
- * @param dir {@code true} means to export all files in the active file
- * editor's directory; {@code false} means to export only the
- * actively edited file.
- */
- private void file_export_pdf( final boolean dir ) {
- // Don't re-validate the typesetter installation each time. If the
- // user mucks up the typesetter installation, it'll get caught the
- // next time the application is started. Don't use |= because it
- // won't short-circuit.
- mCanTypeset = mCanTypeset || Typesetter.canRun();
-
- if( mCanTypeset ) {
- final var workspace = getWorkspace();
- final var theme = workspace.stringProperty(
- KEY_TYPESET_CONTEXT_THEME_SELECTION
- );
- final var chapters = workspace.stringProperty(
- KEY_TYPESET_CONTEXT_CHAPTERS
- );
-
- final var settings = ExportSettings
- .builder()
- .with( ExportSettings.Mutator::setTheme, theme )
- .with( ExportSettings.Mutator::setChapters, chapters )
- .build();
-
- final var themes = workspace.getFile(
- KEY_TYPESET_CONTEXT_THEMES_PATH
- );
-
- // If the typesetter is installed, allow the user to select a theme. If
- // the themes aren't installed, a status message will appear.
- if( ExportDialog.choose( getWindow(), themes, settings, dir ) ) {
- file_export( APPLICATION_PDF, dir );
- }
- }
- else {
- fireExportFailedEvent();
- }
- }
-
- public void file_export_pdf() {
- file_export_pdf( false );
- }
-
- public void file_export_pdf_dir() {
- file_export_pdf( true );
- }
-
- public void file_export_html_dir() {
- file_export( XHTML_TEX, true );
- }
-
- public void file_export_repeat() {
- typeset( mTypesetService );
- }
-
- public void file_export_html_svg() {
- file_export( HTML_TEX_SVG );
- }
-
- public void file_export_html_tex() {
- file_export( HTML_TEX_DELIMITED );
- }
-
- public void file_export_xhtml_tex() {
- file_export( XHTML_TEX );
- }
-
- private void fireExportFailedEvent() {
- runLater( ExportFailedEvent::fire );
- }
-
- public void file_exit() {
- final var window = getWindow();
- fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
- }
-
- public void edit_undo() {
- getActiveTextEditor().undo();
- }
-
- public void edit_redo() {
- getActiveTextEditor().redo();
- }
-
- public void edit_cut() {
- getActiveTextEditor().cut();
- }
-
- public void edit_copy() {
- getActiveTextEditor().copy();
- }
-
- public void edit_paste() {
- getActiveTextEditor().paste();
- }
-
- public void edit_select_all() {
- getActiveTextEditor().selectAll();
- }
-
- public void edit_find() {
- final var nodes = getMainScene().getStatusBar().getLeftItems();
-
- if( nodes.isEmpty() ) {
- final var searchBar = new SearchBar();
-
- searchBar.matchIndexProperty().bind( mSearchModel.matchIndexProperty() );
- searchBar.matchCountProperty().bind( mSearchModel.matchCountProperty() );
-
- searchBar.setOnCancelAction( event -> {
- final var editor = getActiveTextEditor();
- nodes.remove( searchBar );
- editor.unstylize( STYLE_SEARCH );
- editor.getNode().requestFocus();
- } );
-
- searchBar.addInputListener( ( c, o, n ) -> {
- if( n != null && !n.isEmpty() ) {
- mSearchModel.search( n, getActiveTextEditor().getText() );
- }
- } );
-
- searchBar.setOnNextAction( event -> edit_find_next() );
- searchBar.setOnPrevAction( event -> edit_find_prev() );
-
- nodes.add( searchBar );
- searchBar.requestFocus();
- }
- }
-
- public void edit_find_next() {
- mSearchModel.advance();
- }
-
- public void edit_find_prev() {
- mSearchModel.retreat();
- }
-
- public void edit_preferences() {
- try {
- new PreferencesController( getWorkspace() ).show();
- } catch( final Exception ex ) {
- clue( ex );
- }
- }
-
- public void format_bold() {
- getActiveTextEditor().bold();
- }
-
- public void format_italic() {
- getActiveTextEditor().italic();
- }
-
- public void format_monospace() {
- getActiveTextEditor().monospace();
- }
-
- public void format_superscript() {
- getActiveTextEditor().superscript();
- }
-
- public void format_subscript() {
- getActiveTextEditor().subscript();
- }
-
- public void format_strikethrough() {
- getActiveTextEditor().strikethrough();
- }
-
- public void insert_blockquote() {
- getActiveTextEditor().blockquote();
- }
-
- public void insert_code() {
- getActiveTextEditor().code();
- }
-
- public void insert_fenced_code_block() {
- getActiveTextEditor().fencedCodeBlock();
- }
-
- public void insert_link() {
- insertObject( createLinkDialog() );
- }
-
- public void insert_image() {
- insertObject( createImageDialog() );
- }
-
- private void insertObject( final Dialog<String> dialog ) {
- final var textArea = getActiveTextEditor().getTextArea();
- dialog.showAndWait().ifPresent( textArea::replaceSelection );
- }
-
- private Dialog<String> createLinkDialog() {
- return new LinkDialog( getWindow(), createHyperlinkModel() );
- }
-
- private Dialog<String> createImageDialog() {
- final var path = getActiveTextEditor().getPath();
- final var parentDir = path.getParent();
- return new ImageDialog( getWindow(), parentDir );
- }
-
- /**
- * Returns one of: selected text, word under cursor, or parsed hyperlink from
- * the Markdown AST.
- *
- * @return An instance containing the link URL and display text.
- */
- private HyperlinkModel createHyperlinkModel() {
- final var context = getMainPane().createProcessorContext();
- final var editor = getActiveTextEditor();
- final var textArea = editor.getTextArea();
- final var selectedText = textArea.getSelectedText();
-
- // Convert current paragraph to Markdown nodes.
- final var mp = MarkdownProcessor.create( context );
- final var p = textArea.getCurrentParagraph();
- final var paragraph = textArea.getText( p );
- final var node = mp.toNode( paragraph );
- final var visitor = new LinkVisitor( textArea.getCaretColumn() );
- final var link = visitor.process( node );
-
- if( link != null ) {
- textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() );
- }
-
- return createHyperlinkModel( link, selectedText );
- }
-
- private HyperlinkModel createHyperlinkModel(
- final Link link, final String selection ) {
-
- return link == null
- ? new HyperlinkModel( selection, "https://localhost" )
- : new HyperlinkModel( link );
- }
-
- public void insert_heading_1() {
- insert_heading( 1 );
- }
-
- public void insert_heading_2() {
- insert_heading( 2 );
- }
-
- public void insert_heading_3() {
- insert_heading( 3 );
- }
-
- private void insert_heading( final int level ) {
- getActiveTextEditor().heading( level );
- }
-
- public void insert_unordered_list() {
- getActiveTextEditor().unorderedList();
- }
-
- public void insert_ordered_list() {
- getActiveTextEditor().orderedList();
- }
-
- public void insert_horizontal_rule() {
- getActiveTextEditor().horizontalRule();
- }
-
- public void definition_create() {
- getActiveTextDefinition().createDefinition();
- }
-
- public void definition_rename() {
- getActiveTextDefinition().renameDefinition();
- }
-
- public void definition_delete() {
- getActiveTextDefinition().deleteDefinitions();
- }
-
- public void definition_autoinsert() {
- getMainPane().autoinsert();
- }
-
- public void view_refresh() {
- getMainPane().viewRefresh();
- }
-
- public void view_preview() {
- getMainPane().viewPreview();
- }
-
- public void view_outline() {
- getMainPane().viewOutline();
- }
-
- public void view_files() { getMainPane().viewFiles(); }
-
- public void view_statistics() {
- getMainPane().viewStatistics();
- }
-
- public void view_menubar() {
- getMainScene().toggleMenuBar();
- }
-
- public void view_toolbar() {
- getMainScene().toggleToolBar();
- }
-
- public void view_statusbar() {
- getMainScene().toggleStatusBar();
- }
-
- public void view_log() {
- mLogView.view();
- }
-
- public void help_about() {
- final var alert = new Alert( INFORMATION );
- final var prefix = "Dialog.about.";
- alert.setTitle( get( prefix + "title", APP_TITLE ) );
- alert.setHeaderText( get( prefix + "header", APP_TITLE ) );
- alert.setContentText( get( prefix + "content", APP_YEAR, APP_VERSION ) );
- alert.setGraphic( ICON_DIALOG_NODE );
- alert.initOwner( getWindow() );
- alert.showAndWait();
- }
-
- private <T> void typeset( final Service<T> service ) {
- synchronized( mMutex ) {
- if( service != null && !service.isRunning() ) {
- service.reset();
- service.start();
- }
- }
- }
-
- /**
- * Concatenates all the files in the same directory as the given file into
- * a string. The extension is determined by the given file name pattern; the
- * order files are concatenated is based on their numeric sort order (this
- * avoids lexicographic sorting).
- * <p>
- * If the parent path to the file being edited in the text editor cannot
- * be found then this will return the editor's text, without iterating through
- * the parent directory. (Should never happen, but who knows?)
- * </p>
- * <p>
- * New lines are automatically appended to separate each file.
- * </p>
- *
- * @param editor The text editor containing
- * @return All files in the same directory as the file being edited
- * concatenated into a single string.
- */
- private String append( final TextEditor editor ) {
- final var pattern = editor.getPath();
- final var parent = pattern.getParent();
-
- // Short-circuit because nothing else can be done.
- if( parent == null ) {
- clue( "Main.status.export.concat.parent", pattern );
- return editor.getText();
- }
-
- final var filename = SysFile.getFileName( pattern );
- final var extension = getExtension( filename );
-
- if( extension.isBlank() ) {
- clue( "Main.status.export.concat.extension", filename );
- return editor.getText();
- }
-
- try {
- final var command = new ConcatenateCommand(
- parent, extension, getString( KEY_TYPESET_CONTEXT_CHAPTERS ) );
- return command.call();
- } catch( final Throwable t ) {
- clue( t );
- return editor.getText();
- }
+/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+package com.keenwrite.ui.actions;
+
+import com.keenwrite.ExportFormat;
+import com.keenwrite.MainPane;
+import com.keenwrite.MainScene;
+import com.keenwrite.commands.ConcatenateCommand;
+import com.keenwrite.editors.TextDefinition;
+import com.keenwrite.editors.TextEditor;
+import com.keenwrite.editors.markdown.HyperlinkModel;
+import com.keenwrite.editors.markdown.LinkVisitor;
+import com.keenwrite.events.CaretMovedEvent;
+import com.keenwrite.events.ExportFailedEvent;
+import com.keenwrite.io.SysFile;
+import com.keenwrite.preferences.Key;
+import com.keenwrite.preferences.PreferencesController;
+import com.keenwrite.preferences.Workspace;
+import com.keenwrite.processors.markdown.MarkdownProcessor;
+import com.keenwrite.search.SearchModel;
+import com.keenwrite.typesetting.Typesetter;
+import com.keenwrite.ui.controls.SearchBar;
+import com.keenwrite.ui.dialogs.*;
+import com.keenwrite.ui.explorer.FilePicker;
+import com.keenwrite.ui.explorer.FilePickerFactory;
+import com.keenwrite.ui.logging.LogView;
+import com.vladsch.flexmark.ast.Link;
+import javafx.concurrent.Service;
+import javafx.concurrent.Task;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Dialog;
+import javafx.stage.Window;
+import javafx.stage.WindowEvent;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Optional;
+
+import static com.keenwrite.Bootstrap.*;
+import static com.keenwrite.ExportFormat.*;
+import static com.keenwrite.Messages.get;
+import static com.keenwrite.constants.Constants.PDF_DEFAULT;
+import static com.keenwrite.constants.Constants.USER_DIRECTORY;
+import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG_NODE;
+import static com.keenwrite.events.StatusEvent.clue;
+import static com.keenwrite.preferences.AppKeys.*;
+import static com.keenwrite.processors.ProcessorFactory.createProcessors;
+import static com.keenwrite.ui.explorer.FilePickerFactory.SelectionType;
+import static com.keenwrite.ui.explorer.FilePickerFactory.SelectionType.*;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.nio.file.Files.writeString;
+import static javafx.application.Platform.runLater;
+import static javafx.event.Event.fireEvent;
+import static javafx.scene.control.Alert.AlertType.INFORMATION;
+import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
+import static org.apache.commons.io.FilenameUtils.getExtension;
+
+/**
+ * Responsible for abstracting how functionality is mapped to the application.
+ * This allows users to customize accelerator keys and will provide pluggable
+ * functionality so that different text markup languages can change documents
+ * using their respective syntax.
+ */
+public final class GuiCommands {
+ private static final String STYLE_SEARCH = "search";
+
+ /**
+ * When an action is executed, this is one of the recipients.
+ */
+ private final MainPane mMainPane;
+
+ private final MainScene mMainScene;
+
+ private final LogView mLogView;
+
+ /**
+ * Tracks finding text in the active document.
+ */
+ private final SearchModel mSearchModel;
+
+ private boolean mCanTypeset;
+
+ /**
+ * A {@link Task} can only be run once, so wrap it in a {@link Service} to
+ * allow re-launching the typesetting task repeatedly.
+ */
+ private Service<Path> mTypesetService;
+
+ /**
+ * Prevent a race-condition between checking to see if the typesetting task
+ * is running and restarting the task itself.
+ */
+ private final Object mMutex = new Object();
+
+ public GuiCommands( final MainScene scene, final MainPane pane ) {
+ mMainScene = scene;
+ mMainPane = pane;
+ mLogView = new LogView();
+ mSearchModel = new SearchModel();
+ mSearchModel.matchOffsetProperty().addListener( ( c, o, n ) -> {
+ final var editor = getActiveTextEditor();
+
+ // Clear highlighted areas before highlighting a new region.
+ if( o != null ) {
+ editor.unstylize( STYLE_SEARCH );
+ }
+
+ if( n != null ) {
+ editor.moveTo( n.getStart() );
+ editor.stylize( n, STYLE_SEARCH );
+ }
+ } );
+
+ // When the active text editor changes ...
+ mMainPane.textEditorProperty().addListener(
+ ( c, o, n ) -> {
+ // ... update the haystack.
+ mSearchModel.search( getActiveTextEditor().getText() );
+
+ // ... update the status bar with the current caret position.
+ if( n != null ) {
+ final var w = getWorkspace();
+ final var recentDoc = w.fileProperty( KEY_UI_RECENT_DOCUMENT );
+
+ // ... preserve the most recent document.
+ recentDoc.setValue( n.getFile() );
+ CaretMovedEvent.fire( n.getCaret() );
+ }
+ }
+ );
+ }
+
+ public void file_new() {
+ getMainPane().newTextEditor();
+ }
+
+ public void file_open() {
+ pickFiles( FILE_OPEN_MULTIPLE ).ifPresent( l -> getMainPane().open( l ) );
+ }
+
+ public void file_open_url() {
+ pickFile().ifPresent( l -> getMainPane().open( List.of( l ) ) );
+ }
+
+ public void file_close() {
+ getMainPane().close();
+ }
+
+ public void file_close_all() {
+ getMainPane().closeAll();
+ }
+
+ public void file_save() {
+ getMainPane().save();
+ }
+
+ public void file_save_as() {
+ pickFiles( FILE_SAVE_AS ).ifPresent( l -> getMainPane().saveAs( l ) );
+ }
+
+ public void file_save_all() {
+ getMainPane().saveAll();
+ }
+
+ /**
+ * Converts the actively edited file in the given file format.
+ *
+ * @param format The destination file format.
+ */
+ private void file_export( final ExportFormat format ) {
+ file_export( format, false );
+ }
+
+ /**
+ * Converts one or more files into the given file format. If {@code dir}
+ * is set to true, this will first append all files in the same directory
+ * as the actively edited file.
+ *
+ * @param format The destination file format.
+ * @param dir Export all files in the actively edited file's directory.
+ */
+ private void file_export( final ExportFormat format, final boolean dir ) {
+ final var editor = getMainPane().getTextEditor();
+ final var exported = getWorkspace().fileProperty( KEY_UI_RECENT_EXPORT );
+ final var exportParent = exported.get().toPath().getParent();
+ final var editorParent = editor.getPath().getParent();
+ final var userHomeParent = USER_DIRECTORY.toPath();
+ final var exportPath = exportParent != null
+ ? exportParent
+ : editorParent != null
+ ? editorParent
+ : userHomeParent;
+
+ final var filename = format.toExportFilename( editor.getPath() );
+ final var selected = PDF_DEFAULT
+ .getName()
+ .equals( exported.get().getName() );
+ final var selection = pickFile(
+ selected
+ ? filename
+ : exported.get(),
+ exportPath,
+ FILE_EXPORT
+ );
+
+ selection.ifPresent( files -> file_export( editor, format, files, dir ) );
+ }
+
+ private void file_export(
+ final TextEditor editor,
+ final ExportFormat format,
+ final List<File> files,
+ final boolean dir ) {
+ editor.save();
+ final var main = getMainPane();
+ final var exported = getWorkspace().fileProperty( KEY_UI_RECENT_EXPORT );
+
+ final var sourceFile = files.get( 0 );
+ final var sourcePath = sourceFile.toPath();
+ final var document = dir ? append( editor ) : editor.getText();
+ final var context = main.createProcessorContext( sourcePath, format );
+
+ final var service = new Service<Path>() {
+ @Override
+ protected Task<Path> createTask() {
+ final var task = new Task<Path>() {
+ @Override
+ protected Path call() throws Exception {
+ final var chain = createProcessors( context );
+ final var export = chain.apply( document );
+
+ // Processors can export binary files. In such cases, processors
+ // return null to prevent further processing.
+ return export == null
+ ? null
+ : writeString( sourcePath, export, UTF_8 );
+ }
+ };
+
+ task.setOnSucceeded(
+ e -> {
+ // Remember the exported file name for next time.
+ exported.setValue( sourceFile );
+
+ final var result = task.getValue();
+
+ // Binary formats must notify users of success independently.
+ if( result != null ) {
+ clue( "Main.status.export.success", result );
+ }
+ }
+ );
+
+ task.setOnFailed( e -> {
+ final var ex = task.getException();
+ clue( ex );
+
+ if( ex instanceof TypeNotPresentException ) {
+ fireExportFailedEvent();
+ }
+ } );
+
+ return task;
+ }
+ };
+
+ mTypesetService = service;
+ typeset( service );
+ }
+
+ /**
+ * @param dir {@code true} means to export all files in the active file
+ * editor's directory; {@code false} means to export only the
+ * actively edited file.
+ */
+ private void file_export_pdf( final boolean dir ) {
+ // Don't re-validate the typesetter installation each time. If the
+ // user mucks up the typesetter installation, it'll get caught the
+ // next time the application is started. Don't use |= because it
+ // won't short-circuit.
+ mCanTypeset = mCanTypeset || Typesetter.canRun();
+
+ if( mCanTypeset ) {
+ final var workspace = getWorkspace();
+ final var theme = workspace.stringProperty(
+ KEY_TYPESET_CONTEXT_THEME_SELECTION
+ );
+ final var chapters = workspace.stringProperty(
+ KEY_TYPESET_CONTEXT_CHAPTERS
+ );
+
+ final var settings = ExportSettings
+ .builder()
+ .with( ExportSettings.Mutator::setTheme, theme )
+ .with( ExportSettings.Mutator::setChapters, chapters )
+ .build();
+
+ final var themes = workspace.getFile(
+ KEY_TYPESET_CONTEXT_THEMES_PATH
+ );
+
+ // If the typesetter is installed, allow the user to select a theme. If
+ // the themes aren't installed, a status message will appear.
+ if( ExportDialog.choose( getWindow(), themes, settings, dir ) ) {
+ file_export( APPLICATION_PDF, dir );
+ }
+ }
+ else {
+ fireExportFailedEvent();
+ }
+ }
+
+ public void file_export_pdf() {
+ file_export_pdf( false );
+ }
+
+ public void file_export_pdf_dir() {
+ file_export_pdf( true );
+ }
+
+ public void file_export_html_dir() {
+ file_export( XHTML_TEX, true );
+ }
+
+ public void file_export_repeat() {
+ typeset( mTypesetService );
+ }
+
+ public void file_export_html_svg() {
+ file_export( HTML_TEX_SVG );
+ }
+
+ public void file_export_html_tex() {
+ file_export( HTML_TEX_DELIMITED );
+ }
+
+ public void file_export_xhtml_tex() {
+ file_export( XHTML_TEX );
+ }
+
+ private void fireExportFailedEvent() {
+ runLater( ExportFailedEvent::fire );
+ }
+
+ public void file_exit() {
+ final var window = getWindow();
+ fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
+ }
+
+ public void edit_undo() {
+ getActiveTextEditor().undo();
+ }
+
+ public void edit_redo() {
+ getActiveTextEditor().redo();
+ }
+
+ public void edit_cut() {
+ getActiveTextEditor().cut();
+ }
+
+ public void edit_copy() {
+ getActiveTextEditor().copy();
+ }
+
+ public void edit_paste() {
+ getActiveTextEditor().paste();
+ }
+
+ public void edit_select_all() {
+ getActiveTextEditor().selectAll();
+ }
+
+ public void edit_find() {
+ final var nodes = getMainScene().getStatusBar().getLeftItems();
+
+ if( nodes.isEmpty() ) {
+ final var searchBar = new SearchBar();
+
+ searchBar.matchIndexProperty().bind( mSearchModel.matchIndexProperty() );
+ searchBar.matchCountProperty().bind( mSearchModel.matchCountProperty() );
+
+ searchBar.setOnCancelAction( event -> {
+ final var editor = getActiveTextEditor();
+ nodes.remove( searchBar );
+ editor.unstylize( STYLE_SEARCH );
+ editor.getNode().requestFocus();
+ } );
+
+ searchBar.addInputListener( ( c, o, n ) -> {
+ if( n != null && !n.isEmpty() ) {
+ mSearchModel.search( n, getActiveTextEditor().getText() );
+ }
+ } );
+
+ searchBar.setOnNextAction( event -> edit_find_next() );
+ searchBar.setOnPrevAction( event -> edit_find_prev() );
+
+ nodes.add( searchBar );
+ searchBar.requestFocus();
+ }
+ }
+
+ public void edit_find_next() {
+ mSearchModel.advance();
+ }
+
+ public void edit_find_prev() {
+ mSearchModel.retreat();
+ }
+
+ public void edit_preferences() {
+ try {
+ new PreferencesController( getWorkspace() ).show();
+ } catch( final Exception ex ) {
+ clue( ex );
+ }
+ }
+
+ public void format_bold() {
+ getActiveTextEditor().bold();
+ }
+
+ public void format_italic() {
+ getActiveTextEditor().italic();
+ }
+
+ public void format_monospace() {
+ getActiveTextEditor().monospace();
+ }
+
+ public void format_superscript() {
+ getActiveTextEditor().superscript();
+ }
+
+ public void format_subscript() {
+ getActiveTextEditor().subscript();
+ }
+
+ public void format_strikethrough() {
+ getActiveTextEditor().strikethrough();
+ }
+
+ public void insert_blockquote() {
+ getActiveTextEditor().blockquote();
+ }
+
+ public void insert_code() {
+ getActiveTextEditor().code();
+ }
+
+ public void insert_fenced_code_block() {
+ getActiveTextEditor().fencedCodeBlock();
+ }
+
+ public void insert_link() {
+ insertObject( createLinkDialog() );
+ }
+
+ public void insert_image() {
+ insertObject( createImageDialog() );
+ }
+
+ private void insertObject( final Dialog<String> dialog ) {
+ final var textArea = getActiveTextEditor().getTextArea();
+ dialog.showAndWait().ifPresent( textArea::replaceSelection );
+ }
+
+ private Dialog<String> createLinkDialog() {
+ return new LinkDialog( getWindow(), createHyperlinkModel() );
+ }
+
+ private Dialog<String> createImageDialog() {
+ final var path = getActiveTextEditor().getPath();
+ final var parentDir = path.getParent();
+ return new ImageDialog( getWindow(), parentDir );
+ }
+
+ /**
+ * Returns one of: selected text, word under cursor, or parsed hyperlink from
+ * the Markdown AST. When a user opts to insert a hyperlink, this will populate
+ * the insert hyperlink dialog with data from the document, thereby allowing a
+ * user to edit an existing link.
+ *
+ * @return An instance containing the link URL and display text.
+ */
+ private HyperlinkModel createHyperlinkModel() {
+ final var context = getMainPane().createProcessorContext();
+ final var editor = getActiveTextEditor();
+ final var textArea = editor.getTextArea();
+ final var selectedText = textArea.getSelectedText();
+
+ // Convert current paragraph to Markdown nodes.
+ final var mp = MarkdownProcessor.create( context );
+ final var p = textArea.getCurrentParagraph();
+ final var paragraph = textArea.getText( p );
+ final var node = mp.toNode( paragraph );
+ final var visitor = new LinkVisitor( textArea.getCaretColumn() );
+ final var link = visitor.process( node );
+
+ if( link != null ) {
+ textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() );
+ }
+
+ return createHyperlinkModel( link, selectedText );
+ }
+
+ private HyperlinkModel createHyperlinkModel(
+ final Link link, final String selection ) {
+
+ return link == null
+ ? new HyperlinkModel( selection )
+ : new HyperlinkModel( link );
+ }
+
+ public void insert_heading_1() {
+ insert_heading( 1 );
+ }
+
+ public void insert_heading_2() {
+ insert_heading( 2 );
+ }
+
+ public void insert_heading_3() {
+ insert_heading( 3 );
+ }
+
+ private void insert_heading( final int level ) {
+ getActiveTextEditor().heading( level );
+ }
+
+ public void insert_unordered_list() {
+ getActiveTextEditor().unorderedList();
+ }
+
+ public void insert_ordered_list() {
+ getActiveTextEditor().orderedList();
+ }
+
+ public void insert_horizontal_rule() {
+ getActiveTextEditor().horizontalRule();
+ }
+
+ public void definition_create() {
+ getActiveTextDefinition().createDefinition();
+ }
+
+ public void definition_rename() {
+ getActiveTextDefinition().renameDefinition();
+ }
+
+ public void definition_delete() {
+ getActiveTextDefinition().deleteDefinitions();
+ }
+
+ public void definition_autoinsert() {
+ getMainPane().autoinsert();
+ }
+
+ public void view_refresh() {
+ getMainPane().viewRefresh();
+ }
+
+ public void view_preview() {
+ getMainPane().viewPreview();
+ }
+
+ public void view_outline() {
+ getMainPane().viewOutline();
+ }
+
+ public void view_files() {getMainPane().viewFiles();}
+
+ public void view_statistics() {
+ getMainPane().viewStatistics();
+ }
+
+ public void view_menubar() {
+ getMainScene().toggleMenuBar();
+ }
+
+ public void view_toolbar() {
+ getMainScene().toggleToolBar();
+ }
+
+ public void view_statusbar() {
+ getMainScene().toggleStatusBar();
+ }
+
+ public void view_log() {
+ mLogView.view();
+ }
+
+ public void help_about() {
+ final var alert = new Alert( INFORMATION );
+ final var prefix = "Dialog.about.";
+ alert.setTitle( get( prefix + "title", APP_TITLE ) );
+ alert.setHeaderText( get( prefix + "header", APP_TITLE ) );
+ alert.setContentText( get( prefix + "content", APP_YEAR, APP_VERSION ) );
+ alert.setGraphic( ICON_DIALOG_NODE );
+ alert.initOwner( getWindow() );
+ alert.showAndWait();
+ }
+
+ private <T> void typeset( final Service<T> service ) {
+ synchronized( mMutex ) {
+ if( service != null && !service.isRunning() ) {
+ service.reset();
+ service.start();
+ }
+ }
+ }
+
+ /**
+ * Concatenates all the files in the same directory as the given file into
+ * a string. The extension is determined by the given file name pattern; the
+ * order files are concatenated is based on their numeric sort order (this
+ * avoids lexicographic sorting).
+ * <p>
+ * If the parent path to the file being edited in the text editor cannot
+ * be found then this will return the editor's text, without iterating through
+ * the parent directory. (Should never happen, but who knows?)
+ * </p>
+ * <p>
+ * New lines are automatically appended to separate each file.
+ * </p>
+ *
+ * @param editor The text editor containing
+ * @return All files in the same directory as the file being edited
+ * concatenated into a single string.
+ */
+ private String append( final TextEditor editor ) {
+ final var pattern = editor.getPath();
+ final var parent = pattern.getParent();
+
+ // Short-circuit because nothing else can be done.
+ if( parent == null ) {
+ clue( "Main.status.export.concat.parent", pattern );
+ return editor.getText();
+ }
+
+ final var filename = SysFile.getFileName( pattern );
+ final var extension = getExtension( filename );
+
+ if( extension.isBlank() ) {
+ clue( "Main.status.export.concat.extension", filename );
+ return editor.getText();
+ }
+
+ try {
+ final var command = new ConcatenateCommand(
+ parent, extension, getString( KEY_TYPESET_CONTEXT_CHAPTERS ) );
+ return command.call();
+ } catch( final Throwable t ) {
+ clue( t );
+ return editor.getText();
+ }
+ }
+
+ private Optional<File> pickFile() {
+ return new OpenUrlDialog( getWindow() ).showAndWait();
}
src/main/java/com/keenwrite/ui/dialogs/AbstractDialog.java
-/* Copyright 2017-2021 White Magic Software, Ltd. -- All rights reserved. */
+/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
+ *
+ * SPDX-License-Identifier: MIT
+ */
package com.keenwrite.ui.dialogs;
* @param title The messages title to display in the title bar.
*/
- @SuppressWarnings( "OverridableMethodCallInConstructor" )
public AbstractDialog( final Window owner, final String title ) {
setTitle( get( title ) );
protected final void initCloseAction() {
final var window = getDialogPane().getScene().getWindow();
- window.setOnCloseRequest( event -> window.hide() );
+ window.setOnCloseRequest( _ -> window.hide() );
}
src/main/java/com/keenwrite/ui/dialogs/CustomDialog.java
+/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+package com.keenwrite.ui.dialogs;
+
+import com.keenwrite.Messages;
+import com.keenwrite.service.events.impl.ButtonOrderPane;
+import javafx.application.Platform;
+import javafx.beans.value.ChangeListener;
+import javafx.geometry.Insets;
+import javafx.scene.control.ButtonBar.ButtonData;
+import javafx.scene.control.Dialog;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.ColumnConstraints;
+import javafx.scene.layout.GridPane;
+import javafx.stage.Window;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import static com.keenwrite.Messages.get;
+import static javafx.scene.control.ButtonType.CANCEL;
+import static javafx.scene.control.ButtonType.OK;
+import static javafx.scene.layout.Priority.ALWAYS;
+import static javafx.scene.layout.Priority.NEVER;
+
+/**
+ * TODO: Replace {@link AbstractDialog} with this class, then remove
+ * {@link AbstractDialog}.
+ *
+ * @param <T> The type of data returned from the dialog upon acceptance.
+ */
+public abstract class CustomDialog<T> extends Dialog<T> {
+ private final GridPane mContentPane = new GridPane( 10, 10 );
+ private final List<TextField> mInputFields = new LinkedList<>();
+
+ public CustomDialog( final Window owner, final String title ) {
+ assert owner != null;
+ assert validate( title );
+
+ initOwner( owner );
+ setTitle( get( title ) );
+ setResizable( true );
+ }
+
+ protected void initialize() {
+ initDialogPane();
+ initDialogButtons();
+ initInputFields();
+ initContentPane();
+
+ assert !mInputFields.isEmpty();
+
+ final var first = mInputFields.getFirst();
+ assert first != null;
+
+ Platform.runLater( first::requestFocus );
+
+ setResultConverter( button -> {
+ final ButtonData data = button == null ? null : button.getButtonData();
+ return data == ButtonData.OK_DONE ? handleAccept() : null;
+ } );
+ }
+
+ /**
+ * Invoked when the user selects the OK button to confirm the input values.
+ *
+ * @return The type of data provided by using the dialog.
+ */
+ protected abstract T handleAccept();
+
+ /**
+ * Subclasses must call this method at least once.
+ *
+ * @param id The unique identifier for the input field.
+ * @param label The input field's label property key.
+ * @param prompt The prompt property key, which provides context.
+ * @param value The initial value to provide for the field.
+ * @see Messages#get(String)
+ */
+ protected void addInputField(
+ final String id,
+ final String label,
+ final String prompt,
+ final String value,
+ final ChangeListener<String> listener ) {
+ assert validate( id );
+ assert validate( label );
+ assert validate( prompt );
+ assert validate( value );
+
+ final int row = mInputFields.size();
+ final Label fieldLabel = new Label( get( label ) );
+ final TextField fieldInput = new TextField();
+
+ fieldInput.setPromptText( get( prompt ) );
+ fieldInput.setId( id );
+ fieldInput.textProperty().addListener( listener );
+ fieldInput.setText( value );
+
+ mContentPane.add( fieldLabel, 0, row );
+ mContentPane.add( fieldInput, 1, row );
+ mInputFields.add( fieldInput );
+ }
+
+ /**
+ * Subclasses must add at least one input field.
+ */
+ protected abstract void initInputFields();
+
+ /**
+ * Set the dialog to use a button order pane with an OK and a CANCEL button.
+ */
+ protected void initDialogPane() {
+ setDialogPane( new ButtonOrderPane() );
+ }
+
+ /**
+ * Set an OK and CANCEL button on the dialog.
+ */
+ protected void initDialogButtons() {
+ getDialogPane().getButtonTypes().addAll( OK, CANCEL );
+ }
+
+ /**
+ * Called after the input fields have been added. This adds the input
+ * fields to the main dialog pane.
+ */
+ protected void initContentPane() {
+ mContentPane.setPadding( new Insets( 20, 10, 10, 10 ) );
+
+ final var cc1 = new ColumnConstraints();
+ final var cc2 = new ColumnConstraints();
+
+ cc1.setHgrow( NEVER );
+ cc2.setHgrow( ALWAYS );
+ cc2.setMinWidth( 250 );
+ mContentPane.getColumnConstraints().addAll( cc1, cc2 );
+
+ getDialogPane().setContent( mContentPane );
+ }
+
+ private static boolean validate( final String s ) {
+ assert s != null;
+ assert !s.isBlank();
+
+ return true;
+ }
+}
src/main/java/com/keenwrite/ui/dialogs/LinkDialog.java
-/*
- * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
+/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
*
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ * SPDX-License-Identifier: MIT
*/
package com.keenwrite.ui.dialogs;
-import com.keenwrite.ui.controls.EscapeTextField;
import com.keenwrite.editors.markdown.HyperlinkModel;
-import javafx.application.Platform;
-import javafx.beans.binding.Bindings;
-import javafx.beans.property.SimpleStringProperty;
-import javafx.beans.property.StringProperty;
-import javafx.scene.control.ButtonBar.ButtonData;
-import javafx.scene.control.DialogPane;
-import javafx.scene.control.Label;
import javafx.stage.Window;
-import org.tbee.javafx.scene.layout.fxml.MigPane;
-
-import static com.keenwrite.Messages.get;
-import static javafx.scene.control.ButtonType.OK;
/**
* Dialog to enter a Markdown link.
*/
-public class LinkDialog extends AbstractDialog<String> {
-
- private final StringProperty link = new SimpleStringProperty();
-
- public LinkDialog(
- final Window owner, final HyperlinkModel hyperlink ) {
- super( owner, "Dialog.link.title" );
-
- final DialogPane dialogPane = getDialogPane();
- dialogPane.setContent( pane );
-
- dialogPane.lookupButton( OK ).disableProperty().bind(
- urlField.escapedTextProperty().isEmpty() );
+public class LinkDialog extends CustomDialog<String> {
+ private static final String PREFIX = "Dialog.link.";
- textField.setText( hyperlink.getText() );
- urlField.setText( hyperlink.getUrl() );
- titleField.setText( hyperlink.getTitle() );
+ /**
+ * Contains information about the hyperlink at the caret position in the
+ * document, if a hyperlink is present at that location. This allows users
+ * to edit existing hyperlinks using this {@link LinkDialog}.
+ */
+ private final HyperlinkModel mModel;
- link.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
- .then( Bindings.format( "[%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
- .otherwise( Bindings.when( textField.escapedTextProperty().isNotEmpty() )
- .then( Bindings.format( "[%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) )
- .otherwise( urlField.escapedTextProperty() ) ) );
+ /**
+ * @param owner {@link Window} responsible for the dialog resource.
+ * @param model Existing hyperlink data, or blank for a new link.
+ */
+ public LinkDialog( final Window owner, final HyperlinkModel model ) {
+ super( owner, STR."\{PREFIX}title" );
- setResultConverter( dialogButton -> {
- ButtonData data = dialogButton != null ? dialogButton.getButtonData() : null;
- return data == ButtonData.OK_DONE ? link.get() : null;
- } );
+ mModel = model;
- Platform.runLater( () -> {
- urlField.requestFocus();
- urlField.selectRange( 0, urlField.getLength() );
- } );
+ super.initialize();
}
@Override
- protected void initComponents() {
- // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
- pane = new MigPane();
- Label urlLabel = new Label();
- urlField = new EscapeTextField();
- Label textLabel = new Label();
- textField = new EscapeTextField();
- Label titleLabel = new Label();
- titleField = new EscapeTextField();
-
- //======== pane ========
- {
- pane.setCols( "[shrink 0,fill][300,grow,fill][fill][fill]" );
- pane.setRows( "[][][][]" );
-
- //---- urlLabel ----
- urlLabel.setText( get( "Dialog.link.urlLabel.text" ) );
- pane.add( urlLabel, "cell 0 0" );
-
- //---- urlField ----
- urlField.setEscapeCharacters( "()" );
- pane.add( urlField, "cell 1 0" );
-
- //---- textLabel ----
- textLabel.setText( get( "Dialog.link.textLabel.text" ) );
- pane.add( textLabel, "cell 0 1" );
-
- //---- textField ----
- textField.setEscapeCharacters( "[]" );
- pane.add( textField, "cell 1 1 3 1" );
-
- //---- titleLabel ----
- titleLabel.setText( get( "Dialog.link.titleLabel.text" ) );
- pane.add( titleLabel, "cell 0 2" );
- pane.add( titleField, "cell 1 2 3 1" );
- }
- // JFormDesigner - End of component initialization //GEN-END:initComponents
+ protected void initInputFields() {
+ addInputField(
+ "text",
+ STR."\{PREFIX}label.text", STR."\{PREFIX}prompt.text",
+ mModel.getText(),
+ ( _, _, n ) -> mModel.setText( n )
+ );
+ addInputField(
+ "url",
+ STR."\{PREFIX}label.url", STR."\{PREFIX}prompt.url",
+ mModel.getUrl(),
+ ( _, _, n ) -> mModel.setUrl( n )
+ );
+ addInputField(
+ "title",
+ STR."\{PREFIX}label.title", STR."\{PREFIX}prompt.title",
+ mModel.getTitle(),
+ ( _, _, n ) -> mModel.setTitle( n )
+ );
}
- // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables
- private MigPane pane;
- private EscapeTextField urlField;
- private EscapeTextField textField;
- private EscapeTextField titleField;
- // JFormDesigner - End of variables declaration //GEN-END:variables
+ @Override
+ protected String handleAccept() {
+ return mModel.toString();
+ }
}
src/main/java/com/keenwrite/ui/dialogs/LinkDialog.jfd
-JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
-
-new FormModel {
- "i18n.bundlePackage": "com.scrivendor"
- "i18n.bundleName": "messages"
- "i18n.autoExternalize": true
- "i18n.keyPrefix": "LinkDialog"
- contentType: "form/javafx"
- root: new FormRoot {
- add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
- "$layoutConstraints": ""
- "$columnConstraints": "[shrink 0,fill][300,grow,fill][fill][fill]"
- "$rowConstraints": "[][][][]"
- } ) {
- name: "pane"
- add( new FormComponent( "javafx.scene.control.Label" ) {
- name: "urlLabel"
- "text": new FormMessage( null, "LinkDialog.urlLabel.text" )
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 0"
- } )
- add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
- name: "urlField"
- "escapeCharacters": "()"
- "text": "http://yourlink.com"
- "promptText": "http://yourlink.com"
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 0"
- } )
- add( new FormComponent( "com.scrivendor.controls.BrowseDirectoryButton" ) {
- name: "linkBrowseDirectoyButton"
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 2 0"
- } )
- add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) {
- name: "linkBrowseFileButton"
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 3 0"
- } )
- add( new FormComponent( "javafx.scene.control.Label" ) {
- name: "textLabel"
- "text": new FormMessage( null, "LinkDialog.textLabel.text" )
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 1"
- } )
- add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
- name: "textField"
- "escapeCharacters": "[]"
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 1 3 1"
- } )
- add( new FormComponent( "javafx.scene.control.Label" ) {
- name: "titleLabel"
- "text": new FormMessage( null, "LinkDialog.titleLabel.text" )
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 2"
- } )
- add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
- name: "titleField"
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 2 3 1"
- } )
- add( new FormComponent( "javafx.scene.control.Label" ) {
- name: "previewLabel"
- "text": new FormMessage( null, "LinkDialog.previewLabel.text" )
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 3"
- } )
- add( new FormComponent( "javafx.scene.control.Label" ) {
- name: "previewField"
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 3 3 1"
- } )
- }, new FormLayoutConstraints( null ) {
- "location": new javafx.geometry.Point2D( 0.0, 0.0 )
- "size": new javafx.geometry.Dimension2D( 500.0, 300.0 )
- } )
- }
-}
src/main/java/com/keenwrite/ui/dialogs/OpenUrlDialog.java
+package com.keenwrite.ui.dialogs;
+
+import javafx.stage.Window;
+
+import java.io.File;
+
+public class OpenUrlDialog extends CustomDialog<File> {
+ private static final String PREFIX = "Dialog.open_url.";
+
+ private String mUrl;
+
+ /**
+ * Ensures that all dialogs can be closed.
+ *
+ * @param owner The parent window of this dialog.
+ */
+ public OpenUrlDialog( final Window owner ) {
+ super( owner, STR."\{PREFIX}title" );
+ super.initialize();
+ }
+
+ @Override
+ protected void initInputFields() {
+ addInputField(
+ "url",
+ STR."\{PREFIX}label.url", STR."\{PREFIX}prompt.url",
+ "",
+ ( _, _, n ) -> mUrl = n
+ );
+ }
+
+ @Override
+ protected File handleAccept() {
+ System.out.println( STR."OPEN URL: \{mUrl}" );
+
+ return null;
+ }
+}
src/main/resources/com/keenwrite/messages.properties
# ########################################################################
-# Image Dialog
+# Open URL dialog
# ########################################################################
-Dialog.image.title=Image
+Dialog.open_url.title=Open URL
+Dialog.open_url.label.url=URL\:
+Dialog.open_url.prompt.url=https://example.com/filename.md
+
+# ########################################################################
+# Insert image dialog
+# ########################################################################
+
+Dialog.image.title=Insert image
Dialog.image.chooser.imagesFilter=Images
Dialog.image.previewLabel.text=Markdown Preview\:
Dialog.image.textLabel.text=Alternate Text\:
Dialog.image.titleLabel.text=Title (tooltip)\:
Dialog.image.urlLabel.text=Image URL\:
# ########################################################################
-# Hyperlink Dialog
+# Insert hyperlink dialog
# ########################################################################
-Dialog.link.title=Link
-Dialog.link.previewLabel.text=Markdown Preview\:
-Dialog.link.textLabel.text=Link Text\:
-Dialog.link.titleLabel.text=Title (tooltip)\:
-Dialog.link.urlLabel.text=Link URL\:
+Dialog.link.title=Insert hyperlink
+Dialog.link.label.text=Text\:
+Dialog.link.label.url=URL\:
+Dialog.link.label.title=Title\:
+Dialog.link.prompt.text=Hyperlink text
+Dialog.link.prompt.url=https://example.com/index.html
+Dialog.link.prompt.title=Hyperlink tooltip
# ########################################################################
-# Typesetting Settings Dialog
+# Typesetting settings dialog
# ########################################################################
# ########################################################################
-# About Dialog
+# About dialog
# ########################################################################
Action.file.open.text=_Open...
Action.file.open.icon=FOLDER_OPEN_ALT
+
+Action.file.open_url.description=Open a URL
+Action.file.open_url.accelerator=Shortcut+Alt+O
+Action.file.open_url.text=Open _URL...
+Action.file.open_url.icon=FOLDER_OPEN_ALT
Action.file.close.description=Close the current document
Action.file.close.accelerator=Shortcut+W
Action.file.close.text=_Close
Action.file.close_all.description=Close all open documents
-Action.file.close_all.accelerator=Ctrl+F4
+Action.file.close_all.accelerator=Shortcut+F4
Action.file.close_all.text=Close All
Action.edit.preferences.description=Edit user preferences
-Action.edit.preferences.accelerator=Ctrl+Alt+S
+Action.edit.preferences.accelerator=Shortcut+Alt+S
Action.edit.preferences.text=_Preferences
Action.insert.blockquote.description=Insert blockquote
-Action.insert.blockquote.accelerator=Ctrl+Q
+Action.insert.blockquote.accelerator=Shortcut+Q
Action.insert.blockquote.text=_Blockquote
Action.insert.blockquote.icon=QUOTE_LEFT
Action.definition.insert.description=Insert a variable
-Action.definition.insert.accelerator=Ctrl+Space
+Action.definition.insert.accelerator=Shortcut+Space
Action.definition.insert.text=_Insert
Action.definition.insert.icon=STAR
Action.view.files.description=Open file manager
-Action.view.files.accelerator=Ctrl+F8
+Action.view.files.accelerator=Shortcut+F8
Action.view.files.text=Files
Action.view.menubar.description=Toggle menu bar
-Action.view.menubar.accelerator=Ctrl+F9
+Action.view.menubar.accelerator=Shortcut+F9
Action.view.menubar.text=Menu bar
Action.view.toolbar.description=Toggle toolbar
-Action.view.toolbar.accelerator=Ctrl+Shift+F9
+Action.view.toolbar.accelerator=Shortcut+Shift+F9
Action.view.toolbar.text=Toolbar
Action.view.statusbar.description=Toggle status bar
-Action.view.statusbar.accelerator=Ctrl+Shift+Alt+F9
+Action.view.statusbar.accelerator=Shortcut+Shift+Alt+F9
Action.view.statusbar.text=Status bar

Adds new open URL dialog, replaces insert hyperlink dialog

Author DaveJarvis <email>
Date 2023-12-24 14:45:56 GMT-0800
Commit 265439b8699639f8907cff08a77c5c3c1954582a
Parent 750c4f7
Delta 966 lines added, 909 lines removed, 57-line increase