diff --git a/repos/gems/run/mixer_gui_qt_test.run b/repos/gems/run/mixer_gui_qt_test.run
new file mode 100644
index 0000000000..2390a932da
--- /dev/null
+++ b/repos/gems/run/mixer_gui_qt_test.run
@@ -0,0 +1,263 @@
+#
+# Build
+#
+
+if {![have_spec linux]} {
+ puts "This run script requires linux!"
+ exit 1
+}
+
+set build_components {
+ core init
+ drivers/timer
+ server/ram_fs
+ drivers/framebuffer
+ server/dynamic_rom
+ server/report_rom
+ server/nitpicker
+ server/fs_rom
+ server/wm
+ app/pointer
+ app/floating_window_layouter
+ app/decorator
+ app/mixer_gui_qt
+}
+
+source ${genode_dir}/repos/base/run/platform_drv.inc
+append_platform_drv_build_components
+
+build $build_components
+
+create_boot_directory
+
+#
+# Generate config
+#
+
+set config {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+
+append_platform_drv_config
+
+append_if [have_spec sdl] config {
+
+
+
+
+
+
+ }
+
+append config {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+
+append config {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+
+append config {
+
+}
+
+install_config $config
+
+#
+# Prepare resources needed by the application
+#
+
+# get fonts
+exec rm -rf bin/qt5_fs/mixer_gui_qt/qt
+exec mkdir -p bin/qt5_fs/mixer_gui_qt/qt/lib
+exec ln -sf [pwd]/bin/qt5_fs/qt/lib/fonts bin/qt5_fs/mixer_gui_qt/qt/lib/fonts
+
+# create tar archive containg Qt5 resources
+exec tar chf bin/qt5_fs_mixer_gui_qt.tar -C bin/qt5_fs/mixer_gui_qt .
+
+#
+# Boot modules
+#
+
+set boot_modules {
+ core init timer
+ ld.lib.so libc.lib.so
+
+ report_rom dynamic_rom ram_fs
+ fs_rom
+
+ qt5_gui.lib.so
+ qt5_widgets.lib.so
+ qt5_xml.lib.so
+ qt5_core.lib.so
+ freetype.lib.so
+ gallium.lib.so
+ icu.lib.so
+ libc_lock_pipe.lib.so
+ libm.lib.so
+ libpng.lib.so
+ jpeg.lib.so
+ zlib.lib.so
+ stdcxx.lib.so
+ pthread.lib.so
+ mixer_gui_qt
+ qt5_fs_mixer_gui_qt.tar
+ nitpicker
+ wm
+ pointer
+ floating_window_layouter
+ decorator
+}
+
+append_platform_drv_boot_modules
+
+lappend_if [have_spec linux] boot_modules fb_sdl
+
+build_boot_image $boot_modules
+
+run_genode_until forever
+
+# vi: set ft=tcl :
diff --git a/repos/gems/src/app/mixer_gui_qt/main.cpp b/repos/gems/src/app/mixer_gui_qt/main.cpp
new file mode 100644
index 0000000000..c8f0b41e3e
--- /dev/null
+++ b/repos/gems/src/app/mixer_gui_qt/main.cpp
@@ -0,0 +1,115 @@
+/*
+ * \brief Mixer frontend
+ * \author Josef Soentgen
+ * \date 2015-10-15
+ */
+
+/* Genode includes */
+#include
+#include
+#include
+
+/* Qt includes */
+#include
+#include
+#include
+
+/* application includes */
+#include "main_window.h"
+
+
+enum { THREAD_STACK_SIZE = 2 * 1024 * sizeof(long) };
+
+
+struct Report_thread : Genode::Thread
+{
+ QMember proxy;
+
+ Genode::Attached_rom_dataspace channels_rom { "channel_list" };
+
+ Genode::Signal_receiver sig_rec;
+ Genode::Signal_dispatcher channels_dispatcher;
+
+ Genode::Lock _report_lock { Genode::Lock::LOCKED };
+
+ void _report(char const *data, size_t size)
+ {
+ Genode::Xml_node node(data, size);
+ proxy->report_changed(&_report_lock, &node);
+
+ /* wait until the report was handled */
+ _report_lock.lock();
+ }
+
+ void _handle_channels(unsigned)
+ {
+ channels_rom.update();
+ _report(channels_rom.local_addr(), channels_rom.size());
+ }
+
+ Report_thread()
+ :
+ Genode::Thread("report_thread"),
+ channels_dispatcher(sig_rec, *this, &Report_thread::_handle_channels)
+ {
+ channels_rom.sigh(channels_dispatcher);
+ }
+
+ void entry() override
+ {
+ using namespace Genode;
+ while (true) {
+ Signal sig = sig_rec.wait_for_signal();
+ int num = sig.num();
+
+ Signal_dispatcher_base *dispatcher;
+ dispatcher = dynamic_cast(sig.context());
+ dispatcher->dispatch(num);
+ }
+ }
+
+ void connect_window(Main_window *win)
+ {
+ QObject::connect(proxy, SIGNAL(report_changed(void *,void const*)),
+ win, SLOT(report_changed(void *, void const*)),
+ Qt::QueuedConnection);
+ }
+};
+
+
+static inline void load_stylesheet()
+{
+ QFile file(":style.qss");
+ if (!file.open(QFile::ReadOnly)) {
+ qWarning() << "Warning:" << file.errorString()
+ << "opening file" << file.fileName();
+ return;
+ }
+
+ qApp->setStyleSheet(QLatin1String(file.readAll()));
+}
+
+
+int main(int argc, char *argv[])
+{
+ Report_thread *report_thread;
+ try { report_thread = new Report_thread(); }
+ catch (...) {
+ PERR("Could not create Report_thread");
+ return -1;
+ }
+
+ QApplication app(argc, argv);
+
+ load_stylesheet();
+
+ QMember main_window;
+ main_window->show();
+
+ report_thread->connect_window(main_window);
+ report_thread->start();
+
+ app.connect(&app, SIGNAL(lastWindowClosed()), SLOT(quit()));
+
+ return app.exec();
+}
diff --git a/repos/gems/src/app/mixer_gui_qt/main_window.cpp b/repos/gems/src/app/mixer_gui_qt/main_window.cpp
new file mode 100644
index 0000000000..fffcea19a0
--- /dev/null
+++ b/repos/gems/src/app/mixer_gui_qt/main_window.cpp
@@ -0,0 +1,415 @@
+/*
+ * \brief Main window of the mixer frontend
+ * \author Josef Soentgen
+ * \date 2015-10-15
+ */
+
+/*
+ * Copyright (C) 2015 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU General Public License version 2.
+ */
+
+/* Genode includes */
+#include
+#include
+#include
+#include
+#include
+
+/* Qt includes */
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/* application includes */
+#include "main_window.h"
+
+
+/************
+ ** helper **
+ ************/
+
+typedef Mixer::Channel Channel;
+
+static struct Names {
+ char const *name;
+ Channel::Number number;
+} names[] = {
+ { "left", Channel::Number::LEFT },
+ { "front left", Channel::Number::LEFT },
+ { "right", Channel::Number::RIGHT },
+ { "front right", Channel::Number::RIGHT },
+ { nullptr, Channel::Number::MAX_CHANNELS }
+};
+
+
+static char const *channel_string_from_number(Channel::Number ch)
+{
+ for (Names *n = names; n->name; ++n)
+ if (ch == n->number)
+ return n->name;
+ return nullptr;
+}
+
+
+/* keep sorted! */
+static struct Types {
+ char const *name;
+ Channel::Type type;
+} types[] = {
+ { "invalid", Channel::Type::TYPE_INVALID },
+ { "input", Channel::Type::INPUT },
+ { "output", Channel::Type::OUTPUT }
+};
+
+
+static char const *type_to_string(Channel::Type t) { return types[t].name; }
+
+
+class Channel_widget : public Compound_widget,
+ public Genode::List::Element
+{
+ Q_OBJECT
+
+ private:
+
+ Channel::Number _number;
+ Channel::Type _type;
+
+ QCheckBox _muted_checkbox;
+ QSlider _slider { Qt::Vertical };
+ QHBoxLayout _slider_hbox;
+
+ Q_SIGNALS:
+
+ void channel_changed();
+
+ public:
+
+ Channel_widget(Channel::Type type, Channel::Number number)
+ :
+ _number(number), _type(type),
+ _muted_checkbox("mute")
+ {
+ _slider.setMinimum(Channel::Volume_level::MIN);
+ _slider.setMaximum(Channel::Volume_level::MAX);
+
+ _slider_hbox.addStretch();
+ _slider_hbox.addWidget(&_slider, Qt::AlignCenter);
+ _slider_hbox.addStretch();
+
+ _layout->addLayout(&_slider_hbox);
+ _layout->addWidget(&_muted_checkbox);
+
+ connect(&_slider, SIGNAL(sliderReleased()),
+ this, SIGNAL(channel_changed()));
+ connect(&_muted_checkbox, SIGNAL(stateChanged(int)),
+ this, SIGNAL(channel_changed()));
+ }
+
+ Channel::Number number() const { return _number; }
+ Channel::Type type() const { return _type; }
+ int volume() const { return _slider.value(); }
+ void volume(int v) { _slider.setValue(v); }
+ bool muted() const { return _muted_checkbox.checkState() == Qt::Checked; }
+ void muted(bool v) { _muted_checkbox.setChecked(v); }
+};
+
+
+class Client_widget : public Compound_widget,
+ public Genode::List::Element
+{
+ Q_OBJECT
+
+ public:
+
+ bool valid { true };
+
+ void _sorted_insert(Channel_widget *cw)
+ {
+ Channel::Number const nr = cw->number();
+
+ Channel_widget const *last = nullptr;
+ Channel_widget const *w = _list.first();
+ for (; w; w = w->next()) {
+ if (w->number() > nr)
+ break;
+ last = w;
+ }
+ _list.insert(cw, last);
+ }
+
+ private:
+
+ Genode::List _list;
+ Genode::Allocator &_alloc;
+ Channel::Label _label;
+
+ QLabel _name;
+ QHBoxLayout _hlayout;
+
+ static char const *_strip_label(Channel::Label const &label)
+ {
+ char const * str = label.string();
+ int pos = 0;
+ for (int i = 0; str[i]; i++)
+ if (str[i] == '>') pos = i+1;
+
+ return str+pos;
+ }
+
+ Q_SIGNALS:
+
+ void client_changed();
+
+ public:
+
+ Client_widget(Genode::Allocator &alloc, Channel::Label const &label)
+ :
+ _alloc(alloc), _label(label),
+ _name(_strip_label(_label))
+ {
+ setFrameStyle(QFrame::Panel | QFrame::Raised);
+ setLineWidth(4);
+ setToolTip(_label.string());
+
+ _name.setAlignment(Qt::AlignCenter);
+ _name.setContentsMargins(0, 0, 0, 5);
+
+ _layout->addWidget(&_name);
+ _layout->addLayout(&_hlayout);
+ _layout->setContentsMargins(10, 10, 10, 10);
+ }
+
+ ~Client_widget()
+ {
+ while (Channel_widget *ch = _list.first()) {
+ disconnect(ch, SIGNAL(channel_changed()));
+ _hlayout.removeWidget(ch);
+ _list.remove(ch);
+ Genode::destroy(&_alloc, ch);
+ }
+ }
+
+ Channel::Label const &label() const { return _label; }
+
+ Channel_widget* lookup_channel(Channel::Number const number)
+ {
+ for (Channel_widget *ch = _list.first(); ch; ch = ch->next())
+ if (number == ch->number())
+ return ch;
+ return nullptr;
+ }
+
+ Channel_widget* add_channel(Channel::Type const type,
+ Channel::Number const number)
+ {
+ Channel_widget *ch = new (&_alloc) Channel_widget(type, number);
+ connect(ch, SIGNAL(channel_changed()),
+ this, SIGNAL(client_changed()));
+
+ _sorted_insert(ch);
+ _hlayout.addWidget(ch);
+
+ return ch;
+ }
+
+ Channel_widget const* first_channel() const { return _list.first(); }
+
+ void only_show_first()
+ {
+ Channel_widget *cw = _list.first();
+ while ((cw = cw->next())) cw->hide();
+ }
+
+ bool combined_control() const
+ {
+ /*
+ * Having a seperate volume control widget for each channel is
+ * nice-to-have but for now it is unnecessary. We therefore disable
+ * it the hardcoded way.
+ */
+ return true;
+ }
+};
+
+
+class Client_widget_registry : public QObject
+{
+ Q_OBJECT
+
+ private:
+
+ Genode::List _list;
+ Genode::Allocator &_alloc;
+
+ void _remove_destroy(Client_widget *c)
+ {
+ disconnect(c, SIGNAL(client_changed()));
+ _list.remove(c);
+ Genode::destroy(&_alloc, c);
+ }
+
+ Q_SIGNALS:
+
+ void registry_changed();
+
+ public:
+
+ Client_widget_registry(Genode::Allocator &alloc) : QObject(), _alloc(alloc) { }
+
+ Client_widget* first() { return _list.first(); }
+
+ Client_widget* lookup(Channel::Label const &label)
+ {
+ for (Client_widget *c = _list.first(); c; c = c->next()) {
+ if (label == c->label())
+ return c;
+ }
+ return nullptr;
+ }
+
+ Client_widget* alloc_insert(Channel::Label const &label)
+ {
+ Client_widget *c = lookup(label);
+ if (c == nullptr) {
+ c = new (&_alloc) Client_widget(_alloc, label);
+ connect(c, SIGNAL(client_changed()),
+ this, SIGNAL(registry_changed()));
+ _list.insert(c);
+ }
+ return c;
+ }
+
+ void invalidate_all()
+ {
+ for (Client_widget *c = _list.first(); c; c = c->next())
+ c->valid = false;
+ }
+
+ void remove_invalid()
+ {
+ for (Client_widget *c = _list.first(); c; c = c->next())
+ if (c->valid == false) _remove_destroy(c);
+ }
+};
+
+
+static Client_widget_registry *client_registry()
+{
+ static Client_widget_registry inst(*Genode::env()->heap());
+ return &inst;
+}
+
+
+static Genode::Reporter config_reporter { "mixer.config" };
+
+
+void Main_window::_update_config()
+{
+ config_reporter.enabled(true);
+
+ try {
+ Genode::Reporter::Xml_generator xml(config_reporter, [&] {
+
+ xml.node("channel_list", [&] {
+ for (Client_widget const *c = client_registry()->first(); c; c = c->next()) {
+ bool const combined = c->combined_control();
+
+ static int vol = 0;
+ static bool muted = true;
+ if (combined) {
+ Channel_widget const *w = c->first_channel();
+ vol = w->volume();
+ muted = w->muted();
+ }
+
+ for (Channel_widget const *w = c->first_channel(); w; w = w->next()) {
+ Channel::Number const nr = w->number();
+ xml.node("channel", [&] {
+ xml.attribute("type", type_to_string(w->type()));
+ xml.attribute("label", c->label().string());
+ xml.attribute("name", channel_string_from_number(nr));
+ xml.attribute("number", nr);
+ xml.attribute("volume", combined ? vol : w->volume());
+ xml.attribute("muted", combined ? muted : w->muted());
+ });
+ }
+ }
+ });
+ });
+ } catch (...) { PWRN("could not report channels"); }
+}
+
+
+void Main_window::_update_clients(Genode::Xml_node &channels)
+{
+ for (Client_widget *c = client_registry()->first(); c; c = c->next())
+ _layout->removeWidget(c);
+
+ client_registry()->invalidate_all();
+
+ channels.for_each_sub_node("channel", [&] (Genode::Xml_node const &node) {
+ try {
+ Channel ch(node);
+
+ Client_widget *c = client_registry()->lookup(ch.label);
+ if (c == nullptr)
+ c = client_registry()->alloc_insert(ch.label);
+
+ Channel_widget *w = c->lookup_channel(ch.number);
+ if (w == nullptr)
+ w = c->add_channel(ch.type, ch.number);
+
+ w->volume(ch.volume);
+ w->muted(ch.muted);
+
+ if (c->combined_control()) c->only_show_first();
+ else w->show();
+
+ c->valid = true;
+
+ _layout->addWidget(c);
+ resize(sizeHint());
+ } catch (Channel::Invalid_channel) { PWRN("invalid channel node"); }
+ });
+
+ client_registry()->remove_invalid();
+}
+
+
+/**
+ * Gets called from the Genode to Qt proxy object when the report was
+ * updated with a pointer to the XML document.
+ */
+void Main_window::report_changed(void *l, void const *p)
+{
+ Genode::Lock &lock = *reinterpret_cast(l);
+ Genode::Xml_node &node = *((Genode::Xml_node*)p);
+
+ if (node.has_type("channel_list"))
+ _update_clients(node);
+
+ lock.unlock();
+}
+
+
+Main_window::Main_window()
+{
+ connect(client_registry(), SIGNAL(registry_changed()),
+ this, SLOT(_update_config()));
+}
+
+
+Main_window::~Main_window()
+{
+ disconnect(client_registry(), SIGNAL(registry_changed()));
+}
+
+#include "main_window.moc"
diff --git a/repos/gems/src/app/mixer_gui_qt/main_window.h b/repos/gems/src/app/mixer_gui_qt/main_window.h
new file mode 100644
index 0000000000..72f10c106c
--- /dev/null
+++ b/repos/gems/src/app/mixer_gui_qt/main_window.h
@@ -0,0 +1,70 @@
+/*
+ * \brief Main window of the mixer Qt frontend
+ * \author Josef Soentgen
+ * \date 2015-10-15
+ */
+
+/*
+ * Copyright (C) 2015 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU General Public License version 2.
+ */
+
+#ifndef _MAIN_WINDOW_H_
+#define _MAIN_WINDOW_H_
+
+/* Genode includes */
+#include
+#include
+#include
+
+/* Qt includes */
+#include
+#include
+#include
+
+/* Qoost includes */
+#include
+#include
+
+/* application includes */
+
+
+/**
+ * This class proxies Genode signals to Qt signals
+ */
+struct Report_proxy : QObject
+{
+ Q_OBJECT
+
+ Q_SIGNALS:
+
+ void report_changed(void *, void const *);
+};
+
+
+class Main_window : public Compound_widget
+{
+ Q_OBJECT
+
+ private:
+
+ void _update_clients(Genode::Xml_node &);
+
+ private Q_SLOTS:
+
+ void _update_config();
+
+ public Q_SLOTS:
+
+ void report_changed(void *, void const *);
+
+ public:
+
+ Main_window();
+
+ ~Main_window();
+};
+
+#endif /* _MAIN_WINDOW_H_ */
diff --git a/repos/gems/src/app/mixer_gui_qt/mixer_gui_qt.pro b/repos/gems/src/app/mixer_gui_qt/mixer_gui_qt.pro
new file mode 100644
index 0000000000..ae99977e51
--- /dev/null
+++ b/repos/gems/src/app/mixer_gui_qt/mixer_gui_qt.pro
@@ -0,0 +1,7 @@
+QT += core gui widgets
+TEMPLATE = app
+
+SOURCES += main.cpp \
+ main_window.cpp
+HEADERS += main_window.h
+RESOURCES = style.qrc
diff --git a/repos/gems/src/app/mixer_gui_qt/style.qrc b/repos/gems/src/app/mixer_gui_qt/style.qrc
new file mode 100644
index 0000000000..8a44d74480
--- /dev/null
+++ b/repos/gems/src/app/mixer_gui_qt/style.qrc
@@ -0,0 +1,6 @@
+
+
+
+style.qss
+
+
diff --git a/repos/gems/src/app/mixer_gui_qt/style.qss b/repos/gems/src/app/mixer_gui_qt/style.qss
new file mode 100644
index 0000000000..c045aaaaa1
--- /dev/null
+++ b/repos/gems/src/app/mixer_gui_qt/style.qss
@@ -0,0 +1,5 @@
+Main_window { min-width: 100; min-height: 100px; }
+
+Client_widget QFrame {
+ padding: 5px;
+}
diff --git a/repos/gems/src/app/mixer_gui_qt/target.mk b/repos/gems/src/app/mixer_gui_qt/target.mk
new file mode 100644
index 0000000000..75559f7cd9
--- /dev/null
+++ b/repos/gems/src/app/mixer_gui_qt/target.mk
@@ -0,0 +1,11 @@
+# identify the qt5 repository by searching for a file that is unique for qt5
+QT5_REP_DIR := $(call select_from_repositories,lib/import/import-qt5.inc)
+QT5_REP_DIR := $(realpath $(dir $(QT5_REP_DIR))../..)
+
+include $(QT5_REP_DIR)/src/app/qt5/tmpl/target_defaults.inc
+
+include $(QT5_REP_DIR)/src/app/qt5/tmpl/target_final.inc
+
+main_window.o: main_window.moc
+
+LIBS += qoost