GitHubLionel / wxMathPlot

An enhanced version of the wxMathPlot component for wxWidgets
11 stars 4 forks source link

question: how to use my own mouse event handler in an interactive application #33

Open asmwarrior opened 1 month ago

asmwarrior commented 1 month ago

Hi, currently, when the plot window is shown, the left mouse click event handler is hard coded, see below:

void mpWindow::OnMouseLeftDown(wxMouseEvent &event)
{
  m_mouseLClick = event.GetPosition();
#ifdef MATHPLOT_DO_LOGGING
  wxLogMessage(_T("mpWindow::OnMouseLeftDown() X = %d , Y = %d"), event.GetX(), event.GetY());
#endif
  m_movingInfoLayer = IsInsideInfoLayer(m_mouseLClick);
#ifdef MATHPLOT_DO_LOGGING
  if (m_movingInfoLayer != NULL)
  {
    wxLogMessage(_T("mpWindow::OnMouseLeftDown() started moving layer %p"), m_movingInfoLayer);
  }
#endif
  if (m_InInfoLegend)
  {
    int select = m_InfoLegend->GetPointed(*this, m_mouseLClick);
    if (m_configWindow == NULL)
      m_configWindow = new MathPlotConfigDialog(this);

    m_configWindow->Initialize(3);
    m_configWindow->SelectChoiceSerie(select);
    m_configWindow->Show();
  }

  event.Skip();
}

This is connected with the macros, see below:

EVT_MIDDLE_UP(mpWindow::OnShowPopupMenu)
EVT_RIGHT_DOWN(mpWindow::OnMouseRightDown) // JLB
EVT_RIGHT_UP (mpWindow::OnShowPopupMenu)
EVT_MOUSEWHEEL(mpWindow::OnMouseWheel )// JLB
EVT_MOTION(mpWindow::OnMouseMove)// JLB
EVT_LEAVE_WINDOW(mpWindow::OnMouseLeave)
EVT_LEFT_DOWN(mpWindow::OnMouseLeftDown)
EVT_LEFT_UP(mpWindow::OnMouseLeftRelease)

What I want to do is that in some applications, I need some interactive feature. For example, when in some mode, when left mouse click happens, I need to add one point to a serial, or I need to use the mouse drag to draw a custom poly-line.

So, I'd like ask some suggestion from you that how to do that?

Maybe I need to edit the event handler function, and add an if condition at the begin of the function?

Something like:

void mpWindow::OnMouseLeftDown(wxMouseEvent &event)
{
  if (is_interactive_draw_mode)
  {
    // my own draw code here
  }
  else
  {
    // the old mathplot's mouse event handler code
  }
  event.Skip();
}

Maybe you can have more suggestions? Thanks.

asmwarrior commented 1 month ago

For example, if you ever use python and matplotlib, you will see its toolbar is very nice, see below:

Interactive navigation — Matplotlib 3.2.2 documentation

When you click on the toolbar button, the "zoom" or "pan" feature can be enabled or not.

So, I'd like to see a similar feature in my own interactive application, because I plan to create some kinds of doodle application, which can draw lines by mouse, and later I can save the poly-lines with the mathplot's x,y coordinates, so later I can reload those lines again to the plot.

GitHubLionel commented 1 month ago

I maybe can add a callback just before the last event.Skip(); to run your code ? Something like that : if (m_OnUserMouseAction) m_OnUserMouseAction(this, event);

asmwarrior commented 1 month ago

I maybe can add a callback just before the last event.Skip(); to run your code ? Something like that : if (m_OnUserMouseAction) m_OnUserMouseAction(this, event);

Thanks for the help.

I think putting the user action(function call) before the event.Skip(); is not correct, because all the native mathplot's code about mouse handling is already done.

What I need is that the user action code could be put at the beginning of the event handler function.

One method: you can have an option to "disable" the build-int mouse handling(like zoom or pan). And if user want to add some custom action, they can use "Bind" function to add a dynamic event handler.

GitHubLionel commented 1 month ago

Yes but I don't want to break the present behaviour. I try something with callback. With this, your can do your stuff and ignore or no the official behaviour.

In your frame class, you just will have :

in Public section :
void OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel);

.... in m_plot create :

  m_plot->SetOnUserMouseAction([this](void *Sender, wxMouseEvent &event, bool &cancel)
      { OnUserMouseAction(Sender, event, cancel);});

And your specific action :
void MyFrame::OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel)
{
  wxMessageBox(wxString::Format("Type = %d", event.GetEventType()), ((mpWindow *)Sender)->GetLabel(),
  wxOK | wxICON_INFORMATION, this);
// your code

  cancel = false; // if you want continue the normal code
}

I put in Sandbox the code.

asmwarrior commented 1 month ago

Hi, thanks for the work.

Normally, I think a lot of mouse event handler need to have a "user action".

EVT_MIDDLE_UP(mpWindow::OnShowPopupMenu)     // this need user action
EVT_RIGHT_DOWN(mpWindow::OnMouseRightDown)  // this need user action
EVT_RIGHT_UP (mpWindow::OnShowPopupMenu)     // this need user action
EVT_MOUSEWHEEL(mpWindow::OnMouseWheel )    // this need user action
EVT_MOTION(mpWindow::OnMouseMove)// JLB     // this need user action
EVT_LEAVE_WINDOW(mpWindow::OnMouseLeave)
EVT_LEFT_DOWN(mpWindow::OnMouseLeftDown)  // this need user action
EVT_LEFT_UP(mpWindow::OnMouseLeftRelease)     // this need user action

So, do I need to add all the user action functor to those build-in event handler?

It looks complex, and that's why I suggest a global bool variable to control the user mode and build-in mouse handling. (like the way Matplotlib's toolbar do, when the zoom or pan toolbar button is pressed on, the zoom/pan feature is enabled)

asmwarrior commented 1 month ago

Maybe, you need to add the same code (see below) to every mouse event handler function's beginning?

void mpWindow::OnMouseLeftDown(wxMouseEvent &event)
{
  if (m_OnUserMouseAction != NULL)
  {
    bool cancel = true;
    m_OnUserMouseAction(this, event, cancel);
    if (cancel)
    {
      event.Skip();
      return;
    }
  }

// build in mouse event handling

So that in all mouse event handler, the same m_OnUserMouseAction function will be called.

And the user need to check which event type he is interested to handle.

See: https://docs.wxwidgets.org/3.2.5/classwx_mouse_event.html

Am I correct?

asmwarrior commented 1 month ago

It looks like my guess is correct.

Here is the code:

wxString GetEventTypeString(int eventType)
{
    if (eventType == wxEVT_LEFT_DOWN)
        return "mouse left down";
    else if (eventType == wxEVT_LEFT_UP)
        return "mouse left up";
    else if (eventType == wxEVT_RIGHT_DOWN)
        return "mouse right down";
    else if (eventType == wxEVT_RIGHT_UP)
        return "mouse right up";
    else if (eventType == wxEVT_MOTION)
        return "mouse motion";
    else if (eventType == wxEVT_KEY_DOWN)
        return "key down";
    else if (eventType == wxEVT_KEY_UP)
        return "key up";
    // Add more conditions for other event types as needed
    else
        return wxString::Format("unknown event (%d)", eventType);
}

void MathPlotDemoFrame::OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel)
{
    wxString eventTypeString = GetEventTypeString(event.GetEventType());
    wxString label = ((mpWindow *)Sender)->GetLabel();
    wxLogMessage(wxString::Format("Type = %s, Label = %s", eventTypeString, label));
    // your code

    cancel = true; // set to false, if you want continue the normal code
}

I have enabled the console window by using this function call:

    wxLog::SetActiveTarget(new wxLogStream(&std::cout));

    mPlot->SetOnUserMouseAction([this](void *Sender, wxMouseEvent &event, bool &cancel)
      { OnUserMouseAction(Sender, event, cancel);});

Now it works, I mean I can print all the mouse event type strings in the console window.

like below:

...
17:21:33: Type = mouse motion, Label = Mathplot
17:21:33: Type = mouse motion, Label = Mathplot
17:21:33: Type = mouse motion, Label = Mathplot
17:21:33: Type = mouse motion, Label = Mathplot
17:21:33: Type = mouse motion, Label = Mathplot
17:21:33: Type = mouse motion, Label = Mathplot
17:21:33: Type = mouse left down, Label = Mathplot
17:21:34: Type = mouse left up, Label = Mathplot
17:21:35: Type = mouse left down, Label = Mathplot
17:21:35: Type = mouse left up, Label = Mathplot
17:21:35: Type = mouse right down, Label = Mathplot
17:21:36: Type = mouse right up, Label = Mathplot
17:21:36: Type = mouse right down, Label = Mathplot
17:21:36: Type = mouse right up, Label = Mathplot
17:21:37: Type = mouse motion, Label = Mathplot
17:21:37: Type = mouse motion, Label = Mathplot
17:21:37: Type = mouse motion, Label = Mathplot
17:21:37: Type = mouse motion, Label = Mathplot
...
asmwarrior commented 1 month ago

Here is my codes to use the mouse to draw a polyline

wxString GetEventTypeString(int eventType)
{
    if (eventType == wxEVT_LEFT_DOWN)
        return "mouse left down";
    else if (eventType == wxEVT_LEFT_UP)
        return "mouse left up";
    else if (eventType == wxEVT_RIGHT_DOWN)
        return "mouse right down";
    else if (eventType == wxEVT_RIGHT_UP)
        return "mouse right up";
    else if (eventType == wxEVT_MOTION)
        return "mouse motion";
    else if (eventType == wxEVT_KEY_DOWN)
        return "key down";
    else if (eventType == wxEVT_KEY_UP)
        return "key up";
    // Add more conditions for other event types as needed
    else
        return wxString::Format("unknown event (%d)", eventType);
}

void MathPlotDemoFrame::OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel)
{
    wxString eventTypeString = GetEventTypeString(event.GetEventType());
    wxString label = ((mpWindow *)Sender)->GetLabel();
    wxLogMessage(wxString::Format("Type = %s, Label = %s", eventTypeString, label));

    // Get the mouse position relative to the mpWindow
    wxPoint mousePosition = event.GetPosition();

    // Cast Sender to mpWindow and convert the coordinates
    mpWindow *plotWindow = (mpWindow *)Sender;
    double plotX, plotY;

    plotX = plotWindow->p2x(mousePosition.x);
    plotY = plotWindow->p2y(mousePosition.y);

    wxLogMessage(wxString::Format("Mouse Position in Plot Coordinates: X = %f, Y = %f", plotX, plotY));

        // Left mouse button down
    if (event.LeftDown())
    {
        wxLogMessage(wxString::Format("Start dragging, add the initial point: X = %f, Y = %f", plotX, plotY));
        // Start dragging, add the initial point
        isDragging = true;
        points.clear();
        points.push_back(wxRealPoint(plotX, plotY));
    }
    // Mouse dragging with left button held down
    else if (event.Dragging() && event.LeftIsDown())
    {
        if (isDragging)
        {
            wxLogMessage(wxString::Format("Start dragging, add the initial point: X = %f, Y = %f", plotX, plotY));
            // Update the last point during dragging
            points.push_back(wxRealPoint(plotX, plotY));
            //plotWindow->Update();
        }
    }
    // Left mouse button released
    else if (event.LeftUp())
    {
        if (isDragging)
        {
            // Finalize the polyline by adding the last point
            points.push_back(wxRealPoint(plotX, plotY));
            isDragging = false;
            AddNewPolyline();
            //plotWindow->Update();
        }
    }

    // Your code
    cancel = true; // Set to false if you want to continue the normal code
}

// Override the drawing function to draw the polyline
void MathPlotDemoFrame::AddNewPolyline()
{
    mpFXYVector* plotLayer = new mpFXYVector();  // Assuming mpFXYVector for drawing

    std::vector<double> xData, yData;
    for (const auto& point : points)
    {
        xData.push_back(point.x);
        yData.push_back(point.y);
    }

    plotLayer->SetData(xData, yData);

    // Customize the appearance of the polyline (optional)
    plotLayer->SetPen(wxPen(*wxBLUE, 2));  // Set line color and thickness

    plotLayer->SetContinuity(true);

    mPlot->AddLayer(plotLayer, true);  // Add the layer to the plot

    // mPlot->Update();
    // mPlot->Fit();  // Fit the plot to show all points
}

And I have some member variables in the main frame class:

        void OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel);
        std::vector<wxRealPoint> points;  // Store the points of the polyline
        bool isDragging = false;          // Track if the user is dragging the mouse
        void AddNewPolyline();

And here is the screen cast of user actions to draw the polyline.

https://github.com/user-attachments/assets/6e601404-a59a-4d5c-ba09-a8557475a4bc

I think the next step is:

I need to draw some lines(they are temporary lines when I drag the mouse) in the pixel coordinates when mouse motion, when I finally mouse button up, I need to remove the those lines and make the polyline in the plot(call the AddNewPolyline() function).

I'm not sure it is easy to do that, because this kinds of rubber band selection.

GitHubLionel commented 1 month ago

Yes, for the test, I just put the code in "mouse left down" event but the structure of the callback is intended to work for other events. I see that is what you do. So I can validate the test and push it on the main branch.

GitHubLionel commented 1 month ago

If necessary, I can add some other parameters to the callback. But with the Sender parameter, we can access to all the plot parameters :+1:

asmwarrior commented 1 month ago

Hi, thanks.

I'm currently learning how to use the wxDCOverlay class, it looks like this is the way I need to use when I repaint the window in mouse motion event handler.

See discussion here:

wxWidgets: wxOverlay Class Reference

The code I'm learning is the "drawing" sample code from wxWidgets.

Currently, I can access all the functions inside the mpWindow class.

GitHubLionel commented 1 month ago

Note : You don't need a point buffer. You can draw directly your polyline :

void MathPlotDemoFrame::OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel)
{
    wxString eventTypeString = GetEventTypeString(event.GetEventType());
    wxString label = ((mpWindow *)Sender)->GetLabel();
//    wxLogMessage(wxString::Format("Type = %s, Label = %s", eventTypeString, label));

    // Get the mouse position relative to the mpWindow
    wxPoint mousePosition = event.GetPosition();

    // Cast Sender to mpWindow and convert the coordinates
    mpWindow *plotWindow = (mpWindow *)Sender;
    double plotX, plotY;

    plotX = plotWindow->p2x(mousePosition.x);
    plotY = plotWindow->p2y(mousePosition.y);

//    wxLogMessage(wxString::Format("Mouse Position in Plot Coordinates: X = %f, Y = %f", plotX, plotY));

        // Left mouse button down
    if (event.LeftDown())
    {
//        wxLogMessage(wxString::Format("Start dragging, add the initial point: X = %f, Y = %f", plotX, plotY));
        // Start dragging, add the initial point
        isDragging = true;
        points.clear();
        points.push_back(wxRealPoint(plotX, plotY));

        CurrentPolyline = new mpFXYVector("New polyline");
        CurrentPolyline->AddData(plotX, plotY, false);
        wxColour random_color = wxIndexColour(rand() * 20 / RAND_MAX);
        CurrentPolyline->SetPen(wxPen(random_color, 2));
        CurrentPolyline->SetContinuity(true);
        plotWindow->AddLayer(CurrentPolyline);
    }
    // Mouse dragging with left button held down
    else if (event.Dragging() && event.LeftIsDown())
    {
        if (isDragging)
        {
 //           wxLogMessage(wxString::Format("Start dragging, add the initial point: X = %f, Y = %f", plotX, plotY));
            // Update the last point during dragging
            points.push_back(wxRealPoint(plotX, plotY));
            //plotWindow->Update();

            CurrentPolyline->AddData(plotX, plotY, true);
        }
    }
    // Left mouse button released
    else if (event.LeftUp())
    {
        if (isDragging)
        {
            // Finalize the polyline by adding the last point
            points.push_back(wxRealPoint(plotX, plotY));
            isDragging = false;
 //           AddNewPolyline();
            //plotWindow->Update();
            CurrentPolyline->AddData(plotX, plotY, true); // Last point
        }
    }

    // Your code
    cancel = true; // Set to false if you want to continue the normal code
}

And the member private variable :

mpFXYVector* CurrentPolyline = NULL;
asmwarrior commented 1 month ago
CurrentPolyline->AddData(plotX, plotY, true);

Will this code cause too much repaint, and draw too many times when mouse motion.

GitHubLionel commented 1 month ago

Yes repaint for each point but with my (old) computer, I don't see flashing. You can plot one point over x points with a modulo for example, one over 10 : CurrentPolyline->AddData(plotX, plotY, (counter++ % 10)); and define a static counter. Note that with this, we have a strange behaviour : line is slobbery !

GitHubLionel commented 1 month ago

Oups, I forgot the == 0 ! Anyway, you have two options : CurrentPolyline->AddData(plotX, plotY, false); if ((counter++ % 5) == 0) plotWindow->Refresh(); Or CurrentPolyline->AddData(plotX, plotY, (counter++ % 5) == 0); With the second, you have a dotted line until the final point (you need a plotWindow->Refresh(); for the last point). And I correct a potential bug, not sure but not bad.

GitHubLionel commented 1 month ago

Another remark in your code. Maybe it is better to initialize cancel to false at the beginning. And set cancel to true only when you have captured the event. With that, you keep the normal behaviour for other events (like wheel, ...)

asmwarrior commented 1 month ago

I see the latest code that the comment is wrong for the new added function:

@@ -2770,6 +2789,13 @@ class WXDLLIMPEXP_MATHPLOT mpWindow: public wxWindow
       m_OnDeleteLayer = event;
     }

+    /** On delete layer event
+     @return reference to event */
+    void SetOnUserMouseAction(mpOnUserMouseAction event)
+    {
+      m_OnUserMouseAction = event;
+    }
+

This is a copy-paste error.

asmwarrior commented 1 month ago

This is my current code:

void MathPlotDemoFrame::OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel)
{
    wxString eventTypeString = GetEventTypeString(event.GetEventType());
    wxString label = ((mpWindow *)Sender)->GetLabel();
    // wxLogMessage(wxString::Format("Type = %s, Label = %s", eventTypeString, label));

    // Get the mouse position relative to the mpWindow
    wxPoint mousePosition = event.GetPosition();

    static int counter = 0;

    // Cast Sender to mpWindow and convert the coordinates
    mpWindow *plotWindow = (mpWindow *)Sender;
    double plotX, plotY;

    plotX = plotWindow->p2x(mousePosition.x);
    plotY = plotWindow->p2y(mousePosition.y);

    // wxLogMessage(wxString::Format("Mouse Position in Plot Coordinates: X = %f, Y = %f", plotX, plotY));

    // Left mouse button down
    if (event.LeftDown())
    {
        // wxLogMessage(wxString::Format("Start dragging, add the initial point: X = %f, Y = %f", plotX, plotY));
        // Start dragging, add the initial point
        isDragging = true;
        points.clear();
        points.push_back(wxRealPoint(plotX, plotY));
        counter = 0;

        CurrentPolyline = new mpFXYVector("New polyline");
        CurrentPolyline->AddData(plotX, plotY, false);
        wxColour random_color = wxIndexColour(rand() * 20 / RAND_MAX);
        CurrentPolyline->SetPen(wxPen(random_color, 2));
        CurrentPolyline->SetContinuity(true);
        plotWindow->AddLayer(CurrentPolyline);
    }
    // Mouse dragging with left button held down
    else if (event.Dragging() && event.LeftIsDown())
    {
        if (isDragging)
        {
            // wxLogMessage(wxString::Format("Start dragging, add the initial point: X = %f, Y = %f", plotX, plotY));
            // Update the last point during dragging
            points.push_back(wxRealPoint(plotX, plotY));
            //plotWindow->Update();

            // CurrentPolyline->AddData(plotX, plotY, true);
            CurrentPolyline->AddData(plotX, plotY, (counter++ % 5) == 0);
        }
    }
    // Left mouse button released
    else if (event.LeftUp())
    {
        if (isDragging)
        {
            // Finalize the polyline by adding the last point
            points.push_back(wxRealPoint(plotX, plotY));
            isDragging = false;
            // AddNewPolyline();
            // plotWindow->Update();
            CurrentPolyline->AddData(plotX, plotY, true); // Last point
        }
    }

    // Your code
    cancel = true; // Set to false if you want to continue the normal code
}

When I finish dragging, It looks like the last function call CurrentPolyline->AddData(plotX, plotY, true); // Last point does not refresh the whole plot?

See the screen cast below: I'm not sure why.

https://github.com/user-attachments/assets/f38b1404-2443-4e25-a1a9-a045ca685a6f

asmwarrior commented 1 month ago

Yes repaint for each point but with my (old) computer, I don't see flashing.

My guess is that the plot is quite simple. But in my own project, I'd like to draw some bitmaps on the screen, maybe, there are some other extra contour lines I need to draw on top of the screen, so I guess the performance will be bad if I need to refresh the whole plot when mouse moves(mouse motion event). The only way I found is using a wxDCOverlay like class, this class will hold the background window's image, and let you draw something on top of it. The actual mpFXYVector object will be added after I release the mouse button(mouse motion finished).

asmwarrior commented 1 month ago
void MathPlotDemoFrame::OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel)
{

static wxOverlay  m_overlay;
    wxString eventTypeString = GetEventTypeString(event.GetEventType());
    wxString label = ((mpWindow *)Sender)->GetLabel();
    // wxLogMessage(wxString::Format("Type = %s, Label = %s", eventTypeString, label));

    // Get the mouse position relative to the mpWindow
    wxPoint mousePosition = event.GetPosition();

    static int counter = 0;

    // Cast Sender to mpWindow and convert the coordinates
    mpWindow *plotWindow = (mpWindow *)Sender;
    double plotX, plotY;
    int x = mousePosition.x;
    int y = mousePosition.y;

    plotX = plotWindow->p2x(mousePosition.x);
    plotY = plotWindow->p2y(mousePosition.y);

    // wxLogMessage(wxString::Format("Mouse Position in Plot Coordinates: X = %f, Y = %f", plotX, plotY));

    // Left mouse button down
    if (event.LeftDown())
    {
        // wxLogMessage(wxString::Format("Start dragging, add the initial point: X = %f, Y = %f", plotX, plotY));
        // Start dragging, add the initial point
        isDragging = true;
        points.clear();
        points.push_back(wxRealPoint(x, y));
        counter = 0;
        wxLogMessage(wxString::Format("Start dragging, pixel point: X = %d, Y = %d", x, y));

    }
    // Mouse dragging with left button held down
    else if (event.Dragging() && event.LeftIsDown())
    {
        if (isDragging)
        {
            wxClientDC dc(plotWindow);
            PrepareDC(dc);
            wxDCOverlay overlay(m_overlay, &dc);

            // Only draw the last segment
            if (points.size() > 0)
            {
                wxPoint lastPoint(points[points.size() - 1].x, points[points.size() - 1].y);
                wxPoint currentPoint(x, y);

                dc.SetPen(wxPen(*wxLIGHT_GREY, 2));
                dc.DrawLine(lastPoint, currentPoint);
            }

            // Add the current point to the points vector
            points.push_back(wxRealPoint(x, y));
        }
    }
    // Left mouse button released
    else if (event.LeftUp())
    {
        if (isDragging)
        {
            // Finalize the polyline by adding the last point
            points.push_back(wxRealPoint(x, y));
            isDragging = false;
            m_overlay.Reset();
            // AddNewPolyline();
            // plotWindow->Update();
            mpFXYVector* CurrentPolylineNew = new mpFXYVector("New polyline");
            wxColour random_color = wxIndexColour(rand() * 20 / RAND_MAX);
            CurrentPolylineNew->SetPen(wxPen(random_color, 2));
            CurrentPolylineNew->SetContinuity(true);
            plotWindow->AddLayer(CurrentPolylineNew);

            for (int i=0; i<points.size(); i++)
            {
                plotX = plotWindow->p2x(points[i].x);
                plotY = plotWindow->p2y(points[i].y);
                CurrentPolylineNew->AddData(plotX, plotY, false);
            }
//            std::vector<double> xData, yData;
//            for (const auto& point : points)
//            {
//                xData.push_back(plotWindow->p2x(point.x));
//                yData.push_back(plotWindow->p2y(point.y));
//            }
//
//            CurrentPolylineNew->SetData(xData, yData);

            plotWindow->Update();
        }
    }

    // Your code
    cancel = true; // Set to false if you want to continue the normal code
}

The above code works correctly with the wxDCOverlay, and see the screen cast below, it is very fast when in mouse motion event handler, because the whole background image does not need to re-draw in the event handler.

https://github.com/user-attachments/assets/d11a0280-a7bc-4075-940b-124d514bbbd7

Now, the user can add any other graphics, like polylines, triangles, rectangles, I think we can even add some any other customized controls which can be converted to wxBitmap. (for example, JamesBremner/DXF_Viewer: A simple DXF File viewer project can be used to shown a CAD file in the wxWidgets windows, so the wxMathPlot may be used to show CAD files in the future).

So, the wxMathPlot control can be more interactive. That's great!

asmwarrior commented 1 month ago

I found an potential issue:

I try to create a mpFXYVector like below:

            mpFXYVector* CurrentPolylineNew = new mpFXYVector("New polyline");

            for (int i=0; i<points.size(); i++)
            {
                plotX = plotWindow->p2x(points[i].x);
                plotY = plotWindow->p2y(points[i].y);
                CurrentPolylineNew->AddData(plotX, plotY, false);
            }

            wxColour random_color = wxIndexColour(rand() * 20 / RAND_MAX);
            CurrentPolylineNew->SetPen(wxPen(random_color, 2));
            CurrentPolylineNew->SetContinuity(true);
            plotWindow->AddLayer(CurrentPolylineNew);

But I got crash in the above code, because I see that the CurrentPolylineNew->AddData(plotX, plotY, false); will access the mpWindow* m_win; member variables, but this variable is not set before the function call plotWindow->AddLayer(CurrentPolylineNew);.

So, my guess is that the member variable inside the

class WXDLLIMPEXP_MATHPLOT mpLayer: public wxObject

Should set the nullptr in its constructor?

Any ideas?

asmwarrior commented 1 month ago

Another remark in your code. Maybe it is better to initialize cancel to false at the beginning. And set cancel to true only when you have captured the event. With that, you keep the normal behaviour for other events (like wheel, ...)

I think my idea is that I need a "toggle button" or "check box button", so that I can switch the user action and the normal mouse build-in zoom/pan behavior.

GitHubLionel commented 1 month ago

Ok so :

GitHubLionel commented 1 month ago

For your potential bug, yes you can not call AddData with parameter true or false before have added the new layer. That's logical because we need the window to calculate the bound, may be shall I add a test to prevent this. EDIT : add warning and initialise m_win to null

asmwarrior commented 1 month ago

Ok so :

* corrected copy/paste comments

* Super the overlay. In summary, you draw points in the Overlay when the mouse is moving and when the mouse button is released, you create the mpFXYVector layer.

* No need the point buffer array. I clean your code, work fine :
void MyFrame::OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel)
{
  static wxOverlay m_overlay;
  wxString eventTypeString = GetEventTypeString(event.GetEventType());
  wxString label = ((mpWindow*)Sender)->GetLabel();
  // wxLogMessage(wxString::Format("Type = %s, Label = %s", eventTypeString, label));

  // Get the mouse position relative to the mpWindow
  wxPoint mousePosition = event.GetPosition();

  // Cast Sender to mpWindow and convert the coordinates
  mpWindow* plotWindow = (mpWindow*)Sender;

  static wxPoint currentPoint;
  static wxPoint lastPoint;
  double plotX, plotY;
  int x = mousePosition.x;
  int y = mousePosition.y;

  plotX = plotWindow->p2x(mousePosition.x);
  plotY = plotWindow->p2y(mousePosition.y);

  cancel = false;
  // Left mouse button down
  if (event.LeftDown())
  {
    // Start dragging, add the initial point
    isDragging = true;

    CurrentPolyline = new mpFXYVector("New polyline");
    wxColour random_color = wxIndexColour(rand() * 20 / RAND_MAX);
    CurrentPolyline->SetPen(wxPen(random_color, 2));
    CurrentPolyline->SetContinuity(true);
    // Add new Polyline but not plot it
    plotWindow->AddLayer(CurrentPolyline, false);
    // Add point to Polyline but not plot it
    CurrentPolyline->AddData(plotX, plotY, false);
    lastPoint = wxPoint(x, y);
    cancel = true;
  }
  // Mouse dragging with left button held down
  else
    if (event.Dragging() && event.LeftIsDown())
    {
      if (isDragging)
      {
        wxClientDC dc(plotWindow);
        PrepareDC(dc);
        wxDCOverlay overlay(m_overlay, &dc);

        // Only draw the last segment

        currentPoint = wxPoint(x, y);
        dc.SetPen(wxPen(*wxLIGHT_GREY, 2));
        dc.DrawLine(lastPoint, currentPoint);
        lastPoint = currentPoint;

        cancel = true;

        // Add point to Polyline but not plot it
        CurrentPolyline->AddData(plotX, plotY, false);
      }
    }
    // Left mouse button released
    else
      if (event.LeftUp())
      {
        if (isDragging)
        {
          // Finalize the polyline by adding the last point
          isDragging = false;
          m_overlay.Reset();

          CurrentPolyline->AddData(plotX, plotY, false); // Last point
          plotWindow->Refresh();  // then refresh the plot
          cancel = true;
        }
      }
}`

Thanks, this code works fine.

BTW, I see there will be two context menu shown on the plot. Here is the steps to reproduce the bug:

1, right mouse button click, the context menu will be shown 2, middle mouse button click, another context menu will be shown again.

So, there are two context menus shown on the plot, so I guess this is a bug.

GitHubLionel commented 1 month ago

Yes and if you click several times on the middle mouse button, you will have several context menu. Not fatal but not desired. I can just suppress EVT_MIDDLE_UP(mpWindow::OnShowPopupMenu) I add a GetSize() function to get the number of points in a series. With this function we can avoid single click and release at the same point in your code :

void MyFrame::OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel)
{
  static wxOverlay m_overlay;
  wxString eventTypeString = GetEventTypeString(event.GetEventType());
  wxString label = ((mpWindow*)Sender)->GetLabel();
  // wxLogMessage(wxString::Format("Type = %s, Label = %s", eventTypeString, label));

  // Get the mouse position relative to the mpWindow
  wxPoint mousePosition = event.GetPosition();

  // Cast Sender to mpWindow and convert the coordinates
  mpWindow* plotWindow = (mpWindow*)Sender;

  static wxPoint currentPoint;
  static wxPoint lastPoint;
  double plotX, plotY;
  int x = mousePosition.x;
  int y = mousePosition.y;

  plotX = plotWindow->p2x(mousePosition.x);
  plotY = plotWindow->p2y(mousePosition.y);

  cancel = false;
  // Left mouse button down
  if (event.LeftDown())
  {
    // Start dragging, add the initial point
    isDragging = true;

    CurrentPolyline = new mpFXYVector("New polyline");
    wxColour random_color = wxIndexColour(rand() * 20 / RAND_MAX);
    CurrentPolyline->SetPen(wxPen(random_color, 2));
    CurrentPolyline->SetContinuity(true);

    // Add new Polyline but not plot it
    plotWindow->AddLayer(CurrentPolyline, false);
    // Add point to Polyline but not plot it
    CurrentPolyline->AddData(plotX, plotY, false);

    lastPoint = wxPoint(x, y);
    cancel = true;
  }
  // Mouse dragging with left button held down
  else
    if (event.Dragging() && event.LeftIsDown())
    {
      if (isDragging)
      {
        wxClientDC dc(plotWindow);
        PrepareDC(dc);
        wxDCOverlay overlay(m_overlay, &dc);

        // Only draw the last segment
        currentPoint = wxPoint(x, y);
        dc.SetPen(wxPen(*wxLIGHT_GREY, 2));
        dc.DrawLine(lastPoint, currentPoint);
        lastPoint = currentPoint;

        // Add point to Polyline but not plot it
        CurrentPolyline->AddData(plotX, plotY, false);
        cancel = true;
      }
    }
    // Left mouse button released
    else
      if (event.LeftUp())
      {
        if (isDragging)
        {
          // Finalize the polyline by adding the last point
          isDragging = false;
          m_overlay.Reset();

          // Prevent the simple click with no dragging
          if (CurrentPolyline->GetSize() == 1)
          {
            plotWindow->DelLayer(CurrentPolyline, true, false);
          }
          else
          {
            CurrentPolyline->AddData(plotX, plotY, false); // Last point
            plotWindow->Refresh();  // then refresh the plot
          }
          cancel = true;
        }
      }
}

Another point : I think I can pass several functions from public to protected. For example void DoPlot(wxDC &dc, mpWindow &w). Your opinion ?

asmwarrior commented 1 month ago

Another point : I think I can pass several functions from public to protected. For example void DoPlot(wxDC &dc, mpWindow &w). Your opinion ?

I agree, I think those kinds of function should not be called from the client code, they should only be called internally in the mpWindow class.

I add a GetSize() function to get the number of points in a series. With this function we can avoid single click and release at the same point in your code

Thanks, that's great.

GitHubLionel commented 1 month ago

Ok, I passed DoPlot from public to protected.

DRNadler commented 4 weeks ago

Hi Guys - A few thoughts about this, please bear with me for longish explanation... First, have a look at this demo, noting:

Demo: https://www.nadler.com/backups/2024-08-18_AnalysisProgramProgress.mp4

The normal way to extend functionality in C++ is with virtual member functions. Thus to add the track highlight circle, I very simply extend mpFXY::DoPlot:

    virtual void DoPlot(wxDC &dc, mpWindow &w) override {
        MathPlot::mpFXY::DoPlot(dc,w); // call base class to perform majority of drawing
        // If enabled, draw circle to highlight a specific observation point
        if(!highlightEnabled) return;
        const GPSinfo_T &GPS = logInstance.sensorRecords[highlightIdx].gps;
        wxCoord x = w.x2p(GPS.longitude);
        wxCoord y = w.y2p(GPS.latitude);
        dc.SetPen(wxPen( *wxBLACK, 3));
        dc.DrawCircle(x,y,10);
    }

To support my need for custom mpInfoCoords , I modified MathPlot (and Lionel has accepted pull request) by simply refactoring the text preparation into a virtual function. So a very tiny modifications to MathPlot. I can simply replace the virtual function like this:

    virtual wxString GetInfoCoordsText(double xVal, double yVal, double y2Val, bool isY2Axis = false) {
        int idx = logInstance.RangeCheckedIdx((int)xVal);
        const SensorDerivedValues &sdv =logInstance.sensorDerivedValues[idx];
        wxString result;
        result.Printf("idx=%d\nAlt=%f.0\nTE=%f.2", idx, sdv.altitudeM, sdv.TEvario);
        return result;
    }

MathPlot was designed to use virtual functions throughout. But for some reason, the mouse functions were not virtual. I think the best way to make it possible to override the mouse behavior is simply make these functions virtual, so they can simply be overridden completely, or extended as I did above with DoPlot. Reasons:

I suggest we remove the added callback function API and members such as CheckUserMouseAction, and simply make all the mouse functions virtual. This is simpler, more consistent with the library design, and makes it easy to override mouse behavior such as OnMouseLeave (I need this for my application)...

@asmwarrior @GitHubLionel - what do you think? @asmwarrior you would need to make (small) changes to your code...

asmwarrior commented 4 weeks ago

I suggest we remove the added callback function API and members such as CheckUserMouseAction, and simply make all the mouse functions virtual. This is simpler, more consistent with the library design, and makes it easy to override mouse behavior such as OnMouseLeave (I need this for my application)...

If you make the mouse event handler a "virtual function", can you still use the macro based event binding methods?

In wxWidgets, I think the macro based event binding way is a static method, we can't simply change the behavior easily.

BTW: I have view your posted video, looks nice!

asmwarrior commented 4 weeks ago

Oh, about the virtual function method, you can first use the macro based event binding to connect a normal (non virtual) function in the mpWindow class, than in the normal function, you call a virtual function. So that the virtual function method should work.

GitHubLionel commented 3 weeks ago

If you make the mouse event handler a "virtual function", can you still use the macro based event binding methods?

I try, seem to work so I can just add virtual to all event functions. So we can keep the two methods.