| # License | ||
| -Copyright 2020 White Magic Software, Ltd. | ||
| - | ||
| -Copyright 2015 Karl Tauber | ||
| +Copyright 2023 White Magic Software, Ltd. | ||
| All rights reserved. |
| implementation 'org.fxmisc.flowless:flowless:0.7.2' | ||
| implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3' | ||
| - implementation 'com.miglayout:miglayout-javafx:11.3' | ||
| implementation 'com.dlsc.preferencesfx:preferencesfx-core:11.16.0' | ||
| implementation 'com.panemu:tiwulfx-dock:0.2' |
| # Credits | ||
| -* Karl Tauber: [Markdown Writer FX](https://github.com/JFormDesigner/markdown-writer-fx) | ||
| -* Tomas Mikula: [RichTextFX](https://github.com/TomasMikula/RichTextFX), [ReactFX](https://github.com/TomasMikula/ReactFX), [WellBehavedFX](https://github.com/TomasMikula/WellBehavedFX), [Flowless](https://github.com/TomasMikula/Flowless), and [UndoFX](https://github.com/TomasMikula/UndoFX) | ||
| -* Mikael Grev: [MigLayout](http://www.miglayout.com/) | ||
| -* Tom Eugelink: [MigPane](https://github.com/mikaelgrev/miglayout/blob/master/javafx/src/main/java/org/tbee/javafx/scene/layout/fxml/MigPane.java) | ||
| +Using libraries from: | ||
| + | ||
| +* Tomas Mikula: [RichTextFX](https://github.com/TomasMikula/RichTextFX), [WellBehavedFX](https://github.com/TomasMikula/WellBehavedFX), [Flowless](https://github.com/TomasMikula/Flowless), and [UndoFX](https://github.com/TomasMikula/UndoFX) | ||
| * Jens Deters: [FontAwesomeFX](https://bitbucket.org/Jerady/fontawesomefx) | ||
| -* Dieter Holz, [PreferencesFX](https://github.com/dlsc-software-consulting-gmbh/PreferencesFX) | ||
| -* David Croft, [File Preferences](http://www.davidc.net/programming/java/java-preferences-using-file-backing-store) | ||
| -* Alex Bertram, [Renjin](https://www.renjin.org/) | ||
| +* Dieter Holz: [PreferencesFX](https://github.com/dlsc-software-consulting-gmbh/PreferencesFX) | ||
| +* David Croft: [File Preferences](http://www.davidc.net/programming/java/java-preferences-using-file-backing-store) | ||
| +* Alex Bertram: [Renjin](https://www.renjin.org/) | ||
| * Vladimir Schneider: [flexmark](https://github.com/vsch/flexmark-java) | ||
| -* Shy Shalom, Kohei Taketa: [juniversalchardet](https://github.com/takscape/juniversalchardet) | ||
| +* Alberto Fernández, Shy Shalom, Kohei Taketa: [juniversalchardet](https://github.com/takscape/juniversalchardet) | ||
| +* Morten Nobel-Jørgensen: [Java Image Scaling](https://github.com/mortennobel/java-image-scaling) | ||
| + | ||
| +Inspired by: | ||
| +* Karl Tauber: [Markdown Writer FX](https://github.com/JFormDesigner/markdown-writer-fx) | ||
| All rights reserved. | ||
| -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | ||
| - | ||
| -Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 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. | ||
| +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the | ||
| +following conditions are met: | ||
| -Neither the name of the <ORGANIZATION> nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 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 | ||
| +Redistributions of source code must retain the above copyright notice, this list of conditions and the following | ||
| +disclaimer. | ||
| +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. | ||
| +Neither the name of the <ORGANIZATION> nor the names of its contributors may be used to endorse or promote products | ||
| +derived from this software without specific prior written permission. | ||
| +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. | ||
| - | ||
| Contributor(s): | ||
| + Alberto Fernández <infjaf@gmail.com> | ||
| Shy Shalom <shooshX@gmail.com> | ||
| Kohei TAKETA <k-tak@void.in> (Java port) | ||
| the provisions above, a recipient may use your version of this file under | ||
| the terms of any one of the MPL, the GPL or the LGPL. | ||
| - | ||
| -Copyright © 2020 Mark Raynsford <code@io7m.com> http://io7m.com | ||
| - | ||
| -Permission to use, copy, modify, and/or distribute this software for any | ||
| -purpose with or without fee is hereby granted, provided that the above | ||
| -copyright notice and this permission notice appear in all copies. | ||
| - | ||
| -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | ||
| -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | ||
| -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | ||
| -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | ||
| -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | ||
| -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | ||
| -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | ||
| -Copyright (c) 2015 Karl Tauber <karl@jformdesigner.com> | ||
| -All rights reserved. | ||
| - | ||
| -Redistribution and use in source and binary forms, with or without | ||
| -modification, are permitted provided that the following conditions are met: | ||
| - | ||
| -* Redistributions of source code must retain the above copyright | ||
| - notice, this list of conditions and the following disclaimer. | ||
| - | ||
| -* 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. | ||
| - | ||
| -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. | ||
| -Copyright (c) 2000 Mikael Grev | ||
| -All rights reserved. | ||
| - | ||
| -Redistribution and use in source and binary forms, with or without | ||
| -modification, are permitted provided that the following conditions | ||
| -are met: | ||
| -1. Redistributions of source code must retain the above copyright | ||
| - notice, this list of conditions and the following disclaimer. | ||
| -2. 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. | ||
| -3. The name of the author may not be used to endorse or promote products | ||
| - derived from this software without specific prior written permission. | ||
| - | ||
| -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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. | ||
| - | ||
| -Copyright (c) 2013-2014, Tomas Mikula | ||
| -All rights reserved. | ||
| - | ||
| -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | ||
| - | ||
| -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. | ||
| - | ||
| -2. 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. | ||
| - | ||
| -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. | ||
| out( "%n%s version %s", APP_TITLE, APP_VERSION ); | ||
| out( "Copyright 2016-%s White Magic Software, Ltd.", APP_YEAR ); | ||
| - out( "Portions copyright 2015-2020 Karl Tauber.%n" ); | ||
| } | ||
| -/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | ||
| - * | ||
| - * SPDX-License-Identifier: MIT | ||
| - */ | ||
| -package com.keenwrite.editors.markdown; | ||
| - | ||
| -import com.vladsch.flexmark.ast.Link; | ||
| - | ||
| -/** | ||
| - * Represents the model for a hyperlink: text, url, and title. | ||
| - */ | ||
| -public final class HyperlinkModel { | ||
| - 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). | ||
| - */ | ||
| - public HyperlinkModel( final String text ) { | ||
| - this( text, null, null ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Constructs a new hyperlink model for the given AST link. | ||
| - * | ||
| - * @param link A Markdown link. | ||
| - */ | ||
| - public HyperlinkModel( final Link link ) { | ||
| - this( | ||
| - link.getText().toString(), | ||
| - link.getUrl().toString(), | ||
| - link.getTitle().toString() | ||
| - ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Constructs a new hyperlink model in Markdown format by default. | ||
| - * | ||
| - * @param text The hyperlink text displayed (e.g., displayed to the user). | ||
| - * @param url The destination URL (e.g., when clicked). | ||
| - * @param title The hyperlink title (e.g., shown as a tooltip). | ||
| - */ | ||
| - public HyperlinkModel( | ||
| - final String text, final String url, final String title ) { | ||
| - setText( text ); | ||
| - setUrl( url ); | ||
| - setTitle( title ); | ||
| - } | ||
| - | ||
| - public void setText( final String text ) { | ||
| - mText = sanitize( text ); | ||
| - } | ||
| - | ||
| - public void setUrl( final String url ) { | ||
| - mUrl = sanitize( url ); | ||
| - } | ||
| - | ||
| - public void setTitle( final String title ) { | ||
| - mTitle = sanitize( title ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Answers whether text has been set for the hyperlink. | ||
| - * | ||
| - * @return true This is a text link. | ||
| - */ | ||
| - public boolean hasText() { | ||
| - return !getText().isEmpty(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Answers whether a title (tooltip) has been set for the hyperlink. | ||
| - * | ||
| - * @return true There is a title. | ||
| - */ | ||
| - public boolean hasTitle() { | ||
| - return !getTitle().isEmpty(); | ||
| - } | ||
| - | ||
| - public String getText() { | ||
| - return mText; | ||
| - } | ||
| - | ||
| - public String getUrl() { | ||
| - return mUrl; | ||
| - } | ||
| - | ||
| - public String getTitle() { | ||
| - 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() ); | ||
| - } | ||
| -} | ||
| 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 ) ); | ||
| +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.keenwrite.ui.models.HyperlinkModel; | ||
| +import com.keenwrite.ui.models.ImageModel; | ||
| +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( ( _, 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( | ||
| + ( _, _, 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.getFirst(); | ||
| + 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( | ||
| + _ -> { | ||
| + // 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( _ -> { | ||
| + 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( _ -> { | ||
| + final var editor = getActiveTextEditor(); | ||
| + nodes.remove( searchBar ); | ||
| + editor.unstylize( STYLE_SEARCH ); | ||
| + editor.getNode().requestFocus(); | ||
| + } ); | ||
| + | ||
| + searchBar.addInputListener( ( _, _, n ) -> { | ||
| + if( n != null && !n.isEmpty() ) { | ||
| + mSearchModel.search( n, getActiveTextEditor().getText() ); | ||
| + } | ||
| + } ); | ||
| + | ||
| + searchBar.setOnNextAction( _ -> edit_find_next() ); | ||
| + searchBar.setOnPrevAction( _ -> 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 HyperlinkDialog( getWindow(), createHyperlinkModel() ); | ||
| + } | ||
| + | ||
| + private Dialog<String> createImageDialog() { | ||
| + return new ImageDialog( getWindow(), createImageModel() ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * 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 ); | ||
| + } | ||
| + | ||
| + private ImageModel createImageModel() { | ||
| + return new ImageModel( "" ); | ||
| + } | ||
| + | ||
| + 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( STR."\{prefix}title", APP_TITLE ) ); | ||
| + alert.setHeaderText( get( STR."\{prefix}header", APP_TITLE ) ); | ||
| + alert.setContentText( get( STR."\{prefix}content", APP_YEAR, APP_VERSION ) ); | ||
| alert.setGraphic( ICON_DIALOG_NODE ); | ||
| alert.initOwner( getWindow() ); |
| -/* 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.controls; | ||
| -/* | ||
| - * Copyright 2015 Karl Tauber <karl at jformdesigner dot com> | ||
| - * 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. | ||
| - * | ||
| - * 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. | ||
| - */ | ||
| - | ||
| -package com.keenwrite.ui.controls; | ||
| - | ||
| -import com.keenwrite.Messages; | ||
| -import javafx.beans.property.ObjectProperty; | ||
| -import javafx.beans.property.SimpleObjectProperty; | ||
| -import javafx.event.ActionEvent; | ||
| -import javafx.scene.control.Button; | ||
| -import javafx.scene.control.Tooltip; | ||
| -import javafx.scene.input.KeyCode; | ||
| -import javafx.scene.input.KeyEvent; | ||
| -import javafx.stage.FileChooser; | ||
| -import javafx.stage.FileChooser.ExtensionFilter; | ||
| - | ||
| -import java.io.File; | ||
| -import java.nio.file.Path; | ||
| -import java.util.ArrayList; | ||
| -import java.util.List; | ||
| - | ||
| -import static com.keenwrite.io.SysFile.toFile; | ||
| -import static com.keenwrite.ui.fonts.IconFactory.createGraphic; | ||
| -import static org.controlsfx.glyphfont.FontAwesome.Glyph.FILE_ALT; | ||
| - | ||
| -/** | ||
| - * Button that opens a file chooser to select a local file for a URL. | ||
| - */ | ||
| -public class BrowseFileButton extends Button { | ||
| - | ||
| - private final List<ExtensionFilter> mExtensionFilters = new ArrayList<>(); | ||
| - private final ObjectProperty<Path> mBasePath = new SimpleObjectProperty<>(); | ||
| - private final ObjectProperty<String> mUrl = new SimpleObjectProperty<>(); | ||
| - | ||
| - public BrowseFileButton() { | ||
| - setGraphic( createGraphic( FILE_ALT ) ); | ||
| - setTooltip( new Tooltip( Messages.get( "BrowseFileButton.tooltip" ) ) ); | ||
| - setOnAction( this::browse ); | ||
| - | ||
| - disableProperty().bind( mBasePath.isNull() ); | ||
| - | ||
| - // workaround for a JavaFX bug: | ||
| - // avoid closing the dialog that contains this control when the user | ||
| - // closes the FileChooser or DirectoryChooser using the ESC key | ||
| - addEventHandler( KeyEvent.KEY_RELEASED, e -> { | ||
| - if( e.getCode() == KeyCode.ESCAPE ) { | ||
| - e.consume(); | ||
| - } | ||
| - } ); | ||
| - } | ||
| - | ||
| - public void addExtensionFilter( ExtensionFilter extensionFilter ) { | ||
| - mExtensionFilters.add( extensionFilter ); | ||
| - } | ||
| - | ||
| - public ObjectProperty<String> urlProperty() { | ||
| - return mUrl; | ||
| - } | ||
| - | ||
| - private void browse( ActionEvent e ) { | ||
| - var fileChooser = new FileChooser(); | ||
| - fileChooser.setTitle( Messages.get( "BrowseFileButton.chooser.title" ) ); | ||
| - fileChooser.getExtensionFilters().addAll( mExtensionFilters ); | ||
| - fileChooser.getExtensionFilters() | ||
| - .add( new ExtensionFilter( Messages.get( | ||
| - "BrowseFileButton.chooser.allFilesFilter" ), "*.*" ) ); | ||
| - fileChooser.setInitialDirectory( getInitialDirectory() ); | ||
| - var result = fileChooser.showOpenDialog( getScene().getWindow() ); | ||
| - if( result != null ) { | ||
| - updateUrl( result ); | ||
| - } | ||
| - } | ||
| - | ||
| - private File getInitialDirectory() { | ||
| - //TODO build initial directory based on current value of 'url' property | ||
| - return toFile( getBasePath() ); | ||
| - } | ||
| - | ||
| - private void updateUrl( File file ) { | ||
| - String newUrl; | ||
| - try { | ||
| - newUrl = getBasePath().relativize( file.toPath() ).toString(); | ||
| - } catch( final Exception ex ) { | ||
| - newUrl = file.toString(); | ||
| - } | ||
| - mUrl.set( newUrl.replace( '\\', '/' ) ); | ||
| - } | ||
| - | ||
| - public void setBasePath( Path basePath ) { | ||
| - this.mBasePath.set( basePath ); | ||
| - } | ||
| - | ||
| - private Path getBasePath() { | ||
| - return mBasePath.get(); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * Copyright 2020 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. | ||
| - * | ||
| - * 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. | ||
| - */ | ||
| - | ||
| -package com.keenwrite.ui.controls; | ||
| - | ||
| -import javafx.beans.property.SimpleStringProperty; | ||
| -import javafx.beans.property.StringProperty; | ||
| -import javafx.scene.control.TextField; | ||
| -import javafx.util.StringConverter; | ||
| - | ||
| -/** | ||
| - * Responsible for escaping/unescaping characters for Markdown. | ||
| - */ | ||
| -public class EscapeTextField extends TextField { | ||
| - | ||
| - public EscapeTextField() { | ||
| - escapedText.bindBidirectional( | ||
| - textProperty(), | ||
| - new StringConverter<>() { | ||
| - @Override | ||
| - public String toString( String object ) { | ||
| - return escape( object ); | ||
| - } | ||
| - | ||
| - @Override | ||
| - public String fromString( String string ) { | ||
| - return unescape( string ); | ||
| - } | ||
| - } | ||
| - ); | ||
| - escapeCharacters.addListener( | ||
| - e -> escapedText.set( escape( textProperty().get() ) ) | ||
| - ); | ||
| - } | ||
| - | ||
| - // 'escapedText' property | ||
| - private final StringProperty escapedText = new SimpleStringProperty(); | ||
| - | ||
| - public StringProperty escapedTextProperty() { | ||
| - return escapedText; | ||
| - } | ||
| - | ||
| - // 'escapeCharacters' property | ||
| - private final StringProperty escapeCharacters = new SimpleStringProperty(); | ||
| - | ||
| - public String getEscapeCharacters() { | ||
| - return escapeCharacters.get(); | ||
| - } | ||
| - | ||
| - public void setEscapeCharacters( String escapeCharacters ) { | ||
| - this.escapeCharacters.set( escapeCharacters ); | ||
| - } | ||
| - | ||
| - private String escape( final String s ) { | ||
| - final String escapeChars = getEscapeCharacters(); | ||
| - | ||
| - return isEmpty( escapeChars ) ? s : | ||
| - s.replaceAll( "([" + escapeChars.replaceAll( | ||
| - "(.)", | ||
| - "\\\\$1" ) + "])", "\\\\$1" ); | ||
| - } | ||
| - | ||
| - private String unescape( final String s ) { | ||
| - final String escapeChars = getEscapeCharacters(); | ||
| - | ||
| - return isEmpty( escapeChars ) ? s : | ||
| - s.replaceAll( "\\\\([" + escapeChars | ||
| - .replaceAll( "(.)", "\\\\$1" ) + "])", "$1" ); | ||
| - } | ||
| - | ||
| - private static boolean isEmpty( final String s ) { | ||
| - return s == null || s.isEmpty(); | ||
| - } | ||
| -} | ||
| import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG; | ||
| import static com.keenwrite.Messages.get; | ||
| +import static com.keenwrite.util.Strings.validate; | ||
| import static javafx.scene.control.ButtonType.CANCEL; | ||
| import static javafx.scene.control.ButtonType.OK; | ||
| */ | ||
| public AbstractDialog( final Window owner, final String title ) { | ||
| + assert owner != null; | ||
| + assert validate( title ); | ||
| + | ||
| setTitle( get( title ) ); | ||
| setResizable( true ); | ||
| import com.keenwrite.Messages; | ||
| import com.keenwrite.service.events.impl.ButtonOrderPane; | ||
| +import com.keenwrite.util.Strings; | ||
| import javafx.application.Platform; | ||
| import javafx.beans.value.ChangeListener; | ||
| import static com.keenwrite.Messages.get; | ||
| +import static com.keenwrite.util.Strings.*; | ||
| import static javafx.scene.control.ButtonType.CANCEL; | ||
| import static javafx.scene.control.ButtonType.OK; | ||
| getDialogPane().setContent( mContentPane ); | ||
| - } | ||
| - | ||
| - private static boolean validate( final String s ) { | ||
| - assert s != null; | ||
| - assert !s.isBlank(); | ||
| - | ||
| - return true; | ||
| } | ||
| } | ||
| textField.textProperty().addListener( | ||
| - ( c, o, n ) -> textField.setText( RangeValidator.normalize( n ) ) | ||
| + ( _, _, n ) -> textField.setText( RangeValidator.normalize( n ) ) | ||
| ); | ||
| return textField; | ||
| } | ||
| private Label createLabel( final String key ) { | ||
| - final var label = new Label( get( key ) + ":" ); | ||
| + final var label = new Label( STR."\{get( key )}:" ); | ||
| final var font = label.getFont(); | ||
| final var upscale = new Font( font.getName(), 14 ); |
| +/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.ui.dialogs; | ||
| + | ||
| +import com.keenwrite.ui.models.HyperlinkModel; | ||
| +import javafx.stage.Window; | ||
| + | ||
| +/** | ||
| + * Dialog to enter a Markdown link. | ||
| + */ | ||
| +public class HyperlinkDialog extends CustomDialog<String> { | ||
| + private static final String PREFIX = "Dialog.link."; | ||
| + | ||
| + /** | ||
| + * 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 HyperlinkDialog}. | ||
| + */ | ||
| + private final HyperlinkModel mModel; | ||
| + | ||
| + /** | ||
| + * @param owner {@link Window} responsible for the dialog resource. | ||
| + * @param model Existing hyperlink data, or blank for a new link. | ||
| + */ | ||
| + public HyperlinkDialog( final Window owner, final HyperlinkModel model ) { | ||
| + super( owner, STR."\{PREFIX}title" ); | ||
| + | ||
| + mModel = model; | ||
| + | ||
| + super.initialize(); | ||
| + } | ||
| + | ||
| + @Override | ||
| + 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 ) | ||
| + ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + protected String handleAccept() { | ||
| + return mModel.toString(); | ||
| + } | ||
| +} | ||
| -/* | ||
| - * Copyright 2015 Karl Tauber <karl at jformdesigner dot com> | ||
| - * 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 static com.keenwrite.Messages.get; | ||
| -import com.keenwrite.ui.controls.BrowseFileButton; | ||
| -import com.keenwrite.ui.controls.EscapeTextField; | ||
| -import java.nio.file.Path; | ||
| -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 static javafx.scene.control.ButtonType.OK; | ||
| -import javafx.scene.control.DialogPane; | ||
| -import javafx.scene.control.Label; | ||
| -import javafx.stage.FileChooser.ExtensionFilter; | ||
| +import com.keenwrite.ui.models.ImageModel; | ||
| import javafx.stage.Window; | ||
| -import org.tbee.javafx.scene.layout.fxml.MigPane; | ||
| /** | ||
| * Dialog to enter a Markdown image. | ||
| */ | ||
| -public class ImageDialog extends AbstractDialog<String> { | ||
| - | ||
| - private final StringProperty image = new SimpleStringProperty(); | ||
| - | ||
| - public ImageDialog( final Window owner, final Path basePath ) { | ||
| - super(owner, "Dialog.image.title" ); | ||
| - | ||
| - final DialogPane dialogPane = getDialogPane(); | ||
| - dialogPane.setContent( pane ); | ||
| - | ||
| - linkBrowseFileButton.setBasePath( basePath ); | ||
| - linkBrowseFileButton.addExtensionFilter( new ExtensionFilter( get( "Dialog.image.chooser.imagesFilter" ), "*.png", "*.gif", "*.jpg" ) ); | ||
| - linkBrowseFileButton.urlProperty().bindBidirectional( urlField.escapedTextProperty() ); | ||
| - | ||
| - dialogPane.lookupButton( OK ).disableProperty().bind( | ||
| - urlField.escapedTextProperty().isEmpty() | ||
| - .or( textField.escapedTextProperty().isEmpty() ) ); | ||
| +public class ImageDialog extends CustomDialog<String> { | ||
| + private static final String PREFIX = "Dialog.image."; | ||
| - image.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() ) | ||
| - .then( Bindings.format( "", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) ) | ||
| - .otherwise( Bindings.format( "", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) ); | ||
| - previewField.textProperty().bind( image ); | ||
| + private final ImageModel mModel; | ||
| - setResultConverter( dialogButton -> { | ||
| - ButtonData data = dialogButton != null ? dialogButton.getButtonData() : null; | ||
| - return data == ButtonData.OK_DONE ? image.get() : null; | ||
| - } ); | ||
| + public ImageDialog( final Window owner, final ImageModel model ) { | ||
| + super( owner, STR."\{PREFIX}title" ); | ||
| - Platform.runLater( () -> { | ||
| - urlField.requestFocus(); | ||
| + mModel = model; | ||
| - if( urlField.getText().startsWith( "http://" ) ) { | ||
| - urlField.selectRange( "http://".length(), 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(); | ||
| - linkBrowseFileButton = new BrowseFileButton(); | ||
| - Label textLabel = new Label(); | ||
| - textField = new EscapeTextField(); | ||
| - Label titleLabel = new Label(); | ||
| - titleField = new EscapeTextField(); | ||
| - Label previewLabel = new Label(); | ||
| - previewField = new Label(); | ||
| - | ||
| - //======== pane ======== | ||
| - { | ||
| - pane.setCols( "[shrink 0,fill][300,grow,fill][fill]" ); | ||
| - pane.setRows( "[][][][]" ); | ||
| - | ||
| - //---- urlLabel ---- | ||
| - urlLabel.setText( get( "Dialog.image.urlLabel.text" ) ); | ||
| - pane.add( urlLabel, "cell 0 0" ); | ||
| - | ||
| - //---- urlField ---- | ||
| - urlField.setEscapeCharacters( "()" ); | ||
| - urlField.setText( "https://yourlink.com" ); | ||
| - urlField.setPromptText( "https://yourlink.com" ); | ||
| - pane.add( urlField, "cell 1 0" ); | ||
| - pane.add( linkBrowseFileButton, "cell 2 0" ); | ||
| - | ||
| - //---- textLabel ---- | ||
| - textLabel.setText( get( "Dialog.image.textLabel.text" ) ); | ||
| - pane.add( textLabel, "cell 0 1" ); | ||
| - | ||
| - //---- textField ---- | ||
| - textField.setEscapeCharacters( "[]" ); | ||
| - pane.add( textField, "cell 1 1 2 1" ); | ||
| - | ||
| - //---- titleLabel ---- | ||
| - titleLabel.setText( get( "Dialog.image.titleLabel.text" ) ); | ||
| - pane.add( titleLabel, "cell 0 2" ); | ||
| - pane.add( titleField, "cell 1 2 2 1" ); | ||
| - | ||
| - //---- previewLabel ---- | ||
| - previewLabel.setText( get( "Dialog.image.previewLabel.text" ) ); | ||
| - pane.add( previewLabel, "cell 0 3" ); | ||
| - pane.add( previewField, "cell 1 3 2 1" ); | ||
| - } | ||
| - // JFormDesigner - End of component initialization //GEN-END:initComponents | ||
| + protected void initInputFields() { | ||
| + addInputField( | ||
| + "url", | ||
| + STR."\{PREFIX}label.url", STR."\{PREFIX}prompt.url", | ||
| + mModel.getUrl(), | ||
| + ( _, _, n ) -> mModel.setUrl( n ) | ||
| + ); | ||
| + addInputField( | ||
| + "text", | ||
| + STR."\{PREFIX}label.text", STR."\{PREFIX}prompt.text", | ||
| + mModel.getText(), | ||
| + ( _, _, n ) -> mModel.setText( 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 BrowseFileButton linkBrowseFileButton; | ||
| - private EscapeTextField textField; | ||
| - private EscapeTextField titleField; | ||
| - private Label previewField; | ||
| - // JFormDesigner - End of variables declaration //GEN-END:variables | ||
| + @Override | ||
| + protected String handleAccept() { | ||
| + return mModel.toString(); | ||
| + } | ||
| } | ||
| -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": "ImageDialog" | ||
| - 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]" | ||
| - "$rowConstraints": "[][][][]" | ||
| - } ) { | ||
| - name: "pane" | ||
| - add( new FormComponent( "javafx.scene.control.Label" ) { | ||
| - name: "urlLabel" | ||
| - "text": new FormMessage( null, "ImageDialog.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.BrowseFileButton" ) { | ||
| - name: "linkBrowseFileButton" | ||
| - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | ||
| - "value": "cell 2 0" | ||
| - } ) | ||
| - add( new FormComponent( "javafx.scene.control.Label" ) { | ||
| - name: "textLabel" | ||
| - "text": new FormMessage( null, "ImageDialog.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 2 1" | ||
| - } ) | ||
| - add( new FormComponent( "javafx.scene.control.Label" ) { | ||
| - name: "titleLabel" | ||
| - "text": new FormMessage( null, "ImageDialog.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 2 1" | ||
| - } ) | ||
| - add( new FormComponent( "javafx.scene.control.Label" ) { | ||
| - name: "previewLabel" | ||
| - "text": new FormMessage( null, "ImageDialog.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 2 1" | ||
| - } ) | ||
| - }, new FormLayoutConstraints( null ) { | ||
| - "location": new javafx.geometry.Point2D( 0.0, 0.0 ) | ||
| - "size": new javafx.geometry.Dimension2D( 500.0, 300.0 ) | ||
| - } ) | ||
| - } | ||
| -} | ||
| -/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | ||
| - * | ||
| - * SPDX-License-Identifier: MIT | ||
| - */ | ||
| -package com.keenwrite.ui.dialogs; | ||
| - | ||
| -import com.keenwrite.editors.markdown.HyperlinkModel; | ||
| -import javafx.stage.Window; | ||
| - | ||
| -/** | ||
| - * Dialog to enter a Markdown link. | ||
| - */ | ||
| -public class LinkDialog extends CustomDialog<String> { | ||
| - private static final String PREFIX = "Dialog.link."; | ||
| - | ||
| - /** | ||
| - * 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; | ||
| - | ||
| - /** | ||
| - * @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" ); | ||
| - | ||
| - mModel = model; | ||
| - | ||
| - super.initialize(); | ||
| - } | ||
| - | ||
| - @Override | ||
| - 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 ) | ||
| - ); | ||
| - } | ||
| - | ||
| - @Override | ||
| - protected String handleAccept() { | ||
| - return mModel.toString(); | ||
| - } | ||
| -} | ||
| +/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| 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; | ||
| + private String mUrl = ""; | ||
| /** | ||
| "url", | ||
| STR."\{PREFIX}label.url", STR."\{PREFIX}prompt.url", | ||
| - "", | ||
| + mUrl, | ||
| ( _, _, n ) -> mUrl = n | ||
| ); | ||
| +/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.ui.models; | ||
| + | ||
| +import com.vladsch.flexmark.ast.Link; | ||
| + | ||
| +/** | ||
| + * Represents the model for a hyperlink: text, url, and title. | ||
| + */ | ||
| +public final class HyperlinkModel extends ObjectModel { | ||
| + | ||
| + /** | ||
| + * 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). | ||
| + */ | ||
| + public HyperlinkModel( final String text ) { | ||
| + super( text ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Constructs a new hyperlink model in Markdown format by default. | ||
| + * | ||
| + * @param text The hyperlink text displayed (e.g., displayed to the user). | ||
| + * @param url The destination URL (e.g., when clicked). | ||
| + * @param title The hyperlink title (e.g., shown as a tooltip). | ||
| + */ | ||
| + public HyperlinkModel( | ||
| + final String text, final String url, final String title ) { | ||
| + super( text, url, title ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Constructs a new hyperlink model for the given AST link. | ||
| + * | ||
| + * @param link A Markdown link. | ||
| + */ | ||
| + public HyperlinkModel( final Link link ) { | ||
| + this( | ||
| + link.getText().toString(), | ||
| + link.getUrl().toString(), | ||
| + link.getTitle().toString() | ||
| + ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * 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() ); | ||
| + } | ||
| +} | ||
| +/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.ui.models; | ||
| + | ||
| +/** | ||
| + * Represents the model for an image: text, url, and title. | ||
| + */ | ||
| +public final class ImageModel extends ObjectModel { | ||
| + | ||
| + /** | ||
| + * Constructs a new image model in Markdown format by default with no | ||
| + * title (i.e., tooltip). | ||
| + * | ||
| + * @param text The alternate text (e.g., displayed to the user). | ||
| + */ | ||
| + public ImageModel( final String text ) { | ||
| + super( text ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns the string in Markdown format by default. | ||
| + * | ||
| + * @return An image reference using Markdown syntax. | ||
| + */ | ||
| + @Override | ||
| + public String toString() { | ||
| + final String format = hasText() | ||
| + ? STR."![%s]\{hasTitle() ? "(%s \"%s\")" : "(%s%s)"}" | ||
| + : "%s"; | ||
| + | ||
| + return String.format( format, getText(), getUrl(), getTitle() ); | ||
| + } | ||
| +} | ||
| +/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.ui.models; | ||
| + | ||
| +import static com.keenwrite.util.Strings.sanitize; | ||
| + | ||
| +/** | ||
| + * Represents the model for an object containing text, url, and title. | ||
| + */ | ||
| +class ObjectModel { | ||
| + private String mText; | ||
| + private String mUrl; | ||
| + private String mTitle; | ||
| + | ||
| + /** | ||
| + * Constructs a new object model in Markdown format by default with no | ||
| + * title (i.e., tooltip). | ||
| + * | ||
| + * @param text The hyperlink text displayed (e.g., displayed to the user). | ||
| + */ | ||
| + public ObjectModel( final String text ) { | ||
| + this( text, null, null ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Constructs a new object model in Markdown format by default. | ||
| + * | ||
| + * @param text The text displayed (e.g., to the user). | ||
| + * @param url The destination URL (e.g., when clicked). | ||
| + * @param title The text title (e.g., shown as a tooltip). | ||
| + */ | ||
| + public ObjectModel( | ||
| + final String text, final String url, final String title ) { | ||
| + setText( text ); | ||
| + setUrl( url ); | ||
| + setTitle( title ); | ||
| + } | ||
| + | ||
| + public void setText( final String text ) { | ||
| + mText = sanitize( text ); | ||
| + } | ||
| + | ||
| + public void setUrl( final String url ) { | ||
| + mUrl = sanitize( url ); | ||
| + } | ||
| + | ||
| + public void setTitle( final String title ) { | ||
| + mTitle = sanitize( title ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Answers whether text has been set for the model. | ||
| + * | ||
| + * @return true The text description is set. | ||
| + */ | ||
| + public boolean hasText() { | ||
| + return !getText().isEmpty(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Answers whether a title (tooltip) has been set for the model. | ||
| + * | ||
| + * @return true The title is set. | ||
| + */ | ||
| + public boolean hasTitle() { | ||
| + return !getTitle().isEmpty(); | ||
| + } | ||
| + | ||
| + public String getText() { | ||
| + return mText; | ||
| + } | ||
| + | ||
| + public String getUrl() { | ||
| + return mUrl; | ||
| + } | ||
| + | ||
| + public String getTitle() { | ||
| + return mTitle; | ||
| + } | ||
| +} | ||
| return str.substring( start, end ); | ||
| } | ||
| + | ||
| + public static boolean validate( final String s ) { | ||
| + assert s != null; | ||
| + assert !s.isBlank(); | ||
| + | ||
| + return true; | ||
| + } | ||
| + | ||
| + public static String sanitize( final String s ) { | ||
| + return s == null ? "" : s; | ||
| + } | ||
| } | ||
| # ######################################################################## | ||
| -# Browse File | ||
| -# ######################################################################## | ||
| - | ||
| -BrowseFileButton.chooser.title=Open local file | ||
| -BrowseFileButton.chooser.allFilesFilter=All Files | ||
| -BrowseFileButton.tooltip=${BrowseFileButton.chooser.title} | ||
| - | ||
| -# ######################################################################## | ||
| # Browse Directory | ||
| # ######################################################################## | ||
| 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\: | ||
| +Dialog.image.label.url=File or URL\: | ||
| +Dialog.image.label.text=Alternate text\: | ||
| +Dialog.image.label.title=Title\: | ||
| +Dialog.image.prompt.url=Image resource | ||
| +Dialog.image.prompt.text=Image description | ||
| +Dialog.image.prompt.title=Image tooltip | ||
| # ######################################################################## | ||
| -/* | ||
| - * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com> | ||
| - * 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. | ||
| - * | ||
| - * 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. | ||
| - */ | ||
| - | ||
| .tool-bar { | ||
| -fx-spacing: 0; |
| Author | DaveJarvis <email> |
|---|---|
| Date | 2023-12-24 15:41:03 GMT-0800 |
| Commit | 4ad7f2dcee575dff9dadab0648846e565d310dd6 |
| Parent | 265439b |
| Delta | 931 lines added, 1339 lines removed, 408-line decrease |