mirror of
https://gitlab.com/biskuteri-cafe/JKomasto2.git
synced 2024-11-20 06:24:50 +01:00
e6fea4c061
(Before this, JKomasto and sometimes the Mastodon web client would get '411 Record Not Found' when submitting the same text after deleting and redrafting. Presumably the Mastodon server caches both whether an idempotency key was fulfilled and which post it leads to, and for some reason it looks up the second and fails.)
1033 lines
25 KiB
Java
1033 lines
25 KiB
Java
|
|
import javax.swing.JFrame;
|
|
import javax.swing.JPanel;
|
|
import javax.swing.JComponent;
|
|
import javax.swing.JButton;
|
|
import javax.swing.JLabel;
|
|
import javax.swing.JMenu;
|
|
import javax.swing.JMenuItem;
|
|
import javax.swing.JMenuBar;
|
|
import javax.swing.JSeparator;
|
|
import javax.swing.JRadioButton;
|
|
import javax.swing.JOptionPane;
|
|
import javax.swing.ButtonGroup;
|
|
import javax.swing.Box;
|
|
import javax.swing.BorderFactory;
|
|
import java.awt.BorderLayout;
|
|
import java.awt.GridBagLayout;
|
|
import java.awt.GridBagConstraints;
|
|
import java.awt.FlowLayout;
|
|
import java.awt.Font;
|
|
import java.awt.Dimension;
|
|
import java.awt.Insets;
|
|
import java.awt.Cursor;
|
|
import java.awt.Color;
|
|
import java.awt.Graphics;
|
|
import java.awt.Image;
|
|
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;
|
|
import java.awt.event.MouseEvent;
|
|
import java.awt.event.FocusListener;
|
|
import java.awt.event.FocusEvent;
|
|
import java.awt.event.KeyListener;
|
|
import java.awt.event.KeyEvent;
|
|
|
|
import cafe.biskuteri.hinoki.Tree;
|
|
import java.io.IOException;
|
|
import java.time.ZonedDateTime;
|
|
import java.time.ZoneId;
|
|
import java.time.Period;
|
|
import java.time.temporal.ChronoUnit;
|
|
import java.time.format.DateTimeParseException;
|
|
|
|
class
|
|
TimelineWindow extends JFrame
|
|
implements ActionListener {
|
|
|
|
private JKomasto
|
|
primaire;
|
|
|
|
private MastodonApi
|
|
api;
|
|
|
|
private WindowUpdater
|
|
windowUpdater;
|
|
|
|
private TimelinePage
|
|
page;
|
|
|
|
// - -%- -
|
|
|
|
private TimelineComponent
|
|
display;
|
|
|
|
private JMenuItem
|
|
openHome,
|
|
openMessages,
|
|
openLocal,
|
|
openFederated,
|
|
openNotifications,
|
|
openOwnProfile,
|
|
openProfile,
|
|
createPost,
|
|
openAutoPostView,
|
|
quit;
|
|
|
|
private JMenuItem
|
|
flipToNewestPost;
|
|
|
|
private boolean
|
|
showingLatest;
|
|
|
|
// - -%- -
|
|
|
|
private static final int
|
|
PREVIEW_COUNT = TimelineComponent.PREVIEW_COUNT;
|
|
|
|
// ---%-@-%---
|
|
|
|
public synchronized void
|
|
use(TimelinePage page)
|
|
{
|
|
assert page != null;
|
|
this.page = page;
|
|
|
|
List<PostPreviewComponent> previews;
|
|
previews = display.getPostPreviews();
|
|
|
|
int available = page.posts.length;
|
|
int max = previews.size();
|
|
assert available <= max;
|
|
|
|
for (int o = 0; o < available; ++o)
|
|
{
|
|
PostPreviewComponent preview = previews.get(o);
|
|
Post post = page.posts[o];
|
|
|
|
preview.setTopLeft(post.author.name);
|
|
if (post.boostedPost != null)
|
|
{
|
|
String s = "boosted by " + post.author.name;
|
|
preview.setTopLeft(s);
|
|
post = post.boostedPost;
|
|
}
|
|
|
|
String flags = "";
|
|
if (post.attachments.length > 0) flags += "a";
|
|
post.resolveRelativeTime();
|
|
preview.setTopRight(flags + " " + post.relativeTime);
|
|
|
|
post.resolveApproximateText();
|
|
if (post.contentWarning != null)
|
|
preview.setBottom("(" + post.contentWarning + ")");
|
|
else
|
|
preview.setBottom(post.approximateText);
|
|
}
|
|
for (int o = available; o < max; ++o)
|
|
{
|
|
previews.get(o).reset();
|
|
}
|
|
|
|
boolean full = !(available < PREVIEW_COUNT);
|
|
display.setNextPageAvailable(full);
|
|
display.setPreviousPageAvailable(true);
|
|
display.resetFocus();
|
|
}
|
|
|
|
public void
|
|
readEntity(Tree<String> postEntityArray)
|
|
{
|
|
TimelinePage page = new TimelinePage(postEntityArray);
|
|
page.type = this.page.type;
|
|
page.accountNumId = this.page.accountNumId;
|
|
page.listId = this.page.listId;
|
|
use(page);
|
|
}
|
|
|
|
public void
|
|
refresh()
|
|
{
|
|
String firstId = null;
|
|
if (!showingLatest)
|
|
{
|
|
assert page.posts != null;
|
|
assert page.posts.length != 0;
|
|
firstId = page.posts[0].id;
|
|
}
|
|
|
|
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
|
api.getTimelinePage(
|
|
page.type,
|
|
PREVIEW_COUNT, firstId, null,
|
|
page.accountNumId, page.listId,
|
|
new RequestListener() {
|
|
|
|
public void
|
|
connectionFailed(IOException eIo)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
TimelineWindow.this,
|
|
"Failed to fetch page.."
|
|
+ "\n" + eIo.getMessage()
|
|
);
|
|
}
|
|
|
|
public void
|
|
requestFailed(int httpCode, Tree<String> json)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
TimelineWindow.this,
|
|
"Failed to fetch page.."
|
|
+ "\n" + json.get("error").value
|
|
+ "\n(HTTP code: " + httpCode + ")"
|
|
);
|
|
}
|
|
|
|
public void
|
|
requestSucceeded(Tree<String> json)
|
|
{
|
|
if (json.size() < PREVIEW_COUNT)
|
|
{
|
|
showLatestPage();
|
|
return;
|
|
}
|
|
readEntity(json);
|
|
}
|
|
|
|
}
|
|
);
|
|
display.setCursor(null);
|
|
}
|
|
|
|
public void
|
|
showLatestPage()
|
|
{
|
|
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
|
api.getTimelinePage(
|
|
page.type,
|
|
PREVIEW_COUNT, null, null,
|
|
page.accountNumId, page.listId,
|
|
new RequestListener() {
|
|
|
|
public void
|
|
connectionFailed(IOException eIo)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
TimelineWindow.this,
|
|
"Failed to fetch page.."
|
|
+ "\n" + eIo.getMessage()
|
|
);
|
|
}
|
|
|
|
public void
|
|
requestFailed(int httpCode, Tree<String> json)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
TimelineWindow.this,
|
|
"Failed to fetch page.."
|
|
+ "\n" + json.get("error").value
|
|
+ "\n(HTTP code: " + httpCode + ")"
|
|
);
|
|
}
|
|
|
|
public void
|
|
requestSucceeded(Tree<String> json)
|
|
{
|
|
readEntity(json);
|
|
showingLatest = true;
|
|
windowUpdater.add(TimelineWindow.this);
|
|
}
|
|
|
|
}
|
|
);
|
|
display.setCursor(null);
|
|
}
|
|
|
|
public void
|
|
showNextPage()
|
|
{
|
|
assert page.posts != null;
|
|
assert page.posts.length != 0;
|
|
String lastId = page.posts[page.posts.length - 1].id;
|
|
|
|
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
|
api.getTimelinePage(
|
|
page.type,
|
|
PREVIEW_COUNT, lastId, null,
|
|
page.accountNumId, page.listId,
|
|
new RequestListener() {
|
|
|
|
public void
|
|
connectionFailed(IOException eIo)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
TimelineWindow.this,
|
|
"Failed to fetch page.."
|
|
+ "\n" + eIo.getMessage()
|
|
);
|
|
}
|
|
|
|
public void
|
|
requestFailed(int httpCode, Tree<String> json)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
TimelineWindow.this,
|
|
"Failed to fetch page.."
|
|
+ "\n" + json.get("error").value
|
|
+ "\n(HTTP code: " + httpCode + ")"
|
|
);
|
|
}
|
|
|
|
public void
|
|
requestSucceeded(Tree<String> json)
|
|
{
|
|
if (json.size() == 0)
|
|
{
|
|
// We should probably say something
|
|
// to the user here? For now, we
|
|
// quietly cancel.
|
|
return;
|
|
}
|
|
readEntity(json);
|
|
showingLatest = false;
|
|
windowUpdater.remove(TimelineWindow.this);
|
|
}
|
|
|
|
}
|
|
);
|
|
display.setCursor(null);
|
|
}
|
|
|
|
public void
|
|
showPreviousPage()
|
|
{
|
|
assert page.posts != null;
|
|
assert page.posts.length != 0;
|
|
String firstId = page.posts[0].id;
|
|
|
|
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
|
api.getTimelinePage(
|
|
page.type,
|
|
PREVIEW_COUNT, null, firstId,
|
|
page.accountNumId, page.listId,
|
|
new RequestListener() {
|
|
|
|
public void
|
|
connectionFailed(IOException eIo)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
TimelineWindow.this,
|
|
"Failed to fetch page.."
|
|
+ "\n" + eIo.getMessage()
|
|
);
|
|
}
|
|
|
|
public void
|
|
requestFailed(int httpCode, Tree<String> json)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
TimelineWindow.this,
|
|
"Failed to fetch page.."
|
|
+ "\n" + json.get("error").value
|
|
+ "\n(HTTP code: " + httpCode + ")"
|
|
);
|
|
}
|
|
|
|
public void
|
|
requestSucceeded(Tree<String> json)
|
|
{
|
|
if (json.size() < PREVIEW_COUNT)
|
|
{
|
|
showLatestPage();
|
|
return;
|
|
}
|
|
readEntity(json);
|
|
showingLatest = false;
|
|
windowUpdater.remove(TimelineWindow.this);
|
|
}
|
|
|
|
}
|
|
);
|
|
display.setCursor(null);
|
|
}
|
|
|
|
public synchronized void
|
|
setTimelineType(TimelineType type)
|
|
{
|
|
assert type != TimelineType.LIST;
|
|
assert type != TimelineType.PROFILE;
|
|
|
|
page.type = type;
|
|
page.accountNumId = null;
|
|
setTitle(toString(type) + " timeline - JKomasto");
|
|
|
|
String f = type.toString().toLowerCase();
|
|
display.setBackgroundImage(ImageApi.local(f));
|
|
/*
|
|
* (注) Java's image renderer draws images with transparency
|
|
* darker than GIMP does. Overcompensate in lightening.
|
|
*/
|
|
display.repaint();
|
|
}
|
|
|
|
public synchronized TimelineType
|
|
getTimelineType() { return page.type; }
|
|
|
|
public synchronized void
|
|
showAuthorPosts(String authorNumId)
|
|
{
|
|
assert authorNumId != null;
|
|
|
|
page.type = TimelineType.PROFILE;
|
|
page.accountNumId = authorNumId;
|
|
setTitle(authorNumId + " - JKomasto");
|
|
|
|
display.setBackgroundImage(ImageApi.local("profile"));
|
|
display.repaint();
|
|
}
|
|
|
|
public synchronized void
|
|
openOwnProfile()
|
|
{
|
|
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
|
|
|
Tree<String> accountDetails = api.getAccountDetails();
|
|
ProfileWindow w = new ProfileWindow(primaire);
|
|
w.use(new Account(accountDetails));
|
|
w.setLocationByPlatform(true);
|
|
w.setVisible(true);
|
|
|
|
display.setCursor(null);
|
|
}
|
|
|
|
public void
|
|
openProfile()
|
|
{
|
|
String query = JOptionPane.showInputDialog(
|
|
this,
|
|
"Whose account do you want to see?\n"
|
|
+ "Type an account name with the instance,\n"
|
|
+ "or a display name if you can't remember.\n",
|
|
"Profile search",
|
|
JOptionPane.PLAIN_MESSAGE
|
|
);
|
|
if (query == null) return;
|
|
|
|
class Handler implements RequestListener {
|
|
|
|
public Tree<String>
|
|
json;
|
|
|
|
// -=%=-
|
|
|
|
public void
|
|
connectionFailed(IOException eIo)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
TimelineWindow.this,
|
|
"Tried to fetch accounts, but it failed.."
|
|
+ "\n" + eIo.getMessage()
|
|
);
|
|
}
|
|
|
|
public void
|
|
requestFailed(int httpCode, Tree<String> json)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
TimelineWindow.this,
|
|
"Tried to fetch accounts, but it failed.."
|
|
+ "\n" + json.get("error").value
|
|
+ "\n(HTTP code: " + httpCode + ")"
|
|
);
|
|
}
|
|
|
|
public void
|
|
requestSucceeded(Tree<String> json) { this.json = json; }
|
|
|
|
}
|
|
// (知) Have to create a named class because
|
|
// it has to hold the variable.
|
|
Handler handler = new Handler();
|
|
api.getAccounts(query, handler);
|
|
if (handler.json == null) return;
|
|
|
|
if (handler.json.size() == 0)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
this,
|
|
"There were no results from the query.. ☹️"
|
|
);
|
|
return;
|
|
}
|
|
|
|
Tree<String> openee = null;
|
|
if (query.startsWith("@")) query = query.substring(1);
|
|
|
|
List<Object> message = new ArrayList<>();
|
|
message.add("Maybe one of these?");
|
|
ButtonGroup selGroup = new ButtonGroup();
|
|
for (Tree<String> account: handler.json)
|
|
{
|
|
String dname = account.get("display_name").value;
|
|
String acct = account.get("acct").value;
|
|
if (query.equals(acct)) {
|
|
openee = account;
|
|
break;
|
|
}
|
|
JRadioButton b = new JRadioButton();
|
|
b.setText(dname + " (" + acct + ")");
|
|
selGroup.add(b);
|
|
message.add(b);
|
|
}
|
|
if (openee == null)
|
|
{
|
|
int response = JOptionPane.showConfirmDialog(
|
|
this,
|
|
message.toArray(),
|
|
"Search results",
|
|
JOptionPane.OK_CANCEL_OPTION
|
|
);
|
|
if (response == JOptionPane.CANCEL_OPTION) return;
|
|
for (int o = 1; o < message.size(); ++o)
|
|
{
|
|
JRadioButton b = (JRadioButton)message.get(o);
|
|
if (selGroup.isSelected(b.getModel()))
|
|
{
|
|
openee = handler.json.get(o - 1);
|
|
break;
|
|
}
|
|
}
|
|
if (openee == null) return;
|
|
/*
|
|
* It seems like this can happen if someone
|
|
* presses escape out of the confirm dialog.
|
|
* I don't know why that doesn't map to cancel.
|
|
*/
|
|
}
|
|
|
|
ProfileWindow w = new ProfileWindow(primaire);
|
|
w.use(new Account(openee));
|
|
w.setLocationByPlatform(true);
|
|
w.setVisible(true);
|
|
}
|
|
|
|
// - -%- -
|
|
|
|
public synchronized void
|
|
previewSelected(int index)
|
|
{
|
|
if (index > page.posts.length) return;
|
|
Post post = page.posts[index - 1];
|
|
primaire.getAutoViewWindow().use(post);
|
|
}
|
|
|
|
public synchronized void
|
|
previewOpened(int index)
|
|
{
|
|
if (index > page.posts.length) return;
|
|
Post post = page.posts[index - 1];
|
|
|
|
PostWindow w = new PostWindow(primaire);
|
|
w.use(post);
|
|
w.setLocationRelativeTo(this);
|
|
w.setVisible(true);
|
|
}
|
|
|
|
public void
|
|
actionPerformed(ActionEvent eA)
|
|
{
|
|
Object src = eA.getSource();
|
|
|
|
if (src == openHome)
|
|
{
|
|
setTimelineType(TimelineType.HOME);
|
|
showLatestPage();
|
|
}
|
|
if (src == openFederated)
|
|
{
|
|
setTimelineType(TimelineType.FEDERATED);
|
|
showLatestPage();
|
|
}
|
|
if (src == openLocal)
|
|
{
|
|
setTimelineType(TimelineType.LOCAL);
|
|
showLatestPage();
|
|
}
|
|
if (src == openOwnProfile)
|
|
{
|
|
openOwnProfile();
|
|
}
|
|
if (src == openProfile)
|
|
{
|
|
openProfile();
|
|
}
|
|
if (src == createPost)
|
|
{
|
|
primaire.getComposeWindow().setVisible(true);
|
|
}
|
|
if (src == openAutoPostView)
|
|
{
|
|
PostWindow w = primaire.getAutoViewWindow();
|
|
w.setLocation(getX() + 10 + getWidth(), getY());
|
|
w.setVisible(true);
|
|
}
|
|
if (src == openNotifications)
|
|
{
|
|
NotificationsWindow w =
|
|
primaire.getNotificationsWindow();
|
|
if (!w.isVisible())
|
|
{
|
|
w.setLocationByPlatform(true);
|
|
w.setVisible(true);
|
|
}
|
|
}
|
|
if (src == flipToNewestPost)
|
|
{
|
|
showLatestPage();
|
|
}
|
|
if (src == quit)
|
|
{
|
|
/*
|
|
* Umm.. should we even have a quit option?
|
|
* Wouldn't closing every window work? By
|
|
* disposing of everyone. We won't be having any
|
|
* background threads IIRC (and they can check
|
|
* if the Swing thread is alive).
|
|
*/
|
|
dispose();
|
|
}
|
|
}
|
|
|
|
// - -%- -
|
|
|
|
private static String
|
|
toString(TimelineType type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case FEDERATED: return "Federated";
|
|
case LOCAL: return "Local";
|
|
case HOME: return "Home";
|
|
default: return "";
|
|
}
|
|
}
|
|
|
|
// ---%-@-%---
|
|
|
|
TimelineWindow(JKomasto primaire)
|
|
{
|
|
this.primaire = primaire;
|
|
this.api = primaire.getMastodonApi();
|
|
this.windowUpdater = primaire.getWindowUpdater();
|
|
|
|
getContentPane().setPreferredSize(new Dimension(320, 460));
|
|
pack();
|
|
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
|
|
|
openHome = new JMenuItem("Open home timeline");
|
|
openFederated = new JMenuItem("Open federated timeline");
|
|
openNotifications = new JMenuItem("Open notifications");
|
|
openOwnProfile = new JMenuItem("Open own profile");
|
|
openProfile = new JMenuItem("Open profile..");
|
|
createPost = new JMenuItem("Create a post");
|
|
openAutoPostView = new JMenuItem("Open auto post view");
|
|
quit = new JMenuItem("Quit");
|
|
openHome.addActionListener(this);
|
|
openFederated.addActionListener(this);
|
|
openNotifications.addActionListener(this);
|
|
openOwnProfile.addActionListener(this);
|
|
openProfile.addActionListener(this);
|
|
createPost.addActionListener(this);
|
|
openAutoPostView.addActionListener(this);
|
|
quit.addActionListener(this);
|
|
|
|
flipToNewestPost = new JMenuItem("Flip to newest post");
|
|
flipToNewestPost.addActionListener(this);
|
|
|
|
JMenu programMenu = new JMenu("Program");
|
|
programMenu.setMnemonic(KeyEvent.VK_P);
|
|
programMenu.add(openHome);
|
|
programMenu.add(openFederated);
|
|
programMenu.add(openNotifications);
|
|
programMenu.add(openOwnProfile);
|
|
programMenu.add(openProfile);
|
|
programMenu.add(new JSeparator());
|
|
programMenu.add(createPost);
|
|
programMenu.add(openAutoPostView);
|
|
programMenu.add(new JSeparator());
|
|
programMenu.add(quit);
|
|
JMenu timelineMenu = new JMenu("Timeline");
|
|
timelineMenu.setMnemonic(KeyEvent.VK_T);
|
|
timelineMenu.add(flipToNewestPost);
|
|
JMenuBar menuBar = new JMenuBar();
|
|
menuBar.add(programMenu);
|
|
menuBar.add(timelineMenu);
|
|
setJMenuBar(menuBar);
|
|
|
|
page = new TimelinePage();
|
|
page.posts = new Post[0];
|
|
|
|
display = new TimelineComponent(this);
|
|
display.setNextPageAvailable(false);
|
|
display.setPreviousPageAvailable(false);
|
|
setContentPane(display);
|
|
|
|
setTimelineType(TimelineType.HOME);
|
|
setIconImage(primaire.getProgramIcon());
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class
|
|
TimelineComponent extends JPanel
|
|
implements
|
|
ActionListener, KeyListener,
|
|
MouseListener, FocusListener {
|
|
|
|
private TimelineWindow
|
|
primaire;
|
|
|
|
// - -%- -
|
|
|
|
private JButton
|
|
next, prev;
|
|
|
|
private JLabel
|
|
pageLabel;
|
|
|
|
private final List<PostPreviewComponent>
|
|
postPreviews;
|
|
|
|
private boolean
|
|
hoverSelect;
|
|
|
|
private Image
|
|
backgroundImage;
|
|
|
|
// - -%- -
|
|
|
|
static final int
|
|
PREVIEW_COUNT = 10;
|
|
|
|
// ---%-@-%---
|
|
|
|
public List<PostPreviewComponent>
|
|
getPostPreviews()
|
|
{
|
|
return postPreviews;
|
|
}
|
|
|
|
public void
|
|
setPageLabel(String label)
|
|
{
|
|
assert label != null;
|
|
pageLabel.setText("" + label);
|
|
}
|
|
|
|
public void
|
|
setNextPageAvailable(boolean n) { next.setEnabled(n); }
|
|
|
|
public void
|
|
setPreviousPageAvailable(boolean n) { prev.setEnabled(n); }
|
|
|
|
public void
|
|
setHoverSelect(boolean n) { hoverSelect = n; }
|
|
|
|
public void
|
|
setBackgroundImage(Image n) { backgroundImage = n; }
|
|
|
|
public void
|
|
resetFocus() { postPreviews.get(0).requestFocusInWindow(); }
|
|
|
|
// - -%- -
|
|
|
|
protected void
|
|
paintComponent(Graphics g)
|
|
{
|
|
int w = getWidth(), h = getHeight();
|
|
g.clearRect(0, 0, w, h);
|
|
|
|
if (backgroundImage != null)
|
|
{
|
|
int b = h * 5 / 10;
|
|
int iw = backgroundImage.getWidth(this);
|
|
int ih = backgroundImage.getHeight(this);
|
|
if (ih > iw) {
|
|
ih = ih * b / iw;
|
|
iw = b;
|
|
}
|
|
else {
|
|
iw = iw * b / ih;
|
|
ih = b;
|
|
}
|
|
int x = w - iw, y = h - ih;
|
|
g.drawImage(backgroundImage, x, y, iw, ih, this);
|
|
}
|
|
|
|
((java.awt.Graphics2D)g).setRenderingHint(
|
|
java.awt.RenderingHints.KEY_ANTIALIASING,
|
|
java.awt.RenderingHints.VALUE_ANTIALIAS_ON
|
|
);
|
|
}
|
|
|
|
private void
|
|
select(Object c)
|
|
{
|
|
assert c instanceof PostPreviewComponent;
|
|
|
|
for (PostPreviewComponent p: postPreviews)
|
|
{
|
|
if (p == c) {
|
|
int index = 1 + postPreviews.indexOf(p);
|
|
primaire.previewSelected(index);
|
|
p.setSelected(true);
|
|
p.repaint();
|
|
}
|
|
else {
|
|
p.setSelected(false);
|
|
p.repaint();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void
|
|
deselect(Object c)
|
|
{
|
|
assert c instanceof PostPreviewComponent;
|
|
PostPreviewComponent p = (PostPreviewComponent)c;
|
|
|
|
p.setSelected(false);
|
|
p.repaint();
|
|
}
|
|
|
|
private void
|
|
open(Object c)
|
|
{
|
|
int o = postPreviews.indexOf(c);
|
|
assert o != -1;
|
|
primaire.previewOpened(1 + o);
|
|
}
|
|
|
|
public void
|
|
focusGained(FocusEvent eF) { select(eF.getSource()); }
|
|
|
|
public void
|
|
focusLost(FocusEvent eF) { deselect(eF.getSource()); }
|
|
|
|
public void
|
|
mouseClicked(MouseEvent eM)
|
|
{
|
|
if (eM.getClickCount() == 2) open(eM.getSource());
|
|
else select(eM.getSource());
|
|
}
|
|
|
|
public void
|
|
mouseEntered(MouseEvent eM)
|
|
{
|
|
if (!hoverSelect) return;
|
|
mouseClicked(eM);
|
|
}
|
|
|
|
public void
|
|
mouseExited(MouseEvent eM)
|
|
{
|
|
if (!hoverSelect) return;
|
|
deselect(eM.getSource());
|
|
}
|
|
|
|
// (知) First time I'm using these two..!
|
|
|
|
public void
|
|
keyPressed(KeyEvent eK)
|
|
{
|
|
if (eK.getKeyCode() != KeyEvent.VK_ENTER) return;
|
|
|
|
PostPreviewComponent selected = null;
|
|
for (PostPreviewComponent c: postPreviews)
|
|
if (c.isSelected()) selected = c;
|
|
|
|
if (selected == null) return;
|
|
open(selected);
|
|
}
|
|
|
|
public void
|
|
actionPerformed(ActionEvent eA)
|
|
{
|
|
Object src = eA.getSource();
|
|
|
|
if (src == next) primaire.showNextPage();
|
|
else if (src == prev) primaire.showPreviousPage();
|
|
/*
|
|
* I think the page previews will just forward to us.
|
|
* But I think they'll have to tell us where they are
|
|
* in the list somehow, because we need to show only
|
|
* one post as selected.
|
|
*/
|
|
}
|
|
|
|
|
|
public void
|
|
mousePressed(MouseEvent eM) { }
|
|
|
|
public void
|
|
mouseReleased(MouseEvent eM) { }
|
|
|
|
public void
|
|
keyTyped(KeyEvent eK) { }
|
|
|
|
public void
|
|
keyReleased(KeyEvent eK) { }
|
|
|
|
// ---%-@-%---
|
|
|
|
TimelineComponent(TimelineWindow primaire)
|
|
{
|
|
this.primaire = primaire;
|
|
|
|
postPreviews = new ArrayList<>(PREVIEW_COUNT);
|
|
hoverSelect = true;
|
|
|
|
prev = new JButton("<");
|
|
next = new JButton(">");
|
|
prev.setEnabled(false);
|
|
next.setEnabled(false);
|
|
prev.setMnemonic(KeyEvent.VK_PAGE_UP);
|
|
next.setMnemonic(KeyEvent.VK_PAGE_DOWN);
|
|
prev.addActionListener(this);
|
|
next.addActionListener(this);
|
|
|
|
pageLabel = new JLabel("0");
|
|
|
|
Box bottom = Box.createHorizontalBox();
|
|
bottom.setBackground(null);
|
|
bottom.setOpaque(false);
|
|
bottom.add(Box.createGlue());
|
|
bottom.add(prev);
|
|
bottom.add(Box.createHorizontalStrut(8));
|
|
bottom.add(next);
|
|
|
|
JPanel centre = new JPanel();
|
|
centre.setOpaque(false);
|
|
centre.setLayout(new GridBagLayout());
|
|
GridBagConstraints constraints = new GridBagConstraints();
|
|
constraints.fill = GridBagConstraints.HORIZONTAL;
|
|
constraints.gridx = 0;
|
|
constraints.weightx = 1;
|
|
constraints.insets = new Insets(4, 0, 4, 0);
|
|
for (int n = PREVIEW_COUNT; n > 0; --n)
|
|
{
|
|
PostPreviewComponent c = new PostPreviewComponent();
|
|
c.reset();
|
|
c.addMouseListener(this);
|
|
c.addFocusListener(this);
|
|
c.addKeyListener(this);
|
|
centre.add(c, constraints);
|
|
postPreviews.add(c);
|
|
}
|
|
|
|
setLayout(new BorderLayout());
|
|
add(centre, BorderLayout.CENTER);
|
|
add(bottom, BorderLayout.SOUTH);
|
|
|
|
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class
|
|
PostPreviewComponent extends JComponent {
|
|
|
|
private JLabel
|
|
topLeft, topRight, bottom;
|
|
|
|
private boolean
|
|
selected;
|
|
|
|
// ---%-@-%---
|
|
|
|
public void
|
|
setTopLeft(String text) { topLeft.setText(text); }
|
|
|
|
public void
|
|
setTopRight(String text) { topRight.setText(text); }
|
|
|
|
public void
|
|
setBottom(String text) { bottom.setText(text); }
|
|
|
|
public void
|
|
reset()
|
|
{
|
|
setTopLeft(" ");
|
|
setTopRight(" ");
|
|
setBottom(" ");
|
|
}
|
|
|
|
public void
|
|
setSelected(boolean selected) { this.selected = selected; }
|
|
|
|
public boolean
|
|
isSelected() { return selected; }
|
|
|
|
// - -%- -
|
|
|
|
protected void
|
|
paintComponent(Graphics g)
|
|
{
|
|
if (selected)
|
|
{
|
|
g.setColor(new Color(0, 0, 0, 25));
|
|
g.fillRect(0, 0, getWidth(), getHeight());
|
|
}
|
|
}
|
|
|
|
// ---%-@-%---
|
|
|
|
public
|
|
PostPreviewComponent()
|
|
{
|
|
Font f1 = new Font("Dialog", Font.PLAIN, 12);
|
|
Font f2 = new Font("Dialog", Font.ITALIC, 12);
|
|
Font f3 = new Font("Dialog", Font.PLAIN, 14);
|
|
|
|
topLeft = new JLabel();
|
|
topLeft.setFont(f1);
|
|
topLeft.setOpaque(false);
|
|
|
|
topRight = new JLabel();
|
|
topRight.setHorizontalAlignment(JLabel.RIGHT);
|
|
topRight.setFont(f2);
|
|
topRight.setOpaque(false);
|
|
|
|
Box top = Box.createHorizontalBox();
|
|
top.setOpaque(false);
|
|
top.add(topLeft);
|
|
top.add(Box.createGlue());
|
|
top.add(topRight);
|
|
|
|
bottom = new JLabel();
|
|
bottom.setFont(f3);
|
|
bottom.setOpaque(false);
|
|
|
|
JPanel left = new JPanel();
|
|
left.setOpaque(false);
|
|
left.setLayout(new BorderLayout());
|
|
left.add(top, BorderLayout.NORTH);
|
|
left.add(bottom);
|
|
|
|
setFocusable(true);
|
|
setOpaque(false);
|
|
setLayout(new BorderLayout());
|
|
add(left);
|
|
}
|
|
|
|
}
|