diff --git a/Jenkinsfile b/Jenkinsfile
index 55abed27..d2c40c39 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -4,4 +4,16 @@
def repoName = "rups"
def dependencyRegex = "itextcore"
-automaticJavaBuild(repoName, dependencyRegex, 'jdk-17-openjdk')
\ No newline at end of file
+automaticJavaBuild(repoName, dependencyRegex)
+
+pipeline {
+ agent any
+
+ stages {
+ stage('test') {
+ steps {
+ sh 'Xvfb -ac :99 -screen 0 1280x1024x16 & export DISPLAY=:99'
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 0e858285..2617ccc6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -47,7 +47,6 @@
- 3.2.0
2.1.3
7.2.5
5.9.2
@@ -57,8 +56,11 @@
RUPS
11
11
- 2.22.0
+ 3.5.0
+ 3.1.0
+ 3.1.0
0.8.9
+ 0.17.2
5.1.4
@@ -93,6 +95,11 @@
dom4j
${dom4j.version}
+
+ org.uispec4j
+ uispec4j
+ 17.0-rc1
+
com.itextpdf
pdftest
@@ -152,7 +159,14 @@
${maven-surefire-plugin.version}
false
- @{jacoco.agent.argLine}
+
+ @{jacoco.agent.argLine}
+ --add-opens=java.desktop/java.awt=ALL-UNNAMED
+ --add-opens=java.desktop/java.awt.peer=ALL-UNNAMED
+ --add-opens=java.desktop/sun.awt=ALL-UNNAMED
+ --add-opens=java.desktop/sun.awt.windows=ALL-UNNAMED
+ --add-opens=java.desktop/sun.awt.windows.WToolkit=ALL-UNNAMED
+
@@ -197,8 +211,9 @@
${maven-bundle-plugin.version}
+ org.apache.maven.plugins
maven-assembly-plugin
- ${assembly-plugin.version}
+ ${maven-assembly-plugin.version}
make-assembly
@@ -229,7 +244,9 @@
+ org.apache.maven.plugins
maven-jar-plugin
+ ${maven-jar-plugin.version}
@@ -250,6 +267,7 @@
com.github.siom79.japicmp
japicmp-maven-plugin
+ ${japicmp-maven-plugin.version}
true
diff --git a/src/main/java/com/itextpdf/rups/RupsLauncher.java b/src/main/java/com/itextpdf/rups/RupsLauncher.java
index 56954d90..a947b2ac 100644
--- a/src/main/java/com/itextpdf/rups/RupsLauncher.java
+++ b/src/main/java/com/itextpdf/rups/RupsLauncher.java
@@ -50,7 +50,6 @@ This file is part of the iText (R) project.
* of a PDF file.
*/
public class RupsLauncher {
-
/**
* Main method. Starts the RUPS application.
*
diff --git a/src/main/java/com/itextpdf/rups/controller/PdfReaderController.java b/src/main/java/com/itextpdf/rups/controller/PdfReaderController.java
index edf8fa41..609a165e 100644
--- a/src/main/java/com/itextpdf/rups/controller/PdfReaderController.java
+++ b/src/main/java/com/itextpdf/rups/controller/PdfReaderController.java
@@ -57,6 +57,8 @@ This file is part of the iText (R) project.
import com.itextpdf.rups.event.NodeAddDictChildEvent;
import com.itextpdf.rups.event.NodeDeleteArrayChildEvent;
import com.itextpdf.rups.event.NodeDeleteDictChildEvent;
+import com.itextpdf.rups.event.NodeUpdateArrayChildEvent;
+import com.itextpdf.rups.event.NodeUpdateDictChildEvent;
import com.itextpdf.rups.event.OpenPlainTextEvent;
import com.itextpdf.rups.event.OpenStructureEvent;
import com.itextpdf.rups.event.RupsEvent;
@@ -95,6 +97,7 @@ This file is part of the iText (R) project.
import javax.swing.event.ChangeListener;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreePath;
/**
@@ -167,7 +170,9 @@ public class PdfReaderController extends Observable implements Observer {
*/
public PdfReaderController(TreeSelectionListener treeSelectionListener,
PageSelectionListener pageSelectionListener) {
+
pdfTree = new PdfTree();
+ pdfTree.setName("pdfTree");
pdfTree.addTreeSelectionListener(treeSelectionListener);
JPopupMenu menu = PdfTreeContextMenu.getPopupMenu(pdfTree);
@@ -189,6 +194,7 @@ public PdfReaderController(TreeSelectionListener treeSelectionListener,
addObserver(text);
navigationTabs = new JTabbedPane();
+ navigationTabs.setName("navigationTabs");
final String pagesString = Language.PAGES.getString();
navigationTabs.addTab(pagesString, null, new JScrollPane(pages), pagesString);
navigationTabs.addTab(Language.OUTLINES.getString(), null, new JScrollPane(outlines),
@@ -219,17 +225,19 @@ public void stateChanged(ChangeEvent e) {
}
});
- objectPanel = new PdfObjectPanel();
+ objectPanel = new PdfObjectPanel(this);
addObserver(objectPanel);
objectPanel.addObserver(this);
streamPane = new SyntaxHighlightedStreamPane(this);
addObserver(streamPane);
JScrollPane debug = new JScrollPane(DebugView.getInstance().getTextArea());
editorTabs = new JTabbedPane();
+ editorTabs.setName("editorTabs");
editorTabs.addTab(Language.STREAM.getString(), null, streamPane, Language.STREAM.getString());
editorTabs.addTab(Language.FORM_XFA.getString(), null, form.getXfaTextArea(),
Language.FORM_XFA_LONG_FORM.getString());
editorTabs.addTab(Language.DEBUG_INFO.getString(), null, debug, Language.DEBUG_INFO_DESCRIPTION.getString());
+
}
/**
@@ -343,11 +351,24 @@ public void update(Observable observable, Object obj) {
case RupsEvent.NODE_ADD_DICT_CHILD_EVENT:
addTreeNodeDictChild(((NodeAddDictChildEvent.Content) event.getContent()).parent,
((NodeAddDictChildEvent.Content) event.getContent()).key,
- ((NodeAddDictChildEvent.Content) event.getContent()).index);
+ ((NodeAddDictChildEvent.Content) event.getContent()).index,
+ ((NodeAddDictChildEvent.Content) event.getContent()).value);
break;
case RupsEvent.NODE_ADD_ARRAY_CHILD_EVENT:
addTreeNodeArrayChild(((NodeAddArrayChildEvent.Content) event.getContent()).parent,
- ((NodeAddArrayChildEvent.Content) event.getContent()).index);
+ ((NodeAddArrayChildEvent.Content) event.getContent()).index,
+ ((NodeAddArrayChildEvent.Content) event.getContent()).value);
+ break;
+ case RupsEvent.NODE_UPDATE_DICT_CHILD_EVENT:
+ updateTreeNodeDictChild(((NodeUpdateDictChildEvent.Content) event.getContent()).parent,
+ ((NodeUpdateDictChildEvent.Content) event.getContent()).key,
+ ((NodeUpdateDictChildEvent.Content) event.getContent()).index,
+ ((NodeUpdateDictChildEvent.Content) event.getContent()).value);
+ break;
+ case RupsEvent.NODE_UPDATE_ARRAY_CHILD_EVENT:
+ updateTreeNodeArrayChild(((NodeUpdateArrayChildEvent.Content) event.getContent()).parent,
+ ((NodeUpdateArrayChildEvent.Content) event.getContent()).index,
+ ((NodeUpdateArrayChildEvent.Content) event.getContent()).value);
break;
case RupsEvent.NODE_DELETE_ARRAY_CHILD_EVENT:
deleteTreeChild(((NodeDeleteArrayChildEvent.Content) event.getContent()).parent,
@@ -466,20 +487,54 @@ public int deleteTreeNodeDictChild(PdfObjectTreeNode parent, PdfName key) {
}
//Returns index of the added child
- public int addTreeNodeDictChild(PdfObjectTreeNode parent, PdfName key, int index) {
- PdfObjectTreeNode child = PdfObjectTreeNode.getInstance((PdfDictionary) parent.getPdfObject(), key);
+ public int addTreeNodeDictChild(PdfObjectTreeNode parent, PdfName key, int index, PdfObject childValue) {
+ PdfObjectTreeNode child = PdfObjectTreeNode.getInstance(childValue, key);
return addTreeNodeChild(parent, child, index);
}
//Returns index of the added child
- public int addTreeNodeArrayChild(PdfObjectTreeNode parent, int index) {
- PdfObjectTreeNode child = PdfObjectTreeNode.getInstance(((PdfArray) parent.getPdfObject()).get(index, false));
- return addTreeNodeChild(parent, child, index);
+ public int addTreeNodeArrayChild(PdfObjectTreeNode parent, int index, PdfObject child) {
+ PdfArray parentArray = (PdfArray) parent.getPdfObject();
+ PdfObjectTreeNode childNode = PdfObjectTreeNode.getInstance(child);
+ return addTreeNodeChild(parent, childNode, index);
+ }
+
+ //Returns index of the updated child
+ public int updateTreeNodeDictChild(PdfObjectTreeNode parent, PdfName key, int index, PdfObject child) {
+ //PdfObjectTreeNode child = PdfObjectTreeNode.getInstance((PdfDictionary) parent.getPdfObject(), key);
+ return updateTreeChild(parent, PdfObjectTreeNode.getInstance(child), index);
+ }
+
+ //Returns index of the updated child
+ public int updateTreeNodeArrayChild(PdfObjectTreeNode parent, int index, PdfObject child) {
+ return updateTreeChild(parent, PdfObjectTreeNode.getInstance(child), index);
}
public int deleteTreeChild(PdfObjectTreeNode parent, int index) {
parent.remove(index);
- ((DefaultTreeModel) pdfTree.getModel()).reload(parent);
+ deleteObjectChild(parent.getPdfObject(), index);
+ updateObject(parent);
+ updateView(parent);
+ return index;
+ }
+
+ //Returns index of the updated child
+ public int updateTreeChild(PdfObjectTreeNode parent, PdfObjectTreeNode child, int index) {
+ PdfObjectTreeNode oldChild = (PdfObjectTreeNode) parent.getChildAt(index);
+ int childrenToTransfer = oldChild.getChildCount();
+ if (childrenToTransfer > 0){
+ for (int childIndex = 0; childIndex < childrenToTransfer; childIndex++){
+ PdfObjectTreeNode childToTransfer = (PdfObjectTreeNode) oldChild.getChildAt(childIndex);
+ addObjectChild(child.getPdfObject(),childToTransfer.getPdfObject(), child.getKey(), childIndex);
+ child.add((MutableTreeNode) childToTransfer);
+ }
+ }
+ parent.remove(oldChild);
+ parent.insert(child, index);
+ updateObjectChild(parent.getPdfObject(), child.getPdfObject(), index);
+ nodes.expandNode(child);
+ updateObject(parent);
+ updateView(parent);
return index;
}
@@ -487,7 +542,50 @@ public int deleteTreeChild(PdfObjectTreeNode parent, int index) {
public int addTreeNodeChild(PdfObjectTreeNode parent, PdfObjectTreeNode child, int index) {
parent.insert(child, index);
nodes.expandNode(child);
- ((DefaultTreeModel) pdfTree.getModel()).reload(parent);
+ addObjectChild(parent.getPdfObject(), child.getPdfObject(), child.getKey(), index);
+ updateObject(parent);
+ updateView(parent);
return index;
}
+
+ private void updateView(PdfObjectTreeNode parent) {
+ ((DefaultTreeModel) pdfTree.getModel()).reload(parent);
+ objectPanel.render(parent,getParser());
+ }
+
+ private void updateObject (PdfObjectTreeNode parent){
+ if (parent.isDictionary()) {
+ PdfDictionary parentDict = (PdfDictionary) parent.getPdfObject();
+ parentDict.keySet();
+ }else if(parent.isArray()){
+ PdfArray parentArray = (PdfArray)parent.getPdfObject();
+ }
+ }
+
+ protected void addObjectChild(PdfObject parent, PdfObject child, PdfName key, int index){
+ if (parent.isDictionary()) {
+ PdfDictionary parentDict = ((PdfDictionary) parent);
+ parentDict.put(key, child);
+ }else if(parent.isArray()){
+ PdfArray parentArray = (PdfArray) parent;
+ parentArray.add(index, child);
+ }
+ }
+ protected void updateObjectChild(PdfObject parent, PdfObject child, int index){
+ if (parent.isDictionary()) {
+ PdfDictionary parentDict = ((PdfDictionary) parent);
+ parentDict.put((PdfName) parentDict.keySet().toArray()[index], child);
+ }else if(parent.isArray()){
+ PdfArray parentArray = (PdfArray) parent;
+ parentArray.set(index, child);
+ }
+ }
+ protected void deleteObjectChild(PdfObject parent, int index){
+ if (parent.isDictionary()) {
+ PdfDictionary parentDict = ((PdfDictionary) parent);
+ parentDict.remove((PdfName) parentDict.keySet().toArray()[index]);
+ }else if(parent.isArray()){
+ ((PdfArray)parent).remove(index);
+ }
+ }
}
diff --git a/src/main/java/com/itextpdf/rups/controller/RupsInstanceController.java b/src/main/java/com/itextpdf/rups/controller/RupsInstanceController.java
index 410f9f27..59a644a0 100644
--- a/src/main/java/com/itextpdf/rups/controller/RupsInstanceController.java
+++ b/src/main/java/com/itextpdf/rups/controller/RupsInstanceController.java
@@ -141,14 +141,17 @@ public RupsInstanceController(Dimension dimension, JPanel owner) {
masterComponent.setDividerSize(2);
final JSplitPane content = new JSplitPane();
+ content.setName("content");
masterComponent.add(content, JSplitPane.TOP);
final JSplitPane info = new JSplitPane();
+ info.setName("info");
masterComponent.add(info, JSplitPane.BOTTOM);
content.setOrientation(JSplitPane.HORIZONTAL_SPLIT);
content.setDividerLocation((int) (dimension.getWidth() * .6));
content.setDividerSize(1);
JPanel treePanel = new JPanel(new BorderLayout());
+ treePanel.setName("treePanel");
treePanel.add(new JScrollPane(readerController.getPdfTree()), BorderLayout.CENTER);
content.add(treePanel, JSplitPane.LEFT);
content.add(readerController.getNavigationTabs(), JSplitPane.RIGHT);
@@ -157,7 +160,9 @@ public RupsInstanceController(Dimension dimension, JPanel owner) {
info.setDividerSize(1);
info.add(readerController.getObjectPanel(), JSplitPane.LEFT);
final JTabbedPane editorPane = readerController.getEditorTabs();
+ editorPane.setName("editorPane");
final JScrollPane cons = new JScrollPane(console.getTextArea());
+ cons.setName("console");
console.getTextArea().addMouseListener(
new ContextMenuMouseListener(ConsoleContextMenu.getPopupMenu(console.getTextArea()),
console.getTextArea()));
diff --git a/src/main/java/com/itextpdf/rups/event/NodeUpdateArrayChildEvent.java b/src/main/java/com/itextpdf/rups/event/NodeUpdateArrayChildEvent.java
new file mode 100644
index 00000000..e6b4b419
--- /dev/null
+++ b/src/main/java/com/itextpdf/rups/event/NodeUpdateArrayChildEvent.java
@@ -0,0 +1,78 @@
+/*
+ This file is part of the iText (R) project.
+ Copyright (c) 1998-2022 iText Group NV
+ Authors: iText Software.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License version 3
+ as published by the Free Software Foundation with the addition of the
+ following permission added to Section 15 as permitted in Section 7(a):
+ FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
+ ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
+ OF THIRD PARTY RIGHTS
+
+ This program is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ or FITNESS FOR A PARTICULAR PURPOSE.
+ See the GNU Affero General Public License for more details.
+ You should have received a copy of the GNU Affero General Public License
+ along with this program; if not, see http://www.gnu.org/licenses or write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA, 02110-1301 USA, or download the license from the following URL:
+ http://itextpdf.com/terms-of-use/
+
+ The interactive user interfaces in modified source and object code versions
+ of this program must display Appropriate Legal Notices, as required under
+ Section 5 of the GNU Affero General Public License.
+
+ In accordance with Section 7(b) of the GNU Affero General Public License,
+ a covered work must retain the producer line in every PDF that is created
+ or manipulated using iText.
+
+ You can be released from the requirements of the license by purchasing
+ a commercial license. Buying such a license is mandatory as soon as you
+ develop commercial activities involving the iText software without
+ disclosing the source code of your own applications.
+ These activities include: offering paid services to customers as an ASP,
+ serving PDFs on the fly in a web application, shipping iText with a closed
+ source product.
+
+ For more information, please contact iText Software Corp. at this
+ address: sales@itextpdf.com
+ */
+package com.itextpdf.rups.event;
+
+import com.itextpdf.kernel.pdf.PdfObject;
+import com.itextpdf.rups.event.RupsEvent;
+import com.itextpdf.rups.view.itext.treenodes.PdfObjectTreeNode;
+
+public class NodeUpdateArrayChildEvent extends RupsEvent {
+
+ Content content;
+
+ public NodeUpdateArrayChildEvent(PdfObject value, PdfObjectTreeNode parent, int index) {
+ content = new Content(value, parent, index);
+ }
+
+ @Override
+ public int getType() {
+ return NODE_UPDATE_ARRAY_CHILD_EVENT;
+ }
+
+ @Override
+ public Object getContent() {
+ return content;
+ }
+
+ public class Content {
+ public PdfObject value;
+ public PdfObjectTreeNode parent;
+ public int index;
+
+ public Content(PdfObject value, PdfObjectTreeNode parent, int index) {
+ this.value = value;
+ this.parent = parent;
+ this.index = index;
+ }
+ }
+}
diff --git a/src/main/java/com/itextpdf/rups/event/NodeUpdateDictChildEvent.java b/src/main/java/com/itextpdf/rups/event/NodeUpdateDictChildEvent.java
new file mode 100644
index 00000000..321171c0
--- /dev/null
+++ b/src/main/java/com/itextpdf/rups/event/NodeUpdateDictChildEvent.java
@@ -0,0 +1,81 @@
+/*
+ This file is part of the iText (R) project.
+ Copyright (c) 1998-2022 iText Group NV
+ Authors: iText Software.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License version 3
+ as published by the Free Software Foundation with the addition of the
+ following permission added to Section 15 as permitted in Section 7(a):
+ FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
+ ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
+ OF THIRD PARTY RIGHTS
+
+ This program is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ or FITNESS FOR A PARTICULAR PURPOSE.
+ See the GNU Affero General Public License for more details.
+ You should have received a copy of the GNU Affero General Public License
+ along with this program; if not, see http://www.gnu.org/licenses or write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA, 02110-1301 USA, or download the license from the following URL:
+ http://itextpdf.com/terms-of-use/
+
+ The interactive user interfaces in modified source and object code versions
+ of this program must display Appropriate Legal Notices, as required under
+ Section 5 of the GNU Affero General Public License.
+
+ In accordance with Section 7(b) of the GNU Affero General Public License,
+ a covered work must retain the producer line in every PDF that is created
+ or manipulated using iText.
+
+ You can be released from the requirements of the license by purchasing
+ a commercial license. Buying such a license is mandatory as soon as you
+ develop commercial activities involving the iText software without
+ disclosing the source code of your own applications.
+ These activities include: offering paid services to customers as an ASP,
+ serving PDFs on the fly in a web application, shipping iText with a closed
+ source product.
+
+ For more information, please contact iText Software Corp. at this
+ address: sales@itextpdf.com
+ */
+package com.itextpdf.rups.event;
+
+import com.itextpdf.kernel.pdf.PdfName;
+import com.itextpdf.kernel.pdf.PdfObject;
+import com.itextpdf.rups.event.RupsEvent;
+import com.itextpdf.rups.view.itext.treenodes.PdfObjectTreeNode;
+
+public class NodeUpdateDictChildEvent extends RupsEvent {
+
+ Content content;
+
+ public NodeUpdateDictChildEvent(PdfName key, PdfObject value, PdfObjectTreeNode parent, int index) {
+ content = new Content(key, value, parent, index);
+ }
+
+ @Override
+ public int getType() {
+ return NODE_UPDATE_DICT_CHILD_EVENT;
+ }
+
+ @Override
+ public Object getContent() {
+ return content;
+ }
+
+ public class Content {
+ public PdfName key;
+ public PdfObject value;
+ public PdfObjectTreeNode parent;
+ public int index;
+
+ public Content(PdfName key, PdfObject value, PdfObjectTreeNode parent, int index) {
+ this.key = key;
+ this.value = value;
+ this.parent = parent;
+ this.index = index;
+ }
+ }
+}
diff --git a/src/main/java/com/itextpdf/rups/event/RupsEvent.java b/src/main/java/com/itextpdf/rups/event/RupsEvent.java
index cbb1f620..58ca6e94 100644
--- a/src/main/java/com/itextpdf/rups/event/RupsEvent.java
+++ b/src/main/java/com/itextpdf/rups/event/RupsEvent.java
@@ -46,7 +46,8 @@ This file is part of the iText (R) project.
* Abstract Event Class that other events should extend from.
*/
public abstract class RupsEvent {
-
+ // Does this need to remain a set of static bytes for a particular reason?
+ // It feels like it's reinventing the enum in this use-case.
public static final byte OPEN_FILE_EVENT = 0;
public static final byte OPEN_DOCUMENT_POST_EVENT = 1;
public static final byte CLOSE_DOCUMENT_EVENT = 2;
@@ -65,6 +66,8 @@ public abstract class RupsEvent {
public static final byte NEW_INDIRECT_OBJECT_EVENT = 15;
public static final byte POST_NEW_INDIRECT_OBJECT_EVENT = 16;
public static final byte ALL_FILES_CLOSED = 17;
+ public static final byte NODE_UPDATE_DICT_CHILD_EVENT = 18;
+ public static final byte NODE_UPDATE_ARRAY_CHILD_EVENT = 19;
public abstract int getType();
diff --git a/src/main/java/com/itextpdf/rups/io/PdfObjectTreeEdit.java b/src/main/java/com/itextpdf/rups/io/PdfObjectTreeEdit.java
new file mode 100644
index 00000000..f27d01c2
--- /dev/null
+++ b/src/main/java/com/itextpdf/rups/io/PdfObjectTreeEdit.java
@@ -0,0 +1,368 @@
+/*
+ This file is part of the iText (R) project.
+ Copyright (c) 1998-2022 iText Group NV
+ Authors: iText Software.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License version 3
+ as published by the Free Software Foundation with the addition of the
+ following permission added to Section 15 as permitted in Section 7(a):
+ FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
+ ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
+ OF THIRD PARTY RIGHTS
+
+ This program is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ or FITNESS FOR A PARTICULAR PURPOSE.
+ See the GNU Affero General Public License for more details.
+ You should have received a copy of the GNU Affero General Public License
+ along with this program; if not, see http://www.gnu.org/licenses or write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA, 02110-1301 USA, or download the license from the following URL:
+ http://itextpdf.com/terms-of-use/
+
+ The interactive user interfaces in modified source and object code versions
+ of this program must display Appropriate Legal Notices, as required under
+ Section 5 of the GNU Affero General Public License.
+
+ In accordance with Section 7(b) of the GNU Affero General Public License,
+ a covered work must retain the producer line in every PDF that is created
+ or manipulated using iText.
+
+ You can be released from the requirements of the license by purchasing
+ a commercial license. Buying such a license is mandatory as soon as you
+ develop commercial activities involving the iText software without
+ disclosing the source code of your own applications.
+ These activities include: offering paid services to customers as an ASP,
+ serving PDFs on the fly in a web application, shipping iText with a closed
+ source product.
+
+ For more information, please contact iText Software Corp. at this
+ address: sales@itextpdf.com
+ */
+package com.itextpdf.rups.io;
+
+import com.itextpdf.kernel.pdf.PdfName;
+import com.itextpdf.rups.controller.PdfReaderController;
+import com.itextpdf.rups.event.NodeAddArrayChildEvent;
+import com.itextpdf.rups.event.NodeAddDictChildEvent;
+import com.itextpdf.rups.event.NodeDeleteArrayChildEvent;
+import com.itextpdf.rups.event.NodeDeleteDictChildEvent;
+import com.itextpdf.rups.event.NodeUpdateArrayChildEvent;
+import com.itextpdf.rups.event.NodeUpdateDictChildEvent;
+import com.itextpdf.rups.event.RupsEvent;
+import com.itextpdf.rups.model.ErrorDialogPane;
+import com.itextpdf.rups.view.itext.PdfObjectPanel;
+import com.itextpdf.rups.view.itext.treenodes.PdfObjectTreeNode;
+
+import javax.swing.undo.CannotRedoException;
+import javax.swing.undo.CannotUndoException;
+import javax.swing.undo.UndoableEdit;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.Stack;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+public class PdfObjectTreeEdit implements UndoableEdit {
+
+ enum ObjectType {
+ ARRAY, DICT
+ }
+ enum ActionType {
+ ADD, DELETE, UPDATE
+ }
+
+ ObjectType objectType;
+ ActionType actionType;
+ PdfReaderController controller;
+ int editType; // The type of edit, as defined by the static bytes within RupsEvent.java (Refactor to Enum?)
+ PdfObjectTreeNode parent; // Parent Node
+ PdfObjectTreeNode oldChild; // Child Node
+ PdfObjectTreeNode child; // Child Node
+ int index; // Index at which the edit occurred.
+ PdfName key; // Index at which the edit occurred.
+ List addedEdits;
+ boolean dead;
+
+ public PdfObjectTreeEdit(PdfReaderController controller, RupsEvent editEvent) {
+ this.controller = controller;
+ this.addedEdits = new LinkedList<>();
+ this.dead = false;
+ extractEditData(editEvent);
+ }
+
+ private void extractEditData(RupsEvent editEvent) {
+ // TODO: Maybe we can add some granularity to these events?
+ // i.e. (abs)RupsEvent>(abs)NodeArrayEvent>NodeAddArrayEvent so the common array interface of Index, Parent and Value are
+ // always present and (abs)RupsEvent>(abs)NodeDictEvent>NodeAddDictEvent so that Key, Parent and Value are always present?
+
+ editType = editEvent.getType();
+ switch (editType) {
+ case RupsEvent.NODE_ADD_DICT_CHILD_EVENT:
+ NodeAddDictChildEvent.Content addDictContent = (NodeAddDictChildEvent.Content)editEvent.getContent();
+ parent = addDictContent.parent;
+ child = PdfObjectTreeNode.getInstance(addDictContent.value);
+ index = addDictContent.index;
+ key = addDictContent.key;
+ objectType = ObjectType.DICT;
+ actionType = ActionType.ADD;
+ break;
+ case RupsEvent.NODE_ADD_ARRAY_CHILD_EVENT:
+ NodeAddArrayChildEvent.Content addArrayContent = (NodeAddArrayChildEvent.Content)editEvent.getContent();
+ parent = addArrayContent.parent;
+ child = PdfObjectTreeNode.getInstance(addArrayContent.value);
+ index = addArrayContent.index;
+ objectType = ObjectType.ARRAY;
+ actionType = ActionType.ADD;
+ break;
+ case RupsEvent.NODE_DELETE_DICT_CHILD_EVENT:
+ NodeDeleteDictChildEvent.Content deleteDictContent = (NodeDeleteDictChildEvent.Content)editEvent.getContent();
+ parent = deleteDictContent.parent;
+ key = deleteDictContent.key;
+ child = deleteDictContent.parent.getDictionaryChildNode(key);
+ index = deleteDictContent.parent.getIndex(this.child);
+ objectType = ObjectType.DICT;
+ actionType = ActionType.DELETE;
+ break;
+ case RupsEvent.NODE_DELETE_ARRAY_CHILD_EVENT:
+ NodeDeleteArrayChildEvent.Content deleteArrayContent = (NodeDeleteArrayChildEvent.Content)editEvent.getContent();
+ parent = deleteArrayContent.parent;
+ index = deleteArrayContent.index;
+ child = (PdfObjectTreeNode) parent.getChildAt(index);
+ objectType = ObjectType.ARRAY;
+ actionType = ActionType.DELETE;
+ break;
+ case RupsEvent.NODE_UPDATE_DICT_CHILD_EVENT:
+ NodeUpdateDictChildEvent.Content updateDictContent = (NodeUpdateDictChildEvent.Content)editEvent.getContent();
+ parent = updateDictContent.parent;
+ key = updateDictContent.key;
+ oldChild = parent.getDictionaryChildNode(key);
+ child = PdfObjectTreeNode.getInstance(updateDictContent.value);
+ index = updateDictContent.index;
+ objectType = ObjectType.DICT;
+ actionType = ActionType.UPDATE;
+ break;
+ case RupsEvent.NODE_UPDATE_ARRAY_CHILD_EVENT:
+ NodeUpdateArrayChildEvent.Content updateArrayContent = (NodeUpdateArrayChildEvent.Content)editEvent.getContent();
+ parent = updateArrayContent.parent;
+ index = updateArrayContent.index;
+ child = PdfObjectTreeNode.getInstance(updateArrayContent.value);
+ oldChild = (PdfObjectTreeNode) parent.getChildAt(index);
+ objectType = ObjectType.ARRAY;
+ actionType = ActionType.UPDATE;
+ break;
+ }
+ }
+
+ @Override
+ public void undo() throws CannotUndoException {
+ if(!canUndo()) throw new CannotUndoException();
+ RupsEvent newChild;
+ switch (this.editType) {
+ case RupsEvent.NODE_ADD_DICT_CHILD_EVENT:
+ newChild = new NodeDeleteDictChildEvent(key, parent);
+ break;
+ case RupsEvent.NODE_ADD_ARRAY_CHILD_EVENT:
+ newChild = new NodeDeleteArrayChildEvent(index, parent);
+ break;
+ case RupsEvent.NODE_DELETE_DICT_CHILD_EVENT:
+ newChild = new NodeAddDictChildEvent(key, child.getPdfObject(), parent, index);
+ break;
+ case RupsEvent.NODE_DELETE_ARRAY_CHILD_EVENT:
+ newChild = new NodeAddArrayChildEvent(child.getPdfObject(), parent, index);
+ break;
+ case RupsEvent.NODE_UPDATE_DICT_CHILD_EVENT:
+ newChild = new NodeUpdateDictChildEvent(key, oldChild.getPdfObject(), parent, index);
+ break;
+ case RupsEvent.NODE_UPDATE_ARRAY_CHILD_EVENT:
+ newChild = new NodeUpdateArrayChildEvent(oldChild.getPdfObject(), parent, index);
+ break;
+ default:
+ throw new CannotUndoException();
+ }
+ for (int i = addedEdits.size() - 1; i > -1; i--) {
+ addedEdits.get(i).undo();
+ }
+ updateController(newChild, CannotRedoException.class);
+ }
+
+ private void updateController(RupsEvent newChild, Class extends Exception> exceptionClass) {
+ try {
+ controller.update(controller, newChild);
+ controller.render(parent);
+ } catch (Exception ex) {
+ ErrorDialogPane.showErrorDialog(controller.getEditorTabs().getParent(), ex);
+ System.err.print(ex.getLocalizedMessage());
+ try {
+ throw exceptionClass.getDeclaredConstructor().newInstance(ex.getLocalizedMessage());
+ } catch (Exception e) {
+ throw new RuntimeException(ex.getLocalizedMessage());
+ }
+ }
+ }
+
+ @Override
+ public boolean canUndo() {
+ if(!validateSelf()) return false;
+ return true;
+ }
+
+ @Override
+ public void redo() throws CannotRedoException {
+ if(!canRedo()) throw new CannotRedoException();
+
+ try {
+ RupsEvent newChild;
+ switch (editType) {
+ case RupsEvent.NODE_ADD_DICT_CHILD_EVENT:
+ newChild = new NodeAddDictChildEvent(key,child.getPdfObject(),parent,index);
+ break;
+ case RupsEvent.NODE_ADD_ARRAY_CHILD_EVENT:
+ newChild = new NodeAddArrayChildEvent(child.getPdfObject(),parent,index);
+ break;
+ case RupsEvent.NODE_DELETE_DICT_CHILD_EVENT:
+ newChild = new NodeDeleteDictChildEvent(key,parent);
+ break;
+ case RupsEvent.NODE_DELETE_ARRAY_CHILD_EVENT:
+ newChild = new NodeDeleteArrayChildEvent(index,parent);
+ break;
+ case RupsEvent.NODE_UPDATE_DICT_CHILD_EVENT:
+ newChild = new NodeUpdateDictChildEvent(key,child.getPdfObject(),parent,index);
+ break;
+ case RupsEvent.NODE_UPDATE_ARRAY_CHILD_EVENT:
+ newChild = new NodeUpdateArrayChildEvent(child.getPdfObject(),parent,index);
+ break;
+ default:
+ throw new CannotRedoException();
+ }
+ updateController(newChild, CannotRedoException.class);
+
+ for (int i = 0; i < addedEdits.size(); i++) {
+ addedEdits.get(i).redo();
+ }
+ } catch (CannotRedoException ex) {
+ ErrorDialogPane.showErrorDialog( controller.getEditorTabs().getParent(), ex);
+ System.err.print(ex.getLocalizedMessage());
+ throw ex;
+ } catch (Exception ex) {
+ ErrorDialogPane.showErrorDialog( controller.getEditorTabs().getParent(), ex);
+ System.err.print(ex.getLocalizedMessage());
+ throw new CannotRedoException();
+ }
+ }
+
+ @Override
+ public boolean canRedo() {
+ return validateSelf();
+ }
+
+ @Override
+ public void die() {
+ this.dead = true;
+ }
+
+ @Override
+ public boolean addEdit(UndoableEdit anEdit) {
+ return false;
+ }
+
+ @Override
+ public boolean replaceEdit(UndoableEdit anEdit) {
+ if (!(anEdit instanceof PdfObjectTreeEdit)) {
+ return false;
+ }
+ if (isSignificant()){
+ return false;
+ }
+ PdfObjectTreeEdit objectTreeEdit = (PdfObjectTreeEdit) anEdit;
+
+ if (!checkParity(objectTreeEdit)){
+ return false;
+ }
+
+ dead = true;
+ return true;
+ }
+
+ @Override
+ public boolean isSignificant() {
+ return true;
+ }
+
+ @Override
+ public String getPresentationName() {
+ StringBuilder returnString = new StringBuilder();
+ switch(actionType){
+ case ADD:
+ returnString.append("Addition");
+ break;
+ case DELETE:
+ returnString.append("Removal");
+ break;
+ case UPDATE:
+ returnString.append("Update");
+ break;
+ default:
+ return "Error";
+ }
+ switch (objectType){
+ case DICT:
+ returnString.append(" of a Dict value");
+ break;
+ case ARRAY:
+ returnString.append(" of an Array value");
+ }
+ return returnString.toString();
+ }
+
+ @Override
+ public String getUndoPresentationName() {
+ return "Undo the ".concat(getPresentationName());
+ }
+
+ @Override
+ public String getRedoPresentationName() {
+ return "Redo the ".concat(getPresentationName());
+ }
+
+ private boolean validateSelf() {
+ if (dead) return false;
+
+ switch(this.editType){
+ case RupsEvent.NODE_ADD_DICT_CHILD_EVENT:
+ case RupsEvent.NODE_ADD_ARRAY_CHILD_EVENT:
+ case RupsEvent.NODE_UPDATE_DICT_CHILD_EVENT:
+ case RupsEvent.NODE_UPDATE_ARRAY_CHILD_EVENT:
+ case RupsEvent.NODE_DELETE_DICT_CHILD_EVENT:
+ case RupsEvent.NODE_DELETE_ARRAY_CHILD_EVENT:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private boolean checkParity(PdfObjectTreeEdit objectTreeEdit) {
+
+ if(objectTreeEdit.controller != controller){
+ return false;
+ }
+
+ if(objectTreeEdit.objectType != objectType){
+ return false;
+ }
+
+ if(objectTreeEdit.index != index){
+ return false;
+ }
+
+ if(objectTreeEdit.parent != parent){
+ return false;
+ }
+
+ if(objectTreeEdit.key != key){
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/com/itextpdf/rups/model/TreeNodeFactory.java b/src/main/java/com/itextpdf/rups/model/TreeNodeFactory.java
index b161c3d4..e51ef5f3 100644
--- a/src/main/java/com/itextpdf/rups/model/TreeNodeFactory.java
+++ b/src/main/java/com/itextpdf/rups/model/TreeNodeFactory.java
@@ -133,7 +133,8 @@ public void expandNode(PdfObjectTreeNode node) {
break;
case PdfObject.ARRAY:
final PdfArray array = (PdfArray) object;
- for (int i = 0; i < array.size(); ++i) {
+ int arrayLength = array.size();
+ for (int i = 0; i < arrayLength; ++i) {
leaf = PdfObjectTreeNode.getInstance(array.get(i, false));
associateIfIndirect(leaf);
addNodes(node, leaf);
diff --git a/src/main/java/com/itextpdf/rups/view/RupsTabbedPane.java b/src/main/java/com/itextpdf/rups/view/RupsTabbedPane.java
index 713b03be..2df84100 100644
--- a/src/main/java/com/itextpdf/rups/view/RupsTabbedPane.java
+++ b/src/main/java/com/itextpdf/rups/view/RupsTabbedPane.java
@@ -75,8 +75,8 @@ public void openNewFile(File file, Dimension dimension, boolean readonly) {
if (this.defaultTab.equals(this.jTabbedPane.getSelectedComponent())) {
this.jTabbedPane.removeTabAt(this.jTabbedPane.getSelectedIndex());
}
-
RupsPanel rupsPanel = new RupsPanel();
+ rupsPanel.setName(file.getName());
RupsInstanceController rupsInstanceController = new RupsInstanceController(dimension, rupsPanel);
rupsPanel.setRupsInstanceController(rupsInstanceController);
rupsInstanceController.loadFile(file, readonly);
diff --git a/src/main/java/com/itextpdf/rups/view/contextmenu/SaveToFilePdfTreeAction.java b/src/main/java/com/itextpdf/rups/view/contextmenu/SaveToFilePdfTreeAction.java
index baae9387..c232d25c 100644
--- a/src/main/java/com/itextpdf/rups/view/contextmenu/SaveToFilePdfTreeAction.java
+++ b/src/main/java/com/itextpdf/rups/view/contextmenu/SaveToFilePdfTreeAction.java
@@ -102,7 +102,7 @@ public void actionPerformed(ActionEvent event) {
try (FileOutputStream fos = new FileOutputStream(path)) {
fos.write(array);
}
- } catch (IOException e) { // TODO : Catch this exception properly
+ } catch (IOException e) {
LoggerHelper.error(Language.ERROR_WRITING_FILE.getString(), e, getClass());
}
}
diff --git a/src/main/java/com/itextpdf/rups/view/itext/PdfObjectPanel.java b/src/main/java/com/itextpdf/rups/view/itext/PdfObjectPanel.java
index 50621085..402c798a 100644
--- a/src/main/java/com/itextpdf/rups/view/itext/PdfObjectPanel.java
+++ b/src/main/java/com/itextpdf/rups/view/itext/PdfObjectPanel.java
@@ -52,7 +52,10 @@ This file is part of the iText (R) project.
import com.itextpdf.rups.event.NodeAddDictChildEvent;
import com.itextpdf.rups.event.NodeDeleteArrayChildEvent;
import com.itextpdf.rups.event.NodeDeleteDictChildEvent;
+import com.itextpdf.rups.event.NodeUpdateArrayChildEvent;
+import com.itextpdf.rups.event.NodeUpdateDictChildEvent;
import com.itextpdf.rups.event.RupsEvent;
+import com.itextpdf.rups.io.PdfObjectTreeEdit;
import com.itextpdf.rups.model.PdfSyntaxParser;
import com.itextpdf.rups.view.Language;
import com.itextpdf.rups.view.icons.IconFetcher;
@@ -63,16 +66,26 @@ This file is part of the iText (R) project.
import com.itextpdf.rups.view.models.PdfArrayTableModel;
import java.awt.CardLayout;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
+import java.util.LinkedList;
+import java.util.List;
import java.util.Observable;
import java.util.Observer;
+import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
+import javax.swing.KeyStroke;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
+import javax.swing.undo.CannotRedoException;
+import javax.swing.undo.CannotUndoException;
+import javax.swing.undo.UndoManager;
+import javax.swing.undo.UndoableEdit;
public class PdfObjectPanel extends Observable implements Observer {
@@ -89,11 +102,14 @@ public class PdfObjectPanel extends Observable implements Observer {
*/
private static final String TEXT = Language.TEXT.getString();
+ private final PdfReaderController controller;
+
/**
* The layout that will show the info about the PDF object that is being analyzed.
*/
protected CardLayout layout = new CardLayout();
+ protected UndoManager undoManager;
/**
* Table with dictionary entries.
@@ -112,22 +128,41 @@ public class PdfObjectPanel extends Observable implements Observer {
/**
* Creates a PDF object panel.
*/
- public PdfObjectPanel() {
+ public PdfObjectPanel(PdfReaderController controller) {
+
+ this.controller = controller;
+
+ // panel name
+ panel.setName("PdfObjectPanel");
+
+ table.setName("PdfObjectTable");
+
// layout
panel.setLayout(layout);
// dictionary / array / stream
final JScrollPane dictScrollPane = new JScrollPane();
dictScrollPane.setViewportView(table);
+ dictScrollPane.setName("dictScrollPane");
panel.add(dictScrollPane, TABLE);
// number / string / ...
final JScrollPane textScrollPane = new JScrollPane();
textScrollPane.setViewportView(text);
+ textScrollPane.setName("textScrollPane");
text.setEditable(false);
panel.add(textScrollPane, TEXT);
table.addMouseListener(new JTableButtonMouseListener());
+
+ this.undoManager = new UndoManager();
+ this.undoManager.setLimit(8192);
+
+ //TODO: Check if WHEN_IN_FOCUSED_WINDOW bleeds across open docs
+ panel.registerKeyboardAction(new UndoAction(undoManager),
+ KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_DOWN_MASK), JComponent.WHEN_IN_FOCUSED_WINDOW);
+ panel.registerKeyboardAction(new RedoAction(undoManager),
+ KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_DOWN_MASK), JComponent.WHEN_IN_FOCUSED_WINDOW);
}
/**
@@ -160,8 +195,8 @@ public void update(Observable observable, Object obj) {
*/
public void render(PdfObjectTreeNode node, PdfSyntaxParser parser) {
target = node;
- final PdfObject object = node.getPdfObject();
- if (object == null) {
+ final PdfObject pdfObjectClone = node.getPdfObject().clone();
+ if (pdfObjectClone == null) {
text.setText(null);
layout.show(panel, TEXT);
panel.repaint();
@@ -169,34 +204,36 @@ public void render(PdfObjectTreeNode node, PdfSyntaxParser parser) {
return;
}
- switch (object.getType()) {
+ DictionaryTableModelButton rowButtons = new DictionaryTableModelButton(IconFetcher.getIcon(CROSS_ICON), IconFetcher.getIcon(ADD_ICON));
+ rowButtons.setName("rowButton");
+ switch (pdfObjectClone.getType()) {
case PdfObject.DICTIONARY:
case PdfObject.STREAM:
final DictionaryTableModel model =
- new DictionaryTableModel((PdfDictionary) object, parser, panel);
+ new DictionaryTableModel((PdfDictionary) pdfObjectClone, parser, panel);
model.addTableModelListener(new DictionaryModelListener());
table.setModel(model);
- table.getColumn("").setCellRenderer(new DictionaryTableModelButton(
- IconFetcher.getIcon(CROSS_ICON), IconFetcher.getIcon(ADD_ICON)));
+ table.getColumn("").setCellRenderer(rowButtons);
layout.show(panel, TABLE);
panel.repaint();
+ table.revalidate();
break;
case PdfObject.ARRAY:
final PdfArrayTableModel arrayModel =
- new PdfArrayTableModel((PdfArray) object, parser, panel);
+ new PdfArrayTableModel((PdfArray) pdfObjectClone, parser, panel);
arrayModel.addTableModelListener(new ArrayModelListener());
table.setModel(arrayModel);
- table.getColumn("").setCellRenderer(new DictionaryTableModelButton(IconFetcher.getIcon(CROSS_ICON),
- IconFetcher.getIcon(ADD_ICON)));
+ table.getColumn("").setCellRenderer(rowButtons);
layout.show(panel, TABLE);
panel.repaint();
+ table.revalidate();
break;
case PdfObject.STRING:
- text.setText(((PdfString) object).toUnicodeString());
+ text.setText(((PdfString) pdfObjectClone).toUnicodeString());
layout.show(panel, TEXT);
break;
default:
- text.setText(object.toString());
+ text.setText(pdfObjectClone.toString());
layout.show(panel, TEXT);
break;
}
@@ -236,19 +273,27 @@ public void tableChanged(TableModelEvent e) {
return;
}
final PdfName key = (PdfName) table.getValueAt(row, 0);
- final PdfObject value = ((PdfDictionary) target.getPdfObject()).get(key, false);
+ final PdfObject value;
+ RupsEvent notification = null;
switch (e.getType()) {
- case TableModelEvent.UPDATE:
- break;
case TableModelEvent.DELETE:
- PdfObjectPanel.this.setChanged();
- PdfObjectPanel.this.notifyObservers(new NodeDeleteDictChildEvent(key, target));
+ notification = new NodeDeleteDictChildEvent(key, target);
+ break;
+ case TableModelEvent.UPDATE:
+ value = ((PdfDictionary) ((DictionaryTableModel) table.getModel()).getPdfObject()).get(key, false);
+ notification = new NodeUpdateDictChildEvent(key, value, target, row);
break;
case TableModelEvent.INSERT:
- PdfObjectPanel.this.setChanged();
- PdfObjectPanel.this.notifyObservers(new NodeAddDictChildEvent(key, value, target, row));
+ value = ((PdfDictionary) ((DictionaryTableModel) table.getModel()).getPdfObject()).get(key, false);
+ notification = new NodeAddDictChildEvent(key, value, target, row);
break;
}
+ if (notification == null) {
+ return;
+ }
+ undoManager.addEdit(new PdfObjectTreeEdit(controller, notification));
+ PdfObjectPanel.this.setChanged();
+ PdfObjectPanel.this.notifyObservers(notification);
}
}
@@ -262,19 +307,28 @@ public void tableChanged(TableModelEvent e) {
if (row != e.getLastRow()) {
return;
}
- final PdfObject value = ((PdfArray) target.getPdfObject()).get(row, false);
+ // TODO: Maybe abstract the PDF object used for backing the UI elements from those backing the document tree.
+ RupsEvent notification = null;
+ final PdfObject value;
switch (e.getType()) {
- case TableModelEvent.UPDATE:
- break;
case TableModelEvent.DELETE:
- PdfObjectPanel.this.setChanged();
- PdfObjectPanel.this.notifyObservers(new NodeDeleteArrayChildEvent(row, target));
+ notification = new NodeDeleteArrayChildEvent(row, target);
+ break;
+ case TableModelEvent.UPDATE:
+ value = ((PdfArray) ((PdfArrayTableModel) table.getModel()).getPdfObject()).get(row, false);
+ notification = new NodeUpdateArrayChildEvent(value, target, row);
break;
case TableModelEvent.INSERT:
- PdfObjectPanel.this.setChanged();
- PdfObjectPanel.this.notifyObservers(new NodeAddArrayChildEvent(value, target, row));
+ value = ((PdfArray) ((PdfArrayTableModel) table.getModel()).getPdfObject()).get(row, false);
+ notification = new NodeAddArrayChildEvent(value, target, row);
break;
}
+ if (notification == null) {
+ return;
+ }
+ undoManager.addEdit(new PdfObjectTreeEdit(controller, notification));
+ PdfObjectPanel.this.setChanged();
+ PdfObjectPanel.this.notifyObservers(notification);
}
}
}
diff --git a/src/main/java/com/itextpdf/rups/view/itext/SyntaxHighlightedStreamPane.java b/src/main/java/com/itextpdf/rups/view/itext/SyntaxHighlightedStreamPane.java
index f107c015..cd83253f 100644
--- a/src/main/java/com/itextpdf/rups/view/itext/SyntaxHighlightedStreamPane.java
+++ b/src/main/java/com/itextpdf/rups/view/itext/SyntaxHighlightedStreamPane.java
@@ -88,8 +88,6 @@ This file is part of the iText (R) project.
public class SyntaxHighlightedStreamPane extends JScrollPane implements Observer {
- private static final int MAX_NUMBER_OF_EDITS = 8192;
-
private static Method pdfStreamGetInputStreamMethod;
/**
@@ -103,6 +101,8 @@ public class SyntaxHighlightedStreamPane extends JScrollPane implements Observer
protected UndoManager manager;
+ protected int MAX_NUMBER_OF_EDITS = 8192;
+
//Todo: Remove that field after proper application structure will be implemented.
private final PdfReaderController controller;
@@ -132,8 +132,9 @@ public SyntaxHighlightedStreamPane(PdfReaderController controller) {
text.setComponentPopupMenu(popupMenu);
text.addMouseListener(new ContextMenuMouseListener(popupMenu, text));
- manager = new UndoManager();
- manager.setLimit(MAX_NUMBER_OF_EDITS);
+ this.manager = new UndoManager();
+ this.manager.setLimit(MAX_NUMBER_OF_EDITS);
+
text.getDocument().addUndoableEditListener(manager);
text.registerKeyboardAction(new UndoAction(manager),
KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_DOWN_MASK), JComponent.WHEN_FOCUSED);
diff --git a/src/main/java/com/itextpdf/rups/view/itext/treenodes/PdfObjectTreeNode.java b/src/main/java/com/itextpdf/rups/view/itext/treenodes/PdfObjectTreeNode.java
index 86ada289..ab3628ca 100644
--- a/src/main/java/com/itextpdf/rups/view/itext/treenodes/PdfObjectTreeNode.java
+++ b/src/main/java/com/itextpdf/rups/view/itext/treenodes/PdfObjectTreeNode.java
@@ -192,6 +192,19 @@ public static PdfObjectTreeNode getInstance(PdfDictionary dict, PdfName key) {
return node;
}
+ /**
+ * Creates an instance of a tree node for the object corresponding with a key in a dictionary.
+ *
+ * @param obj the pdf object that is represented by this tree node.
+ * @param key the dictionary key corresponding with the PDF object in this tree node.
+ * @return a PdfObjectTreeNode
+ */
+ public static PdfObjectTreeNode getInstance(PdfObject obj, PdfName key) {
+ final PdfObjectTreeNode node = getInstance(obj);
+ node.key = key;
+ return node;
+ }
+
/**
* Getter for the PDF Object.
*
@@ -201,6 +214,15 @@ public PdfObject getPdfObject() {
return object;
}
+ /**
+ * Getter for the ObjectTree's key.
+ *
+ * @return the PDF Name representing this tree node.
+ */
+ public PdfName getKey(){
+ return key;
+ }
+
/**
* Getter for the object number in case the object is indirect or if it is reference.
*
diff --git a/src/main/java/com/itextpdf/rups/view/models/AbstractPdfObjectPanelTableModel.java b/src/main/java/com/itextpdf/rups/view/models/AbstractPdfObjectPanelTableModel.java
index 178d5c7c..86efaa43 100644
--- a/src/main/java/com/itextpdf/rups/view/models/AbstractPdfObjectPanelTableModel.java
+++ b/src/main/java/com/itextpdf/rups/view/models/AbstractPdfObjectPanelTableModel.java
@@ -42,10 +42,14 @@ This file is part of the iText (R) project.
*/
package com.itextpdf.rups.view.models;
+import com.itextpdf.kernel.pdf.PdfObject;
+
import javax.swing.table.AbstractTableModel;
public abstract class AbstractPdfObjectPanelTableModel extends AbstractTableModel {
+ public abstract PdfObject getPdfObject();
+
public abstract void removeRow(int rowIndex);
public abstract void validateTempRow();
diff --git a/src/main/java/com/itextpdf/rups/view/models/DictionaryTableModel.java b/src/main/java/com/itextpdf/rups/view/models/DictionaryTableModel.java
index c32ce171..3ba5813b 100644
--- a/src/main/java/com/itextpdf/rups/view/models/DictionaryTableModel.java
+++ b/src/main/java/com/itextpdf/rups/view/models/DictionaryTableModel.java
@@ -168,8 +168,7 @@ public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
final PdfObject newValue = parser.parseString(value, parent);
if (newValue != null) {
final PdfName oldName = keys.get(rowIndex);
- removeRow(rowIndex);
- addRow(oldName, newValue);
+ updateRow(rowIndex, newValue);
}
}
}
@@ -249,6 +248,12 @@ private void addRow(PdfName key, PdfObject value) {
fireTableRowsInserted(index, index);
}
+ private void updateRow(int index, PdfObject value) {
+ PdfName key = keys.get(index);
+ dictionary.put(key, value);
+ fireTableRowsUpdated(index, index);
+ }
+
private PdfName getCorrectKey(String value) {
if (!value.startsWith("/")) {
value = "/" + value;
@@ -262,4 +267,9 @@ private PdfName getCorrectKey(String value) {
LoggerHelper.error(Language.ERROR_KEY_IS_NOT_NAME.getString(), getClass());
return null;
}
+
+ @Override
+ public PdfObject getPdfObject() {
+ return dictionary;
+ }
}
diff --git a/src/main/java/com/itextpdf/rups/view/models/PdfArrayTableModel.java b/src/main/java/com/itextpdf/rups/view/models/PdfArrayTableModel.java
index cecb6816..21bb3c87 100644
--- a/src/main/java/com/itextpdf/rups/view/models/PdfArrayTableModel.java
+++ b/src/main/java/com/itextpdf/rups/view/models/PdfArrayTableModel.java
@@ -130,8 +130,9 @@ public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
final String value = (String) aValue;
final PdfObject newValue = parser.parseString(value, parent);
if (newValue != null) {
- removeRow(rowIndex);
- addRow(rowIndex, newValue);
+ removeRow(rowIndex, true);
+ addRow(rowIndex, newValue, true);
+ fireTableCellUpdated(rowIndex,columnIndex);
}
}
}
@@ -153,8 +154,15 @@ public String getColumnName(int columnIndex) {
@Override
public void removeRow(int rowIndex) {
- fireTableRowsDeleted(rowIndex, rowIndex);
+ removeRow(rowIndex, false);
+ }
+
+ protected void removeRow(int rowIndex, boolean compoundAction) {
array.remove(rowIndex);
+ if(compoundAction){
+ return;
+ }
+ fireTableRowsDeleted(rowIndex, rowIndex);
fireTableDataChanged();
}
@@ -189,7 +197,7 @@ public void validateTempRow() {
LoggerHelper.warn(Language.ERROR_INDEX_NOT_IN_RANGE.getString(), getClass());
}
}
- addRow(index, value);
+ addRow(index, value,false);
tempValue = "";
fireTableDataChanged();
}
@@ -200,10 +208,17 @@ public int getButtonColumn() {
return 1;
}
- private void addRow(int index, PdfObject value) {
+ private void addRow(int index, PdfObject value, boolean compoundAction) {
array.add(index, value);
+ if (compoundAction){
+ return;
+ }
fireTableRowsInserted(index, index);
}
+ @Override
+ public PdfObject getPdfObject() {
+ return array;
+ }
}
diff --git a/src/test/java/com/itextpdf/rups/view/RupsUndoRedoTest.java b/src/test/java/com/itextpdf/rups/view/RupsUndoRedoTest.java
new file mode 100644
index 00000000..568335b7
--- /dev/null
+++ b/src/test/java/com/itextpdf/rups/view/RupsUndoRedoTest.java
@@ -0,0 +1,349 @@
+/*
+ This file is part of the iText (R) project.
+ Copyright (c) 1998-2022 iText Group NV
+ Authors: iText Software.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License version 3
+ as published by the Free Software Foundation with the addition of the
+ following permission added to Section 15 as permitted in Section 7(a):
+ FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
+ ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
+ OF THIRD PARTY RIGHTS
+
+ This program is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ or FITNESS FOR A PARTICULAR PURPOSE.
+ See the GNU Affero General Public License for more details.
+ You should have received a copy of the GNU Affero General Public License
+ along with this program; if not, see http://www.gnu.org/licenses or write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA, 02110-1301 USA, or download the license from the following URL:
+ http://itextpdf.com/terms-of-use/
+
+ The interactive user interfaces in modified source and object code versions
+ of this program must display Appropriate Legal Notices, as required under
+ Section 5 of the GNU Affero General Public License.
+
+ In accordance with Section 7(b) of the GNU Affero General Public License,
+ a covered work must retain the producer line in every PDF that is created
+ or manipulated using iText.
+
+ You can be released from the requirements of the license by purchasing
+ a commercial license. Buying such a license is mandatory as soon as you
+ develop commercial activities involving the iText software without
+ disclosing the source code of your own applications.
+ These activities include: offering paid services to customers as an ASP,
+ serving PDFs on the fly in a web application, shipping iText with a closed
+ source product.
+
+ For more information, please contact iText Software Corp. at this
+ address: sales@itextpdf.com
+ */
+package com.itextpdf.rups.view;
+
+import com.itextpdf.kernel.actions.data.ITextCoreProductData;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.uispec4j.Key;
+import org.uispec4j.Panel;
+import org.uispec4j.Table;
+import org.uispec4j.Tree;
+import org.uispec4j.Trigger;
+import org.uispec4j.UIComponent;
+import org.uispec4j.Window;
+import org.uispec4j.interception.WindowHandler;
+import org.uispec4j.interception.WindowInterceptor;
+
+@Tag("Integration")
+@Tag("GUI")
+public class RupsUndoRedoTest extends RupsWindowTest {
+
+ private static final String INPUT_FILE = "src/test/resources/com/itextpdf/rups/controller/hello_world.pdf";
+
+ private static Window MAIN_WINDOW;
+ private static Panel objectPanel;
+ private static Tree pdfTree;
+ private static final String TEST_KEY = "/Test";
+ private static final String MODIFICATION_KEY = "/Producer";
+ private static final String TEST_VALUE = "(Test)";
+
+ public RupsUndoRedoTest(){
+ super("Undo/Redo interaction Test");
+ }
+ private void validateInitialArray(Table infoArray) {
+ Assertions.assertTrue(infoArray.getHeader().contentEquals("Array", "").isTrue(), "Table Header identifies the Content as an Array: ");
+ Assertions.assertTrue(infoArray.rowCountEquals(3).isTrue(), "Table is 3 Cells Tall: ");
+ }
+
+ private void validateInitialDictionary(Table infoArray) {
+ Assertions.assertTrue(infoArray.getHeader().contentEquals("Key", "Value", "").isTrue(), "Table Header identifies the Content as an Dictionary: ");
+ Assertions.assertTrue(infoArray.rowCountEquals(4).isTrue(), "Table is 4 Cells Tall: ");
+ }
+
+ private void clickTree(String targetNode) {
+ clickTree(targetNode, 1);
+ }
+
+ private void doubleClickTree(String targetNode) {
+ clickTree(targetNode, 2);
+ }
+
+ private void clickTree(String targetNode, int times) {
+ pdfTree.selectRoot();
+ Assertions.assertTrue(pdfTree.contains(targetNode).isTrue(), String.format("PDFTree contains `%s` node: ", targetNode));
+ if (times == 2) {
+ pdfTree.doubleClick(targetNode);
+ } else {
+ while (times > 0) {
+ pdfTree.click(targetNode);
+ times--;
+ }
+ }
+ }
+
+ private static void UNDO(UIComponent component) {
+ component.pressKey(Key.control(Key.Z));
+ }
+
+ private static void REDO(UIComponent component) {
+ component.pressKey(Key.control(Key.Y));
+ }
+
+
+ @BeforeEach
+ public void before() {
+ try {
+ setUp(INPUT_FILE);
+ } catch (Exception e) {
+ Assertions.fail(e.getMessage());
+ e.printStackTrace(System.err);
+ }
+ MAIN_WINDOW = getMainWindow();
+
+ String windowTitle = String.format(Language.TITLE.getString(), ITextCoreProductData.getInstance().getVersion());
+ waitUntil(MAIN_WINDOW.titleEquals(windowTitle), 8000);
+ String documentPanelName = INPUT_FILE.substring(1 + INPUT_FILE.lastIndexOf('/'));
+ waitUntil(MAIN_WINDOW.getPanel(documentPanelName).isEnabled(), 8000);
+ pdfTree = MAIN_WINDOW.getTree("pdfTree");
+ objectPanel = MAIN_WINDOW.getPanel("PdfObjectPanel");
+ }
+
+ @Test
+ @Order(1)
+ public void phase1canUndoArrayAddition() {
+
+
+ clickTree("ID");
+ Assertions.assertEquals(2,pdfTree.getChildCount("ID"), "Array Tree has 2 children: ");
+
+ Table infoArray = objectPanel.getTable();
+ validateInitialArray(infoArray);
+
+ infoArray.editCell(2, 0, TEST_VALUE, true);
+
+ WindowInterceptor.init(infoArray.triggerClick(2, 1, Key.Modifier.NONE))
+ .process(new WindowHandler() {
+ @Override
+ public Trigger process(Window dialog) throws Exception {
+ Assertions.assertEquals("Input", dialog.getTitle(), "Dialog Title is 'Input'");
+ Assertions.assertEquals("2", dialog.getInputTextBox().getText(), "Row Value defaults to: 2");
+ return dialog.getButton("OK").triggerClick();
+ }
+ }).run();
+ waitUntil("Table is now 4 Cells Tall: ", infoArray.rowCountEquals(4), 5000);
+ Assertions.assertEquals(3,pdfTree.getChildCount("ID"), "Array Tree has 3 children: ");
+ Assertions.assertEquals(TEST_VALUE, (String) infoArray.getContentAt(2, 0), "Value of cell content is equal to the test value provided: ");
+ UNDO(infoArray);
+ Assertions.assertTrue(infoArray.rowCountEquals(3).isTrue(), "Table is 3 Cells Tall again: ");
+ Assertions.assertEquals("", infoArray.getContentAt(2, 0), "Test value is no longer in the table: ");
+ Assertions.assertEquals(2, pdfTree.getChildCount("ID"), "Array Tree has 2 children: ");
+
+
+ }
+
+ @Test
+ @Order(1)
+ public void phase1canUndoArrayUpdate() {
+ // Click Tree
+ clickTree("ID");
+ Assertions.assertEquals(2, pdfTree.getChildCount("ID"), "Array Tree has 2 children: ");
+ Table infoArray = objectPanel.getTable();
+ validateInitialArray(infoArray);
+ // Store Original Value
+ Assertions.assertEquals(String.class, infoArray.getContentAt(0, 0).getClass(), "Value is a String: ");
+ String originalValue = (String) infoArray.getContentAt(0, 0);
+ // Change Value to "(Test)"
+ infoArray.editCell(0, 0, TEST_VALUE, true);
+ // Press Enter Key
+ infoArray.pressKey(Key.ENTER);
+ // Test Updated value
+ Assertions.assertEquals(TEST_VALUE, (String) infoArray.getContentAt(0, 0), "Cell Value is equal to Test Value: ");
+ Assertions.assertEquals(2, pdfTree.getChildCount("ID"), "Array Tree still has 2 children: ");
+ // Press Ctrl-Z
+ UNDO(infoArray);
+ // Test Updated Value is back to Original
+ Assertions.assertEquals(originalValue, (String) infoArray.getContentAt(0,0), "Value has reset to original: ");
+ validateInitialArray(infoArray);
+ Assertions.assertEquals(2, pdfTree.getChildCount("ID"), "Array Tree still has 2 children: ");
+ }
+
+ @Test
+ @Order(1)
+ public void phase1canUndoArrayDeletion() {
+ // Click Tree
+ clickTree("ID");
+
+ Assertions.assertEquals(2, pdfTree.getChildCount("ID"), "Array Tree has 2 children: ");
+
+ Table infoArray = objectPanel.getTable();
+ validateInitialArray(infoArray);
+ // Store Original Value
+ Assertions.assertEquals(String.class, infoArray.getContentAt(0, 0).getClass(), "Value is a String: ");
+ String originalValue = (String) infoArray.getContentAt(0, 0);
+ // Click Table X Button
+ infoArray.click(0, 1, Key.Modifier.NONE);
+ // Test Tree Size
+ Assertions.assertEquals(1, pdfTree.getChildCount("ID"), "Array Tree has 1 child: ");
+ // Test Table Length
+ Assertions.assertEquals(2, infoArray.getRowCount(), "Table is now 2 Cells Tall: ");
+ // Press Ctrl-Z
+ UNDO(infoArray);
+ Assertions.assertEquals(2, pdfTree.getChildCount("ID"), "Array Tree has 2 children again: ");
+ // Test Table Length
+ validateInitialArray(infoArray);
+ // Test Updated Value is back to Original
+ Assertions.assertEquals(originalValue, (String) infoArray.getContentAt(0, 0), "Value has reset to original: ");
+ }
+
+ @Test
+ @Order(1)
+ public void phase1canUndoDictAddition() {
+ doubleClickTree("Info");
+ Assertions.assertEquals(3,pdfTree.getChildCount("Info/Dictionary"), "Dict Tree has 3 children: ");
+ Table infoDict = objectPanel.getTable();
+ validateInitialDictionary(infoDict);
+ infoDict.editCell(3,0,TEST_KEY,true);
+ infoDict.editCell(3,1,TEST_VALUE,true);
+ infoDict.click(3,2);
+ Assertions.assertEquals(4,pdfTree.getChildCount("Info/Dictionary"), "Dictionary Tree has 4 children: ");
+ Assertions.assertEquals(5, infoDict.getRowCount(), "Table is now 5 Rows tall: ");
+ Assertions.assertEquals(TEST_KEY, (String) infoDict.getContentAt(3,0), "New Key is equal to Test Key: ");
+ Assertions.assertEquals(TEST_VALUE, (String) infoDict.getContentAt(3,1), "New Value is equal to Test Value: ");
+ UNDO(infoDict);
+ Assertions.assertEquals(3,pdfTree.getChildCount("Info/Dictionary"), "Dict Tree has 3 children again: ");
+ validateInitialDictionary(infoDict);
+ assertFalse("Dict Tree no longer contains the Test Key: ", pdfTree.contains(String.format("Info/Dictionary%s",TEST_KEY)));
+ }
+
+ @Test
+ @Order(1)
+ public void phase1canUndoDictUpdate() {
+ doubleClickTree("Info");
+ Table infoDict = objectPanel.getTable();
+ validateInitialDictionary(infoDict);
+ String originalValue = (String) infoDict.getContentAt(2, 1);
+ Assertions.assertEquals(MODIFICATION_KEY, (String) infoDict.getContentAt(2,0), "Key to Modify is on the expected row: ");
+ infoDict.editCell(2,1,TEST_VALUE, true);
+ Assertions.assertEquals(3,pdfTree.getChildCount("Info/Dictionary"), "Dictionary Tree has 3 children: ");
+ Assertions.assertEquals(MODIFICATION_KEY, (String) infoDict.getContentAt(2,0), "Row Key is Unchanged");
+ Assertions.assertEquals(TEST_VALUE, (String) infoDict.getContentAt(2,1), "Modified Cell is set to Test Value");
+ UNDO(infoDict);
+ validateInitialDictionary(infoDict);
+ Assertions.assertEquals(originalValue, (String) infoDict.getContentAt(2,1), "Value has reset to original: ");
+ }
+
+ @Test
+ @Order(1)
+ public void phase1canUndoDictDeletion() {
+ doubleClickTree("Info");
+ Table infoDict = objectPanel.getTable();
+ validateInitialDictionary(infoDict);
+ infoDict.click(2,2);
+ Assertions.assertEquals(2,pdfTree.getChildCount("Info/Dictionary"), "Dictionary Tree has 2 children: ");
+ Assertions.assertEquals(3, infoDict.getRowCount(), "Table is now 3 Rows tall: ");
+ UNDO(infoDict);
+ Assertions.assertEquals(3,pdfTree.getChildCount("Info/Dictionary"), "Dictionary Tree has 3 children: ");
+ Assertions.assertEquals(4, infoDict.getRowCount(), "Table is now 4 Rows tall: ");
+ }
+
+ @Test
+ @Order(2)
+ public void phase2canRedoArrayAddition() {
+
+ // run canUndo test
+ phase1canUndoArrayAddition();
+
+ Table infoArray = objectPanel.getTable();
+ // Press Ctrl-Y
+ REDO(infoArray);
+ Assertions.assertEquals(3, pdfTree.getChildCount("ID"), "Array Tree has 3 children again: ");
+ // Test Table Length
+ Assertions.assertEquals(4, infoArray.getRowCount(), "Table is now 4 Cells Tall again: ");
+ // Test Table Value
+ Assertions.assertEquals(TEST_VALUE, (String) infoArray.getContentAt(2, 0), "Value of cell content is equal to the test value provided: ");
+ }
+
+ @Test
+ @Order(2)
+ public void phase2canRedoArrayUpdate() {
+ // run canUndo test
+ phase1canUndoArrayUpdate();
+ Table infoArray = objectPanel.getTable();
+ // Press Ctrl-Y
+ REDO(infoArray);
+ Assertions.assertEquals(2, pdfTree.getChildCount("ID"), "Array Tree still has 2 children: ");
+ // Test Table Value
+ Assertions.assertEquals(TEST_VALUE, (String) infoArray.getContentAt(0, 0), "Value of cell content is equal to the test value again: ");
+ }
+
+ @Test
+ @Order(2)
+ public void phase2canRedoArrayDeletion() {
+ // run canUndo test
+ phase1canUndoArrayDeletion();
+ Table infoArray = objectPanel.getTable();
+ // Press Ctrl-Y
+ REDO(infoArray);
+ Assertions.assertEquals(1, pdfTree.getChildCount("ID"), "Array Tree has 1 child again: ");
+ // Test Table Length
+ Assertions.assertEquals(2, infoArray.getRowCount(), "Table is now 2 Cells Tall again: ");
+ }
+
+ @Test
+ @Order(2)
+ public void phase2canRedoDictAddition() {
+ phase1canUndoDictAddition();
+ Table infoDict = objectPanel.getTable();
+ REDO(infoDict);
+ Assertions.assertEquals(4,pdfTree.getChildCount("Info/Dictionary"), "Dictionary Tree has 4 children: ");
+ Assertions.assertEquals(5, infoDict.getRowCount(), "Table is now 5 Rows tall: ");
+ Assertions.assertEquals(TEST_KEY, (String) infoDict.getContentAt(3,0), "New Key is equal to Test Key: ");
+ Assertions.assertEquals(TEST_VALUE, (String) infoDict.getContentAt(3,1), "New Value is equal to Test Value: ");
+ }
+
+ @Test
+ @Order(2)
+ public void phase2canRedoDictUpdate() {
+ phase1canUndoDictUpdate();
+ Table infoDict = objectPanel.getTable();
+ REDO(infoDict);
+ Assertions.assertEquals(3,pdfTree.getChildCount("Info/Dictionary"), "Dictionary Tree has 3 children: ");
+ Assertions.assertEquals(4, infoDict.getRowCount(), "Table is still 4 Rows tall: ");
+ Assertions.assertEquals(MODIFICATION_KEY, (String) infoDict.getContentAt(2,0), "Modification Key is unchanged: ");
+ Assertions.assertEquals(TEST_VALUE, (String) infoDict.getContentAt(2,1), "New Value is equal to Test Value: ");
+ }
+
+ @Test
+ @Order(2)
+ public void phase2canRedoDictDeletion() {
+ phase1canUndoDictDeletion();
+ Table infoDict = objectPanel.getTable();
+ REDO(infoDict);
+ Assertions.assertEquals(2,pdfTree.getChildCount("Info/Dictionary"), "Dictionary Tree has 2 children: ");
+ Assertions.assertEquals(3, infoDict.getRowCount(), "Table is now 3 Rows tall: ");
+ }
+}
diff --git a/src/test/java/com/itextpdf/rups/view/RupsWindowTest.java b/src/test/java/com/itextpdf/rups/view/RupsWindowTest.java
new file mode 100644
index 00000000..60efc4bb
--- /dev/null
+++ b/src/test/java/com/itextpdf/rups/view/RupsWindowTest.java
@@ -0,0 +1,273 @@
+/*
+ This file is part of the iText (R) project.
+ Copyright (c) 1998-2022 iText Group NV
+ Authors: iText Software.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License version 3
+ as published by the Free Software Foundation with the addition of the
+ following permission added to Section 15 as permitted in Section 7(a):
+ FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
+ ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
+ OF THIRD PARTY RIGHTS
+
+ This program is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ or FITNESS FOR A PARTICULAR PURPOSE.
+ See the GNU Affero General Public License for more details.
+ You should have received a copy of the GNU Affero General Public License
+ along with this program; if not, see http://www.gnu.org/licenses or write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA, 02110-1301 USA, or download the license from the following URL:
+ http://itextpdf.com/terms-of-use/
+
+ The interactive user interfaces in modified source and object code versions
+ of this program must display Appropriate Legal Notices, as required under
+ Section 5 of the GNU Affero General Public License.
+
+ In accordance with Section 7(b) of the GNU Affero General Public License,
+ a covered work must retain the producer line in every PDF that is created
+ or manipulated using iText.
+
+ You can be released from the requirements of the license by purchasing
+ a commercial license. Buying such a license is mandatory as soon as you
+ develop commercial activities involving the iText software without
+ disclosing the source code of your own applications.
+ These activities include: offering paid services to customers as an ASP,
+ serving PDFs on the fly in a web application, shipping iText with a closed
+ source product.
+
+ For more information, please contact iText Software Corp. at this
+ address: sales@itextpdf.com
+ */
+package com.itextpdf.rups.view;
+
+import com.itextpdf.rups.RupsLauncher;
+import org.junit.jupiter.api.Tag;
+import org.uispec4j.UISpec4J;
+import org.uispec4j.UISpecAdapter;
+import org.uispec4j.Window;
+import org.uispec4j.assertion.Assertion;
+import org.uispec4j.assertion.UISpecAssert;
+import org.uispec4j.interception.MainClassAdapter;
+import org.uispec4j.interception.toolkit.UISpecDisplay;
+
+@Tag("Integration")
+@Tag("GUI")
+public abstract class RupsWindowTest{
+ static final String ADAPTER_CLASS_PROPERTY = "uispec4j.adapter";
+ static final String PROPERTY_NOT_DEFINED;
+
+ private UISpecAdapter adapter;
+
+ static {
+ PROPERTY_NOT_DEFINED =
+ "Adapter class not defined - the '" + ADAPTER_CLASS_PROPERTY +
+ "' property must refer to a class implementing the UISpecAdapter interface";
+ UISpec4J.init();
+ }
+
+ public RupsWindowTest() {
+ this("Window Test");
+ }
+ public RupsWindowTest(String testName) {
+ }
+
+ public void setAdapter(UISpecAdapter adapter) {
+ this.adapter = adapter;
+ }
+
+
+ protected void setUp() throws Exception {
+ setUp("");
+ }
+
+ /**
+ * Initializes the resources needed by the test case.
+ * NB: If you provide your own implementation, do not forget to call this one first.
+ */
+
+
+ protected void setUp(String filePath) throws Exception {
+ String fileName = filePath.substring(1 + filePath.lastIndexOf('/'));
+ UISpecDisplay.instance().reset();
+ setAdapter(new MainClassAdapter(RupsLauncher.class, new String[]{filePath}));
+ waitUntil(getMainWindow().containsSwingComponent(RupsPanel.class), 8000);
+ }
+
+ /**
+ * Checks whether an unexpected exception had occurred, and releases the test resources.
+ */
+ protected void tearDown() throws Exception {
+ adapter = null;
+ UISpecDisplay.instance().rethrowIfNeeded();
+ UISpecDisplay.instance().reset();
+ }
+
+ private void retrieveAdapter() throws AdapterNotFoundException {
+ String adapterClassName = System.getProperty(ADAPTER_CLASS_PROPERTY);
+ if (adapterClassName == null) {
+ throw new AdapterNotFoundException();
+ }
+ try {
+ adapter = (UISpecAdapter)Class.forName(adapterClassName).getDeclaredConstructor().newInstance();
+ }
+ catch (Exception e) {
+ throw new AdapterNotFoundException(adapterClassName, e);
+ }
+ }
+
+ /**
+ * Returns the Window created by the adapter.
+ *
+ * @throws AdapterNotFoundException if the uispec4j.adapter
property does not refer
+ * to a valid adapter
+ */
+ public Window getMainWindow() throws AdapterNotFoundException {
+ // TODO: Find a way to intercept and interact with Modal Dialogs
+ return getAdapter().getMainWindow();
+ }
+
+ /**
+ * Checks the given assertion.
+ * This method is equivalent to {@link #assertThat(Assertion)}.
+ *
+ * @see UISpecAssert#assertTrue(Assertion)
+ */
+ public void assertTrue(Assertion assertion) {
+ UISpecAssert.assertTrue(assertion);
+ }
+
+ /**
+ * Checks the given assertion.
+ * If it fails an AssertionError is thrown with the given message.
+ * This method is equivalent to {@link #assertThat(String,Assertion)}.
+ *
+ * @see UISpecAssert#assertTrue(String,Assertion)
+ */
+ public void assertTrue(String message, Assertion assertion) {
+ UISpecAssert.assertTrue(message, assertion);
+ }
+
+ /**
+ * Checks the given assertion.
+ * This method is equivalent to {@link #assertTrue(Assertion)}.
+ *
+ * @see UISpecAssert#assertThat(Assertion)
+ */
+ public void assertThat(Assertion assertion) {
+ UISpecAssert.assertThat(assertion);
+ }
+
+ /**
+ * Checks the given assertion.
+ * If it fails an AssertionError is thrown with the given message.
+ * This method is equivalent to {@link #assertTrue(String,Assertion)}.
+ *
+ * @see UISpecAssert#assertTrue(String,Assertion)
+ */
+ public void assertThat(String message, Assertion assertion) {
+ UISpecAssert.assertThat(message, assertion);
+ }
+
+ /**
+ * Waits for at most 'waitTimeLimit' ms until the assertion is true.
+ *
+ * @see UISpecAssert#waitUntil(Assertion, long)
+ */
+ public void waitUntil(Assertion assertion, long waitTimeLimit) {
+ UISpecAssert.waitUntil(assertion, waitTimeLimit);
+ }
+
+ /**
+ * Checks that the given assertion fails.
+ *
+ * @see UISpecAssert#assertFalse(Assertion)
+ */
+ public void assertFalse(Assertion assertion) {
+ UISpecAssert.assertFalse(assertion);
+ }
+
+ /**
+ * Waits for at most 'waitTimeLimit' ms until the assertion is true.
+ * If it fails an AssertionError is thrown with the given message.
+ *
+ * @see UISpecAssert#waitUntil(String,Assertion,long)
+ */
+ public void waitUntil(String message, Assertion assertion, long waitTimeLimit) {
+ UISpecAssert.waitUntil(message, assertion, waitTimeLimit);
+ }
+
+ /**
+ * Checks that the given assertion fails.
+ * If it succeeds an AssertionError is thrown with the given message.
+ *
+ * @see UISpecAssert#assertFalse(String,Assertion)
+ */
+ public void assertFalse(String message, Assertion assertion) {
+ UISpecAssert.assertFalse(message, assertion);
+ }
+
+ /**
+ * Returns a negation of the given assertion.
+ *
+ * @see UISpecAssert#not(Assertion)
+ */
+ public Assertion not(Assertion assertion) {
+ return UISpecAssert.not(assertion);
+ }
+
+ /**
+ * Returns the intersection of two assertions.
+ *
+ * @see UISpecAssert#and(Assertion[])
+ */
+ public Assertion and(Assertion... assertions) {
+ return UISpecAssert.and(assertions);
+ }
+
+ /**
+ * Returns the union of two assertions.
+ *
+ * @see UISpecAssert#or(Assertion[])
+ */
+ public Assertion or(Assertion... assertions) {
+ return UISpecAssert.or(assertions);
+ }
+
+ /**
+ * Checks that the given assertion equals the expected parameter.
+ *
+ * @see UISpecAssert#assertEquals(boolean,Assertion)
+ */
+ public void assertEquals(boolean expected, Assertion assertion) {
+ UISpecAssert.assertEquals(expected, assertion);
+ }
+
+ /**
+ * Checks that the given assertion equals the expected parameter.
+ * If it fails an AssertionError is thrown with the given message.
+ *
+ * @see UISpecAssert#assertEquals(String,boolean,Assertion)
+ */
+ public void assertEquals(String message, boolean expected, Assertion assertion) {
+ UISpecAssert.assertEquals(message, expected, assertion);
+ }
+
+ private UISpecAdapter getAdapter() throws AdapterNotFoundException {
+ if (adapter == null) {
+ retrieveAdapter();
+ }
+ return adapter;
+ }
+
+ static class AdapterNotFoundException extends RuntimeException {
+ public AdapterNotFoundException() {
+ super(PROPERTY_NOT_DEFINED);
+ }
+
+ public AdapterNotFoundException(String adapterClassName, Exception e) {
+ super("Adapter class '" + adapterClassName + "' not found", e);
+ }
+ }
+}
\ No newline at end of file