Save or steal other people's avatars from the info panel
Right-clicking a client's avatar offers to save it to a file or to take it as our own avatar on that server, as the Android picture viewer does. Our own avatar only offers saving. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,11 +16,20 @@ import com.ts3client.text.BBCode;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JEditorPane;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JLayeredPane;
|
||||
import javax.swing.JMenuItem;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JPopupMenu;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.text.AttributeSet;
|
||||
import javax.swing.text.BadLocationException;
|
||||
import javax.swing.text.StyleConstants;
|
||||
import javax.swing.text.html.HTML;
|
||||
import javax.swing.text.html.HTMLDocument;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Cursor;
|
||||
import java.awt.Dimension;
|
||||
@@ -30,8 +39,12 @@ import java.awt.event.ComponentAdapter;
|
||||
import java.awt.event.ComponentEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Read-only detail view for the server, channel or client selected, mirroring the
|
||||
@@ -62,6 +75,8 @@ public final class InfoPanel extends JPanel {
|
||||
/** Floats over the bottom-right corner of the details, out of the text's way. */
|
||||
private final JLabel toggle = new JLabel("Show in chat tab");
|
||||
private DescriptionHandler descriptionHandler;
|
||||
/** Takes someone else's avatar as ours; null where that cannot be done. */
|
||||
private Consumer<Avatar> avatarStealer;
|
||||
private ChannelNode shownChannel;
|
||||
private ClientEntry shownClient;
|
||||
/** The server's address while the server itself is shown. */
|
||||
@@ -91,6 +106,17 @@ public final class InfoPanel extends JPanel {
|
||||
if (e.getEventType() != javax.swing.event.HyperlinkEvent.EventType.ACTIVATED) return;
|
||||
Links.open(e.getDescription(), this);
|
||||
});
|
||||
pane.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
if (e.isPopupTrigger()) showAvatarMenu(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (e.isPopupTrigger()) showAvatarMenu(e);
|
||||
}
|
||||
});
|
||||
HtmlStyles.fillViewport(pane);
|
||||
scroll.getViewport().addComponentListener(new ComponentAdapter() {
|
||||
@Override
|
||||
@@ -193,6 +219,10 @@ public final class InfoPanel extends JPanel {
|
||||
this.descriptionHandler = handler;
|
||||
}
|
||||
|
||||
public void setAvatarStealer(Consumer<Avatar> stealer) {
|
||||
this.avatarStealer = stealer;
|
||||
}
|
||||
|
||||
/** Runs when the user closes the info chat tab; shows details in the panel again. */
|
||||
private void onInfoTabClosed() {
|
||||
inChatTab = false;
|
||||
@@ -264,6 +294,55 @@ public final class InfoPanel extends JPanel {
|
||||
return Math.max(UIScale.scale(AVATAR_MIN_SIZE), Math.min(max, box));
|
||||
}
|
||||
|
||||
private void showAvatarMenu(MouseEvent e) {
|
||||
Avatar avatar = shownClient == null ? null : shownAvatar;
|
||||
if (avatar == null || !overAvatar(avatar, e.getPoint())) return;
|
||||
JPopupMenu menu = new JPopupMenu();
|
||||
JMenuItem save = new JMenuItem("Save…");
|
||||
save.addActionListener(a -> saveAvatar(avatar, "avatar_" + shownClient.nickname));
|
||||
menu.add(save);
|
||||
if (!shownClient.self && avatarStealer != null) {
|
||||
JMenuItem steal = new JMenuItem("Steal for avatar");
|
||||
steal.addActionListener(a -> avatarStealer.accept(avatar));
|
||||
menu.add(steal);
|
||||
}
|
||||
menu.show(pane, e.getX(), e.getY());
|
||||
}
|
||||
|
||||
/** Whether {@code point} falls on the avatar's {@code <img>}. */
|
||||
private boolean overAvatar(Avatar avatar, Point point) {
|
||||
if (avatar.file() == null) return false;
|
||||
String src = avatar.file().toURI().toString();
|
||||
int pos = pane.viewToModel2D(point);
|
||||
for (int at : new int[]{pos, pos - 1}) {
|
||||
if (at < 0) continue;
|
||||
var el = ((HTMLDocument) pane.getDocument()).getCharacterElement(at);
|
||||
AttributeSet attrs = el.getAttributes();
|
||||
if (attrs.getAttribute(StyleConstants.NameAttribute) != HTML.Tag.IMG
|
||||
|| !src.equals(attrs.getAttribute(HTML.Attribute.SRC))) continue;
|
||||
try {
|
||||
Rectangle2D start = pane.modelToView2D(el.getStartOffset());
|
||||
Rectangle2D end = pane.modelToView2D(el.getEndOffset());
|
||||
if (start != null && end != null && start.createUnion(end).contains(point)) return true;
|
||||
} catch (BadLocationException ignored) {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void saveAvatar(Avatar avatar, String name) {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setDialogTitle("Save avatar");
|
||||
chooser.setSelectedFile(new File(name + "." + avatar.format().extension));
|
||||
if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) return;
|
||||
try {
|
||||
Files.write(chooser.getSelectedFile().toPath(), avatar.data());
|
||||
} catch (IOException ex) {
|
||||
JOptionPane.showMessageDialog(this, "Could not save the avatar: " + ex.getMessage(),
|
||||
"Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
private String clientBody(ClientEntry cl, ServerModel model, IconRepository icons, Contact contact,
|
||||
Avatar avatar, int avatarBox) {
|
||||
String details = clientDetails(cl, model, icons, contact);
|
||||
|
||||
Reference in New Issue
Block a user