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     */
021    package org.jdesktop.swingx.search;
022    
023    import java.awt.Rectangle;
024    import java.util.regex.Matcher;
025    import java.util.regex.Pattern;
026    
027    import org.jdesktop.swingx.JXTable;
028    import org.jdesktop.swingx.decorator.AbstractHighlighter;
029    import org.jdesktop.swingx.decorator.Highlighter;
030    
031    /**
032     * An Searchable implementation for use in JXTable.
033     * 
034     * @author Jeanette Winzenburg
035     */
036    public 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    }