| Author | DaveJarvis <email> |
|---|---|
| Date | 2022-01-02 13:22:19 GMT-0800 |
| Commit | 46c7c784ac9ad4ea9f6a79aefe3d4fd150f1146c |
| Parent | 13b94f3 |
| Delta | 110 lines added, 353 lines removed, 243-line decrease |
| import com.keenwrite.AwaitFxExtension; | ||
| -import com.keenwrite.Caret; | ||
| +import com.keenwrite.editors.common.Caret; | ||
| import com.keenwrite.preferences.Workspace; | ||
| import com.keenwrite.processors.Processor; |
| package com.keenwrite.ui.tree; | ||
| +import com.keenwrite.ui.cells.AltTreeCell; | ||
| import javafx.scene.control.TreeItem; | ||
| import javafx.scene.control.TreeView; |
| +package com.keenwrite.ui.cells; | ||
| + | ||
| +import javafx.beans.property.ObjectProperty; | ||
| +import javafx.beans.property.Property; | ||
| +import javafx.beans.property.SimpleStringProperty; | ||
| +import javafx.beans.value.ChangeListener; | ||
| +import javafx.beans.value.ObservableValue; | ||
| +import javafx.event.EventHandler; | ||
| +import javafx.scene.Node; | ||
| +import javafx.scene.control.TableCell; | ||
| +import javafx.scene.control.TextField; | ||
| +import javafx.scene.control.TreeCell; | ||
| +import javafx.scene.input.KeyEvent; | ||
| + | ||
| +import java.util.function.Consumer; | ||
| + | ||
| +import static javafx.application.Platform.runLater; | ||
| +import static javafx.scene.input.KeyCode.ENTER; | ||
| +import static javafx.scene.input.KeyCode.TAB; | ||
| +import static javafx.scene.input.KeyEvent.KEY_RELEASED; | ||
| + | ||
| +public class CellEditor { | ||
| + private FocusListener mFocusListener; | ||
| + private final KeyHandler mKeyHandler = new KeyHandler(); | ||
| + private final Property<String> mInputText = new SimpleStringProperty(); | ||
| + private final Consumer<String> mConsumer; | ||
| + | ||
| + /** | ||
| + * Responsible for accepting the text when users press the Enter or Tab key. | ||
| + */ | ||
| + private class KeyHandler implements EventHandler<KeyEvent> { | ||
| + @Override | ||
| + public void handle( final KeyEvent event ) { | ||
| + if( event.getCode() == ENTER || event.getCode() == TAB ) { | ||
| + commitEdit(); | ||
| + event.consume(); | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + /** | ||
| + * Responsible for committing edits when focus is lost. This will also | ||
| + * deselect the input field when focus is gained so that typing text won't | ||
| + * overwrite the entire existing text. | ||
| + */ | ||
| + private class FocusListener implements ChangeListener<Boolean> { | ||
| + private final TextField mInput; | ||
| + | ||
| + private FocusListener( final TextField input ) { | ||
| + mInput = input; | ||
| + } | ||
| + | ||
| + @Override | ||
| + public void changed( | ||
| + final ObservableValue<? extends Boolean> c, | ||
| + final Boolean endedFocus, final Boolean beganFocus ) { | ||
| + | ||
| + if( beganFocus ) { | ||
| + runLater( mInput::deselect ); | ||
| + } | ||
| + else if( endedFocus ) { | ||
| + commitEdit(); | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + /** | ||
| + * Generalized cell editor suitable for use with {@link TableCell} or | ||
| + * {@link TreeCell} instances. | ||
| + * | ||
| + * @param consumer Converts the field input text to the required | ||
| + * data type. | ||
| + * @param graphicProperty Defines the graphical user input field. | ||
| + */ | ||
| + public CellEditor( | ||
| + final Consumer<String> consumer, | ||
| + final ObjectProperty<Node> graphicProperty ) { | ||
| + assert consumer != null; | ||
| + mConsumer = consumer; | ||
| + | ||
| + init( graphicProperty ); | ||
| + } | ||
| + | ||
| + private void init( final ObjectProperty<Node> graphicProperty ) { | ||
| + // When the text field is added as the graphics context, we hook into | ||
| + // the changed value to get a handle on the text field. From there it is | ||
| + // possible to add change the keyboard and focus behaviours. | ||
| + graphicProperty.addListener( ( c, o, n ) -> { | ||
| + if( o instanceof TextField ) { | ||
| + o.removeEventHandler( KEY_RELEASED, mKeyHandler ); | ||
| + o.focusedProperty().removeListener( mFocusListener ); | ||
| + } | ||
| + | ||
| + if( n instanceof final TextField input ) { | ||
| + n.addEventFilter( KEY_RELEASED, mKeyHandler ); | ||
| + mInputText.bind( input.textProperty() ); | ||
| + mFocusListener = new FocusListener( input ); | ||
| + n.focusedProperty().addListener( mFocusListener ); | ||
| + } | ||
| + } ); | ||
| + } | ||
| + | ||
| + private void commitEdit() { | ||
| + mConsumer.accept( mInputText.getValue() ); | ||
| + } | ||
| +} | ||
| import com.keenwrite.editors.TextEditor; | ||
| import com.keenwrite.editors.TextResource; | ||
| +import com.keenwrite.editors.common.ScrollEventHandler; | ||
| +import com.keenwrite.editors.common.VariableNameInjector; | ||
| import com.keenwrite.editors.definition.DefinitionEditor; | ||
| import com.keenwrite.editors.definition.TreeTransformer; |
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| -package com.keenwrite; | ||
| - | ||
| -import javax.net.ssl.*; | ||
| -import java.security.SecureRandom; | ||
| -import java.security.cert.X509Certificate; | ||
| - | ||
| -import static javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier; | ||
| -import static javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory; | ||
| - | ||
| -/** | ||
| - * Responsible for trusting all certificate chains. The purpose of this class | ||
| - * is to work-around certificate issues caused by software that blocks | ||
| - * HTTP requests. For example, zscaler may block HTTP requests to kroki.io | ||
| - * when generating diagrams. | ||
| - */ | ||
| -public final class PermissiveCertificate { | ||
| - /** | ||
| - * Create a trust manager that does not validate certificate chains. | ||
| - */ | ||
| - private final static TrustManager[] TRUST_ALL_CERTS = new TrustManager[]{ | ||
| - new X509TrustManager() { | ||
| - @Override | ||
| - public X509Certificate[] getAcceptedIssuers() { | ||
| - return new X509Certificate[ 0 ]; | ||
| - } | ||
| - | ||
| - @Override | ||
| - public void checkClientTrusted( | ||
| - X509Certificate[] certs, String authType ) { | ||
| - } | ||
| - | ||
| - @Override | ||
| - public void checkServerTrusted( | ||
| - X509Certificate[] certs, String authType ) { | ||
| - } | ||
| - } | ||
| - }; | ||
| - | ||
| - /** | ||
| - * Responsible for permitting all hostnames for making HTTP requests. | ||
| - */ | ||
| - private static class PermissiveHostNameVerifier implements HostnameVerifier { | ||
| - @Override | ||
| - public boolean verify( final String hostname, final SSLSession session ) { | ||
| - return true; | ||
| - } | ||
| - } | ||
| - | ||
| - /** | ||
| - * Install the all-trusting trust manager. If this fails it means that in | ||
| - * certain situations the HTML preview may fail to render diagrams. A way | ||
| - * to work around the issue is to install a local server for generating | ||
| - * diagrams. | ||
| - */ | ||
| - public static boolean installTrustManager() { | ||
| - try { | ||
| - final var context = SSLContext.getInstance( "SSL" ); | ||
| - context.init( null, TRUST_ALL_CERTS, new SecureRandom() ); | ||
| - setDefaultSSLSocketFactory( context.getSocketFactory() ); | ||
| - setDefaultHostnameVerifier( new PermissiveHostNameVerifier() ); | ||
| - return true; | ||
| - } catch( final Exception ex ) { | ||
| - return false; | ||
| - } | ||
| - } | ||
| - | ||
| - /** | ||
| - * Use {@link #installTrustManager()}. | ||
| - */ | ||
| - private PermissiveCertificate() { | ||
| - } | ||
| -} | ||
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| -package com.keenwrite; | ||
| - | ||
| -import com.keenwrite.events.ScrollLockEvent; | ||
| -import javafx.beans.property.BooleanProperty; | ||
| -import javafx.beans.property.SimpleBooleanProperty; | ||
| -import javafx.event.Event; | ||
| -import javafx.event.EventHandler; | ||
| -import javafx.scene.control.ScrollBar; | ||
| -import javafx.scene.control.skin.ScrollBarSkin; | ||
| -import javafx.scene.input.MouseEvent; | ||
| -import javafx.scene.input.ScrollEvent; | ||
| -import javafx.scene.layout.StackPane; | ||
| -import org.fxmisc.flowless.VirtualizedScrollPane; | ||
| -import org.fxmisc.richtext.StyleClassedTextArea; | ||
| -import org.greenrobot.eventbus.Subscribe; | ||
| - | ||
| -import javax.swing.*; | ||
| -import java.util.function.Consumer; | ||
| - | ||
| -import static com.keenwrite.events.Bus.register; | ||
| -import static java.lang.Math.max; | ||
| -import static java.lang.Math.min; | ||
| -import static javafx.geometry.Orientation.VERTICAL; | ||
| -import static javax.swing.SwingUtilities.invokeLater; | ||
| - | ||
| -/** | ||
| - * Converts scroll events from {@link VirtualizedScrollPane} scroll bars to | ||
| - * an instance of {@link JScrollBar}. | ||
| - * <p> | ||
| - * Called to synchronize the scrolling areas for either scrolling with the | ||
| - * mouse or scrolling using the scrollbar's thumb. Both are required to avoid | ||
| - * scrolling on the estimatedScrollYProperty that occurs when text events | ||
| - * fire. Scrolling performed for text events are handled separately to ensure | ||
| - * the preview panel scrolls to the same position in the Markdown editor, | ||
| - * taking into account things like images, tables, and other potentially | ||
| - * long vertical presentation items. | ||
| - * </p> | ||
| - */ | ||
| -public final class ScrollEventHandler implements EventHandler<Event> { | ||
| - | ||
| - private final class MouseHandler implements EventHandler<MouseEvent> { | ||
| - private final EventHandler<? super MouseEvent> mOldHandler; | ||
| - | ||
| - /** | ||
| - * Constructs a new handler for mouse scrolling events. | ||
| - * | ||
| - * @param oldHandler Receives the event after scrolling takes place. | ||
| - */ | ||
| - private MouseHandler( final EventHandler<? super MouseEvent> oldHandler ) { | ||
| - mOldHandler = oldHandler; | ||
| - } | ||
| - | ||
| - @Override | ||
| - public void handle( final MouseEvent event ) { | ||
| - ScrollEventHandler.this.handle( event ); | ||
| - mOldHandler.handle( event ); | ||
| - } | ||
| - } | ||
| - | ||
| - private final class ScrollHandler implements EventHandler<ScrollEvent> { | ||
| - @Override | ||
| - public void handle( final ScrollEvent event ) { | ||
| - ScrollEventHandler.this.handle( event ); | ||
| - } | ||
| - } | ||
| - | ||
| - private final VirtualizedScrollPane<StyleClassedTextArea> mEditorScrollPane; | ||
| - private final JScrollBar mPreviewScrollBar; | ||
| - private final BooleanProperty mEnabled = new SimpleBooleanProperty(); | ||
| - | ||
| - private boolean mLocked; | ||
| - | ||
| - /** | ||
| - * @param editorScrollPane Scroll event source (human movement). | ||
| - * @param previewScrollBar Scroll event destination (corresponding movement). | ||
| - */ | ||
| - public ScrollEventHandler( | ||
| - final VirtualizedScrollPane<StyleClassedTextArea> editorScrollPane, | ||
| - final JScrollBar previewScrollBar ) { | ||
| - mEditorScrollPane = editorScrollPane; | ||
| - mPreviewScrollBar = previewScrollBar; | ||
| - | ||
| - mEditorScrollPane.addEventFilter( ScrollEvent.ANY, new ScrollHandler() ); | ||
| - | ||
| - initVerticalScrollBarThumb( | ||
| - mEditorScrollPane, | ||
| - thumb -> { | ||
| - final var handler = new MouseHandler( thumb.getOnMouseDragged() ); | ||
| - thumb.setOnMouseDragged( handler ); | ||
| - } | ||
| - ); | ||
| - | ||
| - register( this ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Gets a property intended to be bound to selected property of the tab being | ||
| - * scrolled. This is required because there's only one preview pane but | ||
| - * multiple editor panes. Each editor pane maintains its own scroll position. | ||
| - * | ||
| - * @return A {@link BooleanProperty} representing whether the scroll | ||
| - * events for this tab are to be executed. | ||
| - */ | ||
| - public BooleanProperty enabledProperty() { | ||
| - return mEnabled; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Scrolls the preview scrollbar relative to the edit scrollbar. Algorithm | ||
| - * is based on Karl Tauber's ratio calculation. | ||
| - * | ||
| - * @param event Unused; either {@link MouseEvent} or {@link ScrollEvent} | ||
| - */ | ||
| - @Override | ||
| - public void handle( final Event event ) { | ||
| - invokeLater( () -> { | ||
| - if( isEnabled() ) { | ||
| - // e is for editor pane | ||
| - final var eScrollPane = getEditorScrollPane(); | ||
| - final var eScrollY = | ||
| - eScrollPane.estimatedScrollYProperty().getValue().intValue(); | ||
| - final var eHeight = (int) | ||
| - (eScrollPane.totalHeightEstimateProperty().getValue().intValue() | ||
| - - eScrollPane.getHeight()); | ||
| - final var eRatio = eHeight > 0 | ||
| - ? min( max( eScrollY / (float) eHeight, 0 ), 1 ) : 0; | ||
| - | ||
| - // p is for preview pane | ||
| - final var pScrollBar = getPreviewScrollBar(); | ||
| - final var pHeight = pScrollBar.getMaximum() - pScrollBar.getHeight(); | ||
| - final var pScrollY = (int) (pHeight * eRatio); | ||
| - | ||
| - pScrollBar.setValue( pScrollY ); | ||
| - pScrollBar.getParent().repaint(); | ||
| - } | ||
| - } ); | ||
| - } | ||
| - | ||
| - @Subscribe | ||
| - public void handle( final ScrollLockEvent event ) { | ||
| - mLocked = event.isLocked(); | ||
| - } | ||
| - | ||
| - private void initVerticalScrollBarThumb( | ||
| - final VirtualizedScrollPane<StyleClassedTextArea> pane, | ||
| - final Consumer<StackPane> consumer ) { | ||
| - // When the skin property is set, the stack pane is available (not null). | ||
| - getVerticalScrollBar( pane ).skinProperty().addListener( ( c, o, n ) -> { | ||
| - for( final var node : ((ScrollBarSkin) n).getChildren() ) { | ||
| - // Brittle, but what can you do? | ||
| - if( node.getStyleClass().contains( "thumb" ) ) { | ||
| - consumer.accept( (StackPane) node ); | ||
| - } | ||
| - } | ||
| - } ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns the vertical {@link ScrollBar} instance associated with the | ||
| - * given scroll pane. This is {@code null}-safe because the scroll pane | ||
| - * initializes its vertical {@link ScrollBar} upon construction. | ||
| - * | ||
| - * @param pane The scroll pane that contains a vertical {@link ScrollBar}. | ||
| - * @return The vertical {@link ScrollBar} associated with the scroll pane. | ||
| - * @throws IllegalStateException Could not obtain the vertical scroll bar. | ||
| - */ | ||
| - private ScrollBar getVerticalScrollBar( | ||
| - final VirtualizedScrollPane<StyleClassedTextArea> pane ) { | ||
| - | ||
| - for( final var node : pane.getChildrenUnmodifiable() ) { | ||
| - if( node instanceof final ScrollBar scrollBar && | ||
| - scrollBar.getOrientation() == VERTICAL ) { | ||
| - return scrollBar; | ||
| - } | ||
| - } | ||
| - | ||
| - throw new IllegalStateException( "No vertical scroll bar found." ); | ||
| - } | ||
| - | ||
| - private boolean isEnabled() { | ||
| - // TODO: As a minor optimization, when this is set to false, it could remove | ||
| - // the MouseHandler and ScrollHandler so that events only dispatch to one | ||
| - // object (instead of one per editor tab). | ||
| - return mEnabled.get() && !mLocked; | ||
| - } | ||
| - | ||
| - private VirtualizedScrollPane<StyleClassedTextArea> getEditorScrollPane() { | ||
| - return mEditorScrollPane; | ||
| - } | ||
| - | ||
| - private JScrollBar getPreviewScrollBar() { | ||
| - return mPreviewScrollBar; | ||
| - } | ||
| -} | ||
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| -package com.keenwrite; | ||
| - | ||
| -import com.keenwrite.editors.TextDefinition; | ||
| -import com.keenwrite.editors.TextEditor; | ||
| -import com.keenwrite.editors.definition.DefinitionTreeItem; | ||
| - | ||
| -import java.util.function.UnaryOperator; | ||
| - | ||
| -import static com.keenwrite.constants.Constants.*; | ||
| -import static com.keenwrite.events.StatusEvent.clue; | ||
| - | ||
| -/** | ||
| - * Provides the logic for injecting variable names within the editor. | ||
| - */ | ||
| -public final class VariableNameInjector { | ||
| - | ||
| - /** | ||
| - * Find a node that matches the current word and substitute the definition | ||
| - * reference. | ||
| - */ | ||
| - public static void autoinsert( | ||
| - final TextEditor editor, | ||
| - final TextDefinition definitions, | ||
| - final UnaryOperator<String> operator ) { | ||
| - assert editor != null; | ||
| - assert definitions != null; | ||
| - assert operator != null; | ||
| - | ||
| - try { | ||
| - if( definitions.isEmpty() ) { | ||
| - clue( STATUS_DEFINITION_EMPTY ); | ||
| - } | ||
| - else { | ||
| - final var indexes = editor.getCaretWord(); | ||
| - final var word = editor.getText( indexes ); | ||
| - | ||
| - if( word.isBlank() ) { | ||
| - clue( STATUS_DEFINITION_BLANK ); | ||
| - } | ||
| - else { | ||
| - final var leaf = findLeaf( definitions, word ); | ||
| - | ||
| - if( leaf == null ) { | ||
| - clue( STATUS_DEFINITION_MISSING, word ); | ||
| - } | ||
| - else { | ||
| - editor.replaceText( indexes, operator.apply( leaf.toPath() ) ); | ||
| - definitions.expand( leaf ); | ||
| - } | ||
| - } | ||
| - } | ||
| - } catch( final Exception ex ) { | ||
| - clue( STATUS_DEFINITION_BLANK, ex ); | ||
| - } | ||
| - } | ||
| - | ||
| - /** | ||
| - * Looks for the given word, matching first by exact, next by a starts-with | ||
| - * condition with diacritics replaced, then by containment. | ||
| - * | ||
| - * @param word Match the word by: exact, beginning, containment, or other. | ||
| - */ | ||
| - @SuppressWarnings( "ConstantConditions" ) | ||
| - private static DefinitionTreeItem<String> findLeaf( | ||
| - final TextDefinition definition, final String word ) { | ||
| - assert definition != null; | ||
| - assert word != null; | ||
| - | ||
| - DefinitionTreeItem<String> leaf = null; | ||
| - | ||
| - leaf = leaf == null ? definition.findLeafExact( word ) : leaf; | ||
| - leaf = leaf == null ? definition.findLeafStartsWith( word ) : leaf; | ||
| - leaf = leaf == null ? definition.findLeafContains( word ) : leaf; | ||
| - leaf = leaf == null ? definition.findLeafContainsNoCase( word ) : leaf; | ||
| - | ||
| - return leaf; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Prevent instantiation. | ||
| - */ | ||
| - private VariableNameInjector() {} | ||
| -} | ||