/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2013-2017 CERN * @author Tomasz Wlostowski * @author Maciej Suminski * * This program 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, you may find one here: * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html * or you may search the http://www.gnu.org website for the version 2 license, * or you may write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using boost::optional; /// Struct describing the current execution state of a TOOL struct TOOL_MANAGER::TOOL_STATE { TOOL_STATE( TOOL_BASE* aTool ) : theTool( aTool ) { clear(); } TOOL_STATE( const TOOL_STATE& aState ) { theTool = aState.theTool; idle = aState.idle; pendingWait = aState.pendingWait; pendingContextMenu = aState.pendingContextMenu; contextMenu = aState.contextMenu; contextMenuTrigger = aState.contextMenuTrigger; cofunc = aState.cofunc; wakeupEvent = aState.wakeupEvent; waitEvents = aState.waitEvents; transitions = aState.transitions; vcSettings = aState.vcSettings; // do not copy stateStack } ~TOOL_STATE() { assert( stateStack.empty() ); } /// The tool itself TOOL_BASE* theTool; /// Is the tool active (pending execution) or disabled at the moment bool idle; /// Flag defining if the tool is waiting for any event (i.e. if it /// issued a Wait() call). bool pendingWait; /// Is there a context menu being displayed bool pendingContextMenu; /// Context menu currently used by the tool CONTEXT_MENU* contextMenu; /// Defines when the context menu is opened CONTEXT_MENU_TRIGGER contextMenuTrigger; /// Tool execution context COROUTINE* cofunc; /// The event that triggered the execution/wakeup of the tool after Wait() call TOOL_EVENT wakeupEvent; /// List of events the tool is currently waiting for TOOL_EVENT_LIST waitEvents; /// List of possible transitions (ie. association of events and state handlers that are executed /// upon the event reception std::vector transitions; /// VIEW_CONTROLS settings to preserve settings when the tools are switched KIGFX::VC_SETTINGS vcSettings; void operator=( const TOOL_STATE& aState ) { theTool = aState.theTool; idle = aState.idle; pendingWait = aState.pendingWait; pendingContextMenu = aState.pendingContextMenu; contextMenu = aState.contextMenu; contextMenuTrigger = aState.contextMenuTrigger; cofunc = aState.cofunc; wakeupEvent = aState.wakeupEvent; waitEvents = aState.waitEvents; transitions = aState.transitions; vcSettings = aState.vcSettings; // do not copy stateStack } bool operator==( const TOOL_MANAGER::TOOL_STATE& aRhs ) const { return aRhs.theTool == this->theTool; } bool operator!=( const TOOL_MANAGER::TOOL_STATE& aRhs ) const { return aRhs.theTool != this->theTool; } /** * Function Push() * Stores the current state of the tool on stack. Stacks are stored internally and are not * shared between different TOOL_STATE objects. */ void Push() { auto state = std::make_unique( *this ); stateStack.push( std::move( state ) ); clear(); } /** * Function Pop() * Restores state of the tool from stack. Stacks are stored internally and are not * shared between different TOOL_STATE objects. * @return True if state was restored, false if the stack was empty. */ bool Pop() { delete cofunc; if( !stateStack.empty() ) { *this = *stateStack.top().get(); stateStack.pop(); return true; } else { cofunc = NULL; return false; } } private: ///> Stack preserving previous states of a TOOL. std::stack> stateStack; ///> Restores the initial state. void clear() { idle = true; pendingWait = false; pendingContextMenu = false; cofunc = NULL; contextMenu = NULL; contextMenuTrigger = CMENU_OFF; vcSettings.Reset(); transitions.clear(); } }; TOOL_MANAGER::TOOL_MANAGER() : m_model( NULL ), m_view( NULL ), m_viewControls( NULL ), m_editFrame( NULL ), m_passEvent( false ), m_menuActive( false ) { m_actionMgr = new ACTION_MANAGER( this ); // Keep default VIEW_CONTROLS settings at the bottom of the stack m_vcStack.push( KIGFX::VC_SETTINGS() ); } TOOL_MANAGER::~TOOL_MANAGER() { std::map::iterator it, it_end; for( it = m_toolState.begin(), it_end = m_toolState.end(); it != it_end; ++it ) { delete it->second->cofunc; // delete cofunction delete it->second; // delete TOOL_STATE delete it->first; // delete the tool itself } delete m_actionMgr; } void TOOL_MANAGER::RegisterTool( TOOL_BASE* aTool ) { wxASSERT_MSG( m_toolNameIndex.find( aTool->GetName() ) == m_toolNameIndex.end(), wxT( "Adding two tools with the same name may result in unexpected behaviour.") ); wxASSERT_MSG( m_toolIdIndex.find( aTool->GetId() ) == m_toolIdIndex.end(), wxT( "Adding two tools with the same ID may result in unexpected behaviour.") ); wxASSERT_MSG( m_toolTypes.find( typeid( *aTool ).name() ) == m_toolTypes.end(), wxT( "Adding two tools of the same type may result in unexpected behaviour.") ); TOOL_STATE* st = new TOOL_STATE( aTool ); m_toolState[aTool] = st; m_toolNameIndex[aTool->GetName()] = st; m_toolIdIndex[aTool->GetId()] = st; m_toolTypes[typeid( *aTool ).name()] = st->theTool; aTool->attachManager( this ); } bool TOOL_MANAGER::InvokeTool( TOOL_ID aToolId ) { TOOL_BASE* tool = FindTool( aToolId ); if( tool && tool->GetType() == INTERACTIVE ) return invokeTool( tool ); return false; // there is no tool with the given id } bool TOOL_MANAGER::InvokeTool( const std::string& aToolName ) { TOOL_BASE* tool = FindTool( aToolName ); if( tool && tool->GetType() == INTERACTIVE ) return invokeTool( tool ); return false; // there is no tool with the given name } void TOOL_MANAGER::RegisterAction( TOOL_ACTION* aAction ) { m_actionMgr->RegisterAction( aAction ); } void TOOL_MANAGER::UnregisterAction( TOOL_ACTION* aAction ) { m_actionMgr->UnregisterAction( aAction ); } bool TOOL_MANAGER::RunAction( const std::string& aActionName, bool aNow, void* aParam ) { TOOL_ACTION* action = m_actionMgr->FindAction( aActionName ); if( !action ) { wxASSERT_MSG( false, wxString::Format( wxT( "Could not find action %s." ), aActionName ) ); return false; } RunAction( *action, aNow, aParam ); return false; } void TOOL_MANAGER::RunAction( const TOOL_ACTION& aAction, bool aNow, void* aParam ) { TOOL_EVENT event = aAction.MakeEvent(); // Allow to override the action parameter if( aParam ) event.SetParameter( aParam ); if( aNow ) processEvent( event ); else PostEvent( event ); } int TOOL_MANAGER::GetHotKey( const TOOL_ACTION& aAction ) { return m_actionMgr->GetHotKey( aAction ); } void TOOL_MANAGER::UpdateHotKeys() { m_actionMgr->UpdateHotKeys(); } bool TOOL_MANAGER::invokeTool( TOOL_BASE* aTool ) { wxASSERT( aTool != NULL ); TOOL_EVENT evt( TC_COMMAND, TA_ACTIVATE, aTool->GetName() ); processEvent( evt ); return true; } bool TOOL_MANAGER::runTool( TOOL_ID aToolId ) { TOOL_BASE* tool = FindTool( aToolId ); if( tool && tool->GetType() == INTERACTIVE ) return runTool( tool ); return false; // there is no tool with the given id } bool TOOL_MANAGER::runTool( const std::string& aToolName ) { TOOL_BASE* tool = FindTool( aToolName ); if( tool && tool->GetType() == INTERACTIVE ) return runTool( tool ); return false; // there is no tool with the given name } bool TOOL_MANAGER::runTool( TOOL_BASE* aTool ) { wxASSERT( aTool != NULL ); if( !isRegistered( aTool ) ) { wxASSERT_MSG( false, wxT( "You cannot run unregistered tools" ) ); return false; } TOOL_ID id = aTool->GetId(); // If the tool is already active, bring it to the top of the active tools stack if( isActive( aTool ) ) { m_activeTools.erase( std::find( m_activeTools.begin(), m_activeTools.end(), id ) ); m_activeTools.push_front( id ); return false; } aTool->Reset( TOOL_INTERACTIVE::RUN ); aTool->SetTransitions(); // Add the tool on the front of the processing queue (it gets events first) m_activeTools.push_front( id ); return true; } TOOL_BASE* TOOL_MANAGER::FindTool( int aId ) const { std::map::const_iterator it = m_toolIdIndex.find( aId ); if( it != m_toolIdIndex.end() ) return it->second->theTool; return NULL; } TOOL_BASE* TOOL_MANAGER::FindTool( const std::string& aName ) const { std::map::const_iterator it = m_toolNameIndex.find( aName ); if( it != m_toolNameIndex.end() ) return it->second->theTool; return NULL; } void TOOL_MANAGER::DeactivateTool() { // Deactivate the active tool, but do not run anything new TOOL_EVENT evt( TC_COMMAND, TA_CANCEL_TOOL ); processEvent( evt ); } void TOOL_MANAGER::ResetTools( TOOL_BASE::RESET_REASON aReason ) { DeactivateTool(); for( auto& state : m_toolState ) { state.first->Reset( aReason ); state.first->SetTransitions(); } } void TOOL_MANAGER::InitTools() { for( auto it = m_toolState.begin(); it != m_toolState.end(); /* iteration in the loop */ ) { TOOL_BASE* tool = it->first; TOOL_STATE* state = it->second; ++it; // keep the iterator valid if the element is going to be erased if( !tool->Init() ) { wxMessageBox( wxString::Format( "Initialization of tool '%s' failed", tool->GetName() ) ); // Unregister the tool m_toolState.erase( tool ); m_toolNameIndex.erase( tool->GetName() ); m_toolIdIndex.erase( tool->GetId() ); m_toolTypes.erase( typeid( *tool ).name() ); delete state; delete tool; } } ResetTools( TOOL_BASE::RUN ); } int TOOL_MANAGER::GetPriority( int aToolId ) const { int priority = 0; for( auto it = m_activeTools.begin(), itEnd = m_activeTools.end(); it != itEnd; ++it ) { if( *it == aToolId ) return priority; ++priority; } return -1; } void TOOL_MANAGER::ScheduleNextState( TOOL_BASE* aTool, TOOL_STATE_FUNC& aHandler, const TOOL_EVENT_LIST& aConditions ) { TOOL_STATE* st = m_toolState[aTool]; st->transitions.push_back( TRANSITION( aConditions, aHandler ) ); } void TOOL_MANAGER::RunMainStack( TOOL_BASE* aTool, std::function aFunc ) { TOOL_STATE* st = m_toolState[aTool]; st->cofunc->RunMainStack( std::move( aFunc ) ); } optional TOOL_MANAGER::ScheduleWait( TOOL_BASE* aTool, const TOOL_EVENT_LIST& aConditions ) { TOOL_STATE* st = m_toolState[aTool]; assert( !st->pendingWait ); // everything collapses on two KiYield() in a row // indicate to the manager that we are going to sleep and we shall be // woken up when an event matching aConditions arrive st->pendingWait = true; st->waitEvents = aConditions; // switch context back to event dispatcher loop st->cofunc->KiYield(); return st->wakeupEvent; } void TOOL_MANAGER::dispatchInternal( const TOOL_EVENT& aEvent ) { // iterate over all registered tools for( auto it = m_activeTools.begin(); it != m_activeTools.end(); ++it ) { TOOL_STATE* st = m_toolIdIndex[*it]; // the tool state handler is waiting for events (i.e. called Wait() method) if( st->pendingWait ) { if( st->waitEvents.Matches( aEvent ) ) { // By default, only messages are passed further m_passEvent = ( aEvent.Category() == TC_MESSAGE ); // got matching event? clear wait list and wake up the coroutine st->wakeupEvent = aEvent; st->pendingWait = false; st->waitEvents.clear(); if( st->cofunc ) { pushViewControls(); applyViewControls( st ); bool end = !st->cofunc->Resume(); saveViewControls( st ); popViewControls(); if( end ) it = finishTool( st ); } // If the tool did not request to propagate // the event to other tools, we should stop it now if( !m_passEvent ) break; } } } for( auto& state : m_toolState ) { TOOL_STATE* st = state.second; bool finished = false; // no state handler in progress - check if there are any transitions (defined by // Go() method that match the event. if( !st->transitions.empty() ) { for( TRANSITION& tr : st->transitions ) { if( tr.first.Matches( aEvent ) ) { auto func_copy = tr.second; // if there is already a context, then store it if( st->cofunc ) st->Push(); st->cofunc = new COROUTINE( std::move( func_copy ) ); // as the state changes, the transition table has to be set up again st->transitions.clear(); // got match? Run the handler. pushViewControls(); applyViewControls( st ); st->cofunc->Call( aEvent ); saveViewControls( st ); popViewControls(); if( !st->cofunc->Running() ) finishTool( st ); // The couroutine has finished immediately? // if it is a message, continue processing finished = !( aEvent.Category() == TC_MESSAGE ); // there is no point in further checking, as transitions got cleared break; } } } if( finished ) break; // only the first tool gets the event } } bool TOOL_MANAGER::dispatchStandardEvents( const TOOL_EVENT& aEvent ) { if( aEvent.Action() == TA_KEY_PRESSED ) { // Check if there is a hotkey associated if( m_actionMgr->RunHotKey( aEvent.Modifier() | aEvent.KeyCode() ) ) return false; // hotkey event was handled so it does not go any further } return true; } bool TOOL_MANAGER::dispatchActivation( const TOOL_EVENT& aEvent ) { if( aEvent.IsActivate() ) { std::map::iterator tool = m_toolNameIndex.find( *aEvent.GetCommandStr() ); if( tool != m_toolNameIndex.end() ) { runTool( tool->second->theTool ); return true; } } return false; } void TOOL_MANAGER::dispatchContextMenu( const TOOL_EVENT& aEvent ) { // Store the current tool ID to decide whether to restore the cursor position TOOL_ID activeTool = GetCurrentToolId(); for( TOOL_ID toolId : m_activeTools ) { TOOL_STATE* st = m_toolIdIndex[toolId]; // the tool requested a context menu. The menu is activated on RMB click (CMENU_BUTTON mode) // or immediately (CMENU_NOW) mode. The latter is used for clarification lists. if( st->contextMenuTrigger == CMENU_OFF ) continue; if( st->contextMenuTrigger == CMENU_BUTTON && !aEvent.IsClick( BUT_RIGHT ) ) break; st->pendingWait = true; st->waitEvents = TOOL_EVENT( TC_ANY, TA_ANY ); // Store the menu pointer in case it is changed by the TOOL when handling menu events CONTEXT_MENU* m = st->contextMenu; if( st->contextMenuTrigger == CMENU_NOW ) st->contextMenuTrigger = CMENU_OFF; VECTOR2D cursor = m_viewControls->GetCursorPosition(); // Temporarily store the cursor position, so the tools could execute actions // using the point where the user has invoked a context menu if( m_viewControls->ForcedCursorPosition() ) m_origCursor = cursor; m_viewControls->ForceCursorPosition( true, cursor ); // Display a copy of menu std::unique_ptr menu( m->Clone() ); // Run update handlers on the created copy menu->UpdateAll(); m_menuActive = true; GetEditFrame()->PopupMenu( menu.get() ); m_menuActive = false; // If nothing was chosen from the context menu, we must notify the tool as well if( menu->GetSelected() < 0 ) { TOOL_EVENT evt( TC_COMMAND, TA_CONTEXT_MENU_CHOICE, -1 ); evt.SetParameter( m ); dispatchInternal( evt ); } // Notify the tools that menu has been closed TOOL_EVENT evt( TC_COMMAND, TA_CONTEXT_MENU_CLOSED ); evt.SetParameter( m ); dispatchInternal( evt ); // Restore the cursor settings if the tool is still active if( activeTool == GetCurrentToolId() ) { m_viewControls->ForceCursorPosition( (bool) m_origCursor, m_origCursor.get_value_or( VECTOR2D( 0, 0 ) ) ); } m_origCursor = boost::none; break; } } TOOL_MANAGER::ID_LIST::iterator TOOL_MANAGER::finishTool( TOOL_STATE* aState ) { auto it = std::find( m_activeTools.begin(), m_activeTools.end(), aState->theTool->GetId() ); if( !aState->Pop() ) { // Deactivate the tool if there are no other contexts saved on the stack if( it != m_activeTools.end() ) it = m_activeTools.erase( it ); } // Set transitions to be ready for future TOOL_EVENTs aState->theTool->SetTransitions(); m_viewControls->ForceCursorPosition( false ); return it; } void TOOL_MANAGER::ProcessEvent( const TOOL_EVENT& aEvent ) { processEvent( aEvent ); if( TOOL_STATE* active = GetCurrentToolState() ) { applyViewControls( active ); } if( m_view->IsDirty() ) { EDA_DRAW_FRAME* f = static_cast( GetEditFrame() ); f->GetGalCanvas()->Refresh(); // fixme: ugly hack, provide a method in TOOL_DISPATCHER. } } void TOOL_MANAGER::ScheduleContextMenu( TOOL_BASE* aTool, CONTEXT_MENU* aMenu, CONTEXT_MENU_TRIGGER aTrigger ) { TOOL_STATE* st = m_toolState[aTool]; st->contextMenu = aMenu; st->contextMenuTrigger = aTrigger; } bool TOOL_MANAGER::SaveClipboard( const std::string& aText ) { if( wxTheClipboard->Open() ) { wxTheClipboard->SetData( new wxTextDataObject( wxString( aText.c_str(), wxConvUTF8 ) ) ); wxTheClipboard->Close(); return true; } return false; } std::string TOOL_MANAGER::GetClipboard() const { std::string result; if( wxTheClipboard->Open() ) { if( wxTheClipboard->IsSupported( wxDF_TEXT ) ) { wxTextDataObject data; wxTheClipboard->GetData( data ); result = data.GetText().mb_str(); } wxTheClipboard->Close(); } return result; } TOOL_ID TOOL_MANAGER::MakeToolId( const std::string& aToolName ) { static int currentId; return currentId++; } void TOOL_MANAGER::SetEnvironment( EDA_ITEM* aModel, KIGFX::VIEW* aView, KIGFX::VIEW_CONTROLS* aViewControls, wxWindow* aFrame ) { m_model = aModel; m_view = aView; m_viewControls = aViewControls; m_editFrame = aFrame; m_actionMgr->UpdateHotKeys(); } bool TOOL_MANAGER::isActive( TOOL_BASE* aTool ) { if( !isRegistered( aTool ) ) return false; // Just check if the tool is on the active tools stack return std::find( m_activeTools.begin(), m_activeTools.end(), aTool->GetId() ) != m_activeTools.end(); } void TOOL_MANAGER::saveViewControls( TOOL_STATE* aState ) { aState->vcSettings = m_viewControls->GetSettings(); // If context menu has overridden the cursor position, restore the original position // (see dispatchContextMenu()) if( m_menuActive ) { if( m_origCursor ) { aState->vcSettings.m_forceCursorPosition = true; aState->vcSettings.m_forcedPosition = *m_origCursor; } else { aState->vcSettings.m_forceCursorPosition = false; } } } void TOOL_MANAGER::applyViewControls( TOOL_STATE* aState ) { m_viewControls->ApplySettings( aState->vcSettings ); } void TOOL_MANAGER::pushViewControls() { m_vcStack.push( m_viewControls->GetSettings() ); } void TOOL_MANAGER::popViewControls() { m_viewControls->ApplySettings( m_vcStack.top() ); m_vcStack.pop(); } void TOOL_MANAGER::processEvent( const TOOL_EVENT& aEvent ) { // Early dispatch of events destined for the TOOL_MANAGER if( !dispatchStandardEvents( aEvent ) ) return; dispatchInternal( aEvent ); dispatchActivation( aEvent ); dispatchContextMenu( aEvent ); // Dispatch queue while( !m_eventQueue.empty() ) { TOOL_EVENT event = m_eventQueue.front(); m_eventQueue.pop_front(); processEvent( event ); } }