basctl/inc/strings.hrc                     |    3 
 basctl/source/basicide/idedataprovider.cxx |   32 ++
 basctl/source/basicide/objectbrowser.cxx   |  383 ++++++++++++++++++++++++++++-
 basctl/source/inc/idedataprovider.hxx      |    1 
 basctl/source/inc/objectbrowser.hxx        |    8 
 5 files changed, 421 insertions(+), 6 deletions(-)

New commits:
commit 2b5803c3b4eeea5ea9fb4dd108adb35de3c5b1db
Author:     Devansh Varshney <[email protected]>
AuthorDate: Wed Oct 8 01:29:37 2025 +0530
Commit:     Jonathan Clark <[email protected]>
CommitDate: Fri Oct 17 01:51:10 2025 +0200

    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 <[email protected]>

diff --git a/basctl/inc/strings.hrc b/basctl/inc/strings.hrc
index 166d53f239a2..344cb8382630 100644
--- a/basctl/inc/strings.hrc
+++ b/basctl/inc/strings.hrc
@@ -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")
diff --git a/basctl/source/basicide/idedataprovider.cxx 
b/basctl/source/basicide/idedataprovider.cxx
index 4106b524f269..06f8444297c6 100644
--- a/basctl/source/basicide/idedataprovider.cxx
+++ b/basctl/source/basicide/idedataprovider.cxx
@@ -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;
 }
 
diff --git a/basctl/source/basicide/objectbrowser.cxx 
b/basctl/source/basicide/objectbrowser.cxx
index 2dfc5ac0e253..11e475356274 100644
--- a/basctl/source/basicide/objectbrowser.cxx
+++ b/basctl/source/basicide/objectbrowser.cxx
@@ -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"
Error: "_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)
 {
diff --git a/basctl/source/inc/idedataprovider.hxx 
b/basctl/source/inc/idedataprovider.hxx
index bb2470bd4a84..fef8ae40b7db 100644
--- a/basctl/source/inc/idedataprovider.hxx
+++ b/basctl/source/inc/idedataprovider.hxx
@@ -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;
diff --git a/basctl/source/inc/objectbrowser.hxx 
b/basctl/source/inc/objectbrowser.hxx
index 68d0b73c0bc0..c073f75be893 100644
--- a/basctl/source/inc/objectbrowser.hxx
+++ b/basctl/source/inc/objectbrowser.hxx
@@ -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);
 };

Reply via email to