//============================================================================ // // SSSS tt lll lll // SS SS tt ll ll // SS tttttt eeee ll ll aaaa // SSSS tt ee ee ll ll aa // SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" // SS SS tt ee ll ll aa aa // SSSS ttt eeeee llll llll aaaaa // // Copyright (c) 1995-2017 by Bradford W. Mott, Stephen Anthony // and the Stella Team // // See the file "License.txt" for information on usage and redistribution of // this file, and for a DISCLAIMER OF ALL WARRANTIES. //============================================================================ #include #include #include #include "ScrollBarWidget.hxx" #include "FrameBuffer.hxx" #include "EventHandler.hxx" #include "Version.hxx" #include "Debugger.hxx" #include "DebuggerDialog.hxx" #include "DebuggerParser.hxx" #include "PromptWidget.hxx" #include "CartDebug.hxx" #define PROMPT "> " // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PromptWidget::PromptWidget(GuiObject* boss, const GUI::Font& font, int x, int y, int w, int h) : Widget(boss, font, x, y, w - kScrollBarWidth, h), CommandSender(boss), _historyIndex(0), _makeDirty(false), _firstTime(true), _exitedEarly(false) { _flags = WIDGET_ENABLED | WIDGET_CLEARBG | WIDGET_RETAIN_FOCUS | WIDGET_WANTS_TAB | WIDGET_WANTS_RAWDATA; _textcolor = kTextColor; _bgcolor = kWidColor; _kConsoleCharWidth = font.getMaxCharWidth(); _kConsoleCharHeight = font.getFontHeight(); _kConsoleLineHeight = _kConsoleCharHeight + 2; // Calculate depending values _lineWidth = (_w - kScrollBarWidth - 2) / _kConsoleCharWidth; _linesPerPage = (_h - 2) / _kConsoleLineHeight; _linesInBuffer = kBufferSize / _lineWidth; // Add scrollbar _scrollBar = new ScrollBarWidget(boss, font, _x + _w, _y, kScrollBarWidth, _h); _scrollBar->setTarget(this); // Init colors _inverse = false; clearScreen(); addFocusWidget(this); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::drawWidget(bool hilite) { //cerr << "PromptWidget::drawWidget\n"; uInt32 fgcolor, bgcolor; FBSurface& s = _boss->dialog().surface(); // Draw text int start = _scrollLine - _linesPerPage + 1; int y = _y + 2; for (int line = 0; line < _linesPerPage; ++line) { int x = _x + 1; for (int column = 0; column < _lineWidth; ++column) { int c = buffer((start + line) * _lineWidth + column); if(c & (1 << 17)) // inverse video flag { fgcolor = _bgcolor; bgcolor = (c & 0x1ffff) >> 8; s.fillRect(x, y, _kConsoleCharWidth, _kConsoleCharHeight, bgcolor); } else fgcolor = c >> 8; s.drawChar(_font, c & 0x7f, x, y, fgcolor); x += _kConsoleCharWidth; } y += _kConsoleLineHeight; } // Draw the caret drawCaret(); // Draw the scrollbar _scrollBar->draw(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::handleMouseDown(int x, int y, int button, int clickCount) { // cerr << "PromptWidget::handleMouseDown\n"; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::handleMouseWheel(int x, int y, int direction) { _scrollBar->handleMouseWheel(x, y, direction); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::printPrompt() { string watches = instance().debugger().showWatches(); if(watches.length() > 0) print(watches); print(PROMPT); _promptStartPos = _promptEndPos = _currentPos; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool PromptWidget::handleText(char text) { if(text >= 0) { // FIXME - convert this class to inherit from EditableWidget for(int i = _promptEndPos - 1; i >= _currentPos; i--) buffer(i + 1) = buffer(i); _promptEndPos++; putcharIntern(text); scrollToCurrent(); } return true; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool PromptWidget::handleKeyDown(StellaKey key, StellaMod mod) { bool handled = true; bool dirty = false; switch(key) { case KBDK_RETURN: case KBDK_KP_ENTER: { nextLine(); assert(_promptEndPos >= _promptStartPos); int len = _promptEndPos - _promptStartPos; if (len > 0) { // Copy the user input to command string command; for (int i = 0; i < len; i++) command += buffer(_promptStartPos + i) & 0x7f; // Add the input to the history addToHistory(command.c_str()); // Pass the command to the debugger, and print the result string result = instance().debugger().run(command); // This is a bit of a hack // Certain commands remove the debugger dialog from underneath us, // so we shouldn't print any messages // Those commands will return '_EXIT_DEBUGGER' as their result if(result == "_EXIT_DEBUGGER") { _exitedEarly = true; return true; } else if(result != "") print(result + "\n"); } printPrompt(); dirty = true; break; } case KBDK_TAB: { // Tab completion: we complete either commands or labels, but not // both at once. if(_currentPos <= _promptStartPos) break; scrollToCurrent(); int len = _promptEndPos - _promptStartPos; if(len > 255) len = 255; int lastDelimPos = -1; char delimiter = '\0'; char str[256]; for (int i = 0; i < len; i++) { str[i] = buffer(_promptStartPos + i) & 0x7f; // whitespace characters if(strchr("{*@<> =[]()+-/&|!^~%", str[i]) != NULL ) { lastDelimPos = i; delimiter = str[i]; } } str[len] = '\0'; StringList list; string completionList; string prefix; if(lastDelimPos < 0) { // no delimiters, do command completion: const DebuggerParser& parser = instance().debugger().parser(); parser.getCompletions(str, list); if(list.size() < 1) break; sort(list.begin(), list.end()); completionList = list[0]; for(uInt32 i = 1; i < list.size(); ++i) completionList += " " + list[i]; prefix = getCompletionPrefix(list); } else { // Special case for 'help' command if(BSPF::startsWithIgnoreCase(str, "help")) { instance().debugger().parser().getCompletions(str + lastDelimPos + 1, list); } else { // we got a delimiter, so this must be a label or a function const Debugger& dbg = instance().debugger(); dbg.cartDebug().getCompletions(str + lastDelimPos + 1, list); dbg.getCompletions(str + lastDelimPos + 1, list); } if(list.size() < 1) break; sort(list.begin(), list.end()); completionList = list[0]; for(uInt32 i = 1; i < list.size(); ++i) completionList += " " + list[i]; prefix = getCompletionPrefix(list); } if(list.size() == 1) { // add to buffer as though user typed it (plus a space) _currentPos = _promptStartPos + lastDelimPos + 1; const char* clptr = completionList.c_str(); while(*clptr != '\0') putcharIntern(*clptr++); putcharIntern(' '); _promptEndPos = _currentPos; } else { nextLine(); // add to buffer as-is, then add PROMPT plus whatever we have so far _currentPos = _promptStartPos + lastDelimPos + 1; print("\n"); print(completionList); print("\n"); print(PROMPT); _promptStartPos = _currentPos; for(int i = 0; i < lastDelimPos; i++) putcharIntern(str[i]); if(lastDelimPos > 0) putcharIntern(delimiter); print(prefix); _promptEndPos = _currentPos; } dirty = true; break; } case KBDK_BACKSPACE: if (_currentPos > _promptStartPos) killChar(-1); scrollToCurrent(); dirty = true; break; case KBDK_DELETE: killChar(+1); dirty = true; break; case KBDK_PAGEUP: if (instance().eventHandler().kbdShift(mod)) { // Don't scroll up when at top of buffer if(_scrollLine < _linesPerPage) break; _scrollLine -= _linesPerPage - 1; if (_scrollLine < _firstLineInBuffer + _linesPerPage - 1) _scrollLine = _firstLineInBuffer + _linesPerPage - 1; updateScrollBuffer(); dirty = true; } break; case KBDK_PAGEDOWN: if (instance().eventHandler().kbdShift(mod)) { // Don't scroll down when at bottom of buffer if(_scrollLine >= _promptEndPos / _lineWidth) break; _scrollLine += _linesPerPage - 1; if (_scrollLine > _promptEndPos / _lineWidth) _scrollLine = _promptEndPos / _lineWidth; updateScrollBuffer(); dirty = true; } break; case KBDK_HOME: if (instance().eventHandler().kbdShift(mod)) { _scrollLine = _firstLineInBuffer + _linesPerPage - 1; updateScrollBuffer(); } else _currentPos = _promptStartPos; dirty = true; break; case KBDK_END: if (instance().eventHandler().kbdShift(mod)) { _scrollLine = _promptEndPos / _lineWidth; if (_scrollLine < _linesPerPage - 1) _scrollLine = _linesPerPage - 1; updateScrollBuffer(); } else _currentPos = _promptEndPos; dirty = true; break; case KBDK_UP: if (instance().eventHandler().kbdShift(mod)) { if(_scrollLine <= _firstLineInBuffer + _linesPerPage - 1) break; _scrollLine -= 1; updateScrollBuffer(); dirty = true; } else historyScroll(+1); break; case KBDK_DOWN: if (instance().eventHandler().kbdShift(mod)) { // Don't scroll down when at bottom of buffer if(_scrollLine >= _promptEndPos / _lineWidth) break; _scrollLine += 1; updateScrollBuffer(); dirty = true; } else historyScroll(-1); break; case KBDK_RIGHT: if (_currentPos < _promptEndPos) _currentPos++; dirty = true; break; case KBDK_LEFT: if (_currentPos > _promptStartPos) _currentPos--; dirty = true; break; default: if (instance().eventHandler().kbdControl(mod)) { specialKeys(key); } else if (instance().eventHandler().kbdAlt(mod)) { // Placeholder only - this will never be reached } else handled = false; break; } // Take care of changes made above if(dirty) setDirty(); // There are times when we want the prompt and scrollbar to be marked // as dirty *after* they've been drawn above. One such occurrence is // when we issue a command that indirectly redraws the entire parent // dialog (such as 'scanline' or 'frame'). // In those cases, the return code of the command must be shown, but the // entire dialog contents are redrawn at a later time. So the prompt and // scrollbar won't be redrawn unless they're dirty again. if(_makeDirty) { setDirty(); _scrollBar->setDirty(); _makeDirty = false; } return handled; } #if 0 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::insertIntoPrompt(const char* str) { Int32 l = (Int32)strlen(str); for(Int32 i = _promptEndPos - 1; i >= _currentPos; i--) buffer(i + l) = buffer(i); for(Int32 j = 0; j < l; ++j) { _promptEndPos++; putcharIntern(str[j]); } } #endif // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::handleCommand(CommandSender* sender, int cmd, int data, int id) { switch (cmd) { case GuiObject::kSetPositionCmd: int newPos = int(data) + _linesPerPage - 1 + _firstLineInBuffer; if (newPos != _scrollLine) { _scrollLine = newPos; setDirty(); } break; } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::loadConfig() { // See logic at the end of handleKeyDown for an explanation of this _makeDirty = true; // Show the prompt the first time we draw this widget if(_firstTime) { // Display greetings & prompt string version = string("Stella ") + STELLA_VERSION + "\n"; print(version); print(PROMPT); // Take care of one-time debugger stuff // fill the history from the saved breaks, traps and watches commands StringList history; print(instance().debugger().autoExec(&history)); for(uInt32 i = 0; i < history.size(); i++) { addToHistory(history[i].c_str()); } history.clear(); print(instance().debugger().cartDebug().loadConfigFile() + "\n"); print(instance().debugger().cartDebug().loadListFile() + "\n"); print(instance().debugger().cartDebug().loadSymbolFile() + "\n"); print(PROMPT); _promptStartPos = _promptEndPos = _currentPos; _firstTime = false; _exitedEarly = false; } else if(_exitedEarly) { printPrompt(); _exitedEarly = false; } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - int PromptWidget::getWidth() const { return _w + kScrollBarWidth; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::specialKeys(StellaKey key) { bool handled = true; switch(key) { case KBDK_D: killChar(+1); break; case KBDK_K: killLine(+1); break; case KBDK_U: killLine(-1); break; case KBDK_W: killLastWord(); break; case KBDK_A: textSelectAll(); break; case KBDK_X: textCut(); break; case KBDK_C: textCopy(); break; case KBDK_V: textPaste(); break; default: handled = false; break; } if(handled) setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::killChar(int direction) { if(direction == -1) // Delete previous character (backspace) { if(_currentPos <= _promptStartPos) return; _currentPos--; for (int i = _currentPos; i < _promptEndPos; i++) buffer(i) = buffer(i + 1); buffer(_promptEndPos) = ' '; _promptEndPos--; } else if(direction == 1) // Delete next character (delete) { if(_currentPos >= _promptEndPos) return; // There are further characters to the right of cursor if(_currentPos + 1 <= _promptEndPos) { for (int i = _currentPos; i < _promptEndPos; i++) buffer(i) = buffer(i + 1); buffer(_promptEndPos) = ' '; _promptEndPos--; } } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::killLine(int direction) { if(direction == -1) // erase from current position to beginning of line { int count = _currentPos - _promptStartPos; if(count > 0) for (int i = 0; i < count; i++) killChar(-1); } else if(direction == 1) // erase from current position to end of line { for (int i = _currentPos; i < _promptEndPos; i++) buffer(i) = ' '; _promptEndPos = _currentPos; } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::killLastWord() { int cnt = 0; bool space = true; while (_currentPos > _promptStartPos) { if ((buffer(_currentPos - 1) & 0xff) == ' ') { if (!space) break; } else space = false; _currentPos--; cnt++; } for (int i = _currentPos; i < _promptEndPos; i++) buffer(i) = buffer(i + cnt); buffer(_promptEndPos) = ' '; _promptEndPos -= cnt; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::textSelectAll() { } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::textCut() { } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::textCopy() { } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::textPaste() { } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::addToHistory(const char* str) { strncpy(_history[_historyIndex], str, kLineBufferSize-1); _historyIndex = (_historyIndex + 1) % kHistorySize; _historyLine = 0; if (_historySize < kHistorySize) _historySize++; } #if 0 // FIXME // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - int PromptWidget::compareHistory(const char *histLine) { return 1; } #endif // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::historyScroll(int direction) { if (_historySize == 0) return; if (_historyLine == 0 && direction > 0) { int i; for (i = 0; i < _promptEndPos - _promptStartPos; i++) _history[_historyIndex][i] = buffer(_promptStartPos + i); _history[_historyIndex][i] = '\0'; } // Advance to the next line in the history int line = _historyLine + direction; if ((direction < 0 && line < 0) || (direction > 0 && line > _historySize)) return; // If they press arrow-up with anything in the buffer, search backwards // in the history. /* if(direction < 0 && _currentPos > _promptStartPos) { for(;line > 0; line--) { if(compareHistory(_history[line]) == 0) break; } } */ _historyLine = line; // Remove the current user text _currentPos = _promptStartPos; killLine(1); // to end of line // ... and ensure the prompt is visible scrollToCurrent(); // Print the text from the history int idx; if (_historyLine > 0) idx = (_historyIndex - _historyLine + _historySize) % _historySize; else idx = _historyIndex; for (int i = 0; i < kLineBufferSize && _history[idx][i] != '\0'; i++) putcharIntern(_history[idx][i]); _promptEndPos = _currentPos; // Ensure once more the caret is visible (in case of very long history entries) scrollToCurrent(); setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::nextLine() { // Reset colors every line, so I don't have to remember to do it myself _textcolor = kTextColor; _inverse = false; int line = _currentPos / _lineWidth; if (line == _scrollLine) _scrollLine++; _currentPos = (line + 1) * _lineWidth; updateScrollBuffer(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Call this (at least) when the current line changes or when a new line is added void PromptWidget::updateScrollBuffer() { int lastchar = std::max(_promptEndPos, _currentPos); int line = lastchar / _lineWidth; int numlines = (line < _linesInBuffer) ? line + 1 : _linesInBuffer; int firstline = line - numlines + 1; if (firstline > _firstLineInBuffer) { // clear old line from buffer for (int i = lastchar; i < (line+1) * _lineWidth; ++i) buffer(i) = ' '; _firstLineInBuffer = firstline; } _scrollBar->_numEntries = numlines; _scrollBar->_currentPos = _scrollBar->_numEntries - (line - _scrollLine + _linesPerPage); _scrollBar->_entriesPerPage = _linesPerPage; _scrollBar->recalc(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - int PromptWidget::printf(const char* format, ...) { va_list argptr; va_start(argptr, format); int count = this->vprintf(format, argptr); va_end (argptr); return count; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - int PromptWidget::vprintf(const char* format, va_list argptr) { char buf[2048]; // Note: generates warnings about 'nonliteral' format int count = std::vsnprintf(buf, sizeof(buf), format, argptr); print(buf); return count; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::putcharIntern(int c) { if (c == '\n') nextLine(); else if(c & 0x80) { // set foreground color to TIA color // don't print or advance cursor // there are only 128 TIA colors, but // OverlayColor contains 256 of them _textcolor = (c & 0x7f) << 1; } else if(c && c < 0x1e) { // first actual character is large dash // More colors (the regular GUI ones) _textcolor = c + 0x100; } else if(c == 0x7f) { // toggle inverse video (DEL char) _inverse = !_inverse; } else if(isprint(c)) { buffer(_currentPos) = c | (_textcolor << 8) | (_inverse << 17); _currentPos++; if ((_scrollLine + 1) * _lineWidth == _currentPos) { _scrollLine++; updateScrollBuffer(); } } setDirty(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::print(const string& str) { for(char c: str) putcharIntern(c); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::drawCaret() { //cerr << "PromptWidget::drawCaret()\n"; FBSurface& s = _boss->dialog().surface(); int line = _currentPos / _lineWidth; // Don't draw the cursor if it's not in the current view if(_scrollLine < line) return; int displayLine = line - _scrollLine + _linesPerPage - 1; int x = _x + 1 + (_currentPos % _lineWidth) * _kConsoleCharWidth; int y = _y + displayLine * _kConsoleLineHeight; char c = buffer(_currentPos); s.fillRect(x, y, _kConsoleCharWidth, _kConsoleLineHeight, kTextColor); s.drawChar(_font, c, x, y + 2, kBGColor); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::scrollToCurrent() { int line = _promptEndPos / _lineWidth; if (line + _linesPerPage <= _scrollLine) { // TODO - this should only occur for long edit lines, though } else if (line > _scrollLine) { _scrollLine = line; updateScrollBuffer(); } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - bool PromptWidget::saveBuffer(const FilesystemNode& file) { ofstream out(file.getPath()); if(!out.is_open()) return false; for(int start = 0; start < _promptStartPos; start += _lineWidth) { int end = start + _lineWidth - 1; // Look for first non-space, printing char from end of line while( char(_buffer[end] & 0xff) <= ' ' && end >= start) end--; // Spit out the line minus its trailing junk // Strip off any color/inverse bits for(int j = start; j <= end; ++j) out << char(_buffer[j] & 0xff); // add a \n out << endl; } return true; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - string PromptWidget::getCompletionPrefix(const StringList& completions) { // Find the largest match at the beginning of the completions provided for(int len = 1;; len++) { for(const auto& s1 : completions) { if(s1.length() < len) { return s1.substr(0, len - 1); } string find = s1.substr(0, len); for(const auto& s2 : completions) { if(!BSPF::matches(s2, find)) { return s1.substr(0, len - 1); } } } } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void PromptWidget::clearScreen() { // Initialize start position _currentPos = 0; _scrollLine = _linesPerPage - 1; _firstLineInBuffer = 0; _promptStartPos = _promptEndPos = -1; memset(_buffer, 0, kBufferSize * sizeof(int)); if(!_firstTime) updateScrollBuffer(); }