From 9cba910d53ce58c7aac28e16b89d3122ed5443f2 Mon Sep 17 00:00:00 2001 From: Seth Hillbrand Date: Fri, 30 Aug 2024 16:03:06 -0700 Subject: [PATCH] ADDED: Library Table Editor Edits all symbols in a library at once. Supports copy/paste, multiple field assignment and selective coloration based on existing data Fixes https://gitlab.com/kicad/code/kicad/-/issues/11506 --- common/tool/actions.cpp | 6 + common/trace_helpers.cpp | 1 + common/widgets/grid_striped_renderer.cpp | 47 + eeschema/CMakeLists.txt | 3 + eeschema/dialogs/dialog_lib_fields.cpp | 1157 +++++++++++++++++ eeschema/dialogs/dialog_lib_fields.h | 86 ++ eeschema/dialogs/dialog_lib_fields_base.cpp | 203 +++ eeschema/dialogs/dialog_lib_fields_base.fbp | 898 +++++++++++++ eeschema/dialogs/dialog_lib_fields_base.h | 92 ++ eeschema/lib_fields_data_model.cpp | 1112 ++++++++++++++++ eeschema/lib_fields_data_model.h | 332 +++++ .../symbol_editor/symbol_editor_settings.cpp | 7 + .../symbol_editor/symbol_editor_settings.h | 9 + eeschema/tools/symbol_editor_control.cpp | 73 +- eeschema/tools/symbol_editor_control.h | 2 + include/tool/actions.h | 1 + include/trace_helpers.h | 7 + include/widgets/grid_striped_renderer.h | 132 ++ 18 files changed, 4153 insertions(+), 15 deletions(-) create mode 100644 common/widgets/grid_striped_renderer.cpp create mode 100644 eeschema/dialogs/dialog_lib_fields.cpp create mode 100644 eeschema/dialogs/dialog_lib_fields.h create mode 100644 eeschema/dialogs/dialog_lib_fields_base.cpp create mode 100644 eeschema/dialogs/dialog_lib_fields_base.fbp create mode 100644 eeschema/dialogs/dialog_lib_fields_base.h create mode 100644 eeschema/lib_fields_data_model.cpp create mode 100644 eeschema/lib_fields_data_model.h create mode 100644 include/widgets/grid_striped_renderer.h diff --git a/common/tool/actions.cpp b/common/tool/actions.cpp index d81bd835b2..278233b016 100644 --- a/common/tool/actions.cpp +++ b/common/tool/actions.cpp @@ -953,6 +953,12 @@ TOOL_ACTION ACTIONS::unpinLibrary( TOOL_ACTION_ARGS() .FriendlyName( _( "Unpin Library" ) ) .Tooltip( _( "No longer keep the library at the top of the list" ) ) ); +TOOL_ACTION ACTIONS::showLibraryTable( TOOL_ACTION_ARGS() + .Name( "common.Control.showLibraryTable" ) + .Scope( AS_GLOBAL ) + .FriendlyName( _( "Library Table" ) ) + .Icon( BITMAPS::table ) ); + TOOL_ACTION ACTIONS::showLibraryTree( TOOL_ACTION_ARGS() .Name( "common.Control.showLibraryTree" ) .Scope( AS_GLOBAL ) diff --git a/common/trace_helpers.cpp b/common/trace_helpers.cpp index b9f76653ee..62f01cb668 100644 --- a/common/trace_helpers.cpp +++ b/common/trace_helpers.cpp @@ -59,6 +59,7 @@ const wxChar* const traceUiProfile = wxT( "KICAD_UI_PROFILE" ); const wxChar* const traceGit = wxT( "KICAD_GIT" ); const wxChar* const traceEagleIo = wxT( "KICAD_EAGLE_IO" ); const wxChar* const traceDesignBlocks = wxT( "KICAD_DESIGN_BLOCK" ); +const wxChar* const traceLibFieldTable = wxT( "KICAD_LIB_FIELD_TABLE" ); wxString dump( const wxArrayString& aArray ) diff --git a/common/widgets/grid_striped_renderer.cpp b/common/widgets/grid_striped_renderer.cpp new file mode 100644 index 0000000000..9a11752d94 --- /dev/null +++ b/common/widgets/grid_striped_renderer.cpp @@ -0,0 +1,47 @@ + +#include + +void STRIPED_CELL_RENDERER::Draw(wxGrid& grid, wxGridCellAttr& attr, wxDC& dc, + const wxRect& rect, int row, int col, bool isSelected) +{ + // First draw the striped background for empty cells + wxString cellValue = grid.GetCellValue(row, col); + if (cellValue.IsEmpty()) + { + DrawStripedBackground(dc, rect, isSelected); + } + + // Then draw the text content using the parent class + wxGridCellStringRenderer::Draw(grid, attr, dc, rect, row, col, isSelected); +} + +void STRIPED_CELL_RENDERER::drawStripedBackground(wxDC& dc, const wxRect& rect, bool isSelected) const +{ + if (isSelected) + { + // For selected cells, use the selection color + dc.SetBrush(wxBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT))); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(rect); + return; + } + + // Draw horizontal stripes + const int stripeHeight = 3; + wxColour color1 = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + wxColour color2(240, 240, 240); // Light gray + + dc.SetPen(*wxTRANSPARENT_PEN); + + bool useColor1 = true; + for (int y = rect.GetTop(); y < rect.GetBottom(); y += stripeHeight) + { + wxColour currentColor = useColor1 ? color1 : color2; + dc.SetBrush(wxBrush(currentColor)); + + int stripeBottom = wxMin(y + stripeHeight, rect.GetBottom()); + dc.DrawRectangle(rect.GetLeft(), y, rect.GetWidth(), stripeBottom - y); + + useColor1 = !useColor1; + } +} \ No newline at end of file diff --git a/eeschema/CMakeLists.txt b/eeschema/CMakeLists.txt index 66ae33ba4f..faa0e2a809 100644 --- a/eeschema/CMakeLists.txt +++ b/eeschema/CMakeLists.txt @@ -122,6 +122,8 @@ set( EESCHEMA_DLGS dialogs/dialog_label_properties_base.cpp dialogs/dialog_lib_edit_pin_table.cpp dialogs/dialog_lib_edit_pin_table_base.cpp + dialogs/dialog_lib_fields_base.cpp + dialogs/dialog_lib_fields.cpp dialogs/dialog_lib_new_symbol.cpp dialogs/dialog_lib_new_symbol_base.cpp dialogs/dialog_lib_symbol_properties.cpp @@ -365,6 +367,7 @@ set( EESCHEMA_SRCS generate_alias_info.cpp gfx_import_utils.cpp junction_helpers.cpp + lib_fields_data_model.cpp lib_symbol.cpp libarch.cpp menubar.cpp diff --git a/eeschema/dialogs/dialog_lib_fields.cpp b/eeschema/dialogs/dialog_lib_fields.cpp new file mode 100644 index 0000000000..54e6eeb679 --- /dev/null +++ b/eeschema/dialogs/dialog_lib_fields.cpp @@ -0,0 +1,1157 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2025 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 . + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +using DIALOG_NEW_SYMBOL = DIALOG_LIB_NEW_SYMBOL; + +#ifdef __WXMAC__ +#define COLUMN_MARGIN 3 +#else +#define COLUMN_MARGIN 15 +#endif + + +enum +{ + MYID_SELECT_FOOTPRINT = GRIDTRICKS_FIRST_CLIENT_ID, + MYID_SHOW_DATASHEET, + MYID_REVERT_ROW, + MYID_CLEAR_CELL, + MYID_CREATE_DERIVED_SYMBOL +}; +class LIB_FIELDS_EDITOR_GRID_TRICKS : public GRID_TRICKS +{ +public: + LIB_FIELDS_EDITOR_GRID_TRICKS( DIALOG_SHIM* aParent, + WX_GRID* aGrid, + wxDataViewListCtrl* aFieldsCtrl, + LIB_FIELDS_EDITOR_GRID_DATA_MODEL* aDataModel ) : + GRID_TRICKS( aGrid ), + m_dlg( aParent ), + m_fieldsCtrl( aFieldsCtrl ), + m_dataModel( aDataModel ) + {} + +protected: + void showPopupMenu( wxMenu& menu, wxGridEvent& aEvent ) override + { + wxMenuItem* revertMenu = menu.Append( MYID_REVERT_ROW, _( "Revert symbol" ), _( "Revert the symbol to its last saved state" ), wxITEM_NORMAL ); + wxMenuItem* clearMenu = menu.Append( MYID_CLEAR_CELL, _( "Clear cell" ), _( "Clear the cell value" ), wxITEM_NORMAL ); + menu.AppendSeparator(); + wxMenuItem* createDerivedSymbolMenu = menu.Append( MYID_CREATE_DERIVED_SYMBOL, _( "Create Derived Symbol" ), _( "Create a new symbol derived from the selected one" ), wxITEM_NORMAL ); + + // Get global mouse position and convert to grid client coords + wxPoint mousePos = wxGetMousePosition(); + wxPoint gridPt = m_grid->ScreenToClient( mousePos ); + + // Offset by grid header (column label) height so header area doesn't map to a row. + int headerHeight = m_grid->GetColLabelSize(); + gridPt.y -= headerHeight; + if ( gridPt.y < 0 ) + gridPt.y = 0; + + int row = m_grid->YToRow( gridPt.y ); + int col = m_grid->XToCol( gridPt.x ); + m_grid->SetGridCursor( row, col ); + + revertMenu->Enable( m_dataModel->IsCellEdited( row, col ) ); + clearMenu->Enable( !m_dataModel->IsCellClear( row, col ) ); + createDerivedSymbolMenu->Enable( m_dataModel->IsRowSingleSymbol( row ) ); + + if( m_dataModel->GetColFieldName( col ) == GetCanonicalFieldName( FIELD_T::FOOTPRINT ) ) + { + menu.Append( MYID_SELECT_FOOTPRINT, _( "Select Footprint..." ), + _( "Browse for footprint" ) ); + menu.AppendSeparator(); + } + else if( m_dataModel->GetColFieldName( col ) == GetCanonicalFieldName( FIELD_T::DATASHEET ) ) + { + menu.Append( MYID_SHOW_DATASHEET, _( "Show Datasheet" ), + _( "Show datasheet in browser" ) ); + menu.AppendSeparator(); + } + + GRID_TRICKS::showPopupMenu( menu, aEvent ); + } + + void doPopupSelection( wxCommandEvent& event ) override + { + int row = m_grid->GetGridCursorRow(); + int col = m_grid->GetGridCursorCol(); + + if( event.GetId() == MYID_REVERT_ROW ) + { + if( m_grid->CommitPendingChanges( false ) ) + m_dataModel->RevertRow( row ); + + if( m_dataModel->IsEdited() ) + m_dlg->OnModify(); + else + m_dlg->ClearModify(); + + m_grid->ForceRefresh(); + } + else if( event.GetId() == MYID_CLEAR_CELL ) + { + if( m_grid->CommitPendingChanges( false ) ) + m_dataModel->ClearCell( row, col ); + + if( m_dataModel->IsEdited() ) + m_dlg->OnModify(); + else + m_dlg->ClearModify(); + + m_grid->ForceRefresh(); + } + else if( event.GetId() == MYID_CREATE_DERIVED_SYMBOL ) + { + const LIB_SYMBOL* parentSymbol = m_dataModel->GetSymbolForRow( row ); + + wxArrayString symbolNames; + m_dataModel->GetSymbolNames( symbolNames ); + + auto validator = [&]( wxString newName ) -> bool + { + return symbolNames.Index( newName ) == wxNOT_FOUND; + }; + + EDA_DRAW_FRAME* frame = dynamic_cast( m_dlg->GetParent() ); + DIALOG_NEW_SYMBOL dlg( frame, symbolNames, parentSymbol->GetName(), validator ); + + if( dlg.ShowModal() != wxID_OK ) + return; + + wxString derivedName = dlg.GetName(); + + m_dataModel->CreateDerivedSymbolImmediate( row, col, derivedName ); + + if( m_dataModel->IsEdited() ) + m_dlg->OnModify(); + + m_grid->ForceRefresh(); + } + else if( event.GetId() == MYID_SELECT_FOOTPRINT ) + { + // pick a footprint using the footprint picker. + wxString fpid = m_grid->GetCellValue( row, col ); + + if( KIWAY_PLAYER* frame = m_dlg->Kiway().Player( FRAME_FOOTPRINT_CHOOSER, true, + m_dlg ) ) + { + if( frame->ShowModal( &fpid, m_dlg ) ) + m_grid->SetCellValue( row, col, fpid ); + + frame->Destroy(); + } + } + else if (event.GetId() == MYID_SHOW_DATASHEET ) + { + wxString datasheet_uri = m_grid->GetCellValue( row, col ); + GetAssociatedDocument( m_dlg, datasheet_uri, &m_dlg->Prj(), + PROJECT_SCH::SchSearchS( &m_dlg->Prj() ) ); + } + else + { + // We have grid tricks events to show/hide the columns from the popup menu + // and we need to make sure the data model is updated to match the grid, + // so do it through our code instead + if( event.GetId() >= GRIDTRICKS_FIRST_SHOWHIDE ) + { + // Pop-up column order is the order of the shown fields, not the + // fieldsCtrl order + col = event.GetId() - GRIDTRICKS_FIRST_SHOWHIDE; + + bool show = !m_dataModel->GetShowColumn( col ); + + // Convert data model column to by iterating over m_fieldsCtrl rows + // and finding the matching field name + wxString fieldName = m_dataModel->GetColFieldName( col ); + + for( row = 0; row < m_fieldsCtrl->GetItemCount(); row++ ) + { + if( m_fieldsCtrl->GetTextValue( row, FIELD_NAME_COLUMN ) == fieldName ) + { + if( m_grid->CommitPendingChanges( false ) ) + m_fieldsCtrl->SetToggleValue( show, row, SHOW_FIELD_COLUMN ); + + break; + } + } + } + else + { + GRID_TRICKS::doPopupSelection( event ); + } + } + } + + DIALOG_SHIM* m_dlg; + wxDataViewListCtrl* m_fieldsCtrl; + LIB_FIELDS_EDITOR_GRID_DATA_MODEL* m_dataModel; +}; + + +DIALOG_LIB_FIELDS::DIALOG_LIB_FIELDS( SYMBOL_EDIT_FRAME* parent, wxString libId ) : + DIALOG_LIB_FIELDS_BASE( parent, wxID_ANY, wxString::Format( _( "Symbol Library Fields: %s" ), libId ) ), + m_libId( libId ), + m_parent( parent ) +{ + // Get all symbols from the library + loadSymbols(); + + m_bRefresh->SetBitmap( KiBitmapBundle( BITMAPS::small_refresh ) ); + + m_addFieldButton->SetBitmap( KiBitmapBundle( BITMAPS::small_plus ) ); + m_removeFieldButton->SetBitmap( KiBitmapBundle( BITMAPS::small_trash ) ); + m_renameFieldButton->SetBitmap( KiBitmapBundle( BITMAPS::small_edit ) ); + + m_removeFieldButton->Enable( false ); + m_renameFieldButton->Enable( false ); + + m_fieldsCtrl->AppendTextColumn( _( "Field" ), wxDATAVIEW_CELL_INERT, 0, wxALIGN_LEFT, wxCOL_HIDDEN ); + m_fieldsCtrl->AppendTextColumn( _( "Label" ), wxDATAVIEW_CELL_EDITABLE, 0, wxALIGN_LEFT, 0 ); + m_fieldsCtrl->AppendToggleColumn( _( "Show" ), wxDATAVIEW_CELL_ACTIVATABLE, 0, + wxALIGN_CENTER, 0 ); + m_fieldsCtrl->AppendToggleColumn( _( "Group By" ), wxDATAVIEW_CELL_ACTIVATABLE, 0, + wxALIGN_CENTER, 0 ); + + // GTK asserts if the number of columns doesn't match the data, but we still don't want + // to display the canonical names. So we'll insert a column for them, but keep it 0 width. + m_fieldsCtrl->AppendTextColumn( _( "Name" ), wxDATAVIEW_CELL_INERT, 0, wxALIGN_LEFT, 0 ); + + // SetWidth( wxCOL_WIDTH_AUTOSIZE ) fails here on GTK, so we calculate the title sizes and + // set the column widths ourselves. + wxDataViewColumn* column = m_fieldsCtrl->GetColumn( SHOW_FIELD_COLUMN ); + m_showColWidth = KIUI::GetTextSize( column->GetTitle(), m_fieldsCtrl ).x + COLUMN_MARGIN; + column->SetMinWidth( m_showColWidth ); + + column = m_fieldsCtrl->GetColumn( GROUP_BY_COLUMN ); + m_groupByColWidth = KIUI::GetTextSize( column->GetTitle(), m_fieldsCtrl ).x + COLUMN_MARGIN; + column->SetMinWidth( m_groupByColWidth ); + + // The fact that we're a list should keep the control from reserving space for the + // expander buttons... but it doesn't. Fix by forcing the indent to 0. + m_fieldsCtrl->SetIndent( 0 ); + + m_filter->SetDescriptiveText( _( "Filter" ) ); + + wxGridCellAttr* attr = new wxGridCellAttr; + attr->SetEditor( new GRID_CELL_URL_EDITOR( this, PROJECT_SCH::SchSearchS( &Prj() ) ) ); + m_dataModel = new LIB_FIELDS_EDITOR_GRID_DATA_MODEL( m_symbolsList ); + + // Now that the fields are loaded we can set the initial location of the splitter + // based on the list width. Again, SetWidth( wxCOL_WIDTH_AUTOSIZE ) fails us on GTK. + m_fieldNameColWidth = 0; + m_labelColWidth = 0; + + int colWidth = 0; + + for( int row = 0; row < m_fieldsCtrl->GetItemCount(); ++row ) + { + const wxString& displayName = m_fieldsCtrl->GetTextValue( row, DISPLAY_NAME_COLUMN ); + colWidth = std::max( colWidth, KIUI::GetTextSize( displayName, m_fieldsCtrl ).x ); + + const wxString& label = m_fieldsCtrl->GetTextValue( row, LABEL_COLUMN ); + colWidth = std::max( colWidth, KIUI::GetTextSize( label, m_fieldsCtrl ).x ); + } + + m_fieldNameColWidth = colWidth + 20; + m_labelColWidth = colWidth + 20; + + int fieldsMinWidth = m_fieldNameColWidth + m_labelColWidth + m_groupByColWidth + m_showColWidth; + + m_fieldsCtrl->GetColumn( DISPLAY_NAME_COLUMN )->SetWidth( m_fieldNameColWidth ); + m_fieldsCtrl->GetColumn( LABEL_COLUMN )->SetWidth( m_labelColWidth ); + + // This is used for data only. Don't show it to the user. + m_fieldsCtrl->GetColumn( FIELD_NAME_COLUMN )->SetHidden( true ); + + m_splitterMainWindow->SetMinimumPaneSize( fieldsMinWidth ); + m_splitterMainWindow->SetSashPosition( fieldsMinWidth + 40 ); + + m_grid->UseNativeColHeader( true ); + m_grid->SetTable( m_dataModel, true ); + + // must be done after SetTable(), which appears to re-set it + m_grid->SetSelectionMode( wxGrid::wxGridSelectCells ); + + // add Cut, Copy, and Paste to wxGrid + m_grid->PushEventHandler( new LIB_FIELDS_EDITOR_GRID_TRICKS( this, m_grid, m_fieldsCtrl, + m_dataModel ) ); + + // give a bit more room for comboboxes + m_grid->SetDefaultRowSize( m_grid->GetDefaultRowSize() + 4 ); + + + SetInitialFocus( m_grid ); + m_grid->ClearSelection(); + + SetupStandardButtons(); + + finishDialogSettings(); + + SetSize( wxSize( horizPixelsFromDU( 600 ), vertPixelsFromDU( 300 ) ) ); + + Center(); + + // Connect Events + m_grid->Bind( wxEVT_GRID_COL_SORT, &DIALOG_LIB_FIELDS::OnColSort, this ); + m_grid->Bind( wxEVT_GRID_COL_MOVE, &DIALOG_LIB_FIELDS::OnColMove, this ); + m_grid->Bind( wxEVT_GRID_CELL_LEFT_CLICK, &DIALOG_LIB_FIELDS::OnTableCellClick, this ); + m_fieldsCtrl->Bind( wxEVT_DATAVIEW_ITEM_VALUE_CHANGED, &DIALOG_LIB_FIELDS::OnColLabelChange, this ); + + OnInit(); +} + + +DIALOG_LIB_FIELDS::~DIALOG_LIB_FIELDS() +{ + // Disconnect Events + m_grid->Unbind( wxEVT_GRID_COL_SORT, &DIALOG_LIB_FIELDS::OnColSort, this ); + m_grid->Unbind( wxEVT_GRID_COL_MOVE, &DIALOG_LIB_FIELDS::OnColMove, this ); + m_grid->Unbind( wxEVT_GRID_CELL_LEFT_CLICK, &DIALOG_LIB_FIELDS::OnTableCellClick, this ); + m_fieldsCtrl->Unbind( wxEVT_DATAVIEW_ITEM_VALUE_CHANGED, &DIALOG_LIB_FIELDS::OnColLabelChange, this ); + + // Delete the GRID_TRICKS. + m_grid->PopEventHandler( true ); +} + +void DIALOG_LIB_FIELDS::OnInit() +{ + // Update the field list and refresh the grid + UpdateFieldList(); + + int colWidth = 0; + + for( int row = 0; row < m_fieldsCtrl->GetItemCount(); ++row ) + { + const wxString& displayName = m_fieldsCtrl->GetTextValue( row, DISPLAY_NAME_COLUMN ); + colWidth = std::max( colWidth, KIUI::GetTextSize( displayName, m_fieldsCtrl ).x ); + + const wxString& label = m_fieldsCtrl->GetTextValue( row, LABEL_COLUMN ); + colWidth = std::max( colWidth, KIUI::GetTextSize( label, m_fieldsCtrl ).x ); + } + + m_fieldNameColWidth = colWidth + 20; + m_labelColWidth = colWidth + 20; + + int fieldsMinWidth = m_fieldNameColWidth + m_labelColWidth + m_groupByColWidth + m_showColWidth; + + m_fieldsCtrl->GetColumn( DISPLAY_NAME_COLUMN )->SetWidth( m_fieldNameColWidth ); + m_fieldsCtrl->GetColumn( LABEL_COLUMN )->SetWidth( m_labelColWidth ); + + m_splitterMainWindow->SetMinimumPaneSize( fieldsMinWidth ); + m_splitterMainWindow->SetSashPosition( fieldsMinWidth + 40 ); + + SetupAllColumnProperties(); + RegroupSymbols(); +} + + +void DIALOG_LIB_FIELDS::loadSymbols() +{ + // Clear any existing data + m_symbolsList.clear(); + + try + { + // Get all symbol names from the library manager + wxArrayString symbolNames; + m_parent->GetLibManager().GetSymbolNames( m_libId, symbolNames ); + + if( symbolNames.IsEmpty() ) + { + wxMessageBox( wxString::Format( _( "No symbols found in library %s." ), m_libId ) ); + return; + } + + // Load each symbol from the library manager and add it to our list + for( const wxString& symbolName : symbolNames ) + { + LIB_SYMBOL* symbol = nullptr; + + try + { + symbol = m_parent->GetLibManager().GetSymbol( symbolName, m_libId ); + + if( symbol ) + m_symbolsList.push_back( symbol ); + } + catch( const IO_ERROR& ioe ) + { + // Log the error and continue + wxLogWarning( wxString::Format( _( "Error loading symbol %s: %s" ), symbolName, ioe.What() ) ); + } + } + } + catch( const IO_ERROR& ioe ) + { + wxString msg = wxString::Format( _( "Error accessing library %s.\n\n%s" ), m_libId, ioe.What() ); + DisplayErrorMessage( this, msg ); + return; + } + + if( m_symbolsList.empty() ) + { + wxMessageBox( _( "No symbols could be loaded from the library." ) ); + return; + } +} + +void DIALOG_LIB_FIELDS::OnClose(wxCloseEvent& aEvent) +{ + // This is a cancel, so commit quietly as we're going to throw the results away anyway. + m_grid->CommitPendingChanges( true ); + + if( m_dataModel->IsEdited() ) + { + if( !HandleUnsavedChanges( this, _( "Save changes?" ), + [&]() -> bool + { + return TransferDataFromWindow(); + } ) ) + { + aEvent.Veto(); + return; + } + } + + // Save all our settings since we're really closing + SYMBOL_EDITOR_SETTINGS* cfg = m_parent->GetSettings(); + + cfg->m_LibFieldEditor.width = GetSize().x; + cfg->m_LibFieldEditor.height = GetSize().y; + + for( int i = 0; i < m_grid->GetNumberCols(); i++ ) + { + if( m_grid->IsColShown( i ) ) + { + std::string fieldName( m_dataModel->GetColFieldName( i ).ToUTF8() ); + cfg->m_LibFieldEditor.field_widths[fieldName] = m_grid->GetColSize( i ); + } + } + + aEvent.Skip(); +} + +bool DIALOG_LIB_FIELDS::TransferDataFromWindow() +{ + if( !m_grid->CommitPendingChanges() ) + return false; + + if( !wxDialog::TransferDataFromWindow() ) + return false; + + m_dataModel->ApplyData( + [&]( LIB_SYMBOL* ) + { + m_parent->OnModify(); + }, + [&]() + { + // Handle newly created derived symbols + auto createdSymbols = m_dataModel->GetAndClearCreatedDerivedSymbols(); + + wxLogTrace( traceLibFieldTable, "Post-apply handler: found %zu created derived symbols", + createdSymbols.size() ); + + for( const auto& [symbol, libraryName] : createdSymbols ) + { + if( !libraryName.IsEmpty() ) + { + wxLogTrace( traceLibFieldTable, "Updating symbol '%s' (UUID: %s) in library '%s'", + symbol->GetName(), symbol->m_Uuid.AsString(), libraryName ); + // Update the symbol in the library manager to properly register it + m_parent->GetLibManager().UpdateSymbol( symbol, libraryName ); + } + } + + // Sync libraries and refresh tree if there were changes + if( !createdSymbols.empty() ) + { + wxLogTrace( traceLibFieldTable, "Syncing libraries due to %zu new symbols", createdSymbols.size() ); + + // Store references to the created symbols before sync + std::vector symbolsToPreserve; + for( const auto& [symbol, libraryName] : createdSymbols ) + { + symbolsToPreserve.push_back( symbol ); + } + + // Synchronize libraries to update the tree view + m_parent->SyncLibraries( false ); + + // Ensure created symbols are still in the symbol list after sync + for( LIB_SYMBOL* symbol : symbolsToPreserve ) + { + bool found = false; + for( LIB_SYMBOL* existingSymbol : m_symbolsList ) + { + if( existingSymbol->m_Uuid == symbol->m_Uuid ) + { + found = true; + break; + } + } + if( !found ) + { + wxLogTrace( traceLibFieldTable, "Re-adding symbol '%s' to list after sync", symbol->GetName() ); + m_symbolsList.push_back( symbol ); + } + } + } + + wxLogTrace( traceLibFieldTable, "Dialog symbol list size after processing: %zu", m_symbolsList.size() ); + } ); + + ClearModify(); + + wxLogTrace( traceLibFieldTable, "About to rebuild grid rows to include new symbols" ); + RegroupSymbols(); + wxLogTrace( traceLibFieldTable, "Grid rebuild completed" ); + return true; +} + +void DIALOG_LIB_FIELDS::OnColumnItemToggled(wxDataViewEvent& event) +{ + wxDataViewItem item = event.GetItem(); + int row = m_fieldsCtrl->ItemToRow( item ); + int col = event.GetColumn(); + + switch ( col ) + { + case SHOW_FIELD_COLUMN: + { + wxString name = m_fieldsCtrl->GetTextValue( row, FIELD_NAME_COLUMN ); + bool value = m_fieldsCtrl->GetToggleValue( row, col ); + int dataCol = m_dataModel->GetFieldNameCol( name ); + + m_dataModel->SetShowColumn( dataCol, value ); + + if( dataCol != -1 ) + { + if( value ) + m_grid->ShowCol( dataCol ); + else + m_grid->HideCol( dataCol ); + } + + break; + } + + case GROUP_BY_COLUMN: + { + m_dataModel->SetGroupingEnabled( false ); + // Check if any columns are grouped. We don't keep a separate UI state for this + // so check all rows anytime we change a grouping column. + + for( int i = 0; i < m_fieldsCtrl->GetItemCount(); ++i ) + { + if( m_fieldsCtrl->GetToggleValue( i, GROUP_BY_COLUMN ) ) + { + m_dataModel->SetGroupingEnabled( true ); + break; + } + } + + wxString name = m_fieldsCtrl->GetTextValue( row, FIELD_NAME_COLUMN ); + bool value = m_fieldsCtrl->GetToggleValue( row, GROUP_BY_COLUMN ); + + wxString fieldName = m_fieldsCtrl->GetTextValue( row, FIELD_NAME_COLUMN ); + + m_dataModel->SetGroupColumn( m_dataModel->GetFieldNameCol( fieldName ), value ); + RegroupSymbols(); + break; + } + + default: + break; + } +} + +void DIALOG_LIB_FIELDS::OnFieldsCtrlSelectionChanged(wxDataViewEvent& event) +{ + // Enable/disable Rename/Remove buttons based on selection + int row = m_fieldsCtrl->GetSelectedRow(); + bool enable = ( row != -1 ); // Basic check, could add mandatory field check later + + // Check if the selected field is mandatory (cannot be removed/renamed) + if( enable ) + { + wxString fieldName = m_fieldsCtrl->GetTextValue( row, FIELD_NAME_COLUMN ); + if( fieldName == GetCanonicalFieldName( FIELD_T::REFERENCE ) || + fieldName == GetCanonicalFieldName( FIELD_T::VALUE ) || + fieldName == GetCanonicalFieldName( FIELD_T::FOOTPRINT ) || + fieldName == GetCanonicalFieldName( FIELD_T::DATASHEET ) ) + { + enable = false; + } + } + + m_removeFieldButton->Enable( enable ); + m_renameFieldButton->Enable( enable ); + + event.Skip(); // Allow default processing +} + +void DIALOG_LIB_FIELDS::OnSizeFieldList(wxSizeEvent& event) +{ + int width = KIPLATFORM::UI::GetUnobscuredSize( m_fieldsCtrl ).x + - m_showColWidth + - m_groupByColWidth; +#ifdef __WXMAC__ + // TODO: something in wxWidgets 3.1.x pads checkbox columns with extra space. (It used to + // also be that the width of the column would get set too wide (to 30), but that's patched in + // our local wxWidgets fork.) + width -= 50; +#endif + + m_fieldNameColWidth = width / 2; + m_labelColWidth = width = m_fieldNameColWidth; + + // GTK loses its head and messes these up when resizing the splitter bar: + m_fieldsCtrl->GetColumn( SHOW_FIELD_COLUMN )->SetWidth( m_showColWidth ); + m_fieldsCtrl->GetColumn( GROUP_BY_COLUMN )->SetWidth( m_groupByColWidth ); + + m_fieldsCtrl->GetColumn( FIELD_NAME_COLUMN )->SetHidden( true ); + m_fieldsCtrl->GetColumn( DISPLAY_NAME_COLUMN )->SetWidth( m_fieldNameColWidth ); + m_fieldsCtrl->GetColumn( LABEL_COLUMN )->SetWidth( m_labelColWidth ); + + m_fieldsCtrl->Refresh(); // To refresh checkboxes on Windows. + + event.Skip(); +} + +void DIALOG_LIB_FIELDS::OnAddField(wxCommandEvent& event) +{ + wxTextEntryDialog dlg( this, _( "New field name:" ), _( "Add Field" ) ); + + if( dlg.ShowModal() != wxID_OK ) + return; + + wxString fieldName = dlg.GetValue(); + + if( fieldName.IsEmpty() ) + { + DisplayError( this, _( "Field must have a name." ) ); + return; + } + + for( int i = 0; i < m_dataModel->GetNumberCols(); ++i ) + { + if( fieldName == m_dataModel->GetColFieldName( i ) ) + { + DisplayError( this, wxString::Format( _( "Field name '%s' already in use." ), + fieldName ) ); + return; + } + } + + AddField( fieldName, GetGeneratedFieldDisplayName( fieldName ), true, false, true ); + + SetupColumnProperties( m_dataModel->GetColsCount() - 1 ); + + OnModify(); +} + +void DIALOG_LIB_FIELDS::OnRenameField(wxCommandEvent& event) +{ + int row = m_fieldsCtrl->GetSelectedRow(); + + if( row == -1 ) + { + wxBell(); + return; + } + + wxString fieldName = m_fieldsCtrl->GetTextValue( row, FIELD_NAME_COLUMN ); + + int col = m_dataModel->GetFieldNameCol( fieldName ); + wxCHECK_RET( col != -1, wxS( "Existing field name missing from data model" ) ); + + wxTextEntryDialog dlg( this, _( "New field name:" ), _( "Rename Field" ), fieldName ); + + if( dlg.ShowModal() != wxID_OK ) + return; + + wxString newFieldName = dlg.GetValue(); + + if( newFieldName == fieldName ) + return; + + if( m_dataModel->GetFieldNameCol( newFieldName ) != -1 ) + { + DisplayError( this, wxString::Format( _( "Field name '%s' already exists." ), newFieldName ) ); + return; + } + + RenameField( fieldName, newFieldName ); + + m_fieldsCtrl->SetTextValue( newFieldName, row, DISPLAY_NAME_COLUMN ); + m_fieldsCtrl->SetTextValue( newFieldName, row, FIELD_NAME_COLUMN ); + m_fieldsCtrl->SetTextValue( newFieldName, row, LABEL_COLUMN ); + + SetupColumnProperties( col ); + OnModify(); +} + +void DIALOG_LIB_FIELDS::OnRemoveField(wxCommandEvent& event) +{ + int row = m_fieldsCtrl->GetSelectedRow(); + + if( row == -1 ) + { + wxBell(); + return; + } + + wxString fieldName = m_fieldsCtrl->GetTextValue( row, FIELD_NAME_COLUMN ); + wxString displayName = m_fieldsCtrl->GetTextValue( row, DISPLAY_NAME_COLUMN ); + + wxString confirm_msg = wxString::Format( _( "Are you sure you want to remove the field '%s'?" ), + displayName ); + + if( !IsOK( this, confirm_msg ) ) + return; + + int col = m_dataModel->GetFieldNameCol( fieldName ); + + RemoveField( fieldName ); + + m_fieldsCtrl->DeleteItem( row ); + + if( row > 0 ) + m_fieldsCtrl->SelectRow( row - 1 ); + + wxGridTableMessage msg( m_dataModel, wxGRIDTABLE_NOTIFY_COLS_DELETED, col, 1 ); + m_grid->ProcessTableMessage( msg ); + + OnModify(); +} + +void DIALOG_LIB_FIELDS::OnFilterMouseMoved(wxMouseEvent& aEvent) +{ + wxPoint pos = aEvent.GetPosition(); + wxRect ctrlRect = m_filter->GetScreenRect(); + int buttonWidth = ctrlRect.GetHeight(); // Presume buttons are square + + // TODO: restore cursor when mouse leaves the filter field (or is it a MSW bug?) + if( m_filter->IsSearchButtonVisible() && pos.x < buttonWidth ) + SetCursor( wxCURSOR_ARROW ); + else if( m_filter->IsCancelButtonVisible() && pos.x > ctrlRect.GetWidth() - buttonWidth ) + SetCursor( wxCURSOR_ARROW ); + else + SetCursor( wxCURSOR_IBEAM ); +} + +void DIALOG_LIB_FIELDS::OnFilterText( wxCommandEvent& event ) +{ + m_dataModel->SetFilter( m_filter->GetValue() ); + RegroupSymbols(); +} + +void DIALOG_LIB_FIELDS::OnRegroupSymbols( wxCommandEvent& event ) +{ + RegroupSymbols(); +} + +void DIALOG_LIB_FIELDS::OnTableValueChanged( wxGridEvent& event ) +{ + m_grid->ForceRefresh(); + OnModify(); +} + +void DIALOG_LIB_FIELDS::OnTableCellClick( wxGridEvent& event ) +{ + wxString cellValue = m_grid->GetCellValue( event.GetRow(), event.GetCol() ); + + if( cellValue.StartsWith( wxS( "> " ) ) || cellValue.StartsWith( wxS( "v " ) ) ) + { + m_grid->ClearSelection(); + m_dataModel->ExpandCollapseRow( event.GetRow() ); + m_grid->SetGridCursor( event.GetRow(), event.GetCol() ); + // Don't call event.Skip() - we've handled this event + } + else + { + event.Skip(); // Let normal cell editing proceed + } +} + +void DIALOG_LIB_FIELDS::OnTableItemContextMenu( wxGridEvent& event ) +{ + // Try to use the mouse position to determine the actual cell under the cursor. + // Fall back to the event's row/col if mapping fails. + int col = event.GetCol(); + int row = event.GetRow(); + + if ( m_grid ) + { + // Get global mouse position and convert to grid client coords + wxPoint mousePos = wxGetMousePosition(); + wxPoint gridPt = m_grid->ScreenToClient( mousePos ); + + row = m_grid->YToRow( gridPt.y ); + col = m_grid->XToCol( gridPt.x ); + } + + if ( row != -1 && col != -1 ) + m_grid->SetGridCursor( row, col ); + + event.Skip(); +} + +void DIALOG_LIB_FIELDS::OnTableColSize(wxGridSizeEvent& aEvent) +{ + int col = aEvent.GetRowOrCol(); + std::string key( m_dataModel->GetColFieldName( col ).ToUTF8() ); + + aEvent.Skip(); + + m_grid->ForceRefresh(); +} + +void DIALOG_LIB_FIELDS::OnCancel(wxCommandEvent& event) +{ + EndModal( wxID_CANCEL ); +} + +void DIALOG_LIB_FIELDS::OnOk(wxCommandEvent& event) +{ + if( TransferDataFromWindow() ) + { + EndModal( wxID_OK ); + } +} + +void DIALOG_LIB_FIELDS::OnApply(wxCommandEvent& event) +{ + TransferDataFromWindow(); +} + +void DIALOG_LIB_FIELDS::UpdateFieldList() +{ + auto addMandatoryField = + [&]( FIELD_T fieldId, bool show, bool groupBy ) + { + AddField( GetCanonicalFieldName( fieldId ), + GetDefaultFieldName( fieldId, DO_TRANSLATE ), show, groupBy ); + }; + + // Add mandatory fields first show groupBy + addMandatoryField( FIELD_T::REFERENCE, false, false ); + addMandatoryField( FIELD_T::VALUE, true, false ); + addMandatoryField( FIELD_T::FOOTPRINT, true, false ); + addMandatoryField( FIELD_T::DATASHEET, true, false ); + addMandatoryField( FIELD_T::DESCRIPTION, false, false ); + + AddField( wxS( "Keywords" ), _( "Keywords" ), true, false ); + + // Add attribute fields as checkboxes show groupBy user checkbox + AddField( wxS( "${EXCLUDE_FROM_BOM}" ), _( "Exclude From BOM" ), true, false, false, true ); + AddField( wxS( "${EXCLUDE_FROM_SIM}" ), _( "Exclude From Simulation" ), true, false, false, true ); + AddField( wxS( "${EXCLUDE_FROM_BOARD}" ), _( "Exclude From Board" ), true, false, false, true ); + + AddField( wxS( "Power" ), _( "Power Symbol" ), true, false, false, true ); + AddField( wxS( "LocalPower" ), _( "Local Power Symbol" ), true, false, false, true ); + + // User fields next + std::set userFieldNames; + + for( LIB_SYMBOL* symbol : m_symbolsList ) + { + std::vector< SCH_FIELD* > fields; + symbol->GetFields( fields ); + + for( SCH_FIELD* field : fields ) + { + if( !field->IsMandatory() && !field->IsPrivate() ) + userFieldNames.insert( field->GetName() ); + } + } + + for( const wxString& fieldName : userFieldNames ) + AddField( fieldName, GetGeneratedFieldDisplayName( fieldName ), true, false ); +} + +void DIALOG_LIB_FIELDS::AddField( const wxString& aFieldName, const wxString& aLabelValue, + bool show, bool groupBy, bool addedByUser, bool aIsCheckbox ) +{ + // Users can add fields with variable names that match the special names in the grid, + // e.g. ${QUANTITY} so make sure we don't add them twice + for( int i = 0; i < m_fieldsCtrl->GetItemCount(); i++ ) + { + if( m_fieldsCtrl->GetTextValue( i, FIELD_NAME_COLUMN ) == aFieldName ) + return; + } + + m_dataModel->AddColumn( aFieldName, aLabelValue, addedByUser, aIsCheckbox ); + + wxVector fieldsCtrlRow; + std::string key( aFieldName.ToUTF8() ); + + // Don't change these to emplace_back: some versions of wxWidgets don't support it + fieldsCtrlRow.push_back( wxVariant( aFieldName ) ); + fieldsCtrlRow.push_back( wxVariant( aLabelValue ) ); + fieldsCtrlRow.push_back( wxVariant( show ) ); + fieldsCtrlRow.push_back( wxVariant( groupBy ) ); + fieldsCtrlRow.push_back( wxVariant( aFieldName ) ); + + m_fieldsCtrl->AppendItem( fieldsCtrlRow ); + + wxGridTableMessage msg( m_dataModel, wxGRIDTABLE_NOTIFY_COLS_APPENDED, 1 ); + m_grid->ProcessTableMessage( msg ); +} + +void DIALOG_LIB_FIELDS::RemoveField(const wxString& fieldName) +{ + int col = m_dataModel->GetFieldNameCol( fieldName ); + wxCHECK_RET( col != -1, wxS( "Field name not found" ) ); + + m_dataModel->RemoveColumn( col ); +} + +void DIALOG_LIB_FIELDS::RenameField(const wxString& oldName, const wxString& newName) +{ + int col = m_dataModel->GetFieldNameCol( oldName ); + wxCHECK_RET( col != -1, wxS( "Existing field name missing from data model" ) ); + + m_dataModel->RenameColumn( col, newName ); +} + +void DIALOG_LIB_FIELDS::RegroupSymbols() +{ + m_dataModel->RebuildRows(); + m_grid->ForceRefresh(); +} + +void DIALOG_LIB_FIELDS::OnColSort(wxGridEvent& aEvent) +{ + int sortCol = aEvent.GetCol(); + std::string key( m_dataModel->GetColFieldName( sortCol ).ToUTF8() ); + bool ascending; + + // This is bonkers, but wxWidgets doesn't tell us ascending/descending in the event, and + // if we ask it will give us pre-event info. + if( m_grid->IsSortingBy( sortCol ) ) + { + // same column; invert ascending + ascending = !m_grid->IsSortOrderAscending(); + } + else + { + // different column; start with ascending + ascending = true; + } + + m_dataModel->SetSorting( sortCol, ascending ); + RegroupSymbols(); +} + +void DIALOG_LIB_FIELDS::OnColMove(wxGridEvent& aEvent) +{ + int origPos = aEvent.GetCol(); + + // Save column widths since the setup function uses the saved config values + SYMBOL_EDITOR_SETTINGS* cfg = m_parent->GetSettings(); + + for( int i = 0; i < m_grid->GetNumberCols(); i++ ) + { + if( m_grid->IsColShown( i ) ) + { + std::string fieldName( m_dataModel->GetColFieldName( i ).ToUTF8() ); + cfg->m_LibFieldEditor.field_widths[fieldName] = m_grid->GetColSize( i ); + } + } + + CallAfter( + [origPos, this]() + { + int newPos = m_grid->GetColPos( origPos ); + +#ifdef __WXMAC__ + if( newPos < origPos ) + newPos += 1; +#endif + + m_dataModel->MoveColumn( origPos, newPos ); + + // "Unmove" the column since we've moved the column internally + m_grid->ResetColPos(); + + // We need to reset all the column attr's to the correct column order + SetupAllColumnProperties(); + + m_grid->ForceRefresh(); + } ); +} + +void DIALOG_LIB_FIELDS::OnColLabelChange(wxDataViewEvent& aEvent) +{ + wxDataViewItem item = aEvent.GetItem(); + int row = m_fieldsCtrl->ItemToRow( item ); + wxString label = m_fieldsCtrl->GetTextValue( row, LABEL_COLUMN ); + wxString fieldName = m_fieldsCtrl->GetTextValue( row, FIELD_NAME_COLUMN ); + int col = m_dataModel->GetFieldNameCol( fieldName ); + + if( col != -1 ) + m_dataModel->SetColLabelValue( col, label ); + + aEvent.Skip(); + + m_grid->ForceRefresh(); +} + + +void DIALOG_LIB_FIELDS::SetupColumnProperties( int aCol ) +{ + wxGridCellAttr* attr = new wxGridCellAttr; + attr->SetReadOnly( false ); + + // Set some column types to specific editors + if( m_dataModel->GetColFieldName( aCol ) == GetCanonicalFieldName( FIELD_T::FOOTPRINT ) ) + { + attr->SetEditor( new GRID_CELL_FPID_EDITOR( this, wxEmptyString ) ); + m_dataModel->SetColAttr( attr, aCol ); + } + else if( m_dataModel->GetColFieldName( aCol ) == GetCanonicalFieldName( FIELD_T::DATASHEET ) ) + { + // set datasheet column viewer button + attr->SetEditor( new GRID_CELL_URL_EDITOR( this, PROJECT_SCH::SchSearchS( &Prj() ) ) ); + m_dataModel->SetColAttr( attr, aCol ); + } + else if( m_dataModel->ColIsCheck( aCol ) ) + { + attr->SetAlignment( wxALIGN_CENTER, wxALIGN_CENTER ); + attr->SetRenderer( new GRID_CELL_CHECKBOX_RENDERER() ); + m_dataModel->SetColAttr( attr, aCol ); + } + else if( IsGeneratedField( m_dataModel->GetColFieldName( aCol ) ) ) + { + attr->SetReadOnly(); + m_dataModel->SetColAttr( attr, aCol ); + } + else + { + attr->SetEditor( m_grid->GetDefaultEditor() ); + m_dataModel->SetColAttr( attr, aCol ); + } +} + + +void DIALOG_LIB_FIELDS::SetupAllColumnProperties() +{ + SYMBOL_EDITOR_SETTINGS* cfg = m_parent->GetSettings(); + wxSize defaultDlgSize = ConvertDialogToPixels( wxSize( 600, 300 ) ); + + // Restore column sorting order and widths + m_grid->AutoSizeColumns( false ); + int sortCol = 0; + bool sortAscending = true; + + // Find the VALUE column for initial sorting + int valueCol = m_dataModel->GetFieldNameCol( GetCanonicalFieldName( FIELD_T::VALUE ) ); + + // Set initial sort to VALUE field (ascending) if no previous sort preference exists + if( m_dataModel->GetSortCol() == 0 && valueCol != -1 ) + { + sortCol = valueCol; + sortAscending = true; + m_dataModel->SetSorting( sortCol, sortAscending ); + } + + for( int col = 0; col < m_grid->GetNumberCols(); ++col ) + { + SetupColumnProperties( col ); + + if( col == m_dataModel->GetSortCol() ) + { + sortCol = col; + sortAscending = m_dataModel->GetSortAsc(); + } + } + + // sync m_grid's column visibilities to Show checkboxes in m_fieldsCtrl + for( int i = 0; i < m_fieldsCtrl->GetItemCount(); ++i ) + { + int col = m_dataModel->GetFieldNameCol( m_fieldsCtrl->GetTextValue( i, FIELD_NAME_COLUMN ) ); + + if( col == -1 ) + continue; + + bool show = m_fieldsCtrl->GetToggleValue( i, SHOW_FIELD_COLUMN ); + m_dataModel->SetShowColumn( col, show ); + + if( show ) + { + m_grid->ShowCol( col ); + + std::string key( m_dataModel->GetColFieldName( col ).ToUTF8() ); + + if( cfg->m_LibFieldEditor.field_widths.count( key ) + && ( cfg->m_LibFieldEditor.field_widths.at( key ) > 0 ) ) + { + m_grid->SetColSize( col, cfg->m_LibFieldEditor.field_widths.at( key ) ); + } + else + { + int textWidth = m_dataModel->GetDataWidth( col ) + COLUMN_MARGIN; + int maxWidth = defaultDlgSize.x / 3; + + m_grid->SetColSize( col, std::clamp( textWidth, 100, maxWidth ) ); + } + } + else + { + m_grid->HideCol( col ); + } + } + + m_dataModel->SetSorting( sortCol, sortAscending ); + m_grid->SetSortingColumn( sortCol, sortAscending ); +} + diff --git a/eeschema/dialogs/dialog_lib_fields.h b/eeschema/dialogs/dialog_lib_fields.h new file mode 100644 index 0000000000..4856223d86 --- /dev/null +++ b/eeschema/dialogs/dialog_lib_fields.h @@ -0,0 +1,86 @@ +/* + * 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 . + */ + +#ifndef DIALOG_LIB_FIELDS_H +#define DIALOG_LIB_FIELDS_H + +#include +#include + +class SYMBOL_EDIT_FRAME; +class LIB_FIELDS_EDITOR_GRID_DATA_MODEL; +class LIB_SYMBOL; + +class DIALOG_LIB_FIELDS : public DIALOG_LIB_FIELDS_BASE +{ +public: + DIALOG_LIB_FIELDS( SYMBOL_EDIT_FRAME* parent, wxString libId ); + ~DIALOG_LIB_FIELDS() override; + + void OnInit(); + +protected: + void OnClose( wxCloseEvent& event ) override; + void OnColumnItemToggled( wxDataViewEvent& event ) override; + void OnFieldsCtrlSelectionChanged( wxDataViewEvent& event ) override; + void OnSizeFieldList( wxSizeEvent& event ) override; + void OnAddField( wxCommandEvent& event ) override; + void OnRenameField( wxCommandEvent& event ) override; + void OnRemoveField( wxCommandEvent& event ) override; + void OnFilterMouseMoved( wxMouseEvent& event ) override; + void OnFilterText( wxCommandEvent& event ) override; + void OnRegroupSymbols( wxCommandEvent& event ) override; + void OnTableValueChanged( wxGridEvent& event ) override; + void OnTableCellClick( wxGridEvent& event ) override; + void OnTableItemContextMenu( wxGridEvent& event ) override; + void OnTableColSize( wxGridSizeEvent& event ) override; + void OnCancel( wxCommandEvent& event ) override; + void OnOk( wxCommandEvent& event ) override; + void OnApply( wxCommandEvent& event ) override; + bool TransferDataFromWindow() override; + +private: + void UpdateFieldList(); + void AddField( const wxString& aFieldName, const wxString& aLabelValue, bool show, bool groupBy, bool addedByUser = false, bool aIsCheckbox = false ); + void RemoveField( const wxString& fieldName ); + void RenameField( const wxString& oldName, const wxString& newName ); + void RegroupSymbols(); + + void OnColSort( wxGridEvent& aEvent ); + void OnColMove( wxGridEvent& aEvent ); + void OnColLabelChange( wxDataViewEvent& aEvent ); + void SetupColumnProperties( int aCol ); + void SetupAllColumnProperties(); + + void loadSymbols(); + + wxString m_libId; + SYMBOL_EDIT_FRAME* m_parent; + + int m_fieldNameColWidth; + int m_labelColWidth; + int m_showColWidth; + int m_groupByColWidth; + + LIB_FIELDS_EDITOR_GRID_DATA_MODEL* m_dataModel; + std::vector m_symbolsList; +}; + + +#endif // DIALOG_LIB_FIELDS_H diff --git a/eeschema/dialogs/dialog_lib_fields_base.cpp b/eeschema/dialogs/dialog_lib_fields_base.cpp new file mode 100644 index 0000000000..6ce7a19fc4 --- /dev/null +++ b/eeschema/dialogs/dialog_lib_fields_base.cpp @@ -0,0 +1,203 @@ +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version 4.2.1-0-g80c4cb6a-dirty) +// http://www.wxformbuilder.org/ +// +// PLEASE DO *NOT* EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// + +#include "widgets/std_bitmap_button.h" +#include "widgets/wx_grid.h" + +#include "dialog_lib_fields_base.h" + +/////////////////////////////////////////////////////////////////////////// + +DIALOG_LIB_FIELDS_BASE::DIALOG_LIB_FIELDS_BASE( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : DIALOG_SHIM( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxSize( -1,-1 ), wxDefaultSize ); + + wxBoxSizer* bMainSizer; + bMainSizer = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bEditSizer; + bEditSizer = new wxBoxSizer( wxVERTICAL ); + + m_splitterMainWindow = new wxSplitterWindow( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSP_3DSASH|wxSP_LIVE_UPDATE|wxSP_NO_XP_THEME ); + m_splitterMainWindow->SetMinimumPaneSize( 200 ); + + m_leftPanel = new wxPanel( m_splitterMainWindow, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + wxBoxSizer* bLeftSizer; + bLeftSizer = new wxBoxSizer( wxVERTICAL ); + + m_fieldsCtrl = new wxDataViewListCtrl( m_leftPanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 ); + m_fieldsCtrl->SetMinSize( wxSize( -1,250 ) ); + + bLeftSizer->Add( m_fieldsCtrl, 1, wxBOTTOM|wxEXPAND|wxLEFT|wxRIGHT|wxTOP, 5 ); + + wxBoxSizer* bFieldsButtons; + bFieldsButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_addFieldButton = new STD_BITMAP_BUTTON( m_leftPanel, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 ); + bFieldsButtons->Add( m_addFieldButton, 0, wxBOTTOM|wxLEFT, 5 ); + + m_renameFieldButton = new STD_BITMAP_BUTTON( m_leftPanel, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 ); + bFieldsButtons->Add( m_renameFieldButton, 0, wxBOTTOM|wxLEFT, 5 ); + + + bFieldsButtons->Add( 15, 0, 0, wxEXPAND, 5 ); + + m_removeFieldButton = new STD_BITMAP_BUTTON( m_leftPanel, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 ); + bFieldsButtons->Add( m_removeFieldButton, 0, wxBOTTOM|wxLEFT, 5 ); + + + bLeftSizer->Add( bFieldsButtons, 0, wxEXPAND, 5 ); + + + m_leftPanel->SetSizer( bLeftSizer ); + m_leftPanel->Layout(); + bLeftSizer->Fit( m_leftPanel ); + m_rightPanel = new wxPanel( m_splitterMainWindow, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + wxBoxSizer* bRightSizer; + bRightSizer = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bControls; + bControls = new wxBoxSizer( wxHORIZONTAL ); + + m_filter = new wxSearchCtrl( m_rightPanel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + #ifndef __WXMAC__ + m_filter->ShowSearchButton( true ); + #endif + m_filter->ShowCancelButton( true ); + m_filter->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, wxEmptyString ) ); + m_filter->SetMinSize( wxSize( 140,-1 ) ); + + bControls->Add( m_filter, 1, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + m_staticline31 = new wxStaticLine( m_rightPanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bControls->Add( m_staticline31, 0, wxEXPAND | wxALL, 3 ); + + m_bRefresh = new STD_BITMAP_BUTTON( m_rightPanel, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 ); + m_bRefresh->SetMinSize( wxSize( 30,30 ) ); + + bControls->Add( m_bRefresh, 0, wxALIGN_CENTER_VERTICAL|wxLEFT|wxRIGHT, 5 ); + + + bRightSizer->Add( bControls, 0, wxEXPAND|wxLEFT|wxTOP, 5 ); + + m_grid = new WX_GRID( m_rightPanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 ); + + // Grid + m_grid->CreateGrid( 5, 5 ); + m_grid->EnableEditing( true ); + m_grid->EnableGridLines( true ); + m_grid->EnableDragGridSize( false ); + m_grid->SetMargins( 0, 0 ); + + // Columns + m_grid->EnableDragColMove( true ); + m_grid->EnableDragColSize( true ); + m_grid->SetColLabelSize( 24 ); + m_grid->SetColLabelAlignment( wxALIGN_CENTER, wxALIGN_CENTER ); + + // Rows + m_grid->EnableDragRowSize( false ); + m_grid->SetRowLabelSize( 0 ); + m_grid->SetRowLabelAlignment( wxALIGN_CENTER, wxALIGN_CENTER ); + + // Label Appearance + + // Cell Defaults + m_grid->SetDefaultCellAlignment( wxALIGN_LEFT, wxALIGN_CENTER ); + m_grid->SetMinSize( wxSize( 400,200 ) ); + + bRightSizer->Add( m_grid, 1, wxBOTTOM|wxEXPAND|wxLEFT|wxRIGHT|wxTOP, 5 ); + + + m_rightPanel->SetSizer( bRightSizer ); + m_rightPanel->Layout(); + bRightSizer->Fit( m_rightPanel ); + m_splitterMainWindow->SplitVertically( m_leftPanel, m_rightPanel, -1 ); + bEditSizer->Add( m_splitterMainWindow, 1, wxEXPAND|wxTOP|wxBOTTOM, 5 ); + + + bMainSizer->Add( bEditSizer, 1, wxEXPAND, 5 ); + + wxBoxSizer* bButtonsSizer; + bButtonsSizer = new wxBoxSizer( wxHORIZONTAL ); + + + bButtonsSizer->Add( 0, 0, 9, wxEXPAND, 5 ); + + m_sdbSizer = new wxStdDialogButtonSizer(); + m_sdbSizerOK = new wxButton( this, wxID_OK ); + m_sdbSizer->AddButton( m_sdbSizerOK ); + m_sdbSizerApply = new wxButton( this, wxID_APPLY ); + m_sdbSizer->AddButton( m_sdbSizerApply ); + m_sdbSizerCancel = new wxButton( this, wxID_CANCEL ); + m_sdbSizer->AddButton( m_sdbSizerCancel ); + m_sdbSizer->Realize(); + + bButtonsSizer->Add( m_sdbSizer, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + + bMainSizer->Add( bButtonsSizer, 0, wxEXPAND, 5 ); + + + this->SetSizer( bMainSizer ); + this->Layout(); + bMainSizer->Fit( this ); + + this->Centre( wxBOTH ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( DIALOG_LIB_FIELDS_BASE::OnClose ) ); + m_fieldsCtrl->Connect( wxEVT_COMMAND_DATAVIEW_ITEM_VALUE_CHANGED, wxDataViewEventHandler( DIALOG_LIB_FIELDS_BASE::OnColumnItemToggled ), NULL, this ); + m_fieldsCtrl->Connect( wxEVT_COMMAND_DATAVIEW_SELECTION_CHANGED, wxDataViewEventHandler( DIALOG_LIB_FIELDS_BASE::OnFieldsCtrlSelectionChanged ), NULL, this ); + m_fieldsCtrl->Connect( wxEVT_SIZE, wxSizeEventHandler( DIALOG_LIB_FIELDS_BASE::OnSizeFieldList ), NULL, this ); + m_addFieldButton->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_FIELDS_BASE::OnAddField ), NULL, this ); + m_renameFieldButton->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_FIELDS_BASE::OnRenameField ), NULL, this ); + m_removeFieldButton->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_FIELDS_BASE::OnRemoveField ), NULL, this ); + m_filter->Connect( wxEVT_MOTION, wxMouseEventHandler( DIALOG_LIB_FIELDS_BASE::OnFilterMouseMoved ), NULL, this ); + m_filter->Connect( wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler( DIALOG_LIB_FIELDS_BASE::OnFilterText ), NULL, this ); + m_bRefresh->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_FIELDS_BASE::OnRegroupSymbols ), NULL, this ); + m_grid->Connect( wxEVT_GRID_CELL_CHANGED, wxGridEventHandler( DIALOG_LIB_FIELDS_BASE::OnTableValueChanged ), NULL, this ); + m_grid->Connect( wxEVT_GRID_CELL_LEFT_CLICK, wxGridEventHandler( DIALOG_LIB_FIELDS_BASE::OnTableCellClick ), NULL, this ); + m_grid->Connect( wxEVT_GRID_CELL_LEFT_DCLICK, wxGridEventHandler( DIALOG_LIB_FIELDS_BASE::OnTableCellClick ), NULL, this ); + m_grid->Connect( wxEVT_GRID_CELL_RIGHT_CLICK, wxGridEventHandler( DIALOG_LIB_FIELDS_BASE::OnTableItemContextMenu ), NULL, this ); + m_grid->Connect( wxEVT_GRID_CELL_LEFT_CLICK, wxGridEventHandler( DIALOG_LIB_FIELDS_BASE::OnTableCmdCellClick ), NULL, this ); + m_grid->Connect( wxEVT_GRID_CELL_LEFT_DCLICK, wxGridEventHandler( DIALOG_LIB_FIELDS_BASE::OnTableCmdCellClick ), NULL, this ); + m_grid->Connect( wxEVT_GRID_COL_SIZE, wxGridSizeEventHandler( DIALOG_LIB_FIELDS_BASE::OnTableColSize ), NULL, this ); + m_grid->Connect( wxEVT_GRID_EDITOR_SHOWN, wxGridEventHandler( DIALOG_LIB_FIELDS_BASE::OnEditorShown ), NULL, this ); + m_grid->Connect( wxEVT_GRID_SELECT_CELL, wxGridEventHandler( DIALOG_LIB_FIELDS_BASE::OnTableSelectCell ), NULL, this ); + m_sdbSizerApply->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_FIELDS_BASE::OnApply ), NULL, this ); + m_sdbSizerCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_FIELDS_BASE::OnCancel ), NULL, this ); + m_sdbSizerOK->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_FIELDS_BASE::OnOk ), NULL, this ); +} + +DIALOG_LIB_FIELDS_BASE::~DIALOG_LIB_FIELDS_BASE() +{ + // Disconnect Events + this->Disconnect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( DIALOG_LIB_FIELDS_BASE::OnClose ) ); + m_fieldsCtrl->Disconnect( wxEVT_COMMAND_DATAVIEW_ITEM_VALUE_CHANGED, wxDataViewEventHandler( DIALOG_LIB_FIELDS_BASE::OnColumnItemToggled ), NULL, this ); + m_fieldsCtrl->Disconnect( wxEVT_COMMAND_DATAVIEW_SELECTION_CHANGED, wxDataViewEventHandler( DIALOG_LIB_FIELDS_BASE::OnFieldsCtrlSelectionChanged ), NULL, this ); + m_fieldsCtrl->Disconnect( wxEVT_SIZE, wxSizeEventHandler( DIALOG_LIB_FIELDS_BASE::OnSizeFieldList ), NULL, this ); + m_addFieldButton->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_FIELDS_BASE::OnAddField ), NULL, this ); + m_renameFieldButton->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_FIELDS_BASE::OnRenameField ), NULL, this ); + m_removeFieldButton->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_FIELDS_BASE::OnRemoveField ), NULL, this ); + m_filter->Disconnect( wxEVT_MOTION, wxMouseEventHandler( DIALOG_LIB_FIELDS_BASE::OnFilterMouseMoved ), NULL, this ); + m_filter->Disconnect( wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler( DIALOG_LIB_FIELDS_BASE::OnFilterText ), NULL, this ); + m_bRefresh->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_FIELDS_BASE::OnRegroupSymbols ), NULL, this ); + m_grid->Disconnect( wxEVT_GRID_CELL_CHANGED, wxGridEventHandler( DIALOG_LIB_FIELDS_BASE::OnTableValueChanged ), NULL, this ); + m_grid->Disconnect( wxEVT_GRID_CELL_LEFT_CLICK, wxGridEventHandler( DIALOG_LIB_FIELDS_BASE::OnTableCellClick ), NULL, this ); + m_grid->Disconnect( wxEVT_GRID_CELL_LEFT_DCLICK, wxGridEventHandler( DIALOG_LIB_FIELDS_BASE::OnTableCellClick ), NULL, this ); + m_grid->Disconnect( wxEVT_GRID_CELL_RIGHT_CLICK, wxGridEventHandler( DIALOG_LIB_FIELDS_BASE::OnTableItemContextMenu ), NULL, this ); + m_grid->Disconnect( wxEVT_GRID_CELL_LEFT_CLICK, wxGridEventHandler( DIALOG_LIB_FIELDS_BASE::OnTableCmdCellClick ), NULL, this ); + m_grid->Disconnect( wxEVT_GRID_CELL_LEFT_DCLICK, wxGridEventHandler( DIALOG_LIB_FIELDS_BASE::OnTableCmdCellClick ), NULL, this ); + m_grid->Disconnect( wxEVT_GRID_COL_SIZE, wxGridSizeEventHandler( DIALOG_LIB_FIELDS_BASE::OnTableColSize ), NULL, this ); + m_grid->Disconnect( wxEVT_GRID_EDITOR_SHOWN, wxGridEventHandler( DIALOG_LIB_FIELDS_BASE::OnEditorShown ), NULL, this ); + m_grid->Disconnect( wxEVT_GRID_SELECT_CELL, wxGridEventHandler( DIALOG_LIB_FIELDS_BASE::OnTableSelectCell ), NULL, this ); + m_sdbSizerApply->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_FIELDS_BASE::OnApply ), NULL, this ); + m_sdbSizerCancel->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_FIELDS_BASE::OnCancel ), NULL, this ); + m_sdbSizerOK->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_FIELDS_BASE::OnOk ), NULL, this ); + +} diff --git a/eeschema/dialogs/dialog_lib_fields_base.fbp b/eeschema/dialogs/dialog_lib_fields_base.fbp new file mode 100644 index 0000000000..91427a49ab --- /dev/null +++ b/eeschema/dialogs/dialog_lib_fields_base.fbp @@ -0,0 +1,898 @@ + + + + + C++ + + 1 + connect + none + + + 0 + 0 + res + UTF-8 + dialog_lib_fields_base + 1000 + 1 + 1 + UI + dialog_lib_fields_base + . + 0 + source_name + 1 + 0 + source_name + + + 1 + 1 + 0 + 0 + + 0 + wxAUI_MGR_DEFAULT + + wxBOTH + + 1 + 0 + 1 + impl_virtual + + + + 0 + wxID_ANY + + -1,-1 + DIALOG_LIB_FIELDS_BASE + + -1,-1 + wxDEFAULT_DIALOG_STYLE|wxMAXIMIZE_BOX|wxRESIZE_BORDER + DIALOG_SHIM; dialog_shim.h + Library Fields Editor + + 0 + + + + OnClose + + -1,-1 + bMainSizer + wxVERTICAL + none + + 5 + wxEXPAND + 1 + + + bEditSizer + wxVERTICAL + none + + 5 + wxEXPAND|wxTOP|wxBOTTOM + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + 200 + + 0 + -1,-1 + 1 + m_splitterMainWindow + 1 + + + protected + 1 + + Resizable + 0.0 + -1 + -1 + 1 + + wxSPLIT_VERTICAL + wxSP_3DSASH|wxSP_LIVE_UPDATE|wxSP_NO_XP_THEME + + 0 + + + + + + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_leftPanel + 1 + + + protected + 1 + + Resizable + 1 + + + 0 + + + + wxTAB_TRAVERSAL + + + bLeftSizer + wxVERTICAL + none + + 5 + wxBOTTOM|wxEXPAND|wxLEFT|wxRIGHT|wxTOP + 1 + + + + 1 + 0 + 1 + + + 0 + wxID_ANY + + -1,250 + m_fieldsCtrl + protected + + + + + + + + + OnColumnItemToggled + OnFieldsCtrlSelectionChanged + OnSizeFieldList + + + + 5 + wxEXPAND + 0 + + + bFieldsButtons + wxHORIZONTAL + none + + 5 + wxBOTTOM|wxLEFT + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + MyButton + + 0 + + 0 + + + 0 + + 1 + m_addFieldButton + 1 + + + protected + 1 + + + + Resizable + 1 + + + STD_BITMAP_BUTTON; widgets/std_bitmap_button.h; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + OnAddField + + + + 5 + wxBOTTOM|wxLEFT + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Rename Field... + + 0 + + 0 + + + 0 + + 1 + m_renameFieldButton + 1 + + + protected + 1 + + + + Resizable + 1 + + + STD_BITMAP_BUTTON; widgets/std_bitmap_button.h; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + OnRenameField + + + + 5 + wxEXPAND + 0 + + 0 + protected + 15 + + + + 5 + wxBOTTOM|wxLEFT + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Remove Field... + + 0 + + 0 + + + 0 + + 1 + m_removeFieldButton + 1 + + + protected + 1 + + + + Resizable + 1 + + + STD_BITMAP_BUTTON; widgets/std_bitmap_button.h; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + OnRemoveField + + + + + + + + + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_rightPanel + 1 + + + protected + 1 + + Resizable + 1 + + + 0 + + + + wxTAB_TRAVERSAL + + + bRightSizer + wxVERTICAL + none + + 5 + wxEXPAND|wxLEFT|wxTOP + 0 + + + bControls + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER_VERTICAL|wxRIGHT + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + 1 + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + ,90,400,-1,70,0 + 0 + 0 + wxID_ANY + + 0 + + + 0 + 140,-1 + 1 + m_filter + 1 + + + protected + 1 + + Resizable + 1 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + OnFilterMouseMoved + OnFilterText + + + + 3 + wxEXPAND | wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_staticline31 + 1 + + + protected + 1 + + Resizable + 1 + + wxLI_VERTICAL + ; ; forward_declare + 0 + + + + + + + + 5 + wxALIGN_CENTER_VERTICAL|wxLEFT|wxRIGHT + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Refresh Grouping + + 0 + + 0 + + + 0 + 30,30 + 1 + m_bRefresh + 1 + + + protected + 1 + + + + Resizable + 1 + + + STD_BITMAP_BUTTON; widgets/std_bitmap_button.h; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + OnRegroupSymbols + + + + + + 5 + wxBOTTOM|wxEXPAND|wxLEFT|wxRIGHT|wxTOP + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + 0 + + + + 1 + + + wxALIGN_LEFT + + wxALIGN_CENTER + 0 + 1 + wxALIGN_CENTER + 24 + + wxALIGN_CENTER + 5 + + + 1 + 0 + Dock + 0 + Left + 0 + 1 + 1 + 0 + 0 + 1 + 1 + + 1 + + + 1 + 0 + 0 + wxID_ANY + + + + 0 + 0 + + 0 + + + 0 + 400,200 + 1 + m_grid + 1 + + + protected + 1 + + Resizable + wxALIGN_CENTER + 0 + + wxALIGN_CENTER + + 5 + 1 + + WX_GRID; widgets/wx_grid.h; forward_declare + 0 + + + + + OnTableValueChanged + OnTableCellClick + OnTableCellClick + OnTableItemContextMenu + OnTableCmdCellClick + OnTableCmdCellClick + OnTableColSize + OnEditorShown + OnTableSelectCell + + + + + + + + + + + 5 + wxEXPAND + 0 + + + bButtonsSizer + wxHORIZONTAL + none + + 5 + wxEXPAND + 9 + + 0 + protected + 0 + + + + 5 + wxALIGN_CENTER_VERTICAL|wxALL + 0 + + 1 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + + m_sdbSizer + protected + OnApply + OnCancel + OnOk + + + + + + + + diff --git a/eeschema/dialogs/dialog_lib_fields_base.h b/eeschema/dialogs/dialog_lib_fields_base.h new file mode 100644 index 0000000000..fad8df4ede --- /dev/null +++ b/eeschema/dialogs/dialog_lib_fields_base.h @@ -0,0 +1,92 @@ +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version 4.2.1-0-g80c4cb6a-dirty) +// http://www.wxformbuilder.org/ +// +// PLEASE DO *NOT* EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +class STD_BITMAP_BUTTON; +class WX_GRID; + +#include "dialog_shim.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////////////// +/// Class DIALOG_LIB_FIELDS_BASE +/////////////////////////////////////////////////////////////////////////////// +class DIALOG_LIB_FIELDS_BASE : public DIALOG_SHIM +{ + private: + + protected: + wxSplitterWindow* m_splitterMainWindow; + wxPanel* m_leftPanel; + wxDataViewListCtrl* m_fieldsCtrl; + STD_BITMAP_BUTTON* m_addFieldButton; + STD_BITMAP_BUTTON* m_renameFieldButton; + STD_BITMAP_BUTTON* m_removeFieldButton; + wxPanel* m_rightPanel; + wxSearchCtrl* m_filter; + wxStaticLine* m_staticline31; + STD_BITMAP_BUTTON* m_bRefresh; + WX_GRID* m_grid; + wxStdDialogButtonSizer* m_sdbSizer; + wxButton* m_sdbSizerOK; + wxButton* m_sdbSizerApply; + wxButton* m_sdbSizerCancel; + + // Virtual event handlers, override them in your derived class + virtual void OnClose( wxCloseEvent& event ) { event.Skip(); } + virtual void OnColumnItemToggled( wxDataViewEvent& event ) { event.Skip(); } + virtual void OnFieldsCtrlSelectionChanged( wxDataViewEvent& event ) { event.Skip(); } + virtual void OnSizeFieldList( wxSizeEvent& event ) { event.Skip(); } + virtual void OnAddField( wxCommandEvent& event ) { event.Skip(); } + virtual void OnRenameField( wxCommandEvent& event ) { event.Skip(); } + virtual void OnRemoveField( wxCommandEvent& event ) { event.Skip(); } + virtual void OnFilterMouseMoved( wxMouseEvent& event ) { event.Skip(); } + virtual void OnFilterText( wxCommandEvent& event ) { event.Skip(); } + virtual void OnRegroupSymbols( wxCommandEvent& event ) { event.Skip(); } + virtual void OnTableValueChanged( wxGridEvent& event ) { event.Skip(); } + virtual void OnTableCellClick( wxGridEvent& event ) { event.Skip(); } + virtual void OnTableItemContextMenu( wxGridEvent& event ) { event.Skip(); } + virtual void OnTableCmdCellClick( wxGridEvent& event ) { event.Skip(); } + virtual void OnTableColSize( wxGridSizeEvent& event ) { event.Skip(); } + virtual void OnEditorShown( wxGridEvent& event ) { event.Skip(); } + virtual void OnTableSelectCell( wxGridEvent& event ) { event.Skip(); } + virtual void OnApply( wxCommandEvent& event ) { event.Skip(); } + virtual void OnCancel( wxCommandEvent& event ) { event.Skip(); } + virtual void OnOk( wxCommandEvent& event ) { event.Skip(); } + + + public: + + DIALOG_LIB_FIELDS_BASE( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("Library Fields Editor"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( -1,-1 ), long style = wxDEFAULT_DIALOG_STYLE|wxMAXIMIZE_BOX|wxRESIZE_BORDER ); + + ~DIALOG_LIB_FIELDS_BASE(); + +}; + diff --git a/eeschema/lib_fields_data_model.cpp b/eeschema/lib_fields_data_model.cpp new file mode 100644 index 0000000000..96d73046e5 --- /dev/null +++ b/eeschema/lib_fields_data_model.cpp @@ -0,0 +1,1112 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 + * Copyright (C) 2023 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 . + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "string_utils.h" +#include + +#include "lib_fields_data_model.h" + + +const wxString LIB_FIELDS_EDITOR_GRID_DATA_MODEL::ITEM_NUMBER_VARIABLE = wxS( "${ITEM_NUMBER}" ); + + +void LIB_FIELDS_EDITOR_GRID_DATA_MODEL::AddColumn( const wxString& aFieldName, const wxString& aLabel, + bool aAddedByUser, bool aIsCheckbox ) +{ + // Don't add a field twice + if( GetFieldNameCol( aFieldName ) != -1 ) + return; + + m_cols.emplace_back( aFieldName, aLabel, aAddedByUser, false, false, aIsCheckbox ); + + for( LIB_SYMBOL* symbol : m_symbolsList ) + updateDataStoreSymbolField( symbol, aFieldName ); +} + + +void LIB_FIELDS_EDITOR_GRID_DATA_MODEL::updateDataStoreSymbolField( const LIB_SYMBOL* aSymbol, + const wxString& aFieldName ) +{ + int col = GetFieldNameCol( aFieldName ); + LIB_DATA_ELEMENT& dataElement = m_dataStore[aSymbol->m_Uuid][aFieldName]; + + if( col != -1 && ColIsCheck( col ) ) + { + dataElement.m_originalData = getAttributeValue( aSymbol, aFieldName ); + dataElement.m_currentData = getAttributeValue( aSymbol, aFieldName ); + dataElement.m_originallyEmpty = false; + dataElement.m_currentlyEmpty = false; + dataElement.m_isModified = false; + } + else if( const SCH_FIELD* field = aSymbol->GetField( aFieldName ) ) + { + dataElement.m_originalData = field->GetText(); + dataElement.m_currentData = field->GetText(); + dataElement.m_originallyEmpty = false; + dataElement.m_currentlyEmpty = false; + dataElement.m_isModified = false; + } + else + { + m_dataStore[aSymbol->m_Uuid][aFieldName].m_originalData = wxEmptyString; + m_dataStore[aSymbol->m_Uuid][aFieldName].m_currentData = wxEmptyString; + m_dataStore[aSymbol->m_Uuid][aFieldName].m_originallyEmpty = true; + m_dataStore[aSymbol->m_Uuid][aFieldName].m_currentlyEmpty = true; + m_dataStore[aSymbol->m_Uuid][aFieldName].m_isModified = false; + } +} + + +void LIB_FIELDS_EDITOR_GRID_DATA_MODEL::RemoveColumn( int aCol ) +{ + const wxString fieldName = m_cols[aCol].m_fieldName; + + for( LIB_SYMBOL* symbol : m_symbolsList ) + { + std::map& fieldStore = m_dataStore[symbol->m_Uuid]; + auto it = fieldStore.find( fieldName ); + if( it != fieldStore.end() ) + { + it->second.m_currentData = wxEmptyString; + it->second.m_currentlyEmpty = true; + it->second.m_isModified = true; + fieldStore.erase( it ); + } + } + + m_cols.erase( m_cols.begin() + aCol ); + m_edited = true; +} + + +void LIB_FIELDS_EDITOR_GRID_DATA_MODEL::RenameColumn( int aCol, const wxString& newName ) +{ + for( LIB_SYMBOL* symbol : m_symbolsList ) + { + // Careful; field may have already been renamed from another sheet instance + if( auto node = m_dataStore[symbol->m_Uuid].extract( m_cols[aCol].m_fieldName ) ) + { + node.key() = newName; + node.mapped().m_isModified = true; + m_dataStore[symbol->m_Uuid].insert( std::move( node ) ); + } + } + + m_cols[aCol].m_fieldName = newName; + m_cols[aCol].m_label = newName; + m_edited = true; +} + + +int LIB_FIELDS_EDITOR_GRID_DATA_MODEL::GetFieldNameCol( wxString aFieldName ) +{ + for( size_t i = 0; i < m_cols.size(); i++ ) + { + if( m_cols[i].m_fieldName == aFieldName ) + return (int) i; + } + + return -1; +} + + +void LIB_FIELDS_EDITOR_GRID_DATA_MODEL::SetFieldsOrder( const std::vector& aNewOrder ) +{ + size_t foundCount = 0; + + if( aNewOrder.size() > m_cols.size() ) + wxLogDebug( "New order contains more fields than existing columns." ); + + for( const wxString& newField : aNewOrder ) + { + bool found = false; + for( size_t i = 0; i < m_cols.size() && foundCount < m_cols.size(); i++ ) + { + if( m_cols[i].m_fieldName == newField ) + { + std::swap( m_cols[foundCount], m_cols[i] ); + foundCount++; + found = true; + break; + } + } + + if( !found ) + wxLogDebug( "Field '%s' not found in existing columns.", newField ); + } + + if( foundCount != m_cols.size() && foundCount != aNewOrder.size() ) + { + wxLogDebug( "Not all fields in the new order were found in the existing columns." ); + } +} + + +wxString LIB_FIELDS_EDITOR_GRID_DATA_MODEL::GetValue( int aRow, int aCol ) +{ + // Check if aCol is the first visible column + bool isFirstVisible = true; + + for( int col = 0; col < aCol; ++col ) + { + if( m_cols[col].m_show ) + { + isFirstVisible = false; + break; + } + } + + GetView()->SetReadOnly( aRow, aCol, false ); + + if( isFirstVisible ) + { + if( m_rows[aRow].m_Flag == GROUP_COLLAPSED ) + { + GetView()->SetReadOnly( aRow, aCol, true ); + return wxT( "> " ) + GetValue( m_rows[aRow], aCol ); + } + else if( m_rows[aRow].m_Flag == GROUP_EXPANDED ) + { + GetView()->SetReadOnly( aRow, aCol, true ); + return wxT( "v " ) + GetValue( m_rows[aRow], aCol ); + } + else if( m_rows[aRow].m_Flag == CHILD_ITEM ) + { + return wxT( " " ) + GetValue( m_rows[aRow], aCol ); + } + else + { + return wxT( " " ) + GetValue( m_rows[aRow], aCol ); + } + } + + return GetValue(m_rows[aRow], aCol); +} + + +wxString LIB_FIELDS_EDITOR_GRID_DATA_MODEL::GetValue( const LIB_DATA_MODEL_ROW& group, int aCol ) +{ + wxString fieldValue = INDETERMINATE_STATE; + wxCHECK( aCol >= 0 && aCol < (int) m_cols.size(), fieldValue ); + + LIB_DATA_MODEL_COL& col = m_cols[aCol]; + + for( const LIB_SYMBOL* ref : group.m_Refs ) + { + const KIID& symbolID = ref->m_Uuid; + + if( !m_dataStore.contains( symbolID ) + || !m_dataStore[symbolID].contains( col.m_fieldName ) ) + { + return INDETERMINATE_STATE; + } + + wxString refFieldValue = m_dataStore[symbolID][col.m_fieldName].m_currentData; + + if( ref == group.m_Refs.front() ) + fieldValue = refFieldValue; + else if( fieldValue != refFieldValue ) + return INDETERMINATE_STATE; + } + + return fieldValue; +} + + +void LIB_FIELDS_EDITOR_GRID_DATA_MODEL::SetValue( int aRow, int aCol, const wxString& aValue ) +{ + wxCHECK_RET( aCol >= 0 && aCol < (int) m_cols.size(), wxS( "Invalid column number" ) ); + + LIB_DATA_MODEL_ROW& rowGroup = m_rows[aRow]; + + for( const LIB_SYMBOL* ref : rowGroup.m_Refs ) + { + LIB_DATA_ELEMENT& dataElement = m_dataStore[ref->m_Uuid][m_cols[aCol].m_fieldName]; + dataElement.m_currentData = aValue; + dataElement.m_isModified = ( dataElement.m_currentData != dataElement.m_originalData ); + dataElement.m_currentlyEmpty = false; + } + + m_edited = true; +} + +wxGridCellAttr* LIB_FIELDS_EDITOR_GRID_DATA_MODEL::GetAttr( int aRow, int aCol, wxGridCellAttr::wxAttrKind aKind ) +{ + wxGridCellAttr* attr = wxGridTableBase::GetAttr( aRow, aCol, aKind ); + + if( !attr ) + attr = new wxGridCellAttr; + + bool rowModified = false; + bool cellModified = false; + bool cellEmpty = true; + bool blankModified = false; + + const wxString& fieldName = m_cols[aCol].m_fieldName; + + for( const LIB_SYMBOL* ref : m_rows[aRow].m_Refs ) + { + LIB_DATA_ELEMENT& element = m_dataStore[ref->m_Uuid][fieldName]; + + if( element.m_isModified ) + cellModified = true; + + bool elementEmpty = element.m_currentlyEmpty + || ( element.m_originallyEmpty && !element.m_isModified ); + + if( !elementEmpty ) + cellEmpty = false; + + if( element.m_currentData.IsEmpty() && element.m_isModified ) + blankModified = true; + + if( !rowModified ) + { + for( const LIB_DATA_MODEL_COL& col : m_cols ) + { + if( m_dataStore[ref->m_Uuid][col.m_fieldName].m_isModified ) + { + rowModified = true; + break; + } + } + } + + if( cellModified && rowModified && !cellEmpty ) + break; + } + + // Apply striped renderer for appropriate empty cells + if( cellEmpty && isStripeableField( aCol ) ) + { + wxGridCellRenderer* stripedRenderer = getStripedRenderer( aCol ); + + if( stripedRenderer ) + { + attr->SetRenderer( stripedRenderer ); + attr->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + for( const LIB_SYMBOL* ref : m_rows[aRow].m_Refs ) + { + if( m_dataStore[ref->m_Uuid][fieldName].m_isModified ) + { + m_dataStore[ref->m_Uuid][fieldName].m_isStriped = true; + + if( m_dataStore[ref->m_Uuid][fieldName].m_currentlyEmpty ) + { + if( m_dataStore[ref->m_Uuid][fieldName].m_originallyEmpty ) + { + attr->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + } + else if( m_dataStore[ref->m_Uuid][fieldName].m_originalData.empty() ) + { + attr->SetBackgroundColour( wxColour( 180, 220, 180 ) ); + } + else + { + attr->SetBackgroundColour( wxColour( 220, 180, 180 ) ); + } + } + else if( m_dataStore[ref->m_Uuid][fieldName].m_currentData.IsEmpty() ) + { + attr->SetBackgroundColour( wxColour( 180, 200, 180 ) ); + } + else + { + attr->SetBackgroundColour( wxColour( 200, 180, 180 ) ); + } + } + } + } + } + else + { + bool wasStriped = false; + + for( const LIB_SYMBOL* ref : m_rows[aRow].m_Refs ) + { + if( m_dataStore[ref->m_Uuid][fieldName].m_isStriped ) + { + wasStriped = true; + m_dataStore[ref->m_Uuid][fieldName].m_isStriped = false; + } + } + + // If the cell was previously striped, we need to reset the attribute + if( wasStriped ) + attr = new wxGridCellAttr; + + if( rowModified ) + attr->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOWFRAME ) ); + + if( blankModified ) + attr->SetBackgroundColour( wxColour( 192, 255, 192 ) ); + } + + if( cellModified ) + { + wxFont font; + if( attr->HasFont() ) + { + font = attr->GetFont(); + } + else if( GetView() ) + { + font = GetView()->GetDefaultCellFont(); + } + else + { + font = wxFont(); + } + + if( font.IsOk() ) + { + font.MakeBold(); + attr->SetFont( font ); + } + } + + return attr; +} + + +void LIB_FIELDS_EDITOR_GRID_DATA_MODEL::CreateDerivedSymbol( int aRow, int aCol, wxString& aNewSymbolName ) +{ + wxCHECK_RET( aRow >= 0 && aRow < (int) m_rows.size(), "Invalid Row Number" ); + wxCHECK_RET( aCol >= 0 && aCol < (int) m_cols.size(), "Invalid Column Number" ); + + const LIB_SYMBOL* parentSymbol = m_rows[aRow].m_Refs[0]; + const wxString& fieldName = m_cols[aCol].m_fieldName; + + std::map& parentFieldStore = m_dataStore[parentSymbol->m_Uuid]; + + // Use a special field name that won't conflict with real fields + wxString derivedSymbolFieldName = "__DERIVED_SYMBOL_" + fieldName + "__"; + + // Store derived symbol creation data under special field name so ApplyData can find it + LIB_DATA_ELEMENT& targetElement = parentFieldStore[derivedSymbolFieldName]; + targetElement.m_createDerivedSymbol = true; + targetElement.m_derivedSymbolName = aNewSymbolName; + targetElement.m_currentData = aNewSymbolName; + targetElement.m_isModified = true; + targetElement.m_originalData = parentSymbol->m_Uuid.AsString(); + + wxLogTrace( traceLibFieldTable, "CreateDerivedSymbol: Parent symbol name='%s', UUID='%s'", + parentSymbol->GetName(), parentSymbol->m_Uuid.AsString() ); + wxLogTrace( traceLibFieldTable, "CreateDerivedSymbol: Stored creation request for symbol '%s' under parent UUID %s, special field '%s'", + aNewSymbolName, parentSymbol->m_Uuid.AsString(), derivedSymbolFieldName ); + + m_edited = true; +} + +void LIB_FIELDS_EDITOR_GRID_DATA_MODEL::CreateDerivedSymbolImmediate( int aRow, int aCol, wxString& aNewSymbolName ) +{ + wxCHECK_RET( aRow >= 0 && aRow < (int) m_rows.size(), "Invalid Row Number" ); + wxCHECK_RET( aCol >= 0 && aCol < (int) m_cols.size(), "Invalid Column Number" ); + + const LIB_SYMBOL* parentSymbol = m_rows[aRow].m_Refs[0]; + + wxLogTrace( traceLibFieldTable, "CreateDerivedSymbolImmediate: Creating '%s' from parent '%s' immediately", + aNewSymbolName, parentSymbol->GetName() ); + + // Generate a fresh UUID for the new derived symbol + KIID newDerivedSymbolUuid; + + // Create the symbol immediately + createActualDerivedSymbol( parentSymbol, aNewSymbolName, newDerivedSymbolUuid ); + + // Rebuild the grid to show the new symbol + RebuildRows(); + + m_edited = true; +} + +void LIB_FIELDS_EDITOR_GRID_DATA_MODEL::createActualDerivedSymbol( const LIB_SYMBOL* aParentSymbol, const wxString& aNewSymbolName, const KIID& aNewSymbolUuid ) +{ + wxLogTrace( traceLibFieldTable, "createActualDerivedSymbol: Creating '%s' from parent '%s', symbol list size before: %zu", + aNewSymbolName, aParentSymbol->GetName(), m_symbolsList.size() ); + + LIB_SYMBOL* newSymbol = nullptr; + + for( LIB_SYMBOL* sym : m_symbolsList ) + { + if( sym->m_Uuid == aNewSymbolUuid ) + { + newSymbol = sym; + break; + } + } + + if( !newSymbol ) + { + newSymbol = new LIB_SYMBOL( *aParentSymbol ); + newSymbol->SetName( aNewSymbolName ); + + // Also update the VALUE field to reflect the new name for derived symbols + newSymbol->GetValueField().SetText( aNewSymbolName ); + + newSymbol->SetParent( const_cast( aParentSymbol ) ); + // Note: SetLib() not called here - library association handled by dialog's library manager + const_cast( newSymbol->m_Uuid ) = aNewSymbolUuid; + m_symbolsList.push_back( newSymbol ); + + wxLogTrace( traceLibFieldTable, "createActualDerivedSymbol: Added new symbol to list, size now: %zu", m_symbolsList.size() ); + + // Initialize field data for the new symbol in the data store + for( const auto& col : m_cols ) + { + updateDataStoreSymbolField( newSymbol, col.m_fieldName ); + } + + wxLogTrace( traceLibFieldTable, "createActualDerivedSymbol: Initialized field data for new symbol" ); + } + + // Note: Not adding to symbolLibrary directly - this will be handled by the dialog's library manager integration + wxString libraryName = aParentSymbol->GetLibId().GetLibNickname(); + m_createdDerivedSymbols.emplace_back( newSymbol, libraryName ); + + wxLogTrace( traceLibFieldTable, "Created derived symbol '%s' for library '%s', total tracked: %zu", + aNewSymbolName, libraryName, m_createdDerivedSymbols.size() ); +} + +void LIB_FIELDS_EDITOR_GRID_DATA_MODEL::RevertRow( int aRow ) +{ + LIB_DATA_MODEL_ROW& rowGroup = m_rows[aRow]; + + for( const LIB_SYMBOL* ref : rowGroup.m_Refs ) + { + auto& fieldStore = m_dataStore[ref->m_Uuid]; + + for( auto& [name, element] : fieldStore ) + { + element.m_currentData = element.m_originalData; + element.m_isModified = false; + element.m_currentlyEmpty = false; + } + } + + m_edited = false; + + for( const auto& [symId, fieldStore] : m_dataStore ) + { + for( const auto& [name, element] : fieldStore ) + { + if( element.m_isModified ) + { + m_edited = true; + return; + } + } + } +} + + +void LIB_FIELDS_EDITOR_GRID_DATA_MODEL::ClearCell( int aRow, int aCol ) +{ + wxCHECK_RET( aCol >= 0 && aCol < (int) m_cols.size(), wxS( "Invalid column number" ) ); + + LIB_DATA_MODEL_ROW& rowGroup = m_rows[aRow]; + + for( const LIB_SYMBOL* ref : rowGroup.m_Refs ) + { + LIB_DATA_ELEMENT& dataElement = m_dataStore[ref->m_Uuid][m_cols[aCol].m_fieldName]; + dataElement.m_currentData = wxEmptyString; + dataElement.m_currentlyEmpty = true; + dataElement.m_isModified = ( dataElement.m_currentData != dataElement.m_originalData ) + || ( dataElement.m_currentlyEmpty != dataElement.m_originallyEmpty ); + } + + m_edited = true; +} + + +bool LIB_FIELDS_EDITOR_GRID_DATA_MODEL::ColIsValue( int aCol ) +{ + wxCHECK( aCol >= 0 && aCol < static_cast( m_cols.size() ), false ); + return m_cols[aCol].m_fieldName == GetCanonicalFieldName( FIELD_T::VALUE ); +} + + +bool LIB_FIELDS_EDITOR_GRID_DATA_MODEL::ColIsCheck( int aCol ) +{ + wxCHECK( aCol >= 0 && aCol < static_cast( m_cols.size() ), false ); + return m_cols[aCol].m_isCheckbox; +} + + +bool LIB_FIELDS_EDITOR_GRID_DATA_MODEL::cmp( const LIB_DATA_MODEL_ROW& lhGroup, + const LIB_DATA_MODEL_ROW& rhGroup, + LIB_FIELDS_EDITOR_GRID_DATA_MODEL* dataModel, int sortCol, + bool ascending ) +{ + // Empty rows always go to the bottom, whether ascending or descending + if( lhGroup.m_Refs.size() == 0 ) + return true; + else if( rhGroup.m_Refs.size() == 0 ) + return false; + + // N.B. To meet the iterator sort conditions, we cannot simply invert the truth + // to get the opposite sort. i.e. ~(ab) + auto local_cmp = + [ ascending ]( const auto a, const auto b ) + { + if( ascending ) + return a < b; + else + return a > b; + }; + + // Primary sort key is sortCol; secondary is always VALUE (column 1) + wxString lhs = dataModel->GetValue( lhGroup, sortCol ).Trim( true ).Trim( false ); + wxString rhs = dataModel->GetValue( rhGroup, sortCol ).Trim( true ).Trim( false ); + + if( lhs == rhs && lhGroup.m_Refs.size() > 1 && rhGroup.m_Refs.size() > 1 ) + { + wxString lhRef = lhGroup.m_Refs[1]->GetRef( nullptr ); + wxString rhRef = rhGroup.m_Refs[1]->GetRef( nullptr ); + return local_cmp( lhRef, rhRef ); + } + else + { + return local_cmp( lhs, rhs ); + } +} + + +void LIB_FIELDS_EDITOR_GRID_DATA_MODEL::Sort() +{ + CollapseForSort(); + + // We're going to sort the rows based on their first reference, so the first reference + // had better be the lowest one. + for( LIB_DATA_MODEL_ROW& row : m_rows ) + { + std::sort( row.m_Refs.begin(), row.m_Refs.end(), + []( const LIB_SYMBOL* lhs, const LIB_SYMBOL* rhs ) + { + wxString lhs_ref( lhs->GetRef( nullptr ) ); + wxString rhs_ref( rhs->GetRef( nullptr ) ); + return StrNumCmp( lhs_ref, rhs_ref, true ) < 0; + } ); + } + + std::sort( m_rows.begin(), m_rows.end(), + [this]( const LIB_DATA_MODEL_ROW& lhs, const LIB_DATA_MODEL_ROW& rhs ) -> bool + { + return cmp( lhs, rhs, this, m_sortColumn, m_sortAscending ); + } ); + + // Time to renumber the item numbers + int itemNumber = 1; + + for( LIB_DATA_MODEL_ROW& row : m_rows ) + { + row.m_ItemNumber = itemNumber++; + } + + ExpandAfterSort(); +} + + +bool LIB_FIELDS_EDITOR_GRID_DATA_MODEL::groupMatch( const LIB_SYMBOL* lhRef, const LIB_SYMBOL* rhRef ) + +{ + bool matchFound = false; + const KIID& lhRefID = lhRef->m_Uuid; + const KIID& rhRefID = rhRef->m_Uuid; + + // Now check all the other columns. + for( size_t i = 0; i < m_cols.size(); ++i ) + { + const LIB_DATA_MODEL_COL& col = m_cols[i]; + + if( !col.m_group ) + continue; + + if( m_dataStore[lhRefID][col.m_fieldName].m_currentData != m_dataStore[rhRefID][col.m_fieldName].m_currentData ) + return false; + + matchFound = true; + } + + return matchFound; +} + + +wxString LIB_FIELDS_EDITOR_GRID_DATA_MODEL::getAttributeValue( const LIB_SYMBOL* aSymbol, + const wxString& aAttributeName ) +{ + if( aAttributeName == wxS( "${DNP}" ) ) + return aSymbol->GetDNP() ? wxS( "1" ) : wxS( "0" ); + + if( aAttributeName == wxS( "${EXCLUDE_FROM_BOARD}" ) ) + return aSymbol->GetExcludedFromBoard() ? wxS( "1" ) : wxS( "0" ); + + if( aAttributeName == wxS( "${EXCLUDE_FROM_BOM}" ) ) + return aSymbol->GetExcludedFromBOM() ? wxS( "1" ) : wxS( "0" ); + + if( aAttributeName == wxS( "${EXCLUDE_FROM_SIM}" ) ) + return aSymbol->GetExcludedFromSim() ? wxS( "1" ) : wxS( "0" ); + + if( aAttributeName == wxS( "Power" ) ) + return aSymbol->IsPower() ? wxS( "1" ) : wxS( "0" ); + + if( aAttributeName == wxS( "LocalPower" ) ) + return aSymbol->IsLocalPower() ? wxS( "1" ) : wxS( "0" ); + + + return wxS( "0" ); +} + +void LIB_FIELDS_EDITOR_GRID_DATA_MODEL::setAttributeValue( LIB_SYMBOL* aSymbol, + const wxString& aAttributeName, + const wxString& aValue ) +{ + if( aAttributeName == wxS( "${DNP}" ) ) + aSymbol->SetDNP( aValue == wxS( "1" ) ); + else if( aAttributeName == wxS( "${EXCLUDE_FROM_BOARD}" ) ) + aSymbol->SetExcludedFromBoard( aValue == wxS( "1" ) ); + else if( aAttributeName == wxS( "${EXCLUDE_FROM_BOM}" ) ) + aSymbol->SetExcludedFromBOM( aValue == wxS( "1" ) ); + else if( aAttributeName == wxS( "${EXCLUDE_FROM_SIM}" ) ) + aSymbol->SetExcludedFromSim( aValue == wxS( "1" ) ); + else if( aAttributeName == wxS( "LocalPower" ) ) + { + // Turning off local power still leaves the global flag set + if( aValue == wxS( "0" ) ) + aSymbol->SetGlobalPower(); + else + aSymbol->SetLocalPower(); + } + else if( aAttributeName == wxS( "Power" ) ) + { + if( aValue == wxS( "0" ) ) + aSymbol->SetNormal(); + else + aSymbol->SetGlobalPower(); + } + else + wxLogDebug( "Unknown attribute name: %s", aAttributeName ); +} + + +void LIB_FIELDS_EDITOR_GRID_DATA_MODEL::RebuildRows() +{ + wxLogTrace( traceLibFieldTable, "RebuildRows: Starting rebuild with %zu symbols in list", m_symbolsList.size() ); + + if( GetView() ) + { + // Commit any pending in-place edits before the row gets moved out from under + // the editor. + static_cast( GetView() )->CommitPendingChanges( true ); + + wxGridTableMessage msg( this, wxGRIDTABLE_NOTIFY_ROWS_DELETED, 0, m_rows.size() ); + GetView()->ProcessTableMessage( msg ); + } + + m_rows.clear(); + + wxLogTrace( traceLibFieldTable, "RebuildRows: About to process %zu symbols", m_symbolsList.size() ); + + for( LIB_SYMBOL* ref : m_symbolsList ) + { + wxLogTrace( traceLibFieldTable, "RebuildRows: Processing symbol '%s' (UUID: %s)", + ref->GetName(), ref->m_Uuid.AsString() ); + + if( !m_filter.IsEmpty() && !WildCompareString( m_filter, ref->GetValue( false, nullptr, false ), false ) ) + continue; + + bool matchFound = false; + + // Performance optimization for ungrouped case to skip the N^2 for loop + if( !m_groupingEnabled ) + { + m_rows.emplace_back( LIB_DATA_MODEL_ROW( ref, GROUP_SINGLETON ) ); + continue; + } + + // See if we already have a row which this symbol fits into + for( LIB_DATA_MODEL_ROW& row : m_rows ) + { + // all group members must have identical refs so just use the first one + const LIB_SYMBOL* rowRef = row.m_Refs[0]; + + if( m_groupingEnabled && groupMatch( ref, rowRef ) ) + { + matchFound = true; + row.m_Refs.push_back( ref ); + row.m_Flag = GROUP_COLLAPSED; + break; + } + } + + if( !matchFound ) + m_rows.emplace_back( LIB_DATA_MODEL_ROW( ref, GROUP_SINGLETON ) ); + } + + if( GetView() ) + { + wxGridTableMessage msg( this, wxGRIDTABLE_NOTIFY_ROWS_APPENDED, m_rows.size() ); + GetView()->ProcessTableMessage( msg ); + } + + wxLogTrace( traceLibFieldTable, "RebuildRows: Completed rebuild with %zu rows created", m_rows.size() ); + Sort(); +} + + +void LIB_FIELDS_EDITOR_GRID_DATA_MODEL::ExpandRow( int aRow ) +{ + std::vector children; + + for( const LIB_SYMBOL* ref : m_rows[aRow].m_Refs ) + { + bool matchFound = false; + + if( !matchFound ) + children.emplace_back( LIB_DATA_MODEL_ROW( ref, CHILD_ITEM ) ); + } + + if( children.size() < 2 ) + return; + + std::sort( children.begin(), children.end(), + [this]( const LIB_DATA_MODEL_ROW& lhs, const LIB_DATA_MODEL_ROW& rhs ) -> bool + { + return cmp( lhs, rhs, this, m_sortColumn, m_sortAscending ); + } ); + + m_rows[aRow].m_Flag = GROUP_EXPANDED; + m_rows.insert( m_rows.begin() + aRow + 1, children.begin(), children.end() ); + + wxGridTableMessage msg( this, wxGRIDTABLE_NOTIFY_ROWS_INSERTED, aRow, children.size() ); + GetView()->ProcessTableMessage( msg ); +} + + +void LIB_FIELDS_EDITOR_GRID_DATA_MODEL::CollapseRow( int aRow ) +{ + auto firstChild = m_rows.begin() + aRow + 1; + auto afterLastChild = firstChild; + int deleted = 0; + + while( afterLastChild != m_rows.end() && afterLastChild->m_Flag == CHILD_ITEM ) + { + deleted++; + afterLastChild++; + } + + m_rows[aRow].m_Flag = GROUP_COLLAPSED; + m_rows.erase( firstChild, afterLastChild ); + + wxGridTableMessage msg( this, wxGRIDTABLE_NOTIFY_ROWS_DELETED, aRow + 1, deleted ); + GetView()->ProcessTableMessage( msg ); +} + + +void LIB_FIELDS_EDITOR_GRID_DATA_MODEL::ExpandCollapseRow( int aRow ) +{ + LIB_DATA_MODEL_ROW& group = m_rows[aRow]; + + if( group.m_Flag == GROUP_COLLAPSED ) + ExpandRow( aRow ); + else if( group.m_Flag == GROUP_EXPANDED ) + CollapseRow( aRow ); +} + + +void LIB_FIELDS_EDITOR_GRID_DATA_MODEL::CollapseForSort() +{ + for( size_t i = 0; i < m_rows.size(); ++i ) + { + if( m_rows[i].m_Flag == GROUP_EXPANDED ) + { + CollapseRow( i ); + m_rows[i].m_Flag = GROUP_COLLAPSED_DURING_SORT; + } + } +} + + +void LIB_FIELDS_EDITOR_GRID_DATA_MODEL::ExpandAfterSort() +{ + for( size_t i = 0; i < m_rows.size(); ++i ) + { + if( m_rows[i].m_Flag == GROUP_COLLAPSED_DURING_SORT ) + ExpandRow( i ); + } +} + + +void LIB_FIELDS_EDITOR_GRID_DATA_MODEL::ApplyData( + std::function symbolChangeHandler, + std::function postApplyHandler ) +{ + for( LIB_SYMBOL* symbol : m_symbolsList ) + { + symbolChangeHandler( symbol ); + + std::map& fieldStore = m_dataStore[symbol->m_Uuid]; + + for( auto& srcData : fieldStore ) + { + const wxString& srcName = srcData.first; + LIB_DATA_ELEMENT& dataElement = srcData.second; + const wxString& srcValue = dataElement.m_currentData; + int col = GetFieldNameCol( srcName ); + + // Attributes bypass the field logic, so handle them first + if( col != -1 && ColIsCheck( col ) ) + { + setAttributeValue( symbol, srcName, srcValue ); + continue; + } + + // Skip special fields with variables as names (e.g. ${QUANTITY}), + // they can't be edited + if( IsGeneratedField( srcName ) ) + continue; + + // Skip special derived symbol creation fields - these are handled separately + if( srcName.StartsWith( "__DERIVED_SYMBOL_" ) && srcName.EndsWith( "__" ) ) + continue; + + SCH_FIELD* destField = symbol->GetField( srcName ); + bool userAdded = ( col != -1 && m_cols[col].m_userAdded ); + + // Add a not existing field if it has a value for this symbol + bool createField = !destField && ( !srcValue.IsEmpty() || userAdded ); + + if( createField ) + { + const VECTOR2I symbolPos = symbol->GetPosition(); + destField = new SCH_FIELD( symbol, FIELD_T::USER, srcName ); + destField->SetPosition( symbolPos ); + symbol->AddField( destField ); + } + + if( !destField ) + continue; + + if( destField->GetId() == FIELD_T::REFERENCE ) + { + // Reference is not editable from this dialog + } + else if( destField->GetId() == FIELD_T::VALUE ) + { + // Value field cannot be empty + if( !srcValue.IsEmpty() ) + symbol->GetField( FIELD_T::VALUE )->SetText( srcValue ); + } + else if( destField->GetId() == FIELD_T::FOOTPRINT ) + { + symbol->GetField(FIELD_T::FOOTPRINT)->SetText( srcValue ); + } + else + { + destField->SetText( srcValue ); + } + + dataElement.m_originalData = dataElement.m_currentData; + dataElement.m_isModified = false; + dataElement.m_currentlyEmpty = false; + dataElement.m_originallyEmpty = dataElement.m_currentlyEmpty; + } + + std::vector symbolFields; + symbol->GetFields( symbolFields ); + + // Remove any fields that are not mandatory + for( SCH_FIELD* field : symbolFields ) + { + if( field->IsMandatory() ) + continue; + + // Remove any fields that are not in the fieldStore + if( !fieldStore.contains( field->GetName() ) ) + { + symbol->RemoveField( field ); + delete field; + } + } + } + + // Process derived symbol creation requests + for( auto& [symId, fieldStore] : m_dataStore ) + { + for( auto& [fieldName, element] : fieldStore ) + { + if( element.m_createDerivedSymbol ) + { + const LIB_SYMBOL* parentSymbol = nullptr; + + // First try to interpret as UUID + try + { + KIID parentUuid( element.m_originalData ); + + for( const LIB_SYMBOL* sym : m_symbolsList ) + { + if( sym->m_Uuid == parentUuid ) + { + parentSymbol = sym; + break; + } + } + } + catch( ... ) + { + // Not a valid UUID, try looking up by symbol name + } + + // If UUID lookup failed, try looking up by symbol name + if( !parentSymbol ) + { + for( const LIB_SYMBOL* sym : m_symbolsList ) + { + if( sym->GetName() == element.m_originalData ) + { + parentSymbol = sym; + break; + } + } + } + + if( parentSymbol ) + { + wxString actualDerivedName = element.m_derivedSymbolName; + + // If the derived name is the same as the parent name, auto-generate a unique name + if( actualDerivedName == parentSymbol->GetName() ) + { + // Try common variant patterns first + actualDerivedName = parentSymbol->GetName() + "_1"; + + // If that exists, try incrementing the number + int variant = 2; + bool nameExists = true; + while( nameExists && variant < 100 ) + { + nameExists = false; + for( const LIB_SYMBOL* sym : m_symbolsList ) + { + if( sym->GetName() == actualDerivedName ) + { + nameExists = true; + break; + } + } + if( nameExists ) + { + actualDerivedName = parentSymbol->GetName() + "_" + wxString::Format( "%d", variant ); + variant++; + } + } + } + + // Generate a fresh UUID for the new derived symbol (don't reuse symId which is for an existing symbol) + KIID newDerivedSymbolUuid; + + createActualDerivedSymbol( parentSymbol, actualDerivedName, newDerivedSymbolUuid ); + } + + element.m_createDerivedSymbol = false; + break; + } + } + } + + m_edited = false; + + // Call post-apply handler if provided (for library operations and tree refresh) + if( postApplyHandler ) + postApplyHandler(); +} + + +int LIB_FIELDS_EDITOR_GRID_DATA_MODEL::GetDataWidth( int aCol ) +{ + int width = 0; + wxString fieldName = GetColFieldName( aCol ); // symbol fieldName or Qty string + + for( const LIB_SYMBOL* symbol : m_symbolsList ) + { + LIB_DATA_ELEMENT& text = m_dataStore[symbol->m_Uuid][fieldName]; + + width = std::max( width, KIUI::GetTextSize( text.m_currentData, GetView() ).x ); + } + + return width; +} + + +wxString LIB_FIELDS_EDITOR_GRID_DATA_MODEL::GetTypeName( int row, int col ) +{ + if( ColIsCheck( col ) ) + return wxGRID_VALUE_BOOL; + + return wxGridTableBase::GetTypeName( row, col ); +} + + +wxGridCellRenderer* LIB_FIELDS_EDITOR_GRID_DATA_MODEL::getStripedRenderer( int aCol ) const +{ + wxCHECK( aCol >= 0 && aCol < (int) m_cols.size(), nullptr ); + + const wxString& fieldName = m_cols[aCol].m_fieldName; + + // Check if we already have a striped renderer for this field type + auto it = m_stripedRenderers.find( fieldName ); + if( it != m_stripedRenderers.end() ) + { + it->second->IncRef(); + return it->second; + } + + wxGridCellRenderer* stripedRenderer = nullptr; + // Default to striped string renderer + stripedRenderer = new STRIPED_STRING_RENDERER(); + + // Cache the renderer for future use - the cache owns one reference + stripedRenderer->IncRef(); + m_stripedRenderers[fieldName] = stripedRenderer; + + // Return with IncRef for the caller (SetRenderer will consume this reference) + stripedRenderer->IncRef(); + return stripedRenderer; +} + +// lib_fields_data_model.cpp - Add the isStripeableField method +bool LIB_FIELDS_EDITOR_GRID_DATA_MODEL::isStripeableField( int aCol ) +{ + wxCHECK( aCol >= 0 && aCol < (int) m_cols.size(), false ); + + // Don't apply stripes to checkbox fields + return !ColIsCheck( aCol ); +} \ No newline at end of file diff --git a/eeschema/lib_fields_data_model.h b/eeschema/lib_fields_data_model.h new file mode 100644 index 0000000000..05af86880a --- /dev/null +++ b/eeschema/lib_fields_data_model.h @@ -0,0 +1,332 @@ +/* + * 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 . + */ +#include +#include +#include +#include +// The field name in the data model (translated) +#define DISPLAY_NAME_COLUMN 0 + +// The field name's label for exporting (CSV, etc.) +#define LABEL_COLUMN 1 +#define SHOW_FIELD_COLUMN 2 +#define GROUP_BY_COLUMN 3 + +// The internal field name (untranslated) +#define FIELD_NAME_COLUMN 4 + +struct BOM_FIELD; +struct BOM_PRESET; +struct BOM_FMT_PRESET; + +enum GROUP_TYPE +{ + GROUP_SINGLETON, + GROUP_COLLAPSED, + GROUP_COLLAPSED_DURING_SORT, + GROUP_EXPANDED, + CHILD_ITEM +}; + + +struct LIB_DATA_MODEL_ROW +{ + LIB_DATA_MODEL_ROW( const LIB_SYMBOL* aFirstReference, GROUP_TYPE aType ) + : m_ItemNumber( 0 ), m_Flag( aType ), m_Refs( { aFirstReference } ) + { + } + + int m_ItemNumber; + GROUP_TYPE m_Flag; + std::vector m_Refs; +}; + + +struct LIB_DATA_MODEL_COL +{ + wxString m_fieldName; + wxString m_label; + bool m_userAdded; + bool m_show; + bool m_group; + bool m_isCheckbox; +}; + + +struct LIB_DATA_ELEMENT +{ + LIB_DATA_ELEMENT() + { + m_originalData = wxEmptyString; + m_currentData = wxEmptyString; + m_originallyEmpty = false; + m_currentlyEmpty = false; + m_isModified = false; + m_isStriped = false; + m_createDerivedSymbol = false; + m_derivedSymbolName = wxEmptyString; + } + + wxString m_originalData; + wxString m_currentData; + bool m_originallyEmpty; + bool m_currentlyEmpty; + bool m_isModified; + bool m_isStriped; + bool m_createDerivedSymbol; + wxString m_derivedSymbolName; +}; + + +class LIB_FIELDS_EDITOR_GRID_DATA_MODEL : public wxGridTableBase +{ +public: + enum SCOPE : int + { + SCOPE_ALL = 0, + SCOPE_SHEET = 1, + SCOPE_SHEET_RECURSIVE = 2 + }; + + LIB_FIELDS_EDITOR_GRID_DATA_MODEL( const std::vector& aSymbolsList ) : + m_symbolsList( aSymbolsList ), m_edited( false ), m_sortColumn( 0 ), + m_sortAscending( false ), m_filter( wxEmptyString ), m_scope( SCOPE_ALL ), + m_path(), m_groupingEnabled( false ), m_cols(), m_rows(), + m_dataStore(), m_stripedStringRenderer( nullptr ) + { + } + + ~LIB_FIELDS_EDITOR_GRID_DATA_MODEL() + { + for (auto& pair : m_stripedRenderers) + pair.second->DecRef(); + + m_stripedRenderers.clear(); + } + + static const wxString QUANTITY_VARIABLE; + static const wxString ITEM_NUMBER_VARIABLE; + + void CreateDerivedSymbol( int aRow, int aCol, wxString& aNewSymbolName ); + void CreateDerivedSymbolImmediate( int aRow, int aCol, wxString& aNewSymbolName ); + + void AddColumn( const wxString& aFieldName, const wxString& aLabel, bool aAddedByUser, bool aIsCheckbox ); + void RemoveColumn( int aCol ); + void RenameColumn( int aCol, const wxString& newName ); + + void MoveColumn( int aCol, int aNewPos ) + { + wxCHECK_RET( aCol >= 0 && aCol < (int) m_cols.size(), "Invalid Column Number" ); + + if( aCol == aNewPos ) + return; + else if( aCol < aNewPos ) + std::rotate( std::begin( m_cols ) + aCol, std::begin( m_cols ) + aCol + 1, + std::begin( m_cols ) + aNewPos + 1 ); + else + std::rotate( std::begin( m_cols ) + aNewPos, std::begin( m_cols ) + aCol, + std::begin( m_cols ) + aCol + 1 ); + } + + int GetNumberRows() override { return (int) m_rows.size(); } + int GetNumberCols() override { return (int) m_cols.size(); } + + void SetColLabelValue( int aCol, const wxString& aLabel ) override + { + wxCHECK_RET( aCol >= 0 && aCol < (int) m_cols.size(), "Invalid Column Number" ); + m_cols[aCol].m_label = aLabel; + } + + + wxString GetColLabelValue( int aCol ) override + { + wxCHECK( aCol >= 0 && aCol < (int) m_cols.size(), wxString() ); + return m_cols[aCol].m_label; + } + + wxString GetColFieldName( int aCol ) + { + wxCHECK( aCol >= 0 && aCol < (int) m_cols.size(), wxString() ); + return m_cols[aCol].m_fieldName; + } + + int GetFieldNameCol( wxString aFieldName ); + + void SetFieldsOrder( const std::vector& aNewOrder ); + + bool IsEmptyCell( int aRow, int aCol ) override + { + return false; // don't allow adjacent cell overflow, even if we are actually empty + } + + wxString GetValue( int aRow, int aCol ) override; + wxString GetTypeName( int row, int col ) override; + wxString GetValue( const LIB_DATA_MODEL_ROW& group, int aCol ); + void SetValue( int aRow, int aCol, const wxString& aValue ) override; + + wxGridCellAttr* GetAttr( int row, int col, wxGridCellAttr::wxAttrKind kind ) override; + void RevertRow( int aRow ); + void ClearCell( int aRow, int aCol ); + + bool ColIsValue( int aCol ); + bool ColIsCheck( int aCol ); + + void SetSorting( int aCol, bool ascending ) + { + wxCHECK_RET( aCol >= 0 && aCol < (int) m_cols.size(), "Invalid Column Number" ); + m_sortColumn = aCol; + m_sortAscending = ascending; + } + + int GetSortCol() { return m_sortColumn; } + bool GetSortAsc() { return m_sortAscending; } + const LIB_SYMBOL* GetSymbolForRow( int aRow ) + { + wxCHECK( aRow >= 0 && aRow < (int) m_rows.size(), nullptr ); + return m_rows[aRow].m_Refs[0]; + } + + void GetSymbolNames( wxArrayString& aList ) + { + aList.Clear(); + for( const LIB_SYMBOL* symbol : m_symbolsList ) + aList.Add( symbol->GetName() ); + } + void RebuildRows(); + + void ExpandRow( int aRow ); + void CollapseRow( int aRow ); + void ExpandCollapseRow( int aRow ); + void CollapseForSort(); + void ExpandAfterSort(); + + void ApplyData( std::function symbolChangeHandler, + std::function postApplyHandler = nullptr ); + + /// Get and clear the list of newly created derived symbols for library manager processing + std::vector> GetAndClearCreatedDerivedSymbols() + { + auto result = std::move( m_createdDerivedSymbols ); + m_createdDerivedSymbols.clear(); + return result; + } + + bool IsEdited() { return m_edited; } + + int GetDataWidth( int aCol ); + + void SetFilter( const wxString& aFilter ) { m_filter = aFilter; } + const wxString& GetFilter() { return m_filter; } + + void SetGroupingEnabled( bool group ) { m_groupingEnabled = group; } + bool GetGroupingEnabled() { return m_groupingEnabled; } + + void SetGroupColumn( int aCol, bool group ) + { + wxCHECK_RET( aCol >= 0 && aCol < (int) m_cols.size(), "Invalid Column Number" ); + m_cols[aCol].m_group = group; + } + + bool GetGroupColumn( int aCol ) + { + wxCHECK_MSG( aCol >= 0 && aCol < (int) m_cols.size(), false, "Invalid Column Number" ); + return m_cols[aCol].m_group; + } + + void SetShowColumn( int aCol, bool show ) + { + wxCHECK_RET( aCol >= 0 && aCol < (int) m_cols.size(), "Invalid Column Number" ); + m_cols[aCol].m_show = show; + } + + bool GetShowColumn( int aCol ) + { + wxCHECK_MSG( aCol >= 0 && aCol < (int) m_cols.size(), false, "Invalid Column Number" ); + return m_cols[aCol].m_show; + } + + bool IsRowEditable( int aRow ) + { + wxCHECK_MSG( aRow >= 0 && aRow < (int) m_rows.size(), false, "Invalid Row Number" ); + return m_rows[aRow].m_Flag == GROUP_SINGLETON || m_rows[aRow].m_Flag == GROUP_SINGLETON; + } + + bool IsCellEdited( int aRow, int aCol ) + { + wxCHECK_MSG( aRow >= 0 && aRow < (int) m_rows.size(), false, "Invalid Row Number" ); + wxCHECK_MSG( aCol >= 0 && aCol < (int) m_cols.size(), false, "Invalid Column Number" ); + return m_dataStore[m_rows[aRow].m_Refs[0]->m_Uuid][m_cols[aCol].m_fieldName].m_isModified; + } + + bool IsCellClear( int aRow, int aCol ) + { + wxCHECK_MSG( aRow >= 0 && aRow < (int) m_rows.size(), false, "Invalid Row Number" ); + wxCHECK_MSG( aCol >= 0 && aCol < (int) m_cols.size(), false, "Invalid Column Number" ); + return m_dataStore[m_rows[aRow].m_Refs[0]->m_Uuid][m_cols[aCol].m_fieldName].m_currentlyEmpty; + } + + bool IsRowSingleSymbol( int aRow ) + { + wxCHECK_MSG( aRow >= 0 && aRow < (int) m_rows.size(), false, "Invalid Row Number" ); + return m_rows[aRow].m_Flag == GROUP_SINGLETON || m_rows[aRow].m_Flag == CHILD_ITEM; + } + +private: + static bool cmp( const LIB_DATA_MODEL_ROW& lhGroup, const LIB_DATA_MODEL_ROW& rhGroup, + LIB_FIELDS_EDITOR_GRID_DATA_MODEL* dataModel, int sortCol, bool ascending ); + bool groupMatch( const LIB_SYMBOL* lhRef, const LIB_SYMBOL* rhRef ); + wxString getAttributeValue( const LIB_SYMBOL*, const wxString& aAttributeName ); + void setAttributeValue( LIB_SYMBOL* aSymbol, const wxString& aAttributeName, + const wxString& aValue ); + + void createActualDerivedSymbol( const LIB_SYMBOL* aParentSymbol, const wxString& aNewSymbolName, + const KIID& aNewSymbolUuid ); + + void Sort(); + + void updateDataStoreSymbolField( const LIB_SYMBOL* aSymbol, const wxString& aFieldName ); + + bool isStripeableField( int aCol ); + wxGridCellRenderer* getStripedRenderer( int aCol ) const; + +protected: + std::vector m_symbolsList; + bool m_edited; + int m_sortColumn; + bool m_sortAscending; + wxString m_filter; + enum SCOPE m_scope; + SCH_SHEET_PATH m_path; + bool m_groupingEnabled; + + std::vector m_cols; + std::vector m_rows; + + // Data store + // The data model is fundamentally m_componentRefs X m_fieldNames. + // A map of compID : fieldSet, where fieldSet is a map of fieldName : LIB_DATA_ELEMENT + std::map> m_dataStore; + + // Track newly created derived symbols for library manager integration + std::vector> m_createdDerivedSymbols; // symbol, library name + + // stripe bitmap support + mutable STRIPED_STRING_RENDERER* m_stripedStringRenderer; + mutable std::map m_stripedRenderers; +}; diff --git a/eeschema/symbol_editor/symbol_editor_settings.cpp b/eeschema/symbol_editor/symbol_editor_settings.cpp index f7acc1bd81..3b5efff2e0 100644 --- a/eeschema/symbol_editor/symbol_editor_settings.cpp +++ b/eeschema/symbol_editor/symbol_editor_settings.cpp @@ -130,6 +130,13 @@ SYMBOL_EDITOR_SETTINGS::SYMBOL_EDITOR_SETTINGS() : m_params.emplace_back( new PARAM( "use_eeschema_color_settings", &m_UseEeschemaColorSettings, true ) ); + m_params.emplace_back( new PARAM_MAP( "field_editor.field_widths", &m_LibFieldEditor.field_widths, {} ) ); + + m_params.emplace_back( new PARAM( "field_editor.width", &m_LibFieldEditor.width, 0 ) ); + + m_params.emplace_back( new PARAM( "field_editor.height", &m_LibFieldEditor.height, 0 ) ); + + m_params.emplace_back( new PARAM_LAMBDA( "selection_filter", [&]() -> nlohmann::json { diff --git a/eeschema/symbol_editor/symbol_editor_settings.h b/eeschema/symbol_editor/symbol_editor_settings.h index fff3712344..426ec70b09 100644 --- a/eeschema/symbol_editor/symbol_editor_settings.h +++ b/eeschema/symbol_editor/symbol_editor_settings.h @@ -65,6 +65,13 @@ public: int dxf_units; }; + struct LIB_FIELD_EDITOR + { + std::map field_widths; + int width; + int height; + }; + SYMBOL_EDITOR_SETTINGS(); virtual ~SYMBOL_EDITOR_SETTINGS() {} @@ -99,6 +106,8 @@ public: SCH_SELECTION_FILTER_OPTIONS m_SelectionFilter; + LIB_FIELD_EDITOR m_LibFieldEditor; + protected: virtual std::string getLegacyFrameName() const override { return "LibeditFrame"; } diff --git a/eeschema/tools/symbol_editor_control.cpp b/eeschema/tools/symbol_editor_control.cpp index d0eee0b71b..c75f0c8215 100644 --- a/eeschema/tools/symbol_editor_control.cpp +++ b/eeschema/tools/symbol_editor_control.cpp @@ -25,25 +25,28 @@ #include "tools/symbol_editor_control.h" #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include -#include -#include // To default when file manager setting is empty +#include #include // To open with a text editor +#include +#include +#include // To default when file manager setting is empty +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include -#include "string_utils.h" + bool SYMBOL_EDITOR_CONTROL::Init() { @@ -155,6 +158,30 @@ bool SYMBOL_EDITOR_CONTROL::Init() return false; }; + + auto librarySelectedCondition = [this]( const SELECTION& aSel ) -> bool + { + bool result = false; + LIB_TREE* libTree = m_frame->GetLibTree(); + + if( libTree ) + { + std::vector selection; + libTree->GetSelectedTreeNodes( selection ); + + if( selection.size() == 1 ) + { + const LIB_TREE_NODE* lib = selection[0]; + if( lib && lib->m_Type == LIB_TREE_NODE::TYPE::LIBRARY ) + { + result = true; + } + } + } + + return result; + }; + // clang-format off ctxMenu.AddItem( SCH_ACTIONS::newSymbol, libInferredCondition, 10 ); ctxMenu.AddItem( SCH_ACTIONS::deriveFromExistingSymbol, symbolSelectedCondition, 10 ); @@ -190,6 +217,8 @@ bool SYMBOL_EDITOR_CONTROL::Init() ctxMenu.AddItem( ACTIONS::openDirectory, canOpenExternally && ( symbolSelectedCondition || libSelectedCondition ), 200 ); } + ctxMenu.AddItem( ACTIONS::showLibraryTable, librarySelectedCondition, 300 ); + libraryTreeTool->AddContextMenuItems( &ctxMenu ); } // clang-format on @@ -887,6 +916,18 @@ int SYMBOL_EDITOR_CONTROL::ChangeUnit( const TOOL_EVENT& aEvent ) } +int SYMBOL_EDITOR_CONTROL::ShowLibraryTable( const TOOL_EVENT& aEvent ) +{ + SYMBOL_EDIT_FRAME* editFrame = getEditFrame(); + wxString libName = editFrame->GetTreeLIBID().GetLibNickname(); + + DIALOG_LIB_FIELDS dlg( editFrame, libName ); + dlg.SetTitle( _( "Library Fields" ) ); + dlg.ShowModal(); + return 0; +} + + void SYMBOL_EDITOR_CONTROL::setTransitions() { // clang-format off @@ -931,6 +972,8 @@ void SYMBOL_EDITOR_CONTROL::setTransitions() Go( &SYMBOL_EDITOR_CONTROL::ToggleHiddenFields, SCH_ACTIONS::showHiddenFields.MakeEvent() ); Go( &SYMBOL_EDITOR_CONTROL::TogglePinAltIcons, SCH_ACTIONS::togglePinAltIcons.MakeEvent() ); + Go( &SYMBOL_EDITOR_CONTROL::ShowLibraryTable, ACTIONS::showLibraryTable.MakeEvent() ); + Go( &SYMBOL_EDITOR_CONTROL::ChangeUnit, SCH_ACTIONS::previousUnit.MakeEvent() ); Go( &SYMBOL_EDITOR_CONTROL::ChangeUnit, SCH_ACTIONS::nextUnit.MakeEvent() ); // clang-format on diff --git a/eeschema/tools/symbol_editor_control.h b/eeschema/tools/symbol_editor_control.h index 2e5079c3f3..73f06e1912 100644 --- a/eeschema/tools/symbol_editor_control.h +++ b/eeschema/tools/symbol_editor_control.h @@ -75,6 +75,8 @@ public: int DdAddLibrary( const TOOL_EVENT& aEvent ); + int ShowLibraryTable( const TOOL_EVENT& aEvent ); + private: ///< Set up handlers for various events. void setTransitions() override; diff --git a/include/tool/actions.h b/include/tool/actions.h index 2fd930bebb..57a7c88462 100644 --- a/include/tool/actions.h +++ b/include/tool/actions.h @@ -264,6 +264,7 @@ public: static TOOL_ACTION updateSchematicFromPcb; static TOOL_ACTION showProperties; static TOOL_ACTION showDatasheet; + static TOOL_ACTION showLibraryTable; // Internal static TOOL_ACTION updateMenu; diff --git a/include/trace_helpers.h b/include/trace_helpers.h index 248423f830..a94e54a249 100644 --- a/include/trace_helpers.h +++ b/include/trace_helpers.h @@ -248,6 +248,13 @@ extern KICOMMON_API const wxChar* const traceEagleIo; */ extern KICOMMON_API const wxChar* const traceDesignBlocks; +/* + * Flag to enable library field table debug tracing. + * + * Use "KICAD_LIB_FIELD_TABLE" to enable. + */ +extern KICOMMON_API const wxChar* const traceLibFieldTable; + ///@} /** diff --git a/include/widgets/grid_striped_renderer.h b/include/widgets/grid_striped_renderer.h new file mode 100644 index 0000000000..fd13ce7b9b --- /dev/null +++ b/include/widgets/grid_striped_renderer.h @@ -0,0 +1,132 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The 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, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + */ + +#pragma once + +#include +#include +#include + +class wxGrid; + +template +class STRIPED_CELL_RENDERER : public T +{ +public: + STRIPED_CELL_RENDERER() : T() {} + + // Perfect forwarding constructor to handle any base renderer constructor arguments + template + explicit STRIPED_CELL_RENDERER(Args&&... args) : T(std::forward(args)...) {} + + void Draw( wxGrid& grid, wxGridCellAttr& attr, wxDC& dc, + const wxRect& rect, int row, int col, bool isSelected ) override + { + // First draw the striped background for empty cells + wxString cellValue = grid.GetCellValue( row, col ); + + if( cellValue.IsEmpty() ) + { + drawStripedBackground( dc, attr, rect, isSelected ); + attr.SetBackgroundColour( wxColour( 0, 0, 0, wxALPHA_TRANSPARENT ) ); + } + + // Then draw the foreground content using the base renderer + T::Draw( grid, attr, dc, rect, row, col, isSelected ); + } + + wxGridCellRenderer* Clone() const override + { + return new STRIPED_CELL_RENDERER(*this); + } + +private: + void drawStripedBackground(wxDC& dc, wxGridCellAttr& attr, const wxRect& rect, bool isSelected) const + { + if( isSelected ) + { + // For selected cells, use the selection color + dc.SetBrush( wxBrush( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT ) ) ); + dc.SetPen( *wxTRANSPARENT_PEN ); + dc.DrawRectangle( rect ); + return; + } + + // First fill with background color + wxColour bgColor = attr.GetBackgroundColour(); + dc.SetBrush( wxBrush( bgColor ) ); + dc.SetPen( *wxTRANSPARENT_PEN ); + dc.DrawRectangle( rect ); + + // Draw diagonal stripes + const int stripeSpacing = 20; // Distance between diagonal lines + + wxColour stripeColor; + + int bgLuminance = bgColor.GetLuminance(); + + if( bgLuminance < 128 ) + stripeColor = wxColour( 220, 180, 180 ); // Light red for stripes + else + stripeColor = wxColour( 100, 10, 10 ); // Dark red for stripes + + wxPen stripePen( stripeColor, 1, wxPENSTYLE_SOLID ); + dc.SetPen( stripePen ); + + // Calculate the diagonal stripes from top-left to bottom-right + int startX = rect.GetLeft() - rect.GetHeight(); + int endX = rect.GetRight() + rect.GetHeight(); + + // Draw diagonal lines spaced evenly + for( int x = startX; x < endX; x += stripeSpacing ) + { + int x1 = x; + int y1 = rect.GetTop(); + int x2 = x + rect.GetHeight(); + int y2 = rect.GetBottom(); + + // Clip the line to the rectangle bounds + if( x1 < rect.GetLeft() ) + { + int deltaY = rect.GetLeft() - x1; + x1 = rect.GetLeft(); + y1 = rect.GetTop() + deltaY; + } + + if( x2 > rect.GetRight() ) + { + int deltaY = x2 - rect.GetRight(); + x2 = rect.GetRight(); + y2 = rect.GetBottom() - deltaY; + } + + // Only draw if the line is within the rectangle + if( x1 <= rect.GetRight() && x2 >= rect.GetLeft() && y1 <= rect.GetBottom() && y2 >= rect.GetTop() ) + { + dc.DrawLine( x1, y1, x2, y2 ); + } + } + } +}; + + +using STRIPED_STRING_RENDERER = STRIPED_CELL_RENDERER; +using STRIPED_NUMBER_RENDERER = STRIPED_CELL_RENDERER; +using STRIPED_BOOL_RENDERER = STRIPED_CELL_RENDERER; \ No newline at end of file