org.netbeans.modules.quicksearch.ResultsModel Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.netbeans.modules.quicksearch;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.CharConversionException;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import javax.swing.AbstractListModel;
import javax.swing.KeyStroke;
import javax.swing.Timer;
import org.netbeans.spi.quicksearch.SearchRequest;
import org.openide.xml.XMLUtil;
/**
* Model of search results. Works as ListModel for JList which is displaying
* results. Actual results data are stored in List of CategoryResult objects.
*
* As model changes can come very frequently, firing of changes is coalesced.
* Coalescing of changes helps UI to reduce flicker and unnecessary updates.
*
* @author Jan Becicka
*/
public final class ResultsModel extends AbstractListModel implements ActionListener {
private static ResultsModel instance;
private List extends CategoryResult> results;
/* Timer for coalescing fast coming changes of model */
private Timer fireTimer;
/** Amount of time during which model has to be unchanged in order to fire
* changes to listeners. */
static final int COALESCE_TIME = 200;
/** Singleton */
private ResultsModel () {
}
public static ResultsModel getInstance () {
if (instance == null) {
instance = new ResultsModel();
}
return instance;
}
public void setContent (List extends CategoryResult> categories) {
List extends CategoryResult> oldRes = this.results;
this.results = categories;
if (oldRes != null) {
for (CategoryResult cr : oldRes) {
cr.setObsolete(true);
}
}
maybeFireChanges();
}
public List extends CategoryResult> getContent () {
return results;
}
/******* AbstractListModel impl ********/
public int getSize() {
if (results == null) {
return 0;
}
int size = 0;
for (CategoryResult cr : results) {
size += cr.getItems().size();
}
return size;
}
public Object getElementAt (int index) {
if (results == null) {
return null;
}
// TBD - should probably throw AIOOBE if invalid index is on input
int catIndex = index;
int catSize = 0;
List catItems = null;
for (CategoryResult cr : results) {
catItems = cr.getItems();
catSize = catItems.size();
if (catIndex < catSize) {
return catIndex >= 0 ? catItems.get(catIndex) : null;
}
catIndex -= catSize;
}
return null;
}
public static final class ItemResult {
private static final String HTML = "";
private CategoryResult category;
private Runnable action;
private String displayName;
private List extends KeyStroke> shortcut;
private String displayHint;
private Date date; //time of last access, used for recent searches
public ItemResult (CategoryResult category, SearchRequest sRequest,
Runnable action, String displayName) {
this(category, sRequest, action, displayName, null, null);
}
public ItemResult (CategoryResult category, Runnable action, String displayName, Date date) {
this(category, null, action, displayName, null, null);
this.date = date;
}
public ItemResult (CategoryResult category, SearchRequest sRequest,
Runnable action, String displayName, List extends KeyStroke> shortcut,
String displayHint) {
this.category = category;
this.action = action;
this.displayName = sRequest != null ?
highlightSubstring(displayName, sRequest) : displayName;
this.shortcut = shortcut;
this.displayHint = displayHint;
}
public Runnable getAction() {
return action;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public String getDisplayName () {
return displayName;
}
public String getDisplayHint() {
return displayHint;
}
public List extends KeyStroke> getShortcut() {
return shortcut;
}
public CategoryResult getCategory() {
return category;
}
private String highlightSubstring (String text, SearchRequest sRequest) {
if (text.startsWith(HTML)) {
// provider handles highliting itself, okay
return text;
}
// try to find substring
String searchedText = sRequest.getText();
int index = text.toLowerCase(Locale.ENGLISH).indexOf(searchedText.toLowerCase(Locale.ENGLISH));
if (index == -1) {
return HTML + safeEscape(text);
}
// found, bold it
int endIndex = index + searchedText.length();
StringBuilder sb = new StringBuilder(HTML);
if (index > 0) {
sb.append(safeEscape(text.substring(0, index)));
}
sb.append("");
sb.append(safeEscape(text.substring(index, endIndex)));
sb.append("");
if (endIndex < text.length()) {
sb.append(safeEscape(text.substring(endIndex, text.length())));
}
return sb.toString();
}
// XXX need API in XMLUtil
private static String safeEscape(String raw) {
try {
return XMLUtil.toElementContent(raw);
} catch (CharConversionException x) {
return raw;
}
}
}
void categoryChanged (CategoryResult cr) {
// fire change only if category is contained in model
if (results != null && results.contains(cr)) {
maybeFireChanges();
}
}
private void maybeFireChanges () {
if (fireTimer == null) {
fireTimer = new Timer(COALESCE_TIME, this);
}
if (!fireTimer.isRunning()) {
// first change in possible flurry, start timer
fireTimer.start();
} else {
// model change came too fast, let's wait until providers calm down :)
fireTimer.restart();
}
}
public void actionPerformed(ActionEvent e) {
fireTimer.stop();
fireContentsChanged(this, 0, getSize());
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy