Search code examples
javaswinglook-and-feeljtabbedpane

JTextFieldUI painted in another tab of the JTabbedPane with my personal look and feel


I'm developing a new look and feel and now I have a bug with the component JtextField when to use it inside the JTabbledPane component.

So the bug is this

enter image description here

I have this code for my JTextFieldUI, it extends BasicLookAndFell

public class MyPersonalFieldUI extends BasicTextFieldUI {

    protected static final String PROPERTY_LINE_COLOR = "lineColor";
    protected static final String PROPERTY_SELECTION_COLOR = "selectionColor";
    protected static final String PROPERTY_SELECTION_TEXT_COLOR = "selectedTextColor";
    protected static final String ProprietyPrefix = "TextField";

    protected boolean drawLine;
    protected JTextComponent textComponent;
    protected Color background;
    protected Color foreground;
    protected Color activeBackground;
    protected Color activeForeground;
    protected Color inactiveBackground;
    protected Color inactiveForeground;
    protected Color colorLineInactive;
    protected Color colorLineActive;
    protected Color colorLine;
    protected FocusListener focusListenerColorLine;
    protected PropertyChangeListener propertyChangeListener;
    protected PropertyChangeSupport propertyChangeSupport;

    public MyPersonalFieldUI() {
        this(true);
    }

    public MyPersonalFieldUI(boolean drawLine) {
        super();
        this.drawLine = drawLine;
        this.focusListenerColorLine = new FocusListenerColorLine();
        this.propertyChangeListener = new MaterialPropertyChangeListener();
        this.propertyChangeSupport = new PropertyChangeSupport(this);
    }

    @Override
    protected String getPropertyPrefix() {
        return ProprietyPrefix;
    }

    public static ComponentUI createUI(JComponent c) {
        return new MyPersonalFieldUI();
    }

    @Override
    public void installUI(JComponent c) {
        super.installUI(c);
        this.textComponent = (JTextField) c;
    }

    @Override
    protected void installDefaults() {
        super.installDefaults();
        installMyDefaults();
    }

    @Override
    public void uninstallUI(JComponent c) {
        super.uninstallUI(c);

        c.setFont(null);
        c.setBackground(null);
        c.setForeground(null);
        c.setBorder(null);
        c.setCursor(null);

    }

    @Override
    protected void uninstallDefaults() {
        super.uninstallDefaults();
        getComponent().setBorder(null);
    }

    @Override
    protected void installListeners() {
        super.installListeners();
        getComponent().addFocusListener(focusListenerColorLine);
        getComponent().addPropertyChangeListener(propertyChangeListener);
        propertyChangeSupport.addPropertyChangeListener(propertyChangeListener);
    }

    @Override
    protected void uninstallListeners() {
        getComponent().removeFocusListener(focusListenerColorLine);
        getComponent().removePropertyChangeListener(propertyChangeListener);
        propertyChangeSupport.removePropertyChangeListener(propertyChangeListener);
        super.uninstallListeners();
    }

    @Override
    public void paintSafely(Graphics g) {
        super.paintSafely(g);

        paintLine(g);
    }

    protected void logicForChangeColorOnFocus(JComponent component, Color background, Color foreground) {
        if (background == null || foreground == null) {
            return;
        }
        JTextField textField = (JTextField) component;
        if (!this.activeForeground.equals(foreground)) { //TODO this comment resolve the issue but I don't not if it introduce some bug
            textField.setSelectedTextColor(inactiveForeground);
        } else {
            textField.setSelectedTextColor(foreground);
        }
        textField.setSelectionColor(background);
    }

    protected void installMyDefaults() {
        this.background = UIManager.getColor(getPropertyPrefix() + ".background");
        this.foreground = UIManager.getColor(getPropertyPrefix() + ".foreground");
        this.activeBackground = UIManager.getColor(getPropertyPrefix() + ".selectionBackground");
        this.activeForeground = UIManager.getColor(getPropertyPrefix() + ".selectionForeground");
        this.inactiveBackground = UIManager.getColor(getPropertyPrefix() + ".inactiveBackground");
        this.inactiveForeground = UIManager.getColor(getPropertyPrefix() + ".inactiveForeground");
        colorLineInactive = UIManager.getColor(getPropertyPrefix() + "[Line].inactiveColor");
        colorLineActive = UIManager.getColor(getPropertyPrefix() + "[Line].activeColor");
        getComponent().setFont(UIManager.getFont(getPropertyPrefix() + ".font"));
        colorLine = getComponent().hasFocus() && getComponent().isEditable() ? colorLineActive : colorLineInactive;
        getComponent().setSelectionColor(getComponent().hasFocus() && getComponent().isEnabled() ? activeBackground : inactiveBackground);
        getComponent().setSelectedTextColor(getComponent().hasFocus() && getComponent().isEnabled() ? activeForeground : inactiveForeground);
        getComponent().setForeground(getComponent().hasFocus() && getComponent().isEnabled() ? activeForeground : inactiveForeground);
        getComponent().setBorder(UIManager.getBorder(getPropertyPrefix() + ".border"));
    }

    protected void logicForPropertyChange(Color newColor, boolean isForeground) {
        if (newColor == null) {
            return;
        }
        if (isForeground && (!newColor.equals(activeForeground) && !newColor.equals(inactiveForeground))) {
            this.activeForeground = newColor;
            getComponent().repaint();
        }
        if (!isForeground && !newColor.equals(activeBackground) && !newColor.equals(inactiveBackground)) {
            this.activeBackground = newColor;
            getComponent().repaint();
        }
    }

    protected void changeColorOnFocus(boolean hasFocus) {
        JTextComponent c = getComponent();
        if (c == null) {
            return;
        }
        if (hasFocus && (activeBackground != null) && (activeForeground != null)) {
            logicForChangeColorOnFocus(c, activeBackground, activeForeground);
            //TODO create a new changePropriety
            paintLine(c.getGraphics());
        }

        if (!hasFocus && (inactiveBackground != null) && (inactiveForeground != null)) {
            logicForChangeColorOnFocus(c, inactiveBackground, inactiveForeground);
            paintLine(c.getGraphics());
        }
        if (c.getGraphics() != null) {
            c.paint(c.getGraphics());
        }
    }

    protected synchronized void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
        if ((propertyName == null || propertyName.isEmpty()) || oldValue == null || newValue == null) {
            throw new IllegalArgumentException("Some property null");
        }
        //TODO refectoring this code
        if (propertyChangeSupport == null || (oldValue != null && newValue != null && oldValue.equals(newValue))) {
            return;
        }
        if (propertyChangeSupport == null || oldValue == newValue) {
            return;
        }
        propertyChangeSupport.firePropertyChange(propertyName, oldValue, newValue);
    }

    protected void paintLine(Graphics graphics) {
        if (graphics == null) {
            return;
        }
        JTextComponent c = getComponent();

        if (drawLine) {
            int x = c.getInsets().left;
            int y = c.getInsets().top;
            int w = c.getWidth() - c.getInsets().left - c.getInsets().right;
            graphics.setColor(colorLine);

            graphics.fillRect(x, c.getHeight() - y, w, 1);
        }
    }

    protected class FocusListenerColorLine implements FocusListener {

        @Override
        public void focusGained(FocusEvent e) {
            firePropertyChange(PROPERTY_LINE_COLOR, colorLineInactive, colorLineActive);

            changeColorOnFocus(true);
        }

        @Override
        public void focusLost(FocusEvent e) {
            firePropertyChange(PROPERTY_LINE_COLOR, colorLineActive, colorLineInactive);
            changeColorOnFocus(false);
        }
    }

    protected class MaterialPropertyChangeListener implements PropertyChangeListener {

        @Override
        public void propertyChange(PropertyChangeEvent pce) {
            if (getComponent() == null) {
                return;
            }
            if (pce.getPropertyName().equals(PROPERTY_SELECTION_COLOR)) {
                Color newColor = (Color) pce.getNewValue();
                logicForPropertyChange(newColor, false);
            }

            if (pce.getPropertyName().equals(PROPERTY_SELECTION_TEXT_COLOR)) {
                Color newColor = (Color) pce.getNewValue();
                logicForPropertyChange(newColor, true);
            }

            if (pce.getPropertyName().equals(PROPERTY_LINE_COLOR)) {
                Color newColor = (Color) pce.getNewValue();
                colorLine = newColor;
                getComponent().repaint();
            }

            if (pce.getPropertyName().equals("background")) {
                getComponent().repaint();
            }
        }
    }

}

Sorry for my big code but I think is necessary for find my error inside it

also, this is my minimal reproducible example

public class DemoLookAndFeel extends JFrame {

    static {
        try {
             UIManager.setLookAndFeel(new MyLookAndFeel());
        } catch (UnsupportedLookAndFeelException ex) {
        }
    }

    public void init() {
        JPanel panelOne = new JPanel();
        panelOne.add(new JTextField("write inside me and change the tab"));

        JPanel panelTwo = new JPanel();
        //panelTwo.add(new Label("Now seee the JTextField?"));

        JTabbedPane tabbedPane = new JTabbedPane();

        tabbedPane.add("One", panelOne);
        tabbedPane.add("Two", panelTwo);

        this.setContentPane(tabbedPane);

        this.setSize(800,800);
        this.pack();
        this.setLocationRelativeTo(null);
        this.setVisible(true);
    }

    private static class MyLookAndFeel extends BasicLookAndFeel {

        @Override
        public String getName() {
            return "my look and feel";
        }

        @Override
        public String getID() {
            return "qwerty";
        }

        @Override
        public String getDescription() {
            return "";
        }

        @Override
        public boolean isNativeLookAndFeel() {
            return false;
        }

        @Override
        public boolean isSupportedLookAndFeel() {
            return true;
        }

        @Override
        protected void initClassDefaults(UIDefaults table) {
            super.initClassDefaults(table); 
              table.put("TextFieldUI", MyPersonalFieldUI.class.getCanonicalName());
        }



        @Override
        protected void initSystemColorDefaults(UIDefaults table) {
            super.initSystemColorDefaults(table); 

            table.put("TextField.background", Color.GRAY);
            table.put("TextField.foreground", Color.BLACK);
            table.put("TextField.inactiveForeground", Color.BLACK);
            table.put("TextField.inactiveBackground", Color.GRAY);
            table.put("TextField.selectionBackground", Color.BLUE);
            table.put("TextField.selectionForeground", Color.WHITE);
            table.put("TextField[Line].inactiveColor", Color.WHITE);
            table.put("TextField[Line].activeColor", Color.BLUE);
        }

    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                DemoLookAndFeel demo = new DemoLookAndFeel();
                demo.init();
            }
        });
    }

}

ps:Sorry for my Big code


Solution

  • The problem is on how you manipulate painting. Inside changeColorOnFocus (called by a FocusListener) method you have these lines:

    if (c.getGraphics() != null) {
        c.paint(c.getGraphics());
    }
    

    Here, you use graphics and do a kind of custom painting outside of a method that is responsible for painting in component-painting-heirarchy. I wish I could explain it better, but long story short, play with Graphics only in methods that give them to you as an argument (and call its super method). In an UI class the method is paintSafely, in a component is paintComponent. They both give you Graphics object hence they are proper for custom painting.

    So, the solution is inside your FocusListener to add a flag in order to know whether the component is focused, and call changeColorOnFocus inside paintSafely(Graphics g) method. Your UI class should be changed in the following parts (I don't paste it all since it is kind of big).

    private boolean focused; //a field
    protected class FocusListenerColorLine implements FocusListener {
    
        @Override
        public void focusGained(FocusEvent e) {
            firePropertyChange(PROPERTY_LINE_COLOR, colorLineInactive, colorLineActive);
            focused = true;
        }
    
        @Override
        public void focusLost(FocusEvent e) {
            firePropertyChange(PROPERTY_LINE_COLOR, colorLineActive, colorLineInactive);
            focused = false;
        }
    }
    
    
    
    @Override
    public void paintSafely(Graphics g) {
        super.paintSafely(g);
        paintLine(g);
        changeColorOnFocus(g);
    }
    
    protected void changeColorOnFocus(Graphics g) {
        boolean hasFocus = focused;
        JTextComponent c = getComponent();
        if (c == null) {
            return;
        }
        if (hasFocus && (activeBackground != null) && (activeForeground != null)) {
            logicForChangeColorOnFocus(c, activeBackground, activeForeground);
            //TODO create a new changePropriety
            paintLine(c.getGraphics());
        }
    
        if (!hasFocus && (inactiveBackground != null) && (inactiveForeground != null)) {
            logicForChangeColorOnFocus(c, inactiveBackground, inactiveForeground);
            paintLine(c.getGraphics());
        }
    }
    

    Preview:

    preview