/* copyright This file is part of JKomasto2. Written in 2022 by Usawashi This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . copyright */ import javax.swing.AbstractButton; import javax.swing.DefaultButtonModel; import javax.swing.Icon; import javax.swing.ImageIcon; import java.awt.Image; 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; import java.awt.event.KeyListener; import java.awt.event.KeyEvent; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; import java.awt.event.FocusListener; import java.awt.event.FocusEvent; import java.net.URL; import javax.swing.JFrame; class TwoToggleButton extends AbstractButton implements KeyListener, MouseListener, FocusListener { private String primaryName, secondaryName; // - -%- - private boolean primaryToggled = false, secondaryToggled = false; private Image primaryToggledIcon, secondaryToggledIcon, primaryUntoggledIcon, secondaryUntoggledIcon; private int nextEventID = ActionEvent.ACTION_FIRST; // - -%- - private static Image button, selectedOverlay, disabledOverlay; // ---%-@-%--- public void togglePrimary() { setPrimaryToggled(!primaryToggled); } public void toggleSecondary() { setSecondaryToggled(!secondaryToggled); } public void setPrimaryToggled(boolean toggled) { primaryToggled = toggled; repaint(); announce(primaryName, toggled); } public void setSecondaryToggled(boolean toggled) { secondaryToggled = toggled; repaint(); announce(secondaryName, toggled); } public boolean getPrimaryToggled() { return primaryToggled; } public boolean getSecondaryToggled() { return secondaryToggled; } // - -%- - private void announce(String name, boolean toggled) { ActionEvent eA = new ActionEvent( this, nextEventID++, name + (toggled ? "On" : "Off") ); for (ActionListener listener: getActionListeners()) listener.actionPerformed(eA); } protected void paintComponent(Graphics g) { g.drawImage(button, 0, 0, this); if (!isEnabled()) g.drawImage(disabledOverlay, 0, 0, this); if (isFocusOwner()) g.drawImage(selectedOverlay, 0, 0, this); if (secondaryToggled) g.drawImage(secondaryToggledIcon, 0, 0, this); else g.drawImage(secondaryUntoggledIcon, 0, 0, this); if (primaryToggled) g.drawImage(primaryToggledIcon, 0, 0, this); else g.drawImage(primaryUntoggledIcon, 0, 0, this); } public void mousePressed(MouseEvent eM) { boolean shift = eM.isShiftDown(); boolean prim = eM.getButton() == MouseEvent.BUTTON1; boolean secon = eM.getButton() == MouseEvent.BUTTON3; secon |= shift && prim; if (secon) toggleSecondary(); else if (prim) togglePrimary(); requestFocusInWindow(); } public void keyPressed(KeyEvent eK) { boolean shift = eK.isShiftDown(); boolean prim = eK.getKeyCode() == KeyEvent.VK_SPACE; boolean secon = eK.getKeyCode() == KeyEvent.VK_ENTER; secon |= shift && prim; if (secon) toggleSecondary(); else if (prim) togglePrimary(); requestFocusInWindow(); } public void focusGained(FocusEvent eF) { repaint(); } public void focusLost(FocusEvent eF) { repaint(); } public void mouseClicked(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) { } // (悪) If we don't have the 'armed' behaviour of JButton, // we really need to rectify that, it's important for this // use case. // ---%-@-%--- TwoToggleButton(String primaryName, String secondaryName) { if (button == null) loadCommonImages(); this.primaryName = primaryName; this.secondaryName = secondaryName; setModel(new DefaultButtonModel()); setFocusable(true); setOpaque(false); int w = button.getWidth(null); int h = button.getHeight(null); setPreferredSize(new Dimension(w, h)); loadSpecificImages(); this.addKeyListener(this); this.addMouseListener(this); this.addFocusListener(this); } private void loadSpecificImages() { String p1 = "graphics/" + primaryName + "Toggled.png"; String p2 = "graphics/" + secondaryName + "Toggled.png"; String p3 = "graphics/" + primaryName + "Untoggled.png"; String p4 = "graphics/" + secondaryName + "Untoggled.png"; URL u1 = getClass().getResource(p1); URL u2 = getClass().getResource(p2); URL u3 = getClass().getResource(p3); URL u4 = getClass().getResource(p4); if (u1 == null) primaryToggledIcon = null; else primaryToggledIcon = new ImageIcon(u1).getImage(); if (u2 == null) secondaryToggledIcon = null; else secondaryToggledIcon = new ImageIcon(u2).getImage(); if (u3 == null) primaryUntoggledIcon = null; else primaryUntoggledIcon = new ImageIcon(u3).getImage(); if (u4 == null) secondaryUntoggledIcon = null; else secondaryUntoggledIcon = new ImageIcon(u4).getImage(); } // - -%- - 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 RoundButton extends AbstractButton implements KeyListener, MouseListener, FocusListener { private Image image; // - -%- - private Image copy, scaled; private int nextEventID = ActionEvent.ACTION_FIRST; // - -%- - private static Image button, 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 img, int f, int x, int y, int w, int 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 paintComponent(Graphics g) { g.drawImage(button, 0, 0, this); if (!isEnabled()) g.drawImage(disabledOverlay, 0, 0, this); if (isFocusOwner()) g.drawImage(selectedOverlay, 0, 0, this); if (scaled == null) return; Rectangle b = roundClip.getBounds(); Shape defaultClip = g.getClip(); g.setClip(roundClip); 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); } 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(); int radius = 6; roundClip = new Ellipse2D.Float( radius, radius, button.getWidth(null) - (2 * radius), button.getHeight(null) - (2 * radius) ); } } class TwoToggleButtonTest { public static void main(String... args) { if (args.length != 2) { String err = "Please give two toggle names."; System.err.println(err); System.exit(1); } String p = args[0], s = args[1]; TwoToggleButton t1 = new TwoToggleButton(p, s); TwoToggleButton t2 = new TwoToggleButton(p, s); TwoToggleButton t3 = new TwoToggleButton(p, s); JFrame mainframe = new JFrame("Two-toggle button test"); mainframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); mainframe.setLocationByPlatform(true); mainframe.add(t1, java.awt.BorderLayout.WEST); mainframe.add(t2); mainframe.add(t3, java.awt.BorderLayout.EAST); mainframe.pack(); t2.setEnabled(false); t3.requestFocusInWindow(); mainframe.setVisible(true); } }