Kaydet (Commit) 4971468b authored tarafından Norbert Thiebaud's avatar Norbert Thiebaud Kaydeden (comit) Miklos Vajna

gridfixes: #i117398# XMutableGridDataModel:

allow inserting rows at arbitrary positions

Change-Id: Ia5af125035979951c61d6c8cd9a916e8f81bb6c0
Reviewed-on: https://gerrit.libreoffice.org/545Reviewed-by: 's avatarMiklos Vajna <vmiklos@suse.cz>
Tested-by: 's avatarMiklos Vajna <vmiklos@suse.cz>
üst fe08068c
......@@ -44,7 +44,7 @@
@since OOo 3.3
*/
service DefaultGridDataModel : ::com::sun::star::awt::grid::XMutableGridDataModel;
published service DefaultGridDataModel : ::com::sun::star::awt::grid::XMutableGridDataModel;
}; }; }; };};
......
......@@ -44,7 +44,7 @@ module com { module sun { module star { module awt { module grid {
@since OOo 3.3
*/
struct GridDataEvent: com::sun::star::lang::EventObject
published struct GridDataEvent: com::sun::star::lang::EventObject
{
/** denotes the first column affected by a change.
......
......@@ -43,7 +43,7 @@ module com { module sun { module star { module awt { module grid {
@since OOo 3.3
*/
interface XGridDataListener: com::sun::star::lang::XEventListener
published interface XGridDataListener: com::sun::star::lang::XEventListener
{
/** is called when one or more rows of data have been inserted into a grid control's data model.
......
......@@ -38,9 +38,9 @@ module com { module sun { module star { module awt { module grid {
/** allows to modify the data represented by a <type>XGridDataModel</type>
*/
interface XMutableGridDataModel : XGridDataModel
published interface XMutableGridDataModel : XGridDataModel
{
/** adds a row to the model.
/** appends a row to the model.
@param Heading
denotes the heading of the row.
......@@ -49,7 +49,7 @@ interface XMutableGridDataModel : XGridDataModel
*/
void addRow( [in] any Heading, [in] sequence< any > Data );
/** adds multiple rows of data to the model.
/** appends multiple rows of data to the model.
@param Headings
denotes the headings of the to-be-added rows.
@param Data
......@@ -60,6 +60,38 @@ interface XMutableGridDataModel : XGridDataModel
void addRows( [in] sequence< any > Headings, [in] sequence< sequence< any > > Data )
raises ( ::com::sun::star::lang::IllegalArgumentException );
/** inserts a row into the set of data rows
@param Index
denotes the position at which the row is to be inserted
@param Heading
denotes the heading of the row.
@param Data
specifies the content of the row.
@throws ::com::sun::star::lang::IndexOutOfBoundsException
if <code>Index</code> is smaller than <code>0</code> or greater than the number of
rows in the model.
*/
void insertRow( [in] long Index, [in] any Heading, [in] sequence< any > Data )
raises ( ::com::sun::star::lang::IndexOutOfBoundsException );
/** inserts multiple rows of data into the model.
@param Index
denotes the position at which the rows are to be inserted
@param Headings
denotes the headings of the to-be-added rows.
@param Data
specifies the data of the rows to be added.
@throws ::com::sun::star::lang::IllegalArgumentException
if <code>Titles</code> and <code>Data</code> are of different length.
@throws ::com::sun::star::lang::IndexOutOfBoundsException
if <code>Index</code> is smaller than <code>0</code> or greater than the number of
rows in the model.
*/
void insertRows( [in] long Index, [in] sequence< any > Headings, [in] sequence< sequence< any > > Data )
raises ( ::com::sun::star::lang::IndexOutOfBoundsException
, ::com::sun::star::lang::IllegalArgumentException );
/** removes a row of data from the model
@param RowIndex
......
......@@ -205,6 +205,8 @@ public class GridControl
TMutableGridDataModel test = new TMutableGridDataModel( m_dataModel );
test.testAddRow();
test.testAddRows();
test.testInsertRow();
test.testInsertRows();
test.testRemoveRow();
test.testRemoveAllRows();
test.testUpdateCellData();
......
......@@ -52,14 +52,14 @@ public class TMutableGridDataModel
*/
public void testAddRow() throws IndexOutOfBoundsException
{
m_dataModel.addRow( 1, m_rowValues[0] );
m_dataModel.addRow( m_rowHeadings[0], m_rowValues[0] );
GridDataEvent event = m_listener.assertSingleRowInsertionEvent();
m_listener.reset();
assertEquals( "row insertion: wrong FirstRow (1)", 0, event.FirstRow );
assertEquals( "row insertion: wrong LastRow (1)", 0, event.LastRow );
impl_assertRowData( 0 );
m_dataModel.addRow( 2, m_rowValues[1] );
m_dataModel.addRow( m_rowHeadings[1], m_rowValues[1] );
event = m_listener.assertSingleRowInsertionEvent();
m_listener.reset();
assertEquals( "row insertion: wrong FirstRow (2)", 1, event.FirstRow );
......@@ -74,7 +74,9 @@ public class TMutableGridDataModel
{
assertEquals( "precondition not met: call this directly after testAddRow, please!", 2, m_dataModel.getRowCount() );
m_dataModel.addRows( new Object[] { "3", 4.0, "5" }, new Object[][] { m_rowValues[2], m_rowValues[3], m_rowValues[4] } );
m_dataModel.addRows(
new Object[] { m_rowHeadings[2], m_rowHeadings[3], m_rowHeadings[4] },
new Object[][] { m_rowValues[2], m_rowValues[3], m_rowValues[4] } );
GridDataEvent event = m_listener.assertSingleRowInsertionEvent();
assertEquals( "row insertion: wrong FirstRow (1)", 2, event.FirstRow );
assertEquals( "row insertion: wrong LastRow (1)", 4, event.LastRow );
......@@ -95,6 +97,145 @@ public class TMutableGridDataModel
m_dataModel, "addRows", new Object[] { new Object[0], new Object[1][2] }, IllegalArgumentException.class );
}
/**
* tests the XMutableGridDataModel.insertRow method
*/
public void testInsertRow() throws IndexOutOfBoundsException
{
int expectedRowCount = m_rowValues.length;
assertEquals( "precondition not met: call this directly after testAddRows, please!", expectedRowCount, m_dataModel.getRowCount() );
// inserting some row somewhere between the other rows
final Object heading = "inbetweenRow";
final Object[] inbetweenRow = new Object[] { "foo", "bar", 3, 4, 5 };
final int insertionPos = 2;
m_dataModel.insertRow( insertionPos, heading, inbetweenRow );
++expectedRowCount;
assertEquals( "inserting a row is expected to increment the row count",
expectedRowCount, m_dataModel.getRowCount() );
final GridDataEvent event = m_listener.assertSingleRowInsertionEvent();
assertEquals( "inserting a row results in wrong FirstRow being notified", insertionPos, event.FirstRow );
assertEquals( "inserting a row results in wrong LastRow being notified", insertionPos, event.LastRow );
m_listener.reset();
for ( int row=0; row<expectedRowCount; ++row )
{
final Object[] actualRowData = m_dataModel.getRowData( row );
final Object[] expectedRowData =
( row < insertionPos )
? m_rowValues[ row ]
: ( row == insertionPos )
? inbetweenRow
: m_rowValues[ row - 1 ];
assertArrayEquals( "row number " + row + " has wrong content content after inserting a row",
expectedRowData, actualRowData );
final Object actualHeading = m_dataModel.getRowHeading(row);
final Object expectedHeading =
( row < insertionPos )
? m_rowHeadings[ row ]
: ( row == insertionPos )
? heading
: m_rowHeadings[ row - 1 ];
assertEquals( "row " + row + " has a wrong heading after invoking insertRow",
expectedHeading, actualHeading );
}
// exceptions
assertException( "inserting a row at a position > rowCount is expected to throw",
m_dataModel, "insertRow",
new Class[] { Integer.class, Object.class, Object[].class },
new Object[] { expectedRowCount + 1, "", new Object[] { "1", 2, 3 } },
IndexOutOfBoundsException.class );
assertException( "inserting a row at a position < 0 is expected to throw",
m_dataModel, "insertRow",
new Class[] { Integer.class, Object.class, Object[].class },
new Object[] { -1, "", new Object[] { "1", 2, 3 } },
IndexOutOfBoundsException.class );
// remove the row, to create the situation expected by the next test
m_dataModel.removeRow( insertionPos );
m_listener.reset();
}
/**
* tests the XMutableGridDataModel.insertRows method
*/
public void testInsertRows() throws IndexOutOfBoundsException, IllegalArgumentException
{
int expectedRowCount = m_rowValues.length;
assertEquals( "precondition not met: call this directly after testInsertRow, please!", expectedRowCount, m_dataModel.getRowCount() );
// inserting some rows somewhere between the other rows
final int insertionPos = 3;
final Object[] rowHeadings = new Object[] { "A", "B", "C" };
final Object[][] rowData = new Object[][] {
new Object[] { "A", "B", "C", "D", "E" },
new Object[] { "J", "I", "H", "G", "F" },
new Object[] { "K", "L", "M", "N", "O" }
};
final int insertedRowCount = rowData.length;
assertEquals( "invalid test data", rowHeadings.length, insertedRowCount );
m_dataModel.insertRows( insertionPos, rowHeadings, rowData );
expectedRowCount += insertedRowCount;
final GridDataEvent event = m_listener.assertSingleRowInsertionEvent();
assertEquals( "inserting multiple rows results in wrong FirstRow being notified",
insertionPos, event.FirstRow );
assertEquals( "inserting multiple rows results in wrong LastRow being notified",
insertionPos + insertedRowCount - 1, event.LastRow );
m_listener.reset();
for ( int row=0; row<expectedRowCount; ++row )
{
final Object[] actualRowData = m_dataModel.getRowData( row );
final Object[] expectedRowData =
( row < insertionPos )
? m_rowValues[ row ]
: ( row >= insertionPos ) && ( row < insertionPos + insertedRowCount )
? rowData[ row - insertionPos ]
: m_rowValues[ row - insertedRowCount ];
assertArrayEquals( "row number " + row + " has wrong content content after inserting multiple rows",
expectedRowData, actualRowData );
final Object actualHeading = m_dataModel.getRowHeading(row);
final Object expectedHeading =
( row < insertionPos )
? m_rowHeadings[ row ]
: ( row >= insertionPos ) && ( row < insertionPos + insertedRowCount )
? rowHeadings[ row - insertionPos ]
: m_rowHeadings[ row - insertedRowCount ];
assertEquals( "row " + row + " has a wrong heading after invoking insertRows",
expectedHeading, actualHeading );
}
// exceptions
assertException( "inserting multiple rows at a position > rowCount is expected to throw an IndexOutOfBoundsException",
m_dataModel, "insertRows",
new Class[] { Integer.class, Object[].class, Object[][].class },
new Object[] { expectedRowCount + 1, new Object[0], new Object[][] { } },
IndexOutOfBoundsException.class );
assertException( "inserting multiple rows at a position < 0 is expected to throw an IndexOutOfBoundsException",
m_dataModel, "insertRows",
new Class[] { Integer.class, Object[].class, Object[][].class },
new Object[] { -1, new Object[0], new Object[][] { } },
IndexOutOfBoundsException.class );
assertException( "inserting multiple rows with inconsistent array lengths is expected to throw an IllegalArgumentException",
m_dataModel, "insertRows",
new Class[] { Integer.class, Object[].class, Object[][].class },
new Object[] { 0, new Object[0], new Object[][] { new Object[0] } },
IllegalArgumentException.class );
// remove the row, to create the situation expected by the next test
for ( int i=0; i<insertedRowCount; ++i )
{
m_dataModel.removeRow( insertionPos );
m_listener.reset();
}
}
/**
* tests the XMutableGridDataModel.removeRow method
*/
......@@ -149,7 +290,7 @@ public class TMutableGridDataModel
{
assertEquals( "precondition not met: call this directly after testRemoveAllRows, please!", 0, m_dataModel.getRowCount() );
m_dataModel.addRows( new Object[] { 1, 2, 3, 4, 5 }, m_rowValues );
m_dataModel.addRows( m_rowHeadings, m_rowValues );
m_listener.assertSingleRowInsertionEvent();
m_listener.reset();
......@@ -310,4 +451,8 @@ public class TMutableGridDataModel
new Object[] { 4, 5, 6, 7, "8" },
new Object[] { 5, "6", 7, 8, 9 },
};
private final static Object[] m_rowHeadings = new Object[] {
"1", 2, 3.0, "4", (float)5.0
};
}
......@@ -101,7 +101,7 @@ namespace toolkit
::sal_Int32 SAL_CALL DefaultGridDataModel::getRowCount() throw (::com::sun::star::uno::RuntimeException)
{
::comphelper::ComponentGuard aGuard( *this, rBHelper );
return m_aData.size();
return impl_getRowCount_nolck();
}
//------------------------------------------------------------------------------------------------------------------
......@@ -188,53 +188,71 @@ namespace toolkit
return resultData;
}
//------------------------------------------------------------------------------------------------------------------
void DefaultGridDataModel::impl_insertRow( sal_Int32 const i_position, Any const & i_heading, Sequence< Any > const & i_rowData, sal_Int32 const i_assumedColCount )
{
OSL_PRECOND( ( i_assumedColCount <= 0 ) || ( i_assumedColCount >= i_rowData.getLength() ),
"DefaultGridDataModel::impl_insertRow: invalid column count!" );
// insert heading
m_aRowHeaders.insert( m_aRowHeaders.begin() + i_position, i_heading );
// create new data row
RowData newRow( i_assumedColCount > 0 ? i_assumedColCount : i_rowData.getLength() );
RowData::iterator cellData = newRow.begin();
for ( const Any* pData = stl_begin( i_rowData ); pData != stl_end( i_rowData ); ++pData, ++cellData )
cellData->first = *pData;
// insert data row
m_aData.insert( m_aData.begin() + i_position, newRow );
}
//------------------------------------------------------------------------------------------------------------------
void SAL_CALL DefaultGridDataModel::addRow( const Any& i_heading, const Sequence< Any >& i_data ) throw (RuntimeException)
{
::comphelper::ComponentGuard aGuard( *this, rBHelper );
insertRow( getRowCount(), i_heading, i_data );
}
sal_Int32 const columnCount = i_data.getLength();
//------------------------------------------------------------------------------------------------------------------
void SAL_CALL DefaultGridDataModel::addRows( const Sequence< Any >& i_headings, const Sequence< Sequence< Any > >& i_data ) throw (IllegalArgumentException, RuntimeException)
{
insertRows( getRowCount(), i_headings, i_data );
}
// store header name
m_aRowHeaders.push_back( i_heading );
//------------------------------------------------------------------------------------------------------------------
void SAL_CALL DefaultGridDataModel::insertRow( ::sal_Int32 i_index, const Any& i_heading, const Sequence< Any >& i_data ) throw (RuntimeException, IndexOutOfBoundsException)
{
::comphelper::ComponentGuard aGuard( *this, rBHelper );
// store row m_aData
impl_addRow( i_data );
if ( ( i_index < 0 ) || ( i_index > impl_getRowCount_nolck() ) )
throw IndexOutOfBoundsException( ::rtl::OUString(), *this );
// actually insert the row
impl_insertRow( i_index, i_heading, i_data );
// update column count
sal_Int32 const columnCount = i_data.getLength();
if ( columnCount > m_nColumnCount )
m_nColumnCount = columnCount;
sal_Int32 const rowIndex = sal_Int32( m_aData.size() - 1 );
broadcast(
GridDataEvent( *this, -1, -1, rowIndex, rowIndex ),
GridDataEvent( *this, -1, -1, i_index, i_index ),
&XGridDataListener::rowsInserted,
aGuard
);
}
//------------------------------------------------------------------------------------------------------------------
void DefaultGridDataModel::impl_addRow( Sequence< Any > const & i_rowData, sal_Int32 const i_assumedColCount )
{
OSL_PRECOND( ( i_assumedColCount <= 0 ) || ( i_assumedColCount >= i_rowData.getLength() ),
"DefaultGridDataModel::impl_addRow: invalid column count!" );
RowData newRow( i_assumedColCount > 0 ? i_assumedColCount : i_rowData.getLength() );
RowData::iterator cellData = newRow.begin();
for ( const Any* pData = stl_begin( i_rowData ); pData != stl_end( i_rowData ); ++pData, ++cellData )
cellData->first = *pData;
m_aData.push_back( newRow );
}
//------------------------------------------------------------------------------------------------------------------
void SAL_CALL DefaultGridDataModel::addRows( const Sequence< Any >& i_headings, const Sequence< Sequence< Any > >& i_data ) throw (IllegalArgumentException, RuntimeException)
void SAL_CALL DefaultGridDataModel::insertRows( ::sal_Int32 i_index, const Sequence< Any>& i_headings, const Sequence< Sequence< Any > >& i_data ) throw (IllegalArgumentException, IndexOutOfBoundsException, RuntimeException)
{
if ( i_headings.getLength() != i_data.getLength() )
throw IllegalArgumentException( ::rtl::OUString(), *this, -1 );
::comphelper::ComponentGuard aGuard( *this, rBHelper );
if ( ( i_index < 0 ) || ( i_index > impl_getRowCount_nolck() ) )
throw IndexOutOfBoundsException( ::rtl::OUString(), *this );
sal_Int32 const rowCount = i_headings.getLength();
if ( rowCount == 0 )
return;
......@@ -250,17 +268,14 @@ namespace toolkit
for ( sal_Int32 row=0; row<rowCount; ++row )
{
m_aRowHeaders.push_back( i_headings[row] );
impl_addRow( i_data[row], maxColCount );
impl_insertRow( i_index + row, i_headings[row], i_data[row], maxColCount );
}
if ( maxColCount > m_nColumnCount )
m_nColumnCount = maxColCount;
sal_Int32 const firstRow = sal_Int32( m_aData.size() - rowCount );
sal_Int32 const lastRow = sal_Int32( m_aData.size() - 1 );
broadcast(
GridDataEvent( *this, -1, -1, firstRow, lastRow ),
GridDataEvent( *this, -1, -1, i_index, i_index + rowCount - 1 ),
&XGridDataListener::rowsInserted,
aGuard
);
......
......@@ -66,6 +66,8 @@ public:
// XMutableGridDataModel
virtual void SAL_CALL addRow( const Any& i_heading, const ::com::sun::star::uno::Sequence< ::com::sun::star::uno::Any >& Data ) throw (::com::sun::star::uno::RuntimeException);
virtual void SAL_CALL addRows( const ::com::sun::star::uno::Sequence< ::com::sun::star::uno::Any>& Headings, const ::com::sun::star::uno::Sequence< ::com::sun::star::uno::Sequence< ::com::sun::star::uno::Any > >& Data ) throw (::com::sun::star::lang::IllegalArgumentException, ::com::sun::star::uno::RuntimeException);
virtual void SAL_CALL insertRow( ::sal_Int32 i_index, const ::com::sun::star::uno::Any& i_heading, const ::com::sun::star::uno::Sequence< ::com::sun::star::uno::Any >& Data ) throw (::com::sun::star::uno::RuntimeException, ::com::sun::star::lang::IndexOutOfBoundsException);
virtual void SAL_CALL insertRows( ::sal_Int32 i_index, const ::com::sun::star::uno::Sequence< ::com::sun::star::uno::Any>& Headings, const ::com::sun::star::uno::Sequence< ::com::sun::star::uno::Sequence< ::com::sun::star::uno::Any > >& Data ) throw (::com::sun::star::lang::IllegalArgumentException, ::com::sun::star::lang::IndexOutOfBoundsException, ::com::sun::star::uno::RuntimeException);
virtual void SAL_CALL removeRow( ::sal_Int32 RowIndex ) throw (::com::sun::star::lang::IndexOutOfBoundsException, ::com::sun::star::uno::RuntimeException);
virtual void SAL_CALL removeAllRows( ) throw (::com::sun::star::uno::RuntimeException);
virtual void SAL_CALL updateCellData( ::sal_Int32 ColumnIndex, ::sal_Int32 RowIndex, const ::com::sun::star::uno::Any& Value ) throw (::com::sun::star::lang::IndexOutOfBoundsException, ::com::sun::star::uno::RuntimeException);
......@@ -106,7 +108,9 @@ private:
::comphelper::ComponentGuard & i_instanceLock
);
void impl_addRow( Sequence< Any > const & i_rowData, sal_Int32 const i_assumedColCount = -1 );
void impl_insertRow( sal_Int32 const i_position, Any const & i_heading, Sequence< Any > const & i_rowData, sal_Int32 const i_assumedColCount = -1 );
::sal_Int32 impl_getRowCount_nolck() const { return sal_Int32( m_aData.size() ); }
CellData const & impl_getCellData_throw( sal_Int32 const i_columnIndex, sal_Int32 const i_rowIndex ) const;
CellData& impl_getCellDataAccess_throw( sal_Int32 const i_columnIndex, sal_Int32 const i_rowIndex );
......
......@@ -604,6 +604,34 @@ namespace toolkit
delegator->addRows( i_headings, i_data );
}
//------------------------------------------------------------------------------------------------------------------
void SAL_CALL SortableGridDataModel::insertRow( ::sal_Int32 i_index, const Any& i_heading, const Sequence< Any >& i_data ) throw (RuntimeException, IndexOutOfBoundsException)
{
MethodGuard aGuard( *this, rBHelper );
DBG_CHECK_ME();
::sal_Int32 const rowIndex = i_index == getRowCount() ? i_index : impl_getPrivateRowIndex_throw( i_index );
// note that |RowCount| is a valid index in this method, but not for impl_getPrivateRowIndex_throw
Reference< XMutableGridDataModel > const delegator( m_delegator );
aGuard.clear();
delegator->insertRow( i_index, i_heading, i_data );
}
//------------------------------------------------------------------------------------------------------------------
void SAL_CALL SortableGridDataModel::insertRows( ::sal_Int32 i_index, const Sequence< Any>& i_headings, const Sequence< Sequence< Any > >& i_data ) throw (IllegalArgumentException, IndexOutOfBoundsException, RuntimeException)
{
MethodGuard aGuard( *this, rBHelper );
DBG_CHECK_ME();
::sal_Int32 const rowIndex = i_index == getRowCount() ? i_index : impl_getPrivateRowIndex_throw( i_index );
// note that |RowCount| is a valid index in this method, but not for impl_getPrivateRowIndex_throw
Reference< XMutableGridDataModel > const delegator( m_delegator );
aGuard.clear();
delegator->insertRows( i_index, i_headings, i_data );
}
//------------------------------------------------------------------------------------------------------------------
void SAL_CALL SortableGridDataModel::removeRow( ::sal_Int32 i_rowIndex ) throw (IndexOutOfBoundsException, RuntimeException)
{
......
......@@ -84,6 +84,8 @@ namespace toolkit
// XMutableGridDataModel
virtual void SAL_CALL addRow( const ::com::sun::star::uno::Any& Heading, const ::com::sun::star::uno::Sequence< ::com::sun::star::uno::Any >& Data ) throw (::com::sun::star::uno::RuntimeException);
virtual void SAL_CALL addRows( const ::com::sun::star::uno::Sequence< ::com::sun::star::uno::Any >& Headings, const ::com::sun::star::uno::Sequence< ::com::sun::star::uno::Sequence< ::com::sun::star::uno::Any > >& Data ) throw (::com::sun::star::lang::IllegalArgumentException, ::com::sun::star::uno::RuntimeException);
virtual void SAL_CALL insertRow( ::sal_Int32 i_index, const ::com::sun::star::uno::Any& i_heading, const ::com::sun::star::uno::Sequence< ::com::sun::star::uno::Any >& Data ) throw (::com::sun::star::uno::RuntimeException, ::com::sun::star::lang::IndexOutOfBoundsException);
virtual void SAL_CALL insertRows( ::sal_Int32 i_index, const ::com::sun::star::uno::Sequence< ::com::sun::star::uno::Any>& Headings, const ::com::sun::star::uno::Sequence< ::com::sun::star::uno::Sequence< ::com::sun::star::uno::Any > >& Data ) throw (::com::sun::star::lang::IllegalArgumentException, ::com::sun::star::lang::IndexOutOfBoundsException, ::com::sun::star::uno::RuntimeException);
virtual void SAL_CALL removeRow( ::sal_Int32 RowIndex ) throw (::com::sun::star::lang::IndexOutOfBoundsException, ::com::sun::star::uno::RuntimeException);
virtual void SAL_CALL removeAllRows( ) throw (::com::sun::star::uno::RuntimeException);
virtual void SAL_CALL updateCellData( ::sal_Int32 ColumnIndex, ::sal_Int32 RowIndex, const ::com::sun::star::uno::Any& Value ) throw (::com::sun::star::lang::IndexOutOfBoundsException, ::com::sun::star::uno::RuntimeException);
......
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