swift
joystickwindows.cpp
1 // SPDX-FileCopyrightText: Copyright (C) 2014 swift Project Community / Contributors
2 // SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-swift-pilot-client-1
3 
4 #include "joystickwindows.h"
5 
6 #include "Dbt.h"
7 #include "comdef.h"
8 
9 #include "misc/logmessage.h"
10 #include "misc/metadatautils.h"
11 
12 // Qt5 defines UNICODE, hence we can expect an wchar_t strings.
13 // If it fails to compile, because of char/wchar_t errors, you are most likely
14 // using ascii functions of WINAPI. To fix it, introduce #ifdef UNICODE and add char
15 // handling in the second branch.
16 
17 using namespace swift::misc;
18 using namespace swift::misc::input;
19 
20 namespace swift::input
21 {
22  CJoystickDevice::CJoystickDevice(DirectInput8Ptr directInputPtr, const DIDEVICEINSTANCE *pdidInstance,
23  QObject *parent)
24  : QObject(parent), m_guidDevice(pdidInstance->guidInstance), m_guidProduct(pdidInstance->guidProduct),
25  m_deviceName(QString::fromWCharArray(pdidInstance->tszInstanceName).simplified()),
26  m_productName(QString::fromWCharArray(pdidInstance->tszProductName).simplified()),
27  m_directInput(directInputPtr)
28  {
29  this->setObjectName(classNameShort(this));
30  }
31 
32  bool CJoystickDevice::init(HWND helperWindow)
33  {
34  HRESULT hr;
35  // Create device
36  {
37  IDirectInputDevice8 *diDevice = nullptr;
38  if (FAILED(hr = m_directInput->CreateDevice(m_guidDevice, &diDevice, nullptr)))
39  {
40  CLogMessage(this).warning(u"IDirectInput8::CreateDevice failed: ") << hr;
41  return false;
42  }
43  m_directInputDevice.reset(diDevice);
44  }
45 
46  // Set cooperative level
47  if (!helperWindow) { return false; }
48  if (FAILED(hr = m_directInputDevice->SetCooperativeLevel(helperWindow, DISCL_NONEXCLUSIVE | DISCL_BACKGROUND)))
49  {
50  CLogMessage(this).warning(u"IDirectInputDevice8::SetCooperativeLevel failed: ") << hr;
51  return false;
52  }
53 
54  // Set data format to c_dfDIJoystick2
55  if (FAILED(hr = m_directInputDevice->SetDataFormat(&c_dfDIJoystick2)))
56  {
57  CLogMessage(this).warning(u"IDirectInputDevice8::SetDataFormat failed: ") << hr;
58  return false;
59  }
60 
61  DIDEVCAPS deviceCaps;
62  deviceCaps.dwSize = sizeof(DIDEVCAPS);
63  // Get device capabilities - we are interested in the number of buttons.
64  if (FAILED(hr = m_directInputDevice->GetCapabilities(&deviceCaps)))
65  {
66  CLogMessage(this).warning(u"IDirectInputDevice8::GetCapabilities failed: ") << hr;
67  return false;
68  }
69 
70  // Filter devices with 0 buttons
71  if (deviceCaps.dwButtons == 0) { return false; }
72 
73  // fix for the toggle button issue T585
74  // if (FAILED(hr = m_directInputDevice->EnumObjects(enumObjectsCallback, this, DIDFT_BUTTON)))
75  if (FAILED(hr = m_directInputDevice->EnumObjects(enumObjectsCallback, this, DIDFT_PSHBUTTON)))
76  {
77  CLogMessage(this).warning(u"IDirectInputDevice8::EnumObjects failed: ") << hr;
78  return false;
79  }
80 
81  CLogMessage(this).info(u"Created joystick device '%1' with %2 buttons") << m_deviceName << deviceCaps.dwButtons;
82  this->startTimer(50);
83  return true;
84  }
85 
87  {
88  CJoystickButtonList buttons;
89  for (const CJoystickDeviceInput &deviceInput : m_joystickDeviceInputs)
90  {
91  buttons.push_back(deviceInput.m_button);
92  }
93  return buttons;
94  }
95 
96  void CJoystickDevice::timerEvent(QTimerEvent *event)
97  {
98  Q_UNUSED(event)
99  pollDeviceState();
100  }
101 
102  HRESULT CJoystickDevice::pollDeviceState()
103  {
104  m_directInputDevice->Poll();
105 
106  DIJOYSTATE2 state;
107  HRESULT hr = m_directInputDevice->GetDeviceState(sizeof(DIJOYSTATE2), &state);
108  if (hr == DIERR_INPUTLOST || hr == DIERR_NOTACQUIRED)
109  {
110  m_directInputDevice->Acquire();
111  m_directInputDevice->Poll();
112  hr = m_directInputDevice->GetDeviceState(sizeof(DIJOYSTATE2), &state);
113  }
114 
115  if (FAILED(hr))
116  {
117  CLogMessage(this).warning(u"Cannot acquire and poll joystick device %1. Removing it.") << m_deviceName;
118  emit connectionLost(m_guidDevice);
119  return hr;
120  }
121 
122  for (const CJoystickDeviceInput &input : std::as_const(m_joystickDeviceInputs))
123  {
124  const qint32 buttonIndex = input.m_offset - DIJOFS_BUTTON0;
125  const bool isPressed = state.rgbButtons[buttonIndex] & 0x80;
126  emit this->buttonChanged(input.m_button, isPressed);
127  }
128  return hr;
129  }
130 
131  QString CJoystickDevice::hrString(HRESULT hr)
132  {
133  // https://stackoverflow.com/questions/7008047/is-there-a-way-to-get-the-string-representation-of-hresult-value-using-win-api
134  _com_error err(hr);
135  LPCTSTR errMsg = err.ErrorMessage();
136  return QString::fromWCharArray(errMsg);
137  }
138 
139  BOOL CALLBACK CJoystickDevice::enumObjectsCallback(const DIDEVICEOBJECTINSTANCE *dev, LPVOID pvRef)
140  {
141  CJoystickDevice *joystickDevice = static_cast<CJoystickDevice *>(pvRef);
142 
143  // Make sure we only got GUID_Button types
144  if (dev->guidType != GUID_Button) return DIENUM_CONTINUE;
145 
146  CJoystickDeviceInput deviceInput;
147  const int number = joystickDevice->m_joystickDeviceInputs.size();
148  deviceInput.m_offset = DIJOFS_BUTTON(number);
149  deviceInput.m_button = CJoystickButton(joystickDevice->m_deviceName, DIJOFS_BUTTON(number) - DIJOFS_BUTTON0);
150 
151  joystickDevice->m_joystickDeviceInputs.append(deviceInput);
152  CLogMessage(static_cast<CJoystickWindows *>(nullptr)).debug()
153  << "Found joystick button" << QString::fromWCharArray(dev->tszName) << joystickDevice->m_deviceName;
154 
155  return DIENUM_CONTINUE;
156  }
157 
158  CJoystickWindows::CJoystickWindows(QObject *parent) : IJoystick(parent)
159  {
160  // Initialize COM.
161  // https://docs.microsoft.com/en-us/windows/desktop/api/combaseapi/nf-combaseapi-coinitializeex
162  HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
163 
164  // RPC_E_CHANGED_MODE: CoInitializeEx was already called by someone else in this thread with a different mode.
165  if (hr == RPC_E_CHANGED_MODE)
166  {
167  CLogMessage(this).debug(u"CoInitializeEx was already called with a different mode. Trying again.");
168  hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
169  }
170 
171  // Continue here only if CoInitializeEx was successful
172  // S_OK: The COM library was initialized successfully on this thread.
173  // S_FALSE: The COM library is already initialized on this thread. Reference count was incremented. This is not
174  // an error.
175  if (hr == S_OK || hr == S_FALSE)
176  {
177  m_coInitializeSucceeded = true;
178  this->createHelperWindow();
179 
180  if (helperWindow)
181  {
182  this->initDirectInput();
183  this->enumJoystickDevices();
184  this->requestDeviceNotification();
185  }
186  }
187  else { CLogMessage(this).warning(u"CoInitializeEx returned error code %1"); }
188  }
189 
191  {
192  // All DirectInput devices need to be cleaned up before the call to CoUninitialize()
193  for (CJoystickDevice *joystickDevice : std::as_const(m_joystickDevices)) { delete joystickDevice; }
194  m_joystickDevices.clear();
195  m_directInput.reset();
196  if (m_coInitializeSucceeded) { CoUninitialize(); }
197  if (hDevNotify) { UnregisterDeviceNotification(hDevNotify); }
198  destroyHelperWindow();
199  }
200 
202  {
203  CJoystickButtonList availableButtons;
204  for (const CJoystickDevice *device : std::as_const(m_joystickDevices))
205  {
206  availableButtons.push_back(device->getDeviceButtons());
207  }
208  return availableButtons;
209  }
210 
211  void ReleaseDirectInput(IDirectInput8 *obj)
212  {
213  if (obj) { obj->Release(); }
214  }
215 
216  HRESULT CJoystickWindows::initDirectInput()
217  {
218  IDirectInput8 *directInput = nullptr;
219  // HRESULT hr = DirectInput8Create(GetModuleHandle(nullptr), DIRECTINPUT_VERSION, IID_IDirectInput8,
220  // reinterpret_cast<LPVOID *>(&directInput), nullptr);
221  HRESULT hr = CoCreateInstance(CLSID_DirectInput8, nullptr, CLSCTX_INPROC_SERVER, IID_IDirectInput8,
222  reinterpret_cast<LPVOID *>(&directInput));
223  if (FAILED(hr)) { return hr; }
224  m_directInput = DirectInput8Ptr(directInput, ReleaseDirectInput);
225 
226  HINSTANCE instance = GetModuleHandle(nullptr);
227  hr = m_directInput->Initialize(instance, DIRECTINPUT_VERSION);
228  return hr;
229  }
230 
231  HRESULT CJoystickWindows::enumJoystickDevices()
232  {
233  if (!m_directInput)
234  {
235  CLogMessage(this).warning(u"No direct input");
236  return E_FAIL;
237  }
238 
239  HRESULT hr;
240  if (FAILED(hr = m_directInput->EnumDevices(DI8DEVCLASS_GAMECTRL, enumJoysticksCallback, this,
241  DIEDFL_ATTACHEDONLY)))
242  {
243  CLogMessage(this).error(u"Error reading joystick devices");
244  return hr;
245  }
246 
247  if (m_joystickDevices.isEmpty()) { CLogMessage(this).info(u"No joystick device found"); }
248  return hr;
249  }
250 
251  int CJoystickWindows::createHelperWindow()
252  {
253  // Make sure window isn't created twice
254  if (helperWindow != nullptr) { return 0; }
255 
256  HINSTANCE hInstance = GetModuleHandle(nullptr);
257  WNDCLASSEX wce;
258  ZeroMemory(&wce, sizeof(wce));
259  wce.cbSize = sizeof(wce);
260  wce.lpfnWndProc = windowProc;
261  wce.lpszClassName = static_cast<LPCWSTR>(helperWindowClassName);
262  wce.hInstance = hInstance;
263 
264  /* Register the class. */
265  if (!RegisterClassEx(&wce)) { return -1; }
266 
267  /* Create the window. */
268  helperWindow =
269  CreateWindowEx(0, helperWindowClassName, helperWindowName, WS_OVERLAPPED, CW_USEDEFAULT, CW_USEDEFAULT,
270  CW_USEDEFAULT, CW_USEDEFAULT, HWND_MESSAGE, nullptr, hInstance, nullptr);
271  if (helperWindow == nullptr)
272  {
273  UnregisterClass(helperWindowClassName, hInstance);
274  return -1;
275  }
276 
277  SetProp(helperWindow, L"CJoystickWindows", this);
278 
279  return 0;
280  }
281 
282  void CJoystickWindows::requestDeviceNotification()
283  {
284  DEV_BROADCAST_DEVICEINTERFACE notificationFilter;
285  ZeroMemory(&notificationFilter, sizeof(notificationFilter));
286  notificationFilter.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE);
287  notificationFilter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
288  hDevNotify = RegisterDeviceNotification(helperWindow, &notificationFilter,
289  DEVICE_NOTIFY_WINDOW_HANDLE | DEVICE_NOTIFY_ALL_INTERFACE_CLASSES);
290  }
291 
292  void CJoystickWindows::destroyHelperWindow()
293  {
294  HINSTANCE hInstance = GetModuleHandle(nullptr);
295 
296  if (helperWindow == nullptr) { return; }
297 
298  DestroyWindow(helperWindow);
299  helperWindow = nullptr;
300 
301  UnregisterClass(helperWindowClassName, hInstance);
302  }
303 
304  void CJoystickWindows::addJoystickDevice(const DIDEVICEINSTANCE *pdidInstance)
305  {
306  CJoystickDevice *device = new CJoystickDevice(m_directInput, pdidInstance, this);
307  bool success = device->init(helperWindow);
308  if (success)
309  {
310  connect(device, &CJoystickDevice::buttonChanged, this, &CJoystickWindows::joystickButtonChanged);
311  connect(device, &CJoystickDevice::connectionLost, this, &CJoystickWindows::removeJoystickDevice);
312  m_joystickDevices.push_back(device);
313  }
314  else { delete device; }
315  }
316 
317  bool CJoystickWindows::isJoystickAlreadyAdded(const DIDEVICEINSTANCE *pdidInstance) const
318  {
319  for (const CJoystickDevice *device : m_joystickDevices)
320  {
321  if (IsEqualGUID(device->getDeviceGuid(), pdidInstance->guidInstance)) { return true; }
322  }
323 
324  return false;
325  }
326 
327  void CJoystickWindows::joystickButtonChanged(const CJoystickButton &joystickButton, bool isPressed)
328  {
329  CHotkeyCombination oldCombination(m_buttonCombination);
330  if (isPressed) { m_buttonCombination.addJoystickButton(joystickButton); }
331  else { m_buttonCombination.removeJoystickButton(joystickButton); }
332 
333  if (oldCombination != m_buttonCombination) { emit buttonCombinationChanged(m_buttonCombination); }
334  }
335 
336  void CJoystickWindows::removeJoystickDevice(const GUID &guid)
337  {
338  for (auto it = m_joystickDevices.begin(); it != m_joystickDevices.end(); ++it)
339  {
340  CJoystickDevice *device = *it;
341  if (IsEqualGUID(guid, device->getDeviceGuid()))
342  {
343  device->deleteLater();
344  m_joystickDevices.erase(it);
345  break;
346  }
347  }
348  }
349 
350  //
351  // Window callback function (handles window messages)
352  //
353  LRESULT CALLBACK CJoystickWindows::windowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
354  {
355  CJoystickWindows *joystickWindows = static_cast<CJoystickWindows *>(GetProp(hWnd, L"CJoystickWindows"));
356 
357  if (joystickWindows)
358  {
359  switch (uMsg)
360  {
361  case WM_DEVICECHANGE:
362  {
363  if (wParam == DBT_DEVICEARRIVAL)
364  {
365  DEV_BROADCAST_HDR *dbh = reinterpret_cast<DEV_BROADCAST_HDR *>(lParam);
366  // DEV_BROADCAST_HDR *dbh = (DEV_BROADCAST_HDR *) lParam;
367  if (dbh && dbh->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE)
368  {
369  joystickWindows->enumJoystickDevices();
370  }
371  }
372  }
373  }
374  }
375 
376  return DefWindowProc(hWnd, uMsg, wParam, lParam);
377  }
378 
379  BOOL CALLBACK CJoystickWindows::enumJoysticksCallback(const DIDEVICEINSTANCE *pdidInstance, VOID *pContext)
380  {
381  CJoystickWindows *obj = static_cast<CJoystickWindows *>(pContext);
382 
383  /* ignore XInput devices here, keep going. */
384  // if (isXInputDevice(&pdidInstance->guidProduct)) return DIENUM_CONTINUE;
385 
386  if (!obj->isJoystickAlreadyAdded(pdidInstance))
387  {
388  obj->addJoystickDevice(pdidInstance);
389  CLogMessage(static_cast<CJoystickWindows *>(nullptr)).debug()
390  << "Found joystick device" << QString::fromWCharArray(pdidInstance->tszInstanceName);
391  }
392  return DIENUM_CONTINUE;
393  }
394 
395  bool operator==(const CJoystickDevice &lhs, const CJoystickDevice &rhs)
396  {
397  return lhs.m_guidDevice == rhs.m_guidDevice && lhs.m_guidProduct == rhs.m_guidProduct &&
398  lhs.m_deviceName == rhs.m_deviceName && lhs.m_productName == rhs.m_productName;
399  }
400 
401  bool operator==(CJoystickDeviceInput const &lhs, CJoystickDeviceInput const &rhs)
402  {
403  return lhs.m_offset == rhs.m_offset && lhs.m_button == rhs.m_button;
404  }
405 } // namespace swift::input
Linux Joystick device.
Definition: joysticklinux.h:24
bool init(const IOHIDDeviceRef device)
Initialize device.
void connectionLost(const GUID &guid)
Connection to joystick lost. Probably unplugged.
void buttonChanged(const QString &name, int index, bool isPressed)
Joystick button changed.
virtual void timerEvent(QTimerEvent *event)
Timer based updates.
swift::misc::input::CJoystickButtonList getDeviceButtons() const
Get all available device buttons.
CJoystickDevice(const QString &path, QFile *fd, QObject *parent)
Constructor.
virtual ~CJoystickWindows()
Destructor.
virtual swift::misc::input::CJoystickButtonList getAllAvailableJoystickButtons() const
Get all available joystick buttons.
CJoystickWindows(CJoystickWindows const &)=delete
Copy Constructor.
void buttonCombinationChanged(const swift::misc::input::CHotkeyCombination &)
Joystick button combination has changed.
Class for emitting a log message.
Definition: logmessage.h:27
Derived & warning(const char16_t(&format)[N])
Set the severity to warning, providing a format string.
Derived & error(const char16_t(&format)[N])
Set the severity to error, providing a format string.
Derived & debug()
Set the severity to debug.
Derived & info(const char16_t(&format)[N])
Set the severity to info, providing a format string.
void push_back(const T &value)
Appends an element at the end of the sequence.
Definition: sequence.h:305
Value object representing hotkey sequence.
void removeJoystickButton(CJoystickButton button)
Remove joystick button.
void addJoystickButton(const CJoystickButton &button)
Add joystick button.
Value object representing a joystick button.
Value object encapsulating a list of joystick buttons.
std::shared_ptr< IDirectInput8 > DirectInput8Ptr
Shared IDirectInput8 ptr.
Free functions in swift::misc.
QString classNameShort(const QObject *object)
Class name as from QMetaObject::className without namespace.
Joystick device input/button.
swift::misc::input::CJoystickButton m_button
Joystick button.