2

I'm making my first steps with desktop GUI design. What I'm trying to achieve is displaying in one of table columns. I want them to be visually attractive and also clickable.

At first, I thought that it's possible to do that with JButton with some hacks, as suggested in this thread. But then I realized that I don't know how to manage dynamic width of labels and stretching the graphics to fill buttons in 100%.

I want to achieve something similar to image below. In HTML+CSS it's very easy, each tag:

  • has left edge as graphics
  • has repeatable 1px wide background
  • has right edge as graphics

But how to do that in Java?

info: I don't care if this is a JButton or not, as long as it's nice, clickable and draggable.

tag graphics

UPDATE

Mockup below represents what I want to do. It involves JTable or multiple JLists as well as those tags floating left next to the word. It's easy to do in HTML+CSS, but almost impossible for me to do that in Java.

mockup

Community
  • 1
  • 1
ex3v
  • 3,518
  • 4
  • 33
  • 55

4 Answers4

5

I have no doubt there are multitudes of ways to achieve this, a better way would be to simply write a UI delegate, but let's try and keep it basic for the moment...

enter image description here

(I've updated the button code, so I've included it in the update)

The problem you have is JTable just isn't designed to do what you are trying to. Grouped rows and variable row heights are not easily achieved.

This is just an example, but what I've done is generate a series of compound components which "mimic" your basic requirements...

enter image description here

The following makes use `WrapLayout' written by own camrik and SwingLabs, SwingX libraries

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.LinearGradientPaint;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Path2D;
import java.util.ArrayList;
import java.util.List;
import javax.swing.AbstractButton;
import javax.swing.DefaultButtonModel;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.border.Border;
import javax.swing.border.MatteBorder;
import org.jdesktop.swingx.*;

public class TestTagButton {

    public static void main(String[] args) {
        new TestTagButton();
    }

    public TestTagButton() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                }

                JPanel wordsPane = new JPanel(new VerticalLayout());
                wordsPane.setBackground(Color.WHITE);

                Word word = new Word("Hand");
                word.addTag("body parts", 55);
                List<String> translations = new ArrayList<>(3);
                translations.add("Reka");
                translations.add("Dion");
                translations.add("Garsec");

                wordsPane.add(new WordGroupPane(word, translations));

                word = new Word("Roof");
                word.addTag("house", 17);
                word.addTag("architecture", 8);
                translations = new ArrayList<>(3);
                translations.add("Dach");
                translations.add("Zadaszenie");

                wordsPane.add(new WordGroupPane(word, translations));

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
//                frame.add(new JScrollPane(wordsPane));
                frame.add(wordsPane);
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public static class WordGroupPane extends JPanel {

        public WordGroupPane(Word word, List<String> translations) {
            setOpaque(false);
            setLayout(new GridLayout(0, 2));

            add(new WordPane(word));
            add(new TranslationsPane(translations));
        }

    }

    public static class TranslationsPane extends JPanel {

        protected static final Border SPLIT_BORDER = new MatteBorder(0, 0, 1, 0, Color.GRAY);

        public TranslationsPane(List<String> translations) {
            setOpaque(false);
            setLayout(new GridLayout(0, 1));
            for (String translation : translations) {
                JLabel lbl = new JLabel(translation);
                lbl.setHorizontalAlignment(JLabel.LEFT);
                lbl.setBorder(SPLIT_BORDER);
                add(lbl);
            }
        }

    }

    public static class WordPane extends JPanel {

        public WordPane(Word word) {
            setBorder(new MatteBorder(0, 0, 1, 1, Color.GRAY));
            setOpaque(false);
            setLayout(new GridBagLayout());
            GridBagConstraints gbc = new GridBagConstraints();
            gbc.gridx = 0;
            gbc.gridy = 0;
            gbc.anchor = GridBagConstraints.NORTH;
            gbc.insets = new Insets(10, 8, 10, 8);
            JLabel label = new JLabel(word.getWord());
            add(label, gbc);

            gbc.gridx = 1;
            gbc.weightx = 1;
            gbc.fill = GridBagConstraints.HORIZONTAL;
            gbc.insets = new Insets(0, 0, 0, 0);
            JPanel tagsPane = new JPanel(new WrapLayout(WrapLayout.LEFT));
            tagsPane.setOpaque(false);
            for (Tag tag : word.getTags()) {
                TagButton tb = new TagButton(tag.getText());
                Font font = tb.getFont();
                font = font.deriveFont(font.getSize() - 3f);
                tb.setFont(font);
                tb.setTag(Integer.toString(tag.getCount()));
                tb.setSelected(true);
                tagsPane.add(tb);
            }
            add(tagsPane, gbc);
        }

    }

    public class Word {

        private String word;
        private List<Tag> tags;

        public Word(String word) {
            this.word = word;
            this.tags = new ArrayList<>(25);
        }

        public void addTag(String text, int count) {
            addTag(new Tag(text, count));
        }

        public String getWord() {
            return word;
        }

        public List<Tag> getTags() {
            return tags;
        }

        public void addTag(Tag tag) {
            tags.add(tag);
        }

    }

    public class Tag {

        private String text;
        private int count;

        public Tag(String text, int count) {
            this.text = text;
            this.count = count;
        }

        public String getText() {
            return text;
        }

        public int getCount() {
            return count;
        }

    }

    public static class TagButton extends AbstractButton {

        private JLabel renderer;
        private String tag;

        public TagButton(String text) {
            this();
            setText(text);
        }

        public TagButton() {
            setModel(new DefaultButtonModel());
            setMargin(new Insets(8, 8, 8, 8));
            addMouseListener(new MouseAdapter() {

                @Override
                public void mousePressed(MouseEvent e) {
                    getModel().setPressed(true);
                }

                @Override
                public void mouseReleased(MouseEvent e) {
                    getModel().setPressed(false);
                    getModel().setSelected(!isSelected());
                }

            });
            setFont(UIManager.getFont("Button.font"));
        }

        public void setTag(String value) {
            if (tag == null ? value != null : !tag.equals(value)) {

                String old = tag;
                tag = value;
                firePropertyChange("tag", old, tag);
                revalidate();

            }
        }

        public String getTag() {
            return tag;
        }

        protected JLabel getRenderer() {

            if (renderer == null) {

                renderer = new JLabel(getText());

            }

            return renderer;

        }

        @Override
        public Dimension getPreferredSize() {

            Insets margin = getMargin();
            Dimension size = new Dimension();
            size.width = margin.left + margin.right;
            size.height = margin.top + margin.bottom;

            JLabel renderer = getRenderer();
            renderer.setText(getText());
            size.width += renderer.getPreferredSize().width;
            size.height += renderer.getPreferredSize().height;

            size.width += getTagWidth();

            return size;
        }

        protected int getTagWidth() {

            JLabel renderer = getRenderer();
            renderer.setText(getTag());
            renderer.setFont(getFont());

            return renderer.getPreferredSize().width + 16;

        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
            g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
            g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
            g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

//            int fullWidth = getTagWidth() + 8;
            int width = getTagWidth() + 8;
            int height = getHeight() - 3;
            int tagWidth = getWidth() - 1 - width;

            Shape insert = new TagInsert(width, height);
            int x = getWidth() - width - 1;
            if (!isSelected()) {
                x -= getTagWidth();
            }

            x -= 4;

            g2d.translate(x, 1);
            g2d.setPaint(new Color(242, 95, 0));
            g2d.fill(insert);
            g2d.setPaint(new Color(222, 83, 0));
            g2d.draw(insert);

            Stroke stroke = g2d.getStroke();
            BasicStroke stitch = new BasicStroke(1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10f, new float[]{3f}, 0f);
            g2d.setStroke(stitch);
            g2d.setColor(new Color(167, 65, 1));
            g2d.drawLine(0, 2, width, 2);
            g2d.drawLine(0, height - 2, width, height - 2);
            g2d.setColor(new Color(249, 127, 50));
            g2d.drawLine(0, 3, width, 3);
            g2d.drawLine(0, height - 1, width, height - 1);
            g2d.setStroke(stroke);

            if (isSelected()) {

                JLabel renderer = getRenderer();
                renderer.setFont(getFont());
                renderer.setText(getTag());
                renderer.setSize(width - 8, renderer.getPreferredSize().height);
                renderer.setForeground(Color.WHITE);
                renderer.setHorizontalAlignment(JLabel.CENTER);
                int xPos = 4;//((tagWidth - renderer.getWidth()) / 2);
                int yPos = (height - renderer.getHeight()) / 2;
                g2d.translate(xPos, yPos);
                renderer.printAll(g2d);
                g2d.translate(-xPos, -yPos);

            }

            g2d.translate(-x, -1);

            height = getHeight() - 1;

            Shape baseShape = new TagShape(tagWidth, height);

            LinearGradientPaint lgpFill = new LinearGradientPaint(
                            new Point(0, 0),
                            new Point(0, getHeight() - 1),
                            new float[]{0f, 1f},
                            new Color[]{new Color(248, 248, 248), new Color(241, 241, 241)}
            );
            g2d.setPaint(lgpFill);
            g2d.fill(baseShape);

            LinearGradientPaint lgpOutline = new LinearGradientPaint(
                            new Point(0, 0),
                            new Point(0, getHeight() - 1),
                            new float[]{0f, 1f},
                            new Color[]{UIManager.getColor("Button.shadow"), UIManager.getColor("Button.darkShadow")}
            );
            g2d.setPaint(lgpOutline);
            g2d.draw(baseShape);

            JLabel renderer = getRenderer();
            renderer.setFont(getFont());
            renderer.setText(getText());
            renderer.setSize(renderer.getPreferredSize());
            renderer.setForeground(getForeground());

            x = (tagWidth - renderer.getWidth()) / 2;
            int y = (height - renderer.getHeight()) / 2;
            renderer.setLocation(x, y);
            g2d.translate(x, y);
            renderer.printAll(g2d);
            g2d.translate(-x, -y);

            g2d.setColor(Color.RED);
//            g2d.drawRect(0, 0, getWidth() - 1, getHeight() - 1);

            g2d.dispose();
        }
    }

    protected static class TagInsert extends Path2D.Float {

        public TagInsert(float width, float height) {

            float gap = 3;

            float radius = (height - (gap * 5)) / 4f;
            moveTo(0, 0);
            lineTo(width, 0);

            float yPos = 0;
            lineTo(width, 1);
            float topY = gap;
            for (int index = 0; index < 4; index++) {

                float bottomY = topY + radius;
                float x = width - (radius / 2);

                lineTo(width, topY);
                curveTo(x, topY, x, bottomY, width, bottomY);
                topY += radius;
                topY += gap;
                lineTo(width, topY);

            }

            lineTo(width, height);
            lineTo(0, height);
            lineTo(0, 0);

        }

    }

    protected static class TagShape extends Path2D.Float {

        protected static final float RADIUS = 8;

        public TagShape(float width, float height) {

            moveTo(RADIUS, 0);
            lineTo(width, 0);

            float clip = RADIUS / 2f;
            float topY = (height / 2f) - clip;
            float bottomY = (height / 2f) + clip;
            lineTo(width, topY);
            curveTo(width - clip, topY, width - clip, bottomY, width, bottomY);
            lineTo(width, height);
            lineTo(RADIUS, height);

            curveTo(0, height, 0, height, 0, height - RADIUS);
            lineTo(0, RADIUS);
            curveTo(0, 0, 0, 0, RADIUS, 0);

        }

    }
}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • 1
    @ex3v I've done a "basic" mock up. Personally, I'd generate a better model to wrap the word, tags and translations together, but this about demonstrating an idea – MadProgrammer Nov 08 '13 at 10:36
1

use javafx buttons rather than swing. javafx buttons have css properties.

subash
  • 3,116
  • 3
  • 18
  • 22
  • I took a quick look at javafx. It seems to look just great for someone with webDev background. It doesn't solve the main problem (doing this in pure Swing) but it just looks very modern. And that is what I was searching for. Thanks! – ex3v Nov 07 '13 at 12:32
1

Based on your requirements it does seem like JButton is a good starting point because it has most of what you want.

As far as the width goes, you will most likely need to subclass JButton so that you can you can update the width in the constructor, something like this:

private static class FancyButton extends JButton{
    public FancyButton(String name) {
        super(name);
        //Calculate new width
        setPreferredSize(new Dimension(newWidth, 30));
    }       
}  

Or have a method that sets up your buttons that determines and sets the width.

jzd
  • 23,473
  • 9
  • 54
  • 76
  • Calculating new width is easy... but what about resizing graphics? – ex3v Nov 07 '13 at 12:27
  • You would most likely need to extend paintComponent on your JButton to do your graphic resizing. Hard to tell exactly what you mean because I am not sure how you have implemented it thus far. – jzd Nov 07 '13 at 12:32
  • I haven't implemented it at all. I focused on coding backend and now I'm looking for best way on how to create the GUI I really want. – ex3v Nov 07 '13 at 12:40
  • Then I don't understand your issue with resizing graphics. Can you explain more? – jzd Nov 07 '13 at 13:03
  • I'll try my best: in my first post, you have image with few examples. Let's take first one (named "design") and see what we have. As I wrote in my oryginal post, this button is built using 3 separate graphics: one on the left, one on the right and one 1px wide repeatable graphics, that repeats as many times as it's needed to maintain correct width for different button label text widths. It has to be build that way - only stretching the graphics to fit the label width will end up in shredded background image. – ex3v Nov 07 '13 at 14:11
1

As you want to use these in a JTable, you'll need a custom renderer that draws the button with or without the number showing. The numeric value and state should be stored in your TableModel. To get the behavior of a button, you'll need a custom editor, perhaps one returning an instance of JToggleButton. In this example, an instance of JCheckBox (a subclass of JToggleButton) is used to display both state and value in a column.

trashgod
  • 203,806
  • 29
  • 246
  • 1,045