/*
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.event.*;
import java.io.*;
import java.lang.ref.WeakReference;
import java.lang.reflect.*;
import java.util.*;
import java.util.List;
import java.util.regex.*;
import javax.swing.*;
import org.w3c.dom.*;
import util.*;
import util.xml.*;
import com.leafdigital.prefs.api.*;
import com.leafdigital.ui.api.*;
import com.leafdigital.ui.api.Button;
import com.leafdigital.ui.api.Dialog;
import com.leafdigital.ui.api.Label;
import com.leafdigital.ui.api.Window;
import leafchat.core.api.*;
/**
* User interface factory, creates UI objects
*/
public class UISingleton implements UI
{
/** Name of themes folder */
private static final String THEME_FOLDER = "themes";
/** Extension used for theme files */
private static final String THEME_EXTENSION = ".leafChatTheme";
/** Pattern matching RGB options */
private final static Pattern RGB = Pattern.compile("[0-9]+,[0-9]+,[0-9]+");
/** Field with a number at the end */
private static final Pattern ARRAYFIELD = Pattern.compile("(.*)([0-9]+)");
/** List of WeakReferences to ThemeListeners */
private LinkedList<WeakReference<ThemeListener>> themeListeners =
new LinkedList<WeakReference<ThemeListener>>();
/** One and only internaldesktop */
private InternalDesktop desktop = null;
/** Context */
private PluginContext context;
/** Toolbar */
private ToolBar toolbar = new ToolBar(this);
/** Main frame */
private JFrame f;
/** Switch bar (null if not used) */
private SwitchBar sb;
/** Tabs panel (null if not used) */
private JPanel tabs;
/** Layout manager for tabs */
private CardLayout tabsLayout;
private boolean skipNextActivation = true; // Skip first activation
private long skipActiveUntil = 0;
private int focusRunning = 0;
/** Currently-selected theme */
private ThemeImp currentTheme;
/** Default shared theme */
private ThemeImp sharedTheme;
/** Current UI style */
private int uiStyle = UISTYLE_SINGLEWINDOW;
/** List of open windows */
private LinkedList<WindowImp> windows = new LinkedList<WindowImp>();
/**
* @param context Plugin context for UI plugin
* @throws BugException
*/
public UISingleton(PluginContext context)
{
this.context = context;
// Init theme
Theme[] themes = getAvailableThemes();
if(themes.length == 0)
{
throw new BugException("No themes available");
}
for(int i=0; i<themes.length; i++)
{
if(((ThemeImp)themes[i]).getFile().getName().equals(
"shared" + THEME_EXTENSION))
{
sharedTheme = (ThemeImp)themes[i];
break;
}
}
if(sharedTheme==null)
{
throw new BugException("Can't find shared theme");
}
Preferences p = context.getSingle(Preferences.class);
PreferencesGroup pg = p.getGroup(context.getPlugin());
String themeID = pg.get("theme", "leaves");
for(int i=0; i<themes.length; i++)
{
if(((ThemeImp)themes[i]).getFile().getName().equals(
themeID + THEME_EXTENSION))
{
currentTheme = (ThemeImp)themes[i];
if(currentTheme != sharedTheme)
{
currentTheme.setParent(sharedTheme);
}
break;
}
}
// Init UI style
String style = pg.get(UIPrefs.PREF_UISTYLE, UIPrefs.PREFDEFAULT_UISTYLE);
uiStyle = UISTYLE_SINGLEWINDOW;
if(style.equals(UIPrefs.PREFVALUE_UISTYLE_SEPARATE))
{
uiStyle = UISTYLE_MULTIWINDOW;
}
else if(style.equals(UIPrefs.PREFVALUE_UISTYLE_TABS))
{
uiStyle = UISTYLE_TABBED;
}
}
@Override
public Window newWindow(Object oCallbacks)
{
return (new WindowImp(this, oCallbacks)).getInterface();
}
@Override
public Dialog newDialog(Object oCallbacks)
{
return (new DialogImp(this, oCallbacks)).getInterface();
}
@Override
public Page newPage(Object oCallbacks)
{
return (new PageImp(this, oCallbacks)).getInterface();
}
@Override
public Page newPage()
{
return (new PageImp(this)).getInterface();
}
@Override
public BorderPanel newBorderPanel()
{
BorderPanel l = (new BorderPanelImp()).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public SplitPanel newSplitPanel()
{
SplitPanel l = (new SplitPanelImp()).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public VerticalPanel newVerticalPanel()
{
VerticalPanel l = (new VerticalPanelImp()).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public HorizontalPanel newHorizontalPanel()
{
HorizontalPanel l = (new HorizontalPanelImp()).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public GroupPanel newGroupPanel()
{
GroupPanel l = (new GroupPanelImp()).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public TabPanel newTabPanel()
{
TabPanel l = (new TabPanelImp()).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public ChoicePanel newChoicePanel()
{
ChoicePanel l = (new ChoicePanelImp()).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public ScrollPanel newScrollPanel()
{
ScrollPanel l = (new ScrollPanelImp()).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public DecoratedPanel newDecoratedPanel()
{
DecoratedPanel l = (new DecoratedPanelImp()).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public ButtonPanel newButtonPanel()
{
ButtonPanel l = (new ButtonPanelImp()).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public Dropdown newDropdown()
{
Dropdown l = (new DropdownImp()).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public com.leafdigital.ui.api.PopupMenu newPopupMenu()
{
return (new PopupMenuImp()).getInterface();
}
@Override
public Widget newJComponentWrapper(JComponent c)
{
Widget l = new JComponentWrapper(c);
((InternalWidget)l).setUI(this);
return l;
}
@Override
public Button newButton()
{
Button l = (new ButtonImp()).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public RadioButton newRadioButton()
{
RadioButton l = (new RadioButtonImp()).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public CheckBox newCheckBox()
{
CheckBox l = (new CheckBoxImp()).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public Label newLabel()
{
Label l = (new LabelImp()).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public Pic newPic()
{
Pic p = new PicImp(this).getInterface();
((InternalWidget)p).setUI(this);
return p;
}
@Override
public TextView newTextView()
{
TextView l = (new TextViewImp(this)).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public EditBox newEditBox()
{
EditBox l = (new EditBoxImp(this)).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public EditArea newEditArea()
{
EditArea l = (new EditAreaImp(this)).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public ListBox newListBox()
{
ListBox l = (new ListBoxImp(this)).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public TreeBox newTreeBox()
{
TreeBox l = (new TreeBoxImp()).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public Table newTable()
{
Table l = (new TableImp()).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public Progress newProgress()
{
Progress l = (new ProgressImp()).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
@Override
public Spacer newSpacer()
{
Spacer l = (new SpacerImp()).getInterface();
((InternalWidget)l).setUI(this);
return l;
}
/**
* @return Single internal desktop
*/
InternalDesktop getInternalDesktop()
{
return desktop;
}
void informInactiveWindow(WindowImp wi)
{
skipActiveUntil = System.currentTimeMillis() + 100L;
}
/**
* Requests that the given component is focused. The actual focus happens
* in invokeLater. If another focus request is received first, the first
* focus will never happen - this helps prevent irritating loops.
* @param c Component to focus
*/
public void focus(final JComponent c)
{
runInSwing(new Runnable()
{
@Override
public void run()
{
focusRunning++;
SwingUtilities.invokeLater(new Runnable()
{
@Override
public void run()
{
focusRunning--;
if(focusRunning==0)
{
c.requestFocus();
}
}
});
}
});
}
/**
* Called when the main frame has been minimized.
*/
private void minimized()
{
// Do we need to minimise to tray?
Preferences p = context.getSingle(Preferences.class);
PreferencesGroup group = p.getGroup(p.getPluginOwner(context.getPlugin()));
if(group.get(UIPrefs.PREF_MINIMISE_TO_TRAY,
UIPrefs.PREFDEFAULT_MINIMISE_TO_TRAY).equals("t"))
{
// If so, hide minimised window
f.setVisible(false);
}
}
@Override
public void activate()
{
runInSwing(new Runnable()
{
@Override
public void run()
{
f.setVisible(true);
f.setExtendedState(Frame.NORMAL);
if (!f.isActive())
{
// This is necessary to bring the window to front. You can't do
// f.bringToFront; that doesn't work on any platform because the
// platforms decided to block it (which was a pretty stupid idea
// because although it's bad practice in some cases, not in response
// to a user request it isn't, and the platform can't tell that).
f.setVisible(false);
f.setVisible(true);
}
}
});
}
@Override
public void showLatest()
{
runInSwing(new Runnable()
{
@Override
public void run()
{
// Find most recently changed window
long bestTime = 0;
WindowImp bestWindow = null;
for(WindowImp window : windows)
{
long time = window.getHolder().getAttentionTime();
if(time > bestTime)
{
bestTime = time;
bestWindow = window;
}
}
// Focus it
if(bestWindow != null)
{
bestWindow.getHolder().focusFrame();
}
}
});
}
/**
* Create the application main window.
* @param sAppTitle Title for window
*/
public void init(String sAppTitle)
{
if(desktop!=null) return;
f = new JFrame(sAppTitle);
f.addWindowListener(new WindowAdapter()
{
@Override
public void windowActivated(WindowEvent e)
{
if(skipNextActivation)
{
skipNextActivation = false;
return;
}
if(System.currentTimeMillis()<skipActiveUntil)
return;
if(e.getOppositeWindow()==null && uiStyle==UISTYLE_MULTIWINDOW)
{
// Focus all the other windows too when somebody focuses this
// one.
synchronized(UISingleton.this)
{
for(WindowImp wi : windows)
{
wi.getInterface().activate();
}
}
skipNextActivation = true;
f.toFront();
}
}
@Override
public void windowClosing(WindowEvent e)
{
// Send app shutdown message
SystemStateMsg.sendShutdown();
}
@Override
public void windowIconified(WindowEvent e)
{
minimized();
}
});
sb = new SwitchBar(this);
desktop = new InternalDesktop(sb, toolbar);
tabsLayout = new CardLayout();
tabs = new JPanel(tabsLayout);
sb.setUIStyle(uiStyle);
f.getContentPane().setLayout(new BorderLayout());
Preferences p = context.getSingle(Preferences.class);
final PreferencesGroup mainWindow = p.getGroup(context.getPlugin()).getChild(UIPlugin.PREFGROUP_MAINWINDOW);
Dimension size = new Dimension(
p.toInt(mainWindow.get(UIPlugin.PREF_WIDTH, UIPlugin.PREFDEFAULT_WIDTH)),
p.toInt(mainWindow.get(UIPlugin.PREF_HEIGHT, UIPlugin.PREFDEFAULT_HEIGHT) ));
Point position = new Point(
p.toInt(mainWindow.get(UIPlugin.PREF_X, UIPlugin.PREFDEFAULT_X)),
p.toInt(mainWindow.get(UIPlugin.PREF_Y, UIPlugin.PREFDEFAULT_Y) ));
if(position.x==-1 || position.y==-1)
{
if(uiStyle==UISTYLE_MULTIWINDOW)
{
position.x = 0;
position.y = 0;
}
else
{
// Size has not been stored yet, so try to centre
Rectangle screenSize = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
size.width = Math.min(size.width, screenSize.width);
size.height = Math.min(size.height, screenSize.height);
position.x = (screenSize.width-size.width) / 2 + screenSize.x;
position.y = (screenSize.height-size.height) / 2 + screenSize.y;
}
}
switch(uiStyle)
{
case UISTYLE_SINGLEWINDOW:
f.getContentPane().add(desktop, BorderLayout.CENTER);
f.getContentPane().add(sb, BorderLayout.SOUTH);
f.getContentPane().add(toolbar, BorderLayout.NORTH);
f.setLocation(position);
f.setSize(size);
break;
case UISTYLE_MULTIWINDOW:
f.getContentPane().add(toolbar, BorderLayout.NORTH);
f.pack();
f.setLocation(position);
f.setResizable(false);
break;
case UISTYLE_TABBED:
f.getContentPane().add(toolbar, BorderLayout.NORTH);
JPanel tabsHolder = new JPanel(new BorderLayout());
tabsHolder.add(sb, BorderLayout.NORTH);
tabsHolder.add(tabs, BorderLayout.CENTER);
f.getContentPane().add(tabsHolder, BorderLayout.CENTER);
f.setLocation(position);
f.setSize(size);
break;
}
f.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
f.setVisible(true);
f.addComponentListener(new ComponentAdapter()
{
@Override
public void componentMoved(ComponentEvent e)
{
mainWindow.set(UIPlugin.PREF_X, "" + f.getLocation().x);
mainWindow.set(UIPlugin.PREF_Y, "" + f.getLocation().y);
}
@Override
public void componentResized(ComponentEvent e)
{
if(uiStyle==UISTYLE_MULTIWINDOW) return;
mainWindow.set(UIPlugin.PREF_WIDTH, "" + f.getSize().width);
mainWindow.set(UIPlugin.PREF_HEIGHT, "" + f.getSize().height);
}
});
initDefaultIcon(f);
}
void initDefaultIcon(Frame f)
{
try
{
// Don't set on Mac, it makes minimise view ugly
if(PlatformUtils.isMac()) return;
if(PlatformUtils.isJavaVersionAtLeast(1, 6))
{
LinkedList<Image> iconImages = new LinkedList<Image>();
iconImages.add(GraphicsUtils.loadImage(getClass().getResource("icon48.png")));
iconImages.add(GraphicsUtils.loadImage(getClass().getResource("icon32.png")));
iconImages.add(GraphicsUtils.loadImage(getClass().getResource("icon16.png")));
JFrame.class.getMethod("setIconImages", new Class[] { List.class }).invoke(
f, new Object[] { iconImages });
}
else
{
// Java 1.4 version
f.setIconImage(GraphicsUtils.loadImage(getClass().getResource("icon48.png")));
}
}
catch(Exception e)
{
throw new BugException(e);
}
}
/**
* Creates a frame for the contents and makes them visible.
* @param wi New contents that need a window
* @param initialScreen True if initial pos is a screen location not window relative
* @param initial Initial position (null if unknown)
* @param minimized True to start minimized
*/
protected void showFrameContents(WindowImp wi, boolean initialScreen, Point initial, boolean minimized)
{
switch(uiStyle)
{
case UISTYLE_SINGLEWINDOW:
new FrameInside(desktop, wi, initialScreen, initial, minimized);
break;
case UISTYLE_MULTIWINDOW:
if(initialScreen)
new FrameOutside(wi, initial);
else
new FrameOutside(wi);
break;
case UISTYLE_TABBED:
new FrameTab(this, wi, initialScreen ? initial : null, !minimized);
break;
}
}
/** Throws error if thread is not Swing thread */
public static void checkSwing()
{
if(!SwingUtilities.isEventDispatchThread())
throw new Error("Must be in Swing thread");
}
/**
* Run something in Swing, either right away or with invokeLater.
* @param r Runnable
*/
public static void runInSwing(Runnable r)
{
if(SwingUtilities.isEventDispatchThread())
{
r.run();
}
else
{
SwingUtilities.invokeLater(r);
}
}
@Override
public Widget createWidget(Document d, WidgetOwner wOwner)
{
return createWidget(d.getDocumentElement(), wOwner);
}
@Override
public Window createWindow(Document d, Object callbacks)
{
// Check document
Element el = d.getDocumentElement();
if(!el.getTagName().equals("Window"))
throw new BugException("Window document must have <Window> as root");
// Create window and set properties
Window w = newWindow(callbacks);
invokeSetMethods(el, w, "Window: ", callbacks);
// Create window contents
Element[] children = XML.getChildren(el);
if(children.length!=1)
{
throw new BugException("Window tag must contain single child");
}
w.setContents(children[0]);
((InternalWidgetOwner)w).markCreated();
return w;
}
@Override
public Window createWindow(String xml, Object callbacks)
{
checkUIHandler(xml, callbacks);
try
{
Document d = XML.parse(callbacks.getClass().getResourceAsStream(
xml + ".xml"));
return createWindow(d, callbacks);
}
catch(XMLException e)
{
throw new BugException(e);
}
}
@Override
public Page createPage(Document d, Object callbacks)
{
// Check document
Element el = d.getDocumentElement();
if(!el.getTagName().equals("Page"))
throw new BugException("Page document must have <Page> as root");
// Create page and set properties
Page p = newPage(callbacks);
invokeSetMethods(el, p, "Page: ", callbacks);
// Create page contents
Element[] children = XML.getChildren(el);
if(children.length!=1)
throw new BugException("Page tag must contain single child");
p.setContents(children[0]);
((InternalWidgetOwner)p).markCreated();
return p;
}
@Override
public Page createPage(String xml, Object callbacks)
{
checkUIHandler(xml, callbacks);
try
{
Document d = XML.parse(callbacks.getClass().getResourceAsStream(
xml + ".xml"));
return createPage(d, callbacks);
}
catch(XMLException e)
{
throw new BugException(e);
}
}
@Override
public Dialog createDialog(Document d, Object callbacks)
{
// Check document
Element el = d.getDocumentElement();
if(!el.getTagName().equals("Dialog"))
throw new BugException("Dialog document must have <Dialog> as root");
// Create dialog and set properties
Dialog newDialog = newDialog(callbacks);
invokeSetMethods(el, newDialog, "Dialog: ", callbacks);
// Create window contents
Element[] children = XML.getChildren(el);
if(children.length!=1)
throw new BugException("Dialog tag must contain single child");
newDialog.setContents(children[0]);
((InternalWidgetOwner)newDialog).markCreated();
return newDialog;
}
@Override
public Dialog createDialog(String xml, Object callbacks)
{
checkUIHandler(xml, callbacks);
try
{
Document d = XML.parse(callbacks.getClass().getResourceAsStream(
xml + ".xml"));
return createDialog(d, callbacks);
}
catch(XMLException e)
{
throw new BugException(e);
}
}
/**
* Checks that the given callback object includes annotation for the given
* XML name.
* <p>
* Note: This does not break backward compatibility because it is only
* called for the new versions of {@link #createWindow(String, Object)}
* etc. and not the older deprecated ones.
* @param xml Name of xml file without ".xml"
* @param callbacks Object that contains callbacks
* @throws BugException If it doesn't
*/
private void checkUIHandler(String xml, Object callbacks) throws BugException
{
// Check annotation
UIHandler annotation = callbacks.getClass().getAnnotation(UIHandler.class);
if(annotation == null)
{
throw new BugException("Callback class must be marked with @UIHandler(\""
+ xml + "\")");
}
boolean ok = false;
for(String value : annotation.value())
{
if(value.equals(xml))
{
ok = true;
break;
}
}
if(!ok)
{
throw new BugException("Callback class @UIHandler must list \""
+ xml + "\"");
}
}
/**
* @param e XML widget document element
* @param wOwner Window that maintains a list of IDs for the created widgets
* @return Top-level widget
* @throws BugException If the XML document is invalid
*/
Widget createWidget(Element e, WidgetOwner wOwner)
{
// Create tree of widgets first; then run all set methods (in case the
// set methods depend on relationships with other widgets)
List<SetMethods> sets = new LinkedList<SetMethods>();
Widget w = createWidgetInner(e, wOwner, sets);
for(SetMethods sm : sets)
{
sm.invoke();
}
return w;
}
private class SetMethods
{
private Element e;
private InternalWidget iw;
private String sErrorPrefix;
private Object callbacks;
SetMethods(Element e, InternalWidget iw, String sErrorPrefix, Object callbacks)
{
this.e = e; this.iw = iw; this.sErrorPrefix = sErrorPrefix; this.callbacks = callbacks;
}
void invoke()
{
invokeSetMethods(e, iw, sErrorPrefix, callbacks);
}
}
/**
* @param e XML widget document element
* @param owner Window that maintains a list of IDs for the created widgets
* @param setMethods List of set methods
* @return Top-level widget
* @throws BugException If the XML document is invalid
*/
private Widget createWidgetInner(Element e, WidgetOwner owner,
List<SetMethods> setMethods)
{
// Get ID if present
String sID=e.getAttribute("id");
if(sID!=null && sID.equals("")) sID=null;
// Name used in error messages
String sReportName = "<" + e.getTagName() + ">";
if(sID!=null) sReportName += " [" + sID + "]";
InternalWidget iw;
try
{
// Element name maps to a newThing() method, if it's valid
Method m = getClass().getDeclaredMethod("new" + e.getTagName(), new Class[0]);
iw = (InternalWidget)m.invoke(this, new Object[0]);
}
catch(NoSuchMethodException nsme)
{
throw new BugException(sReportName + ": Not a valid widget tag");
}
catch(InvocationTargetException ite)
{
throw new BugException(sReportName + ": Error instantiating widget", ite.getCause());
}
catch(IllegalAccessException iae)
{
throw new BugException(sReportName + ": Unexpected error instantiating widget", iae);
}
catch (IllegalArgumentException iae)
{
throw new BugException(
sReportName + ": Unexpected error instantiating widget", iae);
}
// Initialise widget
iw.setUI(this);
iw.setOwner(owner);
if(sID!=null) iw.setID(sID);
// Use attributes to invoke set methods
setMethods.add(new SetMethods(e, iw, sReportName + ": ",
((CallbackHandlerImp)owner.getCallbackHandler()).getCallbackObject()));
// Get list of reserved (non-slot) child elements
Set<String> reserved = new HashSet<String>();
String[] asReserved = iw.getReservedChildren();
for(int i=0; i<asReserved.length; i++)
{
reserved.add(asReserved[i]);
}
List<Element> reservedElements = new LinkedList<Element>();
// Child elements count as slots, within which other widgets are placed,
// unless widget has the SINGLESLOT or NAMELESSSLOTS property
int contentType = iw.getContentType();
Element[] children = XML.getChildren(e);
// Extract reserved elements
for(int childIndex=0; childIndex<children.length; childIndex++)
{
// Get slotname and widget tag
String sSlot = children[childIndex].getTagName();
if(reserved.contains(sSlot))
{
reservedElements.add(children[childIndex]);
children[childIndex]=null;
}
}
switch(contentType)
{
case InternalWidget.CONTENT_NONE:
if(children.length!=reservedElements.size())
throw new BugException(sReportName + ": may not contain other components");
break;
case InternalWidget.CONTENT_SINGLE:
if(children.length>1)
throw new BugException(sReportName + ": must contain at most one component");
// Fall through
case InternalWidget.CONTENT_UNNAMEDSLOTS:
for(int childIndex = 0; childIndex<children.length; childIndex++)
{
if(children[childIndex]==null) continue;
// Construct widget and add to this one
Widget wChild = createWidgetInner(children[childIndex], owner, setMethods);
try
{
((InternalWidget)wChild).setParent(iw);
iw.addXMLChild(null, wChild);
}
catch(BugException be)
{
// Add widget identifier to any error messages
throw new BugException(sReportName + ": " + be.getMessage());
}
}
break;
case InternalWidget.CONTENT_NAMEDSLOTS:
for(int childIndex = 0; childIndex<children.length; childIndex++)
{
if(children[childIndex]==null) continue;
// Get slotname and widget tag
String sSlot = children[childIndex].getTagName();
Element[] aeWidgets = XML.getChildren(children[childIndex]);
if(aeWidgets.length!=1)
throw new BugException(sReportName + ": " + sSlot +
" must contain precisely one widget");
// Construct widget and add to this one
Widget wChild = createWidgetInner(aeWidgets[0], owner, setMethods);
try
{
((InternalWidget)wChild).setParent(iw);
iw.addXMLChild(sSlot, wChild);
}
catch(BugException be)
{
// Add widget identifier to any error messages
throw new BugException(sReportName + ": " + be.getMessage());
}
}
break;
}
// Call with any reserved children
if(!reserved.isEmpty())
{
try
{
iw.setReservedData(
reservedElements.toArray(new Element[reservedElements.size()]));
}
catch(BugException be)
{
// Add widget identifier to any error messages
throw new BugException(sReportName + ": " + be.getMessage());
}
}
// Remember widget within owner, and return it
if(sID!=null) owner.setWidgetID(sID, iw);
return iw;
}
private static void invokeSetMethods(Element e, Object o, String errorPrefix, Object callbacks)
throws BugException
{
// Attributes apart from id map to setXXX methods
String[] attributes = XML.getAttributeNames(e);
for(int attribute = 0; attribute<attributes.length; attribute++)
{
// Get attribute name and value
String name = attributes[attribute], value = e.getAttribute(name);
// ID we use if it matches a public variable in the handler object to
// set up that variable automagically.
if(name.equals("id"))
{
if(callbacks!=null)
{
// Try from this class to superclasses
for(Class<?> tryClass = callbacks.getClass(); tryClass!=null; tryClass = tryClass.getSuperclass())
{
// See if there's a public field with that name plus 'UI' in the callback class
Field f;
try
{
f = tryClass.getField(value + "UI");
f.set(callbacks, o);
break;
}
catch(NoSuchFieldException x)
{
// Not a field, that's cool. If this ends in a number, is there an
// array?
Matcher m = ARRAYFIELD.matcher(value);
if(m.matches())
{
String arrayName = m.group(1);
int index = Integer.parseInt(m.group(2));
try
{
f = tryClass.getField(arrayName + "UI");
Object array = f.get(callbacks);
if(array==null)
{
array = Array.newInstance(f.getType().getComponentType(), index + 1);
f.set(callbacks, array);
}
if(Array.getLength(array)<=index)
{
Object newArray = Array.newInstance(f.getType().getComponentType(), index + 1);
System.arraycopy(array, 0, newArray, 0, Array.getLength(array));
array = newArray;
f.set(callbacks, array);
}
Array.set(array, index, o);
break;
}
catch(NoSuchFieldException xx)
{
// No array either, that's cool
}
catch(Exception xx)
{
throw new BugException("Error autosetting public array field " + arrayName + "UI", xx);
}
}
}
catch(Exception x)
{
throw new BugException("Error autosetting public field " + value + "UI", x);
}
}
}
// But otherwise it doesn't call any set methods or anything
continue;
}
String sPropertyErrorPrefix = errorPrefix + "Property " + name + ":";
// See if there's a set method for either string, int, boolean, or two
// ints, or Color, and that name
try
{
outer: do // This is not really a loop; I just want to be able to break out of it
{
try
{
Method mString = o.getClass().getMethod(
"set" + name, new Class[] { String.class });
mString.invoke(o, new Object[] {value});
break;
}
catch(NoSuchMethodException nsme)
{
// Not string property
}
try
{
Method mInt = o.getClass().getMethod(
"set" + name, new Class[] { int.class });
int iValue = getIntValue(o, errorPrefix, value);
mInt.invoke(o, iValue);
break;
}
catch(NoSuchMethodException nsme)
{
// Not int property
}
try
{
Method mBoolean = o.getClass().getMethod(
"set" + name, new Class[] { boolean.class });
boolean bValue;
if(value.equals("y"))
bValue = true;
else if(value.equals("n"))
bValue = false;
else
throw new BugException(sPropertyErrorPrefix +
"Value must be y or n, not: " + value);
mBoolean.invoke(o, new Object[] { new Boolean(bValue)});
break;
}
catch(NoSuchMethodException nsme)
{
// Not int property
}
for(int multi = 2; multi<=4; multi++)
{
try
{
Class<?>[] classes = new Class<?>[multi];
for(int i = 0; i < classes.length; i++)
{
classes[i]=int.class;
}
Method multiInt = o.getClass().getMethod("set" + name, classes);
String[] values = value.split(",");
if(values.length!=multi)
throw new BugException(sPropertyErrorPrefix +
"Expecting " + multi + " values separated by commas, not: " + value);
Object[] objects = new Object[multi];
for(int i = 0; i<objects.length; i++)
{
objects[i] = getIntValue(o, errorPrefix, values[i]);
}
multiInt.invoke(o, objects);
break outer;
}
catch(NoSuchMethodException nsme)
{
// Not int, int
}
}
try
{
Method mColor = o.getClass().getMethod(
"set" + name, Color.class);
Color c = getColorValue(o, errorPrefix, value);
mColor.invoke(o, c);
break;
}
catch(NoSuchMethodException nsme)
{
// Not Color
}
// We didn't find any appropriate methods
throw new BugException(sPropertyErrorPrefix + "No such property");
}
while(false);
}
catch (IllegalArgumentException iae)
{
throw new BugException(
sPropertyErrorPrefix + "Unexpected error setting widget property", iae);
}
catch (IllegalAccessException iae)
{
throw new BugException(
sPropertyErrorPrefix + "Unexpected error setting widget property", iae);
}
catch (InvocationTargetException ite)
{
throw new BugException(
sPropertyErrorPrefix + "Error setting widget property", ite.getCause());
}
}
}
private static Color getColorValue(Object o, String sPropertyErrorPrefix, String sValue)
throws BugException
{
String sV=sValue;
if(!sV.startsWith("#"))
throw new BugException(sPropertyErrorPrefix +
"Expecting color beginning with #, not: " + sValue);
sV=sV.substring(1);
if(sV.length()==3)
{
sV=sV.substring(0, 1) + sV.charAt(0) +
sV.charAt(1) + sV.charAt(1) + sV.charAt(2) + sV.charAt(2);
}
if(sV.length()!=6)
throw new BugException(sPropertyErrorPrefix +
"Expecting three or six-digit hex color, not: " + sValue);
try
{
return new Color(
Integer.parseInt(sV.substring(0, 2), 16),
Integer.parseInt(sV.substring(2, 4), 16),
Integer.parseInt(sV.substring(4, 6), 16));
}
catch(NumberFormatException nfe)
{
throw new BugException(sPropertyErrorPrefix +
"Expecting valid hex color, not: " + sValue);
}
}
/**
* Converts a string to integer, allowing both standard integers and also
* constants that are declared in any interface of the given object's class.
* @param o Target object
* @param sPropertyErrorPrefix Prefix used in error exceptions
* @param sValue Value to parse
* @return Integer value
* @throws IllegalAccessException If there is a problem while searching
* @throws BugException If the string can't be matched to an integer
*/
private static int getIntValue(
Object o,
String sPropertyErrorPrefix,
String sValue)
throws IllegalAccessException, BugException
{
int iValue = -1; // This should always be set later, but Java isn't
// clever enough to work that out
try
{
iValue = Integer.parseInt(sValue);
}
catch(NumberFormatException nfe)
{
// Look for a constant of that name in all interfaces iw
// implements
Class<?>[] ac = o.getClass().getInterfaces();
boolean bGot = false;
searchLoop: for(int iInterface = 0; iInterface<ac.length; iInterface++)
{
Class<?> cInterface = ac[iInterface];
Field[] af = cInterface.getFields();
for(int iField = 0; iField<af.length; iField++)
{
if(af[iField].getName().equals(sValue))
{
try
{
iValue = af[iField].getInt(o);
bGot = true;
break searchLoop;
}
catch(IllegalArgumentException iae)
{
// Looks like this field isn't an option, let's just
// continue with the loop so we eventually throw
// invalid value (below)
}
}
}
}
if(!bGot) throw new BugException(sPropertyErrorPrefix + "Expecting integer or constant, not: " + sValue);
}
return iValue;
}
@Override
public void registerTool(final Tool t)
{
toolbar.register(t);
}
@Override
public void unregisterTool(final Tool t)
{
toolbar.unregister(t);
}
@Override
public void resizeToolbar()
{
runInSwing(new Runnable()
{
@Override
public void run()
{
toolbar.rearrangeTools();
if(uiStyle == UISTYLE_MULTIWINDOW)
{
if(f != null)
{
f.pack();
}
}
}
});
}
/**
* @return UI singleton's plugin context (for use by other UI components)
*/
PluginContext getPluginContext()
{
return context;
}
/** Handler for the yes/no/cancel question dialog */
public static class QuestionDialogHandler
{
/**
* Constructs handler.
* @param defaultButton Default button for user
*/
public QuestionDialogHandler(int defaultButton)
{
result = defaultButton;
}
private Dialog d;
private boolean remember;
void setDialog(Dialog d)
{
this.d = d;
}
private int result;
/** UI action: User clicks Yes button. */
public void actionYes()
{
result = BUTTON_YES;
d.close();
}
/** UI action: User clicks No button. */
public void actionNo()
{
result = BUTTON_NO;
d.close();
}
/** UI action: User clicks Cancel button. */
public void actionCancel()
{
result = BUTTON_CANCEL;
d.close();
}
/** UI action: User toggles Remember checkbox. */
public void changeRemember()
{
remember = ((CheckBox)d.getWidget("remember")).isChecked();
}
int getResult()
{
return result;
}
boolean isRemember()
{
return remember;
}
}
@Override
public int showQuestion(WidgetOwner parent, String title, String message, int buttons,
String yesLabel, String noLabel, String cancelLabel, int defaultButton)
{
try
{
QuestionDialogHandler qdh = new QuestionDialogHandler(
(buttons & BUTTON_CANCEL)!=0 ? BUTTON_CANCEL : defaultButton);
Dialog d = createDialog(XML.parse(UISingleton.class, "questiondialog.xml"), qdh);
qdh.setDialog(d);
d.setTitle(title);
((Label)d.getWidget("message")).setText(message);
Button yes = (Button)d.getWidget("yes"), no = (Button)d.getWidget("no"),
cancel = (Button)d.getWidget("cancel");
if(noLabel!=null) no.setLabel(noLabel);
if(yesLabel!=null) yes.setLabel(yesLabel);
if(cancelLabel!=null) cancel.setLabel(cancelLabel);
if((buttons & BUTTON_YES)==0) yes.setVisible(false);
if((buttons & BUTTON_NO)==0) no.setVisible(false);
if((buttons & BUTTON_CANCEL)==0) cancel.setVisible(false);
switch(defaultButton)
{
case BUTTON_YES: yes.setDefault(true); break;
case BUTTON_NO: no.setDefault(true); break;
case BUTTON_CANCEL: cancel.setDefault(true); break;
default: throw new BugException("Unexpected default button " + defaultButton);
}
d.show(parent);
return qdh.getResult();
}
catch(XMLException e)
{
ErrorMsg.report("Error showing question dialog", e);
if((buttons&BUTTON_CANCEL)!=0)
return BUTTON_CANCEL;
else
return defaultButton;
}
}
@Override
public int showOptionalQuestion(String prefID, WidgetOwner parent, String title, String message, int buttons,
String yesLabel, String noLabel, String cancelLabel, int defaultButton)
{
Preferences p = context.getSingle(Preferences.class);
PreferencesGroup group = p.getGroup(context.getPlugin()).getChild("optional-questions");
String selected = group.get(prefID, null);
if(selected!=null)
{
return p.toInt(selected);
}
try
{
QuestionDialogHandler qdh = new QuestionDialogHandler(
(buttons & BUTTON_CANCEL)!=0 ? BUTTON_CANCEL : defaultButton);
Dialog d = createDialog(XML.parse(UISingleton.class, "optionalquestiondialog.xml"), qdh);
qdh.setDialog(d);
d.setTitle(title);
((Label)d.getWidget("message")).setText(message);
Button yes = (Button)d.getWidget("yes"), no = (Button)d.getWidget("no"),
cancel = (Button)d.getWidget("cancel");
if(noLabel!=null) no.setLabel(noLabel);
if(yesLabel!=null) yes.setLabel(yesLabel);
if(cancelLabel!=null) cancel.setLabel(cancelLabel);
if((buttons & BUTTON_YES)==0) yes.setVisible(false);
if((buttons & BUTTON_NO)==0) no.setVisible(false);
if((buttons & BUTTON_CANCEL)==0) cancel.setVisible(false);
switch(defaultButton)
{
case BUTTON_YES: yes.setDefault(true); yes.focus(); break;
case BUTTON_NO: no.setDefault(true); no.focus(); break;
case BUTTON_CANCEL: cancel.setDefault(true); cancel.focus(); break;
default: throw new BugException("Unexpected default button " + defaultButton);
}
d.show(parent);
int result = qdh.getResult();
if(result!=BUTTON_CANCEL && qdh.isRemember())
{
group.set(prefID, p.fromInt(result));
}
return result;
}
catch(XMLException e)
{
ErrorMsg.report("Error showing question dialog", e);
if((buttons&BUTTON_CANCEL)!=0)
return BUTTON_CANCEL;
else
return defaultButton;
}
}
/** Handler for user error dialog where there's just an OK button. */
public static class OKDialogHandler
{
private Dialog d;
void setDialog(Dialog d)
{
this.d = d;
}
/**
* User clicks OK.
*/
public void actionOK()
{
d.close();
}
}
@Override
public void showUserError(WidgetOwner parent, String title, String message)
{
try
{
Document dXML=XML.parse(UISingleton.class, "usererror.xml");
OKDialogHandler odh = new OKDialogHandler();
Dialog d = createDialog(dXML, odh);
odh.setDialog(d);
d.setTitle(title);
((Label)d.getWidget("message")).setText(message);
d.show(parent);
}
catch(XMLException e)
{
ErrorMsg.report("Error showing user error dialog", e);
}
}
// Theme methods
@Override
public File getThemeFolder(boolean system)
{
if(system)
{
return new File(THEME_FOLDER);
}
else
{
File userThemes = new File(PlatformUtils.getUserFolder(), THEME_FOLDER);
if(!userThemes.exists())
userThemes.mkdirs();
return userThemes;
}
}
@Override
public void installUserTheme(File newTheme) throws GeneralException
{
// Test theme
try
{
new ThemeImp(newTheme);
}
catch(Exception e)
{
throw new GeneralException(e);
}
try
{
// Check target for theme file
File target = new File(getThemeFolder(false), newTheme.getName());
boolean isCurrent = currentTheme.getFile().getCanonicalPath().equals(target.getCanonicalPath());
// If it's the current theme, switch to something else real quick like
if(isCurrent) currentTheme = sharedTheme;
// Copy in the new file
IOUtils.copy(newTheme, target, true);
// If it was current, switch the theme back
if(isCurrent)
{
Theme[] result = getAvailableThemes();
for(int i = 0; i<result.length; i++)
{
if(((ThemeImp)result[i]).getFile().getCanonicalPath().equals(target.getCanonicalPath()))
{
setTheme(result[i]);
}
}
}
}
catch(IOException e)
{
throw new GeneralException(e);
}
}
@Override
public Theme[] getAvailableThemes()
{
SortedSet<Theme> themes = new TreeSet<Theme>();
if(currentTheme!=null)
themes.add(currentTheme);
if(sharedTheme!=null && sharedTheme!=currentTheme)
themes.add(sharedTheme);
File[][] search = new File[2][];
File userThemes = getThemeFolder(false);
search[0]=IOUtils.listFiles(userThemes);
File programThemes = getThemeFolder(true);
if(programThemes.exists())
search[1]=IOUtils.listFiles(programThemes);
for(int i = 0; i<search.length; i++)
{
File[] files = search[i];
if(files==null) continue;
for(int j = 0; j<files.length; j++)
{
if(files[j].getName().endsWith(THEME_EXTENSION))
{
if(
(currentTheme==null || !files[j].equals(currentTheme.getFile())) &&
(sharedTheme==null || !files[j].equals(sharedTheme.getFile())))
{
try
{
themes.add(new ThemeImp(files[j]));
}
catch(IOException e)
{
ErrorMsg.report(
"An error occurred when trying to load the theme " + files[j].getName(), e);
}
}
}
}
}
return themes.toArray(new Theme[themes.size()]);
}
@Override
public Theme getTheme()
{
return currentTheme==null ? sharedTheme : currentTheme;
}
@Override
public synchronized void setTheme(Theme t)
{
currentTheme = (ThemeImp)t;
if(currentTheme!=null && currentTheme!=sharedTheme)
{
currentTheme.setParent(sharedTheme);
}
Preferences p;
try
{
p = context.getSingle(Preferences.class);
p.getGroup(context.getPlugin()).set("theme",
currentTheme==null ? "" :
currentTheme.getFile().getName().replaceAll("\\" + THEME_EXTENSION, ""));
}
catch(BugException e)
{
ErrorMsg.report("Error setting theme in preferences", e);
}
refreshTheme();
}
@Override
public synchronized void refreshTheme()
{
Theme t = getTheme();
for(Iterator<WeakReference<ThemeListener>> i = themeListeners.iterator();
i.hasNext(); )
{
WeakReference<ThemeListener> wr = i.next();
ThemeListener tl = wr.get();
if(tl==null)
{
i.remove();
}
else
{
tl.updateTheme(t);
}
}
// TODO Maybe fire an event as well
}
/**
* @param parent Parent referent or null to use default
* @return Frame to use as dialog parent. May be null if program hasn't
* started up yet.
*/
Frame getDialogFrame(WidgetOwner parent)
{
if(parent!=null)
{
JComponent c = getPositionReferent(parent);
Container topLevel = c.getTopLevelAncestor();
if(topLevel instanceof Frame)
{
return (Frame)topLevel;
}
}
return f;
}
JComponent getPositionReferent(WidgetOwner parent)
{
// Position it relative to specified or main window
while(parent!=null && (parent instanceof Page))
parent = ((Page)parent).getOwner();
if(parent==null)
return null;
else if(parent instanceof Window)
return ((WindowImp.WindowInterface)parent).getPositionReferent();
else if(parent instanceof Dialog)
return ((DialogImp.DialogInterface)parent).getPositionReferent();
else
throw new BugException("Unexpected WidgetOwner type");
}
/**
* @param parent Parent component marker (may be null)
* @return Component referring to that parent, or the main frame, or null
* if the main frame isn't up yet.
*/
Component getParentComponent(WidgetOwner parent)
{
Component c = getPositionReferent(parent);
if(c==null)
return f;
else
return c;
}
void setDialogPosition(JDialog d, WidgetOwner parent)
{
d.setLocationRelativeTo(getDialogFrame(parent));
}
@Override
public File showFileSelect(WidgetOwner parent, String title, boolean saveMode, File folder,
File file, final String[] extensions, final String filterName)
{
if(PlatformUtils.isMac())
{
FileDialog fd = new FileDialog(getDialogFrame(parent),
title, saveMode ? FileDialog.SAVE : FileDialog.LOAD);
if(folder!=null && file==null)
{
fd.setDirectory(folder.getPath());
}
if(file!=null)
{
fd.setDirectory(file.getParent());
fd.setFile(file.getName());
}
if(extensions!=null)
{
fd.setFilenameFilter(new FilenameFilter()
{
@Override
public boolean accept(File dir, String name)
{
for(int i = 0; i<extensions.length; i++)
{
if(name.toLowerCase().endsWith(extensions[i].toLowerCase()))
return true;
}
return false;
}
});
}
fd.setVisible(true);
if(fd.getFile()==null)
return null;
else
return new File(fd.getDirectory(), fd.getFile());
}
else // On non-Mac platforms the Swing dialog is better
{
JFileChooser fc;
if(folder==null)
fc = new JFileChooser();
else
fc = new JFileChooser(folder);
if(saveMode)
{
fc.setDialogType(JFileChooser.SAVE_DIALOG);
}
fc.setDialogTitle(title);
if(extensions!=null)
{
javax.swing.filechooser.FileFilter[] filters = fc.getChoosableFileFilters();
for(int i = 0; i<filters.length; i++)
fc.removeChoosableFileFilter(filters[i]);
fc.setFileFilter(new javax.swing.filechooser.FileFilter()
{
@Override
public boolean accept(File f)
{
for(int i = 0; i<extensions.length; i++)
{
if(f.getName().toLowerCase().endsWith(extensions[i].toLowerCase()))
return true;
}
return false;
}
@Override
public String getDescription()
{
return filterName==null ? "Appropriate files" : filterName;
}
});
}
fc.setFileHidingEnabled(true);
if(file!=null)
{
fc.setSelectedFile(file);
}
int result;
if(saveMode)
{
result = fc.showSaveDialog(getParentComponent(parent));
}
else
{
result = fc.showOpenDialog(getParentComponent(parent));
}
if(result != JFileChooser.APPROVE_OPTION)
{
return null;
}
else
{
if(saveMode && fc.getSelectedFile().exists())
{
int ok = showQuestion(parent, "Confirm overwrite", "The file " +
fc.getSelectedFile().getName() + " already exists. Are you sure " +
"you want to overwrite it?", UI.BUTTON_YES|UI.BUTTON_CANCEL,
"Overwrite", "", "Cancel", UI.BUTTON_CANCEL);
if(ok != UI.BUTTON_YES)
{
return null;
}
}
return fc.getSelectedFile();
}
}
}
@Override
public File showFolderSelect(WidgetOwner parent, String title, File folder)
{
if(PlatformUtils.isMac())
{
FileDialog fd = new FileDialog(getDialogFrame(parent),
title, FileDialog.LOAD);
fd.setFile(folder.getName());
if(folder!=null && folder.getParentFile()!=null)
fd.setDirectory(folder.getParentFile().getPath());
System.setProperty("apple.awt.fileDialogForDirectories", "true");
fd.setVisible(true);
System.setProperty("apple.awt.fileDialogForDirectories", "false");
if(fd.getFile()==null)
return null;
else
return new File(fd.getDirectory(), fd.getFile());
}
else // On non-Mac platforms the Swing dialog is better
{
JFileChooser fc;
if(folder==null || folder.getParentFile()==null)
fc = new JFileChooser();
else
fc = new JFileChooser(folder.getParentFile());
fc.setDialogTitle(title);
fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
javax.swing.filechooser.FileFilter[] filters = fc.getChoosableFileFilters();
for(int i = 0; i<filters.length; i++)
fc.removeChoosableFileFilter(filters[i]);
fc.setFileFilter(new javax.swing.filechooser.FileFilter()
{
@Override
public boolean accept(File f)
{
return f.isDirectory() && !f.isHidden();
}
@Override
public String getDescription()
{
return "All folders";
}
});
fc.setFileHidingEnabled(true);
fc.setSelectedFile(folder);
if(fc.showOpenDialog(getParentComponent(parent))!=JFileChooser.APPROVE_OPTION)
return null;
else
return fc.getSelectedFile();
}
}
@Override
public Color showColourSelect(WidgetOwner parent, String title, Color original)
{
ColourSelectDialog selector = new ColourSelectDialog(
getParentComponent(parent), title, original);
return selector.getChosenColour();
}
@Override
public synchronized int getUIStyle()
{
return uiStyle;
}
private void focusNowAndLater(WindowImp active)
{
if(active!=null)
{
final WindowImp activeFinal = active;
Runnable r = new Runnable()
{
@Override
public void run()
{
activeFinal.getHolder().focusFrame();
}
};
r.run();
SwingUtilities.invokeLater(r);
}
}
@Override
public synchronized void setUIStyle(int uiStyle)
{
if(this.uiStyle==uiStyle) return;
int styleBefore = this.uiStyle;
this.uiStyle = uiStyle;
sb.setUIStyle(uiStyle);
context.getSingle(Preferences.class).getGroup(context.getPlugin()).set(
UIPrefs.PREF_UISTYLE,
uiStyle==UISTYLE_TABBED ? UIPrefs.PREFVALUE_UISTYLE_TABS
: uiStyle==UISTYLE_MULTIWINDOW ? UIPrefs.PREFVALUE_UISTYLE_SEPARATE
: UIPrefs.PREFVALUE_UISTYLE_CLASSIC);
f.setResizable(uiStyle!=UISTYLE_MULTIWINDOW);
switch(uiStyle)
{
case UISTYLE_MULTIWINDOW:
{
// Move each window outside
WindowImp active = null;
WindowImp[] present = windows.toArray(new WindowImp[windows.size()]);
for(int i = 0; i<present.length; i++)
{
WindowImp wi = present[i];
FrameHolder fh = wi.getHolder();
if(fh==null) continue;
if(fh instanceof FrameInside)
{
FrameInside fi = (FrameInside)fh;
Point
local = fi.getLocation(), // This is done because it might not be visible
parent = fi.getParent().getLocationOnScreen();
Point before = new Point(local.x + parent.x, local.y + parent.y);
if(PlatformUtils.isMac())
{
before.x += MacFixInternalFrame.getStandardInsets().left;
}
new FrameOutside(wi, before);
fh.killSilently();
}
else if(fh instanceof FrameTab)
{
FrameTab ft = (FrameTab)fh;
wi.getContents().setSize(ft.getPreviousContentSize());
new FrameOutside(wi, ft.getPreviousPosition());
fh.killSilently();
}
if(active==null || wi.isActive())
active = wi;
}
if(styleBefore==UISTYLE_SINGLEWINDOW)
{
// Now kill the internal desktop
f.getContentPane().remove(desktop);
f.getContentPane().remove(sb);
}
else if(styleBefore==UISTYLE_TABBED)
{
f.getContentPane().remove(tabs.getParent());
sb.getParent().remove(sb);
}
f.pack();
focusNowAndLater(active);
}
break;
case UISTYLE_SINGLEWINDOW:
{
Dimension currentSize = f.getSize();
if(styleBefore==UISTYLE_TABBED)
{
currentSize = tabs.getSize();
// Remove tab holder
f.getContentPane().remove(tabs.getParent());
sb.getParent().remove(sb);
}
// Put the internal desktop back
f.getContentPane().add(desktop, BorderLayout.CENTER);
f.getContentPane().add(sb, BorderLayout.SOUTH);
// Make it fit the windows
Point maxTL=desktop.getLocationOnScreen();
Point maxBR=new Point(
maxTL.x + Math.max(600, currentSize.width),
maxTL.y + Math.max(450, currentSize.height));
WindowImp active = null;
for(WindowImp wi : windows)
{
FrameHolder fh = wi.getHolder();
if(fh==null) continue;
Point tl;
Dimension d;
Insets internalInsets = MacFixInternalFrame.getStandardInsets();
if(fh instanceof FrameOutside)
{
FrameOutside fo = (FrameOutside)fh;
tl = fo.getLocation();
d = fo.getSize();
}
else if(fh instanceof FrameTab)
{
FrameTab ft = (FrameTab)fh;
if(ft.getPreviousPosition()==null) continue; // Don't consider this one's size
tl = ft.getPreviousPosition();
d = ft.getPreviousContentSize();
d.height += internalInsets.top;
}
else throw new BugException("Unexpected frame type");
if(wi.isActive() || active==null)
active = wi;
d.width += internalInsets.right + internalInsets.left;
d.height += internalInsets.bottom;
Point br = new Point(tl.x + d.width, tl.y + d.height);
if(tl.x<maxTL.x) maxTL.x = tl.x;
if(tl.y<maxTL.y) maxTL.y = tl.y;
if(br.x>maxBR.x) maxBR.x = br.x;
if(br.y>maxBR.y) maxBR.y = br.y;
}
Insets offsets = new Insets(
f.getInsets().top + toolbar.getHeight(),
f.getInsets().left,
f.getInsets().bottom + sb.getPreferredSize().height,
f.getInsets().right);
maxTL.x -= offsets.left;
maxTL.y -= offsets.top;
maxBR.x += offsets.right;
maxBR.y += offsets.bottom;
Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
if(maxTL.x<0) maxTL.x = 0;
if(maxTL.y<0) maxTL.y = 0;
if(maxBR.x>screen.width) maxBR.x = screen.width;
if(maxBR.y>screen.height) maxBR.y = screen.height;
f.setLocation(maxTL);
f.setSize(new Dimension(maxBR.x-maxTL.x, maxBR.y-maxTL.y));
// The size params in the InternalDesktop haven't updated yet, we
// need them to
desktop.setOverrideSize(new Dimension(
maxBR.x-maxTL.x-offsets.left-offsets.right,
maxBR.y-maxTL.y-offsets.top-offsets.bottom));
WindowImp[] windowsArray = windows.toArray(new WindowImp[windows.size()]);
for(int i = 0; i<windowsArray.length; i++)
{
WindowImp wi = windowsArray[i];
FrameHolder fh = wi.getHolder();
if(fh==null) continue;
if(fh instanceof FrameOutside)
{
Point before = ((FrameOutside)fh).getLocationOnScreen();
if(PlatformUtils.isMac())
{
before.x -= MacFixInternalFrame.getStandardInsets().left;
}
new FrameInside(desktop, wi, true, before, fh.isMinimized());
}
else if(fh instanceof FrameTab)
{
FrameTab ft = (FrameTab)fh;
wi.getContents().setSize(ft.getPreviousContentSize());
new FrameInside(desktop, wi, true,
ft.getPreviousPosition(), fh.isMinimized());
}
fh.killSilently();
}
focusNowAndLater(active);
desktop.clearOverrideSize();
}
break;
case UISTYLE_TABBED:
{
// Get window positions (can't do this after hiding them)
Map<FrameHolder, Point> positions = new HashMap<FrameHolder, Point>();
for(WindowImp wi : windows)
{
FrameHolder fh = wi.getHolder();
if(fh==null) continue;
if(fh instanceof FrameInside)
{
FrameInside fi = (FrameInside)fh;
if(fi.getParent() == null)
{
// Dunno why this would happen, but it does (#13)
continue;
}
Point
local = fi.getLocation(), // This is done because it might not be visible
parent = fi.getParent().getLocationOnScreen();
positions.put(fh, new Point(local.x + parent.x, local.y + parent.y));
}
else if(fh instanceof FrameOutside)
{
FrameOutside fo = (FrameOutside)fh;
positions.put(fh, new Point(fo.getLocation()));
}
}
if(styleBefore==UISTYLE_SINGLEWINDOW)
{
// Remove the internal desktop and add the tabbed pane instead
f.getContentPane().remove(desktop);
f.getContentPane().remove(sb);
}
JPanel tabsHolder = new JPanel(new BorderLayout());
tabsHolder.add(sb, BorderLayout.NORTH);
tabsHolder.add(tabs, BorderLayout.CENTER);
f.getContentPane().add(tabsHolder, BorderLayout.CENTER);
WindowImp active = null;
for(WindowImp wi : windows)
{
FrameHolder fh = wi.getHolder();
if(fh==null) continue;
if(wi.isActive() || active==null) active = wi;
new FrameTab(this, wi, positions.get(fh), false);
fh.killSilently();
}
focusNowAndLater(active);
if(styleBefore==UISTYLE_MULTIWINDOW)
{
Preferences p = context.getSingle(Preferences.class);
PreferencesGroup mainWindow = p.getGroup(context.getPlugin()).getChild(UIPlugin.PREFGROUP_MAINWINDOW);
Dimension size = new Dimension(
p.toInt(mainWindow.get(UIPlugin.PREF_WIDTH, UIPlugin.PREFDEFAULT_WIDTH)),
p.toInt(mainWindow.get(UIPlugin.PREF_HEIGHT, UIPlugin.PREFDEFAULT_HEIGHT)));
f.setSize(size);
}
// On some platforms (tested on Ubuntu 12.04 with Java 7r3), a
// revalidate is needed here, or else it doesn't update the display.
if(styleBefore==UISTYLE_SINGLEWINDOW)
{
((JComponent)f.getContentPane()).revalidate();
}
}
break;
}
}
synchronized void addWindow(WindowImp wi)
{
windows.addLast(wi);
}
synchronized void removeWindow(WindowImp wi)
{
windows.remove(wi);
}
synchronized void informActiveWindow(WindowImp wi)
{
windows.remove(wi);
windows.addLast(wi);
}
/**
* Adds a tab.
* @param tab Tab
*/
public void addTab(FrameTab tab)
{
tabs.add(tab, tab.getID() + "");
sb.addFrame(tab);
}
/**
* Selects a tab.
* @param tab Tab
*/
public void selectTab(FrameTab tab)
{
tabsLayout.show(tabs, tab.getID() + "");
sb.informActiveFrame(tab);
Component[] innerTabs = tabs.getComponents();
for(int i = 0; i<innerTabs.length; i++)
{
((FrameTab)innerTabs[i]).informInactive();
}
tab.informActive();
}
/**
* @return Selected tab
*/
public FrameTab getSelectedTab()
{
return (FrameTab)sb.getActiveFrame();
}
/**
* Removes a tab.
* @param tab Tab
*/
public void removeTab(FrameTab tab)
{
tabs.remove(tab);
sb.removeFrame(tab);
Component[] check = tabs.getComponents();
for(int i = 0; i<check.length; i++)
{
if(check[i].isVisible())
sb.informActiveFrame((FrameTab)check[i]);
}
}
/**
* Adds something to be informed when theme changes. This is stored as
* a weak reference so you don't need to remove it.
* @param tl Listener
*/
synchronized void informThemeListener(ThemeListener tl)
{
themeListeners.add(new WeakReference<ThemeListener>(tl));
}
// Font settings
////////////////
/** @return Stylesheet containing user settings */
String getSettingsStylesheet()
{
StringBuffer sb = new StringBuffer();
Preferences p = context.getSingle(Preferences.class);
PreferencesGroup pg = p.getGroup(context.getPlugin());
if(!p.toBoolean(pg.get(UIPrefs.PREF_SYSTEMFONT, UIPrefs.PREFDEFAULT_SYSTEMFONT)))
{
String name = pg.get(UIPrefs.PREF_FONTNAME, UIPrefs.PREFDEFAULT_FONTNAME);
String family = "SansSerif"; // I haven't found a better way to get this
sb.append("_root { font-name: \"" + name + "\", " + family + "; " +
"font-size: " + pg.get(UIPrefs.PREF_FONTSIZE, UIPrefs.PREFDEFAULT_FONTSIZE) + "; }\n");
}
PreferencesGroup[] colours = pg.getChild(UIPrefs.PREFGROUP_COLOURS).getAnon();
for(int i = 0; i<colours.length; i++)
{
String keyword = colours[i].get(UIPrefs.PREF_KEYWORD);
String rgb = colours[i].get(UIPrefs.PREF_RGB);
Matcher m = RGB.matcher(rgb);
if(!m.matches()) continue;
sb.append("@rgb " + keyword + " \"User-set\" rgb(" + rgb + "); \n");
}
return sb.toString();
}
/** @return Font if user-set, or null if default should be used */
Font getFont()
{
Preferences p = context.getSingle(Preferences.class);
PreferencesGroup pg = p.getGroup(context.getPlugin());
if(p.toBoolean(pg.get(UIPrefs.PREF_SYSTEMFONT, UIPrefs.PREFDEFAULT_SYSTEMFONT)))
return null;
return new Font(
pg.get(UIPrefs.PREF_FONTNAME, UIPrefs.PREFDEFAULT_FONTNAME),
Font.PLAIN,
Integer.parseInt(pg.get(UIPrefs.PREF_FONTSIZE, UIPrefs.PREFDEFAULT_FONTSIZE)));
}
/**
* Called on system close.
*/
public void close()
{
// Before we quit, tell all the windows they're closed
WindowImp[] windowsArray = windows.toArray(new WindowImp[windows.size()]);
for(int i = 0; i<windowsArray.length; i++)
{
windowsArray[i].informClosed();
}
// And the toolbar
toolbar.informClosed();
}
@Override
public void runInThread(Runnable r)
{
SwingUtilities.invokeLater(r);
}
@Override
public boolean isAppActive()
{
return KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow()!=null;
}
/**
* Gets the Mac indent in pixels that corresponds to a specific string
* constant as used in the XML files.
* @param macIndent Indent constant
* @return Indent in pixels (will be 0 if not on a Mac)
*/
static int getMacIndent(String macIndent)
{
if(macIndent.equals(SupportsMacIndent.TYPE_BUTTON))
{
return PlatformUtils.getMacIndentButton();
}
else if(macIndent.equals(SupportsMacIndent.TYPE_EDIT)
|| macIndent.equals(SupportsMacIndent.TYPE_EDIT_LEGACY))
{
return PlatformUtils.getMacIndentEdit();
}
else if(macIndent.equals(SupportsMacIndent.TYPE_NONE))
{
return 0;
}
throw new IllegalArgumentException("Unknown MacIndent value: " + macIndent);
}
}