diff --git a/IConsole/IConsoleTest.cc b/IConsole/IConsoleTest.cc new file mode 100644 index 0000000000..cb5f2dc2a0 --- /dev/null +++ b/IConsole/IConsoleTest.cc @@ -0,0 +1,23 @@ +#include "retro/IConsole.h" +#include + +namespace retro +{ + void InitIConsole(); +} + +int main() +{ + retro::InitIConsole(); + std::string out = "Hello, \033[1mexternal world of true beauty and \033[4mgreatness\033[0m.\nEnter \"exit\" to quit.\n"; + retro::IConsole::currentInstance->write(out.data(), out.size()); + + std::string in; + do + { + in = retro::IConsole::currentInstance->ReadLine(); + out = "You Entered: " + in; + retro::IConsole::currentInstance->write(out.data(), out.size()); + } while(in != "exit\n"); + return 0; +} diff --git a/IConsole/retro/IConsole.cc b/IConsole/retro/IConsole.cc new file mode 100644 index 0000000000..53c4fa4656 --- /dev/null +++ b/IConsole/retro/IConsole.cc @@ -0,0 +1,508 @@ +/* + Copyright 2012 Wolfgang Thaller. + + This file is part of Retro68. + + Retro68 is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Retro68 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Retro68. If not, see . +*/ + +#include "IConsole.h" +#include "MacUtils.h" +#include "Fonts.h" +#include "Processes.h" + +#include + +using namespace retro; + +IConsole *IConsole::currentInstance = NULL; + +namespace +{ + class FontSetup + { + short saveFont, saveSize, saveFace; + public: + FontSetup() + { +#if TARGET_API_MAC_CARBON + GrafPtr port; + GetPort(&port); + saveFont = GetPortTextFont(port); + saveSize = GetPortTextSize(port); +#else + saveFont = qd.thePort->txFont; + saveSize = qd.thePort->txSize; + saveFace = qd.thePort->txFace; +#endif + TextFont(kFontIDMonaco); + TextSize(9); + TextFace(normal); + } + + ~FontSetup() + { + TextFont(saveFont); + TextSize(saveSize); + TextFace(saveFace); + } + }; +} + +IConsole::IConsole() +{ +} + +IConsole::IConsole(GrafPtr port, Rect r) +{ + Init(port, r); +} + +IConsole::~IConsole() +{ + if(currentInstance == this) + currentInstance = NULL; +} + +void IConsole::Init(GrafPtr port, Rect r) +{ + consolePort = port; + bounds = r; + + PortSetter setport(consolePort); + FontSetup fontSetup; + + InsetRect(&bounds, 2,2); + + cellSizeY = 12; + cellSizeX = CharWidth('M'); + + rows = (bounds.bottom - bounds.top) / cellSizeY; + cols = (bounds.right - bounds.left) / cellSizeX; + + chars = std::vector(rows*cols, ' '); + attrs = std::vector(rows*cols); + + onscreen = chars; + + cursorX = cursorY = 0; + isProcessingEscSequence=false; +} + +void IConsole::SetAttributes(Attributes aa) +{ + TextFace(aa.isBold?bold:0 + aa.isUnderline?underline:0); +} + +Rect IConsole::CellRect(short x, short y) +{ + return { (short) (bounds.top + y * cellSizeY), (short) (bounds.left + x * cellSizeX), + (short) (bounds.top + (y+1) * cellSizeY), (short) (bounds.left + (x+1) * cellSizeX) }; +} +void IConsole::DrawCell(short x, short y, bool erase) +{ + Rect r = CellRect(x,y); + + if(cursorDrawn) + { + if(y == cursorY && x == cursorX) + { + erase = true; + cursorDrawn = false; + } + } + + if(erase) + EraseRect(&r); + MoveTo(r.left, r.bottom - 2); + DrawChar(chars[y * cols + x]); +} + +void IConsole::DrawCells(short x1, short x2, short y, bool erase) +{ + Attributes a=attrs[y * cols + x1]; + SetAttributes(a); + int start=0; + int xstart=0; + int xend=0; + for(int i=0; i cursorX) + { + erase = true; + cursorDrawn = false; + } + } + + if(erase) + EraseRect(&r); + MoveTo(r.left, r.bottom - 2); + + a=attrs[y * cols + x1]; + SetAttributes(a); + start=x1; + for(int i=x1; i 0 ? dirtyRect.top - 1 : 0; + dirtyRect.bottom = dirtyRect.bottom > 0 ? dirtyRect.bottom - 1 : 0; +} + +void IConsole::ProcessEscSequence(char c) +{ + switch(sequenceStep) + { + case 0: + if(c=='[') + ++sequenceStep; + else + isProcessingEscSequence=false; + break; + case 1: + ++sequenceStep; + switch(c) + { + case '0': // Normal character + currentAttr.reset(); + break; + case '1': // Bold + currentAttr.isBold=true; + break; + case '4': // Underline + currentAttr.isUnderline=true; + break; + default: + isProcessingEscSequence=false; + } + break; + case 2: + if(c=='m') + isProcessingEscSequence=false; + else if(c==';') + sequenceStep=1; + else + isProcessingEscSequence=false; + break; + default: + sequenceStep=0; + } +} + +void IConsole::PutCharNoUpdate(char c) +{ + if(isProcessingEscSequence) + { + ProcessEscSequence(c); + return; + } + InvalidateCursor(); + switch(c) + { + case '\033': // Begin of an ANSI escape sequence + isProcessingEscSequence=true; + sequenceStep=0; + break; + case '\r': + cursorX = 0; + break; + case '\n': + cursorY++; + cursorX = 0; + if(cursorY >= rows) + ScrollUp(); + break; + default: + chars[cursorY * cols + cursorX] = c; + attrs[cursorY * cols + cursorX] = currentAttr; + + if(dirtyRect.right == 0) + { + dirtyRect.right = (dirtyRect.left = cursorX) + 1; + dirtyRect.bottom = (dirtyRect.top = cursorY) + 1; + } + else + { + dirtyRect.left = std::min(dirtyRect.left, cursorX); + dirtyRect.top = std::min(dirtyRect.top, cursorY); + dirtyRect.right = std::max(dirtyRect.right, short(cursorX + 1)); + dirtyRect.bottom = std::max(dirtyRect.bottom, short(cursorY + 1)); + } + + cursorX++; + if(cursorX >= cols) + PutCharNoUpdate('\n'); + } +} + +void IConsole::Update() +{ + PortSetter setport(consolePort); + FontSetup fontSetup; + + for(short row = dirtyRect.top; row < dirtyRect.bottom; ++row) + { + short start = -1; + bool needclear = false; + for(short col = dirtyRect.left; col < dirtyRect.right; ++col) + { + char old = onscreen[row * cols + col]; + if(chars[row * cols + col] != old) + { + if(start == -1) + start = col; + if(old != ' ') + needclear = true; + onscreen[row * cols + col] = chars[row * cols + col]; + } + else + { + if(start != -1) + DrawCells(start, col, row, needclear); + start = -1; + needclear = false; + } + } + if(start != -1) + DrawCells(start, dirtyRect.right, row, needclear); + } + dirtyRect = Rect(); + + if(cursorVisible != cursorDrawn) + { + Rect r = CellRect(cursorX, cursorY); + if(cursorDrawn) + DrawCell(cursorX, cursorY, true); + else + InvertRect(&r); + cursorDrawn = !cursorDrawn; + } + +#if TARGET_API_MAC_CARBON + QDFlushPortBuffer(consolePort,NULL); +#endif +} + +void IConsole::putch(char c) +{ + if(!rows) + return; + PutCharNoUpdate(c); + Update(); +} + +void IConsole::write(const char *p, int n) +{ + if(!rows) + return; + + for(int i = 0; i < n; i++) + IConsole::currentInstance->PutCharNoUpdate(*p++); + Update(); +} + + +std::string IConsole::ReadLine() +{ + if(!consolePort) + return ""; + + std::string buffer; + char c; + + do + { + c = WaitNextChar(); + if(!c) + { + eof = true; + return ""; + } + + if(c == '\r') + c = '\n'; + + if(c == '\b') + { + if(buffer.size()) + { + InvalidateCursor(); + cursorX--; + PutCharNoUpdate(' '); + cursorX--; + Update(); + + buffer.resize(buffer.size()-1); + } + + continue; + } + + putch(c); + buffer.append(1,c); + } while(c != '\n'); + return buffer; +} + +void IConsole::InvalidateCursor() +{ + if(cursorDrawn) + { + PortSetter setport(consolePort); + + DrawCell(cursorX, cursorY, true); + cursorDrawn = false; + } +} + +void IConsole::Idle() +{ + long ticks = TickCount(); + if(ticks - blinkTicks > 60) + { + cursorVisible = !cursorVisible; + blinkTicks = ticks; + Update(); + } +} + +void IConsole::Reshape(Rect newBounds) +{ + if(!consolePort) + return; + + InsetRect(&newBounds, 2,2); + + bounds = newBounds; + short newRows = (bounds.bottom - bounds.top) / cellSizeY; + short newCols = (bounds.right - bounds.left) / cellSizeX; + + short upshift = 0; + if(cursorY >= newRows) + { + upshift = cursorY - (newRows - 1); + + InvalidateCursor(); + cursorY = newRows - 1; + } + + std::vector newChars(newRows*newCols, ' '); + for(short row = 0; row < newRows && row + upshift < rows; row++) + { + char *src = &chars[(row+upshift) * cols]; + char *dst = &newChars[row * newCols]; + std::copy(src, src + std::min(cols, newCols), dst); + } + chars.swap(newChars); + /*newChars = std::vector(newRows*newCols, ' '); + for(short row = 0; row < newRows && row < rows; row++) + { + char *src = &chars[row * cols]; + char *dst = &newChars[row * newCols]; + std::copy(src, src + std::min(cols, newCols), dst); + } + onscreen.swap(newChars);*/ + onscreen = newChars; + + rows = newRows; + cols = newCols; + + if(upshift) + { + //dirtyRect = Rect { 0, 0, rows, cols }; + //Update(); + Draw(); + } +} + +char IConsole::WaitNextChar() +{ + return 0; +} diff --git a/IConsole/retro/IConsole.h b/IConsole/retro/IConsole.h new file mode 100644 index 0000000000..611df06c8e --- /dev/null +++ b/IConsole/retro/IConsole.h @@ -0,0 +1,123 @@ +/* + Copyright 2012 Wolfgang Thaller. + + This file is part of Retro68. + + Retro68 is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Retro68 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Retro68. If not, see . +*/ +#ifndef RETRO68_CONSOLE_H_ +#define RETRO68_CONSOLE_H_ + +#include +#include +#include + +namespace retro +{ + class Attributes + { + public: + bool isBold; + bool isUnderline; + Attributes(void) // Code should not be in .h + { + reset(); + } + void reset(void) + { + isBold=false; + isUnderline=false; + } + }; + + inline bool operator==(const Attributes& lhs, const Attributes& rhs) + { + return lhs.isBold==rhs.isBold && lhs.isUnderline==rhs.isUnderline; + } + inline bool operator!=(const Attributes& lhs, const Attributes& rhs) + { + return !(lhs == rhs); + } + + class IConsole + { + public: + IConsole(); + IConsole(GrafPtr port, Rect r); + ~IConsole(); + + void Reshape(Rect newBounds); + + void Draw(Rect r); + void Draw() { Draw(bounds); } + void putch(char c); + + void write(const char *s, int n); + std::string ReadLine(); + + static IConsole *currentInstance; + + short GetRows() const { return rows; } + short GetCols() const { return cols; } + + void Idle(); + + bool IsEOF() const { return eof; } + private: + GrafPtr consolePort = nullptr; + Rect bounds; + Attributes currentAttr; + + std::vector chars, onscreen; + std::vector attrs; + bool isProcessingEscSequence; + int sequenceStep; + + short cellSizeX; + short cellSizeY; + + short rows = 0, cols = 0; + + short cursorX, cursorY; + + Rect dirtyRect = {}; + + long blinkTicks = 0; + bool cursorDrawn = false; + bool cursorVisible = true; + bool eof = false; + + void PutCharNoUpdate(char c); + void Update(); + + Rect CellRect(short x, short y); + void DrawCell(short x, short y, bool erase = true); + void DrawCells(short x1, short x2, short y, bool erase = true); + void ScrollUp(short n = 1); + void ProcessEscSequence(char c); + void SetAttributes(Attributes aa); + + void InvalidateCursor(); + + virtual char WaitNextChar(); + + protected: + void Init(GrafPtr port, Rect r); + + }; + + +} + +#endif /* RETRO68_CONSOLE_H_ */ diff --git a/IConsole/retro/IConsoleWindow.cc b/IConsole/retro/IConsoleWindow.cc new file mode 100644 index 0000000000..f8043b62d8 --- /dev/null +++ b/IConsole/retro/IConsoleWindow.cc @@ -0,0 +1,127 @@ +/* + Copyright 2012 Wolfgang Thaller. + + This file is part of Retro68. + + Retro68 is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Retro68 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Retro68. If not, see . +*/ + +#include "IConsoleWindow.h" +#include "Events.h" +#include + +using namespace retro; + +namespace +{ + std::unordered_map *windows = NULL; +} + +IConsoleWindow::IConsoleWindow(Rect r, ConstStr255Param title) +{ + GrafPtr port; + //Retro68 Improved Console + win = NewWindow(NULL, &r, "\pThe Queen's Footsteps", true, 0, (WindowPtr)-1, false, 0); + +#if !TARGET_API_MAC_CARBON + port = win; + Rect portRect = port->portRect; +#else + port = GetWindowPort(win); + Rect portRect; + GetPortBounds(port, &portRect); +#endif + + SetPort(port); + EraseRect(&portRect); + + if(!windows) + windows = new std::unordered_map(); + (*windows)[win] = this; + + Init(port, portRect); +} + +IConsoleWindow::~IConsoleWindow() +{ + windows->erase(win); + DisposeWindow(win); +} + +char IConsoleWindow::WaitNextChar() +{ + EventRecord event; + WindowPtr eventWin; + IConsoleWindow *realConsole; +#if TARGET_API_MAC_CARBON + Rect *boundsPtr = NULL; +#else + Rect *boundsPtr = &qd.screenBits.bounds; +#endif + + do + { + #if TARGET_API_MAC_CARBON + #define SystemTask() + #endif + SystemTask(); + Idle(); + while(!GetNextEvent(everyEvent, &event)) + { + SystemTask(); + Idle(); + } + + switch(event.what) + { + case updateEvt: + eventWin = (WindowPtr)event.message; + realConsole = (*windows)[(WindowPtr)event.message]; + if(realConsole) + { + Rect updateRect; + BeginUpdate(eventWin); +#if TARGET_API_MAC_CARBON + RgnHandle rgn = NewRgn(); + GetPortVisibleRegion(GetWindowPort(eventWin), rgn); + GetRegionBounds(rgn, &updateRect); + DisposeRgn(rgn); +#else + updateRect = (*qd.thePort->visRgn)->rgnBBox; // Life was simple back then. +#endif + realConsole->Draw(updateRect); + EndUpdate(eventWin); + } + break; + case mouseDown: + + switch(FindWindow(event.where, &eventWin)) + { + case inDrag: + DragWindow(eventWin, event.where, boundsPtr); + break; + case inGrow: + { + long growResult = GrowWindow(eventWin, event.where, boundsPtr); + SizeWindow(eventWin, growResult & 0xFFFF, growResult >> 16, false); + Reshape(Rect {0, 0, (short) (growResult >> 16), (short) (growResult & 0xFFFF) }); + } + break; + } + break; + } + } while(event.what != keyDown && event.what != autoKey); + + return event.message & charCodeMask; +} diff --git a/IConsole/retro/IConsoleWindow.h b/IConsole/retro/IConsoleWindow.h new file mode 100644 index 0000000000..62d820f246 --- /dev/null +++ b/IConsole/retro/IConsoleWindow.h @@ -0,0 +1,38 @@ +/* + Copyright 2012 Wolfgang Thaller. + + This file is part of Retro68. + + Retro68 is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Retro68 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Retro68. If not, see . +*/ + +#include +#include +#include + +#include "IConsole.h" + +namespace retro +{ + class IConsoleWindow : public IConsole + { + public: + IConsoleWindow(Rect r, ConstStr255Param title); + ~IConsoleWindow(); + private: + WindowPtr win; + + virtual char WaitNextChar(); + }; +} diff --git a/IConsole/retro/InitIConsole.cc b/IConsole/retro/InitIConsole.cc new file mode 100644 index 0000000000..01df867552 --- /dev/null +++ b/IConsole/retro/InitIConsole.cc @@ -0,0 +1,108 @@ +/* + Copyright 2014 Wolfgang Thaller. + + This file is part of Retro68. + + Retro68 is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Retro68 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Retro68. If not, see . +*/ + +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include "MacUtils.h" +#include "IConsole.h" +#include "IConsoleWindow.h" + +namespace retro +{ + void InitIConsole(); +} + +using namespace retro; + +void retro::InitIConsole() +{ + if(IConsole::currentInstance) + return; + IConsole::currentInstance = (IConsole*) -1; + +#if !TARGET_API_MAC_CARBON + InitGraf(&qd.thePort); + InitFonts(); + InitWindows(); + InitMenus(); + + Rect r = qd.screenBits.bounds; +#else + Rect r = (*GetMainDevice())->gdRect; +#endif + { + // give MultiFinder a chance to bring the App to front + // see Technote TB 35 - MultiFinder Miscellanea + // "If your application [...] has the canBackground bit set in the + // size resource, then it should call _EventAvail several times + // (or _WaitNextEvent or _GetNextEvent) before putting up the splash + // screen, or the splash screen will come up behind the frontmost + // layer. If the canBackground bit is set, MultiFinder will not move + // your layer to the front until you call _GetNextEvent, + // _WaitNextEvent, or _EventAvail." + + EventRecord event; + for(int i = 0; i < 5; i++) + EventAvail(everyEvent, &event); + } + + r.top += 40; + InsetRect(&r, 5,5); + + IConsole::currentInstance = new IConsoleWindow(r, "\pRetro68 IConsole"); + InitCursor(); +} + +extern "C" ssize_t _consolewrite(int fd, const void *buf, size_t count) +{ + if(!IConsole::currentInstance) + InitIConsole(); + if(IConsole::currentInstance == (IConsole*)-1) + return 0; + + IConsole::currentInstance->write((const char*)buf, count); + return count; +} + +extern "C" ssize_t _consoleread(int fd, void *buf, size_t count) +{ + if(!IConsole::currentInstance) + InitIConsole(); + if(IConsole::currentInstance == (IConsole*)-1) + return 0; + + static std::string consoleBuf; + if(consoleBuf.size() == 0) + consoleBuf = IConsole::currentInstance->ReadLine(); + + if(count > consoleBuf.size()) + count = consoleBuf.size(); + memcpy(buf, consoleBuf.data(), count); + consoleBuf = consoleBuf.substr(count); + return count; +} diff --git a/IConsole/retro/MacUtils.h b/IConsole/retro/MacUtils.h new file mode 100644 index 0000000000..4bff8f907c --- /dev/null +++ b/IConsole/retro/MacUtils.h @@ -0,0 +1,40 @@ +/* + Copyright 2012 Wolfgang Thaller. + + This file is part of Retro68. + + Retro68 is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Retro68 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Retro68. If not, see . +*/ + +#include +#include + +namespace retro +{ + class PortSetter + { + GrafPtr save; + public: + PortSetter(GrafPtr port) + { + ::GetPort(&save); + ::SetPort(port); + } + + ~PortSetter() + { + ::SetPort(save); + } + }; +}