diff options
Diffstat (limited to 'curses')
42 files changed, 8581 insertions, 0 deletions
diff --git a/curses/.gitignore b/curses/.gitignore new file mode 100644 index 0000000..eec2732 --- /dev/null +++ b/curses/.gitignore @@ -0,0 +1 @@ +/curses_config.h.in diff --git a/curses/Makefile.am b/curses/Makefile.am new file mode 100644 index 0000000..4206bd4 --- /dev/null +++ b/curses/Makefile.am @@ -0,0 +1,35 @@ +# +# Copyright 2002, Double Precision Inc. +# +# See COPYING for distribution information. +# + +noinst_LIBRARIES=libcurses.a + +libcurses_a_SOURCES=mycurses.H curses.C \ + cursesbutton.H cursesbutton.C \ + curseschoicebutton.H curseschoicebutton.C \ + cursescontainer.H cursescontainer.C \ + cursesdialog.H cursesdialog.C \ + cursesfield.H cursesfield.C \ + cursesfilereq.H cursesfilereq.C \ + cursesflowedline.H \ + curseskeyhandler.H curseskeyhandler.C \ + curseslabel.H curseslabel.C \ + cursesmainscreen.H cursesmainscreen.C \ + cursesmultilinelabel.H cursesmultilinelabel.C \ + cursesmoronize.H cursesmoronize.C \ + cursesobject.H cursesobject.C \ + cursesscreen.H cursesscreen.C \ + cursesstatusbar.H cursesstatusbar.C \ + cursestitlebar.H cursestitlebar.C \ + cursesvscroll.H cursesvscroll.C \ + timer.C timer.H \ + widechar.C widechar.H + +noinst_DATA=curseslib + +DISTCLEANFILES=curseslib + +curseslib: config.status + echo @CURSESLIB@ >curseslib diff --git a/curses/configure.in b/curses/configure.in new file mode 100644 index 0000000..57a576d --- /dev/null +++ b/curses/configure.in @@ -0,0 +1,103 @@ +dnl Process this file with autoconf to produce a configure script. + +# +# Copyright 2002-2004, Double Precision Inc. +# +# See COPYING for distribution information. +# +AC_PREREQ(2.59) +AC_INIT(curses, 0.11, courier-cone@lists.sourceforge.net) +AC_CONFIG_SRCDIR(mycurses.H) +AC_CONFIG_AUX_DIR(../..) +AM_CONFIG_HEADER(curses_config.h) +AM_INIT_AUTOMAKE +>confdefs.h + +dnl Checks for programs. +AC_USE_SYSTEM_EXTENSIONS +AC_PROG_CXX +AC_PROG_AWK +AC_PROG_CC +AC_PROG_RANLIB +AC_PROG_INSTALL +AC_PROG_LN_S + +dnl Checks for libraries. + +CURSESLIBRARY="" + +for f in ncursesw cursesw ncurses curses +do + AC_CHECK_LIB($f, wgetch, + [if test "$CURSESLIBRARY" = "" + then + CURSESLIBRARY="$f" + fi + ]) +done + +if test "$CURSESLIBRARY" = "" +then + AC_MSG_ERROR([curses library not found.]) +fi + +CURSESLIB="-l$CURSESLIBRARY" +AC_SUBST(CURSESLIB) + +dnl Checks for header files. +AC_CHECK_HEADERS(sys/time.h unistd.h sys/wait.h glob.h) +AC_CHECK_HEADERS(ncursesw/curses.h) + +AC_HEADER_TIME +AC_HEADER_DIRENT +AC_HEADER_SYS_WAIT + +save_LIBS="$LIBS" +LIBS="$CURSESLIB $LIBS" + +AC_MSG_CHECKING(for use_default_colors()) + +AC_TRY_LINK([ +#include <curses.h> +],[ + use_default_colors(); +], [AC_MSG_RESULT(yes) + +AC_DEFINE_UNQUOTED(HAS_USE_DEFAULT_COLORS,1,[Whether we have use_default_colors()]) +], AC_MSG_RESULT(no)) + +AC_COMPILE_IFELSE([AC_LANG_SOURCE([[ +#if HAVE_NCURSESW_CURSES_H +#include <ncursesw/curses.h> +#else +#include <curses.h> +#endif + +#ifdef KEY_F +#if KEY_F(0)+63 == KEY_F(63) +#define SANEFKEY +#endif +#endif + +#ifndef SANEFKEY +#error Not a sane FKEY +#endif +]])], +AC_DEFINE_UNQUOTED(HAS_FUNCTIONKEYS,1,[Whether curses implements function keys with known semantics])) + +LIBS="$save_LIBS" + +dnl Checks for typedefs, structures, and compiler characteristics. +AC_C_CONST + +dnl Checks for library functions. +AC_TYPE_SIGNAL +AC_SYS_LARGEFILE + +AC_CHECK_FUNCS(glob) +if test "$GXX" = "yes" +then + CPPFLAGS="-Wall $CPPFLAGS" +fi + +AC_OUTPUT(Makefile) diff --git a/curses/curses.C b/curses/curses.C new file mode 100644 index 0000000..5dbf22f --- /dev/null +++ b/curses/curses.C @@ -0,0 +1,524 @@ +/* +** +** Copyright 2002-2011, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#include "curses_config.h" + +#include "mycurses.H" +#include "widechar.H" +#include "cursescontainer.H" +#include "curseskeyhandler.H" + +#include <algorithm> + +#include <stdio.h> +#include <stdlib.h> +#include <errno.h> +#include <limits.h> +#include <iostream> + +bool Curses::keepgoing=false; +bool Curses::shiftmode=false; + +std::string Curses::suspendCommand; + +#define TABSIZE 8 + +const char Curses::Key::LEFT[]="LEFT", + Curses::Key::RIGHT[]="RIGHT", + Curses::Key::SHIFTLEFT[]="SHIFTLEFT", + Curses::Key::SHIFTRIGHT[]="SHIFTRIGHT", + Curses::Key::UP[]="UP", + Curses::Key::DOWN[]="DOWN", + Curses::Key::SHIFTUP[]="SHIFTUP", + Curses::Key::SHIFTDOWN[]="SHIFTDOWN", + Curses::Key::DEL[]="DEL", + Curses::Key::CLREOL[]="CLREOL", + Curses::Key::BACKSPACE[]="BACKSPACE", + Curses::Key::ENTER[]="ENTER", + Curses::Key::PGUP[]="PGUP", + Curses::Key::PGDN[]="PGDN", + Curses::Key::SHIFTPGUP[]="SHIFTPGUP", + Curses::Key::SHIFTPGDN[]="SHIFTPGDN", + Curses::Key::HOME[]="HOME", + Curses::Key::END[]="END", + Curses::Key::SHIFTHOME[]="SHIFTHOME", + Curses::Key::SHIFTEND[]="SHIFTEND", + Curses::Key::SHIFT[]="SHIFT", + Curses::Key::RESIZE[]="RESIZE"; + +bool Curses::Key::operator==(const std::vector<unicode_char> &v) const +{ + return keycode == 0 && + std::find(v.begin(), v.end(), ukey) != v.end(); +} + +Curses::Curses(CursesContainer *parentArg) : parent(parentArg), + row(0), col(0), + alignment(Curses::LEFT) +{ + if (parent != NULL) + parent->addChild(this); +} + +Curses::~Curses() +{ + CursesContainer *p=getParent(); + + if (p) + p->deleteChild(this); + + if (hasFocus()) + currentFocus=0; +} + +int Curses::getScreenWidth() const +{ + const CursesContainer *p=getParent(); + + while (p && p->getParent()) + p=p->getParent(); + + return (p ? p->getWidth():0); +} + +int Curses::getScreenHeight() const +{ + const CursesContainer *p=getParent(); + + while (p && p->getParent()) + p=p->getParent(); + + return (p ? p->getHeight():0); +} + +int Curses::getRow() const +{ + return row; +} + +int Curses::getCol() const +{ + return col; +} + +void Curses::setRow(int r) +{ + row=r; +} + +void Curses::scrollTo(size_t r) +{ + if (parent) + parent->scrollTo(r + row); +} + +void Curses::setCol(int c) +{ + col=c; +} + +void Curses::resized() +{ + draw(); +} + +void Curses::getVerticalViewport(size_t &first_row, + size_t &nrows) +{ + CursesContainer *p=getParent(); + + if (!p) + { + first_row=0; + nrows=getHeight(); + return; + } + + p->getVerticalViewport(first_row, nrows); + size_t r=getRow(); + size_t h=getHeight(); + + + if (r + h <= first_row || r >= first_row + nrows) + { + first_row=nrows=0; + return; + } + + if (first_row < r) + { + nrows -= r-first_row; + first_row=r; + } + + first_row -= r; + + if (first_row + nrows > h) + nrows=h-first_row; +} + +void Curses::setAlignment(Alignment alignmentArg) +{ + alignment=alignmentArg; +} + +Curses::Alignment Curses::getAlignment() +{ + return alignment; +} + +int Curses::getRowAligned() const +{ + return row; +} + +int Curses::getColAligned() const +{ + int c=col; + + if (alignment == PARENTCENTER) + { + const CursesContainer *p=getParent(); + + if (p != NULL) + { + c=p->getWidth()/2; + c -= getWidth()/2; + } + } + else if (alignment == CENTER) + c -= getWidth() / 2; + else if (alignment == RIGHT) + c -= getWidth(); + return c; +} + +Curses *Curses::getDialogChild() const +{ + return NULL; +} + +bool Curses::writeText(const char *text, int r, int c, + const CursesAttr &attr) const +{ + CursesContainer *p=parent; + + if (!isDialog() && p && p->getDialogChild()) + return false; // Parent has a dialog and it ain't us + + if (p) + return p->writeText(text, r+getRowAligned(), + c+getColAligned(), attr); + return false; +} + +bool Curses::writeText(const std::vector<unicode_char> &text, int r, int c, + const Curses::CursesAttr &attr) const +{ + CursesContainer *p=parent; + + if (!isDialog() && p && p->getDialogChild()) + return false; // Parent has a dialog and it ain't us + + if (p) + return p->writeText(text, r+getRowAligned(), + c+getColAligned(), attr); + return false; +} + + +void Curses::writeText(std::string text, int row, int col, + const CursesAttr &attr) const +{ + writeText(text.c_str(), row, col, attr); +} + +bool Curses::isDialog() const +{ + return false; +} + +void Curses::erase() +{ + const CursesContainer *p=getParent(); + + if (!isDialog() && p && p->getDialogChild()) + return; // Parent has a dialog and it ain't us + + size_t w=getWidth(); + size_t h=getHeight(); + + std::vector<unicode_char> spaces; + + spaces.insert(spaces.end(), w, ' '); + + size_t i; + + CursesAttr attr; + + for (i=0; i<h; i++) + writeText(spaces, i, 0, attr); +} + +void Curses::beepError() +{ + CursesContainer *p=getParent(); + + if (p) + p->beepError(); +} + +Curses *Curses::currentFocus=0; + +void Curses::requestFocus() +{ + Curses *oldFocus=currentFocus; + + currentFocus=NULL; + + cursesPtr<Curses> ptr=this; + + if (oldFocus) + oldFocus->focusLost(); + + if (!ptr.isDestroyed() && currentFocus == NULL) + { + currentFocus=this; + focusGained(); + } +} + +void Curses::dropFocus() +{ + Curses *oldFocus=currentFocus; + + currentFocus=NULL; + + if (oldFocus) + oldFocus->focusLost(); +} + +void Curses::focusGained() +{ + draw(); +} + +void Curses::focusLost() +{ + draw(); +} + +void Curses::flush() +{ + CursesContainer *p=getParent(); + + if (p) + p->flush(); +} + +bool Curses::hasFocus() +{ + return currentFocus == this; +} + +int Curses::getCursorPosition(int &r, int &c) +{ + r += getRowAligned(); + c += getColAligned(); + + CursesContainer *parent=getParent(); + if (parent) + return parent->getCursorPosition(r, c); + + return 1; +} + + +bool Curses::processKey(const Key &k) +{ + if (k == Key::RESIZE) + return true; // No-op. + + return CursesKeyHandler::handle(k, currentFocus); +} + +bool Curses::processKeyInFocus(const Key &key) +{ + if ((key.plain() && key.ukey == '\t') + || key == key.DOWN || key == key.SHIFTDOWN + || key == key.ENTER) + { + transferNextFocus(); + return true; + } + + if (key == key.UP || key == key.SHIFTUP) + { + transferPrevFocus(); + return true; + } + + if (key == key.PGUP || key == key.SHIFTPGUP) + { + int h=getScreenHeight(); + + if (h > 5) + h -= 5; + + if (h < 5) + h=5; + + while (h) + { + processKey(Key(key == + Key::PGUP ? Key::UP:Key::SHIFTUP)); + --h; + } + return true; + } + + if (key == key.PGDN || key == key.SHIFTPGDN) + { + int h=getScreenHeight(); + + if (h > 5) + h -= 5; + + if (h < 5) + h=5; + + while (h) + { + processKey(Key(key == + Key::PGDN ? Key::DOWN:Key::SHIFTDOWN)); + --h; + } + return true; + } + + return false; +} + +bool Curses::isFocusable() +{ + return 0; +} + +void Curses::transferNextFocus() +{ + Curses *p=this; + + do + { + p=p->getNextFocus(); + + } while (p != NULL && !p->isFocusable()); + + if (p) + p->requestFocus(); +} + +void Curses::transferPrevFocus() +{ + Curses *p=this; + + do + { + p=p->getPrevFocus(); + } while (p != NULL && !p->isFocusable()); + + if (p) + p->requestFocus(); +} + +bool Curses::childPositionCompareFunc(Curses *a, Curses *b) +{ + if (a->getRow() < b->getRow()) + return true; + + if (a->getRow() == b->getRow()) + { + if (a->getCol() < b->getCol()) + return true; + if (a->getCol() == b->getCol()) + { + CursesContainer *p=a->getParent(); + size_t ai, bi; + + for (ai=0; ai<p->children.size(); ai++) + if (a == p->children[ai].child) + { + for (bi=0; bi<p->children.size(); bi++) + if (b == p->children[bi].child) + return ai < bi; + break; + } + } + } + return false; +} + +Curses *Curses::getNextFocus() +{ + CursesContainer *p=getParent(); + Curses *child=this; + + while (p != NULL) + { + size_t i; + Curses *nextFocusPtr=NULL; + + for (i=0; i<p->children.size(); i++) + if ( childPositionCompareFunc(child, + p->children[i].child) && + (nextFocusPtr == NULL || + childPositionCompareFunc(p->children[i].child, + nextFocusPtr))) + nextFocusPtr=p->children[i].child; + + if (nextFocusPtr) + return nextFocusPtr; + + if (p->isDialog()) + break; // Do not cross dialog box boundaries. + + child=p; + p=p->getParent(); + } + return NULL; +} + +Curses *Curses::getPrevFocus() +{ + CursesContainer *p=getParent(); + Curses *child=this; + + while (p != NULL) + { + size_t i; + + Curses *prevFocusPtr=NULL; + + for (i=0; i<p->children.size(); i++) + if ( childPositionCompareFunc(p->children[i].child, + child) && + (prevFocusPtr == NULL || + childPositionCompareFunc(prevFocusPtr, + p->children[i].child))) + prevFocusPtr=p->children[i].child; + + if (prevFocusPtr) + return prevFocusPtr; + + if (p->isDialog()) + break; // Do not cross dialog box boundaries. + + child=p; + p=p->getParent(); + } + return NULL; +} diff --git a/curses/cursesbutton.C b/curses/cursesbutton.C new file mode 100644 index 0000000..3638860 --- /dev/null +++ b/curses/cursesbutton.C @@ -0,0 +1,136 @@ +/* +** Copyright 2002, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#include "curses_config.h" +#include "cursesbutton.H" + +using namespace std; + +CursesButton::CursesButton(CursesContainer *parent, + string textArg, + int toggle) : CursesLabel(parent, ""), + toggleButton(toggle), + buttonName(textArg) +{ + setStyle(toggleButton ? TOGGLE:NORMAL); +} + +CursesButton::~CursesButton() +{ + erase(); +} + +void CursesButton::setText(string textArg) +{ + buttonName=textArg; + setStyle(currentStyle); +} + +void CursesButton::setToggled(bool flag) +{ + toggleButton= flag ? -1:1; + setStyle(TOGGLE); +} + +void CursesButton::setStyle(Style style) +{ + currentStyle=style; + + switch (currentStyle) { + case TOGGLE: + CursesLabel::setText((toggleButton < 0 ? "[X] ":"[ ] ") + buttonName); + break; + case MENU: + CursesLabel::setText(buttonName); + break; + default: + CursesLabel::setText("[ " + buttonName + " ]"); + break; + } +} + +void CursesButton::draw() +{ + int fg=attribute.getFgColor(); + int bg=attribute.getBgColor(); + + switch (currentStyle) { + case TOGGLE: + attribute=Curses::CursesAttr(); + break; + case MENU: + attribute=Curses::CursesAttr().setReverse(hasFocus()); + break; + default: + if (hasFocus()) + attribute=Curses::CursesAttr().setReverse(); + else + attribute=Curses::CursesAttr().setHighlight(); + break; + } + + attribute.setFgColor(fg); + attribute.setBgColor(bg); + CursesLabel::draw(); +} + +bool CursesButton::isFocusable() +{ + return true; +} + +void CursesButton::focusGained() +{ + draw(); +} + +void CursesButton::focusLost() +{ + draw(); +} + +int CursesButton::getCursorPosition(int &r, int &c) +{ + r=0; + + if (currentStyle == TOGGLE) + { + c=1; + CursesLabel::getCursorPosition(r, c); + return 1; + } + + c=CursesLabel::getWidth(); + + CursesLabel::getCursorPosition(r, c); + + return 0; +} + +int CursesButton::getWidth() const +{ + return CursesLabel::getWidth()+1; +} + +void CursesButton::clicked() +{ +} + +bool CursesButton::processKeyInFocus(const Key &key) +{ + if (key == key.ENTER || (key.plain() && key.ukey == ' ')) + { + if (toggleButton) + { + toggleButton= -toggleButton; + setStyle(TOGGLE); + } + clicked(); + return true; + } + + return CursesLabel::processKeyInFocus(key); +} diff --git a/curses/cursesbutton.H b/curses/cursesbutton.H new file mode 100644 index 0000000..216c248 --- /dev/null +++ b/curses/cursesbutton.H @@ -0,0 +1,103 @@ +/* +** Copyright 2002, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#ifndef cursesbutton_H +#define cursesbutton_H + +#include "mycurses.H" +#include "curseslabel.H" + +//////////////////////////////////////////////////////////////////////// +// +// A plain, garden variety, button. Centered, or right-aligned, perhaps. +// +// CursesButton simply subclasses CursesLabel, makes it focusable, and +// overrides WriteText. +// + +class CursesButton : public CursesLabel { + + int toggleButton; + + std::string buttonName; + +public: + // Button style: + + enum Style { + NORMAL, + TOGGLE, + MENU + }; + +private: + Style currentStyle; +public: + CursesButton(CursesContainer *parent, + std::string textArg, int toggle=0); + ~CursesButton(); + + void setStyle(Style); + void draw(); + + bool isFocusable(); + void focusGained(); + void focusLost(); + void setText(std::string textArg); + + int getCursorPosition(int &r, int &c); + + int getWidth() const; + + bool processKeyInFocus(const Curses::Key &key); + virtual void clicked(); + + int getSelected() { return toggleButton < 0; } + void setToggled(bool flag); +}; + +////////////////////////////////////////////////////////////////////////// +// +// Instead of subclassing CursesButton, here's a template to have it be a +// member of another class. Typical usage: +// +// class X { +// +// CursesButtonRedirect<X> button1; +// +// void button1clicked(); +// } ; +// +// X::X() +// { +// button1=this; +// button1=&X::button1clicked; +// } + +template<class T> class CursesButtonRedirect : public CursesButton { + + T *myClass; + void (T::*mymethod)(); +public: + CursesButtonRedirect(CursesContainer *parent, + std::string textArg, int toggle=0) + : CursesButton(parent, textArg, toggle) + { + } + + ~CursesButtonRedirect() + { + } + + void operator=(T *p) { myClass=p; } + void operator=(void (T::*p)()) {mymethod=p; } + + void clicked() { (myClass->*mymethod)(); } + + T *getObject() const { return myClass; } +}; + +#endif diff --git a/curses/curseschoicebutton.C b/curses/curseschoicebutton.C new file mode 100644 index 0000000..be607d4 --- /dev/null +++ b/curses/curseschoicebutton.C @@ -0,0 +1,38 @@ +/* +** Copyright 2002, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#include "curses_config.h" +#include "curseschoicebutton.H" + +using namespace std; + +CursesChoiceButton::CursesChoiceButton(CursesContainer *parent) + : CursesButton(parent, ""), + selectedOption(0) +{ +} + +void CursesChoiceButton::setOptions(const vector<string> &optionListArg, + size_t initialOption) +{ + optionList=optionListArg; + selectedOption=initialOption; + setText(optionListArg[selectedOption]); +} + +CursesChoiceButton::~CursesChoiceButton() +{ +} + +void CursesChoiceButton::clicked() +{ + if (optionList.size() > 0) + { + selectedOption = (selectedOption+1) % optionList.size(); + setText(optionList[selectedOption]); + } +} + diff --git a/curses/curseschoicebutton.H b/curses/curseschoicebutton.H new file mode 100644 index 0000000..bfa223b --- /dev/null +++ b/curses/curseschoicebutton.H @@ -0,0 +1,81 @@ +/* +** Copyright 2002, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#ifndef curseschoicebutton_H +#define curseschoicebutton_H + +#include "mycurses.H" +#include "cursesbutton.H" + +#include <vector> + +//////////////////////////////////////////////////////////////////////// +// +// A button that cycles through a small list of options, each time it +// is "clicked". +// + +class CursesChoiceButton : public CursesButton { + + std::vector<std::string> optionList; + size_t selectedOption; + +public: + CursesChoiceButton(CursesContainer *parent); + ~CursesChoiceButton(); + + void setOptions(const std::vector<std::string> &optionListArg, + size_t initialOption); + + size_t getSelectedOption() const { return selectedOption; } + + void clicked(); +}; + +////////////////////////////////////////////////////////////////////////// +// +// Instead of subclassing CursesChoiceButton, here's a template to have it be a +// member of another class. Typical usage: +// +// class X { +// +// CursesChoiceButtonRedirect<X> button1; +// +// void button1clicked(); +// } ; +// +// X::X() +// { +// button1=this; +// button1=&X::button1clicked; +// } + +template<class T> class CursesChoiceButtonRedirect + : public CursesChoiceButton { + + T *myClass; + void (T::*mymethod)(); +public: + CursesChoiceButtonRedirect(CursesContainer *parent) + : CursesChoiceButton(parent) + { + } + + ~CursesChoiceButtonRedirect() + { + } + + void operator=(T *p) { myClass=p; } + void operator=(void (T::*p)()) {mymethod=p; } + + void clicked() + { + CursesChoiceButton::clicked(); + (myClass->*mymethod)(); + } +}; + +#endif diff --git a/curses/cursescontainer.C b/curses/cursescontainer.C new file mode 100644 index 0000000..9ce1db7 --- /dev/null +++ b/curses/cursescontainer.C @@ -0,0 +1,178 @@ +/* +** Copyright 2002, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + + +#include "cursescontainer.H" + +using namespace std; + +CursesContainer::CursesContainer(CursesContainer *parent) + : Curses(parent), drawIndex(0) +{ +} + +int CursesContainer::getWidth() const +{ + vector<Child>::const_iterator b=children.begin(), e=children.end(); + + int maxw=0; + + while (b != e) + { + const Child &c= *b++; + + // If the child's alignment is parent's center, child's + // getColAligned() will call this->getWidth() again! + // Prevent this infinite loop by considering child's + // position at column 0 + + int w= c.child->getAlignment() == PARENTCENTER + ? 0:c.child->getColAligned(); + + w += c.child->getWidth(); + + if (w > maxw) + maxw=w; + } + return maxw; +} + +int CursesContainer::getHeight() const +{ + vector<Child>::const_iterator b=children.begin(), e=children.end(); + + int maxh=0; + + while (b != e) + { + const Child &c= *b++; + + int h=c.child->getRowAligned() + c.child->getHeight(); + + if (h > maxh) + maxh=h; + } + return maxh; +} + +void CursesContainer::addChild(Curses *child) +{ + if (child->getParent()) + child->getParent()->deleteChild(child); + + children.push_back(Child(child)); + + child->setParent(this); +} + +void CursesContainer::deleteChild(Curses *child) +{ + vector<Child>::iterator b=children.begin(), e=children.end(); + + while (b != e) + { + Child &c= *b; + + if (c.child == child) + { + child->setParent(0); + children.erase(b, b+1); + break; + } + b++; + } +} + +CursesContainer::~CursesContainer() +{ + vector<Child>::iterator b=children.begin(), e=children.end(); + + while (b != e) + (*b++).child->setParent(0); +} + +Curses *CursesContainer::getDialogChild() const +{ + vector<Child>::const_iterator b, e; + + b=children.begin(); + e=children.end(); + + while (b != e) + { + Curses *p=(*b++).child; + + if (p->isDialog()) + return p; + } + + return NULL; +} + +void CursesContainer::draw() +{ + vector<Child>::iterator b, e; + + Curses *p=getDialogChild(); + + if (p) + { + p->draw(); + return; + } + + b=children.begin(); + e=children.end(); + + while (b != e) + (*b++).child->draw(); +} + +void CursesContainer::erase() +{ + vector<Child>::iterator b, e; + + Curses *p=getDialogChild(); + + if (p) + { + p->erase(); + return; + } + + b=children.begin(); + e=children.end(); + + while (b != e) + (*b++).child->erase(); +} + +void CursesContainer::resized() +{ + vector<Child>::iterator b=children.begin(), e=children.end(); + + while (b != e) + (*b++).child->resized(); +} + +Curses *CursesContainer::getNextFocus() +{ + if (children.size() == 0) + return Curses::getNextFocus(); + + return children[0].child; +} + +Curses *CursesContainer::getPrevFocus() +{ + size_t n=children.size(); + + if (n == 0) + return Curses::getNextFocus(); + --n; + + return children[n].child; +} diff --git a/curses/cursescontainer.H b/curses/cursescontainer.H new file mode 100644 index 0000000..11f45f3 --- /dev/null +++ b/curses/cursescontainer.H @@ -0,0 +1,76 @@ +/* +** Copyright 2002, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#ifndef cursescontainer_H +#define cursescontainer_H + +#include "../curses/curses_config.h" +#include "mycurses.H" + +#include <wchar.h> +#include <vector> + +//////////////////////////////////////////////////////////////////////// +// +// CursesContainer is a superclass for all Curses objects that contain +// other Curses object. + +class CursesContainer : public Curses { + + class Child { + public: + Curses *child; + + Child(Curses *childArg) : child(childArg) + { + } + }; + + std::vector<Child> children; + + std::vector<Child>::iterator drawIndex; + +public: + friend class Curses; + + CursesContainer(CursesContainer *parent=0); + ~CursesContainer(); + + // getWidth()/getHeight() computes the largest rectangle that + // contains all current children. + + virtual int getWidth() const; + virtual int getHeight() const; + + virtual Curses *getDialogChild() const; + + // draw/erase recursively invoke draw/erase methods of all children, + // unless a child CursesObject's isDialog() method returns true, in + // which case only that child's draw/erase method is called. + + virtual void draw(); + virtual void erase(); + + // The default resized() method recursively invokes the resized() + // method of all children. + + virtual void resized(); + + // The constructor and the destructor of a Curses object automatically + // calls addChild/deleteChild to register the new child process. + + virtual void addChild(Curses *child); + virtual void deleteChild(Curses *child); + + // The default implementation of getNextFocus()/getPrevFocus() + // return the pointer to the first or the last child. + + virtual Curses *getNextFocus(); + virtual Curses *getPrevFocus(); +}; + + +#endif diff --git a/curses/cursesdialog.C b/curses/cursesdialog.C new file mode 100644 index 0000000..fb87a97 --- /dev/null +++ b/curses/cursesdialog.C @@ -0,0 +1,233 @@ +/* +** Copyright 2002-2011, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#include "curses_config.h" +#include "cursesdialog.H" +#include "curseslabel.H" +#include "cursesfield.H" + +using namespace std; + +CursesDialog::CursesDialog(CursesContainer *parent) + : CursesContainer(parent), max_label_width(0), max_field_width(0), + draw_flag(0) +{ +} + +CursesDialog::~CursesDialog() +{ +} + +// Automatically size to the largest prompt/input field combo + +int CursesDialog::getWidth() const +{ + return max_label_width + max_field_width; +} + +// Automatically size to the lowest input field + +int CursesDialog::getHeight() const +{ + int h=0; + + vector< pair<CursesLabel *, Curses *> >::const_iterator + b=prompts.begin(), e=prompts.end(); + + while (b != e) + { + if (b->first && b->first->getRow() >= h) + h=b->first->getRow()+1; + + if (b->second && b->second->getRow() >= h) + h=b->second->getRow()+1; + b++; + + } + + return h; +} + +void CursesDialog::draw() +{ + draw_flag=1; + CursesContainer::draw(); +} + +void CursesDialog::addPrompt(CursesLabel *label, Curses *field) +{ + size_t maxrow=0; + + vector< pair<CursesLabel *, Curses *> >::iterator + b=prompts.begin(), e=prompts.end(); + + while (b != e) + { + if ( b->first != NULL && (size_t)b->first->getRow() >= maxrow) + maxrow=b->first->getRow()+1; + + if ( b->second != NULL && (size_t)b->second->getRow() >=maxrow) + maxrow=b->second->getRow()+1; + + b++; + } + + addPrompt(label, field, maxrow); +} + + +void CursesDialog::addPrompt(CursesLabel *label, Curses *field, size_t atRow) +{ + vector< pair<CursesLabel *, Curses *> >::iterator + b=prompts.begin(), e=prompts.end(); + + // If the new field should precede the existing fields, push them down + // by one row. + + while (b != e) + { + if ( b->first != NULL && (size_t)b->first->getRow() >= atRow) + b->first->setRow(b->first->getRow()+1); + + if ( b->second != NULL && (size_t)b->second->getRow() >= atRow) + b->second->setRow(b->second->getRow()+1); + b++; + } + + if (label != NULL) + { + label->setAlignment(Curses::RIGHT); + label->setRow(atRow); + addChild(label); + } + + if (field != NULL) + { + field->setRow(atRow); + addChild(field); + } + + prompts.push_back(make_pair(label, field)); + + int w; + + if (label != NULL) + { + w=label->getWidth(); + + if (w > max_label_width) + max_label_width=w; + } + + if (field != NULL) + { + w=field->getWidth(); + + if (w > max_field_width) + max_field_width=w; + } + + b=prompts.begin(); + e=prompts.end(); + + while (b != e) + { + if (b->first != NULL) + b->first->setCol(max_label_width); + if (b->second != NULL) + b->second->setCol(max_label_width); + b++; + } +} + +void CursesDialog::delPrompt(CursesLabel *label) +{ + vector< pair<CursesLabel *, Curses *> >::iterator + b=prompts.begin(), e=prompts.end(); + + while (b != e) + { + if (b->first == label) + { + delPrompt(b, b->first->getRow()); + return; + } + b++; + } +} + +void CursesDialog::delPrompt(Curses *field) +{ + vector< pair<CursesLabel *, Curses *> >::iterator + b=prompts.begin(), e=prompts.end(); + + while (b != e) + { + if (b->second == field) + { + delPrompt(b, b->second->getRow()); + return; + } + b++; + } +} + +void CursesDialog::delPrompt(vector< pair<CursesLabel *, Curses *> >::iterator + p, int row) +{ + prompts.erase(p); + + vector< pair<CursesLabel *, Curses *> >::iterator + b=prompts.begin(), e=prompts.end(); + + while (b != e) + { + if (b->first && b->first->getRow() > row) + b->first->setRow(b->first->getRow()-1); + + if (b->second && b->second->getRow() > row) + b->second->setRow(b->second->getRow()-1); + b++; + } +} + +bool CursesDialog::writeText(const char *text, int row, int col, + const CursesAttr &attr) const +{ + if (draw_flag) + return CursesContainer::writeText(text, row, col, attr); + return false; +} + +bool CursesDialog::writeText(const std::vector<unicode_char> &text, + int row, int col, + const Curses::CursesAttr &attr) const +{ + if (draw_flag) + CursesContainer::writeText(text, row, col, attr); + return false; +} + + +void CursesDialog::deleteChild(Curses *child) +{ + vector< pair<CursesLabel *, Curses *> >::iterator + b=prompts.begin(), e=prompts.end(); + + while (b != e) + { + if ( b->first != NULL && + ((Curses *)b->first) == child) + b->first=NULL; + + if ( b->second != NULL && + ((Curses *)b->second) == child) + b->second=NULL; + b++; + } + + CursesContainer::deleteChild(child); +} diff --git a/curses/cursesdialog.H b/curses/cursesdialog.H new file mode 100644 index 0000000..f31d1cc --- /dev/null +++ b/curses/cursesdialog.H @@ -0,0 +1,74 @@ +/* +** Copyright 2002, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#ifndef cursesdialog_H +#define cursesdialog_H + +#include "../curses/curses_config.h" +#include "cursescontainer.H" + +#include <vector> + +//////////////////////////////////////////////////////////////////////////// +// +// A helper container that formats dialog screens. +// +// The CursesDialog contains a list of fields, and their corresponding labels. +// The labels are right aligned. +// + +class CursesLabel; +class CursesField; + +class CursesDialog : public CursesContainer { + + std::vector< std::pair<CursesLabel *, Curses *> > prompts; + + int max_label_width; + int max_field_width; + + // To prevent a lot of extra work, quietly eat all WriteText()s + // until the dialog is complete. + int draw_flag; + +public: + CursesDialog(CursesContainer *parent); + ~CursesDialog(); + + // Calculate max size. + + int getWidth() const; + int getHeight() const; + + void draw(); + + // Add a new input field, and its corresponding prompt + // (either one may be NULL, both can be NULL to append some useful + // filler). + + virtual void addPrompt(CursesLabel *label, + Curses *field); + virtual void addPrompt(CursesLabel *label, + Curses *field, + size_t atRow); + void delPrompt(CursesLabel *label); + void delPrompt(Curses *field); + +private: + void delPrompt(std::vector< std::pair<CursesLabel *, Curses *> >::iterator p, + int row); +public: + + void deleteChild(Curses *child); + + bool writeText(const char *text, int row, int col, + const CursesAttr &attr) const; + bool writeText(const std::vector<unicode_char> &text, int row, int col, + const Curses::CursesAttr &attr) const; + +}; + +#endif diff --git a/curses/cursesfield.C b/curses/cursesfield.C new file mode 100644 index 0000000..498ab67 --- /dev/null +++ b/curses/cursesfield.C @@ -0,0 +1,689 @@ +/* +** Copyright 2002-2011, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#include "curses_config.h" +#include "cursesfield.H" +#include "curseskeyhandler.H" +#include "cursesmoronize.H" +#include "widechar.H" +#include <algorithm> +#include <iostream> + +std::vector<unicode_char> CursesField::yesKeys, CursesField::noKeys; +unicode_char CursesField::yankKey='\x19'; +unicode_char CursesField::clrEolKey='\x0B'; + +std::list<CursesFlowedLine> CursesField::cutBuffer; // for cut/paste + +CursesField::CursesField(CursesContainer *parent, + size_t widthArg, + size_t maxlengthArg, + std::string initValue) + : Curses(parent), width(widthArg), maxlength(maxlengthArg), + shiftoffset(0), + selectpos(0), passwordChar(0), yesnoField(false), + isUnderlined(true) +{ + if (yesKeys.size() == 0) + mail::iconvert::convert(std::string("yY"), + unicode_default_chset(), + yesKeys); + + if (noKeys.size() == 0) + mail::iconvert::convert(std::string("nN"), + unicode_default_chset(), + yesKeys); + + std::vector<unicode_char> utext; + + mail::iconvert::convert(initValue, unicode_default_chset(), utext); + + text.set_contents(utext, std::vector<unicode_char>()); + + selectpos=text.before_insert.graphemes.size(); +} + +void CursesField::setAttribute(Curses::CursesAttr attr) +{ + attribute=attr; + draw(); +} + +CursesField::~CursesField() +{ + if (optionHelp.size() > 0) + CursesKeyHandler::handlerListModified=true; +} + +void CursesField::setOptionHelp(const std::vector< std::pair<std::string, + std::string> > + &help) +{ + CursesKeyHandler::handlerListModified=true; + optionHelp=help; +} + +void CursesField::setRow(int row) +{ + erase(); + Curses::setRow(row); + draw(); +} + +void CursesField::setCol(int col) +{ + erase(); + Curses::setCol(col); + draw(); +} + +void CursesField::setText(std::string textArg) +{ + std::vector<unicode_char> utext; + + mail::iconvert::convert(textArg, unicode_default_chset(), utext); + + text.set_contents(utext, std::vector<unicode_char>()); + + selectpos=text.before_insert.graphemes.size(); + shiftoffset=0; + draw(); +} + +std::string CursesField::getText() const +{ + std::vector<unicode_char> b, a; + + text.get_contents(b, a); + + b.insert(b.end(), a.begin(), a.end()); + + return mail::iconvert::convert(b, unicode_default_chset()); +} + +int CursesField::getWidth() const +{ + return width; +} + +void CursesField::setWidth(int w) +{ + if (w > 0) + width=w; + draw(); +} + +int CursesField::getHeight() const +{ + return 1; +} + +void CursesField::getbeforeafter(widecharbuf &wbefore, widecharbuf &wafter) +{ + std::vector<unicode_char> before, after; + text.get_contents(before, after); + + wbefore.init_unicode(before.begin(), before.end()); + wafter.init_unicode(after.begin(), after.end()); +} + +void CursesField::draw() +{ + widecharbuf wbefore, wafter; + + getbeforeafter(wbefore, wafter); + + wafter.expandtabs(wbefore.expandtabs(0)); + + size_t a=wbefore.graphemes.size(); + size_t b=selectpos; + + if (a > b) + { + size_t c=a; a=b; b=c; + } + + if (a < shiftoffset) + a=shiftoffset; + + if (b < shiftoffset) + b=shiftoffset; + + a -= shiftoffset; + b -= shiftoffset; + + wbefore += wafter; + + std::vector<unicode_char> ubeforesel, usel, uaftersel; + size_t usel_pos=0, uaftersel_pos=0; + + size_t p=shiftoffset; + size_t col=0; + + size_t virtual_col=0; + + for (size_t i=0; i<p && i < wbefore.graphemes.size(); ++i) + virtual_col += wbefore.graphemes[i].wcwidth(virtual_col); + + std::vector<unicode_char> unicodes; + + while (p < wbefore.graphemes.size() && col < width) + { + const widecharbuf::grapheme_t &grapheme=wbefore.graphemes[p]; + + size_t grapheme_width=grapheme.wcwidth(virtual_col); + + if (grapheme_width + col > width) + break; + + std::vector<unicode_char> *vp; + + if (a) + { + vp= &ubeforesel; + --a; + --b; + usel_pos += grapheme_width; + uaftersel_pos += grapheme_width; + } + else if (b) + { + vp= &usel; + --b; + uaftersel_pos += grapheme_width; + } + else + vp= &uaftersel; + + unicodes.clear(); + + if (passwordChar) + vp->insert(vp->end(), grapheme_width, passwordChar); + else + vp->insert(vp->end(), grapheme.uptr, + grapheme.uptr+grapheme.cnt); + + col += grapheme_width; + virtual_col += grapheme_width; + ++p; + } + + uaftersel.insert(uaftersel.end(), width-col, ' '); + + bool underlined=isUnderlined; + + { + Curses::CursesAttr aa=attribute; + + if (ubeforesel.size()) + writeText(ubeforesel, 0, 0, + aa.setUnderline(underlined)); + } + + { + Curses::CursesAttr aa=attribute; + + if (usel.size()) + writeText(usel, 0, usel_pos, + aa.setReverse().setUnderline(underlined)); + } + + { + Curses::CursesAttr aa=attribute; + + if (uaftersel.size()) + writeText(uaftersel, 0, uaftersel_pos, + aa.setUnderline(underlined)); + } +} + +void CursesField::erase() +{ + std::vector<unicode_char> v; + + v.insert(v.end(), width, ' '); + + writeText(v, 0, 0, CursesAttr()); +} + +bool CursesField::isFocusable() +{ + return true; +} + +void CursesField::focusGained() +{ + text.to_before(); + + selectpos=0; + shiftoffset=0; + draw(); +} + +void CursesField::focusLost() +{ + text.to_after(); + selectpos=shiftoffset=0; + draw(); +} + +int CursesField::getCursorPosition(int &row, int &col) +{ + // Automatically scroll field to keep the cursor visible + + row=0; + + size_t save_shiftoffset=shiftoffset; + + widecharbuf wbefore, wafter; + + col=text.adjust_shift_pos(shiftoffset, width, wbefore, wafter); + + if (save_shiftoffset != shiftoffset) + draw(); + + Curses::getCursorPosition(row, col); + + return selectpos == wbefore.graphemes.size(); +} + +// Set selection position to current position + +void CursesField::setselectpos() +{ + text.insert_to_before(); + + widecharbuf wbefore, wafter; + + getbeforeafter(wbefore, wafter); + + selectpos=wbefore.graphemes.size(); +} + +bool CursesField::processKeyInFocus(const Key &key) +{ + if (key == key.HOME) + { + text.to_after(); + shiftoffset=0; + selectpos=0; + draw(); + return true; + } + + if (key == key.SHIFTHOME) + { + text.to_after(); + shiftoffset=0; + draw(); + return true; + } + + if (key == key.END) + { + text.to_before(); + setselectpos(); + draw(); + return true; + } + + if (key == key.SHIFTEND) + { + setselectpos(); + text.to_before(); + draw(); + return true; + } + + if (key == key.LEFT) + { + left(); + draw(); + return true; + } + + if (key == key.SHIFTLEFT) + { + size_t savepos=selectpos; + left(); + selectpos=savepos; + draw(); + return true; + } + + if (key == key.RIGHT) + { + right(); + draw(); + return true; + } + + if (key == key.SHIFTRIGHT) + { + size_t savepos=selectpos; + right(); + + selectpos=savepos; + draw(); + return true; + } + + if ((key.plain() && key.ukey == '\t') || + key == key.DOWN || key == key.UP || + key == key.SHIFTDOWN || key == key.SHIFTUP || key == key.ENTER || + key == key.PGDN || key == key.SHIFTPGDN || + key == key.PGUP || key == key.SHIFTPGUP) + { + setselectpos(); + draw(); + return Curses::processKeyInFocus(key); + } + + std::vector<unicode_char> cut_text; + + if (text.inserted.empty() && key != key.SHIFT) + text.contents_cut(selectpos, cut_text); + + if (!cut_text.empty()) + { + if (!(key.plain() && key.ukey == yankKey)) + { + cutBuffer.clear(); + cutBuffer.push_back(CursesFlowedLine(mail::iconvert + ::convert(cut_text, + "utf-8"), + false)); + } + + selectpos=text.before_insert.graphemes.size(); + draw(); + + if (key == key.BACKSPACE || key == key.DEL) + return true; + } + + if (key == key.BACKSPACE) + { + text.insert_to_before(); + + if (text.before_insert.graphemes.size() > 0) + { + text.contents_cut(text.before_insert + .graphemes.size()-1, cut_text); + setselectpos(); + } + draw(); + return true; + } + + if (key == key.DEL) + { + text.insert_to_before(); + + if (text.after_insert.graphemes.size() > 0) + { + text.contents_cut(text.before_insert + .graphemes.size()+1, cut_text); + setselectpos(); + } + draw(); + return true; + } + + if (!key.plain()) + { + return Curses::processKeyInFocus(key); + } + + unicode_char k=key.ukey; + + if (k == yankKey && !yesnoField && + optionField.size() == 0 && !cutBuffer.empty()) + ; // Will paste later + else if (k < ' ') + { + return Curses::processKeyInFocus(key); + } + + bool doYesNoField=true; + + if (optionField.size() > 0) + { + text.inserted.push_back(k); + + std::vector<unicode_char> before, after; + + text.get_contents(before, after); + + before.insert(before.end(), after.begin(), after.end()); + + if (before.empty()) + return true; + + text.inserted.pop_back(); + + std::vector<unicode_char>::iterator p= + find(optionField.begin(), + optionField.end(), *before.begin()); + + + if (p == optionField.end()) + { + if (!yesnoField) + return true; + } + else doYesNoField=false; + + } + + if (doYesNoField && yesnoField) + { + text.inserted.push_back(k); + + std::vector<unicode_char> before, after; + + text.get_contents(before, after); + + before.insert(before.end(), after.begin(), after.end()); + + if (before.empty()) + return true; + + std::vector<unicode_char>::iterator p= + find(yesKeys.begin(), + yesKeys.end(), *before.begin()); + + if (p != yesKeys.end()) + { + before.clear(); + after.clear(); + before.push_back('Y'); + text.set_contents(before, after); + this->processKeyInFocus(Key(Key::ENTER)); + // Automatic enter + return true; + } + + p=find(noKeys.begin(), noKeys.end(), *before.begin()); + + if (p != noKeys.end()) + { + before.clear(); + after.clear(); + before.push_back('N'); + text.set_contents(before, after); + this->processKeyInFocus(Key(Key::ENTER)); + // Automatic enter + return true; + } + + text.inserted.pop_back(); + } + + if (k == yankKey) + { + std::vector<unicode_char> ucut; + + if (!cutBuffer.empty()) + mail::iconvert::convert(cutBuffer.front().text, + "utf-8", + ucut); + + text.insert_to_before(); + + std::vector<unicode_char> before, after; + text.get_contents(before, after); + + before.insert(before.end(), ucut.begin(), ucut.end()); + + widecharbuf wbefore, wafter; + + wbefore.init_unicode(before.begin(), before.end()); + wafter.init_unicode(after.begin(), after.end()); + + size_t wbefore_w=wbefore.wcwidth(0); + size_t wafter_w=wafter.wcwidth(wbefore_w); + + if (wbefore_w + wafter_w > maxlength) + beepError(); + else + text.set_contents(before, after); + } + else + { + text.inserted.push_back(k); + + widecharbuf wbefore, wafter; + + getbeforeafter(wbefore, wafter); + + size_t wbefore_w=wbefore.wcwidth(0); + size_t wafter_w=wafter.wcwidth(wbefore_w); + + if (wbefore_w + wafter_w > maxlength) + { + beepError(); + text.inserted.pop_back(); + } + } + + { + widecharbuf wbefore, wafter; + + getbeforeafter(wbefore, wafter); + + selectpos=wbefore.graphemes.size(); + } + + draw(); + + if (yesnoField || optionField.size() > 0) + this->processKeyInFocus(Key(Key::ENTER)); // Automatic enter + else if (CursesMoronize::enabled && !passwordChar && + !text.inserted.empty()) + { + size_t i; + unicode_char m_buf[CursesMoronize::max_keycode_len+1]; + std::vector<unicode_char>::iterator b(text.inserted.begin()), + e(text.inserted.end()), p=e; + + for (i=0; i<sizeof(m_buf)/sizeof(m_buf[0])-1; ++i) + { + if (b == p) + break; + + --p; + + m_buf[i]= *p; + } + + m_buf[i]=0; + + if (m_buf[0] && CursesMoronize::enabled) + { + std::vector<unicode_char> repl_c; + + i=CursesMoronize::moronize(m_buf, repl_c); + + if (i > 0) + { + while (i > 0) + { + processKeyInFocus(Curses::Key + (Curses::Key::BACKSPACE)); + --i; + } + + std::string s(mail::iconvert::convert + (repl_c, unicode_default_chset())); + + std::vector<wchar_t> wc; + + towidechar(s.begin(), s.end(), wc); + + for (std::vector<wchar_t>::iterator + b(wc.begin()), e(wc.end()); b != e; + ++b) + { + processKeyInFocus(Curses::Key(*b)); + } + } + } + } + + return true; +} + +void CursesField::left() +{ + text.insert_to_before(); + + if (text.before_insert.graphemes.size() > 0) + { + std::vector<unicode_char> cut_text; + + text.contents_cut(text.before_insert.graphemes.size()-1, + cut_text); + + std::vector<unicode_char> before, after; + text.get_contents(before, after); + + after.insert(after.begin(), cut_text.begin(), cut_text.end()); + + text.set_contents(before, after); + } + setselectpos(); + draw(); +} + +void CursesField::right() +{ + text.insert_to_before(); + + if (text.after_insert.graphemes.size() > 0) + { + std::vector<unicode_char> cut_text; + + text.contents_cut(text.before_insert.graphemes.size()+1, + cut_text); + + std::vector<unicode_char> before, after; + text.get_contents(before, after); + + before.insert(before.end(), cut_text.begin(), cut_text.end()); + + text.set_contents(before, after); + } + setselectpos(); + draw(); +} + +void CursesField::setCursorPos(size_t o) +{ + text.to_position(o); + setselectpos(); + draw(); +} diff --git a/curses/cursesfield.H b/curses/cursesfield.H new file mode 100644 index 0000000..164ae4c --- /dev/null +++ b/curses/cursesfield.H @@ -0,0 +1,160 @@ +/* +** Copyright 2002-2011, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#ifndef cursesfield_H +#define cursesfield_H + +#include "mycurses.H" +#include "cursesflowedline.H" +#include <wchar.h> +#include <vector> + +/////////////////////////////////////////////////////////////////////////// +// +// An editable field. +// + +class CursesField : public Curses { + + editablewidechar text; // Editable text. + + void getbeforeafter(widecharbuf &, widecharbuf &); + + void setselectpos(); + + size_t width; // Field width + size_t maxlength; // Maximum text length + + size_t shiftoffset; // First text char shown + size_t selectpos; // Start of selection position + + unicode_char passwordChar; // password overlay character + bool yesnoField; // True if yes/no field. + + std::vector<unicode_char> optionField; // One character list of options + + std::vector<std::pair<std::string, std::string> > optionHelp; // Option help keys + // This field is of primary use by cursesstatusbar + + bool isUnderlined; +protected: + Curses::CursesAttr attribute; +public: + static std::vector<unicode_char> yesKeys, noKeys; + static unicode_char yankKey, clrEolKey; + + static std::list<CursesFlowedLine> cutBuffer; // for cut/paste. + + CursesField(CursesContainer *parent, + size_t widthArg=20, + size_t maxlengthArg=255, + std::string initValue=""); + ~CursesField(); + + void setUnderlined(bool flag) { isUnderlined=flag; } + void setRow(int row); + void setCol(int col); + void setText(std::string textArg); + void setAttribute(Curses::CursesAttr attr); + void setCursorPos(size_t o); + std::string getText() const; // Return entered text + + int getWidth() const; // Our width is known + int getHeight() const; // We're one row high + + void setWidth(int); + + void setPasswordChar(unicode_char ch='*') { passwordChar=ch; } + void setYesNo() { yesnoField=true; } + void setOptionField(const std::vector<unicode_char> &vec) + { + optionField=vec; + } + + void setOptionHelp(const std::vector< std::pair<std::string, std::string> > &help); + + const std::vector< std::pair<std::string, std::string> > &getOptionHelp() const + { + return optionHelp; + } + + void draw(); + + int getCursorPosition(int &row, int &col); + bool processKeyInFocus(const Key &key); + + bool isFocusable(); // Yes we are + void focusGained(); // Select old text automatically at entry. + void focusLost(); // Unselect any selected text at exit. + + void erase(); +private: + void left(); + void right(); +}; + +////////////////////////////////////////////////////////////////////////// +// +// Instead of subclassing a CursesField, provide a template to make it easier +// to have CursesField be a member of another object. Typical usage: +// +// class X { +// +// CursesFieldRedirect<X> field1; +// CursesFieldRedirect<X> field2; +// +// void field1Enter(); // ENTER key pressed in field1 +// void field2Enter(); // ENTER key pressed in field2 +// +// }; +// +// +// X::X() +// { +// +// field1=this; +// field2=this; +// +// field1= &X::field1Enter; +// field2= &X::field2Enter; +// } +// + +template<class T> class CursesFieldRedirect : public CursesField { + +public: + T *myClass; + void (T::*myMethod)(void); + + CursesFieldRedirect(CursesContainer *parent, + size_t widthArg=20, + size_t maxlengthArg=255, + std::string initValue="") + : CursesField(parent, widthArg, maxlengthArg, initValue), + myClass(0), myMethod(0) + { + } + + ~CursesFieldRedirect() + { + } + + void operator=(T *p) { myClass=p; } + void operator=( void (T::*p)(void) ) { myMethod=p; } + + bool processKeyInFocus(const Key &key) + { + if (key == Key::ENTER && myClass && myMethod) + { + (myClass->*myMethod)(); + return true; + } + + return CursesField::processKeyInFocus(key); + } +}; + +#endif diff --git a/curses/cursesfilereq.C b/curses/cursesfilereq.C new file mode 100644 index 0000000..1d2aac4 --- /dev/null +++ b/curses/cursesfilereq.C @@ -0,0 +1,808 @@ +/* +** Copyright 2002-2011, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#include "curses_config.h" +#include "cursesfilereq.H" +#include "cursesmainscreen.H" + +#include <algorithm> + +#include <sstream> +#include <string.h> +#include <pwd.h> + +#if HAVE_UNISTD_H +#include <unistd.h> +#endif + +#include <sys/types.h> +#if HAVE_DIRENT_H +#include <dirent.h> +#define NAMLEN(dirent) strlen((dirent)->d_name) +#else +#define dirent direct +#define NAMLEN(dirent) (dirent)->d_namlen +#if HAVE_SYS_NDIR_H +#include <sys/ndir.h> +#endif +#if HAVE_SYS_DIR_H +#include <sys/dir.h> +#endif +#if HAVE_NDIR_H +#include <ndir.h> +#endif +#endif +#include <sys/stat.h> + +#if HAVE_GLOB_H +#include <glob.h> +#endif + +CursesFileReq::Dirlist::Dirlist(CursesFileReq *parentArg) + : CursesVScroll(parentArg), parent(parentArg), currentRow(-1) +{ +} + +CursesFileReq::Dirlist::~Dirlist() +{ +} + +int CursesFileReq::Dirlist::getHeight() const +{ + int h=parent->getHeight(); + int r=getRow(); + + return (r < h ? h-r:1); +} + +int CursesFileReq::Dirlist::getWidth() const +{ + return (parent->getWidth()); +} + +void CursesFileReq::Dirlist::draw() +{ + size_t n, wh; + + getVerticalViewport(n, wh); + + size_t h=directory.size() / 2; + + size_t i; + + for (i=0; i<wh; i++) + { + size_t p=n+i; + + if (p < h) + drawItem(p); + + if (p + h < directory.size()) + drawItem(p+h); + } +} + +void CursesFileReq::Dirlist::drawItem(size_t n) +{ + if (n >= directory.size()) + return; + + size_t h=directory.size()/2; + + size_t w=getWidth()/2; + + if (w > 2) + --w; + + if (w <= 0) + w=1; + + size_t rowNum=n; + size_t colNum=0; + + if (rowNum >= h) + { + rowNum -= h; + colNum = w+2; + } + + std::pair<std::vector<unicode_char>, size_t> nameW; + + { + widecharbuf wc; + + wc.init_string(directory[n].name); + wc.expandtabs(0); + nameW=wc.get_unicode_truncated(w, 0); + } + + std::string s= (*CursesFileReq::fmtSizeFunc)(directory[n].size, + directory[n].isDir); + + std::pair<std::vector<unicode_char>, size_t> sizeW; + + sizeW.second=0; + if (nameW.second < w) + { + widecharbuf wc; + + wc.init_string(s); + wc.expandtabs(0); + sizeW=wc.get_unicode_truncated(w-nameW.second, 0); + } + + nameW.first.insert(nameW.first.end(), w - nameW.second - sizeW.second, + ' '); + + nameW.first.insert(nameW.first.end(), sizeW.first.begin(), + sizeW.first.end()); + + writeText(nameW.first, rowNum, colNum, + CursesAttr().setReverse(currentRow >= 0 && + (size_t)currentRow == n)); +} + +void CursesFileReq::Dirlist::operator=(std::vector<Direntry> &directoryArg) +{ + directory=directoryArg; + currentRow= -1; + scrollTo(0); + draw(); +} + +int CursesFileReq::Dirlist::getCursorPosition(int &row, int &col) +{ + if (currentRow < 0) + { + row=0; + col=0; + CursesVScroll::getCursorPosition(row, col); + return 0; + } + + size_t h=directory.size()/2; + + size_t w=getWidth()/2; + + if (w > 2) + --w; + + if (w <= 0) + w=1; + + row=currentRow; + col=w-1; + + if ( (size_t)currentRow >= h) + { + row -= h; + col += w+2; + } + CursesVScroll::getCursorPosition(row, col); + return 0; +} + +bool CursesFileReq::Dirlist::isFocusable() +{ + return true; +} + +bool CursesFileReq::Dirlist::processKeyInFocus(const Key &key) +{ + if (key == key.UP) + { + if (currentRow == 0) + { + Curses::transferPrevFocus(); + return true; + } + + --currentRow; + drawItem(currentRow); + drawItem(currentRow+1); + return true; + } + + if (key == key.DOWN) + { + if ((size_t)currentRow + 1 >= directory.size()) + { + Curses::transferNextFocus(); + return true; + } + + ++currentRow; + drawItem(currentRow); + drawItem(currentRow-1); + return true; + } + + if (key == key.PGUP) + { + size_t h=getHeight(); + + size_t oldRow=currentRow; + + currentRow=h < (size_t)currentRow ? currentRow-h:0; + + drawItem(oldRow); + drawItem(currentRow); + return true; + } + + if (key == key.PGDN) + { + size_t h=getHeight(); + + size_t oldRow=currentRow; + + currentRow=currentRow + h < directory.size() ? currentRow+h: + directory.size() > 0 ? directory.size()-1:0; + + drawItem(oldRow); + drawItem(currentRow); + return true; + } + + if (key == key.LEFT) + { + size_t h=directory.size() / 2; + + if ((size_t)currentRow >= h && (size_t)currentRow - h < h) + { + currentRow -= h; + drawItem(currentRow); + drawItem(currentRow+h); + } + return true; + } + + if (key == key.RIGHT) + { + size_t h=directory.size() / 2; + + if ((size_t)currentRow < h && + (size_t)currentRow + h < directory.size()) + { + currentRow += h; + drawItem(currentRow); + drawItem(currentRow-h); + } + return true; + } + + if (key == key.ENTER) + { + if (currentRow != -1 && + (size_t)currentRow < directory.size()) + parent->select(directory[currentRow]); + return true; + } + + return false; +} + +// Must override getPrevFocus/getNextFocus in CursesContainer, because we +// do not have any children and should behave like Curses. + +Curses *CursesFileReq::Dirlist::getPrevFocus() +{ + return Curses::getPrevFocus(); +} + +Curses *CursesFileReq::Dirlist::getNextFocus() +{ + return Curses::getNextFocus(); +} + +void CursesFileReq::Dirlist::focusGained() +{ + currentRow=0; + draw(); +} + +void CursesFileReq::Dirlist::focusLost() +{ + currentRow= -1; + draw(); +} + +std::string CursesFileReq::currentDir; + +static std::string defaultSizeFunc(unsigned long size, bool isDir) +{ + if (isDir) + return " (Dir)"; + + std::string buffer; + + { + std::ostringstream o; + + o << " (" << size << ")"; + buffer=o.str(); + } + + return buffer; +} + +std::string (*CursesFileReq::fmtSizeFunc)(unsigned long, bool)= &defaultSizeFunc; + +CursesFileReq::CursesFileReq(CursesMainScreen *mainScreen, + const char *filenameLabelArg, + const char *directoryLabelArg) + : CursesContainer(mainScreen), CursesKeyHandler(PRI_DIALOGHANDLER), + directoryLabel(this, directoryLabelArg), + directoryName(this, ""), + filenameLabel(this, filenameLabelArg), + filenameField(this), + dirlist(this), + parent(mainScreen) +{ + filenameField=this; + filenameField= &CursesFileReq::filenameEnter; + + int w=directoryLabel.getWidth(); + int ww=filenameLabel.getWidth(); + + if (ww > w) + w=ww; + + directoryLabel.setRow(0); + directoryLabel.setCol(w); + directoryLabel.setAlignment(directoryLabel.RIGHT); + + filenameLabel.setRow(2); + filenameLabel.setCol(w); + filenameLabel.setAlignment(filenameLabel.RIGHT); + + directoryName.setRow(0); + directoryName.setCol(w); + + filenameField.setRow(2); + filenameField.setCol(w); + + dirlist.setRow(4); + + if (currentDir.size() == 0) + { + std::string homedir=getenv("HOME"); + + if (homedir.size() == 0) + { + struct passwd *pw=getpwuid(getuid()); + + if (pw) + homedir=pw->pw_dir; + else + homedir=""; + } + + currentDir=homedir; + } + + doresized(); + opendir(); +} + +int CursesFileReq::getWidth() const +{ + return parent->getWidth(); +} + +int CursesFileReq::getHeight() const +{ + return (parent->getHeight()); +} + +CursesFileReq::~CursesFileReq() +{ +} + +bool CursesFileReq::isDialog() const +{ + return true; +} + +void CursesFileReq::resized() +{ + doresized(); + CursesContainer::resized(); +} + +void CursesFileReq::draw() +{ + CursesContainer::draw(); +} + +void CursesFileReq::doresized() +{ + int w=getWidth(); + + int c=filenameField.getCol(); + + filenameField.setWidth(c > w ? 1: w-c); + + // If too narrow fo the full directory name, truncate it. + + c=directoryName.getCol(); + + size_t dls=c < w ? w-c:1; + + std::vector<unicode_char> directoryW; + + mail::iconvert::convert(currentDir, unicode_default_chset(), + directoryW); + + size_t directoryW_size; + + { + widecharbuf wc; + + wc.init_unicode(directoryW.begin(), directoryW.end()); + wc.expandtabs(0); + + wc.tounicode(directoryW); + + directoryW_size=wc.wcwidth(0); + } + + if (directoryW_size > dls) + { + std::vector<unicode_char>::iterator b, e, c, lastSlash; + + b=directoryW.begin(); + e=directoryW.end(); + + lastSlash=b; + + for (c=b; c != e; ) + { + if (*c++ == '/') + lastSlash=c; + } + + std::vector<unicode_char> replVector; + + replVector.push_back('/'); + replVector.push_back('.'); + replVector.push_back('.'); + replVector.push_back('.'); + replVector.push_back('/'); + + replVector.insert(replVector.end(), lastSlash, e); + + size_t suffixSize; + + { + widecharbuf wc; + + wc.init_unicode(replVector.begin(), replVector.end()); + + suffixSize=wc.wcwidth(0); + } + + if (suffixSize < dls) + { + widecharbuf wc; + + wc.init_unicode(directoryW.begin(), lastSlash); + + directoryW=wc.get_unicode_truncated(dls-suffixSize, 0) + .first; + + lastSlash=b=directoryW.begin(); + e=directoryW.end(); + + for (c=b; c != e; ++c) + { + if (*c == '/') + lastSlash=c; + } + + } + else + { + lastSlash=directoryW.begin(); + } + + directoryW.erase(lastSlash, directoryW.end()); + + directoryW.insert(directoryW.end(), + replVector.begin(), replVector.end()); + } + + directoryName.setText(mail::iconvert::convert(directoryW, + unicode_default_chset())); +} + +void CursesFileReq::requestFocus() +{ + filenameField.requestFocus(); +} + +void CursesFileReq::select(Direntry &d) +{ + if (d.isDir) // Open this directory. + { + currentDir = currentDir + "/" + d.name; + opendir(); + Curses::erase(); + doresized(); + draw(); + filenameField.requestFocus(); + return; + } + + std::vector<std::string> filenameList; + + filenameList.push_back(currentDir + "/" + d.name); + selected(filenameList); +} + +void CursesFileReq::expand(std::string pattern, + std::vector<std::string> &filenameList) +{ + filenameList.clear(); + +#if HAVE_GLOB + + glob_t pglob; + + if (glob(pattern.c_str(), GLOB_ERR|GLOB_NOCHECK + +#ifdef GLOB_BRACE + | GLOB_BRACE +#endif + +#ifdef GLOB_NOMAGIC + | GLOB_NOMAGIC +#endif + +#ifdef GLOB_TILDE + | GLOB_TILDE +#endif + , NULL, &pglob) == 0) + { + if (pglob.gl_pathc > 0) + { + try { + size_t i; + + for (i=0; i<pglob.gl_pathc; i++) + { + filenameList + .push_back(washfname(pglob + .gl_pathv + [i])); + } + } catch (...) { + globfree(&pglob); + throw; + } + } + globfree(&pglob); + } + +#endif + if (filenameList.size() == 0) + { + filenameList.push_back(washfname(pattern)); + } +} + + +void CursesFileReq::filenameEnter() +{ + std::string s=filenameField.getText(); + + if (s.size() == 0) + return; + + // Relative path? Prepend current directory + + if (s[0] != '/') + s=currentDir + "/" + s; + + std::vector<std::string> filenameList; + + expand(s, filenameList); + + struct stat stat_buf; + + if (filenameList.size() == 1 && + stat(filenameList[0].c_str(), &stat_buf) == 0 && + S_ISDIR(stat_buf.st_mode)) + { + currentDir=filenameList[0]; + filenameField.setText(""); + opendir(); + Curses::erase(); + doresized(); + draw(); + filenameField.requestFocus(); + return; + } + + + selected(filenameList); +} + +bool CursesFileReq::processKey(const Curses::Key &key) +{ + if (key.plain() && key.ukey == '\x03') + { + abort(); + return true; + } + return key.plain() && (unsigned char)key.ukey == key.ukey && + (unsigned char)key.ukey < ' '; +} + +void CursesFileReq::selected(std::vector<std::string> &filenameList) +{ + selectedFiles=filenameList; + keepgoing=false; +} + +void CursesFileReq::abort() +{ + selectedFiles.clear(); + keepgoing=false; +} + +void CursesFileReq::opendir() +{ + currentDir=washfname(currentDir); // cleanup filename. + + std::vector<Direntry> newDirList; + + { + std::vector<std::string> filenames; + + DIR *dirp= ::opendir(currentDir.c_str()); + + struct dirent *de; + + try { + while (dirp && (de=readdir(dirp)) != NULL) + filenames.push_back(de->d_name); + + if (dirp) + closedir(dirp); + } catch (...) { + if (dirp) + closedir(dirp); + throw; + } + + // stat everything, figure out if its a dir or not + + std::vector<std::string>::iterator b=filenames.begin(), + e=filenames.end(); + + while (b != e) + { + Direntry d; + + d.name= *b; + + struct stat statInfo; + + std::string n=currentDir + "/" + *b++; + + if (stat(n.c_str(), &statInfo) < 0) + continue; + + d.size=statInfo.st_size; + + d.isDir= S_ISDIR(statInfo.st_mode); + + newDirList.push_back(d); + } + } + + sort(newDirList.begin(), newDirList.end(), DirentryComparator()); + + dirlist=newDirList; +} + +std::string CursesFileReq::washfname(std::string filename) +{ + filename += "/"; // Temporary thing. + + std::string::iterator b, c, e; + + b=filename.begin(); + e=filename.end(); + c=b; + + // Combine multiple /s into one. + + while (b != e) + { + if (*b != '/') + { + *c++ = *b++; + continue; + } + + while (b != e && *b == '/') + b++; + + *c++ = '/'; + } + + filename.erase(c, e); + + b=filename.begin(); + e=filename.end(); + c=b; + + while (b != e) + { + if (*b == '/') + { + if (e - b >= 3 && + b[1] == '.' && b[2] == '/') + { + b += 2; + continue; + } + else if (e - b >= 4 && + b[1] == '.' && b[2] == '.' && b[3] == '/') + { + b += 3; + + while (c != filename.begin()) + { + if (*--c == '/') + break; + } + continue; + } + } + + *c++ = *b++; + } + + if (c != filename.begin()) + --c; // Trailing / + + filename.erase(c, e); + return filename; +} + +// Sort filename list. Directories first, then files. + +int CursesFileReq::DirentryComparator::group(const Direntry &d) +{ + if (d.name == ".") + return 0; + + if (d.name == "..") + return 1; + + if (d.isDir) + return 2; + return 3; +} + +bool CursesFileReq::DirentryComparator::operator()(const Direntry &a, + const Direntry &b) +{ + int d=group(a) - group(b); + + if (d != 0) + return (d < 0); + + return (strcoll(a.name.c_str(), b.name.c_str()) < 0); +} + +bool CursesFileReq::listKeys( std::vector< std::pair<std::string, std::string> > &list) +{ + return true; +} diff --git a/curses/cursesfilereq.H b/curses/cursesfilereq.H new file mode 100644 index 0000000..da83a1f --- /dev/null +++ b/curses/cursesfilereq.H @@ -0,0 +1,175 @@ +/* +** Copyright 2002, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#ifndef cursesfilereq_H +#define cursesfilereq_H + +#include "../curses/curses_config.h" + +#include "cursescontainer.H" +#include "curseskeyhandler.H" +#include "cursesfield.H" +#include "curseslabel.H" +#include "cursesvscroll.H" + +#include <vector> + +class CursesMainScreen; + +//////////////////////////////////////////////////////////////////////////// +// +// A file requester dialog. It consists of an editable field, where file +// or a directory name may be manually entered, and scrollable contents of +// the current directory (and some miscellaneous labels, here and there) + +class CursesFileReq : public CursesContainer, + public CursesKeyHandler { + + struct Direntry { + std::string name; + unsigned long size; + bool isDir; + }; + + // Sorting collator - directories go first. + + class DirentryComparator { + + static int group(const Direntry &d); + + public: + bool operator()(const Direntry &a, const Direntry &b); + }; + + + class Dirlist : public CursesVScroll { + + CursesFileReq *parent; + std::vector<Direntry> directory; // True - this is a dir entry + int currentRow; // Current row where the cursor is + + void drawItem(size_t n); + public: + Dirlist(CursesFileReq *parentArg); + ~Dirlist(); + + // This is a child of CursesFileReq, and its size is + // automatically extended to the bottom of its parent. + // Its width is the same as the parent's width. + + int getHeight() const; + int getWidth() const; + + bool isFocusable(); // Yes we are. + + void focusGained(); // Move to the first row + void focusLost(); // Turn off cursor + void draw(); + + // Even though this is a CursesContainer subclass, its focus + // behavior must be the same as Curses's focus behavior + // (the default CursesContainer implementation doesn't work, + // because this object does not have any children). + + Curses *getPrevFocus(); + Curses *getNextFocus(); + + void operator=(std::vector<Direntry> &directoryArg); + + int getCursorPosition(int &row, int &col); + bool processKeyInFocus(const Key &key); + + }; + + CursesLabel directoryLabel; + CursesLabel directoryName; + CursesLabel filenameLabel; + CursesFieldRedirect<CursesFileReq> filenameField; + + Dirlist dirlist; // Current directory's contents. + + void doresized(); // Reset directoryName's contents + + bool isDialog() const; // Yes we are + + CursesMainScreen *parent; // My parent + + void opendir(); // Open a new directory + +public: + + // The current directory (persistent). + static std::string currentDir; + + // + // An externally-provided function that converts a byte count to + // a displayable size (such as "14Kb") + // + // A default implementation is provided, but it can be overriden, + // if necessary. + + static std::string (*fmtSizeFunc)(unsigned long size, bool isDir); + + CursesFileReq(CursesMainScreen *mainScreen, + const char *filenameLabel="Filename: ", + const char *directoryLabel="Directory: "); + ~CursesFileReq(); + + // Provide a default filename + + void setFilename(std::string n) + { + filenameField.setText(n); + } + void resized(); + void draw(); + void requestFocus(); + int getWidth() const; + int getHeight() const; + + // Select the following entry. + + void select(Direntry &d); + + // Callback function - file(s) have been selected. + // The default implementation saves the filenames in selectedFile, + // and sets Curses::keepgoing to false. + // + // More than one file can be selected using globs. + + virtual void selected(std::vector<std::string> &); + + // Callback function - file selection aborted. + // The default implementation sets selectedFiles to empty + // and sets Curses::keepgoing to false. + virtual void abort(); + +private: + bool processKey(const Curses::Key &key); + + void filenameEnter(); // Enter pressed in the filename field + + std::vector<std::string> selectedFiles; // The selected files + + // + // Standize the filename. Remove all references to "/./" and "/../" + // + +public: + static std::string washfname(std::string); + + static void expand(std::string pattern, + std::vector<std::string> &filenameList); + + bool listKeys( std::vector< std::pair<std::string, std::string> > &list); + + std::vector<std::string> &getFilenameList() + { + return selectedFiles; + } +}; + +#endif diff --git a/curses/cursesflowedline.H b/curses/cursesflowedline.H new file mode 100644 index 0000000..c46fc6d --- /dev/null +++ b/curses/cursesflowedline.H @@ -0,0 +1,32 @@ +/* +** Copyright 2011, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#ifndef cursesflowedline_H +#define cursesflowedline_H + +#include <string> + +// +// A line of text, a string, encoded in UTF-8, and a flag whether this line +// flows (or wraps) into the next one. +// + +class CursesFlowedLine { + +public: + std::string text; + bool flowed; + + CursesFlowedLine(const std::string &textArg="", bool flowedArg=false) + : text(textArg), flowed(flowedArg) + { + } + + CursesFlowedLine(const std::vector<unicode_char> &textArg, + bool flowedArg); +}; + +#endif diff --git a/curses/curseskeyhandler.C b/curses/curseskeyhandler.C new file mode 100644 index 0000000..3ca30bd --- /dev/null +++ b/curses/curseskeyhandler.C @@ -0,0 +1,75 @@ +/* +** Copyright 2002, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#include "curses_config.h" +#include "curseskeyhandler.H" + +#include <functional> +#include <algorithm> + +using namespace std; + +list<CursesKeyHandler *> CursesKeyHandler::handlers; +bool CursesKeyHandler::handlerListModified=true; + +CursesKeyHandler::CursesKeyHandler(int priorityArg) + : priority(priorityArg) +{ + list<CursesKeyHandler *>::iterator b=handlers.begin(), + e=handlers.end(); + + while (b != e) + { + if ( (*b)->priority > priorityArg) + break; + b++; + } + + handlers.insert(b, 1, this); + + handlerListModified=true; +} + +CursesKeyHandler::~CursesKeyHandler() +{ + list<CursesKeyHandler *>::iterator me= + find_if(handlers.begin(), handlers.end(), + bind2nd(equal_to<CursesKeyHandler *>(), this)); + + handlers.erase(me); + handlerListModified=true; +} + +bool CursesKeyHandler::listKeys( vector< pair<string, string> > &list) +{ + return false; +} + +bool CursesKeyHandler::handle(const Curses::Key &key, Curses *focus) +{ + list<CursesKeyHandler *>::iterator b=handlers.begin(), + e=handlers.end(); + + while (b != e) + { + CursesKeyHandler *p= *b++; + + if (p->priority >= 0 && focus) + { + if (focus->processKeyInFocus(key)) + return true; + focus=NULL; + } + + if ( p->processKey(key)) + return true; + } + + if (focus) + if (focus->processKeyInFocus(key)) + return true; + return false; +} diff --git a/curses/curseskeyhandler.H b/curses/curseskeyhandler.H new file mode 100644 index 0000000..7b95b5e --- /dev/null +++ b/curses/curseskeyhandler.H @@ -0,0 +1,79 @@ +/* +** Copyright 2002, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#ifndef curseskeyhandler_H +#define curseskeyhandler_H + +#include <list> + +#include "mycurses.H" + +#define PRI_STATUSOVERRIDEHANDLER -4 // Override even the status line +#define PRI_STATUSHANDLER -3 // Status line +#define PRI_DIALOGHANDLER -2 // Dialog +#define PRI_PRISCREENHANDLER -1 // Normal screenwide handler, + // but prior to any focus handling +#define PRI_SCREENHANDLER 0 // Normal screenwide handler +#define PRI_DEFAULTCTRLCHANDLER 1 // CTRL-C handler + +///////////////////////////////////////////////////////////////////////////// +// +// A list of prioritized key handlers. CursesKeyHandler objects are created +// whenever a function key shortcut should be used. Each CursesKeyHandler +// defines a processKey() method that returns true if it has processed the +// received key input. The object should also implement listKeys(), to append +// a list of keys it handles to the list argument. +// +// Each key handler has a defined priority. The handle() method runs +// the processKey method of all defined handlers, in priority order, until +// processKey returns true. +// The processKeyInFocus method of the specified Curses object may be called +// (if focus is not NULL), in the event that no key handler with a negative +// priority processed the key (and if processKeyInFocus also doesn't handle +// the key, any remaining non-negative keyhandlers are given a crack at this). + +class CursesKeyHandler { + + int priority; + +public: + CursesKeyHandler(int priorityArg); + virtual ~CursesKeyHandler(); + + static bool handle(const Curses::Key &key, Curses *focus); + // Returns true if the key was consumed. + +protected: + virtual bool processKey(const Curses::Key &key)=0; + + // Key handlers should subclass this and enumerate all the keys + // they handle. <key name, description> should be added to list. + // Subclass should return true to ignore the rest of keyhandlers. +public: + virtual bool listKeys( std::vector< std::pair<std::string, + std::string> > &list); + +private: + static std::list<CursesKeyHandler *> handlers; + +public: + static std::list<CursesKeyHandler *>::const_iterator begin() + { + return handlers.begin(); + } + + static std::list<CursesKeyHandler *>::const_iterator end() + { + return handlers.end(); + } + + static bool handlerListModified; + // Reset to true each time a handler is added or removed, used to + // indicate when the status line should be redrawn + +}; + +#endif diff --git a/curses/curseslabel.C b/curses/curseslabel.C new file mode 100644 index 0000000..fe49828 --- /dev/null +++ b/curses/curseslabel.C @@ -0,0 +1,96 @@ +/* +** Copyright 2002-2011, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#include "curses_config.h" +#include "curseslabel.H" + +using namespace std; + +CursesLabel::CursesLabel(CursesContainer *parent, + string textArg, + Curses::CursesAttr attributeArg) + : Curses(parent), attribute(attributeArg) +{ + setutext(textArg); +} + +CursesLabel::~CursesLabel() +{ +} + +void CursesLabel::setRow(int row) +{ + erase(); + Curses::setRow(row); + draw(); +} + +void CursesLabel::setCol(int col) +{ + erase(); + Curses::setCol(col); + draw(); +} + +void CursesLabel::setAlignment(Alignment newAlignment) +{ + erase(); + Curses::setAlignment(newAlignment); + draw(); +} + +void CursesLabel::setAttribute(Curses::CursesAttr attr) +{ + attribute=attr; + draw(); +} + +void CursesLabel::setText(string textArg) +{ + erase(); + setutext(textArg); + draw(); +} + +void CursesLabel::setutext(const std::string &textArg) +{ + std::vector<unicode_char> buf; + + mail::iconvert::convert(textArg, unicode_default_chset(), buf); + + widecharbuf wc; + + wc.init_unicode(buf.begin(), buf.end()); + wc.expandtabs(0); + + wc.tounicode(utext); + w=wc.wcwidth(0); +} + +int CursesLabel::getWidth() const +{ + return w; +} + +int CursesLabel::getHeight() const +{ + return 1; +} + +void CursesLabel::draw() +{ + writeText(utext, 0, 0, attribute); +} + +void CursesLabel::erase() +{ + std::vector<unicode_char> s; + + s.insert(s.end(), getWidth(), ' '); + + writeText(s, 0, 0, CursesAttr()); +} + diff --git a/curses/curseslabel.H b/curses/curseslabel.H new file mode 100644 index 0000000..574ff25 --- /dev/null +++ b/curses/curseslabel.H @@ -0,0 +1,47 @@ +/* +** Copyright 2002-2011, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#ifndef curseslabel_H +#define curseslabel_H + +#include "mycurses.H" + +//////////////////////////////////////////////////////////////////////// +// +// A plain, garden variety, label. Centered, or right-aligned, perhaps. +// +// A lot of work, just for this + +class CursesLabel : public Curses { + + std::vector<unicode_char> utext; + + size_t w; + + void setutext(const std::string &textArg); + +protected: + Curses::CursesAttr attribute; +public: + CursesLabel(CursesContainer *parent, + std::string textArg, + Curses::CursesAttr attributeArg=Curses::CursesAttr()); + ~CursesLabel(); + + void setRow(int row); + void setCol(int col); + void setAlignment(Alignment alignment); + void setAttribute(Curses::CursesAttr attr); + virtual void setText(std::string textArg); + + int getWidth() const; + int getHeight() const; + + void draw(); + void erase(); +}; + +#endif diff --git a/curses/cursesmainscreen.C b/curses/cursesmainscreen.C new file mode 100644 index 0000000..dae7539 --- /dev/null +++ b/curses/cursesmainscreen.C @@ -0,0 +1,76 @@ +/* +** Copyright 2002-2011, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#include "curses_config.h" + +#include "cursesmainscreen.H" + +CursesMainScreen::CursesMainScreen(CursesContainer *parent, + Curses *titleBarArg, + Curses *statusBarArg) + : CursesVScroll(parent), titleBar(titleBarArg), + statusBar(statusBarArg), lockcnt(0) +{ + setRow(titleBar->getHeight()); +} + +CursesMainScreen::~CursesMainScreen() +{ +} + +int CursesMainScreen::getWidth() const +{ + return getParent()->getWidth(); +} + +int CursesMainScreen::getHeight() const +{ + int h=getParent()->getHeight(); + + int hh=titleBar->getHeight() + statusBar->getHeight(); + + return (hh < h ? h-hh:0); +} + +CursesMainScreen::Lock::Lock(CursesMainScreen *p, bool noupdateArg) + : screen(p), noupdate(noupdateArg) +{ + if (p) + ++p->lockcnt; +} + +CursesMainScreen::Lock::~Lock() +{ + if (!screen.isDestroyed()) + { + if (--screen->lockcnt == 0 && !noupdate) + { + screen->erase(); + screen->draw(); + } + } +} + +bool CursesMainScreen::writeText(const char *text, int row, int col, + const CursesAttr &attr) const +{ + char dummy=0; + + if (lockcnt > 0) + text= &dummy; + + return CursesVScroll::writeText(text, row, col, attr); +} + +bool CursesMainScreen::writeText(const std::vector<unicode_char> &text, + int row, int col, + const Curses::CursesAttr &attr) const +{ + std::vector<unicode_char> dummy; + + return CursesVScroll::writeText(lockcnt > 0 ? dummy:text, + row, col, attr); +} diff --git a/curses/cursesmainscreen.H b/curses/cursesmainscreen.H new file mode 100644 index 0000000..fd667f6 --- /dev/null +++ b/curses/cursesmainscreen.H @@ -0,0 +1,66 @@ +/* +** Copyright 2002-2011, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#ifndef cursesmainscreen_H +#define cursesmainscreen_H + +#include "mycurses.H" +#include "cursesvscroll.H" + +// +// A CursesVScroll used for the main screen. It's parent is the CursesScreen, +// so it's size is defined as the width of its parent, and its parent's +// height minus the status bar height and the title bar height. +// + +class CursesMainScreen : public CursesVScroll { + + Curses *titleBar, *statusBar; + + // A lock can be obtained that blocks out updates to the screen. + // This is useful when we know in advance about multiple changes to + // the screen that will occur over a period of time, which might be + // confusing. A Lock structure prevents changes to the main screen + // from taking place. Instantiating a Lock structure increments + // lockcnt. The destructor decrements lockcnt. + // WriteText() is overriden and will terminate without doing anything + // if lockcnt is non-zero. + // When lockcnt is decrement back to 0 the entire object is redrawn. + + unsigned lockcnt; + +public: + + class Lock { + cursesPtr<CursesMainScreen> screen; + + Lock(const Lock &); // UNIMPLEMENTED + Lock &operator=(const Lock &); // UNIMPLEMENTED + + bool noupdate; + + public: + Lock(CursesMainScreen *, bool noupdateArg=false); + ~Lock(); + }; + + friend class Lock; + + CursesMainScreen(CursesContainer *parent, + Curses *titleBarArg, + Curses *statusBarArg); + ~CursesMainScreen(); + + int getWidth() const; + int getHeight() const; + + bool writeText(const char *text, int row, int col, + const CursesAttr &attr) const; + bool writeText(const std::vector<unicode_char> &text, int row, int col, + const Curses::CursesAttr &attr) const; +}; + +#endif diff --git a/curses/cursesmoronize.C b/curses/cursesmoronize.C new file mode 100644 index 0000000..9e3060c --- /dev/null +++ b/curses/cursesmoronize.C @@ -0,0 +1,97 @@ +/* +** Copyright 2003-2004, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#include "curses_config.h" +#include "cursesmoronize.H" + +bool CursesMoronize::enabled=false; + +static const unicode_char plain_169[]={ ')', 'C', '('}; +static const unicode_char plain_174[]={ ')', 'R', '('}; +static const unicode_char plain_177[]={ '-', '/', '+'}; +static const unicode_char plain_188[]={ ' ', '4', '/', '1'}; +static const unicode_char plain_189[]={ ' ', '2', '/', '1'}; +static const unicode_char plain_190[]={ ' ', '4', '/', '3'}; +static const unicode_char plain_8482[]={ ']', 'm', 't', '['}; +static const unicode_char plain_8592[]={ '-', '<'}; +static const unicode_char plain_8594[]={ '>', '-'}; +static const unicode_char plain_8220[]={ '`', '`'}; +static const unicode_char plain_8221[]={ '\'', '\''}; +static const unicode_char plain_8226[]={ ' ', '*', ' '}; +static const unicode_char plain_8230[]={ '.', '.', '.'}; +static const unicode_char plain_8211[]={ ' ', '-', '-', ' '}; +static const unicode_char plain_8212[]={ ' ', '-', '-', '-', ' '}; + + +static const unicode_char repl_169[]={ 169, 0}; +static const unicode_char repl_174[]={ 174, 0}; +static const unicode_char repl_177[]={ 177, 0}; +static const unicode_char repl_188[]={ 188, 0}; +static const unicode_char repl_189[]={ 189, 0}; +static const unicode_char repl_190[]={ 190, 0}; +static const unicode_char repl_8482[]={ 8482, 0}; +static const unicode_char repl_8592[]={ 8592, 0}; +static const unicode_char repl_8594[]={ 8594, 0}; +static const unicode_char repl_8220[]={ 8220, 0}; +static const unicode_char repl_8221[]={ 8221, 0}; +static const unicode_char repl_8226[]={ 8226, 0}; +static const unicode_char repl_8230[]={ 8230, 0}; +static const unicode_char repl_8211[]={ ' ', 8211, ' ', 0}; +static const unicode_char repl_8212[]={ ' ', 8212, ' ', 0}; + + +CursesMoronize::Entry CursesMoronize::moronizationList[] = { + { plain_169, 3, repl_169}, + { plain_174, 3, repl_174}, + { plain_177, 3, repl_177}, + { plain_188, 4, repl_188}, + { plain_189, 4, repl_189}, + { plain_190, 4, repl_190}, + { plain_8482, 4, repl_8482}, + { plain_8592, 2, repl_8592}, + { plain_8594, 2, repl_8594}, + { plain_8220, 2, repl_8220}, + { plain_8221, 2, repl_8221}, + { plain_8226, 3, repl_8226}, + { plain_8230, 3, repl_8230}, + { plain_8211, 4, repl_8211}, + { plain_8212, 5, repl_8212}, + { NULL, 0, 0}}; + +size_t CursesMoronize::moronize(const unicode_char *buf, + std::vector<unicode_char> &nreplaced) +{ + Entry *e=moronizationList; + + nreplaced.clear(); + + while (e->keycode) + { + size_t i=0; + + for (i=0; ; ++i) + { + if (i == e->keycodeLen) + { + if (e->replacements == 0) + return 0; + + for (i=0; e->replacements[i]; ++i) + nreplaced.push_back(e->replacements[i]); + return e->keycodeLen; + } + + if (i == e->keycodeLen || buf[i] == 0) + break; + + if (e->keycode[i] != buf[i]) + break; + } + + ++e; + } + return 0; +} diff --git a/curses/cursesmoronize.H b/curses/cursesmoronize.H new file mode 100644 index 0000000..c4d5371 --- /dev/null +++ b/curses/cursesmoronize.H @@ -0,0 +1,52 @@ +/* +** Copyright 2003-2004, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#ifndef cursesmoronize_H +#define cursesmoronize_H + +#include "mycurses.H" + +/////////////////////////////////////////////////////////////////////////// +// +// "Smart characters" processing, a.k.a. moronization +// Example: Convert typed "1/4" to the ISO-8859-1 character for 1/4. +// + +class CursesMoronize { + +public: + + + static size_t moronize(const unicode_char *buf, + std::vector<unicode_char> &nreplaced); + // + // 'buf' should be the characters just preceding the current cursor + // position, IN REVERSE ORDER. So, if the cursor is now: + // + // ...1/4_ _ marks the cursor position + // + // ... then 'buf' should be '4/1...'. buf can be as long as the + // caller wants. We search for a suitable replacement using + // strncmp + // + // If found a replacement, returns non-zero chars to replace, and + // set nreplaced to the replacement character + + class Entry { + public: + const unicode_char *keycode; + size_t keycodeLen; + const unicode_char *replacements; + }; + + static bool enabled; + static Entry moronizationList[]; + + static const int max_keycode_len=5; + +}; + +#endif diff --git a/curses/cursesmultilinelabel.C b/curses/cursesmultilinelabel.C new file mode 100644 index 0000000..5075914 --- /dev/null +++ b/curses/cursesmultilinelabel.C @@ -0,0 +1,126 @@ +/* +** Copyright 2004, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#include "curses_config.h" +#include "cursesmultilinelabel.H" + +CursesMultilineLabel::CursesMultilineLabel(CursesContainer *parent, + std::string textArg, + Curses::CursesAttr attributeArg) + : Curses(parent), width(0), attribute(attributeArg) +{ + mail::iconvert::convert(textArg, unicode_default_chset(), text); +} + +void CursesMultilineLabel::init() +{ + int w=getWidth(); + + if (w < 10) + w=10; + + lines.clear(); + + std::back_insert_iterator< std::vector< std::vector<unicode_char> > > + insert_iter(lines); + + unicodewordwrap(text.begin(), text.end(), + unicoderewrapnone(), insert_iter, w, true); +} + +CursesMultilineLabel::~CursesMultilineLabel() +{ +} + +void CursesMultilineLabel::setRow(int row) +{ + erase(); + Curses::setRow(row); + draw(); +} + +void CursesMultilineLabel::setCol(int col) +{ + erase(); + Curses::setCol(col); + draw(); +} + +void CursesMultilineLabel::setText(std::string newText) +{ + erase(); + text.clear(); + mail::iconvert::convert(newText, unicode_default_chset(), text); + init(); + draw(); +} + +void CursesMultilineLabel::setAlignment(Alignment newAlignment) +{ + erase(); + Curses::setAlignment(newAlignment); + draw(); +} + +void CursesMultilineLabel::setAttribute(Curses::CursesAttr attr) +{ + attribute=attr; + draw(); +} + +int CursesMultilineLabel::getWidth() const +{ + return width; +} + +void CursesMultilineLabel::setWidth(int w) +{ + erase(); + width=w; + init(); + draw(); +} + +int CursesMultilineLabel::getHeight() const +{ + int n=(int)lines.size(); + + if (n <= 0) + n=1; + return n; +} + +void CursesMultilineLabel::resized() +{ + erase(); + init(); + draw(); +} + +void CursesMultilineLabel::draw() +{ + erase(); + + size_t row=0; + + for ( std::vector< std::vector<unicode_char> >::iterator + b(lines.begin()), e(lines.end()); b != e; ++b, ++row) + { + writeText(*b, row, 0, attribute); + } +} + +void CursesMultilineLabel::erase() +{ + std::vector<unicode_char> uc; + + uc.insert(uc.end(), getWidth(), ' '); + + size_t i, n=getHeight(); + for (i=0; i<n; i++) + writeText(uc, i, 0, attribute); +} + diff --git a/curses/cursesmultilinelabel.H b/curses/cursesmultilinelabel.H new file mode 100644 index 0000000..00ec206 --- /dev/null +++ b/curses/cursesmultilinelabel.H @@ -0,0 +1,54 @@ +/* +** Copyright 2004-2011, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#ifndef cursesmultilinelabel_H +#define cursesmultilinelabel_H + +#include "mycurses.H" + +//////////////////////////////////////////////////////////////////////// +// +// A multiline label that's word-wrapped to a given width. +// A plain, garden variety, label. Centered, or right-aligned, perhaps. +// +// A lot of work, just for this + +class CursesMultilineLabel : public Curses { + + std::vector<unicode_char> text; + + std::vector< std::vector<unicode_char> > lines; + +protected: + int width; + + Curses::CursesAttr attribute; +public: + void init(); // Subclass must call init() in constructor. + + CursesMultilineLabel(CursesContainer *parent, + std::string textArg, + Curses::CursesAttr attributeArg + =Curses::CursesAttr()); + ~CursesMultilineLabel(); + + void setRow(int row); + void setCol(int col); + void resized(); + void setAlignment(Alignment alignment); + void setAttribute(Curses::CursesAttr attr); + virtual void setText(std::string textArg); + + int getWidth() const; + int getHeight() const; + + void setWidth(int w); + + void draw(); + void erase(); +}; + +#endif diff --git a/curses/cursesobject.C b/curses/cursesobject.C new file mode 100644 index 0000000..155f492 --- /dev/null +++ b/curses/cursesobject.C @@ -0,0 +1,40 @@ +/* +** Copyright 2002, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#include "curses_config.h" +#include "cursesobject.H" + +using namespace std; + +cursesPtrBase::cursesPtrBase() +{ +} + +cursesPtrBase::~cursesPtrBase() +{ +} + +CursesObj::CursesObj() +{ +} + +CursesObj::CursesObj(const CursesObj &o) +{ +} + +CursesObj &CursesObj::operator=(const CursesObj &o) +{ + return *this; +} + +CursesObj::~CursesObj() +{ + set<cursesPtrBase *>::iterator b=cursesBase.begin(), + e=cursesBase.end(); + + while (b != e) + (*b++)->ptrDestroyed(); +} diff --git a/curses/cursesobject.H b/curses/cursesobject.H new file mode 100644 index 0000000..377c539 --- /dev/null +++ b/curses/cursesobject.H @@ -0,0 +1,102 @@ +/* +** Copyright 2002, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#ifndef cursesobject_H +#define cursesobject_H + +/////////////////////////////////////////////////////////////////////////// +// +// Helper class that detects when the object being pointed to is destroyed. +// +// Subclass cursesObj. Declare CursesPtr<T>, where T is cursesObj's +// subclass. After CursesObj is destroyed, CursesPtr<T>::operator T *() +// will return NULL. +// +// The cursesObj object maintains a list of all CursesPtr<T>s that point to +// it. cursesObj's destructor nulls them out. + +#include "../curses/curses_config.h" +#include <set> +#include <cstdio> + +class cursesPtrBase { +public: + cursesPtrBase(); + virtual ~cursesPtrBase(); + virtual void ptrDestroyed()=0; +}; + +template<class T> class cursesPtr : public cursesPtrBase { + + T *ptr; + +public: + cursesPtr(T *ptrArg) : ptr(NULL) + { + if (ptrArg) + ptrArg->cursesBase.insert(this); + ptr=ptrArg; + } + + cursesPtr(const cursesPtr &o) : ptr(NULL) + { + (*this)=o; + } + + cursesPtr &operator=(const cursesPtr &o) + { + if (o.ptr == NULL || + o.ptr->cursesBase.count(this) == 0) + { + if (o.ptr) + o.ptr->cursesBase.insert(this); + + if (ptr && ptr->cursesBase.count(this) > 0) + ptr->cursesBase.erase(ptr->cursesBase + .find(this)); + } + ptr=o.ptr; + + return *this; + } + + ~cursesPtr() + { + if (ptr && ptr->cursesBase.count(this) > 0) + ptr->cursesBase.erase(ptr->cursesBase + .find(this)); + } + + operator T *() const + { + return ptr; + } + + T * operator->() const + { + return ptr; + } + + bool isDestroyed() const { return ptr == 0; } + + void ptrDestroyed() { ptr=NULL; } + +}; + +class CursesObj { + +public: + std::set<cursesPtrBase *> cursesBase; + + CursesObj(); + virtual ~CursesObj(); + + // UNDEFINED: + CursesObj(const CursesObj &); + CursesObj &operator=(const CursesObj &); +}; + +#endif diff --git a/curses/cursesscreen.C b/curses/cursesscreen.C new file mode 100644 index 0000000..0b0458a --- /dev/null +++ b/curses/cursesscreen.C @@ -0,0 +1,885 @@ +/* +** Copyright 2002-2011, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#include "curses_config.h" +#include <signal.h> +#include <unistd.h> +#include <fcntl.h> +#include <iomanip> +#include <iostream> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <cerrno> + +#if HAVE_SYS_WAIT_H +#include <sys/wait.h> +#endif +#ifndef WEXITSTATUS +#define WEXITSTATUS(stat_val) ((unsigned)(stat_val) >> 8) +#endif +#ifndef WIFEXITED +#define WIFEXITED(stat_val) (((stat_val) & 255) == 0) +#endif + +#include "cursesscreen.H" +#include "cursesfield.H" + +static unsigned char termStopKey= 'Z' & 31; + +static RETSIGTYPE bye(int dummy) +{ + endwin(); + kill( getpid(), SIGKILL); + _exit(0); +} + +int Curses::getColorCount() +{ + if (has_colors()) + return COLORS; + return 0; +} + + +CursesScreen::KeyReader::KeyReader() + : h(iconv_open(libmail_u_ucs4_native, unicode_default_chset())), + winput_cnt(0) +{ + if (h == (iconv_t)-1) + { + std::cerr << "Unable to initiale " + << unicode_default_chset() + << " mapping to UCS-4" << std::endl; + exit(1); + } +} + +CursesScreen::KeyReader::~KeyReader() +{ + if (h != (iconv_t)-1) + iconv_close(h); +} + +void CursesScreen::KeyReader::operator<<(char ch) +{ + input_buf.push_back(ch); + + winput_buf.resize(winput_cnt+128); + + while (input_buf.size() > 0) + { + + char *inbuf=&input_buf[0], *outbuf=&winput_buf[winput_cnt]; + size_t inbytesleft=input_buf.size(), + outbytesleft=winput_buf.size()-winput_cnt; + + size_t res=iconv(h, &inbuf, &inbytesleft, &outbuf, + &outbytesleft); + + if (inbuf != &input_buf[0]) + input_buf.erase(input_buf.begin(), + input_buf.begin() + + (inbuf - &input_buf[0])); + + winput_cnt=outbuf - &winput_buf[0]; + + if (res == (size_t)-1 && errno == EILSEQ) + { + if (input_buf.size() > 0) + input_buf.erase(input_buf.begin(), + input_buf.begin()+1); + continue; + } + + if (res == (size_t)-1 && errno == E2BIG) + winput_buf.resize(winput_buf.size()+128); + + if (res == (size_t)-1 && errno == EINVAL) + break; + } +} + +bool CursesScreen::KeyReader::operator>>(unicode_char &ch) +{ + if (winput_cnt < sizeof(ch)) + return false; + + memcpy(&ch, &winput_buf[0], sizeof(ch)); + + winput_buf.erase(winput_buf.begin(), + winput_buf.begin()+sizeof(ch)); + winput_cnt -= sizeof(ch); + return true; +} + +CursesScreen::CursesScreen() : inputcounter(0) +{ + initscr(); + start_color(); + +#if HAS_USE_DEFAULT_COLORS + use_default_colors(); +#endif + + // Assign meaningful colors + + if (has_colors()) + { + short f; + short b; + + int i; + + pair_content(0, &f, &b); + + int colorcount=getColorCount(); + + if (colorcount > 0) + --colorcount; + + for (i=1; i<COLOR_PAIRS; i++) + { + if (colorcount <= 1) + f=b=0; + else + { + f=(i / COLORS) % COLORS; + b=(i % COLORS); + } + +#if HAS_USE_DEFAULT_COLORS + if (b == 0) + b= -1; +#endif + init_pair(i, f, b); + } + } + + raw(); + noecho(); + nodelay(stdscr, true); + timeout(0); + nonl(); + intrflush(stdscr, FALSE); + keypad(stdscr, TRUE); + + // fcntl(0, F_SETFL, O_NONBLOCK); + + signal(SIGHUP, bye); + signal(SIGTERM, bye); + + save_w=COLS; + save_h=LINES; + shiftmode=false; + + const char *underline_hack_env=getenv("UNDERLINE_HACK"); + + underline_hack=underline_hack_env && atoi(underline_hack_env); +} + +CursesScreen::~CursesScreen() +{ + endwin(); +} + +int CursesScreen::getWidth() const +{ + return COLS; +} + +int CursesScreen::getHeight() const +{ + return LINES; +} + +void CursesScreen::draw() +{ + attroff(A_STANDOUT | A_BOLD | A_UNDERLINE | A_REVERSE); + clear(); + CursesContainer::draw(); +} + +void CursesScreen::resized() +{ + CursesContainer::resized(); + draw(); +} + +bool CursesScreen::writeText(const char *ctext, int row, int col, + const Curses::CursesAttr &attr) const +{ + std::vector<unicode_char> ubuf; + + mail::iconvert::convert(ctext, unicode_default_chset(), ubuf); + + return writeText(ubuf, row, col, attr); +} + +// +// Eliminate time-consuming expantabs() call in the hot writeText() codepath +// by loading widecharbuf from an iterator that replaces tabs with a single +// space character. writeText() should not get a string with tabs anyway. +// +// As a bonus, any NUL character gets also replaced with a space. +// + +class CursesScreen::repltabs_spaces : + public std::iterator<std::random_access_iterator_tag, + const unicode_char> { + + const unicode_char *p; + + unicode_char tmp; + +public: + repltabs_spaces() : p(0) {} + + repltabs_spaces(const unicode_char *pVal) : p(pVal) {} + + bool operator==(const repltabs_spaces &v) + { + return p == v.p; + } + + bool operator!=(const repltabs_spaces &v) + { + return p != v.p; + } + + bool operator<(const repltabs_spaces &v) + { + return p < v.p; + } + + bool operator<=(const repltabs_spaces &v) + { + return p <= v.p; + } + + bool operator>(const repltabs_spaces &v) + { + return p < v.p; + } + + bool operator>=(const repltabs_spaces &v) + { + return p <= v.p; + } + + unicode_char operator*() const { return operator[](0); } + + repltabs_spaces &operator++() { ++p; return *this; } + + const unicode_char *operator++(int) { tmp=operator*(); ++p; return &tmp; } + + repltabs_spaces &operator--() { --p; return *this; } + + const unicode_char *operator--(int) { tmp=operator*(); --p; return &tmp; } + + repltabs_spaces &operator+=(difference_type diff) { p += diff; return *this; } + + repltabs_spaces &operator-=(difference_type diff) { p -= diff; return *this; } + + repltabs_spaces operator+(difference_type diff) const { repltabs_spaces tmp=*this; tmp += diff; return tmp; } + repltabs_spaces operator-(difference_type diff) const { repltabs_spaces tmp=*this; tmp -= diff; return tmp; } + + difference_type operator-(const repltabs_spaces b) const { return p-b.p; } + + const unicode_char operator[](difference_type diff) const { return p[diff] == 0 || p[diff] == '\t' ? ' ':p[diff]; } +}; + + +// +// When writing EM and EN dashes, use the EM dash character, and write it out +// multiple times. Corresponds to logic in widecharbuf::charwidth(). +// + +class CursesScreen::writetext_iter_helper + : public std::iterator<std::input_iterator_tag, + char, void, void, void> { + + const unicode_char *uptr; + size_t multcnt; + +public: + writetext_iter_helper(const unicode_char *uptrArg) + : uptr(uptrArg), multcnt(0) {} + + unicode_char operator*() + { + unicode_char c=*uptr; + + if (c == 0x2014) + c=0x2013; + return c; + } + + writetext_iter_helper &operator++() + { + if (multcnt == 0) + switch (*uptr) { + case 0x2013: + multcnt=2; + break; + case 0x2014: + multcnt=3; + break; + default: + multcnt=1; + } + + if (--multcnt == 0) + ++uptr; + return *this; + } + + writetext_iter_helper operator++(int) + { + writetext_iter_helper helper=*this; + + operator++(); + return helper; + } + + bool operator==(const writetext_iter_helper &o) const + { + return uptr == o.uptr; + } + + bool operator!=(const writetext_iter_helper &o) const + { + return !operator==(o); + } +}; + + + +bool CursesScreen::writeText(const std::vector<unicode_char> &utext, + int row, int col, + const Curses::CursesAttr &attr) const +{ + // Truncate text to fit within the display + + if (row < 0 || row >= getHeight() || col >= getWidth()) + return false; + + widecharbuf wc; + + { + repltabs_spaces ptr(utext.size() ? &*utext.begin():NULL); + + wc.init_unicode(ptr, ptr+utext.size()); + } + + // Advance past any part of the script beyond the left margin + + bool left_margin_crossed=false; + + std::vector<widecharbuf::grapheme_t>::const_iterator + b(wc.graphemes.begin()), e(wc.graphemes.end()); + + if (b != e) + { + while (col < 0) + { + if (b == e) + return false; + + col += b->wcwidth(col); + ++b; + left_margin_crossed=true; + } + + if (col < 0 || b == e) + return false; + } + + if (left_margin_crossed) + { + // Clear any cells that contain a partial graphemes that + // extends from beyond the left margin + move(row, 0); + + for (int i=0; i<col; i++) + addstr(" "); + } + else + move(row, col); + + int w=getWidth(); + + if (col >= w) + return false; + + if (b == e) + return true; + + size_t cells=w-col; + + const unicode_char *uptr=b->uptr; + size_t ucnt=0; + + bool right_margin_crossed=false; + + while (b != e) + { + size_t grapheme_width=b->wcwidth(col); + + if (grapheme_width > cells) + { + if (cells) + right_margin_crossed=true; + break; + } + + ucnt += b->cnt; + cells -= grapheme_width; + col += grapheme_width; + ++b; + } + + std::vector<wchar_t> text_buf; + + { + std::string s; + + mail::iconvert::fromu::convert(writetext_iter_helper(uptr), + writetext_iter_helper(uptr+ucnt), + unicode_default_chset(), s); + + towidechar(s.begin(), s.end(), text_buf); + } + text_buf.push_back(0); + + wchar_t *text=&text_buf[0]; + + int c=0; + + // If color was requested, then the colors will wrap if the requested + // color number exceeds the # of colors reported by curses. + + int ncolors=getColorCount(); + + if (ncolors > 2 && COLOR_PAIRS > 0) + { + int bg=attr.getBgColor(); + int fg=attr.getFgColor(); + + if (fg || bg) + { + if (bg > 0) + bg= ((bg - 1) % (ncolors-1)) + 1; + + fg %= ncolors; + + if (fg == bg) + fg = (fg + ncolors/2) % ncolors; + } + + c=bg + ncolors * fg; + + c %= COLOR_PAIRS; + } + + if (c > 0) + attron(COLOR_PAIR(c)); + + if (attr.getHighlight()) + attron(A_BOLD); + if (attr.getReverse()) + attron(A_REVERSE); + + if (attr.getUnderline()) + { + if (termattrs() & A_UNDERLINE) + { + if (underline_hack) + { + size_t i=0; + + for (i=0; text[i]; i++) + if (text[i] == ' ') + text[i]=0xA0; + } + attron(A_UNDERLINE); + } + else + { + size_t i; + + for (i=0; text[i]; i++) + if (text[i] == ' ') + text[i]='_'; + } + } + + addwstr(text); + + if (c) + attroff(COLOR_PAIR(c)); + attroff(A_STANDOUT | A_BOLD | A_UNDERLINE | A_REVERSE); + + if (right_margin_crossed) + clrtoeol(); + + return true; +} + +void CursesScreen::flush() +{ + refresh(); +} + +void (*Curses::suspendedHook)(void)=&Curses::suspendedStub; + +void Curses::suspendedStub(void) +{ +} + +void Curses::setSuspendHook(void (*func)(void)) +{ + suspendedHook=func; +} + +Curses::Key CursesScreen::getKey() +{ + for (;;) + { + Key k=doGetKey(); + + if (k.plain()) + { + if (k.plain() && k.ukey == termStopKey) + { + if (suspendCommand.size() > 0) + { + std::vector<const char *> argv; + + const char *p=getenv("SHELL"); + + if (!p || !*p) + p="/bin/sh"; + + argv.push_back(p); + argv.push_back("-c"); + argv.push_back(suspendCommand.c_str()); + argv.push_back(NULL); + Curses::runCommand(argv, -1, ""); + draw(); + (*suspendedHook)(); + continue; + } + + endwin(); + kill( getpid(), SIGSTOP ); + draw(); + refresh(); + (*suspendedHook)(); + continue; + } + } + else if (k == Key::RESIZE) + { + save_w=COLS; + save_h=LINES; + + ::erase(); // old window + resized(); + touchwin(stdscr); + flush(); + draw(); + flush(); + } + return k; + } +} + +int Curses::runCommand(std::vector<const char *> &argv, + int stdinpipe, + std::string continuePrompt) +{ + endwin(); + if (continuePrompt.size() > 0) + { + std::cout << std::endl << std::endl; + std::cout.flush(); + } + + pid_t editor_pid, p2; + +#ifdef WIFSTOPPED + + RETSIGTYPE (*save_tstp)(int)=signal(SIGTSTP, SIG_IGN); + RETSIGTYPE (*save_cont)(int)=signal(SIGCONT, SIG_IGN); +#endif + + editor_pid=fork(); + int waitstat; + + signal(SIGCHLD, SIG_DFL); + + if (editor_pid < 0) + { +#ifdef WIFSTOPPED + signal(SIGTSTP, save_tstp); + signal(SIGCONT, save_cont); +#endif + beep(); + return -1; + } + + if (editor_pid == 0) + { + signal(SIGTSTP, SIG_DFL); + signal(SIGCONT, SIG_DFL); + if (continuePrompt.size() > 0) + { + dup2(1, 2); + } + signal(SIGINT, SIG_DFL); + if (stdinpipe >= 0) + { + dup2(stdinpipe, 0); + close(stdinpipe); + } + execvp(argv[0], (char **)&argv[0]); + kill(getpid(), SIGKILL); + _exit(1); + } + + if (stdinpipe >= 0) + close(stdinpipe); + + signal(SIGINT, SIG_IGN); + + for (;;) + { + +#ifdef WIFSTOPPED + + p2=waitpid(editor_pid, &waitstat, WUNTRACED); + + if (p2 != editor_pid) + continue; + + if (WIFSTOPPED(waitstat)) + { + kill(getpid(), SIGSTOP); + kill(editor_pid, SIGCONT); + continue; + } +#else + + p2=wait(&waitstat); + + if (p2 != editor_pid) + continue; +#endif + break; + } + + signal(SIGINT, SIG_DFL); + +#ifdef WIFSTOPPED + signal(SIGTSTP, save_tstp); + signal(SIGCONT, save_cont); +#endif + + if (continuePrompt.size() > 0) + { + char buffer[80]; + + std::cout << std::endl << continuePrompt; + std::cout.flush(); + std::cin.getline(buffer, sizeof(buffer)); + } + + refresh(); + + return WIFEXITED(waitstat) ? WEXITSTATUS(waitstat):-1; +} + +Curses::Key CursesScreen::doGetKey() +{ + // If, for some reason, a unicode key is available, take it + + { + unicode_char uk; + + if (keyreader >> uk) + return doGetPlainKey(uk); + } + + // Position cursor first. + + int row=getHeight(); + int col=getWidth(); + + if (row > 0) --row; + if (col > 0) --col; + + if (currentFocus != NULL) + { + int new_row=currentFocus->getHeight(); + if (new_row > 0) --new_row; + + int new_col=currentFocus->getWidth(); + if (new_col > 0) --new_col; + curs_set(currentFocus->getCursorPosition(new_row, new_col)); + + if (new_row < row) + row=new_row; + + if (new_col < col) + col=new_col; + } + else + curs_set(0); + + move(row, col); + + // As long as the bytes from the keyboard keep getting read, + // accumulate them. + + while (1) + { + int k=getch(); + + if (k == ERR) + { + flush(); + + if (++inputcounter >= 100) + { + // Detect closed xterm without SIGHUP + + if (write(1, "", 1) < 0) + bye(0); + inputcounter=0; + } + + if (LINES != save_h || COLS != save_w) + return (Key(Key::RESIZE)); + + return Key((unicode_char)0); + } + inputcounter=0; + +#ifdef KEY_SUSPEND + + if (k == KEY_SUSPEND) + k=termStopKey; +#endif + + static const struct { + int keyval; + const char *keycode; + } keys[]={ + +#define K(v,c) { v, Curses::Key::c } + + K(KEY_LEFT, LEFT), + K(KEY_RIGHT, RIGHT), + K(KEY_SLEFT, SHIFTLEFT), + K(KEY_SRIGHT, SHIFTRIGHT), + K(KEY_UP, UP), + K(KEY_DOWN, DOWN), + K(KEY_ENTER, ENTER), + K(KEY_PPAGE, PGUP), + K(KEY_NPAGE, PGDN), + K(KEY_HOME, HOME), + K(KEY_END, END), + K(KEY_SHOME, SHIFTHOME), + K(KEY_SEND, SHIFTEND), + K(KEY_RESIZE, RESIZE), + K(KEY_BACKSPACE, BACKSPACE), + K(KEY_DC, DEL), + K(KEY_EOL, CLREOL), +#undef K + }; + + if (shiftmode) + { + switch (k) { + case KEY_PPAGE: + return (Key(Key::SHIFTPGUP)); + case KEY_NPAGE: + return (Key(Key::SHIFTPGDN)); + case KEY_UP: + return (Key(Key::SHIFTUP)); + case KEY_DOWN: + return (Key(Key::SHIFTDOWN)); + case KEY_LEFT: + k=KEY_SLEFT; + break; + case KEY_RIGHT: + k=KEY_SRIGHT; + break; + case KEY_HOME: + k=KEY_SHOME; + break; + case KEY_END: + k=KEY_SEND; + break; + default: + shiftmode=0; + } + } + + size_t i; + + for (i=0; i<sizeof(keys)/sizeof(keys[0]); i++) + if (keys[i].keyval == k) + return Key(keys[i].keycode); + +#if HAS_FUNCTIONKEYS + if (k >= KEY_F(0) && k <= KEY_F(63)) + { + Key kk("FKEY"); + kk.ukey=k-KEY_F(0); + return kk; + } +#endif + + if ( (unsigned char)k == k) + { + // Plain key + + keyreader << k; + + unicode_char uk; + + if (keyreader >> uk) + return doGetPlainKey(uk); + } + } +} + +Curses::Key CursesScreen::doGetPlainKey(unicode_char k) +{ + if (k == 0) + { + shiftmode= !shiftmode; + + return Key(Key::SHIFT); + } + + if (k == (unicode_char)CursesField::clrEolKey) + return Key(Key::CLREOL); + + if (k == '\r') + return Key(Key::ENTER); + + shiftmode=false; + + return (Key(k)); +} + +void CursesScreen::beepError() +{ + beep(); +} diff --git a/curses/cursesscreen.H b/curses/cursesscreen.H new file mode 100644 index 0000000..e781020 --- /dev/null +++ b/curses/cursesscreen.H @@ -0,0 +1,94 @@ +/* +** Copyright 2002-2011, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#ifndef cursesscreen_H +#define cursesscreen_H + +#include "../curses/curses_config.h" +#include "../unicode/unicode.h" +#include "cursescontainer.H" + +#if HAVE_NCURSESW_CURSES_H +#include <ncursesw/curses.h> +#else +#include <curses.h> +#endif + +#include <iconv.h> +#include <vector> + +// +// A libcurses implementation. A CursesScreen object is typically the +// root object of the Curses hierarchy. The constructor initializes libcurses, +// the destructor cleans it up. +// + +class CursesScreen : public CursesContainer { + + int save_w, save_h; // Fix some libcurses.a resizing bugs. + + int inputcounter; + + bool underline_hack; + + // Read keyboard input, convert to unicode_chars + + class KeyReader { + + iconv_t h; + + std::vector<char> input_buf; + std::vector<char> winput_buf; + + size_t winput_cnt; + + public: + KeyReader(); + ~KeyReader(); + + void operator<<(char); + + bool operator>>(unicode_char &); + }; + + KeyReader keyreader; + + class repltabs_spaces; + +public: + CursesScreen(); + ~CursesScreen(); + + // Calculate max size. + int getWidth() const; + int getHeight() const; + + void flush(); + void draw(); + + class writetext_iter_helper; + + bool writeText(const char *text, int row, int col, + const Curses::CursesAttr &attr) const; + + bool writeText(const std::vector<unicode_char> &text, int row, int col, + const Curses::CursesAttr &attr) const; + + // Return keyboard input. Returns Key::nokey() if no keyboard input + // isavailable. + Key getKey(); + + void beepError(); +private: + + Key doGetKey(); + Key doGetPlainKey(unicode_char); + +public: + void resized(); +}; + +#endif diff --git a/curses/cursesstatusbar.C b/curses/cursesstatusbar.C new file mode 100644 index 0000000..2f2cb22 --- /dev/null +++ b/curses/cursesstatusbar.C @@ -0,0 +1,781 @@ +/* +** Copyright 2002-2011, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#include "curses_config.h" +#include "cursesstatusbar.H" +#include "cursesscreen.H" +#include "cursescontainer.H" + +#include <algorithm> + +std::string CursesStatusBar::extendedErrorPrompt; +std::string CursesStatusBar::shortcut_next_key; +std::string CursesStatusBar::shortcut_next_descr; +unicode_char CursesStatusBar::shortcut_next_keycode= 'O' & 31; + +CursesStatusBar::CursesStatusBar(CursesScreen *parent) : + CursesContainer(parent), CursesKeyHandler(PRI_STATUSHANDLER), + busyCounter(0), parentScreen(parent), fieldActive(NULL), + currentShortcutPage(0) +{ + clearStatusTimer=this; + clearStatusTimer= &CursesStatusBar::clearstatus; + resetRow(); +} + +// Reposition the status bar after a wrapped text message appeared or +// disappered. + +void CursesStatusBar::resetRow() +{ + int h=parentScreen->getHeight(); + int hh=getHeight(); + + if (h > hh) + { + h -= hh; + setRow(h); + } + else + setRow(2); +} + +CursesStatusBar::~CursesStatusBar() +{ + if (fieldActive) + { + delete fieldActive; + CursesKeyHandler::handlerListModified=true; + // Rebuild shortcuts + } +} + +int CursesStatusBar::getWidth() const +{ + return getParent()->getWidth(); +} + +// The height is usually 3 rows, but may be larger when a wrapped message +// is shown + +int CursesStatusBar::getHeight() const +{ + int r=3; + + if (extendedErrorMsg.size() > 0) + r += extendedErrorMsg.size() + 1; + + return r; +} + +void CursesStatusBar::resized() +{ + resetRow(); + CursesKeyHandler::handlerListModified=true; // Rebuild shortcuts + draw(); +} + +bool CursesStatusBar::progressWanted() +{ + if (extendedErrorMsg.size() > 0) + return false; + + time_t now=time(NULL); + + if (progressText.size() > 0 && progressTime == now) + return false; // Too soon + + progressTime=now; + return true; +} + +void CursesStatusBar::progress(std::string progress) +{ + progressText.clear(); + mail::iconvert::convert(progress, + unicode_default_chset(), + progressText); + + draw(); + flush(); +} + +void CursesStatusBar::setStatusBarAttr(CursesAttr a) +{ + attrStatusBar=a; + draw(); +} + +void CursesStatusBar::setHotKeyAttr(CursesAttr a) +{ + attrHotKey=a; + draw(); +} + +void CursesStatusBar::setHotKeyDescr(CursesAttr a) +{ + attrHotKeyDescr=a; + draw(); +} + + +void CursesStatusBar::draw() +{ + std::string spaces=""; + + // To save some time, call rebuildShortcuts() only when necessary + + if (CursesKeyHandler::handlerListModified) + rebuildShortcuts(); + + static const char throbber[]={'|', '/', '-', '\\'}; + + size_t w=getWidth(); + + std::vector<unicode_char> statusLine=statusText; + + statusLine.insert(statusLine.begin(), ' '); + + if (busyCounter) + { + if (busyCounter > (int)sizeof(throbber)) + busyCounter=1; + statusLine.insert(statusLine.begin(), + (unicode_char)throbber[busyCounter-1]); + } + else + statusLine.insert(statusLine.begin(), + ' '); + + if (extendedErrorMsg.size() == 0) + statusLine.insert(statusLine.end(), + progressText.begin(), + progressText.end()); + + + CursesAttr attr=attrStatusBar; + + attr.setReverse(); + + { + widecharbuf wc; + + wc.init_unicode(statusLine.begin(), statusLine.end()); + + wc.expandtabs(0); + + std::pair<std::vector<unicode_char>, size_t> + trunc(wc.get_unicode_truncated(w, 0)); + + if (trunc.second < w) + trunc.first.insert(trunc.first.end(), w-trunc.second, + ' '); + + writeText(trunc.first, getHeight()-3, 0, attr); + + } + + if (extendedErrorMsg.size() > 0) + { + statusLine.clear(); + statusLine.insert(statusLine.begin(), w, ' '); + writeText(statusLine, 0, 0, attr); + size_t n; + + for (n=0; n<extendedErrorMsg.size(); n++) + { + writeText(extendedErrorMsg[n], + n+1, 0, attrStatusBar); + } + } + + // Clear the shortcut area. + + statusLine.clear(); + statusLine.insert(statusLine.begin(), w, ' '); + + int r=1; + + if (extendedErrorMsg.size() > 0) + r += extendedErrorMsg.size()+1; + + writeText(statusLine, r, 0, CursesAttr()); + writeText(statusLine, r+1, 0, CursesAttr()); + + // Draw the current shortcut page. + + if (extendedErrorMsg.size() == 0 && + currentShortcutPage < shortcuts.size()) + { + + std::vector<std::vector< + std::pair< std::vector<unicode_char>, + std::vector<unicode_char> > > > + &shortcutPage= shortcuts[currentShortcutPage]; + + size_t i; + + for (i=0; i < shortcutPage.size(); i++) + { + std::vector< std::pair< std::vector<unicode_char>, + std::vector<unicode_char> > > + &column=shortcutPage[i]; + + size_t j; + + for (j=0; j<column.size(); j++) + { + std::pair< std::vector<unicode_char>, + std::vector<unicode_char> > + &row=column[j]; + + std::vector<unicode_char> cpy=row.first; + + widecharbuf wc; + + wc.init_unicode(cpy.begin(), cpy.end()); + wc.expandtabs(0); + + if (shortcuts.size() > 1 || + row.first.size() > 1 || + row.second.size() > 1) + { + size_t w=wc.wcwidth(0); + + if (w < max_sc_nlen) + cpy.insert(cpy.begin(), + max_sc_nlen - w, + ' '); + } + + CursesAttr h=attrHotKey; + + writeText( cpy, j+1, + i * (max_sc_nlen + max_sc_dlen + 2), + h.setReverse()); + + CursesAttr a; + + if (row.second.size() > 2 && + row.second[0] == '/' && + row.second[1] >= '0' && + row.second[1] <= '9') + { + a.setBgColor(row.second[1] - '0'); + row.second.erase(row.second.begin(), + row.second.begin()+2); + } + else + { + a=attrHotKeyDescr; + } + + writeText( row.second, j+1, + i * (max_sc_nlen + max_sc_dlen + 2) + + max_sc_nlen+1, a); + } + } + } + CursesContainer::draw(); +} + +class CursesStatusBarShortcutSort { +public: + bool operator()(std::pair<std::string, std::string> a, + std::pair<std::string, std::string> b) + { + return (strcoll( a.first.c_str(), b.first.c_str()) < 0); + } +}; + +// +// Create a sorted key shortcut list from all installed keyhandlers. +// + +static void createShortcuts(size_t &max_sc_nlen, size_t &max_sc_dlen, + std::vector< std::pair< std::vector<unicode_char>, + std::vector<unicode_char> > > &keys_w) +{ + std::vector< std::pair<std::string, std::string> > keys; + + std::list<CursesKeyHandler *>::const_iterator + kb=CursesKeyHandler::begin(), + ke=CursesKeyHandler::end(); + + while (kb != ke) + if ((*kb++)->listKeys(keys)) + break; + + sort(keys.begin(), keys.end(), + CursesStatusBarShortcutSort()); + + std::vector< std::pair<std::string, std::string> >::iterator + b=keys.begin(), e=keys.end(); + + while (b != e) + { + std::vector<unicode_char> first_w; + std::vector<unicode_char> second_w; + + mail::iconvert::convert(b->first, unicode_default_chset(), + first_w); + + mail::iconvert::convert(b->second, unicode_default_chset(), + second_w); + + b++; + + { + widecharbuf wc; + + wc.init_unicode(first_w.begin(), first_w.end()); + wc.expandtabs(0); + + size_t w=wc.wcwidth(0); + + if (w > max_sc_nlen) + max_sc_nlen=w; + } + + { + std::vector<unicode_char>::iterator b=second_w.begin(); + + if (second_w.size() && second_w[0] == '/' && + second_w[1] >= '0' && + second_w[1] <= '9') + b += 2; + + widecharbuf wc; + + wc.init_unicode(b, second_w.end()); + + wc.expandtabs(0); + + size_t w=wc.wcwidth(0); + + if (w > max_sc_dlen) + max_sc_dlen=w; + } + + keys_w.push_back(std::make_pair(first_w, second_w)); + } +} + +void CursesStatusBar::rebuildShortcuts() +{ + CursesKeyHandler::handlerListModified=false; + + currentShortcutPage=0; + + std::vector<unicode_char> nextpage_n; + std::vector<unicode_char> nextpage_d; + + if (shortcut_next_key.size() == 0) + shortcut_next_key="^O"; + + if (shortcut_next_descr.size() == 0) + shortcut_next_descr="...mOre"; + + mail::iconvert::convert(shortcut_next_key, unicode_default_chset(), + nextpage_n); + + mail::iconvert::convert(shortcut_next_descr, unicode_default_chset(), + nextpage_d); + + { + widecharbuf wc; + + wc.init_unicode(nextpage_n.begin(), nextpage_n.end()); + wc.expandtabs(0); + max_sc_nlen=wc.wcwidth(0); + } + + { + widecharbuf wc; + + wc.init_unicode(nextpage_d.begin(), nextpage_d.end()); + wc.expandtabs(0); + max_sc_dlen=wc.wcwidth(0); + } + + std::vector< std::pair< std::vector<unicode_char>, + std::vector<unicode_char> > > keys_w; + + createShortcuts(max_sc_nlen, max_sc_dlen, keys_w); + + // Does everything fit on one page? + + size_t w=getWidth(); + + size_t ncols=w / (max_sc_nlen + 2 + max_sc_dlen); + + if (ncols <= 0) + ncols=1; + + shortcuts.clear(); + bool multiplePages=false; + + size_t n=keys_w.size(); + size_t i=0; + + while (i < n) + { + if (n - i > ncols * 2) + multiplePages=true; + + std::vector< std::vector< std::pair< std::vector<unicode_char>, + std::vector<unicode_char> + > > > + columns; + + size_t j; + + for (j=0; j < ncols; j++ ) + { + std::vector< std::pair< std::vector<unicode_char>, + std::vector<unicode_char> > > + column; + + if (i < n) + { + column.push_back( keys_w[i++] ); + } + else + { + std::vector<unicode_char> zero; + + column.push_back(std::make_pair(zero, zero)); + } + + // Add ^O to last cell on each page. + + if (j + 1 == ncols && multiplePages) + { + std::pair< std::vector<unicode_char>, + std::vector<unicode_char> > + p=std::make_pair(nextpage_n, + nextpage_d); + column.push_back(p); + } + else if (i < n) + { + column.push_back( keys_w[i++] ); + } + else + { + std::vector<unicode_char> zero; + column.push_back (make_pair(zero, zero)); + } + + columns.push_back(column); + } + + shortcuts.push_back(columns); + } +} + +void CursesStatusBar::status(std::string text, statusLevel level) +{ + size_t origSize; + + if (((statusText.size() > 0 || extendedErrorMsg.size() > 0) && + currentLevel > level) || fieldActive != NULL) + { + // Message too low of a priority + + if (CursesKeyHandler::handlerListModified || busyCounter) + { + busyCounter=0; + draw(); // Update the list of shortcuts + } + return; + } + + origSize=extendedErrorMsg.size(); + + busyCounter=0; + + statusText.clear(); + progressText.clear(); + extendedErrorMsg.clear(); + + currentLevel=level; + + if (origSize > 0) + parentScreen->resized(); + + size_t w=getWidth(); + + if (w > 10) + w -= 4; + + { + std::vector<unicode_char> uc; + + mail::iconvert::convert(text, unicode_default_chset(), uc); + + extendedErrorMsg.clear(); + + std::back_insert_iterator + < std::vector< std::vector<unicode_char> > > + insert_iter(extendedErrorMsg); + + unicodewordwrap(uc.begin(), uc.end(), + unicoderewrapnone(), + insert_iter, w, true); + } + + switch (extendedErrorMsg.size()) { + case 0: + extendedErrorMsg.push_back(std::vector<unicode_char>()); + // FALLTHROUGH + case 1: + statusText=extendedErrorMsg.front(); + extendedErrorMsg.clear(); + break; + default: + extendedErrorMsg.push_back(std::vector<unicode_char>()); + + { + std::vector<unicode_char> statusLine; + + mail::iconvert::convert(extendedErrorPrompt, + unicode_default_chset(), + statusLine); + + extendedErrorMsg.push_back(statusLine); + } + + clearStatusTimer.cancelTimer(); + statusText.clear(); + parentScreen->resized(); + return; + } + + clearStatusTimer.cancelTimer(); + draw(); +} + +void CursesStatusBar::clearstatus() +{ + if (fieldActive != NULL) + return; + + statusText.clear(); + progressText.clear(); + busyCounter=0; + if (extendedErrorMsg.size() > 0) + { + extendedErrorMsg.clear(); + parentScreen->resized(); + return; + } + draw(); +} + +void CursesStatusBar::busy() +{ + ++busyCounter; + draw(); +} + +void CursesStatusBar::notbusy() +{ + busyCounter=0; + draw(); +} + +bool CursesStatusBar::processKey(const Curses::Key &key) +{ + if (key.plain() && key.ukey == shortcut_next_keycode) + { + if (++currentShortcutPage >= shortcuts.size()) + currentShortcutPage=0; + draw(); + return true; + + } + + if (extendedErrorMsg.size() == 0) + { + // Status line messages are erased ten seconds after + // the first key is received. + + if (statusText.size() > 0 && + clearStatusTimer.expired()) + clearStatusTimer.setTimer(10); + + return false; + } + + clearstatus(); // Clear wrapped popup + return true; +} + +bool CursesStatusBar::listKeys( std::vector< std::pair<std::string, std::string> > &list) +{ + if (!fieldActive) + return false; + + list.insert(list.end(), fieldActive->getOptionHelp().begin(), + fieldActive->getOptionHelp().end()); + return true; +} + +CursesField *CursesStatusBar::createPrompt(std::string prompt, std::string initvalue) +{ + if (fieldActive) // Old prompt? + { + Curses *f=fieldActive; + + fieldActive=NULL; + delete f; + } + + // The following requestFocus() may end up invoking focusLost() + // method of another Curses object. That, in turn, _may_ run some + // code that creates another dialog prompt. We'd rather that this + // happen at THIS point. Otherwise, the second dialog prompt will + // now blow away this dialog. + + dropFocus(); + + clearstatus(); + + size_t maxW=getWidth(); + + if (maxW > 20) + maxW -= 20; + else + maxW=0; + + size_t textW; + + { + std::vector<unicode_char> uc; + + mail::iconvert::convert(prompt, unicode_default_chset(), uc); + + widecharbuf wc; + + wc.init_unicode(uc.begin(), uc.end()); + + std::pair<std::vector<unicode_char>, size_t> + fragment=wc.get_unicode_truncated(maxW, 0); + + statusText=fragment.first; + textW=fragment.second; + } + + size_t fieldSize=getWidth()-textW; + + if (fieldSize > 2) + fieldSize -= 2; + else + fieldSize=1; + + fieldActive=new Field(this, fieldSize, 255, initvalue); + if (!fieldActive) + return NULL; + + fieldActive->setAttribute(attrStatusBar); + + CursesKeyHandler::handlerListModified=true; + // Rebuild shortcuts + + fieldActive->setCol(textW+2); + draw(); + if (fieldActive) + fieldActive->requestFocus(); + return fieldActive; +} + +// Callback from Field, when ENTER is pressed + +void CursesStatusBar::fieldEnter() +{ + fieldValue=fieldActive->getText(); + delete fieldActive; + fieldActive=NULL; + fieldAborted=false; + CursesKeyHandler::handlerListModified=true; + keepgoing=false; + clearstatus(); +} + +void CursesStatusBar::fieldAbort() +{ + fieldValue=""; + delete fieldActive; + fieldActive=NULL; + fieldAborted=true; + CursesKeyHandler::handlerListModified=true; + keepgoing=false; + clearstatus(); +} + +////////////////////////////////////////////////////////////////////// +// +// The custom field + +CursesStatusBar::Field::Field(CursesStatusBar *parent, size_t widthArg, + size_t maxlengthArg, + std::string initValue) + : CursesField(parent, widthArg, maxlengthArg, initValue), + me(parent) +{ +} + +CursesStatusBar::Field::~Field() +{ +} + +bool CursesStatusBar::Field::processKeyInFocus(const Key &key) +{ + if ((key.plain() && key.ukey == '\t') || + key == key.DOWN || key == key.UP || + key == key.SHIFTDOWN || key == key.SHIFTUP || + key == key.PGDN || key == key.PGUP || + key == key.SHIFTPGDN || key == key.SHIFTPGUP) + + return true; // No changing focus, please. + + if (key.plain() && key.ukey == '\x03') + { + setText(""); + me->fieldAbort(); + return true; + } + + if (key == key.ENTER) + { + me->fieldEnter(); + return true; + } + + return CursesField::processKeyInFocus(key); +} + +bool CursesStatusBar::Field::writeText(const char *text, int row, int col, + const CursesAttr &attr) const +{ + CursesAttr attr_cpy=attr; + attr_cpy.setReverse(!attr_cpy.getReverse()); + attr_cpy.setUnderline(false); + + return CursesField::writeText(text, row, col, attr_cpy); +} + +bool CursesStatusBar::Field::writeText(const std::vector<unicode_char> &text, + int row, int col, + const Curses::CursesAttr &attr) const +{ + CursesAttr attr_cpy=attr; + attr_cpy.setReverse(!attr_cpy.getReverse()); + attr_cpy.setUnderline(false); + + return CursesField::writeText(text, row, col, attr_cpy); +} diff --git a/curses/cursesstatusbar.H b/curses/cursesstatusbar.H new file mode 100644 index 0000000..50bc4c3 --- /dev/null +++ b/curses/cursesstatusbar.H @@ -0,0 +1,185 @@ +/* +** Copyright 2002-2011, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#ifndef cursesstatusbar_H +#define cursesstatusbar_H + +#include "mycurses.H" +#include "curseskeyhandler.H" +#include "cursescontainer.H" +#include "cursesfield.H" + +#include "timer.H" + +#include <string> +#include <vector> +#include <wchar.h> + +////////////////////////////////////////////////////////////////////////////// +// +// The status bar - moves itself to the bottom of the screen. Occupies three +// last lines: a one line status/help line, and two lines that list current +// keyboard shortcuts. +// +// Status messages are prioritized, and only messages of the same or higher +// priority level are shown. The clearstatus() clears the current message, +// which is also automatically cleared 10 seconds after the first keypress +// +// Messages that cannot fit on one line are automatically word-wrapped, and +// are shown in a larger popup that comes up from the bottom of the screen, +// which must be cleared with a keypress +// +// The status bar can also be converted to a one-line input field, by calling +// the createPrompt() method, which returns a pointer to the created +// CursesField object (which may be further customized). The status bar +// automatically destroys the CursesField abort when the input concludes +// (by pressing ENTER, or by manually calling fieldAbort() ). +// +// The first character of the status line may be a busy-throbber (a rotating +// line). The throbber must be animated by repeatedly calling the busy() +// method (which should be kicked off a 1 second timer). The throbber is +// cleared by clearstatus(). +// +// The status bar also includes a summary of all installed KeyHandler +// objects. If the list of available function keys exceeds the available +// space, the list is shown in pieces, with a control key cycling through +// to the next status key summary piece. + +class CursesScreen; + +class CursesStatusBar : public CursesContainer, public CursesKeyHandler { + + std::vector<unicode_char> statusText; + std::vector<unicode_char> progressText; + time_t progressTime; + + int busyCounter; + + CursesScreen *parentScreen; + + std::vector< std::vector<unicode_char> > extendedErrorMsg; + + // The status bar is sometimes turned into a one-line field entry. + // This is done by creating a CursesField child, which is why + // CursesStatusBar subclasses CursesContainer. + // We need to subclass CursesField in order to fix up a few things... + + class Field : public CursesField { + CursesStatusBar *me; + public: + Field(CursesStatusBar *parent, size_t widthArg, + size_t maxlengthArg, + std::string initValue); + ~Field(); + + bool processKeyInFocus(const Key &key); + + bool writeText(const char *text, int row, int col, + const CursesAttr &attr) const; + bool writeText(const std::vector<unicode_char> &text, + int row, int col, + const Curses::CursesAttr &attr) const; + }; + + Field *fieldActive; // Not NULL when input takes place + + std::string fieldValue; // Entered input + bool fieldAborted; // Whether input was aborted + + TimerRedirect<CursesStatusBar> clearStatusTimer; + + CursesAttr attrStatusBar, attrHotKey, attrHotKeyDescr; + +public: + void fieldEnter(); // Accept input + void fieldAbort(); // Abort input +private: + + // Shortcut keys. + + size_t max_sc_nlen; // Largest shortcut name + size_t max_sc_dlen; // Largest shortcut description + + std::vector <std::vector <std::vector < + std::pair< std::vector<unicode_char>, + std::vector<unicode_char> > > > > shortcuts; + + // A std::pair of name/description string, inside a two-element vector + // (the two-row column on the status bar), inside a vector of all + // columns that fit on one screen, inside a vector of all pages of + // shortcut descriptions + + size_t currentShortcutPage; // Current page of shortcuts shown + + void rebuildShortcuts(); // Rebuild the shortcuts array +public: + + static std::string shortcut_next_key; + static std::string shortcut_next_descr; + static unicode_char shortcut_next_keycode; + + friend class Field; + + // Status line priorities + + enum statusLevel { + NORMAL, // Normal message + INPROGRESS, // Something's in progress + EXPUNGED, // Folder's been expunged + SYSERROR, // System error + DISCONNECTED, // Server dropped connection + SERVERERROR, // Other server error + LOGINERROR // Login error + }; + + CursesStatusBar(CursesScreen *parent); + ~CursesStatusBar(); + + static std::string extendedErrorPrompt; // Non empty - wrapped text + + int getWidth() const; + int getHeight() const; + + void setStatusBarAttr(CursesAttr); + void setHotKeyAttr(CursesAttr); + void setHotKeyDescr(CursesAttr); + + void draw(); + void resized(); + + void status(std::string statusText, statusLevel level=NORMAL); + void progress(std::string progress); + bool progressWanted(); + + void clearstatus(); + + void busy(); // Throbber animation + + void notbusy(); // Not busy any more + + // Create a one-line input field. + + CursesField *createPrompt(std::string prompt, std::string initvalue=""); + + bool prompting() const { return fieldActive != NULL; } + bool promptAborted() const { return fieldAborted; } + + std::string getPromptValue() const + { + return (fieldActive != NULL ? fieldActive->getText() + : fieldValue); + } + +private: + statusLevel currentLevel; + + void resetRow(); + + bool processKey(const Curses::Key &key); + bool listKeys( std::vector< std::pair<std::string, std::string> > &list); +}; + +#endif diff --git a/curses/cursestitlebar.C b/curses/cursestitlebar.C new file mode 100644 index 0000000..c100dbc --- /dev/null +++ b/curses/cursestitlebar.C @@ -0,0 +1,110 @@ +/* +** Copyright 2002, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#include "curses_config.h" +#include "cursestitlebar.H" +#include "cursescontainer.H" + +using namespace std; + +CursesTitleBar::CursesTitleBar(CursesContainer *parent, string titleArg) + : Curses(parent), title(titleArg) +{ + setRow(0); + setCol(0); +} + +CursesTitleBar::~CursesTitleBar() +{ +} + +int CursesTitleBar::getHeight() const +{ + return 1; +} + +int CursesTitleBar::getWidth() const +{ + return getParent()->getWidth(); +} + +void CursesTitleBar::setTitles(string left, string right) +{ + leftTitle=left; + rightTitle=right; + draw(); +} + +void CursesTitleBar::setAttribute(CursesAttr attr) +{ + attribute=attr; + draw(); +} + +void CursesTitleBar::draw() +{ + size_t w=getWidth(); + + Curses *p=getParent(); + + if (p == NULL) + return; + + // First wipe out the status line + + string spaces=""; + + spaces.append(w, ' '); + + CursesAttr rev=attribute; + + rev.setReverse(); + + p->writeText(spaces, 0, 0, rev); + + // Now, write out the text strings + + p->writeText(leftTitle, 0, 1, rev); + + size_t l; + + { + widecharbuf wc; + + wc.init_string(rightTitle); + wc.expandtabs(0); + + l=wc.wcwidth(0); + + if (l + 1< w) + { + std::vector<unicode_char> uc; + + wc.tounicode(uc); + + p->writeText(uc, 0, w - 1 - l, rev); + } + } + + { + widecharbuf wc; + + wc.init_string(title); + wc.expandtabs(0); + + l=wc.wcwidth(0); + + if (l < w) + { + + std::vector<unicode_char> uc; + + wc.tounicode(uc); + + p->writeText(uc, 0, (w-l)/2, rev); + } + } +} diff --git a/curses/cursestitlebar.H b/curses/cursestitlebar.H new file mode 100644 index 0000000..d77b19a --- /dev/null +++ b/curses/cursestitlebar.H @@ -0,0 +1,40 @@ +/* +** Copyright 2002, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#ifndef cursestitlebar_H +#define cursestitlebar_H + +#include "mycurses.H" + +#include <string> + +// +// The title bar - moves itself to the first line on the parent screen, +// centers the title. +// + +class CursesTitleBar : public Curses { + + std::string title; + std::string leftTitle; + std::string rightTitle; + + CursesAttr attribute; + +public: + CursesTitleBar(CursesContainer *parent, std::string title); + ~CursesTitleBar(); + + void setTitles(std::string left, std::string right); + + void setAttribute(CursesAttr attr); + + int getWidth() const; + int getHeight() const; + void draw(); +}; + +#endif diff --git a/curses/cursesvscroll.C b/curses/cursesvscroll.C new file mode 100644 index 0000000..f24e2ba --- /dev/null +++ b/curses/cursesvscroll.C @@ -0,0 +1,148 @@ +/* +** Copyright 2002-2011, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#include "curses_config.h" +#include "cursesvscroll.H" + +CursesVScroll::CursesVScroll(CursesContainer *parent) + : CursesContainer(parent), firstRowShown(0) +{ +} + +CursesVScroll::~CursesVScroll() +{ +} + +int CursesVScroll::getCursorPosition(int &row, int &col) +{ + scrollTo(row); + + row -= firstRowShown; + + return (CursesContainer::getCursorPosition(row, col)); +} + + +int CursesVScroll::getWidth() const +{ + return getScreenWidth(); +} + +void CursesVScroll::getVerticalViewport(size_t &first_row, + size_t &nrows) +{ + first_row=firstRowShown; + nrows=getHeight(); +} + +void CursesVScroll::erase() +{ + size_t w=getWidth(), h=getHeight(); + + std::vector<unicode_char> spaces; + + spaces.insert(spaces.end(), w, ' '); + + size_t i; + + for (i=0; i<h; i++) + CursesContainer::writeText(spaces, i, 0, CursesAttr()); +} + +void CursesVScroll::deleteChild(Curses *child) +{ + CursesContainer::deleteChild(child); + scrollTo(0); +} + +void CursesVScroll::scrollTo(size_t row) +{ + size_t h=getHeight(); + +#if 0 + if (row < 0 || (size_t)row < h) + { + if (firstRowShown > 0) + { + firstRowShown=0; + redraw(); + } + } + else +#endif + if ((size_t)row < firstRowShown) + { + if (row + 1 < firstRowShown) + firstRowShown=row; + else + { + if (h > 6) + { + firstRowShown=row+5; + + if (firstRowShown >= h) + firstRowShown -= h; + else + firstRowShown=0; + } + else + firstRowShown=row; + } + redraw(); + } + else if ((size_t)row >= firstRowShown + h) + { + if (firstRowShown + h < (size_t)row) + { + firstRowShown=row + 1 - h; + } + else + { + if (h > 6) + firstRowShown=row-5; + else + firstRowShown=row + 1 - h; + } + redraw(); + } +} + +bool CursesVScroll::writeText(const char *text, int row, int col, + const CursesAttr &attr) const +{ + if (row < 0 || + (size_t)row < firstRowShown || + (size_t)row >= firstRowShown + getHeight()) + return false; // This row is not shown + + row -= firstRowShown; + + return CursesContainer::writeText(text, row, col, attr); +} + +bool CursesVScroll::writeText(const std::vector<unicode_char> &text, + int row, int col, + const Curses::CursesAttr &attr) const +{ + if (row < 0 || + (size_t)row < firstRowShown || + (size_t)row >= firstRowShown + getHeight()) + return false; // Ditto. + + row -= firstRowShown; + + return CursesContainer::writeText(text, row, col, attr); +} + +// +// We just logically scrolled. Clear the window, redraw. +// + +void CursesVScroll::redraw() +{ + erase(); + draw(); +} diff --git a/curses/cursesvscroll.H b/curses/cursesvscroll.H new file mode 100644 index 0000000..d27342f --- /dev/null +++ b/curses/cursesvscroll.H @@ -0,0 +1,71 @@ +/* +** Copyright 2002, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#ifndef cursesvscroll_H +#define cursesvscroll_H + +#include "mycurses.H" +#include "cursescontainer.H" + +// +// A vertically-scrolling viewport. The subclass must define getWidth() +// and getHeight(). +// + +class CursesVScroll : public CursesContainer { + + size_t firstRowShown; + +public: + CursesVScroll(CursesContainer *parent); + ~CursesVScroll(); + + // When something is deleted from the viewport, jump the view port + // to the top + + void deleteChild(Curses *child); + + // Automatically scroll the viewport to following keyboard input. + // This is done by overriding getCursorPosition(), calling + // scrollTo(), then simply subtracting the topmost row shown. + + int getCursorPosition(int &row, int &col); + + // + // Adjust for vertical scroll position: + // + + bool writeText(const char *text, int row, int col, + const CursesAttr &attr) const; + bool writeText(const std::vector<unicode_char> &text, + int row, int col, + const Curses::CursesAttr &attr) const; + + // + // Make sure the following row is visible right now. + // + void scrollTo(size_t rowNum); + + // Intercept getVerticalViewport(), and return reality. + + void getVerticalViewport(size_t &first_row, + size_t &nrows); + + void erase(); + + int getWidth() const; + size_t getFirstRowShown() const { return firstRowShown; } + void setFirstRowShown(size_t rowNum) + { + firstRowShown=rowNum; + redraw(); + } + +private: + void redraw(); +}; + +#endif diff --git a/curses/mycurses.H b/curses/mycurses.H new file mode 100644 index 0000000..9a305a4 --- /dev/null +++ b/curses/mycurses.H @@ -0,0 +1,439 @@ +/* +** Copyright 2002-2011, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#ifndef curses_H +#define curses_H + +#include "../curses/curses_config.h" +#include "../unicode/unicode.h" +#include <string.h> + +#include <wchar.h> +#include <string> +#include <vector> + +#include "cursesobject.H" +#include "widechar.H" + +class Curses; +class CursesContainer; + +/////////////////////////////////////////////////////////////////////////// +// +// A simple OO interface to libcurses. +// +// The Curses object is the common abstract superclass. All other objects +// subclass from Curses. +// +// Curses objects are arranged in a tree hierarchy. The root of the tree +// is the CursesScreen objects, which implements the WriteText() methods +// that use libcurses. All Curses objects are anchored at some row/column +// pair, relative to its hierarchy parent. The Curses object provides a +// default WriteText() implementation that adds the given text position to +// its row/column position, and then calls its parent's WriteText() method. +// +// Two WriteText methods are provided: one for text in the current +// (possibly multibyte) character set; and one for wide-character text. +// +// The remaining methods generally follow a similar path - a default +// Curses implementation that performs local processing, then calls its +// parent's method. + +class Curses : public CursesObj { + + // My parent, possibly NULL; + + CursesContainer *parent; + + int row, col; // My location. + + // Comparison function compares Curses objects based on their + // location. This is used to determine input focus tabbing. + + static bool childPositionCompareFunc(Curses *, Curses *); + +public: + + // Curses object that receives keyboard input at this time. + // May be NULL + + static Curses *currentFocus; + + // Keepgoing is initialized to true. Some objects, (FileReq, + // StatusBar) reset it to false to indicate that they're done. + // keepgoing must be manually reset to true. + + static bool keepgoing; + + // Emulate SHIFT keys for keyboards that can't directly generate + // then. shiftmode is set to true if the previous keypress was the + // SHIFT keypress. + + static bool shiftmode; + + static std::string suspendCommand; // Run this instead of SIGSTOP + + static int runCommand(std::vector<const char *> &argv, + int stdinpipe, + std::string continuePrompt); + // Disable curses, run the command, reenable curses + + // CursesAttr encapsulates generic text attributes, for WriteText + + class CursesAttr { + + int bgcolor; + int fgcolor; + bool highlight; + bool reverse; + bool underline; + public: + CursesAttr() : bgcolor(0), + fgcolor(0), + highlight(0), reverse(0), underline(0) + { + } + ~CursesAttr() + { + } + + CursesAttr &setBgColor(int c) + { + bgcolor=c; + return *this; + } + + CursesAttr &setFgColor(int c) + { + fgcolor=c; + return *this; + } + + CursesAttr &setHighlight(bool h=true) + { + highlight=h; + return *this; + } + + CursesAttr &setReverse(bool r=1) + { + reverse=r; + return *this; + } + + CursesAttr &setUnderline(bool u=1) + { + underline=u; + return *this; + } + + int getBgColor() const { return bgcolor; } + int getFgColor() const { return fgcolor; } + bool getHighlight() const { return highlight; } + bool getReverse() const { return reverse; } + bool getUnderline() const { return underline; } + }; + + // Generic encapsulation of keyboard input. Keyboard input is either + // a key, wchar_t, or a special function key, like a cursor key. + // Rather than pull in all the baggage of curses.h, just to get + // the key definition, we define our own constants. Which also + // makes it possible to define magic keys that are not defined by + // curses.h + + class Key { + public: + unicode_char ukey; + const char *keycode; + + Key(unicode_char ch) : ukey(ch), keycode(0) {} + + Key(const char *k) : ukey(0), keycode(k) {} + + bool plain() const { return keycode == 0; } + + bool nokey() const { return ukey == 0 && keycode == 0; } + + bool fkey() const { return keycode && + strcmp(keycode, "FKEY") == 0; } + // This is a function key + + int fkeynum() const { return (int)ukey; } + + static const char LEFT[], + RIGHT[], + SHIFTLEFT[], + SHIFTRIGHT[], + UP[], + DOWN[], + SHIFTUP[], + SHIFTDOWN[], + DEL[], + CLREOL[], + BACKSPACE[], + ENTER[], + PGUP[], + PGDN[], + SHIFTPGUP[], + SHIFTPGDN[], + HOME[], + END[], + SHIFTHOME[], + SHIFTEND[], + SHIFT[], + RESIZE[]; + + bool operator==(const char *p) const + { + return keycode != 0 && strcmp(keycode, p) == 0; + } + +#if 0 + bool operator==(unicode_char k) const + { + return keycode == 0 && key == k; + } +#endif + + bool operator==(const Key &k) const + { + return strcmp(keycode ? keycode:"", + k.keycode ? k.keycode:"") == 0 && + ukey == k.ukey; + } + + bool operator==(const std::vector<unicode_char> &v) const; + + bool operator!=(const char *p) const + { + return !operator==(p); + } + +#if 0 + bool operator!=(wchar_t k) const + { + return !operator==(k); + } +#endif + bool operator!=(const std::vector<unicode_char> &v) const + { + return !operator==(v); + } + }; + + // This humble function received and handles keyboard input. + // processKey() runs either an installed key handler, or runs + // currentFocus->processKeyInFocus(). When a file requester, or + // StatusBar input prompt is active, at exit keepgoing may be set + // to false (ENTER key closes the file requester or status bar input + // field). + + static bool processKey(const Key &k); + + // How to interpret Curses::col + + enum Alignment { + LEFT, // col is the left corner + CENTER, // col is the center of this Curses object + RIGHT, // col is the right corner + PARENTCENTER}; // Ignore col, center this object in its parent + + Curses(CursesContainer *parent=0); + virtual ~Curses(); + + // Subclasses must define getWidth() and getHeight() to provide their + // sizes. + + virtual int getWidth() const=0; + virtual int getHeight() const=0; + + // Children of CursesVScroll may not have their entire contents + // shown. To optimize their draw() implementation, instead of + // writing out their contents in entire, getVerticalViewport may + // be called to obtain the first line of this object that's actually + // viewable, and the total number of rows that are viewable. + // Calls to write text to lines outside this range will be no-oped. + // nrows may be 0, if this object is entirely off-screen. + // + // The default Curses implementation sets first_row to 0, and + // nrows to getHeight(), then calls the getVerticalViewPort() method + // of its parent. The results of parent's GetVerticalViewPort(), + // combined with this object's getRow() method, is used to potentially + // narrow down the first_row/nrows range. For example, if the parent's + // vertical viewport is rows 5-9, and this object's row is 4, and + // its height is 10, then the resulting first_row/nrows will be 1/5 + // (instead of 0/10). + + virtual void getVerticalViewport(size_t &first_row, + size_t &nrows); + + // The default implementation of getScreenWidth()/getScreenHeight() + // walk up the parent chain, and invoke the hierarchy root's Curses + // object's getWidth/getHeight methods, which should reflect the + // should be the actual screen size. + + int getScreenWidth() const; + int getScreenHeight() const; + + // get/set row/col methods have an obvious default implementation, + // that some subclasses may override (the usual reason is to + // automatically redraw the curses object if it moves) + + virtual int getRow() const; + virtual int getCol() const; + virtual void setRow(int row); + virtual void setCol(int col); + + // The default implementation of scrollTo adds the supplied row number + // to the starting row of this curses object, and recursively + // calls the parent's scrollTo() method. CursesVScroll overrides + // this method to make sure that the indicated row is currently + // visible. + + virtual void scrollTo(size_t row); + + CursesContainer *getParent() { return (parent); } + const CursesContainer *getParent() const { return (parent); } + void setParent(CursesContainer *p) { parent=p; } + + // If CursesContainer sees an isDialog() child, it will get drawn + // instead of all the other children. Uses by CursesFileReq, for + // example, to take over the display. + + virtual bool isDialog() const; + + // Find my dialog child + + virtual Curses *getDialogChild() const; + + // draw() must be subclassed to invoked WriteText() to actually + // print the Curses object's contents. + + virtual void draw()=0; + + // The default implementation of erase() calls getWidth()/getHeight() + // then writes out a bunch of whitespace to clear everything out. + + virtual void erase(); + + // The default implementation of flush() calls the parent's flush() + // method. The top level CursesScreen object calls libcurses.a + // instead. + + virtual void flush(); + + // The screen has been resized. + // The default implementation of resized() just calls draw() + // + + virtual void resized(); + + static int getColorCount(); + // # of colors supported by display (0 if no colors) + + // See the beginning of this file for a description of writeText(). + + virtual bool writeText(const char *text, int row, int col, + const CursesAttr &attr) const; + virtual bool writeText(const std::vector<unicode_char> &text, + int row, int col, + const Curses::CursesAttr &attr) const; + + void writeText(std::string text, int row, int col, + const CursesAttr &attr) const; + + // Beep the terminal. + + virtual void beepError(); + +private: + Alignment alignment; + +public: + // Return the coordinates of the top/left corner of this object, + // in the context of this parent, after taking into account all + // the alignment information. + + int getRowAligned() const; + int getColAligned() const; + + virtual void setAlignment(Alignment alignmentArg); + virtual Alignment getAlignment(); + + // Indicate whether this object can handle keyboard input + // (default: no). + + virtual bool isFocusable(); + + // If this object is handling keyboard input, return the next or the + // previous object, in the natural tabbing order, that should receive + // keyboard input. + + virtual Curses *getNextFocus(); + virtual Curses *getPrevFocus(); + + // Actually move the keyboard focus to the next or prev curses object. + + virtual void transferNextFocus(); + virtual void transferPrevFocus(); + + // Explicitly request keyboard input + + virtual void requestFocus(); + + // Callback function that is invoked whenever this object begins + // handling keyboard input. The default implementation just calls + // draw() + + virtual void focusGained(); + + // Callback function that is invoked whenever this object stops + // handling keyboard input. The default implementation just calls + // draw() + + virtual void focusLost(); + + // Remove keyboard input from any object that's currently receiving + // keyboard input (explicitly calling focusLost()). This can be + // useful to force focusLost() processing (to make sure that any + // further popup notice activity won't cause any side effects). + // Eventually, somebody's requestFocus() method should be called + // to reenable input processing + + static void dropFocus(); + + // Return whether this object is handling keyboard input + + virtual bool hasFocus(); + + // Handle keyboard input. The default implementation changes + // keyboard input focus in response to basic cursor movement + // keys. Subclasses typically override, but also call the superclass's + // method for the default action. + // processKeyInFocus returns true if the key was processed, or if the + // function did not recognize and process the key. + + virtual bool processKeyInFocus(const Key &key); + + // Map the coordinates of a character sell in this Curses object to + // the screen coordinates. The default implementation just adds + // row/col to the top/left starting coords of this object, then runs + // the parent's getCursorPosition() method. + + virtual int getCursorPosition(int &row, int &col); + + + // Set a callback function invoked after a suspend + + static void setSuspendHook( void (*)(void) ); + + static void (*suspendedHook)(void); +private: + static void suspendedStub(void); + +}; + +#endif diff --git a/curses/timer.C b/curses/timer.C new file mode 100644 index 0000000..8097f37 --- /dev/null +++ b/curses/timer.C @@ -0,0 +1,172 @@ +/* +** Copyright 2002, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#include "curses_config.h" +#include "timer.H" + +std::set<Timer *> Timer::timer_list; + +Timer::Timer() +{ + timeout.tv_sec=0; + timeout.tv_usec=0; + + timer_list.insert(this); +} + +Timer::~Timer() +{ + timer_list.erase(this); +} + +void Timer::setTimer(int nSeconds) +{ + gettimeofday(&timeout, NULL); + timeout.tv_sec += nSeconds; +} + +void Timer::setTimer(struct timeval tv) +{ + gettimeofday(&timeout, NULL); + timeout.tv_sec += tv.tv_sec; + + timeout.tv_usec += tv.tv_usec; + + if (timeout.tv_usec >= 1000000) + { + ++timeout.tv_sec; + timeout.tv_usec %= 1000000; + } +} + +struct timeval Timer::getTimer() const +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + + return getTimer(tv); +} + +struct timeval Timer::getTimer(const struct timeval &tv) const +{ + struct timeval t; + + t.tv_sec=0; + t.tv_usec=0; + + if (timeout.tv_sec == 0 && timeout.tv_usec == 0) + return t; + + if (tv.tv_sec > timeout.tv_sec) + return t; + + if (tv.tv_sec == timeout.tv_sec && + tv.tv_usec >= timeout.tv_usec) + return t; + + t=timeout; + + t.tv_sec -= tv.tv_sec; + t.tv_usec -= tv.tv_usec; + + if (t.tv_usec < 0) + { + t.tv_usec += 1000000; + t.tv_sec--; + } + + return t; +} + +#define DEBUG 1 +#undef DEBUG + +struct timeval Timer::getNextTimeout(bool &alarmCalledFlag) +{ + struct timeval now; + + gettimeofday(&now, NULL); + + bool alarmed; + + bool wasAlarmed=false; + + struct timeval s; + + alarmCalledFlag=false; + + do + { +#if DEBUG + cerr << "In getNextTimeout:" << endl; +#endif + + alarmed=false; + + std::set<Timer *>::iterator b=timer_list.begin(), + e=timer_list.end(); + + s.tv_sec=0; + s.tv_usec=0; + + while (b != e) + { + Timer *t= *b++; + + if (t->timeout.tv_sec == 0 && + t->timeout.tv_usec == 0) + continue; + + struct timeval v=t->getTimer(now); + +#if DEBUG + cerr << "Timer " << t << ": " << v.tv_sec + << "." << v.tv_usec << endl; +#endif + + if (v.tv_sec == 0 && v.tv_usec == 0) + { +#if DEBUG + cerr << "ALARM: " << + (wasAlarmed ? "ignored":"handled") + << endl; +#endif + if (wasAlarmed) + { + // We can get here if an alarm + // went off, and the alarm handler + // reset the alarm to 0 seconds again. + // We'll get it on the next go-round + + s=v; + break; // Kick the alarm next time + } + t->timeout=v; + t->alarm(); + alarmCalledFlag=true; + alarmed=true; + } + else if ((s.tv_sec == 0 && s.tv_usec == 0) || + v.tv_sec < s.tv_sec || + (v.tv_sec == s.tv_sec + && v.tv_usec < s.tv_usec)) + s=v; + } + + wasAlarmed=alarmed; + + } while (alarmed); + +#if DEBUG + cerr << "getNextTimeout: " << s.tv_sec << "." << s.tv_usec << endl; +#endif + return s; +} + +void Timer::alarm() +{ +} diff --git a/curses/timer.H b/curses/timer.H new file mode 100644 index 0000000..b4b533d --- /dev/null +++ b/curses/timer.H @@ -0,0 +1,124 @@ +/* +** Copyright 2002, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#ifndef timer_H +#define timer_H + +#include "../curses/curses_config.h" +#if TIME_WITH_SYS_TIME +# include <sys/time.h> +# include <time.h> +#else +# if HAVE_SYS_TIME_H +# include <sys/time.h> +# else +# include <time.h> +# endif +#endif + +#include <set> + +//////////////////////////////////////////////////////////////////////////// +// +// The Timer object is used primarily by CursesStatusBar to automatically +// clear messages after a short delay, but it can be used as a general +// purpose timer. +// +// Timer processing depends on the application repeatedly calling the +// getNextTimeout() method, which checks for any timers that need to go +// off. getNextTimeout() returns a timeval when the getNextTimeout() should +// be called again, to check for any more timers. +// +// The Timer object may be instantiated as is, or subclassed. The expired() +// method may be invoked, after getNextTimeout(), to check if the timer has +// gone off. Alternativelly, the subclass can implement the alarm() method. + +class Timer { + struct timeval timeout; + + static std::set<Timer *> timer_list; + +public: + Timer(); + virtual ~Timer(); + + // Set a timeout for the given number of seconds. + + void setTimer(int); + + // Set a timeout for the given number of seconds/milliseconds. + + void setTimer(struct timeval tv); + + // Cancel this timer. + + void cancelTimer() { timeout.tv_sec=0; timeout.tv_usec=0; } + bool expired() { return timeout.tv_sec == 0 && timeout.tv_usec == 0; } + + // Compute how long before this timer goes off. + + struct timeval getTimer() const; + + // Compute how long before this timer goes off, if the current time is + // 'now'. + + struct timeval getTimer(const struct timeval &now) const; + + // The timer has gone off. + + virtual void alarm(); + + // Trigger any pending timers. alarmCalledFlag gets set to true if + // any alarms were kicked off + + static struct timeval getNextTimeout(bool &alarmCalledFlag); +}; + +////////////////////////////////////////////////////////////////////////// +// +// A helpful template that embeds Timer as a member of another class. +// Rather than subclassing from Timer, multiple Timer objects may be +// members of a class. Typical usage: +// +// class X { +// ... +// +// TimerRedirect<X> timera, timerb; +// +// void timeraFunc(); +// void timerbFunc(); +// }; +// +// X::X() +// { +// timera=this; +// timerb=this; +// +// timera= &X::timerAFunc; +// timerb= &X::timerBFunc; +// } +// +// ... +// +// timera.setTimer(10); +// + +template<class T> class TimerRedirect : public Timer { + + T *myClass; + void (T::*myMethod)(); + +public: + TimerRedirect() : myClass(0), myMethod(0) {} + ~TimerRedirect() {} + + void operator=(T *classPtr) { myClass=classPtr; } + void operator=( void (T::*methodPtr)() ) { myMethod=methodPtr; } + + void alarm() { if (myClass && myMethod) (myClass->*myMethod)(); } +}; + +#endif diff --git a/curses/widechar.C b/curses/widechar.C new file mode 100644 index 0000000..95e03a5 --- /dev/null +++ b/curses/widechar.C @@ -0,0 +1,498 @@ +/* +** Copyright 2011, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#include "curses_config.h" +#include "widechar.H" +#include "cursesflowedline.H" +#include <algorithm> + +bool unicoderewrapnone::operator()(size_t n) const +{ + return false; +} + +void widecharbuf::tounicode(std::vector<unicode_char> &text) const +{ + text=ustring; +} + +void widecharbuf::init_string(const std::string &str) +{ + std::vector<unicode_char> uc; + + mail::iconvert::convert(str, unicode_default_chset(), uc); + + init_unicode(uc.begin(), uc.end()); +} + +std::pair<std::string, size_t> +widecharbuf::get_string_truncated(size_t maxwidth, ssize_t col) const +{ + std::pair<std::vector<unicode_char>, size_t> + tempret(get_unicode_truncated(maxwidth, col)); + + std::pair<std::string, size_t> ret; + + ret.second=tempret.second; + + mail::iconvert::fromu::convert(tempret.first, unicode_default_chset(), + ret.first); + return ret; +} + +std::string widecharbuf::get_substring(size_t first_grapheme, + size_t grapheme_cnt) const +{ + return mail::iconvert::fromu + ::convert(get_unicode_substring(first_grapheme, + grapheme_cnt), + unicode_default_chset()); +} + +std::vector<unicode_char> +widecharbuf::get_unicode_substring(size_t first_grapheme, + size_t grapheme_cnt) const +{ + if (first_grapheme >= graphemes.size()) + return std::vector<unicode_char>(); + + size_t max_graphemes=graphemes.size() - first_grapheme; + + if (grapheme_cnt > max_graphemes) + grapheme_cnt=max_graphemes; + + return std::vector<unicode_char>(ustring.begin() + +first_grapheme, + ustring.begin() + +first_grapheme+grapheme_cnt); +} + +std::pair<std::vector<unicode_char>, size_t> +widecharbuf::get_unicode_truncated(size_t maxwidth, ssize_t col) const +{ + size_t i; + size_t cnt=0; + size_t width=0; + + maxwidth += col; + + for (i=0; i<graphemes.size(); ++i) + { + size_t w=graphemes[i].wcwidth(col); + + if (col + w > maxwidth) + break; + + col += w; + width += w; + cnt += graphemes[i].cnt; + } + + std::string s; + + mail::iconvert::fromu::convert(ustring.begin(), + ustring.begin()+cnt, + unicode_default_chset(), + s); + return std::make_pair(std::vector<unicode_char> + (ustring.begin(), ustring.begin()+cnt), width); +} + +std::vector<unicode_char> widecharbuf::get_unicode_fixedwidth(size_t width, + ssize_t atcol) + const +{ + std::pair<std::vector<unicode_char>, size_t> + truncated=get_unicode_truncated(width, atcol); + + if (truncated.second < width) + truncated.first.insert(truncated.first.end(), + width-truncated.second, ' '); + + return truncated.first; +} + +size_t widecharbuf::wcwidth(ssize_t start_col) const +{ + size_t n=0; + + for (std::vector<grapheme_t>::const_iterator + b(graphemes.begin()), e(graphemes.end()); b != e; ++b) + { + size_t grapheme_width=b->wcwidth(start_col); + + n += grapheme_width; + start_col += grapheme_width; + } + return n; +} + +void widecharbuf::resetgraphemes() +{ + graphemes.clear(); + + if (ustring.empty()) + return; + + const unicode_char *b=&*ustring.begin(), *e(b+ustring.size()); + + do + { + const unicode_char *start_pos=b; + + unicode_char first, second=*b; + + do + { + first=second; + ++b; + + } while (b != e && (second=*b) != '\t' && first != '\t' && + !unicode_grapheme_break(first, second)); + + graphemes.push_back(grapheme_t(start_pos, b-start_pos)); + } while (b != e); +} + +widecharbuf &widecharbuf::operator+=(const widecharbuf &o) +{ + ustring.insert(ustring.end(), o.ustring.begin(), o.ustring.end()); + resetgraphemes(); + resetgraphemes(); + return *this; +} + +widecharbuf &widecharbuf::operator=(const widecharbuf &o) +{ + ustring=o.ustring; + resetgraphemes(); + return *this; +} + +size_t widecharbuf::charwidth(wchar_t ch, ssize_t atcol) +{ + // We visually multiply dashes, corresponds to code + // in CursesScreen::writeText() + + if (ch == 0x2013) + return 2; // EN DASH + + if (ch == 0x2014) + return 3; // EM DASH + + if (ch != '\t') + { + int s=::wcwidth(ch); + + if (s == -1) + s=1; // Unprintable character, punt + return s; + } + + size_t cnt=0; + + do + { + ++atcol; + ++cnt; + } while (atcol & 7); + return cnt; +} + + +size_t widecharbuf::grapheme_t::wcwidth(ssize_t col) const +{ + std::string s; + + mail::iconvert::fromu::convert(uptr, uptr+cnt, + unicode_default_chset(), s); + + return towidechar(s.begin(), s.end(), towidechar_wcwidth_iter(col)); +} + +ssize_t widecharbuf::expandtabs(ssize_t col) +{ + std::vector<unicode_char> replbuf; + + replbuf.reserve(ustring.size()*2); + + for (std::vector<grapheme_t>::iterator + b(graphemes.begin()), + e(graphemes.end()); b != e; ++b) + { + size_t w=b->wcwidth(col); + + if (*b->uptr == '\t') + { + replbuf.insert(replbuf.end(), w, ' '); + } + else + { + replbuf.insert(replbuf.end(), b->uptr, + b->uptr + b->cnt); + } + + col += w; + } + ustring=replbuf; + resetgraphemes(); + return col; +} + +void editablewidechar::set_contents(const std::vector<unicode_char> + &before, + const std::vector<unicode_char> + &after) +{ + before_insert.init_unicode(before.begin(), before.end()); + after_insert.init_unicode(after.begin(), after.end()); + inserted.clear(); +} + +void editablewidechar::get_contents(std::vector<unicode_char> &before, + std::vector<unicode_char> &after) + const +{ + before.clear(); + + before_insert.tounicode(before); + before.insert(before.end(), inserted.begin(), inserted.end()); + after_insert.tounicode(after); +} + +void editablewidechar::contents_cut(size_t cut_pos, + std::vector<unicode_char> &cut_text) +{ + if (cut_pos == before_insert.graphemes.size() && + inserted.empty()) + return; + + std::vector<unicode_char> before, after; + + if (cut_pos < before_insert.graphemes.size()) + { + before=before_insert.get_unicode_substring(0, cut_pos); + cut_text=before_insert + .get_unicode_substring(cut_pos, + before_insert.graphemes.size() + -cut_pos); + } + else + before_insert.tounicode(before); + + if (cut_pos > before_insert.graphemes.size()) + { + cut_pos -= before_insert.graphemes.size(); + + cut_text=after_insert.get_unicode_substring(0, cut_pos); + after=after_insert + .get_unicode_substring(cut_pos, + after_insert.graphemes.size() + -cut_pos); + } + else + after_insert.tounicode(after); + + set_contents(before, after); +} + +void editablewidechar::insert_to_before() +{ + std::vector<unicode_char> before, after; + + get_contents(before, after); + set_contents(before, after); +} + +void editablewidechar::to_before() +{ + insert_to_before(); + before_insert += after_insert; + after_insert.clear(); +} + +void editablewidechar::to_after() +{ + insert_to_before(); + before_insert += after_insert; + after_insert=before_insert; + before_insert.clear(); +} + +void editablewidechar::to_position(size_t o) +{ + insert_to_before(); + + std::vector<unicode_char> cut_text, before, after; + + size_t col=0; + + for (std::vector<widecharbuf::grapheme_t>::iterator + b(before_insert.graphemes.begin()), + e(before_insert.graphemes.end()); b != e; ++b) + { + size_t grapheme_width=b->wcwidth(col); + + if (grapheme_width > o) + { + contents_cut(b-before_insert.graphemes.begin(), + cut_text); + get_contents(before, after); + after.insert(after.begin(), cut_text.begin(), + cut_text.end()); + set_contents(before, after); + return; + } + o -= grapheme_width; + col += grapheme_width; + } + + std::vector<widecharbuf::grapheme_t>::iterator b, e; + + for (b=after_insert.graphemes.begin(), + e=after_insert.graphemes.end(); b != e; ++b) + { + size_t grapheme_width=b->wcwidth(col); + + if (grapheme_width > o) + break; + o -= grapheme_width; + col += grapheme_width; + } + + contents_cut(b-after_insert.graphemes.begin(), cut_text); + get_contents(before, after); + before.insert(before.end(), cut_text.begin(), cut_text.end()); + set_contents(before, after); +} + +size_t editablewidechar::adjust_shift_pos(size_t &shiftoffset, + size_t width, + widecharbuf &wbefore, + widecharbuf &wafter) +{ + { + std::vector<unicode_char> before, after; + get_contents(before, after); + + wbefore.init_unicode(before.begin(), before.end()); + wafter.init_unicode(after.begin(), after.end()); + } + + if (wbefore.graphemes.size() < shiftoffset) + // Cursor position before the current left margin + shiftoffset=wbefore.graphemes.size(); + + size_t last_grapheme_width=0; + + size_t virtual_col=0; + + for (size_t i=0; i<wbefore.graphemes.size(); ++i) + virtual_col += (last_grapheme_width=wbefore.graphemes[i] + .wcwidth(virtual_col)); + + if (shiftoffset > 0 + && shiftoffset == wbefore.graphemes.size() + && width > last_grapheme_width) + // If the field position is shifted, and the cursor is position + // on the left margin, and the field width is sufficient to + // show the last grapheme, shift left. This basically keeps + // the cursor off the left margin if the field is shifted + // horizontally. + --shiftoffset; + + + size_t w=0; + + for (std::vector<widecharbuf::grapheme_t>::const_iterator + b(wbefore.graphemes.begin()), + e(wbefore.graphemes.end()); b != e; ++b) + { + if (w >= width) + break; + + w += b->wcwidth(w); + } + + for (std::vector<widecharbuf::grapheme_t>::const_iterator + b(wafter.graphemes.begin()), + e(wafter.graphemes.end()); b != e; ++b) + { + if (w >= width) + break; + + w += b->wcwidth(w); + } + + if (w < width && shiftoffset != 0) + // Entire contents fit, so shift offset must be 0 + shiftoffset=0; + + // Compute shift offset for the cursor position to be on the last + // position in the field. + + w=width; + + if (w) + --w; + + virtual_col=0; + for (std::vector<widecharbuf::grapheme_t>::const_iterator + b(wbefore.graphemes.begin()), + e(wbefore.graphemes.end()); b != e; ) + { + --e; + + size_t grapheme_width=e->wcwidth(virtual_col); + + if (grapheme_width > w) + { + size_t newoffset=e-b+1; + + if (newoffset > shiftoffset) + { + // Must shift right. + + shiftoffset=newoffset; + break; + } + } + w -= grapheme_width; + virtual_col += w; + } + + size_t col=0; + + if (shiftoffset < wbefore.graphemes.size()) + { + virtual_col=0; + + for (std::vector<widecharbuf::grapheme_t>::const_iterator + b(wbefore.graphemes.begin()), + e(wbefore.graphemes.begin()+shiftoffset); + b != e; ++b) + virtual_col += b->wcwidth(virtual_col); + + for (std::vector<widecharbuf::grapheme_t>::const_iterator + b(wbefore.graphemes.begin()+shiftoffset), + e(wbefore.graphemes.end()); b != e; ++b) + { + size_t grapheme_width=b->wcwidth(virtual_col); + + col += grapheme_width; + virtual_col += grapheme_width; + } + } + + return col; +} + +CursesFlowedLine::CursesFlowedLine(const std::vector<unicode_char> &textArg, + bool flowedArg) + : text(mail::iconvert::convert(textArg, "utf-8")), flowed(flowedArg) +{ +} + diff --git a/curses/widechar.H b/curses/widechar.H new file mode 100644 index 0000000..b541a25 --- /dev/null +++ b/curses/widechar.H @@ -0,0 +1,678 @@ +/* +** Copyright 2011, Double Precision Inc. +** +** See COPYING for distribution information. +*/ + +#ifndef widechar_H +#define widechar_H + +#include "../curses/curses_config.h" +#include "../unicode/unicode.h" +#include <string.h> + +#include <wchar.h> +#include <vector> +#include <iterator> + +/* +** Convert a char sequence defined by beg_iter and end_iter iterators into +** a wchar_t sequence, written to the out_iter output iterator +*/ + +template<typename input_iter, typename output_iter> +output_iter towidechar(input_iter beg_iter, + input_iter end_iter, + output_iter out_iter) +{ + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + + std::vector<char> cbuf; + cbuf.reserve(MB_CUR_MAX*2); + + int cnt=MB_CUR_MAX*2; + + while (1) + { + if (cnt == 0 || beg_iter == end_iter) + { + const char *b=&cbuf[0]; + const char *e=&cbuf[cbuf.size()]; + size_t cnt=0; + + while (b != e) + { + wchar_t wc; + size_t r=mbrtowc(&wc, b, e-b, &ps); + + if (r == (size_t)-1) + { + wc='?'; + memset(&ps, 0, sizeof(ps)); + r=1; + } + + if (r > (size_t)(e-b)) + // must be (size_t)-2 - + // Incomplete multibyte sequence + break; + + if (r == 0) + { + wc=0; + r=1; + } + + *out_iter++=wc; + + b += r; + cnt += r; + } + cbuf.clear(); + cnt=MB_CUR_MAX; + + if (beg_iter == end_iter) + break; + } + + cbuf.push_back(*beg_iter++); + --cnt; + } + + return out_iter; +} + +/* +** Convenience function. +*/ + +template<typename input_iter> +void towidechar(input_iter beg_iter, + input_iter end_iter, + std::vector<wchar_t> &uc) +{ + uc.clear(); + + towidechar(beg_iter, end_iter, + std::back_insert_iterator<std::vector<wchar_t> >(uc)); +} + +/* +** Convert a wchar_t sequence defined by beg_iter and end_iter iterators into +** a char sequence, written to the out_iter output iterator +*/ + +template<typename input_iter, + typename output_iter> +output_iter fromwidechar(input_iter b, input_iter e, output_iter out_iter) +{ + mbstate_t ps; + std::vector<char> buf; + + buf.resize(MB_CUR_MAX*2); + + memset(&ps, 0, sizeof(ps)); + + for (; b != e; ++b) + { + size_t n=wcrtomb(&buf[0], *b, &ps); + + if (n == (size_t)-1) + continue; + + out_iter=std::copy(buf.begin(), buf.begin()+n, out_iter); + } + + size_t n=wcrtomb(&buf[0], 0, &ps); + + if (n != (size_t)-1) + { + if (n > 0 && buf[n-1] == 0) + --n; + out_iter=std::copy(buf.begin(), buf.begin()+n, out_iter); + } + return out_iter; +} + +/* +** Convenience function. +*/ + +template<typename input_iter> +std::string fromwidechar(input_iter beg_iter, + input_iter end_iter) +{ + std::string s; + + fromwidechar(beg_iter, end_iter, + std::back_insert_iterator<std::string>(s)); + + return s; +} + +// A unicode string demarcated at grapheme boundaries + +class widecharbuf { + +public: + widecharbuf() {} + + // Initialize from a string + + void init_string(const std::string &str); + + // Given the maximum desired column width of the string, + // return a pair of string, and the width of the returned + // string. + std::pair<std::string, size_t> + get_string_truncated(size_t maxwidth, ssize_t atcol) const; + + // Return as a unicode string + std::pair<std::vector<unicode_char>, size_t> + get_unicode_truncated(size_t maxwidth, ssize_t atcol) const; + + // Return a substring + std::string get_substring(size_t first_grapheme, + size_t grapheme_cnt) const; + + // Return a unicode substring + std::vector<unicode_char> get_unicode_substring(size_t first_grapheme, + size_t grapheme_cnt) + const; + + // Return as a unicode string, truncated or padded to the given width + + std::vector<unicode_char> get_unicode_fixedwidth(size_t width, + ssize_t atcol) const; + + // Initialize from a beginning and ending iterator, iterating + // over unicode_chars. + + template<typename iter_type> + void init_unicode(iter_type b, iter_type e) + { + std::string s; + + mail::iconvert::fromu::convert(b, e, + unicode_default_chset(), s); + + std::vector<wchar_t> wc; + + towidechar(s.begin(), s.end(), wc); + + s=fromwidechar(wc.begin(), wc.end()); + + mail::iconvert::tou::convert(s.begin(), s.end(), + unicode_default_chset(), + ustring); + resetgraphemes(); + } + + // The unicode string + + std::vector<unicode_char> ustring; + + // A grapheme: a pointer somewhere in ustring, plus wchar count + + class grapheme_t { + + public: + const unicode_char *uptr; // Offset into ustring + size_t cnt; // How many unicode chars in the grapheme + + grapheme_t(const unicode_char *uptrArg, size_t cntArg) + : uptr(uptrArg), cnt(cntArg) {} + + size_t wcwidth(ssize_t start_col) const; + }; + + std::vector<grapheme_t> graphemes; + + // Reset wchar_t in each grapheme + void resetgraphemes(); + + void clear() + { + ustring.clear(); + graphemes.clear(); + } + + ssize_t expandtabs(ssize_t col); + + // Append + widecharbuf &operator+=(const widecharbuf &); + + // Replace + + widecharbuf(const widecharbuf &o) + { + operator=(o); + } + + widecharbuf &operator=(const widecharbuf &o); + + size_t wcwidth(ssize_t start_col) const; + + void tounicode(std::vector<unicode_char> &text) const; + + static size_t charwidth(wchar_t ch, ssize_t atcol); +}; + +// +// Output iterator for towidechar() that throws away the wide character, +// but keeps track of its width. +// +// widecharbuf::grapheme_t::wcwidth() is hot. Saving the output of +// towidechar() into a vector is very expensive. + +class towidechar_wcwidth_iter + : public std::iterator<std::output_iterator_tag, void, void, void, void> +{ + + size_t col; + size_t w; + +public: + + towidechar_wcwidth_iter(size_t colArg) : col(colArg), w(0) {} + + towidechar_wcwidth_iter &operator++() { return *this; } + towidechar_wcwidth_iter &operator++(int) { return *this; } + towidechar_wcwidth_iter &operator*() { return *this; } + + void operator=(wchar_t wc) + { + size_t ww=widecharbuf::charwidth(wc, col); + + col += ww; + w += ww; + } + + operator size_t() const { return w; } +}; + +// Editable wchar_ts, together with an insertion point. +// +// The wchar_ts get internally divided into three parts: before the +// insertion point, at the insertion point, and past the insertion +// point. Before and after contain valid graphemes. The current +// insertion point may or may not contain valid graphemes. +// +// set_contents() initializes wchar_ts before and after the +// insertion point, setting at the insertion point to an empty list. +// +// add() adds a wchar_t at the insertion point. +// +// get_contents() retrieves the contents as unicode characters, +// combining the insertion point wchar_ts with wchar_ts before the +// insertion point. +// +// contents_cut() removes graphemes between the cut_pos and the +// insertion point. contents_cut() may only be invoked when the +// insertion point is empty. The removed graphemes get placed into +// cut_text. +// +// insert_to_before() calls get_contents() then set_contents(), +// essentially converting wchar_ts at the insertion point to +// valid graphemes, and moving them to before_insert. +// +// to_before() calls insert_to_before(), then moves after_insert +// to before_insert(). +// +// to_after() calls insert_to_before(), then moves before_isnert +// to after_insert(). +// +// to_position() calls insert_to_before() then moves the insertion +// point to the given position. +// +// adjust_shift_position() calculates horizontal scrolling. It takes +// the editable field's width, and a reference to the current +// horizontal shift position, which gets adjusted, as necessary, to +// keep the cursor position visible, and returns the cursor position. + +class editablewidechar { + +public: + widecharbuf before_insert; + + std::vector<unicode_char> inserted; + + widecharbuf after_insert; + + void set_contents(const std::vector<unicode_char> &before, + const std::vector<unicode_char> &after); + + void get_contents(std::vector<unicode_char> &before, + std::vector<unicode_char> &after) const; + + void contents_cut(size_t cut_pos, + std::vector<unicode_char> &cut_text); + + void insert_char(unicode_char wc) { inserted.push_back(wc); } + + void insert_to_before(); + + void to_before(); + + void to_after(); + + void clear() + { + before_insert.clear(); + after_insert.clear(); + inserted.clear(); + } + + void to_position(size_t pos); + + size_t adjust_shift_pos(size_t &shiftoffset, size_t width, + + // wbefore and wafter: wide characters + // before and after cursor position, + // as computed by adjust_shift_pos(). + // Useful to the caller + widecharbuf &wbefore, + widecharbuf &wafter); +}; + +/* +** Helper class for the word-wrapping logic. +** +** This class receives unicode_char pieces, that can be broken. The collector +** converts them to system wchars, measures each piece, and collects them +** until the next piece can't fit within the alloted width. +*/ + +template<typename output_sink> +class wordwrap_collector { + +public: + /* Collected line */ + std::vector<unicode_char> linebuf; + + /* Width of the collected line */ + size_t col; + + /* + ** iter(line) gets invoked for each assembled line, where + ** line is a std::vector<wchar_t>. + */ + + output_sink &iter; + + /* + ** Desired line width. + */ + + size_t towidth; + + /* + ** If true, the trailing space on each line gets removed. + ** In all cases, each line is wrapped at towidth() characters, + ** where possible. + */ + + bool delsp; + + wordwrap_collector(output_sink &iterArg, + size_t towidthArg, + bool delspArg) : col(0), iter(iterArg), + towidth(towidthArg), + delsp(delspArg) + { + } + + void addsegment(const std::vector<unicode_char> &segment) + { + std::vector<wchar_t> wsegment; + + { + std::string s=mail::iconvert + ::convert(segment, unicode_default_chset()); + + towidechar(s.begin(), s.end(), wsegment); + + if (wsegment.empty()) + return; + } + + size_t width=0; + + for (std::vector<wchar_t>::const_iterator + b(wsegment.begin()), + e(wsegment.end()); b != e; ++b) + width += widecharbuf::charwidth(*b, col+width); + + if (!(delsp && width + col == towidth+1 && + *--wsegment.end() == ' ')) + { + if (width + col > towidth && !linebuf.empty()) + breakline(); + } + + linebuf.insert(linebuf.end(), segment.begin(), segment.end()); + col += width; + } + + void breakline() + { + if (delsp && !linebuf.empty() && + linebuf[linebuf.size()-1] == ' ') + linebuf.pop_back(); + + *iter++=linebuf; + col=0; + linebuf.clear(); + } +}; + + +/* +** A default rewrap helper object that does not rewrap anything. +*/ + +class unicoderewrapnone { + +public: + bool operator()(size_t n) const; +}; + +/* +** Unicode-based linewrapping logic. +** +** This template defines an output iterator that takes in unicode_chars. +** The constructor receives a rewrap helper object reference, an output +** iterator, the requested width, and whether to trim the trailing +** space from each wrapped line. +** +** As this iterator is iterated over unicode_chars, it will iterate the +** received output iterator over std::vector<unicode_char>s, representing +** each line wrapped to the requested width. +** +** The trim flag should normally be false. This properly preserves all +** whitespace in the unicode character sequence. The trim flag may be true +** only in contexts where the wrapped text will never be rewrapped again. +** Removal of a trailing space on each line allows an extra character to be +** present instead of the trailing space. +** +** The rewrap helper object instance must define a bool operator()(size_t n) +** const. size_t receives a character offset, and should return true if +** character #n is the first character in an original line of text. If so, +** and the unicode word wrap algorithm does not indicate that there's a +** potential linebreak here, a space gets appended at this point. This is +** used to rewrap existing lines of text which may not end with a space +** character. +** +** After the original unicode chars are iterated over, eof() must be +** invoked in order to output any partially-wrapped content that's still +** held internally in this iterator. +*/ + +template<typename output_sink_t, + typename rewrap_helper_t=unicoderewrapnone> class unicodewordwrapper : + public std::iterator<std::output_iterator_tag, void, void, void, void> { + + // State maintained by the iterator. This iterator is copyable. + // The state is associated with only one iterator instance. Copying + // an iterator copies the state from the original iterator into the + // new one. + + class buffer : public mail::linebreakc_callback_save_buf { + + public: + std::vector<unicode_char> segment; // Current word + size_t cnt; // Counts characters that are being wrapped. + + wordwrap_collector<output_sink_t> collector; + // The collector object. + + const rewrap_helper_t &rewrapper; + // The rewrap helper object. + + buffer(const rewrap_helper_t &rewrapperArg, + output_sink_t &out_iter, + size_t towidth, + bool delsp) + : cnt(0), + collector(out_iter, towidth, delsp), + rewrapper(rewrapperArg) {} + }; + + mutable buffer *buf; + + typedef unicodewordwrapper<output_sink_t, rewrap_helper_t> iter_t; + +public: + // Iterator constructor + unicodewordwrapper(const rewrap_helper_t &rewrap_helper, + output_sink_t &out_iter, + size_t towidth, + bool delsp) + : buf(new buffer(rewrap_helper, out_iter, towidth, delsp)) + { + buf->set_opts(UNICODE_LB_OPT_PRBREAK| + UNICODE_LB_OPT_SYBREAK| + UNICODE_LB_OPT_DASHWJ); + } + + // End iterator constructor + unicodewordwrapper() : buf(NULL) + { + } + + ~unicodewordwrapper() + { + eof(); + } + + // Assignment operator moves the state + + iter_t &operator=(const iter_t &o) + { + if (buf) + delete buf; + + buf=o.buf; + o.buf=NULL; + return *this; + } + + // Copy constructor moves the state + unicodewordwrapper(const iter_t &o) : buf(o.buf) + { + o.buf=NULL; + } + + // Operator implementation + + iter_t &operator++() { return *this; } + iter_t &operator++(int) { return *this; } + iter_t &operator*() { return *this; } + + void operator=(unicode_char ch) + { + if (!buf) + return; + + // Feed into the linebreaking algorithm. + + buf->operator<<(ch); + + // Process linebreaking algorithm output. + + while (!buf->lb_buf.empty()) + { + std::pair<int, unicode_char> ch(buf->lb_buf.front()); + buf->lb_buf.pop_front(); + + // If text is being rewrapped, and the linebreaking + // algorithm prohibits a linebreak here, but this + // was the first character of the pre-wrapped line, + // then there must've been a space character here, + // which allows a break. + + if (ch.first == UNICODE_LB_NONE && buf->cnt > 0 && + (buf->rewrapper)(buf->cnt)) + { + buf->segment.push_back(' '); + ch.first=UNICODE_LB_ALLOWED; + } + ++buf->cnt; + + // Process a potential linebreak. + + if (ch.first != UNICODE_LB_NONE) + { + buf->collector.addsegment(buf->segment); + + if (ch.first == UNICODE_LB_MANDATORY) + buf->collector.breakline(); + buf->segment.clear(); + } + + if (ch.second != '\r' && ch.second != '\n') + buf->segment.push_back(ch.second); + } + } + + // Finish remaining content, and clean up. + + void eof() + { + if (buf) + { + buf->collector.addsegment(buf->segment); + if (!buf->collector.linebuf.empty()) + buf->collector.breakline(); + + delete buf; + buf=NULL; + } + } +}; + +// A convenience function to iterate over an arbitrary sequence defined +// by a beginning and an ending iterator, and wrap it. + +template<typename input_iter, typename output_sink, + typename rewrap_helper_t> +void unicodewordwrap(input_iter beg_iter, + input_iter end_iter, + const rewrap_helper_t &rewrap_helper, + output_sink &out_iter, + size_t towidth, + bool delsp) +{ + unicodewordwrapper<output_sink, rewrap_helper_t> + iter(rewrap_helper, out_iter, towidth, delsp); + + while (beg_iter != end_iter) + { + iter= *beg_iter; + ++beg_iter; + } + iter.eof(); +} + +#endif |
