/* -*- 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::sdb::XColumn; using ::com::sun::star::awt::XControl; using ::com::sun::star::awt::TabController; using ::com::sun::star::awt::XToolkit; using ::com::sun::star::awt::XWindowPeer; using ::com::sun::star::form::XGrid; using ::com::sun::star::beans::XPropertySet; using ::com::sun::star::uno::UNO_SET_THROW; using ::com::sun::star::uno::UNO_QUERY_THROW; using ::com::sun::star::container::XIndexAccess; using ::com::sun::star::uno::Exception; using ::com::sun::star::uno::XInterface; using ::com::sun::star::uno::UNO_QUERY; using ::com::sun::star::uno::Sequence; using ::com::sun::star::uno::Reference; using ::com::sun::star::beans::XPropertySetInfo; using ::com::sun::star::beans::PropertyValue; using ::com::sun::star::lang::IndexOutOfBoundsException; using ::com::sun::star::sdb::XInteractionSupplyParameters; using ::com::sun::star::awt::XTextComponent; using ::com::sun::star::awt::XTextListener; using ::com::sun::star::uno::Any; using ::com::sun::star::frame::XDispatch; using ::com::sun::star::lang::XMultiServiceFactory; using ::com::sun::star::uno::Type; using ::com::sun::star::lang::IllegalArgumentException; using ::com::sun::star::sdbc::XConnection; using ::com::sun::star::sdbc::XRowSet; using ::com::sun::star::sdbc::XDatabaseMetaData; using ::com::sun::star::util::XNumberFormatsSupplier; using ::com::sun::star::util::NumberFormatter; using ::com::sun::star::util::XNumberFormatter; using ::com::sun::star::sdbcx::XColumnsSupplier; using ::com::sun::star::container::XNameAccess; using ::com::sun::star::lang::EventObject; using ::com::sun::star::beans::Property; using ::com::sun::star::container::XEnumeration; using ::com::sun::star::form::XFormComponent; using ::com::sun::star::form::runtime::XFormOperations; using ::com::sun::star::form::runtime::FilterEvent; using ::com::sun::star::form::runtime::XFilterControllerListener; using ::com::sun::star::awt::XControlContainer; using ::com::sun::star::container::XIdentifierReplace; using ::com::sun::star::form::XFormControllerListener; using ::com::sun::star::awt::XWindow; using ::com::sun::star::sdbc::XResultSet; using ::com::sun::star::awt::XControlModel; using ::com::sun::star::awt::XTabControllerModel; using ::com::sun::star::beans::PropertyChangeEvent; using ::com::sun::star::form::validation::XValidatableFormComponent; using ::com::sun::star::form::XLoadable; using ::com::sun::star::form::XBoundControl; using ::com::sun::star::beans::XPropertyChangeListener; using ::com::sun::star::awt::TextEvent; using ::com::sun::star::form::XBoundComponent; using ::com::sun::star::awt::XCheckBox; using ::com::sun::star::awt::XComboBox; using ::com::sun::star::awt::XListBox; using ::com::sun::star::awt::ItemEvent; using ::com::sun::star::util::XModifyListener; using ::com::sun::star::form::XReset; using ::com::sun::star::frame::XDispatchProviderInterception; using ::com::sun::star::form::XGridControl; using ::com::sun::star::awt::XVclWindowPeer; using ::com::sun::star::form::validation::XValidator; using ::com::sun::star::awt::FocusEvent; using ::com::sun::star::sdb::SQLContext; using ::com::sun::star::container::XChild; using ::com::sun::star::form::TabulatorCycle_RECORDS; using ::com::sun::star::container::ContainerEvent; using ::com::sun::star::lang::DisposedException; using ::com::sun::star::lang::Locale; using ::com::sun::star::lang::NoSupportException; using ::com::sun::star::sdb::RowChangeEvent; using ::com::sun::star::frame::XStatusListener; using ::com::sun::star::frame::XDispatchProviderInterceptor; using ::com::sun::star::sdb::SQLErrorEvent; using ::com::sun::star::form::DatabaseParameterEvent; using ::com::sun::star::sdb::ParametersRequest; using ::com::sun::star::task::XInteractionRequest; using ::com::sun::star::util::URL; using ::com::sun::star::frame::FeatureStateEvent; using ::com::sun::star::form::runtime::XFormControllerContext; using ::com::sun::star::task::InteractionHandler; using ::com::sun::star::task::XInteractionHandler; using ::com::sun::star::form::runtime::FormOperations; using ::com::sun::star::container::XContainer; using ::com::sun::star::sdbc::SQLWarning;
struct ColumnInfo
{ // information about the column itself
Reference< XColumn > xColumn;
sal_Int32 nNullable; bool bAutoIncrement; bool bReadOnly;
OUString sName;
// information about the control(s) bound to this column
/// the first control which is bound to the given column, and which requires input
Reference< XControl > xFirstControlWithInputRequired; /** the first grid control which contains a column which is bound to the given database column, and requires input
*/
Reference< XGrid > xFirstGridWithInputRequiredColumn; /** if xFirstControlWithInputRequired is a grid control, then nRequiredGridColumn specifies the position of the grid column which is actually bound
*/
sal_Int32 nRequiredGridColumn;
void ColumnInfoCache::initializeControls( const Sequence< Reference< XControl > >& _rControls )
{ try
{ // for every of our known columns, find the controls which are bound to this column for (auto& rCol : m_aColumns)
{
OSL_ENSURE( !rCol.xFirstControlWithInputRequired.is() && !rCol.xFirstGridWithInputRequiredColumn.is()
&& ( rCol.nRequiredGridColumn == -1 ), "ColumnInfoCache::initializeControls: called me twice?" );
if ( !lcl_isBoundTo( xGridColumnModel, xNormColumn )
|| !lcl_isInputRequired( xGridColumnModel )
) continue; // with next grid column
break;
}
if ( gridCol < gridColCount )
{ // found a grid column which is bound to the given
rCol.xFirstGridWithInputRequiredColumn = std::move(xGrid);
rCol.nRequiredGridColumn = gridCol; break;
}
continue; // with next control
}
if ( !xModelPSI->hasPropertyByName( FM_PROP_BOUNDFIELD )
|| !lcl_isBoundTo( xModel, xNormColumn )
|| !lcl_isInputRequired( xModel )
) continue; // with next control
break;
}
if ( pControl == pControlEnd ) // did not find a control which is bound to this particular column, and for which the input is required continue; // with next DB column
struct UpdateAllListeners
{ booloperator()( const Reference< XDispatch >& _rxDispatcher ) const
{ static_cast< svx::OSingleFeatureDispatcher* >( _rxDispatcher.get() )->updateAllListeners(); // the return is a dummy only so we can use this struct in a lambda expression returntrue;
}
};
}
IMPL_LINK_NOARG( FormController, OnInvalidateFeatures, Timer*, void )
{
::osl::MutexGuard aGuard( m_aMutex ); for (constauto& rFeature : m_aInvalidFeatures)
{
DispatcherContainer::const_iterator aDispatcherPos = m_aFeatureDispatchers.find( rFeature ); if ( aDispatcherPos != m_aFeatureDispatchers.end() )
{ // TODO: for the real and actual listener notifications, we should release // our mutex
UpdateAllListeners( )( aDispatcherPos->second );
}
}
}
Sequence< OUString> SAL_CALL FormController::getSupportedServiceNames()
{ // service names which are supported only, but cannot be used to created an // instance at a service factory static constexpr OUString aNonCreatableServiceNames[] { u"com.sun.star.form.FormControllerDispatcher"_ustr };
// services which can be used to created an instance at a service factory
Sequence< OUString > aCreatableServiceNames( getSupportedServiceNames_Static() ); return ::comphelper::concatSequences( aCreatableServiceNames, aNonCreatableServiceNames );
}
// reset the text for all controls
::std::for_each( m_aFilterComponents.begin(), m_aFilterComponents.end(), ResetComponentText() );
if ( m_aFilterRows.empty() ) // nothing to do anymore return;
if ( m_nCurrentFilterPosition < 0 ) return;
// set the text for all filters
OSL_ENSURE( m_aFilterRows.size() > o3tl::make_unsigned(m_nCurrentFilterPosition), "FormController::impl_setTextOnAllFilter_throw: m_nCurrentFilterPosition too big" );
if ( ( Term < 0 ) || ( Term >= getDisjunctiveTerms() ) ) throw IndexOutOfBoundsException( OUString(), *this );
// if the to-be-deleted row is our current row, we need to shift if ( Term == m_nCurrentFilterPosition )
{ if ( m_nCurrentFilterPosition < sal_Int32( m_aFilterRows.size() - 1 ) )
++m_nCurrentFilterPosition; else
--m_nCurrentFilterPosition;
}
// if we're still active, simulate a "deactivated" event if ( m_xActiveControl.is() )
m_aActivateListeners.notifyEach( &XFormControllerListener::formDeactivated, aEvt );
// clean up our children for (constauto& rpChild : m_aChildren)
{ // search the position of the model within the form
Reference< XFormComponent > xForm(rpChild->getModel(), UNO_QUERY);
sal_uInt32 nPos = m_xModelAsIndex->getCount();
Reference< XFormComponent > xTemp; for( ; nPos; )
{
if (bAutoFields)
{ // as we don't want new controls to be attached to the scripting environment // we change attach flags
m_bAttachEvents = false; for (sal_Int32 i = nControls; i > 0;)
{
Reference< XControl > xControl = pControls[--i]; if (xControl.is())
{
Reference< XPropertySet > xSet(xControl->getModel(), UNO_QUERY); if (xSet.is() && ::comphelper::hasProperty(FM_PROP_BOUNDFIELD, xSet))
{ // does the model use a bound field ?
Reference< XPropertySet > xField;
xSet->getPropertyValue(FM_PROP_BOUNDFIELD) >>= xField;
// is it an autofield? if ( xField.is()
&& ::comphelper::hasProperty( FM_PROP_AUTOINCREMENT, xField )
&& ::comphelper::getBOOL( xField->getPropertyValue( FM_PROP_AUTOINCREMENT ) )
)
{
replaceControl( xControl, new FmXAutoControl() );
}
}
}
}
m_bAttachEvents = true;
} else
{
m_bDetachEvents = false; for (sal_Int32 i = nControls; i > 0;)
{
Reference< XControl > xControl = pControls[--i]; if (xControl.is())
{
Reference< XPropertySet > xSet(xControl->getModel(), UNO_QUERY); if (xSet.is() && ::comphelper::hasProperty(FM_PROP_BOUNDFIELD, xSet))
{ // does the model use a bound field ?
Reference< XPropertySet > xField;
xSet->getPropertyValue(FM_PROP_BOUNDFIELD) >>= xField;
// do we have a new filter if (!aText.isEmpty())
rRow[xText] = aText; else
{ // do we have the control in the row
FmFilterRow::iterator iter = rRow.find(xText); // erase the entry out of the row if (iter != rRow.end())
rRow.erase(iter);
}
try
{ if ( _rEvent.Source != m_xActiveControl )
{ // let this control grab the focus // (this case may happen if somebody moves the scroll wheel of the mouse over a control // which does not have the focus) // 85511 - 29.05.2001 - frank.schoenheit@germany.sun.com
// also, it happens when an image control gets a new image by double-clicking it // #i88458# / 2009-01-12 / frank.schoenheit@sun.com
Reference< XWindow > xControlWindow( _rEvent.Source, UNO_QUERY_THROW );
xControlWindow->setFocus();
}
} catch( const Exception& )
{
DBG_UNHANDLED_EXCEPTION("svx");
}
impl_onModify();
}
void FormController::impl_checkDisposed_throw() const
{ if ( impl_isDisposed_nofail() ) throw DisposedException( OUString(), *const_cast< FormController* >( this ) );
}
bool FormController::determineLockState() const
{
OSL_ENSURE( !impl_isDisposed_nofail(), "FormController: already disposed!" ); // a.) in filter mode we are always locked // b.) if we have no valid model or our model (a result set) is not alive -> we're locked // c.) if we are inserting everything is OK and we are not locked // d.) if are not updatable or on invalid position
Reference< XResultSet > xResultSet(m_xModelAsIndex, UNO_QUERY); if (m_bFiltering || !xResultSet.is() || !isRowSetAlive(xResultSet)) returntrue; else return !(m_bCanInsert && m_bCurrentRecordNew)
&& (xResultSet->isBeforeFirst() || xResultSet->isAfterLast() || xResultSet->rowDeleted() || !m_bCanUpdate);
}
Reference< XControl > xControl(e.Source, UNO_QUERY); if (m_bDBConnection)
{ // do we need to keep the locking of the commit // we hold the lock as long as the control differs from the current // otherwise we disabled the lock
m_bCommitLock = m_bCommitLock && xControl.get() != m_xCurrentControl.get(); if (m_bCommitLock) return;
// when do we have to commit a value to form or a filter // a.) if the current value is modified // b.) there must be a current control // c.) and it must be different from the new focus owning control or // d.) the focus is moving around (so we have only one control)
if ( ( m_bModified || m_bFiltering )
&& m_xCurrentControl.is()
&& ( ( xControl.get() != m_xCurrentControl.get() )
|| ( ( e.FocusFlags & FocusChangeReason::AROUND )
&& ( m_bCycle || m_bFiltering )
)
)
)
{ // check the old control if the content is ok #if OSL_DEBUG_LEVEL > 0 && !defined NDEBUG
Reference< XBoundControl > xLockingTest(m_xCurrentControl, UNO_QUERY); bool bControlIsLocked = xLockingTest.is() && xLockingTest->getLock();
assert(!bControlIsLocked && "FormController::Gained: I'm modified and the current control is locked ? How this ?"); // normally, a locked control should not be modified, so probably my bModified must // have been set from a different context, which I would not understand ... #endif
DBG_ASSERT(m_xCurrentControl.is(), "no CurrentControl set"); // first the control ask if it supports the IFace
Reference< XBoundComponent > xBound(m_xCurrentControl, UNO_QUERY); if (!xBound.is() && m_xCurrentControl.is())
xBound.set(m_xCurrentControl->getModel(), UNO_QUERY);
// lock if we lose the focus during commit
m_bCommitLock = true;
// commit unsuccessful, reset focus if (xBound.is() && !xBound->commit())
{ // the commit failed and we don't commit again until the current control // which couldn't be commit gains the focus again
Reference< XWindow > xWindow(m_xCurrentControl, UNO_QUERY); if (xWindow.is())
xWindow->setFocus(); return;
} else
{
m_bModified = false;
m_bCommitLock = false;
}
}
if (!m_bFiltering && m_bCycle && (e.FocusFlags & FocusChangeReason::AROUND) && m_xCurrentControl.is())
{
OSL_ENSURE( m_xFormOperations.is(), "FormController::focusGained: hmm?" ); // should have been created in setModel try
{ if ( e.FocusFlags & FocusChangeReason::FORWARD )
{ if ( m_xFormOperations.is() && m_xFormOperations->isEnabled( FormFeature::MoveToNext ) )
m_xFormOperations->execute( FormFeature::MoveToNext );
} else// backward
{ if ( m_xFormOperations.is() && m_xFormOperations->isEnabled( FormFeature::MoveToPrevious ) )
m_xFormOperations->execute( FormFeature::MoveToPrevious );
}
} catch ( const Exception& )
{ // don't handle this any further. That's an ... admissible error.
DBG_UNHANDLED_EXCEPTION("svx");
}
}
}
// still one and the same control if ( ( m_xActiveControl == xControl )
&& ( xControl == m_xCurrentControl )
)
{
DBG_ASSERT(m_xCurrentControl.is(), "No CurrentControl selected"); return;
}
// invalidate all features which depend on the currently focused control if ( m_bDBConnection && !m_bFiltering )
implInvalidateCurrentControlDependentFeatures();
if ( !m_xCurrentControl.is() ) return;
// control gets focus, then possibly in the visible range
Reference< XFormControllerContext > xContext( m_xFormControllerContext );
Reference< XControl > xCurrentControl( m_xCurrentControl );
aGuard.clear(); // <-- SYNCHRONIZED
if ( xContext.is() )
xContext->makeVisible( xCurrentControl );
}
Reference< XWindowPeer > xNext(e.NextFocus, UNO_QUERY); // if focus hasn't passed to some other window, e.g. focus in a welded item, don't deactivate if (!xNext) return;
Reference< XControl > xNextControl = isInList(xNext); if (!xNextControl.is())
{
m_xActiveControl = nullptr;
m_aDeactivationEvent.Call();
}
}
void SAL_CALL FormController::mousePressed( const awt::MouseEvent& /*_rEvent*/ )
{ // not interested in
}
void SAL_CALL FormController::mouseReleased( const awt::MouseEvent& /*_rEvent*/ )
{ // not interested in
}
try
{ // disconnect from the old model if (m_xModelAsIndex.is())
{ if (m_bDBConnection)
{ // we are currently working on the model
EventObject aEvt(m_xModelAsIndex);
unloaded(aEvt);
}
Reference< XLoadable > xForm(m_xModelAsIndex, UNO_QUERY); if (xForm.is())
xForm->removeLoadListener(this);
Reference< XSQLErrorBroadcaster > xBroadcaster(m_xModelAsIndex, UNO_QUERY); if (xBroadcaster.is())
xBroadcaster->removeSQLErrorListener(this);
Reference< XDatabaseParameterBroadcaster > xParamBroadcaster(m_xModelAsIndex, UNO_QUERY); if (xParamBroadcaster.is())
xParamBroadcaster->removeParameterListener(this);
}
disposeAllFeaturesAndDispatchers();
if ( m_xFormOperations.is() )
m_xFormOperations->dispose();
m_xFormOperations.clear();
// set the new model wait for the load event if (m_xTabController.is())
m_xTabController->setModel(Model);
m_xModelAsIndex.set(Model, UNO_QUERY);
m_xModelAsManager.set(Model, UNO_QUERY);
// only if both ifaces exit, the controller will work successful if (!m_xModelAsIndex.is() || !m_xModelAsManager.is())
{
m_xModelAsManager = nullptr;
m_xModelAsIndex = nullptr;
}
if (m_xModelAsIndex.is())
{ // re-create m_xFormOperations
m_xFormOperations = FormOperations::createWithFormController( m_xComponentContext, this );
m_xFormOperations->setFeatureInvalidation( this );
// adding load and ui interaction listeners
Reference< XLoadable > xForm(Model, UNO_QUERY); if (xForm.is())
xForm->addLoadListener(this);
Reference< XSQLErrorBroadcaster > xBroadcaster(Model, UNO_QUERY); if (xBroadcaster.is())
xBroadcaster->addSQLErrorListener(this);
Reference< XDatabaseParameterBroadcaster > xParamBroadcaster(Model, UNO_QUERY); if (xParamBroadcaster.is())
xParamBroadcaster->addParameterListener(this);
// well, is the database already loaded? // then we have to simulate a load event
Reference< XLoadable > xCursor(m_xModelAsIndex, UNO_QUERY); if (xCursor.is() && xCursor->isLoaded())
{
EventObject aEvt(xCursor);
loaded(aEvt);
}
void FormController::addToEventAttacher(const Reference< XControl > & xControl)
{
OSL_ENSURE( !impl_isDisposed_nofail(), "FormController: already disposed!" );
OSL_ENSURE( xControl.is(), "FormController::addToEventAttacher: invalid control - how did you reach this?" ); if ( !xControl.is() ) return; /* throw IllegalArgumentException(); */
// register at the event attacher
Reference< XFormComponent > xComp(xControl->getModel(), UNO_QUERY); if (!(xComp.is() && m_xModelAsIndex.is())) return;
// and look for the position of the ControlModel in it
sal_uInt32 nPos = m_xModelAsIndex->getCount();
Reference< XFormComponent > xTemp; for( ; nPos; )
{
m_xModelAsIndex->getByIndex(--nPos) >>= xTemp; if (xComp.get() == xTemp.get())
{
m_xModelAsManager->attach( nPos, Reference<XInterface>( xControl, UNO_QUERY ), Any(xControl) ); break;
}
}
}
void FormController::removeFromEventAttacher(const Reference< XControl > & xControl)
{
OSL_ENSURE( !impl_isDisposed_nofail(), "FormController: already disposed!" );
OSL_ENSURE( xControl.is(), "FormController::removeFromEventAttacher: invalid control - how did you reach this?" ); if ( !xControl.is() ) return; /* throw IllegalArgumentException(); */
// register at the event attacher
Reference< XFormComponent > xComp(xControl->getModel(), UNO_QUERY); if ( !(xComp.is() && m_xModelAsIndex.is()) ) return;
// and look for the position of the ControlModel in it
sal_uInt32 nPos = m_xModelAsIndex->getCount();
Reference< XFormComponent > xTemp; for( ; nPos; )
{
m_xModelAsIndex->getByIndex(--nPos) >>= xTemp; if (xComp.get() == xTemp.get())
{
m_xModelAsManager->detach( nPos, Reference<XInterface>( xControl, UNO_QUERY ) ); break;
}
}
}
void FormController::setContainer(const Reference< XControlContainer > & xContainer)
{
OSL_ENSURE( !impl_isDisposed_nofail(), "FormController: already disposed!" );
Reference< XTabControllerModel > xTabModel(getModel());
DBG_ASSERT(xTabModel.is() || !xContainer.is(), "No Model defined"); // if we have a new container we need a model
DBG_ASSERT(m_xTabController.is(), "FormController::setContainer : invalid aggregate !");
::osl::MutexGuard aGuard( m_aMutex );
Reference< XContainer > xCurrentContainer; if (m_xTabController.is())
xCurrentContainer.set(m_xTabController->getContainer(), UNO_QUERY); if (xCurrentContainer.is())
{
xCurrentContainer->removeContainerListener(this);
if ( m_aTabActivationIdle.IsActive() )
m_aTabActivationIdle.Stop();
// clear the filter map
::std::for_each( m_aFilterComponents.begin(), m_aFilterComponents.end(), RemoveComponentTextListener( this ) );
m_aFilterComponents.clear();
// collecting the controls for (const Reference<XControl>& rControl : m_aControls)
implControlRemoved( rControl, true );
// make database-specific things if (m_bDBConnection && isListeningForChanges())
stopListening();
m_aControls.realloc( 0 );
}
if (m_xTabController.is())
m_xTabController->setContainer(xContainer);
// What controls belong to the container? if (xContainer.is() && xTabModel.is())
{ const Sequence< Reference< XControlModel > > aModels = xTabModel->getControlModels();
Sequence< Reference< XControl > > aAllControls = xContainer->getControls();
// not every model had an associated control if (j != nCount)
m_aControls.realloc(j);
// listen at the container
Reference< XContainer > xNewContainer(xContainer, UNO_QUERY); if (xNewContainer.is())
xNewContainer->addContainerListener(this);
// make database-specific things if (m_bDBConnection)
{
m_bLocked = determineLockState();
setLocks(); if (!isLocked())
startListening();
}
} // the controls are in the right order
m_bControlsSorted = true;
}
// It is locked // a. if the entire record is locked // b. if the associated field is locked
Reference< XBoundControl > xBound(xControl, UNO_QUERY); if (!(xBound.is() &&
( (bLocked && bLocked != bool(xBound->getLock())) ||
!bLocked))) // always uncheck individual fields when unlocking return;
// there is a data source
Reference< XPropertySet > xSet(xControl->getModel(), UNO_QUERY); if (!(xSet.is() && ::comphelper::hasProperty(FM_PROP_BOUNDFIELD, xSet))) return;
// what about the ReadOnly and Enable properties bool bTouch = true; if (::comphelper::hasProperty(FM_PROP_ENABLED, xSet))
bTouch = ::comphelper::getBOOL(xSet->getPropertyValue(FM_PROP_ENABLED)); if (bTouch && ::comphelper::hasProperty(FM_PROP_READONLY, xSet))
bTouch = !::comphelper::getBOOL(xSet->getPropertyValue(FM_PROP_READONLY));
if (!bTouch) return;
Reference< XPropertySet > xField;
xSet->getPropertyValue(FM_PROP_BOUNDFIELD) >>= xField; if (!xField.is()) return;
if (bLocked)
xBound->setLock(bLocked); else
{ try
{
Any aVal = xField->getPropertyValue(FM_PROP_ISREADONLY); if (aVal.hasValue() && ::comphelper::getBOOL(aVal))
xBound->setLock(true); else
xBound->setLock(bLocked);
} catch( const Exception& )
{
DBG_UNHANDLED_EXCEPTION("svx");
}
}
}
void FormController::setLocks()
{
OSL_ENSURE( !impl_isDisposed_nofail(), "FormController: already disposed!" ); // lock/unlock all controls connected to a data source for (const Reference<XControl>& rControl : m_aControls)
setControlLock( rControl );
}
bool bModifyListening = lcl_shouldListenForModifications( xControl, this );
// artificial while while ( bModifyListening )
{
Reference< XModifyBroadcaster > xMod(xControl, UNO_QUERY); if (xMod.is())
{
xMod->addModifyListener(this); break;
}
// all the text to prematurely recognize a modified
Reference< XTextComponent > xText(xControl, UNO_QUERY); if (xText.is())
{
xText->addTextListener(this); break;
}
// artificial while while (bModifyListening)
{
Reference< XModifyBroadcaster > xMod(xControl, UNO_QUERY); if (xMod.is())
{
xMod->removeModifyListener(this); break;
} // all the text to prematurely recognize a modified
Reference< XTextComponent > xText(xControl, UNO_QUERY); if (xText.is())
{
xText->removeTextListener(this); break;
}
void FormController::implControlInserted( const Reference< XControl>& _rxControl, bool _bAddToEventAttacher )
{
Reference< XWindow > xWindow( _rxControl, UNO_QUERY ); if ( xWindow.is() )
{
xWindow->addFocusListener( this );
xWindow->addMouseListener( this );
if ( _bAddToEventAttacher )
addToEventAttacher( _rxControl );
}
// add a dispatch interceptor to the control (if supported)
Reference< XDispatchProviderInterception > xInterception( _rxControl, UNO_QUERY ); if ( xInterception.is() )
createInterceptor( xInterception );
// we want to know about the reset of the model of our controls // (for correctly resetting m_bModified)
Reference< XReset > xReset( xModel, UNO_QUERY ); if ( xReset.is() )
xReset->addResetListener( this );
// and we want to know about the validity, to visually indicate it
Reference< XValidatableFormComponent > xValidatable( xModel, UNO_QUERY ); if ( xValidatable.is() )
{
xValidatable->addFormComponentValidityListener( this );
m_aControlBorderManager.validityChanged( _rxControl, xValidatable );
}
}
void FormController::implControlRemoved( const Reference< XControl>& _rxControl, bool _bRemoveFromEventAttacher )
{
Reference< XWindow > xWindow( _rxControl, UNO_QUERY ); if ( xWindow.is() )
{
xWindow->removeFocusListener( this );
xWindow->removeMouseListener( this );
if ( _bRemoveFromEventAttacher )
removeFromEventAttacher( _rxControl );
}
if ( isListeningForChanges() && m_bDetachEvents )
stopControlModifyListening( xControl );
}
// XLoadListener
void FormController::loaded(const EventObject& rEvent)
{
OSL_ENSURE( rEvent.Source == m_xModelAsIndex, "FormController::loaded: where did this come from?" );
OSL_ENSURE( !impl_isDisposed_nofail(), "FormController: already disposed!" );
::osl::MutexGuard aGuard( m_aMutex );
Reference< XRowSet > xForm(rEvent.Source, UNO_QUERY); // do we have a connected data source if (xForm.is() && getConnection(xForm).is())
{
Reference< XPropertySet > xSet(xForm, UNO_QUERY); if (xSet.is())
{
Any aVal = xSet->getPropertyValue(FM_PROP_CYCLE);
sal_Int32 aVal2 = 0;
::cppu::enum2int(aVal2,aVal);
m_bCycle = !aVal.hasValue() || static_cast<form::TabulatorCycle>(aVal2) == TabulatorCycle_RECORDS;
m_bCanUpdate = canUpdate(xSet);
m_bCanInsert = canInsert(xSet);
m_bCurrentRecordModified = ::comphelper::getBOOL(xSet->getPropertyValue(FM_PROP_ISMODIFIED));
m_bCurrentRecordNew = ::comphelper::getBOOL(xSet->getPropertyValue(FM_PROP_ISNEW));
startFormListening( xSet, false );
// set the locks for the current controls if (getContainer().is())
{
m_aLoadEvent.Call();
}
} else
{
m_bCanInsert = m_bCanUpdate = m_bCycle = false;
m_bCurrentRecordModified = false;
m_bCurrentRecordNew = false;
m_bLocked = false;
}
m_bDBConnection = true;
} else
{
m_bDBConnection = false;
m_bCanInsert = m_bCanUpdate = m_bCycle = false;
m_bCurrentRecordModified = false;
m_bCurrentRecordNew = false;
m_bLocked = false;
}
void FormController::removeBoundFieldListener()
{ for (const Reference<XControl>& rControl : m_aControls)
{
Reference< XPropertySet > xProp( rControl, UNO_QUERY ); if ( xProp.is() )
xProp->removePropertyChangeListener( FM_PROP_BOUNDFIELD, this );
}
}
void FormController::startFormListening( const Reference< XPropertySet >& _rxForm, bool _bPropertiesOnly )
{ try
{ if ( m_bCanInsert || m_bCanUpdate ) // form can be modified
{
_rxForm->addPropertyChangeListener( FM_PROP_ISNEW, this );
_rxForm->addPropertyChangeListener( FM_PROP_ISMODIFIED, this );
if ( !_bPropertiesOnly )
{ // set the Listener for UI interaction
Reference< XRowSetApproveBroadcaster > xApprove( _rxForm, UNO_QUERY ); if ( xApprove.is() )
xApprove->addRowSetApproveListener( this );
// listener for row set changes
Reference< XRowSet > xRowSet( _rxForm, UNO_QUERY ); if ( xRowSet.is() )
xRowSet->addRowSetListener( this );
}
}
if ( m_aTabActivationIdle.IsActive() )
m_aTabActivationIdle.Stop();
m_aTabActivationIdle.Start();
} // are we in filtermode and a XModeSelector has inserted an element elseif (m_bFiltering && Reference< XModeSelector > (evt.Source, UNO_QUERY).is())
{
xModel.set(evt.Source, UNO_QUERY); if (xModel.is() && m_xModelAsIndex == xModel->getParent())
{
Reference< XPropertySet > xSet(xControl->getModel(), UNO_QUERY); if (xSet.is() && ::comphelper::hasProperty(FM_PROP_BOUNDFIELD, xSet))
{ // does the model use a bound field ?
Reference< XPropertySet > xField;
xSet->getPropertyValue(FM_PROP_BOUNDFIELD) >>= xField;
Reference< XTextComponent > xText(xControl, UNO_QUERY); // may we filter the field? if (xText.is() && xField.is() && ::comphelper::hasProperty(FM_PROP_SEARCHABLE, xField) &&
::comphelper::getBOOL(xField->getPropertyValue(FM_PROP_SEARCHABLE)))
{
m_aFilterComponents.push_back( xText );
xText->addTextListener( this );
}
}
}
}
}
Reference< XControl > xControl;
evt.Element >>= xControl; if (!xControl.is()) return;
Reference< XFormComponent > xModel(xControl->getModel(), UNO_QUERY); if (xModel.is() && m_xModelAsIndex == xModel->getParent())
{
removeControl(xControl); // Do not recalculate TabOrder, because it must already work internally!
} // are we in filtermode and a XModeSelector has inserted an element elseif (m_bFiltering && Reference< XModeSelector > (evt.Source, UNO_QUERY).is())
{
FilterComponents::iterator componentPos = ::std::find(
m_aFilterComponents.begin(), m_aFilterComponents.end(), xControl ); if ( componentPos != m_aFilterComponents.end() )
m_aFilterComponents.erase( componentPos );
}
}
// the parent of our (to-be-)child must be our own model
Reference< XFormComponent > xFormOfChild( ChildController->getModel(), UNO_QUERY ); if ( !xFormOfChild.is() ) throw IllegalArgumentException( OUString(), *this, 1 ); // TODO: (localized) error message
// ok, we receive the list of filters as sequence of fieldnames, value // now we have to transform the fieldname into UI names, that could be a label of the field or // an aliasname or the fieldname itself
// first adjust the field names if necessary
Reference< XNameAccess > xQueryColumns =
Reference< XColumnsSupplier >( m_xComposer, UNO_QUERY_THROW )->getColumns();
for (auto& rFieldInfo : rFieldInfos)
{ if ( xQueryColumns->hasByName(rFieldInfo.aFieldName) )
{ if ( (xQueryColumns->getByName(rFieldInfo.aFieldName) >>= rFieldInfo.xField) && rFieldInfo.xField.is() )
rFieldInfo.xField->getPropertyValue(FM_PROP_REALNAME) >>= rFieldInfo.aFieldName;
}
}
Reference< XDatabaseMetaData> xMetaData(xConnection->getMetaData()); // now transfer the filters into Value/TextComponent pairs
::comphelper::UStringMixEqual aCompare(xMetaData->storesMixedCaseQuotedIdentifiers());
// retrieving the filter for (const Sequence < PropertyValue >& rRow : aFilterRows)
{
FmFilterRow aRow;
// search a field for the given name for (const PropertyValue& rRefValue : rRow)
{ // look for the text component
Reference< XPropertySet > xField; try
{
Reference< XPropertySet > xSet;
OUString aRealName;
// first look with the given name if (xQueryColumns->hasByName(rRefValue.Name))
{
xQueryColumns->getByName(rRefValue.Name) >>= xSet;
// get the RealName
xSet->getPropertyValue(u"RealName"_ustr) >>= aRealName;
// compare the condition field name and the RealName if (aCompare(aRealName, rRefValue.Name))
xField = xSet;
} if (!xField.is())
{ // no we have to check every column to find the realname
Reference< XIndexAccess > xColumnsByIndex(xQueryColumns, UNO_QUERY); for (sal_Int32 n = 0, nCount = xColumnsByIndex->getCount(); n < nCount; n++)
{
xColumnsByIndex->getByIndex(n) >>= xSet;
xSet->getPropertyValue(u"RealName"_ustr) >>= aRealName; if (aCompare(aRealName, rRefValue.Name))
{ // get the column by its alias
xField = xSet; break;
}
}
} if (!xField.is()) continue;
} catch (const Exception&)
{ continue;
}
// find the text component for (constauto& rFieldInfo : rFieldInfos)
{ // we found the field so insert a new entry to the filter row if (rFieldInfo.xField == xField)
{ // do we already have the control ? if (aRow.find(rFieldInfo.xText) != aRow.end())
{
OString aVal = m_pParser->getContext().getIntlKeywordAscii(IParseContext::InternationalKeyCode::And);
aRow[rFieldInfo.xText] = aRow[rFieldInfo.xText] + " " +
OStringToOUString(aVal, RTL_TEXTENCODING_ASCII_US) + " " +
::comphelper::getString(rRefValue.Value);
} else
{
OUString sPredicate,sErrorMsg;
rRefValue.Value >>= sPredicate;
std::unique_ptr< OSQLParseNode > pParseNode = predicateTree(sErrorMsg, sPredicate, xFormatter, xField); if ( pParseNode != nullptr )
{
OUString sCriteria; switch (rRefValue.Handle)
{ case css::sdb::SQLFilterOperator::EQUAL:
sCriteria += "="; break; case css::sdb::SQLFilterOperator::NOT_EQUAL:
sCriteria += "!="; break; case css::sdb::SQLFilterOperator::LESS:
sCriteria += "<"; break; case css::sdb::SQLFilterOperator::GREATER:
sCriteria += ">"; break; case css::sdb::SQLFilterOperator::LESS_EQUAL:
sCriteria += "<="; break; case css::sdb::SQLFilterOperator::GREATER_EQUAL:
sCriteria += ">="; break; case css::sdb::SQLFilterOperator::LIKE:
sCriteria += "LIKE "; break; case css::sdb::SQLFilterOperator::NOT_LIKE:
sCriteria += "NOT LIKE "; break; case css::sdb::SQLFilterOperator::SQLNULL:
sCriteria += "IS NULL"; break; case css::sdb::SQLFilterOperator::NOT_SQLNULL:
sCriteria += "IS NOT NULL"; break;
}
pParseNode->parseNodeToPredicateStr( sCriteria
,xConnection
,xFormatter
,xField
,OUString()
,aAppLocale
,strDecimalSeparator
,getParseContext());
aRow[rFieldInfo.xText] = sCriteria;
}
}
}
}
}
if (aRow.empty()) continue;
impl_addFilterRow( aRow );
}
}
// now set the filter controls for (constauto& rFieldInfo : rFieldInfos)
{
m_aFilterComponents.push_back( rFieldInfo.xText );
}
}
Reference< XConnection > xConnection( getConnection( Reference< XRowSet >( m_xModelAsIndex, UNO_QUERY ) ) ); if ( !xConnection.is() ) // nothing to do - can't filter a form which is not connected return;
// stop listening for controls if (isListeningForChanges())
stopListening();
m_bFiltering = true;
// as we don't want new controls to be attached to the scripting environment // we change attach flags
m_bAttachEvents = false;
// exchanging the controls for the current form
Sequence< Reference< XControl > > aControlsCopy( m_aControls ); const Reference< XControl >* pControls = aControlsCopy.getConstArray();
sal_Int32 nControlCount = aControlsCopy.getLength();
// the control we have to activate after replacement
Reference< XNumberFormatsSupplier > xFormatSupplier = getNumberFormats(xConnection, true);
Reference< XNumberFormatter > xFormatter = NumberFormatter::create(m_xComponentContext);
xFormatter->attachNumberFormatsSupplier(xFormatSupplier);
// structure for storing the field info
::std::vector<FmFieldInfo> aFieldInfos;
for (sal_Int32 i = nControlCount; i > 0;)
{
Reference< XControl > xControl = pControls[--i]; if (xControl.is())
{ // no events for the control anymore
removeFromEventAttacher(xControl);
// do we have a mode selector
Reference< XModeSelector > xSelector(xControl, UNO_QUERY); if (xSelector.is())
{
xSelector->setMode( u"FilterMode"_ustr );
// listening for new controls of the selector
Reference< XContainer > xContainer(xSelector, UNO_QUERY); if (xContainer.is())
xContainer->addContainerListener(this);
Reference< XEnumerationAccess > xElementAccess(xSelector, UNO_QUERY); if (xElementAccess.is())
{
Reference< XEnumeration > xEnumeration(xElementAccess->createEnumeration());
Reference< XControl > xSubControl; while (xEnumeration->hasMoreElements())
{
xEnumeration->nextElement() >>= xSubControl; if (xSubControl.is())
{
Reference< XPropertySet > xSet(xSubControl->getModel(), UNO_QUERY); if (xSet.is() && ::comphelper::hasProperty(FM_PROP_BOUNDFIELD, xSet))
{ // does the model use a bound field ?
Reference< XPropertySet > xField;
xSet->getPropertyValue(FM_PROP_BOUNDFIELD) >>= xField;
Reference< XTextComponent > xText(xSubControl, UNO_QUERY); // may we filter the field? if (xText.is() && xField.is() && ::comphelper::hasProperty(FM_PROP_SEARCHABLE, xField) &&
::comphelper::getBOOL(xField->getPropertyValue(FM_PROP_SEARCHABLE)))
{
aFieldInfos.emplace_back(xField, xText);
xText->addTextListener(this);
}
}
}
}
} continue;
}
Reference< XPropertySet > xModel( xControl->getModel(), UNO_QUERY ); if (xModel.is() && ::comphelper::hasProperty(FM_PROP_BOUNDFIELD, xModel))
{ // does the model use a bound field ?
Any aVal = xModel->getPropertyValue(FM_PROP_BOUNDFIELD);
Reference< XPropertySet > xField;
aVal >>= xField;
// we have all filter controls now, so the next step is to read the filters from the form // resolve all aliases and set the current filter to the according structure
setFilter(aFieldInfos);
// lock all controls which are not used for filtering
m_bLocked = determineLockState();
setLocks();
m_bAttachEvents = true;
}
void FormController::stopFiltering()
{
OSL_ENSURE( !impl_isDisposed_nofail(), "FormController: already disposed!" ); if ( !m_bFiltering ) // #104693# OJ
{ // nothing to do return;
}
m_bFiltering = false;
m_bDetachEvents = false;
::comphelper::disposeComponent(m_xComposer);
// exchanging the controls for the current form
Sequence< Reference< XControl > > aControlsCopy( m_aControls ); const Reference< XControl > * pControls = aControlsCopy.getConstArray();
sal_Int32 nControlCount = aControlsCopy.getLength();
// clear the filter control map
::std::for_each( m_aFilterComponents.begin(), m_aFilterComponents.end(), RemoveComponentTextListener( this ) );
m_aFilterComponents.clear();
for ( sal_Int32 i = nControlCount; i > 0; )
{
Reference< XControl > xControl = pControls[--i]; if (xControl.is())
{ // now enable event handling again
addToEventAttacher(xControl);
// listening for new controls of the selector
Reference< XContainer > xContainer(xSelector, UNO_QUERY); if (xContainer.is())
xContainer->removeContainerListener(this); continue;
}
Reference< XPropertySet > xSet(xControl->getModel(), UNO_QUERY); if (xSet.is() && ::comphelper::hasProperty(FM_PROP_BOUNDFIELD, xSet))
{ // does the model use a bound field ?
Reference< XPropertySet > xField;
xSet->getPropertyValue(FM_PROP_BOUNDFIELD) >>= xField;
Reference< XValidatableFormComponent > xValidatable; while ( xControlEnumeration->hasMoreElements() )
{ if ( !( xControlEnumeration->nextElement() >>= xValidatable ) ) // control does not support validation continue;
if ( xValidatable->isValid() ) continue;
Reference< XValidator > xValidator( xValidatable->getValidator() );
OSL_ENSURE( xValidator.is(), "FormController::checkFormComponentValidity: invalid, but no validator?" ); if ( !xValidator.is() ) // this violates the interface definition of css.form.validation.XValidatableFormComponent ... continue;
// first, check whether the form has a property telling us the answer // this allows people to use the XPropertyContainer interface of a form to control // the behaviour on a per-form basis.
Reference< XPropertySet > xFormProps( _rxForm, UNO_QUERY_THROW );
Reference< XPropertySetInfo > xPSI( xFormProps->getPropertySetInfo() ); if ( xPSI->hasPropertyByName( s_sFormsCheckRequiredFields ) )
{ bool bShouldValidate = true;
OSL_VERIFY( xFormProps->getPropertyValue( s_sFormsCheckRequiredFields ) >>= bShouldValidate ); return bShouldValidate;
}
// next, check the data source which created the connection
Reference< XChild > xConnectionAsChild( xFormProps->getPropertyValue( FM_PROP_ACTIVE_CONNECTION ), UNO_QUERY_THROW );
Reference< XPropertySet > xDataSource( xConnectionAsChild->getParent(), UNO_QUERY ); if ( !xDataSource.is() ) // seldom (but possible): this is not a connection created by a data source returntrue;
// if some of the control models are bound to validators, check them
OUString sInvalidityExplanation;
Reference< XControlModel > xInvalidModel; if ( !checkFormComponentValidity( sInvalidityExplanation, xInvalidModel ) )
{
Reference< XControl > xControl( locateControl( xInvalidModel ) );
aGuard.clear();
displayErrorSetFocus( sInvalidityExplanation, xControl, getDialogParentWindow(this) ); returnfalse;
}
// check values on NULL and required flag if ( !lcl_shouldValidateRequiredFields_nothrow( _rEvent.Source ) ) returntrue;
OSL_ENSURE(m_pColumnInfoCache, "FormController::approveRowChange: no column infos!"); if (!m_pColumnInfoCache) returntrue;
try
{ if ( !m_pColumnInfoCache->controlsInitialized() )
m_pColumnInfoCache->initializeControls( getControls() );
size_t colCount = m_pColumnInfoCache->getColumnCount(); for ( size_t col = 0; col < colCount; ++col )
{ const ColumnInfo& rColInfo = m_pColumnInfoCache->getColumnInfo( col );
if ( rColInfo.bAutoIncrement ) continue;
if ( rColInfo.bReadOnly ) continue;
if ( !rColInfo.xFirstControlWithInputRequired.is() && !rColInfo.xFirstGridWithInputRequiredColumn.is() )
{ continue;
}
// TODO: in case of binary fields, this "getString" below is extremely expensive if ( !rColInfo.xColumn->getString().isEmpty() || !rColInfo.xColumn->wasNull() ) continue;
// the control to focus
Reference< XControl > xControl( rColInfo.xFirstControlWithInputRequired ); if ( !xControl.is() )
xControl.set( rColInfo.xFirstGridWithInputRequiredColumn, UNO_QUERY );
::comphelper::OInterfaceIteratorHelper3 aIter(m_aDeleteListeners); if (aIter.hasMoreElements())
{
RowChangeEvent aEvt(aEvent);
aEvt.Source = *this; return aIter.next()->confirmDelete(aEvt);
} // default handling: instantiate an interaction handler and let it handle the request
try
{ if ( !ensureInteractionHandler() ) returnfalse;
// two continuations allowed: Yes and No
rtl::Reference<OInteractionApprove> pApprove = new OInteractionApprove;
rtl::Reference<OInteractionDisapprove> pDisapprove = new OInteractionDisapprove;
void SAL_CALL FormController::invalidateFeatures( const Sequence< ::sal_Int16 >& Features )
{
::osl::MutexGuard aGuard( m_aMutex ); // for now, just copy the ids of the features, because...
m_aInvalidFeatures.insert( Features.begin(), Features.end() );
// ... we will do the real invalidation asynchronously if ( !m_aFeatureInvalidationTimer.IsActive() )
m_aFeatureInvalidationTimer.Start();
}
// dispatches of FormSlot-URLs we have to translate if ( !xReturn.is() && m_xFormOperations.is() )
{ // find the slot id which corresponds to the URL
sal_Int32 nFeatureSlotId = svx::FeatureSlotTranslation::getControllerFeatureSlotIdForURL( aURL.Main );
sal_Int16 nFormFeature = ( nFeatureSlotId != -1 ) ? svx::FeatureSlotTranslation::getFormFeatureForSlotId( nFeatureSlotId ) : -1; if ( nFormFeature > 0 )
{ // get the dispatcher for this feature, create if necessary
DispatcherContainer::const_iterator aDispatcherPos = m_aFeatureDispatchers.find( nFormFeature ); if ( aDispatcherPos == m_aFeatureDispatchers.end() )
{
aDispatcherPos = m_aFeatureDispatchers.emplace(
nFormFeature, new svx::OSingleFeatureDispatcher( aURL, nFormFeature, m_xFormOperations, m_aMutex )
).first;
}
OSL_ENSURE( aDispatcherPos->second.is(), "FormController::interceptedQueryDispatch: should have a dispatcher by now!" ); return aDispatcherPos->second;
}
}
// no more to offer return xReturn;
}
void SAL_CALL FormController::dispatch( const URL& _rURL, const Sequence< PropertyValue >& _rArgs )
{ if ( _rArgs.getLength() != 1 )
{
OSL_FAIL( "FormController::dispatch: no arguments -> no dispatch!" ); return;
}
if ( _rURL.Complete == FMURL_CONFIRM_DELETION )
{
OSL_FAIL( "FormController::dispatch: How do you expect me to return something via this call?" ); // confirmDelete has a return value - dispatch hasn't return;
}
void SAL_CALL FormController::removeStatusListener( const Reference< XStatusListener >& /*_rxListener*/, const URL& _rURL )
{
OSL_ENSURE(_rURL.Complete == FMURL_CONFIRM_DELETION, "FormController::removeStatusListener: invalid (unsupported) URL!"); // we never really added the listener, so we don't need to remove it
}
Reference< XDispatchProviderInterceptor > FormController::createInterceptor(const Reference< XDispatchProviderInterception > & _xInterception)
{
OSL_ENSURE( !impl_isDisposed_nofail(), "FormController: already disposed!" ); #ifdef DBG_UTIL // check if we already have an interceptor for the given object for ( constauto & it : m_aControlDispatchInterceptors )
{ if (it->getIntercepted() == _xInterception)
OSL_FAIL("FormController::createInterceptor : we already do intercept this objects dispatches !");
} #endif
rtl::Reference<DispatchInterceptionMultiplexer> pInterceptor(new DispatchInterceptionMultiplexer( _xInterception, this ));
m_aControlDispatchInterceptors.push_back( pInterceptor );
return pInterceptor;
}
bool FormController::ensureInteractionHandler()
{ if ( m_xInteractionHandler.is() ) returntrue; if ( m_bAttemptedHandlerCreation ) returnfalse;
m_bAttemptedHandlerCreation = true;
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.