package org.basex.gui;
import org.basex.core.*;
import static org.basex.core.Text.*;
import org.basex.core.cmd.Find;
import org.basex.core.cmd.Set;
import org.basex.core.cmd.XQuery;
import org.basex.data.Data;
import org.basex.data.Namespaces;
import org.basex.data.Nodes;
import org.basex.data.Result;
import static org.basex.gui.GUIConstants.*;
import org.basex.gui.dialog.Dialog;
import org.basex.gui.dialog.DialogPass;
import org.basex.gui.layout.*;
import org.basex.gui.view.ViewContainer;
import org.basex.gui.view.ViewNotifier;
import org.basex.gui.view.editor.EditorView;
import org.basex.gui.view.explore.ExploreView;
import org.basex.gui.view.folder.FolderView;
import org.basex.gui.view.info.InfoView;
import org.basex.gui.view.map.MapView;
import org.basex.gui.view.plot.PlotView;
import org.basex.gui.view.table.TableView;
import org.basex.gui.view.text.TextView;
import org.basex.gui.view.tree.TreeView;
import org.basex.io.IOUrl;
import org.basex.io.out.ArrayOutput;
import org.basex.query.QueryException;
import org.basex.util.Performance;
import org.basex.util.Token;
import static org.basex.util.Token.token;
import org.basex.util.Util;
import org.basex.util.Version;
import javax.swing.*;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.EtchedBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This class is the main window of the GUI. It is the central instance
* for user interactions.
*
* @author BaseX Team 2005-12, BSD License
* @author Christian Gruen
*/
public final class GUI extends AGUI {
/** View Manager. */
public final ViewNotifier notify;
/** Status line. */
public final GUIStatus status;
/** Input field. */
public final GUIInput input;
/** Filter button. */
public final BaseXButton filter;
/** Search view. */
public final EditorView editor;
/** Info view. */
public final InfoView info;
/** Painting flag; if activated, interactive operations are skipped. */
public boolean painting;
/** Updating flag; if activated, operations accessing the data are skipped. */
public boolean updating;
/** Fullscreen flag. */
public boolean fullscreen;
/** Result panel. */
private final GUIMenu menu;
/** Button panel. */
public final BaseXBack buttons;
/** Navigation/input panel. */
public final BaseXBack nav;
/** Content panel, containing all views. */
final ViewContainer views;
/** History button. */
final BaseXButton hist;
/** Current input Mode. */
final BaseXCombo mode;
/** Text view. */
private final TextView text;
/** Top panel. */
private final BaseXBack top;
/** Execution Button. */
private final BaseXButton go;
/** Control panel. */
private final BaseXBack control;
/** Results label. */
private final BaseXLabel hits;
/** Buttons. */
private final GUIToolBar toolbar;
/** Current command. */
private Command command;
/** Menu panel height. */
private int menuHeight;
/** Fullscreen Window. */
private JFrame fullscr;
/** Thread counter. */
private int threadID;
/**
* Default constructor.
* @param ctx database context
* @param gprops gui properties
*/
public GUI(final Context ctx, final GUIProp gprops) {
super(ctx, gprops);
// set window size
final Dimension scr = Toolkit.getDefaultToolkit().getScreenSize();
final int[] ps = gprop.nums(GUIProp.GUILOC);
final int[] sz = gprop.nums(GUIProp.GUISIZE);
final int x = Math.max(0, Math.min(scr.width - sz[0], ps[0]));
final int y = Math.max(0, Math.min(scr.height - sz[1], ps[1]));
setBounds(x, y, sz[0], sz[1]);
if(gprop.is(GUIProp.MAXSTATE)) {
setExtendedState(MAXIMIZED_HORIZ);
setExtendedState(MAXIMIZED_VERT);
setExtendedState(MAXIMIZED_BOTH);
}
top = new BaseXBack(new BorderLayout());
// add header
control = new BaseXBack(new BorderLayout());
// add menu bar
menu = new GUIMenu(this);
setJMenuBar(menu);
buttons = new BaseXBack(new BorderLayout());
toolbar = new GUIToolBar(TOOLBAR, this);
buttons.add(toolbar, BorderLayout.WEST);
hits = new BaseXLabel(" ");
hits.setFont(hits.getFont().deriveFont(18f));
BaseXLayout.setWidth(hits, 150);
hits.setHorizontalAlignment(SwingConstants.RIGHT);
BaseXBack b = new BaseXBack();
b.add(hits);
buttons.add(b, BorderLayout.EAST);
if(gprop.is(GUIProp.SHOWBUTTONS)) control.add(buttons, BorderLayout.CENTER);
nav = new BaseXBack(new BorderLayout(5, 0)).border(2, 2, 0, 2);
mode = new BaseXCombo(this, SEARCH, XQUERY, COMMAND);
mode.setSelectedIndex(2);
mode.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
final int s = mode.getSelectedIndex();
if(s == gprop.num(GUIProp.SEARCHMODE) || !mode.isEnabled()) return;
gprop.set(GUIProp.SEARCHMODE, s);
input.setText("");
refreshControls();
}
});
nav.add(mode, BorderLayout.WEST);
input = new GUIInput(this);
hist = new BaseXButton(this, "hist", token(H_SHOW_HISTORY));
hist.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
final JPopupMenu pop = new JPopupMenu();
final ActionListener al = new ActionListener() {
@Override
public void actionPerformed(final ActionEvent ac) {
input.setText(ac.getActionCommand());
input.requestFocusInWindow();
pop.setVisible(false);
}
};
final int i = context.data() == null ? 2 :
gprop.num(GUIProp.SEARCHMODE);
final String[] hs = gprop.strings(i == 0 ? GUIProp.SEARCH : i == 1 ?
GUIProp.XQUERY : GUIProp.COMMANDS);
for(final String en : hs) {
final JMenuItem jmi = new JMenuItem(en);
jmi.addActionListener(al);
pop.add(jmi);
}
pop.show(hist, 0, hist.getHeight());
}
});
b = new BaseXBack(new BorderLayout(5, 0));
b.add(hist, BorderLayout.WEST);
b.add(input, BorderLayout.CENTER);
nav.add(b, BorderLayout.CENTER);
go = new BaseXButton(this, "go", token(H_EXECUTE_QUERY));
go.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
execute();
}
});
filter = BaseXButton.command(GUICommands.C_FILTER, this);
b = new BaseXBack(new TableLayout(1, 3));
b.add(go);
b.add(Box.createHorizontalStrut(1));
b.add(filter);
nav.add(b, BorderLayout.EAST);
if(gprop.is(GUIProp.SHOWINPUT)) control.add(nav, BorderLayout.SOUTH);
top.add(control, BorderLayout.NORTH);
// create views
notify = new ViewNotifier(this);
text = new TextView(notify);
editor = new EditorView(notify);
info = new InfoView(notify);
// create panels for closed and opened database mode
views = new ViewContainer(this, text, editor, info,
new FolderView(notify), new PlotView(notify), new TableView(notify),
new MapView(notify), new TreeView(notify), new ExploreView(notify)
);
top.add(views, BorderLayout.CENTER);
setContentBorder();
// add status bar
status = new GUIStatus(this);
if(gprop.is(GUIProp.SHOWSTATUS)) top.add(status, BorderLayout.SOUTH);
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
add(top);
setVisible(true);
views.updateViews();
refreshControls();
// start logo animation as thread
new Thread() {
@Override
public void run() {
views.run();
checkVersion();
}
}.start();
input.requestFocusInWindow();
}
@Override
public void dispose() {
// close opened queries
if(!editor.confirm()) return;
final boolean max = getExtendedState() == MAXIMIZED_BOTH;
gprop.set(GUIProp.MAXSTATE, max);
if(!max) {
gprop.set(GUIProp.GUILOC, new int[] { getX(), getY() });
gprop.set(GUIProp.GUISIZE, new int[] { getWidth(), getHeight() });
}
super.dispose();
gprop.write();
context.close();
}
/**
* Executes the input of the {@link GUIInput} bar.
*/
void execute() {
final String in = input.getText().trim();
final boolean cmd = mode.getSelectedIndex() == 2;
if(cmd || in.startsWith("!")) {
// run as command: command mode or exclamation mark as first character
final int i = cmd ? 0 : 1;
if(i == in.length()) return;
try {
final PasswordReader pr = new PasswordReader() {
@Override
public String password() {
final DialogPass dp = new DialogPass();
return dp.ok() ? Token.md5(dp.pass()) : "";
}
};
final CommandParser cp = new CommandParser(in.substring(i), context);
cp.password(pr);
// parse and execute all commands
execute(false, cp.parse());
} catch(final QueryException ex) {
if(!info.visible()) GUICommands.C_SHOWINFO.execute(this);
info.setInfo(ex.getMessage(), null, null, false);
info.reset();
}
} else if(gprop.num(GUIProp.SEARCHMODE) == 1 || in.startsWith("/")) {
xquery(in, false);
} else {
final String qu = Find.find(in, context, gprop.is(GUIProp.FILTERRT));
execute(false, new XQuery(qu));
}
}
/**
* Launches a query. Adds the default namespace, if available.
* The command is ignored if an update operation takes place.
* @param qu query to be run
* @param edit editor panel
*/
public void xquery(final String qu, final boolean edit) {
// check and add default namespace
final Namespaces ns = context.data().nspaces;
String in = qu.trim().isEmpty() ? "()" : qu;
final int u = ns.uri(Token.EMPTY, 0);
if(u != 0) in = Util.info("declare default element namespace \"%\"; %",
ns.uri(u), in);
execute(edit, new XQuery(in));
}
/**
* Launches the specified command in a separate thread.
* The command is ignored if an update operation takes place.
* @param cmd command to be launched
*/
public void execute(final Command cmd) {
execute(false, cmd);
}
/**
* Launches the specified commands in a separate thread.
* The command is ignored if an update operation takes place.
* @param edit call from editor view
* @param cmd command to be launched
*/
public void execute(final boolean edit, final Command... cmd) {
// ignore command if updates take place
if(updating) return;
new Thread() {
@Override
public void run() {
for(final Command c : cmd) if(!exec(c, edit)) break;
}
}.start();
}
/**
* Executes the specified command.
* @param c command to be executed
* @param edit call from editor panel
* @return success flag
*/
boolean exec(final Command c, final boolean edit) {
// wait when command is still running
final int thread = ++threadID;
while(command != null) {
command.stop();
Performance.sleep(50);
if(threadID != thread) return true;
}
cursor(CURSORWAIT);
boolean ok = true;
try {
final Performance perf = new Performance();
// reset current context if realtime filter is activated
final Data data = context.data();
if(gprop.is(GUIProp.FILTERRT) && context.current() != null &&
!context.root()) context.update();
// cache some variables before executing the command
final Nodes current = context.current();
command = c;
// execute command and cache result
final ArrayOutput ao = new ArrayOutput().max(gprop.num(GUIProp.MAXTEXT));
final boolean up = c.updating(context);
updating = up;
// updates the query editor
if(edit) {
editor.start();
} else if(editor.visible()) {
editor.reset();
}
// evaluate command
String inf = null;
try {
c.execute(context, ao);
inf = c.info();
} catch(final BaseXException ex) {
ok = false;
inf = ex.getMessage();
} finally {
updating = false;
}
final String time = perf.getTimer();
// show query info
info.setInfo(inf, c, time, ok);
info.reset();
// sends feedback to the query editor
final boolean interrupted = inf.startsWith(INTERRUPTED);
if(edit) {
editor.info(interrupted ? INTERRUPTED : ok ? OK : inf,
ok || interrupted);
}
// check if query feedback was evaluated in the query view
if(!ok && !interrupted) {
// display error in info view
if((!edit || inf.startsWith(BUGINFO)) && !info.visible()) {
GUICommands.C_SHOWINFO.execute(this);
}
} else {
// get query result
final Result result = c.result();
final Nodes nodes = result instanceof Nodes &&
result.size() != 0 ? (Nodes) result : null;
// treat text view different to other views
if(nodes == null) {
// display text view
if(!text.visible()) GUICommands.C_SHOWTEXT.execute(this);
text.setText(ao, c);
}
final Data ndata = context.data();
Nodes marked = context.marked;
if(ndata != data) {
// database reference has changed - notify views
notify.init();
} else if(up) {
// update command
notify.update();
} else if(result != null) {
final Nodes nd = context.current();
final boolean flt = gprop.is(GUIProp.FILTERRT);
if(flt || nd != null && !nd.sameAs(current)) {
// refresh context if at least one node was found
if(nodes != null) notify.context((Nodes) result, flt, null);
} else if(marked != null) {
// refresh highlight
if(nodes != null) {
// use query result
marked = nodes;
} else if(marked.size() != 0) {
// remove old highlight
marked = new Nodes(data);
}
// refresh views
notify.mark(marked, null);
if(thread != threadID) {
command = null;
return true;
}
}
}
// show number of hits
setResults(result == null ? 0 : result.size());
// show status info
status.setText(Util.info(TIME_NEEDED_X, time));
}
} catch(final Exception ex) {
// unexpected error
Util.stack(ex);
Dialog.error(this, Util.info(EXEC_ERROR, c,
!ex.toString().isEmpty() ? ex.toString() : ex.getMessage()));
updating = false;
}
cursor(CURSORARROW, true);
command = null;
return ok;
}
/**
* Stops the current process.
*/
public void stop() {
if(command != null) command.stop();
cursor(CURSORARROW, true);
command = null;
}
/**
* Sets a property and displays the command in the info view.
* @param pr property to be set
* @param val value
*/
public void set(final Object[] pr, final Object val) {
set(context.prop, pr, val);
}
/**
* Sets a main property and displays the command in the info view.
* @param pr property to be set
* @param val value
*/
public void setMain(final Object[] pr, final Object val) {
set(context.mprop, pr, val);
}
/**
* Sets a property and displays the command in the info view.
* @param prop property instance
* @param pr property to be set
* @param val value
*/
private void set(final AProp prop, final Object[] pr, final Object val) {
if(!prop.sameAs(pr, val)) {
final Set cmd = new Set(pr, val);
cmd.run(context);
info.setInfo(cmd.info(), cmd, null, true);
}
}
/**
* Sets the border of the content area.
*/
private void setContentBorder() {
final int n = control.getComponentCount();
final int n2 = top.getComponentCount();
if(n == 0 && n2 == 2) {
views.border(0, 0, 0, 0);
} else {
views.setBorder(new CompoundBorder(new EmptyBorder(3, 1, 3, 1),
new EtchedBorder()));
}
}
/**
* Refreshes the layout.
*/
public void updateLayout() {
init(gprop);
notify.layout();
views.repaint();
}
/**
* Updates the control panel.
* @param comp component to be updated
* @param show true if component is visible
* @param layout component layout
*/
public void updateControl(final JComponent comp, final boolean show,
final String layout) {
if(comp == status) {
if(!show) top.remove(comp);
else top.add(comp, layout);
} else if(comp == menu) {
if(!show) menuHeight = menu.getHeight();
final int s = show ? menuHeight : 0;
BaseXLayout.setHeight(menu, s);
menu.setSize(menu.getWidth(), s);
} else { // buttons, input
if(!show) control.remove(comp);
else control.add(comp, layout);
}
setContentBorder();
(fullscr == null ? getRootPane() : fullscr).validate();
refreshControls();
}
/**
* Updates the view layout.
*/
public void layoutViews() {
views.updateViews();
refreshControls();
repaint();
}
/**
* Refreshes the menu and the buttons.
*/
public void refreshControls() {
final Nodes marked = context.marked;
if(marked != null) setResults(marked.size());
filter.setEnabled(marked != null && marked.size() != 0);
final boolean inf = gprop.is(GUIProp.SHOWINFO);
context.prop.set(Prop.QUERYINFO, inf);
context.prop.set(Prop.XMLPLAN, inf);
final Data data = context.data();
final int t = mode.getSelectedIndex();
final int s = data == null ? 2 : gprop.num(GUIProp.SEARCHMODE);
mode.setEnabled(data != null);
go.setEnabled(s == 2 || !gprop.is(GUIProp.EXECRT));
if(s != t) {
mode.setSelectedIndex(s);
input.setText("");
input.requestFocusInWindow();
}
toolbar.refresh();
menu.refresh();
final int i = context.data() == null ? 2 : gprop.num(GUIProp.SEARCHMODE);
final String[] hs = i == 0 ? gprop.strings(GUIProp.SEARCH) : i == 1 ?
gprop.strings(GUIProp.XQUERY) : gprop.strings(GUIProp.COMMANDS);
hist.setEnabled(hs.length != 0);
}
/**
* Sets results information.
* @param n number of results
*/
private void setResults(final long n) {
hits.setText(Util.info(RESULTS_X, n));
}
/**
* Turns fullscreen mode on/off.
*/
public void fullscreen() {
fullscreen ^= true;
fullscreen(fullscreen);
}
/**
* Turns fullscreen mode on/off.
* @param full fullscreen mode
*/
public void fullscreen(final boolean full) {
if(full ^ fullscr == null) return;
if(full) {
control.remove(buttons);
control.remove(nav);
getRootPane().remove(menu);
top.remove(status);
remove(top);
fullscr = new JFrame();
fullscr.setIconImage(getIconImage());
fullscr.setTitle(getTitle());
fullscr.setUndecorated(true);
fullscr.setJMenuBar(menu);
fullscr.add(top);
fullscr.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
} else {
fullscr.removeAll();
fullscr.dispose();
fullscr = null;
if(!gprop.is(GUIProp.SHOWBUTTONS))
control.add(buttons, BorderLayout.CENTER);
if(!gprop.is(GUIProp.SHOWINPUT)) control.add(nav, BorderLayout.SOUTH);
if(!gprop.is(GUIProp.SHOWSTATUS)) top.add(status, BorderLayout.SOUTH);
setJMenuBar(menu);
add(top);
}
gprop.set(GUIProp.SHOWBUTTONS, !full);
gprop.set(GUIProp.SHOWINPUT, !full);
gprop.set(GUIProp.SHOWSTATUS, !full);
fullscreen = full;
GraphicsEnvironment.getLocalGraphicsEnvironment().
getDefaultScreenDevice().setFullScreenWindow(fullscr);
setContentBorder();
refreshControls();
updateControl(menu, !full, BorderLayout.NORTH);
setVisible(!full);
}
/**
* Checks for a new version and shows a confirmation dialog.
*/
void checkVersion() {
final Version disk = new Version(gprop.get(GUIProp.UPDATEVERSION));
final Version used = new Version(Prop.VERSION.replaceAll(" .*", ""));
if(disk.compareTo(used) < 0) {
// update version property to latest used version
gprop.set(GUIProp.UPDATEVERSION, used.toString());
} else {
try {
final String page = Token.string(new IOUrl(VERSION_URL).read());
final Matcher m = Pattern.compile("^(Version )?([\\w\\d.]*?)( .*|$)",
Pattern.DOTALL).matcher(page);
if(m.matches()) {
final Version latest = new Version(m.group(2));
if(disk.compareTo(latest) < 0 && Dialog.confirm(this,
Util.info(H_NEW_VERSION, Prop.NAME, latest))) {
Dialog.browse(this, UPDATE_URL);
} else {
// don't show update dialog anymore if it has been rejected once
gprop.set(GUIProp.UPDATEVERSION, latest.toString());
}
}
} catch(final Exception ex) {
// ignore connection failure
}
}
}
}