/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2017 Oliver Walters
 * 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 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, you may find one here:
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * or you may search the http://www.gnu.org website for the version 2 license,
 * or you may write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 */

#include <advanced_config.h>
#include <common.h>
#include <base_units.h>
#include <bitmaps.h>
#include <confirm.h>
#include <eda_doc.h>
#include <wildcards_and_files_ext.h>
#include <schematic_settings.h>
#include <general.h>
#include <grid_tricks.h>
#include <string_utils.h>
#include <kiface_base.h>
#include <sch_edit_frame.h>
#include <widgets/wx_infobar.h>
#include <sch_reference_list.h>
#include <tools/sch_editor_control.h>
#include <kiplatform/ui.h>
#include <widgets/grid_text_button_helpers.h>
#include <widgets/grid_text_helpers.h>
#include <widgets/bitmap_button.h>
#include <widgets/std_bitmap_button.h>
#include <widgets/wx_grid.h>
#include <widgets/grid_checkbox.h>
#include <wx/debug.h>
#include <wx/ffile.h>
#include <wx/grid.h>
#include <wx/textdlg.h>
#include <wx/filedlg.h>
#include <wx/msgdlg.h>
#include <dialogs/eda_view_switcher.h>
#include "dialog_symbol_fields_table.h"
#include <fields_data_model.h>
#include <eda_list_dialog.h>
#include <project_sch.h>
#include <jobs/job_export_sch_bom.h>

wxDEFINE_EVENT( EDA_EVT_CLOSE_DIALOG_SYMBOL_FIELDS_TABLE, wxCommandEvent );

#ifdef __WXMAC__
#define COLUMN_MARGIN 4
#else
#define COLUMN_MARGIN 15
#endif

using SCOPE = FIELDS_EDITOR_GRID_DATA_MODEL::SCOPE;


enum
{
    MYID_SELECT_FOOTPRINT = GRIDTRICKS_FIRST_CLIENT_ID,
    MYID_SHOW_DATASHEET
};

class VIEW_CONTROLS_GRID_TRICKS : public GRID_TRICKS
{
public:
    VIEW_CONTROLS_GRID_TRICKS( WX_GRID* aGrid ) :
            GRID_TRICKS( aGrid )
    {}

protected:
    void doPopupSelection( wxCommandEvent& event ) override
    {
        if( event.GetId() >= GRIDTRICKS_FIRST_SHOWHIDE )
            m_grid->PostSizeEvent();

        GRID_TRICKS::doPopupSelection( event );
    }
};


class FIELDS_EDITOR_GRID_TRICKS : public GRID_TRICKS
{
public:
    FIELDS_EDITOR_GRID_TRICKS( DIALOG_SYMBOL_FIELDS_TABLE* aParent, WX_GRID* aGrid,
                               VIEW_CONTROLS_GRID_DATA_MODEL* aViewFieldsData,
                               FIELDS_EDITOR_GRID_DATA_MODEL* aDataModel, EMBEDDED_FILES* aFiles ) :
            GRID_TRICKS( aGrid ),
            m_dlg( aParent ),
            m_viewControlsDataModel( aViewFieldsData ),
            m_dataModel( aDataModel ),
            m_files( aFiles )
    {}

protected:
    void showPopupMenu( wxMenu& menu, wxGridEvent& aEvent ) override
    {
        int col = m_grid->GetGridCursorCol();

        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_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() ),
                                   { m_files } );
        }
        else if( event.GetId() >= GRIDTRICKS_FIRST_SHOWHIDE )
        {
            if( !m_grid->CommitPendingChanges( false ) )
                return;

            // Pop-up column order is the order of the shown fields, not the viewControls order
            col = event.GetId() - GRIDTRICKS_FIRST_SHOWHIDE;

            bool show = !m_dataModel->GetShowColumn( col );

            m_dlg->ShowHideColumn( col, show );

            wxString fieldName = m_dataModel->GetColFieldName( col );

            for( row = 0; row < m_viewControlsDataModel->GetNumberRows(); row++ )
            {
                if( m_viewControlsDataModel->GetCanonicalFieldName( row ) == fieldName )
                    m_viewControlsDataModel->SetValueAsBool( row, SHOW_FIELD_COLUMN, show );
            }

            if( m_viewControlsDataModel->GetView() )
                m_viewControlsDataModel->GetView()->ForceRefresh();
        }
        else
        {
            GRID_TRICKS::doPopupSelection( event );
        }
    }

private:
    DIALOG_SYMBOL_FIELDS_TABLE*    m_dlg;
    VIEW_CONTROLS_GRID_DATA_MODEL* m_viewControlsDataModel;
    FIELDS_EDITOR_GRID_DATA_MODEL* m_dataModel;
    EMBEDDED_FILES*                m_files;
};


DIALOG_SYMBOL_FIELDS_TABLE::DIALOG_SYMBOL_FIELDS_TABLE( SCH_EDIT_FRAME* parent, JOB_EXPORT_SCH_BOM* aJob ) :
        DIALOG_SYMBOL_FIELDS_TABLE_BASE( parent ),
        m_currentBomPreset( nullptr ),
        m_lastSelectedBomPreset( nullptr ),
        m_parent( parent ),
        m_viewControlsDataModel( nullptr ),
        m_dataModel( nullptr ),
        m_schSettings( parent->Schematic().Settings() ),
        m_job( aJob )
{
    // Get all symbols from the list of schematic sheets
    m_parent->Schematic().Hierarchy().GetSymbols( m_symbolsList, SYMBOL_FILTER_NON_POWER );

    m_bRefresh->SetBitmap( KiBitmapBundle( BITMAPS::small_refresh ) );
    m_bMenu->SetBitmap( KiBitmapBundle( BITMAPS::config ) );
    m_bRefreshPreview->SetBitmap( KiBitmapBundle( BITMAPS::small_refresh ) );
    m_browseButton->SetBitmap( KiBitmapBundle( BITMAPS::small_folder ) );

    m_addFieldButton->SetBitmap( KiBitmapBundle( BITMAPS::small_plus ) );
    m_removeFieldButton->SetBitmap( KiBitmapBundle( BITMAPS::small_trash ) );
    m_renameFieldButton->SetBitmap( KiBitmapBundle( BITMAPS::small_edit ) );

    m_addVariantButton->SetBitmap( KiBitmapBundle( BITMAPS::small_plus ) );
    m_deleteVariantButton->SetBitmap( KiBitmapBundle( BITMAPS::small_trash ) );
    m_renameVariantButton->SetBitmap( KiBitmapBundle( BITMAPS::small_edit ) );
    m_copyVariantButton->SetBitmap( KiBitmapBundle( BITMAPS::copy ) );
    m_editVariantDescButton->SetBitmap( KiBitmapBundle( BITMAPS::text ) );

    m_sidebarButton->SetBitmap( KiBitmapBundle( BITMAPS::left ) );

    // The active notebook page is dictated by the tool that opens this dialog
    // (EditSymbolFields vs GenerateBOM), so suppress DIALOG_SHIM's tab persistence.
    OptOut( m_nbPages );

    m_viewControlsDataModel = new VIEW_CONTROLS_GRID_DATA_MODEL( true );

    m_viewControlsGrid->UseNativeColHeader( true );
    m_viewControlsGrid->SetTable( m_viewControlsDataModel, true );

    // must be done after SetTable(), which appears to re-set it
    m_viewControlsGrid->SetSelectionMode( wxGrid::wxGridSelectCells );

    // add Cut, Copy, and Paste to wxGrid
    m_viewControlsGrid->PushEventHandler( new VIEW_CONTROLS_GRID_TRICKS( m_viewControlsGrid ) );

    wxGridCellAttr* attr = new wxGridCellAttr;
    attr->SetReadOnly( true );
    m_viewControlsDataModel->SetColAttr( attr, DISPLAY_NAME_COLUMN );

    attr = new wxGridCellAttr;
    attr->SetRenderer( new wxGridCellBoolRenderer() );
    attr->SetReadOnly();    // not really; we delegate interactivity to GRID_TRICKS
    attr->SetAlignment( wxALIGN_CENTER, wxALIGN_CENTER );
    m_viewControlsDataModel->SetColAttr( attr, SHOW_FIELD_COLUMN );

    attr = new wxGridCellAttr;
    attr->SetRenderer( new wxGridCellBoolRenderer() );
    attr->SetReadOnly();    // not really; we delegate interactivity to GRID_TRICKS
    attr->SetAlignment( wxALIGN_CENTER, wxALIGN_CENTER );
    m_viewControlsDataModel->SetColAttr( attr, GROUP_BY_COLUMN );

    // Compress the view controls grid.  (We want it to look different from the fields grid.)
    m_viewControlsGrid->SetDefaultRowSize( m_viewControlsGrid->GetDefaultRowSize() - FromDIP( 4 ) );

    m_filter->SetDescriptiveText( _( "Filter" ) );

    attr = new wxGridCellAttr;
    attr->SetEditor( new GRID_CELL_URL_EDITOR( this, PROJECT_SCH::SchSearchS( &Prj() ), { &m_parent->Schematic() } ) );
    m_dataModel = new FIELDS_EDITOR_GRID_DATA_MODEL( m_symbolsList, attr );

    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 FIELDS_EDITOR_GRID_TRICKS( this, m_grid, m_viewControlsDataModel, m_dataModel,
                                                             &m_parent->Schematic() ) );

    m_variantListBox->Set( parent->Schematic().GetVariantNamesForUI() );

    if( !m_parent->Schematic().GetCurrentVariant().IsEmpty() )
    {
        int toSelect = m_variantListBox->FindString( m_parent->Schematic().GetCurrentVariant() );

        if( toSelect == wxNOT_FOUND )
            m_variantListBox->SetSelection( 0 );
        else
            m_variantListBox->SetSelection( toSelect );
    }
    else
    {
        m_variantListBox->SetSelection( 0 );
    }

    updateVariantButtonStates();

    if( m_job )
        SetTitle( m_job->GetSettingsDialogTitle() );

    // DIALOG_SHIM needs a unique hash_key because classname will be the same for both job and
    // non-job versions (which have different sizes).
    m_hash_key = TO_UTF8( GetTitle() );

    // Set the current variant for highlighting variant-specific field values
    m_dataModel->SetCurrentVariant( m_parent->Schematic().GetCurrentVariant() );

    SetInitialFocus( m_grid );
    m_grid->ClearSelection();

    SetupStandardButtons();

    finishDialogSettings();

    SetSize( wxSize( horizPixelsFromDU( 600 ), vertPixelsFromDU( 300 ) ) );

    EESCHEMA_SETTINGS::PANEL_SYMBOL_FIELDS_TABLE& cfg = m_parent->eeconfig()->m_FieldEditorPanel;

    m_viewControlsGrid->ShowHideColumns( "0 1 2 3" );

    CallAfter( [this, cfg]()
               {
                   if( cfg.sidebar_collapsed )
                       m_splitterMainWindow->Unsplit( m_leftPanel );
                   else
                       m_splitterMainWindow->SetSashPosition( cfg.sash_pos );

                   setSideBarButtonLook( cfg.sidebar_collapsed );

                   m_splitter_left->SetSashPosition( cfg.variant_sash_pos );
               } );

    if( m_job )
        m_outputFileName->SetValue( m_job->GetConfiguredOutputPath() );
    else
        m_outputFileName->SetValue( m_schSettings.m_BomExportFileName );

    Center();

    // Connect Events
    m_grid->Bind( wxEVT_GRID_COL_SORT, &DIALOG_SYMBOL_FIELDS_TABLE::OnColSort, this );
    m_grid->Bind( wxEVT_GRID_COL_MOVE, &DIALOG_SYMBOL_FIELDS_TABLE::OnColMove, this );
    m_grid->GetGridWindow()->Bind( wxEVT_MOTION, &DIALOG_SYMBOL_FIELDS_TABLE::OnGridMouseMove, this );
    m_cbBomPresets->Bind( wxEVT_CHOICE, &DIALOG_SYMBOL_FIELDS_TABLE::onBomPresetChanged, this );
    m_cbBomFmtPresets->Bind( wxEVT_CHOICE, &DIALOG_SYMBOL_FIELDS_TABLE::onBomFmtPresetChanged, this );
    m_viewControlsGrid->Bind( wxEVT_GRID_CELL_CHANGED, &DIALOG_SYMBOL_FIELDS_TABLE::OnViewControlsCellChanged, this );

    if( !m_job )
    {
        // Start listening for schematic changes
        m_parent->Schematic().AddListener( this );
    }
    else
    {
        // Don't allow editing
        m_grid->EnableEditing( false );
        m_buttonApply->Hide();
        m_buttonExport->Hide();
    }
}


DIALOG_SYMBOL_FIELDS_TABLE::~DIALOG_SYMBOL_FIELDS_TABLE()
{
    savePresetsToSchematic();
    m_schSettings.m_BomExportFileName = m_outputFileName->GetValue();

    EESCHEMA_SETTINGS::PANEL_SYMBOL_FIELDS_TABLE& cfg = m_parent->eeconfig()->m_FieldEditorPanel;

    if( !cfg.sidebar_collapsed )
        cfg.sash_pos = m_splitterMainWindow->GetSashPosition();

    cfg.variant_sash_pos = m_splitter_left->GetSashPosition();

    for( int i = 0; i < m_grid->GetNumberCols(); i++ )
    {
        if( m_grid->IsColShown( i ) )
        {
            std::string fieldName( m_dataModel->GetColFieldName( i ).ToUTF8() );
            cfg.field_widths[fieldName] = m_grid->GetColSize( i );
        }
    }

    // Disconnect Events
    m_grid->GetGridWindow()->Unbind( wxEVT_MOTION, &DIALOG_SYMBOL_FIELDS_TABLE::OnGridMouseMove, this );
    m_grid->Unbind( wxEVT_GRID_COL_SORT, &DIALOG_SYMBOL_FIELDS_TABLE::OnColSort, this );
    m_grid->Unbind( wxEVT_GRID_COL_SORT, &DIALOG_SYMBOL_FIELDS_TABLE::OnColMove, this );
    m_cbBomPresets->Unbind( wxEVT_CHOICE, &DIALOG_SYMBOL_FIELDS_TABLE::onBomPresetChanged, this );
    m_cbBomFmtPresets->Unbind( wxEVT_CHOICE, &DIALOG_SYMBOL_FIELDS_TABLE::onBomFmtPresetChanged, this );
    m_viewControlsGrid->Unbind( wxEVT_GRID_CELL_CHANGED, &DIALOG_SYMBOL_FIELDS_TABLE::OnViewControlsCellChanged, this );

    // Delete the GRID_TRICKS.
    m_viewControlsGrid->PopEventHandler( true );
    m_grid->PopEventHandler( true );

    // we gave ownership of m_viewControlsDataModel & m_dataModel to the wxGrids...
}


void DIALOG_SYMBOL_FIELDS_TABLE::setSideBarButtonLook( bool aIsLeftPanelCollapsed )
{
    // Set bitmap and tooltip according to left panel visibility

    if( aIsLeftPanelCollapsed )
    {
        m_sidebarButton->SetBitmap( KiBitmapBundle( BITMAPS::right ) );
        m_sidebarButton->SetToolTip( _( "Expand left panel" ) );
    }
    else
    {
        m_sidebarButton->SetBitmap( KiBitmapBundle( BITMAPS::left ) );
        m_sidebarButton->SetToolTip( _( "Collapse left panel" ) );
    }
}


void DIALOG_SYMBOL_FIELDS_TABLE::SetupColumnProperties( int aCol )
{
    wxGridCellAttr* attr = new wxGridCellAttr;
    attr->SetReadOnly( false );

    // Set some column types to specific editors
    if( m_dataModel->ColIsReference( aCol ) )
    {
        attr->SetReadOnly();
        attr->SetRenderer( new GRID_CELL_TEXT_RENDERER() );
        m_dataModel->SetColAttr( attr, aCol );
    }
    else 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_parent->Schematic() } ) );
        m_dataModel->SetColAttr( attr, aCol );
    }
    else if( m_dataModel->ColIsQuantity( aCol ) || m_dataModel->ColIsItemNumber( aCol ) )
    {
        attr->SetReadOnly();
        attr->SetAlignment( wxALIGN_RIGHT, wxALIGN_CENTER );
        attr->SetRenderer( new wxGridCellNumberRenderer() );
        m_dataModel->SetColAttr( attr, aCol );
    }
    else if( m_dataModel->ColIsAttribute( aCol ) )
    {
        attr->SetAlignment( wxALIGN_CENTER, wxALIGN_CENTER );
        attr->SetRenderer( new GRID_CELL_CHECKBOX_RENDERER() );
        attr->SetReadOnly();    // not really; we delegate interactivity to GRID_TRICKS
        m_dataModel->SetColAttr( attr, aCol );
    }
    else if( IsGeneratedField( m_dataModel->GetColFieldName( aCol ) ) )
    {
        attr->SetReadOnly();
        m_dataModel->SetColAttr( attr, aCol );
    }
    else
    {
        attr->SetRenderer( new GRID_CELL_TEXT_RENDERER() );
        attr->SetEditor( m_grid->GetDefaultEditor() );
        m_dataModel->SetColAttr( attr, aCol );
    }
}


void DIALOG_SYMBOL_FIELDS_TABLE::SetupAllColumnProperties()
{
    EESCHEMA_SETTINGS* cfg = m_parent->eeconfig();
    wxSize defaultDlgSize = ConvertDialogToPixels( wxSize( 600, 300 ) );

    // Restore column sorting order and widths
    m_grid->AutoSizeColumns( false );
    int  sortCol = 0;
    bool sortAscending = true;

    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_viewControlsGrid
    for( int i = 0; i < m_viewControlsDataModel->GetNumberRows(); ++i )
    {
        int col = m_dataModel->GetFieldNameCol( m_viewControlsDataModel->GetCanonicalFieldName( i ) );

        if( col == -1 )
            continue;

        bool show = m_viewControlsDataModel->GetValueAsBool( 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_FieldEditorPanel.field_widths.count( key )
                && ( cfg->m_FieldEditorPanel.field_widths.at( key ) > 0 ) )
            {
                m_grid->SetColSize( col, cfg->m_FieldEditorPanel.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 );
}


bool DIALOG_SYMBOL_FIELDS_TABLE::TransferDataToWindow()
{
    if( !wxDialog::TransferDataToWindow() )
        return false;

    LoadFieldNames();   // loads rows into m_viewControlsDataModel and columns into m_dataModel

    // Load our BOM view presets
    SetUserBomPresets( m_schSettings.m_BomPresets );

    BOM_PRESET preset = m_schSettings.m_BomSettings;

    if( m_job )
    {
        preset.name = m_job->m_bomPresetName;
        preset.excludeDNP = m_job->m_excludeDNP;
        preset.filterString = m_job->m_filterString;
        preset.sortAsc = m_job->m_sortAsc;
        preset.sortField = m_job->m_sortField;
        preset.groupSymbols = m_job->m_groupSymbols;

        preset.fieldsOrdered.clear();

        size_t i = 0;

        for( const wxString& fieldName : m_job->m_fieldsOrdered )
        {
            BOM_FIELD field;
            field.name = fieldName;
            field.show = !fieldName.StartsWith( wxT( "__" ), &field.name );
            field.groupBy = alg::contains( m_job->m_fieldsGroupBy, field.name );

            if( ( m_job->m_fieldsLabels.size() > i ) && !m_job->m_fieldsLabels[i].IsEmpty() )
                field.label = m_job->m_fieldsLabels[i];
            else if( IsGeneratedField( field.name ) )
                field.label = GetGeneratedFieldDisplayName( field.name );
            else
                field.label = field.name;

            preset.fieldsOrdered.emplace_back( field );
            i++;
        }
    }

    ApplyBomPreset( preset );
    syncBomPresetSelection();

    // Load BOM export format presets
    SetUserBomFmtPresets( m_schSettings.m_BomFmtPresets );
    BOM_FMT_PRESET fmtPreset = m_schSettings.m_BomFmtSettings;

    if( m_job )
    {
        fmtPreset.name = m_job->m_bomFmtPresetName;
        fmtPreset.fieldDelimiter = m_job->m_fieldDelimiter;
        fmtPreset.keepLineBreaks = m_job->m_keepLineBreaks;
        fmtPreset.keepTabs = m_job->m_keepTabs;
        fmtPreset.refDelimiter = m_job->m_refDelimiter;
        fmtPreset.refRangeDelimiter = m_job->m_refRangeDelimiter;
        fmtPreset.stringDelimiter = m_job->m_stringDelimiter;
    }

    ApplyBomFmtPreset( fmtPreset );
    syncBomFmtPresetSelection();

    TOOL_MANAGER*       toolMgr = m_parent->GetToolManager();
    SCH_SELECTION_TOOL* selectionTool = toolMgr->GetTool<SCH_SELECTION_TOOL>();
    SCH_SELECTION&      selection = selectionTool->GetSelection();
    SCH_SYMBOL*         symbol = nullptr;

    m_dataModel->SetGroupingEnabled( m_groupSymbolsBox->GetValue() );

    wxCommandEvent dummy;
    OnScope( dummy );

    if( selection.GetSize() == 1 )
    {
        EDA_ITEM* item = selection.Front();

        if( item->Type() == SCH_SYMBOL_T )
            symbol = (SCH_SYMBOL*) item;
        else if( item->GetParent() && item->GetParent()->Type() == SCH_SYMBOL_T )
            symbol = (SCH_SYMBOL*) item->GetParent();
    }

    if( symbol )
    {
        for( int row = 0; row < m_dataModel->GetNumberRows(); ++row )
        {
            std::vector<SCH_REFERENCE> references = m_dataModel->GetRowReferences( row );
            bool                       found = false;

            for( const SCH_REFERENCE& ref : references )
            {
                if( ref.GetSymbol() == symbol )
                {
                    found = true;
                    break;
                }
            }

            if( found )
            {
                // Find the value column and the reference column if they're shown
                int valueCol = -1;
                int refCol = -1;
                int anyCol = -1;

                for( int col = 0; col < m_dataModel->GetNumberCols(); col++ )
                {
                    if( m_dataModel->ColIsValue( col ) )
                        valueCol = col;
                    else if( m_dataModel->ColIsReference( col ) )
                        refCol = col;
                    else if( anyCol == -1 && m_dataModel->GetShowColumn( col ) )
                        anyCol = col;
                }

                if( valueCol != -1 && m_dataModel->GetShowColumn( valueCol ) )
                    m_grid->GoToCell( row, valueCol );
                else if( refCol != -1 && m_dataModel->GetShowColumn( refCol ) )
                    m_grid->GoToCell( row, refCol );
                else if( anyCol != -1 )
                    m_grid->GoToCell( row, anyCol );

                break;
            }
        }
    }

    // We don't want table range selection events to happen until we've loaded the data or we
    // we'll clear our selection as the grid is built before the code above can get the
    // user's current selection.
    EnableSelectionEvents();

    return true;
}


bool DIALOG_SYMBOL_FIELDS_TABLE::TransferDataFromWindow()
{
    if( !m_grid->CommitPendingChanges() )
        return false;

    if( !wxDialog::TransferDataFromWindow() )
        return false;

    if( m_job )
    {
        // and exit, don't even dream of saving changes from the data model
        return true;
    }

    SCH_COMMIT     commit( m_parent );
    SCH_SHEET_PATH currentSheet = m_parent->GetCurrentSheet();
    wxString       currentVariant = m_parent->Schematic().GetCurrentVariant();

    m_dataModel->ApplyData( commit, m_schSettings.m_TemplateFieldNames, currentVariant );

    if( !commit.Empty() )
    {
        commit.Push( wxS( "Symbol Fields Table Edit" ) );  // Push clears the commit buffer.
        m_parent->OnModify();
    }

    // Reset the view to where we left the user
    m_parent->SetCurrentSheet( currentSheet );
    m_parent->SyncView();
    m_parent->Refresh();

    return true;
}


void DIALOG_SYMBOL_FIELDS_TABLE::AddField( const wxString& aFieldName, const wxString& aLabelValue,
                                           bool show, bool groupBy, bool addedByUser )
{
    // 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 row = 0; row < m_viewControlsDataModel->GetNumberRows(); row++ )
    {
        if( m_viewControlsDataModel->GetCanonicalFieldName( row ).CmpNoCase( aFieldName ) == 0 )
            return;
    }

    m_dataModel->AddColumn( aFieldName, aLabelValue, addedByUser, m_parent->Schematic().GetCurrentVariant() );

    wxGridTableMessage msg( m_dataModel, wxGRIDTABLE_NOTIFY_COLS_APPENDED, 1 );
    m_grid->ProcessTableMessage( msg );

    m_viewControlsGrid->OnAddRow(
            [&]() -> std::pair<int, int>
            {
                m_viewControlsDataModel->AppendRow( aFieldName, aLabelValue, show, groupBy );

                return { m_viewControlsDataModel->GetNumberRows() - 1, -1 };
            } );
}


void DIALOG_SYMBOL_FIELDS_TABLE::LoadFieldNames()
{
    auto addMandatoryField =
            [&]( FIELD_T fieldId, bool show, bool groupBy )
            {
                m_mandatoryFieldListIndexes[fieldId] = m_viewControlsDataModel->GetNumberRows();

                AddField( GetCanonicalFieldName( fieldId ), GetDefaultFieldName( fieldId, DO_TRANSLATE ),
                          show, groupBy );
            };

    // Add mandatory fields first            show   groupBy
    addMandatoryField( FIELD_T::REFERENCE,   true,   true   );
    addMandatoryField( FIELD_T::VALUE,       true,   true   );
    addMandatoryField( FIELD_T::FOOTPRINT,   true,   true   );
    addMandatoryField( FIELD_T::DATASHEET,   true,   false  );
    addMandatoryField( FIELD_T::DESCRIPTION, false,  false  );

    // Generated fields present only in the fields table
    AddField( FIELDS_EDITOR_GRID_DATA_MODEL::QUANTITY_VARIABLE, _( "Qty" ), true, false );
    AddField( FIELDS_EDITOR_GRID_DATA_MODEL::ITEM_NUMBER_VARIABLE, _( "#" ), true, false );

    // User fields next
    auto caseInsensitiveLess = []( const wxString& a, const wxString& b )
    {
        return a.CmpNoCase( b ) < 0;
    };

    std::map<wxString, std::map<wxString, int>, decltype( caseInsensitiveLess )> userFieldGroups( caseInsensitiveLess );

    for( int ii = 0; ii < (int) m_symbolsList.GetCount(); ++ii )
    {
        SCH_SYMBOL* symbol = m_symbolsList[ii].GetSymbol();

        for( const SCH_FIELD& field : symbol->GetFields() )
        {
            if( !field.IsMandatory() && !field.IsPrivate() )
                userFieldGroups[field.GetName()][field.GetName()]++;
        }
    }

    for( const auto& [groupKey, exactCounts] : userFieldGroups )
    {
        wxString canonicalName;

        if( const TEMPLATE_FIELDNAME* tfn = m_schSettings.m_TemplateFieldNames.GetFieldName( groupKey ) )
        {
            canonicalName = tfn->m_Name;
        }
        else
        {
            int bestCount = -1;

            for( const auto& [name, count] : exactCounts )
            {
                if( count > bestCount )
                {
                    bestCount = count;
                    canonicalName = name;
                }
            }
        }

        AddField( canonicalName, GetGeneratedFieldDisplayName( canonicalName ), true, false );
    }

    // Add any templateFieldNames which aren't already present.
    for( const TEMPLATE_FIELDNAME& tfn : m_schSettings.m_TemplateFieldNames.GetTemplateFieldNames() )
    {
        if( userFieldGroups.count( tfn.m_Name ) == 0 )
            AddField( tfn.m_Name, GetGeneratedFieldDisplayName( tfn.m_Name ), false, false );
    }
}


void DIALOG_SYMBOL_FIELDS_TABLE::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.CmpNoCase( m_dataModel->GetColFieldName( i ) ) == 0 )
        {
            DisplayError( this, wxString::Format( _( "Field name '%s' already in use." ), fieldName ) );
            return;
        }
    }

    AddField( fieldName, GetGeneratedFieldDisplayName( fieldName ), true, false, true );

    SetupColumnProperties( m_dataModel->GetColsCount() - 1 );

    syncBomPresetSelection();
    OnModify();
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnRemoveField( wxCommandEvent& event )
{
    m_viewControlsGrid->OnDeleteRows(
            [&]( int row )
            {
                for( FIELD_T id : MANDATORY_FIELDS )
                {
                    if( m_mandatoryFieldListIndexes[id] == row )
                    {
                        DisplayError( this, wxString::Format( _( "The first %d fields are mandatory." ),
                                                              (int) m_mandatoryFieldListIndexes.size() ) );
                        return false;
                    }
                }

                return IsOK( this, wxString::Format( _( "Are you sure you want to remove the field '%s'?" ),
                                                     m_viewControlsDataModel->GetValue( row, DISPLAY_NAME_COLUMN ) ) );
            },
            [&]( int row )
            {
                wxString fieldName = m_viewControlsDataModel->GetCanonicalFieldName( row );
                int col = m_dataModel->GetFieldNameCol( fieldName );

                if( col != -1 )
                    m_dataModel->RemoveColumn( col );

                m_viewControlsDataModel->DeleteRow( row );

                syncBomPresetSelection();
                OnModify();
            } );
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnRenameField( wxCommandEvent& event )
{
    wxArrayInt selectedRows = m_viewControlsGrid->GetSelectedRows();

    if( selectedRows.empty() && m_viewControlsGrid->GetGridCursorRow() >= 0 )
        selectedRows.push_back( m_viewControlsGrid->GetGridCursorRow() );

    if( selectedRows.empty() )
        return;

    int row = selectedRows[0];

    for( FIELD_T id : MANDATORY_FIELDS )
    {
        if( m_mandatoryFieldListIndexes[id] == row )
        {
            DisplayError( this, wxString::Format( _( "The first %d fields are mandatory and names cannot be changed." ),
                                                  (int) m_mandatoryFieldListIndexes.size() ) );
            return;
        }
    }

    wxString fieldName = m_viewControlsDataModel->GetCanonicalFieldName( row );
    wxString label = m_viewControlsDataModel->GetValue( row, LABEL_COLUMN );
    bool     labelIsAutogenerated = label.IsSameAs( GetGeneratedFieldDisplayName( fieldName ) );

    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();

    // No change, no-op
    if( newFieldName == fieldName )
         return;

    // New field name already exists
    if( m_dataModel->GetFieldNameCol( newFieldName ) != -1 )
    {
         wxString confirm_msg = wxString::Format( _( "Field name %s already exists." ), newFieldName );
         DisplayError( this, confirm_msg );
         return;
    }

    m_dataModel->RenameColumn( col, newFieldName );
    m_viewControlsDataModel->SetCanonicalFieldName( row, newFieldName );
    m_viewControlsDataModel->SetValue( row, DISPLAY_NAME_COLUMN, newFieldName );

    if( labelIsAutogenerated )
    {
        m_viewControlsDataModel->SetValue( row, LABEL_COLUMN, GetGeneratedFieldDisplayName( newFieldName ) );
        wxGridEvent evt( m_viewControlsGrid->GetId(), wxEVT_GRID_CELL_CHANGED, m_viewControlsGrid, row, LABEL_COLUMN );
        OnViewControlsCellChanged( evt );
    }

    syncBomPresetSelection();
    OnModify();
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnFilterText( wxCommandEvent& aEvent )
{
    m_dataModel->SetFilter( m_filter->GetValue() );
    m_dataModel->RebuildRows();
    m_grid->ForceRefresh();

    syncBomPresetSelection();
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnFilterMouseMoved( wxMouseEvent& aEvent )
{
#if defined( __WXOSX__ ) // Doesn't work properly on other ports
    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 );
#endif
}


void DIALOG_SYMBOL_FIELDS_TABLE::setScope( SCOPE aScope )
{
    m_dataModel->SetPath( m_parent->GetCurrentSheet() );
    m_dataModel->SetScope( aScope );
    m_dataModel->RebuildRows();
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnScope( wxCommandEvent& aEvent )
{
    switch( aEvent.GetSelection() )
    {
    case 0: setScope( SCOPE::SCOPE_ALL );             break;
    case 1: setScope( SCOPE::SCOPE_SHEET );           break;
    case 2: setScope( SCOPE::SCOPE_SHEET_RECURSIVE ); break;
    }
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnGroupSymbolsToggled( wxCommandEvent& event )
{
    m_dataModel->SetGroupingEnabled( m_groupSymbolsBox->GetValue() );
    m_dataModel->RebuildRows();
    m_grid->ForceRefresh();

    syncBomPresetSelection();
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnMenu( wxCommandEvent& event )
{
    EESCHEMA_SETTINGS::PANEL_SYMBOL_FIELDS_TABLE& cfg = m_parent->eeconfig()->m_FieldEditorPanel;

    // Build a pop menu:
    wxMenu menu;

    menu.Append( 4204, _( "Include 'DNP' Symbols" ),
                 _( "Show symbols marked 'DNP' in the table.  This setting also controls whether or not 'DNP' "
                    "symbols are included on export." ),
                 wxITEM_CHECK );
    menu.Check( 4204, !m_dataModel->GetExcludeDNP() );

    menu.Append( 4205, _( "Include 'Exclude from BOM' Symbols" ),
                 _( "Show symbols marked 'Exclude from BOM' in the table.  Symbols marked 'Exclude from BOM' "
                    "are never included on export." ),
                 wxITEM_CHECK );
    menu.Check( 4205, m_dataModel->GetIncludeExcludedFromBOM() );

    menu.AppendSeparator();

    menu.Append( 4206, _( "Highlight on Cross-probe" ),
                 _( "Highlight corresponding item on canvas when it is selected in the table" ),
                 wxITEM_CHECK );
    menu.Check( 4206, cfg.selection_mode == 0 );

    menu.Append( 4207, _( "Select on Cross-probe" ),
                 _( "Select corresponding item on canvas when it is selected in the table" ),
                 wxITEM_CHECK );
    menu.Check( 4207, cfg.selection_mode == 1 );

    // menu_id is the selected submenu id from the popup menu or wxID_NONE
    int menu_id = m_bMenu->GetPopupMenuSelectionFromUser( menu );

    if( menu_id == 0 || menu_id == 4204 )
    {
        m_dataModel->SetExcludeDNP( !m_dataModel->GetExcludeDNP() );
        m_dataModel->RebuildRows();
        m_grid->ForceRefresh();

        syncBomPresetSelection();
    }
    else if( menu_id == 1 || menu_id == 4205 )
    {
        m_dataModel->SetIncludeExcludedFromBOM( !m_dataModel->GetIncludeExcludedFromBOM() );
        m_dataModel->RebuildRows();
        m_grid->ForceRefresh();

        syncBomPresetSelection();
    }
    else if( menu_id == 3 || menu_id == 4206 )
    {
        if( cfg.selection_mode != 0 )
            cfg.selection_mode = 0;
        else
            cfg.selection_mode = 2;
    }
    else if( menu_id == 4 || menu_id == 4207 )
    {
        if( cfg.selection_mode != 1 )
            cfg.selection_mode = 1;
        else
            cfg.selection_mode = 2;
    }
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnColSort( wxGridEvent& aEvent )
{
    int         sortCol = aEvent.GetCol();
    std::string key( m_dataModel->GetColFieldName( sortCol ).ToUTF8() );
    bool        ascending;

    // Don't sort by item number, it is generated by the sort
    if( m_dataModel->ColIsItemNumber( sortCol ) )
    {
        aEvent.Veto();
        return;
    }

    // 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 );
    m_dataModel->RebuildRows();
    m_grid->ForceRefresh();

    syncBomPresetSelection();
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnColMove( wxGridEvent& aEvent )
{
    int origPos = aEvent.GetCol();

    // Save column widths since the setup function uses the saved config values
    EESCHEMA_SETTINGS* cfg = m_parent->eeconfig();

    for( int i = 0; i < m_grid->GetNumberCols(); i++ )
    {
        if( m_grid->IsColShown( i ) )
        {
            std::string fieldName( m_dataModel->GetColFieldName( i ).ToUTF8() );
            cfg->m_FieldEditorPanel.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();
            } );

    syncBomPresetSelection();
}


void DIALOG_SYMBOL_FIELDS_TABLE::ShowHideColumn( int aCol, bool aShow )
{
    if( aShow )
        m_grid->ShowCol( aCol );
    else
        m_grid->HideCol( aCol );

    m_dataModel->SetShowColumn( aCol, aShow );

    syncBomPresetSelection();

    if( m_nbPages->GetSelection() == 1 )
        PreviewRefresh();
    else
        m_grid->ForceRefresh();

    OnModify();
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnViewControlsCellChanged( wxGridEvent& aEvent )
{
    int row = aEvent.GetRow();

    wxCHECK( row < m_viewControlsGrid->GetNumberRows(), /* void */ );

    switch( aEvent.GetCol() )
    {
    case LABEL_COLUMN:
    {
        wxString label = m_viewControlsDataModel->GetValue( row, LABEL_COLUMN );
        wxString fieldName = m_viewControlsDataModel->GetCanonicalFieldName( row );
        int      dataCol = m_dataModel->GetFieldNameCol( fieldName );

        if( dataCol != -1 )
        {
            m_dataModel->SetColLabelValue( dataCol, label );
            m_grid->SetColLabelValue( dataCol, label );

            if( m_nbPages->GetSelection() == 1 )
                PreviewRefresh();
            else
                m_grid->ForceRefresh();

            syncBomPresetSelection();
            OnModify();
        }

        break;
    }

    case SHOW_FIELD_COLUMN:
    {
        wxString fieldName = m_viewControlsDataModel->GetCanonicalFieldName( row );
        bool     value = m_viewControlsDataModel->GetValueAsBool( row, SHOW_FIELD_COLUMN );
        int      dataCol = m_dataModel->GetFieldNameCol( fieldName );

        if( dataCol != -1 )
            ShowHideColumn( dataCol, value );

        break;
    }

    case GROUP_BY_COLUMN:
    {
        wxString fieldName = m_viewControlsDataModel->GetCanonicalFieldName( row );
        bool     value = m_viewControlsDataModel->GetValueAsBool( row, GROUP_BY_COLUMN );
        int      dataCol = m_dataModel->GetFieldNameCol( fieldName );

        if( m_dataModel->ColIsQuantity( dataCol ) && value )
        {
            DisplayError( this, _( "The Quantity column cannot be grouped by." ) );

            value = false;
            m_viewControlsDataModel->SetValueAsBool( row, GROUP_BY_COLUMN, value );
            break;
        }

        if( m_dataModel->ColIsItemNumber( dataCol ) && value )
        {
            DisplayError( this, _( "The Item Number column cannot be grouped by." ) );

            value = false;
            m_viewControlsDataModel->SetValueAsBool( row, GROUP_BY_COLUMN, value );
            break;
        }

        m_dataModel->SetGroupColumn( dataCol, value );
        m_dataModel->RebuildRows();

        if( m_nbPages->GetSelection() == 1 )
            PreviewRefresh();
        else
            m_grid->ForceRefresh();

        syncBomPresetSelection();
        OnModify();
        break;
    }

    default:
        break;
    }
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnTableValueChanged( wxGridEvent& aEvent )
{
    m_grid->ForceRefresh();
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnTableColSize( wxGridSizeEvent& aEvent )
{
    aEvent.Skip();

    m_grid->ForceRefresh();
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnRegroupSymbols( wxCommandEvent& aEvent )
{
    m_dataModel->RebuildRows();
    m_grid->ForceRefresh();
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnTableCellClick( wxGridEvent& event )
{
    if( m_dataModel->IsExpanderColumn( event.GetCol() ) )
    {
        m_grid->ClearSelection();

        m_dataModel->ExpandCollapseRow( event.GetRow() );
        m_grid->SetGridCursor( event.GetRow(), event.GetCol() );
    }
    else
    {
        event.Skip();
    }
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnGridMouseMove( wxMouseEvent& aEvent )
{
    aEvent.Skip();

    wxPoint pos = aEvent.GetPosition();
    int     ux, uy;
    m_grid->CalcUnscrolledPosition( pos.x, pos.y, &ux, &uy );
    int row = m_grid->YToRow( uy );
    int col = m_grid->XToCol( ux );


    if( row == wxNOT_FOUND || col == wxNOT_FOUND )
    {
        m_grid->GetGridWindow()->UnsetToolTip();
        return;
    }

    wxString rawValue = m_dataModel->GetValue( row, col );

    if( rawValue.Contains( wxT( "${" ) ) )
    {
        m_grid->GetGridWindow()->SetToolTip( rawValue );
    }
    else
    {
        m_grid->GetGridWindow()->UnsetToolTip();
    }
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnTableRangeSelected( wxGridRangeSelectEvent& aEvent )
{
    EESCHEMA_SETTINGS::PANEL_SYMBOL_FIELDS_TABLE& cfg = m_parent->eeconfig()->m_FieldEditorPanel;

    // Cross-probing should only work in Edit page
    if( m_nbPages->GetSelection() != 0 )
        return;

    // Multi-select can grab the rows that are expanded child refs, and also the row
    // containing the list of all child refs. Make sure we add refs/symbols uniquely
    std::set<SCH_REFERENCE> refs;
    std::set<SCH_ITEM*>     symbols;

    // This handler handles selecting and deselecting
    if( aEvent.Selecting() )
    {
        for( int i = aEvent.GetTopRow(); i <= aEvent.GetBottomRow(); i++ )
        {
            for( const SCH_REFERENCE& ref : m_dataModel->GetRowReferences( i ) )
                refs.insert( ref );
        }

        for( const SCH_REFERENCE& ref : refs )
            symbols.insert( ref.GetSymbol() );
    }

    if( cfg.selection_mode == 0 )
    {
        SCH_EDITOR_CONTROL* editor = m_parent->GetToolManager()->GetTool<SCH_EDITOR_CONTROL>();

        if( refs.size() > 0 )
        {
            // Use of full path based on UUID allows select of not yet annotated or duplicated
            // symbols
            wxString symbol_path = refs.begin()->GetFullPath();

            // Focus only handles one item at this time
            editor->FindSymbolAndItem( &symbol_path, nullptr, true, HIGHLIGHT_SYMBOL, wxEmptyString );
        }
        else
        {
            m_parent->ClearFocus();
        }
    }
    else if( cfg.selection_mode == 1 )
    {
        SCH_SELECTION_TOOL*    selTool = m_parent->GetToolManager()->GetTool<SCH_SELECTION_TOOL>();
        std::vector<SCH_ITEM*> items( symbols.begin(), symbols.end() );

        if( refs.size() > 0 )
            selTool->SyncSelection( refs.begin()->GetSheetPath(), nullptr, items );
        else
            selTool->ClearSelection();
    }
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnSizeViewControlsGrid( wxSizeEvent& event )
{
    const wxString& showColLabel = m_viewControlsGrid->GetColLabelValue( SHOW_FIELD_COLUMN );
    const wxString& groupByColLabel = m_viewControlsGrid->GetColLabelValue( GROUP_BY_COLUMN );
    int             showColWidth = KIUI::GetTextSize( showColLabel, m_viewControlsGrid ).x + COLUMN_MARGIN;
    int             groupByColWidth = KIUI::GetTextSize( groupByColLabel, m_viewControlsGrid ).x + COLUMN_MARGIN;
    int             remainingWidth = m_viewControlsGrid->GetSize().GetX() - showColWidth - groupByColWidth;

    m_viewControlsGrid->SetColSize( showColWidth, SHOW_FIELD_COLUMN );
    m_viewControlsGrid->SetColSize( groupByColWidth, GROUP_BY_COLUMN );

    if( m_viewControlsGrid->IsColShown( DISPLAY_NAME_COLUMN ) && m_viewControlsGrid->IsColShown( LABEL_COLUMN ) )
    {
        m_viewControlsGrid->SetColSize( DISPLAY_NAME_COLUMN, std::max( remainingWidth / 2, 60 ) );
        m_viewControlsGrid->SetColSize( LABEL_COLUMN, std::max( remainingWidth - ( remainingWidth / 2 ), 60 ) );
    }
    else if( m_viewControlsGrid->IsColShown( DISPLAY_NAME_COLUMN ) )
    {
        m_viewControlsGrid->SetColSize( DISPLAY_NAME_COLUMN, std::max( remainingWidth, 60 ) );
    }
    else if( m_viewControlsGrid->IsColShown( LABEL_COLUMN ) )
    {
        m_viewControlsGrid->SetColSize( LABEL_COLUMN, std::max( remainingWidth, 60 ) );
    }

    event.Skip();
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnSaveAndContinue( wxCommandEvent& aEvent )
{
    if( TransferDataFromWindow() )
    {
        m_parent->SaveProject();
        ClearModify();
    }
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnPageChanged( wxNotebookEvent& event )
{
    if( m_dataModel->GetColsCount() )
        PreviewRefresh();
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnPreviewRefresh( wxCommandEvent& event )
{
    PreviewRefresh();
    syncBomFmtPresetSelection();
}


void DIALOG_SYMBOL_FIELDS_TABLE::PreviewRefresh()
{
    bool saveIncludeExcudedFromBOM = m_dataModel->GetIncludeExcludedFromBOM();

    m_dataModel->SetIncludeExcludedFromBOM( false );
    m_dataModel->RebuildRows();

    m_textOutput->SetValue( m_dataModel->Export( GetCurrentBomFmtSettings() ) );

    if( saveIncludeExcudedFromBOM )
    {
        m_dataModel->SetIncludeExcludedFromBOM( true );
        m_dataModel->RebuildRows();
    }
}


BOM_FMT_PRESET DIALOG_SYMBOL_FIELDS_TABLE::GetCurrentBomFmtSettings()
{
    BOM_FMT_PRESET current;

    current.name = m_cbBomFmtPresets->GetStringSelection();
    current.fieldDelimiter = m_textFieldDelimiter->GetValue();
    current.stringDelimiter = m_textStringDelimiter->GetValue();
    current.refDelimiter = m_textRefDelimiter->GetValue();
    current.refRangeDelimiter = m_textRefRangeDelimiter->GetValue();
    current.keepTabs = m_checkKeepTabs->GetValue();
    current.keepLineBreaks = m_checkKeepLineBreaks->GetValue();

    return current;
}


void DIALOG_SYMBOL_FIELDS_TABLE::ShowEditTab()
{
    m_nbPages->SetSelection( 0 );
}


void DIALOG_SYMBOL_FIELDS_TABLE::ShowExportTab()
{
    m_nbPages->SetSelection( 1 );
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnOutputFileBrowseClicked( wxCommandEvent& event )
{
    // Build the absolute path of current output directory to preselect it in the file browser.
    wxString path = ExpandEnvVarSubstitutions( m_outputFileName->GetValue(), &Prj() );
    path = Prj().AbsolutePath( path );


    // Calculate the export filename
    wxFileName fn( Prj().AbsolutePath( m_parent->Schematic().GetFileName() ) );
    fn.SetExt( FILEEXT::CsvFileExtension );

    wxFileDialog saveDlg( this, _( "Bill of Materials Output File" ), path, fn.GetFullName(),
                          FILEEXT::CsvFileWildcard(), wxFD_SAVE | wxFD_OVERWRITE_PROMPT );

    KIPLATFORM::UI::AllowNetworkFileSystems( &saveDlg );

    if( saveDlg.ShowModal() == wxID_CANCEL )
        return;


    wxFileName file = wxFileName( saveDlg.GetPath() );
    wxString   defaultPath = fn.GetPathWithSep();

    if( IsOK( this, wxString::Format( _( "Do you want to use a path relative to\n'%s'?" ), defaultPath ) ) )
    {
        if( !file.MakeRelativeTo( defaultPath ) )
        {
            DisplayErrorMessage( this, _( "Cannot make path relative (target volume different from schematic "
                                          "file volume)!" ) );
        }
    }

    m_outputFileName->SetValue( file.GetFullPath() );
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnSidebarToggle( wxCommandEvent& event )
{
    EESCHEMA_SETTINGS::PANEL_SYMBOL_FIELDS_TABLE& cfg = m_parent->eeconfig()->m_FieldEditorPanel;

    if( cfg.sidebar_collapsed )
    {
        cfg.sidebar_collapsed = false;
        m_splitterMainWindow->SplitVertically( m_leftPanel, m_rightPanel, cfg.sash_pos );
    }
    else
    {
        cfg.sash_pos = m_splitterMainWindow->GetSashPosition();

        cfg.sidebar_collapsed = true;
        m_splitterMainWindow->Unsplit( m_leftPanel );
    }

    setSideBarButtonLook( cfg.sidebar_collapsed );
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnExport( wxCommandEvent& aEvent )
{
    if( m_dataModel->IsEdited() )
    {
        if( OKOrCancelDialog( nullptr, _( "Unsaved data" ),
                              _( "Changes have not yet been saved. Export unsaved data?" ), "",
                              _( "OK" ), _( "Cancel" ) )
            == wxID_CANCEL )
        {
            return;
        }
    }

    // Create output directory if it does not exist (also transform it in absolute form).
    // Bail if it fails.

    std::function<bool( wxString* )> textResolver =
            [&]( wxString* token ) -> bool
            {
                SCHEMATIC& schematic = m_parent->Schematic();

                // Handles m_board->GetTitleBlock() *and* m_board->GetProject()
                return schematic.ResolveTextVar( &schematic.CurrentSheet(), token, 0 );
            };

    wxString path = m_outputFileName->GetValue();

    if( path.IsEmpty() )
    {
        DisplayError( this, _( "No output file specified in Export tab." ) );
        return;
    }

    path = ExpandTextVars( path, &textResolver );
    path = ExpandEnvVarSubstitutions( path, &Prj() );

    wxFileName outputFile = wxFileName::FileName( path );
    wxString msg;

    if( !EnsureFileDirectoryExists( &outputFile, Prj().AbsolutePath( m_parent->Schematic().GetFileName() ),
                                    &NULL_REPORTER::GetInstance() ) )
    {
        msg.Printf( _( "Could not open/create path '%s'." ), outputFile.GetPath() );
        DisplayError( this, msg );
        return;
    }

    wxFFile out( outputFile.GetFullPath(), "wb" );

    if( !out.IsOpened() )
    {
        msg.Printf( _( "Could not create BOM output '%s'." ), outputFile.GetFullPath() );
        DisplayError( this, msg );
        return;
    }

    PreviewRefresh();

    if( !out.Write( m_textOutput->GetValue() ) )
    {
        msg.Printf( _( "Could not write BOM output '%s'." ), outputFile.GetFullPath() );
        DisplayError( this, msg );
        return;
    }

    // close the file before we tell the user it's done with the info modal :workflow meme:
    out.Close();
    msg.Printf( _( "Wrote BOM output to '%s'" ), outputFile.GetFullPath() );
    DisplayInfoMessage( this, msg );
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnCancel( wxCommandEvent& aEvent )
{
    if( m_job )
        EndModal( wxID_CANCEL );
    else
        Close();
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnOk( wxCommandEvent& aEvent )
{
    TransferDataFromWindow();

    if( m_job )
    {
        m_job->SetConfiguredOutputPath( m_outputFileName->GetValue() );

        if( m_currentBomFmtPreset )
            m_job->m_bomFmtPresetName = m_currentBomFmtPreset->name;
        else
            m_job->m_bomFmtPresetName = wxEmptyString;

        if( m_currentBomPreset )
            m_job->m_bomPresetName = m_currentBomPreset->name;
        else
            m_job->m_bomPresetName = wxEmptyString;

        BOM_FMT_PRESET fmtSettings = GetCurrentBomFmtSettings();
        m_job->m_fieldDelimiter = fmtSettings.fieldDelimiter;
        m_job->m_stringDelimiter = fmtSettings.stringDelimiter;
        m_job->m_refDelimiter = fmtSettings.refDelimiter;
        m_job->m_refRangeDelimiter = fmtSettings.refRangeDelimiter;
        m_job->m_keepTabs = fmtSettings.keepTabs;
        m_job->m_keepLineBreaks = fmtSettings.keepLineBreaks;

        BOM_PRESET presetFields = m_dataModel->GetBomSettings();
        m_job->m_sortAsc = presetFields.sortAsc;
        m_job->m_excludeDNP = presetFields.excludeDNP;
        m_job->m_filterString = presetFields.filterString;
        m_job->m_sortField = presetFields.sortField;
        m_job->m_groupSymbols = presetFields.groupSymbols;

        m_job->m_fieldsOrdered.clear();
        m_job->m_fieldsLabels.clear();
        m_job->m_fieldsGroupBy.clear();

        for( const BOM_FIELD& modelField : m_dataModel->GetFieldsOrdered() )
        {
            if( modelField.show )
                m_job->m_fieldsOrdered.emplace_back( modelField.name );
            else
                m_job->m_fieldsOrdered.emplace_back( wxT( "__" ) + modelField.name );

            m_job->m_fieldsLabels.emplace_back( modelField.label );

            if( modelField.groupBy )
                m_job->m_fieldsGroupBy.emplace_back( modelField.name );
        }

        wxString selectedVariant = getSelectedVariant();

        if( !selectedVariant.IsEmpty() )
            m_job->m_variantNames.push_back( selectedVariant );

        EndModal( wxID_OK );
    }
    else
    {
        Close();
    }
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnClose( wxCloseEvent& aEvent )
{
    if( m_job )
    {
        aEvent.Skip();
        return;
    }

    m_grid->CommitPendingChanges( true );

    if( m_dataModel->IsEdited() && aEvent.CanVeto() )
    {
        if( !HandleUnsavedChanges( this, _( "Save changes?" ),
                                   [&]() -> bool
                                   {
                                       return TransferDataFromWindow();
                                   } ) )
        {
            aEvent.Veto();
            return;
        }
    }

    // Stop listening to schematic events
    m_parent->Schematic().RemoveListener( this );
    m_parent->ClearFocus();

    wxCommandEvent* evt = new wxCommandEvent( EDA_EVT_CLOSE_DIALOG_SYMBOL_FIELDS_TABLE, wxID_ANY );

    if( wxWindow* parent = GetParent() )
        wxQueueEvent( parent, evt );
}


std::vector<BOM_PRESET> DIALOG_SYMBOL_FIELDS_TABLE::GetUserBomPresets() const
{
    std::vector<BOM_PRESET> ret;

    for( const std::pair<const wxString, BOM_PRESET>& pair : m_bomPresets )
    {
        if( !pair.second.readOnly )
            ret.emplace_back( pair.second );
    }

    return ret;
}


void DIALOG_SYMBOL_FIELDS_TABLE::SetUserBomPresets( std::vector<BOM_PRESET>& aPresetList )
{
    // Reset to defaults
    loadDefaultBomPresets();

    for( const BOM_PRESET& preset : aPresetList )
    {
        if( m_bomPresets.count( preset.name ) )
            continue;

        m_bomPresets[preset.name] = preset;

        m_bomPresetMRU.Add( preset.name );
    }

    rebuildBomPresetsWidget();
}


void DIALOG_SYMBOL_FIELDS_TABLE::ApplyBomPreset( const wxString& aPresetName )
{
    updateBomPresetSelection( aPresetName );

    wxCommandEvent dummy;
    onBomPresetChanged( dummy );
}


void DIALOG_SYMBOL_FIELDS_TABLE::ApplyBomPreset( const BOM_PRESET& aPreset )
{
    if( m_bomPresets.count( aPreset.name ) )
        m_currentBomPreset = &m_bomPresets[aPreset.name];
    else
        m_currentBomPreset = nullptr;

    if( m_currentBomPreset && !m_currentBomPreset->readOnly )
        m_lastSelectedBomPreset = m_currentBomPreset;
    else
        m_lastSelectedBomPreset = nullptr;

    updateBomPresetSelection( aPreset.name );
    doApplyBomPreset( aPreset );
}


void DIALOG_SYMBOL_FIELDS_TABLE::loadDefaultBomPresets()
{
    m_bomPresets.clear();
    m_bomPresetMRU.clear();

    // Load the read-only defaults
    for( const BOM_PRESET& preset : BOM_PRESET::BuiltInPresets() )
    {
        m_bomPresets[preset.name] = preset;
        m_bomPresets[preset.name].readOnly = true;

        m_bomPresetMRU.Add( preset.name );
    }
}


void DIALOG_SYMBOL_FIELDS_TABLE::rebuildBomPresetsWidget()
{
    m_cbBomPresets->Clear();

    int idx = 0;
    int default_idx = 0;

    for( const auto& [presetName, preset] : m_bomPresets )
    {
        m_cbBomPresets->Append( wxGetTranslation( presetName ), (void*) &preset );

        if( presetName == BOM_PRESET::DefaultEditing().name )
            default_idx = idx;

        idx++;
    }

    m_cbBomPresets->Append( wxT( "---" ) );
    m_cbBomPresets->Append( _( "Save preset..." ) );
    m_cbBomPresets->Append( _( "Delete preset..." ) );

    // At least the built-in presets should always be present
    wxASSERT( !m_bomPresets.empty() );

    m_cbBomPresets->SetSelection( default_idx );
    m_currentBomPreset = static_cast<BOM_PRESET*>( m_cbBomPresets->GetClientData( default_idx ) );
}


void DIALOG_SYMBOL_FIELDS_TABLE::syncBomPresetSelection()
{
    BOM_PRESET current = m_dataModel->GetBomSettings();

    auto it = std::find_if( m_bomPresets.begin(), m_bomPresets.end(),
            [&]( const std::pair<const wxString, BOM_PRESET>& aPair )
            {
                const BOM_PRESET& preset = aPair.second;

                // Check the simple settings first
                if( !( preset.sortAsc == current.sortAsc
                       && preset.filterString == current.filterString
                       && preset.groupSymbols == current.groupSymbols
                       && preset.excludeDNP == current.excludeDNP
                       && preset.includeExcludedFromBOM == current.includeExcludedFromBOM ) )
                {
                    return false;
                }

                // We should compare preset.name and current.name.  Unfortunately current.name is
                // empty because m_dataModel->GetBomSettings() does not store the .name member.
                // So use sortField member as a (not very efficient) auxiliary filter.
                // As a further complication, sortField can be translated in m_bomPresets list, so
                // current.sortField needs to be translated.
                // Probably this not efficient and error prone test should be removed (JPC).
                if( preset.sortField != wxGetTranslation( current.sortField ) )
                    return false;

                // Only compare shown or grouped fields
                std::vector<BOM_FIELD> A, B;

                for( const BOM_FIELD& field : preset.fieldsOrdered )
                {
                    if( field.show || field.groupBy )
                        A.emplace_back( field );
                }

                for( const BOM_FIELD& field : current.fieldsOrdered )
                {
                    if( field.show || field.groupBy )
                        B.emplace_back( field );
                }

                return A == B;
            } );

    if( it != m_bomPresets.end() )
    {
        // Select the right m_cbBomPresets item.
        // but these items are translated if they are predefined items.
        bool     do_translate = it->second.readOnly;
        wxString text = do_translate ? wxGetTranslation( it->first ) : it->first;
        m_cbBomPresets->SetStringSelection( text );
    }
    else
    {
        m_cbBomPresets->SetSelection( m_cbBomPresets->GetCount() - 3 ); // separator
    }

    m_currentBomPreset = static_cast<BOM_PRESET*>( m_cbBomPresets->GetClientData( m_cbBomPresets->GetSelection() ) );
}


void DIALOG_SYMBOL_FIELDS_TABLE::updateBomPresetSelection( const wxString& aName )
{
    // Look at m_userBomPresets to know if aName is a read only preset, or a user preset.
    // Read-only presets have translated names in UI, so we have to use a translated name
    // in UI selection.  But for a user preset name we search for the untranslated aName.
    wxString ui_label = aName;

    for( const auto& [presetName, preset] : m_bomPresets )
    {
        if( presetName == aName )
        {
            if( preset.readOnly == true )
                ui_label = wxGetTranslation( aName );

            break;
        }
    }

    int idx = m_cbBomPresets->FindString( ui_label );

    if( idx >= 0 && m_cbBomPresets->GetSelection() != idx )
    {
        m_cbBomPresets->SetSelection( idx );
        m_currentBomPreset = static_cast<BOM_PRESET*>( m_cbBomPresets->GetClientData( idx ) );
    }
    else if( idx < 0 )
    {
        m_cbBomPresets->SetSelection( m_cbBomPresets->GetCount() - 3 ); // separator
    }
}


void DIALOG_SYMBOL_FIELDS_TABLE::onBomPresetChanged( wxCommandEvent& aEvent )
{
    int count = m_cbBomPresets->GetCount();
    int index = m_cbBomPresets->GetSelection();

    auto resetSelection =
            [&]()
            {
                if( m_currentBomPreset )
                    m_cbBomPresets->SetStringSelection( m_currentBomPreset->name );
                else
                    m_cbBomPresets->SetSelection( m_cbBomPresets->GetCount() - 3 );
            };

    if( index == count - 3 )
    {
        // Separator: reject the selection
        resetSelection();
        return;
    }
    else if( index == count - 2 )
    {
        // Save current state to new preset
        wxString name;

        if( m_lastSelectedBomPreset )
            name = m_lastSelectedBomPreset->name;

        wxTextEntryDialog dlg( this, _( "BOM preset name:" ), _( "Save BOM Preset" ), name );

        if( dlg.ShowModal() != wxID_OK )
        {
            resetSelection();
            return;
        }

        name = dlg.GetValue();
        bool exists = m_bomPresets.count( name );

        if( !exists )
        {
            m_bomPresets[name] = m_dataModel->GetBomSettings();
            m_bomPresets[name].readOnly = false;
            m_bomPresets[name].name = name;
        }

        BOM_PRESET* preset = &m_bomPresets[name];

        if( !exists )
        {
            index = m_cbBomPresets->Insert( name, index - 1, static_cast<void*>( preset ) );
        }
        else if( preset->readOnly )
        {
            wxMessageBox( _( "Default presets cannot be modified.\nPlease use a different name." ),
                          _( "Error" ), wxOK | wxICON_ERROR, this );
            resetSelection();
            return;
        }
        else
        {
            // Ask the user if they want to overwrite the existing preset
            if( !IsOK( this, _( "Overwrite existing preset?" ) ) )
            {
                resetSelection();
                return;
            }

            *preset = m_dataModel->GetBomSettings();
            preset->name = name;

            index = m_cbBomPresets->FindString( name );

            if( m_bomPresetMRU.Index( name ) != wxNOT_FOUND )
                m_bomPresetMRU.Remove( name );
        }

        m_currentBomPreset = preset;
        m_cbBomPresets->SetSelection( index );
        m_bomPresetMRU.Insert( name, 0 );

        return;
    }
    else if( index == count - 1 )
    {
        // Delete a preset
        wxArrayString              headers;
        std::vector<wxArrayString> items;

        headers.Add( _( "Presets" ) );

        for( const auto& [name, preset] : m_bomPresets )
        {
            if( !preset.readOnly )
            {
                wxArrayString item;
                item.Add( name );
                items.emplace_back( item );
            }
        }

        EDA_LIST_DIALOG dlg( this, _( "Delete Preset" ), headers, items );
        dlg.SetListLabel( _( "Select preset:" ) );

        if( dlg.ShowModal() == wxID_OK )
        {
            wxString presetName = dlg.GetTextSelection();
            int      idx = m_cbBomPresets->FindString( presetName );

            if( idx != wxNOT_FOUND )
            {
                m_bomPresets.erase( presetName );

                m_cbBomPresets->Delete( idx );
                m_currentBomPreset = nullptr;
            }

            if( m_bomPresetMRU.Index( presetName ) != wxNOT_FOUND )
                m_bomPresetMRU.Remove( presetName );
        }

        resetSelection();
        return;
    }

    BOM_PRESET* preset = static_cast<BOM_PRESET*>( m_cbBomPresets->GetClientData( index ) );
    m_currentBomPreset = preset;

    m_lastSelectedBomPreset = ( !preset || preset->readOnly ) ? nullptr : preset;

    if( preset )
    {
        doApplyBomPreset( *preset );
        syncBomPresetSelection();
        m_currentBomPreset = preset;

        if( !m_currentBomPreset->name.IsEmpty() )
        {
            if( m_bomPresetMRU.Index( preset->name ) != wxNOT_FOUND )
                m_bomPresetMRU.Remove( preset->name );

            m_bomPresetMRU.Insert( preset->name, 0 );
        }
    }
}


void DIALOG_SYMBOL_FIELDS_TABLE::doApplyBomPreset( const BOM_PRESET& aPreset )
{
    // Disable rebuilds while we're applying the preset otherwise we'll be
    // rebuilding the model constantly while firing off wx events
    m_dataModel->DisableRebuilds();

    // Basically, we apply the BOM preset to the data model and then
    // update our UI to reflect resulting the data model state, not the preset.
    m_dataModel->ApplyBomPreset( aPreset, m_parent->Schematic().GetCurrentVariant() );

    // BOM Presets can add, but not remove, columns, so make sure the view controls
    // grid has all of them before starting
    for( int i = 0; i < m_dataModel->GetColsCount(); i++ )
    {
        const wxString& fieldName( m_dataModel->GetColFieldName( i ) );
        bool            found = false;

        for( int j = 0; j < m_viewControlsDataModel->GetNumberRows(); j++ )
        {
            if( m_viewControlsDataModel->GetCanonicalFieldName( j ) == fieldName )
            {
                found = true;
                break;
            }
        }

        // Properties like label, etc. will be added in the next loop
        if( !found )
            AddField( fieldName, GetGeneratedFieldDisplayName( fieldName ), false, false );
    }

    // Sync all fields
    for( int i = 0; i < m_viewControlsDataModel->GetNumberRows(); i++ )
    {
        const wxString& fieldName( m_viewControlsDataModel->GetCanonicalFieldName( i ) );
        int             col = m_dataModel->GetFieldNameCol( fieldName );

        if( col == -1 )
        {
            wxASSERT_MSG( true, "Fields control has a field not found in the data model." );
            continue;
        }

        EESCHEMA_SETTINGS* cfg = m_parent->eeconfig();
        std::string        fieldNameStr( fieldName.ToUTF8() );

        // Set column labels
        const wxString& label = m_dataModel->GetColLabelValue( col );
        m_viewControlsDataModel->SetValue( i, LABEL_COLUMN, label );
        m_grid->SetColLabelValue( col, label );

        if( cfg->m_FieldEditorPanel.field_widths.count( fieldNameStr ) )
            m_grid->SetColSize( col, cfg->m_FieldEditorPanel.field_widths.at( fieldNameStr ) );

        // Set shown columns
        bool show = m_dataModel->GetShowColumn( col );
        m_viewControlsDataModel->SetValueAsBool( i, SHOW_FIELD_COLUMN, show );

        if( show )
            m_grid->ShowCol( col );
        else
            m_grid->HideCol( col );

        // Set grouped columns
        bool groupBy = m_dataModel->GetGroupColumn( col );
        m_viewControlsDataModel->SetValueAsBool( i, GROUP_BY_COLUMN, groupBy );
    }

    m_grid->SetSortingColumn( m_dataModel->GetSortCol(), m_dataModel->GetSortAsc() );
    m_groupSymbolsBox->SetValue( m_dataModel->GetGroupingEnabled() );
    m_filter->ChangeValue( m_dataModel->GetFilter() );

    SetupAllColumnProperties();

    // This will rebuild all rows and columns in the model such that the order
    // and labels are right, then we refresh the shown grid data to match
    m_dataModel->EnableRebuilds();
    m_dataModel->RebuildRows();

    if( m_nbPages->GetSelection() == 1 )
        PreviewRefresh();
    else
        m_grid->ForceRefresh();
}


std::vector<BOM_FMT_PRESET> DIALOG_SYMBOL_FIELDS_TABLE::GetUserBomFmtPresets() const
{
    std::vector<BOM_FMT_PRESET> ret;

    for( const auto& [name, preset] : m_bomFmtPresets )
    {
        if( !preset.readOnly )
            ret.emplace_back( preset );
    }

    return ret;
}


void DIALOG_SYMBOL_FIELDS_TABLE::SetUserBomFmtPresets( std::vector<BOM_FMT_PRESET>& aPresetList )
{
    // Reset to defaults
    loadDefaultBomFmtPresets();

    for( const BOM_FMT_PRESET& preset : aPresetList )
    {
        if( m_bomFmtPresets.count( preset.name ) )
            continue;

        m_bomFmtPresets[preset.name] = preset;

        m_bomFmtPresetMRU.Add( preset.name );
    }

    rebuildBomFmtPresetsWidget();
}


void DIALOG_SYMBOL_FIELDS_TABLE::ApplyBomFmtPreset( const wxString& aPresetName )
{
    updateBomFmtPresetSelection( aPresetName );

    wxCommandEvent dummy;
    onBomFmtPresetChanged( dummy );
}


void DIALOG_SYMBOL_FIELDS_TABLE::ApplyBomFmtPreset( const BOM_FMT_PRESET& aPreset )
{
    m_currentBomFmtPreset = nullptr;
    m_lastSelectedBomFmtPreset = nullptr;

    if( m_bomFmtPresets.count( aPreset.name ) )
        m_currentBomFmtPreset = &m_bomFmtPresets[aPreset.name];

    if( m_currentBomFmtPreset && !m_currentBomFmtPreset->readOnly )
        m_lastSelectedBomFmtPreset =m_currentBomFmtPreset;

    updateBomFmtPresetSelection( aPreset.name );
    doApplyBomFmtPreset( aPreset );
}


void DIALOG_SYMBOL_FIELDS_TABLE::loadDefaultBomFmtPresets()
{
    m_bomFmtPresets.clear();
    m_bomFmtPresetMRU.clear();

    // Load the read-only defaults
    for( const BOM_FMT_PRESET& preset : BOM_FMT_PRESET::BuiltInPresets() )
    {
        m_bomFmtPresets[preset.name] = preset;
        m_bomFmtPresets[preset.name].readOnly = true;

        m_bomFmtPresetMRU.Add( preset.name );
    }
}


void DIALOG_SYMBOL_FIELDS_TABLE::rebuildBomFmtPresetsWidget()
{
    m_cbBomFmtPresets->Clear();

    int idx = 0;
    int default_idx = 0;

    for( const auto& [presetName, preset] : m_bomFmtPresets )
    {
        m_cbBomFmtPresets->Append( wxGetTranslation( presetName ), (void*) &preset );

        if( presetName == BOM_FMT_PRESET::CSV().name )
            default_idx = idx;

        idx++;
    }

    m_cbBomFmtPresets->Append( wxT( "---" ) );
    m_cbBomFmtPresets->Append( _( "Save preset..." ) );
    m_cbBomFmtPresets->Append( _( "Delete preset..." ) );

    // At least the built-in presets should always be present
    wxASSERT( !m_bomFmtPresets.empty() );

    m_cbBomFmtPresets->SetSelection( default_idx );
    m_currentBomFmtPreset = static_cast<BOM_FMT_PRESET*>( m_cbBomFmtPresets->GetClientData( default_idx ) );
}


void DIALOG_SYMBOL_FIELDS_TABLE::syncBomFmtPresetSelection()
{
    BOM_FMT_PRESET current = GetCurrentBomFmtSettings();

    auto it = std::find_if( m_bomFmtPresets.begin(), m_bomFmtPresets.end(),
                            [&]( const std::pair<const wxString, BOM_FMT_PRESET>& aPair )
                            {
                                return ( aPair.second.fieldDelimiter == current.fieldDelimiter
                                         && aPair.second.stringDelimiter == current.stringDelimiter
                                         && aPair.second.refDelimiter == current.refDelimiter
                                         && aPair.second.refRangeDelimiter == current.refRangeDelimiter
                                         && aPair.second.keepTabs == current.keepTabs
                                         && aPair.second.keepLineBreaks == current.keepLineBreaks );
                            } );

    if( it != m_bomFmtPresets.end() )
    {
        // Select the right m_cbBomFmtPresets item.
        // but these items are translated if they are predefined items.
        bool     do_translate = it->second.readOnly;
        wxString text = do_translate ? wxGetTranslation( it->first ) : it->first;

        m_cbBomFmtPresets->SetStringSelection( text );
    }
    else
    {
        m_cbBomFmtPresets->SetSelection( m_cbBomFmtPresets->GetCount() - 3 ); // separator
    }

    int idx = m_cbBomFmtPresets->GetSelection();
    m_currentBomFmtPreset = static_cast<BOM_FMT_PRESET*>( m_cbBomFmtPresets->GetClientData( idx ) );
}


void DIALOG_SYMBOL_FIELDS_TABLE::updateBomFmtPresetSelection( const wxString& aName )
{
    // look at m_userBomFmtPresets to know if aName is a read only preset, or a user preset.
    // Read only presets have translated names in UI, so we have to use a translated name in UI selection.
    // But for a user preset name we should search for aName (not translated)
    wxString ui_label = aName;

    for( const auto& [presetName, preset] : m_bomFmtPresets )
    {
        if( presetName == aName )
        {
            if( preset.readOnly )
                ui_label = wxGetTranslation( aName );

            break;
        }
    }

    int idx = m_cbBomFmtPresets->FindString( ui_label );

    if( idx >= 0 && m_cbBomFmtPresets->GetSelection() != idx )
    {
        m_cbBomFmtPresets->SetSelection( idx );
        m_currentBomFmtPreset = static_cast<BOM_FMT_PRESET*>( m_cbBomFmtPresets->GetClientData( idx ) );
    }
    else if( idx < 0 )
    {
        m_cbBomFmtPresets->SetSelection( m_cbBomFmtPresets->GetCount() - 3 ); // separator
    }
}


void DIALOG_SYMBOL_FIELDS_TABLE::onBomFmtPresetChanged( wxCommandEvent& aEvent )
{
    int count = m_cbBomFmtPresets->GetCount();
    int index = m_cbBomFmtPresets->GetSelection();

    auto resetSelection =
            [&]()
            {
                if( m_currentBomFmtPreset )
                    m_cbBomFmtPresets->SetStringSelection( m_currentBomFmtPreset->name );
                else
                    m_cbBomFmtPresets->SetSelection( m_cbBomFmtPresets->GetCount() - 3 );
            };

    if( index == count - 3 )
    {
        // Separator: reject the selection
        resetSelection();
        return;
    }
    else if( index == count - 2 )
    {
        // Save current state to new preset
        wxString name;

        if( m_lastSelectedBomFmtPreset )
            name = m_lastSelectedBomFmtPreset->name;

        wxTextEntryDialog dlg( this, _( "BOM preset name:" ), _( "Save BOM Preset" ), name );

        if( dlg.ShowModal() != wxID_OK )
        {
            resetSelection();
            return;
        }

        name = dlg.GetValue();
        bool exists = m_bomFmtPresets.count( name );

        if( !exists )
        {
            m_bomFmtPresets[name] = GetCurrentBomFmtSettings();
            m_bomFmtPresets[name].readOnly = false;
            m_bomFmtPresets[name].name = name;
        }

        BOM_FMT_PRESET* preset = &m_bomFmtPresets[name];

        if( !exists )
        {
            index = m_cbBomFmtPresets->Insert( name, index - 1, static_cast<void*>( preset ) );
        }
        else if( preset->readOnly )
        {
            wxMessageBox( _( "Default presets cannot be modified.\nPlease use a different name." ),
                          _( "Error" ), wxOK | wxICON_ERROR, this );
            resetSelection();
            return;
        }
        else
        {
            // Ask the user if they want to overwrite the existing preset
            if( !IsOK( this, _( "Overwrite existing preset?" ) ) )
            {
                resetSelection();
                return;
            }

            *preset = GetCurrentBomFmtSettings();
            preset->name = name;

            index = m_cbBomFmtPresets->FindString( name );

            if( m_bomFmtPresetMRU.Index( name ) != wxNOT_FOUND )
                m_bomFmtPresetMRU.Remove( name );
        }

        m_currentBomFmtPreset = preset;
        m_cbBomFmtPresets->SetSelection( index );
        m_bomFmtPresetMRU.Insert( name, 0 );

        return;
    }
    else if( index == count - 1 )
    {
        // Delete a preset
        wxArrayString              headers;
        std::vector<wxArrayString> items;

        headers.Add( _( "Presets" ) );

        for( std::pair<const wxString, BOM_FMT_PRESET>& pair : m_bomFmtPresets )
        {
            if( !pair.second.readOnly )
            {
                wxArrayString item;
                item.Add( pair.first );
                items.emplace_back( item );
            }
        }

        EDA_LIST_DIALOG dlg( this, _( "Delete Preset" ), headers, items );
        dlg.SetListLabel( _( "Select preset:" ) );

        if( dlg.ShowModal() == wxID_OK )
        {
            wxString presetName = dlg.GetTextSelection();
            int      idx = m_cbBomFmtPresets->FindString( presetName );

            if( idx != wxNOT_FOUND )
            {
                m_bomFmtPresets.erase( presetName );

                m_cbBomFmtPresets->Delete( idx );
                m_currentBomFmtPreset = nullptr;
            }

            if( m_bomFmtPresetMRU.Index( presetName ) != wxNOT_FOUND )
                m_bomFmtPresetMRU.Remove( presetName );
        }

        resetSelection();
        return;
    }

    auto* preset = static_cast<BOM_FMT_PRESET*>( m_cbBomFmtPresets->GetClientData( index ) );
    m_currentBomFmtPreset = preset;

    m_lastSelectedBomFmtPreset = ( !preset || preset->readOnly ) ? nullptr : preset;

    if( preset )
    {
        doApplyBomFmtPreset( *preset );
        syncBomFmtPresetSelection();
        m_currentBomFmtPreset = preset;

        if( !m_currentBomFmtPreset->name.IsEmpty() )
        {
            if( m_bomFmtPresetMRU.Index( preset->name ) != wxNOT_FOUND )
                m_bomFmtPresetMRU.Remove( preset->name );

            m_bomFmtPresetMRU.Insert( preset->name, 0 );
        }
    }
}


void DIALOG_SYMBOL_FIELDS_TABLE::doApplyBomFmtPreset( const BOM_FMT_PRESET& aPreset )
{
    m_textFieldDelimiter->ChangeValue( aPreset.fieldDelimiter );
    m_textStringDelimiter->ChangeValue( aPreset.stringDelimiter );
    m_textRefDelimiter->ChangeValue( aPreset.refDelimiter );
    m_textRefRangeDelimiter->ChangeValue( aPreset.refRangeDelimiter );
    m_checkKeepTabs->SetValue( aPreset.keepTabs );
    m_checkKeepLineBreaks->SetValue( aPreset.keepLineBreaks );

    // Refresh the preview if that's the current page
    if( m_nbPages->GetSelection() == 1 )
        PreviewRefresh();
}


void DIALOG_SYMBOL_FIELDS_TABLE::savePresetsToSchematic()
{
    bool modified = false;

    // Save our BOM presets
    std::vector<BOM_PRESET> presets;

    for( const auto& [name, preset] : m_bomPresets )
    {
        if( !preset.readOnly )
            presets.emplace_back( preset );
    }

    if( m_schSettings.m_BomPresets != presets )
    {
        modified = true;
        m_schSettings.m_BomPresets = presets;
    }

    if( m_schSettings.m_BomSettings != m_dataModel->GetBomSettings() && !m_job )
    {
        modified = true;
        m_schSettings.m_BomSettings = m_dataModel->GetBomSettings();
    }

    // Save our BOM Format presets
    std::vector<BOM_FMT_PRESET> fmts;

    for( const auto& [name, preset] : m_bomFmtPresets )
    {
        if( !preset.readOnly )
            fmts.emplace_back( preset );
    }

    if( m_schSettings.m_BomFmtPresets != fmts )
    {
        modified = true;
        m_schSettings.m_BomFmtPresets = fmts;
    }

    if( m_schSettings.m_BomFmtSettings != GetCurrentBomFmtSettings() && !m_job )
    {
        modified = true;
        m_schSettings.m_BomFmtSettings = GetCurrentBomFmtSettings();
    }

    if( modified )
        m_parent->OnModify();
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnSchItemsAdded( SCHEMATIC& aSch, std::vector<SCH_ITEM*>& aSchItem )
{
    std::set<wxString> savedSelection = SaveGridSelection();

    SCH_REFERENCE_LIST allRefs;
    m_parent->Schematic().Hierarchy().GetSymbols( allRefs, SYMBOL_FILTER_ALL );

    for( SCH_ITEM* item : aSchItem )
    {
        if( item->Type() == SCH_SYMBOL_T )
        {
            SCH_SYMBOL* symbol = static_cast<SCH_SYMBOL*>( item );

            // Don't add power symbols
            if( !symbol->IsMissingLibSymbol() && symbol->IsPower() )
                continue;

            // Add all fields again in case this symbol has a new one
            for( SCH_FIELD& field : symbol->GetFields() )
                AddField( field.GetCanonicalName(), field.GetName(), true, false, true );

            m_dataModel->AddReferences( getSymbolReferences( symbol, allRefs ) );
        }
        else if( item->Type() == SCH_SHEET_T )
        {
            std::set<SCH_SYMBOL*> symbols;
            SCH_REFERENCE_LIST refs = getSheetSymbolReferences( *static_cast<SCH_SHEET*>( item ) );

            for( SCH_REFERENCE& ref : refs )
                symbols.insert( ref.GetSymbol() );

            for( SCH_SYMBOL* symbol : symbols )
            {
                // Add all fields again in case this symbol has a new one
                for( SCH_FIELD& field : symbol->GetFields() )
                    AddField( field.GetCanonicalName(), field.GetName(), true, false, true );
            }

            m_dataModel->AddReferences( refs );
        }
    }

    DisableSelectionEvents();
    m_dataModel->RebuildRows();
    RestoreGridSelection( savedSelection );
    EnableSelectionEvents();
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnSchItemsRemoved( SCHEMATIC& aSch, std::vector<SCH_ITEM*>& aSchItem )
{
    std::set<wxString> savedSelection = SaveGridSelection();

    for( SCH_ITEM* item : aSchItem )
    {
        if( item->Type() == SCH_SYMBOL_T )
            m_dataModel->RemoveSymbol( *static_cast<SCH_SYMBOL*>( item ) );
        else if( item->Type() == SCH_SHEET_T )
            m_dataModel->RemoveReferences( getSheetSymbolReferences( *static_cast<SCH_SHEET*>( item ) ) );
    }

    DisableSelectionEvents();
    m_dataModel->RebuildRows();
    RestoreGridSelection( savedSelection );
    EnableSelectionEvents();
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnSchItemsChanged( SCHEMATIC& aSch, std::vector<SCH_ITEM*>& aSchItem )
{
    std::set<wxString> savedSelection = SaveGridSelection();

    SCH_REFERENCE_LIST allRefs;
    m_parent->Schematic().Hierarchy().GetSymbols( allRefs, SYMBOL_FILTER_ALL );

    for( SCH_ITEM* item : aSchItem )
    {
        if( item->Type() == SCH_SYMBOL_T )
        {
            SCH_SYMBOL* symbol = static_cast<SCH_SYMBOL*>( item );

            // Don't add power symbols
            if( !symbol->IsMissingLibSymbol() && symbol->IsPower() )
                continue;

            // Add all fields again in case this symbol has a new one
            for( SCH_FIELD& field : symbol->GetFields() )
                AddField( field.GetCanonicalName(), field.GetName(), true, false, true );

            m_dataModel->UpdateReferences( getSymbolReferences( symbol, allRefs ),
                                           m_parent->Schematic().GetCurrentVariant() );
        }
        else if( item->Type() == SCH_SHEET_T )
        {
            std::set<SCH_SYMBOL*> symbols;
            SCH_REFERENCE_LIST refs = getSheetSymbolReferences( *static_cast<SCH_SHEET*>( item ) );

            for( SCH_REFERENCE& ref : refs )
                symbols.insert( ref.GetSymbol() );

            for( SCH_SYMBOL* symbol : symbols )
            {
                // Add all fields again in case this symbol has a new one
                for( SCH_FIELD& field : symbol->GetFields() )
                    AddField( field.GetCanonicalName(), field.GetName(), true, false, true );
            }

            m_dataModel->UpdateReferences( refs, m_parent->Schematic().GetCurrentVariant() );
        }
    }

    DisableSelectionEvents();
    m_dataModel->RebuildRows();
    RestoreGridSelection( savedSelection );
    EnableSelectionEvents();
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnSchSheetChanged( SCHEMATIC& aSch )
{
    m_dataModel->SetPath( aSch.CurrentSheet() );

    if( m_dataModel->GetScope() != FIELDS_EDITOR_GRID_DATA_MODEL::SCOPE::SCOPE_ALL )
    {
        std::set<wxString> savedSelection = SaveGridSelection();

        DisableSelectionEvents();
        m_dataModel->RebuildRows();
        RestoreGridSelection( savedSelection );
        EnableSelectionEvents();
    }
}


void DIALOG_SYMBOL_FIELDS_TABLE::OnSchCurrentVariantChanged( SCHEMATIC& aSch )
{
    if( m_syncingVariantSelection )
        return;

    m_variantListBox->Set( aSch.GetVariantNamesForUI() );
    syncVariantSelection( aSch.GetCurrentVariant(), false );
}


void DIALOG_SYMBOL_FIELDS_TABLE::EnableSelectionEvents()
{
    m_grid->Connect( wxEVT_GRID_RANGE_SELECTED,
                     wxGridRangeSelectEventHandler( DIALOG_SYMBOL_FIELDS_TABLE::OnTableRangeSelected ),
                     nullptr, this );
}


void DIALOG_SYMBOL_FIELDS_TABLE::DisableSelectionEvents()
{
    m_grid->Disconnect( wxEVT_GRID_RANGE_SELECTED,
                        wxGridRangeSelectEventHandler( DIALOG_SYMBOL_FIELDS_TABLE::OnTableRangeSelected ),
                        nullptr, this );
}


std::set<wxString> DIALOG_SYMBOL_FIELDS_TABLE::SaveGridSelection()
{
    std::set<wxString> selectedFullPaths;

    wxGridCellCoordsArray topLeft = m_grid->GetSelectionBlockTopLeft();
    wxGridCellCoordsArray bottomRight = m_grid->GetSelectionBlockBottomRight();

    for( size_t i = 0; i < topLeft.size(); ++i )
    {
        for( int row = topLeft[i].GetRow(); row <= bottomRight[i].GetRow(); ++row )
        {
            for( const SCH_REFERENCE& ref : m_dataModel->GetRowReferences( row ) )
                selectedFullPaths.insert( ref.GetFullPath() );
        }
    }

    wxArrayInt selectedRows = m_grid->GetSelectedRows();

    for( int row : selectedRows )
    {
        for( const SCH_REFERENCE& ref : m_dataModel->GetRowReferences( row ) )
            selectedFullPaths.insert( ref.GetFullPath() );
    }

    int cursorRow = m_grid->GetGridCursorRow();

    if( cursorRow >= 0 && selectedFullPaths.empty() )
    {
        for( const SCH_REFERENCE& ref : m_dataModel->GetRowReferences( cursorRow ) )
            selectedFullPaths.insert( ref.GetFullPath() );
    }

    return selectedFullPaths;
}


void DIALOG_SYMBOL_FIELDS_TABLE::RestoreGridSelection( const std::set<wxString>& aFullPaths )
{
    if( aFullPaths.empty() )
        return;

    m_grid->ClearSelection();

    bool firstSelection = true;

    for( int row = 0; row < m_dataModel->GetNumberRows(); ++row )
    {
        std::vector<SCH_REFERENCE> refs = m_dataModel->GetRowReferences( row );

        for( const SCH_REFERENCE& ref : refs )
        {
            if( aFullPaths.count( ref.GetFullPath() ) )
            {
                m_grid->SelectRow( row, true );

                if( firstSelection )
                {
                    m_grid->SetGridCursor( row, m_grid->GetGridCursorCol() );
                    firstSelection = false;
                }

                break;
            }
        }
    }
}


SCH_REFERENCE_LIST DIALOG_SYMBOL_FIELDS_TABLE::getSymbolReferences( SCH_SYMBOL* aSymbol,
                                                                    SCH_REFERENCE_LIST& aCachedRefs )
{
    SCH_REFERENCE_LIST symbolRefs;

    for( size_t i = 0; i < aCachedRefs.GetCount(); i++ )
    {
        SCH_REFERENCE& ref = aCachedRefs[i];

        if( ref.GetSymbol() == aSymbol )
        {
            ref.Split(); // Figures out if we are annotated or not
            symbolRefs.AddItem( ref );
        }
    }

    return symbolRefs;
}


SCH_REFERENCE_LIST DIALOG_SYMBOL_FIELDS_TABLE::getSheetSymbolReferences( SCH_SHEET& aSheet )
{
    SCH_SHEET_LIST     allSheets = m_parent->Schematic().Hierarchy();
    SCH_REFERENCE_LIST sheetRefs;

    // We need to operate on all instances of the sheet
    for( const SCH_SHEET_INSTANCE& instance : aSheet.GetInstances() )
    {
        // For every sheet instance we need to get the current schematic sheet
        // instance that matches that particular sheet path from the root
        for( SCH_SHEET_PATH& basePath : allSheets )
        {
            if( basePath.Path() == instance.m_Path )
            {
                SCH_SHEET_PATH sheetPath = basePath;
                sheetPath.push_back( &aSheet );

                // Create a list of all sheets in this path, starting with the path
                // of the sheet that we just deleted, then all of its subsheets
                SCH_SHEET_LIST subSheets;
                subSheets.push_back( sheetPath );
                allSheets.GetSheetsWithinPath( subSheets, sheetPath );

                subSheets.GetSymbolsWithinPath( sheetRefs, sheetPath, SYMBOL_FILTER_NON_POWER, false );
                break;
            }
        }
    }

    for( SCH_REFERENCE& ref : sheetRefs )
        ref.Split();

    return sheetRefs;
}


void DIALOG_SYMBOL_FIELDS_TABLE::onAddVariant( wxCommandEvent& aEvent )
{
    if( !m_parent->ShowAddVariantDialog() )
        return;

    wxArrayString ctrlContents;
    ctrlContents.Add( GetDefaultVariantName() );

    for( const wxString& variant : m_parent->Schematic().GetVariantNames() )
        ctrlContents.Add( variant );

    ctrlContents.Sort( SortVariantNames );
    m_variantListBox->Set( ctrlContents );

    wxString currentVariant = m_parent->Schematic().GetCurrentVariant();
    int      newSelection = m_variantListBox->FindString(
                currentVariant.IsEmpty() ? GetDefaultVariantName() : currentVariant );

    if( newSelection != wxNOT_FOUND )
        m_variantListBox->SetSelection( newSelection );

    updateVariantButtonStates();
}


void DIALOG_SYMBOL_FIELDS_TABLE::onDeleteVariant( wxCommandEvent& aEvent )
{
    int selection = m_variantListBox->GetSelection();

    // An empty or default selection cannot be deleted.
    if( ( selection == wxNOT_FOUND ) || ( selection == 0 ) )
    {
        m_parent->GetInfoBar()->ShowMessageFor( _( "Cannot delete the default variant." ),
                                                 10000, wxICON_ERROR );
        return;
    }

    wxString variantName = m_variantListBox->GetString( selection );
    m_variantListBox->Delete( selection );
    m_parent->Schematic().DeleteVariant( variantName );
    m_parent->OnModify();

    int newSelection = std::max( 0, selection - 1 );
    m_variantListBox->SetSelection( newSelection );

    wxString selectedVariant = getSelectedVariant();
    m_parent->SetCurrentVariant( selectedVariant );

    if( m_grid->CommitPendingChanges( true ) )
    {
        m_dataModel->SetCurrentVariant( selectedVariant );
        m_dataModel->UpdateReferences( m_dataModel->GetReferenceList(), selectedVariant );
        m_dataModel->RebuildRows();

        if( m_nbPages->GetSelection() == 1 )
            PreviewRefresh();
        else
            m_grid->ForceRefresh();
    }

    updateVariantButtonStates();
    m_parent->UpdateVariantSelectionCtrl( m_parent->Schematic().GetVariantNamesForUI() );
}


void DIALOG_SYMBOL_FIELDS_TABLE::onRenameVariant( wxCommandEvent& aEvent )
{
    int selection = m_variantListBox->GetSelection();

    // An empty or default selection cannot be renamed.
    if( ( selection == wxNOT_FOUND ) || ( selection == 0 ) )
    {
        m_parent->GetInfoBar()->ShowMessageFor( _( "Cannot rename the default variant." ),
                                                 10000, wxICON_ERROR );
        return;
    }

    wxString oldVariantName = m_variantListBox->GetString( selection );

    wxTextEntryDialog dlg( this, _( "Enter new variant name:" ), _( "Rename Design Variant" ),
                           oldVariantName, wxOK | wxCANCEL | wxCENTER );

    if( dlg.ShowModal() == wxID_CANCEL )
        return;

    wxString newVariantName = dlg.GetValue().Trim().Trim( false );

    // Empty name is not allowed.
    if( newVariantName.IsEmpty() )
    {
        m_parent->GetInfoBar()->ShowMessageFor( _( "Variant name cannot be empty." ), 10000, wxICON_ERROR );
        return;
    }

    // Reserved name is not allowed (case-insensitive).
    if( newVariantName.CmpNoCase( GetDefaultVariantName() ) == 0 )
    {
        m_parent->GetInfoBar()->ShowMessageFor( wxString::Format( _( "'%s' is a reserved variant name." ),
                                                                  GetDefaultVariantName() ),
                                                10000, wxICON_ERROR );
        return;
    }

    // Same name (exact match) - nothing to do
    if( newVariantName == oldVariantName )
        return;

    // Duplicate name is not allowed (case-insensitive).
    for( const wxString& existingName : m_parent->Schematic().GetVariantNames() )
    {
        if( existingName.CmpNoCase( newVariantName ) == 0
            && existingName.CmpNoCase( oldVariantName ) != 0 )
        {
            m_parent->GetInfoBar()->ShowMessageFor( wxString::Format( _( "Variant '%s' already exists." ),
                                                                      existingName ),
                                                    0000, wxICON_ERROR );
            return;
        }
    }

    m_parent->Schematic().RenameVariant( oldVariantName, newVariantName );
    m_parent->OnModify();

    wxArrayString ctrlContents = m_variantListBox->GetStrings();
    ctrlContents.Remove( oldVariantName );
    ctrlContents.Add( newVariantName );
    ctrlContents.Sort( SortVariantNames );
    m_variantListBox->Set( ctrlContents );

    int newSelection = m_variantListBox->FindString( newVariantName );

    if( newSelection != wxNOT_FOUND )
        m_variantListBox->SetSelection( newSelection );

    updateVariantButtonStates();
    m_parent->UpdateVariantSelectionCtrl( m_parent->Schematic().GetVariantNamesForUI() );
}


void DIALOG_SYMBOL_FIELDS_TABLE::onCopyVariant( wxCommandEvent& aEvent )
{
    int selection = m_variantListBox->GetSelection();

    // An empty or default selection cannot be copied.
    if( ( selection == wxNOT_FOUND ) || ( selection == 0 ) )
    {
        m_parent->GetInfoBar()->ShowMessageFor( _( "Cannot copy the default variant." ),
                                                 10000, wxICON_ERROR );
        return;
    }

    wxString sourceVariantName = m_variantListBox->GetString( selection );

    wxTextEntryDialog dlg( this, _( "Enter name for the copied variant:" ), _( "Copy Design Variant" ),
                           sourceVariantName + wxS( "_copy" ), wxOK | wxCANCEL | wxCENTER );

    if( dlg.ShowModal() == wxID_CANCEL )
        return;

    wxString newVariantName = dlg.GetValue().Trim().Trim( false );

    // Empty name is not allowed.
    if( newVariantName.IsEmpty() )
    {
        m_parent->GetInfoBar()->ShowMessageFor( _( "Variant name cannot be empty." ), 10000, wxICON_ERROR );
        return;
    }

    // Duplicate name is not allowed.
    if( m_variantListBox->FindString( newVariantName ) != wxNOT_FOUND )
    {
        m_parent->GetInfoBar()->ShowMessageFor( wxString::Format( _( "Variant '%s' already exists." ),
                                                                  newVariantName ),
                                                10000, wxICON_ERROR );
        return;
    }

    m_parent->Schematic().CopyVariant( sourceVariantName, newVariantName );
    m_parent->OnModify();

    wxArrayString ctrlContents = m_variantListBox->GetStrings();
    ctrlContents.Add( newVariantName );
    ctrlContents.Sort( SortVariantNames );
    m_variantListBox->Set( ctrlContents );

    int newSelection = m_variantListBox->FindString( newVariantName );

    if( newSelection != wxNOT_FOUND )
        m_variantListBox->SetSelection( newSelection );

    updateVariantButtonStates();
    m_parent->UpdateVariantSelectionCtrl( m_parent->Schematic().GetVariantNamesForUI() );
}


void DIALOG_SYMBOL_FIELDS_TABLE::onEditVariantDescription( wxCommandEvent& aEvent )
{
    int selection = m_variantListBox->GetSelection();

    if( ( selection == wxNOT_FOUND ) || ( selection == 0 ) )
    {
        m_parent->GetInfoBar()->ShowMessageFor( _( "Cannot edit the default variant description." ), 10000,
                                                wxICON_ERROR );
        return;
    }

    wxString variantName = m_variantListBox->GetString( selection );
    wxString currentDesc = m_parent->Schematic().GetVariantDescription( variantName );

    wxDialog dlg( this, wxID_ANY, wxString::Format( _( "Edit Description for '%s'" ), variantName ), wxDefaultPosition,
                  wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER );

    wxBoxSizer* mainSizer = new wxBoxSizer( wxVERTICAL );

    wxStaticText* label = new wxStaticText( &dlg, wxID_ANY, _( "Description:" ) );
    mainSizer->Add( label, 0, wxLEFT | wxRIGHT | wxTOP | wxEXPAND, 10 );

    mainSizer->AddSpacer( 3 );

    wxTextCtrl* descCtrl =
            new wxTextCtrl( &dlg, wxID_ANY, currentDesc, wxDefaultPosition, wxSize( 300, 60 ), wxTE_MULTILINE );
    mainSizer->Add( descCtrl, 1, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 10 );

    wxStdDialogButtonSizer* btnSizer = new wxStdDialogButtonSizer();
    btnSizer->AddButton( new wxButton( &dlg, wxID_OK ) );
    btnSizer->AddButton( new wxButton( &dlg, wxID_CANCEL ) );
    btnSizer->Realize();
    mainSizer->Add( btnSizer, 0, wxALL | wxALIGN_RIGHT, 5 );

    dlg.SetSizer( mainSizer );
    dlg.Fit();
    dlg.Centre();

    if( dlg.ShowModal() == wxID_CANCEL )
        return;

    wxString newDesc = descCtrl->GetValue().Trim().Trim( false );

    m_parent->Schematic().SetVariantDescription( variantName, newDesc );
    m_parent->OnModify();
}


void DIALOG_SYMBOL_FIELDS_TABLE::onVariantSelectionChange( wxCommandEvent& aEvent )
{
    wxUnusedVar( aEvent );

    syncVariantSelection( getSelectedVariant(), true );
}


void DIALOG_SYMBOL_FIELDS_TABLE::syncVariantSelection( const wxString& aVariantName, bool aUpdateSchematic )
{
    wxString selectedVariant = aVariantName;

    if( selectedVariant == GetDefaultVariantName() )
        selectedVariant.Clear();

    wxString selectionName = selectedVariant.IsEmpty() ? GetDefaultVariantName() : selectedVariant;
    int      selection = m_variantListBox->FindString( selectionName );

    if( selection != wxNOT_FOUND && m_variantListBox->GetSelection() != selection )
        m_variantListBox->SetSelection( selection );

    updateVariantButtonStates();

    if( aUpdateSchematic && m_parent && m_parent->Schematic().GetCurrentVariant() != selectedVariant )
    {
        m_syncingVariantSelection = true;
        m_parent->SetCurrentVariant( selectedVariant );
        m_syncingVariantSelection = false;
    }

    wxString currentVariant = m_dataModel->GetCurrentVariant();

    if( currentVariant != selectedVariant )
    {
        m_grid->CommitPendingChanges( true );

        SCH_COMMIT     commit( m_parent );

        m_dataModel->ApplyData( commit, m_schSettings.m_TemplateFieldNames, currentVariant );

        if( !commit.Empty() )
        {
            commit.Push( wxS( "Symbol Fields Table Edit" ) );  // Push clears the commit buffer.
            m_parent->OnModify();
        }

        // Update the data model's current variant for field highlighting
        m_dataModel->SetCurrentVariant( selectedVariant );
        m_dataModel->UpdateReferences( m_dataModel->GetReferenceList(), selectedVariant );
        m_dataModel->RebuildRows();

        if( m_nbPages->GetSelection() == 1 )
            PreviewRefresh();
        else
            m_grid->ForceRefresh();

        syncBomFmtPresetSelection();
    }
}


void DIALOG_SYMBOL_FIELDS_TABLE::updateVariantButtonStates()
{
    int selection = m_variantListBox->GetSelection();

    // Copy, rename, and delete are only enabled for non-default variant selections
    bool canModify = ( selection != wxNOT_FOUND ) && ( selection != 0 );

    m_copyVariantButton->Enable( canModify );
    m_renameVariantButton->Enable( canModify );
    m_editVariantDescButton->Enable( canModify );
    m_deleteVariantButton->Enable( canModify );
}


wxString DIALOG_SYMBOL_FIELDS_TABLE::getSelectedVariant() const
{
    wxString retv;

    int selection = m_variantListBox->GetSelection();

    if( ( selection == wxNOT_FOUND ) || ( m_variantListBox->GetString( selection ) == GetDefaultVariantName() ) )
        return retv;

    return m_variantListBox->GetString( selection );
}
