7
mirror of https://gitlab.com/kicad/code/kicad.git synced 2024-11-25 16:15:01 +00:00
kicad/pcbnew/dialogs/dialog_shape_properties.cpp

1274 lines
38 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2019 Jean-Pierre Charras jp.charras at wanadoo.fr
* Copyright (C) 1992-2023 KiCad Developers, see AUTHORS.txt for contributors.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, 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
*/
/*
* Edit properties of Lines, Circles, Arcs and Polygons for PCBNew and Footprint Editor
*/
#include <pcb_base_edit_frame.h>
#include <pcb_edit_frame.h>
#include <wx/valnum.h>
#include <board_commit.h>
#include <pcb_layer_box_selector.h>
#include <dialogs/html_message_box.h>
#include <tool/tool_manager.h>
#include <tool/actions.h>
#include <pcb_shape.h>
#include <macros.h>
#include <widgets/unit_binder.h>
#include <dialog_shape_properties_base.h>
#include <tools/drawing_tool.h>
struct BOUND_CONTROL
{
std::unique_ptr<UNIT_BINDER> m_Binder;
wxTextCtrl* m_Ctrl;
};
/**
* A class that operates over a list of BOUND_CONTROLs
* and keeps them in sync with a PCB_SHAPE. Exactly how that is done
* depends on the kind of shape.
*
* Inherit from this class and implement the relvant update functions
* and listen for changes on the right controls for each mode
* (e.g. edit line segment by endpoints).
*/
class GEOM_SYNCER : public wxEvtHandler
{
public:
GEOM_SYNCER( PCB_SHAPE& aShape, std::vector<BOUND_CONTROL>& aBoundCtrls ) :
m_shape( aShape ), m_boundCtrls( aBoundCtrls )
{
}
void BindCtrls( size_t aFrom, size_t aTo, std::function<void()> aCb )
{
wxCHECK( aFrom < m_boundCtrls.size(), /* void */ );
wxCHECK( aTo < m_boundCtrls.size(), /* void */ );
for( size_t i = aFrom; i <= aTo; ++i )
{
m_boundCtrls[i].m_Ctrl->Bind( wxEVT_TEXT,
[aCb]( wxCommandEvent& aEvent )
{
aCb();
} );
}
}
void SetShape( PCB_SHAPE& aShape )
{
m_shape = aShape;
updateAll();
}
virtual bool Validate( wxArrayString& aErrs ) const { return true; }
protected:
virtual void updateAll() = 0;
wxTextCtrl* GetCtrl( size_t aIndex ) const
{
wxCHECK( aIndex < m_boundCtrls.size(), nullptr );
return m_boundCtrls[aIndex].m_Ctrl;
}
int GetIntValue( size_t aIndex ) const
{
wxCHECK( aIndex < m_boundCtrls.size(), 0.0 );
return static_cast<int>( m_boundCtrls[aIndex].m_Binder->GetValue() );
}
EDA_ANGLE GetAngleValue( size_t aIndex ) const
{
wxCHECK( aIndex < m_boundCtrls.size(), EDA_ANGLE() );
return m_boundCtrls[aIndex].m_Binder->GetAngleValue();
}
void ChangeValue( size_t aIndex, int aValue )
{
wxCHECK( aIndex < m_boundCtrls.size(), /* void */ );
m_boundCtrls[aIndex].m_Binder->ChangeValue( aValue );
}
void ChangeAngleValue( size_t aIndex, const EDA_ANGLE& aValue )
{
wxCHECK( aIndex < m_boundCtrls.size(), /* void */ );
m_boundCtrls[aIndex].m_Binder->ChangeAngleValue( aValue );
}
PCB_SHAPE& GetShape() { return m_shape; }
const PCB_SHAPE& GetShape() const { return m_shape; }
private:
PCB_SHAPE& m_shape;
std::vector<BOUND_CONTROL>& m_boundCtrls;
};
/**
* Class that keeps a rectangle's various fields all up to date.
*/
class RECTANGLE_GEOM_SYNCER : public GEOM_SYNCER
{
public:
enum CTRL_IDX
{
START_X = 0,
START_Y,
END_X,
END_Y,
CORNER_X,
CORNER_Y,
CORNER_W,
CORNER_H,
CENTER_X,
CENTER_Y,
CENTER_W,
CENTER_H,
NUM_CTRLS,
};
RECTANGLE_GEOM_SYNCER( PCB_SHAPE& aShape, std::vector<BOUND_CONTROL>& aBoundCtrls ) :
GEOM_SYNCER( aShape, aBoundCtrls )
{
wxASSERT( aBoundCtrls.size() == NUM_CTRLS );
wxASSERT( GetShape().GetShape() == SHAPE_T::RECTANGLE );
BindCtrls( START_X, END_Y,
[this]()
{
OnCornersChange();
} );
BindCtrls( CORNER_X, CORNER_H,
[this]()
{
OnCornerSizeChange();
} );
BindCtrls( CENTER_X, CENTER_H,
[this]()
{
OnCenterSizeChange();
} );
}
bool Validate( wxArrayString& aErrs ) const override
{
const VECTOR2I p0{ GetIntValue( START_X ), GetIntValue( START_Y ) };
const VECTOR2I p1{ GetIntValue( END_X ), GetIntValue( END_Y ) };
if( p0 == p1 )
{
aErrs.push_back( _( "Rectangle cannot be zero-sized." ) );
return false;
}
return true;
}
void updateAll() override
{
updateCorners();
updateCornerSize();
updateCenterSize();
}
void OnCornersChange()
{
const VECTOR2I p0{ GetIntValue( START_X ), GetIntValue( START_Y ) };
const VECTOR2I p1{ GetIntValue( END_X ), GetIntValue( END_Y ) };
GetShape().SetStart( p0 );
GetShape().SetEnd( p1 );
updateCenterSize();
updateCornerSize();
}
void updateCorners()
{
const VECTOR2I p0 = GetShape().GetStart();
const VECTOR2I p1 = GetShape().GetEnd();
ChangeValue( START_X, p0.x );
ChangeValue( START_Y, p0.y );
ChangeValue( END_X, p1.x );
ChangeValue( END_Y, p1.y );
}
void OnCornerSizeChange()
{
const VECTOR2I p0{ GetIntValue( CORNER_X ), GetIntValue( CORNER_Y ) };
const VECTOR2I size{ GetIntValue( CORNER_W ), GetIntValue( CORNER_H ) };
GetShape().SetStart( p0 );
GetShape().SetEnd( p0 + size );
updateCorners();
updateCenterSize();
}
void updateCornerSize()
{
const VECTOR2I p0 = GetShape().GetStart();
ChangeValue( CORNER_X, p0.x );
ChangeValue( CORNER_Y, p0.y );
ChangeValue( CORNER_W, GetShape().GetRectangleWidth() );
ChangeValue( CORNER_H, GetShape().GetRectangleHeight() );
}
void OnCenterSizeChange()
{
const VECTOR2I center = { GetIntValue( CENTER_X ), GetIntValue( CENTER_Y ) };
const VECTOR2I size = { GetIntValue( CENTER_W ), GetIntValue( CENTER_H ) };
GetShape().SetStart( center - size / 2 );
GetShape().SetEnd( center + size / 2 );
updateCorners();
updateCornerSize();
}
void updateCenterSize()
{
const VECTOR2I c = GetShape().GetCenter();
ChangeValue( CENTER_X, c.x );
ChangeValue( CENTER_Y, c.y );
ChangeValue( CENTER_W, GetShape().GetRectangleWidth() );
ChangeValue( CENTER_H, GetShape().GetRectangleHeight() );
}
};
class LINE_GEOM_SYNCER : public GEOM_SYNCER
{
public:
enum CTRL_IDX
{
START_X = 0,
START_Y,
END_X,
END_Y,
POLAR_START_X,
POLAR_START_Y,
LENGTH,
ANGLE,
MID_X,
MID_Y,
MID_END_X,
MID_END_Y,
NUM_CTRLS,
};
LINE_GEOM_SYNCER( PCB_SHAPE& aShape, std::vector<BOUND_CONTROL>& aBoundCtrls ) :
GEOM_SYNCER( aShape, aBoundCtrls )
{
wxASSERT( aBoundCtrls.size() == NUM_CTRLS );
wxASSERT( GetShape().GetShape() == SHAPE_T::SEGMENT );
BindCtrls( START_X, END_Y,
[this]()
{
OnEndsChange();
} );
BindCtrls( POLAR_START_X, ANGLE,
[this]()
{
OnPolarChange();
} );
BindCtrls( MID_X, MID_END_Y,
[this]()
{
OnMidEndpointChange();
} );
}
void updateAll() override
{
updateEnds();
updatePolar();
updateMidEndpoint();
}
void OnEndsChange()
{
const VECTOR2I p0{ GetIntValue( START_X ), GetIntValue( START_Y ) };
const VECTOR2I p1{ GetIntValue( END_X ), GetIntValue( END_Y ) };
GetShape().SetStart( p0 );
GetShape().SetEnd( p1 );
updatePolar();
updateMidEndpoint();
}
void updateEnds()
{
const VECTOR2I p0 = GetShape().GetStart();
const VECTOR2I p1 = GetShape().GetEnd();
ChangeValue( START_X, p0.x );
ChangeValue( START_Y, p0.y );
ChangeValue( END_X, p1.x );
ChangeValue( END_Y, p1.y );
}
void OnPolarChange()
{
const VECTOR2I p0{ GetIntValue( POLAR_START_X ), GetIntValue( POLAR_START_Y ) };
const int length = GetIntValue( LENGTH );
const EDA_ANGLE angle = GetAngleValue( ANGLE );
VECTOR2I polar = GetRotated( VECTOR2I{ length, 0 }, angle );
GetShape().SetStart( p0 );
GetShape().SetEnd( polar );
updateEnds();
updateMidEndpoint();
}
void updatePolar()
{
const VECTOR2I p0 = GetShape().GetStart();
const VECTOR2I p1 = GetShape().GetEnd();
ChangeValue( POLAR_START_X, p0.x );
ChangeValue( POLAR_START_Y, p0.y );
ChangeValue( LENGTH, p0.Distance( p1 ) );
ChangeAngleValue( ANGLE, -EDA_ANGLE( p1 - p0 ) );
}
void OnMidEndpointChange()
{
const VECTOR2I mid{ GetIntValue( MID_X ), GetIntValue( MID_Y ) };
const VECTOR2I end{ GetIntValue( MID_END_X ), GetIntValue( MID_END_Y ) };
GetShape().SetStart( mid - ( end - mid ) );
GetShape().SetEnd( mid + ( end - mid ) );
updateEnds();
updatePolar();
}
void updateMidEndpoint()
{
const VECTOR2I c = GetShape().GetCenter();
const VECTOR2I e = GetShape().GetStart();
ChangeValue( MID_X, c.x );
ChangeValue( MID_Y, c.y );
ChangeValue( MID_END_X, e.x );
ChangeValue( MID_END_Y, e.y );
}
};
class ARC_GEOM_SYNCER : public GEOM_SYNCER
{
public:
enum CTRL_IDX
{
//CSA
CSA_CENTER_X = 0,
CSA_CENTER_Y,
CSA_START_X,
CSA_START_Y,
CSA_ANGLE,
SME_START_X,
SME_START_Y,
SME_MID_X,
SME_MID_Y,
SME_END_X,
SME_END_Y,
NUM_CTRLS
};
ARC_GEOM_SYNCER( PCB_SHAPE& aShape, std::vector<BOUND_CONTROL>& aBoundCtrls ) :
GEOM_SYNCER( aShape, aBoundCtrls )
{
wxASSERT( aBoundCtrls.size() == NUM_CTRLS );
wxASSERT( GetShape().GetShape() == SHAPE_T::ARC );
BindCtrls( CSA_CENTER_X, CSA_ANGLE,
[this]()
{
OnCSAChange();
} );
BindCtrls( SME_START_X, SME_END_Y,
[this]()
{
OnSMEChange();
} );
}
bool Validate( wxArrayString& aErrs ) const override
{
const EDA_ANGLE angle = GetAngleValue( CSA_ANGLE );
if( angle == 0 )
{
aErrs.push_back( _( "Arc angle must be greater than 0" ) );
return false;
}
const VECTOR2I start{ GetIntValue( SME_START_X ), GetIntValue( SME_START_Y ) };
const VECTOR2I mid{ GetIntValue( SME_MID_X ), GetIntValue( SME_MID_Y ) };
const VECTOR2I end{ GetIntValue( SME_END_X ), GetIntValue( SME_END_Y ) };
if( start == mid || mid == end || start == end )
{
aErrs.push_back( _( "Arc must have 3 distinct points" ) );
return false;
}
else
{
const VECTOR2D center = CalcArcCenter( start, end, angle );
double radius = ( center - start ).EuclideanNorm();
double max_offset = std::max( std::abs( center.x ), std::abs( center.y ) ) + radius;
VECTOR2I center_i = VECTOR2I( center.x, center.y );
if( max_offset >= ( std::numeric_limits<VECTOR2I::coord_type>::max() / 2.0 )
|| center_i == start || center_i == end )
{
aErrs.push_back( wxString::Format( _( "Invalid Arc with radius %f and angle %f." ),
radius, angle.AsDegrees() ) );
return false;
}
}
return true;
}
void updateAll() override
{
updateCSA();
updateSME();
}
void OnCSAChange()
{
const VECTOR2I center{ GetIntValue( CSA_CENTER_X ), GetIntValue( CSA_CENTER_Y ) };
const VECTOR2I start{ GetIntValue( CSA_START_X ), GetIntValue( CSA_START_Y ) };
const int angle = GetIntValue( CSA_ANGLE );
GetShape().SetCenter( center );
GetShape().SetStart( start );
GetShape().SetArcAngleAndEnd( angle );
updateSME();
}
void updateCSA()
{
const VECTOR2I center = GetShape().GetCenter();
const VECTOR2I start = GetShape().GetStart();
ChangeValue( CSA_CENTER_X, center.x );
ChangeValue( CSA_CENTER_Y, center.y );
ChangeValue( CSA_START_X, start.x );
ChangeValue( CSA_START_Y, start.y );
ChangeAngleValue( CSA_ANGLE, GetShape().GetArcAngle() );
}
void OnSMEChange()
{
const VECTOR2I p0{ GetIntValue( SME_START_X ), GetIntValue( SME_START_Y ) };
const VECTOR2I p1{ GetIntValue( SME_MID_X ), GetIntValue( SME_MID_Y ) };
const VECTOR2I p2{ GetIntValue( SME_END_X ), GetIntValue( SME_END_Y ) };
GetShape().SetArcGeometry( p0, p1, p2 );
updateCSA();
}
void updateSME()
{
const VECTOR2I p0 = GetShape().GetStart();
const VECTOR2I p1 = GetShape().GetArcMid();
const VECTOR2I p2 = GetShape().GetEnd();
ChangeValue( SME_START_X, p0.x );
ChangeValue( SME_START_Y, p0.y );
ChangeValue( SME_MID_X, p1.x );
ChangeValue( SME_MID_Y, p1.y );
ChangeValue( SME_END_X, p2.x );
ChangeValue( SME_END_Y, p2.y );
}
};
class CIRCLE_GEOM_SYNCER : public GEOM_SYNCER
{
public:
enum CTRL_IDX
{
CENTER_X = 0,
CENTER_Y,
RADIUS,
CENTER_PT_X,
CENTER_PT_Y,
PT_PT_X,
PT_PT_Y,
NUM_CTRLS,
};
CIRCLE_GEOM_SYNCER( PCB_SHAPE& aShape, std::vector<BOUND_CONTROL>& aBoundCtrls ) :
GEOM_SYNCER( aShape, aBoundCtrls )
{
wxASSERT( aBoundCtrls.size() == NUM_CTRLS );
wxASSERT( GetShape().GetShape() == SHAPE_T::CIRCLE );
BindCtrls( CENTER_X, RADIUS,
[this]()
{
OnCenterRadiusChange();
} );
BindCtrls( CENTER_PT_X, PT_PT_Y,
[this]()
{
OnCenterPointChange();
} );
}
void updateAll() override
{
updateCenterRadius();
updateCenterPoint();
}
bool Validate( wxArrayString& aErrs ) const override
{
if( GetIntValue( RADIUS ) <= 0 )
{
aErrs.push_back( _( "Radius must be greater than 0" ) );
return false;
}
return true;
}
void OnCenterRadiusChange()
{
const VECTOR2I center{ GetIntValue( CENTER_X ), GetIntValue( CENTER_Y ) };
const int radius = GetIntValue( RADIUS );
GetShape().SetCenter( center );
GetShape().SetRadius( radius );
updateCenterPoint();
}
void updateCenterRadius()
{
const VECTOR2I center = GetShape().GetCenter();
ChangeValue( CENTER_X, center.x );
ChangeValue( CENTER_Y, center.y );
ChangeValue( RADIUS, GetShape().GetRadius() );
}
void OnCenterPointChange()
{
const VECTOR2I center{ GetIntValue( CENTER_PT_X ), GetIntValue( CENTER_PT_Y ) };
const VECTOR2I pt{ GetIntValue( PT_PT_X ), GetIntValue( PT_PT_Y ) };
GetShape().SetCenter( center );
GetShape().SetEnd( pt );
updateCenterRadius();
}
void updateCenterPoint()
{
const VECTOR2I center = GetShape().GetCenter();
const VECTOR2I pt = GetShape().GetEnd();
ChangeValue( CENTER_PT_X, center.x );
ChangeValue( CENTER_PT_Y, center.y );
ChangeValue( PT_PT_X, pt.x );
ChangeValue( PT_PT_Y, pt.y );
}
};
class BEZIER_GEOM_SYNCER : public GEOM_SYNCER
{
public:
enum CTRL_IDX
{
START_X = 0,
START_Y,
END_X,
END_Y,
CTRL1_X,
CTRL1_Y,
CTRL2_X,
CTRL2_Y,
NUM_CTRLS,
};
BEZIER_GEOM_SYNCER( PCB_SHAPE& aShape, std::vector<BOUND_CONTROL>& aBoundCtrls ) :
GEOM_SYNCER( aShape, aBoundCtrls )
{
wxASSERT( aBoundCtrls.size() == NUM_CTRLS );
wxASSERT( GetShape().GetShape() == SHAPE_T::BEZIER );
BindCtrls( START_X, CTRL2_Y,
[this]()
{
OnBezierChange();
} );
}
void updateAll() override
{
updateBezier();
}
void OnBezierChange()
{
const VECTOR2I p0{ GetIntValue( START_X ), GetIntValue( START_Y ) };
const VECTOR2I p1{ GetIntValue( END_X ), GetIntValue( END_Y ) };
const VECTOR2I c1{ GetIntValue( CTRL1_X ), GetIntValue( CTRL1_Y ) };
const VECTOR2I c2{ GetIntValue( CTRL2_X ), GetIntValue( CTRL2_Y ) };
GetShape().SetStart( p0 );
GetShape().SetEnd( p1 );
GetShape().SetBezierC1( c1 );
GetShape().SetBezierC2( c2 );
}
void updateBezier()
{
const VECTOR2I p0 = GetShape().GetStart();
const VECTOR2I p1 = GetShape().GetEnd();
const VECTOR2I c1 = GetShape().GetBezierC1();
const VECTOR2I c2 = GetShape().GetBezierC2();
ChangeValue( START_X, p0.x );
ChangeValue( START_Y, p0.y );
ChangeValue( END_X, p1.x );
ChangeValue( END_Y, p1.y );
ChangeValue( CTRL1_X, c1.x );
ChangeValue( CTRL1_Y, c1.y );
ChangeValue( CTRL2_X, c2.x );
ChangeValue( CTRL2_Y, c2.y );
}
};
class DIALOG_SHAPE_PROPERTIES : public DIALOG_SHAPE_PROPERTIES_BASE
{
public:
DIALOG_SHAPE_PROPERTIES( PCB_BASE_EDIT_FRAME* aParent, PCB_SHAPE* aShape );
~DIALOG_SHAPE_PROPERTIES() {};
private:
bool TransferDataToWindow() override;
bool TransferDataFromWindow() override;
void onFilledCheckbox( wxCommandEvent& event ) override;
void onLayerSelection( wxCommandEvent& event ) override;
void onTechLayersChanged( wxCommandEvent& event ) override;
bool Validate() override;
// Show/hide the widgets used in net selection (shown only for copper layers)
void showHideNetInfo()
{
bool isCopper = IsCopperLayer( m_LayerSelectionCtrl->GetLayerSelection() );
m_netSelector->Show( isCopper );
m_netLabel->Show( isCopper );
}
void showHideTechLayers()
{
bool isExtCopper = IsExternalCopperLayer( m_LayerSelectionCtrl->GetLayerSelection() );
m_techLayersLabel->Enable( isExtCopper );
m_hasSolderMask->Enable( isExtCopper );
bool showMaskMargin = isExtCopper && m_hasSolderMask->GetValue();
m_solderMaskMarginLabel->Enable( showMaskMargin );
m_solderMaskMarginCtrl->Enable( showMaskMargin );
m_solderMaskMarginUnit->Enable( showMaskMargin );
}
private:
PCB_BASE_EDIT_FRAME* m_parent;
PCB_SHAPE* m_item;
UNIT_BINDER m_thickness;
UNIT_BINDER m_solderMaskMargin;
std::vector<BOUND_CONTROL> m_boundCtrls;
std::unique_ptr<GEOM_SYNCER> m_geomSync;
PCB_SHAPE m_workingCopy;
};
static void AddXYPointToSizer( EDA_DRAW_FRAME& aFrame, wxGridBagSizer& aSizer, int row, int col,
const wxString aName, bool aRelative,
std::vector<BOUND_CONTROL>& aBoundCtrls )
{
// Name
// X [Ctrl] mm
// Y [Ctrl] mm
wxWindow* parent = aSizer.GetContainingWindow();
wxStaticText* titleLabel = new wxStaticText( parent, wxID_ANY, aName );
aSizer.Add( titleLabel, wxGBPosition( row, col ), wxGBSpan( 1, 3 ),
wxALIGN_CENTER_VERTICAL | wxALIGN_CENTER_HORIZONTAL | wxALL | wxEXPAND );
row++;
for( size_t coord = 0; coord < 2; ++coord )
{
wxStaticText* label =
new wxStaticText( parent, wxID_ANY, coord == 0 ? _( "X" ) : _( "Y" ) );
aSizer.Add( label, wxGBPosition( row, col ), wxDefaultSpan,
wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL | wxLEFT, 5 );
wxTextCtrl* ctrl = new wxTextCtrl( parent, wxID_ANY, "" );
aSizer.Add( ctrl, wxGBPosition( row, col + 1 ), wxDefaultSpan,
wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 5 );
wxStaticText* units = new wxStaticText( parent, wxID_ANY, _( "mm" ) );
aSizer.Add( units, wxGBPosition( row, col + 2 ), wxDefaultSpan,
wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL | wxRIGHT, 5 );
auto binder = std::make_unique<UNIT_BINDER>( &aFrame, label, ctrl, units );
if( aRelative )
binder->SetCoordType( coord == 0 ? ORIGIN_TRANSFORMS::REL_X_COORD
: ORIGIN_TRANSFORMS::REL_Y_COORD );
else
binder->SetCoordType( coord == 0 ? ORIGIN_TRANSFORMS::ABS_X_COORD
: ORIGIN_TRANSFORMS::ABS_Y_COORD );
aBoundCtrls.push_back( BOUND_CONTROL{ std::move( binder ), ctrl } );
row++;
}
if( !aSizer.IsColGrowable( col + 1 ) )
aSizer.AddGrowableCol( col + 1 );
}
void AddFieldToSizer( EDA_DRAW_FRAME& aFrame, wxGridBagSizer& aSizer, int row, int col,
const wxString aName, ORIGIN_TRANSFORMS::COORD_TYPES_T aCoordType,
bool aIsAngle, std::vector<BOUND_CONTROL>& aBoundCtrls )
{
// Name [Ctrl] mm
wxWindow* parent = aSizer.GetContainingWindow();
wxStaticText* label = new wxStaticText( parent, wxID_ANY, aName );
aSizer.Add( label, wxGBPosition( row, col ), wxDefaultSpan,
wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL | wxLEFT, 5 );
wxTextCtrl* ctrl = new wxTextCtrl( parent, wxID_ANY );
aSizer.Add( ctrl, wxGBPosition( row, col + 1 ), wxDefaultSpan,
wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 5 );
wxStaticText* units = new wxStaticText( parent, wxID_ANY, _( "mm" ) );
aSizer.Add( units, wxGBPosition( row, col + 2 ), wxDefaultSpan,
wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL | wxRIGHT, 5 );
auto binder = std::make_unique<UNIT_BINDER>( &aFrame, label, ctrl, units );
binder->SetCoordType( aCoordType );
if( aIsAngle )
{
binder->SetPrecision( 4 );
binder->SetUnits( EDA_UNITS::DEGREES );
}
aBoundCtrls.push_back( BOUND_CONTROL{ std::move( binder ), ctrl } );
if( !aSizer.IsColGrowable( col + 1 ) )
aSizer.AddGrowableCol( col + 1 );
}
static std::map<SHAPE_T, int> s_lastTabForShape;
DIALOG_SHAPE_PROPERTIES::DIALOG_SHAPE_PROPERTIES( PCB_BASE_EDIT_FRAME* aParent, PCB_SHAPE* aShape ):
DIALOG_SHAPE_PROPERTIES_BASE( aParent ),
m_parent( aParent ),
m_item( aShape ),
m_thickness( aParent, m_thicknessLabel, m_thicknessCtrl, m_thicknessUnits ),
m_solderMaskMargin( aParent, m_solderMaskMarginLabel, m_solderMaskMarginCtrl, m_solderMaskMarginUnit ),
m_workingCopy( *m_item )
{
SetTitle( wxString::Format( GetTitle(), m_item->GetFriendlyName() ) );
m_hash_key = TO_UTF8( GetTitle() );
wxFont infoFont = KIUI::GetInfoFont( this );
m_techLayersLabel->SetFont( infoFont );
// All the pages exist in the WxFB template, but we'll scrap the ones we don't
// use. Constructing on-demand would work fine too.
std::set<int> shownPages;
const auto showPage = [&]( wxSizer& aMainSizer, bool aSelect = false )
{
// Get the parent of the sizer, which is the panel
wxWindow* page = aMainSizer.GetContainingWindow();
wxCHECK( page, /* void */ );
page->Layout();
const int pageIdx = m_notebookShapeDefs->FindPage( page );
shownPages.insert( pageIdx );
if( aSelect )
m_notebookShapeDefs->SetSelection( pageIdx );
};
switch( m_item->GetShape() )
{
case SHAPE_T::RECTANGLE:
// For all these functions, it's very important that the fields are added in the same order
// as the CTRL_IDX enums in the GEOM_SYNCER classes.
AddXYPointToSizer( *aParent, *m_gbsRectangleByCorners, 0, 0, _( "Start Point" ), false, m_boundCtrls);
AddXYPointToSizer( *aParent, *m_gbsRectangleByCorners, 0, 3, _( "End Point" ), false, m_boundCtrls );
AddXYPointToSizer( *aParent, *m_gbsRectangleByCornerSize, 0, 0, _( "Start Point" ), false, m_boundCtrls);
AddXYPointToSizer( *aParent, *m_gbsRectangleByCornerSize, 0, 3, _( "Size" ), true, m_boundCtrls );
AddXYPointToSizer( *aParent, *m_gbsRectangleByCenterSize, 0, 0, _( "Center" ), false, m_boundCtrls);
AddXYPointToSizer( *aParent, *m_gbsRectangleByCenterSize, 0, 3, _( "Size" ), true, m_boundCtrls );
m_geomSync = std::make_unique<RECTANGLE_GEOM_SYNCER>( m_workingCopy, m_boundCtrls );
showPage( *m_gbsRectangleByCorners, true );
showPage( *m_gbsRectangleByCornerSize );
showPage( *m_gbsRectangleByCenterSize );
break;
case SHAPE_T::SEGMENT:
AddXYPointToSizer( *aParent, *m_gbsLineByEnds, 0, 0, _( "Start Point" ), false, m_boundCtrls);
AddXYPointToSizer( *aParent, *m_gbsLineByEnds, 0, 3, _( "End Point" ), false, m_boundCtrls);
AddXYPointToSizer( *aParent, *m_gbsLineByLengthAngle, 0, 0, _( "Start Point" ), false, m_boundCtrls);
AddFieldToSizer( *aParent, *m_gbsLineByLengthAngle, 1, 3, _( "Length" ), ORIGIN_TRANSFORMS::NOT_A_COORD, false, m_boundCtrls );
AddFieldToSizer( *aParent, *m_gbsLineByLengthAngle, 2, 3, _( "Angle" ), ORIGIN_TRANSFORMS::NOT_A_COORD, true, m_boundCtrls );
AddXYPointToSizer( *aParent, *m_gbsLineByMidEnd, 0, 0, _( "Mid Point" ), false, m_boundCtrls );
AddXYPointToSizer( *aParent, *m_gbsLineByMidEnd, 0, 3, _( "End Point" ), false, m_boundCtrls );
m_geomSync = std::make_unique<LINE_GEOM_SYNCER>( m_workingCopy, m_boundCtrls );
showPage( *m_gbsLineByEnds, true );
showPage( *m_gbsLineByLengthAngle );
showPage( *m_gbsLineByMidEnd );
break;
case SHAPE_T::ARC:
AddXYPointToSizer( *aParent, *m_gbsArcByCSA, 0, 0, _( "Center" ), false, m_boundCtrls);
AddXYPointToSizer( *aParent, *m_gbsArcByCSA, 0, 3, _( "Start Point" ), false, m_boundCtrls);
AddFieldToSizer( *aParent, *m_gbsArcByCSA, 3, 0, _( "Start Angle" ), ORIGIN_TRANSFORMS::NOT_A_COORD, true, m_boundCtrls );
AddXYPointToSizer( *aParent, *m_gbsArcBySME, 0, 0, _( "Start Point" ), false, m_boundCtrls);
AddXYPointToSizer( *aParent, *m_gbsArcBySME, 0, 3, _( "Mid Point" ), false, m_boundCtrls);
AddXYPointToSizer( *aParent, *m_gbsArcBySME, 3, 0, _( "End Point" ), false, m_boundCtrls);
m_geomSync = std::make_unique<ARC_GEOM_SYNCER>( m_workingCopy, m_boundCtrls );
showPage( *m_gbsArcByCSA, true );
showPage( *m_gbsArcBySME );
break;
case SHAPE_T::CIRCLE:
AddXYPointToSizer( *aParent, *m_gbsCircleCenterRadius, 0, 0, _( "Center" ), false, m_boundCtrls);
AddFieldToSizer( *aParent, *m_gbsCircleCenterRadius, 3, 0, _( "Radius" ), ORIGIN_TRANSFORMS::NOT_A_COORD, false, m_boundCtrls );
AddXYPointToSizer( *aParent, *m_gbsCircleCenterPoint, 0, 0, _( "Center" ), false, m_boundCtrls);
AddXYPointToSizer( *aParent, *m_gbsCircleCenterPoint, 0, 3, _( "Point on Circle" ), false, m_boundCtrls);
m_geomSync = std::make_unique<CIRCLE_GEOM_SYNCER>( m_workingCopy, m_boundCtrls );
showPage( *m_gbsCircleCenterRadius, true );
showPage( *m_gbsCircleCenterPoint );
break;
case SHAPE_T::BEZIER:
AddXYPointToSizer( *aParent, *m_gbsBezier, 0, 0, _( "Start Point" ), false, m_boundCtrls);
AddXYPointToSizer( *aParent, *m_gbsBezier, 0, 3, _( "End Point" ), false, m_boundCtrls);
AddXYPointToSizer( *aParent, *m_gbsBezier, 3, 0, _( "Control Point 1" ), false, m_boundCtrls);
AddXYPointToSizer( *aParent, *m_gbsBezier, 3, 3, _( "Control Point 2" ), false, m_boundCtrls);
m_geomSync = std::make_unique<BEZIER_GEOM_SYNCER>( m_workingCopy, m_boundCtrls );
showPage( *m_gbsBezier, TRUE );
break;
case SHAPE_T::POLY:
m_notebookShapeDefs->Hide();
// Nothing to do here...yet
break;
case SHAPE_T::UNDEFINED:
wxFAIL_MSG( "Undefined shape" );
break;
}
// Remove any tabs not used (Hide() doesn't work on Windows)
for( int i = m_notebookShapeDefs->GetPageCount() - 1; i >= 0; --i )
{
if( shownPages.count( i ) == 0 )
m_notebookShapeDefs->RemovePage( i );
}
// Used the last saved tab if any
if( s_lastTabForShape.count( m_item->GetShape() ) > 0 )
{
m_notebookShapeDefs->SetSelection( s_lastTabForShape[m_item->GetShape()] );
}
// Find the first control in the shown tab
wxWindow* tabPanel = m_notebookShapeDefs->GetCurrentPage();
for( size_t i = 0; i < m_boundCtrls.size(); ++i )
{
if( m_boundCtrls[i].m_Ctrl->IsDescendant( tabPanel ) )
{
m_boundCtrls[i].m_Ctrl->SetFocus();
break;
}
}
// Do not allow locking items in the footprint editor
m_locked->Show( dynamic_cast<PCB_EDIT_FRAME*>( aParent ) != nullptr );
// Configure the layers list selector
if( m_parent->GetFrameType() == FRAME_FOOTPRINT_EDITOR )
{
LSET forbiddenLayers = LSET::ForbiddenFootprintLayers();
// If someone went to the trouble of setting the layer in a text editor, then there's
// very little sense in nagging them about it.
forbiddenLayers.set( m_item->GetLayer(), false );
m_LayerSelectionCtrl->SetNotAllowedLayerSet( forbiddenLayers );
}
for( const auto& [ lineStyle, lineStyleDesc ] : lineTypeNames )
m_lineStyleCombo->Append( lineStyleDesc.name, KiBitmapBundle( lineStyleDesc.bitmap ) );
m_lineStyleCombo->Append( DEFAULT_STYLE );
m_LayerSelectionCtrl->SetLayersHotkeys( false );
m_LayerSelectionCtrl->SetBoardFrame( m_parent );
m_LayerSelectionCtrl->Resync();
m_netSelector->SetBoard( aParent->GetBoard() );
m_netSelector->SetNetInfo( &aParent->GetBoard()->GetNetInfo() );
if( m_parent->GetFrameType() == FRAME_FOOTPRINT_EDITOR )
{
m_netLabel->Hide();
m_netSelector->Hide();
}
else
{
int net = aShape->GetNetCode();
if( net >= 0 )
{
m_netSelector->SetSelectedNetcode( net );
}
else
{
m_netSelector->SetIndeterminateString( INDETERMINATE_STATE );
m_netSelector->SetIndeterminate();
}
}
if( m_item->GetShape() == SHAPE_T::ARC || m_item->GetShape() == SHAPE_T::SEGMENT )
m_filledCtrl->Show( false );
SetupStandardButtons();
// Now all widgets have the size fixed, call FinishDialogSettings
finishDialogSettings();
}
void PCB_BASE_EDIT_FRAME::ShowGraphicItemPropertiesDialog( PCB_SHAPE* aShape )
{
wxCHECK_RET( aShape, wxT( "ShowGraphicItemPropertiesDialog() error: NULL item" ) );
DIALOG_SHAPE_PROPERTIES dlg( this, aShape );
if( dlg.ShowQuasiModal() == wxID_OK )
{
if( aShape->IsOnLayer( GetActiveLayer() ) )
{
DRAWING_TOOL* drawingTool = m_toolManager->GetTool<DRAWING_TOOL>();
drawingTool->SetStroke( aShape->GetStroke(), GetActiveLayer() );
}
}
}
void DIALOG_SHAPE_PROPERTIES::onLayerSelection( wxCommandEvent& event )
{
if( m_LayerSelectionCtrl->GetLayerSelection() >= 0 )
{
showHideNetInfo();
}
showHideTechLayers();
}
void DIALOG_SHAPE_PROPERTIES::onFilledCheckbox( wxCommandEvent& event )
{
if( m_filledCtrl->GetValue() )
{
m_lineStyleCombo->SetSelection( 0 );
m_lineStyleLabel->Enable( false );
m_lineStyleCombo->Enable( false );
}
else
{
LINE_STYLE style = m_item->GetStroke().GetLineStyle();
if( style == LINE_STYLE::DEFAULT )
style = LINE_STYLE::SOLID;
if( (int) style < (int) lineTypeNames.size() )
m_lineStyleCombo->SetSelection( (int) style );
m_lineStyleLabel->Enable( true );
m_lineStyleCombo->Enable( true );
}
}
void DIALOG_SHAPE_PROPERTIES::onTechLayersChanged( wxCommandEvent& event )
{
showHideTechLayers();
}
bool DIALOG_SHAPE_PROPERTIES::TransferDataToWindow()
{
if( !m_item )
return false;
// Not al shapes have a syncer (e.g. polygons)
if( m_geomSync )
m_geomSync->SetShape( *m_item );
m_filledCtrl->SetValue( m_item->IsFilled() );
m_locked->SetValue( m_item->IsLocked() );
m_thickness.SetValue( m_item->GetStroke().GetWidth() );
int style = static_cast<int>( m_item->GetStroke().GetLineStyle() );
if( style == -1 )
m_lineStyleCombo->SetStringSelection( DEFAULT_STYLE );
else if( style < (int) lineTypeNames.size() )
m_lineStyleCombo->SetSelection( style );
else
wxFAIL_MSG( "Line type not found in the type lookup map" );
m_LayerSelectionCtrl->SetLayerSelection( m_item->GetLayer() );
m_hasSolderMask->SetValue( m_item->HasSolderMask() );
if( m_item->GetLocalSolderMaskMargin().has_value() )
m_solderMaskMargin.SetValue( m_item->GetLocalSolderMaskMargin().value() );
else
m_solderMaskMargin.SetValue( wxEmptyString );
showHideNetInfo();
showHideTechLayers();
return DIALOG_SHAPE_PROPERTIES_BASE::TransferDataToWindow();
}
bool DIALOG_SHAPE_PROPERTIES::TransferDataFromWindow()
{
if( !DIALOG_SHAPE_PROPERTIES_BASE::TransferDataFromWindow() )
return false;
if( !m_item )
return true;
int layer = m_LayerSelectionCtrl->GetLayerSelection();
BOARD_COMMIT commit( m_parent );
commit.Modify( m_item );
bool pushCommit = ( m_item->GetEditFlags() == 0 );
// Set IN_EDIT flag to force undo/redo/abort proper operation and avoid new calls to
// SaveCopyInUndoList for the same text if is moved, and then rotated, edited, etc....
if( !pushCommit )
m_item->SetFlags( IN_EDIT );
*m_item = m_workingCopy;
bool wasLocked = m_item->IsLocked();
m_item->SetFilled( m_filledCtrl->GetValue() );
m_item->SetLocked( m_locked->GetValue() );
STROKE_PARAMS stroke = m_item->GetStroke();
stroke.SetWidth( m_thickness.GetIntValue() );
auto it = lineTypeNames.begin();
std::advance( it, m_lineStyleCombo->GetSelection() );
if( it == lineTypeNames.end() )
stroke.SetLineStyle( LINE_STYLE::DEFAULT );
else
stroke.SetLineStyle( it->first );
m_item->SetStroke( stroke );
m_item->SetLayer( ToLAYER_ID( layer ) );
m_item->SetHasSolderMask( m_hasSolderMask->GetValue() );
if( m_solderMaskMargin.IsNull() )
m_item->SetLocalSolderMaskMargin( {} );
else
m_item->SetLocalSolderMaskMargin( m_solderMaskMargin.GetIntValue() );
m_item->RebuildBezierToSegmentsPointsList( ARC_HIGH_DEF );
if( m_item->IsOnCopperLayer() )
m_item->SetNetCode( m_netSelector->GetSelectedNetcode() );
else
m_item->SetNetCode( -1 );
if( pushCommit )
commit.Push( _( "Edit Shape Properties" ) );
// Save the tab
s_lastTabForShape[m_item->GetShape()] = m_notebookShapeDefs->GetSelection();
// Notify clients which treat locked and unlocked items differently (ie: POINT_EDITOR)
if( wasLocked != m_item->IsLocked() )
m_parent->GetToolManager()->PostEvent( EVENTS::SelectedEvent );
return true;
}
bool DIALOG_SHAPE_PROPERTIES::Validate()
{
wxArrayString errors;
if( !DIALOG_SHAPE_PROPERTIES_BASE::Validate() )
return false;
if( m_geomSync )
m_geomSync->Validate( errors );
// Type specific checks.
switch( m_item->GetShape() )
{
case SHAPE_T::ARC:
if( m_thickness.GetValue() <= 0 )
errors.Add( _( "Line width must be greater than zero." ) );
break;
case SHAPE_T::CIRCLE:
if( !m_filledCtrl->GetValue() && m_thickness.GetValue() <= 0 )
errors.Add( _( "Line width must be greater than zero for an unfilled circle." ) );
break;
case SHAPE_T::RECTANGLE:
if( !m_filledCtrl->GetValue() && m_thickness.GetValue() <= 0 )
errors.Add( _( "Line width must be greater than zero for an unfilled rectangle." ) );
break;
case SHAPE_T::POLY:
if( !m_filledCtrl->GetValue() && m_thickness.GetValue() <= 0 )
errors.Add( _( "Line width must be greater than zero for an unfilled polygon." ) );
break;
case SHAPE_T::SEGMENT:
if( m_thickness.GetValue() <= 0 )
errors.Add( _( "Line width must be greater than zero." ) );
break;
case SHAPE_T::BEZIER:
if( !m_filledCtrl->GetValue() && m_thickness.GetValue() <= 0 )
errors.Add( _( "Line width must be greater than zero for an unfilled curve." ) );
break;
default:
UNIMPLEMENTED_FOR( m_item->SHAPE_T_asString() );
break;
}
if( errors.GetCount() )
{
HTML_MESSAGE_BOX dlg( this, _( "Error List" ) );
dlg.ListSet( errors );
dlg.ShowModal();
}
return errors.GetCount() == 0;
}