Files
odfstudio/cui/source/customize/CommandCategoryListBox.cxx
Neil Roberts 54ba86b578 scripting: Replace XAggregation of XBrowseNode with helper function
The browse node factory had a wrapper using XAggregation that wraps
every XBrowseNode. As far as I can tell this was used to replace the
implementation of getChildNodes with one that checks for exceptions from
the underlying provider and then sorts all of the resulting nodes by
name. The trouble with this is that it prevents the script providers
from detecting when a node is one from the same script provider and thus
makes it difficult to implement the XCopyableBrowseNode interface. The
documentation for XAggregation mentions this problem and also states
that the interface is deprecated.

To avoid this overhead, this patch replaces the aggregation with a
helper function in comphelper that should be used to retrieve children
from a browse node. That takes care of catching exceptions and sorting
the results.

Change-Id: I319888fee1b84ddcef397317d2cf7e72bd2867cd
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/208160
Tested-by: Jenkins
Reviewed-by: Neil Roberts <nroberts@libreoffice.org>
2026-07-23 15:18:22 +02:00

580 lines
22 KiB
C++

/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
* 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 <CommandCategoryListBox.hxx>
#include <com/sun/star/uno/XInterface.hpp>
#include <com/sun/star/beans/XPropertySet.hpp>
#include <com/sun/star/frame/XDispatchInformationProvider.hpp>
#include <com/sun/star/frame/theUICommandDescription.hpp>
#include <com/sun/star/ui/theUICategoryDescription.hpp>
#include <com/sun/star/script/browse/XBrowseNode.hpp>
#include <com/sun/star/script/browse/BrowseNodeTypes.hpp>
#include <com/sun/star/script/browse/theBrowseNodeFactory.hpp>
#include <com/sun/star/script/browse/BrowseNodeFactoryViewTypes.hpp>
#include <vcl/commandinfoprovider.hxx>
#include <vcl/settings.hxx>
#include <vcl/svapp.hxx>
// include search util
#include <com/sun/star/util/SearchFlags.hpp>
#include <com/sun/star/util/SearchAlgorithms2.hpp>
#include <comphelper/diagnose_ex.hxx>
#include <unotools/textsearch.hxx>
#include <dialmgr.hxx>
#include <strings.hrc>
#include <comphelper/processfactory.hxx>
#include <comphelper/sequenceashashmap.hxx>
#include <comphelper/SetFlagContextHelper.hxx>
#include <comphelper/scriptbrowse.hxx>
#include <comphelper/string.hxx>
#include <officecfg/Office/Common.hxx>
#include <i18nlangtag/languagetag.hxx>
#include <i18nutil/searchopt.hxx>
#include <sal/log.hxx>
#include <cfg.hxx> //for SaveInData
CommandCategoryListBox::CommandCategoryListBox(std::unique_ptr<weld::ComboBox> xControl)
: pStylesInfo(nullptr)
, m_xControl(std::move(xControl))
{
//Initialize search util
m_searchOptions.AlgorithmType2 = css::util::SearchAlgorithms2::ABSOLUTE;
m_searchOptions.transliterateFlags |= TransliterationFlags::IGNORE_CASE;
m_searchOptions.searchFlag |= (css::util::SearchFlags::REG_NOT_BEGINOFLINE
| css::util::SearchFlags::REG_NOT_ENDOFLINE);
}
CommandCategoryListBox::~CommandCategoryListBox() { ClearAll(); }
void CommandCategoryListBox::ClearAll()
{
m_aGroupInfo.clear();
m_xControl->clear();
}
void CommandCategoryListBox::Init(const css::uno::Reference<css::uno::XComponentContext>& xContext,
const css::uno::Reference<css::frame::XFrame>& xFrame,
const OUString& sModuleLongName)
{
// User will not see incomplete UI
m_xControl->freeze();
ClearAll();
m_xContext = xContext;
m_xFrame = xFrame;
m_sModuleLongName = sModuleLongName;
m_xGlobalCategoryInfo = css::ui::theUICategoryDescription::get(m_xContext);
m_xModuleCategoryInfo.set(m_xGlobalCategoryInfo->getByName(m_sModuleLongName),
css::uno::UNO_QUERY_THROW);
m_xUICmdDescription = css::frame::theUICommandDescription::get(m_xContext);
// Support style commands
css::uno::Reference<css::frame::XController> xController;
css::uno::Reference<css::frame::XModel> xModel;
if (xFrame.is())
xController = xFrame->getController();
if (xController.is())
xModel = xController->getModel();
m_aStylesInfo.init(sModuleLongName, xModel);
SetStylesInfo(&m_aStylesInfo);
try
{
css::uno::Reference<css::frame::XDispatchInformationProvider> xProvider(
m_xFrame, css::uno::UNO_QUERY_THROW);
css::uno::Sequence<sal_Int16> lGroups = xProvider->getSupportedCommandGroups();
sal_Int32 nGroupsLength = lGroups.getLength();
if (nGroupsLength > 0)
{
// Add the category of "All commands"
m_aGroupInfo.push_back(std::make_unique<SfxGroupInfo_Impl>(
SfxGroupInfo_Data(SfxCfgKindAsIndex<SfxCfgKind::GROUP_ALLFUNCTIONS>), 0));
m_xControl->append(weld::toId(m_aGroupInfo.back().get()),
CuiResId(RID_CUISTR_ALLFUNCTIONS));
}
// Separate the "All commands"category from the actual categories
m_xControl->append_separator(u""_ustr);
typedef std::pair<OUString, sal_Int16> str_id;
std::vector<str_id> aCategories;
// Add the actual categories
for (sal_Int32 i = 0; i < nGroupsLength; ++i)
{
sal_Int16 nGroupID = lGroups[i];
OUString sGroupID = OUString::number(nGroupID);
OUString sGroupName;
try
{
m_xModuleCategoryInfo->getByName(sGroupID) >>= sGroupName;
if (sGroupName.isEmpty())
continue;
}
catch (const css::container::NoSuchElementException&)
{
continue;
}
aCategories.emplace_back(std::make_pair(sGroupName, nGroupID));
}
auto const sort = comphelper::string::NaturalStringSorter(
comphelper::getProcessComponentContext(),
Application::GetSettings().GetUILanguageTag().getLocale());
std::sort(aCategories.begin(), aCategories.end(),
[&sort](const str_id& a, const str_id& b) {
return sort.compare(a.first, b.first) < 0;
});
// Add the actual categories
for (const auto& a : aCategories)
{
const OUString& rGroupName = a.first;
sal_Int16 nGroupID = a.second;
m_aGroupInfo.push_back(std::make_unique<SfxGroupInfo_Impl>(
SfxGroupInfo_Data(SfxCfgKindAsIndex<SfxCfgKind::GROUP_FUNCTION>), nGroupID));
m_xControl->append(weld::toId(m_aGroupInfo.back().get()), rGroupName);
}
// Separate regular commands from styles and macros
m_xControl->append_separator(u""_ustr);
// Add macros category
m_aGroupInfo.push_back(std::make_unique<SfxGroupInfo_Impl>(
SfxGroupInfo_Data(SfxCfgKindAsIndex<SfxCfgKind::GROUP_SCRIPTCONTAINER>), 0));
m_xControl->append(weld::toId(m_aGroupInfo.back().get()), CuiResId(RID_CUISTR_MACROS));
// Add styles category
//TODO: last param should contain user data?
m_aGroupInfo.push_back(std::make_unique<SfxGroupInfo_Impl>(
SfxGroupInfo_Data(SfxCfgKindAsIndex<SfxCfgKind::GROUP_STYLES>), 0));
m_xControl->append(weld::toId(m_aGroupInfo.back().get()),
CuiResId(RID_CUISTR_GROUP_STYLES));
}
catch (const css::uno::RuntimeException&)
{
throw;
}
catch (const css::uno::Exception&)
{
}
// Reveal the updated UI to user
m_xControl->thaw();
m_xControl->set_active(0);
}
void CommandCategoryListBox::FillFunctionsList(
const css::uno::Sequence<css::frame::DispatchInformation>& xCommands,
CuiConfigFunctionListBox* pFunctionListBox, const OUString& filterTerm,
SaveInData* pCurrentSaveInData)
{
// Setup search filter parameters
m_searchOptions.searchString = filterTerm;
utl::TextSearch textSearch(m_searchOptions);
const bool bInExperimentalMode = officecfg::Office::Common::Misc::ExperimentalMode::get();
for (const auto& rInfo : xCommands)
{
auto aProperties
= vcl::CommandInfoProvider::GetCommandProperties(rInfo.Command, m_sModuleLongName);
OUString sUIName = getCommandName(rInfo.Command);
OUString sLabel = vcl::CommandInfoProvider::GetLabelForCommand(aProperties);
OUString sTooltipLabel
= vcl::CommandInfoProvider::GetTooltipForCommand(rInfo.Command, aProperties, m_xFrame);
OUString sPopupLabel = (vcl::CommandInfoProvider::GetPopupLabelForCommand(aProperties))
.replaceFirst("~", "");
bool bIsExperimental
= vcl::CommandInfoProvider::IsExperimental(rInfo.Command, m_sModuleLongName);
// Hide experimental commands when not in experimental mode
bool bHideExperimental = bIsExperimental && !bInExperimentalMode;
// Apply the search filter
if (bHideExperimental
|| (!filterTerm.isEmpty() && !textSearch.searchForward(sUIName)
&& !textSearch.searchForward(sLabel) && !textSearch.searchForward(sTooltipLabel)
&& !textSearch.searchForward(sPopupLabel)))
{
continue;
}
css::uno::Reference<css::graphic::XGraphic> xImage;
if (pCurrentSaveInData)
xImage = pCurrentSaveInData->GetImage(rInfo.Command);
m_aGroupInfo.push_back(std::make_unique<SfxGroupInfo_Impl>(
SfxGroupInfo_Data(SfxCfgKindAsIndex<SfxCfgKind::FUNCTION_SLOT>), 0));
SfxGroupInfo_Impl* pGrpInfo = m_aGroupInfo.back().get();
pGrpInfo->sCommand = rInfo.Command;
pGrpInfo->sLabel = sUIName;
pGrpInfo->sTooltip = sTooltipLabel;
pFunctionListBox->append(weld::toId(m_aGroupInfo.back().get()), sUIName, xImage);
}
}
OUString CommandCategoryListBox::getCommandName(const OUString& sCommand)
{
OUString sUIName;
try
{
css::uno::Reference<css::container::XNameAccess> xModuleConf;
m_xUICmdDescription->getByName(m_sModuleLongName) >>= xModuleConf;
if (xModuleConf.is())
{
::comphelper::SequenceAsHashMap lProps(xModuleConf->getByName(sCommand));
sUIName = lProps.getUnpackedValueOrDefault(u"Name"_ustr, OUString());
}
}
catch (const css::uno::RuntimeException&)
{
throw;
}
catch (css::uno::Exception&)
{
sUIName.clear();
}
// fallback for missing UINames !?
if (sUIName.isEmpty())
{
sUIName = sCommand;
}
return sUIName;
}
void CommandCategoryListBox::categorySelected(CuiConfigFunctionListBox* pFunctionListBox,
const OUString& filterTerm,
SaveInData* pCurrentSaveInData)
{
SfxGroupInfo_Impl* pInfo = weld::fromId<SfxGroupInfo_Impl*>(m_xControl->get_active_id());
std::vector<std::unique_ptr<weld::TreeIter>> aNodesToExpand;
pFunctionListBox->freeze();
pFunctionListBox->ClearAll();
switch (pInfo->getKind())
{
case SfxCfgKind::GROUP_ALLFUNCTIONS:
{
css::uno::Reference<css::frame::XDispatchInformationProvider> xProvider(
m_xFrame, css::uno::UNO_QUERY);
sal_Int32 nEntryCount = m_xControl->get_count();
for (sal_Int32 nCurPos = 0; nCurPos < nEntryCount; ++nCurPos)
{
SfxGroupInfo_Impl* pCurrentInfo
= weld::fromId<SfxGroupInfo_Impl*>(m_xControl->get_id(nCurPos));
if (!pCurrentInfo) //separator
continue;
if (pCurrentInfo->getKind() == SfxCfgKind::GROUP_FUNCTION)
{
css::uno::Sequence<css::frame::DispatchInformation> lCommands;
try
{
lCommands = xProvider->getConfigurableDispatchInformation(
pCurrentInfo->nUniqueID);
FillFunctionsList(lCommands, pFunctionListBox, filterTerm,
pCurrentSaveInData);
}
catch (css::container::NoSuchElementException&)
{
}
}
}
break;
}
case SfxCfgKind::GROUP_FUNCTION:
{
sal_uInt16 nGroup = pInfo->nUniqueID;
css::uno::Reference<css::frame::XDispatchInformationProvider> xProvider(
m_xFrame, css::uno::UNO_QUERY_THROW);
css::uno::Sequence<css::frame::DispatchInformation> lCommands
= xProvider->getConfigurableDispatchInformation(nGroup);
FillFunctionsList(lCommands, pFunctionListBox, filterTerm, pCurrentSaveInData);
break;
}
case SfxCfgKind::GROUP_SCRIPTCONTAINER: //Macros
{
SAL_INFO("cui.customize", "** ** About to initialise SF Scripts");
// Add Scripting Framework entries
css::uno::Reference<css::script::browse::XBrowseNode> rootNode;
try
{
css::uno::Reference<css::script::browse::XBrowseNodeFactory> xFac
= css::script::browse::theBrowseNodeFactory::get(m_xContext);
rootNode.set(xFac->createView(
css::script::browse::BrowseNodeFactoryViewTypes::MACROSELECTOR));
}
catch (css::uno::Exception const&)
{
TOOLS_WARN_EXCEPTION(
"cui.customize",
"Caught some exception whilst retrieving browse nodes from factory");
// TODO exception handling
}
if (rootNode.is() && rootNode->hasChildNodes())
{
m_aGroupInfo.push_back(std::make_unique<SfxGroupInfo_Impl>(
SfxGroupInfo_Data(SfxCfgKindAsIndex<SfxCfgKind::GROUP_SCRIPTCONTAINER>,
rootNode),
0));
// Add main macro groups
const std::vector<css::uno::Reference<css::script::browse::XBrowseNode>> aChildNodes
= comphelper::scriptbrowse::getChildNodes(rootNode);
for (auto const& childGroup : aChildNodes)
{
if (childGroup->hasChildNodes())
{
OUString sUIName;
if (childGroup->getName() == "user")
{
sUIName = CuiResId(RID_CUISTR_MYMACROS);
}
else if (childGroup->getName() == "share")
{
sUIName = CuiResId(RID_CUISTR_PRODMACROS);
}
else
{
sUIName = childGroup->getName();
}
if (sUIName.isEmpty())
{
continue;
}
m_aGroupInfo.push_back(std::make_unique<SfxGroupInfo_Impl>(
SfxGroupInfo_Data(SfxCfgKindAsIndex<SfxCfgKind::GROUP_SCRIPTCONTAINER>),
0));
std::unique_ptr<weld::TreeIter> xMacroGroup(pFunctionListBox->tree_append(
weld::toId(m_aGroupInfo.back().get()), sUIName));
{
// tdf#128010: Do not nag user asking to enable JRE: if it's disabled,
// simply don't show relevant entries (user chose to not use JRE)
css::uno::ContextLayer layer(
comphelper::NoEnableJavaInteractionContext());
//Add the children and the grand children
addChildren(xMacroGroup.get(), childGroup, pFunctionListBox, filterTerm,
pCurrentSaveInData, aNodesToExpand);
}
// Remove the main group if empty
if (!pFunctionListBox->iter_has_child(*xMacroGroup))
{
pFunctionListBox->remove(*xMacroGroup);
}
else if (!filterTerm.isEmpty())
{
aNodesToExpand.emplace_back(std::move(xMacroGroup));
}
}
}
}
break;
}
case SfxCfgKind::GROUP_STYLES:
{
const std::vector<SfxStyleInfo_Impl> lStyleFamilies = pStylesInfo->getStyleFamilies();
for (const auto& pIt : lStyleFamilies)
{
if (pIt.sLabel.isEmpty())
{
continue;
}
m_aGroupInfo.push_back(std::make_unique<SfxGroupInfo_Impl>(
SfxGroupInfo_Data(SfxCfgKindAsIndex<SfxCfgKind::GROUP_STYLES>), 0));
// pIt.sLabel is Name of the style family
std::unique_ptr<weld::TreeIter> xFuncEntry(pFunctionListBox->tree_append(
weld::toId(m_aGroupInfo.back().get()), pIt.sLabel));
const std::vector<SfxStyleInfo_Impl> lStyles = pStylesInfo->getStyles(pIt.sFamily);
// Setup search filter parameters
m_searchOptions.searchString = filterTerm;
utl::TextSearch textSearch(m_searchOptions);
// Insert children (styles)
for (const auto& pStyleIt : lStyles)
{
OUString sUIName = pStyleIt.sLabel;
sal_Int32 aStartPos = 0;
sal_Int32 aEndPos = sUIName.getLength();
// Apply the search filter
if (!filterTerm.isEmpty()
&& !textSearch.SearchForward(sUIName, &aStartPos, &aEndPos))
{
continue;
}
auto pStyle = std::make_unique<SfxStyleInfo_Impl>(pStyleIt);
m_aGroupInfo.push_back(std::make_unique<SfxGroupInfo_Impl>(
SfxGroupInfo_Data(SfxCfgKindAsIndex<SfxCfgKind::GROUP_STYLES>,
std::move(pStyle)),
0));
m_aGroupInfo.back()->sCommand = pStyleIt.sCommand;
m_aGroupInfo.back()->sLabel = pStyleIt.sLabel;
pFunctionListBox->append(weld::toId(m_aGroupInfo.back().get()), sUIName,
xFuncEntry.get());
}
// Remove the style group from the list if no children
if (!pFunctionListBox->iter_has_child(*xFuncEntry))
{
pFunctionListBox->remove(*xFuncEntry);
}
else if (!filterTerm.isEmpty())
{
aNodesToExpand.emplace_back(std::move(xFuncEntry));
}
}
break;
}
default:
// Do nothing, the list box will stay empty
SAL_INFO("cui.customize", "Ignoring unexpected SfxCfgKind: " << pInfo->aData.index());
break;
}
pFunctionListBox->thaw();
if (pFunctionListBox->n_children())
pFunctionListBox->select(0);
//post freeze
for (const auto& it : aNodesToExpand)
pFunctionListBox->expand_row(*it);
}
void CommandCategoryListBox::SetStylesInfo(SfxStylesInfo_Impl* pStyles) { pStylesInfo = pStyles; }
void CommandCategoryListBox::addChildren(
const weld::TreeIter* parentEntry,
const css::uno::Reference<css::script::browse::XBrowseNode>& parentNode,
CuiConfigFunctionListBox* pFunctionListBox, const OUString& filterTerm,
SaveInData* pCurrentSaveInData, std::vector<std::unique_ptr<weld::TreeIter>>& rNodesToExpand)
{
// Setup search filter parameters
m_searchOptions.searchString = filterTerm;
utl::TextSearch textSearch(m_searchOptions);
const std::vector<css::uno::Reference<css::script::browse::XBrowseNode>> aChildNodes
= comphelper::scriptbrowse::getSortedChildNodes(parentNode);
for (auto const& child : aChildNodes)
{
if (child->hasChildNodes())
{
OUString sUIName = child->getName();
m_aGroupInfo.push_back(std::make_unique<SfxGroupInfo_Impl>(
SfxGroupInfo_Data(SfxCfgKindAsIndex<SfxCfgKind::GROUP_SCRIPTCONTAINER>, child), 0));
std::unique_ptr<weld::TreeIter> xNewEntry(pFunctionListBox->tree_append(
weld::toId(m_aGroupInfo.back().get()), sUIName, parentEntry));
addChildren(xNewEntry.get(), child, pFunctionListBox, filterTerm, pCurrentSaveInData,
rNodesToExpand);
// Remove the group if empty
if (!pFunctionListBox->iter_has_child(*xNewEntry))
pFunctionListBox->remove(*xNewEntry);
else
rNodesToExpand.emplace_back(std::move(xNewEntry));
}
else if (child->getType() == css::script::browse::BrowseNodeTypes::SCRIPT)
{
// Prepare for filtering
OUString sUIName = child->getName();
sal_Int32 aStartPos = 0;
sal_Int32 aEndPos = sUIName.getLength();
// Apply the search filter
if (!filterTerm.isEmpty() && !textSearch.SearchForward(sUIName, &aStartPos, &aEndPos))
{
continue;
}
OUString uri, description;
css::uno::Reference<css::beans::XPropertySet> xPropSet(child, css::uno::UNO_QUERY);
if (!xPropSet.is())
{
continue;
}
css::uno::Any value = xPropSet->getPropertyValue(u"URI"_ustr);
value >>= uri;
try
{
value = xPropSet->getPropertyValue(u"Description"_ustr);
value >>= description;
}
catch (css::uno::Exception&)
{
// do nothing, the description will be empty
}
if (description.isEmpty())
{
description = CuiResId(RID_CUISTR_NOMACRODESC);
}
css::uno::Reference<css::graphic::XGraphic> xImage;
if (pCurrentSaveInData)
xImage = pCurrentSaveInData->GetImage(uri);
m_aGroupInfo.push_back(std::make_unique<SfxGroupInfo_Impl>(
SfxGroupInfo_Data(SfxCfgKindAsIndex<SfxCfgKind::FUNCTION_SCRIPT>, uri), 0));
m_aGroupInfo.back()->sCommand = uri;
m_aGroupInfo.back()->sLabel = sUIName;
m_aGroupInfo.back()->sHelpText = description;
pFunctionListBox->append(weld::toId(m_aGroupInfo.back().get()), sUIName, xImage,
parentEntry);
}
}
}
/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */