diff --git a/data/meson.build b/data/meson.build index ea20a57a..04529279 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', install_dir:'/etc/pam.d/') +install_data('xdpw/wayfire', install_dir: '/etc/xdg/xdg-desktop-portal-wlr/') + subdir('css') 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/meson.build b/proto/meson.build index 831308a0..28d4037a 100644 --- a/proto/meson.build +++ b/proto/meson.build @@ -1,5 +1,4 @@ -wl_protocol_dir = wayland_protos.get_pkgconfig_variable('pkgdatadir') - +wl_protocol_dir = wayland_protos.get_variable(pkgconfig: 'pkgdatadir', internal: 'pkgdatadir') wayland_scanner = find_program('wayland-scanner') wayland_scanner_code = generator( @@ -15,25 +14,26 @@ 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', + [wl_protocol_dir, 'unstable/xdg-output/xdg-output-unstable-v1.xml'], + [wl_protocol_dir, 'unstable/linux-dmabuf/linux-dmabuf-unstable-v1.xml'], + [wl_protocol_dir, 'staging/ext-foreign-toplevel-list/ext-foreign-toplevel-list-v1.xml'], + [wl_protocol_dir, 'staging/ext-image-capture-source/ext-image-capture-source-v1.xml'], + [wl_protocol_dir, 'staging/ext-image-copy-capture/ext-image-copy-capture-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() - client_protocols += [join_paths(wl_protocol_dir, 'unstable/linux-dmabuf/linux-dmabuf-unstable-v1.xml')] -endif - -wl_protos_src = [] +wl_protos_client_src = [] wl_protos_headers = [] foreach p : client_protocols xml = join_paths(p) + wl_protos_client_src += wayland_scanner_code.process(xml) wl_protos_headers += wayland_scanner_client.process(xml) - wl_protos_src += wayland_scanner_code.process(xml) endforeach -lib_wl_protos = static_library('wl_protos', wl_protos_src + wl_protos_headers, +lib_wl_protos = static_library('wl_protos', wl_protos_client_src + wl_protos_headers, dependencies: [wayland_client]) # for the include directory wf_protos = declare_dependency( 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/mainlayout.cpp b/src/stream-chooser/mainlayout.cpp new file mode 100644 index 00000000..9e62ca32 --- /dev/null +++ b/src/stream-chooser/mainlayout.cpp @@ -0,0 +1,29 @@ +#include "mainlayout.hpp" +void MainLayout::allocate_vfunc(const Gtk::Widget& widget, int width, int height, int baseline) +{ + Gtk::Widget& widget_not_const = const_cast(widget); + + auto inner = widget_not_const.get_children()[0]; + auto alloc = Gtk::Allocation(); + alloc.set_height(height / 2); + alloc.set_width(width / 2); + alloc.set_y(height / 4); + alloc.set_x(width / 4); + inner->size_allocate(alloc, -1); +} + +void MainLayout::measure_vfunc(const Gtk::Widget& widget, Gtk::Orientation orientation, + int for_size, int& minimum, int& natural, int& minimum_baseline, + int& natural_baseline) const +{ + // Answer 1:1 aspect ratio + minimum = std::min(for_size, 300); + natural = std::min(for_size, 2000); + minimum_baseline = -1; + natural_baseline = -1; +} + +Gtk::SizeRequestMode MainLayout::get_request_mode_vfunc(const Gtk::Widget& widget) const +{ + return Gtk::SizeRequestMode::WIDTH_FOR_HEIGHT; +} diff --git a/src/stream-chooser/mainlayout.hpp b/src/stream-chooser/mainlayout.hpp new file mode 100644 index 00000000..6696a8ea --- /dev/null +++ b/src/stream-chooser/mainlayout.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + + +class MainLayout : public Gtk::LayoutManager +{ + protected: + void allocate_vfunc(const Gtk::Widget& widget, int width, int height, int baseline) override; + void measure_vfunc(const Gtk::Widget& widget, Gtk::Orientation orientation, int for_size, int& minimum, + int& natural, int& minimum_baseline, int& natural_baseline) const override; + Gtk::SizeRequestMode get_request_mode_vfunc(const Gtk::Widget& widget) const override; + std::shared_ptr layout; + + public: + MainLayout() + {} +}; diff --git a/src/stream-chooser/meson.build b/src/stream-chooser/meson.build new file mode 100644 index 00000000..d5b7ee1f --- /dev/null +++ b/src/stream-chooser/meson.build @@ -0,0 +1,20 @@ +deps = [ + gtkmm, + wf_protos, + wfconfig, + libutil, + gtklayershell, +] + +executable( + 'wf-stream-chooser', + [ + 'stream-chooser.cpp', + 'toplevelwidget.cpp', + 'outputwidget.cpp', + 'toplevellayout.cpp', + 'mainlayout.cpp', + ], + dependencies: deps, + install: true, +) diff --git a/src/stream-chooser/outputwidget.cpp b/src/stream-chooser/outputwidget.cpp new file mode 100644 index 00000000..de611176 --- /dev/null +++ b/src/stream-chooser/outputwidget.cpp @@ -0,0 +1,301 @@ +#include +#include + +#include "outputwidget.hpp" +#include "gdk/wayland/gdkwayland.h" +#include "stream-chooser.hpp" + +/* Copy Capture Callbacks */ + +static void frame_handle_transform(void*, + struct ext_image_copy_capture_frame_v1*, + uint32_t) +{} + +static void frame_handle_damage(void*, + struct ext_image_copy_capture_frame_v1*, + int32_t, int32_t, int32_t, int32_t) +{} + +static void frame_handle_presentation_time(void*, + struct ext_image_copy_capture_frame_v1*, + uint32_t, uint32_t, uint32_t) +{} + +static void frame_handle_ready(void *data, + struct ext_image_copy_capture_frame_v1*) +{ + WayfireChooserOutput *output = (WayfireChooserOutput*)data; + output->buffer_ready(); +} + +static void frame_handle_failed(void*, + struct ext_image_copy_capture_frame_v1 *handle, + uint32_t reason) +{ + std::cerr << "Failed to copy frame because reason: " << reason << std::endl; + ext_image_copy_capture_frame_v1_destroy(handle); +} + +static const struct ext_image_copy_capture_frame_v1_listener frame_listener = { + .transform = frame_handle_transform, + .damage = frame_handle_damage, + .presentation_time = frame_handle_presentation_time, + .ready = frame_handle_ready, + .failed = frame_handle_failed, +}; + +/* SHM Callbacks*/ +static int backingfile(off_t size) +{ + static int count; + char name[128]; + sprintf(name, "/tmp/wf-stream-chooser-%d-XXXXXX", count++); + int fd = mkstemp(name); + if (fd < 0) + { + perror("mkstemp"); + return -1; + } + + int ret; + while ((ret = ftruncate(fd, size)) == EINTR) + {} + + if (ret < 0) + { + perror("ret < 0"); + close(fd); + return -1; + } + + unlink(name); + return fd; +} + +static struct wl_buffer *output_create_shm_buffer(int width, int height, void **data_out, size_t *size) +{ + *size = width * 4 * height; + + int fd = backingfile(*size); + if (fd < 0) + { + perror("Creating a buffer file failed"); + return NULL; + } + + void *data = mmap(NULL, *size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (data == MAP_FAILED) + { + perror("mmap failed"); + close(fd); + return NULL; + } + + wl_shm_pool *pool = wl_shm_create_pool(WayfireStreamChooserApp::getInstance().shm, fd, *size); + close(fd); + wl_buffer *buffer = wl_shm_pool_create_buffer(pool, 0, width, height, + width * 4, WL_SHM_FORMAT_ABGR8888); + wl_shm_pool_destroy(pool); + + *data_out = data; + return buffer; +} + +void output_free_shm_buffer(std::shared_ptr& buffer) +{ + if (buffer->buffer == NULL) + { + return; + } + + munmap(buffer->data, buffer->size); + wl_buffer_destroy(buffer->buffer); + buffer->buffer = NULL; +} + +static void session_handle_buffer_size(void *data, + struct ext_image_copy_capture_session_v1*, + uint32_t width, uint32_t height) +{ + WayfireChooserOutput *output = (WayfireChooserOutput*)data; + output->current_buffer_width = width; + output->current_buffer_height = height; +} + +static void session_handle_shm_format(void *data, + struct ext_image_copy_capture_session_v1*, + uint32_t format) +{ + WayfireChooserOutput *output = (WayfireChooserOutput*)data; + output->current_buffer_format = format; +} + +static void session_handle_dmabuf_device(void*, + struct ext_image_copy_capture_session_v1*, + struct wl_array*) +{} + +static void session_handle_dmabuf_format(void*, + struct ext_image_copy_capture_session_v1*, + uint32_t, + struct wl_array*) +{} + +static void session_handle_done(void *data, + struct ext_image_copy_capture_session_v1*) +{ + WayfireChooserOutput *output = (WayfireChooserOutput*)data; + output->size(); +} + +static void session_handle_stopped(void*, + struct ext_image_copy_capture_session_v1 *session) +{ + ext_image_copy_capture_session_v1_destroy(session); + session = NULL; +} + +static const struct ext_image_copy_capture_session_v1_listener recording_session_listener = { + .buffer_size = session_handle_buffer_size, + .shm_format = session_handle_shm_format, + .dmabuf_device = session_handle_dmabuf_device, + .dmabuf_format = session_handle_dmabuf_format, + .done = session_handle_done, + .stopped = session_handle_stopped, +}; + +void WayfireChooserOutput::grab_output_screenshot() +{ + if (WayfireStreamChooserApp::getInstance().is_in_use) + { + Glib::signal_timeout().connect( + [this] () + { + grab_output_screenshot(); + return G_SOURCE_REMOVE; + }, 100); + return; + } + + WayfireStreamChooserApp::getInstance().is_in_use = true; + + auto copy_capture_source = ext_output_image_capture_source_manager_v1_create_source( + WayfireStreamChooserApp::getInstance().output_capture_manager, + output_handle); + recording_session = ext_image_copy_capture_manager_v1_create_session( + WayfireStreamChooserApp::getInstance().manager, + copy_capture_source, + 0); + ext_image_copy_capture_session_v1_add_listener(recording_session, &recording_session_listener, this); + ext_image_capture_source_v1_destroy(copy_capture_source); +} + +WayfireChooserOutput::WayfireChooserOutput(std::shared_ptr output) : output(output) +{ + append(contents); + append(model); + append(connector); + set_size_request(150, 150); + set_valign(Gtk::Align::FILL); + set_halign(Gtk::Align::FILL); + + output_handle = gdk_wayland_monitor_get_wl_output(output->gobj()); + + /* 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()); + }); + + grab_output_screenshot(); +} + +void WayfireChooserOutput::print() +{ + std::cout << "Monitor: " << output->get_connector() << std::endl; + exit(0); +} + +void WayfireChooserOutput::size() +{ + if ((current_buffer_width <= 0) || (current_buffer_height <= 0)) + { + printf("%s invalid size\n", __func__); + return; + } + + buffer = std::make_shared(); + + if (frame) + { + ext_image_copy_capture_frame_v1_destroy(frame); + frame = NULL; + } + + buffer->width = current_buffer_width; + buffer->height = current_buffer_height; + + frame = ext_image_copy_capture_session_v1_create_frame(recording_session); + buffer->frame = frame; + ext_image_copy_capture_frame_v1_add_listener(buffer->frame, &frame_listener, this); + + output_free_shm_buffer(buffer); + buffer->buffer = + output_create_shm_buffer(buffer->width, buffer->height, &buffer->data, &buffer->size); + + if (buffer->buffer == NULL) + { + printf("%s failed to create buffer\n", __func__); + exit(EXIT_FAILURE); + } + + ext_image_copy_capture_frame_v1_attach_buffer(buffer->frame, buffer->buffer); + ext_image_copy_capture_frame_v1_damage_buffer(buffer->frame, 0, 0, buffer->width, buffer->height); + ext_image_copy_capture_frame_v1_capture(buffer->frame); +} + +void WayfireChooserOutput::buffer_ready() +{ + if ((buffer == nullptr) || (buffer->buffer == nullptr)) + { + printf("%s buffer null\n", __func__); + + return; + } + + /* buffer->data is now valid */ + std::shared_ptr bytes = 0; + size_t size = buffer->size; + if (buffer->data) + { + bytes = Glib::Bytes::create(buffer->data, size); + } + + if (!bytes) + { + return; + } + + auto builder = Gdk::MemoryTextureBuilder::create(); + builder->set_bytes(bytes); + builder->set_width(buffer->width); + builder->set_height(buffer->height); + builder->set_stride(buffer->width * 4); + builder->set_format(Gdk::MemoryFormat::R8G8B8A8); + + auto texture = builder->build(); + + contents.set_paintable(texture); + ext_image_copy_capture_frame_v1_destroy(frame); + frame = NULL; + ext_image_copy_capture_session_v1_destroy(recording_session); + recording_session = NULL; + WayfireStreamChooserApp::getInstance().is_in_use = false; +} diff --git a/src/stream-chooser/outputwidget.hpp b/src/stream-chooser/outputwidget.hpp new file mode 100644 index 00000000..b53ee086 --- /dev/null +++ b/src/stream-chooser/outputwidget.hpp @@ -0,0 +1,36 @@ +#pragma once +#include "gtkmm/picture.h" +#include +#include +#include +#include "ext-image-copy-capture-v1-client-protocol.h" +struct output_buffer +{ + int width = 0; + int height = 0; + void *data; + wl_buffer *buffer; + size_t size = 0; + ext_image_copy_capture_frame_v1 *frame = NULL; +}; + +class WayfireChooserOutput : public Gtk::Box +{ + Gtk::Label connector, model; + Gtk::Picture contents; + + wl_output *output_handle; + std::shared_ptr output; + std::shared_ptr buffer = nullptr; + ext_image_copy_capture_frame_v1 *frame = NULL; + ext_image_copy_capture_session_v1 *recording_session = NULL; + void grab_output_screenshot(); + + public: + void print(); + void size(); + void buffer_ready(); + + WayfireChooserOutput(std::shared_ptr output); + int current_buffer_width = 0, current_buffer_height = 0, current_buffer_format = 0; +}; diff --git a/src/stream-chooser/stream-chooser.cpp b/src/stream-chooser/stream-chooser.cpp new file mode 100644 index 00000000..5f7703d9 --- /dev/null +++ b/src/stream-chooser/stream-chooser.cpp @@ -0,0 +1,323 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "stream-chooser.hpp" +#include "gtkmm/enums.h" +#include "mainlayout.hpp" +#include "outputwidget.hpp" +#include "toplevelwidget.hpp" + +/* 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().has_foreign_toplevel_list = true; + 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_manager_v1_interface.name) == 0) + { + auto manager = (ext_image_copy_capture_manager_v1*)wl_registry_bind(registry, name, + &ext_image_copy_capture_manager_v1_interface, version); + WayfireStreamChooserApp::getInstance().has_image_copy_capture = true; + WayfireStreamChooserApp::getInstance().set_copy_capture_manager(manager); + } else if (strcmp(interface, wl_shm_interface.name) == 0) + { + auto shm = (wl_shm*)wl_registry_bind(registry, name, &wl_shm_interface, 1); + WayfireStreamChooserApp::getInstance().set_shm(shm); + } else if (strcmp(interface, ext_foreign_toplevel_image_capture_source_manager_v1_interface.name) == 0) + { + auto toplevel_capture_manager = + (ext_foreign_toplevel_image_capture_source_manager_v1*)wl_registry_bind(registry, name, + &ext_foreign_toplevel_image_capture_source_manager_v1_interface, version); + WayfireStreamChooserApp::getInstance().has_image_capture_source = true; + WayfireStreamChooserApp::getInstance().set_toplevel_capture_manager(toplevel_capture_manager); + } else if (strcmp(interface, ext_output_image_capture_source_manager_v1_interface.name) == 0) + { + auto output_capture_manager = + (ext_output_image_capture_source_manager_v1*)wl_registry_bind(registry, name, + &ext_output_image_capture_source_manager_v1_interface, + version); + WayfireStreamChooserApp::getInstance().set_output_capture_manager(output_capture_manager); + } +} + +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.add_css_class("stream-chooser"); + window.set_size_request(300, 300); + add_window(window); + window.set_child(main); + layout = std::make_shared(); + window.set_layout_manager(layout); + main.add_css_class("main-chooser"); + main.set_valign(Gtk::Align::FILL); + main.set_halign(Gtk::Align::FILL); + main.append(header); + main.append(notebook); + main.append(buttons); + auto window_display = window.get_display(); + auto css_provider = Gtk::CssProvider::create(); + css_provider->load_from_data( + "window.stream-chooser { background-color: rgba(0, 0, 0, 0.5); } .main-chooser { background-color: unset; }"); + Gtk::StyleContext::add_provider_for_display(window_display, + css_provider, GTK_STYLE_PROVIDER_PRIORITY_USER); + + scroll_window.set_child(window_list); + scroll_screen.set_child(screen_list); + + scroll_window.set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC); + scroll_screen.set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC); + + window_list.set_homogeneous(true); + screen_list.set_homogeneous(true); + + window_list.set_halign(Gtk::Align::START); + window_list.set_valign(Gtk::Align::START); + screen_list.set_halign(Gtk::Align::START); + screen_list.set_valign(Gtk::Align::START); + + notebook.set_expand(true); + notebook.append_page(scroll_window, window_label); + notebook.append_page(scroll_screen, 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); + + bool failed = false; + if (!has_image_copy_capture) + { + failed = true; + std::cerr << "Compositor has not advertised ext-image-copy-capture-v1" << std::endl; + } + + if (!has_foreign_toplevel_list) + { + failed = true; + std::cerr << "Compositor has not advertised ext-foreign-toplevel-list-v1" << std::endl; + } + + if (!has_image_capture_source) + { + failed = true; + std::cerr << "Compositor has not advertised ext-image-capture-source-v1" << std::endl; + } + + if (failed) + { + 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); + } + + auto debug = Glib::getenv("WF_CHOOSER_DEBUG"); + if (debug != "1") + { + 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_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_BOTTOM, true); + gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_LEFT, true); + gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_RIGHT, true); + + gtk_layer_set_keyboard_mode(window.gobj(), GTK_LAYER_SHELL_KEYBOARD_MODE_EXCLUSIVE); + 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::set_shm(wl_shm *shm) +{ + this->shm = shm; +} + +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; +} + +void WayfireStreamChooserApp::set_copy_capture_manager(ext_image_copy_capture_manager_v1 *manager) +{ + this->manager = manager; +} + +void WayfireStreamChooserApp::set_toplevel_capture_manager( + ext_foreign_toplevel_image_capture_source_manager_v1 *toplevel_capture_manager) +{ + this->toplevel_capture_manager = toplevel_capture_manager; +} + +void WayfireStreamChooserApp::set_output_capture_manager( + ext_output_image_capture_source_manager_v1 *output_capture_manager) +{ + this->output_capture_manager = output_capture_manager; +} + +/* 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..9aa1496f --- /dev/null +++ b/src/stream-chooser/stream-chooser.hpp @@ -0,0 +1,67 @@ +#pragma once +#include +#include +#include +#include +#include + +#include "mainlayout.hpp" +#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; + Gtk::ScrolledWindow scroll_window, scroll_screen; + WayfireStreamChooserApp(); + + wl_display *display; + wl_registry *registry; + ext_foreign_toplevel_list_v1 *list; + Glib::RefPtr layout; + + public: + bool is_in_use = false; + bool has_foreign_toplevel_list = false; + bool has_image_copy_capture = false; + bool has_image_capture_source = false; + + wl_shm *shm; + ext_image_copy_capture_manager_v1 *manager; + ext_foreign_toplevel_image_capture_source_manager_v1 *toplevel_capture_manager; + ext_output_image_capture_source_manager_v1 *output_capture_manager; + + std::map> toplevels; + std::map> outputs; + + static WayfireStreamChooserApp& getInstance() + { + static WayfireStreamChooserApp instance; + return instance; + } + + void set_shm(wl_shm *shm); + void set_toplevel_list(ext_foreign_toplevel_list_v1 *list); + void set_copy_capture_manager(ext_image_copy_capture_manager_v1 *manager); + void set_toplevel_capture_manager( + ext_foreign_toplevel_image_capture_source_manager_v1 *toplevel_capture_manager); + void set_output_capture_manager(ext_output_image_capture_source_manager_v1 *output_capture_manager); + 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/toplevellayout.cpp b/src/stream-chooser/toplevellayout.cpp new file mode 100644 index 00000000..41162c26 --- /dev/null +++ b/src/stream-chooser/toplevellayout.cpp @@ -0,0 +1,41 @@ +#include "toplevellayout.hpp" +void ToplevelLayout::allocate_vfunc(const Gtk::Widget& widget, int width, int height, int baseline) +{ + Gtk::Widget& widget_not_const = const_cast(widget); + + auto overlay = widget_not_const.get_children()[0]; + auto label = widget_not_const.get_children()[1]; + + /* Label */ + auto label_height = std::max(16, label->get_height()); + auto l_alloc = Gtk::Allocation(); + l_alloc.set_height(label_height); + l_alloc.set_width(width); + l_alloc.set_y(0); + l_alloc.set_x(0); + label->size_allocate(l_alloc, -1); + + /* Overlay */ + auto o_alloc = Gtk::Allocation(); + o_alloc.set_height(height - label_height); + o_alloc.set_width(width); + o_alloc.set_y(label_height); + o_alloc.set_x(0); + overlay->size_allocate(o_alloc, -1); +} + +void ToplevelLayout::measure_vfunc(const Gtk::Widget& widget, Gtk::Orientation orientation, + int for_size, int& minimum, int& natural, int& minimum_baseline, + int& natural_baseline) const +{ + // Answer 1:1 aspect ratio + minimum = for_size; + natural = for_size; + minimum_baseline = -1; + natural_baseline = -1; +} + +Gtk::SizeRequestMode ToplevelLayout::get_request_mode_vfunc(const Gtk::Widget& widget) const +{ + return Gtk::SizeRequestMode::WIDTH_FOR_HEIGHT; +} diff --git a/src/stream-chooser/toplevellayout.hpp b/src/stream-chooser/toplevellayout.hpp new file mode 100644 index 00000000..871c602b --- /dev/null +++ b/src/stream-chooser/toplevellayout.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + + +class ToplevelLayout : public Gtk::LayoutManager +{ + protected: + void allocate_vfunc(const Gtk::Widget& widget, int width, int height, int baseline) override; + void measure_vfunc(const Gtk::Widget& widget, Gtk::Orientation orientation, int for_size, int& minimum, + int& natural, int& minimum_baseline, int& natural_baseline) const override; + Gtk::SizeRequestMode get_request_mode_vfunc(const Gtk::Widget& widget) const override; + std::shared_ptr layout; + + public: + ToplevelLayout() + {} +}; diff --git a/src/stream-chooser/toplevelwidget.cpp b/src/stream-chooser/toplevelwidget.cpp new file mode 100644 index 00000000..7d2b9587 --- /dev/null +++ b/src/stream-chooser/toplevelwidget.cpp @@ -0,0 +1,401 @@ +#include + +#include +#include +#include "ext-foreign-toplevel-list-v1-client-protocol.h" +#include "ext-image-capture-source-v1-client-protocol.h" +#include "ext-image-copy-capture-v1-client-protocol.h" +#include "glib.h" +#include "glibmm/main.h" +#include "gtk-utils.hpp" +#include "stream-chooser.hpp" +#include "toplevellayout.hpp" +#include "toplevelwidget.hpp" + +/* Toplevel Callbacks */ + +static void toplevel_handle_closed(void *data, + struct ext_foreign_toplevel_handle_v1 *handle) +{ + WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; + WayfireStreamChooserApp::getInstance().remove_toplevel(toplevel); + ext_foreign_toplevel_handle_v1_destroy(handle); +} + +static void toplevel_handle_done(void *data, + struct ext_foreign_toplevel_handle_v1 *handle) +{ + WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; + toplevel->commit(); +} + +static void toplevel_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 toplevel_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 toplevel_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 = toplevel_handle_closed, + .done = toplevel_handle_done, + .title = toplevel_handle_title, + .app_id = toplevel_handle_app_id, + .identifier = toplevel_handle_identifier, +}; + +/* SHM Callbacks*/ +static int backingfile(off_t size) +{ + static int count; + char name[128]; + sprintf(name, "/tmp/wf-stream-chooser-%d-XXXXXX", count++); + int fd = mkstemp(name); + if (fd < 0) + { + perror("mkstemp"); + return -1; + } + + int ret; + while ((ret = ftruncate(fd, size)) == EINTR) + {} + + if (ret < 0) + { + perror("ret < 0"); + close(fd); + return -1; + } + + unlink(name); + return fd; +} + +static struct wl_buffer *toplevel_create_shm_buffer(int width, int height, void **data_out, size_t *size) +{ + *size = width * 4 * height; + + int fd = backingfile(*size); + if (fd < 0) + { + perror("Creating a buffer file failed"); + return NULL; + } + + void *data = mmap(NULL, *size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (data == MAP_FAILED) + { + perror("mmap failed"); + close(fd); + return NULL; + } + + wl_shm_pool *pool = wl_shm_create_pool(WayfireStreamChooserApp::getInstance().shm, fd, *size); + close(fd); + wl_buffer *buffer = wl_shm_pool_create_buffer(pool, 0, width, height, + width * 4, WL_SHM_FORMAT_ABGR8888); + wl_shm_pool_destroy(pool); + + *data_out = data; + return buffer; +} + +void toplevel_free_shm_buffer(std::shared_ptr& buffer) +{ + if (buffer->buffer == NULL) + { + return; + } + + munmap(buffer->data, buffer->size); + wl_buffer_destroy(buffer->buffer); + buffer->buffer = NULL; +} + +static void session_handle_buffer_size(void *data, + struct ext_image_copy_capture_session_v1*, + uint32_t width, uint32_t height) +{ + WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; + toplevel->current_buffer_width = width; + toplevel->current_buffer_height = height; +} + +static void session_handle_shm_format(void *data, + struct ext_image_copy_capture_session_v1*, + uint32_t format) +{ + WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; + toplevel->current_buffer_format = format; +} + +static void session_handle_dmabuf_device(void*, + struct ext_image_copy_capture_session_v1*, + struct wl_array*) +{} + +static void session_handle_dmabuf_format(void*, + struct ext_image_copy_capture_session_v1*, + uint32_t, + struct wl_array*) +{} + +static void session_handle_done(void *data, + struct ext_image_copy_capture_session_v1*) +{ + WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; + toplevel->size(); +} + +static void session_handle_stopped(void*, + struct ext_image_copy_capture_session_v1 *session) +{ + ext_image_copy_capture_session_v1_destroy(session); + session = NULL; +} + +static const struct ext_image_copy_capture_session_v1_listener recording_session_listener = { + .buffer_size = session_handle_buffer_size, + .shm_format = session_handle_shm_format, + .dmabuf_device = session_handle_dmabuf_device, + .dmabuf_format = session_handle_dmabuf_format, + .done = session_handle_done, + .stopped = session_handle_stopped, +}; + +/* Copy Capture Callbacks */ + +static void frame_handle_transform(void*, + struct ext_image_copy_capture_frame_v1*, + uint32_t) +{} + +static void frame_handle_damage(void*, + struct ext_image_copy_capture_frame_v1*, + int32_t, int32_t, int32_t, int32_t) +{} + +static void frame_handle_presentation_time(void*, + struct ext_image_copy_capture_frame_v1*, + uint32_t, uint32_t, uint32_t) +{} + +static void frame_handle_ready(void *data, + struct ext_image_copy_capture_frame_v1*) +{ + WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; + toplevel->buffer_ready(); +} + +static void frame_handle_failed(void*, + struct ext_image_copy_capture_frame_v1 *handle, + uint32_t reason) +{ + std::cerr << "Failed to copy frame because reason: " << reason << std::endl; + ext_image_copy_capture_frame_v1_destroy(handle); +} + +static const struct ext_image_copy_capture_frame_v1_listener frame_listener = { + .transform = frame_handle_transform, + .damage = frame_handle_damage, + .presentation_time = frame_handle_presentation_time, + .ready = frame_handle_ready, + .failed = frame_handle_failed, +}; + +void WayfireChooserTopLevel::grab_toplevel_screenshot() +{ + if (WayfireStreamChooserApp::getInstance().is_in_use) + { + Glib::signal_timeout().connect( + [this] () + { + grab_toplevel_screenshot(); + return G_SOURCE_REMOVE; + }, 100); + return; + } + + WayfireStreamChooserApp::getInstance().is_in_use = true; + + auto copy_capture_source = ext_foreign_toplevel_image_capture_source_manager_v1_create_source( + WayfireStreamChooserApp::getInstance().toplevel_capture_manager, + handle); + recording_session = ext_image_copy_capture_manager_v1_create_session( + WayfireStreamChooserApp::getInstance().manager, + copy_capture_source, + 0); + ext_image_copy_capture_session_v1_add_listener(recording_session, &recording_session_listener, this); + ext_image_capture_source_v1_destroy(copy_capture_source); +} + +void WayfireChooserTopLevel::size() +{ + if ((current_buffer_width <= 0) || (current_buffer_height <= 0)) + { + printf("%s invalid size\n", __func__); + return; + } + + buffer = std::make_shared(); + + if (frame) + { + ext_image_copy_capture_frame_v1_destroy(frame); + frame = NULL; + } + + buffer->width = current_buffer_width; + buffer->height = current_buffer_height; + + frame = ext_image_copy_capture_session_v1_create_frame(recording_session); + buffer->frame = frame; + ext_image_copy_capture_frame_v1_add_listener(buffer->frame, &frame_listener, this); + + toplevel_free_shm_buffer(buffer); + buffer->buffer = + toplevel_create_shm_buffer(buffer->width, buffer->height, &buffer->data, &buffer->size); + + if (buffer->buffer == NULL) + { + printf("%s failed to create buffer\n", __func__); + exit(EXIT_FAILURE); + } + + ext_image_copy_capture_frame_v1_attach_buffer(buffer->frame, buffer->buffer); + ext_image_copy_capture_frame_v1_damage_buffer(buffer->frame, 0, 0, buffer->width, buffer->height); + ext_image_copy_capture_frame_v1_capture(buffer->frame); +} + +void WayfireChooserTopLevel::buffer_ready() +{ + if ((buffer == nullptr) || (buffer->buffer == nullptr)) + { + printf("%s buffer null\n", __func__); + + return; + } + + /* buffer->data is now valid */ + std::shared_ptr bytes = 0; + size_t size = buffer->size; + if (buffer->data) + { + bytes = Glib::Bytes::create(buffer->data, size); + } + + if (!bytes) + { + return; + } + + auto builder = Gdk::MemoryTextureBuilder::create(); + builder->set_bytes(bytes); + builder->set_width(buffer->width); + builder->set_height(buffer->height); + builder->set_stride(buffer->width * 4); + builder->set_format(Gdk::MemoryFormat::R8G8B8A8); + + auto texture = builder->build(); + + screenshot.set_paintable(texture); + ext_image_copy_capture_frame_v1_destroy(frame); + frame = NULL; + ext_image_copy_capture_session_v1_destroy(recording_session); + recording_session = NULL; + WayfireStreamChooserApp::getInstance().is_in_use = false; +} + +/* Gtk Overlay showing information about a window */ +WayfireChooserTopLevel::WayfireChooserTopLevel(ext_foreign_toplevel_handle_v1 *handle) : handle(handle) +{ + set_size_request(150, 150); + set_valign(Gtk::Align::FILL); + set_halign(Gtk::Align::FILL); + layout = std::make_shared(); + set_layout_manager(layout); + append(overlay); + append(label); + overlay.set_child(screenshot); + overlay.add_overlay(icon); + icon.set_halign(Gtk::Align::START); + icon.set_valign(Gtk::Align::END); + screenshot.set_halign(Gtk::Align::FILL); + screenshot.set_valign(Gtk::Align::FILL); + 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; + IconProvider::image_set_icon(icon, 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 = ""; + } + + /* If we have the protocols, grab a screenshot */ + if (WayfireStreamChooserApp::getInstance().has_image_copy_capture) + { + grab_toplevel_screenshot(); + } +} + +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..77c72580 --- /dev/null +++ b/src/stream-chooser/toplevelwidget.hpp @@ -0,0 +1,48 @@ +#pragma once +#include +#include +#include "ext-foreign-toplevel-list-v1-client-protocol.h" +#include "ext-image-copy-capture-v1-client-protocol.h" +#include "toplevellayout.hpp" + +struct toplevel_buffer +{ + int width = 0; + int height = 0; + void *data; + wl_buffer *buffer; + size_t size = 0; + ext_image_copy_capture_frame_v1 *frame = NULL; +}; + +class WayfireChooserTopLevel : public Gtk::Box +{ + private: + Gtk::Overlay overlay; + Gtk::Image icon; + Gtk::Picture screenshot; + Gtk::Label label; + + std::string buffered_title = "", title = ""; + std::string buffered_app_id = "", app_id = ""; + std::string buffered_identifier = "", identifier = ""; + Glib::RefPtr layout; + + std::shared_ptr buffer = nullptr; + ext_image_copy_capture_frame_v1 *frame = NULL; + ext_image_copy_capture_session_v1 *recording_session = NULL; + + public: + int current_buffer_width = 0, current_buffer_height = 0, current_buffer_format = 0; + ext_foreign_toplevel_handle_v1 *handle = nullptr; + 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 grab_toplevel_screenshot(); + void size(); + void buffer_ready(); + void print(); +};