Author: bpoussin Date: 2010-11-04 16:08:51 +0100 (Thu, 04 Nov 2010) New Revision: 290 Url: http://nuiton.org/repositories/revision/nuiton-widgets/290 Log: first version of save/restore of swing UI Added: trunk/src/main/java/org/nuiton/widget/SwingSession.java Added: trunk/src/main/java/org/nuiton/widget/SwingSession.java =================================================================== --- trunk/src/main/java/org/nuiton/widget/SwingSession.java (rev 0) +++ trunk/src/main/java/org/nuiton/widget/SwingSession.java 2010-11-04 15:08:51 UTC (rev 290) @@ -0,0 +1,904 @@ +package org.nuiton.widget; + + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dialog; +import java.awt.Frame; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.Rectangle; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.DefaultPersistenceDelegate; +import java.beans.Encoder; +import java.beans.ExceptionListener; +import java.beans.Expression; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.XMLDecoder; +import java.beans.XMLEncoder; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import javax.mail.Session; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTabbedPane; +import javax.swing.JTable; +import javax.swing.JTextArea; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableColumn; +import javax.swing.table.TableModel; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Use to store and restore position and size of application. Supported widgets + * are: + * <li> java.awt.Window (and subclasses) + * <li> javax.swing.JTabbedPane (and subclasses) + * <li> javax.swing.JSplitPane (and subclasses) + * <li> javax.swing.JTable (and subclasses) + * + * You can use: + * <li> explicite call to save + * <li> auto save + * + * This code is partialy inspired from http://kenai.com/projects/bsaf/pages/Home + * project. This project is under LGPL v2.1 license. We can't reuse directly this + * library because to many fields and methods are private and we can't implements + * it and modify some behavior. + * + * @author poussin + * @version $Revision$ + * + * Last update: $Date$ + * by : $Author$ + */ +public class SwingSession { + + /** to use log facility, just put in your code: log.info(\"...\"); */ + static private Log log = LogFactory.getLog(SwingSession.class); + + protected File file; + protected boolean autoSave; + protected LinkedHashSet<Component> registeredComponent = + new LinkedHashSet<Component>(); + /** State object registered to get and set State. + * key: class of component managed by the state; value: the state*/ + protected Map<Class, State> stateManager = new HashMap<Class, State>(); + /** state of all component added with add method. + * key: path of compoenent; value: State */ + protected Map<String, State> states; + + public SwingSession(File file, boolean autoSave) { + this.file = file; + this.autoSave = autoSave; + stateManager.put(Window.class, new WindowState()); + stateManager.put(JTable.class, new JTableState()); + stateManager.put(JTabbedPane.class, new JTabbedPaneState()); + stateManager.put(JSplitPane.class, new JSplitPaneState()); + + states = loadStates(file); + if (states == null) { + states = new HashMap<String, State>(); + } + } + + @Override + protected void finalize() throws Throwable { + save(); + super.finalize(); + } + + + /* If an exception occurs in the XMLEncoder/Decoder, we want + * to throw an IOException. The exceptionThrow listener method + * doesn't throw a checked exception so we just set a flag + * here and check it when the encode/decode operation finishes + */ + private static class AbortExceptionListener implements ExceptionListener { + + public Exception exception = null; + + @Override + public void exceptionThrown(Exception e) { + if (exception == null) { + exception = e; + } + } + } + + /* There are some (old) Java classes that aren't proper beans. Rectangle + * is one of these. When running within the secure sandbox, writing a + * Rectangle with XMLEncoder causes a security exception because + * DefaultPersistenceDelegate calls Field.setAccessible(true) to gain + * access to private fields. This is a workaround for that problem. + * A bug has been filed, see JDK bug ID 4741757 + */ + private static class RectanglePD extends DefaultPersistenceDelegate { + + public RectanglePD() { + super(new String[]{"x", "y", "width", "height"}); + } + + @Override + protected Expression instantiate(Object oldInstance, Encoder out) { + Rectangle oldR = (Rectangle) oldInstance; + Object[] constructorArgs = new Object[]{ + oldR.x, oldR.y, oldR.width, oldR.height + }; + return new Expression(oldInstance, oldInstance.getClass(), "new", constructorArgs); + } + } + + public void save() { + updateState(); + AbortExceptionListener el = new AbortExceptionListener(); + ByteArrayOutputStream bst = new ByteArrayOutputStream(); + XMLEncoder e = null; + /* Buffer the XMLEncoder's output so that decoding errors don't + * cause us to trash the current version of the specified file. + */ + try { + e = new XMLEncoder(bst); + e.setPersistenceDelegate(Rectangle.class, new RectanglePD()); + e.setExceptionListener(el); + e.writeObject(states); + } finally { + if (e != null) { + e.close(); + } + } + if (el.exception != null) { + log.warn("save failed \"" + file + "\"", el.exception); + } else { + OutputStream ost = null; + try { + ost = new FileOutputStream(file); + ost.write(bst.toByteArray()); + } catch (IOException eee) { + log.warn("save failed \"" + file + "\"", eee); + } finally { + if (ost != null) { + try { + ost.close(); + } catch (IOException eee) { + log.warn("can't close properly \"" + file + "\"", eee); + } + } + } + } + } + + /** + * Loads the states from the file + */ + public Map<String, State> loadStates(File file) { + Map<String, State> result = null; + if (file.exists()) { + XMLDecoder d = null; + try { + InputStream ist = new FileInputStream(file); + d = new XMLDecoder(ist); + AbortExceptionListener eee = new AbortExceptionListener(); + d.setExceptionListener(eee); + Object bean = d.readObject(); + if (eee.exception != null) { + log.warn("load failed \"" + file + "\"", eee.exception); + } else { + result = (Map<String, State>) bean; + } + } catch (IOException eee) { + log.warn("load failed \"" + file + "\"", eee); + } finally { + if (d != null) { + d.close(); + } + } + } + return result; + } + + public void updateState() { + walkThrowComponent("", registeredComponent, + new SaveStateAction()); + } + + public void add(Component c) { + if (registeredComponent.contains(c)) { + log.warn(String.format( + "Component already added %s(%s)", c.getClass(), c.getName())); + } else { + registeredComponent.add(c); + walkThrowComponent("", Collections.singleton(c), + new RestoreStateAction()); + if (autoSave) { + // register do save if necessary + walkThrowComponent("", Collections.singleton(c), + new RegisterAsListenerAction()); + } + } + } + + public void remove(Component c) { + registeredComponent.remove(c); + if (autoSave) { + walkThrowComponent("/", Collections.singleton(c), + new UnregisterAsListenerAction()); + } + } + + protected String getComponentName(Component c) { + String name = c.getName(); + if (name == null) { + int n = c.getParent().getComponentZOrder(c); + if (n >= 0) { + Class clazz = c.getClass(); + name = clazz.getSimpleName(); + if (name.length() == 0) { + name = "Anonymous" + clazz.getSuperclass().getSimpleName(); + } + name = name + n; + } else { + // Implies that the component tree is changing + // while we're computing the path. Punt. + log.warn("Couldn't compute pathname for " + c); + } + } + return name; + } + + public State getStateManager(Class clazz) { + State result = null; + while (result == null && clazz != null) { + result = stateManager.get(clazz); + clazz = clazz.getSuperclass(); + } + return result; + } + + public State getStates(String path) { + return states.get(path); + } + + public void setStates(String path, State state) { + this.states.put(path, state); + } + + protected void walkThrowComponent( + String path, Collection<Component> roots, Action action) { + for (Component root : roots) { + if (root != null) { + String pathname = path + "/" + getComponentName(root); + State state = getStateManager(root.getClass()); + if (state != null) { + action.doAction(this, pathname, root); + } + if (root instanceof Container) { + Component[] children = ((Container) root).getComponents(); + if ((children != null) && (children.length > 0)) { + walkThrowComponent(pathname, Arrays.asList(children), action); + } + } + if (root instanceof JFrame) { + Component[] children = ((JFrame) root).getContentPane().getComponents(); + if ((children != null) && (children.length > 0)) { + walkThrowComponent(pathname, Arrays.asList(children), action); + } + } + } + } + } + + static public interface Action { + public void doAction(SwingSession session, String path, Component c); + } + + static public class SaveStateAction implements Action { + @Override + public void doAction(SwingSession session, String path, Component c) { + State manager = session.getStateManager(c.getClass()); + State state = manager.getState(c); + session.setStates(path, state); + } + } + + static public class RestoreStateAction implements Action { + @Override + public void doAction(SwingSession session, String path, Component c) { + State manager = session.getStateManager(c.getClass()); + State state = session.getStates(path); + if (state != null) { + manager.setState(c, state); + } + } + } + + static public class RegisterAsListenerAction implements Action { + @Override + public void doAction(SwingSession session, String path, Component c) { + State state = session.getStates(path); + if (state == null) { + State manager = session.getStateManager(c.getClass()); + state = manager.getState(c); + session.setStates(path, state); + } + state.addListener(c, state); + } + } + + static public class UnregisterAsListenerAction implements Action { + @Override + public void doAction(SwingSession session, String path, Component c) { + State state = session.getStates(path); + if (state != null) { + state.removeListener(c, state); + } + } + } + + /** + * get(save) and set(restore) state of object passed in argument + */ + static public interface State { + public void addListener(Object o, Object l); + public void removeListener(Object o, Object l); + public State getState(Object o); + public void setState(Object o, State state); + } + + /** + * State for JTabbedPane + */ + static public class JTabbedPaneState implements State, ChangeListener { + + protected int selectedIndex = -1; + protected int tabCount; + + public JTabbedPaneState() { + } + + public int getSelectedIndex() { + return selectedIndex; + } + + public void setSelectedIndex(int selectedIndex) { + this.selectedIndex = selectedIndex; + } + + public int getTabCount() { + return tabCount; + } + + public void setTabCount(int tabCount) { + this.tabCount = tabCount; + } + + + protected JTabbedPane checkComponent(Object o) { + if (o == null) { + throw new IllegalArgumentException("null component"); + } + if (!(o instanceof JTabbedPane)) { + throw new IllegalArgumentException("invalid component"); + } + return (JTabbedPane) o; + } + + + @Override + public void addListener(Object o, Object l) { + JTabbedPane p = checkComponent(o); + p.addChangeListener((ChangeListener)l); + } + + @Override + public void removeListener(Object o, Object l) { + JTabbedPane p = checkComponent(o); + p.removeChangeListener((ChangeListener)l); + } + + public void updateState(Object o, JTabbedPaneState state) { + JTabbedPane p = checkComponent(o); + state.setSelectedIndex(p.getSelectedIndex()); + state.setTabCount(p.getTabCount()); + } + + @Override + public State getState(Object o) { + JTabbedPaneState result = new JTabbedPaneState(); + updateState(o, result); + return result; + } + + @Override + public void setState(Object o, State state) { + if (state == null) { + return; + } + if (state instanceof JTabbedPaneState) { + JTabbedPane p = checkComponent(o); + JTabbedPaneState tps = (JTabbedPaneState) state; + if (tps.getSelectedIndex() != -1 + && p.getTabCount() == tps.getTabCount()) { + p.setSelectedIndex(tps.getSelectedIndex()); + } + } else { + throw new IllegalArgumentException("invalid state"); + } + } + + @Override + public void stateChanged(ChangeEvent e) { + updateState(e.getSource(), this); + } + + } + + /** + * State for JSplit + * FIXME add listener for divider move action + */ + static public class JSplitPaneState implements State, PropertyChangeListener { + + protected int dividerLocation = -1; + protected int orientation = JSplitPane.HORIZONTAL_SPLIT; + + public JSplitPaneState() { + } + + public int getDividerLocation() { + return dividerLocation; + } + + public void setDividerLocation(int dividerLocation) { + this.dividerLocation = dividerLocation; + } + + public int getOrientation() { + return orientation; + } + + public void setOrientation(int orientation) { + this.orientation = orientation; + } + + protected JSplitPane checkComponent(Object o) { + if (o == null) { + throw new IllegalArgumentException("null component"); + } + if (!(o instanceof JSplitPane)) { + throw new IllegalArgumentException("invalid component"); + } + return (JSplitPane) o; + } + + @Override + public void addListener(Object o, Object l) { + throw new UnsupportedOperationException("Not supported yet."); + // FIXME poussin 20101104 est ce possible de se mettre listener + // pour recevoir les events de modification de la possition du split ? + } + + @Override + public void removeListener(Object o, Object l) { + throw new UnsupportedOperationException("Not supported yet."); + } + + protected void updateState(Object o, JSplitPaneState state) { + JSplitPane p = checkComponent(o); + state.setDividerLocation(p.getUI().getDividerLocation(p)); + state.setOrientation(p.getOrientation()); + } + + @Override + public State getState(Object o) { + JSplitPaneState result = new JSplitPaneState(); + updateState(o, result); + return result; + } + + @Override + public void setState(Object o, State state) { + if (state == null) { + return; + } + JSplitPane p = checkComponent(o); + if (state instanceof JSplitPaneState) { + JSplitPaneState sps = (JSplitPaneState) state; + if (sps.getDividerLocation() != -1 + && p.getOrientation() == sps.getOrientation()) { + p.setDividerLocation(sps.getDividerLocation()); + } + } else { + throw new IllegalArgumentException("invalid state"); + } + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + throw new UnsupportedOperationException("Not supported yet."); + } + + } + + /** + * State for JTable. + * TODO add support for column order change + */ + static public class JTableState implements State, PropertyChangeListener { + + protected int[] columnWidths = new int[0]; + + public JTableState() { + } + + public JTableState(int[] columnWidths) { + this.columnWidths = columnWidths; + } + + public int[] getColumnWidths() { + return columnWidths; + } + + public void setColumnWidths(int[] columnWidths) { + this.columnWidths = columnWidths; + } + + protected JTable checkComponent(Object o) { + if (o == null) { + throw new IllegalArgumentException("null component"); + } + if (!(o instanceof JTable)) { + throw new IllegalArgumentException("invalid component"); + } + return (JTable) o; + } + + @Override + public void addListener(Object o, Object l) { + JTable table = checkComponent(o); + for (Enumeration<TableColumn> tc = + table.getColumnModel().getColumns(); tc.hasMoreElements();) { + // on se met listener pour recevoir les events de resize + tc.nextElement().addPropertyChangeListener(this); + } + } + + @Override + public void removeListener(Object o, Object l) { + JTable table = checkComponent(o); + for (Enumeration<TableColumn> tc = + table.getColumnModel().getColumns(); tc.hasMoreElements();) { + tc.nextElement().removePropertyChangeListener(this); + } + } + + public void updateState(Object o, JTableState state) { + JTable table = checkComponent(o); + int[] columnWidths = new int[table.getColumnCount()]; + boolean resizableColumnExists = false; + for (int i = 0; i < columnWidths.length; i++) { + TableColumn tc = table.getColumnModel().getColumn(i); + columnWidths[i] = (tc.getResizable()) ? tc.getWidth() : -1; + if (tc.getResizable()) { + resizableColumnExists = true; + } + } + if (resizableColumnExists) { + state.setColumnWidths(columnWidths); + } + } + + @Override + public State getState(Object o) { + JTableState result = new JTableState(columnWidths); + updateState(o, result); + return result; + } + + @Override + public void setState(Object o, State state) { + if (!(state instanceof JTableState)) { + throw new IllegalArgumentException("invalid state"); + } + JTable table = checkComponent(o); + int[] columnWidths = ((JTableState) state).getColumnWidths(); + if (columnWidths != null + && table.getColumnCount() == columnWidths.length) { + for (int i = 0; i < columnWidths.length; i++) { + if (columnWidths[i] != -1) { + TableColumn tc = table.getColumnModel().getColumn(i); + if (tc.getResizable()) { + tc.setPreferredWidth(columnWidths[i]); + } + } + } + } + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + updateState(evt.getSource(), this); + } + + } + + /** + * State for Window + */ + static public class WindowState implements State, PropertyChangeListener { + private static final String WINDOW_STATE_NORMAL_BOUNDS = + "WindowState.normalBounds"; + + protected Rectangle bounds; + protected Rectangle gcBounds; + protected int frameState = Frame.NORMAL; + + public WindowState() { + } + + public WindowState(Rectangle bounds, Rectangle gcBounds, int frameState) { + this.bounds = new Rectangle(bounds); + this.gcBounds = new Rectangle(gcBounds); + this.frameState = frameState; + } + + public Rectangle getBounds() { + return bounds; + } + + public void setBounds(Rectangle bounds) { + this.bounds = bounds; + } + + public Rectangle getGcBounds() { + return gcBounds; + } + + public void setGcBounds(Rectangle gcBounds) { + this.gcBounds = gcBounds; + } + + public int getFrameState() { + return frameState; + } + + public void setFrameState(int frameState) { + this.frameState = frameState; + } + + protected Window checkComponent(Object o) { + if (o == null) { + throw new IllegalArgumentException("null component"); + } + if (!(o instanceof Window)) { + throw new IllegalArgumentException("invalid component"); + } + return (Window) o; + } + + /** + * Checks whether the window supports resizing + * @param window the {@code Window} to be checked + * @return true if the window supports resizing + */ + protected static boolean isResizable(Window window) { + boolean resizable = true; + if (window instanceof Frame) { + resizable = ((Frame) window).isResizable(); + } else if (window instanceof Dialog) { + resizable = ((Dialog) window).isResizable(); + } + return resizable; + } + + /** + * Gets {@code Window} bounds from the client property + * @param window the source {@code Window} + * @return bounds from the client property + */ + protected static Rectangle getWindowNormalBounds(Window window) { + Rectangle result = null; + if (window instanceof JFrame) { + Object res = ((JFrame) window).getRootPane().getClientProperty( + WINDOW_STATE_NORMAL_BOUNDS); + if (res instanceof Rectangle) { + result = (Rectangle) res; + } + } + return result; + } + + /** + * Calculates virtual graphic bounds. + * On multiscreen systems all screens are united into one virtual screen. + * @return the graphic bounds + */ + public static Rectangle computeVirtualGraphicsBounds() { + Rectangle virtualBounds = new Rectangle(); + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice[] gs = ge.getScreenDevices(); + for (GraphicsDevice gd : gs) { + GraphicsConfiguration gc = gd.getDefaultConfiguration(); + virtualBounds = virtualBounds.union(gc.getBounds()); + } + return virtualBounds; + } + + /** + * Puts {@code Window} bounds to client property. + * @param window the target {@code Window} + * @param bounds bounds + */ + public static void putWindowNormalBounds(Window window, Rectangle bounds) { + if (window instanceof JFrame) { + ((JFrame) window).getRootPane().putClientProperty( + WINDOW_STATE_NORMAL_BOUNDS, bounds); + } + } + + @Override + public void addListener(Object o, Object l) { + Window w = checkComponent(o); + w.addPropertyChangeListener("extendedState", (PropertyChangeListener)l); + w.addPropertyChangeListener("bounds", (PropertyChangeListener)l); + } + + @Override + public void removeListener(Object o, Object l) { + Window w = checkComponent(o); + w.removePropertyChangeListener("extendedState", (PropertyChangeListener)l); + w.removePropertyChangeListener("bounds", (PropertyChangeListener)l); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + updateState(evt.getSource(), this); + } + + protected void updateState(Object o, WindowState state) { + Window c = checkComponent(o); + int frameState = Frame.NORMAL; + if (c instanceof Frame) { + frameState = ((Frame) c).getExtendedState(); + } + GraphicsConfiguration gc = c.getGraphicsConfiguration(); + Rectangle gcBounds = (gc == null) ? null : gc.getBounds(); + Rectangle frameBounds = c.getBounds(); + /* If this is a JFrame created by FrameView and it's been maximized, + * retrieve the frame's normal (not maximized) bounds. More info: + * see FrameStateListener#windowStateChanged in FrameView. + */ + if ((c instanceof JFrame) && (0 != (frameState & Frame.MAXIMIZED_BOTH))) { + frameBounds = getWindowNormalBounds((JFrame) c); + } + if (frameBounds.isEmpty()) { + return; + } + state.setBounds(frameBounds); + state.setGcBounds(gcBounds); + state.setFrameState(frameState); + } + + @Override + public State getState(Object o) { + WindowState result = new WindowState(); + updateState(o, result); + return result; + } + + @Override + public void setState(Object o, State state) { + Window w = checkComponent(o); + if ((state != null) && !(state instanceof WindowState)) { + throw new IllegalArgumentException("invalid state"); + } + WindowState windowState = (WindowState) state; + if (windowState.getBounds() != null) { + putWindowNormalBounds(w, windowState.getBounds()); + if (!w.isLocationByPlatform() && (state != null)) { + + Rectangle gcBounds0 = windowState.getGcBounds(); + if (gcBounds0 != null && isResizable(w)) { + if (computeVirtualGraphicsBounds().contains(gcBounds0.getLocation())) { + w.setBounds(windowState.getBounds()); + } else { + w.setSize(windowState.getBounds().getSize()); + } + } + if (w instanceof Frame) { + ((Frame) w).setExtendedState(windowState.getFrameState()); + + } + } + } + } + + } + + + /** + * Just for test rapidly + * @param args + */ + static public void main(String[] args) { + final SwingSession session = + new SwingSession(new File("/tmp/SwingSession.config"), false); + + JFrame frame = new JFrame("truc"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setBounds(10, 20, 300, 500); + + TableModel dataModel = new AbstractTableModel() { + + @Override + public String getColumnName(int column) { + return "c" + column; + } + + @Override + public int getColumnCount() { + return 5; + } + + @Override + public int getRowCount() { + return 10; + } + + @Override + public Object getValueAt(int row, int col) { + return new Integer(row * col); + } + }; + JTable table = new JTable(dataModel); + table.setName("MaTable"); + + JTabbedPane tab = new JTabbedPane(); + tab.setName("MaTab"); + tab.add("tab1", new JTextArea()); + tab.add("tab2", new JTextArea()); + tab.add("tab3", new JTextArea()); + + JSplitPane split = new JSplitPane(); + split.setTopComponent(new JScrollPane(table)); + split.setBottomComponent(tab); + + JButton button = new JButton("Save"); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + session.save(); + } + }); + + frame.getContentPane().setLayout(new BorderLayout()); + frame.getContentPane().add(button, BorderLayout.NORTH); + frame.getContentPane().add(split, BorderLayout.CENTER); + + frame.setVisible(true); + session.add(frame); + session.save(); + } +}
participants (1)
-
bpoussin@users.nuiton.org