diff --git a/include/plugins/launcher_plugin.hpp b/include/plugins/launcher_plugin.hpp index feef6e15..0a9baeb2 100644 --- a/include/plugins/launcher_plugin.hpp +++ b/include/plugins/launcher_plugin.hpp @@ -1,12 +1,21 @@ #pragma once +#include #include "plugins/plugin.hpp" class LauncherPlugin : public Plugin { + Q_OBJECT + public: LauncherPlugin() { this->settings.beginGroup("Launcher"); } virtual ~LauncherPlugin() = default; virtual void remove_widget(int idx) { this->loaded_widgets.removeAt(idx); } + virtual void add_widget(QWidget *widget){} + +public slots: + + signals: + void widget_added(QWidget *widget); protected: QList loaded_widgets; diff --git a/include/plugins/plugin.hpp b/include/plugins/plugin.hpp index eeb1c3c1..a0e61739 100644 --- a/include/plugins/plugin.hpp +++ b/include/plugins/plugin.hpp @@ -7,7 +7,9 @@ class Arbiter; -class Plugin { +class Plugin : public QObject { + Q_OBJECT + public: Plugin() : settings(qApp->organizationName(), "plugins") {} virtual ~Plugin() = default; diff --git a/install.sh b/install.sh index d8a5c649..de8d48b0 100755 --- a/install.sh +++ b/install.sh @@ -394,8 +394,13 @@ else exit 1 fi - echo Running Dash make - make + + coreQ=$(grep 'cpu cores' /proc/cpuinfo | uniq) + cores=${coreQ:12:1} + echo Running Dash make -j$cores + make -j$cores + + if [[ $? -eq 0 ]]; then echo -e Dash make ok, executable can be found ../bin/dash echo diff --git a/plugins/brightness/mocked/mocked.hpp b/plugins/brightness/mocked/mocked.hpp index 7825a0a0..12dffcbd 100644 --- a/plugins/brightness/mocked/mocked.hpp +++ b/plugins/brightness/mocked/mocked.hpp @@ -5,7 +5,7 @@ #include "plugins/brightness_plugin.hpp" -class Mocked : public QObject, BrightnessPlugin { +class Mocked : public BrightnessPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID BrightnessPlugin_iid FILE "mocked.json") Q_INTERFACES(BrightnessPlugin) diff --git a/plugins/brightness/official_rpi/official_rpi.hpp b/plugins/brightness/official_rpi/official_rpi.hpp index 5344a94b..16bd4adb 100644 --- a/plugins/brightness/official_rpi/official_rpi.hpp +++ b/plugins/brightness/official_rpi/official_rpi.hpp @@ -6,7 +6,7 @@ #include "plugins/brightness_plugin.hpp" -class OfficialRPi : public QObject, BrightnessPlugin { +class OfficialRPi : public BrightnessPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID BrightnessPlugin_iid FILE "official_rpi.json") Q_INTERFACES(BrightnessPlugin) diff --git a/plugins/brightness/x/x.hpp b/plugins/brightness/x/x.hpp index ccb4265b..9e622867 100644 --- a/plugins/brightness/x/x.hpp +++ b/plugins/brightness/x/x.hpp @@ -3,7 +3,7 @@ #include #include "plugins/brightness_plugin.hpp" -class X : public QObject, BrightnessPlugin { +class X : public BrightnessPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID BrightnessPlugin_iid FILE "x.json") Q_INTERFACES(BrightnessPlugin) diff --git a/plugins/launcher/app/app.cpp b/plugins/launcher/app/app.cpp index 5cbd21b8..33b8933b 100644 --- a/plugins/launcher/app/app.cpp +++ b/plugins/launcher/app/app.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "app/config.hpp" #include "app/arbiter.hpp" @@ -134,7 +135,7 @@ Launcher::Launcher(Arbiter &arbiter, QSettings &settings, int idx, QWidget *pare layout->addWidget(this->launcher_widget()); layout->addWidget(this->app); - if (this->auto_launch) + if (this->auto_launch) this->app->start(launcher_app); } @@ -291,7 +292,12 @@ void App::remove_widget(int idx) this->settings.remove(QString::number(idx)); for (int i = 0; i < this->loaded_widgets.size(); i++) { + auto typeStr(typeid(this->loaded_widgets[i]).name()); + qDebug() << typeStr; + if (Launcher *launcher = qobject_cast(this->loaded_widgets[i])) launcher->update_idx(i); + + } } diff --git a/plugins/launcher/app/app.hpp b/plugins/launcher/app/app.hpp index 845d45f2..9f40178e 100644 --- a/plugins/launcher/app/app.hpp +++ b/plugins/launcher/app/app.hpp @@ -100,7 +100,7 @@ class Launcher : public QWidget { }; -class App : public QObject, LauncherPlugin { +class App : public LauncherPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID LauncherPlugin_iid FILE "app.json") Q_INTERFACES(LauncherPlugin) diff --git a/plugins/launcher/home/home.cpp b/plugins/launcher/home/home.cpp new file mode 100644 index 00000000..c25d7c02 --- /dev/null +++ b/plugins/launcher/home/home.cpp @@ -0,0 +1,506 @@ +#include +#include +#include +#include + +#include "app/config.hpp" +#include "app/arbiter.hpp" +#include "app/session.hpp" +#include "app/pages/launcher.hpp" + +#include "home.hpp" + +XWorker::WindowProp::WindowProp(char *prop, unsigned long size) +{ + this->size = size; + this->prop = new char[this->size + 1]; + + std::copy(prop, prop + size, (char *)this->prop); + ((char *)this->prop)[size] = '\0'; +} + +XWorker::WindowProp::~WindowProp() +{ + if (this->prop != nullptr) { + delete (char *)this->prop; + this->prop = nullptr; + } +} + +//REFACTOR: create XInfo Class and implement classes for traversing the x tree, tracking newly opened +//windows +XWorker::XWorker(QObject *parent) : QObject(parent) +{ + this->display = XOpenDisplay(0); + this->root_window = DefaultRootWindow(this->display); +} + +int XWorker::get_window(uint64_t pid) +{ + int retries = 0; + while (retries < MAX_RETRIES) { + WindowProp client_list = this->get_window_prop(this->root_window, XA_WINDOW, "_NET_CLIENT_LIST"); + Window *windows = (Window *)client_list.prop; + for (unsigned long i = 0; i < (client_list.size / sizeof(Window)); i++) { + if (pid == *(uint64_t *)this->get_window_prop(windows[i], XA_CARDINAL, "_NET_WM_PID").prop) + return windows[i]; + } + usleep(500000); + retries++; + } + + return -1; +} + +XWorker::WindowProp XWorker::get_window_prop(Window window, Atom type, const char *name) +{ + Atom prop = XInternAtom(this->display, name, false); + + Atom actual_type_return; + int actual_format_return; + unsigned long nitems_return; + unsigned long bytes_after_return; + unsigned char *prop_return; + XGetWindowProperty(this->display, window, prop, 0, 1024, false, type, &actual_type_return, &actual_format_return, + &nitems_return, &bytes_after_return, &prop_return); + + unsigned long size = (actual_format_return / 8) * nitems_return; + if (actual_format_return == 32) size *= sizeof(long) / 4; + + WindowProp window_prop((char *)prop_return, size); + XFree(prop_return); + + return window_prop; +} + +EmbeddedApp::EmbeddedApp(Arbiter *arbiter, QWidget *parent) : QWidget(parent), process() +{ + this->arbiter = arbiter; + this->worker = new XWorker(this); + + this->process.setStandardOutputFile(QProcess::nullDevice()); + this->process.setStandardErrorFile(QProcess::nullDevice()); + connect(&this->process, QOverload::of(&QProcess::finished), + [this](int, QProcess::ExitStatus) { this->end(); }); + + this->container = new QVBoxLayout(this); + this->container->setContentsMargins(0, 0, 0, 0); +} + +EmbeddedApp::~EmbeddedApp() +{ + this->process.kill(); + this->process.waitForFinished(); + + delete this->container; + delete this->worker; +} + +void EmbeddedApp::start(QString app) +{ + this->process.setProgram(app); + this->process.start(); + this->process.waitForStarted(); + + QWindow *window = QWindow::fromWinId(worker->get_window(this->process.processId())); + + QWindow *thisWindow = QWindow::fromWinId(this->winId()); + window->setParent(thisWindow); + window->setFlags(Qt::FramelessWindowHint); + usleep(50000); + + this->container->addWidget(QWidget::createWindowContainer(window, this)); + emit opened(); +} + +void EmbeddedApp::end() +{ + this->process.terminate(); + delete this->container->takeAt(0); + emit closed(); +} + +ILauncherPlugin::ILauncherPlugin(){ + this->settings.beginGroup("Home"); + + window_manager = WindowManager::Create(); + if (!window_manager) { + qDebug() << "[ERROR] could not load Window Manager!"; + //TODO: remove self(plugin) + } else { + window_manager->Run(); + if(window_manager->isRunning) + qDebug() << "Window manager started"; + else + qDebug() << "[INFO] a window manager is already running, unable to take control"; + qDebug() << "[TIP] start dash in a new session without a window manager to take control"; + //TODO: connect slots / signals + + } + +} + +void ILauncherPlugin::init(){ + + this->loaded_widgets.push_front(new Home(this->arbiter, this->settings, 0, this)); + +} + +QList ILauncherPlugin::widgets() +{ + if(this->loaded_widgets.count() == 0) this->init(); + return this->loaded_widgets; +} + +void ILauncherPlugin::remove_widget(int idx){ + + //TODO: remove apps + +} + +void ILauncherPlugin::add_widget(QWidget *widget){ + + this->loaded_widgets.push_back(widget); + emit widget_added(widget); + +} + +Home::Home(Arbiter *arbiter, QSettings &settings, int idx, ILauncherPlugin *plugin, QWidget *parent) + : QWidget(parent) + , arbiter(arbiter) + , settings(settings) + , idx(idx) +{ + this->plugin = plugin; + this->setup_ui(); + +} + +void Home::setup_ui() +{ + this->setObjectName("Home"); + QStackedLayout *layout = new QStackedLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + container = new QWidget(this); + entries_grid = new QGridLayout(container); + container->setLayout(entries_grid); + auto entries = DesktopEntry::get_entries(this->arbiter, this->plugin); + QScrollArea *scroll_area = new QScrollArea(this); + Session::Forge::to_touch_scroller(scroll_area); + scroll_area->setWidgetResizable(true); + scroll_area->setWidget(container); + layout->addWidget(scroll_area); + + //TODO: detect width and set appropriately + int x = 0; + int y = 0; + for (int i = 0; i < entries.count(); ++i){ + + DesktopEntry *entry = entries[i]; + connect(entry, &DesktopEntry::clicked, [this, entry]() { + + EmbeddedApp *app = new EmbeddedApp(this->arbiter, this); + app->setObjectName(entry->get_name()); + this->plugin->add_widget(app); + app->start(entry->get_exec()); + + }); + entries_grid->addWidget(entry, y, x, 1, 1); + //set x & y + if(x > 6) { + x = 0; + y += 1; + } else { + x += 1; + } + + } + +} + +void Home::update_idx(int idx){ + + if (idx == this->idx) + return; + + this->idx = idx; + +} + +QWidget *Home::config_widget() +{ + //NOT IMPLEMENTED + QWidget *widget = new QWidget(this); + return widget; +} + +DesktopEntry::DesktopEntry(QString fileLocation, Arbiter *arbiter, ILauncherPlugin *plugin, QWidget *parent) +{ + this->arbiter = arbiter; + this->plugin = plugin; + + QFile inputFile(fileLocation); + inputFile.open(QIODevice::ReadOnly); + + if (!inputFile.isOpen()) return; + //parse desktop entry + QTextStream stream(&inputFile); + for (QString line = stream.readLine(); + !line.isNull(); + line = stream.readLine()) + { + + if(line.contains("Exec")){ + QStringList vals = line.split( "=" ); + this->exec_ = vals.value(1); + + QStringList args = vals.value(1).split( " " ); + this->exec_ = args.value(0); + args.removeAt(0); + this->args_ = args; + + } + + if(line.contains("Name") && !line.contains("Generic") && !line.contains("[") && !line.contains("]")){ + QStringList vals = line.split( "=" ); + this->name_ = vals.value(1); + } + + if(line.contains("Icon")){ + QStringList vals = line.split( "=" ); + this->icon_ = vals.value(1); + } + + if(line.contains("[Desktop") && !line.contains("Entry]")) break;//skip extra stuff in Desktop Entry + + }; + + this->setup_ui(); + +} + +void DesktopEntry::setup_ui(){ + //setup ui + verticalLayout = new QVBoxLayout(this); + verticalLayout->setObjectName(QString::fromUtf8("verticalLayout")); + horizontalLayout = new QHBoxLayout(); + horizontalLayout->setObjectName(QString::fromUtf8("horizontalLayout")); + iconLabel = new QLabel(this); + iconLabel->setMaximumSize(QSize(64, 64)); + iconLabel->setObjectName(QString::fromUtf8("iconLabel")); + iconLabel->setAlignment(Qt::AlignCenter); + iconLabel->setPixmap (this->get_pixmap().scaled(64,64,Qt::KeepAspectRatio)); + iconLabel->show(); + horizontalLayout->addWidget(iconLabel); + verticalLayout->addLayout(horizontalLayout); + nameLabel = new QLabel(this); + QFontMetrics metrics(nameLabel->font()); + QString elidedText = metrics.elidedText(this->get_name(), Qt::ElideRight, (nameLabel->width() * 2) - 16); + nameLabel->setObjectName(QString::fromUtf8("nameLabel")); + nameLabel->setText(elidedText); + nameLabel->setMaximumSize(QSize(124, 64)); + nameLabel->setAlignment(Qt::AlignCenter); + nameLabel->setWordWrap(true); + verticalLayout->addWidget(nameLabel); + this->setMaximumSize(QSize(128, 128)); + +} + +void DesktopEntry::mousePressEvent(QMouseEvent *event){ + emit clicked(); +} + +int DesktopEntry::resolutionFromString(QString string){ + + if(string.contains("scalable")) return 10000; + if(string.contains("128")) return 128; + if(string.contains("64")) return 64; + if(string.contains("48")) return 48; + if(string.contains("42")) return 42; + if(string.contains("36")) return 36; + if(string.contains("32")) return 32; + if(string.contains("24")) return 24; + if(string.contains("22")) return 22; + if(string.contains("16")) return 16; + return 0;//error +} + +QPixmap DesktopEntry::get_pixmap(){ + + /* The freedesktop.org standard specifies in which order and directories programs should look for icons: + + $HOME/.icons (for backwards compatibility) + $XDG_DATA_DIRS/icons + /usr/share/pixmaps + + from the spec: + + "In the theme directory are also a set of subdirectories containing image files. + Each directory contains icons designed for a certain nominal icon size and scale, + as described by the index.theme file. The subdirectories are allowed to be several levels deep, + e.g. the subdirectory "48x48/apps" in the theme "hicolor" would end up at $basedir/icons/hicolor/48x48/apps{icon_}.{png|svg}" + + **Note: + This implementation isn't fully spec compliant. It doesn't lookup themes. + The current implementation searches the icon directories for all themes and grabs the highest + resolution it can from the first theme it finds a match in. + */ + + if(this->icon_ == "") { + //TODO: change icons on dark / light change + QPixmap pixmap(":/icons/widgets.svg"); + QSize size(512, 512); + auto icon_mask = pixmap.scaled(size).createMaskFromColor(Qt::transparent); + auto color = (this->arbiter->theme().mode == Session::Theme::Light) ? QColor(0, 0, 0) : QColor(255, 255, 255); + color.setAlpha((this->arbiter->theme().mode == Session::Theme::Light) ? 162 : 134); + QPixmap icon(size); + icon.fill(color); + icon.setMask(icon_mask); + return icon; + + } + + //check if icon is path + if(this->icon_.contains("/") && (this->icon_.contains(".png") || this->icon_.contains(".svg") || this->icon_.contains(".xpm"))){ + if(!QFile::exists(this->icon_)){ + return QPixmap(":/icons/widgets.svg"); + } + return QPixmap(this->icon_); + } + + //check in $HOME/.icons + auto h_png_path = QDir::homePath() + "/" + this->icon_ + ".png"; + if(QFile::exists(h_png_path)) return QPixmap(h_png_path); + auto h_svg_path = QDir::homePath() + "/" + this->icon_ + ".svg"; + if(QFile::exists(h_svg_path)) return QPixmap(h_svg_path); + auto h_xpm_path = QDir::homePath() + "/" + this->icon_ + ".xpm"; + if(QFile::exists(h_xpm_path)) return QPixmap(h_xpm_path); + + //get theme dirs from DATA_DIRS + QString xdg = qgetenv("XDG_DATA_DIRS"); + auto xdgArr = xdg.split(":"); + for(QString dir_name : xdgArr) + { + //drop repeated dirs + if(dir_name.endsWith("/")) continue; + + //search themes in dir + //TODO: prefer certain themes + QDirIterator themes( dir_name + "/icons", QDir::Dirs | QDir::NoDotAndDotDot); + while ( themes.hasNext() ) { + + QString themeDirPath = themes.next(); + QDir themeDir(themeDirPath); + QString theme = themeDir.dirName(); + QDirIterator resolutions( themeDirPath, QDir::Dirs | QDir::NoDotAndDotDot); + + int highestResolution = 0; + QString highestResPath = ""; + + //get highest resolution icon possible + while ( resolutions.hasNext() ) { + + QString resolutionPath = resolutions.next(); + QDir resolutionDir(resolutionPath); + QString resolution = resolutionDir.dirName(); + int iRes = DesktopEntry::resolutionFromString(resolution); + + //skip if higher resolution already available + if(highestResolution > iRes) continue; + QDirIterator subdirs( resolutionPath, QDir::Dirs | QDir::NoDotAndDotDot); + while ( subdirs.hasNext() ) { + + QDir subDir(subdirs.next()); + QString sub_dir_name = subDir.dirName(); + auto xdg_png_path = dir_name + "/icons/" + theme + "/" + resolution + "/" + sub_dir_name + "/" + this->icon_ + ".png"; + if(QFile::exists(xdg_png_path)) { + highestResolution = iRes; + highestResPath = xdg_png_path; + + } + auto xdg_svg_path = dir_name + "/icons/" + theme + "/" + resolution + "/" + sub_dir_name + "/" + this->icon_ + ".svg"; + if(QFile::exists(xdg_svg_path)) { + highestResolution = iRes; + highestResPath = xdg_svg_path; + } + + auto xdg_xpm_path = dir_name + "/icons/" + theme + "/" + resolution + "/" + sub_dir_name + "/" + this->icon_ + ".xpm"; + if(QFile::exists(xdg_xpm_path)) { + highestResolution = iRes; + highestResPath = xdg_xpm_path; + } + + } + + } + + if(highestResolution > 0){ + return QPixmap(highestResPath); + } + + } + } + + //check pixmaps + auto pixmap_png_path = "/usr/share/pixmaps/" + this->icon_ + ".png"; + if(QFile::exists(pixmap_png_path)) { + return QPixmap(pixmap_png_path); + } + auto pixmap_svg_path = "/usr/share/pixmaps/" + this->icon_ + ".svg"; + if(QFile::exists(pixmap_svg_path)) { + return QPixmap(pixmap_svg_path); + } + + auto pixmap_xpm_path = "/usr/share/pixmaps/" + this->icon_ + ".xpm"; + if(QFile::exists(pixmap_xpm_path)) { + return QPixmap(pixmap_xpm_path); + } + + //no icon found, return default + QPixmap pixmap(":/icons/widgets.svg"); + QSize size(512, 512); + auto icon_mask = pixmap.scaled(size).createMaskFromColor(Qt::transparent); + auto color = (this->arbiter->theme().mode == Session::Theme::Light) ? QColor(0, 0, 0) : QColor(255, 255, 255); + color.setAlpha((this->arbiter->theme().mode == Session::Theme::Light) ? 162 : 134); + QPixmap icon(size); + icon.fill(color); + icon.setMask(icon_mask); + + return icon; +} + +QList DesktopEntry::get_entries(Arbiter *arbiter, ILauncherPlugin *plugin) +{ + /* + Desktop entry files must reside in the $XDG_DATA_DIRS/applications directory and must have a .desktop file extension. + If $XDG_DATA_DIRS is not set, then the default path is /usr/share is used. + If $XDG_DATA_HOME is not set, then the default path ~/.local/share is used. + Desktop entries are collected from all directories in the $XDG_DATA_DIRS environment variable. + Directories which appear first in $XDG_CONFIG_DIRS are given precedence when there are several .desktop files with the same name. + */ + + QList rtn; + QString xdg = qgetenv("XDG_DATA_DIRS"); + for(QString dir_name : xdg.split(":")) + { + + auto dir = dir_name + "/applications"; + QDir source(dir); + if (!source.exists()) + continue; + + QStringList files = source.entryList(QStringList() << "*.desktop", QDir::Files); + for (QString file: files) + { + //TODO: validate & error handle + QString path = source.absoluteFilePath(file); + DesktopEntry *entry = new DesktopEntry(path, arbiter, plugin); + rtn.push_back(entry); + + } + + } + + return rtn; + +} diff --git a/plugins/launcher/home/home.hpp b/plugins/launcher/home/home.hpp new file mode 100644 index 00000000..78c23615 --- /dev/null +++ b/plugins/launcher/home/home.hpp @@ -0,0 +1,169 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "plugins/launcher_plugin.hpp" + +#include "util.hpp" +#include "window_manager.hpp" +#include + +#include +#include +#undef Bool +#undef CurrentTime +#undef CursorShape +#undef Expose +#undef KeyPress +#undef KeyRelease +#undef FocusIn +#undef FocusOut +#undef FontChange +#undef None +#undef Status +#undef Unsorted + +class Arbiter; +class Config; +class Theme; +class DesktopEntry; +class ILauncherPlugin; + +class XWorker : public QObject { + Q_OBJECT + + public: + XWorker(QObject *parent = nullptr); + int get_window(uint64_t pid); + + private: + const int MAX_RETRIES = 60; + + struct WindowProp { + WindowProp(char *prop, unsigned long size); + ~WindowProp(); + void *prop; + unsigned long size; + }; + + WindowProp get_window_prop(Window window, Atom type, const char *name); + + Display *display; + Window root_window; +}; + +class EmbeddedApp : public QWidget { + Q_OBJECT + + public: + EmbeddedApp(Arbiter *arbiter, QWidget *parent = nullptr); + ~EmbeddedApp(); + + void start(QString app); + void end(); + + private: + QProcess process; + QVBoxLayout *container; + XWorker *worker; + Arbiter *arbiter; + + signals: + void closed(); + void opened(); +}; + +class ILauncherPlugin : public LauncherPlugin { + Q_OBJECT + Q_PLUGIN_METADATA(IID LauncherPlugin_iid FILE "home.json") + Q_INTERFACES(LauncherPlugin) + +public: + ILauncherPlugin(); + QList widgets() override; + void remove_widget(int idx) override; + void add_widget(QWidget *widget) override; + +private: + void init(); + WindowManager *window_manager; + +}; + +class Home : public QWidget { + Q_OBJECT + + public: + Home(Arbiter *arbiter, QSettings &settings, int idx, ILauncherPlugin *plugin, QWidget *parent = nullptr); + void update_idx(int idx); + + private: + //const QString DEFAULT_DIR = QDir().absolutePath(); + QGridLayout *entries_grid; + QWidget *container; + QScrollArea *scroll_area; + QStackedLayout *layout; + QWidget *config_widget();//not implemented + + Arbiter *arbiter; + QSettings &settings; + ILauncherPlugin *plugin; + int idx; + + void setup_ui(); + +}; + + +class DesktopEntry : public QWidget{ + Q_OBJECT + +public: + DesktopEntry(QString fileLocation, Arbiter *arbiter, ILauncherPlugin *plugin, QWidget *parent = nullptr); + + static QList get_entries(Arbiter *arbiter, ILauncherPlugin *plugin); + static int resolutionFromString(QString string); + inline QString get_exec() { return this->exec_; }; + inline QString get_icon() { return this->icon_; }; + inline QString get_name() { return this->name_; }; + inline QString get_path() { return this->path_; }; + inline QList get_args() {return this->args_;}; + QPixmap get_pixmap(); + QWidget *get_widget(); + +public slots: + +signals: + void clicked(); + +protected: + void mousePressEvent(QMouseEvent *event) override; + +private: + QVBoxLayout *verticalLayout; + QHBoxLayout *horizontalLayout; + QLabel *iconLabel; + QLabel *nameLabel; + + Arbiter *arbiter; + ILauncherPlugin *plugin; + QString exec_; + QString path_; + QString name_; + QString icon_; + QList args_; + + void setup_ui(); + +}; + diff --git a/plugins/launcher/home/home.json b/plugins/launcher/home/home.json new file mode 100644 index 00000000..c7de6519 --- /dev/null +++ b/plugins/launcher/home/home.json @@ -0,0 +1,4 @@ +{ + "name" : "home", + "version" : "1.0" +} diff --git a/plugins/launcher/home/util.cpp b/plugins/launcher/home/util.cpp new file mode 100644 index 00000000..512e5ab6 --- /dev/null +++ b/plugins/launcher/home/util.cpp @@ -0,0 +1,366 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2013-2017 Chuan Ji * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "util.hpp" +#include +#include +#include + +using ::std::string; +using ::std::vector; +using ::std::pair; +using ::std::ostringstream; + +string ToString(const XEvent& e) { + static const char* const X_EVENT_TYPE_NAMES[] = { + "", + "", + "KeyPress", + "KeyRelease", + "ButtonPress", + "ButtonRelease", + "MotionNotify", + "EnterNotify", + "LeaveNotify", + "FocusIn", + "FocusOut", + "KeymapNotify", + "Expose", + "GraphicsExpose", + "NoExpose", + "VisibilityNotify", + "CreateNotify", + "DestroyNotify", + "UnmapNotify", + "MapNotify", + "MapRequest", + "ReparentNotify", + "ConfigureNotify", + "ConfigureRequest", + "GravityNotify", + "ResizeRequest", + "CirculateNotify", + "CirculateRequest", + "PropertyNotify", + "SelectionClear", + "SelectionRequest", + "SelectionNotify", + "ColormapNotify", + "ClientMessage", + "MappingNotify", + "GeneralEvent", + }; + + if (e.type < 2 || e.type >= LASTEvent) { + ostringstream out; + out << "Unknown (" << e.type << ")"; + return out.str(); + } + + // 1. Compile properties we care about. + vector> properties; + switch (e.type) { + case CreateNotify: + properties.emplace_back( + "window", ToString(e.xcreatewindow.window)); + properties.emplace_back( + "parent", ToString(e.xcreatewindow.parent)); + properties.emplace_back( + "size", + Size(e.xcreatewindow.width, e.xcreatewindow.height).ToString()); + properties.emplace_back( + "position", + Position(e.xcreatewindow.x, e.xcreatewindow.y).ToString()); + properties.emplace_back( + "border_width", + ToString(e.xcreatewindow.border_width)); + properties.emplace_back( + "override_redirect", + ToString(static_cast(e.xcreatewindow.override_redirect))); + break; + case DestroyNotify: + properties.emplace_back( + "window", ToString(e.xdestroywindow.window)); + break; + case MapNotify: + properties.emplace_back( + "window", ToString(e.xmap.window)); + properties.emplace_back( + "event", ToString(e.xmap.event)); + properties.emplace_back( + "override_redirect", + ToString(static_cast(e.xmap.override_redirect))); + break; + case UnmapNotify: + properties.emplace_back( + "window", ToString(e.xunmap.window)); + properties.emplace_back( + "event", ToString(e.xunmap.event)); + properties.emplace_back( + "from_configure", + ToString(static_cast(e.xunmap.from_configure))); + break; + case ConfigureNotify: + properties.emplace_back( + "window", ToString(e.xconfigure.window)); + properties.emplace_back( + "size", + Size(e.xconfigure.width, e.xconfigure.height).ToString()); + properties.emplace_back( + "position", + Position(e.xconfigure.x, e.xconfigure.y).ToString()); + properties.emplace_back( + "border_width", + ToString(e.xconfigure.border_width)); + properties.emplace_back( + "override_redirect", + ToString(static_cast(e.xconfigure.override_redirect))); + break; + case ReparentNotify: + properties.emplace_back( + "window", ToString(e.xreparent.window)); + properties.emplace_back( + "parent", ToString(e.xreparent.parent)); + properties.emplace_back( + "position", + Position(e.xreparent.x, e.xreparent.y).ToString()); + properties.emplace_back( + "override_redirect", + ToString(static_cast(e.xreparent.override_redirect))); + break; + case MapRequest: + properties.emplace_back( + "window", ToString(e.xmaprequest.window)); + break; + case ConfigureRequest: + properties.emplace_back( + "window", ToString(e.xconfigurerequest.window)); + properties.emplace_back( + "parent", ToString(e.xconfigurerequest.parent)); + properties.emplace_back( + "value_mask", + XConfigureWindowValueMaskToString(e.xconfigurerequest.value_mask)); + properties.emplace_back( + "position", + Position(e.xconfigurerequest.x, + e.xconfigurerequest.y).ToString()); + properties.emplace_back( + "size", + Size(e.xconfigurerequest.width, + e.xconfigurerequest.height).ToString()); + properties.emplace_back( + "border_width", + ToString(e.xconfigurerequest.border_width)); + break; + case ButtonPress: + case ButtonRelease: + properties.emplace_back( + "window", ToString(e.xbutton.window)); + properties.emplace_back( + "button", ToString(e.xbutton.button)); + properties.emplace_back( + "position_root", + Position(e.xbutton.x_root, e.xbutton.y_root).ToString()); + break; + case MotionNotify: + properties.emplace_back( + "window", ToString(e.xmotion.window)); + properties.emplace_back( + "position_root", + Position(e.xmotion.x_root, e.xmotion.y_root).ToString()); + properties.emplace_back( + "state", ToString(e.xmotion.state)); + properties.emplace_back( + "time", ToString(e.xmotion.time)); + break; + case KeyPress: + case KeyRelease: + properties.emplace_back( + "window", ToString(e.xkey.window)); + properties.emplace_back( + "state", ToString(e.xkey.state)); + properties.emplace_back( + "keycode", ToString(e.xkey.keycode)); + break; + default: + // No properties are printed for unused events. + break; + } + + // 2. Build final string. + const string properties_string = Join( + properties, ", ", [] (const pair &pair) { + return pair.first + ": " + pair.second; + }); + ostringstream out; + out << X_EVENT_TYPE_NAMES[e.type] << " { " << properties_string << " }"; + return out.str(); +} + +string XConfigureWindowValueMaskToString(unsigned long value_mask) { + vector masks; + if (value_mask & CWX) { + masks.emplace_back("X"); + } + if (value_mask & CWY) { + masks.emplace_back("Y"); + } + if (value_mask & CWWidth) { + masks.emplace_back("Width"); + } + if (value_mask & CWHeight) { + masks.emplace_back("Height"); + } + if (value_mask & CWBorderWidth) { + masks.emplace_back("BorderWidth"); + } + if (value_mask & CWSibling) { + masks.emplace_back("Sibling"); + } + if (value_mask & CWStackMode) { + masks.emplace_back("StackMode"); + } + return Join(masks, "|"); +} + +string XRequestCodeToString(unsigned char request_code) { + static const char* const X_REQUEST_CODE_NAMES[] = { + "", + "CreateWindow", + "ChangeWindowAttributes", + "GetWindowAttributes", + "DestroyWindow", + "DestroySubwindows", + "ChangeSaveSet", + "ReparentWindow", + "MapWindow", + "MapSubwindows", + "UnmapWindow", + "UnmapSubwindows", + "ConfigureWindow", + "CirculateWindow", + "GetGeometry", + "QueryTree", + "InternAtom", + "GetAtomName", + "ChangeProperty", + "DeleteProperty", + "GetProperty", + "ListProperties", + "SetSelectionOwner", + "GetSelectionOwner", + "ConvertSelection", + "SendEvent", + "GrabPointer", + "UngrabPointer", + "GrabButton", + "UngrabButton", + "ChangeActivePointerGrab", + "GrabKeyboard", + "UngrabKeyboard", + "GrabKey", + "UngrabKey", + "AllowEvents", + "GrabServer", + "UngrabServer", + "QueryPointer", + "GetMotionEvents", + "TranslateCoords", + "WarpPointer", + "SetInputFocus", + "GetInputFocus", + "QueryKeymap", + "OpenFont", + "CloseFont", + "QueryFont", + "QueryTextExtents", + "ListFonts", + "ListFontsWithInfo", + "SetFontPath", + "GetFontPath", + "CreatePixmap", + "FreePixmap", + "CreateGC", + "ChangeGC", + "CopyGC", + "SetDashes", + "SetClipRectangles", + "FreeGC", + "ClearArea", + "CopyArea", + "CopyPlane", + "PolyPoint", + "PolyLine", + "PolySegment", + "PolyRectangle", + "PolyArc", + "FillPoly", + "PolyFillRectangle", + "PolyFillArc", + "PutImage", + "GetImage", + "PolyText8", + "PolyText16", + "ImageText8", + "ImageText16", + "CreateColormap", + "FreeColormap", + "CopyColormapAndFree", + "InstallColormap", + "UninstallColormap", + "ListInstalledColormaps", + "AllocColor", + "AllocNamedColor", + "AllocColorCells", + "AllocColorPlanes", + "FreeColors", + "StoreColors", + "StoreNamedColor", + "QueryColors", + "LookupColor", + "CreateCursor", + "CreateGlyphCursor", + "FreeCursor", + "RecolorCursor", + "QueryBestSize", + "QueryExtension", + "ListExtensions", + "ChangeKeyboardMapping", + "GetKeyboardMapping", + "ChangeKeyboardControl", + "GetKeyboardControl", + "Bell", + "ChangePointerControl", + "GetPointerControl", + "SetScreenSaver", + "GetScreenSaver", + "ChangeHosts", + "ListHosts", + "SetAccessControl", + "SetCloseDownMode", + "KillClient", + "RotateProperties", + "ForceScreenSaver", + "SetPointerMapping", + "GetPointerMapping", + "SetModifierMapping", + "GetModifierMapping", + "NoOperation", + }; + return X_REQUEST_CODE_NAMES[request_code]; +} diff --git a/plugins/launcher/home/util.hpp b/plugins/launcher/home/util.hpp new file mode 100644 index 00000000..0cbf764e --- /dev/null +++ b/plugins/launcher/home/util.hpp @@ -0,0 +1,241 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2013-2017 Chuan Ji * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef UTIL_HPP +#define UTIL_HPP + +extern "C" { +#include +} +#include +#include + +// Represents a 2D size. +template +struct Size { + T width, height; + + Size() = default; + Size(T w, T h) + : width(w), height(h) { + } + + ::std::string ToString() const; +}; + +// Outputs a Size as a string to a std::ostream. +template +::std::ostream& operator << (::std::ostream& out, const Size& size); + +// Represents a 2D position. +template +struct Position { + T x, y; + + Position() = default; + Position(T _x, T _y) + : x(_x), y(_y) { + } + + ::std::string ToString() const; +}; + +// Represents a 2D vector. +template +struct Vector2D { + T x, y; + + Vector2D() = default; + Vector2D(T _x, T _y) + : x(_x), y(_y) { + } + + ::std::string ToString() const; +}; + +// Outputs a Size as a string to a std::ostream. +template +::std::ostream& operator << (::std::ostream& out, const Position& pos); + +// Position operators. +template +Vector2D operator - (const Position& a, const Position& b); +template +Position operator + (const Position& a, const Vector2D &v); +template +Position operator + (const Vector2D &v, const Position& a); +template +Position operator - (const Position& a, const Vector2D &v); + +// Size operators. +template +Vector2D operator - (const Size& a, const Size& b); +template +Size operator + (const Size& a, const Vector2D &v); +template +Size operator + (const Vector2D &v, const Size& a); +template +Size operator - (const Size& a, const Vector2D &v); + +// Joins a container of elements into a single string, with elements separated +// by a delimiter. Any element can be used as long as an operator << on ostream +// is defined. +template +::std::string Join(const Container& container, const ::std::string& delimiter); + +// Joins a container of elements into a single string, with elements separated +// by a delimiter. The elements are converted to string using a converter +// function. +template +::std::string Join( + const Container& container, + const ::std::string& delimiter, + Converter converter); + +// Returns a string representation of a built-in type that we already have +// ostream support for. +template +::std::string ToString(const T& x); + +// Returns a string describing an X event for debugging purposes. +extern ::std::string ToString(const XEvent& e); + +// Returns a string describing an X window configuration value mask. +extern ::std::string XConfigureWindowValueMaskToString(unsigned long value_mask); + +// Returns the name of an X request code. +extern ::std::string XRequestCodeToString(unsigned char request_code); + + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * IMPLEMENTATION * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +#include +#include +#include + +template +::std::string Size::ToString() const { + ::std::ostringstream out; + out << width << 'x' << height; + return out.str(); +} + +template +::std::ostream& operator << (::std::ostream& out, const Size& size) { + return out << size.ToString(); +} + +template +::std::string Position::ToString() const { + ::std::ostringstream out; + out << "(" << x << ", " << y << ")"; + return out.str(); +} + +template +::std::ostream& operator << (::std::ostream& out, const Position& size) { + return out << size.ToString(); +} + +template +::std::string Vector2D::ToString() const { + ::std::ostringstream out; + out << "(" << x << ", " << y << ")"; + return out.str(); +} + +template +::std::ostream& operator << (::std::ostream& out, const Vector2D& size) { + return out << size.ToString(); +} + +template +Vector2D operator - (const Position& a, const Position& b) { + return Vector2D(a.x - b.x, a.y - b.y); +} + +template +Position operator + (const Position& a, const Vector2D &v) { + return Position(a.x + v.x, a.y + v.y); +} + +template +Position operator + (const Vector2D &v, const Position& a) { + return Position(a.x + v.x, a.y + v.y); +} + +template +Position operator - (const Position& a, const Vector2D &v) { + return Position(a.x - v.x, a.y - v.y); +} + +template +Vector2D operator - (const Size& a, const Size& b) { + return Vector2D(a.width - b.width, a.height - b.height); +} + +template +Size operator + (const Size& a, const Vector2D &v) { + return Size(a.width + v.x, a.height + v.y); +} + +template +Size operator + (const Vector2D &v, const Size& a) { + return Size(a.width + v.x, a.height + v.y); +} + +template +Size operator - (const Size& a, const Vector2D &v) { + return Size(a.width - v.x, a.height - v.y); +} + +template +::std::string Join(const Container& container, const ::std::string& delimiter) { + ::std::ostringstream out; + for (auto i = container.cbegin(); i != container.cend(); ++i) { + if (i != container.cbegin()) { + out << delimiter; + } + out << *i; + } + return out.str(); +} + +template +::std::string Join( + const Container& container, + const ::std::string& delimiter, + Converter converter) { + ::std::vector<::std::string> converted_container(container.size()); + ::std::transform( + container.cbegin(), + container.cend(), + converted_container.begin(), + converter); + return Join(converted_container, delimiter); +} + +template +::std::string ToString(const T& x) { + ::std::ostringstream out; + out << x; + return out.str(); +} + +#endif diff --git a/plugins/launcher/home/window_manager.cpp b/plugins/launcher/home/window_manager.cpp new file mode 100644 index 00000000..9bc1e6d5 --- /dev/null +++ b/plugins/launcher/home/window_manager.cpp @@ -0,0 +1,483 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2013-2017 Chuan Ji * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "window_manager.hpp" +extern "C" { +#include +} +#include +#include +#include +#include "util.hpp" + +using ::std::max; +using ::std::mutex; +using ::std::string; +using ::std::unique_ptr; + +bool WindowManager::wm_detected_; +mutex WindowManager::wm_detected_mutex_; + +WindowManager* WindowManager::Create(const string& display_str) { + // 1. Open X display. + const char* display_c_str = + display_str.empty() ? nullptr : display_str.c_str(); + Display* display = XOpenDisplay(display_c_str); + if (display == nullptr) { + qCritical() << "Failed to open X display " << XDisplayName(display_c_str); + return nullptr; + } + // 2. Construct WindowManager instance. + return new WindowManager(display); +} + +WindowManager::WindowManager(Display* display) + : display_(CHECK_NOTNULL(display)), + root_(DefaultRootWindow(display_)), + WM_PROTOCOLS(XInternAtom(display_, "WM_PROTOCOLS", false)), + WM_DELETE_WINDOW(XInternAtom(display_, "WM_DELETE_WINDOW", false)) { +} + +WindowManager::~WindowManager() { + XCloseDisplay(display_); +} + +void WindowManager::Run() { + // 1. Initialization. + // a. Select events on root window. Use a special error handler so we can + // exit gracefully if another window manager is already running. + { + ::std::lock_guard lock(wm_detected_mutex_); + + wm_detected_ = false; + XSetErrorHandler(&WindowManager::OnWMDetected); + XSelectInput( + display_, + root_, + SubstructureRedirectMask | SubstructureNotifyMask); + XSync(display_, false); + if (wm_detected_) { + qDebug() << "Detected another window manager on display " + << XDisplayString(display_); + return; + } else { + this->isRunning = true; + qDebug() << "No window manager running, proceeding..."; + } + } + // b. Set error handler. + XSetErrorHandler(&WindowManager::OnXError); + // c. Grab X server to prevent windows from changing under us. + XGrabServer(display_); + // d. Reparent existing top-level windows. + // i. Query existing top-level windows. + Window returned_root, returned_parent; + Window* top_level_windows; + unsigned int num_top_level_windows; + XQueryTree(display_, root_, &returned_root, &returned_parent, &top_level_windows, &num_top_level_windows); + if(returned_root != root_) qDebug() << "[ERROR] root mismatch"; + // ii. Frame each top-level window. + for (unsigned int i = 0; i < num_top_level_windows; ++i) { + Frame(top_level_windows[i], true); + } + // iii. Free top-level window array. + XFree(top_level_windows); + // e. Ungrab X server. + XUngrabServer(display_); + + // 2. Main event loop. + for (;;) { + // 1. Get next event. + XEvent e; + XNextEvent(display_, &e); + LOG(INFO) << "Received event: " << ToString(e); + QMessageBox msgBox; + QString qstr = QString::fromStdString(ToString(e)); + msgBox.setText("Received event: " + qstr); + msgBox.exec(); + + // 2. Dispatch event. + switch (e.type) { + case CreateNotify: + OnCreateNotify(e.xcreatewindow); + break; + case DestroyNotify: + OnDestroyNotify(e.xdestroywindow); + break; + case ReparentNotify: + OnReparentNotify(e.xreparent); + break; + case MapNotify: + OnMapNotify(e.xmap); + break; + case UnmapNotify: + OnUnmapNotify(e.xunmap); + break; + case ConfigureNotify: + OnConfigureNotify(e.xconfigure); + break; + case MapRequest: + OnMapRequest(e.xmaprequest); + break; + case ConfigureRequest: + OnConfigureRequest(e.xconfigurerequest); + break; + case ButtonPress: + OnButtonPress(e.xbutton); + break; + case ButtonRelease: + OnButtonRelease(e.xbutton); + break; + case MotionNotify: + // Skip any already pending motion events. + while (XCheckTypedWindowEvent( + display_, e.xmotion.window, MotionNotify, &e)) {} + OnMotionNotify(e.xmotion); + break; + case KeyPress: + OnKeyPress(e.xkey); + break; + case KeyRelease: + OnKeyRelease(e.xkey); + break; + default: + LOG(WARNING) << "Ignored event"; + } + } +} + +void WindowManager::Frame(Window w, bool was_created_before_window_manager) { + // Visual properties of the frame to create. + const unsigned int BORDER_WIDTH = 3; + const unsigned long BORDER_COLOR = 0xff0000; + const unsigned long BG_COLOR = 0x0000ff; + + // We shouldn't be framing windows we've already framed. + CHECK(!clients_.count(w)); + + // 1. Retrieve attributes of window to frame. + XWindowAttributes x_window_attrs; + CHECK(XGetWindowAttributes(display_, w, &x_window_attrs)); + + // 2. If window was created before window manager started, we should frame + // it only if it is visible and doesn't set override_redirect. + if (was_created_before_window_manager) { + if (x_window_attrs.override_redirect || + x_window_attrs.map_state != IsViewable) { + return; + } + } + + // 3. Create frame. + const Window frame = XCreateSimpleWindow( + display_, + root_, + x_window_attrs.x, + x_window_attrs.y, + x_window_attrs.width, + x_window_attrs.height, + BORDER_WIDTH, + BORDER_COLOR, + BG_COLOR); + // 4. Select events on frame. + XSelectInput( + display_, + frame, + SubstructureRedirectMask | SubstructureNotifyMask); + // 5. Add client to save set, so that it will be restored and kept alive if we + // crash. + XAddToSaveSet(display_, w); + // 6. Reparent client window. + XReparentWindow( + display_, + w, + frame, + 0, 0); // Offset of client window within frame. + // 7. Map frame. + XMapWindow(display_, frame); + // 8. Save frame handle. + clients_[w] = frame; + // 9. Grab universal window management actions on client window. + // a. Move windows with alt + left button. + XGrabButton( + display_, + Button1, + Mod1Mask, + w, + false, + ButtonPressMask | ButtonReleaseMask | ButtonMotionMask, + GrabModeAsync, + GrabModeAsync, + None, + None); + // b. Resize windows with alt + right button. + XGrabButton( + display_, + Button3, + Mod1Mask, + w, + false, + ButtonPressMask | ButtonReleaseMask | ButtonMotionMask, + GrabModeAsync, + GrabModeAsync, + None, + None); + // c. Kill windows with alt + f4. + XGrabKey( + display_, + XKeysymToKeycode(display_, XK_F4), + Mod1Mask, + w, + false, + GrabModeAsync, + GrabModeAsync); + // d. Switch windows with alt + tab. + XGrabKey( + display_, + XKeysymToKeycode(display_, XK_Tab), + Mod1Mask, + w, + false, + GrabModeAsync, + GrabModeAsync); + + LOG(INFO) << "Framed window " << w << " [" << frame << "]"; +} + +void WindowManager::Unframe(Window w) { + CHECK(clients_.count(w)); + + // We reverse the steps taken in Frame(). + const Window frame = clients_[w]; + // 1. Unmap frame. + XUnmapWindow(display_, frame); + // 2. Reparent client window. + XReparentWindow( + display_, + w, + root_, + 0, 0); // Offset of client window within root. + // 3. Remove client window from save set, as it is now unrelated to us. + XRemoveFromSaveSet(display_, w); + // 4. Destroy frame. + XDestroyWindow(display_, frame); + // 5. Drop reference to frame handle. + clients_.erase(w); + + LOG(INFO) << "Unframed window " << w << " [" << frame << "]"; +} + +void WindowManager::OnCreateNotify(const XCreateWindowEvent& e) {} + +void WindowManager::OnDestroyNotify(const XDestroyWindowEvent& e) {} + +void WindowManager::OnReparentNotify(const XReparentEvent& e) {} + +void WindowManager::OnMapNotify(const XMapEvent& e) {} + +void WindowManager::OnUnmapNotify(const XUnmapEvent& e) { + // If the window is a client window we manage, unframe it upon UnmapNotify. We + // need the check because we will receive an UnmapNotify event for a frame + // window we just destroyed ourselves. + if (!clients_.count(e.window)) { + LOG(INFO) << "Ignore UnmapNotify for non-client window " << e.window; + return; + } + + // Ignore event if it is triggered by reparenting a window that was mapped + // before the window manager started. + // + // Since we receive UnmapNotify events from the SubstructureNotify mask, the + // event attribute specifies the parent window of the window that was + // unmapped. This means that an UnmapNotify event from a normal client window + // should have this attribute set to a frame window we maintain. Only an + // UnmapNotify event triggered by reparenting a pre-existing window will have + // this attribute set to the root window. + if (e.event == root_) { + LOG(INFO) << "Ignore UnmapNotify for reparented pre-existing window " + << e.window; + return; + } + + Unframe(e.window); +} + +void WindowManager::OnConfigureNotify(const XConfigureEvent& e) {} + +void WindowManager::OnMapRequest(const XMapRequestEvent& e) { + // 1. Frame or re-frame window. + Frame(e.window, false); + // 2. Actually map window. + XMapWindow(display_, e.window); +} + +void WindowManager::OnConfigureRequest(const XConfigureRequestEvent& e) { + XWindowChanges changes; + changes.x = e.x; + changes.y = e.y; + changes.width = e.width; + changes.height = e.height; + changes.border_width = e.border_width; + changes.sibling = e.above; + changes.stack_mode = e.detail; + if (clients_.count(e.window)) { + const Window frame = clients_[e.window]; + XConfigureWindow(display_, frame, e.value_mask, &changes); + LOG(INFO) << "Resize [" << frame << "] to " << Size(e.width, e.height); + } + XConfigureWindow(display_, e.window, e.value_mask, &changes); + LOG(INFO) << "Resize " << e.window << " to " << Size(e.width, e.height); +} + +void WindowManager::OnButtonPress(const XButtonEvent& e) { + CHECK(clients_.count(e.window)); + const Window frame = clients_[e.window]; + + // 1. Save initial cursor position. + drag_start_pos_ = Position(e.x_root, e.y_root); + + // 2. Save initial window info. + Window returned_root; + int x, y; + unsigned width, height, border_width, depth; + CHECK(XGetGeometry( + display_, + frame, + &returned_root, + &x, &y, + &width, &height, + &border_width, + &depth)); + drag_start_frame_pos_ = Position(x, y); + drag_start_frame_size_ = Size(width, height); + + // 3. Raise clicked window to top. + XRaiseWindow(display_, frame); +} + +void WindowManager::OnButtonRelease(const XButtonEvent& e) {} + +void WindowManager::OnMotionNotify(const XMotionEvent& e) { + CHECK(clients_.count(e.window)); + const Window frame = clients_[e.window]; + const Position drag_pos(e.x_root, e.y_root); + const Vector2D delta = drag_pos - drag_start_pos_; + + if (e.state & Button1Mask ) { + // alt + left button: Move window. + const Position dest_frame_pos = drag_start_frame_pos_ + delta; + XMoveWindow( + display_, + frame, + dest_frame_pos.x, dest_frame_pos.y); + } else if (e.state & Button3Mask) { + // alt + right button: Resize window. + // Window dimensions cannot be negative. + const Vector2D size_delta( + max(delta.x, -drag_start_frame_size_.width), + max(delta.y, -drag_start_frame_size_.height)); + const Size dest_frame_size = drag_start_frame_size_ + size_delta; + // 1. Resize frame. + XResizeWindow( + display_, + frame, + dest_frame_size.width, dest_frame_size.height); + // 2. Resize client window. + XResizeWindow( + display_, + e.window, + dest_frame_size.width, dest_frame_size.height); + } +} + +void WindowManager::OnKeyPress(const XKeyEvent& e) { + if ((e.state & Mod1Mask) && + (e.keycode == XKeysymToKeycode(display_, XK_F4))) { + // alt + f4: Close window. + // + // There are two ways to tell an X window to close. The first is to send it + // a message of type WM_PROTOCOLS and value WM_DELETE_WINDOW. If the client + // has not explicitly marked itself as supporting this more civilized + // behavior (using XSetWMProtocols()), we kill it with XKillClient(). + Atom* supported_protocols; + int num_supported_protocols; + if (XGetWMProtocols(display_, + e.window, + &supported_protocols, + &num_supported_protocols) && + (::std::find(supported_protocols, + supported_protocols + num_supported_protocols, + WM_DELETE_WINDOW) != + supported_protocols + num_supported_protocols)) { + LOG(INFO) << "Gracefully deleting window " << e.window; + // 1. Construct message. + XEvent msg; + memset(&msg, 0, sizeof(msg)); + msg.xclient.type = ClientMessage; + msg.xclient.message_type = WM_PROTOCOLS; + msg.xclient.window = e.window; + msg.xclient.format = 32; + msg.xclient.data.l[0] = WM_DELETE_WINDOW; + // 2. Send message to window to be closed. + CHECK(XSendEvent(display_, e.window, false, 0, &msg)); + } else { + LOG(INFO) << "Killing window " << e.window; + XKillClient(display_, e.window); + } + } else if ((e.state & Mod1Mask) && + (e.keycode == XKeysymToKeycode(display_, XK_Tab))) { + // alt + tab: Switch window. + // 1. Find next window. + auto i = clients_.find(e.window); + CHECK(i != clients_.end()); + ++i; + if (i == clients_.end()) { + i = clients_.begin(); + } + // 2. Raise and set focus. + XRaiseWindow(display_, i->second); + XSetInputFocus(display_, i->first, RevertToPointerRoot, CurrentTime); + } +} + +void WindowManager::OnKeyRelease(const XKeyEvent& e) {} + +int WindowManager::OnXError(Display* display, XErrorEvent* e) { + const int MAX_ERROR_TEXT_LENGTH = 1024; + char error_text[MAX_ERROR_TEXT_LENGTH]; + XGetErrorText(display, e->error_code, error_text, sizeof(error_text)); + LOG(ERROR) << "Received X error:\n" + << " Request: " << int(e->request_code) + << " - " << XRequestCodeToString(e->request_code) << "\n" + << " Error code: " << int(e->error_code) + << " - " << error_text << "\n" + << " Resource ID: " << e->resourceid; + // The return value is ignored. + return 0; +} + +int WindowManager::OnWMDetected(Display* display, XErrorEvent* e) { + // In the case of an already running window manager, the error code from + // XSelectInput is BadAccess. We don't expect this handler to receive any + // other errors. + CHECK_EQ(static_cast(e->error_code), BadAccess); + // Set flag. + wm_detected_ = true; + // The return value is ignored. + return 0; +} diff --git a/plugins/launcher/home/window_manager.hpp b/plugins/launcher/home/window_manager.hpp new file mode 100644 index 00000000..6493f9d9 --- /dev/null +++ b/plugins/launcher/home/window_manager.hpp @@ -0,0 +1,116 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2013-2017 Chuan Ji * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +#pragma once + +#include +#include +#include +#include +#include + +extern "C" { +#include +} +#include +#include +#include +#include +#include "util.hpp" + + +// Implementation of a window manager for an X screen. +class WindowManager : QObject { + Q_OBJECT + + public: + // Creates a WindowManager instance for the X display/screen specified by the + // argument string, or if unspecified, the DISPLAY environment variable. On + // failure, returns nullptr. + static WindowManager* Create(const std::string& display_str = std::string()); + ~WindowManager(); + // The entry point to this class. Enters the main event loop. + void Run(); + bool isRunning = false; + + public slots: + + signals: + + void OnCreateSignal(const XCreateWindowEvent& e); + void OnDestroySignal(const XDestroyWindowEvent& e); + void OnReparentSignal(const XReparentEvent& e); + void OnMapSignal(const XMapEvent& e); + void OnUnmapSignal(const XUnmapEvent& e); + + private: + // Invoked internally by Create(). + WindowManager(Display* display); + // Frames a top-level window. + void Frame(Window w, bool was_created_before_window_manager); + // Unframes a client window. + void Unframe(Window w); + + // Event handlers. + void OnCreateNotify(const XCreateWindowEvent& e); + void OnDestroyNotify(const XDestroyWindowEvent& e); + void OnReparentNotify(const XReparentEvent& e); + void OnMapNotify(const XMapEvent& e); + void OnUnmapNotify(const XUnmapEvent& e); + void OnConfigureNotify(const XConfigureEvent& e); + void OnMapRequest(const XMapRequestEvent& e); + void OnConfigureRequest(const XConfigureRequestEvent& e); + void OnButtonPress(const XButtonEvent& e); + void OnButtonRelease(const XButtonEvent& e); + void OnMotionNotify(const XMotionEvent& e); + void OnKeyPress(const XKeyEvent& e); + void OnKeyRelease(const XKeyEvent& e); + + // Xlib error handler. It must be static as its address is passed to Xlib. + static int OnXError(Display* display, XErrorEvent* e); + // Xlib error handler used to determine whether another window manager is + // running. It is set as the error handler right before selecting substructure + // redirection mask on the root window, so it is invoked if and only if + // another window manager is running. It must be static as its address is + // passed to Xlib. + static int OnWMDetected(Display* display, XErrorEvent* e); + // Whether an existing window manager has been detected. Set by OnWMDetected, + // and hence must be static. + static bool wm_detected_; + // A mutex for protecting wm_detected_. It's not strictly speaking needed as + // this program is single threaded, but better safe than sorry. + static ::std::mutex wm_detected_mutex_; + + // Handle to the underlying Xlib Display struct. + Display* display_; + // Handle to root window. + const Window root_; + // Maps top-level windows to their frame windows. + ::std::unordered_map clients_; + + // The cursor position at the start of a window move/resize. + Position drag_start_pos_; + // The position of the affected window at the start of a window + // move/resize. + Position drag_start_frame_pos_; + // The size of the affected window at the start of a window move/resize. + Size drag_start_frame_size_; + + // Atom constants. + const Atom WM_PROTOCOLS; + const Atom WM_DELETE_WINDOW; +}; diff --git a/plugins/radio/rtl_sdr/rtl_sdr.hpp b/plugins/radio/rtl_sdr/rtl_sdr.hpp index 319c7730..37190d12 100644 --- a/plugins/radio/rtl_sdr/rtl_sdr.hpp +++ b/plugins/radio/rtl_sdr/rtl_sdr.hpp @@ -6,7 +6,7 @@ #include "plugins/radio_plugin.hpp" -class RtlSdr : public QObject, RadioPlugin { +class RtlSdr : public RadioPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID RadioPlugin_iid FILE "rtl_sdr.json") Q_INTERFACES(RadioPlugin) diff --git a/plugins/vehicle/test/test.hpp b/plugins/vehicle/test/test.hpp index 36c53c7e..a5a1c054 100644 --- a/plugins/vehicle/test/test.hpp +++ b/plugins/vehicle/test/test.hpp @@ -5,7 +5,7 @@ #include "app/widgets/climate.hpp" #include "canbus/socketcanbus.hpp" -class Test : public QObject, VehiclePlugin { +class Test : public VehiclePlugin { Q_OBJECT Q_PLUGIN_METADATA(IID VehiclePlugin_iid FILE "test.json") Q_INTERFACES(VehiclePlugin) diff --git a/src/app/pages/launcher.cpp b/src/app/pages/launcher.cpp index d2bc5204..14e5f772 100644 --- a/src/app/pages/launcher.cpp +++ b/src/app/pages/launcher.cpp @@ -27,16 +27,21 @@ LauncherPlugins::LauncherPlugins(Arbiter &arbiter, QWidget *parent) if (!key.isNull()) { auto plugin_loader = new QPluginLoader(this); plugin_loader->setFileName(this->plugins[key].absoluteFilePath()); - if (LauncherPlugin *plugin = qobject_cast(plugin_loader->instance())) { plugin->dashize(&this->arbiter); for (QWidget *tab : plugin->widgets()) this->addTab(tab, tab->objectName()); + + connect(plugin, &LauncherPlugin::widget_added, [this](QWidget *tab){ + this->addTab(tab, tab->objectName()); + }); + this->active_plugins.append(plugin_loader); this->active_plugins_list->addItem(key); this->config->set_launcher_plugin(key); } else { + if(!plugin_loader->isLoaded()) qDebug() << plugin_loader->errorString(); delete plugin_loader; } } @@ -57,6 +62,11 @@ LauncherPlugins::LauncherPlugins(Arbiter &arbiter, QWidget *parent) plugin->dashize(&this->arbiter); for (QWidget *tab : plugin->widgets()) this->addTab(tab, tab->objectName()); + + connect(plugin, &LauncherPlugin::widget_added, [this](QWidget *tab){ + this->addTab(tab, tab->objectName()); + + }); this->active_plugins.append(plugin_loader); this->active_plugins_list->addItem(launcher_plugin); }