/* Copyright (C) 2005-2008 Remon Sijrier This file is part of Traverso Traverso is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "InputEngine.h" #include "ContextItem.h" #include "ContextPointer.h" #include "Information.h" #include "Command.h" #include #include "Utils.h" #include #include #include #include #include #include #include #include // Always put me below _all_ includes, this is needed // in case we run with memory leak detection enabled! #include "Debugger.h" #define MAX_TIME_DIFFERENCE_FOR_DOUBLE_KEY_CONSIDERATION 50 #define MouseScrollHorizontalLeft -1 #define MouseScrollHorizontalRight -2 #define MouseScrollVerticalUp -3 #define MouseScrollVerticalDown -4 /** * \class InputEngine * \brief Processes keyboard/mouse events, dispatches the result, and handles the returned Command objects * InputEngine forms, together with ViewPort, Command, ContextPointer, ContextItem
and Qt's Undo Framework, the framework that makes up the the Contextual
Interaction Interface, with analog type of actions, and un/redo (aka History) support. Dispatching key facts to ContextItem objects InputEngine parses the keyboard/mouse events generated by the pointed ViewPort
If the keysequence matches that of any given in the keymap file, it call's
broadcast_action(). A list of pointed ContextItem objects is retrieved then
from ContextPointer. This list represents all (gui) ContextItem objects with their
corresponding 'core' ContextItem objects, stacked, with the topmost gui object on top. For each ContextItem in the list, the class name is retreived, and looked up in the keymap
if for the detected key fact an object was supplied with the exact same name.
If this is the case, the function name, also given in the keymap file by the given object name
is used to call the ContextItem's function. If succesfull, the InputEngine will stop iterating
over the list, and start handling the returned Command object. If the keymap specified that the object's doesn't have a function (slot) to be called, but instead
uses a CommandPlugin, the list of loaded CommandPlugins is searched to find a match for the
plugin name supplied in the keymap file, if there is a match, the Plugin is used to create
the Command object, and the same routine is used to handle this Command object. If the Command object returned no error during the handling, it'll be placed on it's
historystack. If no historystack was available, it's do_action() will be called, and
deleted afterwards. * \sa Command, ContextPointer, ViewPort, CommandPlugin */ static void set_hexcode(int & variable, const QString& text) { variable = 0; QString s; int x = 0; if ((text != QString::null) && (text.length() > 0) ) { s="ABCDEFGHIJKLMNOPQRSTUVWXYZ"; x = s.indexOf(text); if (x>=0) { variable = Qt::Key_A + x; } else { s="|ESC |TAB |BACKTAB |BKSPACE |RETURN |ENTER |INSERT |DELETE " "|PAUSE |PRINT |SYSREQ |CLEAR "; x = s.indexOf("|" + text); if (x>=0) variable = Qt::Key_Escape + (x/9); else { s="|HOME |END |LARROW |UARROW |RARROW " "|DARROW |PRIOR |NEXT "; x = s.indexOf("|" + text); if (x>=0) variable = Qt::Key_Home + (x/9); else { s="|SHIFT |CTRL |META |ALT |CAPS " "|NUMLOCK |SCROLL "; x = s.indexOf("|" + text); if (x>=0) variable = Qt::Key_Shift + (x/9); else { s="F1 F2 F3 F4 F5 F6 F7 F8 F9 F10F11F12"; x=s.indexOf(text); if (x>=0) { variable = Qt::Key_F1 + (x/3); } else if (text=="SPACE") { variable = Qt::Key_Space; } else if (text == "MouseButtonLeft") { variable = Qt::LeftButton; } else if (text == "MouseButtonRight") { variable = Qt::RightButton; } else if (text == "MouseButtonMiddle") { variable = Qt::MidButton; } else if (text == "MouseButtonX1") { variable = Qt::XButton1; } else if (text == "MouseButtonX2") { variable = Qt::XButton2; } else if (text == "MouseScrollHorizontalLeft") { variable = MouseScrollHorizontalLeft; } else if (text =="MouseScrollHorizontalRight") { variable = MouseScrollHorizontalRight; } else if (text == "MouseScrollVerticalUp") { variable = MouseScrollVerticalUp; } else if( text == "MouseScrollVerticalDown") { variable = MouseScrollVerticalDown; } else { PERROR("No HEX code found for %s", QS_C(text)); } } } } } } PMESG3("HEXCODE FOR %s=%d", QS_C(text), variable); } InputEngine& ie() { static InputEngine inputengine; return inputengine; } InputEngine::InputEngine() { PENTERCONS; holdingCommand = 0; // holdEvenCode MUST be a value != ANY key code! // when set to 'not matching any key!!!!!! holdEventCode = -100; isJogging = false; reset(); clearTime = 2000; assumeHoldTime = 200; // it will wait a release for 200 ms. Otherwise it will assume a hold doubleFactWaitTime = 200; collectedNumber = -1; sCollectedNumber = "-1"; activate(); //#define profile #if defined (profile) trav_time_t starttime = get_microseconds(); #endif foreach (QObject* obj, QPluginLoader::staticInstances()) { CommandPlugin* plug = qobject_cast(obj); m_commandplugins.insert(plug->metaObject()->className(), plug); } #if !defined (STATIC_BUILD) QDir pluginsDir("lib/commandplugins"); foreach (const QString &fileName, pluginsDir.entryList(QDir::Files)) { QPluginLoader loader(pluginsDir.absoluteFilePath(fileName)); CommandPlugin* plug = qobject_cast(loader.instance()); if (plug) { m_commandplugins.insert(plug->metaObject()->className(), plug); printf("InputEngine:: Succesfully loaded plugin: %s\n", plug->metaObject()->className()); } else { printf("InputEngine:: Plugin load failed with %s\n", QS_C(loader.errorString())); } } #endif #if defined (profile) int processtime = (int) (get_microseconds() - starttime); printf("InputEngine::Plugin load time: %d useconds\n\n", processtime); #endif } InputEngine::~ InputEngine( ) { foreach(IEAction* action, m_ieActions) { delete action; } } void InputEngine::activate() { PENTER3; isFirstFact=true; active=true; } void InputEngine::suspend() { PENTER3; active=false; set_jogging(false); } int InputEngine::broadcast_action_from_contextmenu(const QString& keySequence) { PENTER2; IEAction* action = 0; foreach(IEAction* ieaction, m_ieActions) { if (ieaction->keySequence == keySequence) { action = ieaction; break; } } if (keySequence.contains("++")) { info().information(tr("Modifier key actions are not supported from Context Menu")); return -1; } if (! action) { PERROR("ContextMenu keySequence doesn't apply to any InputEngine knows off!! (%s)", QS_C(keySequence)); return -1; } if ( action && ((action->type == HOLDKEY) || (action->type == HKEY2))) { info().information(tr("Hold actions are not supported from Context Menu")); return -1; } return broadcast_action(action, false, true); } int InputEngine::broadcast_action(IEAction* action, bool autorepeat, bool fromContextMenu) { PENTER2; Command* k = 0; QObject* item = 0; int useX=0, useY=0; QList list; if ( ! fromContextMenu ) { list = cpointer().get_context_items(); } else { list = cpointer().get_contextmenu_items(); } QString slotsignature = ""; if (holdingCommand) { list.prepend(holdingCommand); } PMESG("Trying to find IEAction for key sequence %s", action->keySequence.data()); for (int i=0; i < list.size(); ++i) { k = 0; m_broadcastResult = 0; item = list.at(i); if (!item) { PERROR("no item in cpointer()'s context item list ??"); continue; } IEAction::Data* data = action->objectUsingModifierKeys.value(QString(item->metaObject()->className())); // A match was found for actions using the modifier key // let's see if it is valid for the current active modifier keys! if (data) { PMESG("found match in objectUsingModierKeys"); bool modifierkeymatch = true; if (data->modifierkeys.size()) { foreach(int key, data->modifierkeys) { if ( ! m_activeModifierKeys.contains(key)) { PMESG("m_activeModifierKeys doesn't contain code %d", key); modifierkeymatch = false; break; } } } else { modifierkeymatch = false; } if (! modifierkeymatch) { data = 0; } } // No match found for actions using a modifier key, let's see if there // is one in the 'normal' actions list. if (! data ) { // This test makes sure that we don't select an unmodified command // when the user is holding down modifier keys. if (m_activeModifierKeys.size() > 0) { continue; } data = action->objects.value(QString(item->metaObject()->className())); if (! data ) { PMESG("No data found for object %s", item->metaObject()->className()); continue; } } // Now that we found a match, we still have to check if // the current mode is valid for this data! QString currentmode = m_modes.key(cpointer().get_current_mode()); QString allmodes = m_modes.key(0); if ( (! data->modes.contains(currentmode)) && (! data->modes.contains(allmodes))) { PMESG("%s on %s is not valid for mode %s", action->keySequence.data(), item->metaObject()->className(), QS_C(currentmode)); continue; } PMESG("Data found for %s!", item->metaObject()->className()); PMESG("setting slotsignature to %s", QS_C(data->slotsignature)); PMESG("setting pluginname to %s", QS_C(data->pluginname)); PMESG("setting plugincommand to %s", QS_C(data->commandname)); QString pluginname = "", commandname = ""; slotsignature = data->slotsignature; pluginname = data->pluginname; commandname = data->commandname; useX = data->useX; useY = data->useY; if (item == holdingCommand) { if (QMetaObject::invokeMethod(item, QS_C(slotsignature), Qt::DirectConnection, Q_ARG(bool, autorepeat))) { PMESG("HIT, invoking %s::%s", holdingCommand->metaObject()->className(), QS_C(slotsignature)); break; } } // We first try to find if there is a match in the loaded plugins. if ( ! holdingCommand ) { if ( ! pluginname.isEmpty() ) { CommandPlugin* plug = m_commandplugins.value(pluginname); if (!plug) { info().critical(tr("Command Plugin %1 not found!").arg(pluginname)); } else { if ( ! plug->implements(commandname) ) { info().critical(tr("Plugin %1 doesn't implement Command %2") .arg(pluginname).arg(commandname)); } else { PMESG("InputEngine:: Using plugin %s for command %s", QS_C(pluginname), QS_C(data->commandname)); k = plug->create(item, commandname, data->arguments); } } } } // Either the plugins didn't have a match, or were holding. if ( ! k ) { IEAction::Data* delegatingdata; QString delegatedobject; if (holdingCommand) { delegatingdata = action->objects.value("HoldCommand"); delegatedobject = "HoldCommand"; } else { delegatedobject = item->metaObject()->className(); if (m_activeModifierKeys.size() > 0) { delegatingdata = action->objectUsingModifierKeys.value(delegatedobject); } else { delegatingdata = action->objects.value(delegatedobject); } PMESG("delegatedobject is %s", QS_C(delegatedobject)); } if ( ! delegatingdata) { PMESG("No delegating data ? WEIRD"); continue; } QStringList strlist = delegatingdata->slotsignature.split("::"); if (strlist.size() == 2) { PMESG("Detected delegate action, checking if it is valid!"); QString classname = strlist.at(0); QString slot = strlist.at(1); QObject* obj = 0; bool validobject = false; for (int j=0; j < list.size(); ++j) { obj = list.at(j); if (obj->metaObject()->className() == classname) { PMESG("Found an item in the contextitem list that equals delegated object"); validobject = true; break; } } if (validobject) { if (QMetaObject::invokeMethod(obj, QS_C(slot), Qt::DirectConnection, Q_RETURN_ARG(Command*, k))) { PMESG("HIT, invoking (delegated) %s::%s", QS_C(classname), QS_C(slot)); } else { PMESG("Delegated object slot call didn't work out, sorry!"); PMESG("%s::%s() --> %s::%s()", item->metaObject()->className(), QS_C(slot), QS_C(classname), QS_C(slot)); } } else { PMESG("Delegated object %s was not found in the context items list!", QS_C(classname)); } } else { if (QMetaObject::invokeMethod(item, QS_C(slotsignature), Qt::DirectConnection, Q_RETURN_ARG(Command*, k))) { PMESG("HIT, invoking %s::%s", item->metaObject()->className(), QS_C(slotsignature)); } else { PMESG("nope %s wasn't the right one, next ...", item->metaObject()->className()); } } } // Let's see if the ContextItem used either succes(), failure() or did_not_implement() // return functions, so we can detect to either return happily, the action was succesfull // but no command object needed to be returned, the action was not succesfull, and we // don't want to try lower level context items or the action was succesfull but we'd like // to give a lower level contextitem precedence over the current one. if (m_broadcastResult) { if (m_broadcastResult == SUCCES) { PMESG("Broadcast Result indicates succes, but no returned Command object"); return 1; } if (m_broadcastResult == FAILURE) { PMESG("Broadcast Result indicates failure, and doesn't want lower level items to be processed"); return 0; } if (m_broadcastResult == DIDNOTIMPLEMENT) { PMESG("Broadcast Result indicates succes, but didn't want to perform it's action," "so we continue traversing the contextitem list"); continue; } } if (k && (!isHolding)) { if (k->prepare_actions() != -1) { k->set_valid(true); if (k->push_to_history_stack() < 0) { // The command doesn't have a history stack, or wasn't // historable for some reason.... At least call do_action // since that still isn't done (should be done by QUndoStack...) k->do_action(); delete k; k = 0; } } else { PWARN("prepare actions failed!"); delete k; k = 0; } } if (k && isHolding) { if (k->begin_hold() != -1) { k->set_valid(true); k->set_cursor_shape(useX, useY); holdingCommand = k; set_jogging(true); } else { PERROR("hold action begin_hold() failed!"); // OOPSSS, something went wrong when making the Command // set following stuff to zero to make finish_hold do nothing delete k; k = 0; set_jogging( false ); wholeMapIndex = -1; } } break; } return 1; } Command* InputEngine::succes() { m_broadcastResult = SUCCES; return 0; } Command* InputEngine::failure() { m_broadcastResult = FAILURE; return 0; } Command* InputEngine::did_not_implement() { m_broadcastResult = DIDNOTIMPLEMENT; return 0; } void InputEngine::jog() { PENTER3; if (isJogging) { if (holdingCommand) { if (m_bypassJog) { QPoint diff = m_jogBypassPos - cpointer().pos(); if (diff.manhattanLength() > m_unbypassJogDistance) { m_bypassJog = false; } else { return; } m_jogBypassPos = cpointer().pos(); } holdingCommand->jog(); } } } void InputEngine::bypass_jog_until_mouse_movements_exceeded_manhattenlength(int length) { m_unbypassJogDistance = length; m_bypassJog = true; m_jogBypassPos = cpointer().pos(); } void InputEngine::set_jogging(bool jog) { if (jog) { cpointer().jog_start(); } else { cpointer().jog_finished(); } isJogging = jog; } bool InputEngine::is_jogging() { return isJogging; } // JMB ENGINE : EVENT LEVEL HANDLING ------------------------------- // reset the event stack and the press 'stack' (not exactly a stack) void InputEngine::reset() { PENTER3; isFirstFact = true; isDoubleKey = false; fact1_k1 = 0; fact1_k2 = 0; isHolding = false; isPressEventLocked = false; m_cancelHold = false; stackIndex = 0; pressEventCounter = 0; fact2_k1 = 0; fact2_k2 = 0; wholeMapIndex = -1; m_bypassJog = false; for (int i=0; i < STACK_SIZE; i++) { eventType[i] = 0; eventStack[i] = 0; eventTime[i] = 0; } } void InputEngine::clear_modifier_keys() { m_activeModifierKeys.clear(); } // Everthing starts here. Catch event takes anything happen in the keyboard // and pushes it into a stack. void InputEngine::catch_key_press(QKeyEvent * e ) { if (e->isAutoRepeat() && !isHolding) { return; } PENTER4; process_press_event(e->key(), e->isAutoRepeat()); } void InputEngine::catch_key_release( QKeyEvent * e) { if (e->isAutoRepeat()) { return; } PENTER4; process_release_event(e->key()); } void InputEngine::catch_mousebutton_press( QMouseEvent * e ) { process_press_event(e->button()); } void InputEngine::catch_mousebutton_release( QMouseEvent * e ) { process_release_event(e->button()); } void InputEngine::catch_mousebutton_doubleclick( QMouseEvent * e ) { process_press_event(e->button()); process_release_event(e->button()); process_press_event(e->button()); process_release_event(e->button()); } void InputEngine::catch_scroll(QWheelEvent* e) { if (e->orientation() == Qt::Horizontal) { if (e->delta() > 0) { } if (e->delta() < 0) { } } else { if (e->delta() > 0) { process_press_event(MouseScrollVerticalUp); process_release_event(MouseScrollVerticalUp); } if (e->delta() < 0) { process_press_event(MouseScrollVerticalDown); process_release_event(MouseScrollVerticalDown); } } } void InputEngine::process_press_event(int eventcode, bool isAutoRepeat) { if (eventcode == Qt::Key_Escape && is_holding()) { m_cancelHold = true; finish_hold(); return; } // first check if this fact is just a collected number if (check_number_collection(eventcode)) { // another digit was collected. return; } if (is_modifier_keyfact(eventcode)) { if ( (! isAutoRepeat) && (! m_activeModifierKeys.contains(eventcode)) ) { m_activeModifierKeys.append(eventcode); } return; } if (isFirstFact && !isHolding) { cpointer().inputengine_first_input_event(); if (eventStack[0] == 0) { // Here we jump straight to the command if "K" is unambiguously an FKEY int fkey_index = find_index_for_instant_fkey(eventcode); if (fkey_index >= 0) { catcher.holdTimer.stop(); // quit the holding check.. IEAction* action = m_ieActions.at(fkey_index); broadcast_action(action, isAutoRepeat); conclusion(); return; } } else { // Here we jump straight to the command if "KL" is unambiguously an FKEY2 int fkey2_index = find_index_for_instant_fkey2(eventcode, eventStack[0]); if (fkey2_index >= 0) { catcher.holdTimer.stop(); // quit the holding check.. IEAction* action = m_ieActions.at(fkey2_index); broadcast_action(action, isAutoRepeat); conclusion(); return; } } } if (isHolding) { int index = find_index_for_single_fact(FKEY, eventcode, 0); // PRE-CONDITION: // The eventcode must be bind to a single key fact AND // the eventcode must be != the current active holding // command's eventcode! if (index >= 0 && holdEventCode != eventcode) { IEAction* action = m_ieActions.at(index); broadcast_action(action, isAutoRepeat); } return; } if (!isPressEventLocked) { if (pressEventCounter < 2) { pressEventCounter++; push_event(PRESS_EVENT, eventcode); press_checker(); if (!catcher.holdTimer.isActive()) catcher.holdTimer.start( assumeHoldTime); // single shot timer } else { isPressEventLocked = true; } } } void InputEngine::process_release_event(int eventcode) { if (is_modifier_keyfact(eventcode)) { m_activeModifierKeys.removeAll(eventcode); return; } if (isHolding) { if (eventcode != holdEventCode) { PMESG("release event during hold action, but NOT for holdaction itself!!"); return; } else { PMESG("release event for hold action detected!"); holdEventCode = -100; } } if ((!is_fake(eventcode)) && (stackIndex != 0)) { push_event(RELEASE_EVENT, eventcode); release_checker(); } } // This pushes an event to the stack void InputEngine::push_event( int pType, int pCode ) { PENTER3; if (stackIndex < STACK_SIZE) { eventType[stackIndex] = pType; eventStack[stackIndex] = pCode; QTime currTime = QTime::currentTime(); long ts=currTime.msec() + (currTime.second() * 1000) + (currTime.minute() * 1000 * 60); eventTime[stackIndex] = ts; PMESG3("Pushing EVENT %d (%s) key=%d at %ld",stackIndex,( pType==PRESS_EVENT ? "PRESS" : "RELEASE" ),pCode,ts); stackIndex++; for (int j=0; jK>K action // where user typed too fast, so the release of 1st P happend AFTER the // 2nd press. In this case, I have to assume this is a >K>K // action, and dispatch TWO facts. // Now forcing prematurally a 2 facts by splitting and syncronizing // these events into 2 different facts. Probably this is a >K>K // of course, this >K>K assumption mentioned above can be made ONLY if it is the first fact. if (isFirstFact) { PMESG("Inital double key too slow. It must be a premature >K>K. Dispatching 2 individual facts."); int f1_k1=eventStack[0]; int f2_k1=eventStack[1]; push_fact (f1_k1,0); push_fact (f2_k1,0); } } } // ----------------------------- JMB ENGINE : PRESS LEVEL HANDLING ------------------------- void InputEngine::push_fact(int k1,int k2) { PENTER3; PMESG3("Pushing FACT : k1=%d k2=%d",k1,k2); catcher.holdTimer.stop(); // quit the holding check.. if (isFirstFact) { // this is the first fact PMESG3("First fact detected !"); // first try to find some action like k1k2 fact1_k1 = k1; fact1_k2 = k2; int mapIndex = identify_first_fact(); if (mapIndex < 0) { PMESG3("First fact alone does not match anything in the map. Waiting for a second fact..."); // Action not identified. Maybe is part of a double fact action. so... give_a_chance_for_second_fact(); return; } PMESG3("First fact matches map in position %d",mapIndex); // there is a single-fact action which matches this !! now must check if is not an immediate action if (!m_ieActions.at(mapIndex)->isInstantaneous) { PMESG3("Although this could be an SINGLE PRESS Action, it is not protected, so..."); // action is not an immediate action, so... give_a_chance_for_second_fact(); return; } // Action exists AND it is a immediate action. So // forces it to be a single fact action PMESG3("This is protected (immediate) action. It'll be treated as "); dispatch_action(mapIndex); conclusion(); } else // ok . We are in the second fact. { catcher.secondChanceTimer.stop(); fact2_k1 = k1; fact2_k2 = k2; if (fact2_k1!=0) { // this is the second press PMESG3("Second fact detected !"); } // try to complement the first press. wholeMapIndex = identify_first_and_second_facts_together(); if (wholeMapIndex >= 0) { PMESG3("First and second facts together matches with action %d !! Dispatching it...",wholeMapIndex ); dispatch_action(wholeMapIndex); } else { PMESG3("Apparently, first and second facts together do not match any action. Sorry :-("); } conclusion(); } } int InputEngine::identify_first_fact() { PENTER3; fact1Type = 0; // First we need to know the first fact type. if (fact1_k2==0) // or [K] { if (!isHolding) { PMESG3("Detected "); fact1Type = FKEY; } else { PMESG3("Detected [K]"); fact1Type = HOLDKEY; holdEventCode = fact1_k1; } } else // or [KK] { if (!isHolding) { PMESG3("Detected "); fact1Type = FKEY2; } else { PMESG3("Detected [KK]"); fact1Type = HKEY2; holdEventCode = fact1_k2; } } // Fact 1 Type identified . int index = find_index_for_single_fact(fact1Type, fact1_k1, fact1_k2); if (index >= 0) { return index; } PMESG3("No single fact candidate action found. Keep going, since a 2nd fact might come soon"); give_a_chance_for_second_fact(); return -1; } // Only return a valid index if there is an FKEY defined for key, and there are no // other conflicting keyfacts (HOLDKEY, FKEY2, etc) int InputEngine::find_index_for_instant_fkey( int key ) { int fkey_index = find_index_for_single_fact(FKEY, key, 0); if (fkey_index < 0) { return -1; } foreach(IEAction* action, m_ieActions) { if (action->type == FKEY) continue; if (action->fact1_key1==key || action->fact1_key2==key) { PMESG3("Found a conflict (%s) for instantaneous keyfact key=%d", action->keySequence.data(), key); return -1; } } return fkey_index; } // Only return a valid index if there is an FKEY2 defined for key1, key2, and there are no // other conflicting keyfacts (HOLDKEY2, etc) int InputEngine::find_index_for_instant_fkey2( int key1, int key2 ) { int fkey2_index = find_index_for_single_fact(FKEY2, key1, key2); if (fkey2_index < 0) { return -1; } foreach(IEAction* action, m_ieActions) { if (action->type == D_FKEY2 || action->type == HKEY2) { if ( (action->fact1_key1==key1 && action->fact1_key2==key2) || (action->fact1_key1==key2 && action->fact1_key2==key1) ) { PMESG3("Found a conflict (%s) for instantaneous keyfact keys=%d,%d", action->keySequence.data(), key1, key2); return -1; } } } return fkey2_index; } int InputEngine::find_index_for_single_fact( int type, int key1, int key2 ) { foreach(IEAction* action, m_ieActions) { if (action->type != type ) continue; if ( ( ((action->fact1_key1==key1) && ( action->fact1_key2==key2)) || ((action->fact1_key1==key2) && ( action->fact1_key2==key1)) ) && ( action->fact2_key1 == 0 ) && ( action->fact2_key2 == 0 ) ) { // 'i' is a candidate for first press PMESG3("Found a match in map position %d, keyfact %s", m_ieActions.indexOf(action), action->keySequence.data()); return m_ieActions.indexOf(action); } } return -1; } // This is called whenever a second fact happens right after the first int InputEngine::identify_first_and_second_facts_together() { PENTER3; PMESG3("Adding a 2nd fact %d,%d to the 1st one %d,%d",fact2_k1,fact2_k2,fact1_k1,fact1_k2); if (fact2_k1!=0) { if (!isHolding) { if (fact1_k2==0) // first press is (I know that its not [K] because if it was I'd never reach identify_first_and_second_facts_together()) { if (fact2_k2==0) if (fact1_k1 == fact2_k1) { PMESG3("Whole action is a <>"); wholeActionType = D_FKEY; // <> } else { PMESG3("Whole action is a >K>K"); wholeActionType = S_FKEY_FKEY; // >K>K } else { PMESG3("Whole action is a >K>KK"); wholeActionType = S_FKEY_FKEY2; // >K>KK } } else { if (fact2_k2==0) { PMESG3("Whole action is a >KK>K"); wholeActionType = S_FKEY2_FKEY; // >KK>K } else { if ( ((fact1_k1==fact2_k1) && (fact1_k2==fact2_k2)) || ((fact1_k1==fact2_k2) && (fact1_k2==fact2_k1)) ) { PMESG3("Whole action is a <>"); wholeActionType = D_FKEY2; } else { PMESG3("Whole action is a >KK>KK"); wholeActionType = S_FKEY2_FKEY2; } } } } else { if (fact1_k2==0) // first press is (I know that its not [K] because if it was I'd never reach identify_first_and_second_facts_together()) { if (fact2_k2==0) if (fact1_k1 == fact2_k1) { PMESG3("Whole action is a <[K]>"); wholeActionType = FHKEY; } else { PMESG3("Whole action is a >K[K]"); wholeActionType = S_FKEY_HKEY; } else { PMESG3("Whole action is a >K[KK]"); wholeActionType = S_FKEY_HKEY2; } } else { if (fact2_k2==0) { PMESG3("Whole action is a KK[K]"); wholeActionType = S_FKEY2_HKEY; } else { PMESG3("Whole action is a >KK[KK]"); wholeActionType = S_FKEY2_HKEY2; } } } } else { PMESG3("Second fact is null (0,0). Assuming wholeActionType is %d", fact1Type); wholeActionType = fact1Type; } // whole action type identified . PMESG3("Searching for a %d action that matches %d,%d,%d,%d ", wholeActionType, fact1_k1, fact1_k2, fact2_k1, fact2_k2); foreach(IEAction* action, m_ieActions) { if ( action->type != wholeActionType ) continue; int ap1k1 = action->fact1_key1; int ap1k2 = action->fact1_key2; int ap2k1 = action->fact2_key1; int ap2k2 = action->fact2_key2; PMESG4("COMPARING %d,%d,%d,%d \tWITH %d,%d,%d,%d",ap1k1,ap1k2,ap2k1,ap2k2,fact1_k1,fact1_k2,fact2_k1,fact2_k2); if ( ( ((ap1k1==fact1_k1) && (ap1k2==fact1_k2)) || ((ap1k1==fact1_k2) && (ap1k2==fact1_k1)) ) && ( ((ap2k1==fact2_k1) && (ap2k2==fact2_k2)) || ((ap2k1==fact2_k2) && (ap2k2==fact2_k1)) ) ) { // 'i' is a candidate the whole action PMESG3("Found a match : action %s", action->keySequence.data()); return m_ieActions.indexOf(action); } } PMESG3("No candidates found :-("); return -1; } void InputEngine::give_a_chance_for_second_fact() { PENTER3; PMESG3("Waiting %d ms for second fact ...",doubleFactWaitTime ); catcher.secondChanceTimer.start( doubleFactWaitTime ); isFirstFact=false; isHolding = false; isPressEventLocked = false; stackIndex = 0; pressEventCounter = 0; fact2_k1 = 0; fact2_k2 = 0; wholeMapIndex = -1; for (int i=0; i < STACK_SIZE; i++) { eventType[i] = 0; eventStack[i] = 0; eventTime[i] = 0; } } // ----------------------------- JMB ENGINE : ACTION LEVEL HANDLING ------------------------- void InputEngine::dispatch_action(int mapIndex) { PENTER2; broadcast_action(m_ieActions.at(mapIndex)); } // This is called by void InputEngine::dispatch_hold() { PENTER2; catcher.clearOutputTimer.stop(); isHoldingOutput=false; wholeMapIndex = -1; if (isFirstFact) { fact1_k1 = eventStack[0]; fact1_k2 = eventStack[1]; wholeMapIndex = identify_first_fact(); // I can consider first press the last because there is nothing after a [] } else { fact2_k1 = eventStack[0]; fact2_k2 = eventStack[1]; wholeMapIndex = identify_first_and_second_facts_together(); } if (wholeMapIndex>=0) { broadcast_action(m_ieActions.at(wholeMapIndex)); } stop_collecting(); // note that we dont call conclusion() here :-) } void InputEngine::finish_hold() { PENTER3; PMESG("Finishing hold action %d",wholeMapIndex); isHolding = false; if (m_cancelHold) { PMESG("Canceling this hold command"); if (holdingCommand) { holdingCommand->cancel_action(); delete holdingCommand; holdingCommand = 0; } cpointer().reset_cursor(); } else if (holdingCommand) { cpointer().reset_cursor(); int holdFinish = holdingCommand->finish_hold(); int holdprepare = -1; if (holdFinish > 0) { holdprepare = holdingCommand->prepare_actions(); if (holdprepare > 0) { PMESG("holdingCommand->prepare_actions() returned succes!"); holdingCommand->set_valid(true); } else { PMESG("holdingCommand->prepare_actions() returned <= 0, so either it failed, or nothing happened!"); holdingCommand->set_valid( false ); } } else { PMESG("holdingCommand->finish_hold() returned <= 0, so either it failed, or nothing happened!"); holdingCommand->set_valid( false ); } if (holdingCommand->push_to_history_stack() < 0) { if (holdprepare == 1) { holdingCommand->do_action(); } delete holdingCommand; } holdingCommand = 0; } set_jogging(false); conclusion(); } void InputEngine::conclusion() { PENTER3; reset(); hold_output(); } void InputEngine::hold_output() { PENTER3; if (!isHoldingOutput) { catcher.clearOutputTimer.start(clearTime); isHoldingOutput=true; } } int InputEngine::init_map(const QString& keymap) { PENTER; QString filename = ":/keymaps/" + keymap + ".xml"; if ( ! QFile::exists(filename)) { filename = QDir::homePath() + "/.traverso/keymaps/" + keymap + ".xml"; } QDomDocument doc("keymap"); QFile file(filename); if (!file.open(QIODevice::ReadOnly)) return -1; if (!doc.setContent(&file)) { file.close(); return -1; } file.close(); PMESG("Using keymap: %s", QS_C(keymap)); foreach(IEAction* action, m_ieActions) { delete action; } m_ieActions.clear(); m_modifierKeys.clear(); m_modes.clear(); QDomElement root = doc.documentElement(); QDomNode modifierKeysNode = root.firstChildElement("ModifierKeys"); QDomNode modifierKeyNode = modifierKeysNode.firstChild(); int keycode; QString key; while( !modifierKeyNode.isNull() ) { QDomElement e = modifierKeyNode.toElement(); key = e.attribute( "key", ""); set_hexcode(keycode, key); m_modifierKeys.append(keycode); modifierKeyNode = modifierKeyNode.nextSibling(); } QDomNode modesNode = root.firstChildElement("Modes"); QDomNode modeNode = modesNode.firstChild(); int id; QString modename; while( !modeNode.isNull() ) { QDomElement e = modeNode.toElement(); modename = e.attribute("name", ""); id = e.attribute("id", "-1").toInt(); m_modes.insert(modename, id); modeNode = modeNode.nextSibling(); } QDomNode keyfactsNode = root.firstChildElement("Keyfacts"); QDomNode keyfactNode = keyfactsNode.firstChild(); QString keyFactType; QString key1, key2, key3, key4, mouseHint, modifierKeys; IEAction::Data* data; while( !keyfactNode.isNull() ) { QDomElement e = keyfactNode.toElement(); if( e.isNull() ) { continue; } if( ! (e.tagName() == "keyfact" ) ) { PERROR("Detected wrong tagname, misspelled: keyfact !!"); continue; } IEAction* action = new IEAction(); keyFactType = e.attribute( "type", "" ); key1 = e.attribute( "key1", ""); key2 = e.attribute( "key2", "" ); if (keyFactType == "FKEY") action->type = FKEY; else if (keyFactType == "FKEY2") action->type = FKEY2; else if (keyFactType == "HKEY") action->type = HOLDKEY; else if (keyFactType == "HKEY2") action->type = HKEY2; else if (keyFactType == "D_FKEY") action->type = D_FKEY; else if (keyFactType == "D_FKEY2") action->type = D_FKEY2; else if (keyFactType == "S_FKEY_FKEY") action->type = S_FKEY_FKEY; else { PWARN("keyFactType not supported!"); } set_hexcode(action->fact1_key1, key1); set_hexcode(action->fact1_key2, key2); set_hexcode(action->fact2_key1, key3); set_hexcode(action->fact2_key2, key4); // Fix the keyCode positions if (( action->type == D_FKEY ) || ( action->type == FHKEY )) { action->fact2_key1=action->fact1_key1; } else if (( action->type == D_FKEY2 ) || ( action->type == FHKEY )) { action->fact2_key1=action->fact1_key1; action->fact2_key2=action->fact1_key2; } else if (( action->type == S_FKEY_FKEY ) || ( action->type == S_FKEY_HKEY )) { action->fact2_key1=action->fact1_key2; action->fact1_key2=0; } QDomElement objectsNode = e.firstChildElement("Objects"); QDomNode objectNode = objectsNode.firstChild(); while(!objectNode.isNull()) { data = new IEAction::Data; QDomElement e = objectNode.toElement(); QString objectname = e.attribute("objectname", ""); data->slotsignature = e.attribute("slotsignature", ""); data->modes = e.attribute("modes", "").split(";"); data->pluginname = e.attribute( "pluginname", ""); data->commandname = e.attribute( "commandname", ""); data->submenu = e.attribute("submenu", ""); data->sortorder = e.attribute( "sortorder", "0").toInt(); mouseHint = e.attribute( "mousehint", "" ); QString args = e.attribute("arguments", ""); modifierKeys = e.attribute("modifierkeys", ""); if ( ! args.isEmpty() ) { QStringList arglist = args.split(";"); for (int i=0; iarguments.append(arglist.at(i)); } } if (! modifierKeys.isEmpty()) { QStringList modifierlist = modifierKeys.split(";"); for (int i=0; imodifierkeys.append(keycode); } } data->useX = data->useY = false; if (mouseHint == "LR") { data->useX = true; } if (mouseHint == "UD") { data->useY = true; } if (mouseHint == "LRUD") { data->useX = data->useY = true; } if (QString(objectname) == "") { PERROR("no objectname given in keyaction %s", QS_C(keyFactType)); } if (data->slotsignature.isEmpty() && data->pluginname.isEmpty()) { PERROR("no slotsignature given in keyaction %s, object %s", QS_C(keyFactType), QS_C(objectname)); } if (QString(data->modes.join(";")) == "") { PERROR("no modes given in keyaction %s, object %s", QS_C(keyFactType), QS_C(objectname)); } if (modifierKeys.isEmpty()) { action->objects.insert(objectname, data); } else { action->objectUsingModifierKeys.insert(objectname, data); } objectNode = objectNode.nextSibling(); } action->isInstantaneous = false; action->render_key_sequence(key1, key2); bool exists = false; for (int i=0; ifact1_key1 == existingaction->fact1_key1) && (action->fact1_key2 == existingaction->fact1_key2) && (action->fact2_key1 == existingaction->fact2_key1) && (action->fact2_key2 == existingaction->fact2_key2) && (action->type == existingaction->type) ) { exists = true; QString errorstring = QString("InputEngine:: keyfact with: type=%1, key1='%2', key2='%3' already exists!\n" "You should only define keyfact types one time!!\n").arg(keyFactType).arg(key1).arg(key2); printf(QS_C(errorstring)); info().warning(errorstring); break; } } if (!exists) { m_ieActions.append(action); PMESG2("ADDED action: type=%d keys=%d,%d,%d,%d useX=%d useY=%d, slot=%s", action->type, action->fact1_key1,action->fact1_key2,action->fact2_key1,action->fact2_key2,data->useX,data->useY, QS_C(data->slotsignature)); } keyfactNode = keyfactNode.nextSibling(); } PMESG2("Optimizing map for best performance ..."); int optimizedActions=0; foreach(IEAction* action, m_ieActions) { int c1A = action->fact1_key1; int c2A = action->fact1_key2; if ( (action->type == FKEY) ) { bool alone = true; foreach(IEAction* aloneAction, m_ieActions) { if (action == aloneAction) continue; int tt = aloneAction->type; int t1A = aloneAction->fact1_key1; if ( (t1A==c1A) && ( (tt==D_FKEY) || (tt==FHKEY) || (tt==S_FKEY_FKEY) || (tt==S_FKEY_FKEY2) || (tt==S_FKEY_HKEY) || (tt==S_FKEY_HKEY2) ) ) alone=false; } if (alone) { PMESG3("Setting fact (slot=%s) as protected (Instantaneous response)", action->keySequence.data()); action->isInstantaneous = true; optimizedActions++; } else action->isInstantaneous = false; } else if ((action->type == FKEY2)) { bool alone = true; foreach(IEAction* aloneAction, m_ieActions) { if (action == aloneAction) continue; int tt = aloneAction->type; int t1A = aloneAction->fact1_key1; int t2A = aloneAction->fact1_key2; if ( ((t1A==c1A) && (t2A==c2A)) && ( (tt==HOLDKEY) || (tt==D_FKEY2) || (tt==S_FKEY2_FKEY) || (tt==S_FKEY2_FKEY2)|| (tt==S_FKEY2_HKEY) || (tt==S_FKEY2_HKEY2) ) ) alone=false; } if (alone) { PMESG3("Setting fact (slot=%s) for instantaneous response", action->keySequence.data()); action->isInstantaneous = true; optimizedActions++; } else action->isInstantaneous = false; } } PMESG2("Keymap initialized! %d actions registered ( %d instantanious) .", m_ieActions.size(), optimizedActions); return 1; } void InputEngine::set_clear_time(int time) { clearTime = time; } void InputEngine::set_hold_sensitiveness(int htime) { assumeHoldTime=htime; } void InputEngine::set_double_fact_interval(int time) { doubleFactWaitTime = time; } void InputEngine::register_command_plugin(CommandPlugin *plugin, const QString &pluginName) { m_commandplugins.insert(pluginName, plugin); } // Number colector bool InputEngine::check_number_collection(int eventcode) { if (((eventcode >= Qt::Key_0) && (eventcode <= Qt::Key_9)) || (eventcode == Qt::Key_Comma) || (eventcode == Qt::Key_Period)) { sCollectedNumber.append( QChar(eventcode) ); // it had a ",1" complement after fact1_k1... why? PMESG("Collected %s so far...", QS_C(sCollectedNumber) ) ; QString sn = "NUMBER " + sCollectedNumber; collectedNumber = sCollectedNumber.toInt(); if (holdingCommand) { holdingCommand->set_collected_number(sCollectedNumber); } return true; } if (eventcode == Qt::Key_Backspace) { if (sCollectedNumber.size() > 0) { sCollectedNumber = sCollectedNumber.left(sCollectedNumber.size() - 1); if (holdingCommand) { holdingCommand->set_collected_number(sCollectedNumber); } } return true; } if (eventcode == Qt::Key_Minus) { if (sCollectedNumber.contains("-")) { sCollectedNumber = sCollectedNumber.remove("-"); } else { sCollectedNumber.prepend("-"); } if (holdingCommand) { holdingCommand->set_collected_number(sCollectedNumber); } } return false; } void InputEngine::stop_collecting() { PENTER3; collectedNumber = sCollectedNumber.toInt(); sCollectedNumber = ""; } int InputEngine::collected_number( ) { int n = collectedNumber; sCollectedNumber = "-1"; // collectedNumber has a life of only one get. return n; } bool InputEngine::is_holding( ) { return isHolding; } Command * InputEngine::get_holding_command() const { return holdingCommand; } void InputEngine::create_menudata_for_metaobject(const QMetaObject * mo, QList< MenuData > & list) const { const char* classname = mo->className(); for (int i=0; i datalist; datalist.append(ieaction->objects.value(classname)); datalist.append(ieaction->objectUsingModifierKeys.value(classname)); foreach(IEAction::Data* iedata, datalist) { if ( ! iedata ) { continue; } MenuData menudata; if ( ! iedata->pluginname.isEmpty() ) { CommandPlugin* plug = m_commandplugins.value(iedata->pluginname); if ( ! plug) { continue; } int classInfoIndex = plug->metaObject()->indexOfClassInfo(QS_C(iedata->commandname)); if (classInfoIndex >= 0) { QMetaClassInfo classInfo = plug->metaObject()->classInfo(classInfoIndex); // Set the translated string! menudata.description = QCoreApplication::translate(classname, classInfo.value()); } else { menudata.description = QString("Add a Q_CLASSINFO() in CommandPlug %1.h, Command %2 please") .arg(iedata->pluginname).arg(iedata->commandname); PWARN("%s", QS_C(menudata.description)); } } else { int classInfoIndex = mo->indexOfClassInfo(QS_C(iedata->slotsignature)); int methodIndex = -1; for (int i=0; i < mo->methodCount(); i++) { if ( ! (mo->method(i).methodType() == QMetaMethod::Slot) ) { continue; } QString slotsignature = mo->method(i).methodSignature(); slotsignature = slotsignature.left(slotsignature.indexOf("(")); if (iedata->slotsignature == slotsignature) { methodIndex = i; break; } } if (classInfoIndex >= 0 && methodIndex >= 0) { QMetaClassInfo classInfo = mo->classInfo(classInfoIndex); // Set the translated string! menudata.description = QCoreApplication::translate(classname, classInfo.value()); } else { if (methodIndex >= 0) { menudata.description = QString("Add a Q_CLASSINFO() for %1::%2 please") .arg(classname).arg(iedata->slotsignature); PWARN("%s", QS_C(menudata.description)); } else { continue; } } } menudata.keysequence = ieaction->keySequence; menudata.iedata = ieaction->keySequence; menudata.sortorder = iedata->sortorder; menudata.submenu = iedata->submenu; menudata.modifierkeys = iedata->modifierkeys; if (menudata.modifierkeys.size()) { menudata.iedata.prepend("++"); } list.append(menudata); } } } QList< MenuData > InputEngine::create_menudata_for(QObject* item) { QList list; ContextItem* contextitem; do { const QMetaObject* mo = item->metaObject(); create_menudata_for_metaobject(mo, list); contextitem = qobject_cast(item); } while (contextitem && (item = contextitem->get_context()) ); return list; } //---------- EVENT CATCHER ------------- EventCatcher::EventCatcher() { holdTimer.setSingleShot(true); connect( &holdTimer, SIGNAL(timeout()), this, SLOT(assume_hold())); connect( &secondChanceTimer, SIGNAL(timeout()), this, SLOT(quit_second_chance())); connect( &clearOutputTimer, SIGNAL(timeout()), this, SLOT(clear_output())); } void EventCatcher::assume_hold() // no release so far ? so consider it a hold... { PENTER3; PMESG3("No release so far (waited %d ms). Assuming this is a hold and dispatching it",ie().assumeHoldTime); holdTimer.stop(); // quit the holding check.. ie().isHolding = true; ie().dispatch_hold(); } void EventCatcher::clear_output() { PENTER3; if ((ie().isHoldingOutput)) { clearOutputTimer.stop(); ie().isHoldingOutput=false; } } void EventCatcher::quit_second_chance() { PENTER3; secondChanceTimer.stop(); if (!ie().isHolding) // if it is holding, there is no need to push a new fact { PMESG3("No second fact (waited %d ms) ... Forcing a null second fact",ie().doubleFactWaitTime); ie().push_fact(0,0); // no second press performed, so I am adding a pair of Zeros as second press and keep going... } } void IEAction::render_key_sequence(const QString& key1, const QString& key2) { switch(type) { case FKEY: keySequence = QString("< " + key1 + " >").toLatin1().data(); break; case FKEY2: keySequence = QString("< " + key1 + " " + key2 + " >").toLatin1().data(); break; case HOLDKEY: keySequence = QString("[ " + key1 + " ]").toLatin1().data(); break; case HKEY2: keySequence = QString("[ " + key1 + " " + key2 + " ]").toLatin1().data(); break; case D_FKEY: keySequence = QString("<< " + key1 + " >>").toLatin1().data(); break; case D_FKEY2: keySequence = QString("<< " + key1 + " " + key2 + " >>").toLatin1().data(); break; case S_FKEY_FKEY: keySequence = QString("> " + key1 + " > " + key2).toLatin1().data(); break; default : keySequence = "Unknown Key Sequence"; } } IEAction::~ IEAction() { foreach(Data* data, objects) { delete data; } foreach(Data* data, objectUsingModifierKeys) { delete data; } }