Tool Framework documentation

This commit is contained in:
John Beard 2017-01-18 16:26:08 +01:00 committed by Maciej Suminski
parent bb463ad434
commit 5a2be26934
1 changed files with 491 additions and 0 deletions

View File

@ -0,0 +1,491 @@
# Tool Framework #
This document briefly outlines the structure of the tool system in the
GAL canvases.
[TOC]
# Introduction # {#intro}
The GAL framework provides a powerful method of easily adding tools to
KiCad. A GAL "tool" is a class which provides one or more "actions"
to perform. An action can be a simple one-off action (e.g. "zoom in"
or "flip object"), or an interactive process (e.g. "manually edit
polygon points").
Some examples of tools in the Pcbnew GAL are:
* The selection tool - the "normal" tool. This tool enters a state where
items can be added to a list of selected objects, which are then made
available for other tools to act on.
(pcbnew/tools/selection_tool.cpp, pcbnew/tools/selection_tool.h)
* The edit tool - this tool is active when a component is "picked up",
and tracks the mouse position to allow the user to move a component.
Aspects of editing (e.g. flip) are also make available for use by
hotkeys or other tools.
(pcbnew/tools/edit_tool.cpp,pcbnew/tools/edit_tool.h)
* The drawing tool - this tool controls the process of drawing graphics
elements such as line segments and circles.
(pcbnew/tools/drawing_tool.cpp,pcbnew/tools/drawing_tool.h)
* The zoom tool - allows the user to zoom in and out
## Major parts of a tool
There are two main aspects to tools: the actions and the the tool class.
### Tool actions
The `TOOL_ACTION` class acts as a handle for the GAL framework to
call on actions provided by tools. Generally, every action, interactive
or not, has a `TOOL_ACTION` instance. This provides:
* A "name", which is of the format `pcbnew.ToolName.actionName`, which
is used internally to dispatch the action
* A "scope", which determines when the tools is available:
* `AS_CONTEXT`, when the action is specific to a particular tool. For
example, `pcbnew.InteractiveDrawing.incWidth` increases the width
of a line while the line is still being drawn.
* `AS_GLOBAL`, when the tool can always be invoked, by a hotkey, or
during the execution of a different tool. For example, the zoom
actions can be accessed from the selection tool's menu during the
interactive selection process.
* A "default hotkey", which is used if the user doesn't provide their
own configuration.
* A "menu item", which is the (translatable) string shown when this tool
is accessed from a menu.
* A "menu description", which is the string shown in the menu item's
tooltip and provides a more detailed description if needed.
* An "icon", which is shown in menus and on buttons for the action
* "Flags" which include:
* `AF_ACTIVATE` which indicates that the tool enters an active state
* A parameter, which allows different actions to call the same function
with different effects, for example "step left" and "step right".
### The tool class
GAL tools inherit the `TOOL_BASE` class. A Pcbnew tool will generally
inherit from `PCB_TOOL`, which is a `TOOL_INTERACTIVE`, which is
a `TOOL_BASE`. In the future, Eeschema tools will be developed in a similar
manner.
The tool class for a tool can be fairly lightweight - much of the
functionality is inherited from the tool's base classes. These base
classes provide access to several things, particularly:
* Access to the `PCB_EDIT_FRAME`, which can be used to modify the
viewport, set cursors and status bar content, etc.
* Access to the `TOOL_MANAGER` which can be used to access other tools'
actions.
* Access to the `BOARD` object which is used to modify the PCB content.
* Access to the `KIGFX::VIEW`, which is used to manipulate the GAL canvas.
The major parts of tool's implementation are the functions used by the
`TOOL_MANAGER` to set up and manage the tool:
* Constructor and destructor to establish whatever class members are
required.
* The TOOL_BASE class requires a string to be passed for the
tool name, which normally looks like `pcbnew.ToolName`.
* `Init()` function (optional), which is commonly used to fill in
a context menu, either belonging to this tool, or access another
tool's menu and add items to that. This function is called once, when
the tool is registered with the tool manager.
* `Reset()` function, called when the model (e.g. the `BOARD`) is reloaded,
when the GAL canvas is switched, and also just after tool registration.
Any resource claimed from the GAL view or the model must be released
in this function, as they could become invalid.
* `SetTransitions()` function, which maps tool actions to functions
within the tool class.
* One or more functions to call when actions are invoked. Many actions
can invoke the same function if desired. The functions have the
following signature:
* int TOOL_CLASS::FunctionName( const TOOL_EVENT& aEvent )
* Returning 0 means success.
* These functions are called by the `TOOL_MANAGER` in case an associated
event arrives (association is created with TOOL_INTERACTIVE::Go() function).
* These can generally be private, as they are not called directly
by any other code, but are invoked by the tool manager's coroutine
framework according to the `SetTransitions()` map.
#### Interactive actions
The action handlers for an interactive actions handle repeated actions
from the tool manager in a loop, until an action indicating that the
tool should exit.
Interactive tools also normally indicate that they are active with
a cursor change and by setting a status string.
int TOOL_NAME::someAction( const TOOL_EVENT& aEvent )
{
auto& frame = *getEditFrame<PCB_EDIT_FRAME>();
// set tool hint and cursor (actually looks like a crosshair)
frame.SetToolID( ID_PCB_SHOW_1_RATSNEST_BUTT,
wxCURSOR_PENCIL, _( "Select item to move left" ) );
getViewControls()->ShowCursor( true );
// activate the tool, now it will be the first one to receive events
// you can skip this, if you are writing a handler for a single action
// (e.g. zoom in), opposed to interactive tool that requires further
// events to operate (e.g. dragging a component)
Activate();
// the main event loop
while( OPT_TOOL_EVENT evt = Wait() )
{
if( evt->IsCancel() || evt->IsActivate() )
{
// end of interactive tool
break;
}
else if( evt->IsClick( BUT_LEFT ) )
{
// do something here
}
// other events...
}
// reset the PCB frame to how it was when we got it
frame.SetToolID( ID_NO_TOOL_SELECTED, wxCURSOR_DEFAULT, wxEmptyString );
getViewControls()->ShowCursor( false );
return 0;
}
### The tool menu
Top level tools, i.e. tools that the user enters directly, usually
provide their own context menu. Tools that are called only from other
tools' interactive modes add their menu items to those tools' menus.
To use a `TOOL_MENU` in a top level tool, simply add one as a member
and initialise it with a reference to the tools at construction time:
TOOL_NAME: public PCB_TOOL
{
public:
TOOL_NAME() :
PCB_TOOL( "pcbnew.MyNewTool" ),
m_menu( *this )
{}
private:
TOOL_MENU m_menu;
}
You can then add a menu accessor, or provide a custom function to
allow other tools to add any other actions, or a subset that you
think appropriate.
You can then invoke the menu from an interactive tool loop by
calling `m_menu.ShowContextMenu()`. Clicking on the tool's entry in
this menu will trigger the action - there is no further action
needed in your tool's event loop.
# Tutorial: Adding a new tool
Without getting too heavily into the details of how the GAL tool framework
is implemented under the surface, let's look at how you could add a
brand new tool to Pcbnew. Our tool will have the following (rather
useless) functions:
* An interactive tool which will allow the user to select a point,
choose from the items at that point and then move that item 10mm to
the left.
* While in this mode, the context menu will have more options:
* Use of the "normal" canvas zoom and grid options
* A non-interactive tool which will add a fixed circle at a fixed point.
* A way to invoke the non-interactive "unfill all zones" tool from
the PCB_EDITOR_CONTROL tool.
## Add tool actions
The first step is to add tool actions. We will implement two actions
named:
* `Pcbnew.UselessTool.MoveItemLeft` - the interactive tool
* `Pcbnew.UselessTool.FixedCircle` - the non-interactive tool.
The "unfill tool" already exists with the name
`pcbnew.EditorControl.zoneUnfillAll`.
In `pcbnew/tools/common_action.h`, we add the following to the
`COMMON_ACTION` class, which declares our tools:
static TOOL_ACTION uselessMoveItemLeft;
static TOOL_ACTION uselessFixedCircle;
In `pcbnew/tools/common_action.cpp`, we then define the actions:
TOOL_ACTION COMMON_ACTIONS::uselessMoveItemLeft(
"pcbnew.UselessTool.MoveItemLeft",
AS_GLOBAL, MD_CTRL + MD_SHIFT + int( 'L' ),
_( "Move item left" ), _( "Select and move item left" ) );
TOOL_ACTION COMMON_ACTIONS::uselessFixedCircle(
"pcbnew.UselessTool.FixedCircle",
AS_GLOBAL, MD_CTRL + MD_SHIFT + int( 'C' ),
_( "Fixed circle" ), _( "Add a fixed size circle in a fixed place" ),
add_circle_xpm );
We have defined hotkeys for each action, and they are both global. This
means you can use `Shift+Ctrl+L` and `Shift-Ctrl-R` to access each tool
respectively.
We defined an icon for one of the tools, which should appear in any
menu the item is added to.
We now have two actions defined, but they are not connected to anything.
We need to define a functions which implement the right actions.
You can add these to an existing tool (for example `PCB_EDITOR_CONTROL`,
which deals with many general PCB modification operation like zone
filling), or you can write a whole new tool to keep things separate
and give you more scope for adding tool state.
We will write our own tool to demonstrate the process.
## Add tool class declaration
Add a new tool class header `pcbnew/tools/useless_tool.h` containing
the following class:
class USELESS_TOOL : public PCB_TOOL
{
public:
USELESS_TOOL();
~USELESS_TOOL();
///> React to model/view changes
void Reset( RESET_REASON aReason ) override;
///> Basic initalization
bool Init() override;
///> Bind handlers to corresponding TOOL_ACTIONs
void SetTransitions() override;
private:
///> 'Move selected left' interactive tool
int moveLeft( const TOOL_EVENT& aEvent );
///> Internal function to perform the move left action
void moveLeftInt();
///> Add a fixed size circle
int fixedCircle( const TOOL_EVENT& aEvent );
///> Menu model displayed by the tool.
TOOL_MENU m_menu;
};
## Implement tool class methods:
In the `pcbnew/tools/useless_tool.cpp`, implement the required methods.
In this file, you might also add free function helpers, other classes,
and so on.
You will need to add this file to the `pcbnew/CMakeLists.txt` to
build it.
Below you will find the contents of useless_tool.cpp:
#include "useless_tool.h"
#include <wxPcbStruct.h>
#include <class_draw_panel_gal.h>
#include <view/view_controls.h>
#include <view/view.h>
#include <tool/tool_manager.h>
#include <pcbnew_id.h>
#include <class_board_item.h>
#include <class_drawsegment.h>
#include <board_commit.h>
#include "common_actions.h"
#include "selection_tool.h"
USELESS_TOOL::USELESS_TOOL() :
PCB_TOOL( "pcbnew.UselessTool" ),
m_menu( *this )
{
}
USELESS_TOOL::~USELESS_TOOL()
{}
void USELESS_TOOL::Reset( RESET_REASON aReason )
{
}
bool USELESS_TOOL::Init()
{
auto& menu = m_menu.GetMenu();
// add our own tool's action
menu.AddItem( COMMON_ACTIONS::uselessFixedCircle);
// add the PCB_EDITOR_CONTROL's zone unfill all action
menu.AddItem( COMMON_ACTIONS::zoneUnfillAll);
// Add standard zoom and grid tool actions
m_menu.AddStandardSubMenus( *getEditFrame<PCB_BASE_FRAME>() );
return true;
}
void USELESS_TOOL::moveLeftInt()
{
// we will call actions on the selection tool to get the current
// selection. The selection tools will handle item deisambiguation
SELECTION_TOOL* selectionTool = m_toolMgr->GetTool<SELECTION_TOOL>();
assert( selectionTool );
// call the actions
m_toolMgr->RunAction( COMMON_ACTIONS::selectionClear, true );
m_toolMgr->RunAction( COMMON_ACTIONS::selectionCursor, true );
selectionTool->SanitizeSelection();
const SELECTION& selection = selectionTool->GetSelection();
// nothing selected, return to event loop
if( selection.Empty() )
return;
// iterate BOARD_ITEM* container, moving each item
for( auto item : selection )
{
item->Move( wxPoint(-5 * IU_PER_MM, 0) );
}
}
int USELESS_TOOL::moveLeft( const TOOL_EVENT& aEvent )
{
auto& frame = *getEditFrame<PCB_EDIT_FRAME>();
// set tool hint and cursor (actually looks like a crosshair)
frame.SetToolID( ID_PCB_SHOW_1_RATSNEST_BUTT,
wxCURSOR_PENCIL, _( "Select item to move left" ) );
getViewControls()->ShowCursor( true );
Activate();
// handle tool events for as long as the tool is active
while( OPT_TOOL_EVENT evt = Wait() )
{
if( evt->IsCancel() || evt->IsActivate() )
{
// end of interactive tool
break;
}
else if( evt->IsClick( BUT_RIGHT ) )
{
m_menu.ShowContextMenu();
}
else if( evt->IsClick( BUT_LEFT ) )
{
// invoke the main action logic
moveLeftInt();
// keep showing the edit cursor
getViewControls()->ShowCursor( true );
}
}
// reset the PCB frame to how it was we got it
frame.SetToolID( ID_NO_TOOL_SELECTED, wxCURSOR_DEFAULT, wxEmptyString );
getViewControls()->ShowCursor( false );
// exit action
return 0;
}
int USELESS_TOOL::fixedCircle( const TOOL_EVENT& aEvent )
{
auto& frame = *getEditFrame<PCB_EDIT_FRAME>();
// new circle to add (ideally use a smart pointer)
DRAWSEGMENT* circle = new DRAWSEGMENT;
// Set the circle attributes
circle->SetShape( S_CIRCLE );
circle->SetWidth( 5 * IU_PER_MM );
circle->SetStart( wxPoint( 50 * IU_PER_MM, 50 * IU_PER_MM ) );
circle->SetEnd( wxPoint( 80 * IU_PER_MM, 80 * IU_PER_MM ) );
circle->SetLayer( LAYER_ID::F_SilkS );
// commit the circle to the BOARD
BOARD_COMMIT commit( &frame );
commit.Add( circle );
commit.Push( _( "Draw a circle" ) );
return 0;
}
void USELESS_TOOL::SetTransitions()
{
Go( &USELESS_TOOL::fixedCircle, COMMON_ACTIONS::uselessFixedCircle.MakeEvent() );
Go( &USELESS_TOOL::moveLeft, COMMON_ACTIONS::uselessMoveItemLeft.MakeEvent() );
}
## Register the tool
The last step is to register the tool in the tool manager.
This is done by adding a new instance of the tool to the
`registerAllTools()` function in `pcbnew/tools/tools_common.cpp`.
Note - because the `RegisterTool()` function calls the tool's `Init()`
function, any _tools_ that your tools refers to in the init function
must already be registered so that your tool can access it via the
tool manager. Equally, if your tool is referenced by another in its
`Init()` function, your tool must be registered. In general, "top
level" tools go first, and other tools add items to their menus later.
If you register _actions_, that's OK, by the time the menu is invoked,
the tools will all be ready and bound to actions.
In our case, it doesn't matter as our menu is not touched by anyone
else, we only add an _action_.
// add your tool header
#include <tools/useless_tool.h>
void registerAllTools( TOOL_MANAGER *aToolManager )
{
....
aToolManager->RegisterTool( new USELESS_TOOL );
....
}
## Build and run
When this is all done, you should have modified the following files:
* `pcbnew/tools/common_actions.h` - action declarations
* `pcbnew/tools/common_actions.cpp` - action definitions
* `pcbnew/tools/useless_tool.h` - your tool header
* `pcbnew/tools/useless_tool.cpp` - your tool implementation
* `pcbnew/tools/tools_common.cpp` - registration of your tool
* `pcbnew/CMakeLists.txt` - for building the new .cpp files
When you run Pcbnew, you should be able to press `Shift+Ctrl+L` to
enter the "move item left" tool - the cursor will change to a crosshair
and "Select item to move left" appears in the bottom right corner.
When you right-click, you get a menu, which contains an entry for
our "create fixed circle" tool and one for the existing "unfill all
zones" tool which we added to the menu. You can also use `Shift+Ctrl+R`
to access the fixed circle action.
Congratulations, you have just created your first KiCad tool!