/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2020 Ian McInerney * Copyright (C) 2020-2023 KiCad Developers, see AUTHORS.txt for contributors. * * 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 3 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, see . */ #include #include #include #include #include #include #include #include #ifdef GDK_WINDOWING_X11 #include #endif #ifdef GDK_WINDOWING_WAYLAND #include #endif #ifdef KICAD_WAYLAND #include "wayland-pointer-constraints-unstable-v1.h" #endif // Set WXTRACE=KICAD_WAYLAND to see logs const wxString traceWayland = wxS( "KICAD_WAYLAND" ); bool KIPLATFORM::UI::IsDarkTheme() { wxColour bg = wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ); // Weighted W3C formula double brightness = ( bg.Red() / 255.0 ) * 0.299 + ( bg.Green() / 255.0 ) * 0.587 + ( bg.Blue() / 255.0 ) * 0.117; return brightness < 0.5; } wxColour KIPLATFORM::UI::GetDialogBGColour() { return wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ); } void KIPLATFORM::UI::ForceFocus( wxWindow* aWindow ) { aWindow->SetFocus(); } bool KIPLATFORM::UI::IsWindowActive( wxWindow* aWindow ) { if( !aWindow ) return false; GtkWindow* window = GTK_WINDOW( aWindow->GetHandle() ); if( window ) return gtk_window_is_active( window ); // We shouldn't really ever reach this point return false; } void KIPLATFORM::UI::ReparentQuasiModal( wxNonOwnedWindow* aWindow ) { // Not needed on this platform } void KIPLATFORM::UI::FixupCancelButtonCmdKeyCollision( wxWindow *aWindow ) { // Not needed on this platform } bool KIPLATFORM::UI::IsStockCursorOk( wxStockCursor aCursor ) { switch( aCursor ) { case wxCURSOR_BULLSEYE: case wxCURSOR_HAND: case wxCURSOR_ARROW: case wxCURSOR_BLANK: return true; default: return false; } } /** * The following two functions are based on the "hack" contained in the attached patch at * https://gitlab.gnome.org/GNOME/gtk/-/issues/1910 which is supposed to speed up creation of * GTK choice boxes. * * The basic idea is to disable some of the event handlers on the menus for the choice box to * prevent them from running, which will speed up the creation of the choice box and its popup menu. */ static void disable_area_apply_attributes_cb( GtkWidget* pItem, gpointer userdata ) { // GTK needs this enormous chain to get the actual type of item that we want GtkMenuItem* pMenuItem = GTK_MENU_ITEM( pItem ); GtkWidget* child = gtk_bin_get_child( GTK_BIN( pMenuItem ) ); GtkCellView* pCellView = GTK_CELL_VIEW( child ); GtkCellLayout* pCellLayout = GTK_CELL_LAYOUT( pCellView ); GtkCellArea* pCellArea = gtk_cell_layout_get_area( pCellLayout ); g_signal_handlers_block_matched( pCellArea, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, userdata ); } void KIPLATFORM::UI::LargeChoiceBoxHack( wxChoice* aChoice ) { AtkObject* atkObj = gtk_combo_box_get_popup_accessible( GTK_COMBO_BOX( aChoice->m_widget ) ); if( !atkObj || !GTK_IS_ACCESSIBLE( atkObj ) ) return; GtkWidget* widget = gtk_accessible_get_widget( GTK_ACCESSIBLE( atkObj ) ); if( !widget || !GTK_IS_MENU( widget ) ) return; GtkMenu* menu = GTK_MENU( widget ); gtk_container_foreach( GTK_CONTAINER( menu ), disable_area_apply_attributes_cb, menu ); } void KIPLATFORM::UI::EllipsizeChoiceBox( wxChoice* aChoice ) { // This function is based on the code inside the function post_process_ui in // gtkfilechooserwidget.c GList* cells = gtk_cell_layout_get_cells( GTK_CELL_LAYOUT( aChoice->m_widget ) ); if( !cells ) return; GtkCellRenderer* cell = (GtkCellRenderer*) cells->data; if( !cell ) return; g_object_set( G_OBJECT( cell ), "ellipsize", PANGO_ELLIPSIZE_END, nullptr ); // Only the list of cells must be freed, the renderer isn't ours to free g_list_free( cells ); } double KIPLATFORM::UI::GetPixelScaleFactor( const wxWindow* aWindow ) { double val = 1.0; GtkWidget* widget = static_cast( aWindow->GetHandle() ); if( widget && gtk_check_version( 3, 10, 0 ) == nullptr ) val = gtk_widget_get_scale_factor( widget ); return val; } double KIPLATFORM::UI::GetContentScaleFactor( const wxWindow* aWindow ) { // TODO: Do we need something different here? return GetPixelScaleFactor( aWindow ); } wxSize KIPLATFORM::UI::GetUnobscuredSize( const wxWindow* aWindow ) { return wxSize( aWindow->GetSize().GetX() - wxSystemSettings::GetMetric( wxSYS_VSCROLL_X ), aWindow->GetSize().GetY() - wxSystemSettings::GetMetric( wxSYS_HSCROLL_Y ) ); } void KIPLATFORM::UI::SetOverlayScrolling( const wxWindow* aWindow, bool overlay ) { gtk_scrolled_window_set_overlay_scrolling( GTK_SCROLLED_WINDOW( aWindow->GetHandle() ), overlay ); } bool KIPLATFORM::UI::AllowIconsInMenus() { gboolean allowed = 1; g_object_get( gtk_settings_get_default(), "gtk-menu-images", &allowed, NULL ); return !!allowed; } #if defined( GDK_WINDOWING_WAYLAND ) && defined( KICAD_WAYLAND ) static bool wayland_warp_pointer( GtkWidget* aWidget, GdkDisplay* aDisplay, GdkWindow* aWindow, GdkDevice* aPtrDev, int aX, int aY ); // GDK doesn't know if we've moved the cursor using Wayland pointer constraints. // So emulate the actual position here static wxPoint s_warped_from; static wxPoint s_warped_to; wxPoint KIPLATFORM::UI::GetMousePosition() { wxPoint wx_pos = wxGetMousePosition(); if( wx_pos == s_warped_from ) { wxLogTrace( traceWayland, wxS( "Faked mouse pos %d %d -> %d %d" ), wx_pos.x, wx_pos.y, s_warped_to.x, s_warped_to.y ); return s_warped_to; } else { // Mouse has moved s_warped_from = wxPoint(); s_warped_to = wxPoint(); } return wx_pos; } #endif void KIPLATFORM::UI::WarpPointer( wxWindow* aWindow, int aX, int aY ) { if( !wxGetEnv( wxT( "WAYLAND_DISPLAY" ), nullptr ) ) { aWindow->WarpPointer( aX, aY ); } else { GtkWidget* widget = static_cast( aWindow->GetHandle() ); GdkDisplay* disp = gtk_widget_get_display( widget ); GdkSeat* seat = gdk_display_get_default_seat( disp ); GdkDevice* ptrdev = gdk_seat_get_pointer( seat ); #if defined( GDK_WINDOWING_WAYLAND ) && defined( KICAD_WAYLAND ) if( GDK_IS_WAYLAND_DISPLAY( disp ) ) { wxPoint initialPos = wxGetMousePosition(); GdkWindow* win = aWindow->GTKGetDrawingWindow(); if( wayland_warp_pointer( widget, disp, win, ptrdev, aX, aY ) ) { s_warped_from = initialPos; s_warped_to = aWindow->ClientToScreen( wxPoint( aX, aY ) ); wxLogTrace( traceWayland, wxS( "Set warped from %d %d to %d %d" ), s_warped_from.x, s_warped_from.y, s_warped_to.x, s_warped_to.y ); } } #endif #ifdef GDK_WINDOWING_X11 if( GDK_IS_X11_DISPLAY( disp ) ) { GdkWindow* win = gdk_device_get_window_at_position( ptrdev, nullptr, nullptr ); GdkCursor* blank_cursor = gdk_cursor_new_for_display( disp, GDK_BLANK_CURSOR ); GdkCursor* cur_cursor = gdk_window_get_cursor( win ); if( cur_cursor ) g_object_ref( cur_cursor ); gdk_window_set_cursor( win, blank_cursor ); aWindow->WarpPointer( aX, aY ); gdk_window_set_cursor( win, cur_cursor ); } #endif } } void KIPLATFORM::UI::ImmControl( wxWindow* aWindow, bool aEnable ) { } // // **** Wayland hacks ahead **** // #if defined( GDK_WINDOWING_WAYLAND ) && defined( KICAD_WAYLAND ) static bool s_wl_initialized = false; static struct wl_compositor* s_wl_compositor = NULL; static struct zwp_pointer_constraints_v1* s_wl_pointer_constraints = NULL; static struct zwp_confined_pointer_v1* s_wl_confined_pointer = NULL; static struct wl_region* s_wl_confinement_region = NULL; static bool s_wl_locked_flag = false; static void handle_global( void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version ) { if( strcmp( interface, wl_compositor_interface.name ) == 0 ) { wxLogTrace( traceWayland, "handle_global received %s", interface ); s_wl_compositor = static_cast( wl_registry_bind( registry, name, &wl_compositor_interface, version ) ); } else if( strcmp( interface, zwp_pointer_constraints_v1_interface.name ) == 0 ) { wxLogTrace( traceWayland, "handle_global received %s", interface ); s_wl_pointer_constraints = static_cast( wl_registry_bind( registry, name, &zwp_pointer_constraints_v1_interface, version ) ); } } static const struct wl_registry_listener registry_listener = { .global = handle_global, .global_remove = NULL, }; static void confined_handler( void* data, struct zwp_confined_pointer_v1* zwp_confined_pointer_v1 ) { wxLogTrace( traceWayland, wxS( "Pointer confined" ) ); } static void unconfined_handler( void* data, struct zwp_confined_pointer_v1* zwp_confined_pointer_v1 ) { wxLogTrace( traceWayland, wxS( "Pointer unconfined" ) ); } static const struct zwp_confined_pointer_v1_listener confined_pointer_listener = { .confined = confined_handler, .unconfined = unconfined_handler }; static void locked_handler( void* data, struct zwp_locked_pointer_v1* zwp_locked_pointer_v1 ) { s_wl_locked_flag = true; wxLogTrace( traceWayland, wxS( "Pointer locked" ) ); } static void unlocked_handler( void* data, struct zwp_locked_pointer_v1* zwp_locked_pointer_v1 ) { wxLogTrace( traceWayland, wxS( "Pointer unlocked" ) ); } static const struct zwp_locked_pointer_v1_listener locked_pointer_listener = { .locked = locked_handler, .unlocked = unlocked_handler }; /** * Initializes `compositor` and `pointer_constraints` global variables. */ static void initialize_wayland( wl_display* wldisp ) { if( s_wl_initialized ) { return; } struct wl_registry* registry = wl_display_get_registry( wldisp ); wl_registry_add_listener( registry, ®istry_listener, NULL ); wl_display_roundtrip( wldisp ); s_wl_initialized = true; } struct zwp_locked_pointer_v1* s_wl_locked_pointer = NULL; static int s_after_paint_handler_id = 0; static void on_frame_clock_after_paint( GdkFrameClock* clock, GtkWidget* widget ) { if( s_wl_locked_pointer ) { zwp_locked_pointer_v1_destroy( s_wl_locked_pointer ); s_wl_locked_pointer = NULL; wxLogTrace( traceWayland, wxS( "after-paint: locked_pointer destroyed" ) ); g_signal_handler_disconnect( (gpointer) clock, s_after_paint_handler_id ); s_after_paint_handler_id = 0; // restore confinement if( s_wl_confinement_region != NULL ) { wxLogTrace( traceWayland, wxS( "after-paint: Restoring confinement" ) ); GdkDisplay* disp = gtk_widget_get_display( widget ); GdkSeat* seat = gdk_display_get_default_seat( disp ); GdkDevice* ptrdev = gdk_seat_get_pointer( seat ); GdkWindow* window = gtk_widget_get_window( widget ); wl_display* wldisp = gdk_wayland_display_get_wl_display( disp ); wl_surface* wlsurf = gdk_wayland_window_get_wl_surface( window ); wl_pointer* wlptr = gdk_wayland_device_get_wl_pointer( ptrdev ); s_wl_confined_pointer = zwp_pointer_constraints_v1_confine_pointer( s_wl_pointer_constraints, wlsurf, wlptr, s_wl_confinement_region, ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT ); wl_display_roundtrip( wldisp ); } } } static bool wayland_warp_pointer( GtkWidget* aWidget, GdkDisplay* aDisplay, GdkWindow* aWindow, GdkDevice* aPtrDev, int aX, int aY ) { wl_display* wldisp = gdk_wayland_display_get_wl_display( aDisplay ); wl_surface* wlsurf = gdk_wayland_window_get_wl_surface( aWindow ); wl_pointer* wlptr = gdk_wayland_device_get_wl_pointer( aPtrDev ); if( s_after_paint_handler_id ) { // Previous paint not done yet wxLogTrace( traceWayland, wxS( "Not warping: after-paint pending" ) ); return false; } initialize_wayland( wldisp ); if( s_wl_locked_pointer ) { // This shouldn't happen but let's be safe wxLogTrace( traceWayland, wxS( "** Destroying previous locked_pointer **" ) ); zwp_locked_pointer_v1_destroy( s_wl_locked_pointer ); wl_display_roundtrip( wldisp ); s_wl_locked_pointer = NULL; } // wl_surface_commit causes an assert on GNOME, but has to be called on KDE // before destroying the locked pointer, so wait until GDK commits the surface. GdkFrameClock* frame_clock = gdk_window_get_frame_clock( aWindow ); s_after_paint_handler_id = g_signal_connect_after( frame_clock, "after-paint", G_CALLBACK( on_frame_clock_after_paint ), aWidget ); // temporary disable confinement to allow pointer warping if( s_wl_confinement_region && s_wl_confined_pointer ) { zwp_confined_pointer_v1_destroy( s_wl_confined_pointer ); wl_display_roundtrip( wldisp ); s_wl_confined_pointer = NULL; } s_wl_locked_flag = false; s_wl_locked_pointer = zwp_pointer_constraints_v1_lock_pointer( s_wl_pointer_constraints, wlsurf, wlptr, NULL, ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT ); zwp_locked_pointer_v1_add_listener(s_wl_locked_pointer, &locked_pointer_listener, NULL); gint wx, wy; gtk_widget_translate_coordinates( aWidget, gtk_widget_get_toplevel( aWidget ), 0, 0, &wx, &wy ); zwp_locked_pointer_v1_set_cursor_position_hint( s_wl_locked_pointer, wl_fixed_from_int( aX + wx ), wl_fixed_from_int( aY + wy ) ); // Don't call wl_surface_commit, wait for GDK because of an assert on GNOME. wl_display_roundtrip( wldisp ); // To receive "locked" event return s_wl_locked_flag; } void KIPLATFORM::UI::InfiniteDragPrepareWindow( wxWindow* aWindow ) { wxLogTrace( traceWayland, wxS( "InfiniteDragPrepareWindow" ) ); if( s_wl_confined_pointer != NULL ) { KIPLATFORM::UI::InfiniteDragReleaseWindow(); } GtkWidget* widget = static_cast( aWindow->GetHandle() ); GdkDisplay* disp = gtk_widget_get_display( widget ); if( !GDK_IS_WAYLAND_DISPLAY( disp ) ) { return; } GdkSeat* seat = gdk_display_get_default_seat( disp ); GdkDevice* ptrdev = gdk_seat_get_pointer( seat ); GdkWindow* win = aWindow->GTKGetDrawingWindow(); wl_display* wldisp = gdk_wayland_display_get_wl_display( disp ); wl_surface* wlsurf = gdk_wayland_window_get_wl_surface( win ); wl_pointer* wlptr = gdk_wayland_device_get_wl_pointer( ptrdev ); initialize_wayland( wldisp ); gint x, y, width, height; gdk_window_get_geometry( gdk_window_get_toplevel( win ), &x, &y, &width, &height ); wxLogTrace( traceWayland, wxS( "Confine region: %d %d %d %d" ), x, y, width, height ); s_wl_confinement_region = wl_compositor_create_region( s_wl_compositor ); wl_region_add( s_wl_confinement_region, x, y, width, height ); s_wl_confined_pointer = zwp_pointer_constraints_v1_confine_pointer( s_wl_pointer_constraints, wlsurf, wlptr, s_wl_confinement_region, ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT ); zwp_confined_pointer_v1_add_listener(s_wl_confined_pointer, &confined_pointer_listener, NULL); wl_display_roundtrip( wldisp ); }; void KIPLATFORM::UI::InfiniteDragReleaseWindow() { wxLogTrace( traceWayland, wxS( "InfiniteDragReleaseWindow" ) ); if( s_wl_confined_pointer ) { zwp_confined_pointer_v1_destroy( s_wl_confined_pointer ); s_wl_confined_pointer = NULL; } if( s_wl_confinement_region ) { wl_region_destroy( s_wl_confinement_region ); s_wl_confinement_region = NULL; } }; #else // No Wayland support void KIPLATFORM::UI::InfiniteDragPrepareWindow( wxWindow* aWindow ) { // Not needed on X11 }; void KIPLATFORM::UI::InfiniteDragReleaseWindow() { // Not needed on X11 }; wxPoint KIPLATFORM::UI::GetMousePosition() { return wxGetMousePosition(); } #endif