package watch.sound.swing.ui.cdi;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDesktopPane;
import javax.swing.JFrame;
import javax.swing.JInternalFrame;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JSplitPane;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
/**
* a desktop with additional functions:
* <li>iconified frame goes to a toolbar-like place holder
* <li>iconified frame group together based on the frame icon
* <li>configurable system popupmenu registered to both main window and toolbar
* <li>internal frames can be merged to create customized subwindow
* <li>panels within merged window can register to share a common channel
* <li>internal frames can be tear off to be a seperate window, which can also be
* docked back
* @author Hanning Ni
* @reviewer
*/
public class CDIDesktop extends JPanel implements PopupMenuListener{
private static final long serialVersionUID = 1L;
private static String IMAGE_DIR = "smile/me/control/mdi/resource" + java.io.File.separator;
private static ImageIcon ICONIZE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"iconize.gif");
private static ImageIcon CLOSE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"close.gif");
private static ImageIcon MAXIMIZE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"maximize.gif");
private static ImageIcon MINIMIZE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"minimize.gif");
private Set ids = new HashSet();
private final int OVERLAP = 2;
private final float OVERLAP_DEGREE = 1.2f;
protected HashMap map = new HashMap();
protected JDesktopPane desktop;
protected CDIToolbar controlPanel;
protected Map icon2button = new HashMap();
//for merged frame only
protected Map icon2button2 = new HashMap();
protected Map btn2frames = new HashMap();
protected Map id2JFrame = new HashMap();
protected JPopupMenu systemPopup = new JPopupMenu();
protected AbstractAction closeAll, disambleAll, dockBackAll ;
protected JMenuItem iconifyAll;
public CDIDesktop(){
setLayout(new BorderLayout());
desktop = new JDesktopPane();
add(desktop, BorderLayout.CENTER);
JPanel mousetrap = new JPanel();
desktop. add(mousetrap,
new Integer(Integer.MIN_VALUE));
mousetrap.setOpaque(false);
mousetrap.setBounds(0,0,1000,1000);
controlPanel = new CDIToolbar();
add(controlPanel, BorderLayout.SOUTH);
disambleAll = new AbstractAction("Disamble All in main frame"){
public void actionPerformed(ActionEvent e) {
disambleAll();
}};
systemPopup.add(disambleAll);
dockBackAll = new AbstractAction("Dockback All"){
public void actionPerformed(ActionEvent e) {
dockBackAll();
}};
systemPopup.add(dockBackAll);
iconifyAll = new JMenuItem(new AbstractAction(){
public void actionPerformed(ActionEvent e) {
iconifyAll("Iconify All".equals(iconifyAll.getText()));
}});
systemPopup.add(iconifyAll);
closeAll = new AbstractAction("Close All"){
public void actionPerformed(ActionEvent e) {
closeAll();
}};
systemPopup.add(closeAll);
systemPopup.addPopupMenuListener(this);
MouseListener l = new MouseAdapter(){
public void mousePressed(MouseEvent e) {
show(e);
}
public void mouseClicked(MouseEvent e) {
show(e);
}
public void mouseReleased(MouseEvent e) {
show(e);
}
private void show(MouseEvent e){
if(e.isPopupTrigger()){
if(e.getComponent() == controlPanel)
iconifyAll.setText("Deiconify All");
else
iconifyAll.setText("Iconify All");
systemPopup.show(e.getComponent(), e.getPoint().x, e.getPoint().y);
}
}
};
mousetrap.addMouseListener(l);
controlPanel.addMouseListener(l);
UIManager.put("InternalFrame.maximizeIcon",MAXIMIZE_BUTTON_ICON);
UIManager.put("InternalFrame.minimizeIcon",MINIMIZE_BUTTON_ICON);
UIManager.put("InternalFrame.iconifyIcon",ICONIZE_BUTTON_ICON);
UIManager.put("InternalFrame.closeIcon",CLOSE_BUTTON_ICON);
}
/**
* close all open windows, include
* <li> shown frame inside main window
* <li> iconified window
* <li> tear off window
*
*/
protected void closeAll() {
for(Iterator it = id2JFrame.values().iterator(); it.hasNext();){
DockableJFrame f = (DockableJFrame)it.next();
f.forceClose();
}
id2JFrame.clear();
for(Iterator it = btn2frames.keySet().iterator(); it.hasNext();){
List fs = (List)btn2frames.get(it.next());
for(Iterator itt = fs.iterator(); itt.hasNext();){
CDIInternalFrame f = (CDIInternalFrame)itt.next();
f.dispose();
}
}
controlPanel.removeAllIcons();
btn2frames.clear();
icon2button .clear();
icon2button2 .clear();
closeAllFramesWithinDesktop();
}
/**
* set toolbar floatable. default is false
* @param floatable
*/
public void setToolbarFloatable(boolean floatable){
controlPanel.setFloatable(floatable);
}
/**
* iconify all shown frames within main window, or deiconfiy all iconfied
* frames back into main window
* @param iconify
*/
protected void iconifyAll(boolean iconify) {
if(iconify){
CDIInternalFrame[] fs = getAllShownFramesWithinDesktop();
for(int i = fs.length -1; i >=0;i--){
addIconifiedFrame(fs[i]);
}
}else{
for(Iterator it = btn2frames.keySet().iterator(); it.hasNext();){
List fs = (List)btn2frames.get(it.next());
for(Iterator itt = fs.iterator(); itt.hasNext();){
CDIInternalFrame f = (CDIInternalFrame)itt.next();
f.setVisible(true);
}
fs.clear();
}
controlPanel.removeAllIcons();
btn2frames.clear();
icon2button .clear();
icon2button2 .clear();
}
}
/**
* dock back all tear off window into main window
*/
protected void dockBackAll() {
List ids = new ArrayList(id2JFrame.values());
for(Iterator it = ids.iterator(); it.hasNext();){
DockableJFrame f = (DockableJFrame)it.next();
dockBack(f,f.id);
f.forceClose();
}
}
/**
* disamble all merged frames within main window
*/
protected void disambleAll() {
CDIInternalFrame[] fs = getAllMergedShownFramesWithinDesktop();
for(int i = fs.length -1; i >=0;i--){
disemble(fs[i]);
}
}
/**
* tear off given internal frame
* @param comp
*/
public void tearOff(CDIInternalFrame comp){
cacheLoc(comp.getId(), comp.getBounds());
DockableJFrame frame = new DockableJFrame(comp.getProperties(), this);
frame.setContentPane(comp.getContentPane());
frame.setBounds(comp.getBounds() );
frame.setVisible(true);
id2JFrame.put(comp.getProperties(), frame);
comp.close();
}
/**
* dock back tear-offed frame,create a new internal frame and configure it
* with given property setting
* @param frame
* @param id
*/
public void dockBack(JFrame frame, CDIFrameProperties id){
id2JFrame.remove(id);
backToFrame(frame.getContentPane(),id);
}
private void backToFrame(Container content, CDIFrameProperties id){
CDIInternalFrame iframe =
new CDIInternalFrame(id.getTitle(), this);
iframe.setProperties(id);
iframe.setContentPane(content);
iframe.setBounds( restoreLoc(id.getId()));
addInternalFrame(iframe);
iframe.show();
}
/**
* merge two internal frames, new frame will
* <li> has title from both frames
* <li> new id
* <li> frame icon from source1
* <li> location of both frames can saved
* @param source1
* @param source2
* @param orientation
*/
public void mergablePanel(CDIInternalFrame source1,
CDIInternalFrame source2,
int orientation){
cacheLoc(source1.getId(), source1.getBounds());
cacheLoc(source2.getId(), source2.getBounds());
MergablePanel mergedPanel = new MergablePanel(
(JComponent)source1.getContentPane(),
source1.getProperties(),
(JComponent)source2.getContentPane(),
source2.getProperties(),
orientation);
CDIFrameProperties p1 = source1.getProperties();
CDIFrameProperties p2 = source2.getProperties();
CDIFrameProperties p = new CDIFrameProperties();
p.setFrameIcon(p1.getFrameIcon());
p.setTitle(p1.getTitle() + ":" + p2.getTitle());
p.setMergable(true);
p.setMergeStatus(true);
CDIInternalFrame iframe =
new CDIInternalFrame("", this);
String id = iframe.getId();
p.setId(id);
iframe.setProperties(p);
iframe.setContentPane(mergedPanel);
Rectangle dest = new Rectangle();
Rectangle.union(source1.getBounds(), source2.getBounds(), dest);
iframe.setBounds(dest);
cacheLoc(p.getId(), dest);
addInternalFrame(iframe);
iframe.show();
source1.close();
source2.close();
}
/**
* dissemble merged frame comp, if comp is not merged one, do
* nothing
* @param comp
*/
public void disemble(CDIInternalFrame comp){
if(!comp.isMergedStatus())
return;
Container content = comp.getContentPane();
if(content instanceof MergablePanel)
disemble((MergablePanel)content);
comp.close();
}
private void disemble(MergablePanel panel) {
Container p = (Container)panel.getSplitPane().getLeftComponent();
if(p instanceof MergablePanel)
disemble((MergablePanel)p);
else{
CDIFrameProperties afp = panel.getProperties1();
afp.setMergable(false);
backToFrame(p, afp);
}
p = (Container)panel.getSplitPane().getRightComponent();
if(p instanceof MergablePanel)
disemble((MergablePanel)p);
else{
CDIFrameProperties afp = panel.getProperties2();
afp.setMergable(false);
backToFrame(p, afp);
}
}
/**
* add internal frame to desktop's main window
* @param comp
*/
public void addInternalFrame(CDIInternalFrame comp){
desktop.add(comp);
}
/**
* add internal frame to toolbar as iconified one
* @param comp
*/
public void addIconifiedFrame(CDIInternalFrame comp){
Icon icon = comp.getFrameIcon();
IconifiedFrameButton btn = null;
if(comp.isMergedStatus())
btn = (IconifiedFrameButton)icon2button2.get(icon);
else
btn = (IconifiedFrameButton)icon2button.get(icon);
if(btn == null){
btn = new IconifiedFrameButton(icon);
if(comp.isMergedStatus())
icon2button2.put(icon, btn);
else
icon2button.put(icon, btn);
controlPanel.add(!comp.isMergedStatus(), btn);
}
List frames = (List)btn2frames.get(btn);
if(frames == null){
frames = new ArrayList();
btn2frames.put(btn, frames);
}
if(!frames.contains(comp))
frames.add(comp);
btn.setNeedArraw(frames.size() > 1);
comp.setVisible(false);
}
/**
* cache the location of given id
* @param id is id of the frame, which will keep unchanged during tearoff, iconify and
* deiconify status
* @param frame
*/
public void cacheLoc(String id, Rectangle frame){
map.put(id, frame);
}
/**
* restore location for the frame of given id
* @param id
* @return
*/
public Rectangle restoreLoc(String id){
return (Rectangle)map.get(id);
}
/**
* create unique id
* @return
*/
public synchronized String getNextAvailableId() {
String id = "" + System.currentTimeMillis();
Random r = new Random();
while(ids.contains(id)){
id = r.nextInt(1000000) + "";
}
ids.add(id);
return id;
}
private class IconifiedFrameButton extends JButton implements ActionListener {
private static final long serialVersionUID = 1L;
Dimension mdim;
boolean needArraw;
Icon originalIcon;
Icon arrawIcon;
public IconifiedFrameButton( Icon ii) {
super(ii);
originalIcon = ii;
mdim = new Dimension(ii.getIconWidth(), ii.getIconHeight());
setContentAreaFilled(false);
setBorder(null);
addActionListener(this);
}
public boolean isFocusTraversable() { return false; }
public void requestFocus() {};
public Dimension getPreferredSize() {
return mdim;
}
public Dimension getMinimumSize() {
return mdim;
}
public Dimension getMaximumSize() {
return mdim;
}
public boolean isNeedArraw() {
return needArraw;
}
public void setNeedArraw(boolean needArraw) {
this.needArraw = needArraw;
if(needArraw){
if(arrawIcon == null)
arrawIcon = new ImageIconWithArraw(originalIcon);
setIcon(arrawIcon);
}else{
setIcon(originalIcon);
}
mdim = new Dimension(getIcon().getIconWidth(), getIcon().getIconHeight());
controlPanel.repaint();
}
public void actionPerformed(ActionEvent e) {
final List frames = (List)btn2frames.get(this);
if(frames == null){
return ;//error??
}else if(frames.size() == 1 ){
CDIInternalFrame f = (CDIInternalFrame)
frames.get(0);
frames.remove(f);
if(f.isMergedStatus())
icon2button2.remove(f.getFrameIcon());
else
icon2button.remove(f.getFrameIcon());
controlPanel.remove(this);
controlPanel.repaint();
btn2frames.remove(this);
f.setVisible(true);
return;
}else{
final JPopupMenu menu = new JPopupMenu();
for(int i =0;i < frames.size();i++){
final CDIInternalFrame f = (CDIInternalFrame)
frames.get(i);
JMenuItem item = new JMenuItem(f.getTitle().trim().length() ==0 ?
"frame " + i : f.getTitle());
item.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e) {
frames.remove(f);
setNeedArraw(frames.size()>1);
f.setVisible(true);
}} );
menu.add(item);
}
SwingUtilities.invokeLater(new Runnable(){
public void run() {
menu.show( IconifiedFrameButton.this,
0,
IconifiedFrameButton.this.getHeight());
}});
}
//
}
}
private class ImageIconWithArraw implements Icon{
Icon icon;
public ImageIconWithArraw( Icon icon){
this.icon = icon;
}
public int getIconHeight() {
return icon.getIconHeight();
}
public int getIconWidth() {
return icon.getIconWidth() + 10;
}
public synchronized void paintIcon(Component c, Graphics g, int x, int y) {
icon.paintIcon(c, g,x, y);
g.translate(icon.getIconWidth() + 4 , icon.getIconHeight() /2 -3);
g.fillPolygon(new int[]{0,4,2}, new int[]{0,0,3}, 3);
g.translate(-icon.getIconWidth() - 4 , -icon.getIconHeight() /2 +3);
}
}
/**
* check if there are internalframe is mergable and
* is at the merge location
* @param frame
*/
public boolean tryMerge(CDIInternalFrame frame) {
Rectangle loc = frame.getBounds();
JInternalFrame[] frames = desktop.getAllFrames();
for(int i = frames.length -1; i >=0;i--){
CDIInternalFrame f = (CDIInternalFrame)frames[i];
if(f == frame || !f.isVisible() || !f.isMergable())
continue;
Rectangle loc2 = f.getBounds();
if(Math.abs(loc.x+ loc.width - loc2.x ) < OVERLAP){
if( overlap(loc.y, loc.y + loc.height,
loc2.y, loc2.y + loc2.height)){
mergablePanel(frame, f, JSplitPane.HORIZONTAL_SPLIT);
return true;
}
}else if(Math.abs(loc.y+ loc.height - loc2.y ) < OVERLAP){
if( overlap(loc.x, loc.x + loc.width,
loc2.x, loc2.x + loc2.width)){
mergablePanel(frame, f, JSplitPane.VERTICAL_SPLIT);
return true;
}
}else if(Math.abs(loc2.x+ loc2.width - loc.x ) < OVERLAP){
if( overlap(loc2.y, loc2.y + loc2.height,
loc .y, loc .y + loc .height)){
mergablePanel(f, frame, JSplitPane.HORIZONTAL_SPLIT);
return true;
}
}else if(Math.abs(loc2.y+ loc2.height - loc .y ) < OVERLAP){
if( overlap(loc2.x, loc2.x + loc2.width,
loc .x, loc .x + loc .width)){
mergablePanel(f,frame, JSplitPane.VERTICAL_SPLIT);
return true;
}
}
}
return false;
}
/**
* check overlap between [x1,y1],[x2,y2]
* and overlap is great than 90% of smaller one
* @param x1
* @param y1
* @param x2
* @param y2
* @return
*/
private boolean overlap(int x1, int y1, int x2, int y2){
float overlapLen = (Math.min(y1,y2) -Math.max(x1, x2))*
OVERLAP_DEGREE;
return overlapLen > y1-x1 || overlapLen > y2-x2;
}
/**
* dispath event to all SmileGroupChangableI components within
* the toplevel container which contains eventSource
* <p>event will not get dispatched if
* <li> oldValue and newValue are the same
* @param groupInterface the panel the event original comes from
* @param eventSource control which fire event
* @param source object whose property changed
* @param property
* @param oldValue
* @param newValue
*/
public static void dispatchGroupEvent(
CDIGroupChangableI groupInterface,
Container eventSource,
Object source,
String property,
Object oldValue,
Object newValue){
if(oldValue == null && newValue == null)
return;
if(newValue != null && newValue.equals(oldValue))
return;
Container parent = null;
Container kid = eventSource;
while( (parent = SwingUtilities.getAncestorOfClass(MergablePanel.class, kid))
!= null){
kid = parent;
}
parent = kid;
dispatchGroupEvent2( parent,
groupInterface, eventSource, source,property,oldValue,newValue);
}
/**
* dispath event to all SmileGroupChangableI components within
* the toplevel container comp
* <p>event will not get dispatched if
* <li> oldValue and newValue are the same
* <li> comp is null
* <li> current component is groupInterface itself
* @param comp top level component
* @param groupInterface the panel the event original comes from
* @param eventSource control which fire event
* @param source object whose property changed
* @param property
* @param oldValue
* @param newValue
*/
public static void dispatchGroupEvent2(Container comp,
CDIGroupChangableI groupInterface,
Container eventSource,
Object source,
String property,
Object oldValue,
Object newValue){
if(comp == null)
return;
if(oldValue == null && newValue == null)
return;
if(newValue != null && newValue.equals(oldValue))
return;
if(comp instanceof CDIGroupChangableI &&
comp != groupInterface){
((CDIGroupChangableI)comp).groupChangeEventReceived(
eventSource, source, property, oldValue, newValue);
}
Component[] childs = comp.getComponents();
for(int i = childs.length -1; i >=0;i--){
if(childs[i] instanceof Container){
dispatchGroupEvent2( (Container)childs[i],
groupInterface, eventSource, source,property,oldValue,newValue);
}
}
}
/**
* get all shown internal frames winthin the main window
* @return
*/
public CDIInternalFrame[] getAllShownFramesWithinDesktop(){
JInternalFrame[] fs = desktop.getAllFrames();
List sfs = new ArrayList();
for(int i = 0 ; i < fs.length;i++){
if(fs[i].isValid() && fs[i].isVisible() && (fs[i] instanceof CDIInternalFrame))
sfs.add(fs[i]);
}
return (CDIInternalFrame[]) sfs.toArray(new CDIInternalFrame[0]);
}
/**
* get all merged internal frames which is shown in the main window
* @return
*/
public CDIInternalFrame[] getAllMergedShownFramesWithinDesktop(){
JInternalFrame[] fs = desktop.getAllFrames();
List sfs = new ArrayList();
for(int i = 0 ; i < fs.length;i++){
if(fs[i] instanceof CDIInternalFrame &&
((CDIInternalFrame)fs[i]).isMergedStatus())
sfs.add(fs[i]);
}
return (CDIInternalFrame[]) sfs.toArray(new CDIInternalFrame[0]);
}
/**
* close all frames within main window
*/
public void closeAllFramesWithinDesktop(){
JInternalFrame[] fs = desktop.getAllFrames();
for(int i = 0 ; i < fs.length;i++){
fs[i].dispose();
}
}
/**
* get a system popup menu
* @return
*/
public JPopupMenu getSystemPopup() {
return systemPopup;
}
public void popupMenuCanceled(PopupMenuEvent e) { }
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { }
/**
* the place to configure system popup menu
*/
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
closeAll.setEnabled( getAllShownFramesWithinDesktop().length +
id2JFrame.size() + icon2button.size() +
icon2button2.size() > 0) ;
dockBackAll.setEnabled(id2JFrame.size() >0);
disambleAll.setEnabled(getAllMergedShownFramesWithinDesktop().length +
icon2button2.size() >0);
if(closeAll.isEnabled() ){
if("Deiconify All".equals(iconifyAll.getText())){
iconifyAll.setEnabled(icon2button.size() + icon2button2.size() > 0);
}else{
iconifyAll.setEnabled(getAllShownFramesWithinDesktop().length >0);
}
}else{
iconifyAll.setEnabled(false);
}
}
}