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
21#include <unotools/moduleoptions.hxx>
22#include <unotools/dynamicmenuoptions.hxx>
23#include <unotools/historyoptions.hxx>
24#include <rtl/ustring.hxx>
25#include <tools/urlobj.hxx>
26#include <osl/file.h>
27#include <comphelper/sequenceashashmap.hxx>
28#include <sfx2/app.hxx>
29#include <sal/macros.h>
30#include <sfx2/sfxresid.hxx>
31#include <sfx2/strings.hrc>
32#include <vcl/svapp.hxx>
33#include "shutdownicon.hxx"
34
35#include <com/sun/star/util/XStringWidth.hpp>
36
37#include <cppuhelper/implbase.hxx>
38
39#include <set>
40#include <vector>
41
42#include <premac.h>
43#include <objc/objc-runtime.h>
44#include <Cocoa/Cocoa.h>
45#include <postmac.h>
46
47#define MI_OPEN                    1
48#define MI_WRITER                  2
49#define MI_CALC                    3
50#define MI_IMPRESS                 4
51#define MI_DRAW                    5
52#define MI_BASE                    6
53#define MI_MATH                    7
54#define MI_TEMPLATE                8
55#define MI_STARTMODULE             9
56
57@interface QSMenuExecute : NSObject
58{
59}
60-(void)executeMenuItem: (NSMenuItem*)pItem;
61-(void)dockIconClicked: (NSObject*)pSender;
62@end
63
64@implementation QSMenuExecute
65-(void)executeMenuItem: (NSMenuItem*)pItem
66{
67    switch( [pItem tag] )
68    {
69    case MI_OPEN:
70        ShutdownIcon::FileOpen();
71        break;
72    case MI_WRITER:
73        ShutdownIcon::OpenURL( WRITER_URL, "_default" );
74        break;
75    case MI_CALC:
76        ShutdownIcon::OpenURL( CALC_URL, "_default" );
77        break;
78    case MI_IMPRESS:
79        ShutdownIcon::OpenURL( IMPRESS_URL, "_default" );
80        break;
81    case MI_DRAW:
82        ShutdownIcon::OpenURL( DRAW_URL, "_default" );
83        break;
84    case MI_BASE:
85        ShutdownIcon::OpenURL( BASE_URL, "_default" );
86        break;
87    case MI_MATH:
88        ShutdownIcon::OpenURL( MATH_URL, "_default" );
89        break;
90    case MI_TEMPLATE:
91        ShutdownIcon::FromTemplate();
92        break;
93    case MI_STARTMODULE:
94        ShutdownIcon::OpenURL( STARTMODULE_URL, "_default" );
95        break;
96    default:
97        break;
98    }
99}
100
101-(void)dockIconClicked: (NSObject*)pSender
102{
103    (void)pSender;
104    // start module
105    ShutdownIcon::OpenURL( STARTMODULE_URL, "_default" );
106}
107
108@end
109
110bool ShutdownIcon::IsQuickstarterInstalled()
111{
112    return true;
113}
114
115static NSMenuItem* pDefMenu = nil, *pDockSubMenu = nil;
116static QSMenuExecute* pExecute = nil;
117
118static std::set< OUString > aShortcuts;
119
120static NSString* getAutoreleasedString( const OUString& rStr )
121{
122    return [[[NSString alloc] initWithCharacters: reinterpret_cast<unichar const *>(rStr.getStr()) length: rStr.getLength()] autorelease];
123}
124
125namespace {
126
127struct RecentMenuEntry
128{
129    OUString aURL;
130    OUString aFilter;
131    OUString aTitle;
132    OUString aPassword;
133};
134
135class RecentFilesStringLength : public ::cppu::WeakImplHelper< css::util::XStringWidth >
136{
137    public:
138        RecentFilesStringLength() {}
139
140        // XStringWidth
141        sal_Int32 SAL_CALL queryStringWidth( const OUString& aString ) override
142        {
143            return aString.getLength();
144        }
145};
146
147}
148
149@interface RecentMenuDelegate : NSObject <NSMenuDelegate>
150{
151    std::vector< RecentMenuEntry >* m_pRecentFilesItems;
152}
153-(id)init;
154-(void)dealloc;
155-(void)menuNeedsUpdate:(NSMenu *)menu;
156-(void)executeRecentEntry: (NSMenuItem*)item;
157@end
158
159@implementation RecentMenuDelegate
160-(id)init
161{
162    if( (self = [super init]) )
163    {
164        m_pRecentFilesItems = new std::vector< RecentMenuEntry >();
165    }
166    return self;
167}
168
169-(void)dealloc
170{
171    delete m_pRecentFilesItems;
172    [super dealloc];
173}
174
175-(void)menuNeedsUpdate:(NSMenu *)menu
176{
177    // clear menu
178    int nItems = [menu numberOfItems];
179    while( nItems -- )
180        [menu removeItemAtIndex: 0];
181
182    // update recent item list
183    css::uno::Sequence< css::uno::Sequence< css::beans::PropertyValue > > aHistoryList( SvtHistoryOptions().GetList( EHistoryType::PickList ) );
184
185    int nPickListMenuItems = ( aHistoryList.getLength() > 99 ) ? 99 : aHistoryList.getLength();
186
187    m_pRecentFilesItems->clear();
188    if( nPickListMenuItems > 0 )
189    {
190        for ( int i = 0; i < nPickListMenuItems; i++ )
191        {
192            css::uno::Sequence< css::beans::PropertyValue > const & rPickListEntry = aHistoryList[i];
193            RecentMenuEntry aRecentFile;
194
195            for ( const css::beans::PropertyValue& rProp : rPickListEntry )
196            {
197                const css::uno::Any& a = rProp.Value;
198
199                if ( rProp.Name == HISTORY_PROPERTYNAME_URL )
200                    a >>= aRecentFile.aURL;
201                else if ( rProp.Name == HISTORY_PROPERTYNAME_FILTER )
202                    a >>= aRecentFile.aFilter;
203                else if ( rProp.Name == HISTORY_PROPERTYNAME_TITLE )
204                    a >>= aRecentFile.aTitle;
205                else if ( rProp.Name == HISTORY_PROPERTYNAME_PASSWORD )
206                    a >>= aRecentFile.aPassword;
207            }
208
209            m_pRecentFilesItems->push_back( aRecentFile );
210        }
211    }
212
213    // insert new recent items
214    for ( std::vector<RecentMenuEntry>::size_type i = 0; i < m_pRecentFilesItems->size(); i++ )
215    {
216        OUString   aMenuTitle;
217        INetURLObject   aURL( (*m_pRecentFilesItems)[i].aURL );
218
219        if ( aURL.GetProtocol() == INetProtocol::File )
220        {
221            // Do handle file URL differently => convert it to a system
222            // path and abbreviate it with a special function:
223            OUString aSystemPath( aURL.getFSysPath( FSysStyle::Detect ) );
224            OUString aCompactedSystemPath;
225
226            oslFileError nError = osl_abbreviateSystemPath( aSystemPath.pData, &aCompactedSystemPath.pData, 46, nullptr );
227            if ( !nError )
228                aMenuTitle = aCompactedSystemPath;
229            else
230                aMenuTitle = aSystemPath;
231        }
232        else
233        {
234            // Use INetURLObject to abbreviate all other URLs
235            css::uno::Reference< css::util::XStringWidth > xStringLength( new RecentFilesStringLength() );
236            aMenuTitle = aURL.getAbbreviated( xStringLength, 46, INetURLObject::DecodeMechanism::Unambiguous );
237        }
238
239        NSMenuItem* pNewItem = [[NSMenuItem alloc] initWithTitle: getAutoreleasedString( aMenuTitle )
240                                                   action: @selector(executeRecentEntry:)
241                                                   keyEquivalent: @""];
242        [pNewItem setTag: i];
243        [pNewItem setTarget: self];
244        [pNewItem setEnabled: YES];
245        [menu addItem: pNewItem];
246        [pNewItem autorelease];
247    }
248}
249
250-(void)executeRecentEntry: (NSMenuItem*)item
251{
252    sal_Int32 nIndex = [item tag];
253    if( ( nIndex >= 0 ) && ( nIndex < static_cast<sal_Int32>( m_pRecentFilesItems->size() ) ) )
254    {
255        const RecentMenuEntry& rRecentFile = (*m_pRecentFilesItems)[ nIndex ];
256        int NUM_OF_PICKLIST_ARGS = 3;
257        css::uno::Sequence< css::beans::PropertyValue > aArgsList( NUM_OF_PICKLIST_ARGS );
258
259        aArgsList[0].Name = "Referer";
260        aArgsList[0].Value <<= OUString( "private:user" );
261
262        // documents in the picklist will never be opened as templates
263        aArgsList[1].Name = "AsTemplate";
264        aArgsList[1].Value <<= false;
265
266        OUString  aFilter( rRecentFile.aFilter );
267        sal_Int32 nPos = aFilter.indexOf( '|' );
268        if ( nPos >= 0 )
269        {
270            OUString aFilterOptions;
271
272            if ( nPos < ( aFilter.getLength() - 1 ) )
273                aFilterOptions = aFilter.copy( nPos+1 );
274
275            aArgsList[2].Name = "FilterOptions";
276            aArgsList[2].Value <<= aFilterOptions;
277
278            aFilter = aFilter.copy( 0, nPos-1 );
279            aArgsList.realloc( ++NUM_OF_PICKLIST_ARGS );
280        }
281
282        aArgsList[NUM_OF_PICKLIST_ARGS-1].Name = "FilterName";
283        aArgsList[NUM_OF_PICKLIST_ARGS-1].Value <<= aFilter;
284
285        ShutdownIcon::OpenURL( rRecentFile.aURL, "_default", aArgsList );
286    }
287}
288@end
289
290static RecentMenuDelegate* pRecentDelegate = nil;
291
292static OUString getShortCut( const OUString& i_rTitle )
293{
294    // create shortcut
295    OUString aKeyEquiv;
296    for( sal_Int32 nIndex = 0; nIndex < i_rTitle.getLength(); nIndex++ )
297    {
298        OUString aShortcut( i_rTitle.copy( nIndex, 1 ).toAsciiLowerCase() );
299        if( aShortcuts.find( aShortcut ) == aShortcuts.end() )
300        {
301            aShortcuts.insert( aShortcut );
302            aKeyEquiv = aShortcut;
303            break;
304        }
305    }
306
307    return aKeyEquiv;
308}
309
310static void appendMenuItem( NSMenu* i_pMenu, NSMenu* i_pDockMenu, const OUString& i_rTitle, int i_nTag, const OUString& i_rKeyEquiv )
311{
312    if( ! i_rTitle.getLength() )
313        return;
314
315    NSMenuItem* pItem = [[NSMenuItem alloc] initWithTitle: getAutoreleasedString( i_rTitle )
316                                            action: @selector(executeMenuItem:)
317                                            keyEquivalent: (i_rKeyEquiv.getLength() ? getAutoreleasedString( i_rKeyEquiv ) : @"")
318                        ];
319    [pItem setTag: i_nTag];
320    [pItem setTarget: pExecute];
321    [pItem setEnabled: YES];
322    [i_pMenu addItem: pItem];
323
324    if( i_pDockMenu )
325    {
326        // create a similar entry in the dock menu
327        pItem = [[NSMenuItem alloc] initWithTitle: getAutoreleasedString( i_rTitle )
328                                    action: @selector(executeMenuItem:)
329                                    keyEquivalent: @""
330                            ];
331        [pItem setTag: i_nTag];
332        [pItem setTarget: pExecute];
333        [pItem setEnabled: YES];
334        [i_pDockMenu addItem: pItem];
335    }
336}
337
338static void appendRecentMenu( NSMenu* i_pMenu, NSMenu* i_pDockMenu, const OUString& i_rTitle )
339{
340    if( ! pRecentDelegate )
341        pRecentDelegate = [[RecentMenuDelegate alloc] init];
342
343    NSMenuItem* pItem = [i_pMenu addItemWithTitle: getAutoreleasedString( i_rTitle )
344                                                   action: @selector(executeMenuItem:)
345                                                   keyEquivalent: @""
346                        ];
347    [pItem setEnabled: YES];
348    NSMenu* pRecentMenu = [[NSMenu alloc] initWithTitle: getAutoreleasedString( i_rTitle ) ];
349
350    [pRecentMenu setDelegate: pRecentDelegate];
351
352    [pRecentMenu setAutoenablesItems: NO];
353    [pItem setSubmenu: pRecentMenu];
354
355    if( i_pDockMenu )
356    {
357        // create a similar entry in the dock menu
358        pItem = [i_pDockMenu addItemWithTitle: getAutoreleasedString( i_rTitle )
359                             action: @selector(executeMenuItem:)
360                             keyEquivalent: @""
361                        ];
362        [pItem setEnabled: YES];
363        pRecentMenu = [[NSMenu alloc] initWithTitle: getAutoreleasedString( i_rTitle ) ];
364
365        [pRecentMenu setDelegate: pRecentDelegate];
366
367        [pRecentMenu setAutoenablesItems: NO];
368        [pItem setSubmenu: pRecentMenu];
369    }
370}
371
372
373extern "C"
374{
375
376void aqua_init_systray()
377{
378    SolarMutexGuard aGuard;
379
380    ShutdownIcon *pShutdownIcon = ShutdownIcon::getInstance();
381    if( ! pShutdownIcon )
382        return;
383
384    // disable shutdown
385    pShutdownIcon->SetVeto( true );
386    ShutdownIcon::addTerminateListener();
387
388    if( ! pDefMenu )
389    {
390        if( [NSApp respondsToSelector: @selector(addFallbackMenuItem:)] )
391        {
392            aShortcuts.clear();
393
394            pExecute = [[QSMenuExecute alloc] init];
395            pDefMenu = [[NSMenuItem alloc] initWithTitle: getAutoreleasedString( SfxResId(STR_QUICKSTART_FILE) ) action: nullptr keyEquivalent: @""];
396            pDockSubMenu = [[NSMenuItem alloc] initWithTitle: getAutoreleasedString( SfxResId(STR_QUICKSTART_FILE) ) action: nullptr keyEquivalent: @""];
397            NSMenu* pMenu = [[NSMenu alloc] initWithTitle: getAutoreleasedString( SfxResId(STR_QUICKSTART_FILE) )];
398            [pMenu setAutoenablesItems: NO];
399            NSMenu* pDockMenu = [[NSMenu alloc] initWithTitle: getAutoreleasedString( SfxResId(STR_QUICKSTART_FILE) )];
400            [pDockMenu setAutoenablesItems: NO];
401
402            // collect the URLs of the entries in the File/New menu
403            SvtModuleOptions    aModuleOptions;
404            std::set< OUString > aFileNewAppsAvailable;
405            std::vector < SvtDynMenuEntry > const aNewMenu = SvtDynamicMenuOptions().GetMenu( EDynamicMenuType::NewMenu );
406
407            for ( SvtDynMenuEntry const & newMenuProp : aNewMenu )
408            {
409                if ( !newMenuProp.sURL.isEmpty() )
410                    aFileNewAppsAvailable.insert( newMenuProp.sURL );
411            }
412
413            // describe the menu entries for launching the applications
414            struct MenuEntryDescriptor
415            {
416                SvtModuleOptions::EModule   eModuleIdentifier;
417                int                         nMenuTag;
418                const char*                 pAsciiURLDescription;
419            }   aMenuItems[] =
420            {
421                { SvtModuleOptions::EModule::WRITER,    MI_WRITER,  WRITER_URL },
422                { SvtModuleOptions::EModule::CALC,      MI_CALC,    CALC_URL },
423                { SvtModuleOptions::EModule::IMPRESS,   MI_IMPRESS, IMPRESS_WIZARD_URL },
424                { SvtModuleOptions::EModule::DRAW,      MI_DRAW,    DRAW_URL },
425                { SvtModuleOptions::EModule::DATABASE,  MI_BASE,    BASE_URL },
426                { SvtModuleOptions::EModule::MATH,      MI_MATH,    MATH_URL }
427            };
428
429            // insert entry for startcenter
430            if( aModuleOptions.IsModuleInstalled( SvtModuleOptions::EModule::STARTMODULE ) )
431            {
432                appendMenuItem( pMenu, nil, SfxResId(STR_QUICKSTART_STARTCENTER), MI_STARTMODULE, "n" );
433                if( [NSApp respondsToSelector: @selector(setDockIconClickHandler:)] )
434                    [NSApp performSelector:@selector(setDockIconClickHandler:) withObject: pExecute];
435                else
436                    OSL_FAIL( "setDockIconClickHandler selector failed on NSApp" );
437
438            }
439
440            // insert the menu entries for launching the applications
441            for ( size_t i = 0; i < SAL_N_ELEMENTS( aMenuItems ); ++i )
442            {
443                if ( !aModuleOptions.IsModuleInstalled( aMenuItems[i].eModuleIdentifier ) )
444                    // the complete application is not even installed
445                    continue;
446
447                OUString sURL( OUString::createFromAscii( aMenuItems[i].pAsciiURLDescription ) );
448
449                if ( aFileNewAppsAvailable.find( sURL ) == aFileNewAppsAvailable.end() )
450                    // the application is installed, but the entry has been configured to *not* appear in the File/New
451                    // menu => also let not appear it in the quickstarter
452                    continue;
453
454                OUString aKeyEquiv( getShortCut( ShutdownIcon::GetUrlDescription( sURL ) ) );
455
456                appendMenuItem( pMenu, pDockMenu, ShutdownIcon::GetUrlDescription( sURL ), aMenuItems[i].nMenuTag, aKeyEquiv );
457            }
458
459            // insert the remaining menu entries
460
461            // add recent menu
462            appendRecentMenu( pMenu, pDockMenu, SfxResId(STR_QUICKSTART_RECENTDOC) );
463
464            OUString aTitle( SfxResId(STR_QUICKSTART_FROMTEMPLATE) );
465            OUString aKeyEquiv( getShortCut( aTitle ) );
466            appendMenuItem( pMenu, pDockMenu, aTitle, MI_TEMPLATE, aKeyEquiv );
467            aTitle = SfxResId(STR_QUICKSTART_FILEOPEN);
468            aKeyEquiv = getShortCut( aTitle );
469            appendMenuItem( pMenu, pDockMenu, aTitle, MI_OPEN, aKeyEquiv );
470
471            [pDefMenu setSubmenu: pMenu];
472            [NSApp performSelector:@selector(addFallbackMenuItem:) withObject: pDefMenu];
473
474            if( [NSApp respondsToSelector: @selector(addDockMenuItem:)] )
475            {
476                [pDockSubMenu setSubmenu: pDockMenu];
477                // add the submenu
478                [NSApp performSelector:@selector(addDockMenuItem:) withObject: pDockSubMenu];
479            }
480            else
481                OSL_FAIL( "addDockMenuItem selector failed on NSApp" );
482        }
483        else
484            OSL_FAIL( "addFallbackMenuItem selector failed on NSApp" );
485    }
486}
487
488void SAL_DLLPUBLIC_EXPORT aqua_shutdown_systray()
489{
490}
491
492}
493
494/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
495