package org.basex.gui; import static org.basex.core.Text.*; import static org.basex.gui.GUIConstants.*; import java.awt.*; import java.awt.event.*; import java.text.*; import java.util.regex.*; import javax.swing.*; import javax.swing.border.*; import org.basex.core.*; import org.basex.core.cmd.*; import org.basex.core.parse.*; import org.basex.data.*; import org.basex.gui.dialog.*; import org.basex.gui.layout.*; import org.basex.gui.view.*; import org.basex.gui.view.editor.*; import org.basex.gui.view.explore.*; import org.basex.gui.view.folder.*; import org.basex.gui.view.info.*; import org.basex.gui.view.map.*; import org.basex.gui.view.plot.*; import org.basex.gui.view.table.*; import org.basex.gui.view.text.*; import org.basex.gui.view.tree.*; import org.basex.io.*; import org.basex.io.out.*; import org.basex.query.*; import org.basex.query.value.*; import org.basex.query.value.seq.*; import org.basex.util.*; import org.basex.util.options.*; /** * This class is the main window of the GUI. It is the central instance for user interactions. * * @author BaseX Team 2005-17, BSD License * @author Christian Gruen */ public final class GUI extends JFrame { /** Database Context. */ public final Context context; /** GUI options. */ public final GUIOptions gopts; /** View Manager. */ public final ViewNotifier notify; /** Status line. */ public final GUIStatus status; /** Input field. */ public final GUIInput input; /** Filter button. */ public final AbstractButton 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; /** Currently executed command ({@code null} otherwise). */ public Command command; /** ID of currently executed command. */ public int commandID; /** Fullscreen flag. */ boolean fullscreen; /** Button panel. */ final BaseXBack buttons; /** Navigation/input panel. */ final BaseXBack nav; /** Result panel. */ private final GUIMenu menu; /** Content panel, containing all views. */ private final ViewContainer views; /** History button. */ private final AbstractButton hist; /** Execution Button. */ private final AbstractButton go; /** Execution Button. */ private final AbstractButton stop; /** Current input Mode. */ private final BaseXCombo mode; /** Text view. */ private final TextView text; /** Top panel. */ private final BaseXBack top; /** Control panel. */ private final BaseXBack control; /** Results label. */ private final BaseXLabel hits; /** Buttons. */ private final GUIToolBar toolbar; /** Menu panel height. */ private int menuHeight; /** Fullscreen Window. */ private JFrame fullscr; /** Password reader. */ private static volatile PasswordReader pwReader; /** * Default constructor. * @param context database context * @param gopts gui options */ public GUI(final Context context, final GUIOptions gopts) { this.context = context; this.gopts = gopts; setIconImage(BaseXImages.get("logo_64")); setTitle(); GUIMacOSX.enableOSXFullscreen(this); // set window size final Dimension scr = Toolkit.getDefaultToolkit().getScreenSize(); final int[] loc = this.gopts.get(GUIOptions.GUILOC); final int[] size = this.gopts.get(GUIOptions.GUISIZE); final int x = Math.max(0, Math.min(scr.width - size[0], loc[0])); final int y = Math.max(0, Math.min(scr.height - size[1], loc[1])); setBounds(x, y, size[0], size[1]); if(this.gopts.get(GUIOptions.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(18.0f)); hits.setHorizontalAlignment(SwingConstants.RIGHT); BaseXBack b = new BaseXBack(); b.add(hits); buttons.add(b, BorderLayout.EAST); if(this.gopts.get(GUIOptions.SHOWBUTTONS)) control.add(buttons, BorderLayout.CENTER); nav = new BaseXBack(new BorderLayout(5, 0)).border(2, 2, 0, 2); mode = new BaseXCombo(this, FIND, XQUERY, COMMAND); mode.setSelectedIndex(2); mode.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { final int s = mode.getSelectedIndex(); if(s == gopts.get(GUIOptions.SEARCHMODE) || !mode.isEnabled()) return; gopts.set(GUIOptions.SEARCHMODE, s); input.mode(mode.getSelectedItem()); refreshControls(); } }); nav.add(mode, BorderLayout.WEST); input = new GUIInput(this); input.mode(mode.getSelectedItem()); hist = BaseXButton.get("c_hist", INPUT_HISTORY, false, this); 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 : gopts.get(GUIOptions.SEARCHMODE); final String[] hs = gopts.get( i == 0 ? GUIOptions.SEARCH : i == 1 ? GUIOptions.XQUERY : GUIOptions.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); stop = BaseXButton.get("c_stop", STOP, false, this); stop.setEnabled(false); stop.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { if(command != null) { command.stop(); stop.setEnabled(false); } } }); go = BaseXButton.get("c_go", RUN_QUERY, false, this); go.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { input.store(); execute(); } }); filter = BaseXButton.command(GUIMenuCmd.C_FILTER, this); b = new BaseXBack(new TableLayout(1, 3, 1, 0)); b.add(stop); b.add(go); b.add(filter); nav.add(b, BorderLayout.EAST); if(this.gopts.get(GUIOptions.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(this.gopts.get(GUIOptions.SHOWSTATUS)) top.add(status, BorderLayout.SOUTH); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); add(top); setVisible(true); views.updateViews(); refreshControls(); // check version checkVersion(); input.requestFocusInWindow(); } @Override public void dispose() { saveOptions(); // check if all modified texts are saved or closed if(editor.confirm(null)) { context.close(); super.dispose(); } } /** * Saves the current configuration. */ public void saveOptions() { editor.saveOptions(); final boolean max = getExtendedState() == MAXIMIZED_BOTH; gopts.set(GUIOptions.MAXSTATE, max); if(!max) { gopts.set(GUIOptions.GUILOC, new int[] { getX(), getY()}); gopts.set(GUIOptions.GUISIZE, new int[] { getWidth(), getHeight()}); } gopts.write(); context.soptions.write(); } /** * Sets the window title. */ public void setTitle() { final TokenBuilder tb = new TokenBuilder(); final EditorArea ea = editor == null ? null : editor.getEditor(); if(ea != null) { if(ea.opened()) { tb.add(ea.file().path()); } else { tb.add(ea.file().name()); } if(ea.modified()) tb.add('*'); } final Data data = context.data(); if(data != null) { if(!tb.isEmpty()) tb.add(' '); tb.add("[").add(data.meta.name).add("]"); } if(!tb.isEmpty()) tb.add(" - "); tb.add(Prop.TITLE); setTitle(tb.toString()); } /** * Sets a cursor. * @param c cursor to be set */ public void cursor(final Cursor c) { cursor(c, false); } /** * Sets a cursor, forcing a new look if necessary. * @param c cursor to be set * @param force new cursor */ public void cursor(final Cursor c, final boolean force) { final Cursor cc = getCursor(); if(cc != c && (cc != CURSORWAIT || force)) setCursor(c); } /** * Executes the input of the {@link GUIInput} bar. */ void execute() { final String in = input.getText().trim(); final boolean cmd = mode.getSelectedIndex() == 2; // run as command: command mode or exclamation mark as first character final boolean exc = in.startsWith("!"); if(cmd || exc) { try { // parse and execute all commands final CommandParser cp = CommandParser.get(in.substring(exc ? 1 : 0), context); if(pwReader == null) pwReader = new PasswordReader() { @Override public String password() { final DialogPass dp = new DialogPass(GUI.this); return dp.ok() ? dp.password() : ""; } }; cp.pwReader(pwReader); execute(cp.parse()); } catch(final QueryException ex) { if(!info.visible()) GUIMenuCmd.C_SHOWINFO.execute(this); info.setInfo(Util.message(ex), null, false, true); } } else if(gopts.get(GUIOptions.SEARCHMODE) == 1 || in.startsWith("/")) { simpleQuery(in); } else { execute(new Find(in, gopts.get(GUIOptions.FILTERRT))); } } /** * Launches a simple single-line query. Adds the default namespace if available. * @param query expression to be run */ public void simpleQuery(final String query) { // check and add default namespace String q = query.trim().isEmpty() ? "()" : query; final Data data = context.data(); final Namespaces ns = data.nspaces; final int uriId = ns.uriIdForPrefix(Token.EMPTY, 0, data); if(uriId != 0) q = Util.info("declare default element namespace \"%\"; %", ns.uri(uriId), q); execute(new XQuery(q)); } /** * Launches the specified commands in a separate thread. * Commands are ignored if an update operation takes place. * @param cmd commands to be executed */ public void execute(final Command... cmd) { execute(false, cmd); } /** * Launches the specified commands in a separate thread. * Commands are ignored if an update operation takes place. * @param edit call from editor view * @param cmd commands to be executed */ public void execute(final boolean edit, final Command... cmd) { // ignore command if updates take place if(updating) return; new Thread() { @Override public void run() { if(cmd.length == 0) info.setInfo("", null, true, true); for(final Command c : cmd) { if(!exec(c, edit)) break; } } }.start(); } /** * Executes the specified command. * @param cmd command to be executed * @param edit called from editor view * @return success flag */ private boolean exec(final Command cmd, final boolean edit) { // wait when command is still running final int thread = ++commandID; while(true) { final Command c = command; if(c == null) break; c.stop(); Performance.sleep(1); if(commandID != thread) return true; } // indicates that the command will be executed cursor(CURSORWAIT); input.setCursor(CURSORWAIT); stop.setEnabled(true); if(edit) editor.pleaseWait(thread); final Data data = context.data(); // reset current context if realtime filter is activated if(gopts.get(GUIOptions.FILTERRT) && data != null && !context.root()) context.invalidate(); // remember current command and context nodes final DBNodes current = context.current(); command = cmd; // execute command and cache result final ArrayOutput ao = new ArrayOutput(); ao.setLimit(gopts.get(GUIOptions.MAXTEXT)); // sets the maximum number of hits cmd.maxResults(gopts.get(GUIOptions.MAXRESULTS)); final Performance perf = new Performance(); boolean ok = true; try { // checks if the command is updating updating = cmd.updating(context); // reset visualizations if data reference will be changed if(cmd.newData(context)) notify.init(); // attaches the info listener to the command cmd.jc().tracer = info; // evaluate command String inf; Throwable cause = null; try { cmd.execute(context, ao); inf = cmd.info(); } catch(final BaseXException ex) { cause = ex.getCause(); if(cause == null) cause = ex; ok = false; inf = Util.message(ex); } finally { updating = false; } // show query info, send feedback to query editor final String time = info.setInfo(inf, cmd, perf.getTime(), ok, true); final boolean stopped = inf.endsWith(INTERRUPTED); if(edit) editor.info(cause, stopped, true); // get query result and node references to currently opened database final Value result = cmd.result(); DBNodes nodes = result instanceof DBNodes && !result.isEmpty() ? (DBNodes) result : null; // show text view if a non-empty result does not reference the currently opened database if(!text.visible() && ao.size() != 0 && nodes == null) { GUIMenuCmd.C_SHOWRESULT.execute(this); } // check if query feedback was evaluated in the query view if(!ok && !stopped) { // display error in info view text.setText(ao); if(!info.visible() && (!edit || inf.startsWith(S_BUGINFO))) { GUIMenuCmd.C_SHOWINFO.execute(this); } } else { final boolean updated = cmd.updated(context); if(context.data() != data) { // database reference has changed - notify views notify.init(); } else if(updated) { // update visualizations notify.update(); // adopt updated nodes as result set if(nodes == null) nodes = context.current(); } else if(result != null) { // check if result has changed final boolean flt = gopts.get(GUIOptions.FILTERRT); final DBNodes curr = context.current(); if(flt || curr != null && !curr.sameAs(current)) { // refresh context if at least one node was found if(nodes != null) notify.context(nodes, flt, null); } else if(context.marked != null) { // refresh highlight DBNodes m = context.marked; if(nodes != null) { // use query result m = nodes; } else if(!m.isEmpty()) { // remove old highlighting m = new DBNodes(data); } // refresh views if(context.marked != m) notify.mark(m, null); } } if(thread == commandID && !stopped) { // show status info status.setText(TIME_REQUIRED + COLS + time); // show number of hits if(result != null) setResults(result.size()); // assign textual output if no node result was created if(nodes == null) text.setText(ao); // only cache output if data has not been updated (in which case notifyUpdate was called) if(!updated) text.cache(ao, cmd, result); } } } catch(final Exception ex) { // unexpected error BaseXDialog.error(this, Util.info(EXEC_ERROR_X_X, cmd, Util.bug(ex))); updating = false; } stop(); return ok; } /** * Stops the current command. */ public void stop() { if(command != null) command.stop(); cursor(CURSORARROW, true); input.setCursor(CURSORTEXT); stop.setEnabled(false); command = null; } /** * Sets an option if its value differs from current value and displays the command in the info * view. * @param <T> option type * @param <V> value type * @param opt option to be set * @param val value */ public <T extends Option<V>, V> void set(final T opt, final V val) { if(!context.options.get(opt).toString().equals(val.toString())) { final Set cmd = new Set(opt, val); cmd.run(context); info.setInfo(cmd.info(), cmd, true, false); } } /** * 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); } else { views.setBorder(new CompoundBorder(BaseXLayout.border(3, 1, 3, 1), BorderFactory.createEtchedBorder(EtchedBorder.LOWERED))); } } /** * Refreshes the layout. */ public void updateLayout() { init(gopts); 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 */ void updateControl(final JComponent comp, final boolean show, final String layout) { if(comp == status) { if(show) top.add(comp, layout); else top.remove(comp); } else if(comp == menu) { if(!show) menuHeight = menu.getHeight(); final int s = show ? menuHeight : 0; comp.setPreferredSize(new Dimension(comp.getPreferredSize().width, s)); menu.setSize(menu.getWidth(), s); } else { // buttons, input if(show) control.add(comp, layout); else control.remove(comp); } 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 DBNodes marked = context.marked; if(marked != null) setResults(marked.size()); filter.setEnabled(marked != null && !marked.isEmpty()); final boolean inf = gopts.get(GUIOptions.SHOWINFO); context.options.set(MainOptions.QUERYINFO, inf); context.options.set(MainOptions.XMLPLAN, inf); final Data data = context.data(); final int t = mode.getSelectedIndex(); final int s = data == null ? 2 : gopts.get(GUIOptions.SEARCHMODE); mode.setEnabled(data != null); go.setEnabled(s == 2 || !gopts.get(GUIOptions.EXECRT)); if(s != t) { mode.setSelectedIndex(s); input.mode(mode.getSelectedItem()); input.requestFocusInWindow(); } toolbar.refresh(); menu.refresh(); final int i = context.data() == null ? 2 : gopts.get(GUIOptions.SEARCHMODE); final StringsOption options = i == 0 ? GUIOptions.SEARCH : i == 1 ? GUIOptions.XQUERY : GUIOptions.COMMANDS; hist.setEnabled(gopts.get(options).length != 0); } /** * Sets results information. * @param count number of results */ private void setResults(final long count) { final int max = gopts.get(GUIOptions.MAXRESULTS); final String num = new DecimalFormat("#,###,###").format(count); hits.setText(Util.info(RESULTS_X, (count >= max ? "\u2265" : "") + num)); } /** * Toggles fullscreen mode. */ void fullscreen() { fullscreen ^= true; fullscreen(fullscreen); } /** * Turns fullscreen mode on/off. * @param full fullscreen flag */ 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(!gopts.get(GUIOptions.SHOWBUTTONS)) control.add(buttons, BorderLayout.CENTER); if(!gopts.get(GUIOptions.SHOWINPUT)) control.add(nav, BorderLayout.SOUTH); if(!gopts.get(GUIOptions.SHOWSTATUS)) top.add(status, BorderLayout.SOUTH); setJMenuBar(menu); add(top); } gopts.set(GUIOptions.SHOWBUTTONS, !full); gopts.set(GUIOptions.SHOWINPUT, !full); gopts.set(GUIOptions.SHOWSTATUS, !full); fullscreen = full; GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().setFullScreenWindow( fullscr); setContentBorder(); refreshControls(); updateControl(menu, !full, BorderLayout.NORTH); setVisible(!full); } /** * Starts a new thread that checks for new versions. */ private void checkVersion() { // ignore snapshots and beta versions if(Strings.contains(Prop.VERSION, ' ')) return; new GUIWorker<Version>() { @Override protected Version doInBackground() throws Exception { final Version disk = new Version(gopts.get(GUIOptions.UPDATEVERSION)); final Version used = new Version(Prop.VERSION); if(disk.compareTo(used) < 0) { // update version option to latest used version writeVersion(used); } else { final String page = Token.string(new IOUrl(Prop.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) return latest; } } return null; } @Override protected void done(final Version latest) { if(BaseXDialog.confirm(GUI.this, Util.info(H_NEW_VERSION, Prop.NAME, latest))) { // jump to browser BaseXDialog.browse(GUI.this, Prop.UPDATE_URL); } else { // don't show update dialog anymore if it has been rejected once writeVersion(latest); } } private void writeVersion(final Version version) { gopts.set(GUIOptions.UPDATEVERSION, version.toString()); gopts.write(); } }.execute(); } }