tdf#165785: basctl: Implement Right-Pane Interactivity

This patch brings the Object Browser's right-hand members pane to life.

The right pane now supports on-demand expansion for nested UNO types.
Double-clicking items now performs a context-aware action: opening
the relevant Doxygen API page for UNO types or navigating to the
source code for BASIC macros.

(GSoC 2025 - Object Browser: Right-Pane UI Enhancement)

Change-Id: Ibcc2a0479746f3724372c9bd89853626506d42f3
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/192050
Tested-by: Jenkins
Reviewed-by: Jonathan Clark <jonathan@libreoffice.org>
This commit is contained in:
Devansh Varshney
2025-10-17 01:51:10 +02:00
committed by Jonathan Clark
parent 34612217f8
commit 2b5803c3b4
5 changed files with 421 additions and 6 deletions
+3
View File
@@ -103,6 +103,8 @@
#define RID_STR_RUN NC_("RID_STR_RUN", "Run")
#define RID_STR_RECORD NC_("RID_STR_RECORD", "~Save")
#define RID_STR_OBJECT_BROWSER NC_("RID_STR_OBJECT_BROWSER", "Object Browser")
#define RID_STR_OB_DOCS_ERROR_TITLE NC_("RID_STR_OB_DOCS_ERROR_TITLE", "Documentation Error")
#define RID_STR_OB_BROWSER_LAUNCH_FAILED NC_("RID_STR_OB_BROWSER_LAUNCH_FAILED", "Could not open the documentation in an external web browser.")
#define RID_STR_OB_GROUP_PROPERTIES NC_("RID_STR_OB_GROUP_PROPERTIES", "Properties")
#define RID_STR_OB_GROUP_METHODS NC_("RID_STR_OB_GROUP_METHODS", "Methods")
#define RID_STR_OB_GROUP_FIELDS NC_("RID_STR_OB_GROUP_FIELDS", "Fields")
@@ -112,6 +114,7 @@
#define RID_STR_OB_SCOPE_CURRENT NC_("RID_STR_OB_SCOPE_CURRENT", "Current Document")
#define RID_STR_OB_GROUP_FUNCTIONS NC_("RID_STR_OB_GROUP_FUNCTIONS", "Functions")
#define RID_STR_OB_GROUP_OTHER NC_("RID_STR_OB_GROUP_OTHER", "Other")
#define RID_STR_OB_NO_DOCUMENTATION NC_("RID_STR_OB_NO_DOCUMENTATION", "No documentation link is available for the selected item.")
#define RID_STR_OB_SEARCH_PLACEHOLDER NC_("RID_STR_OB_SEARCH_PLACEHOLDER", "Search objects and members...")
#define RID_STR_OB_BACK_BUTTON NC_("RID_STR_OB_BACK_BUTTON", "Back")
#define RID_STR_OB_FORWARD_BUTTON NC_("RID_STR_OB_FORWARD_BUTTON", "Forward")
@@ -93,6 +93,12 @@ void ImplGetMembersOfUnoType(SymbolInfoList& rMembers, const IdeSymbolInfo& rNod
}
auto pNode = std::make_shared<IdeSymbolInfo>(
xMethod->getName(), IdeSymbolKind::UNO_METHOD, rNode.sIdentifier);
if (!rNode.sQualifiedName.isEmpty())
{
pNode->sQualifiedName = rNode.sQualifiedName + u"." + xMethod->getName();
pNode->sParentName = rNode.sQualifiedName;
}
pNode->sReturnTypeName = helper_getTypeName(xMethod->getReturnType());
for (const auto& rParam : xMethod->getParameterInfos())
{
@@ -129,6 +135,12 @@ void ImplGetMembersOfUnoType(SymbolInfoList& rMembers, const IdeSymbolInfo& rNod
auto pMember
= std::make_shared<IdeSymbolInfo>(xField->getName(), eFieldKind, rNode.sIdentifier);
if (!rNode.sQualifiedName.isEmpty())
{
pMember->sQualifiedName = rNode.sQualifiedName + u"." + xField->getName();
pMember->sParentName = rNode.sQualifiedName;
}
pMember->sTypeName = helper_getTypeName(xField->getType());
Reference<XIdlClass> xFieldType = xField->getType();
@@ -198,6 +210,16 @@ void ImplGetMembersOfBasicModule(SymbolInfoList& rMembers, const IdeSymbolInfo&
: IdeSymbolKind::FUNCTION;
auto pMember
= std::make_shared<IdeSymbolInfo>(pMethod->GetName(), eKind, rNode.sIdentifier);
pMember->sOriginLibrary = rNode.sOriginLibrary;
pMember->sOriginModule = rNode.sName; // The parent node name is the module name
pMember->sOriginLocation = rNode.sOriginLocation;
if (!rNode.sOriginLibrary.isEmpty() && !rNode.sName.isEmpty())
{
pMember->sParentName = rNode.sOriginLibrary + u"." + rNode.sName;
pMember->sQualifiedName = pMember->sParentName + u"." + pMethod->GetName();
}
rMembers.push_back(pMember);
}
}
@@ -367,6 +389,7 @@ void IdeDataProvider::Reset()
SAL_INFO("basctl", "IdeDataProvider: Resetting state.");
m_bInitialized = false;
m_aAllTopLevelNodes.clear();
m_aMembersCache.clear();
m_pUnoHierarchy = std::make_unique<UnoApiHierarchy>();
}
@@ -521,6 +544,12 @@ SymbolInfoList IdeDataProvider::GetTopLevelNodes()
GroupedSymbolInfoList IdeDataProvider::GetMembers(const IdeSymbolInfo& rNode)
{
auto it = m_aMembersCache.find(rNode.sIdentifier);
if (it != m_aMembersCache.end())
{
return it->second;
}
GroupedSymbolInfoList aGroupedMembers;
SymbolInfoList aFlatMembers;
@@ -548,6 +577,9 @@ GroupedSymbolInfoList IdeDataProvider::GetMembers(const IdeSymbolInfo& rNode)
{
aGroupedMembers[pMemberInfo->eKind].push_back(pMemberInfo);
}
m_aMembersCache[rNode.sIdentifier] = aGroupedMembers;
return aGroupedMembers;
}
+378 -5
View File
@@ -17,15 +17,24 @@
#include <iderid.hxx>
#include <strings.hrc>
#include <basctl/sbxitem.hxx>
#include <comphelper/processfactory.hxx>
#include <sal/log.hxx>
#include <sfx2/bindings.hxx>
#include <sfx2/dispatch.hxx>
#include <sfx2/event.hxx>
#include <svl/itemset.hxx>
#include <sfx2/sfxsids.hrc>
#include <sfx2/viewfrm.hxx>
#include <vcl/svapp.hxx>
#include <vcl/taskpanelist.hxx>
#include <vcl/weld.hxx>
#include <com/sun/star/system/XSystemShellExecute.hpp>
#include <com/sun/star/system/SystemShellExecuteFlags.hpp>
#include <com/sun/star/uno/XComponentContext.hpp>
#include <com/sun/star/lang/XMultiComponentFactory.hpp>
namespace basctl
{
namespace
@@ -193,6 +202,124 @@ void AddEntry(weld::TreeView& rTargetTree, std::vector<std::shared_ptr<IdeSymbol
rTargetTree.set_image(*pRetIter, sIconName, -1);
}
OUString BuildDoxygenUrl(const IdeSymbolInfo& rSymbol)
{
if (rSymbol.sQualifiedName.isEmpty())
return OUString();
if (rSymbol.sQualifiedName.startsWith("ooo.vba."))
{
SAL_INFO("basctl", "BuildDoxygenUrl: Skipping VBA type '" << rSymbol.sQualifiedName << "'");
return OUString();
}
// Doxygen mangles names by replacing '.' with "_1_1"
OUString sMangledName;
OUString sTypePrefix;
OUString sAnchor;
switch (rSymbol.eKind)
{
// Case 1: Types that have their OWN dedicated .html page
case IdeSymbolKind::UNO_STRUCT:
sTypePrefix = u"struct"_ustr;
sMangledName = rSymbol.sQualifiedName.replaceAll(".", "_1_1");
break;
case IdeSymbolKind::UNO_INTERFACE:
sTypePrefix = u"interface"_ustr;
sMangledName = rSymbol.sQualifiedName.replaceAll(".", "_1_1");
break;
case IdeSymbolKind::UNO_SERVICE:
sTypePrefix = u"service"_ustr;
sMangledName = rSymbol.sQualifiedName.replaceAll(".", "_1_1");
break;
case IdeSymbolKind::UNO_EXCEPTION:
sTypePrefix = u"exception"_ustr;
sMangledName = rSymbol.sQualifiedName.replaceAll(".", "_1_1");
break;
case IdeSymbolKind::UNO_NAMESPACE:
sTypePrefix = u"namespace"_ustr;
sMangledName = rSymbol.sQualifiedName.replaceAll(".", "_1_1");
break;
// Case 2: Types that are documented as ANCHORS on their parent namespace page
case IdeSymbolKind::UNO_ENUM:
case IdeSymbolKind::UNO_CONSTANTS:
case IdeSymbolKind::UNO_TYPEDEF:
{
if (rSymbol.sParentName.isEmpty())
{
return OUString();
}
sTypePrefix = u"namespace"_ustr;
sMangledName = rSymbol.sParentName.replaceAll(".", "_1_1");
sAnchor = u"#"_ustr + rSymbol.sName;
break;
}
// This symbol kind does not have a Doxygen page
default:
return OUString();
}
if (sMangledName.isEmpty() || sTypePrefix.isEmpty())
{
return OUString();
}
return u"https://api.libreoffice.org/docs/idl/ref/"_ustr + sTypePrefix + sMangledName
+ u".html"_ustr + sAnchor;
}
void ShowDocsError(vcl::Window* pParent, const OUString& sPrimaryText,
const OUString& sSecondaryText = OUString())
{
if (!pParent)
return;
std::unique_ptr<weld::MessageDialog> xBox(Application::CreateMessageDialog(
pParent->GetFrameWeld(), VclMessageType::Warning, VclButtonsType::Ok, sPrimaryText));
xBox->set_title(IDEResId(RID_STR_OB_DOCS_ERROR_TITLE));
if (!sSecondaryText.isEmpty())
{
xBox->set_secondary_text(sSecondaryText);
}
xBox->run();
}
void OpenDoxygenDocumentation(vcl::Window* pParent, const IdeSymbolInfo& rSymbol)
{
OUString sUrl = BuildDoxygenUrl(rSymbol);
SAL_INFO("basctl", "OpenDoxygenDocumentation: Built URL='" << sUrl << "' for symbol '"
<< rSymbol.sName << "'");
if (sUrl.isEmpty())
{
ShowDocsError(pParent, IDEResId(RID_STR_OB_NO_DOCUMENTATION));
return;
}
try
{
css::uno::Reference<css::uno::XComponentContext> xContext
= comphelper::getProcessComponentContext();
css::uno::Reference<css::lang::XMultiComponentFactory> xServiceManager
= xContext->getServiceManager();
css::uno::Reference<css::system::XSystemShellExecute> xSystemShell(
xServiceManager->createInstanceWithContext(
u"com.sun.star.system.SystemShellExecute"_ustr, xContext),
css::uno::UNO_QUERY_THROW);
xSystemShell->execute(sUrl, OUString(), css::system::SystemShellExecuteFlags::URIS_ONLY);
}
catch (const css::uno::Exception& e)
{
SAL_WARN("basctl", "Failed to open Doxygen documentation: " << e.Message);
OUString sSecondaryMsg = u"URL: "_ustr + sUrl + u"\nError: "_ustr + e.Message;
ShowDocsError(pParent, IDEResId(RID_STR_OB_BROWSER_LAUNCH_FAILED), sSecondaryMsg);
}
}
} // anonymous namespace
ObjectBrowser::ObjectBrowser(Shell& rShell, vcl::Window* pParent)
@@ -251,6 +378,9 @@ void ObjectBrowser::Initialize()
{
m_xRightMembersView->connect_selection_changed(
LINK(this, ObjectBrowser, OnRightTreeSelect));
m_xRightMembersView->connect_expanding(LINK(this, ObjectBrowser, OnRightNodeExpand));
m_xRightMembersView->connect_row_activated(
LINK(this, ObjectBrowser, OnRightTreeDoubleClick));
}
if (m_xBackButton)
@@ -372,6 +502,13 @@ void ObjectBrowser::Show(bool bVisible)
void ObjectBrowser::RefreshUI(bool /*bForceKeepUno*/)
{
IdeTimer aTimer(u"ObjectBrowser::RefreshUI"_ustr);
if (m_bPerformingAction)
{
SAL_INFO("basctl", "ObjectBrowser::RefreshUI: Blocked because an action is in progress.");
return;
}
if (!m_pDataProvider || !m_pDataProvider->IsInitialized())
{
ShowLoadingState();
@@ -432,9 +569,12 @@ void ObjectBrowser::Notify(SfxBroadcaster& /*rBC*/, const SfxHint& rHint)
return;
}
// Filter out activations of the BASIC IDE
if (pObjShell->GetFactory().GetDocumentServiceName().endsWith(u"BasicIDE"))
OUString sServiceName = pObjShell->GetFactory().GetDocumentServiceName();
// Filter out BasicIDE and HTML/Web documents (Doxygen docs)
if (sServiceName.endsWith(u"BasicIDE") || sServiceName.endsWith(u"WebDocument"))
{
SAL_INFO("basctl", "ObjectBrowser::Notify: Ignoring BasicIDE & WebDocument activation");
return;
}
@@ -496,10 +636,63 @@ void ObjectBrowser::ClearRightTreeView()
void ObjectBrowser::ScheduleRefresh()
{
if (m_bPerformingAction)
{
SAL_INFO("basctl",
"ScheduleRefresh: Blocked refresh request because an action is in progress.");
return;
}
m_bDataMayBeStale = true;
if (IsVisible())
{
RefreshUI();
}
}
void ObjectBrowser::NavigateToMacroSource(const IdeSymbolInfo& rSymbol)
{
SAL_INFO("basctl", "NavigateToMacroSource: Entry - Library='"
<< rSymbol.sOriginLibrary << "', Module='" << rSymbol.sOriginModule
<< "', Method='" << rSymbol.sName << "'");
if (!m_pShell || rSymbol.sOriginLibrary.isEmpty() || rSymbol.sOriginModule.isEmpty())
{
SAL_WARN("basctl", "NavigateToMacroSource: Invalid parameters.");
return;
}
ScriptDocument aDoc
= rSymbol.sOriginLocation.isEmpty()
? ScriptDocument::getApplicationScriptDocument()
: ScriptDocument::getDocumentWithURLOrCaption(rSymbol.sOriginLocation);
if (aDoc.isAlive())
{
SAL_INFO("basctl",
"NavigateToMacroSource: Document is alive, dispatching SID_BASICIDE_SHOWSBX.");
// Pass the method name so it navigates to the exact line
SbxItem aSbxItem(SID_BASICIDE_ARG_SBX, aDoc, rSymbol.sOriginLibrary, rSymbol.sOriginModule,
rSymbol.sName, basctl::SBX_TYPE_METHOD);
SfxViewFrame& rViewFrame = m_pShell->GetViewFrame();
if (SfxDispatcher* pDispatcher = rViewFrame.GetDispatcher())
{
std::ignore = pDispatcher->ExecuteList(SID_BASICIDE_SHOWSBX, SfxCallMode::SYNCHRON,
{ &aSbxItem });
SAL_INFO("basctl", "NavigateToMacroSource: Dispatched successfully.");
}
else
{
SAL_WARN("basctl", "NavigateToMacroSource: Could not get dispatcher.");
}
}
else
m_bDataMayBeStale = true;
{
SAL_WARN("basctl", "NavigateToMacroSource: ScriptDocument is not alive.");
}
}
void ObjectBrowser::PopulateMembersPane(const IdeSymbolInfo& rSymbol)
@@ -535,8 +728,10 @@ void ObjectBrowser::PopulateMembersPane(const IdeSymbolInfo& rSymbol)
for (const auto& pMemberInfo : rPair.second)
{
bool bHasNestedMembers = !pMemberInfo->mapMembers.empty();
AddEntry(*m_xRightMembersView, m_aRightTreeSymbolStore, m_aRightTreeSymbolIndex,
xGroupIter.get(), pMemberInfo, false);
xGroupIter.get(), pMemberInfo, bHasNestedMembers);
}
aGroupItersToExpand.push_back(std::move(xGroupIter));
}
@@ -587,7 +782,185 @@ IMPL_LINK(ObjectBrowser, OnLeftTreeSelect, weld::TreeView&, rTree, void)
}
}
IMPL_STATIC_LINK(ObjectBrowser, OnRightTreeSelect, weld::TreeView&, /*rTree*/, void) { /* STUB */}
IMPL_LINK(ObjectBrowser, OnRightTreeSelect, weld::TreeView&, rTree, void)
{
if (m_bDisposed)
{
return;
}
auto xSelectedIter = rTree.make_iterator();
if (!rTree.get_selected(xSelectedIter.get()))
{
return;
}
auto pSymbol = GetSymbolForIter(*xSelectedIter, rTree, m_aRightTreeSymbolIndex);
if (!pSymbol || pSymbol->eKind == IdeSymbolKind::PLACEHOLDER)
{
return;
}
}
IMPL_LINK(ObjectBrowser, OnRightNodeExpand, const weld::TreeIter&, rParentIter, bool)
{
if (m_bDisposed)
{
return false;
}
auto pParentSymbol
= GetSymbolForIter(rParentIter, *m_xRightMembersView, m_aRightTreeSymbolIndex);
if (!pParentSymbol)
{
return false;
}
for (const auto& pair : pParentSymbol->mapMembers)
{
const auto& pChildSymbol = pair.second;
if (pChildSymbol)
{
// Check if the child is also expandable
bool bChildHasChildren = !pChildSymbol->mapMembers.empty();
AddEntry(*m_xRightMembersView, m_aRightTreeSymbolStore, m_aRightTreeSymbolIndex,
&rParentIter, pChildSymbol, bChildHasChildren);
}
}
return true;
}
IMPL_LINK(ObjectBrowser, OnRightTreeDoubleClick, weld::TreeView&, rTree, bool)
{
SAL_INFO("basctl", "OnRightTreeDoubleClick: Handler entered.");
if (m_bDisposed)
{
SAL_WARN("basctl", "OnRightTreeDoubleClick: Browser is disposed, aborting.");
return false;
}
auto xSelectedIter = rTree.make_iterator();
if (!rTree.get_selected(xSelectedIter.get()))
{
SAL_INFO("basctl", "OnRightTreeDoubleClick: No item selected.");
return false;
}
auto pSymbol = GetSymbolForIter(*xSelectedIter, rTree, m_aRightTreeSymbolIndex);
if (!pSymbol || pSymbol->eKind == IdeSymbolKind::PLACEHOLDER)
{
SAL_INFO("basctl",
"OnRightTreeDoubleClick: Selected item is a placeholder or has no symbol.");
return false;
}
m_bPerformingAction = true;
SAL_INFO("basctl", "OnRightTreeDoubleClick: Processing symbol '"
<< pSymbol->sName << "' of kind " << static_cast<int>(pSymbol->eKind));
// Symbol is a BASIC Macro
if (pSymbol->eKind == IdeSymbolKind::SUB || pSymbol->eKind == IdeSymbolKind::FUNCTION)
{
SAL_INFO("basctl", "OnRightTreeDoubleClick: Action is NavigateToMacroSource.");
NavigateToMacroSource(*pSymbol);
m_bPerformingAction = false;
return true;
}
// The symbol itself is documentable
OUString sSymbolUrl = BuildDoxygenUrl(*pSymbol);
if (!sSymbolUrl.isEmpty())
{
SAL_INFO("basctl",
"OnRightTreeDoubleClick: Symbol is a documentable container. Opening its docs.");
OpenDoxygenDocumentation(this, *pSymbol);
m_bPerformingAction = false;
return true;
}
// Find documentable parent in RIGHT tree first
auto xParentIter = rTree.make_iterator(xSelectedIter.get());
while (rTree.iter_parent(*xParentIter))
{
auto pParentSymbol = GetSymbolForIter(*xParentIter, rTree, m_aRightTreeSymbolIndex);
if (pParentSymbol && pParentSymbol->eKind != IdeSymbolKind::PLACEHOLDER)
{
// If this parent has a qualified name it's documentable
if (!pParentSymbol->sQualifiedName.isEmpty())
{
SAL_INFO("basctl",
"OnRightTreeDoubleClick: Action is OpenDoxygenDocumentation for parent '"
<< pParentSymbol->sName << "' (from right pane).");
OpenDoxygenDocumentation(this, *pParentSymbol);
m_bPerformingAction = false;
return true;
}
// Otherwise, keep walking up the tree
}
}
// Find documentable parent in LEFT tree
auto xLeftTreeParentIter = m_xLeftTreeView->make_iterator();
if (m_xLeftTreeView->get_selected(xLeftTreeParentIter.get()))
{
auto pParentSymbol
= GetSymbolForIter(*xLeftTreeParentIter, *m_xLeftTreeView, m_aLeftTreeSymbolIndex);
if (pParentSymbol)
{
// Try to build URL for parent
OUString sParentUrl = BuildDoxygenUrl(*pParentSymbol);
if (sParentUrl.isEmpty())
{
auto xAncestorIter = m_xLeftTreeView->make_iterator(xLeftTreeParentIter.get());
while (m_xLeftTreeView->iter_parent(*xAncestorIter))
{
auto pAncestorSymbol = GetSymbolForIter(*xAncestorIter, *m_xLeftTreeView,
m_aLeftTreeSymbolIndex);
if (pAncestorSymbol)
{
if (pAncestorSymbol->sQualifiedName.startsWith("ooo.vba"))
{
SAL_INFO("basctl", "OnRightTreeDoubleClick: Ancestor is VBA namespace, "
"skipping documentation.");
break;
}
OUString sAncestorUrl = BuildDoxygenUrl(*pAncestorSymbol);
if (!sAncestorUrl.isEmpty())
{
SAL_INFO(
"basctl",
"OnRightTreeDoubleClick: Parent has no docs, opening ancestor '"
<< pAncestorSymbol->sName << "'");
OpenDoxygenDocumentation(this, *pAncestorSymbol);
m_bPerformingAction = false;
return true;
}
}
}
SAL_INFO("basctl", "OnRightTreeDoubleClick: No documentable ancestor found for '"
<< pParentSymbol->sName << "'");
}
else
{
SAL_INFO("basctl",
"OnRightTreeDoubleClick: Action is OpenDoxygenDocumentation for parent '"
<< pParentSymbol->sName << "' (from left pane).");
OpenDoxygenDocumentation(this, *pParentSymbol);
m_bPerformingAction = false;
return true;
}
}
}
m_bPerformingAction = false;
SAL_WARN("basctl", "OnRightTreeDoubleClick: No action taken for symbol.");
ShowDocsError(this, IDEResId(RID_STR_OB_NO_DOCUMENTATION));
return false;
}
IMPL_LINK(ObjectBrowser, OnNodeExpand, const weld::TreeIter&, rParentIter, bool)
{
+1
View File
@@ -77,6 +77,7 @@ private:
// Core data
std::unique_ptr<UnoApiHierarchy> m_pUnoHierarchy;
SymbolInfoList m_aAllTopLevelNodes;
std::map<OUString, GroupedSymbolInfoList> m_aMembersCache;
// State management
bool m_bInitialized = false;
+7 -1
View File
@@ -92,6 +92,10 @@ private:
ObjectBrowserInitState m_eInitState = ObjectBrowserInitState::NotInitialized;
bool m_bDataMayBeStale = true;
OUString m_sLastActiveDocumentIdentifier;
bool m_bPerformingAction = false; // Flag for Right Pane Double Click
// Helper Method for Double-Click Actions
void NavigateToMacroSource(const IdeSymbolInfo& rSymbol);
// UI Widgets
std::unique_ptr<weld::ComboBox> m_xScopeSelector;
@@ -119,7 +123,9 @@ private:
// UI Event Handlers
DECL_LINK(OnLeftTreeSelect, weld::TreeView&, void);
DECL_STATIC_LINK(ObjectBrowser, OnRightTreeSelect, weld::TreeView&, void);
DECL_LINK(OnRightTreeSelect, weld::TreeView&, void);
DECL_LINK(OnRightNodeExpand, const weld::TreeIter&, bool);
DECL_LINK(OnRightTreeDoubleClick, weld::TreeView&, bool);
DECL_LINK(OnNodeExpand, const weld::TreeIter&, bool);
DECL_LINK(OnScopeChanged, weld::ComboBox&, void);
};