/* This file is part of leafdigital leafChat. leafChat is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. leafChat is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with leafChat. If not, see <http://www.gnu.org/licenses/>. Copyright 2012 Samuel Marshall. */ package com.leafdigital.ui; import java.awt.*; import java.awt.image.BufferedImage; import java.util.*; import javax.swing.*; import org.w3c.dom.Element; import util.*; import com.leafdigital.prefs.api.*; import com.leafdigital.ui.api.*; import com.leafdigital.ui.api.Window; import leafchat.core.api.*; /** * Represents the contents of a window, whether it's internal * (FrameInside) or external (FrameOutside) */ public class WindowImp { /** Owner */ private UISingleton owner; /** Callback method for when window is closed */ private String onClosed; /** Callback method for when window is about to be closed */ private String onClosing; /** Callback method for when window becomes active */ private String onActive; /** Handles callbacks */ private CallbackHandler ch; /** Content panel */ private BorderPanelImp contents=new BorderPanelImp(); /** True if the window is active */ private boolean active=false; /** True if the window is maximized */ private boolean maximized=false; /** True if window has been closed */ private boolean closed=false; /** Preference group for remembering position, if used */ private PreferencesGroup prefsGroup=null; /** Preference ID for remembering position, if used */ private String prefsID=null; /** Initial position for window, or null for default */ private Point initialPosition=null; /** If true, initialPosition is relative to screen */ private boolean initialScreenRelative; private String remember=null; private String extraRemember=null; UISingleton getUI() { return owner; } /** Owner window */ private FrameHolder fh; /** Absolute min size */ final static int MINWIDTH=150,MINHEIGHT=100; /** Min width and height */ private Dimension minSize=new Dimension(MINWIDTH,MINHEIGHT); /** True if attention flag can be cleared at present */ private boolean canClearAttention=true; /** @return True if attention flag can be cleared at present */ boolean canClearAttention() { return canClearAttention; } private WindowInterface externalInterface = new WindowInterface(); class WindowInterface implements Window, InternalWidgetOwner { private Dimension initialSize=null; private boolean shown, created; // Stuff that needs to be stored if fh isn't up yet private String titlePre=null; private Image iconPre=null; private boolean unResizable=false; private boolean unClosable=false; private boolean minimisePre=false; /** Map of string -> Widget for contained widgets */ private Map<String, Widget> widgetIDs = new HashMap<String, Widget>(); private Map<String, ButtonGroup> groups=new HashMap<String, ButtonGroup>(); private HashMap<String, Set<BaseGroup>> arbitraryGroups = new HashMap<String, Set<BaseGroup>>(); @Override public void setTitle(String title) { if(fh==null) titlePre=title; else fh.setTitle(title); } @Override public String getTitle() { if(fh==null) return titlePre; else return fh.getTitle(); } @Override public void setIcon(Image icon) { if(fh==null) iconPre=icon; else fh.setIcon(icon); } @Override public void setCanClearAttention(boolean canClearAttention) { if(WindowImp.this.canClearAttention!=canClearAttention) { WindowImp.this.canClearAttention=canClearAttention; } } @Override public boolean getCanClearAttention() { return canClearAttention; } @Override public boolean isActive() { return active; } @Override public void attention() { if(fh!=null) fh.attention(); } @Override public void minimize() { if(fh==null) minimisePre=true; else fh.handleMinimize(); } @Override public void setInitialSize(int width, int height) { initialSize=new Dimension(width,height); } @Override public void setResizable(boolean resizable) { if(fh==null) { if(!resizable) unResizable=true; } else fh.setResizable(resizable); } @Override public void setClosable(boolean closable) { if(fh==null) { if(!closable) unClosable=true; } else fh.setClosable(closable); } @Override public void setMinSize(int minWidth, int minHeight) { if(minWidth < MINWIDTH) minWidth=MINWIDTH; if(minHeight < MINHEIGHT) minHeight=MINHEIGHT; minSize=new Dimension(minWidth,minHeight); } @Override public void setRemember(String category, String id) { Preferences p=owner.getPluginContext().getSingle(Preferences.class); prefsGroup=p.getGroup(owner.getPluginContext().getPlugin()). getChild("window-positions").getChild(category); if(id==null) prefsID="pos"; else prefsID=p.getSafeToken(id); String value=prefsGroup.get(prefsID,null); if(value!=null && value.matches("[+@][0-9]+,[0-9]+:[0-9]+,[0-9]+(:.*)?")) { String[] coords=value.substring(1).split("[,:]",5); initialSize=new Dimension(Integer.parseInt(coords[2]),Integer.parseInt(coords[3])); initialPosition=new Point(Integer.parseInt(coords[0]),Integer.parseInt(coords[1])); if(coords.length==5) { remember=value.replaceAll(":[^:]*$",""); extraRemember=coords[4]; } else { remember=value; extraRemember=null; } initialScreenRelative=value.charAt(0)=='@'; } } @Override public void show(final boolean minimised) { if(shown) return; shown=true; UISingleton.runInSwing(new Runnable() { @Override public void run() { int startW,startH; if(initialSize!=null) { startW=initialSize.width; startH=initialSize.height; } else { startW=((InternalWidget)(contents.getInterface())).getPreferredWidth(); startW=Math.max(minSize.width,startW); startH=((InternalWidget)(contents.getInterface())).getPreferredHeight(startW); } int width=Math.max(startW,minSize.width), height=Math.max(startH,minSize.height); // Hack because Java makes its windows smaller if(PlatformUtils.isMacOSVersion(10,5,0) && owner.getUIStyle()==UISingleton.UISTYLE_MULTIWINDOW) width-=28; contents.setSize(width,height); // Delegate to UI singleton to give us a holder owner.showFrameContents(WindowImp.this,initialScreenRelative,initialPosition,minimised); // We now have a holder so do any deferred stuff if(titlePre!=null) setTitle(titlePre); if(iconPre!=null) setIcon(iconPre); if(unResizable) setResizable(false); if(unClosable) setClosable(false); if(minimisePre) minimize(); } }); } @Override public void close() { fh.handleClose(); } @Override public boolean isClosed() { return closed; } @Override public void setContents(Widget w) { // Clear IDs widgetIDs.clear(); // Just set it within the content panel contents.getInterface().set(BorderPanel.CENTRAL,w); } @Override public void setContents(Element e) { // Clear IDs widgetIDs.clear(); contents.getInterface().set(BorderPanel.CENTRAL, owner.createWidget(e,this)); } @Override public Widget getWidget(String id) { Widget w=widgetIDs.get(id); if(w==null) throw new BugException("Widget ID not present: "+id); else return w; } @Override public void setWidgetID(String id,Widget w) { if(widgetIDs.put(id,w)!=null) throw new BugException( "Widget ID not unique: "+id); } @Override public ButtonGroup getButtonGroup(String group) { ButtonGroup bg=groups.get(group); if(bg==null) { bg=new ButtonGroup(); groups.put(group,bg); } return bg; } @Override public RadioButton getGroupSelected(String group) { ButtonGroup bg=getButtonGroup(group); for(Enumeration<AbstractButton> e=bg.getElements();e.hasMoreElements();) { RadioButtonImp.MyRadioButton rb=(RadioButtonImp.MyRadioButton)e.nextElement(); if(rb.isSelected()) return rb.getInterface(); } return null; } @Override public Set<BaseGroup> getArbitraryGroup(String group) { Set<BaseGroup> s = arbitraryGroups.get(group); if(s==null) { s = new HashSet<BaseGroup>(); arbitraryGroups.put(group,s); } return s; } @Override public void activate() { fh.focusFrame(); } @Override public CallbackHandler getCallbackHandler() { return ch; } @Override public void setOnClosed(String callback) { getCallbackHandler().check(callback); onClosed=callback; } @Override public void setOnClosing(String callback) { getCallbackHandler().check(callback); onClosing=callback; } @Override public void setOnActive(String callback) { getCallbackHandler().check(callback); onActive=callback; } JComponent getPositionReferent() { return contents; } @Override public String getExtraRemember() { return extraRemember; } @Override public void setExtraRemember(String text) { if(prefsGroup==null) throw new BugException("Can't set extra remember when remember isn't enabled."); extraRemember=text; updateRemember(); } @Override public boolean isHidden() { // If it doesn't have a holder set up, it's probably not going to be hidden, // it's just about to appear return fh==null ? false : fh.isHidden(); } @Override public boolean isMinimized() { // If it doesn't have a holder set up, it's probably not going to be hidden, // it's just about to appear return fh==null ? false : fh.isMinimized(); } private void informClosed() { for(Widget w : widgetIDs.values()) { w.informClosed(); } } @Override public boolean isCreated() { return created; } @Override public void markCreated() { created = true; } } /** @return API interface for this object */ Window getInterface() { return externalInterface; } void setFrame(FrameHolder newHolder) { // First time, just set it if(fh!=null) newHolder.initialiseFrom(fh); fh=newHolder; } /** @return Holder for this window */ FrameHolder getHolder() { return fh; } /** * @param owner UI singleton * @param callbacks Object that receives callbacks */ WindowImp(UISingleton owner, Object callbacks) { this.owner=owner; ch=new CallbackHandlerImp(callbacks); // Content pane; ensure it stays the right size ((InternalWidget)contents.getInterface()).setOwner(externalInterface); contents.setBackground(Color.white); contents.setFocusable(false); owner.addWindow(this); } /** @return Actual contents */ JComponent getContents() { return contents; } /** * Rescale an image to the desired (square) size, or return the image if * it's already that size. * @param i Image * @param buttonSize Desired size * @return Scaled image */ static Image rescaleIcon(Image i,int buttonSize) { int iOrigWidth=i.getWidth(null),iOrigHeight=i.getHeight(null); if(iOrigWidth==iOrigHeight && iOrigWidth==buttonSize) return i; BufferedImage bi=new BufferedImage( buttonSize,buttonSize,BufferedImage.TYPE_INT_ARGB); Graphics2D g2=bi.createGraphics(); g2.setRenderingHint( RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); g2.drawImage(i, 0,0,buttonSize,buttonSize, 0,0,iOrigWidth,iOrigHeight,null); return bi; } /** * Sets the focus (only) to a component within this window. * <p> * This is <b>not</b> the right command to programmatically switch to a * window. For that, see {@link FrameHolder#focusFrame()}. */ void focus() { focusFirstAppropriateComponent(new java.awt.Component[] {contents}); } /** * Depth-first search for first component to focus. * @param choices Possible components * @return True if it focused a component, false if it didn't find one */ private static boolean focusFirstAppropriateComponent(java.awt.Component[] choices) { for(int i=0;i<choices.length;i++) { if(choices[i].isFocusable()) { choices[i].requestFocus(); return true; } else if(choices[i] instanceof Container) { if(focusFirstAppropriateComponent( ((Container)choices[i]).getComponents())) return true; } } return false; } // public boolean isFocusCycleRoot() // { // // Focus loops within this window. // return true; // } private long lastActiveChange=0; private boolean doingActiveTimer=false; /** * Set active state of frame. * @param active True if active */ void informActive(boolean active) { if(this.active==active) return; // Sometimes you get 'deactive, active, deactive' instead of just 'deactive' // This is a hack to prevent that. The first change is processed immediately // but after that there is 200ms delay before processing any future changes long now=System.currentTimeMillis(); if(now-lastActiveChange < 200) { if(doingActiveTimer) return; doingActiveTimer=true; TimeUtils.addTimedEvent(new Runnable() { @Override public void run() { doingActiveTimer=false; informActive(fh.isActive()); } },200,true); return; } lastActiveChange=now; this.active=active; if(active) owner.informActiveWindow(this); else owner.informInactiveWindow(this); if(active && onActive!=null) { getInterface().getCallbackHandler().callHandleErrors(onActive); } } /** @return True if frame is in active state */ boolean isActive() { return active; } /** * Set maximized state of frame. * @param maximized True if maximized */ void informMaximized(boolean maximized) { if(this.maximized==maximized) return; this.maximized=maximized; } /** * Called before window is closed. * @return True if it's ok for window to close */ boolean informClosing() { if(onClosing==null) return true; getInterface().getCallbackHandler().callHandleErrors(onClosing); return false; } /** * Called after window has been closed */ void informClosed() { // Before actually closing the window, we need to get any tables to stop // editing stuff. Otherwise it gets the 'changed' event after this 'closed' // event, which is silly. recursivelyStopTableEditing(contents.getInterface()); closed=true; owner.removeWindow(this); if(onClosed!=null) { getInterface().getCallbackHandler().callHandleErrors(onClosed); } externalInterface.informClosed(); } private void recursivelyStopTableEditing(Widget w) { if(w instanceof TableImp.TableInterface) { ((TableImp.TableInterface)w).stopEditing(); } else if(w instanceof WidgetParent) { for(Widget child : ((WidgetParent)w).getWidgets()) { recursivelyStopTableEditing(child); } } } /** * Called to inform window that it's been moved or resized. Co-ordinates * refer to the position of the window frame. * @param screen If true, the window is screen-relative. Otherwise it's * main-window-relative * @param x * @param y */ void informMoved(boolean screen,int x,int y) { if(prefsGroup!=null) { try { // TODO Not sure if I should be using the frame W/H or the inner W/H remember=(screen ? "@" : "+")+x+","+y+":"+ contents.getWidth()+","+contents.getHeight(); updateRemember(); } catch(BugException e) { ErrorMsg.report("Failed to store window position in preferences",e); } } } private void updateRemember() { if(remember==null) return; // Do it later String value=remember; if(extraRemember!=null) value+=":"+extraRemember; prefsGroup.set(prefsID,value); } }