diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:06:44 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:06:44 +0000 |
commit | ed5640d8b587fbcfed7dd7967f3de04b37a76f26 (patch) | |
tree | 7a5f7c6c9d02226d7471cb3cc8fbbf631b415303 /fpicker/source/win32/VistaFilePickerImpl.cxx | |
parent | Initial commit. (diff) | |
download | libreoffice-ed5640d8b587fbcfed7dd7967f3de04b37a76f26.tar.xz libreoffice-ed5640d8b587fbcfed7dd7967f3de04b37a76f26.zip |
Adding upstream version 4:7.4.7.upstream/4%7.4.7upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'fpicker/source/win32/VistaFilePickerImpl.cxx')
-rw-r--r-- | fpicker/source/win32/VistaFilePickerImpl.cxx | 1249 |
1 files changed, 1249 insertions, 0 deletions
diff --git a/fpicker/source/win32/VistaFilePickerImpl.cxx b/fpicker/source/win32/VistaFilePickerImpl.cxx new file mode 100644 index 000000000..741fdadb6 --- /dev/null +++ b/fpicker/source/win32/VistaFilePickerImpl.cxx @@ -0,0 +1,1249 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <memory> + +#include "VistaFilePickerImpl.hxx" + +#include <com/sun/star/ui/dialogs/ExtendedFilePickerElementIds.hpp> +#include <com/sun/star/ui/dialogs/ControlActions.hpp> +#include <com/sun/star/ui/dialogs/TemplateDescription.hpp> +#include <com/sun/star/beans/StringPair.hpp> +#include <com/sun/star/awt/XWindow.hpp> +#include <com/sun/star/awt/XSystemDependentWindowPeer.hpp> +#include <com/sun/star/lang/SystemDependent.hpp> +#include <comphelper/sequence.hxx> +#include <fpicker/strings.hrc> +#include <fpicker/fpsofficeResMgr.hxx> +#include <osl/file.hxx> +#include <rtl/process.h> +#include <o3tl/char16_t2wchar_t.hxx> +#include <o3tl/string_view.hxx> +#include <vcl/svapp.hxx> +#include "WinImplHelper.hxx" + +#include <shlguid.h> +#include <shlobj.h> + +static bool is_current_process_window(HWND hwnd) +{ + DWORD pid; + GetWindowThreadProcessId(hwnd, &pid); + return (pid == GetCurrentProcessId()); +} + +static HWND choose_parent_window() +{ + HWND hwnd_parent = GetForegroundWindow(); + if (!is_current_process_window(hwnd_parent)) + hwnd_parent = GetDesktopWindow(); + return hwnd_parent; +} + +namespace { + +bool createFolderItem(OUString const& url, sal::systools::COMReference<IShellItem>& folder) +{ + OUString path; + if (osl::FileBase::getSystemPathFromFileURL(url, path) + != osl::FileBase::E_None) + { + return false; + } + HRESULT res = SHCreateItemFromParsingName( + o3tl::toW(path.getStr()), nullptr, + IID_PPV_ARGS(&folder)); + return SUCCEEDED(res); +} + +} + +namespace fpicker{ +namespace win32{ +namespace vista{ + + +// types, const etcpp. + + +const ::sal_Int16 INVALID_CONTROL_ID = -1; +const ::sal_Int16 INVALID_CONTROL_ACTION = -1; + +// Guids used for IFileDialog::SetClientGuid +const GUID CLIENTID_FILEDIALOG_SIMPLE = {0xB8628FD3, 0xA3F5, 0x4845, 0x9B, 0x62, 0xD5, 0x1E, 0xDF, 0x97, 0xC4, 0x83}; +const GUID CLIENTID_FILEDIALOG_OPTIONS = {0x93ED486F, 0x0D04, 0x4807, 0x8C, 0x44, 0xAC, 0x26, 0xCB, 0x6C, 0x5D, 0x36}; +const GUID CLIENTID_FILESAVE_PASSWORD = {0xC12D4F4C, 0x4D41, 0x4D4F, 0x97, 0xEF, 0x87, 0xF9, 0x8D, 0xB6, 0x1E, 0xA6}; +const GUID CLIENTID_FILESAVE_SELECTION = {0x5B2482B3, 0x0358, 0x4E09, 0xAA, 0x64, 0x2B, 0x76, 0xB2, 0xA0, 0xDD, 0xFE}; +const GUID CLIENTID_FILESAVE_TEMPLATE = {0x9996D877, 0x20D5, 0x424B, 0x9C, 0x2E, 0xD3, 0xB6, 0x31, 0xEC, 0xF7, 0xCE}; +const GUID CLIENTID_FILEOPEN_LINK_TEMPLATE = {0x32237796, 0x1509, 0x49D1, 0xBB, 0x7E, 0x63, 0xAD, 0x36, 0xAE, 0x86, 0x8C}; +const GUID CLIENTID_FILEOPEN_LINK_ANCHOR = {0xBE3188CB, 0x399A, 0x45AE, 0x8F, 0x78, 0x75, 0x17, 0xAF, 0x26, 0x81, 0xEA}; +const GUID CLIENTID_FILEOPEN_PLAY = {0x32CFB147, 0xF5AE, 0x4F90, 0xA1, 0xF1, 0x81, 0x20, 0x72, 0xBB, 0x2F, 0xC5}; +const GUID CLIENTID_FILEOPEN_LINK = {0x39AC4BAE, 0x7D2D, 0x46BC, 0xBE, 0x2E, 0xF8, 0x8C, 0xB5, 0x65, 0x5E, 0x6A}; + + +class TDialogImplBase +{ +public: + TDialogImplBase(IFileDialog* iDialog) + : m_iDialog(iDialog) + { + } + + virtual ~TDialogImplBase() = default; + + TFileDialog getComPtr() { return m_iDialog; } + virtual sal::systools::COMReference<IShellItemArray> getResult(bool bInExecute) + { + sal::systools::COMReference<IShellItem> iItem; + if (m_iDialog.is()) + { + if (bInExecute) + m_iDialog->GetCurrentSelection(&iItem); + else + m_iDialog->GetResult(&iItem); + } + void* iItems = nullptr; + if (iItem.is()) + SHCreateShellItemArrayFromShellItem(iItem.get(), IID_IShellItemArray, &iItems); + return static_cast<IShellItemArray*>(iItems); + } + +private: + TFileDialog m_iDialog; +}; + +namespace { + +template <class ComPtrDialog, REFCLSID CLSID> class TDialogImpl : public TDialogImplBase +{ +public: + TDialogImpl() + : TDialogImplBase(ComPtrDialog(CLSID).get()) + { + } +}; + +class TOpenDialogImpl : public TDialogImpl<TFileOpenDialog, CLSID_FileOpenDialog> +{ +public: + sal::systools::COMReference<IShellItemArray> getResult(bool bInExecute) override + { + sal::systools::COMReference<IShellItemArray> iItems; + TFileOpenDialog iDialog(getComPtr(), sal::systools::COM_QUERY_THROW); + bool bGetResult = false; + if (!iDialog.is()) + bGetResult = true; + else if (FAILED(bInExecute ? iDialog->GetSelectedItems(&iItems) : iDialog->GetResults(&iItems))) + bGetResult = true; + + if (bGetResult) + iItems = TDialogImplBase::getResult(bInExecute); + + return iItems; + } +}; + +} + +using TSaveDialogImpl = TDialogImpl<TFileSaveDialog, CLSID_FileSaveDialog>; +using TFolderPickerDialogImpl = TDialogImpl<TFileOpenDialog, CLSID_FileOpenDialog>; + + +static OUString lcl_getURLFromShellItem (IShellItem* pItem) +{ + LPWSTR pStr = nullptr; + OUString sURL; + HRESULT hr; + + hr = pItem->GetDisplayName ( SIGDN_FILESYSPATH, &pStr ); + if (SUCCEEDED(hr)) + { + ::osl::FileBase::getFileURLFromSystemPath( OUString(o3tl::toU(pStr)), sURL ); + goto cleanup; + } + + hr = pItem->GetDisplayName ( SIGDN_URL, &pStr ); + if (SUCCEEDED(hr)) + { + sURL = o3tl::toU(pStr); + goto cleanup; + } + + hr = pItem->GetDisplayName ( SIGDN_PARENTRELATIVEPARSING, &pStr ); + if (SUCCEEDED(hr)) + { + GUID known_folder_id; + std::wstring aStr = pStr; + CoTaskMemFree (pStr); + + if (0 == aStr.compare(0, 3, L"::{")) + aStr = aStr.substr(2); + hr = IIDFromString(aStr.c_str(), &known_folder_id); + if (SUCCEEDED(hr)) + { + hr = SHGetKnownFolderPath(known_folder_id, 0, nullptr, &pStr); + if (SUCCEEDED(hr)) + { + ::osl::FileBase::getFileURLFromSystemPath(OUString(o3tl::toU(pStr)), sURL); + goto cleanup; + } + } + } + + // Default fallback + hr = SHGetKnownFolderPath(FOLDERID_Documents, 0, nullptr, &pStr); + if (SUCCEEDED(hr)) + ::osl::FileBase::getFileURLFromSystemPath(OUString(o3tl::toU(pStr)), sURL); + else // shouldn't happen... + goto bailout; + +cleanup: + CoTaskMemFree (pStr); +bailout: + return sURL; +} + +// Vista file picker shows the filter mask next to filter name in the list; so we need to remove the +// mask from the filter name to avoid duplicating masks +static OUString lcl_AdjustFilterName(const OUString& sName) +{ + const sal_Int32 idx = sName.indexOf("(."); + return (idx > 0) ? OUString(o3tl::trim(sName.subView(0, idx))) : sName; +} + +// rvStrings holds the OUStrings, pointers to which data are stored in returned COMDLG_FILTERSPEC +static ::std::vector<COMDLG_FILTERSPEC> lcl_buildFilterList(CFilterContainer& rContainer, + std::vector<OUString>& rvStrings) +{ + ::std::vector< COMDLG_FILTERSPEC > lList ; + CFilterContainer::FILTER_ENTRY_T aFilter; + + rContainer.beginEnumFilter( ); + while( rContainer.getNextFilter(aFilter) ) + { + COMDLG_FILTERSPEC aSpec; + + rvStrings.push_back(lcl_AdjustFilterName(aFilter.first)); // to avoid dangling pointer + aSpec.pszName = o3tl::toW(rvStrings.back().getStr()); + aSpec.pszSpec = o3tl::toW(aFilter.second.getStr()); + + lList.push_back(aSpec); + } + + return lList; +} + + +VistaFilePickerImpl::VistaFilePickerImpl() + : m_lFilters () + , m_iEventHandler(new VistaFilePickerEventHandler(this)) + , m_bInExecute (false) + , m_bWasExecuted (false) + , m_hParentWindow(nullptr) + , m_sDirectory () + , m_sFilename () +{ +} + + +VistaFilePickerImpl::~VistaFilePickerImpl() +{ +} + + +void VistaFilePickerImpl::doRequest(Request& rRequest) +{ + try + { + switch(rRequest.getRequest()) + { + case E_ADD_PICKER_LISTENER : + impl_sta_addFilePickerListener(rRequest); + break; + + case E_REMOVE_PICKER_LISTENER : + impl_sta_removeFilePickerListener(rRequest); + break; + + case E_APPEND_FILTER : + impl_sta_appendFilter(rRequest); + break; + + case E_APPEND_FILTERGROUP : + impl_sta_appendFilterGroup(rRequest); + break; + + case E_SET_CURRENT_FILTER : + impl_sta_setCurrentFilter(rRequest); + break; + + case E_GET_CURRENT_FILTER : + impl_sta_getCurrentFilter(rRequest); + break; + + case E_CREATE_OPEN_DIALOG : + impl_sta_CreateOpenDialog(rRequest); + break; + + case E_CREATE_SAVE_DIALOG : + impl_sta_CreateSaveDialog(rRequest); + break; + + case E_CREATE_FOLDER_PICKER: + impl_sta_CreateFolderPicker(rRequest); + break; + + case E_SET_MULTISELECTION_MODE : + impl_sta_SetMultiSelectionMode(rRequest); + break; + + case E_SET_TITLE : + impl_sta_SetTitle(rRequest); + break; + + case E_SET_FILENAME: + impl_sta_SetFileName(rRequest); + break; + + case E_SET_DIRECTORY : + impl_sta_SetDirectory(rRequest); + break; + + case E_GET_DIRECTORY : + impl_sta_GetDirectory(rRequest); + break; + + case E_SET_DEFAULT_NAME : + impl_sta_SetDefaultName(rRequest); + break; + + case E_GET_SELECTED_FILES : + impl_sta_getSelectedFiles(rRequest); + break; + + case E_SHOW_DIALOG_MODAL : + impl_sta_ShowDialogModal(rRequest); + break; + + case E_SET_CONTROL_VALUE : + impl_sta_SetControlValue(rRequest); + break; + + case E_GET_CONTROL_VALUE : + impl_sta_GetControlValue(rRequest); + break; + + case E_SET_CONTROL_LABEL : + impl_sta_SetControlLabel(rRequest); + break; + + case E_GET_CONTROL_LABEL : + impl_sta_GetControlLabel(rRequest); + break; + + case E_ENABLE_CONTROL : + impl_sta_EnableControl(rRequest); + break; + + // no default: let the compiler detect changes on enum ERequest ! + } + } + catch(...) + {} +} + + +void VistaFilePickerImpl::impl_sta_addFilePickerListener(Request& rRequest) +{ + const css::uno::Reference< css::ui::dialogs::XFilePickerListener > xListener = rRequest.getArgumentOrDefault(PROP_PICKER_LISTENER, css::uno::Reference< css::ui::dialogs::XFilePickerListener >()); + if ( ! xListener.is()) + return; + + if (m_iEventHandler.is()) + { + auto* pHandlerImpl = static_cast<VistaFilePickerEventHandler*>(m_iEventHandler.get()); + pHandlerImpl->addFilePickerListener(xListener); + } +} + + +void VistaFilePickerImpl::impl_sta_removeFilePickerListener(Request& rRequest) +{ + const css::uno::Reference< css::ui::dialogs::XFilePickerListener > xListener = rRequest.getArgumentOrDefault(PROP_PICKER_LISTENER, css::uno::Reference< css::ui::dialogs::XFilePickerListener >()); + if ( ! xListener.is()) + return; + + if (m_iEventHandler.is()) + { + auto* pHandlerImpl = static_cast<VistaFilePickerEventHandler*>(m_iEventHandler.get()); + pHandlerImpl->removeFilePickerListener(xListener); + } +} + + +void VistaFilePickerImpl::impl_sta_appendFilter(Request& rRequest) +{ + const OUString sTitle = rRequest.getArgumentOrDefault(PROP_FILTER_TITLE, OUString()); + const OUString sFilter = rRequest.getArgumentOrDefault(PROP_FILTER_VALUE, OUString()); + + m_lFilters.addFilter(sTitle, sFilter); +} + + +void VistaFilePickerImpl::impl_sta_appendFilterGroup(Request& rRequest) +{ + const css::uno::Sequence< css::beans::StringPair > aFilterGroup = + rRequest.getArgumentOrDefault(PROP_FILTER_GROUP, css::uno::Sequence< css::beans::StringPair >()); + + if ( m_lFilters.numFilter() > 0 && aFilterGroup.getLength() > 0 ) + m_lFilters.addFilter( STRING_SEPARATOR, "", true ); + + ::sal_Int32 c = aFilterGroup.getLength(); + ::sal_Int32 i = 0; + for (i=0; i<c; ++i) + { + const css::beans::StringPair& rFilter = aFilterGroup[i]; + m_lFilters.addFilter(rFilter.First, rFilter.Second); + } +} + + +void VistaFilePickerImpl::impl_sta_setCurrentFilter(Request& rRequest) +{ + const OUString sTitle = rRequest.getArgumentOrDefault(PROP_FILTER_TITLE, OUString()); + + m_lFilters.setCurrentFilter(sTitle); +} + + +void VistaFilePickerImpl::impl_sta_getCurrentFilter(Request& rRequest) +{ + TFileDialog iDialog = impl_getBaseDialogInterface(); + if (!iDialog.is()) + return; + + UINT nIndex = UINT_MAX; + HRESULT hResult = iDialog->GetFileTypeIndex(&nIndex); + if ( + ( FAILED(hResult) ) || + ( nIndex == UINT_MAX ) // COM dialog sometimes return S_OK for empty filter lists .-( + ) + return; + + OUString sTitle; + ::sal_Int32 nRealIndex = nIndex-1; // COM dialog base on 1 ... filter container on 0 .-) + if ( + (nRealIndex >= 0 ) && + (m_lFilters.getFilterNameByIndex(nRealIndex, sTitle)) + ) + rRequest.setArgument(PROP_FILTER_TITLE, sTitle); + else if ( nRealIndex == -1 ) // Dialog not visible yet + { + sTitle = m_lFilters.getCurrentFilter(); + rRequest.setArgument(PROP_FILTER_TITLE, sTitle); + } +} + + +template <class TDialogImplClass> void VistaFilePickerImpl::impl_sta_CreateDialog() +{ + m_pDialog = std::make_shared<TDialogImplClass>(); +} + + +void VistaFilePickerImpl::impl_sta_InitDialog(Request& rRequest, DWORD nOrFlags) +{ + TFileDialog iDialog = impl_getBaseDialogInterface(); + if (!iDialog.is()) + return; + + DWORD nFlags = 0; + iDialog->GetOptions ( &nFlags ); + + nFlags &= ~FOS_FORCESHOWHIDDEN; + nFlags |= FOS_PATHMUSTEXIST; + nFlags |= FOS_DONTADDTORECENT; + nFlags |= nOrFlags; + + iDialog->SetOptions ( nFlags ); + + css::uno::Reference<css::awt::XWindow> xWindow = rRequest.getArgumentOrDefault(PROP_PARENT_WINDOW, css::uno::Reference<css::awt::XWindow>()); + if(xWindow.is()) + { + css::uno::Reference<css::awt::XSystemDependentWindowPeer> xSysDepWin(xWindow,css::uno::UNO_QUERY); + if(xSysDepWin.is()) { + css::uno::Sequence<sal_Int8> aProcessIdent(16); + rtl_getGlobalProcessId(reinterpret_cast<sal_uInt8*>(aProcessIdent.getArray())); + css::uno::Any aAny = xSysDepWin->getWindowHandle(aProcessIdent,css::lang::SystemDependent::SYSTEM_WIN32); + sal_Int64 tmp = 0; + aAny >>= tmp; + if(tmp != 0) + { + m_hParentWindow = reinterpret_cast<HWND>(tmp); + } + } + } + + ::sal_Int32 nFeatures = rRequest.getArgumentOrDefault(PROP_FEATURES, ::sal_Int32(0)); + ::sal_Int32 nTemplate = rRequest.getArgumentOrDefault(PROP_TEMPLATE_DESCR, ::sal_Int32(0)); + impl_sta_enableFeatures(nFeatures, nTemplate); + + if (m_iEventHandler.is()) + { + auto* pHandlerImpl = static_cast<VistaFilePickerEventHandler*>(m_iEventHandler.get()); + pHandlerImpl->startListening(iDialog); + } +} + + +void VistaFilePickerImpl::impl_sta_CreateOpenDialog(Request& rRequest) +{ + impl_sta_CreateDialog<TOpenDialogImpl>(); + impl_sta_InitDialog(rRequest, FOS_FILEMUSTEXIST | FOS_OVERWRITEPROMPT); +} + + +void VistaFilePickerImpl::impl_sta_CreateSaveDialog(Request& rRequest) +{ + impl_sta_CreateDialog<TSaveDialogImpl>(); + impl_sta_InitDialog(rRequest, FOS_FILEMUSTEXIST | FOS_OVERWRITEPROMPT); +} + + +void VistaFilePickerImpl::impl_sta_CreateFolderPicker(Request& rRequest) +{ + impl_sta_CreateDialog<TFolderPickerDialogImpl>(); + impl_sta_InitDialog(rRequest, FOS_PICKFOLDERS); +} + + +const ::sal_Int32 GROUP_VERSION = 1; +const ::sal_Int32 GROUP_TEMPLATE = 2; +const ::sal_Int32 GROUP_IMAGETEMPLATE = 3; +const ::sal_Int32 GROUP_CHECKBOXES = 4; +const ::sal_Int32 GROUP_IMAGEANCHOR = 5; + + +static void setLabelToControl(TFileDialogCustomize iCustom, sal_uInt16 nControlId) +{ + OUString aLabel = CResourceProvider::getResString(nControlId); + aLabel = SOfficeToWindowsLabel(aLabel); + iCustom->SetControlLabel(nControlId, o3tl::toW(aLabel.getStr()) ); +} + + +void VistaFilePickerImpl::impl_sta_enableFeatures(::sal_Int32 nFeatures, ::sal_Int32 nTemplate) +{ + GUID aGUID = {}; + switch (nTemplate) + { + case css::ui::dialogs::TemplateDescription::FILEOPEN_SIMPLE : + case css::ui::dialogs::TemplateDescription::FILEOPEN_PREVIEW : + case css::ui::dialogs::TemplateDescription::FILESAVE_SIMPLE : + aGUID = CLIENTID_FILEDIALOG_SIMPLE; + break; + + case css::ui::dialogs::TemplateDescription::FILEOPEN_READONLY_VERSION : + case css::ui::dialogs::TemplateDescription::FILESAVE_AUTOEXTENSION_PASSWORD_FILTEROPTIONS : + aGUID = CLIENTID_FILEDIALOG_OPTIONS; + break; + + case css::ui::dialogs::TemplateDescription::FILESAVE_AUTOEXTENSION_PASSWORD : + aGUID = CLIENTID_FILESAVE_PASSWORD; + break; + + case css::ui::dialogs::TemplateDescription::FILESAVE_AUTOEXTENSION : + case css::ui::dialogs::TemplateDescription::FILESAVE_AUTOEXTENSION_SELECTION : + aGUID = CLIENTID_FILESAVE_SELECTION; + break; + + case css::ui::dialogs::TemplateDescription::FILESAVE_AUTOEXTENSION_TEMPLATE : + aGUID = CLIENTID_FILESAVE_TEMPLATE; + break; + + case css::ui::dialogs::TemplateDescription::FILEOPEN_LINK_PREVIEW_IMAGE_TEMPLATE : + aGUID = CLIENTID_FILEOPEN_LINK_TEMPLATE; + break; + + case css::ui::dialogs::TemplateDescription::FILEOPEN_LINK_PREVIEW_IMAGE_ANCHOR : + aGUID = CLIENTID_FILEOPEN_LINK_ANCHOR; + break; + + case css::ui::dialogs::TemplateDescription::FILEOPEN_PLAY : + case css::ui::dialogs::TemplateDescription::FILEOPEN_LINK_PLAY : + aGUID = CLIENTID_FILEOPEN_PLAY; + break; + + case css::ui::dialogs::TemplateDescription::FILEOPEN_LINK_PREVIEW : + aGUID = CLIENTID_FILEOPEN_LINK; + break; + } + TFileDialog iDialog = impl_getBaseDialogInterface(); + if (iDialog.is()) + iDialog->SetClientGuid ( aGUID ); + + TFileDialogCustomize iCustom = impl_getCustomizeInterface(); + if (!iCustom.is()) + return; + + if ((nFeatures & FEATURE_VERSION) == FEATURE_VERSION) + { + iCustom->StartVisualGroup (GROUP_VERSION, o3tl::toW(FpsResId(STR_SVT_FILEPICKER_VERSION).replaceFirst("~","").getStr())); + iCustom->AddComboBox (css::ui::dialogs::ExtendedFilePickerElementIds::LISTBOX_VERSION); + iCustom->EndVisualGroup (); + iCustom->MakeProminent (GROUP_VERSION); + } + + if ((nFeatures & FEATURE_TEMPLATE) == FEATURE_TEMPLATE) + { + iCustom->StartVisualGroup (GROUP_TEMPLATE, o3tl::toW(FpsResId(STR_SVT_FILEPICKER_TEMPLATES).replaceFirst("~","").getStr())); + iCustom->AddComboBox (css::ui::dialogs::ExtendedFilePickerElementIds::LISTBOX_TEMPLATE); + iCustom->EndVisualGroup (); + iCustom->MakeProminent (GROUP_TEMPLATE); + } + + if ((nFeatures & FEATURE_IMAGETEMPLATE) == FEATURE_IMAGETEMPLATE) + { + iCustom->StartVisualGroup (GROUP_IMAGETEMPLATE, o3tl::toW(FpsResId(STR_SVT_FILEPICKER_IMAGE_TEMPLATE).replaceFirst("~","").getStr())); + iCustom->AddComboBox (css::ui::dialogs::ExtendedFilePickerElementIds::LISTBOX_IMAGE_TEMPLATE); + iCustom->EndVisualGroup (); + iCustom->MakeProminent (GROUP_IMAGETEMPLATE); + } + + if ((nFeatures & FEATURE_IMAGEANCHOR) == FEATURE_IMAGEANCHOR) + { + iCustom->StartVisualGroup (GROUP_IMAGEANCHOR, o3tl::toW(FpsResId(STR_SVT_FILEPICKER_IMAGE_ANCHOR).replaceFirst("~","").getStr())); + iCustom->AddComboBox (css::ui::dialogs::ExtendedFilePickerElementIds::LISTBOX_IMAGE_ANCHOR); + iCustom->EndVisualGroup (); + iCustom->MakeProminent (GROUP_IMAGEANCHOR); + } + + iCustom->StartVisualGroup (GROUP_CHECKBOXES, L""); + + sal_uInt16 nControlId(0); + if ((nFeatures & FEATURE_AUTOEXTENSION) == FEATURE_AUTOEXTENSION) + { + nControlId = css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_AUTOEXTENSION; + iCustom->AddCheckButton (nControlId, o3tl::toW(FpsResId(STR_SVT_FILEPICKER_AUTO_EXTENSION).replaceFirst("~","").getStr()), true); + setLabelToControl(iCustom, nControlId); + } + + if ((nFeatures & FEATURE_PASSWORD) == FEATURE_PASSWORD) + { + nControlId = css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_PASSWORD; + iCustom->AddCheckButton (nControlId, o3tl::toW(FpsResId(STR_SVT_FILEPICKER_PASSWORD).replaceFirst("~","").getStr()), false); + setLabelToControl(iCustom, nControlId); + } + + if ((nFeatures & FEATURE_GPGPASSWORD) == FEATURE_GPGPASSWORD) + { + nControlId = css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_GPGENCRYPTION; + iCustom->AddCheckButton (nControlId, L"GpgPassword", false); + setLabelToControl(iCustom, nControlId); + } + + if ((nFeatures & FEATURE_READONLY) == FEATURE_READONLY) + { + nControlId = css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_READONLY; + iCustom->AddCheckButton (nControlId, o3tl::toW(FpsResId(STR_SVT_FILEPICKER_READONLY).replaceFirst("~","").getStr()), false); + setLabelToControl(iCustom, nControlId); + } + + if ((nFeatures & FEATURE_FILTEROPTIONS) == FEATURE_FILTEROPTIONS) + { + nControlId = css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_FILTEROPTIONS; + iCustom->AddCheckButton (nControlId, o3tl::toW(FpsResId(STR_SVT_FILEPICKER_FILTER_OPTIONS).replaceFirst("~","").getStr()), false); + setLabelToControl(iCustom, nControlId); + } + + if ((nFeatures & FEATURE_LINK) == FEATURE_LINK) + { + nControlId = css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_LINK; + iCustom->AddCheckButton (nControlId, o3tl::toW(FpsResId(STR_SVT_FILEPICKER_INSERT_AS_LINK).replaceFirst("~","").getStr()), false); + setLabelToControl(iCustom, nControlId); + } + + if ((nFeatures & FEATURE_SELECTION) == FEATURE_SELECTION) + { + nControlId = css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_SELECTION; + iCustom->AddCheckButton (nControlId, o3tl::toW(FpsResId(STR_SVT_FILEPICKER_SELECTION).replaceFirst("~","").getStr()), false); + setLabelToControl(iCustom, nControlId); + } + + /* can be ignored ... new COM dialog supports preview native now ! + if ((nFeatures & FEATURE_PREVIEW) == FEATURE_PREVIEW) + iCustom->AddCheckButton (css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_PREVIEW, L"Preview", false); + */ + + iCustom->EndVisualGroup(); + + if ((nFeatures & FEATURE_PLAY) == FEATURE_PLAY) + iCustom->AddPushButton (css::ui::dialogs::ExtendedFilePickerElementIds::PUSHBUTTON_PLAY, o3tl::toW(FpsResId(STR_SVT_FILEPICKER_PLAY).replaceFirst("~","").getStr())); + +} + + +void VistaFilePickerImpl::impl_sta_SetMultiSelectionMode(Request& rRequest) +{ + const bool bMultiSelection = rRequest.getArgumentOrDefault(PROP_MULTISELECTION_MODE, true); + + TFileDialog iDialog = impl_getBaseDialogInterface(); + if (!iDialog.is()) + return; + + DWORD nFlags = 0; + iDialog->GetOptions(&nFlags); + + if (bMultiSelection) + nFlags |= FOS_ALLOWMULTISELECT; + else + nFlags &= ~FOS_ALLOWMULTISELECT; + + iDialog->SetOptions ( nFlags ); +} + + +void VistaFilePickerImpl::impl_sta_SetTitle(Request& rRequest) +{ + OUString sTitle = rRequest.getArgumentOrDefault(PROP_TITLE, OUString()); + + TFileDialog iDialog = impl_getBaseDialogInterface(); + if (!iDialog.is()) + return; + + iDialog->SetTitle(o3tl::toW(sTitle.getStr())); +} + + +void VistaFilePickerImpl::impl_sta_SetFileName(Request& rRequest) +{ + OUString sFileName = rRequest.getArgumentOrDefault(PROP_FILENAME, OUString()); + + TFileDialog iDialog = impl_getBaseDialogInterface(); + if (!iDialog.is()) + return; + + iDialog->SetFileName(o3tl::toW(sFileName.getStr())); +} + + +void VistaFilePickerImpl::impl_sta_SetDirectory(Request& rRequest) +{ + OUString sDirectory = rRequest.getArgumentOrDefault(PROP_DIRECTORY, OUString()); + + if( !m_bInExecute) + { + // Vista stores last used folders for file dialogs + // so we don't want the application to change the folder + // in most cases. + // Store the requested folder in the meantime and decide later + // what to do + m_sDirectory = sDirectory; + } + + TFileDialog iDialog = impl_getBaseDialogInterface(); + if (!iDialog.is()) + return; + + sal::systools::COMReference<IShellItem> pFolder; + if ( !createFolderItem(sDirectory, pFolder) ) + return; + + iDialog->SetFolder(pFolder.get()); +} + +OUString VistaFilePickerImpl::GetDirectory() +{ + TFileDialog iDialog = impl_getBaseDialogInterface(); + if (!iDialog.is()) + return OUString(); + sal::systools::COMReference<IShellItem> pFolder; + HRESULT hResult = iDialog->GetFolder( &pFolder ); + if ( FAILED(hResult) ) + return OUString(); + return lcl_getURLFromShellItem(pFolder.get()); +} + +void VistaFilePickerImpl::impl_sta_GetDirectory(Request& rRequest) +{ + const OUString sFolder = m_sDirectory.isEmpty() ? GetDirectory() : m_sDirectory; + if (!sFolder.isEmpty()) + rRequest.setArgument(PROP_DIRECTORY, sFolder); +} + +void VistaFilePickerImpl::impl_sta_SetDefaultName(Request& rRequest) +{ + OUString sFilename = rRequest.getArgumentOrDefault(PROP_FILENAME, OUString()); + TFileDialog iDialog = impl_getBaseDialogInterface(); + if (!iDialog.is()) + return; + + TFileDialogCustomize iCustom = impl_getCustomizeInterface(); + if ( ! iCustom.is()) + return; + + // if we have the autoextension check box set, remove (or change ???) the extension of the filename + // so that the autoextension mechanism can do its job + BOOL bValue = FALSE; + HRESULT hResult = iCustom->GetCheckButtonState( css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_AUTOEXTENSION, &bValue); + if ( FAILED(hResult) ) + return; + if ( bValue ) + { + sal_Int32 nSepPos = sFilename.lastIndexOf( '.' ); + if ( -1 != nSepPos ) + sFilename = sFilename.copy(0, nSepPos); + } + + iDialog->SetFileName (o3tl::toW(sFilename.getStr())); + m_sFilename = sFilename; +} + + +void VistaFilePickerImpl::impl_sta_setFiltersOnDialog() +{ + std::vector<OUString> vStrings; // to hold the adjusted filter names, pointers to which will be + // stored in lFilters + ::std::vector< COMDLG_FILTERSPEC > lFilters = lcl_buildFilterList(m_lFilters, vStrings); + OUString sCurrentFilter = m_lFilters.getCurrentFilter(); + sal_Int32 nCurrentFilter = m_lFilters.getFilterPos(sCurrentFilter); + TFileDialog iDialog = impl_getBaseDialogInterface(); + if (!iDialog.is()) + return; + TFileDialogCustomize iCustomize = impl_getCustomizeInterface(); + if (!iCustomize.is()) + return; + + if (lFilters.empty()) + return; + + COMDLG_FILTERSPEC *pFilt = lFilters.data(); + iDialog->SetFileTypes(lFilters.size(), pFilt/*&lFilters[0]*/); + iDialog->SetFileTypeIndex(nCurrentFilter + 1); + + BOOL bValue = FALSE; + HRESULT hResult = iCustomize->GetCheckButtonState( css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_AUTOEXTENSION, &bValue); + if ( FAILED(hResult) ) + return; + + if ( bValue ) + { + PCWSTR lpFilterExt = lFilters[0].pszSpec; + + lpFilterExt = wcsrchr( lpFilterExt, '.' ); + if ( lpFilterExt ) + lpFilterExt++; + iDialog->SetDefaultExtension( lpFilterExt ); + } + +} + + +void VistaFilePickerImpl::impl_sta_getSelectedFiles(Request& rRequest) +{ + if (m_pDialog == nullptr) + return; + + // ask dialog for results + // we must react different if dialog is in execute or not .-( + sal::systools::COMReference<IShellItemArray> iItems = m_pDialog->getResult(m_bInExecute); + if (!iItems.is()) + return; + + // convert and pack results + std::vector< OUString > lFiles; + if (DWORD nCount; SUCCEEDED(iItems->GetCount(&nCount))) + { + for (DWORD i = 0; i < nCount; ++i) + { + if (sal::systools::COMReference<IShellItem> iItem; + SUCCEEDED(iItems->GetItemAt(i, &iItem))) + { + if (const OUString sURL = lcl_getURLFromShellItem(iItem.get()); !sURL.isEmpty()) + lFiles.push_back(sURL); + } + } + } + + rRequest.setArgument(PROP_SELECTED_FILES, comphelper::containerToSequence(lFiles)); +} + + +void VistaFilePickerImpl::impl_sta_ShowDialogModal(Request& rRequest) +{ + impl_sta_setFiltersOnDialog(); + + TFileDialog iDialog = impl_getBaseDialogInterface(); + if (!iDialog.is()) + return; + + // it's important to know if we are showing the dialog. + // Some dialog interface methods can't be called then or some + // tasks must be done differently .-) (e.g. see impl_sta_getSelectedFiles()) + m_bInExecute = true; + + m_bWasExecuted = true; + + // we set the directory only if we have a save dialog and a filename + // for the other cases, the file dialog remembers its last location + // according to its client guid. + if( m_sDirectory.getLength()) + { + sal::systools::COMReference<IShellItem> pFolder; + if ( createFolderItem(m_sDirectory, pFolder) ) + { + if (m_sFilename.getLength()) + { + OUString aFileURL(m_sDirectory); + sal_Int32 nIndex = aFileURL.lastIndexOf('/'); + if (nIndex != aFileURL.getLength()-1) + aFileURL += "/"; + aFileURL += m_sFilename; + + TFileDialogCustomize iCustom = impl_getCustomizeInterface(); + if (!iCustom.is()) + return; + + BOOL bValue = FALSE; + HRESULT hResult = iCustom->GetCheckButtonState( css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_AUTOEXTENSION, &bValue); + if ( bValue ) + { + UINT nFileType; + hResult = iDialog->GetFileTypeIndex(&nFileType); + if ( SUCCEEDED(hResult) && nFileType > 0 ) + { + // COM dialog base on 1 ... filter container on 0 .-) + ::size_t nRealIndex = nFileType-1; + OUString sFilter; + if (m_lFilters.getFilterByIndex(nRealIndex, sFilter)) + { + const sal_Int32 idx = sFilter.indexOf('.'); + if (idx >= 0) + aFileURL += sFilter.subView(idx); + } + } + } + + // Check existence of file. Set folder only for this special case + OUString aSystemPath; + osl_getSystemPathFromFileURL( aFileURL.pData, &aSystemPath.pData ); + + WIN32_FIND_DATAW aFindFileData; + HANDLE hFind = FindFirstFileW( o3tl::toW(aSystemPath.getStr()), &aFindFileData ); + if (hFind != INVALID_HANDLE_VALUE) + iDialog->SetFolder(pFolder.get()); + else + hResult = iDialog->AddPlace(pFolder.get(), FDAP_TOP); + + FindClose( hFind ); + } + else + iDialog->AddPlace(pFolder.get(), FDAP_TOP); + } + } + + HRESULT hResult = E_FAIL; + try + { + // tdf#146007: Make sure we don't hold solar mutex: COM may need to forward + // the execution to the main thread, and holding solar mutex could deadlock + SolarMutexGuard g; // First acquire, to avoid releaser failure + SolarMutexReleaser r; + // show dialog and wait for user decision + hResult = iDialog->Show(m_hParentWindow ? m_hParentWindow + : choose_parent_window()); // parent window needed + } + catch(...) + {} + + m_bInExecute = false; + + if (m_iEventHandler.is()) + { + auto* pHandlerImpl = static_cast<VistaFilePickerEventHandler*>(m_iEventHandler.get()); + pHandlerImpl->stopListening(); + } + + if ( FAILED(hResult) ) + return; + + impl_sta_getSelectedFiles(rRequest); + rRequest.setArgument(PROP_DIALOG_SHOW_RESULT, true); +} + + +TFileDialog VistaFilePickerImpl::impl_getBaseDialogInterface() +{ + TFileDialog iDialog; + + if (m_pDialog != nullptr) + iDialog = m_pDialog->getComPtr(); + + return iDialog; +} + + +TFileDialogCustomize VistaFilePickerImpl::impl_getCustomizeInterface() +{ + if (m_pDialog != nullptr) + return { m_pDialog->getComPtr(), sal::systools::COM_QUERY_THROW }; + + return {}; +} + + +static void lcl_removeControlItemsWorkaround(const TFileDialogCustomize& iCustom , + ::sal_Int16 nControlId) +{ + (void)iCustom->SetSelectedControlItem(nControlId, 1000); // Don't care if this fails (useless?) + DWORD i = 0; + HRESULT hResult = S_OK; + while ( SUCCEEDED(hResult) ) + hResult = iCustom->RemoveControlItem(nControlId, i++); +} + + +void VistaFilePickerImpl::impl_sta_SetControlValue(Request& rRequest) +{ + ::sal_Int16 nId = rRequest.getArgumentOrDefault(PROP_CONTROL_ID , INVALID_CONTROL_ID ); + ::sal_Int16 nAction = rRequest.getArgumentOrDefault(PROP_CONTROL_ACTION, INVALID_CONTROL_ACTION); + css::uno::Any aValue = rRequest.getValue(PROP_CONTROL_VALUE); + + // don't check for right values here ... + // most parameters are optional ! + + TFileDialogCustomize iCustom = impl_getCustomizeInterface(); + if ( ! iCustom.is()) + return; + + switch (nId) + { + case css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_AUTOEXTENSION : + case css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_PASSWORD : + case css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_READONLY : + case css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_FILTEROPTIONS : + case css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_LINK : + //case css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_PREVIEW : // can be ignored ... preview is supported native now ! + case css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_SELECTION : + { + bool bValue = false; + aValue >>= bValue; + iCustom->SetCheckButtonState(nId, bValue); + } + break; + + case css::ui::dialogs::ExtendedFilePickerElementIds::LISTBOX_VERSION : + case css::ui::dialogs::ExtendedFilePickerElementIds::LISTBOX_TEMPLATE : + case css::ui::dialogs::ExtendedFilePickerElementIds::LISTBOX_IMAGE_TEMPLATE : + case css::ui::dialogs::ExtendedFilePickerElementIds::LISTBOX_IMAGE_ANCHOR : + { + HRESULT hResult; + switch (nAction) + { + case css::ui::dialogs::ControlActions::DELETE_ITEMS : + { + hResult = iCustom->RemoveAllControlItems(nId); + if ( FAILED(hResult) ) + lcl_removeControlItemsWorkaround(iCustom, nId); + } + break; + + case css::ui::dialogs::ControlActions::ADD_ITEMS : + { + aValue >>= m_lItems; + for (::sal_Int32 i=0; i<m_lItems.getLength(); ++i) + { + const OUString& sItem = m_lItems[i]; + hResult = iCustom->AddControlItem(nId, i, o3tl::toW(sItem.getStr())); + } + } + break; + + case css::ui::dialogs::ControlActions::SET_SELECT_ITEM : + { + ::sal_Int32 nItem = 0; + aValue >>= nItem; + hResult = iCustom->SetSelectedControlItem(nId, nItem); + } + break; + } + } + break; + + case css::ui::dialogs::ExtendedFilePickerElementIds::PUSHBUTTON_PLAY : + { + } + break; + } +} + + +void VistaFilePickerImpl::impl_sta_GetControlValue(Request& rRequest) +{ + ::sal_Int16 nId = rRequest.getArgumentOrDefault(PROP_CONTROL_ID , INVALID_CONTROL_ID ); + + // don't check for right values here ... + // most parameters are optional ! + + TFileDialogCustomize iCustom = impl_getCustomizeInterface(); + if ( ! iCustom.is()) + return; + + css::uno::Any aValue; + if( m_bWasExecuted ) + switch (nId) + { + case css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_PASSWORD : + case css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_GPGENCRYPTION : + case css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_READONLY : + case css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_FILTEROPTIONS : + case css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_LINK : + //case css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_PREVIEW : // can be ignored ... preview is supported native now ! + case css::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_SELECTION : + { + BOOL bValue = FALSE; + HRESULT hResult = iCustom->GetCheckButtonState(nId, &bValue); + if ( SUCCEEDED(hResult) ) + aValue <<= bool(bValue); + } + break; + case css::ui::dialogs::ExtendedFilePickerElementIds::LISTBOX_VERSION: + case css::ui::dialogs::ExtendedFilePickerElementIds::LISTBOX_TEMPLATE: + case css::ui::dialogs::ExtendedFilePickerElementIds::LISTBOX_IMAGE_TEMPLATE: + case css::ui::dialogs::ExtendedFilePickerElementIds::LISTBOX_IMAGE_ANCHOR: + { + DWORD bValue = 0; + HRESULT hResult = iCustom->GetSelectedControlItem(nId, &bValue); + if ( SUCCEEDED(hResult) ) + { + const OUString& sItem = m_lItems[bValue]; + aValue <<= OUString(sItem.getStr()); + } + } + break; + } + + if (aValue.hasValue()) + rRequest.setArgument(PROP_CONTROL_VALUE, aValue); +} + + +void VistaFilePickerImpl::impl_sta_SetControlLabel(Request& rRequest) +{ + ::sal_Int16 nId = rRequest.getArgumentOrDefault(PROP_CONTROL_ID , INVALID_CONTROL_ID ); + OUString sLabel = rRequest.getArgumentOrDefault(PROP_CONTROL_LABEL, OUString() ); + + // don't check for right values here ... + // most parameters are optional ! + + TFileDialogCustomize iCustom = impl_getCustomizeInterface(); + if ( ! iCustom.is()) + return; + iCustom->SetControlLabel (nId, o3tl::toW(sLabel.getStr())); +} + + +void VistaFilePickerImpl::impl_sta_GetControlLabel(Request& /*rRequest*/) +{ +} + + +void VistaFilePickerImpl::impl_sta_EnableControl(Request& rRequest) +{ + ::sal_Int16 nId = rRequest.getArgumentOrDefault(PROP_CONTROL_ID , INVALID_CONTROL_ID ); + bool bEnabled = rRequest.getArgumentOrDefault(PROP_CONTROL_ENABLE, true); + + // don't check for right values here ... + // most parameters are optional ! + + TFileDialogCustomize iCustom = impl_getCustomizeInterface(); + if ( ! iCustom.is()) + return; + + CDCONTROLSTATEF eState = CDCS_VISIBLE; + if (bEnabled) + eState |= CDCS_ENABLED; + else + eState |= CDCS_INACTIVE; + + iCustom->SetControlState(nId, eState); +} + +void VistaFilePickerImpl::impl_SetDefaultExtension( const OUString& currentFilter ) +{ + TFileDialog iDialog = impl_getBaseDialogInterface(); + if (!iDialog.is()) + return; + + if (currentFilter.getLength()) + { + OUString FilterExt; + m_lFilters.getFilterByName(currentFilter, FilterExt); + + sal_Int32 posOfPoint = FilterExt.indexOf(L'.'); + const sal_Unicode* pFirstExtStart = FilterExt.getStr() + posOfPoint + 1; + + sal_Int32 posOfSemiColon = FilterExt.indexOf(L';') - 1; + if (posOfSemiColon < 0) + posOfSemiColon = FilterExt.getLength() - 1; + + FilterExt = OUString(pFirstExtStart, posOfSemiColon - posOfPoint); + iDialog->SetDefaultExtension ( o3tl::toW(FilterExt.getStr()) ); + } +} + +void VistaFilePickerImpl::onAutoExtensionChanged (bool bChecked) +{ + const OUString sFilter = m_lFilters.getCurrentFilter (); + OUString sExt ; + if (!m_lFilters.getFilterByName(sFilter, sExt)) + return; + + TFileDialog iDialog = impl_getBaseDialogInterface(); + if (!iDialog.is()) + return; + + PCWSTR pExt = nullptr; + if ( bChecked ) + { + pExt = o3tl::toW(sExt.getStr()); + pExt = wcsrchr( pExt, '.' ); + if ( pExt ) + pExt++; + } + iDialog->SetDefaultExtension( pExt ); +} + +bool VistaFilePickerImpl::onFileTypeChanged( UINT /*nTypeIndex*/ ) +{ + return true; +} + +void VistaFilePickerImpl::onDirectoryChanged() +{ + m_sDirectory = GetDirectory(); +} + +} // namespace vista +} // namespace win32 +} // namespace fpicker + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |