biskuteri-cafe-JKomasto2/ComposeWindow.java

969 lines
24 KiB
Java
Raw Normal View History

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JLabel;
2021-07-28 22:23:42 +02:00
import javax.swing.JComboBox;
import javax.swing.JButton;
import javax.swing.JSeparator;
import javax.swing.Box;
import javax.swing.BorderFactory;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.UIManager;
import javax.swing.JToggleButton;
import javax.swing.ButtonGroup;
import javax.swing.border.Border;
import javax.swing.JPopupMenu;
import javax.swing.JMenuItem;
import javax.swing.JFileChooser;
import javax.swing.ImageIcon;
import javax.swing.text.JTextComponent;
import java.nio.file.Files;
import java.awt.GridLayout;
import java.awt.BorderLayout;
import java.awt.Dimension;
2021-07-28 22:23:42 +02:00
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.event.KeyListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseEvent;
2021-07-28 22:23:42 +02:00
import java.awt.Cursor;
import java.awt.Color;
import java.awt.Font;
import java.awt.Insets;
import java.awt.FlowLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Image;
import java.io.File;
import javax.swing.event.CaretListener;
import javax.swing.event.CaretEvent;
import java.util.List;
import java.util.ArrayList;
import cafe.biskuteri.hinoki.Tree;
import java.io.IOException;
class
ComposeWindow extends JFrame {
private JKomasto
primaire;
private MastodonApi
api;
// - -%- -
2021-07-17 11:46:27 +02:00
private Composition
composition;
private ComposeComponent
contentsDisplay;
private AttachmentsComponent
attachmentsDisplay;
private JTabbedPane
tabs;
2021-07-17 11:46:27 +02:00
// ---%-@-%---
public synchronized void
setComposition(Composition composition)
2021-07-28 22:23:42 +02:00
{
assert composition != null;
this.composition = composition;
syncDisplayToComposition();
2021-07-28 22:23:42 +02:00
}
public synchronized void
newComposition()
{
composition = new Composition();
composition.text = "";
2022-04-28 03:56:25 +02:00
composition.visibility = PostVisibility.UNLISTED;
composition.replyToPostId = null;
composition.contentWarning = null;
syncDisplayToComposition();
}
public synchronized void
2021-07-28 22:23:42 +02:00
submit()
2021-07-17 11:46:27 +02:00
{
syncCompositionToDisplay();
if (composition.replyToPostId != null)
assert !composition.replyToPostId.trim().isEmpty();
if (composition.contentWarning != null)
assert !composition.contentWarning.trim().isEmpty();
//tabs.setCursor(new Cursor(Cursor.WAIT_CURSOR));
/*
* setCursor only works for components that are enabled.
* I don't think there's any technical reason for this,
* but it's what it is.. rely on the enablement visuals
* or a statusbar to indicate to user.
*
* If we really wanted it, I suspect we could use a glass
* pane (or a scroll pane with no scrolling) to have an
* enabled pass-through on top.
*
* Technically contentsDisplay and attachmentsDisplay
* themselves aren't disabled. But disabling the tab pane
* covers both the tab area and content area. We can't
* just the tab area, except maybe by disabling the
* individual tabs.
*/
tabs.setEnabled(false);
tabs.setSelectedComponent(contentsDisplay);
contentsDisplay.setSubmitting(true);
tabs.paintImmediately(tabs.getBounds());
boolean uploadsOkay = true;
for (Attachment a: composition.attachments)
{
if (a.id != null) continue;
// Assume it had already been uploaded.
api.uploadFile(
a.uploadee, a.description,
new RequestListener() {
public void
connectionFailed(IOException eIo)
{
JOptionPane.showMessageDialog(
ComposeWindow.this,
"Tried to upload attachment, failed..."
+ "\n" + eIo.getMessage()
);
}
public void
requestFailed(int httpCode, Tree<String> json)
{
JOptionPane.showMessageDialog(
ComposeWindow.this,
"Tried to upload attachment, failed..."
+ "\n" + json.get("error").value
+ "\n(HTTP code: " + httpCode + ")"
);
}
public void
requestSucceeded(Tree<String> json)
{
a.id = json.get("id").value;
}
});
uploadsOkay &= a.id != null;
}
if (!uploadsOkay)
{
contentsDisplay.setSubmitting(false);
tabs.setEnabled(true);
//tabs.setCursor(null);
return;
}
int amt = composition.attachments.length;
String[] mediaIDs = new String[amt];
for (int o = 0; o < mediaIDs.length; ++o)
mediaIDs[o] = composition.attachments[o].id;
api.submit(
composition.text, composition.visibility,
composition.replyToPostId, composition.contentWarning,
mediaIDs,
new RequestListener() {
public void
connectionFailed(IOException eIo)
{
JOptionPane.showMessageDialog(
ComposeWindow.this,
"Tried to submit post, failed..."
+ "\n" + eIo.getMessage()
);
}
public void
requestFailed(int httpCode, Tree<String> json)
{
JOptionPane.showMessageDialog(
ComposeWindow.this,
"Tried to submit post, failed..."
+ "\n" + json.get("error").value
+ "\n(HTTP error code: " + httpCode + ")"
);
}
public void
requestSucceeded(Tree<String> json)
{
newComposition();
}
}
);
contentsDisplay.setSubmitting(false);
tabs.setEnabled(true);
tabs.setCursor(null);
}
// - -%- -
2021-07-28 22:23:42 +02:00
private synchronized void
syncDisplayToComposition()
{
ComposeComponent d1 = contentsDisplay;
d1.setText(composition.text);
d1.setReplyToPostId(composition.replyToPostId);
d1.setVisibility(stringFor(composition.visibility));
d1.setContentWarning(composition.contentWarning);
AttachmentsComponent d2 = attachmentsDisplay;
d2.setAttachments(composition.attachments);
}
private synchronized void
syncCompositionToDisplay()
{
Composition c = composition;
ComposeComponent d1 = contentsDisplay;
c.text = d1.getText();
c.visibility = visibilityFrom(d1.getVisibility());
c.replyToPostId = nonEmpty(d1.getReplyToPostId());
c.contentWarning = nonEmpty(d1.getContentWarning());
AttachmentsComponent d2 = attachmentsDisplay;
c.attachments = d2.getAttachments();
}
// - -%- -
private static String
nonEmpty(String s)
{
if (s.trim().isEmpty()) return null;
return s;
2021-07-17 11:46:27 +02:00
}
// ---%-@-%---
ComposeWindow(JKomasto primaire)
{
super("Submit a new post");
this.primaire = primaire;
this.api = primaire.getMastodonApi();
Dimension sz = new Dimension(360, 270);
2021-07-16 11:46:17 +02:00
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
contentsDisplay = new ComposeComponent(this);
attachmentsDisplay = new AttachmentsComponent(this);
newComposition();
2021-07-17 12:03:59 +02:00
tabs = new JTabbedPane();
contentsDisplay.setPreferredSize(sz);
tabs.addTab("Text", contentsDisplay);
tabs.addTab("Media", attachmentsDisplay);
setBackground((Color)UIManager.get("TabbedPane.tabAreaBackground"));
setContentPane(tabs);
pack();
setIconImage(primaire.getProgramIcon());
}
// - -%- -
private static final String
stringFor(PostVisibility visibility)
{
switch (visibility)
{
case PUBLIC: return "Public";
case UNLISTED: return "Unlisted";
case FOLLOWERS: return "Followers";
case MENTIONED: return "Mentioned";
}
assert false; return null;
}
private static final PostVisibility
visibilityFrom(String string)
{
if (string.equals("Public"))
return PostVisibility.PUBLIC;
if (string.equals("Unlisted"))
return PostVisibility.UNLISTED;
if (string.equals("Followers"))
return PostVisibility.FOLLOWERS;
if (string.equals("Mentioned"))
return PostVisibility.MENTIONED;
assert false; return null;
}
}
class
2021-07-28 22:23:42 +02:00
ComposeComponent extends JPanel
implements ActionListener, CaretListener, KeyListener {
2021-07-28 22:23:42 +02:00
private ComposeWindow
primaire;
// - -%- -
private JTextArea
text;
private JTextField
reply, contentWarning;
private JLabel
textLength;
2021-07-28 22:23:42 +02:00
private JComboBox<String>
visibility;
private JButton
submit;
// ---%-@-%---
2021-07-28 22:23:42 +02:00
public void
setText(String text)
{
this.text.setText(text);
}
2021-07-28 22:23:42 +02:00
public void
setReplyToPostId(String postId)
{
this.reply.setText(postId);
}
public void
setVisibility(String visibility)
{
if (visibility.equals("Public"))
this.visibility.setSelectedIndex(0);
else if (visibility.equals("Unlisted"))
this.visibility.setSelectedIndex(1);
else if (visibility.equals("Followers"))
this.visibility.setSelectedIndex(2);
else if (visibility.equals("Mentioned"))
this.visibility.setSelectedIndex(3);
2021-07-28 22:23:42 +02:00
}
public void
setContentWarning(String contentWarning)
{
this.contentWarning.setText(contentWarning);
}
2021-07-28 22:23:42 +02:00
public String
getText()
{
return text.getText();
}
2021-07-28 22:23:42 +02:00
public String
getReplyToPostId()
{
return reply.getText();
}
public String
getContentWarning()
{
return contentWarning.getText();
}
2021-07-28 22:23:42 +02:00
public String
getVisibility()
2021-07-28 22:23:42 +02:00
{
return (String)visibility.getSelectedItem();
}
public void
setSubmitting(boolean submitting)
2021-07-28 22:23:42 +02:00
{
if (submitting)
{
text.setEnabled(false);
visibility.setEnabled(false);
submit.setEnabled(false);
}
else
{
text.setEnabled(true);
visibility.setEnabled(true);
submit.setEnabled(true);
}
2021-07-28 22:23:42 +02:00
}
// - -%- -
2021-07-28 22:23:42 +02:00
public void
actionPerformed(ActionEvent eA)
{
if (eA.getSource() == submit)
primaire.submit();
}
2021-07-28 22:23:42 +02:00
public void
caretUpdate(CaretEvent eCa) { updateTextLength(); }
public void
keyPressed(KeyEvent eK)
{
boolean esc = eK.getKeyCode() == KeyEvent.VK_ESCAPE;
if (esc)
{
Container fcr = getFocusCycleRootAncestor();
fcr.getFocusTraversalPolicy()
.getComponentAfter(fcr, text)
.requestFocusInWindow();
}
else updateTextLength();
}
public void
keyReleased(KeyEvent eK) { }
public void
keyTyped(KeyEvent eK) { }
private void
updateTextLength()
{
int length = text.getText().length();
/*
* The web interface doesn't do this expensive thing.
* It has an upwards counter, incremented by I'm not
* sure what. Presumably they have some control over
* the text input. I'd rather not, cause I use a
* Japanese IME, I'm going to see how laggy this is.
* It raises our app's system requirements, but, I was
* going to transition it to multithreading anyways,
* I don't think we're going to be very cheap.. Which
* sucks, but the Mastodon API is not helping us here.
*/
textLength.setText(Integer.toString(length));
/*
* Another thing I could do is temporarily move the
* caret to the end and then find its position, then
* seek back. Not sure how much that would help, but
* if this is too laggy, that's what I'd try next.
*/
}
2021-07-28 22:23:42 +02:00
// ---%-@-%---
ComposeComponent(ComposeWindow primaire)
{
this.primaire = primaire;
Border b1 = BorderFactory.createEmptyBorder(8, 8, 8, 8);
Border b2 = BorderFactory.createEmptyBorder(4, 4, 4, 4);
Border b3 = BorderFactory.createLineBorder(Color.GRAY);
Border bc = BorderFactory.createCompoundBorder(b3, b2);
TextActionPopupMenu textActionPopup;
textActionPopup = new TextActionPopupMenu();
reply = new JTextField();
JLabel replyLabel = new JLabel("In reply to: ");
replyLabel.setLabelFor(reply);
reply.addMouseListener(textActionPopup);
contentWarning = new JTextField();
JLabel cwLabel = new JLabel("Content warning: ");
cwLabel.setLabelFor(contentWarning);
contentWarning.addMouseListener(textActionPopup);
JPanel top = new JPanel();
top.setOpaque(false);
top.setLayout(new GridLayout(2, 2, 8, 0));
top.add(replyLabel);
top.add(reply);
top.add(cwLabel);
top.add(contentWarning);
textLength = new JLabel("0");
textLength.setFont(textLength.getFont().deriveFont(14f));
2021-07-28 22:23:42 +02:00
visibility = new JComboBox<>(new String[] {
"Public",
"Unlisted",
2021-07-28 22:23:42 +02:00
"Followers",
"Mentioned"
// Where should we be saving strings..
2021-07-28 22:23:42 +02:00
});
visibility.setPreferredSize(new Dimension(48, 24));
2021-07-28 22:23:42 +02:00
submit = new JButton("Submit");
2021-07-28 22:23:42 +02:00
submit.addActionListener(this);
Box bottom = Box.createHorizontalBox();
bottom.add(Box.createGlue());
bottom.add(textLength);
bottom.add(Box.createHorizontalStrut(12));
2021-07-28 22:23:42 +02:00
bottom.add(visibility);
bottom.add(Box.createHorizontalStrut(12));
bottom.add(submit);
text = new JTextArea();
text.setLineWrap(true);
text.setWrapStyleWord(true);
text.setFont(text.getFont().deriveFont(16f));
text.setBorder(bc);
text.addCaretListener(this);
text.addKeyListener(this);
text.addMouseListener(textActionPopup);
setLayout(new BorderLayout(0, 8));
add(top, BorderLayout.NORTH);
add(text, BorderLayout.CENTER);
add(bottom, BorderLayout.SOUTH);
setBorder(b1);
}
}
class
AttachmentsComponent extends JPanel
implements ActionListener {
private ComposeWindow
primaire;
// - -%- -
private List<Attachment>
working;
private JPanel
selections;
private JToggleButton
attachment1,
attachment2,
attachment3,
attachment4;
private JButton
add;
private JButton
delete,
revert;
private JLabel
descriptionLabel;
private JTextArea
description;
private JFileChooser
chooser;
// ---%-@-%---
public void
setAttachments(Attachment[] n)
{
working.clear();
if (n != null) for (Attachment attachment: n)
{
working.add(attachment);
}
updateButtons();
}
public Attachment[]
getAttachments()
{
return working.toArray(new Attachment[0]);
}
// - -%- -
private void
updateButtons()
{
Dimension sz = add.getPreferredSize();
selections.removeAll();
if (working.size() > 0) selections.add(attachment1);
if (working.size() > 1) selections.add(attachment2);
if (working.size() > 2) selections.add(attachment3);
if (working.size() > 3) selections.add(attachment4);
if (working.size() < 4) selections.add(add);
if (working.size() > 3)
{
Image i = working.get(3).image;
attachment4.setIcon(new ImageIcon(i));
}
else if (working.size() > 2)
{
Image i = working.get(2).image;
attachment3.setIcon(new ImageIcon(i));
}
else if (working.size() > 1)
{
Image i = working.get(1).image;
attachment2.setIcon(new ImageIcon(i));
}
else if (working.size() > 0)
{
Image i = working.get(0).image;
attachment1.setIcon(new ImageIcon(i));
}
attachment4.setSelected(working.size() > 3);
attachment3.setSelected(working.size() > 2);
attachment2.setSelected(working.size() > 1);
attachment1.setSelected(working.size() > 0);
int bw = sz.width;
int hgap = 4;
int count = selections.getComponents().length;
int w = count * bw + (count - 1) * hgap;
int h = bw;
selections.setPreferredSize(new Dimension(w, h));
selections.setMaximumSize(new Dimension(w, h));
selections.revalidate();
}
public void
actionPerformed(ActionEvent eA)
{
Object src = eA.getSource();
if (src == add)
{
int r = chooser.showOpenDialog(this);
if (r != JFileChooser.APPROVE_OPTION) return;
File f = chooser.getSelectedFile();
Attachment a = new Attachment();
a.uploadee = f;
String mime = "", primary = "";
try
{
mime = Files.probeContentType(f.toPath());
primary = mime.split("/")[0];
// Too lazy to instantiate a
// javax.activation.MimeType.
}
catch (IOException eIo) { }
if (primary.equals("image"))
{
String urlr = f.toURI().toString();
a.image = ImageApi.remote(urlr);
}
working.add(a);
updateButtons();
}
if (src == delete)
{
if (attachment1.isSelected())
{
assert working.size() > 0;
working.remove(0);
}
if (attachment2.isSelected())
{
assert working.size() > 1;
working.remove(1);
}
if (attachment3.isSelected())
{
assert working.size() > 2;
working.remove(2);
}
if (attachment4.isSelected())
{
assert working.size() > 3;
working.remove(3);
}
updateButtons();
return;
}
if (src != add && selections.isAncestorOf((Component)src))
{
int iCurr = getSelectedIndex();
int iNext = getIndex(src);
System.err.println(iCurr + ":" + iNext);
assert iNext != 0;
if (iCurr == iNext) {
save();
return;
}
if (iCurr != 0) save();
load(iNext);
}
if (src == revert)
{
assert getSelectedIndex() != 0;
// Should the controls be disabled until
// there is an attachment?
load(getSelectedIndex());
}
}
private int
getIndex(Object src)
{
if (src == attachment4) return 4;
if (src == attachment3) return 3;
if (src == attachment2) return 2;
if (src == attachment1) return 1;
return 0;
}
private int
getSelectedIndex()
{
if (attachment4.isSelected()) return 4;
if (attachment3.isSelected()) return 3;
if (attachment2.isSelected()) return 2;
if (attachment1.isSelected()) return 1;
return 0;
}
private void
save()
{
int index = getSelectedIndex();
assert index != 0;
Attachment a = working.get(index - 1);
a.description = this.description.getText();
}
private void
load(int index)
{
assert index > 0;
assert working.size() >= index;
Attachment a = working.get(index - 1);
this.description.setText(a.description);
}
// ---%-@-%---
AttachmentsComponent(ComposeWindow primaire)
{
this.primaire = primaire;
Border b1 = BorderFactory.createEmptyBorder(8, 8, 8, 8);
Border b2 = BorderFactory.createEmptyBorder(4, 4, 4, 4);
Border b3 = BorderFactory.createLineBorder(Color.GRAY);
Border b4 = BorderFactory.createEmptyBorder(4, 8, 8, 8);
Border b5 = BorderFactory.createEtchedBorder();
Border bc1 = BorderFactory.createCompoundBorder(b3, b2);
Border bc2 = BorderFactory.createCompoundBorder(b4, b2);
TextActionPopupMenu textActionPopup;
textActionPopup = new TextActionPopupMenu();
chooser = new JFileChooser();
add = new JButton("+");
add.setPreferredSize(new Dimension(32, 32));
add.setMargin(new Insets(0, 0, 0, 0));
add.addActionListener(this);
attachment1 = new JToggleButton("1");
attachment2 = new JToggleButton("2");
attachment3 = new JToggleButton("3");
attachment4 = new JToggleButton("4");
attachment1.setMargin(add.getMargin());
attachment2.setMargin(add.getMargin());
attachment3.setMargin(add.getMargin());
attachment4.setMargin(add.getMargin());
attachment1.addActionListener(this);
attachment2.addActionListener(this);
attachment3.addActionListener(this);
attachment4.addActionListener(this);
selections = new JPanel();
selections.setOpaque(false);
selections.setLayout(new GridLayout(1, 0, 4, 0));
working = new ArrayList<Attachment>();
ButtonGroup selectionsGroup = new ButtonGroup();
selectionsGroup.add(attachment1);
selectionsGroup.add(attachment2);
selectionsGroup.add(attachment3);
selectionsGroup.add(attachment4);
updateButtons();
JButton del = new JButton("D");
JButton rev = new JButton("R");
JButton ml = new JButton("");
JButton mr = new JButton("");
del.setMargin(new Insets(0, 0, 0, 0));
//ml.setMargin(new Insets(0, 0, 0, 0));
//mr.setMargin(new Insets(0, 0, 0, 0));
rev.setMargin(new Insets(0, 0, 0, 0));
JPanel actions = new JPanel();
actions.setOpaque(false);
actions.setLayout(new GridLayout(1, 4, 4, 4));
actions.add(del);
actions.add(rev);
actions.add(ml);
actions.add(mr);
actions.setPreferredSize(new Dimension(108, 24));
actions.setMaximumSize(new Dimension(108, 24));
delete = new JButton("Delete");
revert = new JButton("Revert");
delete.addActionListener(this);
revert.addActionListener(this);
Box top = Box.createHorizontalBox();
top.add(selections);
top.add(Box.createGlue());
Box bottom = Box.createHorizontalBox();
bottom.add(ml);
bottom.add(mr);
bottom.add(Box.createHorizontalStrut(8));
bottom.add(delete);
bottom.add(Box.createGlue());
bottom.add(revert);
description = new JTextArea();
description.setLineWrap(true);
description.setWrapStyleWord(true);
java.awt.Font f = description.getFont();
description.setFont(f.deriveFont(16f));
description.setBorder(bc1);
description.addMouseListener(textActionPopup);
descriptionLabel = new JLabel("Description");
descriptionLabel.setLabelFor(description);
JPanel row1 = new JPanel();
row1.setOpaque(false);
row1.setLayout(new BorderLayout());
row1.add(descriptionLabel, BorderLayout.NORTH);
row1.add(description, BorderLayout.CENTER);
/*
Box row2 = Box.createHorizontalBox();
row2.add(Box.createGlue());
row2.add(delete);
row2.add(Box.createHorizontalStrut(8));
row2.add(revert);
*/
Box centre = Box.createVerticalBox();
centre.setBorder(b4);
centre.add(row1);
setLayout(new BorderLayout(8, 8));
add(centre, BorderLayout.CENTER);
add(top, BorderLayout.NORTH);
add(bottom, BorderLayout.SOUTH);
setBorder(b1);
}
}
class
TextActionPopupMenu extends JPopupMenu
implements MouseListener, ActionListener {
private JMenuItem
copy,
cut,
paste;
private long
pressed;
// ---%-@-%---
public void
mousePressed(MouseEvent eM)
{
assert eM.getSource() instanceof JTextComponent;
if (!eM.isPopupTrigger()) return;
show((Component)eM.getSource(), eM.getX(), eM.getY());
}
public void
mouseReleased(MouseEvent eM)
{
if (eM.getClickCount() == 0) setVisible(false);
}
public void
mouseClicked(MouseEvent eM) { }
public void
mouseEntered(MouseEvent eM) { }
public void
mouseExited(MouseEvent eM) { }
public void
actionPerformed(ActionEvent eA)
{
assert getInvoker() instanceof JTextComponent;
JTextComponent inv = (JTextComponent)getInvoker();
if (eA.getSource() == copy) inv.copy();
if (eA.getSource() == cut) inv.cut();
if (eA.getSource() == paste) inv.paste();
}
// ---%-@-%---
public
TextActionPopupMenu()
{
copy = new JMenuItem("Copy");
cut = new JMenuItem("Cut");
paste = new JMenuItem("Paste");
copy.addActionListener(this);
cut.addActionListener(this);
paste.addActionListener(this);
add(cut);
add(copy);
add(paste);
}
}