2022-08-21 11:26:47 +00:00
|
|
|
/*
|
|
|
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
|
|
|
*
|
|
|
|
* Copyright (C) 2018 CERN
|
|
|
|
* Copyright (C) 2021-2022 KiCad Developers, see AUTHORS.txt for contributors.
|
|
|
|
* @author Jon Evans <jon@craftyjon.com>
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify it
|
|
|
|
* under the terms of the GNU General Public License as published by the
|
|
|
|
* Free Software Foundation, either version 3 of the License, or (at your
|
|
|
|
* option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful, but
|
|
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
|
|
* General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License along
|
|
|
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include <widgets/wx_grid.h>
|
|
|
|
#include <confirm.h>
|
|
|
|
#include <sch_edit_frame.h>
|
|
|
|
#include <schematic.h>
|
|
|
|
#include <dialogs/panel_setup_buses.h>
|
2022-09-09 11:18:05 +00:00
|
|
|
#include "grid_tricks.h"
|
2022-08-21 11:26:47 +00:00
|
|
|
|
|
|
|
PANEL_SETUP_BUSES::PANEL_SETUP_BUSES( wxWindow* aWindow, SCH_EDIT_FRAME* aFrame ) :
|
|
|
|
PANEL_SETUP_BUSES_BASE( aWindow ),
|
2022-08-22 16:52:42 +00:00
|
|
|
m_frame( aFrame ),
|
2022-08-25 18:21:00 +00:00
|
|
|
m_lastAlias( 0 ),
|
2022-09-10 09:25:52 +00:00
|
|
|
m_membersGridDirty( false ),
|
2022-08-22 16:52:42 +00:00
|
|
|
m_errorGrid( nullptr ),
|
|
|
|
m_errorRow( -1 )
|
2022-08-21 11:26:47 +00:00
|
|
|
{
|
|
|
|
m_membersLabelTemplate = m_membersLabel->GetLabel();
|
|
|
|
|
|
|
|
m_addAlias->SetBitmap( KiBitmap( BITMAPS::small_plus ) );
|
|
|
|
m_deleteAlias->SetBitmap( KiBitmap( BITMAPS::small_trash ) );
|
|
|
|
m_addMember->SetBitmap( KiBitmap( BITMAPS::small_plus ) );
|
|
|
|
m_removeMember->SetBitmap( KiBitmap( BITMAPS::small_trash ) );
|
|
|
|
|
|
|
|
m_source->SetFont( KIUI::GetInfoFont( aWindow ) );
|
|
|
|
|
2022-09-09 11:18:05 +00:00
|
|
|
m_aliasesGrid->PushEventHandler( new GRID_TRICKS( m_aliasesGrid,
|
|
|
|
[this]( wxCommandEvent& aEvent )
|
|
|
|
{
|
|
|
|
OnAddAlias( aEvent );
|
|
|
|
} ) );
|
|
|
|
|
|
|
|
m_membersGrid->PushEventHandler( new GRID_TRICKS( m_membersGrid,
|
|
|
|
[this]( wxCommandEvent& aEvent )
|
|
|
|
{
|
|
|
|
wxIdleEvent dummy;
|
|
|
|
reloadMembersGridOnIdle( dummy );
|
|
|
|
|
|
|
|
OnAddMember( aEvent );
|
|
|
|
} ) );
|
|
|
|
|
2022-08-21 11:26:47 +00:00
|
|
|
// wxFormBuilder doesn't include this event...
|
|
|
|
m_aliasesGrid->Connect( wxEVT_GRID_CELL_CHANGING,
|
|
|
|
wxGridEventHandler( PANEL_SETUP_BUSES::OnAliasesGridCellChanging ),
|
|
|
|
nullptr, this );
|
|
|
|
m_membersGrid->Connect( wxEVT_GRID_CELL_CHANGING,
|
|
|
|
wxGridEventHandler( PANEL_SETUP_BUSES::OnMemberGridCellChanging ),
|
|
|
|
nullptr, this );
|
|
|
|
|
|
|
|
Layout();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-09-09 11:18:05 +00:00
|
|
|
PANEL_SETUP_BUSES::~PANEL_SETUP_BUSES()
|
|
|
|
{
|
|
|
|
// Delete the GRID_TRICKS.
|
|
|
|
m_aliasesGrid->PopEventHandler( true );
|
|
|
|
m_membersGrid->PopEventHandler( true );
|
|
|
|
|
|
|
|
m_aliasesGrid->Disconnect( wxEVT_GRID_CELL_CHANGING,
|
|
|
|
wxGridEventHandler( PANEL_SETUP_BUSES::OnAliasesGridCellChanging ),
|
|
|
|
nullptr, this );
|
|
|
|
m_membersGrid->Disconnect( wxEVT_GRID_CELL_CHANGING,
|
|
|
|
wxGridEventHandler( PANEL_SETUP_BUSES::OnMemberGridCellChanging ),
|
|
|
|
nullptr, this );
|
|
|
|
}
|
|
|
|
|
2022-08-21 11:26:47 +00:00
|
|
|
bool PANEL_SETUP_BUSES::TransferDataToWindow()
|
|
|
|
{
|
|
|
|
auto contains =
|
|
|
|
[&]( const std::shared_ptr<BUS_ALIAS>& alias ) -> bool
|
|
|
|
{
|
2022-08-21 19:35:02 +00:00
|
|
|
wxString aName = alias->GetName();
|
|
|
|
std::vector<wxString> aMembers = alias->Members();
|
2022-08-21 11:26:47 +00:00
|
|
|
|
2022-08-21 19:35:02 +00:00
|
|
|
std::sort( aMembers.begin(), aMembers.end() );
|
2022-08-21 11:26:47 +00:00
|
|
|
|
|
|
|
for( const std::shared_ptr<BUS_ALIAS>& candidate : m_aliases )
|
|
|
|
{
|
2022-08-21 19:35:02 +00:00
|
|
|
wxString bName = candidate->GetName();
|
|
|
|
std::vector<wxString> bMembers = candidate->Members();
|
2022-08-21 11:26:47 +00:00
|
|
|
|
2022-08-21 19:35:02 +00:00
|
|
|
std::sort( bMembers.begin(), bMembers.end() );
|
2022-08-21 11:26:47 +00:00
|
|
|
|
|
|
|
if( aName == bName && aMembers == bMembers )
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
|
|
|
SCH_SCREENS screens( m_frame->Schematic().Root() );
|
|
|
|
|
|
|
|
// collect aliases from each open sheet
|
|
|
|
for( SCH_SCREEN* screen = screens.GetFirst(); screen != nullptr; screen = screens.GetNext() )
|
|
|
|
{
|
|
|
|
for( const std::shared_ptr<BUS_ALIAS>& alias : screen->GetBusAliases() )
|
|
|
|
{
|
|
|
|
if( !contains( alias ) )
|
|
|
|
m_aliases.push_back( alias->Clone() );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
int ii = 0;
|
|
|
|
|
|
|
|
m_aliasesGrid->ClearRows();
|
|
|
|
m_aliasesGrid->AppendRows( m_aliases.size() );
|
|
|
|
|
|
|
|
for( const std::shared_ptr<BUS_ALIAS>& alias : m_aliases )
|
|
|
|
m_aliasesGrid->SetCellValue( ii++, 0, alias->GetName() );
|
|
|
|
|
|
|
|
m_membersBook->SetSelection( 1 );
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool PANEL_SETUP_BUSES::TransferDataFromWindow()
|
|
|
|
{
|
|
|
|
if( !m_aliasesGrid->CommitPendingChanges() || !m_membersGrid->CommitPendingChanges() )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// Copy names back just in case they didn't get caught on the GridCellChanging event
|
|
|
|
for( int ii = 0; ii < m_aliasesGrid->GetNumberRows(); ++ii )
|
|
|
|
m_aliases[ii]->SetName( m_aliasesGrid->GetCellValue( ii, 0 ) );
|
|
|
|
|
|
|
|
SCH_SCREENS screens( m_frame->Schematic().Root() );
|
|
|
|
|
|
|
|
for( SCH_SCREEN* screen = screens.GetFirst(); screen != nullptr; screen = screens.GetNext() )
|
|
|
|
screen->ClearBusAliases();
|
|
|
|
|
|
|
|
for( const std::shared_ptr<BUS_ALIAS>& alias : m_aliases )
|
|
|
|
alias->GetParent()->AddBusAlias( alias );
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void PANEL_SETUP_BUSES::OnAddAlias( wxCommandEvent& aEvent )
|
|
|
|
{
|
|
|
|
if( !m_aliasesGrid->CommitPendingChanges() || !m_membersGrid->CommitPendingChanges() )
|
|
|
|
return;
|
|
|
|
|
|
|
|
// New aliases get stored on the currently visible sheet
|
|
|
|
m_aliases.push_back( std::make_shared<BUS_ALIAS>( m_frame->GetScreen() ) );
|
|
|
|
|
|
|
|
int row = m_aliasesGrid->GetNumberRows();
|
|
|
|
m_aliasesGrid->AppendRows();
|
|
|
|
|
|
|
|
m_aliasesGrid->MakeCellVisible( row, 0 );
|
|
|
|
m_aliasesGrid->SetGridCursor( row, 0 );
|
|
|
|
|
|
|
|
m_aliasesGrid->EnableCellEditControl( true );
|
|
|
|
m_aliasesGrid->ShowCellEditControl();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void PANEL_SETUP_BUSES::OnDeleteAlias( wxCommandEvent& aEvent )
|
|
|
|
{
|
|
|
|
if( !m_aliasesGrid->CommitPendingChanges() || !m_membersGrid->CommitPendingChanges() )
|
|
|
|
return;
|
|
|
|
|
|
|
|
int curRow = m_aliasesGrid->GetGridCursorRow();
|
|
|
|
|
|
|
|
if( curRow < 0 )
|
|
|
|
return;
|
|
|
|
|
|
|
|
// Clear the members grid first so we don't try to write it back to a deleted alias
|
|
|
|
m_membersGrid->ClearRows();
|
|
|
|
m_lastAlias = -1;
|
|
|
|
m_lastAliasName = wxEmptyString;
|
|
|
|
|
|
|
|
m_aliases.erase( m_aliases.begin() + curRow );
|
|
|
|
|
|
|
|
m_aliasesGrid->DeleteRows( curRow, 1 );
|
|
|
|
|
|
|
|
if( m_aliasesGrid->GetNumberRows() > 0 )
|
|
|
|
{
|
|
|
|
m_aliasesGrid->MakeCellVisible( std::max( 0, curRow-1 ), 0 );
|
|
|
|
m_aliasesGrid->SetGridCursor( std::max( 0, curRow-1 ), 0 );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void PANEL_SETUP_BUSES::OnAddMember( wxCommandEvent& aEvent )
|
|
|
|
{
|
|
|
|
if( !m_membersGrid->CommitPendingChanges() )
|
|
|
|
return;
|
|
|
|
|
2022-09-09 11:18:05 +00:00
|
|
|
int row = m_membersGrid->GetNumberRows();
|
2022-08-21 11:26:47 +00:00
|
|
|
m_membersGrid->AppendRows();
|
|
|
|
|
|
|
|
m_membersGrid->MakeCellVisible( row, 0 );
|
|
|
|
m_membersGrid->SetGridCursor( row, 0 );
|
|
|
|
|
|
|
|
m_membersGrid->EnableCellEditControl( true );
|
|
|
|
m_membersGrid->ShowCellEditControl();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void PANEL_SETUP_BUSES::OnRemoveMember( wxCommandEvent& aEvent )
|
|
|
|
{
|
|
|
|
if( !m_membersGrid->CommitPendingChanges() )
|
|
|
|
return;
|
|
|
|
|
|
|
|
int curRow = m_membersGrid->GetGridCursorRow();
|
|
|
|
|
|
|
|
if( curRow < 0 )
|
|
|
|
return;
|
|
|
|
|
|
|
|
m_membersGrid->DeleteRows( curRow, 1 );
|
|
|
|
|
|
|
|
if( m_membersGrid->GetNumberRows() > 0 )
|
|
|
|
{
|
|
|
|
m_membersGrid->MakeCellVisible( std::max( 0, curRow-1 ), 0 );
|
|
|
|
m_membersGrid->SetGridCursor( std::max( 0, curRow-1 ), 0 );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void PANEL_SETUP_BUSES::OnAliasesGridCellChanging( wxGridEvent& event )
|
|
|
|
{
|
|
|
|
int row = event.GetRow();
|
|
|
|
|
|
|
|
if( row >= 0 )
|
|
|
|
{
|
|
|
|
wxString name = event.GetString();
|
|
|
|
|
|
|
|
for( int ii = 0; ii < m_aliasesGrid->GetNumberRows(); ++ii )
|
|
|
|
{
|
|
|
|
if( ii == event.GetRow() )
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if( name == m_aliasesGrid->GetCellValue( ii, 0 )
|
|
|
|
&& m_aliases[ row ]->GetParent() == m_aliases[ ii ]->GetParent() )
|
|
|
|
{
|
|
|
|
m_errorMsg = wxString::Format( _( "Alias name '%s' already in use." ), name );
|
|
|
|
m_errorGrid = m_aliasesGrid;
|
|
|
|
m_errorRow = row;
|
|
|
|
|
|
|
|
event.Veto();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
m_aliases[ row ]->SetName( name );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void PANEL_SETUP_BUSES::OnMemberGridCellChanging( wxGridEvent& event )
|
|
|
|
{
|
|
|
|
int row = event.GetRow();
|
|
|
|
|
|
|
|
if( row >= 0 )
|
|
|
|
{
|
|
|
|
wxString name = event.GetString();
|
|
|
|
|
|
|
|
if( name.IsEmpty() )
|
|
|
|
{
|
|
|
|
m_errorMsg = _( "Member net/alias name cannot be empty." );
|
|
|
|
m_errorGrid = m_membersGrid;
|
|
|
|
m_errorRow = event.GetRow();
|
|
|
|
|
|
|
|
event.Veto();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const std::shared_ptr<BUS_ALIAS>& alias = m_aliases[ m_lastAlias ];
|
|
|
|
|
2022-08-21 19:35:02 +00:00
|
|
|
alias->Members().clear();
|
2022-08-21 11:26:47 +00:00
|
|
|
|
|
|
|
for( int ii = 0; ii < m_membersGrid->GetNumberRows(); ++ii )
|
|
|
|
{
|
|
|
|
if( ii == row )
|
|
|
|
{
|
|
|
|
// Parse a space-separated list and add each one
|
|
|
|
wxStringTokenizer tok( name, " " );
|
|
|
|
|
|
|
|
while( tok.HasMoreTokens() )
|
2022-08-21 19:35:02 +00:00
|
|
|
alias->Members().push_back( tok.GetNextToken() );
|
2022-08-21 11:26:47 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2022-08-21 19:35:02 +00:00
|
|
|
alias->Members().push_back( m_membersGrid->GetCellValue( ii, 0 ) );
|
2022-08-21 11:26:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-09 11:18:05 +00:00
|
|
|
m_membersGridDirty = true;
|
2022-08-21 19:35:02 +00:00
|
|
|
Bind( wxEVT_IDLE, &PANEL_SETUP_BUSES::reloadMembersGridOnIdle, this );
|
2022-08-21 11:26:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-08-21 19:35:02 +00:00
|
|
|
void PANEL_SETUP_BUSES::doReloadMembersGrid()
|
2022-08-21 11:26:47 +00:00
|
|
|
{
|
|
|
|
if( m_lastAlias >= 0 && m_lastAlias < m_aliasesGrid->GetNumberRows() )
|
|
|
|
{
|
|
|
|
const std::shared_ptr<BUS_ALIAS>& alias = m_aliases[ m_lastAlias ];
|
2022-08-26 16:02:59 +00:00
|
|
|
wxString source;
|
|
|
|
wxString membersLabel;
|
|
|
|
|
|
|
|
if( alias->GetParent() )
|
|
|
|
{
|
|
|
|
wxFileName sheet_name( alias->GetParent()->GetFileName() );
|
|
|
|
source.Printf( wxS( "(" ) + sheet_name.GetFullName() + wxS( ")" ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
membersLabel.Printf( m_membersLabelTemplate, m_lastAliasName );
|
|
|
|
|
|
|
|
m_source->SetLabel( source );
|
|
|
|
m_membersLabel->SetLabel( membersLabel );
|
2022-08-21 11:26:47 +00:00
|
|
|
|
|
|
|
m_membersGrid->ClearRows();
|
2022-08-21 19:35:02 +00:00
|
|
|
m_membersGrid->AppendRows( alias->Members().size() );
|
2022-08-21 11:26:47 +00:00
|
|
|
|
2022-08-21 19:35:02 +00:00
|
|
|
int ii = 0;
|
|
|
|
|
|
|
|
for( const wxString& member : alias->Members() )
|
|
|
|
m_membersGrid->SetCellValue( ii++, 0, member );
|
2022-08-21 11:26:47 +00:00
|
|
|
}
|
2022-09-09 11:18:05 +00:00
|
|
|
|
|
|
|
m_membersGridDirty = false;
|
2022-08-21 19:35:02 +00:00
|
|
|
}
|
|
|
|
|
2022-08-21 11:26:47 +00:00
|
|
|
|
2022-08-21 19:35:02 +00:00
|
|
|
void PANEL_SETUP_BUSES::reloadMembersGridOnIdle( wxIdleEvent& aEvent )
|
|
|
|
{
|
2022-09-09 11:18:05 +00:00
|
|
|
if( m_membersGridDirty )
|
|
|
|
doReloadMembersGrid();
|
2022-08-21 19:35:02 +00:00
|
|
|
|
|
|
|
Unbind( wxEVT_IDLE, &PANEL_SETUP_BUSES::reloadMembersGridOnIdle, this );
|
2022-08-21 11:26:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void PANEL_SETUP_BUSES::OnSizeGrid( wxSizeEvent& event )
|
|
|
|
{
|
|
|
|
auto setColSize =
|
|
|
|
[]( WX_GRID* grid )
|
|
|
|
{
|
|
|
|
int colSize = std::max( grid->GetClientSize().x, grid->GetVisibleWidth( 0 ) );
|
|
|
|
|
|
|
|
if( grid->GetColSize( 0 ) != colSize )
|
|
|
|
grid->SetColSize( 0, colSize );
|
|
|
|
};
|
|
|
|
|
|
|
|
setColSize( m_aliasesGrid );
|
|
|
|
setColSize( m_membersGrid );
|
|
|
|
|
|
|
|
// Always propagate for a grid repaint (needed if the height changes, as well as width)
|
|
|
|
event.Skip();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void PANEL_SETUP_BUSES::OnUpdateUI( wxUpdateUIEvent& event )
|
|
|
|
{
|
|
|
|
// 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.
|
|
|
|
if( !m_errorMsg.IsEmpty() )
|
|
|
|
{
|
|
|
|
// We will re-enter this routine when the error dialog is displayed, so make
|
|
|
|
// sure we don't keep putting up more dialogs.
|
|
|
|
wxString errorMsg = m_errorMsg;
|
|
|
|
m_errorMsg = wxEmptyString;
|
|
|
|
|
|
|
|
DisplayErrorMessage( this, errorMsg );
|
|
|
|
|
|
|
|
m_errorGrid->SetFocus();
|
|
|
|
m_errorGrid->MakeCellVisible( m_errorRow, 0 );
|
|
|
|
m_errorGrid->SetGridCursor( m_errorRow, 0 );
|
|
|
|
|
|
|
|
m_errorGrid->EnableCellEditControl( true );
|
|
|
|
m_errorGrid->ShowCellEditControl();
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( !m_membersGrid->IsCellEditControlShown() )
|
|
|
|
{
|
|
|
|
int row = -1;
|
|
|
|
wxString aliasName;
|
|
|
|
|
|
|
|
if( m_aliasesGrid->IsCellEditControlShown() )
|
|
|
|
{
|
|
|
|
row = m_aliasesGrid->GetGridCursorRow();
|
|
|
|
wxGridCellEditor* cellEditor = m_aliasesGrid->GetCellEditor( row, 0 );
|
|
|
|
|
|
|
|
if( wxTextEntry* txt = dynamic_cast<wxTextEntry*>( cellEditor->GetControl() ) )
|
|
|
|
aliasName = txt->GetValue();
|
|
|
|
|
|
|
|
cellEditor->DecRef();
|
|
|
|
}
|
|
|
|
else if( m_aliasesGrid->GetGridCursorRow() >= 0 )
|
|
|
|
{
|
|
|
|
row = m_aliasesGrid->GetGridCursorRow();
|
|
|
|
aliasName = m_aliasesGrid->GetCellValue( row, 0 );
|
|
|
|
}
|
|
|
|
else if( m_lastAlias >= 0 && m_lastAlias < m_aliasesGrid->GetNumberRows() )
|
|
|
|
{
|
|
|
|
row = m_lastAlias;
|
|
|
|
aliasName = m_lastAliasName;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( row < 0 )
|
|
|
|
{
|
|
|
|
m_membersBook->SetSelection( 1 );
|
|
|
|
}
|
|
|
|
else if( row != m_lastAlias || aliasName != m_lastAliasName )
|
|
|
|
{
|
2022-08-26 16:02:59 +00:00
|
|
|
m_lastAlias = row;
|
|
|
|
m_lastAliasName = aliasName;
|
|
|
|
|
|
|
|
m_membersBook->SetSelection( 0 );
|
|
|
|
m_membersBook->GetPage( 0 )->Layout();
|
2022-08-21 11:26:47 +00:00
|
|
|
|
|
|
|
const std::shared_ptr<BUS_ALIAS>& alias = m_aliases[ row ];
|
|
|
|
alias->SetName( aliasName );
|
|
|
|
|
2022-09-09 11:18:05 +00:00
|
|
|
m_membersGridDirty = true;
|
2022-08-26 16:02:59 +00:00
|
|
|
Bind( wxEVT_IDLE, &PANEL_SETUP_BUSES::reloadMembersGridOnIdle, this );
|
2022-08-21 11:26:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|