From 2a8e472c5c0d9dc0f8549927188a2a2f8f10a005 Mon Sep 17 00:00:00 2001 From: Patrick Corless Date: Fri, 11 Oct 2024 21:07:38 -0600 Subject: [PATCH 1/7] GH-372 update maven builds for qa/jfx and bring over broken jfx 8 code. --- examples/javafx/pom.xml | 2 +- pom.xml | 1 + qa/pom.xml | 77 + qa/viewer-jfx/build.gradle | 8 +- qa/viewer-jfx/pom.xml | 88 + settings.gradle | 1 + viewer/pom.xml | 2 +- viewer/viewer-awt/build.gradle | 4 +- viewer/viewer-fx/build.gradle | 123 + viewer/viewer-fx/pom.xml | 52 + .../core/util/FontPropertiesManager.java | 91 + .../icepdf/core/util/PropertiesManager.java | 26 + .../icepdf/fx/scene/control/DocumentView.java | 158 + .../com/icepdf/fx/scene/control/PageView.java | 130 + .../behavior/DocumentViewBehavior.java | 28 + .../scene/control/skin/DocumentViewSkin.java | 269 ++ .../scene/control/skin/PageCaptureTask.java | 100 + .../fx/scene/control/skin/PageViewSkin.java | 319 ++ .../control/skin/VirtualContainerBase.java | 94 + .../scene/control/skin/VirtualPageFlow.java | 2852 +++++++++++++++++ .../scene/control/skin/VirtualScrollBar.java | 92 + .../java/com/icepdf/fx/util/ImageLoader.java | 14 + .../icepdf/fx/util/SettingsLoaderTask.java | 43 + .../icepdf/fx/viewer/DocumentViewTest.java | 32 + .../java/com/icepdf/fx/viewer/Launcher.java | 208 ++ .../com/icepdf/fx/viewer/ListViewTest.java | 36 + .../main/java/com/icepdf/fx/viewer/Main.java | 256 ++ 27 files changed, 5098 insertions(+), 8 deletions(-) create mode 100644 qa/pom.xml create mode 100644 qa/viewer-jfx/pom.xml create mode 100644 viewer/viewer-fx/build.gradle create mode 100644 viewer/viewer-fx/pom.xml create mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/core/util/FontPropertiesManager.java create mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/core/util/PropertiesManager.java create mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/DocumentView.java create mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/PageView.java create mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/behavior/DocumentViewBehavior.java create mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/DocumentViewSkin.java create mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/PageCaptureTask.java create mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/PageViewSkin.java create mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualContainerBase.java create mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualPageFlow.java create mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualScrollBar.java create mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/util/ImageLoader.java create mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/util/SettingsLoaderTask.java create mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/DocumentViewTest.java create mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/Launcher.java create mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/ListViewTest.java create mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/Main.java diff --git a/examples/javafx/pom.xml b/examples/javafx/pom.xml index 2a39659da..8dc08cac4 100644 --- a/examples/javafx/pom.xml +++ b/examples/javafx/pom.xml @@ -33,7 +33,7 @@ org.openjfx javafx-maven-plugin - 0.0.4 + 0.0.8 PdfFXViewer diff --git a/pom.xml b/pom.xml index 4f96f0d3e..580aa93ff 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,7 @@ core viewer examples + qa diff --git a/qa/pom.xml b/qa/pom.xml new file mode 100644 index 000000000..e06210056 --- /dev/null +++ b/qa/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + com.github.pcorless.icepdf + icepdf + 7.3.0-SNAPSHOT + + qa + pom + ICEpdf :: QA + + The ICEpdf common QA rendering compare. + + + + viewer-jfx + + + + + + com.fasterxml.jackson.core + jackson-core + 2.8.6 + + + com.fasterxml.jackson.core + jackson-annotations + 2.8.6 + + + com.fasterxml.jackson.core + jackson-databind + 2.12.7.1 + + + org.apache.commons + commons-io + 1.3.2 + + + + org.bouncycastle + bcprov-jdk18on + ${bouncy.version} + + + + org.bouncycastle + bcpkix-jdk18on + ${bouncy.version} + + + + com.twelvemonkeys.imageio + imageio-tiff + ${twelve-monkey.version} + true + + + org.apache.pdfbox + jbig2-imageio + ${jbig2.version} + true + + + com.github.jai-imageio + jai-imageio-jpeg2000 + ${jai-imageio.version} + provided + true + + + + + diff --git a/qa/viewer-jfx/build.gradle b/qa/viewer-jfx/build.gradle index 8af9a0502..735ea93c5 100644 --- a/qa/viewer-jfx/build.gradle +++ b/qa/viewer-jfx/build.gradle @@ -1,6 +1,6 @@ plugins { id 'application' - id 'org.openjfx.javafxplugin' version '0.0.9' + id 'org.openjfx.javafxplugin' version '0.1.0' } repositories { @@ -53,9 +53,9 @@ javafx { dependencies { // jackson json. - implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.8.6' - implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.8.6' - implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.12.7.1' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.17.2' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.17.2' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.17.0' implementation group: 'org.apache.commons', name: 'commons-io', version: '1.3.2' implementation group: 'junit', name: 'junit', version: '4.13.1' // signature validation. diff --git a/qa/viewer-jfx/pom.xml b/qa/viewer-jfx/pom.xml new file mode 100644 index 000000000..352e19a4d --- /dev/null +++ b/qa/viewer-jfx/pom.xml @@ -0,0 +1,88 @@ + + + + com.github.pcorless.icepdf + qa + 7.3.0-SNAPSHOT + + 4.0.0 + qa-viewer + jar + ICEpdf :: QA :: Viewer + + + + org.openjfx + javafx-controls + 11 + + + + com.fasterxml.jackson.core + jackson-core + 2.17.2 + + + com.fasterxml.jackson.core + jackson-annotations + 2.17.2 + + + com.fasterxml.jackson.core + jackson-databind + 2.17.0 + + + commons-io + commons-io + 2.14.0 + + + + org.bouncycastle + bcprov-jdk18on + ${bouncy.version} + + + + org.bouncycastle + bcpkix-jdk18on + ${bouncy.version} + + + + com.twelvemonkeys.imageio + imageio-tiff + ${twelve-monkey.version} + + + org.apache.pdfbox + jbig2-imageio + ${jbig2.version} + + + com.github.jai-imageio + jai-imageio-jpeg2000 + ${jai-imageio.version} + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.3 + + + org.openjfx + javafx-maven-plugin + 0.0.8 + + org.icepdf.qa.viewer.Launcher + + + + + + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 5f2d39909..44c30f4e5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,6 @@ include 'core:core-awt', 'viewer:viewer-awt', + 'viewer:viewer-fx', 'qa:viewer-jfx', 'examples:annotation:callback', 'examples:annotation:creation', diff --git a/viewer/pom.xml b/viewer/pom.xml index fca030862..2c99f8786 100644 --- a/viewer/pom.xml +++ b/viewer/pom.xml @@ -16,7 +16,7 @@ viewer-awt - + viewer-fx diff --git a/viewer/viewer-awt/build.gradle b/viewer/viewer-awt/build.gradle index 855c5a589..206f42a91 100644 --- a/viewer/viewer-awt/build.gradle +++ b/viewer/viewer-awt/build.gradle @@ -3,7 +3,7 @@ plugins { id 'application' } -description 'ICEpdf viewer reference implementation project' +description 'ICEpdf AWT viewer reference implementation project' mainClassName = "org.icepdf.ri.viewer.Launcher" applicationDefaultJvmArgs = ["-Xms64m", "-Xmx1024m"] @@ -36,7 +36,7 @@ publishing { artifactId 'icepdf-viewer' version "${VERSION + (RELEASE_TYPE?.trim() ? '-' + RELEASE_TYPE : '')}" pom.withXml { - asNode().appendNode('description', 'ICEpdf core rendering library.') + asNode().appendNode('description', 'ICEpdf AWT viewer reference implementation.') asNode().appendNode('url', 'https://github.com/pcorless/icepdf') asNode().appendNode('scm') .appendNode('connection', 'scm:git:https://github.com/pcorless/icepdf').parent() diff --git a/viewer/viewer-fx/build.gradle b/viewer/viewer-fx/build.gradle new file mode 100644 index 000000000..5e3bdbacd --- /dev/null +++ b/viewer/viewer-fx/build.gradle @@ -0,0 +1,123 @@ +plugins { + id 'application' + id 'org.openjfx.javafxplugin' version '0.1.0' +} + +repositories { + mavenCentral() +} + +description 'ICEpdf FX viewer reference implementation project' + +mainClassName = "org.icepdf.fx.viewer.Main" +applicationDefaultJvmArgs = ["-Xms64m", "-Xmx1024m"] + +def sectionName = 'com/icepdf/fx/viewer' +def baseJarName = 'icepdf' +def baseAppendixName = 'viewer-fx' + +repositories { + mavenCentral() + jcenter() +} + +javafx { + version = "11.0.2" + modules = [ 'javafx.base', 'javafx.controls', 'javafx.graphics', 'javafx.swing' ] +} + + +dependencies { + implementation project(':core:core-awt') + // signature validation. + implementation 'org.bouncycastle:bcprov-jdk18on:' + "${BOUNCY_VERSION}" + implementation 'org.bouncycastle:bcpkix-jdk18on:' + "${BOUNCY_VERSION}" + // tests + testImplementation(platform("org.junit:junit-bom:${JUNIT_BOM_VERSION}")) + testImplementation('org.junit.jupiter:junit-jupiter') +} + +// generatePomFileForViewerJarPublication +publishing { + publications { + viewerJar(MavenPublication) { + from components.java + afterEvaluate { + groupId 'org.icepdf.os' + artifactId 'icepdf-viewer-fx' + version "${VERSION + (RELEASE_TYPE?.trim() ? '-' + RELEASE_TYPE : '')}" + pom.withXml { + asNode().appendNode('description', 'ICEpdf JX Viewer reference implementation.') + asNode().appendNode('url', 'https://github.com/pcorless/icepdf') + asNode().appendNode('scm') + .appendNode('connection', 'scm:git:https://github.com/pcorless/icepdf').parent() + .appendNode('url', 'https://www.apache.org/licenses/LICENSE-2.0.txt').parent() + .appendNode('tag', 'icepdf-' + version + '-maven') + asNode().appendNode('licenses').appendNode('license') + .appendNode('name', 'Apache License, Version 2.0').parent() + .appendNode('url', 'https://www.apache.org/licenses/LICENSE-2.0.html').parent() + .appendNode('distribution', 'repo') + } + artifact sourcesJar + artifact javadocJar + } + } + } +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } +} + +jar { + archiveBaseName.set('icepdf') + archiveAppendix.set("viewer-fx") + archiveVersion.set("${VERSION}") + archiveClassifier.set("${RELEASE_TYPE}") + + doFirst { + manifest { + attributes ('Created-By': System.getProperty('java.version') + ' (' + System.getProperty('java.vendor') + ')') + // executable jar + attributes("Main-Class": 'org.icepdf.ri.viewer.Launcher') + if (!configurations.runtimeClasspath.isEmpty()) { + attributes('Class-Path': configurations.runtimeClasspath.files.collect{it.name}.join(' ')) + } + } + } + + manifest { + // section names attributes + attributes("Implementation-Title": "${archiveBaseName.get() + '-' + archiveAppendix.get()}", "${sectionName}") + attributes("Implementation-Version": "${VERSION + (RELEASE_TYPE?.trim()? '-' + RELEASE_TYPE:'')}", "${sectionName}") + attributes("Implementation-Vendor": "${COMPANY}", "${sectionName}") + } +} + +task sourcesJar(type: Jar, dependsOn: classes) { + description = 'Assembles a jar archive containing the main classes source code.' + group = 'Documentation' + archiveBaseName.set("${baseJarName}") + archiveAppendix.set("${baseAppendixName}") + archiveVersion.set("${VERSION}") + archiveClassifier.set("sources") + manifest { + attributes("Implementation-Title": "${archiveBaseName.get() + '-' + archiveAppendix.get()}", "${sectionName}") + attributes("Implementation-Version": "${VERSION + (RELEASE_TYPE?.trim()? '-' + RELEASE_TYPE:'')}", "${sectionName}") + attributes("Implementation-Vendor": "${COMPANY}", "${sectionName}") + } + from sourceSets.main.allSource +} + +task javadocJar(type: Jar, dependsOn: 'javadoc') { + from javadoc + archiveClassifier.set("javadoc") +} + +artifacts { + archives sourcesJar + archives javadocJar +} diff --git a/viewer/viewer-fx/pom.xml b/viewer/viewer-fx/pom.xml new file mode 100644 index 000000000..80b1b4340 --- /dev/null +++ b/viewer/viewer-fx/pom.xml @@ -0,0 +1,52 @@ + + + + com.github.pcorless.icepdf + viewer + 7.3.0-SNAPSHOT + + 4.0.0 + icepdf-viewer-fx + jar + ICEpdf :: Viewer : JavaFX Viewer RI + + ICEpdf JavaFX reference implementation. + + + + + org.openjfx + javafx-controls + 11 + + + com.github.pcorless.icepdf + icepdf-core + + + org.junit.jupiter + junit-jupiter + 5.9.3 + test + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.3 + + + org.openjfx + javafx-maven-plugin + 0.0.8 + + org.icepdf.qa.viewer.Launcher + + + + + + \ No newline at end of file diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/core/util/FontPropertiesManager.java b/viewer/viewer-fx/src/main/java/com/icepdf/core/util/FontPropertiesManager.java new file mode 100644 index 000000000..ef9bce157 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/com/icepdf/core/util/FontPropertiesManager.java @@ -0,0 +1,91 @@ +package com.icepdf.core.util; + +import org.icepdf.core.pobjects.fonts.FontManager; + +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.prefs.BackingStoreException; +import java.util.prefs.Preferences; + +/** + * \ + */ +public class FontPropertiesManager { + + private static final Logger logger = Logger.getLogger(FontPropertiesManager.class.toString()); + + private static Preferences prefs = Preferences.userNodeForPackage(FontPropertiesManager.class); + + private static FontPropertiesManager fontPropertiesManager; + + private static FontManager fontManager = FontManager.getInstance(); + + private FontPropertiesManager() { + + } + + public static FontPropertiesManager getInstance() { + if (fontPropertiesManager == null) { + fontPropertiesManager = new FontPropertiesManager(); + } + return fontPropertiesManager; + } + + public void readDefaultProperties(String... paths) { + try { + fontManager.readSystemFonts(paths); + } catch (Exception e) { + if (logger.isLoggable(Level.WARNING)) { + logger.log(Level.WARNING, "Error reading system fonts path: ", e); + } + } + } + + public void readFontProperties(String... paths) { + try { + // If you application needs to look at other font directories + // they can be added via the readSystemFonts method. + fontManager.readFonts(paths); + } catch (Exception e) { + if (logger.isLoggable(Level.WARNING)) { + logger.log(Level.WARNING, "Error reading system paths:", e); + } + } + } + + public void loadProperties() { + fontManager.setFontProperties(prefs); + } + + public void clearProperties() { + try { + prefs.clear(); + } catch (BackingStoreException e) { + if (logger.isLoggable(Level.WARNING)) { + logger.log(Level.WARNING, "Error reading system paths:", e); + } + } + } + + public void updateProperties() { + Properties fontProps = fontManager.getFontProperties(); + for (Object key : fontProps.keySet()) { + prefs.put((String) key, fontProps.getProperty((String) key)); + } + } + + public boolean isPropertiesEmpty() { + try { + return prefs.keys().length == 0; + } catch (BackingStoreException e) { + e.printStackTrace(); + } + return false; + } + + + public static FontManager getFontManager() { + return FontManager.getInstance(); + } +} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/core/util/PropertiesManager.java b/viewer/viewer-fx/src/main/java/com/icepdf/core/util/PropertiesManager.java new file mode 100644 index 000000000..e7d13d15d --- /dev/null +++ b/viewer/viewer-fx/src/main/java/com/icepdf/core/util/PropertiesManager.java @@ -0,0 +1,26 @@ +package com.icepdf.core.util; + +import java.util.logging.Logger; + +/** + * + */ +public class PropertiesManager { + + private static final Logger logger = Logger.getLogger(PropertiesManager.class.toString()); + + public static final String DEFAULT_MESSAGE_BUNDLE = "com.icepdf.fx.resources.MessageBundle"; + public static final String DEFAULT_SPLASH_CSS = "/com/icepdf/fx/css/splash.css"; + + private static PropertiesManager propertiesManager; + + private PropertiesManager() { + } + + public static PropertiesManager getInstance() { + if (propertiesManager == null) { + propertiesManager = new PropertiesManager(); + } + return propertiesManager; + } +} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/DocumentView.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/DocumentView.java new file mode 100644 index 000000000..b6a729e22 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/DocumentView.java @@ -0,0 +1,158 @@ +package com.icepdf.fx.scene.control; + +import com.icepdf.fx.scene.control.skin.DocumentViewSkin; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.control.Control; +import javafx.scene.control.ScrollBar; +import javafx.scene.control.Skin; +import javafx.scene.paint.Color; +import org.icepdf.core.pobjects.Document; + +/** + * + */ +public class DocumentView extends Control { + + private enum PageViewMode {SINGLE_PAGE, SINGLE_COLUMN, DOUBLE_PAGE, DOUBLE_COLUMN} + + ; + + private DoubleProperty scaleIncrementValue; + private DoubleProperty scaleMaxValue; + private DoubleProperty scaleMinValue; + + private int currentPageIndex; + + private DoubleProperty scale; + private DoubleProperty rotation; + private ObjectProperty document; + + // viewport + private ScrollBar hbar; + + private ObjectProperty backgroundFill; + + private int numberOfPages; + + // todo Document insertion. + public DocumentView(Document document) { + this.numberOfPages = document.getNumberOfPages(); + this.document = new SimpleObjectProperty<>(document); + scale = new SimpleDoubleProperty(1); + rotation = new SimpleDoubleProperty(0); + backgroundFill = new SimpleObjectProperty<>(Color.LIGHTGRAY); + + // mouse wheel and touch zoom level + scaleIncrementValue = new SimpleDoubleProperty(0.05); + scaleMaxValue = new SimpleDoubleProperty(8); + scaleMinValue = new SimpleDoubleProperty(0.05); + } + + @Override + protected Skin createDefaultSkin() { + return new DocumentViewSkin(this); + } + + public double getScale() { + return scale.get(); + } + + public DoubleProperty scaleProperty() { + return scale; + } + + public void setScale(double scale) { + this.scale.set(scale); + } + + public double getRotation() { + return rotation.get(); + } + + public DoubleProperty rotationProperty() { + return rotation; + } + + public void setRotation(double rotation) { + this.rotation.set(rotation); + } + + public Color getBackgroundFill() { + return backgroundFill.get(); + } + + public ObjectProperty backgroundFillProperty() { + return backgroundFill; + } + + public void setBackgroundFill(Color backgroundFill) { + this.backgroundFill.set(backgroundFill); + } + + public int getNumberOfPages() { + return numberOfPages; + } + + public void setNumberOfPages(int numberOfPages) { + this.numberOfPages = numberOfPages; + } + + public double getScaleIncrementValue() { + return scaleIncrementValue.get(); + } + + public DoubleProperty scaleIncrementValueProperty() { + return scaleIncrementValue; + } + + public void setScaleIncrementValue(double scaleIncrementValue) { + this.scaleIncrementValue.set(scaleIncrementValue); + } + + public double getScaleMaxValue() { + return scaleMaxValue.get(); + } + + public DoubleProperty scaleMaxValueProperty() { + return scaleMaxValue; + } + + public void setScaleMaxValue(double scaleMaxValue) { + this.scaleMaxValue.set(scaleMaxValue); + } + + public double getScaleMinValue() { + return scaleMinValue.get(); + } + + public DoubleProperty scaleMinValueProperty() { + return scaleMinValue; + } + + public void setScaleMinValue(double scaleMinValue) { + this.scaleMinValue.set(scaleMinValue); + } + + public Document getDocument() { + return document.get(); + } + + public ObjectProperty documentProperty() { + return document; + } + + public void setDocument(Document document) { + this.document.set(document); + } + + public ScrollBar getHbar() { + return hbar; + } + + public void setHbar(ScrollBar hbar) { + this.hbar = hbar; + } +} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/PageView.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/PageView.java new file mode 100644 index 000000000..4bef5ba60 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/PageView.java @@ -0,0 +1,130 @@ +package com.icepdf.fx.scene.control; + +import com.icepdf.fx.scene.control.skin.PageViewSkin; +import javafx.beans.property.*; +import javafx.geometry.BoundingBox; +import javafx.geometry.Bounds; +import javafx.scene.control.Control; +import javafx.scene.control.Skin; +import javafx.scene.paint.Color; + +import java.util.Random; + +/** + * + */ +public class PageView extends Control { + + private DoubleProperty scale; + private DoubleProperty rotation; + private ObjectProperty documentView; + private ObjectProperty clipBounds; + private ObjectProperty pageBounds; + + private ReadOnlyIntegerWrapper index = new ReadOnlyIntegerWrapper(this, "index", -1); + + private ObjectProperty backgroundFill; + + private static Random random = new Random(); + + public PageView() { + scale = new SimpleDoubleProperty(1); + rotation = new SimpleDoubleProperty(0); + documentView = new SimpleObjectProperty<>(); + backgroundFill = new SimpleObjectProperty<>(Color.WHITE); + clipBounds = new SimpleObjectProperty<>(new BoundingBox(0, 0, 0, 0)); + pageBounds = new SimpleObjectProperty<>(new BoundingBox(0, 0, 0, 0)); + + } + + + @Override + protected Skin createDefaultSkin() { + return new PageViewSkin(this); + } + + public double getScale() { + return scale.get(); + } + + public DoubleProperty scaleProperty() { + return scale; + } + + public void setScale(double scale) { + this.scale.set(scale); + } + + public void setScale(float scale) { + this.scale.set(scale); + } + + public double getRotation() { + return rotation.get(); + } + + public DoubleProperty rotationProperty() { + return rotation; + } + + public void setRotation(double rotation) { + this.rotation.set(rotation); + } + + + public Color getBackgroundFill() { + return backgroundFill.get(); + } + + public ObjectProperty backgroundFillProperty() { + return backgroundFill; + } + + public void setBackgroundFill(Color backgroundFill) { + this.backgroundFill.set(backgroundFill); + } + + + public Bounds getClipBounds() { + return clipBounds.get(); + } + + public ObjectProperty clipBoundsProperty() { + return clipBounds; + } + + public Bounds getPageBounds() { + return pageBounds.get(); + } + + public ObjectProperty pageBoundsProperty() { + return pageBounds; + } + + public final int getIndex() { + return index.get(); + } + + public ReadOnlyIntegerWrapper indexProperty() { + return index; + } + + public void updateIndex(int i) { + final int oldIndex = index.get(); + index.set(i); +// indexChanged(oldIndex, i); + } + + public DocumentView getDocumentView() { + return documentView.get(); + } + + + public ObjectProperty documentViewProperty() { + return documentView; + } + + public void setDocumentView(DocumentView documentView) { + this.documentView.set(documentView); + } +} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/behavior/DocumentViewBehavior.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/behavior/DocumentViewBehavior.java new file mode 100644 index 000000000..25a10400f --- /dev/null +++ b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/behavior/DocumentViewBehavior.java @@ -0,0 +1,28 @@ +package com.icepdf.fx.scene.control.behavior; + +import com.icepdf.fx.scene.control.DocumentView; +import com.sun.javafx.scene.control.behavior.BehaviorBase; +import com.sun.javafx.scene.control.behavior.KeyBinding; + +import java.util.ArrayList; +import java.util.List; + +import static javafx.scene.input.KeyCode.PAGE_DOWN; +import static javafx.scene.input.KeyCode.PAGE_UP; + +/** + * + */ +public class DocumentViewBehavior extends BehaviorBase { + + protected static final List DOCUMENT_VIEW_BINDINGS = new ArrayList(); + + static { + DOCUMENT_VIEW_BINDINGS.add(new KeyBinding(PAGE_UP, "ScrollUp")); + DOCUMENT_VIEW_BINDINGS.add(new KeyBinding(PAGE_DOWN, "ScrollDown")); + } + + public DocumentViewBehavior(DocumentView control) { + super(control, DOCUMENT_VIEW_BINDINGS); + } +} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/DocumentViewSkin.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/DocumentViewSkin.java new file mode 100644 index 000000000..d06981cdb --- /dev/null +++ b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/DocumentViewSkin.java @@ -0,0 +1,269 @@ +package com.icepdf.fx.scene.control.skin; + +import com.icepdf.fx.scene.control.DocumentView; +import com.icepdf.fx.scene.control.PageView; +import com.icepdf.fx.scene.control.behavior.DocumentViewBehavior; +import javafx.geometry.Dimension2D; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.effect.DropShadow; +import javafx.scene.image.Image; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import org.icepdf.core.pobjects.Document; +import org.icepdf.core.pobjects.PDimension; + +import java.lang.ref.SoftReference; +import java.util.HashMap; + +/** + * + */ +public class DocumentViewSkin extends VirtualContainerBase { + + private HashMap> imageCaptureCache = new HashMap<>(25); + + private StackPane placeholderRegion; + private Node placeholderNode; + + private int itemCount = -1; + + private boolean needCellsRebuilt = true; + private boolean needCellsReconfigured = false; + + private Dimension2D defaultDimension; + + private DropShadow dropShadow; + + public DocumentViewSkin(DocumentView documentView) { + super(documentView, new DocumentViewBehavior(documentView)); + + updateListViewItems(); + + flow.setId("virtual-flow"); + flow.setPannable(true); + flow.setVertical(true); + flow.setCreateCell(flow1 -> DocumentViewSkin.this.createCell()); + getChildren().add(flow); + + updatePageCount(); + + // page shadow reuse. + dropShadow = new DropShadow(); + dropShadow.setRadius(5.0); + dropShadow.setOffsetX(3.0); + dropShadow.setOffsetY(3.0); + dropShadow.setColor(Color.color(0.4, 0.5, 0.5)); + + + if (getSkinnable().getNumberOfPages() > 0) { + // find page size so we can apply a default size. + Document document = getSkinnable().getDocument(); + PDimension dim = document.getPageDimension(0, (float) getSkinnable().getRotation(), + (float) getSkinnable().getScale()); + defaultDimension = new Dimension2D(dim.getWidth(), dim.getHeight()); + } +// flow.setOnScroll(t -> { +// // todo centering point +// double x = t.getX(); +// double y = t.getY(); +// if (t.isControlDown()) { +// if (t.getDeltaY() > 0) { +// incrementScale(); +// } else { +// decrementScale(); +// } +// t.consume(); +// } +// }); + + // init the behavior 'closures' +// getBehavior().setOnFocusPreviousRow(() -> { onFocusPreviousCell(); }); +// getBehavior().setOnFocusNextRow(() -> { onFocusNextCell(); }); +// getBehavior().setOnMoveToFirstCell(() -> { onMoveToFirstCell(); }); +// getBehavior().setOnMoveToLastCell(() -> { onMoveToLastCell(); }); +// getBehavior().setOnScrollPageDown(isFocusDriven -> onScrollPageDown(isFocusDriven)); +// getBehavior().setOnScrollPageUp(isFocusDriven -> onScrollPageUp(isFocusDriven)); +// getBehavior().setOnSelectPreviousRow(() -> { onSelectPreviousCell(); }); +// getBehavior().setOnSelectNextRow(() -> { onSelectNextCell(); }); + +// documentView.itemsProperty().addListener(new WeakInvalidationListener(itemsChangeListener)); + } + + private void incrementScale() { + DocumentView documentView = getSkinnable(); + double scale = documentView.getScale() + documentView.getScaleIncrementValue(); + if (scale <= documentView.getScaleMaxValue()) { + documentView.setScale(scale); + } + } + + private void decrementScale() { + DocumentView documentView = getSkinnable(); + double scale = documentView.getScale() - documentView.getScaleIncrementValue(); + if (scale >= documentView.getScaleMinValue()) { + documentView.setScale(scale); + } + } + + @Override + protected void handleControlPropertyChanged(String p) { + super.handleControlPropertyChanged(p); + if ("ITEMS".equals(p)) { + updateListViewItems(); + } else if ("ORIENTATION".equals(p)) { +// flow.setVertical(getSkinnable().getOrientation() == Orientation.VERTICAL); + } else if ("CELL_FACTORY".equals(p)) { + flow.recreateCells(); + } else if ("PARENT".equals(p)) { + if (getSkinnable().getParent() != null && getSkinnable().isVisible()) { + getSkinnable().requestLayout(); + } + } else if ("PLACEHOLDER".equals(p)) { + updatePlaceholderRegionVisibility(); + } +// else if ("FIXED_CELL_SIZE".equals(p)) { +// flow.setFixedCellSize(getSkinnable().getFixedCellSize()); +// } + } +// +// private MapChangeListener propertiesMapListener = c -> { +// if (! c.wasAdded()) return; +// if (RECREATE.equals(c.getKey())) { +// needCellsRebuilt = true; +// getSkinnable().requestLayout(); +// getSkinnable().getProperties().remove(RECREATE); +// } +// }; + + + public void updateListViewItems() { + + pageCountDirty = true; + getSkinnable().requestLayout(); + } + + @Override + public int getPageCount() { + return itemCount; + } + + @Override + protected void updatePageCount() { + if (flow == null) return; + + Document document = getSkinnable().getDocument(); + + int oldCount = itemCount; + int newCount = document == null ? 0 : document.getNumberOfPages(); + + itemCount = newCount; + + flow.setCellCount(newCount); + + getSkinnable().setHbar(flow.getHbar()); + + updatePlaceholderRegionVisibility(); + if (newCount != oldCount) { + needCellsRebuilt = true; + } else { + needCellsReconfigured = true; + } + } + + protected final void updatePlaceholderRegionVisibility() { + boolean visible = getPageCount() == 0; + + if (visible) { + placeholderNode = null;//getSkinnable().getPlaceholder(); + if (placeholderNode == null) { + placeholderNode = new Label(); + ((Label) placeholderNode).setText("empty document"); + } + if (placeholderNode != null) { + if (placeholderRegion == null) { + placeholderRegion = new StackPane(); + placeholderRegion.getStyleClass().setAll("placeholder"); + getChildren().add(placeholderRegion); + } + placeholderRegion.getChildren().setAll(placeholderNode); + } + } + flow.setVisible(!visible); + if (placeholderRegion != null) { + placeholderRegion.setVisible(visible); + } + } + + @Override + public PageView createCell() { + PageView cell = createDefaultCellImpl(); + cell.setLayoutX(-1); + cell.setLayoutY(-1); + cell.setDocumentView(getSkinnable()); + cell.scaleProperty().bind(getSkinnable().scaleProperty()); + cell.rotationProperty().bind(getSkinnable().rotationProperty()); + cell.setPadding(new Insets(10, 20, 10, 20)); + cell.setEffect(dropShadow); + + return cell; + } + + private static PageView createDefaultCellImpl() { + // todo add back factory to build pageView or documentView + return new PageView(); + } + + @Override + protected void layoutChildren(final double x, final double y, + final double w, final double h) { + super.layoutChildren(x, y, w, h); + + if (needCellsRebuilt) { + flow.rebuildCells(); + } else if (needCellsReconfigured) { + flow.reconfigureCells(); + } + + needCellsRebuilt = false; + needCellsReconfigured = false; + + if (getPageCount() == 0) { + // show message overlay instead of empty listview + if (placeholderRegion != null) { + placeholderRegion.setVisible(w > 0 && h > 0); + placeholderRegion.resizeRelocate(x, y, w, h); + } + } else { + flow.resizeRelocate(x, y, w, h); + } + } + + @Override + protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { + checkState(); + +// if (getPageCount() == 0) { +// if (placeholderRegion == null) { +// updatePlaceholderRegionVisibility(); +// } +// if (placeholderRegion != null) { +// return placeholderRegion.prefWidth(height) + leftInset + rightInset; +// } +// } +// +// return computePrefHeight(-1, topInset, rightInset, bottomInset, leftInset) * 0.618033987; + return defaultDimension.getWidth(); + } + + @Override + protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { + return defaultDimension.getHeight(); + } + + public HashMap> getImageCaptureCache() { + return imageCaptureCache; + } + +} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/PageCaptureTask.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/PageCaptureTask.java new file mode 100644 index 000000000..13052d6c3 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/PageCaptureTask.java @@ -0,0 +1,100 @@ +package com.icepdf.fx.scene.control.skin; + +import com.icepdf.fx.scene.control.DocumentView; +import javafx.application.Platform; +import javafx.concurrent.Task; +import javafx.embed.swing.SwingFXUtils; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.shape.Rectangle; +import org.icepdf.core.pobjects.Catalog; +import org.icepdf.core.pobjects.graphics.images.ImageUtility; +import org.icepdf.core.pobjects.PDimension; +import org.icepdf.core.pobjects.Page; +import org.icepdf.core.util.GraphicsRenderingHints; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.lang.ref.SoftReference; + +/** + * Created by pcorl_000 on 2017-03-31. + */ +public class PageCaptureTask extends Task { + + private ImageView pageImageView; + private DocumentView documentView; + + private javafx.scene.shape.Rectangle clip; + + private int index; + + + public PageCaptureTask(int index, ImageView pageImageView, javafx.scene.shape.Rectangle clip, DocumentView documentView) { + this.pageImageView = pageImageView; + this.index = index; + this.documentView = documentView; + this.clip = clip; + } + + @Override + protected Image call() throws Exception { + + try { + Catalog catalog = documentView.getDocument().getCatalog(); + + Page page = catalog.getPageTree().getPage(index); + page.init(); + //todo make pageBounder a property + PDimension sz = page.getSize(Page.BOUNDARY_MEDIABOX, 0, (float) documentView.getScale()); + + int pageWidth = (int) sz.getWidth(); + int pageHeight = (int) sz.getHeight(); + if (clip != null && clip.getWidth() > 0) { + pageWidth = (int) Math.round(clip.getWidth()); + pageHeight = (int) Math.round(clip.getHeight()); + } + + if (isCancelled()) { + return null; + } + + BufferedImage image = ImageUtility.createCompatibleImage(pageWidth, pageHeight); +// System.out.println(pageWidth + " " + pageHeight); + Graphics g = image.createGraphics(); + g.translate((int) -clip.getX(), (int) -clip.getY()); + page.paint(g, GraphicsRenderingHints.SCREEN, Page.BOUNDARY_CROPBOX, 0, (float) documentView.getScale()); + g.dispose(); + + if (isCancelled()) { + return null; + } + + final Image pageImage = SwingFXUtils.toFXImage(image, null); + + if (!isCancelled()) { + DocumentViewSkin documentViewSkin = (DocumentViewSkin) documentView.getSkin(); + documentViewSkin.getImageCaptureCache().put(index, new SoftReference<>(pageImage)); + Platform.runLater(() -> { + // update the location + pageImageView.relocate(clip.getX(), clip.getY()); +// pageImageView.setScaleX(1); +// pageImageView.setScaleY(1); +// pageImageView.setTranslateX(0); +// pageImageView.setTranslateY(0); + pageImageView.setImage(pageImage); + pageImageView.setFitWidth(clip.getWidth()); + pageImageView.setClip(new Rectangle(0, 0, clip.getWidth(), clip.getHeight())); + // make sure it's visible. + pageImageView.setVisible(true); + }); + } + + return pageImage; + } catch (InterruptedException e) { +// e.printStackTrace(); + } + + return null; + } +} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/PageViewSkin.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/PageViewSkin.java new file mode 100644 index 000000000..06e93a8d8 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/PageViewSkin.java @@ -0,0 +1,319 @@ +package com.icepdf.fx.scene.control.skin; + +import com.icepdf.fx.scene.control.DocumentView; +import com.icepdf.fx.scene.control.PageView; +import javafx.concurrent.Task; +import javafx.geometry.Bounds; +import javafx.geometry.HPos; +import javafx.geometry.Point2D; +import javafx.geometry.VPos; +import javafx.scene.Group; +import javafx.scene.control.SkinBase; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Region; +import javafx.scene.paint.Color; +import javafx.scene.shape.*; +import org.icepdf.core.pobjects.Document; +import org.icepdf.core.pobjects.PDimension; +import org.icepdf.core.util.Library; + +import java.lang.ref.SoftReference; + +/** + * + */ +public class PageViewSkin extends SkinBase { + + // default for a4 is 595 X 842, letter, 595 x 792 + public static final double WIDTH = 595; + public static final double HEIGHT = 842; + + private double width = WIDTH; + private double height = HEIGHT; + + private double xViewport = -1; + private double yViewport = -1; + + private Path pageOutline; + private Group pageGroup; + private ImageView pageImageView; + private Rectangle clipRectangle; + private Point2D oldImageView; + + private PageCaptureTask pageCaptureTask; + + private boolean invalidPage = true; + + public PageViewSkin(PageView control) { + super(control); + // match min and max to preferred + getSkinnable().setMaxWidth(Region.USE_PREF_SIZE); + getSkinnable().setMaxHeight(Region.USE_PREF_SIZE); + getSkinnable().setMinWidth(Region.USE_PREF_SIZE); + getSkinnable().setMinHeight(Region.USE_PREF_SIZE); + + // todo remove. + control.backgroundFillProperty().addListener(observable -> { + updatePageColour(); + }); + + control.scaleProperty().addListener((observable, oldValue, newValue) -> { + updateClipRectangle(); + scalePreviousRendering(oldValue.doubleValue(), newValue.doubleValue()); + updatePageZoom(); + }); + control.rotationProperty().addListener(observable -> { + updateClipRectangle(); + updatePageRotation(); + }); + + control.indexProperty().addListener((observable, oldValue, newValue) -> { + updateClipRectangle(); + updatePageRendering(); + }); + + // listen for x:y changes so we can recalculate the page image clip + getSkinnable().layoutYProperty().addListener((observable, oldValue, newValue) -> { + updateClipRectangle(); + updatePageRendering(); + }); + + getSkinnable().getDocumentView().getHbar().valueProperty().addListener((observable, oldValue, newValue) -> { + updateClipRectangle(); + updatePageRendering(); + }); + + getSkinnable().getDocumentView().widthProperty().addListener((observable, oldValue, newValue) -> { + updateClipRectangle(); + updatePageRendering(); + }); + + getSkinnable().getDocumentView().heightProperty().addListener((observable, oldValue, newValue) -> { + updateClipRectangle(); + updatePageRendering(); + }); + + } + + private void initialize() { + + if (pageGroup != null) { + pageGroup.getChildren().clear(); + } else { + pageGroup = new Group(); + getChildren().add(pageGroup); + } + int index = getSkinnable().getIndex(); + Document document = getSkinnable().getDocumentView().getDocument(); + PDimension dimension = document.getPageDimension(index, 0,//(float) getSkinnable().getRotation(), + (float) getSkinnable().getScale()); + pageOutline = new Path(); + pageOutline.getElements().add(new MoveTo(0, 0)); + pageOutline.getElements().add(new LineTo(dimension.getWidth(), 0)); + pageOutline.getElements().add(new LineTo(dimension.getWidth(), dimension.getHeight())); + pageOutline.getElements().add(new LineTo(0, dimension.getHeight())); + pageOutline.getElements().add(new ClosePath()); + pageOutline.setStroke(Color.BLACK); + pageOutline.setFill(Color.WHITE); + + if (pageImageView == null) { + pageImageView = new ImageView(); + pageImageView.setPreserveRatio(true); + } + oldImageView = new Point2D(pageImageView.getLayoutX(), pageImageView.getLayoutY()); + + if (clipRectangle == null) { + clipRectangle = new Rectangle(0, 0, dimension.getWidth(), dimension.getHeight()); + } + // trim rectangle to view port size. + updateClipRectangle(); + clipRectangle.setStroke(Color.RED); + clipRectangle.setFill(null); + updatePageRendering(); + + pageGroup.getChildren().addAll(pageOutline, pageImageView, clipRectangle); + } + + public void updatePageColour() { + if (pageOutline != null) { + pageOutline.setFill(getSkinnable().getBackgroundFill()); + } + } + + public void updatePageRotation() { + if (pageGroup != null) { + pageGroup.setRotate(getSkinnable().getRotation()); + getSkinnable().requestLayout(); + } + } + + public void updatePageZoom() { + if (pageGroup != null) { + invalidPage = true; + getSkinnable().requestLayout(); + } + } + + public void updateClipRectangle() { + DocumentView documentView = getSkinnable().getDocumentView(); + int index = getSkinnable().getIndex(); + + // if page index -1, avoid doing clip work? + if (index == -1) { + return; + } + + Bounds viewport = getNode().getParent().getParent().getLayoutBounds();//getSkinnable().getDocumentView().getBoundsInLocal(); + double scrollBarWidth = getSkinnable().getDocumentView().getHbar().getHeight(); + double x = getSkinnable().getDocumentView().getHbar().getValue(); + double y = getSkinnable().getLayoutY(); // subtract inset. + +// System.out.println(viewport); + + PDimension pDimension = documentView.getDocument().getPageDimension(index, 0,//(float) getSkinnable().getRotation(), + (float) getSkinnable().getScale()); + double pageWidth = pDimension.getWidth(); + double pageHeight = pDimension.getHeight(); + double insetTop = getSkinnable().getInsets().getTop(); + double insetLeft = getSkinnable().getInsets().getLeft(); + double viewportWidth = viewport.getWidth(); + double viewportHeight = viewport.getHeight(); + double xClip = 0; + double yClip = 0; + double clipWidth = pageWidth; + double clipHeight = pageHeight; + + // todo further work is needed here to tighten up the bounds as well as support side by side page views + if (pageWidth >= viewportWidth) { + double max = (pageWidth + insetLeft + scrollBarWidth) - viewportWidth; + if (x > insetLeft) { + xClip = x - insetLeft; + } + if (x >= max) { + clipWidth = viewportWidth - (x - max) - scrollBarWidth; + } else { + clipWidth = viewportWidth;//- scrollBarWidth; + } + + } + if (pageHeight >= viewportHeight) { + if (y < 0) { + yClip = -y - insetTop; + if (yClip < 0) yClip = 0; + clipHeight = pageHeight - yClip; + if (clipHeight > viewportHeight) { + clipHeight = viewportHeight; + } + } else { + clipHeight = viewportHeight - y; + } + } + // update our clip outline. + if (clipRectangle != null) { + clipRectangle.setX(xClip); + clipRectangle.setY(yClip); + clipRectangle.setWidth(clipWidth); + clipRectangle.setHeight(clipHeight); +// System.out.println(clipRectangle); + } + } + + private void scalePreviousRendering(double previousScale, double newScale) { +// pageImageView.setVisible(false); + if (getSkinnable().getIndex() < 0) { + return; + } + + double scaleIncrement = newScale / previousScale; //newScale - previousScale; + + +// double diff = oldImageView.getX() / oldClipRectangle.getWidth(); + +// System.out.println(clipRectangle.getX() + " " +clipRectangle.getY()); +// System.out.println(oldImageView.getX() + " " +oldImageView.getY()); +// System.out.println(); + +// pageImageView.relocate(clipRectangle.getX() * scaleIncrement, clipRectangle.getY() * scaleIncrement); + pageImageView.setFitWidth(clipRectangle.getWidth()); + } + + + /** + * Index has changed and we need to update the contents fo the cell with the new page data. + */ + public void updatePageRendering() { + + int index = getSkinnable().getIndex(); + + stopTask(); + if (index >= 0 && pageImageView != null) { +// pageImageView.setVisible(false); + DocumentView documentView = getSkinnable().getDocumentView(); + // todo cache needs to store previous x,y position, so we can make the repaint smarter + DocumentViewSkin documentViewSkin = (DocumentViewSkin) documentView.getSkin(); + SoftReference cachedImageReference = documentViewSkin.getImageCaptureCache().get(index); + if (cachedImageReference != null) { + Image cachedImage = cachedImageReference.get(); + if (cachedImage != null) { + pageImageView.setImage(cachedImage); + pageImageView.setVisible(true); +// pageImageView.setFitWidth(clipRectangle.getWidth()); + } + } + + if ((pageCaptureTask == null || !pageCaptureTask.isRunning())) { + PDimension pDimension = documentView.getDocument().getPageDimension(index, 0, (float) 1.0f); + width = pDimension.getWidth(); + height = pDimension.getHeight(); + + pageImageView.setClip(new Rectangle(0, 0, clipRectangle.getWidth(), clipRectangle.getHeight())); + + oldImageView = new Point2D(pageImageView.getLayoutX(), pageImageView.getLayoutY()); + + // we have a new page size so we can setup a paint + pageCaptureTask = new PageCaptureTask(index, pageImageView, clipRectangle, documentView); + Library.execute(pageCaptureTask); + + pageCaptureTask.setOnCancelled(t -> { + pageImageView.setVisible(false); + }); + } + } + invalidPage = true; + getSkinnable().requestLayout(); + } + + private boolean stopTask() { + if (pageCaptureTask != null) { + pageCaptureTask.cancel(); + boolean cancelled = pageCaptureTask.isCancelled() || pageCaptureTask.getState() == Task.State.READY; + pageCaptureTask = null; + return cancelled; + } + return false; + } + + + @Override + protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) { + if (invalidPage) { + initialize(); + invalidPage = false; + } + layoutInArea(pageGroup, contentX, contentY, contentWidth, contentHeight, -1, + HPos.CENTER, VPos.CENTER); + } + + @Override + protected double computePrefWidth(double height, double topInset, double rightInset, + double bottomInset, double leftInset) { + return leftInset + rightInset + (width * getSkinnable().getScale()); + } + + @Override + protected double computePrefHeight(double width, double topInset, double rightInset, + double bottomInset, double leftInset) { + return topInset + bottomInset + (height * getSkinnable().getScale()); + } +} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualContainerBase.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualContainerBase.java new file mode 100644 index 000000000..0984f706f --- /dev/null +++ b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualContainerBase.java @@ -0,0 +1,94 @@ +package com.icepdf.fx.scene.control.skin; + + +import com.icepdf.fx.scene.control.PageView; +import com.sun.javafx.scene.control.behavior.BehaviorBase; +import com.sun.javafx.scene.control.skin.BehaviorSkinBase; +import javafx.scene.control.Control; +import javafx.scene.control.ScrollToEvent; + +/** + * Parent class to control skins whose contents are virtualized and scrollable. + * This class handles the interaction with the VirtualFlow class, which is the + * main class handling the virtualization of the contents of this container. + * + * @profile common + */ +public abstract class VirtualContainerBase, I extends PageView> extends BehaviorSkinBase { + + protected boolean pageCountDirty; + + public VirtualContainerBase(final C control, B behavior) { + super(control, behavior); + flow = createVirtualFlow(); + + control.addEventHandler(ScrollToEvent.scrollToTopIndex(), event -> { + // Fix for RT-24630: The row count in VirtualFlow was incorrect + // (normally zero), so the scrollTo call was misbehaving. + if (pageCountDirty) { + // update row count before we do a scroll + updatePageCount(); + pageCountDirty = false; + } + flow.scrollTo(event.getScrollTarget()); + }); + } + + /** + * The virtualized container which handles the layout and scrolling of + * all the cells. + */ + protected final VirtualPageFlow flow; + + /** + * Returns a Cell available to be used in the virtual flow. This means you + * may return either a previously used, but now unrequired cell, or alternatively + * create a new Cell instance. + *

+ * Preference is obviously given to reusing cells whenever possible, to keep + * performance costs down. + */ + public abstract I createCell(); + + /** + * This enables skin subclasses to provide a custom VirtualFlow implementation, + * rather than have VirtualContainerBase instantiate the default instance. + */ + protected VirtualPageFlow createVirtualFlow() { + return new VirtualPageFlow<>(); + } + + /** + * Returns the total number of items in this container, including those + * that are currently hidden because they are out of view. + */ + public abstract int getPageCount(); + + protected abstract void updatePageCount(); + + double getMaxCellWidth(int rowsToCount) { + return snappedLeftInset() + flow.getMaxCellWidth(rowsToCount) + snappedRightInset(); + } + + double getVirtualFlowPreferredHeight(int rows) { + double height = 1.0; + + for (int i = 0; i < rows && i < getPageCount(); i++) { + height += flow.getCellLength(i); + } + + return height + snappedTopInset() + snappedBottomInset(); + } + + @Override + protected void layoutChildren(double x, double y, double w, double h) { + checkState(); + } + + protected void checkState() { + if (pageCountDirty) { + updatePageCount(); + pageCountDirty = false; + } + } +} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualPageFlow.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualPageFlow.java new file mode 100644 index 000000000..b2a144a55 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualPageFlow.java @@ -0,0 +1,2852 @@ +package com.icepdf.fx.scene.control.skin; + + +import com.icepdf.fx.scene.control.PageView; +import com.sun.javafx.scene.control.Logging; +import com.sun.javafx.scene.control.skin.Utils; +import com.sun.javafx.scene.traversal.Algorithm; +import com.sun.javafx.scene.traversal.Direction; +import com.sun.javafx.scene.traversal.ParentTraversalEngine; +import com.sun.javafx.scene.traversal.TraversalContext; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.application.ConditionalFeature; +import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.BooleanPropertyBase; +import javafx.beans.value.ChangeListener; +import javafx.collections.ObservableList; +import javafx.event.EventDispatcher; +import javafx.event.EventHandler; +import javafx.geometry.Orientation; +import javafx.scene.*; +import javafx.scene.control.ScrollBar; +import javafx.scene.input.MouseEvent; +import javafx.scene.input.ScrollEvent; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.shape.Rectangle; +import javafx.util.Callback; +import javafx.util.Duration; +import sun.util.logging.PlatformLogger; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; + +/** + * Implementation of a virtualized container using a cell based mechanism. + */ +public class VirtualPageFlow extends Region { + + protected final static boolean IS_TOUCH_SUPPORTED = Platform.isSupported(ConditionalFeature.INPUT_TOUCH); + + /** + * Scroll events may request to scroll about a number of "lines". We first + * decide how big one "line" is - for fixed cell size it's clear, + * for variable cell size we settle on a single number so that the scrolling + * speed is consistent. Now if the line is so big that + * MIN_SCROLLING_LINES_PER_PAGE of them don't fit into one page, we make + * them smaller to prevent the scrolling step to be too big (perhaps + * even more than one page). + */ + private static final int MIN_SCROLLING_LINES_PER_PAGE = 8; + + private boolean touchDetected = false; + private boolean mouseDown = false; + + + private BooleanProperty vertical; + + public final void setVertical(boolean value) { + verticalProperty().set(value); + } + + public final boolean isVertical() { + return vertical == null ? true : vertical.get(); + } + + public final BooleanProperty verticalProperty() { + if (vertical == null) { + vertical = new BooleanPropertyBase(true) { + @Override + protected void invalidated() { + pile.clear(); + sheetChildren.clear(); + cells.clear(); + lastWidth = lastHeight = -1; + setMaxPrefBreadth(-1); + setViewportBreadth(0); + setViewportLength(0); + lastPosition = 0; + hbar.setValue(0); + vbar.setValue(0); + setPosition(0.0f); + setNeedsLayout(true); + requestLayout(); + } + + @Override + public Object getBean() { + return VirtualPageFlow.this; + } + + @Override + public String getName() { + return "vertical"; + } + }; + } + return vertical; + } + + /** + * Indicates whether the VirtualFlow viewport is capable of being panned + * by the user (either via the mouse or touch events). + */ + private boolean pannable = true; + + public boolean isPannable() { + return pannable; + } + + public void setPannable(boolean value) { + this.pannable = value; + } + + /** + * Indicates the number of cells that should be in the flow. The user of + * the VirtualFlow must set this appropriately. When the cell count changes + * the VirtualFlow responds by updating the visuals. If the items backing + * the cells change, but the count has not changed, you must call the + * reconfigureCells() function to update the visuals. + */ + private int cellCount; + + public int getCellCount() { + return cellCount; + } + + public void setCellCount(int i) { + int oldCount = cellCount; + this.cellCount = i; + + boolean countChanged = oldCount != cellCount; + + // ensure that the virtual scrollbar adjusts in size based on the current + // cell count. + if (countChanged) { + VirtualScrollBar lengthBar = isVertical() ? vbar : hbar; + lengthBar.setMax(i); + } + + // I decided *not* to reset maxPrefBreadth here for the following + // situation. Suppose I have 30 cells and then I add 10 more. Just + // because I added 10 more doesn't mean the max pref should be + // reset. Suppose the first 3 cells were extra long, and I was + // scrolled down such that they weren't visible. If I were to reset + // maxPrefBreadth when subsequent cells were added or removed, then the + // scroll bars would erroneously reset as well. So I do not reset + // the maxPrefBreadth here. + + // Fix for RT-12512, RT-14301 and RT-14864. + // Without this, the VirtualFlow length-wise scrollbar would not change + // as expected. This would leave items unable to be shown, as they + // would exist outside of the visible area, even when the scrollbar + // was at its maximum position. + // FIXME this should be only executed on the pulse, so this will likely + // lead to performance degradation until it is handled properly. + if (countChanged) { + layoutChildren(); + + // Fix for RT-13965: Without this line of code, the number of items in + // the sheet would constantly grow, leaking memory for the life of the + // application. This was especially apparent when the total number of + // cells changes - regardless of whether it became bigger or smaller. + sheetChildren.clear(); + + Parent parent = getParent(); + if (parent != null) parent.requestLayout(); + } + // TODO suppose I had 100 cells and I added 100 more. Further + // suppose I was scrolled to the bottom when that happened. I + // actually want to update the position of the mapper such that + // the view remains "stable". + } + + /** + * The position of the VirtualFlow within its list of cells. This is a value + * between 0 and 1. + */ + private double position; + + public double getPosition() { + return position; + } + + public void setPosition(double newPosition) { + boolean needsUpdate = this.position != newPosition; + this.position = com.sun.javafx.util.Utils.clamp(0, newPosition, 1); + if (needsUpdate) { + requestLayout(); + } + } + + /** + * For optimisation purposes, some use cases can trade dynamic cell length + * for speed - if fixedCellSize is greater than zero we'll use that rather + * than determine it by querying the cell itself. + */ + private double fixedCellSize = 0; + private boolean fixedCellSizeEnabled = false; + + public void setFixedCellSize(final double value) { + this.fixedCellSize = value; + this.fixedCellSizeEnabled = fixedCellSize > 0; + needsCellsLayout = true; + layoutChildren(); + } + + /** + * Callback which is invoked whenever the VirtualFlow needs a new + * IndexedCell. The VirtualFlow attempts to reuse cells whenever possible + * and only creates the minimal number of cells necessary. + */ + private Callback createCell; + + public Callback getCreateCell() { + return createCell; + } + + public void setCreateCell(Callback cc) { + this.createCell = cc; + + if (createCell != null) { + accumCell = null; + setNeedsLayout(true); + recreateCells(); + if (getParent() != null) getParent().requestLayout(); + } + } + + /** + * The maximum preferred size in the non-virtual direction. For example, + * if vertical, then this is the max pref width of all cells encountered. + *

+ * In general, this is the largest preferred size in the non-virtual + * direction that we have ever encountered. We don't reduce this size + * unless instructed to do so, so as to reduce the amount of scroll bar + * jitter. The access on this variable is package ONLY FOR TESTING. + */ + private double maxPrefBreadth; + + protected final void setMaxPrefBreadth(double value) { + this.maxPrefBreadth = value; + } + + protected final double getMaxPrefBreadth() { + return maxPrefBreadth; + } + + /** + * The breadth of the viewport portion of the VirtualFlow as computed during + * the layout pass. In a vertical flow this would be the same as the clip + * view width. In a horizontal flow this is the clip view height. + * The access on this variable is package ONLY FOR TESTING. + */ + private double viewportBreadth; + + protected final void setViewportBreadth(double value) { + this.viewportBreadth = value; + } + + protected final double getViewportBreadth() { + return viewportBreadth; + } + + /** + * The length of the viewport portion of the VirtualFlow as computed + * during the layout pass. In a vertical flow this would be the same as the + * clip view height. In a horizontal flow this is the clip view width. + * The access on this variable is package ONLY FOR TESTING. + */ + private double viewportLength; + + void setViewportLength(double value) { + this.viewportLength = value; + } + + protected double getViewportLength() { + return viewportLength; + } + + + /** + * The width of the VirtualFlow the last time it was laid out. We + * use this information for several fast paths during the layout pass. + */ + double lastWidth = -1; + + /** + * The height of the VirtualFlow the last time it was laid out. We + * use this information for several fast paths during the layout pass. + */ + double lastHeight = -1; + + /** + * The number of "virtual" cells in the flow the last time it was laid out. + * For example, there may have been 1000 virtual cells, but only 20 actual + * cells created and in use. In that case, lastCellCount would be 1000. + */ + int lastCellCount = 0; + + /** + * We remember the last value for vertical the last time we laid out the + * flow. If vertical has changed, we will want to change the max & value + * for the different scroll bars. Since we do all the scroll bar update + * work in the layoutChildren function, we need to know what the old value for + * vertical was. + */ + boolean lastVertical; + + /** + * The position last time we laid out. If none of the lastXXX vars have + * changed respective to their values in layoutChildren, then we can just punt + * out of the method (I hope...) + */ + double lastPosition; + + /** + * The breadth of the first visible cell last time we laid out. + */ + double lastCellBreadth = -1; + + /** + * The length of the first visible cell last time we laid out. + */ + double lastCellLength = -1; + + /** + * The list of cells representing those cells which actually make up the + * current view. The cells are ordered such that the first cell in this + * list is the first in the view, and the last cell is the last in the + * view. When pixel scrolling, the list is simply shifted and items drop + * off the beginning or the end, depending on the order of scrolling. + *

+ * This is package private ONLY FOR TESTING + */ + final ArrayLinkedList cells = new ArrayLinkedList(); + + protected List getCells() { + return cells; + } + + /** + * A structure containing cells that can be reused later. These are cells + * that at one time were needed to populate the view, but now are no longer + * needed. We keep them here until they are needed again. + *

+ * This is package private ONLY FOR TESTING + */ + final ArrayLinkedList pile = new ArrayLinkedList(); + + /** + * A special cell used to accumulate bounds, such that we reduce object + * churn. This cell must be recreated whenever the cell factory function + * changes. This has package access ONLY for testing. + */ + T accumCell; + + /** + * This group is used for holding the 'accumCell'. 'accumCell' must + * be added to the skin for it to be styled. Otherwise, it doesn't + * report the correct width/height leading to issues when scrolling + * the flow + */ + Group accumCellParent; + + /** + * The group which holds the cells. + */ + final Group sheet; + + final ObservableList sheetChildren; + + /** + * The scroll bar used for scrolling horizontally. This has package access + * ONLY for testing. + */ + private VirtualScrollBar hbar = new VirtualScrollBar(this); + + protected final VirtualScrollBar getHbar() { + return hbar; + } + + /** + * The scroll bar used to scrolling vertically. This has package access + * ONLY for testing. + */ + private VirtualScrollBar vbar = new VirtualScrollBar(this); + + protected final VirtualScrollBar getVbar() { + return vbar; + } + + /** + * Control in which the cell's sheet is placed and forms the viewport. The + * viewportBreadth and viewportLength are simply the dimensions of the + * clipView. This has package access ONLY for testing. + */ + ClippedContainer clipView; + + /** + * When both the horizontal and vertical scroll bars are visible, + * we have to 'fill in' the bottom right corner where the two scroll bars + * meet. This is handled by this corner region. This has package access + * ONLY for testing. + */ + StackPane corner; + + // used for panning the virtual flow + private double lastX; + private double lastY; + private boolean isPanning = false; + + public VirtualPageFlow() { + getStyleClass().add("virtual-flow"); + setId("virtual-flow"); + + // initContent + // --- sheet + sheet = new Group(); + sheet.getStyleClass().add("sheet"); + sheet.setStyle("-fx-background-color: red"); + sheet.setAutoSizeChildren(false); + + sheetChildren = sheet.getChildren(); + + // --- clipView + clipView = new ClippedContainer(this); + clipView.setNode(sheet); + getChildren().add(clipView); + + // --- accumCellParent + accumCellParent = new Group(); + + accumCellParent.setVisible(false); + getChildren().add(accumCellParent); + + + /* + ** don't allow the ScrollBar to handle the ScrollEvent, + ** In a VirtualFlow a vertical scroll should scroll on the vertical only, + ** whereas in a horizontal ScrollBar it can scroll horizontally. + */ + // block the event from being passed down to children + final EventDispatcher blockEventDispatcher = (event, tail) -> event; + // block ScrollEvent from being passed down to scrollbar's skin + final EventDispatcher oldHsbEventDispatcher = hbar.getEventDispatcher(); + hbar.setEventDispatcher((event, tail) -> { + if (event.getEventType() == ScrollEvent.SCROLL && + !((ScrollEvent) event).isDirect()) { + tail = tail.prepend(blockEventDispatcher); + tail = tail.prepend(oldHsbEventDispatcher); + return tail.dispatchEvent(event); + } + return oldHsbEventDispatcher.dispatchEvent(event, tail); + }); + // block ScrollEvent from being passed down to scrollbar's skin + final EventDispatcher oldVsbEventDispatcher = vbar.getEventDispatcher(); + vbar.setEventDispatcher((event, tail) -> { + if (event.getEventType() == ScrollEvent.SCROLL && + !((ScrollEvent) event).isDirect()) { + tail = tail.prepend(blockEventDispatcher); + tail = tail.prepend(oldVsbEventDispatcher); + return tail.dispatchEvent(event); + } + return oldVsbEventDispatcher.dispatchEvent(event, tail); + }); + /* + ** listen for ScrollEvents over the whole of the VirtualFlow + ** area, the above dispatcher having removed the ScrollBars + ** scroll event handling. + */ + setOnScroll(new EventHandler() { + @Override + public void handle(ScrollEvent event) { + if (IS_TOUCH_SUPPORTED) { + if (touchDetected == false && mouseDown == false) { + startSBReleasedAnimation(); + } + } + /* + ** calculate the delta in the direction of the flow. + */ + double virtualDelta = 0.0; + if (isVertical()) { + switch (event.getTextDeltaYUnits()) { + case PAGES: + virtualDelta = event.getTextDeltaY() * lastHeight; + break; + case LINES: + double lineSize; +// if (fixedCellSizeEnabled) { +// lineSize = fixedCellSize; +// } else { + // For the scrolling to be reasonably consistent + // we set the lineSize to the average size + // of all currently loaded lines. + T lastCell = cells.getLast(); + lineSize = + (getCellPosition(lastCell) + + getCellLength(lastCell) + - getCellPosition(cells.getFirst())) + / cells.size(); +// } + + if (lastHeight / lineSize < MIN_SCROLLING_LINES_PER_PAGE) { + lineSize = lastHeight / MIN_SCROLLING_LINES_PER_PAGE; + } + + virtualDelta = event.getTextDeltaY() * lineSize; + break; + case NONE: + virtualDelta = event.getDeltaY(); + } + } else { // horizontal + switch (event.getTextDeltaXUnits()) { + case CHARACTERS: + // can we get character size here? + // for now, fall through to pixel values + case NONE: + double dx = event.getDeltaX(); + double dy = event.getDeltaY(); + + virtualDelta = (Math.abs(dx) > Math.abs(dy) ? dx : dy); + } + } + + if (virtualDelta != 0.0) { + /* + ** only consume it if we use it + */ + double result = adjustPixels(-virtualDelta); + if (result != 0.0) { + event.consume(); + } + } + + ScrollBar nonVirtualBar = isVertical() ? hbar : vbar; + if (needBreadthBar) { + double nonVirtualDelta = isVertical() ? event.getDeltaX() : event.getDeltaY(); + if (nonVirtualDelta != 0.0) { + double newValue = nonVirtualBar.getValue() - nonVirtualDelta; + if (newValue < nonVirtualBar.getMin()) { + nonVirtualBar.setValue(nonVirtualBar.getMin()); + } else if (newValue > nonVirtualBar.getMax()) { + nonVirtualBar.setValue(nonVirtualBar.getMax()); + } else { + nonVirtualBar.setValue(newValue); + } + event.consume(); + } + } + } + }); + + + addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler() { + @Override + public void handle(MouseEvent e) { + mouseDown = true; + if (IS_TOUCH_SUPPORTED) { + scrollBarOn(); + } + if (isFocusTraversable()) { + // We check here to see if the current focus owner is within + // this VirtualFlow, and if so we back-off from requesting + // focus back to the VirtualFlow itself. This is particularly + // relevant given the bug identified in RT-32869. In this + // particular case TextInputControl was clearing selection + // when the focus on the TextField changed, meaning that the + // right-click context menu was not showing the correct + // options as there was no selection in the TextField. + boolean doFocusRequest = true; + Node focusOwner = getScene().getFocusOwner(); + if (focusOwner != null) { + Parent parent = focusOwner.getParent(); + while (parent != null) { + if (parent.equals(VirtualPageFlow.this)) { + doFocusRequest = false; + break; + } + parent = parent.getParent(); + } + } + + if (doFocusRequest) { + requestFocus(); + } + } + + lastX = e.getX(); + lastY = e.getY(); + + // determine whether the user has push down on the virtual flow, + // or whether it is the scrollbar. This is done to prevent + // mouse events being 'doubled up' when dragging the scrollbar + // thumb - it has the side-effect of also starting the panning + // code, leading to flicker + isPanning = !(vbar.getBoundsInParent().contains(e.getX(), e.getY()) + || hbar.getBoundsInParent().contains(e.getX(), e.getY())); + } + }); + addEventFilter(MouseEvent.MOUSE_RELEASED, e -> { + mouseDown = false; + if (IS_TOUCH_SUPPORTED) { + startSBReleasedAnimation(); + } + }); + addEventFilter(MouseEvent.MOUSE_DRAGGED, e -> { + if (IS_TOUCH_SUPPORTED) { + scrollBarOn(); + } + if (!isPanning || !isPannable()) return; + + // With panning enabled, we support panning in both vertical + // and horizontal directions, regardless of the fact that + // VirtualFlow is virtual in only one direction. + double xDelta = lastX - e.getX(); + double yDelta = lastY - e.getY(); + + // figure out the distance that the mouse moved in the virtual + // direction, and then perform the movement along that axis + // virtualDelta will contain the amount we actually did move + double virtualDelta = isVertical() ? yDelta : xDelta; + double actual = adjustPixels(virtualDelta); + if (actual != 0) { + // update last* here, as we know we've just adjusted the + // scrollbar. This means we don't get the situation where a + // user presses-and-drags a long way past the min or max + // values, only to change directions and see the scrollbar + // start moving immediately. + if (isVertical()) lastY = e.getY(); + else lastX = e.getX(); + } + + // similarly, we do the same in the non-virtual direction + double nonVirtualDelta = isVertical() ? xDelta : yDelta; + ScrollBar nonVirtualBar = isVertical() ? hbar : vbar; + if (nonVirtualBar.isVisible()) { + double newValue = nonVirtualBar.getValue() + nonVirtualDelta; + if (newValue < nonVirtualBar.getMin()) { + nonVirtualBar.setValue(nonVirtualBar.getMin()); + } else if (newValue > nonVirtualBar.getMax()) { + nonVirtualBar.setValue(nonVirtualBar.getMax()); + } else { + nonVirtualBar.setValue(newValue); + + // same as the last* comment above + if (isVertical()) lastX = e.getX(); + else lastY = e.getY(); + } + } + }); + + /* + * We place the scrollbars _above_ the rectangle, such that the drag + * operations often used in conjunction with scrollbars aren't + * misinterpreted as drag operations on the rectangle as well (which + * would be the case if the scrollbars were underneath it as the + * rectangle itself doesn't block the mouse. + */ + // --- vbar + vbar.setOrientation(Orientation.VERTICAL); + vbar.addEventHandler(MouseEvent.ANY, event -> { + event.consume(); + }); + getChildren().add(vbar); + + // --- hbar + hbar.setOrientation(Orientation.HORIZONTAL); + hbar.addEventHandler(MouseEvent.ANY, event -> { + event.consume(); + }); + getChildren().add(hbar); + + // --- corner + corner = new StackPane(); + corner.getStyleClass().setAll("corner"); + getChildren().add(corner); + + + // initBinds + // clipView binds + InvalidationListener listenerX = valueModel -> { + updateHbar(); + }; + verticalProperty().addListener(listenerX); + hbar.valueProperty().addListener(listenerX); + hbar.visibleProperty().addListener(listenerX); + +// ChangeListener listenerY = new ChangeListener() { +// @Override public void handle(Bean bean, PropertyReference property) { +// clipView.setClipY(isVertical() ? 0 : vbar.getValue()); +// } +// }; +// addChangedListener(VERTICAL, listenerY); +// vbar.addChangedListener(ScrollBar.VALUE, listenerY); + + ChangeListener listenerY = (ov, t, t1) -> { + clipView.setClipY(isVertical() ? 0 : vbar.getValue()); +// System.out.println("v" + getBoundsInLocal()); +// for (T cell : cells){ +// Node parent = cell.getParent(); +// System.out.println(cell.getBoundsInParent()); +// System.out.println(cell.getLocalToParentTransform()); +// } +// System.out.println(); + }; + vbar.valueProperty().addListener(listenerY); + + super.heightProperty().addListener((observable, oldHeight, newHeight) -> { + // Fix for RT-8480, where the VirtualFlow does not show its content + // after changing size to 0 and back. + if (oldHeight.doubleValue() == 0 && newHeight.doubleValue() > 0) { + recreateCells(); + } + }); + + + /* + ** there are certain animations that need to know if the touch is + ** happening..... + */ + setOnTouchPressed(e -> { + touchDetected = true; + scrollBarOn(); + }); + + setOnTouchReleased(e -> { + touchDetected = false; + startSBReleasedAnimation(); + }); + + setImpl_traversalEngine(new ParentTraversalEngine(this, new Algorithm() { + + Node selectNextAfterIndex(int index, TraversalContext context) { + T nextCell; + while ((nextCell = getVisibleCell(++index)) != null) { + if (nextCell.isFocusTraversable()) { + return nextCell; + } + Node n = context.selectFirstInParent(nextCell); + if (n != null) { + return n; + } + } + return null; + } + + Node selectPreviousBeforeIndex(int index, TraversalContext context) { + T prevCell; + while ((prevCell = getVisibleCell(--index)) != null) { + Node prev = context.selectLastInParent(prevCell); + if (prev != null) { + return prev; + } + if (prevCell.isFocusTraversable()) { + return prevCell; + } + } + return null; + } + + @Override + public Node select(Node owner, Direction dir, TraversalContext context) { + T cell; + if (cells.isEmpty()) return null; + if (cells.contains(owner)) { + cell = (T) owner; + } else { + cell = findOwnerCell(owner); + Node next = context.selectInSubtree(cell, owner, dir); + if (next != null) { + return next; + } + if (dir == Direction.NEXT) dir = Direction.NEXT_IN_LINE; + } + int cellIndex = cell.getIndex(); + switch (dir) { + case PREVIOUS: + return selectPreviousBeforeIndex(cellIndex, context); + case NEXT: + Node n = context.selectFirstInParent(cell); + if (n != null) { + return n; + } + // Intentional fall-through + case NEXT_IN_LINE: + return selectNextAfterIndex(cellIndex, context); + } + return null; + } + + private T findOwnerCell(Node owner) { + Parent p = owner.getParent(); + while (!cells.contains(p)) { + p = p.getParent(); + } + return (T) p; + } + + @Override + public Node selectFirst(TraversalContext context) { + T firstCell = cells.getFirst(); + if (firstCell == null) return null; + if (firstCell.isFocusTraversable()) return firstCell; + Node n = context.selectFirstInParent(firstCell); + if (n != null) { + return n; + } + return selectNextAfterIndex(firstCell.getIndex(), context); + } + + @Override + public Node selectLast(TraversalContext context) { + T lastCell = cells.getLast(); + if (lastCell == null) return null; + Node p = context.selectLastInParent(lastCell); + if (p != null) { + return p; + } + if (lastCell.isFocusTraversable()) return lastCell; + return selectPreviousBeforeIndex(lastCell.getIndex(), context); + } + })); + + } + + void updateHbar() { + // Bring the clipView.clipX back to 0 if control is vertical or + // the hbar isn't visible (fix for RT-11666) + if (!isVisible() || getScene() == null) return; + + if (isVertical()) { + if (hbar.isVisible()) { + clipView.setClipX(hbar.getValue()); + } else { + // all cells are now less than the width of the flow, + // so we should shift the hbar/clip such that + // everything is visible in the viewport. + clipView.setClipX(0); + hbar.setValue(0); + } + } + + } + + /*************************************************************************** + * * + * Layout Functionality * + * * + **************************************************************************/ + + /** + * Overridden to implement somewhat more efficient support for layout. The + * VirtualFlow can generally be considered as being unmanaged, in that + * whenever the position changes, or other such things change, we need + * to perform a layout but there is no reason to notify the parent. However + * when things change which may impact the preferred size (such as + * vertical, createCell, and configCell) then we need to notify the + * parent. + */ + @Override + public void requestLayout() { + // isNeedsLayout() is commented out due to RT-21417. This does not + // appear to impact performance (indeed, it may help), and resolves the + // issue identified in RT-21417. + setNeedsLayout(true); + } + + @Override + protected void layoutChildren() { + if (needsRecreateCells) { + lastWidth = -1; + lastHeight = -1; + releaseCell(accumCell); +// accumCell = null; +// accumCellParent.getChildren().clear(); + sheet.getChildren().clear(); + for (int i = 0, max = cells.size(); i < max; i++) { + cells.get(i).updateIndex(-1); + } + cells.clear(); + pile.clear(); + releaseAllPrivateCells(); + } else if (needsRebuildCells) { + lastWidth = -1; + lastHeight = -1; + releaseCell(accumCell); + for (int i = 0; i < cells.size(); i++) { + cells.get(i).updateIndex(-1); + } + addAllToPile(); + releaseAllPrivateCells(); + } else if (needsReconfigureCells) { + setMaxPrefBreadth(-1); + lastWidth = -1; + lastHeight = -1; + } + + if (!dirtyCells.isEmpty()) { + int index; + final int cellsSize = cells.size(); + while ((index = dirtyCells.nextSetBit(0)) != -1 && index < cellsSize) { + T cell = cells.get(index); + // updateIndex(-1) works for TableView, but breaks ListView. + // For now, the TableView just does not use the dirtyCells API +// cell.updateIndex(-1); + if (cell != null) { + cell.requestLayout(); + } + dirtyCells.clear(index); + } + + setMaxPrefBreadth(-1); + lastWidth = -1; + lastHeight = -1; + } + + final boolean hasSizeChange = sizeChanged; + boolean recreatedOrRebuilt = needsRebuildCells || needsRecreateCells || sizeChanged; + + needsRecreateCells = false; + needsReconfigureCells = false; + needsRebuildCells = false; + sizeChanged = false; + + if (needsCellsLayout) { + for (int i = 0, max = cells.size(); i < max; i++) { + PageView cell = cells.get(i); + if (cell != null) { + cell.requestLayout(); + } + } + needsCellsLayout = false; + + // yes, we return here - if needsCellsLayout was set to true, we + // only did it to do the above - not rerun the entire layout. + return; + } + + final double width = getWidth(); + final double height = getHeight(); + final boolean isVertical = isVertical(); + final double position = getPosition(); + + // if the width and/or height is 0, then there is no point doing + // any of this work. In particular, this can happen during startup + if (width <= 0 || height <= 0) { + addAllToPile(); + lastWidth = width; + lastHeight = height; + hbar.setVisible(false); + vbar.setVisible(false); + corner.setVisible(false); + return; + } + + // we check if any of the cells in the cells list need layout. This is a + // sign that they are perhaps animating their sizes. Without this check, + // we may not perform a layout here, meaning that the cell will likely + // 'jump' (in height normally) when the user drags the virtual thumb as + // that is the first time the layout would occur otherwise. + boolean cellNeedsLayout = false; + boolean thumbNeedsLayout = false; + + if (IS_TOUCH_SUPPORTED) { + if ((tempVisibility == true && (hbar.isVisible() == false || vbar.isVisible() == false)) || + (tempVisibility == false && (hbar.isVisible() == true || vbar.isVisible() == true))) { + thumbNeedsLayout = true; + } + } + + if (!cellNeedsLayout) { + for (int i = 0; i < cells.size(); i++) { + PageView cell = cells.get(i); + cellNeedsLayout = cell.isNeedsLayout(); + if (cellNeedsLayout) break; + } + } + + + T firstCell = getFirstVisibleCell(); + + // If no cells need layout, we check other criteria to see if this + // layout call is even necessary. If it is found that no layout is + // needed, we just punt. + if (!cellNeedsLayout && !thumbNeedsLayout) { + boolean cellSizeChanged = false; + if (firstCell != null) { + double breadth = getCellBreadth(firstCell); + double length = getCellLength(firstCell); + cellSizeChanged = (breadth != lastCellBreadth) || (length != lastCellLength); + lastCellBreadth = breadth; + lastCellLength = length; + } + + if (width == lastWidth && + height == lastHeight && + cellCount == lastCellCount && + isVertical == lastVertical && + position == lastPosition && + !cellSizeChanged) { + // TODO this happens to work around the problem tested by + // testCellLayout_LayoutWithoutChangingThingsUsesCellsInSameOrderAsBefore + // but isn't a proper solution. Really what we need to do is, when + // laying out cells, we need to make sure that if a cell is pressed + // AND we are doing a full rebuild then we need to make sure we + // use that cell in the same physical location as before so that + // it gets the mouse release event. + return; + } + } + + /* + * This function may get called under a variety of circumstances. + * It will determine what has changed from the last time it was laid + * out, and will then take one of several execution paths based on + * what has changed so as to perform minimal layout work and also to + * give the expected behavior. One or more of the following may have + * happened: + * + * 1) width/height has changed + * - If the width and/or height has been reduced (but neither of + * them has been expanded), then we simply have to reposition and + * resize the scroll bars + * - If the width (in the vertical case) has expanded, then we + * need to resize the existing cells and reposition and resize + * the scroll bars + * - If the height (in the vertical case) has expanded, then we + * need to resize and reposition the scroll bars and add + * any trailing cells + * + * 2) cell count has changed + * - If the number of cells is bigger, or it is smaller but not + * so small as to move the position then we can just update the + * cells in place without performing layout and update the + * scroll bars. + * - If the number of cells has been reduced and it affects the + * position, then move the position and rebuild all the cells + * and update the scroll bars + * + * 3) size of the cell has changed + * - If the size changed in the virtual direction (ie: height + * in the case of vertical) then layout the cells, adding + * trailing cells as necessary and updating the scroll bars + * - If the size changed in the non virtual direction (ie: width + * in the case of vertical) then simply adjust the widths of + * the cells as appropriate and adjust the scroll bars + * + * 4) vertical changed, cells is empty, maxPrefBreadth == -1, etc + * - Full rebuild. + * + * Each of the conditions really resolves to several of a handful of + * possible outcomes: + * a) reposition & rebuild scroll bars + * b) resize cells in non-virtual direction + * c) add trailing cells + * d) update cells + * e) resize cells in the virtual direction + * f) all of the above + * + * So this function first determines what outcomes need to occur, and + * then will execute all the ones that really need to happen. Every code + * path ends up touching the "reposition & rebuild scroll bars" outcome, + * so that one will be executed every time. + */ + boolean needTrailingCells = false; + boolean rebuild = cellNeedsLayout || + isVertical != lastVertical || + cells.isEmpty() || + getMaxPrefBreadth() == -1 || + position != lastPosition || + cellCount != lastCellCount || + hasSizeChange || + (isVertical && height < lastHeight) || (!isVertical && width < lastWidth); + + if (!rebuild) { + // Check if maxPrefBreadth didn't change + double maxPrefBreadth = getMaxPrefBreadth(); + boolean foundMax = false; + for (int i = 0; i < cells.size(); ++i) { + double breadth = getCellBreadth(cells.get(i)); + if (maxPrefBreadth == breadth) { + foundMax = true; + } else if (breadth > maxPrefBreadth) { + rebuild = true; + break; + } + } + if (!foundMax) { // All values were lower + rebuild = true; + } + } + + if (!rebuild) { + if ((isVertical && height > lastHeight) || (!isVertical && width > lastWidth)) { + // resized in the virtual direction + needTrailingCells = true; + } + } + + initViewport(); + + // Get the index of the "current" cell + int currentIndex = computeCurrentIndex(); + if (lastCellCount != cellCount) { + // The cell count has changed. We want to keep the viewport + // stable if possible. If position was 0 or 1, we want to keep + // the position in the same place. If the new cell count is >= + // the currentIndex, then we will adjust the position to be 1. + // Otherwise, our goal is to leave the index of the cell at the + // top consistent, with the same translation etc. + if (position == 0 || position == 1) { + // Update the item count +// setItemCount(cellCount); + } else if (currentIndex >= cellCount) { + setPosition(1.0f); +// setItemCount(cellCount); + } else if (firstCell != null) { + double firstCellOffset = getCellPosition(firstCell); + int firstCellIndex = getCellIndex(firstCell); +// setItemCount(cellCount); + adjustPositionToIndex(firstCellIndex); + double viewportTopToCellTop = -computeOffsetForCell(firstCellIndex); + adjustByPixelAmount(viewportTopToCellTop - firstCellOffset); + } + + // Update the current index + currentIndex = computeCurrentIndex(); + } + + if (rebuild) { + setMaxPrefBreadth(-1); + // Start by dumping all the cells into the pile + addAllToPile(); + + // The distance from the top of the viewport to the top of the + // cell for the current index. + double offset = -computeViewportOffset(getPosition()); + + // Add all the leading and trailing cells (the call to add leading + // cells will add the current cell as well -- that is, the one that + // represents the current position on the mapper). + addLeadingCells(currentIndex, offset); + + // Force filling of space with empty cells if necessary + addTrailingCells(false); + } else if (needTrailingCells) { + addTrailingCells(false); + } + + computeBarVisiblity(); + updateScrollBarsAndCells(recreatedOrRebuilt); + + lastWidth = getWidth(); + lastHeight = getHeight(); + lastCellCount = getCellCount(); + lastVertical = isVertical(); + lastPosition = getPosition(); + + cleanPile(); + } + + /** + * Adds all the cells prior to and including the given currentIndex, until + * no more can be added without falling off the flow. The startOffset + * indicates the distance from the leading edge (top) of the viewport to + * the leading edge (top) of the currentIndex. + */ + protected void addLeadingCells(int currentIndex, double startOffset) { + // The offset will keep track of the distance from the top of the + // viewport to the top of the current index. We will increment it + // as we lay out leading cells. + double offset = startOffset; + // The index is the absolute index of the cell being laid out + int index = currentIndex; + + // Offset should really be the bottom of the current index + boolean first = true; // first time in, we just fudge the offset and let + // it be the top of the current index then redefine + // it as the bottom of the current index thereafter + // while we have not yet laid out so many cells that they would fall + // off the flow, we will continue to create and add cells. The + // offset is our indication of whether we can lay out additional + // cells. If the offset is ever < 0, except in the case of the very + // first cell, then we must quit. + T cell = null; + + // special case for the position == 1.0, skip adding last invisible cell + if (index == cellCount && offset == getViewportLength()) { + index--; + first = false; + } + while (index >= 0 && (offset > 0 || first)) { + cell = getAvailableCell(index); + setCellIndex(cell, index); + resizeCellSize(cell); // resize must be after config + cells.addFirst(cell); + + // A little gross but better than alternatives because it reduces + // the number of times we have to update a cell or compute its + // size. The first time into this loop "offset" is actually the + // top of the current index. On all subsequent visits, it is the + // bottom of the current index. + if (first) { + first = false; + } else { + offset -= getCellLength(cell); + } + + // Position the cell, and update the maxPrefBreadth variable as we go. + positionCell(cell, offset); + setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell))); + cell.setVisible(true); + --index; + } + + // There are times when after laying out the cells we discover that + // the top of the first cell which represents index 0 is below the top + // of the viewport. In these cases, we have to adjust the cells up + // and reset the mapper position. This might happen when items got + // removed at the top or when the viewport size increased. + if (cells.size() > 0) { + cell = cells.getFirst(); + int firstIndex = getCellIndex(cell); + double firstCellPos = getCellPosition(cell); + if (firstIndex == 0 && firstCellPos > 0) { + setPosition(0.0f); + offset = 0; + for (int i = 0; i < cells.size(); i++) { + cell = cells.get(i); + positionCell(cell, offset); + offset += getCellLength(cell); + } + } + } else { + // reset scrollbar to top, so if the flow sees cells again it starts at the top + vbar.setValue(0); + hbar.setValue(0); + } + } + + /** + * Adds all the trailing cells that come after the last index in + * the cells ObservableList. + */ + protected boolean addTrailingCells(boolean fillEmptyCells) { + // If cells is empty then addLeadingCells bailed for some reason and + // we're hosed, so just punt + if (cells.isEmpty()) return false; + + // While we have not yet laid out so many cells that they would fall + // off the flow, so we will continue to create and add cells. When the + // offset becomes greater than the width/height of the flow, then we + // know we cannot add any more cells. + T startCell = cells.getLast(); + double offset = getCellPosition(startCell) + getCellLength(startCell); + int index = getCellIndex(startCell) + 1; + boolean filledWithNonEmpty = index <= cellCount; + + final double viewportLength = getViewportLength(); + + // Fix for RT-37421, which was a regression caused by RT-36556 + if (offset < 0 && !fillEmptyCells) { + return false; + } + + // + // RT-36507: viewportLength - offset gives the maximum number of + // additional cells that should ever be able to fit in the viewport if + // every cell had a height of 1. If index ever exceeds this count, + // then offset is not incrementing fast enough, or at all, which means + // there is something wrong with the cell size calculation. + // + final double maxCellCount = viewportLength - offset; + while (offset < viewportLength) { + if (index >= cellCount) { + if (offset < viewportLength) filledWithNonEmpty = false; + if (!fillEmptyCells) return filledWithNonEmpty; + // RT-36507 - return if we've exceeded the maximum + if (index > maxCellCount) { + final PlatformLogger logger = Logging.getControlsLogger(); + if (logger.isLoggable(PlatformLogger.Level.INFO)) { + if (startCell != null) { + logger.info("index exceeds maxCellCount. Check size calculations for " + startCell.getClass()); + } else { + logger.info("index exceeds maxCellCount"); + } + } + return filledWithNonEmpty; + } + } + T cell = getAvailableCell(index); + setCellIndex(cell, index); + resizeCellSize(cell); // resize happens after config! + cells.addLast(cell); + + // Position the cell and update the max pref + positionCell(cell, offset); + setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell))); + + offset += getCellLength(cell); + cell.setVisible(true); + ++index; + } + + // Discover whether the first cell coincides with index #0. If after + // adding all the trailing cells we find that a) the first cell was + // not index #0 and b) there are trailing cells, then we have a + // problem. We need to shift all the cells down and add leading cells, + // one at a time, until either the very last non-empty cells is aligned + // with the bottom OR we have laid out cell index #0 at the first + // position. + T firstCell = cells.getFirst(); + index = getCellIndex(firstCell); + T lastNonEmptyCell = getLastVisibleCell(); + double start = getCellPosition(firstCell); + double end = getCellPosition(lastNonEmptyCell) + getCellLength(lastNonEmptyCell); + if ((index != 0 || (index == 0 && start < 0)) && fillEmptyCells && + lastNonEmptyCell != null && getCellIndex(lastNonEmptyCell) == cellCount - 1 && end < viewportLength) { + + double prospectiveEnd = end; + double distance = viewportLength - end; + while (prospectiveEnd < viewportLength && index != 0 && (-start) < distance) { + index--; + T cell = getAvailableCell(index); + setCellIndex(cell, index); + resizeCellSize(cell); // resize must be after config + cells.addFirst(cell); + double cellLength = getCellLength(cell); + start -= cellLength; + prospectiveEnd += cellLength; + positionCell(cell, start); + setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell))); + cell.setVisible(true); + } + + // The amount by which to translate the cells down + firstCell = cells.getFirst(); + start = getCellPosition(firstCell); + double delta = viewportLength - end; + if (getCellIndex(firstCell) == 0 && delta > (-start)) { + delta = (-start); + } + // Move things + for (int i = 0; i < cells.size(); i++) { + T cell = cells.get(i); + positionCell(cell, getCellPosition(cell) + delta); + } + + // Check whether the first cell, subsequent to our adjustments, is + // now index #0 and aligned with the top. If so, change the position + // to be at 0 instead of 1. + start = getCellPosition(firstCell); + if (getCellIndex(firstCell) == 0 && start == 0) { + setPosition(0); + } else if (getPosition() != 1) { + setPosition(1); + } + } + + return filledWithNonEmpty; + } + + /** + * @return true if bar visibility changed + */ + private boolean computeBarVisiblity() { + if (cells.isEmpty()) { + // In case no cells are set yet, we assume no bars are needed + needLengthBar = false; + needBreadthBar = false; + return true; + } + + final boolean isVertical = isVertical(); + boolean barVisibilityChanged = false; + + VirtualScrollBar breadthBar = isVertical ? hbar : vbar; + VirtualScrollBar lengthBar = isVertical ? vbar : hbar; + + final double viewportBreadth = getViewportBreadth(); + + final int cellsSize = cells.size(); + for (int i = 0; i < 2; i++) { + final boolean lengthBarVisible = getPosition() > 0 + || cellCount > cellsSize + || (cellCount == cellsSize && (getCellPosition(cells.getLast()) + getCellLength(cells.getLast())) > getViewportLength()) + || (cellCount == cellsSize - 1 && barVisibilityChanged && needBreadthBar); + + if (lengthBarVisible ^ needLengthBar) { + needLengthBar = lengthBarVisible; + barVisibilityChanged = true; + } + + // second conditional removed for RT-36669. + final boolean breadthBarVisible = (maxPrefBreadth > viewportBreadth);// || (needLengthBar && maxPrefBreadth > (viewportBreadth - lengthBarBreadth)); + if (breadthBarVisible ^ needBreadthBar) { + needBreadthBar = breadthBarVisible; + barVisibilityChanged = true; + } + } + + // Start by optimistically deciding whether the length bar and + // breadth bar are needed and adjust the viewport dimensions + // accordingly. If during layout we find that one or the other of the + // bars actually is needed, then we will perform a cleanup pass + + if (!IS_TOUCH_SUPPORTED) { + updateViewportDimensions(); + breadthBar.setVisible(needBreadthBar); + lengthBar.setVisible(needLengthBar); + } else { + breadthBar.setVisible(needBreadthBar && tempVisibility); + lengthBar.setVisible(needLengthBar && tempVisibility); + } + + return barVisibilityChanged; + } + + private void updateViewportDimensions() { + final boolean isVertical = isVertical(); + final double breadthBarLength = snapSize(isVertical ? hbar.prefHeight(-1) : vbar.prefWidth(-1)); + final double lengthBarBreadth = snapSize(isVertical ? vbar.prefWidth(-1) : hbar.prefHeight(-1)); + + setViewportBreadth((isVertical ? getWidth() : getHeight()) - (needLengthBar ? lengthBarBreadth : 0)); + setViewportLength((isVertical ? getHeight() : getWidth()) - (needBreadthBar ? breadthBarLength : 0)); + } + + private void initViewport() { + // Initialize the viewportLength and viewportBreadth to match the + // width/height of the flow + final boolean isVertical = isVertical(); + + updateViewportDimensions(); + + VirtualScrollBar breadthBar = isVertical ? hbar : vbar; + VirtualScrollBar lengthBar = isVertical ? vbar : hbar; + + // If there has been a switch between the virtualized bar, then we + // will want to do some stuff TODO. + breadthBar.setVirtual(false); + lengthBar.setVirtual(true); + } + + @Override + protected void setWidth(double value) { + if (value != lastWidth) { + super.setWidth(value); + sizeChanged = true; + setNeedsLayout(true); + requestLayout(); + } + } + + @Override + protected void setHeight(double value) { + if (value != lastHeight) { + super.setHeight(value); + sizeChanged = true; + setNeedsLayout(true); + requestLayout(); + } + } + + private void updateScrollBarsAndCells(boolean recreate) { + // Assign the hbar and vbar to the breadthBar and lengthBar so as + // to make some subsequent calculations easier. + final boolean isVertical = isVertical(); + VirtualScrollBar breadthBar = isVertical ? hbar : vbar; + VirtualScrollBar lengthBar = isVertical ? vbar : hbar; + + // We may have adjusted the viewport length and breadth after the + // layout due to scroll bars becoming visible. So we need to perform + // a follow up pass and resize and shift all the cells to fit the + // viewport. Note that the prospective viewport size is always >= the + // final viewport size, so we don't have to worry about adding + // cells during this cleanup phase. + fitCells(); + + // Update cell positions. + // When rebuilding the cells, we add the cells and along the way compute + // the maxPrefBreadth. Based on the computed value, we may add + // the breadth scrollbar which changes viewport length, so we need + // to re-position the cells. + if (!cells.isEmpty()) { + final double currOffset = -computeViewportOffset(getPosition()); + final int currIndex = computeCurrentIndex() - cells.getFirst().getIndex(); + final int size = cells.size(); + + // position leading cells + double offset = currOffset; + + for (int i = currIndex - 1; i >= 0 && i < size; i--) { + final T cell = cells.get(i); + + offset -= getCellLength(cell); + + positionCell(cell, offset); + } + + // position trailing cells + offset = currOffset; + for (int i = currIndex; i >= 0 && i < size; i++) { + final T cell = cells.get(i); + positionCell(cell, offset); + + offset += getCellLength(cell); + } + } + + // Toggle visibility on the corner + corner.setVisible(breadthBar.isVisible() && lengthBar.isVisible()); + + double sumCellLength = 0; + double flowLength = (isVertical ? getHeight() : getWidth()) - + (breadthBar.isVisible() ? breadthBar.prefHeight(-1) : 0); + + final double viewportBreadth = getViewportBreadth(); + final double viewportLength = getViewportLength(); + + // Now position and update the scroll bars + if (breadthBar.isVisible()) { + /* + ** Positioning the ScrollBar + */ + if (!IS_TOUCH_SUPPORTED) { + if (isVertical) { + hbar.resizeRelocate(0, viewportLength, + viewportBreadth, hbar.prefHeight(viewportBreadth)); + } else { + vbar.resizeRelocate(viewportLength, 0, + vbar.prefWidth(viewportBreadth), viewportBreadth); + } + } else { + if (isVertical) { + hbar.resizeRelocate(0, (viewportLength - hbar.getHeight()), + viewportBreadth, hbar.prefHeight(viewportBreadth)); + } else { + vbar.resizeRelocate((viewportLength - vbar.getWidth()), 0, + vbar.prefWidth(viewportBreadth), viewportBreadth); + } + } + + if (getMaxPrefBreadth() != -1) { + double newMax = Math.max(1, getMaxPrefBreadth() - viewportBreadth); + if (newMax != breadthBar.getMax()) { + breadthBar.setMax(newMax); + + double breadthBarValue = breadthBar.getValue(); + boolean maxed = breadthBarValue != 0 && newMax == breadthBarValue; + if (maxed || breadthBarValue > newMax) { + breadthBar.setValue(newMax); + } + + breadthBar.setVisibleAmount((viewportBreadth / getMaxPrefBreadth()) * newMax); + } + } + } + + // determine how many cells there are on screen so that the scrollbar + // thumb can be appropriately sized + if (recreate && (lengthBar.isVisible() || IS_TOUCH_SUPPORTED)) { + int numCellsVisibleOnScreen = 0; + for (int i = 0, max = cells.size(); i < max; i++) { + T cell = cells.get(i); + if (cell != null) {// && !cell.isEmpty()) { + sumCellLength += (isVertical ? cell.getHeight() : cell.getWidth()); + if (sumCellLength > flowLength) { + break; + } + + numCellsVisibleOnScreen++; + } + } + + lengthBar.setMax(1); + if (numCellsVisibleOnScreen == 0 && cellCount == 1) { + // special case to help resolve RT-17701 and the case where we have + // only a single row and it is bigger than the viewport + lengthBar.setVisibleAmount(flowLength / sumCellLength); + } else { + lengthBar.setVisibleAmount(numCellsVisibleOnScreen / (float) cellCount); + } + } + + if (lengthBar.isVisible()) { + // Fix for RT-11873. If this isn't here, we can have a situation where + // the scrollbar scrolls endlessly. This is possible when the cell + // count grows as the user hits the maximal position on the scrollbar + // (i.e. the list size dynamically grows as the user needs more). + // + // This code was commented out to resolve RT-14477 after testing + // whether RT-11873 can be recreated. It could not, and therefore + // for now this code will remained uncommented until it is deleted + // following further testing. +// if (lengthBar.getValue() == 1.0 && lastCellCount != cellCount) { +// lengthBar.setValue(0.99); +// } + + /* + ** Positioning the ScrollBar + */ + if (!IS_TOUCH_SUPPORTED) { + if (isVertical) { + vbar.resizeRelocate(viewportBreadth, 0, vbar.prefWidth(viewportLength), viewportLength); + } else { + hbar.resizeRelocate(0, viewportBreadth, viewportLength, hbar.prefHeight(-1)); + } + } else { + if (isVertical) { + vbar.resizeRelocate((viewportBreadth - vbar.getWidth()), 0, vbar.prefWidth(viewportLength), viewportLength); + } else { + hbar.resizeRelocate(0, (viewportBreadth - hbar.getHeight()), viewportLength, hbar.prefHeight(-1)); + } + } + } + + if (corner.isVisible()) { + if (!IS_TOUCH_SUPPORTED) { + corner.resize(vbar.getWidth(), hbar.getHeight()); + corner.relocate(hbar.getLayoutX() + hbar.getWidth(), vbar.getLayoutY() + vbar.getHeight()); + } else { + corner.resize(vbar.getWidth(), hbar.getHeight()); + corner.relocate(hbar.getLayoutX() + (hbar.getWidth() - vbar.getWidth()), vbar.getLayoutY() + (vbar.getHeight() - hbar.getHeight())); + hbar.resize(hbar.getWidth() - vbar.getWidth(), hbar.getHeight()); + vbar.resize(vbar.getWidth(), vbar.getHeight() - hbar.getHeight()); + } + } + + clipView.resize(snapSize(isVertical ? viewportBreadth : viewportLength), + snapSize(isVertical ? viewportLength : viewportBreadth)); + + // If the viewportLength becomes large enough that all cells fit + // within the viewport, then we want to update the value to match. + if (getPosition() != lengthBar.getValue()) { + lengthBar.setValue(getPosition()); + } + } + + /** + * Adjusts the cells location and size if necessary. The breadths of all + * cells will be adjusted to fit the viewportWidth or maxPrefBreadth, and + * the layout position will be updated if necessary based on index and + * offset. + */ + private void fitCells() { + double size = Math.max(getMaxPrefBreadth(), getViewportBreadth()); + boolean isVertical = isVertical(); + + // Note: Do not optimise this loop by pre-calculating the cells size and + // storing that into a int value - this can lead to RT-32828 + for (int i = 0; i < cells.size(); i++) { + PageView cell = cells.get(i); + if (isVertical) { + cell.resize(size, cell.prefHeight(size)); + } else { + cell.resize(cell.prefWidth(size), size); + } + } + } + + private void cull() { + final double viewportLength = getViewportLength(); + for (int i = cells.size() - 1; i >= 0; i--) { + T cell = cells.get(i); + double cellSize = getCellLength(cell); + double cellStart = getCellPosition(cell); + double cellEnd = cellStart + cellSize; + if (cellStart >= viewportLength || cellEnd < 0) { + addToPile(cells.remove(i)); + } + } + } + + /*************************************************************************** + * * + * Helper functions for working with cells * + * * + **************************************************************************/ + + /** + * Return the index for a given cell. This allows subclasses to customise + * how cell indices are retrieved. + */ + protected int getCellIndex(T cell) { + return cell.getIndex(); + } + + + /** + * Return a cell for the given index. This may be called for any cell, + * including beyond the range defined by cellCount, in which case an + * empty cell will be returned. The returned value should not be stored for + * any reason. + */ + public T getCell(int index) { + // If there are cells, then we will attempt to get an existing cell + if (!cells.isEmpty()) { + // First check the cells that have already been created and are + // in use. If this call returns a value, then we can use it + T cell = getVisibleCell(index); + if (cell != null) return cell; + } + + // check the pile + for (int i = 0; i < pile.size(); i++) { + T cell = pile.get(i); + if (getCellIndex(cell) == index) { + // Note that we don't remove from the pile: if we do it leads + // to a severe performance decrease. This seems to be OK, as + // getCell() is only used for cell measurement purposes. + // pile.remove(i); + return cell; + } + } + + if (pile.size() > 0) { + return pile.get(0); + } + + // We need to use the accumCell and return that + if (accumCell == null) { + Callback createCell = getCreateCell(); + if (createCell != null) { + accumCell = createCell.call(this); + accumCell.getProperties().put(NEW_CELL, null); + accumCellParent.getChildren().setAll(accumCell); + + // Note the screen reader will attempt to find all + // the items inside the view to calculate the item count. + // Having items under different parents (sheet and accumCellParent) + // leads the screen reader to compute wrong values. + // The regular scheme to provide items to the screen reader + // uses getPrivateCell(), which places the item in the sheet. + // The accumCell, and its children, should be ignored by the + // screen reader. + accumCell.setAccessibleRole(AccessibleRole.NODE); + accumCell.getChildrenUnmodifiable().addListener((Observable c) -> { + for (Node n : accumCell.getChildrenUnmodifiable()) { + n.setAccessibleRole(AccessibleRole.NODE); + } + }); + } + } + setCellIndex(accumCell, index); + resizeCellSize(accumCell); + return accumCell; + } + + /** + * After using the accum cell, it needs to be released! + */ + private void releaseCell(T cell) { + if (accumCell != null && cell == accumCell) { + accumCell.updateIndex(-1); + } + } + + /** + * This method is an experts-only method - if the requested index is not + * already an existing visible cell, it will create a cell for the + * given index and insert it into the sheet. From that point on it will be + * unmanaged, and is up to the caller of this method to manage it. + */ + T getPrivateCell(int index) { + T cell = null; + + // If there are cells, then we will attempt to get an existing cell + if (!cells.isEmpty()) { + // First check the cells that have already been created and are + // in use. If this call returns a value, then we can use it + cell = getVisibleCell(index); + if (cell != null) { + // Force the underlying text inside the cell to be updated + // so that when the screen reader runs, it will match the + // text in the cell (force updateDisplayedText()) + cell.layout(); + return cell; + } + } + + // check the existing sheet children + if (cell == null) { + for (int i = 0; i < sheetChildren.size(); i++) { + T _cell = (T) sheetChildren.get(i); + if (getCellIndex(_cell) == index) { + return _cell; + } + } + } + + if (cell == null) { + Callback createCell = getCreateCell(); + if (createCell != null) { + cell = createCell.call(this); + } + } + + if (cell != null) { + setCellIndex(cell, index); + resizeCellSize(cell); + cell.setVisible(false); + sheetChildren.add(cell); + privateCells.add(cell); + } + + return cell; + } + + private final List privateCells = new ArrayList<>(); + + private void releaseAllPrivateCells() { + sheetChildren.removeAll(privateCells); + } + + /** + * Compute and return the length of the cell for the given index. This is + * called both internally when adjusting by pixels, and also at times + * by PositionMapper (see the getItemSize callback). When called by + * PositionMapper, it is possible that it will be called for some index + * which is not associated with any cell, so we have to do a bit of work + * to use a cell as a helper for computing cell size in some cases. + */ + protected double getCellLength(int index) { + if (fixedCellSizeEnabled) return fixedCellSize; + + T cell = getCell(index); + double length = getCellLength(cell); + releaseCell(cell); + return length; + } + + /** + */ + protected double getCellBreadth(int index) { + T cell = getCell(index); + double b = getCellBreadth(cell); + releaseCell(cell); + return b; + } + + /** + * Gets the length of a specific cell + */ + protected double getCellLength(T cell) { + if (cell == null) return 0; + if (fixedCellSizeEnabled) return fixedCellSize; + + return isVertical() ? + cell.getLayoutBounds().getHeight() + : cell.getLayoutBounds().getWidth(); + } + +// private double getCellPrefLength(T cell) { +// return isVertical() ? +// cell.prefHeight(-1) +// : cell.prefWidth(-1); +// } + + /** + * Gets the breadth of a specific cell + */ + protected double getCellBreadth(PageView cell) { + return isVertical() ? + cell.prefWidth(-1) + : cell.prefHeight(-1); + } + + /** + * Gets the layout position of the cell along the length axis + */ + protected double getCellPosition(T cell) { + if (cell == null) return 0; + + return isVertical() ? + cell.getLayoutY() + : cell.getLayoutX(); + } + + protected void positionCell(T cell, double position) { + if (isVertical()) { + cell.setLayoutX(0);//getWidth()/2 - cell.getPageBounds().getWidth()/2); + cell.setLayoutY((position)); + } else { + cell.setLayoutX(snapSize(position)); + cell.setLayoutY(0); + } + } + + protected void resizeCellSize(T cell) { + if (cell == null) return; + + if (isVertical()) { + double width = Math.max(getMaxPrefBreadth(), getViewportBreadth()); + cell.resize(width, fixedCellSizeEnabled ? fixedCellSize : Utils.boundedSize(cell.prefHeight(width), cell.minHeight(width), cell.maxHeight(width))); + } else { + double height = Math.max(getMaxPrefBreadth(), getViewportBreadth()); + cell.resize(fixedCellSizeEnabled ? fixedCellSize : Utils.boundedSize(cell.prefWidth(height), cell.minWidth(height), cell.maxWidth(height)), height); + } + } + + protected void setCellIndex(T cell, int index) { + assert cell != null; + + cell.updateIndex(index); + + // make sure the cell is sized correctly. This is important for both + // general layout of cells in a VirtualFlow, but also in cases such as + // RT-34333, where the sizes were being reported incorrectly to the + // ComboBox popup. + if ((cell.isNeedsLayout() && cell.getScene() != null) || cell.getProperties().containsKey(NEW_CELL)) { + cell.applyCss(); + cell.getProperties().remove(NEW_CELL); + } + } + + /*************************************************************************** + * * + * Helper functions for cell management * + * * + **************************************************************************/ + + + /** + * Indicates that this is a newly created cell and we need call impl_processCSS for it. + *

+ * See RT-23616 for more details. + */ + private static final String NEW_CELL = "newcell"; + + /** + * Get a cell which can be used in the layout. This function will reuse + * cells from the pile where possible, and will create new cells when + * necessary. + */ + protected T getAvailableCell(int prefIndex) { + T cell = null; + + // Fix for RT-12822. We try to retrieve the cell from the pile rather + // than just grab a random cell from the pile (or create another cell). + for (int i = 0, max = pile.size(); i < max; i++) { + T _cell = pile.get(i); + assert _cell != null; + + if (getCellIndex(_cell) == prefIndex) { + cell = _cell; + pile.remove(i); + break; + } + cell = null; + } + + if (cell == null) { + if (pile.size() > 0) { + // we try to get a cell with an index that is the same even/odd + // as the prefIndex. This saves us from having to run so much + // css on the cell as it will not change from even to odd, or + // vice versa + final boolean prefIndexIsEven = (prefIndex & 1) == 0; + for (int i = 0, max = pile.size(); i < max; i++) { + final T c = pile.get(i); + final int cellIndex = getCellIndex(c); + + if ((cellIndex & 1) == 0 && prefIndexIsEven) { + cell = c; + pile.remove(i); + break; + } else if ((cellIndex & 1) == 1 && !prefIndexIsEven) { + cell = c; + pile.remove(i); + break; + } + } + + if (cell == null) { + cell = pile.removeFirst(); + } + } else { + cell = getCreateCell().call(this); + cell.getProperties().put(NEW_CELL, null); + } + } + + if (cell.getParent() == null) { + sheetChildren.add(cell); + } + + return cell; + } + + // protected to allow subclasses to clean up + protected void addAllToPile() { + for (int i = 0, max = cells.size(); i < max; i++) { + addToPile(cells.removeFirst()); + } + } + + /** + * Puts the given cell onto the pile. This is called whenever a cell has + * fallen off the flow's start. + */ + private void addToPile(T cell) { + assert cell != null; + pile.addLast(cell); + } + + private void cleanPile() { + boolean wasFocusOwner = false; + + for (int i = 0, max = pile.size(); i < max; i++) { + T cell = pile.get(i); + wasFocusOwner = wasFocusOwner || doesCellContainFocus(cell); + cell.setVisible(false); + } + + // Fix for RT-35876: Rather than have the cells do weird things with + // focus (in particular, have focus jump between cells), we return focus + // to the VirtualFlow itself. + if (wasFocusOwner) { + requestFocus(); + } + } + + private boolean doesCellContainFocus(PageView c) { + Scene scene = c.getScene(); + final Node focusOwner = scene == null ? null : scene.getFocusOwner(); + + if (focusOwner != null) { + if (c.equals(focusOwner)) { + return true; + } + + Parent p = focusOwner.getParent(); + while (p != null && !(p instanceof VirtualPageFlow)) { + if (c.equals(p)) { + return true; + } + p = p.getParent(); + } + } + + return false; + } + + /** + * Gets a cell for the given index if the cell has been created and laid out. + * "Visible" is a bit of a misnomer, the cell might not be visible in the + * viewport (it may be clipped), but does distinguish between cells that + * have been created and are in use vs. those that are in the pile or + * not created. + */ + public T getVisibleCell(int index) { + if (cells.isEmpty()) return null; + + // check the last index + T lastCell = cells.getLast(); + int lastIndex = getCellIndex(lastCell); + if (index == lastIndex) return lastCell; + + // check the first index + T firstCell = cells.getFirst(); + int firstIndex = getCellIndex(firstCell); + if (index == firstIndex) return firstCell; + + // if index is > firstIndex and < lastIndex then we can get the index + if (index > firstIndex && index < lastIndex) { + T cell = cells.get(index - firstIndex); + if (getCellIndex(cell) == index) return cell; + } + + // there is no visible cell for the specified index + return null; + } + + /** + * Locates and returns the last non-empty IndexedCell that is currently + * partially or completely visible. This function may return null if there + * are no cells, or if the viewport length is 0. + */ + public T getLastVisibleCell() { + if (cells.isEmpty() || getViewportLength() <= 0) return null; + + T cell; + for (int i = cells.size() - 1; i >= 0; i--) { + cell = cells.get(i); +// if (!cell.isEmpty()) { + return cell; +// } + } + + return null; + } + + /** + * Locates and returns the first non-empty IndexedCell that is partially or + * completely visible. This really only ever returns null if there are no + * cells or the viewport length is 0. + */ + public T getFirstVisibleCell() { + if (cells.isEmpty() || getViewportLength() <= 0) return null; + T cell = cells.getFirst(); + return cell; //cell.isEmpty() ? null : cell; + } + + // Returns last visible cell whose bounds are entirely within the viewport + public T getLastVisibleCellWithinViewPort() { + if (cells.isEmpty() || getViewportLength() <= 0) return null; + + T cell; + final double max = getViewportLength(); + for (int i = cells.size() - 1; i >= 0; i--) { + cell = cells.get(i); +// if (cell.isEmpty()) continue; + + final double cellStart = getCellPosition(cell); + final double cellEnd = cellStart + getCellLength(cell); + + // we use the magic +2 to allow for a little bit of fuzziness, + // this is to help in situations such as RT-34407 + if (cellEnd <= (max + 2)) { + return cell; + } + } + + return null; + } + + // Returns first visible cell whose bounds are entirely within the viewport + public T getFirstVisibleCellWithinViewPort() { + if (cells.isEmpty() || getViewportLength() <= 0) return null; + + T cell; + for (int i = 0; i < cells.size(); i++) { + cell = cells.get(i); +// if (cell.isEmpty()) continue; + + final double cellStart = getCellPosition(cell); + if (cellStart >= 0) { + return cell; + } + } + + return null; + } + + /** + * Adjust the position of cells so that the specified cell + * will be positioned at the start of the viewport. The given cell must + * already be "live". This is bad public API! + */ + public void showAsFirst(T firstCell) { + if (firstCell != null) { + adjustPixels(getCellPosition(firstCell)); + } + } + + /** + * Adjust the position of cells so that the specified cell + * will be positioned at the end of the viewport. The given cell must + * already be "live". This is bad public API! + */ + public void showAsLast(T lastCell) { + if (lastCell != null) { + adjustPixels(getCellPosition(lastCell) + getCellLength(lastCell) - getViewportLength()); + } + } + + /** + * Adjusts the cells such that the selected cell will be fully visible in + * the viewport (but only just). + */ + public void show(T cell) { + if (cell != null) { + final double start = getCellPosition(cell); + final double length = getCellLength(cell); + final double end = start + length; + final double viewportLength = getViewportLength(); + + if (start < 0) { + adjustPixels(start); + } else if (end > viewportLength) { + adjustPixels(end - viewportLength); + } + } + } + + public void show(int index) { + T cell = getVisibleCell(index); + if (cell != null) { + show(cell); + } else { + // See if the previous index is a visible cell + T prev = getVisibleCell(index - 1); + if (prev != null) { + // Need to add a new cell and then we can show it +// layingOut = true; + cell = getAvailableCell(index); + setCellIndex(cell, index); + resizeCellSize(cell); // resize must be after config + cells.addLast(cell); + positionCell(cell, getCellPosition(prev) + getCellLength(prev)); + setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell))); + cell.setVisible(true); + show(cell); +// layingOut = false; + return; + } + // See if the next index is a visible cell + T next = getVisibleCell(index + 1); + if (next != null) { +// layingOut = true; + cell = getAvailableCell(index); + setCellIndex(cell, index); + resizeCellSize(cell); // resize must be after config + cells.addFirst(cell); + positionCell(cell, getCellPosition(next) - getCellLength(cell)); + setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell))); + cell.setVisible(true); + show(cell); +// layingOut = false; + return; + } + + // In this case, we're asked to show a random cell +// layingOut = true; + adjustPositionToIndex(index); + addAllToPile(); + requestLayout(); +// layingOut = false; + } + } + + public void scrollTo(int index) { + boolean posSet = false; + + if (index >= cellCount - 1) { + setPosition(1); + posSet = true; + } else if (index < 0) { + setPosition(0); + posSet = true; + } + + if (!posSet) { + adjustPositionToIndex(index); + double offset = -computeOffsetForCell(index); + adjustByPixelAmount(offset); + } + + requestLayout(); + } + + //TODO We assume all the cell have the same length. We will need to support + // cells of different lengths. + public void scrollToOffset(int offset) { + adjustPixels(offset * getCellLength(0)); + } + + /** + * Given a delta value representing a number of pixels, this method attempts + * to move the VirtualFlow in the given direction (positive is down/right, + * negative is up/left) the given number of pixels. It returns the number of + * pixels actually moved. + */ + public double adjustPixels(final double delta) { + // Short cut this method for cases where nothing should be done + if (delta == 0) return 0; + + final boolean isVertical = isVertical(); + if (((isVertical && (tempVisibility ? !needLengthBar : !vbar.isVisible())) || + (!isVertical && (tempVisibility ? !needLengthBar : !hbar.isVisible())))) return 0; + + double pos = getPosition(); + if (pos == 0.0f && delta < 0) return 0; + if (pos == 1.0f && delta > 0) return 0; + + adjustByPixelAmount(delta); + if (pos == getPosition()) { + // The pos hasn't changed, there's nothing to do. This is likely + // to occur when we hit either extremity + return 0; + } + + // Now move stuff around. Translating by pixels fundamentally means + // moving the cells by the delta. However, after having + // done that, we need to go through the cells and see which cells, + // after adding in the translation factor, now fall off the viewport. + // Also, we need to add cells as appropriate to the end (or beginning, + // depending on the direction of travel). + // + // One simplifying assumption (that had better be true!) is that we + // will only make it this far in the function if the virtual scroll + // bar is visible. Otherwise, we never will pixel scroll. So as we go, + // if we find that the maxPrefBreadth exceeds the viewportBreadth, + // then we will be sure to show the breadthBar and update it + // accordingly. + if (cells.size() > 0) { + double position; + for (int i = 0; i < cells.size(); i++) { + T cell = cells.get(i); + assert cell != null; + position = getCellPosition(cell) - delta; + positionCell(cell, position); + } + + // Fix for RT-32908 + T firstCell = cells.getFirst(); + double layoutY = firstCell == null ? 0 : getCellPosition(firstCell); + for (int i = 0; i < cells.size(); i++) { + T cell = cells.get(i); + assert cell != null; + double actualLayoutY = getCellPosition(cell); + if (actualLayoutY != layoutY) { + // we need to shift the cell to layoutY + positionCell(cell, layoutY); + } + + layoutY += getCellLength(cell); + } + // end of fix for RT-32908 + cull(); + firstCell = cells.getFirst(); + + // Add any necessary leading cells + if (firstCell != null) { + int firstIndex = getCellIndex(firstCell); + double prevIndexSize = getCellLength(firstIndex - 1); + addLeadingCells(firstIndex - 1, getCellPosition(firstCell) - prevIndexSize); + } else { + int currentIndex = computeCurrentIndex(); + + // The distance from the top of the viewport to the top of the + // cell for the current index. + double offset = -computeViewportOffset(getPosition()); + + // Add all the leading and trailing cells (the call to add leading + // cells will add the current cell as well -- that is, the one that + // represents the current position on the mapper). + addLeadingCells(currentIndex, offset); + } + + // Starting at the tail of the list, loop adding cells until + // all the space on the table is filled up. We want to make + // sure that we DO NOT add empty trailing cells (since we are + // in the full virtual case and so there are no trailing empty + // cells). + if (!addTrailingCells(false)) { + // Reached the end, but not enough cells to fill up to + // the end. So, remove the trailing empty space, and translate + // the cells down + final T lastCell = getLastVisibleCell(); + final double lastCellSize = getCellLength(lastCell); + final double cellEnd = getCellPosition(lastCell) + lastCellSize; + final double viewportLength = getViewportLength(); + + if (cellEnd < viewportLength) { + // Reposition the nodes + double emptySize = viewportLength - cellEnd; + for (int i = 0; i < cells.size(); i++) { + T cell = cells.get(i); + positionCell(cell, getCellPosition(cell) + emptySize); + } + setPosition(1.0f); + // fill the leading empty space + firstCell = cells.getFirst(); + int firstIndex = getCellIndex(firstCell); + double prevIndexSize = getCellLength(firstIndex - 1); + addLeadingCells(firstIndex - 1, getCellPosition(firstCell) - prevIndexSize); + } + } + } + + // Now throw away any cells that don't fit + cull(); + + // Finally, update the scroll bars + updateScrollBarsAndCells(false); + lastPosition = getPosition(); + + // notify + return delta; // TODO fake + } + + private boolean needsReconfigureCells = false; // when cell contents are the same + private boolean needsRecreateCells = false; // when cell factory changed + private boolean needsRebuildCells = false; // when cell contents have changed + private boolean needsCellsLayout = false; + private boolean sizeChanged = false; + private final BitSet dirtyCells = new BitSet(); + + public void reconfigureCells() { + needsReconfigureCells = true; + requestLayout(); + } + + public void recreateCells() { + needsRecreateCells = true; + requestLayout(); + } + + public void rebuildCells() { + needsRebuildCells = true; + requestLayout(); + } + + public void requestCellLayout() { + needsCellsLayout = true; + requestLayout(); + } + + public void setCellDirty(int index) { + dirtyCells.set(index); + requestLayout(); + } + + private static final double GOLDEN_RATIO_MULTIPLIER = 0.618033987; + + private double getPrefBreadth(double oppDimension) { + double max = getMaxCellWidth(10); + + // This primarily exists for the case where we do not want the breadth + // to grow to ensure a golden ratio between width and height (for example, + // when a ListView is used in a ComboBox - the width should not grow + // just because items are being added to the ListView) + if (oppDimension > -1) { + double prefLength = getPrefLength(); + max = Math.max(max, prefLength * GOLDEN_RATIO_MULTIPLIER); + } + + return max; + } + + private double getPrefLength() { + double sum = 0.0; + int rows = Math.min(10, cellCount); + for (int i = 0; i < rows; i++) { + sum += getCellLength(i); + } + return sum; + } + + @Override + protected double computePrefWidth(double height) { + double w = isVertical() ? getPrefBreadth(height) : getPrefLength(); + return w + vbar.prefWidth(-1); + } + + @Override + protected double computePrefHeight(double width) { + double h = isVertical() ? getPrefLength() : getPrefBreadth(width); + return h + hbar.prefHeight(-1); + } + + double getMaxCellWidth(int rowsToCount) { + double max = 0.0; + + // we always measure at least one row + int rows = Math.max(1, rowsToCount == -1 ? cellCount : rowsToCount); + for (int i = 0; i < rows; i++) { + max = Math.max(max, getCellBreadth(i)); + } + return max; + } + + + // Old PositionMapper + + /** + * Given a position value between 0 and 1, compute and return the viewport + * offset from the "current" cell associated with that position value. + * That is, if the return value of this function where used as a translation + * factor for a sheet that contained all the items, then the current + * item would end up positioned correctly. + */ + private double computeViewportOffset(double position) { + double p = com.sun.javafx.util.Utils.clamp(0, position, 1); + double fractionalPosition = p * getCellCount(); + int cellIndex = (int) fractionalPosition; + double fraction = fractionalPosition - cellIndex; + double cellSize = getCellLength(cellIndex); + double pixelOffset = cellSize * fraction; + double viewportOffset = getViewportLength() * p; + return pixelOffset - viewportOffset; + } + + private void adjustPositionToIndex(int index) { + int cellCount = getCellCount(); + if (cellCount <= 0) { + setPosition(0.0f); + } else { + setPosition(((double) index) / cellCount); + } + } + + /** + * Adjust the position based on a delta of pixels. If negative, then the + * position will be adjusted negatively. If positive, then the position will + * be adjusted positively. If the pixel amount is too great for the range of + * the position, then it will be clamped such that position is always + * strictly between 0 and 1 + */ + private void adjustByPixelAmount(double numPixels) { + if (numPixels == 0) return; + // Starting from the current cell, we move in the direction indicated + // by numPixels one cell at a team. For each cell, we discover how many + // pixels the "position" line would move within that cell, and adjust + // our count of numPixels accordingly. When we come to the "final" cell, + // then we can take the remaining number of pixels and multiply it by + // the "travel rate" of "p" within that cell to get the delta. Add + // the delta to "p" to get position. + + // get some basic info about the list and the current cell + boolean forward = numPixels > 0; + int cellCount = getCellCount(); + double fractionalPosition = getPosition() * cellCount; + int cellIndex = (int) fractionalPosition; + if (forward && cellIndex == cellCount) return; + double cellSize = getCellLength(cellIndex); + double fraction = fractionalPosition - cellIndex; + double pixelOffset = cellSize * fraction; + + // compute the percentage of "position" that represents each cell + double cellPercent = 1.0 / cellCount; + + // To help simplify the algorithm, we pretend as though the current + // position is at the beginning of the current cell. This reduces some + // of the corner cases and provides a simpler algorithm without adding + // any overhead to performance. + double start = computeOffsetForCell(cellIndex); + double end = cellSize + computeOffsetForCell(cellIndex + 1); + + // We need to discover the distance that the fictional "position line" + // would travel within this cell, from its current position to the end. + double remaining = end - start; + + // Keep track of the number of pixels left to travel + double n = forward ? + numPixels + pixelOffset - (getViewportLength() * getPosition()) - start + : -numPixels + end - (pixelOffset - (getViewportLength() * getPosition())); + + // "p" represents the most recent value for position. This is always + // based on the edge between two cells, except at the very end of the + // algorithm where it is added to the computed "p" offset for the final + // value of Position. + double p = cellPercent * cellIndex; + + // Loop over the cells one at a time until either we reach the end of + // the cells, or we find that the "n" will fall within the cell we're on + while (n > remaining && ((forward && cellIndex < cellCount - 1) || (!forward && cellIndex > 0))) { + if (forward) cellIndex++; + else cellIndex--; + n -= remaining; + cellSize = getCellLength(cellIndex); + start = computeOffsetForCell(cellIndex); + end = cellSize + computeOffsetForCell(cellIndex + 1); + remaining = end - start; + p = cellPercent * cellIndex; + } + + // if remaining is < n, then we must have hit an end, so as a + // fast path, we can just set position to 1.0 or 0.0 and return + // because we know we hit the end + if (n > remaining) { + setPosition(forward ? 1.0f : 0.0f); + } else if (forward) { + double rate = cellPercent / Math.abs(end - start); + setPosition(p + (rate * n)); + } else { + double rate = cellPercent / Math.abs(end - start); + setPosition((p + cellPercent) - (rate * n)); + } + } + + private int computeCurrentIndex() { + return (int) (getPosition() * getCellCount()); + } + + /** + * Given an item index, this function will compute and return the viewport + * offset from the beginning of the specified item. Notice that because each + * item has the same percentage of the position dedicated to it, and since + * we are measuring from the start of each item, this is a very simple + * calculation. + */ + private double computeOffsetForCell(int itemIndex) { + double cellCount = getCellCount(); + double p = com.sun.javafx.util.Utils.clamp(0, itemIndex, cellCount) / cellCount; + return -(getViewportLength() * p); + } + +// /** +// * Adjust the position based on a chunk of pixels. The position is based +// * on the start of the scrollbar position. +// */ +// private void adjustByPixelChunk(double numPixels) { +// setPosition(0); +// adjustByPixelAmount(numPixels); +// } + // end of old PositionMapper code + + + /** + * A simple extension to Region that ensures that anything wanting to flow + * outside of the bounds of the Region is clipped. + */ + static class ClippedContainer extends Region { + + /** + * The Node which is embedded within this {@code ClipView}. + */ + private Node node; + + public Node getNode() { + return this.node; + } + + public void setNode(Node n) { + this.node = n; + + getChildren().clear(); + getChildren().add(node); + } + + public void setClipX(double clipX) { + setLayoutX(-clipX); + clipRect.setLayoutX(clipX); + } + + public void setClipY(double clipY) { + setLayoutY(-clipY); + clipRect.setLayoutY(clipY); + } + + private final Rectangle clipRect; + + public ClippedContainer(final VirtualPageFlow flow) { + if (flow == null) { + throw new IllegalArgumentException("VirtualFlow can not be null"); + } + + getStyleClass().add("clipped-container"); + + // clipping + clipRect = new Rectangle(); + clipRect.setSmooth(false); + setClip(clipRect); + // --- clipping + + super.widthProperty().addListener(valueModel -> { + clipRect.setWidth(getWidth()); + }); + super.heightProperty().addListener(valueModel -> { + clipRect.setHeight(getHeight()); + }); + } + } + + /** + * A List-like implementation that is exceedingly efficient for the purposes + * of the VirtualFlow. Typically there is not much variance in the number of + * cells -- it is always some reasonably consistent number. Yet for efficiency + * in code, we like to use a linked list implementation so as to append to + * start or append to end. However, at times when we need to iterate, LinkedList + * is expensive computationally as well as requiring the construction of + * temporary iterators. + *

+ * This linked list like implementation is done using an array. It begins by + * putting the first item in the center of the allocated array, and then grows + * outward (either towards the first or last of the array depending on whether + * we are inserting at the head or tail). It maintains an index to the start + * and end of the array, so that it can efficiently expose iteration. + *

+ * This class is package private solely for the sake of testing. + */ + public static class ArrayLinkedList extends AbstractList { + /** + * The array list backing this class. We default the size of the array + * list to be fairly large so as not to require resizing during normal + * use, and since that many ArrayLinkedLists won't be created it isn't + * very painful to do so. + */ + private final ArrayList array; + + private int firstIndex = -1; + private int lastIndex = -1; + + public ArrayLinkedList() { + array = new ArrayList(50); + + for (int i = 0; i < 50; i++) { + array.add(null); + } + } + + public T getFirst() { + return firstIndex == -1 ? null : array.get(firstIndex); + } + + public T getLast() { + return lastIndex == -1 ? null : array.get(lastIndex); + } + + public void addFirst(T cell) { + // if firstIndex == -1 then that means this is the first item in the + // list and we need to initialize firstIndex and lastIndex + if (firstIndex == -1) { + firstIndex = lastIndex = array.size() / 2; + array.set(firstIndex, cell); + } else if (firstIndex == 0) { + // we're already at the head of the array, so insert at position + // 0 and then increment the lastIndex to compensate + array.add(0, cell); + lastIndex++; + } else { + // we're not yet at the head of the array, so insert at the + // firstIndex - 1 position and decrement first position + array.set(--firstIndex, cell); + } + } + + public void addLast(T cell) { + // if lastIndex == -1 then that means this is the first item in the + // list and we need to initialize the firstIndex and lastIndex + if (firstIndex == -1) { + firstIndex = lastIndex = array.size() / 2; + array.set(lastIndex, cell); + } else if (lastIndex == array.size() - 1) { + // we're at the end of the array so need to "add" so as to force + // the array to be expanded in size + array.add(++lastIndex, cell); + } else { + array.set(++lastIndex, cell); + } + } + + public int size() { + return firstIndex == -1 ? 0 : lastIndex - firstIndex + 1; + } + + public boolean isEmpty() { + return firstIndex == -1; + } + + public T get(int index) { + if (index > (lastIndex - firstIndex) || index < 0) { + // Commented out exception due to RT-29111 + // throw new java.lang.ArrayIndexOutOfBoundsException(); + return null; + } + + return array.get(firstIndex + index); + } + + public void clear() { + for (int i = 0; i < array.size(); i++) { + array.set(i, null); + } + + firstIndex = lastIndex = -1; + } + + public T removeFirst() { + if (isEmpty()) return null; + return remove(0); + } + + public T removeLast() { + if (isEmpty()) return null; + return remove(lastIndex - firstIndex); + } + + public T remove(int index) { + if (index > lastIndex - firstIndex || index < 0) { + throw new java.lang.ArrayIndexOutOfBoundsException(); + } + + // if the index == 0, then we're removing the first + // item and can simply set it to null in the array and increment + // the firstIndex unless there is only one item, in which case + // we have to also set first & last index to -1. + if (index == 0) { + T cell = array.get(firstIndex); + array.set(firstIndex, null); + if (firstIndex == lastIndex) { + firstIndex = lastIndex = -1; + } else { + firstIndex++; + } + return cell; + } else if (index == lastIndex - firstIndex) { + // if the index == lastIndex - firstIndex, then we're removing the + // last item and can simply set it to null in the array and + // decrement the lastIndex + T cell = array.get(lastIndex); + array.set(lastIndex--, null); + return cell; + } else { + // if the index is somewhere in between, then we have to remove the + // item and decrement the lastIndex + T cell = array.get(firstIndex + index); + array.set(firstIndex + index, null); + for (int i = (firstIndex + index + 1); i <= lastIndex; i++) { + array.set(i - 1, array.get(i)); + } + array.set(lastIndex--, null); + return cell; + } + } + } + + Timeline sbTouchTimeline; + KeyFrame sbTouchKF1; + KeyFrame sbTouchKF2; + + private boolean needBreadthBar; + private boolean needLengthBar; + private boolean tempVisibility = false; + + protected void startSBReleasedAnimation() { + if (sbTouchTimeline == null) { + /* + ** timeline to leave the scrollbars visible for a short + ** while after a scroll/drag + */ + sbTouchTimeline = new Timeline(); + sbTouchKF1 = new KeyFrame(Duration.millis(0), event -> { + tempVisibility = true; + requestLayout(); + }); + + sbTouchKF2 = new KeyFrame(Duration.millis(1000), event -> { + if (touchDetected == false && mouseDown == false) { + tempVisibility = false; + requestLayout(); + } + }); + sbTouchTimeline.getKeyFrames().addAll(sbTouchKF1, sbTouchKF2); + } + sbTouchTimeline.playFromStart(); + } + + protected void scrollBarOn() { + tempVisibility = true; + requestLayout(); + } +} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualScrollBar.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualScrollBar.java new file mode 100644 index 000000000..2319ba557 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualScrollBar.java @@ -0,0 +1,92 @@ +package com.icepdf.fx.scene.control.skin; + + +import com.icepdf.fx.scene.control.PageView; +import com.sun.javafx.util.Utils; +import javafx.scene.control.ScrollBar; + +/** + * This custom ScrollBar is used to map the increment & decrement features + * to pixel based scrolling rather than thumb/track based scrolling, if the + * "virtual" attribute is true. + */ +public class VirtualScrollBar extends ScrollBar { + private final VirtualPageFlow flow; + + private boolean virtual; + + private boolean adjusting; + + public VirtualScrollBar(final VirtualPageFlow flow) { + this.flow = flow; + + super.valueProperty().addListener(valueModel -> { + if (isVirtual()/* && oldValue != newValue*/) { + if (adjusting) { + // no-op + } else { + flow.setPosition(getValue()); + } + } + }); + } + + public void setVirtual(boolean virtual) { + this.virtual = virtual; + } + + public boolean isVirtual() { + return this.virtual; + } + + @Override + public void decrement() { + if (isVirtual()) { + flow.adjustPixels(-10); + } else { + super.decrement(); + } + } + + @Override + public void increment() { + if (isVirtual()) { + flow.adjustPixels(10); + } else { + super.increment(); + } + } + +// private double lastAdjustValue = 0.0; + + // this method is called when the user clicks in the scrollbar track, so + // we special-case it to allow for page-up and page-down clicking to work + // as expected. + @Override + public void adjustValue(double pos) { + if (isVirtual()) { +// if (pos == lastAdjustValue) { +// return; +// } + + adjusting = true; + double oldValue = flow.getPosition(); + + double newValue = ((getMax() - getMin()) * Utils.clamp(0, pos, 1)) + getMin(); + if (newValue < oldValue) { + PageView cell = flow.getFirstVisibleCell(); + if (cell == null) return; + flow.showAsLast(cell); + } else if (newValue > oldValue) { + PageView cell = flow.getLastVisibleCell(); + if (cell == null) return; + flow.showAsFirst(cell); + } +// lastAdjustValue = pos; + + adjusting = false; + } else { + super.adjustValue(pos); + } + } +} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/util/ImageLoader.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/util/ImageLoader.java new file mode 100644 index 000000000..71918107b --- /dev/null +++ b/viewer/viewer-fx/src/main/java/com/icepdf/fx/util/ImageLoader.java @@ -0,0 +1,14 @@ +package com.icepdf.fx.util; + +import javafx.scene.image.Image; + +/** + * Aid for loading image from image resources path. + */ +public class ImageLoader { + + public static Image loadImage(String filename) { + return new Image(filename.getClass().getResourceAsStream( + "/com/icepdf/fx/images/" + filename)); + } +} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/util/SettingsLoaderTask.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/util/SettingsLoaderTask.java new file mode 100644 index 000000000..a397acf2f --- /dev/null +++ b/viewer/viewer-fx/src/main/java/com/icepdf/fx/util/SettingsLoaderTask.java @@ -0,0 +1,43 @@ +package com.icepdf.fx.util; + +import com.icepdf.core.util.FontPropertiesManager; +import com.icepdf.core.util.PropertiesManager; +import javafx.concurrent.Task; + +import java.util.ResourceBundle; + +/** + * + */ +public class SettingsLoaderTask extends Task { + + @Override + protected Object call() throws Exception { + ResourceBundle messageBundle = ResourceBundle.getBundle( + PropertiesManager.DEFAULT_MESSAGE_BUNDLE); + FontPropertiesManager fontPropertiesManager = FontPropertiesManager.getInstance(); + + if (fontPropertiesManager.isPropertiesEmpty()) { + updateMessage(messageBundle.getString( + "icepdf.fx.common.utility.SettingsLoaderTask.loadingFonts.label")); + updateProgress(-1, 10); + fontPropertiesManager.readDefaultProperties(); + fontPropertiesManager.updateProperties(); + updateProgress(10, 10); + updateMessage(messageBundle.getString( + "icepdf.fx.common.utility.SettingsLoaderTask.loadingFontsComplete.label")); + } else { + updateMessage(messageBundle.getString( + "icepdf.fx.common.utility.SettingsLoaderTask.loadingSettings.label")); + fontPropertiesManager.loadProperties(); + for (int i = 0, max = 10; i < 10; i++) { + updateProgress(i + 1, max); + Thread.sleep(10); + } +// fontPropertiesManager.clearProperties(); + updateMessage(messageBundle.getString( + "icepdf.fx.common.utility.SettingsLoaderTask.loadingSettingsComplete.label")); + } + return null; + } +} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/DocumentViewTest.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/DocumentViewTest.java new file mode 100644 index 000000000..3b44f1b51 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/DocumentViewTest.java @@ -0,0 +1,32 @@ +package com.icepdf.fx.viewer; + +import com.icepdf.fx.scene.control.DocumentView; +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.stage.Stage; +import org.icepdf.core.pobjects.Document; + +/** + * + */ +public class DocumentViewTest extends Application { + + public static void main(String[] args) throws Exception { + launch(args); + } + + @Override + public void start(Stage primaryStage) throws Exception { + + Document document = new Document(); + + document.setFile("d:\\pdf-qa\\pdf_reference_addendum_redaction.pdf"); + + Scene myScene = new Scene(new DocumentView(document)); + primaryStage.setScene(myScene); + + primaryStage.setWidth(600); + primaryStage.setHeight(400); + primaryStage.show(); + } +} \ No newline at end of file diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/Launcher.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/Launcher.java new file mode 100644 index 000000000..db1b82cdd --- /dev/null +++ b/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/Launcher.java @@ -0,0 +1,208 @@ +package com.icepdf.fx.viewer; + +import com.icepdf.core.util.PropertiesManager; +import com.icepdf.fx.scene.control.DocumentView; +import com.icepdf.fx.util.ImageLoader; +import javafx.collections.ObservableList; +import javafx.geometry.Rectangle2D; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.layout.BorderPane; +import javafx.stage.Screen; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import org.icepdf.core.exceptions.PDFException; +import org.icepdf.core.exceptions.PDFSecurityException; +import org.icepdf.core.pobjects.Document; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Path; +import java.util.ResourceBundle; +import java.util.prefs.Preferences; + +/** + * Uses the build class to assemble full viewer RI. + */ +public class Launcher { + + private static Preferences prefs = Preferences.userNodeForPackage(Launcher.class); + private static String VIEWER_WINDOW_WIDTH = "last_used_width"; + private static String VIEWER_WINDOW_HEIGHT = "last_used_width"; + private static String VIEWER_WINDOW_X = "last_used_X"; + private static String VIEWER_WINDOW_Y = "last_used_Y"; + + private static Launcher launcher; + +// private PropertiesManager properties; + +// private ArrayList controllers; + +// private long newWindowInvocationCounter = 0; + +// private ResourceBundle messageBundle = null; + + private Launcher() { + } + + public static Launcher getInstance() { + if (launcher == null) { + launcher = new Launcher(); + } + return launcher; + } + + public Stage newViewerWindow() { + ResourceBundle messageBundle = ResourceBundle.getBundle( + PropertiesManager.DEFAULT_MESSAGE_BUNDLE); + + Stage mainStage = new Stage(StageStyle.DECORATED); + mainStage.setTitle(messageBundle.getString("icepdf.fx.viewer.ViewerPane.default.title")); + mainStage.getIcons().addAll( + ImageLoader.loadImage("app_icon_128x128.png"), + ImageLoader.loadImage("app_icon_64x64.png"), + ImageLoader.loadImage("app_icon_48x48.png"), + ImageLoader.loadImage("app_icon_32x32.png"), + ImageLoader.loadImage("app_icon_16x16.png")); + + + BorderPane borderPane = new BorderPane(); + + // zoom controls. + Slider zoomSlider = new Slider(0.05, 8, 1.0); + zoomSlider.setSnapToTicks(true); + zoomSlider.setMajorTickUnit(0.05); + zoomSlider.setBlockIncrement(0.05); + zoomSlider.setMinorTickCount(0); + zoomSlider.setShowTickLabels(false); + zoomSlider.setShowTickMarks(false); + Button zoomInButton = new Button("-"); + zoomInButton.setOnAction(event -> zoomSlider.decrement()); + Button zoomOutButton = new Button("+"); + zoomOutButton.setOnAction(event -> zoomSlider.increment()); + + // rotation controls + Slider rotationSlider = new Slider(-180, 180, 0); + rotationSlider.setSnapToTicks(true); + rotationSlider.setMajorTickUnit(15); + rotationSlider.setBlockIncrement(15); + rotationSlider.setMinorTickCount(0); + rotationSlider.setShowTickLabels(false); + rotationSlider.setShowTickMarks(false); + Button rotateLeftButton = new Button("-"); + rotateLeftButton.setOnAction(event -> rotationSlider.decrement()); + Button rotateRightButton = new Button("+"); + rotateRightButton.setOnAction(event -> rotationSlider.increment()); + + // view mode controls + + Document document = new Document(); + + try { +// document.setFile("E:\\pdf-qa\\metrics\\content-parser\\ottawa_cycling_map.pdf"); +// document.setFile("E:\\pdf-qa\\metrics\\content-parser\\map.pdf"); +// document.setFile("E:\\pdf-qa\\metrics\\content-parser\\SF_923200345630.pdf"); +// document.setFile("E:\\pdf-qa\\PDF32000_2008.pdf"); +// document.setFile("d:\\pdf-qa\\fonts\\cid\\R&D-05-Carbon.pdf"); + document.setFile("d:\\pdf-qa\\metrics\\full-monty\\fcom.pdf"); +// document.setFile("E:\\pdf-qa\\metrics\\full-monty\\ACEASPA_LIBGIO_2013_00001_53923_000001_0703467_V3.pdf"); + } catch (PDFException e) { + e.printStackTrace(); + } catch (PDFSecurityException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + // main view model + DocumentView documentView = new DocumentView(document); + + // setup scale binding for touch and wheel mouse interaction + documentView.scaleProperty().bindBidirectional(zoomSlider.valueProperty()); + documentView.scaleIncrementValueProperty().bind(zoomSlider.blockIncrementProperty()); + documentView.scaleMaxValueProperty().bind(zoomSlider.minProperty()); + documentView.scaleMaxValueProperty().bind(zoomSlider.maxProperty()); + + // rotation slider binding. + documentView.rotationProperty().bind(rotationSlider.valueProperty()); + borderPane.setCenter(documentView); + + // setup view mode controls. + + // but the toolbar together + ToolBar toolbar = new ToolBar(); + toolbar.getItems().addAll( + new Label("Scale"), zoomInButton, zoomSlider, zoomOutButton, + new Separator(), + new Label("Rotation"), rotateLeftButton, rotationSlider, rotateRightButton); + borderPane.setTop(toolbar); + + mainStage.setScene(new Scene(borderPane)); + calculateStageLocation(mainStage); + mainStage.setOnCloseRequest(event -> { + prefs.putDouble(VIEWER_WINDOW_X, mainStage.getX()); + prefs.putDouble(VIEWER_WINDOW_Y, mainStage.getY()); + prefs.putDouble(VIEWER_WINDOW_WIDTH, mainStage.getWidth()); + prefs.putDouble(VIEWER_WINDOW_HEIGHT, mainStage.getHeight()); + }); + mainStage.show(); + mainStage.toFront(); + + return mainStage; + } + + public Stage newViewerWindow(Path path) { + return newViewerWindow(); + } + + public Stage newViewerWindow(URL url) { + return newViewerWindow(); + } + + public void disposeViewerWindow(Object controller, Scene scene) { + + } + + public void minimiseAllViewerWindows() { + + } + +// public void bringAllViewerWindowsToFront(Object frontMost){ +// +// } +// +// public void bringWindowToFront(int index); + +// public List getWindowDocumentOriginList(Controller giveIndex); + + public void quit(Object controller, Scene viewer) { + + } + + private void calculateStageLocation(Stage stage) { + final Rectangle2D bounds = Screen.getPrimary().getBounds(); + + double width = prefs.getDouble(VIEWER_WINDOW_WIDTH, 800); + double height = prefs.getDouble(VIEWER_WINDOW_HEIGHT, 600); + + // default center for width on primary screen. + double x = bounds.getMinX() + bounds.getWidth() / 2 - width / 2; + double y = bounds.getMinY() + bounds.getHeight() / 2 - height / 2; + + double previousX = prefs.getDouble(VIEWER_WINDOW_X, x); + double previousY = prefs.getDouble(VIEWER_WINDOW_Y, y); + + // quick check to make sure the viewer will be visable in at least one screen, if not we default to primary + ObservableList screens = Screen.getScreensForRectangle(previousX, previousY, width, height); + if (screens.size() == 0) { + previousX = x; + previousY = y; + } + + stage.setWidth(width); + stage.setHeight(height); + stage.setX(previousX); + stage.setY(previousY); + } + +} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/ListViewTest.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/ListViewTest.java new file mode 100644 index 000000000..487b6c072 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/ListViewTest.java @@ -0,0 +1,36 @@ +package com.icepdf.fx.viewer; + +import javafx.application.Application; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.Scene; +import javafx.scene.control.ListView; +import javafx.stage.Stage; + +/** + * Created by pcorl_000 on 2017-03-30. + */ +public class ListViewTest extends Application { + + public static void main(String[] args) throws Exception { + launch(args); + } + + @Override + public void start(Stage primaryStage) throws Exception { + ListView listView = new ListView<>(); + ObservableList list = FXCollections.observableArrayList(); + for (int i = 0; i < 3; i++) { + list.add("string cell " + (i + 1)); + } + + listView.setItems(list); + + Scene myScene = new Scene(listView); + primaryStage.setScene(myScene); + + primaryStage.setWidth(600); + primaryStage.setHeight(400); + primaryStage.show(); + } +} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/Main.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/Main.java new file mode 100644 index 000000000..458645311 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/Main.java @@ -0,0 +1,256 @@ +package com.icepdf.fx.viewer; + +import com.icepdf.core.util.PropertiesManager; +import com.icepdf.fx.util.ImageLoader; +import com.icepdf.fx.util.SettingsLoaderTask; +import javafx.animation.FadeTransition; +import javafx.application.Application; +import javafx.concurrent.Task; +import javafx.concurrent.Worker; +import javafx.geometry.Rectangle2D; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressBar; +import javafx.scene.effect.DropShadow; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.stage.Screen; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.util.Duration; + +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.MessageFormat; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.logging.Logger; + +/** + * The main class is responsible for displaying the splash screen and doing any settings loading on a separate thread. + * When the viewer ri is launched for the first time the font manager will read the systems fonts which can take + * quite a bit of time at which a indeterminate progress bar is used to show that the scan is taking place. + * + * @since 6.5 + */ +public class Main extends Application { + + private static final Logger logger = Logger.getLogger(Main.class.toString()); + + private static final int SPLASH_WIDTH = 640; + private static final int SPLASH_HEIGHT = 400; + private static final String ARGS_FILE_PARAMTER = "-loadfile"; + private static final String ARGS_URL_PARAMTER = "-loadurl"; + private static final String JNLP_FILE_PARAMTER = "loadfile"; + private static final String JNLP_URL_PARAMTER = "Loadurl"; + + private static ResourceBundle messageBundle; + + private Pane splashPane; + private ProgressBar loadProgress; + private Label progressText; + private Stage initStage; + + public static void main(String[] args) throws Exception { + launch(args); + } + + @Override + public void init() { + // load message bundle + messageBundle = ResourceBundle.getBundle( + PropertiesManager.DEFAULT_MESSAGE_BUNDLE); + + splashPane = new StackPane(); + splashPane.getStylesheets().add( + getClass().getResource(PropertiesManager.DEFAULT_SPLASH_CSS).toExternalForm()); + ImageView splashImageView = new ImageView(); + splashImageView.setId("splash-image"); + loadProgress = new ProgressBar(); + loadProgress.setId("progress-bar"); + progressText = new Label("Will find friends for peanuts . . ."); + progressText.getStyleClass().add("progress-label"); + + BorderPane splashContentPane = new BorderPane(); + splashPane.getChildren().addAll(splashImageView, splashContentPane); + + // padding is used to move the progress bar to desired location + Pane progressPane = new VBox(); + progressPane.getStyleClass().add("progress-pane"); + progressPane.getChildren().addAll(progressText, loadProgress); + splashContentPane.setCenter(progressPane); + + // licence values. + Pane licencePane = new VBox(); + licencePane.getStyleClass().add("license-pane"); + Label copyrightLabel = new Label(messageBundle.getString("icepdf.fx.viewer.Main.copyRight.label")); + copyrightLabel.getStyleClass().add("copyright-label"); + licencePane.getChildren().addAll(copyrightLabel); + splashContentPane.setBottom(licencePane); + splashPane.getStyleClass().add("splash-layout"); + splashPane.setEffect(new DropShadow()); + } + + @Override + public void start(final Stage initStage) throws Exception { + this.initStage = initStage; + + initStage.getIcons().addAll( + ImageLoader.loadImage("app_icon_128x128.png"), + ImageLoader.loadImage("app_icon_64x64.png"), + ImageLoader.loadImage("app_icon_48x48.png"), + ImageLoader.loadImage("app_icon_32x32.png"), + ImageLoader.loadImage("app_icon_16x16.png")); + SettingsLoaderTask fontReaderTask = new SettingsLoaderTask(); + showSplash(initStage, fontReaderTask, + this::showMainStage); + new Thread(fontReaderTask).start(); + } + + private void showMainStage() { + + Launcher launcher = Launcher.getInstance(); + // check for -loadfile or -loadurl. + Map jnlpArgs = getParameters().getNamed(); + List args = getParameters().getUnnamed(); + if (jnlpArgs.size() > 0) { + processJNLPArguments(launcher, jnlpArgs); + } else if (args.size() > 0) { + processArguments(launcher, args); + } else { + launcher.newViewerWindow(); + } + } + + private void showSplash( + final Stage initStage, + Task task, + InitCompletionHandler initCompletionHandler) { + progressText.textProperty().bind(task.messageProperty()); + loadProgress.progressProperty().bind(task.progressProperty()); + task.stateProperty().addListener((observableValue, oldState, newState) -> { + if (newState == Worker.State.SUCCEEDED) { + loadProgress.progressProperty().unbind(); + loadProgress.setProgress(1); + initStage.toFront(); + FadeTransition fadeSplash = new FadeTransition(Duration.seconds(1.2), splashPane); + fadeSplash.setFromValue(1.0); + fadeSplash.setToValue(0.0); + fadeSplash.setOnFinished(actionEvent -> initStage.hide()); + fadeSplash.play(); + // setup callback to start the main application state + initCompletionHandler.complete(); + } + }); + // center the splash screen on the primary monitory + Scene splashScene = new Scene(splashPane, Color.TRANSPARENT); + initStage.initStyle(StageStyle.TRANSPARENT); +// initStage.setAlwaysOnTop(true); + initStage.setScene(splashScene); + final Rectangle2D bounds = Screen.getPrimary().getBounds(); + initStage.setX(bounds.getMinX() + bounds.getWidth() / 2 - SPLASH_WIDTH / 2); + initStage.setY(bounds.getMinY() + bounds.getHeight() / 2 - SPLASH_HEIGHT / 2); + initStage.initStyle(StageStyle.TRANSPARENT); + initStage.show(); + } + + public interface InitCompletionHandler { + void complete(); + } + + private void processJNLPArguments(Launcher launcher, Map jnlpArgs) { + String file = null; + String url = null; + if (jnlpArgs.containsKey(JNLP_FILE_PARAMTER)) { + file = jnlpArgs.get(JNLP_FILE_PARAMTER).trim(); + } else if (jnlpArgs.containsKey(JNLP_URL_PARAMTER)) { + url = jnlpArgs.get(JNLP_URL_PARAMTER).trim(); + } + // load default empty viewer. + if (file == null && url == null) { + launcher.newViewerWindow(); + } + if (file != null) { + checkAndLaunchFile(launcher, file); + } + if (url != null) { + checkAndLaunchUrl(launcher, url); + } + } + + private void processArguments(Launcher launcher, List args) { + if (args.size() == 2) { + String command = args.get(0); + String uri = args.get(1); + if (ARGS_FILE_PARAMTER.equals(command)) { + checkAndLaunchFile(launcher, uri); + } else if (ARGS_URL_PARAMTER.equals(command)) { + checkAndLaunchUrl(launcher, uri); + } + } else { + launcher.newViewerWindow(); + } + } + + private void checkAndLaunchFile(Launcher launcher, String file) { + try { + Path filePath = Paths.get(file); + boolean exists = Files.exists(filePath); + if (exists) { + launcher.newViewerWindow(filePath); + } else { + throw new InvalidPathException(file, "Not found. "); + } + } catch (InvalidPathException e) { + logger.warning("Could not load file path: " + file); + this.initStage = launcher.newViewerWindow(); + Alert alert = new Alert(Alert.AlertType.WARNING); + alert.setTitle(messageBundle.getString("icepdf.fx.viewer.Main.dialog.loadError.title")); + alert.setHeaderText(messageBundle.getString("icepdf.fx.viewer.Main.dialog.loadUriError.header")); + MessageFormat formatter = new MessageFormat( + messageBundle.getString("icepdf.fx.viewer.Main.dialog.loadUriError.content")); + messageBundle.getString("icepdf.fx.viewer.Main.dialog.loadUriError.content"); + alert.setContentText(formatter.format(new Object[]{file})); + alert.initOwner(initStage); + alert.showAndWait(); + } + } + + private void checkAndLaunchUrl(Launcher launcher, String url) { + try { + URL fileUrl = new URL(url); + HttpURLConnection.setFollowRedirects(false); + HttpURLConnection con = (HttpURLConnection) fileUrl.openConnection(); + con.setInstanceFollowRedirects(false); + con.setRequestMethod("HEAD"); + if (con.getResponseCode() == HttpURLConnection.HTTP_OK) { + launcher.newViewerWindow(fileUrl); + } else { + throw new IllegalStateException(); + } + con.disconnect(); + } catch (Exception e) { + logger.warning("Could not load url: " + url); + this.initStage = launcher.newViewerWindow(); + Alert alert = new Alert(Alert.AlertType.WARNING); + alert.setTitle(messageBundle.getString("icepdf.fx.viewer.Main.dialog.loadError.title")); + alert.setHeaderText(messageBundle.getString("icepdf.fx.viewer.Main.dialog.loadUriError.header")); + MessageFormat formatter = new MessageFormat( + messageBundle.getString("icepdf.fx.viewer.Main.dialog.loadUriError.content")); + messageBundle.getString("icepdf.fx.viewer.Main.dialog.loadUriError.content"); + alert.setContentText(formatter.format(new Object[]{url})); + alert.initOwner(initStage); + alert.showAndWait(); + } + } +} \ No newline at end of file From 78a05996c873ff590c55bda1f8f1fb65052bfed1 Mon Sep 17 00:00:00 2001 From: Patrick Corless Date: Sun, 27 Oct 2024 22:46:50 -0600 Subject: [PATCH 2/7] GH-372 experimenting with javafx design patterns. --- viewer/viewer-fx/README.md | 1 + viewer/viewer-fx/build.gradle | 4 +- viewer/viewer-fx/pom.xml | 7 +- .../core/util/FontPropertiesManager.java | 91 - .../icepdf/core/util/PropertiesManager.java | 26 - .../icepdf/fx/scene/control/DocumentView.java | 158 - .../com/icepdf/fx/scene/control/PageView.java | 130 - .../behavior/DocumentViewBehavior.java | 28 - .../scene/control/skin/DocumentViewSkin.java | 269 -- .../scene/control/skin/PageCaptureTask.java | 100 - .../fx/scene/control/skin/PageViewSkin.java | 319 -- .../control/skin/VirtualContainerBase.java | 94 - .../scene/control/skin/VirtualPageFlow.java | 2852 ----------------- .../scene/control/skin/VirtualScrollBar.java | 92 - .../java/com/icepdf/fx/util/ImageLoader.java | 14 - .../icepdf/fx/util/SettingsLoaderTask.java | 43 - .../icepdf/fx/viewer/DocumentViewTest.java | 32 - .../java/com/icepdf/fx/viewer/Launcher.java | 208 -- .../com/icepdf/fx/viewer/ListViewTest.java | 36 - .../main/java/com/icepdf/fx/viewer/Main.java | 256 -- .../org/icepdf/fx/ri/viewer/FxController.java | 28 + .../org/icepdf/fx/ri/viewer/Interactor.java | 34 + .../org/icepdf/fx/ri/viewer/Launcher.java | 37 + .../org/icepdf/fx/ri/viewer/ViewBuilder.java | 56 + .../org/icepdf/fx/ri/viewer/ViewerModel.java | 27 + .../fx/ri/viewer/ViewerStageManager.java | 48 + .../icepdf/fx/ri/viewer/WindowManager.java | 4 + .../icepdf/fx/ri/viewer/commands/Command.java | 5 + .../ri/viewer/commands/OpenFileCommand.java | 54 + .../fx/ri/viewer/commands/ZoomInCommand.java | 23 + .../fx/ri/viewer/commands/ZoomOutCommand.java | 24 + .../listeners/DocumentChangeListener.java | 27 + .../listeners/StageCloseRequestListener.java | 26 + .../icepdf/fx/ri/views/DocumentViewPane.java | 48 + .../icepdf/fx/ri/views/PageViewWidget.java | 71 + .../fx/images/icepdf-app-icon-32x32.png | Bin 0 -> 2450 bytes .../fx/images/icepdf-app-icon-64x64.png | Bin 0 -> 4425 bytes 37 files changed, 521 insertions(+), 4751 deletions(-) create mode 100644 viewer/viewer-fx/README.md delete mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/core/util/FontPropertiesManager.java delete mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/core/util/PropertiesManager.java delete mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/DocumentView.java delete mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/PageView.java delete mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/behavior/DocumentViewBehavior.java delete mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/DocumentViewSkin.java delete mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/PageCaptureTask.java delete mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/PageViewSkin.java delete mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualContainerBase.java delete mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualPageFlow.java delete mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualScrollBar.java delete mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/util/ImageLoader.java delete mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/util/SettingsLoaderTask.java delete mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/DocumentViewTest.java delete mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/Launcher.java delete mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/ListViewTest.java delete mode 100644 viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/Main.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/FxController.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Interactor.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Launcher.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewBuilder.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerModel.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerStageManager.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/WindowManager.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/Command.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/OpenFileCommand.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/ZoomInCommand.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/ZoomOutCommand.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/listeners/DocumentChangeListener.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/listeners/StageCloseRequestListener.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java create mode 100644 viewer/viewer-fx/src/main/resources/org/icepdf/fx/images/icepdf-app-icon-32x32.png create mode 100644 viewer/viewer-fx/src/main/resources/org/icepdf/fx/images/icepdf-app-icon-64x64.png diff --git a/viewer/viewer-fx/README.md b/viewer/viewer-fx/README.md new file mode 100644 index 000000000..c60d1745f --- /dev/null +++ b/viewer/viewer-fx/README.md @@ -0,0 +1 @@ +mvn javafx:run -f ./viewer/viewer-fx/pom.xml diff --git a/viewer/viewer-fx/build.gradle b/viewer/viewer-fx/build.gradle index 5e3bdbacd..a8c81f352 100644 --- a/viewer/viewer-fx/build.gradle +++ b/viewer/viewer-fx/build.gradle @@ -9,10 +9,10 @@ repositories { description 'ICEpdf FX viewer reference implementation project' -mainClassName = "org.icepdf.fx.viewer.Main" +mainClassName = "org.icepdf.fx.ri.viewer.Launcher" applicationDefaultJvmArgs = ["-Xms64m", "-Xmx1024m"] -def sectionName = 'com/icepdf/fx/viewer' +def sectionName = 'org/icepdf/fx/ri/viewer' def baseJarName = 'icepdf' def baseAppendixName = 'viewer-fx' diff --git a/viewer/viewer-fx/pom.xml b/viewer/viewer-fx/pom.xml index 80b1b4340..74dc25e7d 100644 --- a/viewer/viewer-fx/pom.xml +++ b/viewer/viewer-fx/pom.xml @@ -19,6 +19,11 @@ javafx-controls 11 + + org.openjfx + javafx-swing + 11 + com.github.pcorless.icepdf icepdf-core @@ -43,7 +48,7 @@ javafx-maven-plugin 0.0.8 - org.icepdf.qa.viewer.Launcher + org.icepdf.fx.ri.viewer.Launcher diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/core/util/FontPropertiesManager.java b/viewer/viewer-fx/src/main/java/com/icepdf/core/util/FontPropertiesManager.java deleted file mode 100644 index ef9bce157..000000000 --- a/viewer/viewer-fx/src/main/java/com/icepdf/core/util/FontPropertiesManager.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.icepdf.core.util; - -import org.icepdf.core.pobjects.fonts.FontManager; - -import java.util.Properties; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.prefs.BackingStoreException; -import java.util.prefs.Preferences; - -/** - * \ - */ -public class FontPropertiesManager { - - private static final Logger logger = Logger.getLogger(FontPropertiesManager.class.toString()); - - private static Preferences prefs = Preferences.userNodeForPackage(FontPropertiesManager.class); - - private static FontPropertiesManager fontPropertiesManager; - - private static FontManager fontManager = FontManager.getInstance(); - - private FontPropertiesManager() { - - } - - public static FontPropertiesManager getInstance() { - if (fontPropertiesManager == null) { - fontPropertiesManager = new FontPropertiesManager(); - } - return fontPropertiesManager; - } - - public void readDefaultProperties(String... paths) { - try { - fontManager.readSystemFonts(paths); - } catch (Exception e) { - if (logger.isLoggable(Level.WARNING)) { - logger.log(Level.WARNING, "Error reading system fonts path: ", e); - } - } - } - - public void readFontProperties(String... paths) { - try { - // If you application needs to look at other font directories - // they can be added via the readSystemFonts method. - fontManager.readFonts(paths); - } catch (Exception e) { - if (logger.isLoggable(Level.WARNING)) { - logger.log(Level.WARNING, "Error reading system paths:", e); - } - } - } - - public void loadProperties() { - fontManager.setFontProperties(prefs); - } - - public void clearProperties() { - try { - prefs.clear(); - } catch (BackingStoreException e) { - if (logger.isLoggable(Level.WARNING)) { - logger.log(Level.WARNING, "Error reading system paths:", e); - } - } - } - - public void updateProperties() { - Properties fontProps = fontManager.getFontProperties(); - for (Object key : fontProps.keySet()) { - prefs.put((String) key, fontProps.getProperty((String) key)); - } - } - - public boolean isPropertiesEmpty() { - try { - return prefs.keys().length == 0; - } catch (BackingStoreException e) { - e.printStackTrace(); - } - return false; - } - - - public static FontManager getFontManager() { - return FontManager.getInstance(); - } -} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/core/util/PropertiesManager.java b/viewer/viewer-fx/src/main/java/com/icepdf/core/util/PropertiesManager.java deleted file mode 100644 index e7d13d15d..000000000 --- a/viewer/viewer-fx/src/main/java/com/icepdf/core/util/PropertiesManager.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.icepdf.core.util; - -import java.util.logging.Logger; - -/** - * - */ -public class PropertiesManager { - - private static final Logger logger = Logger.getLogger(PropertiesManager.class.toString()); - - public static final String DEFAULT_MESSAGE_BUNDLE = "com.icepdf.fx.resources.MessageBundle"; - public static final String DEFAULT_SPLASH_CSS = "/com/icepdf/fx/css/splash.css"; - - private static PropertiesManager propertiesManager; - - private PropertiesManager() { - } - - public static PropertiesManager getInstance() { - if (propertiesManager == null) { - propertiesManager = new PropertiesManager(); - } - return propertiesManager; - } -} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/DocumentView.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/DocumentView.java deleted file mode 100644 index b6a729e22..000000000 --- a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/DocumentView.java +++ /dev/null @@ -1,158 +0,0 @@ -package com.icepdf.fx.scene.control; - -import com.icepdf.fx.scene.control.skin.DocumentViewSkin; -import javafx.beans.property.DoubleProperty; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleDoubleProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.scene.control.Control; -import javafx.scene.control.ScrollBar; -import javafx.scene.control.Skin; -import javafx.scene.paint.Color; -import org.icepdf.core.pobjects.Document; - -/** - * - */ -public class DocumentView extends Control { - - private enum PageViewMode {SINGLE_PAGE, SINGLE_COLUMN, DOUBLE_PAGE, DOUBLE_COLUMN} - - ; - - private DoubleProperty scaleIncrementValue; - private DoubleProperty scaleMaxValue; - private DoubleProperty scaleMinValue; - - private int currentPageIndex; - - private DoubleProperty scale; - private DoubleProperty rotation; - private ObjectProperty document; - - // viewport - private ScrollBar hbar; - - private ObjectProperty backgroundFill; - - private int numberOfPages; - - // todo Document insertion. - public DocumentView(Document document) { - this.numberOfPages = document.getNumberOfPages(); - this.document = new SimpleObjectProperty<>(document); - scale = new SimpleDoubleProperty(1); - rotation = new SimpleDoubleProperty(0); - backgroundFill = new SimpleObjectProperty<>(Color.LIGHTGRAY); - - // mouse wheel and touch zoom level - scaleIncrementValue = new SimpleDoubleProperty(0.05); - scaleMaxValue = new SimpleDoubleProperty(8); - scaleMinValue = new SimpleDoubleProperty(0.05); - } - - @Override - protected Skin createDefaultSkin() { - return new DocumentViewSkin(this); - } - - public double getScale() { - return scale.get(); - } - - public DoubleProperty scaleProperty() { - return scale; - } - - public void setScale(double scale) { - this.scale.set(scale); - } - - public double getRotation() { - return rotation.get(); - } - - public DoubleProperty rotationProperty() { - return rotation; - } - - public void setRotation(double rotation) { - this.rotation.set(rotation); - } - - public Color getBackgroundFill() { - return backgroundFill.get(); - } - - public ObjectProperty backgroundFillProperty() { - return backgroundFill; - } - - public void setBackgroundFill(Color backgroundFill) { - this.backgroundFill.set(backgroundFill); - } - - public int getNumberOfPages() { - return numberOfPages; - } - - public void setNumberOfPages(int numberOfPages) { - this.numberOfPages = numberOfPages; - } - - public double getScaleIncrementValue() { - return scaleIncrementValue.get(); - } - - public DoubleProperty scaleIncrementValueProperty() { - return scaleIncrementValue; - } - - public void setScaleIncrementValue(double scaleIncrementValue) { - this.scaleIncrementValue.set(scaleIncrementValue); - } - - public double getScaleMaxValue() { - return scaleMaxValue.get(); - } - - public DoubleProperty scaleMaxValueProperty() { - return scaleMaxValue; - } - - public void setScaleMaxValue(double scaleMaxValue) { - this.scaleMaxValue.set(scaleMaxValue); - } - - public double getScaleMinValue() { - return scaleMinValue.get(); - } - - public DoubleProperty scaleMinValueProperty() { - return scaleMinValue; - } - - public void setScaleMinValue(double scaleMinValue) { - this.scaleMinValue.set(scaleMinValue); - } - - public Document getDocument() { - return document.get(); - } - - public ObjectProperty documentProperty() { - return document; - } - - public void setDocument(Document document) { - this.document.set(document); - } - - public ScrollBar getHbar() { - return hbar; - } - - public void setHbar(ScrollBar hbar) { - this.hbar = hbar; - } -} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/PageView.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/PageView.java deleted file mode 100644 index 4bef5ba60..000000000 --- a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/PageView.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.icepdf.fx.scene.control; - -import com.icepdf.fx.scene.control.skin.PageViewSkin; -import javafx.beans.property.*; -import javafx.geometry.BoundingBox; -import javafx.geometry.Bounds; -import javafx.scene.control.Control; -import javafx.scene.control.Skin; -import javafx.scene.paint.Color; - -import java.util.Random; - -/** - * - */ -public class PageView extends Control { - - private DoubleProperty scale; - private DoubleProperty rotation; - private ObjectProperty documentView; - private ObjectProperty clipBounds; - private ObjectProperty pageBounds; - - private ReadOnlyIntegerWrapper index = new ReadOnlyIntegerWrapper(this, "index", -1); - - private ObjectProperty backgroundFill; - - private static Random random = new Random(); - - public PageView() { - scale = new SimpleDoubleProperty(1); - rotation = new SimpleDoubleProperty(0); - documentView = new SimpleObjectProperty<>(); - backgroundFill = new SimpleObjectProperty<>(Color.WHITE); - clipBounds = new SimpleObjectProperty<>(new BoundingBox(0, 0, 0, 0)); - pageBounds = new SimpleObjectProperty<>(new BoundingBox(0, 0, 0, 0)); - - } - - - @Override - protected Skin createDefaultSkin() { - return new PageViewSkin(this); - } - - public double getScale() { - return scale.get(); - } - - public DoubleProperty scaleProperty() { - return scale; - } - - public void setScale(double scale) { - this.scale.set(scale); - } - - public void setScale(float scale) { - this.scale.set(scale); - } - - public double getRotation() { - return rotation.get(); - } - - public DoubleProperty rotationProperty() { - return rotation; - } - - public void setRotation(double rotation) { - this.rotation.set(rotation); - } - - - public Color getBackgroundFill() { - return backgroundFill.get(); - } - - public ObjectProperty backgroundFillProperty() { - return backgroundFill; - } - - public void setBackgroundFill(Color backgroundFill) { - this.backgroundFill.set(backgroundFill); - } - - - public Bounds getClipBounds() { - return clipBounds.get(); - } - - public ObjectProperty clipBoundsProperty() { - return clipBounds; - } - - public Bounds getPageBounds() { - return pageBounds.get(); - } - - public ObjectProperty pageBoundsProperty() { - return pageBounds; - } - - public final int getIndex() { - return index.get(); - } - - public ReadOnlyIntegerWrapper indexProperty() { - return index; - } - - public void updateIndex(int i) { - final int oldIndex = index.get(); - index.set(i); -// indexChanged(oldIndex, i); - } - - public DocumentView getDocumentView() { - return documentView.get(); - } - - - public ObjectProperty documentViewProperty() { - return documentView; - } - - public void setDocumentView(DocumentView documentView) { - this.documentView.set(documentView); - } -} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/behavior/DocumentViewBehavior.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/behavior/DocumentViewBehavior.java deleted file mode 100644 index 25a10400f..000000000 --- a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/behavior/DocumentViewBehavior.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.icepdf.fx.scene.control.behavior; - -import com.icepdf.fx.scene.control.DocumentView; -import com.sun.javafx.scene.control.behavior.BehaviorBase; -import com.sun.javafx.scene.control.behavior.KeyBinding; - -import java.util.ArrayList; -import java.util.List; - -import static javafx.scene.input.KeyCode.PAGE_DOWN; -import static javafx.scene.input.KeyCode.PAGE_UP; - -/** - * - */ -public class DocumentViewBehavior extends BehaviorBase { - - protected static final List DOCUMENT_VIEW_BINDINGS = new ArrayList(); - - static { - DOCUMENT_VIEW_BINDINGS.add(new KeyBinding(PAGE_UP, "ScrollUp")); - DOCUMENT_VIEW_BINDINGS.add(new KeyBinding(PAGE_DOWN, "ScrollDown")); - } - - public DocumentViewBehavior(DocumentView control) { - super(control, DOCUMENT_VIEW_BINDINGS); - } -} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/DocumentViewSkin.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/DocumentViewSkin.java deleted file mode 100644 index d06981cdb..000000000 --- a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/DocumentViewSkin.java +++ /dev/null @@ -1,269 +0,0 @@ -package com.icepdf.fx.scene.control.skin; - -import com.icepdf.fx.scene.control.DocumentView; -import com.icepdf.fx.scene.control.PageView; -import com.icepdf.fx.scene.control.behavior.DocumentViewBehavior; -import javafx.geometry.Dimension2D; -import javafx.geometry.Insets; -import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.effect.DropShadow; -import javafx.scene.image.Image; -import javafx.scene.layout.StackPane; -import javafx.scene.paint.Color; -import org.icepdf.core.pobjects.Document; -import org.icepdf.core.pobjects.PDimension; - -import java.lang.ref.SoftReference; -import java.util.HashMap; - -/** - * - */ -public class DocumentViewSkin extends VirtualContainerBase { - - private HashMap> imageCaptureCache = new HashMap<>(25); - - private StackPane placeholderRegion; - private Node placeholderNode; - - private int itemCount = -1; - - private boolean needCellsRebuilt = true; - private boolean needCellsReconfigured = false; - - private Dimension2D defaultDimension; - - private DropShadow dropShadow; - - public DocumentViewSkin(DocumentView documentView) { - super(documentView, new DocumentViewBehavior(documentView)); - - updateListViewItems(); - - flow.setId("virtual-flow"); - flow.setPannable(true); - flow.setVertical(true); - flow.setCreateCell(flow1 -> DocumentViewSkin.this.createCell()); - getChildren().add(flow); - - updatePageCount(); - - // page shadow reuse. - dropShadow = new DropShadow(); - dropShadow.setRadius(5.0); - dropShadow.setOffsetX(3.0); - dropShadow.setOffsetY(3.0); - dropShadow.setColor(Color.color(0.4, 0.5, 0.5)); - - - if (getSkinnable().getNumberOfPages() > 0) { - // find page size so we can apply a default size. - Document document = getSkinnable().getDocument(); - PDimension dim = document.getPageDimension(0, (float) getSkinnable().getRotation(), - (float) getSkinnable().getScale()); - defaultDimension = new Dimension2D(dim.getWidth(), dim.getHeight()); - } -// flow.setOnScroll(t -> { -// // todo centering point -// double x = t.getX(); -// double y = t.getY(); -// if (t.isControlDown()) { -// if (t.getDeltaY() > 0) { -// incrementScale(); -// } else { -// decrementScale(); -// } -// t.consume(); -// } -// }); - - // init the behavior 'closures' -// getBehavior().setOnFocusPreviousRow(() -> { onFocusPreviousCell(); }); -// getBehavior().setOnFocusNextRow(() -> { onFocusNextCell(); }); -// getBehavior().setOnMoveToFirstCell(() -> { onMoveToFirstCell(); }); -// getBehavior().setOnMoveToLastCell(() -> { onMoveToLastCell(); }); -// getBehavior().setOnScrollPageDown(isFocusDriven -> onScrollPageDown(isFocusDriven)); -// getBehavior().setOnScrollPageUp(isFocusDriven -> onScrollPageUp(isFocusDriven)); -// getBehavior().setOnSelectPreviousRow(() -> { onSelectPreviousCell(); }); -// getBehavior().setOnSelectNextRow(() -> { onSelectNextCell(); }); - -// documentView.itemsProperty().addListener(new WeakInvalidationListener(itemsChangeListener)); - } - - private void incrementScale() { - DocumentView documentView = getSkinnable(); - double scale = documentView.getScale() + documentView.getScaleIncrementValue(); - if (scale <= documentView.getScaleMaxValue()) { - documentView.setScale(scale); - } - } - - private void decrementScale() { - DocumentView documentView = getSkinnable(); - double scale = documentView.getScale() - documentView.getScaleIncrementValue(); - if (scale >= documentView.getScaleMinValue()) { - documentView.setScale(scale); - } - } - - @Override - protected void handleControlPropertyChanged(String p) { - super.handleControlPropertyChanged(p); - if ("ITEMS".equals(p)) { - updateListViewItems(); - } else if ("ORIENTATION".equals(p)) { -// flow.setVertical(getSkinnable().getOrientation() == Orientation.VERTICAL); - } else if ("CELL_FACTORY".equals(p)) { - flow.recreateCells(); - } else if ("PARENT".equals(p)) { - if (getSkinnable().getParent() != null && getSkinnable().isVisible()) { - getSkinnable().requestLayout(); - } - } else if ("PLACEHOLDER".equals(p)) { - updatePlaceholderRegionVisibility(); - } -// else if ("FIXED_CELL_SIZE".equals(p)) { -// flow.setFixedCellSize(getSkinnable().getFixedCellSize()); -// } - } -// -// private MapChangeListener propertiesMapListener = c -> { -// if (! c.wasAdded()) return; -// if (RECREATE.equals(c.getKey())) { -// needCellsRebuilt = true; -// getSkinnable().requestLayout(); -// getSkinnable().getProperties().remove(RECREATE); -// } -// }; - - - public void updateListViewItems() { - - pageCountDirty = true; - getSkinnable().requestLayout(); - } - - @Override - public int getPageCount() { - return itemCount; - } - - @Override - protected void updatePageCount() { - if (flow == null) return; - - Document document = getSkinnable().getDocument(); - - int oldCount = itemCount; - int newCount = document == null ? 0 : document.getNumberOfPages(); - - itemCount = newCount; - - flow.setCellCount(newCount); - - getSkinnable().setHbar(flow.getHbar()); - - updatePlaceholderRegionVisibility(); - if (newCount != oldCount) { - needCellsRebuilt = true; - } else { - needCellsReconfigured = true; - } - } - - protected final void updatePlaceholderRegionVisibility() { - boolean visible = getPageCount() == 0; - - if (visible) { - placeholderNode = null;//getSkinnable().getPlaceholder(); - if (placeholderNode == null) { - placeholderNode = new Label(); - ((Label) placeholderNode).setText("empty document"); - } - if (placeholderNode != null) { - if (placeholderRegion == null) { - placeholderRegion = new StackPane(); - placeholderRegion.getStyleClass().setAll("placeholder"); - getChildren().add(placeholderRegion); - } - placeholderRegion.getChildren().setAll(placeholderNode); - } - } - flow.setVisible(!visible); - if (placeholderRegion != null) { - placeholderRegion.setVisible(visible); - } - } - - @Override - public PageView createCell() { - PageView cell = createDefaultCellImpl(); - cell.setLayoutX(-1); - cell.setLayoutY(-1); - cell.setDocumentView(getSkinnable()); - cell.scaleProperty().bind(getSkinnable().scaleProperty()); - cell.rotationProperty().bind(getSkinnable().rotationProperty()); - cell.setPadding(new Insets(10, 20, 10, 20)); - cell.setEffect(dropShadow); - - return cell; - } - - private static PageView createDefaultCellImpl() { - // todo add back factory to build pageView or documentView - return new PageView(); - } - - @Override - protected void layoutChildren(final double x, final double y, - final double w, final double h) { - super.layoutChildren(x, y, w, h); - - if (needCellsRebuilt) { - flow.rebuildCells(); - } else if (needCellsReconfigured) { - flow.reconfigureCells(); - } - - needCellsRebuilt = false; - needCellsReconfigured = false; - - if (getPageCount() == 0) { - // show message overlay instead of empty listview - if (placeholderRegion != null) { - placeholderRegion.setVisible(w > 0 && h > 0); - placeholderRegion.resizeRelocate(x, y, w, h); - } - } else { - flow.resizeRelocate(x, y, w, h); - } - } - - @Override - protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { - checkState(); - -// if (getPageCount() == 0) { -// if (placeholderRegion == null) { -// updatePlaceholderRegionVisibility(); -// } -// if (placeholderRegion != null) { -// return placeholderRegion.prefWidth(height) + leftInset + rightInset; -// } -// } -// -// return computePrefHeight(-1, topInset, rightInset, bottomInset, leftInset) * 0.618033987; - return defaultDimension.getWidth(); - } - - @Override - protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { - return defaultDimension.getHeight(); - } - - public HashMap> getImageCaptureCache() { - return imageCaptureCache; - } - -} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/PageCaptureTask.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/PageCaptureTask.java deleted file mode 100644 index 13052d6c3..000000000 --- a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/PageCaptureTask.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.icepdf.fx.scene.control.skin; - -import com.icepdf.fx.scene.control.DocumentView; -import javafx.application.Platform; -import javafx.concurrent.Task; -import javafx.embed.swing.SwingFXUtils; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.shape.Rectangle; -import org.icepdf.core.pobjects.Catalog; -import org.icepdf.core.pobjects.graphics.images.ImageUtility; -import org.icepdf.core.pobjects.PDimension; -import org.icepdf.core.pobjects.Page; -import org.icepdf.core.util.GraphicsRenderingHints; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.lang.ref.SoftReference; - -/** - * Created by pcorl_000 on 2017-03-31. - */ -public class PageCaptureTask extends Task { - - private ImageView pageImageView; - private DocumentView documentView; - - private javafx.scene.shape.Rectangle clip; - - private int index; - - - public PageCaptureTask(int index, ImageView pageImageView, javafx.scene.shape.Rectangle clip, DocumentView documentView) { - this.pageImageView = pageImageView; - this.index = index; - this.documentView = documentView; - this.clip = clip; - } - - @Override - protected Image call() throws Exception { - - try { - Catalog catalog = documentView.getDocument().getCatalog(); - - Page page = catalog.getPageTree().getPage(index); - page.init(); - //todo make pageBounder a property - PDimension sz = page.getSize(Page.BOUNDARY_MEDIABOX, 0, (float) documentView.getScale()); - - int pageWidth = (int) sz.getWidth(); - int pageHeight = (int) sz.getHeight(); - if (clip != null && clip.getWidth() > 0) { - pageWidth = (int) Math.round(clip.getWidth()); - pageHeight = (int) Math.round(clip.getHeight()); - } - - if (isCancelled()) { - return null; - } - - BufferedImage image = ImageUtility.createCompatibleImage(pageWidth, pageHeight); -// System.out.println(pageWidth + " " + pageHeight); - Graphics g = image.createGraphics(); - g.translate((int) -clip.getX(), (int) -clip.getY()); - page.paint(g, GraphicsRenderingHints.SCREEN, Page.BOUNDARY_CROPBOX, 0, (float) documentView.getScale()); - g.dispose(); - - if (isCancelled()) { - return null; - } - - final Image pageImage = SwingFXUtils.toFXImage(image, null); - - if (!isCancelled()) { - DocumentViewSkin documentViewSkin = (DocumentViewSkin) documentView.getSkin(); - documentViewSkin.getImageCaptureCache().put(index, new SoftReference<>(pageImage)); - Platform.runLater(() -> { - // update the location - pageImageView.relocate(clip.getX(), clip.getY()); -// pageImageView.setScaleX(1); -// pageImageView.setScaleY(1); -// pageImageView.setTranslateX(0); -// pageImageView.setTranslateY(0); - pageImageView.setImage(pageImage); - pageImageView.setFitWidth(clip.getWidth()); - pageImageView.setClip(new Rectangle(0, 0, clip.getWidth(), clip.getHeight())); - // make sure it's visible. - pageImageView.setVisible(true); - }); - } - - return pageImage; - } catch (InterruptedException e) { -// e.printStackTrace(); - } - - return null; - } -} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/PageViewSkin.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/PageViewSkin.java deleted file mode 100644 index 06e93a8d8..000000000 --- a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/PageViewSkin.java +++ /dev/null @@ -1,319 +0,0 @@ -package com.icepdf.fx.scene.control.skin; - -import com.icepdf.fx.scene.control.DocumentView; -import com.icepdf.fx.scene.control.PageView; -import javafx.concurrent.Task; -import javafx.geometry.Bounds; -import javafx.geometry.HPos; -import javafx.geometry.Point2D; -import javafx.geometry.VPos; -import javafx.scene.Group; -import javafx.scene.control.SkinBase; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.layout.Region; -import javafx.scene.paint.Color; -import javafx.scene.shape.*; -import org.icepdf.core.pobjects.Document; -import org.icepdf.core.pobjects.PDimension; -import org.icepdf.core.util.Library; - -import java.lang.ref.SoftReference; - -/** - * - */ -public class PageViewSkin extends SkinBase { - - // default for a4 is 595 X 842, letter, 595 x 792 - public static final double WIDTH = 595; - public static final double HEIGHT = 842; - - private double width = WIDTH; - private double height = HEIGHT; - - private double xViewport = -1; - private double yViewport = -1; - - private Path pageOutline; - private Group pageGroup; - private ImageView pageImageView; - private Rectangle clipRectangle; - private Point2D oldImageView; - - private PageCaptureTask pageCaptureTask; - - private boolean invalidPage = true; - - public PageViewSkin(PageView control) { - super(control); - // match min and max to preferred - getSkinnable().setMaxWidth(Region.USE_PREF_SIZE); - getSkinnable().setMaxHeight(Region.USE_PREF_SIZE); - getSkinnable().setMinWidth(Region.USE_PREF_SIZE); - getSkinnable().setMinHeight(Region.USE_PREF_SIZE); - - // todo remove. - control.backgroundFillProperty().addListener(observable -> { - updatePageColour(); - }); - - control.scaleProperty().addListener((observable, oldValue, newValue) -> { - updateClipRectangle(); - scalePreviousRendering(oldValue.doubleValue(), newValue.doubleValue()); - updatePageZoom(); - }); - control.rotationProperty().addListener(observable -> { - updateClipRectangle(); - updatePageRotation(); - }); - - control.indexProperty().addListener((observable, oldValue, newValue) -> { - updateClipRectangle(); - updatePageRendering(); - }); - - // listen for x:y changes so we can recalculate the page image clip - getSkinnable().layoutYProperty().addListener((observable, oldValue, newValue) -> { - updateClipRectangle(); - updatePageRendering(); - }); - - getSkinnable().getDocumentView().getHbar().valueProperty().addListener((observable, oldValue, newValue) -> { - updateClipRectangle(); - updatePageRendering(); - }); - - getSkinnable().getDocumentView().widthProperty().addListener((observable, oldValue, newValue) -> { - updateClipRectangle(); - updatePageRendering(); - }); - - getSkinnable().getDocumentView().heightProperty().addListener((observable, oldValue, newValue) -> { - updateClipRectangle(); - updatePageRendering(); - }); - - } - - private void initialize() { - - if (pageGroup != null) { - pageGroup.getChildren().clear(); - } else { - pageGroup = new Group(); - getChildren().add(pageGroup); - } - int index = getSkinnable().getIndex(); - Document document = getSkinnable().getDocumentView().getDocument(); - PDimension dimension = document.getPageDimension(index, 0,//(float) getSkinnable().getRotation(), - (float) getSkinnable().getScale()); - pageOutline = new Path(); - pageOutline.getElements().add(new MoveTo(0, 0)); - pageOutline.getElements().add(new LineTo(dimension.getWidth(), 0)); - pageOutline.getElements().add(new LineTo(dimension.getWidth(), dimension.getHeight())); - pageOutline.getElements().add(new LineTo(0, dimension.getHeight())); - pageOutline.getElements().add(new ClosePath()); - pageOutline.setStroke(Color.BLACK); - pageOutline.setFill(Color.WHITE); - - if (pageImageView == null) { - pageImageView = new ImageView(); - pageImageView.setPreserveRatio(true); - } - oldImageView = new Point2D(pageImageView.getLayoutX(), pageImageView.getLayoutY()); - - if (clipRectangle == null) { - clipRectangle = new Rectangle(0, 0, dimension.getWidth(), dimension.getHeight()); - } - // trim rectangle to view port size. - updateClipRectangle(); - clipRectangle.setStroke(Color.RED); - clipRectangle.setFill(null); - updatePageRendering(); - - pageGroup.getChildren().addAll(pageOutline, pageImageView, clipRectangle); - } - - public void updatePageColour() { - if (pageOutline != null) { - pageOutline.setFill(getSkinnable().getBackgroundFill()); - } - } - - public void updatePageRotation() { - if (pageGroup != null) { - pageGroup.setRotate(getSkinnable().getRotation()); - getSkinnable().requestLayout(); - } - } - - public void updatePageZoom() { - if (pageGroup != null) { - invalidPage = true; - getSkinnable().requestLayout(); - } - } - - public void updateClipRectangle() { - DocumentView documentView = getSkinnable().getDocumentView(); - int index = getSkinnable().getIndex(); - - // if page index -1, avoid doing clip work? - if (index == -1) { - return; - } - - Bounds viewport = getNode().getParent().getParent().getLayoutBounds();//getSkinnable().getDocumentView().getBoundsInLocal(); - double scrollBarWidth = getSkinnable().getDocumentView().getHbar().getHeight(); - double x = getSkinnable().getDocumentView().getHbar().getValue(); - double y = getSkinnable().getLayoutY(); // subtract inset. - -// System.out.println(viewport); - - PDimension pDimension = documentView.getDocument().getPageDimension(index, 0,//(float) getSkinnable().getRotation(), - (float) getSkinnable().getScale()); - double pageWidth = pDimension.getWidth(); - double pageHeight = pDimension.getHeight(); - double insetTop = getSkinnable().getInsets().getTop(); - double insetLeft = getSkinnable().getInsets().getLeft(); - double viewportWidth = viewport.getWidth(); - double viewportHeight = viewport.getHeight(); - double xClip = 0; - double yClip = 0; - double clipWidth = pageWidth; - double clipHeight = pageHeight; - - // todo further work is needed here to tighten up the bounds as well as support side by side page views - if (pageWidth >= viewportWidth) { - double max = (pageWidth + insetLeft + scrollBarWidth) - viewportWidth; - if (x > insetLeft) { - xClip = x - insetLeft; - } - if (x >= max) { - clipWidth = viewportWidth - (x - max) - scrollBarWidth; - } else { - clipWidth = viewportWidth;//- scrollBarWidth; - } - - } - if (pageHeight >= viewportHeight) { - if (y < 0) { - yClip = -y - insetTop; - if (yClip < 0) yClip = 0; - clipHeight = pageHeight - yClip; - if (clipHeight > viewportHeight) { - clipHeight = viewportHeight; - } - } else { - clipHeight = viewportHeight - y; - } - } - // update our clip outline. - if (clipRectangle != null) { - clipRectangle.setX(xClip); - clipRectangle.setY(yClip); - clipRectangle.setWidth(clipWidth); - clipRectangle.setHeight(clipHeight); -// System.out.println(clipRectangle); - } - } - - private void scalePreviousRendering(double previousScale, double newScale) { -// pageImageView.setVisible(false); - if (getSkinnable().getIndex() < 0) { - return; - } - - double scaleIncrement = newScale / previousScale; //newScale - previousScale; - - -// double diff = oldImageView.getX() / oldClipRectangle.getWidth(); - -// System.out.println(clipRectangle.getX() + " " +clipRectangle.getY()); -// System.out.println(oldImageView.getX() + " " +oldImageView.getY()); -// System.out.println(); - -// pageImageView.relocate(clipRectangle.getX() * scaleIncrement, clipRectangle.getY() * scaleIncrement); - pageImageView.setFitWidth(clipRectangle.getWidth()); - } - - - /** - * Index has changed and we need to update the contents fo the cell with the new page data. - */ - public void updatePageRendering() { - - int index = getSkinnable().getIndex(); - - stopTask(); - if (index >= 0 && pageImageView != null) { -// pageImageView.setVisible(false); - DocumentView documentView = getSkinnable().getDocumentView(); - // todo cache needs to store previous x,y position, so we can make the repaint smarter - DocumentViewSkin documentViewSkin = (DocumentViewSkin) documentView.getSkin(); - SoftReference cachedImageReference = documentViewSkin.getImageCaptureCache().get(index); - if (cachedImageReference != null) { - Image cachedImage = cachedImageReference.get(); - if (cachedImage != null) { - pageImageView.setImage(cachedImage); - pageImageView.setVisible(true); -// pageImageView.setFitWidth(clipRectangle.getWidth()); - } - } - - if ((pageCaptureTask == null || !pageCaptureTask.isRunning())) { - PDimension pDimension = documentView.getDocument().getPageDimension(index, 0, (float) 1.0f); - width = pDimension.getWidth(); - height = pDimension.getHeight(); - - pageImageView.setClip(new Rectangle(0, 0, clipRectangle.getWidth(), clipRectangle.getHeight())); - - oldImageView = new Point2D(pageImageView.getLayoutX(), pageImageView.getLayoutY()); - - // we have a new page size so we can setup a paint - pageCaptureTask = new PageCaptureTask(index, pageImageView, clipRectangle, documentView); - Library.execute(pageCaptureTask); - - pageCaptureTask.setOnCancelled(t -> { - pageImageView.setVisible(false); - }); - } - } - invalidPage = true; - getSkinnable().requestLayout(); - } - - private boolean stopTask() { - if (pageCaptureTask != null) { - pageCaptureTask.cancel(); - boolean cancelled = pageCaptureTask.isCancelled() || pageCaptureTask.getState() == Task.State.READY; - pageCaptureTask = null; - return cancelled; - } - return false; - } - - - @Override - protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) { - if (invalidPage) { - initialize(); - invalidPage = false; - } - layoutInArea(pageGroup, contentX, contentY, contentWidth, contentHeight, -1, - HPos.CENTER, VPos.CENTER); - } - - @Override - protected double computePrefWidth(double height, double topInset, double rightInset, - double bottomInset, double leftInset) { - return leftInset + rightInset + (width * getSkinnable().getScale()); - } - - @Override - protected double computePrefHeight(double width, double topInset, double rightInset, - double bottomInset, double leftInset) { - return topInset + bottomInset + (height * getSkinnable().getScale()); - } -} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualContainerBase.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualContainerBase.java deleted file mode 100644 index 0984f706f..000000000 --- a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualContainerBase.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.icepdf.fx.scene.control.skin; - - -import com.icepdf.fx.scene.control.PageView; -import com.sun.javafx.scene.control.behavior.BehaviorBase; -import com.sun.javafx.scene.control.skin.BehaviorSkinBase; -import javafx.scene.control.Control; -import javafx.scene.control.ScrollToEvent; - -/** - * Parent class to control skins whose contents are virtualized and scrollable. - * This class handles the interaction with the VirtualFlow class, which is the - * main class handling the virtualization of the contents of this container. - * - * @profile common - */ -public abstract class VirtualContainerBase, I extends PageView> extends BehaviorSkinBase { - - protected boolean pageCountDirty; - - public VirtualContainerBase(final C control, B behavior) { - super(control, behavior); - flow = createVirtualFlow(); - - control.addEventHandler(ScrollToEvent.scrollToTopIndex(), event -> { - // Fix for RT-24630: The row count in VirtualFlow was incorrect - // (normally zero), so the scrollTo call was misbehaving. - if (pageCountDirty) { - // update row count before we do a scroll - updatePageCount(); - pageCountDirty = false; - } - flow.scrollTo(event.getScrollTarget()); - }); - } - - /** - * The virtualized container which handles the layout and scrolling of - * all the cells. - */ - protected final VirtualPageFlow flow; - - /** - * Returns a Cell available to be used in the virtual flow. This means you - * may return either a previously used, but now unrequired cell, or alternatively - * create a new Cell instance. - *

- * Preference is obviously given to reusing cells whenever possible, to keep - * performance costs down. - */ - public abstract I createCell(); - - /** - * This enables skin subclasses to provide a custom VirtualFlow implementation, - * rather than have VirtualContainerBase instantiate the default instance. - */ - protected VirtualPageFlow createVirtualFlow() { - return new VirtualPageFlow<>(); - } - - /** - * Returns the total number of items in this container, including those - * that are currently hidden because they are out of view. - */ - public abstract int getPageCount(); - - protected abstract void updatePageCount(); - - double getMaxCellWidth(int rowsToCount) { - return snappedLeftInset() + flow.getMaxCellWidth(rowsToCount) + snappedRightInset(); - } - - double getVirtualFlowPreferredHeight(int rows) { - double height = 1.0; - - for (int i = 0; i < rows && i < getPageCount(); i++) { - height += flow.getCellLength(i); - } - - return height + snappedTopInset() + snappedBottomInset(); - } - - @Override - protected void layoutChildren(double x, double y, double w, double h) { - checkState(); - } - - protected void checkState() { - if (pageCountDirty) { - updatePageCount(); - pageCountDirty = false; - } - } -} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualPageFlow.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualPageFlow.java deleted file mode 100644 index b2a144a55..000000000 --- a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualPageFlow.java +++ /dev/null @@ -1,2852 +0,0 @@ -package com.icepdf.fx.scene.control.skin; - - -import com.icepdf.fx.scene.control.PageView; -import com.sun.javafx.scene.control.Logging; -import com.sun.javafx.scene.control.skin.Utils; -import com.sun.javafx.scene.traversal.Algorithm; -import com.sun.javafx.scene.traversal.Direction; -import com.sun.javafx.scene.traversal.ParentTraversalEngine; -import com.sun.javafx.scene.traversal.TraversalContext; -import javafx.animation.KeyFrame; -import javafx.animation.Timeline; -import javafx.application.ConditionalFeature; -import javafx.application.Platform; -import javafx.beans.InvalidationListener; -import javafx.beans.Observable; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.BooleanPropertyBase; -import javafx.beans.value.ChangeListener; -import javafx.collections.ObservableList; -import javafx.event.EventDispatcher; -import javafx.event.EventHandler; -import javafx.geometry.Orientation; -import javafx.scene.*; -import javafx.scene.control.ScrollBar; -import javafx.scene.input.MouseEvent; -import javafx.scene.input.ScrollEvent; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import javafx.scene.shape.Rectangle; -import javafx.util.Callback; -import javafx.util.Duration; -import sun.util.logging.PlatformLogger; - -import java.util.AbstractList; -import java.util.ArrayList; -import java.util.BitSet; -import java.util.List; - -/** - * Implementation of a virtualized container using a cell based mechanism. - */ -public class VirtualPageFlow extends Region { - - protected final static boolean IS_TOUCH_SUPPORTED = Platform.isSupported(ConditionalFeature.INPUT_TOUCH); - - /** - * Scroll events may request to scroll about a number of "lines". We first - * decide how big one "line" is - for fixed cell size it's clear, - * for variable cell size we settle on a single number so that the scrolling - * speed is consistent. Now if the line is so big that - * MIN_SCROLLING_LINES_PER_PAGE of them don't fit into one page, we make - * them smaller to prevent the scrolling step to be too big (perhaps - * even more than one page). - */ - private static final int MIN_SCROLLING_LINES_PER_PAGE = 8; - - private boolean touchDetected = false; - private boolean mouseDown = false; - - - private BooleanProperty vertical; - - public final void setVertical(boolean value) { - verticalProperty().set(value); - } - - public final boolean isVertical() { - return vertical == null ? true : vertical.get(); - } - - public final BooleanProperty verticalProperty() { - if (vertical == null) { - vertical = new BooleanPropertyBase(true) { - @Override - protected void invalidated() { - pile.clear(); - sheetChildren.clear(); - cells.clear(); - lastWidth = lastHeight = -1; - setMaxPrefBreadth(-1); - setViewportBreadth(0); - setViewportLength(0); - lastPosition = 0; - hbar.setValue(0); - vbar.setValue(0); - setPosition(0.0f); - setNeedsLayout(true); - requestLayout(); - } - - @Override - public Object getBean() { - return VirtualPageFlow.this; - } - - @Override - public String getName() { - return "vertical"; - } - }; - } - return vertical; - } - - /** - * Indicates whether the VirtualFlow viewport is capable of being panned - * by the user (either via the mouse or touch events). - */ - private boolean pannable = true; - - public boolean isPannable() { - return pannable; - } - - public void setPannable(boolean value) { - this.pannable = value; - } - - /** - * Indicates the number of cells that should be in the flow. The user of - * the VirtualFlow must set this appropriately. When the cell count changes - * the VirtualFlow responds by updating the visuals. If the items backing - * the cells change, but the count has not changed, you must call the - * reconfigureCells() function to update the visuals. - */ - private int cellCount; - - public int getCellCount() { - return cellCount; - } - - public void setCellCount(int i) { - int oldCount = cellCount; - this.cellCount = i; - - boolean countChanged = oldCount != cellCount; - - // ensure that the virtual scrollbar adjusts in size based on the current - // cell count. - if (countChanged) { - VirtualScrollBar lengthBar = isVertical() ? vbar : hbar; - lengthBar.setMax(i); - } - - // I decided *not* to reset maxPrefBreadth here for the following - // situation. Suppose I have 30 cells and then I add 10 more. Just - // because I added 10 more doesn't mean the max pref should be - // reset. Suppose the first 3 cells were extra long, and I was - // scrolled down such that they weren't visible. If I were to reset - // maxPrefBreadth when subsequent cells were added or removed, then the - // scroll bars would erroneously reset as well. So I do not reset - // the maxPrefBreadth here. - - // Fix for RT-12512, RT-14301 and RT-14864. - // Without this, the VirtualFlow length-wise scrollbar would not change - // as expected. This would leave items unable to be shown, as they - // would exist outside of the visible area, even when the scrollbar - // was at its maximum position. - // FIXME this should be only executed on the pulse, so this will likely - // lead to performance degradation until it is handled properly. - if (countChanged) { - layoutChildren(); - - // Fix for RT-13965: Without this line of code, the number of items in - // the sheet would constantly grow, leaking memory for the life of the - // application. This was especially apparent when the total number of - // cells changes - regardless of whether it became bigger or smaller. - sheetChildren.clear(); - - Parent parent = getParent(); - if (parent != null) parent.requestLayout(); - } - // TODO suppose I had 100 cells and I added 100 more. Further - // suppose I was scrolled to the bottom when that happened. I - // actually want to update the position of the mapper such that - // the view remains "stable". - } - - /** - * The position of the VirtualFlow within its list of cells. This is a value - * between 0 and 1. - */ - private double position; - - public double getPosition() { - return position; - } - - public void setPosition(double newPosition) { - boolean needsUpdate = this.position != newPosition; - this.position = com.sun.javafx.util.Utils.clamp(0, newPosition, 1); - if (needsUpdate) { - requestLayout(); - } - } - - /** - * For optimisation purposes, some use cases can trade dynamic cell length - * for speed - if fixedCellSize is greater than zero we'll use that rather - * than determine it by querying the cell itself. - */ - private double fixedCellSize = 0; - private boolean fixedCellSizeEnabled = false; - - public void setFixedCellSize(final double value) { - this.fixedCellSize = value; - this.fixedCellSizeEnabled = fixedCellSize > 0; - needsCellsLayout = true; - layoutChildren(); - } - - /** - * Callback which is invoked whenever the VirtualFlow needs a new - * IndexedCell. The VirtualFlow attempts to reuse cells whenever possible - * and only creates the minimal number of cells necessary. - */ - private Callback createCell; - - public Callback getCreateCell() { - return createCell; - } - - public void setCreateCell(Callback cc) { - this.createCell = cc; - - if (createCell != null) { - accumCell = null; - setNeedsLayout(true); - recreateCells(); - if (getParent() != null) getParent().requestLayout(); - } - } - - /** - * The maximum preferred size in the non-virtual direction. For example, - * if vertical, then this is the max pref width of all cells encountered. - *

- * In general, this is the largest preferred size in the non-virtual - * direction that we have ever encountered. We don't reduce this size - * unless instructed to do so, so as to reduce the amount of scroll bar - * jitter. The access on this variable is package ONLY FOR TESTING. - */ - private double maxPrefBreadth; - - protected final void setMaxPrefBreadth(double value) { - this.maxPrefBreadth = value; - } - - protected final double getMaxPrefBreadth() { - return maxPrefBreadth; - } - - /** - * The breadth of the viewport portion of the VirtualFlow as computed during - * the layout pass. In a vertical flow this would be the same as the clip - * view width. In a horizontal flow this is the clip view height. - * The access on this variable is package ONLY FOR TESTING. - */ - private double viewportBreadth; - - protected final void setViewportBreadth(double value) { - this.viewportBreadth = value; - } - - protected final double getViewportBreadth() { - return viewportBreadth; - } - - /** - * The length of the viewport portion of the VirtualFlow as computed - * during the layout pass. In a vertical flow this would be the same as the - * clip view height. In a horizontal flow this is the clip view width. - * The access on this variable is package ONLY FOR TESTING. - */ - private double viewportLength; - - void setViewportLength(double value) { - this.viewportLength = value; - } - - protected double getViewportLength() { - return viewportLength; - } - - - /** - * The width of the VirtualFlow the last time it was laid out. We - * use this information for several fast paths during the layout pass. - */ - double lastWidth = -1; - - /** - * The height of the VirtualFlow the last time it was laid out. We - * use this information for several fast paths during the layout pass. - */ - double lastHeight = -1; - - /** - * The number of "virtual" cells in the flow the last time it was laid out. - * For example, there may have been 1000 virtual cells, but only 20 actual - * cells created and in use. In that case, lastCellCount would be 1000. - */ - int lastCellCount = 0; - - /** - * We remember the last value for vertical the last time we laid out the - * flow. If vertical has changed, we will want to change the max & value - * for the different scroll bars. Since we do all the scroll bar update - * work in the layoutChildren function, we need to know what the old value for - * vertical was. - */ - boolean lastVertical; - - /** - * The position last time we laid out. If none of the lastXXX vars have - * changed respective to their values in layoutChildren, then we can just punt - * out of the method (I hope...) - */ - double lastPosition; - - /** - * The breadth of the first visible cell last time we laid out. - */ - double lastCellBreadth = -1; - - /** - * The length of the first visible cell last time we laid out. - */ - double lastCellLength = -1; - - /** - * The list of cells representing those cells which actually make up the - * current view. The cells are ordered such that the first cell in this - * list is the first in the view, and the last cell is the last in the - * view. When pixel scrolling, the list is simply shifted and items drop - * off the beginning or the end, depending on the order of scrolling. - *

- * This is package private ONLY FOR TESTING - */ - final ArrayLinkedList cells = new ArrayLinkedList(); - - protected List getCells() { - return cells; - } - - /** - * A structure containing cells that can be reused later. These are cells - * that at one time were needed to populate the view, but now are no longer - * needed. We keep them here until they are needed again. - *

- * This is package private ONLY FOR TESTING - */ - final ArrayLinkedList pile = new ArrayLinkedList(); - - /** - * A special cell used to accumulate bounds, such that we reduce object - * churn. This cell must be recreated whenever the cell factory function - * changes. This has package access ONLY for testing. - */ - T accumCell; - - /** - * This group is used for holding the 'accumCell'. 'accumCell' must - * be added to the skin for it to be styled. Otherwise, it doesn't - * report the correct width/height leading to issues when scrolling - * the flow - */ - Group accumCellParent; - - /** - * The group which holds the cells. - */ - final Group sheet; - - final ObservableList sheetChildren; - - /** - * The scroll bar used for scrolling horizontally. This has package access - * ONLY for testing. - */ - private VirtualScrollBar hbar = new VirtualScrollBar(this); - - protected final VirtualScrollBar getHbar() { - return hbar; - } - - /** - * The scroll bar used to scrolling vertically. This has package access - * ONLY for testing. - */ - private VirtualScrollBar vbar = new VirtualScrollBar(this); - - protected final VirtualScrollBar getVbar() { - return vbar; - } - - /** - * Control in which the cell's sheet is placed and forms the viewport. The - * viewportBreadth and viewportLength are simply the dimensions of the - * clipView. This has package access ONLY for testing. - */ - ClippedContainer clipView; - - /** - * When both the horizontal and vertical scroll bars are visible, - * we have to 'fill in' the bottom right corner where the two scroll bars - * meet. This is handled by this corner region. This has package access - * ONLY for testing. - */ - StackPane corner; - - // used for panning the virtual flow - private double lastX; - private double lastY; - private boolean isPanning = false; - - public VirtualPageFlow() { - getStyleClass().add("virtual-flow"); - setId("virtual-flow"); - - // initContent - // --- sheet - sheet = new Group(); - sheet.getStyleClass().add("sheet"); - sheet.setStyle("-fx-background-color: red"); - sheet.setAutoSizeChildren(false); - - sheetChildren = sheet.getChildren(); - - // --- clipView - clipView = new ClippedContainer(this); - clipView.setNode(sheet); - getChildren().add(clipView); - - // --- accumCellParent - accumCellParent = new Group(); - - accumCellParent.setVisible(false); - getChildren().add(accumCellParent); - - - /* - ** don't allow the ScrollBar to handle the ScrollEvent, - ** In a VirtualFlow a vertical scroll should scroll on the vertical only, - ** whereas in a horizontal ScrollBar it can scroll horizontally. - */ - // block the event from being passed down to children - final EventDispatcher blockEventDispatcher = (event, tail) -> event; - // block ScrollEvent from being passed down to scrollbar's skin - final EventDispatcher oldHsbEventDispatcher = hbar.getEventDispatcher(); - hbar.setEventDispatcher((event, tail) -> { - if (event.getEventType() == ScrollEvent.SCROLL && - !((ScrollEvent) event).isDirect()) { - tail = tail.prepend(blockEventDispatcher); - tail = tail.prepend(oldHsbEventDispatcher); - return tail.dispatchEvent(event); - } - return oldHsbEventDispatcher.dispatchEvent(event, tail); - }); - // block ScrollEvent from being passed down to scrollbar's skin - final EventDispatcher oldVsbEventDispatcher = vbar.getEventDispatcher(); - vbar.setEventDispatcher((event, tail) -> { - if (event.getEventType() == ScrollEvent.SCROLL && - !((ScrollEvent) event).isDirect()) { - tail = tail.prepend(blockEventDispatcher); - tail = tail.prepend(oldVsbEventDispatcher); - return tail.dispatchEvent(event); - } - return oldVsbEventDispatcher.dispatchEvent(event, tail); - }); - /* - ** listen for ScrollEvents over the whole of the VirtualFlow - ** area, the above dispatcher having removed the ScrollBars - ** scroll event handling. - */ - setOnScroll(new EventHandler() { - @Override - public void handle(ScrollEvent event) { - if (IS_TOUCH_SUPPORTED) { - if (touchDetected == false && mouseDown == false) { - startSBReleasedAnimation(); - } - } - /* - ** calculate the delta in the direction of the flow. - */ - double virtualDelta = 0.0; - if (isVertical()) { - switch (event.getTextDeltaYUnits()) { - case PAGES: - virtualDelta = event.getTextDeltaY() * lastHeight; - break; - case LINES: - double lineSize; -// if (fixedCellSizeEnabled) { -// lineSize = fixedCellSize; -// } else { - // For the scrolling to be reasonably consistent - // we set the lineSize to the average size - // of all currently loaded lines. - T lastCell = cells.getLast(); - lineSize = - (getCellPosition(lastCell) - + getCellLength(lastCell) - - getCellPosition(cells.getFirst())) - / cells.size(); -// } - - if (lastHeight / lineSize < MIN_SCROLLING_LINES_PER_PAGE) { - lineSize = lastHeight / MIN_SCROLLING_LINES_PER_PAGE; - } - - virtualDelta = event.getTextDeltaY() * lineSize; - break; - case NONE: - virtualDelta = event.getDeltaY(); - } - } else { // horizontal - switch (event.getTextDeltaXUnits()) { - case CHARACTERS: - // can we get character size here? - // for now, fall through to pixel values - case NONE: - double dx = event.getDeltaX(); - double dy = event.getDeltaY(); - - virtualDelta = (Math.abs(dx) > Math.abs(dy) ? dx : dy); - } - } - - if (virtualDelta != 0.0) { - /* - ** only consume it if we use it - */ - double result = adjustPixels(-virtualDelta); - if (result != 0.0) { - event.consume(); - } - } - - ScrollBar nonVirtualBar = isVertical() ? hbar : vbar; - if (needBreadthBar) { - double nonVirtualDelta = isVertical() ? event.getDeltaX() : event.getDeltaY(); - if (nonVirtualDelta != 0.0) { - double newValue = nonVirtualBar.getValue() - nonVirtualDelta; - if (newValue < nonVirtualBar.getMin()) { - nonVirtualBar.setValue(nonVirtualBar.getMin()); - } else if (newValue > nonVirtualBar.getMax()) { - nonVirtualBar.setValue(nonVirtualBar.getMax()); - } else { - nonVirtualBar.setValue(newValue); - } - event.consume(); - } - } - } - }); - - - addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler() { - @Override - public void handle(MouseEvent e) { - mouseDown = true; - if (IS_TOUCH_SUPPORTED) { - scrollBarOn(); - } - if (isFocusTraversable()) { - // We check here to see if the current focus owner is within - // this VirtualFlow, and if so we back-off from requesting - // focus back to the VirtualFlow itself. This is particularly - // relevant given the bug identified in RT-32869. In this - // particular case TextInputControl was clearing selection - // when the focus on the TextField changed, meaning that the - // right-click context menu was not showing the correct - // options as there was no selection in the TextField. - boolean doFocusRequest = true; - Node focusOwner = getScene().getFocusOwner(); - if (focusOwner != null) { - Parent parent = focusOwner.getParent(); - while (parent != null) { - if (parent.equals(VirtualPageFlow.this)) { - doFocusRequest = false; - break; - } - parent = parent.getParent(); - } - } - - if (doFocusRequest) { - requestFocus(); - } - } - - lastX = e.getX(); - lastY = e.getY(); - - // determine whether the user has push down on the virtual flow, - // or whether it is the scrollbar. This is done to prevent - // mouse events being 'doubled up' when dragging the scrollbar - // thumb - it has the side-effect of also starting the panning - // code, leading to flicker - isPanning = !(vbar.getBoundsInParent().contains(e.getX(), e.getY()) - || hbar.getBoundsInParent().contains(e.getX(), e.getY())); - } - }); - addEventFilter(MouseEvent.MOUSE_RELEASED, e -> { - mouseDown = false; - if (IS_TOUCH_SUPPORTED) { - startSBReleasedAnimation(); - } - }); - addEventFilter(MouseEvent.MOUSE_DRAGGED, e -> { - if (IS_TOUCH_SUPPORTED) { - scrollBarOn(); - } - if (!isPanning || !isPannable()) return; - - // With panning enabled, we support panning in both vertical - // and horizontal directions, regardless of the fact that - // VirtualFlow is virtual in only one direction. - double xDelta = lastX - e.getX(); - double yDelta = lastY - e.getY(); - - // figure out the distance that the mouse moved in the virtual - // direction, and then perform the movement along that axis - // virtualDelta will contain the amount we actually did move - double virtualDelta = isVertical() ? yDelta : xDelta; - double actual = adjustPixels(virtualDelta); - if (actual != 0) { - // update last* here, as we know we've just adjusted the - // scrollbar. This means we don't get the situation where a - // user presses-and-drags a long way past the min or max - // values, only to change directions and see the scrollbar - // start moving immediately. - if (isVertical()) lastY = e.getY(); - else lastX = e.getX(); - } - - // similarly, we do the same in the non-virtual direction - double nonVirtualDelta = isVertical() ? xDelta : yDelta; - ScrollBar nonVirtualBar = isVertical() ? hbar : vbar; - if (nonVirtualBar.isVisible()) { - double newValue = nonVirtualBar.getValue() + nonVirtualDelta; - if (newValue < nonVirtualBar.getMin()) { - nonVirtualBar.setValue(nonVirtualBar.getMin()); - } else if (newValue > nonVirtualBar.getMax()) { - nonVirtualBar.setValue(nonVirtualBar.getMax()); - } else { - nonVirtualBar.setValue(newValue); - - // same as the last* comment above - if (isVertical()) lastX = e.getX(); - else lastY = e.getY(); - } - } - }); - - /* - * We place the scrollbars _above_ the rectangle, such that the drag - * operations often used in conjunction with scrollbars aren't - * misinterpreted as drag operations on the rectangle as well (which - * would be the case if the scrollbars were underneath it as the - * rectangle itself doesn't block the mouse. - */ - // --- vbar - vbar.setOrientation(Orientation.VERTICAL); - vbar.addEventHandler(MouseEvent.ANY, event -> { - event.consume(); - }); - getChildren().add(vbar); - - // --- hbar - hbar.setOrientation(Orientation.HORIZONTAL); - hbar.addEventHandler(MouseEvent.ANY, event -> { - event.consume(); - }); - getChildren().add(hbar); - - // --- corner - corner = new StackPane(); - corner.getStyleClass().setAll("corner"); - getChildren().add(corner); - - - // initBinds - // clipView binds - InvalidationListener listenerX = valueModel -> { - updateHbar(); - }; - verticalProperty().addListener(listenerX); - hbar.valueProperty().addListener(listenerX); - hbar.visibleProperty().addListener(listenerX); - -// ChangeListener listenerY = new ChangeListener() { -// @Override public void handle(Bean bean, PropertyReference property) { -// clipView.setClipY(isVertical() ? 0 : vbar.getValue()); -// } -// }; -// addChangedListener(VERTICAL, listenerY); -// vbar.addChangedListener(ScrollBar.VALUE, listenerY); - - ChangeListener listenerY = (ov, t, t1) -> { - clipView.setClipY(isVertical() ? 0 : vbar.getValue()); -// System.out.println("v" + getBoundsInLocal()); -// for (T cell : cells){ -// Node parent = cell.getParent(); -// System.out.println(cell.getBoundsInParent()); -// System.out.println(cell.getLocalToParentTransform()); -// } -// System.out.println(); - }; - vbar.valueProperty().addListener(listenerY); - - super.heightProperty().addListener((observable, oldHeight, newHeight) -> { - // Fix for RT-8480, where the VirtualFlow does not show its content - // after changing size to 0 and back. - if (oldHeight.doubleValue() == 0 && newHeight.doubleValue() > 0) { - recreateCells(); - } - }); - - - /* - ** there are certain animations that need to know if the touch is - ** happening..... - */ - setOnTouchPressed(e -> { - touchDetected = true; - scrollBarOn(); - }); - - setOnTouchReleased(e -> { - touchDetected = false; - startSBReleasedAnimation(); - }); - - setImpl_traversalEngine(new ParentTraversalEngine(this, new Algorithm() { - - Node selectNextAfterIndex(int index, TraversalContext context) { - T nextCell; - while ((nextCell = getVisibleCell(++index)) != null) { - if (nextCell.isFocusTraversable()) { - return nextCell; - } - Node n = context.selectFirstInParent(nextCell); - if (n != null) { - return n; - } - } - return null; - } - - Node selectPreviousBeforeIndex(int index, TraversalContext context) { - T prevCell; - while ((prevCell = getVisibleCell(--index)) != null) { - Node prev = context.selectLastInParent(prevCell); - if (prev != null) { - return prev; - } - if (prevCell.isFocusTraversable()) { - return prevCell; - } - } - return null; - } - - @Override - public Node select(Node owner, Direction dir, TraversalContext context) { - T cell; - if (cells.isEmpty()) return null; - if (cells.contains(owner)) { - cell = (T) owner; - } else { - cell = findOwnerCell(owner); - Node next = context.selectInSubtree(cell, owner, dir); - if (next != null) { - return next; - } - if (dir == Direction.NEXT) dir = Direction.NEXT_IN_LINE; - } - int cellIndex = cell.getIndex(); - switch (dir) { - case PREVIOUS: - return selectPreviousBeforeIndex(cellIndex, context); - case NEXT: - Node n = context.selectFirstInParent(cell); - if (n != null) { - return n; - } - // Intentional fall-through - case NEXT_IN_LINE: - return selectNextAfterIndex(cellIndex, context); - } - return null; - } - - private T findOwnerCell(Node owner) { - Parent p = owner.getParent(); - while (!cells.contains(p)) { - p = p.getParent(); - } - return (T) p; - } - - @Override - public Node selectFirst(TraversalContext context) { - T firstCell = cells.getFirst(); - if (firstCell == null) return null; - if (firstCell.isFocusTraversable()) return firstCell; - Node n = context.selectFirstInParent(firstCell); - if (n != null) { - return n; - } - return selectNextAfterIndex(firstCell.getIndex(), context); - } - - @Override - public Node selectLast(TraversalContext context) { - T lastCell = cells.getLast(); - if (lastCell == null) return null; - Node p = context.selectLastInParent(lastCell); - if (p != null) { - return p; - } - if (lastCell.isFocusTraversable()) return lastCell; - return selectPreviousBeforeIndex(lastCell.getIndex(), context); - } - })); - - } - - void updateHbar() { - // Bring the clipView.clipX back to 0 if control is vertical or - // the hbar isn't visible (fix for RT-11666) - if (!isVisible() || getScene() == null) return; - - if (isVertical()) { - if (hbar.isVisible()) { - clipView.setClipX(hbar.getValue()); - } else { - // all cells are now less than the width of the flow, - // so we should shift the hbar/clip such that - // everything is visible in the viewport. - clipView.setClipX(0); - hbar.setValue(0); - } - } - - } - - /*************************************************************************** - * * - * Layout Functionality * - * * - **************************************************************************/ - - /** - * Overridden to implement somewhat more efficient support for layout. The - * VirtualFlow can generally be considered as being unmanaged, in that - * whenever the position changes, or other such things change, we need - * to perform a layout but there is no reason to notify the parent. However - * when things change which may impact the preferred size (such as - * vertical, createCell, and configCell) then we need to notify the - * parent. - */ - @Override - public void requestLayout() { - // isNeedsLayout() is commented out due to RT-21417. This does not - // appear to impact performance (indeed, it may help), and resolves the - // issue identified in RT-21417. - setNeedsLayout(true); - } - - @Override - protected void layoutChildren() { - if (needsRecreateCells) { - lastWidth = -1; - lastHeight = -1; - releaseCell(accumCell); -// accumCell = null; -// accumCellParent.getChildren().clear(); - sheet.getChildren().clear(); - for (int i = 0, max = cells.size(); i < max; i++) { - cells.get(i).updateIndex(-1); - } - cells.clear(); - pile.clear(); - releaseAllPrivateCells(); - } else if (needsRebuildCells) { - lastWidth = -1; - lastHeight = -1; - releaseCell(accumCell); - for (int i = 0; i < cells.size(); i++) { - cells.get(i).updateIndex(-1); - } - addAllToPile(); - releaseAllPrivateCells(); - } else if (needsReconfigureCells) { - setMaxPrefBreadth(-1); - lastWidth = -1; - lastHeight = -1; - } - - if (!dirtyCells.isEmpty()) { - int index; - final int cellsSize = cells.size(); - while ((index = dirtyCells.nextSetBit(0)) != -1 && index < cellsSize) { - T cell = cells.get(index); - // updateIndex(-1) works for TableView, but breaks ListView. - // For now, the TableView just does not use the dirtyCells API -// cell.updateIndex(-1); - if (cell != null) { - cell.requestLayout(); - } - dirtyCells.clear(index); - } - - setMaxPrefBreadth(-1); - lastWidth = -1; - lastHeight = -1; - } - - final boolean hasSizeChange = sizeChanged; - boolean recreatedOrRebuilt = needsRebuildCells || needsRecreateCells || sizeChanged; - - needsRecreateCells = false; - needsReconfigureCells = false; - needsRebuildCells = false; - sizeChanged = false; - - if (needsCellsLayout) { - for (int i = 0, max = cells.size(); i < max; i++) { - PageView cell = cells.get(i); - if (cell != null) { - cell.requestLayout(); - } - } - needsCellsLayout = false; - - // yes, we return here - if needsCellsLayout was set to true, we - // only did it to do the above - not rerun the entire layout. - return; - } - - final double width = getWidth(); - final double height = getHeight(); - final boolean isVertical = isVertical(); - final double position = getPosition(); - - // if the width and/or height is 0, then there is no point doing - // any of this work. In particular, this can happen during startup - if (width <= 0 || height <= 0) { - addAllToPile(); - lastWidth = width; - lastHeight = height; - hbar.setVisible(false); - vbar.setVisible(false); - corner.setVisible(false); - return; - } - - // we check if any of the cells in the cells list need layout. This is a - // sign that they are perhaps animating their sizes. Without this check, - // we may not perform a layout here, meaning that the cell will likely - // 'jump' (in height normally) when the user drags the virtual thumb as - // that is the first time the layout would occur otherwise. - boolean cellNeedsLayout = false; - boolean thumbNeedsLayout = false; - - if (IS_TOUCH_SUPPORTED) { - if ((tempVisibility == true && (hbar.isVisible() == false || vbar.isVisible() == false)) || - (tempVisibility == false && (hbar.isVisible() == true || vbar.isVisible() == true))) { - thumbNeedsLayout = true; - } - } - - if (!cellNeedsLayout) { - for (int i = 0; i < cells.size(); i++) { - PageView cell = cells.get(i); - cellNeedsLayout = cell.isNeedsLayout(); - if (cellNeedsLayout) break; - } - } - - - T firstCell = getFirstVisibleCell(); - - // If no cells need layout, we check other criteria to see if this - // layout call is even necessary. If it is found that no layout is - // needed, we just punt. - if (!cellNeedsLayout && !thumbNeedsLayout) { - boolean cellSizeChanged = false; - if (firstCell != null) { - double breadth = getCellBreadth(firstCell); - double length = getCellLength(firstCell); - cellSizeChanged = (breadth != lastCellBreadth) || (length != lastCellLength); - lastCellBreadth = breadth; - lastCellLength = length; - } - - if (width == lastWidth && - height == lastHeight && - cellCount == lastCellCount && - isVertical == lastVertical && - position == lastPosition && - !cellSizeChanged) { - // TODO this happens to work around the problem tested by - // testCellLayout_LayoutWithoutChangingThingsUsesCellsInSameOrderAsBefore - // but isn't a proper solution. Really what we need to do is, when - // laying out cells, we need to make sure that if a cell is pressed - // AND we are doing a full rebuild then we need to make sure we - // use that cell in the same physical location as before so that - // it gets the mouse release event. - return; - } - } - - /* - * This function may get called under a variety of circumstances. - * It will determine what has changed from the last time it was laid - * out, and will then take one of several execution paths based on - * what has changed so as to perform minimal layout work and also to - * give the expected behavior. One or more of the following may have - * happened: - * - * 1) width/height has changed - * - If the width and/or height has been reduced (but neither of - * them has been expanded), then we simply have to reposition and - * resize the scroll bars - * - If the width (in the vertical case) has expanded, then we - * need to resize the existing cells and reposition and resize - * the scroll bars - * - If the height (in the vertical case) has expanded, then we - * need to resize and reposition the scroll bars and add - * any trailing cells - * - * 2) cell count has changed - * - If the number of cells is bigger, or it is smaller but not - * so small as to move the position then we can just update the - * cells in place without performing layout and update the - * scroll bars. - * - If the number of cells has been reduced and it affects the - * position, then move the position and rebuild all the cells - * and update the scroll bars - * - * 3) size of the cell has changed - * - If the size changed in the virtual direction (ie: height - * in the case of vertical) then layout the cells, adding - * trailing cells as necessary and updating the scroll bars - * - If the size changed in the non virtual direction (ie: width - * in the case of vertical) then simply adjust the widths of - * the cells as appropriate and adjust the scroll bars - * - * 4) vertical changed, cells is empty, maxPrefBreadth == -1, etc - * - Full rebuild. - * - * Each of the conditions really resolves to several of a handful of - * possible outcomes: - * a) reposition & rebuild scroll bars - * b) resize cells in non-virtual direction - * c) add trailing cells - * d) update cells - * e) resize cells in the virtual direction - * f) all of the above - * - * So this function first determines what outcomes need to occur, and - * then will execute all the ones that really need to happen. Every code - * path ends up touching the "reposition & rebuild scroll bars" outcome, - * so that one will be executed every time. - */ - boolean needTrailingCells = false; - boolean rebuild = cellNeedsLayout || - isVertical != lastVertical || - cells.isEmpty() || - getMaxPrefBreadth() == -1 || - position != lastPosition || - cellCount != lastCellCount || - hasSizeChange || - (isVertical && height < lastHeight) || (!isVertical && width < lastWidth); - - if (!rebuild) { - // Check if maxPrefBreadth didn't change - double maxPrefBreadth = getMaxPrefBreadth(); - boolean foundMax = false; - for (int i = 0; i < cells.size(); ++i) { - double breadth = getCellBreadth(cells.get(i)); - if (maxPrefBreadth == breadth) { - foundMax = true; - } else if (breadth > maxPrefBreadth) { - rebuild = true; - break; - } - } - if (!foundMax) { // All values were lower - rebuild = true; - } - } - - if (!rebuild) { - if ((isVertical && height > lastHeight) || (!isVertical && width > lastWidth)) { - // resized in the virtual direction - needTrailingCells = true; - } - } - - initViewport(); - - // Get the index of the "current" cell - int currentIndex = computeCurrentIndex(); - if (lastCellCount != cellCount) { - // The cell count has changed. We want to keep the viewport - // stable if possible. If position was 0 or 1, we want to keep - // the position in the same place. If the new cell count is >= - // the currentIndex, then we will adjust the position to be 1. - // Otherwise, our goal is to leave the index of the cell at the - // top consistent, with the same translation etc. - if (position == 0 || position == 1) { - // Update the item count -// setItemCount(cellCount); - } else if (currentIndex >= cellCount) { - setPosition(1.0f); -// setItemCount(cellCount); - } else if (firstCell != null) { - double firstCellOffset = getCellPosition(firstCell); - int firstCellIndex = getCellIndex(firstCell); -// setItemCount(cellCount); - adjustPositionToIndex(firstCellIndex); - double viewportTopToCellTop = -computeOffsetForCell(firstCellIndex); - adjustByPixelAmount(viewportTopToCellTop - firstCellOffset); - } - - // Update the current index - currentIndex = computeCurrentIndex(); - } - - if (rebuild) { - setMaxPrefBreadth(-1); - // Start by dumping all the cells into the pile - addAllToPile(); - - // The distance from the top of the viewport to the top of the - // cell for the current index. - double offset = -computeViewportOffset(getPosition()); - - // Add all the leading and trailing cells (the call to add leading - // cells will add the current cell as well -- that is, the one that - // represents the current position on the mapper). - addLeadingCells(currentIndex, offset); - - // Force filling of space with empty cells if necessary - addTrailingCells(false); - } else if (needTrailingCells) { - addTrailingCells(false); - } - - computeBarVisiblity(); - updateScrollBarsAndCells(recreatedOrRebuilt); - - lastWidth = getWidth(); - lastHeight = getHeight(); - lastCellCount = getCellCount(); - lastVertical = isVertical(); - lastPosition = getPosition(); - - cleanPile(); - } - - /** - * Adds all the cells prior to and including the given currentIndex, until - * no more can be added without falling off the flow. The startOffset - * indicates the distance from the leading edge (top) of the viewport to - * the leading edge (top) of the currentIndex. - */ - protected void addLeadingCells(int currentIndex, double startOffset) { - // The offset will keep track of the distance from the top of the - // viewport to the top of the current index. We will increment it - // as we lay out leading cells. - double offset = startOffset; - // The index is the absolute index of the cell being laid out - int index = currentIndex; - - // Offset should really be the bottom of the current index - boolean first = true; // first time in, we just fudge the offset and let - // it be the top of the current index then redefine - // it as the bottom of the current index thereafter - // while we have not yet laid out so many cells that they would fall - // off the flow, we will continue to create and add cells. The - // offset is our indication of whether we can lay out additional - // cells. If the offset is ever < 0, except in the case of the very - // first cell, then we must quit. - T cell = null; - - // special case for the position == 1.0, skip adding last invisible cell - if (index == cellCount && offset == getViewportLength()) { - index--; - first = false; - } - while (index >= 0 && (offset > 0 || first)) { - cell = getAvailableCell(index); - setCellIndex(cell, index); - resizeCellSize(cell); // resize must be after config - cells.addFirst(cell); - - // A little gross but better than alternatives because it reduces - // the number of times we have to update a cell or compute its - // size. The first time into this loop "offset" is actually the - // top of the current index. On all subsequent visits, it is the - // bottom of the current index. - if (first) { - first = false; - } else { - offset -= getCellLength(cell); - } - - // Position the cell, and update the maxPrefBreadth variable as we go. - positionCell(cell, offset); - setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell))); - cell.setVisible(true); - --index; - } - - // There are times when after laying out the cells we discover that - // the top of the first cell which represents index 0 is below the top - // of the viewport. In these cases, we have to adjust the cells up - // and reset the mapper position. This might happen when items got - // removed at the top or when the viewport size increased. - if (cells.size() > 0) { - cell = cells.getFirst(); - int firstIndex = getCellIndex(cell); - double firstCellPos = getCellPosition(cell); - if (firstIndex == 0 && firstCellPos > 0) { - setPosition(0.0f); - offset = 0; - for (int i = 0; i < cells.size(); i++) { - cell = cells.get(i); - positionCell(cell, offset); - offset += getCellLength(cell); - } - } - } else { - // reset scrollbar to top, so if the flow sees cells again it starts at the top - vbar.setValue(0); - hbar.setValue(0); - } - } - - /** - * Adds all the trailing cells that come after the last index in - * the cells ObservableList. - */ - protected boolean addTrailingCells(boolean fillEmptyCells) { - // If cells is empty then addLeadingCells bailed for some reason and - // we're hosed, so just punt - if (cells.isEmpty()) return false; - - // While we have not yet laid out so many cells that they would fall - // off the flow, so we will continue to create and add cells. When the - // offset becomes greater than the width/height of the flow, then we - // know we cannot add any more cells. - T startCell = cells.getLast(); - double offset = getCellPosition(startCell) + getCellLength(startCell); - int index = getCellIndex(startCell) + 1; - boolean filledWithNonEmpty = index <= cellCount; - - final double viewportLength = getViewportLength(); - - // Fix for RT-37421, which was a regression caused by RT-36556 - if (offset < 0 && !fillEmptyCells) { - return false; - } - - // - // RT-36507: viewportLength - offset gives the maximum number of - // additional cells that should ever be able to fit in the viewport if - // every cell had a height of 1. If index ever exceeds this count, - // then offset is not incrementing fast enough, or at all, which means - // there is something wrong with the cell size calculation. - // - final double maxCellCount = viewportLength - offset; - while (offset < viewportLength) { - if (index >= cellCount) { - if (offset < viewportLength) filledWithNonEmpty = false; - if (!fillEmptyCells) return filledWithNonEmpty; - // RT-36507 - return if we've exceeded the maximum - if (index > maxCellCount) { - final PlatformLogger logger = Logging.getControlsLogger(); - if (logger.isLoggable(PlatformLogger.Level.INFO)) { - if (startCell != null) { - logger.info("index exceeds maxCellCount. Check size calculations for " + startCell.getClass()); - } else { - logger.info("index exceeds maxCellCount"); - } - } - return filledWithNonEmpty; - } - } - T cell = getAvailableCell(index); - setCellIndex(cell, index); - resizeCellSize(cell); // resize happens after config! - cells.addLast(cell); - - // Position the cell and update the max pref - positionCell(cell, offset); - setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell))); - - offset += getCellLength(cell); - cell.setVisible(true); - ++index; - } - - // Discover whether the first cell coincides with index #0. If after - // adding all the trailing cells we find that a) the first cell was - // not index #0 and b) there are trailing cells, then we have a - // problem. We need to shift all the cells down and add leading cells, - // one at a time, until either the very last non-empty cells is aligned - // with the bottom OR we have laid out cell index #0 at the first - // position. - T firstCell = cells.getFirst(); - index = getCellIndex(firstCell); - T lastNonEmptyCell = getLastVisibleCell(); - double start = getCellPosition(firstCell); - double end = getCellPosition(lastNonEmptyCell) + getCellLength(lastNonEmptyCell); - if ((index != 0 || (index == 0 && start < 0)) && fillEmptyCells && - lastNonEmptyCell != null && getCellIndex(lastNonEmptyCell) == cellCount - 1 && end < viewportLength) { - - double prospectiveEnd = end; - double distance = viewportLength - end; - while (prospectiveEnd < viewportLength && index != 0 && (-start) < distance) { - index--; - T cell = getAvailableCell(index); - setCellIndex(cell, index); - resizeCellSize(cell); // resize must be after config - cells.addFirst(cell); - double cellLength = getCellLength(cell); - start -= cellLength; - prospectiveEnd += cellLength; - positionCell(cell, start); - setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell))); - cell.setVisible(true); - } - - // The amount by which to translate the cells down - firstCell = cells.getFirst(); - start = getCellPosition(firstCell); - double delta = viewportLength - end; - if (getCellIndex(firstCell) == 0 && delta > (-start)) { - delta = (-start); - } - // Move things - for (int i = 0; i < cells.size(); i++) { - T cell = cells.get(i); - positionCell(cell, getCellPosition(cell) + delta); - } - - // Check whether the first cell, subsequent to our adjustments, is - // now index #0 and aligned with the top. If so, change the position - // to be at 0 instead of 1. - start = getCellPosition(firstCell); - if (getCellIndex(firstCell) == 0 && start == 0) { - setPosition(0); - } else if (getPosition() != 1) { - setPosition(1); - } - } - - return filledWithNonEmpty; - } - - /** - * @return true if bar visibility changed - */ - private boolean computeBarVisiblity() { - if (cells.isEmpty()) { - // In case no cells are set yet, we assume no bars are needed - needLengthBar = false; - needBreadthBar = false; - return true; - } - - final boolean isVertical = isVertical(); - boolean barVisibilityChanged = false; - - VirtualScrollBar breadthBar = isVertical ? hbar : vbar; - VirtualScrollBar lengthBar = isVertical ? vbar : hbar; - - final double viewportBreadth = getViewportBreadth(); - - final int cellsSize = cells.size(); - for (int i = 0; i < 2; i++) { - final boolean lengthBarVisible = getPosition() > 0 - || cellCount > cellsSize - || (cellCount == cellsSize && (getCellPosition(cells.getLast()) + getCellLength(cells.getLast())) > getViewportLength()) - || (cellCount == cellsSize - 1 && barVisibilityChanged && needBreadthBar); - - if (lengthBarVisible ^ needLengthBar) { - needLengthBar = lengthBarVisible; - barVisibilityChanged = true; - } - - // second conditional removed for RT-36669. - final boolean breadthBarVisible = (maxPrefBreadth > viewportBreadth);// || (needLengthBar && maxPrefBreadth > (viewportBreadth - lengthBarBreadth)); - if (breadthBarVisible ^ needBreadthBar) { - needBreadthBar = breadthBarVisible; - barVisibilityChanged = true; - } - } - - // Start by optimistically deciding whether the length bar and - // breadth bar are needed and adjust the viewport dimensions - // accordingly. If during layout we find that one or the other of the - // bars actually is needed, then we will perform a cleanup pass - - if (!IS_TOUCH_SUPPORTED) { - updateViewportDimensions(); - breadthBar.setVisible(needBreadthBar); - lengthBar.setVisible(needLengthBar); - } else { - breadthBar.setVisible(needBreadthBar && tempVisibility); - lengthBar.setVisible(needLengthBar && tempVisibility); - } - - return barVisibilityChanged; - } - - private void updateViewportDimensions() { - final boolean isVertical = isVertical(); - final double breadthBarLength = snapSize(isVertical ? hbar.prefHeight(-1) : vbar.prefWidth(-1)); - final double lengthBarBreadth = snapSize(isVertical ? vbar.prefWidth(-1) : hbar.prefHeight(-1)); - - setViewportBreadth((isVertical ? getWidth() : getHeight()) - (needLengthBar ? lengthBarBreadth : 0)); - setViewportLength((isVertical ? getHeight() : getWidth()) - (needBreadthBar ? breadthBarLength : 0)); - } - - private void initViewport() { - // Initialize the viewportLength and viewportBreadth to match the - // width/height of the flow - final boolean isVertical = isVertical(); - - updateViewportDimensions(); - - VirtualScrollBar breadthBar = isVertical ? hbar : vbar; - VirtualScrollBar lengthBar = isVertical ? vbar : hbar; - - // If there has been a switch between the virtualized bar, then we - // will want to do some stuff TODO. - breadthBar.setVirtual(false); - lengthBar.setVirtual(true); - } - - @Override - protected void setWidth(double value) { - if (value != lastWidth) { - super.setWidth(value); - sizeChanged = true; - setNeedsLayout(true); - requestLayout(); - } - } - - @Override - protected void setHeight(double value) { - if (value != lastHeight) { - super.setHeight(value); - sizeChanged = true; - setNeedsLayout(true); - requestLayout(); - } - } - - private void updateScrollBarsAndCells(boolean recreate) { - // Assign the hbar and vbar to the breadthBar and lengthBar so as - // to make some subsequent calculations easier. - final boolean isVertical = isVertical(); - VirtualScrollBar breadthBar = isVertical ? hbar : vbar; - VirtualScrollBar lengthBar = isVertical ? vbar : hbar; - - // We may have adjusted the viewport length and breadth after the - // layout due to scroll bars becoming visible. So we need to perform - // a follow up pass and resize and shift all the cells to fit the - // viewport. Note that the prospective viewport size is always >= the - // final viewport size, so we don't have to worry about adding - // cells during this cleanup phase. - fitCells(); - - // Update cell positions. - // When rebuilding the cells, we add the cells and along the way compute - // the maxPrefBreadth. Based on the computed value, we may add - // the breadth scrollbar which changes viewport length, so we need - // to re-position the cells. - if (!cells.isEmpty()) { - final double currOffset = -computeViewportOffset(getPosition()); - final int currIndex = computeCurrentIndex() - cells.getFirst().getIndex(); - final int size = cells.size(); - - // position leading cells - double offset = currOffset; - - for (int i = currIndex - 1; i >= 0 && i < size; i--) { - final T cell = cells.get(i); - - offset -= getCellLength(cell); - - positionCell(cell, offset); - } - - // position trailing cells - offset = currOffset; - for (int i = currIndex; i >= 0 && i < size; i++) { - final T cell = cells.get(i); - positionCell(cell, offset); - - offset += getCellLength(cell); - } - } - - // Toggle visibility on the corner - corner.setVisible(breadthBar.isVisible() && lengthBar.isVisible()); - - double sumCellLength = 0; - double flowLength = (isVertical ? getHeight() : getWidth()) - - (breadthBar.isVisible() ? breadthBar.prefHeight(-1) : 0); - - final double viewportBreadth = getViewportBreadth(); - final double viewportLength = getViewportLength(); - - // Now position and update the scroll bars - if (breadthBar.isVisible()) { - /* - ** Positioning the ScrollBar - */ - if (!IS_TOUCH_SUPPORTED) { - if (isVertical) { - hbar.resizeRelocate(0, viewportLength, - viewportBreadth, hbar.prefHeight(viewportBreadth)); - } else { - vbar.resizeRelocate(viewportLength, 0, - vbar.prefWidth(viewportBreadth), viewportBreadth); - } - } else { - if (isVertical) { - hbar.resizeRelocate(0, (viewportLength - hbar.getHeight()), - viewportBreadth, hbar.prefHeight(viewportBreadth)); - } else { - vbar.resizeRelocate((viewportLength - vbar.getWidth()), 0, - vbar.prefWidth(viewportBreadth), viewportBreadth); - } - } - - if (getMaxPrefBreadth() != -1) { - double newMax = Math.max(1, getMaxPrefBreadth() - viewportBreadth); - if (newMax != breadthBar.getMax()) { - breadthBar.setMax(newMax); - - double breadthBarValue = breadthBar.getValue(); - boolean maxed = breadthBarValue != 0 && newMax == breadthBarValue; - if (maxed || breadthBarValue > newMax) { - breadthBar.setValue(newMax); - } - - breadthBar.setVisibleAmount((viewportBreadth / getMaxPrefBreadth()) * newMax); - } - } - } - - // determine how many cells there are on screen so that the scrollbar - // thumb can be appropriately sized - if (recreate && (lengthBar.isVisible() || IS_TOUCH_SUPPORTED)) { - int numCellsVisibleOnScreen = 0; - for (int i = 0, max = cells.size(); i < max; i++) { - T cell = cells.get(i); - if (cell != null) {// && !cell.isEmpty()) { - sumCellLength += (isVertical ? cell.getHeight() : cell.getWidth()); - if (sumCellLength > flowLength) { - break; - } - - numCellsVisibleOnScreen++; - } - } - - lengthBar.setMax(1); - if (numCellsVisibleOnScreen == 0 && cellCount == 1) { - // special case to help resolve RT-17701 and the case where we have - // only a single row and it is bigger than the viewport - lengthBar.setVisibleAmount(flowLength / sumCellLength); - } else { - lengthBar.setVisibleAmount(numCellsVisibleOnScreen / (float) cellCount); - } - } - - if (lengthBar.isVisible()) { - // Fix for RT-11873. If this isn't here, we can have a situation where - // the scrollbar scrolls endlessly. This is possible when the cell - // count grows as the user hits the maximal position on the scrollbar - // (i.e. the list size dynamically grows as the user needs more). - // - // This code was commented out to resolve RT-14477 after testing - // whether RT-11873 can be recreated. It could not, and therefore - // for now this code will remained uncommented until it is deleted - // following further testing. -// if (lengthBar.getValue() == 1.0 && lastCellCount != cellCount) { -// lengthBar.setValue(0.99); -// } - - /* - ** Positioning the ScrollBar - */ - if (!IS_TOUCH_SUPPORTED) { - if (isVertical) { - vbar.resizeRelocate(viewportBreadth, 0, vbar.prefWidth(viewportLength), viewportLength); - } else { - hbar.resizeRelocate(0, viewportBreadth, viewportLength, hbar.prefHeight(-1)); - } - } else { - if (isVertical) { - vbar.resizeRelocate((viewportBreadth - vbar.getWidth()), 0, vbar.prefWidth(viewportLength), viewportLength); - } else { - hbar.resizeRelocate(0, (viewportBreadth - hbar.getHeight()), viewportLength, hbar.prefHeight(-1)); - } - } - } - - if (corner.isVisible()) { - if (!IS_TOUCH_SUPPORTED) { - corner.resize(vbar.getWidth(), hbar.getHeight()); - corner.relocate(hbar.getLayoutX() + hbar.getWidth(), vbar.getLayoutY() + vbar.getHeight()); - } else { - corner.resize(vbar.getWidth(), hbar.getHeight()); - corner.relocate(hbar.getLayoutX() + (hbar.getWidth() - vbar.getWidth()), vbar.getLayoutY() + (vbar.getHeight() - hbar.getHeight())); - hbar.resize(hbar.getWidth() - vbar.getWidth(), hbar.getHeight()); - vbar.resize(vbar.getWidth(), vbar.getHeight() - hbar.getHeight()); - } - } - - clipView.resize(snapSize(isVertical ? viewportBreadth : viewportLength), - snapSize(isVertical ? viewportLength : viewportBreadth)); - - // If the viewportLength becomes large enough that all cells fit - // within the viewport, then we want to update the value to match. - if (getPosition() != lengthBar.getValue()) { - lengthBar.setValue(getPosition()); - } - } - - /** - * Adjusts the cells location and size if necessary. The breadths of all - * cells will be adjusted to fit the viewportWidth or maxPrefBreadth, and - * the layout position will be updated if necessary based on index and - * offset. - */ - private void fitCells() { - double size = Math.max(getMaxPrefBreadth(), getViewportBreadth()); - boolean isVertical = isVertical(); - - // Note: Do not optimise this loop by pre-calculating the cells size and - // storing that into a int value - this can lead to RT-32828 - for (int i = 0; i < cells.size(); i++) { - PageView cell = cells.get(i); - if (isVertical) { - cell.resize(size, cell.prefHeight(size)); - } else { - cell.resize(cell.prefWidth(size), size); - } - } - } - - private void cull() { - final double viewportLength = getViewportLength(); - for (int i = cells.size() - 1; i >= 0; i--) { - T cell = cells.get(i); - double cellSize = getCellLength(cell); - double cellStart = getCellPosition(cell); - double cellEnd = cellStart + cellSize; - if (cellStart >= viewportLength || cellEnd < 0) { - addToPile(cells.remove(i)); - } - } - } - - /*************************************************************************** - * * - * Helper functions for working with cells * - * * - **************************************************************************/ - - /** - * Return the index for a given cell. This allows subclasses to customise - * how cell indices are retrieved. - */ - protected int getCellIndex(T cell) { - return cell.getIndex(); - } - - - /** - * Return a cell for the given index. This may be called for any cell, - * including beyond the range defined by cellCount, in which case an - * empty cell will be returned. The returned value should not be stored for - * any reason. - */ - public T getCell(int index) { - // If there are cells, then we will attempt to get an existing cell - if (!cells.isEmpty()) { - // First check the cells that have already been created and are - // in use. If this call returns a value, then we can use it - T cell = getVisibleCell(index); - if (cell != null) return cell; - } - - // check the pile - for (int i = 0; i < pile.size(); i++) { - T cell = pile.get(i); - if (getCellIndex(cell) == index) { - // Note that we don't remove from the pile: if we do it leads - // to a severe performance decrease. This seems to be OK, as - // getCell() is only used for cell measurement purposes. - // pile.remove(i); - return cell; - } - } - - if (pile.size() > 0) { - return pile.get(0); - } - - // We need to use the accumCell and return that - if (accumCell == null) { - Callback createCell = getCreateCell(); - if (createCell != null) { - accumCell = createCell.call(this); - accumCell.getProperties().put(NEW_CELL, null); - accumCellParent.getChildren().setAll(accumCell); - - // Note the screen reader will attempt to find all - // the items inside the view to calculate the item count. - // Having items under different parents (sheet and accumCellParent) - // leads the screen reader to compute wrong values. - // The regular scheme to provide items to the screen reader - // uses getPrivateCell(), which places the item in the sheet. - // The accumCell, and its children, should be ignored by the - // screen reader. - accumCell.setAccessibleRole(AccessibleRole.NODE); - accumCell.getChildrenUnmodifiable().addListener((Observable c) -> { - for (Node n : accumCell.getChildrenUnmodifiable()) { - n.setAccessibleRole(AccessibleRole.NODE); - } - }); - } - } - setCellIndex(accumCell, index); - resizeCellSize(accumCell); - return accumCell; - } - - /** - * After using the accum cell, it needs to be released! - */ - private void releaseCell(T cell) { - if (accumCell != null && cell == accumCell) { - accumCell.updateIndex(-1); - } - } - - /** - * This method is an experts-only method - if the requested index is not - * already an existing visible cell, it will create a cell for the - * given index and insert it into the sheet. From that point on it will be - * unmanaged, and is up to the caller of this method to manage it. - */ - T getPrivateCell(int index) { - T cell = null; - - // If there are cells, then we will attempt to get an existing cell - if (!cells.isEmpty()) { - // First check the cells that have already been created and are - // in use. If this call returns a value, then we can use it - cell = getVisibleCell(index); - if (cell != null) { - // Force the underlying text inside the cell to be updated - // so that when the screen reader runs, it will match the - // text in the cell (force updateDisplayedText()) - cell.layout(); - return cell; - } - } - - // check the existing sheet children - if (cell == null) { - for (int i = 0; i < sheetChildren.size(); i++) { - T _cell = (T) sheetChildren.get(i); - if (getCellIndex(_cell) == index) { - return _cell; - } - } - } - - if (cell == null) { - Callback createCell = getCreateCell(); - if (createCell != null) { - cell = createCell.call(this); - } - } - - if (cell != null) { - setCellIndex(cell, index); - resizeCellSize(cell); - cell.setVisible(false); - sheetChildren.add(cell); - privateCells.add(cell); - } - - return cell; - } - - private final List privateCells = new ArrayList<>(); - - private void releaseAllPrivateCells() { - sheetChildren.removeAll(privateCells); - } - - /** - * Compute and return the length of the cell for the given index. This is - * called both internally when adjusting by pixels, and also at times - * by PositionMapper (see the getItemSize callback). When called by - * PositionMapper, it is possible that it will be called for some index - * which is not associated with any cell, so we have to do a bit of work - * to use a cell as a helper for computing cell size in some cases. - */ - protected double getCellLength(int index) { - if (fixedCellSizeEnabled) return fixedCellSize; - - T cell = getCell(index); - double length = getCellLength(cell); - releaseCell(cell); - return length; - } - - /** - */ - protected double getCellBreadth(int index) { - T cell = getCell(index); - double b = getCellBreadth(cell); - releaseCell(cell); - return b; - } - - /** - * Gets the length of a specific cell - */ - protected double getCellLength(T cell) { - if (cell == null) return 0; - if (fixedCellSizeEnabled) return fixedCellSize; - - return isVertical() ? - cell.getLayoutBounds().getHeight() - : cell.getLayoutBounds().getWidth(); - } - -// private double getCellPrefLength(T cell) { -// return isVertical() ? -// cell.prefHeight(-1) -// : cell.prefWidth(-1); -// } - - /** - * Gets the breadth of a specific cell - */ - protected double getCellBreadth(PageView cell) { - return isVertical() ? - cell.prefWidth(-1) - : cell.prefHeight(-1); - } - - /** - * Gets the layout position of the cell along the length axis - */ - protected double getCellPosition(T cell) { - if (cell == null) return 0; - - return isVertical() ? - cell.getLayoutY() - : cell.getLayoutX(); - } - - protected void positionCell(T cell, double position) { - if (isVertical()) { - cell.setLayoutX(0);//getWidth()/2 - cell.getPageBounds().getWidth()/2); - cell.setLayoutY((position)); - } else { - cell.setLayoutX(snapSize(position)); - cell.setLayoutY(0); - } - } - - protected void resizeCellSize(T cell) { - if (cell == null) return; - - if (isVertical()) { - double width = Math.max(getMaxPrefBreadth(), getViewportBreadth()); - cell.resize(width, fixedCellSizeEnabled ? fixedCellSize : Utils.boundedSize(cell.prefHeight(width), cell.minHeight(width), cell.maxHeight(width))); - } else { - double height = Math.max(getMaxPrefBreadth(), getViewportBreadth()); - cell.resize(fixedCellSizeEnabled ? fixedCellSize : Utils.boundedSize(cell.prefWidth(height), cell.minWidth(height), cell.maxWidth(height)), height); - } - } - - protected void setCellIndex(T cell, int index) { - assert cell != null; - - cell.updateIndex(index); - - // make sure the cell is sized correctly. This is important for both - // general layout of cells in a VirtualFlow, but also in cases such as - // RT-34333, where the sizes were being reported incorrectly to the - // ComboBox popup. - if ((cell.isNeedsLayout() && cell.getScene() != null) || cell.getProperties().containsKey(NEW_CELL)) { - cell.applyCss(); - cell.getProperties().remove(NEW_CELL); - } - } - - /*************************************************************************** - * * - * Helper functions for cell management * - * * - **************************************************************************/ - - - /** - * Indicates that this is a newly created cell and we need call impl_processCSS for it. - *

- * See RT-23616 for more details. - */ - private static final String NEW_CELL = "newcell"; - - /** - * Get a cell which can be used in the layout. This function will reuse - * cells from the pile where possible, and will create new cells when - * necessary. - */ - protected T getAvailableCell(int prefIndex) { - T cell = null; - - // Fix for RT-12822. We try to retrieve the cell from the pile rather - // than just grab a random cell from the pile (or create another cell). - for (int i = 0, max = pile.size(); i < max; i++) { - T _cell = pile.get(i); - assert _cell != null; - - if (getCellIndex(_cell) == prefIndex) { - cell = _cell; - pile.remove(i); - break; - } - cell = null; - } - - if (cell == null) { - if (pile.size() > 0) { - // we try to get a cell with an index that is the same even/odd - // as the prefIndex. This saves us from having to run so much - // css on the cell as it will not change from even to odd, or - // vice versa - final boolean prefIndexIsEven = (prefIndex & 1) == 0; - for (int i = 0, max = pile.size(); i < max; i++) { - final T c = pile.get(i); - final int cellIndex = getCellIndex(c); - - if ((cellIndex & 1) == 0 && prefIndexIsEven) { - cell = c; - pile.remove(i); - break; - } else if ((cellIndex & 1) == 1 && !prefIndexIsEven) { - cell = c; - pile.remove(i); - break; - } - } - - if (cell == null) { - cell = pile.removeFirst(); - } - } else { - cell = getCreateCell().call(this); - cell.getProperties().put(NEW_CELL, null); - } - } - - if (cell.getParent() == null) { - sheetChildren.add(cell); - } - - return cell; - } - - // protected to allow subclasses to clean up - protected void addAllToPile() { - for (int i = 0, max = cells.size(); i < max; i++) { - addToPile(cells.removeFirst()); - } - } - - /** - * Puts the given cell onto the pile. This is called whenever a cell has - * fallen off the flow's start. - */ - private void addToPile(T cell) { - assert cell != null; - pile.addLast(cell); - } - - private void cleanPile() { - boolean wasFocusOwner = false; - - for (int i = 0, max = pile.size(); i < max; i++) { - T cell = pile.get(i); - wasFocusOwner = wasFocusOwner || doesCellContainFocus(cell); - cell.setVisible(false); - } - - // Fix for RT-35876: Rather than have the cells do weird things with - // focus (in particular, have focus jump between cells), we return focus - // to the VirtualFlow itself. - if (wasFocusOwner) { - requestFocus(); - } - } - - private boolean doesCellContainFocus(PageView c) { - Scene scene = c.getScene(); - final Node focusOwner = scene == null ? null : scene.getFocusOwner(); - - if (focusOwner != null) { - if (c.equals(focusOwner)) { - return true; - } - - Parent p = focusOwner.getParent(); - while (p != null && !(p instanceof VirtualPageFlow)) { - if (c.equals(p)) { - return true; - } - p = p.getParent(); - } - } - - return false; - } - - /** - * Gets a cell for the given index if the cell has been created and laid out. - * "Visible" is a bit of a misnomer, the cell might not be visible in the - * viewport (it may be clipped), but does distinguish between cells that - * have been created and are in use vs. those that are in the pile or - * not created. - */ - public T getVisibleCell(int index) { - if (cells.isEmpty()) return null; - - // check the last index - T lastCell = cells.getLast(); - int lastIndex = getCellIndex(lastCell); - if (index == lastIndex) return lastCell; - - // check the first index - T firstCell = cells.getFirst(); - int firstIndex = getCellIndex(firstCell); - if (index == firstIndex) return firstCell; - - // if index is > firstIndex and < lastIndex then we can get the index - if (index > firstIndex && index < lastIndex) { - T cell = cells.get(index - firstIndex); - if (getCellIndex(cell) == index) return cell; - } - - // there is no visible cell for the specified index - return null; - } - - /** - * Locates and returns the last non-empty IndexedCell that is currently - * partially or completely visible. This function may return null if there - * are no cells, or if the viewport length is 0. - */ - public T getLastVisibleCell() { - if (cells.isEmpty() || getViewportLength() <= 0) return null; - - T cell; - for (int i = cells.size() - 1; i >= 0; i--) { - cell = cells.get(i); -// if (!cell.isEmpty()) { - return cell; -// } - } - - return null; - } - - /** - * Locates and returns the first non-empty IndexedCell that is partially or - * completely visible. This really only ever returns null if there are no - * cells or the viewport length is 0. - */ - public T getFirstVisibleCell() { - if (cells.isEmpty() || getViewportLength() <= 0) return null; - T cell = cells.getFirst(); - return cell; //cell.isEmpty() ? null : cell; - } - - // Returns last visible cell whose bounds are entirely within the viewport - public T getLastVisibleCellWithinViewPort() { - if (cells.isEmpty() || getViewportLength() <= 0) return null; - - T cell; - final double max = getViewportLength(); - for (int i = cells.size() - 1; i >= 0; i--) { - cell = cells.get(i); -// if (cell.isEmpty()) continue; - - final double cellStart = getCellPosition(cell); - final double cellEnd = cellStart + getCellLength(cell); - - // we use the magic +2 to allow for a little bit of fuzziness, - // this is to help in situations such as RT-34407 - if (cellEnd <= (max + 2)) { - return cell; - } - } - - return null; - } - - // Returns first visible cell whose bounds are entirely within the viewport - public T getFirstVisibleCellWithinViewPort() { - if (cells.isEmpty() || getViewportLength() <= 0) return null; - - T cell; - for (int i = 0; i < cells.size(); i++) { - cell = cells.get(i); -// if (cell.isEmpty()) continue; - - final double cellStart = getCellPosition(cell); - if (cellStart >= 0) { - return cell; - } - } - - return null; - } - - /** - * Adjust the position of cells so that the specified cell - * will be positioned at the start of the viewport. The given cell must - * already be "live". This is bad public API! - */ - public void showAsFirst(T firstCell) { - if (firstCell != null) { - adjustPixels(getCellPosition(firstCell)); - } - } - - /** - * Adjust the position of cells so that the specified cell - * will be positioned at the end of the viewport. The given cell must - * already be "live". This is bad public API! - */ - public void showAsLast(T lastCell) { - if (lastCell != null) { - adjustPixels(getCellPosition(lastCell) + getCellLength(lastCell) - getViewportLength()); - } - } - - /** - * Adjusts the cells such that the selected cell will be fully visible in - * the viewport (but only just). - */ - public void show(T cell) { - if (cell != null) { - final double start = getCellPosition(cell); - final double length = getCellLength(cell); - final double end = start + length; - final double viewportLength = getViewportLength(); - - if (start < 0) { - adjustPixels(start); - } else if (end > viewportLength) { - adjustPixels(end - viewportLength); - } - } - } - - public void show(int index) { - T cell = getVisibleCell(index); - if (cell != null) { - show(cell); - } else { - // See if the previous index is a visible cell - T prev = getVisibleCell(index - 1); - if (prev != null) { - // Need to add a new cell and then we can show it -// layingOut = true; - cell = getAvailableCell(index); - setCellIndex(cell, index); - resizeCellSize(cell); // resize must be after config - cells.addLast(cell); - positionCell(cell, getCellPosition(prev) + getCellLength(prev)); - setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell))); - cell.setVisible(true); - show(cell); -// layingOut = false; - return; - } - // See if the next index is a visible cell - T next = getVisibleCell(index + 1); - if (next != null) { -// layingOut = true; - cell = getAvailableCell(index); - setCellIndex(cell, index); - resizeCellSize(cell); // resize must be after config - cells.addFirst(cell); - positionCell(cell, getCellPosition(next) - getCellLength(cell)); - setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell))); - cell.setVisible(true); - show(cell); -// layingOut = false; - return; - } - - // In this case, we're asked to show a random cell -// layingOut = true; - adjustPositionToIndex(index); - addAllToPile(); - requestLayout(); -// layingOut = false; - } - } - - public void scrollTo(int index) { - boolean posSet = false; - - if (index >= cellCount - 1) { - setPosition(1); - posSet = true; - } else if (index < 0) { - setPosition(0); - posSet = true; - } - - if (!posSet) { - adjustPositionToIndex(index); - double offset = -computeOffsetForCell(index); - adjustByPixelAmount(offset); - } - - requestLayout(); - } - - //TODO We assume all the cell have the same length. We will need to support - // cells of different lengths. - public void scrollToOffset(int offset) { - adjustPixels(offset * getCellLength(0)); - } - - /** - * Given a delta value representing a number of pixels, this method attempts - * to move the VirtualFlow in the given direction (positive is down/right, - * negative is up/left) the given number of pixels. It returns the number of - * pixels actually moved. - */ - public double adjustPixels(final double delta) { - // Short cut this method for cases where nothing should be done - if (delta == 0) return 0; - - final boolean isVertical = isVertical(); - if (((isVertical && (tempVisibility ? !needLengthBar : !vbar.isVisible())) || - (!isVertical && (tempVisibility ? !needLengthBar : !hbar.isVisible())))) return 0; - - double pos = getPosition(); - if (pos == 0.0f && delta < 0) return 0; - if (pos == 1.0f && delta > 0) return 0; - - adjustByPixelAmount(delta); - if (pos == getPosition()) { - // The pos hasn't changed, there's nothing to do. This is likely - // to occur when we hit either extremity - return 0; - } - - // Now move stuff around. Translating by pixels fundamentally means - // moving the cells by the delta. However, after having - // done that, we need to go through the cells and see which cells, - // after adding in the translation factor, now fall off the viewport. - // Also, we need to add cells as appropriate to the end (or beginning, - // depending on the direction of travel). - // - // One simplifying assumption (that had better be true!) is that we - // will only make it this far in the function if the virtual scroll - // bar is visible. Otherwise, we never will pixel scroll. So as we go, - // if we find that the maxPrefBreadth exceeds the viewportBreadth, - // then we will be sure to show the breadthBar and update it - // accordingly. - if (cells.size() > 0) { - double position; - for (int i = 0; i < cells.size(); i++) { - T cell = cells.get(i); - assert cell != null; - position = getCellPosition(cell) - delta; - positionCell(cell, position); - } - - // Fix for RT-32908 - T firstCell = cells.getFirst(); - double layoutY = firstCell == null ? 0 : getCellPosition(firstCell); - for (int i = 0; i < cells.size(); i++) { - T cell = cells.get(i); - assert cell != null; - double actualLayoutY = getCellPosition(cell); - if (actualLayoutY != layoutY) { - // we need to shift the cell to layoutY - positionCell(cell, layoutY); - } - - layoutY += getCellLength(cell); - } - // end of fix for RT-32908 - cull(); - firstCell = cells.getFirst(); - - // Add any necessary leading cells - if (firstCell != null) { - int firstIndex = getCellIndex(firstCell); - double prevIndexSize = getCellLength(firstIndex - 1); - addLeadingCells(firstIndex - 1, getCellPosition(firstCell) - prevIndexSize); - } else { - int currentIndex = computeCurrentIndex(); - - // The distance from the top of the viewport to the top of the - // cell for the current index. - double offset = -computeViewportOffset(getPosition()); - - // Add all the leading and trailing cells (the call to add leading - // cells will add the current cell as well -- that is, the one that - // represents the current position on the mapper). - addLeadingCells(currentIndex, offset); - } - - // Starting at the tail of the list, loop adding cells until - // all the space on the table is filled up. We want to make - // sure that we DO NOT add empty trailing cells (since we are - // in the full virtual case and so there are no trailing empty - // cells). - if (!addTrailingCells(false)) { - // Reached the end, but not enough cells to fill up to - // the end. So, remove the trailing empty space, and translate - // the cells down - final T lastCell = getLastVisibleCell(); - final double lastCellSize = getCellLength(lastCell); - final double cellEnd = getCellPosition(lastCell) + lastCellSize; - final double viewportLength = getViewportLength(); - - if (cellEnd < viewportLength) { - // Reposition the nodes - double emptySize = viewportLength - cellEnd; - for (int i = 0; i < cells.size(); i++) { - T cell = cells.get(i); - positionCell(cell, getCellPosition(cell) + emptySize); - } - setPosition(1.0f); - // fill the leading empty space - firstCell = cells.getFirst(); - int firstIndex = getCellIndex(firstCell); - double prevIndexSize = getCellLength(firstIndex - 1); - addLeadingCells(firstIndex - 1, getCellPosition(firstCell) - prevIndexSize); - } - } - } - - // Now throw away any cells that don't fit - cull(); - - // Finally, update the scroll bars - updateScrollBarsAndCells(false); - lastPosition = getPosition(); - - // notify - return delta; // TODO fake - } - - private boolean needsReconfigureCells = false; // when cell contents are the same - private boolean needsRecreateCells = false; // when cell factory changed - private boolean needsRebuildCells = false; // when cell contents have changed - private boolean needsCellsLayout = false; - private boolean sizeChanged = false; - private final BitSet dirtyCells = new BitSet(); - - public void reconfigureCells() { - needsReconfigureCells = true; - requestLayout(); - } - - public void recreateCells() { - needsRecreateCells = true; - requestLayout(); - } - - public void rebuildCells() { - needsRebuildCells = true; - requestLayout(); - } - - public void requestCellLayout() { - needsCellsLayout = true; - requestLayout(); - } - - public void setCellDirty(int index) { - dirtyCells.set(index); - requestLayout(); - } - - private static final double GOLDEN_RATIO_MULTIPLIER = 0.618033987; - - private double getPrefBreadth(double oppDimension) { - double max = getMaxCellWidth(10); - - // This primarily exists for the case where we do not want the breadth - // to grow to ensure a golden ratio between width and height (for example, - // when a ListView is used in a ComboBox - the width should not grow - // just because items are being added to the ListView) - if (oppDimension > -1) { - double prefLength = getPrefLength(); - max = Math.max(max, prefLength * GOLDEN_RATIO_MULTIPLIER); - } - - return max; - } - - private double getPrefLength() { - double sum = 0.0; - int rows = Math.min(10, cellCount); - for (int i = 0; i < rows; i++) { - sum += getCellLength(i); - } - return sum; - } - - @Override - protected double computePrefWidth(double height) { - double w = isVertical() ? getPrefBreadth(height) : getPrefLength(); - return w + vbar.prefWidth(-1); - } - - @Override - protected double computePrefHeight(double width) { - double h = isVertical() ? getPrefLength() : getPrefBreadth(width); - return h + hbar.prefHeight(-1); - } - - double getMaxCellWidth(int rowsToCount) { - double max = 0.0; - - // we always measure at least one row - int rows = Math.max(1, rowsToCount == -1 ? cellCount : rowsToCount); - for (int i = 0; i < rows; i++) { - max = Math.max(max, getCellBreadth(i)); - } - return max; - } - - - // Old PositionMapper - - /** - * Given a position value between 0 and 1, compute and return the viewport - * offset from the "current" cell associated with that position value. - * That is, if the return value of this function where used as a translation - * factor for a sheet that contained all the items, then the current - * item would end up positioned correctly. - */ - private double computeViewportOffset(double position) { - double p = com.sun.javafx.util.Utils.clamp(0, position, 1); - double fractionalPosition = p * getCellCount(); - int cellIndex = (int) fractionalPosition; - double fraction = fractionalPosition - cellIndex; - double cellSize = getCellLength(cellIndex); - double pixelOffset = cellSize * fraction; - double viewportOffset = getViewportLength() * p; - return pixelOffset - viewportOffset; - } - - private void adjustPositionToIndex(int index) { - int cellCount = getCellCount(); - if (cellCount <= 0) { - setPosition(0.0f); - } else { - setPosition(((double) index) / cellCount); - } - } - - /** - * Adjust the position based on a delta of pixels. If negative, then the - * position will be adjusted negatively. If positive, then the position will - * be adjusted positively. If the pixel amount is too great for the range of - * the position, then it will be clamped such that position is always - * strictly between 0 and 1 - */ - private void adjustByPixelAmount(double numPixels) { - if (numPixels == 0) return; - // Starting from the current cell, we move in the direction indicated - // by numPixels one cell at a team. For each cell, we discover how many - // pixels the "position" line would move within that cell, and adjust - // our count of numPixels accordingly. When we come to the "final" cell, - // then we can take the remaining number of pixels and multiply it by - // the "travel rate" of "p" within that cell to get the delta. Add - // the delta to "p" to get position. - - // get some basic info about the list and the current cell - boolean forward = numPixels > 0; - int cellCount = getCellCount(); - double fractionalPosition = getPosition() * cellCount; - int cellIndex = (int) fractionalPosition; - if (forward && cellIndex == cellCount) return; - double cellSize = getCellLength(cellIndex); - double fraction = fractionalPosition - cellIndex; - double pixelOffset = cellSize * fraction; - - // compute the percentage of "position" that represents each cell - double cellPercent = 1.0 / cellCount; - - // To help simplify the algorithm, we pretend as though the current - // position is at the beginning of the current cell. This reduces some - // of the corner cases and provides a simpler algorithm without adding - // any overhead to performance. - double start = computeOffsetForCell(cellIndex); - double end = cellSize + computeOffsetForCell(cellIndex + 1); - - // We need to discover the distance that the fictional "position line" - // would travel within this cell, from its current position to the end. - double remaining = end - start; - - // Keep track of the number of pixels left to travel - double n = forward ? - numPixels + pixelOffset - (getViewportLength() * getPosition()) - start - : -numPixels + end - (pixelOffset - (getViewportLength() * getPosition())); - - // "p" represents the most recent value for position. This is always - // based on the edge between two cells, except at the very end of the - // algorithm where it is added to the computed "p" offset for the final - // value of Position. - double p = cellPercent * cellIndex; - - // Loop over the cells one at a time until either we reach the end of - // the cells, or we find that the "n" will fall within the cell we're on - while (n > remaining && ((forward && cellIndex < cellCount - 1) || (!forward && cellIndex > 0))) { - if (forward) cellIndex++; - else cellIndex--; - n -= remaining; - cellSize = getCellLength(cellIndex); - start = computeOffsetForCell(cellIndex); - end = cellSize + computeOffsetForCell(cellIndex + 1); - remaining = end - start; - p = cellPercent * cellIndex; - } - - // if remaining is < n, then we must have hit an end, so as a - // fast path, we can just set position to 1.0 or 0.0 and return - // because we know we hit the end - if (n > remaining) { - setPosition(forward ? 1.0f : 0.0f); - } else if (forward) { - double rate = cellPercent / Math.abs(end - start); - setPosition(p + (rate * n)); - } else { - double rate = cellPercent / Math.abs(end - start); - setPosition((p + cellPercent) - (rate * n)); - } - } - - private int computeCurrentIndex() { - return (int) (getPosition() * getCellCount()); - } - - /** - * Given an item index, this function will compute and return the viewport - * offset from the beginning of the specified item. Notice that because each - * item has the same percentage of the position dedicated to it, and since - * we are measuring from the start of each item, this is a very simple - * calculation. - */ - private double computeOffsetForCell(int itemIndex) { - double cellCount = getCellCount(); - double p = com.sun.javafx.util.Utils.clamp(0, itemIndex, cellCount) / cellCount; - return -(getViewportLength() * p); - } - -// /** -// * Adjust the position based on a chunk of pixels. The position is based -// * on the start of the scrollbar position. -// */ -// private void adjustByPixelChunk(double numPixels) { -// setPosition(0); -// adjustByPixelAmount(numPixels); -// } - // end of old PositionMapper code - - - /** - * A simple extension to Region that ensures that anything wanting to flow - * outside of the bounds of the Region is clipped. - */ - static class ClippedContainer extends Region { - - /** - * The Node which is embedded within this {@code ClipView}. - */ - private Node node; - - public Node getNode() { - return this.node; - } - - public void setNode(Node n) { - this.node = n; - - getChildren().clear(); - getChildren().add(node); - } - - public void setClipX(double clipX) { - setLayoutX(-clipX); - clipRect.setLayoutX(clipX); - } - - public void setClipY(double clipY) { - setLayoutY(-clipY); - clipRect.setLayoutY(clipY); - } - - private final Rectangle clipRect; - - public ClippedContainer(final VirtualPageFlow flow) { - if (flow == null) { - throw new IllegalArgumentException("VirtualFlow can not be null"); - } - - getStyleClass().add("clipped-container"); - - // clipping - clipRect = new Rectangle(); - clipRect.setSmooth(false); - setClip(clipRect); - // --- clipping - - super.widthProperty().addListener(valueModel -> { - clipRect.setWidth(getWidth()); - }); - super.heightProperty().addListener(valueModel -> { - clipRect.setHeight(getHeight()); - }); - } - } - - /** - * A List-like implementation that is exceedingly efficient for the purposes - * of the VirtualFlow. Typically there is not much variance in the number of - * cells -- it is always some reasonably consistent number. Yet for efficiency - * in code, we like to use a linked list implementation so as to append to - * start or append to end. However, at times when we need to iterate, LinkedList - * is expensive computationally as well as requiring the construction of - * temporary iterators. - *

- * This linked list like implementation is done using an array. It begins by - * putting the first item in the center of the allocated array, and then grows - * outward (either towards the first or last of the array depending on whether - * we are inserting at the head or tail). It maintains an index to the start - * and end of the array, so that it can efficiently expose iteration. - *

- * This class is package private solely for the sake of testing. - */ - public static class ArrayLinkedList extends AbstractList { - /** - * The array list backing this class. We default the size of the array - * list to be fairly large so as not to require resizing during normal - * use, and since that many ArrayLinkedLists won't be created it isn't - * very painful to do so. - */ - private final ArrayList array; - - private int firstIndex = -1; - private int lastIndex = -1; - - public ArrayLinkedList() { - array = new ArrayList(50); - - for (int i = 0; i < 50; i++) { - array.add(null); - } - } - - public T getFirst() { - return firstIndex == -1 ? null : array.get(firstIndex); - } - - public T getLast() { - return lastIndex == -1 ? null : array.get(lastIndex); - } - - public void addFirst(T cell) { - // if firstIndex == -1 then that means this is the first item in the - // list and we need to initialize firstIndex and lastIndex - if (firstIndex == -1) { - firstIndex = lastIndex = array.size() / 2; - array.set(firstIndex, cell); - } else if (firstIndex == 0) { - // we're already at the head of the array, so insert at position - // 0 and then increment the lastIndex to compensate - array.add(0, cell); - lastIndex++; - } else { - // we're not yet at the head of the array, so insert at the - // firstIndex - 1 position and decrement first position - array.set(--firstIndex, cell); - } - } - - public void addLast(T cell) { - // if lastIndex == -1 then that means this is the first item in the - // list and we need to initialize the firstIndex and lastIndex - if (firstIndex == -1) { - firstIndex = lastIndex = array.size() / 2; - array.set(lastIndex, cell); - } else if (lastIndex == array.size() - 1) { - // we're at the end of the array so need to "add" so as to force - // the array to be expanded in size - array.add(++lastIndex, cell); - } else { - array.set(++lastIndex, cell); - } - } - - public int size() { - return firstIndex == -1 ? 0 : lastIndex - firstIndex + 1; - } - - public boolean isEmpty() { - return firstIndex == -1; - } - - public T get(int index) { - if (index > (lastIndex - firstIndex) || index < 0) { - // Commented out exception due to RT-29111 - // throw new java.lang.ArrayIndexOutOfBoundsException(); - return null; - } - - return array.get(firstIndex + index); - } - - public void clear() { - for (int i = 0; i < array.size(); i++) { - array.set(i, null); - } - - firstIndex = lastIndex = -1; - } - - public T removeFirst() { - if (isEmpty()) return null; - return remove(0); - } - - public T removeLast() { - if (isEmpty()) return null; - return remove(lastIndex - firstIndex); - } - - public T remove(int index) { - if (index > lastIndex - firstIndex || index < 0) { - throw new java.lang.ArrayIndexOutOfBoundsException(); - } - - // if the index == 0, then we're removing the first - // item and can simply set it to null in the array and increment - // the firstIndex unless there is only one item, in which case - // we have to also set first & last index to -1. - if (index == 0) { - T cell = array.get(firstIndex); - array.set(firstIndex, null); - if (firstIndex == lastIndex) { - firstIndex = lastIndex = -1; - } else { - firstIndex++; - } - return cell; - } else if (index == lastIndex - firstIndex) { - // if the index == lastIndex - firstIndex, then we're removing the - // last item and can simply set it to null in the array and - // decrement the lastIndex - T cell = array.get(lastIndex); - array.set(lastIndex--, null); - return cell; - } else { - // if the index is somewhere in between, then we have to remove the - // item and decrement the lastIndex - T cell = array.get(firstIndex + index); - array.set(firstIndex + index, null); - for (int i = (firstIndex + index + 1); i <= lastIndex; i++) { - array.set(i - 1, array.get(i)); - } - array.set(lastIndex--, null); - return cell; - } - } - } - - Timeline sbTouchTimeline; - KeyFrame sbTouchKF1; - KeyFrame sbTouchKF2; - - private boolean needBreadthBar; - private boolean needLengthBar; - private boolean tempVisibility = false; - - protected void startSBReleasedAnimation() { - if (sbTouchTimeline == null) { - /* - ** timeline to leave the scrollbars visible for a short - ** while after a scroll/drag - */ - sbTouchTimeline = new Timeline(); - sbTouchKF1 = new KeyFrame(Duration.millis(0), event -> { - tempVisibility = true; - requestLayout(); - }); - - sbTouchKF2 = new KeyFrame(Duration.millis(1000), event -> { - if (touchDetected == false && mouseDown == false) { - tempVisibility = false; - requestLayout(); - } - }); - sbTouchTimeline.getKeyFrames().addAll(sbTouchKF1, sbTouchKF2); - } - sbTouchTimeline.playFromStart(); - } - - protected void scrollBarOn() { - tempVisibility = true; - requestLayout(); - } -} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualScrollBar.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualScrollBar.java deleted file mode 100644 index 2319ba557..000000000 --- a/viewer/viewer-fx/src/main/java/com/icepdf/fx/scene/control/skin/VirtualScrollBar.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.icepdf.fx.scene.control.skin; - - -import com.icepdf.fx.scene.control.PageView; -import com.sun.javafx.util.Utils; -import javafx.scene.control.ScrollBar; - -/** - * This custom ScrollBar is used to map the increment & decrement features - * to pixel based scrolling rather than thumb/track based scrolling, if the - * "virtual" attribute is true. - */ -public class VirtualScrollBar extends ScrollBar { - private final VirtualPageFlow flow; - - private boolean virtual; - - private boolean adjusting; - - public VirtualScrollBar(final VirtualPageFlow flow) { - this.flow = flow; - - super.valueProperty().addListener(valueModel -> { - if (isVirtual()/* && oldValue != newValue*/) { - if (adjusting) { - // no-op - } else { - flow.setPosition(getValue()); - } - } - }); - } - - public void setVirtual(boolean virtual) { - this.virtual = virtual; - } - - public boolean isVirtual() { - return this.virtual; - } - - @Override - public void decrement() { - if (isVirtual()) { - flow.adjustPixels(-10); - } else { - super.decrement(); - } - } - - @Override - public void increment() { - if (isVirtual()) { - flow.adjustPixels(10); - } else { - super.increment(); - } - } - -// private double lastAdjustValue = 0.0; - - // this method is called when the user clicks in the scrollbar track, so - // we special-case it to allow for page-up and page-down clicking to work - // as expected. - @Override - public void adjustValue(double pos) { - if (isVirtual()) { -// if (pos == lastAdjustValue) { -// return; -// } - - adjusting = true; - double oldValue = flow.getPosition(); - - double newValue = ((getMax() - getMin()) * Utils.clamp(0, pos, 1)) + getMin(); - if (newValue < oldValue) { - PageView cell = flow.getFirstVisibleCell(); - if (cell == null) return; - flow.showAsLast(cell); - } else if (newValue > oldValue) { - PageView cell = flow.getLastVisibleCell(); - if (cell == null) return; - flow.showAsFirst(cell); - } -// lastAdjustValue = pos; - - adjusting = false; - } else { - super.adjustValue(pos); - } - } -} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/util/ImageLoader.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/util/ImageLoader.java deleted file mode 100644 index 71918107b..000000000 --- a/viewer/viewer-fx/src/main/java/com/icepdf/fx/util/ImageLoader.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.icepdf.fx.util; - -import javafx.scene.image.Image; - -/** - * Aid for loading image from image resources path. - */ -public class ImageLoader { - - public static Image loadImage(String filename) { - return new Image(filename.getClass().getResourceAsStream( - "/com/icepdf/fx/images/" + filename)); - } -} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/util/SettingsLoaderTask.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/util/SettingsLoaderTask.java deleted file mode 100644 index a397acf2f..000000000 --- a/viewer/viewer-fx/src/main/java/com/icepdf/fx/util/SettingsLoaderTask.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.icepdf.fx.util; - -import com.icepdf.core.util.FontPropertiesManager; -import com.icepdf.core.util.PropertiesManager; -import javafx.concurrent.Task; - -import java.util.ResourceBundle; - -/** - * - */ -public class SettingsLoaderTask extends Task { - - @Override - protected Object call() throws Exception { - ResourceBundle messageBundle = ResourceBundle.getBundle( - PropertiesManager.DEFAULT_MESSAGE_BUNDLE); - FontPropertiesManager fontPropertiesManager = FontPropertiesManager.getInstance(); - - if (fontPropertiesManager.isPropertiesEmpty()) { - updateMessage(messageBundle.getString( - "icepdf.fx.common.utility.SettingsLoaderTask.loadingFonts.label")); - updateProgress(-1, 10); - fontPropertiesManager.readDefaultProperties(); - fontPropertiesManager.updateProperties(); - updateProgress(10, 10); - updateMessage(messageBundle.getString( - "icepdf.fx.common.utility.SettingsLoaderTask.loadingFontsComplete.label")); - } else { - updateMessage(messageBundle.getString( - "icepdf.fx.common.utility.SettingsLoaderTask.loadingSettings.label")); - fontPropertiesManager.loadProperties(); - for (int i = 0, max = 10; i < 10; i++) { - updateProgress(i + 1, max); - Thread.sleep(10); - } -// fontPropertiesManager.clearProperties(); - updateMessage(messageBundle.getString( - "icepdf.fx.common.utility.SettingsLoaderTask.loadingSettingsComplete.label")); - } - return null; - } -} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/DocumentViewTest.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/DocumentViewTest.java deleted file mode 100644 index 3b44f1b51..000000000 --- a/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/DocumentViewTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.icepdf.fx.viewer; - -import com.icepdf.fx.scene.control.DocumentView; -import javafx.application.Application; -import javafx.scene.Scene; -import javafx.stage.Stage; -import org.icepdf.core.pobjects.Document; - -/** - * - */ -public class DocumentViewTest extends Application { - - public static void main(String[] args) throws Exception { - launch(args); - } - - @Override - public void start(Stage primaryStage) throws Exception { - - Document document = new Document(); - - document.setFile("d:\\pdf-qa\\pdf_reference_addendum_redaction.pdf"); - - Scene myScene = new Scene(new DocumentView(document)); - primaryStage.setScene(myScene); - - primaryStage.setWidth(600); - primaryStage.setHeight(400); - primaryStage.show(); - } -} \ No newline at end of file diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/Launcher.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/Launcher.java deleted file mode 100644 index db1b82cdd..000000000 --- a/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/Launcher.java +++ /dev/null @@ -1,208 +0,0 @@ -package com.icepdf.fx.viewer; - -import com.icepdf.core.util.PropertiesManager; -import com.icepdf.fx.scene.control.DocumentView; -import com.icepdf.fx.util.ImageLoader; -import javafx.collections.ObservableList; -import javafx.geometry.Rectangle2D; -import javafx.scene.Scene; -import javafx.scene.control.*; -import javafx.scene.layout.BorderPane; -import javafx.stage.Screen; -import javafx.stage.Stage; -import javafx.stage.StageStyle; -import org.icepdf.core.exceptions.PDFException; -import org.icepdf.core.exceptions.PDFSecurityException; -import org.icepdf.core.pobjects.Document; - -import java.io.IOException; -import java.net.URL; -import java.nio.file.Path; -import java.util.ResourceBundle; -import java.util.prefs.Preferences; - -/** - * Uses the build class to assemble full viewer RI. - */ -public class Launcher { - - private static Preferences prefs = Preferences.userNodeForPackage(Launcher.class); - private static String VIEWER_WINDOW_WIDTH = "last_used_width"; - private static String VIEWER_WINDOW_HEIGHT = "last_used_width"; - private static String VIEWER_WINDOW_X = "last_used_X"; - private static String VIEWER_WINDOW_Y = "last_used_Y"; - - private static Launcher launcher; - -// private PropertiesManager properties; - -// private ArrayList controllers; - -// private long newWindowInvocationCounter = 0; - -// private ResourceBundle messageBundle = null; - - private Launcher() { - } - - public static Launcher getInstance() { - if (launcher == null) { - launcher = new Launcher(); - } - return launcher; - } - - public Stage newViewerWindow() { - ResourceBundle messageBundle = ResourceBundle.getBundle( - PropertiesManager.DEFAULT_MESSAGE_BUNDLE); - - Stage mainStage = new Stage(StageStyle.DECORATED); - mainStage.setTitle(messageBundle.getString("icepdf.fx.viewer.ViewerPane.default.title")); - mainStage.getIcons().addAll( - ImageLoader.loadImage("app_icon_128x128.png"), - ImageLoader.loadImage("app_icon_64x64.png"), - ImageLoader.loadImage("app_icon_48x48.png"), - ImageLoader.loadImage("app_icon_32x32.png"), - ImageLoader.loadImage("app_icon_16x16.png")); - - - BorderPane borderPane = new BorderPane(); - - // zoom controls. - Slider zoomSlider = new Slider(0.05, 8, 1.0); - zoomSlider.setSnapToTicks(true); - zoomSlider.setMajorTickUnit(0.05); - zoomSlider.setBlockIncrement(0.05); - zoomSlider.setMinorTickCount(0); - zoomSlider.setShowTickLabels(false); - zoomSlider.setShowTickMarks(false); - Button zoomInButton = new Button("-"); - zoomInButton.setOnAction(event -> zoomSlider.decrement()); - Button zoomOutButton = new Button("+"); - zoomOutButton.setOnAction(event -> zoomSlider.increment()); - - // rotation controls - Slider rotationSlider = new Slider(-180, 180, 0); - rotationSlider.setSnapToTicks(true); - rotationSlider.setMajorTickUnit(15); - rotationSlider.setBlockIncrement(15); - rotationSlider.setMinorTickCount(0); - rotationSlider.setShowTickLabels(false); - rotationSlider.setShowTickMarks(false); - Button rotateLeftButton = new Button("-"); - rotateLeftButton.setOnAction(event -> rotationSlider.decrement()); - Button rotateRightButton = new Button("+"); - rotateRightButton.setOnAction(event -> rotationSlider.increment()); - - // view mode controls - - Document document = new Document(); - - try { -// document.setFile("E:\\pdf-qa\\metrics\\content-parser\\ottawa_cycling_map.pdf"); -// document.setFile("E:\\pdf-qa\\metrics\\content-parser\\map.pdf"); -// document.setFile("E:\\pdf-qa\\metrics\\content-parser\\SF_923200345630.pdf"); -// document.setFile("E:\\pdf-qa\\PDF32000_2008.pdf"); -// document.setFile("d:\\pdf-qa\\fonts\\cid\\R&D-05-Carbon.pdf"); - document.setFile("d:\\pdf-qa\\metrics\\full-monty\\fcom.pdf"); -// document.setFile("E:\\pdf-qa\\metrics\\full-monty\\ACEASPA_LIBGIO_2013_00001_53923_000001_0703467_V3.pdf"); - } catch (PDFException e) { - e.printStackTrace(); - } catch (PDFSecurityException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - - // main view model - DocumentView documentView = new DocumentView(document); - - // setup scale binding for touch and wheel mouse interaction - documentView.scaleProperty().bindBidirectional(zoomSlider.valueProperty()); - documentView.scaleIncrementValueProperty().bind(zoomSlider.blockIncrementProperty()); - documentView.scaleMaxValueProperty().bind(zoomSlider.minProperty()); - documentView.scaleMaxValueProperty().bind(zoomSlider.maxProperty()); - - // rotation slider binding. - documentView.rotationProperty().bind(rotationSlider.valueProperty()); - borderPane.setCenter(documentView); - - // setup view mode controls. - - // but the toolbar together - ToolBar toolbar = new ToolBar(); - toolbar.getItems().addAll( - new Label("Scale"), zoomInButton, zoomSlider, zoomOutButton, - new Separator(), - new Label("Rotation"), rotateLeftButton, rotationSlider, rotateRightButton); - borderPane.setTop(toolbar); - - mainStage.setScene(new Scene(borderPane)); - calculateStageLocation(mainStage); - mainStage.setOnCloseRequest(event -> { - prefs.putDouble(VIEWER_WINDOW_X, mainStage.getX()); - prefs.putDouble(VIEWER_WINDOW_Y, mainStage.getY()); - prefs.putDouble(VIEWER_WINDOW_WIDTH, mainStage.getWidth()); - prefs.putDouble(VIEWER_WINDOW_HEIGHT, mainStage.getHeight()); - }); - mainStage.show(); - mainStage.toFront(); - - return mainStage; - } - - public Stage newViewerWindow(Path path) { - return newViewerWindow(); - } - - public Stage newViewerWindow(URL url) { - return newViewerWindow(); - } - - public void disposeViewerWindow(Object controller, Scene scene) { - - } - - public void minimiseAllViewerWindows() { - - } - -// public void bringAllViewerWindowsToFront(Object frontMost){ -// -// } -// -// public void bringWindowToFront(int index); - -// public List getWindowDocumentOriginList(Controller giveIndex); - - public void quit(Object controller, Scene viewer) { - - } - - private void calculateStageLocation(Stage stage) { - final Rectangle2D bounds = Screen.getPrimary().getBounds(); - - double width = prefs.getDouble(VIEWER_WINDOW_WIDTH, 800); - double height = prefs.getDouble(VIEWER_WINDOW_HEIGHT, 600); - - // default center for width on primary screen. - double x = bounds.getMinX() + bounds.getWidth() / 2 - width / 2; - double y = bounds.getMinY() + bounds.getHeight() / 2 - height / 2; - - double previousX = prefs.getDouble(VIEWER_WINDOW_X, x); - double previousY = prefs.getDouble(VIEWER_WINDOW_Y, y); - - // quick check to make sure the viewer will be visable in at least one screen, if not we default to primary - ObservableList screens = Screen.getScreensForRectangle(previousX, previousY, width, height); - if (screens.size() == 0) { - previousX = x; - previousY = y; - } - - stage.setWidth(width); - stage.setHeight(height); - stage.setX(previousX); - stage.setY(previousY); - } - -} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/ListViewTest.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/ListViewTest.java deleted file mode 100644 index 487b6c072..000000000 --- a/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/ListViewTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.icepdf.fx.viewer; - -import javafx.application.Application; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.scene.Scene; -import javafx.scene.control.ListView; -import javafx.stage.Stage; - -/** - * Created by pcorl_000 on 2017-03-30. - */ -public class ListViewTest extends Application { - - public static void main(String[] args) throws Exception { - launch(args); - } - - @Override - public void start(Stage primaryStage) throws Exception { - ListView listView = new ListView<>(); - ObservableList list = FXCollections.observableArrayList(); - for (int i = 0; i < 3; i++) { - list.add("string cell " + (i + 1)); - } - - listView.setItems(list); - - Scene myScene = new Scene(listView); - primaryStage.setScene(myScene); - - primaryStage.setWidth(600); - primaryStage.setHeight(400); - primaryStage.show(); - } -} diff --git a/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/Main.java b/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/Main.java deleted file mode 100644 index 458645311..000000000 --- a/viewer/viewer-fx/src/main/java/com/icepdf/fx/viewer/Main.java +++ /dev/null @@ -1,256 +0,0 @@ -package com.icepdf.fx.viewer; - -import com.icepdf.core.util.PropertiesManager; -import com.icepdf.fx.util.ImageLoader; -import com.icepdf.fx.util.SettingsLoaderTask; -import javafx.animation.FadeTransition; -import javafx.application.Application; -import javafx.concurrent.Task; -import javafx.concurrent.Worker; -import javafx.geometry.Rectangle2D; -import javafx.scene.Scene; -import javafx.scene.control.Alert; -import javafx.scene.control.Label; -import javafx.scene.control.ProgressBar; -import javafx.scene.effect.DropShadow; -import javafx.scene.image.ImageView; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.Pane; -import javafx.scene.layout.StackPane; -import javafx.scene.layout.VBox; -import javafx.scene.paint.Color; -import javafx.stage.Screen; -import javafx.stage.Stage; -import javafx.stage.StageStyle; -import javafx.util.Duration; - -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.InvalidPathException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.text.MessageFormat; -import java.util.List; -import java.util.Map; -import java.util.ResourceBundle; -import java.util.logging.Logger; - -/** - * The main class is responsible for displaying the splash screen and doing any settings loading on a separate thread. - * When the viewer ri is launched for the first time the font manager will read the systems fonts which can take - * quite a bit of time at which a indeterminate progress bar is used to show that the scan is taking place. - * - * @since 6.5 - */ -public class Main extends Application { - - private static final Logger logger = Logger.getLogger(Main.class.toString()); - - private static final int SPLASH_WIDTH = 640; - private static final int SPLASH_HEIGHT = 400; - private static final String ARGS_FILE_PARAMTER = "-loadfile"; - private static final String ARGS_URL_PARAMTER = "-loadurl"; - private static final String JNLP_FILE_PARAMTER = "loadfile"; - private static final String JNLP_URL_PARAMTER = "Loadurl"; - - private static ResourceBundle messageBundle; - - private Pane splashPane; - private ProgressBar loadProgress; - private Label progressText; - private Stage initStage; - - public static void main(String[] args) throws Exception { - launch(args); - } - - @Override - public void init() { - // load message bundle - messageBundle = ResourceBundle.getBundle( - PropertiesManager.DEFAULT_MESSAGE_BUNDLE); - - splashPane = new StackPane(); - splashPane.getStylesheets().add( - getClass().getResource(PropertiesManager.DEFAULT_SPLASH_CSS).toExternalForm()); - ImageView splashImageView = new ImageView(); - splashImageView.setId("splash-image"); - loadProgress = new ProgressBar(); - loadProgress.setId("progress-bar"); - progressText = new Label("Will find friends for peanuts . . ."); - progressText.getStyleClass().add("progress-label"); - - BorderPane splashContentPane = new BorderPane(); - splashPane.getChildren().addAll(splashImageView, splashContentPane); - - // padding is used to move the progress bar to desired location - Pane progressPane = new VBox(); - progressPane.getStyleClass().add("progress-pane"); - progressPane.getChildren().addAll(progressText, loadProgress); - splashContentPane.setCenter(progressPane); - - // licence values. - Pane licencePane = new VBox(); - licencePane.getStyleClass().add("license-pane"); - Label copyrightLabel = new Label(messageBundle.getString("icepdf.fx.viewer.Main.copyRight.label")); - copyrightLabel.getStyleClass().add("copyright-label"); - licencePane.getChildren().addAll(copyrightLabel); - splashContentPane.setBottom(licencePane); - splashPane.getStyleClass().add("splash-layout"); - splashPane.setEffect(new DropShadow()); - } - - @Override - public void start(final Stage initStage) throws Exception { - this.initStage = initStage; - - initStage.getIcons().addAll( - ImageLoader.loadImage("app_icon_128x128.png"), - ImageLoader.loadImage("app_icon_64x64.png"), - ImageLoader.loadImage("app_icon_48x48.png"), - ImageLoader.loadImage("app_icon_32x32.png"), - ImageLoader.loadImage("app_icon_16x16.png")); - SettingsLoaderTask fontReaderTask = new SettingsLoaderTask(); - showSplash(initStage, fontReaderTask, - this::showMainStage); - new Thread(fontReaderTask).start(); - } - - private void showMainStage() { - - Launcher launcher = Launcher.getInstance(); - // check for -loadfile or -loadurl. - Map jnlpArgs = getParameters().getNamed(); - List args = getParameters().getUnnamed(); - if (jnlpArgs.size() > 0) { - processJNLPArguments(launcher, jnlpArgs); - } else if (args.size() > 0) { - processArguments(launcher, args); - } else { - launcher.newViewerWindow(); - } - } - - private void showSplash( - final Stage initStage, - Task task, - InitCompletionHandler initCompletionHandler) { - progressText.textProperty().bind(task.messageProperty()); - loadProgress.progressProperty().bind(task.progressProperty()); - task.stateProperty().addListener((observableValue, oldState, newState) -> { - if (newState == Worker.State.SUCCEEDED) { - loadProgress.progressProperty().unbind(); - loadProgress.setProgress(1); - initStage.toFront(); - FadeTransition fadeSplash = new FadeTransition(Duration.seconds(1.2), splashPane); - fadeSplash.setFromValue(1.0); - fadeSplash.setToValue(0.0); - fadeSplash.setOnFinished(actionEvent -> initStage.hide()); - fadeSplash.play(); - // setup callback to start the main application state - initCompletionHandler.complete(); - } - }); - // center the splash screen on the primary monitory - Scene splashScene = new Scene(splashPane, Color.TRANSPARENT); - initStage.initStyle(StageStyle.TRANSPARENT); -// initStage.setAlwaysOnTop(true); - initStage.setScene(splashScene); - final Rectangle2D bounds = Screen.getPrimary().getBounds(); - initStage.setX(bounds.getMinX() + bounds.getWidth() / 2 - SPLASH_WIDTH / 2); - initStage.setY(bounds.getMinY() + bounds.getHeight() / 2 - SPLASH_HEIGHT / 2); - initStage.initStyle(StageStyle.TRANSPARENT); - initStage.show(); - } - - public interface InitCompletionHandler { - void complete(); - } - - private void processJNLPArguments(Launcher launcher, Map jnlpArgs) { - String file = null; - String url = null; - if (jnlpArgs.containsKey(JNLP_FILE_PARAMTER)) { - file = jnlpArgs.get(JNLP_FILE_PARAMTER).trim(); - } else if (jnlpArgs.containsKey(JNLP_URL_PARAMTER)) { - url = jnlpArgs.get(JNLP_URL_PARAMTER).trim(); - } - // load default empty viewer. - if (file == null && url == null) { - launcher.newViewerWindow(); - } - if (file != null) { - checkAndLaunchFile(launcher, file); - } - if (url != null) { - checkAndLaunchUrl(launcher, url); - } - } - - private void processArguments(Launcher launcher, List args) { - if (args.size() == 2) { - String command = args.get(0); - String uri = args.get(1); - if (ARGS_FILE_PARAMTER.equals(command)) { - checkAndLaunchFile(launcher, uri); - } else if (ARGS_URL_PARAMTER.equals(command)) { - checkAndLaunchUrl(launcher, uri); - } - } else { - launcher.newViewerWindow(); - } - } - - private void checkAndLaunchFile(Launcher launcher, String file) { - try { - Path filePath = Paths.get(file); - boolean exists = Files.exists(filePath); - if (exists) { - launcher.newViewerWindow(filePath); - } else { - throw new InvalidPathException(file, "Not found. "); - } - } catch (InvalidPathException e) { - logger.warning("Could not load file path: " + file); - this.initStage = launcher.newViewerWindow(); - Alert alert = new Alert(Alert.AlertType.WARNING); - alert.setTitle(messageBundle.getString("icepdf.fx.viewer.Main.dialog.loadError.title")); - alert.setHeaderText(messageBundle.getString("icepdf.fx.viewer.Main.dialog.loadUriError.header")); - MessageFormat formatter = new MessageFormat( - messageBundle.getString("icepdf.fx.viewer.Main.dialog.loadUriError.content")); - messageBundle.getString("icepdf.fx.viewer.Main.dialog.loadUriError.content"); - alert.setContentText(formatter.format(new Object[]{file})); - alert.initOwner(initStage); - alert.showAndWait(); - } - } - - private void checkAndLaunchUrl(Launcher launcher, String url) { - try { - URL fileUrl = new URL(url); - HttpURLConnection.setFollowRedirects(false); - HttpURLConnection con = (HttpURLConnection) fileUrl.openConnection(); - con.setInstanceFollowRedirects(false); - con.setRequestMethod("HEAD"); - if (con.getResponseCode() == HttpURLConnection.HTTP_OK) { - launcher.newViewerWindow(fileUrl); - } else { - throw new IllegalStateException(); - } - con.disconnect(); - } catch (Exception e) { - logger.warning("Could not load url: " + url); - this.initStage = launcher.newViewerWindow(); - Alert alert = new Alert(Alert.AlertType.WARNING); - alert.setTitle(messageBundle.getString("icepdf.fx.viewer.Main.dialog.loadError.title")); - alert.setHeaderText(messageBundle.getString("icepdf.fx.viewer.Main.dialog.loadUriError.header")); - MessageFormat formatter = new MessageFormat( - messageBundle.getString("icepdf.fx.viewer.Main.dialog.loadUriError.content")); - messageBundle.getString("icepdf.fx.viewer.Main.dialog.loadUriError.content"); - alert.setContentText(formatter.format(new Object[]{url})); - alert.initOwner(initStage); - alert.showAndWait(); - } - } -} \ No newline at end of file diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/FxController.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/FxController.java new file mode 100644 index 000000000..37c8519ca --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/FxController.java @@ -0,0 +1,28 @@ +package org.icepdf.fx.ri.viewer; + +import javafx.scene.layout.Region; +import org.icepdf.fx.ri.viewer.listeners.DocumentChangeListener; + +public class FxController { + + private final ViewerModel model; + private final Interactor interactor; + private final ViewBuilder viewBuilder; + + public FxController() { + this.model = new ViewerModel(); + this.interactor = new Interactor(model); // is this really a mediator? + this.viewBuilder = new ViewBuilder(model); + + // clean up this viewer if the document changes + this.model.document.addListener(new DocumentChangeListener(model)); + } + + public ViewerModel getModel() { + return model; + } + + public Region getView() { + return viewBuilder.build(); + } +} diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Interactor.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Interactor.java new file mode 100644 index 000000000..155232d1d --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Interactor.java @@ -0,0 +1,34 @@ +package org.icepdf.fx.ri.viewer; + +public class Interactor { + + private final ViewerModel model; + private int changeCount = 0; +// private DomainObject domainObject; +// private Service service = new Service(); + + public Interactor(ViewerModel model) { + this.model = model; + createModelBindings(); + } + + private void createModelBindings() { +// model.bindProperty3(Bindings.createBooleanBinding(() -> !model.getProperty1().isEmpty(), model +// .property1Property())); + } + + public void updateModelAfterSave() { +// model.setProperty1(""); +// model.setProperty2(domainObject.getSomeValue()); +// changeCount = 0; + + } + + public void saveData() { +// domainObject = service.saveDataSomewhere(model.getProperty1() + " --> " + changeCount); + } + + public void updateChangeCount() { + changeCount++; + } +} diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Launcher.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Launcher.java new file mode 100644 index 000000000..b46ecddf0 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Launcher.java @@ -0,0 +1,37 @@ +package org.icepdf.fx.ri.viewer; + +import javafx.application.Application; +import javafx.stage.Stage; + +import java.util.logging.Logger; + +public class Launcher extends Application { + + private static final Logger logger = Logger.getLogger(Launcher.class.toString()); + + + public static void main(String[] args) throws Exception { + Application.launch(args); + } + + + @Override + public void start(final Stage primaryStage) throws Exception { + + + // read stored system font properties. +// FontPropertiesManager.getInstance().loadOrReadSystemFonts(); + + // setup the viewer ri properties manager + + ViewerStageManager stageManager = ViewerStageManager.getInstance(); + stageManager.createViewer(primaryStage, null); + stageManager.setTitleAndIcons(primaryStage); + + primaryStage.show(); + + + } + + +} \ No newline at end of file diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewBuilder.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewBuilder.java new file mode 100644 index 000000000..be2d2e376 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewBuilder.java @@ -0,0 +1,56 @@ +package org.icepdf.fx.ri.viewer; + +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.ToolBar; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Region; +import javafx.stage.Window; +import javafx.util.Builder; +import org.icepdf.fx.ri.viewer.commands.OpenFileCommand; +import org.icepdf.fx.ri.viewer.commands.ZoomInCommand; +import org.icepdf.fx.ri.views.DocumentViewPane; + +import java.util.function.Consumer; + +public class ViewBuilder implements Builder { + + private final ViewerModel model; + + private Consumer openDocumentActionHandler; + + DocumentViewPane documentViewPane = new DocumentViewPane(null); + + public ViewBuilder(ViewerModel model) { + this.model = model; + } + + @Override + public Region build() { + BorderPane borderPane = new BorderPane(); + borderPane.setTop(createToolbar()); + borderPane.setCenter(documentViewPane); + return borderPane; + } + + private ToolBar createToolbar() { + Button openDocument = new Button("Open Document"); + openDocument.setOnAction(event -> { + Node source = (Node) event.getSource(); + Window stage = source.getScene().getWindow(); + new OpenFileCommand(stage, model).execute(); + }); + + Button zoomOut = new Button("Zoom Out"); + zoomOut.setOnAction(event -> new ZoomInCommand(documentViewPane, model).execute()); + zoomOut.disableProperty().bind(model.toolbarDisabled); + + Button zoomIn = new Button("Zoom In"); + zoomIn.setOnAction(event -> new ZoomInCommand(documentViewPane, model).execute()); + zoomIn.disableProperty().bind(model.toolbarDisabled); + + ToolBar toolbar = new ToolBar(); + toolbar.getItems().addAll(openDocument, zoomOut, zoomIn); + return toolbar; + } +} diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerModel.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerModel.java new file mode 100644 index 000000000..bdf9c9e7e --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerModel.java @@ -0,0 +1,27 @@ +package org.icepdf.fx.ri.viewer; + +import javafx.beans.property.*; +import org.icepdf.core.pobjects.Document; + +public class ViewerModel { + + public final BooleanProperty useSingleViewerStage = new SimpleBooleanProperty(false); + + // document + public final ObjectProperty document; + + // todo, pretty sure we don't need this property: file/url path + public final StringProperty filePath; + + // toolbar disabled state + public BooleanProperty toolbarDisabled; + + // zoom factor increment + public final FloatProperty zoomFactorIncrement = new SimpleFloatProperty(0.1f); + + public ViewerModel() { + document = new SimpleObjectProperty<>(null); + filePath = new SimpleStringProperty(null); + toolbarDisabled = new SimpleBooleanProperty(true); + } +} diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerStageManager.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerStageManager.java new file mode 100644 index 000000000..cb946b550 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerStageManager.java @@ -0,0 +1,48 @@ +package org.icepdf.fx.ri.viewer; + +import javafx.scene.Scene; +import javafx.scene.image.Image; +import javafx.stage.Stage; +import org.icepdf.core.pobjects.Document; +import org.icepdf.fx.ri.viewer.listeners.StageCloseRequestListener; + +public class ViewerStageManager { + + private static ViewerStageManager singleton; + + private ViewerStageManager() { + } + + // todo keep track of open stages + + public void createViewer(Stage stage, Document document) { + FxController controller = new FxController(); + controller.getModel().document.set(document); + Scene scene = new Scene(controller.getView(), 400, 200); + stage.setScene(scene); + stage.setOnCloseRequest(new StageCloseRequestListener(controller.getModel())); + } + + public Stage createViewerStage(Document document) { + Stage stage = new Stage(); + setTitleAndIcons(stage); + createViewer(stage, document); + return stage; + } + + public void setTitleAndIcons(Stage stage) { + stage.setTitle("Icepdf Viewer"); + stage.getIcons().addAll( + new Image(ViewerStageManager.class.getResourceAsStream( + "/org/icepdf/fx/images/icepdf-app-icon-32x32.png")), + new Image(ViewerStageManager.class.getResourceAsStream( + "/org/icepdf/fx/images/icepdf-app-icon-64x64.png"))); + } + + public static ViewerStageManager getInstance() { + if (singleton == null) { + singleton = new ViewerStageManager(); + } + return singleton; + } +} diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/WindowManager.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/WindowManager.java new file mode 100644 index 000000000..f77ac11a0 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/WindowManager.java @@ -0,0 +1,4 @@ +package org.icepdf.fx.ri.viewer; + +public class WindowManager { +} diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/Command.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/Command.java new file mode 100644 index 000000000..df14d2f05 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/Command.java @@ -0,0 +1,5 @@ +package org.icepdf.fx.ri.viewer.commands; + +public interface Command { + void execute(); +} diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/OpenFileCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/OpenFileCommand.java new file mode 100644 index 000000000..52a3c377d --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/OpenFileCommand.java @@ -0,0 +1,54 @@ +package org.icepdf.fx.ri.viewer.commands; + +import javafx.stage.FileChooser; +import javafx.stage.Stage; +import javafx.stage.Window; +import org.icepdf.core.exceptions.PDFSecurityException; +import org.icepdf.core.pobjects.Document; +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.ViewerStageManager; + +import java.io.File; +import java.io.IOException; + +public class OpenFileCommand implements Command { + + private final Window stage; + private final ViewerModel model; + + public OpenFileCommand(Window parentStage, ViewerModel model) { + this.model = model; + this.stage = parentStage; + } + + @Override + public void execute() { + // show file chooser + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Open Resource File"); + File file = fileChooser.showOpenDialog(stage); + if (file != null) { + // set document + try { + Document document = new Document(); + + if (model.useSingleViewerStage.get()) { + model.document.set(document); + model.filePath.set(file.getAbsolutePath()); + } else { + ViewerStageManager stageManager = ViewerStageManager.getInstance(); + Stage stage = stageManager.createViewerStage(document); + stageManager.setTitleAndIcons(stage); + stage.show(); + } + + // todo push of a thread to load the document, use Consumer to make sure UI is updated properly + document.setFile(file.getAbsolutePath()); + + } catch (PDFSecurityException | IOException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + } +} diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/ZoomInCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/ZoomInCommand.java new file mode 100644 index 000000000..f113df4e4 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/ZoomInCommand.java @@ -0,0 +1,23 @@ +package org.icepdf.fx.ri.viewer.commands; + +import javafx.beans.property.FloatProperty; +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.views.DocumentViewPane; + +public class ZoomInCommand implements Command { + private final ViewerModel model; + private final DocumentViewPane documentViewPane; + + + public ZoomInCommand(DocumentViewPane documentViewPane, ViewerModel model) { + this.model = model; + this.documentViewPane = documentViewPane; + } + + @Override + public void execute() { + // get the current zoom level + FloatProperty scale = documentViewPane.scaleProperty(); + scale.set(scale.get() - model.zoomFactorIncrement.get()); + } +} diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/ZoomOutCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/ZoomOutCommand.java new file mode 100644 index 000000000..ee67db322 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/ZoomOutCommand.java @@ -0,0 +1,24 @@ +package org.icepdf.fx.ri.viewer.commands; + +import javafx.beans.property.FloatProperty; +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.views.DocumentViewPane; + +public class ZoomOutCommand implements Command { + private final ViewerModel model; + private final DocumentViewPane documentViewPane; + + + public ZoomOutCommand(DocumentViewPane documentViewPane, ViewerModel model) { + this.model = model; + this.documentViewPane = documentViewPane; + } + + @Override + public void execute() { + // get the current zoom level + FloatProperty scale = documentViewPane.scaleProperty(); + scale.set(scale.get() + model.zoomFactorIncrement.get()); + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/listeners/DocumentChangeListener.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/listeners/DocumentChangeListener.java new file mode 100644 index 000000000..b6f689e51 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/listeners/DocumentChangeListener.java @@ -0,0 +1,27 @@ +package org.icepdf.fx.ri.viewer.listeners; + +import javafx.beans.value.ChangeListener; +import org.icepdf.core.pobjects.Document; +import org.icepdf.fx.ri.viewer.ViewerModel; + +public class DocumentChangeListener implements ChangeListener { + + private ViewerModel model; + + public DocumentChangeListener(ViewerModel model) { + this.model = model; + } + + @Override + public void changed(javafx.beans.value.ObservableValue observable, Document oldDocument, + Document newDocument) { + if (oldDocument != null) { + oldDocument.dispose(); + } + if (newDocument != null) { + model.toolbarDisabled.set(false); + } else { + model.toolbarDisabled.set(true); + } + } +} diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/listeners/StageCloseRequestListener.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/listeners/StageCloseRequestListener.java new file mode 100644 index 000000000..731423bfe --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/listeners/StageCloseRequestListener.java @@ -0,0 +1,26 @@ +package org.icepdf.fx.ri.viewer.listeners; + +import javafx.event.EventHandler; +import javafx.stage.WindowEvent; +import org.icepdf.core.pobjects.Document; +import org.icepdf.fx.ri.viewer.ViewerModel; + + +public class StageCloseRequestListener implements EventHandler { + + private final ViewerModel model; + + public StageCloseRequestListener(ViewerModel model) { + this.model = model; + } + + @Override + public void handle(WindowEvent event) { + if (event.getEventType() == WindowEvent.WINDOW_CLOSE_REQUEST) { + Document document = model.document.get(); + if (document != null) { + document.dispose(); + } + } + } +} diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java new file mode 100644 index 000000000..aafc60243 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java @@ -0,0 +1,48 @@ +package org.icepdf.fx.ri.views; + +import javafx.beans.property.FloatProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleFloatProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.*; +import org.icepdf.core.pobjects.Document; + +public class DocumentViewPane extends Region { + + private FloatProperty scale = new SimpleFloatProperty(1.0f); + private FloatProperty rotation = new SimpleFloatProperty(0.0f); + private IntegerProperty currentPageIndex = new SimpleIntegerProperty(0); + + + public DocumentViewPane(Document document) { + createLayout(document); + } + + private void createLayout(Document document) { + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setFitToWidth(true); + scrollPane.prefWidthProperty().bind(this.widthProperty()); + scrollPane.prefHeightProperty().bind(this.heightProperty()); + + TilePane tilePane = new TilePane(); + tilePane.setPrefColumns(1); + tilePane.setVgap(25); + tilePane.setHgap(25); + + // create a page view for each page + for (int i = 0; i < 250; i++) { + PageViewWidget pageViewPane = new PageViewWidget(i, scale, rotation); + pageViewPane.setBorder(new Border(new BorderStroke(null, BorderStrokeStyle.SOLID, null, + new BorderWidths(1)))); + tilePane.getChildren().add(pageViewPane); + } + scrollPane.setContent(tilePane); + getChildren().add(scrollPane); + } + + public FloatProperty scaleProperty() { + return scale; + } + +} diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java new file mode 100644 index 000000000..7073fe187 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java @@ -0,0 +1,71 @@ +package org.icepdf.fx.ri.views; + +import javafx.beans.property.*; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; + +public class PageViewWidget extends Region { + + private FloatProperty scale; + private FloatProperty rotation; + private IntegerProperty pageIndex; + + private DoubleProperty width; + private DoubleProperty height; + + private Label pageIndexLabel; + private Label scaleLabel; + private Label rotationLabel; + + public PageViewWidget(int pageIndex, FloatProperty scale, FloatProperty rotation) { + this.pageIndex = new SimpleIntegerProperty(pageIndex); + this.scale = new SimpleFloatProperty(); + this.scale.bind(scale); + this.rotation = new SimpleFloatProperty(); + this.rotation.bind(rotation); + + width = new SimpleDoubleProperty(75); + height = new SimpleDoubleProperty(50); + + scale.addListener((observable, oldValue, newValue) -> { + width.set(75 * newValue.floatValue()); + height.set(50 * newValue.floatValue()); + }); + + pageIndexLabel = new Label("Page " + pageIndex); + scaleLabel = new Label(); + rotationLabel = new Label(); + createLayout(); + } + + private void createLayout() { + VBox vBox = new VBox(); + scaleLabel.textProperty().bind(scale.asString()); + rotationLabel.textProperty().bind(width.asString()); + vBox.getChildren().addAll(pageIndexLabel, scaleLabel, rotationLabel); + minWidthProperty().bind(width); + minHeightProperty().bind(height); + getChildren().add(vBox); + } + + public int getPageIndex() { + return pageIndex.get(); + } + + public float getScale() { + return scale.get(); + } + + public FloatProperty scaleProperty() { + return scale; + } + + public float getRotation() { + return rotation.get(); + } + + public FloatProperty rotationProperty() { + return rotation; + } +} diff --git a/viewer/viewer-fx/src/main/resources/org/icepdf/fx/images/icepdf-app-icon-32x32.png b/viewer/viewer-fx/src/main/resources/org/icepdf/fx/images/icepdf-app-icon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..f3c0774ba9118b3ee8713da681cd0a05131943bf GIT binary patch literal 2450 zcmV;D32pX?P)fqw6tAnc`2tGg@otzY1q{MTRLW>wL9PY<|@Bee|xq#59FwF{$0h(@`>10C8=2pe7 zSA-CzieXA-mN6$uDfq6hdj$A?7w1|2_x@bHYTjZ%KqQ`JhG`RT5KnK~2Iqa^C@aY- z@j3CRNevP|a$WKGjdRgufoDd|OnRO;N-P#TSm|I^GBx5!;+U%GlrLmGRyl8R)+#mD z>XW}PlGj(3xlT2NBo?s*2_h8KP(}qd;004R>004l5008;`004mK004C`008P>0026e z000+ooVrmw00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000Mc zNliru=n57B1QY64a~S{t2HQzQK~z}7eV1!&Tvrjte{=Wl+FpBY*Qp=t(8PA^#E@2X z32_jJgn%0)R3)Mfh(`+&LIMF2;_(5|4^<^9AypLxgb)&HDX4%7iCWrzqE!+?3k2at zB&kDM$HZ~6cAVJjeR#9G*)x2&uXERh`{C~1bLY&=`Okmm&d82!k14?=fcn4q%j(^{ zWsN<14;sm$sH(R;SXWC{6%}IUT*O+jjR%tA{g>dS_`ezoH*XFd@YRH>TmME@DUL|VEPVbZB$TP56s{I>Vaz?+>UMNk0L788w+L; z#DY0ki*bV}@Qz3bmIo~o!60J0XYoxULfP>pPG}8u+0-EB3$6`o{H=r>2%*NxjF5_i zaN^6UDvmVehz2?|rq>Oz-r#*)f%vftNa*R%h~x?_EeY=Z*mg3RR$lt!n-mJwaR1=u z4t7s&p?{zg5jc6`EzX@?iqf$8@{|YpAB8{yT3Ini)px+<)1P@CgM&T1cJd-?Yn}tc zBi%gm=*OA3XNXFrPOVlam&E5(>a`k;D{iCmxh^4k_Qg&pkWNB-^g+Afo$}xUgh$9 z5m5U2+W79n`0_^BHg#8`0Cg8aPZ(=eDYIE{P@Yg(P$|5 z9UNzB>OCAgHp35}IKj20G6MtceCsVei35~g|#Xo-~+<6U-@cv!;R||DwR3_g+i5UOJ!D9ZeolE z4)`TS;QI9v=g)iIiwKS*EG(3mpI@QBzl+;%Z=*AlV)N$PxO!!kx!F99Bj5;5LYSRh z9hBN*J zlMZdINxPgx_2V6@U5J2}TD`p;Y~SwXtg2!fhU{QFTkhy#X{o^Sa)n}{N-mdYaIl-L zLtPkSd|739q?>_kw_o%Bx6#lsZE_%;ZsD_^n_$OyH|cbO&F|{q!G|X3>+j?*Cudn* zxk;f=FjLd!O!nvXtT|D%KNqTxZ{;vb7F?etT2SkMB}13D6HMY5kXaX{k4mf%T?y)R&boix(LYj(Z@BLZRZzH z-A}#V;7eaTLOy?!_VyI0QZ9SM6FYV&#wdw|(%zoL5m;ZZQLQy_!miq*eAEM&I5GVP z*M0ymPjA(gwY6&anR3Fk)v%v)1dM@Vq2|p3f(Jd(*HNrqA^MbB^M(W=KrzN(Oas)! zjfNk%*k=w6yIxeK z3F?5=ibXAU1in~Yu&3jg#{p;5B4^;djetFQg$P(9UTmrjnQXBvG~Pr`|5(lBj9Ip) zB`ZY#1%`E*akuAA761SM8+1ijbZKK_I%98ed2(rIXmkKjbz*F3V<1FtZDDC{wvjPg z0000YbVXQnX=7tLWMy+>a%pgMX>V=-M@dakAYyfCY;-MYZXk0~M?y?zK@$J~02Opa zSafM)V>)eNZgpm1V{~EX>4Tx04R}tkvmAkP!xv$rY(w89PA+CkfAzR5MMZI6^c+H)C#RSnB4RQO&XFE z7e~Rh;NWAi>fqw6tAnc`2tGg@otzY1q{MTRLW>wL9PY<|@Bee|xq#59FwF{$0h(@` z>10C8=2pe7SA-CzieXA-mN6$uDfq6hdj$A?7w1|2_x@bHYTjZ%KqQ`JhG`RT5KnK~ z2Iqa^C@aY-@j3CRNevP|a$WKGjdRgufoDd|OnRO;N-P#TSm|I^GBx5!;+U%GlrLmG zRyl8R)+#mD>XW}PlGj(3xlT2NBo?s*2_h8KP(}qd;h%shDY{NJv z6kkN_Mz-)xSQpE>>*(yBw?B5z?swfiAdg2nyWh9_eed;opZ7b|yY?9d2m}CNFytQs zv}*{*M)x`V0~}nY>JZk(#}8v+vN(!`cZzcu$^+!TndLdr-(;8<@64fSKyXp~2T=3~ zQl^UcGIfblRV1tpELRh>Kit`-H?_gy{AdM7dn1fEjZ!!i) z-2#a0ZXoHIBx~YoZXi>%7u`WmXPTx-kJiRDu~|thU@|$dk`@IVO<7t>NHD$71;7Mi z8O@MUYn0-j%0`Gj##}kevk?HYmJppt>m;NbOG?zOfi&>LLVzMA1CuH|>xM`&m{fl^ zVLv9x|Mkp4au$e9PcT}pC9=xVp4gs*xdsiI1g`7f4w1z`nk+#!2tc?eBP>dc2QZD5 z41_F22_IwZS&%6Sxk!o7RwzqawUKRf(AX4ss_$1Yal?y*$r7)iKhv!vG&N_C$;2xtm3+)j zRiaTY87pUskQqw?5ndQsfr|OnNXybB^xik%mxI-~av3QwG}3-mzK9e-DA&dN zEM^B&Jm5rYSlUs<>eZbfB8-kSqrT2Di;xJe3q11Z@8C;c+JVJ472r6!e|$7FCY5e( zv`LD=N5u88xqM+0AhL{)?m;a1bSp{W-%WvaX?nbeAZ3X5H=>sc0k zK5^m$eC?}mVt%d?J1^@_=%kjw5s^vBER2y+vCv!uJG=CWTmWcl%Ai=x0{|)&#=yW3 z=I2-_)KuINvck2Nz$|fphGK_|w65VHLg8ejg=$Fd5@`}VtiC>jx;i(yHaA<6m&TgJ zg&-){-I67BXvhW+pPltFHtGQ&6bl*Lwxx($H+P{}Yytob4^QIQ(Tg~BauPE$zQyh3 zi`Dd+Z6x&cG~uqhyRmH9Vz?OxlasSJeda2T9vQ;$P{~?>R^v#v(ZQ1TdH_J%l12%r z*44XMv8qu>(~Q|!4O;;{qG&Y(IeMWHAeu}^)}+kC-1`6ZR^q0)F5GX->+b3XbcB_ zauh%QVSfV85A1w!CHCz3Q@E}JfZ)1;J{`xw!w>%vzVL-lNi#p_zkuC;{-2ncioukt zx_d*`MK&8qwV|ONkNjB=cJICixm-3%EzE%91ms6HTaPVUda!C$JDz*?Rs8J0B{OLm z#)z%kiun6~cn~XBc1RnqrYhIXVEOVTc{RDw(Ae03OhzZR>g(&Y zYYo|YxEYGz8CO|h+fT9Bj7J~cj$AH_+1V0KpB}{d^TQ}F_-OBF!KO_s(cWGJ5uvrU z1%LIGPh)840N#1~x-m&CUy;S%?%Rp(?xoQ?#>c1d{`==~`SK_T!15KX*tBU43WXeY z?YaX416Qo!}F+BI|K^#7O4W;?2rhsM38}K(zZO5H=Zh+%BXlpCr@h9%Wsgo~ZW~L$; zK*l9J{^hM$v!*N3`03LZ@K4XYic{}T!t*NOa|yR@Zo!_t_oKIW1^W6{%H^!^=UZ=% z;P^W~0l?_#U4-XfcoL4|f|+sX(0lmqw@yR=jfvrx8RardCK;tcHZ>iHFg-nwZ+zn* zUj3hID9y(!@XEl23-j3X)M51ZUySB#+O!(?{ozurFzV@T!rgaojNs|jt7F)=_hr2I z?gV@<&Y_-H!SQ#-v2X8-xOVNjQp?8(yZXO0S3zlx;e|j)BopT6DwvM>pIa=$OyrQuyNz^Nc$5L)A+BSor9^NE$ZN>2hLz>YF6Jo0Url7v5}h#U4Rn+ z=ETG_PMy4A5m6l7ZjAf5Fc=GRy0(@Vu;E82$3;2m zklwiEDkoU4jbYT%Ix`!BT5&r`v7}hcp)qSXKE*mm+tCqIP@d;wa4<$;rFmb9=B=%H zH0KqIj;kNs%IG&S~QH#YK)^ErIEEJ#2n(Xs=2i@IEqIsjElej(> zS6$Q7K0f*=4ra@`TG3K;EgOhcd{b*ZIy+mF6#7Z111T&f#sJsNVCVgPaO+~S!qE7^ zmaR9TySqJ_H!v`Si5tFP0gV3sOOf!3i}U#7oofv0k%QTTyL!>mlCuk{R=Zcyr^tRg zNqCBguzmXmY`eV}av3a)4mVTGVE69Z(GXJ4p6BDx>k4=R@XE`-!SwW8P#0tzeC~7G zuwrF4ez)@awLMMPwQHMR>DlNqPL~Bk?+`hObC{9OH{+>2J8;Jxg&-gbDX@KMJ@!3) zCvLtu)@FbIhxqwxm$ig*`s5Uj9z7FT!OE5G_}Vj{!P<3A+Iu(rZVvzW%me7^YSXfd zh=RDr7!aLaGK_H{p7K;=bm`IvT3hp2wW^~)nJV9W3K;Rioz$B7f?FgQ2@B4E|3Hr#ew zFWTA)@cjw~2ZykB?Xm<-b3#E7lebJ}6LBpaizt{KJa`gc{Ni@Bv^3-1dvC@)_f+D> zr6AIYi5Yz7I|uRR8zWXf95^?NumAHa*t_pf(An7vB0@*UV(i?xHPUqTxxVk?=dT?H z0IXTlrQiw!9iIlOEXQpfDexGuz`uR}48Hj-g%^hp|xh;CVjE zWe;V~Gc>pnGQmK(9F8md@T2mWlh3X8^*kFDSBvr)?Em-AqOWgx&`10AZ?XH)m#{Ej zL0h|vKe)FO8#i{MP-q4-V{CK^$B$pckzWpnX3v`MizyUB{ny;=;P*Bcv3>h$EL~be z#&s|?HHUL&hw#RsK@6Us1HdRQ&Y-1G2QX0fd|bX*QW|41LDRV`3&#m)@zK!|W~Y1! z3bWy~sB3F9sG1zUR>lj@e~1^J|IiRp%_q2}z7BC{V8OMS86QX98pV;fMkVG}72x=F z594E=)}mD{-77OLUYIkQxfVwSQMfQ2PdCyIwWJ&U_<97H*#hr5y-7yqZo6T#ToYnq z;mMAn1WMT;CxdskTLerL2yyaJcUHS@Y_0NaV1dZACW|_%)$LYic+rRyV1aD5wIKn9mN@Y$+ z%>uPVR|6(sd!Sq!1Ksu_YMF?qfMeW~IQHrLPJlQteUICt4F@ zWDo=Npeuh@(AdRIvx0=rfwBbn5#k4H!IUCB*?W^Sl6u9O1}QBEYpEk8-3c2e5jj!~ zdnC4|MwSG~uywm_`*3X-$*2X}hN*^vitJS*u_@ry==;h_3Fa2Q?dSgiZF4!v-&JpP P00000NkvXXu0mjf=lX5x literal 0 HcmV?d00001 From 2cd12f73671615d5af3b449b5ca1101263299941 Mon Sep 17 00:00:00 2001 From: Patrick Corless Date: Wed, 30 Oct 2024 21:03:17 -0600 Subject: [PATCH 3/7] GH-372 experimenting with javafx design patterns. --- viewer/viewer-fx/build.gradle | 1 + .../org/icepdf/fx/ri/viewer/Launcher.java | 10 ++- .../org/icepdf/fx/ri/viewer/ViewBuilder.java | 4 +- .../fx/ri/viewer/ViewerStageManager.java | 2 +- .../ri/viewer/commands/OpenFileCommand.java | 3 +- .../icepdf/fx/ri/views/DocumentViewPane.java | 82 +++++++++++++++---- .../icepdf/fx/ri/views/PageViewWidget.java | 27 +++++- 7 files changed, 108 insertions(+), 21 deletions(-) diff --git a/viewer/viewer-fx/build.gradle b/viewer/viewer-fx/build.gradle index a8c81f352..50bb104e4 100644 --- a/viewer/viewer-fx/build.gradle +++ b/viewer/viewer-fx/build.gradle @@ -15,6 +15,7 @@ applicationDefaultJvmArgs = ["-Xms64m", "-Xmx1024m"] def sectionName = 'org/icepdf/fx/ri/viewer' def baseJarName = 'icepdf' def baseAppendixName = 'viewer-fx' +def commandlineArgs = '-loadfile "/home/pcorless/dev/pdf-qa/PDF32000_2008.pdf"' repositories { mavenCentral() diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Launcher.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Launcher.java index b46ecddf0..5788217c6 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Launcher.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Launcher.java @@ -2,6 +2,7 @@ import javafx.application.Application; import javafx.stage.Stage; +import org.icepdf.core.pobjects.Document; import java.util.logging.Logger; @@ -18,6 +19,13 @@ public static void main(String[] args) throws Exception { @Override public void start(final Stage primaryStage) throws Exception { + String filePath = System.getenv().get("-loadfile"); + Document document = null; + if (filePath != null) { + document = new Document(); + document.setFile(filePath); + } + // read stored system font properties. // FontPropertiesManager.getInstance().loadOrReadSystemFonts(); @@ -25,7 +33,7 @@ public void start(final Stage primaryStage) throws Exception { // setup the viewer ri properties manager ViewerStageManager stageManager = ViewerStageManager.getInstance(); - stageManager.createViewer(primaryStage, null); + stageManager.createViewer(primaryStage, document); stageManager.setTitleAndIcons(primaryStage); primaryStage.show(); diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewBuilder.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewBuilder.java index be2d2e376..945ca3936 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewBuilder.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewBuilder.java @@ -19,9 +19,10 @@ public class ViewBuilder implements Builder { private Consumer openDocumentActionHandler; - DocumentViewPane documentViewPane = new DocumentViewPane(null); + DocumentViewPane documentViewPane; public ViewBuilder(ViewerModel model) { + this.model = model; } @@ -29,6 +30,7 @@ public ViewBuilder(ViewerModel model) { public Region build() { BorderPane borderPane = new BorderPane(); borderPane.setTop(createToolbar()); + documentViewPane = new DocumentViewPane(model); borderPane.setCenter(documentViewPane); return borderPane; } diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerStageManager.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerStageManager.java index cb946b550..8b42ce8e9 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerStageManager.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerStageManager.java @@ -17,8 +17,8 @@ private ViewerStageManager() { public void createViewer(Stage stage, Document document) { FxController controller = new FxController(); - controller.getModel().document.set(document); Scene scene = new Scene(controller.getView(), 400, 200); + controller.getModel().document.set(document); stage.setScene(scene); stage.setOnCloseRequest(new StageCloseRequestListener(controller.getModel())); } diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/OpenFileCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/OpenFileCommand.java index 52a3c377d..8ad0d8ed5 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/OpenFileCommand.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/OpenFileCommand.java @@ -31,8 +31,9 @@ public void execute() { // set document try { Document document = new Document(); + document.setFile(file.getAbsolutePath()); - if (model.useSingleViewerStage.get()) { + if (model.useSingleViewerStage.get() || model.document.get() == null) { model.document.set(document); model.filePath.set(file.getAbsolutePath()); } else { diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java index aafc60243..05f8c7471 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java @@ -4,9 +4,12 @@ import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleFloatProperty; import javafx.beans.property.SimpleIntegerProperty; +import javafx.geometry.Bounds; +import javafx.scene.Node; import javafx.scene.control.ScrollPane; import javafx.scene.layout.*; import org.icepdf.core.pobjects.Document; +import org.icepdf.fx.ri.viewer.ViewerModel; public class DocumentViewPane extends Region { @@ -14,33 +17,80 @@ public class DocumentViewPane extends Region { private FloatProperty rotation = new SimpleFloatProperty(0.0f); private IntegerProperty currentPageIndex = new SimpleIntegerProperty(0); + private Pane tilePane; + private ScrollPane scrollPane; - public DocumentViewPane(Document document) { - createLayout(document); + public DocumentViewPane(ViewerModel model) { + createLayout(model); + model.document.addListener((observable, oldValue, newValue) -> { + tilePane.getChildren().clear(); + // create a page view for each page + long start = System.currentTimeMillis(); + Document document = model.document.get(); + if (document != null) { + for (int i = 0, max = document.getNumberOfPages(); i < max; i++) { + PageViewWidget pageViewPane = new PageViewWidget(i, scale, rotation, scrollPane); + pageViewPane.setBorder(new Border(new BorderStroke(null, BorderStrokeStyle.SOLID, null, + new BorderWidths(1)))); + tilePane.getChildren().add(pageViewPane); + pageViewPane.viewportBounds.bind(scrollPane.viewportBoundsProperty()); + } + } + + long end = System.currentTimeMillis(); + System.out.printf("Page creation time: %dms%n", end - start); + }); } - private void createLayout(Document document) { - ScrollPane scrollPane = new ScrollPane(); + + private void createLayout(ViewerModel model) { + scrollPane = new ScrollPane(); + scrollPane.setFitToWidth(true); scrollPane.prefWidthProperty().bind(this.widthProperty()); scrollPane.prefHeightProperty().bind(this.heightProperty()); - TilePane tilePane = new TilePane(); - tilePane.setPrefColumns(1); - tilePane.setVgap(25); - tilePane.setHgap(25); - - // create a page view for each page - for (int i = 0; i < 250; i++) { - PageViewWidget pageViewPane = new PageViewWidget(i, scale, rotation); - pageViewPane.setBorder(new Border(new BorderStroke(null, BorderStrokeStyle.SOLID, null, - new BorderWidths(1)))); - tilePane.getChildren().add(pageViewPane); - } + tilePane = new VBox(); + + scrollPane.vvalueProperty().addListener((observable, oldValue, newValue) -> { + long start = System.currentTimeMillis(); + for (int i = 0; i < tilePane.getChildren().size(); i++) { + Node label = (Node) tilePane.getChildren().get(i); + if (isNodeIntersectingViewport(scrollPane, label)) { + System.out.println("Page " + ((PageViewWidget) label).getPageIndex() + " is in the viewport"); + // todo trigger a page capture + // - page should be doing intersection check + // - any size change would trigger a repaint + // - eventually bring clipped painting + // - try painting to a buffer and then paint that buffer to the screen (from prevoius work + // - try painting to graphics contet too, maybe it's fast/optimized for reactive painting. + } + } + long end = System.currentTimeMillis(); +// System.out.println("Viewport check time: " + (end - start) + "ms"); +// System.out.println(); + }); + scrollPane.setContent(tilePane); getChildren().add(scrollPane); } + private boolean isNodeIntersectingViewport(ScrollPane scrollPane, Node node) { + Bounds viewportBounds = scrollPane.getViewportBounds(); + Bounds nodeBounds = node.localToScene(node.getBoundsInLocal()); + Bounds scrollPaneBounds = scrollPane.localToScene(scrollPane.getBoundsInLocal()); + double x = scrollPaneBounds.getMinX() + viewportBounds.getMinX(); + double y = scrollPaneBounds.getMinY() + viewportBounds.getMinY(); + double width = viewportBounds.getWidth(); + double height = viewportBounds.getHeight(); + return nodeBounds.intersects( + x, + y, + width, + height + ); + } + public FloatProperty scaleProperty() { return scale; } diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java index 7073fe187..e3e98efbe 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java @@ -1,7 +1,10 @@ package org.icepdf.fx.ri.views; import javafx.beans.property.*; +import javafx.geometry.Bounds; +import javafx.scene.Node; import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; @@ -11,6 +14,8 @@ public class PageViewWidget extends Region { private FloatProperty rotation; private IntegerProperty pageIndex; + public ObjectProperty viewportBounds; + private DoubleProperty width; private DoubleProperty height; @@ -18,13 +23,15 @@ public class PageViewWidget extends Region { private Label scaleLabel; private Label rotationLabel; - public PageViewWidget(int pageIndex, FloatProperty scale, FloatProperty rotation) { + public PageViewWidget(int pageIndex, FloatProperty scale, FloatProperty rotation, ScrollPane scrollPane) { this.pageIndex = new SimpleIntegerProperty(pageIndex); this.scale = new SimpleFloatProperty(); this.scale.bind(scale); this.rotation = new SimpleFloatProperty(); this.rotation.bind(rotation); + this.viewportBounds = new SimpleObjectProperty<>(); + width = new SimpleDoubleProperty(75); height = new SimpleDoubleProperty(50); @@ -37,6 +44,23 @@ public PageViewWidget(int pageIndex, FloatProperty scale, FloatProperty rotation scaleLabel = new Label(); rotationLabel = new Label(); createLayout(); + +// setOnMouseClicked(event -> { +// System.out.println("Page " + pageIndex + " " + isNodeIntersectingViewport(scrollPane, this)); +// }); + } + + private boolean isNodeIntersectingViewport(ScrollPane scrollPane, Node node) { + Bounds viewportBounds = scrollPane.getViewportBounds(); + Bounds nodeBounds = node.localToScene(node.getBoundsInLocal()); + Bounds scrollPaneBounds = scrollPane.localToScene(scrollPane.getBoundsInLocal()); + + return nodeBounds.intersects( + scrollPaneBounds.getMinX() + viewportBounds.getMinX(), + scrollPaneBounds.getMinY() + viewportBounds.getMinY(), + viewportBounds.getWidth(), + viewportBounds.getHeight() + ); } private void createLayout() { @@ -47,6 +71,7 @@ private void createLayout() { minWidthProperty().bind(width); minHeightProperty().bind(height); getChildren().add(vBox); + } public int getPageIndex() { From 8743cb8c9da474885ed6268acdac9fbe7055d63e Mon Sep 17 00:00:00 2001 From: Patrick Corless Date: Thu, 7 Nov 2024 22:31:23 -0700 Subject: [PATCH 4/7] GH-372 very under cooked page viewer. Still lots of experimenting going on --- qa/viewer-jfx/pom.xml | 2 +- viewer/viewer-fx/build.gradle | 4 +- viewer/viewer-fx/pom.xml | 20 +- .../fx/ri/util/FontPropertiesManager.java | 91 +++++++++ .../org/icepdf/fx/ri/viewer/Launcher.java | 9 + .../icepdf/fx/ri/views/DocumentViewPane.java | 28 +-- .../icepdf/fx/ri/views/PageViewWidget.java | 178 ++++++++++++++++-- 7 files changed, 287 insertions(+), 45 deletions(-) create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/util/FontPropertiesManager.java diff --git a/qa/viewer-jfx/pom.xml b/qa/viewer-jfx/pom.xml index 352e19a4d..33d512c12 100644 --- a/qa/viewer-jfx/pom.xml +++ b/qa/viewer-jfx/pom.xml @@ -14,7 +14,7 @@ org.openjfx javafx-controls - 11 + 21 diff --git a/viewer/viewer-fx/build.gradle b/viewer/viewer-fx/build.gradle index 50bb104e4..e56375d9b 100644 --- a/viewer/viewer-fx/build.gradle +++ b/viewer/viewer-fx/build.gradle @@ -23,7 +23,7 @@ repositories { } javafx { - version = "11.0.2" + version = "21.0.5" modules = [ 'javafx.base', 'javafx.controls', 'javafx.graphics', 'javafx.swing' ] } @@ -33,6 +33,8 @@ dependencies { // signature validation. implementation 'org.bouncycastle:bcprov-jdk18on:' + "${BOUNCY_VERSION}" implementation 'org.bouncycastle:bcpkix-jdk18on:' + "${BOUNCY_VERSION}" + // drawing tests + implementation 'org.jfree:org.jfree.fxgraphics2d:2.1' // tests testImplementation(platform("org.junit:junit-bom:${JUNIT_BOM_VERSION}")) testImplementation('org.junit.jupiter:junit-jupiter') diff --git a/viewer/viewer-fx/pom.xml b/viewer/viewer-fx/pom.xml index 74dc25e7d..31ff4f4e0 100644 --- a/viewer/viewer-fx/pom.xml +++ b/viewer/viewer-fx/pom.xml @@ -17,12 +17,22 @@ org.openjfx javafx-controls - 11 + 21 + + + org.openjfx + javafx-graphics + 21 org.openjfx javafx-swing - 11 + 21 + + + org.jfree + org.jfree.fxgraphics2d + 2.1 com.github.pcorless.icepdf @@ -34,6 +44,12 @@ 5.9.3 test + + org.openjfx + javafx-graphics + 23 + compile + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/util/FontPropertiesManager.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/util/FontPropertiesManager.java new file mode 100644 index 000000000..46054c7e7 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/util/FontPropertiesManager.java @@ -0,0 +1,91 @@ +package org.icepdf.fx.ri.util; + +import org.icepdf.core.pobjects.fonts.FontManager; + +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.prefs.BackingStoreException; +import java.util.prefs.Preferences; + +/** + * \ + */ +public class FontPropertiesManager { + + private static final Logger logger = Logger.getLogger(FontPropertiesManager.class.toString()); + + private static Preferences prefs = Preferences.userNodeForPackage(FontPropertiesManager.class); + + private static FontPropertiesManager fontPropertiesManager; + + private static FontManager fontManager = FontManager.getInstance(); + + private FontPropertiesManager() { + + } + + public static FontPropertiesManager getInstance() { + if (fontPropertiesManager == null) { + fontPropertiesManager = new FontPropertiesManager(); + } + return fontPropertiesManager; + } + + public void readDefaultProperties(String... paths) { + try { + fontManager.readSystemFonts(paths); + } catch (Exception e) { + if (logger.isLoggable(Level.WARNING)) { + logger.log(Level.WARNING, "Error reading system fonts path: ", e); + } + } + } + + public void readFontProperties(String... paths) { + try { + // If you application needs to look at other font directories + // they can be added via the readSystemFonts method. + fontManager.readFonts(paths); + } catch (Exception e) { + if (logger.isLoggable(Level.WARNING)) { + logger.log(Level.WARNING, "Error reading system paths:", e); + } + } + } + + public void loadProperties() { + fontManager.setFontProperties(prefs); + } + + public void clearProperties() { + try { + prefs.clear(); + } catch (BackingStoreException e) { + if (logger.isLoggable(Level.WARNING)) { + logger.log(Level.WARNING, "Error reading system paths:", e); + } + } + } + + public void updateProperties() { + Properties fontProps = fontManager.getFontProperties(); + for (Object key : fontProps.keySet()) { + prefs.put((String) key, fontProps.getProperty((String) key)); + } + } + + public boolean isPropertiesEmpty() { + try { + return prefs.keys().length == 0; + } catch (BackingStoreException e) { + e.printStackTrace(); + } + return false; + } + + + public static FontManager getFontManager() { + return FontManager.getInstance(); + } +} diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Launcher.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Launcher.java index 5788217c6..a3076607a 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Launcher.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Launcher.java @@ -3,6 +3,7 @@ import javafx.application.Application; import javafx.stage.Stage; import org.icepdf.core.pobjects.Document; +import org.icepdf.fx.ri.util.FontPropertiesManager; import java.util.logging.Logger; @@ -12,6 +13,14 @@ public class Launcher extends Application { public static void main(String[] args) throws Exception { + FontPropertiesManager fontPropertiesManager = FontPropertiesManager.getInstance(); + + if (fontPropertiesManager.isPropertiesEmpty()) { + fontPropertiesManager.readDefaultProperties(); + fontPropertiesManager.updateProperties(); + } else { + fontPropertiesManager.loadProperties(); + } Application.launch(args); } diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java index 05f8c7471..1e3729065 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java @@ -17,7 +17,7 @@ public class DocumentViewPane extends Region { private FloatProperty rotation = new SimpleFloatProperty(0.0f); private IntegerProperty currentPageIndex = new SimpleIntegerProperty(0); - private Pane tilePane; + private TilePane tilePane; private ScrollPane scrollPane; public DocumentViewPane(ViewerModel model) { @@ -29,7 +29,7 @@ public DocumentViewPane(ViewerModel model) { Document document = model.document.get(); if (document != null) { for (int i = 0, max = document.getNumberOfPages(); i < max; i++) { - PageViewWidget pageViewPane = new PageViewWidget(i, scale, rotation, scrollPane); + PageViewWidget pageViewPane = new PageViewWidget(model, i, scale, rotation, scrollPane); pageViewPane.setBorder(new Border(new BorderStroke(null, BorderStrokeStyle.SOLID, null, new BorderWidths(1)))); tilePane.getChildren().add(pageViewPane); @@ -40,6 +40,7 @@ public DocumentViewPane(ViewerModel model) { long end = System.currentTimeMillis(); System.out.printf("Page creation time: %dms%n", end - start); }); + } @@ -50,27 +51,8 @@ private void createLayout(ViewerModel model) { scrollPane.prefWidthProperty().bind(this.widthProperty()); scrollPane.prefHeightProperty().bind(this.heightProperty()); - tilePane = new VBox(); - - scrollPane.vvalueProperty().addListener((observable, oldValue, newValue) -> { - long start = System.currentTimeMillis(); - for (int i = 0; i < tilePane.getChildren().size(); i++) { - Node label = (Node) tilePane.getChildren().get(i); - if (isNodeIntersectingViewport(scrollPane, label)) { - System.out.println("Page " + ((PageViewWidget) label).getPageIndex() + " is in the viewport"); - // todo trigger a page capture - // - page should be doing intersection check - // - any size change would trigger a repaint - // - eventually bring clipped painting - // - try painting to a buffer and then paint that buffer to the screen (from prevoius work - // - try painting to graphics contet too, maybe it's fast/optimized for reactive painting. - } - } - long end = System.currentTimeMillis(); -// System.out.println("Viewport check time: " + (end - start) + "ms"); -// System.out.println(); - }); - + tilePane = new TilePane(); + tilePane.setPrefColumns(1); scrollPane.setContent(tilePane); getChildren().add(scrollPane); } diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java index e3e98efbe..7ffb5d4ef 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java @@ -1,12 +1,27 @@ package org.icepdf.fx.ri.views; import javafx.beans.property.*; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; import javafx.geometry.Bounds; +import javafx.scene.Group; import javafx.scene.Node; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.layout.Region; -import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.shape.ArcType; +import javafx.scene.text.FontSmoothingType; +import org.icepdf.core.pobjects.Document; +import org.icepdf.core.pobjects.PDimension; +import org.icepdf.core.util.GraphicsRenderingHints; +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.jfree.fx.FXGraphics2D; +import org.jfree.fx.FXHints; + +import java.awt.*; public class PageViewWidget extends Region { @@ -18,26 +33,57 @@ public class PageViewWidget extends Region { private DoubleProperty width; private DoubleProperty height; + private double pageWidth; + private double pageHeight; private Label pageIndexLabel; private Label scaleLabel; private Label rotationLabel; - public PageViewWidget(int pageIndex, FloatProperty scale, FloatProperty rotation, ScrollPane scrollPane) { + private ViewerModel model; + private ScrollPane scrollPane; + + private Canvas canvas; + + public PageViewWidget(ViewerModel model, int pageIndex, FloatProperty scale, FloatProperty rotation, ScrollPane scrollPane) { this.pageIndex = new SimpleIntegerProperty(pageIndex); this.scale = new SimpleFloatProperty(); this.scale.bind(scale); this.rotation = new SimpleFloatProperty(); this.rotation.bind(rotation); + this.scrollPane = scrollPane; + this.model = model; + this.viewportBounds = new SimpleObjectProperty<>(); - width = new SimpleDoubleProperty(75); - height = new SimpleDoubleProperty(50); + + Document document = model.document.get(); + PDimension pageSize = document.getPageDimension(pageIndex, rotation.get(), scale.get()); + pageWidth = pageSize.getWidth(); + pageHeight = pageSize.getHeight(); + width = new SimpleDoubleProperty(pageWidth); + height = new SimpleDoubleProperty(pageHeight); + + + scale.addListener((observable, oldValue, newValue) -> { - width.set(75 * newValue.floatValue()); - height.set(50 * newValue.floatValue()); + width.set(pageWidth * newValue.floatValue()); + height.set(pageHeight * newValue.floatValue()); + }); + + scrollPane.vvalueProperty().addListener((observable, oldValue, newValue) -> { + if (isNodeIntersectingViewport(scrollPane, this)) { +// System.out.println("Page " + this.getPageIndex() + " is in the viewport"); + draw(); + // todo trigger a page capture + // - page should be doing intersection check + // - any size change would trigger a repaint + // - eventually bring clipped painting + // - try painting to a buffer and then paint that buffer to the screen (from prevoius work + // - try painting to graphics contet too, maybe it's fast/optimized for reactive painting. + } }); pageIndexLabel = new Label("Page " + pageIndex); @@ -45,32 +91,128 @@ public PageViewWidget(int pageIndex, FloatProperty scale, FloatProperty rotation rotationLabel = new Label(); createLayout(); -// setOnMouseClicked(event -> { -// System.out.println("Page " + pageIndex + " " + isNodeIntersectingViewport(scrollPane, this)); -// }); + setOnMouseClicked(event -> { + System.out.println("Page " + pageIndex + " " + isNodeIntersectingViewport(scrollPane, this)); + }); + + scrollPane.viewportBoundsProperty().addListener((observable, oldValue, newValue) -> { + draw(); + }); + + // Add listener to detect when the node has valid bounds and is laid out + boundsInParentProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observable, Bounds oldBounds, Bounds newBounds) { + if (newBounds.getWidth() == width.get() && newBounds.getHeight() == height.get()) { + draw(); + } + } + }); + } + + public void draw() { + if (canvas == null && isNodeIntersectingViewport(scrollPane, this)) { + System.out.println("drawing page " + this.pageIndex); + + canvas = new Canvas(pageWidth, pageHeight); + Group root = new Group(); + GraphicsContext gc = canvas.getGraphicsContext2D(); +// drawShapes(gc); + root.getChildren().add(canvas); + getChildren().add(root); + + + gc.setFontSmoothingType(FontSmoothingType.LCD); + FXGraphics2D fxg2 = new FXGraphics2D(gc); + fxg2.setRenderingHint(FXHints.KEY_USE_FX_FONT_METRICS, true); + fxg2.setZeroStrokeWidth(0.1); + fxg2.setRenderingHint( + RenderingHints.KEY_FRACTIONALMETRICS, + RenderingHints.VALUE_FRACTIONALMETRICS_ON); + fxg2.setClip(0, 0, (int) width.get(), (int) height.get()); + fxg2.scale(1, -1); + fxg2.translate(0, -height.get()); + try { + model.document.get().getPageTree().getPage(pageIndex.get()) + .paintPageContent(fxg2, GraphicsRenderingHints.PRINT, rotation.get(), scale.get(), true, true); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + private void drawShapes(GraphicsContext gc) { + gc.setFill(Color.GREEN); + gc.setStroke(Color.BLUE); + gc.setLineWidth(5); + gc.strokeLine(40, 10, 10, 40); + gc.fillOval(10, 60, 30, 30); + gc.strokeOval(60, 60, 30, 30); + gc.fillRoundRect(110, 60, 30, 30, 10, 10); + gc.strokeRoundRect(160, 60, 30, 30, 10, 10); + gc.fillArc(10, 110, 30, 30, 45, 240, ArcType.OPEN); + gc.fillArc(60, 110, 30, 30, 45, 240, ArcType.CHORD); + gc.fillArc(110, 110, 30, 30, 45, 240, ArcType.ROUND); + gc.strokeArc(10, 160, 30, 30, 45, 240, ArcType.OPEN); + gc.strokeArc(60, 160, 30, 30, 45, 240, ArcType.CHORD); + gc.strokeArc(110, 160, 30, 30, 45, 240, ArcType.ROUND); + gc.fillPolygon(new double[]{10, 40, 10, 40}, + new double[]{210, 210, 240, 240}, 4); + gc.strokePolygon(new double[]{60, 90, 60, 90}, + new double[]{210, 210, 240, 240}, 4); + gc.strokePolyline(new double[]{110, 140, 110, 140}, + new double[]{210, 210, 240, 240}, 4); } private boolean isNodeIntersectingViewport(ScrollPane scrollPane, Node node) { + + Bounds nodeBounds = node.getBoundsInParent(); +// if (nodeBounds.getWidth() != width.get() || nodeBounds.getHeight() != height.get()) { +// return false; +// } + if (nodeBounds.getMinY() == 0 && pageIndex.get() > 0) { + return false; + } Bounds viewportBounds = scrollPane.getViewportBounds(); - Bounds nodeBounds = node.localToScene(node.getBoundsInLocal()); - Bounds scrollPaneBounds = scrollPane.localToScene(scrollPane.getBoundsInLocal()); + + double hmin = scrollPane.getHmin(); + double hmax = scrollPane.getHmax(); + double hvalue = scrollPane.getHvalue(); + double contentWidth = scrollPane.getContent().getLayoutBounds().getWidth(); + double viewportWidth = scrollPane.getViewportBounds().getWidth(); + + double hoffset = + Math.max(0, contentWidth - viewportWidth) * (hvalue - hmin) / (hmax - hmin); + + double vmin = scrollPane.getVmin(); + double vmax = scrollPane.getVmax(); + double vvalue = scrollPane.getVvalue(); + double contentHeight = scrollPane.getContent().getLayoutBounds().getHeight(); + double viewportHeight = scrollPane.getViewportBounds().getHeight(); + + double voffset = + Math.max(0, contentHeight - viewportHeight) * (vvalue - vmin) / (vmax - vmin); + +// System.out.printf("Offset: [%.1f, %.1f] width: %.1f height: %.1f %n", +// hoffset, voffset, viewportWidth, viewportHeight); return nodeBounds.intersects( - scrollPaneBounds.getMinX() + viewportBounds.getMinX(), - scrollPaneBounds.getMinY() + viewportBounds.getMinY(), + hoffset, + voffset, viewportBounds.getWidth(), viewportBounds.getHeight() ); } private void createLayout() { - VBox vBox = new VBox(); - scaleLabel.textProperty().bind(scale.asString()); - rotationLabel.textProperty().bind(width.asString()); - vBox.getChildren().addAll(pageIndexLabel, scaleLabel, rotationLabel); minWidthProperty().bind(width); minHeightProperty().bind(height); - getChildren().add(vBox); + +// VBox vBox = new VBox(); +// scaleLabel.textProperty().bind(scale.asString()); +// rotationLabel.textProperty().bind(width.asString()); +// vBox.getChildren().addAll(pageIndexLabel, scaleLabel, rotationLabel); +// getChildren().add(vBox); } From 783eb7bcfa6cff2c2b6c3d17d4659a412ad9465d Mon Sep 17 00:00:00 2001 From: Patrick Corless Date: Tue, 12 Nov 2024 22:29:19 -0700 Subject: [PATCH 5/7] GH-372 filling out some basic page paint functionality --- .../fx/ri/viewer/ViewerStageManager.java | 2 +- .../icepdf/fx/ri/views/DocumentViewPane.java | 23 ++- .../icepdf/fx/ri/views/PageViewWidget.java | 153 ++++++++++++++---- 3 files changed, 138 insertions(+), 40 deletions(-) diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerStageManager.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerStageManager.java index 8b42ce8e9..005506d35 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerStageManager.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerStageManager.java @@ -17,7 +17,7 @@ private ViewerStageManager() { public void createViewer(Stage stage, Document document) { FxController controller = new FxController(); - Scene scene = new Scene(controller.getView(), 400, 200); + Scene scene = new Scene(controller.getView(), 1024, 768); controller.getModel().document.set(document); stage.setScene(scene); stage.setOnCloseRequest(new StageCloseRequestListener(controller.getModel())); diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java index 1e3729065..902411309 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java @@ -5,6 +5,7 @@ import javafx.beans.property.SimpleFloatProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.geometry.Bounds; +import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.ScrollPane; import javafx.scene.layout.*; @@ -17,13 +18,13 @@ public class DocumentViewPane extends Region { private FloatProperty rotation = new SimpleFloatProperty(0.0f); private IntegerProperty currentPageIndex = new SimpleIntegerProperty(0); - private TilePane tilePane; + private VBox pageLayoutPane; private ScrollPane scrollPane; public DocumentViewPane(ViewerModel model) { createLayout(model); model.document.addListener((observable, oldValue, newValue) -> { - tilePane.getChildren().clear(); + pageLayoutPane.getChildren().clear(); // create a page view for each page long start = System.currentTimeMillis(); Document document = model.document.get(); @@ -32,7 +33,7 @@ public DocumentViewPane(ViewerModel model) { PageViewWidget pageViewPane = new PageViewWidget(model, i, scale, rotation, scrollPane); pageViewPane.setBorder(new Border(new BorderStroke(null, BorderStrokeStyle.SOLID, null, new BorderWidths(1)))); - tilePane.getChildren().add(pageViewPane); + pageLayoutPane.getChildren().add(pageViewPane); pageViewPane.viewportBounds.bind(scrollPane.viewportBoundsProperty()); } } @@ -48,12 +49,22 @@ private void createLayout(ViewerModel model) { scrollPane = new ScrollPane(); scrollPane.setFitToWidth(true); + scrollPane.setPannable(true); scrollPane.prefWidthProperty().bind(this.widthProperty()); scrollPane.prefHeightProperty().bind(this.heightProperty()); - tilePane = new TilePane(); - tilePane.setPrefColumns(1); - scrollPane.setContent(tilePane); + + VBox parent = new VBox(); + parent.setAlignment(Pos.CENTER); + + pageLayoutPane = new VBox(); + pageLayoutPane.setAlignment(Pos.CENTER); + pageLayoutPane.setSpacing(10); + pageLayoutPane.setMaxWidth(Region.USE_PREF_SIZE); + + parent.getChildren().add(pageLayoutPane); + + scrollPane.setContent(parent); getChildren().add(scrollPane); } diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java index 7ffb5d4ef..368119eb1 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java @@ -1,8 +1,11 @@ package org.icepdf.fx.ri.views; +import javafx.animation.PauseTransition; +import javafx.application.Platform; import javafx.beans.property.*; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; +import javafx.concurrent.Task; import javafx.geometry.Bounds; import javafx.scene.Group; import javafx.scene.Node; @@ -14,8 +17,10 @@ import javafx.scene.paint.Color; import javafx.scene.shape.ArcType; import javafx.scene.text.FontSmoothingType; +import javafx.util.Duration; import org.icepdf.core.pobjects.Document; import org.icepdf.core.pobjects.PDimension; +import org.icepdf.core.pobjects.Page; import org.icepdf.core.util.GraphicsRenderingHints; import org.icepdf.fx.ri.viewer.ViewerModel; import org.jfree.fx.FXGraphics2D; @@ -45,6 +50,11 @@ public class PageViewWidget extends Region { private Canvas canvas; + private Task pageCaptureTask; + + private static final Duration SCROLL_PAUSE_DURATION = Duration.millis(200); + private PauseTransition scrollPause; + public PageViewWidget(ViewerModel model, int pageIndex, FloatProperty scale, FloatProperty rotation, ScrollPane scrollPane) { this.pageIndex = new SimpleIntegerProperty(pageIndex); this.scale = new SimpleFloatProperty(); @@ -53,6 +63,10 @@ public PageViewWidget(ViewerModel model, int pageIndex, FloatProperty scale, Flo this.rotation.bind(rotation); this.scrollPane = scrollPane; this.model = model; + scrollPause = new PauseTransition(SCROLL_PAUSE_DURATION); + scrollPause.setOnFinished(event -> { + draw(); + }); this.viewportBounds = new SimpleObjectProperty<>(); @@ -64,9 +78,8 @@ public PageViewWidget(ViewerModel model, int pageIndex, FloatProperty scale, Flo pageHeight = pageSize.getHeight(); width = new SimpleDoubleProperty(pageWidth); height = new SimpleDoubleProperty(pageHeight); - - - + setWidth(pageWidth); + setHeight(pageHeight); scale.addListener((observable, oldValue, newValue) -> { width.set(pageWidth * newValue.floatValue()); @@ -74,16 +87,15 @@ public PageViewWidget(ViewerModel model, int pageIndex, FloatProperty scale, Flo }); scrollPane.vvalueProperty().addListener((observable, oldValue, newValue) -> { - if (isNodeIntersectingViewport(scrollPane, this)) { + // System.out.println("Page " + this.getPageIndex() + " is in the viewport"); - draw(); - // todo trigger a page capture - // - page should be doing intersection check - // - any size change would trigger a repaint - // - eventually bring clipped painting - // - try painting to a buffer and then paint that buffer to the screen (from prevoius work - // - try painting to graphics contet too, maybe it's fast/optimized for reactive painting. - } + scrollPause.playFromStart(); + // todo trigger a page capture + // - page should be doing intersection check + // - any size change would trigger a repaint + // - eventually bring clipped painting + // - try painting to a buffer and then paint that buffer to the screen (from prevoius work + // - try painting to graphics contet too, maybe it's fast/optimized for reactive painting. }); pageIndexLabel = new Label("Page " + pageIndex); @@ -96,7 +108,9 @@ public PageViewWidget(ViewerModel model, int pageIndex, FloatProperty scale, Flo }); scrollPane.viewportBoundsProperty().addListener((observable, oldValue, newValue) -> { - draw(); + if (isNodeIntersectingViewport(scrollPane, this)) { + draw(); + } }); // Add listener to detect when the node has valid bounds and is laid out @@ -114,33 +128,77 @@ public void draw() { if (canvas == null && isNodeIntersectingViewport(scrollPane, this)) { System.out.println("drawing page " + this.pageIndex); - canvas = new Canvas(pageWidth, pageHeight); - Group root = new Group(); - GraphicsContext gc = canvas.getGraphicsContext2D(); -// drawShapes(gc); - root.getChildren().add(canvas); - getChildren().add(root); - - - gc.setFontSmoothingType(FontSmoothingType.LCD); - FXGraphics2D fxg2 = new FXGraphics2D(gc); - fxg2.setRenderingHint(FXHints.KEY_USE_FX_FONT_METRICS, true); - fxg2.setZeroStrokeWidth(0.1); - fxg2.setRenderingHint( - RenderingHints.KEY_FRACTIONALMETRICS, - RenderingHints.VALUE_FRACTIONALMETRICS_ON); - fxg2.setClip(0, 0, (int) width.get(), (int) height.get()); - fxg2.scale(1, -1); - fxg2.translate(0, -height.get()); try { - model.document.get().getPageTree().getPage(pageIndex.get()) - .paintPageContent(fxg2, GraphicsRenderingHints.PRINT, rotation.get(), scale.get(), true, true); + Page page = model.document.get().getPageTree().getPage(pageIndex.get()); + if (!page.isInitiated() && (pageCaptureTask == null || pageCaptureTask.isDone())) { + pageCaptureTask = new Task<>() { + @Override + protected Void call() throws Exception { + long start = System.currentTimeMillis(); + page.init(); + long end = System.currentTimeMillis(); + System.out.println("Page init time: " + (end - start) + "ms"); + Platform.runLater(() -> draw()); +// +// WritableImage image = new WritableImage((int) width.get(), (int) height.get()); +// image.getPixelWriter(). + + return null; + } + }; + Thread pageInitThread = new Thread(pageCaptureTask); + pageInitThread.start(); + return; + } + + canvas = new Canvas(pageWidth, pageHeight); + + Group root = new Group(); + GraphicsContext gc = canvas.getGraphicsContext2D(); + root.getChildren().add(canvas); + getChildren().add(root); + + // try and get this working with + canvas.scaleXProperty().bind(scale); + canvas.scaleYProperty().bind(scale); + + gc.setStroke(Color.RED); + gc.setLineWidth(5); + calculateAndDrawClip(gc); + + gc.setFontSmoothingType(FontSmoothingType.LCD); + FXGraphics2D fxg2 = new FXGraphics2D(gc); + fxg2.setRenderingHint(FXHints.KEY_USE_FX_FONT_METRICS, true); + fxg2.setZeroStrokeWidth(0.1); + fxg2.setRenderingHint( + RenderingHints.KEY_FRACTIONALMETRICS, + RenderingHints.VALUE_FRACTIONALMETRICS_ON); + fxg2.setClip(0, 0, (int) width.get(), (int) height.get()); + fxg2.scale(1, -1); + fxg2.translate(0, -height.get()); + + + long start = System.currentTimeMillis(); + page.paintPageContent(fxg2, GraphicsRenderingHints.PRINT, rotation.get(), scale.get(), true, true); + long end = System.currentTimeMillis(); + System.out.println("Page paint time: " + (end - start) + "ms"); } catch (InterruptedException e) { throw new RuntimeException(e); } } } + private void calculateAndDrawClip(GraphicsContext gc) { + Bounds viewportBounds = scrollPane.getViewportBounds(); + Bounds nodeBounds = localToScene(getBoundsInLocal()); + Bounds scrollPaneBounds = scrollPane.localToScene(scrollPane.getBoundsInLocal()); + double x = scrollPaneBounds.getMinX() + viewportBounds.getMinX(); + double y = scrollPaneBounds.getMinY() + viewportBounds.getMinY(); + double width = viewportBounds.getWidth(); + double height = viewportBounds.getHeight(); + gc.strokeRect(x, y, width, height); + } + private void drawShapes(GraphicsContext gc) { gc.setFill(Color.GREEN); gc.setStroke(Color.BLUE); @@ -204,6 +262,35 @@ private boolean isNodeIntersectingViewport(ScrollPane scrollPane, Node node) { ); } + private Bounds intersectionClip(ScrollPane scrollPane, Node node) { + + Bounds nodeBounds = node.getBoundsInParent(); + + Bounds viewportBounds = scrollPane.getViewportBounds(); + + double hmin = scrollPane.getHmin(); + double hmax = scrollPane.getHmax(); + double hvalue = scrollPane.getHvalue(); + double contentWidth = scrollPane.getContent().getLayoutBounds().getWidth(); + double viewportWidth = scrollPane.getViewportBounds().getWidth(); + + double hoffset = + Math.max(0, contentWidth - viewportWidth) * (hvalue - hmin) / (hmax - hmin); + + double vmin = scrollPane.getVmin(); + double vmax = scrollPane.getVmax(); + double vvalue = scrollPane.getVvalue(); + double contentHeight = scrollPane.getContent().getLayoutBounds().getHeight(); + double viewportHeight = scrollPane.getViewportBounds().getHeight(); + + double voffset = + Math.max(0, contentHeight - viewportHeight) * (vvalue - vmin) / (vmax - vmin); + + // todo calculate intersection, so we can define a small canvas to draw to on high zoom levels + + return null; + } + private void createLayout() { minWidthProperty().bind(width); minHeightProperty().bind(height); From f65bb383dd165288e70670e0b1114bd029fba843 Mon Sep 17 00:00:00 2001 From: Patrick Corless Date: Mon, 18 Nov 2024 21:12:19 -0700 Subject: [PATCH 6/7] GH-372 more javafx experiments. --- viewer/viewer-fx/README.md | 3 + .../fx/ri/util/FontPropertiesManager.java | 2 +- .../org/icepdf/fx/ri/viewer/FxController.java | 5 +- .../org/icepdf/fx/ri/viewer/Launcher.java | 12 +- .../org/icepdf/fx/ri/viewer/ViewBuilder.java | 3 +- .../org/icepdf/fx/ri/viewer/ViewerModel.java | 1 - .../ri/viewer/commands/OpenFileCommand.java | 1 - .../fx/ri/viewer/commands/ZoomInCommand.java | 1 - .../listeners/DocumentChangeListener.java | 3 + .../listeners/StageCloseRequestListener.java | 4 +- .../icepdf/fx/ri/views/DocumentViewPane.java | 22 --- .../icepdf/fx/ri/views/PageViewWidget.java | 177 ++++++++---------- 12 files changed, 99 insertions(+), 135 deletions(-) diff --git a/viewer/viewer-fx/README.md b/viewer/viewer-fx/README.md index c60d1745f..984d10c79 100644 --- a/viewer/viewer-fx/README.md +++ b/viewer/viewer-fx/README.md @@ -1 +1,4 @@ mvn javafx:run -f ./viewer/viewer-fx/pom.xml + +~/dev/git/icepdf/viewer/viewer-fx/build.gradle +gradle :viewer:viewer-fx:run \ No newline at end of file diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/util/FontPropertiesManager.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/util/FontPropertiesManager.java index 46054c7e7..8bfe9b5d1 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/util/FontPropertiesManager.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/util/FontPropertiesManager.java @@ -9,7 +9,7 @@ import java.util.prefs.Preferences; /** - * \ + * todo: figure out module issue, can't access core classes from viewer. */ public class FontPropertiesManager { diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/FxController.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/FxController.java index 37c8519ca..eb2cee80d 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/FxController.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/FxController.java @@ -3,6 +3,9 @@ import javafx.scene.layout.Region; import org.icepdf.fx.ri.viewer.listeners.DocumentChangeListener; +/** + * Controller for the viewer application. + */ public class FxController { private final ViewerModel model; @@ -14,7 +17,7 @@ public FxController() { this.interactor = new Interactor(model); // is this really a mediator? this.viewBuilder = new ViewBuilder(model); - // clean up this viewer if the document changes + // auto clean up this viewer if the document changes this.model.document.addListener(new DocumentChangeListener(model)); } diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Launcher.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Launcher.java index a3076607a..ab6d28c4d 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Launcher.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Launcher.java @@ -35,20 +35,10 @@ public void start(final Stage primaryStage) throws Exception { document.setFile(filePath); } - - // read stored system font properties. -// FontPropertiesManager.getInstance().loadOrReadSystemFonts(); - - // setup the viewer ri properties manager - + // create first viewer window ViewerStageManager stageManager = ViewerStageManager.getInstance(); stageManager.createViewer(primaryStage, document); stageManager.setTitleAndIcons(primaryStage); - primaryStage.show(); - - } - - } \ No newline at end of file diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewBuilder.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewBuilder.java index 945ca3936..56e729b14 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewBuilder.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewBuilder.java @@ -9,6 +9,7 @@ import javafx.util.Builder; import org.icepdf.fx.ri.viewer.commands.OpenFileCommand; import org.icepdf.fx.ri.viewer.commands.ZoomInCommand; +import org.icepdf.fx.ri.viewer.commands.ZoomOutCommand; import org.icepdf.fx.ri.views.DocumentViewPane; import java.util.function.Consumer; @@ -44,7 +45,7 @@ private ToolBar createToolbar() { }); Button zoomOut = new Button("Zoom Out"); - zoomOut.setOnAction(event -> new ZoomInCommand(documentViewPane, model).execute()); + zoomOut.setOnAction(event -> new ZoomOutCommand(documentViewPane, model).execute()); zoomOut.disableProperty().bind(model.toolbarDisabled); Button zoomIn = new Button("Zoom In"); diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerModel.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerModel.java index bdf9c9e7e..9fa57207e 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerModel.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerModel.java @@ -7,7 +7,6 @@ public class ViewerModel { public final BooleanProperty useSingleViewerStage = new SimpleBooleanProperty(false); - // document public final ObjectProperty document; // todo, pretty sure we don't need this property: file/url path diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/OpenFileCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/OpenFileCommand.java index 8ad0d8ed5..10a4d67f4 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/OpenFileCommand.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/OpenFileCommand.java @@ -23,7 +23,6 @@ public OpenFileCommand(Window parentStage, ViewerModel model) { @Override public void execute() { - // show file chooser FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Open Resource File"); File file = fileChooser.showOpenDialog(stage); diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/ZoomInCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/ZoomInCommand.java index f113df4e4..d26c5775e 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/ZoomInCommand.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/ZoomInCommand.java @@ -8,7 +8,6 @@ public class ZoomInCommand implements Command { private final ViewerModel model; private final DocumentViewPane documentViewPane; - public ZoomInCommand(DocumentViewPane documentViewPane, ViewerModel model) { this.model = model; this.documentViewPane = documentViewPane; diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/listeners/DocumentChangeListener.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/listeners/DocumentChangeListener.java index b6f689e51..fdf78be86 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/listeners/DocumentChangeListener.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/listeners/DocumentChangeListener.java @@ -4,6 +4,9 @@ import org.icepdf.core.pobjects.Document; import org.icepdf.fx.ri.viewer.ViewerModel; +/** + * Document change listener that will dispose of the old document and enable the toolbar if a new document is set. + */ public class DocumentChangeListener implements ChangeListener { private ViewerModel model; diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/listeners/StageCloseRequestListener.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/listeners/StageCloseRequestListener.java index 731423bfe..b4f721db9 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/listeners/StageCloseRequestListener.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/listeners/StageCloseRequestListener.java @@ -5,7 +5,9 @@ import org.icepdf.core.pobjects.Document; import org.icepdf.fx.ri.viewer.ViewerModel; - +/** + * Stage close request listener that will dispose of the document when the stage is closed. + */ public class StageCloseRequestListener implements EventHandler { private final ViewerModel model; diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java index 902411309..b451a50b1 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java @@ -4,9 +4,7 @@ import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleFloatProperty; import javafx.beans.property.SimpleIntegerProperty; -import javafx.geometry.Bounds; import javafx.geometry.Pos; -import javafx.scene.Node; import javafx.scene.control.ScrollPane; import javafx.scene.layout.*; import org.icepdf.core.pobjects.Document; @@ -44,16 +42,13 @@ public DocumentViewPane(ViewerModel model) { } - private void createLayout(ViewerModel model) { scrollPane = new ScrollPane(); - scrollPane.setFitToWidth(true); scrollPane.setPannable(true); scrollPane.prefWidthProperty().bind(this.widthProperty()); scrollPane.prefHeightProperty().bind(this.heightProperty()); - VBox parent = new VBox(); parent.setAlignment(Pos.CENTER); @@ -61,29 +56,12 @@ private void createLayout(ViewerModel model) { pageLayoutPane.setAlignment(Pos.CENTER); pageLayoutPane.setSpacing(10); pageLayoutPane.setMaxWidth(Region.USE_PREF_SIZE); - parent.getChildren().add(pageLayoutPane); scrollPane.setContent(parent); getChildren().add(scrollPane); } - private boolean isNodeIntersectingViewport(ScrollPane scrollPane, Node node) { - Bounds viewportBounds = scrollPane.getViewportBounds(); - Bounds nodeBounds = node.localToScene(node.getBoundsInLocal()); - Bounds scrollPaneBounds = scrollPane.localToScene(scrollPane.getBoundsInLocal()); - double x = scrollPaneBounds.getMinX() + viewportBounds.getMinX(); - double y = scrollPaneBounds.getMinY() + viewportBounds.getMinY(); - double width = viewportBounds.getWidth(); - double height = viewportBounds.getHeight(); - return nodeBounds.intersects( - x, - y, - width, - height - ); - } - public FloatProperty scaleProperty() { return scale; } diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java index 368119eb1..61516ec6b 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java @@ -7,26 +7,19 @@ import javafx.beans.value.ObservableValue; import javafx.concurrent.Task; import javafx.geometry.Bounds; +import javafx.geometry.Rectangle2D; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; -import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.layout.Region; import javafx.scene.paint.Color; -import javafx.scene.shape.ArcType; -import javafx.scene.text.FontSmoothingType; import javafx.util.Duration; import org.icepdf.core.pobjects.Document; import org.icepdf.core.pobjects.PDimension; import org.icepdf.core.pobjects.Page; -import org.icepdf.core.util.GraphicsRenderingHints; import org.icepdf.fx.ri.viewer.ViewerModel; -import org.jfree.fx.FXGraphics2D; -import org.jfree.fx.FXHints; - -import java.awt.*; public class PageViewWidget extends Region { @@ -41,10 +34,6 @@ public class PageViewWidget extends Region { private double pageWidth; private double pageHeight; - private Label pageIndexLabel; - private Label scaleLabel; - private Label rotationLabel; - private ViewerModel model; private ScrollPane scrollPane; @@ -87,20 +76,9 @@ public PageViewWidget(ViewerModel model, int pageIndex, FloatProperty scale, Flo }); scrollPane.vvalueProperty().addListener((observable, oldValue, newValue) -> { - -// System.out.println("Page " + this.getPageIndex() + " is in the viewport"); scrollPause.playFromStart(); - // todo trigger a page capture - // - page should be doing intersection check - // - any size change would trigger a repaint - // - eventually bring clipped painting - // - try painting to a buffer and then paint that buffer to the screen (from prevoius work - // - try painting to graphics contet too, maybe it's fast/optimized for reactive painting. }); - pageIndexLabel = new Label("Page " + pageIndex); - scaleLabel = new Label(); - rotationLabel = new Label(); createLayout(); setOnMouseClicked(event -> { @@ -125,7 +103,7 @@ public void changed(ObservableValue observable, Bounds oldBoun } public void draw() { - if (canvas == null && isNodeIntersectingViewport(scrollPane, this)) { + if (isNodeIntersectingViewport(scrollPane, this)) { System.out.println("drawing page " + this.pageIndex); try { @@ -139,10 +117,6 @@ protected Void call() throws Exception { long end = System.currentTimeMillis(); System.out.println("Page init time: " + (end - start) + "ms"); Platform.runLater(() -> draw()); -// -// WritableImage image = new WritableImage((int) width.get(), (int) height.get()); -// image.getPixelWriter(). - return null; } }; @@ -151,77 +125,67 @@ protected Void call() throws Exception { return; } - canvas = new Canvas(pageWidth, pageHeight); - - Group root = new Group(); + if (canvas == null) { + canvas = new Canvas(pageWidth, pageHeight); + Group root = new Group(); + root.getChildren().add(canvas); + getChildren().add(root); +// canvas.scaleXProperty().bind(scale); +// canvas.scaleYProperty().bind(scale); + } GraphicsContext gc = canvas.getGraphicsContext2D(); - root.getChildren().add(canvas); - getChildren().add(root); - - // try and get this working with - canvas.scaleXProperty().bind(scale); - canvas.scaleYProperty().bind(scale); - gc.setStroke(Color.RED); - gc.setLineWidth(5); + gc.clearRect(0, 0, width.get(), height.get()); + gc.save(); + // clip testing + gc.setFill(Color.BLUE); + gc.fillRect(0, 0, 50, 50); calculateAndDrawClip(gc); - gc.setFontSmoothingType(FontSmoothingType.LCD); - FXGraphics2D fxg2 = new FXGraphics2D(gc); - fxg2.setRenderingHint(FXHints.KEY_USE_FX_FONT_METRICS, true); - fxg2.setZeroStrokeWidth(0.1); - fxg2.setRenderingHint( - RenderingHints.KEY_FRACTIONALMETRICS, - RenderingHints.VALUE_FRACTIONALMETRICS_ON); - fxg2.setClip(0, 0, (int) width.get(), (int) height.get()); - fxg2.scale(1, -1); - fxg2.translate(0, -height.get()); - - - long start = System.currentTimeMillis(); - page.paintPageContent(fxg2, GraphicsRenderingHints.PRINT, rotation.get(), scale.get(), true, true); - long end = System.currentTimeMillis(); - System.out.println("Page paint time: " + (end - start) + "ms"); - } catch (InterruptedException e) { + // paint page to canvas + // todo: likely fall back on paint directly to image and pain this image as node at the + // correct location. Canvas is a buffer but a lot of work would be need to convert the + // rendering core to full javafx draw ops. Not ruling it out as there are some benefits to + // some of the blending effects that are build into javafx. +// gc.save(); +// Affine prePaintTransform = gc.getTransform(); +// gc.setFontSmoothingType(FontSmoothingType.LCD); +// FXGraphics2D fxg2 = new FXGraphics2D(gc); +// AffineTransform test = fxg2.getTransform(); +// fxg2.setRenderingHint(FXHints.KEY_USE_FX_FONT_METRICS, true); +// fxg2.setZeroStrokeWidth(0.1); +// fxg2.setRenderingHint( +// RenderingHints.KEY_FRACTIONALMETRICS, +// RenderingHints.VALUE_FRACTIONALMETRICS_ON); +// fxg2.setClip(0, 0, (int) width.get(), (int) height.get()); +// fxg2.scale(1, -1); +// fxg2.translate(0, -height.get()); + +// long start = System.currentTimeMillis(); +// page.paintPageContent(fxg2, GraphicsRenderingHints.PRINT, rotation.get(), scale.get(), true, true); +// long end = System.currentTimeMillis(); +// fxg2.transform(test); +// +// System.out.println("Page paint time: " + (end - start) + "ms"); +// gc.restore(); +// fxg2.scale(1, -1); +// gc.transform(prePaintTransform); + } catch (Throwable e) { throw new RuntimeException(e); } } } private void calculateAndDrawClip(GraphicsContext gc) { - Bounds viewportBounds = scrollPane.getViewportBounds(); - Bounds nodeBounds = localToScene(getBoundsInLocal()); - Bounds scrollPaneBounds = scrollPane.localToScene(scrollPane.getBoundsInLocal()); - double x = scrollPaneBounds.getMinX() + viewportBounds.getMinX(); - double y = scrollPaneBounds.getMinY() + viewportBounds.getMinY(); - double width = viewportBounds.getWidth(); - double height = viewportBounds.getHeight(); - gc.strokeRect(x, y, width, height); - } - - private void drawShapes(GraphicsContext gc) { - gc.setFill(Color.GREEN); - gc.setStroke(Color.BLUE); + Rectangle2D rect = intersectionClip(scrollPane, this); + gc.setStroke(Color.RED); gc.setLineWidth(5); - gc.strokeLine(40, 10, 10, 40); - gc.fillOval(10, 60, 30, 30); - gc.strokeOval(60, 60, 30, 30); - gc.fillRoundRect(110, 60, 30, 30, 10, 10); - gc.strokeRoundRect(160, 60, 30, 30, 10, 10); - gc.fillArc(10, 110, 30, 30, 45, 240, ArcType.OPEN); - gc.fillArc(60, 110, 30, 30, 45, 240, ArcType.CHORD); - gc.fillArc(110, 110, 30, 30, 45, 240, ArcType.ROUND); - gc.strokeArc(10, 160, 30, 30, 45, 240, ArcType.OPEN); - gc.strokeArc(60, 160, 30, 30, 45, 240, ArcType.CHORD); - gc.strokeArc(110, 160, 30, 30, 45, 240, ArcType.ROUND); - gc.fillPolygon(new double[]{10, 40, 10, 40}, - new double[]{210, 210, 240, 240}, 4); - gc.strokePolygon(new double[]{60, 90, 60, 90}, - new double[]{210, 210, 240, 240}, 4); - gc.strokePolyline(new double[]{110, 140, 110, 140}, - new double[]{210, 210, 240, 240}, 4); + System.out.println("Drawing clip: " + rect); + gc.strokeRect(rect.getMinX(), rect.getMinY(), rect.getWidth(), rect.getHeight()); + ; } + private boolean isNodeIntersectingViewport(ScrollPane scrollPane, Node node) { Bounds nodeBounds = node.getBoundsInParent(); @@ -262,7 +226,7 @@ private boolean isNodeIntersectingViewport(ScrollPane scrollPane, Node node) { ); } - private Bounds intersectionClip(ScrollPane scrollPane, Node node) { + private Rectangle2D intersectionClip(ScrollPane scrollPane, Node node) { Bounds nodeBounds = node.getBoundsInParent(); @@ -286,21 +250,44 @@ private Bounds intersectionClip(ScrollPane scrollPane, Node node) { double voffset = Math.max(0, contentHeight - viewportHeight) * (vvalue - vmin) / (vmax - vmin); - // todo calculate intersection, so we can define a small canvas to draw to on high zoom levels + Rectangle2D intersection = new Rectangle2D( + Math.max(hoffset, nodeBounds.getMinX()), + Math.max(voffset, nodeBounds.getMinY()), + Math.min(viewportWidth, nodeBounds.getWidth()), + Math.min(viewportHeight, nodeBounds.getHeight()) + ); +// Rectangle2D intersection = findIntersection( +// new Rectangle2D(hoffset, voffset, viewportWidth, viewportHeight), +// new Rectangle2D(nodeBounds.getMinX(), nodeBounds.getMinY(), nodeBounds.getWidth(), nodeBounds.getHeight()) +// ); + Rectangle2D normalizedToNode = new Rectangle2D( + intersection.getMinX() - nodeBounds.getMinX(), + intersection.getMinY() - nodeBounds.getMinY(), + intersection.getWidth(), + intersection.getHeight() + ); + + return normalizedToNode; + } + + // second method to calculate intersection + public static Rectangle2D findIntersection(Rectangle2D rect1, Rectangle2D rect2) { + double x1 = Math.max(rect1.getMinX(), rect2.getMinX()); + double y1 = Math.max(rect1.getMinY(), rect2.getMinY()); + double x2 = Math.min(rect1.getMinX() + rect1.getWidth(), rect2.getMinX() + rect2.getWidth()); + double y2 = Math.min(rect1.getMinY() + rect1.getHeight(), rect2.getMinY() + rect2.getWidth()); + + if (x1 < x2 && y1 < y2) { + return new Rectangle2D(x1, y1, x2 - x1, y2 - y1); + } + // No intersection return null; } private void createLayout() { minWidthProperty().bind(width); minHeightProperty().bind(height); - -// VBox vBox = new VBox(); -// scaleLabel.textProperty().bind(scale.asString()); -// rotationLabel.textProperty().bind(width.asString()); -// vBox.getChildren().addAll(pageIndexLabel, scaleLabel, rotationLabel); -// getChildren().add(vBox); - } public int getPageIndex() { From 7b9e85fc4fa7df7c7c2e10855e97e2f8411bc1e1 Mon Sep 17 00:00:00 2001 From: Patrick Corless Date: Mon, 23 Mar 2026 21:24:58 -0600 Subject: [PATCH 7/7] GH-372 code generation experiments --- qa/viewer-jfx/pom.xml | 50 +-- viewer/viewer-fx/build.gradle | 30 +- viewer/viewer-fx/pom.xml | 8 +- .../fx/ri/ui/common/NavigationCommands.java | 46 +++ .../icepdf/fx/ri/ui/dialogs/AboutDialog.java | 204 ++++++++++ .../ui/dialogs/DocumentPropertiesDialog.java | 322 +++++++++++++++ .../fx/ri/ui/dialogs/PreferencesDialog.java | 376 ++++++++++++++++++ .../icepdf/fx/ri/ui/dialogs/PrintDialog.java | 308 ++++++++++++++ .../icepdf/fx/ri/ui/dialogs/SearchDialog.java | 344 ++++++++++++++++ .../fx/ri/ui/menubar/MenuBarBuilder.java | 338 ++++++++++++++++ .../fx/ri/ui/sidepanel/AnnotationsPanel.java | 268 +++++++++++++ .../fx/ri/ui/sidepanel/AttachmentsPanel.java | 215 ++++++++++ .../fx/ri/ui/sidepanel/LayersPanel.java | 213 ++++++++++ .../fx/ri/ui/sidepanel/OutlinePanel.java | 213 ++++++++++ .../fx/ri/ui/sidepanel/SearchPanel.java | 239 +++++++++++ .../ri/ui/sidepanel/SidePanelContainer.java | 84 ++++ .../fx/ri/ui/sidepanel/SignaturesPanel.java | 248 ++++++++++++ .../fx/ri/ui/sidepanel/ThumbnailsPanel.java | 277 +++++++++++++ .../fx/ri/ui/statusbar/StatusBarBuilder.java | 107 +++++ .../fx/ri/ui/toolbar/ToolBarBuilder.java | 208 ++++++++++ .../org/icepdf/fx/ri/viewer/FxController.java | 16 +- .../org/icepdf/fx/ri/viewer/ViewBuilder.java | 84 ++-- .../org/icepdf/fx/ri/viewer/ViewerModel.java | 67 +++- .../fx/ri/viewer/ViewerStageManager.java | 2 +- .../document/CloseDocumentCommand.java | 41 ++ .../document/DocumentPropertiesCommand.java | 32 ++ .../{ => document}/OpenFileCommand.java | 3 +- .../document/PrintDocumentCommand.java | 49 +++ .../document/SaveDocumentCommand.java | 51 +++ .../commands/document/SearchCommand.java | 37 ++ .../commands/navigation/FirstPageCommand.java | 25 ++ .../commands/navigation/GoToPageCommand.java | 32 ++ .../commands/navigation/LastPageCommand.java | 25 ++ .../commands/navigation/NextPageCommand.java | 25 ++ .../navigation/PreviousPageCommand.java | 25 ++ .../commands/view/ActualSizeCommand.java | 26 ++ .../viewer/commands/view/FitPageCommand.java | 26 ++ .../viewer/commands/view/FitWidthCommand.java | 26 ++ .../commands/view/RotateLeftCommand.java | 27 ++ .../commands/view/RotateRightCommand.java | 27 ++ .../commands/{ => view}/ZoomInCommand.java | 3 +- .../commands/{ => view}/ZoomOutCommand.java | 3 +- .../viewer/commands/window/AboutCommand.java | 24 ++ .../commands/window/CloseWindowCommand.java | 37 ++ .../window/MinimizeWindowCommand.java | 25 ++ .../commands/window/NewWindowCommand.java | 30 ++ .../commands/window/PreferencesCommand.java | 27 ++ .../icepdf/fx/ri/views/PageViewWidget.java | 2 +- 48 files changed, 4806 insertions(+), 89 deletions(-) create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/common/NavigationCommands.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/dialogs/AboutDialog.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/dialogs/DocumentPropertiesDialog.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/dialogs/PreferencesDialog.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/dialogs/PrintDialog.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/dialogs/SearchDialog.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/menubar/MenuBarBuilder.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/AnnotationsPanel.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/AttachmentsPanel.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/LayersPanel.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/OutlinePanel.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/SearchPanel.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/SidePanelContainer.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/SignaturesPanel.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/ThumbnailsPanel.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/statusbar/StatusBarBuilder.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/toolbar/ToolBarBuilder.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/CloseDocumentCommand.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/DocumentPropertiesCommand.java rename viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/{ => document}/OpenFileCommand.java (94%) create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/PrintDocumentCommand.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/SaveDocumentCommand.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/SearchCommand.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/navigation/FirstPageCommand.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/navigation/GoToPageCommand.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/navigation/LastPageCommand.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/navigation/NextPageCommand.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/navigation/PreviousPageCommand.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/ActualSizeCommand.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/FitPageCommand.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/FitWidthCommand.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/RotateLeftCommand.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/RotateRightCommand.java rename viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/{ => view}/ZoomInCommand.java (87%) rename viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/{ => view}/ZoomOutCommand.java (87%) create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/window/AboutCommand.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/window/CloseWindowCommand.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/window/MinimizeWindowCommand.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/window/NewWindowCommand.java create mode 100644 viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/window/PreferencesCommand.java diff --git a/qa/viewer-jfx/pom.xml b/qa/viewer-jfx/pom.xml index 33d512c12..592a30891 100644 --- a/qa/viewer-jfx/pom.xml +++ b/qa/viewer-jfx/pom.xml @@ -38,33 +38,33 @@ 2.14.0 - - org.bouncycastle - bcprov-jdk18on - ${bouncy.version} - + + + + + - - org.bouncycastle - bcpkix-jdk18on - ${bouncy.version} - + + + + + - - com.twelvemonkeys.imageio - imageio-tiff - ${twelve-monkey.version} - - - org.apache.pdfbox - jbig2-imageio - ${jbig2.version} - - - com.github.jai-imageio - jai-imageio-jpeg2000 - ${jai-imageio.version} - + + + + + + + + + + + + + + + diff --git a/viewer/viewer-fx/build.gradle b/viewer/viewer-fx/build.gradle index e56375d9b..2564325d2 100644 --- a/viewer/viewer-fx/build.gradle +++ b/viewer/viewer-fx/build.gradle @@ -3,24 +3,29 @@ plugins { id 'org.openjfx.javafxplugin' version '0.1.0' } +// JavaFX 21 requires Java 17+, override parent toolchain for this module +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + repositories { mavenCentral() } -description 'ICEpdf FX viewer reference implementation project' +description = 'ICEpdf FX viewer reference implementation project' -mainClassName = "org.icepdf.fx.ri.viewer.Launcher" -applicationDefaultJvmArgs = ["-Xms64m", "-Xmx1024m"] +application { + mainClass = "org.icepdf.fx.ri.viewer.Launcher" + applicationDefaultJvmArgs = ["-Xms64m", "-Xmx1024m"] +} def sectionName = 'org/icepdf/fx/ri/viewer' def baseJarName = 'icepdf' def baseAppendixName = 'viewer-fx' def commandlineArgs = '-loadfile "/home/pcorless/dev/pdf-qa/PDF32000_2008.pdf"' -repositories { - mavenCentral() - jcenter() -} javafx { version = "21.0.5" @@ -46,9 +51,9 @@ publishing { viewerJar(MavenPublication) { from components.java afterEvaluate { - groupId 'org.icepdf.os' - artifactId 'icepdf-viewer-fx' - version "${VERSION + (RELEASE_TYPE?.trim() ? '-' + RELEASE_TYPE : '')}" + groupId = 'org.icepdf.os' + artifactId = 'icepdf-viewer-fx' + version = "${VERSION + (RELEASE_TYPE?.trim() ? '-' + RELEASE_TYPE : '')}" pom.withXml { asNode().appendNode('description', 'ICEpdf JX Viewer reference implementation.') asNode().appendNode('url', 'https://github.com/pcorless/icepdf') @@ -120,7 +125,4 @@ task javadocJar(type: Jar, dependsOn: 'javadoc') { archiveClassifier.set("javadoc") } -artifacts { - archives sourcesJar - archives javadocJar -} +// Archives are now automatically included via publishing plugin diff --git a/viewer/viewer-fx/pom.xml b/viewer/viewer-fx/pom.xml index 31ff4f4e0..0f42a4aac 100644 --- a/viewer/viewer-fx/pom.xml +++ b/viewer/viewer-fx/pom.xml @@ -3,7 +3,7 @@ com.github.pcorless.icepdf viewer - 7.3.0-SNAPSHOT + 7.4.0-SNAPSHOT 4.0.0 icepdf-viewer-fx @@ -19,11 +19,6 @@ javafx-controls 21 - - org.openjfx - javafx-graphics - 21 - org.openjfx javafx-swing @@ -37,6 +32,7 @@ com.github.pcorless.icepdf icepdf-core + ${project.version} org.junit.jupiter diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/common/NavigationCommands.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/common/NavigationCommands.java new file mode 100644 index 000000000..96ef88a46 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/common/NavigationCommands.java @@ -0,0 +1,46 @@ +package org.icepdf.fx.ri.ui.common; + +import org.icepdf.fx.ri.viewer.ViewerModel; + +/** + * Helper class for common navigation commands. + * Provides static methods for page navigation operations. + */ +public class NavigationCommands { + + public static void firstPage(ViewerModel model) { + if (model.document.get() != null && model.totalPages.get() > 0) { + model.currentPage.set(1); + model.statusMessage.set("First page"); + } + } + + public static void previousPage(ViewerModel model) { + if (model.document.get() != null && model.currentPage.get() > 1) { + model.currentPage.set(model.currentPage.get() - 1); + model.statusMessage.set("Page " + model.currentPage.get()); + } + } + + public static void nextPage(ViewerModel model) { + if (model.document.get() != null && model.currentPage.get() < model.totalPages.get()) { + model.currentPage.set(model.currentPage.get() + 1); + model.statusMessage.set("Page " + model.currentPage.get()); + } + } + + public static void lastPage(ViewerModel model) { + if (model.document.get() != null && model.totalPages.get() > 0) { + model.currentPage.set(model.totalPages.get()); + model.statusMessage.set("Last page"); + } + } + + public static void goToPage(ViewerModel model, int pageNumber) { + if (model.document.get() != null && pageNumber >= 1 && pageNumber <= model.totalPages.get()) { + model.currentPage.set(pageNumber); + model.statusMessage.set("Page " + pageNumber); + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/dialogs/AboutDialog.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/dialogs/AboutDialog.java new file mode 100644 index 000000000..0a3102efa --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/dialogs/AboutDialog.java @@ -0,0 +1,204 @@ +package org.icepdf.fx.ri.ui.dialogs; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.stage.Window; + +/** + * About dialog displaying application information, version, and license. + */ +public class AboutDialog extends Dialog { + + private static final String VERSION = "7.3.0"; // TODO: Load from build properties + private static final String BUILD_DATE = "2026-03-21"; // TODO: Load from build properties + + public AboutDialog(Window owner) { + initOwner(owner); + setTitle("About ICEpdf Viewer"); + setResizable(false); + + // Create dialog content + TabPane tabPane = new TabPane(); + tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); + tabPane.setPrefSize(500, 400); + + tabPane.getTabs().addAll( + createAboutTab(), + createLicenseTab(), + createSystemInfoTab() + ); + + getDialogPane().setContent(tabPane); + getDialogPane().getButtonTypes().add(ButtonType.CLOSE); + } + + /** + * Creates the About tab with version and credits. + */ + private Tab createAboutTab() { + Tab tab = new Tab("About"); + + VBox vbox = new VBox(15); + vbox.setPadding(new Insets(20)); + vbox.setAlignment(Pos.CENTER); + + // Application name + Label titleLabel = new Label("ICEpdf Viewer"); + titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24)); + + // Version + Label versionLabel = new Label("Version " + VERSION); + versionLabel.setFont(Font.font("System", 14)); + + // Build date + Label buildLabel = new Label("Build Date: " + BUILD_DATE); + buildLabel.setFont(Font.font("System", 12)); + + // Description + TextArea descriptionArea = new TextArea( + "ICEpdf is an open source PDF engine for viewing, printing, and manipulating PDF documents.\n\n" + + "This JavaFX-based viewer provides a modern interface for working with PDF files." + ); + descriptionArea.setWrapText(true); + descriptionArea.setEditable(false); + descriptionArea.setPrefRowCount(4); + descriptionArea.setStyle("-fx-background-color: transparent;"); + + // Copyright + Label copyrightLabel = new Label("© 2001-2026 ICEsoft Technologies Canada Corp."); + copyrightLabel.setFont(Font.font("System", 10)); + + // Website link + Hyperlink websiteLink = new Hyperlink("https://www.icesoft.com/java/projects/ICEpdf/overview.jsf"); + websiteLink.setOnAction(e -> { + getHostServices().showDocument(websiteLink.getText()); + }); + + vbox.getChildren().addAll( + titleLabel, + versionLabel, + buildLabel, + new Separator(), + descriptionArea, + new Separator(), + copyrightLabel, + websiteLink + ); + + tab.setContent(vbox); + return tab; + } + + /** + * Creates the License tab. + */ + private Tab createLicenseTab() { + Tab tab = new Tab("License"); + + VBox vbox = new VBox(10); + vbox.setPadding(new Insets(20)); + + Label titleLabel = new Label("Apache License 2.0"); + titleLabel.setFont(Font.font("System", FontWeight.BOLD, 14)); + + TextArea licenseArea = new TextArea( + "Licensed under the Apache License, Version 2.0 (the \"License\");\n" + + "you may not use this file except in compliance with the License.\n" + + "You may obtain a copy of the License at\n\n" + + " http://www.apache.org/licenses/LICENSE-2.0\n\n" + + "Unless required by applicable law or agreed to in writing, software\n" + + "distributed under the License is distributed on an \"AS IS\" BASIS,\n" + + "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" + + "See the License for the specific language governing permissions and\n" + + "limitations under the License.\n\n" + + "---\n\n" + + "ICEpdf is an open source project and includes contributions from various developers.\n" + + "For more information, visit the project website or repository." + ); + licenseArea.setWrapText(true); + licenseArea.setEditable(false); + licenseArea.setPrefRowCount(15); + + vbox.getChildren().addAll(titleLabel, licenseArea); + tab.setContent(vbox); + + return tab; + } + + /** + * Creates the System Info tab. + */ + private Tab createSystemInfoTab() { + Tab tab = new Tab("System Info"); + + VBox vbox = new VBox(10); + vbox.setPadding(new Insets(20)); + + TextArea infoArea = new TextArea(); + infoArea.setWrapText(true); + infoArea.setEditable(false); + infoArea.setPrefRowCount(15); + + // Gather system information + StringBuilder info = new StringBuilder(); + info.append("Java Version: ").append(System.getProperty("java.version")).append("\n"); + info.append("Java Vendor: ").append(System.getProperty("java.vendor")).append("\n"); + info.append("Java Home: ").append(System.getProperty("java.home")).append("\n"); + info.append("\n"); + info.append("JavaFX Version: ").append(System.getProperty("javafx.version", "Unknown")).append("\n"); + info.append("JavaFX Runtime: ").append(System.getProperty("javafx.runtime.version", "Unknown")).append("\n"); + info.append("\n"); + info.append("Operating System: ").append(System.getProperty("os.name")).append("\n"); + info.append("OS Version: ").append(System.getProperty("os.version")).append("\n"); + info.append("OS Architecture: ").append(System.getProperty("os.arch")).append("\n"); + info.append("\n"); + info.append("User Name: ").append(System.getProperty("user.name")).append("\n"); + info.append("User Home: ").append(System.getProperty("user.home")).append("\n"); + info.append("User Directory: ").append(System.getProperty("user.dir")).append("\n"); + info.append("\n"); + + // Memory information + Runtime runtime = Runtime.getRuntime(); + long maxMemory = runtime.maxMemory() / (1024 * 1024); + long totalMemory = runtime.totalMemory() / (1024 * 1024); + long freeMemory = runtime.freeMemory() / (1024 * 1024); + long usedMemory = totalMemory - freeMemory; + + info.append("Max Memory: ").append(maxMemory).append(" MB\n"); + info.append("Total Memory: ").append(totalMemory).append(" MB\n"); + info.append("Used Memory: ").append(usedMemory).append(" MB\n"); + info.append("Free Memory: ").append(freeMemory).append(" MB\n"); + info.append("\n"); + info.append("Available Processors: ").append(runtime.availableProcessors()).append("\n"); + + infoArea.setText(info.toString()); + + Button copyButton = new Button("Copy to Clipboard"); + copyButton.setOnAction(e -> { + javafx.scene.input.ClipboardContent content = new javafx.scene.input.ClipboardContent(); + content.putString(infoArea.getText()); + javafx.scene.input.Clipboard.getSystemClipboard().setContent(content); + }); + + vbox.getChildren().addAll(infoArea, copyButton); + tab.setContent(vbox); + + return tab; + } + + /** + * Helper to get host services (for opening links). + * This is a workaround since Dialog doesn't have direct access to HostServices. + */ + private javafx.application.HostServices getHostServices() { + // JavaFX Dialog doesn't provide direct access to HostServices + // In a real implementation, pass HostServices through the constructor + // For now, just return null and handle gracefully + return null; + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/dialogs/DocumentPropertiesDialog.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/dialogs/DocumentPropertiesDialog.java new file mode 100644 index 000000000..57fd10232 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/dialogs/DocumentPropertiesDialog.java @@ -0,0 +1,322 @@ +package org.icepdf.fx.ri.ui.dialogs; + +import javafx.geometry.Insets; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.Window; +import org.icepdf.core.pobjects.Document; +import org.icepdf.core.pobjects.PInfo; +import org.icepdf.core.pobjects.security.Permissions; +import org.icepdf.core.pobjects.security.SecurityManager; +import org.icepdf.fx.ri.viewer.ViewerModel; + +/** + * Dialog displaying document properties including metadata, security, and fonts. + */ +public class DocumentPropertiesDialog extends Dialog { + + private final ViewerModel model; + private TabPane tabPane; + + public DocumentPropertiesDialog(ViewerModel model, Window owner) { + this.model = model; + + initOwner(owner); + setTitle("Document Properties"); + setResizable(true); + + // Create dialog content + tabPane = new TabPane(); + tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); + tabPane.setPrefSize(600, 400); + + // Add tabs + tabPane.getTabs().addAll( + createGeneralTab(), + createSecurityTab(), + createFontsTab() + ); + + getDialogPane().setContent(tabPane); + getDialogPane().getButtonTypes().add(ButtonType.CLOSE); + } + + /** + * Creates the General tab with document metadata. + */ + private Tab createGeneralTab() { + Tab tab = new Tab("General"); + + GridPane grid = new GridPane(); + grid.setHgap(10); + grid.setVgap(10); + grid.setPadding(new Insets(20)); + + Document document = model.document.get(); + if (document != null) { + PInfo info = document.getInfo(); + + int row = 0; + + // File name + addProperty(grid, row++, "File:", model.filePath.get() != null ? + model.filePath.get() : "Unknown"); + + // File size + long sizeBytes = model.documentSizeBytes.get(); + String sizeStr = formatFileSize(sizeBytes); + addProperty(grid, row++, "File Size:", sizeStr); + + // Title + String title = info.getTitle(); + addProperty(grid, row++, "Title:", title != null ? title : "(Not set)"); + + // Author + String author = info.getAuthor(); + addProperty(grid, row++, "Author:", author != null ? author : "(Not set)"); + + // Subject + String subject = info.getSubject(); + addProperty(grid, row++, "Subject:", subject != null ? subject : "(Not set)"); + + // Keywords + String keywords = info.getKeywords(); + addProperty(grid, row++, "Keywords:", keywords != null ? keywords : "(Not set)"); + + // Creator + String creator = info.getCreator(); + addProperty(grid, row++, "Creator:", creator != null ? creator : "(Not set)"); + + // Producer + String producer = info.getProducer(); + addProperty(grid, row++, "Producer:", producer != null ? producer : "(Not set)"); + + // Creation Date + String creationDate = info.getCreationDate() != null ? + info.getCreationDate().toString() : "(Not set)"; + addProperty(grid, row++, "Creation Date:", creationDate); + + // Modification Date + String modDate = info.getModDate() != null ? + info.getModDate().toString() : "(Not set)"; + addProperty(grid, row++, "Modification Date:", modDate); + + // Page count + addProperty(grid, row++, "Page Count:", String.valueOf(model.totalPages.get())); + } + + ScrollPane scrollPane = new ScrollPane(grid); + scrollPane.setFitToWidth(true); + tab.setContent(scrollPane); + + return tab; + } + + /** + * Creates the Security tab with permissions and encryption info. + */ + private Tab createSecurityTab() { + Tab tab = new Tab("Security"); + + GridPane grid = new GridPane(); + grid.setHgap(10); + grid.setVgap(10); + grid.setPadding(new Insets(20)); + + Document document = model.document.get(); + if (document != null) { + int row = 0; + + // Encryption - check if SecurityManager exists + SecurityManager securityManager = document.getSecurityManager(); + boolean isEncrypted = (securityManager != null); + addProperty(grid, row++, "Encrypted:", isEncrypted ? "Yes" : "No"); + + if (isEncrypted) { + // Security handler + String securityHandler = securityManager.getClass().getSimpleName(); + addProperty(grid, row++, "Security Handler:", securityHandler); + } + + // Permissions + if (securityManager != null && securityManager.getPermissions() != null) { + Permissions permissions = securityManager.getPermissions(); + addProperty(grid, row++, "Printing:", + permissions.getPermissions(Permissions.PRINT_DOCUMENT) ? "Allowed" : "Not Allowed"); + addProperty(grid, row++, "Content Copying:", + permissions.getPermissions(Permissions.CONTENT_EXTRACTION) ? "Allowed" : "Not Allowed"); + addProperty(grid, row++, "Document Assembly:", + permissions.getPermissions(Permissions.DOCUMENT_ASSEMBLY) ? "Allowed" : "Not Allowed"); + addProperty(grid, row++, "Form Filling:", + permissions.getPermissions(Permissions.FORM_FIELD_FILL_SIGNING) ? "Allowed" : "Not Allowed"); + } else { + addProperty(grid, row++, "Printing:", "Allowed"); + addProperty(grid, row++, "Content Copying:", "Allowed"); + addProperty(grid, row++, "Document Assembly:", "Allowed"); + addProperty(grid, row++, "Form Filling:", "Allowed"); + } + } + + ScrollPane scrollPane = new ScrollPane(grid); + scrollPane.setFitToWidth(true); + tab.setContent(scrollPane); + + return tab; + } + + /** + * Creates the Fonts tab with embedded fonts list. + */ + private Tab createFontsTab() { + Tab tab = new Tab("Fonts"); + + VBox vbox = new VBox(10); + vbox.setPadding(new Insets(20)); + + Label label = new Label("Embedded Fonts:"); + + TableView table = new TableView<>(); + table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + + // Font Name column + TableColumn nameCol = new TableColumn<>("Font Name"); + nameCol.setCellValueFactory(cellData -> cellData.getValue().nameProperty()); + nameCol.setPrefWidth(200); + + // Font Type column + TableColumn typeCol = new TableColumn<>("Type"); + typeCol.setCellValueFactory(cellData -> cellData.getValue().typeProperty()); + typeCol.setPrefWidth(100); + + // Encoding column + TableColumn encodingCol = new TableColumn<>("Encoding"); + encodingCol.setCellValueFactory(cellData -> cellData.getValue().encodingProperty()); + encodingCol.setPrefWidth(150); + + // Embedded column + TableColumn embeddedCol = new TableColumn<>("Embedded"); + embeddedCol.setCellValueFactory(cellData -> cellData.getValue().embeddedProperty()); + embeddedCol.setPrefWidth(100); + + table.getColumns().addAll(nameCol, typeCol, encodingCol, embeddedCol); + + // Populate fonts (placeholder - would need to extract from document) + Document document = model.document.get(); + if (document != null) { + // TODO: Extract font information from document + // This would require iterating through pages and extracting font resources + table.setPlaceholder(new Label("Font information extraction not yet implemented")); + } + + Button copyButton = new Button("Copy to Clipboard"); + copyButton.setOnAction(e -> copyFontsToClipboard(table)); + + vbox.getChildren().addAll(label, table, copyButton); + tab.setContent(vbox); + + return tab; + } + + /** + * Adds a property row to the grid. + */ + private void addProperty(GridPane grid, int row, String label, String value) { + Label labelNode = new Label(label); + labelNode.setStyle("-fx-font-weight: bold;"); + + TextField valueField = new TextField(value); + valueField.setEditable(false); + valueField.setStyle("-fx-background-color: transparent;"); + + grid.add(labelNode, 0, row); + grid.add(valueField, 1, row); + } + + /** + * Formats file size in human-readable format. + */ + private String formatFileSize(long bytes) { + if (bytes < 1024) { + return bytes + " bytes"; + } else if (bytes < 1024 * 1024) { + return String.format("%.2f KB", bytes / 1024.0); + } else if (bytes < 1024 * 1024 * 1024) { + return String.format("%.2f MB", bytes / (1024.0 * 1024.0)); + } else { + return String.format("%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0)); + } + } + + /** + * Copies font information to clipboard. + */ + private void copyFontsToClipboard(TableView table) { + StringBuilder sb = new StringBuilder(); + sb.append("Font Name\tType\tEncoding\tEmbedded\n"); + + for (FontInfo font : table.getItems()) { + sb.append(font.getName()).append("\t"); + sb.append(font.getType()).append("\t"); + sb.append(font.getEncoding()).append("\t"); + sb.append(font.getEmbedded()).append("\n"); + } + + javafx.scene.input.ClipboardContent content = new javafx.scene.input.ClipboardContent(); + content.putString(sb.toString()); + javafx.scene.input.Clipboard.getSystemClipboard().setContent(content); + } + + /** + * Helper class to represent font information. + */ + public static class FontInfo { + private final javafx.beans.property.SimpleStringProperty name; + private final javafx.beans.property.SimpleStringProperty type; + private final javafx.beans.property.SimpleStringProperty encoding; + private final javafx.beans.property.SimpleStringProperty embedded; + + public FontInfo(String name, String type, String encoding, String embedded) { + this.name = new javafx.beans.property.SimpleStringProperty(name); + this.type = new javafx.beans.property.SimpleStringProperty(type); + this.encoding = new javafx.beans.property.SimpleStringProperty(encoding); + this.embedded = new javafx.beans.property.SimpleStringProperty(embedded); + } + + public String getName() { + return name.get(); + } + + public javafx.beans.property.SimpleStringProperty nameProperty() { + return name; + } + + public String getType() { + return type.get(); + } + + public javafx.beans.property.SimpleStringProperty typeProperty() { + return type; + } + + public String getEncoding() { + return encoding.get(); + } + + public javafx.beans.property.SimpleStringProperty encodingProperty() { + return encoding; + } + + public String getEmbedded() { + return embedded.get(); + } + + public javafx.beans.property.SimpleStringProperty embeddedProperty() { + return embedded; + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/dialogs/PreferencesDialog.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/dialogs/PreferencesDialog.java new file mode 100644 index 000000000..e64c3fea7 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/dialogs/PreferencesDialog.java @@ -0,0 +1,376 @@ +package org.icepdf.fx.ri.ui.dialogs; + +import javafx.geometry.Insets; +import javafx.scene.control.*; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; +import javafx.stage.Window; +import org.icepdf.fx.ri.viewer.ViewerModel; + +/** + * Dialog for application preferences and settings. + */ +public class PreferencesDialog extends Dialog { + + private final ViewerModel model; + private TabPane tabPane; + + // General preferences + private CheckBox useSingleWindowCheckBox; + private CheckBox showToolbarCheckBox; + private CheckBox showStatusBarCheckBox; + private CheckBox showLeftPanelCheckBox; + + // Display preferences + private Slider zoomIncrementSlider; + private ComboBox defaultViewModeCombo; + private ComboBox defaultFitModeCombo; + + // Rendering preferences + private CheckBox antiAliasingCheckBox; + private CheckBox textAntiAliasingCheckBox; + private ComboBox renderQualityCombo; + + // Memory preferences + private Slider maxMemorySlider; + private CheckBox enableCachingCheckBox; + + public PreferencesDialog(ViewerModel model, Window owner) { + this.model = model; + + initOwner(owner); + setTitle("Preferences"); + setResizable(true); + + // Create dialog content + tabPane = new TabPane(); + tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); + tabPane.setPrefSize(600, 500); + + // Add tabs + tabPane.getTabs().addAll( + createGeneralTab(), + createDisplayTab(), + createRenderingTab(), + createMemoryTab() + ); + + getDialogPane().setContent(tabPane); + getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL, ButtonType.APPLY); + + // Handle Apply button + Button applyButton = (Button) getDialogPane().lookupButton(ButtonType.APPLY); + applyButton.addEventFilter(javafx.event.ActionEvent.ACTION, event -> { + applyPreferences(); + event.consume(); // Don't close dialog + }); + + // Handle OK button + setResultConverter(buttonType -> { + if (buttonType == ButtonType.OK) { + applyPreferences(); + } + return buttonType; + }); + } + + /** + * Creates the General preferences tab. + */ + private Tab createGeneralTab() { + Tab tab = new Tab("General"); + + VBox vbox = new VBox(15); + vbox.setPadding(new Insets(20)); + + // Window behavior + Label windowLabel = new Label("Window Behavior"); + windowLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 14px;"); + + useSingleWindowCheckBox = new CheckBox("Use single window for all documents"); + useSingleWindowCheckBox.setSelected(model.useSingleViewerStage.get()); + + // UI visibility + Label uiLabel = new Label("User Interface"); + uiLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 14px;"); + + showToolbarCheckBox = new CheckBox("Show toolbar"); + showToolbarCheckBox.setSelected(model.toolBarVisible.get()); + + showStatusBarCheckBox = new CheckBox("Show status bar"); + showStatusBarCheckBox.setSelected(model.statusBarVisible.get()); + + showLeftPanelCheckBox = new CheckBox("Show left panel"); + showLeftPanelCheckBox.setSelected(model.leftPanelVisible.get()); + + vbox.getChildren().addAll( + windowLabel, + useSingleWindowCheckBox, + new Separator(), + uiLabel, + showToolbarCheckBox, + showStatusBarCheckBox, + showLeftPanelCheckBox + ); + + tab.setContent(vbox); + return tab; + } + + /** + * Creates the Display preferences tab. + */ + private Tab createDisplayTab() { + Tab tab = new Tab("Display"); + + GridPane grid = new GridPane(); + grid.setHgap(10); + grid.setVgap(15); + grid.setPadding(new Insets(20)); + + int row = 0; + + // Zoom increment + Label zoomLabel = new Label("Zoom Increment:"); + zoomIncrementSlider = new Slider(0.05, 0.5, model.zoomFactorIncrement.get()); + zoomIncrementSlider.setShowTickLabels(true); + zoomIncrementSlider.setShowTickMarks(true); + zoomIncrementSlider.setMajorTickUnit(0.1); + zoomIncrementSlider.setMinorTickCount(1); + zoomIncrementSlider.setBlockIncrement(0.05); + + Label zoomValueLabel = new Label(String.format("%.0f%%", zoomIncrementSlider.getValue() * 100)); + zoomIncrementSlider.valueProperty().addListener((obs, oldVal, newVal) -> { + zoomValueLabel.setText(String.format("%.0f%%", newVal.doubleValue() * 100)); + }); + + grid.add(zoomLabel, 0, row); + grid.add(zoomIncrementSlider, 1, row); + grid.add(zoomValueLabel, 2, row); + row++; + + // Default view mode + Label viewModeLabel = new Label("Default View Mode:"); + defaultViewModeCombo = new ComboBox<>(); + defaultViewModeCombo.getItems().addAll( + "Single Page", + "Continuous", + "Facing Pages", + "Continuous Facing" + ); + defaultViewModeCombo.setValue(getViewModeString(model.viewMode.get())); + + grid.add(viewModeLabel, 0, row); + grid.add(defaultViewModeCombo, 1, row, 2, 1); + row++; + + // Default fit mode + Label fitModeLabel = new Label("Default Fit Mode:"); + defaultFitModeCombo = new ComboBox<>(); + defaultFitModeCombo.getItems().addAll( + "None", + "Fit Width", + "Fit Height", + "Fit Page", + "Actual Size" + ); + defaultFitModeCombo.setValue(getFitModeString(model.fitMode.get())); + + grid.add(fitModeLabel, 0, row); + grid.add(defaultFitModeCombo, 1, row, 2, 1); + + tab.setContent(grid); + return tab; + } + + /** + * Creates the Rendering preferences tab. + */ + private Tab createRenderingTab() { + Tab tab = new Tab("Rendering"); + + VBox vbox = new VBox(15); + vbox.setPadding(new Insets(20)); + + // Anti-aliasing + Label aaLabel = new Label("Anti-aliasing"); + aaLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 14px;"); + + antiAliasingCheckBox = new CheckBox("Enable graphics anti-aliasing"); + antiAliasingCheckBox.setSelected(true); + + textAntiAliasingCheckBox = new CheckBox("Enable text anti-aliasing"); + textAntiAliasingCheckBox.setSelected(true); + + // Render quality + Label qualityLabel = new Label("Render Quality"); + qualityLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 14px;"); + + renderQualityCombo = new ComboBox<>(); + renderQualityCombo.getItems().addAll("Low", "Medium", "High"); + renderQualityCombo.setValue("Medium"); + + vbox.getChildren().addAll( + aaLabel, + antiAliasingCheckBox, + textAntiAliasingCheckBox, + new Separator(), + qualityLabel, + renderQualityCombo + ); + + tab.setContent(vbox); + return tab; + } + + /** + * Creates the Memory preferences tab. + */ + private Tab createMemoryTab() { + Tab tab = new Tab("Memory"); + + GridPane grid = new GridPane(); + grid.setHgap(10); + grid.setVgap(15); + grid.setPadding(new Insets(20)); + + int row = 0; + + // Max memory + Label memoryLabel = new Label("Maximum Memory Usage:"); + maxMemorySlider = new Slider(128, 2048, 512); + maxMemorySlider.setShowTickLabels(true); + maxMemorySlider.setShowTickMarks(true); + maxMemorySlider.setMajorTickUnit(256); + maxMemorySlider.setMinorTickCount(4); + maxMemorySlider.setBlockIncrement(128); + + Label memoryValueLabel = new Label(String.format("%.0f MB", maxMemorySlider.getValue())); + maxMemorySlider.valueProperty().addListener((obs, oldVal, newVal) -> { + memoryValueLabel.setText(String.format("%.0f MB", newVal.doubleValue())); + }); + + grid.add(memoryLabel, 0, row); + grid.add(maxMemorySlider, 1, row); + grid.add(memoryValueLabel, 2, row); + row++; + + // Caching + enableCachingCheckBox = new CheckBox("Enable page caching"); + enableCachingCheckBox.setSelected(true); + + grid.add(enableCachingCheckBox, 0, row, 3, 1); + row++; + + // Current memory info + Label infoLabel = new Label("Current Memory Usage:"); + infoLabel.setStyle("-fx-font-weight: bold;"); + + Runtime runtime = Runtime.getRuntime(); + long usedMemory = (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024); + long maxMemory = runtime.maxMemory() / (1024 * 1024); + + Label memoryInfo = new Label(String.format("%d MB / %d MB", usedMemory, maxMemory)); + + grid.add(infoLabel, 0, row); + grid.add(memoryInfo, 1, row, 2, 1); + + tab.setContent(grid); + return tab; + } + + /** + * Applies the preferences to the model. + */ + private void applyPreferences() { + // General preferences + model.useSingleViewerStage.set(useSingleWindowCheckBox.isSelected()); + model.toolBarVisible.set(showToolbarCheckBox.isSelected()); + model.statusBarVisible.set(showStatusBarCheckBox.isSelected()); + model.leftPanelVisible.set(showLeftPanelCheckBox.isSelected()); + + // Display preferences + model.zoomFactorIncrement.set((float) zoomIncrementSlider.getValue()); + model.viewMode.set(parseViewMode(defaultViewModeCombo.getValue())); + model.fitMode.set(parseFitMode(defaultFitModeCombo.getValue())); + + // TODO: Save preferences to file/properties + } + + /** + * Converts ViewMode enum to string. + */ + private String getViewModeString(ViewerModel.ViewMode mode) { + switch (mode) { + case SINGLE_PAGE: + return "Single Page"; + case CONTINUOUS: + return "Continuous"; + case FACING_PAGES: + return "Facing Pages"; + case CONTINUOUS_FACING: + return "Continuous Facing"; + default: + return "Single Page"; + } + } + + /** + * Parses ViewMode from string. + */ + private ViewerModel.ViewMode parseViewMode(String mode) { + switch (mode) { + case "Single Page": + return ViewerModel.ViewMode.SINGLE_PAGE; + case "Continuous": + return ViewerModel.ViewMode.CONTINUOUS; + case "Facing Pages": + return ViewerModel.ViewMode.FACING_PAGES; + case "Continuous Facing": + return ViewerModel.ViewMode.CONTINUOUS_FACING; + default: + return ViewerModel.ViewMode.SINGLE_PAGE; + } + } + + /** + * Converts FitMode enum to string. + */ + private String getFitModeString(ViewerModel.FitMode mode) { + switch (mode) { + case NONE: + return "None"; + case FIT_WIDTH: + return "Fit Width"; + case FIT_HEIGHT: + return "Fit Height"; + case FIT_PAGE: + return "Fit Page"; + case ACTUAL_SIZE: + return "Actual Size"; + default: + return "None"; + } + } + + /** + * Parses FitMode from string. + */ + private ViewerModel.FitMode parseFitMode(String mode) { + switch (mode) { + case "None": + return ViewerModel.FitMode.NONE; + case "Fit Width": + return ViewerModel.FitMode.FIT_WIDTH; + case "Fit Height": + return ViewerModel.FitMode.FIT_HEIGHT; + case "Fit Page": + return ViewerModel.FitMode.FIT_PAGE; + case "Actual Size": + return ViewerModel.FitMode.ACTUAL_SIZE; + default: + return ViewerModel.FitMode.NONE; + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/dialogs/PrintDialog.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/dialogs/PrintDialog.java new file mode 100644 index 000000000..860f25949 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/dialogs/PrintDialog.java @@ -0,0 +1,308 @@ +package org.icepdf.fx.ri.ui.dialogs; + +import javafx.geometry.Insets; +import javafx.print.*; +import javafx.scene.control.*; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; +import javafx.stage.Window; +import org.icepdf.fx.ri.viewer.ViewerModel; + +/** + * Print dialog for configuring print settings. + */ +public class PrintDialog extends Dialog { + + private final ViewerModel model; + private PrinterJob printerJob; + + // Controls + private ComboBox printerCombo; + private RadioButton allPagesRadio; + private RadioButton currentPageRadio; + private RadioButton pageRangeRadio; + private TextField pageRangeField; + private Spinner copiesSpinner; + private ComboBox pageScalingCombo; + private CheckBox fitToPageCheckBox; + private CheckBox centerOnPageCheckBox; + private ComboBox orientationCombo; + + public PrintDialog(ViewerModel model, Window owner) { + this.model = model; + + initOwner(owner); + setTitle("Print"); + setResizable(true); + + // Create printer job + printerJob = PrinterJob.createPrinterJob(); + if (printerJob == null) { + Alert alert = new Alert(Alert.AlertType.ERROR, + "Unable to create printer job. No printers available.", + ButtonType.OK); + alert.showAndWait(); + return; + } + + // Create dialog content + VBox content = new VBox(15); + content.setPadding(new Insets(20)); + content.setPrefWidth(500); + + content.getChildren().addAll( + createPrinterSection(), + new Separator(), + createPageRangeSection(), + new Separator(), + createOptionsSection() + ); + + getDialogPane().setContent(content); + getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + + // Set result converter + setResultConverter(buttonType -> { + if (buttonType == ButtonType.OK) { + configurePrinterJob(); + return printerJob; + } + return null; + }); + } + + /** + * Creates the printer selection section. + */ + private VBox createPrinterSection() { + VBox section = new VBox(10); + + Label titleLabel = new Label("Printer"); + titleLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 14px;"); + + GridPane grid = new GridPane(); + grid.setHgap(10); + grid.setVgap(10); + + // Printer selection + Label printerLabel = new Label("Name:"); + printerCombo = new ComboBox<>(); + printerCombo.getItems().addAll(Printer.getAllPrinters()); + printerCombo.setValue(Printer.getDefaultPrinter()); + printerCombo.setPrefWidth(300); + + // Printer info + Label statusLabel = new Label("Status:"); + Label statusValue = new Label("Ready"); + + grid.add(printerLabel, 0, 0); + grid.add(printerCombo, 1, 0); + grid.add(statusLabel, 0, 1); + grid.add(statusValue, 1, 1); + + // Properties button + Button propertiesButton = new Button("Properties..."); + propertiesButton.setOnAction(e -> showPrinterProperties()); + + section.getChildren().addAll(titleLabel, grid, propertiesButton); + return section; + } + + /** + * Creates the page range section. + */ + private VBox createPageRangeSection() { + VBox section = new VBox(10); + + Label titleLabel = new Label("Page Range"); + titleLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 14px;"); + + ToggleGroup rangeGroup = new ToggleGroup(); + + allPagesRadio = new RadioButton("All pages"); + allPagesRadio.setToggleGroup(rangeGroup); + allPagesRadio.setSelected(true); + + currentPageRadio = new RadioButton("Current page"); + currentPageRadio.setToggleGroup(rangeGroup); + + pageRangeRadio = new RadioButton("Pages:"); + pageRangeRadio.setToggleGroup(rangeGroup); + + pageRangeField = new TextField("1-" + model.totalPages.get()); + pageRangeField.setPrefWidth(150); + pageRangeField.setDisable(true); + + pageRangeRadio.selectedProperty().addListener((obs, oldVal, newVal) -> { + pageRangeField.setDisable(!newVal); + }); + + Label rangeHintLabel = new Label("(e.g., 1-3, 5, 8-10)"); + rangeHintLabel.setStyle("-fx-font-size: 10px; -fx-text-fill: gray;"); + + GridPane grid = new GridPane(); + grid.setHgap(10); + grid.setVgap(5); + grid.add(allPagesRadio, 0, 0, 2, 1); + grid.add(currentPageRadio, 0, 1, 2, 1); + grid.add(pageRangeRadio, 0, 2); + grid.add(pageRangeField, 1, 2); + grid.add(rangeHintLabel, 1, 3); + + section.getChildren().addAll(titleLabel, grid); + return section; + } + + /** + * Creates the print options section. + */ + private VBox createOptionsSection() { + VBox section = new VBox(10); + + Label titleLabel = new Label("Options"); + titleLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 14px;"); + + GridPane grid = new GridPane(); + grid.setHgap(10); + grid.setVgap(10); + + int row = 0; + + // Copies + Label copiesLabel = new Label("Copies:"); + copiesSpinner = new Spinner<>(1, 999, 1); + copiesSpinner.setEditable(true); + copiesSpinner.setPrefWidth(80); + + grid.add(copiesLabel, 0, row); + grid.add(copiesSpinner, 1, row); + row++; + + // Page scaling + Label scalingLabel = new Label("Page Scaling:"); + pageScalingCombo = new ComboBox<>(); + pageScalingCombo.getItems().addAll( + "None", + "Fit to Printable Area", + "Shrink to Printable Area" + ); + pageScalingCombo.setValue("None"); + pageScalingCombo.setPrefWidth(200); + + grid.add(scalingLabel, 0, row); + grid.add(pageScalingCombo, 1, row); + row++; + + // Orientation + Label orientationLabel = new Label("Orientation:"); + orientationCombo = new ComboBox<>(); + orientationCombo.getItems().addAll("Portrait", "Landscape"); + orientationCombo.setValue("Portrait"); + orientationCombo.setPrefWidth(200); + + grid.add(orientationLabel, 0, row); + grid.add(orientationCombo, 1, row); + row++; + + // Checkboxes + fitToPageCheckBox = new CheckBox("Fit to page"); + centerOnPageCheckBox = new CheckBox("Center on page"); + centerOnPageCheckBox.setSelected(true); + + grid.add(fitToPageCheckBox, 0, row, 2, 1); + row++; + grid.add(centerOnPageCheckBox, 0, row, 2, 1); + + section.getChildren().addAll(titleLabel, grid); + return section; + } + + /** + * Configures the printer job with selected settings. + */ + private void configurePrinterJob() { + if (printerJob == null) { + return; + } + + // Set printer + Printer selectedPrinter = printerCombo.getValue(); + if (selectedPrinter != null) { + // Note: JavaFX PrinterJob doesn't allow changing printer after creation + // This would need to recreate the job + } + + // Configure page layout + Printer printer = printerJob.getPrinter(); + PageLayout pageLayout = printer.createPageLayout( + Paper.A4, + orientationCombo.getValue().equals("Portrait") ? + PageOrientation.PORTRAIT : PageOrientation.LANDSCAPE, + Printer.MarginType.DEFAULT + ); + + // Set job settings + JobSettings jobSettings = printerJob.getJobSettings(); + jobSettings.setPageLayout(pageLayout); + jobSettings.setCopies(copiesSpinner.getValue()); + + // Set page ranges if specified + if (pageRangeRadio.isSelected()) { + String rangeStr = pageRangeField.getText(); + PageRange[] ranges = parsePageRanges(rangeStr); + if (ranges != null && ranges.length > 0) { + jobSettings.setPageRanges(ranges); + } + } else if (currentPageRadio.isSelected()) { + int currentPage = model.currentPage.get(); + jobSettings.setPageRanges(new PageRange(currentPage, currentPage)); + } + + // Note: Additional settings like page scaling would need to be handled + // during the actual printing process + } + + /** + * Parses page range string (e.g., "1-3,5,8-10") into PageRange array. + */ + private PageRange[] parsePageRanges(String rangeStr) { + try { + String[] parts = rangeStr.split(","); + PageRange[] ranges = new PageRange[parts.length]; + + for (int i = 0; i < parts.length; i++) { + String part = parts[i].trim(); + if (part.contains("-")) { + String[] bounds = part.split("-"); + int start = Integer.parseInt(bounds[0].trim()); + int end = Integer.parseInt(bounds[1].trim()); + ranges[i] = new PageRange(start, end); + } else { + int page = Integer.parseInt(part); + ranges[i] = new PageRange(page, page); + } + } + + return ranges; + } catch (Exception e) { + Alert alert = new Alert(Alert.AlertType.ERROR, + "Invalid page range format: " + rangeStr, + ButtonType.OK); + alert.showAndWait(); + return null; + } + } + + /** + * Shows printer properties dialog. + */ + private void showPrinterProperties() { + // JavaFX doesn't provide a built-in printer properties dialog + // This would need to be implemented or use native dialogs + Alert alert = new Alert(Alert.AlertType.INFORMATION, + "Printer properties dialog not yet implemented.", + ButtonType.OK); + alert.showAndWait(); + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/dialogs/SearchDialog.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/dialogs/SearchDialog.java new file mode 100644 index 000000000..a89ffee69 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/dialogs/SearchDialog.java @@ -0,0 +1,344 @@ +package org.icepdf.fx.ri.ui.dialogs; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Insets; +import javafx.scene.control.*; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.Window; +import org.icepdf.fx.ri.viewer.ViewerModel; + +/** + * Advanced search dialog with search options and results display. + */ +public class SearchDialog extends Stage { + + private final ViewerModel model; + + // Search controls + private TextField searchField; + private CheckBox caseSensitiveCheckBox; + private CheckBox wholeWordCheckBox; + private CheckBox regexCheckBox; + private CheckBox highlightAllCheckBox; + + // Results + private ListView resultsListView; + private ObservableList searchResults; + private Label statusLabel; + private ProgressBar progressBar; + + // Navigation + private Button findNextButton; + private Button findPreviousButton; + private Button findAllButton; + private Button clearButton; + + public SearchDialog(ViewerModel model, Window owner) { + this.model = model; + this.searchResults = FXCollections.observableArrayList(); + + initOwner(owner); + initModality(Modality.NONE); // Non-modal dialog + setTitle("Search"); + setResizable(true); + + // Create UI + BorderPane root = new BorderPane(); + root.setPadding(new Insets(10)); + + root.setTop(createSearchPanel()); + root.setCenter(createResultsPanel()); + root.setBottom(createStatusPanel()); + + setScene(new javafx.scene.Scene(root, 500, 600)); + } + + /** + * Creates the search input and options panel. + */ + private VBox createSearchPanel() { + VBox panel = new VBox(10); + panel.setPadding(new Insets(10)); + + // Search field + Label searchLabel = new Label("Find what:"); + searchField = new TextField(); + searchField.setPromptText("Enter search text..."); + searchField.setPrefWidth(400); + + // Search on Enter + searchField.setOnAction(e -> findNext()); + + // Search options + Label optionsLabel = new Label("Options:"); + optionsLabel.setStyle("-fx-font-weight: bold;"); + + caseSensitiveCheckBox = new CheckBox("Case sensitive"); + wholeWordCheckBox = new CheckBox("Whole words only"); + regexCheckBox = new CheckBox("Regular expression"); + highlightAllCheckBox = new CheckBox("Highlight all matches"); + highlightAllCheckBox.setSelected(true); + + GridPane optionsGrid = new GridPane(); + optionsGrid.setHgap(10); + optionsGrid.setVgap(5); + optionsGrid.add(caseSensitiveCheckBox, 0, 0); + optionsGrid.add(wholeWordCheckBox, 1, 0); + optionsGrid.add(regexCheckBox, 0, 1); + optionsGrid.add(highlightAllCheckBox, 1, 1); + + // Buttons + HBox buttonBox = new HBox(10); + + findNextButton = new Button("Find Next"); + findNextButton.setDefaultButton(true); + findNextButton.setOnAction(e -> findNext()); + + findPreviousButton = new Button("Find Previous"); + findPreviousButton.setOnAction(e -> findPrevious()); + + findAllButton = new Button("Find All"); + findAllButton.setOnAction(e -> findAll()); + + clearButton = new Button("Clear"); + clearButton.setOnAction(e -> clear()); + + buttonBox.getChildren().addAll(findNextButton, findPreviousButton, findAllButton, clearButton); + + panel.getChildren().addAll( + searchLabel, + searchField, + new Separator(), + optionsLabel, + optionsGrid, + new Separator(), + buttonBox + ); + + return panel; + } + + /** + * Creates the search results panel. + */ + private VBox createResultsPanel() { + VBox panel = new VBox(10); + panel.setPadding(new Insets(10)); + + Label resultsLabel = new Label("Results:"); + resultsLabel.setStyle("-fx-font-weight: bold;"); + + resultsListView = new ListView<>(searchResults); + resultsListView.setPrefHeight(400); + resultsListView.setCellFactory(lv -> new SearchResultCell()); + + // Navigate to result on click + resultsListView.setOnMouseClicked(e -> { + if (e.getClickCount() == 2) { + SearchResult selected = resultsListView.getSelectionModel().getSelectedItem(); + if (selected != null) { + navigateToResult(selected); + } + } + }); + + panel.getChildren().addAll(resultsLabel, resultsListView); + return panel; + } + + /** + * Creates the status panel at the bottom. + */ + private VBox createStatusPanel() { + VBox panel = new VBox(5); + panel.setPadding(new Insets(10)); + + statusLabel = new Label("Ready"); + statusLabel.setStyle("-fx-font-size: 10px;"); + + progressBar = new ProgressBar(0); + progressBar.setPrefWidth(Double.MAX_VALUE); + progressBar.setVisible(false); + + panel.getChildren().addAll(statusLabel, progressBar); + return panel; + } + + /** + * Finds the next occurrence of the search term. + */ + private void findNext() { + String searchText = searchField.getText(); + if (searchText == null || searchText.trim().isEmpty()) { + showAlert("Please enter search text."); + return; + } + + // TODO: Implement search in document + statusLabel.setText("Searching for next occurrence..."); + + // Placeholder implementation + showAlert("Find Next not yet implemented.\nSearch text: " + searchText); + } + + /** + * Finds the previous occurrence of the search term. + */ + private void findPrevious() { + String searchText = searchField.getText(); + if (searchText == null || searchText.trim().isEmpty()) { + showAlert("Please enter search text."); + return; + } + + // TODO: Implement search in document + statusLabel.setText("Searching for previous occurrence..."); + + // Placeholder implementation + showAlert("Find Previous not yet implemented.\nSearch text: " + searchText); + } + + /** + * Finds all occurrences of the search term. + */ + private void findAll() { + String searchText = searchField.getText(); + if (searchText == null || searchText.trim().isEmpty()) { + showAlert("Please enter search text."); + return; + } + + // Clear previous results + searchResults.clear(); + + // TODO: Implement search in document + statusLabel.setText("Searching entire document..."); + progressBar.setVisible(true); + progressBar.setProgress(-1); // Indeterminate + + // Placeholder - simulate search results + javafx.concurrent.Task searchTask = new javafx.concurrent.Task() { + @Override + protected Void call() throws Exception { + // Simulate search delay + Thread.sleep(500); + + // Add dummy results + javafx.application.Platform.runLater(() -> { + searchResults.add(new SearchResult(1, "Line 1", searchText, 10)); + searchResults.add(new SearchResult(1, "Line 2", searchText, 50)); + searchResults.add(new SearchResult(2, "Line 1", searchText, 20)); + + statusLabel.setText("Found " + searchResults.size() + " matches"); + progressBar.setVisible(false); + }); + + return null; + } + }; + + new Thread(searchTask).start(); + } + + /** + * Clears search results and resets the search. + */ + private void clear() { + searchField.clear(); + searchResults.clear(); + statusLabel.setText("Ready"); + progressBar.setVisible(false); + + // TODO: Clear highlighting in document + } + + /** + * Navigates to the selected search result. + */ + private void navigateToResult(SearchResult result) { + // TODO: Navigate to page and highlight result + statusLabel.setText("Navigating to page " + result.getPageNumber() + "..."); + model.currentPage.set(result.getPageNumber()); + } + + /** + * Shows an alert dialog. + */ + private void showAlert(String message) { + Alert alert = new Alert(Alert.AlertType.INFORMATION, message, ButtonType.OK); + alert.initOwner(this); + alert.showAndWait(); + } + + /** + * Custom cell for displaying search results. + */ + private static class SearchResultCell extends ListCell { + @Override + protected void updateItem(SearchResult item, boolean empty) { + super.updateItem(item, empty); + + if (empty || item == null) { + setText(null); + setGraphic(null); + } else { + VBox vbox = new VBox(2); + + Label pageLabel = new Label("Page " + item.getPageNumber()); + pageLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 10px;"); + + Label contextLabel = new Label(item.getContext()); + contextLabel.setStyle("-fx-font-size: 11px;"); + contextLabel.setWrapText(true); + + vbox.getChildren().addAll(pageLabel, contextLabel); + setGraphic(vbox); + } + } + } + + /** + * Represents a search result. + */ + public static class SearchResult { + private final int pageNumber; + private final String context; + private final String matchText; + private final int position; + + public SearchResult(int pageNumber, String context, String matchText, int position) { + this.pageNumber = pageNumber; + this.context = context; + this.matchText = matchText; + this.position = position; + } + + public int getPageNumber() { + return pageNumber; + } + + public String getContext() { + return context; + } + + public String getMatchText() { + return matchText; + } + + public int getPosition() { + return position; + } + + @Override + public String toString() { + return "Page " + pageNumber + ": " + context; + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/menubar/MenuBarBuilder.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/menubar/MenuBarBuilder.java new file mode 100644 index 000000000..e6a8d57e5 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/menubar/MenuBarBuilder.java @@ -0,0 +1,338 @@ +package org.icepdf.fx.ri.ui.menubar; + +import javafx.application.Platform; +import javafx.scene.control.*; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; +import javafx.stage.Window; +import javafx.util.Builder; +import org.icepdf.fx.ri.viewer.Interactor; +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.document.DocumentPropertiesCommand; +import org.icepdf.fx.ri.viewer.commands.document.OpenFileCommand; +import org.icepdf.fx.ri.viewer.commands.document.PrintDocumentCommand; +import org.icepdf.fx.ri.viewer.commands.document.SearchCommand; +import org.icepdf.fx.ri.viewer.commands.view.ZoomInCommand; +import org.icepdf.fx.ri.viewer.commands.view.ZoomOutCommand; +import org.icepdf.fx.ri.viewer.commands.window.AboutCommand; +import org.icepdf.fx.ri.viewer.commands.window.PreferencesCommand; +import org.icepdf.fx.ri.views.DocumentViewPane; + +/** + * Builder for the main menu bar. + * Creates File, Edit, View, Document, Window, and Help menus. + */ +public class MenuBarBuilder implements Builder { + + private final ViewerModel model; + private final Interactor interactor; + private final Window window; + private final DocumentViewPane documentViewPane; + + public MenuBarBuilder(ViewerModel model, Interactor interactor, Window window, DocumentViewPane documentViewPane) { + this.model = model; + this.interactor = interactor; + this.window = window; + this.documentViewPane = documentViewPane; + } + + @Override + public MenuBar build() { + MenuBar menuBar = new MenuBar(); + menuBar.getMenus().addAll( + buildFileMenu(), + buildEditMenu(), + buildViewMenu(), + buildDocumentMenu(), + buildWindowMenu(), + buildHelpMenu() + ); + return menuBar; + } + + private Menu buildFileMenu() { + // Open + MenuItem open = new MenuItem("Open..."); + open.setAccelerator(new KeyCodeCombination(KeyCode.O, KeyCombination.CONTROL_DOWN)); + open.setOnAction(e -> new OpenFileCommand(window, model).execute()); + + // Close + MenuItem close = new MenuItem("Close"); + close.setAccelerator(new KeyCodeCombination(KeyCode.W, KeyCombination.CONTROL_DOWN)); + close.disableProperty().bind(model.document.isNull()); + close.setOnAction(e -> closeDocument()); + + // Save (placeholder) + MenuItem save = new MenuItem("Save..."); + save.setAccelerator(new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN)); + save.disableProperty().bind(model.document.isNull()); + save.setOnAction(e -> model.statusMessage.set("Save not yet implemented")); + + // Print + MenuItem print = new MenuItem("Print..."); + print.setAccelerator(new KeyCodeCombination(KeyCode.P, KeyCombination.CONTROL_DOWN)); + print.disableProperty().bind(model.document.isNull()); + print.setOnAction(e -> new PrintDocumentCommand(window, model).execute()); + + // Recent Files submenu (placeholder) + Menu recentFiles = new Menu("Recent Files"); + recentFiles.getItems().add(new MenuItem("(No recent files)")); + + // Exit + MenuItem exit = new MenuItem("Exit"); + exit.setAccelerator(new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN)); + exit.setOnAction(e -> Platform.exit()); + + return new Menu("File", null, + open, + close, + new SeparatorMenuItem(), + save, + print, + new SeparatorMenuItem(), + recentFiles, + new SeparatorMenuItem(), + exit + ); + } + + private Menu buildEditMenu() { + // Copy + MenuItem copy = new MenuItem("Copy"); + copy.setAccelerator(new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_DOWN)); + copy.disableProperty().bind(model.selectedText.isEmpty()); + copy.setOnAction(e -> copySelectedText()); + + // Select All + MenuItem selectAll = new MenuItem("Select All"); + selectAll.setAccelerator(new KeyCodeCombination(KeyCode.A, KeyCombination.CONTROL_DOWN)); + selectAll.disableProperty().bind(model.document.isNull()); + selectAll.setOnAction(e -> model.statusMessage.set("Select All not yet implemented")); + + // Search + MenuItem search = new MenuItem("Search..."); + search.setAccelerator(new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN)); + search.disableProperty().bind(model.document.isNull()); + search.setOnAction(e -> new SearchCommand(model, window).execute()); + + // Preferences + MenuItem preferences = new MenuItem("Preferences..."); + preferences.setAccelerator(new KeyCodeCombination(KeyCode.COMMA, KeyCombination.CONTROL_DOWN)); + preferences.setOnAction(e -> new PreferencesCommand(model, window).execute()); + + return new Menu("Edit", null, + copy, + selectAll, + new SeparatorMenuItem(), + search, + new SeparatorMenuItem(), + preferences + ); + } + + private Menu buildViewMenu() { + // Zoom In + MenuItem zoomIn = new MenuItem("Zoom In"); + zoomIn.setAccelerator(new KeyCodeCombination(KeyCode.PLUS, KeyCombination.CONTROL_DOWN)); + zoomIn.disableProperty().bind(model.document.isNull()); + zoomIn.setOnAction(e -> new ZoomInCommand(documentViewPane, model).execute()); + + // Zoom Out + MenuItem zoomOut = new MenuItem("Zoom Out"); + zoomOut.setAccelerator(new KeyCodeCombination(KeyCode.MINUS, KeyCombination.CONTROL_DOWN)); + zoomOut.disableProperty().bind(model.document.isNull()); + zoomOut.setOnAction(e -> new ZoomOutCommand(documentViewPane, model).execute()); + + // Actual Size + MenuItem actualSize = new MenuItem("Actual Size"); + actualSize.setAccelerator(new KeyCodeCombination(KeyCode.DIGIT0, KeyCombination.CONTROL_DOWN)); + actualSize.disableProperty().bind(model.document.isNull()); + actualSize.setOnAction(e -> setActualSize()); + + // Fit Width + MenuItem fitWidth = new MenuItem("Fit Width"); + fitWidth.disableProperty().bind(model.document.isNull()); + fitWidth.setOnAction(e -> { + model.fitMode.set(ViewerModel.FitMode.FIT_WIDTH); + model.statusMessage.set("Fit Width not yet implemented"); + }); + + // Fit Page + MenuItem fitPage = new MenuItem("Fit Page"); + fitPage.disableProperty().bind(model.document.isNull()); + fitPage.setOnAction(e -> { + model.fitMode.set(ViewerModel.FitMode.FIT_PAGE); + model.statusMessage.set("Fit Page not yet implemented"); + }); + + // Rotation submenu + Menu rotation = new Menu("Rotate"); + MenuItem rotateLeft = new MenuItem("Rotate Left"); + rotateLeft.setAccelerator(new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN)); + rotateLeft.disableProperty().bind(model.document.isNull()); + rotateLeft.setOnAction(e -> rotateLeft()); + + MenuItem rotateRight = new MenuItem("Rotate Right"); + rotateRight.setAccelerator(new KeyCodeCombination(KeyCode.R, KeyCombination.CONTROL_DOWN)); + rotateRight.disableProperty().bind(model.document.isNull()); + rotateRight.setOnAction(e -> rotateRight()); + + rotation.getItems().addAll(rotateLeft, rotateRight); + + // Panels submenu + Menu panels = new Menu("Panels"); + CheckMenuItem showLeftPanel = new CheckMenuItem("Show Left Panel"); + showLeftPanel.selectedProperty().bindBidirectional(model.leftPanelVisible); + + CheckMenuItem showStatusBar = new CheckMenuItem("Show Status Bar"); + showStatusBar.selectedProperty().bindBidirectional(model.statusBarVisible); + + CheckMenuItem showToolBar = new CheckMenuItem("Show Tool Bar"); + showToolBar.selectedProperty().bindBidirectional(model.toolBarVisible); + + panels.getItems().addAll(showLeftPanel, showStatusBar, showToolBar); + + // Full Screen + MenuItem fullScreen = new MenuItem("Full Screen"); + fullScreen.setAccelerator(new KeyCodeCombination(KeyCode.F11)); + fullScreen.setOnAction(e -> toggleFullScreen()); + + return new Menu("View", null, + zoomIn, + zoomOut, + actualSize, + new SeparatorMenuItem(), + fitWidth, + fitPage, + new SeparatorMenuItem(), + rotation, + new SeparatorMenuItem(), + panels, + new SeparatorMenuItem(), + fullScreen + ); + } + + private Menu buildDocumentMenu() { + // Document Properties + MenuItem properties = new MenuItem("Properties..."); + properties.setAccelerator(new KeyCodeCombination(KeyCode.D, KeyCombination.CONTROL_DOWN)); + properties.disableProperty().bind(model.document.isNull()); + properties.setOnAction(e -> new DocumentPropertiesCommand(window, model).execute()); + + // Document Information + MenuItem information = new MenuItem("Information..."); + information.disableProperty().bind(model.document.isNull()); + information.setOnAction(e -> model.statusMessage.set("Document Information not yet implemented")); + + // Fonts + MenuItem fonts = new MenuItem("Fonts..."); + fonts.disableProperty().bind(model.document.isNull()); + fonts.setOnAction(e -> model.statusMessage.set("Fonts dialog not yet implemented")); + + // Security/Permissions + MenuItem security = new MenuItem("Security..."); + security.disableProperty().bind(model.document.isNull()); + security.setOnAction(e -> model.statusMessage.set("Security dialog not yet implemented")); + + return new Menu("Document", null, + properties, + information, + new SeparatorMenuItem(), + fonts, + security + ); + } + + private Menu buildWindowMenu() { + // New Window + MenuItem newWindow = new MenuItem("New Window"); + newWindow.setAccelerator(new KeyCodeCombination(KeyCode.N, KeyCombination.CONTROL_DOWN)); + newWindow.setOnAction(e -> model.statusMessage.set("New Window not yet implemented")); + + // Minimize + MenuItem minimize = new MenuItem("Minimize"); + minimize.setAccelerator(new KeyCodeCombination(KeyCode.M, KeyCombination.CONTROL_DOWN)); + minimize.setOnAction(e -> minimizeWindow()); + + return new Menu("Window", null, + newWindow, + minimize + ); + } + + private Menu buildHelpMenu() { + // Documentation + MenuItem documentation = new MenuItem("Documentation..."); + documentation.setOnAction(e -> model.statusMessage.set("Documentation not yet implemented")); + + // About + MenuItem about = new MenuItem("About..."); + about.setOnAction(e -> new AboutCommand(window).execute()); + + return new Menu("Help", null, + documentation, + new SeparatorMenuItem(), + about + ); + } + + // Helper methods + + private void closeDocument() { + if (model.document.get() != null) { + model.document.get().dispose(); + model.document.set(null); + model.filePath.set(null); + model.documentTitle.set(""); + model.currentPage.set(1); + model.totalPages.set(0); + model.statusMessage.set("Document closed"); + } + } + + private void copySelectedText() { + String text = model.selectedText.get(); + if (text != null && !text.isEmpty()) { + javafx.scene.input.Clipboard clipboard = javafx.scene.input.Clipboard.getSystemClipboard(); + javafx.scene.input.ClipboardContent content = new javafx.scene.input.ClipboardContent(); + content.putString(text); + clipboard.setContent(content); + model.statusMessage.set("Text copied to clipboard"); + } + } + + private void setActualSize() { + model.zoomLevel.set(1.0); + model.fitMode.set(ViewerModel.FitMode.ACTUAL_SIZE); + model.statusMessage.set("Actual Size (100%)"); + } + + private void rotateLeft() { + double current = model.rotationAngle.get(); + model.rotationAngle.set((current - 90) % 360); + model.statusMessage.set("Rotated left"); + } + + private void rotateRight() { + double current = model.rotationAngle.get(); + model.rotationAngle.set((current + 90) % 360); + model.statusMessage.set("Rotated right"); + } + + private void toggleFullScreen() { + if (window instanceof javafx.stage.Stage) { + javafx.stage.Stage stage = (javafx.stage.Stage) window; + stage.setFullScreen(!stage.isFullScreen()); + } + } + + private void minimizeWindow() { + if (window instanceof javafx.stage.Stage) { + javafx.stage.Stage stage = (javafx.stage.Stage) window; + stage.setIconified(true); + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/AnnotationsPanel.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/AnnotationsPanel.java new file mode 100644 index 000000000..20a774400 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/AnnotationsPanel.java @@ -0,0 +1,268 @@ +package org.icepdf.fx.ri.ui.sidepanel; + +import javafx.geometry.Insets; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import org.icepdf.core.pobjects.Document; +import org.icepdf.core.pobjects.Page; +import org.icepdf.core.pobjects.annotations.Annotation; +import org.icepdf.core.pobjects.annotations.TextMarkupAnnotation; +import org.icepdf.fx.ri.viewer.ViewerModel; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +/** + * Panel for viewing and navigating to annotations in the PDF document. + */ +public class AnnotationsPanel extends VBox { + + private static final Logger logger = Logger.getLogger(AnnotationsPanel.class.getName()); + + private final ViewerModel model; + private final TableView annotationsTable; + private final ComboBox filterComboBox; + private final Label emptyLabel; + private final List allAnnotations; + + public AnnotationsPanel(ViewerModel model) { + this.model = model; + this.annotationsTable = new TableView<>(); + this.filterComboBox = new ComboBox<>(); + this.emptyLabel = new Label("No annotations"); + this.allAnnotations = new ArrayList<>(); + + initializeUI(); + setupBindings(); + } + + private void initializeUI() { + setSpacing(5); + setPadding(new Insets(5)); + + // Filter ComboBox + filterComboBox.getItems().addAll( + "All Types", + "Text", + "Highlight", + "Link", + "Markup", + "Widget", + "Other" + ); + filterComboBox.setValue("All Types"); + filterComboBox.setMaxWidth(Double.MAX_VALUE); + filterComboBox.setOnAction(e -> applyFilter()); + + // Configure TableView columns + TableColumn pageCol = new TableColumn<>("Page"); + pageCol.setCellValueFactory(new PropertyValueFactory<>("pageNumber")); + pageCol.setPrefWidth(60); + + TableColumn typeCol = new TableColumn<>("Type"); + typeCol.setCellValueFactory(new PropertyValueFactory<>("type")); + typeCol.setPrefWidth(80); + + TableColumn contentCol = new TableColumn<>("Content"); + contentCol.setCellValueFactory(new PropertyValueFactory<>("content")); + contentCol.setPrefWidth(150); + + annotationsTable.getColumns().addAll(pageCol, typeCol, contentCol); + annotationsTable.setPlaceholder(emptyLabel); + annotationsTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + + // Navigate to annotation on double-click + annotationsTable.setOnMouseClicked(event -> { + if (event.getClickCount() == 2) { + AnnotationItem selected = annotationsTable.getSelectionModel().getSelectedItem(); + if (selected != null) { + navigateToAnnotation(selected); + } + } + }); + + VBox.setVgrow(annotationsTable, Priority.ALWAYS); + + getChildren().addAll( + new Label("Filter by type:"), + filterComboBox, + annotationsTable + ); + } + + private void setupBindings() { + // Listen for document changes + model.document.addListener((observable, oldDoc, newDoc) -> { + handleDocumentChange(newDoc); + }); + + // Load annotations if document already exists + if (model.document.get() != null) { + handleDocumentChange(model.document.get()); + } + } + + private void handleDocumentChange(Document newDoc) { + allAnnotations.clear(); + annotationsTable.getItems().clear(); + + if (newDoc != null) { + try { + int pageCount = newDoc.getNumberOfPages(); + + for (int i = 0; i < pageCount; i++) { + Page page = newDoc.getPageTree().getPage(i); + if (page != null) { + page.init(); + List pageAnnotations = page.getAnnotations(); + + if (pageAnnotations != null && !pageAnnotations.isEmpty()) { + for (Annotation annotation : pageAnnotations) { + AnnotationItem item = createAnnotationItem(i + 1, annotation); + if (item != null) { + allAnnotations.add(item); + } + } + } + } + } + + // Display all annotations initially + annotationsTable.getItems().setAll(allAnnotations); + + } catch (Exception e) { + logger.warning("Error loading annotations: " + e.getMessage()); + } + } + } + + private AnnotationItem createAnnotationItem(int pageNumber, Annotation annotation) { + try { + String type = getAnnotationType(annotation); + String content = getAnnotationContent(annotation); + + return new AnnotationItem(pageNumber, type, content, annotation); + } catch (Exception e) { + logger.warning("Error creating annotation item: " + e.getMessage()); + return null; + } + } + + private String getAnnotationType(Annotation annotation) { + org.icepdf.core.pobjects.Name subtype = annotation.getSubType(); + + if (subtype == null) return "Other"; + + if (subtype.equals(Annotation.SUBTYPE_TEXT)) { + return "Text"; + } else if (subtype.equals(Annotation.SUBTYPE_LINK)) { + return "Link"; + } else if (subtype.equals(Annotation.SUBTYPE_HIGHLIGHT)) { + return "Highlight"; + } else if (subtype.equals(TextMarkupAnnotation.SUBTYPE_UNDERLINE)) { + return "Underline"; + } else if (subtype.equals(TextMarkupAnnotation.SUBTYPE_STRIKE_OUT)) { + return "Strikeout"; + } else if (subtype.equals(Annotation.SUBTYPE_SQUARE)) { + return "Square"; + } else if (subtype.equals(Annotation.SUBTYPE_CIRCLE)) { + return "Circle"; + } else if (subtype.equals(Annotation.SUBTYPE_INK)) { + return "Ink"; + } else if (subtype.equals(Annotation.SUBTYPE_POPUP)) { + return "Popup"; + } else if (subtype.equals(Annotation.SUBTYPE_FREE_TEXT)) { + return "Free Text"; + } else if (subtype.equals(Annotation.SUBTYPE_WIDGET)) { + return "Widget"; + } else { + return "Other"; + } + } + + private String getAnnotationContent(Annotation annotation) { + try { + // Try to get annotation contents/title + String contents = annotation.getContents(); + if (contents != null && !contents.trim().isEmpty()) { + return truncate(contents, 50); + } + + // Fallback to annotation type description + return getAnnotationType(annotation); + } catch (Exception e) { + return "(No content)"; + } + } + + private String truncate(String text, int maxLength) { + if (text.length() <= maxLength) { + return text; + } + return text.substring(0, maxLength - 3) + "..."; + } + + private void applyFilter() { + String filter = filterComboBox.getValue(); + + if ("All Types".equals(filter)) { + annotationsTable.getItems().setAll(allAnnotations); + } else { + List filtered = new ArrayList<>(); + for (AnnotationItem item : allAnnotations) { + if (item.getType().equals(filter)) { + filtered.add(item); + } + } + annotationsTable.getItems().setAll(filtered); + } + } + + private void navigateToAnnotation(AnnotationItem item) { + if (item != null) { + model.currentPage.set(item.getPageNumber()); + // TODO: Highlight or focus the annotation on the page + logger.info("Navigated to annotation on page " + item.getPageNumber()); + } + } + + /** + * Represents an annotation item in the table. + */ + public static class AnnotationItem { + private final int pageNumber; + private final String type; + private final String content; + private final Annotation annotation; + + public AnnotationItem(int pageNumber, String type, String content, Annotation annotation) { + this.pageNumber = pageNumber; + this.type = type; + this.content = content; + this.annotation = annotation; + } + + public int getPageNumber() { + return pageNumber; + } + + public String getType() { + return type; + } + + public String getContent() { + return content; + } + + public Annotation getAnnotation() { + return annotation; + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/AttachmentsPanel.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/AttachmentsPanel.java new file mode 100644 index 000000000..ed2c90a4f --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/AttachmentsPanel.java @@ -0,0 +1,215 @@ +package org.icepdf.fx.ri.ui.sidepanel; + +import javafx.geometry.Insets; +import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import org.icepdf.core.pobjects.Document; +import org.icepdf.core.pobjects.NameTree; +import org.icepdf.fx.ri.viewer.ViewerModel; + +import java.io.File; +import java.util.List; +import java.util.logging.Logger; + +/** + * Panel for viewing and extracting embedded file attachments from the PDF. + */ +public class AttachmentsPanel extends VBox { + + private static final Logger logger = Logger.getLogger(AttachmentsPanel.class.getName()); + + private final ViewerModel model; + private final TableView attachmentsTable; + private final Button extractButton; + private final Label emptyLabel; + + public AttachmentsPanel(ViewerModel model) { + this.model = model; + this.attachmentsTable = new TableView<>(); + this.extractButton = new Button("Extract..."); + this.emptyLabel = new Label("No attachments"); + + initializeUI(); + setupBindings(); + } + + private void initializeUI() { + setSpacing(5); + setPadding(new Insets(5)); + + // Configure TableView columns + TableColumn nameCol = new TableColumn<>("Name"); + nameCol.setCellValueFactory(new PropertyValueFactory<>("name")); + nameCol.setPrefWidth(150); + + TableColumn typeCol = new TableColumn<>("Type"); + typeCol.setCellValueFactory(new PropertyValueFactory<>("type")); + typeCol.setPrefWidth(80); + + TableColumn sizeCol = new TableColumn<>("Size"); + sizeCol.setCellValueFactory(new PropertyValueFactory<>("size")); + sizeCol.setPrefWidth(80); + + attachmentsTable.getColumns().addAll(nameCol, typeCol, sizeCol); + attachmentsTable.setPlaceholder(emptyLabel); + attachmentsTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + + VBox.setVgrow(attachmentsTable, Priority.ALWAYS); + + // Extract button + extractButton.setMaxWidth(Double.MAX_VALUE); + extractButton.setOnAction(e -> extractSelectedAttachment()); + extractButton.disableProperty().bind( + attachmentsTable.getSelectionModel().selectedItemProperty().isNull() + ); + + getChildren().addAll(attachmentsTable, extractButton); + } + + private void setupBindings() { + // Listen for document changes + model.document.addListener((observable, oldDoc, newDoc) -> { + handleDocumentChange(newDoc); + }); + + // Load attachments if document already exists + if (model.document.get() != null) { + handleDocumentChange(model.document.get()); + } + } + + private void handleDocumentChange(Document newDoc) { + attachmentsTable.getItems().clear(); + + if (newDoc != null) { + try { + // Get embedded files from the document catalog + org.icepdf.core.pobjects.Catalog catalog = newDoc.getCatalog(); + if (catalog != null) { + NameTree embeddedFiles = catalog.getEmbeddedFilesNameTree(); + if (embeddedFiles != null) { + // getNamesAndValues returns a List alternating between name (String) and value (Object) + List namesAndValues = embeddedFiles.getNamesAndValues(); + if (namesAndValues != null && !namesAndValues.isEmpty()) { + // Iterate through pairs (name, value) + for (int i = 0; i < namesAndValues.size() - 1; i += 2) { + Object nameObj = namesAndValues.get(i); + Object valueObj = namesAndValues.get(i + 1); + + if (nameObj instanceof String) { + String name = (String) nameObj; + + // Value should be a FileSpec dictionary + if (valueObj instanceof org.icepdf.core.pobjects.FileSpecification) { + org.icepdf.core.pobjects.FileSpecification fileSpec = + (org.icepdf.core.pobjects.FileSpecification) valueObj; + AttachmentItem item = createAttachmentItem(name, fileSpec); + if (item != null) { + attachmentsTable.getItems().add(item); + } + } + } + } + } + } + } + } catch (Exception e) { + logger.warning("Error loading attachments: " + e.getMessage()); + } + } + } + + private AttachmentItem createAttachmentItem(String name, org.icepdf.core.pobjects.FileSpecification fileSpec) { + try { + // Get file type from FileSpec + String type = "Unknown"; + long size = 0; + + // Try to get embedded file stream for more details + // Note: FileSpecification API may vary - this is a simplified version + + String sizeStr = formatFileSize(size); + + return new AttachmentItem(name, type, sizeStr, fileSpec); + } catch (Exception e) { + logger.warning("Error creating attachment item: " + e.getMessage()); + return null; + } + } + + private String formatFileSize(long bytes) { + if (bytes < 1024) return bytes + " B"; + if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0); + return String.format("%.1f MB", bytes / (1024.0 * 1024.0)); + } + + private void extractSelectedAttachment() { + AttachmentItem selected = attachmentsTable.getSelectionModel().getSelectedItem(); + if (selected == null) return; + + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Save Attachment"); + fileChooser.setInitialFileName(selected.getName()); + + File file = fileChooser.showSaveDialog(getScene().getWindow()); + if (file != null) { + try { + // TODO: Extract actual file data from FileSpecification + // This requires understanding the FileSpecification API + showAlert(Alert.AlertType.INFORMATION, "Not Implemented", + "Attachment extraction not yet fully implemented."); + logger.info("Would extract attachment to: " + file.getAbsolutePath()); + } catch (Exception e) { + logger.warning("Error extracting attachment: " + e.getMessage()); + showAlert(Alert.AlertType.ERROR, "Error", + "Failed to extract attachment: " + e.getMessage()); + } + } + } + + private void showAlert(Alert.AlertType type, String title, String message) { + Alert alert = new Alert(type); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } + + /** + * Represents an attachment item in the table. + */ + public static class AttachmentItem { + private final String name; + private final String type; + private final String size; + private final org.icepdf.core.pobjects.FileSpecification fileSpec; + + public AttachmentItem(String name, String type, String size, + org.icepdf.core.pobjects.FileSpecification fileSpec) { + this.name = name; + this.type = type; + this.size = size; + this.fileSpec = fileSpec; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public String getSize() { + return size; + } + + public org.icepdf.core.pobjects.FileSpecification getFileSpec() { + return fileSpec; + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/LayersPanel.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/LayersPanel.java new file mode 100644 index 000000000..0acfe0f16 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/LayersPanel.java @@ -0,0 +1,213 @@ +package org.icepdf.fx.ri.ui.sidepanel; + +import javafx.geometry.Insets; +import javafx.scene.control.*; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import org.icepdf.core.pobjects.Document; +import org.icepdf.core.pobjects.OptionalContent; +import org.icepdf.fx.ri.viewer.ViewerModel; + +import java.util.List; +import java.util.logging.Logger; + +/** + * Panel for managing optional content (layers) in the PDF document. + * Allows toggling layer visibility. + */ +public class LayersPanel extends VBox { + + private static final Logger logger = Logger.getLogger(LayersPanel.class.getName()); + + private final ViewerModel model; + private final TreeView layersTreeView; + private final Label emptyLabel; + + public LayersPanel(ViewerModel model) { + this.model = model; + this.layersTreeView = new TreeView<>(); + this.emptyLabel = new Label("No layers available"); + + initializeUI(); + setupBindings(); + } + + private void initializeUI() { + setSpacing(5); + setPadding(new Insets(5)); + + // Configure TreeView + layersTreeView.setShowRoot(false); + layersTreeView.setCellFactory(tv -> new LayerTreeCell()); + // Note: TreeView doesn't have setPlaceholder like ListView + + VBox.setVgrow(layersTreeView, Priority.ALWAYS); + getChildren().add(layersTreeView); + } + + private void setupBindings() { + // Listen for document changes + model.document.addListener((observable, oldDoc, newDoc) -> { + handleDocumentChange(newDoc); + }); + + // Load layers if document already exists + if (model.document.get() != null) { + handleDocumentChange(model.document.get()); + } + } + + private void handleDocumentChange(Document newDoc) { + // Clear existing layers + layersTreeView.setRoot(null); + + if (newDoc != null) { + try { + // Get optional content from catalog + org.icepdf.core.pobjects.Catalog catalog = newDoc.getCatalog(); + if (catalog != null) { + OptionalContent optionalContent = catalog.getOptionalContent(); + if (optionalContent != null && !optionalContent.isEmptyDefinition()) { + // Get the ordered list of layers + List order = optionalContent.getOrder(); + + if (order != null && !order.isEmpty()) { + TreeItem root = new TreeItem<>( + new LayerWrapper(null, "Layers", true) + ); + + // Build layer tree from order list + buildLayerTree(root, order, optionalContent); + + root.setExpanded(true); + layersTreeView.setRoot(root); + } + } + } + } catch (Exception e) { + logger.warning("Error loading layers: " + e.getMessage()); + } + } + } + + /** + * Recursively builds the layer tree from the order list. + */ + @SuppressWarnings("unchecked") + private void buildLayerTree(TreeItem parent, List items, + OptionalContent optionalContent) { + for (Object item : items) { + if (item instanceof org.icepdf.core.pobjects.Reference) { + // It's a reference to an OptionalContentGroup + org.icepdf.core.pobjects.OptionalContentGroup ocg = + optionalContent.getOCGs((org.icepdf.core.pobjects.Reference) item); + if (ocg != null) { + String name = ocg.getName(); + boolean visible = optionalContent.isVisible(ocg); + + TreeItem layerItem = new TreeItem<>( + new LayerWrapper(ocg, name != null ? name : "(Unnamed)", visible) + ); + parent.getChildren().add(layerItem); + } + } else if (item instanceof List) { + // It's a nested list (possibly with a label as first element) + List nestedList = (List) item; + if (!nestedList.isEmpty()) { + Object first = nestedList.get(0); + if (first instanceof String || first instanceof org.icepdf.core.pobjects.StringObject) { + // First element is a label + String label = first.toString(); + TreeItem labelItem = new TreeItem<>( + new LayerWrapper(null, label, true) + ); + parent.getChildren().add(labelItem); + + // Process remaining items under this label + if (nestedList.size() > 1) { + buildLayerTree(labelItem, (List) nestedList.subList(1, nestedList.size()), + optionalContent); + } + } else { + // No label, just nested items + buildLayerTree(parent, (List) nestedList, optionalContent); + } + } + } + } + } + + /** + * Wrapper class for layer items. + */ + private static class LayerWrapper { + private final org.icepdf.core.pobjects.OptionalContentGroup layer; + private final String name; + private boolean visible; + + public LayerWrapper(org.icepdf.core.pobjects.OptionalContentGroup layer, String name, boolean visible) { + this.layer = layer; + this.name = name; + this.visible = visible; + } + + public org.icepdf.core.pobjects.OptionalContentGroup getLayer() { + return layer; + } + + public String getName() { + return name; + } + + public boolean isVisible() { + return visible; + } + + public void setVisible(boolean visible) { + this.visible = visible; + if (layer != null) { + layer.setVisible(visible); + } + } + + @Override + public String toString() { + return name; + } + } + + /** + * Custom TreeCell with checkboxes for layer visibility. + */ + private class LayerTreeCell extends TreeCell { + + private final CheckBox checkBox; + + public LayerTreeCell() { + this.checkBox = new CheckBox(); + checkBox.setOnAction(e -> { + LayerWrapper wrapper = getItem(); + if (wrapper != null) { + wrapper.setVisible(checkBox.isSelected()); + // TODO: Trigger page refresh to show/hide layer + logger.info("Layer '" + wrapper.getName() + "' visibility: " + checkBox.isSelected()); + } + }); + } + + @Override + protected void updateItem(LayerWrapper item, boolean empty) { + super.updateItem(item, empty); + + if (empty || item == null) { + setText(null); + setGraphic(null); + } else { + checkBox.setText(item.getName()); + checkBox.setSelected(item.isVisible()); + setGraphic(checkBox); + } + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/OutlinePanel.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/OutlinePanel.java new file mode 100644 index 000000000..d1e2a5dfe --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/OutlinePanel.java @@ -0,0 +1,213 @@ +package org.icepdf.fx.ri.ui.sidepanel; + +import javafx.geometry.Insets; +import javafx.scene.control.Label; +import javafx.scene.control.TreeCell; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import org.icepdf.core.pobjects.Document; +import org.icepdf.core.pobjects.OutlineItem; +import org.icepdf.core.pobjects.Reference; +import org.icepdf.fx.ri.viewer.ViewerModel; + +import java.util.logging.Logger; + +/** + * Panel displaying the document outline (bookmarks) in a tree structure. + * Allows navigation to destinations by clicking on outline items. + */ +public class OutlinePanel extends VBox { + + private static final Logger logger = Logger.getLogger(OutlinePanel.class.getName()); + + private final ViewerModel model; + private final TreeView outlineTreeView; + private final Label emptyLabel; + + public OutlinePanel(ViewerModel model) { + this.model = model; + this.outlineTreeView = new TreeView<>(); + this.emptyLabel = new Label("No outline available"); + + initializeUI(); + setupBindings(); + } + + private void initializeUI() { + setSpacing(5); + setPadding(new Insets(5)); + + // Configure TreeView + outlineTreeView.setShowRoot(false); + outlineTreeView.setCellFactory(tv -> new OutlineTreeCell()); + // Note: TreeView doesn't have setPlaceholder like ListView + + // Handle selection + outlineTreeView.getSelectionModel().selectedItemProperty().addListener( + (observable, oldValue, newValue) -> { + if (newValue != null && newValue.getValue() != null) { + navigateToOutlineItem(newValue.getValue()); + } + } + ); + + VBox.setVgrow(outlineTreeView, Priority.ALWAYS); + getChildren().add(outlineTreeView); + } + + private void setupBindings() { + // Listen for document changes + model.document.addListener((observable, oldDoc, newDoc) -> { + handleDocumentChange(newDoc); + }); + + // Load outline if document already exists + if (model.document.get() != null) { + handleDocumentChange(model.document.get()); + } + } + + private void handleDocumentChange(Document newDoc) { + // Clear existing outline + outlineTreeView.setRoot(null); + + if (newDoc != null) { + // Get the document catalog's outlines + org.icepdf.core.pobjects.Catalog catalog = newDoc.getCatalog(); + if (catalog != null) { + org.icepdf.core.pobjects.Outlines outlines = catalog.getOutlines(); + if (outlines != null) { + OutlineItem rootOutlineItem = outlines.getRootOutlineItem(); + if (rootOutlineItem != null && !rootOutlineItem.isEmpty()) { + TreeItem root = new TreeItem<>( + new OutlineItemWrapper(null, "Root") + ); + + // Add children of root + int subItemCount = rootOutlineItem.getSubItemCount(); + for (int i = 0; i < subItemCount; i++) { + OutlineItem item = rootOutlineItem.getSubItem(i); + TreeItem treeItem = createTreeItem(item); + if (treeItem != null) { + root.getChildren().add(treeItem); + } + } + + root.setExpanded(true); + outlineTreeView.setRoot(root); + } + } + } + } + } + + /** + * Recursively creates tree items from outline items. + */ + private TreeItem createTreeItem(OutlineItem outlineItem) { + if (outlineItem == null) return null; + + String title = outlineItem.getTitle(); + if (title == null || title.trim().isEmpty()) { + title = "(Untitled)"; + } + + OutlineItemWrapper wrapper = new OutlineItemWrapper(outlineItem, title); + TreeItem treeItem = new TreeItem<>(wrapper); + + // Process children recursively + int subItemCount = outlineItem.getSubItemCount(); + if (subItemCount > 0) { + for (int i = 0; i < subItemCount; i++) { + OutlineItem child = outlineItem.getSubItem(i); + TreeItem childItem = createTreeItem(child); + if (childItem != null) { + treeItem.getChildren().add(childItem); + } + } + } + + return treeItem; + } + + /** + * Navigates to the destination specified by an outline item. + */ + private void navigateToOutlineItem(OutlineItemWrapper wrapper) { + if (wrapper == null || wrapper.getOutlineItem() == null) return; + + OutlineItem item = wrapper.getOutlineItem(); + + try { + // Try to get destination from action or directly + org.icepdf.core.pobjects.Destination dest = item.getDest(); + + if (dest != null) { + Reference pageRef = dest.getPageReference(); + if (pageRef != null && model.document.get() != null) { + // Look up the page number from the reference + Document doc = model.document.get(); + org.icepdf.core.pobjects.PageTree pageTree = doc.getPageTree(); + int pageNumber = pageTree.getPageNumber(pageRef); + + if (pageNumber >= 0) { + // Convert to 1-based page number + model.currentPage.set(pageNumber + 1); + logger.fine("Navigated to page " + (pageNumber + 1) + " from outline"); + } + } + } + } catch (Exception e) { + logger.warning("Failed to navigate to outline destination: " + e.getMessage()); + } + } + + /** + * Wrapper class for outline items to display in TreeView. + */ + private static class OutlineItemWrapper { + private final OutlineItem outlineItem; + private final String displayTitle; + + public OutlineItemWrapper(OutlineItem outlineItem, String displayTitle) { + this.outlineItem = outlineItem; + this.displayTitle = displayTitle; + } + + public OutlineItem getOutlineItem() { + return outlineItem; + } + + public String getDisplayTitle() { + return displayTitle; + } + + @Override + public String toString() { + return displayTitle; + } + } + + /** + * Custom TreeCell for outline items with icons. + */ + private class OutlineTreeCell extends TreeCell { + + @Override + protected void updateItem(OutlineItemWrapper item, boolean empty) { + super.updateItem(item, empty); + + if (empty || item == null) { + setText(null); + setGraphic(null); + } else { + setText(item.getDisplayTitle()); + // Could add icons here in the future + setGraphic(null); + } + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/SearchPanel.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/SearchPanel.java new file mode 100644 index 000000000..641963969 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/SearchPanel.java @@ -0,0 +1,239 @@ +package org.icepdf.fx.ri.ui.sidepanel; + +import javafx.concurrent.Task; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import org.icepdf.core.pobjects.Document; +import org.icepdf.core.search.DocumentSearchController; +import org.icepdf.fx.ri.viewer.ViewerModel; + +import java.util.List; +import java.util.logging.Logger; + +/** + * Panel for searching text within the PDF document. + * Provides search options and displays results in a list. + */ +public class SearchPanel extends VBox { + + private static final Logger logger = Logger.getLogger(SearchPanel.class.getName()); + + private final ViewerModel model; + private final TextField searchField; + private final Button searchButton; + private final Button clearButton; + private final CheckBox caseSensitiveCheck; + private final CheckBox wholeWordCheck; + private final ListView resultsListView; + private final Label statusLabel; + private final ProgressIndicator progressIndicator; + + private DocumentSearchController searchController; + private Task> currentSearchTask; + + public SearchPanel(ViewerModel model) { + this.model = model; + this.searchField = new TextField(); + this.searchButton = new Button("Search"); + this.clearButton = new Button("Clear"); + this.caseSensitiveCheck = new CheckBox("Case sensitive"); + this.wholeWordCheck = new CheckBox("Whole word"); + this.resultsListView = new ListView<>(); + this.statusLabel = new Label(""); + this.progressIndicator = new ProgressIndicator(); + + initializeUI(); + setupBindings(); + } + + private void initializeUI() { + setSpacing(5); + setPadding(new Insets(10)); + + // Search input section + VBox searchInputSection = new VBox(5); + searchField.setPromptText("Enter search text..."); + searchField.setOnAction(e -> performSearch()); + + HBox buttonBox = new HBox(5); + searchButton.setDefaultButton(true); + searchButton.setOnAction(e -> performSearch()); + clearButton.setOnAction(e -> clearSearch()); + buttonBox.getChildren().addAll(searchButton, clearButton); + + searchInputSection.getChildren().addAll( + new Label("Search:"), + searchField, + buttonBox + ); + + // Search options section + VBox optionsSection = new VBox(5); + optionsSection.getChildren().addAll( + new Label("Options:"), + caseSensitiveCheck, + wholeWordCheck + ); + + // Results section + VBox resultsSection = new VBox(5); + Label resultsLabel = new Label("Results:"); + + resultsListView.setCellFactory(lv -> new SearchResultCell()); + resultsListView.setPlaceholder(new Label("No search results")); + resultsListView.getSelectionModel().selectedItemProperty().addListener( + (observable, oldValue, newValue) -> { + if (newValue != null) { + navigateToResult(newValue); + } + } + ); + + VBox.setVgrow(resultsListView, Priority.ALWAYS); + + // Status section + HBox statusBox = new HBox(5); + statusBox.setAlignment(Pos.CENTER_LEFT); + progressIndicator.setMaxSize(20, 20); + progressIndicator.setVisible(false); + statusBox.getChildren().addAll(progressIndicator, statusLabel); + + resultsSection.getChildren().addAll(resultsLabel, resultsListView, statusBox); + + // Add all sections to main panel + getChildren().addAll( + searchInputSection, + new Separator(), + optionsSection, + new Separator(), + resultsSection + ); + } + + private void setupBindings() { + // Listen for document changes + model.document.addListener((observable, oldDoc, newDoc) -> { + handleDocumentChange(newDoc); + }); + + // Initialize if document already loaded + if (model.document.get() != null) { + handleDocumentChange(model.document.get()); + } + + // Disable search when no document + searchButton.disableProperty().bind(model.document.isNull()); + searchField.disableProperty().bind(model.document.isNull()); + } + + private void handleDocumentChange(Document newDoc) { + clearSearch(); + + // TODO: Implement DocumentSearchController for JavaFX + // DocumentSearchController is an interface - need to port the implementation + // from viewer-awt (DocumentSearchControllerImpl) or create a simpler version + searchController = null; + } + + private void performSearch() { + String searchText = searchField.getText(); + if (searchText == null || searchText.trim().isEmpty()) { + statusLabel.setText("Please enter search text"); + return; + } + + // TODO: Implement search functionality + // For now, just show a placeholder message + statusLabel.setText("Search not yet implemented - pending DocumentSearchController port"); + logger.info("Search requested for: '" + searchText + "' (not yet implemented)"); + + // Future implementation will: + // 1. Port DocumentSearchControllerImpl from viewer-awt or create JavaFX-specific version + // 2. Perform background search using Task + // 3. Display results in resultsListView + // 4. Navigate to results on click + // 5. Highlight matches in document view + } + + private void clearSearch() { + searchField.clear(); + resultsListView.getItems().clear(); + statusLabel.setText(""); + + if (currentSearchTask != null && currentSearchTask.isRunning()) { + currentSearchTask.cancel(); + } + } + + private void navigateToResult(SearchResultItem result) { + if (result != null) { + // Navigate to the page containing the result + model.currentPage.set(result.getPageNumber()); + + // TODO: Highlight the search hit on the page + // This will require integration with the page view + logger.info("Navigated to search result on page " + result.getPageNumber()); + } + } + + /** + * Represents a single search result item. + */ + private static class SearchResultItem { + private final int pageNumber; + private final String preview; + private final org.icepdf.core.pobjects.graphics.text.WordText wordText; + + public SearchResultItem(int pageNumber, String preview, + org.icepdf.core.pobjects.graphics.text.WordText wordText) { + this.pageNumber = pageNumber; + this.preview = preview; + this.wordText = wordText; + } + + public int getPageNumber() { + return pageNumber; + } + + public String getPreview() { + return preview; + } + + public org.icepdf.core.pobjects.graphics.text.WordText getWordText() { + return wordText; + } + } + + /** + * Custom ListCell for displaying search results. + */ + private class SearchResultCell extends ListCell { + + @Override + protected void updateItem(SearchResultItem item, boolean empty) { + super.updateItem(item, empty); + + if (empty || item == null) { + setText(null); + setGraphic(null); + } else { + VBox content = new VBox(2); + + Label pageLabel = new Label("Page " + item.getPageNumber()); + pageLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 11px;"); + + Label previewLabel = new Label(item.getPreview()); + previewLabel.setWrapText(true); + previewLabel.setStyle("-fx-font-size: 10px;"); + + content.getChildren().addAll(pageLabel, previewLabel); + setGraphic(content); + } + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/SidePanelContainer.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/SidePanelContainer.java new file mode 100644 index 000000000..78a2b30ee --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/SidePanelContainer.java @@ -0,0 +1,84 @@ +package org.icepdf.fx.ri.ui.sidepanel; + +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.layout.Region; +import javafx.util.Builder; +import org.icepdf.fx.ri.viewer.ViewerModel; + +/** + * Container for all side panels in the JavaFX viewer. + * Hosts panels in a TabPane that can be placed on the left or right side of the main view. + * + *

Panels included:

+ *
    + *
  • Thumbnails - Page thumbnails for quick navigation
  • + *
  • Outline - Document bookmarks/outline tree
  • + *
  • Search - Text search interface
  • + *
  • Layers - Optional content layers
  • + *
  • Attachments - Embedded file attachments
  • + *
  • Annotations - Annotation summary and navigation
  • + *
  • Signatures - Digital signature information
  • + *
+ */ +public class SidePanelContainer implements Builder { + + private final ViewerModel model; + private TabPane tabPane; + + public SidePanelContainer(ViewerModel model) { + this.model = model; + } + + @Override + public Region build() { + tabPane = new TabPane(); + tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); + tabPane.setMinWidth(200); + tabPane.setPrefWidth(250); + + // Create all side panel tabs + tabPane.getTabs().addAll( + createTab("Thumbnails", new ThumbnailsPanel(model)), + createTab("Outline", new OutlinePanel(model)), + createTab("Search", new SearchPanel(model)), + createTab("Layers", new LayersPanel(model)), + createTab("Attachments", new AttachmentsPanel(model)), + createTab("Annotations", new AnnotationsPanel(model)), + createTab("Signatures", new SignaturesPanel(model)) + ); + + // Bind selected tab to model for persistence + tabPane.getSelectionModel().selectedIndexProperty() + .addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + model.selectedSidePanelIndex.set(newValue.intValue()); + } + }); + + // Restore previously selected tab + if (model.selectedSidePanelIndex.get() >= 0 && + model.selectedSidePanelIndex.get() < tabPane.getTabs().size()) { + tabPane.getSelectionModel().select(model.selectedSidePanelIndex.get()); + } + + return tabPane; + } + + /** + * Creates a tab with the specified title and content panel. + */ + private Tab createTab(String title, Region content) { + Tab tab = new Tab(title); + tab.setContent(content); + return tab; + } + + /** + * Gets the TabPane component for direct manipulation if needed. + */ + public TabPane getTabPane() { + return tabPane; + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/SignaturesPanel.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/SignaturesPanel.java new file mode 100644 index 000000000..c77dfde59 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/SignaturesPanel.java @@ -0,0 +1,248 @@ +package org.icepdf.fx.ri.ui.sidepanel; + +import javafx.geometry.Insets; +import javafx.scene.control.*; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import org.icepdf.core.pobjects.Document; +import org.icepdf.core.pobjects.acroform.InteractiveForm; +import org.icepdf.core.pobjects.annotations.SignatureWidgetAnnotation; +import org.icepdf.fx.ri.viewer.ViewerModel; + +import java.util.List; +import java.util.logging.Logger; + +/** + * Panel for viewing digital signature information in the PDF document. + */ +public class SignaturesPanel extends VBox { + + private static final Logger logger = Logger.getLogger(SignaturesPanel.class.getName()); + + private final ViewerModel model; + private final ListView signaturesListView; + private final Button detailsButton; + private final Label emptyLabel; + + public SignaturesPanel(ViewerModel model) { + this.model = model; + this.signaturesListView = new ListView<>(); + this.detailsButton = new Button("View Details"); + this.emptyLabel = new Label("No signatures"); + + initializeUI(); + setupBindings(); + } + + private void initializeUI() { + setSpacing(5); + setPadding(new Insets(5)); + + // Configure ListView + signaturesListView.setCellFactory(lv -> new SignatureCell()); + signaturesListView.setPlaceholder(emptyLabel); + + VBox.setVgrow(signaturesListView, Priority.ALWAYS); + + // Details button + detailsButton.setMaxWidth(Double.MAX_VALUE); + detailsButton.setOnAction(e -> showSignatureDetails()); + detailsButton.disableProperty().bind( + signaturesListView.getSelectionModel().selectedItemProperty().isNull() + ); + + getChildren().addAll(signaturesListView, detailsButton); + } + + private void setupBindings() { + // Listen for document changes + model.document.addListener((observable, oldDoc, newDoc) -> { + handleDocumentChange(newDoc); + }); + + // Load signatures if document already exists + if (model.document.get() != null) { + handleDocumentChange(model.document.get()); + } + } + + private void handleDocumentChange(Document newDoc) { + signaturesListView.getItems().clear(); + + if (newDoc != null) { + try { + // Get interactive form + org.icepdf.core.pobjects.Catalog catalog = newDoc.getCatalog(); + if (catalog != null) { + InteractiveForm form = catalog.getInteractiveForm(); + if (form != null) { + // Get signature fields + List fields = form.getFields(); + if (fields != null) { + for (Object field : fields) { + if (field instanceof SignatureWidgetAnnotation) { + SignatureWidgetAnnotation sigWidget = + (SignatureWidgetAnnotation) field; + SignatureInfo info = createSignatureInfo(sigWidget); + if (info != null) { + signaturesListView.getItems().add(info); + } + } + } + } + } + } + } catch (Exception e) { + logger.warning("Error loading signatures: " + e.getMessage()); + } + } + } + + private SignatureInfo createSignatureInfo(SignatureWidgetAnnotation annotation) { + try { + String name = annotation.getName(); + if (name == null || name.trim().isEmpty()) { + name = "Unnamed Signature"; + } + + // Try to get signer info + String signer = "Unknown"; + String reason = ""; + String location = ""; + boolean isValid = false; + + try { + // Check if signature has been applied + if (annotation.hasSignatureDictionary()) { + org.icepdf.core.pobjects.acroform.SignatureDictionary sigDict = + annotation.getSignatureDictionary(); + + if (sigDict != null) { + // Try to validate signature + try { + var validator = annotation.getSignatureValidator(); + if (validator != null) { + // Check if signature data hasn't been modified + isValid = !validator.isSignedDataModified(); + // Additional info could be extracted from validator + // e.g., validator.isCertificateChainTrusted(), etc. + } + } catch (Exception e) { + logger.fine("Could not validate signature: " + e.getMessage()); + } + } + } else { + // Unsigned signature field + logger.fine("Signature field not yet signed"); + } + + } catch (Exception e) { + logger.fine("Could not extract full signature details: " + e.getMessage()); + } + + return new SignatureInfo(name, signer, reason, location, isValid); + + } catch (Exception e) { + logger.warning("Error creating signature info: " + e.getMessage()); + return null; + } + } + + private void showSignatureDetails() { + SignatureInfo selected = signaturesListView.getSelectionModel().getSelectedItem(); + if (selected == null) return; + + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle("Signature Details"); + alert.setHeaderText(selected.getName()); + + StringBuilder details = new StringBuilder(); + details.append("Signer: ").append(selected.getSigner()).append("\n"); + details.append("Status: ").append(selected.isValid() ? "Valid" : "Invalid/Unknown").append("\n"); + + if (selected.getReason() != null && !selected.getReason().isEmpty()) { + details.append("Reason: ").append(selected.getReason()).append("\n"); + } + + if (selected.getLocation() != null && !selected.getLocation().isEmpty()) { + details.append("Location: ").append(selected.getLocation()).append("\n"); + } + + alert.setContentText(details.toString()); + alert.showAndWait(); + } + + /** + * Represents signature information. + */ + private static class SignatureInfo { + private final String name; + private final String signer; + private final String reason; + private final String location; + private final boolean valid; + + public SignatureInfo(String name, String signer, String reason, + String location, boolean valid) { + this.name = name; + this.signer = signer; + this.reason = reason; + this.location = location; + this.valid = valid; + } + + public String getName() { + return name; + } + + public String getSigner() { + return signer; + } + + public String getReason() { + return reason; + } + + public String getLocation() { + return location; + } + + public boolean isValid() { + return valid; + } + } + + /** + * Custom ListCell for displaying signature information. + */ + private class SignatureCell extends ListCell { + + @Override + protected void updateItem(SignatureInfo item, boolean empty) { + super.updateItem(item, empty); + + if (empty || item == null) { + setText(null); + setGraphic(null); + } else { + VBox content = new VBox(3); + + Label nameLabel = new Label(item.getName()); + nameLabel.setStyle("-fx-font-weight: bold;"); + + Label signerLabel = new Label("Signer: " + item.getSigner()); + signerLabel.setStyle("-fx-font-size: 10px;"); + + Label statusLabel = new Label( + "Status: " + (item.isValid() ? "Valid" : "Invalid/Unknown") + ); + statusLabel.setStyle("-fx-font-size: 10px;" + + (item.isValid() ? "-fx-text-fill: green;" : "-fx-text-fill: gray;")); + + content.getChildren().addAll(nameLabel, signerLabel, statusLabel); + setGraphic(content); + } + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/ThumbnailsPanel.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/ThumbnailsPanel.java new file mode 100644 index 000000000..258a82907 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/sidepanel/ThumbnailsPanel.java @@ -0,0 +1,277 @@ +package org.icepdf.fx.ri.ui.sidepanel; + +import javafx.application.Platform; +import javafx.concurrent.Task; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import org.icepdf.core.pobjects.Document; +import org.icepdf.core.pobjects.PDimension; +import org.icepdf.core.pobjects.Page; +import org.icepdf.core.util.GraphicsRenderingHints; +import org.icepdf.fx.ri.viewer.ViewerModel; + +import java.awt.image.BufferedImage; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +/** + * Panel displaying page thumbnails for quick navigation. + * Features lazy loading, caching, and click-to-navigate functionality. + */ +public class ThumbnailsPanel extends VBox { + + private static final Logger logger = Logger.getLogger(ThumbnailsPanel.class.getName()); + + private static final int THUMBNAIL_WIDTH = 120; + private static final int THUMBNAIL_HEIGHT = 160; + private static final float THUMBNAIL_SCALE = 0.15f; // 15% of actual page size + + private final ViewerModel model; + private final ListView thumbnailListView; + private final Map thumbnailCache; + private final Label emptyLabel; + + public ThumbnailsPanel(ViewerModel model) { + this.model = model; + this.thumbnailCache = new HashMap<>(); + this.thumbnailListView = new ListView<>(); + this.emptyLabel = new Label("No document loaded"); + + initializeUI(); + setupBindings(); + } + + private void initializeUI() { + setSpacing(5); + setPadding(new Insets(5)); + + // Configure ListView + thumbnailListView.setCellFactory(lv -> new ThumbnailCell()); + thumbnailListView.setPlaceholder(emptyLabel); + + // Select page on click + thumbnailListView.getSelectionModel().selectedItemProperty().addListener( + (observable, oldValue, newValue) -> { + if (newValue != null && newValue > 0) { + model.currentPage.set(newValue); + } + } + ); + + VBox.setVgrow(thumbnailListView, Priority.ALWAYS); + getChildren().add(thumbnailListView); + } + + private void setupBindings() { + // Listen for document changes + model.document.addListener((observable, oldDoc, newDoc) -> { + handleDocumentChange(oldDoc, newDoc); + }); + + // Highlight current page + model.currentPage.addListener((observable, oldValue, newValue) -> { + if (newValue != null && newValue.intValue() > 0) { + // Select the current page in the list without triggering navigation + Platform.runLater(() -> { + thumbnailListView.getSelectionModel().select(newValue.intValue()); + thumbnailListView.scrollTo(newValue.intValue() - 1); + }); + } + }); + } + + private void handleDocumentChange(Document oldDoc, Document newDoc) { + // Clear cache and list + thumbnailCache.clear(); + thumbnailListView.getItems().clear(); + + if (newDoc != null) { + int pageCount = newDoc.getNumberOfPages(); + // Populate list with page numbers (1-based) + for (int i = 1; i <= pageCount; i++) { + thumbnailListView.getItems().add(i); + } + } + } + + /** + * Custom ListCell for displaying page thumbnails. + */ + private class ThumbnailCell extends ListCell { + + private final VBox container; + private final ImageView imageView; + private final Label pageLabel; + private final ProgressIndicator loadingIndicator; + + public ThumbnailCell() { + container = new VBox(5); + container.setAlignment(Pos.CENTER); + container.setPadding(new Insets(5)); + + imageView = new ImageView(); + imageView.setPreserveRatio(true); + imageView.setFitWidth(THUMBNAIL_WIDTH); + imageView.setFitHeight(THUMBNAIL_HEIGHT); + imageView.setSmooth(true); + + pageLabel = new Label(); + pageLabel.setStyle("-fx-font-weight: bold;"); + + loadingIndicator = new ProgressIndicator(); + loadingIndicator.setMaxSize(40, 40); + + container.getChildren().addAll(imageView, pageLabel); + + // Style for selected cell + selectedProperty().addListener((obs, wasSelected, isSelected) -> { + if (isSelected) { + container.setStyle("-fx-background-color: -fx-accent; -fx-background-radius: 5;"); + pageLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: white;"); + } else { + container.setStyle("-fx-background-color: transparent;"); + pageLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: -fx-text-base-color;"); + } + }); + } + + @Override + protected void updateItem(Integer pageNum, boolean empty) { + super.updateItem(pageNum, empty); + + if (empty || pageNum == null) { + setGraphic(null); + } else { + pageLabel.setText("Page " + pageNum); + + // Check cache first + Image cachedImage = thumbnailCache.get(pageNum); + if (cachedImage != null) { + imageView.setImage(cachedImage); + container.getChildren().setAll(imageView, pageLabel); + } else { + // Show loading indicator + container.getChildren().setAll(loadingIndicator, pageLabel); + loadThumbnailAsync(pageNum); + } + + setGraphic(container); + } + } + + /** + * Loads a thumbnail asynchronously to avoid blocking the UI thread. + */ + private void loadThumbnailAsync(int pageNum) { + Document doc = model.document.get(); + if (doc == null) return; + + Task loadTask = new Task<>() { + @Override + protected Image call() throws Exception { + try { + // Get the page (0-based index) + Page page = doc.getPageTree().getPage(pageNum - 1); + if (page == null) return null; + + // Initialize the page if needed + page.init(); + + // Calculate dimensions based on page size + PDimension pageSize = page.getSize(Page.BOUNDARY_CROPBOX, 0f, 1.0f); + float pageWidth = (float) pageSize.getWidth(); + float pageHeight = (float) pageSize.getHeight(); + + // Scale to thumbnail size while maintaining aspect ratio + float scale = Math.min( + THUMBNAIL_WIDTH / pageWidth, + THUMBNAIL_HEIGHT / pageHeight + ); + + int thumbWidth = (int) (pageWidth * scale); + int thumbHeight = (int) (pageHeight * scale); + + // Render the page to a BufferedImage + BufferedImage bufferedImage = new BufferedImage( + thumbWidth, thumbHeight, BufferedImage.TYPE_INT_RGB + ); + java.awt.Graphics2D g2d = bufferedImage.createGraphics(); + + // Set rendering hints for better quality + g2d.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION, + java.awt.RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g2d.setRenderingHint(java.awt.RenderingHints.KEY_RENDERING, + java.awt.RenderingHints.VALUE_RENDER_QUALITY); + + // Paint the page + page.paint(g2d, GraphicsRenderingHints.PRINT, + Page.BOUNDARY_CROPBOX, 0, scale); + + g2d.dispose(); + + // Convert BufferedImage to JavaFX Image + Image fxImage = convertToFxImage(bufferedImage); + + return fxImage; + } catch (Exception e) { + logger.warning("Failed to load thumbnail for page " + pageNum + ": " + e.getMessage()); + return null; + } + } + }; + + loadTask.setOnSucceeded(event -> { + Image image = loadTask.getValue(); + if (image != null) { + thumbnailCache.put(pageNum, image); + imageView.setImage(image); + container.getChildren().setAll(imageView, pageLabel); + } else { + // Show error placeholder + Label errorLabel = new Label("Failed to load"); + errorLabel.setStyle("-fx-text-fill: red;"); + container.getChildren().setAll(errorLabel, pageLabel); + } + }); + + loadTask.setOnFailed(event -> { + logger.warning("Thumbnail load task failed: " + loadTask.getException()); + Label errorLabel = new Label("Error"); + errorLabel.setStyle("-fx-text-fill: red;"); + container.getChildren().setAll(errorLabel, pageLabel); + }); + + // Run in background thread + new Thread(loadTask).start(); + } + + /** + * Converts AWT BufferedImage to JavaFX Image. + */ + private Image convertToFxImage(BufferedImage bufferedImage) { + javafx.scene.image.WritableImage fxImage = new javafx.scene.image.WritableImage( + bufferedImage.getWidth(), bufferedImage.getHeight() + ); + javafx.scene.image.PixelWriter pixelWriter = fxImage.getPixelWriter(); + + for (int y = 0; y < bufferedImage.getHeight(); y++) { + for (int x = 0; x < bufferedImage.getWidth(); x++) { + int rgb = bufferedImage.getRGB(x, y); + pixelWriter.setArgb(x, y, rgb); + } + } + + return fxImage; + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/statusbar/StatusBarBuilder.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/statusbar/StatusBarBuilder.java new file mode 100644 index 000000000..35312351e --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/statusbar/StatusBarBuilder.java @@ -0,0 +1,107 @@ +package org.icepdf.fx.ri.ui.statusbar; + +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressBar; +import javafx.scene.control.Separator; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.util.Builder; +import org.icepdf.fx.ri.viewer.ViewerModel; + +/** + * Builder for the status bar. + * Displays current page, total pages, zoom level, status messages, and loading progress. + */ +public class StatusBarBuilder implements Builder { + + private final ViewerModel model; + + public StatusBarBuilder(ViewerModel model) { + this.model = model; + } + + @Override + public HBox build() { + HBox statusBar = new HBox(10); + statusBar.setPadding(new Insets(5, 10, 5, 10)); + statusBar.setStyle("-fx-background-color: -fx-background; -fx-border-color: -fx-box-border; -fx-border-width:" + + " 1 0 0 0;"); + + // Status message label (left side, takes available space) + Label statusLabel = new Label(); + statusLabel.textProperty().bind(model.statusMessage); + statusLabel.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(statusLabel, Priority.ALWAYS); + + // Page information + Label pageInfo = new Label(); + pageInfo.textProperty().bind( + javafx.beans.binding.Bindings.createStringBinding( + () -> { + if (model.document.get() == null) { + return "No document"; + } + return String.format("Page %d of %d", + model.currentPage.get(), + model.totalPages.get()); + }, + model.document, + model.currentPage, + model.totalPages + ) + ); + pageInfo.setMinWidth(120); + + // Zoom information + Label zoomInfo = new Label(); + zoomInfo.textProperty().bind( + javafx.beans.binding.Bindings.concat( + model.zoomLevel.multiply(100).asString("%.0f"), + "%" + ) + ); + zoomInfo.setMinWidth(60); + + // Document title (optional, middle section) + Label documentTitle = new Label(); + documentTitle.textProperty().bind(model.documentTitle); + documentTitle.setStyle("-fx-font-weight: bold;"); + documentTitle.visibleProperty().bind(model.documentTitle.isNotEmpty()); + documentTitle.managedProperty().bind(documentTitle.visibleProperty()); + + // Loading progress indicator + ProgressBar progressBar = new ProgressBar(); + progressBar.setPrefWidth(100); + progressBar.progressProperty().bind(model.loadingProgress); + progressBar.visibleProperty().bind(model.isLoading); + progressBar.managedProperty().bind(progressBar.visibleProperty()); + + // Assembly + statusBar.getChildren().addAll( + statusLabel, + createSpacer(), + documentTitle, + createSeparator(), + pageInfo, + createSeparator(), + zoomInfo, + progressBar + ); + + return statusBar; + } + + private Region createSpacer() { + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.SOMETIMES); + return spacer; + } + + private Separator createSeparator() { + return new Separator(Orientation.VERTICAL); + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/toolbar/ToolBarBuilder.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/toolbar/ToolBarBuilder.java new file mode 100644 index 000000000..dcf769d7a --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/ui/toolbar/ToolBarBuilder.java @@ -0,0 +1,208 @@ +package org.icepdf.fx.ri.ui.toolbar; + +import javafx.scene.control.*; +import javafx.scene.layout.Region; +import javafx.stage.Window; +import javafx.util.Builder; +import org.icepdf.fx.ri.ui.common.NavigationCommands; +import org.icepdf.fx.ri.viewer.Interactor; +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.document.OpenFileCommand; +import org.icepdf.fx.ri.viewer.commands.view.ZoomInCommand; +import org.icepdf.fx.ri.viewer.commands.view.ZoomOutCommand; +import org.icepdf.fx.ri.views.DocumentViewPane; + +/** + * Builder for the main toolbar. + * Creates toolbar with file operations, navigation, zoom, rotation, and view mode controls. + */ +public class ToolBarBuilder implements Builder { + + private final ViewerModel model; + private final Interactor interactor; + private final Window window; + private final DocumentViewPane documentViewPane; + + public ToolBarBuilder(ViewerModel model, Interactor interactor, Window window, DocumentViewPane documentViewPane) { + this.model = model; + this.interactor = interactor; + this.window = window; + this.documentViewPane = documentViewPane; + } + + @Override + public ToolBar build() { + ToolBar toolBar = new ToolBar(); + + toolBar.getItems().addAll( + createFileTools() + ); + toolBar.getItems().add(new Separator()); + toolBar.getItems().addAll( + createNavigationTools() + ); + toolBar.getItems().add(new Separator()); + toolBar.getItems().addAll( + createZoomTools() + ); + toolBar.getItems().add(new Separator()); + toolBar.getItems().addAll( + createRotationTools() + ); + toolBar.getItems().add(new Separator()); + toolBar.getItems().addAll( + createViewModeTools() + ); + + return toolBar; + } + + private Button[] createFileTools() { + Button open = createButton("Open", "Open Document"); + open.setOnAction(e -> new OpenFileCommand(window, model).execute()); + + Button print = createButton("Print", "Print Document"); + print.disableProperty().bind(model.document.isNull()); + print.setOnAction(e -> model.statusMessage.set("Print not yet implemented")); + + return new Button[]{open, print}; + } + + private Region[] createNavigationTools() { + Button first = createButton("|◀", "First Page"); + first.disableProperty().bind(model.document.isNull() + .or(model.currentPage.isEqualTo(1))); + first.setOnAction(e -> NavigationCommands.firstPage(model)); + + Button previous = createButton("◀", "Previous Page"); + previous.disableProperty().bind(model.document.isNull() + .or(model.currentPage.isEqualTo(1))); + previous.setOnAction(e -> NavigationCommands.previousPage(model)); + + // Page number field + TextField pageField = new TextField(); + pageField.setPrefColumnCount(4); + pageField.setPromptText("Page"); + pageField.textProperty().bind(model.currentPage.asString()); + pageField.setTooltip(new Tooltip("Current Page")); + pageField.disableProperty().bind(model.document.isNull()); + + // Total pages label + Label totalLabel = new Label(); + totalLabel.textProperty().bind(javafx.beans.binding.Bindings.concat(" / ", model.totalPages.asString())); + + Button next = createButton("▶", "Next Page"); + next.disableProperty().bind(model.document.isNull() + .or(model.currentPage.greaterThanOrEqualTo(model.totalPages))); + next.setOnAction(e -> NavigationCommands.nextPage(model)); + + Button last = createButton("▶|", "Last Page"); + last.disableProperty().bind(model.document.isNull() + .or(model.currentPage.greaterThanOrEqualTo(model.totalPages))); + last.setOnAction(e -> NavigationCommands.lastPage(model)); + + return new Region[]{first, previous, pageField, totalLabel, next, last}; + } + + private Region[] createZoomTools() { + Button zoomOut = createButton("−", "Zoom Out"); + zoomOut.disableProperty().bind(model.document.isNull()); + zoomOut.setOnAction(e -> new ZoomOutCommand(documentViewPane, model).execute()); + + Button zoomIn = createButton("+", "Zoom In"); + zoomIn.disableProperty().bind(model.document.isNull()); + zoomIn.setOnAction(e -> new ZoomInCommand(documentViewPane, model).execute()); + + // Zoom level display + Label zoomLabel = new Label(); + zoomLabel.textProperty().bind( + javafx.beans.binding.Bindings.concat( + model.zoomLevel.multiply(100).asString("%.0f"), + "%" + ) + ); + zoomLabel.setTooltip(new Tooltip("Current Zoom Level")); + + // Fit width button + Button fitWidth = createButton("⬌", "Fit Width"); + fitWidth.disableProperty().bind(model.document.isNull()); + fitWidth.setOnAction(e -> { + model.fitMode.set(ViewerModel.FitMode.FIT_WIDTH); + model.statusMessage.set("Fit Width not yet implemented"); + }); + + // Fit page button + Button fitPage = createButton("⬚", "Fit Page"); + fitPage.disableProperty().bind(model.document.isNull()); + fitPage.setOnAction(e -> { + model.fitMode.set(ViewerModel.FitMode.FIT_PAGE); + model.statusMessage.set("Fit Page not yet implemented"); + }); + + return new Region[]{zoomOut, zoomLabel, zoomIn, fitWidth, fitPage}; + } + + private Button[] createRotationTools() { + Button rotateLeft = createButton("↶", "Rotate Left (90°)"); + rotateLeft.disableProperty().bind(model.document.isNull()); + rotateLeft.setOnAction(e -> { + double current = model.rotationAngle.get(); + model.rotationAngle.set((current - 90 + 360) % 360); + model.statusMessage.set("Rotated left"); + }); + + Button rotateRight = createButton("↷", "Rotate Right (90°)"); + rotateRight.disableProperty().bind(model.document.isNull()); + rotateRight.setOnAction(e -> { + double current = model.rotationAngle.get(); + model.rotationAngle.set((current + 90) % 360); + model.statusMessage.set("Rotated right"); + }); + + return new Button[]{rotateLeft, rotateRight}; + } + + private ToggleButton[] createViewModeTools() { + ToggleGroup viewModeGroup = new ToggleGroup(); + + ToggleButton singlePage = createToggleButton("☰", "Single Page", viewModeGroup); + singlePage.setSelected(true); + singlePage.disableProperty().bind(model.document.isNull()); + singlePage.setOnAction(e -> { + model.viewMode.set(ViewerModel.ViewMode.SINGLE_PAGE); + model.statusMessage.set("Single Page view"); + }); + + ToggleButton continuous = createToggleButton("≡", "Continuous", viewModeGroup); + continuous.disableProperty().bind(model.document.isNull()); + continuous.setOnAction(e -> { + model.viewMode.set(ViewerModel.ViewMode.CONTINUOUS); + model.statusMessage.set("Continuous view"); + }); + + ToggleButton facing = createToggleButton("⚏", "Facing Pages", viewModeGroup); + facing.disableProperty().bind(model.document.isNull()); + facing.setOnAction(e -> { + model.viewMode.set(ViewerModel.ViewMode.FACING_PAGES); + model.statusMessage.set("Facing Pages view"); + }); + + return new ToggleButton[]{singlePage, continuous, facing}; + } + + // Helper methods + + private Button createButton(String text, String tooltip) { + Button button = new Button(text); + button.setTooltip(new Tooltip(tooltip)); + return button; + } + + private ToggleButton createToggleButton(String text, String tooltip, ToggleGroup group) { + ToggleButton button = new ToggleButton(text); + button.setTooltip(new Tooltip(tooltip)); + button.setToggleGroup(group); + return button; + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/FxController.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/FxController.java index eb2cee80d..44a03b528 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/FxController.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/FxController.java @@ -1,6 +1,7 @@ package org.icepdf.fx.ri.viewer; import javafx.scene.layout.Region; +import javafx.stage.Window; import org.icepdf.fx.ri.viewer.listeners.DocumentChangeListener; /** @@ -10,12 +11,12 @@ public class FxController { private final ViewerModel model; private final Interactor interactor; - private final ViewBuilder viewBuilder; + private ViewBuilder viewBuilder; + private Window window; public FxController() { this.model = new ViewerModel(); - this.interactor = new Interactor(model); // is this really a mediator? - this.viewBuilder = new ViewBuilder(model); + this.interactor = new Interactor(model); // auto clean up this viewer if the document changes this.model.document.addListener(new DocumentChangeListener(model)); @@ -25,7 +26,16 @@ public ViewerModel getModel() { return model; } + public Region getView(Window window) { + this.window = window; + this.viewBuilder = new ViewBuilder(model, interactor, window); + return viewBuilder.build(); + } + public Region getView() { + if (viewBuilder == null) { + throw new IllegalStateException("Must call getView(Window) first"); + } return viewBuilder.build(); } } diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewBuilder.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewBuilder.java index 56e729b14..233dff10d 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewBuilder.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewBuilder.java @@ -1,15 +1,15 @@ package org.icepdf.fx.ri.viewer; -import javafx.scene.Node; -import javafx.scene.control.Button; -import javafx.scene.control.ToolBar; +import javafx.scene.control.SplitPane; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; import javafx.stage.Window; import javafx.util.Builder; -import org.icepdf.fx.ri.viewer.commands.OpenFileCommand; -import org.icepdf.fx.ri.viewer.commands.ZoomInCommand; -import org.icepdf.fx.ri.viewer.commands.ZoomOutCommand; +import org.icepdf.fx.ri.ui.menubar.MenuBarBuilder; +import org.icepdf.fx.ri.ui.sidepanel.SidePanelContainer; +import org.icepdf.fx.ri.ui.statusbar.StatusBarBuilder; +import org.icepdf.fx.ri.ui.toolbar.ToolBarBuilder; import org.icepdf.fx.ri.views.DocumentViewPane; import java.util.function.Consumer; @@ -17,43 +17,65 @@ public class ViewBuilder implements Builder { private final ViewerModel model; + private final Interactor interactor; + private final Window window; private Consumer openDocumentActionHandler; DocumentViewPane documentViewPane; - public ViewBuilder(ViewerModel model) { - + public ViewBuilder(ViewerModel model, Interactor interactor, Window window) { this.model = model; + this.interactor = interactor; + this.window = window; } @Override public Region build() { - BorderPane borderPane = new BorderPane(); - borderPane.setTop(createToolbar()); + BorderPane root = new BorderPane(); + + // Create document view pane (needed by builders) documentViewPane = new DocumentViewPane(model); - borderPane.setCenter(documentViewPane); - return borderPane; + + // Create builders + MenuBarBuilder menuBarBuilder = new MenuBarBuilder(model, interactor, window, documentViewPane); + ToolBarBuilder toolBarBuilder = new ToolBarBuilder(model, interactor, window, documentViewPane); + StatusBarBuilder statusBarBuilder = new StatusBarBuilder(model); + SidePanelContainer sidePanelContainer = new SidePanelContainer(model); + + // Top: Menu + Toolbar + VBox topContainer = new VBox(); + topContainer.getChildren().addAll( + menuBarBuilder.build(), + toolBarBuilder.build() + ); + topContainer.visibleProperty().bind(model.menuBarVisible.or(model.toolBarVisible)); + topContainer.managedProperty().bind(topContainer.visibleProperty()); + root.setTop(topContainer); + + // Center: SplitPane with side panel and document view + SplitPane centerPane = new SplitPane(); + Region sidePanel = sidePanelContainer.build(); + + // Bind side panel visibility + sidePanel.visibleProperty().bind(model.leftPanelVisible); + sidePanel.managedProperty().bind(sidePanel.visibleProperty()); + + centerPane.getItems().addAll(sidePanel, documentViewPane); + centerPane.setDividerPositions(0.2); // 20% for side panel, 80% for document + + root.setCenter(centerPane); + + // Bottom: Status bar + Region statusBar = statusBarBuilder.build(); + statusBar.visibleProperty().bind(model.statusBarVisible); + statusBar.managedProperty().bind(statusBar.visibleProperty()); + root.setBottom(statusBar); + + return root; } - private ToolBar createToolbar() { - Button openDocument = new Button("Open Document"); - openDocument.setOnAction(event -> { - Node source = (Node) event.getSource(); - Window stage = source.getScene().getWindow(); - new OpenFileCommand(stage, model).execute(); - }); - - Button zoomOut = new Button("Zoom Out"); - zoomOut.setOnAction(event -> new ZoomOutCommand(documentViewPane, model).execute()); - zoomOut.disableProperty().bind(model.toolbarDisabled); - - Button zoomIn = new Button("Zoom In"); - zoomIn.setOnAction(event -> new ZoomInCommand(documentViewPane, model).execute()); - zoomIn.disableProperty().bind(model.toolbarDisabled); - - ToolBar toolbar = new ToolBar(); - toolbar.getItems().addAll(openDocument, zoomOut, zoomIn); - return toolbar; + public DocumentViewPane getDocumentViewPane() { + return documentViewPane; } } diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerModel.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerModel.java index 9fa57207e..e1e667df6 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerModel.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerModel.java @@ -5,22 +5,79 @@ public class ViewerModel { + // Window management public final BooleanProperty useSingleViewerStage = new SimpleBooleanProperty(false); + // Document properties public final ObjectProperty document; - - // todo, pretty sure we don't need this property: file/url path public final StringProperty filePath; + public final StringProperty documentTitle = new SimpleStringProperty(""); + public final LongProperty documentSizeBytes = new SimpleLongProperty(0); + + // Page navigation properties + public final IntegerProperty currentPage = new SimpleIntegerProperty(1); + public final IntegerProperty totalPages = new SimpleIntegerProperty(0); - // toolbar disabled state - public BooleanProperty toolbarDisabled; + // View properties + public final DoubleProperty zoomLevel = new SimpleDoubleProperty(1.0); + public final DoubleProperty rotationAngle = new SimpleDoubleProperty(0.0); + public final ObjectProperty viewMode = new SimpleObjectProperty<>(ViewMode.SINGLE_PAGE); + public final ObjectProperty fitMode = new SimpleObjectProperty<>(FitMode.NONE); - // zoom factor increment + // zoom factor increment (for zoom in/out commands) public final FloatProperty zoomFactorIncrement = new SimpleFloatProperty(0.1f); + // UI state properties + public final BooleanProperty leftPanelVisible = new SimpleBooleanProperty(true); + public final BooleanProperty rightPanelVisible = new SimpleBooleanProperty(false); + public final BooleanProperty statusBarVisible = new SimpleBooleanProperty(true); + public final BooleanProperty toolBarVisible = new SimpleBooleanProperty(true); + public final BooleanProperty menuBarVisible = new SimpleBooleanProperty(true); + public final IntegerProperty selectedSidePanelIndex = new SimpleIntegerProperty(0); + + // toolbar disabled state (legacy - kept for compatibility) + public final BooleanProperty toolbarDisabled; + + // Operation properties + public final StringProperty statusMessage = new SimpleStringProperty(""); + public final DoubleProperty loadingProgress = new SimpleDoubleProperty(0.0); + public final BooleanProperty isLoading = new SimpleBooleanProperty(false); + + // Selection properties + public final StringProperty selectedText = new SimpleStringProperty(""); + + // View mode enums + public enum ViewMode { + SINGLE_PAGE, + CONTINUOUS, + FACING_PAGES, + CONTINUOUS_FACING + } + + public enum FitMode { + NONE, + FIT_WIDTH, + FIT_HEIGHT, + FIT_PAGE, + ACTUAL_SIZE + } + public ViewerModel() { document = new SimpleObjectProperty<>(null); filePath = new SimpleStringProperty(null); toolbarDisabled = new SimpleBooleanProperty(true); + + // Update toolbar disabled when document changes + document.addListener((observable, oldValue, newValue) -> { + // todo, not needed can just check if document is null + toolbarDisabled.set(newValue == null); + if (newValue != null) { + totalPages.set(newValue.getNumberOfPages()); + currentPage.set(1); + } else { + totalPages.set(0); + currentPage.set(1); + } + }); } } diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerStageManager.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerStageManager.java index 005506d35..63ccec300 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerStageManager.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerStageManager.java @@ -17,7 +17,7 @@ private ViewerStageManager() { public void createViewer(Stage stage, Document document) { FxController controller = new FxController(); - Scene scene = new Scene(controller.getView(), 1024, 768); + Scene scene = new Scene(controller.getView(stage), 1024, 768); controller.getModel().document.set(document); stage.setScene(scene); stage.setOnCloseRequest(new StageCloseRequestListener(controller.getModel())); diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/CloseDocumentCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/CloseDocumentCommand.java new file mode 100644 index 000000000..b13cb3de6 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/CloseDocumentCommand.java @@ -0,0 +1,41 @@ +package org.icepdf.fx.ri.viewer.commands.document; + +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.Command; + +/** + * Command to close the currently open document. + */ +public class CloseDocumentCommand implements Command { + + private final ViewerModel model; + + public CloseDocumentCommand(ViewerModel model) { + this.model = model; + } + + @Override + public void execute() { + if (model.document.get() != null) { + // Dispose the document + model.document.get().dispose(); + + // Clear model state + model.document.set(null); + model.filePath.set(null); + model.documentTitle.set(""); + model.documentSizeBytes.set(0); + model.currentPage.set(1); + model.totalPages.set(0); + model.selectedText.set(""); + + // Reset view state + model.zoomLevel.set(1.0); + model.rotationAngle.set(0.0); + model.fitMode.set(ViewerModel.FitMode.NONE); + + model.statusMessage.set("Document closed"); + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/DocumentPropertiesCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/DocumentPropertiesCommand.java new file mode 100644 index 000000000..e918f7ed1 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/DocumentPropertiesCommand.java @@ -0,0 +1,32 @@ +package org.icepdf.fx.ri.viewer.commands.document; + +import javafx.stage.Window; +import org.icepdf.fx.ri.ui.dialogs.DocumentPropertiesDialog; +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.Command; + +/** + * Command to show document properties dialog. + */ +public class DocumentPropertiesCommand implements Command { + + private final Window window; + private final ViewerModel model; + + public DocumentPropertiesCommand(Window window, ViewerModel model) { + this.window = window; + this.model = model; + } + + @Override + public void execute() { + if (model.document.get() == null) { + model.statusMessage.set("No document to show properties"); + return; + } + + DocumentPropertiesDialog dialog = new DocumentPropertiesDialog(model, window); + dialog.showAndWait(); + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/OpenFileCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/OpenFileCommand.java similarity index 94% rename from viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/OpenFileCommand.java rename to viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/OpenFileCommand.java index 10a4d67f4..afb1747b5 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/OpenFileCommand.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/OpenFileCommand.java @@ -1,4 +1,4 @@ -package org.icepdf.fx.ri.viewer.commands; +package org.icepdf.fx.ri.viewer.commands.document; import javafx.stage.FileChooser; import javafx.stage.Stage; @@ -7,6 +7,7 @@ import org.icepdf.core.pobjects.Document; import org.icepdf.fx.ri.viewer.ViewerModel; import org.icepdf.fx.ri.viewer.ViewerStageManager; +import org.icepdf.fx.ri.viewer.commands.Command; import java.io.File; import java.io.IOException; diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/PrintDocumentCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/PrintDocumentCommand.java new file mode 100644 index 000000000..8ed53e418 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/PrintDocumentCommand.java @@ -0,0 +1,49 @@ +package org.icepdf.fx.ri.viewer.commands.document; + +import javafx.print.PrinterJob; +import javafx.stage.Window; +import org.icepdf.fx.ri.ui.dialogs.PrintDialog; +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.Command; + +import java.util.Optional; + +/** + * Command to print the current document. + */ +public class PrintDocumentCommand implements Command { + + private final Window window; + private final ViewerModel model; + + public PrintDocumentCommand(Window window, ViewerModel model) { + this.window = window; + this.model = model; + } + + @Override + public void execute() { + if (model.document.get() == null) { + model.statusMessage.set("No document to print"); + return; + } + + PrintDialog dialog = new PrintDialog(model, window); + Optional result = dialog.showAndWait(); + + result.ifPresent(printerJob -> { + // TODO: Implement actual printing logic + // This would involve rendering pages and sending to printer + model.statusMessage.set("Printing to " + printerJob.getPrinter().getName() + "..."); + + // For now, just show a message + javafx.scene.control.Alert alert = new javafx.scene.control.Alert( + javafx.scene.control.Alert.AlertType.INFORMATION, + "Print job configured. Actual printing not yet implemented.", + javafx.scene.control.ButtonType.OK + ); + alert.showAndWait(); + }); + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/SaveDocumentCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/SaveDocumentCommand.java new file mode 100644 index 000000000..392e10f5e --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/SaveDocumentCommand.java @@ -0,0 +1,51 @@ +package org.icepdf.fx.ri.viewer.commands.document; + +import javafx.stage.FileChooser; +import javafx.stage.Window; +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.Command; + +import java.io.File; + +/** + * Command to save the current document. + * Placeholder implementation - to be completed. + */ +public class SaveDocumentCommand implements Command { + + private final Window window; + private final ViewerModel model; + + public SaveDocumentCommand(Window window, ViewerModel model) { + this.window = window; + this.model = model; + } + + @Override + public void execute() { + if (model.document.get() == null) { + model.statusMessage.set("No document to save"); + return; + } + + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Save PDF Document"); + fileChooser.getExtensionFilters().add( + new FileChooser.ExtensionFilter("PDF Files", "*.pdf") + ); + + // Set initial file name from current file path + if (model.filePath.get() != null) { + File currentFile = new File(model.filePath.get()); + fileChooser.setInitialDirectory(currentFile.getParentFile()); + fileChooser.setInitialFileName(currentFile.getName()); + } + + File file = fileChooser.showSaveDialog(window); + if (file != null) { + // TODO: Implement actual save logic + model.statusMessage.set("Save functionality not yet implemented: " + file.getName()); + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/SearchCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/SearchCommand.java new file mode 100644 index 000000000..3ffb2e33b --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/SearchCommand.java @@ -0,0 +1,37 @@ +package org.icepdf.fx.ri.viewer.commands.document; + +import javafx.stage.Window; +import org.icepdf.fx.ri.ui.dialogs.SearchDialog; +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.Command; + +/** + * Command to open the search dialog. + */ +public class SearchCommand implements Command { + + private final ViewerModel model; + private final Window owner; + private SearchDialog searchDialog; + + public SearchCommand(ViewerModel model, Window owner) { + this.model = model; + this.owner = owner; + } + + @Override + public void execute() { + // Reuse existing search dialog if open + if (searchDialog == null || !searchDialog.isShowing()) { + searchDialog = new SearchDialog(model, owner); + } + + if (!searchDialog.isShowing()) { + searchDialog.show(); + } else { + searchDialog.toFront(); + searchDialog.requestFocus(); + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/navigation/FirstPageCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/navigation/FirstPageCommand.java new file mode 100644 index 000000000..807b54855 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/navigation/FirstPageCommand.java @@ -0,0 +1,25 @@ +package org.icepdf.fx.ri.viewer.commands.navigation; + +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.Command; + +/** + * Command to navigate to the first page of the document. + */ +public class FirstPageCommand implements Command { + + private final ViewerModel model; + + public FirstPageCommand(ViewerModel model) { + this.model = model; + } + + @Override + public void execute() { + if (model.document.get() != null && model.totalPages.get() > 0) { + model.currentPage.set(1); + model.statusMessage.set("First page"); + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/navigation/GoToPageCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/navigation/GoToPageCommand.java new file mode 100644 index 000000000..6cd044e95 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/navigation/GoToPageCommand.java @@ -0,0 +1,32 @@ +package org.icepdf.fx.ri.viewer.commands.navigation; + +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.Command; + +/** + * Command to navigate to a specific page number. + */ +public class GoToPageCommand implements Command { + + private final ViewerModel model; + private final int pageNumber; + + public GoToPageCommand(ViewerModel model, int pageNumber) { + this.model = model; + this.pageNumber = pageNumber; + } + + @Override + public void execute() { + if (model.document.get() != null && + pageNumber >= 1 && + pageNumber <= model.totalPages.get()) { + model.currentPage.set(pageNumber); + model.statusMessage.set("Page " + pageNumber); + } else if (model.document.get() != null) { + model.statusMessage.set("Invalid page number: " + pageNumber + + " (valid range: 1-" + model.totalPages.get() + ")"); + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/navigation/LastPageCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/navigation/LastPageCommand.java new file mode 100644 index 000000000..be68b1d5b --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/navigation/LastPageCommand.java @@ -0,0 +1,25 @@ +package org.icepdf.fx.ri.viewer.commands.navigation; + +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.Command; + +/** + * Command to navigate to the last page of the document. + */ +public class LastPageCommand implements Command { + + private final ViewerModel model; + + public LastPageCommand(ViewerModel model) { + this.model = model; + } + + @Override + public void execute() { + if (model.document.get() != null && model.totalPages.get() > 0) { + model.currentPage.set(model.totalPages.get()); + model.statusMessage.set("Last page"); + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/navigation/NextPageCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/navigation/NextPageCommand.java new file mode 100644 index 000000000..6fc668eec --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/navigation/NextPageCommand.java @@ -0,0 +1,25 @@ +package org.icepdf.fx.ri.viewer.commands.navigation; + +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.Command; + +/** + * Command to navigate to the next page of the document. + */ +public class NextPageCommand implements Command { + + private final ViewerModel model; + + public NextPageCommand(ViewerModel model) { + this.model = model; + } + + @Override + public void execute() { + if (model.document.get() != null && model.currentPage.get() < model.totalPages.get()) { + model.currentPage.set(model.currentPage.get() + 1); + model.statusMessage.set("Page " + model.currentPage.get()); + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/navigation/PreviousPageCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/navigation/PreviousPageCommand.java new file mode 100644 index 000000000..5a6098ea7 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/navigation/PreviousPageCommand.java @@ -0,0 +1,25 @@ +package org.icepdf.fx.ri.viewer.commands.navigation; + +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.Command; + +/** + * Command to navigate to the previous page of the document. + */ +public class PreviousPageCommand implements Command { + + private final ViewerModel model; + + public PreviousPageCommand(ViewerModel model) { + this.model = model; + } + + @Override + public void execute() { + if (model.document.get() != null && model.currentPage.get() > 1) { + model.currentPage.set(model.currentPage.get() - 1); + model.statusMessage.set("Page " + model.currentPage.get()); + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/ActualSizeCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/ActualSizeCommand.java new file mode 100644 index 000000000..1eec655df --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/ActualSizeCommand.java @@ -0,0 +1,26 @@ +package org.icepdf.fx.ri.viewer.commands.view; + +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.Command; + +/** + * Command to set zoom to actual size (100%). + */ +public class ActualSizeCommand implements Command { + + private final ViewerModel model; + + public ActualSizeCommand(ViewerModel model) { + this.model = model; + } + + @Override + public void execute() { + if (model.document.get() != null) { + model.zoomLevel.set(1.0); + model.fitMode.set(ViewerModel.FitMode.ACTUAL_SIZE); + model.statusMessage.set("Actual Size (100%)"); + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/FitPageCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/FitPageCommand.java new file mode 100644 index 000000000..7d9fb3a5c --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/FitPageCommand.java @@ -0,0 +1,26 @@ +package org.icepdf.fx.ri.viewer.commands.view; + +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.Command; + +/** + * Command to fit the entire page in the viewport. + */ +public class FitPageCommand implements Command { + + private final ViewerModel model; + + public FitPageCommand(ViewerModel model) { + this.model = model; + } + + @Override + public void execute() { + if (model.document.get() != null) { + model.fitMode.set(ViewerModel.FitMode.FIT_PAGE); + // TODO: Calculate actual zoom level based on viewport size and page size + model.statusMessage.set("Fit Page - calculation not yet implemented"); + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/FitWidthCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/FitWidthCommand.java new file mode 100644 index 000000000..3e49b4801 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/FitWidthCommand.java @@ -0,0 +1,26 @@ +package org.icepdf.fx.ri.viewer.commands.view; + +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.Command; + +/** + * Command to fit the document to page width. + */ +public class FitWidthCommand implements Command { + + private final ViewerModel model; + + public FitWidthCommand(ViewerModel model) { + this.model = model; + } + + @Override + public void execute() { + if (model.document.get() != null) { + model.fitMode.set(ViewerModel.FitMode.FIT_WIDTH); + // TODO: Calculate actual zoom level based on viewport width and page width + model.statusMessage.set("Fit Width - calculation not yet implemented"); + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/RotateLeftCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/RotateLeftCommand.java new file mode 100644 index 000000000..f9e4d008a --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/RotateLeftCommand.java @@ -0,0 +1,27 @@ +package org.icepdf.fx.ri.viewer.commands.view; + +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.Command; + +/** + * Command to rotate the document 90 degrees counter-clockwise (left). + */ +public class RotateLeftCommand implements Command { + + private final ViewerModel model; + + public RotateLeftCommand(ViewerModel model) { + this.model = model; + } + + @Override + public void execute() { + if (model.document.get() != null) { + double current = model.rotationAngle.get(); + double newRotation = (current - 90 + 360) % 360; + model.rotationAngle.set(newRotation); + model.statusMessage.set("Rotated left (90° CCW)"); + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/RotateRightCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/RotateRightCommand.java new file mode 100644 index 000000000..561a2f409 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/RotateRightCommand.java @@ -0,0 +1,27 @@ +package org.icepdf.fx.ri.viewer.commands.view; + +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.Command; + +/** + * Command to rotate the document 90 degrees clockwise (right). + */ +public class RotateRightCommand implements Command { + + private final ViewerModel model; + + public RotateRightCommand(ViewerModel model) { + this.model = model; + } + + @Override + public void execute() { + if (model.document.get() != null) { + double current = model.rotationAngle.get(); + double newRotation = (current + 90) % 360; + model.rotationAngle.set(newRotation); + model.statusMessage.set("Rotated right (90° CW)"); + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/ZoomInCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/ZoomInCommand.java similarity index 87% rename from viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/ZoomInCommand.java rename to viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/ZoomInCommand.java index d26c5775e..9ba9b9ac7 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/ZoomInCommand.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/ZoomInCommand.java @@ -1,7 +1,8 @@ -package org.icepdf.fx.ri.viewer.commands; +package org.icepdf.fx.ri.viewer.commands.view; import javafx.beans.property.FloatProperty; import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.Command; import org.icepdf.fx.ri.views.DocumentViewPane; public class ZoomInCommand implements Command { diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/ZoomOutCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/ZoomOutCommand.java similarity index 87% rename from viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/ZoomOutCommand.java rename to viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/ZoomOutCommand.java index ee67db322..01638e80f 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/ZoomOutCommand.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/ZoomOutCommand.java @@ -1,7 +1,8 @@ -package org.icepdf.fx.ri.viewer.commands; +package org.icepdf.fx.ri.viewer.commands.view; import javafx.beans.property.FloatProperty; import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.Command; import org.icepdf.fx.ri.views.DocumentViewPane; public class ZoomOutCommand implements Command { diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/window/AboutCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/window/AboutCommand.java new file mode 100644 index 000000000..7b0f4fc52 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/window/AboutCommand.java @@ -0,0 +1,24 @@ +package org.icepdf.fx.ri.viewer.commands.window; + +import javafx.stage.Window; +import org.icepdf.fx.ri.ui.dialogs.AboutDialog; +import org.icepdf.fx.ri.viewer.commands.Command; + +/** + * Command to open the About dialog. + */ +public class AboutCommand implements Command { + + private final Window owner; + + public AboutCommand(Window owner) { + this.owner = owner; + } + + @Override + public void execute() { + AboutDialog dialog = new AboutDialog(owner); + dialog.showAndWait(); + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/window/CloseWindowCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/window/CloseWindowCommand.java new file mode 100644 index 000000000..4fe525128 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/window/CloseWindowCommand.java @@ -0,0 +1,37 @@ +package org.icepdf.fx.ri.viewer.commands.window; + +import javafx.application.Platform; +import javafx.stage.Stage; +import javafx.stage.Window; +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.Command; + +/** + * Command to close the current window. + */ +public class CloseWindowCommand implements Command { + + private final Window window; + private final ViewerModel model; + + public CloseWindowCommand(Window window, ViewerModel model) { + this.window = window; + this.model = model; + } + + @Override + public void execute() { + // Dispose document if open + if (model.document.get() != null) { + model.document.get().dispose(); + } + + // Close the window + if (window instanceof Stage) { + ((Stage) window).close(); + } else { + Platform.exit(); + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/window/MinimizeWindowCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/window/MinimizeWindowCommand.java new file mode 100644 index 000000000..a6a8ff05d --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/window/MinimizeWindowCommand.java @@ -0,0 +1,25 @@ +package org.icepdf.fx.ri.viewer.commands.window; + +import javafx.stage.Stage; +import javafx.stage.Window; +import org.icepdf.fx.ri.viewer.commands.Command; + +/** + * Command to minimize the current window. + */ +public class MinimizeWindowCommand implements Command { + + private final Window window; + + public MinimizeWindowCommand(Window window) { + this.window = window; + } + + @Override + public void execute() { + if (window instanceof Stage) { + ((Stage) window).setIconified(true); + } + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/window/NewWindowCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/window/NewWindowCommand.java new file mode 100644 index 000000000..eeb2de169 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/window/NewWindowCommand.java @@ -0,0 +1,30 @@ +package org.icepdf.fx.ri.viewer.commands.window; + +import javafx.stage.Stage; +import org.icepdf.core.pobjects.Document; +import org.icepdf.fx.ri.viewer.ViewerStageManager; +import org.icepdf.fx.ri.viewer.commands.Command; + +/** + * Command to create a new viewer window. + */ +public class NewWindowCommand implements Command { + + private final Document document; + + public NewWindowCommand() { + this.document = null; + } + + public NewWindowCommand(Document document) { + this.document = document; + } + + @Override + public void execute() { + ViewerStageManager stageManager = ViewerStageManager.getInstance(); + Stage newStage = stageManager.createViewerStage(document); + newStage.show(); + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/window/PreferencesCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/window/PreferencesCommand.java new file mode 100644 index 000000000..55451cde7 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/window/PreferencesCommand.java @@ -0,0 +1,27 @@ +package org.icepdf.fx.ri.viewer.commands.window; + +import javafx.stage.Window; +import org.icepdf.fx.ri.ui.dialogs.PreferencesDialog; +import org.icepdf.fx.ri.viewer.ViewerModel; +import org.icepdf.fx.ri.viewer.commands.Command; + +/** + * Command to open the preferences/settings dialog. + */ +public class PreferencesCommand implements Command { + + private final ViewerModel model; + private final Window owner; + + public PreferencesCommand(ViewerModel model, Window owner) { + this.model = model; + this.owner = owner; + } + + @Override + public void execute() { + PreferencesDialog dialog = new PreferencesDialog(model, owner); + dialog.showAndWait(); + } +} + diff --git a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java index 61516ec6b..62742e1bd 100644 --- a/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java @@ -160,7 +160,7 @@ protected Void call() throws Exception { // fxg2.setClip(0, 0, (int) width.get(), (int) height.get()); // fxg2.scale(1, -1); // fxg2.translate(0, -height.get()); - +// // long start = System.currentTimeMillis(); // page.paintPageContent(fxg2, GraphicsRenderingHints.PRINT, rotation.get(), scale.get(), true, true); // long end = System.currentTimeMillis();