8393267c65
Change-Id: I1033c33a6b4bf06e365ce183b1a73c98bea777ab Reviewed-on: https://gerrit.libreoffice.org/c/core/+/176442 Tested-by: Jenkins Reviewed-by: Noel Grandin <noel.grandin@collabora.co.uk>
1185 lines
41 KiB
C++
1185 lines
41 KiB
C++
/* -*- 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 .
|
|
*/
|
|
|
|
#include <sal/config.h>
|
|
|
|
#include <sal/log.hxx>
|
|
#include <unotools/configitem.hxx>
|
|
#include <unotools/configmgr.hxx>
|
|
#include <unotools/configpaths.hxx>
|
|
#include <com/sun/star/beans/XPropertySet.hpp>
|
|
#include <com/sun/star/util/XChangesListener.hpp>
|
|
#include <com/sun/star/util/XChangesNotifier.hpp>
|
|
#include <com/sun/star/container/XHierarchicalNameAccess.hpp>
|
|
#include <com/sun/star/configuration/XTemplateContainer.hpp>
|
|
#include <com/sun/star/container/XNameContainer.hpp>
|
|
#include <com/sun/star/lang/XSingleServiceFactory.hpp>
|
|
#include <com/sun/star/lang/XServiceInfo.hpp>
|
|
#include <com/sun/star/beans/PropertyValue.hpp>
|
|
#include <com/sun/star/beans/PropertyAttribute.hpp>
|
|
#include <com/sun/star/util/XChangesBatch.hpp>
|
|
#include <o3tl/deleter.hxx>
|
|
#include <osl/diagnose.h>
|
|
#include <comphelper/configuration.hxx>
|
|
#include <comphelper/sequence.hxx>
|
|
#include <comphelper/solarmutex.hxx>
|
|
#include <comphelper/diagnose_ex.hxx>
|
|
#include <comphelper/propertyvalue.hxx>
|
|
#include <cppuhelper/implbase.hxx>
|
|
#include <utility>
|
|
|
|
using namespace utl;
|
|
using namespace com::sun::star::uno;
|
|
using namespace com::sun::star::util;
|
|
using namespace com::sun::star::lang;
|
|
using namespace com::sun::star::beans;
|
|
using namespace com::sun::star::container;
|
|
using namespace com::sun::star::configuration;
|
|
|
|
/*
|
|
The ConfigChangeListener_Impl receives notifications from the configuration about changes that
|
|
have happened. It forwards this notification to the ConfigItem it knows a pParent by calling its
|
|
"CallNotify" method. As ConfigItems are most probably not thread safe, the SolarMutex is acquired
|
|
before doing so.
|
|
*/
|
|
|
|
namespace utl{
|
|
class ConfigChangeListener_Impl : public cppu::WeakImplHelper
|
|
<
|
|
css::util::XChangesListener
|
|
>
|
|
{
|
|
public:
|
|
ConfigItem* pParent;
|
|
const Sequence< OUString > aPropertyNames;
|
|
ConfigChangeListener_Impl(ConfigItem& rItem, const Sequence< OUString >& rNames);
|
|
|
|
//XChangesListener
|
|
virtual void SAL_CALL changesOccurred( const ChangesEvent& Event ) override;
|
|
|
|
//XEventListener
|
|
virtual void SAL_CALL disposing( const EventObject& Source ) override;
|
|
};
|
|
}
|
|
|
|
namespace {
|
|
|
|
class ValueCounter_Impl
|
|
{
|
|
sal_Int16& rCnt;
|
|
public:
|
|
explicit ValueCounter_Impl(sal_Int16& rCounter)
|
|
: rCnt(rCounter)
|
|
{
|
|
rCnt++;
|
|
}
|
|
~ValueCounter_Impl()
|
|
{
|
|
OSL_ENSURE(rCnt>0, "RefCount < 0 ??");
|
|
rCnt--;
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
ConfigChangeListener_Impl::ConfigChangeListener_Impl(
|
|
ConfigItem& rItem, const Sequence< OUString >& rNames) :
|
|
pParent(&rItem),
|
|
aPropertyNames(rNames)
|
|
{
|
|
}
|
|
|
|
void ConfigChangeListener_Impl::changesOccurred( const ChangesEvent& rEvent )
|
|
{
|
|
Sequence<OUString> aChangedNames(rEvent.Changes.getLength());
|
|
OUString* pNames = aChangedNames.getArray();
|
|
|
|
sal_Int32 nNotify = 0;
|
|
for(const auto& rElementChange : rEvent.Changes)
|
|
{
|
|
OUString sTemp;
|
|
rElementChange.Accessor >>= sTemp;
|
|
//true if the path is completely correct or if it is longer
|
|
//i.e ...Print/Content/Graphic and .../Print
|
|
bool bFound = std::any_of(aPropertyNames.begin(), aPropertyNames.end(),
|
|
[&sTemp](const OUString& rCheckPropertyName) { return isPrefixOfConfigurationPath(sTemp, rCheckPropertyName); });
|
|
if(bFound)
|
|
pNames[nNotify++] = sTemp;
|
|
}
|
|
if( nNotify )
|
|
{
|
|
::comphelper::SolarMutex *pMutex = ::comphelper::SolarMutex::get();
|
|
if ( pMutex )
|
|
{
|
|
osl::Guard<comphelper::SolarMutex> aMutexGuard( pMutex );
|
|
aChangedNames.realloc(nNotify);
|
|
pParent->CallNotify(aChangedNames);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ConfigChangeListener_Impl::disposing( const EventObject& /*rSource*/ )
|
|
{
|
|
pParent->RemoveChangesListener();
|
|
}
|
|
|
|
ConfigItem::ConfigItem(OUString aSubTree, ConfigItemMode nSetMode ) :
|
|
sSubTree(std::move(aSubTree)),
|
|
m_nMode(nSetMode),
|
|
m_bIsModified(false),
|
|
m_bEnableInternalNotification(false),
|
|
m_nInValueChange(0)
|
|
{
|
|
if (comphelper::IsFuzzing())
|
|
return;
|
|
|
|
if (nSetMode & ConfigItemMode::ReleaseTree)
|
|
ConfigManager::getConfigManager().addConfigItem(*this);
|
|
else
|
|
m_xHierarchyAccess = ConfigManager::getConfigManager().addConfigItem(*this);
|
|
}
|
|
|
|
ConfigItem::ConfigItem(ConfigItem const &) = default;
|
|
ConfigItem::ConfigItem(ConfigItem &&) = default;
|
|
|
|
ConfigItem::~ConfigItem()
|
|
{
|
|
suppress_fun_call_w_exception(RemoveChangesListener());
|
|
ConfigManager::getConfigManager().removeConfigItem(*this);
|
|
}
|
|
|
|
void ConfigItem::CallNotify( const css::uno::Sequence<OUString>& rPropertyNames )
|
|
{
|
|
// the call is forwarded to the virtual Notify() method
|
|
// it is pure virtual, so all classes deriving from ConfigItem have to decide how they
|
|
// want to notify listeners
|
|
if(m_nInValueChange <= 0 || m_bEnableInternalNotification)
|
|
Notify(rPropertyNames);
|
|
}
|
|
|
|
// In special mode ALL_LOCALES we must support reading/writing of localized cfg entries as Sequence< PropertyValue >.
|
|
// These methods are helper to convert given lists of names and Any-values.
|
|
// format: PropertyValue.Name = <locale as ISO string>
|
|
// PropertyValue.Value = <value; type depends from cfg entry!>
|
|
// e.g.
|
|
// LOCALIZED NODE
|
|
// "UIName"
|
|
// LOCALE VALUE
|
|
// "de" "Mein Name"
|
|
// "en-US" "my name"
|
|
|
|
static void impl_packLocalizedProperties( const Sequence< OUString >& lInNames ,
|
|
const Sequence< Any >& lInValues ,
|
|
Sequence< Any >& lOutValues )
|
|
{
|
|
// This method should be called for special AllLocales ConfigItem-mode only!
|
|
|
|
// Optimise follow algorithm ... A LITTLE BIT :-)
|
|
// There exist two different possibilities:
|
|
// i ) There exist no localized entries ... => size of lOutValues will be the same like lInNames/lInValues!
|
|
// ii) There exist some (mostly one or two) localized entries ... => size of lOutValues will be the same like lInNames/lInValues!
|
|
// ... Why? If a localized value exist - the any is filled with an XInterface object (is a SetNode-service).
|
|
// We read all his child nodes and pack it into Sequence< PropertyValue >.
|
|
// The result list we pack into the return any. We never change size of lists!
|
|
lOutValues.realloc(lInNames.getLength());
|
|
|
|
// Algorithm:
|
|
// Copy all names and values from in to out lists.
|
|
// Look for special localized entries ... You can detect it as "XInterface" packed into an Any.
|
|
// Use this XInterface-object to read all localized values and pack it into Sequence< PropertyValue >.
|
|
// Add this list to out lists then.
|
|
|
|
std::transform(lInValues.begin(), lInValues.end(), lOutValues.getArray(), [](const Any& value)
|
|
{
|
|
// If item is a special localized one ... convert and pack it ...
|
|
if (value.getValueTypeName() == "com.sun.star.uno.XInterface")
|
|
{
|
|
if (auto xSetAccess = value.query<XNameContainer>())
|
|
{
|
|
// list of all locales for localized entry
|
|
Sequence<OUString> locales = xSetAccess->getElementNames();
|
|
// localized values of a configuration entry packed for return
|
|
Sequence<PropertyValue> lProperties(locales.getLength());
|
|
|
|
std::transform(
|
|
locales.begin(), locales.end(), lProperties.getArray(),
|
|
[&xSetAccess](const OUString& s)
|
|
{ return comphelper::makePropertyValue(s, xSetAccess->getByName(s)); });
|
|
|
|
return Any(lProperties);
|
|
}
|
|
}
|
|
// ... or copy normal items to return lists directly.
|
|
return value;
|
|
});
|
|
}
|
|
|
|
static void impl_unpackLocalizedProperties( const Sequence< OUString >& lInNames ,
|
|
const Sequence< Any >& lInValues ,
|
|
Sequence< OUString >& lOutNames ,
|
|
Sequence< Any >& lOutValues)
|
|
{
|
|
// This method should be called for special AllLocales ConfigItem-mode only!
|
|
|
|
sal_Int32 nSourceSize; // marks end of loop over input lists
|
|
sal_Int32 nDestinationCounter; // actual position in output lists
|
|
sal_Int32 nPropertiesSize; // marks end of inner loop
|
|
OUString sNodeName; // base name of node ( e.g. "UIName/" ) ... expand to locale ( e.g. "UIName/de" )
|
|
Sequence< PropertyValue > lProperties; // localized values of a configuration entry gotten from lInValues-Any
|
|
|
|
// Optimise follow algorithm ... A LITTLE BIT :-)
|
|
// There exist two different possibilities:
|
|
// i ) There exist no localized entries ... => size of lOutNames/lOutValues will be the same like lInNames/lInValues!
|
|
// ii) There exist some (mostly one or two) localized entries ... => size of lOutNames/lOutValues will be some bytes greater than lInNames/lInValues.
|
|
// => I think we should make it fast for i). ii) is a special case and mustn't be SOOOO... fast.
|
|
// We should reserve same space for output list like input ones first.
|
|
// Follow algorithm looks for these borders and change it for ii) only!
|
|
// It will be faster then a "realloc()" call in every loop ...
|
|
nSourceSize = lInNames.getLength();
|
|
|
|
lOutNames.realloc ( nSourceSize );
|
|
auto plOutNames = lOutNames.getArray();
|
|
lOutValues.realloc ( nSourceSize );
|
|
auto plOutValues = lOutValues.getArray();
|
|
|
|
// Algorithm:
|
|
// Copy all names and values from const to return lists.
|
|
// Look for special localized entries ... You can detect it as Sequence< PropertyValue > packed into an Any.
|
|
// Split it ... insert PropertyValue.Name to lOutNames and PropertyValue.Value to lOutValues.
|
|
|
|
nDestinationCounter = 0;
|
|
for( sal_Int32 nSourceCounter=0; nSourceCounter<nSourceSize; ++nSourceCounter )
|
|
{
|
|
// If item a special localized one ... split it and insert his parts to output lists ...
|
|
if( lInValues[nSourceCounter].getValueType() == cppu::UnoType<Sequence<PropertyValue>>::get() )
|
|
{
|
|
lInValues[nSourceCounter] >>= lProperties;
|
|
nPropertiesSize = lProperties.getLength();
|
|
|
|
sNodeName = lInNames[nSourceCounter] + "/";
|
|
|
|
if( (nDestinationCounter+nPropertiesSize) > lOutNames.getLength() )
|
|
{
|
|
lOutNames.realloc ( nDestinationCounter+nPropertiesSize );
|
|
plOutNames = lOutNames.getArray();
|
|
lOutValues.realloc ( nDestinationCounter+nPropertiesSize );
|
|
plOutValues = lOutValues.getArray();
|
|
}
|
|
|
|
for (const auto& rProperty : lProperties)
|
|
{
|
|
plOutNames [nDestinationCounter] = sNodeName + rProperty.Name;
|
|
plOutValues[nDestinationCounter] = rProperty.Value;
|
|
++nDestinationCounter;
|
|
}
|
|
}
|
|
// ... or copy normal items to return lists directly.
|
|
else
|
|
{
|
|
if( (nDestinationCounter+1) > lOutNames.getLength() )
|
|
{
|
|
lOutNames.realloc ( nDestinationCounter+1 );
|
|
plOutNames = lOutNames.getArray();
|
|
lOutValues.realloc ( nDestinationCounter+1 );
|
|
plOutValues = lOutValues.getArray();
|
|
}
|
|
|
|
plOutNames [nDestinationCounter] = lInNames [nSourceCounter];
|
|
plOutValues[nDestinationCounter] = lInValues[nSourceCounter];
|
|
++nDestinationCounter;
|
|
}
|
|
}
|
|
}
|
|
|
|
Sequence< sal_Bool > ConfigItem::GetReadOnlyStates(const css::uno::Sequence< OUString >& rNames)
|
|
{
|
|
sal_Int32 i;
|
|
|
|
// size of return list is fix!
|
|
// Every item must match to length of incoming name list.
|
|
sal_Int32 nCount = rNames.getLength();
|
|
Sequence< sal_Bool > lStates(nCount);
|
|
sal_Bool* plStates = lStates.getArray();
|
|
|
|
// We must be sure to return a valid information every time!
|
|
// Set default to non readonly... similar to the configuration handling of this property.
|
|
std::fill_n(plStates, lStates.getLength(), false);
|
|
|
|
// no access - no information...
|
|
Reference< XHierarchicalNameAccess > xHierarchyAccess = GetTree();
|
|
if (!xHierarchyAccess.is())
|
|
return lStates;
|
|
|
|
for (i=0; i<nCount; ++i)
|
|
{
|
|
try
|
|
{
|
|
const OUString& sName = rNames[i];
|
|
OUString sPath;
|
|
OUString sProperty;
|
|
|
|
(void)::utl::splitLastFromConfigurationPath(sName,sPath,sProperty);
|
|
if (sPath.isEmpty() && sProperty.isEmpty())
|
|
{
|
|
OSL_FAIL("ConfigItem::IsReadonly() split failed");
|
|
continue;
|
|
}
|
|
|
|
Reference< XInterface > xNode;
|
|
Reference< XPropertySet > xSet;
|
|
Reference< XPropertySetInfo > xInfo;
|
|
if (!sPath.isEmpty())
|
|
{
|
|
Any aNode = xHierarchyAccess->getByHierarchicalName(sPath);
|
|
if (!(aNode >>= xNode) || !xNode.is())
|
|
{
|
|
OSL_FAIL("ConfigItem::IsReadonly() no set available");
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
xNode = xHierarchyAccess;
|
|
}
|
|
|
|
xSet.set(xNode, UNO_QUERY);
|
|
if (xSet.is())
|
|
{
|
|
xInfo = xSet->getPropertySetInfo();
|
|
OSL_ENSURE(xInfo.is(), "ConfigItem::IsReadonly() getPropertySetInfo failed ...");
|
|
}
|
|
else
|
|
{
|
|
xInfo.set(xNode, UNO_QUERY);
|
|
OSL_ENSURE(xInfo.is(), "ConfigItem::IsReadonly() UNO_QUERY failed ...");
|
|
}
|
|
|
|
if (!xInfo.is())
|
|
{
|
|
OSL_FAIL("ConfigItem::IsReadonly() no prop info available");
|
|
continue;
|
|
}
|
|
|
|
Property aProp = xInfo->getPropertyByName(sProperty);
|
|
plStates[i] = (aProp.Attributes & PropertyAttribute::READONLY) == PropertyAttribute::READONLY;
|
|
}
|
|
catch (const Exception&)
|
|
{
|
|
}
|
|
}
|
|
|
|
return lStates;
|
|
}
|
|
|
|
Sequence< Any > ConfigItem::GetProperties(const Sequence< OUString >& rNames)
|
|
{
|
|
Reference<XHierarchicalNameAccess> xHierarchyAccess = GetTree();
|
|
if(xHierarchyAccess.is())
|
|
return GetProperties(xHierarchyAccess, rNames,
|
|
(m_nMode & ConfigItemMode::AllLocales ) == ConfigItemMode::AllLocales);
|
|
return Sequence< Any >(rNames.getLength());
|
|
}
|
|
|
|
Sequence< Any > ConfigItem::GetProperties(
|
|
css::uno::Reference<css::container::XHierarchicalNameAccess> const & xHierarchyAccess,
|
|
const Sequence< OUString >& rNames,
|
|
bool bAllLocales)
|
|
{
|
|
Sequence< Any > aRet(rNames.getLength());
|
|
const OUString* pNames = rNames.getConstArray();
|
|
Any* pRet = aRet.getArray();
|
|
for(int i = 0; i < rNames.getLength(); i++)
|
|
{
|
|
try
|
|
{
|
|
pRet[i] = xHierarchyAccess->getByHierarchicalName(pNames[i]);
|
|
}
|
|
catch (const Exception&)
|
|
{
|
|
TOOLS_WARN_EXCEPTION(
|
|
"unotools.config",
|
|
"ignoring XHierarchicalNameAccess " << pNames[i]);
|
|
}
|
|
}
|
|
|
|
// In special mode "ALL_LOCALES" we must convert localized values to Sequence< PropertyValue >.
|
|
if(bAllLocales)
|
|
{
|
|
Sequence< Any > lValues;
|
|
impl_packLocalizedProperties( rNames, aRet, lValues );
|
|
aRet = std::move(lValues);
|
|
}
|
|
return aRet;
|
|
}
|
|
|
|
bool ConfigItem::PutProperties( const Sequence< OUString >& rNames,
|
|
const Sequence< Any>& rValues)
|
|
{
|
|
ValueCounter_Impl aCounter(m_nInValueChange);
|
|
Reference<XHierarchicalNameAccess> xHierarchyAccess = GetTree();
|
|
Reference<XNameReplace> xTopNodeReplace(xHierarchyAccess, UNO_QUERY);
|
|
bool bRet = xHierarchyAccess.is() && xTopNodeReplace.is();
|
|
if(bRet)
|
|
{
|
|
Sequence< OUString > lNames;
|
|
Sequence< Any > lValues;
|
|
const OUString* pNames = nullptr;
|
|
const Any* pValues = nullptr;
|
|
sal_Int32 nNameCount;
|
|
if(( m_nMode & ConfigItemMode::AllLocales ) == ConfigItemMode::AllLocales )
|
|
{
|
|
// If ConfigItem works in "ALL_LOCALES"-mode ... we must support a Sequence< PropertyValue >
|
|
// as value of a localized configuration entry!
|
|
// How we can do that?
|
|
// We must split all PropertyValues to "Sequence< OUString >" AND "Sequence< Any >"!
|
|
impl_unpackLocalizedProperties( rNames, rValues, lNames, lValues );
|
|
pNames = lNames.getConstArray ();
|
|
pValues = lValues.getConstArray ();
|
|
nNameCount = lNames.getLength ();
|
|
}
|
|
else
|
|
{
|
|
// This is the normal mode ...
|
|
// Use given input lists directly.
|
|
pNames = rNames.getConstArray ();
|
|
pValues = rValues.getConstArray ();
|
|
nNameCount = rNames.getLength ();
|
|
}
|
|
for(int i = 0; i < nNameCount; i++)
|
|
{
|
|
try
|
|
{
|
|
OUString sNode, sProperty;
|
|
if (splitLastFromConfigurationPath(pNames[i],sNode, sProperty))
|
|
{
|
|
Any aNode = xHierarchyAccess->getByHierarchicalName(sNode);
|
|
|
|
Reference<XNameAccess> xNodeAcc;
|
|
aNode >>= xNodeAcc;
|
|
Reference<XNameReplace> xNodeReplace(xNodeAcc, UNO_QUERY);
|
|
Reference<XNameContainer> xNodeCont (xNodeAcc, UNO_QUERY);
|
|
|
|
bool bExist = (xNodeAcc.is() && xNodeAcc->hasByName(sProperty));
|
|
if (bExist && xNodeReplace.is())
|
|
xNodeReplace->replaceByName(sProperty, pValues[i]);
|
|
else
|
|
if (!bExist && xNodeCont.is())
|
|
xNodeCont->insertByName(sProperty, pValues[i]);
|
|
else
|
|
bRet = false;
|
|
}
|
|
else //direct value
|
|
{
|
|
xTopNodeReplace->replaceByName(sProperty, pValues[i]);
|
|
}
|
|
}
|
|
catch (css::uno::Exception &)
|
|
{
|
|
TOOLS_WARN_EXCEPTION("unotools.config", "Exception from PutProperties");
|
|
}
|
|
}
|
|
try
|
|
{
|
|
Reference<XChangesBatch> xBatch(xHierarchyAccess, UNO_QUERY);
|
|
xBatch->commitChanges();
|
|
}
|
|
catch (css::uno::Exception &)
|
|
{
|
|
TOOLS_WARN_EXCEPTION("unotools.config", "Exception from commitChanges");
|
|
}
|
|
}
|
|
|
|
return bRet;
|
|
}
|
|
|
|
bool ConfigItem::PutProperties(
|
|
css::uno::Reference<css::container::XHierarchicalNameAccess> const & xHierarchyAccess,
|
|
const Sequence< OUString >& rNames,
|
|
const Sequence< Any>& rValues,
|
|
bool bAllLocales)
|
|
{
|
|
Reference<XNameReplace> xTopNodeReplace(xHierarchyAccess, UNO_QUERY);
|
|
bool bRet = xTopNodeReplace.is();
|
|
if(bRet)
|
|
{
|
|
Sequence< OUString > lNames;
|
|
Sequence< Any > lValues;
|
|
const OUString* pNames = nullptr;
|
|
const Any* pValues = nullptr;
|
|
sal_Int32 nNameCount;
|
|
if(bAllLocales)
|
|
{
|
|
// If ConfigItem works in "ALL_LOCALES"-mode ... we must support a Sequence< PropertyValue >
|
|
// as value of a localized configuration entry!
|
|
// How we can do that?
|
|
// We must split all PropertyValues to "Sequence< OUString >" AND "Sequence< Any >"!
|
|
impl_unpackLocalizedProperties( rNames, rValues, lNames, lValues );
|
|
pNames = lNames.getConstArray ();
|
|
pValues = lValues.getConstArray ();
|
|
nNameCount = lNames.getLength ();
|
|
}
|
|
else
|
|
{
|
|
// This is the normal mode ...
|
|
// Use given input lists directly.
|
|
pNames = rNames.getConstArray ();
|
|
pValues = rValues.getConstArray ();
|
|
nNameCount = rNames.getLength ();
|
|
}
|
|
for(int i = 0; i < nNameCount; i++)
|
|
{
|
|
try
|
|
{
|
|
OUString sNode, sProperty;
|
|
if (splitLastFromConfigurationPath(pNames[i],sNode, sProperty))
|
|
{
|
|
Any aNode = xHierarchyAccess->getByHierarchicalName(sNode);
|
|
|
|
Reference<XNameAccess> xNodeAcc;
|
|
aNode >>= xNodeAcc;
|
|
Reference<XNameReplace> xNodeReplace(xNodeAcc, UNO_QUERY);
|
|
Reference<XNameContainer> xNodeCont (xNodeAcc, UNO_QUERY);
|
|
|
|
bool bExist = (xNodeAcc.is() && xNodeAcc->hasByName(sProperty));
|
|
if (bExist && xNodeReplace.is())
|
|
xNodeReplace->replaceByName(sProperty, pValues[i]);
|
|
else
|
|
if (!bExist && xNodeCont.is())
|
|
xNodeCont->insertByName(sProperty, pValues[i]);
|
|
else
|
|
bRet = false;
|
|
}
|
|
else //direct value
|
|
{
|
|
xTopNodeReplace->replaceByName(sProperty, pValues[i]);
|
|
}
|
|
}
|
|
catch (css::uno::Exception &)
|
|
{
|
|
TOOLS_WARN_EXCEPTION("unotools.config", "Exception from PutProperties");
|
|
}
|
|
}
|
|
try
|
|
{
|
|
Reference<XChangesBatch> xBatch(xHierarchyAccess, UNO_QUERY);
|
|
xBatch->commitChanges();
|
|
}
|
|
catch (css::uno::Exception &)
|
|
{
|
|
TOOLS_WARN_EXCEPTION("unotools.config", "Exception from commitChanges");
|
|
}
|
|
}
|
|
|
|
return bRet;
|
|
}
|
|
|
|
void ConfigItem::DisableNotification()
|
|
{
|
|
OSL_ENSURE( xChangeLstnr.is(), "ConfigItem::DisableNotification: notifications not enabled currently!" );
|
|
RemoveChangesListener();
|
|
}
|
|
|
|
bool ConfigItem::EnableNotification(const Sequence< OUString >& rNames,
|
|
bool bEnableInternalNotification )
|
|
{
|
|
OSL_ENSURE(!(m_nMode & ConfigItemMode::ReleaseTree), "notification in ConfigItemMode::ReleaseTree mode not possible");
|
|
m_bEnableInternalNotification = bEnableInternalNotification;
|
|
Reference<XHierarchicalNameAccess> xHierarchyAccess = GetTree();
|
|
Reference<XChangesNotifier> xChgNot(xHierarchyAccess, UNO_QUERY);
|
|
if(!xChgNot.is())
|
|
return false;
|
|
|
|
OSL_ENSURE(!xChangeLstnr.is(), "EnableNotification already called");
|
|
if(xChangeLstnr.is())
|
|
xChgNot->removeChangesListener( xChangeLstnr );
|
|
bool bRet = true;
|
|
|
|
try
|
|
{
|
|
xChangeLstnr = new ConfigChangeListener_Impl(*this, rNames);
|
|
xChgNot->addChangesListener( xChangeLstnr );
|
|
}
|
|
catch (const RuntimeException&)
|
|
{
|
|
bRet = false;
|
|
}
|
|
return bRet;
|
|
}
|
|
|
|
void ConfigItem::RemoveChangesListener()
|
|
{
|
|
Reference<XHierarchicalNameAccess> xHierarchyAccess = GetTree();
|
|
if(!xHierarchyAccess.is())
|
|
return;
|
|
|
|
Reference<XChangesNotifier> xChgNot(xHierarchyAccess, UNO_QUERY);
|
|
if(xChgNot.is() && xChangeLstnr.is())
|
|
{
|
|
try
|
|
{
|
|
xChgNot->removeChangesListener( xChangeLstnr );
|
|
xChangeLstnr = nullptr;
|
|
}
|
|
catch (const Exception&)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
static void lcl_normalizeLocalNames(Sequence< OUString >& _rNames, ConfigNameFormat _eFormat, Reference<XInterface> const& _xParentNode)
|
|
{
|
|
switch (_eFormat)
|
|
{
|
|
case ConfigNameFormat::LocalNode:
|
|
// unaltered - this is our input format
|
|
break;
|
|
|
|
case ConfigNameFormat::LocalPath:
|
|
{
|
|
Reference<XTemplateContainer> xTypeContainer(_xParentNode, UNO_QUERY);
|
|
if (xTypeContainer.is())
|
|
{
|
|
OUString sTypeName = xTypeContainer->getElementTemplateName();
|
|
sTypeName = sTypeName.copy(sTypeName.lastIndexOf('/')+1);
|
|
|
|
std::transform(std::cbegin(_rNames), std::cend(_rNames), _rNames.getArray(),
|
|
[&sTypeName](const OUString& rName) -> OUString { return wrapConfigurationElementName(rName,sTypeName); });
|
|
}
|
|
else
|
|
{
|
|
Reference<XServiceInfo> xSVI(_xParentNode, UNO_QUERY);
|
|
if (xSVI.is() && xSVI->supportsService(u"com.sun.star.configuration.SetAccess"_ustr))
|
|
{
|
|
std::transform(std::cbegin(_rNames), std::cend(_rNames), _rNames.getArray(),
|
|
[](const OUString& rName) -> OUString { return wrapConfigurationElementName(rName); });
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
}
|
|
}
|
|
|
|
Sequence< OUString > ConfigItem::GetNodeNames(const OUString& rNode)
|
|
{
|
|
ConfigNameFormat const eDefaultFormat = ConfigNameFormat::LocalNode; // CONFIG_NAME_DEFAULT;
|
|
|
|
return GetNodeNames(rNode, eDefaultFormat);
|
|
}
|
|
|
|
Sequence< OUString > ConfigItem::GetNodeNames(const OUString& rNode, ConfigNameFormat eFormat)
|
|
{
|
|
Reference<XHierarchicalNameAccess> xHierarchyAccess = GetTree();
|
|
if(xHierarchyAccess.is())
|
|
return GetNodeNames(xHierarchyAccess, rNode, eFormat);
|
|
return Sequence< OUString >();
|
|
}
|
|
|
|
Sequence< OUString > ConfigItem::GetNodeNames(
|
|
css::uno::Reference<css::container::XHierarchicalNameAccess> const & xHierarchyAccess,
|
|
const OUString& rNode,
|
|
ConfigNameFormat eFormat)
|
|
{
|
|
Sequence< OUString > aRet;
|
|
try
|
|
{
|
|
Reference<XNameAccess> xCont;
|
|
if(!rNode.isEmpty())
|
|
{
|
|
Any aNode = xHierarchyAccess->getByHierarchicalName(rNode);
|
|
aNode >>= xCont;
|
|
}
|
|
else
|
|
xCont.set(xHierarchyAccess, UNO_QUERY);
|
|
if(xCont.is())
|
|
{
|
|
aRet = xCont->getElementNames();
|
|
lcl_normalizeLocalNames(aRet,eFormat,xCont);
|
|
}
|
|
|
|
}
|
|
catch (css::uno::Exception &)
|
|
{
|
|
TOOLS_WARN_EXCEPTION("unotools.config", "Exception from GetNodeNames");
|
|
}
|
|
return aRet;
|
|
}
|
|
|
|
bool ConfigItem::ClearNodeSet(const OUString& rNode)
|
|
{
|
|
ValueCounter_Impl aCounter(m_nInValueChange);
|
|
bool bRet = false;
|
|
Reference<XHierarchicalNameAccess> xHierarchyAccess = GetTree();
|
|
if(xHierarchyAccess.is())
|
|
bRet = ClearNodeSet(xHierarchyAccess, rNode);
|
|
return bRet;
|
|
}
|
|
|
|
bool ConfigItem::ClearNodeSet(
|
|
css::uno::Reference<css::container::XHierarchicalNameAccess> const & xHierarchyAccess,
|
|
const OUString& rNode)
|
|
{
|
|
bool bRet = false;
|
|
try
|
|
{
|
|
Reference<XNameContainer> xCont;
|
|
if(!rNode.isEmpty())
|
|
{
|
|
Any aNode = xHierarchyAccess->getByHierarchicalName(rNode);
|
|
aNode >>= xCont;
|
|
}
|
|
else
|
|
xCont.set(xHierarchyAccess, UNO_QUERY);
|
|
if(!xCont.is())
|
|
return false;
|
|
const Sequence< OUString > aNames = xCont->getElementNames();
|
|
Reference<XChangesBatch> xBatch(xHierarchyAccess, UNO_QUERY);
|
|
for(const OUString& rName : aNames)
|
|
{
|
|
try
|
|
{
|
|
xCont->removeByName(rName);
|
|
}
|
|
catch (css::uno::Exception &)
|
|
{
|
|
TOOLS_WARN_EXCEPTION("unotools.config", "Exception from removeByName");
|
|
}
|
|
}
|
|
xBatch->commitChanges();
|
|
bRet = true;
|
|
}
|
|
catch (css::uno::Exception &)
|
|
{
|
|
TOOLS_WARN_EXCEPTION("unotools.config", "Exception from ClearNodeSet");
|
|
}
|
|
return bRet;
|
|
}
|
|
|
|
bool ConfigItem::ClearNodeElements(const OUString& rNode, Sequence< OUString > const & rElements)
|
|
{
|
|
ValueCounter_Impl aCounter(m_nInValueChange);
|
|
bool bRet = false;
|
|
Reference<XHierarchicalNameAccess> xHierarchyAccess = GetTree();
|
|
if(xHierarchyAccess.is())
|
|
{
|
|
try
|
|
{
|
|
Reference<XNameContainer> xCont;
|
|
if(!rNode.isEmpty())
|
|
{
|
|
Any aNode = xHierarchyAccess->getByHierarchicalName(rNode);
|
|
aNode >>= xCont;
|
|
}
|
|
else
|
|
xCont.set(xHierarchyAccess, UNO_QUERY);
|
|
if(!xCont.is())
|
|
return false;
|
|
try
|
|
{
|
|
for(const OUString& rElement : rElements)
|
|
{
|
|
xCont->removeByName(rElement);
|
|
}
|
|
Reference<XChangesBatch> xBatch(xHierarchyAccess, UNO_QUERY);
|
|
xBatch->commitChanges();
|
|
}
|
|
catch (css::uno::Exception &)
|
|
{
|
|
TOOLS_WARN_EXCEPTION("unotools.config", "Exception from commitChanges()");
|
|
}
|
|
bRet = true;
|
|
}
|
|
catch (css::uno::Exception &)
|
|
{
|
|
TOOLS_WARN_EXCEPTION("unotools.config", "Exception from GetNodeNames()");
|
|
}
|
|
}
|
|
return bRet;
|
|
}
|
|
|
|
static OUString lcl_extractSetPropertyName( const OUString& rInPath, std::u16string_view rPrefix )
|
|
{
|
|
OUString const sSubPath = dropPrefixFromConfigurationPath( rInPath, rPrefix);
|
|
return extractFirstFromConfigurationPath( sSubPath );
|
|
}
|
|
|
|
static
|
|
Sequence< OUString > lcl_extractSetPropertyNames( const Sequence< PropertyValue >& rValues, std::u16string_view rPrefix )
|
|
{
|
|
Sequence< OUString > aSubNodeNames(rValues.getLength());
|
|
OUString* pSubNodeNames = aSubNodeNames.getArray();
|
|
|
|
OUString sLastSubNode;
|
|
sal_Int32 nSubIndex = 0;
|
|
|
|
for(const PropertyValue& rProperty : rValues)
|
|
{
|
|
OUString const sSubPath = dropPrefixFromConfigurationPath( rProperty.Name, rPrefix);
|
|
OUString const sSubNode = extractFirstFromConfigurationPath( sSubPath );
|
|
|
|
if(sLastSubNode != sSubNode)
|
|
{
|
|
pSubNodeNames[nSubIndex++] = sSubNode;
|
|
}
|
|
|
|
sLastSubNode = sSubNode;
|
|
}
|
|
aSubNodeNames.realloc(nSubIndex);
|
|
|
|
return aSubNodeNames;
|
|
}
|
|
|
|
// Add or change properties
|
|
bool ConfigItem::SetSetProperties(
|
|
const OUString& rNode, const Sequence< PropertyValue >& rValues)
|
|
{
|
|
ValueCounter_Impl aCounter(m_nInValueChange);
|
|
Reference<XHierarchicalNameAccess> xHierarchyAccess = GetTree();
|
|
if(!xHierarchyAccess.is())
|
|
return true;
|
|
return SetSetProperties(xHierarchyAccess, rNode, rValues);
|
|
}
|
|
|
|
// Add or change properties
|
|
bool ConfigItem::SetSetProperties(
|
|
css::uno::Reference<css::container::XHierarchicalNameAccess> const & xHierarchyAccess,
|
|
const OUString& rNode, const Sequence< PropertyValue >& rValues)
|
|
{
|
|
bool bRet = true;
|
|
Reference<XChangesBatch> xBatch(xHierarchyAccess, UNO_QUERY);
|
|
try
|
|
{
|
|
Reference<XNameContainer> xCont;
|
|
if(!rNode.isEmpty())
|
|
{
|
|
Any aNode = xHierarchyAccess->getByHierarchicalName(rNode);
|
|
aNode >>= xCont;
|
|
}
|
|
else
|
|
xCont.set(xHierarchyAccess, UNO_QUERY);
|
|
if(!xCont.is())
|
|
return false;
|
|
|
|
Reference<XSingleServiceFactory> xFac(xCont, UNO_QUERY);
|
|
|
|
if(xFac.is())
|
|
{
|
|
const Sequence< OUString > aSubNodeNames = lcl_extractSetPropertyNames(rValues, rNode);
|
|
|
|
for(const auto& rSubNodeName : aSubNodeNames)
|
|
{
|
|
if(!xCont->hasByName(rSubNodeName))
|
|
{
|
|
Reference<XInterface> xInst = xFac->createInstance();
|
|
Any aVal; aVal <<= xInst;
|
|
xCont->insertByName(rSubNodeName, aVal);
|
|
}
|
|
//set values
|
|
}
|
|
try
|
|
{
|
|
xBatch->commitChanges();
|
|
}
|
|
catch (css::uno::Exception &)
|
|
{
|
|
TOOLS_WARN_EXCEPTION("unotools.config", "Exception from commitChanges()");
|
|
}
|
|
|
|
const PropertyValue* pProperties = rValues.getConstArray();
|
|
|
|
Sequence< OUString > aSetNames(rValues.getLength());
|
|
OUString* pSetNames = aSetNames.getArray();
|
|
|
|
Sequence< Any> aSetValues(rValues.getLength());
|
|
Any* pSetValues = aSetValues.getArray();
|
|
|
|
bool bEmptyNode = rNode.isEmpty();
|
|
for(sal_Int32 k = 0; k < rValues.getLength(); k++)
|
|
{
|
|
pSetNames[k] = pProperties[k].Name.copy( bEmptyNode ? 1 : 0);
|
|
pSetValues[k] = pProperties[k].Value;
|
|
}
|
|
bRet = PutProperties(xHierarchyAccess, aSetNames, aSetValues, /*bAllLocales*/false);
|
|
}
|
|
else
|
|
{
|
|
//if no factory is available then the node contains basic data elements
|
|
for(const PropertyValue& rValue : rValues)
|
|
{
|
|
try
|
|
{
|
|
OUString sSubNode = lcl_extractSetPropertyName( rValue.Name, rNode );
|
|
|
|
if(xCont->hasByName(sSubNode))
|
|
xCont->replaceByName(sSubNode, rValue.Value);
|
|
else
|
|
xCont->insertByName(sSubNode, rValue.Value);
|
|
|
|
OSL_ENSURE( xHierarchyAccess->hasByHierarchicalName(rValue.Name),
|
|
"Invalid config path" );
|
|
}
|
|
catch (css::uno::Exception &)
|
|
{
|
|
TOOLS_WARN_EXCEPTION("unotools.config", "Exception from insert/replaceByName()");
|
|
}
|
|
}
|
|
xBatch->commitChanges();
|
|
}
|
|
}
|
|
catch (const Exception&)
|
|
{
|
|
TOOLS_WARN_EXCEPTION("unotools.config", "Exception from SetSetProperties");
|
|
bRet = false;
|
|
}
|
|
return bRet;
|
|
}
|
|
|
|
bool ConfigItem::ReplaceSetProperties(
|
|
const OUString& rNode, const Sequence< PropertyValue >& rValues)
|
|
{
|
|
ValueCounter_Impl aCounter(m_nInValueChange);
|
|
bool bRet = true;
|
|
Reference<XHierarchicalNameAccess> xHierarchyAccess = GetTree();
|
|
if(xHierarchyAccess.is())
|
|
bRet = ReplaceSetProperties(xHierarchyAccess, rNode, rValues,
|
|
( m_nMode & ConfigItemMode::AllLocales ) == ConfigItemMode::AllLocales);
|
|
return bRet;
|
|
}
|
|
|
|
bool ConfigItem::ReplaceSetProperties(
|
|
css::uno::Reference<css::container::XHierarchicalNameAccess> const & xHierarchyAccess,
|
|
const OUString& rNode,
|
|
const Sequence< PropertyValue >& rValues,
|
|
bool bAllLocales)
|
|
{
|
|
bool bRet = true;
|
|
Reference<XChangesBatch> xBatch(xHierarchyAccess, UNO_QUERY);
|
|
try
|
|
{
|
|
Reference<XNameContainer> xCont;
|
|
if(!rNode.isEmpty())
|
|
{
|
|
Any aNode = xHierarchyAccess->getByHierarchicalName(rNode);
|
|
aNode >>= xCont;
|
|
}
|
|
else
|
|
xCont.set(xHierarchyAccess, UNO_QUERY);
|
|
if(!xCont.is())
|
|
return false;
|
|
|
|
// JB: Change: now the same name handling for sets of simple values
|
|
const Sequence< OUString > aSubNodeNames = lcl_extractSetPropertyNames(rValues, rNode);
|
|
|
|
Reference<XSingleServiceFactory> xFac(xCont, UNO_QUERY);
|
|
const bool isSimpleValueSet = !xFac.is();
|
|
|
|
//remove unknown members first
|
|
{
|
|
const Sequence<OUString> aContainerSubNodes = xCont->getElementNames();
|
|
|
|
for(const OUString& rContainerSubNode : aContainerSubNodes)
|
|
{
|
|
bool bFound = comphelper::findValue(aSubNodeNames, rContainerSubNode) != -1;
|
|
if(!bFound)
|
|
try
|
|
{
|
|
xCont->removeByName(rContainerSubNode);
|
|
}
|
|
catch (const Exception&)
|
|
{
|
|
if (isSimpleValueSet)
|
|
{
|
|
try
|
|
{
|
|
// #i37322#: fallback action: replace with <void/>
|
|
xCont->replaceByName(rContainerSubNode, Any());
|
|
// fallback successful: continue looping
|
|
continue;
|
|
}
|
|
catch (Exception &)
|
|
{} // propagate original exception, if fallback fails
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
try { xBatch->commitChanges(); }
|
|
catch (css::uno::Exception &)
|
|
{
|
|
TOOLS_WARN_EXCEPTION("unotools.config", "Exception from commitChanges");
|
|
}
|
|
}
|
|
|
|
if(xFac.is()) // !isSimpleValueSet
|
|
{
|
|
for(const OUString& rSubNodeName : aSubNodeNames)
|
|
{
|
|
if(!xCont->hasByName(rSubNodeName))
|
|
{
|
|
//create if not available
|
|
Reference<XInterface> xInst = xFac->createInstance();
|
|
Any aVal; aVal <<= xInst;
|
|
xCont->insertByName(rSubNodeName, aVal);
|
|
}
|
|
}
|
|
try { xBatch->commitChanges(); }
|
|
catch (css::uno::Exception &)
|
|
{
|
|
TOOLS_WARN_EXCEPTION("unotools.config", "Exception from commitChanges");
|
|
}
|
|
|
|
const PropertyValue* pProperties = rValues.getConstArray();
|
|
|
|
Sequence< OUString > aSetNames(rValues.getLength());
|
|
OUString* pSetNames = aSetNames.getArray();
|
|
|
|
Sequence< Any> aSetValues(rValues.getLength());
|
|
Any* pSetValues = aSetValues.getArray();
|
|
|
|
bool bEmptyNode = rNode.isEmpty();
|
|
for(sal_Int32 k = 0; k < rValues.getLength(); k++)
|
|
{
|
|
pSetNames[k] = pProperties[k].Name.copy( bEmptyNode ? 1 : 0);
|
|
pSetValues[k] = pProperties[k].Value;
|
|
}
|
|
bRet = PutProperties(xHierarchyAccess, aSetNames, aSetValues, bAllLocales);
|
|
}
|
|
else
|
|
{
|
|
//if no factory is available then the node contains basic data elements
|
|
for(const PropertyValue& rValue : rValues)
|
|
{
|
|
try
|
|
{
|
|
OUString sSubNode = lcl_extractSetPropertyName( rValue.Name, rNode );
|
|
|
|
if(xCont->hasByName(sSubNode))
|
|
xCont->replaceByName(sSubNode, rValue.Value);
|
|
else
|
|
xCont->insertByName(sSubNode, rValue.Value);
|
|
}
|
|
catch (css::uno::Exception &)
|
|
{
|
|
TOOLS_WARN_EXCEPTION("unotools.config", "Exception from insert/replaceByName");
|
|
}
|
|
}
|
|
xBatch->commitChanges();
|
|
}
|
|
}
|
|
catch (const Exception& )
|
|
{
|
|
TOOLS_WARN_EXCEPTION("unotools.config", "Exception from ReplaceSetProperties");
|
|
bRet = false;
|
|
}
|
|
return bRet;
|
|
}
|
|
|
|
bool ConfigItem::AddNode(const OUString& rNode, const OUString& rNewNode)
|
|
{
|
|
ValueCounter_Impl aCounter(m_nInValueChange);
|
|
bool bRet = true;
|
|
Reference<XHierarchicalNameAccess> xHierarchyAccess = GetTree();
|
|
if(xHierarchyAccess.is())
|
|
{
|
|
Reference<XChangesBatch> xBatch(xHierarchyAccess, UNO_QUERY);
|
|
try
|
|
{
|
|
Reference<XNameContainer> xCont;
|
|
if(!rNode.isEmpty())
|
|
{
|
|
Any aNode = xHierarchyAccess->getByHierarchicalName(rNode);
|
|
aNode >>= xCont;
|
|
}
|
|
else
|
|
xCont.set(xHierarchyAccess, UNO_QUERY);
|
|
if(!xCont.is())
|
|
return false;
|
|
|
|
Reference<XSingleServiceFactory> xFac(xCont, UNO_QUERY);
|
|
|
|
if(xFac.is())
|
|
{
|
|
if(!xCont->hasByName(rNewNode))
|
|
{
|
|
Reference<XInterface> xInst = xFac->createInstance();
|
|
Any aVal; aVal <<= xInst;
|
|
xCont->insertByName(rNewNode, aVal);
|
|
}
|
|
try
|
|
{
|
|
xBatch->commitChanges();
|
|
}
|
|
catch (css::uno::Exception &)
|
|
{
|
|
TOOLS_WARN_EXCEPTION("unotools.config", "Exception from commitChanges");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//if no factory is available then the node contains basic data elements
|
|
try
|
|
{
|
|
if(!xCont->hasByName(rNewNode))
|
|
xCont->insertByName(rNewNode, Any());
|
|
}
|
|
catch (css::uno::Exception &)
|
|
{
|
|
TOOLS_WARN_EXCEPTION("unotools.config", "Exception from AddNode");
|
|
}
|
|
}
|
|
xBatch->commitChanges();
|
|
}
|
|
catch (const Exception&)
|
|
{
|
|
DBG_UNHANDLED_EXCEPTION("unotools.config");
|
|
bRet = false;
|
|
}
|
|
}
|
|
return bRet;
|
|
}
|
|
|
|
|
|
void ConfigItem::SetModified()
|
|
{
|
|
m_bIsModified = true;
|
|
}
|
|
|
|
void ConfigItem::ClearModified()
|
|
{
|
|
m_bIsModified = false;
|
|
}
|
|
|
|
Reference< XHierarchicalNameAccess> ConfigItem::GetTree()
|
|
{
|
|
Reference< XHierarchicalNameAccess> xRet;
|
|
if (comphelper::IsFuzzing())
|
|
return xRet;
|
|
if(!m_xHierarchyAccess.is())
|
|
xRet = ConfigManager::acquireTree(*this);
|
|
else
|
|
xRet = m_xHierarchyAccess;
|
|
return xRet;
|
|
}
|
|
|
|
void ConfigItem::Commit()
|
|
{
|
|
ImplCommit();
|
|
ClearModified();
|
|
}
|
|
|
|
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|