diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties index d55aaa4561..b3948ff00f 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties @@ -7,3 +7,10 @@ KeywordSearchTopComponent.filesIndexedNameLabel.text=Files indexed: KeywordSearchTopComponent.filesIndexedValLabel.text=- KeywordSearchTopComponent.filesIndexedNameLabel.AccessibleContext.accessibleName=Files indexed: KeywordSearchTopComponent.filesIndexedValLabel.AccessibleContext.accessibleName=- +ExtractedContentPanel.hitLabel.text=Hit: +ExtractedContentPanel.hitCountLabel.text=- +ExtractedContentPanel.hitOfLabel.text=of +ExtractedContentPanel.hitTotalLabel.text=- +ExtractedContentPanel.hitButtonsLabel.text=Hit +ExtractedContentPanel.hitPreviousButton.text= +ExtractedContentPanel.hitNextButton.text= diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentFind.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentFind.java new file mode 100644 index 0000000000..be7c31e151 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentFind.java @@ -0,0 +1,167 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sleuthkit.autopsy.keywordsearch; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.apache.commons.lang.StringEscapeUtils; + + +public class ExtractedContentFind { + + private static final Logger logger = Logger.getLogger(ExtractedContentFind.class.getName()); + + ExtractedContentFind(ExtractedContentViewer viewer) { + this.viewer = viewer; + findIndex = new HashMap>(); + curIndex = new HashMap(); + } + + private HashMap>findIndex; + private HashMapcurIndex; + private ExtractedContentViewer viewer; + + public static int INDEX_INITIALIZED = -1; + public static int INDEX_NOT_FOUND = -2; + + public int getCurrentIndexTotal(MarkupSource source) { + ArrayList index = indexSource(source); + return index.size(); + } + + public int getCurrentIndexI(MarkupSource source) { + indexSource(source); + Integer curI = curIndex.get(source); + if (curI != null) + return curI; + else return -1; + } + + /** + * get next line number corresponding to indexed match, no wrapping + * requires call to hasNext() first + * or INDEX_NOT_FOUND if no next hit + * @param source + * @return line number where match occurs + */ + public long getNext(MarkupSource source) { + ArrayList index = indexSource(source); + int total = index.size(); + int cur = curIndex.get(source); + if (total == 0 || cur == total -1) return INDEX_NOT_FOUND; + ++cur; + //update curIndex location + curIndex.put(source, cur); + return index.get(cur); + } + + /** + * + * @param source + * @return true if the source has next hit + */ + public boolean hasNext(MarkupSource source) { + ArrayList index = indexSource(source); + int total = index.size(); + int cur = curIndex.get(source); + if (total == 0) return false; + else if (cur == INDEX_INITIALIZED) return true; + else if (cur == total - 1) + return false; + return true; + } + + /** + * + * @param source + * @return true if the source has previous hit + */ + public boolean hasPrevious(MarkupSource source) { + ArrayList index = indexSource(source); + int total = index.size(); + int cur = curIndex.get(source); + if (total == 0) return false; + else if (cur == INDEX_INITIALIZED) return false; + else if (cur == 0) return false; + return true; + } + + /** + * get previous line number corresponding to indexed match, no wrapping + * requires call to hasPrevious() first + * or INDEX_NOT_FOUND if no previous hit + * @param source + * @return line number where match occurs + */ + public long getPrevious(MarkupSource source) { + ArrayList index = indexSource(source); + int total = index.size(); + int cur = curIndex.get(source); + if (total == 0 || cur == 0) return INDEX_NOT_FOUND; + --cur; + //update curIndex location + curIndex.put(source, cur); + return index.get(cur); + } + + /** + * Add MarkupSource to find functionality, or return if already exists for that source. + * @param source MarkupSource to add to find + */ + private ArrayList indexSource(MarkupSource source) { + //return if already indexed + ArrayList indexed = findIndex.get(source); + if (indexed != null || source.isSearchable() == false) + return indexed; + + indexed = new ArrayList(); + String markup = source.getMarkup(); + + //logger.log(Level.INFO,markup); + final String indexSearchTok = source.getSearchToken(); + if (indexSearchTok == null || indexSearchTok.equals("")) { + return indexed; + } + final int indexSearchTokLen = indexSearchTok.length(); + long docOffset = 0; + long index = -1; + + while ((index = markup.indexOf(indexSearchTok, (int)docOffset)) >= 0) { + //TODO check if (int) cast above presents limitation for large files + + //calculate and store index stripping all markup for scrolling to work properly + //need to map index to content with no html + //try cheat: compensata fot highlight tags (might be other things, such as escape chars) + //perfectly we'd scan both documents at same time and map index from one to another + indexed.add(index); + docOffset = index + indexSearchTokLen; //next offset past the keyword + } + //add indices to index collection + findIndex.put(source, indexed); + //add current for tracking + curIndex.put(source, INDEX_INITIALIZED); + + return indexed; + } + + +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentPanel.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentPanel.form index eb0c3f0639..71cdca10a6 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentPanel.form +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentPanel.form @@ -17,7 +17,21 @@ - + + + + + + + + + + + + + + + @@ -27,9 +41,20 @@ - + + + + + + + + + + + + - + @@ -62,5 +87,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentPanel.java index 744ba3e949..39b3b1f368 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentPanel.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentPanel.java @@ -18,10 +18,19 @@ */ package org.sleuthkit.autopsy.keywordsearch; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.JViewport; +import javax.swing.SwingUtilities; +import org.apache.commons.logging.Log; /** * Panel displays HTML content sent to ExtractedContentViewer, and provides @@ -29,9 +38,12 @@ import java.util.List; */ class ExtractedContentPanel extends javax.swing.JPanel { + private static Logger logger = Logger.getLogger(ExtractedContentPanel.class.getName()); + ExtractedContentPanel() { initComponents(); - + + initControls(); extractedTextPane.setContentType("text/html"); sourceComboBox.addItemListener(new ItemListener() { @@ -59,6 +71,13 @@ class ExtractedContentPanel extends javax.swing.JPanel { jScrollPane1 = new javax.swing.JScrollPane(); extractedTextPane = new javax.swing.JTextPane(); sourceComboBox = new javax.swing.JComboBox(); + hitLabel = new javax.swing.JLabel(); + hitCountLabel = new javax.swing.JLabel(); + hitOfLabel = new javax.swing.JLabel(); + hitTotalLabel = new javax.swing.JLabel(); + hitButtonsLabel = new javax.swing.JLabel(); + hitPreviousButton = new javax.swing.JButton(); + hitNextButton = new javax.swing.JButton(); extractedTextPane.setEditable(false); extractedTextPane.setAutoscrolls(false); @@ -66,12 +85,42 @@ class ExtractedContentPanel extends javax.swing.JPanel { sourceComboBox.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "Item 1", "Item 2", "Item 3", "Item 4" })); + hitLabel.setText(org.openide.util.NbBundle.getMessage(ExtractedContentPanel.class, "ExtractedContentPanel.hitLabel.text")); // NOI18N + + hitCountLabel.setText(org.openide.util.NbBundle.getMessage(ExtractedContentPanel.class, "ExtractedContentPanel.hitCountLabel.text")); // NOI18N + + hitOfLabel.setText(org.openide.util.NbBundle.getMessage(ExtractedContentPanel.class, "ExtractedContentPanel.hitOfLabel.text")); // NOI18N + + hitTotalLabel.setText(org.openide.util.NbBundle.getMessage(ExtractedContentPanel.class, "ExtractedContentPanel.hitTotalLabel.text")); // NOI18N + + hitButtonsLabel.setText(org.openide.util.NbBundle.getMessage(ExtractedContentPanel.class, "ExtractedContentPanel.hitButtonsLabel.text")); // NOI18N + + hitPreviousButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/keywordsearch/arrow_left.gif"))); // NOI18N + hitPreviousButton.setText(org.openide.util.NbBundle.getMessage(ExtractedContentPanel.class, "ExtractedContentPanel.hitPreviousButton.text")); // NOI18N + + hitNextButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/keywordsearch/arrow_right.gif"))); // NOI18N + hitNextButton.setText(org.openide.util.NbBundle.getMessage(ExtractedContentPanel.class, "ExtractedContentPanel.hitNextButton.text")); // NOI18N + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addContainerGap(334, Short.MAX_VALUE) + .addContainerGap() + .addComponent(hitLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(hitCountLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 12, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(hitOfLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(hitTotalLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 18, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(41, 41, 41) + .addComponent(hitButtonsLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(hitPreviousButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(hitNextButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 125, Short.MAX_VALUE) .addComponent(sourceComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addContainerGap()) .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 400, Short.MAX_VALUE) @@ -79,18 +128,33 @@ class ExtractedContentPanel extends javax.swing.JPanel { layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() - .addComponent(sourceComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(sourceComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(hitLabel) + .addComponent(hitButtonsLabel) + .addComponent(hitOfLabel) + .addComponent(hitTotalLabel) + .addComponent(hitCountLabel)) + .addComponent(hitPreviousButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(hitNextButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 274, Short.MAX_VALUE)) + .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 271, Short.MAX_VALUE)) ); }// //GEN-END:initComponents // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JTextPane extractedTextPane; + private javax.swing.JLabel hitButtonsLabel; + private javax.swing.JLabel hitCountLabel; + private javax.swing.JLabel hitLabel; + private javax.swing.JButton hitNextButton; + private javax.swing.JLabel hitOfLabel; + private javax.swing.JButton hitPreviousButton; + private javax.swing.JLabel hitTotalLabel; private javax.swing.JScrollPane jScrollPane1; private javax.swing.JComboBox sourceComboBox; // End of variables declaration//GEN-END:variables - /** * Set the available sources (selects the first source in the list by * default) @@ -109,8 +173,105 @@ class ExtractedContentPanel extends javax.swing.JPanel { } } + /** + * + * @return currently available sources on the panel + */ + public List getSources() { + ArrayList sources = new ArrayList(); + for (int i = 0; i < sourceComboBox.getItemCount(); ++i) { + sources.add((MarkupSource) sourceComboBox.getItemAt(i)); + } + return sources; + } + + /** + * + * @return currently selected Source + */ + public MarkupSource getSelectedSource() { + return (MarkupSource) sourceComboBox.getSelectedItem(); + } + private void setPanelText(String text) { extractedTextPane.setText(text); extractedTextPane.setCaretPosition(0); + logger.log(Level.INFO, extractedTextPane.getText()); + } + + private void initControls() { + hitPreviousButton.setEnabled(false); + hitNextButton.setEnabled(false); + } + + /** + * + * @param offset to scroll to + */ + public void scrollTo(int offset) { + //extractedTextPane.setCaretPosition(offset); + // + JViewport viewport = (JViewport) SwingUtilities.getAncestorOfClass(JViewport.class, extractedTextPane); + if (viewport == null) { + return; + } + int height = viewport.getExtentSize().height; + try { + Rectangle viewRectangle = extractedTextPane.modelToView(offset); + if (viewRectangle == null) { + return; + } + int y = viewRectangle.y - height / 2; + y = Math.max(0, y); + y = Math.min(y, extractedTextPane.getHeight() - height); + viewport.setViewPosition(new Point(0, y)); + } catch (javax.swing.text.BadLocationException ex) { + logger.log(Level.WARNING, "Failed scrolling to index " + offset); + } + + } + + /** + * + * @param current, current hit to update the display with + */ + public void updateCurrentDisplay(int current) { + hitCountLabel.setText(Integer.toString(current)); + } + + /** + * + * @param total total number of hits to update the display with + */ + public void updateTotalDisplay(int total) { + hitTotalLabel.setText(Integer.toString(total)); + } + + /** + * enable previous hit control + * @param enable whether to enable or disable + */ + public void enablePrevControl(boolean enable) { + hitPreviousButton.setEnabled(enable); + } + + /** + * enable previous hit control + * @param enable whether to enable or disable + */ + public void enableNextControl(boolean enable) { + hitNextButton.setEnabled(enable); + } + + public void addPrevControlListener(ActionListener l) { + hitPreviousButton.addActionListener(l); + } + + public void addNextControlListener(ActionListener l) { + hitNextButton.addActionListener(l); + } + + public void addSourceComboControlListener(ActionListener l) { + sourceComboBox.addActionListener(l); } } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentViewer.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentViewer.java index 6ec57b3da3..ca29a72c43 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentViewer.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentViewer.java @@ -19,6 +19,8 @@ package org.sleuthkit.autopsy.keywordsearch; import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -43,8 +45,10 @@ public class ExtractedContentViewer implements DataContentViewer { private static final Logger logger = Logger.getLogger(ExtractedContentViewer.class.getName()); private ExtractedContentPanel panel; + private ExtractedContentFind find; public ExtractedContentViewer() { + find = new ExtractedContentFind(this); } @Override @@ -64,7 +68,7 @@ public class ExtractedContentViewer implements DataContentViewer { if (solrHasContent(selectedNode)) { - + sources.add(new MarkupSource() { @Override @@ -82,8 +86,18 @@ public class ExtractedContentViewer implements DataContentViewer { public String toString() { return "Extracted Content"; } + + @Override + public boolean isSearchable() { + return false; + } + + @Override + public String getSearchToken() { + return ""; + } }); - + } // first source will be the default displayed @@ -104,6 +118,9 @@ public class ExtractedContentViewer implements DataContentViewer { public Component getComponent() { if (panel == null) { panel = new ExtractedContentPanel(); + panel.addPrevControlListener(new PrevFindActionListener()); + panel.addNextControlListener(new NextFindActionListener()); + panel.addSourceComboControlListener(new SourceChangeActionListener()); } return panel; } @@ -123,7 +140,7 @@ public class ExtractedContentViewer implements DataContentViewer { return !sources.isEmpty() || solrHasContent(node); } - + @Override public boolean isPreferred(ContentNode node) { return isSupported(node); @@ -177,4 +194,89 @@ public class ExtractedContentViewer implements DataContentViewer { String content = (String) solrCore.query(q).getResults().get(0).getFieldValue("content"); return content; } + + class NextFindActionListener implements ActionListener { + + @Override + public void actionPerformed(ActionEvent e) { + MarkupSource source = panel.getSelectedSource(); + if (find.hasNext(source)) { + long indexVal = find.getNext(source); + logger.log(Level.INFO, "INDEX NEXT: " + indexVal); + + //scroll + panel.scrollTo((int)indexVal); + + //update display + panel.updateCurrentDisplay(find.getCurrentIndexI(source) + 1); + panel.updateTotalDisplay(find.getCurrentIndexTotal(source)); + + //update controls if needed + if (!find.hasNext(source)) { + panel.enableNextControl(false); + } + if (find.hasPrevious(source)) { + panel.enablePrevControl(true); + } + } + } + } + + class PrevFindActionListener implements ActionListener { + + @Override + public void actionPerformed(ActionEvent e) { + MarkupSource source = panel.getSelectedSource(); + if (find.hasPrevious(source)) { + long indexVal = find.getPrevious(source); + logger.log(Level.INFO, "INDEX PREVIOUS: " + indexVal); + //scroll + panel.scrollTo((int)indexVal); + + //update display + panel.updateCurrentDisplay(find.getCurrentIndexI(source) + 1); + panel.updateTotalDisplay(find.getCurrentIndexTotal(source)); + + //update controls if needed + if (!find.hasPrevious(source)) { + panel.enablePrevControl(false); + } + if (find.hasNext(source)) { + panel.enableNextControl(true); + } + } + } + } + + class SourceChangeActionListener implements ActionListener { + + @Override + public void actionPerformed(ActionEvent e) { + MarkupSource source = panel.getSelectedSource(); + + //setup find buttons + + if (source != null && source.isSearchable()) { + panel.updateCurrentDisplay(find.getCurrentIndexI(source) + 1); + panel.updateTotalDisplay(find.getCurrentIndexTotal(source)); + + if (find.hasNext(source)) { + panel.enableNextControl(true); + } else { + panel.enableNextControl(false); + } + + if (find.hasPrevious(source)) { + panel.enablePrevControl(true); + } else { + panel.enablePrevControl(false); + } + } else { + panel.enableNextControl(false); + panel.enablePrevControl(false); + } + + + } + } } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HighlightedMatchesSource.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HighlightedMatchesSource.java index cbec781053..464a97e349 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HighlightedMatchesSource.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HighlightedMatchesSource.java @@ -33,6 +33,9 @@ import org.sleuthkit.datamodel.Content; class HighlightedMatchesSource implements MarkupSource { private static final Logger logger = Logger.getLogger(HighlightedMatchesSource.class.getName()); + private static final String HIGHLIGHT_PRE = ""; + private static final String HIGHLIGHT_POST = ""; + Content content; String solrQuery; Core solrCore; @@ -54,8 +57,8 @@ class HighlightedMatchesSource implements MarkupSource { q.setQuery(solrQuery); q.addFilterQuery("id:" + content.getId()); q.addHighlightField("content"); - q.setHighlightSimplePre(""); - q.setHighlightSimplePost(""); + q.setHighlightSimplePre(HIGHLIGHT_PRE); + q.setHighlightSimplePost(HIGHLIGHT_POST); q.setHighlightFragsize(0); // don't fragment the highlight try { @@ -76,4 +79,14 @@ class HighlightedMatchesSource implements MarkupSource { public String toString() { return "Search Matches"; } + + @Override + public boolean isSearchable() { + return true; + } + + @Override + public String getSearchToken() { + return HIGHLIGHT_PRE; + } } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/MarkupSource.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/MarkupSource.java index 21f16032e0..a7f6da782c 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/MarkupSource.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/MarkupSource.java @@ -29,6 +29,19 @@ public interface MarkupSource { * components can handle in their setText() method. */ String getMarkup(); + + /** + * + * @return true if markup is marked to be searchable + */ + boolean isSearchable(); + + /** + * If searchable markup, returns search token, otherwise return empty string + * TODO pull up into SearchableMarkupSource abstract class + * @return + */ + String getSearchToken(); /** * @return title of markup source diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/arrow_left.gif b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/arrow_left.gif new file mode 100644 index 0000000000..d0d85dba4b Binary files /dev/null and b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/arrow_left.gif differ diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/arrow_right.gif b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/arrow_right.gif new file mode 100644 index 0000000000..85272ad99c Binary files /dev/null and b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/arrow_right.gif differ