← Back to team overview

kicad-developers team mailing list archive

[PATCH] ActionPlugin undo and redo feature

 

Dear All,

 Find attached a patch to add undo/redo feature for external python
plugin called from the "External Plugins" menu.

 Attached:
- The patch himself
- A plugin to test is (named testundoredo.py):
   - Implement 3 plugins for undo/redo test purpose only
          - Remove all modules
          - Generate random content
          - Move elements randomly

To test it:

- Apply the patch and compile at least with option
KICAD_SCRIPTING_ACTION_MENU=ON
- Copy the script testundoredo.py into your plugin directory
(~/.kicad_plugins under Linux).
- Open pcbnew (from kicad or directly from command line)
- Run plugins in the order you wish to test it.


Regards,
From 5ec8199885de451d62765068cc889c52c0685ae7 Mon Sep 17 00:00:00 2001
From: Jean-Samuel Reynaud <js.reynaud@xxxxxxxxx>
Date: Thu, 2 Feb 2017 10:40:34 +0100
Subject: [PATCH] Adding Undo/Redo support for ActionPlugin calls from menu
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="------------2.7.4"

This is a multi-part message in MIME format.
--------------2.7.4
Content-Type: text/plain; charset=UTF-8; format=fixed
Content-Transfer-Encoding: 8bit

---
 pcbnew/swig/pcbnew_action_plugins.cpp | 196 ++++++++++++++++++++++++++++++++--
 1 file changed, 187 insertions(+), 9 deletions(-)


--------------2.7.4
Content-Type: text/x-patch; name="0001-Adding-Undo-Redo-support-for-ActionPlugin-calls-from.patch"
Content-Transfer-Encoding: 8bit
Content-Disposition: attachment; filename="0001-Adding-Undo-Redo-support-for-ActionPlugin-calls-from.patch"

diff --git a/pcbnew/swig/pcbnew_action_plugins.cpp b/pcbnew/swig/pcbnew_action_plugins.cpp
index 7118f7f..2ecfbfb 100644
--- a/pcbnew/swig/pcbnew_action_plugins.cpp
+++ b/pcbnew/swig/pcbnew_action_plugins.cpp
@@ -36,6 +36,8 @@
 #include <class_board.h>
 #include <class_module.h>
 #include <class_track.h>
+#include <class_drawsegment.h>
+#include <class_zone.h>
 #include <board_commit.h>
 #include <kicad_device_context.h>
 
@@ -176,14 +178,190 @@ void PCB_EDIT_FRAME::OnActionPlugin( wxCommandEvent& aEvent )
 
     if( actionPlugin )
     {
-        // TODO: Adding recovery point for jobs
-        // BOARD_COMMIT commit( this );
-        // commit.Push( _( "External plugin" ) );
+        PICKED_ITEMS_LIST itemsList;
+        BOARD*  currentPcb  = GetBoard();
+        bool    fromEmpty   = false;
 
-        actionPlugin->Run();
+        itemsList.m_Status = UR_CHANGED;
 
         OnModify();
 
+        // Append tracks:
+        for( BOARD_ITEM* item = currentPcb->m_Track; item != NULL; item = item->Next() )
+        {
+            ITEM_PICKER picker( item, UR_CHANGED );
+            itemsList.PushItem( picker );
+        }
+
+        // Append modules:
+        for( BOARD_ITEM* item = currentPcb->m_Modules; item != NULL; item = item->Next() )
+        {
+            ITEM_PICKER picker( item, UR_CHANGED );
+            itemsList.PushItem( picker );
+        }
+
+        // Append drawings
+        for( BOARD_ITEM* item = currentPcb->m_Drawings; item != NULL; item = item->Next() )
+        {
+            ITEM_PICKER picker( item, UR_CHANGED );
+            itemsList.PushItem( picker );
+        }
+
+        // Append zones outlines
+        for( int ii = 0; ii < currentPcb->GetAreaCount(); ii++ )
+        {
+            ITEM_PICKER picker( (EDA_ITEM*) currentPcb->GetArea(
+                            ii ), UR_CHANGED );
+            itemsList.PushItem( picker );
+        }
+
+        // Append zones segm:
+        for( BOARD_ITEM* item = currentPcb->m_Zone; item != NULL; item = item->Next() )
+        {
+            ITEM_PICKER picker( item, UR_CHANGED );
+            itemsList.PushItem( picker );
+        }
+
+        if( itemsList.GetCount() > 0 )
+            SaveCopyInUndoList( itemsList, UR_CHANGED, wxPoint( 0.0, 0.0 ) );
+        else
+            fromEmpty = true;
+
+        itemsList.ClearItemsList();
+
+        // Execute plugin himself...
+        actionPlugin->Run();
+
+        currentPcb->m_Status_Pcb = 0;
+
+        // Get back the undo buffer to fix some modifications
+        PICKED_ITEMS_LIST* oldBuffer = NULL;
+
+        if( fromEmpty )
+        {
+            oldBuffer = new PICKED_ITEMS_LIST();
+            oldBuffer->m_Status = UR_NEW;
+        }
+        else
+        {
+            oldBuffer = GetScreen()->PopCommandFromUndoList();
+            wxASSERT( oldBuffer );
+        }
+
+        // Try do discover what was modified
+
+        PICKED_ITEMS_LIST deletedItemsList;
+
+        // Found deleted modules
+        for( unsigned int i = 0; i < oldBuffer->GetCount(); i++ )
+        {
+            BOARD_ITEM* item = (BOARD_ITEM*) oldBuffer->GetPickedItem( i );
+            ITEM_PICKER picker( item, UR_DELETED );
+
+            wxASSERT( item );
+
+            switch( item->Type() )
+            {
+            case PCB_NETINFO_T:
+            case PCB_MARKER_T:
+            case PCB_MODULE_T:
+            case PCB_TRACE_T:
+            case PCB_VIA_T:
+            case PCB_LINE_T:
+            case PCB_TEXT_T:
+            case PCB_DIMENSION_T:
+            case PCB_TARGET_T:
+            case PCB_ZONE_T:
+
+                // If item has a list it's mean that the element is on the board
+                if( item->GetList() == NULL )
+                {
+                    deletedItemsList.PushItem( picker );
+                }
+
+                break;
+
+            case PCB_ZONE_AREA_T:
+            {
+                bool zoneFound = false;
+
+                for( int ii = 0; ii < currentPcb->GetAreaCount(); ii++ )
+                    zoneFound |= currentPcb->GetArea( ii ) == item;
+
+                if( !zoneFound )
+                {
+                    deletedItemsList.PushItem( picker );
+                }
+
+                break;
+            }
+
+            default:
+                wxString msg;
+                msg.Printf( wxT( "(PCB_EDIT_FRAME::OnActionPlugin) needs work: "
+                                 "BOARD_ITEM type (%d) not handled" ),
+                        item->Type() );
+                wxFAIL_MSG( msg );
+                break;
+            }
+        }
+
+        // Mark deleted elements in undolist
+        for( unsigned int i = 0; i < deletedItemsList.GetCount(); i++ )
+        {
+            oldBuffer->PushItem( deletedItemsList.GetItemWrapper( i ) );
+        }
+
+        // Find new modules
+        for( BOARD_ITEM* item = currentPcb->m_Modules; item != NULL; item = item->Next() )
+        {
+            if( !oldBuffer->ContainsItem( item ) )
+            {
+                ITEM_PICKER picker( item, UR_NEW );
+                oldBuffer->PushItem( picker );
+            }
+        }
+
+        for( BOARD_ITEM* item = currentPcb->m_Track; item != NULL; item = item->Next() )
+        {
+            if( !oldBuffer->ContainsItem( item ) )
+            {
+                ITEM_PICKER picker( item, UR_NEW );
+                oldBuffer->PushItem( picker );
+            }
+        }
+
+        for( BOARD_ITEM* item = currentPcb->m_Drawings; item != NULL; item = item->Next() )
+        {
+            if( !oldBuffer->ContainsItem( item ) )
+            {
+                ITEM_PICKER picker( item, UR_NEW );
+                oldBuffer->PushItem( picker );
+            }
+        }
+
+        for( BOARD_ITEM* item = currentPcb->m_Zone; item != NULL; item = item->Next() )
+        {
+            if( !oldBuffer->ContainsItem( item ) )
+            {
+                ITEM_PICKER picker( item, UR_NEW );
+                oldBuffer->PushItem( picker );
+            }
+        }
+
+        for( int ii = 0; ii < currentPcb->GetAreaCount(); ii++ )
+        {
+            if( !oldBuffer->ContainsItem( (EDA_ITEM*) currentPcb->GetArea( ii ) ) )
+            {
+                ITEM_PICKER picker( (EDA_ITEM*) currentPcb->GetArea(
+                                ii ), UR_NEW );
+                oldBuffer->PushItem( picker );
+            }
+        }
+
+
+        GetScreen()->PushCommandToUndoList( oldBuffer );
+
         if( IsGalCanvasActive() )
         {
             UseGalCanvas( GetGalCanvas() );
@@ -201,7 +379,7 @@ void PCB_EDIT_FRAME::RebuildActionPluginMenus()
 {
     wxMenu* actionMenu = GetMenuBar()->FindItem( ID_TOOLBARH_PCB_ACTION_PLUGIN )->GetSubMenu();
 
-    if( !actionMenu )   // Should not occur.
+    if( !actionMenu ) // Should not occur.
         return;
 
     // First, remove existing submenus, if they are too many
@@ -235,7 +413,7 @@ void PCB_EDIT_FRAME::RebuildActionPluginMenus()
     {
         wxMenuItem* item;
 
-        if( ii < (int)available_menus.size() )
+        if( ii < (int) available_menus.size() )
         {
             item = available_menus[ii];
             item->SetItemLabel( ACTION_PLUGINS::GetAction( ii )->GetName() );
@@ -248,9 +426,9 @@ void PCB_EDIT_FRAME::RebuildActionPluginMenus()
                     ACTION_PLUGINS::GetAction( ii )->GetDescription(),
                     KiBitmap( hammer_xpm ) );
 
-                Connect( item->GetId(), wxEVT_COMMAND_MENU_SELECTED,
-                         (wxObjectEventFunction) (wxEventFunction) (wxCommandEventFunction) &
-                         PCB_EDIT_FRAME::OnActionPlugin );
+            Connect( item->GetId(), wxEVT_COMMAND_MENU_SELECTED,
+                    (wxObjectEventFunction) (wxEventFunction) (wxCommandEventFunction) &
+                    PCB_EDIT_FRAME::OnActionPlugin );
         }
 
         ACTION_PLUGINS::SetActionMenu( ii, item->GetId() );

--------------2.7.4--


from pcbnew import *
import random
import pprint

class testundoredo1(ActionPlugin):

    def defaults(self):
        self.name = "Test Undo/Redo: Remove all modules"
        self.category = "Test Undo/Redo"
        self.description = ""

    def Run(self):
        pcb = GetBoard()

        while pcb.GetAreaCount() > 0:
            area = pcb.GetArea(0)
            pcb.RemoveNative(area)

        for module in pcb.GetModules():
            pcb.RemoveNative(module)

        for track in pcb.GetTracks():
            pcb.RemoveNative(track)

        for draw in pcb.m_Drawings:
            pcb.RemoveNative(draw)

class testundoredo2(ActionPlugin):

    def defaults(self):
        self.name = "Test Undo/Redo: Generate random content"
        self.category = "Test Undo/Redo"
        self.description = ""

    def createFPCXModule(self,pads):
        size_025_160mm = wxSizeMM(0.25,1.6)
        size_150_200mm = wxSizeMM(1.50,2.0)
        # create a new module, it's parent is our previously created pcb
        module = MODULE(self.pcb)
        module.SetReference("FPC"+str(pads))   # give it a reference name
        module.Reference().SetPosition(wxPointMM(-1,-1))
        self.pcb.Add(module)             # add it to our pcb
        m_pos = wxPointMM(0,0)#random.randint(10,200),random.randint(10,200))
        module.SetPosition(m_pos)

        # create a pad array and add it to the module


        def smdRectPad(module,size,pos,name):
            pad = D_PAD(module)
            pad.SetSize(size)
            pad.SetShape(PAD_SHAPE_RECT)
            pad.SetAttribute(PAD_ATTRIB_SMD)
            pad.SetLayerSet(pad.SMDMask())
            pad.SetPosition(pos)
            pad.SetPadName(name)
            return pad

        for n in range (0,pads):
            pad = smdRectPad(module,size_025_160mm,wxPointMM(0.5*n,0),str(n+1))
            module.Add(pad)


        pad_s0 = smdRectPad(module,size_150_200mm,wxPointMM(-1.6,1.3),"0")
        pad_s1 = smdRectPad(module,size_150_200mm,wxPointMM((pads-1)*0.5+1.6,1.3),"0")
        module.Add(pad_s0)
        module.Add(pad_s1)

        e = EDGE_MODULE(module)
        e.SetStart0(wxPointMM(-1,0))
        e.SetEnd0(wxPointMM(0,0))
        e.SetWidth(FromMM(0.2))
        e.SetLayer(F_SilkS)
        e.SetShape(S_SEGMENT)
        module.Add(e)
        module.SetPosition(wxPointMM(random.randint(20,200),random.randint(20,150)))
        return module



    def Run(self):
        self.pcb = GetBoard()
        random.seed()

        for i in range(10):
            seg = DRAWSEGMENT()
            seg.SetLayer( random.choice([Edge_Cuts,Cmts_User,Eco1_User,Eco2_User]) )
            seg.SetStart( wxPointMM( random.randint(10,100),
                                random.randint(10,100) ) )
            seg.SetEnd( wxPointMM( random.randint(10,100),
                                random.randint(10,100) ) )
            self.pcb.Add( seg )

            if i%2 == 0:
                t = TRACK(None)
            else:
                t = VIA(None)
                #t.SetLayerPair(segments['layerPair'][0],segments['layerPair'][1])
                t.SetViaType(VIA_THROUGH)
                t.SetDrill(FromMM(random.randint(1,20)/10.0))
            t.SetStart(wxPointMM(random.randint(100,150),random.randint(100,150)))
            t.SetEnd(wxPointMM(random.randint(100,150),random.randint(100,150)))
            t.SetWidth(FromMM(random.randint(1,15)/10.0))
            t.SetLayer(random.choice([F_Cu,B_Cu]))
            self.pcb.Add(t)

            self.createFPCXModule(random.randint(2,40))


class testundoredo3(ActionPlugin):

    def defaults(self):
        self.name = "Test Undo/Redo: Move elements randomly"
        self.category = "Test Undo/Redo"
        self.description = ""

    def Run(self):
        pcb = GetBoard()

        for i in range(0,pcb.GetAreaCount()):
            area = pcb.GetArea(i)
            area.Move(wxPointMM(random.randint(-20,20),random.randint(-20,20)))

        for module in pcb.GetModules():
            module.Move(wxPointMM(random.randint(-20,20),random.randint(-20,20)))
            if random.randint(0,10) > 5:
                module.Flip(module.GetPosition())

        for track in pcb.GetTracks():
            track.Move(wxPointMM(random.randint(-20,20),random.randint(-20,20)))

        for draw in pcb.m_Drawings:
            draw.Move(wxPointMM(random.randint(-20,20),random.randint(-20,20)))



testundoredo1().register()
testundoredo2().register()
testundoredo3().register()

Follow ups