diff --git a/ComposeWindow.java b/ComposeWindow.java index 2858fc0..0089040 100644 --- a/ComposeWindow.java +++ b/ComposeWindow.java @@ -130,7 +130,9 @@ ComposeWindow extends JFrame { if (a.id != null) continue; // Assume it had already been uploaded. - api.uploadFile(a.uploadee, new RequestListener() { + api.uploadFile( + a.uploadee, a.description, + new RequestListener() { public void connectionFailed(IOException eIo) @@ -561,9 +563,6 @@ implements ActionListener { private JPanel selections; - private ButtonGroup - selectionsGroup; - private JToggleButton attachment1, attachment2, @@ -621,29 +620,29 @@ implements ActionListener { if (working.size() > 3) { - attachment4.doClick(); Image i = working.get(3).image; attachment4.setIcon(new ImageIcon(i)); } else if (working.size() > 2) { - attachment3.doClick(); Image i = working.get(2).image; attachment3.setIcon(new ImageIcon(i)); } else if (working.size() > 1) { - attachment2.doClick(); Image i = working.get(1).image; attachment2.setIcon(new ImageIcon(i)); } else if (working.size() > 0) { - attachment1.doClick(); Image i = working.get(0).image; attachment1.setIcon(new ImageIcon(i)); } - else selectionsGroup.clearSelection(); + + 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; @@ -661,11 +660,6 @@ implements ActionListener { { Object src = eA.getSource(); - if (false) - { - // Clicked on filled attachment button. - } - if (src == add) { int r = chooser.showOpenDialog(this); @@ -697,40 +691,93 @@ implements ActionListener { if (src == delete) { - Object sm = selectionsGroup.getSelection(); - if (sm == attachment1.getModel()) + if (attachment1.isSelected()) { assert working.size() > 0; working.remove(0); - updateButtons(); } - if (sm == attachment2.getModel()) + if (attachment2.isSelected()) { assert working.size() > 1; working.remove(1); - updateButtons(); } - if (sm == attachment3.getModel()) + if (attachment3.isSelected()) { assert working.size() > 2; working.remove(2); - updateButtons(); } - if (sm == attachment4.getModel()) + if (attachment4.isSelected()) { assert working.size() > 3; working.remove(3); - updateButtons(); } + 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) { - return; + 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) @@ -762,17 +809,20 @@ implements ActionListener { 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(); - selectionsGroup = new ButtonGroup(); + ButtonGroup selectionsGroup = new ButtonGroup(); selectionsGroup.add(attachment1); selectionsGroup.add(attachment2); selectionsGroup.add(attachment3); selectionsGroup.add(attachment4); - // Have to add selection listener to button group updateButtons(); JButton del = new JButton("D"); diff --git a/ImageApi.java b/ImageApi.java index d41d1e8..22eb144 100644 --- a/ImageApi.java +++ b/ImageApi.java @@ -2,6 +2,11 @@ import javax.swing.ImageIcon; import java.awt.Image; import java.awt.Toolkit; +import java.awt.MediaTracker; +import java.awt.Graphics; +import java.awt.Component; +import java.awt.image.ImageObserver; +import java.awt.image.BufferedImage; import java.net.URL; import java.net.MalformedURLException; diff --git a/MastodonApi.java b/MastodonApi.java index 4624b18..9ac4b6c 100644 --- a/MastodonApi.java +++ b/MastodonApi.java @@ -435,9 +435,10 @@ MastodonApi { } public void - uploadFile(File file, RequestListener handler) + uploadFile(File file, String alt, RequestListener handler) { assert file != null; + assert alt != null; assert file.canRead(); String bct = @@ -496,10 +497,12 @@ MastodonApi { String url = instanceUrl + "/api/v1/media/"; try { - URL endpoint = new URL(url); + String s1 = "?description=" + encode(alt); + + URL endpoint = new URL(url + s1); HttpURLConnection conn = cast(endpoint.openConnection()); - String s1 = "Bearer " + token; - conn.setRequestProperty("Authorization", s1); + String s2 = "Bearer " + token; + conn.setRequestProperty("Authorization", s2); conn.setDoOutput(true); conn.setRequestMethod("POST"); conn.setFixedLengthStreamingMode(contentLength); diff --git a/PostWindow.java b/PostWindow.java index bfd42e4..8b42487 100644 --- a/PostWindow.java +++ b/PostWindow.java @@ -429,54 +429,7 @@ implements ActionListener { setAuthorId(String n) { authorId.setText(n); } public void - setAuthorAvatar(Image n) - { - try { - MediaTracker mt = new MediaTracker(this); - mt.addImage(n, 0); - mt.waitForID(0); - } - catch (InterruptedException eIt) { - assert false; - } - /* - * Blocks until the image loads. We have to do this because - * there is an unfixed JDK bug in Image#getScaledInstance - * (which RoundButton uses) where when you pass it an AWT - * image, it incorrectly sets the ColorModel's transfer type, - * causing a ClassCastException, and returns a valid image - * with blank data. It seems using loaded images does not - * replicate the issue, which would explain why the bug - * hasn't been found and fixed.. - * - * For future readers wondering. JDK 11, up to JDK 15. - * java.awt.image.ColorModel#getAlpha(:845), who was called - * sun.awt.image.ImageRepresentation#convertToRGB, who was - * called by AreaAveragingScaleFilter trying to #setPixels. - * Cause RoundButton was using Image.SCALE_SMOOTH. - * - * I guess it kind of makes sense, why would I try to get a - * scaled instance of an image that hadn't finished loading. - * An intelligent class would render the awt.Image as-is until - * it has completely loaded, then call #getScaledInstance - * instead. I won't have RoundButton do that for now, as - * my other classes like RichTextPane3 might have this too. - * - * If you asked me how I'd generalise the solution. My - * expectation had been that #getScaledInstance waits for - * the image too, returning a null image until it's done. - * So I'd put a #getScaledInstance in ImageApi, which does - * this, using MediaTracker. That assumes what I'm doing - * right now definitely works. - */ - /* - * (悪) It doesn't work. I might be forced to use - * BufferedImage with ImageIO. Or have - * ImageApi#getScaledInstance spam until it works. - */ - - profile.setImage(n); - } + setAuthorAvatar(Image n) { profile.setImage(n); } public void setDate(String n) { date.setText(n); } diff --git a/TwoToggleButton.java b/TwoToggleButton.java index 3906675..98314f7 100644 --- a/TwoToggleButton.java +++ b/TwoToggleButton.java @@ -8,6 +8,8 @@ import java.awt.Graphics; import java.awt.Dimension; import java.awt.Shape; import java.awt.Rectangle; +import java.awt.Component; +import java.awt.image.BufferedImage; import java.awt.geom.Ellipse2D; import java.awt.event.MouseListener; import java.awt.event.MouseEvent; @@ -255,6 +257,7 @@ implements KeyListener, MouseListener, FocusListener { // - -%- - private Image + copy, scaled; private int @@ -267,22 +270,78 @@ implements KeyListener, MouseListener, FocusListener { selectedOverlay, disabledOverlay; + private static Shape + roundClip; + // ---%-@-%--- public void setImage(Image n) { image = n; + copy = null; scaled = null; + + if (image != null) + { + image.flush(); + prepareImage(image, this); + } } // - -%- - public boolean - imageUpdate(Image i, int f, int x, int y, int w, int h) + imageUpdate(Image img, int f, int x, int y, int w, int h) { - if ((f | WIDTH) != 0) repaint(); - return super.imageUpdate(i, f, x, y, w, h); + // AbstractButton overrode this to refuse updates for + // images that aren't the button's icon. We don't use + // the icon, so we're overriding it back. Also, we have + // some async work to do regarding the images. + + if ((f & (ABORT|ERROR)) != 0) return false; + + boolean all = (f & ALLBITS) != 0; + boolean frame = (f & FRAMEBITS) != 0; + boolean some = (f & SOMEBITS) != 0; + + if (frame && img != this.image) return false; + + if (img == this.image && (all || frame)) + { + int ow = img.getWidth(null); + int oh = img.getHeight(null); + + if (copy == null) + { + int type = BufferedImage.TYPE_INT_ARGB; + copy = new BufferedImage(ow, oh, type); + } + + Graphics g = copy.getGraphics(); + g.drawImage(img, 0, 0, null); + g.dispose(); + + int algo = Image.SCALE_SMOOTH; + Rectangle b = roundClip.getBounds(); + int sw = ow > oh ? -1 : b.width; + int sh = oh > ow ? -1 : b.height; + scaled = copy.getScaledInstance(sw, sh, algo); + /* + * We create a scaled instance from a BufferedImage copy + * rather than this.image directly, to avoid a ClassCast + * Exception bug in the JDK, where ColorModel was + * incorrectly casting an int array of input data into + * a byte array. I'm not sure why that bug exists nor + * why they haven't noticed it, but. + */ + repaint(); + } + if (img == scaled && (some || all)) + { + repaint(); + } + return all ? false : true; } protected void @@ -294,34 +353,13 @@ implements KeyListener, MouseListener, FocusListener { if (isFocusOwner()) g.drawImage(selectedOverlay, 0, 0, this); - if (image == null) return; - - int ow = image.getWidth(this); - int oh = image.getHeight(this); - int nx = 6; - int ny = 6; - int nw = button.getWidth(this) - 12; - int nh = button.getHeight(this) - 12; - Shape defaultClip, roundClip; - defaultClip = g.getClip(); - roundClip = new Ellipse2D.Float(nx, ny, nw, nh); - - if (scaled == null) - { - int sw = ow > oh ? -1 : nw; - int sh = oh > ow ? -1 : nh; - // We do not defend against unloaded images. There is - // a bug on those anyways, see comment in PostWindow. - scaled = image.getScaledInstance( - sw, sh, - Image.SCALE_SMOOTH - ); - } - // (知) Be careful to be idempotent when calling - // async methods like #drawImage. + if (scaled == null) return; + Rectangle b = roundClip.getBounds(); + Shape defaultClip = g.getClip(); g.setClip(roundClip); - g.drawImage(scaled, nx, ny, getParent()); + g.drawImage(scaled, b.x, b.y, this); + getParent().repaint(); // I don't know why, but when we repaint ourselves, our // parent doesn't repaint, so nothing seems to happen. g.setClip(defaultClip); @@ -383,8 +421,8 @@ implements KeyListener, MouseListener, FocusListener { setFocusable(true); setOpaque(false); - int w = button.getWidth(this); - int h = button.getHeight(this); + int w = button.getWidth(null); + int h = button.getHeight(null); setPreferredSize(new Dimension(w, h)); this.addKeyListener(this); @@ -405,6 +443,14 @@ implements KeyListener, MouseListener, FocusListener { button = new ImageIcon(u1).getImage(); disabledOverlay = new ImageIcon(u2).getImage(); selectedOverlay = new ImageIcon(u3).getImage(); + + int radius = 6; + roundClip = new Ellipse2D.Float( + radius, + radius, + button.getWidth(null) - (2 * radius), + button.getHeight(null) - (2 * radius) + ); } } diff --git a/graphics/nextToggled.png b/graphics/nextToggled.png index f288ec4..3d288ab 100755 Binary files a/graphics/nextToggled.png and b/graphics/nextToggled.png differ diff --git a/graphics/nextUntoggled.png b/graphics/nextUntoggled.png index 4f70b6c..04cb7a3 100755 Binary files a/graphics/nextUntoggled.png and b/graphics/nextUntoggled.png differ