/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * This file is part of the LibreOffice project. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * This file incorporates work covered by the following license notice: * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed * with this work for additional information regarding copyright * ownership. The ASF licenses this file to you under the Apache * License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.apache.org/licenses/LICENSE-2.0 .
*/
using ::com::sun::star::accessibility::AccessibleTableModelChange; using ::com::sun::star::uno::Any; using ::com::sun::star::accessibility::XAccessible; using ::com::sun::star::uno::Reference;
/** default implementation of an ->ITableModel, used as fallback when no real model is present
Instances of this class are static in any way, and provide the least necessary default functionality for a table model.
*/ class EmptyTableModel : public ITableModel
{ public:
EmptyTableModel()
{
}
// adjust the current column, if it is larger than the column count now if ( m_nCurColumn >= m_nColumnCount )
{ if ( m_nColumnCount > 0 ) goTo( m_nCurColumn - 1, m_nCurRow ); else
m_nCurColumn = COL_INVALID;
}
// determine the right-most border of the last column which is // at least partially visible
aArea.SetRight( m_nRowHeaderWidthPixel ); if ( !m_aColumnWidths.empty() )
{ // the number of pixels which are scrolled out of the left hand // side of the window const tools::Long nScrolledOutLeft = m_nLeftColumn == 0 ? 0 : m_aColumnWidths[ m_nLeftColumn - 1 ].getEnd();
ColumnPositions::const_reverse_iterator loop = m_aColumnWidths.rbegin(); do
{
aArea.SetRight(loop->getEnd() - nScrolledOutLeft);
++loop;
} while ( ( loop != m_aColumnWidths.rend() )
&& ( loop->getEnd() - nScrolledOutLeft >= aArea.Right() )
);
} // so far, aArea.Right() denotes the first pixel *after* the cell area
aArea.AdjustRight( -1 );
// determine the last row which is at least partially visible
aArea.SetBottom(
m_nColHeaderHeightPixel
+ impl_getVisibleRows( true ) * m_nRowHeightPixel
- 1 );
bool lcl_updateScrollbar( vcl::Window& _rParent, VclPtr<ScrollBar>& _rpBar, boolconst i_needBar, tools::Long _nVisibleUnits,
tools::Long _nPosition, tools::Long _nRange, bool _bHorizontal, const Link<ScrollBar*,void>& _rScrollHandler )
{ // do we currently have the scrollbar? bool bHaveBar = _rpBar != nullptr;
// do we need to correct the scrollbar visibility? if ( bHaveBar && !i_needBar )
{ if ( _rpBar->IsTracking() )
_rpBar->EndTracking();
_rpBar.disposeAndClear();
} elseif ( !bHaveBar && i_needBar )
{
_rpBar = VclPtr<ScrollBar>::Create(
&_rParent,
WB_DRAG | ( _bHorizontal ? WB_HSCROLL : WB_VSCROLL )
);
_rpBar->SetScrollHdl( _rScrollHandler ); // get some speed into the scrolling...
lcl_setButtonRepeat( *_rpBar );
}
/** returns the number of rows fitting into the given range, for the given row height. Partially fitting rows are counted, too, if the respective parameter says so.
*/
TableSize lcl_getRowsFittingInto( tools::Long _nOverallHeight, tools::Long _nRowHeightPixel, bool _bAcceptPartialRow )
{ return _bAcceptPartialRow
? ( _nOverallHeight + ( _nRowHeightPixel - 1 ) ) / _nRowHeightPixel
: _nOverallHeight / _nRowHeightPixel;
}
/** returns the number of columns fitting into the given area, with the first visible column as given. Partially fitting columns are counted, too, if the respective parameter says so.
*/
TableSize lcl_getColumnsVisibleWithin( const tools::Rectangle& _rArea, ColPos _nFirstVisibleColumn, const TableControl_Impl& _rControl, bool _bAcceptPartialRow )
{
TableSize visibleColumns = 0;
TableColumnGeometry aColumn( _rControl, _rArea, _nFirstVisibleColumn ); while ( aColumn.isValid() )
{ if ( !_bAcceptPartialRow ) if ( aColumn.getRect().Right() > _rArea.Right() ) // this column is only partially visible, and this is not allowed break;
// flexibility
::sal_Int32 flexibility = pColumn->getFlexibility();
OSL_ENSURE( flexibility >= 0, "TableControl_Impl::impl_ni_calculateColumnWidths: a column's flexibility should be non-negative." ); if ( ( flexibility < 0 ) // normalization
|| ( !pColumn->isResizable() ) // column not resizable => no auto-resize
|| ( col <= i_assumeInflexibleColumnsUpToIncluding ) // column shall be treated as inflexible => respect this
)
flexibility = 0;
// min/max width
tools::Long effectiveMin = currentWidth, effectiveMax = currentWidth; // if the column is not flexible, it will not be asked for min/max, but we assume the current width as limit then if ( flexibility > 0 )
{
tools::Longconst minWidth = appFontWidthToPixel( pColumn->getMinWidth() ); if ( minWidth > 0 )
effectiveMin = minWidth; else
effectiveMin = MIN_COLUMN_WIDTH_PIXEL;
o_newColWidthsPixel = currentColWidths; if ( flexibilityDenominator == 0 )
{ // no column is flexible => don't adjust anything
} elseif ( gridWidthPixel > accumulatedCurrentWidth )
{ // we have space to give away ...
tools::Long distributePixel = gridWidthPixel - accumulatedCurrentWidth; if ( gridWidthPixel > accumulatedMaxWidth )
{ // ... but the column's maximal widths are still less than we have // => set them all to max for ( svt::table::TableSize i = 0; i < colCount; ++i )
{
o_newColWidthsPixel[i] = effectiveColumnLimits[i].second;
}
} else
{ bool startOver = false; do
{
startOver = false; // distribute the remaining space amongst all columns with a positive flexibility for ( size_t i=0; i<o_newColWidthsPixel.size() && !startOver; ++i )
{
tools::Longconst columnFlexibility = columnFlexibilities[i]; if ( columnFlexibility == 0 ) continue;
if ( newColWidth > effectiveColumnLimits[i].second )
{ // that was too much, we hit the col's maximum // set the new width to exactly this maximum
newColWidth = effectiveColumnLimits[i].second; // adjust the flexibility denominator ...
flexibilityDenominator -= columnFlexibility;
columnFlexibilities[i] = 0;
--flexibleColumnCount; // ... and the remaining width ...
tools::Longconst difference = newColWidth - currentColWidths[i];
distributePixel -= difference; // ... this way, we ensure that the width not taken up by this column is consumed by the other // flexible ones (if there are some)
// and start over with the first column, since there might be earlier columns which need // to be recalculated now
startOver = true;
}
o_newColWidthsPixel[i] = newColWidth;
}
} while ( startOver );
// are there pixels left (might be caused by rounding errors)?
distributePixel = gridWidthPixel - ::std::accumulate( o_newColWidthsPixel.begin(), o_newColWidthsPixel.end(), 0 ); while ( ( distributePixel > 0 ) && ( flexibleColumnCount > 0 ) )
{ // yes => ignore relative flexibilities, and subsequently distribute single pixels to all flexible // columns which did not yet reach their maximum. for ( size_t i=0; ( i < o_newColWidthsPixel.size() ) && ( distributePixel > 0 ); ++i )
{ if ( columnFlexibilities[i] == 0 ) continue;
++o_newColWidthsPixel[i];
--distributePixel;
}
}
}
} elseif ( gridWidthPixel < accumulatedCurrentWidth )
{ // we need to take away some space from the columns which allow it ...
tools::Long takeAwayPixel = accumulatedCurrentWidth - gridWidthPixel; if ( gridWidthPixel < accumulatedMinWidth )
{ // ... but the column's minimal widths are still more than we have // => set them all to min for ( svt::table::TableSize i = 0; i < colCount; ++i )
{
o_newColWidthsPixel[i] = effectiveColumnLimits[i].first;
}
} else
{ bool startOver = false; do
{
startOver = false; // take away the space we need from the columns with a positive flexibility for ( size_t i=0; i<o_newColWidthsPixel.size() && !startOver; ++i )
{
tools::Longconst columnFlexibility = columnFlexibilities[i]; if ( columnFlexibility == 0 ) continue;
if ( newColWidth < effectiveColumnLimits[i].first )
{ // that was too much, we hit the col's minimum // set the new width to exactly this minimum
newColWidth = effectiveColumnLimits[i].first; // adjust the flexibility denominator ...
flexibilityDenominator -= columnFlexibility;
columnFlexibilities[i] = 0;
--flexibleColumnCount; // ... and the remaining width ...
tools::Longconst difference = currentColWidths[i] - newColWidth;
takeAwayPixel -= difference;
// and start over with the first column, since there might be earlier columns which need // to be recalculated now
startOver = true;
}
o_newColWidthsPixel[i] = newColWidth;
}
} while ( startOver );
// are there pixels left (might be caused by rounding errors)?
takeAwayPixel = ::std::accumulate( o_newColWidthsPixel.begin(), o_newColWidthsPixel.end(), 0 ) - gridWidthPixel; while ( ( takeAwayPixel > 0 ) && ( flexibleColumnCount > 0 ) )
{ // yes => ignore relative flexibilities, and subsequently take away pixels from all flexible // columns which did not yet reach their minimum. for ( size_t i=0; ( i < o_newColWidthsPixel.size() ) && ( takeAwayPixel > 0 ); ++i )
{ if ( columnFlexibilities[i] == 0 ) continue;
// 1. adjust column widths, leaving space for a vertical scrollbar // 2. determine need for a vertical scrollbar // - V-YES: all fine, result from 1. is still valid // - V-NO: result from 1. is still under consideration
// 3. determine need for a horizontal scrollbar // - H-NO: all fine, result from 2. is still valid // - H-YES: reconsider need for a vertical scrollbar, if result of 2. was V-NO // - V-YES: all fine, result from 1. is still valid // - V-NO: redistribute the remaining space (if any) amongst all columns which allow it
// the width/height of a scrollbar, needed several times below
tools::Longconst nScrollbarMetrics = m_rAntiImpl.GetSettings().GetStyleSettings().GetScrollBarSize();
// determine the playground for the data cells (excluding headers) // TODO: what if the control is smaller than needed for the headers/scrollbars?
tools::Rectangle aDataCellPlayground( Point( 0, 0 ), m_rAntiImpl.GetOutputSizePixel() );
aDataCellPlayground.SetLeft( m_nRowHeaderWidthPixel );
aDataCellPlayground.SetTop( m_nColHeaderHeightPixel );
OSL_ENSURE( ( m_nRowCount == m_pModel->getRowCount() ) && ( m_nColumnCount == m_pModel->getColumnCount() ), "TableControl_Impl::impl_ni_relayout: how is this expected to work with invalid data?" );
tools::Longconst nAllColumnsWidth = ::std::accumulate( newWidthsPixel.begin(), newWidthsPixel.end(), 0 );
// do we need a vertical scrollbar? bool bNeedVerticalScrollbar = lcl_determineScrollbarNeed(
m_nTopRow, eVertScrollbar, aDataCellPlayground.GetHeight(), m_nRowHeightPixel * m_nRowCount ); bool bFirstRoundVScrollNeed = false; if ( bNeedVerticalScrollbar )
{
aDataCellPlayground.AdjustRight( -nScrollbarMetrics );
bFirstRoundVScrollNeed = true;
}
// do we need a horizontal scrollbar? boolconst bNeedHorizontalScrollbar = lcl_determineScrollbarNeed(
m_nLeftColumn, eHorzScrollbar, aDataCellPlayground.GetWidth(), nAllColumnsWidth ); if ( bNeedHorizontalScrollbar )
{
aDataCellPlayground.AdjustBottom( -nScrollbarMetrics );
// now that we just found that we need a horizontal scrollbar, // the need for a vertical one may have changed, since the horizontal // SB might just occupy enough space so that not all rows do fit // anymore if ( !bFirstRoundVScrollNeed )
{
bNeedVerticalScrollbar = lcl_determineScrollbarNeed(
m_nTopRow, eVertScrollbar, aDataCellPlayground.GetHeight(), m_nRowHeightPixel * m_nRowCount ); if ( bNeedVerticalScrollbar )
{
aDataCellPlayground.AdjustRight( -nScrollbarMetrics );
}
}
}
// the initial call to impl_ni_calculateColumnWidths assumed that we need a vertical scrollbar. If, by now, // we know that this is not the case, re-calculate the column widths. if ( !bNeedVerticalScrollbar )
gridWidthPixel = impl_ni_calculateColumnWidths( i_assumeInflexibleColumnsUpToIncluding, false, newWidthsPixel );
// update the column objects with the new widths we finally calculated
TableSize const colCount = m_pModel->getColumnCount();
m_aColumnWidths.reserve( colCount );
tools::Long accumulatedWidthPixel = m_nRowHeaderWidthPixel; bool anyColumnWidthChanged = false; for ( ColPos col = 0; col < colCount; ++col )
{ const tools::Long columnStart = accumulatedWidthPixel; const tools::Long columnEnd = columnStart + newWidthsPixel[col];
m_aColumnWidths.emplace_back( columnStart, columnEnd );
accumulatedWidthPixel = columnEnd;
// and don't forget to forward this to the column models
PColumnModel const pColumn = m_pModel->getColumnModel( col );
ENSURE_OR_THROW( !!pColumn, "invalid column returned by the model!" );
// if the column widths changed, ensure everything is repainted if ( anyColumnWidthChanged )
invalidate( TableArea::All );
// if the column resizing happened to leave some space at the right, but there are columns // scrolled out to the left, scroll them in while ( ( m_nLeftColumn > 0 )
&& ( accumulatedWidthPixel - m_aColumnWidths[ m_nLeftColumn - 1 ].getStart() <= gridWidthPixel )
)
{
--m_nLeftColumn;
}
// now adjust the column metrics, since they currently ignore the horizontal scroll position if ( m_nLeftColumn > 0 )
{ const tools::Long offsetPixel = m_aColumnWidths[ 0 ].getStart() - m_aColumnWidths[ m_nLeftColumn ].getStart(); for (auto & columnWidth : m_aColumnWidths)
{
columnWidth.move( offsetPixel );
}
}
// show or hide the scrollbars as needed, and position the data window
impl_ni_positionChildWindows( aDataCellPlayground, bNeedVerticalScrollbar, bNeedHorizontalScrollbar );
}
// our current style settings, to be passed to the renderer const StyleSettings& rStyle = rRenderContext.GetSettings().GetStyleSettings();
m_nRowCount = m_pModel->getRowCount(); // the area occupied by all (at least partially) visible cells, including // headers
tools::Rectangle const aAllCellsWithHeaders( impl_getAllVisibleCellsArea() );
// draw the header column area if (m_pModel->hasColumnHeaders())
{
TableRowGeometry const aHeaderRow(*this, tools::Rectangle(Point(0, 0), aAllCellsWithHeaders.BottomRight()), ROW_COL_HEADERS);
tools::Rectangle const aColRect(aHeaderRow.getRect());
pRenderer->PaintHeaderArea(rRenderContext, aColRect, true, false, rStyle); // Note that strictly, aHeaderRow.getRect() also contains the intersection between column // and row header area. However, below we go to paint this intersection, again, // so this hopefully doesn't hurt if we already paint it here.
for (TableCellGeometry aCell(aHeaderRow, m_nLeftColumn); aCell.isValid(); aCell.moveRight())
{ if (_rUpdateRect.GetIntersection(aCell.getRect()).IsEmpty()) continue;
pRenderer->PaintColumnHeader(aCell.getColumn(), rRenderContext, aCell.getRect(), rStyle);
}
} // the area occupied by the row header, if any
tools::Rectangle aRowHeaderArea; if (m_pModel->hasRowHeaders())
{
aRowHeaderArea = aAllCellsWithHeaders;
aRowHeaderArea.SetRight( m_nRowHeaderWidthPixel - 1 );
pRenderer->PaintHeaderArea(rRenderContext, aRowHeaderArea, false, true, rStyle); // Note that strictly, aRowHeaderArea also contains the intersection between column // and row header area. However, below we go to paint this intersection, again, // so this hopefully doesn't hurt if we already paint it here.
// give the renderer a chance to prepare the row
pRenderer->PrepareRow(aRowIterator.getRow(), isControlFocused, isSelectedRow, rRenderContext, aRect, rStyle);
// paint the row header if (m_pModel->hasRowHeaders())
{ const tools::Rectangle aCurrentRowHeader(aRowHeaderArea.GetIntersection(aRowIterator.getRect()));
pRenderer->PaintRowHeader(rRenderContext, aCurrentRowHeader, rStyle);
}
if (!colCount) continue;
// paint all cells in this row for (TableCellGeometry aCell(aRowIterator, m_nLeftColumn); aCell.isValid(); aCell.moveRight())
{
pRenderer->PaintCell(aCell.getColumn(), isSelectedRow, isControlFocused,
rRenderContext, aCell.getRect(), rStyle);
}
}
}
case TableControlAction::cursorSelectRow:
{ if(m_pSelEngine->GetSelectionMode() == SelectionMode::NONE) returnfalse; //pos is the position of the current row in the vector of selected rows, if current row is selected int pos = getRowSelectedNumber(m_aSelectedRows, m_nCurRow); //if current row is selected, it should be deselected, when ALT+SPACE are pressed if(pos>-1)
{
m_aSelectedRows.erase(m_aSelectedRows.begin()+pos); if(m_aSelectedRows.empty() && m_nAnchor != -1)
m_nAnchor = -1;
} //else select the row->put it in the vector else
m_aSelectedRows.push_back(m_nCurRow);
invalidateRow( m_nCurRow );
selectionChanged = true;
bSuccess = true;
} break; case TableControlAction::cursorSelectRowUp:
{ if(m_pSelEngine->GetSelectionMode() == SelectionMode::NONE) returnfalse; elseif(m_pSelEngine->GetSelectionMode() == SelectionMode::Single)
{ //if there are other selected rows, deselect them returnfalse;
} else
{ //there are other selected rows if(!m_aSelectedRows.empty())
{ //the anchor wasn't set -> a region is not selected, that's why clear all selection //and select the current row if(m_nAnchor==-1)
{
invalidateSelectedRows();
m_aSelectedRows.clear();
m_aSelectedRows.push_back(m_nCurRow);
invalidateRow( m_nCurRow );
} else
{ //a region is already selected, prevRow is last selected row and the row above - nextRow - should be selected int prevRow = getRowSelectedNumber(m_aSelectedRows, m_nCurRow); int nextRow = getRowSelectedNumber(m_aSelectedRows, m_nCurRow-1); if(prevRow>-1)
{ //if m_nCurRow isn't the upper one, can move up, otherwise not if(m_nCurRow>0)
m_nCurRow--; else returntrue; //if nextRow already selected, deselect it, otherwise select it if(nextRow>-1 && m_aSelectedRows[nextRow] == m_nCurRow)
{
m_aSelectedRows.erase(m_aSelectedRows.begin()+prevRow);
invalidateRow( m_nCurRow + 1 );
} else
{
m_aSelectedRows.push_back(m_nCurRow);
invalidateRow( m_nCurRow );
}
} else
{ if(m_nCurRow>0)
{
m_aSelectedRows.push_back(m_nCurRow);
m_nCurRow--;
m_aSelectedRows.push_back(m_nCurRow);
invalidateSelectedRegion( m_nCurRow+1, m_nCurRow );
}
}
}
} else
{ //if nothing is selected and the current row isn't the upper one //select the current and one row above //otherwise select only the upper row if(m_nCurRow>0)
{
m_aSelectedRows.push_back(m_nCurRow);
m_nCurRow--;
m_aSelectedRows.push_back(m_nCurRow);
invalidateSelectedRegion( m_nCurRow+1, m_nCurRow );
} else
{
m_aSelectedRows.push_back(m_nCurRow);
invalidateRow( m_nCurRow );
}
}
m_pSelEngine->SetAnchor(true);
m_nAnchor = m_nCurRow;
ensureVisible(m_nCurColumn, m_nCurRow);
selectionChanged = true;
bSuccess = true;
}
} break; case TableControlAction::cursorSelectRowDown:
{ if(m_pSelEngine->GetSelectionMode() == SelectionMode::NONE)
bSuccess = false; elseif(m_pSelEngine->GetSelectionMode() == SelectionMode::Single)
{
bSuccess = false;
} else
{ if(!m_aSelectedRows.empty())
{ //the anchor wasn't set -> a region is not selected, that's why clear all selection //and select the current row if(m_nAnchor==-1)
{
invalidateSelectedRows();
m_aSelectedRows.clear();
m_aSelectedRows.push_back(m_nCurRow);
invalidateRow( m_nCurRow );
} else
{ //a region is already selected, prevRow is last selected row and the row beneath - nextRow - should be selected int prevRow = getRowSelectedNumber(m_aSelectedRows, m_nCurRow); int nextRow = getRowSelectedNumber(m_aSelectedRows, m_nCurRow+1); if(prevRow>-1)
{ //if m_nCurRow isn't the last one, can move down, otherwise not if(m_nCurRow<m_nRowCount-1)
m_nCurRow++; else returntrue; //if next row already selected, deselect it, otherwise select it if(nextRow>-1 && m_aSelectedRows[nextRow] == m_nCurRow)
{
m_aSelectedRows.erase(m_aSelectedRows.begin()+prevRow);
invalidateRow( m_nCurRow - 1 );
} else
{
m_aSelectedRows.push_back(m_nCurRow);
invalidateRow( m_nCurRow );
}
} else
{ if(m_nCurRow<m_nRowCount-1)
{
m_aSelectedRows.push_back(m_nCurRow);
m_nCurRow++;
m_aSelectedRows.push_back(m_nCurRow);
invalidateSelectedRegion( m_nCurRow-1, m_nCurRow );
}
}
}
} else
{ //there wasn't any selection, select current and row beneath, otherwise only row beneath if(m_nCurRow<m_nRowCount-1)
{
m_aSelectedRows.push_back(m_nCurRow);
m_nCurRow++;
m_aSelectedRows.push_back(m_nCurRow);
invalidateSelectedRegion( m_nCurRow-1, m_nCurRow );
} else
{
m_aSelectedRows.push_back(m_nCurRow);
invalidateRow( m_nCurRow );
}
}
m_pSelEngine->SetAnchor(true);
m_nAnchor = m_nCurRow;
ensureVisible(m_nCurColumn, m_nCurRow);
selectionChanged = true;
bSuccess = true;
}
} break;
case TableControlAction::cursorSelectRowAreaTop:
{ if(m_pSelEngine->GetSelectionMode() == SelectionMode::NONE)
bSuccess = false; elseif(m_pSelEngine->GetSelectionMode() == SelectionMode::Single)
bSuccess = false; else
{ //select the region between the current and the upper row
RowPos iter = m_nCurRow;
invalidateSelectedRegion( m_nCurRow, 0 ); //put the rows in vector while(iter>=0)
{ if ( !isRowSelected( iter ) )
m_aSelectedRows.push_back(iter);
--iter;
}
m_nCurRow = 0;
m_nAnchor = m_nCurRow;
m_pSelEngine->SetAnchor(true);
ensureVisible(m_nCurColumn, 0);
selectionChanged = true;
bSuccess = true;
}
} break;
case TableControlAction::cursorSelectRowAreaBottom:
{ if(m_pSelEngine->GetSelectionMode() == SelectionMode::NONE) returnfalse; elseif(m_pSelEngine->GetSelectionMode() == SelectionMode::Single) returnfalse; //select the region between the current and the last row
RowPos iter = m_nCurRow;
invalidateSelectedRegion( m_nCurRow, m_nRowCount-1 ); //put the rows in the vector while(iter<=m_nRowCount)
{ if ( !isRowSelected( iter ) )
m_aSelectedRows.push_back(iter);
++iter;
}
m_nCurRow = m_nRowCount-1;
m_nAnchor = m_nCurRow;
m_pSelEngine->SetAnchor(true);
ensureVisible(m_nCurColumn, m_nRowCount-1);
selectionChanged = true;
bSuccess = true;
}
--> --------------------
--> maximum size reached
--> --------------------
Messung V0.5
¤ Dauer der Verarbeitung: 0.22 Sekunden
(vorverarbeitet)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.