/*
* 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 net.java.sip.communicator.plugin.desktoputil.*;
import net.java.sip.communicator.service.keybindings.*;
/**
* Implementation of the BindingPanel that provides configuring functionality
* for the keystroke component of key bindings. Methods provide a means of
* producing predefined, sweeping changes in the display. This defaults to a
* light blue color scheme with an index indent style.
* Though display elements are still accessible, manual changes are not
* particularly recommended unless automated changes to the appearance (the
* indentation style and color scheme) are disabled since they may be
* unexpectedly reverted or clash any alterations made.
*
* @author Damian Johnson (atagar1@gmail.com)
* @version September 1, 2007
*/
public class BindingChooser
extends BindingPanel
{
private static final long serialVersionUID = 0;
private IndentStyle indentStyle = IndentStyle.INDEX;
private boolean isShortcutEditable = true; // Determines if shortcut fields
// can be selected
private BindingEntry selectedEntry = null; // None selected when null
private String selectedText = "Press shortcut...";
/**
* Keybinding set.
*/
private KeybindingSet set = null;
/**
* Displays a dialog allowing the user to redefine the keystroke component
* of key bindings. The top has light blue labels describing the fields and
* the bottom provides an 'OK' and 'Cancel' option. This uses the default
* color scheme and indent style. If no entries are selected then the enter
* key is equivalent to pressing 'OK' and escape is the same as 'Cancel'.
*
* @param parent frame to which to apply modal property and center within
* (centers within screen if null)
* @param bindings initial mapping of keystrokes to their actions
* @return redefined mapping of keystrokes to their actions, null if cancel
* is pressed
*/
public static LinkedHashMap showDialog(Component parent,
Map bindings)
{
BindingChooser display = new BindingChooser();
display.putAllBindings(bindings);
return showDialog(parent, display, "Key Bindings", true, display
.makeAdaptor());
}
/**
* 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 set mapping between keystrokes and actions to be added
*/
public void putAllBindings(KeybindingSet set)
{
this.set = set;
putAllBindings(set.getBindings());
}
/**
* Displays a dialog allowing the user to redefine the keystroke component
* of key bindings. The bottom provides an 'OK' and 'Cancel' option. If no
* entries are selected then the enter key is equivalent to pressing 'OK'
* and escape is the same as 'Cancel'. Label and button backgrounds try to
* match color scheme if set.
* Including focusable elements in the display will prevent user input from
* setting the selected shortcut field. Also note that labels use the
* default entry size and should be omitted if using content with custom
* dimensions.
*
* @param parent frame to which to apply modal property and center within
* (centers within screen if null)
* @param display body of the display, containing current bindings and
* appearance properties
* @param dialogTitle title of the displayed dialog
* @param showLabels if true the top has labels describing the fields,
* otherwise they are omitted
* @param adaptor adaptor used to provide configuring functionality
* @return redefined mapping of keystrokes to their actions, null if cancel
* is pressed
*/
public static LinkedHashMap showDialog(Component parent,
final BindingChooser display, String dialogTitle, boolean showLabels,
BindingAdaptor adaptor)
{
final JDialog dialog = new JDialog();
dialog.setTitle(dialogTitle);
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
dialog.setResizable(false);
if (showLabels)
dialog.add(display.makeLabels(), BorderLayout.NORTH);
dialog.add(display);
// Bottom controls
JPanel controlSection = new TransparentPanel(new GridLayout(1, 0));
// HACK: Uses button's name as a mutable value to determine if pressed
final String PRESSED_STATE = "pressed";
final JButton okButton = new JButton("OK");
okButton.setName("not " + PRESSED_STATE);
okButton.setPreferredSize(new Dimension(
okButton.getPreferredSize().width, 25));
okButton.setFocusable(false);
okButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent event)
{
okButton.setName(PRESSED_STATE);
dialog.dispose();
}
});
controlSection.add(okButton);
final JButton cancelButton = new JButton("Cancel");
cancelButton.setPreferredSize(new Dimension(cancelButton
.getPreferredSize().width, 25));
cancelButton.setFocusable(false);
cancelButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent event)
{
dialog.dispose();
}
});
controlSection.add(cancelButton);
dialog.add(controlSection, BorderLayout.SOUTH);
// Adds listener that closes dialog when pressing 'enter' or 'escape'
dialog.addKeyListener(new KeyAdapter()
{
@Override
public void keyPressed(KeyEvent event)
{
if (display.selectedEntry == null)
{
if (event.getKeyCode() == KeyEvent.VK_ENTER)
{
okButton.doClick();
}
else if (event.getKeyCode() == KeyEvent.VK_ESCAPE)
{
cancelButton.doClick();
}
}
}
});
dialog.addKeyListener(adaptor); // Listener for shortcut field input
dialog.pack();
dialog.setVisible(true);
if (okButton.getName().equals(PRESSED_STATE))
return display.getBindingMap();
else
return null;
}
/**
* This is called upon:
* Component reordering (inherited functionality from BindingPanel)
* Visual changes to the entry
* Component validation
*/
@Override
protected void onUpdate(int index, BindingEntry entry, boolean isNew)
{
this.indentStyle.apply(entry, index);
}
/**
* Invoked on click.
*/
@Override
protected void onClick(MouseEvent event, BindingEntry entry,
BindingEntry.Field field)
{
// Selects shortcut fields when they're clicked
if (field == BindingEntry.Field.SHORTCUT)
{
// Deselects if already selected
if (entry.equals(this.selectedEntry))
setSelected(null);
else
setSelected(entry);
}
}
/**
* Sets if the shortcut fields of entries can be selected to provide editing
* functionality or not. If false, any selected entry is deselected.
*
* @param editable if true shortcut fields may be selected to have their
* values changed, otherwise user input and calls to the
* setSelected method are ignored
*/
public void setEditable(boolean editable)
{
if (!editable && this.selectedEntry != null)
{
setSelected(null); // Deselects current selection
}
this.isShortcutEditable = editable;
}
/**
* Provides the indent style used by the chooser.
*
* @return type of content in the indent field
*/
public IndentStyle getIndentStyle()
{
return this.indentStyle;
}
/**
* Sets content display in the indent field of entries. This will prompt an
* onUpdate on all entries unless setting the style to NONE.
*
* @param style type of content displayed in entry's indent field
*/
public void setIndentStyle(IndentStyle style)
{
this.indentStyle = style;
if (style == IndentStyle.NONE)
return;
ArrayList bindings = getBindings();
for (int i = 0; i < bindings.size(); ++i)
{
onUpdate(i, bindings.get(i), false);
}
}
/**
* Sets the message of the selected shortcut field when awaiting user input.
* By default this is "Press shortcut...".
*
* @param message prompt for user input
*/
public void setSelectedText(String message)
{
if (this.selectedEntry != null)
{
this.selectedEntry.getField(BindingEntry.Field.SHORTCUT).setText(
message);
}
this.selectedText = message;
}
/**
* Returns if a binding is currently awaiting input or not.
*
* @return true if a binding is awaiting input, false otherwise
*/
public boolean isBindingSelected()
{
return this.selectedEntry != null;
}
/**
* Provides the currently selected entry if awaiting input.
*
* @return entry currently awaiting input, if one exists
*/
public BindingEntry getSelected()
{
return this.selectedEntry;
}
/**
* Sets the shortcut field of an entry to prompt user input. The next call
* to doInput sets set its shortcut field and deselects the entry. Any other
* currently selected entry is deselected. If null, then this simply reverts
* any selections (leaving no entry selected). The onUpdate method is called
* whenever an entry is either selected or deselected.
*
* @param entry binding entry awaiting input for its shortcut field
* @throws IllegalArgumentException if entry is not contained in chooser
*/
public void setSelected(BindingEntry entry)
{
if (!this.isShortcutEditable)
return; // Selection can't be changed
if (entry != null && entry.equals(this.selectedEntry))
return; // Entry is already selected
if (entry != null && !getBindings().contains(entry))
{
throw new IllegalArgumentException(
"BindingEntry not contained in display.");
}
BindingEntry previousSelection = this.selectedEntry;
this.selectedEntry = entry;
// Reverts previously selected field's attributes
if (previousSelection != null)
{
onUpdate(getBindingIndex(previousSelection), previousSelection,
false);
previousSelection.setShortcut(previousSelection.getShortcut()); // Reverts
// text
}
// Sets the new selection
if (this.selectedEntry != null)
{
onUpdate(getBindingIndex(this.selectedEntry), this.selectedEntry,
false);
this.selectedEntry.getField(BindingEntry.Field.SHORTCUT).setText(
" " + this.selectedText);
}
}
/**
* Provides a key adaptor that can provide editing functionality for the
* selected entry.
*
* @return binding adaptor configured to this chooser
*/
public BindingAdaptor makeAdaptor()
{
return new BindingAdaptor(this);
}
/**
* Provides the labels naming the fields. These are based on the settings
* when constructed and aren't updated when the display changes. Labels use
* the default entry dimensions.
*
* @return labels used in dialog
*/
public BindingEntry makeLabels()
{
BindingEntry labels = new BindingEntry(null, "");
labels.setOpaque(false);
for (BindingEntry.Field field : BindingEntry.Field.values())
{
JLabel fieldLabel = labels.getField(field);
if (field == BindingEntry.Field.INDENT)
{
// Removes indent field if omitted from the rest of the display.
fieldLabel.setVisible(indentStyle != IndentStyle.EMPTY);
}
else if (field == BindingEntry.Field.ACTION)
{
fieldLabel.setText(" Action:");
}
else if (field == BindingEntry.Field.SHORTCUT)
{
fieldLabel.setText(" Shortcut:");
}
else
{
// BindingEntry.Field has changed and this should be updated
// accordingly
assert false : BindingChooser.class.getName()
+ " doesn't recognize the '" + field + "' field.";
}
}
return labels;
}
/**
* Emulates keyboard input, setting the selected entry's shortcut if an
* entry's currently awaiting input.
*
* @param input keystroke input for selected entry
*/
void doInput(KeyStroke input)
{
if (isBindingSelected())
{
this.selectedEntry.setShortcut(input);
//apply configuration
set.setBindings(this.getBindingMap());
// TYPE indent can change according to the shortcut
// this.indentStyle.apply(this.selectedEntry,
// getBindingIndex(this.selectedEntry));
setSelected(null); // Deselects shortcut field
}
}
@Override
public void validate()
{
super.validate();
ArrayList bindings = getBindings();
for (int i = 0; i < bindings.size(); ++i)
{
onUpdate(i, bindings.get(i), false);
}
}
/**
* Supported appearances of the indent field, which includes:
* NONE- No actions are taken to change the indent field's appearance.
* EMPTY- Indent field is set to be invisible (effectively removing it from
* the display).
* SPACER- Blank field that occupies its currently set dimensions.
* TYPE- Displays Unicode arrows according to the shortcut's event type
* (down for KEY_PRESSED, up for KEY_RELEASED, bidirectional for KEY_TYPED,
* and an 'X' if disabled).
* INDEX- Displays the field's index from the top (starting with one).
*/
public static enum IndentStyle
{
NONE, EMPTY, SPACER, TYPE, INDEX;
/**
* Returns the enum representation of a string. This is case sensitive.
*
* @param str toString representation of this enum
* @return enum associated with a string
* @throws IllegalArgumentException if argument is not represented by
* this enum.
*/
public static IndentStyle fromString(String str)
{
for (IndentStyle type : IndentStyle.values())
{
if (str.equals(type.toString()))
return type;
}
throw new IllegalArgumentException();
}
// Applies this style to the indent field of an entry. This uses a zero
// based index.
private void apply(BindingEntry entry, int index)
{
if (this == NONE)
return;
JLabel indentField = entry.getField(BindingEntry.Field.INDENT);
indentField.setVisible(this != EMPTY);
indentField.setIcon(null);
String fieldText = "";
if (this == TYPE)
{
if (entry.getShortcut() == BindingEntry.DISABLED)
{
fieldText = " X";
}
else
{
int type = entry.getShortcut().getKeyEventType();
if (type == KeyEvent.KEY_PRESSED)
fieldText = " \u2193";
else if (type == KeyEvent.KEY_RELEASED)
fieldText = " \u2191";
else if (type == KeyEvent.KEY_TYPED)
fieldText = " \u2195";
else
{
// Should be unreachable according to the AWTKeyStroke
// class
assert false : "Unrecognized key type: " + type;
fieldText = "";
}
}
}
else if (this == INDEX)
{
fieldText = " " + (index + 1) + ".";
}
indentField.setText(fieldText);
}
@Override
public String toString()
{
if (this == TYPE)
return "Event Type";
return getReadableConstant(this.name());
}
}
/**
* Provides a more readable version of constant names. Spaces replace
* underscores and this changes the input to lowercase except the first
* letter of each word. For instance, "RARE_CARDS" would become
* "Rare Cards".
*
* @param input string to be converted
* @return reader friendly variant of constant name
*/
public static String getReadableConstant(String input)
{
char[] name = input.toCharArray();
boolean isStartOfWord = true;
for (int i = 0; i < name.length; ++i)
{
char chr = name[i];
if (chr == '_')
name[i] = ' ';
else if (isStartOfWord)
name[i] = Character.toUpperCase(chr);
else
name[i] = Character.toLowerCase(chr);
isStartOfWord = chr == '_';
}
return new String(name);
}
}