Google Tag Manager

2025/04/30

Switch JEditorPane's StyleSheet to Light or Dark Theme

Code

public static void updateTheme(JEditorPane editor) {
  updateTheme(editor, isDarkMode());
}

public static void updateTheme(JEditorPane editor, boolean isDark) {
  EditorKit kit = editor.getEditorKit();
  HTMLEditorKit htmlEditorKit;
  if (kit instanceof HTMLEditorKit) {
    htmlEditorKit = (HTMLEditorKit) kit;
  } else {
    htmlEditorKit = new HTMLEditorKit();
  }
  if (isDark) {
    htmlEditorKit.setStyleSheet(makeDarkStyleSheet());
    editor.setBackground(new Color(0x1E_1F_22));
  } else {
    htmlEditorKit.setStyleSheet(makeLightStyleSheet());
    editor.setBackground(new Color(0xEE_EE_EE));
  }
  editor.setEditorKit(htmlEditorKit);
}

public static boolean isDarkMode() {
  boolean isDark;
  String os = System.getProperty("os.name").toLowerCase(Locale.ENGLISH);
  if (os.contains("windows")) {
    isDark = isWindowsDarkMode();
  } else if (os.contains("linux")) {
    isDark = isLinuxDarkMode();
  } else if (os.contains("mac")) {
    isDark = isMackDarkMode();
  } else {
    isDark = false;
  }
  return isDark;
}

public static String getProcessOutput(String... cmd)
      throws IOException, InterruptedException {
  ProcessBuilder builder = new ProcessBuilder(cmd);
  builder.redirectErrorStream(true);
  Process p = builder.start();
  String str;
  try (BufferedReader pr = new BufferedReader(
            new InputStreamReader(p.getInputStream()))) {
    str = pr.lines().collect(Collectors.joining(System.lineSeparator()));
  }
  return str;
}

public static boolean isWindowsDarkMode() {
  String[] cmd = {
      "powershell.exe",
      "Get-ItemPropertyValue",
      "-Path",
      "HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
      "-Name",
      "AppsUseLightTheme;"
  };
  boolean isDarkMode;
  try {
    String str = getProcessOutput(cmd).trim();
    isDarkMode = Objects.equals("0", str);
  } catch (IOException | InterruptedException ex) {
    Logger.getGlobal().severe(ex::getMessage);
    isDarkMode = false;
  }
  return isDarkMode;
}

public static boolean isLinuxDarkMode() {
  Toolkit tk = Toolkit.getDefaultToolkit();
  Object theme = tk.getDesktopProperty("gnome.Net/ThemeName");
  return Objects.toString(theme).contains("dark");
}

public static boolean isMackDarkMode() {
  return false;
}

// https://raw.githack.com/google/code-prettify/master/styles/index.html
// Prettify Themes Gallery
public static StyleSheet makeLightStyleSheet() {
  StyleSheet styleSheet = new StyleSheet();
  styleSheet.addRule("pre{background:#eeeeee}");
  styleSheet.addRule(".str{color:#008800}");
  styleSheet.addRule(".kwd{color:#000088}");
  styleSheet.addRule(".com{color:#880000}");
  styleSheet.addRule(".typ{color:#660066}");
  styleSheet.addRule(".lit{color:#006666}");
  styleSheet.addRule(".pun{color:#666600}");
  styleSheet.addRule(".pln{color:#000000}");
  styleSheet.addRule(".tag{color:#000088}");
  styleSheet.addRule(".atn{color:#660066}");
  styleSheet.addRule(".atv{color:#008800}");
  styleSheet.addRule(".dec{color:#660066}");
  return styleSheet;
}

public static StyleSheet makeDarkStyleSheet() {
  StyleSheet styleSheet = new StyleSheet();
  styleSheet.addRule("pre{background:#1e1f22}");
  styleSheet.addRule(".str{color:#ffa0a0}");
  styleSheet.addRule(".kwd{color:#f0e68c;font-weight:bold}");
  styleSheet.addRule(".com{color:#87ceeb}");
  styleSheet.addRule(".typ{color:#98fb98}");
  styleSheet.addRule(".lit{color:#cd5c5c}");
  styleSheet.addRule(".pun{color:#ffffff}");
  styleSheet.addRule(".pln{color:#ffffff}");
  styleSheet.addRule(".tag{color:#f0e68c;font-weight:bold}");
  styleSheet.addRule(".atn{color:#bdb76b;font-weight:bold}");
  styleSheet.addRule(".atv{color:#ffa0a0}");
  styleSheet.addRule(".dec{color:#98fb98}");
  return styleSheet;
}

References

2025/03/31

Rounds all corners of a JTextComponent's selection highlight polygon and fills it with a translucent background color

Code

class RoundedSelectionHighlightPainter extends DefaultHighlightPainter {
  protected RoundedSelectionHighlightPainter() {
    super(null);
  }

  @Override public void paint(
      Graphics g, int offs0, int offs1, Shape bounds, JTextComponent c) {
    Graphics2D g2 = (Graphics2D) g.create();
    g2.setRenderingHint(
      RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    int rgba = c.getSelectionColor().getRGB() & 0xFF_FF_FF | (64 << 24);
    g2.setColor(new Color(rgba, true));
    try {
      Area area = getLinesArea(c, offs0, offs1);
      for (Area a : GeomUtils.singularization(area)) {
        List<Point2D> lst = GeomUtils.convertAreaToPoint2DList(a);
        GeomUtils.flatteningStepsOnRightSide(lst, 3d * 2d);
        g2.fill(GeomUtils.convertRoundedPath(lst, 3d));
      }
    } catch (BadLocationException ex) {
      // can't render
      Logger.getGlobal().severe(ex::getMessage);
    }
    g2.dispose();
  }

  private static Area getLinesArea(JTextComponent c, int offs0, int offs1)
      throws BadLocationException {
    TextUI mapper = c.getUI();
    Area area = new Area();
    int cur = offs0;
    do {
      int startOffset = Utilities.getRowStart(c, cur);
      int endOffset = Utilities.getRowEnd(c, cur);
      Rectangle p0 = mapper.modelToView(c, Math.max(startOffset, offs0));
      Rectangle p1 = mapper.modelToView(c, Math.min(endOffset, offs1));
      if (offs1 > endOffset) {
        p1.width += 6;
      }
      addRectToArea(area, p0.union(p1));
      cur = endOffset + 1;
    } while (cur < offs1);
    return area;
  }

  private static void addRectToArea(Area area, Rectangle rect) {
    area.add(new Area(rect));
  }
}

class RoundedSelectionCaret extends DefaultCaret {
  @Override protected HighlightPainter getSelectionPainter() {
    return new RoundedSelectionHighlightPainter();
  }

  @SuppressWarnings("PMD.AvoidSynchronizedAtMethodLevel")
  @Override protected synchronized void damage(Rectangle r) {
    JTextComponent c = getComponent();
    int startOffset = c.getSelectionStart();
    int endOffset = c.getSelectionEnd();
    if (startOffset == endOffset) {
      super.damage(r);
    } else {
      TextUI mapper = c.getUI();
      try {
        Rectangle p0 = mapper.modelToView(c, startOffset);
        Rectangle p1 = mapper.modelToView(c, endOffset);
        int h = (int) (p1.getMaxY() - p0.getMinY());
        c.repaint(new Rectangle(0, p0.y, c.getWidth(), h));
      } catch (BadLocationException ex) {
        UIManager.getLookAndFeel().provideErrorFeedback(c);
      }
    }
  }
}

References

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