diff --git a/lldb/source/Core/IOHandlerCursesGUI.cpp b/lldb/source/Core/IOHandlerCursesGUI.cpp --- a/lldb/source/Core/IOHandlerCursesGUI.cpp +++ b/lldb/source/Core/IOHandlerCursesGUI.cpp @@ -392,6 +392,12 @@ void Box(chtype v_char = ACS_VLINE, chtype h_char = ACS_HLINE) { ::box(m_window, v_char, h_char); } + void VerticalLine(int n, chtype v_char = ACS_VLINE) { + ::wvline(m_window, v_char, n); + } + void HorizontalLine(int n, chtype h_char = ACS_HLINE) { + ::whline(m_window, h_char, n); + } void Clear() { ::wclear(m_window); } void Erase() { ::werase(m_window); } Rect GetBounds() const { @@ -674,6 +680,36 @@ AttributeOff(attr); } + void DrawBox(const Rect &bounds, chtype v_char = ACS_VLINE, + chtype h_char = ACS_HLINE) { + MoveCursor(bounds.origin.x, bounds.origin.y); + VerticalLine(bounds.size.height); + HorizontalLine(bounds.size.width); + PutChar(ACS_ULCORNER); + + MoveCursor(bounds.origin.x + bounds.size.width - 1, bounds.origin.y); + VerticalLine(bounds.size.height); + PutChar(ACS_URCORNER); + + MoveCursor(bounds.origin.x, bounds.origin.y + bounds.size.height - 1); + HorizontalLine(bounds.size.width); + PutChar(ACS_LLCORNER); + + MoveCursor(bounds.origin.x + bounds.size.width - 1, + bounds.origin.y + bounds.size.height - 1); + PutChar(ACS_LRCORNER); + } + + void DrawTitledBox(const Rect &bounds, const char *title, + chtype v_char = ACS_VLINE, chtype h_char = ACS_HLINE) { + DrawBox(bounds, v_char, h_char); + int title_offset = 2; + MoveCursor(bounds.origin.x + title_offset, bounds.origin.y); + PutChar('['); + PutCString(title, bounds.size.width - title_offset); + PutChar(']'); + } + virtual void Draw(bool force) { if (m_delegate_sp && m_delegate_sp->WindowDelegateDraw(*this, force)) return; @@ -869,6 +905,636 @@ const Window &operator=(const Window &) = delete; }; +///////// +// Forms +///////// + +class FieldDelegate { +public: + virtual ~FieldDelegate() = default; + + virtual Rect FieldDelegateGetBounds() = 0; + + virtual void FieldDelegateDraw(Window &window, bool is_active) = 0; + + virtual HandleCharResult FieldDelegateHandleChar(int key) { + return eKeyNotHandled; + } +}; + +typedef std::shared_ptr FieldDelegateSP; + +class TextFieldDelegate : public FieldDelegate { +public: + TextFieldDelegate(const char *label, int width, Point origin, + const char *content) + : m_label(label), m_width(width), m_origin(origin), m_cursor_position(0), + m_first_visibile_char(0) { + if (content) + m_content = content; + assert(m_width > 2); + } + + // Get the bounding box of the field. The text field has a height of 3, 2 + // lines for borders and 1 for the content. + Rect FieldDelegateGetBounds() override { + return Rect(m_origin, Size(m_width, 3)); + } + + // Get the start X position of the content in window space, without the + // borders. + int GetX() { return m_origin.x + 1; } + + // Get the start Y position of the content in window space, without the + // borders. + int GetY() { return m_origin.y + 1; } + + // Get the effective width of the field, without the borders. + int GetEffectiveWidth() { return m_width - 2; } + + // Get the cursor position in window space. + int GetCursorWindowXPosition() { + return GetX() + m_cursor_position - m_first_visibile_char; + } + + int GetContentLength() { return m_content.length(); } + + void FieldDelegateDraw(Window &window, bool is_active) override { + // Draw label box. + window.DrawTitledBox(FieldDelegateGetBounds(), m_label.c_str()); + + // Draw content. + window.MoveCursor(GetX(), GetY()); + const char *text = m_content.c_str() + m_first_visibile_char; + window.PutCString(text, GetEffectiveWidth()); + + // Highlight the cursor. + window.MoveCursor(GetCursorWindowXPosition(), GetY()); + if (is_active) + window.AttributeOn(A_REVERSE); + if (m_cursor_position == GetContentLength()) + // Cursor is past the last character. Highlight an empty space. + window.PutChar(' '); + else + window.PutChar(m_content[m_cursor_position]); + if (is_active) + window.AttributeOff(A_REVERSE); + } + + // The cursor is allowed to move one character past the string. + // m_cursor_position is in range [0, GetContentLength()]. + void MoveCursorRight() { + if (m_cursor_position < GetContentLength()) + m_cursor_position++; + } + + void MoveCursorLeft() { + if (m_cursor_position > 0) + m_cursor_position--; + } + + // If the cursor moved past the last visible character, scroll right by one + // character. + void ScrollRightIfNeeded() { + if (m_cursor_position - m_first_visibile_char == GetEffectiveWidth()) + m_first_visibile_char++; + } + + void ScrollLeft() { + if (m_first_visibile_char > 0) + m_first_visibile_char--; + } + + // If the cursor moved past the first visible character, scroll left by one + // character. + void ScrollLeftIfNeeded() { + if (m_cursor_position < m_first_visibile_char) + m_first_visibile_char--; + } + + // Insert a character at the current cursor position, advance the cursor + // position, and make sure to scroll right if needed. + void InsertChar(char character) { + m_content.insert(m_cursor_position, 1, character); + m_cursor_position++; + ScrollRightIfNeeded(); + } + + // Remove the character before the cursor position, retreat the cursor + // position, and make sure to scroll left if needed. + void RemoveChar() { + if (m_cursor_position == 0) + return; + + m_content.erase(m_cursor_position - 1, 1); + m_cursor_position--; + ScrollLeft(); + } + + // True if the key represents a char that can be inserted in the field + // content, false otherwise. + virtual bool IsAcceptableChar(int key) { return isprint(key); } + + HandleCharResult FieldDelegateHandleChar(int key) override { + if (IsAcceptableChar(key)) { + InsertChar((char)key); + return eKeyHandled; + } + + switch (key) { + case KEY_RIGHT: + MoveCursorRight(); + ScrollRightIfNeeded(); + return eKeyHandled; + case KEY_LEFT: + MoveCursorLeft(); + ScrollLeftIfNeeded(); + return eKeyHandled; + case KEY_BACKSPACE: + RemoveChar(); + return eKeyHandled; + default: + break; + } + return eKeyNotHandled; + } + + // Returns the text content of the field. + const std::string &GetText() { return m_content; } + +protected: + std::string m_label; + // The total width of the field, including the two border characters. So the + // effective width is two characters less. + int m_width; + // The position of the top left corner character of the border. + Point m_origin; + std::string m_content; + // The cursor position in the content string itself. Can be in the range + // [0, GetContentLength()]. + int m_cursor_position; + // The index of the first visible character in the content. + int m_first_visibile_char; +}; + +class IntegerFieldDelegate : public TextFieldDelegate { +public: + IntegerFieldDelegate(const char *label, int width, Point origin, int content) + : TextFieldDelegate(label, width, origin, + std::to_string(content).c_str()) {} + + // Only accept digits. + bool IsAcceptableChar(int key) override { return isdigit(key); } + + // Returns the integer content of the field. + int GetInteger() { return std::stoi(m_content); } +}; + +class BooleanFieldDelegate : public FieldDelegate { +public: + BooleanFieldDelegate(const char *label, Point origin, bool content) + : m_label(label), m_origin(origin), m_content(content) {} + + // Get the bounding box of the field. The boolean field is drawn as follows: + // [X] Label or [ ] Label + // So 4 characters plus the length of the label. And only a single line. + Rect FieldDelegateGetBounds() override { + return Rect(m_origin, Size(4 + m_label.length(), 1)); + } + + // [X] Label or [ ] Label + void FieldDelegateDraw(Window &window, bool is_active) override { + window.MoveCursor(m_origin.x, m_origin.y); + window.PutChar('['); + if (is_active) + window.AttributeOn(A_REVERSE); + window.PutChar(m_content ? ACS_DIAMOND : ' '); + if (is_active) + window.AttributeOff(A_REVERSE); + window.PutChar(']'); + window.PutChar(' '); + window.PutCString(m_label.c_str()); + } + + void ToggleContent() { m_content = !m_content; } + + void SetContentToTrue() { m_content = true; } + + void SetContentToFalse() { m_content = false; } + + HandleCharResult FieldDelegateHandleChar(int key) override { + switch (key) { + case 't': + case '1': + SetContentToTrue(); + return eKeyHandled; + case 'f': + case '0': + SetContentToFalse(); + return eKeyHandled; + case ' ': + case '\r': + case '\n': + case KEY_ENTER: + ToggleContent(); + return eKeyHandled; + default: + break; + } + return eKeyNotHandled; + } + + // Returns the boolean content of the field. + bool GetBoolean() { return m_content; } + +protected: + std::string m_label; + // The window space position of the first character. + Point m_origin; + bool m_content; +}; + +class ChoicesFieldDelegate : public FieldDelegate { +public: + ChoicesFieldDelegate(const char *label, int width, int height, Point origin, + std::vector choices) + : m_label(label), m_width(width), m_height(height), m_origin(origin), + m_choices(choices), m_choice(0), m_first_visibile_choice(0) { + assert(m_width > 3); + assert(m_height > 2); + } + + // Get the bounding box of the field. The height is 2 border characters with + // one or more space to show choices in a list. + Rect FieldDelegateGetBounds() override { + return Rect(m_origin, Size(m_width, m_height)); + } + + // Get the X position of the choices in window space, without the borders. + int GetX() { return m_origin.x + 1; } + + // Get the Y position of the first visible choice in window space. + int GetY() { return m_origin.y + 1; } + + // Get the effective width of the field, without the borders. + int GetEffectiveWidth() { return m_width - 2; } + // + // Get the effective height of the field, without the borders. + int GetEffectiveHeight() { return m_height - 2; } + + int GetNumberOfChoices() { return m_choices.size(); } + + // Get the index of the last visible choice. + int GetLastVisibleChoice() { + return std::min(m_first_visibile_choice + GetEffectiveHeight() - 1, + GetNumberOfChoices() - 1); + } + + void FieldDelegateDraw(Window &window, bool is_active) override { + // Draw label box. + window.DrawTitledBox(FieldDelegateGetBounds(), m_label.c_str()); + + // Draw content. + int choices_to_draw = GetLastVisibleChoice() - m_first_visibile_choice + 1; + for (int i = 0; i < choices_to_draw; i++) { + window.MoveCursor(GetX(), GetY() + i); + int current_choice = m_first_visibile_choice + i; + const char *text = m_choices[current_choice].c_str(); + bool highlight = is_active && current_choice == m_choice; + if (highlight) + window.AttributeOn(A_REVERSE); + window.PutChar(current_choice == m_choice ? ACS_DIAMOND : ' '); + window.PutCString(text); + if (highlight) + window.AttributeOff(A_REVERSE); + } + } + + void SelectPrevious() { + if (m_choice > 0) + m_choice--; + } + + void SelectNext() { + if (m_choice < GetNumberOfChoices() - 1) + m_choice++; + } + + // If the cursor moved past the first visible choice, scroll up by one + // choice. + void ScrollUpIfNeeded() { + if (m_choice < m_first_visibile_choice) + m_first_visibile_choice--; + } + + // If the cursor moved past the last visible choice, scroll down by one + // choice. + void ScrollDownIfNeeded() { + if (m_choice > GetLastVisibleChoice()) + m_first_visibile_choice++; + } + + HandleCharResult FieldDelegateHandleChar(int key) override { + switch (key) { + case KEY_UP: + SelectPrevious(); + ScrollUpIfNeeded(); + return eKeyHandled; + case KEY_DOWN: + SelectNext(); + ScrollDownIfNeeded(); + return eKeyHandled; + default: + break; + } + return eKeyNotHandled; + } + + // Returns the content of the choice as a string. + std::string GetChoiceContent() { return m_choices[m_choice]; } + + // Returns the index of the choice. + int GetChoice() { return m_choice; } + +protected: + std::string m_label; + // The total width of the field, including the two border characters. So the + // effective width is two characters less. + int m_width; + // The total height of the field, including the two border characters. So the + // effective width is two characters less. + int m_height; + // The position of the top left corner character of the border. + Point m_origin; + std::vector m_choices; + // The index of the selected choice. + int m_choice; + // The index of the first visible choice in the field. + int m_first_visibile_choice; +}; + +class Field { +public: + Field(FieldDelegateSP &delegate_sp, int page_index) + : m_delegate_sp(delegate_sp), m_page_index(page_index) {} + + void Draw(Window &window, bool is_active) { + m_delegate_sp->FieldDelegateDraw(window, is_active); + } + + HandleCharResult HandleChar(int key) { + return m_delegate_sp->FieldDelegateHandleChar(key); + } + + int GetPageIndex() { return m_page_index; } + +protected: + FieldDelegateSP m_delegate_sp; + // The index of the page this field belongs to. + int m_page_index; +}; + +class FormDelegate { +public: + FormDelegate() : m_has_error(false), m_last_page_index(0) {} + + virtual ~FormDelegate() = default; + + virtual HandleCharResult FormDelegateHandleChar(int selected_field_index, + int key) { + return m_fields[selected_field_index].HandleChar(key); + } + + virtual void FormDelegateDraw(Window &window, int selected_field_index) { + int active_page_index = GetActivePageIndex(selected_field_index); + for (int i = 0; i < GetNumberOfFields(); i++) { + if (m_fields[i].GetPageIndex() != active_page_index) + continue; + + bool is_field_selected = selected_field_index == i; + m_fields[i].Draw(window, is_field_selected); + } + } + + // Return true if submission was successful, false otherwise. If false, the + // method should set the m_error member to an appropriate error message. + virtual bool FormDelegateSubmit() = 0; + + int GetNumberOfFields() { return m_fields.size(); } + + bool HasError() { return m_has_error; } + + std::string &GetError() { return m_error; } + + // Return the index of the page that includes the selected field. If no field + // is selected, that is, if the selected field index is not in the correct + // range, return the last page index. + int GetActivePageIndex(int selected_field_index) { + if (selected_field_index < GetNumberOfFields()) + return m_fields[selected_field_index].GetPageIndex(); + return m_last_page_index; + } + + int GetNumberOfPages() { return m_last_page_index + 1; } + + // Factory methods to create and add fields of specific types. + + TextFieldDelegate *AddTextField(const char *label, int width, Point origin, + const char *content) { + TextFieldDelegate *delegate = + new TextFieldDelegate(label, width, origin, content); + FieldDelegateSP delegate_sp = FieldDelegateSP(delegate); + m_fields.push_back(Field(delegate_sp, m_last_page_index)); + return delegate; + } + + IntegerFieldDelegate *AddIntegerField(const char *label, int width, + Point origin, int content) { + IntegerFieldDelegate *delegate = + new IntegerFieldDelegate(label, width, origin, content); + FieldDelegateSP delegate_sp = FieldDelegateSP(delegate); + m_fields.push_back(Field(delegate_sp, m_last_page_index)); + return delegate; + } + + BooleanFieldDelegate *AddBooleanField(const char *label, Point origin, + bool content) { + BooleanFieldDelegate *delegate = + new BooleanFieldDelegate(label, origin, content); + FieldDelegateSP delegate_sp = FieldDelegateSP(delegate); + m_fields.push_back(Field(delegate_sp, m_last_page_index)); + return delegate; + } + + ChoicesFieldDelegate *AddChoicesField(const char *label, int width, + int height, Point origin, + std::vector choices) { + ChoicesFieldDelegate *delegate = + new ChoicesFieldDelegate(label, width, height, origin, choices); + FieldDelegateSP delegate_sp = FieldDelegateSP(delegate); + m_fields.push_back(Field(delegate_sp, m_last_page_index)); + return delegate; + } + + void NewPage() { m_last_page_index++; } + +protected: + bool m_has_error; + std::string m_error; + std::vector m_fields; + // The index of the last page. + int m_last_page_index; +}; + +typedef std::shared_ptr FormDelegateSP; + +class FormWindowDelegate : public WindowDelegate { +public: + FormWindowDelegate(FormDelegateSP &delegate_sp) + : m_delegate_sp(delegate_sp), m_selected_field_index(0) {} + + // A form window is divided into two sections. A body section which is padded + // by one character from every direction and contains the fields. The body can + // have multiple "pages" which are switched automatically as the user selects + // next fields. The number of pages is signified by a number of centered dots + // at the last line of the body section, the active page will have its dot + // highlighted. Additionally a footer section contains the submit button in + // one line and a possible error message in the next line. Finally, a + // horizontal line separates both sections. + // + // ___
_________________________________________________ + // | | + // | | + // | Form elements here. | + // | | + // | ... | + // |-------------------------------------------------------------| + // | [ SUBMIT ] | + // | Error message if it exists. | + // |______________________________________[Press Esc to cancel]__| + // + // The following methods describe this structure in numbers. + + int GetPageIndicatorYLocation(Window &window) { + return window.GetHeight() - 5; + } + + int GetSeparatorYLocation(Window &window) { return window.GetHeight() - 4; } + + int GetButtonYLocation(Window &window) { return window.GetHeight() - 3; } + + void DrawPageIndicators(Window &window) { + int number_of_pages = m_delegate_sp->GetNumberOfPages(); + if (number_of_pages == 1) { + return; + } + int x = (window.GetWidth() - number_of_pages) / 2; + window.MoveCursor(x, GetPageIndicatorYLocation(window)); + int active_page = m_delegate_sp->GetActivePageIndex(m_selected_field_index); + for (int i = 0; i < number_of_pages; i++) { + bool is_active = active_page == i; + if (is_active) + window.AttributeOn(A_REVERSE); + window.PutChar(ACS_BULLET); + if (is_active) + window.AttributeOff(A_REVERSE); + } + } + + bool WindowDelegateDraw(Window &window, bool force) override { + window.Erase(); + + window.DrawTitleBox(window.GetName(), "Press Esc to cancel"); + + // Draw field elements. + m_delegate_sp->FormDelegateDraw(window, m_selected_field_index); + + // Draw a horizontal line separating the fields and the submit button. + window.MoveCursor(1, GetSeparatorYLocation(window)); + window.HorizontalLine(window.GetWidth() - 2); + + DrawPageIndicators(window); + + // Draw the centered submit button. + const char *button_text = "[Submit]"; + int x = (window.GetWidth() - sizeof(button_text) - 1) / 2; + window.MoveCursor(x, GetButtonYLocation(window)); + if (IsButtonActive()) + window.AttributeOn(A_REVERSE); + window.PutCString(button_text); + if (IsButtonActive()) + window.AttributeOff(A_REVERSE); + + // Draw the error if it exists. + if (m_delegate_sp->HasError()) { + window.MoveCursor(2, window.GetHeight() - 2); + window.AttributeOn(COLOR_PAIR(RedOnBlack)); + window.PutChar(ACS_DIAMOND); + window.PutChar(' '); + window.PutCStringTruncated(1, m_delegate_sp->GetError().c_str()); + window.AttributeOff(COLOR_PAIR(RedOnBlack)); + } + + return true; + } + + // The index can be equal to the number of fields, hence the plus one. See + // IsButtonActive(). + void SelectedNextField() { + m_selected_field_index++; + int number_of_fields = m_delegate_sp->GetNumberOfFields(); + m_selected_field_index %= number_of_fields + 1; + } + + void SubmitForm(Window &window) { + bool is_successful = m_delegate_sp->FormDelegateSubmit(); + if (is_successful) + window.GetParent()->RemoveSubWindow(&window); + } + + HandleCharResult WindowDelegateHandleChar(Window &window, int key) override { + switch (key) { + case '\r': + case '\n': + case KEY_ENTER: + if (IsButtonActive()) { + SubmitForm(window); + return eKeyHandled; + } + break; + case '\t': + SelectedNextField(); + return eKeyHandled; + case KEY_ESCAPE: + window.GetParent()->RemoveSubWindow(&window); + return eKeyHandled; + default: + break; + } + + // If the key wasn't handled and one of the fields is active, pass the key + // to that field. + if (!IsButtonActive()) { + return m_delegate_sp->FormDelegateHandleChar(m_selected_field_index, key); + } + + return eKeyNotHandled; + } + + // When the selected field index is equal to the number of selected fields, + // this denotes that the submit button is selected. + bool IsButtonActive() { + int number_of_fields = m_delegate_sp->GetNumberOfFields(); + return m_selected_field_index == number_of_fields; + } + +protected: + FormDelegateSP m_delegate_sp; + // The index of the selected field. This can be equal to the number of fields, + // in which case, it denotes that the submit button is selected. + int m_selected_field_index; +}; + class MenuDelegate { public: virtual ~MenuDelegate() = default;