ThemeManager BackgroundPaint issue

Hello Beni,

Long time no chat. Here’s a bug I found totally by accident.

The other day I turned on debugging in my application to do some testing. I went out to lunch and when I got back, I had a massively large log file. Nobody was using my PC (the screen was dark). When I reviewed the log file, there were thousands and thousands of the getValueAt() method of a TableModel being called.

I have spent the last 2 days trying to figure out what is invoking the getValueAt() method of a TableModel over and over again.

  1. I checked the older version of the application before Docking Frames and there were no issues.
  2. In the current release, I commented out all Docking Frame components in the application and added the JScrollPane with JTable directly to the JFrame and there were no issues.
  3. So, I uncommented everything and started piece by piece commenting out different parts of the code until I finally tracked down the issue (what a pain!!).

Many years ago, I asked you „How do I add a background image to cwArea?“ see: Background image for WorkingArea and you gave me the example BackgroundIconTest class. I have been using it without issue or at least I didn’t realize that there was an issue.

On startup, the paint method of the LogoBackgroundPaint gets invoked many times a second which is not great but ok. When you open docks in the CWorkingArea that is where things go bad. Even though the image is covered by the new dock with JTable, the paint method of the LogoBackgroundPaint is still getting invoked many times a second. This causes the ALL of the components over the background image (which is hidden) to be repainted/revalidated.

Hence, that is why the getValueAt() of a TableModel being called.

I figured since this is really weird that you would want to have an example that shows the issue. The image I use for the background is an animated GIF. I put the URL to an animated GIF in the code for you to download it. Comment out the call to installBackground() and see how its suppose to run. Then run it with the Background image and you will see it go wild.

Here is the code:

import java.awt.*;
import java.awt.event.*;
import java.text.SimpleDateFormat;
import java.util.*;

import javax.swing.*;
import javax.swing.table.DefaultTableModel;

import bibliothek.extension.gui.dock.theme.flat.FlatStationPaint;
import bibliothek.gui.dock.common.*;
import bibliothek.gui.dock.common.intern.*;
import bibliothek.gui.dock.common.theme.*;
import bibliothek.gui.dock.station.*;
import bibliothek.gui.dock.themes.*;
import bibliothek.gui.dock.toolbar.*;
import bibliothek.gui.dock.util.*;

public class Test_DF3  extends JFrame  implements ActionListener
{
   private static final SimpleDateFormat  LOGGER_TIMESTAMP = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS");
   
   private CControl        control = null;
   private CWorkingArea    cwArea = null;
   private int             count = 1;
   private Image           image = null;

   public Test_DF3()
   {
      super();

      logger("");

      /*
       * Get the GIF from: https://i.pinimg.com/originals/e9/fa/42/e9fa42a3ec0a38efc9d1ef2db3f12f3d.gif
       * I put a copy of the GIF here: https://www.capitalware.com/dl/products/mqve2/e9fa42a3ec0a38efc9d1ef2db3f12f3d.gif
       */
      ImageIcon ii = new ImageIcon(Test_DF3.class.getResource("e9fa42a3ec0a38efc9d1ef2db3f12f3d.gif"));
      image = ii.getImage();
      
      createMenubar();
      
      control = new CControl(this);

      control.setTheme(ThemeMap.KEY_ECLIPSE_THEME);
      ((CEclipseTheme) control.getController().getTheme()).intern().setPaint(new FlatStationPaint());

      this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      this.setSize(1024, 750);
      
      CToolbarContentArea ctcArea = new CToolbarContentArea( control, "base" );
      control.addStationContainer( ctcArea );
      /* ... and added to the frame */
      this.add( ctcArea );

      cwArea = control.createWorkingArea( "work" );
      installBackground( control, cwArea );

      CGrid grid = new CGrid(control);
      
      grid.add( 0, 0, 100, 100, cwArea );  // x,y w,h
      
      ctcArea.getCenterArea().deploy(grid);

      this.setVisible(true);
   }
   
   private void createMenubar()
   {
      logger("");
      
      JMenuBar menuBar = new JMenuBar();
      this.setJMenuBar(menuBar);
      
      JMenu fileMenu = new JMenu("File");
      fileMenu.setOpaque(true);
      menuBar.add(fileMenu);

      JMenuItem newEditor = new JMenuItem( "New Editor" );
      newEditor.setActionCommand("NewEditor");
      newEditor.addActionListener(this);
      fileMenu.add( newEditor );
      
      JMenuItem closeAllEditors = new JMenuItem( "Close All Editors" );
      closeAllEditors.setActionCommand("CloseAllEditors");
      closeAllEditors.addActionListener(this); 
      fileMenu.add( closeAllEditors );
   }

   @Override
   public void actionPerformed(ActionEvent e)
   {
      String actionCmd = e.getActionCommand();
      
      logger("actionCmd="+actionCmd);
      
      if ("NewEditor".equals(actionCmd))
      {
         DefaultMultipleCDockable editor = new DefaultMultipleCDockable( null );
         
         editor.setTitleText( "Editor " + (count++) );
         editor.setCloseable( true );
         editor.setExternalizable(false);
         editor.setRemoveOnClose(true);
         
         Object[][] data = 
         {
            {"xxxx", "5"}, 
            {"yyyyy", "Abc"},
            {"zz", "7898"},
            {"aaaa", "157"},
            {"BBBB", "9876"},
         };
         
         String[] headers = {"Field", "Value"};

         DefaultTableModel dtm = new DefaultTableModel(5, 2)
         {
            @Override
            public Object getValueAt(int r, int c)
            {
               logger("r="+r+" : c="+c);
               return ((Vector) dataVector.get(r)).get(c);
            }
         };
         dtm.setDataVector(data, headers);
         
         JTable jt = new JTable(dtm);
         jt.setRowSelectionAllowed(true);
         
         editor.add( new JScrollPane(jt, 
                                     JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
                                     JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED));

         cwArea.show( editor );
         editor.toFront();
      }
      else if ("CloseAllEditors".equals(actionCmd))
      {
         int c = control.getCDockableCount();
         for (int i=(c-1); i >= 0; i--)
         {
            logger("i="+i);
            CDockable focus = control.getCDockable(i);
            if( (focus != null) && (focus.getWorkingArea() == cwArea) )
            {
               control.removeDockable((MultipleCDockable)focus);
            }
         }
      }
   }

   private void installBackground( CControl control, final CWorkingArea area )
   {
      ThemeManager themeManager = control.getController().getThemeManager();

      // custom UIBridge may install custom background to any DockStation (note: there can only be one bridge for each KIND of component)
      themeManager.setBackgroundPaintBridge( StationBackgroundComponent.KIND, new UIBridge<BackgroundPaint, UIValue<BackgroundPaint>>() 
      {
         public void set( String id, BackgroundPaint value, UIValue<BackgroundPaint> uiValue ) 
         {
            logger("");
            // this cast is safe, because we installed this bridge with the constant StationBackgroundComponent.KIND
            StationBackgroundComponent component = (StationBackgroundComponent)uiValue;

            if( area.getStation() == component.getStation())
            {
               logger("[if]   id="+id+ " : value=" + value + " : uiValue="+uiValue);
               uiValue.set( new LogoBackgroundPaint() );
            }
            else
            {
               logger("[else] id="+id+ " : value=" + value + " : uiValue="+uiValue);
               uiValue.set( value );
            }
         }

         public void remove( String id, UIValue<BackgroundPaint> uiValue ) 
         {
            logger("");
         }

         public void add( String id, UIValue<BackgroundPaint> uiValue ) 
         {
            logger("       id="+id+ "              : uiValue="+uiValue);
         }
      });
   }

   private class LogoBackgroundPaint implements BackgroundPaint
   {
      public void install( BackgroundComponent component ) 
      {
         logger("");
      }

      public void uninstall( BackgroundComponent component ) 
      {
         logger("");
      }

      public void paint( BackgroundComponent background, PaintableComponent paintable, Graphics g ) 
      {
         logger("");
         // don't forget to paint the ordinary background first
         paintable.paintBackground( g );

         // then paint a custom logo
         Component c = paintable.getComponent();
         
         // The icon is 267 width, so subtract it then divide by 2 to figure out where to start drawing 
         int xp = (c.getWidth()-267)/2;  

         if (image != null)
            g.drawImage(image, xp, 28, c);  // 28 is to push the logo down a little bit otherwise you can see it after a tab is opened. 
      }
   }

   /**
    * A simple logging method
    * @param data
    */
   public static void logger(String data)
   {
      String className = Thread.currentThread().getStackTrace()[2].getClassName();

      // Remove the package info.
      if ( (className != null) && (className.lastIndexOf('.') != -1) )
         className = className.substring(className.lastIndexOf('.')+1);

      System.out.println(LOGGER_TIMESTAMP.format(new Date())+" "+className+": "+Thread.currentThread().getStackTrace()[2].getMethodName()+": "+data);
   }

   public static void main(String[] args)
   {
      logger("");

      try
      {
         UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
      } 
      catch(Exception e)
      {
         e.printStackTrace();
      }

      new Test_DF3();
   }
}

Regards,
Roger

1 Like

Hi Roger,

This is answer 1 of 3:

I get the table to be repainted without DockingFrames too, check out shortened version of your demo.

If I remove the border around the table (line 49/50) the repainting is gone.

What happens here: Swing does not know where exactly we are painting the image. Each time the image changes the entire background is marked as dirty, leading to the entire UI being repainted.

The code responsible for the invalidating the entire UI is the method Component.imageUpdate.

package test;

import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Insets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Vector;

import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.UIManager;
import javax.swing.table.DefaultTableModel;

public class TestDF4 extends JFrame {
	private static final SimpleDateFormat LOGGER_TIMESTAMP = new SimpleDateFormat( "yyyy/MM/dd HH:mm:ss.SSS" );

	private Image image = null;

	public TestDF4() {
		super();

		logger( "" );

		/*
		 * Get the GIF from: https://i.pinimg.com/originals/e9/fa/42/e9fa42a3ec0a38efc9d1ef2db3f12f3d.gif I put a copy
		 * of the GIF here: https://www.capitalware.com/dl/products/mqve2/e9fa42a3ec0a38efc9d1ef2db3f12f3d.gif
		 */
		ImageIcon ii = new ImageIcon( TestDF4.class.getResource( "e9fa42a3ec0a38efc9d1ef2db3f12f3d.gif" ) );
		image = ii.getImage();

		this.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
		this.setSize( 1024, 750 );

		JComponent background = buildBackground();
		this.add( background );

		background.setLayout( new GridBagLayout() );

		// -> choose one of the insets:
		// with a border of 10: the table gets repainted all the time
		// with a border of 0: repainting is optimized away
		Insets insets = new Insets( 10, 10, 10, 10 );
		// Insets insets = new Insets( 0, 0, 0, 0 );

		background.add( buildDemoTable(), new GridBagConstraints( 0, 0, 1, 1, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.BOTH, insets, 0, 0 ) );

		this.setVisible( true );
	}

	private JComponent buildDemoTable() {
		Object[][] data = {
				{ "xxxx", "5" },
				{ "yyyyy", "Abc" },
				{ "zz", "7898" },
				{ "aaaa", "157" },
				{ "BBBB", "9876" },
		};

		String[] headers = { "Field", "Value" };

		DefaultTableModel dtm = new DefaultTableModel( 5, 2 ) {
			@Override
			public Object getValueAt( int r, int c ) {
				logger( "r=" + r + " : c=" + c );
				return ((Vector) dataVector.get( r )).get( c );
			}
		};
		dtm.setDataVector( data, headers );

		JTable jt = new JTable( dtm );
		jt.setRowSelectionAllowed( true );

		return new JScrollPane( jt,
				JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
				JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED );
	}

	private JComponent buildBackground() {
		return new JPanel() {
			@Override
			protected void paintComponent( Graphics g ) {
				super.paintComponent( g );

				logger( "" );

				// The icon is 267 width, so subtract it then divide by 2 to figure out where to start drawing
				int xp = (getWidth() - 267) / 2;

				if( image != null )
					g.drawImage( image, xp, 28, this );
				// 28 is to push the logo down a little bit otherwise you can see it after a tab is opened.
			}

			@Override
			public boolean imageUpdate( Image img, int infoflags, int x, int y, int w, int h ) {
				logger( String.format( "image updated: %d, %d, %d, %d", x, y, w, h ) );
				return super.imageUpdate( img, infoflags, x, y, w, h );
			}
		};
	}

	/**
	 * A simple logging method
	 * 
	 * @param data
	 */
	public static void logger( String data ) {
		String className = Thread.currentThread().getStackTrace()[2].getClassName();

		// Remove the package info.
		if( (className != null) && (className.lastIndexOf( '.' ) != -1) )
			className = className.substring( className.lastIndexOf( '.' ) + 1 );

		System.out.println( LOGGER_TIMESTAMP.format( new Date() ) + " " + className + ": " + Thread.currentThread().getStackTrace()[2].getMethodName() + ": " + data );
	}

	public static void main( String[] args ) {
		logger( "" );

		try {
			UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
		} catch( Exception e ) {
			e.printStackTrace();
		}

		new TestDF4();
	}
}

This is answer 2 of 3

We can filter unnecessary repaint cycles because we know exactly where the image is painted. We can do this by creating our own ImageObserver. Try out this demo, check out the LogoBackgroundImageObserver.

Note: if you open two editors repainting may start again, due to the image shining through the gap between the Dockables.

package test;

import java.awt.Component;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.ImageObserver;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Vector;

import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.UIManager;
import javax.swing.table.DefaultTableModel;

import bibliothek.extension.gui.dock.theme.flat.FlatStationPaint;
import bibliothek.gui.dock.common.CControl;
import bibliothek.gui.dock.common.CGrid;
import bibliothek.gui.dock.common.CWorkingArea;
import bibliothek.gui.dock.common.DefaultMultipleCDockable;
import bibliothek.gui.dock.common.MultipleCDockable;
import bibliothek.gui.dock.common.intern.CDockable;
import bibliothek.gui.dock.common.theme.CEclipseTheme;
import bibliothek.gui.dock.common.theme.ThemeMap;
import bibliothek.gui.dock.station.StationBackgroundComponent;
import bibliothek.gui.dock.themes.ThemeManager;
import bibliothek.gui.dock.toolbar.CToolbarContentArea;
import bibliothek.gui.dock.util.BackgroundComponent;
import bibliothek.gui.dock.util.BackgroundPaint;
import bibliothek.gui.dock.util.PaintableComponent;
import bibliothek.gui.dock.util.UIBridge;
import bibliothek.gui.dock.util.UIValue;

public class TestDF3 extends JFrame implements ActionListener {
	private static final SimpleDateFormat LOGGER_TIMESTAMP = new SimpleDateFormat( "yyyy/MM/dd HH:mm:ss.SSS" );

	private CControl control = null;
	private CWorkingArea cwArea = null;
	private int count = 1;
	private ImageIcon image = null;

	public TestDF3() {
		super();

		logger( "" );

		/*
		 * Get the GIF from: https://i.pinimg.com/originals/e9/fa/42/e9fa42a3ec0a38efc9d1ef2db3f12f3d.gif I put a copy
		 * of the GIF here: https://www.capitalware.com/dl/products/mqve2/e9fa42a3ec0a38efc9d1ef2db3f12f3d.gif
		 */
		image = new ImageIcon( TestDF3.class.getResource( "e9fa42a3ec0a38efc9d1ef2db3f12f3d.gif" ) );

		createMenubar();

		control = new CControl( this );

		control.setTheme( ThemeMap.KEY_ECLIPSE_THEME );
		((CEclipseTheme) control.getController().getTheme()).intern().setPaint( new FlatStationPaint() );

		this.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
		this.setSize( 1024, 750 );

		CToolbarContentArea ctcArea = new CToolbarContentArea( control, "base" );
		control.addStationContainer( ctcArea );
		/* ... and added to the frame */
		this.add( ctcArea );

		cwArea = control.createWorkingArea( "work" );
		installBackground( control, cwArea );

		CGrid grid = new CGrid( control );

		grid.add( 0, 0, 100, 100, cwArea ); // x,y w,h

		ctcArea.getCenterArea().deploy( grid );

		this.setVisible( true );
	}

	private void createMenubar() {
		logger( "" );

		JMenuBar menuBar = new JMenuBar();
		this.setJMenuBar( menuBar );

		JMenu fileMenu = new JMenu( "File" );
		fileMenu.setOpaque( true );
		menuBar.add( fileMenu );

		JMenuItem newEditor = new JMenuItem( "New Editor" );
		newEditor.setActionCommand( "NewEditor" );
		newEditor.addActionListener( this );
		fileMenu.add( newEditor );

		JMenuItem closeAllEditors = new JMenuItem( "Close All Editors" );
		closeAllEditors.setActionCommand( "CloseAllEditors" );
		closeAllEditors.addActionListener( this );
		fileMenu.add( closeAllEditors );
	}

	@Override
	public void actionPerformed( ActionEvent e ) {
		String actionCmd = e.getActionCommand();

		logger( "actionCmd=" + actionCmd );

		if( "NewEditor".equals( actionCmd ) ) {
			DefaultMultipleCDockable editor = new DefaultMultipleCDockable( null );

			editor.setTitleText( "Editor " + (count++) );
			editor.setCloseable( true );
			editor.setExternalizable( false );
			editor.setRemoveOnClose( true );

			Object[][] data = {
					{ "xxxx", "5" },
					{ "yyyyy", "Abc" },
					{ "zz", "7898" },
					{ "aaaa", "157" },
					{ "BBBB", "9876" },
			};

			String[] headers = { "Field", "Value" };

			DefaultTableModel dtm = new DefaultTableModel( 5, 2 ) {
				@Override
				public Object getValueAt( int r, int c ) {
					logger( "r=" + r + " : c=" + c );
					return ((Vector) dataVector.get( r )).get( c );
				}
			};
			dtm.setDataVector( data, headers );

			JTable jt = new JTable( dtm );
			jt.setRowSelectionAllowed( true );

			editor.add( new JScrollPane( jt,
					JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
					JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED ) );

			cwArea.show( editor );
			editor.toFront();
		} else if( "CloseAllEditors".equals( actionCmd ) ) {
			int c = control.getCDockableCount();
			for( int i = (c - 1); i >= 0; i-- ) {
				logger( "i=" + i );
				CDockable focus = control.getCDockable( i );
				if( (focus != null) && (focus.getWorkingArea() == cwArea) ) {
					control.removeDockable( (MultipleCDockable) focus );
				}
			}
		}
	}

	private void installBackground( CControl control, final CWorkingArea area ) {
		ThemeManager themeManager = control.getController().getThemeManager();

		// custom UIBridge may install custom background to any DockStation (note: there can only be one bridge for each
		// KIND of component)
		themeManager.setBackgroundPaintBridge( StationBackgroundComponent.KIND, new UIBridge<BackgroundPaint, UIValue<BackgroundPaint>>() {
			public void set( String id, BackgroundPaint value, UIValue<BackgroundPaint> uiValue ) {
				logger( "" );
				// this cast is safe, because we installed this bridge with the constant StationBackgroundComponent.KIND
				StationBackgroundComponent component = (StationBackgroundComponent) uiValue;

				if( area.getStation() == component.getStation() ) {
					logger( "[if]   id=" + id + " : value=" + value + " : uiValue=" + uiValue );
					uiValue.set( new LogoBackgroundPaint() );
				} else {
					logger( "[else] id=" + id + " : value=" + value + " : uiValue=" + uiValue );
					uiValue.set( value );
				}
			}

			public void remove( String id, UIValue<BackgroundPaint> uiValue ) {
				logger( "" );
			}

			public void add( String id, UIValue<BackgroundPaint> uiValue ) {
				logger( "       id=" + id + "              : uiValue=" + uiValue );
			}
		} );
	}

	private class LogoBackgroundPaint implements BackgroundPaint {
		public void install( BackgroundComponent component ) {
			logger( "" );
		}

		public void uninstall( BackgroundComponent component ) {
			logger( "" );
		}

		public void paint( BackgroundComponent background, PaintableComponent paintable, Graphics g ) {
			logger( "" );
			// don't forget to paint the ordinary background first
			paintable.paintBackground( g );

			// then paint a custom logo
			Component c = paintable.getComponent();

			// The icon is 267 width, so subtract it then divide by 2 to figure out where to start drawing
			int xp = (c.getWidth() - 267) / 2;

			if( image != null ) {
				// 28 is to push the logo down a little bit otherwise you can see it after a tab is opened.
				ImageObserver imageObserver = new LogoBackgroundImageObserver( (JComponent) c );
				g.drawImage( image.getImage(), xp, 28, imageObserver );
			}
		}
	}

	private class LogoBackgroundImageObserver implements ImageObserver {
		private JComponent target;

		public LogoBackgroundImageObserver( JComponent target ) {
			this.target = target;
		}

		@Override
		public boolean imageUpdate( Image img, int infoflags, int x, int y, int width, int height ) {
			int xp = (target.getWidth() - 267) / 2;
			int yp = 28;

			Rectangle clip = new Rectangle( xp, yp, image.getIconWidth(), image.getIconHeight() );

			if( isImageVisible( clip ) ) {
				return target.imageUpdate( img, infoflags, xp, yp, width, height );
			} else {
				return (infoflags & (ALLBITS | ABORT)) == 0;
			}
		}

		private boolean isImageVisible( Rectangle clip ) {
			for( int i = 0, n = target.getComponentCount(); i < n; i++ ) {
				Component child = target.getComponent( i );
				Rectangle intersection = clip.intersection( child.getBounds() );
				if( clip.equals( intersection ) ) {
					logger( String.format( "image is hidden by %s", child ) );
					return false;
				}
			}
			logger( "image is visible" );
			return true;
		}
	}

	/**
	 * A simple logging method
	 * 
	 * @param data
	 */
	public static void logger( String data ) {
		String className = Thread.currentThread().getStackTrace()[2].getClassName();

		// Remove the package info.
		if( (className != null) && (className.lastIndexOf( '.' ) != -1) )
			className = className.substring( className.lastIndexOf( '.' ) + 1 );

		System.out.println( LOGGER_TIMESTAMP.format( new Date() ) + " " + className + ": " + Thread.currentThread().getStackTrace()[2].getMethodName() + ": " + data );
	}

	public static void main( String[] args ) {
		logger( "" );

		try {
			UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
		} catch( Exception e ) {
			e.printStackTrace();
		}

		new TestDF3();
	}
}

This is answer 3 of 3:

We can further optimize repainting by playing with clipping areas. As is done in this demo, check out the LogoBackgroundImageObserver again.

Here we calculate which part of the background is actually visible, and ask Swing to repaint only the visible part. Because of clipping only the gap between Dockables gets repainted, even if the image shines through the gap.

Some warnings about this demo:

  1. this solution will not work if 3 Dockables build a T shaped gap: because we can only repaint a rectangle.
  2. you may need to make sure that repaint is not called too often, the image refreshes itself quite often. Maybe enfcore a 100 ms pause between repaint events - Component.imageUpdate already introduces such a delay.

In theory LogoBackgroundPaint could invoke paintable.paintChildren(null), this would prevent the children from being (re)painted at all. This is an optimization that would only work if you:

  • make sure not to paint over the children
  • paint the children if they actually need an update (if more than just the background image changed).

I don’t see any (easy) way to properly implement the last requirement, hence I cannot provide a demo for this idea.


package test;

import java.awt.Component;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.ImageObserver;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Vector;

import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.UIManager;
import javax.swing.table.DefaultTableModel;

import bibliothek.extension.gui.dock.theme.flat.FlatStationPaint;
import bibliothek.gui.dock.common.CControl;
import bibliothek.gui.dock.common.CGrid;
import bibliothek.gui.dock.common.CWorkingArea;
import bibliothek.gui.dock.common.DefaultMultipleCDockable;
import bibliothek.gui.dock.common.MultipleCDockable;
import bibliothek.gui.dock.common.intern.CDockable;
import bibliothek.gui.dock.common.theme.CEclipseTheme;
import bibliothek.gui.dock.common.theme.ThemeMap;
import bibliothek.gui.dock.station.StationBackgroundComponent;
import bibliothek.gui.dock.themes.ThemeManager;
import bibliothek.gui.dock.toolbar.CToolbarContentArea;
import bibliothek.gui.dock.util.BackgroundComponent;
import bibliothek.gui.dock.util.BackgroundPaint;
import bibliothek.gui.dock.util.PaintableComponent;
import bibliothek.gui.dock.util.UIBridge;
import bibliothek.gui.dock.util.UIValue;

public class TestDF3 extends JFrame implements ActionListener {
	private static final SimpleDateFormat LOGGER_TIMESTAMP = new SimpleDateFormat( "yyyy/MM/dd HH:mm:ss.SSS" );

	private CControl control = null;
	private CWorkingArea cwArea = null;
	private int count = 1;
	private ImageIcon image = null;

	public TestDF3() {
		super();

		logger( "" );

		/*
		 * Get the GIF from: https://i.pinimg.com/originals/e9/fa/42/e9fa42a3ec0a38efc9d1ef2db3f12f3d.gif I put a copy
		 * of the GIF here: https://www.capitalware.com/dl/products/mqve2/e9fa42a3ec0a38efc9d1ef2db3f12f3d.gif
		 */
		image = new ImageIcon( TestDF3.class.getResource( "e9fa42a3ec0a38efc9d1ef2db3f12f3d.gif" ) );

		createMenubar();

		control = new CControl( this );

		control.setTheme( ThemeMap.KEY_ECLIPSE_THEME );
		((CEclipseTheme) control.getController().getTheme()).intern().setPaint( new FlatStationPaint() );

		this.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
		this.setSize( 1024, 750 );

		CToolbarContentArea ctcArea = new CToolbarContentArea( control, "base" );
		control.addStationContainer( ctcArea );
		/* ... and added to the frame */
		this.add( ctcArea );

		cwArea = control.createWorkingArea( "work" );
		installBackground( control, cwArea );

		CGrid grid = new CGrid( control );

		grid.add( 0, 0, 100, 100, cwArea ); // x,y w,h

		ctcArea.getCenterArea().deploy( grid );

		this.setVisible( true );
	}

	private void createMenubar() {
		logger( "" );

		JMenuBar menuBar = new JMenuBar();
		this.setJMenuBar( menuBar );

		JMenu fileMenu = new JMenu( "File" );
		fileMenu.setOpaque( true );
		menuBar.add( fileMenu );

		JMenuItem newEditor = new JMenuItem( "New Editor" );
		newEditor.setActionCommand( "NewEditor" );
		newEditor.addActionListener( this );
		fileMenu.add( newEditor );

		JMenuItem closeAllEditors = new JMenuItem( "Close All Editors" );
		closeAllEditors.setActionCommand( "CloseAllEditors" );
		closeAllEditors.addActionListener( this );
		fileMenu.add( closeAllEditors );
	}

	@Override
	public void actionPerformed( ActionEvent e ) {
		String actionCmd = e.getActionCommand();

		logger( "actionCmd=" + actionCmd );

		if( "NewEditor".equals( actionCmd ) ) {
			DefaultMultipleCDockable editor = new DefaultMultipleCDockable( null );

			editor.setTitleText( "Editor " + (count++) );
			editor.setCloseable( true );
			editor.setExternalizable( false );
			editor.setRemoveOnClose( true );

			Object[][] data = {
					{ "xxxx", "5" },
					{ "yyyyy", "Abc" },
					{ "zz", "7898" },
					{ "aaaa", "157" },
					{ "BBBB", "9876" },
			};

			String[] headers = { "Field", "Value" };

			DefaultTableModel dtm = new DefaultTableModel( 5, 2 ) {
				@Override
				public Object getValueAt( int r, int c ) {
					logger( "r=" + r + " : c=" + c );
					return ((Vector) dataVector.get( r )).get( c );
				}
			};
			dtm.setDataVector( data, headers );

			JTable jt = new JTable( dtm );
			jt.setRowSelectionAllowed( true );

			editor.add( new JScrollPane( jt,
					JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
					JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED ) );

			cwArea.show( editor );
			editor.toFront();
		} else if( "CloseAllEditors".equals( actionCmd ) ) {
			int c = control.getCDockableCount();
			for( int i = (c - 1); i >= 0; i-- ) {
				logger( "i=" + i );
				CDockable focus = control.getCDockable( i );
				if( (focus != null) && (focus.getWorkingArea() == cwArea) ) {
					control.removeDockable( (MultipleCDockable) focus );
				}
			}
		}
	}

	private void installBackground( CControl control, final CWorkingArea area ) {
		ThemeManager themeManager = control.getController().getThemeManager();

		// custom UIBridge may install custom background to any DockStation (note: there can only be one bridge for each
		// KIND of component)
		themeManager.setBackgroundPaintBridge( StationBackgroundComponent.KIND, new UIBridge<BackgroundPaint, UIValue<BackgroundPaint>>() {
			public void set( String id, BackgroundPaint value, UIValue<BackgroundPaint> uiValue ) {
				logger( "" );
				// this cast is safe, because we installed this bridge with the constant StationBackgroundComponent.KIND
				StationBackgroundComponent component = (StationBackgroundComponent) uiValue;

				if( area.getStation() == component.getStation() ) {
					logger( "[if]   id=" + id + " : value=" + value + " : uiValue=" + uiValue );
					uiValue.set( new LogoBackgroundPaint() );
				} else {
					logger( "[else] id=" + id + " : value=" + value + " : uiValue=" + uiValue );
					uiValue.set( value );
				}
			}

			public void remove( String id, UIValue<BackgroundPaint> uiValue ) {
				logger( "" );
			}

			public void add( String id, UIValue<BackgroundPaint> uiValue ) {
				logger( "       id=" + id + "              : uiValue=" + uiValue );
			}
		} );
	}

	private class LogoBackgroundPaint implements BackgroundPaint {
		public void install( BackgroundComponent component ) {
			logger( "" );
		}

		public void uninstall( BackgroundComponent component ) {
			logger( "" );
		}

		public void paint( BackgroundComponent background, PaintableComponent paintable, Graphics g ) {
			logger( "" );
			// don't forget to paint the ordinary background first
			paintable.paintBackground( g );

			// then paint a custom logo
			Component c = paintable.getComponent();

			// The icon is 267 width, so subtract it then divide by 2 to figure out where to start drawing
			int xp = (c.getWidth() - 267) / 2;

			if( image != null ) {
				// 28 is to push the logo down a little bit otherwise you can see it after a tab is opened.
				ImageObserver imageObserver = new LogoBackgroundImageObserver( (JComponent) c );
				g.drawImage( image.getImage(), xp, 28, imageObserver );
			}
		}
	}

	private class LogoBackgroundImageObserver implements ImageObserver {
		private JComponent target;

		public LogoBackgroundImageObserver( JComponent target ) {
			this.target = target;
		}

		@Override
		public boolean imageUpdate( Image img, int infoflags, int x, int y, int width, int height ) {
			int xp = (target.getWidth() - 267) / 2;
			int yp = 28;

			Rectangle clip = new Rectangle( xp, yp, image.getIconWidth(), image.getIconHeight() );

			Rectangle visible = visibleClip( clip );

			if( visible != null ) {
				logger( String.format( "image visible, visible part=%s, total image=%s", visible, clip ) );
				target.repaint( 20, visible.x, visible.y, visible.width, visible.height );
			} else {
				logger( String.format( "image not visible" ) );
			}

			return (infoflags & (ALLBITS | ABORT)) == 0;
		}

		private Rectangle visibleClip( Rectangle clip ) {
			for( int i = 0, n = target.getComponentCount(); i < n; i++ ) {
				Component child = target.getComponent( i );
				Rectangle intersection = clip.intersection( child.getBounds() );

				if( clip.equals( intersection ) ) {
					return null;
				}
				if( intersection.width > 0 && intersection.height > 0 ) {
					int x;
					int y;
					int width = clip.width - intersection.width;
					int height = clip.height - intersection.height;

					if( clip.x == intersection.x && width > 0 ) {
						x = intersection.x + intersection.width;
					} else {
						x = clip.x;
					}

					if( clip.y == intersection.y && height > 0 ) {
						y = intersection.y + intersection.height;
					} else {
						y = clip.y;
					}

					if( width == 0 ) {
						width = clip.width;
					}
					if( height == 0 ) {
						height = clip.height;
					}

					clip = new Rectangle( x, y, width, height );
				}
			}
			return clip;
		}
	}

	/**
	 * A simple logging method
	 * 
	 * @param data
	 */
	public static void logger( String data ) {
		String className = Thread.currentThread().getStackTrace()[2].getClassName();

		// Remove the package info.
		if( (className != null) && (className.lastIndexOf( '.' ) != -1) )
			className = className.substring( className.lastIndexOf( '.' ) + 1 );

		System.out.println( LOGGER_TIMESTAMP.format( new Date() ) + " " + className + ": " + Thread.currentThread().getStackTrace()[2].getMethodName() + ": " + data );
	}

	public static void main( String[] args ) {
		logger( "" );

		try {
			UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
		} catch( Exception e ) {
			e.printStackTrace();
		}

		new TestDF3();
	}
}

Hi Beni,

I was going to add more information about the application before using Docking Frames but I thought the post was getting too long.

Before Docking Frames, I used the JTabbedPane class and added/removed tabs as the user needed. When either there were no tabs displayed or on initial startup it displayed a background image.

private void addBackGroundImage( )
{
   gbc.anchor = GridBagConstraints.CENTER;
   gbc.fill = GridBagConstraints.BOTH;
   gbc.gridwidth = GridBagConstraints.REMAINDER;
   gbc.weightx = 1.0;
   gbc.weighty = 1.0;
   gbc.insets = new Insets(25,0,0,0);

   JLabel backGroundImageLabel = new JLabel(new ImageIcon(Anchor.class.getResource("gif/my_image.gif")));
   backGroundImagePane = new JPanel();
   backGroundImagePane.add(backGroundImageLabel,gbc);

   gbc.insets = new Insets(0,0,0,0);
   gbc.gridx = 0;
   gbc.gridy = 2;
   gbc.gridwidth = 50;
   gbc.gridheight = 50;
   getContentPane().add(backGroundImagePane, gbc);
   SwingUtilities.updateComponentTreeUI(this.getContentPane());

   backGroundImageIsShown = true;
}

And in the code to open a new tab, I had:

if (backGroundImageIsShown)
{
   remove(backGroundImagePane);
   backGroundImagePane = null;
   backGroundImageIsShown = false;
   createMainTabbedPane();
}

And where ever the user closed a tab of the JTabbedPane, the code would do:

if (jtp.getTabCount() == 0)
{
   remove(jtp);
   jtp = null;
   addBackGroundImage();
}

So, to your comment in post 1/3, I understand but I never overlaided the JTable or tab with a JTable over the background image.

Your posts 2/3 and 3/3 do not work for me because most users will have several docks open in the CWorkingArea.

If we cannot control what the ThemeManager BackgroundPaint is doing then lets go in the other direction. Can it be removed (aka un-installed) when the user opens a dock in the CWorkingArea and when all docks of the CWorkingArea are closed re-install the background image?

This would seem like the simplest solution.

Regards,
Roger

Sure. To uninstall just call themeManager.setBackgroundPaintBridge with null as argument.

You use CWorkingArea.getStation().getDockStation().addDockStationListener(...) to add a listener that is informed whenever children are added or removed from the station.

To uninstall just call themeManager.setBackgroundPaintBridge with null as argument.

I tried a bunch of combinations and I think you meant:

private void unInstallBackground( CControl control, final CWorkingArea area )
{
   ThemeManager themeManager = control.getController().getThemeManager();
   themeManager.setBackgroundPaintBridge( StationBackgroundComponent.KIND, null);
}

That appears to remove the image BUT the Docking Frames is still doing something in the background driving calls to the layers above it.

Can you provide a working sample? How about you update the Test_DF3 class that I posted above with an unInstallBackground method that is called when a „NewEditor“ action command is invoked.

Regards,
Roger

Hi Beni,

As per my previous posting, I really could use a a working sample on un-installing the background.

How about you update the Test_DF3 class that I posted above with an unInstallBackground method that is called when a „NewEditor“ action command is invoked.

Regards,
Roger

Uh, I did not see your question. :frowning: Don’t have much time today, will write an answer later (guaranteed this week).