From 5f32c91ec5c3153ede9abb2d64fb831f36e9c44e Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Mon, 3 Jun 2024 14:29:06 +0200 Subject: [PATCH] Choose monitor with GTK dialog Fixes: https://github.com/QubesOS/qubes-issues/issues/9282 --- doc/qubes-video-companion.rst | 2 +- qubes-rpc/services/qvc.ScreenShare | 22 +------ sender/screenshare.py | 98 ++++++++++++++++++++++++++---- 3 files changed, 89 insertions(+), 33 deletions(-) diff --git a/doc/qubes-video-companion.rst b/doc/qubes-video-companion.rst index c8a18ea..68bcb21 100644 --- a/doc/qubes-video-companion.rst +++ b/doc/qubes-video-companion.rst @@ -21,7 +21,7 @@ The project emphasizes correctness and security all the while also sporting supe OPTIONS ======= resolution - The video resolution to stream and receive video in. The format is [WIDTHxHEIGHTxFPS], meaning resolution is optional. If you set the environment variable "QVC_MONITOR" in the target, that monitor is going to be preferred and if not found, will fallback to the primary monitor. Example: "1920x1080x60" + The video resolution to stream and receive video in. The format is [WIDTHxHEIGHTxFPS], meaning resolution is optional. Example: "1920x1080x60" video_source diff --git a/qubes-rpc/services/qvc.ScreenShare b/qubes-rpc/services/qvc.ScreenShare index a350ed3..ce6e13a 100755 --- a/qubes-rpc/services/qvc.ScreenShare +++ b/qubes-rpc/services/qvc.ScreenShare @@ -5,27 +5,7 @@ set -eu -## DISPLAY variable used by: xrandr, zenity -export DISPLAY=:0 - true "${XDG_RUNTIME_DIR:="/run/user/$(id -u)"}" true "${DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus"}" -monitors="$(xrandr --listactivemonitors \ - | awk '/^ [0-9]+: \+/ { print "FALSE", $4, $3 }')" -monitor_count="$(echo "${monitors}" | wc -l)" -monitor_longest_line="$(echo "${monitors}" | wc -L)" -dialog_height="$((monitor_count*50+60))" -dialog_width="$((monitor_longest_line*10))" -if test "${monitor_count}" -gt 1; then - # shellcheck disable=SC2086 - QVC_MONITOR="$(zenity --list --radiolist \ - --height="${dialog_height}" --width="${dialog_width}" \ - --column "ID" --column "Name" --column "Resolution" \ - --title "Screen share" \ - --text "Select monitor to present to qube ${QREXEC_REMOTE_DOMAIN}" \ - ${monitors})" -fi -true "${QVC_MONITOR:=}" - -export XDG_RUNTIME_DIR DBUS_SESSION_BUS_ADDRESS QVC_MONITOR +export DISPLAY=:0 XDG_RUNTIME_DIR DBUS_SESSION_BUS_ADDRESS exec python3 -- /usr/share/qubes-video-companion/sender/screenshare.py diff --git a/sender/screenshare.py b/sender/screenshare.py index 8f29130..601ac88 100644 --- a/sender/screenshare.py +++ b/sender/screenshare.py @@ -2,6 +2,7 @@ # Copyright (C) 2021 Elliot Killick # Copyright (C) 2021 Demi Marie Obenour +# Copyright (C) 2024 Benjamin Grande M. S. # Licensed under the MIT License. See LICENSE file for details. """Screen sharing video source module""" @@ -10,10 +11,9 @@ # pylint: disable=wrong-import-position import gi -import os - gi.require_version("Gdk", "3.0") -from gi.repository import Gdk +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gdk, GdkPixbuf from service import Service from typing import List, Tuple @@ -22,6 +22,7 @@ class ScreenShare(Service): """Screen sharing video souce class""" def __init__(self) -> None: + self.selected_monitor_index = None self.main(self) def video_source(self) -> str: @@ -30,16 +31,91 @@ def video_source(self) -> str: def icon(self) -> str: return "video-display" - def parameters(self) -> Tuple[int, int, int]: + def monitor_dialog(self) -> None: display = Gdk.Display().get_default() monitor_count = display.get_n_monitors() - monitor_wanted = os.environ["QVC_MONITOR"] - ## If wanted monitor is not found, use the primary monitor (0). - monitor_index = 0 - for m in range(monitor_count): - if display.get_monitor(m).get_model() == monitor_wanted: - monitor_index = m - break + + if monitor_count == 1: + self.selected_monitor_index = 0 + return + + combobox = Gtk.ComboBoxText() + monitor_screenshots = [] + for monitor_num in range(monitor_count): + monitor_name = display.get_monitor(monitor_num).get_model() + monitor = display.get_monitor(monitor_num) + monitor_geometry = monitor.get_geometry() + monitor_width = monitor_geometry.width + monitor_height = monitor_geometry.height + monitor_x = monitor_geometry.x + monitor_y = monitor_geometry.y + combobox.append_text(f"{monitor_name}: " + f"{monitor_width}x{monitor_height} " + f"{monitor_x}+{monitor_y}") + + pixbuf = Gdk.pixbuf_get_from_window( + display.get_default_screen().get_root_window(), + monitor_geometry.x, + monitor_geometry.y, + monitor_geometry.width, + monitor_geometry.height) + pixbuf = pixbuf.scale_simple( + min(800, pixbuf.get_width()), + min(600, pixbuf.get_height()), + GdkPixbuf.InterpType.BILINEAR) + monitor_screenshots.append(pixbuf) + + window = Gtk.Window(title="Qubes Screen Share") + window.connect("destroy", Gtk.main_quit) + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + window.add(vbox) + + ## TODO: add 'remote_domain' to differentiate calls. + label = Gtk.Label(label="Select a monitor:") + vbox.pack_start(label, False, False, 0) + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + vbox.pack_start(hbox, False, False, 0) + + ## Select first monitor when opening the dialog. + combobox.set_active(0) + + hbox.pack_start(combobox, True, True, 0) + ok_button = Gtk.Button(label="OK") + cancel_button = Gtk.Button(label="Cancel") + hbox.pack_end(ok_button, False, False, 0) + hbox.pack_end(cancel_button, False, False, 0) + + def on_ok_button_clicked(_): + self.selected_monitor_index = combobox.get_active() + window.close() + + def on_cancel_button_clicked(_): + window.close() + + ok_button.connect("clicked", on_ok_button_clicked) + cancel_button.connect("clicked", on_cancel_button_clicked) + + image = Gtk.Image() + vbox.pack_start(image, True, True, 0) + + def on_combobox_changed(combobox): + monitor_index = combobox.get_active() + pixbuf = monitor_screenshots[monitor_index] + image.set_from_pixbuf(pixbuf) + + combobox.connect("changed", on_combobox_changed) + ## Show the first monitor screenshot when opening the dialog. + on_combobox_changed(combobox) + + window.show_all() + Gtk.main() + + def parameters(self) -> Tuple[int, int, int]: + display = Gdk.Display().get_default() + self.monitor_dialog() + monitor_index = self.selected_monitor_index + if monitor_index is None: + raise ValueError("Monitor index was not set") geometry = display.get_monitor(monitor_index).get_geometry() screen = Gdk.Screen().get_default() kwargs = {