1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3  * This file is part of the LibreOffice project.
4  *
5  * This Source Code Form is subject to the terms of the Mozilla Public
6  * License, v. 2.0. If a copy of the MPL was not distributed with this
7  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8  *
9  * This file incorporates work covered by the following license notice:
10  *
11  *   Licensed to the Apache Software Foundation (ASF) under one or more
12  *   contributor license agreements. See the NOTICE file distributed
13  *   with this work for additional information regarding copyright
14  *   ownership. The ASF licenses this file to you under the Apache
15  *   License, Version 2.0 (the "License"); you may not use this file
16  *   except in compliance with the License. You may obtain a copy of
17  *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
18  */
19 
20 #include <Qt5FilePicker.hxx>
21 #include <Qt5FilePicker.moc>
22 
23 #include <Qt5Frame.hxx>
24 #include <Qt5Tools.hxx>
25 #include <Qt5Widget.hxx>
26 #include <Qt5Instance.hxx>
27 
28 #include <com/sun/star/awt/SystemDependentXWindow.hpp>
29 #include <com/sun/star/awt/XSystemDependentWindowPeer.hpp>
30 #include <com/sun/star/awt/XWindow.hpp>
31 #include <com/sun/star/frame/Desktop.hpp>
32 #include <com/sun/star/frame/TerminationVetoException.hpp>
33 #include <com/sun/star/frame/XDesktop.hpp>
34 #include <com/sun/star/lang/DisposedException.hpp>
35 #include <com/sun/star/lang/IllegalArgumentException.hpp>
36 #include <com/sun/star/lang/SystemDependent.hpp>
37 #include <com/sun/star/lang/XMultiServiceFactory.hpp>
38 #include <com/sun/star/ui/dialogs/CommonFilePickerElementIds.hpp>
39 #include <com/sun/star/ui/dialogs/ControlActions.hpp>
40 #include <com/sun/star/ui/dialogs/ExecutableDialogResults.hpp>
41 #include <com/sun/star/ui/dialogs/ExtendedFilePickerElementIds.hpp>
42 #include <com/sun/star/ui/dialogs/TemplateDescription.hpp>
43 #include <com/sun/star/uri/ExternalUriReferenceTranslator.hpp>
44 #include <cppuhelper/interfacecontainer.h>
45 #include <cppuhelper/supportsservice.hxx>
46 #include <rtl/process.h>
47 #include <sal/log.hxx>
48 
49 #include <QtCore/QDebug>
50 #include <QtCore/QRegularExpression>
51 #include <QtCore/QThread>
52 #include <QtCore/QUrl>
53 #include <QtGui/QClipboard>
54 #include <QtGui/QWindow>
55 #include <QtWidgets/QApplication>
56 #include <QtWidgets/QCheckBox>
57 #include <QtWidgets/QComboBox>
58 #include <QtWidgets/QGridLayout>
59 #include <QtWidgets/QHBoxLayout>
60 #include <QtWidgets/QLabel>
61 #include <QtWidgets/QMessageBox>
62 #include <QtWidgets/QPushButton>
63 #include <QtWidgets/QWidget>
64 
65 #include <unx/geninst.h>
66 #include <strings.hrc>
67 
68 using namespace ::com::sun::star;
69 using namespace ::com::sun::star::ui::dialogs;
70 using namespace ::com::sun::star::ui::dialogs::TemplateDescription;
71 using namespace ::com::sun::star::ui::dialogs::ExtendedFilePickerElementIds;
72 using namespace ::com::sun::star::ui::dialogs::CommonFilePickerElementIds;
73 using namespace ::com::sun::star::lang;
74 using namespace ::com::sun::star::beans;
75 using namespace ::com::sun::star::uno;
76 
77 namespace
78 {
FilePicker_getSupportedServiceNames()79 uno::Sequence<OUString> FilePicker_getSupportedServiceNames()
80 {
81     return { "com.sun.star.ui.dialogs.FilePicker", "com.sun.star.ui.dialogs.SystemFilePicker",
82              "com.sun.star.ui.dialogs.Qt5FilePicker" };
83 }
84 }
85 
Qt5FilePicker(css::uno::Reference<css::uno::XComponentContext> const & context,QFileDialog::FileMode eMode,bool bUseNative)86 Qt5FilePicker::Qt5FilePicker(css::uno::Reference<css::uno::XComponentContext> const& context,
87                              QFileDialog::FileMode eMode, bool bUseNative)
88     : Qt5FilePicker_Base(m_aHelperMutex)
89     , m_context(context)
90     , m_bIsFolderPicker(eMode == QFileDialog::Directory)
91     , m_pParentWidget(nullptr)
92     , m_pFileDialog(new QFileDialog(nullptr, {}, QDir::homePath()))
93     , m_pExtraControls(new QWidget())
94 {
95     m_pFileDialog->setOption(QFileDialog::DontUseNativeDialog, !bUseNative);
96 
97     m_pFileDialog->setFileMode(eMode);
98     m_pFileDialog->setWindowModality(Qt::ApplicationModal);
99 
100     if (m_bIsFolderPicker)
101     {
102         m_pFileDialog->setOption(QFileDialog::ShowDirsOnly, true);
103         m_pFileDialog->setWindowTitle(toQString(VclResId(STR_FPICKER_FOLDER_DEFAULT_TITLE)));
104     }
105 
106     m_pLayout = dynamic_cast<QGridLayout*>(m_pFileDialog->layout());
107 
108     setMultiSelectionMode(false);
109 
110     // XFilePickerListener notifications
111     connect(m_pFileDialog.get(), SIGNAL(filterSelected(const QString&)), this,
112             SLOT(filterSelected(const QString&)));
113     connect(m_pFileDialog.get(), SIGNAL(currentChanged(const QString&)), this,
114             SLOT(currentChanged(const QString&)));
115 
116     // update automatic file extension when filter is changed
117     connect(m_pFileDialog.get(), SIGNAL(filterSelected(const QString&)), this,
118             SLOT(updateAutomaticFileExtension()));
119 }
120 
~Qt5FilePicker()121 Qt5FilePicker::~Qt5FilePicker()
122 {
123     SolarMutexGuard g;
124     auto* pSalInst(static_cast<Qt5Instance*>(GetSalData()->m_pInstance));
125     assert(pSalInst);
126     pSalInst->RunInMainThread([this]() {
127         // must delete it in main thread, otherwise
128         // QSocketNotifier::setEnabled() will crash us
129         m_pFileDialog.reset();
130     });
131 }
132 
133 void SAL_CALL
addFilePickerListener(const uno::Reference<XFilePickerListener> & xListener)134 Qt5FilePicker::addFilePickerListener(const uno::Reference<XFilePickerListener>& xListener)
135 {
136     SolarMutexGuard aGuard;
137     m_xListener = xListener;
138 }
139 
removeFilePickerListener(const uno::Reference<XFilePickerListener> &)140 void SAL_CALL Qt5FilePicker::removeFilePickerListener(const uno::Reference<XFilePickerListener>&)
141 {
142     SolarMutexGuard aGuard;
143     m_xListener.clear();
144 }
145 
setTitle(const OUString & title)146 void SAL_CALL Qt5FilePicker::setTitle(const OUString& title)
147 {
148     SolarMutexGuard g;
149     auto* pSalInst(static_cast<Qt5Instance*>(GetSalData()->m_pInstance));
150     assert(pSalInst);
151     pSalInst->RunInMainThread(
152         [this, &title]() { m_pFileDialog->setWindowTitle(toQString(title)); });
153 }
154 
execute()155 sal_Int16 SAL_CALL Qt5FilePicker::execute()
156 {
157     SolarMutexGuard g;
158     auto* pSalInst(static_cast<Qt5Instance*>(GetSalData()->m_pInstance));
159     assert(pSalInst);
160     if (!pSalInst->IsMainThread())
161     {
162         sal_uInt16 ret;
163         pSalInst->RunInMainThread([&ret, this]() { ret = execute(); });
164         return ret;
165     }
166 
167     QWidget* pTransientParent = m_pParentWidget;
168     if (!pTransientParent)
169     {
170         vcl::Window* pWindow = ::Application::GetActiveTopWindow();
171         if (pWindow)
172         {
173             Qt5Frame* pFrame = dynamic_cast<Qt5Frame*>(pWindow->ImplGetFrame());
174             assert(pFrame);
175             if (pFrame)
176                 pTransientParent = pFrame->asChild();
177         }
178     }
179 
180     if (!m_aNamedFilterList.isEmpty())
181         m_pFileDialog->setNameFilters(m_aNamedFilterList);
182     if (!m_aCurrentFilter.isEmpty())
183         m_pFileDialog->selectNameFilter(m_aCurrentFilter);
184 
185     updateAutomaticFileExtension();
186 
187     uno::Reference<css::frame::XDesktop> xDesktop(css::frame::Desktop::create(m_context),
188                                                   UNO_QUERY_THROW);
189 
190     // will hide the window, so do before show
191     m_pFileDialog->setParent(pTransientParent, m_pFileDialog->windowFlags());
192     m_pFileDialog->show();
193     xDesktop->addTerminateListener(this);
194     int result = m_pFileDialog->exec();
195     xDesktop->removeTerminateListener(this);
196     m_pFileDialog->setParent(nullptr, m_pFileDialog->windowFlags());
197 
198     if (QFileDialog::Rejected == result)
199         return ExecutableDialogResults::CANCEL;
200     return ExecutableDialogResults::OK;
201 }
202 
setMultiSelectionMode(sal_Bool multiSelect)203 void SAL_CALL Qt5FilePicker::setMultiSelectionMode(sal_Bool multiSelect)
204 {
205     SolarMutexGuard g;
206     auto* pSalInst(static_cast<Qt5Instance*>(GetSalData()->m_pInstance));
207     assert(pSalInst);
208     pSalInst->RunInMainThread([this, multiSelect]() {
209         if (m_bIsFolderPicker || m_pFileDialog->acceptMode() == QFileDialog::AcceptSave)
210             return;
211 
212         m_pFileDialog->setFileMode(multiSelect ? QFileDialog::ExistingFiles
213                                                : QFileDialog::ExistingFile);
214     });
215 }
216 
setDefaultName(const OUString & name)217 void SAL_CALL Qt5FilePicker::setDefaultName(const OUString& name)
218 {
219     SolarMutexGuard g;
220     auto* pSalInst(static_cast<Qt5Instance*>(GetSalData()->m_pInstance));
221     assert(pSalInst);
222     pSalInst->RunInMainThread([this, &name]() { m_pFileDialog->selectFile(toQString(name)); });
223 }
224 
setDisplayDirectory(const OUString & dir)225 void SAL_CALL Qt5FilePicker::setDisplayDirectory(const OUString& dir)
226 {
227     SolarMutexGuard g;
228     auto* pSalInst(static_cast<Qt5Instance*>(GetSalData()->m_pInstance));
229     assert(pSalInst);
230     pSalInst->RunInMainThread([this, &dir]() {
231         QString qDir(toQString(dir));
232         m_pFileDialog->setDirectoryUrl(QUrl(qDir));
233     });
234 }
235 
getDisplayDirectory()236 OUString SAL_CALL Qt5FilePicker::getDisplayDirectory()
237 {
238     SolarMutexGuard g;
239     OUString ret;
240     auto* pSalInst(static_cast<Qt5Instance*>(GetSalData()->m_pInstance));
241     assert(pSalInst);
242     pSalInst->RunInMainThread(
243         [&ret, this]() { ret = toOUString(m_pFileDialog->directoryUrl().toString()); });
244     return ret;
245 }
246 
getFiles()247 uno::Sequence<OUString> SAL_CALL Qt5FilePicker::getFiles()
248 {
249     uno::Sequence<OUString> seq = getSelectedFiles();
250     if (seq.getLength() > 1)
251         seq.realloc(1);
252     return seq;
253 }
254 
getSelectedFiles()255 uno::Sequence<OUString> SAL_CALL Qt5FilePicker::getSelectedFiles()
256 {
257     SolarMutexGuard g;
258     QList<QUrl> urls;
259     auto* pSalInst(static_cast<Qt5Instance*>(GetSalData()->m_pInstance));
260     assert(pSalInst);
261     pSalInst->RunInMainThread([&urls, this]() { urls = m_pFileDialog->selectedUrls(); });
262 
263     uno::Sequence<OUString> seq(urls.size());
264 
265     auto const trans = css::uri::ExternalUriReferenceTranslator::create(m_context);
266     size_t i = 0;
267     for (const QUrl& aURL : urls)
268     {
269         // Unlike LO, QFileDialog (<https://doc.qt.io/qt-5/qfiledialog.html>) apparently always
270         // treats file-system pathnames as UTF-8--encoded, regardless of LANG/LC_CTYPE locale
271         // setting.  And pathnames containing byte sequences that are not valid UTF-8 are apparently
272         // filtered out and not even displayed by QFileDialog, so aURL will always have a "payload"
273         // that matches the pathname's byte sequence.  So the pathname's byte sequence (which
274         // happens to also be aURL's payload) in the LANG/LC_CTYPE encoding needs to be converted
275         // into LO's internal UTF-8 file URL encoding via
276         // XExternalUriReferenceTranslator::translateToInternal (which looks somewhat paradoxical as
277         // aURL.toEncoded() nominally already has a UTF-8 payload):
278         auto const extUrl = toOUString(aURL.toEncoded());
279         auto intUrl = trans->translateToInternal(extUrl);
280         if (intUrl.isEmpty())
281         {
282             // If translation failed, fall back to original URL:
283             SAL_WARN("vcl.qt5", "cannot convert <" << extUrl << "> from locale encoding to UTF-8");
284             intUrl = extUrl;
285         }
286         seq[i++] = intUrl;
287     }
288 
289     return seq;
290 }
291 
appendFilter(const OUString & title,const OUString & filter)292 void SAL_CALL Qt5FilePicker::appendFilter(const OUString& title, const OUString& filter)
293 {
294     SolarMutexGuard g;
295     auto* pSalInst(static_cast<Qt5Instance*>(GetSalData()->m_pInstance));
296     assert(pSalInst);
297     if (!pSalInst->IsMainThread())
298     {
299         pSalInst->RunInMainThread([this, &title, &filter]() { appendFilter(title, filter); });
300         return;
301     }
302 
303     // '/' need to be escaped else they are assumed to be mime types
304     QString sTitle = toQString(title).replace("/", "\\/");
305 
306     QString sFilterName = sTitle;
307     // the Qt5 non-native file picker adds the extensions to the filter title, so strip them
308     if (m_pFileDialog->testOption(QFileDialog::DontUseNativeDialog))
309     {
310         int pos = sFilterName.indexOf(" (");
311         if (pos >= 0)
312             sFilterName.truncate(pos);
313     }
314 
315     QString sGlobFilter = toQString(filter);
316 
317     // LibreOffice gives us filters separated by ';' qt dialogs just want space separated
318     sGlobFilter.replace(";", " ");
319 
320     // make sure "*.*" is not used as "all files"
321     sGlobFilter.replace("*.*", "*");
322 
323     m_aNamedFilterList << QStringLiteral("%1 (%2)").arg(sFilterName, sGlobFilter);
324     m_aTitleToFilterMap[sTitle] = m_aNamedFilterList.constLast();
325     m_aNamedFilterToExtensionMap[m_aNamedFilterList.constLast()] = sGlobFilter;
326 }
327 
setCurrentFilter(const OUString & title)328 void SAL_CALL Qt5FilePicker::setCurrentFilter(const OUString& title)
329 {
330     SolarMutexGuard g;
331     auto* pSalInst(static_cast<Qt5Instance*>(GetSalData()->m_pInstance));
332     assert(pSalInst);
333     pSalInst->RunInMainThread([this, &title]() {
334         m_aCurrentFilter = m_aTitleToFilterMap.value(toQString(title).replace("/", "\\/"));
335     });
336 }
337 
getCurrentFilter()338 OUString SAL_CALL Qt5FilePicker::getCurrentFilter()
339 {
340     SolarMutexGuard g;
341     QString filter;
342     auto* pSalInst(static_cast<Qt5Instance*>(GetSalData()->m_pInstance));
343     assert(pSalInst);
344     pSalInst->RunInMainThread([&filter, this]() {
345         filter = m_aTitleToFilterMap.key(m_pFileDialog->selectedNameFilter());
346     });
347 
348     if (filter.isEmpty())
349         filter = "ODF Text Document (.odt)";
350     return toOUString(filter);
351 }
352 
appendFilterGroup(const OUString & rGroupTitle,const uno::Sequence<beans::StringPair> & filters)353 void SAL_CALL Qt5FilePicker::appendFilterGroup(const OUString& rGroupTitle,
354                                                const uno::Sequence<beans::StringPair>& filters)
355 {
356     SolarMutexGuard g;
357     auto* pSalInst(static_cast<Qt5Instance*>(GetSalData()->m_pInstance));
358     assert(pSalInst);
359     if (!pSalInst->IsMainThread())
360     {
361         pSalInst->RunInMainThread(
362             [this, &rGroupTitle, &filters]() { appendFilterGroup(rGroupTitle, filters); });
363         return;
364     }
365 
366     const sal_uInt16 length = filters.getLength();
367     for (sal_uInt16 i = 0; i < length; ++i)
368     {
369         beans::StringPair aPair = filters[i];
370         appendFilter(aPair.First, aPair.Second);
371     }
372 }
373 
handleGetListValue(const QComboBox * pWidget,sal_Int16 nControlAction)374 uno::Any Qt5FilePicker::handleGetListValue(const QComboBox* pWidget, sal_Int16 nControlAction)
375 {
376     uno::Any aAny;
377     switch (nControlAction)
378     {
379         case ControlActions::GET_ITEMS:
380         {
381             Sequence<OUString> aItemList(pWidget->count());
382             for (sal_Int32 i = 0; i < pWidget->count(); ++i)
383                 aItemList[i] = toOUString(pWidget->itemText(i));
384             aAny <<= aItemList;
385             break;
386         }
387         case ControlActions::GET_SELECTED_ITEM:
388         {
389             if (!pWidget->currentText().isEmpty())
390                 aAny <<= toOUString(pWidget->currentText());
391             break;
392         }
393         case ControlActions::GET_SELECTED_ITEM_INDEX:
394         {
395             if (pWidget->currentIndex() >= 0)
396                 aAny <<= static_cast<sal_Int32>(pWidget->currentIndex());
397             break;
398         }
399         default:
400             SAL_WARN("vcl.qt5",
401                      "undocumented/unimplemented ControlAction for a list " << nControlAction);
402             break;
403     }
404     return aAny;
405 }
406 
handleSetListValue(QComboBox * pWidget,sal_Int16 nControlAction,const uno::Any & rValue)407 void Qt5FilePicker::handleSetListValue(QComboBox* pWidget, sal_Int16 nControlAction,
408                                        const uno::Any& rValue)
409 {
410     switch (nControlAction)
411     {
412         case ControlActions::ADD_ITEM:
413         {
414             OUString sItem;
415             rValue >>= sItem;
416             pWidget->addItem(toQString(sItem));
417             break;
418         }
419         case ControlActions::ADD_ITEMS:
420         {
421             Sequence<OUString> aStringList;
422             rValue >>= aStringList;
423             for (auto const& sItem : std::as_const(aStringList))
424                 pWidget->addItem(toQString(sItem));
425             break;
426         }
427         case ControlActions::DELETE_ITEM:
428         {
429             sal_Int32 nPos = 0;
430             rValue >>= nPos;
431             pWidget->removeItem(nPos);
432             break;
433         }
434         case ControlActions::DELETE_ITEMS:
435         {
436             pWidget->clear();
437             break;
438         }
439         case ControlActions::SET_SELECT_ITEM:
440         {
441             sal_Int32 nPos = 0;
442             rValue >>= nPos;
443             pWidget->setCurrentIndex(nPos);
444             break;
445         }
446         default:
447             SAL_WARN("vcl.qt5",
448                      "undocumented/unimplemented ControlAction for a list " << nControlAction);
449             break;
450     }
451 
452     pWidget->setEnabled(pWidget->count() > 0);
453 }
454 
setValue(sal_Int16 controlId,sal_Int16 nControlAction,const uno::Any & value)455 void SAL_CALL Qt5FilePicker::setValue(sal_Int16 controlId, sal_Int16 nControlAction,
456                                       const uno::Any& value)
457 {
458     SolarMutexGuard g;
459     auto* pSalInst(static_cast<Qt5Instance*>(GetSalData()->m_pInstance));
460     assert(pSalInst);
461     if (!pSalInst->IsMainThread())
462     {
463         pSalInst->RunInMainThread([this, controlId, nControlAction, &value]() {
464             setValue(controlId, nControlAction, value);
465         });
466         return;
467     }
468 
469     if (m_aCustomWidgetsMap.contains(controlId))
470     {
471         QWidget* widget = m_aCustomWidgetsMap.value(controlId);
472         QCheckBox* cb = dynamic_cast<QCheckBox*>(widget);
473         if (cb)
474             cb->setChecked(value.get<bool>());
475         else
476         {
477             QComboBox* combo = dynamic_cast<QComboBox*>(widget);
478             if (combo)
479                 handleSetListValue(combo, nControlAction, value);
480         }
481     }
482     else
483         SAL_WARN("vcl.qt5", "set value on unknown control " << controlId);
484 }
485 
getValue(sal_Int16 controlId,sal_Int16 nControlAction)486 uno::Any SAL_CALL Qt5FilePicker::getValue(sal_Int16 controlId, sal_Int16 nControlAction)
487 {
488     SolarMutexGuard g;
489     auto* pSalInst(static_cast<Qt5Instance*>(GetSalData()->m_pInstance));
490     assert(pSalInst);
491     if (!pSalInst->IsMainThread())
492     {
493         uno::Any ret;
494         pSalInst->RunInMainThread([&ret, this, controlId, nControlAction]() {
495             ret = getValue(controlId, nControlAction);
496         });
497         return ret;
498     }
499 
500     uno::Any res(false);
501     if (m_aCustomWidgetsMap.contains(controlId))
502     {
503         QWidget* widget = m_aCustomWidgetsMap.value(controlId);
504         QCheckBox* cb = dynamic_cast<QCheckBox*>(widget);
505         if (cb)
506             res <<= cb->isChecked();
507         else
508         {
509             QComboBox* combo = dynamic_cast<QComboBox*>(widget);
510             if (combo)
511                 res = handleGetListValue(combo, nControlAction);
512         }
513     }
514     else
515         SAL_WARN("vcl.qt5", "get value on unknown control " << controlId);
516 
517     return res;
518 }
519 
enableControl(sal_Int16 controlId,sal_Bool enable)520 void SAL_CALL Qt5FilePicker::enableControl(sal_Int16 controlId, sal_Bool enable)
521 {
522     SolarMutexGuard g;
523     auto* pSalInst(static_cast<Qt5Instance*>(GetSalData()->m_pInstance));
524     assert(pSalInst);
525     pSalInst->RunInMainThread([this, controlId, enable]() {
526         if (m_aCustomWidgetsMap.contains(controlId))
527             m_aCustomWidgetsMap.value(controlId)->setEnabled(enable);
528         else
529             SAL_WARN("vcl.qt5", "enable unknown control " << controlId);
530     });
531 }
532 
setLabel(sal_Int16 controlId,const OUString & label)533 void SAL_CALL Qt5FilePicker::setLabel(sal_Int16 controlId, const OUString& label)
534 {
535     SolarMutexGuard g;
536     auto* pSalInst(static_cast<Qt5Instance*>(GetSalData()->m_pInstance));
537     assert(pSalInst);
538     if (!pSalInst->IsMainThread())
539     {
540         pSalInst->RunInMainThread([this, controlId, label]() { setLabel(controlId, label); });
541         return;
542     }
543 
544     if (m_aCustomWidgetsMap.contains(controlId))
545     {
546         QCheckBox* cb = dynamic_cast<QCheckBox*>(m_aCustomWidgetsMap.value(controlId));
547         if (cb)
548             cb->setText(toQString(label));
549     }
550     else
551         SAL_WARN("vcl.qt5", "set label on unknown control " << controlId);
552 }
553 
getLabel(sal_Int16 controlId)554 OUString SAL_CALL Qt5FilePicker::getLabel(sal_Int16 controlId)
555 {
556     SolarMutexGuard g;
557     auto* pSalInst(static_cast<Qt5Instance*>(GetSalData()->m_pInstance));
558     assert(pSalInst);
559     if (!pSalInst->IsMainThread())
560     {
561         OUString ret;
562         pSalInst->RunInMainThread([&ret, this, controlId]() { ret = getLabel(controlId); });
563         return ret;
564     }
565 
566     QString label;
567     if (m_aCustomWidgetsMap.contains(controlId))
568     {
569         QCheckBox* cb = dynamic_cast<QCheckBox*>(m_aCustomWidgetsMap.value(controlId));
570         if (cb)
571             label = cb->text();
572     }
573     else
574         SAL_WARN("vcl.qt5", "get label on unknown control " << controlId);
575 
576     return toOUString(label);
577 }
578 
getResString(const char * pResId)579 QString Qt5FilePicker::getResString(const char* pResId)
580 {
581     QString aResString;
582 
583     if (pResId == nullptr)
584         return aResString;
585 
586     aResString = toQString(VclResId(pResId));
587 
588     return aResString.replace('~', '&');
589 }
590 
addCustomControl(sal_Int16 controlId)591 void Qt5FilePicker::addCustomControl(sal_Int16 controlId)
592 {
593     QWidget* widget = nullptr;
594     QLabel* label = nullptr;
595     const char* resId = nullptr;
596     QCheckBox* pCheckbox = nullptr;
597 
598     switch (controlId)
599     {
600         case CHECKBOX_AUTOEXTENSION:
601             resId = STR_FPICKER_AUTO_EXTENSION;
602             break;
603         case CHECKBOX_PASSWORD:
604             resId = STR_FPICKER_PASSWORD;
605             break;
606         case CHECKBOX_FILTEROPTIONS:
607             resId = STR_FPICKER_FILTER_OPTIONS;
608             break;
609         case CHECKBOX_READONLY:
610             resId = STR_FPICKER_READONLY;
611             break;
612         case CHECKBOX_LINK:
613             resId = STR_FPICKER_INSERT_AS_LINK;
614             break;
615         case CHECKBOX_PREVIEW:
616             resId = STR_FPICKER_SHOW_PREVIEW;
617             break;
618         case CHECKBOX_SELECTION:
619             resId = STR_FPICKER_SELECTION;
620             break;
621         case CHECKBOX_GPGENCRYPTION:
622             resId = STR_FPICKER_GPGENCRYPT;
623             break;
624         case PUSHBUTTON_PLAY:
625             resId = STR_FPICKER_PLAY;
626             break;
627         case LISTBOX_VERSION:
628             resId = STR_FPICKER_VERSION;
629             break;
630         case LISTBOX_TEMPLATE:
631             resId = STR_FPICKER_TEMPLATES;
632             break;
633         case LISTBOX_IMAGE_TEMPLATE:
634             resId = STR_FPICKER_IMAGE_TEMPLATE;
635             break;
636         case LISTBOX_IMAGE_ANCHOR:
637             resId = STR_FPICKER_IMAGE_ANCHOR;
638             break;
639         case LISTBOX_VERSION_LABEL:
640         case LISTBOX_TEMPLATE_LABEL:
641         case LISTBOX_IMAGE_TEMPLATE_LABEL:
642         case LISTBOX_IMAGE_ANCHOR_LABEL:
643         case LISTBOX_FILTER_SELECTOR:
644             break;
645     }
646 
647     switch (controlId)
648     {
649         case CHECKBOX_AUTOEXTENSION:
650             pCheckbox = new QCheckBox(getResString(resId), m_pExtraControls);
651             // to add/remove automatic file extension based on checkbox
652             connect(pCheckbox, SIGNAL(stateChanged(int)), this,
653                     SLOT(updateAutomaticFileExtension()));
654             widget = pCheckbox;
655             break;
656         case CHECKBOX_PASSWORD:
657         case CHECKBOX_FILTEROPTIONS:
658         case CHECKBOX_READONLY:
659         case CHECKBOX_LINK:
660         case CHECKBOX_PREVIEW:
661         case CHECKBOX_SELECTION:
662         case CHECKBOX_GPGENCRYPTION:
663             widget = new QCheckBox(getResString(resId), m_pExtraControls);
664             break;
665         case PUSHBUTTON_PLAY:
666             break;
667         case LISTBOX_VERSION:
668         case LISTBOX_TEMPLATE:
669         case LISTBOX_IMAGE_ANCHOR:
670         case LISTBOX_IMAGE_TEMPLATE:
671         case LISTBOX_FILTER_SELECTOR:
672             label = new QLabel(getResString(resId), m_pExtraControls);
673             widget = new QComboBox(m_pExtraControls);
674             label->setBuddy(widget);
675             break;
676         case LISTBOX_VERSION_LABEL:
677         case LISTBOX_TEMPLATE_LABEL:
678         case LISTBOX_IMAGE_TEMPLATE_LABEL:
679         case LISTBOX_IMAGE_ANCHOR_LABEL:
680             break;
681     }
682 
683     if (widget)
684     {
685         const int row = m_pLayout->rowCount();
686         if (label)
687             m_pLayout->addWidget(label, row, 0);
688         m_pLayout->addWidget(widget, row, 1);
689         m_aCustomWidgetsMap.insert(controlId, widget);
690     }
691 }
692 
initialize(const uno::Sequence<uno::Any> & args)693 void SAL_CALL Qt5FilePicker::initialize(const uno::Sequence<uno::Any>& args)
694 {
695     // parameter checking
696     uno::Any arg;
697     if (args.getLength() == 0)
698         throw lang::IllegalArgumentException("no arguments", static_cast<XFilePicker2*>(this), 1);
699 
700     arg = args[0];
701 
702     if ((arg.getValueType() != cppu::UnoType<sal_Int16>::get())
703         && (arg.getValueType() != cppu::UnoType<sal_Int8>::get()))
704     {
705         throw lang::IllegalArgumentException("invalid argument type",
706                                              static_cast<XFilePicker2*>(this), 1);
707     }
708 
709     SolarMutexGuard g;
710     auto* pSalInst(static_cast<Qt5Instance*>(GetSalData()->m_pInstance));
711     assert(pSalInst);
712     if (!pSalInst->IsMainThread())
713     {
714         pSalInst->RunInMainThread([this, args]() { initialize(args); });
715         return;
716     }
717 
718     m_aNamedFilterToExtensionMap.clear();
719     m_aNamedFilterList.clear();
720     m_aTitleToFilterMap.clear();
721     m_aCurrentFilter.clear();
722 
723     sal_Int16 templateId = -1;
724     arg >>= templateId;
725 
726     QFileDialog::AcceptMode acceptMode = QFileDialog::AcceptOpen;
727     switch (templateId)
728     {
729         case FILEOPEN_SIMPLE:
730             break;
731 
732         case FILESAVE_SIMPLE:
733             acceptMode = QFileDialog::AcceptSave;
734             break;
735 
736         case FILESAVE_AUTOEXTENSION:
737             acceptMode = QFileDialog::AcceptSave;
738             addCustomControl(CHECKBOX_AUTOEXTENSION);
739             break;
740 
741         case FILESAVE_AUTOEXTENSION_PASSWORD:
742             acceptMode = QFileDialog::AcceptSave;
743             addCustomControl(CHECKBOX_AUTOEXTENSION);
744             addCustomControl(CHECKBOX_PASSWORD);
745             addCustomControl(CHECKBOX_GPGENCRYPTION);
746             break;
747 
748         case FILESAVE_AUTOEXTENSION_PASSWORD_FILTEROPTIONS:
749             acceptMode = QFileDialog::AcceptSave;
750             addCustomControl(CHECKBOX_AUTOEXTENSION);
751             addCustomControl(CHECKBOX_PASSWORD);
752             addCustomControl(CHECKBOX_GPGENCRYPTION);
753             addCustomControl(CHECKBOX_FILTEROPTIONS);
754             break;
755 
756         case FILESAVE_AUTOEXTENSION_SELECTION:
757             acceptMode = QFileDialog::AcceptSave;
758             addCustomControl(CHECKBOX_AUTOEXTENSION);
759             addCustomControl(CHECKBOX_SELECTION);
760             break;
761 
762         case FILESAVE_AUTOEXTENSION_TEMPLATE:
763             acceptMode = QFileDialog::AcceptSave;
764             addCustomControl(CHECKBOX_AUTOEXTENSION);
765             addCustomControl(LISTBOX_TEMPLATE);
766             break;
767 
768         case FILEOPEN_LINK_PREVIEW_IMAGE_TEMPLATE:
769             addCustomControl(CHECKBOX_LINK);
770             addCustomControl(CHECKBOX_PREVIEW);
771             addCustomControl(LISTBOX_IMAGE_TEMPLATE);
772             break;
773 
774         case FILEOPEN_LINK_PREVIEW_IMAGE_ANCHOR:
775             addCustomControl(CHECKBOX_LINK);
776             addCustomControl(CHECKBOX_PREVIEW);
777             addCustomControl(LISTBOX_IMAGE_ANCHOR);
778             break;
779 
780         case FILEOPEN_PLAY:
781             addCustomControl(PUSHBUTTON_PLAY);
782             break;
783 
784         case FILEOPEN_LINK_PLAY:
785             addCustomControl(CHECKBOX_LINK);
786             addCustomControl(PUSHBUTTON_PLAY);
787             break;
788 
789         case FILEOPEN_READONLY_VERSION:
790             addCustomControl(CHECKBOX_READONLY);
791             addCustomControl(LISTBOX_VERSION);
792             break;
793 
794         case FILEOPEN_LINK_PREVIEW:
795             addCustomControl(CHECKBOX_LINK);
796             addCustomControl(CHECKBOX_PREVIEW);
797             break;
798 
799         case FILEOPEN_PREVIEW:
800             addCustomControl(CHECKBOX_PREVIEW);
801             break;
802 
803         default:
804             throw lang::IllegalArgumentException("Unknown template",
805                                                  static_cast<XFilePicker2*>(this), 1);
806     }
807 
808     const char* resId = nullptr;
809     switch (acceptMode)
810     {
811         case QFileDialog::AcceptOpen:
812             resId = STR_FPICKER_OPEN;
813             break;
814         case QFileDialog::AcceptSave:
815             resId = STR_FPICKER_SAVE;
816             m_pFileDialog->setFileMode(QFileDialog::AnyFile);
817             break;
818     }
819 
820     m_pFileDialog->setAcceptMode(acceptMode);
821     m_pFileDialog->setWindowTitle(getResString(resId));
822 
823     css::uno::Reference<css::awt::XWindow> xParentWindow;
824     if (args.getLength() > 1)
825         args[1] >>= xParentWindow;
826     if (xParentWindow.is())
827     {
828         css::uno::Reference<css::awt::XSystemDependentWindowPeer> xSysWinPeer(xParentWindow,
829                                                                               css::uno::UNO_QUERY);
830         if (xSysWinPeer.is())
831         {
832             // the sal_*Int8 handling is strange, but it's public API - no way around
833             css::uno::Sequence<sal_Int8> aProcessIdent(16);
834             rtl_getGlobalProcessId(reinterpret_cast<sal_uInt8*>(aProcessIdent.getArray()));
835             uno::Any aAny = xSysWinPeer->getWindowHandle(
836                 aProcessIdent, css::lang::SystemDependent::SYSTEM_XWINDOW);
837             css::awt::SystemDependentXWindow xSysWin;
838             aAny >>= xSysWin;
839 
840             const auto& pFrames = pSalInst->getFrames();
841             const long aWindowHandle = xSysWin.WindowHandle;
842             const auto it = std::find_if(pFrames.begin(), pFrames.end(),
843                                          [&aWindowHandle](auto pFrame) -> bool {
844                                              const SystemEnvData* pData = pFrame->GetSystemData();
845                                              return pData && long(pData->aWindow) == aWindowHandle;
846                                          });
847             if (it != pFrames.end())
848                 m_pParentWidget = static_cast<Qt5Frame*>(*it)->asChild();
849         }
850     }
851 }
852 
cancel()853 void SAL_CALL Qt5FilePicker::cancel() { m_pFileDialog->reject(); }
854 
disposing(const lang::EventObject & rEvent)855 void SAL_CALL Qt5FilePicker::disposing(const lang::EventObject& rEvent)
856 {
857     uno::Reference<XFilePickerListener> xFilePickerListener(rEvent.Source, uno::UNO_QUERY);
858 
859     if (xFilePickerListener.is())
860     {
861         removeFilePickerListener(xFilePickerListener);
862     }
863 }
864 
queryTermination(const css::lang::EventObject &)865 void SAL_CALL Qt5FilePicker::queryTermination(const css::lang::EventObject&)
866 {
867     throw css::frame::TerminationVetoException();
868 }
869 
notifyTermination(const css::lang::EventObject &)870 void SAL_CALL Qt5FilePicker::notifyTermination(const css::lang::EventObject&)
871 {
872     SolarMutexGuard aGuard;
873     m_pFileDialog->reject();
874 }
875 
getImplementationName()876 OUString SAL_CALL Qt5FilePicker::getImplementationName()
877 {
878     return "com.sun.star.ui.dialogs.Qt5FilePicker";
879 }
880 
supportsService(const OUString & ServiceName)881 sal_Bool SAL_CALL Qt5FilePicker::supportsService(const OUString& ServiceName)
882 {
883     return cppu::supportsService(this, ServiceName);
884 }
885 
getSupportedServiceNames()886 uno::Sequence<OUString> SAL_CALL Qt5FilePicker::getSupportedServiceNames()
887 {
888     return FilePicker_getSupportedServiceNames();
889 }
890 
updateAutomaticFileExtension()891 void Qt5FilePicker::updateAutomaticFileExtension()
892 {
893     bool bSetAutoExtension
894         = getValue(CHECKBOX_AUTOEXTENSION, ControlActions::GET_SELECTED_ITEM).get<bool>();
895     if (bSetAutoExtension)
896     {
897         QString sSuffix = m_aNamedFilterToExtensionMap.value(m_pFileDialog->selectedNameFilter());
898         // string is "*.<SUFFIX>" if a specific filter was selected that has exactly one possible file extension
899         if (sSuffix.lastIndexOf("*.") == 0)
900         {
901             sSuffix = sSuffix.remove("*.");
902             m_pFileDialog->setDefaultSuffix(sSuffix);
903         }
904         else
905         {
906             // fall back to setting none otherwise
907             SAL_INFO(
908                 "vcl.qt5",
909                 "Unable to retrieve unambiguous file extension. Will not add any automatically.");
910             bSetAutoExtension = false;
911         }
912     }
913 
914     if (!bSetAutoExtension)
915         m_pFileDialog->setDefaultSuffix("");
916 }
917 
filterSelected(const QString &)918 void Qt5FilePicker::filterSelected(const QString&)
919 {
920     FilePickerEvent aEvent;
921     aEvent.ElementId = LISTBOX_FILTER;
922     SAL_INFO("vcl.qt5", "filter changed");
923     if (m_xListener.is())
924         m_xListener->controlStateChanged(aEvent);
925 }
926 
currentChanged(const QString &)927 void Qt5FilePicker::currentChanged(const QString&)
928 {
929     FilePickerEvent aEvent;
930     SAL_INFO("vcl.qt5", "file selection changed");
931     if (m_xListener.is())
932         m_xListener->fileSelectionChanged(aEvent);
933 }
934 
getDirectory()935 OUString Qt5FilePicker::getDirectory()
936 {
937     uno::Sequence<OUString> seq = getSelectedFiles();
938     if (seq.getLength() > 1)
939         seq.realloc(1);
940     return seq[0];
941 }
942 
setDescription(const OUString &)943 void Qt5FilePicker::setDescription(const OUString&) {}
944 
945 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
946