fltk/src/Fl_Help_View.cxx
Matthias Melcher 910c7d18eb Fix a few more warnings by Clang.
- Apple Clang 17.0.0.
- png, z, jpeg lib integer cast warnings remain
- FLTK callback function cast warnings remain.
2025-11-21 13:37:54 +01:00

4436 lines
126 KiB
C++

//
// Fl_Help_View widget for the Fast Light Tool Kit (FLTK).
//
// Copyright 1997-2010 by Easy Software Products.
// Image support by Matthias Melcher, Copyright 2000-2009.
//
// Buffer management (HV_Edit_Buffer) and more by AlbrechtS and others.
// Copyright 2011-2025 by Bill Spitzak and others.
//
// This library is free software. Distribution and use rights are outlined in
// the file "COPYING" which should have been included with this file. If this
// file is missing or damaged, see the license at:
//
// https://www.fltk.org/COPYING.php
//
// Please see the following page on how to report bugs and issues:
//
// https://www.fltk.org/bugs.php
//
//
// FLTK header files
//
#include <FL/Fl_Help_View.H>
#include <FL/Fl_Shared_Image.H>
#include <FL/Fl_Window.H>
#include <FL/Fl_Pixmap.H>
#include <FL/Fl_Menu_Item.H>
#include <FL/fl_utf8.h>
#include <FL/filename.H> // fl_open_uri()
#include <FL/fl_string_functions.h> // fl_strdup()
#include <FL/fl_draw.H>
#include <FL/filename.H>
#include "flstring.h"
//
// System and C++ header files
//
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <ctype.h>
#include <errno.h>
#include <math.h>
#include <map>
#include <vector>
#include <string>
//
// Debugging
//
// Debug: set to 1 for basic debugging, 2 for more, 0 for none
#define DEBUG_EDIT_BUFFER 0
#if (DEBUG_EDIT_BUFFER > 1)
#define DEBUG_FUNCTION(L,F) \
printf("\n========\n [%d] --- %s\n========\n", L, F); \
fflush(stdout);
#else
#define DEBUG_FUNCTION(L,F)
#endif
//
// Constants
//
static constexpr int MAX_COLUMNS = 200;
//
// Implementation class
//
class Fl_Help_View::Impl
{
friend Fl_Help_View;
Fl_Help_View &view;
public:
Impl(Fl_Help_View *view) : view(*view)
{
title_[0] = '\0';
defcolor_ = FL_FOREGROUND_COLOR;
bgcolor_ = FL_BACKGROUND_COLOR;
textcolor_ = FL_FOREGROUND_COLOR;
linkcolor_ = FL_SELECTION_COLOR;
textfont_ = FL_TIMES;
textsize_ = 12;
value_ = nullptr;
blocks_.clear();
link_ = (Fl_Help_Func *)0;
link_list_.clear();
directory_.clear();
filename_.clear();
topline_ = 0;
leftline_ = 0;
size_ = 0;
hsize_ = 0;
selection_mode_ = Mode::DRAW;
selected_ = false;
selection_first_ = 0;
selection_last_ = 0;
scrollbar_size_ = 0;
}
~Impl()
{
clear_selection();
free_data();
}
private: // classes, structs, and types
/** Helper class to manage margins in Fl_Help_View. */
class Margin_Stack
{
std::vector<int> margins_;
public:
Margin_Stack() = default;
void clear();
int current() const;
int pop();
int push(int indent);
};
/** Internal class to manage the Fl_Help_View edit buffer.
This class is for internal use in this file. Its sole purpose is to
allow buffer management to avoid buffer overflows in stack variables
used to edit strings for formatting and drawing (STR #3275).
*/
class Edit_Buffer : public std::string {
public:
void add(int ucs);
int cmp(const char *str);
int width() const;
};
/** Private struct to describe blocks of text. */
struct Text_Block {
const char *start; // Start of text
const char *end; // End of text
uchar border; // Draw border?
Fl_Color bgcolor; // Background color
int x; // Indentation/starting X coordinate
int y; // Starting Y coordinate
int w; // Width
int h; // Height
int line[32]; // Left starting position for each line
int ol; // is ordered list <OL> element
int ol_num; // item number in ordered list
};
/** Private class to hold a link with target and its position on screen. */
struct Link {
std::string filename_; // Filename part of a link
std::string target; // Target part of a link
Fl_Rect box; // Clickable rectangle that defines the link area
};
/** Private font stack element definition. */
struct Font_Style {
Fl_Font f; ///< Font
Fl_Fontsize s; ///< Font Size
Fl_Color c; ///< Font Color
Font_Style(Fl_Font afont, Fl_Fontsize asize, Fl_Color acolor);
Font_Style() = default; ///< Default constructor
void get(Fl_Font &afont, Fl_Fontsize &asize, Fl_Color &acolor);
void set(Fl_Font afont, Fl_Fontsize asize, Fl_Color acolor);
};
/** Private class to hold font information on a stack. */
struct Font_Stack {
void init(Fl_Font f, Fl_Fontsize s, Fl_Color c);
void top(Fl_Font &f, Fl_Fontsize &s, Fl_Color &c);
void push(Fl_Font f, Fl_Fontsize s, Fl_Color c);
void pop(Fl_Font &f, Fl_Fontsize &s, Fl_Color &c);
size_t count() const;
private:
std::vector<Font_Style> elts_; ///< font elements
};
enum class Align { RIGHT = -1, CENTER, LEFT }; ///< Alignments
enum class Mode { DRAW, PUSH, DRAG }; ///< Draw modes
private: // data members
// HTML source and raw data
const char *value_; ///< Copy of raw HTML text, as set by `value()` or `load()`
std::string directory_; ///< Directory for current document
std::string filename_; ///< Original file name from `load()`
// HTML document data
std::string title_; ///< Title string from <title> tag
Font_Stack fstack_; ///< Font and style stack
std::vector<Text_Block> blocks_; ///< List of all text blocks on screen
std::vector<std::shared_ptr<Link> > link_list_; ///< List of all clickable links and their position on screen
std::map<std::string, int> target_line_map_; ///< List of vertical position of all HTML Targets in a document
int topline_; ///< Vertical offset of document, measure in pixels
int leftline_; ///< Horizontal offset of document, measure in pixels
int size_; ///< Total document height in pixels
int hsize_; ///< Maximum document width in pixels
// Default visual attributes
Fl_Color defcolor_; ///< Default text color, defaults to FL_FOREGROUND_COLOR
Fl_Color bgcolor_; ///< Background color, defaults to FL_BACKGROUND_COLOR
Fl_Color textcolor_; ///< Text color, defaults to FL_FOREGROUND_COLOR
Fl_Color linkcolor_; ///< Link color, FL_SELECTION_COLOR
Fl_Font textfont_; ///< Default font for text, FL_TIMES
Fl_Fontsize textsize_; ///< Default font size, defaults to 12, should be FL_NORMAL_SIZE
// Text selection and mouse handling
Mode selection_mode_; ///< Remember election mode between FL_PUSH, FL_DRAG, and FL_RELEASE
bool selected_; ///< True if there is text selected
int selection_first_; ///< First character of selection, offset in value_
int selection_last_; ///< Last character of selection, offset in value_
Fl_Color tmp_selection_color_; ///< Selection color during draw operation
Fl_Color selection_text_color_; ///< Selection text color during draw operation
// The following members are static because we need them only once during mouse events
static int selection_push_first_; ///< First character of selection during mouse down
static int selection_push_last_; ///< Last character of selection during mouse down
static int selection_drag_first_; ///< First character of selection during mouse drag
static int selection_drag_last_; ///< Last character of selection during mouse drag
static Mode draw_mode_; ///< Temporarily modify `draw()` method to measure selection start or end during `handle()`
static int current_pos_; ///< Temporarily store text offset while measuring during `handle()`
// Callback
Fl_Help_Func *link_; ///< Link transform function
// Scrollbars
int scrollbar_size_; ///< Size for both scrollbars
private: // methods
// HTML source and raw data, getter
void free_data();
std::shared_ptr<Link> find_link(int, int);
void follow_link(std::shared_ptr<Link>);
// HTML interpretation and formatting
Text_Block *add_block(const char *s, int xx, int yy, int ww, int hh, uchar border = 0);
void add_link(const std::string &link, int xx, int yy, int ww, int hh);
void add_target(const std::string &n, int yy);
int do_align(Text_Block *block, int line, int xx, Align a, int &l);
void format();
void format_table(int *table_width, int *columns, const char *table);
Align get_align(const char *p, Align a);
const char *get_attr(const char *p, const char *n, char *buf, int bufsize);
Fl_Color get_color(const char *n, Fl_Color c);
Fl_Shared_Image *get_image(const char *name, int W, int H);
int get_length(const char *l);
// Font and font stack
/// Initialize the font stack with default values.
void initfont(Fl_Font &f, Fl_Fontsize &s, Fl_Color &c) { f = textfont_; s = textsize_; c = textcolor_; fstack_.init(f, s, c); }
/// Push the current font and size onto the stack.
void pushfont(Fl_Font f, Fl_Fontsize s) {fstack_.push(f, s, textcolor_);}
/// Push the current font, size, and color onto the stack.
void pushfont(Fl_Font f, Fl_Fontsize s, Fl_Color c) {fstack_.push(f, s, c);}
/// Get the current font, size, and color from the stack.
void popfont(Fl_Font &f, Fl_Fontsize &s, Fl_Color &c) {fstack_.pop(f, s, c);}
// Text selection
void hv_draw(const char *t, int x, int y, int entity_extra_length = 0);
char begin_selection();
char extend_selection();
void end_selection();
protected:
// Widget management
void draw();
public:
static const char *copy_menu_text;
// Widget management
int handle(int);
void resize(int,int,int,int);
/** Changes the size of the widget. \see Fl_Widget::size(int, int) */
// HTML source and raw data
void value(const char *val);
/** Return a pointer to the internal text buffer. */
const char *value() const { return (value_); }
int load(const char *f);
int find(const char *s, int p = 0);
void link(Fl_Help_Func *fn);
const char *filename() const;
const char *directory() const;
const char *title() const;
// Rendering attributes
/** Return the document height in pixels. */
int size() const { return (size_); }
/** Set the default text color. */
void textcolor(Fl_Color c) { if (textcolor_ == defcolor_) textcolor_ = c; defcolor_ = c; }
/** Return the current default text color. */
Fl_Color textcolor() const { return (defcolor_); }
/** Set the default text font. */
void textfont(Fl_Font f) { textfont_ = f; format(); }
/** Return the default text font. */
Fl_Font textfont() const { return (textfont_); }
/** Set the default text size. */
void textsize(Fl_Fontsize s) { textsize_ = s; format(); }
/** Get the default text size. */
Fl_Fontsize textsize() const { return (textsize_); }
void topline(const char *n);
void topline(int);
/** Get the current top line in pixels. */
int topline() const { return (topline_); }
void leftline(int);
/** Get the left position in pixels. */
int leftline() const { return (leftline_); }
// Text selection
void clear_selection();
void select_all();
int text_selected() const;
int copy(int clipboard=1);
// Scroll bars
int scrollbar_size() const;
void scrollbar_size(int newSize);
};
//
// global and class static values
//
static char initial_load = 0;
// We don't put the offscreen buffer in the help view class because
// we'd need to include platform.H in the header...
static Fl_Offscreen fl_help_view_buffer;
int Fl_Help_View::Impl::selection_push_first_ = 0;
int Fl_Help_View::Impl::selection_push_last_ = 0;
int Fl_Help_View::Impl::selection_drag_first_ = 0;
int Fl_Help_View::Impl::selection_drag_last_ = 0;
Fl_Help_View::Impl::Mode Fl_Help_View::Impl::draw_mode_ = Mode::DRAW;
int Fl_Help_View::Impl::current_pos_ = 0;
//
// Local functions declarations, implementations are at the end of the file
//
static int quote_char(const char *);
static std::string to_lower(const std::string &str);
static size_t url_scheme(const std::string &url, bool skip_slashes=false);
static const char *vanilla(const char *p, const char *end);
static uint32_t command(const char *cmd);
static constexpr uint32_t CMD(char a, char b, char c, char d)
{
return ((a<<24)|(b<<16)|(c<<8)|d);
}
//
// Static data.
//
// Broken Image
static const char * const broken_xpm[] =
{
"16 24 4 1",
"@ c #000000",
" c #ffffff",
"+ c none",
"x c #ff0000",
// pixels
"@@@@@@@+++++++++",
"@ @++++++++++",
"@ @+++++++++++",
"@ @++@++++++++",
"@ @@+++++++++",
"@ @+++@+++++",
"@ @++@@++++@",
"@ xxx @@ @++@@",
"@ xxx xx@@ @",
"@ xxx xxx @",
"@ xxxxxx @",
"@ xxxx @",
"@ xxxxxx @",
"@ xxx xxx @",
"@ xxx xxx @",
"@ xxx xxx @",
"@ @",
"@ @",
"@ @",
"@ @",
"@ @",
"@ @",
"@ @",
"@@@@@@@@@@@@@@@@",
nullptr
};
static Fl_Pixmap broken_image(broken_xpm);
/** This text may be customized at run-time. */
const char *Fl_Help_View::copy_menu_text = "Copy";
static Fl_Menu_Item rmb_menu[] = {
{ nullptr, 0, nullptr, (void*)1 }, // Copy
{ nullptr }
};
//
// Implementation
//
// ---- Helper class to manage margins in Fl_Help_View
void Fl_Help_View::Impl::Margin_Stack::clear() {
margins_.clear();
margins_.push_back(4); // default margin
}
int Fl_Help_View::Impl::Margin_Stack::current() const {
return margins_.back();
}
int Fl_Help_View::Impl::Margin_Stack::pop() {
if (margins_.size() > 1) {
margins_.pop_back();
}
return margins_.back();
}
int Fl_Help_View::Impl::Margin_Stack::push(int indent) {
int xx = current() + indent;
margins_.push_back(xx);
return xx;
}
// ---- Helper class HV_Edit_Buffer to ease buffer management
// Append one Unicode character (code point) to the buffer.
// The Unicode character \p ucs is converted to UTF-8 and appended to
// the buffer.
// \param[in] ucs Unicode character (code point) to be added
void Fl_Help_View::Impl::Edit_Buffer::add(int ucs){
int len;
char cbuf[6];
len = fl_utf8encode((unsigned int)ucs, cbuf); // always returns value >= 1
append(cbuf, len);
}
// case insensitive comparison of buffer contents with a string
int Fl_Help_View::Impl::Edit_Buffer::cmp(const char *str) {
return !strcasecmp(c_str(), str);
}
// string width of the entire buffer contents
int Fl_Help_View::Impl::Edit_Buffer::width() const {
return (int)fl_width(c_str());
}
// ---- Implementation of Font_Style class methods
/**
\brief Constructs a Font_Style object with the specified font, size, and color.
\param afont The font to use.
\param asize The font size.
\param acolor The font color.
*/
Fl_Help_View::Impl::Font_Style::Font_Style(Fl_Font afont, Fl_Fontsize asize, Fl_Color acolor) {
set(afont, asize, acolor);
}
/**
\brief Retrieves the font, font size, and color settings from this Font_Style instance.
\param[out] afont Reference to a variable where the font will be stored.
\param[out] asize Reference to a variable where the font size will be stored.
\param[out] acolor Reference to a variable where the font color will be stored.
*/
void Fl_Help_View::Impl::Font_Style::get(Fl_Font &afont, Fl_Fontsize &asize, Fl_Color &acolor) {
afont=f; asize=s; acolor=c;
}
/**
\brief Sets the font, font size, and color for the Font_Style.
This only set the members of the class, but does not change the current
rendering settings.
\param afont The font to be used.
\param asize The font size to be set.
\param acolor The color to be applied to the font.
*/
void Fl_Help_View::Impl::Font_Style::set(Fl_Font afont, Fl_Fontsize asize, Fl_Color acolor) {
f=afont; s=asize; c=acolor;
}
// ---- Implementation of Font_Stack class methods
/**
\brief Initializes the font stack with a default font, size, and color.
Clears the stack and pushes one element with a default font, size, and color.
\param[in] f font to apply
\param[in] s font size to apply
\param[in] c color to apply
*/
void Fl_Help_View::Impl::Font_Stack::init(Fl_Font f, Fl_Fontsize s, Fl_Color c) {
elts_.clear();
push(f, s, c);
}
/**
\brief Gets the top (current) element on the stack.
\param[out] f font to apply
\param[out] s font size to apply
\param[out] c color to apply
\note This function does not pop the stack, it just returns the top element.
*/
void Fl_Help_View::Impl::Font_Stack::top(Fl_Font &f, Fl_Fontsize &s, Fl_Color &c) {
elts_.back().get(f, s, c);
}
/**
\brief Push the font style triplet on the stack.
Also calls fl_font() and fl_color().
\param[in] f font to apply
\param[in] s font size to apply
\param[in] c color to apply
*/
void Fl_Help_View::Impl::Font_Stack::push(Fl_Font f, Fl_Fontsize s, Fl_Color c) {
elts_.push_back(Font_Style(f, s, c));
fl_font(f, s);
fl_color(c);
}
/**
\brief Pop style form the stack and apply new top style.
Pops the font style triplet from the stack and calls fl_font()
and fl_color().
\param[out] f font to apply
\param[out] s font size to apply
\param[out] c color to apply
\note If the stack has only one element left, that element will not be popped,
but the top element will be applied again.
*/
void Fl_Help_View::Impl::Font_Stack::pop(Fl_Font &f, Fl_Fontsize &s, Fl_Color &c) {
if (elts_.size() > 1)
elts_.pop_back();
top(f, s, c);
fl_font(f, s);
fl_color(c);
}
/**
\brief Gets the current count of font style elements in the stack.
\return stack size in number of elements
*/
size_t Fl_Help_View::Impl::Font_Stack::count() const {
return elts_.size();
}
// ------ Fl_Help_View Private methods
// ---- HTML source and raw data, getter
/**
\brief Frees memory used for the document.
*/
void Fl_Help_View::Impl::free_data() {
// Release all images...
if (value_) {
const char *ptr, // Pointer into block
*attrs; // Pointer to start of element attributes
Edit_Buffer buf; // Text buffer
char attr[1024], // Attribute buffer
wattr[1024], // Width attribute buffer
hattr[1024]; // Height attribute buffer
DEBUG_FUNCTION(__LINE__,__FUNCTION__);
for (ptr = value_; *ptr;)
{
if (*ptr == '<')
{
ptr ++;
if (strncmp(ptr, "!--", 3) == 0)
{
// Comment...
ptr += 3;
if ((ptr = strstr(ptr, "-->")) != nullptr)
{
ptr += 3;
continue;
}
else
break;
}
buf.clear();
while (*ptr && *ptr != '>' && !isspace((*ptr)&255))
buf += *ptr++;
attrs = ptr;
while (*ptr && *ptr != '>')
ptr ++;
if (*ptr == '>')
ptr ++;
if (buf.cmp("IMG"))
{
Fl_Shared_Image *img;
int width;
int height;
get_attr(attrs, "WIDTH", wattr, sizeof(wattr));
get_attr(attrs, "HEIGHT", hattr, sizeof(hattr));
width = get_length(wattr);
height = get_length(hattr);
if (get_attr(attrs, "SRC", attr, sizeof(attr))) {
// Get and release the image to free it from memory...
img = get_image(attr, width, height);
if ((void*)img != &broken_image) {
img->release();
}
}
}
}
else
ptr++;
}
free((void *)value_);
value_ = 0;
}
blocks_ .clear();
link_list_.clear();
target_line_map_.clear();
}
/**
\brief Find out if the mouse is over a hyperlink and return the link data.
\parm[in] xx, yy Pixel coordinates inside the widget.
\return Shared pointer to the link if found, nullptr otherwise.
*/
std::shared_ptr<Fl_Help_View::Impl::Link> Fl_Help_View::Impl::find_link(int xx, int yy)
{
for (auto &link : link_list_) {
if (link->box.contains(xx, yy)) {
return link;
}
}
return nullptr;
}
/**
\brief Follow a link and load the target document or scroll to the target.
This function clears the current selection and loads a new document or
scrolls to a target line in the current document.
\param linkp Shared pointer to the link to follow.
*/
void Fl_Help_View::Impl::follow_link(std::shared_ptr<Link> linkp)
{
clear_selection();
view.set_changed();
std::string target = linkp->target;; // Current target
if ( (linkp->filename_ != filename_) && !linkp->filename_.empty() ) {
// Load the new document, if the filename is different
std::string url;
size_t directory_scheme_length = url_scheme(directory_);
size_t filename_scheme_length = url_scheme(linkp->filename_);
if ( (directory_scheme_length > 0) && (filename_scheme_length == 0) ) {
// If directory_ starts with a scheme (e.g.ftp:), but linkp->filename_ does not:
if (linkp->filename_[0] == '/') {
// If linkp->filename_ is absolute...
url = directory_.substr(0, directory_scheme_length) + linkp->filename_;;
} else {
// If linkp->filename_ is relative, the URL is the directory_ plus the filename
url = directory_ + "/" + linkp->filename_;
}
} else if (linkp->filename_[0] != '/' && (filename_scheme_length == 0)) {
// If the filename is relative and does not start with a scheme (ftp: , etc.)...
if (!directory_.empty()) {
// If we have a current directory, use that as the base for the URL
url = directory_ + "/" + linkp->filename_;
} else {
// If we do not have a current directory, use the application's current working directory
char dir[FL_PATH_MAX]; // Current directory (static size ok until we have fl_getcwd_std()
fl_getcwd(dir, sizeof(dir));
url = "file:" + std::string(dir) + "/" + linkp->filename_;
}
} else {
// If the filename is absolute or starts with a protocol (e.g.ftp:), use it as is
url = linkp->filename_;
}
// If a target is specified, append it to the URL
if (!linkp->target.empty()) {
url += "#" + linkp->target;
}
load(url.c_str());
} else if (!target.empty()) {
// Keep the same document, scroll to the target line
topline(target.c_str());
} else {
// No target, no filename, just scroll to the top of the document
topline(0);
}
// Scroll the content horizontally to the left
leftline(0);
}
// ---- HTML interpretation and formatting
/**
\brief Adds a text block to the list.
\param[in] s Pointer to start of block text
\param[in] xx X position of block
\param[in] yy Y position of block
\param[in] ww Right margin of block
\param[in] hh Height of block
\param[in] border Draw border?
\return Pointer to the new block in the list.
*/
Fl_Help_View::Impl::Text_Block *Fl_Help_View::Impl::add_block(
const char *s,
int xx, int yy, int ww, int hh,
unsigned char border)
{
Text_Block *temp; // New block
// printf("add_block(s = %p, xx = %d, yy = %d, ww = %d, hh = %d, border = %d)\n",
// s, xx, yy, ww, hh, border);
blocks_.push_back(Text_Block()); // Add a new block to the vector
temp = &blocks_.back();
memset(temp, 0, sizeof(Text_Block));
temp->start = s;
temp->end = s;
temp->x = xx;
temp->y = yy;
temp->w = ww;
temp->h = hh;
temp->border = border;
temp->bgcolor = bgcolor_;
return (temp);
}
/**
\brief Add a new link and its postion on screen to the link list.
\param[in] link a filename, followed by a hash and a target. All parts are optional.
\param[in] xx, yy, ww, hh bounding box of the link text on screen
*/
void Fl_Help_View::Impl::add_link(const std::string &link, int xx, int yy, int ww, int hh)
{
auto new_link = std::make_shared<Link>(); // Create a new link storage object.
new_link->box = { xx, yy, ww, hh };
size_t hash_pos = link.find('#'); // Find the hash character
if (hash_pos != std::string::npos) {
// If a '#' is found, split the link into filename and target
new_link->filename_ = link.substr(0, hash_pos);
new_link->target = link.substr(hash_pos + 1);
} else {
// No '#' found, use the whole link as filename
new_link->filename_ = link;
new_link->target.clear();
}
link_list_.push_back(new_link); // Add the link to the list.
}
/**
\brief Adds a new target to the list.
\param[in] n Name of target (string)
\param[in] yy line number of target position
*/
void Fl_Help_View::Impl::add_target(const std::string &n, int yy)
{
std::string target = to_lower(n); // Convert target name to lower case
target_line_map_[target] = yy; // Store the target line in the map
}
/**
\brief Computes the alignment for a line in a block.
\param[in] block Pointer to the block to add to
\param[in] line Current line number in the block
\param[in] xx Current X position in the block
\param[in] a Current alignment
\param[in,out] l Starting link index for alignment adjustment
\return The new line number after alignment adjustment
*/
int Fl_Help_View::Impl::do_align(
Text_Block *block,
int line,
int xx,
Align a,
int &l)
{
int offset; // Alignment offset
switch (a)
{
case Align::RIGHT: // Right align
offset = block->w - xx;
break;
case Align::CENTER: // Center
offset = (block->w - xx) / 2;
break;
default: // Left align
offset = 0;
break;
}
block->line[line] = block->x + offset;
if (line < 31)
line ++;
while (l < (int)link_list_.size()) {
link_list_[l]->box.x( link_list_[l]->box.x() + offset);
l++;
}
return (line);
}
/**
\brief Formats the help text and lays out the HTML content for display.
This function parses the HTML-like text buffer, breaks it into blocks and lines,
computes positions and sizes for each text and image element, manages links and targets,
and sets up the scrolling and rendering parameters for the widget.
The main algorithm consists of an outer loop that may repeat if the computed content
exceeds the available width (to adjust hsize_), and an inner loop that parses the text,
handles tags, manages formatting state, and builds the layout structures.
*/
void Fl_Help_View::Impl::format() {
int i; // Looping var
int done; // Are we done yet?
Text_Block *block = nullptr, // Current block
*cell; // Current table cell
int cells[MAX_COLUMNS],
// Cells in the current row...
row; // Current table row (block number)
const char *ptr, // Pointer into block
*start, // Pointer to start of element
*attrs; // Pointer to start of element attributes
Edit_Buffer buf; // Text buffer
char attr[1024], // Attribute buffer
wattr[1024], // Width attribute buffer
hattr[1024], // Height attribute buffer
linkdest[1024]; // Link destination
int xx, yy, ww, hh = 0; // Size of current text fragment
int line; // Current line in block
int links; // Links for current line
Fl_Font font;
Fl_Fontsize fsize; // Current font and size
Fl_Color fcolor; // Current font color
unsigned char border; // Draw border?
Align talign; // Current alignment
Align newalign; // New alignment
int head, // In the <HEAD> section?
pre, // <PRE> text?
needspace; // Do we need whitespace?
int table_width, // Width of table
table_offset; // Offset of table
int column, // Current table column number
columns[MAX_COLUMNS];
// Column widths
Fl_Color tc, rc; // Table/row background color
Fl_Boxtype b = view.box() ? view.box() : FL_DOWN_BOX;
// Box to draw...
Margin_Stack margins; // Left margin stack...
std::vector<int> OL_num; // if nonnegative, in OL mode and this is the item number
OL_num.push_back(-1);
DEBUG_FUNCTION(__LINE__,__FUNCTION__);
// Reset document width...
int scrollsize = scrollbar_size_ ? scrollbar_size_ : Fl::scrollbar_size();
hsize_ = view.w() - scrollsize - Fl::box_dw(b);
done = 0;
while (!done)
{
// Reset state variables...
done = 1;
blocks_.clear();
link_list_.clear();
target_line_map_.clear();
size_ = 0;
bgcolor_ = view.color();
textcolor_ = textcolor();
linkcolor_ = fl_contrast(FL_BLUE, view.color());
tc = rc = bgcolor_;
title_ = "Untitled";
if (!value_)
return;
// Setup for formatting...
initfont(font, fsize, fcolor);
line = 0;
links = 0;
margins.clear();
xx = 4;
yy = fsize + 2;
ww = 0;
column = 0;
border = 0;
hh = 0;
block = add_block(value_, xx, yy, hsize_, 0);
row = 0;
head = 0;
pre = 0;
talign = Align::LEFT;
newalign = Align::LEFT;
needspace = 0;
linkdest[0] = '\0';
table_offset = 0;
// Html text character loop
for (ptr = value_, buf.clear(); *ptr;)
{
// End of word?
if ((*ptr == '<' || isspace((*ptr)&255)) && buf.size() > 0)
{
// Get width of word parsed so far...
ww = buf.width();
if (!head && !pre)
{
// Check width...
if (ww > hsize_) {
hsize_ = ww;
done = 0;
break;
}
if (needspace && xx > block->x)
ww += (int)fl_width(' ');
// printf("line = %d, xx = %d, ww = %d, block->x = %d, block->w = %d\n",
// line, xx, ww, block->x, block->w);
if ((xx + ww) > block->w)
{
line = do_align(block, line, xx, newalign, links);
xx = block->x;
yy += hh;
block->h += hh;
hh = 0;
}
if (linkdest[0])
add_link(linkdest, xx, yy - fsize, ww, fsize);
xx += ww;
if ((fsize + 2) > hh)
hh = fsize + 2;
needspace = 0;
}
else if (pre)
{
// Add a link as needed...
if (linkdest[0])
add_link(linkdest, xx, yy - hh, ww, hh);
xx += ww;
if ((fsize + 2) > hh)
hh = fsize + 2;
// Handle preformatted text...
while (isspace((*ptr)&255))
{
if (*ptr == '\n')
{
if (xx > hsize_) break;
line = do_align(block, line, xx, newalign, links);
xx = block->x;
yy += hh;
block->h += hh;
hh = fsize + 2;
}
else
xx += (int)fl_width(' ');
if ((fsize + 2) > hh)
hh = fsize + 2;
ptr ++;
}
if (xx > hsize_) {
hsize_ = xx;
done = 0;
break;
}
needspace = 0;
}
else
{
// Handle normal text or stuff in the <HEAD> section...
while (isspace((*ptr)&255))
ptr ++;
}
buf.clear();
}
if (*ptr == '<')
{
// Handle html tags..
start = ptr;
ptr ++;
if (strncmp(ptr, "!--", 3) == 0)
{
// Comment...
ptr += 3;
if ((ptr = strstr(ptr, "-->")) != nullptr)
{
ptr += 3;
continue;
}
else
break;
}
while (*ptr && *ptr != '>' && !isspace((*ptr)&255))
buf += *ptr++;
attrs = ptr;
while (*ptr && *ptr != '>')
ptr ++;
if (*ptr == '>')
ptr ++;
if (buf.cmp("HEAD"))
head = 1;
else if (buf.cmp("/HEAD"))
head = 0;
else if (buf.cmp("TITLE"))
{
// Copy the title in the document...
title_.clear();
for ( ; *ptr != '<' && *ptr; ptr++) {
title_.push_back(*ptr);
}
buf.clear();
}
else if (buf.cmp("A"))
{
if (get_attr(attrs, "NAME", attr, sizeof(attr)) != nullptr)
add_target(attr, yy - fsize - 2);
if (get_attr(attrs, "HREF", attr, sizeof(attr)) != nullptr)
strlcpy(linkdest, attr, sizeof(linkdest));
}
else if (buf.cmp("/A"))
linkdest[0] = '\0';
else if (buf.cmp("BODY"))
{
bgcolor_ = get_color(get_attr(attrs, "BGCOLOR", attr, sizeof(attr)),
view.color());
textcolor_ = get_color(get_attr(attrs, "TEXT", attr, sizeof(attr)),
textcolor());
linkcolor_ = get_color(get_attr(attrs, "LINK", attr, sizeof(attr)),
fl_contrast(FL_BLUE, view.color()));
}
else if (buf.cmp("BR"))
{
line = do_align(block, line, xx, newalign, links);
xx = block->x;
block->h += hh;
yy += hh;
hh = 0;
}
else if (buf.cmp("CENTER") ||
buf.cmp("P") ||
buf.cmp("H1") ||
buf.cmp("H2") ||
buf.cmp("H3") ||
buf.cmp("H4") ||
buf.cmp("H5") ||
buf.cmp("H6") ||
buf.cmp("UL") ||
buf.cmp("OL") ||
buf.cmp("DL") ||
buf.cmp("LI") ||
buf.cmp("DD") ||
buf.cmp("DT") ||
buf.cmp("HR") ||
buf.cmp("PRE") ||
buf.cmp("TABLE"))
{
block->end = start;
line = do_align(block, line, xx, newalign, links);
newalign = buf.cmp("CENTER") ? Align::CENTER : Align::LEFT;
xx = block->x;
block->h += hh;
if (buf.cmp("OL")) {
int ol_num = 1;
if (get_attr(attrs, "START", attr, sizeof(attr)) != nullptr) {
errno = 0;
char *endptr = 0;
ol_num = (int)strtol(attr, &endptr, 10);
if (errno || endptr == attr || ol_num < 0)
ol_num = 1;
}
OL_num.push_back(ol_num);
}
else if (buf.cmp("UL"))
OL_num.push_back(-1);
if (buf.cmp("UL") ||
buf.cmp("OL") ||
buf.cmp("DL"))
{
block->h += fsize + 2;
xx = margins.push(4 * fsize);
}
else if (buf.cmp("TABLE"))
{
if (get_attr(attrs, "BORDER", attr, sizeof(attr)))
border = (uchar)atoi(attr);
else
border = 0;
tc = rc = get_color(get_attr(attrs, "BGCOLOR", attr, sizeof(attr)), bgcolor_);
block->h += fsize + 2;
format_table(&table_width, columns, start);
if ((xx + table_width) > hsize_) {
#ifdef DEBUG
printf("xx=%d, table_width=%d, hsize_=%d\n", xx, table_width,
hsize_);
#endif // DEBUG
hsize_ = xx + table_width;
done = 0;
break;
}
switch (get_align(attrs, talign))
{
default :
table_offset = 0;
break;
case Align::CENTER :
table_offset = (hsize_ - table_width) / 2 - textsize_;
break;
case Align::RIGHT :
table_offset = hsize_ - table_width - textsize_;
break;
}
column = 0;
}
if (tolower(buf[0]) == 'h' && isdigit(buf[1]))
{
font = FL_HELVETICA_BOLD;
fsize = textsize_ + '7' - buf[1];
}
else if (buf.cmp("DT"))
{
font = textfont_ | FL_ITALIC;
fsize = textsize_;
}
else if (buf.cmp("PRE"))
{
font = FL_COURIER;
fsize = textsize_;
pre = 1;
}
else
{
font = textfont_;
fsize = textsize_;
}
pushfont(font, fsize);
yy = block->y + block->h;
hh = 0;
if ((tolower(buf[0]) == 'h' && isdigit(buf[1])) ||
buf.cmp("DD") ||
buf.cmp("DT") ||
buf.cmp("P"))
yy += fsize + 2;
else if (buf.cmp("HR"))
{
hh += 2 * fsize;
yy += fsize;
}
if (row)
block = add_block(start, xx, yy, block->w, 0);
else
block = add_block(start, xx, yy, hsize_, 0);
if (buf.cmp("LI")) {
block->ol = 0;
if (OL_num.size() && (OL_num.back() >= 0)) {
block->ol = 1;
block->ol_num = OL_num.back();
OL_num.back()++;
}
}
needspace = 0;
line = 0;
if (buf.cmp("CENTER"))
newalign = talign = Align::CENTER;
else
newalign = get_align(attrs, talign);
}
else if (buf.cmp("/CENTER") ||
buf.cmp("/P") ||
buf.cmp("/H1") ||
buf.cmp("/H2") ||
buf.cmp("/H3") ||
buf.cmp("/H4") ||
buf.cmp("/H5") ||
buf.cmp("/H6") ||
buf.cmp("/PRE") ||
buf.cmp("/UL") ||
buf.cmp("/OL") ||
buf.cmp("/DL") ||
buf.cmp("/TABLE"))
{
line = do_align(block, line, xx, newalign, links);
xx = block->x;
block->end = ptr;
if (buf.cmp("/OL") ||
buf.cmp("/UL")) {
if (OL_num.size()) OL_num.pop_back();
}
if (buf.cmp("/UL") ||
buf.cmp("/OL") ||
buf.cmp("/DL"))
{
xx = margins.pop();
block->h += fsize + 2;
}
else if (buf.cmp("/TABLE"))
{
block->h += fsize + 2;
xx = margins.current();
}
else if (buf.cmp("/PRE"))
{
pre = 0;
hh = 0;
}
else if (buf.cmp("/CENTER"))
talign = Align::LEFT;
popfont(font, fsize, fcolor);
//#if defined(__GNUC__)
//#warning FIXME this isspace & 255 test will probably not work on a utf8 stream... And we use it everywhere!
//#endif /*__GNUC__*/
while (isspace((*ptr)&255))
ptr ++;
block->h += hh;
yy += hh;
if (tolower(buf[2]) == 'l')
yy += fsize + 2;
if (row)
block = add_block(ptr, xx, yy, block->w, 0);
else
block = add_block(ptr, xx, yy, hsize_, 0);
needspace = 0;
hh = 0;
line = 0;
newalign = talign;
}
else if (buf.cmp("TR"))
{
block->end = start;
line = do_align(block, line, xx, newalign, links);
xx = block->x;
block->h += hh;
if (row)
{
yy = blocks_[row].y + blocks_[row].h;
for (cell = &blocks_[row + 1]; cell <= block; cell ++)
if ((cell->y + cell->h) > yy)
yy = cell->y + cell->h;
block = &blocks_[row];
block->h = yy - block->y + 2;
for (i = 0; i < column; i ++)
if (cells[i])
{
cell = &blocks_[cells[i]];
cell->h = block->h;
}
}
memset(cells, 0, sizeof(cells));
yy = block->y + block->h - 4;
hh = 0;
block = add_block(start, xx, yy, hsize_, 0);
row = (int) (block - &blocks_[0]);
needspace = 0;
column = 0;
line = 0;
rc = get_color(get_attr(attrs, "BGCOLOR", attr, sizeof(attr)), tc);
}
else if (buf.cmp("/TR") && row)
{
line = do_align(block, line, xx, newalign, links);
block->end = start;
block->h += hh;
talign = Align::LEFT;
xx = blocks_[row].x;
yy = blocks_[row].y + blocks_[row].h;
for (cell = &blocks_[row + 1]; cell <= block; cell ++)
if ((cell->y + cell->h) > yy)
yy = cell->y + cell->h;
block = &blocks_[row];
block->h = yy - block->y + 2;
for (i = 0; i < column; i ++)
if (cells[i])
{
cell = &blocks_[cells[i]];
cell->h = block->h;
}
yy = block->y + block->h /*- 4*/;
block = add_block(start, xx, yy, hsize_, 0);
needspace = 0;
row = 0;
line = 0;
}
else if ((buf.cmp("TD") ||
buf.cmp("TH")) && row)
{
int colspan; // COLSPAN attribute
line = do_align(block, line, xx, newalign, links);
block->end = start;
block->h += hh;
if (buf.cmp("TH"))
font = textfont_ | FL_BOLD;
else
font = textfont_;
fsize = textsize_;
xx = blocks_[row].x + fsize + 3 + table_offset;
for (i = 0; i < column; i ++)
xx += columns[i] + 6;
margins.push(xx - margins.current());
if (get_attr(attrs, "COLSPAN", attr, sizeof(attr)) != nullptr)
colspan = atoi(attr);
else
colspan = 1;
for (i = 0, ww = -6; i < colspan; i ++)
ww += columns[column + i] + 6;
if (block->end == block->start && blocks_.size() > 1)
{
blocks_.pop_back();
block --;
}
pushfont(font, fsize);
yy = blocks_[row].y;
hh = 0;
block = add_block(start, xx, yy, xx + ww, 0, border);
needspace = 0;
line = 0;
newalign = get_align(attrs, tolower(buf[1]) == 'h' ? Align::CENTER : Align::LEFT);
talign = newalign;
cells[column] = (int) (block - &blocks_[0]);
column += colspan;
block->bgcolor = get_color(get_attr(attrs, "BGCOLOR", attr,
sizeof(attr)), rc);
}
else if ((buf.cmp("/TD") ||
buf.cmp("/TH")) && row)
{
line = do_align(block, line, xx, newalign, links);
popfont(font, fsize, fcolor);
xx = margins.pop();
talign = Align::LEFT;
}
else if (buf.cmp("FONT"))
{
if (get_attr(attrs, "FACE", attr, sizeof(attr)) != nullptr) {
if (!strncasecmp(attr, "helvetica", 9) ||
!strncasecmp(attr, "arial", 5) ||
!strncasecmp(attr, "sans", 4)) font = FL_HELVETICA;
else if (!strncasecmp(attr, "times", 5) ||
!strncasecmp(attr, "serif", 5)) font = FL_TIMES;
else if (!strncasecmp(attr, "symbol", 6)) font = FL_SYMBOL;
else font = FL_COURIER;
}
if (get_attr(attrs, "SIZE", attr, sizeof(attr)) != nullptr) {
if (isdigit(attr[0] & 255)) {
// Absolute size
fsize = (int)(textsize_ * pow(1.2, atoi(attr) - 3.0));
} else {
// Relative size
fsize = (int)(fsize * pow(1.2, atoi(attr)));
}
}
pushfont(font, fsize);
}
else if (buf.cmp("/FONT"))
popfont(font, fsize, fcolor);
else if (buf.cmp("B") ||
buf.cmp("STRONG"))
pushfont(font |= FL_BOLD, fsize);
else if (buf.cmp("I") ||
buf.cmp("EM"))
pushfont(font |= FL_ITALIC, fsize);
else if (buf.cmp("CODE") ||
buf.cmp("TT"))
pushfont(font = FL_COURIER, fsize);
else if (buf.cmp("KBD"))
pushfont(font = FL_COURIER_BOLD, fsize);
else if (buf.cmp("VAR"))
pushfont(font = FL_COURIER_ITALIC, fsize);
else if (buf.cmp("/B") ||
buf.cmp("/STRONG") ||
buf.cmp("/I") ||
buf.cmp("/EM") ||
buf.cmp("/CODE") ||
buf.cmp("/TT") ||
buf.cmp("/KBD") ||
buf.cmp("/VAR"))
popfont(font, fsize, fcolor);
else if (buf.cmp("IMG"))
{
Fl_Shared_Image *img = 0;
int width;
int height;
get_attr(attrs, "WIDTH", wattr, sizeof(wattr));
get_attr(attrs, "HEIGHT", hattr, sizeof(hattr));
width = get_length(wattr);
height = get_length(hattr);
if (get_attr(attrs, "SRC", attr, sizeof(attr))) {
img = get_image(attr, width, height);
width = img->w();
height = img->h();
}
ww = width;
if (ww > hsize_) {
hsize_ = ww;
done = 0;
break;
}
if (needspace && xx > block->x)
ww += (int)fl_width(' ');
if ((xx + ww) > block->w)
{
line = do_align(block, line, xx, newalign, links);
xx = block->x;
yy += hh;
block->h += hh;
hh = 0;
}
if (linkdest[0])
add_link(linkdest, xx, yy-fsize, ww, height);
xx += ww;
if ((height + 2) > hh)
hh = height + 2;
needspace = 0;
}
buf.clear();
}
else if (*ptr == '\n' && pre)
{
if (linkdest[0])
add_link(linkdest, xx, yy - hh, ww, hh);
if (xx > hsize_) {
hsize_ = xx;
done = 0;
break;
}
line = do_align(block, line, xx, newalign, links);
xx = block->x;
yy += hh;
block->h += hh;
needspace = 0;
ptr ++;
}
else if (isspace((*ptr)&255))
{
needspace = 1;
if ( pre ) {
xx += (int)fl_width(' ');
}
ptr ++;
}
else if (*ptr == '&')
{
// Handle html '&' codes, eg. "&amp;"
ptr ++;
int qch = quote_char(ptr);
if (qch < 0)
buf += '&';
else {
buf.add(qch);
ptr = strchr(ptr, ';') + 1;
}
if ((fsize + 2) > hh)
hh = fsize + 2;
}
else
{
buf += *ptr++;
if ((fsize + 2) > hh)
hh = fsize + 2;
}
}
if (buf.size() > 0 && !head)
{
ww = buf.width();
// printf("line = %d, xx = %d, ww = %d, block->x = %d, block->w = %d\n",
// line, xx, ww, block->x, block->w);
if (ww > hsize_) {
hsize_ = ww;
done = 0;
break;
}
if (needspace && xx > block->x)
ww += (int)fl_width(' ');
if ((xx + ww) > block->w)
{
line = do_align(block, line, xx, newalign, links);
xx = block->x;
yy += hh;
block->h += hh;
hh = 0;
}
if (linkdest[0])
add_link(linkdest, xx, yy - fsize, ww, fsize);
xx += ww;
}
do_align(block, line, xx, newalign, links);
block->end = ptr;
size_ = yy + hh;
}
// Make sure that the last block will have the correct height.
if (hh > block->h) block->h = hh;
// printf("margins.depth_=%d\n", margins.depth_);
int dx = Fl::box_dw(b) - Fl::box_dx(b);
int dy = Fl::box_dh(b) - Fl::box_dy(b);
int ss = scrollbar_size_ ? scrollbar_size_ : Fl::scrollbar_size();
int dw = Fl::box_dw(b) + ss;
int dh = Fl::box_dh(b);
if (hsize_ > (view.w() - dw)) {
view.hscrollbar_.show();
dh += ss;
if (size_ < (view.h() - dh)) {
view.scrollbar_.hide();
view.hscrollbar_.resize(view.x() + Fl::box_dx(b), view.y() + view.h() - ss - dy,
view.w() - Fl::box_dw(b), ss);
} else {
view.scrollbar_.show();
view.scrollbar_.resize(view.x() +view. w() - ss - dx, view.y() + Fl::box_dy(b),
ss, view.h() - ss - Fl::box_dh(b));
view.hscrollbar_.resize(view.x() + Fl::box_dx(b), view.y() + view.h() - ss - dy,
view.w() - ss - Fl::box_dw(b), ss);
}
} else {
view.hscrollbar_.hide();
if (size_ < (view.h() - dh)) view.scrollbar_.hide();
else {
view.scrollbar_.resize(view.x() + view.w() - ss - dx, view.y() + Fl::box_dy(b),
ss, view.h() - Fl::box_dh(b));
view.scrollbar_.show();
}
}
// Reset scrolling if it needs to be...
if (view.scrollbar_.visible()) {
int temph = view.h() - Fl::box_dh(b);
if (view.hscrollbar_.visible()) temph -= ss;
if ((topline_ + temph) > size_) topline(size_ - temph);
else topline(topline_);
} else topline(0);
if (view.hscrollbar_.visible()) {
int tempw = view.w() - ss - Fl::box_dw(b);
if ((leftline_ + tempw) > hsize_) leftline(hsize_ - tempw);
else leftline(leftline_);
} else leftline(0);
}
/**
\brief Format a table
\param[out] table_width Total width of the table
\param[out] columns Array of column widths
\param[in] table Pointer to the start of the table in the HTML text
*/
void Fl_Help_View::Impl::format_table(
int *table_width,
int *columns,
const char *table)
{
int column, // Current column
num_columns, // Number of columns
colspan, // COLSPAN attribute
width, // Current width
temp_width, // Temporary width
max_width, // Maximum width
incell, // In a table cell?
pre, // <PRE> text?
needspace; // Need whitespace?
Edit_Buffer buf; // Text buffer
char attr[1024], // Other attribute
wattr[1024], // WIDTH attribute
hattr[1024]; // HEIGHT attribute
const char *ptr, // Pointer into table
*attrs, // Pointer to attributes
*start; // Start of element
int minwidths[MAX_COLUMNS]; // Minimum widths for each column
Fl_Font font;
Fl_Fontsize fsize; // Current font and size
Fl_Color fcolor; // Currrent font color
DEBUG_FUNCTION(__LINE__,__FUNCTION__);
// Clear widths...
*table_width = 0;
for (column = 0; column < MAX_COLUMNS; column ++)
{
columns[column] = 0;
minwidths[column] = 0;
}
num_columns = 0;
colspan = 0;
max_width = 0;
pre = 0;
needspace = 0;
fstack_.top(font, fsize, fcolor);
// Scan the table...
for (ptr = table, column = -1, width = 0, incell = 0; *ptr;)
{
if ((*ptr == '<' || isspace((*ptr)&255)) && buf.size() > 0 && incell)
{
// Check width...
if (needspace)
{
buf += ' ';
needspace = 0;
}
temp_width = buf.width();
buf.clear();
if (temp_width > minwidths[column])
minwidths[column] = temp_width;
width += temp_width;
if (width > max_width)
max_width = width;
}
if (*ptr == '<')
{
start = ptr;
for (buf.clear(), ptr ++; *ptr && *ptr != '>' && !isspace((*ptr)&255);)
buf += *ptr++;
attrs = ptr;
while (*ptr && *ptr != '>')
ptr ++;
if (*ptr == '>')
ptr ++;
if (buf.cmp("BR") ||
buf.cmp("HR"))
{
width = 0;
needspace = 0;
}
else if (buf.cmp("TABLE") && start > table)
break;
else if (buf.cmp("CENTER") ||
buf.cmp("P") ||
buf.cmp("H1") ||
buf.cmp("H2") ||
buf.cmp("H3") ||
buf.cmp("H4") ||
buf.cmp("H5") ||
buf.cmp("H6") ||
buf.cmp("UL") ||
buf.cmp("OL") ||
buf.cmp("DL") ||
buf.cmp("LI") ||
buf.cmp("DD") ||
buf.cmp("DT") ||
buf.cmp("PRE"))
{
width = 0;
needspace = 0;
if (tolower(buf[0]) == 'h' && isdigit(buf[1]))
{
font = FL_HELVETICA_BOLD;
fsize = textsize_ + '7' - buf[1];
}
else if (buf.cmp("DT"))
{
font = textfont_ | FL_ITALIC;
fsize = textsize_;
}
else if (buf.cmp("PRE"))
{
font = FL_COURIER;
fsize = textsize_;
pre = 1;
}
else if (buf.cmp("LI"))
{
width += 4 * fsize;
font = textfont_;
fsize = textsize_;
}
else
{
font = textfont_;
fsize = textsize_;
}
pushfont(font, fsize);
}
else if (buf.cmp("/CENTER") ||
buf.cmp("/P") ||
buf.cmp("/H1") ||
buf.cmp("/H2") ||
buf.cmp("/H3") ||
buf.cmp("/H4") ||
buf.cmp("/H5") ||
buf.cmp("/H6") ||
buf.cmp("/PRE") ||
buf.cmp("/UL") ||
buf.cmp("/OL") ||
buf.cmp("/DL"))
{
width = 0;
needspace = 0;
popfont(font, fsize, fcolor);
}
else if (buf.cmp("TR") || buf.cmp("/TR") ||
buf.cmp("/TABLE"))
{
// printf("%s column = %d, colspan = %d, num_columns = %d\n",
// buf.c_str(), column, colspan, num_columns);
if (column >= 0)
{
// This is a hack to support COLSPAN...
max_width /= colspan;
while (colspan > 0)
{
if (max_width > columns[column])
columns[column] = max_width;
column ++;
colspan --;
}
}
if (buf.cmp("/TABLE"))
break;
needspace = 0;
column = -1;
width = 0;
max_width = 0;
incell = 0;
}
else if (buf.cmp("TD") ||
buf.cmp("TH"))
{
// printf("BEFORE column = %d, colspan = %d, num_columns = %d\n",
// column, colspan, num_columns);
if (column >= 0)
{
// This is a hack to support COLSPAN...
max_width /= colspan;
while (colspan > 0)
{
if (max_width > columns[column])
columns[column] = max_width;
column ++;
colspan --;
}
}
else
column ++;
if (get_attr(attrs, "COLSPAN", attr, sizeof(attr)) != nullptr)
colspan = atoi(attr);
else
colspan = 1;
// printf("AFTER column = %d, colspan = %d, num_columns = %d\n",
// column, colspan, num_columns);
if ((column + colspan) >= num_columns)
num_columns = column + colspan;
needspace = 0;
width = 0;
incell = 1;
if (buf.cmp("TH"))
font = textfont_ | FL_BOLD;
else
font = textfont_;
fsize = textsize_;
pushfont(font, fsize);
if (get_attr(attrs, "WIDTH", attr, sizeof(attr)) != nullptr)
max_width = get_length(attr);
else
max_width = 0;
// printf("max_width = %d\n", max_width);
}
else if (buf.cmp("/TD") ||
buf.cmp("/TH"))
{
incell = 0;
popfont(font, fsize, fcolor);
}
else if (buf.cmp("B") ||
buf.cmp("STRONG"))
pushfont(font |= FL_BOLD, fsize);
else if (buf.cmp("I") ||
buf.cmp("EM"))
pushfont(font |= FL_ITALIC, fsize);
else if (buf.cmp("CODE") ||
buf.cmp("TT"))
pushfont(font = FL_COURIER, fsize);
else if (buf.cmp("KBD"))
pushfont(font = FL_COURIER_BOLD, fsize);
else if (buf.cmp("VAR"))
pushfont(font = FL_COURIER_ITALIC, fsize);
else if (buf.cmp("/B") ||
buf.cmp("/STRONG") ||
buf.cmp("/I") ||
buf.cmp("/EM") ||
buf.cmp("/CODE") ||
buf.cmp("/TT") ||
buf.cmp("/KBD") ||
buf.cmp("/VAR"))
popfont(font, fsize, fcolor);
else if (buf.cmp("IMG") && incell)
{
Fl_Shared_Image *img = 0;
int iwidth, iheight;
get_attr(attrs, "WIDTH", wattr, sizeof(wattr));
get_attr(attrs, "HEIGHT", hattr, sizeof(hattr));
iwidth = get_length(wattr);
iheight = get_length(hattr);
if (get_attr(attrs, "SRC", attr, sizeof(attr))) {
img = get_image(attr, iwidth, iheight);
iwidth = img->w();
iheight = img->h();
}
if (iwidth > minwidths[column])
minwidths[column] = iwidth;
width += iwidth;
if (needspace)
width += (int)fl_width(' ');
if (width > max_width)
max_width = width;
needspace = 0;
}
buf.clear();
}
else if (*ptr == '\n' && pre)
{
width = 0;
needspace = 0;
ptr ++;
}
else if (isspace((*ptr)&255))
{
needspace = 1;
ptr ++;
}
else if (*ptr == '&' )
{
ptr ++;
int qch = quote_char(ptr);
if (qch < 0)
buf += '&';
else {
buf.add(qch);
ptr = strchr(ptr, ';') + 1;
}
}
else
{
buf += *ptr++;
}
}
// Now that we have scanned the entire table, adjust the table and
// cell widths to fit on the screen...
if (get_attr(table + 6, "WIDTH", attr, sizeof(attr)))
*table_width = get_length(attr);
else
*table_width = 0;
#ifdef DEBUG
printf("num_columns = %d, table_width = %d\n", num_columns, *table_width);
#endif // DEBUG
if (num_columns == 0)
return;
// Add up the widths...
for (column = 0, width = 0; column < num_columns; column ++)
width += columns[column];
#ifdef DEBUG
printf("width = %d, w() = %d\n", width, w());
for (column = 0; column < num_columns; column ++)
printf(" columns[%d] = %d, minwidths[%d] = %d\n", column, columns[column],
column, minwidths[column]);
#endif // DEBUG
// Adjust the width if needed...
int scale_width = *table_width;
int scrollsize = scrollbar_size_ ? scrollbar_size_ : Fl::scrollbar_size();
if (scale_width == 0) {
if (width > (hsize_ - scrollsize)) scale_width = hsize_ - scrollsize;
else scale_width = width;
}
if (width < scale_width) {
#ifdef DEBUG
printf("Scaling table up to %d from %d...\n", scale_width, width);
#endif // DEBUG
*table_width = 0;
scale_width = (scale_width - width) / num_columns;
#ifdef DEBUG
printf("adjusted scale_width = %d\n", scale_width);
#endif // DEBUG
for (column = 0; column < num_columns; column ++) {
columns[column] += scale_width;
(*table_width) += columns[column];
}
}
else if (width > scale_width) {
#ifdef DEBUG
printf("Scaling table down to %d from %d...\n", scale_width, width);
#endif // DEBUG
for (column = 0; column < num_columns; column ++) {
width -= minwidths[column];
scale_width -= minwidths[column];
}
#ifdef DEBUG
printf("adjusted width = %d, scale_width = %d\n", width, scale_width);
#endif // DEBUG
if (width > 0) {
for (column = 0; column < num_columns; column ++) {
columns[column] -= minwidths[column];
columns[column] = scale_width * columns[column] / width;
columns[column] += minwidths[column];
}
}
*table_width = 0;
for (column = 0; column < num_columns; column ++) {
(*table_width) += columns[column];
}
}
else if (*table_width == 0)
*table_width = width;
#ifdef DEBUG
printf("FINAL table_width = %d\n", *table_width);
for (column = 0; column < num_columns; column ++)
printf(" columns[%d] = %d\n", column, columns[column]);
#endif // DEBUG
}
/**
\brief Gets an alignment attribute.
\param[in] p Pointer to start of attributes.
\param[in] a Default alignment.
\return Alignment value, either CENTER, RIGHT, or LEFT.
*/
Fl_Help_View::Impl::Align Fl_Help_View::Impl::get_align(const char *p, Align a)
{
char buf[255]; // Alignment value
if (get_attr(p, "ALIGN", buf, sizeof(buf)) == nullptr)
return (a);
if (strcasecmp(buf, "CENTER") == 0)
return Align::CENTER;
else if (strcasecmp(buf, "RIGHT") == 0)
return Align::RIGHT;
else
return Align::LEFT;
}
/**
\brief Gets an attribute value from the string.
\param[in] p Pointer to start of attributes.
\param[in] n Name of attribute.
\param[out] buf Buffer for attribute value.
\param[in] bufsize Size of buffer.
\return Pointer to buf or nullptr if not found.
*/
const char *Fl_Help_View::Impl::get_attr(
const char *p,
const char *n,
char *buf,
int bufsize)
{
char name[255], // Name from string
*ptr, // Pointer into name or value
quote; // Quote
buf[0] = '\0';
while (*p && *p != '>')
{
while (isspace((*p)&255))
p ++;
if (*p == '>' || !*p)
return (nullptr);
for (ptr = name; *p && !isspace((*p)&255) && *p != '=' && *p != '>';)
if (ptr < (name + sizeof(name) - 1))
*ptr++ = *p++;
else
p ++;
*ptr = '\0';
if (isspace((*p)&255) || !*p || *p == '>')
buf[0] = '\0';
else
{
if (*p == '=')
p ++;
for (ptr = buf; *p && !isspace((*p)&255) && *p != '>';)
if (*p == '\'' || *p == '\"')
{
quote = *p++;
while (*p && *p != quote)
if ((ptr - buf + 1) < bufsize)
*ptr++ = *p++;
else
p ++;
if (*p == quote)
p ++;
}
else if ((ptr - buf + 1) < bufsize)
*ptr++ = *p++;
else
p ++;
*ptr = '\0';
}
if (strcasecmp(n, name) == 0)
return (buf);
else
buf[0] = '\0';
if (*p == '>')
return (nullptr);
}
return (nullptr);
}
/**
\brief Gets a color attribute.
\param[in] n the color name, either a name or a hex value.
\param[in] c the default color value.
\return the color value, either the color from the name or the default value.
*/
Fl_Color Fl_Help_View::Impl::get_color(const char *n, Fl_Color c)
{
int i; // Looping var
int rgb, r, g, b; // RGB values
static const struct { // Color name table
const char *name;
int r, g, b;
} colors[] = {
{ "black", 0x00, 0x00, 0x00 },
{ "red", 0xff, 0x00, 0x00 },
{ "green", 0x00, 0x80, 0x00 },
{ "yellow", 0xff, 0xff, 0x00 },
{ "blue", 0x00, 0x00, 0xff },
{ "magenta", 0xff, 0x00, 0xff },
{ "fuchsia", 0xff, 0x00, 0xff },
{ "cyan", 0x00, 0xff, 0xff },
{ "aqua", 0x00, 0xff, 0xff },
{ "white", 0xff, 0xff, 0xff },
{ "gray", 0x80, 0x80, 0x80 },
{ "grey", 0x80, 0x80, 0x80 },
{ "lime", 0x00, 0xff, 0x00 },
{ "maroon", 0x80, 0x00, 0x00 },
{ "navy", 0x00, 0x00, 0x80 },
{ "olive", 0x80, 0x80, 0x00 },
{ "purple", 0x80, 0x00, 0x80 },
{ "silver", 0xc0, 0xc0, 0xc0 },
{ "teal", 0x00, 0x80, 0x80 }
};
if (!n || !n[0]) return c;
if (n[0] == '#') {
// Do hex color lookup
rgb = (int)strtol(n + 1, nullptr, 16);
if (strlen(n) > 4) {
r = rgb >> 16;
g = (rgb >> 8) & 255;
b = rgb & 255;
} else {
r = (rgb >> 8) * 17;
g = ((rgb >> 4) & 15) * 17;
b = (rgb & 15) * 17;
}
return (fl_rgb_color((uchar)r, (uchar)g, (uchar)b));
} else {
for (i = 0; i < (int)(sizeof(colors) / sizeof(colors[0])); i ++)
if (!strcasecmp(n, colors[i].name)) {
return fl_rgb_color(colors[i].r, colors[i].g, colors[i].b);
}
return c;
}
}
/* Implementation note: (A.S. Apr 05, 2009)
Fl_Help_View::Impl::get_image() uses a static global flag (initial_load)
to determine, if it is called from the initial loading of a document
(load() or value()), or from resize() or draw().
A better solution would be to manage all loaded images in an own
structure like Fl_Help_Target (Fl_Help_Image ?) to avoid using this
global flag, but this would break the ABI !
This should be fixed in FLTK 1.3 !
If initial_load is true, then Fl_Shared_Image::get() is called to
load the image, and the reference count of the shared image is
increased by one.
If initial_load is false, then Fl_Shared_Image::find() is called to
load the image, and the image is released immediately. This avoids
increasing the reference count when calling get_image() from draw()
or resize().
Calling Fl_Shared_Image::find() instead of Fl_Shared_Image::get() avoids
doing unnecessary i/o for "broken images" within each resize/redraw.
Each image must be released exactly once in the destructor or before
a new document is loaded: see free_data().
*/
/**
\brief Gets an inline image.
The image reference count is maintained accordingly, such that
the image can be released exactly once when the document is closed.
\param[in] name the image name, either a local filename or a URL.
\param[in] W, H the size of the image, or 0 if not specified.
\return a pointer to a cached Fl_Shared_Image, if the image can be loaded,
otherwise a pointer to an internal Fl_Pixmap (broken_image).
\todo Fl_Help_View::Impl::get_image() returns a pointer to the internal
Fl_Pixmap broken_image, but this is _not_ compatible with the
return type Fl_Shared_Image (release() must not be called).
*/
Fl_Shared_Image *Fl_Help_View::Impl::get_image(const char *name, int W, int H)
{
std::string url;
Fl_Shared_Image *ip; // Image pointer...
if (!name || !name[0]) {
// No image name given, return broken image
return (Fl_Shared_Image *)&broken_image;
}
std::string imagename = name;
size_t directory_scheme_length = url_scheme(directory_);
size_t imagename_scheme_length = url_scheme(imagename);
// See if the image can be found...
if ( (directory_scheme_length > 0) && (imagename_scheme_length == 0) ) {
// If directory_ starts with a scheme (e.g.ftp:), but linkp->filename_ does not:
if (imagename[0] == '/') {
// If linkp->filename_ is absolute...
url = directory_.substr(0, directory_scheme_length) + imagename;;
} else {
// If linkp->filename_ is relative, the URL is the directory_ plus the filename
url = directory_ + "/" + imagename;
}
} else if (imagename[0] != '/' && (imagename_scheme_length == 0)) {
// If the filename is relative and does not start with a scheme (ftp: , etc.)...
if (!directory_.empty()) {
// If we have a current directory, use that as the base for the URL
url = directory_ + "/" + imagename;
} else {
// If we do not have a current directory, use the application's current working directory
char dir[FL_PATH_MAX]; // Current directory (static size ok until we have fl_getcwd_std()
fl_getcwd(dir, sizeof(dir));
url = "file:" + std::string(dir) + "/" + imagename;
}
} else {
// If the filename is absolute or starts with a protocol (e.g.ftp:), use it as is
url = imagename;
}
if (link_) {
const char *n = (*link_)(&view, url.c_str());
if (n == nullptr)
return 0;
url = n;
}
if (url.empty()) return 0;
// If the URL starts with "file:", remove it
if (url.find("file:") == 0) {
url = url.substr(5);
}
if (initial_load) {
if ((ip = Fl_Shared_Image::get(url.c_str(), W, H)) == nullptr) {
ip = (Fl_Shared_Image *)&broken_image;
}
} else { // draw or resize
if ((ip = Fl_Shared_Image::find(url.c_str(), W, H)) == nullptr) {
ip = (Fl_Shared_Image *)&broken_image;
} else {
ip->release();
}
}
return ip;
}
/**
\brief Gets a length value, either absolute or %.
\param[in] l string containing the length value
\return the length in pixels, or 0 if the string is empty.
*/
int Fl_Help_View::Impl::get_length(const char *l) {
int val;
if (!l[0]) return 0;
val = atoi(l);
if (l[strlen(l) - 1] == '%') {
if (val > 100) val = 100;
else if (val < 0) val = 0;
int scrollsize = scrollbar_size_ ? scrollbar_size_ : Fl::scrollbar_size();
val = val * (hsize_ - scrollsize) / 100;
}
return val;
}
// ---- Text selection
/*
About selecting text:
Still to do:
- &word; style characters mess up our count inside a word boundary
- we can only select words, no individual characters
- no dragging of the selection into another widget
- we are using the draw() function to measure screen postion of text
by rerouting the code via draw_mode_. Some drawing functions are
still called which is slow and requires a fake graphics context.
It may help to get rid of those calls if not in DRAW mode.
matt.
*/
/**
\brief Draws a text string in the help view.
This function draws the text string \p t at position (\p x, \p y) in the help view.
If the text is selected, it draws a selection rectangle around it and changes the text color.
\param[in] t Text to draw
\param[in] x X position to draw at
\param[in] y Y position to draw at
\param[in] entity_extra_length (unclear)
*/
void Fl_Help_View::Impl::hv_draw(const char *t, int x, int y, int entity_extra_length)
{
if (draw_mode_ == Mode::DRAW) {
if (selected_ && current_pos_<selection_last_ && current_pos_>=selection_first_) {
Fl_Color c = fl_color();
fl_color(tmp_selection_color_);
int w = (int)fl_width(t);
if (current_pos_+(int)strlen(t)<selection_last_)
w += (int)fl_width(' ');
fl_rectf(x, y+fl_descent()-fl_height(), w, fl_height());
fl_color(selection_text_color_);
fl_draw(t, x, y);
fl_color(c);
} else {
fl_draw(t, x, y);
}
} else {
// If draw_mode_ is not DRAW, we don't actually draw anything, but instead
// measure where text blocks are on screen during a mouse selection process.
int w = (int)fl_width(t);
if ( (Fl::event_x() >= x) && (Fl::event_x() < x+w) ) {
if ( (Fl::event_y() >= y-fl_height()+fl_descent()) && (Fl::event_y() <= y+fl_descent()) ) {
int f = (int) current_pos_;
int l = (int) (f+strlen(t)); // use 'quote_char' to calculate the true length of the HTML string
if (draw_mode_ == Mode::PUSH) {
selection_push_first_ = selection_drag_first_ = f;
selection_push_last_ = selection_drag_last_ = l;
} else { // Mode::DRAG
selection_drag_first_ = f;
selection_drag_last_ = l + entity_extra_length;
}
}
}
}
}
/**
\brief Called from `handle()>FL_PUSH`, starts new text selection process.
This method return 1 if the user clicks on selectable text. It sets
selection_push_first_ and selection_push_last_ to the current
selection start and end positions, respectively.
\return 1 if the selection was started, 0 if not.
*/
char Fl_Help_View::Impl::begin_selection()
{
clear_selection();
selection_push_first_ = selection_push_last_ = 0;
selection_drag_first_ = selection_drag_last_ = 0;
if (!fl_help_view_buffer) fl_help_view_buffer = fl_create_offscreen(1, 1);
draw_mode_ = Mode::PUSH;
fl_begin_offscreen(fl_help_view_buffer);
draw();
fl_end_offscreen();
draw_mode_ = Mode::DRAW;
if (selection_push_last_) return 1;
else return 0;
}
/**
\brief Called from `handle()>FL_DRAG`, extending text selection.
\return 1 if more than just the initial text is selected.
*/
char Fl_Help_View::Impl::extend_selection()
{
if (Fl::event_is_click())
return 0;
// Give this widget the focus during the selection process. This will
// deselect other text selection and make sure, we receive the Copy
// keyboard shortcut.
if (Fl::focus()!=&view)
Fl::focus(&view);
// printf("old selection_first_=%d, selection_last_=%d\n",
// selection_first_, selection_last_);
int sf = selection_first_, sl = selection_last_;
selected_ = true;
draw_mode_ = Mode::DRAG;
fl_begin_offscreen(fl_help_view_buffer);
draw();
fl_end_offscreen();
draw_mode_ = Mode::DRAW;
if (selection_push_first_ < selection_drag_first_) {
selection_first_ = selection_push_first_;
} else {
selection_first_ = selection_drag_first_;
}
if (selection_push_last_ > selection_drag_last_) {
selection_last_ = selection_push_last_;
} else {
selection_last_ = selection_drag_last_;
}
// printf("new selection_first_=%d, selection_last_=%d\n",
// selection_first_, selection_last_);
if (sf!=selection_first_ || sl!=selection_last_) {
// puts("REDRAW!!!\n");
return 1;
} else {
// puts("");
return 0;
}
}
/**
\brief Called from `handle()>FL_RELEASE`, ends text selection process.
This method clears the static selection helper member variables.
*/
void Fl_Help_View::Impl::end_selection()
{
selection_push_first_ = 0;
selection_push_last_ = 0;
selection_drag_first_ = 0;
selection_drag_last_ = 0;
}
// ------ Fl_Help_View Protected and Public methods
// ---- Widget management
/**
\brief Draws the Fl_Help_View widget.
*/
void Fl_Help_View::draw() {
impl_->draw();
}
/**
\brief Draws the Fl_Help_View widget.
\see Fl_Help_View::draw()
*/
void Fl_Help_View::Impl::draw()
{
int i; // Looping var
const Text_Block *block; // Pointer to current block
const char *ptr, // Pointer to text in block
*attrs; // Pointer to start of element attributes
Edit_Buffer buf; // Text buffer
char attr[1024]; // Attribute buffer
int xx, yy, ww, hh; // Current positions and sizes
int line; // Current line
Fl_Font font;
Fl_Fontsize fsize; // Current font and size
Fl_Color fcolor; // current font color
int head, pre, // Flags for text
needspace; // Do we need whitespace?
Fl_Boxtype b = view.box() ? view.box() : FL_DOWN_BOX;
// Box to draw...
int underline, // Underline text?
xtra_ww; // Extra width for underlined space between words
DEBUG_FUNCTION(__LINE__,__FUNCTION__);
// Draw the scrollbar(s) and box first...
ww = view.w();
hh = view.h();
i = 0;
view.draw_box(b, view.x(), view.y(), ww, hh, bgcolor_);
if ( view.hscrollbar_.visible() || view.scrollbar_.visible() ) {
int scrollsize = scrollbar_size_ ? scrollbar_size_ : Fl::scrollbar_size();
int hor_vis = view.hscrollbar_.visible();
int ver_vis = view.scrollbar_.visible();
// Scrollbar corner
int scorn_x = view.x() + ww - (ver_vis?scrollsize:0) - Fl::box_dw(b) + Fl::box_dx(b);
int scorn_y = view.y() + hh - (hor_vis?scrollsize:0) - Fl::box_dh(b) + Fl::box_dy(b);
if ( hor_vis ) {
if ( view.hscrollbar_.h() != scrollsize ) { // scrollsize changed?
view.hscrollbar_.resize(view.x(), scorn_y, scorn_x - view.x(), scrollsize);
view.init_sizes();
}
view.draw_child(view.hscrollbar_);
hh -= scrollsize;
}
if ( ver_vis ) {
if ( view.scrollbar_.w() != scrollsize ) { // scrollsize changed?
view.scrollbar_.resize(scorn_x, view.y(), scrollsize, scorn_y - view.y());
view.init_sizes();
}
view.draw_child(view.scrollbar_);
ww -= scrollsize;
}
if ( hor_vis && ver_vis ) {
// Both scrollbars visible? Draw little gray box in corner
fl_color(FL_GRAY);
fl_rectf(scorn_x, scorn_y, scrollsize, scrollsize);
}
}
if (!value_)
return;
if (selected_) {
if (Fl::focus() == &view) {
// If this widget has the focus, we use the selection color directly
tmp_selection_color_ = view.selection_color();
} else {
// Otherwise we blend the selection color with the background color
tmp_selection_color_ = fl_color_average(bgcolor_, view.selection_color(), 0.8f);
}
selection_text_color_ = fl_contrast(textcolor_, tmp_selection_color_);
}
current_pos_ = 0;
// Clip the drawing to the inside of the box...
fl_push_clip(view.x() + Fl::box_dx(b), view.y() + Fl::box_dy(b),
ww - Fl::box_dw(b), hh - Fl::box_dh(b));
fl_color(textcolor_);
// Draw all visible blocks...
for (i = 0, block = &blocks_[0]; i < (int)blocks_.size(); i ++, block ++)
if ((block->y + block->h) >= topline_ && block->y < (topline_ + view.h()))
{
line = 0;
xx = block->line[line];
yy = block->y - topline_;
hh = 0;
pre = 0;
head = 0;
needspace = 0;
underline = 0;
initfont(font, fsize, fcolor);
// byte length difference between html entity (encoded by &...;) and
// UTF-8 encoding of same character
int entity_extra_length = 0;
for (ptr = block->start, buf.clear(); ptr < block->end;)
{
if ((*ptr == '<' || isspace((*ptr)&255)) && buf.size() > 0)
{
if (!head && !pre)
{
// Check width...
ww = buf.width();
if (needspace && xx > block->x)
xx += (int)fl_width(' ');
if ((xx + ww) > block->w)
{
if (line < 31)
line ++;
xx = block->line[line];
yy += hh;
hh = 0;
}
hv_draw(buf.c_str(), xx + view.x() - leftline_, yy + view.y(), entity_extra_length);
buf.clear();
entity_extra_length = 0;
if (underline) {
xtra_ww = isspace((*ptr)&255)?(int)fl_width(' '):0;
fl_xyline(xx + view.x() - leftline_, yy + view.y() + 1,
xx + view.x() - leftline_ + ww + xtra_ww);
}
current_pos_ = (int) (ptr-value_);
xx += ww;
if ((fsize + 2) > hh)
hh = fsize + 2;
needspace = 0;
}
else if (pre)
{
while (isspace((*ptr)&255))
{
if (*ptr == '\n')
{
hv_draw(buf.c_str(), xx + view.x() - leftline_, yy + view.y());
if (underline) fl_xyline(xx + view.x() - leftline_, yy + view.y() + 1,
xx + view.x() - leftline_ + buf.width());
buf.clear();
current_pos_ = (int) (ptr-value_);
if (line < 31)
line ++;
xx = block->line[line];
yy += hh;
hh = fsize + 2;
}
else if (*ptr == '\t')
{
// Do tabs every 8 columns...
buf += ' '; // add at least one space
while (buf.size() & 7)
buf += ' ';
}
else {
buf += ' ';
}
if ((fsize + 2) > hh)
hh = fsize + 2;
ptr ++;
}
if (buf.size() > 0)
{
hv_draw(buf.c_str(), xx + view.x() - leftline_, yy + view.y());
ww = buf.width();
buf.clear();
if (underline) fl_xyline(xx + view.x() - leftline_, yy + view.y() + 1,
xx + view.x() - leftline_ + ww);
xx += ww;
current_pos_ = (int) (ptr-value_);
}
needspace = 0;
}
else
{
buf.clear();
while (isspace((*ptr)&255))
ptr ++;
current_pos_ = (int) (ptr-value_);
}
}
if (*ptr == '<')
{
ptr ++;
if (strncmp(ptr, "!--", 3) == 0)
{
// Comment...
ptr += 3;
if ((ptr = strstr(ptr, "-->")) != nullptr)
{
ptr += 3;
continue;
}
else
break;
}
while (*ptr && *ptr != '>' && !isspace((*ptr)&255))
buf += *ptr++;
attrs = ptr;
while (*ptr && *ptr != '>')
ptr ++;
if (*ptr == '>')
ptr ++;
// end of command reached, set the supposed start of printed eord here
current_pos_ = (int) (ptr-value_);
if (buf.cmp("HEAD"))
head = 1;
else if (buf.cmp("BR"))
{
if (line < 31)
line ++;
xx = block->line[line];
yy += hh;
hh = 0;
}
else if (buf.cmp("HR"))
{
fl_line(block->x + view.x(), yy + view.y(), block->w + view.x(),
yy + view.y());
if (line < 31)
line ++;
xx = block->line[line];
yy += 2 * fsize;//hh;
hh = 0;
}
else if (buf.cmp("CENTER") ||
buf.cmp("P") ||
buf.cmp("H1") ||
buf.cmp("H2") ||
buf.cmp("H3") ||
buf.cmp("H4") ||
buf.cmp("H5") ||
buf.cmp("H6") ||
buf.cmp("UL") ||
buf.cmp("OL") ||
buf.cmp("DL") ||
buf.cmp("LI") ||
buf.cmp("DD") ||
buf.cmp("DT") ||
buf.cmp("PRE"))
{
if (tolower(buf[0]) == 'h')
{
font = FL_HELVETICA_BOLD;
fsize = textsize_ + '7' - buf[1];
}
else if (buf.cmp("DT"))
{
font = textfont_ | FL_ITALIC;
fsize = textsize_;
}
else if (buf.cmp("PRE"))
{
font = FL_COURIER;
fsize = textsize_;
pre = 1;
}
if (buf.cmp("LI"))
{
if (block->ol) {
char buf[10];
snprintf(buf, sizeof(buf), "%d. ", block->ol_num);
hv_draw(buf, xx - (int)fl_width(buf) + view.x() - leftline_, yy + view.y());
}
else {
// draw bullet (&bull;) Unicode: U+2022, UTF-8 (hex): e2 80 a2
unsigned char bullet[4] = { 0xe2, 0x80, 0xa2, 0x00 };
hv_draw((char *)bullet, xx - fsize + view.x() - leftline_, yy + view.y());
}
}
pushfont(font, fsize);
buf.clear();
}
else if (buf.cmp("A") &&
get_attr(attrs, "HREF", attr, sizeof(attr)) != nullptr)
{
fl_color(linkcolor_);
underline = 1;
}
else if (buf.cmp("/A"))
{
fl_color(textcolor_);
underline = 0;
}
else if (buf.cmp("FONT"))
{
if (get_attr(attrs, "COLOR", attr, sizeof(attr)) != nullptr) {
textcolor_ = get_color(attr, textcolor_);
}
if (get_attr(attrs, "FACE", attr, sizeof(attr)) != nullptr) {
if (!strncasecmp(attr, "helvetica", 9) ||
!strncasecmp(attr, "arial", 5) ||
!strncasecmp(attr, "sans", 4)) font = FL_HELVETICA;
else if (!strncasecmp(attr, "times", 5) ||
!strncasecmp(attr, "serif", 5)) font = FL_TIMES;
else if (!strncasecmp(attr, "symbol", 6)) font = FL_SYMBOL;
else font = FL_COURIER;
}
if (get_attr(attrs, "SIZE", attr, sizeof(attr)) != nullptr) {
if (isdigit(attr[0] & 255)) {
// Absolute size
fsize = (int)(textsize_ * pow(1.2, atof(attr) - 3.0));
} else {
// Relative size
fsize = (int)(fsize * pow(1.2, atof(attr) - 3.0));
}
}
pushfont(font, fsize);
}
else if (buf.cmp("/FONT"))
{
popfont(font, fsize, textcolor_);
}
else if (buf.cmp("U"))
underline = 1;
else if (buf.cmp("/U"))
underline = 0;
else if (buf.cmp("B") ||
buf.cmp("STRONG"))
pushfont(font |= FL_BOLD, fsize);
else if (buf.cmp("TD") ||
buf.cmp("TH"))
{
int tx, ty, tw, th;
if (tolower(buf[1]) == 'h')
pushfont(font |= FL_BOLD, fsize);
else
pushfont(font = textfont_, fsize);
tx = block->x - 4 - leftline_;
ty = block->y - topline_ - fsize - 3;
tw = block->w - block->x + 7;
th = block->h + fsize - 5;
if (tx < 0)
{
tw += tx;
tx = 0;
}
if (ty < 0)
{
th += ty;
ty = 0;
}
tx += view.x();
ty += view.y();
if (block->bgcolor != bgcolor_)
{
fl_color(block->bgcolor);
fl_rectf(tx, ty, tw, th);
fl_color(textcolor_);
}
if (block->border)
fl_rect(tx, ty, tw, th);
}
else if (buf.cmp("I") ||
buf.cmp("EM"))
pushfont(font |= FL_ITALIC, fsize);
else if (buf.cmp("CODE") ||
buf.cmp("TT"))
pushfont(font = FL_COURIER, fsize);
else if (buf.cmp("KBD"))
pushfont(font = FL_COURIER_BOLD, fsize);
else if (buf.cmp("VAR"))
pushfont(font = FL_COURIER_ITALIC, fsize);
else if (buf.cmp("/HEAD"))
head = 0;
else if (buf.cmp("/H1") ||
buf.cmp("/H2") ||
buf.cmp("/H3") ||
buf.cmp("/H4") ||
buf.cmp("/H5") ||
buf.cmp("/H6") ||
buf.cmp("/B") ||
buf.cmp("/STRONG") ||
buf.cmp("/I") ||
buf.cmp("/EM") ||
buf.cmp("/CODE") ||
buf.cmp("/TT") ||
buf.cmp("/KBD") ||
buf.cmp("/VAR"))
popfont(font, fsize, fcolor);
else if (buf.cmp("/PRE"))
{
popfont(font, fsize, fcolor);
pre = 0;
}
else if (buf.cmp("IMG"))
{
Fl_Shared_Image *img = 0;
int width, height;
char wattr[8], hattr[8];
get_attr(attrs, "WIDTH", wattr, sizeof(wattr));
get_attr(attrs, "HEIGHT", hattr, sizeof(hattr));
width = get_length(wattr);
height = get_length(hattr);
if (get_attr(attrs, "SRC", attr, sizeof(attr))) {
img = get_image(attr, width, height);
if (!width) width = img->w();
if (!height) height = img->h();
}
if (!width || !height) {
if (get_attr(attrs, "ALT", attr, sizeof(attr)) == nullptr) {
strcpy(attr, "IMG");
}
}
ww = width;
if (needspace && xx > block->x)
xx += (int)fl_width(' ');
if ((xx + ww) > block->w)
{
if (line < 31)
line ++;
xx = block->line[line];
yy += hh;
hh = 0;
}
if (img) {
img->draw(xx + view.x() - leftline_,
yy + view.y() - fl_height() + fl_descent() + 2);
}
xx += ww;
if ((height + 2) > hh)
hh = height + 2;
needspace = 0;
}
buf.clear();
}
else if (*ptr == '\n' && pre)
{
hv_draw(buf.c_str(), xx + view.x() - leftline_, yy + view.y());
buf.clear();
if (line < 31)
line ++;
xx = block->line[line];
yy += hh;
hh = fsize + 2;
needspace = 0;
ptr ++;
current_pos_ = (int) (ptr-value_);
}
else if (isspace((*ptr)&255))
{
if (pre)
{
if (*ptr == ' ')
buf += ' ';
else
{
// Do tabs every 8 columns...
buf += ' '; // at least one space
while (buf.size() & 7)
buf += ' ';
}
}
ptr ++;
if (!pre) current_pos_ = (int) (ptr-value_);
needspace = 1;
}
else if (*ptr == '&') // process html entity
{
ptr ++;
int qch = quote_char(ptr);
if (qch < 0)
buf += '&';
else {
size_t utf8l = buf.size();
buf.add(qch);
utf8l = buf.size() - utf8l; // length of added UTF-8 text
const char *oldptr = ptr;
ptr = strchr(ptr, ';') + 1;
entity_extra_length += int(ptr - (oldptr-1)) - utf8l; // extra length between html entity and UTF-8
}
if ((fsize + 2) > hh)
hh = fsize + 2;
}
else
{
buf += *ptr++;
if ((fsize + 2) > hh)
hh = fsize + 2;
}
}
if (buf.size() > 0 && !pre && !head)
{
ww = buf.width();
if (needspace && xx > block->x)
xx += (int)fl_width(' ');
if ((xx + ww) > block->w)
{
if (line < 31)
line ++;
xx = block->line[line];
yy += hh;
hh = 0;
}
}
if (buf.size() > 0 && !head)
{
hv_draw(buf.c_str(), xx + view.x() - leftline_, yy + view.y());
if (underline) fl_xyline(xx + view.x() - leftline_, yy + view.y() + 1,
xx + view.x() - leftline_ + ww);
current_pos_ = (int) (ptr-value_);
}
}
fl_pop_clip();
} // draw()
/**
\brief Creates the Fl_Help_View widget at the specified position and size.
\param[in] xx, yy, ww, hh Position and size of the widget
\param[in] l Label for the widget, can be nullptr
*/
Fl_Help_View::Fl_Help_View(int xx, int yy, int ww, int hh, const char *l)
: Fl_Group(xx, yy, ww, hh, l),
impl_(new Fl_Help_View::Impl(this)),
scrollbar_(xx + ww - Fl::scrollbar_size(), yy, Fl::scrollbar_size(), hh - Fl::scrollbar_size()),
hscrollbar_(xx, yy + hh - Fl::scrollbar_size(), ww - Fl::scrollbar_size(), Fl::scrollbar_size())
{
color(FL_BACKGROUND2_COLOR, FL_SELECTION_COLOR);
scrollbar_.value(0, hh, 0, 1);
scrollbar_.step(8.0);
scrollbar_.show();
scrollbar_.callback( [](Fl_Widget *s, void *u) {
((Fl_Help_View*)u)->topline((int)(((Fl_Scrollbar*)s)->value()));
}, this );
hscrollbar_.value(0, ww, 0, 1);
hscrollbar_.step(8.0);
hscrollbar_.show();
hscrollbar_.type(FL_HORIZONTAL);
hscrollbar_.callback( [](Fl_Widget *s, void *u) {
((Fl_Help_View*)u)->leftline(int(((Fl_Scrollbar*)s)->value()));
}, this );
end();
resize(xx, yy, ww, hh);
}
/**
\brief Destroys the Fl_Help_View widget.
The destructor destroys the widget and frees all memory that has been
allocated for the current document.
*/
Fl_Help_View::~Fl_Help_View()
{
}
/**
\brief Handles events in the widget.
\param[in] event Event to handle.
\return 1 if the event was handled, 0 otherwise.
*/
int Fl_Help_View::handle(int event)
{
return impl_->handle(event);
}
/**
\brief Handles events in the widget.
\see Fl_Help_View::handle(int event)
*/
int Fl_Help_View::Impl::handle(int event)
{
static std::shared_ptr<Link> linkp = nullptr; // currently clicked link
int xx = Fl::event_x() - view.x() + leftline_;
int yy = Fl::event_y() - view.y() + topline_;
switch (event)
{
case FL_FOCUS:
// Selection style changes, so ask for a redraw
if (selected_)
view.redraw();
return 1;
case FL_UNFOCUS:
// Selection style changes, so ask for a redraw
if (selected_)
view.redraw();
return 1;
case FL_ENTER :
view.Fl_Group::handle(event);
return 1;
case FL_LEAVE :
fl_cursor(FL_CURSOR_DEFAULT);
break;
case FL_MOVE:
if (find_link(xx, yy)) fl_cursor(FL_CURSOR_HAND);
else fl_cursor(FL_CURSOR_DEFAULT);
return 1;
case FL_PUSH:
// RMB will pop up a menu
if (Fl::event_button() == FL_RIGHT_MOUSE) {
rmb_menu[0].label(view.copy_menu_text);
if (text_selected())
rmb_menu[0].activate();
else
rmb_menu[0].deactivate();
fl_cursor(FL_CURSOR_DEFAULT);
const Fl_Menu_Item *mi = rmb_menu->popup(Fl::event_x(), Fl::event_y());
if (mi) switch (mi->argument()) {
case 1:
copy();
break;
}
return 1;
}
// Check if the scrollbars used up the event
if (view.Fl_Group::handle(event))
return 1;
// Check if the user clicked on a link
linkp = find_link(xx, yy);
if (linkp) {
fl_cursor(FL_CURSOR_HAND);
return 1;
}
// If nothing else, the user cancels the current selection and might start a new one
if (begin_selection()) {
selection_mode_ = Mode::PUSH;
fl_cursor(FL_CURSOR_INSERT);
return 1;
}
// Nothing to do.
fl_cursor(FL_CURSOR_DEFAULT);
return 1;
case FL_DRAG:
// If we clicked on a link, check if this remains a click, or if the user drags the mouse
if (linkp) {
if (Fl::event_is_click()) {
fl_cursor(FL_CURSOR_HAND);
} else {
// No longer just a click, so we cancel the link and start a drag selection
linkp = 0;
if (begin_selection()) {
selection_mode_ = Mode::PUSH;
fl_cursor(FL_CURSOR_INSERT);
}
}
}
// If the FL_PUSH started a selection, we extend the selection
if (selection_mode_ == Mode::PUSH) {
if (extend_selection())
view.redraw();
fl_cursor(FL_CURSOR_INSERT);
return 1;
}
// Nothing to do.
fl_cursor(FL_CURSOR_DEFAULT);
return 1;
case FL_RELEASE:
// If we clicked on a link, follow it
if (linkp) {
if (Fl::event_is_click()) {
follow_link(linkp);
}
fl_cursor(FL_CURSOR_DEFAULT);
linkp = 0;
return 1;
}
// If in a selection process, end the selection.
if (selection_mode_ == Mode::PUSH) {
end_selection();
selection_mode_ = Mode::DRAW;
return 1;
}
// Nothing to do.
return 1;
case FL_SHORTCUT: {
int mods = Fl::event_state() & (FL_META|FL_CTRL|FL_ALT|FL_SHIFT);
if ( mods == FL_COMMAND) {
switch ( Fl::event_key() ) {
case 'a': select_all(); view.redraw(); return 1;
case 'c':
case 'x': copy(1); return 1;
}
}
break; }
}
return (view.Fl_Group::handle(event));
}
/**
\brief Override the superclass's resize method.
\param[in] xx, yy, ww, hh New position and size of the widget
*/
void Fl_Help_View::resize(int xx, int yy, int ww, int hh)
{
impl_->resize(xx, yy, ww, hh);
}
/**
\brief Override the superclass's resize method.
\see Fl_Help_View::resize(int xx, int yy, int ww, int hh)
*/
void Fl_Help_View::Impl::resize(int xx, int yy, int ww, int hh)
{
Fl_Boxtype b = view.box() ? view.box() : FL_DOWN_BOX; // Box to draw...
view.Fl_Widget::resize(xx, yy, ww, hh);
int scrollsize = scrollbar_size_ ? scrollbar_size_ : Fl::scrollbar_size();
view.scrollbar_.resize(view.x() + view.w() - scrollsize - Fl::box_dw(b) + Fl::box_dx(b),
view.y() + Fl::box_dy(b), scrollsize, view.h() - scrollsize - Fl::box_dh(b));
view.hscrollbar_.resize(view.x() + Fl::box_dx(b),
view.y() + view.h() - scrollsize - Fl::box_dh(b) + Fl::box_dy(b),
view.w() - scrollsize - Fl::box_dw(b), scrollsize);
format();
}
// ---- HTML source and raw data
/**
\brief Sets the current help text buffer to the string provided and reformats the text.
The provided character string \p val is copied internally and will be
freed when value() is called again, or when the widget is destroyed.
If \p val is nullptr, then the widget is cleared.
\param[in] val Text to view, or nullptr to clear the widget,
Fl_Help_View will creat a local copy of the string.
*/
void Fl_Help_View::value(const char *val) {
impl_->value(val);
}
/**
\brief Sets the current help text buffer to the string provided and reformats the text.
\see Fl_Help_View::value(const char *val)
*/
void Fl_Help_View::Impl::value(const char *val)
{
clear_selection();
free_data();
view.set_changed();
if (!val)
return;
value_ = fl_strdup(val);
initial_load = 1;
format();
initial_load = 0;
topline(0);
leftline(0);
}
/**
\brief Loads the specified file.
This method loads the specified file or URL. The filename may end in a
\c \#name style target.
If the URL starts with \a ftp, \a http, \a https, \a ipp, \a mailto, or
\a news, followed by a colon, FLTK will use fl_open_uri() to show the
requested page in an external browser.
In all other cases, the URL is interpreted as a filename. The file is read and
displayed in this browser. Note that Windows style backslashes are not
supported in the file name.
\param[in] f filename or URL
\return 0 on success, -1 on error
\see fl_open_uri()
*/
int Fl_Help_View::load(const char *f) {
return impl_->load(f);
}
/**
\brief Loads the specified file.
\see Fl_Help_View::load(const char *f)
*/
int Fl_Help_View::Impl::load(const char *f)
{
FILE *fp; // File to read from
long len; // Length of file
std::string target; // Target in file
std::string localname; // Local filename
std::string error; // Error buffer
std::string newname; // New filename buffer
// printf("load(%s)\n",f); fflush(stdout);
if (strncmp(f, "ftp:", 4) == 0 ||
strncmp(f, "http:", 5) == 0 ||
strncmp(f, "https:", 6) == 0 ||
strncmp(f, "ipp:", 4) == 0 ||
strncmp(f, "mailto:", 7) == 0 ||
strncmp(f, "news:", 5) == 0)
{
char urimsg[FL_PATH_MAX]; // Use of static size ok.
if ( fl_open_uri(f, urimsg, sizeof(urimsg)) == 0 ) {
clear_selection();
newname = f;
size_t hash_pos = newname.rfind('#');
if (hash_pos != std::string::npos) {
target = newname.substr(hash_pos + 1);
newname.resize(hash_pos);
}
if (link_) {
const char *n = (*link_)(&view, newname.c_str());
if (n == nullptr)
return 0;
localname = n;
} else {
localname = newname;
}
free_data();
filename_ = newname;
// Note: We do not support Windows backslashes, since they are illegal
// in URLs...
directory_ = newname;
size_t slash_pos = directory_.rfind('/');
if (slash_pos == std::string::npos) {
directory_.clear();
} else if ((slash_pos > 0) && (directory_[slash_pos-1] != '/')) {
directory_.resize(slash_pos);
}
error = "<HTML><HEAD><TITLE>Error</TITLE></HEAD>"
"<BODY><H1>Error</H1>"
"<P>Unable to follow the link \""
+ std::string(f) + "\" - " + std::string(urimsg) + ".</P></BODY>";
value(error.c_str());
return -1;
} else {
return 0;
}
}
clear_selection();
newname = f;
size_t hash_pos = newname.rfind('#');
if (hash_pos != std::string::npos) {
target = newname.substr(hash_pos + 1);
newname.resize(hash_pos);
}
if (link_) {
const char *n = (*link_)(&view, newname.c_str());
if (n == nullptr)
return -1;
localname = n;
} else {
localname = newname;
}
free_data();
filename_ = newname;
directory_ = newname;
// Note: We do not support Windows backslashes, since they are illegal
// in URLs...
size_t slash_pos = directory_.rfind('/');
if (slash_pos == std::string::npos) {
directory_.clear();
} else if ((slash_pos > 0) && (directory_[slash_pos-1] != '/')) {
directory_.resize(slash_pos);
}
if (localname.find("file:") == 0) {
localname.erase(0, 5); // Adjust for local filename...
}
int ret = 0;
if ((fp = fl_fopen(localname.c_str(), "rb")) != nullptr)
{
fseek(fp, 0, SEEK_END);
len = ftell(fp);
rewind(fp);
value_ = (const char *)calloc(len + 1, 1);
if (fread((void *)value_, 1, len, fp)==0) { /* use default 0 */ }
fclose(fp);
}
else
{
error = "<HTML><HEAD><TITLE>Error</TITLE></HEAD>"
"<BODY><H1>Error</H1>"
"<P>Unable to follow the link \"" +localname + "\" - "
+ strerror(errno) + ".</P></BODY>";
value_ = fl_strdup(error.c_str());
ret = -1;
}
initial_load = 1;
format();
initial_load = 0;
if (!target.empty())
topline(target.c_str());
else
topline(0);
return ret;
}
/**
\brief Finds the specified string \p s at starting position \p p.
The argument \p p and the return value are offsets in Fl_Help_View::value(),
counting from 0. If \p p is out of range, 0 is used.
The string comparison is simple but honors some special cases:
- the specified string \p s must be in UTF-8 encoding
- HTML tags in value() are filtered (not compared as such, they never match)
- HTML entities like '\&lt;' or '\&x#20ac;' are converted to Unicode (UTF-8)
- ASCII characters (7-bit, \< 0x80) are compared case insensitive
- every newline (LF, '\\n') in value() is treated like a single space
- all other strings are compared as-is (byte by byte)
\param[in] s search string in UTF-8 encoding
\param[in] p starting position for search (0,...), Default = 0
\return the matching position or -1 if not found
*/
int Fl_Help_View::find(const char *s, int p) {
return impl_->find(s, p);
}
/**
\brief Finds the specified string \p s at starting position \p p.
\see Fl_Help_View::find(const char *s, int p)
*/
int Fl_Help_View::Impl::find(const char *s, int p)
{
int i, // Looping var
c; // Current character
Text_Block *b; // Current block
const char *bp, // Block matching pointer
*bs, // Start of current comparison
*sp; // Search string pointer
DEBUG_FUNCTION(__LINE__,__FUNCTION__);
// Range check input and value...
if (!s || !value_) return -1;
if (p < 0 || p >= (int)strlen(value_)) p = 0;
// Look for the string...
for (i = (int)blocks_.size(), b = &blocks_[0]; i > 0; i--, b++) {
if (b->end < (value_ + p))
continue;
if (b->start < (value_ + p))
bp = value_ + p;
else
bp = b->start;
bp = vanilla(bp, b->end);
if (bp == b->end)
continue;
for (sp = s, bs = bp; *sp && *bp && bp < b->end; ) {
bool is_html_entity = false;
if (*bp == '&') {
// decode HTML entity...
if ((c = quote_char(bp + 1)) < 0) {
c = '&';
} else {
const char *entity_end = strchr(bp + 1, ';');
if (entity_end) {
is_html_entity = true; // c contains the unicode character
bp = entity_end;
} else {
c = '&';
}
}
} else {
c = *bp;
}
if (c == '\n') c = ' '; // treat newline as a single space
// *FIXME* *UTF-8* (A.S. 02/14/2016)
// At this point c may be an arbitrary Unicode Code Point corresponding
// to a quoted character (see above), i.e. it _can_ be a multi byte
// UTF-8 sequence and must be compared with the corresponding
// multi byte string in (*sp)...
// For instance: "&euro;" == 0x20ac -> 0xe2 0x82 0xac (UTF-8: 3 bytes).
// Hint: use fl_utf8encode() [see below]
int utf_len = 1;
if (c > 0x20 && c < 0x80 && tolower(*sp) == tolower(c)) {
// Check for ASCII case insensitive match.
//printf("%ld text match %c/%c\n", bp-value_, *sp, c);
sp++;
bp = vanilla(bp+1, b->end);
} else if (is_html_entity && fl_utf8decode(sp, nullptr, &utf_len) == (unsigned int)c ) {
// Check if a &lt; entity ini html matches a UTF-8 character in the
// search string.
//printf("%ld unicode match 0x%02X 0x%02X\n", bp-value_, *sp, c);
sp += utf_len;
bp = vanilla(bp+1, b->end);
} else if (*sp == c) {
// Check if UTF-8 bytes in html and the search string match.
//printf("%ld binary match %c/%c\n", bp-value_, *sp, c);
sp++;
bp = vanilla(bp+1, b->end);
} else {
// No match, so reset to start of search... .
//printf("reset search (%c/%c)\n", *sp, c);
sp = s;
bp = bs = vanilla(bs+1, b->end);
}
}
if (!*sp) { // Found a match!
topline(b->y - b->h);
return int(bs - value_);
}
}
// No match!
return (-1);
}
/**
\brief Set a callback function for following links.
This method assigns a callback function to use when a link is
followed or a file is loaded (via Fl_Help_View::load()) that
requires a different file or path.
The callback function receives a pointer to the Fl_Help_View
widget and the URI or full pathname for the file in question.
It must return a pathname that can be opened as a local file or nullptr:
\code
const char *fn(Fl_Widget *w, const char *uri);
\endcode
The link function can be used to retrieve remote or virtual
documents, returning a temporary file that contains the actual
data. If the link function returns nullptr, the value of
the Fl_Help_View widget will remain unchanged.
If the link callback cannot handle the URI scheme, it should
return the uri value unchanged or set the value() of the widget
before returning nullptr.
\param[in] fn Pointer to the callback function
*/
void Fl_Help_View::link(Fl_Help_Func *fn) {
impl_->link(fn);
}
/**
\brief Set a callback function for following links.
\see Fl_Help_View::link(Fl_Help_Func *fn)
*/
void Fl_Help_View::Impl::link(Fl_Help_Func *fn)
{
link_ = fn;
}
/**
\brief Return the current filename for the text in the buffer.
Fl_Help_View remains the owner of the allocated memory. If the filename
changes, the returned pointer will become stale.
\return nullptr if the filename is empty
*/
const char *Fl_Help_View::filename() const {
return impl_->filename(); // Ensure the filename is up to date
}
/**
\brief Return the current filename for the text in the buffer.
\see Fl_Help_View::filename() const
*/
const char *Fl_Help_View::Impl::filename() const {
if (filename_.empty())
return nullptr;
else
return filename_.c_str();
}
/**
\brief Return the current directory for the text in the buffer.
Fl_Help_View remains the owner of the allocated memory. If the directory
changes, the returned pointer will become stale.
\return nullptr if the directory name is empty
*/
const char *Fl_Help_View::directory() const {
return impl_->directory(); // Ensure the directory is up to date
}
/**
\brief Return the current directory for the text in the buffer.
\see Fl_Help_View::directory() const
*/
const char *Fl_Help_View::Impl::directory() const {
if (directory_.empty())
return nullptr;
else
return directory_.c_str();
}
/**
\brief Return the title of the current document.
Fl_Help_View remains the owner of the allocated memory. If the document
changes, the returned pointer will become stale.
\return empty string if the directory name is empty
*/
const char *Fl_Help_View::title() const {
return impl_->title(); // Ensure the title is up to date
}
/**
\brief Return the title of the current document.
\see Fl_Help_View::title() const
*/
const char *Fl_Help_View::Impl::title() const
{
return title_.c_str();
}
// ---- Rendering attributes
/**
\brief Scroll the text to the given anchor.
\param[in] anchor scroll to this named anchor
*/
void Fl_Help_View::topline(const char *anchor) {
impl_->topline(anchor);
}
/**
\brief Scroll the text to the given anchor.
\see Fl_Help_View::topline(const char *anchor)
*/
void Fl_Help_View::Impl::topline(const char *anchor)
{
std::string target_name = to_lower(anchor); // Convert to lower case
auto tl = target_line_map_.find(target_name);
if (tl != target_line_map_.end()) {
// Found the target name, scroll to the line
topline(tl->second);
} else {
// Scroll to the top.
topline(0);
}
}
/**
\brief Scrolls the text to the indicated position, given a pixel line.
If the given pixel value \p top is out of range, then the text is
scrolled to the top or bottom of the document, resp.
\param[in] top top line number in pixels (0 = start of document)
*/
void Fl_Help_View::topline(int top) {
impl_->topline(top);
}
/**
\brief Scrolls the text to the indicated position, given a pixel line.
\see Fl_Help_View::topline(int top)
*/
void Fl_Help_View::Impl::topline(int top)
{
if (!value_)
return;
int scrollsize = scrollbar_size_ ? scrollbar_size_ : Fl::scrollbar_size();
if (size_ < (view.h() - scrollsize) || top < 0)
top = 0;
else if (top > size_)
top = size_;
topline_ = top;
view.scrollbar_.value(topline_, view.h() - scrollsize, 0, size_);
view.do_callback(FL_REASON_DRAGGED);
view.redraw();
}
/**
\brief Scrolls the text to the indicated position, given a pixel column.
If the given pixel value \p left is out of range, then the text is
scrolled to the left or right side of the document, resp.
\param[in] left left column number in pixels (0 = left side)
*/
void Fl_Help_View::leftline(int left) {
impl_->leftline(left);
}
/**
\brief Scrolls the text to the indicated position, given a pixel column.
\see Fl_Help_View::leftline(int left)
*/
void Fl_Help_View::Impl::leftline(int left)
{
if (!value_)
return;
int scrollsize = scrollbar_size_ ? scrollbar_size_ : Fl::scrollbar_size();
if (hsize_ < (view.w() - scrollsize) || left < 0)
left = 0;
else if (left > hsize_)
left = hsize_;
leftline_ = left;
view.hscrollbar_.value(leftline_, view.w() - scrollsize, 0, hsize_);
view.redraw();
}
// ---- Text selection
/**
\brief Removes the current text selection.
*/
void Fl_Help_View::clear_selection() {
impl_->clear_selection();
}
/**
\brief Removes the current text selection.
\see Fl_Help_View::Impl::clear_selection()
*/
void Fl_Help_View::Impl::clear_selection()
{
selected_ = false;
selection_first_ = 0;
selection_last_ = 0;
view.redraw();
}
/**
\brief Selects all the text in the view.
*/
void Fl_Help_View::select_all() {
impl_->select_all();
}
/**
\brief Selects all the text in the view.
Fl_Help_View::Impl::select_all()
*/
void Fl_Help_View::Impl::select_all()
{
clear_selection();
if (!value_) return;
selection_first_ = 0;
selection_last_ = (int) strlen(value_);
selected_ = true;
}
/**
\brief Check if the user selected text in this view.
\return 1 if text is selected, 0 if no text is selected
*/
int Fl_Help_View::text_selected() const {
return impl_->text_selected();
}
/**
\brief Check if the user selected text in this view.
\see Fl_Help_View::text_selected()
*/
int Fl_Help_View::Impl::text_selected() const
{
return selected_;
}
/**
\brief If text is selected in this view, copy it to a clipboard.
\param[in] clipboard for x11 only, 0=selection buffer, 1=clipboard, 2=both
\return 1 if text is selected, 0 if no text is selected
*/
int Fl_Help_View::copy(int clipboard) {
return impl_->copy(clipboard);
}
/**
\brief If text is selected in this view, copy it to a clipboard.
\see Fl_Help_View::copy(int clipboard)
*/
int Fl_Help_View::Impl::copy(int clipboard)
{
if (!selected_)
return 0;
// convert the select part of our html text into some kind of somewhat readable UTF-8
// and store it in the selection buffer
int p = 0;
char pre = 0;
int len = (int) strlen(value_);
char *txt = (char*)malloc(len+1), *d = txt;
const char *s = value_, *cmd, *src;
for (;;) {
int c = (*s++) & 0xff;
if (c==0) break;
if (c=='<') { // begin of some html command. Skip until we find a '>'
cmd = s;
for (;;) {
c = (*s++) & 0xff;
if (c==0 || c=='>') break;
}
if (c==0) break;
// do something with this command... .
// The replacement string must not be longer than the command
// itself plus '<' and '>'
src = 0;
switch (command(cmd)) {
case CMD('p','r','e', 0 ): pre = 1; break;
case CMD('/','p','r','e'): pre = 0; break;
case CMD('t','d', 0 , 0 ):
case CMD('p', 0 , 0 , 0 ):
case CMD('/','p', 0 , 0 ):
case CMD('b','r', 0 , 0 ): src = "\n"; break;
case CMD('l','i', 0 , 0 ): src = "\n * "; break;
case CMD('/','h','1', 0 ):
case CMD('/','h','2', 0 ):
case CMD('/','h','3', 0 ):
case CMD('/','h','4', 0 ):
case CMD('/','h','5', 0 ):
case CMD('/','h','6', 0 ): src = "\n\n"; break;
case CMD('t','r', 0 , 0 ):
case CMD('h','1', 0 , 0 ):
case CMD('h','2', 0 , 0 ):
case CMD('h','3', 0 , 0 ):
case CMD('h','4', 0 , 0 ):
case CMD('h','5', 0 , 0 ):
case CMD('h','6', 0 , 0 ): src = "\n\n"; break;
case CMD('d','t', 0 , 0 ): src = "\n "; break;
case CMD('d','d', 0 , 0 ): src = "\n - "; break;
}
int n = (int) (s-value_);
if (src && n>selection_first_ && n<=selection_last_) {
while (*src) {
*d++ = *src++;
}
c = src[-1] & 0xff;
p = isspace(c) ? ' ' : c;
}
continue;
}
const char *s2 = s;
if (c=='&') { // special characters (HTML entities)
int xx = quote_char(s);
if (xx >= 0) {
c = xx;
for (;;) {
char cc = *s++;
if (!cc || cc==';') break;
}
}
}
int n = (int) (s2-value_);
if (n>selection_first_ && n<=selection_last_) {
if (!pre && c < 256 && isspace(c)) c = ' ';
if (p != ' ' || c != ' ') {
if (s2 != s) { // c was an HTML entity
d += fl_utf8encode(c, d);
}
else *d++ = c;
}
p = c;
}
if (n>selection_last_) break; // stop parsing html after end of selection
}
*d = 0;
Fl::copy(txt, (int) strlen(txt), clipboard);
// printf("copy [%s]\n", txt);
free(txt);
return 1;
}
// ---- Scroll bars
/**
\brief Get the current size of the scrollbars' troughs, in pixels.
If this value is zero (default), this widget will use the
Fl::scrollbar_size() value as the scrollbar's width.
\returns Scrollbar size in pixels, or 0 if the global Fl::scrollbar_size() is being used.
\see Fl::scrollbar_size(int)
*/
int Fl_Help_View::scrollbar_size() const {
return impl_->scrollbar_size();
}
/**
\brief Get the current size of the scrollbars' troughs, in pixels.
\see Fl_Help_View::scrollbar_size() const
*/
int Fl_Help_View::Impl::scrollbar_size() const {
return(scrollbar_size_);
}
/**
\brief Set the pixel size of the scrollbars' troughs to \p newSize, in pixels.
Normally you should not need this method, and should use
Fl::scrollbar_size(int) instead to manage the size of ALL
your widgets' scrollbars. This ensures your application
has a consistent UI, is the default behavior, and is normally
what you want.
Only use THIS method if you really need to override the global
scrollbar size. The need for this should be rare.
Setting \p newSize to the special value of 0 causes the widget to
track the global Fl::scrollbar_size(), which is the default.
\param[in] newSize Sets the scrollbar size in pixels.\n
If 0 (default), scrollbar size tracks the global Fl::scrollbar_size()
\see Fl::scrollbar_size()
*/
void Fl_Help_View::scrollbar_size(int newSize) {
impl_->scrollbar_size(newSize);
}
/**
\brief Set the pixel size of the scrollbars' troughs to \p newSize, in pixels.
\see Fl_Help_View::scrollbar_size(int)
*/
void Fl_Help_View::Impl::scrollbar_size(int newSize)
{
scrollbar_size_ = newSize;
}
/**
\brief Skips over HTML tags in a text.
In an html style text, set the character pointer p, skipping anything from a
leading '<' up to and including the closing '>'. If the end of the buffer is
reached, the function returns `end`.
No need to handle UTF-8 here.
\param[in] p pointer to html text, UTF-8 characters possible
\param[in] end pointer to the end of the text (need nut be NUL)
\return new pointer to text after skipping over '<...>' blocks, or `end`
if NUL was found or a '<...>' block was not closed.
*/
static const char *vanilla(const char *p, const char *end) {
if (*p == '\0' || p >= end) return end;
for (;;) {
if (*p != '<') {
return p;
} else {
while (*p && p < end && *p != '>') p++;
}
p++;
if (*p == '\0' || p >= end) return end;
}
}
// convert a command with up to four letters into an unsigned int
static uint32_t command(const char *cmd)
{
uint32_t ret = (tolower(cmd[0])<<24);
char c = cmd[1];
if (c=='>' || c==' ' || c==0) return ret;
ret |= (tolower(c)<<16);
c = cmd[2];
if (c=='>' || c==' ' || c==0) return ret;
ret |= (tolower(c)<<8);
c = cmd[3];
if (c=='>' || c==' ' || c==0) return ret;
ret |= tolower(c);
c = cmd[4];
if (c=='>' || c==' ' || c==0) return ret;
return 0;
}
// ------ Some more helper functions
/*
\brief Returns the Unicode Code Point associated with a quoted character (aka "HTML Entity").
Possible encoding formats:
- `&name;` named entity
- `&#nn..;` numeric (decimal) Unicode Code Point
- `&#xnn..;` numeric (hexadecimal) Unicode Code Point
- `&#Xnn..;` numeric (hexadecimal) Unicode Code Point
`nn..` = decimal or hexadecimal number, resp.
Contents of the table `names[]` below:
All printable ASCII (32-126) and ISO-8859-1 (160-255) characters
are encoded with the same value in Unicode. Special characters
outside the range [0-255] are encoded with their Unicode Code Point
as hexadecimal constants. Example:
- Euro sign: (Unicode) U+20ac = (hex) 0x20ac
\note Converted to correct Unicode values and tested (compared with
the display of Firefox). AlbrechtS, 14 Feb. 2016.
\note if you add or remove items to/from this list, please
update the documentation for Fl_Help_View::Fl_Help_View().
\param[in] p Pointer to the quoted character string, e.g. `&copy;` or `&#169;`
\return the Unicode Code Point for the quoted character, or -1 if not found.
*/
static int quote_char(const char *p) {
int i;
static const struct {
const char *name;
int namelen;
int code;
} *nameptr, // Pointer into name array
names[] = // Quoting names
{
{ "Aacute;", 7, 193 },
{ "aacute;", 7, 225 },
{ "Acirc;", 6, 194 },
{ "acirc;", 6, 226 },
{ "acute;", 6, 180 },
{ "AElig;", 6, 198 },
{ "aelig;", 6, 230 },
{ "Agrave;", 7, 192 },
{ "agrave;", 7, 224 },
{ "amp;", 4, '&' },
{ "Aring;", 6, 197 },
{ "aring;", 6, 229 },
{ "Atilde;", 7, 195 },
{ "atilde;", 7, 227 },
{ "Auml;", 5, 196 },
{ "auml;", 5, 228 },
{ "brvbar;", 7, 166 },
{ "bull;", 5, 0x2022 },
{ "Ccedil;", 7, 199 },
{ "ccedil;", 7, 231 },
{ "cedil;", 6, 184 },
{ "cent;", 5, 162 },
{ "copy;", 5, 169 },
{ "curren;", 7, 164 },
{ "dagger;", 7, 0x2020 },
{ "deg;", 4, 176 },
{ "divide;", 7, 247 },
{ "Eacute;", 7, 201 },
{ "eacute;", 7, 233 },
{ "Ecirc;", 6, 202 },
{ "ecirc;", 6, 234 },
{ "Egrave;", 7, 200 },
{ "egrave;", 7, 232 },
{ "ETH;", 4, 208 },
{ "eth;", 4, 240 },
{ "Euml;", 5, 203 },
{ "euml;", 5, 235 },
{ "euro;", 5, 0x20ac },
{ "frac12;", 7, 189 },
{ "frac14;", 7, 188 },
{ "frac34;", 7, 190 },
{ "gt;", 3, '>' },
{ "Iacute;", 7, 205 },
{ "iacute;", 7, 237 },
{ "Icirc;", 6, 206 },
{ "icirc;", 6, 238 },
{ "iexcl;", 6, 161 },
{ "Igrave;", 7, 204 },
{ "igrave;", 7, 236 },
{ "iquest;", 7, 191 },
{ "Iuml;", 5, 207 },
{ "iuml;", 5, 239 },
{ "laquo;", 6, 171 },
{ "lt;", 3, '<' },
{ "macr;", 5, 175 },
{ "micro;", 6, 181 },
{ "middot;", 7, 183 },
{ "nbsp;", 5, ' ' },
{ "ndash;", 6, 0x2013 },
{ "not;", 4, 172 },
{ "Ntilde;", 7, 209 },
{ "ntilde;", 7, 241 },
{ "Oacute;", 7, 211 },
{ "oacute;", 7, 243 },
{ "Ocirc;", 6, 212 },
{ "ocirc;", 6, 244 },
{ "Ograve;", 7, 210 },
{ "ograve;", 7, 242 },
{ "ordf;", 5, 170 },
{ "ordm;", 5, 186 },
{ "Oslash;", 7, 216 },
{ "oslash;", 7, 248 },
{ "Otilde;", 7, 213 },
{ "otilde;", 7, 245 },
{ "Ouml;", 5, 214 },
{ "ouml;", 5, 246 },
{ "para;", 5, 182 },
{ "permil;", 7, 0x2030 },
{ "plusmn;", 7, 177 },
{ "pound;", 6, 163 },
{ "quot;", 5, '\"' },
{ "raquo;", 6, 187 },
{ "reg;", 4, 174 },
{ "sect;", 5, 167 },
{ "shy;", 4, 173 },
{ "sup1;", 5, 185 },
{ "sup2;", 5, 178 },
{ "sup3;", 5, 179 },
{ "szlig;", 6, 223 },
{ "THORN;", 6, 222 },
{ "thorn;", 6, 254 },
{ "times;", 6, 215 },
{ "trade;", 6, 0x2122 },
{ "Uacute;", 7, 218 },
{ "uacute;", 7, 250 },
{ "Ucirc;", 6, 219 },
{ "ucirc;", 6, 251 },
{ "Ugrave;", 7, 217 },
{ "ugrave;", 7, 249 },
{ "uml;", 4, 168 },
{ "Uuml;", 5, 220 },
{ "uuml;", 5, 252 },
{ "Yacute;", 7, 221 },
{ "yacute;", 7, 253 },
{ "yen;", 4, 165 },
{ "Yuml;", 5, 0x0178 },
{ "yuml;", 5, 255 }
};
if (!strchr(p, ';')) return -1;
if (*p == '#') {
if (*(p+1) == 'x' || *(p+1) == 'X') {
return (int)strtol(p+2, nullptr, 16);
} else {
return atoi(p+1);
}
}
for (i = (int)(sizeof(names) / sizeof(names[0])), nameptr = names; i > 0; i --, nameptr ++) {
if (strncmp(p, nameptr->name, nameptr->namelen) == 0)
return nameptr->code;
}
return -1;
}
// The following function is used to make anchors in a link (the part after
// the '#') case insensitive. As it is, the function handles only ASCII
// characters. Should it be extended to handle UTF-8 characters as well?
//
// UTF8 is allowed in anchors, but only when it is encoded between '%', so
// `https://example.com/page#%C3%BCber` is valid, but
// `https://example.com/page#über is *not*.
//
// But as with everything in HTML, nobody cares and everybody does what they
// want anyway ;-) .
static std::string to_lower(const std::string &str) {
std::string lower_str;
lower_str.reserve(str.size());
for (char c : str) {
lower_str += fl_tolower(c);
}
return lower_str;
}
/**
\brief Check if a URL is starting with a scheme (e.g. ftp:).
\param[in] url the URL to check.
\return 0 if not found, otherwise the length of the scheme string including
the following '/' characters.
*/
static size_t url_scheme(const std::string &url, bool skip_slashes)
{
// First skip all ascii letters and digits
size_t pos = 0;
while ( (pos < url.size()) && ( isalnum(url[pos]) || (url[pos] == '+') || (url[pos] == '-') || (url[pos] == '.') )) {
pos++;
}
// Next, check for the ':' character
if ( (pos < url.size()) && (url[pos] == ':') ) {
pos++; // Skip the ':' character
if (skip_slashes) {
// If found, skip up to two '/' characters as well
if ( (pos < url.size()) && (url[pos] == '/') ) {
pos++; // Skip the first '/' character
if ( (pos < url.size()) && (url[pos] == '/') ) {
pos++; // Skip the second '/' character
}
}
}
return pos; // Return the length of the scheme including the following '/' characters
}
return 0; // No scheme found
}
/** Return a pointer to the internal text buffer. */
const char *Fl_Help_View::value() const { return impl_->value(); }
/** Return the document height in pixels. */
int Fl_Help_View::size() const { return impl_->size(); }
/** Set the default text color. */
void Fl_Help_View::textcolor(Fl_Color c) { impl_->textcolor(c); }
/** Return the current default text color. */
Fl_Color Fl_Help_View::textcolor() const { return impl_->textcolor(); }
/** Set the default text font. */
void Fl_Help_View::textfont(Fl_Font f) { impl_->textfont(f); }
/** Return the default text font. */
Fl_Font Fl_Help_View::textfont() const { return impl_->textfont(); }
/** Set the default text size. */
void Fl_Help_View::textsize(Fl_Fontsize s) { impl_->textsize(s); }
/** Get the default text size. */
Fl_Fontsize Fl_Help_View::textsize() const { return impl_->textsize(); }
/** Get the current top line in pixels. */
int Fl_Help_View::topline() const { return impl_->topline(); }
/** Get the left position in pixels. */
int Fl_Help_View::leftline() const { return impl_->leftline(); }