package org.basex.gui.view.editor; import static org.basex.core.Text.*; import static org.basex.gui.GUIConstants.EDITORVIEW; import static org.basex.util.Token.cl; import static org.basex.util.Token.token; import java.awt.BorderLayout; import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.File; import java.io.IOException; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.Box; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.basex.data.Nodes; import org.basex.gui.GUICommands; import org.basex.gui.GUIConstants; import org.basex.gui.GUIConstants.Fill; import org.basex.gui.GUIConstants.Msg; import org.basex.gui.GUIMenu; import org.basex.gui.GUIProp; import org.basex.gui.dialog.Dialog; import org.basex.gui.layout.*; import org.basex.gui.layout.BaseXFileChooser.Mode; import org.basex.gui.layout.BaseXLayout.DropHandler; import org.basex.gui.view.View; import org.basex.gui.view.ViewNotifier; import org.basex.io.IO; import org.basex.io.IOFile; import org.basex.util.Performance; import org.basex.util.Util; import org.basex.util.list.BoolList; import org.basex.util.list.ObjList; /** * This view allows the input and evaluation of queries and documents. * * @author BaseX Team 2005-12, BSD License * @author Christian Gruen */ public final class EditorView extends View { /** Error string. */ private static final String ERRSTRING = STOPPED_AT + ' ' + (LINE_X + ", " + COLUMN_X).replaceAll("%", "([0-9]+)"); /** Error file pattern. */ private static final Pattern FILEPATTERN = Pattern.compile(ERRSTRING + ' ' + IN_FILE_X.replaceAll("%", "(.*)") + COL); /** Execute Button. */ final BaseXButton stop; /** Info label. */ final BaseXLabel info; /** Position label. */ final BaseXLabel pos; /** Query area. */ final BaseXTabs tabs; /** Search field. */ final BaseXTextField find; /** Execute button. */ final BaseXButton go; /** Thread counter. */ int threadID; /** File in which the most recent error occurred. */ String errFile; /** Most recent error position. */ int errPos; /** Header string. */ private final BaseXLabel header; /** Filter button. */ private final BaseXButton filter; /** * Default constructor. * @param man view manager */ public EditorView(final ViewNotifier man) { super(EDITORVIEW, man); border(6, 6, 6, 6).layout(new BorderLayout()).setFocusable(false); header = new BaseXLabel(EDITOR, true, false); final BaseXButton openB = BaseXButton.command(GUICommands.C_EDITOPEN, gui); final BaseXButton saveB = new BaseXButton(gui, "editsave", token(H_SAVE)); final BaseXButton hist = new BaseXButton(gui, "hist", token(H_RECENTLY_OPEN)); find = new BaseXTextField(gui); BaseXLayout.setHeight(find, (int) openB.getPreferredSize().getHeight()); BaseXBack sp = new BaseXBack(Fill.NONE).layout(new TableLayout(1, 7)); sp.add(find); sp.add(Box.createHorizontalStrut(5)); sp.add(openB); sp.add(Box.createHorizontalStrut(1)); sp.add(saveB); sp.add(Box.createHorizontalStrut(1)); sp.add(hist); final BaseXBack b = new BaseXBack(Fill.NONE).layout(new BorderLayout()); b.add(header, BorderLayout.CENTER); b.add(sp, BorderLayout.EAST); add(b, BorderLayout.NORTH); tabs = new BaseXTabs(gui); tabs.setFocusable(false); addCreateTab(); addTab().setSearch(find); add(tabs, BorderLayout.CENTER); /* Scroll Pane. */ final BaseXBack south = new BaseXBack(Fill.NONE).layout( new BorderLayout(8, 0)); info = new BaseXLabel(" "); info.setText(OK, Msg.SUCCESS); pos = new BaseXLabel(" "); sp = new BaseXBack(Fill.NONE).layout(new BorderLayout(8, 0)); sp.add(info, BorderLayout.CENTER); sp.add(pos, BorderLayout.EAST); south.add(sp, BorderLayout.CENTER); stop = new BaseXButton(gui, "stop", token(H_STOP_PROCESS)); stop.addKeyListener(this); stop.setEnabled(false); go = new BaseXButton(gui, "go", token(H_EXECUTE_QUERY)); go.addKeyListener(this); filter = BaseXButton.command(GUICommands.C_FILTER, gui); filter.addKeyListener(this); sp = new BaseXBack(Fill.NONE).border(4, 0, 0, 0).layout( new TableLayout(1, 5)); sp.add(stop); sp.add(Box.createHorizontalStrut(1)); sp.add(go); sp.add(Box.createHorizontalStrut(1)); sp.add(filter); south.add(sp, BorderLayout.EAST); add(south, BorderLayout.SOUTH); refreshLayout(); // add listeners saveB.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { final JPopupMenu pop = new JPopupMenu(); final StringBuilder mnem = new StringBuilder(); final JMenuItem sa = GUIMenu.newItem(GUICommands.C_EDITSAVE, gui, mnem); final JMenuItem sas = GUIMenu.newItem(GUICommands.C_EDITSAVEAS, gui, mnem); GUICommands.C_EDITSAVE.refresh(gui, sa); GUICommands.C_EDITSAVEAS.refresh(gui, sas); pop.add(sa); pop.add(sas); pop.show(saveB, 0, saveB.getHeight()); } }); hist.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { final JPopupMenu popup = new JPopupMenu(); final ActionListener al = new ActionListener() { @Override public void actionPerformed(final ActionEvent ac) { open(new IOFile(ac.getActionCommand())); } }; if(gui.gprop.strings(GUIProp.QUERIES).length == 0) { popup.add(new JMenuItem("- No recently opened files -")); } for(final String en : gui.gprop.strings(GUIProp.QUERIES)) { final JMenuItem jmi = new JMenuItem(en); jmi.addActionListener(al); popup.add(jmi); } popup.show(hist, 0, hist.getHeight()); } }); info.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(final MouseEvent e) { EditorArea edit = getEditor(); if(errFile != null) { edit = find(IO.get(errFile), false); if(edit == null) edit = open(new IOFile(errFile)); tabs.setSelectedComponent(edit); } if(errPos == -1) return; edit.markError(errPos, true); pos.setText(edit.pos()); } }); stop.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { stop.setEnabled(false); go.setEnabled(false); info.setText(OK, Msg.SUCCESS); gui.stop(); } }); go.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { getEditor().query(); } }); tabs.addChangeListener(new ChangeListener() { @Override public void stateChanged(final ChangeEvent e) { final EditorArea edit = getEditor(); if(edit == null) return; edit.setSearch(find); gui.refreshControls(); refreshMark(); pos.setText(edit.pos()); if(gui.gprop.is(GUIProp.EXECRT)) edit.query(); } }); BaseXLayout.addDrop(this, new DropHandler() { @Override public void drop(final Object file) { if(file instanceof File) open(new IOFile((File) file)); } }); } @Override public void refreshInit() { } @Override public void refreshFocus() { } @Override public void refreshMark() { go.setEnabled(getEditor().executable && !gui.gprop.is(GUIProp.EXECRT)); final Nodes marked = gui.context.marked; filter.setEnabled(!gui.gprop.is(GUIProp.FILTERRT) && marked != null && marked.size() != 0); } @Override public void refreshContext(final boolean more, final boolean quick) { } @Override public void refreshLayout() { header.setFont(GUIConstants.lfont); for(final EditorArea edit : editors()) edit.setFont(GUIConstants.mfont); refreshMark(); } @Override public void refreshUpdate() { } @Override public boolean visible() { return gui.gprop.is(GUIProp.SHOWEDITOR); } @Override public void visible(final boolean v) { gui.gprop.set(GUIProp.SHOWEDITOR, v); } @Override protected boolean db() { return false; } /** * Opens a new file. */ public void open() { // open file chooser for XML creation final BaseXFileChooser fc = new BaseXFileChooser(OPEN, gui.gprop.get(GUIProp.XQPATH), gui); fc.addFilter(XQUERY_FILES, IO.XQSUFFIXES); final IOFile file = fc.select(Mode.FOPEN); if(file != null) open(file); } /** * Saves the contents of the currently opened editor. * @return {@code false} if operation was canceled */ public boolean save() { final EditorArea edit = getEditor(); if(!edit.opened()) return saveAs(); save(edit.file()); return true; } /** * Saves the contents of the currently opened editor under a new name. * @return {@code false} if operation was canceled */ public boolean saveAs() { // open file chooser for XML creation final EditorArea edit = getEditor(); final BaseXFileChooser fc = new BaseXFileChooser(SAVE_AS, edit.file().path(), gui); fc.addFilter(XQUERY_FILES, IO.XQSUFFIXES); final IOFile file = fc.select(Mode.FSAVE); if(file == null) return false; save(file); return true; } /** * Creates a new file. */ public void newFile() { addTab(); refresh(false, true); } /** * Opens the specified query file. * @param file query file * @return opened editor */ public EditorArea open(final IOFile file) { if(!visible()) GUICommands.C_SHOWEDITOR.execute(gui); EditorArea edit = find(file, true); try { if(edit != null) { // switch to open file tabs.setSelectedComponent(edit); // check if file in memory was modified, and save it if necessary if(!confirm(edit)) return edit; } else { // get current editor edit = getEditor(); // create new tab if current text is stored on disk or has been modified if(edit.opened() || edit.modified) edit = addTab(); edit.file(file); } // set new text, update file history and refresh the file modification edit.setText(file.read()); gui.gprop.recent(file); refresh(false, true); if(gui.gprop.is(GUIProp.EXECRT)) edit.query(); } catch(final IOException ex) { Dialog.error(gui, FILE_NOT_OPENED); } return edit; } /** * Closes an editor. * @param edit editor to be closed. {@code null} closes the currently * opened editor. * opened editor is to be closed */ public void close(final EditorArea edit) { final EditorArea ea = edit != null ? edit : getEditor(); if(!confirm(ea)) return; tabs.remove(ea); final int t = tabs.getTabCount(); final int i = tabs.getSelectedIndex(); if(t == 1) { // reopen single tab addTab(); } else if(i + 1 == t) { // if necessary, activate last editor tab tabs.setSelectedIndex(i - 1); } } /** * Initializes the info message. */ public void reset() { ++threadID; errFile = null; info.setToolTipText(null); info.setText(OK, Msg.SUCCESS); stop.setEnabled(false); } /** * Starts a thread, which shows a waiting info after a short timeout. */ public void start() { final int thread = ++threadID; new Thread() { @Override public void run() { Performance.sleep(200); if(thread == threadID) { info.setToolTipText(null); info.setText(PLEASE_WAIT_D, Msg.SUCCESS); stop.setEnabled(true); } } }.start(); } /** * Evaluates the info message resulting from a query execution. * @param msg info message * @param ok true if query was successful */ public void info(final String msg, final boolean ok) { ++threadID; errPos = -1; errFile = null; info.setCursor(!ok && error(msg) ? GUIConstants.CURSORHAND : GUIConstants.CURSORARROW); info.setText(msg.replaceAll(STOPPED_AT + ".*\\r?\\n\\[.*?\\] ", ""), ok ? Msg.SUCCESS : Msg.ERROR); info.setToolTipText(ok ? null : msg); stop.setEnabled(false); go.setEnabled(true); } /** * Handles info messages resulting from a query execution. * @param msg info message * @return true if error was found */ private boolean error(final String msg) { EditorArea edit = getEditor(); final Matcher m = FILEPATTERN.matcher(msg.replaceAll("[\\r\\n].*", "")); if(!m.matches()) return true; errFile = m.group(3); edit = find(IO.get(errFile), false); if(edit == null) return true; final int el = Integer.parseInt(m.group(1)); final int ec = Integer.parseInt(m.group(2)); // find approximate error position final int ll = edit.last.length; errPos = ll; for(int e = 0, l = 1, c = 1; e < ll; ++c, e += cl(edit.last, e)) { if(l > el || l == el && c == ec) { errPos = e; break; } if(edit.last[e] == '\n') { ++l; c = 0; } } edit.markError(errPos, false); return errPos != -1; } /** * Shows a quit dialog for all modified query files. * @return {@code false} if confirmation was canceled */ public boolean confirm() { for(final EditorArea edit : editors()) if(!confirm(edit)) return false; return true; } /** * Checks if the current text can be saved. The check returns {@code true} * if the text has not been opened from disk, or if it has been modified. * @return result of check */ public boolean saveable() { final EditorArea area = getEditor(); return !area.opened() || area.modified; } /** * Returns the current editor. * @return editor */ EditorArea getEditor() { final Component c = tabs.getSelectedComponent(); return c instanceof EditorArea ? (EditorArea) c : null; } /** * Refreshes the query modification flag. * @param mod modification flag * @param force action */ void refresh(final boolean mod, final boolean force) { final EditorArea edit = getEditor(); refreshMark(); if(edit.modified == mod && !force) return; String title = edit.file().name(); if(mod) title += "*"; edit.label.setText(title); edit.modified = mod; gui.refreshControls(); } /** * Finds the editor that contains the specified file. * @param file file to be found * @param opened considers only opened files * @return editor */ EditorArea find(final IO file, final boolean opened) { for(final EditorArea edit : editors()) { if(edit.file().eq(file) && (!opened || edit.opened())) return edit; } return null; } /** * Saves the specified editor contents. * @param file file to write */ private void save(final IOFile file) { try { final EditorArea edit = getEditor(); file.write(edit.getText()); edit.file(file); edit.tstamp = file.timeStamp(); gui.gprop.recent(file); refresh(false, true); } catch(final IOException ex) { Dialog.error(gui, FILE_NOT_SAVED); } } /** * Choose a unique tab file. * @return io reference */ private IOFile newTabFile() { // collect numbers of existing files final BoolList bl = new BoolList(); for(final EditorArea edit : editors()) { if(edit.opened()) continue; final String n = edit.file().name().substring(FILE.length()); bl.set(n.isEmpty() ? 1 : Integer.parseInt(n), true); } // find first free file number int c = 0; while(++c < bl.size() && bl.get(c)); // create io reference final String dir = gui.gprop.get(GUIProp.XQPATH); return new IOFile(dir, FILE + (c == 1 ? "" : c)); } /** * Adds a new editor tab. * @return editor reference */ EditorArea addTab() { final EditorArea edit = new EditorArea(this, newTabFile()); edit.setFont(GUIConstants.mfont); final BaseXBack tab = new BaseXBack( new BorderLayout(10, 0)).mode(Fill.NONE); tab.add(edit.label, BorderLayout.CENTER); final BaseXButton close = tabButton("editclose"); close.setRolloverIcon(BaseXLayout.icon("editclose2")); close.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { close(edit); } }); tab.add(close, BorderLayout.EAST); tabs.add(edit, tab, tabs.getComponentCount() - 2); return edit; } /** * Adds a tab for creating new tabs. */ private void addCreateTab() { final BaseXButton add = tabButton("editnew"); add.setRolloverIcon(BaseXLayout.icon("editnew2")); add.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { addTab(); refresh(false, true); } }); tabs.add(new BaseXBack(), add, 0); tabs.setEnabledAt(0, false); } /** * Adds a new tab button. * @param icon button icon * @return button */ private BaseXButton tabButton(final String icon) { final BaseXButton b = new BaseXButton(gui, icon, null); b.border(2, 2, 2, 2).setContentAreaFilled(false); b.setFocusable(false); return b; } /** * Shows a quit dialog for the specified editor. * @param edit editor to be saved * @return {@code false} if confirmation was canceled */ private boolean confirm(final EditorArea edit) { if(edit.modified) { final Boolean ok = Dialog.yesNoCancel(gui, Util.info(CLOSE_FILE_X, edit.file().name())); if(ok == null || ok && !save()) return false; } return true; } /** * Returns all editors. * @return editors */ EditorArea[] editors() { final ObjList<EditorArea> edits = new ObjList<EditorArea>(); for(final Component c : tabs.getComponents()) { if(c instanceof EditorArea) edits.add((EditorArea) c); } return edits.toArray(new EditorArea[edits.size()]); } }