001/* 002 * Copyright 2010-2015 Institut Pasteur. 003 * 004 * This file is part of Icy. 005 * 006 * Icy is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * Icy is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU General Public License for more details. 015 * 016 * You should have received a copy of the GNU General Public License 017 * along with Icy. If not, see <http://www.gnu.org/licenses/>. 018 */ 019package icy.gui.menu.search; 020 021import java.awt.AWTEvent; 022import java.awt.AlphaComposite; 023import java.awt.Color; 024import java.awt.Graphics; 025import java.awt.Graphics2D; 026import java.awt.Insets; 027import java.awt.Point; 028import java.awt.Rectangle; 029import java.awt.RenderingHints; 030import java.awt.event.AWTEventListener; 031import java.awt.event.ActionEvent; 032import java.awt.event.FocusEvent; 033import java.awt.event.FocusListener; 034import java.awt.event.KeyEvent; 035import java.awt.event.MouseAdapter; 036import java.awt.event.MouseEvent; 037import java.awt.geom.Rectangle2D; 038import java.util.Timer; 039import java.util.TimerTask; 040 041import javax.swing.AbstractAction; 042import javax.swing.ActionMap; 043import javax.swing.InputMap; 044import javax.swing.JComponent; 045import javax.swing.KeyStroke; 046 047import org.jdesktop.swingx.painter.BusyPainter; 048 049import icy.gui.component.IcyTextField; 050import icy.resource.ResourceUtil; 051import icy.resource.icon.IcyIcon; 052import icy.search.SearchEngine; 053import icy.search.SearchEngine.SearchEngineListener; 054import icy.search.SearchResult; 055import icy.util.StringUtil; 056 057/** 058 * @author Thomas Provoost & Stephane. 059 */ 060public class SearchBar extends IcyTextField implements SearchEngineListener 061{ 062 /** 063 * 064 */ 065 private static final long serialVersionUID = -931313822004038942L; 066 067 private static final int DELAY = 20; 068 069 private static final int BUSY_PAINTER_SIZE = 15; 070 private static final int BUSY_PAINTER_POINTS = 40; 071 private static final int BUSY_PAINTER_TRAIL = 20; 072 073 /** Internal search engine */ 074 final SearchEngine searchEngine; 075 076 /** 077 * GUI 078 */ 079 final SearchResultPanel resultsPanel; 080 private final IcyIcon searchIcon; 081 082 /** 083 * Internals 084 */ 085 private Timer busyPainterTimer; 086 final BusyPainter busyPainter; 087 int frame; 088 boolean lastSearchingState; 089 boolean initialized; 090 091 public SearchBar() 092 { 093 super(); 094 095 initialized = false; 096 097 searchEngine = new SearchEngine(); 098 searchEngine.addListener(this); 099 100 resultsPanel = new SearchResultPanel(this); 101 searchIcon = new IcyIcon(ResourceUtil.ICON_SEARCH, 16); 102 103 // modify margin so we have space for icon 104 final Insets margin = getMargin(); 105 setMargin(new Insets(margin.top, margin.left, margin.bottom, margin.right + 20)); 106 107 // focusable only when hit Ctrl + F or clicked at the beginning 108 setFocusable(false); 109 110 // SET THE BUSY PAINTER 111 busyPainter = new BusyPainter(BUSY_PAINTER_SIZE); 112 busyPainter.setFrame(0); 113 busyPainter.setPoints(BUSY_PAINTER_POINTS); 114 busyPainter.setTrailLength(BUSY_PAINTER_TRAIL); 115 busyPainter.setPointShape(new Rectangle2D.Float(0, 0, 2, 1)); 116 frame = 0; 117 118 lastSearchingState = false; 119 busyPainterTimer = new Timer("Search animation timer"); 120 121 // ADD LISTENERS 122 addTextChangeListener(new TextChangeListener() 123 { 124 @Override 125 public void textChanged(IcyTextField source, boolean validate) 126 { 127 searchInternal(getText()); 128 } 129 }); 130 addMouseListener(new MouseAdapter() 131 { 132 @Override 133 public void mouseClicked(MouseEvent e) 134 { 135 setFocus(); 136 } 137 }); 138 139 addFocusListener(new FocusListener() 140 { 141 @Override 142 public void focusLost(FocusEvent e) 143 { 144 removeFocus(); 145 } 146 147 @Override 148 public void focusGained(FocusEvent e) 149 { 150 searchInternal(getText()); 151 } 152 }); 153 154 // global key listener to catch Ctrl+F in every case (not elegant) 155 // getToolkit().addAWTEventListener(new AWTEventListener() 156 // { 157 // @Override 158 // public void eventDispatched(AWTEvent event) 159 // { 160 // if (event instanceof KeyEvent) 161 // { 162 // final KeyEvent key = (KeyEvent) event; 163 // 164 // if (key.getID() == KeyEvent.KEY_PRESSED) 165 // { 166 // // Handle key presses 167 // switch (key.getKeyCode()) 168 // { 169 // case KeyEvent.VK_F: 170 // if (EventUtil.isControlDown(key)) 171 // { 172 // setFocus(); 173 // key.consume(); 174 // } 175 // break; 176 // } 177 // } 178 // } 179 // } 180 // }, AWTEvent.KEY_EVENT_MASK); 181 182 // global mouse listener to simulate focus lost (not elegant) 183 getToolkit().addAWTEventListener(new AWTEventListener() 184 { 185 @Override 186 public void eventDispatched(AWTEvent event) 187 { 188 if (!initialized || !hasFocus()) 189 return; 190 191 if (event instanceof MouseEvent) 192 { 193 final MouseEvent evt = (MouseEvent) event; 194 195 if (evt.getID() == MouseEvent.MOUSE_PRESSED) 196 { 197 final Point pt = evt.getLocationOnScreen(); 198 199 // user clicked outside search panel --> close it 200 if (!isInsideSearchComponents(pt)) 201 removeFocus(); 202 } 203 } 204 } 205 }, AWTEvent.MOUSE_EVENT_MASK); 206 207 buildActionMap(); 208 209 initialized = true; 210 } 211 212 void buildActionMap() 213 { 214 final InputMap imap = getInputMap(JComponent.WHEN_FOCUSED); 215 final ActionMap amap = getActionMap(); 216 217 imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "Cancel"); 218 imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "MoveDown"); 219 imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "MoveUp"); 220 imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "Execute"); 221 222 amap.put("Cancel", new AbstractAction() 223 { 224 /** 225 * 226 */ 227 private static final long serialVersionUID = 6690317671269902666L; 228 229 @Override 230 public void actionPerformed(ActionEvent e) 231 { 232 if (initialized) 233 cancelSearch(); 234 } 235 }); 236 getActionMap().put("MoveDown", new AbstractAction() 237 { 238 /** 239 * 240 */ 241 private static final long serialVersionUID = 8864361043092897904L; 242 243 @Override 244 public void actionPerformed(ActionEvent e) 245 { 246 if (initialized) 247 moveDown(); 248 } 249 }); 250 getActionMap().put("MoveUp", new AbstractAction() 251 { 252 /** 253 * 254 */ 255 private static final long serialVersionUID = 6258168037713535447L; 256 257 @Override 258 public void actionPerformed(ActionEvent e) 259 { 260 if (initialized) 261 moveUp(); 262 } 263 }); 264 getActionMap().put("Execute", new AbstractAction() 265 { 266 /** 267 * 268 */ 269 private static final long serialVersionUID = 5363650211730888168L; 270 271 @Override 272 public void actionPerformed(ActionEvent e) 273 { 274 if (initialized) 275 execute(); 276 } 277 }); 278 } 279 280 public SearchEngine getSearchEngine() 281 { 282 return searchEngine; 283 } 284 285 protected boolean isInsideSearchComponents(Point pt) 286 { 287 final Rectangle bounds = new Rectangle(); 288 289 bounds.setLocation(getLocationOnScreen()); 290 bounds.setSize(getSize()); 291 292 if (bounds.contains(pt)) 293 return true; 294 295 if (initialized) 296 { 297 if (resultsPanel.isVisible()) 298 { 299 bounds.setLocation(resultsPanel.getLocationOnScreen()); 300 bounds.setSize(resultsPanel.getSize()); 301 302 return bounds.contains(pt); 303 } 304 } 305 306 return false; 307 } 308 309 public void setFocus() 310 { 311 if (!hasFocus()) 312 { 313 setFocusable(true); 314 requestFocus(); 315 } 316 } 317 318 public void removeFocus() 319 { 320 if (initialized) 321 { 322 resultsPanel.close(true); 323 setFocusable(false); 324 } 325 } 326 327 public void cancelSearch() 328 { 329 setText(""); 330 } 331 332 // public void search(String text) 333 // { 334 // final String filter = text.trim(); 335 // 336 // if (StringUtil.isEmpty(filter)) 337 // searchEngine.cancelSearch(); 338 // else 339 // searchEngine.search(filter); 340 // } 341 // 342 /** 343 * Request search for the specified text. 344 * 345 * @see SearchEngine#search(String) 346 */ 347 public void search(String text) 348 { 349 setText(text); 350 } 351 352 protected void searchInternal(String text) 353 { 354 final String filter = text.trim(); 355 356 if (StringUtil.isEmpty(filter)) 357 searchEngine.cancelSearch(); 358 else 359 searchEngine.search(filter); 360 } 361 362 protected void execute() 363 { 364 // result displayed --> launch selected result 365 if (resultsPanel.isShowing()) 366 resultsPanel.executeSelected(); 367 else 368 searchInternal(getText()); 369 } 370 371 protected void moveDown() 372 { 373 resultsPanel.moveSelection(1); 374 } 375 376 protected void moveUp() 377 { 378 resultsPanel.moveSelection(-1); 379 } 380 381 @Override 382 protected void paintComponent(Graphics g) 383 { 384 super.paintComponent(g); 385 386 Graphics2D g2 = (Graphics2D) g.create(); 387 int w = getWidth(); 388 int h = getHeight(); 389 390 // set rendering presets 391 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 392 g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); 393 394 if (StringUtil.isEmpty(getText()) && !hasFocus()) 395 { 396 // draw "Search" if no focus 397 Insets insets = getMargin(); 398 Color fg = getForeground(); 399 400 g2.setColor(new Color(fg.getRed(), fg.getGreen(), fg.getBlue(), 100)); 401 g2.drawString("Search", insets.left + 2, h - g2.getFontMetrics().getHeight() / 2 + 2); 402 } 403 404 if (searchEngine.isSearching()) 405 { 406 // draw loading icon 407 g2.translate(w - (BUSY_PAINTER_SIZE + 5), 3); 408 busyPainter.paint(g2, this, BUSY_PAINTER_SIZE, BUSY_PAINTER_SIZE); 409 } 410 else 411 { 412 // draw search icon 413 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7f)); 414 searchIcon.paintIcon(this, g2, w - h, 2); 415 } 416 417 g2.dispose(); 418 } 419 420 @Override 421 public void resultChanged(SearchEngine source, SearchResult result) 422 { 423 if (initialized) 424 resultsPanel.resultChanged(result); 425 } 426 427 @Override 428 public void resultsChanged(SearchEngine source) 429 { 430 if (initialized) 431 resultsPanel.resultsChanged(); 432 } 433 434 @Override 435 public void searchStarted(SearchEngine source) 436 { 437 if (!initialized) 438 return; 439 440 // make sure the animation timer for the busy icon is stopped 441 busyPainterTimer.cancel(); 442 443 // ... and restart it 444 final Timer newTimer = new Timer("Search animation timer"); 445 newTimer.scheduleAtFixedRate(new TimerTask() 446 { 447 @Override 448 public void run() 449 { 450 frame = (frame + 1) % BUSY_PAINTER_POINTS; 451 busyPainter.setFrame(frame); 452 453 final boolean searching = searchEngine.isSearching(); 454 455 // this permit to get rid of the small delay between the searchCompleted 456 // event and when isSearching() actually returns false 457 if (searching || (searching != lastSearchingState)) 458 repaint(); 459 460 lastSearchingState = searching; 461 } 462 }, DELAY, DELAY); 463 busyPainterTimer = newTimer; 464 465 // for the busy loop animation 466 repaint(); 467 } 468 469 @Override 470 public void searchCompleted(SearchEngine source) 471 { 472 // stop the animation timer for the rotating busy icon 473 busyPainterTimer.cancel(); 474 475 // for the busy loop animation 476 repaint(); 477 } 478}