diff --git a/examples/javafx/pom.xml b/examples/javafx/pom.xml index 6dade74a4..4f8d955bf 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 4c431febe..31b7098b8 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/pom.xml b/qa/viewer-jfx/pom.xml new file mode 100644 index 000000000..592a30891 --- /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 + 21 + + + + 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.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 efd68b5c3..7e2253832 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,7 @@ include 'core:core-awt', 'core:core-fonts', '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 764f1c3eb..560e902e7 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 6033c6e05..75a85bc5e 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' application { mainClass = "org.icepdf.ri.viewer.Launcher" @@ -44,7 +44,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/README.md b/viewer/viewer-fx/README.md new file mode 100644 index 000000000..984d10c79 --- /dev/null +++ b/viewer/viewer-fx/README.md @@ -0,0 +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/build.gradle b/viewer/viewer-fx/build.gradle new file mode 100644 index 000000000..2564325d2 --- /dev/null +++ b/viewer/viewer-fx/build.gradle @@ -0,0 +1,128 @@ +plugins { + id 'application' + 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' + +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"' + + +javafx { + version = "21.0.5" + 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}" + // 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') +} + +// 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") +} + +// Archives are now automatically included via publishing plugin diff --git a/viewer/viewer-fx/pom.xml b/viewer/viewer-fx/pom.xml new file mode 100644 index 000000000..0f42a4aac --- /dev/null +++ b/viewer/viewer-fx/pom.xml @@ -0,0 +1,69 @@ + + + + com.github.pcorless.icepdf + viewer + 7.4.0-SNAPSHOT + + 4.0.0 + icepdf-viewer-fx + jar + ICEpdf :: Viewer : JavaFX Viewer RI + + ICEpdf JavaFX reference implementation. + + + + + org.openjfx + javafx-controls + 21 + + + org.openjfx + javafx-swing + 21 + + + org.jfree + org.jfree.fxgraphics2d + 2.1 + + + com.github.pcorless.icepdf + icepdf-core + ${project.version} + + + org.junit.jupiter + junit-jupiter + 5.9.3 + test + + + org.openjfx + javafx-graphics + 23 + compile + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.3 + + + org.openjfx + javafx-maven-plugin + 0.0.8 + + org.icepdf.fx.ri.viewer.Launcher + + + + + + \ No newline at end of file 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/util/FontPropertiesManager.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/util/FontPropertiesManager.java new file mode 100644 index 000000000..8bfe9b5d1 --- /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; + +/** + * todo: figure out module issue, can't access core classes from viewer. + */ +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/FxController.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/FxController.java new file mode 100644 index 000000000..44a03b528 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/FxController.java @@ -0,0 +1,41 @@ +package org.icepdf.fx.ri.viewer; + +import javafx.scene.layout.Region; +import javafx.stage.Window; +import org.icepdf.fx.ri.viewer.listeners.DocumentChangeListener; + +/** + * Controller for the viewer application. + */ +public class FxController { + + private final ViewerModel model; + private final Interactor interactor; + private ViewBuilder viewBuilder; + private Window window; + + public FxController() { + this.model = new ViewerModel(); + this.interactor = new Interactor(model); + + // auto clean up this viewer if the document changes + this.model.document.addListener(new DocumentChangeListener(model)); + } + + 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/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..ab6d28c4d --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/Launcher.java @@ -0,0 +1,44 @@ +package org.icepdf.fx.ri.viewer; + +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; + +public class Launcher extends Application { + + private static final Logger logger = Logger.getLogger(Launcher.class.toString()); + + + 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); + } + + + @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); + } + + // 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 new file mode 100644 index 000000000..233dff10d --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewBuilder.java @@ -0,0 +1,81 @@ +package org.icepdf.fx.ri.viewer; + +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.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; + +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, Interactor interactor, Window window) { + this.model = model; + this.interactor = interactor; + this.window = window; + } + + @Override + public Region build() { + BorderPane root = new BorderPane(); + + // Create document view pane (needed by builders) + documentViewPane = new DocumentViewPane(model); + + // 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; + } + + 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 new file mode 100644 index 000000000..e1e667df6 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/ViewerModel.java @@ -0,0 +1,83 @@ +package org.icepdf.fx.ri.viewer; + +import javafx.beans.property.*; +import org.icepdf.core.pobjects.Document; + +public class ViewerModel { + + // Window management + public final BooleanProperty useSingleViewerStage = new SimpleBooleanProperty(false); + + // Document properties + public final ObjectProperty document; + 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); + + // 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 (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 new file mode 100644 index 000000000..63ccec300 --- /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(); + Scene scene = new Scene(controller.getView(stage), 1024, 768); + controller.getModel().document.set(document); + 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/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/document/OpenFileCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/OpenFileCommand.java new file mode 100644 index 000000000..afb1747b5 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/document/OpenFileCommand.java @@ -0,0 +1,55 @@ +package org.icepdf.fx.ri.viewer.commands.document; + +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 org.icepdf.fx.ri.viewer.commands.Command; + +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() { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Open Resource File"); + File file = fileChooser.showOpenDialog(stage); + if (file != null) { + // set document + try { + Document document = new Document(); + document.setFile(file.getAbsolutePath()); + + if (model.useSingleViewerStage.get() || model.document.get() == null) { + 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/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/view/ZoomInCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/ZoomInCommand.java new file mode 100644 index 000000000..9ba9b9ac7 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/ZoomInCommand.java @@ -0,0 +1,23 @@ +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 { + 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/view/ZoomOutCommand.java b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/ZoomOutCommand.java new file mode 100644 index 000000000..01638e80f --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/commands/view/ZoomOutCommand.java @@ -0,0 +1,25 @@ +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 { + 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/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/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..fdf78be86 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/listeners/DocumentChangeListener.java @@ -0,0 +1,30 @@ +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; + +/** + * 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; + + 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..b4f721db9 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/viewer/listeners/StageCloseRequestListener.java @@ -0,0 +1,28 @@ +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; + +/** + * Stage close request listener that will dispose of the document when the stage is closed. + */ +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..b451a50b1 --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/DocumentViewPane.java @@ -0,0 +1,69 @@ +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.geometry.Pos; +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 { + + private FloatProperty scale = new SimpleFloatProperty(1.0f); + private FloatProperty rotation = new SimpleFloatProperty(0.0f); + private IntegerProperty currentPageIndex = new SimpleIntegerProperty(0); + + private VBox pageLayoutPane; + private ScrollPane scrollPane; + + public DocumentViewPane(ViewerModel model) { + createLayout(model); + model.document.addListener((observable, oldValue, newValue) -> { + pageLayoutPane.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(model, i, scale, rotation, scrollPane); + pageViewPane.setBorder(new Border(new BorderStroke(null, BorderStrokeStyle.SOLID, null, + new BorderWidths(1)))); + pageLayoutPane.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(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); + + 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); + } + + 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..62742e1bd --- /dev/null +++ b/viewer/viewer-fx/src/main/java/org/icepdf/fx/ri/views/PageViewWidget.java @@ -0,0 +1,312 @@ +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.geometry.Rectangle2D; +import javafx.scene.Group; +import javafx.scene.Node; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.Region; +import javafx.scene.paint.Color; +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.fx.ri.viewer.ViewerModel; + +public class PageViewWidget extends Region { + + private FloatProperty scale; + private FloatProperty rotation; + private IntegerProperty pageIndex; + + public ObjectProperty viewportBounds; + + private DoubleProperty width; + private DoubleProperty height; + private double pageWidth; + private double pageHeight; + + private ViewerModel model; + private ScrollPane scrollPane; + + 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(); + this.scale.bind(scale); + this.rotation = new SimpleFloatProperty(); + this.rotation.bind(rotation); + this.scrollPane = scrollPane; + this.model = model; + scrollPause = new PauseTransition(SCROLL_PAUSE_DURATION); + scrollPause.setOnFinished(event -> { + draw(); + }); + + + this.viewportBounds = new SimpleObjectProperty<>(); + + + 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); + setWidth(pageWidth); + setHeight(pageHeight); + + scale.addListener((observable, oldValue, newValue) -> { + width.set(pageWidth * newValue.floatValue()); + height.set(pageHeight * newValue.floatValue()); + }); + + scrollPane.vvalueProperty().addListener((observable, oldValue, newValue) -> { + scrollPause.playFromStart(); + }); + + createLayout(); + + setOnMouseClicked(event -> { + System.out.println("Page " + pageIndex + " " + isNodeIntersectingViewport(scrollPane, this)); + }); + + scrollPane.viewportBoundsProperty().addListener((observable, oldValue, newValue) -> { + if (isNodeIntersectingViewport(scrollPane, this)) { + 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 (isNodeIntersectingViewport(scrollPane, this)) { + System.out.println("drawing page " + this.pageIndex); + + try { + 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()); + return null; + } + }; + Thread pageInitThread = new Thread(pageCaptureTask); + pageInitThread.start(); + return; + } + + 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(); + + gc.clearRect(0, 0, width.get(), height.get()); + gc.save(); + // clip testing + gc.setFill(Color.BLUE); + gc.fillRect(0, 0, 50, 50); + calculateAndDrawClip(gc); + + // 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) { + Rectangle2D rect = intersectionClip(scrollPane, this); + gc.setStroke(Color.RED); + gc.setLineWidth(5); + 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(); +// if (nodeBounds.getWidth() != width.get() || nodeBounds.getHeight() != height.get()) { +// return false; +// } + if (nodeBounds.getMinY() == 0 && pageIndex.get() > 0) { + return false; + } + 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); + +// System.out.printf("Offset: [%.1f, %.1f] width: %.1f height: %.1f %n", +// hoffset, voffset, viewportWidth, viewportHeight); + + return nodeBounds.intersects( + hoffset, + voffset, + viewportBounds.getWidth(), + viewportBounds.getHeight() + ); + } + + private Rectangle2D 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); + + 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); + } + + 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 000000000..f3c0774ba Binary files /dev/null and b/viewer/viewer-fx/src/main/resources/org/icepdf/fx/images/icepdf-app-icon-32x32.png differ diff --git a/viewer/viewer-fx/src/main/resources/org/icepdf/fx/images/icepdf-app-icon-64x64.png b/viewer/viewer-fx/src/main/resources/org/icepdf/fx/images/icepdf-app-icon-64x64.png new file mode 100644 index 000000000..a151c4e97 Binary files /dev/null and b/viewer/viewer-fx/src/main/resources/org/icepdf/fx/images/icepdf-app-icon-64x64.png differ