Google Tag Manager

Showing posts with label JEditorPane. Show all posts
Showing posts with label JEditorPane. Show all posts

2025/04/30

Switch JEditorPane's StyleSheet to Light or Dark Theme

Code

public static void updateTheme(JEditorPane editor) {
  updateTheme(editor, isDarkMode());
}

public static void updateTheme(JEditorPane editor, boolean isDark) {
  EditorKit kit = editor.getEditorKit();
  HTMLEditorKit htmlEditorKit;
  if (kit instanceof HTMLEditorKit) {
    htmlEditorKit = (HTMLEditorKit) kit;
  } else {
    htmlEditorKit = new HTMLEditorKit();
  }
  if (isDark) {
    htmlEditorKit.setStyleSheet(makeDarkStyleSheet());
    editor.setBackground(new Color(0x1E_1F_22));
  } else {
    htmlEditorKit.setStyleSheet(makeLightStyleSheet());
    editor.setBackground(new Color(0xEE_EE_EE));
  }
  editor.setEditorKit(htmlEditorKit);
}

public static boolean isDarkMode() {
  boolean isDark;
  String os = System.getProperty("os.name").toLowerCase(Locale.ENGLISH);
  if (os.contains("windows")) {
    isDark = isWindowsDarkMode();
  } else if (os.contains("linux")) {
    isDark = isLinuxDarkMode();
  } else if (os.contains("mac")) {
    isDark = isMackDarkMode();
  } else {
    isDark = false;
  }
  return isDark;
}

public static String getProcessOutput(String... cmd)
      throws IOException, InterruptedException {
  ProcessBuilder builder = new ProcessBuilder(cmd);
  builder.redirectErrorStream(true);
  Process p = builder.start();
  String str;
  try (BufferedReader pr = new BufferedReader(
            new InputStreamReader(p.getInputStream()))) {
    str = pr.lines().collect(Collectors.joining(System.lineSeparator()));
  }
  return str;
}

public static boolean isWindowsDarkMode() {
  String[] cmd = {
      "powershell.exe",
      "Get-ItemPropertyValue",
      "-Path",
      "HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
      "-Name",
      "AppsUseLightTheme;"
  };
  boolean isDarkMode;
  try {
    String str = getProcessOutput(cmd).trim();
    isDarkMode = Objects.equals("0", str);
  } catch (IOException | InterruptedException ex) {
    Logger.getGlobal().severe(ex::getMessage);
    isDarkMode = false;
  }
  return isDarkMode;
}

public static boolean isLinuxDarkMode() {
  Toolkit tk = Toolkit.getDefaultToolkit();
  Object theme = tk.getDesktopProperty("gnome.Net/ThemeName");
  return Objects.toString(theme).contains("dark");
}

public static boolean isMackDarkMode() {
  return false;
}

// https://raw.githack.com/google/code-prettify/master/styles/index.html
// Prettify Themes Gallery
public static StyleSheet makeLightStyleSheet() {
  StyleSheet styleSheet = new StyleSheet();
  styleSheet.addRule("pre{background:#eeeeee}");
  styleSheet.addRule(".str{color:#008800}");
  styleSheet.addRule(".kwd{color:#000088}");
  styleSheet.addRule(".com{color:#880000}");
  styleSheet.addRule(".typ{color:#660066}");
  styleSheet.addRule(".lit{color:#006666}");
  styleSheet.addRule(".pun{color:#666600}");
  styleSheet.addRule(".pln{color:#000000}");
  styleSheet.addRule(".tag{color:#000088}");
  styleSheet.addRule(".atn{color:#660066}");
  styleSheet.addRule(".atv{color:#008800}");
  styleSheet.addRule(".dec{color:#660066}");
  return styleSheet;
}

public static StyleSheet makeDarkStyleSheet() {
  StyleSheet styleSheet = new StyleSheet();
  styleSheet.addRule("pre{background:#1e1f22}");
  styleSheet.addRule(".str{color:#ffa0a0}");
  styleSheet.addRule(".kwd{color:#f0e68c;font-weight:bold}");
  styleSheet.addRule(".com{color:#87ceeb}");
  styleSheet.addRule(".typ{color:#98fb98}");
  styleSheet.addRule(".lit{color:#cd5c5c}");
  styleSheet.addRule(".pun{color:#ffffff}");
  styleSheet.addRule(".pln{color:#ffffff}");
  styleSheet.addRule(".tag{color:#f0e68c;font-weight:bold}");
  styleSheet.addRule(".atn{color:#bdb76b;font-weight:bold}");
  styleSheet.addRule(".atv{color:#ffa0a0}");
  styleSheet.addRule(".dec{color:#98fb98}");
  return styleSheet;
}

References

2025/03/31

Rounds all corners of a JTextComponent's selection highlight polygon and fills it with a translucent background color

Code

class RoundedSelectionHighlightPainter extends DefaultHighlightPainter {
  protected RoundedSelectionHighlightPainter() {
    super(null);
  }

  @Override public void paint(
      Graphics g, int offs0, int offs1, Shape bounds, JTextComponent c) {
    Graphics2D g2 = (Graphics2D) g.create();
    g2.setRenderingHint(
      RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    int rgba = c.getSelectionColor().getRGB() & 0xFF_FF_FF | (64 << 24);
    g2.setColor(new Color(rgba, true));
    try {
      Area area = getLinesArea(c, offs0, offs1);
      for (Area a : GeomUtils.singularization(area)) {
        List<Point2D> lst = GeomUtils.convertAreaToPoint2DList(a);
        GeomUtils.flatteningStepsOnRightSide(lst, 3d * 2d);
        g2.fill(GeomUtils.convertRoundedPath(lst, 3d));
      }
    } catch (BadLocationException ex) {
      // can't render
      Logger.getGlobal().severe(ex::getMessage);
    }
    g2.dispose();
  }

  private static Area getLinesArea(JTextComponent c, int offs0, int offs1)
      throws BadLocationException {
    TextUI mapper = c.getUI();
    Area area = new Area();
    int cur = offs0;
    do {
      int startOffset = Utilities.getRowStart(c, cur);
      int endOffset = Utilities.getRowEnd(c, cur);
      Rectangle p0 = mapper.modelToView(c, Math.max(startOffset, offs0));
      Rectangle p1 = mapper.modelToView(c, Math.min(endOffset, offs1));
      if (offs1 > endOffset) {
        p1.width += 6;
      }
      addRectToArea(area, p0.union(p1));
      cur = endOffset + 1;
    } while (cur < offs1);
    return area;
  }

  private static void addRectToArea(Area area, Rectangle rect) {
    area.add(new Area(rect));
  }
}

class RoundedSelectionCaret extends DefaultCaret {
  @Override protected HighlightPainter getSelectionPainter() {
    return new RoundedSelectionHighlightPainter();
  }

  @SuppressWarnings("PMD.AvoidSynchronizedAtMethodLevel")
  @Override protected synchronized void damage(Rectangle r) {
    JTextComponent c = getComponent();
    int startOffset = c.getSelectionStart();
    int endOffset = c.getSelectionEnd();
    if (startOffset == endOffset) {
      super.damage(r);
    } else {
      TextUI mapper = c.getUI();
      try {
        Rectangle p0 = mapper.modelToView(c, startOffset);
        Rectangle p1 = mapper.modelToView(c, endOffset);
        int h = (int) (p1.getMaxY() - p0.getMinY());
        c.repaint(new Rectangle(0, p0.y, c.getWidth(), h));
      } catch (BadLocationException ex) {
        UIManager.getLookAndFeel().provideErrorFeedback(c);
      }
    }
  }
}

References

2020/05/31

Make text in a JToolTip selectable and copyable

Code

JEditorPane hint = new JEditorPane();
hint.setEditorKit(new HTMLEditorKit());
hint.setEditable(false);
hint.setOpaque(false);

JCheckBox check = new JCheckBox();
check.setOpaque(false);

JPanel panel = new JPanel(new BorderLayout());
panel.add(hint);
panel.add(check, BorderLayout.EAST);

JPopupMenu popup = new JPopupMenu();
popup.add(new JScrollPane(panel));
popup.setBorder(BorderFactory.createEmptyBorder());

JEditorPane editor = new JEditorPane() {
  @Override public JToolTip createToolTip() {
    JToolTip tip = super.createToolTip();
    tip.addHierarchyListener(e -> {
      if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0
            && e.getComponent().isShowing()) {
        panel.setBackground(tip.getBackground());
        popup.show(tip, 0, 0);
      }
    });
    return tip;
  }
};
editor.setEditorKit(new HTMLEditorKit());
editor.setText(HTML_TEXT);
editor.setEditable(false);
editor.addHyperlinkListener(e -> {
  JEditorPane editorPane = (JEditorPane) e.getSource();
  if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
    JOptionPane.showMessageDialog(editorPane, "You click the link with the URL " + e.getURL());
  } else if (e.getEventType() == HyperlinkEvent.EventType.ENTERED) {
    editorPane.setToolTipText("");
    Optional.ofNullable(e.getSourceElement())
        .map(elem -> (AttributeSet) elem.getAttributes().getAttribute(HTML.Tag.A))
        .ifPresent(attr -> {
          String title = Objects.toString(attr.getAttribute(HTML.Attribute.TITLE));
          String url = Objects.toString(e.getURL());
          // String url = Objects.toString(attr.getAttribute(HTML.Attribute.HREF));
          hint.setText(String.format("%s: %s", title, url, url));
          popup.pack();
        });
  } else if (e.getEventType() == HyperlinkEvent.EventType.EXITED) {
    editorPane.setToolTipText(null);
  }
});

  • Override JComponent#createToolTip() method to add HierarchyListener to JToolTip
  • Show JPopupMenu with JToolTip as parent when JToolTip is visible
    • JToolTip hides behind JPopupMenu
    • JPopupMenu adds a JPanel with a JEditorPane and a JCheckBox instead of a JMenuItem
  • Hiding the parent JToolTip with the mouse cursor does not close the JPopupMenu, so you can click the internal JCheckBox or select the text in the JEditorPane and copy it with Ctrl-C, etc
    • It is a normal JPopupMenu, so it is hidden when the focus is moved by clicking on the parent JFrame etc

References

2020/01/30

Automatically update JTree node selection based on scroll position to indicate which link is currently active in the viewport of JEditorPane

Code

JScrollPane scroll = new JScrollPane(editor);
scroll.getVerticalScrollBar().getModel().addChangeListener(e -> {
  HTMLDocument.Iterator itr = doc.getIterator(HTML.Tag.A);
  for (; itr.isValid(); itr.next()) {
    try {
      Rectangle r = editor.modelToView(itr.getStartOffset());
      if (r != null && editor.getVisibleRect().contains(r.getLocation())) {
        searchTreeNode(tree, itr.getAttributes().getAttribute(HTML.Attribute.NAME));
        break;
      }
    } catch (BadLocationException ex) {
      UIManager.getLookAndFeel().provideErrorFeedback(editor);
    }
  }
});
// ...
tree.addTreeSelectionListener(e -> {
  if (!tree.isEnabled()) { // Ignore node selection from JEditorPane side
    return;
  }
  Object o = e.getNewLeadSelectionPath().getLastPathComponent();
  if (o instanceof DefaultMutableTreeNode) {
    DefaultMutableTreeNode node = (DefaultMutableTreeNode) o;
    String ref = Objects.toString(node.getUserObject());
    editor.scrollToReference(ref);
  }
});
// ...
private static void searchTreeNode(JTree tree, Object name) {
  TreeModel model = tree.getModel();
  DefaultMutableTreeNode root = (DefaultMutableTreeNode) model.getRoot();
  Collections.list((Enumeration<?>) root.preorderEnumeration()).stream()
      .filter(DefaultMutableTreeNode.class::isInstance)
      .map(DefaultMutableTreeNode.class::cast)
      .filter(node -> Objects.equals(name, Objects.toString(node.getUserObject())))
      .findFirst()
      .ifPresent(node -> {
        tree.setEnabled(false); // Disable TreeSelectionListener in JTree
        TreePath path = new TreePath(node.getPath());
        tree.setSelectionPath(path);
        tree.scrollPathToVisible(path);
        tree.setEnabled(true); // Restores TreeSelectionListener in JTree
      });
}

References

2019/09/30

Change drawing area of selected highlight in JEditorPane

Code

class ParagraphMarkHighlightPainter extends DefaultHighlightPainter {
  protected ParagraphMarkHighlightPainter(Color color) {
    super(color);
  }

  @Override public Shape paintLayer(
      Graphics g, int offs0, int offs1,
      Shape bounds, JTextComponent c, View view) {
    Shape s = super.paintLayer(g, offs0, offs1, bounds, c, view);
    Rectangle r = s.getBounds();
    if (r.width - 1 <= 0) {
      g.fillRect(r.x + r.width, r.y, r.width + r.height / 2, r.height);
    }
    return s;
  }
}

class WholeLineHighlightPainter extends DefaultHighlightPainter {
  protected WholeLineHighlightPainter(Color color) {
    super(color);
  }

  @Override public Shape paintLayer(
      Graphics g, int offs0, int offs1,
      Shape bounds, JTextComponent c, View view) {
    Rectangle rect = bounds.getBounds();
    rect.width = c.getSize().width;
    return super.paintLayer(g, offs0, offs1, rect, c, view);
  }
}

References

2019/08/31

Create a minimap of JEditorPane with ImageIcon and overlay it on JScrollPane using LayoutManager

Code

private final JEditorPane editor = new JEditorPane();
private final JScrollPane scroll = new JScrollPane(editor);
private final JLabel label = new JLabel() {
  private transient MouseInputListener handler;
  @Override public void updateUI() {
    removeMouseListener(handler);
    removeMouseMotionListener(handler);
    super.updateUI();
    handler = new MiniMapHandler();
    addMouseListener(handler);
    addMouseMotionListener(handler);
  }

  @Override public void paintComponent(Graphics g) {
    super.paintComponent(g);
    Container c = SwingUtilities.getAncestorOfClass(JViewport.class, editor);
    if (!(c instanceof JViewport) || editor == null) {
      return;
    }
    JViewport vport = (JViewport) c;
    Rectangle vrect = vport.getBounds(); // scroll.getViewportBorderBounds();
    Rectangle erect = editor.getBounds();
    Rectangle crect = SwingUtilities.calculateInnerArea(this, new Rectangle());

    Graphics2D g2 = (Graphics2D) g.create();
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                        RenderingHints.VALUE_ANTIALIAS_ON);
    double sy = crect.getHeight() / erect.getHeight();
    AffineTransform at = AffineTransform.getScaleInstance(1d, sy);

    // paint Thumb
    Rectangle thumbRect = new Rectangle(vrect);
    thumbRect.y = vport.getViewPosition().y;
    Rectangle r = at.createTransformedShape(thumbRect).getBounds();
    int y = crect.y + r.y;
    g2.setColor(THUMB_COLOR);
    g2.fillRect(0, y, crect.width, r.height);
    g2.setColor(THUMB_COLOR.darker());
    g2.drawRect(0, y, crect.width - 1, r.height - 1);
    g2.dispose();
  }
};

private class MiniMapHandler extends MouseInputAdapter {
  @Override public void mousePressed(MouseEvent e) {
    processMiniMapMouseEvent(e);
  }

  @Override public void mouseDragged(MouseEvent e) {
    processMiniMapMouseEvent(e);
  }

  protected final void processMiniMapMouseEvent(MouseEvent e) {
    Point pt = e.getPoint();
    Component c = (Component) e.getComponent();
    BoundedRangeModel m = scroll.getVerticalScrollBar().getModel();
    int brm = m.getMaximum() - m.getMinimum();
    int iv = (int) (.5 - m.getExtent() * .5 + pt.y * brm / (double) c.getHeight());
    m.setValue(iv);
  }
}
// ...
scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
scroll.getVerticalScrollBar().getModel().addChangeListener(e -> label.repaint());

JPanel pp = new JPanel(new BorderLayout(0, 0));
pp.add(label, BorderLayout.NORTH);
JScrollPane minimap = new JScrollPane(pp);
minimap.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
minimap.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);

Box box = Box.createHorizontalBox();
box.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
box.add(Box.createHorizontalGlue());
box.add(button);

JPanel p = new JPanel() {
  @Override public boolean isOptimizedDrawingEnabled() {
    return false;
  }
};
p.setLayout(new BorderLayout(0, 0) {
  @Override public void layoutContainer(Container parent) {
    synchronized (parent.getTreeLock()) {
      Insets insets = parent.getInsets();
      int width = parent.getWidth();
      int height = parent.getHeight();
      int top = insets.top;
      int bottom = height - insets.bottom;
      int left = insets.left;
      int right = width - insets.right;
      Component ec = getLayoutComponent(parent, BorderLayout.EAST);
      if (Objects.nonNull(ec)) {
        Dimension d = ec.getPreferredSize();
        JScrollBar vsb = scroll.getVerticalScrollBar();
        int vsw = vsb.isVisible() ? vsb.getSize().width : 0;
        ec.setBounds(right - d.width - vsw, top, d.width, bottom - top);
      }
      Component cc = getLayoutComponent(parent, BorderLayout.CENTER);
      if (Objects.nonNull(cc)) {
        cc.setBounds(left, top, right - left, bottom - top);
      }
    }
  }
});
p.add(minimap, BorderLayout.EAST);
p.add(scroll);

References

2018/07/31

Syntax highlighting the source code in the JEditorPane

Code

// NG: color:#RGB
// OK: color:#RRGGBB
private void loadFile(String path) {
  try (Stream<String> lines = Files.lines(
      Paths.get(path), StandardCharsets.UTF_8)) {
    String txt = lines.map(s -> s.replace("&", "&amp;")
                                 .replace("<", "&lt;")
                                 .replace(">", "&gt;"))
      .collect(Collectors.joining("\n"));
    editor.setText("<pre>" + prettify(engine, txt) + "\n</pre>");
  } catch (IOException ex) {
    ex.printStackTrace();
  }
}

private static String prettify(ScriptEngine engine, String src) {
  try {
    Object w = engine.get("window");
    return (String) ((Invocable) engine).invokeMethod(
        w, "prettyPrintOne", src);
  } catch (ScriptException | NoSuchMethodException ex) {
    ex.printStackTrace();
    return "";
  }
}

References

2010/09/13

Placeholder for an empty JTable

Code

JEditorPane hint = new JEditorPane("text/html", ">html<>a href='dummy'<No data!>/a<>/html<");

table.setFillsViewportHeight(true);
table.setLayout(new GridBagLayout());
table.add(hint);

model.addTableModelListener(new TableModelListener() {
  @Override public void tableChanged(TableModelEvent e) {
    DefaultTableModel model = (DefaultTableModel) e.getSource();
    hint.setVisible(model.getRowCount() == 0);
  }
});

References