Files
ckb-next/src/gui/mainwindow.cpp
T
2018-10-08 00:45:32 +03:00

441 lines
16 KiB
C++

#include "ckbsettings.h"
#include "kbmanager.h"
#include "kbfirmware.h"
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <cstdlib>
#include <QFileInfo>
#include <QSharedMemory>
#include <QShortcut>
#include <QMessageBox>
#include <QMenuBar>
#include <unistd.h>
#include <ckbnextconfig.h>
#include <sys/socket.h>
#include <signal.h>
#include <QProcess>
#ifndef DISABLE_UPDATER
#include "ckbupdater.h"
#include "ckbupdaterwidget.h"
#endif
extern QSharedMemory appShare;
extern QString devpath;
static const QString configLabel = "Settings";
int MainWindow::signalHandlerFd[2] = {0, 0};
#ifndef Q_OS_MACOS
QString daemonDialogText = QObject::tr("Start it once with:") +
"<blockquote><code>sudo systemctl start ckb-next-daemon</code></blockquote>" +
QObject::tr("Enable it for every boot:") +
"<blockquote><code>sudo systemctl enable ckb-next-daemon</code></blockquote>";
#else
QString daemonDialogText = QObject::tr("Start and enable it with:") +
"<blockquote><code>sudo launchctl load -w /Library/LaunchDaemons/org.ckb-next.daemon.plist</code></blockquote>";
#endif
MainWindow* MainWindow::mainWindow = 0;
#ifdef USE_LIBAPPINDICATOR
extern "C" {
void quitIndicator(GtkMenu* menu, gpointer data) {
Q_UNUSED(menu);
MainWindow* window = static_cast<MainWindow*>(data);
window->quitApp();
}
void restoreIndicator(GtkMenu* menu, gpointer data) {
Q_UNUSED(menu);
MainWindow* window = static_cast<MainWindow*>(data);
window->showWindow();
}
}
#endif
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
mainWindow = this;
// Start device manager
KbManager::init(CKB_NEXT_VERSION_STR);
connect(KbManager::kbManager(), SIGNAL(kbConnected(Kb*)), this, SLOT(addDevice(Kb*)));
connect(KbManager::kbManager(), SIGNAL(kbDisconnected(Kb*)), this, SLOT(removeDevice(Kb*)));
connect(KbManager::kbManager(), SIGNAL(versionUpdated()), this, SLOT(updateVersion()));
connect(KbManager::scanTimer(), SIGNAL(timeout()), this, SLOT(timerTick()));
// Set up tray icon
restoreAction = new QAction(tr("Restore"), this);
closeAction = new QAction(tr("Quit"), this);
#ifdef USE_LIBAPPINDICATOR
QProcessEnvironment procEnv = QProcessEnvironment::systemEnvironment();
QString desktop = procEnv.value("XDG_CURRENT_DESKTOP", QString("")).toLower();
QString qpaTheme = procEnv.value("QT_QPA_PLATFORMTHEME", QString("")).toLower();
QString ckbnextAppindicator = procEnv.value("CKB-NEXT_USE_APPINDICATOR", QString("")).toLower();
useAppindicator = false;
trayIcon = 0;
if((desktop == "unity" && qpaTheme == "appmenu-qt5") || qpaTheme == "appmenu-qt5" || !ckbnextAppindicator.isEmpty()){
useAppindicator = true;
indicatorMenu = gtk_menu_new();
indicatorMenuRestoreItem = gtk_menu_item_new_with_label("Restore");
indicatorMenuQuitItem = gtk_menu_item_new_with_label("Quit");
gtk_menu_shell_append(GTK_MENU_SHELL(indicatorMenu), indicatorMenuRestoreItem);
gtk_menu_shell_append(GTK_MENU_SHELL(indicatorMenu), indicatorMenuQuitItem);
g_signal_connect(indicatorMenuQuitItem, "activate",
G_CALLBACK(quitIndicator), this);
g_signal_connect(indicatorMenuRestoreItem, "activate",
G_CALLBACK(restoreIndicator), this);
gtk_widget_show(indicatorMenuRestoreItem);
gtk_widget_show(indicatorMenuQuitItem);
indicator = app_indicator_new("ckb-next", "indicator-messages", APP_INDICATOR_CATEGORY_APPLICATION_STATUS);
app_indicator_set_status(indicator, APP_INDICATOR_STATUS_ACTIVE);
app_indicator_set_menu(indicator, GTK_MENU(indicatorMenu));
app_indicator_set_icon(indicator, "ckb-next");
} else
#endif
{
trayIconMenu = new QMenu(this);
trayIconMenu->addAction(restoreAction);
trayIconMenu->addAction(closeAction);
trayIcon = new QSystemTrayIcon(QIcon(":/img/ckb-next.png"), this);
trayIcon->setContextMenu(trayIconMenu);
connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, SLOT(iconClicked(QSystemTrayIcon::ActivationReason)));
}
toggleTrayIcon(!CkbSettings::get("Program/SuppressTrayIcon").toBool());
#ifdef Q_OS_MACOS
// Make a custom "Close" menu action for OSX, as the default one brings up the "still running" popup unnecessarily
QMenuBar* menuBar = new QMenuBar(this);
setMenuBar(menuBar);
this->menuBar()->addMenu("ckb-next")->addAction(closeAction);
#else
// On linux, add a handler for Ctrl+Q
new QShortcut(QKeySequence("Ctrl+Q"), this, SLOT(quitApp()));
#endif
connect(ui->actionExit, SIGNAL(triggered()), this, SLOT(quitApp()));
connect(closeAction, SIGNAL(triggered()), this, SLOT(quitApp()));
connect(restoreAction, SIGNAL(triggered()), this, SLOT(showWindow()));
connect(qApp, SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(stateChange(Qt::ApplicationState)));
connect(qApp, SIGNAL(aboutToQuit()), this, SLOT(cleanup()));
ui->tabWidget->addTab(settingsWidget = new SettingsWidget(this), configLabel);
settingsWidget->setVersion("ckb-next " CKB_NEXT_VERSION_STR);
// create daemon dialog as a QMessageBox
// this will create a focussed dialog, that has to be interacted with,
// if the daemon is not running
// set the main and informative text to tell the user about the issue
QMessageBox dialog;
dialog.setText(tr("The ckb-next daemon is not running. This program will <b>not</b> work without it!"));
dialog.setInformativeText(daemonDialogText);
dialog.setIcon(QMessageBox::Critical);
// Set up signal handler
socketpair(AF_UNIX, SOCK_STREAM, 0, MainWindow::signalHandlerFd);
sigNotifier = new QSocketNotifier(MainWindow::signalHandlerFd[1], QSocketNotifier::Read, this);
connect(sigNotifier, &QSocketNotifier::activated, this, &MainWindow::QSignalHandler);
signal(SIGINT, MainWindow::PosixSignalHandler);
signal(SIGTERM, MainWindow::PosixSignalHandler);
// check, whether daemon is running
// the daemon creates the root device path on initialization and thus it
// can be assumed, that the daemon is not running if doesn't exist
// `.arg(0)` is necessary to interpolate the correct suffix into the path
// see `./kbmanager.cpp` for details
QFileInfo rootDevPath(devpath.arg(0));
if (!rootDevPath.exists()) {
// set settings widget's status
// show the main window (otherwise only the dialog will be visible)
// finally show the dialog
settingsWidget->setStatus(tr("The ckb-next daemon is not running."));
showWindow();
dialog.exec();
}
#ifndef DISABLE_UPDATER
if(!CkbSettings::get("Program/DisableAutoUpdCheck", false).toBool())
checkForCkbUpdates();
connect(settingsWidget, &SettingsWidget::checkForUpdates, this, &MainWindow::checkForCkbUpdates);
#endif
}
void MainWindow::checkForCkbUpdates(){
#ifndef DISABLE_UPDATER
updater = new CkbUpdater(this);
connect(updater, &CkbUpdater::checkedForNewVer, this, &MainWindow::checkedForNewVer);
updater->checkForNewVersion();
#endif
}
void MainWindow::toggleTrayIcon(bool visible){
#ifdef USE_LIBAPPINDICATOR
if(useAppindicator)
app_indicator_set_status(indicator, visible ? APP_INDICATOR_STATUS_ACTIVE : APP_INDICATOR_STATUS_PASSIVE);
else
#endif
trayIcon->setVisible(visible);
}
void MainWindow::addDevice(Kb* device){
// Connected already?
foreach(KbWidget* w, kbWidgets){
if(w->device == device)
return;
}
// Add the keyboard
KbWidget* widget = new KbWidget(this, device);
kbWidgets.append(widget);
// Add to tabber; switch to this device if the user is on the settings screen
int count = ui->tabWidget->count();
ui->tabWidget->insertTab(count - 1, widget, widget->name());
if(ui->tabWidget->currentIndex() == count)
ui->tabWidget->setCurrentIndex(count - 1);
// Update connected device count
updateVersion();
}
void MainWindow::removeDevice(Kb* device){
foreach(KbWidget* w, kbWidgets){
// Remove this device from the UI
if(w->device == device){
int i = kbWidgets.indexOf(w);
ui->tabWidget->removeTab(i);
kbWidgets.removeAt(i);
w->deleteLater();
}
}
// Update connected device count
updateVersion();
}
void MainWindow::updateVersion(){
QString daemonVersion = KbManager::ckbDaemonVersion();
if(daemonVersion == DAEMON_UNAVAILABLE_STR){
settingsWidget->setStatus(tr("Driver inactive"));
return;
}
int count = kbWidgets.count();
// Warn if the daemon version doesn't match the GUI
QString daemonWarning;
if(daemonVersion != CKB_NEXT_VERSION_STR)
daemonWarning = "<br /><br /><b>Warning:</b> Driver version mismatch (" + daemonVersion + "). Please upgrade ckb-next" + QString(KbManager::ckbDaemonVersionF() > KbManager::ckbGuiVersionF() ? "" : "-daemon") + ". If the problem persists, try rebooting.";
if(count == 0){
#if defined(Q_OS_MACOS) && !defined(OS_MAC_LEGACY)
QProcess kextstat;
kextstat.start("kextstat", QStringList() << "-l" << "-b" << "org.pqrs.driver.Karabiner.VirtualHIDDevice.v060800");
if(!kextstat.waitForFinished())
qDebug() << "Kextstat error";
QString kextstatOut(kextstat.readAll());
if(kextstatOut.isEmpty())
daemonWarning.append(tr("<br /><b>Warning:</b> System Extension by \"Fumihiko Takayama\" is not allowed in Security & Privacy. Please allow it and then unplug and replug your devices."));
#elif defined(Q_OS_LINUX)
QProcess modprobe;
modprobe.start("modprobe", QStringList("uinput"));
if(!modprobe.waitForFinished())
qDebug() << "Modprobe error";
if(modprobe.exitCode())
daemonWarning.append(tr("<br /><b>Warning:</b> The uinput module could not be loaded. If this issue persists after rebooting, compile a kernel with CONFIG_INPUT_UINPUT=y."));
#endif
settingsWidget->setStatus(tr("No devices connected") + daemonWarning);
}
else if(count == 1)
settingsWidget->setStatus(tr("1 device connected") + daemonWarning);
else
settingsWidget->setStatus(QString(tr("%1 devices connected")).arg(count) + daemonWarning);
}
void MainWindow::checkFwUpdates(){
if(!mainWindow->isVisible())
return;
foreach(KbWidget* w, kbWidgets){
// Display firmware upgrade notification if a new version is available
float version = KbFirmware::versionForBoard(w->device->productID);
if(version > w->device->firmware.toFloat()){
if(w->hasShownNewFW)
continue;
w->hasShownNewFW = true;
w->updateFwButton();
// Don't run this method here because it will lock up the timer and prevent devices from working properly
// Use a queued invocation instead
metaObject()->invokeMethod(this, "showFwUpdateNotification", Qt::QueuedConnection, Q_ARG(QWidget*, w), Q_ARG(float, version));
// Don't display more than one of these at once
return;
}
}
}
void MainWindow::showFwUpdateNotification(QWidget* widget, float version){
static bool isShowing = false;
if(isShowing)
return;
isShowing = true;
showWindow();
KbWidget* w = (KbWidget*)widget;
// Ask for update
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){
// If accepted, switch to the firmware tab and bring up the update window
w->showLastTab();
ui->tabWidget->setCurrentIndex(kbWidgets.indexOf(w));
w->showFwUpdate();
}
isShowing = false;
}
void MainWindow::closeEvent(QCloseEvent *event){
// 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
if(!event->spontaneous() || isHidden()){
event->accept();
return;
}
if(!CkbSettings::get("Popups/BGWarning").toBool()){
QMessageBox::information(this, "ckb-next", "ckb-next will still run in the background.\nTo close it, choose Quit from the tray menu\nor click \"Quit\" on the Settings screen.");
CkbSettings::set("Popups/BGWarning", true);
}
hide();
event->ignore();
}
void MainWindow::timerTick(){
// Check shared memory for changes
if(appShare.lock()){
void* data = appShare.data();
QStringList commands = QString((const char*)data).split("\n");
// Restore PID, remove all other data
snprintf((char*)appShare.data(), appShare.size(), "PID %ld", (long)getpid());
appShare.unlock();
// Parse commands
foreach(const QString& line, commands){
// Old ckb option line - bring application to foreground
if(line == "Open")
showWindow();
if(line.startsWith("Option ")){
// New ckb option line
QString option = line.split(" ")[1];
if(option == "Open")
// Bring to foreground
showWindow();
else if(option == "Close")
// Quit application
qApp->quit();
}
}
}
// Check for firmware updates (when appropriate)
if(!CkbSettings::get("Program/DisableAutoFWCheck").toBool()){
KbFirmware::checkUpdates();
checkFwUpdates();
}
// Poll for setting updates
settingsWidget->pollUpdates();
}
void MainWindow::iconClicked(QSystemTrayIcon::ActivationReason reason){
// On Linux, hide/show the app when the tray icon is clicked
// On OSX this just shows the menu
#ifndef Q_OS_MACOS
if(reason == QSystemTrayIcon::DoubleClick || reason == QSystemTrayIcon::Trigger){
if(isVisible())
hide();
else
showWindow();
}
#endif
}
void MainWindow::showWindow(){
showNormal();
raise();
activateWindow();
}
void MainWindow::stateChange(Qt::ApplicationState state){
// On OSX it's possible for the app to be brought to the foreground without the window actually reappearing.
// We want to make sure it's shown when this happens.
#ifdef Q_OS_MAC
static quint64 lastStateChange = 0;
quint64 now = QDateTime::currentMSecsSinceEpoch();
if(state == Qt::ApplicationActive){
// This happens once at startup so ignore it. Also don't allow it to be called more than once every 2s.
if(lastStateChange != 0 && now >= lastStateChange + 2 * 1000)
showWindow();
lastStateChange = now;
}
#endif
}
void MainWindow::quitApp(){
qApp->quit();
}
void MainWindow::cleanup(){
foreach(KbWidget* w, kbWidgets)
delete w;
kbWidgets.clear();
KbManager::stop();
CkbSettings::cleanUp();
}
MainWindow::~MainWindow(){
cleanup();
delete ui;
}
void MainWindow::QSignalHandler(){
sigNotifier->setEnabled(false);
int sig = -1;
int ret = read(signalHandlerFd[1], &sig, sizeof(sig));
if(ret == -1){
qDebug() << "Error on QSignalHandler read";
return;
}
qDebug() << "\nSignal" << sig << "caught. Quitting...";
this->quitApp();
sigNotifier->setEnabled(true);
}
void MainWindow::PosixSignalHandler(int signal){
int ret = write(MainWindow::signalHandlerFd[0], &signal, sizeof(signal));
if(ret == -1)
qDebug() << "Error on PosixSignalHandler write";
}
void MainWindow::checkedForNewVer(QString ver, QString changelog){
#ifndef DISABLE_UPDATER
if(!ver.isEmpty()) {
settingsWidget->setUpdateButtonText("Update to v" + ver);
showWindow();
CkbUpdaterDialog updDialog(ver, changelog, this);
updDialog.exec();
} else {
settingsWidget->setUpdateButtonText("Up to date");
}
updater->deleteLater();
settingsWidget->enableUpdateButton();
#endif
}