Files
pip/libs/console/piscreen.cpp
2022-12-14 13:56:19 +03:00

651 lines
17 KiB
C++

/*
PIP - Platform Independent Primitives
Console output/input
Ivan Pelipenko peri4ko@yandex.ru
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program 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 Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "piscreen.h"
#include "piincludes_p.h"
#ifndef WINDOWS
# include <fcntl.h>
# include <sys/ioctl.h>
# include <termios.h>
#else
# include <wincon.h>
# include <wingdi.h>
# ifndef COMMON_LVB_UNDERSCORE
# define COMMON_LVB_UNDERSCORE 0x8000
# endif
#endif
using namespace PIScreenTypes;
PRIVATE_DEFINITION_START(PIScreen::SystemConsole)
#ifdef WINDOWS
void * hOut;
CONSOLE_SCREEN_BUFFER_INFO sbi, csbi;
CONSOLE_CURSOR_INFO curinfo;
COORD ccoord, ulcoord, bs, bc;
SMALL_RECT srect;
WORD dattr;
DWORD smode, written;
PIVector<CHAR_INFO> chars;
#endif
PRIVATE_DEFINITION_END(PIScreen::SystemConsole)
PIScreen::SystemConsole::SystemConsole() {
width = height = pwidth = pheight = 0;
mouse_x = mouse_y = -1;
}
PIScreen::SystemConsole::~SystemConsole() {
#ifdef WINDOWS
SetConsoleMode(PRIVATE->hOut, PRIVATE->smode);
SetConsoleTextAttribute(PRIVATE->hOut, PRIVATE->dattr);
#endif
}
void PIScreen::SystemConsole::begin() {
int w, h;
#ifdef WINDOWS
PRIVATE->ulcoord.X = 0;
PRIVATE->hOut = GetStdHandle(STD_OUTPUT_HANDLE);
GetConsoleScreenBufferInfo(PRIVATE->hOut, &PRIVATE->sbi);
PRIVATE->dattr = PRIVATE->sbi.wAttributes;
w = PRIVATE->sbi.srWindow.Right - PRIVATE->sbi.srWindow.Left;
h = PRIVATE->sbi.srWindow.Bottom - PRIVATE->sbi.srWindow.Top;
PRIVATE->ulcoord.Y = PRIVATE->sbi.srWindow.Top;
GetConsoleMode(PRIVATE->hOut, &PRIVATE->smode);
GetConsoleCursorInfo(PRIVATE->hOut, &PRIVATE->curinfo);
#else
# ifdef MICRO_PIP
w = 80;
h = 24;
# else
winsize ws;
ioctl(0, TIOCGWINSZ, &ws);
w = ws.ws_col;
h = ws.ws_row;
# endif
#endif
resize(w, h);
#ifdef WINDOWS
SetConsoleMode(PRIVATE->hOut, ENABLE_WRAP_AT_EOL_OUTPUT);
GetConsoleScreenBufferInfo(PRIVATE->hOut, &PRIVATE->sbi);
PRIVATE->bc.X = 0;
PRIVATE->bc.Y = 0;
#endif
clear();
clearScreen();
hideCursor();
}
void PIScreen::SystemConsole::end() {
#ifdef WINDOWS
SetConsoleTextAttribute(PRIVATE->hOut, PRIVATE->dattr);
#else
printf("\e[0m");
#endif
moveTo(0, height);
showCursor();
}
void PIScreen::SystemConsole::prepare() {
int w = 80, h = 24;
#ifdef WINDOWS
GetConsoleScreenBufferInfo(PRIVATE->hOut, &PRIVATE->csbi);
w = PRIVATE->csbi.srWindow.Right - PRIVATE->csbi.srWindow.Left + 1;
h = PRIVATE->csbi.srWindow.Bottom - PRIVATE->csbi.srWindow.Top + 1;
#else
# ifndef MICRO_PIP
winsize ws;
ioctl(0, TIOCGWINSZ, &ws);
w = ws.ws_col;
h = ws.ws_row;
# endif
#endif
resize(w, h);
}
void PIScreen::SystemConsole::clear() {
for (int i = 0; i < cells.size_s(); ++i)
cells[i].fill(Cell());
}
void PIScreen::SystemConsole::resize(int w, int h) {
if (w == pwidth && h == pheight) return;
width = piMaxi(w, 0);
height = piMaxi(h, 0);
pwidth = width;
pheight = height;
cells.resize(height);
pcells.resize(height);
for (int i = 0; i < height; ++i) {
cells[i].resize(width);
pcells[i].resize(width, Cell(PIChar()));
}
#ifdef WINDOWS
PRIVATE->sbi.srWindow = PRIVATE->csbi.srWindow;
PRIVATE->chars.resize(width * height);
#endif
for (int i = 0; i < pcells.size_s(); ++i)
pcells[i].fill(Cell());
clear();
clearScreen();
}
void PIScreen::SystemConsole::print() {
if (mouse_x >= 0 && mouse_x < width && mouse_y >= 0 && mouse_y < height) {
/// cells[mouse_y][mouse_x].format.flags ^= Inverse;
}
#ifdef WINDOWS
PRIVATE->srect = PRIVATE->sbi.srWindow;
int dx0 = -1, dx1 = -1, dy0 = -1, dy1 = -1;
for (int j = 0; j < height; ++j) {
PIVector<Cell> & ccv(cells[j]);
PIVector<Cell> & pcv(pcells[j]);
for (int i = 0; i < width; ++i)
if (ccv[i] != pcv[i]) {
if (dx0 < 0) {
dx0 = dx1 = i;
dy0 = dy1 = j;
} else {
dx0 = piMini(dx0, i);
dx1 = piMaxi(dx1, i);
dy0 = piMini(dy0, j);
dy1 = piMaxi(dy1, j);
}
}
}
if (dx0 < 0) return;
int dw = dx1 - dx0 + 1, dh = dy1 - dy0 + 1;
for (int i = 0; i < dw; ++i)
for (int j = 0; j < dh; ++j) {
int k = j * dw + i;
Cell & c(cells[j + dy0][i + dx0]);
PRIVATE->chars[k].Char.UnicodeChar = 0;
PRIVATE->chars[k].Char.AsciiChar = c.symbol.toConsole1Byte();
PRIVATE->chars[k].Attributes = attributes(c);
}
// piCout << "draw" << dw << dh;
PRIVATE->bs.X = dw;
PRIVATE->bs.Y = dh;
PRIVATE->srect.Left += dx0;
PRIVATE->srect.Top += dy0;
PRIVATE->srect.Right -= width - dx1 - 1;
PRIVATE->srect.Bottom -= height - dy1 - 1;
WriteConsoleOutput(PRIVATE->hOut, PRIVATE->chars.data(), PRIVATE->bs, PRIVATE->bc, &PRIVATE->srect);
#else
PIString s;
int si = 0, sj = 0;
CellFormat prf(0xFFFF);
for (int j = 0; j < height; ++j) {
PIVector<Cell> & ccv(cells[j]);
PIVector<Cell> & pcv(pcells[j]);
for (int i = 0; i < width; ++i) {
Cell & cc(ccv[i]);
Cell & pc(pcv[i]);
if (cc != pc) {
if (s.isEmpty()) {
si = i;
sj = j;
}
if (prf != cc.format) {
prf = cc.format;
s += formatString(cc);
}
s += cc.symbol;
} else {
if (!s.isEmpty()) {
moveTo(si, sj);
PICout::stdoutPIString(s);
s.clear();
}
}
}
if (!s.isEmpty()) {
moveTo(si, sj);
PICout::stdoutPIString(s);
// printf("%s", s.data());
s.clear();
}
}
printf("\e[0m");
fflush(0);
#endif
pcells = cells;
}
#ifdef WINDOWS
# define FOREGROUND_MASK (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE)
# define BACKGROUND_MASK (BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE)
ushort PIScreen::SystemConsole::attributes(const PIScreenTypes::Cell & c) {
WORD attr = PRIVATE->dattr;
if (c.format.flags & Bold)
attr |= FOREGROUND_INTENSITY;
else
attr &= ~FOREGROUND_INTENSITY;
if (c.format.flags & Underline)
attr |= COMMON_LVB_UNDERSCORE;
else
attr &= ~COMMON_LVB_UNDERSCORE;
switch (c.format.color_char) {
case Black: attr = (attr & ~FOREGROUND_MASK); break;
case Red: attr = (attr & ~FOREGROUND_MASK) | FOREGROUND_RED; break;
case Green: attr = (attr & ~FOREGROUND_MASK) | FOREGROUND_GREEN; break;
case Blue: attr = (attr & ~FOREGROUND_MASK) | FOREGROUND_BLUE; break;
case Cyan: attr = (attr & ~FOREGROUND_MASK) | FOREGROUND_GREEN | FOREGROUND_BLUE; break;
case Magenta: attr = (attr & ~FOREGROUND_MASK) | FOREGROUND_RED | FOREGROUND_BLUE; break;
case Yellow: attr = (attr & ~FOREGROUND_MASK) | FOREGROUND_RED | FOREGROUND_GREEN; break;
case White: attr = attr | FOREGROUND_MASK; break;
}
switch (c.format.color_back) {
case Black: attr = (attr & ~BACKGROUND_MASK); break;
case Red: attr = (attr & ~BACKGROUND_MASK) | BACKGROUND_RED; break;
case Green: attr = (attr & ~BACKGROUND_MASK) | BACKGROUND_GREEN; break;
case Blue: attr = (attr & ~BACKGROUND_MASK) | BACKGROUND_BLUE; break;
case Cyan: attr = (attr & ~BACKGROUND_MASK) | BACKGROUND_GREEN | BACKGROUND_BLUE; break;
case Magenta: attr = (attr & ~BACKGROUND_MASK) | BACKGROUND_RED | BACKGROUND_BLUE; break;
case Yellow: attr = (attr & ~BACKGROUND_MASK) | BACKGROUND_RED | BACKGROUND_GREEN; break;
case White: attr = attr | BACKGROUND_MASK; break;
}
if ((c.format.flags & Inverse) == Inverse) {
uchar f = attr & 0xFF;
attr &= 0xFFFFFF00;
f = (f << 4) | (f >> 4);
attr |= f;
}
return attr;
}
# undef FOREGROUND_MASK
# undef BACKGROUND_MASK
void PIScreen::SystemConsole::getWinCurCoord() {
GetConsoleScreenBufferInfo(PRIVATE->hOut, &PRIVATE->csbi);
PRIVATE->ccoord = PRIVATE->csbi.dwCursorPosition;
}
void PIScreen::SystemConsole::clearLine() {
getWinCurCoord();
FillConsoleOutputAttribute(PRIVATE->hOut, PRIVATE->dattr, width - PRIVATE->ccoord.X, PRIVATE->ccoord, &PRIVATE->written);
FillConsoleOutputCharacter(PRIVATE->hOut, ' ', width - PRIVATE->ccoord.X, PRIVATE->ccoord, &PRIVATE->written);
}
void PIScreen::SystemConsole::newLine() {
getWinCurCoord();
PRIVATE->ccoord.X = 0;
PRIVATE->ccoord.Y++;
SetConsoleCursorPosition(PRIVATE->hOut, PRIVATE->ccoord);
}
#else // WINDOWS
PIString PIScreen::SystemConsole::formatString(const PIScreenTypes::Cell & c) {
PIString ts = PIStringAscii("\e[0");
switch (c.format.color_char) {
case Black: ts += PIStringAscii(";30"); break;
case Red: ts += PIStringAscii(";31"); break;
case Green: ts += PIStringAscii(";32"); break;
case Blue: ts += PIStringAscii(";34"); break;
case Cyan: ts += PIStringAscii(";36"); break;
case Magenta: ts += PIStringAscii(";35"); break;
case Yellow: ts += PIStringAscii(";33"); break;
case White: ts += PIStringAscii(";37"); break;
}
switch (c.format.color_back) {
case Black: ts += PIStringAscii(";40"); break;
case Red: ts += PIStringAscii(";41"); break;
case Green: ts += PIStringAscii(";42"); break;
case Blue: ts += PIStringAscii(";44"); break;
case Cyan: ts += PIStringAscii(";46"); break;
case Magenta: ts += PIStringAscii(";45"); break;
case Yellow: ts += PIStringAscii(";43"); break;
case White: ts += PIStringAscii(";47"); break;
}
if ((c.format.flags & Bold) == Bold) ts += PIStringAscii(";1");
if ((c.format.flags & Underline) == Underline) ts += PIStringAscii(";4");
if ((c.format.flags & Blink) == Blink) ts += PIStringAscii(";5");
if ((c.format.flags & Inverse) == Inverse) ts += PIStringAscii(";7");
return ts + 'm';
}
#endif // WINDOWS
void PIScreen::SystemConsole::toUpperLeft() {
#ifdef WINDOWS
SetConsoleCursorPosition(PRIVATE->hOut, PRIVATE->ulcoord);
#else
printf("\e[H");
#endif
}
void PIScreen::SystemConsole::moveTo(int x, int y) {
#ifdef WINDOWS
PRIVATE->ccoord.X = x;
PRIVATE->ccoord.Y = PRIVATE->ulcoord.Y + y;
SetConsoleCursorPosition(PRIVATE->hOut, PRIVATE->ccoord);
#else
printf("\e[%d;%dH", y + 1, x + 1);
#endif
}
void PIScreen::SystemConsole::clearScreen() {
#ifdef WINDOWS
toUpperLeft();
FillConsoleOutputAttribute(PRIVATE->hOut, PRIVATE->dattr, width * (height + 1), PRIVATE->ulcoord, &PRIVATE->written);
FillConsoleOutputCharacter(PRIVATE->hOut, ' ', width * (height + 1), PRIVATE->ulcoord, &PRIVATE->written);
#else
printf("\e[0m\e[H\e[J");
#endif
}
void PIScreen::SystemConsole::clearScreenLower() {
#ifdef WINDOWS
getWinCurCoord();
FillConsoleOutputAttribute(PRIVATE->hOut,
PRIVATE->dattr,
width * height - width * PRIVATE->ccoord.Y + PRIVATE->ccoord.X,
PRIVATE->ccoord,
&PRIVATE->written);
FillConsoleOutputCharacter(PRIVATE->hOut,
' ',
width * height - width * PRIVATE->ccoord.Y + PRIVATE->ccoord.X,
PRIVATE->ccoord,
&PRIVATE->written);
#else
printf("\e[0m\e[J");
#endif
}
void PIScreen::SystemConsole::hideCursor() {
#ifdef WINDOWS
PRIVATE->curinfo.bVisible = false;
SetConsoleCursorInfo(PRIVATE->hOut, &PRIVATE->curinfo);
#else
printf("\e[?25l");
#endif
}
void PIScreen::SystemConsole::showCursor() {
#ifdef WINDOWS
PRIVATE->curinfo.bVisible = true;
SetConsoleCursorInfo(PRIVATE->hOut, &PRIVATE->curinfo);
#else
printf("\e[?25h");
#endif
}
// PIScreen
PIScreen::PIScreen(bool startNow, PIKbdListener::KBFunc slot): PIThread(), drawer_(console.cells), root("rootTile") {
setName("screen");
setPriority(piLow);
needLockRun(true);
mouse_ = false;
ret_func = slot;
tile_focus = tile_dialog = nullptr;
root.screen = this;
listener = new PIKbdListener(key_eventS, this, startNow);
CONNECT1(void, PIKbdListener::MouseEvent, listener, mouseEvent, this, mouse_event);
CONNECT1(void, PIKbdListener::WheelEvent, listener, wheelEvent, this, wheel_event);
if (startNow) start();
}
PIScreen::~PIScreen() {
if (isRunning()) stop();
PIThread::waitForFinish(10);
listener->waitForFinish(10);
delete listener;
}
void PIScreen::setMouseEnabled(bool on) {
mouse_ = on;
console.mouse_x = console.mouse_y = -1;
}
void PIScreen::key_event(PIKbdListener::KeyEvent key) {
/** DEBUG
if (ret_func != 0) ret_func(key, t);
keyPressed(key, t);
return;
*/
PIScreenTile * rtile = rootTile();
if (tile_dialog) rtile = tile_dialog;
bool used = nextFocus(rtile, key);
if (used) return;
if (!used && tile_focus) {
if (tile_focus->visible) {
if (tile_focus->keyEvent(key)) return;
}
}
if (ret_func != 0) ret_func(key, data_);
keyPressed(key, data_);
}
PIVector<PIScreenTile *> PIScreen::prepareMouse(PIKbdListener::MouseEvent * e) {
PIVector<PIScreenTile *> ret;
if (!mouse_ || !e) return ret;
console.mouse_x = e->x;
console.mouse_y = e->y;
PIVector<PIScreenTile *> tl = tilesUnderMouse(e->x, e->y);
bool ff = false;
piForeachR(PIScreenTile * t, tl) {
if (!ff) {
if (t->focus_flags[FocusOnMouse] && (e->action == PIKbdListener::MouseButtonPress)) {
t->setFocus();
ff = true;
}
if (t->focus_flags[FocusOnWheel] && (e->action == PIKbdListener::MouseWheel)) {
t->setFocus();
ff = true;
}
}
}
return tl;
}
PIVector<PIScreenTile *> PIScreen::tilesUnderMouse(int x, int y) {
PIVector<PIScreenTile *> ret;
if (x < 0 || x >= console.width || y < 0 || y >= console.height) return ret;
PIScreenTile * ct = tile_dialog ? tile_dialog : rootTile();
bool f = true;
while (ct) {
if (!f) ret << ct;
f = false;
ct = ct->childUnderMouse(x, y);
}
return ret;
}
void PIScreen::mouse_event(PIKbdListener::MouseEvent me) {
PIVector<PIScreenTile *> tl = prepareMouse(&me);
if (tl.isEmpty()) return;
piForeachR(PIScreenTile * t, tl)
if (t->mouseEvent(me)) break;
}
void PIScreen::wheel_event(PIKbdListener::WheelEvent we) {
PIVector<PIScreenTile *> tl = prepareMouse(&we);
if (tl.isEmpty()) return;
piForeachR(PIScreenTile * t, tl)
if (t->wheelEvent(we)) break;
}
bool PIScreen::nextFocus(PIScreenTile * rt, PIKbdListener::KeyEvent key) {
PIVector<PIScreenTile *> vtl = rt->children(true), ftl;
piForeach(PIScreenTile * t, vtl) {
if (t->focus_flags[CanHasFocus]) ftl << t;
}
int ind = -1;
for (int i = 0; i < ftl.size_s(); ++i)
if (ftl[i] == tile_focus) {
ind = i;
break;
}
if (ind < 0) tile_focus = 0;
if (ftl.isEmpty())
tile_focus = 0;
else {
if (tile_focus)
if (!tile_focus->visible) tile_focus = 0;
int next = tile_focus ? 0 : 1;
if (tile_focus) {
if (tile_focus->focus_flags[NextByTab] && key.key == PIKbdListener::Tab) next = 1;
if (tile_focus->focus_flags[NextByArrowsHorizontal]) {
if (key.key == PIKbdListener::LeftArrow) next = -1;
if (key.key == PIKbdListener::RightArrow) next = 1;
}
if (tile_focus->focus_flags[NextByArrowsVertical]) {
if (key.key == PIKbdListener::UpArrow) next = -1;
if (key.key == PIKbdListener::DownArrow) next = 1;
}
}
// piCout << ftl.size() << ind << next;
if (next != 0) {
PIVector<PIScreenTile *> tl = rt->children();
piForeach(PIScreenTile * t, tl)
t->has_focus = false;
if (!ftl.isEmpty()) {
ind += next;
if (ind >= ftl.size_s()) ind = 0;
if (ind < 0) ind = ftl.size_s() - 1;
tile_focus = ftl[ind];
tile_focus->has_focus = true;
}
return true;
}
}
return false;
}
void PIScreen::tileEventInternal(PIScreenTile * t, TileEvent e) {
tileEvent(t, e);
}
void PIScreen::tileRemovedInternal(PIScreenTile * t) {
if (tile_dialog == t) tile_dialog = 0;
}
void PIScreen::tileSetFocusInternal(PIScreenTile * t) {
PIScreenTile * rt = rootTile();
if (tile_dialog) rt = tile_dialog;
PIVector<PIScreenTile *> tl = rt->children(), ftl;
piForeach(PIScreenTile * i, tl)
i->has_focus = false;
tile_focus = t;
if (!tile_focus) return;
if (tile_focus->focus_flags[CanHasFocus]) tile_focus->has_focus = true;
}
void PIScreen::setDialogTile(PIScreenTile * t) {
tile_dialog = t;
if (!tile_dialog) {
nextFocus(&root);
return;
}
tile_dialog->setScreen(this);
tile_dialog->parent = 0;
nextFocus(tile_dialog);
}
void PIScreen::waitForFinish() {
WAIT_FOR_EXIT
stop();
}
void PIScreen::stop(bool clear) {
PIThread::stopAndWait();
if (clear) console.clearScreen();
#ifndef WINDOWS
fflush(0);
#endif
}
void PIScreen::begin() {
listener->start();
nextFocus(&root);
console.begin();
}
void PIScreen::run() {
console.prepare();
root.width_ = drawer_.width = console.width;
root.height_ = drawer_.height = console.height;
root.layout();
root.drawEventInternal(&drawer_);
if (tile_dialog) {
int sw(0), sh(0);
tile_dialog->sizeHint(sw, sh);
sw = piClampi(sw, tile_dialog->minimumWidth, tile_dialog->maximumWidth);
sh = piClampi(sh, tile_dialog->minimumHeight, tile_dialog->maximumHeight);
tile_dialog->x_ = (console.width - sw) / 2;
tile_dialog->y_ = (console.height - sh) / 2;
tile_dialog->width_ = sw;
tile_dialog->height_ = sh;
tile_dialog->layout();
int dx = tile_dialog->x_ - 1, dy = tile_dialog->y_ - 1, dw = tile_dialog->width_, dh = tile_dialog->height_;
drawer_.drawFrame(dx,
dy,
dx + dw + 1,
dy + dh + 1,
(Color)tile_dialog->back_format.color_char,
(Color)tile_dialog->back_format.color_back,
(CharFlags)tile_dialog->back_format.flags);
tile_dialog->drawEventInternal(&drawer_);
}
console.print();
}
void PIScreen::end() {
listener->stop();
console.end();
}
PIScreenTile * PIScreen::tileByName(const PIString & name) {
PIVector<PIScreenTile *> tl(tiles());
piForeach(PIScreenTile * t, tl)
if (t->name() == name) return t;
return 0;
}