| //===-- IOHandler.cpp -------------------------------------------*- C++ -*-===// |
| // |
| // The LLVM Compiler Infrastructure |
| // |
| // This file is distributed under the University of Illinois Open Source |
| // License. See LICENSE.TXT for details. |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "lldb/Core/IOHandler.h" |
| |
| // C Includes |
| #ifndef LLDB_DISABLE_CURSES |
| #include <curses.h> |
| #include <panel.h> |
| #endif |
| |
| // C++ Includes |
| #if defined(__APPLE__) |
| #include <deque> |
| #endif |
| #include <string> |
| |
| // Other libraries and framework includes |
| // Project includes |
| #include "lldb/Core/Debugger.h" |
| #include "lldb/Core/StreamFile.h" |
| #include "lldb/Host/File.h" // for File |
| #include "lldb/Host/Predicate.h" // for Predicate, ::eBroad... |
| #include "lldb/Utility/Status.h" // for Status |
| #include "lldb/Utility/StreamString.h" // for StreamString |
| #include "lldb/Utility/StringList.h" // for StringList |
| #include "lldb/lldb-forward.h" // for StreamFileSP |
| |
| #ifndef LLDB_DISABLE_LIBEDIT |
| #include "lldb/Host/Editline.h" |
| #endif |
| #include "lldb/Interpreter/CommandCompletions.h" |
| #include "lldb/Interpreter/CommandInterpreter.h" |
| #ifndef LLDB_DISABLE_CURSES |
| #include "lldb/Breakpoint/BreakpointLocation.h" |
| #include "lldb/Core/Module.h" |
| #include "lldb/Core/State.h" |
| #include "lldb/Core/ValueObject.h" |
| #include "lldb/Core/ValueObjectRegister.h" |
| #include "lldb/Symbol/Block.h" |
| #include "lldb/Symbol/Function.h" |
| #include "lldb/Symbol/Symbol.h" |
| #include "lldb/Symbol/VariableList.h" |
| #include "lldb/Target/Process.h" |
| #include "lldb/Target/RegisterContext.h" |
| #include "lldb/Target/StackFrame.h" |
| #include "lldb/Target/StopInfo.h" |
| #include "lldb/Target/Target.h" |
| #include "lldb/Target/Thread.h" |
| #endif |
| |
| #include "llvm/ADT/StringRef.h" // for StringRef |
| |
| #ifdef _MSC_VER |
| #include "lldb/Host/windows/windows.h" |
| #endif |
| |
| #include <memory> // for shared_ptr |
| #include <mutex> // for recursive_mutex |
| |
| #include <assert.h> // for assert |
| #include <ctype.h> // for isspace |
| #include <errno.h> // for EINTR, errno |
| #include <locale.h> // for setlocale |
| #include <stdint.h> // for uint32_t, UINT32_MAX |
| #include <stdio.h> // for size_t, fprintf, feof |
| #include <string.h> // for strlen |
| #include <type_traits> // for move |
| |
| using namespace lldb; |
| using namespace lldb_private; |
| |
| IOHandler::IOHandler(Debugger &debugger, IOHandler::Type type) |
| : IOHandler(debugger, type, |
| StreamFileSP(), // Adopt STDIN from top input reader |
| StreamFileSP(), // Adopt STDOUT from top input reader |
| StreamFileSP(), // Adopt STDERR from top input reader |
| 0) // Flags |
| {} |
| |
| IOHandler::IOHandler(Debugger &debugger, IOHandler::Type type, |
| const lldb::StreamFileSP &input_sp, |
| const lldb::StreamFileSP &output_sp, |
| const lldb::StreamFileSP &error_sp, uint32_t flags) |
| : m_debugger(debugger), m_input_sp(input_sp), m_output_sp(output_sp), |
| m_error_sp(error_sp), m_popped(false), m_flags(flags), m_type(type), |
| m_user_data(nullptr), m_done(false), m_active(false) { |
| // If any files are not specified, then adopt them from the top input reader. |
| if (!m_input_sp || !m_output_sp || !m_error_sp) |
| debugger.AdoptTopIOHandlerFilesIfInvalid(m_input_sp, m_output_sp, |
| m_error_sp); |
| } |
| |
| IOHandler::~IOHandler() = default; |
| |
| int IOHandler::GetInputFD() { |
| return (m_input_sp ? m_input_sp->GetFile().GetDescriptor() : -1); |
| } |
| |
| int IOHandler::GetOutputFD() { |
| return (m_output_sp ? m_output_sp->GetFile().GetDescriptor() : -1); |
| } |
| |
| int IOHandler::GetErrorFD() { |
| return (m_error_sp ? m_error_sp->GetFile().GetDescriptor() : -1); |
| } |
| |
| FILE *IOHandler::GetInputFILE() { |
| return (m_input_sp ? m_input_sp->GetFile().GetStream() : nullptr); |
| } |
| |
| FILE *IOHandler::GetOutputFILE() { |
| return (m_output_sp ? m_output_sp->GetFile().GetStream() : nullptr); |
| } |
| |
| FILE *IOHandler::GetErrorFILE() { |
| return (m_error_sp ? m_error_sp->GetFile().GetStream() : nullptr); |
| } |
| |
| StreamFileSP &IOHandler::GetInputStreamFile() { return m_input_sp; } |
| |
| StreamFileSP &IOHandler::GetOutputStreamFile() { return m_output_sp; } |
| |
| StreamFileSP &IOHandler::GetErrorStreamFile() { return m_error_sp; } |
| |
| bool IOHandler::GetIsInteractive() { |
| return GetInputStreamFile()->GetFile().GetIsInteractive(); |
| } |
| |
| bool IOHandler::GetIsRealTerminal() { |
| return GetInputStreamFile()->GetFile().GetIsRealTerminal(); |
| } |
| |
| void IOHandler::SetPopped(bool b) { m_popped.SetValue(b, eBroadcastOnChange); } |
| |
| void IOHandler::WaitForPop() { m_popped.WaitForValueEqualTo(true); } |
| |
| void IOHandlerStack::PrintAsync(Stream *stream, const char *s, size_t len) { |
| if (stream) { |
| std::lock_guard<std::recursive_mutex> guard(m_mutex); |
| if (m_top) |
| m_top->PrintAsync(stream, s, len); |
| } |
| } |
| |
| IOHandlerConfirm::IOHandlerConfirm(Debugger &debugger, llvm::StringRef prompt, |
| bool default_response) |
| : IOHandlerEditline( |
| debugger, IOHandler::Type::Confirm, |
| nullptr, // nullptr editline_name means no history loaded/saved |
| llvm::StringRef(), // No prompt |
| llvm::StringRef(), // No continuation prompt |
| false, // Multi-line |
| false, // Don't colorize the prompt (i.e. the confirm message.) |
| 0, *this), |
| m_default_response(default_response), m_user_response(default_response) { |
| StreamString prompt_stream; |
| prompt_stream.PutCString(prompt); |
| if (m_default_response) |
| prompt_stream.Printf(": [Y/n] "); |
| else |
| prompt_stream.Printf(": [y/N] "); |
| |
| SetPrompt(prompt_stream.GetString()); |
| } |
| |
| IOHandlerConfirm::~IOHandlerConfirm() = default; |
| |
| int IOHandlerConfirm::IOHandlerComplete(IOHandler &io_handler, |
| const char *current_line, |
| const char *cursor, |
| const char *last_char, |
| int skip_first_n_matches, |
| int max_matches, StringList &matches) { |
| if (current_line == cursor) { |
| if (m_default_response) { |
| matches.AppendString("y"); |
| } else { |
| matches.AppendString("n"); |
| } |
| } |
| return matches.GetSize(); |
| } |
| |
| void IOHandlerConfirm::IOHandlerInputComplete(IOHandler &io_handler, |
| std::string &line) { |
| if (line.empty()) { |
| // User just hit enter, set the response to the default |
| m_user_response = m_default_response; |
| io_handler.SetIsDone(true); |
| return; |
| } |
| |
| if (line.size() == 1) { |
| switch (line[0]) { |
| case 'y': |
| case 'Y': |
| m_user_response = true; |
| io_handler.SetIsDone(true); |
| return; |
| case 'n': |
| case 'N': |
| m_user_response = false; |
| io_handler.SetIsDone(true); |
| return; |
| default: |
| break; |
| } |
| } |
| |
| if (line == "yes" || line == "YES" || line == "Yes") { |
| m_user_response = true; |
| io_handler.SetIsDone(true); |
| } else if (line == "no" || line == "NO" || line == "No") { |
| m_user_response = false; |
| io_handler.SetIsDone(true); |
| } |
| } |
| |
| int IOHandlerDelegate::IOHandlerComplete(IOHandler &io_handler, |
| const char *current_line, |
| const char *cursor, |
| const char *last_char, |
| int skip_first_n_matches, |
| int max_matches, StringList &matches) { |
| switch (m_completion) { |
| case Completion::None: |
| break; |
| |
| case Completion::LLDBCommand: |
| return io_handler.GetDebugger().GetCommandInterpreter().HandleCompletion( |
| current_line, cursor, last_char, skip_first_n_matches, max_matches, |
| matches); |
| |
| case Completion::Expression: { |
| CompletionRequest request(current_line, current_line - cursor, |
| skip_first_n_matches, max_matches, matches); |
| CommandCompletions::InvokeCommonCompletionCallbacks( |
| io_handler.GetDebugger().GetCommandInterpreter(), |
| CommandCompletions::eVariablePathCompletion, request, nullptr); |
| |
| size_t num_matches = request.GetNumberOfMatches(); |
| if (num_matches > 0) { |
| std::string common_prefix; |
| matches.LongestCommonPrefix(common_prefix); |
| const size_t partial_name_len = request.GetCursorArgumentPrefix().size(); |
| |
| // If we matched a unique single command, add a space... Only do this if |
| // the completer told us this was a complete word, however... |
| if (num_matches == 1 && request.GetWordComplete()) { |
| common_prefix.push_back(' '); |
| } |
| common_prefix.erase(0, partial_name_len); |
| matches.InsertStringAtIndex(0, std::move(common_prefix)); |
| } |
| return num_matches; |
| } break; |
| } |
| |
| return 0; |
| } |
| |
| IOHandlerEditline::IOHandlerEditline( |
| Debugger &debugger, IOHandler::Type type, |
| const char *editline_name, // Used for saving history files |
| llvm::StringRef prompt, llvm::StringRef continuation_prompt, |
| bool multi_line, bool color_prompts, uint32_t line_number_start, |
| IOHandlerDelegate &delegate) |
| : IOHandlerEditline(debugger, type, |
| StreamFileSP(), // Inherit input from top input reader |
| StreamFileSP(), // Inherit output from top input reader |
| StreamFileSP(), // Inherit error from top input reader |
| 0, // Flags |
| editline_name, // Used for saving history files |
| prompt, continuation_prompt, multi_line, color_prompts, |
| line_number_start, delegate) {} |
| |
| IOHandlerEditline::IOHandlerEditline( |
| Debugger &debugger, IOHandler::Type type, |
| const lldb::StreamFileSP &input_sp, const lldb::StreamFileSP &output_sp, |
| const lldb::StreamFileSP &error_sp, uint32_t flags, |
| const char *editline_name, // Used for saving history files |
| llvm::StringRef prompt, llvm::StringRef continuation_prompt, |
| bool multi_line, bool color_prompts, uint32_t line_number_start, |
| IOHandlerDelegate &delegate) |
| : IOHandler(debugger, type, input_sp, output_sp, error_sp, flags), |
| #ifndef LLDB_DISABLE_LIBEDIT |
| m_editline_ap(), |
| #endif |
| m_delegate(delegate), m_prompt(), m_continuation_prompt(), |
| m_current_lines_ptr(nullptr), m_base_line_number(line_number_start), |
| m_curr_line_idx(UINT32_MAX), m_multi_line(multi_line), |
| m_color_prompts(color_prompts), m_interrupt_exits(true), |
| m_editing(false) { |
| SetPrompt(prompt); |
| |
| #ifndef LLDB_DISABLE_LIBEDIT |
| bool use_editline = false; |
| |
| use_editline = m_input_sp->GetFile().GetIsRealTerminal(); |
| |
| if (use_editline) { |
| m_editline_ap.reset(new Editline(editline_name, GetInputFILE(), |
| GetOutputFILE(), GetErrorFILE(), |
| m_color_prompts)); |
| m_editline_ap->SetIsInputCompleteCallback(IsInputCompleteCallback, this); |
| m_editline_ap->SetAutoCompleteCallback(AutoCompleteCallback, this); |
| // See if the delegate supports fixing indentation |
| const char *indent_chars = delegate.IOHandlerGetFixIndentationCharacters(); |
| if (indent_chars) { |
| // The delegate does support indentation, hook it up so when any |
| // indentation character is typed, the delegate gets a chance to fix it |
| m_editline_ap->SetFixIndentationCallback(FixIndentationCallback, this, |
| indent_chars); |
| } |
| } |
| #endif |
| SetBaseLineNumber(m_base_line_number); |
| SetPrompt(prompt); |
| SetContinuationPrompt(continuation_prompt); |
| } |
| |
| IOHandlerEditline::~IOHandlerEditline() { |
| #ifndef LLDB_DISABLE_LIBEDIT |
| m_editline_ap.reset(); |
| #endif |
| } |
| |
| void IOHandlerEditline::Activate() { |
| IOHandler::Activate(); |
| m_delegate.IOHandlerActivated(*this); |
| } |
| |
| void IOHandlerEditline::Deactivate() { |
| IOHandler::Deactivate(); |
| m_delegate.IOHandlerDeactivated(*this); |
| } |
| |
| bool IOHandlerEditline::GetLine(std::string &line, bool &interrupted) { |
| #ifndef LLDB_DISABLE_LIBEDIT |
| if (m_editline_ap) { |
| return m_editline_ap->GetLine(line, interrupted); |
| } else { |
| #endif |
| line.clear(); |
| |
| FILE *in = GetInputFILE(); |
| if (in) { |
| if (GetIsInteractive()) { |
| const char *prompt = nullptr; |
| |
| if (m_multi_line && m_curr_line_idx > 0) |
| prompt = GetContinuationPrompt(); |
| |
| if (prompt == nullptr) |
| prompt = GetPrompt(); |
| |
| if (prompt && prompt[0]) { |
| FILE *out = GetOutputFILE(); |
| if (out) { |
| ::fprintf(out, "%s", prompt); |
| ::fflush(out); |
| } |
| } |
| } |
| char buffer[256]; |
| bool done = false; |
| bool got_line = false; |
| m_editing = true; |
| while (!done) { |
| if (fgets(buffer, sizeof(buffer), in) == nullptr) { |
| const int saved_errno = errno; |
| if (feof(in)) |
| done = true; |
| else if (ferror(in)) { |
| if (saved_errno != EINTR) |
| done = true; |
| } |
| } else { |
| got_line = true; |
| size_t buffer_len = strlen(buffer); |
| assert(buffer[buffer_len] == '\0'); |
| char last_char = buffer[buffer_len - 1]; |
| if (last_char == '\r' || last_char == '\n') { |
| done = true; |
| // Strip trailing newlines |
| while (last_char == '\r' || last_char == '\n') { |
| --buffer_len; |
| if (buffer_len == 0) |
| break; |
| last_char = buffer[buffer_len - 1]; |
| } |
| } |
| line.append(buffer, buffer_len); |
| } |
| } |
| m_editing = false; |
| // We might have gotten a newline on a line by itself make sure to return |
| // true in this case. |
| return got_line; |
| } else { |
| // No more input file, we are done... |
| SetIsDone(true); |
| } |
| return false; |
| #ifndef LLDB_DISABLE_LIBEDIT |
| } |
| #endif |
| } |
| |
| #ifndef LLDB_DISABLE_LIBEDIT |
| bool IOHandlerEditline::IsInputCompleteCallback(Editline *editline, |
| StringList &lines, |
| void *baton) { |
| IOHandlerEditline *editline_reader = (IOHandlerEditline *)baton; |
| return editline_reader->m_delegate.IOHandlerIsInputComplete(*editline_reader, |
| lines); |
| } |
| |
| int IOHandlerEditline::FixIndentationCallback(Editline *editline, |
| const StringList &lines, |
| int cursor_position, |
| void *baton) { |
| IOHandlerEditline *editline_reader = (IOHandlerEditline *)baton; |
| return editline_reader->m_delegate.IOHandlerFixIndentation( |
| *editline_reader, lines, cursor_position); |
| } |
| |
| int IOHandlerEditline::AutoCompleteCallback(const char *current_line, |
| const char *cursor, |
| const char *last_char, |
| int skip_first_n_matches, |
| int max_matches, |
| StringList &matches, void *baton) { |
| IOHandlerEditline *editline_reader = (IOHandlerEditline *)baton; |
| if (editline_reader) |
| return editline_reader->m_delegate.IOHandlerComplete( |
| *editline_reader, current_line, cursor, last_char, skip_first_n_matches, |
| max_matches, matches); |
| return 0; |
| } |
| #endif |
| |
| const char *IOHandlerEditline::GetPrompt() { |
| #ifndef LLDB_DISABLE_LIBEDIT |
| if (m_editline_ap) { |
| return m_editline_ap->GetPrompt(); |
| } else { |
| #endif |
| if (m_prompt.empty()) |
| return nullptr; |
| #ifndef LLDB_DISABLE_LIBEDIT |
| } |
| #endif |
| return m_prompt.c_str(); |
| } |
| |
| bool IOHandlerEditline::SetPrompt(llvm::StringRef prompt) { |
| m_prompt = prompt; |
| |
| #ifndef LLDB_DISABLE_LIBEDIT |
| if (m_editline_ap) |
| m_editline_ap->SetPrompt(m_prompt.empty() ? nullptr : m_prompt.c_str()); |
| #endif |
| return true; |
| } |
| |
| const char *IOHandlerEditline::GetContinuationPrompt() { |
| return (m_continuation_prompt.empty() ? nullptr |
| : m_continuation_prompt.c_str()); |
| } |
| |
| void IOHandlerEditline::SetContinuationPrompt(llvm::StringRef prompt) { |
| m_continuation_prompt = prompt; |
| |
| #ifndef LLDB_DISABLE_LIBEDIT |
| if (m_editline_ap) |
| m_editline_ap->SetContinuationPrompt(m_continuation_prompt.empty() |
| ? nullptr |
| : m_continuation_prompt.c_str()); |
| #endif |
| } |
| |
| void IOHandlerEditline::SetBaseLineNumber(uint32_t line) { |
| m_base_line_number = line; |
| } |
| |
| uint32_t IOHandlerEditline::GetCurrentLineIndex() const { |
| #ifndef LLDB_DISABLE_LIBEDIT |
| if (m_editline_ap) |
| return m_editline_ap->GetCurrentLine(); |
| #endif |
| return m_curr_line_idx; |
| } |
| |
| bool IOHandlerEditline::GetLines(StringList &lines, bool &interrupted) { |
| m_current_lines_ptr = &lines; |
| |
| bool success = false; |
| #ifndef LLDB_DISABLE_LIBEDIT |
| if (m_editline_ap) { |
| return m_editline_ap->GetLines(m_base_line_number, lines, interrupted); |
| } else { |
| #endif |
| bool done = false; |
| Status error; |
| |
| while (!done) { |
| // Show line numbers if we are asked to |
| std::string line; |
| if (m_base_line_number > 0 && GetIsInteractive()) { |
| FILE *out = GetOutputFILE(); |
| if (out) |
| ::fprintf(out, "%u%s", m_base_line_number + (uint32_t)lines.GetSize(), |
| GetPrompt() == nullptr ? " " : ""); |
| } |
| |
| m_curr_line_idx = lines.GetSize(); |
| |
| bool interrupted = false; |
| if (GetLine(line, interrupted) && !interrupted) { |
| lines.AppendString(line); |
| done = m_delegate.IOHandlerIsInputComplete(*this, lines); |
| } else { |
| done = true; |
| } |
| } |
| success = lines.GetSize() > 0; |
| #ifndef LLDB_DISABLE_LIBEDIT |
| } |
| #endif |
| return success; |
| } |
| |
| // Each IOHandler gets to run until it is done. It should read data from the |
| // "in" and place output into "out" and "err and return when done. |
| void IOHandlerEditline::Run() { |
| std::string line; |
| while (IsActive()) { |
| bool interrupted = false; |
| if (m_multi_line) { |
| StringList lines; |
| if (GetLines(lines, interrupted)) { |
| if (interrupted) { |
| m_done = m_interrupt_exits; |
| m_delegate.IOHandlerInputInterrupted(*this, line); |
| |
| } else { |
| line = lines.CopyList(); |
| m_delegate.IOHandlerInputComplete(*this, line); |
| } |
| } else { |
| m_done = true; |
| } |
| } else { |
| if (GetLine(line, interrupted)) { |
| if (interrupted) |
| m_delegate.IOHandlerInputInterrupted(*this, line); |
| else |
| m_delegate.IOHandlerInputComplete(*this, line); |
| } else { |
| m_done = true; |
| } |
| } |
| } |
| } |
| |
| void IOHandlerEditline::Cancel() { |
| #ifndef LLDB_DISABLE_LIBEDIT |
| if (m_editline_ap) |
| m_editline_ap->Cancel(); |
| #endif |
| } |
| |
| bool IOHandlerEditline::Interrupt() { |
| // Let the delgate handle it first |
| if (m_delegate.IOHandlerInterrupt(*this)) |
| return true; |
| |
| #ifndef LLDB_DISABLE_LIBEDIT |
| if (m_editline_ap) |
| return m_editline_ap->Interrupt(); |
| #endif |
| return false; |
| } |
| |
| void IOHandlerEditline::GotEOF() { |
| #ifndef LLDB_DISABLE_LIBEDIT |
| if (m_editline_ap) |
| m_editline_ap->Interrupt(); |
| #endif |
| } |
| |
| void IOHandlerEditline::PrintAsync(Stream *stream, const char *s, size_t len) { |
| #ifndef LLDB_DISABLE_LIBEDIT |
| if (m_editline_ap) |
| m_editline_ap->PrintAsync(stream, s, len); |
| else |
| #endif |
| { |
| #ifdef _MSC_VER |
| const char *prompt = GetPrompt(); |
| if (prompt) { |
| // Back up over previous prompt using Windows API |
| CONSOLE_SCREEN_BUFFER_INFO screen_buffer_info; |
| HANDLE console_handle = GetStdHandle(STD_OUTPUT_HANDLE); |
| GetConsoleScreenBufferInfo(console_handle, &screen_buffer_info); |
| COORD coord = screen_buffer_info.dwCursorPosition; |
| coord.X -= strlen(prompt); |
| if (coord.X < 0) |
| coord.X = 0; |
| SetConsoleCursorPosition(console_handle, coord); |
| } |
| #endif |
| IOHandler::PrintAsync(stream, s, len); |
| #ifdef _MSC_VER |
| if (prompt) |
| IOHandler::PrintAsync(GetOutputStreamFile().get(), prompt, |
| strlen(prompt)); |
| #endif |
| } |
| } |
| |
| // we may want curses to be disabled for some builds for instance, windows |
| #ifndef LLDB_DISABLE_CURSES |
| |
| #define KEY_RETURN 10 |
| #define KEY_ESCAPE 27 |
| |
| namespace curses { |
| class Menu; |
| class MenuDelegate; |
| class Window; |
| class WindowDelegate; |
| typedef std::shared_ptr<Menu> MenuSP; |
| typedef std::shared_ptr<MenuDelegate> MenuDelegateSP; |
| typedef std::shared_ptr<Window> WindowSP; |
| typedef std::shared_ptr<WindowDelegate> WindowDelegateSP; |
| typedef std::vector<MenuSP> Menus; |
| typedef std::vector<WindowSP> Windows; |
| typedef std::vector<WindowDelegateSP> WindowDelegates; |
| |
| #if 0 |
| type summary add -s "x=${var.x}, y=${var.y}" curses::Point |
| type summary add -s "w=${var.width}, h=${var.height}" curses::Size |
| type summary add -s "${var.origin%S} ${var.size%S}" curses::Rect |
| #endif |
| |
| struct Point { |
| int x; |
| int y; |
| |
| Point(int _x = 0, int _y = 0) : x(_x), y(_y) {} |
| |
| void Clear() { |
| x = 0; |
| y = 0; |
| } |
| |
| Point &operator+=(const Point &rhs) { |
| x += rhs.x; |
| y += rhs.y; |
| return *this; |
| } |
| |
| void Dump() { printf("(x=%i, y=%i)\n", x, y); } |
| }; |
| |
| bool operator==(const Point &lhs, const Point &rhs) { |
| return lhs.x == rhs.x && lhs.y == rhs.y; |
| } |
| |
| bool operator!=(const Point &lhs, const Point &rhs) { |
| return lhs.x != rhs.x || lhs.y != rhs.y; |
| } |
| |
| struct Size { |
| int width; |
| int height; |
| Size(int w = 0, int h = 0) : width(w), height(h) {} |
| |
| void Clear() { |
| width = 0; |
| height = 0; |
| } |
| |
| void Dump() { printf("(w=%i, h=%i)\n", width, height); } |
| }; |
| |
| bool operator==(const Size &lhs, const Size &rhs) { |
| return lhs.width == rhs.width && lhs.height == rhs.height; |
| } |
| |
| bool operator!=(const Size &lhs, const Size &rhs) { |
| return lhs.width != rhs.width || lhs.height != rhs.height; |
| } |
| |
| struct Rect { |
| Point origin; |
| Size size; |
| |
| Rect() : origin(), size() {} |
| |
| Rect(const Point &p, const Size &s) : origin(p), size(s) {} |
| |
| void Clear() { |
| origin.Clear(); |
| size.Clear(); |
| } |
| |
| void Dump() { |
| printf("(x=%i, y=%i), w=%i, h=%i)\n", origin.x, origin.y, size.width, |
| size.height); |
| } |
| |
| void Inset(int w, int h) { |
| if (size.width > w * 2) |
| size.width -= w * 2; |
| origin.x += w; |
| |
| if (size.height > h * 2) |
| size.height -= h * 2; |
| origin.y += h; |
| } |
| |
| // Return a status bar rectangle which is the last line of this rectangle. |
| // This rectangle will be modified to not include the status bar area. |
| Rect MakeStatusBar() { |
| Rect status_bar; |
| if (size.height > 1) { |
| status_bar.origin.x = origin.x; |
| status_bar.origin.y = size.height; |
| status_bar.size.width = size.width; |
| status_bar.size.height = 1; |
| --size.height; |
| } |
| return status_bar; |
| } |
| |
| // Return a menubar rectangle which is the first line of this rectangle. This |
| // rectangle will be modified to not include the menubar area. |
| Rect MakeMenuBar() { |
| Rect menubar; |
| if (size.height > 1) { |
| menubar.origin.x = origin.x; |
| menubar.origin.y = origin.y; |
| menubar.size.width = size.width; |
| menubar.size.height = 1; |
| ++origin.y; |
| --size.height; |
| } |
| return menubar; |
| } |
| |
| void HorizontalSplitPercentage(float top_percentage, Rect &top, |
| Rect &bottom) const { |
| float top_height = top_percentage * size.height; |
| HorizontalSplit(top_height, top, bottom); |
| } |
| |
| void HorizontalSplit(int top_height, Rect &top, Rect &bottom) const { |
| top = *this; |
| if (top_height < size.height) { |
| top.size.height = top_height; |
| bottom.origin.x = origin.x; |
| bottom.origin.y = origin.y + top.size.height; |
| bottom.size.width = size.width; |
| bottom.size.height = size.height - top.size.height; |
| } else { |
| bottom.Clear(); |
| } |
| } |
| |
| void VerticalSplitPercentage(float left_percentage, Rect &left, |
| Rect &right) const { |
| float left_width = left_percentage * size.width; |
| VerticalSplit(left_width, left, right); |
| } |
| |
| void VerticalSplit(int left_width, Rect &left, Rect &right) const { |
| left = *this; |
| if (left_width < size.width) { |
| left.size.width = left_width; |
| right.origin.x = origin.x + left.size.width; |
| right.origin.y = origin.y; |
| right.size.width = size.width - left.size.width; |
| right.size.height = size.height; |
| } else { |
| right.Clear(); |
| } |
| } |
| }; |
| |
| bool operator==(const Rect &lhs, const Rect &rhs) { |
| return lhs.origin == rhs.origin && lhs.size == rhs.size; |
| } |
| |
| bool operator!=(const Rect &lhs, const Rect &rhs) { |
| return lhs.origin != rhs.origin || lhs.size != rhs.size; |
| } |
| |
| enum HandleCharResult { |
| eKeyNotHandled = 0, |
| eKeyHandled = 1, |
| eQuitApplication = 2 |
| }; |
| |
| enum class MenuActionResult { |
| Handled, |
| NotHandled, |
| Quit // Exit all menus and quit |
| }; |
| |
| struct KeyHelp { |
| int ch; |
| const char *description; |
| }; |
| |
| class WindowDelegate { |
| public: |
| virtual ~WindowDelegate() = default; |
| |
| virtual bool WindowDelegateDraw(Window &window, bool force) { |
| return false; // Drawing not handled |
| } |
| |
| virtual HandleCharResult WindowDelegateHandleChar(Window &window, int key) { |
| return eKeyNotHandled; |
| } |
| |
| virtual const char *WindowDelegateGetHelpText() { return nullptr; } |
| |
| virtual KeyHelp *WindowDelegateGetKeyHelp() { return nullptr; } |
| }; |
| |
| class HelpDialogDelegate : public WindowDelegate { |
| public: |
| HelpDialogDelegate(const char *text, KeyHelp *key_help_array); |
| |
| ~HelpDialogDelegate() override; |
| |
| bool WindowDelegateDraw(Window &window, bool force) override; |
| |
| HandleCharResult WindowDelegateHandleChar(Window &window, int key) override; |
| |
| size_t GetNumLines() const { return m_text.GetSize(); } |
| |
| size_t GetMaxLineLength() const { return m_text.GetMaxStringLength(); } |
| |
| protected: |
| StringList m_text; |
| int m_first_visible_line; |
| }; |
| |
| class Window { |
| public: |
| Window(const char *name) |
| : m_name(name), m_window(nullptr), m_panel(nullptr), m_parent(nullptr), |
| m_subwindows(), m_delegate_sp(), m_curr_active_window_idx(UINT32_MAX), |
| m_prev_active_window_idx(UINT32_MAX), m_delete(false), |
| m_needs_update(true), m_can_activate(true), m_is_subwin(false) {} |
| |
| Window(const char *name, WINDOW *w, bool del = true) |
| : m_name(name), m_window(nullptr), m_panel(nullptr), m_parent(nullptr), |
| m_subwindows(), m_delegate_sp(), m_curr_active_window_idx(UINT32_MAX), |
| m_prev_active_window_idx(UINT32_MAX), m_delete(del), |
| m_needs_update(true), m_can_activate(true), m_is_subwin(false) { |
| if (w) |
| Reset(w); |
| } |
| |
| Window(const char *name, const Rect &bounds) |
| : m_name(name), m_window(nullptr), m_parent(nullptr), m_subwindows(), |
| m_delegate_sp(), m_curr_active_window_idx(UINT32_MAX), |
| m_prev_active_window_idx(UINT32_MAX), m_delete(true), |
| m_needs_update(true), m_can_activate(true), m_is_subwin(false) { |
| Reset(::newwin(bounds.size.height, bounds.size.width, bounds.origin.y, |
| bounds.origin.y)); |
| } |
| |
| virtual ~Window() { |
| RemoveSubWindows(); |
| Reset(); |
| } |
| |
| void Reset(WINDOW *w = nullptr, bool del = true) { |
| if (m_window == w) |
| return; |
| |
| if (m_panel) { |
| ::del_panel(m_panel); |
| m_panel = nullptr; |
| } |
| if (m_window && m_delete) { |
| ::delwin(m_window); |
| m_window = nullptr; |
| m_delete = false; |
| } |
| if (w) { |
| m_window = w; |
| m_panel = ::new_panel(m_window); |
| m_delete = del; |
| } |
| } |
| |
| void AttributeOn(attr_t attr) { ::wattron(m_window, attr); } |
| void AttributeOff(attr_t attr) { ::wattroff(m_window, attr); } |
| void Box(chtype v_char = ACS_VLINE, chtype h_char = ACS_HLINE) { |
| ::box(m_window, v_char, h_char); |
| } |
| void Clear() { ::wclear(m_window); } |
| void Erase() { ::werase(m_window); } |
| Rect GetBounds() { |
| return Rect(GetParentOrigin(), GetSize()); |
| } // Get the rectangle in our parent window |
| int GetChar() { return ::wgetch(m_window); } |
| int GetCursorX() { return getcurx(m_window); } |
| int GetCursorY() { return getcury(m_window); } |
| Rect GetFrame() { |
| return Rect(Point(), GetSize()); |
| } // Get our rectangle in our own coordinate system |
| Point GetParentOrigin() { return Point(GetParentX(), GetParentY()); } |
| Size GetSize() { return Size(GetWidth(), GetHeight()); } |
| int GetParentX() { return getparx(m_window); } |
| int GetParentY() { return getpary(m_window); } |
| int GetMaxX() { return getmaxx(m_window); } |
| int GetMaxY() { return getmaxy(m_window); } |
| int GetWidth() { return GetMaxX(); } |
| int GetHeight() { return GetMaxY(); } |
| void MoveCursor(int x, int y) { ::wmove(m_window, y, x); } |
| void MoveWindow(int x, int y) { MoveWindow(Point(x, y)); } |
| void Resize(int w, int h) { ::wresize(m_window, h, w); } |
| void Resize(const Size &size) { |
| ::wresize(m_window, size.height, size.width); |
| } |
| void PutChar(int ch) { ::waddch(m_window, ch); } |
| void PutCString(const char *s, int len = -1) { ::waddnstr(m_window, s, len); } |
| void Refresh() { ::wrefresh(m_window); } |
| void DeferredRefresh() { |
| // We are using panels, so we don't need to call this... |
| //::wnoutrefresh(m_window); |
| } |
| void SetBackground(int color_pair_idx) { |
| ::wbkgd(m_window, COLOR_PAIR(color_pair_idx)); |
| } |
| void UnderlineOn() { AttributeOn(A_UNDERLINE); } |
| void UnderlineOff() { AttributeOff(A_UNDERLINE); } |
| |
| void PutCStringTruncated(const char *s, int right_pad) { |
| int bytes_left = GetWidth() - GetCursorX(); |
| if (bytes_left > right_pad) { |
| bytes_left -= right_pad; |
| ::waddnstr(m_window, s, bytes_left); |
| } |
| } |
| |
| void MoveWindow(const Point &origin) { |
| const bool moving_window = origin != GetParentOrigin(); |
| if (m_is_subwin && moving_window) { |
| // Can't move subwindows, must delete and re-create |
| Size size = GetSize(); |
| Reset(::subwin(m_parent->m_window, size.height, size.width, origin.y, |
| origin.x), |
| true); |
| } else { |
| ::mvwin(m_window, origin.y, origin.x); |
| } |
| } |
| |
| void SetBounds(const Rect &bounds) { |
| const bool moving_window = bounds.origin != GetParentOrigin(); |
| if (m_is_subwin && moving_window) { |
| // Can't move subwindows, must delete and re-create |
| Reset(::subwin(m_parent->m_window, bounds.size.height, bounds.size.width, |
| bounds.origin.y, bounds.origin.x), |
| true); |
| } else { |
| if (moving_window) |
| MoveWindow(bounds.origin); |
| Resize(bounds.size); |
| } |
| } |
| |
| void Printf(const char *format, ...) __attribute__((format(printf, 2, 3))) { |
| va_list args; |
| va_start(args, format); |
| vwprintw(m_window, format, args); |
| va_end(args); |
| } |
| |
| void Touch() { |
| ::touchwin(m_window); |
| if (m_parent) |
| m_parent->Touch(); |
| } |
| |
| WindowSP CreateSubWindow(const char *name, const Rect &bounds, |
| bool make_active) { |
| WindowSP subwindow_sp; |
| if (m_window) { |
| subwindow_sp.reset(new Window( |
| name, ::subwin(m_window, bounds.size.height, bounds.size.width, |
| bounds.origin.y, bounds.origin.x), |
| true)); |
| subwindow_sp->m_is_subwin = true; |
| } else { |
| subwindow_sp.reset( |
| new Window(name, ::newwin(bounds.size.height, bounds.size.width, |
| bounds.origin.y, bounds.origin.x), |
| true)); |
| subwindow_sp->m_is_subwin = false; |
| } |
| subwindow_sp->m_parent = this; |
| if (make_active) { |
| m_prev_active_window_idx = m_curr_active_window_idx; |
| m_curr_active_window_idx = m_subwindows.size(); |
| } |
| m_subwindows.push_back(subwindow_sp); |
| ::top_panel(subwindow_sp->m_panel); |
| m_needs_update = true; |
| return subwindow_sp; |
| } |
| |
| bool RemoveSubWindow(Window *window) { |
| Windows::iterator pos, end = m_subwindows.end(); |
| size_t i = 0; |
| for (pos = m_subwindows.begin(); pos != end; ++pos, ++i) { |
| if ((*pos).get() == window) { |
| if (m_prev_active_window_idx == i) |
| m_prev_active_window_idx = UINT32_MAX; |
| else if (m_prev_active_window_idx != UINT32_MAX && |
| m_prev_active_window_idx > i) |
| --m_prev_active_window_idx; |
| |
| if (m_curr_active_window_idx == i) |
| m_curr_active_window_idx = UINT32_MAX; |
| else if (m_curr_active_window_idx != UINT32_MAX && |
| m_curr_active_window_idx > i) |
| --m_curr_active_window_idx; |
| window->Erase(); |
| m_subwindows.erase(pos); |
| m_needs_update = true; |
| if (m_parent) |
| m_parent->Touch(); |
| else |
| ::touchwin(stdscr); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| WindowSP FindSubWindow(const char *name) { |
| Windows::iterator pos, end = m_subwindows.end(); |
| size_t i = 0; |
| for (pos = m_subwindows.begin(); pos != end; ++pos, ++i) { |
| if ((*pos)->m_name.compare(name) == 0) |
| return *pos; |
| } |
| return WindowSP(); |
| } |
| |
| void RemoveSubWindows() { |
| m_curr_active_window_idx = UINT32_MAX; |
| m_prev_active_window_idx = UINT32_MAX; |
| for (Windows::iterator pos = m_subwindows.begin(); |
| pos != m_subwindows.end(); pos = m_subwindows.erase(pos)) { |
| (*pos)->Erase(); |
| } |
| if (m_parent) |
| m_parent->Touch(); |
| else |
| ::touchwin(stdscr); |
| } |
| |
| WINDOW *get() { return m_window; } |
| |
| operator WINDOW *() { return m_window; } |
| |
| //---------------------------------------------------------------------- |
| // Window drawing utilities |
| //---------------------------------------------------------------------- |
| void DrawTitleBox(const char *title, const char *bottom_message = nullptr) { |
| attr_t attr = 0; |
| if (IsActive()) |
| attr = A_BOLD | COLOR_PAIR(2); |
| else |
| attr = 0; |
| if (attr) |
| AttributeOn(attr); |
| |
| Box(); |
| MoveCursor(3, 0); |
| |
| if (title && title[0]) { |
| PutChar('<'); |
| PutCString(title); |
| PutChar('>'); |
| } |
| |
| if (bottom_message && bottom_message[0]) { |
| int bottom_message_length = strlen(bottom_message); |
| int x = GetWidth() - 3 - (bottom_message_length + 2); |
| |
| if (x > 0) { |
| MoveCursor(x, GetHeight() - 1); |
| PutChar('['); |
| PutCString(bottom_message); |
| PutChar(']'); |
| } else { |
| MoveCursor(1, GetHeight() - 1); |
| PutChar('['); |
| PutCStringTruncated(bottom_message, 1); |
| } |
| } |
| if (attr) |
| AttributeOff(attr); |
| } |
| |
| virtual void Draw(bool force) { |
| if (m_delegate_sp && m_delegate_sp->WindowDelegateDraw(*this, force)) |
| return; |
| |
| for (auto &subwindow_sp : m_subwindows) |
| subwindow_sp->Draw(force); |
| } |
| |
| bool CreateHelpSubwindow() { |
| if (m_delegate_sp) { |
| const char *text = m_delegate_sp->WindowDelegateGetHelpText(); |
| KeyHelp *key_help = m_delegate_sp->WindowDelegateGetKeyHelp(); |
| if ((text && text[0]) || key_help) { |
| std::unique_ptr<HelpDialogDelegate> help_delegate_ap( |
| new HelpDialogDelegate(text, key_help)); |
| const size_t num_lines = help_delegate_ap->GetNumLines(); |
| const size_t max_length = help_delegate_ap->GetMaxLineLength(); |
| Rect bounds = GetBounds(); |
| bounds.Inset(1, 1); |
| if (max_length + 4 < static_cast<size_t>(bounds.size.width)) { |
| bounds.origin.x += (bounds.size.width - max_length + 4) / 2; |
| bounds.size.width = max_length + 4; |
| } else { |
| if (bounds.size.width > 100) { |
| const int inset_w = bounds.size.width / 4; |
| bounds.origin.x += inset_w; |
| bounds.size.width -= 2 * inset_w; |
| } |
| } |
| |
| if (num_lines + 2 < static_cast<size_t>(bounds.size.height)) { |
| bounds.origin.y += (bounds.size.height - num_lines + 2) / 2; |
| bounds.size.height = num_lines + 2; |
| } else { |
| if (bounds.size.height > 100) { |
| const int inset_h = bounds.size.height / 4; |
| bounds.origin.y += inset_h; |
| bounds.size.height -= 2 * inset_h; |
| } |
| } |
| WindowSP help_window_sp; |
| Window *parent_window = GetParent(); |
| if (parent_window) |
| help_window_sp = parent_window->CreateSubWindow("Help", bounds, true); |
| else |
| help_window_sp = CreateSubWindow("Help", bounds, true); |
| help_window_sp->SetDelegate( |
| WindowDelegateSP(help_delegate_ap.release())); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| virtual HandleCharResult HandleChar(int key) { |
| // Always check the active window first |
| HandleCharResult result = eKeyNotHandled; |
| WindowSP active_window_sp = GetActiveWindow(); |
| if (active_window_sp) { |
| result = active_window_sp->HandleChar(key); |
| if (result != eKeyNotHandled) |
| return result; |
| } |
| |
| if (m_delegate_sp) { |
| result = m_delegate_sp->WindowDelegateHandleChar(*this, key); |
| if (result != eKeyNotHandled) |
| return result; |
| } |
| |
| // Then check for any windows that want any keys that weren't handled. This |
| // is typically only for a menubar. Make a copy of the subwindows in case |
| // any HandleChar() functions muck with the subwindows. If we don't do |
| // this, we can crash when iterating over the subwindows. |
| Windows subwindows(m_subwindows); |
| for (auto subwindow_sp : subwindows) { |
| if (!subwindow_sp->m_can_activate) { |
| HandleCharResult result = subwindow_sp->HandleChar(key); |
| if (result != eKeyNotHandled) |
| return result; |
| } |
| } |
| |
| return eKeyNotHandled; |
| } |
| |
| bool SetActiveWindow(Window *window) { |
| const size_t num_subwindows = m_subwindows.size(); |
| for (size_t i = 0; i < num_subwindows; ++i) { |
| if (m_subwindows[i].get() == window) { |
| m_prev_active_window_idx = m_curr_active_window_idx; |
| ::top_panel(window->m_panel); |
| m_curr_active_window_idx = i; |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| WindowSP GetActiveWindow() { |
| if (!m_subwindows.empty()) { |
| if (m_curr_active_window_idx >= m_subwindows.size()) { |
| if (m_prev_active_window_idx < m_subwindows.size()) { |
| m_curr_active_window_idx = m_prev_active_window_idx; |
| m_prev_active_window_idx = UINT32_MAX; |
| } else if (IsActive()) { |
| m_prev_active_window_idx = UINT32_MAX; |
| m_curr_active_window_idx = UINT32_MAX; |
| |
| // Find first window that wants to be active if this window is active |
| const size_t num_subwindows = m_subwindows.size(); |
| for (size_t i = 0; i < num_subwindows; ++i) { |
| if (m_subwindows[i]->GetCanBeActive()) { |
| m_curr_active_window_idx = i; |
| break; |
| } |
| } |
| } |
| } |
| |
| if (m_curr_active_window_idx < m_subwindows.size()) |
| return m_subwindows[m_curr_active_window_idx]; |
| } |
| return WindowSP(); |
| } |
| |
| bool GetCanBeActive() const { return m_can_activate; } |
| |
| void SetCanBeActive(bool b) { m_can_activate = b; } |
| |
| const WindowDelegateSP &GetDelegate() const { return m_delegate_sp; } |
| |
| void SetDelegate(const WindowDelegateSP &delegate_sp) { |
| m_delegate_sp = delegate_sp; |
| } |
| |
| Window *GetParent() const { return m_parent; } |
| |
| bool IsActive() const { |
| if (m_parent) |
| return m_parent->GetActiveWindow().get() == this; |
| else |
| return true; // Top level window is always active |
| } |
| |
| void SelectNextWindowAsActive() { |
| // Move active focus to next window |
| const size_t num_subwindows = m_subwindows.size(); |
| if (m_curr_active_window_idx == UINT32_MAX) { |
| uint32_t idx = 0; |
| for (auto subwindow_sp : m_subwindows) { |
| if (subwindow_sp->GetCanBeActive()) { |
| m_curr_active_window_idx = idx; |
| break; |
| } |
| ++idx; |
| } |
| } else if (m_curr_active_window_idx + 1 < num_subwindows) { |
| bool handled = false; |
| m_prev_active_window_idx = m_curr_active_window_idx; |
| for (size_t idx = m_curr_active_window_idx + 1; idx < num_subwindows; |
| ++idx) { |
| if (m_subwindows[idx]->GetCanBeActive()) { |
| m_curr_active_window_idx = idx; |
| handled = true; |
| break; |
| } |
| } |
| if (!handled) { |
| for (size_t idx = 0; idx <= m_prev_active_window_idx; ++idx) { |
| if (m_subwindows[idx]->GetCanBeActive()) { |
| m_curr_active_window_idx = idx; |
| break; |
| } |
| } |
| } |
| } else { |
| m_prev_active_window_idx = m_curr_active_window_idx; |
| for (size_t idx = 0; idx < num_subwindows; ++idx) { |
| if (m_subwindows[idx]->GetCanBeActive()) { |
| m_curr_active_window_idx = idx; |
| break; |
| } |
| } |
| } |
| } |
| |
| const char *GetName() const { return m_name.c_str(); } |
| |
| protected: |
| std::string m_name; |
| WINDOW *m_window; |
| PANEL *m_panel; |
| Window *m_parent; |
| Windows m_subwindows; |
| WindowDelegateSP m_delegate_sp; |
| uint32_t m_curr_active_window_idx; |
| uint32_t m_prev_active_window_idx; |
| bool m_delete; |
| bool m_needs_update; |
| bool m_can_activate; |
| bool m_is_subwin; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(Window); |
| }; |
| |
| class MenuDelegate { |
| public: |
| virtual ~MenuDelegate() = default; |
| |
| virtual MenuActionResult MenuDelegateAction(Menu &menu) = 0; |
| }; |
| |
| class Menu : public WindowDelegate { |
| public: |
| enum class Type { Invalid, Bar, Item, Separator }; |
| |
| // Menubar or separator constructor |
| Menu(Type type); |
| |
| // Menuitem constructor |
| Menu(const char *name, const char *key_name, int key_value, |
| uint64_t identifier); |
| |
| ~Menu() override = default; |
| |
| const MenuDelegateSP &GetDelegate() const { return m_delegate_sp; } |
| |
| void SetDelegate(const MenuDelegateSP &delegate_sp) { |
| m_delegate_sp = delegate_sp; |
| } |
| |
| void RecalculateNameLengths(); |
| |
| void AddSubmenu(const MenuSP &menu_sp); |
| |
| int DrawAndRunMenu(Window &window); |
| |
| void DrawMenuTitle(Window &window, bool highlight); |
| |
| bool WindowDelegateDraw(Window &window, bool force) override; |
| |
| HandleCharResult WindowDelegateHandleChar(Window &window, int key) override; |
| |
| MenuActionResult ActionPrivate(Menu &menu) { |
| MenuActionResult result = MenuActionResult::NotHandled; |
| if (m_delegate_sp) { |
| result = m_delegate_sp->MenuDelegateAction(menu); |
| if (result != MenuActionResult::NotHandled) |
| return result; |
| } else if (m_parent) { |
| result = m_parent->ActionPrivate(menu); |
| if (result != MenuActionResult::NotHandled) |
| return result; |
| } |
| return m_canned_result; |
| } |
| |
| MenuActionResult Action() { |
| // Call the recursive action so it can try to handle it with the menu |
| // delegate, and if not, try our parent menu |
| return ActionPrivate(*this); |
| } |
| |
| void SetCannedResult(MenuActionResult result) { m_canned_result = result; } |
| |
| Menus &GetSubmenus() { return m_submenus; } |
| |
| const Menus &GetSubmenus() const { return m_submenus; } |
| |
| int GetSelectedSubmenuIndex() const { return m_selected; } |
| |
| void SetSelectedSubmenuIndex(int idx) { m_selected = idx; } |
| |
| Type GetType() const { return m_type; } |
| |
| int GetStartingColumn() const { return m_start_col; } |
| |
| void SetStartingColumn(int col) { m_start_col = col; } |
| |
| int GetKeyValue() const { return m_key_value; } |
| |
| void SetKeyValue(int key_value) { m_key_value = key_value; } |
| |
| std::string &GetName() { return m_name; } |
| |
| std::string &GetKeyName() { return m_key_name; } |
| |
| int GetDrawWidth() const { |
| return m_max_submenu_name_length + m_max_submenu_key_name_length + 8; |
| } |
| |
| uint64_t GetIdentifier() const { return m_identifier; } |
| |
| void SetIdentifier(uint64_t identifier) { m_identifier = identifier; } |
| |
| protected: |
| std::string m_name; |
| std::string m_key_name; |
| uint64_t m_identifier; |
| Type m_type; |
| int m_key_value; |
| int m_start_col; |
| int m_max_submenu_name_length; |
| int m_max_submenu_key_name_length; |
| int m_selected; |
| Menu *m_parent; |
| Menus m_submenus; |
| WindowSP m_menu_window_sp; |
| MenuActionResult m_canned_result; |
| MenuDelegateSP m_delegate_sp; |
| }; |
| |
| // Menubar or separator constructor |
| Menu::Menu(Type type) |
| : m_name(), m_key_name(), m_identifier(0), m_type(type), m_key_value(0), |
| m_start_col(0), m_max_submenu_name_length(0), |
| m_max_submenu_key_name_length(0), m_selected(0), m_parent(nullptr), |
| m_submenus(), m_canned_result(MenuActionResult::NotHandled), |
| m_delegate_sp() {} |
| |
| // Menuitem constructor |
| Menu::Menu(const char *name, const char *key_name, int key_value, |
| uint64_t identifier) |
| : m_name(), m_key_name(), m_identifier(identifier), m_type(Type::Invalid), |
| m_key_value(key_value), m_start_col(0), m_max_submenu_name_length(0), |
| m_max_submenu_key_name_length(0), m_selected(0), m_parent(nullptr), |
| m_submenus(), m_canned_result(MenuActionResult::NotHandled), |
| m_delegate_sp() { |
| if (name && name[0]) { |
| m_name = name; |
| m_type = Type::Item; |
| if (key_name && key_name[0]) |
| m_key_name = key_name; |
| } else { |
| m_type = Type::Separator; |
| } |
| } |
| |
| void Menu::RecalculateNameLengths() { |
| m_max_submenu_name_length = 0; |
| m_max_submenu_key_name_length = 0; |
| Menus &submenus = GetSubmenus(); |
| const size_t num_submenus = submenus.size(); |
| for (size_t i = 0; i < num_submenus; ++i) { |
| Menu *submenu = submenus[i].get(); |
| if (static_cast<size_t>(m_max_submenu_name_length) < submenu->m_name.size()) |
| m_max_submenu_name_length = submenu->m_name.size(); |
| if (static_cast<size_t>(m_max_submenu_key_name_length) < |
| submenu->m_key_name.size()) |
| m_max_submenu_key_name_length = submenu->m_key_name.size(); |
| } |
| } |
| |
| void Menu::AddSubmenu(const MenuSP &menu_sp) { |
| menu_sp->m_parent = this; |
| if (static_cast<size_t>(m_max_submenu_name_length) < menu_sp->m_name.size()) |
| m_max_submenu_name_length = menu_sp->m_name.size(); |
| if (static_cast<size_t>(m_max_submenu_key_name_length) < |
| menu_sp->m_key_name.size()) |
| m_max_submenu_key_name_length = menu_sp->m_key_name.size(); |
| m_submenus.push_back(menu_sp); |
| } |
| |
| void Menu::DrawMenuTitle(Window &window, bool highlight) { |
| if (m_type == Type::Separator) { |
| window.MoveCursor(0, window.GetCursorY()); |
| window.PutChar(ACS_LTEE); |
| int width = window.GetWidth(); |
| if (width > 2) { |
| width -= 2; |
| for (int i = 0; i < width; ++i) |
| window.PutChar(ACS_HLINE); |
| } |
| window.PutChar(ACS_RTEE); |
| } else { |
| const int shortcut_key = m_key_value; |
| bool underlined_shortcut = false; |
| const attr_t hilgight_attr = A_REVERSE; |
| if (highlight) |
| window.AttributeOn(hilgight_attr); |
| if (isprint(shortcut_key)) { |
| size_t lower_pos = m_name.find(tolower(shortcut_key)); |
| size_t upper_pos = m_name.find(toupper(shortcut_key)); |
| const char *name = m_name.c_str(); |
| size_t pos = std::min<size_t>(lower_pos, upper_pos); |
| if (pos != std::string::npos) { |
| underlined_shortcut = true; |
| if (pos > 0) { |
| window.PutCString(name, pos); |
| name += pos; |
| } |
| const attr_t shortcut_attr = A_UNDERLINE | A_BOLD; |
| window.AttributeOn(shortcut_attr); |
| window.PutChar(name[0]); |
| window.AttributeOff(shortcut_attr); |
| name++; |
| if (name[0]) |
| window.PutCString(name); |
| } |
| } |
| |
| if (!underlined_shortcut) { |
| window.PutCString(m_name.c_str()); |
| } |
| |
| if (highlight) |
| window.AttributeOff(hilgight_attr); |
| |
| if (m_key_name.empty()) { |
| if (!underlined_shortcut && isprint(m_key_value)) { |
| window.AttributeOn(COLOR_PAIR(3)); |
| window.Printf(" (%c)", m_key_value); |
| window.AttributeOff(COLOR_PAIR(3)); |
| } |
| } else { |
| window.AttributeOn(COLOR_PAIR(3)); |
| window.Printf(" (%s)", m_key_name.c_str()); |
| window.AttributeOff(COLOR_PAIR(3)); |
| } |
| } |
| } |
| |
| bool Menu::WindowDelegateDraw(Window &window, bool force) { |
| Menus &submenus = GetSubmenus(); |
| const size_t num_submenus = submenus.size(); |
| const int selected_idx = GetSelectedSubmenuIndex(); |
| Menu::Type menu_type = GetType(); |
| switch (menu_type) { |
| case Menu::Type::Bar: { |
| window.SetBackground(2); |
| window.MoveCursor(0, 0); |
| for (size_t i = 0; i < num_submenus; ++i) { |
| Menu *menu = submenus[i].get(); |
| if (i > 0) |
| window.PutChar(' '); |
| menu->SetStartingColumn(window.GetCursorX()); |
| window.PutCString("| "); |
| menu->DrawMenuTitle(window, false); |
| } |
| window.PutCString(" |"); |
| window.DeferredRefresh(); |
| } break; |
| |
| case Menu::Type::Item: { |
| int y = 1; |
| int x = 3; |
| // Draw the menu |
| int cursor_x = 0; |
| int cursor_y = 0; |
| window.Erase(); |
| window.SetBackground(2); |
| window.Box(); |
| for (size_t i = 0; i < num_submenus; ++i) { |
| const bool is_selected = (i == static_cast<size_t>(selected_idx)); |
| window.MoveCursor(x, y + i); |
| if (is_selected) { |
| // Remember where we want the cursor to be |
| cursor_x = x - 1; |
| cursor_y = y + i; |
| } |
| submenus[i]->DrawMenuTitle(window, is_selected); |
| } |
| window.MoveCursor(cursor_x, cursor_y); |
| window.DeferredRefresh(); |
| } break; |
| |
| default: |
| case Menu::Type::Separator: |
| break; |
| } |
| return true; // Drawing handled... |
| } |
| |
| HandleCharResult Menu::WindowDelegateHandleChar(Window &window, int key) { |
| HandleCharResult result = eKeyNotHandled; |
| |
| Menus &submenus = GetSubmenus(); |
| const size_t num_submenus = submenus.size(); |
| const int selected_idx = GetSelectedSubmenuIndex(); |
| Menu::Type menu_type = GetType(); |
| if (menu_type == Menu::Type::Bar) { |
| MenuSP run_menu_sp; |
| switch (key) { |
| case KEY_DOWN: |
| case KEY_UP: |
| // Show last menu or first menu |
| if (selected_idx < static_cast<int>(num_submenus)) |
| run_menu_sp = submenus[selected_idx]; |
| else if (!submenus.empty()) |
| run_menu_sp = submenus.front(); |
| result = eKeyHandled; |
| break; |
| |
| case KEY_RIGHT: |
| ++m_selected; |
| if (m_selected >= static_cast<int>(num_submenus)) |
| m_selected = 0; |
| if (m_selected < static_cast<int>(num_submenus)) |
| run_menu_sp = submenus[m_selected]; |
| else if (!submenus.empty()) |
| run_menu_sp = submenus.front(); |
| result = eKeyHandled; |
| break; |
| |
| case KEY_LEFT: |
| --m_selected; |
| if (m_selected < 0) |
| m_selected = num_submenus - 1; |
| if (m_selected < static_cast<int>(num_submenus)) |
| run_menu_sp = submenus[m_selected]; |
| else if (!submenus.empty()) |
| run_menu_sp = submenus.front(); |
| result = eKeyHandled; |
| break; |
| |
| default: |
| for (size_t i = 0; i < num_submenus; ++i) { |
| if (submenus[i]->GetKeyValue() == key) { |
| SetSelectedSubmenuIndex(i); |
| run_menu_sp = submenus[i]; |
| result = eKeyHandled; |
| break; |
| } |
| } |
| break; |
| } |
| |
| if (run_menu_sp) { |
| // Run the action on this menu in case we need to populate the menu with |
| // dynamic content and also in case check marks, and any other menu |
| // decorations need to be calculated |
| if (run_menu_sp->Action() == MenuActionResult::Quit) |
| return eQuitApplication; |
| |
| Rect menu_bounds; |
| menu_bounds.origin.x = run_menu_sp->GetStartingColumn(); |
| menu_bounds.origin.y = 1; |
| menu_bounds.size.width = run_menu_sp->GetDrawWidth(); |
| menu_bounds.size.height = run_menu_sp->GetSubmenus().size() + 2; |
| if (m_menu_window_sp) |
| window.GetParent()->RemoveSubWindow(m_menu_window_sp.get()); |
| |
| m_menu_window_sp = window.GetParent()->CreateSubWindow( |
| run_menu_sp->GetName().c_str(), menu_bounds, true); |
| m_menu_window_sp->SetDelegate(run_menu_sp); |
| } |
| } else if (menu_type == Menu::Type::Item) { |
| switch (key) { |
| case KEY_DOWN: |
| if (m_submenus.size() > 1) { |
| const int start_select = m_selected; |
| while (++m_selected != start_select) { |
| if (static_cast<size_t>(m_selected) >= num_submenus) |
| m_selected = 0; |
| if (m_submenus[m_selected]->GetType() == Type::Separator) |
| continue; |
| else |
| break; |
| } |
| return eKeyHandled; |
| } |
| break; |
| |
| case KEY_UP: |
| if (m_submenus.size() > 1) { |
| const int start_select = m_selected; |
| while (--m_selected != start_select) { |
| if (m_selected < static_cast<int>(0)) |
| m_selected = num_submenus - 1; |
| if (m_submenus[m_selected]->GetType() == Type::Separator) |
| continue; |
| else |
| break; |
| } |
| return eKeyHandled; |
| } |
| break; |
| |
| case KEY_RETURN: |
| if (static_cast<size_t>(selected_idx) < num_submenus) { |
| if (submenus[selected_idx]->Action() == MenuActionResult::Quit) |
| return eQuitApplication; |
| window.GetParent()->RemoveSubWindow(&window); |
| return eKeyHandled; |
| } |
| break; |
| |
| case KEY_ESCAPE: // Beware: pressing escape key has 1 to 2 second delay in |
| // case other chars are entered for escaped sequences |
| window.GetParent()->RemoveSubWindow(&window); |
| return eKeyHandled; |
| |
| default: |
| for (size_t i = 0; i < num_submenus; ++i) { |
| Menu *menu = submenus[i].get(); |
| if (menu->GetKeyValue() == key) { |
| SetSelectedSubmenuIndex(i); |
| window.GetParent()->RemoveSubWindow(&window); |
| if (menu->Action() == MenuActionResult::Quit) |
| return eQuitApplication; |
| return eKeyHandled; |
| } |
| } |
| break; |
| } |
| } else if (menu_type == Menu::Type::Separator) { |
| } |
| return result; |
| } |
| |
| class Application { |
| public: |
| Application(FILE *in, FILE *out) |
| : m_window_sp(), m_screen(nullptr), m_in(in), m_out(out) {} |
| |
| ~Application() { |
| m_window_delegates.clear(); |
| m_window_sp.reset(); |
| if (m_screen) { |
| ::delscreen(m_screen); |
| m_screen = nullptr; |
| } |
| } |
| |
| void Initialize() { |
| ::setlocale(LC_ALL, ""); |
| ::setlocale(LC_CTYPE, ""); |
| #if 0 |
| ::initscr(); |
| #else |
| m_screen = ::newterm(nullptr, m_out, m_in); |
| #endif |
| ::start_color(); |
| ::curs_set(0); |
| ::noecho(); |
| ::keypad(stdscr, TRUE); |
| } |
| |
| void Terminate() { ::endwin(); } |
| |
| void Run(Debugger &debugger) { |
| bool done = false; |
| int delay_in_tenths_of_a_second = 1; |
| |
| // Alas the threading model in curses is a bit lame so we need to resort to |
| // polling every 0.5 seconds. We could poll for stdin ourselves and then |
| // pass the keys down but then we need to translate all of the escape |
| // sequences ourselves. So we resort to polling for input because we need |
| // to receive async process events while in this loop. |
| |
| halfdelay(delay_in_tenths_of_a_second); // Poll using some number of tenths |
| // of seconds seconds when calling |
| // Window::GetChar() |
| |
| ListenerSP listener_sp( |
| Listener::MakeListener("lldb.IOHandler.curses.Application")); |
| ConstString broadcaster_class_target(Target::GetStaticBroadcasterClass()); |
| ConstString broadcaster_class_process(Process::GetStaticBroadcasterClass()); |
| ConstString broadcaster_class_thread(Thread::GetStaticBroadcasterClass()); |
| debugger.EnableForwardEvents(listener_sp); |
| |
| bool update = true; |
| #if defined(__APPLE__) |
| std::deque<int> escape_chars; |
| #endif |
| |
| while (!done) { |
| if (update) { |
| m_window_sp->Draw(false); |
| // All windows should be calling Window::DeferredRefresh() instead of |
| // Window::Refresh() so we can do a single update and avoid any screen |
| // blinking |
| update_panels(); |
| |
| // Cursor hiding isn't working on MacOSX, so hide it in the top left |
| // corner |
| m_window_sp->MoveCursor(0, 0); |
| |
| doupdate(); |
| update = false; |
| } |
| |
| #if defined(__APPLE__) |
| // Terminal.app doesn't map its function keys correctly, F1-F4 default |
| // to: \033OP, \033OQ, \033OR, \033OS, so lets take care of this here if |
| // possible |
| int ch; |
| if (escape_chars.empty()) |
| ch = m_window_sp->GetChar(); |
| else { |
| ch = escape_chars.front(); |
| escape_chars.pop_front(); |
| } |
| if (ch == KEY_ESCAPE) { |
| int ch2 = m_window_sp->GetChar(); |
| if (ch2 == 'O') { |
| int ch3 = m_window_sp->GetChar(); |
| switch (ch3) { |
| case 'P': |
| ch = KEY_F(1); |
| break; |
| case 'Q': |
| ch = KEY_F(2); |
| break; |
| case 'R': |
| ch = KEY_F(3); |
| break; |
| case 'S': |
| ch = KEY_F(4); |
| break; |
| default: |
| escape_chars.push_back(ch2); |
| if (ch3 != -1) |
| escape_chars.push_back(ch3); |
| break; |
| } |
| } else if (ch2 != -1) |
| escape_chars.push_back(ch2); |
| } |
| #else |
| int ch = m_window_sp->GetChar(); |
| |
| #endif |
| if (ch == -1) { |
| if (feof(m_in) || ferror(m_in)) { |
| done = true; |
| } else { |
| // Just a timeout from using halfdelay(), check for events |
| EventSP event_sp; |
| while (listener_sp->PeekAtNextEvent()) { |
| listener_sp->GetEvent(event_sp, std::chrono::seconds(0)); |
| |
| if (event_sp) { |
| Broadcaster *broadcaster = event_sp->GetBroadcaster(); |
| if (broadcaster) { |
| // uint32_t event_type = event_sp->GetType(); |
| ConstString broadcaster_class( |
| broadcaster->GetBroadcasterClass()); |
| if (broadcaster_class == broadcaster_class_process) { |
| debugger.GetCommandInterpreter().UpdateExecutionContext( |
| nullptr); |
| update = true; |
| continue; // Don't get any key, just update our view |
| } |
| } |
| } |
| } |
| } |
| } else { |
| HandleCharResult key_result = m_window_sp->HandleChar(ch); |
| switch (key_result) { |
| case eKeyHandled: |
| debugger.GetCommandInterpreter().UpdateExecutionContext(nullptr); |
| update = true; |
| break; |
| case eKeyNotHandled: |
| break; |
| case eQuitApplication: |
| done = true; |
| break; |
| } |
| } |
| } |
| |
| debugger.CancelForwardEvents(listener_sp); |
| } |
| |
| WindowSP &GetMainWindow() { |
| if (!m_window_sp) |
| m_window_sp.reset(new Window("main", stdscr, false)); |
| return m_window_sp; |
| } |
| |
| WindowDelegates &GetWindowDelegates() { return m_window_delegates; } |
| |
| protected: |
| WindowSP m_window_sp; |
| WindowDelegates m_window_delegates; |
| SCREEN *m_screen; |
| FILE *m_in; |
| FILE *m_out; |
| }; |
| |
| } // namespace curses |
| |
| using namespace curses; |
| |
| struct Row { |
| ValueObjectManager value; |
| Row *parent; |
| // The process stop ID when the children were calculated. |
| uint32_t children_stop_id; |
| int row_idx; |
| int x; |
| int y; |
| bool might_have_children; |
| bool expanded; |
| bool calculated_children; |
| std::vector<Row> children; |
| |
| Row(const ValueObjectSP &v, Row *p) |
| : value(v, lldb::eDynamicDontRunTarget, true), parent(p), row_idx(0), |
| x(1), y(1), might_have_children(v ? v->MightHaveChildren() : false), |
| expanded(false), calculated_children(false), children() {} |
| |
| size_t GetDepth() const { |
| if (parent) |
| return 1 + parent->GetDepth(); |
| return 0; |
| } |
| |
| void Expand() { |
| expanded = true; |
| } |
| |
| std::vector<Row> &GetChildren() { |
| ProcessSP process_sp = value.GetProcessSP(); |
| auto stop_id = process_sp->GetStopID(); |
| if (process_sp && stop_id != children_stop_id) { |
| children_stop_id = stop_id; |
| calculated_children = false; |
| } |
| if (!calculated_children) { |
| children.clear(); |
| calculated_children = true; |
| ValueObjectSP valobj = value.GetSP(); |
| if (valobj) { |
| const size_t num_children = valobj->GetNumChildren(); |
| for (size_t i = 0; i < num_children; ++i) { |
| children.push_back(Row(valobj->GetChildAtIndex(i, true), this)); |
| } |
| } |
| } |
| return children; |
| } |
| |
| void Unexpand() { |
| expanded = false; |
| calculated_children = false; |
| children.clear(); |
| } |
| |
| void DrawTree(Window &window) { |
| if (parent) |
| parent->DrawTreeForChild(window, this, 0); |
| |
| if (might_have_children) { |
| // It we can get UTF8 characters to work we should try to use the |
| // "symbol" UTF8 string below |
| // const char *symbol = ""; |
| // if (row.expanded) |
| // symbol = "\xe2\x96\xbd "; |
| // else |
| // symbol = "\xe2\x96\xb7 "; |
| // window.PutCString (symbol); |
| |
| // The ACS_DARROW and ACS_RARROW don't look very nice they are just a 'v' |
| // or '>' character... |
| // if (expanded) |
| // window.PutChar (ACS_DARROW); |
| // else |
| // window.PutChar (ACS_RARROW); |
| // Since we can't find any good looking right arrow/down arrow symbols, |
| // just use a diamond... |
| window.PutChar(ACS_DIAMOND); |
| window.PutChar(ACS_HLINE); |
| } |
| } |
| |
| void DrawTreeForChild(Window &window, Row *child, uint32_t reverse_depth) { |
| if (parent) |
| parent->DrawTreeForChild(window, this, reverse_depth + 1); |
| |
| if (&GetChildren().back() == child) { |
| // Last child |
| if (reverse_depth == 0) { |
| window.PutChar(ACS_LLCORNER); |
| window.PutChar(ACS_HLINE); |
| } else { |
| window.PutChar(' '); |
| window.PutChar(' '); |
| } |
| } else { |
| if (reverse_depth == 0) { |
| window.PutChar(ACS_LTEE); |
| window.PutChar(ACS_HLINE); |
| } else { |
| window.PutChar(ACS_VLINE); |
| window.PutChar(' '); |
| } |
| } |
| } |
| }; |
| |
| struct DisplayOptions { |
| bool show_types; |
| }; |
| |
| class TreeItem; |
| |
| class TreeDelegate { |
| public: |
| TreeDelegate() = default; |
| virtual ~TreeDelegate() = default; |
| |
| virtual void TreeDelegateDrawTreeItem(TreeItem &item, Window &window) = 0; |
| virtual void TreeDelegateGenerateChildren(TreeItem &item) = 0; |
| virtual bool TreeDelegateItemSelected( |
| TreeItem &item) = 0; // Return true if we need to update views |
| }; |
| |
| typedef std::shared_ptr<TreeDelegate> TreeDelegateSP; |
| |
| class TreeItem { |
| public: |
| TreeItem(TreeItem *parent, TreeDelegate &delegate, bool might_have_children) |
| : m_parent(parent), m_delegate(delegate), m_user_data(nullptr), |
| m_identifier(0), m_row_idx(-1), m_children(), |
| m_might_have_children(might_have_children), m_is_expanded(false) {} |
| |
| TreeItem &operator=(const TreeItem &rhs) { |
| if (this != &rhs) { |
| m_parent = rhs.m_parent; |
| m_delegate = rhs.m_delegate; |
| m_user_data = rhs.m_user_data; |
| m_identifier = rhs.m_identifier; |
| m_row_idx = rhs.m_row_idx; |
| m_children = rhs.m_children; |
| m_might_have_children = rhs.m_might_have_children; |
| m_is_expanded = rhs.m_is_expanded; |
| } |
| return *this; |
| } |
| |
| size_t GetDepth() const { |
| if (m_parent) |
| return 1 + m_parent->GetDepth(); |
| return 0; |
| } |
| |
| int GetRowIndex() const { return m_row_idx; } |
| |
| void ClearChildren() { m_children.clear(); } |
| |
| void Resize(size_t n, const TreeItem &t) { m_children.resize(n, t); } |
| |
| TreeItem &operator[](size_t i) { return m_children[i]; } |
| |
| void SetRowIndex(int row_idx) { m_row_idx = row_idx; } |
| |
| size_t GetNumChildren() { |
| m_delegate.TreeDelegateGenerateChildren(*this); |
| return m_children.size(); |
| } |
| |
| void ItemWasSelected() { m_delegate.TreeDelegateItemSelected(*this); } |
| |
| void CalculateRowIndexes(int &row_idx) { |
| SetRowIndex(row_idx); |
| ++row_idx; |
| |
| const bool expanded = IsExpanded(); |
| |
| // The root item must calculate its children, or we must calculate the |
| // number of children if the item is expanded |
| if (m_parent == nullptr || expanded) |
| GetNumChildren(); |
| |
| for (auto &item : m_children) { |
| if (expanded) |
| item.CalculateRowIndexes(row_idx); |
| else |
| item.SetRowIndex(-1); |
| } |
| } |
| |
| TreeItem *GetParent() { return m_parent; } |
| |
| bool IsExpanded() const { return m_is_expanded; } |
| |
| void Expand() { m_is_expanded = true; } |
| |
| void Unexpand() { m_is_expanded = false; } |
| |
| bool Draw(Window &window, const int first_visible_row, |
| const uint32_t selected_row_idx, int &row_idx, int &num_rows_left) { |
| if (num_rows_left <= 0) |
| return false; |
| |
| if (m_row_idx >= first_visible_row) { |
| window.MoveCursor(2, row_idx + 1); |
| |
| if (m_parent) |
| m_parent->DrawTreeForChild(window, this, 0); |
| |
| if (m_might_have_children) { |
| // It we can get UTF8 characters to work we should try to use the |
| // "symbol" UTF8 string below |
| // const char *symbol = ""; |
| // if (row.expanded) |
| // symbol = "\xe2\x96\xbd "; |
| // else |
| // symbol = "\xe2\x96\xb7 "; |
| // window.PutCString (symbol); |
| |
| // The ACS_DARROW and ACS_RARROW don't look very nice they are just a |
| // 'v' or '>' character... |
| // if (expanded) |
| // window.PutChar (ACS_DARROW); |
| // else |
| // window.PutChar (ACS_RARROW); |
| // Since we can't find any good looking right arrow/down arrow symbols, |
| // just use a diamond... |
| window.PutChar(ACS_DIAMOND); |
| window.PutChar(ACS_HLINE); |
| } |
| bool highlight = (selected_row_idx == static_cast<size_t>(m_row_idx)) && |
| window.IsActive(); |
| |
| if (highlight) |
| window.AttributeOn(A_REVERSE); |
| |
| m_delegate.TreeDelegateDrawTreeItem(*this, window); |
| |
| if (highlight) |
| window.AttributeOff(A_REVERSE); |
| ++row_idx; |
| --num_rows_left; |
| } |
| |
| if (num_rows_left <= 0) |
| return false; // We are done drawing... |
| |
| if (IsExpanded()) { |
| for (auto &item : m_children) { |
| // If we displayed all the rows and item.Draw() returns false we are |
| // done drawing and can exit this for loop |
| if (!item.Draw(window, first_visible_row, selected_row_idx, row_idx, |
| num_rows_left)) |
| break; |
| } |
| } |
| return num_rows_left >= 0; // Return true if not done drawing yet |
| } |
| |
| void DrawTreeForChild(Window &window, TreeItem *child, |
| uint32_t reverse_depth) { |
| if (m_parent) |
| m_parent->DrawTreeForChild(window, this, reverse_depth + 1); |
| |
| if (&m_children.back() == child) { |
| // Last child |
| if (reverse_depth == 0) { |
| window.PutChar(ACS_LLCORNER); |
| window.PutChar(ACS_HLINE); |
| } else { |
| window.PutChar(' '); |
| window.PutChar(' '); |
| } |
| } else { |
| if (reverse_depth == 0) { |
| window.PutChar(ACS_LTEE); |
| window.PutChar(ACS_HLINE); |
| } else { |
| window.PutChar(ACS_VLINE); |
| window.PutChar(' '); |
| } |
| } |
| } |
| |
| TreeItem *GetItemForRowIndex(uint32_t row_idx) { |
| if (static_cast<uint32_t>(m_row_idx) == row_idx) |
| return this; |
| if (m_children.empty()) |
| return nullptr; |
| if (IsExpanded()) { |
| for (auto &item : m_children) { |
| TreeItem *selected_item_ptr = item.GetItemForRowIndex(row_idx); |
| if (selected_item_ptr) |
| return selected_item_ptr; |
| } |
| } |
| return nullptr; |
| } |
| |
| void *GetUserData() const { return m_user_data; } |
| |
| void SetUserData(void *user_data) { m_user_data = user_data; } |
| |
| uint64_t GetIdentifier() const { return m_identifier; } |
| |
| void SetIdentifier(uint64_t identifier) { m_identifier = identifier; } |
| |
| void SetMightHaveChildren(bool b) { m_might_have_children = b; } |
| |
| protected: |
| TreeItem *m_parent; |
| TreeDelegate &m_delegate; |
| void *m_user_data; |
| uint64_t m_identifier; |
| int m_row_idx; // Zero based visible row index, -1 if not visible or for the |
| // root item |
| std::vector<TreeItem> m_children; |
| bool m_might_have_children; |
| bool m_is_expanded; |
| }; |
| |
| class TreeWindowDelegate : public WindowDelegate { |
| public: |
| TreeWindowDelegate(Debugger &debugger, const TreeDelegateSP &delegate_sp) |
| : m_debugger(debugger), m_delegate_sp(delegate_sp), |
| m_root(nullptr, *delegate_sp, true), m_selected_item(nullptr), |
| m_num_rows(0), m_selected_row_idx(0), m_first_visible_row(0), |
| m_min_x(0), m_min_y(0), m_max_x(0), m_max_y(0) {} |
| |
| int NumVisibleRows() const { return m_max_y - m_min_y; } |
| |
| bool WindowDelegateDraw(Window &window, bool force) override { |
| ExecutionContext exe_ctx( |
| m_debugger.GetCommandInterpreter().GetExecutionContext()); |
| Process *process = exe_ctx.GetProcessPtr(); |
| |
| bool display_content = false; |
| if (process) { |
| StateType state = process->GetState(); |
| if (StateIsStoppedState(state, true)) { |
| // We are stopped, so it is ok to |
| display_content = true; |
| } else if (StateIsRunningState(state)) { |
| return true; // Don't do any updating when we are running |
| } |
| } |
| |
| m_min_x = 2; |
| m_min_y = 1; |
| m_max_x = window.GetWidth() - 1; |
| m_max_y = window.GetHeight() - 1; |
| |
| window.Erase(); |
| window.DrawTitleBox(window.GetName()); |
| |
| if (display_content) { |
| const int num_visible_rows = NumVisibleRows(); |
| m_num_rows = 0; |
| m_root.CalculateRowIndexes(m_num_rows); |
| |
| // If we unexpanded while having something selected our total number of |
| // rows is less than the num visible rows, then make sure we show all the |
| // rows by setting the first visible row accordingly. |
| if (m_first_visible_row > 0 && m_num_rows < num_visible_rows) |
| m_first_visible_row = 0; |
| |
| // Make sure the selected row is always visible |
| if (m_selected_row_idx < m_first_visible_row) |
| m_first_visible_row = m_selected_row_idx; |
| else if (m_first_visible_row + num_visible_rows <= m_selected_row_idx) |
| m_first_visible_row = m_selected_row_idx - num_visible_rows + 1; |
| |
| int row_idx = 0; |
| int num_rows_left = num_visible_rows; |
| m_root.Draw(window, m_first_visible_row, m_selected_row_idx, row_idx, |
| num_rows_left); |
| // Get the selected row |
| m_selected_item = m_root.GetItemForRowIndex(m_selected_row_idx); |
| } else { |
| m_selected_item = nullptr; |
| } |
| |
| window.DeferredRefresh(); |
| |
| return true; // Drawing handled |
| } |
| |
| const char *WindowDelegateGetHelpText() override { |
| return "Thread window keyboard shortcuts:"; |
| } |
| |
| KeyHelp *WindowDelegateGetKeyHelp() override { |
| static curses::KeyHelp g_source_view_key_help[] = { |
| {KEY_UP, "Select previous item"}, |
| {KEY_DOWN, "Select next item"}, |
| {KEY_RIGHT, "Expand the selected item"}, |
| {KEY_LEFT, |
| "Unexpand the selected item or select parent if not expanded"}, |
| {KEY_PPAGE, "Page up"}, |
| {KEY_NPAGE, "Page down"}, |
| {'h', "Show help dialog"}, |
| {' ', "Toggle item expansion"}, |
| {',', "Page up"}, |
| {'.', "Page down"}, |
| {'\0', nullptr}}; |
| return g_source_view_key_help; |
| } |
| |
| HandleCharResult WindowDelegateHandleChar(Window &window, int c) override { |
| switch (c) { |
| case ',': |
| case KEY_PPAGE: |
| // Page up key |
| if (m_first_visible_row > 0) { |
| if (m_first_visible_row > m_max_y) |
| m_first_visible_row -= m_max_y; |
| else |
| m_first_visible_row = 0; |
| m_selected_row_idx = m_first_visible_row; |
| m_selected_item = m_root.GetItemForRowIndex(m_selected_row_idx); |
| if (m_selected_item) |
| m_selected_item->ItemWasSelected(); |
| } |
| return eKeyHandled; |
| |
| case '.': |
| case KEY_NPAGE: |
| // Page down key |
| if (m_num_rows > m_max_y) { |
| if (m_first_visible_row + m_max_y < m_num_rows) { |
| m_first_visible_row += m_max_y; |
| m_selected_row_idx = m_first_visible_row; |
| m_selected_item = m_root.GetItemForRowIndex(m_selected_row_idx); |
| if (m_selected_item) |
| m_selected_item->ItemWasSelected(); |
| } |
| } |
| return eKeyHandled; |
| |
| case KEY_UP: |
| if (m_selected_row_idx > 0) { |
| --m_selected_row_idx; |
| m_selected_item = m_root.GetItemForRowIndex(m_selected_row_idx); |
| if (m_selected_item) |
| m_selected_item->ItemWasSelected(); |
| } |
| return eKeyHandled; |
| |
| case KEY_DOWN: |
| if (m_selected_row_idx + 1 < m_num_rows) { |
| ++m_selected_row_idx; |
| m_selected_item = m_root.GetItemForRowIndex(m_selected_row_idx); |
| if (m_selected_item) |
| m_selected_item->ItemWasSelected(); |
| } |
| return eKeyHandled; |
| |
| case KEY_RIGHT: |
| if (m_selected_item) { |
| if (!m_selected_item->IsExpanded()) |
| m_selected_item->Expand(); |
| } |
| return eKeyHandled; |
| |
| case KEY_LEFT: |
| if (m_selected_item) { |
| if (m_selected_item->IsExpanded()) |
| m_selected_item->Unexpand(); |
| else if (m_selected_item->GetParent()) { |
| m_selected_row_idx = m_selected_item->GetParent()->GetRowIndex(); |
| m_selected_item = m_root.GetItemForRowIndex(m_selected_row_idx); |
| if (m_selected_item) |
| m_selected_item->ItemWasSelected(); |
| } |
| } |
| return eKeyHandled; |
| |
| case ' ': |
| // Toggle expansion state when SPACE is pressed |
| if (m_selected_item) { |
| if (m_selected_item->IsExpanded()) |
| m_selected_item->Unexpand(); |
| else |
| m_selected_item->Expand(); |
| } |
| return eKeyHandled; |
| |
| case 'h': |
| window.CreateHelpSubwindow(); |
| return eKeyHandled; |
| |
| default: |
| break; |
| } |
| return eKeyNotHandled; |
| } |
| |
| protected: |
| Debugger &m_debugger; |
| TreeDelegateSP m_delegate_sp; |
| TreeItem m_root; |
| TreeItem *m_selected_item; |
| int m_num_rows; |
| int m_selected_row_idx; |
| int m_first_visible_row; |
| int m_min_x; |
| int m_min_y; |
| int m_max_x; |
| int m_max_y; |
| }; |
| |
| class FrameTreeDelegate : public TreeDelegate { |
| public: |
| FrameTreeDelegate() : TreeDelegate() { |
| FormatEntity::Parse( |
| "frame #${frame.index}: {${function.name}${function.pc-offset}}}", |
| m_format); |
| } |
| |
| ~FrameTreeDelegate() override = default; |
| |
| void TreeDelegateDrawTreeItem(TreeItem &item, Window &window) override { |
| Thread *thread = (Thread *)item.GetUserData(); |
| if (thread) { |
| const uint64_t frame_idx = item.GetIdentifier(); |
| StackFrameSP frame_sp = thread->GetStackFrameAtIndex(frame_idx); |
| if (frame_sp) { |
| StreamString strm; |
| const SymbolContext &sc = |
| frame_sp->GetSymbolContext(eSymbolContextEverything); |
| ExecutionContext exe_ctx(frame_sp); |
| if (FormatEntity::Format(m_format, strm, &sc, &exe_ctx, nullptr, |
| nullptr, false, false)) { |
| int right_pad = 1; |
| window.PutCStringTruncated(strm.GetString().str().c_str(), right_pad); |
| } |
| } |
| } |
| } |
| |
| void TreeDelegateGenerateChildren(TreeItem &item) override { |
| // No children for frames yet... |
| } |
| |
| bool TreeDelegateItemSelected(TreeItem &item) override { |
| Thread *thread = (Thread *)item.GetUserData(); |
| if (thread) { |
| thread->GetProcess()->GetThreadList().SetSelectedThreadByID( |
| thread->GetID()); |
| const uint64_t frame_idx = item.GetIdentifier(); |
| thread->SetSelectedFrameByIndex(frame_idx); |
| return true; |
| } |
| return false; |
| } |
| |
| protected: |
| FormatEntity::Entry m_format; |
| }; |
| |
| class ThreadTreeDelegate : public TreeDelegate { |
| public: |
| ThreadTreeDelegate(Debugger &debugger) |
| : TreeDelegate(), m_debugger(debugger), m_tid(LLDB_INVALID_THREAD_ID), |
| m_stop_id(UINT32_MAX) { |
| FormatEntity::Parse("thread #${thread.index}: tid = ${thread.id}{, stop " |
| "reason = ${thread.stop-reason}}", |
| m_format); |
| } |
| |
| ~ThreadTreeDelegate() override = default; |
| |
| ProcessSP GetProcess() { |
| return m_debugger.GetCommandInterpreter() |
| .GetExecutionContext() |
| .GetProcessSP(); |
| } |
| |
| ThreadSP GetThread(const TreeItem &item) { |
| ProcessSP process_sp = GetProcess(); |
| if (process_sp) |
| return process_sp->GetThreadList().FindThreadByID(item.GetIdentifier()); |
| return ThreadSP(); |
| } |
| |
| void TreeDelegateDrawTreeItem(TreeItem &item, Window &window) override { |
| ThreadSP thread_sp = GetThread(item); |
| if (thread_sp) { |
| StreamString strm; |
| ExecutionContext exe_ctx(thread_sp); |
| if (FormatEntity::Format(m_format, strm, nullptr, &exe_ctx, nullptr, |
| nullptr, false, false)) { |
| int right_pad = 1; |
| window.PutCStringTruncated(strm.GetString().str().c_str(), right_pad); |
| } |
| } |
| } |
| |
| void TreeDelegateGenerateChildren(TreeItem &item) override { |
| ProcessSP process_sp = GetProcess(); |
| if (process_sp && process_sp->IsAlive()) { |
| StateType state = process_sp->GetState(); |
| if (StateIsStoppedState(state, true)) { |
| ThreadSP thread_sp = GetThread(item); |
| if (thread_sp) { |
| if (m_stop_id == process_sp->GetStopID() && |
| thread_sp->GetID() == m_tid) |
| return; // Children are already up to date |
| if (!m_frame_delegate_sp) { |
| // Always expand the thread item the first time we show it |
| m_frame_delegate_sp.reset(new FrameTreeDelegate()); |
| } |
| |
| m_stop_id = process_sp->GetStopID(); |
| m_tid = thread_sp->GetID(); |
| |
| TreeItem t(&item, *m_frame_delegate_sp, false); |
| size_t num_frames = thread_sp->GetStackFrameCount(); |
| item.Resize(num_frames, t); |
| for (size_t i = 0; i < num_frames; ++i) { |
| item[i].SetUserData(thread_sp.get()); |
| item[i].SetIdentifier(i); |
| } |
| } |
| return; |
| } |
| } |
| item.ClearChildren(); |
| } |
| |
| bool TreeDelegateItemSelected(TreeItem &item) override { |
| ProcessSP process_sp = GetProcess(); |
| if (process_sp && process_sp->IsAlive()) { |
| StateType state = process_sp->GetState(); |
| if (StateIsStoppedState(state, true)) { |
| ThreadSP thread_sp = GetThread(item); |
| if (thread_sp) { |
| ThreadList &thread_list = thread_sp->GetProcess()->GetThreadList(); |
| std::lock_guard<std::recursive_mutex> guard(thread_list.GetMutex()); |
| ThreadSP selected_thread_sp = thread_list.GetSelectedThread(); |
| if (selected_thread_sp->GetID() != thread_sp->GetID()) { |
| thread_list.SetSelectedThreadByID(thread_sp->GetID()); |
| return true; |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| protected: |
| Debugger &m_debugger; |
| std::shared_ptr<FrameTreeDelegate> m_frame_delegate_sp; |
| lldb::user_id_t m_tid; |
| uint32_t m_stop_id; |
| FormatEntity::Entry m_format; |
| }; |
| |
| class ThreadsTreeDelegate : public TreeDelegate { |
| public: |
| ThreadsTreeDelegate(Debugger &debugger) |
| : TreeDelegate(), m_thread_delegate_sp(), m_debugger(debugger), |
| m_stop_id(UINT32_MAX) { |
| FormatEntity::Parse("process ${process.id}{, name = ${process.name}}", |
| m_format); |
| } |
| |
| ~ThreadsTreeDelegate() override = default; |
| |
| ProcessSP GetProcess() { |
| return m_debugger.GetCommandInterpreter() |
| .GetExecutionContext() |
| .GetProcessSP(); |
| } |
| |
| void TreeDelegateDrawTreeItem(TreeItem &item, Window &window) override { |
| ProcessSP process_sp = GetProcess(); |
| if (process_sp && process_sp->IsAlive()) { |
| StreamString strm; |
| ExecutionContext exe_ctx(process_sp); |
| if (FormatEntity::Format(m_format, strm, nullptr, &exe_ctx, nullptr, |
| nullptr, false, false)) { |
| int right_pad = 1; |
| window.PutCStringTruncated(strm.GetString().str().c_str(), right_pad); |
| } |
| } |
| } |
| |
| void TreeDelegateGenerateChildren(TreeItem &item) override { |
| ProcessSP process_sp = GetProcess(); |
| if (process_sp && process_sp->IsAlive()) { |
| StateType state = process_sp->GetState(); |
| if (StateIsStoppedState(state, true)) { |
| const uint32_t stop_id = process_sp->GetStopID(); |
| if (m_stop_id == stop_id) |
| return; // Children are already up to date |
| |
| m_stop_id = stop_id; |
| |
| if (!m_thread_delegate_sp) { |
| // Always expand the thread item the first time we show it |
| // item.Expand(); |
| m_thread_delegate_sp.reset(new ThreadTreeDelegate(m_debugger)); |
| } |
| |
| TreeItem t(&item, *m_thread_delegate_sp, false); |
| ThreadList &threads = process_sp->GetThreadList(); |
| std::lock_guard<std::recursive_mutex> guard(threads.GetMutex()); |
| size_t num_threads = threads.GetSize(); |
| item.Resize(num_threads, t); |
| for (size_t i = 0; i < num_threads; ++i) { |
| item[i].SetIdentifier(threads.GetThreadAtIndex(i)->GetID()); |
| item[i].SetMightHaveChildren(true); |
| } |
| return; |
| } |
| } |
| item.ClearChildren(); |
| } |
| |
| bool TreeDelegateItemSelected(TreeItem &item) override { return false; } |
| |
| protected: |
| std::shared_ptr<ThreadTreeDelegate> m_thread_delegate_sp; |
| Debugger &m_debugger; |
| uint32_t m_stop_id; |
| FormatEntity::Entry m_format; |
| }; |
| |
| class ValueObjectListDelegate : public WindowDelegate { |
| public: |
| ValueObjectListDelegate() |
| : m_rows(), m_selected_row(nullptr), |
| m_selected_row_idx(0), m_first_visible_row(0), m_num_rows(0), |
| m_max_x(0), m_max_y(0) {} |
| |
| ValueObjectListDelegate(ValueObjectList &valobj_list) |
| : m_rows(), m_selected_row(nullptr), |
| m_selected_row_idx(0), m_first_visible_row(0), m_num_rows(0), |
| m_max_x(0), m_max_y(0) { |
| SetValues(valobj_list); |
| } |
| |
| ~ValueObjectListDelegate() override = default; |
| |
| void SetValues(ValueObjectList &valobj_list) { |
| m_selected_row = nullptr; |
| m_selected_row_idx = 0; |
| m_first_visible_row = 0; |
| m_num_rows = 0; |
| m_rows.clear(); |
| for (auto &valobj_sp : valobj_list.GetObjects()) |
| m_rows.push_back(Row(valobj_sp, nullptr)); |
| } |
| |
| bool WindowDelegateDraw(Window &window, bool force) override { |
| m_num_rows = 0; |
| m_min_x = 2; |
| m_min_y = 1; |
| m_max_x = window.GetWidth() - 1; |
| m_max_y = window.GetHeight() - 1; |
| |
| window.Erase(); |
| window.DrawTitleBox(window.GetName()); |
| |
| const int num_visible_rows = NumVisibleRows(); |
| const int num_rows = CalculateTotalNumberRows(m_rows); |
| |
| // If we unexpanded while having something selected our total number of |
| // rows is less than the num visible rows, then make sure we show all the |
| // rows by setting the first visible row accordingly. |
| if (m_first_visible_row > 0 && num_rows < num_visible_rows) |
| m_first_visible_row = 0; |
| |
| // Make sure the selected row is always visible |
| if (m_selected_row_idx < m_first_visible_row) |
| m_first_visible_row = m_selected_row_idx; |
| else if (m_first_visible_row + num_visible_rows <= m_selected_row_idx) |
| m_first_visible_row = m_selected_row_idx - num_visible_rows + 1; |
| |
| DisplayRows(window, m_rows, g_options); |
| |
| window.DeferredRefresh(); |
| |
| // Get the selected row |
| m_selected_row = GetRowForRowIndex(m_selected_row_idx); |
| // Keep the cursor on the selected row so the highlight and the cursor are |
| // always on the same line |
| if (m_selected_row) |
| window.MoveCursor(m_selected_row->x, m_selected_row->y); |
| |
| return true; // Drawing handled |
| } |
| |
| KeyHelp *WindowDelegateGetKeyHelp() override { |
| static curses::KeyHelp g_source_view_key_help[] = { |
| {KEY_UP, "Select previous item"}, |
| {KEY_DOWN, "Select next item"}, |
| {KEY_RIGHT, "Expand selected item"}, |
| {KEY_LEFT, "Unexpand selected item or select parent if not expanded"}, |
| {KEY_PPAGE, "Page up"}, |
| {KEY_NPAGE, "Page down"}, |
| {'A', "Format as annotated address"}, |
| {'b', "Format as binary"}, |
| {'B', "Format as hex bytes with ASCII"}, |
| {'c', "Format as character"}, |
| {'d', "Format as a signed integer"}, |
| {'D', "Format selected value using the default format for the type"}, |
| {'f', "Format as float"}, |
| {'h', "Show help dialog"}, |
| {'i', "Format as instructions"}, |
| {'o', "Format as octal"}, |
| {'p', "Format as pointer"}, |
| {'s', "Format as C string"}, |
| {'t', "Toggle showing/hiding type names"}, |
| {'u', "Format as an unsigned integer"}, |
| {'x', "Format as hex"}, |
| {'X', "Format as uppercase hex"}, |
| {' ', "Toggle item expansion"}, |
| {',', "Page up"}, |
| {'.', "Page down"}, |
| {'\0', nullptr}}; |
| return g_source_view_key_help; |
| } |
| |
| HandleCharResult WindowDelegateHandleChar(Window &window, int c) override { |
| switch (c) { |
| case 'x': |
| case 'X': |
| case 'o': |
| case 's': |
| case 'u': |
| case 'd': |
| case 'D': |
| case 'i': |
| case 'A': |
| case 'p': |
| case 'c': |
| case 'b': |
| case 'B': |
| case 'f': |
| // Change the format for the currently selected item |
| if (m_selected_row) { |
| auto valobj_sp = m_selected_row->value.GetSP(); |
| if (valobj_sp) |
| valobj_sp->SetFormat(FormatForChar(c)); |
| } |
| return eKeyHandled; |
| |
| case 't': |
| // Toggle showing type names |
| g_options.show_types = !g_options.show_types; |
| return eKeyHandled; |
| |
| case ',': |
| case KEY_PPAGE: |
| // Page up key |
| if (m_first_visible_row > 0) { |
| if (static_cast<int>(m_first_visible_row) > m_max_y) |
| m_first_visible_row -= m_max_y; |
| else |
| m_first_visible_row = 0; |
| m_selected_row_idx = m_first_visible_row; |
| } |
| return eKeyHandled; |
| |
| case '.': |
| case KEY_NPAGE: |
| // Page down key |
| if (m_num_rows > static_cast<size_t>(m_max_y)) { |
| if (m_first_visible_row + m_max_y < m_num_rows) { |
| m_first_visible_row += m_max_y; |
| m_selected_row_idx = m_first_visible_row; |
| } |
| } |
| return eKeyHandled; |
| |
| case KEY_UP: |
| if (m_selected_row_idx > 0) |
| --m_selected_row_idx; |
| return eKeyHandled; |
| |
| case KEY_DOWN: |
| if (m_selected_row_idx + 1 < m_num_rows) |
| ++m_selected_row_idx; |
| return eKeyHandled; |
| |
| case KEY_RIGHT: |
| if (m_selected_row) { |
| if (!m_selected_row->expanded) |
| m_selected_row->Expand(); |
| } |
| return eKeyHandled; |
| |
| case KEY_LEFT: |
| if (m_selected_row) { |
| if (m_selected_row->expanded) |
| m_selected_row->Unexpand(); |
| else if (m_selected_row->parent) |
| m_selected_row_idx = m_selected_row->parent->row_idx; |
| } |
| return eKeyHandled; |
| |
| case ' ': |
| // Toggle expansion state when SPACE is pressed |
| if (m_selected_row) { |
| if (m_selected_row->expanded) |
| m_selected_row->Unexpand(); |
| else |
| m_selected_row->Expand(); |
| } |
| return eKeyHandled; |
| |
| case 'h': |
| window.CreateHelpSubwindow(); |
| return eKeyHandled; |
| |
| default: |
| break; |
| } |
| return eKeyNotHandled; |
| } |
| |
| protected: |
| std::vector<Row> m_rows; |
| Row *m_selected_row; |
| uint32_t m_selected_row_idx; |
| uint32_t m_first_visible_row; |
| uint32_t m_num_rows; |
| int m_min_x; |
| int m_min_y; |
|