Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git

Open links in browser, resize fenced code text, restyle inline code

AuthorDaveJarvis <email>
Date2020-06-27 13:18:09 GMT-0700
Commit5c3146c734e7b7df4194aed3011519b8c83bcf23
Parent09480c4
Delta88 lines added, 162 lines removed, 74-line decrease
src/main/resources/com/scrivenvar/preview/webview.css
/* Must be bundled in JAR file. */
font-family: "Fira Code", monospace;
- font-size: 14pt;
+ font-size: 10pt;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: anywhere;
}
code, tt {
- border: 1px solid #ccc;
background-color: #f8f8f8;
- border-radius: 3px;
+ min-width: 1em;
+ padding: .2em .3em;
+ text-align: center;
+ text-decoration: none;
+ border-radius: .3em;
+ border: none;
}
background-color: #f8f8f8;
border: .125em solid #ccc;
- line-height: 1.6;
padding: .25em .5em;
border-radius: .25em;
}
pre code, pre tt {
background-color: transparent;
border: none;
-}
-
-kbd {
- background-color: #ccc;
- background-repeat: repeat-x;
- border-color: #DDDDDD #CCCCCC #CCCCCC #DDDDDD;
- border-radius: 2px;
- border-style: solid;
- border-width: 1px;
- font-family: sans-serif;
- line-height: 1.25;
- padding: 1px 4px;
}
src/main/java/com/scrivenvar/preview/CustomImageResourceLoader.java
-/*
- * Copyright 2020 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.scrivenvar.preview;
-
-import javafx.beans.property.IntegerProperty;
-import javafx.beans.property.SimpleIntegerProperty;
-import org.xhtmlrenderer.extend.FSImage;
-import org.xhtmlrenderer.resource.ImageResource;
-import org.xhtmlrenderer.swing.ImageResourceLoader;
-
-import java.net.URI;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-
-import static com.scrivenvar.preview.SVGRasterizer.BROKEN_IMAGE_PLACEHOLDER;
-import static org.xhtmlrenderer.swing.AWTFSImage.createImage;
-
-/**
- * Responsible for loading images. If the image cannot be found, a placeholder
- * is used instead.
- */
-public class CustomImageResourceLoader extends ImageResourceLoader {
- /**
- * Placeholder that's displayed when image cannot be found.
- */
- private static final FSImage FS_PLACEHOLDER_IMAGE =
- createImage( BROKEN_IMAGE_PLACEHOLDER );
-
- private final IntegerProperty mMaxWidthProperty = new SimpleIntegerProperty();
-
- public CustomImageResourceLoader() {
- }
-
- public IntegerProperty widthProperty() {
- return mMaxWidthProperty;
- }
-
- @Override
- public synchronized ImageResource get(
- final String uri, final int width, final int height ) {
- assert uri != null;
- assert width >= 0;
- assert height >= 0;
-
- boolean exists;
-
- try {
- exists = Files.exists( Paths.get( new URI( uri ) ) );
- } catch( final Exception e ) {
- exists = false;
- }
-
- return exists
- ? scale( uri, width, height )
- : new ImageResource( uri, FS_PLACEHOLDER_IMAGE );
- }
-
- /**
- * Scales the image found at the given uri.
- *
- * @param uri Path to the image file to load.
- * @param w Unused (usually -1, which is useless).
- * @param h Unused (ditto).
- * @return Resource representing the rendered image and path.
- */
- private ImageResource scale( final String uri, final int w, final int h ) {
- final var ir = super.get( uri, w, h );
- final var image = ir.getImage();
- final var imageWidth = image.getWidth();
- final var imageHeight = image.getHeight();
-
- int maxWidth = mMaxWidthProperty.get();
- int newWidth = imageWidth;
- int newHeight = imageHeight;
-
- // Maintain aspect ratio while shrinking image to view port bounds.
- if( imageWidth > maxWidth ) {
- newWidth = maxWidth;
- newHeight = (newWidth * imageHeight) / imageWidth;
- }
-
- image.scale( newWidth, newHeight );
- return ir;
- }
-}
src/main/java/com/scrivenvar/preview/HTMLPreviewPane.java
package com.scrivenvar.preview;
+import com.scrivenvar.Services;
+import com.scrivenvar.service.events.Notifier;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import org.xhtmlrenderer.simple.XHTMLPanel;
import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler;
-import org.xhtmlrenderer.swing.SwingReplacedElementFactory;
-import org.xhtmlrenderer.util.Configuration;
+import org.xhtmlrenderer.swing.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.nio.file.Path;
import static com.scrivenvar.Constants.PARAGRAPH_ID_PREFIX;
import static com.scrivenvar.Constants.STYLESHEET_PREVIEW;
+import static java.awt.Desktop.Action.BROWSE;
+import static java.awt.Desktop.getDesktop;
import static org.xhtmlrenderer.swing.ImageResourceLoader.NO_OP_REPAINT_LISTENER;
/**
* HTML preview pane is responsible for rendering an HTML document.
*/
public final class HTMLPreviewPane extends Pane {
+ private final static Notifier NOTIFIER = Services.load( Notifier.class );
+
/**
- * Prevent scrolling to the top on every key press.
+ * Suppresses scrolling to the top on every key press.
*/
private static class HTMLPanel extends XHTMLPanel {
@Override
public void resetScrollPosition() {
}
}
/**
- * Prevent scroll attempts until after the document has loaded.
+ * Suppresses scroll attempts until after the document has loaded.
*/
private static final class DocumentEventHandler implements DocumentListener {
@Override
public void onRenderException( final Throwable t ) {
+ }
+ }
+
+ /**
+ * Responsible for ensuring that images are constrained to the panel width
+ * upon resizing.
+ */
+ private final class ResizeListener implements ComponentListener {
+ @Override
+ public void componentResized( final ComponentEvent e ) {
+ // Scaling a bit below the full width prevents the horizontal scrollbar
+ // from appearing.
+ final int width = (int) (e.getComponent().getWidth() * .95);
+ HTMLPreviewPane.this.mImageLoader.widthProperty().set( width );
+ }
+
+ @Override
+ public void componentMoved( final ComponentEvent e ) {
+ }
+
+ @Override
+ public void componentShown( final ComponentEvent e ) {
+ }
+
+ @Override
+ public void componentHidden( final ComponentEvent e ) {
+ }
+ }
+
+ /**
+ * Responsible for launching hyperlinks in the system's default browser.
+ */
+ private static class HyperlinkListener extends LinkListener {
+ @Override
+ public void linkClicked( final BasicPanel panel, final String uri ) {
+ try {
+ final var desktop = getDesktop();
+
+ if( desktop.isSupported( BROWSE ) ) {
+ desktop.browse( new URI( uri ) );
+ }
+ } catch( final Exception e ) {
+ NOTIFIER.notify( e );
+ }
}
}
+ "<body>";
private final static String HTML_FOOTER = "</body></html>";
+
+ private final static W3CDom W3C_DOM = new W3CDom();
+ private final static XhtmlNamespaceHandler NS_HANDLER =
+ new XhtmlNamespaceHandler();
private final StringBuilder mHtmlDocument = new StringBuilder( 65536 );
private final int mHtmlPrefixLength;
- private final W3CDom mW3cDom = new W3CDom();
- private final XhtmlNamespaceHandler mNamespaceHandler =
- new XhtmlNamespaceHandler();
private final HTMLPanel mHtmlRenderer = new HTMLPanel();
private final SwingNode mSwingNode = new SwingNode();
private final JScrollPane mScrollPane = new JScrollPane( mHtmlRenderer );
- private final DocumentEventHandler mDocumentHandler =
- new DocumentEventHandler();
- private final CustomImageResourceLoader mImageLoader =
- new CustomImageResourceLoader();
+ private final DocumentEventHandler mDocHandler = new DocumentEventHandler();
+ private final CustomImageLoader mImageLoader = new CustomImageLoader();
private Path mPath;
/**
* Creates a new preview pane that can scroll to the caret position within the
* document.
*/
public HTMLPreviewPane() {
+ setStyle( "-fx-background-color: white;" );
+
+ // No need to append the header each time the HTML content is updated.
mHtmlDocument.append( HTML_HEADER );
mHtmlPrefixLength = mHtmlDocument.length();
+ // Inject an SVG renderer that produces high-quality SVG buffered images.
final var factory = new ChainedReplacedElementFactory();
factory.addFactory( new SVGReplacedElementFactory() );
factory.addFactory( new SwingReplacedElementFactory(
NO_OP_REPAINT_LISTENER, mImageLoader ) );
+ // Ensure fonts are always anti-aliased.
final var context = getSharedContext();
context.setReplacedElementFactory( factory );
context.getTextRenderer().setSmoothingThreshold( 0 );
mSwingNode.setContent( mScrollPane );
mSwingNode.setCache( true );
-
- mHtmlRenderer.addDocumentListener( mDocumentHandler );
- setStyle( "-fx-background-color: white;" );
-
- mHtmlRenderer.addComponentListener( new ComponentListener() {
- @Override
- public void componentResized( final ComponentEvent e ) {
- // Scaling a bit below the full width prevents the horizontal scrollbar
- // from appearing.
- final int width = (int) (e.getComponent().getWidth() * .95);
- mImageLoader.widthProperty().set( width );
- }
- @Override
- public void componentMoved( final ComponentEvent e ) {
- }
+ mHtmlRenderer.addDocumentListener( mDocHandler );
+ mHtmlRenderer.addComponentListener( new ResizeListener() );
- @Override
- public void componentShown( final ComponentEvent e ) {
+ // 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 );
}
+ }
- @Override
- public void componentHidden( final ComponentEvent e ) {
- }
- } );
+ mHtmlRenderer.addMouseTrackingListener( new HyperlinkListener() );
}
public void update( final String html ) {
final Document jsoupDoc = Jsoup.parse( decorate( html ) );
- final org.w3c.dom.Document w3cDoc = mW3cDom.fromJsoup( jsoupDoc );
+ final org.w3c.dom.Document w3cDoc = W3C_DOM.fromJsoup( jsoupDoc );
- mHtmlRenderer.setDocument( w3cDoc, getBaseUrl(), mNamespaceHandler );
+ mHtmlRenderer.setDocument( w3cDoc, getBaseUrl(), NS_HANDLER );
}
scrollTo( id );
- mDocumentHandler.readyProperty().removeListener( this );
+ mDocHandler.readyProperty().removeListener( this );
}
}
};
- mDocumentHandler.readyProperty().addListener( listener );
+ mDocHandler.readyProperty().addListener( listener );
}