From 1a58beafab5ff1bb7effb2d6d3546102327ab687 Mon Sep 17 00:00:00 2001 From: trigg Date: Mon, 9 Mar 2026 15:25:55 +0000 Subject: [PATCH] xdpw: added a screencast chooser --- data/meson.build | 2 + data/xdpw/wayfire | 3 + proto/ext-foreign-toplevel-list-v1.xml | 219 ++++++++++++++++++++++++ proto/meson.build | 7 +- src/meson.build | 1 + src/stream-chooser/meson.build | 13 ++ src/stream-chooser/outputwidget.cpp | 29 ++++ src/stream-chooser/outputwidget.hpp | 15 ++ src/stream-chooser/stream-chooser.cpp | 228 +++++++++++++++++++++++++ src/stream-chooser/stream-chooser.hpp | 49 ++++++ src/stream-chooser/toplevelwidget.cpp | 115 +++++++++++++ src/stream-chooser/toplevelwidget.hpp | 26 +++ 12 files changed, 704 insertions(+), 3 deletions(-) create mode 100644 data/xdpw/wayfire create mode 100644 proto/ext-foreign-toplevel-list-v1.xml create mode 100644 src/stream-chooser/meson.build create mode 100644 src/stream-chooser/outputwidget.cpp create mode 100644 src/stream-chooser/outputwidget.hpp create mode 100644 src/stream-chooser/stream-chooser.cpp create mode 100644 src/stream-chooser/stream-chooser.hpp create mode 100644 src/stream-chooser/toplevelwidget.cpp create mode 100644 src/stream-chooser/toplevelwidget.hpp diff --git a/data/meson.build b/data/meson.build index e35455ef..bc26276a 100644 --- a/data/meson.build +++ b/data/meson.build @@ -15,4 +15,6 @@ install_data(join_paths('icons', 'scalable', 'wayfire.svg'), install_dir: join_p install_data('wf-locker-password', install_dir:'/etc/pam.d/') +install_data('xdpw/wayfire', install_dir: '/etc/xdg/xdg-desktop-portal-wlr/') + subdir('css') \ No newline at end of file diff --git a/data/xdpw/wayfire b/data/xdpw/wayfire new file mode 100644 index 00000000..8b0ac616 --- /dev/null +++ b/data/xdpw/wayfire @@ -0,0 +1,3 @@ +[screencast] +chooser_cmd=wf-stream-chooser +chooser_type=simple \ No newline at end of file diff --git a/proto/ext-foreign-toplevel-list-v1.xml b/proto/ext-foreign-toplevel-list-v1.xml new file mode 100644 index 00000000..11b0113a --- /dev/null +++ b/proto/ext-foreign-toplevel-list-v1.xml @@ -0,0 +1,219 @@ + + + + Copyright © 2018 Ilia Bozhinov + Copyright © 2020 Isaac Freund + Copyright © 2022 wb9688 + Copyright © 2023 i509VCB + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + The purpose of this protocol is to provide protocol object handles for + toplevels, possibly originating from another client. + + This protocol is intentionally minimalistic and expects additional + functionality (e.g. creating a screencopy source from a toplevel handle, + getting information about the state of the toplevel) to be implemented + in extension protocols. + + The compositor may choose to restrict this protocol to a special client + launched by the compositor itself or expose it to all clients, + this is compositor policy. + + The key words "must", "must not", "required", "shall", "shall not", + "should", "should not", "recommended", "may", and "optional" in this + document are to be interpreted as described in IETF RFC 2119. + + Warning! The protocol described in this file is currently in the testing + phase. Backward compatible changes may be added together with the + corresponding interface version bump. Backward incompatible changes can + only be done by creating a new major version of the extension. + + + + + A toplevel is defined as a surface with a role similar to xdg_toplevel. + XWayland surfaces may be treated like toplevels in this protocol. + + After a client binds the ext_foreign_toplevel_list_v1, each mapped + toplevel window will be sent using the ext_foreign_toplevel_list_v1.toplevel + event. + + Clients which only care about the current state can perform a roundtrip after + binding this global. + + For each instance of ext_foreign_toplevel_list_v1, the compositor must + create a new ext_foreign_toplevel_handle_v1 object for each mapped toplevel. + + If a compositor implementation sends the ext_foreign_toplevel_list_v1.finished + event after the global is bound, the compositor must not send any + ext_foreign_toplevel_list_v1.toplevel events. + + + + + This event is emitted whenever a new toplevel window is created. It is + emitted for all toplevels, regardless of the app that has created them. + + All initial properties of the toplevel (identifier, title, app_id) will be sent + immediately after this event using the corresponding events for + ext_foreign_toplevel_handle_v1. The compositor will use the + ext_foreign_toplevel_handle_v1.done event to indicate when all data has + been sent. + + + + + + + This event indicates that the compositor is done sending events + to this object. The client should destroy the object. + See ext_foreign_toplevel_list_v1.destroy for more information. + + The compositor must not send any more toplevel events after this event. + + + + + + This request indicates that the client no longer wishes to receive + events for new toplevels. + + The Wayland protocol is asynchronous, meaning the compositor may send + further toplevel events until the stop request is processed. + The client should wait for a ext_foreign_toplevel_list_v1.finished + event before destroying this object. + + + + + + This request should be called either when the client will no longer + use the ext_foreign_toplevel_list_v1 or after the finished event + has been received to allow destruction of the object. + + If a client wishes to destroy this object it should send a + ext_foreign_toplevel_list_v1.stop request and wait for a ext_foreign_toplevel_list_v1.finished + event, then destroy the handles and then this object. + + + + + + + A ext_foreign_toplevel_handle_v1 object represents a mapped toplevel + window. A single app may have multiple mapped toplevels. + + + + + This request should be used when the client will no longer use the handle + or after the closed event has been received to allow destruction of the + object. + + When a handle is destroyed, a new handle may not be created by the server + until the toplevel is unmapped and then remapped. Destroying a toplevel handle + is not recommended unless the client is cleaning up child objects + before destroying the ext_foreign_toplevel_list_v1 object, the toplevel + was closed or the toplevel handle will not be used in the future. + + Other protocols which extend the ext_foreign_toplevel_handle_v1 + interface should require destructors for extension interfaces be + called before allowing the toplevel handle to be destroyed. + + + + + + The server will emit no further events on the ext_foreign_toplevel_handle_v1 + after this event. Any requests received aside from the destroy request must + be ignored. Upon receiving this event, the client should destroy the handle. + + Other protocols which extend the ext_foreign_toplevel_handle_v1 + interface must also ignore requests other than destructors. + + + + + + This event is sent after all changes in the toplevel state have + been sent. + + This allows changes to the ext_foreign_toplevel_handle_v1 properties + to be atomically applied. Other protocols which extend the + ext_foreign_toplevel_handle_v1 interface may use this event to also + atomically apply any pending state. + + This event must not be sent after the ext_foreign_toplevel_handle_v1.closed + event. + + + + + + The title of the toplevel has changed. + + The configured state must not be applied immediately. See + ext_foreign_toplevel_handle_v1.done for details. + + + + + + + The app id of the toplevel has changed. + + The configured state must not be applied immediately. See + ext_foreign_toplevel_handle_v1.done for details. + + + + + + + This identifier is used to check if two or more toplevel handles belong + to the same toplevel. + + The identifier is useful for command line tools or privileged clients + which may need to reference an exact toplevel across processes or + instances of the ext_foreign_toplevel_list_v1 global. + + The compositor must only send this event when the handle is created. + + The identifier must be unique per toplevel and it's handles. Two different + toplevels must not have the same identifier. The identifier is only valid + as long as the toplevel is mapped. If the toplevel is unmapped the identifier + must not be reused. An identifier must not be reused by the compositor to + ensure there are no races when sharing identifiers between processes. + + An identifier is a string that contains up to 32 printable ASCII bytes. + An identifier must not be an empty string. It is recommended that a + compositor includes an opaque generation value in identifiers. How the + generation value is used when generating the identifier is implementation + dependent. + + + + + diff --git a/proto/meson.build b/proto/meson.build index 831308a0..ebcaebfa 100644 --- a/proto/meson.build +++ b/proto/meson.build @@ -15,9 +15,10 @@ wayland_scanner_client = generator( ) client_protocols = [ - 'wlr-foreign-toplevel-management-unstable-v1.xml', - 'wlr-screencopy.xml', - wayfire.get_pkgconfig_variable('pkgdatadir') / 'unstable' / 'wayfire-shell-unstable-v2.xml', + 'ext-foreign-toplevel-list-v1.xml', + 'wlr-foreign-toplevel-management-unstable-v1.xml', + 'wlr-screencopy.xml', + wayfire.get_pkgconfig_variable('pkgdatadir') / 'unstable' / 'wayfire-shell-unstable-v2.xml', ] if gbm.found() and drm.found() and not get_option('live-previews-dmabuf').disabled() diff --git a/src/meson.build b/src/meson.build index a2fe8f19..78774b7c 100644 --- a/src/meson.build +++ b/src/meson.build @@ -4,6 +4,7 @@ subdir('background') subdir('dock') subdir('locker') subdir('locker-pin') +subdir('stream-chooser') pkgconfig = import('pkgconfig') pkgconfig.generate( diff --git a/src/stream-chooser/meson.build b/src/stream-chooser/meson.build new file mode 100644 index 00000000..90343525 --- /dev/null +++ b/src/stream-chooser/meson.build @@ -0,0 +1,13 @@ +deps = [ + gtkmm, + wf_protos, + libutil, + gtklayershell, +] + +executable( + 'wf-stream-chooser', + ['stream-chooser.cpp', 'toplevelwidget.cpp', 'outputwidget.cpp'], + dependencies: deps, + install: true, +) \ No newline at end of file diff --git a/src/stream-chooser/outputwidget.cpp b/src/stream-chooser/outputwidget.cpp new file mode 100644 index 00000000..6220b28d --- /dev/null +++ b/src/stream-chooser/outputwidget.cpp @@ -0,0 +1,29 @@ +#include + +#include "outputwidget.hpp" +#include "stream-chooser.hpp" + +WayfireChooserOutput::WayfireChooserOutput(std::shared_ptr output) : output(output) +{ + append(contents); + append(model); + append(connector); + + /* TODO Contents. We should probably grab screenshots of each output and display them */ + + model.set_label(output->get_model()); + connector.set_label(output->get_connector()); + + set_orientation(Gtk::Orientation::VERTICAL); + + output->signal_invalidate().connect([=] + { + WayfireStreamChooserApp::getInstance().remove_output(output->get_connector()); + }); +} + +void WayfireChooserOutput::print() +{ + std::cout << "Monitor: " << output->get_connector() << std::endl; + exit(0); +} diff --git a/src/stream-chooser/outputwidget.hpp b/src/stream-chooser/outputwidget.hpp new file mode 100644 index 00000000..e96b6386 --- /dev/null +++ b/src/stream-chooser/outputwidget.hpp @@ -0,0 +1,15 @@ +#pragma once +#include +#include + +class WayfireChooserOutput : public Gtk::Box +{ + Gtk::Label connector, model; + Gtk::Image contents; + + std::shared_ptr output; + + public: + void print(); + WayfireChooserOutput(std::shared_ptr output); +}; diff --git a/src/stream-chooser/stream-chooser.cpp b/src/stream-chooser/stream-chooser.cpp new file mode 100644 index 00000000..a0a1965c --- /dev/null +++ b/src/stream-chooser/stream-chooser.cpp @@ -0,0 +1,228 @@ +#include +#include +#include +#include + +#include "stream-chooser.hpp" +#include "outputwidget.hpp" +#include "toplevelwidget.hpp" + +#define EXT_IMAGE_COPY_CAPTURE_V1 "ext-image-copy-capture-v1" + +/* Static callbacks for toplevel list object */ +static void handle_toplevel(void *data, + struct ext_foreign_toplevel_list_v1 *list, + struct ext_foreign_toplevel_handle_v1 *toplevel) +{ + WayfireStreamChooserApp::getInstance().add_toplevel(toplevel); +} + +static void handle_finished(void *data, + struct ext_foreign_toplevel_list_v1 *list) +{ + ext_foreign_toplevel_list_v1_stop(list); + ext_foreign_toplevel_list_v1_destroy(list); +} + +ext_foreign_toplevel_list_v1_listener toplevel_list_v1_impl = { + .toplevel = handle_toplevel, + .finished = handle_finished, +}; + +/* Static callbacks for wayland registry */ +static void registry_add_object(void *data, wl_registry *registry, uint32_t name, + const char *interface, uint32_t version) +{ + if (strcmp(interface, ext_foreign_toplevel_list_v1_interface.name) == 0) + { + auto list = (ext_foreign_toplevel_list_v1*) + wl_registry_bind(registry, name, + &ext_foreign_toplevel_list_v1_interface, + version); + + WayfireStreamChooserApp::getInstance().set_toplevel_list(list); + ext_foreign_toplevel_list_v1_add_listener(list, + &toplevel_list_v1_impl, NULL); + } else if (strcmp(interface, EXT_IMAGE_COPY_CAPTURE_V1) == 0) + { + /* We need this to exist, but we're not using it directly */ + WayfireStreamChooserApp::getInstance().has_image_copy_capture = true; + } +} + +static void registry_remove_object(void *data, struct wl_registry *registry, uint32_t name) +{} + +static struct wl_registry_listener registry_listener = +{ + ®istry_add_object, + ®istry_remove_object +}; + +WayfireStreamChooserApp::WayfireStreamChooserApp() : Gtk::Application("org.wayfire.screen-chooser", + Gio::Application::Flags::NONE) +{ + signal_activate().connect(sigc::mem_fun(*this, &WayfireStreamChooserApp::activate)); +} + +void WayfireStreamChooserApp::activate() +{ + window.set_size_request(300, 300); + add_window(window); + window.set_child(main); + main.append(header); + main.append(notebook); + main.append(buttons); + + notebook.set_expand(true); + notebook.append_page(window_list, window_label); + notebook.append_page(screen_list, screen_label); + + main.set_orientation(Gtk::Orientation::VERTICAL); + + buttons.set_hexpand(true); + buttons.append(cancel); + cancel.set_halign(Gtk::Align::START); + buttons.append(done); + done.set_halign(Gtk::Align::END); + + window_label.set_label("Window"); + screen_label.set_label("Screen"); + header.set_label("Choose a view to share"); + + cancel.set_label("Cancel"); + done.set_label("Done"); + buttons.set_homogeneous(true); + + done.signal_clicked().connect([this] () + { + if (notebook.get_current_page() == 0) + { + auto children = window_list.get_selected_children(); + if (children.size() == 1) + { + WayfireChooserTopLevel *cast_child = (WayfireChooserTopLevel*)(children[0]->get_child()); + cast_child->print(); + } + + /* TODO Consider an error to let user know the selection was invalid */ + exit(0); + } else + { + auto children = screen_list.get_selected_children(); + if (children.size() == 1) + { + WayfireChooserOutput *cast_child = (WayfireChooserOutput*)(children[0]->get_child()); + cast_child->print(); + } + + /* TODO Consider an error to let user know the selection was invalid */ + exit(0); + } + }); + + cancel.signal_clicked().connect([] () + { + exit(0); + }); + + /* Attempt to get Window list */ + auto gdk_display = gdk_display_get_default(); + auto display = gdk_wayland_display_get_wl_display(gdk_display); + + this->display = display; + + wl_registry *registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, ®istry_listener, this); + this->registry = registry; + wl_display_roundtrip(display); + + if (this->list && has_image_copy_capture) + {} else + { + std::cerr << "Compositor doesn't support" << + " ext-foreign-toplevel-list and/or ext-image-copy-capture-v1." << + " Only screens can be cast currently." << std::endl; + window_label.set_sensitive(false); + window_label.set_tooltip_text("This compositor does not currently support sharing individual windows"); + notebook.set_current_page(1); + } + + /* Get output list */ + auto gtkdisplay = Gdk::Display::get_default(); + auto monitors = gtkdisplay->get_monitors(); + monitors->signal_items_changed().connect( + [this] (const int pos, const int rem, const int add) + { + auto display = Gdk::Display::get_default(); + auto monitors = display->get_monitors(); + int num_monitors = monitors->get_n_items(); + for (int i = 0; i < num_monitors; i++) + { + auto obj = std::dynamic_pointer_cast(monitors->get_object(i)); + add_output(obj); + } + }); + + // Initial monitors + int num_monitors = monitors->get_n_items(); + for (int i = 0; i < num_monitors; i++) + { + auto obj = std::dynamic_pointer_cast(monitors->get_object(i)); + add_output(obj); + } + + gtk_layer_init_for_window(window.gobj()); + gtk_layer_set_namespace(window.gobj(), "chooser"); + gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_TOP, true); + gtk_layer_set_keyboard_mode(window.gobj(), GTK_LAYER_SHELL_KEYBOARD_MODE_ON_DEMAND); + gtk_layer_set_exclusive_zone(window.gobj(), 0); + window.present(); +} + +void WayfireStreamChooserApp::add_toplevel(ext_foreign_toplevel_handle_v1 *handle) +{ + toplevels.emplace(handle, new WayfireChooserTopLevel(handle)); + window_list.append(*toplevels[handle]); + if (window_list.get_selected_children().size() == 0) + { + auto child = window_list.get_child_at_index(0); + window_list.select_child(*child); + } +} + +void WayfireStreamChooserApp::remove_toplevel(WayfireChooserTopLevel *toplevel) +{ + window_list.remove(*toplevel); + toplevels.erase(toplevel->handle); +} + +void WayfireStreamChooserApp::add_output(std::shared_ptr monitor) +{ + std::string connector = monitor->get_connector(); + outputs.emplace(connector, new WayfireChooserOutput(monitor)); + screen_list.append(*outputs[connector]); + if (screen_list.get_selected_children().size() == 0) + { + auto child = screen_list.get_child_at_index(0); + screen_list.select_child(*child); + } +} + +void WayfireStreamChooserApp::remove_output(std::string connector) +{ + screen_list.remove(*outputs[connector]); + outputs.erase(connector); +} + +void WayfireStreamChooserApp::set_toplevel_list(ext_foreign_toplevel_list_v1 *list) +{ + this->list = list; +} + +/* Starting point */ +int main(int argc, char **argv) +{ + WayfireStreamChooserApp::getInstance().run(); + exit(0); +} diff --git a/src/stream-chooser/stream-chooser.hpp b/src/stream-chooser/stream-chooser.hpp new file mode 100644 index 00000000..6c400e0c --- /dev/null +++ b/src/stream-chooser/stream-chooser.hpp @@ -0,0 +1,49 @@ +#pragma once +#include +#include +#include + +#include "outputwidget.hpp" +#include "toplevelwidget.hpp" + + +class WayfireStreamChooserApp : public Gtk::Application +{ + private: + Gtk::Window window; + Gtk::Notebook notebook; + + Gtk::Box main, buttons; + + Gtk::Label window_label, screen_label, header; + + Gtk::FlowBox window_list, screen_list; + Gtk::Button done, cancel; + WayfireStreamChooserApp(); + + wl_display *display; + wl_registry *registry; + ext_foreign_toplevel_list_v1 *list; + + public: + bool has_image_copy_capture = false; + + std::map> toplevels; + std::map> outputs; + + static WayfireStreamChooserApp& getInstance() + { + static WayfireStreamChooserApp instance; + return instance; + } + + void set_toplevel_list(ext_foreign_toplevel_list_v1 *list); + void add_toplevel(ext_foreign_toplevel_handle_v1 *handle); + void remove_toplevel(WayfireChooserTopLevel *widget); + + void add_output(std::shared_ptr monitor); + void remove_output(std::string connector); + void activate(); + WayfireStreamChooserApp(WayfireStreamChooserApp const&) = delete; + void operator =(WayfireStreamChooserApp const&) = delete; +}; diff --git a/src/stream-chooser/toplevelwidget.cpp b/src/stream-chooser/toplevelwidget.cpp new file mode 100644 index 00000000..26dc182d --- /dev/null +++ b/src/stream-chooser/toplevelwidget.cpp @@ -0,0 +1,115 @@ +#include + +#include "stream-chooser.hpp" +#include "toplevelwidget.hpp" + +static void handle_closed(void *data, + struct ext_foreign_toplevel_handle_v1 *handle) +{ + WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; + WayfireStreamChooserApp::getInstance().remove_toplevel(toplevel); + + /* TODO Clean up */ +} + +static void handle_done(void *data, + struct ext_foreign_toplevel_handle_v1 *handle) +{ + WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; + toplevel->commit(); +} + +static void handle_title(void *data, + struct ext_foreign_toplevel_handle_v1 *ext_foreign_toplevel_handle_v1, + const char *title) +{ + WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; + toplevel->set_title(title); +} + +static void handle_app_id(void *data, + struct ext_foreign_toplevel_handle_v1 *handle1, + const char *app_id) +{ + WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; + toplevel->set_app_id(app_id); +} + +static void handle_identifier(void *data, + struct ext_foreign_toplevel_handle_v1 *ext_foreign_toplevel_handle_v1, + const char *identifier) +{ + WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; + toplevel->set_identifier(identifier); +} + +ext_foreign_toplevel_handle_v1_listener listener = +{ + .closed = handle_closed, + .done = handle_done, + .title = handle_title, + .app_id = handle_app_id, + .identifier = handle_identifier, +}; + +/* Gtk Overlay showing information about a window */ +WayfireChooserTopLevel::WayfireChooserTopLevel(ext_foreign_toplevel_handle_v1 *handle) +{ + append(overlay); + append(label); + overlay.set_child(screenshot); + overlay.add_overlay(icon); + icon.set_halign(Gtk::Align::START); + icon.set_valign(Gtk::Align::END); + label.set_ellipsize(Pango::EllipsizeMode::MIDDLE); + label.set_max_width_chars(40); + + ext_foreign_toplevel_handle_v1_add_listener(handle, &listener, this); +} + +void WayfireChooserTopLevel::set_title(std::string title) +{ + buffered_title = title; +} + +void WayfireChooserTopLevel::set_app_id(std::string app_id) +{ + buffered_app_id = app_id; +} + +void WayfireChooserTopLevel::set_identifier(std::string identifier) +{ + buffered_identifier = identifier; +} + +void WayfireChooserTopLevel::commit() +{ + if (buffered_app_id != "") + { + app_id = buffered_app_id; + icon.set_from_icon_name(app_id); + buffered_app_id = ""; + } + + if (buffered_title != "") + { + title = buffered_title; + label.set_label(title); + buffered_title = ""; + } + + if (buffered_identifier != "") + { + identifier = buffered_identifier; + buffered_identifier = ""; + } +} + +WayfireChooserTopLevel::~WayfireChooserTopLevel() +{} + +void WayfireChooserTopLevel::print() +{ + std::cout << "Window: " << identifier << std::endl; + exit(0); +} diff --git a/src/stream-chooser/toplevelwidget.hpp b/src/stream-chooser/toplevelwidget.hpp new file mode 100644 index 00000000..ef0bd1b3 --- /dev/null +++ b/src/stream-chooser/toplevelwidget.hpp @@ -0,0 +1,26 @@ +#pragma once +#include +#include "ext-foreign-toplevel-list-v1-client-protocol.h" + +class WayfireChooserTopLevel : public Gtk::Box +{ + private: + Gtk::Overlay overlay; + Gtk::Image icon; + Gtk::Image screenshot; + Gtk::Label label; + + std::string buffered_title = "", title = ""; + std::string buffered_app_id = "", app_id = ""; + std::string buffered_identifier = "", identifier = ""; + + public: + ext_foreign_toplevel_handle_v1 *handle; + WayfireChooserTopLevel(ext_foreign_toplevel_handle_v1 *handle); + ~WayfireChooserTopLevel(); + void commit(); + void set_app_id(std::string app_id); + void set_title(std::string title); + void set_identifier(std::string identifier); + void print(); +};