/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2019 CERN * Copyright (C) 2019 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "lib_edit_tool.h" #include // for KiROUND LIB_EDIT_TOOL::LIB_EDIT_TOOL() : EE_TOOL_BASE( "eeschema.SymbolEditTool" ), m_pickerItem( nullptr ) { } bool LIB_EDIT_TOOL::Init() { EE_TOOL_BASE::Init(); LIB_DRAWING_TOOLS* drawingTools = m_toolMgr->GetTool(); LIB_MOVE_TOOL* moveTool = m_toolMgr->GetTool(); wxASSERT_MSG( drawingTools, "eeschema.SymbolDrawing tool is not available" ); auto havePartCondition = [&]( const SELECTION& sel ) { return m_isLibEdit && static_cast( m_frame )->GetCurPart(); }; // Add edit actions to the move tool menu // if( moveTool ) { CONDITIONAL_MENU& moveMenu = moveTool->GetToolMenu().GetMenu(); moveMenu.AddSeparator( 200 ); moveMenu.AddItem( EE_ACTIONS::rotateCCW, EE_CONDITIONS::NotEmpty, 200 ); moveMenu.AddItem( EE_ACTIONS::rotateCW, EE_CONDITIONS::NotEmpty, 200 ); moveMenu.AddItem( EE_ACTIONS::mirrorX, EE_CONDITIONS::NotEmpty, 200 ); moveMenu.AddItem( EE_ACTIONS::mirrorY, EE_CONDITIONS::NotEmpty, 200 ); moveMenu.AddItem( ACTIONS::doDelete, EE_CONDITIONS::NotEmpty, 200 ); moveMenu.AddItem( EE_ACTIONS::properties, EE_CONDITIONS::Count( 1 ), 200 ); moveMenu.AddSeparator( 300 ); moveMenu.AddItem( ACTIONS::cut, EE_CONDITIONS::IdleSelection, 300 ); moveMenu.AddItem( ACTIONS::copy, EE_CONDITIONS::IdleSelection, 300 ); moveMenu.AddItem( ACTIONS::duplicate, EE_CONDITIONS::NotEmpty, 300 ); moveMenu.AddSeparator( 400 ); moveMenu.AddItem( ACTIONS::selectAll, havePartCondition, 400 ); } // Add editing actions to the drawing tool menu // CONDITIONAL_MENU& drawMenu = drawingTools->GetToolMenu().GetMenu(); drawMenu.AddSeparator( 200 ); drawMenu.AddItem( EE_ACTIONS::rotateCCW, EE_CONDITIONS::IdleSelection, 200 ); drawMenu.AddItem( EE_ACTIONS::rotateCW, EE_CONDITIONS::IdleSelection, 200 ); drawMenu.AddItem( EE_ACTIONS::mirrorX, EE_CONDITIONS::IdleSelection, 200 ); drawMenu.AddItem( EE_ACTIONS::mirrorY, EE_CONDITIONS::IdleSelection, 200 ); drawMenu.AddItem( EE_ACTIONS::properties, EE_CONDITIONS::Count( 1 ), 200 ); // Add editing actions to the selection tool menu // CONDITIONAL_MENU& selToolMenu = m_selectionTool->GetToolMenu().GetMenu(); selToolMenu.AddItem( EE_ACTIONS::rotateCCW, EE_CONDITIONS::NotEmpty, 200 ); selToolMenu.AddItem( EE_ACTIONS::rotateCW, EE_CONDITIONS::NotEmpty, 200 ); selToolMenu.AddItem( EE_ACTIONS::mirrorX, EE_CONDITIONS::NotEmpty, 200 ); selToolMenu.AddItem( EE_ACTIONS::mirrorY, EE_CONDITIONS::NotEmpty, 200 ); selToolMenu.AddItem( ACTIONS::doDelete, EE_CONDITIONS::NotEmpty, 200 ); selToolMenu.AddItem( EE_ACTIONS::properties, EE_CONDITIONS::Count( 1 ), 200 ); selToolMenu.AddSeparator( 300 ); selToolMenu.AddItem( ACTIONS::cut, EE_CONDITIONS::IdleSelection, 300 ); selToolMenu.AddItem( ACTIONS::copy, EE_CONDITIONS::IdleSelection, 300 ); selToolMenu.AddItem( ACTIONS::paste, EE_CONDITIONS::Idle, 300 ); selToolMenu.AddItem( ACTIONS::duplicate, EE_CONDITIONS::NotEmpty, 300 ); selToolMenu.AddSeparator( 400 ); selToolMenu.AddItem( ACTIONS::selectAll, havePartCondition, 400 ); return true; } int LIB_EDIT_TOOL::Rotate( const TOOL_EVENT& aEvent ) { EE_SELECTION& selection = m_selectionTool->RequestSelection(); if( selection.GetSize() == 0 ) return 0; wxPoint rotPoint; bool ccw = ( aEvent.Matches( EE_ACTIONS::rotateCCW.MakeEvent() ) ); LIB_ITEM* item = static_cast( selection.Front() ); if( !item->IsMoving() ) saveCopyInUndoList( m_frame->GetCurPart(), UR_LIBEDIT ); if( selection.GetSize() == 1 ) rotPoint = item->GetPosition(); else rotPoint = m_frame->GetNearestGridPosition( mapCoords( selection.GetCenter() ) ); for( unsigned ii = 0; ii < selection.GetSize(); ii++ ) { item = static_cast( selection.GetItem( ii ) ); item->Rotate( rotPoint, ccw ); m_frame->UpdateItem( item ); } if( item->IsMoving() ) m_toolMgr->RunAction( ACTIONS::refreshPreview, true ); else { m_toolMgr->PostEvent( EVENTS::SelectedItemsModified ); if( selection.IsHover() ) m_toolMgr->RunAction( EE_ACTIONS::clearSelection, true ); m_frame->OnModify(); } return 0; } int LIB_EDIT_TOOL::Mirror( const TOOL_EVENT& aEvent ) { EE_SELECTION& selection = m_selectionTool->RequestSelection(); if( selection.GetSize() == 0 ) return 0; wxPoint mirrorPoint; bool xAxis = ( aEvent.Matches( EE_ACTIONS::mirrorX.MakeEvent() ) ); LIB_ITEM* item = static_cast( selection.Front() ); if( !item->IsMoving() ) saveCopyInUndoList( m_frame->GetCurPart(), UR_LIBEDIT ); if( selection.GetSize() == 1 ) mirrorPoint = item->GetPosition(); else mirrorPoint = m_frame->GetNearestGridPosition( mapCoords( selection.GetCenter() ) ); for( unsigned ii = 0; ii < selection.GetSize(); ii++ ) { item = static_cast( selection.GetItem( ii ) ); if( xAxis ) item->MirrorVertical( mirrorPoint ); else item->MirrorHorizontal( mirrorPoint ); m_frame->UpdateItem( item ); } m_toolMgr->PostEvent( EVENTS::SelectedItemsModified ); if( item->IsMoving() ) m_toolMgr->RunAction( ACTIONS::refreshPreview, true ); else { if( selection.IsHover() ) m_toolMgr->RunAction( EE_ACTIONS::clearSelection, true ); m_frame->OnModify(); } return 0; } static KICAD_T nonFields[] = { LIB_PART_T, LIB_ARC_T, LIB_CIRCLE_T, LIB_TEXT_T, LIB_RECTANGLE_T, LIB_POLYLINE_T, LIB_BEZIER_T, LIB_PIN_T, EOT }; int LIB_EDIT_TOOL::DoDelete( const TOOL_EVENT& aEvent ) { LIB_PART* part = m_frame->GetCurPart(); auto items = m_selectionTool->RequestSelection( nonFields ).GetItems(); if( items.empty() ) return 0; // Don't leave a freed pointer in the selection m_toolMgr->RunAction( EE_ACTIONS::clearSelection, true ); saveCopyInUndoList( part, UR_LIBEDIT ); std::set toDelete; for( EDA_ITEM* item : items ) { if( item->Type() == LIB_PIN_T ) { LIB_PIN* pin = static_cast( item ); wxPoint pos = pin->GetPosition(); toDelete.insert( pin ); // when pin editing is synchronized, all pins of the same body style are removed: if( m_frame->SynchronizePins() ) { int curr_convert = pin->GetConvert(); LIB_PIN* next_pin = part->GetNextPin(); while( next_pin != NULL ) { pin = next_pin; next_pin = part->GetNextPin( pin ); if( pin->GetPosition() != pos ) continue; if( pin->GetConvert() != curr_convert ) continue; toDelete.insert( pin ); } } } else { toDelete.insert( (LIB_ITEM*) item ); } } for( auto item : toDelete ) { part->RemoveDrawItem( item ); } m_frame->RebuildView(); m_frame->OnModify(); return 0; } #define HITTEST_THRESHOLD_PIXELS 5 int LIB_EDIT_TOOL::DeleteItemCursor( const TOOL_EVENT& aEvent ) { std::string tool = aEvent.GetCommandStr().get(); PICKER_TOOL* picker = m_toolMgr->GetTool(); m_toolMgr->RunAction( EE_ACTIONS::clearSelection, true ); m_pickerItem = nullptr; // Deactivate other tools; particularly important if another PICKER is currently running Activate(); picker->SetCursor( wxStockCursor( wxCURSOR_BULLSEYE ) ); picker->SetClickHandler( [this] ( const VECTOR2D& aPosition ) -> bool { if( m_pickerItem ) { EE_SELECTION_TOOL* selectionTool = m_toolMgr->GetTool(); selectionTool->UnbrightenItem( m_pickerItem ); selectionTool->AddItemToSel( m_pickerItem, true /*quiet mode*/ ); m_toolMgr->RunAction( ACTIONS::doDelete, true ); m_pickerItem = nullptr; } return true; } ); picker->SetMotionHandler( [this] ( const VECTOR2D& aPos ) { EE_SELECTION_TOOL* selectionTool = m_toolMgr->GetTool(); EE_COLLECTOR collector; collector.m_Threshold = KiROUND( getView()->ToWorld( HITTEST_THRESHOLD_PIXELS ) ); collector.Collect( m_frame->GetScreen(), nonFields, (wxPoint) aPos, m_frame->GetUnit(), m_frame->GetConvert() ); // Remove unselectable items for( int i = collector.GetCount() - 1; i >= 0; --i ) { if( !selectionTool->Selectable( collector[ i ] ) ) collector.Remove( i ); } if( collector.GetCount() > 1 ) selectionTool->GuessSelectionCandidates( collector, aPos ); EDA_ITEM* item = collector.GetCount() == 1 ? collector[ 0 ] : nullptr; if( m_pickerItem != item ) { if( m_pickerItem ) selectionTool->UnbrightenItem( m_pickerItem ); m_pickerItem = item; if( m_pickerItem ) selectionTool->BrightenItem( m_pickerItem ); } } ); picker->SetFinalizeHandler( [this] ( const int& aFinalState ) { if( m_pickerItem ) m_toolMgr->GetTool()->UnbrightenItem( m_pickerItem ); } ); m_toolMgr->RunAction( ACTIONS::pickerTool, true, &tool ); return 0; } int LIB_EDIT_TOOL::Properties( const TOOL_EVENT& aEvent ) { EE_SELECTION& selection = m_selectionTool->RequestSelection(); if( selection.Empty() || aEvent.IsAction( &EE_ACTIONS::symbolProperties ) ) { if( m_frame->GetCurPart() ) editSymbolProperties(); } else if( selection.Size() == 1 ) { LIB_ITEM* item = (LIB_ITEM*) selection.Front(); // Save copy for undo if not in edit (edit command already handle the save copy) if( item->GetEditFlags() == 0 ) saveCopyInUndoList( item->GetParent(), UR_LIBEDIT ); switch( item->Type() ) { case LIB_PIN_T: { LIB_PIN_TOOL* pinTool = m_toolMgr->GetTool(); if( pinTool ) pinTool->EditPinProperties( (LIB_PIN*) item ); break; } case LIB_ARC_T: case LIB_CIRCLE_T: case LIB_RECTANGLE_T: case LIB_POLYLINE_T: editGraphicProperties( item ); break; case LIB_TEXT_T: editTextProperties( item ); break; case LIB_FIELD_T: editFieldProperties( (LIB_FIELD*) item ); break; default: wxFAIL_MSG( wxT( "Unhandled item <" ) + item->GetClass() + wxT( ">" ) ); break; } } m_toolMgr->PostEvent( EVENTS::SelectedItemsModified ); return 0; } void LIB_EDIT_TOOL::editGraphicProperties( LIB_ITEM* aItem ) { if( aItem == NULL ) return; DIALOG_LIB_EDIT_DRAW_ITEM dialog( m_frame, aItem ); if( dialog.ShowModal() != wxID_OK ) return; if( aItem->IsFillable() ) aItem->SetFillMode( (FILL_T) dialog.GetFillStyle() ); aItem->SetWidth( dialog.GetWidth() ); if( dialog.GetApplyToAllConversions() ) aItem->SetConvert( 0 ); else aItem->SetConvert( m_frame->GetConvert() ); if( dialog.GetApplyToAllUnits() ) aItem->SetUnit( 0 ); else aItem->SetUnit( m_frame->GetUnit() ); updateView( aItem ); m_frame->GetCanvas()->Refresh(); m_frame->OnModify( ); LIB_DRAWING_TOOLS* drawingTools = m_toolMgr->GetTool(); drawingTools->SetDrawSpecificConvert( !dialog.GetApplyToAllConversions() ); drawingTools->SetDrawSpecificUnit( !dialog.GetApplyToAllUnits() ); MSG_PANEL_ITEMS items; aItem->GetMsgPanelInfo( m_frame, items ); m_frame->SetMsgPanel( items ); } void LIB_EDIT_TOOL::editTextProperties( LIB_ITEM* aItem ) { if ( ( aItem == NULL ) || ( aItem->Type() != LIB_TEXT_T ) ) return; DIALOG_LIB_EDIT_TEXT dlg( m_frame, (LIB_TEXT*) aItem ); if( dlg.ShowModal() != wxID_OK ) return; updateView( aItem ); m_frame->GetCanvas()->Refresh(); m_frame->OnModify( ); } void LIB_EDIT_TOOL::editFieldProperties( LIB_FIELD* aField ) { if( aField == NULL ) return; wxString caption; LIB_PART* parent = aField->GetParent(); wxCHECK( parent, /* void */ ); // Editing the component value field is equivalent to creating a new component based // on the current component. Set the dialog message to inform the user. if( aField->GetId() == VALUE ) caption = _( "Edit Component Name" ); else caption.Printf( _( "Edit %s Field" ), aField->GetName() ); DIALOG_LIB_EDIT_ONE_FIELD dlg( m_frame, caption, aField ); // The dialog may invoke a kiway player for footprint fields // so we must use a quasimodal dialog. if( dlg.ShowQuasiModal() != wxID_OK ) return; wxString newFieldValue = LIB_ID::FixIllegalChars( dlg.GetText(), LIB_ID::ID_SCH ); wxString oldFieldValue = aField->GetFullText( m_frame->GetUnit() ); bool renamed = aField->GetId() == VALUE && newFieldValue != oldFieldValue; if( renamed ) saveCopyInUndoList( parent, UR_LIB_RENAME ); else saveCopyInUndoList( parent, UR_LIBEDIT ); dlg.UpdateField( aField ); if( renamed ) { parent->SetName( newFieldValue ); m_frame->UpdateAfterSymbolProperties( &oldFieldValue ); } else { updateView( aField ); m_frame->GetCanvas()->Refresh(); m_frame->OnModify(); m_frame->DisplayCmpDoc(); } } void LIB_EDIT_TOOL::editSymbolProperties() { LIB_PART* part = m_frame->GetCurPart(); bool partLocked = part->UnitsLocked(); wxString oldName = part->GetName(); m_toolMgr->RunAction( ACTIONS::cancelInteractive, true ); m_toolMgr->RunAction( EE_ACTIONS::clearSelection, true ); DIALOG_EDIT_COMPONENT_IN_LIBRARY dlg( m_frame, part ); // This dialog itself subsequently can invoke a KIWAY_PLAYER as a quasimodal // frame. Therefore this dialog as a modal frame parent, MUST be run under // quasimodal mode for the quasimodal frame support to work. So don't use // the QUASIMODAL macros here. if( dlg.ShowQuasiModal() != wxID_OK ) return; m_frame->OnModify(); // if m_UnitSelectionLocked has changed, set some edit options or defaults // to the best value if( partLocked != part->UnitsLocked() ) { LIB_DRAWING_TOOLS* tools = m_toolMgr->GetTool(); // Enable synchronized pin edit mode for symbols with interchangeable units m_frame->m_SyncPinEdit = !part->UnitsLocked(); // also set default edit options to the better value // Usually if units are locked, graphic items are specific to each unit // and if units are interchangeable, graphic items are common to units tools->SetDrawSpecificUnit( part->UnitsLocked() ); } } int LIB_EDIT_TOOL::PinTable( const TOOL_EVENT& aEvent ) { LIB_PART* part = m_frame->GetCurPart(); if( !part ) return 0; m_toolMgr->RunAction( EE_ACTIONS::clearSelection, true ); saveCopyInUndoList( part, UR_LIBEDIT ); DIALOG_LIB_EDIT_PIN_TABLE dlg( m_frame, part ); if( dlg.ShowModal() == wxID_CANCEL ) return -1; m_frame->RebuildView(); m_frame->OnModify(); return 0; } int LIB_EDIT_TOOL::Undo( const TOOL_EVENT& aEvent ) { m_frame->GetComponentFromUndoList(); EE_SELECTION_TOOL* selTool = m_toolMgr->GetTool(); selTool->RebuildSelection(); return 0; } int LIB_EDIT_TOOL::Redo( const TOOL_EVENT& aEvent ) { m_frame->GetComponentFromRedoList(); EE_SELECTION_TOOL* selTool = m_toolMgr->GetTool(); selTool->RebuildSelection(); return 0; } int LIB_EDIT_TOOL::Cut( const TOOL_EVENT& aEvent ) { int retVal = Copy( aEvent ); if( retVal == 0 ) retVal = DoDelete( aEvent ); return retVal; } int LIB_EDIT_TOOL::Copy( const TOOL_EVENT& aEvent ) { LIB_PART* part = m_frame->GetCurPart(); EE_SELECTION& selection = m_selectionTool->RequestSelection( nonFields ); if( !part || !selection.GetSize() ) return 0; for( LIB_ITEM& item : part->GetDrawItems() ) { if( item.Type() == LIB_FIELD_T ) continue; wxASSERT( !item.HasFlag( STRUCT_DELETED ) ); if( !item.IsSelected() ) item.SetFlags( STRUCT_DELETED ); } LIB_PART* partCopy = new LIB_PART( *part ); STRING_FORMATTER formatter; SCH_SEXPR_PLUGIN::FormatPart( partCopy, formatter ); delete partCopy; for( LIB_ITEM& item : part->GetDrawItems() ) item.ClearFlags( STRUCT_DELETED ); if( m_toolMgr->SaveClipboard( formatter.GetString() ) ) return 0; else return -1; } int LIB_EDIT_TOOL::Paste( const TOOL_EVENT& aEvent ) { LIB_PART* part = m_frame->GetCurPart(); if( !part ) return 0; std::string text = m_toolMgr->GetClipboard(); STRING_LINE_READER reader( text, "Clipboard" ); LIB_PART* newPart; try { newPart = SCH_SEXPR_PLUGIN::ParsePart( reader ); } catch( IO_ERROR& ) { // If it's not a part then paste as text newPart = new LIB_PART( "dummy_part" ); LIB_TEXT* newText = new LIB_TEXT( newPart ); newText->SetText( text ); newPart->AddDrawItem( newText ); } if( !newPart ) return -1; m_frame->SaveCopyInUndoList( part ); m_selectionTool->ClearSelection(); for( LIB_ITEM& item : part->GetDrawItems() ) item.ClearFlags( IS_NEW | IS_PASTED | SELECTED ); for( LIB_ITEM& item : newPart->GetDrawItems() ) { if( item.Type() == LIB_FIELD_T ) continue; LIB_ITEM* newItem = (LIB_ITEM*) item.Clone(); newItem->SetParent( part ); newItem->SetFlags( IS_NEW | IS_PASTED | SELECTED ); newItem->SetUnit( newItem->GetUnit() ? m_frame->GetUnit() : 0 ); newItem->SetConvert( newItem->GetConvert() ? m_frame->GetConvert() : 0 ); part->GetDrawItems().push_back( newItem ); getView()->Add( newItem ); } delete newPart; m_selectionTool->RebuildSelection(); EE_SELECTION& selection = m_selectionTool->GetSelection(); if( !selection.Empty() ) { selection.SetReferencePoint( getViewControls()->GetCursorPosition( true ) ); m_toolMgr->RunAction( EE_ACTIONS::move, false ); } return 0; } int LIB_EDIT_TOOL::Duplicate( const TOOL_EVENT& aEvent ) { LIB_PART* part = m_frame->GetCurPart(); EE_SELECTION& selection = m_selectionTool->RequestSelection( nonFields ); if( selection.GetSize() == 0 ) return 0; // Doing a duplicate of a new object doesn't really make any sense; we'd just end // up dragging around a stack of objects... if( selection.Front()->IsNew() ) return 0; if( !selection.Front()->IsMoving() ) saveCopyInUndoList( m_frame->GetCurPart(), UR_LIBEDIT ); EDA_ITEMS newItems; for( unsigned ii = 0; ii < selection.GetSize(); ++ii ) { LIB_ITEM* oldItem = static_cast( selection.GetItem( ii ) ); LIB_ITEM* newItem = (LIB_ITEM*) oldItem->Clone(); oldItem->ClearFlags( IS_NEW | IS_PASTED | SELECTED ); newItem->SetFlags( IS_NEW | IS_PASTED | SELECTED ); newItem->SetParent( part ); newItems.push_back( newItem ); part->GetDrawItems().push_back( newItem ); getView()->Add( newItem ); } m_toolMgr->RunAction( EE_ACTIONS::clearSelection, true ); m_toolMgr->RunAction( EE_ACTIONS::addItemsToSel, true, &newItems ); selection.SetReferencePoint( mapCoords( getViewControls()->GetCursorPosition( true ) ) ); m_toolMgr->RunAction( EE_ACTIONS::move, false ); return 0; } void LIB_EDIT_TOOL::setTransitions() { Go( &LIB_EDIT_TOOL::Undo, ACTIONS::undo.MakeEvent() ); Go( &LIB_EDIT_TOOL::Redo, ACTIONS::redo.MakeEvent() ); Go( &LIB_EDIT_TOOL::Cut, ACTIONS::cut.MakeEvent() ); Go( &LIB_EDIT_TOOL::Copy, ACTIONS::copy.MakeEvent() ); Go( &LIB_EDIT_TOOL::Paste, ACTIONS::paste.MakeEvent() ); Go( &LIB_EDIT_TOOL::Duplicate, ACTIONS::duplicate.MakeEvent() ); Go( &LIB_EDIT_TOOL::Rotate, EE_ACTIONS::rotateCW.MakeEvent() ); Go( &LIB_EDIT_TOOL::Rotate, EE_ACTIONS::rotateCCW.MakeEvent() ); Go( &LIB_EDIT_TOOL::Mirror, EE_ACTIONS::mirrorX.MakeEvent() ); Go( &LIB_EDIT_TOOL::Mirror, EE_ACTIONS::mirrorY.MakeEvent() ); Go( &LIB_EDIT_TOOL::DoDelete, ACTIONS::doDelete.MakeEvent() ); Go( &LIB_EDIT_TOOL::DeleteItemCursor, ACTIONS::deleteTool.MakeEvent() ); Go( &LIB_EDIT_TOOL::Properties, EE_ACTIONS::properties.MakeEvent() ); Go( &LIB_EDIT_TOOL::Properties, EE_ACTIONS::symbolProperties.MakeEvent() ); Go( &LIB_EDIT_TOOL::PinTable, EE_ACTIONS::pinTable.MakeEvent() ); }