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