Compare commits

...
Author SHA1 Message Date
ExPikaPaka 19c1d633cc UV editor: keep the mouse capture balanced
The canvas captured the mouse on every button press without checking whether
it already held one, released it in a single place, and handled no capture
loss at all. Two sequences leaked a capture: pressing a second button during a
drag nested a second one that the single release on button-up could not undo,
and a modal R/S skipped that release entirely while waiting for a confirming
click that may never come.

A leaked capture is not a local problem on macOS, where wxEVT_MOUSE_CAPTURE_LOST
is never sent and nothing recovers it. While any wx window holds a capture,
wxOSX routes every mouse event to that window and never calls through to
NSWindow, so the application stops seeing motion and enter/leave, and native
tooltips stop appearing anywhere in it.

Capture through grab_mouse()/drop_mouse() so there is at most one, give it back
on any button-up including a modal gesture (which tracks the pointer and needs
no capture), and cancel on wxEVT_MOUSE_CAPTURE_LOST: commit nothing, put back
what a modal rotate or scale already applied, and do not release a capture that
is already gone.
2026-10-08 09:04:01 +02:00
ExPikaPaka 97b0d2c879 Explain the texture displacement views that cannot be used, and show them
Checker and Distortion are views of a layer's unwrap, so they mean nothing on
any other mapping - but they were offered on all of them and simply drew
nothing when picked. Fade them out on anything but Unwrap (LSCM), with the
reason in the tooltip, and have a click bring the UV editor up on the view it
selected, since that pane is where the unwrap is actually worked on.

Neither view was visible even when it did apply. The overlay is built from the
base patch and drawn with a polygon offset, which biases depth values rather
than moving geometry, so it cannot win against the displaced preview standing
in front of it. Leave that preview out while a UV-check view is on and draw the
undisplaced surface instead, which is what the offset assumes and what the
mapping being inspected belongs to.

In the UV editor, a tool that cannot be used right now is faded rather than
disabled. A disabled window gets no mouse events on GTK or MSW, so every one of
those tools - Cut, Join, Unjoin, Clear seams, Clear UV edits, the select modes,
Snap, Frame - silently had no tooltip in the state where the user most needs to
know what is missing. Each now says what to do instead. The tile size, the
island statistics, the status line and the three select modes gained tooltips
of their own; the select modes now name the gestures they enable, which were
documented nowhere.
2026-10-05 08:35:06 +02:00
3 changed files with 199 additions and 44 deletions
@@ -489,7 +489,15 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
// volume for a shaded pass that then draws nothing is what made the model vanish - most obviously
// with zero layers, but equally with a layer that has no texture picked yet.
const bool use_shaded = m_use_shaded_preview && m_shaded_preview_glmodel.is_initialized() && shaded_preview_ready();
const bool use_true_preview = !use_shaded && m_preview_glmodel.is_initialized();
// Checker/Distortion are built from the *base* patch and drawn with a polygon offset, which biases
// depth values - it does not move the geometry. It therefore cannot win against a surface that
// genuinely stands in front, and the displaced preview does exactly that: it rises above the base
// surface by the layer's depth. Drawn underneath a UV-check overlay it simply occludes it, which is
// why those two modes looked like they did nothing. Leave it out and let the undisplaced volume show
// through instead (toggle_model_objects_visibility below) - that one *is* coincident with the
// overlay, which is what the offset assumes, and it is the surface whose mapping is being inspected.
const bool use_true_preview = !use_shaded && m_uv_check_mode == UVCheckMode::None &&
m_preview_glmodel.is_initialized();
// In Checker/Distortion mode the UV-check overlay *is* the surface visualization the user is
// looking at, so the opaque paint-selection highlight must not be drawn on top of it - same
// reasoning as skipping it for the shaded preview (see bug #12). Without this the painted area
@@ -517,7 +525,13 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
render_triangles(selection);
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
}
} else if (show_paint_overlay) {
} else {
// render_triangles() *is* the model in a painter gizmo (it draws every model-part volume with the
// selector's colours), not an overlay on top of one - so it still has to run under a UV-check
// overlay, or nothing draws the surface at all and the checker floats alone over an empty scene.
// Deliberately without the depth bias the branch above applies: the checker/heatmap is drawn later
// with its own -1 offset and has to win against this. Biasing both by the same amount is what made
// the painted area cover the checker and is why this call used to be skipped outright.
render_triangles(selection);
}
@@ -5710,14 +5724,20 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
const int cur_mode = m_use_shaded_preview ? 1 :
m_uv_check_mode == UVCheckMode::Checker ? 2 :
m_uv_check_mode == UVCheckMode::Distortion ? 3 : 0;
int new_mode = cur_mode;
bool wf_toggle = false;
const wxString distortion_na = active == nullptr ? _L("Add a layer first.") :
active->projection_method != TextureProjectionMethod::LSCM ?
_L("Needs the active layer mapped with Unwrap (LSCM).") :
wxString();
// Distortion over a layer that stopped being an unwrap shows nothing at all, so fall back to Normal.
if (cur_mode == 3 && !distortion_na.empty())
int new_mode = cur_mode;
bool wf_toggle = false;
bool open_uv_editor = false;
// Checker and Distortion both draw *the unwrap* - the first the texture grid laid over it, the second
// its stretch - so they only mean anything for a layer mapped with Unwrap (LSCM). On the default
// triplanar mapping (or cylindrical / spherical / from view) they are faded out with the reason in the
// tooltip, rather than being offered and then showing nothing.
const wxString uv_view_na = active == nullptr ? _L("Add a layer first.") :
active->projection_method != TextureProjectionMethod::LSCM ?
_L("Only for a layer mapped with Unwrap (LSCM) - set the "
"active layer's Mapping to Unwrap to use this view.") :
wxString();
// Either view over a layer that stopped being an unwrap shows nothing at all, so fall back to Normal.
if ((cur_mode == 2 || cur_mode == 3) && !uv_view_na.empty())
new_mode = 0;
const float x0 = ImGui::GetCursorPosX();
@@ -5736,12 +5756,20 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
ImGui::SameLine(0.f, gap_s);
if (icon_toggle(703, "texture_displacement_checker.svg", cur_mode == 2, icon_md, _L("Checker"),
_L("Checker - a test grid instead of the texture. Where the squares stay square the "
"texture is undistorted; where they stretch, it will too")))
new_mode = 2;
"texture is undistorted; where they stretch, it will too. Opens the UV editor if "
"it is closed"),
uv_view_na)) {
new_mode = 2;
open_uv_editor = true;
}
ImGui::SameLine(0.f, gap_s);
if (icon_toggle(704, "texture_displacement_distortion.svg", cur_mode == 3, icon_md, _L("Distortion"),
_L("Distortion - blue-to-red stretch heatmap over the unwrap"), distortion_na))
new_mode = 3;
_L("Distortion - blue-to-red stretch heatmap over the unwrap. Opens the UV editor if "
"it is closed"),
uv_view_na)) {
new_mode = 3;
open_uv_editor = true;
}
vsep(icon_md);
if (icon_toggle(705, "texture_displacement_wireframe.svg", m_wireframe_overlay, icon_md, _L("Wireframe"),
_L("Wireframe - overlay the mesh edges; independent of the view above")))
@@ -5756,6 +5784,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
hover_tip(_u8L("Rebuilds the preview as soon as anything changes. Turn it off on a heavy model if painting "
"or dragging a slider starts to stutter - the preview then waits until you let go."));
// Both are views of the unwrap, so picking one brings the UV editor up with it - including when that
// view is already the active one and only the pane is missing.
if (open_uv_editor && !m_show_uv_editor) {
m_show_uv_editor = true;
if (new_mode == cur_mode)
update_uv_editor(); // otherwise apply_view_mode() below does it
}
if (new_mode != cur_mode)
apply_view_mode(new_mode);
if (wf_toggle) {
+138 -30
View File
@@ -176,6 +176,7 @@ UVEditorCanvas::UVEditorCanvas(wxWindow *parent)
Bind(wxEVT_MIDDLE_DOWN, &UVEditorCanvas::on_mouse, this);
Bind(wxEVT_MIDDLE_UP, &UVEditorCanvas::on_mouse, this);
Bind(wxEVT_MOTION, &UVEditorCanvas::on_mouse, this);
Bind(wxEVT_MOUSE_CAPTURE_LOST, &UVEditorCanvas::on_capture_lost, this);
Bind(wxEVT_MOUSEWHEEL, &UVEditorCanvas::on_mouse, this);
Bind(wxEVT_LEAVE_WINDOW, &UVEditorCanvas::on_leave, this);
Bind(wxEVT_KEY_DOWN, &UVEditorCanvas::on_key, this);
@@ -727,10 +728,47 @@ void UVEditorCanvas::end_gesture()
m_rot_raw_deg = 0.f;
m_rot_applied_deg = 0.f;
m_modal_scale_accum = 1.f;
drop_mouse();
}
void UVEditorCanvas::cancel_gesture()
{
// Undo what the gesture already applied live, as the Esc path does, and commit nothing.
if (m_on_island_edit) {
if (m_gesture == Gesture::RotateIslandModal && m_rot_applied_deg != 0.f)
m_on_island_edit(m_selected_island, Vec2f::Zero(), -m_rot_applied_deg, 1.f, false);
if (m_gesture == Gesture::ScaleIslandModal && m_modal_scale_accum != 1.f)
m_on_island_edit(m_selected_island, Vec2f::Zero(), 0.f, 1.f / m_modal_scale_accum, false);
}
m_gesture = Gesture::None;
m_rot_raw_deg = 0.f;
m_rot_applied_deg = 0.f;
m_modal_scale_accum = 1.f;
m_vertex_edit_moved = false;
}
void UVEditorCanvas::grab_mouse()
{
if (!HasCapture())
CaptureMouse();
}
void UVEditorCanvas::drop_mouse()
{
if (HasCapture())
ReleaseMouse();
}
// The capture was taken from us (a dialog opened, another application grabbed the pointer). wx
// requires this to cancel the gesture: no commit, no Skip(), and no ReleaseMouse() - the capture is
// already gone, and releasing it again would unbalance the stack.
void UVEditorCanvas::on_capture_lost(wxMouseCaptureLostEvent &)
{
cancel_gesture();
Refresh();
}
void UVEditorCanvas::on_key(wxKeyEvent &evt)
{
const int key = evt.GetKeyCode();
@@ -909,7 +947,7 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt)
}
m_gesture = (m_selected_island >= 0) ? Gesture::MoveIsland : Gesture::Pan;
}
CaptureMouse();
grab_mouse();
Refresh();
} else if (type == wxEVT_RIGHT_DOWN && m_selected_island >= 0 && m_select_mode == SelectMode::Island) {
const Vec2f rel = screen_to_uv(pos) - island_centroid(m_selected_island);
@@ -919,12 +957,16 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt)
m_rot_base_deg = island_rotation_deg(m_selected_island);
m_rot_display_deg = m_rot_base_deg;
m_gesture_last_angle = std::atan2(rel.y(), rel.x());
CaptureMouse();
grab_mouse();
} else if (type == wxEVT_MIDDLE_DOWN) {
m_gesture = Gesture::Pan;
m_drag_last_px = pos;
CaptureMouse();
grab_mouse();
} else if (type == wxEVT_LEFT_UP || type == wxEVT_RIGHT_UP || type == wxEVT_MIDDLE_UP) {
// The drag is over either way. A modal R/S keeps running until a click confirms it, but it
// tracks the pointer over this canvas and needs no capture to do so, so the capture goes back
// here rather than waiting for that click - which may never come.
drop_mouse();
if (m_gesture != Gesture::RotateIslandModal && m_gesture != Gesture::ScaleIslandModal) {
end_gesture();
Refresh();
@@ -1712,7 +1754,7 @@ public:
bool toggle, bool accent = false, int size_dip = 26)
: wxWindow(parent, id, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE | wxFULL_REPAINT_ON_RESIZE)
, m_icon_name(icon), m_icon_dip(size_dip >= 26 ? 16 : 14), m_label(label), m_toggle(toggle), m_accent(accent)
, m_size_dip(size_dip)
, m_size_dip(size_dip), m_tip(tip)
{
SetBackgroundStyle(wxBG_STYLE_PAINT);
SetToolTip(tip);
@@ -1725,7 +1767,7 @@ public:
Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent &) { m_hover = true; Refresh(); });
Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent &) { m_hover = false; m_pressed = false; Refresh(); });
Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent &) {
if (IsEnabled()) {
if (usable()) {
m_pressed = true;
Refresh();
}
@@ -1734,7 +1776,7 @@ public:
const bool was_pressed = m_pressed;
m_pressed = false;
Refresh();
if (!was_pressed || !IsEnabled() || !GetClientRect().Contains(e.GetPosition()))
if (!was_pressed || !usable() || !GetClientRect().Contains(e.GetPosition()))
return;
if (m_toggle)
m_on = !m_on;
@@ -1774,6 +1816,30 @@ public:
Refresh();
return changed;
}
// Soft-disable: the button is drawn faded and swallows clicks, but stays a live window, so hovering it
// still raises its tooltip - now with `reason` appended, saying what to do to make it usable. A window
// really disabled with Enable(false) gets no mouse events at all on GTK and MSW, which leaves the user
// guessing; this is the same trade-off the gizmo panel's icon_toggle() makes with its `unavailable`.
// An empty reason makes the button usable again.
void SetUnavailable(const wxString &reason)
{
if (reason == m_unavailable)
return;
m_unavailable = reason;
SetToolTip(reason.empty() || m_tip.empty() ? m_tip : m_tip + "\n\n" + reason);
if (!m_unavailable.empty())
m_pressed = false; // a reason appearing mid-press cancels the press
Refresh();
}
// Replaces the plain tooltip, keeping whatever reason is currently appended to it.
void SetTip(const wxString &tip)
{
if (tip == m_tip)
return;
m_tip = tip;
SetToolTip(m_unavailable.empty() || m_tip.empty() ? m_tip : m_tip + "\n\n" + m_unavailable);
}
bool usable() const { return IsEnabled() && m_unavailable.empty(); }
protected:
wxSize DoGetBestSize() const override
@@ -1792,7 +1858,7 @@ private:
const PaneColors c = PaneColors::current();
const wxRect r = GetClientRect();
const wxColour teal(0x00, 0x96, 0x88);
const bool enabled = IsEnabled();
const bool enabled = usable();
wxColour fill = c.bg, border = c.frame, text = c.ink;
if (m_accent) {
@@ -1852,6 +1918,8 @@ private:
bool m_toggle = false;
bool m_accent = false;
int m_size_dip = 26;
wxString m_tip; // the tooltip without any m_unavailable reason appended
wxString m_unavailable; // non-empty: faded and unclickable, and why (see SetUnavailable())
bool m_on = false;
bool m_badge = false;
bool m_hover = false;
@@ -1897,6 +1965,9 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
});
m_layer_name->SetMinSize(wxSize(FromDIP(30), -1));
m_tile = text(wxEmptyString, c.dim);
m_tile->SetToolTip(_L("The active layer's tile size: how much of the model one repeat of the texture covers. The "
"canvas is measured in tiles, so one grid cell is one repeat. Change it with Tiling in the "
"layer's settings."));
header->Add(m_thumb, 0, wxALIGN_CENTER_VERTICAL);
header->Add(m_layer_name, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, gap);
header->Add(m_tile, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, gap);
@@ -1950,9 +2021,19 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
strip->Add(r, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, FromDIP(11));
strip->AddSpacer(FromDIP(7));
};
m_select[0] = tool(ID_UV_SELECT_ISLAND, "texture_displacement_uv_select_island", _L("Island - move, rotate and scale whole islands"), true);
m_select[1] = tool(ID_UV_SELECT_VERTEX, "texture_displacement_uv_select_vertex", _L("Vertex - drag vertices to reshape; Shift/Ctrl to multi-select"), true);
m_select[2] = tool(ID_UV_SELECT_EDGE, "texture_displacement_uv_select_edge", _L("Edge - drag edges to reshape; Shift/Ctrl to multi-select"), true);
m_select[0] = tool(ID_UV_SELECT_ISLAND, "texture_displacement_uv_select_island",
_L("Island - work on whole islands. Click one to select it, then drag to move it, right-drag to "
"rotate it, or press R to rotate and S to scale with the mouse (click or Enter to confirm, Esc "
"to cancel)."),
true);
m_select[1] = tool(ID_UV_SELECT_VERTEX, "texture_displacement_uv_select_vertex",
_L("Vertex - drag vertices to reshape an island by hand; Shift adds to the selection, Ctrl "
"toggles one in or out of it."),
true);
m_select[2] = tool(ID_UV_SELECT_EDGE, "texture_displacement_uv_select_edge",
_L("Edge - drag edges to reshape an island by hand; Shift adds to the selection, Ctrl toggles "
"one in or out of it."),
true);
strip_rule();
m_mark_seams = tool(ID_UV_MARK_SEAMS, "texture_displacement_uv_seam",
_L("Mark seams - click edges on the model to cut the unwrap along them. The edge under the cursor is "
@@ -1973,7 +2054,11 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
m_clear_edits = tool(ID_UV_CLEAR_EDITS, "texture_displacement_uv_clear_edits",
_L("Clear UV edits - discard all manual vertex/edge moves and return the unwrap to its automatic shape"), false);
m_snap = tool(ID_UV_SNAP, "texture_displacement_uv_snap", _L("Snap - stick islands together when dragging one against another"), true);
m_frame = tool(ID_UV_FRAME, "texture_displacement_uv_frame", _L("Frame all islands (Home)"), false);
m_frame = tool(ID_UV_FRAME, "texture_displacement_uv_frame",
_L("Frame all islands, fitting every one of them in view (Home or F).\n"
"Elsewhere on the canvas: scroll to zoom around the cursor, and middle-drag - or drag empty space - "
"to pan."),
false);
strip->AddSpacer(FromDIP(4));
m_canvas = new UVEditorCanvas(this);
@@ -1984,6 +2069,7 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
// ---- status line: the current gesture on the left, the unwrap summary on the right ----
m_status = text(wxEmptyString, c.dim, wxST_ELLIPSIZE_END);
m_status->SetToolTip(_L("What is selected, and the exact figures of the move, rotation or scale while you drag one."));
m_status->SetMinSize(wxSize(FromDIP(40), -1));
m_stats = text(wxEmptyString, c.dim);
auto *status = new wxBoxSizer(wxHORIZONTAL);
@@ -2059,19 +2145,25 @@ void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s)
} else {
m_thumb->SetBitmap(wxNullBitmap);
}
m_thumb->Enable(s.has_layer);
// Every tool that cannot be used right now is faded with the reason appended to its tooltip, rather than
// being hard-disabled (which would hide the tooltip too - see UVToolButton::SetUnavailable()).
const wxString no_layer = s.has_layer ? wxString() :
_L("The pane follows the active texture layer, and that layer has to be mapped "
"with Unwrap (LSCM). Add a layer and set its Mapping to Unwrap.");
m_thumb->SetUnavailable(no_layer);
m_layer_name->SetToolTip(s.has_layer ? m_thumb->GetToolTipText() : no_layer);
for (int i = 0; i < 3; ++i) {
m_background[i]->SetValue(int(s.background) == i);
m_background[i]->Enable(s.has_layer);
m_background[i]->SetUnavailable(no_layer);
}
m_unwrap->Enable(s.has_layer);
m_unwrap->SetUnavailable(no_layer);
m_unwrap->SetBadge(s.unwrap_stale);
m_unwrap->SetToolTip(s.unwrap_stale ?
_L("Out of date - the paint, the seams or the seam angle changed since this unwrap was made. "
"Press to unwrap again.") :
_L("Flatten the painted area into UV islands. It is computed only when you press this, not on "
"every edit - so paint, change the seam angle or mark seams first, then press Unwrap."));
m_unwrap->SetTip(s.unwrap_stale ?
_L("Out of date - the paint, the seams or the seam angle changed since this unwrap was made. "
"Press to unwrap again.") :
_L("Flatten the painted area into UV islands. It is computed only when you press this, not on "
"every edit - so paint, change the seam angle or mark seams first, then press Unwrap."));
if (m_seam_angle->GetValue() != int(std::lround(s.seam_angle_deg)))
m_seam_angle->SetValue(int(std::lround(s.seam_angle_deg)));
@@ -2080,15 +2172,26 @@ void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s)
m_connect->Enable(s.has_layer);
m_mark_seams->SetValue(s.mark_seams);
m_mark_seams->Enable(s.has_layer);
m_mark_seams->SetUnavailable(no_layer);
m_seam_path->SetValue(s.seam_path);
m_seam_path->Enable(s.has_layer && s.mark_seams);
m_clear_seams->Enable(s.has_layer && s.has_seams);
m_clear_edits->Enable(s.has_layer && s.has_uv_edits);
m_seam_path->SetUnavailable(!no_layer.empty() ? no_layer :
s.mark_seams ? wxString() :
_L("Turn Mark seams on first - Path is a quicker way of marking them."));
m_clear_seams->SetUnavailable(!no_layer.empty() ? no_layer :
s.has_seams ? wxString() :
_L("No seams are marked on this layer."));
m_clear_edits->SetUnavailable(!no_layer.empty() ? no_layer :
s.has_uv_edits ? wxString() :
_L("No islands have been reshaped by hand, so there is nothing to "
"discard."));
m_stats->SetLabel(s.unwrapped ? wxString::Format(_L("%d islands, %s faces"), s.island_count,
wxString(std::to_string(s.face_count))) :
wxString());
m_stats->SetToolTip(s.unwrapped ? _L("How the painted area came out of the unwrap: the number of separate pieces it "
"was cut into (at the seams and at edges sharper than the seam angle), and how "
"many triangles they hold in total.") :
wxString());
refresh_selection_tools();
if (relayout)
Layout();
@@ -2098,19 +2201,24 @@ void UVEditorPanel::refresh_selection_tools()
{
const bool has_islands = m_canvas->has_islands();
const int mode = int(m_canvas->select_mode());
// Faded rather than hard-disabled, so the tooltip still says what is missing (see apply_state()).
const wxString not_unwrapped = has_islands ? wxString() : _L("Press Unwrap first - there are no islands to work on yet.");
for (int i = 0; i < 3; ++i) {
m_select[i]->SetValue(i == mode);
m_select[i]->Enable(has_islands);
m_select[i]->SetUnavailable(not_unwrapped);
}
const bool island_picked = has_islands && m_canvas->select_mode() == UVEditorCanvas::SelectMode::Island &&
m_canvas->selected_island() >= 0;
m_avg_scale->Enable(has_islands);
m_cut->Enable(island_picked);
m_join->Enable(island_picked);
m_unjoin->Enable(island_picked);
m_snap->Enable(has_islands);
const wxString no_island = !not_unwrapped.empty() ? not_unwrapped :
island_picked ? wxString() :
_L("Click an island on the canvas first, in Island mode.");
m_avg_scale->SetUnavailable(not_unwrapped);
m_cut->SetUnavailable(no_island);
m_join->SetUnavailable(no_island);
m_unjoin->SetUnavailable(no_island);
m_snap->SetUnavailable(not_unwrapped);
m_snap->SetValue(m_canvas->snap_enabled());
m_frame->Enable(has_islands);
m_frame->SetUnavailable(not_unwrapped);
}
void UVEditorPanel::on_tool(wxCommandEvent &evt)
+12
View File
@@ -10,6 +10,7 @@
// pulls in the platform's real GL/gl.h, and glad/gl.h errors out if that happens first (it wants
// to be the one to define the standard include guards GL/gl.h itself defines).
#include <glad/gl.h>
#include <wx/event.h>
#include <wx/glcanvas.h>
#include <wx/panel.h>
#include <wx/button.h>
@@ -194,6 +195,7 @@ private:
void on_paint(wxPaintEvent &evt);
void on_size(wxSizeEvent &evt);
void on_mouse(wxMouseEvent &evt);
void on_capture_lost(wxMouseCaptureLostEvent &evt);
void on_key(wxKeyEvent &evt);
void on_leave(wxMouseEvent &evt); // drops the +/- cursor hint when the pointer leaves the canvas
void on_erase_background(wxEraseEvent &evt) {} // required to avoid flicker on MSW, deliberately a no-op
@@ -238,6 +240,16 @@ private:
// vertex of some *other* island, in texture-UV space. Zero if nothing is within reach (#2).
Vec2f snap_correction(int island) const;
void end_gesture();
// Cancels the gesture instead of finishing it: nothing is committed, and a modal rotate/scale is
// put back the way Esc puts it back. Used when the capture is taken away from us.
void cancel_gesture();
// One capture at a time, released exactly once. Pressing a second button mid-drag would otherwise
// nest a second capture that the single release on button-up cannot undo, and macOS never sends
// wxEVT_MOUSE_CAPTURE_LOST to recover from that. A leaked capture is not a local problem there:
// while any wx window holds one, wxOSX routes every mouse event to it and the application stops
// seeing enter/leave and motion entirely, which also takes its tooltips down.
void grab_mouse();
void drop_mouse();
// Rebuilds the status line from the current gesture/selection and pushes it to m_on_status.
void update_status();
// Re-picks what a click at `pos` would grab in the current select mode, and repaints when that changed.