001 /* 002 * Copyright 2002-2004 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 005 * use this file except in compliance with the License. You may obtain a copy of 006 * the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 012 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 013 * License for the specific language governing permissions and limitations under 014 * the License. 015 */ 016 package org.springframework.richclient.text; 017 018 import java.awt.Toolkit; 019 import java.awt.event.ActionEvent; 020 import java.awt.event.ActionListener; 021 import java.awt.event.FocusEvent; 022 import java.awt.event.FocusListener; 023 import java.awt.event.MouseAdapter; 024 import java.awt.event.MouseEvent; 025 import java.util.Enumeration; 026 import java.util.Hashtable; 027 import java.util.Vector; 028 029 import javax.swing.Action; 030 import javax.swing.JPasswordField; 031 import javax.swing.JPopupMenu; 032 import javax.swing.KeyStroke; 033 import javax.swing.Timer; 034 import javax.swing.event.CaretEvent; 035 import javax.swing.event.CaretListener; 036 import javax.swing.event.UndoableEditEvent; 037 import javax.swing.event.UndoableEditListener; 038 import javax.swing.text.JTextComponent; 039 import javax.swing.text.Keymap; 040 import javax.swing.undo.UndoManager; 041 042 import org.springframework.binding.value.CommitTrigger; 043 import org.springframework.binding.value.CommitTriggerListener; 044 import org.springframework.richclient.application.Application; 045 import org.springframework.richclient.application.ApplicationWindow; 046 import org.springframework.richclient.command.ActionCommand; 047 import org.springframework.richclient.command.CommandGroup; 048 import org.springframework.richclient.command.CommandManager; 049 import org.springframework.richclient.command.TargetableActionCommand; 050 import org.springframework.richclient.command.support.AbstractActionCommandExecutor; 051 import org.springframework.richclient.command.support.DefaultCommandManager; 052 import org.springframework.richclient.command.support.GlobalCommandIds; 053 054 /** 055 * Helper class that decorates a <code>JTextComponent</code> with a standard 056 * popup menu. Support for undo/redo is also provided. 057 * 058 * @author Oliver Hutchison 059 */ 060 public class TextComponentPopup extends MouseAdapter implements FocusListener, CaretListener, UndoableEditListener { 061 062 /** 063 * Delay in ms between updates of the paste commands status. We only update 064 * the paste command's status occasionally as this is a quite expensive 065 * operation. 066 */ 067 private static final int PAST_REFRESH_TIMER_DELAY = 100; 068 069 private static final String[] COMMANDS = new String[] { GlobalCommandIds.UNDO, GlobalCommandIds.REDO, 070 GlobalCommandIds.COPY, GlobalCommandIds.CUT, GlobalCommandIds.PASTE, GlobalCommandIds.SELECT_ALL }; 071 072 public static void attachPopup(JTextComponent textComponent, CommitTrigger resetUndoHistoryTrigger) { 073 new TextComponentPopup(textComponent, resetUndoHistoryTrigger); 074 } 075 076 public static void attachPopup(JTextComponent textComponent) { 077 new TextComponentPopup(textComponent, null); 078 } 079 080 private final JTextComponent textComponent; 081 082 private final Timer updatePasteStatusTimer; 083 084 private final UndoManager undoManager = new UndoManager(); 085 086 private final CommitTrigger resetUndoHistoryTrigger; 087 088 private static CommandManager localCommandManager; 089 090 private final UndoCommandExecutor undo = new UndoCommandExecutor(); 091 092 private final RedoCommandExecutor redo = new RedoCommandExecutor(); 093 094 private final CutCommandExecutor cut = new CutCommandExecutor(); 095 096 private final CopyCommandExecutor copy = new CopyCommandExecutor(); 097 098 private final PasteCommandExecutor paste = new PasteCommandExecutor(); 099 100 private final SelectAllCommandExecutor selectAll = new SelectAllCommandExecutor(); 101 102 protected TextComponentPopup(JTextComponent textComponent, CommitTrigger resetUndoHistoryTrigger) { 103 this.textComponent = textComponent; 104 this.resetUndoHistoryTrigger = resetUndoHistoryTrigger; 105 this.updatePasteStatusTimer = new Timer(PAST_REFRESH_TIMER_DELAY, new ActionListener() { 106 public void actionPerformed(ActionEvent e) { 107 updatePasteStatus(); 108 } 109 }); 110 updatePasteStatusTimer.setCoalesce(true); 111 updatePasteStatusTimer.setRepeats(false); 112 updatePasteStatusTimer.setInitialDelay(PAST_REFRESH_TIMER_DELAY); 113 registerListeners(); 114 registerAccelerators(); 115 } 116 117 private void registerListeners() { 118 textComponent.addMouseListener(this); 119 textComponent.addFocusListener(this); 120 textComponent.addCaretListener(this); 121 textComponent.getDocument().addUndoableEditListener(this); 122 if (resetUndoHistoryTrigger != null) { 123 CommitTriggerListener resetUndoHistoryHandler = new CommitTriggerListener() { 124 public void commit() { 125 undoManager.discardAllEdits(); 126 updateUndoRedoState(); 127 } 128 129 public void revert() { 130 } 131 }; 132 resetUndoHistoryTrigger.addCommitTriggerListener(resetUndoHistoryHandler); 133 } 134 } 135 136 protected CommandManager getCommandManager() { 137 CommandManager commandManager; 138 ApplicationWindow appWindow = Application.instance().getActiveWindow(); 139 if (appWindow == null || appWindow.getCommandManager() == null) { 140 if (localCommandManager == null) { 141 localCommandManager = new DefaultCommandManager(); 142 } 143 commandManager = localCommandManager; 144 } 145 else { 146 commandManager = appWindow.getCommandManager(); 147 } 148 for (int i = 0; i < COMMANDS.length; i++) { 149 if (!commandManager.containsActionCommand(COMMANDS[i])) { 150 commandManager.registerCommand(new TargetableActionCommand(COMMANDS[i], null)); 151 } 152 } 153 return commandManager; 154 } 155 156 public void registerAccelerators() { 157 CommandManager commandManager = getCommandManager(); 158 Keymap keymap = new DefaultKeymap(getClass().getName(), textComponent.getKeymap()); 159 for (int i = 0; i < COMMANDS.length; i++) { 160 ActionCommand command = commandManager.getActionCommand(COMMANDS[i]); 161 keymap.addActionForKeyStroke(command.getAccelerator(), command.getActionAdapter()); 162 } 163 if (COMMANDS.length > 0) { 164 textComponent.setKeymap(keymap); 165 } 166 } 167 168 /** 169 * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent) 170 */ 171 public void mousePressed(MouseEvent evt) { 172 maybeShowPopup(evt); 173 } 174 175 /** 176 * @see java.awt.event.MouseAdapter#mouseReleased(java.awt.event.MouseEvent) 177 */ 178 public void mouseReleased(MouseEvent evt) { 179 maybeShowPopup(evt); 180 } 181 182 private void maybeShowPopup(MouseEvent evt) { 183 if (evt.isPopupTrigger()) { 184 updatePasteStatusNow(); 185 createPopup().show(evt.getComponent(), evt.getX(), evt.getY()); 186 } 187 } 188 189 public void caretUpdate(CaretEvent e) { 190 updateState(); 191 } 192 193 public void focusGained(FocusEvent e) { 194 updateState(); 195 registerCommandExecutors(); 196 } 197 198 public void focusLost(FocusEvent e) { 199 if (!e.isTemporary()) { 200 unregisterCommandExecutors(); 201 } 202 } 203 204 public void undoableEditHappened(UndoableEditEvent e) { 205 undoManager.addEdit(e.getEdit()); 206 updateUndoRedoState(); 207 } 208 209 private JPopupMenu createPopup() { 210 if (textComponent instanceof JPasswordField) 211 return getPasswordCommandGroup().createPopupMenu(); 212 213 if (isEditable()) 214 return getEditableCommandGroup().createPopupMenu(); 215 216 return getReadOnlyCommandGroup().createPopupMenu(); 217 } 218 219 private void updateState() { 220 boolean hasSelection = textComponent.getSelectionStart() != textComponent.getSelectionEnd(); 221 copy.setEnabled(hasSelection); 222 selectAll.setEnabled(textComponent.getDocument().getLength() > 0); 223 boolean isEditable = isEditable(); 224 cut.setEnabled(hasSelection && isEditable); 225 if (isEditable) { 226 scheduleUpdatePasteStatus(); 227 } 228 else { 229 paste.setEnabled(false); 230 } 231 updateUndoRedoState(); 232 } 233 234 private void updateUndoRedoState() { 235 undo.setEnabled(undoManager.canUndo()); 236 redo.setEnabled(undoManager.canRedo()); 237 } 238 239 private void scheduleUpdatePasteStatus() { 240 // we do this using a timer as the method canPasteFromClipboard() 241 // can be a schedule significant bottle neck when there's lots of typing 242 // going on 243 if (!updatePasteStatusTimer.isRunning()) { 244 updatePasteStatusTimer.restart(); 245 } 246 } 247 248 private void updatePasteStatusNow() { 249 if (updatePasteStatusTimer.isRunning()) { 250 updatePasteStatusTimer.stop(); 251 } 252 updatePasteStatus(); 253 } 254 255 private void updatePasteStatus() { 256 paste.setEnabled(isEditable() && canPasteFromClipboard()); 257 } 258 259 /** 260 * Try not to call this method to much as SystemClipboard#getContents() 261 * relatively slow. 262 */ 263 private boolean canPasteFromClipboard() { 264 try { 265 return textComponent.getTransferHandler().canImport( 266 textComponent, 267 Toolkit.getDefaultToolkit().getSystemClipboard().getContents(textComponent) 268 .getTransferDataFlavors()); 269 } 270 catch (IllegalStateException e) { 271 /* 272 * as the javadoc of Clipboard.getContents state: the 273 * IllegalStateException can be thrown when the clipboard is not 274 * available (i.e. in use by another application), so we return 275 * false. 276 */ 277 return false; 278 } 279 } 280 281 private boolean isEditable() { 282 return !(textComponent instanceof JPasswordField) && textComponent.isEnabled() && textComponent.isEditable(); 283 } 284 285 protected CommandGroup getEditableCommandGroup() { 286 CommandGroup editGroup = getCommandManager().getCommandGroup("textEditMenu"); 287 if (editGroup == null) { 288 editGroup = getCommandManager().createCommandGroup( 289 "textEditMenu", 290 new Object[] { GlobalCommandIds.UNDO, GlobalCommandIds.REDO, "separator", GlobalCommandIds.CUT, 291 GlobalCommandIds.COPY, GlobalCommandIds.PASTE, "separator", GlobalCommandIds.SELECT_ALL }); 292 } 293 return editGroup; 294 } 295 296 protected CommandGroup getPasswordCommandGroup() { 297 CommandGroup passwordGroup = getCommandManager().getCommandGroup("passwordTextEditMenu"); 298 if (passwordGroup == null) { 299 passwordGroup = getCommandManager().createCommandGroup("passwordTextEditMenu", 300 new Object[] { GlobalCommandIds.UNDO, GlobalCommandIds.REDO }); 301 } 302 return passwordGroup; 303 } 304 305 protected CommandGroup getReadOnlyCommandGroup() { 306 CommandGroup readOnlyGroup = getCommandManager().getCommandGroup("readOnlyTextEditMenu"); 307 if (readOnlyGroup == null) { 308 readOnlyGroup = getCommandManager().createCommandGroup("readOnlyTextEditMenu", 309 new Object[] { GlobalCommandIds.COPY, "separator", GlobalCommandIds.SELECT_ALL }); 310 } 311 return readOnlyGroup; 312 } 313 314 private void registerCommandExecutors() { 315 CommandManager commandManager = getCommandManager(); 316 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.UNDO, undo); 317 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.REDO, redo); 318 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.CUT, cut); 319 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.COPY, copy); 320 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.PASTE, paste); 321 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.SELECT_ALL, selectAll); 322 } 323 324 private void unregisterCommandExecutors() { 325 CommandManager commandManager = getCommandManager(); 326 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.UNDO, null); 327 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.REDO, null); 328 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.CUT, null); 329 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.COPY, null); 330 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.PASTE, null); 331 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.SELECT_ALL, null); 332 } 333 334 private class UndoCommandExecutor extends AbstractActionCommandExecutor { 335 public void execute() { 336 undoManager.undo(); 337 } 338 } 339 340 private class RedoCommandExecutor extends AbstractActionCommandExecutor { 341 public void execute() { 342 undoManager.redo(); 343 } 344 } 345 346 private class CutCommandExecutor extends AbstractActionCommandExecutor { 347 public void execute() { 348 textComponent.cut(); 349 } 350 } 351 352 private class CopyCommandExecutor extends AbstractActionCommandExecutor { 353 public void execute() { 354 textComponent.copy(); 355 } 356 } 357 358 private class PasteCommandExecutor extends AbstractActionCommandExecutor { 359 public void execute() { 360 textComponent.paste(); 361 } 362 } 363 364 private class SelectAllCommandExecutor extends AbstractActionCommandExecutor { 365 public void execute() { 366 textComponent.selectAll(); 367 } 368 } 369 370 /** 371 * We need this class since keymaps are shared in jvm This class is a 100% 372 * copy of the jdk class {@link JTextComponent#DefaultKeymap 373 */ 374 public static class DefaultKeymap implements Keymap { 375 376 String nm; 377 378 Keymap parent; 379 380 Hashtable bindings; 381 382 Action defaultAction; 383 384 DefaultKeymap(String nm, Keymap parent) { 385 this.nm = nm; 386 this.parent = parent; 387 bindings = new Hashtable(); 388 } 389 390 /** 391 * Fetch the default action to fire if a key is typed (ie a KEY_TYPED 392 * KeyEvent is received) and there is no binding for it. Typically this 393 * would be some action that inserts text so that the keymap doesn't 394 * require an action for each possible key. 395 */ 396 public Action getDefaultAction() { 397 if (defaultAction != null) { 398 return defaultAction; 399 } 400 return (parent != null) ? parent.getDefaultAction() : null; 401 } 402 403 /** 404 * Set the default action to fire if a key is typed. 405 */ 406 public void setDefaultAction(Action a) { 407 defaultAction = a; 408 } 409 410 public String getName() { 411 return nm; 412 } 413 414 public Action getAction(KeyStroke key) { 415 Action a = (Action) bindings.get(key); 416 if ((a == null) && (parent != null)) { 417 a = parent.getAction(key); 418 } 419 return a; 420 } 421 422 public KeyStroke[] getBoundKeyStrokes() { 423 KeyStroke[] keys = new KeyStroke[bindings.size()]; 424 int i = 0; 425 for (Enumeration e = bindings.keys(); e.hasMoreElements();) { 426 keys[i++] = (KeyStroke) e.nextElement(); 427 } 428 return keys; 429 } 430 431 public Action[] getBoundActions() { 432 Action[] actions = new Action[bindings.size()]; 433 int i = 0; 434 for (Enumeration e = bindings.elements(); e.hasMoreElements();) { 435 actions[i++] = (Action) e.nextElement(); 436 } 437 return actions; 438 } 439 440 public KeyStroke[] getKeyStrokesForAction(Action a) { 441 if (a == null) { 442 return null; 443 } 444 KeyStroke[] retValue = null; 445 // Determine local bindings first. 446 Vector keyStrokes = null; 447 for (Enumeration enum_ = bindings.keys(); enum_.hasMoreElements();) { 448 Object key = enum_.nextElement(); 449 if (bindings.get(key) == a) { 450 if (keyStrokes == null) { 451 keyStrokes = new Vector(); 452 } 453 keyStrokes.addElement(key); 454 } 455 } 456 // See if the parent has any. 457 if (parent != null) { 458 KeyStroke[] pStrokes = parent.getKeyStrokesForAction(a); 459 if (pStrokes != null) { 460 // Remove any bindings defined in the parent that 461 // are locally defined. 462 int rCount = 0; 463 for (int counter = pStrokes.length - 1; counter >= 0; counter--) { 464 if (isLocallyDefined(pStrokes[counter])) { 465 pStrokes[counter] = null; 466 rCount++; 467 } 468 } 469 if (rCount > 0 && rCount < pStrokes.length) { 470 if (keyStrokes == null) { 471 keyStrokes = new Vector(); 472 } 473 for (int counter = pStrokes.length - 1; counter >= 0; counter--) { 474 if (pStrokes[counter] != null) { 475 keyStrokes.addElement(pStrokes[counter]); 476 } 477 } 478 } 479 else if (rCount == 0) { 480 if (keyStrokes == null) { 481 retValue = pStrokes; 482 } 483 else { 484 retValue = new KeyStroke[keyStrokes.size() + pStrokes.length]; 485 keyStrokes.copyInto(retValue); 486 System.arraycopy(pStrokes, 0, retValue, keyStrokes.size(), pStrokes.length); 487 keyStrokes = null; 488 } 489 } 490 } 491 } 492 if (keyStrokes != null) { 493 retValue = new KeyStroke[keyStrokes.size()]; 494 keyStrokes.copyInto(retValue); 495 } 496 return retValue; 497 } 498 499 public boolean isLocallyDefined(KeyStroke key) { 500 return bindings.containsKey(key); 501 } 502 503 public void addActionForKeyStroke(KeyStroke key, Action a) { 504 bindings.put(key, a); 505 } 506 507 public void removeKeyStrokeBinding(KeyStroke key) { 508 bindings.remove(key); 509 } 510 511 public void removeBindings() { 512 bindings.clear(); 513 } 514 515 public Keymap getResolveParent() { 516 return parent; 517 } 518 519 public void setResolveParent(Keymap parent) { 520 this.parent = parent; 521 } 522 523 /** 524 * String representation of the keymap... potentially a very long 525 * string. 526 */ 527 public String toString() { 528 return "Keymap[" + nm + "]" + bindings; 529 } 530 531 } 532 }