1106 lines
35 KiB
C++
1106 lines
35 KiB
C++
/**
|
|
* @file dialog_design_rules.cpp
|
|
*/
|
|
|
|
/*
|
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
|
*
|
|
* Copyright (C) 2004-2009 Jean-Pierre Charras, jp.charras at wanadoo.fr
|
|
* Copyright (C) 2009 Dick Hollenbeck, dick@softplc.com
|
|
* Copyright (C) 2009-2018 KiCad Developers, see change_log.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
|
|
*/
|
|
|
|
|
|
/* functions relative to the design rules editor
|
|
*/
|
|
#include <fctsys.h>
|
|
#include <class_drawpanel.h>
|
|
#include <base_units.h>
|
|
#include <confirm.h>
|
|
#include <pcbnew.h>
|
|
#include <pcb_edit_frame.h>
|
|
#include <board_design_settings.h>
|
|
#include <bitmaps.h>
|
|
|
|
#include <pcbnew_id.h>
|
|
#include <class_track.h>
|
|
#include <macros.h>
|
|
#include <html_messagebox.h>
|
|
#include <dialog_text_entry.h>
|
|
|
|
#include <dialog_design_rules.h>
|
|
#include <wx/generic/gridctrl.h>
|
|
#include <dialog_design_rules_aux_helper_class.h>
|
|
|
|
#include <tool/tool_event.h>
|
|
#include <tool/tool_manager.h>
|
|
|
|
// Column labels for net lists
|
|
#define NET_TITLE _( "Net" )
|
|
#define CLASS_TITLE _( "Class" )
|
|
|
|
// Field Positions on rules grid
|
|
enum {
|
|
GRID_NAME = 0,
|
|
GRID_CLEARANCE,
|
|
GRID_TRACKSIZE,
|
|
GRID_VIASIZE,
|
|
GRID_VIADRILL,
|
|
GRID_uVIASIZE,
|
|
GRID_uVIADRILL,
|
|
GRID_DIFF_PAIR_WIDTH,
|
|
GRID_DIFF_PAIR_GAP
|
|
};
|
|
|
|
const wxString DIALOG_DESIGN_RULES::wildCard = _( "* (Any)" );
|
|
|
|
// dialog should remember its previously selected tab
|
|
int DIALOG_DESIGN_RULES::s_LastTabSelection = -1;
|
|
|
|
// methods for the helper class NETS_LIST_CTRL
|
|
|
|
wxString NETS_LIST_CTRL::OnGetItemText( long item, long column ) const
|
|
{
|
|
if( column == 0 )
|
|
{
|
|
if( item < (long) m_Netnames.GetCount() )
|
|
return m_Netnames[item];
|
|
else
|
|
return wxEmptyString;
|
|
}
|
|
else if( item < (long) m_Classnames.GetCount() )
|
|
return m_Classnames[item];
|
|
|
|
return wxEmptyString;
|
|
}
|
|
|
|
|
|
void NETS_LIST_CTRL::SetRowItems( unsigned aRow,
|
|
const wxString& aNetname,
|
|
const wxString& aNetclassName )
|
|
{
|
|
// insert blanks if aRow is larger than existing row count
|
|
unsigned cnt = m_Netnames.GetCount();
|
|
|
|
if( cnt <= aRow )
|
|
m_Netnames.Add( wxEmptyString, aRow - cnt + 1 );
|
|
|
|
cnt = m_Classnames.GetCount();
|
|
|
|
if( cnt <= aRow )
|
|
m_Classnames.Add( wxEmptyString, aRow - cnt + 1 );
|
|
|
|
if( (int)aRow <= GetItemCount() )
|
|
SetItemCount( aRow + 1 );
|
|
|
|
m_Netnames[aRow] = aNetname;
|
|
m_Classnames[aRow] = aNetclassName;
|
|
}
|
|
|
|
|
|
DIALOG_DESIGN_RULES::DIALOG_DESIGN_RULES( PCB_EDIT_FRAME* parent ) :
|
|
DIALOG_DESIGN_RULES_BASE( parent ),
|
|
m_trackMinWidth( parent, m_TrackMinWidthTitle, m_SetTrackMinWidthCtrl, m_TrackMinWidthUnits, true, 0 ),
|
|
m_viaMinDiameter( parent, m_ViaMinTitle, m_SetViasMinSizeCtrl, m_ViaMinUnits, true, 0 ),
|
|
m_viaMinDrill( parent, m_ViaMinDrillTitle, m_SetViasMinDrillCtrl, m_ViaMinDrillUnits, true, 0 ),
|
|
m_microViaMinDiameter( parent, m_MicroViaMinSizeTitle, m_SetMicroViasMinSizeCtrl, m_MicroViaMinSizeUnits, true, 0 ),
|
|
m_microViaMinDrill( parent, m_MicroViaMinDrillTitle, m_SetMicroViasMinDrillCtrl, m_MicroViaMinDrillUnits, true, 0 )
|
|
{
|
|
m_Parent = parent;
|
|
|
|
m_gridErrorGrid = nullptr;
|
|
|
|
// Loading the grid messes up the column widths set in wxFormBuilder
|
|
m_originalColWidths = new int[ m_grid->GetNumberCols() ];
|
|
|
|
for( int i = 0; i < m_grid->GetNumberCols(); ++i )
|
|
m_originalColWidths[ i ] = m_grid->GetColSize( i );
|
|
|
|
wxListItem column0;
|
|
wxListItem column1;
|
|
|
|
column0.Clear();
|
|
column1.Clear();
|
|
|
|
column0.SetMask( wxLIST_MASK_TEXT );
|
|
column1.SetMask( wxLIST_MASK_TEXT );
|
|
|
|
column0.SetText( NET_TITLE );
|
|
column1.SetText( CLASS_TITLE );
|
|
|
|
m_leftListCtrl->InsertColumn( 0, column0 );
|
|
m_leftListCtrl->InsertColumn( 1, column1 );
|
|
m_leftListCtrl->SetColumnWidth( 0, wxLIST_AUTOSIZE );
|
|
m_leftListCtrl->SetColumnWidth( 1, wxLIST_AUTOSIZE );
|
|
|
|
m_rightListCtrl->InsertColumn( 0, column0 );
|
|
m_rightListCtrl->InsertColumn( 1, column1 );
|
|
m_rightListCtrl->SetColumnWidth( 0, wxLIST_AUTOSIZE );
|
|
m_rightListCtrl->SetColumnWidth( 1, wxLIST_AUTOSIZE );
|
|
|
|
// if user has been into the dialog before, go back to same tab
|
|
if( s_LastTabSelection != -1 )
|
|
{
|
|
m_DRnotebook->SetSelection( s_LastTabSelection );
|
|
}
|
|
|
|
m_addButton->SetBitmap( KiBitmap( small_plus_xpm ) );
|
|
m_removeButton->SetBitmap( KiBitmap( trash_xpm ) );
|
|
m_moveUpButton->SetBitmap( KiBitmap( small_up_xpm ) );
|
|
m_moveDownButton->SetBitmap( KiBitmap( small_down_xpm ) );
|
|
|
|
m_sdbSizer1OK->SetDefault();
|
|
|
|
// Allow tabbing out of grid controls.
|
|
m_grid->SetTabBehaviour( wxGrid::Tab_Leave );
|
|
m_gridViaSizeList->SetTabBehaviour( wxGrid::Tab_Leave );
|
|
m_gridTrackWidthList->SetTabBehaviour( wxGrid::Tab_Leave );
|
|
|
|
// wxFormBuilder doesn't include this event...
|
|
m_grid->Connect( wxEVT_GRID_CELL_CHANGING, wxGridEventHandler( DIALOG_DESIGN_RULES::OnNetclassGridCellChanging ), NULL, this );
|
|
|
|
m_panelNetClassesEditor->Layout();
|
|
m_panelGolbalDesignRules->Layout();
|
|
Fit();
|
|
|
|
// Now all widgets have the size fixed, call FinishDialogSettings
|
|
FinishDialogSettings();
|
|
}
|
|
|
|
|
|
DIALOG_DESIGN_RULES::~DIALOG_DESIGN_RULES()
|
|
{
|
|
delete [] m_originalColWidths;
|
|
|
|
m_grid->Disconnect( wxEVT_GRID_CELL_CHANGING, wxGridEventHandler( DIALOG_DESIGN_RULES::OnNetclassGridCellChanging ), NULL, this );
|
|
}
|
|
|
|
|
|
bool DIALOG_DESIGN_RULES::TransferDataToWindow()
|
|
{
|
|
if( !wxDialog::TransferDataToWindow() )
|
|
return false;
|
|
|
|
m_Pcb = m_Parent->GetBoard();
|
|
m_BrdSettings = &m_Pcb->GetDesignSettings();
|
|
|
|
// Initialize the Rules List
|
|
transferNetclassesToWindow();
|
|
|
|
// Reassure that all nets have net classes assigned
|
|
m_Pcb->BuildListOfNets();
|
|
|
|
// copy all NETs into m_AllNets by adding them as NETCUPs.
|
|
|
|
// @todo go fix m_Pcb->SynchronizeNetsAndNetClasses() so that the netcode==0 is not
|
|
// present in the BOARD::m_NetClasses
|
|
NETCLASSES& netclasses = m_BrdSettings->m_NetClasses;
|
|
NETCLASSPTR netclass = netclasses.GetDefault();
|
|
|
|
// Initialize list of nets for Default Net Class
|
|
for( NETCLASS::iterator name = netclass->begin(); name != netclass->end(); ++name )
|
|
{
|
|
m_AllNets.push_back( NETCUP( *name, netclass->GetName() ) );
|
|
}
|
|
|
|
// Initialize list of nets for others (custom) Net Classes
|
|
for( NETCLASSES::const_iterator nc = netclasses.begin(); nc != netclasses.end(); ++nc )
|
|
{
|
|
netclass = nc->second;
|
|
|
|
for( NETCLASS::const_iterator name = netclass->begin(); name != netclass->end(); ++name )
|
|
{
|
|
m_AllNets.push_back( NETCUP( *name, netclass->GetName() ) );
|
|
}
|
|
}
|
|
|
|
rebuildNetclassDropdowns();
|
|
transferGlobalRulesToWindow();
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::transferGlobalRulesToWindow()
|
|
{
|
|
m_trackMinWidth.SetValue( m_BrdSettings->m_TrackMinWidth );
|
|
m_viaMinDiameter.SetValue(m_BrdSettings->m_ViasMinSize );
|
|
m_viaMinDrill.SetValue( m_BrdSettings->m_ViasMinDrill );
|
|
|
|
m_OptAllowBlindBuriedVias->SetValue( m_BrdSettings->m_BlindBuriedViaAllowed );
|
|
m_OptAllowMicroVias->SetValue( m_BrdSettings->m_MicroViasAllowed );
|
|
CheckAllowMicroVias();
|
|
|
|
m_microViaMinDiameter.SetValue( m_BrdSettings->m_MicroViasMinSize );
|
|
m_microViaMinDrill.SetValue( m_BrdSettings->m_MicroViasMinDrill );
|
|
|
|
// Initialize Vias and Tracks sizes lists.
|
|
// note we display only extra values, never the current netclass value.
|
|
// (the first value in history list)
|
|
m_TracksWidthList = m_BrdSettings->m_TrackWidthList;
|
|
m_TracksWidthList.erase( m_TracksWidthList.begin() ); // remove the netclass value
|
|
m_ViasDimensionsList = m_BrdSettings->m_ViasDimensionsList;
|
|
m_ViasDimensionsList.erase( m_ViasDimensionsList.begin() ); // remove the netclass value
|
|
InitDimensionsLists();
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::InitDimensionsLists()
|
|
{
|
|
wxString msg;
|
|
|
|
// Fill cells with actual values:
|
|
m_gridViaSizeList->SetCellValue( 0, 0, wxEmptyString );
|
|
m_gridViaSizeList->SetCellValue( 0, 1, wxEmptyString );
|
|
m_gridTrackWidthList->SetCellValue( 0, 0, wxEmptyString );
|
|
|
|
for( unsigned ii = 0; ii < m_TracksWidthList.size(); ii++ )
|
|
{
|
|
msg = StringFromValue( GetUserUnits(), m_TracksWidthList[ii], true, true );
|
|
m_gridTrackWidthList->SetCellValue( ii, 0, msg );
|
|
}
|
|
|
|
for( unsigned ii = 0; ii < m_ViasDimensionsList.size(); ii++ )
|
|
{
|
|
msg = StringFromValue( GetUserUnits(), m_ViasDimensionsList[ii].m_Diameter, true, true );
|
|
m_gridViaSizeList->SetCellValue( ii, 0, msg );
|
|
|
|
if( m_ViasDimensionsList[ii].m_Drill > 0 )
|
|
{
|
|
msg = StringFromValue( GetUserUnits(), m_ViasDimensionsList[ii].m_Drill, true, true );
|
|
m_gridViaSizeList->SetCellValue( ii, 1, msg );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Sort comparison function (helper for makePointers() )
|
|
static bool sortByClassThenName( NETCUP* a, NETCUP* b )
|
|
{
|
|
// return a < b
|
|
if( a->clazz < b->clazz )
|
|
return true;
|
|
|
|
// inside the same class, sort by net name:
|
|
if( a->clazz == b->clazz )
|
|
{
|
|
if( a->net < b->net )
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::makePointers( PNETCUPS* aList, const wxString& aNetClassName )
|
|
{
|
|
aList->clear();
|
|
|
|
if( wildCard == aNetClassName )
|
|
{
|
|
for( NETCUPS::iterator n = m_AllNets.begin(); n != m_AllNets.end(); ++n )
|
|
{
|
|
aList->push_back( &*n );
|
|
}
|
|
|
|
sort( aList->begin(), aList->end(), sortByClassThenName );
|
|
|
|
// could use a different sort order for wildCard case.
|
|
}
|
|
else
|
|
{
|
|
for( NETCUPS::iterator n = m_AllNets.begin(); n != m_AllNets.end(); ++n )
|
|
{
|
|
if( n->clazz == aNetClassName )
|
|
aList->push_back( &*n );
|
|
}
|
|
|
|
sort( aList->begin(), aList->end(), sortByClassThenName );
|
|
}
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::FillListBoxWithNetNames( NETS_LIST_CTRL* aListCtrl,
|
|
const wxString& aNetClass )
|
|
{
|
|
aListCtrl->ClearList();
|
|
|
|
PNETCUPS ptrList;
|
|
|
|
// get a subset of m_AllNets in pointer form, sorted as desired.
|
|
makePointers( &ptrList, aNetClass );
|
|
|
|
// Add netclass info to m_Netnames and m_Classnames wxArrayString buffers
|
|
// aListCtrl uses wxLC_VIRTUAL option, so this is fast
|
|
wxClientDC sDC( aListCtrl );
|
|
int row = 0;
|
|
// recompute the column widths here, after setting texts
|
|
int net_colsize = sDC.GetTextExtent( NET_TITLE ).x;
|
|
int class_colsize = sDC.GetTextExtent( CLASS_TITLE ).x;
|
|
|
|
for( PNETCUPS::iterator i = ptrList.begin(); i!=ptrList.end(); ++i, ++row )
|
|
{
|
|
wxSize net_needed = sDC.GetTextExtent( (*i)->net );
|
|
wxSize class_needed = sDC.GetTextExtent( (*i)->clazz );
|
|
net_colsize = std::max( net_colsize, net_needed.x );
|
|
class_colsize = std::max( class_colsize, class_needed.x );
|
|
aListCtrl->SetRowItems( row, (*i)->net, (*i)->clazz );
|
|
}
|
|
|
|
int margin = sDC.GetTextExtent( wxT( "XX" ) ).x;
|
|
aListCtrl->SetColumnWidth( 0, net_colsize + margin );
|
|
aListCtrl->SetColumnWidth( 1, class_colsize + margin );
|
|
aListCtrl->Refresh();
|
|
}
|
|
|
|
|
|
/* Populates drop-downs with the list of net classes
|
|
*/
|
|
void DIALOG_DESIGN_RULES::rebuildNetclassDropdowns()
|
|
{
|
|
wxString rightSelected = m_rightClassChoice->GetStringSelection();
|
|
wxString leftSelected = m_leftClassChoice->GetStringSelection();
|
|
|
|
m_rightClassChoice->Clear();
|
|
m_leftClassChoice->Clear();
|
|
|
|
m_rightClassChoice->Append( wildCard );
|
|
m_leftClassChoice->Append( wildCard );
|
|
|
|
for( int ii = 0; ii < m_grid->GetNumberRows(); ii++ )
|
|
{
|
|
m_rightClassChoice->Append( m_grid->GetCellValue( ii, GRID_NAME ) );
|
|
m_leftClassChoice->Append( m_grid->GetCellValue( ii, GRID_NAME ) );
|
|
}
|
|
|
|
// Reselect previous choice if still available; otherwise the wildcard
|
|
m_rightClassChoice->Select( std::max( 0, m_rightClassChoice->FindString( rightSelected ) ) );
|
|
m_leftClassChoice->Select( std::max( 0, m_leftClassChoice->FindString( leftSelected ) ) );
|
|
|
|
FillListBoxWithNetNames( m_leftListCtrl, m_leftClassChoice->GetStringSelection() );
|
|
FillListBoxWithNetNames( m_rightListCtrl, m_rightClassChoice->GetStringSelection() );
|
|
}
|
|
|
|
|
|
/* Initialize the rules list from board
|
|
*/
|
|
|
|
static void class2gridRow( EDA_UNITS_T aUnits, wxGrid* grid, int row, const NETCLASSPTR& nc )
|
|
{
|
|
wxString msg;
|
|
|
|
grid->SetCellValue( row, GRID_NAME, nc->GetName() );
|
|
|
|
msg = StringFromValue( aUnits, nc->GetClearance(), true, true );
|
|
grid->SetCellValue( row, GRID_CLEARANCE, msg );
|
|
|
|
msg = StringFromValue( aUnits, nc->GetTrackWidth(), true, true );
|
|
grid->SetCellValue( row, GRID_TRACKSIZE, msg );
|
|
|
|
msg = StringFromValue( aUnits, nc->GetViaDiameter(), true, true );
|
|
grid->SetCellValue( row, GRID_VIASIZE, msg );
|
|
|
|
msg = StringFromValue( aUnits, nc->GetViaDrill(), true, true );
|
|
grid->SetCellValue( row, GRID_VIADRILL, msg );
|
|
|
|
msg = StringFromValue( aUnits, nc->GetuViaDiameter(), true, true );
|
|
grid->SetCellValue( row, GRID_uVIASIZE, msg );
|
|
|
|
msg = StringFromValue( aUnits, nc->GetuViaDrill(), true, true );
|
|
grid->SetCellValue( row, GRID_uVIADRILL, msg );
|
|
|
|
msg = StringFromValue( aUnits, nc->GetDiffPairGap(), true, true );
|
|
grid->SetCellValue( row, GRID_DIFF_PAIR_GAP, msg );
|
|
|
|
msg = StringFromValue( aUnits, nc->GetDiffPairWidth(),true, true );
|
|
grid->SetCellValue( row, GRID_DIFF_PAIR_WIDTH, msg );
|
|
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::transferNetclassesToWindow()
|
|
{
|
|
NETCLASSES& netclasses = m_BrdSettings->m_NetClasses;
|
|
|
|
// the +1 is for the Default NETCLASS.
|
|
if( netclasses.GetCount() + 1 > (unsigned) m_grid->GetNumberRows() )
|
|
m_grid->AppendRows( netclasses.GetCount() + 1 - m_grid->GetNumberRows() );
|
|
|
|
// enter the Default NETCLASS.
|
|
class2gridRow( GetUserUnits(), m_grid, 0, netclasses.GetDefault() );
|
|
|
|
// make the Default NETCLASS name read-only
|
|
wxGridCellAttr* cellAttr = m_grid->GetOrCreateCellAttr( 0, GRID_NAME );
|
|
cellAttr->SetReadOnly();
|
|
cellAttr->DecRef();
|
|
|
|
// enter other netclasses
|
|
int row = 1;
|
|
|
|
for( NETCLASSES::iterator i = netclasses.begin(); i != netclasses.end(); ++i, ++row )
|
|
class2gridRow( GetUserUnits(), m_grid, row, i->second );
|
|
}
|
|
|
|
|
|
static void gridRow2class( EDA_UNITS_T aUnits, wxGrid* grid, int row, const NETCLASSPTR& nc )
|
|
{
|
|
nc->SetName( grid->GetCellValue( row, GRID_NAME ) );
|
|
|
|
#define MYCELL( col ) \
|
|
ValueFromString( aUnits, grid->GetCellValue( row, col ) )
|
|
|
|
nc->SetClearance( MYCELL( GRID_CLEARANCE ) );
|
|
nc->SetTrackWidth( MYCELL( GRID_TRACKSIZE ) );
|
|
nc->SetViaDiameter( MYCELL( GRID_VIASIZE ) );
|
|
nc->SetViaDrill( MYCELL( GRID_VIADRILL ) );
|
|
nc->SetuViaDiameter( MYCELL( GRID_uVIASIZE ) );
|
|
nc->SetuViaDrill( MYCELL( GRID_uVIADRILL ) );
|
|
nc->SetDiffPairGap( MYCELL( GRID_DIFF_PAIR_GAP ) );
|
|
nc->SetDiffPairWidth( MYCELL( GRID_DIFF_PAIR_WIDTH ) );
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::CopyNetclassesToBoard()
|
|
{
|
|
// Commit any pending in-place edits and close the editor
|
|
m_grid->DisableCellEditControl();
|
|
|
|
NETCLASSES& netclasses = m_BrdSettings->m_NetClasses;
|
|
|
|
// Remove all netclasses from board. We'll copy new list after
|
|
netclasses.Clear();
|
|
|
|
// Copy the default NetClass:
|
|
gridRow2class( GetUserUnits(), m_grid, 0, netclasses.GetDefault() );
|
|
|
|
// Copy other NetClasses :
|
|
for( int row = 1; row < m_grid->GetNumberRows(); ++row )
|
|
{
|
|
NETCLASSPTR nc = std::make_shared<NETCLASS>( m_grid->GetCellValue( row, GRID_NAME ) );
|
|
|
|
if( m_BrdSettings->m_NetClasses.Add( nc ) )
|
|
gridRow2class( GetUserUnits(), m_grid, row, nc );
|
|
}
|
|
|
|
// Now read all nets and push them in the corresponding netclass net buffer
|
|
for( NETCUPS::const_iterator netcup = m_AllNets.begin(); netcup != m_AllNets.end(); ++netcup )
|
|
{
|
|
NETCLASSPTR nc = netclasses.Find( netcup->clazz );
|
|
wxASSERT( nc );
|
|
nc->Add( netcup->net );
|
|
}
|
|
|
|
m_Pcb->SynchronizeNetsAndNetClasses();
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::CopyGlobalRulesToBoard()
|
|
{
|
|
// Update tracks minimum values for DRC
|
|
m_BrdSettings->m_TrackMinWidth = m_trackMinWidth.GetValue();
|
|
|
|
// Update vias minimum values for DRC
|
|
m_BrdSettings->m_ViasMinSize = m_viaMinDiameter.GetValue();
|
|
m_BrdSettings->m_ViasMinDrill = m_viaMinDrill.GetValue();
|
|
|
|
m_BrdSettings->m_BlindBuriedViaAllowed = m_OptAllowBlindBuriedVias->GetValue();
|
|
m_BrdSettings->m_MicroViasAllowed = m_OptAllowMicroVias->GetValue();
|
|
|
|
// Update microvias minimum values for DRC
|
|
m_BrdSettings->m_MicroViasMinSize = m_microViaMinDiameter.GetValue();
|
|
m_BrdSettings->m_MicroViasMinDrill = m_microViaMinDrill.GetValue();
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::CopyDimensionsListsToBoard()
|
|
{
|
|
wxString msg;
|
|
|
|
// Commit any pending in-place edits and close editors from grid controls
|
|
m_gridTrackWidthList->DisableCellEditControl();
|
|
m_gridViaSizeList->DisableCellEditControl();
|
|
|
|
// Reinitialize m_TrackWidthList
|
|
m_TracksWidthList.clear();
|
|
|
|
for( int row = 0; row < m_gridTrackWidthList->GetNumberRows(); ++row )
|
|
{
|
|
msg = m_gridTrackWidthList->GetCellValue( row, 0 );
|
|
|
|
if( msg.IsEmpty() )
|
|
continue;
|
|
|
|
int value = ValueFromString( GetUserUnits(), msg );
|
|
m_TracksWidthList.push_back( value );
|
|
}
|
|
|
|
// Sort new list by by increasing value
|
|
sort( m_TracksWidthList.begin(), m_TracksWidthList.end() );
|
|
|
|
// Reinitialize m_ViasDimensionsList
|
|
m_ViasDimensionsList.clear();
|
|
|
|
for( int row = 0; row < m_gridViaSizeList->GetNumberRows(); ++row )
|
|
{
|
|
msg = m_gridViaSizeList->GetCellValue( row, 0 );
|
|
|
|
if( msg.IsEmpty() )
|
|
continue;
|
|
|
|
int value = ValueFromString( GetUserUnits(), msg );
|
|
VIA_DIMENSION via_dim;
|
|
via_dim.m_Diameter = value;
|
|
msg = m_gridViaSizeList->GetCellValue( row, 1 );
|
|
|
|
if( !msg.IsEmpty() )
|
|
{
|
|
value = ValueFromString( g_UserUnit, msg );
|
|
via_dim.m_Drill = value;
|
|
}
|
|
|
|
m_ViasDimensionsList.push_back( via_dim );
|
|
}
|
|
|
|
// Sort new list by by increasing value
|
|
sort( m_ViasDimensionsList.begin(), m_ViasDimensionsList.end() );
|
|
|
|
std::vector<int>* tlist = &m_BrdSettings->m_TrackWidthList;
|
|
|
|
// Remove old "custom" sizes
|
|
tlist->erase( tlist->begin() + 1, tlist->end() );
|
|
|
|
// Add new "custom" sizes
|
|
tlist->insert( tlist->end(), m_TracksWidthList.begin(), m_TracksWidthList.end() );
|
|
|
|
// Reinitialize m_ViaSizeList
|
|
std::vector<VIA_DIMENSION>* vialist = &m_BrdSettings->m_ViasDimensionsList;
|
|
vialist->erase( vialist->begin() + 1, vialist->end() );
|
|
vialist->insert( vialist->end(), m_ViasDimensionsList.begin(), m_ViasDimensionsList.end() );
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::OnNotebookPageChanged( wxNotebookEvent& event )
|
|
{
|
|
s_LastTabSelection = event.GetSelection();
|
|
|
|
// Skip() allows OSX to properly refresh controls.
|
|
event.Skip();
|
|
}
|
|
|
|
|
|
bool DIALOG_DESIGN_RULES::TransferDataFromWindow()
|
|
{
|
|
if( !wxDialog::TransferDataFromWindow() )
|
|
return false;
|
|
|
|
if( !validateData() )
|
|
return false;
|
|
|
|
CopyNetclassesToBoard();
|
|
CopyGlobalRulesToBoard();
|
|
CopyDimensionsListsToBoard();
|
|
m_BrdSettings->SetCurrentNetClass( NETCLASS::Default );
|
|
|
|
//this event causes the routing tool to reload its design rules information
|
|
TOOL_MANAGER* toolManager = m_Parent->GetToolManager();
|
|
TOOL_EVENT event( TC_COMMAND, TA_MODEL_CHANGE, AS_ACTIVE );
|
|
toolManager->ProcessEvent( event );
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool DIALOG_DESIGN_RULES::validateNetclassName( int aRow, wxString aName, bool focusFirst )
|
|
{
|
|
aName.Trim( true );
|
|
aName.Trim( false );
|
|
|
|
if( aName.IsEmpty() )
|
|
{
|
|
setGridError( m_grid, _( "Netclass name cannot be empty." ), aRow, GRID_NAME );
|
|
return false;
|
|
}
|
|
|
|
for( int ii = 0; ii < m_grid->GetNumberRows(); ii++ )
|
|
{
|
|
if( ii != aRow && m_grid->GetRowLabelValue( ii ).CmpNoCase( aName ) == 0 )
|
|
{
|
|
if( focusFirst )
|
|
setGridError( m_grid, _( "Netclass name already in use." ), aRow, GRID_NAME );
|
|
else
|
|
setGridError( m_grid, _( "Netclass name already in use." ), ii, GRID_NAME );
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::OnNetclassGridCellChanging( wxGridEvent& event )
|
|
{
|
|
if( event.GetCol() == GRID_NAME )
|
|
{
|
|
if( validateNetclassName( event.GetRow(), event.GetString() ) )
|
|
m_netclassesDirty = true;
|
|
else
|
|
event.Veto();
|
|
}
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::OnAddNetclassClick( wxCommandEvent& event )
|
|
{
|
|
m_grid->AppendRows();
|
|
|
|
// Copy values of the default class:
|
|
int irow = m_grid->GetNumberRows() - 1;
|
|
|
|
for( int icol = 1; icol < m_grid->GetNumberCols(); icol++ )
|
|
{
|
|
wxString value;
|
|
value = m_grid->GetCellValue( 0, icol );
|
|
m_grid->SetCellValue( irow, icol, value );
|
|
}
|
|
|
|
rebuildNetclassDropdowns();
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::OnRemoveNetclassClick( wxCommandEvent& event )
|
|
{
|
|
int curRow = m_grid->GetGridCursorRow();
|
|
|
|
if( curRow == 0 )
|
|
{
|
|
DisplayErrorMessage( this, _( "The default net class is required." ) );
|
|
return;
|
|
}
|
|
|
|
// reset the net class to default for members of the removed class
|
|
wxString classname = m_grid->GetCellValue( curRow, GRID_NAME );
|
|
swapNetClass( classname, NETCLASS::Default );
|
|
|
|
m_grid->DeleteRows( curRow, 1 );
|
|
|
|
rebuildNetclassDropdowns();
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::AdjustNetclassGridColumns( int aWidth )
|
|
{
|
|
int fixedColsWidth = 0;
|
|
|
|
for( int i = 1; i < m_grid->GetNumberCols(); i++ )
|
|
{
|
|
m_grid->SetColSize( i, m_originalColWidths[ i ] );
|
|
fixedColsWidth += m_originalColWidths[ i ];
|
|
}
|
|
|
|
m_grid->SetColSize( 0, aWidth - fixedColsWidth - 4 );
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::OnSizeNetclassGrid( wxSizeEvent& event )
|
|
{
|
|
AdjustNetclassGridColumns( event.GetSize().GetX() );
|
|
|
|
event.Skip();
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::setGridError( wxGrid* aGrid, const wxString& aMsg, int aRow, int aCol )
|
|
{
|
|
m_gridErrorGrid = aGrid;
|
|
m_gridErrorMsg = aMsg;
|
|
m_gridErrorRow = aRow;
|
|
m_gridErrorCol = aCol;
|
|
|
|
if( m_gridErrorGrid->GetParent() != m_DRnotebook->GetCurrentPage() )
|
|
m_DRnotebook->AdvanceSelection();
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::OnUpdateUI( wxUpdateUIEvent& event )
|
|
{
|
|
if( m_netclassesDirty )
|
|
{
|
|
rebuildNetclassDropdowns();
|
|
m_netclassesDirty = false;
|
|
}
|
|
|
|
// Handle a grid error. This is delayed to OnUpdateUI so that we can change focus
|
|
// even when the original validation was triggered from a killFocus event, and so
|
|
// that the corresponding notebook page can be shown in the background when triggered
|
|
// from an OK.
|
|
if( m_gridErrorGrid )
|
|
{
|
|
// We will re-enter this routine when the error dialog is displayed, so make
|
|
// sure we don't keep putting up more dialogs.
|
|
wxGrid* grid = m_gridErrorGrid;
|
|
m_gridErrorGrid = nullptr;
|
|
|
|
DisplayErrorMessage( this, m_gridErrorMsg );
|
|
|
|
grid->GoToCell( m_gridErrorRow, m_gridErrorCol );
|
|
grid->SetFocus();
|
|
}
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::CheckAllowMicroVias()
|
|
{
|
|
bool enabled = m_OptAllowMicroVias->GetValue();
|
|
|
|
m_microViaMinDiameter.Enable( enabled );
|
|
m_microViaMinDrill.Enable( enabled );
|
|
}
|
|
|
|
/**
|
|
* Function OnAllowMicroVias
|
|
* is called whenever the AllowMicroVias checkbox is toggled
|
|
*/
|
|
void DIALOG_DESIGN_RULES::OnAllowMicroVias( wxCommandEvent& event )
|
|
{
|
|
CheckAllowMicroVias();
|
|
}
|
|
|
|
void DIALOG_DESIGN_RULES::OnMoveUpSelectedNetClass( wxCommandEvent& event )
|
|
{
|
|
// Commit any pending in-place edits and close the editor
|
|
m_grid->DisableCellEditControl();
|
|
|
|
int curRow = m_grid->GetGridCursorRow();
|
|
|
|
if( curRow < 2 )
|
|
{
|
|
wxBell();
|
|
return;
|
|
}
|
|
|
|
// Swap the rule and the previous rule
|
|
for( int icol = 0; icol < m_grid->GetNumberCols(); icol++ )
|
|
{
|
|
wxString curr_value = m_grid->GetCellValue( curRow, icol );
|
|
wxString previous_value = m_grid->GetCellValue( curRow - 1, icol );
|
|
m_grid->SetCellValue( curRow, icol, previous_value );
|
|
m_grid->SetCellValue( curRow - 1, icol, curr_value );
|
|
}
|
|
|
|
m_grid->SetGridCursor( curRow - 1, m_grid->GetGridCursorCol() );
|
|
m_grid->MakeCellVisible( m_grid->GetGridCursorRow(), m_grid->GetGridCursorCol() );
|
|
|
|
m_netclassesDirty = true;
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::OnMoveDownSelectedNetClass( wxCommandEvent& event )
|
|
{
|
|
// Commit any pending in-place edits and close the editor
|
|
m_grid->DisableCellEditControl();
|
|
|
|
int curRow = m_grid->GetGridCursorRow();
|
|
|
|
if( curRow == 0 || curRow == m_grid->GetNumberRows() - 1 )
|
|
{
|
|
wxBell();
|
|
return;
|
|
}
|
|
|
|
// Swap the rule and the next rule
|
|
for( int icol = 0; icol < m_grid->GetNumberCols(); icol++ )
|
|
{
|
|
wxString curr_value = m_grid->GetCellValue( curRow, icol );
|
|
wxString next_value = m_grid->GetCellValue( curRow + 1, icol );
|
|
m_grid->SetCellValue( curRow, icol, next_value );
|
|
m_grid->SetCellValue( curRow + 1, icol, curr_value );
|
|
}
|
|
|
|
m_grid->SetGridCursor( curRow + 1, m_grid->GetGridCursorCol() );
|
|
m_grid->MakeCellVisible( m_grid->GetGridCursorRow(), m_grid->GetGridCursorCol() );
|
|
|
|
m_netclassesDirty = true;
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::OnLeftCBSelection( wxCommandEvent& event )
|
|
{
|
|
FillListBoxWithNetNames( m_leftListCtrl, m_leftClassChoice->GetStringSelection() );
|
|
|
|
if( m_leftClassChoice->GetStringSelection() == m_rightClassChoice->GetStringSelection() )
|
|
{
|
|
m_buttonRightToLeft->Enable( false );
|
|
m_buttonLeftToRight->Enable( false );
|
|
}
|
|
else
|
|
{
|
|
m_buttonRightToLeft->Enable( true );
|
|
m_buttonLeftToRight->Enable( true );
|
|
}
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::OnRightCBSelection( wxCommandEvent& event )
|
|
{
|
|
FillListBoxWithNetNames( m_rightListCtrl, m_rightClassChoice->GetStringSelection() );
|
|
|
|
if( m_leftClassChoice->GetStringSelection() == m_rightClassChoice->GetStringSelection() )
|
|
{
|
|
m_buttonRightToLeft->Enable( false );
|
|
m_buttonLeftToRight->Enable( false );
|
|
}
|
|
else
|
|
{
|
|
m_buttonRightToLeft->Enable( true );
|
|
m_buttonLeftToRight->Enable( true );
|
|
}
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::moveSelectedItems( NETS_LIST_CTRL* src, const wxString& newClassName )
|
|
{
|
|
wxListItem item;
|
|
wxString netName;
|
|
|
|
item.m_mask |= wxLIST_MASK_TEXT; // Validate the member m_text of the wxListItem item
|
|
|
|
for( int row = 0; row < src->GetItemCount(); ++row )
|
|
{
|
|
if( !src->GetItemState( row, wxLIST_STATE_SELECTED ) )
|
|
continue;
|
|
|
|
item.SetColumn( 0 );
|
|
item.SetId( row );
|
|
|
|
src->GetItem( item );
|
|
netName = item.GetText();
|
|
|
|
setNetClass( netName, newClassName == wildCard ? NETCLASS::Default : newClassName );
|
|
}
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::OnRightToLeftCopyButton( wxCommandEvent& event )
|
|
{
|
|
wxString newClassName = m_leftClassChoice->GetStringSelection();
|
|
|
|
moveSelectedItems( m_rightListCtrl, newClassName );
|
|
|
|
FillListBoxWithNetNames( m_leftListCtrl, m_leftClassChoice->GetStringSelection() );
|
|
FillListBoxWithNetNames( m_rightListCtrl, m_rightClassChoice->GetStringSelection() );
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::OnLeftToRightCopyButton( wxCommandEvent& event )
|
|
{
|
|
wxString newClassName = m_rightClassChoice->GetStringSelection();
|
|
|
|
moveSelectedItems( m_leftListCtrl, newClassName );
|
|
|
|
FillListBoxWithNetNames( m_leftListCtrl, m_leftClassChoice->GetStringSelection() );
|
|
FillListBoxWithNetNames( m_rightListCtrl, m_rightClassChoice->GetStringSelection() );
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::OnLeftSelectAllButton( wxCommandEvent& event )
|
|
{
|
|
for( int ii = 0; ii < m_leftListCtrl->GetItemCount(); ii++ )
|
|
m_leftListCtrl->SetItemState( ii, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED );
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::OnRightSelectAllButton( wxCommandEvent& event )
|
|
{
|
|
for( int ii = 0; ii < m_rightListCtrl->GetItemCount(); ii++ )
|
|
m_rightListCtrl->SetItemState( ii, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED );
|
|
}
|
|
|
|
|
|
void DIALOG_DESIGN_RULES::setNetClass( const wxString& aNetName, const wxString& aClassName )
|
|
{
|
|
for( NETCUPS::iterator i = m_AllNets.begin(); i != m_AllNets.end(); ++i )
|
|
{
|
|
if( i->net == aNetName )
|
|
{
|
|
i->clazz = aClassName;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
bool DIALOG_DESIGN_RULES::validateData()
|
|
{
|
|
wxString msg;
|
|
int minViaDia = ValueFromTextCtrl( *m_SetViasMinSizeCtrl );
|
|
int minViaDrill = ValueFromTextCtrl( *m_SetViasMinDrillCtrl );
|
|
int minUViaDia = ValueFromTextCtrl( *m_SetMicroViasMinSizeCtrl );
|
|
int minUViaDrill = ValueFromTextCtrl( *m_SetMicroViasMinDrillCtrl );
|
|
int minTrackWidth = ValueFromTextCtrl( *m_SetTrackMinWidthCtrl );
|
|
|
|
// Test net class parameters.
|
|
for( int row = 0; row < m_grid->GetNumberRows(); row++ )
|
|
{
|
|
wxString netclassName = m_grid->GetCellValue( row, GRID_NAME );
|
|
netclassName.Trim( true );
|
|
netclassName.Trim( false );
|
|
|
|
if( !validateNetclassName( row, netclassName, false ) )
|
|
return false;
|
|
|
|
if( ValueFromString( GetUserUnits(), m_grid->GetCellValue( row, GRID_TRACKSIZE ) )
|
|
< minTrackWidth )
|
|
{
|
|
msg.Printf( _( "Track width less than minimum track width (%s)." ),
|
|
StringFromValue( GetUserUnits(), minTrackWidth, true, true ) );
|
|
setGridError( m_grid, msg, row, GRID_TRACKSIZE );
|
|
return false;
|
|
}
|
|
|
|
if( ValueFromString( GetUserUnits(), m_grid->GetCellValue( row, GRID_DIFF_PAIR_WIDTH ) )
|
|
< minTrackWidth )
|
|
{
|
|
msg.Printf( _( "Differential pair width less than minimum track width (%s)." ),
|
|
StringFromValue( GetUserUnits(), minTrackWidth, true, true ) );
|
|
setGridError( m_grid, msg, row, GRID_DIFF_PAIR_WIDTH );
|
|
return false;
|
|
}
|
|
|
|
// Test vias
|
|
if( ValueFromString( GetUserUnits(), m_grid->GetCellValue( row, GRID_VIASIZE ) )
|
|
< minViaDia )
|
|
{
|
|
msg.Printf( _( "Via diameter less than minimum via diameter (%s)." ),
|
|
StringFromValue( GetUserUnits(), minViaDia, true, true ) );
|
|
setGridError( m_grid, msg, row, GRID_VIASIZE );
|
|
return false;
|
|
}
|
|
|
|
if( ValueFromString( GetUserUnits(), m_grid->GetCellValue( row, GRID_VIADRILL ) )
|
|
>= ValueFromString( GetUserUnits(), m_grid->GetCellValue( row, GRID_VIASIZE ) ) )
|
|
{
|
|
msg = _( "Via drill larger than via diameter." );
|
|
setGridError( m_grid, msg, row, GRID_VIADRILL );
|
|
return false;
|
|
}
|
|
|
|
if( ValueFromString( GetUserUnits(), m_grid->GetCellValue( row, GRID_VIADRILL ) )
|
|
< minViaDrill )
|
|
{
|
|
msg.Printf( _( "Via drill less than minimum via drill (%s)." ),
|
|
StringFromValue( GetUserUnits(), minViaDrill, true, true ) );
|
|
setGridError( m_grid, msg, row, GRID_VIADRILL );
|
|
return false;
|
|
}
|
|
|
|
// Test Micro vias
|
|
if( ValueFromString( GetUserUnits(), m_grid->GetCellValue( row, GRID_uVIASIZE ) )
|
|
< minUViaDia )
|
|
{
|
|
msg.Printf( _( "Microvia diameter less than minimum microvia diameter (%s)." ),
|
|
StringFromValue( GetUserUnits(), minUViaDia, true, true ) );
|
|
setGridError( m_grid, msg, row, GRID_uVIASIZE );
|
|
return false;
|
|
}
|
|
|
|
if( ValueFromString( GetUserUnits(), m_grid->GetCellValue( row, GRID_uVIADRILL ) )
|
|
>= ValueFromString( GetUserUnits(), m_grid->GetCellValue( row, GRID_uVIASIZE ) ) )
|
|
{
|
|
msg = _( "Microvia drill larger than microvia diameter." );
|
|
setGridError( m_grid, msg, row, GRID_uVIADRILL );
|
|
return false;
|
|
}
|
|
|
|
if( ValueFromString( GetUserUnits(), m_grid->GetCellValue( row, GRID_uVIADRILL ) )
|
|
< minUViaDrill )
|
|
{
|
|
msg.Printf( _( "Microvia drill less than minimum microvia drill (%s)." ),
|
|
StringFromValue( GetUserUnits(), minUViaDrill, true, true ) );
|
|
setGridError( m_grid, msg, row, GRID_uVIADRILL );
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Test custom tracks
|
|
for( int row = 0; row < m_gridTrackWidthList->GetNumberRows(); ++row )
|
|
{
|
|
wxString tvalue = m_gridTrackWidthList->GetCellValue( row, 0 );
|
|
|
|
if( tvalue.IsEmpty() )
|
|
continue;
|
|
|
|
if( ValueFromString( GetUserUnits(), tvalue ) < minTrackWidth )
|
|
{
|
|
msg.Printf( _( "Track width less than minimum track width (%s)." ),
|
|
StringFromValue( GetUserUnits(), minTrackWidth, true, true ) );
|
|
setGridError( m_gridTrackWidthList, msg, row, 0 );
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Test custom vias
|
|
for( int row = 0; row < m_gridViaSizeList->GetNumberRows(); ++row )
|
|
{
|
|
wxString viaDia = m_gridViaSizeList->GetCellValue( row, 0 );
|
|
|
|
if( viaDia.IsEmpty() )
|
|
continue;
|
|
|
|
if( ValueFromString( GetUserUnits(), viaDia ) < minViaDia )
|
|
{
|
|
msg.Printf( _( "Via diameter less than minimum via diameter (%s)." ),
|
|
StringFromValue( GetUserUnits(), minViaDia, true, true ) );
|
|
setGridError( m_gridViaSizeList, msg, row, 0 );
|
|
return false;
|
|
}
|
|
|
|
wxString viaDrill = m_gridViaSizeList->GetCellValue( row, 1 );
|
|
|
|
if( viaDrill.IsEmpty() )
|
|
{
|
|
msg = _( "No via drill defined." );
|
|
setGridError( m_gridViaSizeList, msg, row, 1 );
|
|
return false;
|
|
}
|
|
|
|
if( ValueFromString( GetUserUnits(), viaDrill ) < minViaDrill )
|
|
{
|
|
msg.Printf( _( "Via drill less than minimum via drill (%s)." ),
|
|
StringFromValue( GetUserUnits(), minViaDrill, true, true ) );
|
|
setGridError( m_gridViaSizeList, msg, row, 1 );
|
|
return false;
|
|
}
|
|
|
|
if( ValueFromString( GetUserUnits(), viaDrill )
|
|
>= ValueFromString( GetUserUnits(), viaDia ) )
|
|
{
|
|
msg = _( "Via drill larger than via diameter." );
|
|
setGridError( m_gridViaSizeList, msg, row, 1 );
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
|