1 /*
2     SPDX-FileCopyrightText: 2009 Andreas Pakulat <apaku@gmx.de>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "openwithplugin.h"
8 
9 #include <QAction>
10 #include <QApplication>
11 #include <QMenu>
12 #include <QMimeDatabase>
13 #include <QMimeType>
14 #include <QVariantList>
15 
16 #include <kio_version.h>
17 #include <kservice_version.h>
18 #include <KSharedConfig>
19 #include <KConfigGroup>
20 #include <KLocalizedString>
21 #include <KMessageBox>
22 #if KSERVICE_VERSION >= QT_VERSION_CHECK(5, 68, 0)
23 #include <KApplicationTrader>
24 #endif
25 #include <KMimeTypeTrader>
26 #include <KParts/MainWindow>
27 #include <KPluginFactory>
28 #include <KService>
29 #include <KOpenWithDialog>
30 #if KIO_VERSION >= QT_VERSION_CHECK(5, 71, 0)
31 #include <KIO/ApplicationLauncherJob>
32 #include <KIO/JobUiDelegate>
33 #else
34 #include <KRun>
35 #endif
36 
37 #include <interfaces/contextmenuextension.h>
38 #include <interfaces/context.h>
39 #include <project/projectmodel.h>
40 #include <util/path.h>
41 
42 #include <interfaces/icore.h>
43 #include <interfaces/iuicontroller.h>
44 #include <interfaces/idocumentcontroller.h>
45 
46 using namespace KDevelop;
47 
48 K_PLUGIN_FACTORY_WITH_JSON(KDevOpenWithFactory, "kdevopenwith.json", registerPlugin<OpenWithPlugin>();)
49 
50 namespace {
51 
sortActions(QAction * left,QAction * right)52 bool sortActions(QAction* left, QAction* right)
53 {
54     return left->text() < right->text();
55 }
56 
isTextEditor(const KService::Ptr & service)57 bool isTextEditor(const KService::Ptr& service)
58 {
59     return service->serviceTypes().contains( QStringLiteral("KTextEditor/Document") );
60 }
61 
defaultForMimeType(const QString & mimeType)62 QString defaultForMimeType(const QString& mimeType)
63 {
64     KConfigGroup config = KSharedConfig::openConfig()->group("Open With Defaults");
65     if (config.hasKey(mimeType)) {
66         QString storageId = config.readEntry(mimeType, QString());
67         if (!storageId.isEmpty() && KService::serviceByStorageId(storageId)) {
68             return storageId;
69         }
70     }
71     return QString();
72 }
73 
canOpenDefault(const QString & mimeType)74 bool canOpenDefault(const QString& mimeType)
75 {
76     if (defaultForMimeType(mimeType).isEmpty() && mimeType == QLatin1String("inode/directory")) {
77         // potentially happens in non-kde environments apparently, see https://git.reviewboard.kde.org/r/122373
78 #if KSERVICE_VERSION >= QT_VERSION_CHECK(5, 68, 0)
79         return KApplicationTrader::preferredService(mimeType);
80 #else
81         return KMimeTypeTrader::self()->preferredService(mimeType);
82 #endif
83     } else {
84         return true;
85     }
86 }
87 }
88 
OpenWithPlugin(QObject * parent,const QVariantList &)89 OpenWithPlugin::OpenWithPlugin ( QObject* parent, const QVariantList& )
90     : IPlugin ( QStringLiteral("kdevopenwith"), parent )
91 {
92 }
93 
~OpenWithPlugin()94 OpenWithPlugin::~OpenWithPlugin()
95 {
96 }
97 
contextMenuExtension(KDevelop::Context * context,QWidget * parent)98 KDevelop::ContextMenuExtension OpenWithPlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent)
99 {
100     // do not recurse
101     if (context->hasType(KDevelop::Context::OpenWithContext)) {
102         return ContextMenuExtension();
103     }
104 
105     m_urls.clear();
106     m_services.clear();
107 
108     auto* filectx = dynamic_cast<FileContext*>( context );
109     auto* projctx = dynamic_cast<ProjectItemContext*>( context );
110     if ( filectx && filectx->urls().count() > 0 ) {
111         m_urls = filectx->urls();
112     } else if ( projctx && projctx->items().count() > 0 ) {
113         // For now, let's handle *either* files only *or* directories only
114         const auto items = projctx->items();
115         const int wantedType = items.at(0)->type();
116         for (ProjectBaseItem* item : items) {
117             if (wantedType == ProjectBaseItem::File && item->file()) {
118                 m_urls << item->file()->path().toUrl();
119             } else if ((wantedType == ProjectBaseItem::Folder || wantedType == ProjectBaseItem::BuildFolder) && item->folder()) {
120                 m_urls << item->folder()->path().toUrl();
121             }
122         }
123     }
124 
125     if (m_urls.isEmpty()) {
126         return KDevelop::ContextMenuExtension();
127     }
128 
129     // Ok, lets fetch the mimetype for the !!first!! url and the relevant services
130     // TODO: Think about possible alternatives to using the mimetype of the first url.
131     QMimeType mimetype = QMimeDatabase().mimeTypeForUrl(m_urls.first());
132     m_mimeType = mimetype.name();
133 
134     QList<QAction*> partActions = actionsForServiceType(QStringLiteral("KParts/ReadOnlyPart"), parent);
135     QList<QAction*> appActions = actionsForServiceType(QStringLiteral("Application"), parent);
136 
137     OpenWithContext subContext(m_urls, mimetype);
138     const QList<ContextMenuExtension> extensions = ICore::self()->pluginController()->queryPluginsForContextMenuExtensions( &subContext, parent);
139     for (const ContextMenuExtension& ext : extensions) {
140         appActions += ext.actions(ContextMenuExtension::OpenExternalGroup);
141         partActions += ext.actions(ContextMenuExtension::OpenEmbeddedGroup);
142     }
143 
144     {
145         auto other = new QAction(i18nc("@item:menu", "Other..."), parent);
146         connect(other, &QAction::triggered, this, [this] {
147             auto dialog = new KOpenWithDialog(m_urls, ICore::self()->uiController()->activeMainWindow());
148             if (dialog->exec() == QDialog::Accepted && dialog->service()) {
149                 openService(dialog->service());
150             }
151         });
152         appActions << other;
153     }
154 
155     // Now setup a menu with actions for each part and app
156     auto* menu = new QMenu(i18nc("@title:menu", "Open With"), parent);
157     auto documentOpenIcon = QIcon::fromTheme( QStringLiteral("document-open") );
158     menu->setIcon( documentOpenIcon );
159 
160     if (!partActions.isEmpty()) {
161         menu->addSection(i18nc("@title:menu", "Embedded Editors"));
162         menu->addActions( partActions );
163     }
164     if (!appActions.isEmpty()) {
165         menu->addSection(i18nc("@title:menu", "External Applications"));
166         menu->addActions( appActions );
167     }
168 
169     KDevelop::ContextMenuExtension ext;
170 
171     if (canOpenDefault(m_mimeType)) {
172         auto* openAction = new QAction(i18nc("@action:inmenu", "Open"), parent);
173         openAction->setIcon( documentOpenIcon );
174         connect( openAction, &QAction::triggered, this, &OpenWithPlugin::openDefault );
175         ext.addAction( KDevelop::ContextMenuExtension::FileGroup, openAction );
176     }
177 
178     ext.addAction(KDevelop::ContextMenuExtension::FileGroup, menu->menuAction());
179     return ext;
180 }
181 
actionsForServiceType(const QString & serviceType,QWidget * parent)182 QList<QAction*> OpenWithPlugin::actionsForServiceType(const QString& serviceType, QWidget* parent)
183 {
184     const KService::List list = KMimeTypeTrader::self()->query( m_mimeType, serviceType );
185     KService::Ptr pref = KMimeTypeTrader::self()->preferredService( m_mimeType, serviceType );
186 
187     m_services += list;
188     QList<QAction*> actions;
189     QAction* standardAction = nullptr;
190     const QString defaultId = defaultForMimeType(m_mimeType);
191     for (auto& svc : list) {
192         auto* act = new QAction(isTextEditor(svc) ? i18nc("@item:inmenu", "Default Editor") : svc->name(), parent);
193         act->setIcon( QIcon::fromTheme( svc->icon() ) );
194         if (svc->storageId() == defaultId || (defaultId.isEmpty() && isTextEditor(svc))) {
195             QFont font = act->font();
196             font.setBold(true);
197             act->setFont(font);
198         }
199         const QString sid = svc->storageId();
200         connect(act, &QAction::triggered, this, [this, sid]() { open(sid); } );
201         actions << act;
202         if ( isTextEditor(svc) ) {
203             standardAction = act;
204         } else if ( svc->storageId() == pref->storageId() ) {
205             standardAction = act;
206         }
207     }
208     std::sort(actions.begin(), actions.end(), sortActions);
209     if (standardAction) {
210         actions.removeOne(standardAction);
211         actions.prepend(standardAction);
212     }
213     return actions;
214 }
215 
openDefault()216 void OpenWithPlugin::openDefault()
217 {
218     //  check preferred handler
219     const QString defaultId = defaultForMimeType(m_mimeType);
220     if (!defaultId.isEmpty()) {
221         open(defaultId);
222         return;
223     }
224 
225     // default handlers
226     if (m_mimeType == QLatin1String("inode/directory")) {
227 #if KSERVICE_VERSION >= QT_VERSION_CHECK(5, 68, 0)
228         KService::Ptr service = KApplicationTrader::preferredService(m_mimeType);
229 #else
230         KService::Ptr service = KMimeTypeTrader::self()->preferredService(m_mimeType);
231 #endif
232 #if KIO_VERSION >= QT_VERSION_CHECK(5, 71, 0)
233         auto* job = new KIO::ApplicationLauncherJob(service);
234         job->setUrls(m_urls);
235         job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled,
236                                                   ICore::self()->uiController()->activeMainWindow()));
237         job->start();
238 #else
239         KRun::runService(*service, m_urls, ICore::self()->uiController()->activeMainWindow());
240 #endif
241     } else {
242         for (const QUrl& u : qAsConst(m_urls)) {
243             ICore::self()->documentController()->openDocument( u );
244         }
245     }
246 }
247 
open(const QString & storageid)248 void OpenWithPlugin::open( const QString& storageid )
249 {
250     openService(KService::serviceByStorageId( storageid ));
251 }
252 
openService(const KService::Ptr & service)253 void OpenWithPlugin::openService(const KService::Ptr& service)
254 {
255     if (service->isApplication()) {
256 #if KIO_VERSION >= QT_VERSION_CHECK(5, 71, 0)
257         auto* job = new KIO::ApplicationLauncherJob(service);
258         job->setUrls(m_urls);
259         job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled,
260                                                   ICore::self()->uiController()->activeMainWindow()));
261         job->start();
262 #else
263         KRun::runService( *service, m_urls, ICore::self()->uiController()->activeMainWindow() );
264 #endif
265     } else {
266         QString prefName = service->desktopEntryName();
267         if (isTextEditor(service)) {
268             // If the user chose a KTE part, lets make sure we're creating a TextDocument instead of
269             // a PartDocument by passing no preferredpart to the documentcontroller
270             // TODO: Solve this rather inside DocumentController
271             prefName.clear();
272         }
273         for (const QUrl& u : qAsConst(m_urls)) {
274             ICore::self()->documentController()->openDocument( u, prefName );
275         }
276     }
277 
278     KConfigGroup config = KSharedConfig::openConfig()->group("Open With Defaults");
279     if (service->storageId() != config.readEntry(m_mimeType, QString())) {
280         int setDefault = KMessageBox::questionYesNo(
281             qApp->activeWindow(),
282             i18nc("%1: mime type name, %2: app/part name", "Do you want to open all '%1' files by default with %2?",
283                  m_mimeType, service->name() ),
284             i18nc("@title:window", "Set as Default?"),
285             KStandardGuiItem::yes(), KStandardGuiItem::no(),
286             QStringLiteral("OpenWith-%1").arg(m_mimeType)
287         );
288         if (setDefault == KMessageBox::Yes) {
289             config.writeEntry(m_mimeType, service->storageId());
290         }
291     }
292 }
293 
openFilesInternal(const QList<QUrl> & files)294 void OpenWithPlugin::openFilesInternal( const QList<QUrl>& files )
295 {
296     if (files.isEmpty()) {
297         return;
298     }
299 
300     m_urls = files;
301     m_mimeType = QMimeDatabase().mimeTypeForUrl(m_urls.first()).name();
302     openDefault();
303 }
304 
305 #include "openwithplugin.moc"
306