Files
kicad-source-mirror/common/dialogs/rule_editor_dialog_base.cpp
T
2026-03-07 19:28:46 +00:00

1200 lines
39 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2024 KiCad Developers, see AUTHORS.txt for contributors.
*
* 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 3 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, see <http://www.gnu.org/licenses/>.
*/
#include <widgets/resettable_panel.h>
#include <widgets/wx_infobar.h>
#include <widgets/wx_panel.h>
#include <widgets/ui_common.h>
#include <wx/button.h>
#include <wx/grid.h>
#include <wx/sizer.h>
#include <wx/treectrl.h>
#include <wx/listctrl.h>
#include <wx/stc/stc.h>
#include <wx/menu.h>
#include <wx/log.h>
#include <wx/wx.h>
#include <wx/dnd.h>
#include <wx/dcclient.h> // Include for wxClientDC if needed
#include <wx/dcbuffer.h> // Include for double-buffered drawing
#include <wx/bitmap.h>
#include <confirm.h>
#include <paths.h>
#include <bitmaps.h>
#include <launch_ext.h>
#include <algorithm>
#include <dialogs/rule_editor_dialog_base.h>
#include <widgets/std_bitmap_button.h>
static wxSearchCtrl* CreateTextFilterBox( wxWindow* aParent, const wxString& aDescriptiveText )
{
wxSearchCtrl* search_widget = new wxSearchCtrl( aParent, wxID_ANY );
search_widget->ShowSearchButton( false );
search_widget->ShowCancelButton( true );
search_widget->SetDescriptiveText( aDescriptiveText );
#ifdef __WXGTK__
// wxSearchCtrl vertical height is not calculated correctly on some GTK setups
// See https://gitlab.com/kicad/code/kicad/-/issues/9019
search_widget->SetMinSize( wxSize( -1, aParent->GetTextExtent( wxT( "qb" ) ).y + 10 ) );
#endif
return search_widget;
}
RULE_EDITOR_DIALOG_BASE::RULE_EDITOR_DIALOG_BASE( wxWindow* aParent, const wxString& aTitle,
const wxSize& aInitialSize ) :
DIALOG_SHIM( aParent, wxID_ANY, aTitle, wxDefaultPosition, aInitialSize,
wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER ),
m_isDragging( false ),
m_enableMoveUp( false ),
m_enableMoveDown( false ),
m_enableAddRule( false ),
m_enableDuplicateRule( false ),
m_enableDeleteRule( false ),
m_modified( false ),
m_defaultSashPosition( 200 ),
m_title( aTitle ),
m_selectedData( nullptr ),
m_previousId( nullptr ),
m_draggedItem( nullptr )
{
wxBoxSizer* mainSizer = new wxBoxSizer( wxVERTICAL );
SetSizer( mainSizer );
wxBoxSizer* infoBarSizer = new wxBoxSizer( wxHORIZONTAL );
m_infoBar = new WX_INFOBAR( this );
infoBarSizer->Add( m_infoBar, 0, wxEXPAND, 0 );
// Add the tree panel to the content sizer
m_splitter = new wxSplitterWindow( this, wxID_ANY, wxDefaultPosition, wxDefaultSize,
wxBORDER_NONE | wxSP_LIVE_UPDATE | wxSP_3DSASH );
m_splitter->SetMinimumPaneSize( 50 ); // Set a minimum pane size
// Create the tree control panel
WX_PANEL* treeCtrlPanel = new WX_PANEL( m_splitter, wxID_ANY, wxDefaultPosition, wxDefaultSize,
wxBORDER_NONE | wxTAB_TRAVERSAL );
treeCtrlPanel->SetBorders( false, false, false, true );
wxBoxSizer* treeCtrlSizer = new wxBoxSizer( wxVERTICAL );
treeCtrlPanel->SetSizer( treeCtrlSizer );
// Add a search text box above the tree control
m_filterSearch = CreateTextFilterBox( treeCtrlPanel, _( "Type filter text" ) );
treeCtrlSizer->Add( m_filterSearch, 0, wxEXPAND, 5 );
// Create the tree control and set its font
m_ruleTreeCtrl = new wxTreeCtrl( treeCtrlPanel, wxID_ANY, wxDefaultPosition, wxDefaultSize,
wxTR_DEFAULT_STYLE | wxTR_HIDE_ROOT | wxTR_HAS_BUTTONS | wxBORDER_NONE );
m_ruleTreeCtrl->SetFont( KIUI::GetControlFont( this ) );
// Add the tree control to its sizer
treeCtrlSizer->Add( m_ruleTreeCtrl, 1, wxEXPAND | wxBOTTOM, 3 );
// Create a sizer for the action buttons
wxBoxSizer* actionButtonsSizer = new wxBoxSizer( wxHORIZONTAL );
// Create the action buttons
m_addRuleButton = new STD_BITMAP_BUTTON( treeCtrlPanel, wxID_ANY, wxNullBitmap, wxDefaultPosition,
wxDefaultSize, wxBU_AUTODRAW );
m_copyRuleButton = new STD_BITMAP_BUTTON( treeCtrlPanel, wxID_ANY, wxNullBitmap, wxDefaultPosition,
wxDefaultSize, wxBU_AUTODRAW );
m_moveTreeItemUpButton = new STD_BITMAP_BUTTON( treeCtrlPanel, wxID_ANY, wxNullBitmap, wxDefaultPosition,
wxDefaultSize, wxBU_AUTODRAW );
m_moveTreeItemDownButton = new STD_BITMAP_BUTTON( treeCtrlPanel, wxID_ANY, wxNullBitmap, wxDefaultPosition,
wxDefaultSize, wxBU_AUTODRAW );
m_deleteRuleButton = new STD_BITMAP_BUTTON( treeCtrlPanel, wxID_ANY, wxNullBitmap, wxDefaultPosition,
wxDefaultSize, wxBU_AUTODRAW );
actionButtonsSizer->Add( m_addRuleButton, 0, wxLEFT, 5 );
actionButtonsSizer->Add( m_copyRuleButton, 0, wxLEFT, 5 );
actionButtonsSizer->Add( m_moveTreeItemUpButton, 0, wxLEFT, 5 );
actionButtonsSizer->Add( m_moveTreeItemDownButton, 0, wxLEFT, 5 );
// Add a spacer between the "Move" buttons and the "Delete" button
actionButtonsSizer->AddSpacer( 20 ); // Spacer between the Move and Delete buttons
// Add the "Delete" button at the end
actionButtonsSizer->Add( m_deleteRuleButton, 0, wxRIGHT, 5 );
// Add the action buttons sizer to the tree control sizer
treeCtrlSizer->Add( actionButtonsSizer, 0, wxBOTTOM | wxEXPAND, 5 );
treeCtrlPanel->Layout();
m_addRuleButton->SetBitmap( KiBitmapBundle( BITMAPS::small_plus ) );
m_copyRuleButton->SetBitmap( KiBitmapBundle( BITMAPS::copy ) );
m_moveTreeItemUpButton->SetBitmap( KiBitmapBundle( BITMAPS::small_up ) );
m_moveTreeItemDownButton->SetBitmap( KiBitmapBundle( BITMAPS::small_down ) );
m_deleteRuleButton->SetBitmap( KiBitmapBundle( BITMAPS::small_trash ) );
// Create a scrolled window for the dynamic content panel
m_scrolledContentWin = new wxScrolledWindow( m_splitter, wxID_ANY, wxDefaultPosition, wxDefaultSize,
wxVSCROLL | wxBORDER_NONE );
m_scrolledContentWin->SetScrollRate( 0, 10 );
m_scrolledContentWin->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_FRAMEBK ) );
m_contentPanel = nullptr;
// Add the tree control panel to the splitter.
// Calculate the minimum tree panel width based on the tree control's best size.
treeCtrlPanel->Layout();
int minTreeWidth = treeCtrlPanel->GetBestSize().GetWidth();
// Ensure a reasonable minimum width for the tree panel
if( minTreeWidth < 240 )
minTreeWidth = 240;
m_defaultSashPosition = minTreeWidth;
m_splitter->SplitVertically( treeCtrlPanel, m_scrolledContentWin, m_defaultSashPosition );
// Set gravity to 0 so the tree panel maintains its size and the content panel resizes
m_splitter->SetSashGravity( 0.0 );
wxBoxSizer* m_sizerButtons = new wxBoxSizer( wxHORIZONTAL );
wxStdDialogButtonSizer* m_sdbSizer = new wxStdDialogButtonSizer();
m_cancelRuleButton = new wxButton( this, wxID_CANCEL );
m_cancelRuleButton->SetLabelText( _( "Close" ) );
m_sdbSizer->AddButton( m_cancelRuleButton );
m_saveRuleButton = new wxButton( this, wxID_OK );
m_saveRuleButton->SetLabelText( _( "Save" ) );
m_sdbSizer->AddButton( m_saveRuleButton );
m_sdbSizer->Realize();
m_sizerButtons->Add( m_sdbSizer, 1, wxEXPAND, 5 );
// Add the info bar sizer to the main sizer
mainSizer->Add( infoBarSizer, 0, wxEXPAND, 0 );
mainSizer->Add( m_splitter, 1, wxEXPAND | wxBOTTOM, 5 );
mainSizer->Add( m_sizerButtons, 0, wxALL | wxEXPAND, 5 );
if( aInitialSize != wxDefaultSize )
SetSize( aInitialSize );
Layout();
// Bind the context menu event
m_filterSearch->Bind( wxEVT_COMMAND_TEXT_UPDATED, &RULE_EDITOR_DIALOG_BASE::onFilterSearch, this );
m_ruleTreeCtrl->Bind( wxEVT_TREE_ITEM_RIGHT_CLICK, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemRightClick, this );
m_ruleTreeCtrl->Bind( wxEVT_TREE_SEL_CHANGED, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemSelectionChanged, this );
m_ruleTreeCtrl->Bind( wxEVT_TREE_ITEM_ACTIVATED, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemActivated, this );
m_ruleTreeCtrl->Bind( wxEVT_LEFT_DOWN, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemLeftDown, this );
m_ruleTreeCtrl->Bind( wxEVT_MOTION, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemMouseMotion, this );
m_ruleTreeCtrl->Bind( wxEVT_LEFT_UP, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemLeftUp, this );
m_addRuleButton->Bind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::onNewRuleOptionClick, this );
m_copyRuleButton->Bind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::onDuplicateRuleOptionClick, this );
m_moveTreeItemUpButton->Bind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::onMoveUpRuleOptionClick, this );
m_moveTreeItemDownButton->Bind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::onMoveDownRuleOptionClick, this );
m_deleteRuleButton->Bind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::onDeleteRuleOptionClick, this );
m_saveRuleButton->Bind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::OnSave, this );
m_cancelRuleButton->Bind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::OnCancel, this );
// Initially disable Save button until modifications are made
m_saveRuleButton->Enable( false );
this->Bind( wxEVT_SIZE, &RULE_EDITOR_DIALOG_BASE::onResize, this );
this->Bind( wxEVT_CLOSE_WINDOW, &RULE_EDITOR_DIALOG_BASE::onClose, this );
m_scrolledContentWin->Bind( wxEVT_SIZE,
[this]( wxSizeEvent& evt )
{
RefreshContentScrollArea();
evt.Skip();
} );
}
void RULE_EDITOR_DIALOG_BASE::finishInitialization()
{
finishDialogSettings();
// finishDialogSettings() calls GetSizer()->SetSizeHints() which inflates the dialog
// to the full unconstrained best size of the scrolled content area. Reset to the
// caller's initial size and a reasonable minimum so Show() doesn't inherit the
// inflated values.
SetMinSize( wxSize( 400, 500 ) );
SetSize( m_initialSize );
wxLogTrace( "debug_dlg_size", "finishInitialization: size=%dx%d minSize=%dx%d bestSize=%dx%d",
GetSize().x, GetSize().y, GetMinSize().x, GetMinSize().y, GetBestSize().x,
GetBestSize().y );
}
bool RULE_EDITOR_DIALOG_BASE::Show( bool show )
{
bool ret = DIALOG_SHIM::Show( show );
if( show )
{
// DIALOG_SHIM::Show() sets SetMinSize(GetBestSize()) which includes the unconstrained
// height of the scrolled content area. Cap the minimum to a usable value so the user
// can resize the dialog smaller.
wxSize minSize = GetMinSize();
if( minSize.GetHeight() > 500 )
SetMinSize( wxSize( minSize.GetWidth(), 500 ) );
wxLogTrace( "debug_dlg_size", "Show: size=%dx%d minSize=%dx%d",
GetSize().x, GetSize().y, GetMinSize().x, GetMinSize().y );
}
return ret;
}
RULE_EDITOR_DIALOG_BASE::~RULE_EDITOR_DIALOG_BASE()
{
m_filterSearch->Unbind( wxEVT_COMMAND_TEXT_UPDATED, &RULE_EDITOR_DIALOG_BASE::onFilterSearch, this );
m_ruleTreeCtrl->Unbind( wxEVT_TREE_ITEM_RIGHT_CLICK, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemRightClick, this );
m_ruleTreeCtrl->Unbind( wxEVT_TREE_SEL_CHANGED, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemSelectionChanged, this );
m_ruleTreeCtrl->Unbind( wxEVT_TREE_ITEM_ACTIVATED, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemActivated, this );
m_ruleTreeCtrl->Unbind( wxEVT_LEFT_DOWN, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemLeftDown, this );
m_ruleTreeCtrl->Unbind( wxEVT_MOTION, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemMouseMotion, this );
m_ruleTreeCtrl->Unbind( wxEVT_LEFT_UP, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemLeftUp, this );
m_addRuleButton->Unbind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::onNewRuleOptionClick, this );
m_copyRuleButton->Unbind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::onDuplicateRuleOptionClick, this );
m_moveTreeItemUpButton->Unbind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::onMoveUpRuleOptionClick, this );
m_moveTreeItemDownButton->Unbind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::onMoveDownRuleOptionClick, this );
m_deleteRuleButton->Unbind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::onDeleteRuleOptionClick, this );
m_saveRuleButton->Unbind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::OnSave, this );
m_cancelRuleButton->Unbind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::OnCancel, this );
this->Unbind( wxEVT_SIZE, &RULE_EDITOR_DIALOG_BASE::onResize, this );
this->Unbind( wxEVT_CLOSE_WINDOW, &RULE_EDITOR_DIALOG_BASE::onClose, this );
m_selectedData = nullptr;
m_previousId = nullptr;
}
bool RULE_EDITOR_DIALOG_BASE::TransferDataToWindow()
{
finishInitialization();
// Call TransferDataToWindow() only once:
// this is enough on wxWidgets 3.1
if( !DIALOG_SHIM::TransferDataToWindow() )
return false;
return true;
}
bool RULE_EDITOR_DIALOG_BASE::TransferDataFromWindow()
{
bool ret = true;
// Call TransferDataFromWindow() only once:
// this is enough on wxWidgets 3.1
if( !DIALOG_SHIM::TransferDataFromWindow() )
ret = false;
return ret;
}
void RULE_EDITOR_DIALOG_BASE::OnCharHook( wxKeyEvent& aEvt )
{
if( aEvt.GetKeyCode() == 'S' && aEvt.ControlDown() && !aEvt.ShiftDown() && !aEvt.AltDown() )
{
wxCommandEvent evt;
OnSave( evt );
return;
}
DIALOG_SHIM::OnCharHook( aEvt );
}
RULE_EDITOR_DIALOG_BASE* RULE_EDITOR_DIALOG_BASE::GetDialog( wxWindow* aParent )
{
while( aParent )
{
if( RULE_EDITOR_DIALOG_BASE* currentDialog = dynamic_cast<RULE_EDITOR_DIALOG_BASE*>( aParent ) )
return currentDialog;
aParent = aParent->GetParent();
}
return nullptr;
}
void RULE_EDITOR_DIALOG_BASE::InitRuleTreeItems( const std::vector<RULE_TREE_NODE>& aNodes )
{
if( aNodes.empty() )
return;
m_defaultTreeItems = aNodes;
wxTreeItemId rootId = m_ruleTreeCtrl->AddRoot( aNodes[0].m_nodeName );
RULE_TREE_ITEM_DATA* itemData = new RULE_TREE_ITEM_DATA( aNodes[0].m_nodeId, nullptr, rootId );
m_ruleTreeCtrl->SetItemData( rootId, itemData );
std::vector<RULE_TREE_NODE> childNodes;
getRuleTreeChildNodes( aNodes, aNodes[0].m_nodeId, childNodes );
for( const auto& child : childNodes )
{
populateRuleTreeCtrl( aNodes, child, rootId );
}
m_treeHistoryData.clear();
saveRuleTreeState( m_ruleTreeCtrl->GetRootItem(), m_defaultTreeItems[0].m_nodeId );
m_ruleTreeCtrl->SelectItem( m_ruleTreeCtrl->GetFirstVisibleItem() );
m_ruleTreeCtrl->CollapseAll();
}
void RULE_EDITOR_DIALOG_BASE::SetContentPanel( wxPanel* aContentPanel )
{
if( m_contentPanel )
{
unregisterUnitBinders( m_contentPanel );
m_contentPanel->Destroy();
m_contentPanel = nullptr;
}
m_contentPanel = aContentPanel;
m_scrolledContentWin->SetBackgroundColour( m_contentPanel->GetBackgroundColour() );
resetUndoRedoForNewContent( m_contentPanel->GetChildren() );
Layout();
CallAfter(
[this]()
{
RefreshContentScrollArea();
} );
Refresh();
}
void RULE_EDITOR_DIALOG_BASE::RefreshContentScrollArea()
{
if( !m_contentPanel || !m_scrolledContentWin )
return;
m_contentPanel->Layout();
wxSize clientSize = m_scrolledContentWin->GetClientSize();
wxSize bestSize = m_contentPanel->GetBestSize();
int width = clientSize.GetWidth();
int height = std::max( clientSize.GetHeight(), bestSize.GetHeight() );
m_contentPanel->SetSize( width, height );
m_scrolledContentWin->SetVirtualSize( width, height );
m_scrolledContentWin->Refresh();
}
void RULE_EDITOR_DIALOG_BASE::AppendNewRuleTreeItem( const RULE_TREE_NODE& aNode,
wxTreeItemId aParent )
{
wxTreeItemId currentTreeItemId = appendRuleTreeItem( aNode, aParent );
auto it = m_treeHistoryData.find( aNode.m_nodeData->GetParentId() );
if( it != m_treeHistoryData.end() )
{
std::vector<int>& existingChildren = std::get<1>( it->second );
existingChildren.push_back( aNode.m_nodeId );
m_treeHistoryData[aNode.m_nodeId] = { aNode.m_nodeName, {}, currentTreeItemId };
}
m_ruleTreeCtrl->SelectItem( currentTreeItemId );
}
void RULE_EDITOR_DIALOG_BASE::UpdateRuleTreeItemText( wxTreeItemId aItemId, wxString aItemText )
{
m_ruleTreeCtrl->SetItemText( aItemId, aItemText );
}
void RULE_EDITOR_DIALOG_BASE::SetControlsEnabled( bool aEnable )
{
m_ruleTreeCtrl->Enable( aEnable );
m_filterSearch->Enable( aEnable );
updateRuleTreeActionButtonsState( m_selectedData );
if( m_cancelRuleButton )
{
if( !aEnable )
{
m_cancelRuleButton->SetLabelText( _( "Cancel" ) );
}
else if( m_modified )
{
m_cancelRuleButton->SetLabelText( _( "Discard" ) );
}
else
{
m_cancelRuleButton->SetLabelText( _( "Close" ) );
}
}
}
void RULE_EDITOR_DIALOG_BASE::SetModified()
{
m_modified = true;
if( m_saveRuleButton )
m_saveRuleButton->Enable( true );
if( m_cancelRuleButton && m_ruleTreeCtrl->IsEnabled() )
m_cancelRuleButton->SetLabelText( _( "Discard" ) );
if( m_selectedData )
{
wxTreeItemId itemId = m_selectedData->GetTreeItemId();
if( itemId.IsOk() )
{
wxString currentText = m_ruleTreeCtrl->GetItemText( itemId );
if( !currentText.EndsWith( wxS( " *" ) ) )
{
m_ruleTreeCtrl->SetItemText( itemId, currentText + wxS( " *" ) );
}
}
}
}
void RULE_EDITOR_DIALOG_BASE::ClearModified()
{
m_modified = false;
if( m_saveRuleButton )
m_saveRuleButton->Enable( false );
if( m_cancelRuleButton && m_ruleTreeCtrl->IsEnabled() )
m_cancelRuleButton->SetLabelText( _( "Close" ) );
}
void RULE_EDITOR_DIALOG_BASE::DeleteRuleTreeItem( wxTreeItemId aItemId, const int& aNodeId )
{
if( m_selectedData && m_selectedData->GetNodeId() == aNodeId )
m_selectedData = nullptr;
m_ruleTreeCtrl->Delete( aItemId );
m_treeHistoryData.erase( aNodeId );
}
void RULE_EDITOR_DIALOG_BASE::populateRuleTreeCtrl( const std::vector<RULE_TREE_NODE>& aNodes,
const RULE_TREE_NODE& aNode, wxTreeItemId aParent )
{
wxTreeItemId currentId = appendRuleTreeItem( aNode, aParent );
std::vector<RULE_TREE_NODE> childNodes;
getRuleTreeChildNodes( aNodes, aNode.m_nodeId, childNodes );
for( const auto& child : childNodes )
{
populateRuleTreeCtrl( aNodes, child, currentId );
}
m_ruleTreeCtrl->Expand( currentId );
}
void RULE_EDITOR_DIALOG_BASE::onRuleTreeItemRightClick( wxTreeEvent& aEvent )
{
wxTreeItemId item = aEvent.GetItem();
if( !item.IsOk() )
return;
m_ruleTreeCtrl->SelectItem( item );
RULE_TREE_ITEM_DATA* ruleTreeItemData = dynamic_cast<RULE_TREE_ITEM_DATA*>( m_ruleTreeCtrl->GetItemData( item ) );
if( !ruleTreeItemData )
return;
updateRuleTreeActionButtonsState( ruleTreeItemData );
wxMenu menu;
if( m_enableAddRule )
KIUI::AddMenuItem( &menu, ID_NEWRULE, _( "New Rule" ), KiBitmap( BITMAPS::small_plus ) );
if( m_enableAddRule )
KIUI::AddMenuItem( &menu, ID_COPYRULE, _( "Copy Rule" ), KiBitmap( BITMAPS::copy ) );
if( m_enableDeleteRule )
KIUI::AddMenuItem( &menu, ID_DELETERULE, _( "Delete Rule" ), KiBitmap( BITMAPS::small_trash ) );
if( isEnabled( ruleTreeItemData, RULE_EDITOR_TREE_CONTEXT_OPT::MOVE_UP ) && m_enableMoveUp )
KIUI::AddMenuItem( &menu, ID_MOVEUP, _( "Move Up" ), KiBitmap( BITMAPS::small_up ) );
if( isEnabled( ruleTreeItemData, RULE_EDITOR_TREE_CONTEXT_OPT::MOVE_DOWN ) && m_enableMoveDown )
KIUI::AddMenuItem( &menu, ID_MOVEDOWN, _( "Move Down" ), KiBitmap( BITMAPS::small_down ) );
menu.Bind( wxEVT_COMMAND_MENU_SELECTED,
[&]( wxCommandEvent& aCmd )
{
switch( aCmd.GetId() )
{
case ID_NEWRULE: onNewRuleOptionClick( aCmd ); break;
case ID_COPYRULE: onDuplicateRuleOptionClick( aCmd ); break;
case ID_DELETERULE: onDeleteRuleOptionClick( aCmd ); break;
case ID_MOVEUP: onMoveUpRuleOptionClick( aCmd ); break;
case ID_MOVEDOWN: onMoveDownRuleOptionClick( aCmd ); break;
default: aCmd.Skip();
}
} );
PopupMenu( &menu );
}
void RULE_EDITOR_DIALOG_BASE::onRuleTreeItemSelectionChanged( wxTreeEvent& aEvent )
{
wxTreeItemId selectedItem = aEvent.GetItem();
RULE_TREE_ITEM_DATA* selectedItemData =
dynamic_cast<RULE_TREE_ITEM_DATA*>( m_ruleTreeCtrl->GetItemData( selectedItem ) );
if( !selectedItemData )
return;
if( m_selectedData )
{
if( m_selectedData->GetNodeId() == selectedItemData->GetNodeId() )
{
m_selectedData = selectedItemData;
updateRuleTreeActionButtonsState( selectedItemData );
return;
}
m_previousId = m_selectedData->GetTreeItemId();
}
m_selectedData = selectedItemData;
RuleTreeItemSelectionChanged( m_selectedData );
updateRuleTreeActionButtonsState( selectedItemData );
}
void RULE_EDITOR_DIALOG_BASE::onRuleTreeItemActivated( wxTreeEvent& aEvent )
{
RULE_TREE_ITEM_DATA* itemData =
dynamic_cast<RULE_TREE_ITEM_DATA*>( m_ruleTreeCtrl->GetItemData( aEvent.GetItem() ) );
if( itemData &&
isEnabled( itemData, RULE_EDITOR_TREE_CONTEXT_OPT::DUPLICATE_RULE ) )
{
DuplicateRule( itemData );
}
else if( itemData &&
isEnabled( itemData, RULE_EDITOR_TREE_CONTEXT_OPT::ADD_RULE ) )
{
AddNewRule( itemData );
}
}
void RULE_EDITOR_DIALOG_BASE::onNewRuleOptionClick( wxCommandEvent& aEvent )
{
AddNewRule( m_selectedData );
}
void RULE_EDITOR_DIALOG_BASE::onDuplicateRuleOptionClick( wxCommandEvent& aEvent )
{
DuplicateRule( m_selectedData );
}
void RULE_EDITOR_DIALOG_BASE::onDeleteRuleOptionClick( wxCommandEvent& aEvent )
{
RemoveRule( m_selectedData->GetNodeId() );
}
void RULE_EDITOR_DIALOG_BASE::onMoveUpRuleOptionClick( wxCommandEvent& aEvent )
{
if( !m_enableMoveUp )
return;
wxTreeItemId selectedItem = m_ruleTreeCtrl->GetSelection();
if( !selectedItem.IsOk() )
return;
wxTreeItemId parent = m_ruleTreeCtrl->GetItemParent( selectedItem );
if( !parent.IsOk() )
return;
wxTreeItemIdValue cookie;
wxTreeItemId current = m_ruleTreeCtrl->GetFirstChild( parent, cookie );
wxTreeItemId previous;
wxTreeItemId beforePrevious;
while( current.IsOk() && current != selectedItem )
{
beforePrevious = previous;
previous = current;
current = m_ruleTreeCtrl->GetNextChild( parent, cookie );
}
if( !previous.IsOk() )
{
return;
}
wxTreeItemId insertPosition = beforePrevious.IsOk() ? beforePrevious : wxTreeItemId();
wxTreeItemId newItem = m_ruleTreeCtrl->InsertItem( parent, insertPosition,
m_ruleTreeCtrl->GetItemText( selectedItem ) );
moveRuleTreeItemChildrensTooOnDrag( selectedItem, newItem );
wxTreeItemData* itemData = m_ruleTreeCtrl->GetItemData( selectedItem );
if( itemData )
{
RULE_TREE_ITEM_DATA* ruleTreeItemData = dynamic_cast<RULE_TREE_ITEM_DATA*>( itemData );
ruleTreeItemData->SetTreeItemId( newItem );
m_ruleTreeCtrl->SetItemData( newItem, itemData );
m_ruleTreeCtrl->SetItemData( selectedItem, nullptr ); // Detach data from the old item
saveRuleTreeState( newItem, ruleTreeItemData->GetNodeId() );
m_ruleTreeCtrl->SelectItem( newItem );
m_ruleTreeCtrl->Expand( newItem );
m_ruleTreeCtrl->ExpandAllChildren( newItem );
}
m_ruleTreeCtrl->DeleteChildren( selectedItem );
m_ruleTreeCtrl->Delete( selectedItem );
}
void RULE_EDITOR_DIALOG_BASE::onMoveDownRuleOptionClick( wxCommandEvent& aEvent )
{
if( !m_enableMoveDown )
return;
wxTreeItemId selectedItem = m_ruleTreeCtrl->GetSelection();
if( !selectedItem.IsOk() )
return;
wxTreeItemId parent = m_ruleTreeCtrl->GetItemParent( selectedItem );
wxTreeItemIdValue cookie;
wxTreeItemId current = m_ruleTreeCtrl->GetFirstChild( parent, cookie );
while( current.IsOk() && current != selectedItem )
{
current = m_ruleTreeCtrl->GetNextChild( parent, cookie );
}
wxTreeItemId next = m_ruleTreeCtrl->GetNextChild( parent, cookie );
if( !next.IsOk() )
{
return;
}
wxString itemText = m_ruleTreeCtrl->GetItemText( selectedItem );
wxTreeItemData* itemData = m_ruleTreeCtrl->GetItemData( selectedItem );
wxTreeItemId newItem = m_ruleTreeCtrl->InsertItem( parent, next, itemText );
moveRuleTreeItemChildrensTooOnDrag( selectedItem, newItem );
if( itemData )
{
RULE_TREE_ITEM_DATA* ruleTreeItemData = dynamic_cast<RULE_TREE_ITEM_DATA*>( itemData );
ruleTreeItemData->SetTreeItemId( newItem );
m_ruleTreeCtrl->SetItemData( newItem, itemData );
m_ruleTreeCtrl->SetItemData( selectedItem, nullptr ); // Detach data
saveRuleTreeState( newItem, ruleTreeItemData->GetNodeId() );
m_ruleTreeCtrl->SelectItem( newItem );
m_ruleTreeCtrl->Expand( newItem );
}
m_ruleTreeCtrl->DeleteChildren( selectedItem );
m_ruleTreeCtrl->Delete( selectedItem );
}
void RULE_EDITOR_DIALOG_BASE::onRuleTreeItemLeftDown( wxMouseEvent& aEvent )
{
wxPoint pt = aEvent.GetPosition();
int flags;
wxTreeItemId item = m_ruleTreeCtrl->HitTest( pt, flags );
// Check if click is on the expand/collapse button
if( flags & wxTREE_HITTEST_ONITEMBUTTON )
{
// Prevent wxEVT_LEFT_DOWN from propagating further
aEvent.Skip();
return;
}
if( item.IsOk() )
{
m_draggedItem = item;
m_isDragging = false;
}
aEvent.Skip();
}
void RULE_EDITOR_DIALOG_BASE::onRuleTreeItemMouseMotion( wxMouseEvent& aEvent )
{
if( aEvent.Dragging() && m_draggedItem.IsOk() )
{
m_isDragging = true;
// Clear previous highlight
if( m_dropTargetItem.IsOk() )
{
m_ruleTreeCtrl->SetItemBackgroundColour( m_dropTargetItem, wxNullColour );
m_dropTargetItem = wxTreeItemId();
}
// Highlight item under cursor
int flags;
wxTreeItemId hitItem = m_ruleTreeCtrl->HitTest( aEvent.GetPosition(), flags );
if( hitItem.IsOk() && hitItem != m_draggedItem )
{
wxTreeItemId hitParent = m_ruleTreeCtrl->GetItemParent( hitItem );
wxTreeItemId dragParent = m_ruleTreeCtrl->GetItemParent( m_draggedItem );
if( hitParent == dragParent )
{
m_ruleTreeCtrl->SetItemBackgroundColour( hitItem,
wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT ) );
m_dropTargetItem = hitItem;
}
}
}
aEvent.Skip();
}
void RULE_EDITOR_DIALOG_BASE::onRuleTreeItemLeftUp( wxMouseEvent& aEvent )
{
if( m_draggedItem.IsOk() && m_isDragging )
{
m_isDragging = false;
if( m_dropTargetItem.IsOk() )
{
m_ruleTreeCtrl->SetItemBackgroundColour( m_dropTargetItem, wxNullColour );
m_dropTargetItem = wxTreeItemId();
}
wxPoint pos = aEvent.GetPosition();
int flags;
wxTreeItemId targetItem = m_ruleTreeCtrl->HitTest( pos, flags );
if( !targetItem.IsOk() )
return;
wxTreeItemId draggedParent = m_ruleTreeCtrl->GetItemParent( m_draggedItem );
wxTreeItemId targetParent = m_ruleTreeCtrl->GetItemParent( targetItem );
if( draggedParent != targetParent )
return;
if( draggedParent == targetParent )
{
wxTreeItemId newItem;
wxTreeItemIdValue cookie;
if( flags & wxTREE_HITTEST_ONITEMLABEL
&& targetItem == m_ruleTreeCtrl->GetFirstChild( targetParent, cookie ) )
{
// Prepend item as the first child of target parent
newItem = m_ruleTreeCtrl->PrependItem( targetParent,
m_ruleTreeCtrl->GetItemText( m_draggedItem ) );
}
else
{
wxTreeItemId current = m_ruleTreeCtrl->GetFirstChild( targetParent, cookie );
wxTreeItemId previous;
// Traverse through siblings to find the position of the target item
while( current.IsOk() && current != targetItem )
{
previous = current;
current = m_ruleTreeCtrl->GetNextChild( targetParent, cookie );
}
if( !previous.IsOk() )
{
// If no previous item, insert as the first child
newItem = m_ruleTreeCtrl->PrependItem( targetParent,
m_ruleTreeCtrl->GetItemText( m_draggedItem ) );
}
else
{
wxTreeItemId nextSibling = m_ruleTreeCtrl->GetNextChild( targetParent, cookie );
if( nextSibling.IsOk() )
{
// If there is a next sibling, insert after the previous item
newItem = m_ruleTreeCtrl->InsertItem( targetParent, previous,
m_ruleTreeCtrl->GetItemText( m_draggedItem ) );
}
else
{
// If no next sibling, append the item as the last child
newItem = m_ruleTreeCtrl->AppendItem( targetParent,
m_ruleTreeCtrl->GetItemText( m_draggedItem ) );
}
}
}
moveRuleTreeItemChildrensTooOnDrag( m_draggedItem, newItem );
wxTreeItemData* itemData = m_ruleTreeCtrl->GetItemData( m_draggedItem );
if( itemData )
{
RULE_TREE_ITEM_DATA* ruleTreeItemData =
dynamic_cast<RULE_TREE_ITEM_DATA*>( itemData );
ruleTreeItemData->SetTreeItemId( newItem );
m_ruleTreeCtrl->SetItemData( newItem, ruleTreeItemData );
m_ruleTreeCtrl->SetItemData( m_draggedItem, nullptr );
saveRuleTreeState( newItem, ruleTreeItemData->GetNodeId() );
m_ruleTreeCtrl->SelectItem( newItem );
m_ruleTreeCtrl->Expand( newItem );
}
m_ruleTreeCtrl->DeleteChildren( m_draggedItem );
m_ruleTreeCtrl->Delete( m_draggedItem );
}
m_draggedItem = nullptr;
}
aEvent.Skip();
}
void RULE_EDITOR_DIALOG_BASE::onFilterSearch( wxCommandEvent& aEvent )
{
const auto searchStr = aEvent.GetString().Lower();
m_ruleTreeCtrl->DeleteAllItems();
restoreRuleTree( nullptr, m_defaultTreeItems[0].m_nodeId );
wxTreeItemId root = m_ruleTreeCtrl->GetRootItem();
if( root.IsOk() )
{
filterRuleTree( root, searchStr );
}
}
bool RULE_EDITOR_DIALOG_BASE::filterRuleTree( const wxTreeItemId& aItem, const wxString& aFilter )
{
bool matches = m_ruleTreeCtrl->GetItemText( aItem ).Lower().Contains( aFilter );
wxTreeItemIdValue cookie;
wxTreeItemId child = m_ruleTreeCtrl->GetFirstChild( aItem, cookie );
bool hasVisibleChildren = false;
std::vector<wxTreeItemId> itemsToDelete;
while( child.IsOk() )
{
if( filterRuleTree( child, aFilter ) )
{
hasVisibleChildren = true;
}
else
{
itemsToDelete.push_back( child );
}
child = m_ruleTreeCtrl->GetNextChild( aItem, cookie );
}
for( const auto& id : itemsToDelete )
{
m_ruleTreeCtrl->Delete( id );
}
return matches || hasVisibleChildren;
}
void RULE_EDITOR_DIALOG_BASE::saveRuleTreeState( const wxTreeItemId& aItem, const int& aNodeId )
{
wxString itemText = m_ruleTreeCtrl->GetItemText( aItem );
std::vector<int> children;
wxTreeItemIdValue cookie;
wxTreeItemId child = m_ruleTreeCtrl->GetFirstChild( aItem, cookie );
while( child.IsOk() )
{
RULE_TREE_ITEM_DATA* childItemData =
dynamic_cast<RULE_TREE_ITEM_DATA*>( m_ruleTreeCtrl->GetItemData( child ) );
int childId = childItemData->GetNodeId();
children.push_back( childId );
saveRuleTreeState( child, childId );
child = m_ruleTreeCtrl->GetNextChild( aItem, cookie );
}
m_treeHistoryData[aNodeId] = { itemText, children, aItem };
}
void RULE_EDITOR_DIALOG_BASE::restoreRuleTree( const wxTreeItemId& aParent, const int& aNodeId )
{
auto it = m_treeHistoryData.find( aNodeId );
if( it == m_treeHistoryData.end() )
return;
const auto& [itemText, children, itemId] = it->second;
wxTreeItemId newItem;
if( aParent )
newItem = m_ruleTreeCtrl->AppendItem( aParent, itemText );
else
newItem = m_ruleTreeCtrl->AddRoot( itemText );
if( newItem )
{
wxTreeItemId& treeItemId = std::get<2>( it->second );
treeItemId = newItem;
}
RULE_TREE_ITEM_DATA* itemData = new RULE_TREE_ITEM_DATA( aNodeId, aParent, newItem );
m_ruleTreeCtrl->SetItemData( newItem, itemData );
for( const auto& childId : children )
{
restoreRuleTree( newItem, childId );
}
// Don't expand the root item because it is hidden
if( aParent )
m_ruleTreeCtrl->Expand( newItem );
}
wxTreeItemId RULE_EDITOR_DIALOG_BASE::appendRuleTreeItem( const RULE_TREE_NODE& aNode, wxTreeItemId aParent )
{
wxString displayName = aNode.m_nodeName;
if( aNode.m_nodeData && aNode.m_nodeData->IsNew() )
displayName += wxS( " *" );
wxTreeItemId currentTreeItemId = m_ruleTreeCtrl->AppendItem( aParent, displayName );
RULE_TREE_ITEM_DATA* itemData = new RULE_TREE_ITEM_DATA( aNode.m_nodeId, aParent, currentTreeItemId );
m_ruleTreeCtrl->SetItemData( currentTreeItemId, itemData );
m_ruleTreeCtrl->Expand( currentTreeItemId );
return currentTreeItemId;
}
void RULE_EDITOR_DIALOG_BASE::getRuleTreeChildNodes( const std::vector<RULE_TREE_NODE>& aNodes,
int aParentId,
std::vector<RULE_TREE_NODE>& aResult )
{
std::vector<RULE_TREE_NODE> filteredNodes;
std::copy_if( aNodes.begin(), aNodes.end(), std::back_inserter( filteredNodes ),
[&aParentId]( const RULE_TREE_NODE& node )
{
return node.m_nodeData->GetParentId() == aParentId;
} );
if( filteredNodes.size() > 0 )
{
aResult.insert( aResult.end(), filteredNodes.begin(), filteredNodes.end() );
}
}
void RULE_EDITOR_DIALOG_BASE::moveRuleTreeItemChildrensTooOnDrag( wxTreeItemId aSrc,
wxTreeItemId aDest )
{
wxTreeItemIdValue cookie;
wxTreeItemId child = m_ruleTreeCtrl->GetFirstChild( aSrc, cookie );
while( child.IsOk() )
{
wxTreeItemId newChild = m_ruleTreeCtrl->AppendItem( aDest, m_ruleTreeCtrl->GetItemText( child ) );
RULE_TREE_ITEM_DATA* ruleTreeItemData =
dynamic_cast<RULE_TREE_ITEM_DATA*>( m_ruleTreeCtrl->GetItemData( child ) );
ruleTreeItemData->SetParentTreeItemId( aDest );
ruleTreeItemData->SetTreeItemId( newChild );
m_ruleTreeCtrl->SetItemData( newChild, ruleTreeItemData );
m_ruleTreeCtrl->SetItemData( child, nullptr );
moveRuleTreeItemChildrensTooOnDrag( child, newChild );
child = m_ruleTreeCtrl->GetNextChild( aSrc, cookie );
}
}
void RULE_EDITOR_DIALOG_BASE::updateRuleTreeItemMoveOptionState()
{
wxTreeItemId selectedItem = m_ruleTreeCtrl->GetSelection();
m_enableMoveUp = false;
m_enableMoveDown = false;
if( !selectedItem.IsOk() )
return;
if( selectedItem == m_ruleTreeCtrl->GetRootItem() )
return;
RULE_TREE_ITEM_DATA* itemData =
dynamic_cast<RULE_TREE_ITEM_DATA*>( m_ruleTreeCtrl->GetItemData( selectedItem ) );
if( !itemData ||
!isEnabled( itemData, RULE_EDITOR_TREE_CONTEXT_OPT::MOVE_UP ) ||
!isEnabled( itemData, RULE_EDITOR_TREE_CONTEXT_OPT::MOVE_DOWN ) )
{
return;
}
wxTreeItemId parent = m_ruleTreeCtrl->GetItemParent( selectedItem );
wxTreeItemIdValue cookie;
wxTreeItemId firstChild = m_ruleTreeCtrl->GetFirstChild( parent, cookie );
wxTreeItemId lastChild = firstChild;
while( lastChild.IsOk() )
{
wxTreeItemId next = m_ruleTreeCtrl->GetNextChild( parent, cookie );
if( !next.IsOk() )
break;
lastChild = next;
}
m_enableMoveUp = ( selectedItem != firstChild );
m_enableMoveDown = ( selectedItem != lastChild );
}
void RULE_EDITOR_DIALOG_BASE::updateRuleTreeActionButtonsState( RULE_TREE_ITEM_DATA* aData )
{
wxTreeItemId selectedItem = m_ruleTreeCtrl->GetSelection();
if( !selectedItem.IsOk() || !m_ruleTreeCtrl->IsEnabled() )
{
m_addRuleButton->Enable( false );
m_copyRuleButton->Enable( false );
m_moveTreeItemUpButton->Enable( false );
m_moveTreeItemDownButton->Enable( false );
m_deleteRuleButton->Enable( false );
return;
}
m_enableAddRule = false;
m_enableDuplicateRule = false;
m_enableDeleteRule = false;
if( isEnabled( aData, RULE_EDITOR_TREE_CONTEXT_OPT::ADD_RULE ) )
m_enableAddRule = true;
if( isEnabled( aData, RULE_EDITOR_TREE_CONTEXT_OPT::DUPLICATE_RULE ) )
m_enableDuplicateRule = true;
if( isEnabled( aData, RULE_EDITOR_TREE_CONTEXT_OPT::DELETE_RULE ) )
m_enableDeleteRule = true;
updateRuleTreeItemMoveOptionState();
m_addRuleButton->Enable( m_enableAddRule );
m_copyRuleButton->Enable( m_enableDuplicateRule );
m_moveTreeItemUpButton->Enable( m_enableMoveUp );
m_moveTreeItemDownButton->Enable( m_enableMoveDown );
m_deleteRuleButton->Enable( m_enableDeleteRule );
}
void RULE_EDITOR_DIALOG_BASE::onResize( wxSizeEvent& event )
{
Layout();
event.Skip();
}
void RULE_EDITOR_DIALOG_BASE::onClose( wxCloseEvent& aEvt )
{
wxPostEvent( this, wxCommandEvent( wxEVT_COMMAND_BUTTON_CLICKED, wxID_CANCEL ) );
}