001/*
002 * $Id: MultiSplitPane.java,v 1.15 2005/10/26 14:29:54 hansmuller Exp $
003 *
004 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015 * Lesser General Public License for more details.
016 *
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020 */
021package org.openstreetmap.josm.gui.widgets;
022
023import java.awt.Color;
024import java.awt.Cursor;
025import java.awt.Graphics;
026import java.awt.Graphics2D;
027import java.awt.Rectangle;
028import java.awt.event.KeyEvent;
029import java.awt.event.KeyListener;
030import java.awt.event.MouseEvent;
031
032import javax.accessibility.AccessibleContext;
033import javax.accessibility.AccessibleRole;
034import javax.swing.JPanel;
035import javax.swing.event.MouseInputAdapter;
036
037import org.openstreetmap.josm.gui.widgets.MultiSplitLayout.Divider;
038import org.openstreetmap.josm.gui.widgets.MultiSplitLayout.Node;
039
040/**
041 *
042 * <p>
043 * All properties in this class are bound: when a properties value
044 * is changed, all PropertyChangeListeners are fired.
045 *
046 * @author Hans Muller - SwingX
047 */
048public class MultiSplitPane extends JPanel {
049    private transient AccessibleContext accessibleContext;
050    private boolean continuousLayout = true;
051    private transient DividerPainter dividerPainter = new DefaultDividerPainter();
052
053    /**
054     * Creates a MultiSplitPane with it's LayoutManager set to
055     * to an empty MultiSplitLayout.
056     */
057    public MultiSplitPane() {
058        super(new MultiSplitLayout());
059        InputHandler inputHandler = new InputHandler();
060        addMouseListener(inputHandler);
061        addMouseMotionListener(inputHandler);
062        addKeyListener(inputHandler);
063        setFocusable(true);
064    }
065
066    /**
067     * A convenience method that returns the layout manager cast to MultiSplitLayout.
068     *
069     * @return this MultiSplitPane's layout manager
070     * @see java.awt.Container#getLayout
071     * @see #setModel
072     */
073    public final MultiSplitLayout getMultiSplitLayout() {
074        return (MultiSplitLayout) getLayout();
075    }
076
077    /**
078     * A convenience method that sets the MultiSplitLayout model.
079     * Equivalent to <code>getMultiSplitLayout.setModel(model)</code>
080     *
081     * @param model the root of the MultiSplitLayout model
082     * @see #getMultiSplitLayout
083     * @see MultiSplitLayout#setModel
084     */
085    public final void setModel(Node model) {
086        getMultiSplitLayout().setModel(model);
087    }
088
089    /**
090     * A convenience method that sets the MultiSplitLayout dividerSize
091     * property. Equivalent to
092     * <code>getMultiSplitLayout().setDividerSize(newDividerSize)</code>.
093     *
094     * @param dividerSize the value of the dividerSize property
095     * @see #getMultiSplitLayout
096     * @see MultiSplitLayout#setDividerSize
097     */
098    public final void setDividerSize(int dividerSize) {
099        getMultiSplitLayout().setDividerSize(dividerSize);
100    }
101
102    /**
103     * Sets the value of the <code>continuousLayout</code> property.
104     * If true, then the layout is revalidated continuously while
105     * a divider is being moved.  The default value of this property
106     * is true.
107     *
108     * @param continuousLayout value of the continuousLayout property
109     * @see #isContinuousLayout
110     */
111    public void setContinuousLayout(boolean continuousLayout) {
112        boolean oldContinuousLayout = continuousLayout;
113        this.continuousLayout = continuousLayout;
114        firePropertyChange("continuousLayout", oldContinuousLayout, continuousLayout);
115    }
116
117    /**
118     * Returns true if dragging a divider only updates
119     * the layout when the drag gesture ends (typically, when the
120     * mouse button is released).
121     *
122     * @return the value of the <code>continuousLayout</code> property
123     * @see #setContinuousLayout
124     */
125    public boolean isContinuousLayout() {
126        return continuousLayout;
127    }
128
129    /**
130     * Returns the Divider that's currently being moved, typically
131     * because the user is dragging it, or null.
132     *
133     * @return the Divider that's being moved or null.
134     */
135    public Divider activeDivider() {
136        return dragDivider;
137    }
138
139    /**
140     * Draws a single Divider.  Typically used to specialize the
141     * way the active Divider is painted.
142     *
143     * @see #getDividerPainter
144     * @see #setDividerPainter
145     */
146    public interface DividerPainter {
147        /**
148         * Paint a single Divider.
149         *
150         * @param g the Graphics object to paint with
151         * @param divider the Divider to paint
152         */
153        void paint(Graphics g, Divider divider);
154    }
155
156    private class DefaultDividerPainter implements DividerPainter {
157        @Override
158        public void paint(Graphics g, Divider divider) {
159            if ((divider == activeDivider()) && !isContinuousLayout()) {
160                Graphics2D g2d = (Graphics2D) g;
161                g2d.setColor(Color.black);
162                g2d.fill(divider.getBounds());
163            }
164        }
165    }
166
167    /**
168     * The DividerPainter that's used to paint Dividers on this MultiSplitPane.
169     * This property may be null.
170     *
171     * @return the value of the dividerPainter Property
172     * @see #setDividerPainter
173     */
174    public DividerPainter getDividerPainter() {
175        return dividerPainter;
176    }
177
178    /**
179     * Sets the DividerPainter that's used to paint Dividers on this
180     * MultiSplitPane.  The default DividerPainter only draws
181     * the activeDivider (if there is one) and then, only if
182     * continuousLayout is false.  The value of this property is
183     * used by the paintChildren method: Dividers are painted after
184     * the MultiSplitPane's children have been rendered so that
185     * the activeDivider can appear "on top of" the children.
186     *
187     * @param dividerPainter the value of the dividerPainter property, can be null
188     * @see #paintChildren
189     * @see #activeDivider
190     */
191    public void setDividerPainter(DividerPainter dividerPainter) {
192        this.dividerPainter = dividerPainter;
193    }
194
195    /**
196     * Uses the DividerPainter (if any) to paint each Divider that
197     * overlaps the clip Rectangle.  This is done after the call to
198     * <code>super.paintChildren()</code> so that Dividers can be
199     * rendered "on top of" the children.
200     * <p>
201     * {@inheritDoc}
202     */
203    @Override
204    protected void paintChildren(Graphics g) {
205        super.paintChildren(g);
206        DividerPainter dp = getDividerPainter();
207        Rectangle clipR = g.getClipBounds();
208        if ((dp != null) && (clipR != null)) {
209            Graphics dpg = g.create();
210            try {
211                MultiSplitLayout msl = getMultiSplitLayout();
212                for (Divider divider : msl.dividersThatOverlap(clipR)) {
213                    dp.paint(dpg, divider);
214                }
215            } finally {
216                dpg.dispose();
217            }
218        }
219    }
220
221    private boolean dragUnderway;
222    private transient MultiSplitLayout.Divider dragDivider;
223    private Rectangle initialDividerBounds;
224    private boolean oldFloatingDividers = true;
225    private int dragOffsetX;
226    private int dragOffsetY;
227    private int dragMin = -1;
228    private int dragMax = -1;
229
230    private void startDrag(int mx, int my) {
231        requestFocusInWindow();
232        MultiSplitLayout msl = getMultiSplitLayout();
233        MultiSplitLayout.Divider divider = msl.dividerAt(mx, my);
234        if (divider != null) {
235            MultiSplitLayout.Node prevNode = divider.previousSibling();
236            MultiSplitLayout.Node nextNode = divider.nextSibling();
237            if ((prevNode == null) || (nextNode == null)) {
238                dragUnderway = false;
239            } else {
240                initialDividerBounds = divider.getBounds();
241                dragOffsetX = mx - initialDividerBounds.x;
242                dragOffsetY = my - initialDividerBounds.y;
243                dragDivider  = divider;
244                Rectangle prevNodeBounds = prevNode.getBounds();
245                Rectangle nextNodeBounds = nextNode.getBounds();
246                if (dragDivider.isVertical()) {
247                    dragMin = prevNodeBounds.x;
248                    dragMax = nextNodeBounds.x + nextNodeBounds.width;
249                    dragMax -= dragDivider.getBounds().width;
250                } else {
251                    dragMin = prevNodeBounds.y;
252                    dragMax = nextNodeBounds.y + nextNodeBounds.height;
253                    dragMax -= dragDivider.getBounds().height;
254                }
255                oldFloatingDividers = getMultiSplitLayout().getFloatingDividers();
256                getMultiSplitLayout().setFloatingDividers(false);
257                dragUnderway = true;
258            }
259        } else {
260            dragUnderway = false;
261        }
262    }
263
264    private void repaintDragLimits() {
265        Rectangle damageR = dragDivider.getBounds();
266        if (dragDivider.isVertical()) {
267            damageR.x = dragMin;
268            damageR.width = dragMax - dragMin;
269        } else {
270            damageR.y = dragMin;
271            damageR.height = dragMax - dragMin;
272        }
273        repaint(damageR);
274    }
275
276    private void updateDrag(int mx, int my) {
277        if (!dragUnderway) {
278            return;
279        }
280        Rectangle oldBounds = dragDivider.getBounds();
281        Rectangle bounds = new Rectangle(oldBounds);
282        if (dragDivider.isVertical()) {
283            bounds.x = mx - dragOffsetX;
284            bounds.x = Math.max(bounds.x, dragMin);
285            bounds.x = Math.min(bounds.x, dragMax);
286        } else {
287            bounds.y = my - dragOffsetY;
288            bounds.y = Math.max(bounds.y, dragMin);
289            bounds.y = Math.min(bounds.y, dragMax);
290        }
291        dragDivider.setBounds(bounds);
292        if (isContinuousLayout()) {
293            revalidate();
294            repaintDragLimits();
295        } else {
296            repaint(oldBounds.union(bounds));
297        }
298    }
299
300    private void clearDragState() {
301        dragDivider = null;
302        initialDividerBounds = null;
303        oldFloatingDividers = true;
304        dragOffsetX = dragOffsetY = 0;
305        dragMin = dragMax = -1;
306        dragUnderway = false;
307    }
308
309    private void finishDrag() {
310        if (dragUnderway) {
311            clearDragState();
312            if (!isContinuousLayout()) {
313                revalidate();
314                repaint();
315            }
316        }
317    }
318
319    private void cancelDrag() {
320        if (dragUnderway) {
321            dragDivider.setBounds(initialDividerBounds);
322            getMultiSplitLayout().setFloatingDividers(oldFloatingDividers);
323            setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
324            repaint();
325            revalidate();
326            clearDragState();
327        }
328    }
329
330    private void updateCursor(int x, int y, boolean show) {
331        if (dragUnderway) {
332            return;
333        }
334        int cursorID = Cursor.DEFAULT_CURSOR;
335        if (show) {
336            MultiSplitLayout.Divider divider = getMultiSplitLayout().dividerAt(x, y);
337            if (divider != null) {
338                cursorID  = (divider.isVertical()) ?
339                    Cursor.E_RESIZE_CURSOR :
340                    Cursor.N_RESIZE_CURSOR;
341            }
342        }
343        setCursor(Cursor.getPredefinedCursor(cursorID));
344    }
345
346    private class InputHandler extends MouseInputAdapter implements KeyListener {
347
348        @Override
349        public void mouseEntered(MouseEvent e) {
350            updateCursor(e.getX(), e.getY(), true);
351        }
352
353        @Override
354        public void mouseMoved(MouseEvent e) {
355            updateCursor(e.getX(), e.getY(), true);
356        }
357
358        @Override
359        public void mouseExited(MouseEvent e) {
360            updateCursor(e.getX(), e.getY(), false);
361        }
362
363        @Override
364        public void mousePressed(MouseEvent e) {
365            startDrag(e.getX(), e.getY());
366        }
367
368        @Override
369        public void mouseReleased(MouseEvent e) {
370            finishDrag();
371        }
372
373        @Override
374        public void mouseDragged(MouseEvent e) {
375            updateDrag(e.getX(), e.getY());
376        }
377
378        @Override
379        public void keyPressed(KeyEvent e) {
380            if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
381                cancelDrag();
382            }
383        }
384
385        @Override
386        public void keyReleased(KeyEvent e) { }
387
388        @Override
389        public void keyTyped(KeyEvent e) { }
390    }
391
392    @Override
393    public AccessibleContext getAccessibleContext() {
394        if (accessibleContext == null) {
395            accessibleContext = new AccessibleMultiSplitPane();
396        }
397        return accessibleContext;
398    }
399
400    protected class AccessibleMultiSplitPane extends AccessibleJPanel {
401        @Override
402        public AccessibleRole getAccessibleRole() {
403            return AccessibleRole.SPLIT_PANE;
404        }
405    }
406}