// Copyright 2012 Georg-August-Universität Göttingen, Germany
//
// 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 de.ugoe.cs.autoquest.eventcore.gui;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.logging.Level;
import de.ugoe.cs.autoquest.eventcore.Event;
import de.ugoe.cs.autoquest.keyboardmaps.VirtualKey;
import de.ugoe.cs.util.console.Console;
/**
*
* This class provides the functionality to sort and clean up all key interactions in a log. In
* particular:
*
* - In case a combination key (e.g., shift, alt, control) is held down, multiple
* {@link KeyPressed} events are logged, even though only the first one is of importance. This class
* removes all {@link KeyPressed} events for combination keys except the first.
* - In case a normal key is held down, multiple {@link KeyPressed} events are logged, but there
* is only one {@link KeyReleased} event. This class adds a {@link KeyReleased} event for all such
* {@link KeyPressed} events.
* - Due to message filtering of applications, it is possible that a {@link KeyReleased} event
* without a preceding {@link KeyPressed} event is logged. This class either adds the missing
* {@link KeyPressed} right in front of the {@link KeyReleased} or removes the {@link KeyReleased}
* depending on the {@link #mode}.
*
- As a result of steps 2-3, we have always a matching {@link KeyPressed}/{@link KeyReleased}
* pairs for all normal keys. This class replaces these pairs with a {@link KeyTyped} event at the
* position of the {@link KeyPressed} event.
* - Sometimes combination keys are not released in the same order they have been pressed. This
* class ensures that the {@link KeyReleased} are in the opposite order of the {@link KeyPressed}
* events for all combination keys.
*
*
*
* @version 1.0
* @author Steffen Herbold
*/
public class KeyInteractionCorrector {
/**
*
* Describes the clean-up mode.
*
*
* @version 1.0
* @author Steffen Herbold
*/
public static enum CleanupMode {
/**
*
* Single {@link KeyReleased} are removed from the sequence.
*
*/
REMOVAL,
/**
*
* {@link KeyPressed} events are added to single {@link KeyReleased} events
*
*/
ADDITION
};
/**
*
*
*
*/
private final CleanupMode mode;
/**
*
* Constructor. Creates a new {@link KeyInteractionCorrector} with {@link #mode}=
* {@link CleanupMode#ADDITION}.
*
*/
public KeyInteractionCorrector() {
this(CleanupMode.ADDITION);
}
/**
*
* Constructor. Creates a new {@link KeyInteractionCorrector}.
*
*
* @param mode
* {@link #mode} of the instance
*/
public KeyInteractionCorrector(CleanupMode mode) {
this.mode = mode;
}
/**
*
* Sorts and cleans up key interactions according to the class specification (@see
* {@link KeyInteractionCorrector} class comment).
*
*
* This method returns a sorted copy of a sequence, the sequence itself is not changed.
*
*
* @param sequence
* sequence which is sorted
* @return sorted copy of sequence
*/
public List sortKeyInteractions(final List sequence) {
List sortedSequence = new LinkedList(sequence);
handleIncompleteKeyPairs(sortedSequence);
sortCombinationKeyPairs(sortedSequence);
return sortedSequence;
}
/**
*
* Performs tasks 1-4 defined in the class description. Operations are performed in-place on the
* passed sequence.
*
*
* @param sequence
* sequence which is sorted
*/
private void sortCombinationKeyPairs(List sequence) {
LinkedList pressedCombinationKeys = new LinkedList();
for (int i = 0; i < sequence.size(); i++) {
Event event = sequence.get(i);
if (event.getType() instanceof KeyPressed) {
final VirtualKey key = ((KeyPressed) event.getType()).getKey();
if (key.isCombinationKey()) {
pressedCombinationKeys.add(key);
}
}
if (event.getType() instanceof KeyReleased) {
final VirtualKey key = ((KeyReleased) event.getType()).getKey();
if (key.isCombinationKey()) {
/*
* if( pressedCombinationKeys.isEmpty() ) { Console.traceln(Level.INFO, "" + i);
* for( int j=i-30 ; j<=i ; j++ ) { Console.traceln(Level.INFO,
* sequence.get(i).toString()); } }
*/
if (key.equals(pressedCombinationKeys.getLast())) {
pressedCombinationKeys.removeLast();
}
else {
// look-ahead to find new position
int offset;
for (offset = 1; offset + i < sequence.size(); offset++) {
Event lookaheadEvent = sequence.get(i + offset);
if (lookaheadEvent.getType() instanceof KeyReleased) {
if (((KeyReleased) lookaheadEvent.getType()).getKey()
.equals(pressedCombinationKeys.getLast()))
{
break;
}
}
}
sequence.add(i + offset + 1, event);
sequence.remove(i);
i--;
}
}
}
}
}
/**
*
* Performs task 5 defined in the class description. Operations are performed in-place on the
* passed sequence.
*
*
* @param sequence
* sequence which is sorted
*/
private void handleIncompleteKeyPairs(List sequence) {
Set pressedKeys = new HashSet();
int firstPressedIndex = -1;
Set pressedCombinationKeysSession = new HashSet();
for (int i = 0; i < sequence.size(); i++) {
Event event = sequence.get(i);
if (event.getType() instanceof KeyboardFocusChange) {
if (firstPressedIndex != -1) {
sequence.remove(i);
sequence.add(firstPressedIndex, event);
}
}
if (event.getType() instanceof KeyPressed) {
if (pressedKeys.isEmpty()) {
firstPressedIndex = i;
}
VirtualKey key = ((KeyPressed) event.getType()).getKey();
if (!key.isCombinationKey()) {
ListIterator iter = sequence.listIterator(i);
iter.next();
iter.set(new Event(new KeyTyped(key), event.getTarget()));
}
else {
pressedCombinationKeysSession.add(key);
if (pressedKeys.contains(key)) {
sequence.remove(i);
i--;
}
}
pressedKeys.add(key);
}
if (event.getType() instanceof KeyReleased) {
VirtualKey key = ((KeyReleased) event.getType()).getKey();
if (!key.isCombinationKey()) {
if (pressedKeys.contains(key)) {
sequence.remove(i);
i--;
}
else {
// found KeyReleased event without KeyPressed
switch (mode)
{
case REMOVAL:
sequence.remove(i);
i--;
break;
case ADDITION:
ListIterator iter = sequence.listIterator(i);
iter.next();
iter.set(new Event(new KeyTyped(key), event.getTarget()));
break;
default:
throw new AssertionError(
"reached source code that should be unreachable");
}
}
}
else {
if (!pressedKeys.contains(key)) {
if (pressedCombinationKeysSession.contains(key)) {
Console.traceln(Level.SEVERE, "Found a " + key +
" KeyReleased event without a KeyPressed event." +
"The event will be dropped and the session is possibly faulty.");
sequence.remove(i);
i--;
}
else {
Console
.traceln(Level.SEVERE,
"Found a " +
key +
" KeyReleased event without a KeyPressed event." +
"Since no KeyPressed of key " +
key +
" has been part of the session " +
"till now, we assume that the key has been pressed since the beginning " +
"of the session and add a KeyPressed event for " +
key + " to the start " + "of the session.");
sequence.add(0, new Event(new KeyPressed(key), event.getTarget()));
i++;
}
}
}
pressedKeys.remove(key);
if (pressedKeys.isEmpty()) {
firstPressedIndex = -1;
}
}
}
if (!pressedKeys.isEmpty()) {
Console
.traceln(Level.WARNING,
"There was probably a failure during the handling of incomplete key event pairs.");
}
}
}