Framework/Tutorial/Popup Menu Controller

From Apache OpenOffice Wiki
Jump to: navigation, search

This tutorial will give you a detailed step-by-step insight into a very flexible way to add dynamic popup menus into the OpenOffice.org 2.0.x menu bar. Dynamic popup menus are implemented via UNO using the popup menu controller service. This tutorial will be split into small chapters which describe different aspects of popup menu controllers. Everybody is invited to participate. May be someone wants to translate the extension to a different language (e.g. Java or Python) or wants to have more information about a specific topic. You can set a link to this page, if you think that this page adds valuable information.

The reader of this tutorial should know the following

The outcome of this tutorial will be an add-on which exchanges the default recent file list with an enhanced version. Hopefully some people will find it useful. May be it encourages other developers to extend it or create extensions using popup menu controllers.

General abstract of the popup menu controller concept

Popup menu controller architecture

Popup menu controllers are used in Apache OpenOffice when the content of a popup menu is dynamic (e.g. changes at runtime). Technically OpenOfforg uses the following architecture to integrate popup menu controllers.

Menu-Popupmenu-controller-architecture.png

The menubar controls all menu items. At activation time the menubar implementation checks once for all menu items if a popup menu controller has been registered for a command. The menubar uses the CommandURL property and queries the PopupMenuControllerFactory service for an associated popup menu controller. The PopupMenuControllerFactory uses the OpenOffice configuration to store the association between command URL and popup menu controller. An OpenOffice configuration file can easily be provided by extensions. Therefore popup menu controllers are a good way to extend the default OpenOfifce menubar. On success the PopupMenuControllerFactory is asked to create an instance of the popup menu controller. The menubar provides vital information about the current context (e.g. a reference to its popup menu, the associated frame) to the popup menu controller at creation time.

OpenOffice.org 2.0.4 uses the following popup menu controllers within the application modules:

Command

Modules

Service

Purpose

.uno:CharFontName

all

com.sun.star.comp.framework.FontMenuController

Provides a popup menu with all supported fonts.

.uno:FontHeight

all

com.sun.star.comp.framework.FontSizeMenuController

Provides a popup menu with all supported font sizes.

.uno:ObjectMenue

all

com.sun.star.comp.framework.ObjectMenuController

Provides a popup menu with all supported commands of an embedded object.

.uno:InsertPageHeader

all

com.sun.star.comp.framework.HeaderMenuController

Provides a popup menu to insert pages headers.

.uno:InsertPageFooter

all

com.sun.star.comp.framework.FooterMenuController

Provides a popup menu to insert page footers.

.uno:ChangeControlType

all

com.sun.star.comp.framework.ControlMenuController

Provides a popup menu to change the control type of a selected form control.

.uno:AvailableToolbars

all

com.sun.star.comp.framework.ToolBarsMenuController

Provides a popup menu to show/hide toolbars.

.uno:ScriptOrganizer

all

com.sun.star.comp.framework.MacrosMenuController

Provides a popup menu with all available scripting languages.

.uno:RecentFileList

all

com.sun.star.comp.framework.RecentFilesMenuController

Provides a popup menu with recently opened files.

.uno:AddDirect

all

com.sun.star.comp.framework.NewMenuController

Provides a popup menu to create document for all available application modules.

.uno:AutoPilotMenu

all

com.sun.star.comp.framework.NewMenuController

Provides a popup menu with all available wizards.

Configuration

The association between command URL and popup menu controller is controlled by an OpenOffice configuration file. The default OpenOffice.org configuration file is located at the following configuration path org.openoffice.Office.UI/Controller.xcu. The Controller.xcu controls the association for popup menu, toolbar and statusbar controllers.

<!DOCTYPE oor:component-schema SYSTEM "../../../../../component-schema.dtd">
<oor:component-schema xmlns:oor="http://openoffice.org/2001/registry" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" oor:name="Controller" 
oor:package="org.openoffice.Office.UI" xml:lang="en-US">
 <info>
  <author>CD</author>
  <desc>Contains implementation of popup menu controllers.</desc>
 </info>
 <templates>
 <group oor:name="ControllerType">
  <info>
   <desc>Describes a controller implementation.</desc>
  </info>
  <prop oor:name="Command" oor:type="xs:string">
   <info>
    <desc>Specifies the command name which the controller is bound to.</desc>
   </info>
  </prop>
  <prop oor:name="Module" oor:type="xs:string">
   <info>
    <desc>Specifies the model that the controller is associated with. An empty string matches every module.</desc>
   </info>
  </prop>
  <prop oor:name="Controller" oor:type="xs:string">
   <info>
    <desc>Specifies the UNO service to use for the specified tuple Command and Module</desc>
   </info>
  </prop>
  <prop oor:name="Value" oor:type="xs:string">
   <info>
    <desc>Specifies a controller specific value which is provided to every controller instance during initialization.</desc>
   </info>
   <value/>
  </prop>
 </group>
</templates>
<component>
 <group oor:name="Registered">
  <info>
   <desc>Contains all registered controllers for user interface elements.</desc>
  </info>
  <set oor:name="PopupMenu" oor:node-type="ControllerType">
   <info>
    <desc>Contains UNO component implementation names that implement popup menu controller which are bound to a command and module name.</desc>
   </info>
  </set>
  <set oor:name="ToolBar" oor:node-type="ControllerType">
   <info>
    <desc>Contains UNO component implementation names that implement toolbar controller which are bound to a command and module name.</desc>
   </info>
  </set>
  <set oor:name="StatusBar" oor:node-type="ControllerType">
   <info>
    <desc>Contains UNO component implementation names that implement status bar controller which are bound to a command and module name.</desc>
   </info>
  </set>
 </group>
</component>
</oor:component-schema>

The important part of the schema is the group "ControllerType". The group defines an association between a single command URL and a controller implementation.

Property Type Description
Command String Contains the command URL for which the controller is registered. The UI element implementation uses the command URL to query for controllers.
Module String Specifies the module for which the association is active. This allows to use different controller implementations for different modules. An empty entry means active for all modules.
Controller String Specifies the implementation name of the controller. The name will be used by the ControllerFactory to create an instance of the controller.
Value String Contains controller specific data which will be provided at creation time. It interpretation of the data depends on the controller implementation.

The following snippet is directly taken from the Controller.xcu of OpenOffice.org 2.0.4. It associates the popup menu controller responsible for the font name list to the command URL ".uno:CharFontName". As the controller is available for all application modules the propery "Module" is empty. The controller doesn't use the "Value" property.

  ...
  <node oor:name="Registered">
    <node oor:name="PopupMenu">
      <node oor:name="c1" oor:op="replace">
        <prop oor:name="Command">
          <value>.uno:CharFontName</value>
        </prop>
        <prop oor:name="Module">
          <value/>
        </prop>
        <prop oor:name="Controller">
          <value>com.sun.star.comp.framework.FontMenuController</value>
        </prop>
      </node>
  ...

Now that we know how a popup menu controller is registered in the OpenOffice configuration environment we can move to the next important part of this tutorial: the UNO services and interfaces involved.

UNO Services and interfaces

A popup menu controller is described by a UNO service called PopupMenuController. It defines what interfaces must be supported. The following code part shows the UNO IDL description of the service.

module com { module sun { module star { module frame {
 
//============================================================================= 
 
/** provides access to a popup menu controller.
 
    A popup menu controller is used to make special functions available to 
    users, which depend on runtime or context specific conditions.
    A typical example for a popup menu controller can be a recent file list 
    implementation which provides a list of latest files that a user has 
    worked on. This list gets changes consistently during a work session.
 
    @since OOo 2.0.0
*/
 
service PopupMenuController
{
    //-------------------------------------------------------------------------
    /** supports functions to initialize and update a popup menu controller
        implementation.
 
        A popup menu controller implementation gets initialized with a 
        com::sun::star::awt::XPopupMenu object. This assures
        that a popup menu controller can be implemented with any UNO based
        language.
    */
    interface com::sun::star::frame::XPopupMenuController;
 
    //-------------------------------------------------------------------------
    /** provides functions to initialize a popup menu controller with 
        specific data which are needed. 
 
        This interface should not directly used. A factory service is responsible to 
        initialize every controller correctly.
 
        A popup menu controller needs at least two additional arguments
        provided as com::sun::star::beans::PropertyValue:
 
        Frame
          specifies the com::sun::star::frame::XFrame instance to which
          the popup menu controller belongs to.
 
        CommandURL
          specifies which popup menu controller should be created.
 
        @see PopupMenuControllerFactory
    */
    interface com::sun::star::lang::XInitialization;
 
    //-------------------------------------------------------------------------
    /** used to brief the popup menu controller with new status information.
 
        A popup menu controller makes special functions available to users which normally 
        depend on the state of other data. This interface is used to send this data
        to a controller implementation.
    */
    interface com::sun::star::frame::XStatusListener;
};
 
}; }; }; };

The service description clearly states that we have to implement at least three different UNO interfaces.
com::sun::star::frame::XPopupMenuController
This interface is the central part of every popup menu controller. It's used by the Apache OpenOffice menu bar implementation to communicate with the controller. Via this interface the popup menu controller receives its popup menu reference and gets notified whenever the content of the popup menu must be refreshed.

com::sun::star::lang::XInitialization
This interfaces guarantees that the popup menu controller gets initialized with vital context information. At least the frame reference is essential to query for the model, dispatch commands in the correct context and register for notifications.

com::sun::star::frame::XStatusListener
A popup menu controller may need to get status information about certain commands. This interfaces assures that notifications can be received.

The following illustrations shows the objects and interfaces involved in a typical popup menu controller use case.

Communication PopupMenuController.png

As one can see at least three different objects are involved communicating with the a popup menu controller instance. The PopupMenuControllerFactory just initializes the popup menu controller instance. It's not further involved during the life-time of a popup menu controller. The MenuBar object provides the vital com::sun::star::awt::XPopupMenu reference which the popup menu controller has to fill. Additionally, the MenuBar is responsible to notify the popup menu controller that it has to update the popup menu contents. Dispatch objects, retrieved from a dispatch provider, notify the popup menu controller whenever the state of a command changes. To be honest, these three interfaces are normally not enough to implement a full-featured popup menu controller. You need more interfaces to communicate with your popup menu, receive notifications from other components etc.

We know which interfaces our popup menu controller must implement. The next chapter will describe the typical internals of a popup menu controller and provide a real popup menu controller skeleton implementation. This will be the base for our future effort to create a new recent file list controller.

How to implement a popup menu controller

Before we start to create our code skeleton we should have a look at some internals every popup menu controller contains. The following chapter describes some typical work flows and how a popup menu controller processes these requests.

How does a popup menu controller work inside

The following illustration shows the normal process flow in a popup menu controller when it's called via com::sun::star::frame::XPopupMenuController by the menu bar implementation.

XPopupMenuController and PopupMenuController.png

The popup menu controller first will be called via XPopupMenuController::setPopupMenu( rPopupMenu ). The controller must store the popup menu reference for future use and should start to fill it with the current content. The controller should check that the frame reference has been set by the menu bar implementation using initialize( Sequence< Any > aArgs ). The menu bar implementation will call updateMenu() whenever the controller has to update the popup menu content.

The controller should NEVER update the popup menu content outside the setPopupMenu(...)/updateMenu() calls! The menu bar implementation calls updateMenu() short before the user can access it. The performance of OpenOffice can be decreased and hurt user experience if a controller ignores this advice.

The next illustration shows the normal process flow when a user opens the popup menu and selects an entry.

PopupMenuController PopupMenu.png

The menu bar implementation will notify the popup menu controller implementation with updateMenu() to update the content of the popup menu. If the user opens the popup menu the XMenuListener::activate( MenuEvent) notification is sent, that's the last moment to update the state of the popup menu entries. If the user selects an entry XMenuListener::select( MenuEvent ) will be called. The code has to determine which menu item has been selected and should execute the command.

The popup menu controller skeleton

Now it's time to look how we can implement a popup menu controller. One of first things we have to think about is what interfaces we have to support in our implementation. From the first chapter we know that at least three interfaces must be supported.

  • com::sun::star::frame::XPopupMenuController
  • com::sun::star::lang::XInitialization
  • com::sun::star::frame::XStatusListener

The controller has to communicate with the popup menu to get notified whenever the user selects an entry. This is done using the awt toolkit listener XMenuListener.

  • com::sun::star::awt::XMenuListener

Another two interface important for core reflection/inspection should also be implemented.

  • com::sun::star::lang::XServiceInfo
  • com::sun::star::lang::XTypeProvider

The com::sun::star::lang::XTypeProvider interface is already implemented by our base class ::cppu::WeakImplHelper, so we don't need to implement it.

The header file (popupmenucontroller.h):

//_________________________________________________________________________________________________________________
//	interface includes
//_________________________________________________________________________________________________________________
 
#ifndef _COM_SUN_STAR_LANG_XSERVICEINFO_HPP_
#include <com/sun/star/lang/XServiceInfo.hpp>
#endif
#ifndef _COM_SUN_STAR_LANG_XTYPEPROVIDER_HPP_
#include <com/sun/star/lang/XTypeProvider.hpp>
#endif
#ifndef _COM_SUN_STAR_LANG_XINITIALIZATION_HPP_
#include <com/sun/star/lang/XInitialization.hpp>
#endif
#ifndef _COM_SUN_STAR_LANG_XMULTISERVICEFACTORY_HPP_
#include <com/sun/star/lang/XMultiServiceFactory.hpp>
#endif
#ifndef _COM_SUN_STAR_FRAME_XFRAME_HPP_
#include <com/sun/star/frame/XFrame.hpp>
#endif
#ifndef _COM_SUN_STAR_FRAME_XSTATUSLISTENER_HPP_
#include <com/sun/star/frame/XStatusListener.hpp>
#endif
#ifndef _COM_SUN_STAR_FRAME_XPOPUPMENUCONTROLLER_HPP_
#include <com/sun/star/frame/XPopupMenuController.hpp>
#endif
 
//_________________________________________________________________________________________________________________
//	SDK includes 
//_________________________________________________________________________________________________________________
 
#ifndef _CPPUHELPER_BASEMUTEX_HXX_
#include <cppuhelper/basemutex.hxx>
#endif
#ifndef _CPPUHELPER_IMPLBASE5_HXX_
#include <cppuhelper/implbase5.hxx>
#endif
#ifndef _RTL_USTRING_HXX_
#include <rtl/ustring.hxx>
#endif
 
#define MYPOPUPMENUCONTROLLER_IMPLEMENTATIONNAME "org.openoffice.office.comp.mypopupmenucontroller"
#define MYPOPUPMENUCONTROLLER_SERVICENAME        "com.sun.star.frame.PopupMenuController"
 
class PopupMenuController : 
    ::cppu::BaseMutex                                                     ,
    ::cppu::WeakImplHelper5< ::com::sun::star::lang::XServiceInfo          ,
	                     ::com::sun::star::frame::XPopupMenuController ,
                             ::com::sun::star::lang::XInitialization       ,
                             ::com::sun::star::frame::XStatusListener      ,
                             ::com::sun::star::awt::XMenuListener >
{
    public:
        PopupMenuController( 
            const ::com::sun::star::uno::Reference< ::com::sun::star::lang::XMultiServiceFactory >& xServiceManager );
        virtual ~PopupMenuController();
 
        // XServiceInfo
        virtual ::rtl::OUString SAL_CALL getImplementationName() throw (::com::sun::star::uno::RuntimeException);
        virtual sal_Bool SAL_CALL supportsService( const ::rtl::OUString& ServiceName ) throw (::com::sun::star::uno::RuntimeException);
        virtual ::com::sun::star::uno::Sequence< ::rtl::OUString > SAL_CALL getSupportedServiceNames(  ) throw (::com::sun::star::uno::RuntimeException);
 
        // XPopupMenuController
        virtual void SAL_CALL setPopupMenu( 
            const ::com::sun::star::uno::Reference< ::com::sun::star::awt::XPopupMenu >& PopupMenu ) throw (::com::sun::star::uno::RuntimeException);
        virtual void SAL_CALL updatePopupMenu() throw (::com::sun::star::uno::RuntimeException);
 
        // XInitialization
        virtual void SAL_CALL initialize( 
            const ::com::sun::star::uno::Sequence< ::com::sun::star::uno::Any >& aArguments ) throw (::com::sun::star::uno::Exception, ::com::sun::star::uno::RuntimeException);
 
        // XStatusListener
        virtual void SAL_CALL statusChanged( 
            const ::com::sun::star::frame::FeatureStateEvent& Event ) throw ( ::com::sun::star::uno::RuntimeException );
 
        // XMenuListener
        virtual void SAL_CALL highlight( const ::com::sun::star::awt::MenuEvent& rEvent ) throw (::com::sun::star::uno::RuntimeException);
        virtual void SAL_CALL select( const ::com::sun::star::awt::MenuEvent& rEvent ) throw (::com::sun::star::uno::RuntimeException);
        virtual void SAL_CALL activate( const ::com::sun::star::awt::MenuEvent& rEvent ) throw (::com::sun::star::uno::RuntimeException);
        virtual void SAL_CALL deactivate( const ::com::sun::star::awt::MenuEvent& rEvent ) throw (::com::sun::star::uno::RuntimeException);
 
        // XEventListener
	virtual void SAL_CALL disposing( const com::sun::star::lang::EventObject& Source ) throw ( ::com::sun::star::uno::RuntimeException );
 
        static ::com::sun::star::uno::Reference< ::com::sun::star::uno::XInterface > st_createInstance( const ::com::sun::star::uno::Reference< ::com::sun::star::lang::XMultiServiceFactory >& xSMGR );
 
    private:
        void resetPopupMenu( com::sun::star::uno::Reference< com::sun::star::awt::XPopupMenu >& rPopupMenu );
        void fillPopupMenu( com::sun::star::uno::Reference< com::sun::star::awt::XPopupMenu >& rPopupMenu );
 
        bool                                                                             m_bInitialized;
        rtl::OUString                                                                    m_aCommandURL;
        ::com::sun::star::uno::Reference< ::com::sun::star::frame::XFrame >              m_xFrame;
        ::com::sun::star::uno::Reference< ::com::sun::star::lang::XMultiServiceFactory > m_xServiceManager;
        ::com::sun::star::uno::Reference< ::com::sun::star::awt::XPopupMenu >            m_xPopupMenu;
};

Now the implementation part (recentfilelist.cxx):

#include "popupmenucontroller.h"
 
using namespace ::com::sun::star;
 
PopupMenuController::PopupMenuController( 
    const uno::Reference< lang::XMultiServiceFactory >& xServiceManager ) :
    m_xServiceManager( xServiceManager ),
    m_bInitialized( false )
{
}
 
PopupMenuController::~PopupMenuController()
{
}
 
// XServiceInfo
::rtl::OUString SAL_CALL PopupMenuController::getImplementationName() 
throw (uno::RuntimeException)
{
    return ::rtl::OUString::createFromAscii(MYPOPUPMENUCONTROLLER_IMPLEMENTATIONNAME);
}
 
sal_Bool SAL_CALL PopupMenuController::supportsService( const ::rtl::OUString& sServiceName ) 
throw (uno::RuntimeException)
{
    return sServiceName.equalsAscii( MYPOPUPMENUCONTROLLER_SERVICENAME );
}
 
uno::Sequence< ::rtl::OUString > SAL_CALL PopupMenuController::getSupportedServiceNames() 
throw (uno::RuntimeException)
{
    uno::Sequence< ::rtl::OUString > lNames(1);
    lNames[0] = ::rtl::OUString::createFromAscii(MYPOPUPMENUCONTROLLER_SERVICENAME);
    return lNames;
}
 
// XPopupMenuController
void SAL_CALL PopupMenuController::setPopupMenu( const uno::Reference< awt::XPopupMenu >& xPopupMenu ) 
throw (uno::RuntimeException)
{
    ::osl::ResettableGuard < ::osl::Mutex > aGuard( m_aMutex );
    // We must have a valid frame reference to fill our popup menu
    if ( m_xFrame.is() )
        m_xPopupMenu = xPopupMenu;
    aGuard.clear();
 
    fillPopupMenu( xPopupMenu );
}
 
void SAL_CALL PopupMenuController::updatePopupMenu() throw (uno::RuntimeException)
{
    ::osl::ResettableGuard < ::osl::Mutex > aGuard( m_aMutex );
    uno::Reference< awt::XPopupMenu > xPopupMenu( m_xPopupMenu );
    aGuard.clear();
 
    fillPopupMenu( xPopupMenu );
}
 
// XInitialization
void SAL_CALL PopupMenuController::initialize( const uno::Sequence< uno::Any >& aArguments ) 
throw ( uno::Exception, uno::RuntimeException )
{
    const rtl::OUString aFrameName( RTL_CONSTASCII_USTRINGPARAM( "Frame" ));
    const rtl::OUString aCommandURLName( RTL_CONSTASCII_USTRINGPARAM( "CommandURL" ));
 
    ::osl::ResettableGuard < ::osl::Mutex > aGuard( m_aMutex );
 
    // Retrieve vital information from the provided sequence and store them
    if ( !m_bInitialized )
    {
        rtl::OUString                   aCommandURL;
        beans::PropertyValue            aPropValue;
        uno::Reference< frame::XFrame > xFrame;
 
        for ( int i = 0; i < aArguments.getLength(); i++ )
        {
            if ( aArguments[i] >>= aPropValue )
            {
                if ( aPropValue.Name == aFrameName )
                    aPropValue.Value >>= xFrame;
                else if ( aPropValue.Name == aCommandURLName )
                    aPropValue.Value >>= aCommandURL;
            }
        }
 
        if ( xFrame.is() && aCommandURL.getLength() )
        {
            m_xFrame      = xFrame;
            m_aCommandURL = aCommandURL;
        }
        m_bInitialized = true;
    }
}
 
// XStatusListener
void SAL_CALL PopupMenuController::statusChanged( const frame::FeatureStateEvent& Event ) 
throw ( ::com::sun::star::uno::RuntimeException )
{
    // TODO: check status events
}
 
// XMenuListener
void SAL_CALL PopupMenuController::highlight( 
    const ::com::sun::star::awt::MenuEvent& rEvent ) 
throw (::com::sun::star::uno::RuntimeException)
{
    // DO NOTHING
}
 
void SAL_CALL PopupMenuController::select( 
    const ::com::sun::star::awt::MenuEvent& rEvent ) 
throw (::com::sun::star::uno::RuntimeException)
{
    // TODO: implement menu selection
}
 
void SAL_CALL PopupMenuController::activate( 
    const ::com::sun::star::awt::MenuEvent& rEvent ) 
throw (::com::sun::star::uno::RuntimeException)
{
    // DO NOTHING
}
 
void SAL_CALL PopupMenuController::deactivate( 
    const ::com::sun::star::awt::MenuEvent& rEvent ) 
throw (::com::sun::star::uno::RuntimeException)
{
    // DO NOTHING
}
 
// XEventListener
void SAL_CALL PopupMenuController::disposing( 
    const com::sun::star::lang::EventObject& Source ) 
throw ( ::com::sun::star::uno::RuntimeException )
{
    // TODO: check event source to release disposed references
}
 
// remove all menu entries from our popup menu
void PopupMenuController::resetPopupMenu( 
    com::sun::star::uno::Reference< com::sun::star::awt::XPopupMenu >& rPopupMenu )
{
    // Clear popup menu. Depth first schema to be able to 
    // call removeItem with count at the end.
    if ( rPopupMenu.is() )
    {
        sal_Int16 nCount = rPopupMenu->getItemCount();
        for ( sal_Int16 i = 0; i < nCount; i++ )
        {
            sal_Int16 nItemId = rPopupMenu->getItemId( i );
            if ( nItemId != 0 )
            {
                uno::Reference< awt::XPopupMenu > xSubMenu = rPopupMenu->getPopupMenu( nItemId );
                if ( xSubMenu.is() )
                    resetPopupMenu( xSubMenu );
            }
        }
        rPopupMenu.removeItem( 0, nCount );
    }
}
 
// fill the popup menu with the latest entries
void PopupMenuController::fillPopupMenu( com::sun::star::uno::Reference< com::sun::star::awt::XPopupMenu >& rPopupMenu )
{
    resetPopupMenu();
 
    // TODO: fill popup menu
}
 
// static method to create instance of our recent file list popup menu controller
uno::Reference< uno::XInterface > PopupMenuController::st_createInstance(
    const uno::Reference< lang::XMultiServiceFactory >& xSMGR )
{
    PopupMenuController* pPopupCtrl = new PopupMenuController( xSMGR );
    uno::Reference< uno::XInterface > xListener( static_cast< frame::XPopupMenuController* >( pPopupCtrl ), uno::UNO_QUERY );
    return xListener;
}

Implementing an extended recent file popup menu controller

The previous chapter clear up the general idea behind popup menu controllers and how they fit into the general user interface architecture of OpenOffice. We have a skeleton popup menu controller which can be used as a template for real world implementations. Now we want to use this information to implement a specific popup menu controller. The recent file popup menu controller is a well known part of OpenOffice and source of recurrent discussions about missing features. The following chapters will show how to implement a recent file popup menu controller and then extend it. May be this extended version will be a base for other developers to extend it further or to implement other interesting topics. The code is based on the popup menu controller skeleton which was introduced in one of the previous chapters. The class name is now RecentFileListPopupMenuController.

How to retrieve the list of recent used documents

OpenOffice stores the history of opened documents within the configuration. The information is stored in the Common.xcu configuration file located in the registry folder of every user installation. Let's see how the configuration schema looks like for the document history.

<!DOCTYPE oor:component-schema SYSTEM "../../../../component-schema.dtd">
<oor:component-schema xmlns:oor="http://openoffice.org/2001/registry" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" oor:name="Common" oor:package="org.openoffice.Office" xml:lang="en-US">
    <templates>
        ...
        <group oor:name="HistoryType">
            <prop oor:name="URL" oor:type="xs:string"></prop>
            <prop oor:name="Filter" oor:type="xs:string"></prop>
            <prop oor:name="Title" oor:type="xs:string"></prop>
            <prop oor:name="Password" oor:type="xs:string"></prop>
        </group>
        ...
    </templates>
    <component>
        <group oor:name="History">
            ...
            <prop oor:name="PickListSize" oor:type="xs:int">
                <constraints>
                    <minInclusive oor:value="0"></minInclusive>
                    <maxInclusive oor:value="10"></maxInclusive>
                </constraints>
                <value>10</value>
            </prop>
            ...
            <set oor:name="PickList" oor:node-type="HistoryType">
            </set>
        </group>
    </component>

The recent opened document list is stored in the history group, bound to the set PickList. As one can see the PickList consists of HistoryType nodes. There are two special entry within the property PickListSize which may attract someone. It's the block constraints. Constraints are used within a configuration schema to limit the value range for a property. For the PickListSize property these constraints (0 - 10 entries) limits the use of a recent file list too much. Therefore we decided to change it for OpenOffice.org 2.2 and raise the constraint maxInclusive to 100 entries. The default value (10) will not be changed. This enhanced recent file list popup menu controller will enable users to change the number of entries with an additional menu entry.
To get a better idea how the different properties are used the following XML snippet shows the content of a typical recent opened document list.

...
<node oor:name="PickList">
    <node oor:name="p0" oor:op="replace">
        <prop oor:name="Filter" oor:type="xs:string">
            <value>calc8</value>
        </prop>
        <prop oor:name="Password" oor:type="xs:string">
            <value/>
        </prop>
        <prop oor:name="Title" oor:type="xs:string">
            <value>Weekly Schedule.ods</value>
        </prop>
        <prop oor:name="URL" oor:type="xs:string">
            <value>file:///home/abc/schedule.ods</value>
        </prop>
    </node>
    <node oor:name="p1" oor:op="replace">
        <prop oor:name="Filter" oor:type="xs:string">
            <value>draw8</value>
        </prop>
        <prop oor:name="Password" oor:type="xs:string">
            <value/>
        </prop>
        <prop oor:name="Title" oor:type="xs:string">
            <value>PopupMenuController_PopupMenu</value>
        </prop>
        <prop oor:name="URL" oor:type="xs:string">
            <value>file:///home/abc/documents/PopupMenu.odg</value>
        </prop>
    </node>
</node>
...

The following table describes the different properties of a pick list entry. We have to know which properties are important to load a document via our recent file popup menu controller.

Property Type Description
Filter String Contains the filter which was used to load the associated document.
Password String Deprecated. It's NOT used to store the password to open a password protected document.
Title String The title of the associated document.
URL String Contains the URL to access the document. This can be any URL that can be processed by Apache OpenOffice (e.g. http:, ftp:, file:, etc.)

Reading the OpenOffice history configuration

The layout of the pick list configuration is known, therefore we can now start to implement the read access. Before we can read the configuration data we have to create the root access to the OpenOffice configuration. The following code specifies some strings for the properties and services needed.

namespace
{
static const char CONFIGURATION_PICKLIST_ACCESS[] = "/org.openoffice.Office.Common/History/PickList";
static const char CONFIGURATION_SERVICE[]         = "com.sun.star.configuration.ConfigurationProvider";
static const char CONFIGURATION_READACCESS[]      = "com.sun.star.configuration.ConfigurationAccess";
 
static const char PROPERTY_FILTER[]               = "Filter";
static const char PROPERTY_TITLE[]                = "Title";
static const char PROPERTY_PASSWORD[]             = "Password";
static const char PROPERTY_URL[]                  = "URL";
}

The CONFIGURATION_PICKLIST_ACCESS string defines the path to the root node of the pick list configuration data. It must be provided to the ConfiguraionProvider to get read access. The CONFIGURATION_SERVER string defines the name of the configuration service. The configuration provider service is a XMultiServiceFactory which create configuration access objects. To create a read access object we have to provide the CONFIGURATION_READACCES string. The PROPERTY_ strings define the different names of the pick list properties. We need them to retrieve the property values via getByName.

The following code snippets show how the different strings are used to get configuration access and to read the pick list data. The recent file popup menu controller extension work on-demand, means that only code is execute when needed. The next method initializes the configuration access and adds a container listener to retrieve notifications about changes.

void RecentFileListPopupMenuController::initConfiguration()
{
    ::osl::ResettableGuard < ::osl::Mutex > aGuard( m_aMutex );
    uno::Reference< lang::XMultiServiceFactory > xSrvMgr( m_xServiceManager );
    bool bConfigAccessInit( m_bConfigAccessInit );
    aGuard.clear();
 
    if ( !bConfigAccessInit )
    {
        aGuard.reset();
        m_bConfigAccessInit = true;
        aGuard.clear();
 
        try
        {
            // Create configuration provider. It's our gateway to get read access
            // to the pick list configuration data.
            uno::Reference< lang::XMultiServiceFactory > xConfigProvider;
            xConfigProvider = uno::Reference< lang::XMultiServiceFactory >(
                                xSrvMgr->createInstance( 
                                    ::rtl::OUString( RTL_CONSTASCII_USTRINGPARAM( CONFIGURATION_SERVICE ))),
                                uno::UNO_QUERY_THROW );
 
            // We have to specify the node path to the data we want
            // to have access. This must be done using the PropertyValue
            // "nodepath".
            beans::PropertyValue aPropValue;
            uno::Sequence< uno::Any > aArgs( 1 );
 
            ::rtl::OUString aNodePath( RTL_CONSTASCII_USTRINGPARAM( CONFIGURATION_PICKLIST_ACCESS ));
 
            aPropValue.Name  = rtl::OUString( RTL_CONSTASCII_USTRINGPARAM( "nodepath" ));
            aPropValue.Value = uno::makeAny( aNodePath );
            aArgs[0] = uno::makeAny( aPropValue );
 
            // Create a read access to the specified node path.
            uno::Reference< container::XNameAccess > xNameAccess( 
                            xConfigProvider->createInstanceWithArguments( 
                                rtl::OUString( RTL_CONSTASCII_USTRINGPARAM( CONFIGURATION_READACCESS )), 
                            aArgs ), uno::UNO_QUERY_THROW );
 
            // Add as container listener to get notifications when 
            // something has been changed. So we update the popup 
            // only on-demand.
            uno::Reference< container::XContainer > xContainer( m_xConfigAccess, uno::UNO_QUERY );
            if ( xContainer.is() )
                xContainer->addContainerListener( 
                    uno::Reference< container::XContainerListener >( 
                        static_cast< ::cppu::OWeakObject* >( this ), uno::UNO_QUERY ));
 
            aGuard.reset();
            m_xConfigAccess = xNameAccess;
            aGuard.clear();
        }
        catch ( uno::Exception& )
        {
        }
    }
}

With RecentFileListPopupMenuController::initConfiguration() we now have configuration access. Now begins the exciting part to read the data from pick list. The following method called RecentFileListPopupMenuController::updateFileHistory() takes the config access (m_xConfigAccess), tries to read all entries and store them into a std::vector (m_aRecentFileMenu).

bool RecentFileListPopupMenuController::updateFileHistory()
{
    initConfiguration();
 
    ::osl::ResettableGuard < ::osl::Mutex > aGuard( m_aMutex );
    uno::Reference< container::XNameAccess > xNameAccess( m_xConfigAccess );
    bool bUpdateHistory( m_bUpdateHistory );
    aGuard.clear();
 
    if ( xNameAccess.is() && bUpdateHistory )
    {
        try
        {
            // Retrieve all elements from the pick list configuration set.
            uno::Sequence< ::rtl::OUString > aEntries = xNameAccess->getElementNames();
            sal_uInt32                       nCount   = aEntries.getLength();
            if ( nCount > 0 )
            {
                ::rtl::OUString aFilterPropName( RTL_CONSTASCII_USTRINGPARAM( PROPERTY_FILTER ));
                ::rtl::OUString aTitlePropName( RTL_CONSTASCII_USTRINGPARAM( PROPERTY_TITLE ));
                ::rtl::OUString aPasswordPropName( RTL_CONSTASCII_USTRINGPARAM( PROPERTY_PASSWORD ));
                ::rtl::OUString aURLPropName( RTL_CONSTASCII_USTRINGPARAM( PROPERTY_URL ));
 
                RecentFileMenuEntries aRecentFileMenuEntries;
                for ( sal_uInt32 i = 0; i < nCount; i++ )
                {
                    try
                    {
                        // Use element name to access the properties of that element
                        uno::Reference< container::XNameAccess > xPropAccess(
                            xNameAccess->getByName( aEntries[i] ), uno::UNO_QUERY );
                        if ( xPropAccess.is() )
                        {
                            uno::Any aFilter = xPropAccess->getByName( aFilterPropName );
                            uno::Any aTitle  = xPropAccess->getByName( aTitlePropName );
                            uno::Any aPasswd = xPropAccess->getByName( aPasswordPropName );
                            uno::Any aURL    = xPropAccess->getByName( aURLPropName );
 
                            RecentFileEntry aEntry;
                            aFilter >>= aEntry.aFilterName;
                            aTitle  >>= aEntry.aTitle;
                            aPasswd >>= aEntry.aPassword;
                            aURL    >>= aEntry.aURL;
 
                            // store data into local vector
                            aRecentFileMenuEntries.push_back( aEntry );
                        }
                    }
                    catch ( container::NoSuchElementException& )
                    {
                    }
                }
 
                aGuard.reset();
                // Copy back to internal data using guard
                m_aRecentFileMenu = aRecentFileMenuEntries;
                m_bUpdateHistory  = sal_False;
                aGuard.clear();
            }
        }
        catch ( uno::Exception& )
        {
        }
    }
 
    return bUpdateHistory;
}

Notifications about changes in the configuration

It's good practice when code is only processed when it's needed. Therefore we have to know whenever the configuration data of the pick list has been changed by Apache OpenOffice. The configuration supports notification and calls registered listeners whenever necessary. The following code snippet shows how to add a configuration listener.

// Add as container listener to get notifications when 
// something has been changed. So we update the popup 
// only on-demand.
uno::Reference< container::XContainer > xContainer( m_xConfigAccess, uno::UNO_QUERY );
if ( xContainer.is() )
    xContainer->addContainerListener( 
        uno::Reference< container::XContainerListener >( 
            static_cast< ::cppu::OWeakObject* >( this ), uno::UNO_QUERY ));

We just have to query for the com::sun::star::container::XContainerListener interface and call addContainerListener to add our object as listener to the configuration node. The configuration implementation will call us whenever something within our set has been changed.
Attention: Your implementation will also be called when it changes the configuration via API. You have to make sure that your implementation is prepared to be called back.

The next code snippet show how to implement the configuration listener interface.

// XContainerListener
void SAL_CALL RecentFileListPopupMenuController::elementInserted( 
    const ::com::sun::star::container::ContainerEvent& /*aEvent*/ ) 
throw(uno::RuntimeException)
{
    ::osl::ResettableGuard < ::osl::Mutex > aGuard( m_aMutex );
    m_bUpdateHistory = sal_True;
}
 
void SAL_CALL RecentFileListPopupMenuController::elementRemoved ( 
    const ::com::sun::star::container::ContainerEvent& /*aEvent*/ ) 
throw(uno::RuntimeException)
{
    ::osl::ResettableGuard < ::osl::Mutex > aGuard( m_aMutex );
    m_bUpdateHistory = sal_True;
}
 
void SAL_CALL RecentFileListPopupMenuController::elementReplaced( 
    const ::com::sun::star::container::ContainerEvent& /*aEvent*/ ) 
throw(uno::RuntimeException)
{
    ::osl::ResettableGuard < ::osl::Mutex > aGuard( m_aMutex );
    m_bUpdateHistory = sal_True;
}

The implementation of the listener interface just sets a member so the we retrieve the new pick list content on updatePopupMenu().

How to handle the popup menu

This chapter describes how the popup menu controller should handle the XPopupMenuController interface, fill the popup menu and handle the selection of menu items.

Communication with the menu bar implementation (com.sun.star.frame.XPopupMenuController)

The controller gets the reference to its own popup menu via the setPopupMenu(...) call provided by the interface com.sun.star.frame.XPopupMenuController. The central function to fill the popup menu is updatePopupMenu(...) which is called by the Apache OpenOffice menu bar implementation whenever the content must be refreshed. The following code part shows how to implement the setPopupMenu(...) and updatePopupMenu() function.

// XPopupMenuController
void SAL_CALL RecentFileListPopupMenuController::setPopupMenu( const uno::Reference< awt::XPopupMenu >& xPopupMenu ) 
throw (uno::RuntimeException)
{
    ::osl::ResettableGuard < ::osl::Mutex > aGuard( m_aMutex );
    // We must have a valid frame reference to fill our popup menu
    if ( m_xFrame.is() )
        m_xPopupMenu = xPopupMenu;
    aGuard.clear();
 
    // Add a listener to our popup menu
    if ( xPopupMenu.is() )
    {
        xPopupMenu->addMenuListener( uno::Reference< awt::XMenuListener >( 
            static_cast< cppu::OWeakObject* >( this ), uno::UNO_QUERY ));
 
        fillPopupMenu( xPopupMenu );
    }
}
 
void SAL_CALL RecentFileListPopupMenuController::updatePopupMenu() throw (uno::RuntimeException)
{
    ::osl::ResettableGuard < ::osl::Mutex > aGuard( m_aMutex );
    uno::Reference< awt::XPopupMenu > xPopupMenu( m_xPopupMenu );
    aGuard.clear();
 
    fillPopupMenu( xPopupMenu );
}

Both implementation uses the internal method fillPopupMenu(...) to fill the popup menu. The setPopupMenu() stores the provided popup menu reference for later use. SetPopupMenu will only be called once for a menu bar instance and therefore the controller is responsible to store the reference. The implementation also adds itself as a listener for menu events using the addMenuListener function. The event handling will be described in the next chapter Handle menu selections.

Filling the popup menu

Now we can make a further step and try to fill the popup menu with the entries of the history options we retrieved in the previous chapter.

We have to think about some problems we can encounter using pick list options information in our menu. Normally a recent file list uses the path to a document as a title for a menu entry. Using this information directly is not really possible:

  • The path can be very long which degrades the handling of the popup menu. We should maintain a maximal width and create abbreviated paths where the important parts will still be available.
  • The pick list uses encoded URLs to store the location of a document. This information is not a human readable format, we have to transform them into a user interface form.

To solve these problem the recent file list controller uses other services and implementation I want to describe now.

Abbreviation

OpenOffice supports a abbreviation services which can abbreviate certain URIs including http and ftp. This service is available from OpenOffice.org 2.2 on. The abbreviated URI will also be decoded and can therefore be directly used for user interface purposes.

#ifndef __com_sun__star_util_UriAbbreviation_idl__
#define __com_sun__star_util_UriAbbreviation_idl__
 
#ifndef __com_sun_star_util_XStringAbbreviation_idl__
#include <com/sun/star/util/XStringAbbreviation.idl>
#endif
 
module com { module sun { module star { module util {
 
//============================================================================
/** Abbreviate arbitrary URIs.
 
    <p>An abbreviation implementation that is specialized to URIs.</p>
 
    @see XStringAbbreviation
    @since OOo 2.2
 */
    service UriAbbreviation: XStringAbbreviation;
 
}; }; }; };
 
#endif

The service implements the interface com.sun.star.util.XStringAbbreviation which we can see in the following code part:

#ifndef __com_sun_star_util_XStringAbbreviation_idl__
#define __com_sun_star_util_XStringAbbreviation_idl__
 
#ifndef __com_sun_star_uno_XInterface_idl__
#include <com/sun/star/uno/XInterface.idl>
#endif
 
module com { module sun { module star { module util {
     published interface XStringWidth;
}; }; }; };
 
module com { module sun { module star { module util {
 
//============================================================================
/** Abbreviate arbitrary strings.
 
    <p>It is expected that there will be different implementations of this
    interface, that each expect strings conforming to a certain structure
    (e.g., URIs, platform-specific file paths, or newsgroup names).  The
    abbreviation algorithms will then take into account the structural
    information.</p>
 
    @see XStringWidth
 */
published interface XStringAbbreviation: com::sun::star::uno::XInterface
{
    //------------------------------------------------------------------------
    /** Abbreviate a string, so that the resulting abbreviated string is not
        wider than some given width.
 
        <p>The width of a string is an abstract concept here, measured via
        an <type>XStringWidth</type> interface.  Examples are the number of
        characters in the string (<type>XStringWidth</type> will measure the
        string's length), or the width in pixel when displayed with a specific
        font (which <type>XStringWidth</type> would encapsulate).</p>
 
        @param xStringWidth
        The interface that makes concrete the abstract notion of string width.
 
        @param nWidth
        The resulting abbreviated string's width will be no larger than this.
 
        @param aString
        The string that is abbreviated.
 
        @returns
        an abbreviated string.
     */
    string abbreviateString([in] XStringWidth xStringWidth,
                            [in] long nWidth,
                            [in] string aString);
};
 
}; }; }; };
 
#endif

The function abbreviateString uses a callback to retrieve the with of a sub-string. This is needed as com.sun.star.util.XStringAbbreviation implementation should be usable for many use cases. One use case could be that the width of the string is not determined by the number of characters but due to the type of characters (e.g. variable character width and graphical text output). Therefore the width must be determined by an implementation which has the context information. The following code part shows the com.sun.star.util.XStringWidth interface definition.

#ifndef __com_sun_star_util_XStringWidth_idl__
#define __com_sun_star_util_XStringWidth_idl__
 
#ifndef __com_sun_star_uno_XInterface_idl__
#include <com/sun/star/uno/XInterface.idl>
#endif
 
module com { module sun { module star { module util {
 
//============================================================================
/** An interface that encapsulates the abstract notion of string width.
 
    @see XStringAbbreviation
 */
published interface XStringWidth: com::sun::star::uno::XInterface
{
    //------------------------------------------------------------------------
    /** compute the width of a given string.
 
        <p>Depending on the implementation of this interface, the width of a
        string can be rather different things, like the number of characters
        in the string, or the width in pixel when displayed with a specific
        font.</p>
 
        @param aString
        The string that is to be measured.
 
        @returns
        the string's width.
     */
    long queryStringWidth([in] string aString);
};
 
}; }; }; };
 
#endif

The recent file list popup menu controller uses the com.sun.star.util.UriAbbreviation service to abbreviate all URIs excluding the file-protocol. File-protocol URIs are handled by a special osl function which will be described. The recent file list popup menu controller has a special method called initAbbreviation() to lazy initialize the URI abbreviation service. The next code snippet shows this internal method.

void RecentFileListPopupMenuController::initAbbreviation()
{
    ::osl::ResettableGuard < ::osl::Mutex > aGuard( m_aMutex );
    if ( !m_bUriAbbreviationInit )
    {
        uno::Reference< lang::XMultiServiceFactory > xSrvMgr( m_xServiceManager );
        m_bUriAbbreviationInit = true;
        aGuard.clear();
 
        try
        {
            uno::Reference< uno::XComponentContext > xContext;
            uno::Reference< beans::XPropertySet > xPropSet( xSrvMgr, uno::UNO_QUERY_THROW );
 
            uno::Any a = xPropSet->getPropertyValue( rtl::OUString( RTL_CONSTASCII_USTRINGPARAM( "DefaultContext" )) );
            a >>= xContext;
            uno::Reference< util::XStringAbbreviation > xUriAbbreviation( 
                util::UriAbbreviation::create( xContext ));
 
            aGuard.reset();
            m_xUriAbbreviation = xUriAbbreviation;
            aGuard.clear();
        }
        catch ( uno::Exception& )
        {
        }
    }
}

The following two code snippets show how the recent file list controller uses the uri abbreviation service to abbreviate non-file URLs.

...
osl::FileBase::RC nResult = osl::FileBase::getSystemPathFromFileURL( 
    aRecentFileMenuEntries[i].aURL, aSysPath );
if ( nResult == osl::FileBase::E_None )
{
    ...
}
else if ( xUriAbbreviation.is() )
{
    uno::Reference< util::XStringWidth > xStringWidth;
 
    aGuard.reset();
    if ( !m_xStringWidth.is() )
    {
        m_xStringWidth = uno::Reference< util::XStringWidth >(
        static_cast< ::cppu::OWeakObject* >( new RecentFilesStringLength() ), uno::UNO_QUERY );
    }
    xStringWidth = m_xStringWidth;
    aGuard.clear();
 
    // abbreviate URI using the special URI service
    aTitle += xUriAbbreviation->abbreviateString( xStringWidth, 
                                                  MAX_CHARS_FOR_RECENTFILEMENU, 
                                                  aRecentFileMenuEntries[i].aURL );
}
...

This code snippet shows the callback implementation needed by the abbreviateString(...) function. The implementation just returns the length of the given string.

//_________________________________________________________________________________________________________________
//	Helper class to provide correct string length
//_________________________________________________________________________________________________________________
 
class RecentFilesStringLength : public ::cppu::WeakImplHelper1< util::XStringWidth >
{
    public:
        RecentFilesStringLength() {}
        virtual ~RecentFilesStringLength() {}
 
    // XStringWidth
    sal_Int32 SAL_CALL queryStringWidth( const ::rtl::OUString& aString )
        throw ( uno::RuntimeException )
    {
        return aString.getLength();
    }
};

Transforming URLs to a human readable format

As stated in a previous chapter the recent file list in stored in the configuration using URLs. The abbreviation of non file-URLs already transform them into a human readable form, therefore we only have to take care for file-URLs. The following short code snippet from the recentfilelist popup menu controller shows how to transform these URLs into a system path, which can be read by humans.

    ...
    ::rtl::OUString aSysPath;
    ::rtl::OUString aTitle( aMenuShortCut );
 
    osl::FileBase::RC nResult = osl::FileBase::getSystemPathFromFileURL( 
                                    aRecentFileMenuEntries[i].aURL, aSysPath );
    if ( nResult == osl::FileBase::E_None )
    {
        ::rtl::OUString aCompactSysPath;
         oslFileError nError = osl_abbreviateSystemPath( aSysPath.pData, 
                                                         &aCompactSysPath.pData, 
                                                         MAX_CHARS_FOR_RECENTFILEMENU, 
                                                         NULL );
         if ( !nError )
             aTitle += aCompactSysPath;
         else
             aTitle += aSysPath;
    }
    ...

Localization Resources

If an extension needs localized strings to provide them to the user interface (e.g. menus, message boxes, …) it currently (OpenOffice.org 2.2) must use its own configuration schema and data file. The following chapter shows how to create these file and how to access them using the OpenOffice configuration API.
First we have to create our own configuration schema which supports localized strings. You can see the schema file used by the recent file list popup menu controller below.

<!DOCTYPE oor:component-schema SYSTEM "../../../../../component-schema.dtd">
<oor:component-schema xmlns:oor="http://openoffice.org/2001/registry" 
 xmlns:xs="http://www.w3.org/2001/XMLSchema" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 oor:name="RecentFileList" 
 oor:package="org.openoffice.Office.UI.Custom" 
 xml:lang="en-US">
 <info>
  <author>CD</author>
  <desc>Contains recent file list properties, including localization strings.</desc>
 </info>
 <templates/>
 <component>
  <group oor:name="Strings">
   <info>
    <desc>UI Strings for the Recentfilelist popup menu controller</desc>
   </info>
   <prop oor:name="NoDocuments" oor:type="xs:string" oor:localized="true">
    <value/>
   </prop>
   <prop oor:name="ResetList" oor:type="xs:string" oor:localized="true">
    <value/>
   </prop>
  </group>
 </component>
</oor:component-schema>

The schema defines a group called Strings where we have two properties called NoDocuments and ResetList. Both properties use the type string and have the important attribute oor:localized set to true. That means that the properties are language dependent and the OpenOffice configuration provides the correct language dependent entry on read access. That's all we need for our userinterface. The configuration data file which includes the language dependent strings can be seen in the next snippet.

<!DOCTYPE oor:component-data SYSTEM "../../../../../component-update.dtd">
<oor:component-data oor:name="RecentFileList" 
 oor:package="org.openoffice.Office.UI.Custom" 
 xmlns:oor="http://openoffice.org/2001/registry" 
 xmlns:xs="http://www.w3.org/2001/XMLSchema" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 <node oor:name="Strings">
  <prop oor:name="NoDocuments">
   <value xml:lang="en-US">No Documents</value>
   <value xml:lang="de">Keine Dokumente</value>
  </prop>
  <prop oor:name="ResetList">
   <value xml:lang="en-US">Reset List Now</value>
   <value xml:lang="de">Liste jetzt löschen</value>
  </prop>
 </node>
</oor:component-data>

The configuration data file just have the English and German strings for both of our user interface strings. Translations for other languages can be added using the known ISO language codes. The recentfilelist popup menu controller has to retrieve the string values from the OpenOffice configuration which is done by the helper function called impl_ReadStringEntry(...) which you can find below.

static ::rtl::OUString impl_ReadStringEntry(
  const ::rtl::OUString& rPropName,
  uno::Reference< lang::XMultiServiceFactory > xSMGR )
{
  const ::rtl::OUString aNodePath( RTL_CONSTASCII_USTRINGPARAM( "/org.openoffice.Office.UI.Custom.RecentFileList/Strings" ));
 
  ::rtl::OUString aRet;
  try
  {
    uno::Reference< lang::XMultiServiceFactory > xConfigurationProvider(xSMGR->createInstance(
    ::rtl::OUString( RTL_CONSTASCII_USTRINGPARAM( "com.sun.star.configuration.ConfigurationProvider"))),
      uno::UNO_QUERY);
 
    uno::Sequence< uno::Any > aArgs(1);
    aArgs[0] <<= aNodePath;
 
    uno::Reference< uno::XInterface > xIFace = xConfigurationProvider->createInstanceWithArguments(
    ::rtl::OUString(RTL_CONSTASCII_USTRINGPARAM( "com.sun.star.configuration.ConfigurationReadAccess")),
      aArgs);
 
    uno::Reference< container::XNameAccess> xDirectAccess( xIFace, uno::UNO_QUERY );
    if( xDirectAccess.is() )
    {
      try
      {        
        uno::Any aAny = xDirectAccess->getByName( rPropName );
        aAny >>= aRet;
      }
      catch(uno::Exception&)
      {
      }        
    }
  }
  catch(uno::Exception&)
  {
  }
 
  return aRet;
}

The code creates an OpenOffice configuration provider object via the service manager using the configuration service name com.sun.star.configuration.ConfigurationProvider. The configuration provider is a factory to create access objects for configuration nodes. Creating access objects is done by the well known createInstanceWithArguments method where the access point must be provided as a string value (see aNodePath). We use the interface com::sun::star::container::XNameAccess to retrieve the values for our strings. It's easy to access a single property value as we only have to provide the property name and can directly call getByName( aPropName ). The value must be translated from an com.sun.star.uno.Any to the correct type, in our case a string.
That's all we need to have localization support for our recentfilelist popup menu controller.

Full featured fillPopupMenu() method

The next code part shows the full fillPopupMenu(...) method implementation. It uses a default menu item width of 46 characters. This should be a good compromise for people who want to see enough information and people who hates wide popup menus.

// fill the popup menu with the latest entries
void RecentFileListPopupMenuController::fillPopupMenu( 
    const com::sun::star::uno::Reference< com::sun::star::awt::XPopupMenu >& rPopupMenu )
{
    const sal_Int32 MAX_CHARS_FOR_RECENTFILEMENU = 46;
 
    // check/retrieve updates
    if ( rPopupMenu.is() && updateFileHistory() )
    {
        // clear menu before we can fill it again
        resetPopupMenu( rPopupMenu );
 
        // make sure abbreviation service is ready
        initAbbreviation();
 
        ::osl::ResettableGuard < ::osl::Mutex > aGuard( m_aMutex );
        bool bUriAbbreviationInit( m_bUriAbbreviationInit );
        RecentFileMenuEntries aRecentFileMenuEntries = m_aRecentFileMenu;
        uno::Reference< lang::XMultiServiceFactory > xSrvMgr( m_xServiceManager );
        uno::Reference< util::XStringAbbreviation > xUriAbbreviation( m_xUriAbbreviation );
        aGuard.clear();
 
        sal_uInt32 nCount = aRecentFileMenuEntries.size();
        if ( nCount > 0 )
        {
            uno::Reference< awt::XMenuExtended > xMenuExtended( rPopupMenu, uno::UNO_QUERY );
            for ( sal_uInt32 i = 0; i < nCount; i++ )
            {
                // shortcut buffer to have quick access to the
                // first 10 entries
                char menuShortCut[5] = "~n: ";
 
                ::rtl::OUString aMenuShortCut;
                if ( i <= 9 )
                {
                    if ( i == 9 )
                        aMenuShortCut = rtl::OUString( RTL_CONSTASCII_USTRINGPARAM( "1~0: " ));
                    else
                    {
                        menuShortCut[1] = (char)( '1' + i );
                        aMenuShortCut = rtl::OUString( RTL_CONSTASCII_USTRINGPARAM( menuShortCut ));
                    }
                }
                else
                {
                    aMenuShortCut = rtl::OUString::valueOf((sal_Int32)( i + 1 ));
                    aMenuShortCut += rtl::OUString( RTL_CONSTASCII_USTRINGPARAM( ": " ));
                }
 
                ::rtl::OUString aSysPath;
                ::rtl::OUString aTitle( aMenuShortCut );
 
                osl::FileBase::RC nResult = osl::FileBase::getSystemPathFromFileURL( 
                                                  aRecentFileMenuEntries[i].aURL, aSysPath );
                if ( nResult == osl::FileBase::E_None )
                {
                    ::rtl::OUString aCompactSysPath;
                    oslFileError nError = osl_abbreviateSystemPath( aSysPath.pData, 
                                                                    &aCompactSysPath.pData, 
                                                                    MAX_CHARS_FOR_RECENTFILEMENU, 
                                                                    NULL );
                    if ( !nError )
                        aTitle += aCompactSysPath;
                    else
                        aTitle += aSysPath;
                }
                else if ( xUriAbbreviation.is() )
                {
                    uno::Reference< util::XStringWidth > xStringWidth;
 
                    aGuard.reset();
                    if ( !m_xStringWidth.is() )
                    {
                        m_xStringWidth = uno::Reference< util::XStringWidth >(
                            static_cast< ::cppu::OWeakObject* >( new RecentFilesStringLength() ), uno::UNO_QUERY );
                    }
                    xStringWidth = m_xStringWidth;
                    aGuard.clear();
 
                    // abbreviate URI using the special URI service
                    aTitle += xUriAbbreviation->abbreviateString( xStringWidth, 
                                                                  MAX_CHARS_FOR_RECENTFILEMENU, 
                                                                  aRecentFileMenuEntries[i].aURL );
                }
                else
                {
                    // Fallback solution: Just copy the string to our max size
                    rtl::OUString aTmp( aRecentFileMenuEntries[i].aURL );
                    aTitle += aTmp.copy( 0, std::min( MAX_CHARS_FOR_RECENTFILEMENU, aTmp.getLength() ));
                }
 
                rPopupMenu->insertItem( i+1, aTitle, 0, i );
                xMenuExtended->setCommand( i+1, aRecentFileMenuEntries[i].aURL );
            }
        }
	else
        {
            // Empty history - add disabled "No Documents" menu item
            ::rtl::OUString aTitle = impl_ReadStringEntry( ::rtl::OUString( 
                RTL_CONSTASCII_USTRINGPARAM( "NoDocuments" )), xSrvMgr );
            rPopupMenu->insertItem( 1, aTitle, 0, 0 );
            rPopupMenu->enableItem( 1, sal_False );
        }
    }
}

Handle menu selections

This chapter explains how to handle the menu selections of our dynamic popup menu. The recent file list popup menu controller will be called by the UNO AWT menu implementation via the select method available at com.sun.star.awt.XMenuListener interface. The select method has the following signature:

void SAL_CALL RecentFileListPopupMenuController::select( 
    const ::com::sun::star::awt::MenuEvent& rEvent ) 
throw (::com::sun::star::uno::RuntimeException)
{
}

The argument com.sun.star.awt.MenuEvent of the select method gives more information about the selected menu item.

module com {  module sun {  module star {  module awt {  
 
//============================================================================= 
 
/** specifies a menu event.
 */
published struct MenuEvent: com::sun::star::lang::EventObject
{ 
    //------------------------------------------------------------------------- 
 
    /** contains the item id.
    */
    short MenuId; 
 
};
...

As we can see com.sun.star.awt.MenuEvent contains one member called MenuId. The value of MenuId describes the value we provided to the function insertItem at the first position. The recent file popup menu controller uses values from one consistently ongoing. Therefore MenuId - 1 can be used as an index to the internal vector of stored recent files. You see the implementation in the following code snippet.

void SAL_CALL RecentFileListPopupMenuController::select( 
    const ::com::sun::star::awt::MenuEvent& rEvent ) 
throw (::com::sun::star::uno::RuntimeException)
{
        ...
        bool            bEntryFound( false );
        RecentFileEntry aRecentFile;
 
        sal_Int32 nIndex = rEvent.MenuId-1;        
        aGuard.reset();
        if (( nIndex >= 0 ) && ( nIndex < m_aRecentFileMenu.size() ))
        {
            aRecentFile = m_aRecentFileMenu[ nIndex ];
            bEntryFound = true;
        }
        aGuard.clear();
 
        if ( bEntryFound && xURLTransformer.is() )
        {
            ...
        }
        ...
}

Load the document via UNO dispatch API

The recent file popup menu controller should open documents which a user recently opened ( the OpenOffice history options are only updated when a document has been closed). To open a document the recent file popup menu controller uses the dispatch command .uno:Open which supports several arguments. The following table describes these arguments and their data types.

Argument Type Comment
URL String A URL which locates the document to be loaded.
FilterName String The name of the filter to be used for loading the document.
OpenFlags String
Password String An optional password to open password protected files.
FilterOptions String Options for the filter. Depend on the filter use to load the document.
AsTemplate Boolean Specifies if a template document should be handled as a template or a ordinary document.

The OpenOffice dispatch implementation supports some general arguments which can be seen in the following table.

Argument Type Comment
FrameName String Specifies the frame which should execute the command.
SynchronMode Boolean Specifies if the command should be execute synchronous or asynchronous.

Some of these arguments are needed to implement the document open function within the recent file popup menu controller.

Documentation caution.png Due to limitations within some OpenOffice implementation classes it's vital that commands which can lead to the destruction of the component inside our frame are dispatched with the argument SynchronMode set to false.

The following code snippet shows the full implementation of the RecentFileListPopupMenuController::select(...) method.

void SAL_CALL RecentFileListPopupMenuController::select( 
    const ::com::sun::star::awt::MenuEvent& rEvent ) 
throw (::com::sun::star::uno::RuntimeException)
{
    const static sal_Int32 NUM_OF_PICKLIST_ARGS = 5;
 
    uno::Reference< awt::XPopupMenu >            xPopupMenu;
    uno::Reference< awt::XMenuExtended >         xMenuExtended;
    uno::Reference< frame::XDispatch >           xDispatch;
    uno::Reference< frame::XDispatchProvider >   xDispatchProvider;
    uno::Reference< lang::XMultiServiceFactory > xSrvMgr;
 
    ::osl::ResettableGuard < ::osl::Mutex > aGuard( m_aMutex );
    xPopupMenu        = m_xPopupMenu;
    xMenuExtended     = uno::Reference< awt::XMenuExtended >( xPopupMenu, uno::UNO_QUERY );
    xDispatchProvider = uno::Reference< frame::XDispatchProvider >( m_xFrame, uno::UNO_QUERY );
    xSrvMgr           = m_xServiceManager;
    aGuard.clear();
 
    util::URL aTargetURL;
    uno::Sequence< beans::PropertyValue > aArgsList( NUM_OF_PICKLIST_ARGS );
 
    if ( xPopupMenu.is() && 
         xMenuExtended.is() && 
         xDispatchProvider.is() )
    {
        uno::Reference< util::XURLTransformer > xURLTransformer( 
            xSrvMgr->createInstance( 
                rtl::OUString( RTL_CONSTASCII_USTRINGPARAM( "com.sun.star.util.URLTransformer" ))),
            uno::UNO_QUERY );
 
        bool            bEntryFound( false );
        RecentFileEntry aRecentFile;
 
        sal_Int32 nIndex = rEvent.MenuId-1;        
        aGuard.reset();
        if (( nIndex >= 0 ) && ( nIndex < m_aRecentFileMenu.size() ))
        {
            aRecentFile = m_aRecentFileMenu[ nIndex ];
            bEntryFound = true;
        }
        aGuard.clear();
 
        if ( bEntryFound && xURLTransformer.is() )
        {
			aTargetURL.Complete = ::rtl::OUString( RTL_CONSTASCII_USTRINGPARAM( ".uno:Open" ));
            xURLTransformer->parseStrict( aTargetURL );
 
            // templates in the picklist will never be opened as documents
            sal_Int32 nIndex( 0 );
            aArgsList[nIndex].Name = ::rtl::OUString( RTL_CONSTASCII_USTRINGPARAM( "AsTemplate" ));
            aArgsList[nIndex].Value = uno::makeAny( sal_False );
            ++nIndex;
 
            ::rtl::OUString  aFilter( aRecentFile.aFilterName );
            sal_Int32 nPos = aFilter.indexOf( '|' );
            if ( nPos >= 0 )
            {
                ::rtl::OUString aFilterOptions;
 
                if ( nPos < ( aFilter.getLength() - 1 ) )
		aFilterOptions = aFilter.copy( nPos+1 );
 
	        aArgsList[nIndex].Name  = ::rtl::OUString( RTL_CONSTASCII_USTRINGPARAM( "FilterOptions" ));
                aArgsList[nIndex].Value = uno::makeAny( aFilterOptions );
                ++nIndex;
 
	        aFilter = aFilter.copy( 0, nPos-1 );
                aArgsList.realloc( NUM_OF_PICKLIST_ARGS+1 );
            }
 
	    aArgsList[nIndex].Name = ::rtl::OUString( RTL_CONSTASCII_USTRINGPARAM( "FilterName" ));
            aArgsList[nIndex].Value = uno::makeAny( aFilter );
            ++nIndex;
 
            aArgsList[nIndex].Name = ::rtl::OUString( RTL_CONSTASCII_USTRINGPARAM( "URL" ));
            aArgsList[nIndex].Value = uno::makeAny( xMenuExtended->getCommand( rEvent.MenuId ));
            ++nIndex;
 
            aArgsList[nIndex].Name = ::rtl::OUString( RTL_CONSTASCII_USTRINGPARAM( "SynchronMode" ));
            aArgsList[nIndex].Value = uno::makeAny( sal_False );
            ++nIndex;
 
            aArgsList[nIndex].Name = ::rtl::OUString( RTL_CONSTASCII_USTRINGPARAM( "FrameName" ));
            aArgsList[nIndex].Value = uno::makeAny( ::rtl::OUString( RTL_CONSTASCII_USTRINGPARAM( "_default" )));
 
            const ::rtl::OUString aSelf( RTL_CONSTASCII_USTRINGPARAM( "_self" ));
            xDispatch = xDispatchProvider->queryDispatch( aTargetURL, aSelf, 0 );
 
            if ( xDispatch.is() )
                xDispatch->dispatch( aTargetURL, aArgsList );
        }
    }
}

Extending the recent file list popup menu controller

This chapter discusses the new functions of the recent file list popup menu controller. We want to add the following functions to the built-in popup menu controller.

  • Set the number of recent files in a dialog.
  • Reset the recent file list.

Due to a limited implementation of the recent file list container within OpenOffice (svtools/source/config/historyoptions.cxx) I have to shift the extended functions until this has been fixed. The current implementation doesn't permit to clear the list or change the number of entries at runtime using the OpenOffice configuration API. I will extend this chapter as soon as the recent file list container has been changed.

Where to download the extension and source code

The extension and the source code can be found on the framework web page in the Framework - Framework Core - Examples folder There are two entries in the Examples folder


Please keep in mind that the extension can only be used from OpenOffice.org 2.2 or later. The source code needs an OpenOffice.org SDK 2.2 or later. The extension can be installed on:

  • Windows
  • Linux x86
  • Solaris Sparc
  • Solaris Intel

A Java based source code bundle will be uploaded soon which is based on the OpenOffice.org Netbeans integration which makes developing add-ons, add-ins and UNO code much easier.

Personal tools