Skip to content

Commit

Permalink
Application: Support clickable tags in trees
Browse files Browse the repository at this point in the history
  • Loading branch information
ShadelessFox committed May 3, 2024
1 parent 638ce6b commit 1c9431f
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -29,18 +30,26 @@ public ColoredComponent() {
updateUI();
}

public void append(@NotNull String fragment) {
append(fragment, TextAttributes.REGULAR_ATTRIBUTES);
}

public void append(@NotNull String fragment, @NotNull TextAttributes attributes) {
append(fragment, attributes, null);
}

public void append(@NotNull String fragment, @NotNull TextAttributes attributes, @Nullable Tag tag) {
synchronized (fragments) {
ColoredFragment lastFragment;
if (fragments.isEmpty()) {
if (fragments.isEmpty() || tag != null) {
lastFragment = null;
} else {
lastFragment = fragments.get(fragments.size() - 1);
}
if (lastFragment != null && lastFragment.attributes().equals(attributes)) {
fragments.set(fragments.size() - 1, new ColoredFragment(lastFragment.text() + fragment, attributes));
fragments.set(fragments.size() - 1, new ColoredFragment(lastFragment.text() + fragment, attributes, null));
} else {
fragments.add(new ColoredFragment(fragment, attributes));
fragments.add(new ColoredFragment(fragment, attributes, tag));
}
}

Expand All @@ -57,6 +66,46 @@ public void clear() {
repaint();
}

@Nullable
public Tag getFragmentTag(int index) {
return fragments.get(index).tag;
}

@Nullable
public Tag getFragmentTagAt(int x) {
final int index = getFragmentAt(x);
return index < 0 ? null : getFragmentTag(index);
}

public int getFragmentAt(int x) {
float offset = 0;
boolean wasSmaller = false;

if (leadingIcon != null) {
offset += padding.left + leadingIcon.getIconWidth() + iconTextGap;
}

final Font baseFont = getBaseFont();

synchronized (fragments) {
for (int i = 0; i < fragments.size(); i++) {
final ColoredFragment fragment = fragments.get(i);
final TextAttributes attributes = fragment.attributes();
final Font font = deriveFontFromAttributes(baseFont, attributes, wasSmaller);
final float fragmentWidth = computeFragmentWidth(fragment, font);

if (x >= offset && x <= offset + fragmentWidth) {
return i;
}

offset += fragmentWidth;
wasSmaller = attributes.isSmaller();
}
}

return -1;
}

@Nullable
public Icon getLeadingIcon() {
return leadingIcon;
Expand All @@ -83,7 +132,6 @@ public void setTrailingIcon(@Nullable Icon trailingIcon) {
repaint();
}


public int getIconTextGap() {
return iconTextGap;
}
Expand Down Expand Up @@ -245,8 +293,8 @@ private void doPaintTextBackground(@NotNull Graphics2D g, int offset) {
}
}

@SuppressWarnings("MagicConstant")
@NotNull
@SuppressWarnings("MagicConstant")
private Font deriveFontFromAttributes(@NotNull Font font, @NotNull TextAttributes attributes, boolean wasSmaller) {
final int style = attributes.fontStyle();

Expand Down Expand Up @@ -341,5 +389,10 @@ private Font getBaseFont() {
return font;
}

private record ColoredFragment(@NotNull String text, @NotNull TextAttributes attributes) {}
@FunctionalInterface
public interface Tag {
void run(@NotNull MouseEvent e);
}

private record ColoredFragment(@NotNull String text, @NotNull TextAttributes attributes, @Nullable Tag tag) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ public Component getListCellRendererComponent(JList<? extends T> list, T value,
}

@Override
public void append(@NotNull String fragment, @NotNull TextAttributes attributes) {
public void append(@NotNull String fragment, @NotNull TextAttributes attributes, @Nullable Tag tag) {
if (selected) {
super.append(fragment, new TextAttributes(getForeground(), attributes.styles()));
super.append(fragment, new TextAttributes(getForeground(), attributes.styles()), tag);
} else {
super.append(fragment, attributes);
super.append(fragment, attributes, tag);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@

import javax.swing.*;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.lang.ref.WeakReference;
import java.util.Objects;

public abstract class ColoredTreeCellRenderer<T> extends ColoredComponent implements TreeCellRenderer {
Expand All @@ -25,6 +28,12 @@ public ColoredTreeCellRenderer() {
setPadding(new Insets(2, 2, 2, 2));
}

@NotNull
public ColoredTreeCellRenderer<T> withTags(@NotNull JTree tree) {
new TreeTagMouseListener().installOn(tree);
return this;
}

@Override
public void updateUI() {
super.updateUI();
Expand Down Expand Up @@ -64,11 +73,11 @@ public Component getTreeCellRendererComponent(JTree tree, Object value, boolean
}

@Override
public void append(@NotNull String fragment, @NotNull TextAttributes attributes) {
public void append(@NotNull String fragment, @NotNull TextAttributes attributes, @Nullable Tag tag) {
if (selected && isFocused()) {
super.append(fragment, new TextAttributes(getForeground(), attributes.styles()));
super.append(fragment, new TextAttributes(getForeground(), attributes.styles()), tag);
} else {
super.append(fragment, attributes);
super.append(fragment, attributes, tag);
}
}

Expand All @@ -91,4 +100,34 @@ protected final boolean isFocused() {
}

protected abstract void customizeCellRenderer(@NotNull JTree tree, @NotNull T value, boolean selected, boolean expanded, boolean focused, boolean leaf, int row);

private class TreeTagMouseListener extends TagMouseListener<Tag> {
private WeakReference<Object> lastHitNode;

@Nullable
@Override
protected Tag getTagAt(@NotNull MouseEvent e) {
final JTree tree = (JTree) e.getSource();
final TreePath path = tree.getPathForLocation(e.getX(), e.getY());

if (path != null) {
final Object node = path.getLastPathComponent();

if (lastHitNode == null || lastHitNode.get() != node || e.getButton() != MouseEvent.NOBUTTON) {
lastHitNode = new WeakReference<>(node);
getTreeCellRendererComponent(tree, node, false, false, tree.getModel().isLeaf(node), tree.getRowForPath(path), false);
}

return getFragmentTagAt(getRendererRelativeX(e, tree, path));
}

return null;
}

private int getRendererRelativeX(@NotNull MouseEvent e, @NotNull JTree tree, @NotNull TreePath path) {
final Rectangle bounds = tree.getPathBounds(path);
assert bounds != null;
return e.getX() - bounds.x;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.shade.platform.ui.controls;

import com.shade.platform.ui.util.UIUtils;
import com.shade.util.NotNull;
import com.shade.util.Nullable;

import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

public abstract class TagMouseListener<T extends ColoredComponent.Tag> extends MouseAdapter {
public void installOn(@NotNull Component component) {
component.addMouseListener(this);
component.addMouseMotionListener(this);
}

@Override
public void mouseClicked(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) {
final T tag = getTagAt(e);
if (tag != null) {
tag.run(e);
}
}
}

@Override
public void mouseMoved(MouseEvent e) {
final Component component = (Component) e.getSource();
final T tag = getTagAt(e);
UIUtils.setCursor(component, tag != null ? Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) : null);
}

@Nullable
protected abstract T getTagAt(@NotNull MouseEvent e);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public record TextAttributes(@Nullable Color foreground, @Nullable Color backgro
public static final TextAttributes GRAYED_ITALIC_ATTRIBUTES = GRAYED_ATTRIBUTES.italic();
public static final TextAttributes GRAYED_SMALL_ATTRIBUTES = GRAYED_ATTRIBUTES.smaller();

public static final TextAttributes LINK_ATTRIBUTES = new TextAttributes(UIColor.named("Component.linkColor"), EnumSet.of(Style.PLAIN));

public TextAttributes(@Nullable Color foregroundColor, @Nullable Color backgroundColor, @NotNull Style style, @NotNull Style... rest) {
this(foregroundColor, backgroundColor, EnumSet.of(style, rest));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ public final class UIUtils {
private UIUtils() {
}

@NotNull
public static Color getInactiveTextColor() {
return UIManager.getColor("Label.disabledForeground");
}

@NotNull
public static Font getDefaultFont() {
Font font = UIManager.getFont("defaultFont");
Expand Down Expand Up @@ -480,6 +475,12 @@ public static String getTextForAccelerator(@NotNull KeyStroke accelerator) {
return sb.toString();
}

public static void setCursor(@NotNull Component component, @Nullable Cursor cursor) {
if (!component.isCursorSet() || component.getCursor() != cursor) {
component.setCursor(cursor);
}
}

public interface SelectionProvider<T extends JComponent, U> {
@Nullable
U getSelection(@NotNull T component, @Nullable MouseEvent event);
Expand Down

0 comments on commit 1c9431f

Please sign in to comment.