001/*
002 * $Id: TableSearchable.java 3194 2009-01-21 11:39:19Z kleopatra $
003 *
004 * Copyright 2007 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 * 
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015 * Lesser General Public License for more details.
016 * 
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020 */
021package org.jdesktop.swingx.search;
022
023import java.awt.Rectangle;
024import java.util.regex.Matcher;
025import java.util.regex.Pattern;
026
027import org.jdesktop.swingx.JXTable;
028import org.jdesktop.swingx.decorator.AbstractHighlighter;
029import org.jdesktop.swingx.decorator.Highlighter;
030
031/**
032 * An Searchable implementation for use in JXTable.
033 * 
034 * @author Jeanette Winzenburg
035 */
036public class TableSearchable extends AbstractSearchable {
037
038    /** The target JXTable. */
039    protected JXTable table;
040
041    /**
042     * Instantiates a TableSearchable with the given table as target.
043     * 
044     * @param table the JXTable to search.
045     */
046    public TableSearchable(JXTable table) {
047        this.table = table;
048    }
049
050    /**
051     * {@inheritDoc}
052     * <p>
053     * 
054     * This implementation loops through the cells in a row to find a match.
055     */
056    @Override
057    protected void findMatchAndUpdateState(Pattern pattern, int startRow,
058            boolean backwards) {
059        SearchResult matchRow = null;
060        if (backwards) {
061            // CHECK: off-one end still needed?
062            // Probably not - the findXX don't have side-effects any longer
063            // hmmm... still needed: even without side-effects we need to
064            // guarantee calling the notfound update at the very end of the
065            // loop.
066            for (int r = startRow; r >= -1 && matchRow == null; r--) {
067                matchRow = findMatchBackwardsInRow(pattern, r);
068                updateState(matchRow);
069            }
070        } else {
071            for (int r = startRow; r <= getSize() && matchRow == null; r++) {
072                matchRow = findMatchForwardInRow(pattern, r);
073                updateState(matchRow);
074            }
075        }
076        // KEEP - JW: Needed to update if loop wasn't entered!
077        // the alternative is to go one off in the loop. Hmm - which is
078        // preferable?
079        // updateState(matchRow);
080
081    }
082
083    /**
084     * {@inheritDoc}
085     * <p>
086     * 
087     * Implemented to search for an extension in the cell given by row and
088     * foundColumn.
089     */
090    @Override
091    protected SearchResult findExtendedMatch(Pattern pattern, int row) {
092        return findMatchAt(pattern, row, lastSearchResult.foundColumn);
093    }
094
095    /**
096     * Searches forward through columns of the given row. Starts at
097     * lastFoundColumn or first column if lastFoundColumn < 0. returns an
098     * appropriate SearchResult if a matching cell is found in this row or null
099     * if no match is found. A row index out off range results in a no-match.
100     * 
101     * @param pattern <code>Pattern</code> that we will try to locate
102     * @param row the row to search
103     * @return an appropriate <code>SearchResult</code> if a matching cell is
104     *         found in this row or null if no match is found
105     */
106    private SearchResult findMatchForwardInRow(Pattern pattern, int row) {
107        int startColumn = (lastSearchResult.foundColumn < 0) ? 0
108                : lastSearchResult.foundColumn;
109        if (isValidIndex(row)) {
110            for (int column = startColumn; column < table.getColumnCount(); column++) {
111                SearchResult result = findMatchAt(pattern, row, column);
112                if (result != null)
113                    return result;
114            }
115        }
116        return null;
117    }
118
119    /**
120     * Searches forward through columns of the given row. Starts at
121     * lastFoundColumn or first column if lastFoundColumn < 0. returns an
122     * appropriate SearchResult if a matching cell is found in this row or null
123     * if no match is found. A row index out off range results in a no-match.
124     * 
125     * @param pattern <code>Pattern</code> that we will try to locate
126     * @param row the row to search
127     * @return an appropriate <code>SearchResult</code> if a matching cell is
128     *         found in this row or null if no match is found
129     */
130    private SearchResult findMatchBackwardsInRow(Pattern pattern, int row) {
131        int startColumn = (lastSearchResult.foundColumn < 0) ? table
132                .getColumnCount() - 1 : lastSearchResult.foundColumn;
133        if (isValidIndex(row)) {
134            for (int column = startColumn; column >= 0; column--) {
135                SearchResult result = findMatchAt(pattern, row, column);
136                if (result != null)
137                    return result;
138            }
139        }
140        return null;
141    }
142
143    /**
144     * Matches the cell content at row/col against the given Pattern. Returns an
145     * appropriate SearchResult if matching or null if no matching
146     * 
147     * @param pattern <code>Pattern</code> that we will try to locate
148     * @param row a valid row index in view coordinates
149     * @param column a valid column index in view coordinates
150     * @return an appropriate <code>SearchResult</code> if matching or null
151     */
152    protected SearchResult findMatchAt(Pattern pattern, int row, int column) {
153        String text = table.getStringAt(row, column);
154        if ((text != null) && (text.length() > 0)) {
155            Matcher matcher = pattern.matcher(text);
156            if (matcher.find()) {
157                return createSearchResult(matcher, row, column);
158            }
159        }
160        return null;
161    }
162
163    /**
164     * 
165     * {@inheritDoc}
166     * <p>
167     * 
168     * Overridden to adjust the column index to -1.
169     */
170    @Override
171    protected int adjustStartPosition(int startIndex, boolean backwards) {
172        lastSearchResult.foundColumn = -1;
173        return super.adjustStartPosition(startIndex, backwards);
174    }
175
176    /**
177     * {@inheritDoc}
178     * <p>
179     * 
180     * Overridden to loop through all columns in a row.
181     */
182    @Override
183    protected int moveStartPosition(int startRow, boolean backwards) {
184        if (backwards) {
185            lastSearchResult.foundColumn--;
186            if (lastSearchResult.foundColumn < 0) {
187                startRow--;
188            }
189        } else {
190            lastSearchResult.foundColumn++;
191            if (lastSearchResult.foundColumn >= table.getColumnCount()) {
192                lastSearchResult.foundColumn = -1;
193                startRow++;
194            }
195        }
196        return startRow;
197    }
198
199    /**
200     * {@inheritDoc}
201     * <p>
202     * 
203     * Overridden to check the column index of last find.
204     */
205    @Override
206    protected boolean isEqualStartIndex(final int startIndex) {
207        return super.isEqualStartIndex(startIndex)
208                && isValidColumn(lastSearchResult.foundColumn);
209    }
210
211    /**
212     * Checks if row is in range: 0 <= row < getRowCount().
213     * 
214     * @param column the column index to check in view coordinates.
215     * @return true if the column is in range, false otherwise
216     */
217    private boolean isValidColumn(int column) {
218        return column >= 0 && column < table.getColumnCount();
219    }
220
221    /**
222     * {@inheritDoc}
223     */
224    @Override
225    protected int getSize() {
226        return table.getRowCount();
227    }
228
229    /**
230     * {@inheritDoc}
231     */
232    @Override
233    public JXTable getTarget() {
234        return table;
235    }
236
237    /**
238     * Configures the match highlighter to the current match. Ensures that the
239     * matched cell is visible, if there is a match.
240     * 
241     * PRE: markByHighlighter
242     * 
243     */
244    protected void moveMatchByHighlighter() {
245        AbstractHighlighter searchHL = getConfiguredMatchHighlighter();
246        // no match
247        if (!hasMatch()) {
248            return;
249        } else {
250            ensureInsertedSearchHighlighters(searchHL);
251            table.scrollCellToVisible(lastSearchResult.foundRow,
252                    lastSearchResult.foundColumn);
253        }
254    }
255
256    /**
257     * {@inheritDoc}
258     * <p>
259     * 
260     * Overridden to convert the column index in the table's view coordinate
261     * system to model coordinate.
262     * <p>
263     * 
264     * PENDING JW: this is only necessary because the SearchPredicate wants its
265     * highlight column in model coordinates. But code comments in the
266     * SearchPredicate seem to indicate that we probably want to revise that
267     * (legacy?).
268     */
269    @Override
270    protected int convertColumnIndexToModel(int viewColumn) {
271        return getTarget().convertColumnIndexToModel(viewColumn);
272    }
273
274    /**
275     * Moves the row selection to the matching cell and ensures its visibility,
276     * if any. Does nothing if there is no match.
277     * 
278     */
279    protected void moveMatchBySelection() {
280        if (!hasMatch()) {
281            return;
282        }
283        int row = lastSearchResult.foundRow;
284        int column = lastSearchResult.foundColumn;
285        table.changeSelection(row, column, false, false);
286        if (!table.getAutoscrolls()) {
287            // scrolling not handled by moving selection
288            Rectangle cellRect = table.getCellRect(row, column, true);
289            if (cellRect != null) {
290                table.scrollRectToVisible(cellRect);
291            }
292        }
293    }
294
295    /**
296     * {@inheritDoc}
297     * <p>
298     */
299    @Override
300    protected void moveMatchMarker() {
301        if (markByHighlighter()) {
302            moveMatchByHighlighter();
303        } else { // use selection
304            moveMatchBySelection();
305        }
306    }
307
308    /**
309     * {@inheritDoc}
310     * <p>
311     */
312    @Override
313    protected void removeHighlighter(Highlighter searchHighlighter) {
314        table.removeHighlighter(searchHighlighter);
315    }
316
317    /**
318     * {@inheritDoc}
319     * <p>
320     */
321    @Override
322    protected Highlighter[] getHighlighters() {
323        return table.getHighlighters();
324    }
325
326    /**
327     * {@inheritDoc}
328     * <p>
329     */
330    @Override
331    protected void addHighlighter(Highlighter highlighter) {
332        table.addHighlighter(highlighter);
333    }
334
335}