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 <vcl/svapp.hxx>
21 
22 #include "kde5_filepicker.hxx"
23 
24 #include <KWindowSystem>
25 #include <KFileWidget>
26 
27 #include <QtCore/QDebug>
28 #include <QtCore/QUrl>
29 #include <QtWidgets/QCheckBox>
30 #include <QtWidgets/QFileDialog>
31 #include <QtWidgets/QGridLayout>
32 #include <QtWidgets/QWidget>
33 #include <QtWidgets/QApplication>
34 
35 // KDE5FilePicker
36 
KDE5FilePicker(QObject * parent)37 KDE5FilePicker::KDE5FilePicker(QObject* parent)
38     : QObject(parent)
39     , _dialog(new QFileDialog(nullptr, {}, QDir::homePath()))
40     , _extraControls(new QWidget)
41     , _layout(new QGridLayout(_extraControls))
42     , _winId(0)
43 {
44     _dialog->setSupportedSchemes({
45         QStringLiteral("file"), QStringLiteral("ftp"), QStringLiteral("http"),
46         QStringLiteral("https"), QStringLiteral("webdav"), QStringLiteral("webdavs"),
47         QStringLiteral("smb"),
48         QStringLiteral(""), // this makes removable devices shown
49     });
50 
51     setMultiSelectionMode(false);
52 
53     connect(_dialog, &QFileDialog::filterSelected, this, &KDE5FilePicker::filterChanged);
54     connect(_dialog, &QFileDialog::fileSelected, this, &KDE5FilePicker::selectionChanged);
55 
56     setupCustomWidgets();
57 }
58 
enableFolderMode()59 void KDE5FilePicker::enableFolderMode()
60 {
61     _dialog->setOption(QFileDialog::ShowDirsOnly, true);
62     // Workaround for https://bugs.kde.org/show_bug.cgi?id=406464 :
63     // Don't set file mode to QFileDialog::Directory when native KDE Plasma 5
64     // file dialog is used, since clicking on directory "bar" inside directory "foo"
65     // and then confirming would return "foo" rather than "foo/bar";
66     // on the other hand, non-native file dialog needs 'QFileDialog::Directory'
67     // and doesn't allow folder selection otherwise
68     if (Application::GetDesktopEnvironment() != "PLASMA5")
69     {
70         _dialog->setFileMode(QFileDialog::Directory);
71     }
72 }
73 
~KDE5FilePicker()74 KDE5FilePicker::~KDE5FilePicker()
75 {
76     delete _extraControls;
77     delete _dialog;
78 }
79 
setTitle(const QString & title)80 void KDE5FilePicker::setTitle(const QString& title) { _dialog->setWindowTitle(title); }
81 
execute()82 bool KDE5FilePicker::execute()
83 {
84     if (!_filters.isEmpty())
85         _dialog->setNameFilters(_filters);
86     if (!_currentFilter.isEmpty())
87         _dialog->selectNameFilter(_currentFilter);
88 
89     _dialog->show();
90     //block and wait for user input
91     return _dialog->exec() == QFileDialog::Accepted;
92 }
93 
setMultiSelectionMode(bool multiSelect)94 void KDE5FilePicker::setMultiSelectionMode(bool multiSelect)
95 {
96     if (_dialog->acceptMode() == QFileDialog::AcceptSave)
97         return;
98 
99     _dialog->setFileMode(multiSelect ? QFileDialog::ExistingFiles : QFileDialog::ExistingFile);
100 }
101 
setDefaultName(const QString & name)102 void KDE5FilePicker::setDefaultName(const QString& name) { _dialog->selectFile(name); }
103 
setDisplayDirectory(const QString & dir)104 void KDE5FilePicker::setDisplayDirectory(const QString& dir)
105 {
106     _dialog->setDirectoryUrl(QUrl(dir));
107 }
108 
getDisplayDirectory() const109 QString KDE5FilePicker::getDisplayDirectory() const { return _dialog->directoryUrl().url(); }
110 
getSelectedFiles() const111 QList<QUrl> KDE5FilePicker::getSelectedFiles() const { return _dialog->selectedUrls(); }
112 
appendFilter(const QString & title,const QString & filter)113 void KDE5FilePicker::appendFilter(const QString& title, const QString& filter)
114 {
115     QString t = title;
116     QString f = filter;
117     // '/' need to be escaped else they are assumed to be mime types by kfiledialog
118     //see the docs
119     t.replace("/", "\\/");
120 
121     // openoffice gives us filters separated by ';' qt dialogs just want space separated
122     f.replace(";", " ");
123 
124     // make sure "*.*" is not used as "all files"
125     f.replace("*.*", "*");
126 
127     _filters << QStringLiteral("%1 (%2)").arg(t, f);
128     _titleToFilters[t] = _filters.constLast();
129 }
130 
setCurrentFilter(const QString & title)131 void KDE5FilePicker::setCurrentFilter(const QString& title)
132 {
133     _currentFilter = _titleToFilters.value(title);
134 }
135 
getCurrentFilter() const136 QString KDE5FilePicker::getCurrentFilter() const
137 {
138     QString filter = _titleToFilters.key(_dialog->selectedNameFilter());
139 
140     //default if not found
141     if (filter.isEmpty())
142         filter = "ODF Text Document (.odt)";
143 
144     return filter;
145 }
146 
setValue(sal_Int16 controlId,sal_Int16,bool value)147 void KDE5FilePicker::setValue(sal_Int16 controlId, sal_Int16 /*nControlAction*/, bool value)
148 {
149     if (_customWidgets.contains(controlId))
150     {
151         QCheckBox* cb = dynamic_cast<QCheckBox*>(_customWidgets.value(controlId));
152         if (cb)
153             cb->setChecked(value);
154     }
155     else
156         qWarning() << "set value on unknown control" << controlId;
157 }
158 
getValue(sal_Int16 controlId,sal_Int16) const159 bool KDE5FilePicker::getValue(sal_Int16 controlId, sal_Int16 /*nControlAction*/) const
160 {
161     bool ret = false;
162     if (_customWidgets.contains(controlId))
163     {
164         QCheckBox* cb = dynamic_cast<QCheckBox*>(_customWidgets.value(controlId));
165         if (cb)
166             ret = cb->isChecked();
167     }
168     else
169         qWarning() << "get value on unknown control" << controlId;
170 
171     return ret;
172 }
173 
enableControl(sal_Int16 controlId,bool enable)174 void KDE5FilePicker::enableControl(sal_Int16 controlId, bool enable)
175 {
176     if (_customWidgets.contains(controlId))
177         _customWidgets.value(controlId)->setEnabled(enable);
178     else
179         qWarning() << "enable on unknown control" << controlId;
180 }
181 
setLabel(sal_Int16 controlId,const QString & label)182 void KDE5FilePicker::setLabel(sal_Int16 controlId, const QString& label)
183 {
184     if (_customWidgets.contains(controlId))
185     {
186         QCheckBox* cb = dynamic_cast<QCheckBox*>(_customWidgets.value(controlId));
187         if (cb)
188             cb->setText(label);
189     }
190     else
191         qWarning() << "set label on unknown control" << controlId;
192 }
193 
getLabel(sal_Int16 controlId) const194 QString KDE5FilePicker::getLabel(sal_Int16 controlId) const
195 {
196     QString label;
197     if (_customWidgets.contains(controlId))
198     {
199         QCheckBox* cb = dynamic_cast<QCheckBox*>(_customWidgets.value(controlId));
200         if (cb)
201             label = cb->text();
202     }
203     else
204         qWarning() << "get label on unknown control" << controlId;
205 
206     return label;
207 }
208 
addCheckBox(sal_Int16 controlId,const QString & label,bool hidden)209 void KDE5FilePicker::addCheckBox(sal_Int16 controlId, const QString& label, bool hidden)
210 {
211     auto resString = label;
212     resString.replace('~', '&');
213 
214     auto widget = new QCheckBox(resString, _extraControls);
215     widget->setHidden(hidden);
216     if (!hidden)
217     {
218         _layout->addWidget(widget);
219     }
220     _customWidgets.insert(controlId, widget);
221 }
222 
initialize(bool saveDialog)223 void KDE5FilePicker::initialize(bool saveDialog)
224 {
225     //default is opening
226     QFileDialog::AcceptMode operationMode
227         = saveDialog ? QFileDialog::AcceptSave : QFileDialog::AcceptOpen;
228 
229     _dialog->setAcceptMode(operationMode);
230 
231     if (saveDialog)
232     {
233         _dialog->setConfirmOverwrite(true);
234         _dialog->setFileMode(QFileDialog::AnyFile);
235     }
236 }
237 
setWinId(sal_uIntPtr winId)238 void KDE5FilePicker::setWinId(sal_uIntPtr winId) { _winId = winId; }
239 
setupCustomWidgets()240 void KDE5FilePicker::setupCustomWidgets()
241 {
242     // When using the platform-native Plasma/KDE5 file picker, we currently rely on KFileWidget
243     // being present to add the custom controls visible (s. 'eventFilter' method).
244     // Since this doesn't work for other desktop environments, use a non-native
245     // dialog there in order not to lose the custom controls and insert the custom
246     // widget in the layout returned by QFileDialog::layout()
247     // (which returns nullptr for native file dialogs)
248     if (Application::GetDesktopEnvironment() == "PLASMA5")
249     {
250         qApp->installEventFilter(this);
251     }
252     else
253     {
254         _dialog->setOption(QFileDialog::DontUseNativeDialog);
255         QGridLayout* pLayout = static_cast<QGridLayout*>(_dialog->layout());
256         assert(pLayout);
257         const int row = pLayout->rowCount();
258         pLayout->addWidget(_extraControls, row, 1);
259     }
260 }
261 
eventFilter(QObject * o,QEvent * e)262 bool KDE5FilePicker::eventFilter(QObject* o, QEvent* e)
263 {
264     if (e->type() == QEvent::Show && o->isWidgetType())
265     {
266         auto* w = static_cast<QWidget*>(o);
267         if (!w->parentWidget() && w->isModal())
268         {
269             /*
270              To replace when baseline will include kwindowsystem >= 5.62 with:
271              w->setAttribute(Qt::WA_NativeWindow, true);
272              KWindowSystem::setMainWindow(w->windowHandle(), _winId);
273             */
274             SAL_WNODEPRECATED_DECLARATIONS_PUSH
275             KWindowSystem::setMainWindow(w, _winId);
276             SAL_WNODEPRECATED_DECLARATIONS_POP
277             if (auto* fileWidget = w->findChild<KFileWidget*>({}, Qt::FindDirectChildrenOnly))
278             {
279                 fileWidget->setCustomWidget(_extraControls);
280                 // remove event filter again; the only purpose was to set the custom widget here
281                 qApp->removeEventFilter(this);
282             }
283         }
284     }
285     return QObject::eventFilter(o, e);
286 }
287 
288 #include <kde5_filepicker.moc>
289 
290 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
291