| | package com.keenwrite.preview; |
| | |
| | -import com.keenwrite.adapters.DocumentAdapter; |
| | -import javafx.beans.property.BooleanProperty; |
| | -import javafx.beans.property.SimpleBooleanProperty; |
| | import javafx.embed.swing.SwingNode; |
| | import javafx.scene.Node; |
| | -import org.jsoup.Jsoup; |
| | -import org.jsoup.helper.W3CDom; |
| | -import org.xhtmlrenderer.layout.SharedContext; |
| | import org.xhtmlrenderer.render.Box; |
| | -import org.xhtmlrenderer.simple.XHTMLPanel; |
| | -import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler; |
| | -import org.xhtmlrenderer.swing.*; |
| | +import org.xhtmlrenderer.swing.SwingReplacedElementFactory; |
| | |
| | import javax.swing.*; |
| | import java.awt.*; |
| | -import java.awt.event.ComponentAdapter; |
| | -import java.awt.event.ComponentEvent; |
| | -import java.net.URI; |
| | import java.nio.file.Path; |
| | |
| | import static com.keenwrite.Constants.STYLESHEET_PREVIEW; |
| | -import static com.keenwrite.StatusBarNotifier.clue; |
| | -import static com.keenwrite.util.ProtocolResolver.getProtocol; |
| | -import static java.awt.Desktop.Action.BROWSE; |
| | -import static java.awt.Desktop.getDesktop; |
| | import static java.lang.Math.max; |
| | import static java.lang.String.format; |
| | +import static javafx.scene.CacheHint.SPEED; |
| | import static javax.swing.SwingUtilities.invokeLater; |
| | -import static org.xhtmlrenderer.swing.ImageResourceLoader.NO_OP_REPAINT_LISTENER; |
| | |
| | /** |
| | - * Responsible for rendering an HTML document. |
| | + * Responsible for parsing an HTML document. |
| | */ |
| | public final class HtmlPreview extends SwingNode { |
| | - /** |
| | - * Suppresses scrolling to the top on every key press. |
| | - */ |
| | - private static class HtmlPanel extends XHTMLPanel { |
| | - @Override |
| | - public void resetScrollPosition() { |
| | - } |
| | - } |
| | - |
| | - /** |
| | - * Suppresses scroll attempts until after the document has loaded. |
| | - */ |
| | - private static final class DocumentEventHandler extends DocumentAdapter { |
| | - private final BooleanProperty mReadyProperty = new SimpleBooleanProperty(); |
| | - |
| | - public BooleanProperty readyProperty() { |
| | - return mReadyProperty; |
| | - } |
| | - |
| | - @Override |
| | - public void documentStarted() { |
| | - mReadyProperty.setValue( Boolean.FALSE ); |
| | - } |
| | - |
| | - @Override |
| | - public void documentLoaded() { |
| | - mReadyProperty.setValue( Boolean.TRUE ); |
| | - } |
| | - } |
| | - |
| | - /** |
| | - * Ensure that images are constrained to the panel width upon resizing. |
| | - */ |
| | - private final class ResizeListener extends ComponentAdapter { |
| | - @Override |
| | - public void componentResized( final ComponentEvent e ) { |
| | - setWidth( e ); |
| | - } |
| | - |
| | - @Override |
| | - public void componentShown( final ComponentEvent e ) { |
| | - setWidth( e ); |
| | - } |
| | - |
| | - /** |
| | - * Sets the width of the {@link HtmlPreview} so that images can be |
| | - * scaled to fit. The scale factor is adjusted a bit below the full width |
| | - * to prevent the horizontal scrollbar from appearing. |
| | - * |
| | - * @param event The component that defines the image scaling width. |
| | - */ |
| | - private void setWidth( final ComponentEvent event ) { |
| | - final int width = (int) (event.getComponent().getWidth() * .95); |
| | - HtmlPreview.this.mImageLoader.widthProperty().set( width ); |
| | - } |
| | - } |
| | - |
| | - /** |
| | - * Responsible for opening hyperlinks. External hyperlinks are opened in |
| | - * the system's default browser; local file system links are opened in the |
| | - * editor. |
| | - */ |
| | - private static class HyperlinkListener extends LinkListener { |
| | - @Override |
| | - public void linkClicked( final BasicPanel panel, final String link ) { |
| | - try { |
| | - switch( getProtocol( link ) ) { |
| | - case HTTP: |
| | - final var desktop = getDesktop(); |
| | - |
| | - if( desktop.isSupported( BROWSE ) ) { |
| | - desktop.browse( new URI( link ) ); |
| | - } |
| | - break; |
| | - case FILE: |
| | - // TODO: #88 -- publish a message to the event bus. |
| | - break; |
| | - } |
| | - } catch( final Exception ex ) { |
| | - clue( ex ); |
| | - } |
| | - } |
| | - } |
| | |
| | /** |
 |
| | */ |
| | private static final int HTML_PREFIX_LENGTH = HTML_HEAD_OPEN.length(); |
| | - |
| | - private static final W3CDom W3C_DOM = new W3CDom(); |
| | - private static final XhtmlNamespaceHandler NS_HANDLER = |
| | - new XhtmlNamespaceHandler(); |
| | |
| | /** |
| | * The buffer is reused so that previous memory allocations need not repeat. |
| | */ |
| | private final StringBuilder mHtmlDocument = new StringBuilder( 65536 ); |
| | |
| | - private final HtmlPanel mHtmlRenderer = new HtmlPanel(); |
| | - private final JScrollPane mScrollPane = new JScrollPane( mHtmlRenderer ); |
| | - private final CustomImageLoader mImageLoader = new CustomImageLoader(); |
| | + private HtmlPanel mView; |
| | |
| | private String mBaseUriPath = ""; |
 |
| | final var factory = new ChainedReplacedElementFactory(); |
| | factory.addFactory( new SvgReplacedElementFactory() ); |
| | - factory.addFactory( new SwingReplacedElementFactory( |
| | - NO_OP_REPAINT_LISTENER, mImageLoader ) ); |
| | - |
| | - final var context = getSharedContext(); |
| | - final var textRenderer = context.getTextRenderer(); |
| | - context.setReplacedElementFactory( factory ); |
| | - textRenderer.setSmoothingThreshold( 0 ); |
| | + factory.addFactory( new SwingReplacedElementFactory() ); |
| | |
| | - setContent( mScrollPane ); |
| | - mHtmlRenderer.addDocumentListener( new DocumentEventHandler() ); |
| | - mHtmlRenderer.addComponentListener( new ResizeListener() ); |
| | + invokeLater( () -> { |
| | + mView = new HtmlPanel(); |
| | + final var scrollPane = new JScrollPane( mView ); |
| | |
| | - // The default mouse click listener attempts navigation within the |
| | - // preview panel. We want to usurp that behaviour to open the link in |
| | - // a platform-specific browser. |
| | - for( final var listener : mHtmlRenderer.getMouseTrackingListeners() ) { |
| | - if( !(listener instanceof HoverListener) ) { |
| | - mHtmlRenderer.removeMouseTrackingListener( (FSMouseListener) listener ); |
| | - } |
| | - } |
| | + setContent( scrollPane ); |
| | + setCache( true ); |
| | + setCacheHint( SPEED ); |
| | |
| | - mHtmlRenderer.addMouseTrackingListener( new HyperlinkListener() ); |
| | + final var context = mView.getSharedContext(); |
| | + final var textRenderer = context.getTextRenderer(); |
| | + context.setReplacedElementFactory( factory ); |
| | + textRenderer.setSmoothingThreshold( 0 ); |
| | + } ); |
| | } |
| | |
| | /** |
| | * Updates the internal HTML source, loads it into the preview pane, then |
| | * scrolls to the caret position. |
| | * |
| | * @param html The new HTML document to display. |
| | */ |
| | public void process( final String html ) { |
| | - final var docJsoup = Jsoup.parse( decorate( html ) ); |
| | - final var docW3c = W3C_DOM.fromJsoup( docJsoup ); |
| | - |
| | // Access to a Swing component must occur from the Event Dispatch |
| | // Thread (EDT) according to Swing threading restrictions. |
| | - invokeLater( |
| | - () -> mHtmlRenderer.setDocument( docW3c, getBaseUri(), NS_HANDLER ) |
| | - ); |
| | + invokeLater( () -> mView.render( decorate( html ), getBaseUri() ) ); |
| | } |
| | |
 |
| | */ |
| | public void scrollTo( final String id ) { |
| | - scrollTo( getBoxById( id ) ); |
| | + scrollTo( mView.getBoxById( id ) ); |
| | repaintScrollPane(); |
| | } |
 |
| | |
| | private void scrollTo( final Point point ) { |
| | - mHtmlRenderer.scrollTo( point ); |
| | - } |
| | - |
| | - private Box getBoxById( final String id ) { |
| | - return getSharedContext().getBoxById( id ); |
| | + mView.scrollTo( point ); |
| | } |
| | |
| | private String decorate( final String html ) { |
| | // Trim the HTML back to only the prefix. |
| | mHtmlDocument.setLength( HTML_PREFIX_LENGTH ); |
| | |
| | // Write the HTML body element followed by closing tags. |
| | - return mHtmlDocument.append( HTML_HEAD_OPEN ) |
| | - .append( mBaseUriHtml ) |
| | + return mHtmlDocument.append( mBaseUriHtml ) |
| | .append( HTML_HEAD_CLOSE ) |
| | .append( html ) |
 |
| | private Point createPoint( final Box box ) { |
| | assert box != null; |
| | - |
| | - int x = box.getAbsX(); |
| | |
| | // Scroll back up by half the height of the scroll bar to keep the typing |
| | // area within the view port. Otherwise the view port will have jumped too |
| | // high up and the most recently typed letters won't be visible. |
| | - int y = max( |
| | - box.getAbsY() - (mScrollPane.getVerticalScrollBar().getHeight() / 2), |
| | - 0 ); |
| | + int y = max( box.getAbsY() - getVerticalScrollBarHeight() / 2, 0 ); |
| | + int x = box.getAbsX(); |
| | |
| | if( !box.getStyle().isInline() ) { |
| | - final var margin = box.getMargin( mHtmlRenderer.getLayoutContext() ); |
| | - x += margin.left(); |
| | + final var margin = box.getMargin( mView.getLayoutContext() ); |
| | y += margin.top(); |
| | + x += margin.left(); |
| | } |
| | |
 |
| | |
| | private JScrollPane getScrollPane() { |
| | - return mScrollPane; |
| | + return (JScrollPane) getContent(); |
| | } |
| | |
| | - private SharedContext getSharedContext() { |
| | - return mHtmlRenderer.getSharedContext(); |
| | + private int getVerticalScrollBarHeight() { |
| | + return getScrollPane().getVerticalScrollBar().getHeight(); |
| | } |
| | } |