From d50b4cbf3d05e3d3fe7d6f078a9719f90ca01e5f Mon Sep 17 00:00:00 2001 From: SoftFever Date: Wed, 4 Mar 2026 22:29:27 +0800 Subject: [PATCH] Improves usability on Linux. No more double title bar (#12600) * Fix double title bar on Linux by removing WM decorations for GTK * wip * fix * finish * address review comment --- src/slic3r/GUI/BBLTopbar.cpp | 34 +++++++- src/slic3r/GUI/MainFrame.cpp | 156 +++++++++++++++++++++++++++++++++++ src/slic3r/GUI/MainFrame.hpp | 5 ++ task.md | 12 --- 4 files changed, 191 insertions(+), 16 deletions(-) delete mode 100644 task.md diff --git a/src/slic3r/GUI/BBLTopbar.cpp b/src/slic3r/GUI/BBLTopbar.cpp index 151f601e2c..4ac9a7aa29 100644 --- a/src/slic3r/GUI/BBLTopbar.cpp +++ b/src/slic3r/GUI/BBLTopbar.cpp @@ -13,6 +13,10 @@ #include +#ifdef __WXGTK__ +#include +#endif + #define TOPBAR_ICON_SIZE 18 #define TOPBAR_TITLE_WIDTH 300 @@ -532,6 +536,18 @@ void BBLTopbar::OnIconize(wxAuiToolBarEvent& event) void BBLTopbar::OnFullScreen(wxAuiToolBarEvent& event) { +#ifdef __WXGTK__ + GtkWindow* gtk_window = GTK_WINDOW(m_frame->m_widget); + if (gtk_window_is_maximized(gtk_window)) { + gtk_window_unmaximize(gtk_window); + } + else { + m_normalRect = m_frame->GetRect(); + gtk_window_maximize(gtk_window); + } + return; +#endif + if (m_frame->IsMaximized()) { m_frame->Restore(); } @@ -621,17 +637,27 @@ void BBLTopbar::OnMouseLeftDown(wxMouseEvent& event) wxPoint frame_pos = m_frame->GetScreenPosition(); m_delta = mouse_pos - frame_pos; - if (FindToolByCurrentPosition() == NULL + if (FindToolByCurrentPosition() == NULL || this->FindToolByCurrentPosition() == m_title_item) { - CaptureMouse(); #ifdef __WXMSW__ + CaptureMouse(); ReleaseMouse(); ::PostMessage((HWND) m_frame->GetHandle(), WM_NCLBUTTONDOWN, HTCAPTION, MAKELPARAM(mouse_pos.x, mouse_pos.y)); return; -#endif // __WXMSW__ +#elif defined(__WXGTK__) + // Use WM-integrated drag for smoother window movement on Linux. + gtk_window_begin_move_drag( + GTK_WINDOW(m_frame->m_widget), + 1, // left mouse button + mouse_pos.x, mouse_pos.y, + gtk_get_current_event_time()); + return; +#else + CaptureMouse(); +#endif } - + event.Skip(); } diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index c279d0ebb9..d7c0da0127 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -71,6 +71,10 @@ #include #include #endif // _WIN32 + +#ifdef __WXGTK__ +#include +#endif // __WXGTK__ #include @@ -100,6 +104,142 @@ enum class ERescaleTarget SettingsDialog }; +#ifdef __WXGTK__ +// Intercepts mouse events globally to provide resize handles for the +// borderless (CSD) window. When the cursor is within BORDER_PX of a +// window edge, a resize cursor is shown and left-click initiates a +// WM-integrated resize via gtk_window_begin_resize_drag(). +class MainFrame::GtkResizeBorderHandler : public wxEventFilter +{ +public: + static constexpr int BORDER_PX = 8; + + explicit GtkResizeBorderHandler(MainFrame* frame) + : m_frame(frame) + { + wxEvtHandler::AddFilter(this); + } + + ~GtkResizeBorderHandler() override + { + wxEvtHandler::RemoveFilter(this); + } + + int FilterEvent(wxEvent& event) override + { + const wxEventType t = event.GetEventType(); + if (t != wxEVT_MOTION && t != wxEVT_LEFT_DOWN) + return Event_Skip; + + if (!m_frame || !m_frame->IsShown() || m_frame->IsMaximized() || m_frame->IsFullScreen()) + return Event_Skip; + + if (!gtk_widget_get_realized(m_frame->m_widget) || gtk_widget_get_window(m_frame->m_widget) == nullptr) + return Event_Skip; + + wxPoint mouse = ::wxGetMousePosition(); + wxRect rect = m_frame->GetScreenRect(); + + // Don't steal interactions from the custom top bar area. + // Keep corner resizing available by only excluding the pure top edge. + if (m_frame->topbar() != nullptr) { + const wxRect topbar_rect = m_frame->topbar()->GetScreenRect(); + if (topbar_rect.Contains(mouse)) { + const bool near_left_corner = mouse.x < rect.x + BORDER_PX; + const bool near_right_corner = mouse.x > rect.x + rect.width - BORDER_PX; + if (!near_left_corner && !near_right_corner) + return Event_Skip; + } + } + + GdkWindowEdge edge; + if (!hit_test(mouse, rect, edge)) { + // Cursor is not near any edge — restore default cursor if we changed it. + if (m_cursor_set) { + gdk_window_set_cursor(gtk_widget_get_window(m_frame->m_widget), NULL); + m_cursor_set = false; + m_last_edge_valid = false; + } + return Event_Skip; + } + + // Set the appropriate resize cursor. + set_cursor_for_edge(edge); + + if (t == wxEVT_LEFT_DOWN) { + gtk_window_begin_resize_drag( + GTK_WINDOW(m_frame->m_widget), + edge, + 1, // left mouse button + mouse.x, mouse.y, + gtk_get_current_event_time()); + return Event_Processed; + } + + // For motion, keep app interaction working (menus, hover, etc.). + return Event_Skip; + } + +private: + bool hit_test(const wxPoint& mouse, const wxRect& rect, GdkWindowEdge& edge) const + { + bool left = mouse.x >= rect.x && mouse.x < rect.x + BORDER_PX; + bool right = mouse.x > rect.x + rect.width - BORDER_PX && mouse.x <= rect.x + rect.width; + bool top = mouse.y >= rect.y && mouse.y < rect.y + BORDER_PX; + bool bottom = mouse.y > rect.y + rect.height - BORDER_PX && mouse.y <= rect.y + rect.height; + + if (!left && !right && !top && !bottom) + return false; + + if (top && left) edge = GDK_WINDOW_EDGE_NORTH_WEST; + else if (top && right) edge = GDK_WINDOW_EDGE_NORTH_EAST; + else if (bottom && left) edge = GDK_WINDOW_EDGE_SOUTH_WEST; + else if (bottom && right) edge = GDK_WINDOW_EDGE_SOUTH_EAST; + else if (top) edge = GDK_WINDOW_EDGE_NORTH; + else if (bottom) edge = GDK_WINDOW_EDGE_SOUTH; + else if (left) edge = GDK_WINDOW_EDGE_WEST; + else edge = GDK_WINDOW_EDGE_EAST; + return true; + } + + void set_cursor_for_edge(GdkWindowEdge edge) + { + if (m_last_edge_valid && m_cursor_set && edge == m_last_edge) + return; + + const char* cursor_name = nullptr; + switch (edge) { + case GDK_WINDOW_EDGE_NORTH: cursor_name = "n-resize"; break; + case GDK_WINDOW_EDGE_SOUTH: cursor_name = "s-resize"; break; + case GDK_WINDOW_EDGE_WEST: cursor_name = "w-resize"; break; + case GDK_WINDOW_EDGE_EAST: cursor_name = "e-resize"; break; + case GDK_WINDOW_EDGE_NORTH_WEST: cursor_name = "nw-resize"; break; + case GDK_WINDOW_EDGE_NORTH_EAST: cursor_name = "ne-resize"; break; + case GDK_WINDOW_EDGE_SOUTH_WEST: cursor_name = "sw-resize"; break; + case GDK_WINDOW_EDGE_SOUTH_EAST: cursor_name = "se-resize"; break; + default: return; + } + + GdkDisplay* display = gtk_widget_get_display(m_frame->m_widget); + GdkCursor* cursor = gdk_cursor_new_from_name(display, cursor_name); + if (!cursor) + return; + + gdk_window_set_cursor(gtk_widget_get_window(m_frame->m_widget), cursor); + g_object_unref(cursor); + + m_cursor_set = true; + m_last_edge = edge; + m_last_edge_valid = true; + } + + MainFrame* m_frame; + bool m_cursor_set{false}; + GdkWindowEdge m_last_edge{}; + bool m_last_edge_valid{false}; +}; +#endif // __WXGTK__ + #ifdef __APPLE__ class OrcaSlicerTaskBarIcon : public wxTaskBarIcon { @@ -191,6 +331,18 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ set_miniaturizable(GetHandle()); #endif +#ifdef __WXGTK__ + // Zero out the decoration hints that wxGTK computed from the window style. + // When GTKHandleRealized() later calls gdk_window_set_decorations(), it + // will apply zero decorations (no WM title bar). + m_gdkDecor = 0; + + // Install app-level resize border handler as a fallback. + // This keeps resize behavior for undecorated windows without using GTK CSD, + // avoiding CSD repaint/maximize artifacts on some WMs. + m_resize_border_handler = new GtkResizeBorderHandler(this); +#endif + if (!wxGetApp().app_config->has("user_mode")) { wxGetApp().app_config->set("user_mode", "simple"); wxGetApp().app_config->set_bool("developer_mode", false); @@ -937,6 +1089,10 @@ void MainFrame::update_layout() void MainFrame::shutdown() { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "MainFrame::shutdown enter"; +#ifdef __WXGTK__ + delete m_resize_border_handler; + m_resize_border_handler = nullptr; +#endif // BBS: backup Slic3r::set_backup_callback(nullptr); #ifdef _WIN32 diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index faa6e10708..8fa5454cae 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -422,6 +422,11 @@ public: uint32_t m_ulSHChangeNotifyRegister { 0 }; static constexpr int WM_USER_MEDIACHANGED { 0x7FFF }; // WM_USER from 0x0400 to 0x7FFF, picking the last one to not interfere with wxWidgets allocation #endif // _WIN32 + +#ifdef __WXGTK__ + class GtkResizeBorderHandler; + GtkResizeBorderHandler* m_resize_border_handler{nullptr}; +#endif // __WXGTK__ }; wxDECLARE_EVENT(EVT_HTTP_ERROR, wxCommandEvent); diff --git a/task.md b/task.md deleted file mode 100644 index 10fc9ac724..0000000000 --- a/task.md +++ /dev/null @@ -1,12 +0,0 @@ -Analyze the bug that it failed to load project(3mf) from old version. -It failed pass below check in PresetBundle::load_config_file_config function, hence throw error. - if (config.option("extruder_variant_list")) { - //3mf support multiple extruder logic - size_t extruder_count = config.option("nozzle_diameter")->values.size(); - extruder_variant_count = config.option("filament_extruder_variant", true)->size(); - if ((extruder_variant_count != filament_self_indice.size()) - || (extruder_variant_count < num_filaments)) { - assert(false); - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": invalid config file %1%, can not find suitable filament_extruder_variant or filament_self_index") % name_or_path; - throw Slic3r::RuntimeError(std::string("Invalid configuration file: ") + name_or_path); - } \ No newline at end of file