Java tutorial
/* Copyright (C) 2003-2011 JabRef contributors. This program 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 2 of the License, or (at your option) any later version. This program 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 this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package net.sf.jabref.gui; 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.gui.entryeditor.EntryEditor; import net.sf.jabref.gui.util.FocusRequester; import net.sf.jabref.logic.l10n.Localization; import net.sf.jabref.logic.util.OS; import net.sf.jabref.model.entry.BibtexEntry; import net.sf.jabref.gui.desktop.JabRefDesktop; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import net.sf.jabref.*; import net.sf.jabref.external.ExternalFileMenuItem; import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.event.ListEvent; import ca.odell.glazedlists.event.ListEventListener; import java.util.Arrays; import java.util.Collections; import java.util.List; import net.sf.jabref.specialfields.SpecialField; import net.sf.jabref.specialfields.SpecialFieldValue; import net.sf.jabref.specialfields.SpecialFieldsUtils; /** * 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 { private final PreviewPanel[] previewPanel; private final MainTable table; private final BasePanel panel; private final EventList<BibtexEntry> tableRows; private int activePreview = Globals.prefs.getInt(JabRefPreferences.ACTIVE_PREVIEW); private PreviewPanel preview; private boolean previewActive = Globals.prefs.getBoolean(JabRefPreferences.PREVIEW_ENABLED); private boolean workingOnPreview; 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 final int[] lastPressed = new int[20]; private int lastPressedCount; private long lastPressedTime; private static final Log LOGGER = LogFactory.getLog(MainTableSelectionListener.class); //private int lastCharPressed = -1; public MainTableSelectionListener(BasePanel panel, MainTable table) { this.table = table; this.panel = panel; this.tableRows = table.getTableRows(); previewPanel = new PreviewPanel[] { new PreviewPanel(panel.database(), null, panel, panel.metaData(), Globals.prefs.get(JabRefPreferences.PREVIEW_0), true), new PreviewPanel(panel.database(), null, panel, panel.metaData(), Globals.prefs.get(JabRefPreferences.PREVIEW_1), true) }; panel.getSearchBar().getSearchTextObservable().addSearchListener(previewPanel[0]); panel.getSearchBar().getSearchTextObservable().addSearchListener(previewPanel[1]); this.preview = previewPanel[activePreview]; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public void updatePreviews() { try { previewPanel[0].updateLayout(Globals.prefs.get(JabRefPreferences.PREVIEW_0)); previewPanel[1].updateLayout(Globals.prefs.get(JabRefPreferences.PREVIEW_1)); } catch (IOException e) { LOGGER.debug("error while updating preview", e); } } @Override 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.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() { @Override 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, 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; SwingUtilities.invokeLater(() -> { preview.setEntry(toShow); // 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; }); } 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); } @Override public void mouseReleased(MouseEvent e) { // First find the column and row on which the user has clicked. final int col = table.columnAtPoint(e.getPoint()); final int 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() || (e.getButton() == MouseEvent.BUTTON3)) { if (iconType == null) { processPopupTrigger(e, row); } else { showIconRightClickMenu(e, row, iconType); } } } @Override public void mousePressed(MouseEvent e) { // all handling is done in "mouseReleased" } @Override public void mouseClicked(MouseEvent e) { // First find the column on which the user has clicked. final int col = table.columnAtPoint(e.getPoint()); final int 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 (OS.WINDOWS && (iconType != null) && (e.getButton() != MouseEvent.BUTTON1)) { return; } if (iconType != null) { // left click on icon field SpecialField field = SpecialFieldsUtils.getSpecialFieldInstanceFromFieldName(iconType[0]); if ((e.getClickCount() == 1) && (field != null)) { // special field found if (field.isSingleValueField()) { // directly execute toggle action instead of showing a menu with one action field.getValues().get(0).getAction(panel.frame()).action(); } else { JPopupMenu menu = new JPopupMenu(); for (SpecialFieldValue val : field.getValues()) { menu.add(val.getMenuAction(panel.frame())); } menu.show(table, e.getX(), e.getY()); } return; } 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]; //If this is a file link field with specified file types, //we should also pass the types. String[] fileTypes = {}; if ((hasField == 0) && iconType[hasField].equals(Globals.FILE_FIELD) && (iconType.length > 1)) { fileTypes = iconType; } final List<String> listOfFileTypes = Collections.unmodifiableList(Arrays.asList(fileTypes)); // Open it now. We do this in a thread, so the program won't freeze during the wait. JabRefExecutorService.INSTANCE.execute(new Runnable() { @Override public void run() { panel.output(Localization.lang("External viewer called") + '.'); Object link = entry.getField(fieldName); if (link == null) { LOGGER.info("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(Globals.FILE_FIELD)) { // We use a FileListTableModel to parse the field content: FileListTableModel fileList = new FileListTableModel(); fileList.setContent((String) link); FileListEntry flEntry = null; // If there are one or more links of the correct type, // open the first one: if (!listOfFileTypes.isEmpty()) { for (int i = 0; i < fileList.getRowCount(); i++) { flEntry = fileList.getEntry(i); boolean correctType = false; for (String listOfFileType : listOfFileTypes) { if (flEntry.getType().toString().equals(listOfFileType)) { correctType = true; } } if (correctType) { break; } flEntry = null; } } //If there are no file types specified, consider all files. else if (fileList.getRowCount() > 0) { flEntry = fileList.getEntry(0); } if (flEntry != null) { // 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(Localization.lang("Unable to open link.")); } } } else { try { JabRefDesktop.openExternalViewer(panel.metaData(), (String) link, fieldName); } catch (IOException ex) { panel.output(Localization.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(Localization.lang("Unable to open link.")); } */ //Util.openExternalViewer(panel.metaData(), (String)link, fieldName); } //catch (IOException ex) { // panel.output(Globals.lang("Error") + ": " + ex.getMessage()); //} } }); } } /** * 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. */ private 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 occurring 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 occurred. * @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(); boolean showDefaultPopup = true; // 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(Globals.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); //If file types are specified, ignore files of other types. if (iconType.length > 1) { boolean correctType = false; for (int j = 1; j < iconType.length; j++) { if (flEntry.getType().toString().equals(iconType[j])) { correctType = true; } } if (!correctType) { continue; } } String description = flEntry.getDescription(); if ((description == null) || (description.trim().isEmpty())) { description = flEntry.getLink(); } menu.add(new ExternalFileMenuItem(panel.frame(), entry, description, flEntry.getLink(), flEntry.getType().getIcon(), panel.metaData(), flEntry.getType())); showDefaultPopup = false; } } else { SpecialField field = SpecialFieldsUtils.getSpecialFieldInstanceFromFieldName(iconType[0]); if (field != null) { // for (SpecialFieldValue val: field.getValues()) { // menu.add(val.getMenuAction(panel.frame())); // } // full pop should be shown as left click already shows short popup showDefaultPopup = true; } else { for (String anIconType : iconType) { Object o = entry.getField(anIconType); if (o != null) { menu.add(new ExternalFileMenuItem(panel.frame(), entry, (String) o, (String) o, GUIGlobals.getTableIcon(anIconType).getIcon(), panel.metaData(), anIconType)); showDefaultPopup = false; } } } } if (showDefaultPopup) { processPopupTrigger(e, row); } else { 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); } @Override public void mouseEntered(MouseEvent e) { // Do nothing } @Override public void mouseExited(MouseEvent e) { // Do nothing } public void setPreviewActive(boolean enabled) { previewActive = enabled; if (!previewActive) { panel.hideBottomComponent(); } else { if (!table.getSelected().isEmpty()) { updatePreview(table.getSelected().get(0), false); } } } public void switchPreview() { if (activePreview < (previewPanel.length - 1)) { activePreview++; } else { activePreview = 0; } Globals.prefs.putInt(JabRefPreferences.ACTIVE_PREVIEW, activePreview); if (previewActive) { this.preview = previewPanel[activePreview]; if (!table.getSelected().isEmpty()) { 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 */ @Override 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(); long QUICK_JUMP_TIMEOUT = 2000; 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; lastPressedCount++; } 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; } } @Override public void keyReleased(KeyEvent e) { // Do nothing } @Override public void keyPressed(KeyEvent e) { // Do nothing } @Override public void focusGained(FocusEvent e) { // Do nothing } @Override public void focusLost(FocusEvent e) { lastPressedCount = 0; // Reset quick jump when focus is lost. } }