Google Tag Manager

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