Google Tag Manager

2025/06/30

Automatically open and close JPopupMenu without mouse click

Code

/* If the mouse cursor goes out of the region at high speed, JPopupMenu may not close, use AWTEventListener to work around this problem. */
class AutoClosePopupMenu extends JPopupMenu {
  private transient PopupMenuListener listener;

  @Override public void updateUI() {
    removePopupMenuListener(listener);
    super.updateUI();
    listener = new AwtPopupMenuListener();
    addPopupMenuListener(listener);
  }

  private void checkAutoClose(MouseEvent e) {
    Component c = e.getComponent();
    Rectangle r = getBounds();
    r.grow(0, 5);
    Point pt = SwingUtilities.convertPoint(c, e.getPoint(), this);
    if (!r.contains(pt) && !(c instanceof JButton)) {
      setVisible(false);
    }
  }

  private final class AwtPopupMenuListener implements PopupMenuListener {
    private final AWTEventListener handler = e -> {
      if (e instanceof MouseEvent) {
        int id = e.getID();
        if (id == MouseEvent.MOUSE_MOVED || id == MouseEvent.MOUSE_EXITED) {
          checkAutoClose((MouseEvent) e);
        }
      }
    };

    @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
      long mask = AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK;
      Toolkit.getDefaultToolkit().addAWTEventListener(handler, mask);
    }

    @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
      Toolkit.getDefaultToolkit().removeAWTEventListener(handler);
    }

    @Override public void popupMenuCanceled(PopupMenuEvent e) {
      /* not needed */
    }
  }
}
Set up an event listener to automatically open JPopupMenu when the mouse cursor enters the JButton area and automatically close it when the mouse cursor leaves the JButton or JPopupMenu area.

References

2025/05/31

Rounding the Title Background and Border Corners of TitledBorder

Code

/*
- RoundedTitledBorder
    - [TitledBorder(Border, String)](https://docs.oracle.com/javase/8/docs/api/javax/swing/border/TitledBorder.html#TitledBorder-javax.swing.border.Border-java.lang.String-) creates a `TitledBorder` that uses `RoundedBorder` for rounded corners.
    - Additionally, it overrides `TitledBorder#paintBorder(...)` to draw a rounded rectangle with rounded bottom-right corners as the background for the title text.
*/
class RoundedTitledBorder extends TitledBorder {
  private final JLabel label = new JLabel(" ");
  private final int arc;

  protected RoundedTitledBorder(String title, int arc) {
    super(new RoundedBorder(arc), title);
    this.arc = arc;
  }

  @Override public void paintBorder(
      Component c, Graphics g, int x, int y, int width, int height) {
    Graphics2D g2 = (Graphics2D) g.create();
    g2.setRenderingHint(
      RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    Border b = getBorder();
    if (b instanceof RoundedBorder) {
      Dimension d = getLabel(c).getPreferredSize();
      Insets i = b.getBorderInsets(c);
      int a2 = arc * 2;
      int w = d.width + i.left + i.right + a2;
      int h = d.height + i.top + i.bottom;
      g2.setClip(((RoundedBorder) b).getBorderShape(
        x + i.left, y + i.top, w, h));
      g2.setPaint(Color.GRAY);
      Shape titleBg = new RoundRectangle2D.Float(
        x - a2, y - a2, w + a2, h + a2, arc, arc);
      g2.fill(titleBg);
      g2.dispose();
    }
    g2.dispose();
    super.paintBorder(c, g, x, y, width, height);
  }

  private JLabel getLabel(Component c) {
    this.label.setText(getTitle());
    this.label.setFont(getFont(c));
    this.label.setComponentOrientation(c.getComponentOrientation());
    this.label.setEnabled(c.isEnabled());
    return this.label;
  }
}

class RoundedBorder extends EmptyBorder {
  private static final Paint ALPHA_ZERO = new Color(0x0, true);
  private final int arc;

  protected RoundedBorder(int arc) {
    super(2, 2, 2, 2);
    this.arc = arc;
  }

  @Override public void paintBorder(
      Component c, Graphics g, int x, int y, int width, int height) {
    Graphics2D g2 = (Graphics2D) g.create();
    g2.setRenderingHint(
      RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    Shape border = getBorderShape(x, y, width, height);
    g2.setPaint(ALPHA_ZERO);
    Area clear = new Area(new Rectangle2D.Double(x, y, width, height));
    clear.subtract(new Area(border));
    g2.fill(clear);
    g2.setPaint(Color.GRAY);
    g2.setStroke(new BasicStroke(1.5f));
    g2.draw(border);
    g2.dispose();
  }

  protected Shape getBorderShape(int x, int y, int w, int h) {
    return new RoundRectangle2D.Double(x, y, w - 1, h - 1, arc, arc);
  }
}


/*
- TitleLayerUI
- This implementation does not use `TitledBorder` itself, but instead configures a `RoundedBorder` for rounded corners and sets the title to be drawn using a `JLayer`.
    - It overrides `LayerUI#paint(...)` to draw the title text within the component.
    - Within `LayerUI#paint(...)`, it configures the title text to be shown or hidden based on whether the component has focus, and overrides `LayerUI#processFocusEvent` to redraw the entire component and update the border and title text whenever a focus change event occurs.
*/
class TitleLayerUI extends LayerUI<JScrollPane> {
  private final JLabel label;

  protected TitleLayerUI(String title, int arc) {
    super();
    label = new RoundedLabel(title, arc);
    label.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10));
    // label.setOpaque(false);
    label.setForeground(Color.WHITE);
    label.setBackground(Color.GRAY);
  }

  @Override public void paint(Graphics g, JComponent c) {
    super.paint(g, c);
    if (c instanceof JLayer) {
      JScrollPane sp = (JScrollPane) ((JLayer<?>) c).getView();
      Rectangle r = SwingUtilities.calculateInnerArea(sp, sp.getBounds());
      if (r != null && !sp.getViewport().getView().hasFocus()) {
        Dimension d = label.getPreferredSize();
        SwingUtilities.paintComponent(
          g, label, sp, r.x - 1, r.y - 1, d.width, d.height);
      }
    }
  }

  @Override public void updateUI(JLayer<? extends JScrollPane> l) {
    super.updateUI(l);
    SwingUtilities.updateComponentTreeUI(label);
  }

  @Override public void installUI(JComponent c) {
    super.installUI(c);
    if (c instanceof JLayer) {
      ((JLayer<?>) c).setLayerEventMask(AWTEvent.FOCUS_EVENT_MASK);
    }
  }

  @Override public void uninstallUI(JComponent c) {
    if (c instanceof JLayer) {
      ((JLayer<?>) c).setLayerEventMask(0);
    }
    super.uninstallUI(c);
  }

  @Override protected void processFocusEvent(
      FocusEvent e, JLayer<? extends JScrollPane> l) {
    super.processFocusEvent(e, l);
    l.getView().repaint();
  }

  private static class RoundedLabel extends JLabel {
    private final int arc;

    public RoundedLabel(String title, int arc) {
      super(title);
      this.arc = arc;
    }

    @Override protected void paintComponent(Graphics g) {
      if (!isOpaque()) {
        Dimension d = getPreferredSize();
        int w = d.width - 1;
        int h = d.height - 1;
        int h2 = h / 2;
        Graphics2D g2 = (Graphics2D) g.create();
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setPaint(getBackground());
        g2.fillRect(0, 0, w, h2);
        g2.fillRoundRect(-arc, 0, w + arc, h, arc, arc);
        g2.dispose();
      }
      super.paintComponent(g);
    }
  }
}

References

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

2025/02/28

Display the insert cursor for a new column on the boundary of a JTable column

Code

class ColumnInsertLayerUI extends LayerUI<JScrollPane> {
  private static final Color LINE_COLOR = new Color(0x00_78_D7);
  private static final int LINE_WIDTH = 4;
  private final Rectangle2D line = new Rectangle2D.Double();
  private final Ellipse2D plus = new Ellipse2D.Double(0d, 0d, 10d, 10d);

  @Override public void paint(Graphics g, JComponent c) {
    super.paint(g, c);
    if (c instanceof JLayer && !line.isEmpty()) {
      JScrollPane scroll = (JScrollPane) ((JLayer<?>) c).getView();
      JTableHeader header =
          ((JTable) scroll.getViewport().getView()).getTableHeader();
      Graphics2D g2 = (Graphics2D) g.create();
      g2.setRenderingHint(
          RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      Point pt0 = line.getBounds().getLocation();
      Point pt1 = SwingUtilities.convertPoint(header, pt0, c);
      g2.translate(pt1.getX() - pt0.getX(), pt1.getY() - pt0.getY());
      // paint Insert Line
      g2.setPaint(LINE_COLOR);
      g2.fill(line);
      // paint Plus Icon
      g2.setPaint(Color.WHITE);
      g2.fill(plus);
      g2.setPaint(LINE_COLOR);
      double cx = plus.getCenterX();
      double cy = plus.getCenterY();
      double w2 = plus.getWidth() / 2d;
      double h2 = plus.getHeight() / 2d;
      g2.draw(new Line2D.Double(cx - w2, cy, cx + w2, cy));
      g2.draw(new Line2D.Double(cx, cy - h2, cx, cy + h2));
      g2.draw(plus);
      g2.dispose();
    }
  }

  private void updateLineLocation(JScrollPane scroll, Point loc) {
    JTable table = (JTable) scroll.getViewport().getView();
    JTableHeader header = table.getTableHeader();
    Rectangle rect = scroll.getVisibleRect();
    JScrollBar bar = scroll.getHorizontalScrollBar();
    int scrollHeight = bar.isVisible() ? bar.getHeight() : 0;
    Dimension d = new Dimension(
        LINE_WIDTH, rect.height - scrollHeight);
    for (int i = 0; i < table.getColumnCount(); i++) {
      if (canInsert(header, loc, i, d)) {
        return;
      }
    }
  }

  private boolean canInsert(
        JTableHeader header, Point loc, int i, Dimension d) {
    Rectangle r = header.getHeaderRect(i);
    Rectangle r1 = getWestRect(r, i);
    Rectangle r2 = getEastRect(r);
    boolean hit = false;
    if (r1.contains(loc)) {
      updateInsertLineLocation(r1, loc, d, header);
      hit = true;
    } else if (r2.contains(loc)) {
      updateInsertLineLocation(r2, loc, d, header);
      hit = true;
    } else if (r.contains(loc)) {
      line.setFrame(0d, 0d, 0d, 0d);
      header.setCursor(Cursor.getDefaultCursor());
      hit = true;
    }
    return hit;
  }

  private Rectangle getWestRect(Rectangle r, int i) {
    Rectangle rect = r.getBounds();
    Rectangle bounds = plus.getBounds();
    if (i != 0) {
      rect.x -= bounds.width / 2;
    }
    rect.setSize(bounds.getSize());
    return rect;
  }

  private Rectangle getEastRect(Rectangle r) {
    Rectangle rect = r.getBounds();
    Rectangle bounds = plus.getBounds();
    rect.x += rect.width - bounds.width / 2;
    rect.setSize(bounds.getSize());
    return rect;
  }

  private void updateInsertLineLocation(
        Rectangle r, Point loc, Dimension d, Component c) {
    if (r.contains(loc)) {
      double cx = r.getCenterX();
      double cy = r.getCenterY();
      line.setFrame(
          cx - d.getWidth() / 2d, r.getY(), d.width, d.height);
      double pw = plus.getWidth() / 2d;
      double ph = plus.getHeight() / 2d;
      plus.setFrameFromCenter(cx, cy, cx - pw, cy - ph);
      c.setCursor(Cursor.getDefaultCursor());
    } else {
      line.setFrame(0d, 0d, 0d, 0d);
      c.setCursor(Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR));
    }
  }

  @Override public void installUI(JComponent c) {
    super.installUI(c);
    if (c instanceof JLayer) {
      ((JLayer<?>) c).setLayerEventMask(
          AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
    }
  }

  @Override public void uninstallUI(JComponent c) {
    if (c instanceof JLayer) {
      ((JLayer<?>) c).setLayerEventMask(0);
    }
    super.uninstallUI(c);
  }

  @Override protected void processMouseEvent(
        MouseEvent e, JLayer<? extends JScrollPane> l) {
    super.processMouseEvent(e, l);
    if (e.getID() == MouseEvent.MOUSE_CLICKED) {
      JScrollPane scroll = l.getView();
      Point pt = e.getPoint();
      if (plus.contains(pt) && !line.isEmpty()) {
        JTable table = (JTable) scroll.getViewport().getView();
        TableModel model = table.getModel();
        int columnCount = table.getColumnCount();
        int maxColumn = model.getColumnCount();
        if (columnCount < maxColumn) {
          int idx = table.columnAtPoint(
              line.getBounds().getLocation());
          TableColumn column = new TableColumn(columnCount);
          column.setHeaderValue("Column" + columnCount);
          table.addColumn(column);
          table.moveColumn(columnCount, idx + 1);
          updateLineLocation(scroll, pt);
        }
      }
      l.repaint(scroll.getBounds());
    }
  }

  @Override protected void processMouseMotionEvent(
        MouseEvent e, JLayer<? extends JScrollPane> l) {
    super.processMouseMotionEvent(e, l);
    Component c = e.getComponent();
    int id = e.getID();
    JScrollPane scroll = l.getView();
    if (id == MouseEvent.MOUSE_MOVED && c instanceof JTableHeader) {
      updateLineLocation(scroll, e.getPoint());
    } else {
      line.setFrame(0d, 0d, 0d, 0d);
    }
    l.repaint(scroll.getBounds());
  }
}

References

2025/01/31

Limit the area where TableColumns can be reordered by dragging

Limit the area where JTableHeader column reordering drags can be initiated to the top half of the TableColumn and perform mouse cursor changes and draw drag handle icons on the JLayer.

Code

class ColumnDragLayerUI extends LayerUI<JScrollPane> {
  private final Rectangle draggableRect = new Rectangle();

  @Override public void installUI(JComponent c) {
    super.installUI(c);
    if (c instanceof JLayer) {
      ((JLayer<?>) c).setLayerEventMask(
          AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
    }
  }

  @Override public void uninstallUI(JComponent c) {
    if (c instanceof JLayer) {
      ((JLayer<?>) c).setLayerEventMask(0);
    }
    super.uninstallUI(c);
  }

  @Override public void paint(Graphics g, JComponent c) {
    super.paint(g, c);
    if (!draggableRect.isEmpty()) {
      Graphics2D g2 = (Graphics2D) g.create();
      // g2.fill(draggableRect);
      Icon icon = new DragAreaIcon();
      int x = (int) (draggableRect.getCenterX() - icon.getIconWidth() / 2d);
      int y = draggableRect.y + 1;
      icon.paintIcon(c, g2, x, y);
      g2.dispose();
    }
  }

  @Override protected void processMouseEvent(
      MouseEvent e, JLayer<? extends JScrollPane> l) {
    super.processMouseEvent(e, l);
    Component c = e.getComponent();
    if (c instanceof JTableHeader) {
      JTableHeader header = (JTableHeader) c;
      if (e.getID() == MouseEvent.MOUSE_PRESSED) {
        Point pt = e.getPoint();
        updateIconAndCursor(header, pt, l);
      } else if (e.getID() == MouseEvent.MOUSE_RELEASED) {
        header.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
        draggableRect.setSize(0, 0);
      }
    }
  }

  @Override protected void processMouseMotionEvent(
      MouseEvent e, JLayer<? extends JScrollPane> l) {
    Component c = e.getComponent();
    if (c instanceof JTableHeader) {
      JTableHeader header = (JTableHeader) c;
      if (e.getID() == MouseEvent.MOUSE_DRAGGED) {
        TableColumn draggedColumn = header.getDraggedColumn();
        if (!draggableRect.isEmpty() && draggedColumn != null) {
          EventQueue.invokeLater(() -> {
            int modelIndex = draggedColumn.getModelIndex();
            int viewIndex = header.getTable().convertColumnIndexToView(modelIndex);
            Rectangle rect = header.getHeaderRect(viewIndex);
            rect.x += header.getDraggedDistance();
            draggableRect.setRect(SwingUtilities.convertRectangle(header, rect, l));
            header.repaint(rect);
          });
        } else {
          e.consume(); // Refuse to start drag
        }
      } else if (e.getID() == MouseEvent.MOUSE_MOVED) {
        Point pt = e.getPoint();
        updateIconAndCursor(header, pt, l);
        header.repaint();
      }
    } else {
      c.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
      draggableRect.setSize(0, 0);
    }
  }

  private void updateIconAndCursor(JTableHeader header, Point pt, JLayer<?> l) {
    Rectangle r = header.getHeaderRect(header.columnAtPoint(pt));
    r.height /= 2;
    if (r.contains(pt)) {
      header.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
      draggableRect.setRect(SwingUtilities.convertRectangle(header, r, l));
    } else {
      header.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
      draggableRect.setSize(0, 0);
    }
  }
}

References

2024/12/31

Create a multi-line JToolTip using JTextArea's automatic line wrapping

Code

class LineWrapToolTip extends JToolTip {
  private static final double JAVA17 = 17.0;
  private static final JLabel MEASURER = new JLabel(" ");
  private static final int TIP_WIDTH = 200;
  private final JTextArea textArea = new JTextArea(0, 20);

  protected LineWrapToolTip() {
    super();
    textArea.setLineWrap(true);
    textArea.setWrapStyleWord(true);
    textArea.setOpaque(true);
    // textArea.setColumns(20);
    LookAndFeel.installColorsAndFont(
        textArea, "ToolTip.background", "ToolTip.foreground", "ToolTip.font");
    setLayout(new BorderLayout());
    add(textArea);
  }

  @Override public final void setLayout(LayoutManager mgr) {
    super.setLayout(mgr);
  }

  @Override public final Component add(Component comp) {
    return super.add(comp);
  }

  @Override public Dimension getPreferredSize() {
    Dimension d = getLayout().preferredLayoutSize(this);
    Dimension dim;
    String version = System.getProperty("java.specification.version");
    if (Double.parseDouble(version) >= JAVA17) {
      dim = getTextAreaSize17(d);
    } else {
      dim = getTextAreaSize8(d);
    }
    return dim;
  }

  private Dimension getTextAreaSize8(Dimension d) {
    Font font = textArea.getFont();
    MEASURER.setFont(font);
    MEASURER.setText(textArea.getText());
    Insets i = getInsets();
    int pad = getTextAreaPaddingWidth(i);
    // d.width = Math.min(d.width, MEASURER.getPreferredSize().width + pad);
    d.width = Math.min(TIP_WIDTH, MEASURER.getPreferredSize().width + pad);

    // JDK-8226513 JEditorPane is shown with incorrect size - Java Bug System
    // https://bugs.openjdk.org/browse/JDK-8226513
    AttributedString as = new AttributedString(textArea.getText());
    as.addAttribute(TextAttribute.FONT, font);
    AttributedCharacterIterator aci = as.getIterator();
    FontMetrics fm = textArea.getFontMetrics(font);
    FontRenderContext frc = fm.getFontRenderContext();
    LineBreakMeasurer lbm = new LineBreakMeasurer(aci, frc);
    float y = 0f;
    while (lbm.getPosition() < aci.getEndIndex()) {
      TextLayout tl = lbm.nextLayout(TIP_WIDTH);
      y += tl.getDescent() + tl.getLeading() + tl.getAscent();
    }
    d.height = (int) y + getTextAreaPaddingHeight(i);
    return d;
  }

  private Dimension getTextAreaSize17(Dimension d) {
    MEASURER.setFont(textArea.getFont());
    MEASURER.setText(textArea.getText());
    int pad = getTextAreaPaddingWidth(getInsets());
    d.width = Math.min(d.width, MEASURER.getPreferredSize().width + pad);
    return d;
  }

  private int getTextAreaPaddingWidth(Insets i) {
    // @see BasicTextUI.java
    // margin required to show caret in the rightmost position
    int caretMargin = -1;
    Object property = UIManager.get("Caret.width");
    if (property instanceof Number) {
      caretMargin = ((Number) property).intValue();
    }
    property = textArea.getClientProperty("caretWidth");
    if (property instanceof Number) {
      caretMargin = ((Number) property).intValue();
    }
    if (caretMargin < 0) {
      caretMargin = 1;
    }
    Insets ti = textArea.getInsets();
    return i.left + i.right + ti.left + ti.right + caretMargin;
    // Insets tm = textArea.getMargin();
    // return i.left + i.right + ti.left + ti.right + tm.left + tm.right;
  }

  private int getTextAreaPaddingHeight(Insets i) {
    Insets ti = textArea.getInsets();
    return i.top + i.bottom + ti.top + ti.bottom;
  }

  @Override public void setTipText(String tipText) {
    String oldValue = textArea.getText();
    textArea.setText(tipText);
    firePropertyChange("tiptext", oldValue, tipText);
    if (!Objects.equals(oldValue, tipText)) {
      revalidate();
      repaint();
    }
  }

  @Override public String getTipText() {
    return Optional.ofNullable(textArea).map(JTextArea::getText).orElse(null);
  }
}

References

2024/12/01

Implement sticky header in JList

Code

class StickyLayerUI extends LayerUI<JScrollPane> {
  private final JPanel renderer = new JPanel();
  private int currentHeaderIdx = -1;
  private int nextHeaderIdx = -1;

  @Override public void installUI(JComponent c) {
    super.installUI(c);
    if (c instanceof JLayer) {
      ((JLayer<?>) c).setLayerEventMask(
          AWTEvent.MOUSE_WHEEL_EVENT_MASK
          | AWTEvent.MOUSE_MOTION_EVENT_MASK);
    }
  }

  @Override public void uninstallUI(JComponent c) {
    if (c instanceof JLayer) {
      ((JLayer<?>) c).setLayerEventMask(0);
    }
    super.uninstallUI(c);
  }

  @Override protected void processMouseMotionEvent(
      MouseEvent e, JLayer<? extends JScrollPane> l) {
    super.processMouseMotionEvent(e, l);
    Component c = l.getView().getViewport().getView();
    if (e.getID() == MouseEvent.MOUSE_DRAGGED && c instanceof JList) {
      update((JList<?>) c);
    }
  }

  @Override protected void processMouseWheelEvent(
      MouseWheelEvent e, JLayer<? extends JScrollPane> l) {
    super.processMouseWheelEvent(e, l);
    Component c = l.getView().getViewport().getView();
    if (c instanceof JList) {
      update((JList<?>) c);
    }
  }

  private void update(JList<?> list) {
    int idx = list.getFirstVisibleIndex();
    if (idx >= 0) {
      currentHeaderIdx = getHeaderIndex1(list, idx);
      nextHeaderIdx = getNextHeaderIndex1(list, idx);
    } else {
      currentHeaderIdx = -1;
      nextHeaderIdx = -1;
    }
  }

  @Override public void paint(Graphics g, JComponent c) {
    super.paint(g, c);
    JList<?> list = getList(c);
    if (list != null && currentHeaderIdx >= 0) {
      JScrollPane scroll = (JScrollPane) ((JLayer<?>) c).getView();
      Rectangle headerRect = scroll.getViewport().getBounds();
      headerRect.height = list.getFixedCellHeight();
        Graphics2D g2 = (Graphics2D) g.create();
      int firstVisibleIdx = list.getFirstVisibleIndex();
      if (firstVisibleIdx + 1 == nextHeaderIdx) {
        Dimension d = headerRect.getSize();
        Component c1 = getComponent(list, currentHeaderIdx);
        Rectangle r1 = getHeaderRect(list, firstVisibleIdx, c, d);
        SwingUtilities.paintComponent(g2, c1, renderer, r1);
        Component c2 = getComponent(list, nextHeaderIdx);
        Rectangle r2 = getHeaderRect(list, nextHeaderIdx, c, d);
        SwingUtilities.paintComponent(g2, c2, renderer, r2);
        } else {
        Component c1 = getComponent(list, currentHeaderIdx);
        SwingUtilities.paintComponent(g2, c1, renderer, headerRect);
        }
        g2.dispose();
      }
    }

  private static JList<?> getList(JComponent layer) {
    JList<?> list = null;
    if (layer instanceof JLayer) {
      JScrollPane scroll = (JScrollPane) ((JLayer<?>) layer).getView();
      Component view = scroll.getViewport().getView();
      if (view instanceof JList) {
        list = (JList<?>) view;
      }
    }
    return list;
  }

  private static int getHeaderIndex1(JList<?> list, int start) {
    return list.getNextMatch("0", start, Position.Bias.Backward);
  }

  private static int getNextHeaderIndex1(JList<?> list, int start) {
    return list.getNextMatch("0", start, Position.Bias.Forward);
  }

  private static Rectangle getHeaderRect(
      JList<?> list, int i, Component dst, Dimension d) {
    Rectangle r = SwingUtilities.convertRectangle(
        list, list.getCellBounds(i, i), dst);
    r.setSize(d);
    return r;
  }

  private static <E> Component getComponent(JList<E> list, int idx) {
    E value = list.getModel().getElementAt(idx);
    ListCellRenderer<? super E> r = list.getCellRenderer();
    Component c = r.getListCellRendererComponent(
        list, value, idx, false, false);
    c.setBackground(Color.GRAY);
    c.setForeground(Color.WHITE);
    return c;
  }
}

References

2024/10/31

Move and rotate strings to place them along the Shape curve

Code

public static Shape createTextOnPath(Shape shape, GlyphVector gv) {
  double[] points = new double[6];
  Point2D prevPt = new Point2D.Double();
  double nextAdvance = 0d;
  double next = 0d;
  Path2D result = new Path2D.Double();
  int length = gv.getNumGlyphs();
  int idx = 0;
  PathIterator pi = new FlatteningPathIterator(
      shape.getPathIterator(null), 1d);
  while (idx < length && !pi.isDone()) {
    switch (pi.currentSegment(points)) {
      case PathIterator.SEG_MOVETO:
        result.moveTo(points[0], points[1]);
        prevPt.setLocation(points[0], points[1]);
        nextAdvance = gv.getGlyphMetrics(idx).getAdvance() * .5;
        next = nextAdvance;
        break;

      case PathIterator.SEG_LINETO:
        double dx = points[0] - prevPt.getX();
        double dy = points[1] - prevPt.getY();
        double distance = Math.hypot(dx, dy);
        if (distance >= next) {
          double r = 1d / distance;
          double angle = Math.atan2(dy, dx);
          while (idx < length && distance >= next) {
            double x = prevPt.getX() + next * dx * r;
            double y = prevPt.getY() + next * dy * r;
            double advance = nextAdvance;
            nextAdvance = getNextAdvance(gv, idx, length);
            AffineTransform at = AffineTransform.getTranslateInstance(x, y);
            at.rotate(angle);
            Point2D pt = gv.getGlyphPosition(idx);
            at.translate(-pt.getX() - advance, -pt.getY());
            Shape s = gv.getGlyphOutline(idx);
            result.append(at.createTransformedShape(s), false);
            next += advance + nextAdvance;
            idx++;
          }
        }
        next -= distance;
        prevPt.setLocation(points[0], points[1]);
        break;

      default:
    }
    pi.next();
  }
  return result;
}

References

2024/09/30

Set the maximum number of items that can be selected in a group in JCheckBox

Code

class GroupCheckBox extends JCheckBox {
  protected GroupCheckBox(String title) {
    super(title);
  }

  @Override public void updateUI() {
    super.updateUI();
    setModel(new ToggleButtonModel() {
      private static final int GROUP_SIZE = 3;

      @Override public void setSelected(boolean selected) {
        if (selected) {
          if (getSelectedObjects().length == GROUP_SIZE) {
            UIManager.getLookAndFeel()
                .provideErrorFeedback(GroupCheckBox.this);
          } else {
            super.setSelected(true);
          }
        } else {
          super.setSelected(false);
        }
      }

      @Override public Object[] getSelectedObjects() {
        Container parent = getParent();
        return Arrays.stream(parent.getComponents())
            .filter(AbstractButton.class::isInstance)
            .map(AbstractButton.class::cast)
            .filter(AbstractButton::isSelected)
            .toArray();
      }
    });
  }
}

References

2024/08/31

Set JButton as cell renderer for JTableHeader

Code

class ButtonHeaderRenderer extends JButton implements TableCellRenderer {
  private int pushedColumn = -1;
  private int rolloverColumn = -1;

  @Override public void updateUI() {
    super.updateUI();
    setHorizontalTextPosition(LEFT);
  }

  @Override public Component getTableCellRendererComponent(
      JTable table, Object value,
      boolean isSelected, boolean hasFocus,
      int row, int column) {
    setText(Objects.toString(value, ""));
    int modelColumn = table.convertColumnIndexToModel(column);
    JTableHeader header = table.getTableHeader();
    if (header != null) {
      // setColor(header, hasFocus);
      boolean isPressed = modelColumn == pressedColumn;
      getModel().setPressed(isPressed);
      getModel().setArmed(isPressed);
      getModel().setRollover(modelColumn == rolloverColumn);
      setFont(header.getFont());
    }

    Icon sortIcon = null;
    if (table.getRowSorter() != null) {
      List<? extends RowSorter.SortKey> sortKeys =
          table.getRowSorter().getSortKeys();
      if (!sortKeys.isEmpty() &&
          sortKeys.get(0).getColumn() == modelColumn) {
        SortOrder sortOrder = sortKeys.get(0).getSortOrder();
        switch (sortOrder) {
          case ASCENDING:
            sortIcon = UIManager.getIcon("Table.ascendingSortIcon");
            break;
          case DESCENDING:
            sortIcon = UIManager.getIcon("Table.descendingSortIcon");
            break;
          // case UNSORTED:
          //   sortIcon = UIManager.getIcon("Table.naturalSortIcon");
          //   break;
          default:
            sortIcon = UIManager.getIcon("Table.naturalSortIcon");
        }
      }
    }
    setIcon(sortIcon);
    return this;
  }

  public void setPressedColumn(int column) {
    pushedColumn = column;
  }

  public void setRolloverColumn(int column) {
    rolloverColumn = column;
  }
}

class HeaderMouseListener extends MouseAdapter {
  @Override public void mousePressed(MouseEvent e) {
    JTableHeader header = (JTableHeader) e.getComponent();
    JTable table = header.getTable();
    TableCellRenderer renderer = header.getDefaultRenderer();
    int viewColumn = table.columnAtPoint(e.getPoint());
    if (viewColumn >= 0 && renderer instanceof ButtonHeaderRenderer) {
      int column = table.convertColumnIndexToModel(viewColumn);
      ((ButtonHeaderRenderer) renderer).setPressedColumn(column);
    }
  }

  @Override public void mouseReleased(MouseEvent e) {
    JTableHeader header = (JTableHeader) e.getComponent();
    TableCellRenderer renderer = header.getDefaultRenderer();
    if (renderer instanceof ButtonHeaderRenderer) {
      ((ButtonHeaderRenderer) renderer).setPressedColumn(-1);
    }
  }

  @Override public void mouseMoved(MouseEvent e) {
    JTableHeader header = (JTableHeader) e.getComponent();
    JTable table = header.getTable();
    TableCellRenderer renderer = header.getDefaultRenderer();
    int viewColumn = table.columnAtPoint(e.getPoint());
    if (viewColumn >= 0 && renderer instanceof ButtonHeaderRenderer) {
      int column = table.convertColumnIndexToModel(viewColumn);
      ((ButtonHeaderRenderer) renderer).setRolloverColumn(column);
    }
  }

  @Override public void mouseExited(MouseEvent e) {
    JTableHeader header = (JTableHeader) e.getComponent();
    TableCellRenderer renderer = header.getDefaultRenderer();
    if (renderer instanceof ButtonHeaderRenderer) {
      ((ButtonHeaderRenderer) renderer).setRolloverColumn(-1);
    }
  }
}

References

2024/07/31

Animates the effect of expanding and collapsing JTree nodes

Code

JTree tree = new JTree() {
  @Override public void updateUI() {
    super.updateUI();
    setRowHeight(-1);
    setCellRenderer(new HeightTreeCellRenderer());
  }
};
tree.addTreeWillExpandListener(new TreeWillExpandListener() {
  @Override public void treeWillExpand(TreeExpansionEvent e) {
    Object o = e.getPath().getLastPathComponent();
    if (o instanceof DefaultMutableTreeNode) {
      DefaultMutableTreeNode parent = (DefaultMutableTreeNode) o;
      List<DefaultMutableTreeNode> list = getTreeNodes(parent);
      parent.setUserObject(makeUserObject(parent, END_HEIGHT));
      list.forEach(n -> n.setUserObject(makeUserObject(n, START_HEIGHT)));
      startExpandTimer(e, list);
    }
  }

  @Override public void treeWillCollapse(TreeExpansionEvent e)
      throws ExpandVetoException {
    Object c = e.getPath().getLastPathComponent();
    if (c instanceof DefaultMutableTreeNode) {
      DefaultMutableTreeNode p = (DefaultMutableTreeNode) o;
      List<DefaultMutableTreeNode> list = getTreeNodes(p);
      boolean b = list
          .stream()
          .anyMatch(n -> {
            Object obj = n.getUserObject();
            return obj instanceof SizeNode
              && ((SizeNode) obj).height == END_HEIGHT;
          });
      if (b) {
        startCollapseTimer(e, list);
        throw new ExpandVetoException(e);
      }
    }
  }
});

private static void startExpandTimer(
    TreeExpansionEvent e, List<DefaultMutableTreeNode> list) {
  JTree tree = (JTree) e.getSource();
  TreeModel model = tree.getModel();
  AtomicInteger height = new AtomicInteger(START_HEIGHT);
  new Timer(DELAY, ev -> {
    int h = height.getAndIncrement();
    if (h <= END_HEIGHT) {
      list.forEach(n -> {
        Object uo = makeUserObject(n, h);
        model.valueForPathChanged(new TreePath(n.getPath()), uo);
      });
    } else {
      ((Timer) ev.getSource()).stop();
    }
  }).start();
}

private static void startCollapseTimer(
    TreeExpansionEvent e, List<DefaultMutableTreeNode> list) {
  JTree tree = (JTree) e.getSource();
  TreePath path = e.getPath();
  TreeModel model = tree.getModel();
  AtomicInteger height = new AtomicInteger(END_HEIGHT);
  new Timer(DELAY, ev -> {
    int h = height.getAndDecrement();
    if (h >= START_HEIGHT) {
      list.forEach(n -> {
        Object uo = makeUserObject(n, h);
        model.valueForPathChanged(new TreePath(n.getPath()), uo);
      });
    } else {
      ((Timer) ev.getSource()).stop();
      tree.collapsePath(path);
    }
  }).start();
}

// ...
class HeightTreeCellRenderer extends DefaultTreeCellRenderer {
  @Override public Component getTreeCellRendererComponent(
      JTree tree,
      Object value,
      boolean selected,
      boolean expanded,
      boolean leaf,
      int row,
      boolean hasFocus) {
    Component c = super.getTreeCellRendererComponent(
        tree, value, selected, expanded, leaf, row, hasFocus);
    DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
    Object uo = node.getUserObject();
    if (c instanceof JLabel && uo instanceof SizeNode) {
      JLabel l = (JLabel) c;
      SizeNode n = (SizeNode) uo;
      l.setPreferredSize(null); // reset prev preferred size
      l.setText(n.label); // recalculate preferred size
      Dimension d = l.getPreferredSize();
      d.height = n.height;
      l.setPreferredSize(d);
    }
    return c;
  }
}

class SizeNode {
  public final String label;
  public final int height;

  protected SizeNode(String label, int height) {
    this.label = label;
    this.height = height;
  }

  @Override public String toString() {
    return label;
  }
}

References

2024/06/30

Drag and drop to rearrange nodes in the JTree

Code

class TreeTransferHandler extends TransferHandler {
  private final DataFlavor nodesFlavor = new DataFlavor(
      List.class, "List of TreeNode");

  @Override public int getSourceActions(JComponent c) {
    return c instanceof JTree
        && TreeUtils.canStartDrag((JTree) c) ? COPY_OR_MOVE : NONE;
  }

  @Override protected Transferable createTransferable(JComponent c) {
    Transferable transferable = null;
    if (c instanceof JTree && ((JTree) c).getSelectionPaths() != null) {
      List<MutableTreeNode> copies = new ArrayList<>();
      Arrays.stream(((JTree) c).getSelectionPaths()).forEach(path -> {
        DefaultMutableTreeNode node =
            (DefaultMutableTreeNode) path.getLastPathComponent();
        DefaultMutableTreeNode clone =
            new DefaultMutableTreeNode(node.getUserObject());
        copies.add(TreeUtils.deepCopy(node, clone));
      });
      transferable = new Transferable() {
        @Override public DataFlavor[] getTransferDataFlavors() {
          return new DataFlavor[] {nodesFlavor};
        }

        @Override public boolean isDataFlavorSupported(
            DataFlavor flavor) {
          return Objects.equals(nodesFlavor, flavor);
        }

        @Override public Object getTransferData(DataFlavor flavor)
            throws UnsupportedFlavorException {
          if (isDataFlavorSupported(flavor)) {
            return copies;
          } else {
            throw new UnsupportedFlavorException(flavor);
          }
        }
      };
    }
    return transferable;
  }

  @Override public boolean canImport(TransferSupport support) {
    DropLocation dl = support.getDropLocation();
    Component c = support.getComponent();
    return support.isDrop()
        && support.isDataFlavorSupported(nodesFlavor)
        && c instanceof JTree
        && dl instanceof JTree.DropLocation
        && TreeUtils.canImportDropLocation(
            (JTree) c, (JTree.DropLocation) dl);
  }

  @Override public boolean importData(TransferSupport support) {
    Component c = support.getComponent();
    DropLocation dl = support.getDropLocation();
    Transferable transferable = support.getTransferable();
    return canImport(support)
        && c instanceof JTree
        && dl instanceof JTree.DropLocation
        && insertNode((JTree) c, (JTree.DropLocation) dl, transferable);
  }

  private boolean insertNode(
      JTree tree, JTree.DropLocation dl, Transferable transferable) {
    TreePath path = dl.getPath();
    Object p = path.getLastPathComponent();
    TreeModel m = tree.getModel();
    List<?> nodes = getTransferData(transferable);
    if (p instanceof MutableTreeNode && m instanceof DefaultTreeModel) {
      MutableTreeNode parent = (MutableTreeNode) p;
      DefaultTreeModel model = (DefaultTreeModel) m;
      int childIndex = dl.getChildIndex();
      AtomicInteger index = new AtomicInteger(
          getDropIndex(parent, childIndex));
      nodes.stream()
          .filter(MutableTreeNode.class::isInstance)
          .map(MutableTreeNode.class::cast)
          .forEach(n -> model.insertNodeInto(
               n, parent, index.getAndIncrement()));
    }
    return !nodes.isEmpty();
  }

  private static int getDropIndex(
      MutableTreeNode parent, int childIndex) {
    // Configure for drop mode.
    int index = childIndex; // DropMode.INSERT
    if (childIndex == -1) { // DropMode.ON
      index = parent.getChildCount();
    }
    return index;
  }

  private List<?> getTransferData(Transferable t) {
    List<?> nodes;
    try {
      nodes = (List<?>) t.getTransferData(nodesFlavor);
    } catch (UnsupportedFlavorException | IOException ex) {
      nodes = Collections.emptyList();
    }
    return nodes;
  }

  @Override protected void exportDone(
      JComponent src, Transferable data, int action) {
    if (src instanceof JTree && (action & MOVE) == MOVE) {
      cleanup((JTree) src);
    }
  }

  private void cleanup(JTree tree) {
    DefaultTreeModel model = (DefaultTreeModel) tree.getModel();
    TreePath[] selectionPaths = tree.getSelectionPaths();
    if (selectionPaths != null) {
      for (TreePath path : selectionPaths) {
        model.removeNodeFromParent(
            (MutableTreeNode) path.getLastPathComponent());
      }
    }
  }
}

References

2024/05/31

Rounding the corners of a rectilinear polygon generated by node selection in a JTree

Code

public static List<Point2D> flatteningStepsOnRightSide(
    List<Point2D> list, double arc) {
  int sz = list.size();
  for (int i = 0; i < sz; i++) {
    int i1 = (i + 1) % sz;
    int i2 = (i + 2) % sz;
    int i3 = (i + 3) % sz;
    Point2D pt0 = list.get(i);
    Point2D pt1 = list.get(i1);
    Point2D pt2 = list.get(i2);
    Point2D pt3 = list.get(i3);
    double dx1 = pt2.getX() - pt1.getX();
    if (Math.abs(dx1) > 1.0e-1 && Math.abs(dx1) < arc) {
      double max = Math.max(pt0.getX(), pt2.getX());
      replace(list, i, max, pt0.getY());
      replace(list, i1, max, pt1.getY());
      replace(list, i2, max, pt2.getY());
      replace(list, i3, max, pt3.getY());
    }
  }
  return list;
}

private static void replace(List<Point2D> list, int i, double x, double y) {
  list.remove(i);
  list.add(i, new Point2D.Double(x, y));
}

/**
 * Rounding the corners of a Rectilinear Polygon.
 */
public static Path2D convertRoundedPath(List<Point2D> list, double arc) {
  double kappa = 4d * (Math.sqrt(2d) - 1d) / 3d; // = 0.55228...;
  double akv = arc - arc * kappa;
  int sz = list.size();
  Point2D pt0 = list.get(0);
  Path2D path = new Path2D.Double();
  path.moveTo(pt0.getX() + arc, pt0.getY());
  for (int i = 0; i < sz; i++) {
    Point2D prv = list.get((i - 1 + sz) % sz);
    Point2D cur = list.get(i);
    Point2D nxt = list.get((i + 1) % sz);
    double dx0 = signum(cur.getX() - prv.getX(), arc);
    double dy0 = signum(cur.getY() - prv.getY(), arc);
    double dx1 = signum(nxt.getX() - cur.getX(), arc);
    double dy1 = signum(nxt.getY() - cur.getY(), arc);
    path.curveTo(
        cur.getX() - dx0 * akv, cur.getY() - dy0 * akv,
        cur.getX() + dx1 * akv, cur.getY() + dy1 * akv,
        cur.getX() + dx1 * arc, cur.getY() + dy1 * arc);
    path.lineTo(nxt.getX() - dx1 * arc, nxt.getY() - dy1 * arc);
  }
  path.closePath();
  return path;
}

private static double signum(double v, double arc) {
  return Math.abs(v) < arc ? 0d : Math.signum(v);
}

References

2024/04/30

Rounding the corners of a rectilinear polygon generated by a selection of calendar made with JList

Code

public static Path2D convertRoundedPath(List<Point2D> list, double arc) {
  double kappa = 4d * (Math.sqrt(2d) - 1d) / 3d; // = 0.55228...;
  double akv = arc - arc * kappa;
  int sz = list.size();
  Point2D pt0 = list.get(0);
  Path2D path = new Path2D.Double();
  path.moveTo(pt0.getX() + arc, pt0.getY());
  for (int i = 0; i < sz; i++) {
    Point2D prv = list.get((i - 1 + sz) % sz);
    Point2D cur = list.get(i);
    Point2D nxt = list.get((i + 1) % sz);
    double dx0 = Math.signum(cur.getX() - prv.getX());
    double dy0 = Math.signum(cur.getY() - prv.getY());
    double dx1 = Math.signum(nxt.getX() - cur.getX());
    double dy1 = Math.signum(nxt.getY() - cur.getY());
    path.curveTo(
        cur.getX() - dx0 * akv, cur.getY() - dy0 * akv,
        cur.getX() + dx1 * akv, cur.getY() + dy1 * akv,
        cur.getX() + dx1 * arc, cur.getY() + dy1 * arc);
    path.lineTo(nxt.getX() - dx1 * arc, nxt.getY() - dy1 * arc);
  }
  path.closePath();
  return path;
}

References

2024/03/31

Switching between JToolBar and JMenuBar

Click on the hamburger menu-like JButton placed on the JToolBar to switch this with the JMenuBar

Code

JMenuBar mainMenuBar = makeMenuBar();
JButton button = makeHamburgerMenuButton(mainMenuBar);
JMenuBar wrappingMenuBar = new JMenuBar();
wrappingMenuBar.add(makeToolBar(button));
EventQueue.invokeLater(() -> getRootPane().setJMenuBar(wrappingMenuBar));

PopupMenuListener handler = new PopupMenuListener() {
  @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
    // not need
  }

  @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
    EventQueue.invokeLater(() -> {
      if (MenuSelectionManager.defaultManager().getSelectedPath().length == 0) {
        getRootPane().setJMenuBar(wrappingMenuBar);
      }
    });
  }

  @Override public void popupMenuCanceled(PopupMenuEvent e) {
    EventQueue.invokeLater(() -> getRootPane().setJMenuBar(wrappingMenuBar));
  }
};
for (int i = 0; i < mainMenuBar.getMenuCount(); i++) {
  mainMenuBar.getMenu(i).getPopupMenu().addPopupMenuListener(handler);
}
// ...
private JButton makeHamburgerMenuButton(JMenuBar menuBar) {
  JButton button = new JButton("Ξ") {
    @Override public Dimension getPreferredSize() {
      Dimension d = super.getPreferredSize();
      d.height = menuBar.getMenu(0).getPreferredSize().height;
      return d;
    }

    @Override public void updateUI() {
      super.updateUI();
      setContentAreaFilled(false);
      setFocusPainted(false);
      setBorder(BorderFactory.createEmptyBorder(0, 2, 0, 2));
    }
  };
  button.addActionListener(e -> {
    getRootPane().setJMenuBar(menuBar);
    getRootPane().revalidate();
    EventQueue.invokeLater(() -> menuBar.getMenu(0).doClick());
  });
  button.setMnemonic('\\');
  button.setToolTipText("Main Menu(Alt+\\)");
  return button;
}

References

2024/02/29

Paint JMenuItem selection rollover with rounded rectangle

Code

class BasicRoundMenuItemUI extends BasicMenuItemUI {
  @Override protected void paintBackground(
      Graphics g, JMenuItem menuItem, Color bgColor) {
    ButtonModel m = menuItem.getModel();
    Color oldColor = g.getColor();
    int menuWidth = menuItem.getWidth();
    int menuHeight = menuItem.getHeight();
    if (menuItem.isOpaque()) {
      if (m.isArmed() || (menuItem instanceof JMenu && m.isSelected())) {
        Graphics2D g2 = (Graphics2D) g.create();
        g2.setRenderingHint(
          RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        // g2.clearRect(0, 0, menuWidth, menuHeight);
        g2.setPaint(menuItem.getBackground());
        g2.fillRect(0, 0, menuWidth, menuHeight);
        g2.setColor(bgColor);
        g2.fillRoundRect(2, 2, menuWidth - 4, menuHeight - 4, 8, 8);
        g2.dispose();
      } else {
        g.setColor(menuItem.getBackground());
        g.fillRect(0, 0, menuWidth, menuHeight);
      }
      g.setColor(oldColor);
    } else if (m.isArmed() || (menuItem instanceof JMenu && m.isSelected())) {
      g.setColor(bgColor);
      g.fillRoundRect(2, 2, menuWidth - 4, menuHeight - 4, 8, 8);
      g.setColor(oldColor);
    }
  }
}

References

2024/01/31

Paint a JProgressBar with indeterminate progress status in a cell of a JTable

Code

private final JTable table = new JTable(model) {
  @Override public void updateUI() {
    super.updateUI();
    removeColumn(getColumnModel().getColumn(3));
    JProgressBar progress = new JProgressBar();
    TableCellRenderer renderer = new DefaultTableCellRenderer();
    TableColumn tc = getColumnModel().getColumn(2);
    tc.setCellRenderer((tbl, value, isSelected, hasFocus, row, column) -> {
      Component c;
      if (value instanceof JProgressBar) {
        c = (JProgressBar) value;
      } else if (value instanceof Integer) {
        progress.setValue((int) value);
        c = progress;
      } else {
        c = renderer.getTableCellRendererComponent(
            tbl, Objects.toString(value), isSelected, hasFocus, row, column);
      }
      return c;
    });
  }
};

class IndeterminateProgressBarUI extends BasicProgressBarUI {
  @Override public void incrementAnimationIndex() {
    super.incrementAnimationIndex();
  }
}

class BackgroundTask extends SwingWorker<Integer, Object> {
  private final Random rnd = new Random();

  @Override protected Integer doInBackground() throws InterruptedException {
    int lengthOfTask = calculateTaskSize();
    int current = 0;
    int total = 0;
    while (current <= lengthOfTask && !isCancelled()) {
      publish(100 * current / lengthOfTask);
      total += doSomething();
      current++;
    }
    return total;
  }

  private int calculateTaskSize() throws InterruptedException {
    int total = 0;
    JProgressBar indeterminate = new JProgressBar() {
      @Override public void updateUI() {
        super.updateUI();
        setUI(new IndeterminateProgressBarUI());
      }
    };
    indeterminate.setIndeterminate(true);
    // Indeterminate loop:
    for (int i = 0; i < 200; i++) {
      int iv = rnd.nextInt(50) + 1;
      Thread.sleep(iv);
      ProgressBarUI ui = indeterminate.getUI()
      ((IndeterminateProgressBarUI) ui).incrementAnimationIndex();
      publish(indeterminate);
      total += iv;
    }
    return 1 + total / 100;
  }

  private int doSomething() throws InterruptedException {
    int iv = rnd.nextInt(50) + 1;
    Thread.sleep(iv);
    return iv;
  }
}

References

2023/12/31

Create a 4-digit numeric PIN code input field using JPasswordField

Code

class PinCodeDocumentFilter extends DocumentFilter {
  public static final int MAX = 4;

  @Override public void replace(
      DocumentFilter.FilterBypass fb, int offset,
      int length, String text, AttributeSet attrs) throws BadLocationException {
    String str = fb.getDocument().getText(
      0, fb.getDocument().getLength()) + text;
    if (str.length() <= MAX && str.matches("\\d+")) {
      super.replace(fb, offset, length, text, attrs);
    }
  }
}

class PasswordView2 extends PasswordView {
  public PasswordView2(Element elem) {
    super(elem);
  }

  @Override protected int drawUnselectedText(
      Graphics g, int x, int y, int p0, int p1) throws BadLocationException {
    return drawText(g, x, y, p0, p1);
  }

  // @see drawUnselectedTextImpl(...)
  private int drawText(Graphics g, int x, int y, int p0, int p1)
        throws BadLocationException {
    Container c = getContainer();
    int j = x;
    if (c instanceof JPasswordField) {
      JPasswordField f = (JPasswordField) c;
      if (f.isEnabled()) {
        g.setColor(f.getForeground());
      } else {
        g.setColor(f.getDisabledTextColor());
      }
      Graphics2D g2 = (Graphics2D) g;
      char echoChar = f.getEchoChar();
      int n = p1 - p0;
      // Override BasicPasswordFieldUI#create(Element) to return PasswordView2
      // that makes only trailing digits visible without masking
      for (int i = 0; i < n; i++) {
        j = i == n - 1
            ? drawLastChar(g2, j, y, i)
            : drawEchoCharacter(g, j, y, echoChar);
      }
    }
    return j;
  }

  private int drawLastChar(Graphics g, int x, int y, int p1)
        throws BadLocationException {
    Graphics2D g2 = (Graphics2D) g;
    Font font = g2.getFont();
    float fs = font.getSize2D();
    double w = font.getStringBounds("0", g2.getFontRenderContext()).getWidth();
    int sz = (int) ((fs - w) / 2d);
    Document doc = getDocument();
    Segment s = new Segment(); // SegmentCache.getSharedSegment();
    doc.getText(p1, 1, s);
    // int ret = Utilities.drawTabbedText(s, x, y, g, this, p1);
    // SegmentCache.releaseSharedSegment(s);
    return Utilities.drawTabbedText(s, x + sz, y, g, this, p1);
  }
}

References

2023/11/30

Split TableColumn of JTableHeader with diagonal line Border

Code

int size = 32;
JTable table = new JTable(model) {
  @Override public void updateUI() {
    super.updateUI();
    setRowHeight(size);
    TableCellRenderer hr = new VerticalTableHeaderRenderer();
    TableColumnModel cm = getColumnModel();
    cm.getColumn(0).setHeaderRenderer(
        new DiagonallySplitHeaderRenderer());
    cm.getColumn(0).setPreferredWidth(size * 5);
    for (int i = 1; i < cm.getColumnCount(); i++) {
      TableColumn tc = cm.getColumn(i);
      tc.setHeaderRenderer(hr);
      tc.setPreferredWidth(size);
    }
  }
};
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);

JScrollPane scroll = new JScrollPane(table);
scroll.setColumnHeader(new JViewport() {
  @Override public Dimension getPreferredSize() {
    Dimension d = super.getPreferredSize();
    d.height = size * 2;
    return d;
  }
});
// ...

class DiagonallySplitBorder extends MatteBorder {
  protected DiagonallySplitBorder(
      int top, int left, int bottom, int right, Color matteColor) {
    super(top, left, bottom, right, matteColor);
  }

  @Override public void paintBorder(
      Component c, Graphics g, int x, int y, int width, int height) {
    super.paintBorder(c, g, x, y, width, height);
    Graphics2D g2 = (Graphics2D) g.create();
    g2.translate(x, y);
    g2.setRenderingHint(
        RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setPaint(getMatteColor());
    g2.drawLine(0, 0, c.getWidth() - 1, c.getHeight() - 1);
    g2.dispose();
  }
}

References