1 /***************************************************************************
2 qgslinuxnative.h
3 -------------------
4 begin : July 2018
5 copyright : (C) 2018 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************/
8
9 /***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
18 #include "qgslinuxnative.h"
19
20 #include <QCoreApplication>
21 #include <QUrl>
22 #include <QString>
23 #include <QtDBus/QtDBus>
24 #include <QtDebug>
25 #include <QImage>
26 #include <QProcess>
27
capabilities() const28 QgsNative::Capabilities QgsLinuxNative::capabilities() const
29 {
30 return NativeDesktopNotifications | NativeFilePropertiesDialog | NativeOpenTerminalAtPath;
31 }
32
initializeMainWindow(QWindow *,const QString &,const QString &,const QString &)33 void QgsLinuxNative::initializeMainWindow( QWindow *,
34 const QString &,
35 const QString &,
36 const QString & )
37 {
38 // Hardcoded desktop file value matching our official .deb packages
39 mDesktopFile = QStringLiteral( "org.qgis.qgis.desktop" );
40 }
41
openFileExplorerAndSelectFile(const QString & path)42 void QgsLinuxNative::openFileExplorerAndSelectFile( const QString &path )
43 {
44 if ( !QDBusConnection::sessionBus().isConnected() )
45 {
46 QgsNative::openFileExplorerAndSelectFile( path );
47 return;
48 }
49
50 QDBusInterface iface( QStringLiteral( "org.freedesktop.FileManager1" ),
51 QStringLiteral( "/org/freedesktop/FileManager1" ),
52 QStringLiteral( "org.freedesktop.FileManager1" ),
53 QDBusConnection::sessionBus() );
54
55 iface.call( QDBus::NoBlock, QStringLiteral( "ShowItems" ), QStringList( QUrl::fromLocalFile( path ).toString() ), QStringLiteral( "QGIS" ) );
56 if ( iface.lastError().type() != QDBusError::NoError )
57 {
58 QgsNative::openFileExplorerAndSelectFile( path );
59 }
60 }
61
showFileProperties(const QString & path)62 void QgsLinuxNative::showFileProperties( const QString &path )
63 {
64 if ( !QDBusConnection::sessionBus().isConnected() )
65 {
66 QgsNative::showFileProperties( path );
67 return;
68 }
69
70 QDBusInterface iface( QStringLiteral( "org.freedesktop.FileManager1" ),
71 QStringLiteral( "/org/freedesktop/FileManager1" ),
72 QStringLiteral( "org.freedesktop.FileManager1" ),
73 QDBusConnection::sessionBus() );
74
75 iface.call( QDBus::NoBlock, QStringLiteral( "ShowItemProperties" ), QStringList( QUrl::fromLocalFile( path ).toString() ), QStringLiteral( "QGIS" ) );
76 if ( iface.lastError().type() != QDBusError::NoError )
77 {
78 QgsNative::showFileProperties( path );
79 }
80 }
81
showUndefinedApplicationProgress()82 void QgsLinuxNative::showUndefinedApplicationProgress()
83 {
84 const QVariantMap properties
85 {
86 { QStringLiteral( "progress-visible" ), true },
87 { QStringLiteral( "progress" ), 0.0 }
88 };
89
90 QDBusMessage message = QDBusMessage::createSignal( QStringLiteral( "/org/qgis/UnityLauncher" ),
91 QStringLiteral( "com.canonical.Unity.LauncherEntry" ),
92 QStringLiteral( "Update" ) );
93 message.setArguments( {mDesktopFile, properties} );
94 QDBusConnection::sessionBus().send( message );
95 }
96
setApplicationProgress(double progress)97 void QgsLinuxNative::setApplicationProgress( double progress )
98 {
99 const QVariantMap properties
100 {
101 { QStringLiteral( "progress-visible" ), true },
102 { QStringLiteral( "progress" ), progress / 100.0 }
103 };
104
105 QDBusMessage message = QDBusMessage::createSignal( QStringLiteral( "/org/qgis/UnityLauncher" ),
106 QStringLiteral( "com.canonical.Unity.LauncherEntry" ),
107 QStringLiteral( "Update" ) );
108 message.setArguments( {mDesktopFile, properties} );
109 QDBusConnection::sessionBus().send( message );
110 }
111
hideApplicationProgress()112 void QgsLinuxNative::hideApplicationProgress()
113 {
114 const QVariantMap properties
115 {
116 { QStringLiteral( "progress-visible" ), false },
117 };
118
119 QDBusMessage message = QDBusMessage::createSignal( QStringLiteral( "/org/qgis/UnityLauncher" ),
120 QStringLiteral( "com.canonical.Unity.LauncherEntry" ),
121 QStringLiteral( "Update" ) );
122 message.setArguments( {mDesktopFile, properties} );
123 QDBusConnection::sessionBus().send( message );
124 }
125
setApplicationBadgeCount(int count)126 void QgsLinuxNative::setApplicationBadgeCount( int count )
127 {
128 // the badge will only be shown when the count is greater than one
129 const QVariantMap properties
130 {
131 { QStringLiteral( "count-visible" ), count > 1 },
132 { QStringLiteral( "count" ), static_cast< long long >( count ) }
133 };
134
135 QDBusMessage message = QDBusMessage::createSignal( QStringLiteral( "/org/qgis/UnityLauncher" ),
136 QStringLiteral( "com.canonical.Unity.LauncherEntry" ),
137 QStringLiteral( "Update" ) );
138 message.setArguments( {mDesktopFile, properties} );
139 QDBusConnection::sessionBus().send( message );
140 }
141
openTerminalAtPath(const QString & path)142 bool QgsLinuxNative::openTerminalAtPath( const QString &path )
143 {
144 // logic adapted from https://askubuntu.com/a/227669,
145 // https://github.com/Microsoft/vscode/blob/fec1775aa52e2124d3f09c7b2ac8f69c57309549/src/vs/workbench/parts/execution/electron-browser/terminal.ts
146 QString term = QStringLiteral( "xterm" );
147 const QString desktopSession = qgetenv( "DESKTOP_SESSION" );
148 const QString currentDesktop = qgetenv( "XDG_CURRENT_DESKTOP" );
149 const QString gdmSession = qgetenv( "GDMSESSION" );
150 const bool isDebian = QFile::exists( QStringLiteral( "/etc/debian_version" ) );
151 if ( isDebian )
152 {
153 term = QStringLiteral( "x-terminal-emulator" );
154 }
155 else if ( desktopSession.contains( QLatin1String( "gnome" ), Qt::CaseInsensitive ) ||
156 currentDesktop.contains( QLatin1String( "gnome" ), Qt::CaseInsensitive ) ||
157 currentDesktop.contains( QLatin1String( "unity" ), Qt::CaseInsensitive ) )
158 {
159 term = QStringLiteral( "gnome-terminal" );
160 }
161 else if ( desktopSession.contains( QLatin1String( "kde" ), Qt::CaseInsensitive ) ||
162 currentDesktop.contains( QLatin1String( "kde" ), Qt::CaseInsensitive ) ||
163 gdmSession.contains( QLatin1String( "kde" ), Qt::CaseInsensitive ) )
164 {
165 term = QStringLiteral( "konsole" );
166 }
167
168 QStringList arguments;
169 arguments << QStringLiteral( "--working-directory" )
170 << path;
171 return QProcess::startDetached( term, QStringList(), path );
172 }
173
174 /**
175 * Automatic marshaling of a QImage for org.freedesktop.Notifications.Notify
176 *
177 * This function is from the Clementine project (see
178 * http://www.clementine-player.org) and licensed under the GNU General Public
179 * License, version 3 or later.
180 *
181 * Copyright 2010, David Sansome <me@davidsansome.com>
182 */
operator <<(QDBusArgument & arg,const QImage & image)183 QDBusArgument &operator<<( QDBusArgument &arg, const QImage &image )
184 {
185 if ( image.isNull() )
186 {
187 arg.beginStructure();
188 arg << 0 << 0 << 0 << false << 0 << 0 << QByteArray();
189 arg.endStructure();
190 return arg;
191 }
192
193 QImage scaled = image.scaledToHeight( 100, Qt::SmoothTransformation );
194 scaled = scaled.convertToFormat( QImage::Format_ARGB32 );
195
196 #if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
197 // ABGR -> ARGB
198 QImage i = scaled.rgbSwapped();
199 #else
200 // ABGR -> GBAR
201 QImage i( scaled.size(), scaled.format() );
202 for ( int y = 0; y < i.height(); ++y )
203 {
204 QRgb *p = ( QRgb * ) scaled.scanLine( y );
205 QRgb *q = ( QRgb * ) i.scanLine( y );
206 QRgb *end = p + scaled.width();
207 while ( p < end )
208 {
209 *q = qRgba( qGreen( *p ), qBlue( *p ), qAlpha( *p ), qRed( *p ) );
210 p++;
211 q++;
212 }
213 }
214 #endif
215
216 arg.beginStructure();
217 arg << i.width();
218 arg << i.height();
219 arg << i.bytesPerLine();
220 arg << i.hasAlphaChannel();
221 const int channels = i.isGrayscale() ? 1 : ( i.hasAlphaChannel() ? 4 : 3 );
222 arg << i.depth() / channels;
223 arg << channels;
224 arg << QByteArray( reinterpret_cast<const char *>( i.bits() ), i.sizeInBytes() );
225 arg.endStructure();
226 return arg;
227 }
228
operator >>(const QDBusArgument & arg,QImage &)229 const QDBusArgument &operator>>( const QDBusArgument &arg, QImage & )
230 {
231 // This is needed to link but shouldn't be called.
232 Q_ASSERT( 0 );
233 return arg;
234 }
235
showDesktopNotification(const QString & summary,const QString & body,const NotificationSettings & settings)236 QgsNative::NotificationResult QgsLinuxNative::showDesktopNotification( const QString &summary, const QString &body, const NotificationSettings &settings )
237 {
238 NotificationResult result;
239 result.successful = false;
240
241 if ( !QDBusConnection::sessionBus().isConnected() )
242 {
243 return result;
244 }
245
246 qDBusRegisterMetaType<QImage>();
247
248 QDBusInterface iface( QStringLiteral( "org.freedesktop.Notifications" ),
249 QStringLiteral( "/org/freedesktop/Notifications" ),
250 QStringLiteral( "org.freedesktop.Notifications" ),
251 QDBusConnection::sessionBus() );
252
253 QVariantMap hints;
254 hints[QStringLiteral( "transient" )] = settings.transient;
255 if ( !settings.image.isNull() )
256 hints[QStringLiteral( "image_data" )] = settings.image;
257
258 QVariantList argumentList;
259 argumentList << "qgis"; //app_name
260 // replace_id
261 if ( settings.messageId.isValid() )
262 argumentList << static_cast< uint >( settings.messageId.toInt() );
263 else
264 argumentList << static_cast< uint >( 0 );
265 // app_icon
266 if ( !settings.svgAppIconPath.isEmpty() )
267 argumentList << settings.svgAppIconPath;
268 else
269 argumentList << "";
270 argumentList << summary; // summary
271 argumentList << body; // body
272 argumentList << QStringList(); // actions
273 argumentList << hints; // hints
274 argumentList << -1; // timeout in ms "If -1, the notification's expiration time is dependent on the notification server's settings, and may vary for the type of notification."
275
276 const QDBusMessage reply = iface.callWithArgumentList( QDBus::AutoDetect, QStringLiteral( "Notify" ), argumentList );
277 if ( reply.type() == QDBusMessage::ErrorMessage )
278 {
279 qDebug() << "D-Bus Error:" << reply.errorMessage();
280 return result;
281 }
282
283 result.successful = true;
284 result.messageId = reply.arguments().value( 0 );
285 return result;
286 }
287