Google Tag Manager

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

2023/10/31

Sort JTable rows with multiple conditions

Code

RowSorter<? extends TableModel> sorter = table.getRowSorter();
if (sorter instanceof TableRowSorter) {
  TableRowSorter<? extends TableModel> rs
      = (TableRowSorter<? extends TableModel>) sorter;
  rs.setComparator(0, Comparator.comparing(RowData::getPosition));
  rs.setComparator(1, Comparator.comparing(RowData::getTeam));
  rs.setComparator(2, Comparator.comparing(RowData::getMatches));
  rs.setComparator(3, Comparator.comparing(RowData::getWins));
  rs.setComparator(4, Comparator.comparing(RowData::getDraws));
  rs.setComparator(5, Comparator.comparing(RowData::getLosses));
  rs.setComparator(6, Comparator.comparing(RowData::getGoalsFor));
  rs.setComparator(7, Comparator.comparing(RowData::getGoalsAgainst));
  rs.setComparator(8, Comparator.comparing(RowData::getGoalDifference));
  rs.setComparator(9, Comparator.comparing(RowData::getPoints)
      .thenComparing(RowData::getGoalDifference));
}
add(new JScrollPane(table));

References

2023/09/30

Move the selected item in JList up or down by clicking on the JButton placed on the JToolBar

Since DefaultListModel does not have a move method like DefaultTableModel#moveRow(int start, int end, int to), the DefaultListModel#get(int index), DefaultListModel#remove(int index), and DefaultListModel#add(int index, E element) methods are used in combination to move selected items up and down the JList.

Code

JButton up = new JButton("▲");
up.setFocusable(false);
up.addActionListener(e -> {
  int[] pos = list.getSelectedIndices();
  if (pos.length == 0) {
    return;
  }
  boolean isShiftDown = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
  int index0 = isShiftDown ? 0 : Math.max(0, pos[0] - 1);
  int idx = index0;
  for (int i : pos) {
    model.add(idx, model.remove(i));
    list.addSelectionInterval(idx, idx);
    idx++;
  }
  // scroll
  Rectangle r = list.getCellBounds(index0, index0 + pos.length);
  list.scrollRectToVisible(r);
});

JButton down = new JButton("▼");
down.setFocusable(false);
down.addActionListener(e -> {
  int[] pos = list.getSelectedIndices();
  if (pos.length == 0) {
    return;
  }
  boolean isShiftDown = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
  int max = model.getSize();
  int index = isShiftDown ? max : Math.min(max, pos[pos.length - 1] + 1);
  int index0 = index;
  // copy
  for (int i : pos) {
    int idx = Math.min(model.getSize(), ++index);
    model.add(idx, model.get(i));
    list.addSelectionInterval(idx, idx);
  }
  // clean
  for (int i = pos.length - 1; i >= 0; i--) {
    model.remove(pos[i]);
  }
  // scroll
  Rectangle r = list.getCellBounds(index0 - pos.length, index0);
  list.scrollRectToVisible(r);
});

References

2023/08/31

Hide the check icon on the JRadioButtonMenuItem and reduce the text offset

Code

UIManager.put(PRE + "minimumTextOffset", 10);
UIManager.put(PRE + "afterCheckIconGap", 0);
UIManager.put(PRE + "checkIconOffset", 0);
Icon checkIcon = getCheckIcon();
int height = checkIcon == null ? 22 : checkIcon.getIconHeight();
UIManager.put(PRE + "checkIcon", new EmptyIcon());
Dimension d = new Dimension(100, height);
JPopupMenu popup = new JPopupMenu();
ButtonGroup bg = new ButtonGroup();
Arrays.asList(
    makeMenuItem("0.5 pt", .5f, d),
    makeMenuItem("0.75 pt", .75f, d),
    makeMenuItem("1 pt", 1f, d),
    makeMenuItem("1.5 pt", 1.5f, d),
    makeMenuItem("2.25 pt", 2.25f, d),
    makeMenuItem("3 pt", 3f, d)
).forEach(m -> {
  popup.add(m);
  bg.add(m);
});
// ...
private static JMenuItem makeMenuItem(String txt, float width, Dimension d) {
  float px = width * Toolkit.getDefaultToolkit().getScreenResolution() / 72f;
  return new JRadioButtonMenuItem(txt, new LineIcon(new BasicStroke(px), d)) {
    @Override protected void init(String text, Icon icon) {
      super.init(text, icon);
      setHorizontalTextPosition(LEADING);
      setHorizontalAlignment(TRAILING);
    }

    @Override protected void paintComponent(Graphics g) {
      if (isSelected()) {
        Graphics2D g2 = (Graphics2D) g.create();
        g2.setPaint(new Color(0xAA_64_AA_FF, true));
        g2.fillRect(0, 0, getWidth(), getHeight());
        g2.dispose();
      }
      super.paintComponent(g);
    }
  };
}

References

2023/07/31

Add item check boxes to JList cells

Code

@Override public void setSelectionInterval(int anchor, int lead) {
  if (checkedIndex < 0 && isDragging()) {
    super.setSelectionInterval(anchor, lead);
  } else {
    EventQueue.invokeLater(() -> {
      if (checkedIndex >= 0 && lead == anchor && checkedIndex == anchor) {
        super.addSelectionInterval(checkedIndex, checkedIndex);
      } else {
        super.setSelectionInterval(anchor, lead);
      }
    });
  }
}

protected boolean isDragging() {
  Rectangle r = getRubberBand().getBounds();
  return r.width != 0 || r.height != 0;
}

@Override public void removeSelectionInterval(int index0, int index1) {
  if (checkedIndex < 0) {
    super.removeSelectionInterval(index0, index1);
  } else {
    EventQueue.invokeLater(() -> super.removeSelectionInterval(index0, index1));
  }
}

private static <E> Optional<AbstractButton> getItemCheckBox(
    JList<E> list, MouseEvent e, int index) {
  if (e.isShiftDown() || e.isControlDown() || e.isAltDown()) {
    return Optional.empty();
  }
  E proto = list.getPrototypeCellValue();
  ListCellRenderer<? super E> cr = list.getCellRenderer();
  Component c = cr.getListCellRendererComponent(
      list, proto, index, false, false);
  Rectangle r = list.getCellBounds(index, index);
  c.setBounds(r);
  Point pt = e.getPoint();
  pt.translate(-r.x, -r.y);
  return Optional
    .ofNullable(SwingUtilities.getDeepestComponentAt(c, pt.x, pt.y))
    .filter(AbstractButton.class::isInstance)
    .map(AbstractButton.class::cast);
}

private final class ItemCheckBoxesListener extends MouseAdapter {
  private final Point srcPoint = new Point();

  @Override public void mouseDragged(MouseEvent e) {
    checkedIndex = -1;
    JList<?> l = (JList<?>) e.getComponent();
    l.setFocusable(true);
    Point destPoint = e.getPoint();
    Path2D rb = getRubberBand();
    rb.reset();
    rb.moveTo(srcPoint.x, srcPoint.y);
    rb.lineTo(destPoint.x, srcPoint.y);
    rb.lineTo(destPoint.x, destPoint.y);
    rb.lineTo(srcPoint.x, destPoint.y);
    rb.closePath();

    int[] indices = IntStream.range(0, l.getModel().getSize())
            .filter(i -> rb.intersects(l.getCellBounds(i, i))).toArray();
    l.setSelectedIndices(indices);
    l.repaint();
  }

  @Override public void mouseExited(MouseEvent e) {
    rollOverIndex = -1;
    e.getComponent().repaint();
  }

  @Override public void mouseMoved(MouseEvent e) {
    Point pt = e.getPoint();
    int idx = locationToIndex(pt);
    if (!getCellBounds(idx, idx).contains(pt)) {
      idx = -1;
    }
    Rectangle rect = new Rectangle();
    if (idx > 0) {
      rect.add(getCellBounds(idx, idx));
      if (rollOverIndex >= 0 && idx != rollOverIndex) {
        rect.add(getCellBounds(rollOverIndex, rollOverIndex));
      }
      rollOverIndex = idx;
    } else {
      if (rollOverIndex >= 0) {
        rect.add(getCellBounds(rollOverIndex, rollOverIndex));
      }
      rollOverIndex = -1;
    }
    ((JComponent) e.getComponent()).repaint(rect);
  }

  @Override public void mouseReleased(MouseEvent e) {
    getRubberBand().reset();
    Component c = e.getComponent();
    c.setFocusable(true);
    c.repaint();
  }

  @Override public void mousePressed(MouseEvent e) {
    JList<?> l = (JList<?>) e.getComponent();
    int index = l.locationToIndex(e.getPoint());
    if (l.getCellBounds(index, index).contains(e.getPoint())) {
      l.setFocusable(true);
      cellPressed(l, e, index);
    } else {
      l.setFocusable(false);
      l.clearSelection();
      l.getSelectionModel().setAnchorSelectionIndex(-1);
      l.getSelectionModel().setLeadSelectionIndex(-1);
    }
    srcPoint.setLocation(e.getPoint());
    l.repaint();
  }

  private void cellPressed(JList<?> l, MouseEvent e, int index) {
    if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() > 1) {
      ListItem item = getModel().getElementAt(index);
      JOptionPane.showMessageDialog(l.getRootPane(), item.title);
    } else {
      checkedIndex = -1;
      getItemCheckBox(l, e.getPoint(), index).ifPresent(rb -> {
        checkedIndex = index;
        if (l.isSelectedIndex(index)) {
          l.setFocusable(false);
          removeSelectionInterval(index, index);
        } else {
          setSelectionInterval(index, index);
        }
      });
    }
  }
}

References

2023/07/01

Click on the JCheckBox placed in the JTable cell to expand and collapss the row height

Code

int defaultHeight = 20;
JTable table = new JTable(model) {
  @Override public void updateUI() {
    super.updateUI();
    setAutoCreateRowSorter(true);
    setSurrendersFocusOnKeystroke(true);
    setRowHeight(defaultHeight);
    setDefaultRenderer(RowHeader.class, new RowHeaderRenderer());
    setDefaultEditor(RowHeader.class, new RowHeaderEditor());
    TableColumn column = getColumnModel().getColumn(1);
    column.setCellRenderer(new TextAreaCellRenderer());
    column.setPreferredWidth(160);
  }
};
table.getModel().addTableModelListener(e -> {
  int mc = e.getColumn();
  int mr = e.getFirstRow();
  int vc = table.convertColumnIndexToView(mc);
  int vr = table.convertRowIndexToView(mr);
  Object o = table.getValueAt(vr, vc);
  if (mc == 0 && o instanceof RowHeader) {
    RowHeader rh = (RowHeader) o;
    int vc1 = table.convertColumnIndexToView(1);
    TableCellRenderer r = table.getColumnModel().getColumn(vc1)
                               .getCellRenderer();
    Object v = table.getValueAt(vr, vc1);
    Component c = r.getTableCellRendererComponent(
        table, v, true, true, vr, vc1);
    int h = rh.isSelected() ? c.getPreferredSize().height
                            : defaultHeight;
    table.setRowHeight(vr, h);
  }
});

References

2023/04/30

Change the tooltip of a tab in the JTabbedPane to a speech balloon shape, depending on the position of the tab.

Code

class BalloonToolTip extends JToolTip {
  private static final int SIZE = 4;
  private static final double ARC = 4d;
  private transient HierarchyListener listener;
  private transient Shape shape;

  @Override public void updateUI() {
    removeHierarchyListener(listener);
    super.updateUI();
    setLayout(new BorderLayout());
    listener = e -&gt; {
      Component c = e.getComponent();
      if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0
            &amp;&amp; c.isShowing()) {
        Optional.ofNullable(SwingUtilities.getWindowAncestor(c))
            .filter(w -&gt; w.getType() == Window.Type.POPUP)
            .ifPresent(w -&gt; w.setBackground(new Color(0x0, true)));
      }
    };
    addHierarchyListener(listener);
    setOpaque(false);
    setBorder(BorderFactory.createEmptyBorder(SIZE, SIZE, SIZE, SIZE));
  }

  @Override public Dimension getPreferredSize() {
    Dimension d = super.getPreferredSize();
    d.width += SIZE;
    d.height += SIZE;
    return d;
  }

  @Override protected void paintComponent(Graphics g) {
    Graphics2D g2 = (Graphics2D) g.create();
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setColor(getBackground());
    g2.fill(shape);
    g2.setPaint(getForeground());
    g2.draw(shape);
    g2.dispose();
    // super.paintComponent(g);
  }

  public void updateBalloonShape(int placement) {
    Insets i = getInsets();
    Dimension d = getPreferredSize();
    Path2D tail = new Path2D.Double();
    double w = d.getWidth() - i.left - i.right - 1d;
    double h = d.getHeight() - i.top - i.bottom - 1d;
    double cx = w / 2d;
    double cy = h / 2d;
    switch (placement) {
      case SwingConstants.LEFT:
        tail.moveTo(0, cy - SIZE);
        tail.lineTo(-SIZE, cy);
        tail.lineTo(0, cy + SIZE);
        break;
      case SwingConstants.RIGHT:
        tail.moveTo(w, cy - SIZE);
        tail.lineTo(w + SIZE, cy);
        tail.lineTo(w, cy + SIZE);
        break;
      case SwingConstants.BOTTOM:
        tail.moveTo(cx - SIZE, h);
        tail.lineTo(cx, h + SIZE);
        tail.lineTo(cx + SIZE, h);
        break;
      default: // case SwingConstants.TOP:
        tail.moveTo(cx - SIZE, 0);
        tail.lineTo(cx, -SIZE);
        tail.lineTo(cx + SIZE, 0);
    }
    Area area = new Area(new RoundRectangle2D.Double(0, 0, w, h, ARC, ARC));
    area.add(new Area(tail));
    AffineTransform at = AffineTransform.getTranslateInstance(i.left, i.top);
    shape = at.createTransformedShape(area);
  }
}

JTabbedPane tabs = new JTabbedPane(
    SwingConstants.TOP, JTabbedPane.SCROLL_TAB_LAYOUT) {
  private transient BalloonToolTip tip;
  private final JLabel label = new JLabel(" ", CENTER);

  @Override public Point getToolTipLocation(MouseEvent e) {
    int idx = indexAtLocation(e.getX(), e.getY());
    String txt = idx &gt;= 0 ? getToolTipTextAt(idx) : null;
    return Optional.ofNullable(txt).map(toolTipText -&gt; {
      JToolTip tips = createToolTip();
      tips.setTipText(toolTipText);
      label.setText(toolTipText);
      if (tips instanceof BalloonToolTip) {
        ((BalloonToolTip) tips).updateBalloonShape(getTabPlacement());
      }
      return getToolTipPoint(
          getBoundsAt(idx), tips.getPreferredSize());
    }).orElse(null);
  }

  private Point getToolTipPoint(Rectangle r, Dimension d) {
    double dx;
    double dy;
    switch (getTabPlacement()) {
      case LEFT:
        dx = r.getMaxX();
        dy = r.getCenterY() - d.getHeight() / 2d;
        break;
      case RIGHT:
        dx = r.getMinX() - d.width;
        dy = r.getCenterY() - d.getHeight() / 2d;
        break;
      case BOTTOM:
        dx = r.getCenterX() - d.getWidth() / 2d;
        dy = r.getMinY() - d.height;
        break;
      default: // case TOP:
        dx = r.getCenterX() - d.getWidth() / 2d;
        dy = r.getMaxY();
    }
    return new Point((int) (dx + .5), (int) (dy + .5));
  }

  @Override public JToolTip createToolTip() {
    if (tip == null) {
      tip = new BalloonToolTip();
      LookAndFeel.installColorsAndFont(
          label,
          "ToolTip.background",
          "ToolTip.foreground",
          "ToolTip.font");
      tip.add(label);
      tip.updateBalloonShape(getTabPlacement());
      tip.setComponent(this);
    }
    return tip;
  }

  @Override public void updateUI() {
    tip = null;
    super.updateUI();
  }
};

References

2023/03/31

Create a dot matrix LED digital clock with JList

Code

private static JList<Boolean> makeLedDotMatrixList(
    ListModel<Boolean> model, Dimension dim) {
  return new JList<Boolean>(model) {
    @Override public void updateUI() {
      setFixedCellWidth(dim.width);
      setFixedCellHeight(dim.height);
      setVisibleRowCount(ROW);
      setCellRenderer(null);
      super.updateUI();
      setLayoutOrientation(JList.VERTICAL_WRAP);
      setFocusable(false);
      ListCellRenderer<? super Boolean> renderer = getCellRenderer();
      Icon on = new LedDotIcon(true, dim);
      Icon off = new LedDotIcon(false, dim);
      setCellRenderer((list, value, index, isSelected, cellHasFocus) -> {
        Component c = renderer.getListCellRendererComponent(
            list, null, index, false, false);
        if (c instanceof JLabel) {
          ((JLabel) c).setIcon(Objects.equals(Boolean.TRUE, value) ? on : off);
        }
        return c;
      });
      setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
      setBackground(Color.BLACK);
    }
  };
}

class LedDotIcon implements Icon {
  private final Color on = new Color(0x32_FF_AA);
  private final boolean led;
  private final Dimension dim;

  protected LedDotIcon(boolean led, Dimension dim) {
    this.led = led;
    this.dim = dim;
  }

  @Override public void paintIcon(Component c, Graphics g, int x, int y) {
    Graphics2D g2 = (Graphics2D) g.create();
    g2.setRenderingHint(
        RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    // JList#setLayoutOrientation(VERTICAL_WRAP) + Synth, Nimbus, GTK bug???
    // g2.translate(x, y);
    g2.setPaint(led ? on : c.getBackground());
    g2.fillOval(0, 0, getIconWidth() - 1, getIconHeight() - 1);
    g2.dispose();
  }

  @Override public int getIconWidth() {
    return dim.width;
  }

  @Override public int getIconHeight() {
    return dim.height;
  }
}

DefaultListModel<Boolean> model1 = new DefaultListModel<Boolean>() {
  @Override public Boolean getElementAt(int index) {
    return getHoursMinutesDotMatrix(time, index);
  }
};
model1.setSize((COLUMN * 4 + 5) * ROW);

private static boolean getHoursMinutesDotMatrix(LocalTime time, int index) {
  int ten = 10;
  int hours = time.getHour();
  int h1 = hours / ten;
  int start = 0;
  int end = start + COLUMN;
  if (contains(index, start, end, h1)) {
    return hours >= ten;
  }
  int gap = 1;
  int h2 = hours - h1 * ten;
  start = end + gap;
  end = start + COLUMN;
  if (contains(index, start, end, h2)) {
    return true;
  }
  int seconds = time.getSecond();
  int s1 = seconds / ten;
  int s2 = seconds - s1 * ten;
  start = end + gap;
  end = start + gap;
  if (index < end * ROW && s2 % 2 == 0 && DOT.contains(index - start * ROW)) {
    return true;
  }
  int minutes = time.getMinute();
  int m1 = minutes / ten;
  start = end + gap;
  end = start + COLUMN;
  if (contains(index, start, end, m1)) {
    return true;
  }
  int m2 = minutes - m1 * ten;
  start = end + gap;
  end = start + COLUMN;
  return contains(index, start, end, m2);
}

private static final int COLUMN = 4;
private static final int ROW = 7;
private static final List<Set<Integer>> NUMBERS = Arrays.asList(
  Set.of(0, 1, 2, 3, 4, 5, 6, 7, 13, 14, 20, 21, 22, 23, 24, 25, 26, 27), // 0
  Set.of(21, 22, 23, 24, 25, 26, 27), // 1
  Set.of(0, 3, 4, 5, 6, 7, 10, 13, 14, 17, 20, 21, 22, 23, 24, 27), // 2
  Set.of(0, 3, 6, 7, 10, 13, 14, 17, 20, 21, 22, 23, 24, 25, 26, 27), // 3
  Set.of(0, 1, 2, 3, 10, 17, 21, 22, 23, 24, 25, 26, 27), // 4
  Set.of(0, 1, 2, 3, 6, 7, 10, 13, 14, 17, 20, 21, 24, 25, 26, 27), // 5
  Set.of(0, 1, 2, 3, 4, 5, 6, 7, 10, 13, 14, 17, 20, 21, 24, 25, 26, 27), // 6
  Set.of(0, 1, 2, 3, 7, 14, 21, 22, 23, 24, 25, 26, 27), // 7
  Set.of(0, 1, 2, 3, 4, 5, 6, 7, 10, 13, 14, 17, 20, 21, 22, 23, 24, 25, 26, 27), // 8
  Set.of(0, 1, 2, 3, 6, 7, 10, 13, 14, 17, 20, 21, 22, 23, 24, 25, 26, 27)); // 9

Code(alternative pattern)

private static final int COLUMN = 5;
private static final int ROW = 7;
private static final List> NUMBERS = Arrays.asList(
    Set.of(1, 2, 3, 4, 5, 7, 9, 13, 14, 17, 20, 21, 25, 27, 29, 30, 31, 32, 33), // 0
    Set.of(8, 13, 14, 15, 16, 17, 18, 19, 20, 27), // 1
    Set.of(1, 6, 7, 12, 13, 14, 18, 20, 21, 24, 27, 29, 30, 34), // 2
    Set.of(0, 5, 7, 13, 14, 17, 20, 21, 23, 24, 27, 28, 29, 32, 33), // 3
    Set.of(3, 4, 9, 11, 15, 18, 21, 22, 23, 24, 25, 26, 27, 32), // 4
    Set.of(0, 1, 2, 5, 7, 9, 13, 14, 16, 20, 21, 23, 27, 28, 31, 32, 33), // 5
    Set.of(1, 2, 3, 4, 5, 7, 10, 13, 14, 17, 20, 21, 24, 27, 29, 32, 33), // 6
    Set.of(0, 7, 11, 12, 13, 14, 17, 21, 23, 28, 29), // 7
    Set.of(1, 2, 4, 5, 7, 10, 13, 14, 17, 20, 21, 24, 27, 29, 30, 32, 33), // 8
    Set.of(1, 2, 5, 7, 10, 13, 14, 17, 20, 21, 24, 27, 29, 30, 31, 32, 33)); // 9

References

2023/02/28

Draw a projection-transformed regular hexahedron wireframe in JPanel and rotate it in space by mouse dragging

Code

class Vertex {
  private double dx;
  private double dy;
  private double dz;
  public double vx;
  public double vy;

  protected Vertex(double dx, double dy, double dz) {
    this.dx = dx;
    this.dy = dy;
    this.dz = dz;
    projectionTransformation();
  }

  private void projectionTransformation() {
    double screenDistance = 500d;
    double depth = 1000d;
    double gz = dz + depth;
    this.vx = screenDistance * dx / gz;
    this.vy = screenDistance * dy / gz;
  }

  public void rotateTransformation(double kx, double ky, double kz) {
    double x0 = dx * Math.cos(ky) - dz * Math.sin(ky);
    double y0 = dy;
    double z0 = dx * Math.sin(ky) + dz * Math.cos(ky);
    double y1 = y0 * Math.cos(kx) - z0 * Math.sin(kx);
    double z1 = y0 * Math.sin(kx) + z0 * Math.cos(kx);
    this.dx = x0 * Math.cos(kz) - y1 * Math.sin(kz);
    this.dy = x0 * Math.sin(kz) + y1 * Math.cos(kz);
    this.dz = z1;
    projectionTransformation();
  }
}

// ...
List<Vertex> cube = new ArrayList<>(8);
double sideLength = 100;
cube.add(new Vertex(sideLength, sideLength, sideLength));
cube.add(new Vertex(sideLength, sideLength, -sideLength));
cube.add(new Vertex(-sideLength, sideLength, -sideLength));
cube.add(new Vertex(-sideLength, sideLength, sideLength));
cube.add(new Vertex(sideLength, -sideLength, sideLength));
cube.add(new Vertex(sideLength, -sideLength, -sideLength));
cube.add(new Vertex(-sideLength, -sideLength, -sideLength));
cube.add(new Vertex(-sideLength, -sideLength, sideLength));

// ...
// Projection transformation of each vertex of a regular hexahedron by moving the viewpoint to the origin, the distance to the screen to 500, and the z-axis coordinate value to the back of the screen to 1000.
// Draw a wireframe in the center of the JPanel by connecting the transformed vertex coordinates with Path2D
@Override protected void paintComponent(Graphics g) {
  super.paintComponent(g);
  Graphics2D g2 = (Graphics2D) g.create();
  g2.setRenderingHint(
    RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
  Path2D path = new Path2D.Double();
  path.moveTo(cube.get(0).vx, cube.get(0).vy);
  path.lineTo(cube.get(1).vx, cube.get(1).vy);
  path.lineTo(cube.get(2).vx, cube.get(2).vy);
  path.lineTo(cube.get(3).vx, cube.get(3).vy);
  path.lineTo(cube.get(0).vx, cube.get(0).vy);
  path.lineTo(cube.get(4).vx, cube.get(4).vy);
  path.lineTo(cube.get(5).vx, cube.get(5).vy);
  path.lineTo(cube.get(6).vx, cube.get(6).vy);
  path.lineTo(cube.get(7).vx, cube.get(7).vy);
  path.lineTo(cube.get(4).vx, cube.get(4).vy);
  path.moveTo(cube.get(1).vx, cube.get(1).vy);
  path.lineTo(cube.get(5).vx, cube.get(5).vy);
  path.moveTo(cube.get(2).vx, cube.get(2).vy);
  path.lineTo(cube.get(6).vx, cube.get(6).vy);
  path.moveTo(cube.get(3).vx, cube.get(3).vy);
  path.lineTo(cube.get(7).vx, cube.get(7).vy);
  Rectangle r = SwingUtilities.calculateInnerArea(this, null);
  g2.setPaint(Color.WHITE);
  g2.fill(r);
  g2.translate(r.getCenterX(), r.getCenterY());
  g2.setPaint(Color.BLACK);
  g2.draw(path);
  g2.dispose();
}

// ...
// Add the following `MouseAdapter` to the `JPanel` that transforms and draws the projection of the regular hexahedron, and rotate each vertex in the space
private class DragRotateHandler extends MouseAdapter {
  private final Cursor defCursor = Cursor.getDefaultCursor();
  private final Cursor hndCursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
  private final Point pp = new Point();

  @Override public void mouseDragged(MouseEvent e) {
    Point pt = e.getPoint();
    double rotY = (pt.x - pp.x) * .03;
    double rotX = (pt.y - pp.y) * .03;
    double rotZ = 0d;
    for (Vertex v : cube) {
      v.rotateTransformation(rotX, rotY, rotZ);
    }
    pp.setLocation(pt);
    e.getComponent().repaint();
  }

  @Override public void mousePressed(MouseEvent e) {
    e.getComponent().setCursor(hndCursor);
    pp.setLocation(e.getPoint());
  }

  @Override public void mouseReleased(MouseEvent e) {
    e.getComponent().setCursor(defCursor);
  }
}

References

2023/01/31

Create numbers for a 7-segment digital clock using Path2D

Code

class DigitalNumber {
  private final double isosceles;
  private final double dx;
  private final double dy;
  private final double width;
  private final double height;
  private final Rectangle rect = new Rectangle();
  public static final Color OFF = new Color(0xCC_CC_CC);
  public static final Color ON = Color.DARK_GRAY;
  public static final Color BGC = Color.LIGHT_GRAY;
  private final List<Set<Seg>> numbers = Arrays.asList(
      EnumSet.of(Seg.A, Seg.B, Seg.C, Seg.D, Seg.E, Seg.F),
      EnumSet.of(Seg.B, Seg.C),
      EnumSet.of(Seg.A, Seg.B, Seg.D, Seg.E, Seg.G),
      EnumSet.of(Seg.A, Seg.B, Seg.C, Seg.D, Seg.G),
      EnumSet.of(Seg.B, Seg.C, Seg.F, Seg.G),
      EnumSet.of(Seg.A, Seg.C, Seg.D, Seg.F, Seg.G),
      EnumSet.of(Seg.A, Seg.C, Seg.D, Seg.E, Seg.F, Seg.G),
      EnumSet.of(Seg.A, Seg.B, Seg.C),
      EnumSet.of(Seg.A, Seg.B, Seg.C, Seg.D, Seg.E, Seg.F, Seg.G),
      EnumSet.of(Seg.A, Seg.B, Seg.C, Seg.D, Seg.F, Seg.G));
  private Set<Seg> led = EnumSet.noneOf(Seg.class);

  protected DigitalNumber(double dx, double dy, double isosceles) {
    this.isosceles = isosceles;
    this.dx = dx;
    this.dy = dy;
    this.width = 2d * isosceles;
    this.height = width + isosceles;
    rect.setLocation((int) dx, (int) dy);
    rect.setSize((int) (width + 3 * isosceles), (int) (height * 2));
  }

  public Rectangle getBounds() {
    return rect;
  }

  public void setNumber(int num) {
    led = numbers.get(num);
  }

  public void turnOffNumber() {
    led.clear();
  }

  public void drawNumber(Graphics2D g2) {
    EnumSet.allOf(Seg.class).forEach(s -> {
      g2.setColor(led.contains(s) ? ON : OFF);
      Shape seg = s.getShape(dx, dy, width, height, isosceles);
      g2.fill(seg);
      g2.setColor(BGC);
      g2.draw(seg);
    });
  }
}

enum Seg { A, B, C, D, E, F, G }

References