Author: tilman
Date: Tue Aug 25 11:04:53 2026
New Revision: 1937396
Log:
PDFBOX-6240: change rectangle collecting to be done after rendering; improve
map; display popup window for most annotations (swing parts by github copilot)
Modified:
pdfbox/branches/2.0/debugger/src/main/java/org/apache/pdfbox/debugger/pagepane/PagePane.java
Modified:
pdfbox/branches/2.0/debugger/src/main/java/org/apache/pdfbox/debugger/pagepane/PagePane.java
==============================================================================
---
pdfbox/branches/2.0/debugger/src/main/java/org/apache/pdfbox/debugger/pagepane/PagePane.java
Tue Aug 25 11:04:50 2026 (r1937395)
+++
pdfbox/branches/2.0/debugger/src/main/java/org/apache/pdfbox/debugger/pagepane/PagePane.java
Tue Aug 25 11:04:53 2026 (r1937396)
@@ -16,12 +16,14 @@
package org.apache.pdfbox.debugger.pagepane;
+import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Desktop;
import java.awt.Font;
import java.awt.GraphicsEnvironment;
+import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
@@ -35,12 +37,16 @@ import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ExecutionException;
+import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
+import javax.swing.JTextArea;
+import javax.swing.JWindow;
import javax.swing.SwingWorker;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
@@ -66,6 +72,7 @@ import org.apache.pdfbox.pdmodel.interac
import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationMarkup;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import
org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDDestination;
import
org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDNamedDestination;
@@ -94,9 +101,12 @@ public class PagePane implements ActionL
private final PDPage page;
private String labelText = "";
private String currentURI = "";
- private final Map<PDRectangle, String> rectMap = new HashMap<PDRectangle,
String>();
+ private final Map<PDAnnotation,String> map = new
HashMap<PDAnnotation,String>();
private final AffineTransform defaultTransform =
GraphicsEnvironment.getLocalGraphicsEnvironment().
getDefaultScreenDevice().getDefaultConfiguration().getDefaultTransform();
+ private JWindow hoverWindow;
+ private JTextArea hoverTextArea;
+ private PDRectangle hoveredRectangle;
public PagePane(PDDocument document, COSDictionary pageDict, JLabel
statuslabel)
{
@@ -112,7 +122,7 @@ public class PagePane implements ActionL
public void init()
{
initUI();
- initRectMap();
+ initHoverPopup();
}
private void initRectMap()
@@ -121,6 +131,7 @@ public class PagePane implements ActionL
{
collectFieldLocations();
collectLinkLocations();
+ collectPopupLocations();
}
catch (IOException ex)
{
@@ -128,6 +139,27 @@ public class PagePane implements ActionL
}
}
+ private void initHoverPopup()
+ {
+ // code for hover window (here and elsewhere) by github copilot
+ hoverWindow = new JWindow();
+ hoverWindow.setAlwaysOnTop(true);
+
+ hoverTextArea = new JTextArea(4, 24);
+ hoverTextArea.setEditable(false);
+ hoverTextArea.setOpaque(true);
+ hoverTextArea.setLineWrap(true);
+ hoverTextArea.setWrapStyleWord(true);
+ hoverTextArea.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8));
+
+ JPanel content = new JPanel(new BorderLayout());
+ content.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1)); //
thin border
+ content.add(hoverTextArea, BorderLayout.CENTER);
+
+ hoverWindow.setContentPane(content);
+ hoverWindow.pack();
+ }
+
private void collectLinkLocations() throws IOException
{
for (PDAnnotation annotation : page.getAnnotations())
@@ -141,7 +173,8 @@ public class PagePane implements ActionL
private void collectLinkLocation(PDAnnotationLink linkAnnotation) throws
IOException
{
- if (linkAnnotation.getRectangle() == null)
+ PDRectangle rectangle = linkAnnotation.getRectangle();
+ if (rectangle == null)
{
return;
}
@@ -149,7 +182,7 @@ public class PagePane implements ActionL
if (action instanceof PDActionURI)
{
PDActionURI uriAction = (PDActionURI) action;
- rectMap.put(linkAnnotation.getRectangle(), "URI: " +
uriAction.getURI());
+ map.put(linkAnnotation, "URI: " + uriAction.getURI());
return;
}
PDDestination destination = null;
@@ -180,7 +213,7 @@ public class PagePane implements ActionL
int pageNum = pageDestination.retrievePageNumber();
if (pageNum != -1)
{
- rectMap.put(linkAnnotation.getRectangle(), "Page destination:
" + (pageNum + 1));
+ map.put(linkAnnotation, "Page destination: " + (pageNum + 1));
}
}
}
@@ -207,14 +240,32 @@ public class PagePane implements ActionL
{
// check if the annotation widget is on this page
// (checking widget.getPage() also works, but it is sometimes
null)
- if (dictionarySet.contains(widget.getCOSObject()) &&
widget.getRectangle() != null)
+ PDRectangle rectangle = widget.getRectangle();
+ if (dictionarySet.contains(widget.getCOSObject()) && rectangle
!= null)
{
- rectMap.put(widget.getRectangle(), "Field name: " +
field.getFullyQualifiedName() + ", value: " + field.getValueAsString());
+ map.put(widget, "Field name: " +
field.getFullyQualifiedName() + ", value: " + field.getValueAsString());
}
}
}
}
+ private void collectPopupLocations() throws IOException
+ {
+ // collect rectangles where a popup shall be displayed.
+ // Not to be confused with "popup annotations" which are for editing
+ for (PDAnnotation annotation : page.getAnnotations())
+ {
+ PDRectangle rectangle = annotation.getRectangle();
+ String contents = annotation.getContents();
+ if (annotation instanceof PDAnnotationMarkup &&
PDAnnotationMarkup.SUB_TYPE_FREETEXT.equals(annotation.getSubtype()) ||
+ rectangle == null || contents == null || contents.isEmpty())
+ {
+ continue;
+ }
+ map.put(annotation, contents);
+ }
+ }
+
private void initUI()
{
panel = new JPanel();
@@ -360,6 +411,9 @@ public class PagePane implements ActionL
float y = e.getY() / zoomScale * (float) defaultTransform.getScaleY();
int x1;
int y1;
+ PDRectangle hitRect = null;
+ String hitText = null;
+
switch ((RotationMenu.getRotationDegrees() + page.getRotation()) % 360)
{
case 90:
@@ -382,26 +436,73 @@ public class PagePane implements ActionL
}
String text = "x: " + x1 + ", y: " + y1;
- // are we in a field widget or a link annotation?
+ // are we in a field widget, a link annotation, or a text annotation
with a popup?
Cursor cursor = Cursor.getDefaultCursor();
currentURI = "";
- for (Map.Entry<PDRectangle,String> entry : rectMap.entrySet())
+ for (Entry<PDAnnotation,String> entry : map.entrySet())
{
- if (entry.getKey().contains(x1, y1))
+ PDAnnotation annotation = entry.getKey();
+ PDRectangle rectangle = annotation.getRectangle();
+ if (rectangle.contains(x1, y1))
{
- String s = rectMap.get(entry.getKey());
- text += ", " + s;
- if (s.startsWith("URI: "))
+ String s = entry.getValue();
+ if (annotation instanceof PDAnnotationLink &&
s.startsWith("URI: "))
{
currentURI = s.substring(5);
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
}
+ String contents = annotation.getContents();
+ if (annotation.getContents() != null && !contents.isEmpty())
+ {
+ hitRect = rectangle;
+ hitText = entry.getValue();
+ }
+ text += ", " + s;
break;
}
}
panel.setCursor(cursor);
statuslabel.setText(text);
+
+ if (hitRect != null)
+ {
+ if (hoveredRectangle != hitRect)
+ {
+ hoveredRectangle = hitRect;
+ showHoverPopup(e, hitText);
+ }
+ else
+ {
+ showHoverPopup(e, hitText);
+ }
+ }
+ else
+ {
+ hideHoverPopup();
+ }
+ }
+
+ private void showHoverPopup(MouseEvent e, String text)
+ {
+ hoverTextArea.setText(text);
+ hoverWindow.pack();
+
+ Point screen = e.getLocationOnScreen();
+ hoverWindow.setLocation(screen.x + 12, screen.y + 12);
+ if (!hoverWindow.isVisible())
+ {
+ hoverWindow.setVisible(true);
+ }
+ }
+
+ private void hideHoverPopup()
+ {
+ if (hoverWindow != null && hoverWindow.isVisible())
+ {
+ hoverWindow.setVisible(false);
+ }
+ hoveredRectangle = null;
}
@Override
@@ -484,6 +585,7 @@ public class PagePane implements ActionL
public void mouseExited(MouseEvent e)
{
statuslabel.setText(labelText);
+ hideHoverPopup();
}
/**
@@ -530,6 +632,9 @@ public class PagePane implements ActionL
(int) Math.ceil(image.getHeight() /
defaultTransform.getScaleY()));
label.setIcon(new HighResolutionImageIcon(image,
label.getWidth(), label.getHeight()));
label.setText(null);
+
+ // initialize after rendering because annotation rectangles
may have been adjusted
+ initRectMap();
}
catch (InterruptedException ex)
{