1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3  * This file is part of the LibreOffice project.
4  *
5  * This Source Code Form is subject to the terms of the Mozilla Public
6  * License, v. 2.0. If a copy of the MPL was not distributed with this
7  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8  *
9  * This file incorporates work covered by the following license notice:
10  *
11  *   Licensed to the Apache Software Foundation (ASF) under one or more
12  *   contributor license agreements. See the NOTICE file distributed
13  *   with this work for additional information regarding copyright
14  *   ownership. The ASF licenses this file to you under the Apache
15  *   License, Version 2.0 (the "License"); you may not use this file
16  *   except in compliance with the License. You may obtain a copy of
17  *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
18  */
19 
20 #include <strings.hrc>
21 #include <classes/fwkresid.hxx>
22 
23 #include <comphelper/propertyvalue.hxx>
24 #include <cppuhelper/supportsservice.hxx>
25 #include <osl/mutex.hxx>
26 #include <svtools/imagemgr.hxx>
27 #include <svtools/popupmenucontrollerbase.hxx>
28 #include <tools/urlobj.hxx>
29 #include <unotools/historyoptions.hxx>
30 #include <vcl/commandinfoprovider.hxx>
31 #include <vcl/graph.hxx>
32 #include <vcl/settings.hxx>
33 #include <vcl/svapp.hxx>
34 
35 using namespace css;
36 using namespace com::sun::star::uno;
37 using namespace com::sun::star::lang;
38 using namespace com::sun::star::frame;
39 using namespace com::sun::star::beans;
40 using namespace com::sun::star::util;
41 
42 #define MAX_MENU_ITEMS  99
43 
44 namespace {
45 
46 constexpr OUStringLiteral CMD_CLEAR_LIST = u".uno:ClearRecentFileList";
47 constexpr OUStringLiteral CMD_OPEN_AS_TEMPLATE = u".uno:OpenTemplate";
48 constexpr OUStringLiteral CMD_OPEN_REMOTE = u".uno:OpenRemote";
49 
50 class RecentFilesMenuController :  public svt::PopupMenuControllerBase
51 {
52     using svt::PopupMenuControllerBase::disposing;
53 
54 public:
55     RecentFilesMenuController( const uno::Reference< uno::XComponentContext >& xContext,
56                                const uno::Sequence< uno::Any >& args );
57 
58     // XServiceInfo
59     virtual OUString SAL_CALL getImplementationName() override
60     {
61         return "com.sun.star.comp.framework.RecentFilesMenuController";
62     }
63 
64     virtual sal_Bool SAL_CALL supportsService(OUString const & ServiceName) override
65     {
66         return cppu::supportsService(this, ServiceName);
67     }
68 
69     virtual css::uno::Sequence<OUString> SAL_CALL getSupportedServiceNames() override
70     {
71         return {"com.sun.star.frame.PopupMenuController"};
72     }
73 
74     // XStatusListener
75     virtual void SAL_CALL statusChanged( const frame::FeatureStateEvent& Event ) override;
76 
77     // XMenuListener
78     virtual void SAL_CALL itemSelected( const awt::MenuEvent& rEvent ) override;
79     virtual void SAL_CALL itemActivated( const awt::MenuEvent& rEvent ) override;
80 
81     // XDispatchProvider
82     virtual uno::Reference< frame::XDispatch > SAL_CALL queryDispatch( const util::URL& aURL, const OUString& sTarget, sal_Int32 nFlags ) override;
83 
84     // XDispatch
85     virtual void SAL_CALL dispatch( const util::URL& aURL, const uno::Sequence< beans::PropertyValue >& seqProperties ) override;
86 
87     // XEventListener
88     virtual void SAL_CALL disposing( const css::lang::EventObject& Source ) override;
89 
90 private:
91     virtual void impl_setPopupMenu() override;
92     void fillPopupMenu( css::uno::Reference< css::awt::XPopupMenu > const & rPopupMenu );
93     void executeEntry( sal_Int32 nIndex );
94 
95     std::vector< OUString >   m_aRecentFilesItems;
96     bool                      m_bDisabled : 1;
97     bool                      m_bShowToolbarEntries;
98 };
99 
100 RecentFilesMenuController::RecentFilesMenuController( const uno::Reference< uno::XComponentContext >& xContext,
101                                                       const uno::Sequence< uno::Any >& args ) :
102     svt::PopupMenuControllerBase( xContext ),
103     m_bDisabled( false ),
104     m_bShowToolbarEntries( false )
105 {
106     css::beans::PropertyValue aPropValue;
107     for ( uno::Any const & arg : args )
108     {
109         arg >>= aPropValue;
110         if ( aPropValue.Name == "InToolbar" )
111         {
112             aPropValue.Value >>= m_bShowToolbarEntries;
113             break;
114         }
115     }
116 }
117 
118 void InsertItem(const css::uno::Reference<css::awt::XPopupMenu>& rPopupMenu,
119                 const OUString& rCommand,
120                 const css::uno::Reference<css::frame::XFrame>& rFrame)
121 {
122     sal_uInt16 nItemId = rPopupMenu->getItemCount() + 1;
123 
124     if (rFrame.is())
125     {
126         OUString aModuleName(vcl::CommandInfoProvider::GetModuleIdentifier(rFrame));
127         auto aProperties = vcl::CommandInfoProvider::GetCommandProperties(rCommand, aModuleName);
128         OUString aLabel(vcl::CommandInfoProvider::GetPopupLabelForCommand(aProperties));
129         OUString aTooltip(vcl::CommandInfoProvider::GetTooltipForCommand(rCommand, aProperties, rFrame));
130         css::uno::Reference<css::graphic::XGraphic> xGraphic(vcl::CommandInfoProvider::GetXGraphicForCommand(rCommand, rFrame));
131 
132         rPopupMenu->insertItem(nItemId, aLabel, 0, -1);
133         rPopupMenu->setItemImage(nItemId, xGraphic, false);
134         rPopupMenu->setHelpText(nItemId, aTooltip);
135     }
136     else
137         rPopupMenu->insertItem(nItemId, OUString(), 0, -1);
138 
139     rPopupMenu->setCommand(nItemId, rCommand);
140 }
141 
142 
143 // private function
144 void RecentFilesMenuController::fillPopupMenu( Reference< css::awt::XPopupMenu > const & rPopupMenu )
145 {
146     SolarMutexGuard aSolarMutexGuard;
147 
148     resetPopupMenu( rPopupMenu );
149 
150     std::vector< SvtHistoryOptions::HistoryItem > aHistoryList = SvtHistoryOptions::GetList( EHistoryType::PickList );
151 
152     int nPickListMenuItems = std::min<sal_Int32>( aHistoryList.size(), MAX_MENU_ITEMS );
153     m_aRecentFilesItems.clear();
154 
155     if (( nPickListMenuItems > 0 ) && !m_bDisabled )
156     {
157         for ( int i = 0; i < nPickListMenuItems; i++ )
158         {
159             const SvtHistoryOptions::HistoryItem& rPickListEntry = aHistoryList[i];
160             m_aRecentFilesItems.push_back( rPickListEntry.sURL );
161         }
162     }
163 
164     if ( !m_aRecentFilesItems.empty() )
165     {
166         const sal_uInt32 nCount = m_aRecentFilesItems.size();
167         StyleSettings aIconSettings;
168         bool bIsIconsAllowed = aIconSettings.GetUseImagesInMenus();
169 
170         for ( sal_uInt32 i = 0; i < nCount; i++ )
171         {
172 
173             OUStringBuffer aMenuShortCut;
174             if ( i <= 9 )
175             {
176                 if ( i == 9 )
177                     aMenuShortCut.append( "1~0. " );
178                 else
179                 {
180                     aMenuShortCut.append( "~N. " );
181                     aMenuShortCut[ 1 ] = sal_Unicode( i + '1' );
182                 }
183             }
184             else
185             {
186                 aMenuShortCut.append( sal_Int32( i + 1 ) );
187                 aMenuShortCut.append( ". " );
188             }
189 
190             OUString aURLString = "vnd.sun.star.popup:RecentFileList?entry=" + OUString::number(i);
191 
192             // Abbreviate URL
193             OUString   aMenuTitle;
194             INetURLObject   aURL( m_aRecentFilesItems[i] );
195             OUString aTipHelpText( aURL.getFSysPath( FSysStyle::Detect ) );
196 
197             if ( aURL.GetProtocol() == INetProtocol::File )
198             {
199                 // Do handle file URL differently: don't show the protocol, just the file name
200                 aMenuTitle = aURL.GetLastName(INetURLObject::DecodeMechanism::WithCharset);
201             }
202             else
203             {
204                 // In all other URLs show the protocol name before the file name
205                 aMenuTitle   = INetURLObject::GetSchemeName(aURL.GetProtocol()) + ": " + aURL.getName();
206             }
207 
208             aMenuShortCut.append( aMenuTitle );
209 
210             rPopupMenu->insertItem(sal_uInt16( i+1 ), aMenuShortCut.makeStringAndClear(), 0, -1);
211 
212             if ( bIsIconsAllowed ) {
213                 BitmapEx aThumbnail(SvFileInformationManager::GetImageId(aURL, false));
214                 rPopupMenu->setItemImage(sal_uInt16(i + 1), Graphic(aThumbnail).GetXGraphic(), false);
215             }
216 
217             rPopupMenu->setTipHelpText(sal_uInt16(i + 1), aTipHelpText);
218             rPopupMenu->setCommand(sal_uInt16(i + 1), aURLString);
219         }
220 
221         rPopupMenu->insertSeparator(-1);
222         // Clear List menu entry
223         rPopupMenu->insertItem(sal_uInt16(nCount + 1), FwkResId(STR_CLEAR_RECENT_FILES), 0, -1);
224         rPopupMenu->setCommand(sal_uInt16(nCount + 1), CMD_CLEAR_LIST);
225         rPopupMenu->setHelpText(sal_uInt16(nCount + 1), FwkResId(STR_CLEAR_RECENT_FILES_HELP));
226 
227         // Open remote menu entry
228         if ( m_bShowToolbarEntries )
229         {
230             rPopupMenu->insertSeparator(-1);
231             InsertItem(rPopupMenu, CMD_OPEN_AS_TEMPLATE, m_xFrame);
232             InsertItem(rPopupMenu, CMD_OPEN_REMOTE, m_xFrame);
233         }
234     }
235     else
236     {
237         if ( m_bShowToolbarEntries )
238         {
239             InsertItem(rPopupMenu, CMD_OPEN_AS_TEMPLATE, m_xFrame);
240             InsertItem(rPopupMenu, CMD_OPEN_REMOTE, m_xFrame);
241         }
242         else
243         {
244             // Add InsertSeparator(), otherwise it will display
245             // the first item icon of recent files instead of displaying no icon.
246             rPopupMenu->insertSeparator(-1);
247             // No recent documents => insert "no documents" string
248             // Do not disable it, otherwise the Toolbar controller and MenuButton
249             // will display SV_RESID_STRING_NOSELECTIONPOSSIBLE instead of STR_NODOCUMENT
250             rPopupMenu->insertItem(1, FwkResId(STR_NODOCUMENT), static_cast<sal_Int16>(MenuItemBits::NOSELECT), -1);
251         }
252     }
253 }
254 
255 void RecentFilesMenuController::executeEntry( sal_Int32 nIndex )
256 {
257     if (( nIndex < 0 ) ||
258         ( nIndex >= sal::static_int_cast<sal_Int32>( m_aRecentFilesItems.size() )))
259         return;
260 
261     Sequence< PropertyValue > aArgsList{
262         comphelper::makePropertyValue("Referer", OUString( "private:user" )),
263 
264         // documents in the picklist will never be opened as templates
265         comphelper::makePropertyValue("AsTemplate", false),
266 
267         // Type detection needs to know which app we are opening it from.
268         comphelper::makePropertyValue("DocumentService", m_aModuleName)
269     };
270     dispatchCommand( m_aRecentFilesItems[ nIndex ], aArgsList, "_default" );
271 }
272 
273 // XEventListener
274 void SAL_CALL RecentFilesMenuController::disposing( const EventObject& )
275 {
276     Reference< css::awt::XMenuListener > xHolder(this);
277 
278     osl::MutexGuard aLock( m_aMutex );
279     m_xFrame.clear();
280     m_xDispatch.clear();
281 
282     if ( m_xPopupMenu.is() )
283         m_xPopupMenu->removeMenuListener( Reference< css::awt::XMenuListener >(this) );
284     m_xPopupMenu.clear();
285 }
286 
287 // XStatusListener
288 void SAL_CALL RecentFilesMenuController::statusChanged( const FeatureStateEvent& Event )
289 {
290     osl::MutexGuard aLock( m_aMutex );
291     m_bDisabled = !Event.IsEnabled;
292 }
293 
294 void SAL_CALL RecentFilesMenuController::itemSelected( const css::awt::MenuEvent& rEvent )
295 {
296     Reference< css::awt::XPopupMenu > xPopupMenu;
297 
298     {
299         osl::MutexGuard aLock(m_aMutex);
300         xPopupMenu = m_xPopupMenu;
301     }
302 
303     if ( !xPopupMenu.is() )
304         return;
305 
306     const OUString aCommand( xPopupMenu->getCommand( rEvent.MenuId ) );
307 
308     if ( aCommand == CMD_CLEAR_LIST )
309     {
310         SvtHistoryOptions::Clear( EHistoryType::PickList );
311         dispatchCommand(
312             "vnd.org.libreoffice.recentdocs:ClearRecentFileList",
313             css::uno::Sequence< css::beans::PropertyValue >() );
314     }
315     else if ( aCommand == CMD_OPEN_REMOTE )
316     {
317         Sequence< PropertyValue > aArgsList( 0 );
318         dispatchCommand( CMD_OPEN_REMOTE, aArgsList );
319     }
320     else if ( aCommand == CMD_OPEN_AS_TEMPLATE )
321     {
322         Sequence< PropertyValue > aArgsList( 0 );
323         dispatchCommand( CMD_OPEN_AS_TEMPLATE, aArgsList );
324     }
325     else
326         executeEntry( rEvent.MenuId-1 );
327 }
328 
329 void SAL_CALL RecentFilesMenuController::itemActivated( const css::awt::MenuEvent& )
330 {
331     osl::MutexGuard aLock( m_aMutex );
332     impl_setPopupMenu();
333 }
334 
335 // XPopupMenuController
336 void RecentFilesMenuController::impl_setPopupMenu()
337 {
338     if ( m_xPopupMenu.is() )
339         fillPopupMenu( m_xPopupMenu );
340 }
341 
342 // XDispatchProvider
343 Reference< XDispatch > SAL_CALL RecentFilesMenuController::queryDispatch(
344     const URL& aURL,
345     const OUString& /*sTarget*/,
346     sal_Int32 /*nFlags*/ )
347 {
348     osl::MutexGuard aLock( m_aMutex );
349 
350     throwIfDisposed();
351 
352     if ( aURL.Complete.startsWith( m_aBaseURL ) )
353         return Reference< XDispatch >( this );
354     else
355         return Reference< XDispatch >();
356 }
357 
358 // XDispatch
359 void SAL_CALL RecentFilesMenuController::dispatch(
360     const URL& aURL,
361     const Sequence< PropertyValue >& /*seqProperties*/ )
362 {
363     osl::MutexGuard aLock( m_aMutex );
364 
365     throwIfDisposed();
366 
367     if ( !aURL.Complete.startsWith( m_aBaseURL ) )
368         return;
369 
370     // Parse URL to retrieve entry argument and its value
371     sal_Int32 nQueryPart = aURL.Complete.indexOf( '?', m_aBaseURL.getLength() );
372     if ( nQueryPart <= 0 )
373         return;
374 
375     static const OUStringLiteral aEntryArgStr( u"entry=" );
376     sal_Int32 nEntryArg = aURL.Complete.indexOf( aEntryArgStr, nQueryPart );
377     sal_Int32 nEntryPos = nEntryArg + aEntryArgStr.getLength();
378     if (( nEntryArg <= 0 ) || ( nEntryPos >= aURL.Complete.getLength() ))
379         return;
380 
381     sal_Int32 nAddArgs = aURL.Complete.indexOf( '&', nEntryPos );
382     OUString aEntryArg;
383 
384     if ( nAddArgs < 0 )
385         aEntryArg = aURL.Complete.copy( nEntryPos );
386     else
387         aEntryArg = aURL.Complete.copy( nEntryPos, nAddArgs-nEntryPos );
388 
389     sal_Int32 nEntry = aEntryArg.toInt32();
390     executeEntry( nEntry );
391 }
392 
393 }
394 
395 extern "C" SAL_DLLPUBLIC_EXPORT css::uno::XInterface *
396 com_sun_star_comp_framework_RecentFilesMenuController_get_implementation(
397     css::uno::XComponentContext *context,
398     css::uno::Sequence<css::uno::Any> const &args)
399 {
400     return cppu::acquire(new RecentFilesMenuController(context, args));
401 }
402 
403 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
404