Google Tag Manager

2020/11/30

Create switchable buttons in JSlider

Use JSlider to create switch buttons that can be toggled on and off by clicking the mouse or dragging the knob

Code

UIDefaults d = new UIDefaults();
d.put("Slider.thumbHeight", 40);
d.put("Slider.thumbWidth", 40);
d.put("Slider:SliderTrack[Enabled].backgroundPainter", (Painter) (g, c, w, h) -> {
  int arc = 40;
  int fillLeft = 2;
  int fillTop = 2;
  int trackWidth = w - fillLeft - fillLeft;
  int trackHeight = h - fillTop - fillTop;
  int baseline = trackHeight - fillTop - fillTop; // c.getBaseline(w, h);
  String off = "Off";

  g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
  g.setColor(Color.GRAY);
  g.fillRoundRect(fillLeft, fillTop, trackWidth, trackHeight, arc, arc);
  g.setPaint(Color.WHITE);
  g.drawString(off, w - g.getFontMetrics().stringWidth(off) - fillLeft * 5, baseline);

  int fillRight = getXPositionForValue(c, new Rectangle(fillLeft, fillTop, trackWidth, trackHeight));
  g.setColor(Color.ORANGE);
  g.fillRoundRect(fillLeft + 1, fillTop, fillRight - fillLeft, trackHeight, arc, arc);

  g.setPaint(Color.WHITE);
  if (fillRight - fillLeft > 0) {
    g.drawString("On", fillLeft * 5, baseline);
  }
  g.setStroke(new BasicStroke(2.5f));
  g.drawRoundRect(fillLeft, fillTop, trackWidth, trackHeight, arc, arc);
});

Painter thumbPainter = (g, c, w, h) -> {
  int fillLeft = 8;
  int fillTop = 8;
  int trackWidth = w - fillLeft - fillLeft;
  int trackHeight = h - fillTop - fillTop;
  g.setPaint(Color.WHITE);
  g.fillOval(fillLeft, fillTop, trackWidth, trackHeight);
};
d.put("Slider:SliderThumb[Disabled].backgroundPainter", thumbPainter);
d.put("Slider:SliderThumb[Enabled].backgroundPainter", thumbPainter);
d.put("Slider:SliderThumb[Focused+MouseOver].backgroundPainter", thumbPainter);
d.put("Slider:SliderThumb[Focused+Pressed].backgroundPainter", thumbPainter);
d.put("Slider:SliderThumb[Focused].backgroundPainter", thumbPainter);
d.put("Slider:SliderThumb[MouseOver].backgroundPainter", thumbPainter);
d.put("Slider:SliderThumb[Pressed].backgroundPainter", thumbPainter);

JSlider slider = new JSlider(0, 1, 0) {
  @Override public Dimension getPreferredSize() {
    return new Dimension(100, 40);
  }
};
slider.setFont(slider.getFont().deriveFont(Font.BOLD, 32f));
slider.putClientProperty("Nimbus.Overrides", d);

Explanation

  • Default
    • Set JSlider to Min 0, Max 1
    • Change component size by overriding getPreferredSize()
  • Thumb size
    • Resize knobs by setting Slider.thumbWidth, Slider.thumbHeight
  • SliderTrack
    • Create a Painter to draw the track background and change the background color, border, and On/Off text
    • Pressing a track turns On/Off
    • Change the drawing of the knob by setting Painter
    • ​Add MouseMotionListener to redraw the whole thing while dragging, since some afterimages may appear when you drag the knob
    • ​Pressing or clicking the knob does not cause the `On/Off 'toggle
  • JSlider + JLayer
    • ​The drawing of the track and knob is identical to the above SliderTrack
    • ​Use JLayer to set JSlider to switch On/Off regardless of track or knob
    • ​Redraw while dragging the knob is also done in LayerUI#processMouseMotionEvent(...)
class ToggleSwitchLayerUI extends LayerUI {
  @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 l) {
    if (e.getID() == MouseEvent.MOUSE_PRESSED && SwingUtilities.isLeftMouseButton(e)) {
      e.getComponent().dispatchEvent(new MouseEvent(
          e.getComponent(),
          e.getID(), e.getWhen(),
          InputEvent.BUTTON3_DOWN_MASK, // e.getModifiers(),
          e.getX(), e.getY(),
          e.getXOnScreen(), e.getYOnScreen(),
          e.getClickCount(),
          e.isPopupTrigger(),
          MouseEvent.BUTTON3)); // e.getButton());
      e.consume();
    } else if (e.getID() == MouseEvent.MOUSE_CLICKED && SwingUtilities.isLeftMouseButton(e)) {
      JSlider slider = l.getView();
      int v = slider.getValue();
      if (slider.getMinimum() == v) {
        slider.setValue(slider.getMaximum());
      } else if (slider.getMaximum() == v) {
        slider.setValue(slider.getMinimum());
      }
    }
  }

  @Override protected void processMouseMotionEvent(MouseEvent e, JLayer l) {
    l.getView().repaint();
  }
}

References

2020/10/31

Show or hide each TableColumn added to the JTableHeader

Code

class TableHeaderPopupMenu extends JPopupMenu {
  protected TableHeaderPopupMenu(JTable table) {
    super();
    TableColumnModel columnModel = table.getColumnModel();
    List>TableColumn> list = Collections.list(columnModel.getColumns());
    list.forEach(tableColumn -> {
      String name = Objects.toString(tableColumn.getHeaderValue());
      // System.out.format("%s - %s%n", name, tableColumn.getIdentifier());
      JCheckBoxMenuItem item = new JCheckBoxMenuItem(name, true);
      item.addItemListener(e -> {
        if (((AbstractButton) e.getItemSelectable()).isSelected()) {
          columnModel.addColumn(tableColumn);
        } else {
          columnModel.removeColumn(tableColumn);
        }
        updateMenuItems(columnModel);
      });
      add(item);
    });
  }

  @Override public void show(Component c, int x, int y) {
    if (c instanceof JTableHeader) {
      JTableHeader header = (JTableHeader) c;
      JTable table = header.getTable();
      header.setDraggedColumn(null);
      header.repaint();
      table.repaint();
      updateMenuItems(header.getColumnModel());
      super.show(c, x, y);
    }
  }

  private void updateMenuItems(TableColumnModel columnModel) {
    boolean isOnlyOneMenu = columnModel.getColumnCount() == 1;
    if (isOnlyOneMenu) {
      stream(this).map(MenuElement::getComponent).forEach(mi ->
          mi.setEnabled(!(mi instanceof AbstractButton)
                        || !((AbstractButton) mi).isSelected()));
    } else {
      stream(this).forEach(me -> me.getComponent().setEnabled(true));
    }
  }

  private static Stream>MenuElement> stream(MenuElement me) {
    return Stream.of(me.getSubElements())
      .flatMap(m -> Stream.concat(Stream.of(m), stream(m)));
  }
}

Explanation

  • Initially shows all TableColumns generated from a TableModel
    • All JCheckBoxMenuItems are also selected
  • TableColumn hidden with TableColumnModel#removeColumn(TableColumn) method when deselected with JCheckBoxMenuItem
    • The column is removed from TableColumnModel and hidden from JTableHeader, but the column remains in TableModel
    • Check the number of TableColumn columns to enable/disable the JCheckBoxMenuItem, e.g. when opening a JPopupMenu, so that all TableColumns are not hidden
  • Show TableColumn with TableColumnModel#addColumn(TableColumn) method when selected and set with JCheckBoxMenuItem
    • Columns are added to TableColumnModel and appear in JTableHeader, but TableModel is unchanged from its initial state

  • UIManager.put("CheckBoxMenuItem.doNotCloseOnMouseClick", true) in Java 9 or higher; If set to and the currently selected TableColumn is hidden with JCheckBoxMenuItem while JPopupMenu is open, an ArrayIndexOutOfBoundsException will occur
    • Add PopupMenuListener to JPopupMenu or override the JPopupMenu#show(...) method to JTableHeader.setDraggedColumn(null) can be avoided by clearing the selection state in the scene view

References

2020/09/30

Creating StatusBar with size grips to resize the JFrame

Code

class ResizeWindowListener extends MouseInputAdapter {
  private final Rectangle rect = new Rectangle();
  private final Point startPt = new Point();

  @Override public void mousePressed(MouseEvent e) {
    Component p = SwingUtilities.getRoot(e.getComponent());
    if (p instanceof Window) {
      startPt.setLocation(e.getPoint());
      rect.setBounds(p.getBounds());
    }
  }

  @Override public void mouseDragged(MouseEvent e) {
    Component p = SwingUtilities.getRoot(e.getComponent());
    if (!rect.isEmpty() && p instanceof Window) {
      Point pt = e.getPoint();
      rect.width += pt.x - startPt.x;
      rect.height += pt.y - startPt.y;
      p.setBounds(rect);
    }
  }
}

// Size Grip
// Create a GripIcon with a JLabel and position it at the right end(BorderLayout.EAST) of the StatusBar
class BottomRightCornerLabel extends JLabel {
  private transient MouseInputListener handler;

  protected BottomRightCornerLabel() {
    super(new BottomRightCornerIcon());
  }

  @Override public void updateUI() {
    removeMouseListener(handler);
    removeMouseMotionListener(handler);
    super.updateUI();
    handler = new ResizeWindowListener();
    addMouseListener(handler);
    addMouseMotionListener(handler);
    setCursor(Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR));
  }
}

// Grip Icon
// Draw six rectangles to create a Windows 10 like icon
class BottomRightCornerIcon implements Icon {
  private static final Color SQUARE_COLOR = new Color(160, 160, 160, 160);

  @Override public void paintIcon(Component c, Graphics g, int x, int y) {
    int diff = 3;
    Graphics2D g2 = (Graphics2D) g.create();
    g2.translate(getIconWidth() - diff * 3 - 1, getIconHeight() - diff * 3 - 1);

    int firstRow = 0;
    int secondRow = firstRow + diff;
    int thirdRow = secondRow + diff;

    int firstColumn = 0;
    drawSquare(g2, firstColumn, thirdRow);

    int secondColumn = firstColumn + diff;
    drawSquare(g2, secondColumn, secondRow);
    drawSquare(g2, secondColumn, thirdRow);

    int thirdColumn = secondColumn + diff;
    drawSquare(g2, thirdColumn, firstRow);
    drawSquare(g2, thirdColumn, secondRow);
    drawSquare(g2, thirdColumn, thirdRow);

    g2.dispose();
  }

  @Override public int getIconWidth() {
    return 16;
  }

  @Override public int getIconHeight() {
    return 20;
  }

  private void drawSquare(Graphics g, int x, int y) {
    g.setColor(SQUARE_COLOR);
    g.fillRect(x, y, 2, 2);
  }
}

References

2020/08/31

Overlap the JLabel with a ribbon and slanted string in the corner

Code

class BadgeLabel extends JLabel {
  private final Color ribbonColor = new Color(0xAA_FF_64_00, true);
  private final String ribbonText;

  protected BadgeLabel(Icon image) {
    super(image);
    this.ribbonText = null;
  }

  protected BadgeLabel(Icon image, String ribbonText) {
    super(image);
    this.ribbonText = ribbonText;
  }

  @Override public void updateUI() {
    super.updateUI();
    setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
    setVerticalAlignment(SwingConstants.CENTER);
    setVerticalTextPosition(SwingConstants.BOTTOM);
    setHorizontalAlignment(SwingConstants.CENTER);
    setHorizontalTextPosition(SwingConstants.CENTER);
  }

  @Override protected void paintComponent(Graphics g) {
    Graphics2D g2 = (Graphics2D) g.create();
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                        RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setPaint(Color.WHITE);
    g2.fill(getShape());
    super.paintComponent(g);

    if (ribbonText != null) {
      Dimension d = getSize();
      float fontSize = 10f;
      int cx = (d.width - (int) fontSize) / 2;
      double theta = Math.toRadians(45d);

      Font font = g2.getFont().deriveFont(fontSize);
      g2.setFont(font);
      FontRenderContext frc = new FontRenderContext(null, true, true);

      Shape ribbon = new Rectangle2D.Double(cx, -fontSize, d.width, fontSize);
      AffineTransform at = AffineTransform.getRotateInstance(theta, cx, 0);
      g2.setPaint(ribbonColor);
      g2.fill(at.createTransformedShape(ribbon));

      TextLayout tl = new TextLayout(ribbonText, font, frc);
      g2.setPaint(Color.WHITE);
      Rectangle2D r = tl.getOutline(null).getBounds2D();
      double dx = cx + (d.width - cx) / Math.sqrt(2d) - r.getWidth() / 2d;
      double dy = fontSize / 2d + r.getY();
      AffineTransform tx = AffineTransform.getTranslateInstance(dx, dy);
      Shape s = tl.getOutline(tx);
      g2.fill(at.createTransformedShape(s));
    }
    g2.dispose();
  }

  @Override public boolean isOpaque() {
    return false;
  }

  protected Shape getShape() {
    Dimension d = getSize();
    double r = d.width / 2d;
    return new RoundRectangle2D.Double(
        0d, 0d, d.width - 1d, d.height - 1d, r, r);
  }
}

Explanation

JLabel
  • Override JLabel#isOpaque() to make it transparent
  • Overrides JLabel#paintComponent(...) to draw a round rectangle for the background, then the original icon for `JLabel`, then the ribbon in the upper right corner, then the ribbon string
Ribbon
  • Create a Rectangle for the ribbon so that the lower left of the ribbon is positioned near the middle of the x axis of the parent JLabel
  • Rotate the lower left corner of the ribbon rectangle 45 degrees(Math.toRadians(45d)) about the origin
Ribbon string
  • Convert the Ribbon string to Shape using the TextLayout.getOutline(...) method
  • Rotate this Shape 45 degrees from its bottom left

References

2020/07/31

Show a badge using JLayer for icon in a JLabel

Code

class BadgeLayerUI extends LayerUI {
  private static final int BADGE_SIZE = 17;
  private static final Point OFFSET = new Point(6, 2);
  private final Rectangle viewRect = new Rectangle();
  private final Rectangle iconRect = new Rectangle();
  private final Rectangle textRect = new Rectangle();

  @Override public void paint(Graphics g, JComponent c) {
    super.paint(g, c);
    if (c instanceof JLayer) {
      Graphics2D g2 = (Graphics2D) g.create();
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      iconRect.setBounds(0, 0, 0, 0);
      textRect.setBounds(0, 0, 0, 0);
      BadgeLabel label = (BadgeLabel) ((JLayer<?>) c).getView();
      SwingUtilities.calculateInnerArea(label, viewRect);
      SwingUtilities.layoutCompoundLabel(
          label,
          label.getFontMetrics(label.getFont()),
          label.getText(),
          label.getIcon(),
          label.getVerticalAlignment(),
          label.getHorizontalAlignment(),
          label.getVerticalTextPosition(),
          label.getHorizontalTextPosition(),
          viewRect,
          iconRect,
          textRect,
          label.getIconTextGap()
      );

      int x = iconRect.x + iconRect.width - BADGE_SIZE + OFFSET.x;
      int y = iconRect.y + iconRect.height - BADGE_SIZE + OFFSET.y;
      g2.translate(x, y);
      Icon badge = new BadgeIcon(label.getCounter(), Color.WHITE, new Color(0xAA_32_16_16, true));
      badge.paintIcon(label, g2, 0, 0);
      g2.dispose();
    }
  }
}

class BadgeIcon implements Icon {
  private final Color badgeBgc;
  private final Color badgeFgc;
  private final int value;

  protected BadgeIcon(int value, Color fgc, Color bgc) {
    this.value = value;
    this.badgeFgc = fgc;
    this.badgeBgc = bgc;
  }

  @Override public void paintIcon(Component c, Graphics g, int x, int y) {
    if (value <= 0) {
      return;
    }
    int w = getIconWidth();
    int h = getIconHeight();
    Graphics2D g2 = (Graphics2D) g.create();
    g2.translate(x, y);
    RoundRectangle2D badge = new RoundRectangle2D.Double(0, 0, w, h, 6, 6);
    g2.setPaint(badgeBgc);
    g2.fill(badge);
    g2.setPaint(badgeBgc.darker());
    g2.draw(badge);

    g2.setPaint(badgeFgc);
    FontRenderContext frc = g2.getFontRenderContext();
    // Java 12:
    // NumberFormat fmt = NumberFormat.getCompactNumberInstance(
    //     Locale.US, NumberFormat.Style.SHORT);
    // String txt = fmt.format(value);
    String txt = value > 999 ? "1K" : Objects.toString(value);
    AffineTransform at = txt.length() < 3 ? null : AffineTransform.getScaleInstance(.66, 1d);
    Shape shape = new TextLayout(txt, g2.getFont(), frc).getOutline(at);
    Rectangle2D b = shape.getBounds();
    Point2D p = new Point2D.Double(
        b.getX() + b.getWidth() / 2d, b.getY() + b.getHeight() / 2d);
    AffineTransform toCenterAT = AffineTransform.getTranslateInstance(
        w / 2d - p.getX(), h / 2d - p.getY());
    g2.fill(toCenterAT.createTransformedShape(shape));
    g2.dispose();
  }

  @Override public int getIconWidth() {
    return 17;
  }

  @Override public int getIconHeight() {
    return 17;
  }
}

Explanation

  • Set JLabel to JLayer to display Badge near the specified corner of the Icon area inside the JLabel body
    • Icon area inside JLabel can be retrieved with SwingUtilities.layoutCompoundLabel(...) method
    • Show 6px and Badge offset in x axis and 2px offset in y axis, so need to set more margin in JLabel
    • If you also want to display text in JLabel, the text may overlap with Badge if IconTextGap is not set considering the above offset
  • Icons for Badge created with a fixed size of 17x17 using RoundRectangle2D or Ellipse2D
    • Suppress Badge if value is 0
    • Set to 1K for all numbers with more than 4 digits
    • If the number to be displayed is 3 digits, apply a transformation of 66% to set the length.

References