Google Tag Manager

2021/03/31

Add header and footer in the JComboBox dropdown list

Add the header created by JLabel and the clickable footer created by JMenuItem in the drop-down list of JComboBox

Code

class HeaderFooterComboPopup extends BasicComboPopup {
  protected transient JLabel header;
  protected transient JMenuItem footer;

  public HeaderFooterComboPopup(JComboBox combo) {
    super(combo);
  }

  @Override protected void configurePopup() {
    super.configurePopup();
    configureHeader();
    configureFooter();
    add(header, 0);
    add(footer);
  }

  protected void configureHeader() {
    header = new JLabel("History");
    header.setBorder(BorderFactory.createEmptyBorder(4, 5, 4, 0));
    header.setMaximumSize(new Dimension(Short.MAX_VALUE, 20));
    header.setAlignmentX(1f);
  }

  protected void configureFooter() {
    int modifiers = InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK;
    footer = new JMenuItem("Show All Bookmarks");
    footer.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_B, modifiers));
    footer.addActionListener(e -> {
      Window w = SwingUtilities.getWindowAncestor(getInvoker());
      JOptionPane.showMessageDialog(w, "Bookmarks");
    });
  }
}

References

2021/02/28

Create a JList heavyweight cell editor with a centered, fixed width, and line wrapping editor

Code

class EditableList<E extends ListItem> extends JList<E> {
  public static final String RENAME = "rename-title";
  public static final String CANCEL = "cancel-editing";
  public static final String EDITING = "start-editing";
  protected int editingIndex = -1;
  protected int editorWidth = -1;
  private transient MouseAdapter handler;
  // protected final Container glassPane = new EditorGlassPane(); // LightWeightEditor
  protected Window window; // HeavyWeightEditor
  protected final JTextPane editor = new JTextPane() {
    @Override public Dimension getPreferredSize() {
      Dimension d = super.getPreferredSize();
      d.width = editorWidth;
      return d;
    }
  };
  protected final Action startEditing = new AbstractAction() {
    @Override public void actionPerformed(ActionEvent e) {
      // getRootPane().setGlassPane(glassPane);
      int idx = getSelectedIndex();
      editingIndex = idx;
      Rectangle rect = getCellBounds(idx, idx);
      // Point p = SwingUtilities.convertPoint(EditableList.this, rect.getLocation(), glassPane);
      // rect.setLocation(p);
      editorWidth = rect.width;
      editor.setText(getSelectedValue().title);
      int rowHeight = editor.getFontMetrics(editor.getFont()).getHeight();
      rect.y += rect.height - rowHeight - 2 - 1;
      rect.height = editor.getPreferredSize().height;
      editor.setBounds(rect);
      editor.selectAll();
      // glassPane.add(editor);
      // glassPane.setVisible(true);
      Point p = new Point(rect.getLocation());
      SwingUtilities.convertPointToScreen(p, EditableList.this);
      if (window == null) {
        window = new JWindow(SwingUtilities.getWindowAncestor(EditableList.this));
        window.setFocusableWindowState(true);
        window.setModalExclusionType(Dialog.ModalExclusionType.APPLICATION_EXCLUDE);
        // window.setAlwaysOnTop(true);
        window.add(editor);
      }
      window.setLocation(p);
      window.pack();
      window.setVisible(true);
      editor.requestFocusInWindow();
    }
  };
  protected final Action cancelEditing = new AbstractAction() {
    @Override public void actionPerformed(ActionEvent e) {
      // glassPane.setVisible(false);
      window.setVisible(false);
      editingIndex = -1;
    }
  };
  protected final Action renameTitle = new AbstractAction() {
    @Override public void actionPerformed(ActionEvent e) {
      ListModel<E> m = getModel();
      String title = editor.getText().trim();
      int index = editingIndex; // getSelectedIndex();
      if (!title.isEmpty() && index >= 0 && m instanceof DefaultListModel<?>) {
        @SuppressWarnings("unchecked")
        DefaultListModel<ListItem> model = (DefaultListModel<ListItem>) getModel();
        ListItem item = m.getElementAt(index);
        model.remove(index);
        model.add(index, new ListItem(editor.getText().trim(), item.icon));
        setSelectedIndex(index); // 1. Both must be run
        EventQueue.invokeLater(() -> setSelectedIndex(index)); // 2. Both must be run
      }
      // glassPane.setVisible(false);
      window.setVisible(false);
      editingIndex = -1;
    }
  };

  protected EditableList(DefaultListModel<E> model) {
    super(model);
    editor.setBorder(BorderFactory.createLineBorder(Color.GRAY));
    editor.setEditorKit(new WrapEditorKit());
    editor.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE);
    editor.setFont(UIManager.getFont("TextField.font"));
    // editor.setHorizontalAlignment(SwingConstants.CENTER); // JTextField
    // editor.setLineWrap(true); // JTextArea
    StyledDocument doc = editor.getStyledDocument();
    SimpleAttributeSet center = new SimpleAttributeSet();
    StyleConstants.setAlignment(center, StyleConstants.ALIGN_CENTER);
    doc.setParagraphAttributes(0, doc.getLength(), center, false);
    editor.setComponentPopupMenu(new TextComponentPopupMenu());
    editor.getDocument().addDocumentListener(new DocumentListener() {
      private int prev = -1;
      private void update() {
        EventQueue.invokeLater(() -> {
          int h = editor.getPreferredSize().height;
          if (prev != h) {
            Rectangle rect = editor.getBounds();
            rect.height = h;
            editor.setBounds(rect);
            window.pack();
            editor.requestFocusInWindow();
          }
          prev = h;
        });
      }

      @Override public void insertUpdate(DocumentEvent e) {
        update();
      }

      @Override public void removeUpdate(DocumentEvent e) {
        update();
      }

      @Override public void changedUpdate(DocumentEvent e) {
        update();
      }
    });

    InputMap im = editor.getInputMap(JComponent.WHEN_FOCUSED);
    im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), RENAME);
    im.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), RENAME);
    im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), CANCEL);

    ActionMap am = editor.getActionMap();
    am.put(RENAME, renameTitle);
    am.put(CANCEL, cancelEditing);

    getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), EDITING);
    getActionMap().put(EDITING, startEditing);
  }

  @Override public void updateUI() {
    removeMouseListener(handler);
    removeMouseMotionListener(handler);
    setSelectionForeground(null);
    setSelectionBackground(null);
    setCellRenderer(null);
    super.updateUI();
    setLayoutOrientation(JList.HORIZONTAL_WRAP);
    getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
    setVisibleRowCount(0);
    setFixedCellWidth(72);
    setFixedCellHeight(64);
    setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
    setCellRenderer(new ListItemListCellRenderer<>());
    handler = new EditingHandler();
    addMouseListener(handler);
    addMouseMotionListener(handler);
  }

  class EditingHandler extends MouseAdapter {
    private boolean startOutside;

    @Override public void mouseClicked(MouseEvent e) {
      int idx = getSelectedIndex();
      Rectangle rect = getCellBounds(idx, idx);
      if (rect == null) {
        return;
      }
      int h = editor.getPreferredSize().height;
      rect.y = rect.y + rect.height - h - 2 - 1;
      rect.height = h;
      boolean isDoubleClick = e.getClickCount() >= 2;
      if (isDoubleClick && rect.contains(e.getPoint())) {
        startEditing.actionPerformed(new ActionEvent(e.getComponent(), ActionEvent.ACTION_PERFORMED, ""));
      }
    }

    @Override public void mousePressed(MouseEvent e) {
      JList<?> list = (JList<?>) e.getComponent();
      startOutside = !contains(list, e.getPoint());
      if (window != null && window.isVisible() && editingIndex >= 0) {
        renameTitle.actionPerformed(new ActionEvent(editor, ActionEvent.ACTION_PERFORMED, ""));
      } else if (startOutside) {
        clearSelectionAndFocus(list);
      }
    }

    @Override public void mouseReleased(MouseEvent e) {
      startOutside = false;
    }

    @Override public void mouseDragged(MouseEvent e) {
      JList<?> list = (JList<?>) e.getComponent();
      if (contains(list, e.getPoint())) {
        startOutside = false;
      } else if (startOutside) {
        clearSelectionAndFocus(list);
      }
    }

    private void clearSelectionAndFocus(JList<?> list) {
      list.clearSelection();
      list.getSelectionModel().setAnchorSelectionIndex(-1);
      list.getSelectionModel().setLeadSelectionIndex(-1);
    }

    private boolean contains(JList<?> list, Point pt) {
      for (int i = 0; i < list.getModel().getSize(); i++) {
        if (list.getCellBounds(i, i).contains(pt)) {
          return true;
        }
      }
      return false;
    }
  }
}
class WrapLabelView extends LabelView {
  protected WrapLabelView(Element element) {
    super(element);
  }

  @Override public float getMinimumSpan(int axis) {
    switch (axis) {
      case View.X_AXIS:
        return 0;
      case View.Y_AXIS:
        return super.getMinimumSpan(axis);
      default:
        throw new IllegalArgumentException("Invalid axis: " + axis);
    }
  }
}

References

2021/01/31

Draw the upper right, left, and right borders sequentially when JTextField gets focus

Code

class AnimatedBorder extends EmptyBorder {
  private static final int BOTTOM_SPACE = 20;
  private static final double PLAY_TIME = 300d;
  private static final int BORDER = 4;
  private final Timer animator = new Timer(10, null);
  private final transient Stroke stroke = new BasicStroke(BORDER);
  private final transient Stroke bottomStroke = new BasicStroke(BORDER / 2f);
  private long startTime = -1L;
  private final transient List points = new ArrayList<>();
  private transient Shape shape;

  public AnimatedBorder(JComponent c) {
    super(BORDER, BORDER, BORDER + BOTTOM_SPACE, BORDER);
    animator.addActionListener(e -> {
      if (startTime < 0) {
        startTime = System.currentTimeMillis();
      }
      long playTime = System.currentTimeMillis() - startTime;
      double progress = playTime / PLAY_TIME;
      boolean stop = progress > 1d || points.isEmpty();
      if (stop) {
        startTime = -1L;
        ((Timer) e.getSource()).stop();
        c.repaint();
        return;
      }
      Point2D pos = new Point2D.Double();
      pos.setLocation(points.get(0));
      Path2D border = new Path2D.Double();
      border.moveTo(pos.getX(), pos.getY());
      int idx = Math.min(Math.max(0, (int) (points.size() * progress)), points.size() - 1);
      for (int i = 0; i <= idx; i++) {
        pos.setLocation(points.get(i));
        border.lineTo(pos.getX(), pos.getY());
        border.moveTo(pos.getX(), pos.getY());
      }
      border.closePath();
      shape = border;
      c.repaint();
    });
    c.addFocusListener(new FocusListener() {
      @Override public void focusGained(FocusEvent e) {
        Rectangle r = c.getBounds();
        r.height -= BOTTOM_SPACE + 1;
        Path2D p = new Path2D.Double();
        p.moveTo(r.getWidth(), r.getHeight());
        p.lineTo(r.getWidth(), 0d);
        p.lineTo(0d, 0d);
        p.lineTo(0d, r.getHeight());
        p.closePath();
        makePointList(p, points);
        animator.start();
      }

      @Override public void focusLost(FocusEvent e) {
        points.clear();
        shape = null;
        c.repaint();
      }
    });
  }

  @Override public void paintBorder(Component c, Graphics g, int x, int y, int w, int h) {
    super.paintBorder(c, g, x, y, w, h);
    Graphics2D g2 = (Graphics2D) g.create();
    g2.setPaint(c.isEnabled() ? Color.ORANGE : Color.GRAY);
    g2.translate(x, y);
    g2.setStroke(bottomStroke);
    g2.drawLine(0, h - BOTTOM_SPACE, w - 1, h - BOTTOM_SPACE);
    g2.setStroke(stroke);
    if (shape != null) {
      g2.draw(shape);
    }
    g2.dispose();
  }

  private static void makePointList(Shape shape, List points) {
    points.clear();
    PathIterator pi = shape.getPathIterator(null, .01);
    Point2D prev = new Point2D.Double();
    double delta = .02;
    double threshold = 2d;
    double[] coords = new double[6];
    while (!pi.isDone()) {
      int segment = pi.currentSegment(coords);
      Point2D current = createPoint(coords[0], coords[1]);
      if (segment == PathIterator.SEG_MOVETO) {
        points.add(current);
        prev.setLocation(current);
      } else if (segment == PathIterator.SEG_LINETO) {
        double distance = prev.distance(current);
        double fraction = delta;
        if (distance > threshold) {
          Point2D p = interpolate(prev, current, fraction);
          while (distance > prev.distance(p)) {
            points.add(p);
            fraction += delta;
            p = interpolate(prev, current, fraction);
          }
        } else {
          points.add(current);
        }
        prev.setLocation(current);
      }
      pi.next();
    }
  }

  private static Point2D createPoint(double x, double y) {
    return new Point2D.Double(x, y);
  }

  private static Point2D interpolate(Point2D start, Point2D end, double fraction) {
    double dx = end.getX() - start.getX();
    double dy = end.getY() - start.getY();
    double nx = start.getX() + dx * fraction;
    double ny = start.getY() + dy * fraction;
    return new Point2D.Double(nx, ny);
  }
}

References

2020/12/31

Use thumbnail as default Icon for JRadioButton

Code

class SelectedIcon implements Icon {
  private final Icon icon;
  private final Color color;

  protected SelectedIcon(Icon icon, Color color) {
    this.icon = icon;
    this.color = color;
  }

  @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);
    icon.paintIcon(c, g2, 0, 0);
    Path2D triangle = new Path2D.Double();
    triangle.moveTo(getIconWidth(), getIconHeight() / 2d);
    triangle.lineTo(getIconWidth(), getIconHeight());
    triangle.lineTo(getIconWidth() - getIconHeight() / 2d, getIconHeight());
    triangle.closePath();

    g2.setPaint(color);
    g2.fill(triangle);
    g2.setStroke(new BasicStroke(3f));
    g2.drawRect(0, 0, getIconWidth(), getIconHeight());
    g2.setPaint(Color.WHITE);
    Font f = g2.getFont();
    g2.drawString("?", getIconWidth() - f.getSize(), getIconHeight() - 3);
    g2.dispose();
  }

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

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

Explanation

Change the default radio button of JRadioButton to an image thumbnail and the selected state button to an icon with a border drawn on the thumbnail.

References

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