How to control LineIndentation in wxStyledTextCtrl when user presses Enter

298 Views Asked by At

When user presses Enter key in wxStyledTextCtrl, it seems that the cursor always goes to beginning of the line (zero indentation), which is most likely the expected behavior.

I want to be able to write Script code with the following format, with line indents.

for i=1,10 do --say there is no indentation
   i=i+1 -- now there is indentation via tab key
   -- pressing enter should proceed with this level of indentation
   print(i) -- same level of indentation with the previous code line
end

I use the following C++ code to be able to control indentation at a very basic level.

void Script::OnKeyUp(wxKeyEvent& evt)
{
    if ((evt.GetKeyCode() == WXK_RETURN || evt.GetKeyCode() == WXK_NUMPAD_ENTER)) {
        long int col, line;
        PositionToXY(GetInsertionPoint(), &col, &line);
        int PreviousIndentation = GetLineIndentation(line-1);
        SetLineIndentation(line, PreviousIndentation);
        GotoPos(GetCurrentPos() + PreviousIndentation);
    }
}

The above C++ code preserves the indentation level, however, the cursor first goes to the beginning of the line and then to the indentation level. When using other IDEs, this does not happen in such way, such as going to the beginning of line and then to the indentation level. Rather, the cursor immediately goes to /follows the indentation level. Is there a way that the cursor can immediately go to the indentation level without initially going to zero indentation level.

By the way, I tried EVT_STC_CHARADDED, which seems like the way implemented in ZeroBraneStudio, but when Enter key is pressed evt.GetKeyCode() returns a weird integer and evt.GetUnicodeKey returns \0 and moreover EVT_STC_CHARADDED event is called twice (I guess due to CR+LF).

By the way, I am using wxWidgets-3.1.0 on Windows 10.

Any ideas would be appreciated.

2

There are 2 best solutions below

3
On BEST ANSWER

We need to intercept an event and add a copy of the indentation from the previous line to the new line. The first question is which event to use. When the enter key is pressed, the following events are fired:

  • wxEVT_CHAR_HOOK
  • wxEVT_KEY_DOWN
  • wxEVT_STC_MODIFIED - ModificationType: 0x00100000
  • wxEVT_STC_MODIFIED - ModificationType: 0x00000410
  • wxEVT_STC_MODIFIED - ModificationType: 0x00002011
  • wxEVT_STC_CHARADDED
  • wxEVT_STC_UPDATEUI
  • wxEVT_STC_PAINTED
  • wxEVT_KEY_UP

With the char_hook and key_down events, the key hasn't been sent to the control yet, so it won't be able to give the needed position information. The control shouldn't be changed in the stc_modified event, so we shouldn't use those events. By the time of stc_painted event, the cursor has already been drawn, so it and the key_up event are too late. And I learned in the other answer that stc_updateui event won't work.

So by process of elimination, the only possibility is the wxEVT_STC_CHARADDED event. The next question is what to do in that event handler. I've adapted the code from here to work with wxStyledTextCtrl.

void Script::OnCharAdded(wxStyledTextEvent& event)
{
    int new_line_key=(GetEOLMode()==wxSTC_EOL_CR)?13:10;

    if ( event.GetKey() == new_line_key )
    {
        int cur_pos = GetCurrentPos();
        int cur_line = LineFromPosition(cur_pos);

        if ( cur_line > 0 )
        {
            wxString prev_line = GetLine(cur_line-1);
            size_t prev_line_indent_chars(0);
            for ( size_t i=0; i<prev_line.Length(); ++i )
            {
                wxUniChar cur_char=prev_line.GetChar(i);

                if (cur_char==' ')
                {
                    ++prev_line_indent_chars;
                }
                else if (cur_char=='\t')
                {
                    ++prev_line_indent_chars;
                }
                else
                {
                    break;
                }
            }

            AddText(prev_line.Left(prev_line_indent_chars));
        }
    }
}

This might be better since it physically counts the spaces and tabs.

5
On

Note: The comments below point out a fatal flaw in the code for this answer. Adjusting the cursor position in an UpdateUI event handler like I tried to do here is a bad idea. I posted another answer that hopefully works better.


I can't guarentee that this is the best way, but here is one way. First, this requires adding an integer member to your script class to serve as a flag indicating that indentation needs to be added. In the following, I've called it 'm_indentToAdd'.

To detect that a line has been added, you can use the wxEVT_STC_MODIFIED event. If the modification type indicates that it was a user action, text has been inserted, and that 1 line has been added, then the next line will need to have indentation added. In addition to the enter key being pressed, this will catch when a single line including the line endings has been pasted.

void Script::OnModified(wxStyledTextEvent& event)
{
    int mt = event.GetModificationType();

    if(mt&wxSTC_MOD_INSERTTEXT && mt&wxSTC_PERFORMED_USER && event.GetLinesAdded()==1)
    {
        int cur_line = m_stc->LineFromPosition(event.GetPosition());
        int cur_indent = m_stc->GetLineIndentation(cur_line);
        m_indentToAdd=cur_indent;
    }
}

To avoid having the cursor start at the beginning of the line and then move to the indentation, you can handle the wxEVT_STC_UPDATEUI event and reset the position there:

void Script::OnUpdateUI(wxStyledTextEvent& event)
{
    if(m_indentToAdd)
    {
        int cur_pos = m_stc->GetCurrentPos();
        int cur_line = m_stc->LineFromPosition(cur_pos);
        m_stc->SetLineIndentation(cur_line, m_indentToAdd);
        m_stc->GotoPos(cur_pos+m_indentToAdd);

        m_indentToAdd=0;
    }
}

The UpdateUI event doesn't provide the current position or line, so they have to be recomputed before the indentation can be set. I suppose this could be optimized by storing those values in the OnModified event handler and then using them in the UpdateUI event handler.