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