Commit b28e976e authored by jean-pierre charras's avatar jean-pierre charras

Eeschema: BOM list generation: some fixes and enhancements.

drawframe.cpp: commit a fix about scrollbars  from lajos kamocsay
parent bed96be7
......@@ -554,16 +554,16 @@ void EDA_DRAW_FRAME::AdjustScrollBars( const wxPoint& aCenterPosition )
{
virtualSize = drawingRect.GetSize();
}
else if( drawingRect.Contains( logicalClientRect ) )
else
{
virtualSize = drawingRect.GetSize();
if( drawingRect.GetLeft() < logicalClientRect.GetLeft() && drawingRect.GetRight() > logicalClientRect.GetRight() )
{
virtualSize.x = drawingRect.GetSize().x;
}
else
{
int drawingCenterX = drawingRect.x + ( drawingRect.width / 2 );
int clientCenterX = logicalClientRect.x + ( logicalClientRect.width / 2 );
int drawingCenterY = drawingRect.y + ( drawingRect.height / 2 );
int clientCenterY = logicalClientRect.y + ( logicalClientRect.height / 2 );
if( logicalClientRect.width > drawingRect.width )
{
......@@ -589,6 +589,16 @@ void EDA_DRAW_FRAME::AdjustScrollBars( const wxPoint& aCenterPosition )
{
virtualSize.x = drawingRect.width;
}
}
if( drawingRect.GetTop() < logicalClientRect.GetTop() && drawingRect.GetBottom() > logicalClientRect.GetBottom() )
{
virtualSize.y = drawingRect.GetSize().y;
}
else
{
int drawingCenterY = drawingRect.y + ( drawingRect.height / 2 );
int clientCenterY = logicalClientRect.y + ( logicalClientRect.height / 2 );
if( logicalClientRect.height > drawingRect.height )
{
......@@ -615,6 +625,8 @@ void EDA_DRAW_FRAME::AdjustScrollBars( const wxPoint& aCenterPosition )
virtualSize.y = drawingRect.height;
}
}
}
if( screen->m_Center )
{
......
......@@ -110,29 +110,52 @@ bool SCH_REFERENCE_LIST::sortByValueAndRef( const SCH_REFERENCE& item1,
const SCH_REFERENCE& item2 )
{
int ii = item1.CompareValue( item2 );
if( ii == 0 )
ii = RefDesStringCompare( item1.GetRef(), item2.GetRef() );
if( ii == 0 )
ii = item1.m_Unit - item2.m_Unit;
if( ii == 0 )
ii = item1.m_SheetNum - item2.m_SheetNum;
if( ii == 0 )
ii = item1.m_CmpPos.x - item2.m_CmpPos.x;
if( ii == 0 )
ii = item1.m_CmpPos.y - item2.m_CmpPos.y;
if( ii == 0 )
ii = item1.m_TimeStamp - item2.m_TimeStamp;
return ii < 0;
}
/*
* Helper function to calculate in a component value string
* the value, depending on multiplier symbol:
* pico
* nano
* micro (u)
* milli (m)
* kilo (k ou K)
* Mega
* Giga
* Tera
*
* with notations like 1K; 1.5K; 1,5K; 1k5
* returns true if the string is a value, false if not
* (a value is a string starting by a number)
*/
static bool engStrToDouble( wxString aStr, double* aDouble )
{
// A trick to take care of strings without a multiplier
aStr.Append( wxT( "R" ) );
// Regular expression for a value string, e.g., 47k2
static wxRegEx valueRegEx( wxT( "^([0-9]+)([pnumRkMGT.])([0-9]*)" ) );
static wxRegEx valueRegEx( wxT( "^([0-9]+)([pnumRkKMGT.,])([0-9]*)([pnumRkKMGT]*)" ) );
if( !valueRegEx.Matches( aStr ) )
return false;
......@@ -141,6 +164,7 @@ static bool engStrToDouble( wxString aStr, double* aDouble )
+ wxT( "." )
+ valueRegEx.GetMatch( aStr, 3 ) );
wxString multiplierString = valueRegEx.GetMatch( aStr, 2 );
wxString post_multiplierString = valueRegEx.GetMatch( aStr, 4 );
double multiplier;
switch( (wxChar)multiplierString[0] )
......@@ -158,6 +182,7 @@ static bool engStrToDouble( wxString aStr, double* aDouble )
multiplier = 1e-3;
break;
case 'k':
case 'K':
multiplier = 1e3;
break;
case 'M':
......@@ -170,12 +195,46 @@ static bool engStrToDouble( wxString aStr, double* aDouble )
multiplier = 1e12;
break;
case 'R':
case '.':
case '.': // floatting point separator
case ',': // floatting point separator (some languages)
default:
multiplier = 1;
break;
}
switch( (wxChar)post_multiplierString[0] )
{
case 'p':
multiplier = 1e-12;
break;
case 'n':
multiplier = 1e-9;
break;
case 'u':
multiplier = 1e-6;
break;
case 'm':
multiplier = 1e-3;
break;
case 'k':
case 'K':
multiplier = 1e3;
break;
case 'M':
multiplier = 1e6;
break;
case 'G':
multiplier = 1e9;
break;
case 'T':
multiplier = 1e12;
break;
case 'R':
default:
break;
}
LOCALE_IO dummy; // set to C floatting point standard
valueStr.ToDouble( aDouble );
*aDouble *= multiplier;
......@@ -183,11 +242,41 @@ static bool engStrToDouble( wxString aStr, double* aDouble )
}
/* sort the list of references by value.
* Components are grouped by type and are sorted by value:
* The value of a component accept multiplier symbols (p, n, K ..)
* groups are made by first letter of reference
*/
bool SCH_REFERENCE_LIST::sortByValueOnly( const SCH_REFERENCE& item1,
const SCH_REFERENCE& item2 )
{
wxString text1 = item1.GetComponent()->GetField( VALUE )->GetText();
wxString text2 = item2.GetComponent()->GetField( VALUE )->GetText();
// First, group by type, assuming 2 first letter of references
// are different for different types of components.
wxString text1 = item1.GetComponent()->GetField( REFERENCE )->GetText().Left(2);
wxString text2 = item2.GetComponent()->GetField( REFERENCE )->GetText().Left(2);
if( text1[0] != text2[0] )
return text1[0] < text2[0];
// Compare the second letter, if exists
if( text1.length() > 1 && text2.length() > 1 )
{
if( (text1[1] < '0') || (text1[1] > '9') ||
(text2[1] < '0') || (text2[1] > '9') )
return text1[1] < text2[1];
}
// Inside a group of components of same value, it could be good to group per footprints
text1 = item1.GetComponent()->GetField( FOOTPRINT )->GetText();
text2 = item2.GetComponent()->GetField( FOOTPRINT )->GetText();
int same_footprint = text1.IsEmpty() || text2.IsEmpty();
if( same_footprint == 0 )
same_footprint = text1.CmpNoCase( text2 );
// We can compare here 2 values relative to components of the same type
// assuming references are correctly chosen
text1 = item1.GetComponent()->GetField( VALUE )->GetText();
text2 = item2.GetComponent()->GetField( VALUE )->GetText();
double value1, value2;
......@@ -204,7 +293,11 @@ bool SCH_REFERENCE_LIST::sortByValueOnly( const SCH_REFERENCE& item1,
return false;
if( match1 && match2 )
{
if( value1 == value2 )
return same_footprint < 0;
return value1 < value2;
}
// Fall back to normal string compare
int ii = text1.CmpNoCase( text2 );
......
......@@ -60,11 +60,11 @@ static bool s_ListByRef = true;
static bool s_ListByValue = true;
static bool s_ListWithSubCmponents;
static bool s_ListHierarchicalPinByName;
static bool s_ListBySheet;
static bool s_ListHierarchicalPinBySheet;
static bool s_BrowseCreatedList;
static int s_OutputFormOpt;
static int s_OutputSeparatorOpt;
static bool s_Add_FpField_state;
static bool s_Add_FpField_state = true;
static bool s_Add_F1_state;
static bool s_Add_F2_state;
static bool s_Add_F3_state;
......@@ -93,10 +93,17 @@ static bool* s_AddFieldList[] =
};
#define OPTION_BOM_FORMAT wxT( "BomFormat" )
#define OPTION_BOM_LAUNCH_BROWSER wxT( "BomLaunchBrowser" )
#define OPTION_BOM_SEPARATOR wxT( "BomExportSeparator" )
#define OPTION_BOM_ADD_FIELD wxT( "BomAddField" )
const wxString OPTION_BOM_LIST_REF( wxT("BomListPerRef") );
const wxString OPTION_BOM_LIST_VALUE( wxT("BomListPerValue") );
const wxString OPTION_BOM_LIST_HPINS( wxT("BomListPerHPins") );
const wxString OPTION_BOM_LIST_HPINS_BY_SHEET( wxT("BomListHPinsPerSheet") );
const wxString OPTION_BOM_LIST_HPINS_BY_NAME_( wxT("BomListHPinsPerName") );
const wxString OPTION_BOM_LIST_SUB_CMP( wxT("BomListSubCmps") );
const wxString OPTION_BOM_FORMAT( wxT("BomFormat") );
const wxString OPTION_BOM_LAUNCH_BROWSER( wxT("BomLaunchBrowser") );
const wxString OPTION_BOM_SEPARATOR( wxT("BomExportSeparator") );
const wxString OPTION_BOM_ADD_FIELD ( wxT("BomAddField") );
/* list of separators used in bom export to spreadsheet
* (selected by s_OutputSeparatorOpt, and s_OutputSeparatorOpt radiobox)
......@@ -135,8 +142,16 @@ void DIALOG_BUILD_BOM::Init()
SetFocus();
/* Get options */
m_Config->Read( OPTION_BOM_LIST_REF, &s_ListByRef );
m_Config->Read( OPTION_BOM_LIST_VALUE , &s_ListByValue );
m_Config->Read( OPTION_BOM_LIST_HPINS, &s_ListHierarchicalPinByName );
m_Config->Read( OPTION_BOM_LIST_HPINS_BY_SHEET, &s_ListWithSubCmponents );
m_Config->Read( OPTION_BOM_LIST_HPINS_BY_NAME_, &s_ListWithSubCmponents );
m_Config->Read( OPTION_BOM_LIST_SUB_CMP, &s_ListWithSubCmponents );
m_Config->Read( OPTION_BOM_LIST_HPINS_BY_SHEET, &s_ListHierarchicalPinBySheet );
m_Config->Read( OPTION_BOM_LIST_HPINS_BY_NAME_, &s_ListHierarchicalPinByName );
s_OutputFormOpt = m_Config->Read( OPTION_BOM_FORMAT, (long) 0 );
s_BrowseCreatedList = m_Config->Read( OPTION_BOM_LAUNCH_BROWSER, (long) 0 );
m_Config->Read( OPTION_BOM_LAUNCH_BROWSER, &s_BrowseCreatedList );
s_OutputSeparatorOpt = m_Config->Read( OPTION_BOM_SEPARATOR, (long) 0 );
long addfields = m_Config->Read( OPTION_BOM_ADD_FIELD, (long) 0 );
......@@ -155,7 +170,7 @@ void DIALOG_BUILD_BOM::Init()
m_ListSubCmpItems->SetValidator( wxGenericValidator( &s_ListWithSubCmponents ) );
m_ListCmpbyValItems->SetValidator( wxGenericValidator( &s_ListByValue ) );
m_GenListLabelsbyVal->SetValidator( wxGenericValidator( &s_ListHierarchicalPinByName ) );
m_GenListLabelsbySheet->SetValidator( wxGenericValidator( &s_ListBySheet ) );
m_GenListLabelsbySheet->SetValidator( wxGenericValidator( &s_ListHierarchicalPinBySheet ) );
m_OutputFormCtrl->SetValidator( wxGenericValidator( &s_OutputFormOpt ) );
m_OutputSeparatorCtrl->SetValidator( wxGenericValidator( &s_OutputSeparatorOpt ) );
m_GetListBrowser->SetValidator( wxGenericValidator( &s_BrowseCreatedList ) );
......@@ -185,19 +200,34 @@ void DIALOG_BUILD_BOM::Init()
void DIALOG_BUILD_BOM::OnRadioboxSelectFormatSelected( wxCommandEvent& event )
{
if( m_OutputFormCtrl->GetSelection() == 0 )
switch( m_OutputFormCtrl->GetSelection() )
{
case 0:
m_OutputSeparatorCtrl->Enable( false );
m_ListCmpbyRefItems->Enable( true );
m_ListCmpbyValItems->Enable( true );
m_GenListLabelsbyVal->Enable( true );
m_GenListLabelsbySheet->Enable( true );
}
else
{
m_ListSubCmpItems->Enable( true );
break;
case 1:
m_OutputSeparatorCtrl->Enable( true );
m_ListCmpbyRefItems->Enable( false );
m_ListCmpbyValItems->Enable( false );
m_GenListLabelsbyVal->Enable( false );
m_GenListLabelsbySheet->Enable( false );
m_ListSubCmpItems->Enable( true );
break;
case 2:
m_OutputSeparatorCtrl->Enable( true );
m_ListCmpbyRefItems->Enable( false );
m_ListCmpbyValItems->Enable( false );
m_GenListLabelsbyVal->Enable( false );
m_GenListLabelsbySheet->Enable( false );
m_ListSubCmpItems->Enable( false );
break;
}
}
......@@ -237,13 +267,11 @@ void DIALOG_BUILD_BOM::SavePreferences()
wxASSERT( m_Config != NULL );
// Determine current settings of "List items" and "Options" checkboxes
// (NOTE: These 6 settings are restored when the dialog box is next
// invoked, but are *not* still saved after Eeschema is next shut down.)
s_ListByRef = m_ListCmpbyRefItems->GetValue();
s_ListWithSubCmponents = m_ListSubCmpItems->GetValue();
s_ListByValue = m_ListCmpbyValItems->GetValue();
s_ListHierarchicalPinByName = m_GenListLabelsbyVal->GetValue();
s_ListBySheet = m_GenListLabelsbySheet->GetValue();
s_ListHierarchicalPinBySheet = m_GenListLabelsbySheet->GetValue();
s_BrowseCreatedList = m_GetListBrowser->GetValue();
// (saved in config ):
......@@ -268,6 +296,13 @@ void DIALOG_BUILD_BOM::SavePreferences()
s_Add_Alls_state = m_AddAllFields->GetValue();
// Now save current settings of both radiobutton groups
m_Config->Write( OPTION_BOM_LIST_REF, s_ListByRef );
m_Config->Write( OPTION_BOM_LIST_VALUE , s_ListByValue );
m_Config->Write( OPTION_BOM_LIST_HPINS, s_ListHierarchicalPinByName );
m_Config->Write( OPTION_BOM_LIST_HPINS_BY_SHEET, s_ListHierarchicalPinBySheet );
m_Config->Write( OPTION_BOM_LIST_HPINS_BY_NAME_, s_ListHierarchicalPinByName );
m_Config->Write( OPTION_BOM_LIST_SUB_CMP, s_ListWithSubCmponents );
m_Config->Write( OPTION_BOM_FORMAT, (long) s_OutputFormOpt );
m_Config->Write( OPTION_BOM_SEPARATOR, (long) s_OutputSeparatorOpt );
m_Config->Write( OPTION_BOM_LAUNCH_BROWSER, (long) s_BrowseCreatedList );
......@@ -343,15 +378,15 @@ void DIALOG_BUILD_BOM::Create_BOM_Lists( int aTypeFile,
switch( aTypeFile )
{
case 0: // list
GenereListeOfItems( m_ListFileName, aIncludeSubComponents );
GenereListeOfItems( aIncludeSubComponents );
break;
case 1: // spreadsheet
CreateExportList( m_ListFileName, aIncludeSubComponents );
case 1: // spreadsheet, Single Part per line
CreateExportList( aIncludeSubComponents );
break;
case 2: // Single Part per line
CreatePartsList( m_ListFileName, aIncludeSubComponents );
case 2: // spreadsheet, one value per line and no sub-component
CreatePartsList();
break;
}
......@@ -404,31 +439,37 @@ bool DIALOG_BUILD_BOM::IsFieldChecked(int aFieldId)
}
void DIALOG_BUILD_BOM::CreatePartsList( const wxString& aFullFileName, bool aIncludeSubComponents )
/* Prints a list of components, in a form which can be imported by a spreadsheet.
* components having the same value and the same footprint
* are grouped on the same line
* Form is:
* value; number of components; list of references; <footprint>; <field1>; ...;
* list is sorted by values
*/
void DIALOG_BUILD_BOM::CreatePartsList( )
{
FILE* f;
wxString msg;
if( ( f = wxFopen( aFullFileName, wxT( "wt" ) ) ) == NULL )
if( ( f = wxFopen( m_ListFileName, wxT( "wt" ) ) ) == NULL )
{
msg = _( "Failed to open file " );
msg << aFullFileName;
msg << m_ListFileName;
DisplayError( this, msg );
return;
}
SCH_REFERENCE_LIST cmplist;
SCH_SHEET_LIST sheetList; // uses a global
SCH_SHEET_LIST sheetList;
sheetList.GetComponents( cmplist, false );
// sort component list by ref and remove sub components
if( !aIncludeSubComponents )
cmplist.RemoveSubComponentsFromList();
// sort component list by value
cmplist.SortByValueAndRef( );
PrintComponentsListByPart( f, cmplist, aIncludeSubComponents );
PrintComponentsListByPart( f, cmplist, false );
fclose( f );
}
......@@ -437,18 +478,18 @@ void DIALOG_BUILD_BOM::CreatePartsList( const wxString& aFullFileName, bool aInc
/*
* Print a list of components, in a form which can be imported by a spreadsheet
* form is:
* cmp name; cmp val; fields;
* cmp ref; cmp val; fields;
* Components are sorted by reference
*/
void DIALOG_BUILD_BOM::CreateExportList( const wxString& aFullFileName,
bool aIncludeSubComponents )
void DIALOG_BUILD_BOM::CreateExportList( bool aIncludeSubComponents )
{
FILE* f;
wxString msg;
if( ( f = wxFopen( aFullFileName, wxT( "wt" ) ) ) == NULL )
if( ( f = wxFopen( m_ListFileName, wxT( "wt" ) ) ) == NULL )
{
msg = _( "Failed to open file " );
msg << aFullFileName;
msg << m_ListFileName;
DisplayError( this, msg );
return;
}
......@@ -471,21 +512,21 @@ void DIALOG_BUILD_BOM::CreateExportList( const wxString& aFullFileName,
}
/** GenereListeOfItems()
/*
* GenereListeOfItems()
* Main function to create the list of components and/or labels
* (global labels and pin sheets" )
*/
void DIALOG_BUILD_BOM::GenereListeOfItems( const wxString& aFullFileName,
bool aIncludeSubComponents )
void DIALOG_BUILD_BOM::GenereListeOfItems( bool aIncludeSubComponents )
{
FILE* f;
int itemCount;
wxString msg;
if( ( f = wxFopen( aFullFileName, wxT( "wt" ) ) ) == NULL )
if( ( f = wxFopen( m_ListFileName, wxT( "wt" ) ) ) == NULL )
{
msg = _( "Failed to open file " );
msg << aFullFileName;
msg << m_ListFileName;
DisplayError( this, msg );
return;
}
......@@ -559,39 +600,23 @@ order = Alphab. ) count = %d\n\n" ),
fclose( f );
}
#if defined(KICAD_GOST)
wxString DIALOG_BUILD_BOM::PrintFieldData( SCH_COMPONENT* DrawLibItem,
#else
void DIALOG_BUILD_BOM::PrintFieldData( FILE* f, SCH_COMPONENT* DrawLibItem,
#endif
bool CompactForm )
{
#if defined(KICAD_GOST)
wxString outStr;
wxString tmpStr;
#endif
if( IsFieldChecked( FOOTPRINT ) )
{
if( CompactForm )
{
#if defined(KICAD_GOST)
outStr.Printf( wxT( "%c%s" ), s_ExportSeparatorSymbol,
GetChars( DrawLibItem->GetField( FOOTPRINT )->m_Text ) );
#else
fprintf( f, "%c%s", s_ExportSeparatorSymbol,
TO_UTF8( DrawLibItem->GetField( FOOTPRINT )->m_Text ) );
#endif
}
else
{
#if defined(KICAD_GOST)
outStr.Printf( wxT( "; %-12s" ),
GetChars( DrawLibItem->GetField( FOOTPRINT )->m_Text ) );
#else
fprintf( f, "; %-12s",
TO_UTF8( DrawLibItem->GetField( FOOTPRINT )->m_Text ) );
#endif
}
}
......@@ -601,31 +626,19 @@ void DIALOG_BUILD_BOM::PrintFieldData( FILE* f, SCH_COMPONENT* DrawLibItem,
continue;
if( CompactForm )
#if defined(KICAD_GOST)
{
tmpStr.Printf( wxT( "%c%s" ), s_ExportSeparatorSymbol,
GetChars( DrawLibItem->GetField( ii )->m_Text ) );
outStr += tmpStr;
}
#else
fprintf( f, "%c%s", s_ExportSeparatorSymbol,
TO_UTF8( DrawLibItem->GetField( ii )->m_Text ) );
#endif
else
#if defined(KICAD_GOST)
{
tmpStr.Printf( wxT( "; %-12s" ),
GetChars( DrawLibItem->GetField( ii )->m_Text ) );
outStr += tmpStr;
}
#else
fprintf( f, "; %-12s",
TO_UTF8( DrawLibItem->GetField( ii )->m_Text ) );
#endif
}
#if defined(KICAD_GOST)
return outStr;
#endif
}
......@@ -813,8 +826,8 @@ int DIALOG_BUILD_BOM::PrintComponentsListByRef( FILE* f,
fprintf( f, "\n" );
}
#else
PrintFieldData( f, comp, CompactForm );
fprintf( f, "\n" );
wxString tmpStr = PrintFieldData( comp, CompactForm );
fprintf( f, "%s\n", TO_UTF8( tmpStr ) );
#endif
}
......@@ -960,13 +973,7 @@ int DIALOG_BUILD_BOM::PrintComponentsListByVal( FILE* f,
}
}
#if defined(KICAD_GOST)
fprintf( f, "%s", TO_UTF8( PrintFieldData( DrawLibItem ) ) );
#else
PrintFieldData( f, DrawLibItem );
#endif
fputs( "\n", f );
fprintf( f, "%s\n", TO_UTF8( PrintFieldData( DrawLibItem ) ) );
}
msg = _( "#End Cmp\n" );
......
......@@ -20,7 +20,7 @@ class DIALOG_BUILD_BOM : public DIALOG_BUILD_BOM_BASE
private:
EDA_DRAW_FRAME* m_Parent;
wxConfig* m_Config;
wxString m_ListFileName;
wxString m_ListFileName; // The full filename of the file report.
private:
void OnRadioboxSelectFormatSelected( wxCommandEvent& event );
......@@ -34,17 +34,27 @@ private:
char aExportSeparatorSymbol,
bool aRunBrowser );
void GenereListeOfItems( const wxString& FullFileName, bool aIncludeSubComponents );
void CreateExportList( const wxString& FullFileName, bool aIncludeSubComponents );
void GenereListeOfItems( bool aIncludeSubComponents );
/**
* Function CreateParstList
* Function CreateExportList
* prints a list of components, in a form which can be imported by a
* spreadsheet. Form is:
* cmp value; number of components; \<footprint\>; \<field1\>; ...;
* list of references having the same value
* reference; cmp value; \<footprint\>; \<field1\>; ...;
* Components are sorted by reference
*/
void CreatePartsList( const wxString& aFullFileName, bool aIncludeSubComponents );
void CreateExportList( bool aIncludeSubComponents );
/**
* Function CreatePartsList
* prints a list of components, in a form which can be imported by a spreadsheet.
* components having the same value and the same footprint
* are grouped on the same line
* Form is:
* value; number of components; list of references; \<footprint\>; \<field1\>; ...;
* list is sorted by values
*/
void CreatePartsList();
int PrintComponentsListByRef( FILE* f, SCH_REFERENCE_LIST& aList,
bool CompactForm, bool aIncludeSubComponents );
......@@ -55,11 +65,7 @@ private:
int PrintComponentsListByPart( FILE* f, SCH_REFERENCE_LIST& aList,
bool aIncludeSubComponents );
#if defined(KICAD_GOST)
wxString PrintFieldData( SCH_COMPONENT* DrawLibItem, bool CompactForm = false );
#else
void PrintFieldData( FILE* f, SCH_COMPONENT* DrawLibItem, bool CompactForm = false );
#endif
bool IsFieldChecked( int aFieldId );
......
......@@ -416,13 +416,15 @@ public:
* Function SortByValueOnly
* sort the list of references by value.
* <p>
* Components are sorted in the following order:
* Components are grouped by type and are sorted in the following order:
* <ul>
* <li>Value of component.</li>
* <li>Numeric value of reference designator.</li>
* <li>Unit number when component has multiple parts.</li>
* </ul>
* </p>
* groups are made by the first letter of reference
* or the 2 first letters when existing
*/
void SortByValueOnly()
{
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment