/************************************************************************** OmegaT - Computer Assisted Translation (CAT) tool with fuzzy matching, translation memory, keyword search, glossaries, and translation leveraging into updated projects. Copyright (C) 2000-2006 Keith Godfrey and Maxym Mykhalchuk 2006-2007 Henry Pijffers 2010 Alex Buloichik, Didier Briel 2014 Piotr Kulik 2015 Yu Tang Home page: http://www.omegat.org/ Support center: http://groups.yahoo.com/group/OmegaT/ This file is part of OmegaT. OmegaT 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 3 of the License, or (at your option) any later version. OmegaT 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, see <http://www.gnu.org/licenses/>. **************************************************************************/ package org.omegat.gui.search; import java.awt.Color; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.swing.AbstractAction; import javax.swing.ActionMap; import javax.swing.InputMap; import javax.swing.JFrame; import javax.swing.JTextPane; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.event.CaretEvent; import javax.swing.event.CaretListener; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultStyledDocument; import javax.swing.text.Document; import javax.swing.text.MutableAttributeSet; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; import javax.swing.text.StyledDocument; import org.omegat.core.Core; import org.omegat.core.data.SourceTextEntry; import org.omegat.core.search.SearchMatch; import org.omegat.core.search.SearchResultEntry; import org.omegat.core.search.Searcher; import org.omegat.gui.editor.IEditor; import org.omegat.gui.editor.IEditor.CaretPosition; import org.omegat.gui.editor.IEditorFilter; import org.omegat.gui.shortcuts.PropertiesShortcuts; import org.omegat.util.Log; import org.omegat.util.OStrings; import org.omegat.util.Preferences; import org.omegat.util.StringUtil; import org.omegat.util.gui.FontFallbackListener; import org.omegat.util.gui.StaticUIUtils; import org.omegat.util.gui.Styles; import org.omegat.util.gui.UIThreadsUtil; /** * EntryListPane displays translation segments and, upon doubleclick of a * segment, instructs the main UI to jump to that segment this replaces the * previous huperlink interface and is much more flexible in the fonts it * displays than the HTML text * * @author Keith Godfrey * @author Henry Pijffers (henry.pijffers@saxnot.com) * @author Alex Buloichik (alex73mail@gmail.com) * @author Didier Briel */ @SuppressWarnings("serial") class EntryListPane extends JTextPane { protected static final AttributeSet FOUND_MARK = Styles.createAttributeSet(Color.BLUE, null, true, null); protected static final int MARKS_PER_REQUEST = 100; protected static final String ENTRY_SEPARATOR = "---------\n"; private static final String KEY_GO_TO_NEXT_SEGMENT = "gotoNextSegmentMenuItem"; private static final String KEY_GO_TO_PREVIOUS_SEGMENT = "gotoPreviousSegmentMenuItem"; private static final String KEY_TRANSFER_FOCUS = "transferFocus"; private static final String KEY_TRANSFER_FOCUS_BACKWARD = "transferFocusBackward"; private static final String KEY_JUMP_TO_ENTRY_IN_EDITOR = "jumpToEntryInEditor"; private static final int ENTRY_LIST_INDEX_NO_ENTRIES = -1; private static final int ENTRY_LIST_INDEX_END_OF_TEXT = -2; private static void bindKeyStrokesFromMainMenuShortcuts(InputMap map) { // Add KeyStrokes Ctrl+N/P (Cmd+N/P for MacOS) to the map PropertiesShortcuts.getMainMenuShortcuts().bindKeyStrokes(map, KEY_GO_TO_NEXT_SEGMENT, KEY_GO_TO_PREVIOUS_SEGMENT); } private static InputMap createDefaultInputMap(InputMap parent) { InputMap map = new InputMap(); map.setParent(parent); bindKeyStrokesFromMainMenuShortcuts(map); // Add KeyStrokes: Enter, Ctrl+Enter (Cmd+Enter for MacOS) int CTRL_CMD_MASK = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); map.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), KEY_GO_TO_NEXT_SEGMENT); map.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, CTRL_CMD_MASK), KEY_GO_TO_PREVIOUS_SEGMENT); return map; } private static InputMap createDefaultInputMapUseTab(InputMap parent) { InputMap map = new InputMap(); map.setParent(parent); bindKeyStrokesFromMainMenuShortcuts(map); // Add KeyStrokes: Tab, Shift+Tab, Ctrl+Tab, Ctrl+Shift+Tab // (Cmd+Tab is used by the system on OS X) map.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), KEY_GO_TO_NEXT_SEGMENT); map.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.SHIFT_DOWN_MASK), KEY_GO_TO_PREVIOUS_SEGMENT); map.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.CTRL_DOWN_MASK), KEY_TRANSFER_FOCUS); map.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK), KEY_TRANSFER_FOCUS_BACKWARD); // Enter to jump to selected segment in editor map.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), KEY_JUMP_TO_ENTRY_IN_EDITOR); return map; } public EntryListPane() { setDocument(new DefaultStyledDocument()); setDragEnabled(true); setFont(Core.getMainWindow().getApplicationFont()); StaticUIUtils.makeCaretAlwaysVisible(this); StaticUIUtils.setCaretUpdateEnabled(this, false); addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { if (!autoSyncWithEditor && !m_entryList.isEmpty()) { getActiveDisplayedEntry().gotoEntryInEditor(); } JFrame frame = Core.getMainWindow().getApplicationFrame(); frame.setState(JFrame.NORMAL); frame.toFront(); } } }); addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent e) { boolean useTabForAdvance = Core.getEditor().getSettings().isUseTabForAdvance(); if (EntryListPane.this.useTabForAdvance != useTabForAdvance) { EntryListPane.this.useTabForAdvance = useTabForAdvance; initInputMap(useTabForAdvance); } } }); addCaretListener(new CaretListener() { @Override public void caretUpdate(CaretEvent e) { SwingUtilities.invokeLater(highlighter); if (autoSyncWithEditor) { getActiveDisplayedEntry().gotoEntryInEditor(); } } }); setDocument(new DefaultStyledDocument()); getDocument().addDocumentListener(new FontFallbackListener(EntryListPane.this)); initActions(); useTabForAdvance = Core.getEditor().getSettings().isUseTabForAdvance(); autoSyncWithEditor = Preferences.isPreferenceDefault(Preferences.SEARCHWINDOW_AUTO_SYNC, false); initInputMap(useTabForAdvance); setEditable(false); } private void initInputMap(boolean useTabForAdvance) { setFocusTraversalKeysEnabled(!useTabForAdvance); InputMap parent = getInputMap().getParent(); InputMap newMap = useTabForAdvance ? createDefaultInputMapUseTab(parent) : createDefaultInputMap(parent); setInputMap(WHEN_FOCUSED, newMap); } void setAutoSyncWithEditor(boolean autoSyncWithEditor) { this.autoSyncWithEditor = autoSyncWithEditor; if (autoSyncWithEditor) { getActiveDisplayedEntry().gotoEntryInEditor(); } } /** * Show search result for user */ public void displaySearchResult(Searcher searcher, int numberOfResults) { UIThreadsUtil.mustBeSwingThread(); m_searcher = searcher; this.numberOfResults = numberOfResults; currentlyDisplayedMatches = null; m_entryList.clear(); m_offsetList.clear(); m_firstMatchList.clear(); if (searcher == null || searcher.getSearchResults() == null) { // empty marks - just reset setText(""); return; } currentlyDisplayedMatches = new DisplayMatches(searcher.getSearchResults()); highlighter.reset(); SwingUtilities.invokeLater(highlighter); if (autoSyncWithEditor) { getActiveDisplayedEntry().gotoEntryInEditor(); } } private int getActiveEntryListIndex() { int nrEntries = getNrEntries(); if (nrEntries == 0) { // No entry return ENTRY_LIST_INDEX_NO_ENTRIES; } if (nrEntries > 0) { int pos = getSelectionStart(); for (int i = 0; i < nrEntries; i++) { if (pos < m_offsetList.get(i)) { return i; } } } return ENTRY_LIST_INDEX_END_OF_TEXT; } protected class DisplayMatches { private final List<SearchMatch> matches = new ArrayList<SearchMatch>(); public DisplayMatches(final List<SearchResultEntry> entries) { UIThreadsUtil.mustBeSwingThread(); StringBuilder m_stringBuf = new StringBuilder(); // display what's been found so far if (entries.isEmpty()) { // no match addMessage(m_stringBuf, OStrings.getString("ST_NOTHING_FOUND")); } if (entries.size() >= numberOfResults) { addMessage(m_stringBuf, StringUtil.format(OStrings.getString("SW_MAX_FINDS_REACHED"), numberOfResults)); } for (SearchResultEntry e : entries) { addEntry(m_stringBuf, e.getEntryNum(), e.getPreamble(), e.getSrcPrefix(), e.getSrcText(), e.getTranslation(), e.getNote(), e.getSrcMatch(), e.getTargetMatch(), e.getNoteMatch()); } Document doc = getDocument(); try { doc.remove(0, doc.getLength()); doc.insertString(0, m_stringBuf.toString(), null); } catch (Exception ex) { Log.log(ex); } if (!matches.isEmpty()) { SwingUtilities.invokeLater(this::doMarks); } } // add entry text - remember what its number is and where it ends public void addEntry(StringBuilder m_stringBuf, int num, String preamble, String srcPrefix, String src, String loc, String note, SearchMatch[] srcMatches, SearchMatch[] targetMatches, SearchMatch[] noteMatches) { if (m_stringBuf.length() > 0) m_stringBuf.append(ENTRY_SEPARATOR); if (preamble != null && !preamble.equals("")) m_stringBuf.append(preamble + "\n"); if (src != null && !src.equals("")) { m_stringBuf.append("-- "); if (srcPrefix != null) { m_stringBuf.append(srcPrefix); } if (srcMatches != null) { for (SearchMatch m : srcMatches) { m.move(m_stringBuf.length()); matches.add(m); } } m_stringBuf.append(src); m_stringBuf.append('\n'); } if (loc != null && !loc.equals("")) { m_stringBuf.append("-- "); if (targetMatches != null && targetMatches.length > 0) { // Save first match position to select it in Editor pane later if (num > 0) { SearchMatch m = targetMatches[0]; m_firstMatchList.put(num, new CaretPosition(m.getStart(), m.getEnd())); } for (SearchMatch m : targetMatches) { m.move(m_stringBuf.length()); matches.add(m); } } m_stringBuf.append(loc); m_stringBuf.append('\n'); } if (note != null && !note.equals("")) { m_stringBuf.append("= "); if (noteMatches != null) { for (SearchMatch m : noteMatches) { m.move(m_stringBuf.length()); matches.add(m); } } m_stringBuf.append(note); m_stringBuf.append('\n'); } m_entryList.add(num); m_offsetList.add(m_stringBuf.length()); } public void doMarks() { UIThreadsUtil.mustBeSwingThread(); if (currentlyDisplayedMatches != this) { // results changed - shouldn't mark old results return; } StyledDocument doc = (StyledDocument) getDocument(); List<SearchMatch> display = matches.subList(0, Math.min(MARKS_PER_REQUEST, matches.size())); for (SearchMatch m : display) { doc.setCharacterAttributes(m.getStart(), m.getLength(), FOUND_MARK, true); } display.clear(); if (!matches.isEmpty()) { SwingUtilities.invokeLater(this::doMarks); } } } /** * Adds a message text to be displayed. Used for displaying messages that * aren't results. * * @param message * The message to display */ private void addMessage(StringBuilder m_stringBuf, String message) { // Insert entry/message separator if necessary if (m_stringBuf.length() > 0) m_stringBuf.append(ENTRY_SEPARATOR); // Insert the message text m_stringBuf.append(message); } public void reset() { displaySearchResult(null, 0); } public int getNrEntries() { return m_entryList.size(); } public List<Integer> getEntryList() { return m_entryList; } public Searcher getSearcher() { return m_searcher; } private void initActions() { ActionMap actionMap = getActionMap(); // go to next segment actionMap.put(KEY_GO_TO_NEXT_SEGMENT, new AbstractAction() { @Override public void actionPerformed(ActionEvent arg0) { getActiveDisplayedEntry().getNext().activate(); } }); // go to previous segment actionMap.put(KEY_GO_TO_PREVIOUS_SEGMENT, new AbstractAction() { @Override public void actionPerformed(ActionEvent arg0) { getActiveDisplayedEntry().getPrevious().activate(); } }); // transfer focus to next component actionMap.put(KEY_TRANSFER_FOCUS, new AbstractAction() { @Override public void actionPerformed(ActionEvent arg0) { transferFocus(); } }); // transfer focus to previous component actionMap.put(KEY_TRANSFER_FOCUS_BACKWARD, new AbstractAction() { @Override public void actionPerformed(ActionEvent arg0) { transferFocusBackward(); } }); actionMap.put(KEY_JUMP_TO_ENTRY_IN_EDITOR, new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { if (!autoSyncWithEditor && !m_entryList.isEmpty()) { getActiveDisplayedEntry().gotoEntryInEditor(); } } }); } private DisplayedEntry getActiveDisplayedEntry() { int activeEntryListIndex = getActiveEntryListIndex(); switch (activeEntryListIndex) { case ENTRY_LIST_INDEX_NO_ENTRIES: return new EmptyDisplayedEntry(); case ENTRY_LIST_INDEX_END_OF_TEXT: // end of text (out of entries range) return new DisplayedEntryImpl(getNrEntries()); default: return new DisplayedEntryImpl(activeEntryListIndex); } } private interface DisplayedEntry { DisplayedEntry getNext(); DisplayedEntry getPrevious(); void activate(); void gotoEntryInEditor(); } private static class EmptyDisplayedEntry implements DisplayedEntry { @Override public DisplayedEntry getNext() { return this; } @Override public DisplayedEntry getPrevious() { return this; } @Override public void activate() { // Do nothing } @Override public void gotoEntryInEditor() { // Do nothing } } private class DisplayedEntryImpl implements DisplayedEntry { private final int index; private DisplayedEntryImpl(int index) { this.index = index; } @Override public DisplayedEntry getNext() { if (index >= (getNrEntries() - 1)) { return this; } else { return new DisplayedEntryImpl(index + 1); } } @Override public DisplayedEntry getPrevious() { if (index == 0) { return this; } else { return new DisplayedEntryImpl(index - 1); } } @Override public void activate() { if (index >= getNrEntries()) { // end of text (out of entries range) return; } int beginPos = 0; if (index != 0) { beginPos = m_offsetList.get(index - 1) + ENTRY_SEPARATOR.length(); int endPos = m_offsetList.get(index); try { Rectangle endRect = modelToView(endPos); scrollRectToVisible(endRect); } catch (BadLocationException ex) { // Eat exception silently } } setSelectionStart(beginPos); setSelectionEnd(beginPos); } @Override public void gotoEntryInEditor() { if (index >= getNrEntries()) { // end of text (out of entries range) return; } final int entry = m_entryList.get(index); if (entry > 0) { final IEditor editor = Core.getEditor(); int currEntryInEditor = editor.getCurrentEntryNumber(); if (currEntryInEditor != 0 && entry != currEntryInEditor) { final boolean isSegDisplayed = isSegmentDisplayed(entry); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (isSegDisplayed && m_firstMatchList.containsKey(entry)) { // Select search word in Editor pane CaretPosition pos = m_firstMatchList.get(entry); editor.gotoEntry(entry, pos); } else { editor.gotoEntry(entry); } } }); } } } private boolean isSegmentDisplayed(int entry) { IEditorFilter filter = Core.getEditor().getFilter(); if (filter == null) { return true; } else { SourceTextEntry ste = Core.getProject().getAllEntries().get(entry - 1); return filter.allowed(ste); } } } private class SegmentHighlighter implements Runnable { private final AttributeSet attrNormal; private final AttributeSet attrActive; private int entryListIndex = -1; private int offset = -1; private int length = -1; public SegmentHighlighter() { MutableAttributeSet attrNormal = new SimpleAttributeSet(); StyleConstants.setBackground(attrNormal, getBackground()); this.attrNormal = attrNormal; MutableAttributeSet attrActive = new SimpleAttributeSet(); // This is the same as the default value for // Styles.EditorColor.COLOR_ACTIVE_SOURCE, but we hard-code it here // because this panel does not currently support customized colors. StyleConstants.setBackground(attrActive, Color.decode("#c0ffc0")); this.attrActive = attrActive; } @Override public void run() { int activeEntryListIndex = getActiveEntryListIndex(); if (activeEntryListIndex == ENTRY_LIST_INDEX_END_OF_TEXT) { // end of text (out of entries range) should belongs to the last segment activeEntryListIndex = getNrEntries() - 1; } if (activeEntryListIndex != entryListIndex) { removeCurrentHighlight(); addHighlight(activeEntryListIndex); } } public void reset() { entryListIndex = -1; offset = -1; length = -1; } private void removeCurrentHighlight() { if (entryListIndex == -1 || entryListIndex >= m_offsetList.size() || length <= 0) { return; } getStyledDocument().setCharacterAttributes(offset, length, attrNormal, false); reset(); } private void addHighlight(int entryListIndex) { if (entryListIndex == -1 || entryListIndex >= m_offsetList.size()) { return; } int offset = entryListIndex == 0 ? 0 : m_offsetList.get(entryListIndex - 1) + ENTRY_SEPARATOR.length(); int length = m_offsetList.get(entryListIndex) - offset - 1; // except tail line break getStyledDocument().setCharacterAttributes(offset, length, attrActive, false); this.entryListIndex = entryListIndex; this.offset = offset; this.length = length; } } private volatile Searcher m_searcher; private final List<Integer> m_entryList = new ArrayList<Integer>(); private final List<Integer> m_offsetList = new ArrayList<Integer>(); private final Map<Integer, CaretPosition> m_firstMatchList = new HashMap<Integer, CaretPosition>(); private DisplayMatches currentlyDisplayedMatches; private int numberOfResults; private boolean useTabForAdvance; private boolean autoSyncWithEditor; private final SegmentHighlighter highlighter = new SegmentHighlighter(); }