1 /***************************************************************************
2     qgswinnative.cpp - abstracted interface to native system calls
3                              -------------------
4     begin                : January 2017
5     copyright            : (C) 2017 by Matthias Kuhn
6     email                : matthias@opengis.ch
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 "qgswinnative.h"
19 #include <QCoreApplication>
20 #include <QDebug>
21 #include <QString>
22 #include <QDir>
23 #include <QWindow>
24 #include <QProcess>
25 #include <QAbstractEventDispatcher>
26 #include <QtWinExtras/QWinTaskbarButton>
27 #include <QtWinExtras/QWinTaskbarProgress>
28 #include <QtWinExtras/QWinJumpList>
29 #include <QtWinExtras/QWinJumpListItem>
30 #include <QtWinExtras/QWinJumpListCategory>
31 #include "wintoastlib.h"
32 #include <Dbt.h>
33 #include <memory>
34 
35 
36 struct ITEMIDLISTDeleter
37 {
operator ()ITEMIDLISTDeleter38   void operator()( ITEMIDLIST *pidl )
39   {
40     ILFree( pidl );
41   }
42 };
43 
44 using ITEMIDLIST_unique_ptr = std::unique_ptr< ITEMIDLIST, ITEMIDLISTDeleter>;
45 
46 
47 
capabilities() const48 QgsNative::Capabilities QgsWinNative::capabilities() const
49 {
50   return mCapabilities;
51 }
52 
initializeMainWindow(QWindow * window,const QString & applicationName,const QString & organizationName,const QString & version)53 void QgsWinNative::initializeMainWindow( QWindow *window,
54     const QString &applicationName,
55     const QString &organizationName,
56     const QString &version )
57 {
58   mWindow = window;
59   if ( mTaskButton )
60     return; // already initialized!
61 
62   mTaskButton = new QWinTaskbarButton( window );
63   mTaskButton->setWindow( window );
64   mTaskProgress = mTaskButton->progress();
65   mTaskProgress->setVisible( false );
66 
67   QString appName = qgetenv( "QGIS_WIN_APP_NAME" );
68   if ( appName.isEmpty() )
69     appName = applicationName;
70 
71   WinToastLib::WinToast::instance()->setAppName( appName.toStdWString() );
72   WinToastLib::WinToast::instance()->setAppUserModelId(
73     WinToastLib::WinToast::configureAUMI( organizationName.toStdWString(),
74                                           applicationName.toStdWString(),
75                                           applicationName.toStdWString(),
76                                           version.toStdWString() ) );
77   if ( WinToastLib::WinToast::instance()->initialize() )
78   {
79     mWinToastInitialized = true;
80     mCapabilities = mCapabilities | NativeDesktopNotifications;
81   }
82 
83   mNativeEventFilter = new QgsWinNativeEventFilter();
84   QAbstractEventDispatcher::instance()->installNativeEventFilter( mNativeEventFilter );
85   connect( mNativeEventFilter, &QgsWinNativeEventFilter::usbStorageNotification, this, &QgsNative::usbStorageNotification );
86 }
87 
cleanup()88 void QgsWinNative::cleanup()
89 {
90   if ( mWinToastInitialized )
91     WinToastLib::WinToast::instance()->clear();
92   mWindow = nullptr;
93 }
94 
pathToWChar(const QString & path)95 std::unique_ptr< wchar_t[] > pathToWChar( const QString &path )
96 {
97   const QString nativePath = QDir::toNativeSeparators( path );
98 
99   std::unique_ptr< wchar_t[] > pathArray( new wchar_t[static_cast< uint>( nativePath.length() + 1 )] );
100   nativePath.toWCharArray( pathArray.get() );
101   pathArray[static_cast< size_t >( nativePath.length() )] = 0;
102   return pathArray;
103 }
104 
openFileExplorerAndSelectFile(const QString & path)105 void QgsWinNative::openFileExplorerAndSelectFile( const QString &path )
106 {
107   std::unique_ptr< wchar_t[] > pathArray = pathToWChar( path );
108   ITEMIDLIST_unique_ptr pidl( ILCreateFromPathW( pathArray.get() ) );
109   if ( pidl )
110   {
111     SHOpenFolderAndSelectItems( pidl.get(), 0, nullptr, 0 );
112     pidl.reset();
113   }
114 }
115 
showFileProperties(const QString & path)116 void QgsWinNative::showFileProperties( const QString &path )
117 {
118   std::unique_ptr< wchar_t[] > pathArray = pathToWChar( path );
119   ITEMIDLIST_unique_ptr pidl( ILCreateFromPathW( pathArray.get() ) );
120   if ( pidl )
121   {
122     SHELLEXECUTEINFO info{ sizeof( info ) };
123     if ( mWindow )
124       info.hwnd = reinterpret_cast<HWND>( mWindow->winId() );
125     info.nShow = SW_SHOWNORMAL;
126     info.fMask = SEE_MASK_INVOKEIDLIST;
127     info.lpIDList = pidl.get();
128     info.lpVerb = "properties";
129 
130     ShellExecuteEx( &info );
131   }
132 }
133 
showUndefinedApplicationProgress()134 void QgsWinNative::showUndefinedApplicationProgress()
135 {
136   mTaskProgress->setMaximum( 0 );
137   mTaskProgress->show();
138 }
139 
setApplicationProgress(double progress)140 void QgsWinNative::setApplicationProgress( double progress )
141 {
142   mTaskProgress->setMaximum( 100 );
143   mTaskProgress->show();
144   mTaskProgress->setValue( static_cast< int >( std::round( progress ) ) );
145 }
146 
hideApplicationProgress()147 void QgsWinNative::hideApplicationProgress()
148 {
149   mTaskProgress->hide();
150 }
151 
onRecentProjectsChanged(const std::vector<QgsNative::RecentProjectProperties> & recentProjects)152 void QgsWinNative::onRecentProjectsChanged( const std::vector<QgsNative::RecentProjectProperties> &recentProjects )
153 {
154   QWinJumpList jumplist;
155   jumplist.recent()->clear();
156   for ( const RecentProjectProperties &recentProject : recentProjects )
157   {
158     QString name = recentProject.title != recentProject.path ? recentProject.title : QFileInfo( recentProject.path ).baseName();
159     QWinJumpListItem *newProject = new QWinJumpListItem( QWinJumpListItem::Link );
160     newProject->setTitle( name );
161     newProject->setFilePath( QDir::toNativeSeparators( QCoreApplication::applicationFilePath() ) );
162     newProject->setArguments( QStringList( recentProject.path ) );
163     jumplist.recent()->addItem( newProject );
164   }
165 }
166 
167 class NotificationHandler : public WinToastLib::IWinToastHandler
168 {
169   public:
170 
toastActivated() const171     void toastActivated() const override {}
172 
toastActivated(int) const173     void toastActivated( int ) const override {}
174 
toastFailed() const175     void toastFailed() const override
176     {
177       qWarning() << "Error showing notification";
178     }
179 
toastDismissed(WinToastDismissalReason) const180     void toastDismissed( WinToastDismissalReason ) const override {}
181 };
182 
183 
showDesktopNotification(const QString & summary,const QString & body,const QgsNative::NotificationSettings & settings)184 QgsNative::NotificationResult QgsWinNative::showDesktopNotification( const QString &summary, const QString &body, const QgsNative::NotificationSettings &settings )
185 {
186   NotificationResult result;
187   if ( !mWinToastInitialized )
188   {
189     result.successful = false;
190     return result;
191   }
192 
193   WinToastLib::WinToastTemplate templ = WinToastLib::WinToastTemplate( WinToastLib::WinToastTemplate::ImageAndText02 );
194   templ.setImagePath( settings.pngAppIconPath.toStdWString() );
195   templ.setTextField( summary.toStdWString(), WinToastLib::WinToastTemplate::FirstLine );
196   templ.setTextField( body.toStdWString(), WinToastLib::WinToastTemplate::SecondLine );
197   templ.setDuration( WinToastLib::WinToastTemplate::Short );
198 
199 
200   if ( WinToastLib::WinToast::instance()->showToast( templ, new NotificationHandler ) < 0 )
201     result.successful = false;
202   else
203     result.successful = true;
204 
205   return result;
206 }
207 
openTerminalAtPath(const QString & path)208 bool QgsWinNative::openTerminalAtPath( const QString &path )
209 {
210   // logic from https://github.com/Microsoft/vscode/blob/fec1775aa52e2124d3f09c7b2ac8f69c57309549/src/vs/workbench/parts/execution/electron-browser/terminal.ts#L44
211   const bool isWow64 = qEnvironmentVariableIsSet( "PROCESSOR_ARCHITEW6432" );
212   QString windir = qgetenv( "WINDIR" );
213   if ( windir.isEmpty() )
214     windir = QStringLiteral( "C:\\Windows" );
215   const QString term = QStringLiteral( "%1\\%2\\cmd.exe" ).arg( windir, isWow64 ? QStringLiteral( "Sysnative" ) : QStringLiteral( "System32" ) );
216 
217   QProcess process;
218   process.setProgram( term );
219   process.setCreateProcessArgumentsModifier( []( QProcess::CreateProcessArguments * args )
220   {
221     args->flags |= CREATE_NEW_CONSOLE;
222     args->startupInfo->dwFlags &= ~ STARTF_USESTDHANDLES;
223   } );
224   process.setWorkingDirectory( path );
225 
226   qint64 pid;
227   return process.startDetached( &pid );
228 }
229 
nativeEventFilter(const QByteArray & eventType,void * message,long *)230 bool QgsWinNativeEventFilter::nativeEventFilter( const QByteArray &eventType, void *message, long * )
231 {
232   static const QByteArray sWindowsGenericMSG{ "windows_generic_MSG" };
233   if ( !message || eventType != sWindowsGenericMSG )
234     return false;
235 
236   MSG *pWindowsMessage = static_cast<MSG *>( message );
237   if ( pWindowsMessage->message != WM_DEVICECHANGE )
238   {
239     return false;
240   }
241 
242   unsigned int wParam = pWindowsMessage->wParam;
243   if ( wParam == DBT_DEVICEARRIVAL || wParam == DBT_DEVICEREMOVECOMPLETE )
244   {
245     if ( !pWindowsMessage->lParam )
246       return false;
247 
248     unsigned long deviceType = reinterpret_cast<DEV_BROADCAST_HDR *>( pWindowsMessage->lParam )->dbch_devicetype;
249     if ( deviceType == DBT_DEVTYP_VOLUME )
250     {
251       const DEV_BROADCAST_VOLUME *broadcastVolume = reinterpret_cast<const DEV_BROADCAST_VOLUME *>( pWindowsMessage->lParam );
252       if ( !broadcastVolume )
253         return false;
254 
255       // Seen in qfilesystemwatcher_win.cpp from Qt:
256       // WM_DEVICECHANGE/DBT_DEVTYP_VOLUME messages are sent to all toplevel windows. Compare a hash value to ensure
257       // it is handled only once.
258       const quintptr newHash = reinterpret_cast<quintptr>( broadcastVolume ) + pWindowsMessage->wParam
259                                + quintptr( broadcastVolume->dbcv_flags ) + quintptr( broadcastVolume->dbcv_unitmask );
260       if ( newHash == mLastMessageHash )
261         return false;
262       mLastMessageHash = newHash;
263 
264       // need to handle disks with multiple partitions -- these are given by a single event
265       unsigned long unitmask = broadcastVolume->dbcv_unitmask;
266       std::vector< QString > drives;
267       char driveName[] = "A:/";
268       unitmask &= 0x3ffffff;
269       while ( unitmask )
270       {
271         if ( unitmask & 0x1 )
272           drives.emplace_back( QString::fromLatin1( driveName ) );
273         ++driveName[0];
274         unitmask >>= 1;
275       }
276 
277       for ( const QString &drive : drives )
278       {
279         emit usbStorageNotification( QStringLiteral( "%1:/" ).arg( drive ), wParam == DBT_DEVICEARRIVAL );
280       }
281       return false;
282     }
283   }
284   return false;
285 }
286