diff options
Diffstat (limited to 'src/rcw.c')
-rw-r--r-- | src/rcw.c | 4812 |
1 files changed, 4812 insertions, 0 deletions
diff --git a/src/rcw.c b/src/rcw.c new file mode 100644 index 0000000..8d00965 --- /dev/null +++ b/src/rcw.c @@ -0,0 +1,4812 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2009-2011 Vic Lee + * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. * If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. * If you + * do not wish to do so, delete this exception statement from your + * version. * If you delete this exception statement from all source + * files in the program, then also delete it here. + * + */ + + +#include "config.h" + +#ifdef GDK_WINDOWING_X11 +#include <cairo/cairo-xlib.h> +#else +#include <cairo/cairo.h> +#endif +#include <gdk/gdk.h> +#include <gdk/gdkkeysyms.h> +#include <glib/gi18n.h> +#include <stdlib.h> + +#include "remmina.h" +#include "remmina_main.h" +#include "rcw.h" +#include "remmina_applet_menu_item.h" +#include "remmina_applet_menu.h" +#include "remmina_file.h" +#include "remmina_file_manager.h" +#include "remmina_log.h" +#include "remmina_message_panel.h" +#include "remmina_ext_exec.h" +#include "remmina_plugin_manager.h" +#include "remmina_pref.h" +#include "remmina_protocol_widget.h" +#include "remmina_public.h" +#include "remmina_scrolled_viewport.h" +#include "remmina_unlock.h" +#include "remmina_utils.h" +#include "remmina_widget_pool.h" +#include "remmina/remmina_trace_calls.h" + +#ifdef GDK_WINDOWING_WAYLAND +#include <gdk/gdkwayland.h> +#endif + + +#define DEBUG_KB_GRABBING 0 +#include "remmina_exec.h" + +gchar *remmina_pref_file; +RemminaPref remmina_pref; + +G_DEFINE_TYPE(RemminaConnectionWindow, rcw, GTK_TYPE_WINDOW) + +#define MOTION_TIME 100 + +/* default timeout used to hide the floating toolbar when switching profile */ +#define TB_HIDE_TIME_TIME 1500 + +#define FULL_SCREEN_TARGET_MONITOR_UNDEFINED -1 + +struct _RemminaConnectionWindowPriv { + GtkNotebook * notebook; + GtkWidget * floating_toolbar_widget; + GtkWidget * overlay; + GtkWidget * revealer; + GtkWidget * overlay_ftb_overlay; + GtkWidget * overlay_ftb_fr; + + GtkWidget * floating_toolbar_label; + gdouble floating_toolbar_opacity; + + /* Various delayed and timer event source ids */ + guint acs_eventsourceid; // timeout + guint spf_eventsourceid; // idle + guint grab_retry_eventsourceid; // timeout + guint delayed_grab_eventsourceid; + guint ftb_hide_eventsource; // timeout + guint tar_eventsource; // timeout + guint hidetb_eventsource; // timeout + guint dwp_eventsourceid; // timeout + + GtkWidget * toolbar; + GtkWidget * grid; + + /* Toolitems that need to be handled */ + GtkToolItem * toolitem_menu; + GtkToolItem * toolitem_autofit; + GtkToolItem * toolitem_fullscreen; + GtkToolItem * toolitem_switch_page; + GtkToolItem * toolitem_dynres; + GtkToolItem * toolitem_scale; + GtkToolItem * toolitem_grab; + GtkToolItem * toolitem_multimon; + GtkToolItem * toolitem_preferences; + GtkToolItem * toolitem_tools; + GtkToolItem * toolitem_new; + GtkToolItem * toolitem_duplicate; + GtkToolItem * toolitem_screenshot; + GtkWidget * fullscreen_option_button; + GtkWidget * fullscreen_scaler_button; + GtkWidget * scaler_option_button; + + GtkWidget * pin_button; + gboolean pin_down; + + gboolean sticky; + + /* Flag to turn off toolbar signal handling when toolbar is + * reconfiguring, usually due to a tab switch */ + gboolean toolbar_is_reconfiguring; + + /* This is the current view mode, i.e. VIEWPORT_FULLSCREEN_MODE, + * as saved on the "viwemode" profile preference file */ + gint view_mode; + + /* Status variables used when in fullscreen mode. Needed + * to restore a fullscreen mode after coming from scrolled */ + gint fss_view_mode; + /* Status variables used when in scrolled window mode. Needed + * to restore a scrolled window mode after coming from fullscreen */ + gint ss_width, ss_height; + gboolean ss_maximized; + + gboolean kbcaptured; + gboolean pointer_captured; + gboolean hostkey_activated; + gboolean hostkey_used; + + gboolean pointer_entered; + + RemminaConnectionWindowOnDeleteConfirmMode on_delete_confirm_mode; +}; + +typedef struct _RemminaConnectionObject { + RemminaConnectionWindow * cnnwin; + RemminaFile * remmina_file; + + GtkWidget * proto; + GtkWidget * aspectframe; + GtkWidget * viewport; + + GtkWidget * scrolled_container; + + gboolean plugin_can_scale; + + gboolean connected; + gboolean dynres_unlocked; + + gulong deferred_open_size_allocate_handler; +} RemminaConnectionObject; + +enum { + TOOLBARPLACE_SIGNAL, + LAST_SIGNAL +}; + +static guint rcw_signals[LAST_SIGNAL] = +{ 0 }; + +static RemminaConnectionWindow *rcw_create_scrolled(gint width, gint height, gboolean maximize); +static RemminaConnectionWindow *rcw_create_fullscreen(GtkWindow *old, gint view_mode); +static gboolean rcw_hostkey_func(RemminaProtocolWidget *gp, guint keyval, gboolean release); +static GtkWidget *rco_create_tab_page(RemminaConnectionObject *cnnobj); +static GtkWidget *rco_create_tab_label(RemminaConnectionObject *cnnobj); + +void rcw_grab_focus(RemminaConnectionWindow *cnnwin); +static GtkWidget *rcw_create_toolbar(RemminaConnectionWindow *cnnwin, gint mode); +static void rcw_place_toolbar(GtkToolbar *toolbar, GtkGrid *grid, GtkWidget *sibling, int toolbar_placement); +static void rco_update_toolbar(RemminaConnectionObject *cnnobj); +static void rcw_keyboard_grab(RemminaConnectionWindow *cnnwin); +static GtkWidget *rcw_append_new_page(RemminaConnectionWindow *cnnwin, RemminaConnectionObject *cnnobj); + + +static void rcw_ftb_drag_begin(GtkWidget *widget, GdkDragContext *context, gpointer user_data); + +static const GtkTargetEntry dnd_targets_ftb[] = +{ + { + (char *)"text/x-remmina-ftb", + GTK_TARGET_SAME_APP | GTK_TARGET_OTHER_WIDGET, + 0 + }, +}; + +static const GtkTargetEntry dnd_targets_tb[] = +{ + { + (char *)"text/x-remmina-tb", + GTK_TARGET_SAME_APP, + 0 + }, +}; + +static void rcw_class_init(RemminaConnectionWindowClass *klass) +{ + TRACE_CALL(__func__); + GtkCssProvider *provider; + + provider = gtk_css_provider_new(); + + /* It’s important to remove padding, border and shadow from GtkViewport or + * we will never know its internal area size, because GtkViweport::viewport_get_view_allocation, + * which returns the internal size of the GtkViewport, is private and we cannot access it */ + +#if GTK_CHECK_VERSION(3, 14, 0) + gtk_css_provider_load_from_data(provider, + "#remmina-cw-viewport, #remmina-cw-aspectframe {\n" + " padding:0;\n" + " border:0;\n" + " background-color: black;\n" + "}\n" + "GtkDrawingArea {\n" + "}\n" + "GtkToolbar {\n" + " -GtkWidget-window-dragging: 0;\n" + "}\n" + "#remmina-connection-window-fullscreen {\n" + " border-color: black;\n" + "}\n" + "#remmina-small-button {\n" + " outline-offset: 0;\n" + " outline-width: 0;\n" + " padding: 0;\n" + " border: 0;\n" + "}\n" + "#remmina-pin-button {\n" + " outline-offset: 0;\n" + " outline-width: 0;\n" + " padding: 2px;\n" + " border: 0;\n" + "}\n" + "#remmina-tab-page {\n" + " background-color: black;\n" + "}\n" + "#remmina-scrolled-container {\n" + "}\n" + "#remmina-scrolled-container.undershoot {\n" + " background: none;\n" + "}\n" + "#remmina-tab-page {\n" + "}\n" + "#ftbbox-upper {\n" + " background-color: white;\n" + " color: black;\n" + " border-style: none solid solid solid;\n" + " border-width: 1px;\n" + " border-radius: 4px;\n" + " padding: 0px;\n" + "}\n" + "#ftbbox-lower {\n" + " background-color: white;\n" + " color: black;\n" + " border-style: solid solid none solid;\n" + " border-width: 1px;\n" + " border-radius: 4px;\n" + " padding: 0px;\n" + "}\n" + "#ftb-handle {\n" + "}\n" + ".message_panel {\n" + " border: 0px solid;\n" + " padding: 20px 20px 20px 20px;\n" + "}\n" + ".message_panel entry {\n" + " background-image: none;\n" + " border-width: 4px;\n" + " border-radius: 8px;\n" + "}\n" + ".message_panel .title_label {\n" + " font-size: 2em; \n" + "}\n" + , -1, NULL); + +#else + gtk_css_provider_load_from_data(provider, + "#remmina-cw-viewport, #remmina-cw-aspectframe {\n" + " padding:0;\n" + " border:0;\n" + " background-color: black;\n" + "}\n" + "#remmina-cw-message-panel {\n" + "}\n" + "GtkDrawingArea {\n" + "}\n" + "GtkToolbar {\n" + " -GtkWidget-window-dragging: 0;\n" + "}\n" + "#remmina-connection-window-fullscreen {\n" + " border-color: black;\n" + "}\n" + "#remmina-small-button {\n" + " -GtkWidget-focus-padding: 0;\n" + " -GtkWidget-focus-line-width: 0;\n" + " padding: 0;\n" + " border: 0;\n" + "}\n" + "#remmina-pin-button {\n" + " -GtkWidget-focus-padding: 0;\n" + " -GtkWidget-focus-line-width: 0;\n" + " padding: 2px;\n" + " border: 0;\n" + "}\n" + "#remmina-scrolled-container {\n" + "}\n" + "#remmina-scrolled-container.undershoot {\n" + " background: none\n" + "}\n" + "#remmina-tab-page {\n" + "}\n" + "#ftbbox-upper {\n" + " border-style: none solid solid solid;\n" + " border-width: 1px;\n" + " border-radius: 4px;\n" + " padding: 0px;\n" + "}\n" + "#ftbbox-lower {\n" + " border-style: solid solid none solid;\n" + " border-width: 1px;\n" + " border-radius: 4px;\n" + " padding: 0px;\n" + "}\n" + "#ftb-handle {\n" + "}\n" + + , -1, NULL); +#endif + + gtk_style_context_add_provider_for_screen(gdk_screen_get_default(), + GTK_STYLE_PROVIDER(provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + g_object_unref(provider); + + /* Define a signal used to notify all rcws of toolbar move */ + rcw_signals[TOOLBARPLACE_SIGNAL] = g_signal_new("toolbar-place", G_TYPE_FROM_CLASS(klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET(RemminaConnectionWindowClass, toolbar_place), NULL, NULL, + g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); +} + +static RemminaConnectionObject *rcw_get_cnnobj_at_page(RemminaConnectionWindow *cnnwin, gint npage) +{ + GtkWidget *po; + + if (!cnnwin->priv->notebook) + return NULL; + po = gtk_notebook_get_nth_page(GTK_NOTEBOOK(cnnwin->priv->notebook), npage); + return g_object_get_data(G_OBJECT(po), "cnnobj"); +} + +static RemminaConnectionObject *rcw_get_visible_cnnobj(RemminaConnectionWindow *cnnwin) +{ + gint np; + + if (cnnwin != NULL && cnnwin->priv != NULL && cnnwin->priv->notebook != NULL) { + np = gtk_notebook_get_current_page(GTK_NOTEBOOK(cnnwin->priv->notebook)); + if (np < 0) + return NULL; + return rcw_get_cnnobj_at_page(cnnwin, np); + } else { + return NULL; + } +} + +static RemminaScaleMode get_current_allowed_scale_mode(RemminaConnectionObject *cnnobj, gboolean *dynres_avail, gboolean *scale_avail) +{ + TRACE_CALL(__func__); + RemminaScaleMode scalemode; + gboolean plugin_has_dynres, plugin_can_scale; + + scalemode = remmina_protocol_widget_get_current_scale_mode(REMMINA_PROTOCOL_WIDGET(cnnobj->proto)); + + plugin_has_dynres = remmina_protocol_widget_query_feature_by_type(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), + REMMINA_PROTOCOL_FEATURE_TYPE_DYNRESUPDATE); + + plugin_can_scale = remmina_protocol_widget_query_feature_by_type(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), + REMMINA_PROTOCOL_FEATURE_TYPE_SCALE); + + /* Forbid scalemode REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES when not possible */ + if ((!plugin_has_dynres) && scalemode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES) + scalemode = REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE; + + /* Forbid scalemode REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED when not possible */ + if (!plugin_can_scale && scalemode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED) + scalemode = REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE; + + if (scale_avail) + *scale_avail = plugin_can_scale; + if (dynres_avail) + *dynres_avail = (plugin_has_dynres && cnnobj->dynres_unlocked); + + return scalemode; +} + +static void rco_disconnect_current_page(RemminaConnectionObject *cnnobj) +{ + TRACE_CALL(__func__); + + /* Disconnects the connection which is currently in view in the notebook */ + remmina_protocol_widget_close_connection(REMMINA_PROTOCOL_WIDGET(cnnobj->proto)); +} + +static void rcw_kp_ungrab(RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + GdkDisplay *display; + +#if GTK_CHECK_VERSION(3, 20, 0) + GdkSeat *seat; +#else + GdkDeviceManager *manager; + GdkDevice *keyboard = NULL; +#endif + + if (cnnwin->priv->grab_retry_eventsourceid) { + g_source_remove(cnnwin->priv->grab_retry_eventsourceid); + cnnwin->priv->grab_retry_eventsourceid = 0; + } + if (cnnwin->priv->delayed_grab_eventsourceid) { + g_source_remove(cnnwin->priv->delayed_grab_eventsourceid); + cnnwin->priv->delayed_grab_eventsourceid = 0; + } + + display = gtk_widget_get_display(GTK_WIDGET(cnnwin)); +#if GTK_CHECK_VERSION(3, 20, 0) + seat = gdk_display_get_default_seat(display); + // keyboard = gdk_seat_get_pointer(seat); +#else + manager = gdk_display_get_device_manager(display); + keyboard = gdk_device_manager_get_client_pointer(manager); +#endif + + if (!cnnwin->priv->kbcaptured && !cnnwin->priv->pointer_captured) + return; + +#if DEBUG_KB_GRABBING + printf("DEBUG_KB_GRABBING: --- ungrabbing\n"); +#endif + + + +#if GTK_CHECK_VERSION(3, 20, 0) + /* We can use gtk_seat_grab()/_ungrab() only after GTK 3.24 */ + gdk_seat_ungrab(seat); +#else + if (keyboard != NULL) { + if (gdk_device_get_source(keyboard) != GDK_SOURCE_KEYBOARD) + keyboard = gdk_device_get_associated_device(keyboard); + G_GNUC_BEGIN_IGNORE_DEPRECATIONS + gdk_device_ungrab(keyboard, GDK_CURRENT_TIME); + G_GNUC_END_IGNORE_DEPRECATIONS + } +#endif + cnnwin->priv->kbcaptured = FALSE; + cnnwin->priv->pointer_captured = FALSE; +} + +static gboolean rcw_keyboard_grab_retry(gpointer user_data) +{ + TRACE_CALL(__func__); + RemminaConnectionWindow *cnnwin = (RemminaConnectionWindow *)user_data; + +#if DEBUG_KB_GRABBING + printf("%s retry grab\n", __func__); +#endif + rcw_keyboard_grab(cnnwin); + cnnwin->priv->grab_retry_eventsourceid = 0; + return G_SOURCE_REMOVE; +} + +static void rcw_pointer_ungrab(RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); +#if GTK_CHECK_VERSION(3, 20, 0) + GdkSeat *seat; + GdkDisplay *display; + if (!cnnwin->priv->pointer_captured) + return; + + display = gtk_widget_get_display(GTK_WIDGET(cnnwin)); + seat = gdk_display_get_default_seat(display); + gdk_seat_ungrab(seat); +#endif +} + +static void rcw_pointer_grab(RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + /* This function in Wayland is useless and generates a spurious leave-notify event. + * Should we remove it ? https://gitlab.gnome.org/GNOME/mutter/-/issues/2450#note_1588081 */ +#if GTK_CHECK_VERSION(3, 20, 0) + GdkSeat *seat; + GdkDisplay *display; + GdkGrabStatus ggs; + + + if (cnnwin->priv->pointer_captured) { +#if DEBUG_KB_GRABBING + printf("DEBUG_KB_GRABBING: pointer_captured is true, it should not\n"); +#endif + return; + } + + display = gtk_widget_get_display(GTK_WIDGET(cnnwin)); + seat = gdk_display_get_default_seat(display); + ggs = gdk_seat_grab(seat, gtk_widget_get_window(GTK_WIDGET(cnnwin)), + GDK_SEAT_CAPABILITY_ALL_POINTING, TRUE, NULL, NULL, NULL, NULL); + if (ggs != GDK_GRAB_SUCCESS) { +#if DEBUG_KB_GRABBING + printf("DEBUG_KB_GRABBING: GRAB of POINTER failed. GdkGrabStatus: %d\n", (int)ggs); +#endif + } else { + cnnwin->priv->pointer_captured = TRUE; + } + +#endif +} + +static void rcw_keyboard_grab(RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + GdkDisplay *display; + +#if GTK_CHECK_VERSION(3, 20, 0) + GdkSeat *seat; +#else + GdkDeviceManager *manager; +#endif + GdkGrabStatus ggs; + GdkDevice *keyboard = NULL; + + if (cnnwin->priv->kbcaptured) { +#if DEBUG_KB_GRABBING + printf("DEBUG_KB_GRABBING: %s not grabbing because already grabbed.\n", __func__); +#endif + return; + } + + display = gtk_widget_get_display(GTK_WIDGET(cnnwin)); +#if GTK_CHECK_VERSION(3, 20, 0) + seat = gdk_display_get_default_seat(display); + keyboard = gdk_seat_get_pointer(seat); +#else + manager = gdk_display_get_device_manager(display); + keyboard = gdk_device_manager_get_client_pointer(manager); +#endif + + if (keyboard != NULL) { + if (gdk_device_get_source(keyboard) != GDK_SOURCE_KEYBOARD) + keyboard = gdk_device_get_associated_device(keyboard); + + +#if DEBUG_KB_GRABBING + printf("DEBUG_KB_GRABBING: profile asks for grabbing, let’s try.\n"); +#endif + /* Up to GTK version 3.20 we can grab the keyboard with gdk_device_grab(). + * in GTK 3.20 gdk_seat_grab() should be used instead of gdk_device_grab(). + * There is a bug in GTK up to 3.22: When gdk_device_grab() fails + * the widget is hidden: + * https://gitlab.gnome.org/GNOME/gtk/commit/726ad5a5ae7c4f167e8dd454cd7c250821c400ab + * The bugfix will be released with GTK 3.24. + * Also please note that the newer gdk_seat_grab() is still calling gdk_device_grab(). + * + * Warning: gdk_seat_grab() will call XGrabKeyboard() or XIGrabDevice() + * which in turn will generate a core X input event FocusOut and FocusIn + * but not Xinput2 events. + * In some cases, GTK is unable to neutralize FocusIn and FocusOut core + * events (ie: i3wm+Plasma with GDK_CORE_DEVICE_EVENTS=1 because detail=NotifyNonlinear + * instead of detail=NotifyAncestor/detail=NotifyInferior) + * Receiving a FocusOut event for Remmina at this time will cause an infinite loop. + * Therefore is important for GTK to use Xinput2 instead of core X events + * by unsetting GDK_CORE_DEVICE_EVENTS + */ +#if GTK_CHECK_VERSION(3, 20, 0) + ggs = gdk_seat_grab(seat, gtk_widget_get_window(GTK_WIDGET(cnnwin)), + GDK_SEAT_CAPABILITY_KEYBOARD, TRUE, NULL, NULL, NULL, NULL); +#else + ggs = gdk_device_grab(keyboard, gtk_widget_get_window(GTK_WIDGET(cnnwin)), GDK_OWNERSHIP_WINDOW, + TRUE, GDK_KEY_PRESS | GDK_KEY_RELEASE, NULL, GDK_CURRENT_TIME); +#endif + if (ggs != GDK_GRAB_SUCCESS) { +#if DEBUG_KB_GRABBING + printf("GRAB of keyboard failed.\n"); +#endif + /* Reschedule grabbing in half a second if not already done */ + if (cnnwin->priv->grab_retry_eventsourceid == 0) + cnnwin->priv->grab_retry_eventsourceid = g_timeout_add(500, (GSourceFunc)rcw_keyboard_grab_retry, cnnwin); + } else { +#if DEBUG_KB_GRABBING + printf("Keyboard grabbed\n"); +#endif + if (cnnwin->priv->grab_retry_eventsourceid != 0) { + g_source_remove(cnnwin->priv->grab_retry_eventsourceid); + cnnwin->priv->grab_retry_eventsourceid = 0; + } + cnnwin->priv->kbcaptured = TRUE; + } + } else { + rcw_kp_ungrab(cnnwin); + } +} + +static void rcw_close_all_connections(RemminaConnectionWindow *cnnwin) +{ + RemminaConnectionWindowPriv *priv = cnnwin->priv; + GtkNotebook *notebook = GTK_NOTEBOOK(priv->notebook); + GtkWidget *w; + RemminaConnectionObject *cnnobj; + gint i, n; + + if (GTK_IS_WIDGET(notebook)) { + n = gtk_notebook_get_n_pages(notebook); + for (i = n - 1; i >= 0; i--) { + w = gtk_notebook_get_nth_page(notebook, i); + cnnobj = (RemminaConnectionObject *)g_object_get_data(G_OBJECT(w), "cnnobj"); + /* Do close the connection on this tab */ + remmina_protocol_widget_close_connection(REMMINA_PROTOCOL_WIDGET(cnnobj->proto)); + } + } +} + +gboolean rcw_delete(RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv = cnnwin->priv; + GtkNotebook *notebook = GTK_NOTEBOOK(priv->notebook); + GtkWidget *dialog; + gint i, n, nopen; + + if (!REMMINA_IS_CONNECTION_WINDOW(cnnwin)) + return TRUE; + + if (cnnwin->priv->on_delete_confirm_mode != RCW_ONDELETE_NOCONFIRM) { + n = gtk_notebook_get_n_pages(notebook); + nopen = 0; + /* count all non-closed connections */ + for(i = 0; i < n; i ++) { + RemminaConnectionObject *cnnobj = rcw_get_cnnobj_at_page(cnnwin, i); + if (!remmina_protocol_widget_is_closed((RemminaProtocolWidget *)cnnobj->proto)) + nopen ++; + } + if (nopen > 1) { + dialog = gtk_message_dialog_new(GTK_WINDOW(cnnwin), GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, + GTK_BUTTONS_YES_NO, + _("Are you sure you want to close %i active connections in the current window?"), nopen); + i = gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + if (i != GTK_RESPONSE_YES) + return FALSE; + } + else if (nopen == 1) { + if (remmina_pref.confirm_close) { + dialog = gtk_message_dialog_new(GTK_WINDOW(cnnwin), GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, + GTK_BUTTONS_YES_NO, + _("Are you sure you want to close this last active connection?")); + i = gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + if (i != GTK_RESPONSE_YES) + return FALSE; + } + } + } + rcw_close_all_connections(cnnwin); + + return TRUE; +} + +static gboolean rcw_delete_event(GtkWidget *widget, GdkEvent *event, gpointer data) +{ + TRACE_CALL(__func__); + rcw_delete(RCW(widget)); + return TRUE; +} + +static void rcw_destroy(GtkWidget *widget, gpointer data) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv; + RemminaConnectionWindow *cnnwin; + + if (!REMMINA_IS_CONNECTION_WINDOW(widget)) + return; + + cnnwin = (RemminaConnectionWindow *)widget; + priv = cnnwin->priv; + + if (priv->kbcaptured) + rcw_kp_ungrab(cnnwin); + + if (priv->acs_eventsourceid) { + g_source_remove(priv->acs_eventsourceid); + priv->acs_eventsourceid = 0; + } + if (priv->spf_eventsourceid) { + g_source_remove(priv->spf_eventsourceid); + priv->spf_eventsourceid = 0; + } + if (priv->grab_retry_eventsourceid) { + g_source_remove(priv->grab_retry_eventsourceid); + priv->grab_retry_eventsourceid = 0; + } + if (cnnwin->priv->delayed_grab_eventsourceid) { + g_source_remove(cnnwin->priv->delayed_grab_eventsourceid); + cnnwin->priv->delayed_grab_eventsourceid = 0; + } + if (priv->ftb_hide_eventsource) { + g_source_remove(priv->ftb_hide_eventsource); + priv->ftb_hide_eventsource = 0; + } + if (priv->tar_eventsource) { + g_source_remove(priv->tar_eventsource); + priv->tar_eventsource = 0; + } + if (priv->hidetb_eventsource) { + g_source_remove(priv->hidetb_eventsource); + priv->hidetb_eventsource = 0; + } + if (priv->dwp_eventsourceid) { + g_source_remove(priv->dwp_eventsourceid); + priv->dwp_eventsourceid = 0; + } + + /* There is no need to destroy priv->floating_toolbar_widget, + * because it’s our child and will be destroyed automatically */ + + cnnwin->priv = NULL; + g_free(priv); +} + +gboolean rcw_notify_widget_toolbar_placement(GtkWidget *widget, gpointer data) +{ + TRACE_CALL(__func__); + GType rcwtype; + + rcwtype = rcw_get_type(); + if (G_TYPE_CHECK_INSTANCE_TYPE(widget, rcwtype)) { + g_signal_emit_by_name(G_OBJECT(widget), "toolbar-place"); + return TRUE; + } + return FALSE; +} + +static gboolean rcw_tb_drag_failed(GtkWidget *widget, GdkDragContext *context, + GtkDragResult result, gpointer user_data) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv; + RemminaConnectionWindow *cnnwin; + + + cnnwin = (RemminaConnectionWindow *)user_data; + priv = cnnwin->priv; + + if (priv->toolbar) + gtk_widget_show(GTK_WIDGET(priv->toolbar)); + + return TRUE; +} + +static gboolean rcw_tb_drag_drop(GtkWidget *widget, GdkDragContext *context, + gint x, gint y, guint time, gpointer user_data) +{ + TRACE_CALL(__func__); + GtkAllocation wa; + gint new_toolbar_placement; + RemminaConnectionWindowPriv *priv; + RemminaConnectionWindow *cnnwin; + + cnnwin = (RemminaConnectionWindow *)user_data; + priv = cnnwin->priv; + + gtk_widget_get_allocation(widget, &wa); + + if (wa.width * y >= wa.height * x) { + if (wa.width * y > wa.height * (wa.width - x)) + new_toolbar_placement = TOOLBAR_PLACEMENT_BOTTOM; + else + new_toolbar_placement = TOOLBAR_PLACEMENT_LEFT; + } else { + if (wa.width * y > wa.height * (wa.width - x)) + new_toolbar_placement = TOOLBAR_PLACEMENT_RIGHT; + else + new_toolbar_placement = TOOLBAR_PLACEMENT_TOP; + } + + gtk_drag_finish(context, TRUE, TRUE, time); + + if (new_toolbar_placement != remmina_pref.toolbar_placement) { + /* Save new position */ + remmina_pref.toolbar_placement = new_toolbar_placement; + remmina_pref_save(); + + /* Signal all windows that the toolbar must be moved */ + remmina_widget_pool_foreach(rcw_notify_widget_toolbar_placement, NULL); + } + if (priv->toolbar) + gtk_widget_show(GTK_WIDGET(priv->toolbar)); + + return TRUE; +} + +static void rcw_tb_drag_begin(GtkWidget *widget, GdkDragContext *context, gpointer user_data) +{ + TRACE_CALL(__func__); + + cairo_surface_t *surface; + cairo_t *cr; + GtkAllocation wa; + double dashes[] = { 10 }; + + gtk_widget_get_allocation(widget, &wa); + + surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 16, 16); + cr = cairo_create(surface); + cairo_set_source_rgb(cr, 0.6, 0.6, 0.6); + cairo_set_line_width(cr, 4); + cairo_set_dash(cr, dashes, 1, 0); + cairo_rectangle(cr, 0, 0, 16, 16); + cairo_stroke(cr); + cairo_destroy(cr); + + gtk_widget_hide(widget); + + gtk_drag_set_icon_surface(context, surface); +} + +void rcw_update_toolbar_opacity(RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv = cnnwin->priv; + RemminaConnectionObject *cnnobj; + + cnnobj = rcw_get_visible_cnnobj(cnnwin); + if (!cnnobj) return; + + priv->floating_toolbar_opacity = (1.0 - TOOLBAR_OPACITY_MIN) / ((gdouble)TOOLBAR_OPACITY_LEVEL) + * ((gdouble)(TOOLBAR_OPACITY_LEVEL - remmina_file_get_int(cnnobj->remmina_file, "toolbar_opacity", 0))) + + TOOLBAR_OPACITY_MIN; + if (priv->floating_toolbar_widget) + gtk_widget_set_opacity(GTK_WIDGET(priv->overlay_ftb_overlay), priv->floating_toolbar_opacity); +} + +static gboolean rcw_floating_toolbar_make_invisible(gpointer data) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv = (RemminaConnectionWindowPriv *)data; + + gtk_widget_set_opacity(GTK_WIDGET(priv->overlay_ftb_overlay), 0.0); + priv->ftb_hide_eventsource = 0; + return G_SOURCE_REMOVE; +} + +static void rcw_floating_toolbar_show(RemminaConnectionWindow *cnnwin, gboolean show) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv = cnnwin->priv; + + if (priv->floating_toolbar_widget == NULL) + return; + + if (show || priv->pin_down) { + /* Make the FTB no longer transparent, in case we have an hidden toolbar */ + rcw_update_toolbar_opacity(cnnwin); + /* Remove outstanding hide events, if not yet active */ + if (priv->ftb_hide_eventsource) { + g_source_remove(priv->ftb_hide_eventsource); + priv->ftb_hide_eventsource = 0; + } + } else { + /* If we are hiding and the toolbar must be made invisible, schedule + * a later toolbar hide */ + if (remmina_pref.fullscreen_toolbar_visibility == FLOATING_TOOLBAR_VISIBILITY_INVISIBLE) + if (priv->ftb_hide_eventsource == 0) + priv->ftb_hide_eventsource = g_timeout_add(1000, rcw_floating_toolbar_make_invisible, priv); + } + + gtk_revealer_set_reveal_child(GTK_REVEALER(priv->revealer), show || priv->pin_down); +} + +static void rco_get_desktop_size(RemminaConnectionObject *cnnobj, gint *width, gint *height) +{ + TRACE_CALL(__func__); + RemminaProtocolWidget *gp = REMMINA_PROTOCOL_WIDGET(cnnobj->proto); + + + *width = remmina_protocol_widget_get_width(gp); + *height = remmina_protocol_widget_get_height(gp); + if (*width == 0) { + /* Before connecting we do not have real remote width/height, + * so we ask profile values */ + *width = remmina_protocol_widget_get_profile_remote_width(gp); + *height = remmina_protocol_widget_get_profile_remote_height(gp); + } +} + +void rco_set_scrolled_policy(RemminaScaleMode scalemode, GtkScrolledWindow *scrolled_window) +{ + TRACE_CALL(__func__); + + gtk_scrolled_window_set_policy(scrolled_window, + scalemode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED ? GTK_POLICY_NEVER : GTK_POLICY_AUTOMATIC, + scalemode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED ? GTK_POLICY_NEVER : GTK_POLICY_AUTOMATIC); +} + +static GtkWidget *rco_create_scrolled_container(RemminaScaleMode scalemode, int view_mode) +{ + GtkWidget *scrolled_container; + + if (view_mode == VIEWPORT_FULLSCREEN_MODE) { + scrolled_container = remmina_scrolled_viewport_new(); + } else { + scrolled_container = gtk_scrolled_window_new(NULL, NULL); + rco_set_scrolled_policy(scalemode, GTK_SCROLLED_WINDOW(scrolled_container)); + gtk_container_set_border_width(GTK_CONTAINER(scrolled_container), 0); + gtk_widget_set_can_focus(scrolled_container, FALSE); + } + + gtk_widget_set_name(scrolled_container, "remmina-scrolled-container"); + gtk_widget_show(scrolled_container); + + return scrolled_container; +} + +gboolean rcw_toolbar_autofit_restore(RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + + RemminaConnectionWindowPriv *priv = cnnwin->priv; + RemminaConnectionObject *cnnobj; + gint dwidth, dheight; + GtkAllocation nba, ca, ta; + + cnnwin->priv->tar_eventsource = 0; + + if (priv->toolbar_is_reconfiguring) + return G_SOURCE_REMOVE; + + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return FALSE; + + if (cnnobj->connected && GTK_IS_SCROLLED_WINDOW(cnnobj->scrolled_container)) { + rco_get_desktop_size(cnnobj, &dwidth, &dheight); + gtk_widget_get_allocation(GTK_WIDGET(priv->notebook), &nba); + gtk_widget_get_allocation(cnnobj->scrolled_container, &ca); + gtk_widget_get_allocation(priv->toolbar, &ta); + if (remmina_pref.toolbar_placement == TOOLBAR_PLACEMENT_LEFT || + remmina_pref.toolbar_placement == TOOLBAR_PLACEMENT_RIGHT) + gtk_window_resize(GTK_WINDOW(cnnobj->cnnwin), MAX(1, dwidth + ta.width + nba.width - ca.width), + MAX(1, dheight + nba.height - ca.height)); + else + gtk_window_resize(GTK_WINDOW(cnnobj->cnnwin), MAX(1, dwidth + nba.width - ca.width), + MAX(1, dheight + ta.height + nba.height - ca.height)); + gtk_container_check_resize(GTK_CONTAINER(cnnobj->cnnwin)); + } + if (GTK_IS_SCROLLED_WINDOW(cnnobj->scrolled_container)) { + RemminaScaleMode scalemode = get_current_allowed_scale_mode(cnnobj, NULL, NULL); + rco_set_scrolled_policy(scalemode, GTK_SCROLLED_WINDOW(cnnobj->scrolled_container)); + } + + return G_SOURCE_REMOVE; +} + +static void rcw_toolbar_autofit(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionObject *cnnobj; + + if (cnnwin->priv->toolbar_is_reconfiguring) + return; + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return; + + if (GTK_IS_SCROLLED_WINDOW(cnnobj->scrolled_container)) { + if ((gdk_window_get_state(gtk_widget_get_window(GTK_WIDGET(cnnwin))) & GDK_WINDOW_STATE_MAXIMIZED) != 0) + gtk_window_unmaximize(GTK_WINDOW(cnnwin)); + + /* It’s tricky to make the toolbars disappear automatically, while keeping scrollable. + * Please tell me if you know a better way to do this */ + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(cnnobj->scrolled_container), GTK_POLICY_NEVER, GTK_POLICY_NEVER); + + cnnwin->priv->tar_eventsource = g_timeout_add(200, (GSourceFunc)rcw_toolbar_autofit_restore, cnnwin); + } +} + +void rco_get_monitor_geometry(RemminaConnectionObject *cnnobj, GdkRectangle *sz) +{ + TRACE_CALL(__func__); + + /* Fill sz with the monitor (or workarea) size and position + * of the monitor (or workarea) where cnnobj->cnnwin is located */ + + GdkRectangle monitor_geometry; + + sz->x = sz->y = sz->width = sz->height = 0; + + if (!cnnobj) + return; + if (!cnnobj->cnnwin) + return; + if (!gtk_widget_is_visible(GTK_WIDGET(cnnobj->cnnwin))) + return; + +#if GTK_CHECK_VERSION(3, 22, 0) + GdkDisplay *display; + GdkMonitor *monitor; + display = gtk_widget_get_display(GTK_WIDGET(cnnobj->cnnwin)); + monitor = gdk_display_get_monitor_at_window(display, gtk_widget_get_window(GTK_WIDGET(cnnobj->cnnwin))); +#else + GdkScreen *screen; + gint monitor; + screen = gtk_window_get_screen(GTK_WINDOW(cnnobj->cnnwin)); + monitor = gdk_screen_get_monitor_at_window(screen, gtk_widget_get_window(GTK_WIDGET(cnnobj->cnnwin))); +#endif + +#if GTK_CHECK_VERSION(3, 22, 0) + gdk_monitor_get_workarea(monitor, &monitor_geometry); + /* Under Wayland, GTK 3.22, all values returned by gdk_monitor_get_geometry() + * and gdk_monitor_get_workarea() seem to have been divided by the + * gdk scale factor, so we need to adjust the returned rect + * undoing the division */ +#ifdef GDK_WINDOWING_WAYLAND + if (GDK_IS_WAYLAND_DISPLAY(display)) { + int monitor_scale_factor = gdk_monitor_get_scale_factor(monitor); + monitor_geometry.width *= monitor_scale_factor; + monitor_geometry.height *= monitor_scale_factor; + } +#endif +#elif gdk_screen_get_monitor_workarea + gdk_screen_get_monitor_workarea(screen, monitor, &monitor_geometry); +#else + gdk_screen_get_monitor_geometry(screen, monitor, &monitor_geometry); +#endif + *sz = monitor_geometry; +} + +static void rco_check_resize(RemminaConnectionObject *cnnobj) +{ + TRACE_CALL(__func__); + gboolean scroll_required = FALSE; + + GdkRectangle monitor_geometry; + gint rd_width, rd_height; + gint bordersz; + gint scalemode; + + scalemode = remmina_protocol_widget_get_current_scale_mode(REMMINA_PROTOCOL_WIDGET(cnnobj->proto)); + + /* Get remote destkop size */ + rco_get_desktop_size(cnnobj, &rd_width, &rd_height); + + /* Get our monitor size */ + rco_get_monitor_geometry(cnnobj, &monitor_geometry); + + if (!remmina_protocol_widget_get_expand(REMMINA_PROTOCOL_WIDGET(cnnobj->proto)) && + (monitor_geometry.width < rd_width || monitor_geometry.height < rd_height) && + scalemode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE) + scroll_required = TRUE; + + switch (cnnobj->cnnwin->priv->view_mode) { + case SCROLLED_FULLSCREEN_MODE: + gtk_window_resize(GTK_WINDOW(cnnobj->cnnwin), monitor_geometry.width, monitor_geometry.height); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(cnnobj->scrolled_container), + (scroll_required ? GTK_POLICY_AUTOMATIC : GTK_POLICY_NEVER), + (scroll_required ? GTK_POLICY_AUTOMATIC : GTK_POLICY_NEVER)); + break; + + case VIEWPORT_FULLSCREEN_MODE: + bordersz = scroll_required ? SCROLL_BORDER_SIZE : 0; + gtk_window_resize(GTK_WINDOW(cnnobj->cnnwin), monitor_geometry.width, monitor_geometry.height); + if (REMMINA_IS_SCROLLED_VIEWPORT(cnnobj->scrolled_container)) + /* Put a border around Notebook content (RemminaScrolledViewpord), so we can + * move the mouse over the border to scroll */ + gtk_container_set_border_width(GTK_CONTAINER(cnnobj->scrolled_container), bordersz); + + break; + + case SCROLLED_WINDOW_MODE: + if (remmina_file_get_int(cnnobj->remmina_file, "viewmode", UNDEFINED_MODE) == UNDEFINED_MODE) { + /* ToDo: is this really needed ? When ? */ + gtk_window_set_default_size(GTK_WINDOW(cnnobj->cnnwin), + MIN(rd_width, monitor_geometry.width), MIN(rd_height, monitor_geometry.height)); + if (rd_width >= monitor_geometry.width || rd_height >= monitor_geometry.height) { + gtk_window_maximize(GTK_WINDOW(cnnobj->cnnwin)); + remmina_file_set_int(cnnobj->remmina_file, "window_maximize", TRUE); + } else { + rcw_toolbar_autofit(NULL, cnnobj->cnnwin); + remmina_file_set_int(cnnobj->remmina_file, "window_maximize", FALSE); + } + } else { + if (remmina_file_get_int(cnnobj->remmina_file, "window_maximize", FALSE)) + gtk_window_maximize(GTK_WINDOW(cnnobj->cnnwin)); + } + break; + + default: + break; + } +} + +static void rcw_set_tooltip(GtkWidget *item, const gchar *tip, guint key1, guint key2) +{ + TRACE_CALL(__func__); + gchar *s1; + gchar *s2; + + if (remmina_pref.hostkey && key1) { + if (key2) + s1 = g_strdup_printf(" (%s + %s,%s)", gdk_keyval_name(remmina_pref.hostkey), + gdk_keyval_name(gdk_keyval_to_upper(key1)), gdk_keyval_name(gdk_keyval_to_upper(key2))); + else if (key1 == remmina_pref.hostkey) + s1 = g_strdup_printf(" (%s)", gdk_keyval_name(remmina_pref.hostkey)); + else + s1 = g_strdup_printf(" (%s + %s)", gdk_keyval_name(remmina_pref.hostkey), + gdk_keyval_name(gdk_keyval_to_upper(key1))); + } else { + s1 = NULL; + } + s2 = g_strdup_printf("%s%s", tip, s1 ? s1 : ""); + gtk_widget_set_tooltip_text(item, s2); + g_free(s2); + g_free(s1); +} + +static void remmina_protocol_widget_update_alignment(RemminaConnectionObject *cnnobj) +{ + TRACE_CALL(__func__); + RemminaScaleMode scalemode; + gboolean scaledexpandedmode; + int rdwidth, rdheight; + gfloat aratio; + + if (!cnnobj->plugin_can_scale) { + /* If we have a plugin that cannot scale, + * (i.e. SFTP plugin), then we expand proto */ + gtk_widget_set_halign(GTK_WIDGET(cnnobj->proto), GTK_ALIGN_FILL); + gtk_widget_set_valign(GTK_WIDGET(cnnobj->proto), GTK_ALIGN_FILL); + } else { + /* Plugin can scale */ + + scalemode = get_current_allowed_scale_mode(cnnobj, NULL, NULL); + scaledexpandedmode = remmina_protocol_widget_get_expand(REMMINA_PROTOCOL_WIDGET(cnnobj->proto)); + + /* Check if we need aspectframe and create/destroy it accordingly */ + if (scalemode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED && !scaledexpandedmode) { + /* We need an aspectframe as a parent of proto */ + rdwidth = remmina_protocol_widget_get_width(REMMINA_PROTOCOL_WIDGET(cnnobj->proto)); + rdheight = remmina_protocol_widget_get_height(REMMINA_PROTOCOL_WIDGET(cnnobj->proto)); + aratio = (gfloat)rdwidth / (gfloat)rdheight; + if (!cnnobj->aspectframe) { + /* We need a new aspectframe */ + cnnobj->aspectframe = gtk_aspect_frame_new(NULL, 0.5, 0.5, aratio, FALSE); + gtk_widget_set_name(cnnobj->aspectframe, "remmina-cw-aspectframe"); + gtk_frame_set_shadow_type(GTK_FRAME(cnnobj->aspectframe), GTK_SHADOW_NONE); + g_object_ref(cnnobj->proto); + gtk_container_remove(GTK_CONTAINER(cnnobj->viewport), cnnobj->proto); + gtk_container_add(GTK_CONTAINER(cnnobj->viewport), cnnobj->aspectframe); + gtk_container_add(GTK_CONTAINER(cnnobj->aspectframe), cnnobj->proto); + g_object_unref(cnnobj->proto); + gtk_widget_show(cnnobj->aspectframe); + if (cnnobj != NULL && cnnobj->cnnwin != NULL && cnnobj->cnnwin->priv->notebook != NULL) + rcw_grab_focus(cnnobj->cnnwin); + } else { + gtk_aspect_frame_set(GTK_ASPECT_FRAME(cnnobj->aspectframe), 0.5, 0.5, aratio, FALSE); + } + } else { + /* We do not need an aspectframe as a parent of proto */ + if (cnnobj->aspectframe) { + /* We must remove the old aspectframe reparenting proto to viewport */ + g_object_ref(cnnobj->aspectframe); + g_object_ref(cnnobj->proto); + gtk_container_remove(GTK_CONTAINER(cnnobj->aspectframe), cnnobj->proto); + gtk_container_remove(GTK_CONTAINER(cnnobj->viewport), cnnobj->aspectframe); + g_object_unref(cnnobj->aspectframe); + cnnobj->aspectframe = NULL; + gtk_container_add(GTK_CONTAINER(cnnobj->viewport), cnnobj->proto); + g_object_unref(cnnobj->proto); + if (cnnobj != NULL && cnnobj->cnnwin != NULL && cnnobj->cnnwin->priv->notebook != NULL) + rcw_grab_focus(cnnobj->cnnwin); + } + } + + if (scalemode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED || scalemode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES) { + /* We have a plugin that can be scaled, and the scale button + * has been pressed. Give it the correct WxH maintaining aspect + * ratio of remote destkop size */ + gtk_widget_set_halign(GTK_WIDGET(cnnobj->proto), GTK_ALIGN_FILL); + gtk_widget_set_valign(GTK_WIDGET(cnnobj->proto), GTK_ALIGN_FILL); + } else { + /* Plugin can scale, but no scaling is active. Ensure that we have + * aspectframe with a ratio of 1 */ + gtk_widget_set_halign(GTK_WIDGET(cnnobj->proto), GTK_ALIGN_CENTER); + gtk_widget_set_valign(GTK_WIDGET(cnnobj->proto), GTK_ALIGN_CENTER); + } + } +} + +static void nb_set_current_page(GtkNotebook *notebook, GtkWidget *page) +{ + gint np, i; + + np = gtk_notebook_get_n_pages(notebook); + for (i = 0; i < np; i++) { + if (gtk_notebook_get_nth_page(notebook, i) == page) { + gtk_notebook_set_current_page(notebook, i); + break; + } + } +} + +static void nb_migrate_message_panels(GtkWidget *frompage, GtkWidget *topage) +{ + /* Migrate a single connection tab from a notebook to another one */ + GList *lst, *l; + + /* Reparent message panels */ + lst = gtk_container_get_children(GTK_CONTAINER(frompage)); + for (l = lst; l != NULL; l = l->next) { + if (REMMINA_IS_MESSAGE_PANEL(l->data)) { + g_object_ref(l->data); + gtk_container_remove(GTK_CONTAINER(frompage), GTK_WIDGET(l->data)); + gtk_container_add(GTK_CONTAINER(topage), GTK_WIDGET(l->data)); + g_object_unref(l->data); + gtk_box_reorder_child(GTK_BOX(topage), GTK_WIDGET(l->data), 0); + } + } + g_list_free(lst); + +} + +static void rcw_migrate(RemminaConnectionWindow *from, RemminaConnectionWindow *to) +{ + /* Migrate a complete notebook from a window to another */ + + gchar *tag; + gint cp, np, i; + GtkNotebook *from_notebook; + GtkWidget *frompage, *newpage, *old_scrolled_container; + RemminaConnectionObject *cnnobj; + RemminaScaleMode scalemode; + + /* Migrate TAG */ + tag = g_strdup((gchar *)g_object_get_data(G_OBJECT(from), "tag")); + g_object_set_data_full(G_OBJECT(to), "tag", tag, (GDestroyNotify)g_free); + + /* Migrate notebook content */ + from_notebook = from->priv->notebook; + if (from_notebook && GTK_IS_NOTEBOOK(from_notebook)) { + + cp = gtk_notebook_get_current_page(from_notebook); + np = gtk_notebook_get_n_pages(from_notebook); + /* Create pages on dest notebook and migrate + * page content */ + for (i = 0; i < np; i++) { + frompage = gtk_notebook_get_nth_page(from_notebook, i); + cnnobj = g_object_get_data(G_OBJECT(frompage), "cnnobj"); + + /* A scrolled container must be recreated, because it can be different on the new window/page + depending on view_mode */ + scalemode = get_current_allowed_scale_mode(cnnobj, NULL, NULL); + old_scrolled_container = cnnobj->scrolled_container; + cnnobj->scrolled_container = rco_create_scrolled_container(scalemode, to->priv->view_mode); + + newpage = rcw_append_new_page(to, cnnobj); + + nb_migrate_message_panels(frompage, newpage); + + /* Reparent the viewport (which is inside scrolled_container) to the new page */ + g_object_ref(cnnobj->viewport); + gtk_container_remove(GTK_CONTAINER(old_scrolled_container), cnnobj->viewport); + gtk_container_add(GTK_CONTAINER(cnnobj->scrolled_container), cnnobj->viewport); + g_object_unref(cnnobj->viewport); + + /* Destroy old scrolled_container. Not really needed, it will be destroyed + * when removing the page from the notepad */ + gtk_widget_destroy(old_scrolled_container); + + } + + /* Remove all the pages from source notebook */ + for (i = np - 1; i >= 0; i--) + gtk_notebook_remove_page(from_notebook, i); + gtk_notebook_set_current_page(to->priv->notebook, cp); + + } +} + +static void rcw_switch_viewmode(RemminaConnectionWindow *cnnwin, int newmode) +{ + GdkWindowState s; + RemminaConnectionWindow *newwin; + gint old_width, old_height; + int old_mode; + + old_mode = cnnwin->priv->view_mode; + if (old_mode == newmode) + return; + + if (newmode == VIEWPORT_FULLSCREEN_MODE || newmode == SCROLLED_FULLSCREEN_MODE) { + if (old_mode == SCROLLED_WINDOW_MODE) { + /* We are leaving SCROLLED_WINDOW_MODE, save W,H, and maximized + * status before self destruction of cnnwin */ + gtk_window_get_size(GTK_WINDOW(cnnwin), &old_width, &old_height); + s = gdk_window_get_state(gtk_widget_get_window(GTK_WIDGET(cnnwin))); + } + newwin = rcw_create_fullscreen(GTK_WINDOW(cnnwin), cnnwin->priv->fss_view_mode); + rcw_migrate(cnnwin, newwin); + if (old_mode == SCROLLED_WINDOW_MODE) { + newwin->priv->ss_maximized = (s & GDK_WINDOW_STATE_MAXIMIZED) ? TRUE : FALSE; + newwin->priv->ss_width = old_width; + newwin->priv->ss_height = old_height; + } + } else { + newwin = rcw_create_scrolled(cnnwin->priv->ss_width, cnnwin->priv->ss_height, + cnnwin->priv->ss_maximized); + rcw_migrate(cnnwin, newwin); + if (old_mode == VIEWPORT_FULLSCREEN_MODE || old_mode == SCROLLED_FULLSCREEN_MODE) + /* We are leaving a FULLSCREEN mode, save some parameters + * status before self destruction of cnnwin */ + newwin->priv->fss_view_mode = old_mode; + } + + /* Prevent unreleased hostkey from old window to be released here */ + newwin->priv->hostkey_used = TRUE; +} + + +static void rcw_toolbar_fullscreen(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + + RemminaConnectionObject *cnnobj; + + if (cnnwin->priv->toolbar_is_reconfiguring) + return; + + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return; + + RemminaProtocolWidget *gp = REMMINA_PROTOCOL_WIDGET(cnnobj->proto); + + if (remmina_protocol_widget_get_multimon(gp) >= 1) { + REMMINA_DEBUG("Fullscreen on all monitor"); + gdk_window_set_fullscreen_mode(gtk_widget_get_window(GTK_WIDGET(toggle)), GDK_FULLSCREEN_ON_ALL_MONITORS); + } else { + REMMINA_DEBUG("Fullscreen on one monitor"); + } + + if ((toggle != NULL && toggle == cnnwin->priv->toolitem_fullscreen)) { + if (gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(toggle))) { + if (remmina_protocol_widget_get_multimon(gp) >= 1) + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(cnnwin->priv->toolitem_multimon), TRUE); + rcw_switch_viewmode(cnnwin, cnnwin->priv->fss_view_mode); + } else { + rcw_switch_viewmode(cnnwin, SCROLLED_WINDOW_MODE); + } + } else + if (gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(cnnwin->priv->toolitem_multimon))) { + rcw_switch_viewmode(cnnwin, cnnwin->priv->fss_view_mode); + } else { + rcw_switch_viewmode(cnnwin, SCROLLED_WINDOW_MODE); + } +} + +static void rco_viewport_fullscreen_mode(GtkWidget *widget, RemminaConnectionObject *cnnobj) +{ + TRACE_CALL(__func__); + RemminaConnectionWindow *newwin; + + if (!gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget))) + return; + cnnobj->cnnwin->priv->fss_view_mode = VIEWPORT_FULLSCREEN_MODE; + newwin = rcw_create_fullscreen(GTK_WINDOW(cnnobj->cnnwin), VIEWPORT_FULLSCREEN_MODE); + rcw_migrate(cnnobj->cnnwin, newwin); +} + +static void rco_scrolled_fullscreen_mode(GtkWidget *widget, RemminaConnectionObject *cnnobj) +{ + TRACE_CALL(__func__); + RemminaConnectionWindow *newwin; + + if (!gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget))) + return; + cnnobj->cnnwin->priv->fss_view_mode = SCROLLED_FULLSCREEN_MODE; + newwin = rcw_create_fullscreen(GTK_WINDOW(cnnobj->cnnwin), SCROLLED_FULLSCREEN_MODE); + rcw_migrate(cnnobj->cnnwin, newwin); +} + +static void rcw_fullscreen_option_popdown(GtkWidget *widget, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv = cnnwin->priv; + + priv->sticky = FALSE; + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->fullscreen_option_button), FALSE); + rcw_floating_toolbar_show(cnnwin, FALSE); +} + +void rcw_toolbar_fullscreen_option(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionObject *cnnobj; + GtkWidget *menu; + GtkWidget *menuitem; + GSList *group; + + if (cnnwin->priv->toolbar_is_reconfiguring) + return; + + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return; + + if (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle))) + return; + + cnnwin->priv->sticky = TRUE; + + menu = gtk_menu_new(); + + menuitem = gtk_radio_menu_item_new_with_label(NULL, _("Viewport fullscreen mode")); + gtk_widget_show(menuitem); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem); + group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(menuitem)); + if (cnnwin->priv->view_mode == VIEWPORT_FULLSCREEN_MODE) + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem), TRUE); + g_signal_connect(G_OBJECT(menuitem), "toggled", G_CALLBACK(rco_viewport_fullscreen_mode), cnnobj); + + menuitem = gtk_radio_menu_item_new_with_label(group, _("Scrolled fullscreen")); + gtk_widget_show(menuitem); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem); + if (cnnwin->priv->view_mode == SCROLLED_FULLSCREEN_MODE) + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem), TRUE); + g_signal_connect(G_OBJECT(menuitem), "toggled", G_CALLBACK(rco_scrolled_fullscreen_mode), cnnobj); + + g_signal_connect(G_OBJECT(menu), "deactivate", G_CALLBACK(rcw_fullscreen_option_popdown), cnnwin); + +#if GTK_CHECK_VERSION(3, 22, 0) + gtk_menu_popup_at_widget(GTK_MENU(menu), GTK_WIDGET(toggle), + GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, NULL); +#else + gtk_menu_popup(GTK_MENU(menu), NULL, NULL, remmina_public_popup_position, cnnwin->priv->toolitem_fullscreen, 0, + gtk_get_current_event_time()); +#endif +} + + +static void rcw_scaler_option_popdown(GtkWidget *widget, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv = cnnwin->priv; + + if (priv->toolbar_is_reconfiguring) + return; + priv->sticky = FALSE; + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->scaler_option_button), FALSE); + rcw_floating_toolbar_show(cnnwin, FALSE); +} + +static void rcw_scaler_expand(GtkWidget *widget, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionObject *cnnobj; + + if (!gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget))) + return; + cnnobj = rcw_get_visible_cnnobj(cnnwin); + if (!cnnobj) + return; + remmina_protocol_widget_set_expand(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), TRUE); + remmina_file_set_int(cnnobj->remmina_file, "scaler_expand", TRUE); + remmina_protocol_widget_update_alignment(cnnobj); +} +static void rcw_scaler_keep_aspect(GtkWidget *widget, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionObject *cnnobj; + + if (!gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget))) + return; + cnnobj = rcw_get_visible_cnnobj(cnnwin); + if (!cnnobj) + return; + + remmina_protocol_widget_set_expand(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), FALSE); + remmina_file_set_int(cnnobj->remmina_file, "scaler_expand", FALSE); + remmina_protocol_widget_update_alignment(cnnobj); +} + +static void rcw_toolbar_scaler_option(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv; + RemminaConnectionObject *cnnobj; + GtkWidget *menu; + GtkWidget *menuitem; + GSList *group; + gboolean scaler_expand; + + if (cnnwin->priv->toolbar_is_reconfiguring) + return; + + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return; + priv = cnnwin->priv; + + if (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle))) + return; + + scaler_expand = remmina_protocol_widget_get_expand(REMMINA_PROTOCOL_WIDGET(cnnobj->proto)); + + priv->sticky = TRUE; + + menu = gtk_menu_new(); + + menuitem = gtk_radio_menu_item_new_with_label(NULL, _("Keep aspect ratio when scaled")); + gtk_widget_show(menuitem); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem); + group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(menuitem)); + if (!scaler_expand) + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem), TRUE); + g_signal_connect(G_OBJECT(menuitem), "toggled", G_CALLBACK(rcw_scaler_keep_aspect), cnnwin); + + menuitem = gtk_radio_menu_item_new_with_label(group, _("Fill client window when scaled")); + gtk_widget_show(menuitem); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem); + if (scaler_expand) + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem), TRUE); + g_signal_connect(G_OBJECT(menuitem), "toggled", G_CALLBACK(rcw_scaler_expand), cnnwin); + + g_signal_connect(G_OBJECT(menu), "deactivate", G_CALLBACK(rcw_scaler_option_popdown), cnnwin); + +#if GTK_CHECK_VERSION(3, 22, 0) + gtk_menu_popup_at_widget(GTK_MENU(menu), GTK_WIDGET(toggle), + GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, NULL); +#else + gtk_menu_popup(GTK_MENU(menu), NULL, NULL, remmina_public_popup_position, priv->toolitem_scale, 0, + gtk_get_current_event_time()); +#endif +} + +void rco_switch_page_activate(GtkMenuItem *menuitem, RemminaConnectionObject *cnnobj) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv = cnnobj->cnnwin->priv; + gint page_num; + + page_num = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menuitem), "new-page-num")); + gtk_notebook_set_current_page(GTK_NOTEBOOK(priv->notebook), page_num); +} + +void rcw_toolbar_switch_page_popdown(GtkWidget *widget, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv = cnnwin->priv; + + priv->sticky = FALSE; + + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_switch_page), FALSE); + rcw_floating_toolbar_show(cnnwin, FALSE); +} + +static void rcw_toolbar_switch_page(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + + RemminaConnectionWindowPriv *priv = cnnwin->priv; + RemminaConnectionObject *cnnobj; + + GtkWidget *menu; + GtkWidget *menuitem; + GtkWidget *image; + gint i, n; + + if (priv->toolbar_is_reconfiguring) + return; + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return; + + if (!gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(toggle))) + return; + + priv->sticky = TRUE; + + menu = gtk_menu_new(); + + n = gtk_notebook_get_n_pages(GTK_NOTEBOOK(priv->notebook)); + for (i = 0; i < n; i++) { + cnnobj = rcw_get_cnnobj_at_page(cnnobj->cnnwin, i); + + menuitem = gtk_menu_item_new_with_label(remmina_file_get_string(cnnobj->remmina_file, "name")); + gtk_widget_show(menuitem); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem); + + image = gtk_image_new_from_icon_name(remmina_file_get_icon_name(cnnobj->remmina_file), GTK_ICON_SIZE_MENU); + gtk_widget_show(image); + + g_object_set_data(G_OBJECT(menuitem), "new-page-num", GINT_TO_POINTER(i)); + g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(rco_switch_page_activate), cnnobj); + if (i == gtk_notebook_get_current_page(GTK_NOTEBOOK(priv->notebook))) + gtk_widget_set_sensitive(menuitem, FALSE); + } + + g_signal_connect(G_OBJECT(menu), "deactivate", G_CALLBACK(rcw_toolbar_switch_page_popdown), + cnnwin); + +#if GTK_CHECK_VERSION(3, 22, 0) + gtk_menu_popup_at_widget(GTK_MENU(menu), GTK_WIDGET(toggle), + GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, NULL); +#else + gtk_menu_popup(GTK_MENU(menu), NULL, NULL, remmina_public_popup_position, widget, 0, gtk_get_current_event_time()); +#endif +} + +void rco_update_toolbar_autofit_button(RemminaConnectionObject *cnnobj) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv = cnnobj->cnnwin->priv; + GtkToolItem *toolitem; + RemminaScaleMode sc; + + toolitem = priv->toolitem_autofit; + if (toolitem) { + if (priv->view_mode != SCROLLED_WINDOW_MODE) { + gtk_widget_set_sensitive(GTK_WIDGET(toolitem), FALSE); + } else { + sc = get_current_allowed_scale_mode(cnnobj, NULL, NULL); + gtk_widget_set_sensitive(GTK_WIDGET(toolitem), sc == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE); + } + } +} + +static void rco_change_scalemode(RemminaConnectionObject *cnnobj, gboolean bdyn, gboolean bscale) +{ + RemminaScaleMode scalemode; + RemminaConnectionWindowPriv *priv = cnnobj->cnnwin->priv; + + if (bdyn) + scalemode = REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES; + else if (bscale) + scalemode = REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED; + else + scalemode = REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE; + + remmina_protocol_widget_set_current_scale_mode(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), scalemode); + remmina_file_set_int(cnnobj->remmina_file, "scale", scalemode); + gtk_widget_set_sensitive(GTK_WIDGET(priv->scaler_option_button), scalemode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED); + rco_update_toolbar_autofit_button(cnnobj); + + remmina_protocol_widget_call_feature_by_type(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), + REMMINA_PROTOCOL_FEATURE_TYPE_SCALE, 0); + + if (cnnobj->cnnwin->priv->view_mode != SCROLLED_WINDOW_MODE) + rco_check_resize(cnnobj); + if (GTK_IS_SCROLLED_WINDOW(cnnobj->scrolled_container)) { + rco_set_scrolled_policy(scalemode, GTK_SCROLLED_WINDOW(cnnobj->scrolled_container)); + } +} + +static void rcw_toolbar_dynres(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + gboolean bdyn, bscale; + RemminaConnectionObject *cnnobj; + + if (cnnwin->priv->toolbar_is_reconfiguring) + return; + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return; + + if (cnnobj->connected) { + bdyn = gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(toggle)); + bscale = gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(cnnobj->cnnwin->priv->toolitem_scale)); + + if (bdyn && bscale) { + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(cnnobj->cnnwin->priv->toolitem_scale), FALSE); + bscale = FALSE; + } + + rco_change_scalemode(cnnobj, bdyn, bscale); + } +} + +static void rcw_toolbar_scaled_mode(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + gboolean bdyn, bscale; + RemminaConnectionObject *cnnobj; + + if (cnnwin->priv->toolbar_is_reconfiguring) + return; + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return; + + bdyn = gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(cnnobj->cnnwin->priv->toolitem_dynres)); + bscale = gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(toggle)); + + if (bdyn && bscale) { + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(cnnobj->cnnwin->priv->toolitem_dynres), FALSE); + bdyn = FALSE; + } + + rco_change_scalemode(cnnobj, bdyn, bscale); +} + +static void rcw_toolbar_multi_monitor_mode(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionObject *cnnobj; + + if (cnnwin->priv->toolbar_is_reconfiguring) + return; + + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return; + + if (gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(toggle))) { + REMMINA_DEBUG("Saving multimon as 1"); + remmina_file_set_int(cnnobj->remmina_file, "multimon", 1); + remmina_file_save(cnnobj->remmina_file); + remmina_protocol_widget_call_feature_by_type(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), + REMMINA_PROTOCOL_FEATURE_TYPE_MULTIMON, 0); + if (!gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(cnnwin->priv->toolitem_fullscreen))) + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(cnnwin->priv->toolitem_fullscreen), TRUE); + } else { + REMMINA_DEBUG("Saving multimon as 0"); + remmina_file_set_int(cnnobj->remmina_file, "multimon", 0); + remmina_file_save(cnnobj->remmina_file); + rcw_toolbar_fullscreen(NULL, cnnwin); + } +} + +static void rcw_toolbar_open_main(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + + if (cnnwin->priv->toolbar_is_reconfiguring) + return; + + remmina_exec_command(REMMINA_COMMAND_MAIN, NULL); +} + +static void rcw_toolbar_preferences_popdown(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionObject *cnnobj; + + if (cnnwin->priv->toolbar_is_reconfiguring) + return; + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return; + + cnnobj->cnnwin->priv->sticky = FALSE; + + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(cnnobj->cnnwin->priv->toolitem_preferences), FALSE); + rcw_floating_toolbar_show(cnnwin, FALSE); +} + +void rcw_toolbar_menu_popdown(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv = cnnwin->priv; + + if (priv->toolbar_is_reconfiguring) + return; + + priv->sticky = FALSE; + + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_menu), FALSE); + rcw_floating_toolbar_show(cnnwin, FALSE); +} + +void rcw_toolbar_tools_popdown(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv = cnnwin->priv; + + if (priv->toolbar_is_reconfiguring) + return; + + priv->sticky = FALSE; + + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_tools), FALSE); + rcw_floating_toolbar_show(cnnwin, FALSE); +} + +static void rco_call_protocol_feature_radio(GtkMenuItem *menuitem, RemminaConnectionObject *cnnobj) +{ + TRACE_CALL(__func__); + RemminaProtocolFeature *feature; + gpointer value; + + if (gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menuitem))) { + feature = (RemminaProtocolFeature *)g_object_get_data(G_OBJECT(menuitem), "feature-type"); + value = g_object_get_data(G_OBJECT(menuitem), "feature-value"); + + remmina_file_set_string(cnnobj->remmina_file, (const gchar *)feature->opt2, (const gchar *)value); + remmina_protocol_widget_call_feature_by_ref(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), feature); + } +} + +static void rco_call_protocol_feature_check(GtkMenuItem *menuitem, RemminaConnectionObject *cnnobj) +{ + TRACE_CALL(__func__); + RemminaProtocolFeature *feature; + gboolean value; + + feature = (RemminaProtocolFeature *)g_object_get_data(G_OBJECT(menuitem), "feature-type"); + value = gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menuitem)); + remmina_file_set_int(cnnobj->remmina_file, (const gchar *)feature->opt2, value); + remmina_protocol_widget_call_feature_by_ref(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), feature); +} + +static void rco_call_protocol_feature_activate(GtkMenuItem *menuitem, RemminaConnectionObject *cnnobj) +{ + TRACE_CALL(__func__); + RemminaProtocolFeature *feature; + + feature = (RemminaProtocolFeature *)g_object_get_data(G_OBJECT(menuitem), "feature-type"); + remmina_protocol_widget_call_feature_by_ref(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), feature); +} + +void rcw_toolbar_preferences_radio(RemminaConnectionObject *cnnobj, RemminaFile *remminafile, + GtkWidget *menu, const RemminaProtocolFeature *feature, const gchar *domain, gboolean enabled) +{ + TRACE_CALL(__func__); + GtkWidget *menuitem; + GSList *group; + gint i; + const gchar **list; + const gchar *value; + + group = NULL; + value = remmina_file_get_string(remminafile, (const gchar *)feature->opt2); + list = (const gchar **)feature->opt3; + for (i = 0; list[i]; i += 2) { + menuitem = gtk_radio_menu_item_new_with_label(group, g_dgettext(domain, list[i + 1])); + group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(menuitem)); + gtk_widget_show(menuitem); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem); + + if (enabled) { + g_object_set_data(G_OBJECT(menuitem), "feature-type", (gpointer)feature); + g_object_set_data(G_OBJECT(menuitem), "feature-value", (gpointer)list[i]); + + if (value && g_strcmp0(list[i], value) == 0) + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem), TRUE); + + g_signal_connect(G_OBJECT(menuitem), "toggled", + G_CALLBACK(rco_call_protocol_feature_radio), cnnobj); + } else { + gtk_widget_set_sensitive(menuitem, FALSE); + } + } +} + +void rcw_toolbar_preferences_check(RemminaConnectionObject *cnnobj, + GtkWidget *menu, const RemminaProtocolFeature *feature, + const gchar *domain, gboolean enabled) +{ + TRACE_CALL(__func__); + GtkWidget *menuitem; + + menuitem = gtk_check_menu_item_new_with_label(g_dgettext(domain, (const gchar *)feature->opt3)); + gtk_widget_show(menuitem); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem); + + if (enabled) { + g_object_set_data(G_OBJECT(menuitem), "feature-type", (gpointer)feature); + + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem), + remmina_file_get_int(cnnobj->remmina_file, (const gchar *)feature->opt2, FALSE)); + + g_signal_connect(G_OBJECT(menuitem), "toggled", + G_CALLBACK(rco_call_protocol_feature_check), cnnobj); + } else { + gtk_widget_set_sensitive(menuitem, FALSE); + } +} + +static void rcw_toolbar_preferences(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv; + RemminaConnectionObject *cnnobj; + const RemminaProtocolFeature *feature; + GtkWidget *menu; + GtkWidget *menuitem; + gboolean separator; + gchar *domain; + gboolean enabled; + + if (cnnwin->priv->toolbar_is_reconfiguring) + return; + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return; + priv = cnnobj->cnnwin->priv; + + if (!gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(toggle))) + return; + + priv->sticky = TRUE; + + separator = FALSE; + + domain = remmina_protocol_widget_get_domain(REMMINA_PROTOCOL_WIDGET(cnnobj->proto)); + menu = gtk_menu_new(); + for (feature = remmina_protocol_widget_get_features(REMMINA_PROTOCOL_WIDGET(cnnobj->proto)); feature && feature->type; + feature++) { + if (feature->type != REMMINA_PROTOCOL_FEATURE_TYPE_PREF) + continue; + + if (separator) { + menuitem = gtk_separator_menu_item_new(); + gtk_widget_show(menuitem); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem); + separator = FALSE; + } + enabled = remmina_protocol_widget_query_feature_by_ref(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), feature); + switch (GPOINTER_TO_INT(feature->opt1)) { + case REMMINA_PROTOCOL_FEATURE_PREF_RADIO: + rcw_toolbar_preferences_radio(cnnobj, cnnobj->remmina_file, menu, feature, + domain, enabled); + separator = TRUE; + break; + case REMMINA_PROTOCOL_FEATURE_PREF_CHECK: + rcw_toolbar_preferences_check(cnnobj, menu, feature, + domain, enabled); + break; + } + } + + g_free(domain); + + g_signal_connect(G_OBJECT(menu), "deactivate", G_CALLBACK(rcw_toolbar_preferences_popdown), cnnwin); + +#if GTK_CHECK_VERSION(3, 22, 0) + gtk_menu_popup_at_widget(GTK_MENU(menu), GTK_WIDGET(toggle), + GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, NULL); +#else + gtk_menu_popup(GTK_MENU(menu), NULL, NULL, remmina_public_popup_position, widget, 0, gtk_get_current_event_time()); +#endif +} + +static void rcw_toolbar_menu_on_launch_item(RemminaAppletMenu *menu, RemminaAppletMenuItem *menuitem, gpointer data) +{ + TRACE_CALL(__func__); + gchar *s; + + switch (menuitem->item_type) { + case REMMINA_APPLET_MENU_ITEM_NEW: + remmina_exec_command(REMMINA_COMMAND_NEW, NULL); + break; + case REMMINA_APPLET_MENU_ITEM_FILE: + remmina_exec_command(REMMINA_COMMAND_CONNECT, menuitem->filename); + break; + case REMMINA_APPLET_MENU_ITEM_DISCOVERED: + s = g_strdup_printf("%s,%s", menuitem->protocol, menuitem->name); + remmina_exec_command(REMMINA_COMMAND_NEW, s); + g_free(s); + break; + } +} + +static void rcw_toolbar_menu(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv; + RemminaConnectionObject *cnnobj; + GtkWidget *menu; + GtkWidget *menuitem = NULL; + + if (cnnwin->priv->toolbar_is_reconfiguring) + return; + + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return; + priv = cnnobj->cnnwin->priv; + + if (!gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(toggle))) + return; + + priv->sticky = TRUE; + + menu = remmina_applet_menu_new(); + remmina_applet_menu_set_hide_count(REMMINA_APPLET_MENU(menu), remmina_pref.applet_hide_count); + remmina_applet_menu_populate(REMMINA_APPLET_MENU(menu)); + + g_signal_connect(G_OBJECT(menu), "launch-item", G_CALLBACK(rcw_toolbar_menu_on_launch_item), NULL); + //g_signal_connect(G_OBJECT(menu), "edit-item", G_CALLBACK(rcw_toolbar_menu_on_edit_item), NULL); + menuitem = gtk_separator_menu_item_new(); + gtk_widget_show(menuitem); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem); +#if GTK_CHECK_VERSION(3, 22, 0) + gtk_menu_popup_at_widget(GTK_MENU(menu), GTK_WIDGET(toggle), + GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, NULL); +#else + gtk_menu_popup(GTK_MENU(menu), NULL, NULL, remmina_public_popup_position, widget, 0, gtk_get_current_event_time()); +#endif + g_signal_connect(G_OBJECT(menu), "deactivate", G_CALLBACK(rcw_toolbar_menu_popdown), cnnwin); +} + +static void rcw_toolbar_tools(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv; + RemminaConnectionObject *cnnobj; + const RemminaProtocolFeature *feature; + GtkWidget *menu; + GtkWidget *menuitem = NULL; + GtkMenu *submenu_keystrokes; + const gchar *domain; + gboolean enabled; + gchar **keystrokes; + gchar **keystroke_values; + gint i; + + if (cnnwin->priv->toolbar_is_reconfiguring) + return; + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return; + priv = cnnobj->cnnwin->priv; + + if (!gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(toggle))) + return; + + priv->sticky = TRUE; + + domain = remmina_protocol_widget_get_domain(REMMINA_PROTOCOL_WIDGET(cnnobj->proto)); + menu = gtk_menu_new(); + for (feature = remmina_protocol_widget_get_features(REMMINA_PROTOCOL_WIDGET(cnnobj->proto)); feature && feature->type; + feature++) { + if (feature->type != REMMINA_PROTOCOL_FEATURE_TYPE_TOOL) + continue; + + if (feature->opt1) + menuitem = gtk_menu_item_new_with_label(g_dgettext(domain, (const gchar *)feature->opt1)); + if (feature->opt3) + rcw_set_tooltip(menuitem, "", GPOINTER_TO_UINT(feature->opt3), 0); + gtk_widget_show(menuitem); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem); + + enabled = remmina_protocol_widget_query_feature_by_ref(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), feature); + if (enabled) { + g_object_set_data(G_OBJECT(menuitem), "feature-type", (gpointer)feature); + + g_signal_connect(G_OBJECT(menuitem), "activate", + G_CALLBACK(rco_call_protocol_feature_activate), cnnobj); + } else { + gtk_widget_set_sensitive(menuitem, FALSE); + } + } + + /* If the plugin accepts keystrokes include the keystrokes menu */ + if (remmina_protocol_widget_plugin_receives_keystrokes(REMMINA_PROTOCOL_WIDGET(cnnobj->proto))) { + /* Get the registered keystrokes list */ + keystrokes = g_strsplit(remmina_pref.keystrokes, STRING_DELIMITOR, -1); + if (g_strv_length(keystrokes)) { + /* Add a keystrokes submenu */ + menuitem = gtk_menu_item_new_with_label(_("Keystrokes")); + submenu_keystrokes = GTK_MENU(gtk_menu_new()); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), GTK_WIDGET(submenu_keystrokes)); + gtk_widget_show(menuitem); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem); + /* Add each registered keystroke */ + for (i = 0; i < g_strv_length(keystrokes); i++) { + keystroke_values = g_strsplit(keystrokes[i], STRING_DELIMITOR2, -1); + if (g_strv_length(keystroke_values) > 1) { + /* Add the keystroke if no description was available */ + menuitem = gtk_menu_item_new_with_label( + g_strdup(keystroke_values[strlen(keystroke_values[0]) ? 0 : 1])); + g_object_set_data(G_OBJECT(menuitem), "keystrokes", g_strdup(keystroke_values[1])); + g_signal_connect_swapped(G_OBJECT(menuitem), "activate", + G_CALLBACK(remmina_protocol_widget_send_keystrokes), + REMMINA_PROTOCOL_WIDGET(cnnobj->proto)); + gtk_widget_show(menuitem); + gtk_menu_shell_append(GTK_MENU_SHELL(submenu_keystrokes), menuitem); + } + g_strfreev(keystroke_values); + } + menuitem = gtk_menu_item_new_with_label(_("Send clipboard content as keystrokes")); + static gchar k_tooltip[] = + N_("CAUTION: Pasted text will be sent as a sequence of key-codes as if typed on your local keyboard.\n" + "\n" + " • For best results use same keyboard settings for both, client and server.\n" + "\n" + " • If client-keyboard is different from server-keyboard the received text can contain wrong or erroneous characters.\n" + "\n" + " • Unicode characters and other special characters that can't be translated to local key-codes won’t be sent to the server.\n" + "\n"); + gtk_widget_set_tooltip_text(menuitem, k_tooltip); + gtk_menu_shell_append(GTK_MENU_SHELL(submenu_keystrokes), menuitem); + g_signal_connect_swapped(G_OBJECT(menuitem), "activate", + G_CALLBACK(remmina_protocol_widget_send_clipboard), + REMMINA_PROTOCOL_WIDGET(cnnobj->proto)); + gtk_widget_show(menuitem); + } + g_strfreev(keystrokes); + } + + g_signal_connect(G_OBJECT(menu), "deactivate", G_CALLBACK(rcw_toolbar_tools_popdown), cnnwin); + +#if GTK_CHECK_VERSION(3, 22, 0) + gtk_menu_popup_at_widget(GTK_MENU(menu), GTK_WIDGET(toggle), + GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, NULL); +#else + gtk_menu_popup(GTK_MENU(menu), NULL, NULL, remmina_public_popup_position, widget, 0, gtk_get_current_event_time()); +#endif +} + +static void rcw_toolbar_duplicate(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + + RemminaConnectionObject *cnnobj; + + if (cnnwin->priv->toolbar_is_reconfiguring) + return; + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return; + + remmina_file_save(cnnobj->remmina_file); + + remmina_exec_command(REMMINA_COMMAND_CONNECT, cnnobj->remmina_file->filename); +} + +static void rcw_toolbar_screenshot(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + + GdkPixbuf *screenshot; + GdkWindow *active_window; + cairo_t *cr; + gint width, height; + GString *pngstr; + gchar *pngname; + GtkWidget *dialog; + RemminaProtocolWidget *gp; + RemminaPluginScreenshotData rpsd; + RemminaConnectionObject *cnnobj; + cairo_surface_t *srcsurface; + cairo_format_t cairo_format; + cairo_surface_t *surface; + int stride; + + if (cnnwin->priv->toolbar_is_reconfiguring) + return; + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return; + + GDateTime *date = g_date_time_new_now_utc(); + + // We will take a screenshot of the currently displayed RemminaProtocolWidget. + gp = REMMINA_PROTOCOL_WIDGET(cnnobj->proto); + + gchar *denyclip = remmina_pref_get_value("deny_screenshot_clipboard"); + + REMMINA_DEBUG("deny_screenshot_clipboard is set to %s", denyclip); + + GtkClipboard *c = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); + + // Ask the plugin if it can give us a screenshot + if (remmina_protocol_widget_plugin_screenshot(gp, &rpsd)) { + // Good, we have a screenshot from the plugin ! + + REMMINA_DEBUG("Screenshot from plugin: w=%d h=%d bpp=%d bytespp=%d\n", + rpsd.width, rpsd.height, rpsd.bitsPerPixel, rpsd.bytesPerPixel); + + width = rpsd.width; + height = rpsd.height; + + if (rpsd.bitsPerPixel == 32) + cairo_format = CAIRO_FORMAT_ARGB32; + else if (rpsd.bitsPerPixel == 24) + cairo_format = CAIRO_FORMAT_RGB24; + else + cairo_format = CAIRO_FORMAT_RGB16_565; + + stride = cairo_format_stride_for_width(cairo_format, width); + + srcsurface = cairo_image_surface_create_for_data(rpsd.buffer, cairo_format, width, height, stride); + // Transfer the PixBuf in the main clipboard selection + if (denyclip && (g_strcmp0(denyclip, "true"))) + gtk_clipboard_set_image(c, gdk_pixbuf_get_from_surface( + srcsurface, 0, 0, width, height)); + surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, width, height); + cr = cairo_create(surface); + cairo_set_source_surface(cr, srcsurface, 0, 0); + cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); + cairo_paint(cr); + cairo_surface_destroy(srcsurface); + + free(rpsd.buffer); + } else { + // The plugin is not releasing us a screenshot, just try to catch one via GTK + + /* Warn the user if image is distorted */ + if (cnnobj->plugin_can_scale && + get_current_allowed_scale_mode(cnnobj, NULL, NULL) == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED) { + dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, + _("Turn off scaling to avoid screenshot distortion.")); + g_signal_connect(G_OBJECT(dialog), "response", G_CALLBACK(gtk_widget_destroy), NULL); + gtk_widget_show(dialog); + } + + // Get the screenshot. + active_window = gtk_widget_get_window(GTK_WIDGET(gp)); + // width = gdk_window_get_width(gtk_widget_get_window(GTK_WIDGET(cnnobj->cnnwin))); + width = gdk_window_get_width(active_window); + // height = gdk_window_get_height(gtk_widget_get_window(GTK_WIDGET(cnnobj->cnnwin))); + height = gdk_window_get_height(active_window); + + screenshot = gdk_pixbuf_get_from_window(active_window, 0, 0, width, height); + if (screenshot == NULL) + g_print("gdk_pixbuf_get_from_window failed\n"); + + // Transfer the PixBuf in the main clipboard selection + if (denyclip && (g_strcmp0(denyclip, "true"))) + gtk_clipboard_set_image(c, screenshot); + // Prepare the destination Cairo surface. + surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, width, height); + cr = cairo_create(surface); + + // Copy the source pixbuf to the surface and paint it. + gdk_cairo_set_source_pixbuf(cr, screenshot, 0, 0); + cairo_paint(cr); + + // Deallocate screenshot pixbuf + g_object_unref(screenshot); + } + + //home/antenore/Pictures/remmina_%p_%h_%Y %m %d-%H%M%S.png pngname + //home/antenore/Pictures/remmina_st_ _2018 9 24-151958.240374.png + + pngstr = g_string_new(g_strdup_printf("%s/%s.png", + remmina_pref.screenshot_path, + remmina_pref.screenshot_name)); + remmina_utils_string_replace_all(pngstr, "%p", + remmina_file_get_string(cnnobj->remmina_file, "name")); + remmina_utils_string_replace_all(pngstr, "%h", + remmina_file_get_string(cnnobj->remmina_file, "server")); + remmina_utils_string_replace_all(pngstr, "%Y", + g_strdup_printf("%d", g_date_time_get_year(date))); + remmina_utils_string_replace_all(pngstr, "%m", g_strdup_printf("%02d", + g_date_time_get_month(date))); + remmina_utils_string_replace_all(pngstr, "%d", + g_strdup_printf("%02d", g_date_time_get_day_of_month(date))); + remmina_utils_string_replace_all(pngstr, "%H", + g_strdup_printf("%02d", g_date_time_get_hour(date))); + remmina_utils_string_replace_all(pngstr, "%M", + g_strdup_printf("%02d", g_date_time_get_minute(date))); + remmina_utils_string_replace_all(pngstr, "%S", + g_strdup_printf("%02d", g_date_time_get_second(date))); + g_date_time_unref(date); + pngname = g_string_free(pngstr, FALSE); + + cairo_surface_write_to_png(surface, pngname); + + /* send a desktop notification */ + if (g_file_test(pngname, G_FILE_TEST_EXISTS)) + remmina_public_send_notification("remmina-screenshot-is-ready-id", _("Screenshot taken"), pngname); + + //Clean up and return. + cairo_destroy(cr); + cairo_surface_destroy(surface); +} + +static void rcw_toolbar_minimize(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + + if (cnnwin->priv->toolbar_is_reconfiguring) + return; + + rcw_floating_toolbar_show(cnnwin, FALSE); + gtk_window_iconify(GTK_WINDOW(cnnwin)); +} + +static void rcw_toolbar_disconnect(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionObject *cnnobj; + + if (cnnwin->priv->toolbar_is_reconfiguring) + return; + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return; + rco_disconnect_current_page(cnnobj); +} + +static void rcw_toolbar_grab(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + gboolean capture; + RemminaConnectionObject *cnnobj; + + if (cnnwin->priv->toolbar_is_reconfiguring) + return; + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return; + + capture = gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(toggle)); + + if (cnnobj->connected){ + remmina_file_set_int(cnnobj->remmina_file, "keyboard_grab", capture); + } + + if (capture && cnnobj->connected) { + +#if DEBUG_KB_GRABBING + printf("DEBUG_KB_GRABBING: Grabbing for button\n"); +#endif + rcw_keyboard_grab(cnnobj->cnnwin); + if (cnnobj->cnnwin->priv->pointer_entered) + rcw_pointer_grab(cnnobj->cnnwin); + } else { + rcw_kp_ungrab(cnnobj->cnnwin); + } + + rco_update_toolbar(cnnobj); +} + +static GtkWidget * +rcw_create_toolbar(RemminaConnectionWindow *cnnwin, gint mode) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv = cnnwin->priv; + RemminaConnectionObject *cnnobj; + GtkWidget *toolbar; + GtkToolItem *toolitem; + GtkWidget *widget; + GtkWidget *arrow; + + GdkDisplay *display; + gint n_monitors; + + display = gdk_display_get_default(); + n_monitors = gdk_display_get_n_monitors(display); + + cnnobj = rcw_get_visible_cnnobj(cnnwin); + + priv->toolbar_is_reconfiguring = TRUE; + + toolbar = gtk_toolbar_new(); + gtk_widget_show(toolbar); + gtk_toolbar_set_style(GTK_TOOLBAR(toolbar), GTK_TOOLBAR_ICONS); + + /* Main actions */ + + /* Menu */ + toolitem = gtk_toggle_tool_button_new(); + gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "view-more-symbolic"); + gtk_tool_button_set_label(GTK_TOOL_BUTTON(toolitem), _("_Menu")); + gtk_tool_item_set_tooltip_text(toolitem, _("Menu")); + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + gtk_widget_show(GTK_WIDGET(toolitem)); + g_signal_connect(G_OBJECT(toolitem), "toggled", G_CALLBACK(rcw_toolbar_menu), cnnwin); + priv->toolitem_menu = toolitem; + + /* Open Main window */ + toolitem = gtk_tool_button_new(NULL, "Open Remmina Main window"); + gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "go-home-symbolic"); + gtk_tool_item_set_tooltip_text(toolitem, _("Open the Remmina main window")); + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + gtk_widget_show(GTK_WIDGET(toolitem)); + g_signal_connect(G_OBJECT(toolitem), "clicked", G_CALLBACK(rcw_toolbar_open_main), cnnwin); + + priv->toolitem_new = toolitem; + + /* Duplicate session */ + toolitem = gtk_tool_button_new(NULL, "Duplicate connection"); + gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-duplicate-symbolic"); + gtk_tool_item_set_tooltip_text(toolitem, _("Duplicate current connection")); + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + gtk_widget_show(GTK_WIDGET(toolitem)); + g_signal_connect(G_OBJECT(toolitem), "clicked", G_CALLBACK(rcw_toolbar_duplicate), cnnwin); + if (!cnnobj) + gtk_widget_set_sensitive(GTK_WIDGET(toolitem), FALSE); + + priv->toolitem_duplicate = toolitem; + + /* Separator */ + toolitem = gtk_separator_tool_item_new(); + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + gtk_widget_show(GTK_WIDGET(toolitem)); + + /* Auto-Fit */ + toolitem = gtk_tool_button_new(NULL, NULL); + gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-fit-window-symbolic"); + rcw_set_tooltip(GTK_WIDGET(toolitem), _("Resize the window to fit in remote resolution"), + remmina_pref.shortcutkey_autofit, 0); + g_signal_connect(G_OBJECT(toolitem), "clicked", G_CALLBACK(rcw_toolbar_autofit), cnnwin); + priv->toolitem_autofit = toolitem; + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + gtk_widget_show(GTK_WIDGET(toolitem)); + + + /* Fullscreen toggle */ + toolitem = gtk_toggle_tool_button_new(); + gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-fullscreen-symbolic"); + rcw_set_tooltip(GTK_WIDGET(toolitem), _("Toggle fullscreen mode"), + remmina_pref.shortcutkey_fullscreen, 0); + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + gtk_widget_show(GTK_WIDGET(toolitem)); + priv->toolitem_fullscreen = toolitem; + if (kioskmode) { + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(toolitem), FALSE); + } else { + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(toolitem), mode != SCROLLED_WINDOW_MODE); + g_signal_connect(G_OBJECT(toolitem), "clicked", G_CALLBACK(rcw_toolbar_fullscreen), cnnwin); + } + + /* Fullscreen drop-down options */ + toolitem = gtk_tool_item_new(); + gtk_widget_show(GTK_WIDGET(toolitem)); + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + widget = gtk_toggle_button_new(); + gtk_widget_show(widget); + gtk_container_set_border_width(GTK_CONTAINER(widget), 0); + gtk_button_set_relief(GTK_BUTTON(widget), GTK_RELIEF_NONE); +#if GTK_CHECK_VERSION(3, 20, 0) + gtk_widget_set_focus_on_click(GTK_WIDGET(widget), FALSE); + if (remmina_pref.small_toolbutton) + gtk_widget_set_name(widget, "remmina-small-button"); + +#else + gtk_button_set_focus_on_click(GTK_BUTTON(widget), FALSE); +#endif + gtk_container_add(GTK_CONTAINER(toolitem), widget); + +#if GTK_CHECK_VERSION(3, 14, 0) + arrow = gtk_image_new_from_icon_name("org.remmina.Remmina-pan-down-symbolic", GTK_ICON_SIZE_SMALL_TOOLBAR); +#else + arrow = gtk_arrow_new(GTK_ARROW_DOWN, GTK_SHADOW_NONE); +#endif + gtk_widget_show(arrow); + gtk_container_add(GTK_CONTAINER(widget), arrow); + g_signal_connect(G_OBJECT(widget), "toggled", G_CALLBACK(rcw_toolbar_fullscreen_option), cnnwin); + priv->fullscreen_option_button = widget; + if (mode == SCROLLED_WINDOW_MODE) + gtk_widget_set_sensitive(GTK_WIDGET(widget), FALSE); + + /* Multi monitor */ + if (n_monitors > 1) { + toolitem = gtk_toggle_tool_button_new(); + gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-multi-monitor-symbolic"); + rcw_set_tooltip(GTK_WIDGET(toolitem), _("Multi monitor"), + remmina_pref.shortcutkey_multimon, 0); + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + gtk_widget_show(GTK_WIDGET(toolitem)); + g_signal_connect(G_OBJECT(toolitem), "toggled", G_CALLBACK(rcw_toolbar_multi_monitor_mode), cnnwin); + priv->toolitem_multimon = toolitem; + if (!cnnobj) + gtk_widget_set_sensitive(GTK_WIDGET(toolitem), FALSE); + else + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(toolitem), + remmina_file_get_int(cnnobj->remmina_file, "multimon", FALSE)); + } + + /* Dynamic Resolution Update */ + toolitem = gtk_toggle_tool_button_new(); + gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-dynres-symbolic"); + rcw_set_tooltip(GTK_WIDGET(toolitem), _("Toggle dynamic resolution update"), + remmina_pref.shortcutkey_dynres, 0); + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + gtk_widget_show(GTK_WIDGET(toolitem)); + g_signal_connect(G_OBJECT(toolitem), "toggled", G_CALLBACK(rcw_toolbar_dynres), cnnwin); + priv->toolitem_dynres = toolitem; + + /* Scaler button */ + toolitem = gtk_toggle_tool_button_new(); + gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-scale-symbolic"); + rcw_set_tooltip(GTK_WIDGET(toolitem), _("Toggle scaled mode"), remmina_pref.shortcutkey_scale, 0); + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + gtk_widget_show(GTK_WIDGET(toolitem)); + g_signal_connect(G_OBJECT(toolitem), "toggled", G_CALLBACK(rcw_toolbar_scaled_mode), cnnwin); + priv->toolitem_scale = toolitem; + + /* Scaler aspect ratio dropdown menu */ + toolitem = gtk_tool_item_new(); + gtk_widget_show(GTK_WIDGET(toolitem)); + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + widget = gtk_toggle_button_new(); + gtk_widget_show(widget); + gtk_container_set_border_width(GTK_CONTAINER(widget), 0); + gtk_button_set_relief(GTK_BUTTON(widget), GTK_RELIEF_NONE); +#if GTK_CHECK_VERSION(3, 20, 0) + gtk_widget_set_focus_on_click(GTK_WIDGET(widget), FALSE); +#else + gtk_button_set_focus_on_click(GTK_BUTTON(widget), FALSE); +#endif + if (remmina_pref.small_toolbutton) + gtk_widget_set_name(widget, "remmina-small-button"); + gtk_container_add(GTK_CONTAINER(toolitem), widget); +#if GTK_CHECK_VERSION(3, 14, 0) + arrow = gtk_image_new_from_icon_name("org.remmina.Remmina-pan-down-symbolic", GTK_ICON_SIZE_SMALL_TOOLBAR); +#else + arrow = gtk_arrow_new(GTK_ARROW_DOWN, GTK_SHADOW_NONE); +#endif + gtk_widget_show(arrow); + gtk_container_add(GTK_CONTAINER(widget), arrow); + g_signal_connect(G_OBJECT(widget), "toggled", G_CALLBACK(rcw_toolbar_scaler_option), cnnwin); + priv->scaler_option_button = widget; + + /* Separator */ + toolitem = gtk_separator_tool_item_new(); + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + gtk_widget_show(GTK_WIDGET(toolitem)); + + /* Switch tabs */ + toolitem = gtk_toggle_tool_button_new(); + gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-switch-page-symbolic"); + rcw_set_tooltip(GTK_WIDGET(toolitem), _("Switch tab pages"), remmina_pref.shortcutkey_prevtab, + remmina_pref.shortcutkey_nexttab); + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + gtk_widget_show(GTK_WIDGET(toolitem)); + g_signal_connect(G_OBJECT(toolitem), "toggled", G_CALLBACK(rcw_toolbar_switch_page), cnnwin); + priv->toolitem_switch_page = toolitem; + + /* Grab keyboard button */ + toolitem = gtk_toggle_tool_button_new(); + gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-keyboard-symbolic"); + rcw_set_tooltip(GTK_WIDGET(toolitem), _("Grab all keyboard events"), + remmina_pref.shortcutkey_grab, 0); + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + gtk_widget_show(GTK_WIDGET(toolitem)); + g_signal_connect(G_OBJECT(toolitem), "toggled", G_CALLBACK(rcw_toolbar_grab), cnnwin); + priv->toolitem_grab = toolitem; + if (!cnnobj) + gtk_widget_set_sensitive(GTK_WIDGET(toolitem), FALSE); + else { + const gchar *protocol = remmina_file_get_string(cnnobj->remmina_file, "protocol"); + if (g_strcmp0(protocol, "SFTP") == 0 || g_strcmp0(protocol, "SSH") == 0) + gtk_widget_set_sensitive(GTK_WIDGET(toolitem), FALSE); + } + + /* Preferences */ + toolitem = gtk_toggle_tool_button_new(); + gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-preferences-system-symbolic"); + gtk_tool_item_set_tooltip_text(toolitem, _("Preferences")); + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + gtk_widget_show(GTK_WIDGET(toolitem)); + g_signal_connect(G_OBJECT(toolitem), "toggled", G_CALLBACK(rcw_toolbar_preferences), cnnwin); + priv->toolitem_preferences = toolitem; + + /* Tools */ + toolitem = gtk_toggle_tool_button_new(); + gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-system-run-symbolic"); + gtk_tool_button_set_label(GTK_TOOL_BUTTON(toolitem), _("_Tools")); + gtk_tool_item_set_tooltip_text(toolitem, _("Tools")); + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + gtk_widget_show(GTK_WIDGET(toolitem)); + g_signal_connect(G_OBJECT(toolitem), "toggled", G_CALLBACK(rcw_toolbar_tools), cnnwin); + priv->toolitem_tools = toolitem; + + /* Separator */ + toolitem = gtk_separator_tool_item_new(); + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + gtk_widget_show(GTK_WIDGET(toolitem)); + + toolitem = gtk_tool_button_new(NULL, "_Screenshot"); + gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-camera-photo-symbolic"); + rcw_set_tooltip(GTK_WIDGET(toolitem), _("Screenshot"), remmina_pref.shortcutkey_screenshot, 0); + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + gtk_widget_show(GTK_WIDGET(toolitem)); + g_signal_connect(G_OBJECT(toolitem), "clicked", G_CALLBACK(rcw_toolbar_screenshot), cnnwin); + priv->toolitem_screenshot = toolitem; + + /* Separator */ + toolitem = gtk_separator_tool_item_new(); + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + gtk_widget_show(GTK_WIDGET(toolitem)); + + /* Minimize */ + toolitem = gtk_tool_button_new(NULL, "_Bottom"); + gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-go-bottom-symbolic"); + rcw_set_tooltip(GTK_WIDGET(toolitem), _("Minimize window"), remmina_pref.shortcutkey_minimize, 0); + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + gtk_widget_show(GTK_WIDGET(toolitem)); + g_signal_connect(G_OBJECT(toolitem), "clicked", G_CALLBACK(rcw_toolbar_minimize), cnnwin); + if (kioskmode) + gtk_widget_set_sensitive(GTK_WIDGET(toolitem), FALSE); + + /* Disconnect */ + toolitem = gtk_tool_button_new(NULL, "_Disconnect"); + gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-disconnect-symbolic"); + rcw_set_tooltip(GTK_WIDGET(toolitem), _("Disconnect"), remmina_pref.shortcutkey_disconnect, 0); + gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1); + gtk_widget_show(GTK_WIDGET(toolitem)); + g_signal_connect(G_OBJECT(toolitem), "clicked", G_CALLBACK(rcw_toolbar_disconnect), cnnwin); + + priv->toolbar_is_reconfiguring = FALSE; + return toolbar; +} + +static void rcw_place_toolbar(GtkToolbar *toolbar, GtkGrid *grid, GtkWidget *sibling, int toolbar_placement) +{ + /* Place the toolbar inside the grid and set its orientation */ + + if (toolbar_placement == TOOLBAR_PLACEMENT_LEFT || toolbar_placement == TOOLBAR_PLACEMENT_RIGHT) + gtk_orientable_set_orientation(GTK_ORIENTABLE(toolbar), GTK_ORIENTATION_VERTICAL); + else + gtk_orientable_set_orientation(GTK_ORIENTABLE(toolbar), GTK_ORIENTATION_HORIZONTAL); + + + switch (toolbar_placement) { + case TOOLBAR_PLACEMENT_TOP: + gtk_widget_set_hexpand(GTK_WIDGET(toolbar), TRUE); + gtk_widget_set_vexpand(GTK_WIDGET(toolbar), FALSE); + gtk_grid_attach_next_to(GTK_GRID(grid), GTK_WIDGET(toolbar), sibling, GTK_POS_TOP, 1, 1); + break; + case TOOLBAR_PLACEMENT_RIGHT: + gtk_widget_set_vexpand(GTK_WIDGET(toolbar), TRUE); + gtk_widget_set_hexpand(GTK_WIDGET(toolbar), FALSE); + gtk_grid_attach_next_to(GTK_GRID(grid), GTK_WIDGET(toolbar), sibling, GTK_POS_RIGHT, 1, 1); + break; + case TOOLBAR_PLACEMENT_BOTTOM: + gtk_widget_set_hexpand(GTK_WIDGET(toolbar), TRUE); + gtk_widget_set_vexpand(GTK_WIDGET(toolbar), FALSE); + gtk_grid_attach_next_to(GTK_GRID(grid), GTK_WIDGET(toolbar), sibling, GTK_POS_BOTTOM, 1, 1); + break; + case TOOLBAR_PLACEMENT_LEFT: + gtk_widget_set_vexpand(GTK_WIDGET(toolbar), TRUE); + gtk_widget_set_hexpand(GTK_WIDGET(toolbar), FALSE); + gtk_grid_attach_next_to(GTK_GRID(grid), GTK_WIDGET(toolbar), sibling, GTK_POS_LEFT, 1, 1); + break; + } +} + +static void rco_update_toolbar(RemminaConnectionObject *cnnobj) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv = cnnobj->cnnwin->priv; + GtkToolItem *toolitem; + gboolean bval, dynres_avail, scale_avail; + gboolean test_floating_toolbar; + RemminaScaleMode scalemode; + + priv->toolbar_is_reconfiguring = TRUE; + + rco_update_toolbar_autofit_button(cnnobj); + + toolitem = priv->toolitem_switch_page; + if (kioskmode) + bval = FALSE; + else + bval = (gtk_notebook_get_n_pages(GTK_NOTEBOOK(priv->notebook)) > 1); + gtk_widget_set_sensitive(GTK_WIDGET(toolitem), bval); + + if (cnnobj->remmina_file->filename) + gtk_widget_set_sensitive(GTK_WIDGET(priv->toolitem_duplicate), TRUE); + else + gtk_widget_set_sensitive(GTK_WIDGET(priv->toolitem_duplicate), FALSE); + + scalemode = get_current_allowed_scale_mode(cnnobj, &dynres_avail, &scale_avail); + gtk_widget_set_sensitive(GTK_WIDGET(priv->toolitem_dynres), dynres_avail && cnnobj->connected); + gtk_widget_set_sensitive(GTK_WIDGET(priv->toolitem_scale), scale_avail && cnnobj->connected); + + switch (scalemode) { + case REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE: + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_dynres), FALSE); + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_scale), FALSE); + gtk_widget_set_sensitive(GTK_WIDGET(priv->scaler_option_button), FALSE); + break; + case REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED: + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_dynres), FALSE); + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_scale), TRUE); + gtk_widget_set_sensitive(GTK_WIDGET(priv->scaler_option_button), TRUE && cnnobj->connected); + break; + case REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES: + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_dynres), TRUE); + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_scale), FALSE); + gtk_widget_set_sensitive(GTK_WIDGET(priv->scaler_option_button), FALSE); + break; + } + + /* REMMINA_PROTOCOL_FEATURE_TYPE_MULTIMON */ + toolitem = priv->toolitem_multimon; + if (toolitem) { + gint hasmultimon = remmina_protocol_widget_query_feature_by_type(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), + REMMINA_PROTOCOL_FEATURE_TYPE_MULTIMON); + + gtk_widget_set_sensitive(GTK_WIDGET(toolitem), cnnobj->connected); + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(toolitem), + remmina_file_get_int(cnnobj->remmina_file, "multimon", FALSE)); + gtk_widget_set_sensitive(GTK_WIDGET(toolitem), hasmultimon); + } + + toolitem = priv->toolitem_grab; + gtk_widget_set_sensitive(GTK_WIDGET(toolitem), cnnobj->connected); + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(toolitem), + remmina_file_get_int(cnnobj->remmina_file, "keyboard_grab", FALSE)); + const gchar *protocol = remmina_file_get_string(cnnobj->remmina_file, "protocol"); + if (g_strcmp0(protocol, "SFTP") == 0 || g_strcmp0(protocol, "SSH") == 0) { + gtk_widget_set_sensitive(GTK_WIDGET(toolitem), FALSE); + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(toolitem), FALSE); + remmina_file_set_int(cnnobj->remmina_file, "keyboard_grab", FALSE); + } + + toolitem = priv->toolitem_preferences; + gtk_widget_set_sensitive(GTK_WIDGET(toolitem), cnnobj->connected); + bval = remmina_protocol_widget_query_feature_by_type(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), + REMMINA_PROTOCOL_FEATURE_TYPE_PREF); + gtk_widget_set_sensitive(GTK_WIDGET(toolitem), bval && cnnobj->connected); + + toolitem = priv->toolitem_tools; + bval = remmina_protocol_widget_query_feature_by_type(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), + REMMINA_PROTOCOL_FEATURE_TYPE_TOOL); + gtk_widget_set_sensitive(GTK_WIDGET(toolitem), bval && cnnobj->connected); + + gtk_widget_set_sensitive(GTK_WIDGET(priv->toolitem_screenshot), cnnobj->connected); + + gtk_window_set_title(GTK_WINDOW(cnnobj->cnnwin), remmina_file_get_string(cnnobj->remmina_file, "name")); + + test_floating_toolbar = (priv->floating_toolbar_widget != NULL); + + if (test_floating_toolbar) { + const gchar *str = remmina_file_get_string(cnnobj->remmina_file, "name"); + const gchar *format; + GdkRGBA rgba; + gchar *bg; + + bg = g_strdup(remmina_pref.grab_color); + if (!gdk_rgba_parse(&rgba, bg)) { + REMMINA_DEBUG("%s cannot be parsed as a color", bg); + bg = g_strdup("#00FF00"); + } else { + REMMINA_DEBUG("Using %s as background color", bg); + } + + if (remmina_file_get_int(cnnobj->remmina_file, "keyboard_grab", FALSE)) { + if (remmina_pref_get_boolean("grab_color_switch")) { + gtk_widget_override_background_color(priv->overlay_ftb_fr, GTK_STATE_NORMAL, &rgba); + format = g_strconcat("<span bgcolor=\"", bg, "\" size=\"large\"><b>(G: ON) - \%s</b></span>", NULL); + } else { + gtk_widget_override_background_color(priv->overlay_ftb_fr, GTK_STATE_NORMAL, NULL); + format = "<big><b>(G: ON) - \%s</b></big>"; + } + } else { + gtk_widget_override_background_color(priv->overlay_ftb_fr, GTK_STATE_NORMAL, NULL); + format = "<big><b>(G:OFF) - \%s</b></big>"; + } + gchar *markup; + + markup = g_markup_printf_escaped(format, str); + gtk_label_set_markup(GTK_LABEL(priv->floating_toolbar_label), markup); + g_free(markup); + g_free(bg); + } + + priv->toolbar_is_reconfiguring = FALSE; +} + +static void rcw_set_toolbar_visibility(RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv = cnnwin->priv; + + if (priv->view_mode == SCROLLED_WINDOW_MODE) { + if (remmina_pref.hide_connection_toolbar) + gtk_widget_hide(priv->toolbar); + else + gtk_widget_show(priv->toolbar); + } +} + +#if DEBUG_KB_GRABBING +static void print_crossing_event(GdkEventCrossing *event) { + printf("DEBUG_KB_GRABBING: --- Crossing event detail: "); + switch (event->detail) { + case GDK_NOTIFY_ANCESTOR: printf("GDK_NOTIFY_ANCESTOR"); break; + case GDK_NOTIFY_VIRTUAL: printf("GDK_NOTIFY_VIRTUAL"); break; + case GDK_NOTIFY_NONLINEAR: printf("GDK_NOTIFY_NONLINEAR"); break; + case GDK_NOTIFY_NONLINEAR_VIRTUAL: printf("GDK_NOTIFY_NONLINEAR_VIRTUAL"); break; + case GDK_NOTIFY_UNKNOWN: printf("GDK_NOTIFY_UNKNOWN"); break; + case GDK_NOTIFY_INFERIOR: printf("GDK_NOTIFY_INFERIOR"); break; + default: printf("unknown"); + } + printf("\n"); + printf("DEBUG_KB_GRABBING: --- Crossing event mode="); + switch (event->mode) { + case GDK_CROSSING_NORMAL: printf("GDK_CROSSING_NORMAL"); break; + case GDK_CROSSING_GRAB: printf("GDK_CROSSING_GRAB"); break; + case GDK_CROSSING_UNGRAB: printf("GDK_CROSSING_UNGRAB"); break; + case GDK_CROSSING_GTK_GRAB: printf("GDK_CROSSING_GTK_GRAB"); break; + case GDK_CROSSING_GTK_UNGRAB: printf("GDK_CROSSING_GTK_UNGRAB"); break; + case GDK_CROSSING_STATE_CHANGED: printf("GDK_CROSSING_STATE_CHANGED"); break; + case GDK_CROSSING_TOUCH_BEGIN: printf("GDK_CROSSING_TOUCH_BEGIN"); break; + case GDK_CROSSING_TOUCH_END: printf("GDK_CROSSING_TOUCH_END"); break; + case GDK_CROSSING_DEVICE_SWITCH: printf("GDK_CROSSING_DEVICE_SWITCH"); break; + default: printf("unknown"); + } + printf("\n"); +} +#endif + +static gboolean rcw_floating_toolbar_on_enter(GtkWidget *widget, GdkEventCrossing *event, + RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + rcw_floating_toolbar_show(cnnwin, TRUE); + return TRUE; +} + +static gboolean rcw_floating_toolbar_on_leave(GtkWidget *widget, GdkEventCrossing *event, + RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + if (event->detail != GDK_NOTIFY_INFERIOR) + rcw_floating_toolbar_show(cnnwin, FALSE); + return TRUE; +} + + +static gboolean rcw_on_enter_notify_event(GtkWidget *widget, GdkEventCrossing *event, + gpointer user_data) +{ + TRACE_CALL(__func__); +#if DEBUG_KB_GRABBING + printf("DEBUG_KB_GRABBING: enter-notify-event on rcw received\n"); + print_crossing_event(event); +#endif + return FALSE; +} + + + +static gboolean rcw_on_leave_notify_event(GtkWidget *widget, GdkEventCrossing *event, + gpointer user_data) +{ + TRACE_CALL(__func__); + RemminaConnectionWindow *cnnwin = (RemminaConnectionWindow *)widget; + +#if DEBUG_KB_GRABBING + printf("DEBUG_KB_GRABBING: leave-notify-event on rcw received\n"); + print_crossing_event(event); +#endif + + if (event->mode != GDK_CROSSING_NORMAL && event->mode != GDK_CROSSING_UNGRAB) { +#if DEBUG_KB_GRABBING + printf("DEBUG_KB_GRABBING: ignored because mode is not GDK_CROSSING_NORMAL GDK_CROSSING_UNGRAB\n"); +#endif + return FALSE; + } + + if (cnnwin->priv->delayed_grab_eventsourceid) { + g_source_remove(cnnwin->priv->delayed_grab_eventsourceid); + cnnwin->priv->delayed_grab_eventsourceid = 0; + } + + /* Workaround for https://gitlab.gnome.org/GNOME/mutter/-/issues/2450#note_1586570 */ + if (event->mode != GDK_CROSSING_UNGRAB) { + rcw_kp_ungrab(cnnwin); + rcw_pointer_ungrab(cnnwin); + } else { +#if DEBUG_KB_GRABBING + printf("DEBUG_KB_GRABBING: not ungrabbing, this event seems to be an unwanted event from GTK\n"); +#endif + } + + return FALSE; +} + + +static gboolean rco_leave_protocol_widget(GtkWidget *widget, GdkEventCrossing *event, + RemminaConnectionObject *cnnobj) +{ + TRACE_CALL(__func__); + +#if DEBUG_KB_GRABBING + printf("DEBUG_KB_GRABBING: received leave event on RCO.\n"); + print_crossing_event(event); +#endif + + if (cnnobj->cnnwin->priv->delayed_grab_eventsourceid) { + g_source_remove(cnnobj->cnnwin->priv->delayed_grab_eventsourceid); + cnnobj->cnnwin->priv->delayed_grab_eventsourceid = 0; + } + + cnnobj->cnnwin->priv->pointer_entered = FALSE; + + /* Ungrab only if the leave is due to normal mouse motion and not to an inferior */ + if (event->mode == GDK_CROSSING_NORMAL && event->detail != GDK_NOTIFY_INFERIOR) + rcw_kp_ungrab(cnnobj->cnnwin); + + return FALSE; +} + + +gboolean rco_enter_protocol_widget(GtkWidget *widget, GdkEventCrossing *event, + RemminaConnectionObject *cnnobj) +{ + TRACE_CALL(__func__); + gboolean active; + +#if DEBUG_KB_GRABBING + printf("DEBUG_KB_GRABBING: %s: enter on protocol widget event received\n", __func__); + print_crossing_event(event); +#endif + + RemminaConnectionWindowPriv *priv = cnnobj->cnnwin->priv; + if (!priv->sticky && event->mode == GDK_CROSSING_NORMAL) + rcw_floating_toolbar_show(cnnobj->cnnwin, FALSE); + + priv->pointer_entered = TRUE; + + if (event->mode == GDK_CROSSING_UNGRAB) { + // Someone steal our grab, take note and do not attempt to regrab + cnnobj->cnnwin->priv->kbcaptured = FALSE; + cnnobj->cnnwin->priv->pointer_captured = FALSE; + return FALSE; + } + + /* Check if we need grabbing */ + active = gtk_window_is_active(GTK_WINDOW(cnnobj->cnnwin)); + if (remmina_file_get_int(cnnobj->remmina_file, "keyboard_grab", FALSE) && active) { + rcw_keyboard_grab(cnnobj->cnnwin); + rcw_pointer_grab(cnnobj->cnnwin); + } + + return FALSE; +} + +static gboolean focus_in_delayed_grab(RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + +#if DEBUG_KB_GRABBING + printf("DEBUG_KB_GRABBING: %s\n", __func__); +#endif + if (cnnwin->priv->pointer_entered) { +#if DEBUG_KB_GRABBING + printf("DEBUG_KB_GRABBING: delayed requesting kb and pointer grab, because of pointer inside\n"); +#endif + rcw_keyboard_grab(cnnwin); + rcw_pointer_grab(cnnwin); + } +#if DEBUG_KB_GRABBING + else { + printf("DEBUG_KB_GRABBING: %s not grabbing because pointer_entered is false\n", __func__); + } +#endif + cnnwin->priv->delayed_grab_eventsourceid = 0; + return G_SOURCE_REMOVE; +} + +static void rcw_focus_in(RemminaConnectionWindow *cnnwin) +{ + /* This function is the default signal handler for focus-in-event, + * but can also be called after a window focus state change event + * from rcw_state_event(). So expect to be called twice + * when cnnwin gains the focus */ + + TRACE_CALL(__func__); + RemminaConnectionObject *cnnobj; + + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return; + + if (cnnobj && cnnobj->connected && remmina_file_get_int(cnnobj->remmina_file, "keyboard_grab", FALSE)) { +#if DEBUG_KB_GRABBING + printf("DEBUG_KB_GRABBING: Received focus in on rcw, grabbing enabled: requesting kb grab, delayed\n"); +#endif + if (cnnwin->priv->delayed_grab_eventsourceid == 0) + cnnwin->priv->delayed_grab_eventsourceid = g_timeout_add(300, (GSourceFunc)focus_in_delayed_grab, cnnwin); + } +#if DEBUG_KB_GRABBING + else { + printf("DEBUG_KB_GRABBING: Received focus in on rcw, but a condition will prevent to grab\n"); + } +#endif +} + +static void rcw_focus_out(RemminaConnectionWindow *cnnwin) +{ + /* This function is the default signal handler for focus-out-event, + * but can also be called after a window focus state change event + * from rcw_state_event(). So expect to be called twice + * when cnnwin loses the focus */ + + TRACE_CALL(__func__); + RemminaConnectionObject *cnnobj; + + rcw_kp_ungrab(cnnwin); + + cnnwin->priv->hostkey_activated = FALSE; + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return; + + if (REMMINA_IS_SCROLLED_VIEWPORT(cnnobj->scrolled_container)) + remmina_scrolled_viewport_remove_motion(REMMINA_SCROLLED_VIEWPORT(cnnobj->scrolled_container)); + + if (cnnobj->proto && cnnobj->scrolled_container) + remmina_protocol_widget_call_feature_by_type(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), + REMMINA_PROTOCOL_FEATURE_TYPE_UNFOCUS, 0); +} + +static gboolean +rcw_floating_toolbar_hide(RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv = cnnwin->priv; + + priv->hidetb_eventsource = 0; + rcw_floating_toolbar_show(cnnwin, FALSE); + return G_SOURCE_REMOVE; +} + +static gboolean rcw_floating_toolbar_on_scroll(GtkWidget *widget, GdkEventScroll *event, + RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionObject *cnnobj; + + int opacity; + + cnnobj = rcw_get_visible_cnnobj(cnnwin); + if (!cnnobj) + return TRUE; + + opacity = remmina_file_get_int(cnnobj->remmina_file, "toolbar_opacity", 0); + switch (event->direction) { + case GDK_SCROLL_UP: + if (opacity > 0) { + remmina_file_set_int(cnnobj->remmina_file, "toolbar_opacity", opacity - 1); + rcw_update_toolbar_opacity(cnnwin); + return TRUE; + } + break; + case GDK_SCROLL_DOWN: + if (opacity < TOOLBAR_OPACITY_LEVEL) { + remmina_file_set_int(cnnobj->remmina_file, "toolbar_opacity", opacity + 1); + rcw_update_toolbar_opacity(cnnwin); + return TRUE; + } + break; +#if GTK_CHECK_VERSION(3, 4, 0) + case GDK_SCROLL_SMOOTH: + if (event->delta_y < 0 && opacity > 0) { + remmina_file_set_int(cnnobj->remmina_file, "toolbar_opacity", opacity - 1); + rcw_update_toolbar_opacity(cnnwin); + return TRUE; + } + if (event->delta_y > 0 && opacity < TOOLBAR_OPACITY_LEVEL) { + remmina_file_set_int(cnnobj->remmina_file, "toolbar_opacity", opacity + 1); + rcw_update_toolbar_opacity(cnnwin); + return TRUE; + } + break; +#endif + default: + break; + } + return TRUE; +} + +static gboolean rcw_after_configure_scrolled(gpointer user_data) +{ + TRACE_CALL(__func__); + gint width, height; + GdkWindowState s; + gint ipg, npages; + RemminaConnectionWindow *cnnwin; + + cnnwin = (RemminaConnectionWindow *)user_data; + + if (!cnnwin || !cnnwin->priv) + return FALSE; + + s = gdk_window_get_state(gtk_widget_get_window(GTK_WIDGET(cnnwin))); + + + /* Changed window_maximize, window_width and window_height for all + * connections inside the notebook */ + npages = gtk_notebook_get_n_pages(GTK_NOTEBOOK(cnnwin->priv->notebook)); + for (ipg = 0; ipg < npages; ipg++) { + RemminaConnectionObject *cnnobj; + cnnobj = g_object_get_data( + G_OBJECT(gtk_notebook_get_nth_page(GTK_NOTEBOOK(cnnwin->priv->notebook), ipg)), + "cnnobj"); + if (s & GDK_WINDOW_STATE_MAXIMIZED) { + remmina_file_set_int(cnnobj->remmina_file, "window_maximize", TRUE); + } else { + gtk_window_get_size(GTK_WINDOW(cnnobj->cnnwin), &width, &height); + remmina_file_set_int(cnnobj->remmina_file, "window_width", width); + remmina_file_set_int(cnnobj->remmina_file, "window_height", height); + remmina_file_set_int(cnnobj->remmina_file, "window_maximize", FALSE); + } + } + cnnwin->priv->acs_eventsourceid = 0; + return FALSE; +} + +static gboolean rcw_on_configure(GtkWidget *widget, GdkEventConfigure *event, + gpointer data) +{ + TRACE_CALL(__func__); + RemminaConnectionWindow *cnnwin; + RemminaConnectionObject *cnnobj; + + if (!REMMINA_IS_CONNECTION_WINDOW(widget)) + return FALSE; + + cnnwin = (RemminaConnectionWindow *)widget; + + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return FALSE; + + if (cnnwin->priv->acs_eventsourceid) { + g_source_remove(cnnwin->priv->acs_eventsourceid); + cnnwin->priv->acs_eventsourceid = 0; + } + + if (gtk_widget_get_window(GTK_WIDGET(cnnwin)) + && cnnwin->priv->view_mode == SCROLLED_WINDOW_MODE) + /* Under GNOME Shell we receive this configure_event BEFORE a window + * is really unmaximized, so we must read its new state and dimensions + * later, not now */ + cnnwin->priv->acs_eventsourceid = g_timeout_add(500, rcw_after_configure_scrolled, cnnwin); + + if (cnnwin->priv->view_mode != SCROLLED_WINDOW_MODE) + /* Notify window of change so that scroll border can be hidden or shown if needed */ + rco_check_resize(cnnobj); + return FALSE; +} + +static void rcw_update_pin(RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + if (cnnwin->priv->pin_down) + gtk_button_set_image(GTK_BUTTON(cnnwin->priv->pin_button), + gtk_image_new_from_icon_name("org.remmina.Remmina-pin-down-symbolic", GTK_ICON_SIZE_MENU)); + else + gtk_button_set_image(GTK_BUTTON(cnnwin->priv->pin_button), + gtk_image_new_from_icon_name("org.remmina.Remmina-pin-up-symbolic", GTK_ICON_SIZE_MENU)); +} + +static void rcw_toolbar_pin(GtkWidget *widget, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + remmina_pref.toolbar_pin_down = cnnwin->priv->pin_down = !cnnwin->priv->pin_down; + remmina_pref_save(); + rcw_update_pin(cnnwin); +} + +static void rcw_create_floating_toolbar(RemminaConnectionWindow *cnnwin, gint mode) +{ + TRACE_CALL(__func__); + + RemminaConnectionWindowPriv *priv = cnnwin->priv; + GtkWidget *ftb_widget; + GtkWidget *vbox; + GtkWidget *hbox; + GtkWidget *label; + GtkWidget *pinbutton; + GtkWidget *tb; + + + /* A widget to be used for GtkOverlay for GTK >= 3.10 */ + ftb_widget = gtk_event_box_new(); + + vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_show(vbox); + + gtk_container_add(GTK_CONTAINER(ftb_widget), vbox); + + tb = rcw_create_toolbar(cnnwin, mode); + hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_show(hbox); + + + /* The pin button */ + pinbutton = gtk_button_new(); + gtk_widget_show(pinbutton); + gtk_box_pack_start(GTK_BOX(hbox), pinbutton, FALSE, FALSE, 0); + gtk_button_set_relief(GTK_BUTTON(pinbutton), GTK_RELIEF_NONE); +#if GTK_CHECK_VERSION(3, 20, 0) + gtk_widget_set_focus_on_click(GTK_WIDGET(pinbutton), FALSE); +#else + gtk_button_set_focus_on_click(GTK_BUTTON(pinbutton), FALSE); +#endif + gtk_widget_set_name(pinbutton, "remmina-pin-button"); + g_signal_connect(G_OBJECT(pinbutton), "clicked", G_CALLBACK(rcw_toolbar_pin), cnnwin); + priv->pin_button = pinbutton; + priv->pin_down = remmina_pref.toolbar_pin_down; + rcw_update_pin(cnnwin); + + + label = gtk_label_new(""); + gtk_label_set_max_width_chars(GTK_LABEL(label), 50); + gtk_widget_show(label); + + gtk_box_pack_start(GTK_BOX(hbox), label, TRUE, TRUE, 0); + + priv->floating_toolbar_label = label; + + if (remmina_pref.floating_toolbar_placement == FLOATING_TOOLBAR_PLACEMENT_BOTTOM) { + gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(vbox), tb, FALSE, FALSE, 0); + } else { + gtk_box_pack_start(GTK_BOX(vbox), tb, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0); + } + + priv->floating_toolbar_widget = ftb_widget; + gtk_widget_show(ftb_widget); +} + +static void rcw_toolbar_place_signal(RemminaConnectionWindow *cnnwin, gpointer data) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv; + + priv = cnnwin->priv; + /* Detach old toolbar widget and reattach in new position in the grid */ + if (priv->toolbar && priv->grid) { + g_object_ref(priv->toolbar); + gtk_container_remove(GTK_CONTAINER(priv->grid), priv->toolbar); + rcw_place_toolbar(GTK_TOOLBAR(priv->toolbar), GTK_GRID(priv->grid), GTK_WIDGET(priv->notebook), remmina_pref.toolbar_placement); + g_object_unref(priv->toolbar); + } +} + + +static void rcw_init(RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv; + + priv = g_new0(RemminaConnectionWindowPriv, 1); + cnnwin->priv = priv; + + priv->view_mode = SCROLLED_WINDOW_MODE; + if (kioskmode && kioskmode == TRUE) + priv->view_mode = VIEWPORT_FULLSCREEN_MODE; + + priv->floating_toolbar_opacity = 1.0; + priv->kbcaptured = FALSE; + priv->pointer_captured = FALSE; + priv->pointer_entered = FALSE; + priv->fss_view_mode = VIEWPORT_FULLSCREEN_MODE; + priv->ss_width = 640; + priv->ss_height = 480; + priv->ss_maximized = FALSE; + + remmina_widget_pool_register(GTK_WIDGET(cnnwin)); +} + +static gboolean rcw_focus_in_event(GtkWidget *widget, GdkEventWindowState *event, gpointer user_data) +{ + TRACE_CALL(__func__); +#if DEBUG_KB_GRABBING + printf("DEBUG_KB_GRABBING: RCW focus-in-event received\n"); +#endif + rcw_focus_in((RemminaConnectionWindow *)widget); + return FALSE; +} + +static gboolean rcw_focus_out_event(GtkWidget *widget, GdkEventWindowState *event, gpointer user_data) +{ + TRACE_CALL(__func__); +#if DEBUG_KB_GRABBING + printf("DEBUG_KB_GRABBING: RCW focus-out-event received\n"); +#endif + rcw_focus_out((RemminaConnectionWindow *)widget); + return FALSE; +} + + +static gboolean rcw_state_event(GtkWidget *widget, GdkEventWindowState *event, gpointer user_data) +{ + TRACE_CALL(__func__); + + if (!REMMINA_IS_CONNECTION_WINDOW(widget)) + return FALSE; + +#if DEBUG_KB_GRABBING + printf("DEBUG_KB_GRABBING: window-state-event received\n"); +#endif + + if (event->changed_mask & GDK_WINDOW_STATE_FOCUSED) { + if (event->new_window_state & GDK_WINDOW_STATE_FOCUSED) + rcw_focus_in((RemminaConnectionWindow *)widget); + else + rcw_focus_out((RemminaConnectionWindow *)widget); + } + + return FALSE; +} + +static gboolean rcw_map_event(GtkWidget *widget, GdkEvent *event, gpointer data) +{ + TRACE_CALL(__func__); + + + + RemminaConnectionWindow *cnnwin = (RemminaConnectionWindow *)widget; + RemminaConnectionObject *cnnobj; + RemminaProtocolWidget *gp; + + if (cnnwin->priv->toolbar_is_reconfiguring) return FALSE; + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return FALSE; + + gp = REMMINA_PROTOCOL_WIDGET(cnnobj->proto); + REMMINA_DEBUG("Mapping: %s", gtk_widget_get_name(widget)); + if (remmina_protocol_widget_map_event(gp)) + REMMINA_DEBUG("Called plugin mapping function"); + return FALSE; +} + +static gboolean rcw_unmap_event(GtkWidget *widget, GdkEvent *event, gpointer data) +{ + TRACE_CALL(__func__); + + RemminaConnectionWindow *cnnwin = (RemminaConnectionWindow *)widget; + RemminaConnectionObject *cnnobj; + RemminaProtocolWidget *gp; + + if (cnnwin->priv->toolbar_is_reconfiguring) return FALSE; + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return FALSE; + + gp = REMMINA_PROTOCOL_WIDGET(cnnobj->proto); + REMMINA_DEBUG("Unmapping: %s", gtk_widget_get_name(widget)); + if (remmina_protocol_widget_unmap_event(gp)) + REMMINA_DEBUG("Called plugin mapping function"); + return FALSE; +} + +static gboolean rcw_map_event_fullscreen(GtkWidget *widget, GdkEvent *event, gpointer data) +{ + TRACE_CALL(__func__); + RemminaConnectionObject *cnnobj; + gint target_monitor; + + REMMINA_DEBUG("Mapping: %s", gtk_widget_get_name(widget)); + + if (!REMMINA_IS_CONNECTION_WINDOW(widget)) { + REMMINA_DEBUG("Remmina Connection Window undefined, cannot go fullscreen"); + return FALSE; + } + + //RemminaConnectionWindow *cnnwin = (RemminaConnectionWindow *)data; + cnnobj = rcw_get_visible_cnnobj((RemminaConnectionWindow *)widget); + //cnnobj = g_object_get_data(G_OBJECT(widget), "cnnobj"); + if (!cnnobj) { + REMMINA_DEBUG("Remmina Connection Object undefined, cannot go fullscreen"); + return FALSE; + } + + RemminaProtocolWidget *gp = REMMINA_PROTOCOL_WIDGET(cnnobj->proto); + + if (!gp) + REMMINA_DEBUG("Remmina Protocol Widget undefined, cannot go fullscreen"); + + if (remmina_protocol_widget_get_multimon(gp) >= 1) { + REMMINA_DEBUG("Fullscreen on all monitor"); + gdk_window_set_fullscreen_mode(gtk_widget_get_window(widget), GDK_FULLSCREEN_ON_ALL_MONITORS); + gdk_window_fullscreen(gtk_widget_get_window(widget)); + return TRUE; + } else { + REMMINA_DEBUG("Fullscreen on one monitor"); + } + + target_monitor = GPOINTER_TO_INT(data); + +#if GTK_CHECK_VERSION(3, 18, 0) + if (remmina_pref.fullscreen_on_auto) { + if (target_monitor == FULL_SCREEN_TARGET_MONITOR_UNDEFINED) + gtk_window_fullscreen(GTK_WINDOW(widget)); + else + gtk_window_fullscreen_on_monitor(GTK_WINDOW(widget), gtk_window_get_screen(GTK_WINDOW(widget)), + target_monitor); + } else { + REMMINA_DEBUG("Fullscreen managed by WM or by the user, as per settings"); + gtk_window_fullscreen(GTK_WINDOW(widget)); + } +#else + REMMINA_DEBUG("Cannot fullscreen on a specific monitor, feature available from GTK 3.18"); + gtk_window_fullscreen(GTK_WINDOW(widget)); +#endif + + if (remmina_protocol_widget_map_event(gp)) + REMMINA_DEBUG("Called plugin mapping function"); + + return FALSE; +} + +static RemminaConnectionWindow * +rcw_new(gboolean fullscreen, int full_screen_target_monitor) +{ + TRACE_CALL(__func__); + RemminaConnectionWindow *cnnwin; + + cnnwin = RCW(g_object_new(REMMINA_TYPE_CONNECTION_WINDOW, NULL)); + cnnwin->priv->on_delete_confirm_mode = RCW_ONDELETE_CONFIRM_IF_2_OR_MORE; + + if (fullscreen) + /* Put the window in fullscreen after it is mapped to have it appear on the same monitor */ + g_signal_connect(G_OBJECT(cnnwin), "map-event", G_CALLBACK(rcw_map_event_fullscreen), GINT_TO_POINTER(full_screen_target_monitor)); + else + g_signal_connect(G_OBJECT(cnnwin), "map-event", G_CALLBACK(rcw_map_event), NULL); + g_signal_connect(G_OBJECT(cnnwin), "unmap-event", G_CALLBACK(rcw_unmap_event), NULL); + + gtk_container_set_border_width(GTK_CONTAINER(cnnwin), 0); + g_signal_connect(G_OBJECT(cnnwin), "toolbar-place", G_CALLBACK(rcw_toolbar_place_signal), NULL); + + g_signal_connect(G_OBJECT(cnnwin), "delete-event", G_CALLBACK(rcw_delete_event), NULL); + g_signal_connect(G_OBJECT(cnnwin), "destroy", G_CALLBACK(rcw_destroy), NULL); + + /* Under Xorg focus-in-event and focus-out-event don’t work when keyboard is grabbed + * via gdk_device_grab. So we listen for window-state-event to detect focus in and focus out. + * But we must also listen focus-in-event and focus-out-event because some + * window managers missing _NET_WM_STATE_FOCUSED hint, does not update the window state + * in case of focus change */ + g_signal_connect(G_OBJECT(cnnwin), "window-state-event", G_CALLBACK(rcw_state_event), NULL); + g_signal_connect(G_OBJECT(cnnwin), "focus-in-event", G_CALLBACK(rcw_focus_in_event), NULL); + g_signal_connect(G_OBJECT(cnnwin), "focus-out-event", G_CALLBACK(rcw_focus_out_event), NULL); + + g_signal_connect(G_OBJECT(cnnwin), "enter-notify-event", G_CALLBACK(rcw_on_enter_notify_event), NULL); + g_signal_connect(G_OBJECT(cnnwin), "leave-notify-event", G_CALLBACK(rcw_on_leave_notify_event), NULL); + + + g_signal_connect(G_OBJECT(cnnwin), "configure_event", G_CALLBACK(rcw_on_configure), NULL); + + return cnnwin; +} + +/* This function will be called for the first connection. A tag is set to the window so that + * other connections can determine if whether a new tab should be append to the same window + */ +static void rcw_update_tag(RemminaConnectionWindow *cnnwin, RemminaConnectionObject *cnnobj) +{ + TRACE_CALL(__func__); + gchar *tag; + + switch (remmina_pref.tab_mode) { + case REMMINA_TAB_BY_GROUP: + tag = g_strdup(remmina_file_get_string(cnnobj->remmina_file, "group")); + break; + case REMMINA_TAB_BY_PROTOCOL: + tag = g_strdup(remmina_file_get_string(cnnobj->remmina_file, "protocol")); + break; + default: + tag = NULL; + break; + } + g_object_set_data_full(G_OBJECT(cnnwin), "tag", tag, (GDestroyNotify)g_free); +} + +void rcw_grab_focus(RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionObject *cnnobj; + + if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return; + + if (GTK_IS_WIDGET(cnnobj->proto)) + remmina_protocol_widget_grab_focus(REMMINA_PROTOCOL_WIDGET(cnnobj->proto)); +} + +static GtkWidget *nb_find_page_by_cnnobj(GtkNotebook *notebook, RemminaConnectionObject *cnnobj) +{ + gint i, np; + GtkWidget *found_page, *pg; + + if (cnnobj == NULL || cnnobj->cnnwin == NULL || cnnobj->cnnwin->priv == NULL) + return NULL; + found_page = NULL; + np = gtk_notebook_get_n_pages(cnnobj->cnnwin->priv->notebook); + for (i = 0; i < np; i++) { + pg = gtk_notebook_get_nth_page(cnnobj->cnnwin->priv->notebook, i); + if (g_object_get_data(G_OBJECT(pg), "cnnobj") == cnnobj) { + found_page = pg; + break; + } + } + + return found_page; +} + + +void rco_closewin(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaConnectionObject *cnnobj = gp->cnnobj; + GtkWidget *page_to_remove; + + + if (cnnobj && cnnobj->scrolled_container && REMMINA_IS_SCROLLED_VIEWPORT(cnnobj->scrolled_container)) { + REMMINA_DEBUG("deleting motion"); + remmina_scrolled_viewport_remove_motion(REMMINA_SCROLLED_VIEWPORT(cnnobj->scrolled_container)); + } + + if (cnnobj && cnnobj->cnnwin) { + page_to_remove = nb_find_page_by_cnnobj(cnnobj->cnnwin->priv->notebook, cnnobj); + if (page_to_remove) { + gtk_notebook_remove_page( + cnnobj->cnnwin->priv->notebook, + gtk_notebook_page_num(cnnobj->cnnwin->priv->notebook, page_to_remove)); + /* Invalidate pointers to objects destroyed by page removal */ + cnnobj->aspectframe = NULL; + cnnobj->viewport = NULL; + cnnobj->scrolled_container = NULL; + /* we cannot invalidate cnnobj->proto, because it can be already been + * detached from the widget hierarchy in rco_on_disconnect() */ + } + } + if (cnnobj) { + cnnobj->remmina_file = NULL; + g_free(cnnobj); + gp->cnnobj = NULL; + } + + remmina_application_condexit(REMMINA_CONDEXIT_ONDISCONNECT); +} + +void rco_on_close_button_clicked(GtkButton *button, RemminaConnectionObject *cnnobj) +{ + TRACE_CALL(__func__); + if (REMMINA_IS_PROTOCOL_WIDGET(cnnobj->proto)) { + if (!remmina_protocol_widget_is_closed((RemminaProtocolWidget *)cnnobj->proto)) + remmina_protocol_widget_close_connection(REMMINA_PROTOCOL_WIDGET(cnnobj->proto)); + else + rco_closewin((RemminaProtocolWidget *)cnnobj->proto); + } +} + +static GtkWidget *rco_create_tab_label(RemminaConnectionObject *cnnobj) +{ + TRACE_CALL(__func__); + GtkWidget *hbox; + GtkWidget *widget; + GtkWidget *button; + + hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4); + gtk_widget_show(hbox); + + widget = gtk_image_new_from_icon_name(remmina_file_get_icon_name(cnnobj->remmina_file), GTK_ICON_SIZE_MENU); + gtk_widget_show(widget); + gtk_box_pack_start(GTK_BOX(hbox), widget, FALSE, FALSE, 0); + + widget = gtk_label_new(remmina_file_get_string(cnnobj->remmina_file, "name")); + gtk_widget_set_valign(widget, GTK_ALIGN_CENTER); + gtk_widget_set_halign(widget, GTK_ALIGN_CENTER); + + gtk_widget_show(widget); + gtk_box_pack_start(GTK_BOX(hbox), widget, TRUE, TRUE, 0); + + button = gtk_button_new(); // The "x" to close the tab + gtk_button_set_relief(GTK_BUTTON(button), GTK_RELIEF_NONE); +#if GTK_CHECK_VERSION(3, 20, 0) + gtk_widget_set_focus_on_click(GTK_WIDGET(widget), FALSE); +#else + gtk_button_set_focus_on_click(GTK_BUTTON(button), FALSE); +#endif + gtk_widget_set_name(button, "remmina-small-button"); + gtk_widget_show(button); + + widget = gtk_image_new_from_icon_name("window-close", GTK_ICON_SIZE_MENU); + gtk_widget_show(widget); + gtk_container_add(GTK_CONTAINER(button), widget); + + gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0); + + g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(rco_on_close_button_clicked), cnnobj); + + + return hbox; +} + +static GtkWidget *rco_create_tab_page(RemminaConnectionObject *cnnobj) +{ + GtkWidget *page; + + page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_name(page, "remmina-tab-page"); + + + return page; +} + +static GtkWidget *rcw_append_new_page(RemminaConnectionWindow *cnnwin, RemminaConnectionObject *cnnobj) +{ + TRACE_CALL(__func__); + GtkWidget *page, *label; + GtkNotebook *notebook; + + notebook = cnnwin->priv->notebook; + + page = rco_create_tab_page(cnnobj); + g_object_set_data(G_OBJECT(page), "cnnobj", cnnobj); + label = rco_create_tab_label(cnnobj); + + cnnobj->cnnwin = cnnwin; + + gtk_notebook_append_page(notebook, page, label); + gtk_notebook_set_tab_reorderable(notebook, page, TRUE); + gtk_notebook_set_tab_detachable(notebook, page, TRUE); + /* This trick prevents the tab label from being focused */ + gtk_widget_set_can_focus(gtk_widget_get_parent(label), FALSE); + + if (gtk_widget_get_parent(cnnobj->scrolled_container) != NULL) + printf("REMMINA WARNING in %s: scrolled_container already has a parent\n", __func__); + gtk_box_pack_start(GTK_BOX(page), cnnobj->scrolled_container, TRUE, TRUE, 0); + + gtk_widget_show(page); + + return page; +} + + +static void rcw_update_notebook(RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + GtkNotebook *notebook; + gint n; + + notebook = GTK_NOTEBOOK(cnnwin->priv->notebook); + + switch (cnnwin->priv->view_mode) { + case SCROLLED_WINDOW_MODE: + n = gtk_notebook_get_n_pages(notebook); + gtk_notebook_set_show_tabs(notebook, remmina_pref.always_show_tab ? TRUE : n > 1); + gtk_notebook_set_show_border(notebook, remmina_pref.always_show_tab ? TRUE : n > 1); + break; + default: + gtk_notebook_set_show_tabs(notebook, FALSE); + gtk_notebook_set_show_border(notebook, FALSE); + break; + } +} + +static gboolean rcw_on_switch_page_finalsel(gpointer user_data) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv; + RemminaConnectionObject *cnnobj; + + if (!user_data) + return FALSE; + + cnnobj = (RemminaConnectionObject *)user_data; + if (!cnnobj->cnnwin) + return FALSE; + + priv = cnnobj->cnnwin->priv; + + if (GTK_IS_WIDGET(cnnobj->cnnwin)) { + rcw_floating_toolbar_show(cnnobj->cnnwin, TRUE); + if (!priv->hidetb_eventsource) + priv->hidetb_eventsource = g_timeout_add(TB_HIDE_TIME_TIME, (GSourceFunc) + rcw_floating_toolbar_hide, cnnobj->cnnwin); + rco_update_toolbar(cnnobj); + rcw_grab_focus(cnnobj->cnnwin); + if (priv->view_mode != SCROLLED_WINDOW_MODE) + rco_check_resize(cnnobj); + } + priv->spf_eventsourceid = 0; + return FALSE; +} + +static void rcw_on_switch_page(GtkNotebook *notebook, GtkWidget *newpage, guint page_num, + RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + RemminaConnectionWindowPriv *priv = cnnwin->priv; + RemminaConnectionObject *cnnobj_newpage; + + cnnobj_newpage = g_object_get_data(G_OBJECT(newpage), "cnnobj"); + if (priv->spf_eventsourceid) + g_source_remove(priv->spf_eventsourceid); + priv->spf_eventsourceid = g_idle_add(rcw_on_switch_page_finalsel, cnnobj_newpage); +} + +static void rcw_on_page_added(GtkNotebook *notebook, GtkWidget *child, guint page_num, + RemminaConnectionWindow *cnnwin) +{ + if (gtk_notebook_get_n_pages(GTK_NOTEBOOK(cnnwin->priv->notebook)) > 0) + rcw_update_notebook(cnnwin); +} + +static void rcw_on_page_removed(GtkNotebook *notebook, GtkWidget *child, guint page_num, + RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + + if (gtk_notebook_get_n_pages(GTK_NOTEBOOK(cnnwin->priv->notebook)) <= 0) + gtk_widget_destroy(GTK_WIDGET(cnnwin)); + +} + +static GtkNotebook * +rcw_on_notebook_create_window(GtkNotebook *notebook, GtkWidget *page, gint x, gint y, gpointer data) +{ + /* This signal callback is called by GTK when a detachable tab is dropped on the root window + * or in an existing window */ + + TRACE_CALL(__func__); + RemminaConnectionWindow *srccnnwin; + RemminaConnectionWindow *dstcnnwin; + RemminaConnectionObject *cnnobj; + GdkWindow *window; + gchar *srctag; + gint width, height; + +#if GTK_CHECK_VERSION(3, 20, 0) + GdkSeat *seat; +#else + GdkDeviceManager *manager; +#endif + GdkDevice *device = NULL; + +#if GTK_CHECK_VERSION(3, 20, 0) + seat = gdk_display_get_default_seat(gdk_display_get_default()); + device = gdk_seat_get_pointer(seat); +#else + manager = gdk_display_get_device_manager(gdk_display_get_default()); + device = gdk_device_manager_get_client_pointer(manager); +#endif + + window = gdk_device_get_window_at_position(device, &x, &y); + srccnnwin = RCW(gtk_widget_get_toplevel(GTK_WIDGET(notebook))); + dstcnnwin = RCW(remmina_widget_pool_find_by_window(REMMINA_TYPE_CONNECTION_WINDOW, window)); + + if (srccnnwin == dstcnnwin) + return NULL; + + if (gtk_notebook_get_n_pages(GTK_NOTEBOOK(srccnnwin->priv->notebook)) == 1 && !dstcnnwin) + return NULL; + + cnnobj = (RemminaConnectionObject *)g_object_get_data(G_OBJECT(page), "cnnobj"); + + if (!dstcnnwin) { + /* Drop is directed to a new rcw: create a new scrolled window to accommodate + * the dropped connectionand move our cnnobj there. Width and + * height of the new window are cloned from the current window */ + srctag = (gchar *)g_object_get_data(G_OBJECT(srccnnwin), "tag"); + gtk_window_get_size(GTK_WINDOW(srccnnwin), &width, &height); + dstcnnwin = rcw_create_scrolled(width, height, FALSE); // New dropped window is never maximized + g_object_set_data_full(G_OBJECT(dstcnnwin), "tag", g_strdup(srctag), (GDestroyNotify)g_free); + /* when returning, GTK will move the whole tab to the new notebook. + * Prepare cnnobj to be hosted in the new cnnwin */ + cnnobj->cnnwin = dstcnnwin; + } else { + cnnobj->cnnwin = dstcnnwin; + } + + remmina_protocol_widget_set_hostkey_func(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), + (RemminaHostkeyFunc)rcw_hostkey_func); + + return GTK_NOTEBOOK(cnnobj->cnnwin->priv->notebook); +} + +static GtkNotebook * +rcw_create_notebook(RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + GtkNotebook *notebook; + + notebook = GTK_NOTEBOOK(gtk_notebook_new()); + + gtk_notebook_set_scrollable(GTK_NOTEBOOK(notebook), TRUE); + gtk_widget_show(GTK_WIDGET(notebook)); + + g_signal_connect(G_OBJECT(notebook), "create-window", G_CALLBACK(rcw_on_notebook_create_window), NULL); + g_signal_connect(G_OBJECT(notebook), "switch-page", G_CALLBACK(rcw_on_switch_page), cnnwin); + g_signal_connect(G_OBJECT(notebook), "page-added", G_CALLBACK(rcw_on_page_added), cnnwin); + g_signal_connect(G_OBJECT(notebook), "page-removed", G_CALLBACK(rcw_on_page_removed), cnnwin); + gtk_widget_set_can_focus(GTK_WIDGET(notebook), FALSE); + + return notebook; +} + +/* Create a scrolled toplevel window */ +static RemminaConnectionWindow *rcw_create_scrolled(gint width, gint height, gboolean maximize) +{ + TRACE_CALL(__func__); + RemminaConnectionWindow *cnnwin; + GtkWidget *grid; + GtkWidget *toolbar; + GtkNotebook *notebook; + GtkSettings *settings = gtk_settings_get_default(); + + cnnwin = rcw_new(FALSE, 0); + gtk_widget_realize(GTK_WIDGET(cnnwin)); + + gtk_window_set_default_size(GTK_WINDOW(cnnwin), width, height); + g_object_set(settings, "gtk-application-prefer-dark-theme", remmina_pref.dark_theme, NULL); + + /* Create the toolbar */ + toolbar = rcw_create_toolbar(cnnwin, SCROLLED_WINDOW_MODE); + + /* Create the notebook */ + notebook = rcw_create_notebook(cnnwin); + + /* Create the grid container for toolbars+notebook and populate it */ + grid = gtk_grid_new(); + + + gtk_grid_attach(GTK_GRID(grid), GTK_WIDGET(notebook), 0, 0, 1, 1); + + gtk_widget_set_hexpand(GTK_WIDGET(notebook), TRUE); + gtk_widget_set_vexpand(GTK_WIDGET(notebook), TRUE); + + rcw_place_toolbar(GTK_TOOLBAR(toolbar), GTK_GRID(grid), GTK_WIDGET(notebook), remmina_pref.toolbar_placement); + + gtk_container_add(GTK_CONTAINER(cnnwin), grid); + + /* Add drag capabilities to the toolbar */ + gtk_drag_source_set(GTK_WIDGET(toolbar), GDK_BUTTON1_MASK, + dnd_targets_tb, sizeof dnd_targets_tb / sizeof *dnd_targets_tb, GDK_ACTION_MOVE); + g_signal_connect_after(GTK_WIDGET(toolbar), "drag-begin", G_CALLBACK(rcw_tb_drag_begin), NULL); + g_signal_connect(GTK_WIDGET(toolbar), "drag-failed", G_CALLBACK(rcw_tb_drag_failed), cnnwin); + + /* Add drop capabilities to the drop/dest target for the toolbar (the notebook) */ + gtk_drag_dest_set(GTK_WIDGET(notebook), GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_HIGHLIGHT, + dnd_targets_tb, sizeof dnd_targets_tb / sizeof *dnd_targets_tb, GDK_ACTION_MOVE); + gtk_drag_dest_set_track_motion(GTK_WIDGET(notebook), TRUE); + g_signal_connect(GTK_WIDGET(notebook), "drag-drop", G_CALLBACK(rcw_tb_drag_drop), cnnwin); + + cnnwin->priv->view_mode = SCROLLED_WINDOW_MODE; + cnnwin->priv->toolbar = toolbar; + cnnwin->priv->grid = grid; + cnnwin->priv->notebook = notebook; + cnnwin->priv->ss_width = width; + cnnwin->priv->ss_height = height; + cnnwin->priv->ss_maximized = maximize; + + /* The notebook and all its child must be realized now, or a reparent will + * call unrealize() and will destroy a GtkSocket */ + gtk_widget_show(grid); + gtk_widget_show(GTK_WIDGET(cnnwin)); + GtkWindowGroup *wingrp = gtk_window_group_new(); + + gtk_window_group_add_window(wingrp, GTK_WINDOW(cnnwin)); + gtk_window_set_transient_for(GTK_WINDOW(cnnwin), NULL); + + if (maximize) + gtk_window_maximize(GTK_WINDOW(cnnwin)); + + + rcw_set_toolbar_visibility(cnnwin); + + return cnnwin; +} + +static void rcw_create_overlay_ftb_overlay(RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + + GtkWidget *revealer; + RemminaConnectionWindowPriv *priv; + + priv = cnnwin->priv; + + if (priv->overlay_ftb_overlay != NULL) { + gtk_widget_destroy(priv->overlay_ftb_overlay); + priv->overlay_ftb_overlay = NULL; + priv->revealer = NULL; + } + if (priv->overlay_ftb_fr != NULL) { + gtk_widget_destroy(priv->overlay_ftb_fr); + priv->overlay_ftb_fr = NULL; + } + + rcw_create_floating_toolbar(cnnwin, priv->fss_view_mode); + + priv->overlay_ftb_overlay = gtk_event_box_new(); + + GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + + gtk_container_set_border_width(GTK_CONTAINER(vbox), 0); + + GtkWidget *handle = gtk_drawing_area_new(); + + gtk_widget_set_size_request(handle, 4, 4); + gtk_widget_set_name(handle, "ftb-handle"); + + revealer = gtk_revealer_new(); + + gtk_widget_set_halign(GTK_WIDGET(priv->overlay_ftb_overlay), GTK_ALIGN_CENTER); + + if (remmina_pref.floating_toolbar_placement == FLOATING_TOOLBAR_PLACEMENT_BOTTOM) { + gtk_box_pack_start(GTK_BOX(vbox), handle, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(vbox), revealer, FALSE, FALSE, 0); + gtk_revealer_set_transition_type(GTK_REVEALER(revealer), GTK_REVEALER_TRANSITION_TYPE_SLIDE_UP); + gtk_widget_set_valign(GTK_WIDGET(priv->overlay_ftb_overlay), GTK_ALIGN_END); + } else { + gtk_box_pack_start(GTK_BOX(vbox), revealer, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(vbox), handle, FALSE, FALSE, 0); + gtk_revealer_set_transition_type(GTK_REVEALER(revealer), GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN); + gtk_widget_set_valign(GTK_WIDGET(priv->overlay_ftb_overlay), GTK_ALIGN_START); + } + + + gtk_container_add(GTK_CONTAINER(revealer), priv->floating_toolbar_widget); + gtk_widget_set_halign(GTK_WIDGET(revealer), GTK_ALIGN_CENTER); + gtk_widget_set_valign(GTK_WIDGET(revealer), GTK_ALIGN_START); + + priv->revealer = revealer; + + GtkWidget *fr; + + fr = gtk_frame_new(NULL); + priv->overlay_ftb_fr = fr; + gtk_container_add(GTK_CONTAINER(priv->overlay_ftb_overlay), fr); + gtk_container_add(GTK_CONTAINER(fr), vbox); + + gtk_widget_show(vbox); + gtk_widget_show(revealer); + gtk_widget_show(handle); + gtk_widget_show(priv->overlay_ftb_overlay); + gtk_widget_show(fr); + + if (remmina_pref.floating_toolbar_placement == FLOATING_TOOLBAR_PLACEMENT_BOTTOM) + gtk_widget_set_name(fr, "ftbbox-lower"); + else + gtk_widget_set_name(fr, "ftbbox-upper"); + + gtk_overlay_add_overlay(GTK_OVERLAY(priv->overlay), priv->overlay_ftb_overlay); + + rcw_floating_toolbar_show(cnnwin, TRUE); + + g_signal_connect(G_OBJECT(priv->overlay_ftb_overlay), "enter-notify-event", G_CALLBACK(rcw_floating_toolbar_on_enter), cnnwin); + g_signal_connect(G_OBJECT(priv->overlay_ftb_overlay), "leave-notify-event", G_CALLBACK(rcw_floating_toolbar_on_leave), cnnwin); + g_signal_connect(G_OBJECT(priv->overlay_ftb_overlay), "scroll-event", G_CALLBACK(rcw_floating_toolbar_on_scroll), cnnwin); + gtk_widget_add_events( + GTK_WIDGET(priv->overlay_ftb_overlay), + GDK_SCROLL_MASK +#if GTK_CHECK_VERSION(3, 4, 0) + | GDK_SMOOTH_SCROLL_MASK +#endif + ); + + /* Add drag and drop capabilities to the source */ + gtk_drag_source_set(GTK_WIDGET(priv->overlay_ftb_overlay), GDK_BUTTON1_MASK, + dnd_targets_ftb, sizeof dnd_targets_ftb / sizeof *dnd_targets_ftb, GDK_ACTION_MOVE); + g_signal_connect_after(GTK_WIDGET(priv->overlay_ftb_overlay), "drag-begin", G_CALLBACK(rcw_ftb_drag_begin), cnnwin); + + if (remmina_pref.fullscreen_toolbar_visibility == FLOATING_TOOLBAR_VISIBILITY_DISABLE) + /* toolbar in fullscreenmode disabled, hide everything */ + gtk_widget_hide(fr); +} + + +static gboolean rcw_ftb_drag_drop(GtkWidget *widget, GdkDragContext *context, + gint x, gint y, guint time, RemminaConnectionWindow *cnnwin) +{ + TRACE_CALL(__func__); + GtkAllocation wa; + gint new_floating_toolbar_placement; + RemminaConnectionObject *cnnobj; + + gtk_widget_get_allocation(widget, &wa); + + if (y >= wa.height / 2) + new_floating_toolbar_placement = FLOATING_TOOLBAR_PLACEMENT_BOTTOM; + else + new_floating_toolbar_placement = FLOATING_TOOLBAR_PLACEMENT_TOP; + + gtk_drag_finish(context, TRUE, TRUE, time); + + if (new_floating_toolbar_placement != remmina_pref.floating_toolbar_placement) { + /* Destroy and recreate the FTB */ + remmina_pref.floating_toolbar_placement = new_floating_toolbar_placement; + remmina_pref_save(); + rcw_create_overlay_ftb_overlay(cnnwin); + cnnobj = rcw_get_visible_cnnobj(cnnwin); + if (cnnobj) rco_update_toolbar(cnnobj); + } + + return TRUE; +} + +static void rcw_ftb_drag_begin(GtkWidget *widget, GdkDragContext *context, gpointer user_data) +{ + TRACE_CALL(__func__); + + cairo_surface_t *surface; + cairo_t *cr; + GtkAllocation wa; + double dashes[] = { 10 }; + + gtk_widget_get_allocation(widget, &wa); + + surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, wa.width, wa.height); + cr = cairo_create(surface); + cairo_set_source_rgb(cr, 0.6, 0.6, 0.6); + cairo_set_line_width(cr, 2); + cairo_set_dash(cr, dashes, 1, 0); + cairo_rectangle(cr, 0, 0, wa.width, wa.height); + cairo_stroke(cr); + cairo_destroy(cr); + + gtk_drag_set_icon_surface(context, surface); +} + +RemminaConnectionWindow *rcw_create_fullscreen(GtkWindow *old, gint view_mode) +{ + TRACE_CALL(__func__); + RemminaConnectionWindow *cnnwin; + GtkNotebook *notebook; + +#if GTK_CHECK_VERSION(3, 22, 0) + gint n_monitors; + gint i; + GdkMonitor *old_monitor; + GdkDisplay *old_display; + GdkWindow *old_window; +#endif + gint full_screen_target_monitor; + + full_screen_target_monitor = FULL_SCREEN_TARGET_MONITOR_UNDEFINED; + if (old) { +#if GTK_CHECK_VERSION(3, 22, 0) + old_window = gtk_widget_get_window(GTK_WIDGET(old)); + old_display = gdk_window_get_display(old_window); + old_monitor = gdk_display_get_monitor_at_window(old_display, old_window); + n_monitors = gdk_display_get_n_monitors(old_display); + for (i = 0; i < n_monitors; ++i) { + if (gdk_display_get_monitor(old_display, i) == old_monitor) { + full_screen_target_monitor = i; + break; + } + } +#else + full_screen_target_monitor = gdk_screen_get_monitor_at_window( + gdk_screen_get_default(), + gtk_widget_get_window(GTK_WIDGET(old))); +#endif + } + + cnnwin = rcw_new(TRUE, full_screen_target_monitor); + gtk_widget_set_name(GTK_WIDGET(cnnwin), "remmina-connection-window-fullscreen"); + gtk_widget_realize(GTK_WIDGET(cnnwin)); + + if (!view_mode) + view_mode = VIEWPORT_FULLSCREEN_MODE; + + notebook = rcw_create_notebook(cnnwin); + + cnnwin->priv->overlay = gtk_overlay_new(); + gtk_container_add(GTK_CONTAINER(cnnwin), cnnwin->priv->overlay); + gtk_container_add(GTK_CONTAINER(cnnwin->priv->overlay), GTK_WIDGET(notebook)); + gtk_widget_show(GTK_WIDGET(cnnwin->priv->overlay)); + + cnnwin->priv->notebook = notebook; + cnnwin->priv->view_mode = view_mode; + cnnwin->priv->fss_view_mode = view_mode; + + /* Create the floating toolbar */ + rcw_create_overlay_ftb_overlay(cnnwin); + /* Add drag and drop capabilities to the drop/dest target for floating toolbar */ + gtk_drag_dest_set(GTK_WIDGET(cnnwin->priv->overlay), GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_HIGHLIGHT, + dnd_targets_ftb, sizeof dnd_targets_ftb / sizeof *dnd_targets_ftb, GDK_ACTION_MOVE); + gtk_drag_dest_set_track_motion(GTK_WIDGET(cnnwin->priv->notebook), TRUE); + g_signal_connect(GTK_WIDGET(cnnwin->priv->overlay), "drag-drop", G_CALLBACK(rcw_ftb_drag_drop), cnnwin); + + gtk_widget_show(GTK_WIDGET(cnnwin)); + GtkWindowGroup *wingrp = gtk_window_group_new(); + gtk_window_group_add_window(wingrp, GTK_WINDOW(cnnwin)); + gtk_window_set_transient_for(GTK_WINDOW(cnnwin), NULL); + + return cnnwin; +} + +static gboolean rcw_hostkey_func(RemminaProtocolWidget *gp, guint keyval, gboolean release) +{ + TRACE_CALL(__func__); + RemminaConnectionObject *cnnobj = gp->cnnobj; + RemminaConnectionWindowPriv *priv = cnnobj->cnnwin->priv; + const RemminaProtocolFeature *feature; + gint i; + + if (release) { + if (remmina_pref.hostkey && keyval == remmina_pref.hostkey) { + priv->hostkey_activated = FALSE; + if (priv->hostkey_used) + /* hostkey pressed + something else */ + return TRUE; + } + /* If hostkey is released without pressing other keys, we should execute the + * shortcut key which is the same as hostkey. Be default, this is grab/ungrab + * keyboard */ + else if (priv->hostkey_activated) { + /* Trap all key releases when hostkey is pressed */ + /* hostkey pressed + something else */ + return TRUE; + } else { + /* Any key pressed, no hostkey */ + return FALSE; + } + } else if (remmina_pref.hostkey && keyval == remmina_pref.hostkey) { + /** @todo Add callback for hostname transparent overlay #832 */ + priv->hostkey_activated = TRUE; + priv->hostkey_used = FALSE; + return TRUE; + } else if (!priv->hostkey_activated) { + /* Any key pressed, no hostkey */ + return FALSE; + } + + priv->hostkey_used = TRUE; + keyval = gdk_keyval_to_lower(keyval); + if (keyval == GDK_KEY_Up || keyval == GDK_KEY_Down + || keyval == GDK_KEY_Left || keyval == GDK_KEY_Right) { + GtkAdjustment *adjust; + int pos; + + if (cnnobj->connected && GTK_IS_SCROLLED_WINDOW(cnnobj->scrolled_container)) { + if (keyval == GDK_KEY_Up || keyval == GDK_KEY_Down) + adjust = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(cnnobj->scrolled_container)); + else + adjust = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(cnnobj->scrolled_container)); + + if (keyval == GDK_KEY_Up || keyval == GDK_KEY_Left) + pos = 0; + else + pos = gtk_adjustment_get_upper(adjust); + + gtk_adjustment_set_value(adjust, pos); + if (keyval == GDK_KEY_Up || keyval == GDK_KEY_Down) + gtk_scrolled_window_set_vadjustment(GTK_SCROLLED_WINDOW(cnnobj->scrolled_container), adjust); + else + gtk_scrolled_window_set_hadjustment(GTK_SCROLLED_WINDOW(cnnobj->scrolled_container), adjust); + } else if (REMMINA_IS_SCROLLED_VIEWPORT(cnnobj->scrolled_container)) { + RemminaScrolledViewport *gsv; + GtkWidget *child; + GdkWindow *gsvwin; + gint sz; + GtkAdjustment *adj; + gdouble value; + + if (!GTK_IS_BIN(cnnobj->scrolled_container)) + return FALSE; + + gsv = REMMINA_SCROLLED_VIEWPORT(cnnobj->scrolled_container); + child = gtk_bin_get_child(GTK_BIN(gsv)); + if (!GTK_IS_VIEWPORT(child)) + return FALSE; + + gsvwin = gtk_widget_get_window(GTK_WIDGET(gsv)); + if (!gsv) + return FALSE; + + if (keyval == GDK_KEY_Up || keyval == GDK_KEY_Down) { + sz = gdk_window_get_height(gsvwin) + 2; // Add 2px of black scroll border + adj = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(child)); + } else { + sz = gdk_window_get_width(gsvwin) + 2; // Add 2px of black scroll border + adj = gtk_scrollable_get_hadjustment(GTK_SCROLLABLE(child)); + } + + if (keyval == GDK_KEY_Up || keyval == GDK_KEY_Left) + value = 0; + else + value = gtk_adjustment_get_upper(GTK_ADJUSTMENT(adj)) - (gdouble)sz + 2.0; + + gtk_adjustment_set_value(GTK_ADJUSTMENT(adj), value); + } + } + + if (keyval == remmina_pref.shortcutkey_fullscreen && !extrahardening) { + switch (priv->view_mode) { + case SCROLLED_WINDOW_MODE: + rcw_switch_viewmode(cnnobj->cnnwin, priv->fss_view_mode); + break; + case SCROLLED_FULLSCREEN_MODE: + case VIEWPORT_FULLSCREEN_MODE: + rcw_switch_viewmode(cnnobj->cnnwin, SCROLLED_WINDOW_MODE); + break; + default: + break; + } + } else if (keyval == remmina_pref.shortcutkey_autofit && !extrahardening) { + if (priv->toolitem_autofit && gtk_widget_is_sensitive(GTK_WIDGET(priv->toolitem_autofit))) + rcw_toolbar_autofit(GTK_TOOL_ITEM(gp), cnnobj->cnnwin); + } else if (keyval == remmina_pref.shortcutkey_nexttab && !extrahardening) { + i = gtk_notebook_get_current_page(GTK_NOTEBOOK(priv->notebook)) + 1; + if (i >= gtk_notebook_get_n_pages(GTK_NOTEBOOK(priv->notebook))) + i = 0; + gtk_notebook_set_current_page(GTK_NOTEBOOK(priv->notebook), i); + } else if (keyval == remmina_pref.shortcutkey_prevtab && !extrahardening) { + i = gtk_notebook_get_current_page(GTK_NOTEBOOK(priv->notebook)) - 1; + if (i < 0) + i = gtk_notebook_get_n_pages(GTK_NOTEBOOK(priv->notebook)) - 1; + gtk_notebook_set_current_page(GTK_NOTEBOOK(priv->notebook), i); + } else if (keyval == remmina_pref.shortcutkey_clipboard && !extrahardening) { + if (remmina_protocol_widget_plugin_receives_keystrokes(REMMINA_PROTOCOL_WIDGET(cnnobj->proto))) { + remmina_protocol_widget_send_clipboard((RemminaProtocolWidget*)cnnobj->proto, G_OBJECT(cnnobj->proto)); + } + } else if (keyval == remmina_pref.shortcutkey_scale && !extrahardening) { + if (gtk_widget_is_sensitive(GTK_WIDGET(priv->toolitem_scale))) { + gtk_toggle_tool_button_set_active( + GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_scale), + !gtk_toggle_tool_button_get_active( + GTK_TOGGLE_TOOL_BUTTON( + priv->toolitem_scale))); + } + } else if (keyval == remmina_pref.shortcutkey_grab && !extrahardening) { + gtk_toggle_tool_button_set_active( + GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_grab), + !gtk_toggle_tool_button_get_active( + GTK_TOGGLE_TOOL_BUTTON( + priv->toolitem_grab))); + } else if (keyval == remmina_pref.shortcutkey_minimize && !extrahardening) { + rcw_toolbar_minimize(GTK_TOOL_ITEM(gp), + cnnobj->cnnwin); + } else if (keyval == remmina_pref.shortcutkey_viewonly && !extrahardening) { + remmina_file_set_int(cnnobj->remmina_file, "viewonly", + (remmina_file_get_int(cnnobj->remmina_file, "viewonly", 0) + == 0) ? 1 : 0); + } else if (keyval == remmina_pref.shortcutkey_screenshot && !extrahardening) { + rcw_toolbar_screenshot(GTK_TOOL_ITEM(gp), + cnnobj->cnnwin); + } else if (keyval == remmina_pref.shortcutkey_disconnect && !extrahardening) { + rco_disconnect_current_page(cnnobj); + } else if (keyval == remmina_pref.shortcutkey_toolbar && !extrahardening) { + if (priv->view_mode == SCROLLED_WINDOW_MODE) { + remmina_pref.hide_connection_toolbar = + !remmina_pref.hide_connection_toolbar; + rcw_set_toolbar_visibility(cnnobj->cnnwin); + } + } else { + for (feature = + remmina_protocol_widget_get_features( + REMMINA_PROTOCOL_WIDGET( + cnnobj->proto)); + feature && feature->type; + feature++) { + if (feature->type + == REMMINA_PROTOCOL_FEATURE_TYPE_TOOL + && GPOINTER_TO_UINT( + feature->opt3) + == keyval) { + remmina_protocol_widget_call_feature_by_ref( + REMMINA_PROTOCOL_WIDGET( + cnnobj->proto), + feature); + break; + } + } + } + /* If a keypress makes the current cnnobj to move to another window, + * priv is now invalid. So we can no longer use priv here */ + cnnobj->cnnwin->priv->hostkey_activated = FALSE; + + /* Trap all keypresses when hostkey is pressed */ + return TRUE; +} + +static RemminaConnectionWindow *rcw_find(RemminaFile *remminafile) +{ + TRACE_CALL(__func__); + const gchar *tag; + + switch (remmina_pref.tab_mode) { + case REMMINA_TAB_BY_GROUP: + tag = remmina_file_get_string(remminafile, "group"); + break; + case REMMINA_TAB_BY_PROTOCOL: + tag = remmina_file_get_string(remminafile, "protocol"); + break; + case REMMINA_TAB_ALL: + tag = NULL; + break; + case REMMINA_TAB_NONE: + default: + return NULL; + } + return RCW(remmina_widget_pool_find(REMMINA_TYPE_CONNECTION_WINDOW, tag)); +} + +gboolean rcw_delayed_window_present(gpointer user_data) +{ + RemminaConnectionWindow *cnnwin = (RemminaConnectionWindow *)user_data; + + if (cnnwin) { + gtk_window_present_with_time(GTK_WINDOW(cnnwin), (guint32)(g_get_monotonic_time() / 1000)); + rcw_grab_focus(cnnwin); + } + cnnwin->priv->dwp_eventsourceid = 0; + return G_SOURCE_REMOVE; +} + +void rco_on_connect(RemminaProtocolWidget *gp, RemminaConnectionObject *cnnobj) +{ + TRACE_CALL(__func__); + + REMMINA_DEBUG("Connect signal emitted"); + + /* This signal handler is called by a plugin when it’s correctly connected + * (and authenticated) */ + + cnnobj->connected = TRUE; + + remmina_protocol_widget_set_hostkey_func(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), + (RemminaHostkeyFunc)rcw_hostkey_func); + + /** Remember recent list for quick connect, and save the current date + * in the last_used field. + */ + if (remmina_file_get_filename(cnnobj->remmina_file) == NULL) + remmina_pref_add_recent(remmina_file_get_string(cnnobj->remmina_file, "protocol"), + remmina_file_get_string(cnnobj->remmina_file, "server")); + REMMINA_DEBUG("We save the last successful connection date"); + //remmina_file_set_string(cnnobj->remmina_file, "last_success", last_success); + remmina_file_state_last_success(cnnobj->remmina_file); + //REMMINA_DEBUG("Last connection made on %s.", last_success); + + REMMINA_DEBUG("Saving credentials"); + /* Save credentials */ + remmina_file_save(cnnobj->remmina_file); + + if (cnnobj->cnnwin->priv->floating_toolbar_widget) + gtk_widget_show(cnnobj->cnnwin->priv->floating_toolbar_widget); + + rco_update_toolbar(cnnobj); + + REMMINA_DEBUG("Trying to present the window"); + /* Try to present window */ + cnnobj->cnnwin->priv->dwp_eventsourceid = g_timeout_add(200, rcw_delayed_window_present, (gpointer)cnnobj->cnnwin); +} + +static void cb_lasterror_confirmed(void *cbdata, int btn) +{ + TRACE_CALL(__func__); + rco_closewin((RemminaProtocolWidget *)cbdata); +} + +void rco_on_disconnect(RemminaProtocolWidget *gp, gpointer data) +{ + TRACE_CALL(__func__); + RemminaConnectionObject *cnnobj = gp->cnnobj; + RemminaConnectionWindowPriv *priv = cnnobj->cnnwin->priv; + GtkWidget *pparent; + + REMMINA_DEBUG("Disconnect signal received on RemminaProtocolWidget"); + /* Detach the protocol widget from the notebook now, or we risk that a + * window delete will destroy cnnobj->proto before we complete disconnection. + */ + pparent = gtk_widget_get_parent(cnnobj->proto); + if (pparent != NULL) { + g_object_ref(cnnobj->proto); + gtk_container_remove(GTK_CONTAINER(pparent), cnnobj->proto); + } + + cnnobj->connected = FALSE; + + if (remmina_pref.save_view_mode) { + if (cnnobj->cnnwin) + remmina_file_set_int(cnnobj->remmina_file, "viewmode", cnnobj->cnnwin->priv->view_mode); + remmina_file_save(cnnobj->remmina_file); + } + + rcw_kp_ungrab(cnnobj->cnnwin); + gtk_toggle_tool_button_set_active( + GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_grab), + FALSE); + + if (remmina_protocol_widget_has_error(gp)) { + /* We cannot close window immediately, but we must show a message panel */ + RemminaMessagePanel *mp; + /* Destroy scrolled_container (and viewport) and all its children the plugin created + * on it, so they will not receive GUI signals */ + if (cnnobj->scrolled_container) { + gtk_widget_destroy(cnnobj->scrolled_container); + cnnobj->scrolled_container = NULL; + } + cnnobj->viewport = NULL; + mp = remmina_message_panel_new(); + remmina_message_panel_setup_message(mp, remmina_protocol_widget_get_error_message(gp), cb_lasterror_confirmed, gp); + rco_show_message_panel(gp->cnnobj, mp); + REMMINA_DEBUG("Could not disconnect"); + } else { + rco_closewin(gp); + REMMINA_DEBUG("Disconnected"); + } +} + +void rco_on_desktop_resize(RemminaProtocolWidget *gp, gpointer data) +{ + TRACE_CALL(__func__); + RemminaConnectionObject *cnnobj = gp->cnnobj; + + if (cnnobj && cnnobj->cnnwin && cnnobj->cnnwin->priv->view_mode != SCROLLED_WINDOW_MODE) + rco_check_resize(cnnobj); +} + +void rco_on_update_align(RemminaProtocolWidget *gp, gpointer data) +{ + TRACE_CALL(__func__); + RemminaConnectionObject *cnnobj = gp->cnnobj; + + remmina_protocol_widget_update_alignment(cnnobj); +} + +void rco_on_lock_dynres(RemminaProtocolWidget *gp, gpointer data) +{ + TRACE_CALL(__func__); + RemminaConnectionObject *cnnobj = gp->cnnobj; + + cnnobj->dynres_unlocked = FALSE; + rco_update_toolbar(cnnobj); +} + +void rco_on_unlock_dynres(RemminaProtocolWidget *gp, gpointer data) +{ + TRACE_CALL(__func__); + RemminaConnectionObject *cnnobj = gp->cnnobj; + + cnnobj->dynres_unlocked = TRUE; + rco_update_toolbar(cnnobj); +} + +gboolean rcw_open_from_filename(const gchar *filename) +{ + TRACE_CALL(__func__); + RemminaFile *remminafile; + GtkWidget *dialog; + + remminafile = remmina_file_manager_load_file(filename); + if (remminafile) { + if (remmina_file_get_int (remminafile, "profile-lock", FALSE) + && remmina_unlock_new(remmina_main_get_window()) == 0) + return FALSE; + rcw_open_from_file(remminafile); + return TRUE; + } else { + dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, + _("The file “%s” is corrupted, unreadable, or could not be found."), filename); + g_signal_connect(G_OBJECT(dialog), "response", G_CALLBACK(gtk_widget_destroy), NULL); + gtk_widget_show(dialog); + remmina_widget_pool_register(dialog); + return FALSE; + } +} + +static gboolean open_connection_last_stage(gpointer user_data) +{ + RemminaProtocolWidget *gp = (RemminaProtocolWidget *)user_data; + + /* Now we have an allocated size for our RemminaProtocolWidget. We can proceed with the connection */ + remmina_protocol_widget_update_remote_resolution(gp); + remmina_protocol_widget_open_connection(gp); + rco_check_resize(gp->cnnobj); + + return FALSE; +} + +static void rpw_size_allocated_on_connection(GtkWidget *w, GdkRectangle *allocation, gpointer user_data) +{ + RemminaProtocolWidget *gp = (RemminaProtocolWidget *)w; + + /* Disconnect signal handler to avoid to be called again after a normal resize */ + g_signal_handler_disconnect(w, gp->cnnobj->deferred_open_size_allocate_handler); + + /* Allow extra 100 ms for size allocation (do we really need it?) */ + g_timeout_add(100, open_connection_last_stage, gp); + + return; +} + +void rcw_open_from_file(RemminaFile *remminafile) +{ + TRACE_CALL(__func__); + rcw_open_from_file_full(remminafile, NULL, NULL, NULL); +} + +static void set_label_selectable(gpointer data, gpointer user_data) +{ + TRACE_CALL(__func__); + GtkWidget *widget = GTK_WIDGET(data); + + if (GTK_IS_LABEL(widget)) + gtk_label_set_selectable(GTK_LABEL(widget), TRUE); +} + +/** + * @brief These define the response id's of the + * gtksocket-is-not-available-warning-dialog buttons. + */ +enum GTKSOCKET_NOT_AVAIL_RESPONSE_TYPE { + GTKSOCKET_NOT_AVAIL_RESPONSE_OPEN_BROWSER = 0, + GTKSOCKET_NOT_AVAIL_RESPONSE_NUM +}; + +/** + * @brief Gets called if the user interacts with the + * gtksocket-is-not-available-warning-dialog + */ +static void rcw_gtksocket_not_available_dialog_response(GtkDialog * self, + gint response_id, + RemminaConnectionObject * cnnobj) +{ + TRACE_CALL(__func__); + + GError *error = NULL; + + if (response_id == GTKSOCKET_NOT_AVAIL_RESPONSE_OPEN_BROWSER) { + gtk_show_uri_on_window( + NULL, + // TRANSLATORS: This should be a link to the Remmina wiki page: + // TRANSLATORS: 'GtkSocket feature is not available'. + _("https://gitlab.com/Remmina/Remmina/-/wikis/GtkSocket-feature-is-not-available-in-a-Wayland-session"), + GDK_CURRENT_TIME, &error + ); + } + + // Close the current page since it's useless without GtkSocket. + // The user would need to manually click the close button. + if (cnnobj) rco_disconnect_current_page(cnnobj); + + gtk_widget_destroy(GTK_WIDGET(self)); +} + +GtkWidget *rcw_open_from_file_full(RemminaFile *remminafile, GCallback disconnect_cb, gpointer data, guint *handler) +{ + TRACE_CALL(__func__); + RemminaConnectionObject *cnnobj; + + gint ret; + GtkWidget *dialog; + GtkWidget *newpage; + gint width, height; + gboolean maximize; + gint view_mode; + const gchar *msg; + RemminaScaleMode scalemode; + + if (disconnect_cb) { + g_print("disconnect_cb is deprecated inside rcw_open_from_file_full() and should be null\n"); + return NULL; + } + + + /* Create the RemminaConnectionObject */ + cnnobj = g_new0(RemminaConnectionObject, 1); + cnnobj->remmina_file = remminafile; + + /* Create the RemminaProtocolWidget */ + cnnobj->proto = remmina_protocol_widget_new(); + remmina_protocol_widget_setup((RemminaProtocolWidget *)cnnobj->proto, remminafile, cnnobj); + if (remmina_protocol_widget_has_error((RemminaProtocolWidget *)cnnobj->proto)) { + GtkWindow *wparent; + wparent = remmina_main_get_window(); + msg = remmina_protocol_widget_get_error_message((RemminaProtocolWidget *)cnnobj->proto); + dialog = gtk_message_dialog_new(wparent, GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", msg); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + /* We should destroy cnnobj->proto and cnnobj now… TODO: Fix this leak */ + return NULL; + } + + + + /* Set a name for the widget, for CSS selector */ + gtk_widget_set_name(GTK_WIDGET(cnnobj->proto), "remmina-protocol-widget"); + + gtk_widget_set_halign(GTK_WIDGET(cnnobj->proto), GTK_ALIGN_FILL); + gtk_widget_set_valign(GTK_WIDGET(cnnobj->proto), GTK_ALIGN_FILL); + + if (data) + g_object_set_data(G_OBJECT(cnnobj->proto), "user-data", data); + + view_mode = remmina_file_get_int(cnnobj->remmina_file, "viewmode", 0); + if (kioskmode) + view_mode = VIEWPORT_FULLSCREEN_MODE; + gint ismultimon = remmina_file_get_int(cnnobj->remmina_file, "multimon", 0); + + if (ismultimon) + view_mode = VIEWPORT_FULLSCREEN_MODE; + + if (fullscreen) + view_mode = VIEWPORT_FULLSCREEN_MODE; + + /* Create the viewport to make the RemminaProtocolWidget scrollable */ + cnnobj->viewport = gtk_viewport_new(NULL, NULL); + gtk_widget_set_name(cnnobj->viewport, "remmina-cw-viewport"); + gtk_widget_show(cnnobj->viewport); + gtk_container_set_border_width(GTK_CONTAINER(cnnobj->viewport), 0); + gtk_viewport_set_shadow_type(GTK_VIEWPORT(cnnobj->viewport), GTK_SHADOW_NONE); + + /* Create the scrolled container */ + scalemode = get_current_allowed_scale_mode(cnnobj, NULL, NULL); + cnnobj->scrolled_container = rco_create_scrolled_container(scalemode, view_mode); + + gtk_container_add(GTK_CONTAINER(cnnobj->scrolled_container), cnnobj->viewport); + + /* Determine whether the plugin can scale or not. If the plugin can scale and we do + * not want to expand, then we add a GtkAspectFrame to maintain aspect ratio during scaling */ + cnnobj->plugin_can_scale = remmina_plugin_manager_query_feature_by_type(REMMINA_PLUGIN_TYPE_PROTOCOL, + remmina_file_get_string(remminafile, "protocol"), + REMMINA_PROTOCOL_FEATURE_TYPE_SCALE); + + cnnobj->aspectframe = NULL; + gtk_container_add(GTK_CONTAINER(cnnobj->viewport), cnnobj->proto); + + /* Determine whether this connection will be put on a new window + * or in an existing one */ + cnnobj->cnnwin = rcw_find(remminafile); + if (!cnnobj->cnnwin) { + /* Connection goes on a new toplevel window */ + switch (view_mode) { + case SCROLLED_FULLSCREEN_MODE: + case VIEWPORT_FULLSCREEN_MODE: + cnnobj->cnnwin = rcw_create_fullscreen(NULL, view_mode); + break; + case SCROLLED_WINDOW_MODE: + default: + width = remmina_file_get_int(cnnobj->remmina_file, "window_width", 640); + height = remmina_file_get_int(cnnobj->remmina_file, "window_height", 480); + maximize = remmina_file_get_int(cnnobj->remmina_file, "window_maximize", FALSE) ? TRUE : FALSE; + cnnobj->cnnwin = rcw_create_scrolled(width, height, maximize); + break; + } + rcw_update_tag(cnnobj->cnnwin, cnnobj); + rcw_append_new_page(cnnobj->cnnwin, cnnobj); + } else { + newpage = rcw_append_new_page(cnnobj->cnnwin, cnnobj); + gtk_window_present(GTK_WINDOW(cnnobj->cnnwin)); + nb_set_current_page(cnnobj->cnnwin->priv->notebook, newpage); + } + + // Do not call remmina_protocol_widget_update_alignment(cnnobj); here or cnnobj->proto will not fill its parent size + // and remmina_protocol_widget_update_remote_resolution() cannot autodetect available space + + gtk_widget_show(cnnobj->proto); + g_signal_connect(G_OBJECT(cnnobj->proto), "connect", G_CALLBACK(rco_on_connect), cnnobj); + g_signal_connect(G_OBJECT(cnnobj->proto), "disconnect", G_CALLBACK(rco_on_disconnect), NULL); + g_signal_connect(G_OBJECT(cnnobj->proto), "desktop-resize", G_CALLBACK(rco_on_desktop_resize), NULL); + g_signal_connect(G_OBJECT(cnnobj->proto), "update-align", G_CALLBACK(rco_on_update_align), NULL); + g_signal_connect(G_OBJECT(cnnobj->proto), "lock-dynres", G_CALLBACK(rco_on_lock_dynres), NULL); + g_signal_connect(G_OBJECT(cnnobj->proto), "unlock-dynres", G_CALLBACK(rco_on_unlock_dynres), NULL); + g_signal_connect(G_OBJECT(cnnobj->proto), "enter-notify-event", G_CALLBACK(rco_enter_protocol_widget), cnnobj); + g_signal_connect(G_OBJECT(cnnobj->proto), "leave-notify-event", G_CALLBACK(rco_leave_protocol_widget), cnnobj); + + if (!remmina_pref.save_view_mode) + remmina_file_set_int(cnnobj->remmina_file, "viewmode", remmina_pref.default_mode); + + + /* If it is a GtkSocket plugin and X11 is not available, we inform the + * user and close the connection */ + ret = remmina_plugin_manager_query_feature_by_type(REMMINA_PLUGIN_TYPE_PROTOCOL, + remmina_file_get_string(remminafile, "protocol"), + REMMINA_PROTOCOL_FEATURE_TYPE_GTKSOCKET); + if (ret && !remmina_gtksocket_available()) { + gchar *title = _("Warning: This plugin requires GtkSocket, but this " + "feature is unavailable in a Wayland session."); + + gchar *err_msg = + // TRANSLATORS: This should be a link to the Remmina wiki page: + // 'GtkSocket feature is not available'. + _("Plugins relying on GtkSocket can't run in a " + "Wayland session.\nFor more info and a possible " + "workaround, please visit the Remmina wiki at:\n\n" + "https://gitlab.com/Remmina/Remmina/-/wikis/GtkSocket-feature-is-not-available-in-a-Wayland-session"); + + dialog = gtk_message_dialog_new( + GTK_WINDOW(cnnobj->cnnwin), + GTK_DIALOG_MODAL, + GTK_MESSAGE_WARNING, + GTK_BUTTONS_OK, + "%s", title); + + gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), "%s", + err_msg); + gtk_dialog_add_button(GTK_DIALOG(dialog), _("Open in web browser"), + GTKSOCKET_NOT_AVAIL_RESPONSE_OPEN_BROWSER); + + REMMINA_CRITICAL(g_strdup_printf("%s\n%s", title, err_msg)); + + g_signal_connect(G_OBJECT(dialog), + "response", + G_CALLBACK(rcw_gtksocket_not_available_dialog_response), + cnnobj); + + // Make Text selectable. Usefull because of the link in the text. + GtkWidget *area = gtk_message_dialog_get_message_area( + GTK_MESSAGE_DIALOG(dialog)); + GtkContainer *box = (GtkContainer *)area; + + GList *children = gtk_container_get_children(box); + g_list_foreach(children, set_label_selectable, NULL); + g_list_free(children); + + gtk_widget_show(dialog); + + return NULL; /* Should we destroy something before returning? */ + } + + if (cnnobj->cnnwin->priv->floating_toolbar_widget) + gtk_widget_show(cnnobj->cnnwin->priv->floating_toolbar_widget); + + if (remmina_protocol_widget_has_error((RemminaProtocolWidget *)cnnobj->proto)) { + printf("OK, an error occurred in initializing the protocol plugin before connecting. The error is %s.\n" + "TODO: Put this string as an error to show", remmina_protocol_widget_get_error_message((RemminaProtocolWidget *)cnnobj->proto)); + return cnnobj->proto; + } + + + /* GTK window setup is done here, and we are almost ready to call remmina_protocol_widget_open_connection(). + * But size has not yet been allocated by GTK + * to the widgets. If we are in RES_USE_INITIAL_WINDOW_SIZE resolution mode or scale is REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES, + * we should wait for a size allocation from GTK for cnnobj->proto + * before connecting */ + + cnnobj->deferred_open_size_allocate_handler = g_signal_connect(G_OBJECT(cnnobj->proto), "size-allocate", G_CALLBACK(rpw_size_allocated_on_connection), NULL); + + return cnnobj->proto; +} + +GtkWindow *rcw_get_gtkwindow(RemminaConnectionObject *cnnobj) +{ + return &cnnobj->cnnwin->window; +} +GtkWidget *rcw_get_gtkviewport(RemminaConnectionObject *cnnobj) +{ + return cnnobj->viewport; +} + +void rcw_set_delete_confirm_mode(RemminaConnectionWindow *cnnwin, RemminaConnectionWindowOnDeleteConfirmMode mode) +{ + TRACE_CALL(__func__); + cnnwin->priv->on_delete_confirm_mode = mode; +} + +/** + * Deletes a RemminaMessagePanel from the current cnnobj + * and if it was visible, make visible the last remaining one. + */ +void rco_destroy_message_panel(RemminaConnectionObject *cnnobj, RemminaMessagePanel *mp) +{ + TRACE_CALL(__func__); + GList *childs, *cc; + RemminaMessagePanel *lastPanel; + gboolean was_visible; + GtkWidget *page; + + page = nb_find_page_by_cnnobj(cnnobj->cnnwin->priv->notebook, cnnobj); + childs = gtk_container_get_children(GTK_CONTAINER(page)); + cc = g_list_first(childs); + while (cc != NULL) { + if ((RemminaMessagePanel *)cc->data == mp) + break; + cc = g_list_next(cc); + } + g_list_free(childs); + + if (cc == NULL) { + printf("Remmina: Warning. There was a request to destroy a RemminaMessagePanel that is not on the page\n"); + return; + } + was_visible = gtk_widget_is_visible(GTK_WIDGET(mp)); + gtk_widget_destroy(GTK_WIDGET(mp)); + + /* And now, show the last remaining message panel, if needed */ + if (was_visible) { + childs = gtk_container_get_children(GTK_CONTAINER(page)); + cc = g_list_first(childs); + lastPanel = NULL; + while (cc != NULL) { + if (G_TYPE_CHECK_INSTANCE_TYPE(cc->data, REMMINA_TYPE_MESSAGE_PANEL)) + lastPanel = (RemminaMessagePanel *)cc->data; + cc = g_list_next(cc); + } + g_list_free(childs); + if (lastPanel) + gtk_widget_show(GTK_WIDGET(lastPanel)); + } +} + +/** + * Each cnnobj->page can have more than one RemminaMessagePanel, but 0 or 1 are visible. + * + * This function adds a RemminaMessagePanel to cnnobj->page, move it to top, + * and makes it the only visible one. + */ +void rco_show_message_panel(RemminaConnectionObject *cnnobj, RemminaMessagePanel *mp) +{ + TRACE_CALL(__func__); + GList *childs, *cc; + GtkWidget *page; + + /* Hides all RemminaMessagePanels childs of cnnobj->page */ + page = nb_find_page_by_cnnobj(cnnobj->cnnwin->priv->notebook, cnnobj); + childs = gtk_container_get_children(GTK_CONTAINER(page)); + cc = g_list_first(childs); + while (cc != NULL) { + if (G_TYPE_CHECK_INSTANCE_TYPE(cc->data, REMMINA_TYPE_MESSAGE_PANEL)) + gtk_widget_hide(GTK_WIDGET(cc->data)); + cc = g_list_next(cc); + } + g_list_free(childs); + + /* Add the new message panel at the top of cnnobj->page */ + gtk_box_pack_start(GTK_BOX(page), GTK_WIDGET(mp), FALSE, FALSE, 0); + gtk_box_reorder_child(GTK_BOX(page), GTK_WIDGET(mp), 0); + + /* Show the message panel */ + gtk_widget_show_all(GTK_WIDGET(mp)); + + /* Focus the correct field of the RemminaMessagePanel */ + remmina_message_panel_focus_auth_entry(mp); +} |