/jEdit/branches/4.3.x-fix-view-leak/org/gjt/sp/jedit/gui/CompletionPopup.java

# · Java · 490 lines · 313 code · 47 blank · 130 comment · 43 complexity · a4efb05db64a17c55a91034e7b6b6458 MD5 · raw file

  1. /*
  2. * :tabSize=8:indentSize=8:noTabs=false:
  3. * :folding=explicit:collapseFolds=1:
  4. *
  5. * Copyright (C) 2007 KazutoshiSatoda
  6. *
  7. * This program is free software; you can redistribute it and/or
  8. * modify it under the terms of the GNU General Public License
  9. * as published by the Free Software Foundation; either version 2
  10. * of the License, or any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with this program; if not, write to the Free Software
  19. * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  20. */
  21. package org.gjt.sp.jedit.gui;
  22. //{{{ Imports
  23. import java.awt.BorderLayout;
  24. import java.awt.Component;
  25. import java.awt.Point;
  26. import java.awt.Rectangle;
  27. import java.awt.Window;
  28. import java.awt.event.KeyAdapter;
  29. import java.awt.event.KeyEvent;
  30. import java.awt.event.KeyListener;
  31. import java.awt.event.MouseAdapter;
  32. import java.awt.event.MouseEvent;
  33. import java.awt.event.WindowEvent;
  34. import java.awt.event.WindowFocusListener;
  35. import javax.swing.AbstractListModel;
  36. import javax.swing.JList;
  37. import javax.swing.JPanel;
  38. import javax.swing.JScrollPane;
  39. import javax.swing.JWindow;
  40. import javax.swing.ListSelectionModel;
  41. import javax.swing.ListCellRenderer;
  42. import javax.swing.ScrollPaneConstants;
  43. import javax.swing.SwingUtilities;
  44. import org.gjt.sp.jedit.GUIUtilities;
  45. import org.gjt.sp.jedit.View;
  46. //}}}
  47. /**
  48. * Popup window for word completion in text area.
  49. * This class provides basic UI of completion popup.
  50. *
  51. * @since jEdit 4.3pre11
  52. */
  53. public class CompletionPopup extends JWindow
  54. {
  55. //{{{ interface Candidates
  56. /**
  57. * Candidates of completion.
  58. */
  59. public interface Candidates
  60. {
  61. /**
  62. * Returns the number of candidates.
  63. */
  64. public int getSize();
  65. /**
  66. * Returns whether this completion is still valid.
  67. */
  68. public boolean isValid();
  69. /**
  70. * Do the completion.
  71. */
  72. public void complete(int index);
  73. /**
  74. * Returns a component to render a cell for the index
  75. * in the popup.
  76. */
  77. public Component getCellRenderer(JList list, int index,
  78. boolean isSelected, boolean cellHasFocus);
  79. /**
  80. * Returns a description text shown when the index is
  81. * selected in the popup, or null if no description is
  82. * available.
  83. */
  84. public String getDescription(int index);
  85. } //}}}
  86. //{{{ CompletionPopup constructor
  87. /**
  88. * Create a completion popup.
  89. * It is not shown until reset() method is called with valid
  90. * candidates. All key events for the view are intercepted by
  91. * this popup untill end of completion.
  92. * @since jEdit 4.3pre13
  93. */
  94. public CompletionPopup(View view)
  95. {
  96. super(view);
  97. this.view = view;
  98. this.keyHandler = new KeyHandler();
  99. this.candidates = null;
  100. this.list = new JList();
  101. list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
  102. list.setCellRenderer(new CellRenderer());
  103. list.addKeyListener(keyHandler);
  104. list.addMouseListener(new MouseHandler());
  105. JPanel content = new JPanel(new BorderLayout());
  106. content.setFocusTraversalKeysEnabled(false);
  107. // stupid scrollbar policy is an attempt to work around
  108. // bugs people have been seeing with IBM's JDK -- 7 Sep 2000
  109. JScrollPane scroller = new JScrollPane(list,
  110. ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
  111. ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
  112. content.add(scroller, BorderLayout.CENTER);
  113. setContentPane(content);
  114. addWindowFocusListener(new WindowFocusHandler());
  115. }
  116. public CompletionPopup(View view, Point location)
  117. {
  118. this(view);
  119. if (location != null)
  120. {
  121. setLocation(location);
  122. }
  123. } //}}}
  124. //{{{ dispose() method
  125. /**
  126. * Quit completion.
  127. */
  128. public void dispose()
  129. {
  130. if (isDisplayable())
  131. {
  132. if (view.getKeyEventInterceptor() == keyHandler)
  133. {
  134. view.setKeyEventInterceptor(null);
  135. }
  136. super.dispose();
  137. // This is a workaround to ensure setting the
  138. // focus back to the textArea. Without this, the
  139. // focus gets lost after closing the popup in
  140. // some environments. It seems to be a bug in
  141. // J2SE 1.4 or 5.0. Probably it relates to the
  142. // following one.
  143. // "Frame does not receives focus after closing
  144. // of the owned window"
  145. // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4810575
  146. SwingUtilities.invokeLater(new Runnable()
  147. {
  148. public void run()
  149. {
  150. view.getTextArea().requestFocus();
  151. }
  152. });
  153. }
  154. } //}}}
  155. //{{{ reset() method
  156. /**
  157. * Start completion.
  158. * @param candidates The candidates of this completion
  159. * @param active Set focus to the popup
  160. */
  161. public void reset(Candidates candidates, boolean active)
  162. {
  163. if(candidates == null || !candidates.isValid()
  164. || candidates.getSize() <= 0)
  165. {
  166. dispose();
  167. return;
  168. }
  169. this.candidates = candidates;
  170. list.setModel(new CandidateListModel());
  171. list.setVisibleRowCount(Math.min(candidates.getSize(),8));
  172. pack();
  173. setLocation(fitInScreen(getLocation(null),this,
  174. view.getTextArea().getPainter().getFontMetrics().getHeight()));
  175. if (active)
  176. {
  177. setSelectedIndex(0);
  178. GUIUtilities.requestFocus(this,list);
  179. }
  180. setVisible(true);
  181. view.setKeyEventInterceptor(keyHandler);
  182. } //}}}
  183. //{{{ getCandidates() method
  184. /**
  185. * Current candidates of completion.
  186. */
  187. public Candidates getCandidates()
  188. {
  189. return candidates;
  190. } //}}}
  191. //{{{ getSelectedIndex() method
  192. /**
  193. * Returns index of current selection.
  194. * Returns -1 if nothing is selected.
  195. */
  196. public int getSelectedIndex()
  197. {
  198. return list.getSelectedIndex();
  199. } //}}}
  200. //{{{ setSelectedIndex() method
  201. /**
  202. * Set selection.
  203. */
  204. public void setSelectedIndex(int index)
  205. {
  206. if (candidates != null
  207. && 0 <= index && index < candidates.getSize())
  208. {
  209. list.setSelectedIndex(index);
  210. list.ensureIndexIsVisible(index);
  211. String description = candidates.getDescription(index);
  212. if (description != null)
  213. {
  214. view.getStatus().setMessageAndClear(description);
  215. }
  216. }
  217. } //}}}
  218. //{{{ doSelectedCompletion() method
  219. /**
  220. * Do completion with current selection and quit.
  221. */
  222. public boolean doSelectedCompletion()
  223. {
  224. int selected = list.getSelectedIndex();
  225. if (candidates != null &&
  226. 0 <= selected && selected < candidates.getSize())
  227. {
  228. candidates.complete(selected);
  229. dispose();
  230. return true;
  231. }
  232. return false;
  233. } //}}}
  234. //{{{ keyPressed() medhod
  235. /**
  236. * Handle key pressed events.
  237. * Override this method to make additional key handing.
  238. */
  239. protected void keyPressed(KeyEvent e)
  240. {
  241. } //}}}
  242. //{{{ keyTyped() medhod
  243. /**
  244. * Handle key typed events.
  245. * Override this method to make additional key handing.
  246. */
  247. protected void keyTyped(KeyEvent e)
  248. {
  249. } //}}}
  250. //{{{ Private members
  251. //{{{ Instance variables
  252. private final View view;
  253. private final KeyHandler keyHandler;
  254. private Candidates candidates;
  255. private final JList list;
  256. //}}}
  257. //{{{ fitInScreen() method
  258. private static Point fitInScreen(Point p, Window w, int lineHeight)
  259. {
  260. Rectangle screenSize = w.getGraphicsConfiguration().getBounds();
  261. if(p.y + w.getHeight() >= screenSize.height)
  262. p.y = p.y - w.getHeight() - lineHeight;
  263. return p;
  264. } //}}}
  265. //{{{ moveRelative method()
  266. private void moveRelative(int n)
  267. {
  268. int selected = list.getSelectedIndex();
  269. int newSelect = selected + n;
  270. if (newSelect < 0)
  271. {
  272. newSelect = 0;
  273. }
  274. else
  275. {
  276. int numItems = list.getModel().getSize();
  277. if(numItems < 1)
  278. {
  279. return;
  280. }
  281. if(newSelect >= numItems)
  282. {
  283. newSelect = numItems - 1;
  284. }
  285. }
  286. if(newSelect != selected)
  287. {
  288. setSelectedIndex(newSelect);
  289. }
  290. } //}}}
  291. //{{{ moveRelativePages() method
  292. private void moveRelativePages(int n)
  293. {
  294. int pageSize = list.getVisibleRowCount() - 1;
  295. moveRelative(pageSize * n);
  296. } //}}}
  297. //{{{ passKeyEventToView() method
  298. private void passKeyEventToView(KeyEvent e)
  299. {
  300. // Remove intercepter to avoid infinite recursion.
  301. assert (view.getKeyEventInterceptor() == keyHandler);
  302. view.setKeyEventInterceptor(null);
  303. // Here depends on an implementation detail.
  304. // Use ACTION_BAR to force processing KEY_TYPED event in
  305. // the implementation of gui.InputHandler.processKeyEvent().
  306. view.getInputHandler().processKeyEvent(e, View.ACTION_BAR, false);
  307. // Restore keyHandler only if this popup is still alive.
  308. // The key event might trigger dispose() of this popup.
  309. if (this.isDisplayable())
  310. {
  311. view.setKeyEventInterceptor(keyHandler);
  312. }
  313. } //}}}
  314. //{{{ CandidateListModel class
  315. private class CandidateListModel extends AbstractListModel
  316. {
  317. public int getSize()
  318. {
  319. return candidates.getSize();
  320. }
  321. public Object getElementAt(int index)
  322. {
  323. // This value is not used.
  324. // The list is only rendered by components
  325. // returned by getCellRenderer().
  326. return candidates;
  327. }
  328. } //}}}
  329. //{{{ CellRenderer class
  330. private class CellRenderer implements ListCellRenderer
  331. {
  332. public Component getListCellRendererComponent(JList list,
  333. Object value, int index,
  334. boolean isSelected, boolean cellHasFocus)
  335. {
  336. return candidates.getCellRenderer(list, index,
  337. isSelected, cellHasFocus);
  338. }
  339. } //}}}
  340. //{{{ KeyHandler class
  341. private class KeyHandler extends KeyAdapter
  342. {
  343. //{{{ keyPressed() method
  344. public void keyPressed(KeyEvent e)
  345. {
  346. CompletionPopup.this.keyPressed(e);
  347. if (candidates == null || !candidates.isValid())
  348. {
  349. dispose();
  350. }
  351. else if (!e.isConsumed())
  352. {
  353. switch(e.getKeyCode())
  354. {
  355. case KeyEvent.VK_TAB:
  356. case KeyEvent.VK_ENTER:
  357. if (doSelectedCompletion())
  358. {
  359. e.consume();
  360. }
  361. else
  362. {
  363. dispose();
  364. }
  365. break;
  366. case KeyEvent.VK_ESCAPE:
  367. dispose();
  368. e.consume();
  369. break;
  370. case KeyEvent.VK_UP:
  371. moveRelative(-1);
  372. e.consume();
  373. break;
  374. case KeyEvent.VK_DOWN:
  375. moveRelative(1);
  376. e.consume();
  377. break;
  378. case KeyEvent.VK_PAGE_UP:
  379. moveRelativePages(-1);
  380. e.consume();
  381. break;
  382. case KeyEvent.VK_PAGE_DOWN:
  383. moveRelativePages(1);
  384. e.consume();
  385. break;
  386. default:
  387. if(e.isActionKey()
  388. || e.isControlDown()
  389. || e.isAltDown()
  390. || e.isMetaDown())
  391. {
  392. dispose();
  393. }
  394. break;
  395. }
  396. }
  397. if (!e.isConsumed())
  398. {
  399. passKeyEventToView(e);
  400. }
  401. } //}}}
  402. //{{{ keyTyped() method
  403. public void keyTyped(KeyEvent e)
  404. {
  405. CompletionPopup.this.keyTyped(e);
  406. if (candidates == null || !candidates.isValid())
  407. {
  408. dispose();
  409. }
  410. if (!e.isConsumed())
  411. {
  412. passKeyEventToView(e);
  413. }
  414. } //}}}
  415. } //}}}
  416. //{{{ MouseHandler class
  417. private class MouseHandler extends MouseAdapter
  418. {
  419. public void mouseClicked(MouseEvent e)
  420. {
  421. if (doSelectedCompletion())
  422. {
  423. e.consume();
  424. }
  425. else
  426. {
  427. dispose();
  428. }
  429. }
  430. } //}}}
  431. //{{{ WindowFocusHandler class
  432. private class WindowFocusHandler implements WindowFocusListener
  433. {
  434. public void windowGainedFocus(WindowEvent e)
  435. {
  436. }
  437. public void windowLostFocus(WindowEvent e)
  438. {
  439. dispose();
  440. }
  441. } //}}}
  442. //}}}
  443. }