Added image attachment display. Added round buttons.

This commit is contained in:
Snowyfox 2022-04-12 07:52:07 -04:00
parent 9eb1b5256f
commit 0a3154bbfb
23 changed files with 582 additions and 38 deletions

0
ComposeWindow.java Normal file → Executable file
View File

312
ImageWindow.java Normal file
View File

@ -0,0 +1,312 @@
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JButton;
import javax.swing.ImageIcon;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.FontMetrics;
import java.awt.BorderLayout;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.MouseWheelEvent;
import java.awt.Image;
import java.net.URL;
import java.net.MalformedURLException;
class
ImageWindow extends JFrame {
private Attachment[]
attachments;
private int
offset;
// - -%- -
private ImageComponent
display;
// ---%-@-%---
public void
showAttachments(Attachment[] attachments)
{
this.attachments = attachments;
if (attachments.length == 0) {
display.setImage(null);
display.setNext(null);
display.setPrev(null);
display.repaint();
return;
}
toImage(offset = 0);
}
public void
toNextImage()
{
if (attachments.length == 0) return;
assert offset < attachments.length - 1;
toImage(++offset);
}
public void
toPrevImage()
{
if (attachments.length == 0) return;
assert offset > 0;
toImage(--offset);
}
// - -%- -
private void
toImage(int offset)
{
int last = attachments.length - 1;
assert offset < attachments.length;
Attachment prev, curr, next;
curr = attachments[offset];
prev = offset < last ? attachments[offset + 1] : null;
next = offset > 0 ? attachments[offset - 1] : null;
display.setImage(curr.image);
display.setNext(next != null ? next.image : null);
display.setPrev(prev != null ? prev.image : null);
if (!curr.type.equals("image"))
display.setToolTipText(
display.getToolTipText()
+ "\n(Media is of type '" + curr.type + "')"
);
repaint();
}
// ---%-@-%---
ImageWindow()
{
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setSize(400, 400);
display = new ImageComponent(this);
showAttachments(new Attachment[0]);
setContentPane(display);
}
}
class
ImageComponent extends JPanel
implements
ActionListener,
MouseListener, MouseMotionListener, MouseWheelListener {
private ImageWindow
primaire;
// - -%- -
private Image
image, prevImage, nextImage;
private JPanel
buttonArea;
private JButton
prev, next, toggle;
private boolean
scaleImage;
private int
xOffset, yOffset, zoomLevel;
private int
dragX, dragY, xPOffset, yPOffset;
// ---%-@-%---
public void
setImage(Image image)
{
this.image = image;
if (image != null) {
Object p = image.getProperty("comment", this);
String desc = p instanceof String ? (String)p : null;
setToolTipText(desc);
}
xOffset = yOffset = xPOffset = yPOffset = 0;
zoomLevel = 100;
}
public void
setPrev(Image image)
{
prev.setEnabled(image != null);
prev.setIcon(toIcon(image));
}
public void
setNext(Image image)
{
next.setEnabled(image != null);
next.setIcon(toIcon(image));
}
// - -%- -
public void
actionPerformed(ActionEvent eA)
{
if (eA.getSource() == prev) primaire.toPrevImage();
if (eA.getSource() == next) primaire.toNextImage();
if (eA.getSource() == toggle) {
scaleImage = !scaleImage;
if (scaleImage) toggle.setText("Show unscaled");
else toggle.setText("Show scaled to window");
setImage(this.image);
repaint();
}
}
public void
mousePressed(MouseEvent eM)
{
dragX = eM.getX();
dragY = eM.getY();
}
public void
mouseDragged(MouseEvent eM)
{
int dx = eM.getX() - dragX;
int dy = eM.getY() - dragY;
xPOffset = dx;
yPOffset = dy;
repaint();
}
public void
mouseReleased(MouseEvent eM)
{
xOffset += xPOffset;
yOffset += yPOffset;
xPOffset = yPOffset = 0;
}
public void
mouseWheelMoved(MouseWheelEvent eMw)
{
zoomLevel += 10 * -eMw.getUnitsToScroll();
if (zoomLevel < 50) zoomLevel = 50;
if (zoomLevel > 400) zoomLevel = 400;
repaint();
}
public void
mouseEntered(MouseEvent eM) { }
public void
mouseExited(MouseEvent eM) { }
public void
mouseClicked(MouseEvent eM) { }
public void
mouseMoved(MouseEvent eM) { }
// - -%- -
private static ImageIcon
toIcon(Image image)
{
if (image == null) return null;
return new ImageIcon(image);
}
// ---%-@-%---
private class
Painter extends JPanel {
protected void
paintComponent(Graphics g)
{
if (image == null)
{
String str = "(There are no images being displayed.)";
FontMetrics fm = g.getFontMetrics();
int x = (getWidth() - fm.stringWidth(str)) / 2;
int y = (getHeight() + fm.getHeight()) / 2;
g.drawString(str, x, y);
return;
}
int wo = image.getWidth(this);
int ho = image.getHeight(this);
int wn, hn;
if (wo > ho) {
wn = scaleImage ? getWidth() : wo;
hn = ho * wn / wo;
}
else {
hn = scaleImage ? getHeight() : ho;
wn = wo * hn / ho;
}
wn = wn * zoomLevel / 100;
hn = hn * zoomLevel / 100;
int x = (getWidth() - wn) / 2;
int y = (getHeight() - hn) / 2;
x += xOffset + xPOffset;
y += yOffset + yPOffset;
g.drawImage(image, x, y, wn, hn, this);
}
}
// ---%-@-%---
ImageComponent(ImageWindow primaire)
{
this.primaire = primaire;
setOpaque(false);
scaleImage = true;
zoomLevel = 100;
prev = new JButton("<");
toggle = new JButton("Show unscaled");
next = new JButton(">");
prev.addActionListener(this);
toggle.addActionListener(this);
next.addActionListener(this);
buttonArea = new JPanel();
buttonArea.setOpaque(false);
buttonArea.add(prev);
buttonArea.add(toggle);
buttonArea.add(next);
setLayout(new BorderLayout());
add(buttonArea, BorderLayout.SOUTH);
add(new Painter(), BorderLayout.CENTER);
addMouseListener(this);
addMouseMotionListener(this);
addMouseWheelListener(this);
}
}

32
JKomasto.java Normal file → Executable file
View File

@ -5,6 +5,7 @@ import javax.swing.JComponent;
import java.awt.Dimension;
import java.awt.BorderLayout;
import java.awt.Cursor;
import java.awt.Image;
import java.util.List;
import java.time.ZonedDateTime;
@ -24,6 +25,9 @@ JKomasto {
private LoginWindow
loginWindow;
private ImageWindow
mediaWindow;
private MastodonApi
api;
@ -38,7 +42,7 @@ JKomasto {
autoViewWindow.setCursor(new Cursor(Cursor.WAIT_CURSOR));
timelineWindow.setCursor(new Cursor(Cursor.WAIT_CURSOR));
timelineWindow.showLatestPage();
timelineWindow.showLatestPage();
timelineWindow.setLocationByPlatform(true);
timelineWindow.setVisible(true);
@ -56,6 +60,9 @@ JKomasto {
public ComposeWindow
getComposeWindow() { return composeWindow; }
public ImageWindow
getMediaWindow() { return mediaWindow; }
// ---%-@-%---
public static void
@ -72,10 +79,12 @@ JKomasto {
composeWindow = new ComposeWindow(this);
autoViewWindow = new PostWindow(this);
loginWindow = new LoginWindow(this);
mediaWindow = new ImageWindow();
composeWindow.dispose();
autoViewWindow.dispose();
timelineWindow.dispose();
mediaWindow.dispose();
loginWindow.setLocationByPlatform(true);
loginWindow.setVisible(true);
}
@ -131,6 +140,9 @@ Post {
public String
authorId, authorName;
public Image
authorAvatar;
public ZonedDateTime
date;
@ -143,6 +155,24 @@ Post {
public boolean
boosted, favourited;
public Attachment[]
attachments;
}
class
Attachment {
public String
type;
public String
url;
public Image
image;
}

0
LoginWindow.java Normal file → Executable file
View File

0
MastodonApi.java Executable file → Normal file
View File

82
PostWindow.java Normal file → Executable file
View File

@ -11,6 +11,7 @@ import javax.swing.BoxLayout;
import javax.swing.BorderFactory;
import javax.swing.border.Border;
import javax.swing.JOptionPane;
import javax.swing.ImageIcon;
import java.awt.Graphics;
import java.awt.Font;
import java.awt.FontMetrics;
@ -19,6 +20,7 @@ import java.awt.Dimension;
import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.awt.Cursor;
import java.awt.Image;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.util.List;
@ -90,12 +92,19 @@ implements ActionListener {
postDisplay.setAuthorName(post.authorName);
postDisplay.setAuthorId(post.authorId);
postDisplay.setAuthorAvatar(post.authorAvatar);
postDisplay.setDate(DATE_FORMAT.format(post.date));
postDisplay.setTime(TIME_FORMAT.format(post.date));
postDisplay.setText(post.text);
postDisplay.setFavourited(post.favourited);
postDisplay.setBoosted(post.boosted);
postDisplay.setMediaPreview(
post.attachments.length == 0
? null
: post.attachments[0].image
);
repliesDisplay.setReplies(replies);
postDisplay.resetFocus();
repaint();
}
@ -203,7 +212,14 @@ implements ActionListener {
public void
openMedia()
{
ImageWindow w = primaire.getMediaWindow();
w.showAttachments(post.attachments);
int l = Math.min(40, post.text.length());
w.setTitle(post.text.substring(0, l));
if (!w.isVisible()) {
w.setLocation(getX(), getY() + 100);
w.setVisible(true);
}
}
// - -%- -
@ -258,6 +274,7 @@ implements ActionListener {
samplePost.postId = "000000000";
samplePost.boosted = false;
samplePost.favourited = true;
samplePost.attachments = new Attachment[0];
showPost(samplePost);
setContentPane(postDisplay);
@ -284,46 +301,29 @@ implements ActionListener {
replyMisc,
nextPrev;
private JButton
private RoundButton
profile,
media;
// ---%-@-%---
public void
setAuthorName(String authorName)
{
assert authorName != null;
this.authorName = authorName;
}
setAuthorName(String n) { authorName = n; }
public void
setAuthorId(String authorId)
{
assert authorId != null;
this.authorId = authorId;
}
setAuthorId(String n) { authorId = n; }
public void
setAuthorAvatar(Image n) { profile.setImage(n); }
public void
setDate(String date)
{
assert date != null;
this.date = date;
}
setDate(String n) { date = n; }
public void
setTime(String time)
{
assert time != null;
this.time = time;
}
setTime(String n) { time = n; }
public void
setText(String text)
{
assert text != null;
this.text = text;
}
setText(String n) { text = n; }
public void
setFavourited(boolean a)
@ -347,6 +347,12 @@ implements ActionListener {
favouriteBoost.setEnabled(a);
}
public void
setMediaPreview(Image n) { media.setImage(n); }
public void
resetFocus() { media.requestFocusInWindow(); }
// - -%- -
public void
@ -405,6 +411,11 @@ implements ActionListener {
{
g.clearRect(0, 0, getWidth(), getHeight());
((java.awt.Graphics2D)g).setRenderingHint(
java.awt.RenderingHints.KEY_TEXT_ANTIALIASING,
java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON
);
Font f1 = new Font("IPAGothic", Font.PLAIN, 16);
Font f2 = new Font("IPAGothic", Font.PLAIN, 14);
FontMetrics fm1 = g.getFontMetrics(f1);
@ -443,6 +454,13 @@ implements ActionListener {
// - -%- -
private static ImageIcon
toIcon(Image image)
{
if (image == null) return null;
return new ImageIcon(image);
}
private static List<String>
split(String string, int lineLength)
{
@ -487,9 +505,8 @@ implements ActionListener {
Dimension buttonSize = new Dimension(20, 40);
profile = new JButton("P");
profile.setPreferredSize(buttonSize);
profile.setMargin(null);
profile = new RoundButton();
//profile.setPreferredSize(buttonSize);
profile.addActionListener(this);
favouriteBoost = new TwoToggleButton("favourite", "boost");
@ -501,9 +518,8 @@ implements ActionListener {
nextPrev = new TwoToggleButton("next", "prev");
nextPrev.addActionListener(this);
media = new JButton("media");
media.setPreferredSize(buttonSize);
media.setMargin(null);
media = new RoundButton();
//media.setPreferredSize(buttonSize);
media.addActionListener(this);
Box ibuttons = Box.createVerticalBox();

0
RequestListener.java Executable file → Normal file
View File

0
RudimentaryHTMLParser.java Executable file → Normal file
View File

49
TimelineWindow.java Normal file → Executable file
View File

@ -9,6 +9,7 @@ import javax.swing.JMenuItem;
import javax.swing.JMenuBar;
import javax.swing.JSeparator;
import javax.swing.Box;
import javax.swing.ImageIcon;
import javax.swing.BorderFactory;
import java.awt.BorderLayout;
import java.awt.GridBagLayout;
@ -22,6 +23,8 @@ import java.awt.Color;
import java.awt.Graphics;
import java.util.List;
import java.util.ArrayList;
import java.net.URL;
import java.net.MalformedURLException;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.event.MouseListener;
@ -257,12 +260,13 @@ implements ActionListener {
private static List<Post>
toPosts(Tree<String> json)
{
List<Post> posts = new ArrayList<>();
List<Post> posts = new ArrayList<>();
for (Tree<String> post: json.children)
{
Tree<String> account = post.get("account");
Post addee = new Post();
addee.postId = post.get("id").value;
try {
String s = post.get("created_at").value;
addee.date = ZonedDateTime.parse(s);
@ -273,6 +277,7 @@ implements ActionListener {
assert false;
addee.date = ZonedDateTime.now();
}
try {
StringBuilder b = new StringBuilder();
Tree<String> nodes =
@ -299,17 +304,55 @@ implements ActionListener {
eIo.printStackTrace();
assert false;
}
String s = post.get("spoiler_text").value;
if (!s.isEmpty()) addee.contentWarning = s;
else addee.contentWarning = null;
Tree<String> account = post.get("account");
addee.authorId = account.get("acct").value;
addee.authorName = account.get("username").value;
String s2 = account.get("display_name").value;
if (!s2.isEmpty()) addee.authorName = s2;
try {
String av = account.get("avatar").value;
ImageIcon icon = new ImageIcon(new URL(av));
addee.authorAvatar = icon.getImage();
}
catch (MalformedURLException eMu) {
// Weird bug on their part.. We should
// probably react by using a default avatar.
}
String f = post.get("favourited").value;
String b = post.get("reblogged").value;
addee.favourited = f.equals("true");
addee.boosted = b.equals("true");
Tree<String> a1 = post.get("media_attachments");
Attachment[] a2 = new Attachment[a1.size()];
for (int o = 0; o < a2.length; ++o)
{
a2[o] = new Attachment();
a2[o].type = a1.get(o).get("type").value;
a2[o].url = a1.get(o).get("remote_url").value;
if (a2[o].url == null)
a2[o].url = a1.get(o).get("text_url").value;
a2[o].image = null;
if (a2[o].type.equals("image")) try
{
URL url = new URL(a2[o].url);
ImageIcon icon = new ImageIcon(url);
String desc = a1.get(o).get("description").value;
icon.setDescription(desc);
a2[o].image = icon.getImage();
}
catch (MalformedURLException eMu) { }
}
addee.attachments = a2;
posts.add(addee);
}
return posts;
@ -375,7 +418,7 @@ implements ActionListener {
page = new TimelinePage();
page.posts = new ArrayList<>();
setTimelineType(TimelineType.FEDERATED);
setTimelineType(TimelineType.HOME);
display = new TimelineComponent(this);
display.setNextPageAvailable(false);

145
TwoToggleButton.java Normal file → Executable file
View File

@ -6,6 +6,8 @@ import javax.swing.ImageIcon;
import java.awt.Image;
import java.awt.Graphics;
import java.awt.Dimension;
import java.awt.Shape;
import java.awt.geom.Ellipse2D;
import java.awt.event.MouseListener;
import java.awt.event.MouseEvent;
import java.awt.event.KeyListener;
@ -92,7 +94,7 @@ implements KeyListener, MouseListener, FocusListener {
// - -%- -
private void
private void
announce(String name, boolean toggled)
{
ActionEvent eA = new ActionEvent(
@ -232,6 +234,147 @@ implements KeyListener, MouseListener, FocusListener {
}
class
RoundButton extends AbstractButton
implements KeyListener, MouseListener, FocusListener {
private Image
image;
private int
nextEventID = ActionEvent.ACTION_FIRST;
// - -%- -
private static Image
button,
selectedOverlay,
disabledOverlay;
// ---%-@-%---
public void
setImage(Image n) { image = n; }
// - -%- -
protected void
paintComponent(Graphics g)
{
((java.awt.Graphics2D)g).setRenderingHint(
java.awt.RenderingHints.KEY_TEXT_ANTIALIASING,
java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON
);
g.drawImage(button, 0, 0, this);
if (!isEnabled())
g.drawImage(disabledOverlay, 0, 0, this);
if (isFocusOwner())
g.drawImage(selectedOverlay, 0, 0, this);
if (image == null) return;
int w1 = button.getWidth(this);
int h1 = button.getHeight(this);
Shape defaultClip = g.getClip();
Shape roundClip = new Ellipse2D.Float(6, 6, w1 - 12, h1 - 12);
int w2 = image.getWidth(this);
int h2 = image.getHeight(this);
if (h2 > w2) {
h2 = h2 * w1 / w2;
w2 = w1;
}
else {
w2 = w2 * h1 / h2;
h2 = h1;
}
g.setClip(roundClip);
g.drawImage(image, 0, 0, w2, h2, this);
g.setClip(defaultClip);
}
private void
announce()
{
ActionEvent eA = new ActionEvent(this, nextEventID++, null);
for (ActionListener listener: getActionListeners())
listener.actionPerformed(eA);
}
public void
keyPressed(KeyEvent eK)
{
if (eK.getKeyCode() != KeyEvent.VK_SPACE) return;
doClick();
}
public void
mouseClicked(MouseEvent eM) { announce(); }
public void
focusGained(FocusEvent eF) { repaint(); }
public void
focusLost(FocusEvent eF) { repaint(); }
public void
mousePressed(MouseEvent eM) { }
public void
mouseReleased(MouseEvent eM) { }
public void
mouseEntered(MouseEvent eM) { }
public void
mouseExited(MouseEvent eM) { }
public void
keyReleased(KeyEvent eK) { }
public void
keyTyped(KeyEvent eK) { }
// () Again, armed?
// ---%-@-%---
RoundButton()
{
if (button == null) loadCommonImages();
setModel(new DefaultButtonModel());
setFocusable(true);
setOpaque(false);
int w = button.getWidth(null);
int h = button.getHeight(null);
setPreferredSize(new Dimension(w, h));
this.addKeyListener(this);
this.addMouseListener(this);
this.addFocusListener(this);
}
// - -%- -
private static void
loadCommonImages()
{
Class c = TwoToggleButton.class;
URL u1 = c.getResource("graphics/button.png");
URL u2 = c.getResource("graphics/disabledOverlay.png");
URL u3 = c.getResource("graphics/selectedOverlay.png");
assert u1 != null && u2 != null && u3 != null;
button = new ImageIcon(u1).getImage();
disabledOverlay = new ImageIcon(u2).getImage();
selectedOverlay = new ImageIcon(u3).getImage();
}
}
class
TwoToggleButtonTest {

0
graphics/Flags.xcf Normal file → Executable file
View File

0
graphics/Hourglass.xcf Normal file → Executable file
View File

0
graphics/boostToggled.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

0
graphics/boostUntoggled.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

0
graphics/button.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

0
graphics/disabledOverlay.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

0
graphics/favouriteUntoggled.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

0
graphics/ref1.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

0
graphics/selectedOverlay.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 313 B

After

Width:  |  Height:  |  Size: 313 B

0
graphics/test1.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

0
graphics/test2.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

0
graphics/test3.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

0
graphics/test4.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB