Google Tag Manager

2021/10/01

Show a horizontal JScrollBar in the tab area of a JTabbedPane-like component created with CardLayout

Code

class CardLayoutTabbedPane extends JPanel {
  private final CardLayout cardLayout = new CardLayout();
  private final JPanel tabPanel = new JPanel(new FlowLayout(FlowLayout.LEADING, 0, 0));
  private final JPanel contentsPanel = new JPanel(cardLayout);
  private final JButton hiddenTabs = new JButton("V");
  private final ButtonGroup group = new ButtonGroup();
  private final JScrollPane tabArea = new JScrollPane(tabPanel) {
    @Override public boolean isOptimizedDrawingEnabled() {
      return false; // JScrollBar is overlap
    }

    @Override public void updateUI() {
      super.updateUI();
      EventQueue.invokeLater(() -> {
        getVerticalScrollBar().setUI(new OverlappedScrollBarUI());
        getHorizontalScrollBar().setUI(new OverlappedScrollBarUI());
        setLayout(new OverlapScrollPaneLayout());
        setComponentZOrder(getVerticalScrollBar(), 0);
        setComponentZOrder(getHorizontalScrollBar(), 1);
        setComponentZOrder(getViewport(), 2);
      });
      setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
      setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
      getVerticalScrollBar().setOpaque(false);
      getHorizontalScrollBar().setOpaque(false);
      setBackground(Color.DARK_GRAY);
      setViewportBorder(BorderFactory.createEmptyBorder());
      setBorder(BorderFactory.createEmptyBorder());
    }

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

  protected CardLayoutTabbedPane() {
    super(new BorderLayout());
    setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1));
    setBackground(new Color(16, 16, 16));
    tabPanel.setInheritsPopupMenu(true);
    hiddenTabs.setFont(hiddenTabs.getFont().deriveFont(8f));
    hiddenTabs.setBorder(BorderFactory.createEmptyBorder(2, 8, 2, 8));
    hiddenTabs.setOpaque(false);
    hiddenTabs.setFocusable(false);
    hiddenTabs.setContentAreaFilled(false);
    JPanel header = new JPanel(new BorderLayout());
    header.add(new JLayer<>(tabArea, new HorizontalScrollLayerUI()));
    header.add(hiddenTabs, BorderLayout.EAST);
    add(header, BorderLayout.NORTH);
    add(contentsPanel);
  }

  protected JComponent createTabComponent(String title, Icon icon) {
    JToggleButton tab = new TabButton();
    tab.setInheritsPopupMenu(true);
    group.add(tab);
    tab.addMouseListener(new MouseAdapter() {
      @Override public void mousePressed(MouseEvent e) {
        if (SwingUtilities.isLeftMouseButton(e)) {
          ((AbstractButton) e.getComponent()).setSelected(true);
          cardLayout.show(contentsPanel, title);
        }
      }
    });
    EventQueue.invokeLater(() -> tab.setSelected(true));

    JLabel label = new JLabel(title, icon, SwingConstants.LEADING);
    label.setForeground(Color.WHITE);
    label.setIcon(icon);
    label.setOpaque(false);

    JButton close = new JButton(new CloseTabIcon(new Color(0xB0_B0_B0))) {
      @Override public Dimension getPreferredSize() {
        return new Dimension(12, 12);
      }
    };
    close.addActionListener(e -> System.out.println("close button"));
    close.setBorder(BorderFactory.createEmptyBorder());
    close.setFocusable(false);
    close.setOpaque(false);
    // close.setFocusPainted(false);
    close.setContentAreaFilled(false);
    close.setPressedIcon(new CloseTabIcon(new Color(0xFE_FE_FE)));
    close.setRolloverIcon(new CloseTabIcon(new Color(0xA0_A0_A0)));

    tab.add(label);
    tab.add(close, BorderLayout.EAST);
    return tab;
  }

  public void addTab(String title, Icon icon, Component comp) {
    JComponent tab = createTabComponent(title, icon);
    tabPanel.add(tab);
    contentsPanel.add(comp, title);
    cardLayout.show(contentsPanel, title);
    EventQueue.invokeLater(() -> tabPanel.scrollRectToVisible(tab.getBounds()));
  }

  public JScrollPane getTabArea() {
    return tabArea;
  }

  @Override public void doLayout() {
    BoundedRangeModel m = tabArea.getHorizontalScrollBar().getModel();
    hiddenTabs.setVisible(m.getMaximum() - m.getExtent() > 0);
    super.doLayout();
  }
}

References

2021/08/31

Add a JButton to the bottom right inside the JScrollPane to scroll back to the top area of the child component

Code

class ScrollBackToTopLayerUI extends LayerUI<JScrollPane> {
  private static final int GAP = 5;
  private final Container rubberStamp = new JPanel();
  private final Point mousePt = new Point();
  private final JButton button = new JButton(new ScrollBackToTopIcon()) {
    @Override public void updateUI() {
      super.updateUI();
      setBorder(BorderFactory.createEmptyBorder());
      setFocusPainted(false);
      setBorderPainted(false);
      setContentAreaFilled(false);
      setRolloverEnabled(false);
    }
  };
  private final Rectangle buttonRect = new Rectangle(button.getPreferredSize());

  private void updateButtonRect(JScrollPane scroll) {
    JViewport viewport = scroll.getViewport();
    int x = viewport.getX() + viewport.getWidth() - buttonRect.width - GAP;
    int y = viewport.getY() + viewport.getHeight() - buttonRect.height - GAP;
    buttonRect.setLocation(x, y);
  }

  @Override public void paint(Graphics g, JComponent c) {
    super.paint(g, c);
    if (c instanceof JLayer) {
      JScrollPane scroll = (JScrollPane) ((JLayer<?>) c).getView();
      updateButtonRect(scroll);
      if (scroll.getViewport().getViewRect().y > 0) {
        button.getModel().setRollover(buttonRect.contains(mousePt));
        SwingUtilities.paintComponent(g, button, rubberStamp, buttonRect);
      }
    }
  }

  @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) {
    JScrollPane scroll = l.getView();
    Rectangle r = scroll.getViewport().getViewRect();
    Point p = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), scroll);
    mousePt.setLocation(p);
    int id = e.getID();
    if (id == MouseEvent.MOUSE_CLICKED) {
      if (buttonRect.contains(mousePt)) {
        scrollBackToTop(l.getView());
      }
    } else if (id == MouseEvent.MOUSE_PRESSED && r.y > 0 && buttonRect.contains(mousePt)) {
      e.consume();
    }
  }

  @Override protected void processMouseMotionEvent(MouseEvent e, JLayer<? extends JScrollPane> l) {
    Point p = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), l.getView());
    mousePt.setLocation(p);
    l.repaint(buttonRect);
  }

  private void scrollBackToTop(JScrollPane scroll) {
    JComponent c = (JComponent) scroll.getViewport().getView();
    Rectangle current = scroll.getViewport().getViewRect();
    new Timer(20, e -> {
      Timer animator = (Timer) e.getSource();
      if (0 < current.y && animator.isRunning()) {
        current.y -= Math.max(1, current.y / 2);
        c.scrollRectToVisible(current);
      } else {
        animator.stop();
      }
    }).start();
  }
}

References

2021/07/31

Scale the check icon of JCheckBox

Code

class ScaledIcon implements Icon {
  private final Icon icon;
  private final int width;
  private final int height;

  protected ScaledIcon(Icon icon, int width, int height) {
    this.icon = icon;
    this.width = width;
    this.height = height;
  }

  @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);
    g2.translate(x, y);
    double sx = width / (double) icon.getIconWidth();
    double sy = height / (double) icon.getIconHeight();
    g2.scale(sx, sy);
    icon.paintIcon(c, g2, 0, 0);
    g2.dispose();
  }

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

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

class CheckBoxIcon implements Icon {
  @Override public void paintIcon(Component c, Graphics g, int x, int y) {
    if (!(c instanceof AbstractButton)) {
      return;
    }
    Graphics2D g2 = (Graphics2D) g.create();
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                        RenderingHints.VALUE_ANTIALIAS_ON);
    g2.translate(x, y);
    g2.setPaint(Color.DARK_GRAY);
    float s = Math.min(getIconWidth(), getIconHeight()) * .05f;
    float w = getIconWidth() - s - s;
    float h = getIconHeight() - s - s;
    float gw = w / 8f;
    float gh = h / 8f;
    g2.setStroke(new BasicStroke(s));
    g2.draw(new Rectangle2D.Float(s, s, w, h));
    AbstractButton b = (AbstractButton) c;
    if (b.getModel().isSelected()) {
      g2.setStroke(new BasicStroke(3f * s));
      Path2D p = new Path2D.Float();
      p.moveTo(x + 2f * gw, y + .5f * h);
      p.lineTo(x + .4f * w, y + h - 2f * gh);
      p.lineTo(x + w - 2f * gw, y + 2f * gh);
      g2.draw(p);
    }
    g2.dispose();
  }

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

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

//...
JTable table = new JTable(model) {
  private final Insets iconIns = new Insets(4, 4, 4, 4);
  private final transient Icon checkIcon = new CheckBoxIcon();

  @Override public Component prepareRenderer(
        TableCellRenderer renderer, int row, int column) {
    Component c = super.prepareRenderer(renderer, row, column);
    if (c instanceof JCheckBox) {
      int s = getRowHeight(row) - iconIns.top - iconIns.bottom;
      JCheckBox cb = (JCheckBox) c;
      cb.setIcon(new ScaledIcon(checkIcon, s, s));
      cb.setBorderPainted(false);
    }
    return c;
  }

  @Override public Component prepareEditor(
        TableCellEditor editor, int row, int column) {
    Component c = super.prepareEditor(editor, row, column);
    if (c instanceof JCheckBox) {
      int s = getRowHeight(row) - iconIns.top - iconIns.bottom;
      JCheckBox cb = (JCheckBox) c;
      cb.setIcon(new ScaledIcon(checkIcon, s, s));
      cb.setBackground(getSelectionBackground());
    }
    return c;
  }
};
table.setRowHeight(40);
table.setSelectionBackground(Color.WHITE);

References

2021/06/30

Round the corners of JTableHeader

Code

class RoundedHeaderRenderer extends DefaultTableCellRenderer {
  private final JLabel firstLabel = new JLabel() {
    @Override public void paintComponent(Graphics g) {
      Graphics2D g2 = (Graphics2D) g.create();
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      g2.setPaint(getBackground());
      float r = 8f;
      float x = 0f;
      float y = 0f;
      float w = getWidth();
      float h = getHeight();
      Path2D p = new Path2D.Float();
      p.moveTo(x, y + r);
      p.quadTo(x, y, x + r, y);
      p.lineTo(x + w, y);
      p.lineTo(x + w, y + h);
      p.lineTo(x + r, y + h);
      p.quadTo(x, y + h, x, y + h - r);
      p.closePath();
      g2.fill(p);
      g2.dispose();
      super.paintComponent(g);
    }
  };
  private final JLabel lastLabel = new JLabel() {
    @Override public void paintComponent(Graphics g) {
      Graphics2D g2 = (Graphics2D) g.create();
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      g2.setPaint(getBackground());
      float r = 8f;
      float x = 0f;
      float y = 0f;
      float w = getWidth();
      float h = getHeight();
      Path2D p = new Path2D.Float();
      p.moveTo(x, y);
      p.lineTo(x + w - r, y);
      p.quadTo(x + w, y, x + w, y + r);
      p.lineTo(x + w, y + h - r);
      p.quadTo(x + w, y + h, x + w - r, y + h);
      p.lineTo(x, y + h);
      p.closePath();
      g2.fill(p);
      g2.dispose();
      super.paintComponent(g);
    }
  };

  public RoundedHeaderRenderer() {
    super();
    firstLabel.setOpaque(false);
    lastLabel.setOpaque(false);
    setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
    firstLabel.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
    lastLabel.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
  }

  @Override public Component getTableCellRendererComponent(
        JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
    JLabel l = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
    if (column == 0) {
      l = firstLabel;
    } else if (column == table.getColumnCount() - 1) {
      l = lastLabel;
    }
    l.setFont(table.getFont());
    l.setText(value.toString());
    l.setForeground(table.getTableHeader().getForeground());
    l.setBackground(table.getTableHeader().getBackground());
    l.setHorizontalAlignment(SwingConstants.CENTER);
    return l;
  }
}

References

2021/05/31

Drawing Analog Clock Hands in JPanel using Timer

Code

class AnalogClock extends JPanel {
  protected LocalTime time = LocalTime.now(ZoneId.systemDefault());
  protected Timer timer = new Timer(200, e -> {
    time = LocalTime.now(ZoneId.systemDefault());
    repaint();
  });
  private transient HierarchyListener listener;

  @Override public void updateUI() {
    removeHierarchyListener(listener);
    super.updateUI();
    listener = e -> {
      if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) {
        if (e.getComponent().isShowing()) {
          // System.out.println("start");
          timer.start();
        } else {
          // System.out.println("stop");
          timer.stop();
        }
      }
    };
    addHierarchyListener(listener);
  }

  @Override protected void paintComponent(Graphics g) {
    Graphics2D g2 = (Graphics2D) g.create();
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    Rectangle rect = SwingUtilities.calculateInnerArea(this, null);
    g2.setColor(Color.BLACK);
    g2.fill(rect);
    float radius = Math.min(rect.width, rect.height) / 2f - 10f;
    g2.translate(rect.getCenterX(), rect.getCenterY());

    // Drawing the hour and minute markers
    float hourMarkerLen = radius / 6f - 10f;
    Shape hourMarker = new Line2D.Float(0f, hourMarkerLen - radius, 0f, -radius);
    Shape minuteMarker = new Line2D.Float(0f, hourMarkerLen / 2f - radius, 0f, -radius);
    AffineTransform at = AffineTransform.getRotateInstance(0d);
    g2.setStroke(new BasicStroke(2f));
    g2.setColor(Color.WHITE);
    for (int i = 0; i < 60; i++) {
      if (i % 5 == 0) {
        g2.draw(at.createTransformedShape(hourMarker));
      } else {
        g2.draw(at.createTransformedShape(minuteMarker));
      }
      at.rotate(Math.PI / 30d);
    }

    // Drawing the hour hand
    float hourHandLen = radius / 2f;
    Shape hourHand = new Line2D.Float(0f, 0f, 0f, -hourHandLen);
    double minuteRot = time.getMinute() * Math.PI / 30d;
    double hourRot = time.getHour() * Math.PI / 6d + minuteRot / 12d;
    g2.setStroke(new BasicStroke(8f));
    g2.setPaint(Color.LIGHT_GRAY);
    g2.draw(AffineTransform.getRotateInstance(hourRot).createTransformedShape(hourHand));

    // Drawing the minute hand
    float minuteHandLen = 5f * radius / 6f;
    Shape minuteHand = new Line2D.Float(0f, 0f, 0f, -minuteHandLen);
    g2.setStroke(new BasicStroke(4f));
    g2.setPaint(Color.WHITE);
    g2.draw(AffineTransform.getRotateInstance(minuteRot).createTransformedShape(minuteHand));

    // Drawing the second hand
    float r = radius / 6f;
    float secondHandLen = radius - r;
    Shape secondHand = new Line2D.Float(0f, r, 0f, -secondHandLen);
    double secondRot = time.getSecond() * Math.PI / 30d;
    g2.setPaint(Color.RED);
    g2.setStroke(new BasicStroke(1f));
    g2.draw(AffineTransform.getRotateInstance(secondRot).createTransformedShape(secondHand));
    g2.fill(new Ellipse2D.Float(-r / 4f, -r / 4f, r / 2f, r / 2f));

    g2.dispose();
  }
}

References