From 5f2d92f9167547e312d20f61144b349cb3a22b11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20S=C3=B6ntgen?= Date: Mon, 19 Oct 2015 14:04:11 +0200 Subject: [PATCH] gems: add experimental mixer_gui based on Qt Issue #1770. --- repos/gems/run/mixer_gui_qt_test.run | 263 +++++++++++ repos/gems/src/app/mixer_gui_qt/main.cpp | 115 +++++ .../gems/src/app/mixer_gui_qt/main_window.cpp | 415 ++++++++++++++++++ repos/gems/src/app/mixer_gui_qt/main_window.h | 70 +++ .../src/app/mixer_gui_qt/mixer_gui_qt.pro | 7 + repos/gems/src/app/mixer_gui_qt/style.qrc | 6 + repos/gems/src/app/mixer_gui_qt/style.qss | 5 + repos/gems/src/app/mixer_gui_qt/target.mk | 11 + 8 files changed, 892 insertions(+) create mode 100644 repos/gems/run/mixer_gui_qt_test.run create mode 100644 repos/gems/src/app/mixer_gui_qt/main.cpp create mode 100644 repos/gems/src/app/mixer_gui_qt/main_window.cpp create mode 100644 repos/gems/src/app/mixer_gui_qt/main_window.h create mode 100644 repos/gems/src/app/mixer_gui_qt/mixer_gui_qt.pro create mode 100644 repos/gems/src/app/mixer_gui_qt/style.qrc create mode 100644 repos/gems/src/app/mixer_gui_qt/style.qss create mode 100644 repos/gems/src/app/mixer_gui_qt/target.mk 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 000000000..2390a932d --- /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 000000000..c8f0b41e3 --- /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 000000000..fffcea19a --- /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 000000000..72f10c106 --- /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 000000000..ae99977e5 --- /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 000000000..8a44d7448 --- /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 000000000..c045aaaaa --- /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 000000000..75559f7cd --- /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