org.apache.pivot.wtk.RadioButtonGroup Maven / Gradle / Ivy
The newest version!
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you 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 org.apache.pivot.wtk;
import java.util.Iterator;
import org.apache.pivot.collections.ArrayList;
import org.apache.pivot.collections.Group;
import org.apache.pivot.collections.List;
import org.apache.pivot.collections.Sequence;
import org.apache.pivot.util.Filter;
import org.apache.pivot.util.ImmutableIterator;
import org.apache.pivot.wtk.Keyboard.KeyCode;
import org.apache.pivot.wtk.Keyboard.KeyLocation;
import org.apache.pivot.wtk.Keyboard.Modifier;
/**
* Extension of {@link ButtonGroup} providing keyboard navigation within the
* group and modified focus navigation that treats the group as a single
* focusable entity.
*
* {@link KeyCode#UP UP} & {@link KeyCode#LEFT LEFT} Select the previous
* button
* {@link KeyCode#DOWN DOWN} & {@link KeyCode#RIGHT RIGHT} Select the next
* button
* {@link KeyCode#HOME HOME} Select the first button
* {@link KeyCode#END END} Select the last button
*
* (Note that only {@link Component#isFocusable() focusable} buttons are
* considered when searching for a Button to select)
*
* When a button within the group is focused and key is typed, an attempt is
* made to find the next button (default) or previous button (when the SHIFT
* modifier is pressed) whose renderer text starts with the typed character.
* This search will always behave as if the circular
property were
* set.
*
* By default, {@link KeyCode#TAB TAB} and {@link KeyCode#TAB SHIFT+TAB}
* key presses will transfer focus out of the group (forwards or backwards
* respectively).
* This is managed by the {@link #setIntraGroupFocusTransferEnabled(boolean)
* intraGroupFocusTransferEnabled} property.
*
* The {@link #setCircular(boolean) circular} property can be enabled to allow
* the selection to transfer seamlessly from one end of the group to the other.
* (i.e. holding down an arrow key will cycle through all focusable buttons)
*
*
* Note that due to the conflicting return types of the add(T)
and
* remove(T)
methods in the {@link Group#add(Object) Group} and
* {@link Sequence#add(Object) Sequence} interfaces, this class cannot actually
* implement Sequence<Button>
, although most of the same
* methods are implemented.
*/
public class RadioButtonGroup extends ButtonGroup {
/**
* Filter used to determine selectable buttons whose rendered data starts
* with the target character.
*/
private class FirstCharacterFilter implements Filter {
private char target = '\0';
public void setTarget(char target) {
this.target = Character.toUpperCase(target);
}
@Override
public boolean include(Integer index) {
boolean include = defaultFilter.include(index);
if (include) {
Button button = buttonOrder.get(index);
String rendered = button.getDataRenderer().toString(button.getButtonData());
if (rendered != null && rendered.length() > 0) {
char first = Character.toUpperCase(rendered.charAt(0));
if (first != target) {
include = false;
}
}
}
return include;
}
}
/**
* ComponentKeyListener to be applied to all buttons as they are added to
* the group.
*
* At least one button in the group must be focused for this listener to be
* executed, but that won't necessarily be a selected button.
* This also means that the group will not be empty, although some of the
* buttons contained within may not be focusable, or even visible.
*/
private final ComponentKeyListener componentKeyListener = new ComponentKeyListener.Adapter() {
/**
* Handle TAB & SHIFT+TAB focus traversal, HOME, END & arrow keys
*/
@Override
public boolean keyPressed(Component component, int keyCode, KeyLocation keyLocation) {
int modifiers = Keyboard.getModifiers();
boolean handled = false;
/*
* Potentially transfer focus away from the buttons in this group.
*
* At this point we know that at least one button is focused, so we
* just need to find the first or last (and possibly only) focusable
* button depending on the focus transfer direction and then
* transfer away from it.
*/
if (!intraGroupFocusTransferEnabled) {
if (keyCode == KeyCode.TAB) {
if (modifiers == 0) {
Button lastFocusableButton = get(findPrevious(buttonOrder.getLength()));
lastFocusableButton.transferFocus(FocusTraversalDirection.FORWARD);
handled = true;
} else if (modifiers == Modifier.SHIFT.getMask()) {
Button firstFocusableButton = get(findNext(NO_SELECTION_INDEX));
firstFocusableButton.transferFocus(FocusTraversalDirection.BACKWARD);
handled = true;
}
}
}
// Navigation/selection within the group
if (!handled && modifiers == 0) {
RadioButtonGroup radioButtonGroup = RadioButtonGroup.this;
Button selectedButton = radioButtonGroup.getSelection();
handled = true;
if (keyCode == Keyboard.KeyCode.HOME) {
radioButtonGroup.selectFirstButton();
} else if (keyCode == Keyboard.KeyCode.END) {
radioButtonGroup.selectLastButton();
} else if (keyCode == Keyboard.KeyCode.LEFT || keyCode == Keyboard.KeyCode.UP) {
radioButtonGroup.selectPreviousButton(selectedButton);
} else if (keyCode == Keyboard.KeyCode.RIGHT || keyCode == Keyboard.KeyCode.DOWN) {
radioButtonGroup.selectNextButton(selectedButton);
} else {
handled = false;
}
}
return handled;
}
/**
* Attempt to jump to the button whose rendered text begins with the
* typed character.
*/
@Override
public boolean keyTyped(Component component, char character) {
int modifiers = Keyboard.getModifiers();
boolean handled = false;
// We are only interested when a key is typed with no modifier, or
// just SHIFT (which is used to reverse the search direction)
boolean noModifiersPressed = (modifiers == 0);
boolean shiftPressed = (modifiers == Modifier.SHIFT.getMask());
if (noModifiersPressed || shiftPressed) {
RadioButtonGroup radioButtonGroup = RadioButtonGroup.this;
Button selectedButton = radioButtonGroup.getSelection();
firstCharacterFilter.setTarget(character);
// Determine the starting point for the search
int searchStartIndex;
if (selectedButton != null) {
searchStartIndex = radioButtonGroup.indexOf(selectedButton);
} else {
if (noModifiersPressed) {
searchStartIndex = NO_SELECTION_INDEX;
} else {
searchStartIndex = buttonOrder.getLength();
}
}
int result = NOT_FOUND_INDEX;
if (noModifiersPressed) {
result = radioButtonGroup.findNext(searchStartIndex, firstCharacterFilter, true);
} else if (shiftPressed) {
result = radioButtonGroup.findPrevious(searchStartIndex, firstCharacterFilter,
true);
}
// Consider the event to have been handled if a different
// button end up being selected
if (result != NOT_FOUND_INDEX && result != searchStartIndex) {
radioButtonGroup.setSelection(result);
handled = true;
}
}
return handled;
}
};
/**
* Ensure that all buttons in this group have the custom
* ComponentKeyListener.
* This relies on the logic within ButtonGroup to prevent duplicates.
*/
private final ButtonGroupListener buttonGroupListener = new ButtonGroupListener.Adapter() {
@Override
public void buttonAdded(ButtonGroup buttonGroup, Button button) {
button.getComponentKeyListeners().add(componentKeyListener);
}
@Override
public void buttonRemoved(ButtonGroup buttonGroup, Button button) {
button.getComponentKeyListeners().remove(componentKeyListener);
}
};
/**
* Filter used to determine selectable buttons within the group
*/
private final Filter defaultFilter = new Filter() {
@Override
public boolean include(Integer index) {
Button button = buttonOrder.get(index);
boolean focusable = button.isFocusable();
return focusable;
}
};
private final FirstCharacterFilter firstCharacterFilter = new FirstCharacterFilter();
private final List