/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * Licensed 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 net.java.sip.communicator.plugin.keybindingchooser.chooser; import java.awt.*; import java.awt.event.*; import java.util.*; import javax.swing.*; import javax.swing.event.*; import net.java.sip.communicator.plugin.desktoputil.*; /** * Panel containing a listing of current keybinding mappings. This contains * methods that can be overwritten to provide easy editing functionality and * display logic. Note that this does not support the manual addition or removal * of BindingEntry components. However this is designed to tolerate the changing * of entry visibility (including individual fields) and the manual addition and * removal of extra components either to this panel or its BindingEntries.<br> * This represents a mapping of keystrokes to strings, and hence duplicate * shortcuts aren't supported. An exception is made in the case of disabled * shortcuts, but to keep mappings unique duplicate actions among disabled * entries are not permitted. * * @author Damian Johnson (atagar1@gmail.com) * @version September 1, 2007 */ public abstract class BindingPanel extends TransparentPanel { /** * Serial version UID. */ private static final long serialVersionUID = 0L; private ArrayList<BindingEntry> contents = new ArrayList<BindingEntry>(); /** * Method called whenever an entry is either added or shifts in the display. * For instance, if the second entry is removed then this is called on the * third to last elements. * * @param index newly assigned index of entry * @param entry entry that has been added or shifted * @param isNew if true the entry is new to the display, false otherwise */ protected abstract void onUpdate(int index, BindingEntry entry, boolean isNew); /** * Method called upon any mouse clicks within a BindingEntry in the display. * * @param event fired mouse event that triggered method call * @param entry entry on which the click landed * @param field field of entry on which the click landed, null if not a * recognized field */ protected abstract void onClick(MouseEvent event, BindingEntry entry, BindingEntry.Field field); /** * Constructor. */ public BindingPanel() { setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); addMouseListener(new MouseTracker()); } /** * Adds a new key binding mapping to the end of the listing. If this already * contains the shortcut then the previous entry is replaced instead (not * triggering the onUpdate method). Disabled shortcuts trigger replacement * on duplicate actions instead. This uses the normal parameters used to * generate key stokes, such as: * * <pre> * bindingPanel.putBinding('Y', 0, "Confirm Selection"); * bindingPanel.putBinding(KeyEvent.VK_DELETE, KeyEvent.CTRL_MASK * | KeyEvent.ALT_MASK, "Kill Process"); * </pre> * * @param keyCode key code of keystroke component of mapping * @param modifier modifiers of keystroke component of mapping * @param action string component of mapping * @return true if contents did not already include shortcut */ public boolean putBinding(int keyCode, int modifier, String action) { return putBinding(KeyStroke.getKeyStroke(keyCode, modifier), action); } /** * Adds a new key binding mapping to the end of the listing. If this already * contains the shortcut then the previous entry is replaced instead (not * triggering the onUpdate method). Disabled shortcuts trigger replacement * on duplicate actions instead. * * @param shortcut keystroke component of mapping * @param action string component of mapping * @return true if contents did not already include shortcut */ public boolean putBinding(KeyStroke shortcut, String action) { return putBinding(shortcut, action, getComponentCount()); } /** * Adds a new key binding mapping to a particular index of the listing. If * this already contains the shortcut then the previous entry is replaced * instead (not triggering the onUpdate method). Disabled shortcuts trigger * replacement on duplicate actions instead. * * @param shortcut keystroke component of mapping * @param action string component of mapping * @param index location in which to insert mapping * @return true if contents did not already include shortcut * @throws IndexOutOfBoundsException if index is out of range (index < 0 || * index > getBindingCount()). */ public boolean putBinding(KeyStroke shortcut, String action, int index) { return putBinding(new BindingEntry(shortcut, action), index); } /** * Adds a new key binding mapping to a particular index of the listing. If * this already contains the shortcut then the previous entry is replaced * instead (not triggering the onUpdate method). Disabled shortcuts trigger * replacement on duplicate actions instead. * * @param newEntry entry to add to contents * @param index location in which to insert mapping * @return true if contents did not already include shortcut * @throws IndexOutOfBoundsException if index is out of range (index < 0 || * index > getBindingCount()). */ public boolean putBinding(BindingEntry newEntry, int index) { if (index < 0 || index > getBindingCount()) { String message = "Attempting to add to invalid index: " + index; throw new IndexOutOfBoundsException(message); } KeyStroke shortcut = newEntry.getShortcut(); if (shortcut != BindingEntry.DISABLED) { // Checks for duplicate shortcut for (BindingEntry entry : this.contents) { if (shortcut.equals(entry.getShortcut())) { entry.setAction(newEntry.getAction()); return false; } } } else { // Checks if this entry would be a duplicate if (this.contents.contains(newEntry)) return false; } this.contents.add(index, newEntry); // Inserts into display, maintaining ordering of collection if (index > 0) { /* * Places the new entry after previously listed one, transversing * backward to support common case of adding to the end. This * depends on bindings being unique. */ BindingEntry previous = getBinding(index - 1); for (int i = getComponentCount() - 1; i >= 0; --i) { if (getComponent(i).equals(previous)) { add(newEntry, i + 1); break; } assert i != 0 : "Listing doesn't contain expected previous entry " + "when adding to index " + index; } } else { add(newEntry, 0); // Adds to start } // Calls update on add entry and any shifted contents onUpdate(index, newEntry, true); for (int i = index + 1; i < getBindingCount(); ++i) { BindingEntry shifted = getBinding(i); onUpdate(i, shifted, false); } return true; } /** * Adds a collection of new key binding mappings to the end of the listing. * If any shortcuts are already contained then the previous entries are * replaced (not triggering the onUpdate method). Disabled shortcuts trigger * replacement on duplicate actions instead. * * @param bindings mapping between keystrokes and actions to be added */ public void putAllBindings(Map<KeyStroke, String> bindings) { for (KeyStroke action : bindings.keySet()) { putBinding(action, bindings.get(action)); } } /** * Removes a particular binding from the contents. * * @param entry binding to be removed * @return true if binding was in the contents, false otherwise */ public boolean removeBinding(BindingEntry entry) { int index = getBindingIndex(entry); if (index != -1) return removeBinding(index) != null; else return false; } /** * Removes the binding at a particular index of the listing. * * @param index from which to remove entry * @return the entry that was removed from the contents * @throws IndexOutOfBoundsException if index is out of range (index < 0 || * index > getBindingCount()). */ public BindingEntry removeBinding(int index) { if (index < 0 || index > getBindingCount()) { String message = "Attempting to remove from invalid index: " + index; throw new IndexOutOfBoundsException(message); } BindingEntry entry = this.contents.remove(index); remove(entry); // Removes from display // Calls update on shifted entries for (int i = index; i < getBindingCount(); ++i) { BindingEntry shifted = getBinding(i); onUpdate(i, shifted, false); } return entry; } /** * Removes all bindings from the panel. */ public void clearBindings() { while (getBindingCount() > 0) { removeBinding(0); } } /** * Returns if a keystroke is in the panel's current contents. This provides * a preemptive means of checking if adding a non-disabled shortcut would * cause a replacement. * * @param shortcut keystroke to be checked against contents * @return true if contents includes the shortcut, false otherwise */ public boolean contains(KeyStroke shortcut) { for (BindingEntry entry : this.contents) { if (shortcut == BindingEntry.DISABLED) { if (entry.isDisabled()) return true; } else { if (shortcut.equals(entry.getShortcut())) return true; } } return false; } /** * Provides number of key bindings currently present. * * @return number of key bindings in the display */ public int getBindingCount() { return this.contents.size(); } /** * Provides the index of a particular entry. * * @param entry entry for which the index should be returned * @return entry index, -1 if not found */ public int getBindingIndex(BindingEntry entry) { return this.contents.indexOf(entry); } /** * Provides a binding at a particular index. * * @param index index from which to retrieve binding. * @return the entry at the specified position in this list */ public BindingEntry getBinding(int index) { return this.contents.get(index); } /** * Provides listing of the current keybinding entries. * * @return list of current entry contents */ public ArrayList<BindingEntry> getBindings() { return new ArrayList<BindingEntry>(this.contents); } /** * Provides the mapping between keystrokes and actions represented by the * contents of the display. Disabled entries aren't included in the mapping. * * @return mapping between contained keystrokes and their associated actions */ public LinkedHashMap<KeyStroke, String> getBindingMap() { LinkedHashMap<KeyStroke, String> mapping = new LinkedHashMap<KeyStroke, String>(); for (BindingEntry entry : this.contents) { if (entry.isDisabled()) continue; else mapping.put(entry.getShortcut(), entry.getAction()); } return mapping; } /** * Provides an input map associating keystrokes to actions according to the * contents of the display. Disabled entries aren't included in the mapping. * * @return input mapping between contained keystrokes and their associated * actions */ public InputMap getBindingInputMap() { InputMap mapping = new InputMap(); LinkedHashMap<KeyStroke, String> bindingMap = getBindingMap(); for (KeyStroke keystroke : bindingMap.keySet()) { mapping.put(keystroke, bindingMap.get(keystroke)); } return mapping; } // Mouse listener for clicks within display private class MouseTracker extends MouseInputAdapter { @Override public void mousePressed(MouseEvent event) { Point loc = event.getPoint(); Component comp = getComponentAt(loc); if (comp instanceof BindingEntry) { BindingEntry entry = (BindingEntry) comp; // Gets label within entry int x = loc.x - entry.getLocation().x; int y = loc.y - entry.getLocation().y; Component label = entry.findComponentAt(x, y); if (entry.getField(BindingEntry.Field.INDENT).equals(label)) { onClick(event, entry, BindingEntry.Field.INDENT); } else if (entry.getField(BindingEntry.Field.ACTION) .equals(label)) { onClick(event, entry, BindingEntry.Field.ACTION); } else if (entry.getField(BindingEntry.Field.SHORTCUT).equals( label)) { onClick(event, entry, BindingEntry.Field.SHORTCUT); } else { onClick(event, entry, null); // Click fell on unrecognized // component } } } } }