@@ -1335,13 +1335,36 @@
# @show-menubar: Display the main window menubar. Defaults to "on".
# (Since 8.0)
#
+# @connectors: A list of physical monitor/connector names where the
+# GTK windows containing the respective GFX VCs (virtual consoles)
+# are to be placed. The connector names should be provided as
+# a string, with each name separated by a colon
+# (e.g., DP-1:DP-2:HDMI-1:HDMI-2). Each connector name in the
+# string will be used as a label for each VC in order.
+# VCs can be skipped by leaving an empty spot between colons
+# (e.g., DP-1::HDMI-2). If a connector name is not provided for
+# a VC, that VC will not be placed on any physical display and
+# the guest will see it as disconnected. If a valid connector name
+# is provided for a VC but its display cable is not plugged in
+# when the guest is launched, the VC will not be displayed initially.
+# It will appear on the display when the cable is plugged in
+# (hot-plug). If the cable is disconnected, the VC will be hidden
+# and the guest will see its virtual display as disconnected.
+# Multiple VCs can share the same connector name. In this case,
+# all VCs with that name will be displayed on the same physical
+# monitor. However, a single VC cannot have multiple connector
+# names.
+#
+# (Since 9.1)
+#
# Since: 2.12
##
{ 'struct' : 'DisplayGTK',
'data' : { '*grab-on-hover' : 'bool',
'*zoom-to-fit' : 'bool',
'*show-tabs' : 'bool',
- '*show-menubar' : 'bool' } }
+ '*show-menubar' : 'bool',
+ '*connectors' : 'str' } }
##
# @DisplayEGLHeadless:
@@ -83,6 +83,7 @@ typedef struct VirtualConsole {
GtkWidget *menu_item;
GtkWidget *tab_item;
GtkWidget *focus;
+ GdkMonitor *monitor;
VirtualConsoleType type;
union {
VirtualGfxConsole gfx;
@@ -38,6 +38,7 @@
#include "qemu/cutils.h"
#include "qemu/error-report.h"
#include "qemu/main-loop.h"
+#include "qemu/option.h"
#include "ui/console.h"
#include "ui/gtk.h"
@@ -1446,6 +1447,248 @@ static void gd_menu_untabify(GtkMenuItem *item, void *opaque)
}
}
+static void gd_ui_mon_enable(VirtualConsole *vc)
+{
+ GdkWindow *window = gtk_widget_get_window(vc->gfx.drawing_area);
+ QemuUIInfo info;
+
+ if (!dpy_ui_info_supported(vc->gfx.dcl.con)) {
+ return;
+ }
+
+ info = *dpy_get_ui_info(vc->gfx.dcl.con);
+ info.width = gdk_window_get_width(window);
+ info.height = gdk_window_get_height(window);
+ dpy_set_ui_info(vc->gfx.dcl.con, &info, false);
+}
+
+static void gd_ui_mon_disable(VirtualConsole *vc)
+{
+ QemuUIInfo info;
+
+ if (!dpy_ui_info_supported(vc->gfx.dcl.con)) {
+ return;
+ }
+
+ info = *dpy_get_ui_info(vc->gfx.dcl.con);
+ info.width = 0;
+ info.height = 0;
+ dpy_set_ui_info(vc->gfx.dcl.con, &info, false);
+}
+
+static void gd_window_show_on_monitor(GdkDisplay *dpy, VirtualConsole *vc,
+ gint monitor_num)
+{
+ GtkDisplayState *s = vc->s;
+ GdkMonitor *monitor = gdk_display_get_monitor(dpy, monitor_num);
+ GdkRectangle geometry;
+ if (!vc->window) {
+ gd_tab_window_create(vc);
+ }
+
+ gdk_window_show(gtk_widget_get_window(vc->window));
+ gd_update_windowsize(vc);
+ gdk_monitor_get_geometry(monitor, &geometry);
+
+ gtk_window_move(GTK_WINDOW(vc->window), geometry.x, geometry.y);
+
+ if (s->opts->has_full_screen && s->opts->full_screen) {
+ gtk_widget_set_size_request(vc->gfx.drawing_area, -1, -1);
+ gtk_window_fullscreen(GTK_WINDOW(vc->window));
+ } else if ((s->window == vc->window) && s->full_screen) {
+ gd_menu_show_tabs(GTK_MENU_ITEM(s->show_tabs_item), s);
+ if (gtk_check_menu_item_get_active(
+ GTK_CHECK_MENU_ITEM(s->show_menubar_item))) {
+ gtk_widget_show(s->menu_bar);
+ }
+ s->full_screen = false;
+ }
+
+ vc->monitor = monitor;
+ gd_ui_mon_enable(vc);
+ gd_update_cursor(vc);
+}
+
+static int gd_monitor_lookup(GdkDisplay *dpy, char *label)
+{
+ GdkMonitor *monitor;
+ int total_monitors = gdk_display_get_n_monitors(dpy);
+ const char *model;
+ int i;
+
+ for (i = 0; i < total_monitors; i++) {
+ monitor = gdk_display_get_monitor(dpy, i);
+ model = gdk_monitor_get_model(monitor);
+ if (!model) {
+ g_warning("retrieving connector name using\n"
+ "gdk_monitor_get_model isn't supported\n"
+ "please do not use connectors param in\n"
+ "current environment\n");
+ return -1;
+ }
+
+ if (monitor && !g_strcmp0(model, label)) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+static gboolean gd_vc_is_misplaced(GdkDisplay *dpy, GdkMonitor *monitor,
+ VirtualConsole *vc)
+{
+ GdkWindow *window = gtk_widget_get_window(vc->gfx.drawing_area);
+ GdkMonitor *mon = gdk_display_get_monitor_at_window(dpy, window);
+ const char *model = gdk_monitor_get_model(monitor);
+
+ if (!vc->monitor) {
+ if (!g_strcmp0(model, vc->label)) {
+ return TRUE;
+ }
+ } else {
+ if (mon && mon != vc->monitor) {
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static void gd_vc_windows_reposition(GdkDisplay *dpy, GtkDisplayState *s)
+{
+ VirtualConsole *vc;
+ GdkMonitor *monitor;
+ gint monitor_num;
+ int i;
+
+ for (i = 0; i < s->nb_vcs; i++) {
+ vc = &s->vc[i];
+ if (vc->label) {
+ monitor_num = gd_monitor_lookup(dpy, vc->label);
+ if (monitor_num >= 0) {
+ monitor = gdk_display_get_monitor(dpy, monitor_num);
+ if (gd_vc_is_misplaced(dpy, monitor, vc)) {
+ gd_window_show_on_monitor(dpy, vc, monitor_num);
+ }
+ } else if (vc->monitor) {
+ vc->monitor = NULL;
+ gd_ui_mon_disable(vc);
+
+ /* if window exist, hide it */
+ if (vc->window) {
+ gdk_window_hide(gtk_widget_get_window(vc->window));
+ }
+ }
+ }
+ }
+}
+
+static void gd_monitors_reset_timer(void *opaque)
+{
+ GtkDisplayState *s = opaque;
+ GdkDisplay *dpy = gdk_display_get_default();
+
+ gd_vc_windows_reposition(dpy, s);
+}
+
+static void gd_monitors_changed(GdkScreen *scr, void *opaque)
+{
+ GtkDisplayState *s = opaque;
+ QEMUTimer *mon_reset_timer;
+
+ /* This timer setup ensures the compositor finishes placing
+ * all QEMU windows after a display hot plug event
+ * before QEMU rearranges the windows based on connectors
+ * setting.
+ */
+ mon_reset_timer = timer_new_ms(QEMU_CLOCK_REALTIME,
+ gd_monitors_reset_timer, s);
+ timer_mod(mon_reset_timer,
+ qemu_clock_get_ms(QEMU_CLOCK_REALTIME) + 2000);
+}
+
+static VirtualConsole *gd_next_gfx_vc(GtkDisplayState *s)
+{
+ VirtualConsole *vc;
+ int i;
+
+ for (i = 0; i < s->nb_vcs; i++) {
+ vc = &s->vc[i];
+ if (vc->type == GD_VC_GFX &&
+ qemu_console_is_graphic(vc->gfx.dcl.con) &&
+ !vc->label) {
+ return vc;
+ }
+ }
+ return NULL;
+}
+
+static void gd_vc_free_labels(GtkDisplayState *s)
+{
+ VirtualConsole *vc;
+ int i;
+
+ for (i = 0; i < s->nb_vcs; i++) {
+ vc = &s->vc[i];
+ if (vc->type == GD_VC_GFX &&
+ qemu_console_is_graphic(vc->gfx.dcl.con)) {
+ g_free(vc->label);
+ vc->label = NULL;
+ }
+ }
+}
+
+static void gd_connectors_init(GdkDisplay *dpy, GtkDisplayState *s)
+{
+ VirtualConsole *vc;
+ gint monitor_num;
+ gboolean first_vc = TRUE;
+ char *conn = s->opts->u.gtk.connectors;
+ char *this, *ptr;
+
+ gtk_notebook_set_show_tabs(GTK_NOTEBOOK(s->notebook), FALSE);
+ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(s->grab_item),
+ FALSE);
+ gd_vc_free_labels(s);
+
+ ptr = conn;
+
+ while (ptr < conn + strlen(conn)) {
+ this = strchr(ptr, ':');
+
+ vc = gd_next_gfx_vc(s);
+ if (!vc) {
+ break;
+ }
+ if (first_vc) {
+ vc->window = s->window;
+ first_vc = FALSE;
+ }
+
+ if (this == NULL) {
+ vc->label = g_strdup(ptr);
+ } else {
+ vc->label = g_strndup(ptr, this - ptr);
+ }
+
+ monitor_num = gd_monitor_lookup(dpy, vc->label);
+ if (monitor_num >= 0) {
+ gd_window_show_on_monitor(dpy, vc, monitor_num);
+ } else {
+ gd_ui_mon_disable(vc);
+
+ if (vc->window) {
+ gdk_window_hide(gtk_widget_get_window(vc->window));
+ }
+ }
+
+ if (this == NULL) {
+ break;
+ } else {
+ ptr = this + 1;
+ }
+ }
+}
+
static void gd_menu_show_menubar(GtkMenuItem *item, void *opaque)
{
GtkDisplayState *s = opaque;
@@ -2102,6 +2345,10 @@ static void gd_connect_signals(GtkDisplayState *s)
G_CALLBACK(gd_menu_grab_input), s);
g_signal_connect(s->notebook, "switch-page",
G_CALLBACK(gd_change_page), s);
+ if (s->opts->u.gtk.connectors) {
+ g_signal_connect(gdk_screen_get_default(), "monitors-changed",
+ G_CALLBACK(gd_monitors_changed), s);
+ }
}
static GtkWidget *gd_create_menu_machine(GtkDisplayState *s)
@@ -2489,6 +2736,9 @@ static void gtk_display_init(DisplayState *ds, DisplayOptions *opts)
opts->u.gtk.show_tabs) {
gtk_menu_item_activate(GTK_MENU_ITEM(s->show_tabs_item));
}
+ if (s->opts->u.gtk.connectors) {
+ gd_connectors_init(window_display, s);
+ }
#ifdef CONFIG_GTK_CLIPBOARD
gd_clipboard_init(s);
#endif /* CONFIG_GTK_CLIPBOARD */
@@ -2099,6 +2099,7 @@ DEF("display", HAS_ARG, QEMU_OPTION_display,
"-display gtk[,full-screen=on|off][,gl=on|off][,grab-on-hover=on|off]\n"
" [,show-tabs=on|off][,show-cursor=on|off][,window-close=on|off]\n"
" [,show-menubar=on|off][,zoom-to-fit=on|off]\n"
+ " [,connectors=conn1:conn2:...:connN]]\n"
#endif
#if defined(CONFIG_VNC)
"-display vnc=<display>[,<optargs>]\n"
@@ -2195,6 +2196,9 @@ SRST
``zoom-to-fit=on|off`` : Expand video output to the window size,
defaults to "off"
+ ``connectors=conn1:conn2...`` : VC to connector mappings to display the VC
+ window on a specific monitor
+
``curses[,charset=<encoding>]``
Display video output via curses. For graphics device models
which support a text mode, QEMU can display this output using a