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