ckb-next  v0.2.8 at branch master
ckb-next driver for corsair devices
 All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Pages
mainwindow.cpp
Go to the documentation of this file.
1 #include "ckbsettings.h"
2 #include "kbmanager.h"
3 #include "kbfirmware.h"
4 #include "mainwindow.h"
5 #include "ui_mainwindow.h"
6 #include <cstdlib>
7 #include <QFileInfo>
8 #include <QSharedMemory>
9 #include <QShortcut>
10 #include <QMessageBox>
11 #include <QMenuBar>
12 #include <unistd.h>
13 
14 extern QSharedMemory appShare;
15 extern QString devpath;
16 
17 static const QString configLabel = "Settings";
18 
19 #ifndef Q_OS_MACX
20 QString daemonDialogText = QObject::tr("Start it once with:") +
21  "<blockquote><code>sudo systemctl start ckb-daemon</code></blockquote>" +
22  QObject::tr("Enable it for every boot:") +
23  "<blockquote><code>sudo systemctl enable ckb-daemon</code></blockquote>";
24 #else
25 QString daemonDialogText = QObject::tr("Start it once with:") +
26  "<blockquote><code>sudo launchctl start com.ckb.daemon.plist</code></blockquote>" +
27  QObject::tr("Enable it for every boot:") +
28  "<blockquote><code>sudo launchctl enable /Library/LaunchDaemons/com.ckb.daemon.plist</code></blockquote>";
29 #endif
30 
32 
33 #ifdef USE_LIBAPPINDICATOR
34 extern "C" {
35  void quitIndicator(GtkMenu* menu, gpointer data) {
36  Q_UNUSED(menu);
37  MainWindow* window = static_cast<MainWindow*>(data);
38  window->quitApp();
39  }
40 
41  void restoreIndicator(GtkMenu* menu, gpointer data) {
42  Q_UNUSED(menu);
43  MainWindow* window = static_cast<MainWindow*>(data);
44  window->showWindow();
45  }
46 }
47 #endif // USE_LIBAPPINDICATOR
48 
50  QMainWindow(parent),
51  ui(new Ui::MainWindow)
52 {
53  ui->setupUi(this);
54  mainWindow = this;
55 
56  // Start device manager
57  KbManager::init(CKB_VERSION_STR);
58  connect(KbManager::kbManager(), SIGNAL(kbConnected(Kb*)), this, SLOT(addDevice(Kb*)));
59  connect(KbManager::kbManager(), SIGNAL(kbDisconnected(Kb*)), this, SLOT(removeDevice(Kb*)));
60  connect(KbManager::kbManager(), SIGNAL(versionUpdated()), this, SLOT(updateVersion()));
61  connect(KbManager::scanTimer(), SIGNAL(timeout()), this, SLOT(timerTick()));
62 
63  // Set up tray icon
64  restoreAction = new QAction(tr("Restore"), this);
65  closeAction = new QAction(tr("Quit ckb"), this);
66 #ifdef USE_LIBAPPINDICATOR
67  QString desktop = std::getenv("XDG_CURRENT_DESKTOP");
68  unityDesktop = (desktop.toLower() == "unity");
69 
70  if(unityDesktop){
71  trayIcon = 0;
72 
73  indicatorMenu = gtk_menu_new();
74  indicatorMenuRestoreItem = gtk_menu_item_new_with_label("Restore");
75  indicatorMenuQuitItem = gtk_menu_item_new_with_label("Quit ckb");
76 
77  gtk_menu_shell_append(GTK_MENU_SHELL(indicatorMenu), indicatorMenuRestoreItem);
78  gtk_menu_shell_append(GTK_MENU_SHELL(indicatorMenu), indicatorMenuQuitItem);
79 
80  g_signal_connect(indicatorMenuQuitItem, "activate",
81  G_CALLBACK(quitIndicator), this);
82  g_signal_connect(indicatorMenuRestoreItem, "activate",
83  G_CALLBACK(restoreIndicator), this);
84 
85  gtk_widget_show(indicatorMenuRestoreItem);
86  gtk_widget_show(indicatorMenuQuitItem);
87 
88  indicator = app_indicator_new("ckb", "indicator-messages", APP_INDICATOR_CATEGORY_APPLICATION_STATUS);
89 
90  app_indicator_set_status(indicator, APP_INDICATOR_STATUS_ACTIVE);
91  app_indicator_set_menu(indicator, GTK_MENU(indicatorMenu));
92  app_indicator_set_icon(indicator, "ckb");
93  } else
94 #endif // USE_LIBAPPINDICATOR
95  {
96  trayIconMenu = new QMenu(this);
97  trayIconMenu->addAction(restoreAction);
98  trayIconMenu->addAction(closeAction);
99  trayIcon = new QSystemTrayIcon(QIcon(":/img/ckb-logo.png"), this);
100  trayIcon->setContextMenu(trayIconMenu);
101  connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, SLOT(iconClicked(QSystemTrayIcon::ActivationReason)));
102  }
103  toggleTrayIcon(!CkbSettings::get("Program/SuppressTrayIcon").toBool());
104 
105 #ifdef Q_OS_MACX
106  // Make a custom "Close" menu action for OSX, as the default one brings up the "still running" popup unnecessarily
107  QMenuBar* menuBar = new QMenuBar(this);
108  setMenuBar(menuBar);
109  this->menuBar()->addMenu("ckb")->addAction(closeAction);
110 #else
111  // On linux, add a handler for Ctrl+Q
112  new QShortcut(QKeySequence("Ctrl+Q"), this, SLOT(quitApp()));
113 #endif
114 
115  connect(ui->actionExit, SIGNAL(triggered()), this, SLOT(quitApp()));
116  connect(closeAction, SIGNAL(triggered()), this, SLOT(quitApp()));
117  connect(restoreAction, SIGNAL(triggered()), this, SLOT(showWindow()));
118  connect(qApp, SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(stateChange(Qt::ApplicationState)));
119 
120  connect(qApp, SIGNAL(aboutToQuit()), this, SLOT(cleanup()));
121 
122  ui->tabWidget->addTab(settingsWidget = new SettingsWidget(this), configLabel);
123  settingsWidget->setVersion("ckb-next " CKB_VERSION_STR);
124 
125  // create daemon dialog as a QMessageBox
126  // this will create a focussed dialog, that has to be interacted with,
127  // if the daemon is not running
128  // set the main and informative text to tell the user about the issue
129  QMessageBox dialog;
130  dialog.setText(tr("The ckb-next daemon is not running. This program will <b>not</b> work without it!"));
131  dialog.setInformativeText(daemonDialogText);
132  dialog.setIcon(QMessageBox::Critical);
133 
134  // check, whether daemon is running
135  // the daemon creates the root device path on initialization and thus it
136  // can be assumed, that the daemon is not running if doesn't exist
137  // `.arg(0)` is necessary to interpolate the correct suffix into the path
138  // see `./kbmanager.cpp` for details
139  QFileInfo rootDevPath(devpath.arg(0));
140  if (!rootDevPath.exists()) {
141  // set settings widget's status
142  // show the main window (otherwise only the dialog will be visible)
143  // finally show the dialog
144  settingsWidget->setStatus(tr("The ckb-next daemon is not running."));
145  showWindow();
146  dialog.exec();
147  }
148 }
149 
150 void MainWindow::toggleTrayIcon(bool visible) {
151 #ifdef USE_LIBAPPINDICATOR
152  if(unityDesktop)
153  app_indicator_set_status(indicator, visible ? APP_INDICATOR_STATUS_ACTIVE : APP_INDICATOR_STATUS_PASSIVE);
154  else
155 #endif // USE_LIBAPPINDICATOR
156  trayIcon->setVisible(visible);
157 }
158 
159 void MainWindow::addDevice(Kb* device){
160  // Connected already?
161  foreach(KbWidget* w, kbWidgets){
162  if(w->device == device)
163  return;
164  }
165  // Add the keyboard
166  KbWidget* widget = new KbWidget(this, device);
167  kbWidgets.append(widget);
168  // Add to tabber; switch to this device if the user is on the settings screen
169  int count = ui->tabWidget->count();
170  ui->tabWidget->insertTab(count - 1, widget, widget->name());
171  if(ui->tabWidget->currentIndex() == count)
172  ui->tabWidget->setCurrentIndex(count - 1);
173  // Update connected device count
174  updateVersion();
175 }
176 
178  foreach(KbWidget* w, kbWidgets){
179  // Remove this device from the UI
180  if(w->device == device){
181  int i = kbWidgets.indexOf(w);
182  ui->tabWidget->removeTab(i);
183  kbWidgets.removeAt(i);
184  w->deleteLater();
185  }
186  }
187  // Update connected device count
188  updateVersion();
189 }
190 
192  QString daemonVersion = KbManager::ckbDaemonVersion();
193  if(daemonVersion == DAEMON_UNAVAILABLE_STR){
194  settingsWidget->setStatus("Driver inactive");
195  return;
196  }
197  int count = kbWidgets.count();
198  // Warn if the daemon version doesn't match the GUI
199  QString daemonWarning;
200  if(daemonVersion != CKB_VERSION_STR)
201  daemonWarning = "<br /><br /><b>Warning:</b> Driver version mismatch (" + daemonVersion + "). Please upgrade ckb" + QString(KbManager::ckbDaemonVersionF() > KbManager::ckbGuiVersionF() ? "" : "-daemon") + ". If the problem persists, try rebooting.";
202  if(count == 0)
203  settingsWidget->setStatus("No devices connected" + daemonWarning);
204  else if(count == 1)
205  settingsWidget->setStatus("1 device connected" + daemonWarning);
206  else
207  settingsWidget->setStatus(QString("%1 devices connected").arg(count) + daemonWarning);
208 }
209 
211  if(!mainWindow->isVisible())
212  return;
213  foreach(KbWidget* w, kbWidgets){
214  // Display firmware upgrade notification if a new version is available
215  float version = KbFirmware::versionForBoard(w->device->features);
216  if(version > w->device->firmware.toFloat()){
217  if(w->hasShownNewFW)
218  continue;
219  w->hasShownNewFW = true;
220  w->updateFwButton();
221  // Don't run this method here because it will lock up the timer and prevent devices from working properly
222  // Use a queued invocation instead
223  metaObject()->invokeMethod(this, "showFwUpdateNotification", Qt::QueuedConnection, Q_ARG(QWidget*, w), Q_ARG(float, version));
224  // Don't display more than one of these at once
225  return;
226  }
227  }
228 }
229 
230 void MainWindow::showFwUpdateNotification(QWidget* widget, float version){
231  static bool isShowing = false;
232  if(isShowing)
233  return;
234  isShowing = true;
235  showWindow();
236  KbWidget* w = (KbWidget*)widget;
237  // Ask for update
238  if(QMessageBox::information(this, "Firmware update", tr("A new firmware is available for your %1 (v%2)\nWould you like to install it now?").arg(w->device->usbModel, QString::number(version, 'f', 2)), QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No), QMessageBox::Yes) == QMessageBox::Yes){
239  // If accepted, switch to the firmware tab and bring up the update window
240  w->showLastTab();
241  ui->tabWidget->setCurrentIndex(kbWidgets.indexOf(w));
242  w->showFwUpdate();
243  }
244  isShowing = false;
245 }
246 
247 void MainWindow::closeEvent(QCloseEvent *event){
248  // If the window is hidden already or the event is non-spontaneous (can happen on OSX when using the Quit menu), accept it and close
249  if(!event->spontaneous() || isHidden()){
250  event->accept();
251  return;
252  }
253  if(!CkbSettings::get("Popups/BGWarning").toBool()){
254  QMessageBox::information(this, "ckb", "ckb will still run in the background.\nTo close it, choose Exit from the tray menu\nor click \"Quit ckb\" on the Settings screen.");
255  CkbSettings::set("Popups/BGWarning", true);
256  }
257  hide();
258  event->ignore();
259 }
260 
262  // Check shared memory for changes
263  if(appShare.lock()){
264  void* data = appShare.data();
265  QStringList commands = QString((const char*)data).split("\n");
266  // Restore PID, remove all other data
267  snprintf((char*)appShare.data(), appShare.size(), "PID %ld", (long)getpid());
268  appShare.unlock();
269  // Parse commands
270  foreach(const QString& line, commands){
271  // Old ckb option line - bring application to foreground
272  if(line == "Open")
273  showWindow();
274  if(line.startsWith("Option ")){
275  // New ckb option line
276  QString option = line.split(" ")[1];
277  if(option == "Open")
278  // Bring to foreground
279  showWindow();
280  else if(option == "Close")
281  // Quit application
282  qApp->quit();
283  }
284  }
285  }
286  // Check for firmware updates (when appropriate)
287  if(!CkbSettings::get("Program/DisableAutoFWCheck").toBool()){
289  checkFwUpdates();
290  }
291  // Poll for setting updates
293 }
294 
295 void MainWindow::iconClicked(QSystemTrayIcon::ActivationReason reason){
296  // On Linux, hide/show the app when the tray icon is clicked
297  // On OSX this just shows the menu
298 #ifndef Q_OS_MACX
299  if(reason == QSystemTrayIcon::DoubleClick || reason == QSystemTrayIcon::Trigger){
300  if(isVisible())
301  hide();
302  else
303  showWindow();
304  }
305 #endif
306 }
307 
309  showNormal();
310  raise();
311  activateWindow();
312 }
313 
314 void MainWindow::stateChange(Qt::ApplicationState state){
315  // On OSX it's possible for the app to be brought to the foreground without the window actually reappearing.
316  // We want to make sure it's shown when this happens.
317 #ifdef Q_OS_MAC
318  static quint64 lastStateChange = 0;
319  quint64 now = QDateTime::currentMSecsSinceEpoch();
320  if(state == Qt::ApplicationActive){
321  // This happens once at startup so ignore it. Also don't allow it to be called more than once every 2s.
322  if(lastStateChange != 0 && now >= lastStateChange + 2 * 1000)
323  showWindow();
324  lastStateChange = now;
325  }
326 #endif
327 }
328 
330  qApp->quit();
331 }
332 
334  foreach(KbWidget* w, kbWidgets)
335  delete w;
336  kbWidgets.clear();
337  KbManager::stop();
339 }
340 
342  cleanup();
343  delete ui;
344 }
void stateChange(Qt::ApplicationState state)
Definition: mainwindow.cpp:314
static KbManager * kbManager()
Definition: kbmanager.h:23
Ui::MainWindow * ui
Definition: mainwindow.h:75
static void stop()
Definition: kbmanager.cpp:19
void showFwUpdateNotification(QWidget *widget, float version)
Definition: mainwindow.cpp:230
void updateFwButton()
Definition: kbwidget.cpp:326
static void set(const QString &key, const QVariant &value)
void cleanup()
Definition: mainwindow.cpp:333
void checkFwUpdates()
Definition: mainwindow.cpp:210
void quitApp()
Definition: mainwindow.cpp:329
void showWindow()
Definition: mainwindow.cpp:308
void closeEvent(QCloseEvent *event)
Definition: mainwindow.cpp:247
static void cleanUp()
Definition: ckbsettings.cpp:46
static QVariant get(const QString &key, const QVariant &defaultValue=QVariant())
void toggleTrayIcon(bool visible)
Definition: mainwindow.cpp:150
void removeDevice(Kb *device)
Definition: mainwindow.cpp:177
void setStatus(const QString &text)
QString firmware
Definition: kb.h:18
QSharedMemory appShare
QMenu * trayIconMenu
Definition: mainwindow.h:53
static QTimer * scanTimer()
Definition: kbmanager.h:43
static float ckbDaemonVersionF()
Definition: kbmanager.h:31
QAction * restoreAction
Definition: mainwindow.h:43
static const QString configLabel
Definition: mainwindow.cpp:17
QString name() const
Definition: kbwidget.h:25
void updateVersion()
Definition: mainwindow.cpp:191
MainWindow(QWidget *parent=0)
Definition: mainwindow.cpp:49
bool hasShownNewFW
Definition: kbwidget.h:28
static float ckbGuiVersionF()
Definition: kbmanager.h:30
void showFwUpdate()
Definition: kbwidget.h:38
void showLastTab()
Definition: kbwidget.cpp:68
Definition: kb.h:11
static void init(QString guiVersion)
Definition: kbmanager.cpp:12
QSystemTrayIcon * trayIcon
Definition: mainwindow.h:54
static MainWindow * mainWindow
Definition: mainwindow.h:35
QString daemonDialogText
Definition: mainwindow.cpp:20
static bool checkUpdates()
Definition: kbfirmware.h:15
QString usbModel
Definition: kb.h:16
QTabWidget * tabWidget
Definition: ui_mainwindow.h:31
#define DAEMON_UNAVAILABLE_STR
Definition: kbmanager.h:12
void setVersion(const QString &version)
void setupUi(QMainWindow *MainWindow)
Definition: ui_mainwindow.h:33
void iconClicked(QSystemTrayIcon::ActivationReason reason)
Definition: mainwindow.cpp:295
QAction * closeAction
Definition: mainwindow.h:44
SettingsWidget * settingsWidget
Definition: mainwindow.h:40
QString features
Definition: kb.h:18
static QString ckbDaemonVersion()
Definition: kbmanager.h:27
QString devpath
Device path base ("/dev/input/ckb" or "/var/run/ckb")
Definition: kbmanager.cpp:4
void timerTick()
Definition: mainwindow.cpp:261
QList< KbWidget * > kbWidgets
Definition: mainwindow.h:41
Kb * device
Definition: kbwidget.h:24
void addDevice(Kb *device)
Definition: mainwindow.cpp:159
QAction * actionExit
Definition: ui_mainwindow.h:27
static float versionForBoard(const QString &features, bool waitForComplete=false)
Definition: kbfirmware.h:22