1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
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/.
9 * This file incorporates work covered by the following license notice:
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 .
20 #include <config_folders.h>
21 #include <sfx2/sfxhelp.hxx>
24 #include <string_view>
30 #include <Foundation/NSString.h>
31 #include <CoreFoundation/CFURL.h>
32 #include <CoreServices/CoreServices.h>
36 #include <sal/log.hxx>
37 #include <com/sun/star/uno/Reference.h>
38 #include <com/sun/star/frame/Desktop.hpp>
39 #include <com/sun/star/frame/UnknownModuleException.hpp>
40 #include <com/sun/star/frame/XFrame2.hpp>
41 #include <comphelper/processfactory.hxx>
42 #include <com/sun/star/awt/XWindow.hpp>
43 #include <com/sun/star/awt/XTopWindow.hpp>
44 #include <com/sun/star/beans/XPropertySet.hpp>
45 #include <com/sun/star/frame/FrameSearchFlag.hpp>
46 #include <toolkit/helper/vclunohelper.hxx>
47 #include <com/sun/star/frame/ModuleManager.hpp>
48 #include <unotools/configmgr.hxx>
49 #include <unotools/moduleoptions.hxx>
50 #include <tools/urlobj.hxx>
51 #include <ucbhelper/content.hxx>
52 #include <unotools/pathoptions.hxx>
53 #include <rtl/byteseq.hxx>
54 #include <rtl/ustring.hxx>
55 #include <o3tl/string_view.hxx>
56 #include <officecfg/Office/Common.hxx>
57 #include <osl/process.h>
58 #include <osl/file.hxx>
59 #include <unotools/tempfile.hxx>
60 #include <unotools/securityoptions.hxx>
61 #include <rtl/uri.hxx>
62 #include <vcl/commandinfoprovider.hxx>
63 #include <vcl/keycod.hxx>
64 #include <vcl/settings.hxx>
65 #include <vcl/locktoplevels.hxx>
66 #include <vcl/weld.hxx>
67 #include <openuriexternally.hxx>
69 #include <comphelper/lok.hxx>
70 #include <LibreOfficeKit/LibreOfficeKitEnums.h>
71 #include <sfx2/viewsh.hxx>
73 #include "newhelp.hxx"
74 #include <sfx2/flatpak.hxx>
75 #include <sfx2/sfxresid.hxx>
77 #include <sfx2/strings.hrc>
78 #include <vcl/svapp.hxx>
79 #include <rtl/string.hxx>
80 #include <svtools/langtab.hxx>
81 #include <comphelper/diagnose_ex.hxx>
83 using namespace ::com::sun::star::beans
;
84 using namespace ::com::sun::star::frame
;
85 using namespace ::com::sun::star::uno
;
92 std::unique_ptr
<weld::MessageDialog
> m_xErrBox
;
94 DECL_STATIC_LINK(NoHelpErrorBox
, HelpRequestHdl
, weld::Widget
&, bool);
96 explicit NoHelpErrorBox(weld::Widget
* pParent
)
97 : m_xErrBox(Application::CreateMessageDialog(pParent
, VclMessageType::Error
, VclButtonsType::Ok
,
98 SfxResId(RID_STR_HLPFILENOTEXIST
)))
100 // Error message: "No help available"
101 m_xErrBox
->connect_help(LINK(nullptr, NoHelpErrorBox
, HelpRequestHdl
));
111 IMPL_STATIC_LINK_NOARG(NoHelpErrorBox
, HelpRequestHdl
, weld::Widget
&, bool)
113 // do nothing, because no help available
117 static OUString
const & HelpLocaleString();
121 /// Root path of the help.
122 OUString
const & getHelpRootURL()
124 static OUString
const s_instURL
= []()
126 OUString tmp
= officecfg::Office::Common::Path::Current::Help::get();
129 // try to determine path from default
130 tmp
= "$(instpath)/" LIBO_SHARE_HELP_FOLDER
;
133 // replace anything like $(instpath);
134 SvtPathOptions aOptions
;
135 tmp
= aOptions
.SubstituteVariable(tmp
);
138 if (osl::FileBase::getFileURLFromSystemPath(tmp
, url
) == osl::FileBase::E_None
)
145 bool impl_checkHelpLocalePath(OUString
const & rpPath
)
147 osl::DirectoryItem directoryItem
;
150 osl::FileStatus
fileStatus(osl_FileStatus_Mask_Type
| osl_FileStatus_Mask_FileURL
| osl_FileStatus_Mask_FileName
);
151 if (osl::DirectoryItem::get(rpPath
, directoryItem
) == osl::FileBase::E_None
&&
152 directoryItem
.getFileStatus(fileStatus
) == osl::FileBase::E_None
&&
153 fileStatus
.isDirectory())
160 /// Check for built-in help
161 /// Check if help/<lang>/err.html file exist
162 bool impl_hasHelpInstalled()
164 if (comphelper::LibreOfficeKit::isActive())
167 // detect installed locale
168 static OUString
const aLocaleStr
= HelpLocaleString();
170 OUString helpRootURL
= getHelpRootURL() + "/" + aLocaleStr
+ "/err.html";
172 osl::DirectoryItem directoryItem
;
173 if(osl::DirectoryItem::get(helpRootURL
, directoryItem
) == osl::FileBase::E_None
){
177 SAL_INFO( "sfx.appl", "Checking old help installed " << bOK
);
181 /// Check for html built-in help
182 /// Check if help/lang/text folder exist. Only html has it.
183 bool impl_hasHTMLHelpInstalled()
185 if (comphelper::LibreOfficeKit::isActive())
188 // detect installed locale
189 static OUString
const aLocaleStr
= HelpLocaleString();
191 OUString helpRootURL
= getHelpRootURL() + "/" + aLocaleStr
+ "/text";
192 bool bOK
= impl_checkHelpLocalePath( helpRootURL
);
193 SAL_INFO( "sfx.appl", "Checking new help (html) installed " << bOK
);
199 /// Return the locale we prefer for displaying help
200 static OUString
const & HelpLocaleString()
202 if (comphelper::LibreOfficeKit::isActive())
203 return comphelper::LibreOfficeKit::getLanguageTag().getBcp47();
205 static OUString aLocaleStr
;
206 if (!aLocaleStr
.isEmpty())
209 static constexpr OUString
aEnglish(u
"en-US"_ustr
);
210 // detect installed locale
211 aLocaleStr
= utl::ConfigManager::getUILocale();
213 if ( aLocaleStr
.isEmpty() )
215 aLocaleStr
= aEnglish
;
219 // get fall-back language (country)
220 OUString sLang
= aLocaleStr
;
221 sal_Int32 nSepPos
= sLang
.indexOf( '-' );
224 sLang
= sLang
.copy( 0, nSepPos
);
226 OUString
sHelpPath(u
""_ustr
);
227 sHelpPath
= getHelpRootURL() + "/" + utl::ConfigManager::getProductVersion() + "/" + aLocaleStr
;
228 if (impl_checkHelpLocalePath(sHelpPath
))
232 sHelpPath
= getHelpRootURL() + "/" + utl::ConfigManager::getProductVersion() + "/" + sLang
;
233 if (impl_checkHelpLocalePath(sHelpPath
))
238 sHelpPath
= getHelpRootURL() + "/" + aLocaleStr
;
239 if (impl_checkHelpLocalePath(sHelpPath
))
243 sHelpPath
= getHelpRootURL() + "/" + sLang
;
244 if (impl_checkHelpLocalePath(sHelpPath
))
249 sHelpPath
= getHelpRootURL() + "/" + utl::ConfigManager::getProductVersion() + "/" + aEnglish
;
250 if (impl_checkHelpLocalePath(sHelpPath
))
252 aLocaleStr
= aEnglish
;
255 sHelpPath
= getHelpRootURL() + "/" + aEnglish
;
256 if (impl_checkHelpLocalePath(sHelpPath
))
258 aLocaleStr
= aEnglish
;
266 void AppendConfigToken( OUStringBuffer
& rURL
, bool bQuestionMark
)
268 const OUString
& aLocaleStr
= HelpLocaleString();
270 // query part exists?
272 // no, so start with '?'
275 // yes, so only append with '&'
279 rURL
.append("Language=");
280 rURL
.append(aLocaleStr
);
281 rURL
.append("&System=");
282 rURL
.append(officecfg::Office::Common::Help::System::get());
283 rURL
.append("&Version=");
284 rURL
.append(utl::ConfigManager::getProductVersion());
287 static bool GetHelpAnchor_Impl( std::u16string_view _rURL
, OUString
& _rAnchor
)
293 ::ucbhelper::Content
aCnt( INetURLObject( _rURL
).GetMainURL( INetURLObject::DecodeMechanism::NONE
),
294 Reference
< css::ucb::XCommandEnvironment
>(),
295 comphelper::getProcessComponentContext() );
297 if ( aCnt
.getPropertyValue(u
"AnchorName"_ustr
) >>= sAnchor
)
300 if ( !sAnchor
.isEmpty() )
308 SAL_WARN( "sfx.appl", "Property 'AnchorName' is missing" );
311 catch (const css::uno::Exception
&)
323 static OUString
GetHelpText( const OUString
& aCommandURL
, const OUString
& rModule
);
328 OUString
SfxHelp_Impl::GetHelpText( const OUString
& aCommandURL
, const OUString
& rModule
)
331 OUStringBuffer
aHelpURL( SfxHelp::CreateHelpURL( aCommandURL
, rModule
) );
332 // added 'active' parameter
333 sal_Int32 nIndex
= aHelpURL
.lastIndexOf( '#' );
335 nIndex
= aHelpURL
.getLength();
336 aHelpURL
.insert( nIndex
, "&Active=true" );
338 return SfxContentHelper::GetActiveHelpString( aHelpURL
.makeStringAndClear() );
343 , bLaunchingHelp(false)
345 // read the environment variable "HELP_DEBUG"
346 // if it's set, you will see debug output on active help
348 OUString
sEnvVarName( u
"HELP_DEBUG"_ustr
);
349 osl_getEnvironment( sEnvVarName
.pData
, &sHelpDebug
.pData
);
350 bIsDebug
= !sHelpDebug
.isEmpty();
357 static OUString
getDefaultModule_Impl()
359 OUString sDefaultModule
;
360 SvtModuleOptions aModOpt
;
361 if (aModOpt
.IsWriterInstalled())
362 sDefaultModule
= "swriter";
363 else if (aModOpt
.IsCalcInstalled())
364 sDefaultModule
= "scalc";
365 else if (aModOpt
.IsImpressInstalled())
366 sDefaultModule
= "simpress";
367 else if (aModOpt
.IsDrawInstalled())
368 sDefaultModule
= "sdraw";
369 else if (aModOpt
.IsMathInstalled())
370 sDefaultModule
= "smath";
371 else if (aModOpt
.IsChartInstalled())
372 sDefaultModule
= "schart";
373 else if (SvtModuleOptions::IsBasicIDEInstalled())
374 sDefaultModule
= "sbasic";
375 else if (aModOpt
.IsDataBaseInstalled())
376 sDefaultModule
= "sdatabase";
379 SAL_WARN( "sfx.appl", "getDefaultModule_Impl(): no module installed" );
381 return sDefaultModule
;
384 static OUString
getCurrentModuleIdentifier_Impl()
386 OUString sIdentifier
;
387 const Reference
< XComponentContext
>& xContext
= ::comphelper::getProcessComponentContext();
388 Reference
< XModuleManager2
> xModuleManager
= ModuleManager::create(xContext
);
389 Reference
< XDesktop2
> xDesktop
= Desktop::create(xContext
);
390 Reference
< XFrame
> xCurrentFrame
= xDesktop
->getCurrentFrame();
392 if ( xCurrentFrame
.is() )
396 sIdentifier
= xModuleManager
->identify( xCurrentFrame
);
398 catch (const css::frame::UnknownModuleException
&)
400 SAL_INFO( "sfx.appl", "SfxHelp::getCurrentModuleIdentifier_Impl(): unknown module (help in help?)" );
402 catch (const Exception
&)
404 TOOLS_WARN_EXCEPTION( "sfx.appl", "SfxHelp::getCurrentModuleIdentifier_Impl(): exception of XModuleManager::identify()" );
413 OUString
MapModuleIdentifier(const OUString
&rFactoryShortName
)
415 OUString
aFactoryShortName(rFactoryShortName
);
417 // Map some module identifiers to their "real" help module string.
418 if ( aFactoryShortName
== "chart2" )
419 aFactoryShortName
= "schart" ;
420 else if ( aFactoryShortName
== "BasicIDE" )
421 aFactoryShortName
= "sbasic";
422 else if ( aFactoryShortName
== "sweb"
423 || aFactoryShortName
== "sglobal"
424 || aFactoryShortName
== "swxform" )
425 aFactoryShortName
= "swriter" ;
426 else if ( aFactoryShortName
== "dbquery"
427 || aFactoryShortName
== "dbbrowser"
428 || aFactoryShortName
== "dbrelation"
429 || aFactoryShortName
== "dbtable"
430 || aFactoryShortName
== "dbapp"
431 || aFactoryShortName
== "dbreport"
432 || aFactoryShortName
== "dbtdata"
433 || aFactoryShortName
== "swreport"
434 || aFactoryShortName
== "swform" )
435 aFactoryShortName
= "sdatabase";
436 else if ( aFactoryShortName
== "sbibliography"
437 || aFactoryShortName
== "sabpilot"
438 || aFactoryShortName
== "scanner"
439 || aFactoryShortName
== "spropctrlr"
440 || aFactoryShortName
== "StartModule" )
441 aFactoryShortName
.clear();
443 return aFactoryShortName
;
447 OUString
SfxHelp::GetHelpModuleName_Impl(std::u16string_view rHelpID
)
449 OUString aFactoryShortName
;
451 //rhbz#1438876 detect preferred module for this help id, e.g. csv dialog
452 //for calc import before any toplevel is created and so context is
453 //otherwise unknown. Cosmetic, same help is shown in any case because its
454 //in the shared section, but title bar would state "Writer" when context is
455 //expected to be "Calc"
456 std::u16string_view sRemainder
;
457 if (o3tl::starts_with(rHelpID
, u
"modules/", &sRemainder
))
459 std::size_t nEndModule
= sRemainder
.find(u
'/');
460 aFactoryShortName
= nEndModule
!= std::u16string_view::npos
461 ? sRemainder
.substr(0, nEndModule
) : sRemainder
;
464 if (aFactoryShortName
.isEmpty())
466 OUString aModuleIdentifier
= getCurrentModuleIdentifier_Impl();
467 if (!aModuleIdentifier
.isEmpty())
471 Reference
< XModuleManager2
> xModuleManager(
472 ModuleManager::create(::comphelper::getProcessComponentContext()) );
473 Sequence
< PropertyValue
> lProps
;
474 xModuleManager
->getByName( aModuleIdentifier
) >>= lProps
;
475 auto pProp
= std::find_if(std::cbegin(lProps
), std::cend(lProps
),
476 [](const PropertyValue
& rProp
) { return rProp
.Name
== "ooSetupFactoryShortName"; });
477 if (pProp
!= std::cend(lProps
))
478 pProp
->Value
>>= aFactoryShortName
;
480 catch (const Exception
&)
482 TOOLS_WARN_EXCEPTION( "sfx.appl", "SfxHelp::GetHelpModuleName_Impl()" );
487 if (!aFactoryShortName
.isEmpty())
488 aFactoryShortName
= MapModuleIdentifier(aFactoryShortName
);
489 if (aFactoryShortName
.isEmpty())
490 aFactoryShortName
= getDefaultModule_Impl();
492 return aFactoryShortName
;
495 OUString
SfxHelp::CreateHelpURL_Impl( const OUString
& aCommandURL
, const OUString
& rModuleName
)
497 // build up the help URL
498 OUStringBuffer
aHelpURL("vnd.sun.star.help://");
499 bool bHasAnchor
= false;
502 OUString
aModuleName( rModuleName
);
503 if (aModuleName
.isEmpty())
504 aModuleName
= getDefaultModule_Impl();
506 aHelpURL
.append(aModuleName
);
508 if ( aCommandURL
.isEmpty() )
509 aHelpURL
.append("/start");
512 aHelpURL
.append("/" +
513 rtl::Uri::encode(aCommandURL
,
514 rtl_UriCharClassRelSegment
,
515 rtl_UriEncodeKeepEscapes
,
516 RTL_TEXTENCODING_UTF8
));
518 OUStringBuffer aTempURL
= aHelpURL
;
519 AppendConfigToken( aTempURL
, true );
520 bHasAnchor
= GetHelpAnchor_Impl(aTempURL
, aAnchor
);
523 AppendConfigToken( aHelpURL
, true );
526 aHelpURL
.append("#" + aAnchor
);
528 return aHelpURL
.makeStringAndClear();
531 static SfxHelpWindow_Impl
* impl_createHelp(Reference
< XFrame2
>& rHelpTask
,
532 Reference
< XFrame
>& rHelpContent
)
534 Reference
< XDesktop2
> xDesktop
= Desktop::create( ::comphelper::getProcessComponentContext() );
536 // otherwise - create new help task
537 Reference
< XFrame2
> xHelpTask(
538 xDesktop
->findFrame( u
"OFFICE_HELP_TASK"_ustr
, FrameSearchFlag::TASKS
| FrameSearchFlag::CREATE
),
543 // create all internal windows and sub frames ...
544 Reference
< css::awt::XWindow
> xParentWindow
= xHelpTask
->getContainerWindow();
545 VclPtr
<vcl::Window
> pParentWindow
= VCLUnoHelper::GetWindow( xParentWindow
);
546 VclPtrInstance
<SfxHelpWindow_Impl
> pHelpWindow( xHelpTask
, pParentWindow
);
547 Reference
< css::awt::XWindow
> xHelpWindow
= VCLUnoHelper::GetInterface( pHelpWindow
);
549 Reference
< XFrame
> xHelpContent
;
550 if (xHelpTask
->setComponent( xHelpWindow
, Reference
< XController
>() ))
553 xHelpTask
->setName(u
"OFFICE_HELP_TASK"_ustr
);
555 Reference
< XPropertySet
> xProps(xHelpTask
, UNO_QUERY
);
557 xProps
->setPropertyValue(
559 Any(SfxResId(STR_HELP_WINDOW_TITLE
)));
561 pHelpWindow
->setContainerWindow( xParentWindow
);
562 xParentWindow
->setVisible(true);
563 xHelpWindow
->setVisible(true);
565 // This sub frame is created internally (if we called new SfxHelpWindow_Impl() ...)
566 // It should exist :-)
567 xHelpContent
= xHelpTask
->findFrame(u
"OFFICE_HELP"_ustr
, FrameSearchFlag::CHILDREN
);
570 if (!xHelpContent
.is())
572 pHelpWindow
.disposeAndClear();
576 xHelpContent
->setName(u
"OFFICE_HELP"_ustr
);
578 rHelpTask
= std::move(xHelpTask
);
579 rHelpContent
= std::move(xHelpContent
);
583 OUString
SfxHelp::GetHelpText(const OUString
& aCommandURL
)
585 OUString sModuleName
= GetHelpModuleName_Impl(aCommandURL
);
586 auto aProperties
= vcl::CommandInfoProvider::GetCommandProperties(aCommandURL
, getCurrentModuleIdentifier_Impl());
587 OUString sRealCommand
= vcl::CommandInfoProvider::GetRealCommandForCommand(aProperties
);
588 OUString sHelpText
= SfxHelp_Impl::GetHelpText( sRealCommand
.isEmpty() ? aCommandURL
: sRealCommand
, sModuleName
);
590 // add some debug information?
593 sHelpText
+= "\n-------------\n" +
594 sModuleName
+ ": " + aCommandURL
;
600 OUString
SfxHelp::GetURLHelpText(std::u16string_view aURL
)
602 // hyperlinks are handled differently in Online
603 if (comphelper::LibreOfficeKit::isActive())
606 bool bCtrlClickHlink
= SvtSecurityOptions::IsOptionSet(SvtSecurityOptions::EOption::CtrlClickHyperlink
);
608 // "ctrl-click to follow link:" for not MacOS
609 // "⌘-click to follow link:" for MacOs
610 vcl::KeyCode
aCode(KEY_SPACE
);
611 vcl::KeyCode
aModifiedCode(KEY_SPACE
, KEY_MOD1
);
612 OUString
aModStr(aModifiedCode
.GetName());
613 aModStr
= aModStr
.replaceFirst(aCode
.GetName(), "");
614 aModStr
= aModStr
.replaceAll("+", "");
616 = bCtrlClickHlink
? SfxResId(STR_CTRLCLICKHYPERLINK
) : SfxResId(STR_CLICKHYPERLINK
);
617 aHelpStr
= aHelpStr
.replaceFirst("%{key}", aModStr
);
618 aHelpStr
= aHelpStr
.replaceFirst("%{link}", aURL
);
622 void SfxHelp::SearchKeyword( const OUString
& rKeyword
)
624 Start_Impl(OUString(), static_cast<weld::Widget
*>(nullptr), rKeyword
);
627 bool SfxHelp::Start( const OUString
& rURL
, const vcl::Window
* pWindow
)
631 bLaunchingHelp
= true;
632 bool bRet
= Start_Impl( rURL
, pWindow
);
633 bLaunchingHelp
= false;
637 bool SfxHelp::Start(const OUString
& rURL
, weld::Widget
* pWidget
)
641 bLaunchingHelp
= true;
642 bool bRet
= Start_Impl(rURL
, pWidget
, OUString());
643 bLaunchingHelp
= false;
647 /// Redirect the vnd.sun.star.help:// urls to http://help.libreoffice.org
648 static bool impl_showOnlineHelp(const OUString
& rURL
, weld::Widget
* pDialogParent
)
650 static constexpr OUString
aInternal(u
"vnd.sun.star.help://"_ustr
);
651 if ( rURL
.getLength() <= aInternal
.getLength() || !rURL
.startsWith(aInternal
) )
654 OUString aHelpLink
= officecfg::Office::Common::Help::HelpRootURL::get();
655 OUString aTarget
= OUString::Concat("Target=") + rURL
.subView(aInternal
.getLength());
656 aTarget
= aTarget
.replaceAll("%2F", "/").replaceAll("?", "&");
657 aHelpLink
+= aTarget
;
659 if (comphelper::LibreOfficeKit::isActive())
661 if(SfxViewShell
* pViewShell
= SfxViewShell::Current())
663 pViewShell
->libreOfficeKitViewCallback(LOK_CALLBACK_HYPERLINK_CLICKED
,
669 GetpApp()->libreOfficeKitViewCallback(LOK_CALLBACK_HYPERLINK_CLICKED
,
680 LSOpenCFURLRef(CFURLCreateWithString(kCFAllocatorDefault
,
681 CFStringCreateWithCString(kCFAllocatorDefault
,
682 aHelpLink
.toUtf8().getStr(),
683 kCFStringEncodingUTF8
),
688 sfx2::openUriExternally(aHelpLink
, false, pDialogParent
);
692 catch (const Exception
&)
700 bool rewriteFlatpakHelpRootUrl(OUString
* helpRootUrl
) {
701 assert(helpRootUrl
!= nullptr);
702 //TODO: this function for now assumes that the passed-in *helpRootUrl references
703 // /app/libreoffice/help (which belongs to the org.libreoffice.LibreOffice.Help
704 // extension); it replaces it with the corresponding file URL as seen outside the flatpak
706 struct Failure
: public std::exception
{};
708 static auto const url
= [] {
709 // From /.flatpak-info [Instance] section, read
711 // app-extensions=...;org.libreoffice.LibreOffice.Help=<sha>;...
713 osl::File
ini(u
"file:///.flatpak-info"_ustr
);
714 auto err
= ini
.open(osl_File_OpenFlag_Read
);
715 if (err
!= osl::FileBase::E_None
) {
716 SAL_WARN("sfx.appl", "LIBO_FLATPAK mode failure opening /.flatpak-info: " << err
);
721 bool havePath
= false;
722 bool haveExtensions
= false;
723 for (bool instance
= false; !(havePath
&& haveExtensions
);) {
724 rtl::ByteSequence bytes
;
725 err
= ini
.readLine(bytes
);
726 if (err
!= osl::FileBase::E_None
) {
729 "LIBO_FLATPAK mode reading /.flatpak-info fails with " << err
730 << " before [Instance] app-path");
733 std::string_view
const line(
734 reinterpret_cast<char const *>(bytes
.getConstArray()), bytes
.getLength());
736 static constexpr auto keyPath
= std::string_view("app-path=");
737 static constexpr auto keyExtensions
= std::string_view("app-extensions=");
738 if (!havePath
&& line
.length() >= keyPath
.size()
739 && line
.substr(0, keyPath
.size()) == keyPath
.data())
741 auto const value
= line
.substr(keyPath
.size());
742 if (!rtl_convertStringToUString(
743 &path
.pData
, value
.data(), value
.length(),
744 osl_getThreadTextEncoding(),
745 (RTL_TEXTTOUNICODE_FLAGS_UNDEFINED_ERROR
746 | RTL_TEXTTOUNICODE_FLAGS_MBUNDEFINED_ERROR
747 | RTL_TEXTTOUNICODE_FLAGS_INVALID_ERROR
)))
751 "LIBO_FLATPAK mode failure converting app-path \"" << value
756 } else if (!haveExtensions
&& line
.length() >= keyExtensions
.size()
757 && line
.substr(0, keyExtensions
.size()) == keyExtensions
.data())
759 auto const value
= line
.substr(keyExtensions
.size());
760 if (!rtl_convertStringToUString(
761 &extensions
.pData
, value
.data(), value
.length(),
762 osl_getThreadTextEncoding(),
763 (RTL_TEXTTOUNICODE_FLAGS_UNDEFINED_ERROR
764 | RTL_TEXTTOUNICODE_FLAGS_MBUNDEFINED_ERROR
765 | RTL_TEXTTOUNICODE_FLAGS_INVALID_ERROR
)))
769 "LIBO_FLATPAK mode failure converting app-extensions \"" << value
773 haveExtensions
= true;
774 } else if (line
.length() > 0 && line
[0] == '[') {
777 "LIBO_FLATPAK mode /.flatpak-info lacks [Instance] app-path and"
781 } else if (line
== "[Instance]") {
786 // Extract <sha> from ...;org.libreoffice.LibreOffice.Help=<sha>;...:
787 std::u16string_view sha
;
788 for (sal_Int32 i
= 0;;) {
789 OUString elem
= extensions
.getToken(0, ';', i
);
790 if (elem
.startsWith("org.libreoffice.LibreOffice.Help=", &sha
)) {
796 "LIBO_FLATPAK mode /.flatpak-info [Instance] app-extensions \""
797 << extensions
<< "\" org.libreoffice.LibreOffice.Help");
801 // Assuming that <path> is of the form
802 // /.../app/org.libreoffice.LibreOffice/<arch>/<branch>/<sha'>/files
804 // /.../runtime/org.libreoffice.LibreOffice.Help/<arch>/<branch>/<sha>/files
805 // because the extension's files are stored at a different place than the app's files,
806 // so use this hack until flatpak itself provides a better solution:
807 static constexpr OUString segments
= u
"/app/org.libreoffice.LibreOffice/"_ustr
;
808 auto const i1
= path
.lastIndexOf(segments
);
809 // use lastIndexOf instead of indexOf, in case the user-controlled prefix /.../
810 // happens to contain such segments
814 "LIBO_FLATPAK mode /.flatpak-info [Instance] app-path \"" << path
815 << "\" doesn't contain /app/org.libreoffice.LibreOffice/");
818 auto const i2
= i1
+ segments
.getLength();
819 auto i3
= path
.indexOf('/', i2
);
823 "LIBO_FLATPAK mode /.flatpak-info [Instance] app-path \"" << path
824 << "\" doesn't contain branch segment");
827 i3
= path
.indexOf('/', i3
+ 1);
831 "LIBO_FLATPAK mode /.flatpak-info [Instance] app-path \"" << path
832 << "\" doesn't contain sha segment");
836 auto const i4
= path
.indexOf('/', i3
);
840 "LIBO_FLATPAK mode /.flatpak-info [Instance] app-path \"" << path
841 << "\" doesn't contain files segment");
844 path
= path
.subView(0, i1
) + OUString::Concat("/runtime/org.libreoffice.LibreOffice.Help/")
845 + path
.subView(i2
, i3
- i2
) + sha
+ path
.subView(i4
);
846 // Turn <path> into a file URL:
848 err
= osl::FileBase::getFileURLFromSystemPath(path
, url_
);
849 if (err
!= osl::FileBase::E_None
) {
852 "LIBO_FLATPAK mode failure converting app-path \"" << path
<< "\" to URL: "
860 } catch (Failure
&) {
867 // add <noscript> meta for browsers without javascript
869 constexpr OUStringLiteral SHTML1
= u
"<!DOCTYPE HTML><html lang=\"en-US\"><head><meta charset=\"UTF-8\">";
870 constexpr OUStringLiteral SHTML2
= u
"<noscript><meta http-equiv=\"refresh\" content=\"0; url='";
871 constexpr OUStringLiteral SHTML3
= u
"/noscript.html'\"></noscript><meta http-equiv=\"refresh\" content=\"1; url='";
872 constexpr OUStringLiteral SHTML4
= u
"'\"><script type=\"text/javascript\"> window.location.href = \"";
873 constexpr OUStringLiteral SHTML5
= u
"\";</script><title>Help Page Redirection</title></head><body></body></html>";
875 // use a tempfile since e.g. xdg-open doesn't support URL-parameters with file:// URLs
876 static bool impl_showOfflineHelp(const OUString
& rURL
, weld::Widget
* pDialogParent
)
878 OUString aBaseInstallPath
= getHelpRootURL();
879 // For the flatpak case, find the pathname outside the flatpak sandbox that corresponds to
880 // aBaseInstallPath, because that is what needs to be stored in aTempFile below:
881 if (flatpak::isFlatpak() && !rewriteFlatpakHelpRootUrl(&aBaseInstallPath
)) {
885 OUString
aHelpLink( aBaseInstallPath
+ "/index.html?" );
886 OUString aTarget
= OUString::Concat("Target=") + rURL
.subView(RTL_CONSTASCII_LENGTH("vnd.sun.star.help://"));
887 aTarget
= aTarget
.replaceAll("%2F","/").replaceAll("?","&");
888 aHelpLink
+= aTarget
;
890 // Get a html tempfile (for the flatpak case, create it in XDG_CACHE_HOME instead of /tmp for
891 // technical reasons, so that it can be accessed by the browser running outside the sandbox):
892 static constexpr OUStringLiteral
aExtension(u
".html");
893 OUString
* parent
= nullptr;
894 if (flatpak::isFlatpak() && !flatpak::createTemporaryHtmlDirectory(&parent
)) {
897 ::utl::TempFileNamed
aTempFile(u
"NewHelp", true, aExtension
, parent
, false );
899 SvStream
* pStream
= aTempFile
.GetStream(StreamMode::WRITE
);
900 pStream
->SetStreamCharSet(RTL_TEXTENCODING_UTF8
);
902 OUString aTempStr
= SHTML1
+ SHTML2
+
903 aBaseInstallPath
+ "/" + HelpLocaleString() + SHTML3
+
907 pStream
->WriteUnicodeOrByteText(aTempStr
);
909 aTempFile
.CloseStream();
913 LSOpenCFURLRef(CFURLCreateWithString(kCFAllocatorDefault
,
914 CFStringCreateWithCString(kCFAllocatorDefault
,
915 aTempFile
.GetURL().toUtf8().getStr(),
916 kCFStringEncodingUTF8
),
921 sfx2::openUriExternally(aTempFile
.GetURL(), false, pDialogParent
);
925 catch (const Exception
&)
928 aTempFile
.EnableKillingFile();
934 // tdf#119579 skip floating windows as potential parent for missing help dialog
935 const vcl::Window
* GetBestParent(const vcl::Window
* pWindow
)
939 if (pWindow
->IsSystemWindow() && pWindow
->GetType() != WindowType::FLOATINGWINDOW
)
941 pWindow
= pWindow
->GetParent();
949 class HelpManualMessage
: public weld::MessageDialogController
952 std::unique_ptr
<weld::LinkButton
> m_xDownloadInfo
;
953 std::unique_ptr
<weld::CheckButton
> m_xHideOfflineHelpCB
;
955 DECL_LINK(DownloadClickHdl
, weld::LinkButton
&, bool);
957 HelpManualMessage(weld::Widget
* pParent
)
958 : MessageDialogController(pParent
, u
"sfx/ui/helpmanual.ui"_ustr
, u
"onlinehelpmanual"_ustr
, u
"box"_ustr
)
959 , m_xDownloadInfo(m_xBuilder
->weld_link_button(u
"downloadinfo"_ustr
))
960 , m_xHideOfflineHelpCB(m_xBuilder
->weld_check_button(u
"hidedialog"_ustr
))
962 LanguageType aLangType
= Application::GetSettings().GetUILanguageTag().getLanguageType();
963 OUString sLocaleString
= SvtLanguageTable::GetLanguageString(aLangType
);
964 OUString sPrimText
= get_primary_text();
965 set_primary_text(sPrimText
.replaceAll("$UILOCALE", sLocaleString
));
967 m_xDownloadInfo
->connect_activate_link(LINK(this, HelpManualMessage
, DownloadClickHdl
));
970 bool GetOfflineHelpPopUp() const { return !m_xHideOfflineHelpCB
->get_active(); }
973 IMPL_LINK(HelpManualMessage
, DownloadClickHdl
, weld::LinkButton
&, /* rButton */, bool)
975 m_xDialog
->response(RET_YES
);
981 bool SfxHelp::Start_Impl(const OUString
& rURL
, const vcl::Window
* pWindow
)
983 OUStringBuffer
aHelpRootURL("vnd.sun.star.help://");
984 AppendConfigToken(aHelpRootURL
, true);
985 SfxContentHelper::GetResultSet(aHelpRootURL
.makeStringAndClear());
989 * - a HelpID (formerly a long, now a string)
990 * If rURL is a URL, CreateHelpURL should be called for this URL
991 * If rURL is an arbitrary string, the same should happen, but the URL should be tried out
992 * if it delivers real help content. In case only the Help Error Document is returned, the
993 * parent of the window for that help was called, is asked for its HelpID.
994 * For compatibility reasons this upward search is not implemented for "real" URLs.
995 * Help keyword search now is implemented as own method; in former versions it
996 * was done via Help::Start, but this implementation conflicted with the upward search.
999 INetURLObject
aParser( rURL
);
1000 INetProtocol nProtocol
= aParser
.GetProtocol();
1002 switch ( nProtocol
)
1004 case INetProtocol::VndSunStarHelp
:
1005 // already a vnd.sun.star.help URL -> nothing to do
1010 OUString
aHelpModuleName(GetHelpModuleName_Impl(rURL
));
1011 OUString aRealCommand
;
1013 if ( nProtocol
== INetProtocol::Uno
)
1015 // Command can be just an alias to another command.
1016 auto aProperties
= vcl::CommandInfoProvider::GetCommandProperties(rURL
, getCurrentModuleIdentifier_Impl());
1017 aRealCommand
= vcl::CommandInfoProvider::GetRealCommandForCommand(aProperties
);
1020 // no URL, just a HelpID (maybe empty in case of keyword search)
1021 aHelpURL
= CreateHelpURL_Impl( aRealCommand
.isEmpty() ? rURL
: aRealCommand
, aHelpModuleName
);
1023 if ( impl_hasHelpInstalled() && pWindow
&& SfxContentHelper::IsHelpErrorDocument( aHelpURL
) )
1025 // no help found -> try with parent help id.
1026 vcl::Window
* pParent
= pWindow
->GetParent();
1029 OUString aHelpId
= pParent
->GetHelpId();
1030 aHelpURL
= CreateHelpURL( aHelpId
, aHelpModuleName
);
1032 if ( !SfxContentHelper::IsHelpErrorDocument( aHelpURL
) )
1038 pParent
= pParent
->GetParent();
1041 // create help url of start page ( helpid == 0 -> start page)
1042 aHelpURL
= CreateHelpURL( OUString(), aHelpModuleName
);
1051 pWindow
= GetBestParent(pWindow
);
1052 weld::Window
* pWeldWindow
= pWindow
? pWindow
->GetFrameWeld() : nullptr;
1054 if ( comphelper::LibreOfficeKit::isActive() )
1056 impl_showOnlineHelp(aHelpURL
, pWeldWindow
);
1060 if (@
available(macOS
10.14, *)) {
1061 // Workaround: Safari sandboxing prevents it from accessing files in the LibreOffice.app folder
1062 // force online-help instead if Safari is default browser.
1063 CFURLRef pBrowser
= LSCopyDefaultApplicationURLForURL(
1064 CFURLCreateWithString(
1065 kCFAllocatorDefault
,
1066 static_cast<CFStringRef
>(@
"https://www.libreoffice.org"),
1068 kLSRolesAll
, nullptr);
1069 if([static_cast<NSString
*>(CFURLGetString(pBrowser
)) hasSuffix
:@
"/Applications/Safari.app/"]) {
1070 impl_showOnlineHelp(aHelpURL
, pWeldWindow
);
1076 // If the HTML or no help is installed, but aHelpURL nevertheless references valid help content,
1077 // that implies that this help content belongs to an extension (and thus would not be available
1078 // in neither the offline nor online HTML help); in that case, fall through to the "old-help to
1079 // display" code below:
1080 if (SfxContentHelper::IsHelpErrorDocument(aHelpURL
))
1082 if ( impl_hasHTMLHelpInstalled() && impl_showOfflineHelp(aHelpURL
, pWeldWindow
) )
1087 if ( !impl_hasHelpInstalled() )
1089 bool bShowOfflineHelpPopUp
= officecfg::Office::Common::Help::BuiltInHelpNotInstalledPopUp::get();
1090 short retOnlineHelpBox
= RET_CLOSE
;
1092 TopLevelWindowLocker aBusy
;
1094 if(bShowOfflineHelpPopUp
)
1096 aBusy
.incBusy(pWeldWindow
);
1097 HelpManualMessage
aQueryBox(pWeldWindow
);
1098 retOnlineHelpBox
= aQueryBox
.run();
1099 auto xChanges
= comphelper::ConfigurationChanges::create();
1100 officecfg::Office::Common::Help::BuiltInHelpNotInstalledPopUp::set(aQueryBox
.GetOfflineHelpPopUp(), xChanges
);
1104 // Checks whether the user clicked "Read Help Online" (RET_OK) or "Information on downloading offline help" (RET_YES)
1105 if(!bShowOfflineHelpPopUp
|| retOnlineHelpBox
== RET_OK
|| retOnlineHelpBox
== RET_YES
)
1109 if (!bShowOfflineHelpPopUp
|| retOnlineHelpBox
== RET_OK
)
1111 bTopicExists
= impl_showOnlineHelp(aHelpURL
, pWeldWindow
);
1115 // Opens the help page that explains how to install offline help
1116 OUString
aOfflineHelpURL(CreateHelpURL_Impl(HID_HELPMANUAL_OFFLINE
, u
"shared"_ustr
));
1117 impl_showOnlineHelp(aOfflineHelpURL
, pWeldWindow
);
1118 bTopicExists
= true;
1123 aBusy
.incBusy(pWeldWindow
);
1124 NoHelpErrorBox
aErrBox(pWeldWindow
);
1141 // old-help to display
1142 Reference
< XDesktop2
> xDesktop
= Desktop::create( ::comphelper::getProcessComponentContext() );
1144 // check if help window is still open
1145 // If not, create a new one and return access directly to the internal sub frame showing the help content
1146 // search must be done here; search one desktop level could return an arbitrary frame
1147 Reference
< XFrame2
> xHelp(
1148 xDesktop
->findFrame( u
"OFFICE_HELP_TASK"_ustr
, FrameSearchFlag::CHILDREN
),
1150 Reference
< XFrame
> xHelpContent
= xDesktop
->findFrame(
1151 u
"OFFICE_HELP"_ustr
,
1152 FrameSearchFlag::CHILDREN
);
1154 SfxHelpWindow_Impl
* pHelpWindow
= nullptr;
1156 pHelpWindow
= impl_createHelp(xHelp
, xHelpContent
);
1158 pHelpWindow
= static_cast<SfxHelpWindow_Impl
*>(VCLUnoHelper::GetWindow(xHelp
->getComponentWindow()));
1159 if (!xHelp
.is() || !xHelpContent
.is() || !pHelpWindow
)
1162 SAL_INFO("sfx.appl", "HelpId = " << aHelpURL
);
1164 pHelpWindow
->SetHelpURL( aHelpURL
);
1165 pHelpWindow
->loadHelpContent(aHelpURL
);
1167 Reference
< css::awt::XTopWindow
> xTopWindow( xHelp
->getContainerWindow(), UNO_QUERY
);
1168 if ( xTopWindow
.is() )
1169 xTopWindow
->toFront();
1174 bool SfxHelp::Start_Impl(const OUString
& rURL
, weld::Widget
* pWidget
, const OUString
& rKeyword
)
1176 OUStringBuffer
aHelpRootURL("vnd.sun.star.help://");
1177 AppendConfigToken(aHelpRootURL
, true);
1178 SfxContentHelper::GetResultSet(aHelpRootURL
.makeStringAndClear());
1182 * - a HelpID (formerly a long, now a string)
1183 * If rURL is a URL, CreateHelpURL should be called for this URL
1184 * If rURL is an arbitrary string, the same should happen, but the URL should be tried out
1185 * if it delivers real help content. In case only the Help Error Document is returned, the
1186 * parent of the window for that help was called, is asked for its HelpID.
1187 * For compatibility reasons this upward search is not implemented for "real" URLs.
1188 * Help keyword search now is implemented as own method; in former versions it
1189 * was done via Help::Start, but this implementation conflicted with the upward search.
1192 INetURLObject
aParser( rURL
);
1193 INetProtocol nProtocol
= aParser
.GetProtocol();
1195 switch ( nProtocol
)
1197 case INetProtocol::VndSunStarHelp
:
1198 // already a vnd.sun.star.help URL -> nothing to do
1203 OUString
aHelpModuleName(GetHelpModuleName_Impl(rURL
));
1204 OUString aRealCommand
;
1206 if ( nProtocol
== INetProtocol::Uno
)
1208 // Command can be just an alias to another command.
1209 auto aProperties
= vcl::CommandInfoProvider::GetCommandProperties(rURL
, getCurrentModuleIdentifier_Impl());
1210 aRealCommand
= vcl::CommandInfoProvider::GetRealCommandForCommand(aProperties
);
1213 // no URL, just a HelpID (maybe empty in case of keyword search)
1214 aHelpURL
= CreateHelpURL_Impl( aRealCommand
.isEmpty() ? rURL
: aRealCommand
, aHelpModuleName
);
1216 if ( impl_hasHelpInstalled() && pWidget
&& SfxContentHelper::IsHelpErrorDocument( aHelpURL
) )
1218 bool bUseFinalFallback
= true;
1219 // no help found -> try ids of parents.
1220 pWidget
->help_hierarchy_foreach([&aHelpModuleName
, &aHelpURL
, &bUseFinalFallback
](const OUString
& rHelpId
){
1221 if (rHelpId
.isEmpty())
1223 aHelpURL
= CreateHelpURL(rHelpId
, aHelpModuleName
);
1224 bool bFinished
= !SfxContentHelper::IsHelpErrorDocument(aHelpURL
);
1226 bUseFinalFallback
= false;
1230 if (bUseFinalFallback
)
1232 // create help url of start page ( helpid == 0 -> start page)
1233 aHelpURL
= CreateHelpURL( OUString(), aHelpModuleName
);
1240 if ( comphelper::LibreOfficeKit::isActive() )
1242 impl_showOnlineHelp(aHelpURL
, pWidget
);
1246 if (@
available(macOS
10.14, *)) {
1247 // Workaround: Safari sandboxing prevents it from accessing files in the LibreOffice.app folder
1248 // force online-help instead if Safari is default browser.
1249 CFURLRef pBrowser
= LSCopyDefaultApplicationURLForURL(
1250 CFURLCreateWithString(
1251 kCFAllocatorDefault
,
1252 static_cast<CFStringRef
>(@
"https://www.libreoffice.org"),
1254 kLSRolesAll
, nullptr);
1255 if([static_cast<NSString
*>(CFURLGetString(pBrowser
)) hasSuffix
:@
"/Applications/Safari.app/"]) {
1256 impl_showOnlineHelp(aHelpURL
, pWidget
);
1262 // If the HTML or no help is installed, but aHelpURL nevertheless references valid help content,
1263 // that implies that help content belongs to an extension (and thus would not be available
1264 // in neither the offline nor online HTML help); in that case, fall through to the "old-help to
1265 // display" code below:
1266 if (SfxContentHelper::IsHelpErrorDocument(aHelpURL
))
1268 if ( impl_hasHTMLHelpInstalled() && impl_showOfflineHelp(aHelpURL
, pWidget
) )
1273 if ( !impl_hasHelpInstalled() )
1275 bool bShowOfflineHelpPopUp
= officecfg::Office::Common::Help::BuiltInHelpNotInstalledPopUp::get();
1276 short retOnlineHelpBox
= RET_CLOSE
;
1278 TopLevelWindowLocker aBusy
;
1280 if(bShowOfflineHelpPopUp
)
1282 aBusy
.incBusy(pWidget
);
1283 HelpManualMessage
aQueryBox(pWidget
);
1284 retOnlineHelpBox
= aQueryBox
.run();
1285 auto xChanges
= comphelper::ConfigurationChanges::create();
1286 officecfg::Office::Common::Help::BuiltInHelpNotInstalledPopUp::set(aQueryBox
.GetOfflineHelpPopUp(), xChanges
);
1290 // Checks whether the user clicked "Read Help Online" (RET_OK) or "Information on downloading offline help" (RET_YES)
1291 if(!bShowOfflineHelpPopUp
|| retOnlineHelpBox
== RET_OK
|| retOnlineHelpBox
== RET_YES
)
1295 if (!bShowOfflineHelpPopUp
|| retOnlineHelpBox
== RET_OK
)
1297 bTopicExists
= impl_showOnlineHelp(aHelpURL
, pWidget
);
1301 // Opens the help page that explains how to install offline help
1302 OUString
aOfflineHelpURL(CreateHelpURL_Impl(HID_HELPMANUAL_OFFLINE
, u
"shared"_ustr
));
1303 impl_showOnlineHelp(aOfflineHelpURL
, pWidget
);
1304 bTopicExists
= true;
1309 aBusy
.incBusy(pWidget
);
1310 NoHelpErrorBox
aErrBox(pWidget
);
1327 // old-help to display
1328 Reference
< XDesktop2
> xDesktop
= Desktop::create( ::comphelper::getProcessComponentContext() );
1330 // check if help window is still open
1331 // If not, create a new one and return access directly to the internal sub frame showing the help content
1332 // search must be done here; search one desktop level could return an arbitrary frame
1333 Reference
< XFrame2
> xHelp(
1334 xDesktop
->findFrame( u
"OFFICE_HELP_TASK"_ustr
, FrameSearchFlag::CHILDREN
),
1336 Reference
< XFrame
> xHelpContent
= xDesktop
->findFrame(
1337 u
"OFFICE_HELP"_ustr
,
1338 FrameSearchFlag::CHILDREN
);
1340 SfxHelpWindow_Impl
* pHelpWindow
= nullptr;
1342 pHelpWindow
= impl_createHelp(xHelp
, xHelpContent
);
1344 pHelpWindow
= static_cast<SfxHelpWindow_Impl
*>(VCLUnoHelper::GetWindow(xHelp
->getComponentWindow()));
1345 if (!xHelp
.is() || !xHelpContent
.is() || !pHelpWindow
)
1348 SAL_INFO("sfx.appl", "HelpId = " << aHelpURL
);
1350 pHelpWindow
->SetHelpURL( aHelpURL
);
1351 pHelpWindow
->loadHelpContent(aHelpURL
);
1352 if (!rKeyword
.isEmpty())
1353 pHelpWindow
->OpenKeyword( rKeyword
);
1355 Reference
< css::awt::XTopWindow
> xTopWindow( xHelp
->getContainerWindow(), UNO_QUERY
);
1356 if ( xTopWindow
.is() )
1357 xTopWindow
->toFront();
1362 OUString
SfxHelp::CreateHelpURL(const OUString
& aCommandURL
, const OUString
& rModuleName
)
1364 SfxHelp
* pHelp
= static_cast< SfxHelp
* >(Application::GetHelp());
1365 return pHelp
? SfxHelp::CreateHelpURL_Impl( aCommandURL
, rModuleName
) : OUString();
1368 OUString
SfxHelp::GetDefaultHelpModule()
1370 return getDefaultModule_Impl();
1373 OUString
SfxHelp::GetCurrentModuleIdentifier()
1375 return getCurrentModuleIdentifier_Impl();
1378 bool SfxHelp::IsHelpInstalled()
1380 return impl_hasHelpInstalled();
1383 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */