From 4dca2a5a2d8b951d7329e1794d43e1241412a4eb Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Fri, 29 Apr 2011 01:45:10 -0400 Subject: [PATCH] View - Documented the new layout algorithm. - Used Nibbler for decomposing fields into word chunks. - Implemented variable intra padding. - Implemented variable left margin. - Implemented variable extra padding. - Implemented colored headers. - Implemented wrappable headers. - Eliminated need to specify fixed column size. --- src/View.cpp | 162 +++++++++++++++++++++++++++++-------- src/View.h | 2 + src/columns/ColID.cpp | 20 ++--- src/columns/ColID.h | 3 +- src/columns/ColProject.cpp | 48 ++++++----- src/columns/ColProject.h | 3 +- src/columns/Column.cpp | 19 ++--- src/columns/Column.h | 5 +- test/view.t.cpp | 11 ++- 9 files changed, 186 insertions(+), 87 deletions(-) diff --git a/src/View.cpp b/src/View.cpp index 27f18da11..d27f66796 100644 --- a/src/View.cpp +++ b/src/View.cpp @@ -34,6 +34,7 @@ View::View () : _width (0) , _left_margin (0) +, _header (0) , _odd (0) , _even (0) , _intra_padding (1) @@ -53,17 +54,41 @@ View::~View () } //////////////////////////////////////////////////////////////////////////////// -// +-------+ +-------+ +-------+ -// |header | |header | |header | -// +--+--+-------+--+-------+--+-------+--+ -// |ma|ex|cell |in|cell |in|cell |ex| -// +--+--+-------+--+-------+--+-------+--+ -// |ma|ex|cell |in|cell |in|cell |ex| -// +--+--+-------+--+-------+--+-------+--+ +// |<---------- terminal width ---------->| +// +// +-------+ +-------+ +-------+ +// |header | |header | |header | +// +--+--+-------+--+-------+--+-------+--+ +// |ma|ex|cell |in|cell |in|cell |ex| +// +--+--+-------+--+-------+--+-------+--+ +// |ma|ex|cell |in|cell |in|cell |ex| +// +--+--+-------+--+-------+--+-------+--+ // -// margin -// extrapadding -// intrapadding +// margin - indentation for the whole table +// extrapadding - left and right padding for the whole table +// intrapadding - padding between columns +// +// +// Layout Algorithm: +// - Height is irrelevant +// - Determine the usable horizontal space for N columns: +// +// usable = width - ma - (ex * 2) - (in * (N - 1)) +// +// - Look at every column, for every task, and determine the minimum and +// maximum widths. The minimum is the length of the largest indivisible +// word, and the maximum is the full length of the value. +// - If there is sufficient terminal width to display every task using the +// maximum width, then do so. +// - If there is insufficient terminal width to display every task using the +// minimum width, then there is no layout solution. Error. +// - Otherwise there is a need for column wrapping. Calculate the overage, +// which is the difference between the sum of the minimum widths and the +// usable width. +// - Start by using all the minimum column widths, and distribute the overage +// among all columns, one character at a time, while the column width is +// less than the maximum width, and while there is overage remaining. +// std::string View::render (std::vector & data, std::vector & sequence) { // Determine minimal, ideal column widths. @@ -80,6 +105,7 @@ std::string View::render (std::vector & data, std::vector & sequence) std::vector ::iterator d; for (d = data.begin (); d != data.end (); ++d) { + // Determine minimum and ideal width for this column. int min; int ideal; (*i)->measure (*d, min, ideal); @@ -92,48 +118,48 @@ std::string View::render (std::vector & data, std::vector & sequence) ideal.push_back (global_ideal); } - // TODO Remove - std::string combined; - join (combined, ",", minimal); - std::cout << "# minimal " << combined << "\n"; - join (combined, ",", ideal); - std::cout << "# ideal " << combined << "\n"; +// // TODO Remove +// std::string combined; +// join (combined, ",", minimal); +// std::cout << "# minimal " << combined << "\n"; +// join (combined, ",", ideal); +// std::cout << "# ideal " << combined << "\n"; // Sum the minimal widths. int sum_minimal = 0; std::vector ::iterator c; for (c = minimal.begin (); c != minimal.end (); ++c) sum_minimal += *c; - std::cout << "# sum_minimal " << sum_minimal << "\n"; +// std::cout << "# sum_minimal " << sum_minimal << "\n"; // Sum the ideal widths. int sum_ideal = 0; for (c = ideal.begin (); c != ideal.end (); ++c) sum_ideal += *c; - std::cout << "# sum_ideal " << sum_ideal << "\n"; +// std::cout << "# sum_ideal " << sum_ideal << "\n"; // Calculate final column widths. int overage = _width - _left_margin - (2 * _extra_padding) - ((_columns.size () - 1) * _intra_padding); - std::cout << "# width " << _width << "\n"; +// std::cout << "# width " << _width << "\n"; std::vector widths; if (sum_ideal <= overage) - { - std::cout << "# ideal case: " << sum_ideal << " <= " << overage << "\n"; +// { +// std::cout << "# ideal case: " << sum_ideal << " <= " << overage << "\n"; widths = ideal; - } +// } else if (sum_minimal > overage) - { +// { throw std::string ("There is not enough horizontal width to display the results."); - } +// } else { widths = minimal; overage -= sum_minimal; - std::cout << "# overage " << overage << "\n"; +// std::cout << "# overage " << overage << "\n"; // Spread 'overage' among columns where width[i] < ideal[i] while (overage) @@ -147,24 +173,92 @@ std::string View::render (std::vector & data, std::vector & sequence) } } } - - join (combined, ",", widths); - std::cout << "# final widths " << combined << "\n"; +// +// join (combined, ",", widths); +// std::cout << "# final widths " << combined << "\n"; } - // TODO Compose column headers. + // Compose column headers. + int max_lines = 0; + std::vector > headers; + for (int c = 0; c < _columns.size (); ++c) + { + headers.push_back (std::vector ()); + _columns[c]->renderHeader (headers[c], widths[c], _header); - // TODO Render column headers. + if (headers[c].size () > max_lines) + max_lines = headers[c].size (); + } - // TODO Compose, render columns, in sequence. - std::stringstream output; +// for (int i = 0; i < headers.size (); ++i) +// for (int j = 0; j < headers[i].size (); ++j) +// std::cout << "# headers[" << i << "][" << j << "]=<" << headers[i][j] << ">\n"; + + // Output string. + std::string out; + + // Render column headers. + std::string left_margin = std::string (_left_margin, ' '); + std::string extra = std::string (_extra_padding, ' '); + std::string intra = std::string (_intra_padding, ' '); + + for (int i = 0; i < max_lines; ++i) + { + out += left_margin + extra; + + for (int c = 0; c < _columns.size (); ++c) + { + if (c) + out += intra; + + if (headers[i].size () < max_lines - i) + out += _header.colorize (std::string (widths[c], ' ')); + else + out += headers[c][i]; + } + + out += extra + "\n"; + } + + // Compose, render columns, in sequence. + Color color ("cyan"); + std::vector > cells; std::vector ::iterator s; for (s = sequence.begin (); s != sequence.end (); ++s) { - // TODO render each row. + max_lines = 0; + + for (int c = 0; c < _columns.size (); ++c) + { + cells.push_back (std::vector ()); + _columns[c]->render (cells[c], data[*s], widths[c], color); + + if (cells[c].size () > max_lines) + max_lines = cells[c].size (); + } + + for (int i = 0; i < max_lines; ++i) + { + out += left_margin + extra; + + for (int c = 0; c < _columns.size (); ++c) + { + if (c) + out += intra; + + if (i < cells[c].size ()) + out += cells[c][i]; + else + out += color.colorize (std::string (widths[c], ' ')); + } + + out += extra + "\n"; + } + + cells.clear (); } - return output.str (); + return out; } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/View.h b/src/View.h index 9e649dfe7..72bd6f67e 100644 --- a/src/View.h +++ b/src/View.h @@ -43,6 +43,7 @@ public: void add (Column* column) { _columns.push_back (column); } void width (int width) { _width = width; } void leftMargin (int margin) { _left_margin = margin; } + void colorHeader (Color& c) { _header = c; } void colorOdd (Color& c) { _odd = c; } void colorEven (Color& c) { _even = c; } void intraPadding (int padding) { _intra_padding = padding; } @@ -61,6 +62,7 @@ private: std::vector _columns; int _width; int _left_margin; + Color _header; Color _odd; Color _even; int _intra_padding; diff --git a/src/columns/ColID.cpp b/src/columns/ColID.cpp index 22d13a3d0..9dd737937 100644 --- a/src/columns/ColID.cpp +++ b/src/columns/ColID.cpp @@ -25,9 +25,6 @@ // //////////////////////////////////////////////////////////////////////////////// -#include // TODO Remove -#include -#include #include #include #include @@ -61,22 +58,19 @@ void ColumnID::measure (Task& task, int& minimum, int& maximum) else length = (int) log10 ((double) task.id); // Slow minimum = maximum = length; - - std::cout << "# ColID::measure id=" << task.id << " min=" << minimum << " max=" << maximum << "\n"; } //////////////////////////////////////////////////////////////////////////////// -void ColumnID::render (std::vector & lines, Task& task, int width) +void ColumnID::render ( + std::vector & lines, + Task& task, + int width, + Color& color) { - std::stringstream line; - line << std::setw (width) << std::setfill (' ') << task.id; - if (task.id) - line << task.id; + lines.push_back (color.colorize (rightJustify (task.id, width))); else - line << '-'; - - lines.push_back (line.str ()); + lines.push_back (color.colorize (rightJustify ("-", width))); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/columns/ColID.h b/src/columns/ColID.h index 401d39ae0..da25805ac 100644 --- a/src/columns/ColID.h +++ b/src/columns/ColID.h @@ -30,6 +30,7 @@ #include #include #include +#include #include class ColumnID : public Column @@ -39,7 +40,7 @@ public: ~ColumnID (); void measure (Task&, int&, int&); - void render (std::vector &, Task&, int); + void render (std::vector &, Task&, int, Color&); private: }; diff --git a/src/columns/ColProject.cpp b/src/columns/ColProject.cpp index 7fbbf6409..7fd208c6c 100644 --- a/src/columns/ColProject.cpp +++ b/src/columns/ColProject.cpp @@ -25,10 +25,10 @@ // //////////////////////////////////////////////////////////////////////////////// -#include // TODO Remove -#include #include +#include #include +#include extern Context context; @@ -50,36 +50,34 @@ ColumnProject::~ColumnProject () void ColumnProject::measure (Task& task, int& minimum, int& maximum) { std::string project = task.get ("project"); - minimum = maximum = project.length (); - - std::string::size_type space = project.find (' '); - if (space == std::string::npos) - { - std::cout << "# ColProject::measure project=" << project << " min=" << minimum << " max=" << maximum << "\n"; - return; - } - minimum = 0; - int longest = 0; - std::string::size_type last = -1; - while (space != std::string::npos) + maximum = project.length (); + + Nibbler nibbler (project); + std::string word; + while (nibbler.getUntilWS (word)) { - if (space - last - 1 > longest) - longest = space - last - 1; - - last = space; - space = project.find (' ', last + 1); + nibbler.skipWS (); + if (word.length () > minimum) + minimum = word.length (); } - - if (longest) - minimum = longest; - - std::cout << "# ColProject::measure project=" << project << " min=" << minimum << " max=" << maximum << "\n"; } //////////////////////////////////////////////////////////////////////////////// -void ColumnProject::render (std::vector & lines, Task& task, int width) +void ColumnProject::render ( + std::vector & lines, + Task& task, + int width, + Color& color) { + // TODO Can't use Nibbler here. Need to use a UTF8-safe version of extractLines. + Nibbler nibbler (task.get ("project")); + std::string word; + while (nibbler.getUntilWS (word)) + { + nibbler.skipWS (); + lines.push_back (color.colorize (leftJustify (word, width))); + } } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/columns/ColProject.h b/src/columns/ColProject.h index 06bc4ad6b..69c73bb9f 100644 --- a/src/columns/ColProject.h +++ b/src/columns/ColProject.h @@ -30,6 +30,7 @@ #include #include #include +#include #include class ColumnProject : public Column @@ -39,7 +40,7 @@ public: ~ColumnProject (); void measure (Task&, int&, int&); - void render (std::vector &, Task&, int); + void render (std::vector &, Task&, int, Color&); private: }; diff --git a/src/columns/Column.cpp b/src/columns/Column.cpp index 18a4f5f2f..fb8dcf2ec 100644 --- a/src/columns/Column.cpp +++ b/src/columns/Column.cpp @@ -25,7 +25,6 @@ // //////////////////////////////////////////////////////////////////////////////// -#include #include #include #include @@ -87,27 +86,29 @@ Column::~Column () } //////////////////////////////////////////////////////////////////////////////// -void Column::renderHeader (std::vector & lines, int width) +void Column::renderHeader ( + std::vector & lines, + int width, + Color& color) { // Create a basic label. std::string header; header.reserve (width); header = _label; - // Right pad with spaces, if necessary. - int length = characters (_label); - if (length < width) - _label += std::string (' ', width - length); + // Create a fungible copy. + Color c = color; // Now underline the header, or add a dashed line. if (context.config.getBoolean ("fontunderline")) { - lines.push_back (header); + c.blend (Color (Color::nocolor, Color::nocolor, true, false, false)); + lines.push_back (c.colorize (leftJustify (header, width))); } else { - lines.push_back (header); - lines.push_back (std::string ('-', width)); + lines.push_back (c.colorize (leftJustify (header, width))); + lines.push_back (c.colorize (std::string (width, '-'))); } } diff --git a/src/columns/Column.h b/src/columns/Column.h index 3c41581f5..3fa0e21d4 100644 --- a/src/columns/Column.h +++ b/src/columns/Column.h @@ -29,6 +29,7 @@ #include #include +#include #include class Column @@ -49,8 +50,8 @@ public: std::string type () const { return _type; } virtual void measure (Task&, int&, int&) = 0; - virtual void renderHeader (std::vector &, int); - virtual void render (std::vector &, Task&, int) = 0; + virtual void renderHeader (std::vector &, int, Color&); + virtual void render (std::vector &, Task&, int, Color&) = 0; protected: std::string _type; diff --git a/test/view.t.cpp b/test/view.t.cpp index 50c283bc6..e232c38f2 100644 --- a/test/view.t.cpp +++ b/test/view.t.cpp @@ -40,6 +40,9 @@ int main (int argc, char** argv) try { + // Set up configuration. + context.config.set ("fontunderline", true); + // Two sample tasks. Task t1 ("[project:\"Home\"]"); t1.id = 1; @@ -55,14 +58,18 @@ int main (int argc, char** argv) sequence.push_back (0); sequence.push_back (1); + // Create colors. + Color header_color (Color (Color::yellow, Color::nocolor, false, false, false)); + // Create a view. View view; view.add (Column::factory ("id")); view.add (Column::factory ("project")); - view.width (12); - view.leftMargin (0); + view.width (16); + view.leftMargin (4); view.extraPadding (0); view.intraPadding (1); + view.colorHeader (header_color); // Render the view. std::cout << view.render (data, sequence)