package net.sf.jabref.gui; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.io.IOException; import javax.swing.JPopupMenu; import javax.swing.SwingUtilities; import javax.swing.Timer; import net.sf.jabref.BasePanel; import net.sf.jabref.BibtexEntry; import net.sf.jabref.EntryEditor; import net.sf.jabref.FocusRequester; import net.sf.jabref.GUIGlobals; import net.sf.jabref.Globals; import net.sf.jabref.PreviewPanel; import net.sf.jabref.RightClickMenu; import net.sf.jabref.Util; import net.sf.jabref.external.ExternalFileMenuItem; import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.event.ListEvent; import ca.odell.glazedlists.event.ListEventListener; import net.sf.jabref.external.ExternalFileType; /** * List event, mouse, key and focus listener for the main table that makes up the * most part of the BasePanel for a single bib database. */ public class MainTableSelectionListener implements ListEventListener<BibtexEntry>, MouseListener, KeyListener, FocusListener { PreviewPanel[] previewPanel = null; int activePreview = Globals.prefs.getInt("activePreview"); PreviewPanel preview; MainTable table; BasePanel panel; EventList<BibtexEntry> tableRows; private boolean previewActive = Globals.prefs.getBoolean("previewEnabled"); private boolean workingOnPreview = false; private boolean enabled = true; // Register the last character pressed to quick jump in the table. Together // with storing the last row number jumped to, this is used to let multiple // key strokes cycle between all entries starting with the same letter: private int[] lastPressed = new int[20]; private int lastPressedCount = 0; private long lastPressedTime = 0; private long QUICK_JUMP_TIMEOUT = 2000; //private int lastCharPressed = -1; public MainTableSelectionListener(BasePanel panel, MainTable table) { this.table = table; this.panel = panel; this.tableRows = table.getTableRows(); instantiatePreviews(); this.preview = previewPanel[activePreview]; } public void setEnabled(boolean enabled) { this.enabled = enabled; } private void instantiatePreviews() { previewPanel = new PreviewPanel[] { new PreviewPanel(panel.database(), null, panel, panel.metaData(), Globals.prefs .get("preview0")), new PreviewPanel(panel.database(), null, panel, panel.metaData(), Globals.prefs .get("preview1")) }; //BibtexEntry testEntry = PreviewPrefsTab.getTestEntry(); //previewPanel[0].setEntry(testEntry); //previewPanel[1].setEntry(testEntry); } public void updatePreviews() { try { previewPanel[0].readLayout(Globals.prefs.get("preview0")); previewPanel[1].readLayout(Globals.prefs.get("preview1")); } catch (Exception e) { e.printStackTrace(); } } public void listChanged(ListEvent<BibtexEntry> e) { if (!enabled) { return; } EventList<BibtexEntry> selected = e.getSourceList(); Object newSelected = null; while (e.next()) { if (e.getType() == ListEvent.INSERT) { if (newSelected != null) return; // More than one new selected. Do nothing. else { if (e.getIndex() < selected.size()) newSelected = selected.get(e.getIndex()); } } } if (newSelected != null) { // Ok, we have a single new entry that has been selected. Now decide what to do with it: final BibtexEntry toShow = (BibtexEntry) newSelected; final int mode = panel.getMode(); // What is the panel already showing? if ((mode == BasePanel.WILL_SHOW_EDITOR) || (mode == BasePanel.SHOWING_EDITOR)) { // An entry is currently being edited. EntryEditor oldEditor = panel.getCurrentEditor(); String visName = null; if (oldEditor != null) { visName = oldEditor.getVisiblePanelName(); } // Get an old or new editor for the entry to edit: EntryEditor newEditor = panel.getEntryEditor(toShow); if ((oldEditor != null))// && (oldEditor != newEditor)) oldEditor.setMovingToDifferentEntry(); // Show the new editor unless it was already visible: if ((newEditor != oldEditor) || (mode != BasePanel.SHOWING_EDITOR)) { if (visName != null) newEditor.setVisiblePanel(visName); panel.showEntryEditor(newEditor); SwingUtilities.invokeLater(new Runnable() { public void run() { table.ensureVisible(table.getSelectedRow()); } }); } } else { // Either nothing or a preview was shown. Update the preview. if (previewActive) { updatePreview(toShow, false); } } } } private void updatePreview(final BibtexEntry toShow, final boolean changedPreview) { updatePreview(toShow, changedPreview, 0); } private void updatePreview(final BibtexEntry toShow, final boolean changedPreview, int repeats) { if (workingOnPreview) { if (repeats > 0) return; // We've already waited once. Give up on this selection. Timer t = new Timer(50, new ActionListener() { public void actionPerformed(ActionEvent actionEvent) { updatePreview(toShow, changedPreview, 1); } }); t.setRepeats(false); t.start(); return; } EventList<BibtexEntry> list = table.getSelected(); // Check if the entry to preview is still selected: if ((list.size() != 1) || (list.get(0) != toShow)) { return; } final int mode = panel.getMode(); workingOnPreview = true; final Runnable update = new Runnable() { public void run() { // If nothing was already shown, set the preview and move the separator: if (changedPreview || (mode == BasePanel.SHOWING_NOTHING)) { panel.showPreview(preview); panel.adjustSplitter(); } workingOnPreview = false; } }; final Runnable worker = new Runnable() { public void run() { preview.setEntry(toShow); SwingUtilities.invokeLater(update); } }; (new Thread(worker)).start(); } public void editSignalled() { if (table.getSelected().size() == 1) { editSignalled(table.getSelected().get(0)); } } public void editSignalled(BibtexEntry entry) { final int mode = panel.getMode(); EntryEditor editor = panel.getEntryEditor(entry); if (mode != BasePanel.SHOWING_EDITOR) { panel.showEntryEditor(editor); panel.adjustSplitter(); } new FocusRequester(editor); } public void mouseReleased(MouseEvent e) { // First find the row on which the user has clicked. final int row = table.rowAtPoint(e.getPoint()); // Check if the user has right-clicked. If so, open the right-click menu. if (e.isPopupTrigger()) { final int col = table.columnAtPoint(e.getPoint()); // Check if the user has clicked on an icon cell to open url or pdf. final String[] iconType = table.getIconTypeForColumn(col); if (iconType == null) processPopupTrigger(e, row); else showIconRightClickMenu(e, row, iconType); } } public void mousePressed(MouseEvent e) { // First find the column on which the user has clicked. final int col = table.columnAtPoint(e.getPoint()), row = table.rowAtPoint(e.getPoint()); // Check if the user has clicked on an icon cell to open url or pdf. final String[] iconType = table.getIconTypeForColumn(col); // Check if the user has right-clicked. If so, open the right-click menu. if (e.isPopupTrigger()) { if (iconType == null) processPopupTrigger(e, row); else showIconRightClickMenu(e, row, iconType); return; } } public void mouseClicked(MouseEvent e) { // First find the column on which the user has clicked. final int col = table.columnAtPoint(e.getPoint()), row = table.rowAtPoint(e.getPoint()); // A double click on an entry should open the entry's editor. if (e.getClickCount() == 2) { BibtexEntry toShow = tableRows.get(row); editSignalled(toShow); } // Check if the user has clicked on an icon cell to open url or pdf. final String[] iconType = table.getIconTypeForColumn(col); // Workaround for Windows. Right-click is not popup trigger on mousePressed, but // on mouseReleased. Therefore we need to avoid taking action at this point, because // action will be taken when the button is released: if (Globals.ON_WIN && (iconType != null) && (e.getButton() != MouseEvent.BUTTON1)) return; if (iconType != null) { Object value = table.getValueAt(row, col); if (value == null) return; // No icon here, so we do nothing. final BibtexEntry entry = tableRows.get(row); // Get the icon type. Corresponds to the field name. int hasField = -1; for (int i = iconType.length - 1; i >= 0; i--) if (entry.getField(iconType[i]) != null) hasField = i; if (hasField == -1) return; final String fieldName = iconType[hasField]; // Open it now. We do this in a thread, so the program won't freeze during the wait. (new Thread() { public void run() { panel.output(Globals.lang("External viewer called") + "."); Object link = entry.getField(fieldName); if (link == null) { Globals.logger("Error: no link to " + fieldName + "."); return; // There is an icon, but the field is not set. } { // See if this is a simple file link field, or if it is a file-list // field that can specify a list of links: if (fieldName.equals(GUIGlobals.FILE_FIELD)) { // We use a FileListTableModel to parse the field content: FileListTableModel fileList = new FileListTableModel(); fileList.setContent((String)link); // If there are one or more links, open the first one: if (fileList.getRowCount() > 0) { FileListEntry flEntry = fileList.getEntry(0); ExternalFileMenuItem item = new ExternalFileMenuItem (panel.frame(), entry, "", flEntry.getLink(), flEntry.getType().getIcon(), panel.metaData(), flEntry.getType()); boolean success = item.openLink(); if (!success) { panel.output(Globals.lang("Unable to open link.")); } } } else { try { Util.openExternalViewer(panel.metaData(), (String)link, fieldName); } catch (IOException ex) { panel.output(Globals.lang("Unable to open link.")); } /*ExternalFileType type = Globals.prefs.getExternalFileTypeByMimeType("text/html"); ExternalFileMenuItem item = new ExternalFileMenuItem (panel.frame(), entry, "", (String)link, type.getIcon(), panel.metaData(), type); boolean success = item.openLink(); if (!success) { panel.output(Globals.lang("Unable to open link.")); } */ //Util.openExternalViewer(panel.metaData(), (String)link, fieldName); } } //catch (IOException ex) { // panel.output(Globals.lang("Error") + ": " + ex.getMessage()); //} } }).start(); } } /** * Process general right-click events on the table. Show the table context menu at * the position where the user right-clicked. * @param e The mouse event defining the popup trigger. * @param row The row where the event occured. */ protected void processPopupTrigger(MouseEvent e, int row) { int selRow = table.getSelectedRow(); if (selRow == -1 ||// (getSelectedRowCount() == 0)) !table.isRowSelected(table.rowAtPoint(e.getPoint()))) { table.setRowSelectionInterval(row, row); //panel.updateViewToSelected(); } RightClickMenu rightClickMenu = new RightClickMenu(panel, panel.metaData()); rightClickMenu.show(table, e.getX(), e.getY()); } /** * Process popup trigger events occuring on an icon cell in the table. Show * a menu where the user can choose which external resource to open for the * entry. If no relevant external resources exist, let the normal popup trigger * handler do its thing instead. * @param e The mouse event defining this popup trigger. * @param row The row where the event occured. * @param iconType A string array containing the resource fields associated with * this table cell. */ private void showIconRightClickMenu(MouseEvent e, int row, String[] iconType) { BibtexEntry entry = tableRows.get(row); JPopupMenu menu = new JPopupMenu(); int count = 0; // See if this is a simple file link field, or if it is a file-list // field that can specify a list of links: if (iconType[0].equals(GUIGlobals.FILE_FIELD)) { // We use a FileListTableModel to parse the field content: Object o = entry.getField(iconType[0]); FileListTableModel fileList = new FileListTableModel(); fileList.setContent((String)o); // If there are one or more links, open the first one: for (int i=0; i<fileList.getRowCount(); i++) { FileListEntry flEntry = fileList.getEntry(i); String description = flEntry.getDescription(); if ((description == null) || (description.trim().length() == 0)) description = flEntry.getLink(); menu.add(new ExternalFileMenuItem(panel.frame(), entry, description, flEntry.getLink(), flEntry.getType().getIcon(), panel.metaData(), flEntry.getType())); count++; } } else { for (int i=0; i<iconType.length; i++) { Object o = entry.getField(iconType[i]); if (o != null) { menu.add(new ExternalFileMenuItem(panel.frame(), entry, (String)o, (String)o, GUIGlobals.getTableIcon(iconType[i]).getIcon(), panel.metaData(), iconType[i])); count++; } } } if (count == 0) { processPopupTrigger(e, row); return; } menu.show(table, e.getX(), e.getY()); } public void entryEditorClosing(EntryEditor editor) { preview.setEntry(editor.getEntry()); if (previewActive) panel.showPreview(preview); else panel.hideBottomComponent(); panel.adjustSplitter(); new FocusRequester(table); } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } public void setPreviewActive(boolean enabled) { previewActive = enabled; if (!previewActive) { panel.hideBottomComponent(); } else { if (table.getSelected().size() > 0 ) { updatePreview(table.getSelected().get(0), false); } } } public void switchPreview() { if (activePreview < previewPanel.length - 1) activePreview++; else activePreview = 0; Globals.prefs.putInt("activePreview", activePreview); if (previewActive) { this.preview = previewPanel[activePreview]; if (table.getSelected().size() > 0) { updatePreview(table.getSelected().get(0), true); } } } /** * Receive key event on the main table. If the key is a letter or a digit, * we should select the first entry in the table which starts with the given * letter in the column by which the table is sorted. * @param e The KeyEvent */ public void keyTyped(KeyEvent e) { if ((!e.isActionKey()) && Character.isLetterOrDigit(e.getKeyChar()) //&& !e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) { && (e.getModifiers() == 0)) { long time = System.currentTimeMillis(); if (time - lastPressedTime > QUICK_JUMP_TIMEOUT) lastPressedCount = 0; // Reset last pressed character // Update timestamp: lastPressedTime = time; // Add the new char to the search array: int c = e.getKeyChar(); if (lastPressedCount < lastPressed.length) lastPressed[lastPressedCount++] = c; int sortingColumn = table.getSortingColumn(0); if (sortingColumn == -1) return; // No sorting? TODO: look up by author, etc.? // TODO: the following lookup should be done by a faster algorithm, // such as binary search. But the table may not be sorted properly, // due to marked entries, search etc., which rules out the binary search. int startRow = 0; /*if ((c == lastPressed) && (lastQuickJumpRow >= 0)) { if (lastQuickJumpRow < table.getRowCount()-1) startRow = lastQuickJumpRow+1; }*/ boolean done = false; while (!done) { for (int i=startRow; i<table.getRowCount(); i++) { Object o = table.getValueAt(i, sortingColumn); if (o == null) continue; String s = o.toString().toLowerCase(); if (s.length() >= lastPressedCount) for (int j=0; j<lastPressedCount; j++) { if (s.charAt(j) != lastPressed[j]) break; // Escape the loop immediately when we find a mismatch else if (j == lastPressedCount-1) { // We found a match: table.setRowSelectionInterval(i, i); table.ensureVisible(i); return; } } //if ((s.length() >= 1) && (s.charAt(0) == c)) { //} } // Finished, no result. If we didn't start at the beginning of // the table, try that. Otherwise, exit the while loop. if (startRow > 0) startRow = 0; else done = true; } } else if (e.getKeyChar() == KeyEvent.VK_ESCAPE) { lastPressedCount = 0; } } public void keyReleased(KeyEvent e) { } public void keyPressed(KeyEvent e) { } public void focusGained(FocusEvent e) { } public void focusLost(FocusEvent e) { lastPressedCount = 0; // Reset quick jump when focus is lost. } }