summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build.yml6
-rw-r--r--FL/Fl_String.H146
-rw-r--r--FL/fl_ask.H6
-rw-r--r--fluid/ExternalCodeEditor_UNIX.cxx6
-rw-r--r--fluid/ExternalCodeEditor_UNIX.h2
-rw-r--r--fluid/ExternalCodeEditor_WIN32.cxx2
-rw-r--r--fluid/Fl_Menu_Type.cxx14
-rw-r--r--fluid/Fl_Widget_Type.cxx12
-rw-r--r--fluid/Fl_Window_Type.cxx20
-rw-r--r--fluid/code.cxx22
-rw-r--r--fluid/file.cxx16
-rw-r--r--fluid/fluid.cxx32
-rw-r--r--fluid/fluid.h34
-rw-r--r--fluid/widget_panel.fl4
-rw-r--r--src/Fl_String.cxx600
-rw-r--r--src/fl_ask.cxx72
-rw-r--r--test/CMakeLists.txt1
-rw-r--r--test/Makefile3
-rw-r--r--test/ask.cxx9
-rw-r--r--test/unittest_core.cxx253
-rw-r--r--test/unittests.cxx319
-rw-r--r--test/unittests.h174
22 files changed, 1481 insertions, 272 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 7df43f315..b1265db62 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -44,6 +44,12 @@ jobs:
# Execute the build. You can specify a specific target with "--target <NAME>"
run: cmake --build . --config $BUILD_TYPE
+ - name: Unittest
+ working-directory: ${{github.workspace}}/build
+ shell: bash
+ run: ./bin/test/unittests --core
+
+
build-wayland:
runs-on: ubuntu-latest
diff --git a/FL/Fl_String.H b/FL/Fl_String.H
index 72e90f52d..bb38776ad 100644
--- a/FL/Fl_String.H
+++ b/FL/Fl_String.H
@@ -21,94 +21,110 @@
Basic Fl_String class for FLTK.
*/
+// See: https://en.cppreference.com/w/cpp/string/basic_string/basic_string
+
/**
Fl_String is the basic string class for FLTK.
In this version Fl_String can be used to store strings, copy strings,
- and move strings. There are no string manipulation methods yet.
+ move strings, and do basic string manipulation. Fl_String implements a
+ subset of std::string with a couple of extensions. std::string should be
+ a drop-in if we ever decide to allow templates and the std library.
- Fl_String can hold the value of an Fl_Input widget including \e nul bytes
- if the constructor Fl_String(const char *str, int size) is used.
+ Fl_String always maintains a trailing \e nul byte, but can also contain
+ \e nul bytes insde the string if the constructor
+ Fl_String(const char *str, int size) is used.
Assignment and copy constructors \b copy the string value such that the
source string can be freed immediately after the assignment.
- The string value() can be an empty string \c "" or \c NULL.
-
- If value() is not \c NULL it is guaranteed that the string is terminated by
- a trailing \c nul byte even if the string contains embedded \c nul bytes.
+ c_str() and data() can be an empty string \c "", but never be \c NULL.
The method size() returns the full string size, whether the string contains
- embedded \c nul bytes or not. The special method slen() returns 0 if value()
- is \c NULL, otherwise the same as \c strlen() would do.
-
- Examples:
- \code
- Fl_String np(NULL);
- printf(" np : value = %p, size = %d, slen = %d\n", np.value(), np.size(), np.slen());
- Fl_String empty("");
- printf(" empty : value = %p, size = %d\n", empty.value(), empty.size());
- Fl_String fltk("FLTK");
- Fl_Input i(0, 0, 0, 0);
- i.value("abc\0def", 7);
- Fl_String str(i.value(), i.size());
- printf(" str : strlen = %lu, size = %d, capacity = %d\n",
- strlen(str.value()), str.size(), str.capacity());
-
- Output:
-
- np : value = (nil), size = 0, slen = 0
- empty : value = 0x562840befbf0, size = 0
- str : strlen = 3, size = 7, capacity = 15
- \endcode
+ embedded \c nul bytes or not. The special method Fl_String::strlen() returns
+ the length of the string up to the first \e nul.
+
+ All methods of Fl_String work on a byte level. They are not UTF-8 aware,
+ but may hold and manipulate UTF-8 strings if done with care.
\since 1.4.0
*/
-
class Fl_String {
+
private:
+ /*
+ FLTK does no small string optimisation.
+ If the string is empty and capacity is not set, buffer_ will be NULL.
+ */
+ char *buffer_;
int size_;
- char *value_;
int capacity_;
-public:
- Fl_String();
- Fl_String(const char *str);
- Fl_String(const char *str, int size);
-
- // copy constructor
- Fl_String(const Fl_String &in);
-
- // copy assignment operator
- Fl_String& operator=(const Fl_String &in);
+ void init_();
+ void grow_(int n);
+ void shrink_(int n);
+ Fl_String &replace_(int at, int n_del, const char *src, int n_ins);
- // assignment operator for 'const char *'
- Fl_String& operator=(const char *in);
-
- virtual ~Fl_String();
-
-private:
- void init();
- void alloc_buf(int size, bool preserve_text=false);
- void release();
+protected:
+ static const char NUL;
public:
- void value(const char *str);
- void value(const char *str, int len);
- /** Returns a pointer to the first character stored in the object */
- const char *value() const { return value_; }
- /** Returns a pointer to the first byte stored in the object */
- char *buffer() { return value_; }
- /** Returns the number of bytes currently stored in the object */
- int size() const { return size_; }
-
- int slen() const;
- int capacity() const;
- void capacity(int num_bytes);
-
- void debug(const char *info = 0) const; // output string info
- void hexdump(const char *info = 0) const; // output string info + hexdump
+ static const int npos;
+ // ---- Assignment
+ Fl_String();
+ Fl_String(const Fl_String &str);
+ Fl_String(const char *cstr);
+ Fl_String(const char *str, int size);
+ ~Fl_String();
+ Fl_String& operator=(const Fl_String &str);
+ Fl_String& operator=(const char *cstr);
+ Fl_String &assign(const Fl_String &str);
+ Fl_String &assign(const char *cstr);
+ Fl_String &assign(const char *str, int size);
+
+ // ---- Element Access
+ char at(int pos) const;
+ char operator[](int n) const;
+ char &operator[](int n);
+ const char *data() const;
+ char *data();
+ const char *c_str() const;
+
+ // ---- Capacity
+ bool empty() const;
+ int size() const;
+ void reserve(int n);
+ int capacity() const;
+ void shrink_to_fit();
+
+ // --- Operations
+ void clear();
+ Fl_String &insert(int at, const char *src, int n_ins=npos);
+ Fl_String &insert(int at, const Fl_String &src);
+ Fl_String &erase(int at, int n_del);
+ void push_back(char c);
+ void pop_back();
+ Fl_String &append(const char *src, int n_ins=npos);
+ Fl_String &append(const Fl_String &src);
+ Fl_String &append(char c);
+ Fl_String &operator+=(const char *src);
+ Fl_String &operator+=(const Fl_String &src);
+ Fl_String &operator+=(char c);
+ Fl_String &replace(int at, int n_del, const char *src, int n_ins=npos);
+ Fl_String &replace(int at, int n_del, const Fl_String &src);
+ Fl_String substr(int pos=0, int n=npos) const;
+ void resize(int n);
+
+ // --- Non Standard
+ int strlen() const;
+ void debug(const char *info = 0) const;
+ void hexdump(const char *info = 0) const;
}; // class Fl_String
+// ---- Non-member functions
+extern Fl_String operator+(const Fl_String &lhs, const Fl_String &rhs);
+extern Fl_String operator+(const Fl_String &lhs, const char *rhs);
+extern bool operator==(const Fl_String &lhs, const Fl_String &rhs);
+
#endif // _FL_Fl_String_H_
diff --git a/FL/fl_ask.H b/FL/fl_ask.H
index a315fb1f2..c4eb3ab86 100644
--- a/FL/fl_ask.H
+++ b/FL/fl_ask.H
@@ -72,9 +72,15 @@ FL_EXPORT int fl_choice_n(const char *q, const char *b0, const char *b1, const c
FL_EXPORT Fl_String fl_input_str(int maxchar, const char *label, const char *deflt = 0, ...)
__fl_attr((__format__(__printf__, 2, 4)));
+FL_EXPORT Fl_String fl_input_str(int &ret, int maxchar, const char *label, const char *deflt = 0, ...)
+ __fl_attr((__format__(__printf__, 3, 5)));
+
FL_EXPORT Fl_String fl_password_str(int maxchar, const char *label, const char *deflt = 0, ...)
__fl_attr((__format__(__printf__, 2, 4)));
+FL_EXPORT Fl_String fl_password_str(int &ret, int maxchar, const char *label, const char *deflt = 0, ...)
+ __fl_attr((__format__(__printf__, 3, 5)));
+
FL_EXPORT Fl_Widget *fl_message_icon();
extern FL_EXPORT Fl_Font fl_message_font_;
extern FL_EXPORT Fl_Fontsize fl_message_size_;
diff --git a/fluid/ExternalCodeEditor_UNIX.cxx b/fluid/ExternalCodeEditor_UNIX.cxx
index e1e2c0e60..a669d8188 100644
--- a/fluid/ExternalCodeEditor_UNIX.cxx
+++ b/fluid/ExternalCodeEditor_UNIX.cxx
@@ -299,7 +299,7 @@ const char* ExternalCodeEditor::tmp_filename() {
static char path[FL_PATH_MAX+1];
const char *tmpdir = create_tmpdir();
if ( !tmpdir ) return 0;
- const char *ext = g_project.code_file_name; // e.g. ".cxx"
+ const char *ext = g_project.code_file_name.c_str(); // e.g. ".cxx"
snprintf(path, FL_PATH_MAX, "%s/%p%s", tmpdir, (void*)this, ext);
path[FL_PATH_MAX] = 0;
return path;
@@ -385,7 +385,7 @@ int ExternalCodeEditor::start_editor(const char *editor_cmd,
editor_cmd, filename);
char cmd[1024];
snprintf(cmd, sizeof(cmd), "%s %s", editor_cmd, filename);
- command_line_.value(editor_cmd);
+ command_line_ = editor_cmd;
open_alert_pipe();
// Fork editor to background..
switch ( pid_ = fork() ) {
@@ -559,7 +559,7 @@ void ExternalCodeEditor::alert_pipe_cb(FL_SOCKET s, void* d) {
self->last_error_ = 0;
if (::read(s, &self->last_error_, sizeof(int)) != sizeof(int))
return;
- const char* cmd = self->command_line_.value();
+ const char* cmd = self->command_line_.c_str();
if (cmd && *cmd) {
if (cmd[0] == '/') { // is this an absoluet filename?
fl_alert("Can't launch external editor '%s':\n%s\n\ncmd: \"%s\"",
diff --git a/fluid/ExternalCodeEditor_UNIX.h b/fluid/ExternalCodeEditor_UNIX.h
index fd3a93b7b..a619cd352 100644
--- a/fluid/ExternalCodeEditor_UNIX.h
+++ b/fluid/ExternalCodeEditor_UNIX.h
@@ -24,7 +24,7 @@ class ExternalCodeEditor {
time_t file_mtime_; // last modify time of the file (used to determine if file changed)
size_t file_size_; // last file size (used to determine if changed)
const char *filename_;
- Fd_String command_line_;
+ Fl_String command_line_;
int last_error_;
int alert_pipe_[2];
bool alert_pipe_open_;
diff --git a/fluid/ExternalCodeEditor_WIN32.cxx b/fluid/ExternalCodeEditor_WIN32.cxx
index bd334e5c0..1afb0afff 100644
--- a/fluid/ExternalCodeEditor_WIN32.cxx
+++ b/fluid/ExternalCodeEditor_WIN32.cxx
@@ -389,7 +389,7 @@ const char* ExternalCodeEditor::tmp_filename() {
static char path[512];
const char *tmpdir = create_tmpdir();
if ( !tmpdir ) return 0;
- const char *ext = g_project.code_file_name; // e.g. ".cxx"
+ const char *ext = g_project.code_file_name.c_str(); // e.g. ".cxx"
_snprintf(path, sizeof(path), "%s\\%p%s", tmpdir, (void*)this, ext);
path[sizeof(path)-1] = 0;
return path;
diff --git a/fluid/Fl_Menu_Type.cxx b/fluid/Fl_Menu_Type.cxx
index 9d88f56c8..2f89c9572 100644
--- a/fluid/Fl_Menu_Type.cxx
+++ b/fluid/Fl_Menu_Type.cxx
@@ -381,7 +381,7 @@ void Fl_Menu_Item_Type::write_item(Fd_Code_Writer& f) {
switch (g_project.i18n_type) {
case 1:
// we will call i18n when the menu is instantiated for the first time
- f.write_c("%s(", g_project.i18n_static_function.value());
+ f.write_c("%s(", g_project.i18n_static_function.c_str());
f.write_cstring(label());
f.write_c(")");
break;
@@ -482,11 +482,11 @@ void Fl_Menu_Item_Type::write_code1(Fd_Code_Writer& f) {
f.write_c("%sml->labelb = o->label();\n", f.indent());
} else if (g_project.i18n_type==1) {
f.write_c("%sml->labelb = %s(o->label());\n",
- f.indent(), g_project.i18n_function.value());
+ f.indent(), g_project.i18n_function.c_str());
} else if (g_project.i18n_type==2) {
f.write_c("%sml->labelb = catgets(%s,%s,i+%d,o->label());\n",
- f.indent(), g_project.i18n_file[0] ? g_project.i18n_file.value() : "_catalog",
- g_project.i18n_set.value(), msgnum());
+ f.indent(), g_project.i18n_file[0] ? g_project.i18n_file.c_str() : "_catalog",
+ g_project.i18n_set.c_str(), msgnum());
}
f.write_c("%sml->typea = FL_IMAGE_LABEL;\n", f.indent());
f.write_c("%sml->typeb = FL_NORMAL_LABEL;\n", f.indent());
@@ -504,11 +504,11 @@ void Fl_Menu_Item_Type::write_code1(Fd_Code_Writer& f) {
start_menu_initialiser(f, menuItemInitialized, mname, i);
if (g_project.i18n_type==1) {
f.write_c("%so->label(%s(o->label()));\n",
- f.indent(), g_project.i18n_function.value());
+ f.indent(), g_project.i18n_function.c_str());
} else if (g_project.i18n_type==2) {
f.write_c("%so->label(catgets(%s,%s,i+%d,o->label()));\n",
- f.indent(), g_project.i18n_file[0] ? g_project.i18n_file.value() : "_catalog",
- g_project.i18n_set.value(), msgnum());
+ f.indent(), g_project.i18n_file[0] ? g_project.i18n_file.c_str() : "_catalog",
+ g_project.i18n_set.c_str(), msgnum());
}
}
}
diff --git a/fluid/Fl_Widget_Type.cxx b/fluid/Fl_Widget_Type.cxx
index afdb61823..3fb265975 100644
--- a/fluid/Fl_Widget_Type.cxx
+++ b/fluid/Fl_Widget_Type.cxx
@@ -2917,13 +2917,13 @@ void Fl_Widget_Type::write_code1(Fd_Code_Writer& f) {
f.write_cstring(label());
break;
case 1 : /* GNU gettext */
- f.write_c("%s(", g_project.i18n_function.value());
+ f.write_c("%s(", g_project.i18n_function.c_str());
f.write_cstring(label());
f.write_c(")");
break;
case 2 : /* POSIX catgets */
- f.write_c("catgets(%s,%s,%d,", g_project.i18n_file[0] ? g_project.i18n_file.value() : "_catalog",
- g_project.i18n_set.value(), msgnum());
+ f.write_c("catgets(%s,%s,%d,", g_project.i18n_file[0] ? g_project.i18n_file.c_str() : "_catalog",
+ g_project.i18n_set.c_str(), msgnum());
f.write_cstring(label());
f.write_c(")");
break;
@@ -2990,13 +2990,13 @@ void Fl_Widget_Type::write_widget_code(Fd_Code_Writer& f) {
f.write_cstring(tooltip());
break;
case 1 : /* GNU gettext */
- f.write_c("%s(", g_project.i18n_function.value());
+ f.write_c("%s(", g_project.i18n_function.c_str());
f.write_cstring(tooltip());
f.write_c(")");
break;
case 2 : /* POSIX catgets */
- f.write_c("catgets(%s,%s,%d,", g_project.i18n_file[0] ? g_project.i18n_file.value() : "_catalog",
- g_project.i18n_set.value(), msgnum() + 1);
+ f.write_c("catgets(%s,%s,%d,", g_project.i18n_file[0] ? g_project.i18n_file.c_str() : "_catalog",
+ g_project.i18n_set.c_str(), msgnum() + 1);
f.write_cstring(tooltip());
f.write_c(")");
break;
diff --git a/fluid/Fl_Window_Type.cxx b/fluid/Fl_Window_Type.cxx
index 90a573288..78c66cbde 100644
--- a/fluid/Fl_Window_Type.cxx
+++ b/fluid/Fl_Window_Type.cxx
@@ -196,15 +196,15 @@ void show_project_cb(Fl_Widget *, void *) {
use_FL_COMMAND_button->value(g_project.use_FL_COMMAND);
utf8_in_src_button->value(g_project.utf8_in_src);
avoid_early_includes_button->value(g_project.avoid_early_includes);
- header_file_input->value(g_project.header_file_name);
- code_file_input->value(g_project.code_file_name);
+ header_file_input->value(g_project.header_file_name.c_str());
+ code_file_input->value(g_project.code_file_name.c_str());
i18n_type_chooser->value(g_project.i18n_type);
- i18n_function_input->value(g_project.i18n_function);
- i18n_static_function_input->value(g_project.i18n_static_function);
- i18n_file_input->value(g_project.i18n_file);
- i18n_set_input->value(g_project.i18n_set);
- i18n_include_input->value(g_project.i18n_include);
- i18n_conditional_input->value(g_project.i18n_conditional);
+ i18n_function_input->value(g_project.i18n_function.c_str());
+ i18n_static_function_input->value(g_project.i18n_static_function.c_str());
+ i18n_file_input->value(g_project.i18n_file.c_str());
+ i18n_set_input->value(g_project.i18n_set.c_str());
+ i18n_include_input->value(g_project.i18n_include.c_str());
+ i18n_conditional_input->value(g_project.i18n_conditional.c_str());
switch (g_project.i18n_type) {
case 0 : /* None */
i18n_include_input->hide();
@@ -258,12 +258,12 @@ void show_settings_cb(Fl_Widget *, void *) {
}
void header_input_cb(Fl_Input* i, void*) {
- if (strcmp(g_project.header_file_name, i->value()))
+ if (strcmp(g_project.header_file_name.c_str(), i->value()))
set_modflag(1);
g_project.header_file_name = i->value();
}
void code_input_cb(Fl_Input* i, void*) {
- if (strcmp(g_project.code_file_name, i->value()))
+ if (strcmp(g_project.code_file_name.c_str(), i->value()))
set_modflag(1);
g_project.code_file_name = i->value();
}
diff --git a/fluid/code.cxx b/fluid/code.cxx
index 378cb1303..f9f7a62df 100644
--- a/fluid/code.cxx
+++ b/fluid/code.cxx
@@ -134,7 +134,7 @@ int write_strings(const char *sfile) {
case 2 : /* POSIX catgets, put a .msg file out */
fprintf(fp, "$ generated by Fast Light User Interface Designer (fluid) version %.4f\n",
FL_VERSION);
- fprintf(fp, "$set %s\n", g_project.i18n_set.value());
+ fprintf(fp, "$set %s\n", g_project.i18n_set.c_str());
fputs("$quote \"\n", fp);
for (i = 1, p = Fl_Type::first; p; p = p->next) {
@@ -768,7 +768,7 @@ int Fd_Code_Writer::write_code(const char *s, const char *t, bool to_sourceview)
if (t && g_project.include_H_from_C) {
if (to_sourceview) {
write_c("#include \"CodeView.h\"\n");
- } else if (g_project.header_file_name[0] == '.' && strchr(g_project.header_file_name, '/') == NULL) {
+ } else if (g_project.header_file_name[0] == '.' && strchr(g_project.header_file_name.c_str(), '/') == NULL) {
write_c("#include \"%s\"\n", fl_filename_name(t));
} else {
write_c("#include \"%s\"\n", t);
@@ -777,30 +777,30 @@ int Fd_Code_Writer::write_code(const char *s, const char *t, bool to_sourceview)
if (g_project.i18n_type && g_project.i18n_include[0]) {
int conditional = (g_project.i18n_conditional[0]!=0);
if (conditional) {
- write_c("#ifdef %s\n", g_project.i18n_conditional.value());
+ write_c("#ifdef %s\n", g_project.i18n_conditional.c_str());
indentation++;
}
if (g_project.i18n_include[0] != '<' &&
g_project.i18n_include[0] != '\"')
- write_c("#%sinclude \"%s\"\n", indent(), g_project.i18n_include.value());
+ write_c("#%sinclude \"%s\"\n", indent(), g_project.i18n_include.c_str());
else
- write_c("#%sinclude %s\n", indent(), g_project.i18n_include.value());
+ write_c("#%sinclude %s\n", indent(), g_project.i18n_include.c_str());
if (g_project.i18n_type == 2) {
if (g_project.i18n_file[0]) {
- write_c("extern nl_catd %s;\n", g_project.i18n_file.value());
+ write_c("extern nl_catd %s;\n", g_project.i18n_file.c_str());
} else {
write_c("// Initialize I18N stuff now for menus...\n");
write_c("#%sinclude <locale.h>\n", indent());
write_c("static char *_locale = setlocale(LC_MESSAGES, \"\");\n");
- write_c("static nl_catd _catalog = catopen(\"%s\", 0);\n", g_project.i18n_program.value());
+ write_c("static nl_catd _catalog = catopen(\"%s\", 0);\n", g_project.i18n_program.c_str());
}
}
if (conditional) {
write_c("#else\n");
if (g_project.i18n_type == 1) {
if (g_project.i18n_function[0]) {
- write_c("#%sifndef %s\n", indent(), g_project.i18n_function.value());
- write_c("#%sdefine %s(text) text\n", indent_plus(1), g_project.i18n_function.value());
+ write_c("#%sifndef %s\n", indent(), g_project.i18n_function.c_str());
+ write_c("#%sdefine %s(text) text\n", indent_plus(1), g_project.i18n_function.c_str());
write_c("#%sendif\n", indent());
}
}
@@ -813,8 +813,8 @@ int Fd_Code_Writer::write_code(const char *s, const char *t, bool to_sourceview)
write_c("#endif\n");
}
if (g_project.i18n_type == 1 && g_project.i18n_static_function[0]) {
- write_c("#ifndef %s\n", g_project.i18n_static_function.value());
- write_c("#%sdefine %s(text) text\n", indent_plus(1), g_project.i18n_static_function.value());
+ write_c("#ifndef %s\n", g_project.i18n_static_function.c_str());
+ write_c("#%sdefine %s(text) text\n", indent_plus(1), g_project.i18n_static_function.c_str());
write_c("#endif\n");
}
}
diff --git a/fluid/file.cxx b/fluid/file.cxx
index 3d2d8745f..db59a2733 100644
--- a/fluid/file.cxx
+++ b/fluid/file.cxx
@@ -771,26 +771,26 @@ int Fd_Project_Writer::write_project(const char *filename, int selected_only) {
write_string("\navoid_early_includes");
if (g_project.i18n_type) {
write_string("\ni18n_type %d", g_project.i18n_type);
- write_string("\ni18n_include"); write_word(g_project.i18n_include);
- write_string("\ni18n_conditional"); write_word(g_project.i18n_conditional);
+ write_string("\ni18n_include"); write_word(g_project.i18n_include.c_str());
+ write_string("\ni18n_conditional"); write_word(g_project.i18n_conditional.c_str());
switch (g_project.i18n_type) {
case 1 : /* GNU gettext */
- write_string("\ni18n_function"); write_word(g_project.i18n_function);
- write_string("\ni18n_static_function"); write_word(g_project.i18n_static_function);
+ write_string("\ni18n_function"); write_word(g_project.i18n_function.c_str());
+ write_string("\ni18n_static_function"); write_word(g_project.i18n_static_function.c_str());
break;
case 2 : /* POSIX catgets */
if (g_project.i18n_file[0]) {
write_string("\ni18n_file");
- write_word(g_project.i18n_file);
+ write_word(g_project.i18n_file.c_str());
}
- write_string("\ni18n_set"); write_word(g_project.i18n_set);
+ write_string("\ni18n_set"); write_word(g_project.i18n_set.c_str());
break;
}
}
if (!selected_only) {
- write_string("\nheader_name"); write_word(g_project.header_file_name);
- write_string("\ncode_name"); write_word(g_project.code_file_name);
+ write_string("\nheader_name"); write_word(g_project.header_file_name.c_str());
+ write_string("\ncode_name"); write_word(g_project.code_file_name.c_str());
#if 0
// https://github.com/fltk/fltk/issues/328
diff --git a/fluid/fluid.cxx b/fluid/fluid.cxx
index 56fc38b4d..7512e7bc8 100644
--- a/fluid/fluid.cxx
+++ b/fluid/fluid.cxx
@@ -164,8 +164,8 @@ int compile_strings = 0; // fluic -cs
int batch_mode = 0; // if set (-c, -u) don't open display
/// command line arguments override settings in the projectfile
-Fd_String g_code_filename_arg;
-Fd_String g_header_filename_arg;
+Fl_String g_code_filename_arg;
+Fl_String g_header_filename_arg;
/** \var int Fluid_Project::header_file_set
If set, commandline overrides header file name in .fl file.
@@ -1001,19 +1001,20 @@ int write_code_files() {
char cname[FL_PATH_MAX+1];
char hname[FL_PATH_MAX+1];
g_project.i18n_program = fl_filename_name(filename);
- g_project.i18n_program.capacity(FL_PATH_MAX);
- fl_filename_setext(g_project.i18n_program.buffer(), FL_PATH_MAX, "");
- if (g_project.code_file_name[0] == '.' && strchr(g_project.code_file_name, '/') == NULL) {
+ g_project.i18n_program.resize(FL_PATH_MAX);
+ fl_filename_setext(g_project.i18n_program.data(), FL_PATH_MAX, "");
+ g_project.i18n_program.resize(g_project.i18n_program.strlen());
+ if (g_project.code_file_name[0] == '.' && strchr(g_project.code_file_name.c_str(), '/') == NULL) {
strlcpy(cname, fl_filename_name(filename), FL_PATH_MAX);
- fl_filename_setext(cname, FL_PATH_MAX, g_project.code_file_name);
+ fl_filename_setext(cname, FL_PATH_MAX, g_project.code_file_name.c_str());
} else {
- strlcpy(cname, g_project.code_file_name, FL_PATH_MAX);
+ strlcpy(cname, g_project.code_file_name.c_str(), FL_PATH_MAX);
}
- if (g_project.header_file_name[0] == '.' && strchr(g_project.header_file_name, '/') == NULL) {
+ if (g_project.header_file_name[0] == '.' && strchr(g_project.header_file_name.c_str(), '/') == NULL) {
strlcpy(hname, fl_filename_name(filename), FL_PATH_MAX);
- fl_filename_setext(hname, FL_PATH_MAX, g_project.header_file_name);
+ fl_filename_setext(hname, FL_PATH_MAX, g_project.header_file_name.c_str());
} else {
- strlcpy(hname, g_project.header_file_name, FL_PATH_MAX);
+ strlcpy(hname, g_project.header_file_name.c_str(), FL_PATH_MAX);
}
if (!batch_mode) enter_project_dir();
Fd_Code_Writer f;
@@ -1805,7 +1806,7 @@ void set_modflag(int mf, int mfc) {
#endif // _WIN32
else basename = filename;
- code_ext = fl_filename_ext(g_project.code_file_name);
+ code_ext = fl_filename_ext(g_project.code_file_name.c_str());
char mod_star = modflag ? '*' : ' ';
char mod_c_star = modflag_c ? '*' : ' ';
snprintf(title, sizeof(title), "%s%c %s%c",
@@ -1927,11 +1928,12 @@ void update_sourceview_cb(Fl_Button*, void*)
sv_strings->scroll(top, 0);
} else if (sv_source->visible_r() || sv_header->visible_r()) {
g_project.i18n_program = fl_filename_name(sv_source_filename);
- g_project.i18n_program.capacity(FL_PATH_MAX);
- fl_filename_setext(g_project.i18n_program.buffer(), FL_PATH_MAX, "");
- Fd_String code_file_name_bak = g_project.code_file_name;
+ g_project.i18n_program.resize(FL_PATH_MAX);
+ fl_filename_setext(g_project.i18n_program.data(), FL_PATH_MAX, "");
+ g_project.i18n_program.resize(g_project.i18n_program.strlen());
+ Fl_String code_file_name_bak = g_project.code_file_name;
g_project.code_file_name = sv_source_filename;
- Fd_String header_file_name_bak = g_project.header_file_name;
+ Fl_String header_file_name_bak = g_project.header_file_name;
g_project.header_file_name = sv_header_filename;
// generate the code and load the files
diff --git a/fluid/fluid.h b/fluid/fluid.h
index c7b800849..7d2a3ade2 100644
--- a/fluid/fluid.h
+++ b/fluid/fluid.h
@@ -78,18 +78,6 @@ extern int batch_mode;
extern int pasteoffset;
-// ---- string handling
-
-class Fd_String : public Fl_String
-{
-public:
- Fd_String() : Fl_String("") { }
- Fd_String(const char* s) : Fl_String(s) { }
- int empty() { return size()==0; }
- void operator=(const char* s) { value(s); }
- operator const char* () const { return value(); }
-};
-
// ---- project settings
class Fluid_Project {
@@ -99,27 +87,27 @@ public:
void reset();
int i18n_type;
- Fd_String i18n_include;
- Fd_String i18n_conditional;
- Fd_String i18n_function;
- Fd_String i18n_static_function;
- Fd_String i18n_file;
- Fd_String i18n_set;
- Fd_String i18n_program;
+ Fl_String i18n_include;
+ Fl_String i18n_conditional;
+ Fl_String i18n_function;
+ Fl_String i18n_static_function;
+ Fl_String i18n_file;
+ Fl_String i18n_set;
+ Fl_String i18n_program;
int include_H_from_C;
int use_FL_COMMAND;
int utf8_in_src;
int avoid_early_includes;
int header_file_set;
int code_file_set;
- Fd_String header_file_name;
- Fd_String code_file_name;
+ Fl_String header_file_name;
+ Fl_String code_file_name;
};
extern Fluid_Project g_project;
-extern Fd_String g_code_filename_arg;
-extern Fd_String g_header_filename_arg;
+extern Fl_String g_code_filename_arg;
+extern Fl_String g_header_filename_arg;
// ---- public functions
diff --git a/fluid/widget_panel.fl b/fluid/widget_panel.fl
index afd6bb62a..15f28e24b 100644
--- a/fluid/widget_panel.fl
+++ b/fluid/widget_panel.fl
@@ -66,7 +66,7 @@ Use Ctrl-J for newlines.} xywh {95 40 190 20} labelfont 1 labelsize 11 when 15 t
xywh {95 65 309 20} labelfont 1 labelsize 11 align 4
} {
Fl_Input {} {
- callback image_cb
+ callback image_cb selected
tooltip {The active image for the widget.} xywh {95 65 200 20} labelfont 1 labelsize 11 textsize 11 resizable
}
Fl_Button {} {
@@ -471,7 +471,7 @@ h, ph, sh, ch, and i} xywh {275 150 55 20} labelsize 11 align 5 textsize 11
} {}
Fl_Button {} {
callback shortcut_in_cb
- comment {This is a special button that grabs keystrokes directly} selected
+ comment {This is a special button that grabs keystrokes directly}
tooltip {The shortcut key for the widget.
Use 'Backspace' key to clear.} xywh {95 210 310 20} box DOWN_BOX color 7 selection_color 12 labelsize 11 when 1
code0 {\#include <FL/Fl_Shortcut_Button.H>}
diff --git a/src/Fl_String.cxx b/src/Fl_String.cxx
index e07f23a94..222e34583 100644
--- a/src/Fl_String.cxx
+++ b/src/Fl_String.cxx
@@ -17,137 +17,520 @@
#include <FL/Fl_String.H>
#include <stdio.h>
+#include <stdlib.h>
#include <string.h>
+#include <limits.h>
+
/** \file src/Fl_String.cxx
- Basic Fl_String class for FLTK.
-*/
+ Basic Fl_String class for FLTK.
+ */
+
+
+/*
+ If buffer_ is NULL, c_str() and buffer() will point here.
+ */
+const char Fl_String::NUL = 0;
+
+/**
+ Indicate a maximum value or error.
+ This value is generally used as end of string indicator or as the error
+ indicator by the functions that return a string index.
+ */
+const int Fl_String::npos = INT_MAX;
+
+/**
+ Initialise the class instance.
+ */
+void Fl_String::init_() {
+ buffer_ = NULL;
+ size_ = 0;
+ capacity_ = 0;
+}
+
+/**
+ Grow the buffer to a capacity of at least n bytes.
+
+ This method will always grow the buffer size, or keep it as is, but never
+ shrink it. Use shrink_to_fit() to shrink the buffer as much as possible.
+
+ \param[in] n number of bytes needed, not counting the trailing NUL
+ */
+void Fl_String::grow_(int n) {
+ if (n <= capacity_)
+ return;
+ int alloc_size_ = n + 1; // trailing NUL
+ // round n up so we can grow in chunks
+ if (alloc_size_ <= 24) { // allocate at least 24 bytes
+ alloc_size_ = 24;
+ } else if (alloc_size_ < 1024) {
+ alloc_size_ = (alloc_size_+128) & ~127; // allocate in 128 byte chunks
+ } else {
+ alloc_size_ = (alloc_size_+2048) & ~2047; // allocate in 2k chunks
+ }
+ // allocate now
+ char *new_buffer = (char*)::malloc(alloc_size_);
+ if (buffer_ && (size_ > 0)) {
+ memcpy(new_buffer, buffer_, size_);
+ ::free(buffer_);
+ }
+ if (size_ >= 0)
+ new_buffer[size_] = 0; // trailing NUL
+ buffer_ = new_buffer;
+ capacity_ = alloc_size_-1; // trailing NUL
+}
-/** Constructs an empty string */
+/**
+ Shrink the buffer to n bytes, or size, if size > n.
+
+ Shrink the buffer as much as possible. If \p n is 0 and the string is empty,
+ the buffer will be released.
+
+ \param[in] n shrink buffer to n bytes, not counting the trailing NUL
+ */
+void Fl_String::shrink_(int n) {
+ if (n < size_)
+ n = size_;
+ if (n == capacity_)
+ return;
+ if (n == 0) {
+ if (buffer_)
+ ::free(buffer_);
+ buffer_ = NULL;
+ } else {
+ buffer_ = (char*)::realloc(buffer_, n+1); // NUL
+ buffer_[size_] = 0; // trailing NUL
+ }
+ capacity_ = n;
+}
+
+/**
+ Remove \p n_del bytes at \p at and insert \p n_ins bytes from \p src.
+
+ String will remain NUL terminated. Data in \p ins may contain NULs.
+
+ \param[in] at remove and insert bytes at this index
+ \param[in] n_del number of bytes to remove
+ \param[in] ins insert bytes from here, can be NULL if \p n_ins is also 0
+ \param[in] n_ins number of bytes to insert
+ \return self
+ */
+Fl_String &Fl_String::replace_(int at, int n_del, const char *ins, int n_ins) {
+ if (at > size_) at = size_;
+ if (n_del > size_ - at) n_del = size_ - at;
+ int diff = n_ins - n_del, new_size = size_ + diff;
+ if (diff) {
+ int src = at + n_del, dst = at + n_ins, n = size_ - src;
+ grow_(new_size);
+ if (n > 0) ::memmove(buffer_+dst, buffer_+src, n);
+ }
+ if (n_ins > 0) {
+ ::memmove(buffer_+at, ins, n_ins);
+ }
+ size_ = new_size;
+ buffer_[size_] = 0;
+ return *this;
+}
+
+
+// ---- Assignment ----------------------------------------------------- MARK: -
+
+/**
+ Allocate an empty string.
+ */
Fl_String::Fl_String() {
- init();
+ init_();
+}
+
+/**
+ Copy constructor.
+ \param[in] str copy from another Fl_String
+ */
+Fl_String::Fl_String(const Fl_String &str) {
+ init_();
+ assign(str);
}
-/** Constructor from a C-style string */
-Fl_String::Fl_String(const char *str) {
- init();
- value(str);
+/**
+ Constructor from a C-style string.
+ \param[in] cstr a NUL terminated C-style string
+ */
+Fl_String::Fl_String(const char *cstr) {
+ init_();
+ assign(cstr);
}
-/** Constructor from a buffer of \c size bytes */
+/**
+ Constructor from data of \p size bytes.
+ \param[in] str a block of data that may contain NUL characters
+ \paran[in] size number of bytes to copy
+ */
Fl_String::Fl_String(const char *str, int size) {
- init();
- value(str, size);
+ init_();
+ assign(str, size);
}
-void Fl_String::init() {
- size_ = 0;
- value_ = 0;
- capacity_ = 0;
+/**
+ Destructor.
+ */
+Fl_String::~Fl_String() {
+ if (buffer_)
+ ::free(buffer_);
}
-/** copy constructor */
-Fl_String::Fl_String(const Fl_String &in) {
- init();
- value(in.value(), in.size());
+/**
+ Copy assignment operator
+ \param[in] str copy from another Fl_String
+ \return self
+ */
+Fl_String &Fl_String::operator=(const Fl_String &str) {
+ return assign(str);
}
-/** copy assignment operator */
-Fl_String& Fl_String::operator=(const Fl_String &in) {
- if (this == &in)
- return *this;
- value(in.value(), in.size());
- // debug("copy assigned");
+/**
+ Assign a C-style string.
+ \param[in] cstr a NUL terminated C-style string
+ \return self
+ */
+Fl_String &Fl_String::operator=(const char *cstr) {
+ return assign(cstr);
+}
+
+/**
+ Copy another string.
+ \param[in] str copy from another Fl_String
+ \return self
+ */
+Fl_String &Fl_String::assign(const Fl_String &str) {
+ if (&str == this) return *this;
+ return assign(str.data(), str.size());
+}
+
+/**
+ Assign a C-style string.
+ \param[in] cstr a NUL terminated C-style string
+ \return self
+ */
+Fl_String &Fl_String::assign(const char *cstr) {
+ if (cstr && *cstr) {
+ int len = (int)::strlen(cstr);
+ return assign(cstr, len);
+ } else {
+ resize(0);
+ }
return *this;
}
-/** assignment operator for 'const char *' */
-Fl_String& Fl_String::operator=(const char *in) {
- value(in);
- // debug("*STRING* assigned");
+/**
+ Assign a data block of \p size bytes.
+ \param[in] str a block of data that may contain NUL characters
+ \paran[in] size number of bytes to copy
+ \return self
+ */
+Fl_String &Fl_String::assign(const char *str, int size) {
+ if (size > 0) {
+ grow_(size);
+ memcpy(buffer_, str, size);
+ buffer_[size] = 0;
+ size_ = size;
+ } else {
+ resize(0);
+ }
return *this;
}
-/** Destructor */
-Fl_String::~Fl_String() {
- delete[] value_;
+// ---- Element Access ------------------------------------------------- MARK: -
+
+/**
+ Returns the character at specified bounds checked location.
+ \param[in] n index of character
+ \return character at that index, or NUL if out of bounds
+ */
+char Fl_String::at(int n) const {
+ if ((n < 0) || (n >= size_))
+ return 0;
+ return operator[](n);
}
-/** Grow the buffer size to at least size+1 bytes.
- By default, this call destroys the contents of the current buffer.
- \param size in bytes
- \param preserve_text copy existing text into the new buffer
+/**
+ Returns the character at specified location.
+ \param[in] n index of character
+ \return character at that index
*/
-void Fl_String::alloc_buf(int size, bool preserve_text) {
- if (size < 0)
- return;
- if (size > 0 && size <= capacity_)
- return;
+char Fl_String::operator[](int n) const {
+ if (buffer_)
+ return buffer_[n];
+ else
+ return 0;
+}
- int new_size = (size + 1 + 15) & (~15); // round upwards
- char *new_value = new char[new_size];
- capacity_ = new_size - 1;
+/**
+ Returns a reference to the character at specified location.
+ \param[in] n index of character
+ \return reference to that character, so it can be used as lvalue
+ */
+char &Fl_String::operator[](int n) {
+ if (!buffer_)
+ reserve(1);
+ return buffer_[n];
+}
- if (preserve_text) {
- size_ = (int)strlen(value_);
- // the new buffer always has a higher capacity than the old one
- // make sure we copy the trailing NUL.
- memcpy(new_value, value_, size_+1);
- } else {
- size_ = 0;
- }
- delete[] value_;
- value_ = new_value;
+/**
+ Return a pointer to the NUL terminated string.
+ \return reference to non-mutable string
+ */
+const char *Fl_String::data() const {
+ if (buffer_)
+ return buffer_;
+ else
+ return &NUL;
}
-/** Assigns the string value to a C-style string */
-void Fl_String::value(const char *str) {
- value(str, str ? (int)strlen(str) : 0);
+/**
+ Return a pointer to the writable NUL terminated string.
+ \return reference to mutable string
+ */
+char *Fl_String::data() {
+ if (!buffer_)
+ reserve(1);
+ return buffer_;
}
-/** Returns the number of non-null bytes in the object */
-int Fl_String::slen() const {
- if (!value_) return 0;
- return (int)strlen(value_);
+/**
+ Return a pointer to the NUL terminated string.
+ \return reference to non-mutable string
+ \note same as `const char *Fl_String::data() const`
+ */
+const char *Fl_String::c_str() const {
+ return data();
}
-/** Assigns the string value to a buffer of \c len bytes */
-void Fl_String::value(const char *str, int len) {
- if (str) {
- alloc_buf(len);
- size_ = len;
- memcpy(value_, str, size_);
- value_[size_] = '\0';
- } else { // str == NULL
- size_ = 0; // ignore len !
- delete[] value_; // free buffer
- value_ = NULL; // set null pointer (!)
- capacity_ = 0; // reset capacity
- }
+// ---- Capacity ------------------------------------------------------- MARK: -
+
+/**
+ Checks if the string is empty.
+ \return true if string contains no data
+ */
+bool Fl_String::empty() const {
+ return (size_ == 0);
}
-/** Returns the minimum capacity of the object */
+/**
+ Returns the number of bytes in the string.
+ \return number of bytes in string, not counting trailing NUL
+ */
+int Fl_String::size() const {
+ return size_;
+}
+
+/**
+ Reserve n bytes for storage.
+ If n is less or equal than size, the capacity is set to size.
+ \param[in] n requested minimum size, not counting trailing NUL
+ */
+void Fl_String::reserve(int n) {
+ grow_(n);
+}
+
+/**
+ Return the number of chars that are allocated for storage.
+ \return string capacity, not counting trailing NUL
+ */
int Fl_String::capacity() const {
- return capacity_; // > 0 ? capacity_ - 1 : capacity_;
+ return capacity_;
}
-/** Set the minimum capacity to \c num_bytes plus one for a terminating NUL.
- The contents of the string buffer will be copied if needed.
- \param num_bytes minimum size of buffer
+/**
+ Shrink the capacity to fit the current size.
*/
-void Fl_String::capacity(int num_bytes) {
- alloc_buf(num_bytes, true);
+void Fl_String::shrink_to_fit() {
+ shrink_(size_);
}
+// ---- Operations ----------------------------------------------------- MARK: -
-void Fl_String::release() {
- delete[] value_;
- value_ = 0;
- size_ = 0;
- capacity_ = 0;
+/**
+ Set an empty string.
+ */
+void Fl_String::clear() {
+ resize(0);
+}
+
+/**
+ Insert a C-style string or data.
+ \param[in] at insert at this index
+ \param[in] src copy bytes from here
+ \param[in] n_ins optional number of bytes to copy - if not set, copy C-style string
+ \return self
+ */
+Fl_String &Fl_String::insert(int at, const char *src, int n_ins) {
+ if (n_ins == npos) n_ins = src ? (int)::strlen(src) : 0;
+ return replace_(at, 0, src, n_ins);
+}
+
+/**
+ Insert another string.
+ \param[in] at insert at this index
+ \param[in] src copy string from here
+ \return self
+ */
+Fl_String &Fl_String::insert(int at, const Fl_String &src) {
+ return replace_(at, 0, src.buffer_, src.size_);
+}
+
+/**
+ Erase some bytes within a string.
+ \param[in] at erase at this index
+ \param[in] n_del number of bytes to erase
+ \return self
+ */
+Fl_String &Fl_String::erase(int at, int n_del) {
+ return replace_(at, n_del, NULL, 0);
+}
+
+/**
+ Append a single character.
+ \param[in] c append this byte
+ */
+void Fl_String::push_back(char c) {
+ replace_(size_, 0, &c, 1);
+}
+
+/**
+ Remove the last character.
+ */
+void Fl_String::pop_back() {
+ replace_(size_-1, 1, NULL, 0);
+}
+
+/**
+ Append a C-style string or data.
+ \param[in] src copy bytes from here
+ \param[in] n_ins optional number of bytes to copy - if not set, copy C-style string
+ \return self
+ */
+Fl_String &Fl_String::append(const char *src, int n_ins) {
+ if (n_ins == npos) n_ins = src ? (int)::strlen(src) : 0;
+ return replace_(size_, 0, src, n_ins);
+}
+
+/**
+ Append another string.
+ \param[in] src copy string from here
+ \return self
+ */
+Fl_String &Fl_String::append(const Fl_String &src) {
+ return replace_(size_, 0, src.buffer_, src.size_);
+}
+
+/**
+ Append a single byte.
+ \param[in] c single byte character
+ \return self
+ */
+Fl_String &Fl_String::append(char c) {
+ push_back(c);
+ return *this;
+}
+
+/**
+ Append a C-style string or data.
+ \param[in] src copy C-style string from here
+ \return self
+ */
+Fl_String &Fl_String::operator+=(const char *src) {
+ return append(src);
+}
+
+/**
+ Append another string.
+ \param[in] src copy string from here
+ \return self
+ */
+Fl_String &Fl_String::operator+=(const Fl_String &src) {
+ return append(src);
+}
+
+/**
+ Append a single byte.
+ \param[in] c single byte character
+ \return self
+ */
+Fl_String &Fl_String::operator+=(char c) {
+ return append(c);
+}
+
+/**
+ Replace part of the string with a C-style string or data.
+ \param[in] at erase and insert at this index
+ \param[in] n_del number of bytes to erase
+ \param[in] src copy bytes from here
+ \param[in] n_ins optional number of bytes to copy - if not set, copy C-style string
+ \return self
+ */
+Fl_String &Fl_String::replace(int at, int n_del, const char *src, int n_ins) {
+ if (n_ins == npos) n_ins = src ? (int)::strlen(src) : 0;
+ return replace_(at, n_del, src, n_ins);
+}
+
+/**
+ Replace part of the string with another string.
+ \param[in] at erase and insert at this index
+ \param[in] n_del number of bytes to erase
+ \param[in] src copy string from here
+ \return self
+ */
+Fl_String &Fl_String::replace(int at, int n_del, const Fl_String &src) {
+ return replace_(at, n_del, src.buffer_, src.size_);
+}
+
+/**
+ Return a substring from a string.
+ \param[in] pos copy string from here - if omitted, copy from start
+ \param[in] n number of bytes - if omitted, copy all bytes
+ \return a new string
+ */
+Fl_String Fl_String::substr(int pos, int n) const {
+ if (n > size_) n = size_;
+ int first = pos, last = pos + n;
+ if ((first < 0) || (first > size_) || (last <= first))
+ return Fl_String();
+ if (last > size_) last = size_;
+ return Fl_String(buffer_+first, last-first);
+}
+
+/**
+ Resizes the string to n characters.
+
+ If \p n is less than the current size, the string will be cropped. If \p n
+ is more than the current size, the new space will be filled with
+ NUL characters.
+
+ \param[in] n new size of string
+ */
+void Fl_String::resize(int n) {
+ if (n == size_)
+ return;
+ if (n < size_) {
+ size_ = n;
+ if (buffer_) buffer_[size_] = 0;
+ } else {
+ grow_(n);
+ if (buffer_) ::memset(buffer_+size_, 0, n-size_+1);
+ }
+ size_ = n;
}
-// ============================= DEBUG =============================
+// --- Non Standard ---------------------------------------------------- MARK: -
+
+/**
+ Returns the number of bytes until the first NUL byte.
+ \return number of bytes in C-style string
+ */
+int Fl_String::strlen() const {
+ if (!buffer_) return 0;
+ return (int)::strlen(buffer_);
+}
/**
Write some details about the string to stdout.
@@ -163,7 +546,7 @@ void Fl_String::release() {
void Fl_String::debug(const char *info) const {
if (info) {
printf("Fl_String '%-20s': %p, value = %p (%d/%d):\n%s\n",
- info, this, value_, size_, capacity_, value_ ? value_ : "<NULL>");
+ info, this, buffer_, size_, capacity_, buffer_ ? buffer_ : "<NULL>");
}
}
@@ -191,7 +574,46 @@ void Fl_String::hexdump(const char *info) const {
} else if ((i & 3) == 0) { // separator after 4 bytes
printf(" ");
}
- printf(" %02x", (unsigned char)value_[i]);
+ printf(" %02x", (unsigned char)buffer_[i]);
}
printf("\n");
}
+
+// ---- Non-member functions ------------------------------------------- MARK: -
+
+/**
+ Concatenate two strings.
+ \param[in] lhs first string
+ \param[in] rhs second string
+ \return self
+ */
+Fl_String operator+(const Fl_String &lhs, const Fl_String &rhs) {
+ Fl_String ret = lhs;
+ return ret += rhs;
+}
+
+/**
+ Concatenate two strings.
+ \param[in] lhs first string
+ \param[in] rhs second C-style string
+ \return self
+ */
+Fl_String operator+(const Fl_String &lhs, const char *rhs) {
+ Fl_String ret = lhs;
+ return ret += rhs;
+}
+
+/**
+ Compare two strings.
+ \param[in] lhs first string
+ \param[in] rhs second string
+ \return true if strings are the same size and have the same content
+ */
+bool operator==(const Fl_String &lhs, const Fl_String &rhs) {
+ if (lhs.size() == rhs.size()) {
+ int sz = lhs.size();
+ if (sz == 0) return true;
+ if (memcmp(lhs.data(), rhs.data(), sz) == 0) return true;
+ }
+ return false;
+}
diff --git a/src/fl_ask.cxx b/src/fl_ask.cxx
index 5657cd3b0..6ed6b2667 100644
--- a/src/fl_ask.cxx
+++ b/src/fl_ask.cxx
@@ -320,7 +320,7 @@ const char *fl_input(const char *fmt, const char *defstr, ...) {
/** Shows an input dialog displaying the \p fmt message with variable arguments.
- Like fl_input(), but this method has an additional (first) argument \p maxchar
+ Like fl_input(), but this method has the additional argument \p maxchar
that limits the number of \b characters that can be input. Since the
string is encoded in UTF-8 it is possible that the number of bytes
in the string is larger than \p maxchar.
@@ -329,36 +329,56 @@ const char *fl_input(const char *fmt, const char *defstr, ...) {
returns the string in an Fl_String object that must be released after use. This
can be a local/automatic variable.
+ The \p ret variable is set to 0 if the user clicked OK, and to a negative
+ value if the user canceled the dialog. If the dialog was canceled, the returned
+ string will be empty.
+
\code #include <FL/fl_ask.H> \endcode
Example:
\code
- { Fl_String str = fl_input_str(0, "Enter text:", "");
- printf("Text is: '%s'\n", str.value() ? str.value() : "<cancelled>");
+ { int ret;
+ Fl_String str = fl_input_str(ret, 0, "Enter text:", "");
+ if (ret < 0)
+ printf("Text input was canceled.\n");
+ else
+ printf("Text is: '%s'\n", str.c_str());
} // (str goes out of scope)
\endcode
- If the user hits \c Escape or closes the window \c str.value() returns NULL.
-
+ \param[out] ret 0 if user clicked OK, negative if dialog was canceled
\param[in] maxchar input size limit in characters (not bytes), use 0 for no limit
\param[in] fmt can be used as an sprintf-like format and variables for the message text
\param[in] defstr defines the default returned string if no text is entered
- \return the user string input if OK was pushed or NULL in Fl_String::value()
- \retval Fl_String::value() == NULL if Cancel was pushed or the window was closed by the user
+ \return the user string input if OK was clicked which can be empty
+ \return an empty string and set \p ret to a negative value if the user canceled the dialog
\since 1.4.0
*/
-Fl_String fl_input_str(int maxchar, const char *fmt, const char *defstr, ...) {
+Fl_String fl_input_str(int &ret, int maxchar, const char *fmt, const char *defstr, ...) {
+ Fl_Message msg("?");
+ if (maxchar < 0) maxchar = 0;
+ va_list ap;
+ va_start(ap, defstr);
+ const char *r = msg.input_innards(fmt, ap, defstr, FL_NORMAL_INPUT, maxchar);
+ va_end(ap);
+ ret = (r == NULL) ? -1 : 0;
+ return Fl_String(r);
+}
+/** Shows an input dialog displaying the \p fmt message with variable arguments.
+ \note No information is given if the user canceled the dialog or clicked OK.
+ \see fl_input_str(int &ret, int maxchar, const char *label, const char *deflt = 0, ...)
+ */
+Fl_String fl_input_str(int maxchar, const char *fmt, const char *defstr, ...) {
Fl_Message msg("?");
- if (maxchar < 0)
- maxchar = 0;
+ if (maxchar < 0) maxchar = 0;
va_list ap;
va_start(ap, defstr);
const char *r = msg.input_innards(fmt, ap, defstr, FL_NORMAL_INPUT, maxchar);
va_end(ap);
- return r; // Fl_String(r)
+ return Fl_String(r);
}
/** Shows an input dialog displaying the \p fmt message with variable arguments.
@@ -378,7 +398,6 @@ Fl_String fl_input_str(int maxchar, const char *fmt, const char *defstr, ...) {
\retval NULL if Cancel was pushed or the window was closed by the user
*/
const char *fl_password(const char *fmt, const char *defstr, ...) {
-
Fl_Message msg("?");
va_list ap;
va_start(ap, defstr);
@@ -400,27 +419,42 @@ const char *fl_password(const char *fmt, const char *defstr, ...) {
\code #include <FL/fl_ask.H> \endcode
- \param[in] maxchar input size limit in characters (not bytes); use 0 for no limit
+ \param[out] ret 0 if user clicked OK, negative if dialog was canceled
+ \param[in] maxchar input size limit in characters (not bytes), use 0 for no limit
\param[in] fmt can be used as an sprintf-like format and variables for the message text
\param[in] defstr defines the default returned string if no text is entered
- \return the user string input if OK was pushed or NULL in Fl_String::value()
- \retval Fl_String::value() == NULL if Cancel was pushed or the window was closed by the user
+ \return the user string input if OK was clicked which can be empty
+ \return an empty string and set \p ret to a negative value if the user canceled the dialog
\since 1.4.0
*/
-Fl_String fl_password_str(int maxchar, const char *fmt, const char *defstr, ...) {
+Fl_String fl_password_str(int &ret, int maxchar, const char *fmt, const char *defstr, ...) {
+ Fl_Message msg("?");
+ if (maxchar < 0) maxchar = 0;
+ va_list ap;
+ va_start(ap, defstr);
+ const char *r = msg.input_innards(fmt, ap, defstr, FL_SECRET_INPUT, maxchar);
+ va_end(ap);
+ ret = (r == NULL) ? -1 : 0;
+ return Fl_String(r);
+}
+/** Shows an input dialog displaying the \p fmt message with variable arguments.
+ \note No information is given if the user canceled the dialog or clicked OK.
+ \see fl_password_str(int &ret, int maxchar, const char *label, const char *deflt = 0, ...)
+ */
+Fl_String fl_password_str(int maxchar, const char *fmt, const char *defstr, ...) {
Fl_Message msg("?");
- if (maxchar < 0)
- maxchar = 0;
+ if (maxchar < 0) maxchar = 0;
va_list ap;
va_start(ap, defstr);
const char *r = msg.input_innards(fmt, ap, defstr, FL_SECRET_INPUT, maxchar);
va_end(ap);
- return r; // Fl_String(r)
+ return Fl_String(r);
}
+
/** Sets the preferred position for the message box used in
many common dialogs like fl_message(), fl_alert(),
fl_ask(), fl_choice(), fl_input(), fl_password().
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 67c36fb00..b6bc2c303 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -154,6 +154,7 @@ SET (UNITTEST_SRCS
unittests.h
unittest_about.cxx
unittest_points.cxx
+ unittest_core.cxx
unittest_complex_shapes.cxx
unittest_fast_shapes.cxx
unittest_circles.cxx
diff --git a/test/Makefile b/test/Makefile
index 193cd9d17..2a5ea0980 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -30,7 +30,8 @@ CPPUNITTEST = \
unittest_viewport.cxx \
unittest_scrollbarsize.cxx \
unittest_schemes.cxx \
- unittest_simple_terminal.cxx
+ unittest_simple_terminal.cxx \
+ unittest_core.cxx
OBJUNITTEST = \
unittests.o \
diff --git a/test/ask.cxx b/test/ask.cxx
index 0c2365149..973a42d11 100644
--- a/test/ask.cxx
+++ b/test/ask.cxx
@@ -34,16 +34,17 @@
void rename_button(Fl_Widget *o, void *v) {
int what = fl_int(v);
+ int ret = 0;
Fl_String input;
if (what == 0) {
fl_message_icon_label("§");
- input = fl_input_str(0, "Input (no size limit, use ctrl/j for newline):", o->label());
+ input = fl_input_str(ret, 0, "Input (no size limit, use ctrl/j for newline):", o->label());
} else {
fl_message_icon_label("€");
- input = fl_password_str(20, "Enter password (max. 20 characters):", o->label());
+ input = fl_password_str(ret, 20, "Enter password (max. 20 characters):", o->label());
}
- if (input.value()) {
- o->copy_label(input.value());
+ if (ret == 0) {
+ o->copy_label(input.c_str());
o->redraw();
}
}
diff --git a/test/unittest_core.cxx b/test/unittest_core.cxx
new file mode 100644
index 000000000..7e6699956
--- /dev/null
+++ b/test/unittest_core.cxx
@@ -0,0 +1,253 @@
+//
+// Unit tests for the Fast Light Tool Kit (FLTK).
+//
+// Copyright 1998-2023 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
+//
+
+#include "unittests.h"
+
+#include <FL/Fl_Group.H>
+#include <FL/Fl_Simple_Terminal.H>
+#include <FL/Fl_String.H>
+
+/* Test Fl_String constructor and assignment. */
+TEST(Fl_String, Assignment) {
+ Fl_String null;
+ EXPECT_STREQ(null.c_str(), ""); // default initialisation is an empty string
+ EXPECT_TRUE(null.empty());
+
+ Fl_String null2(NULL);
+ EXPECT_STREQ(null2.c_str(), ""); // initialise with a NULL pointer gets an empty string
+ EXPECT_TRUE(null2.empty());
+
+ Fl_String empty("");
+ EXPECT_STREQ(empty.c_str(), ""); // also, empty CString make empty Fl_String
+ EXPECT_TRUE(empty.empty());
+
+ Fl_String text("hello");
+ EXPECT_STREQ(text.c_str(), "hello"); // Load some text from a CString
+ EXPECT_EQ(text.size(), 5); // did we get the size right?
+ EXPECT_EQ(text.strlen(), 5); // do we have a trailing 0
+ EXPECT_GE(text.capacity(), 5); // do we have the capacity
+ EXPECT_TRUE(!text.empty()); // test the empty() method
+
+ Fl_String text2("abcdef", 3);
+ EXPECT_STREQ(text2.c_str(), "abc");
+ EXPECT_EQ(text2.size(), 3);
+
+ Fl_String text3("abc\0def", 7);
+ EXPECT_EQ(text3.strlen(), 3);
+ EXPECT_EQ(text3.size(), 7);
+
+ Fl_String text4(text);
+ EXPECT_STREQ(text4.c_str(), "hello");
+
+ Fl_String text5 = text;
+ EXPECT_STREQ(text5.c_str(), "hello");
+
+ Fl_String text6 = "yoohoo";
+ EXPECT_STREQ(text6.c_str(), "yoohoo");
+
+ return true;
+}
+
+/* Test methods that access Fl_String content and parts of it. */
+TEST(Fl_String, Access) {
+ Fl_String hello = "hello";
+ EXPECT_STREQ(hello.c_str(), "hello");
+ EXPECT_STREQ(hello.data(), "hello");
+ EXPECT_EQ(hello[1], 'e');
+ EXPECT_EQ(hello[hello.size()], 0);
+ EXPECT_EQ(hello.at(1), 'e');
+ EXPECT_EQ(hello.at(-1), 0);
+ EXPECT_EQ(hello.at(11), 0);
+
+ hello[1] = 'a';
+ EXPECT_STREQ(hello.c_str(), "hallo");
+
+ hello.data()[1] = 'e';
+ EXPECT_STREQ(hello.c_str(), "hello");
+
+ return true;
+}
+
+/* Test the Fl_String capacity management. */
+TEST(Fl_String, Capacity) {
+ Fl_String hello;
+ EXPECT_EQ(hello.capacity(), 0);
+
+ hello = "hi";
+ EXPECT_STREQ(hello.c_str(), "hi");
+ EXPECT_GE(hello.capacity(), 2);
+
+ hello = "the quick brown fox jumps over the lazy dog";
+ EXPECT_STREQ(hello.c_str(), "the quick brown fox jumps over the lazy dog");
+ EXPECT_GE(hello.capacity(), 41);
+
+ int c = hello.capacity();
+ hello.reserve(c+100);
+ EXPECT_STREQ(hello.c_str(), "the quick brown fox jumps over the lazy dog");
+ EXPECT_GE(hello.capacity(), 141);
+
+ hello = "hi";
+ hello.shrink_to_fit();
+ EXPECT_EQ(hello.capacity(), 2);
+
+ return true;
+}
+
+/* Test all methods that operate on Fl_String. */
+TEST(Fl_String, Operations) {
+ Fl_String empty;
+ Fl_String hello = "Hello", world = "World";
+ hello.resize(4);
+ EXPECT_STREQ(hello.c_str(), "Hell");
+
+ hello.clear();
+ EXPECT_TRUE(hello.empty());
+
+ hello = "Hello";
+ hello.insert(3, "-");
+ EXPECT_STREQ(hello.c_str(), "Hel-lo");
+ hello = "Hello";
+ hello.erase(2, 2);
+ EXPECT_STREQ(hello.c_str(), "Heo");
+
+ hello = "Hello";
+ hello.push_back('!');
+ EXPECT_STREQ(hello.c_str(), "Hello!");
+ hello.pop_back();
+ EXPECT_STREQ(hello.c_str(), "Hello");
+ hello.append(world);
+ EXPECT_STREQ(hello.c_str(), "HelloWorld");
+ hello.append("!");
+ EXPECT_STREQ(hello.c_str(), "HelloWorld!");
+ hello = "Hello";
+ hello += world;
+ EXPECT_STREQ(hello.c_str(), "HelloWorld");
+ hello += "!";
+ EXPECT_STREQ(hello.c_str(), "HelloWorld!");
+ hello += '?';
+ EXPECT_STREQ(hello.c_str(), "HelloWorld!?");
+
+ hello = "Hello";
+ hello.replace(0, 0, "Say ", 4);
+ EXPECT_STREQ(hello.c_str(), "Say Hello");
+ hello.replace(0, 4, "");
+ EXPECT_STREQ(hello.c_str(), "Hello");
+ hello.replace(2, 2, "bb");
+ EXPECT_STREQ(hello.c_str(), "Hebbo");
+ hello.replace(2, 2, "xxx");
+ EXPECT_STREQ(hello.c_str(), "Hexxxo");
+ hello.replace(2, 3, "ll");
+ EXPECT_STREQ(hello.c_str(), "Hello");
+ hello.replace(2, 0, NULL, 0);
+ EXPECT_STREQ(hello.c_str(), "Hello");
+ hello.replace(Fl_String::npos, Fl_String::npos, world);
+ EXPECT_STREQ(hello.c_str(), "HelloWorld");
+
+ hello = "Hello";
+ Fl_String sub = hello.substr();
+ EXPECT_STREQ(sub.c_str(), "Hello"); // check correct usage
+ sub = hello.substr(2);
+ EXPECT_STREQ(sub.c_str(), "llo");
+ sub = hello.substr(2, 2);
+ EXPECT_STREQ(sub.c_str(), "ll");
+ sub = hello.substr(-1, 2);
+ EXPECT_TRUE(sub.empty()); // check faulty values
+ sub = hello.substr(20, 2);
+ EXPECT_TRUE(sub.empty());
+ sub = empty.substr(0, 2);
+ EXPECT_TRUE(sub.empty());
+
+ return true;
+}
+
+/* Test all Fl_String functions that are no part of the class. */
+TEST(Fl_String, Non-Member Functions) {
+ Fl_String a = "a", b = "b", empty = "", result;
+ result = a + b;
+ EXPECT_STREQ(result.c_str(), "ab");
+ result = a + empty;
+ EXPECT_STREQ(result.c_str(), "a");
+ result = a + "c";
+ EXPECT_STREQ(result.c_str(), "ac");
+ result = empty + "x";
+ EXPECT_STREQ(result.c_str(), "x");
+ EXPECT_TRUE(!(a == b));
+ EXPECT_TRUE(a == a);
+ EXPECT_TRUE(empty == empty);
+ EXPECT_TRUE(a+b == "ab");
+ EXPECT_TRUE(a+"b" == "a" + b);
+
+ return true;
+}
+
+//
+//------- test aspects of the FLTK core library ----------
+//
+
+/*
+ Create a tab with only a terminal window in it. When shown for the first time,
+ unittest will visualize progress by runing all regsitered tests one-by-one
+ every few miliseconds.
+
+ When run in command line mode (option `--core`), all tests are executed
+ at full speed.
+ */
+class Ut_Core_Test : public Fl_Group {
+
+ Fl_Simple_Terminal *tty;
+ bool suite_ran_;
+
+public:
+
+ // Create the tab
+ static Fl_Widget *create() {
+ return new Ut_Core_Test(UT_TESTAREA_X, UT_TESTAREA_Y, UT_TESTAREA_W, UT_TESTAREA_H);
+ }
+
+ // Constructor for this tab
+ Ut_Core_Test(int x, int y, int w, int h)
+ : Fl_Group(x, y, w, h),
+ tty(NULL),
+ suite_ran_(false)
+ {
+ tty = new Fl_Simple_Terminal(x+4, y+4, w-8, h-8, "Unittest Log");
+ tty->ansi(true);
+ end();
+ Ut_Suite::tty = tty;
+ }
+
+ // Run one single test and repeat calling this until all tests are done
+ static void timer_cb(void*) {
+ // Run a test every few miliseconds to visualize the progress
+ if (Ut_Suite::run_next_test())
+ Fl::repeat_timeout(0.2, timer_cb);
+ }
+
+ // Showing this tab for the first time will trigger the tests
+ void show() FL_OVERRIDE {
+ Fl_Group::show();
+ if (!suite_ran_) {
+ Fl::add_timeout(0.5, timer_cb);
+ suite_ran_ = true;
+ }
+ }
+};
+
+// Register this tab with the unittest app.
+UnitTest core(UT_TEST_CORE, "Core Functionality", Ut_Core_Test::create);
+
+
+
diff --git a/test/unittests.cxx b/test/unittests.cxx
index e393613fb..b9103feb5 100644
--- a/test/unittests.cxx
+++ b/test/unittests.cxx
@@ -29,10 +29,12 @@
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Hold_Browser.H>
#include <FL/Fl_Help_View.H>
+#include <FL/Fl_Simple_Terminal.H>
#include <FL/Fl_Group.H>
#include <FL/Fl_Box.H>
#include <FL/fl_draw.H> // fl_text_extents()
#include <FL/fl_string_functions.h> // fl_strdup()
+#include <FL/fl_ask.H> // fl_message()
#include <stdlib.h> // malloc, free
class Ut_Main_Window *mainwin = NULL;
@@ -41,6 +43,8 @@ class Fl_Hold_Browser *browser = NULL;
int UnitTest::num_tests_ = 0;
UnitTest *UnitTest::test_list_[200] = { 0 };
+// ----- UnitTest ------------------------------------------------------ MARK: -
+
UnitTest::UnitTest(int index, const char* label, Fl_Widget* (*create)())
: widget_(0L)
{
@@ -77,6 +81,8 @@ void UnitTest::add(int index, UnitTest* t) {
num_tests_ = index+1;
}
+// ----- Ut_Main_Window ------------------------------------------------ MARK: -
+
Ut_Main_Window::Ut_Main_Window(int w, int h, const char *l)
: Fl_Double_Window(w, h, l),
draw_alignment_test_(0)
@@ -118,10 +124,265 @@ void Ut_Main_Window::test_alignment(int v) {
redraw();
}
-//------- include the various unit tests as inline code -------
+// ----- Ut_Test ------------------------------------------------------- MARK: -
+
+/** Create a unit test that can contain many test cases using EXPECT_*.
+
+ Ut_Test should be instantiated by using the TEST(SUITE, NAME) macro. Multiple
+ TEST() macros can be called within the same source file to generate an
+ arbitrary number of tests. TEST() can be called in multiple source files
+ of the same application.
+
+ The constructor also registers the test with Ut_Suite and groups it by name.
+ */
+Ut_Test::Ut_Test(const char *suitename, const char *testname, Ut_Test_Call call)
+: name_(testname),
+ call_(call),
+ failed_(false),
+ done_(false)
+{
+ Ut_Suite *suite = Ut_Suite::locate(suitename);
+ suite->add(this);
+}
+
+/** Run all cases inside the test and return false when the first case fails. */
+bool Ut_Test::run(const char *suite) {
+ Ut_Suite::printf("%s[ RUN ]%s %s.%s\n",
+ Ut_Suite::green, Ut_Suite::normal, suite, name_);
+ bool ret = call_();
+ if (ret) {
+ Ut_Suite::printf("%s[ OK ]%s %s.%s\n",
+ Ut_Suite::green, Ut_Suite::normal, suite, name_);
+ failed_ = false;
+ } else {
+ Ut_Suite::printf("%s[ FAILED ]%s %s.%s\n",
+ Ut_Suite::red, Ut_Suite::normal, suite, name_);
+ failed_ = true;
+ }
+ done_ = true;
+ return ret;
+}
+
+/** Print a message is the test was previously marked failed. */
+void Ut_Test::print_failed(const char *suite) {
+ if (failed_) {
+ Ut_Suite::printf("%s[ FAILED ]%s %s.%s\n",
+ Ut_Suite::red, Ut_Suite::normal, suite, name_);
+ }
+}
+
+// ----- Ut_Suite ------------------------------------------------------ MARK: -
+
+Ut_Suite **Ut_Suite::suite_list_ = NULL;
+int Ut_Suite::suite_list_size_ = 0;
+int Ut_Suite::num_tests_ = 0;
+int Ut_Suite::num_passed_ = 0;
+int Ut_Suite::num_failed_ = 0;
+const char *Ut_Suite::red = "\033[31m";
+const char *Ut_Suite::green = "\033[32m";
+const char *Ut_Suite::normal = "\033[0m";
+Fl_Simple_Terminal *Ut_Suite::tty = NULL;
+
+/** Switch the user of color escape sequnces in the log text. */
+void Ut_Suite::color(int v) {
+ if (v) {
+ red = "\033[31m";
+ green = "\033[32m";
+ normal = "\033[0m";
+ } else {
+ red = "";
+ green = "";
+ normal = "";
+ }
+}
+
+/** Create a suite that will group tests by the suite name.
+
+ Ut_Suite is automatically instantiated by using the TEST(SUITE, NAME) macro.
+ Multiple TEST() macros are grouped into suits by suite name.
+ */
+Ut_Suite::Ut_Suite(const char *name)
+: test_list_(NULL),
+ test_list_size_(0),
+ name_(name),
+ done_(false)
+{
+}
+
+/** Add a test to the suite. This is done automatically by the TEST() macro. */
+void Ut_Suite::add(Ut_Test *test) {
+ if ( (test_list_size_ % 16) == 0 ) {
+ test_list_ = (Ut_Test**)realloc(test_list_, (test_list_size_+16)*sizeof(Ut_Test*));
+ }
+ test_list_[test_list_size_++] = test;
+}
+
+/** Static method that will find or create a suite by name. */
+Ut_Suite *Ut_Suite::locate(const char *name) {
+ for (int i=0; i<suite_list_size_; i++) {
+ if (strcmp(name, suite_list_[i]->name_)==0)
+ return suite_list_[i];
+ }
+ if ( (suite_list_size_ % 16) == 0 ) {
+ suite_list_ = (Ut_Suite**)realloc(suite_list_, (suite_list_size_+16)*sizeof(Ut_Suite*));
+ }
+ Ut_Suite *s = new Ut_Suite(name);
+ suite_list_[suite_list_size_++] = s;
+ return s;
+}
+
+/** Logs the start of a test suite run. */
+void Ut_Suite::print_suite_epilog() {
+ Ut_Suite::printf("%s[----------]%s %d test%s from %s\n", Ut_Suite::green, Ut_Suite::normal,
+ test_list_size_, test_list_size_ == 1 ? "" : "s", name_);
+}
+
+/** Run all tests in a single suite, returning the number of failed tests. */
+int Ut_Suite::run() {
+ print_suite_epilog();
+ int num_tests_failed = 0;
+ for (int i=0; i<test_list_size_; i++) {
+ if (!test_list_[i]->run(name_)) {
+ num_tests_failed++;
+ }
+ }
+ return num_tests_failed;
+}
+
+/** Static method to log all tests that are marked failed. */
+void Ut_Suite::print_failed() {
+ for (int i=0; i<test_list_size_; i++) {
+ test_list_[i]->print_failed(name_);
+ }
+}
+
+/** Static method to log the start of a test run. */
+void Ut_Suite::print_prolog() {
+ int i;
+ num_tests_ = 0;
+ num_passed_ = 0;
+ num_failed_ = 0;
+ for (i=0; i<suite_list_size_; i++) {
+ num_tests_ += suite_list_[i]->size();
+ }
+ Ut_Suite::printf("%s[==========]%s Running %d tests from %d test case%s.\n",
+ Ut_Suite::green, Ut_Suite::normal,
+ num_tests_, suite_list_size_,
+ suite_list_size_ == 1 ? "" : "s");
+}
+
+/** Static method to log the end of a test run. */
+void Ut_Suite::print_epilog() {
+ int i;
+ Ut_Suite::printf("%s[==========]%s %d tests from %d test case%s ran.\n",
+ Ut_Suite::green, Ut_Suite::normal,
+ num_tests_, suite_list_size_,
+ suite_list_size_ == 1 ? "" : "s");
+ if (num_passed_) {
+ Ut_Suite::printf("%s[ PASSED ]%s %d test%s.\n",
+ Ut_Suite::green, Ut_Suite::normal, num_passed_,
+ num_passed_ == 1 ? "" : "s");
+ }
+ if (num_failed_) {
+ Ut_Suite::printf("%s[ FAILED ]%s %d test%s, listed below:\n",
+ Ut_Suite::red, Ut_Suite::normal, num_failed_,
+ num_failed_ == 1 ? "" : "s");
+ }
+ for (i=0; i<suite_list_size_; i++) {
+ suite_list_[i]->print_failed();
+ }
+}
+
+/** Static method to run all tests in all test suites.
+ Returns the number of failed tests.
+ */
+int Ut_Suite::run_all_tests() {
+ print_prolog();
+ // loop through all suites which then loop through all tests
+ for (int i=0; i<suite_list_size_; i++) {
+ int n = suite_list_[i]->run();
+ num_passed_ += suite_list_[i]->size() - n;
+ num_failed_ += n;
+ }
+ print_epilog();
+ return num_failed_;
+}
+
+/** Static method to run all test, one-by-one, until done.
+ Run all tests by calling `while (Ut_Suite::run_next_test()) { }`.
+ This is used to visualise test progress with the terminal window by runnig test
+ asynchronously and adding a noticable delay between calls.
+ */
+bool Ut_Suite::run_next_test() {
+ // if all suites are done, print the ending text and return
+ Ut_Suite *last = suite_list_[suite_list_size_-1];
+ if (last->done_) {
+ print_epilog();
+ return false;
+ }
+ // if no tests ran yet, print the starting text
+ Ut_Suite *first = suite_list_[0];
+ if (!first->done_ && !first->test_list_[0]->done_) {
+ print_prolog();
+ }
+ // now find the next test that hasn't ran yet
+ for (int i=0; i<suite_list_size_; i++) {
+ Ut_Suite *st = suite_list_[i];
+ if (st->done_) continue;
+ if (!st->test_list_[0]->done_)
+ st->print_suite_epilog();
+ for (int j=0; j<st->test_list_size_; j++) {
+ if (st->test_list_[j]->done_) continue;
+ if (st->test_list_[j]->run(st->name_)) num_passed_++; else num_failed_++;
+ return true;
+ }
+ st->done_ = true;
+ return true;
+ }
+ return true;
+}
+
+/** A printf that is redirected to the terminal or stdout. */
+void Ut_Suite::printf(const char *format, ...)
+{
+ va_list args;
+ va_start(args, format);
+ if (tty) {
+ tty->vprintf(format, args);
+ } else {
+ vprintf(format, args);
+ }
+ va_end(args);
+}
+
+/** Log the result of a boolean case fail. */
+void Ut_Suite::log_bool(const char *file, int line, const char *cond, bool result, bool expected) {
+ Ut_Suite::printf("%s(%d): error:\n", file, line);
+ Ut_Suite::printf("Value of: %s\n", cond);
+ Ut_Suite::printf("Actual: %s\n", result ? "true" : "false");
+ Ut_Suite::printf("Expected: %s\n", expected ? "true" : "false");
+}
+
+/** Log the result of a string comparison case fail. */
+void Ut_Suite::log_string(const char *file, int line, const char *cond, const char *result, const char *expected) {
+ Ut_Suite::printf("%s(%d): error:\n", file, line);
+ Ut_Suite::printf("Value of: %s\n", cond);
+ Ut_Suite::printf(" Actual: %s\n", result);
+ Ut_Suite::printf("Expected: %s\n", expected);
+}
+
+/** Log the result of an integer comparison case fail. */
+void Ut_Suite::log_int(const char *file, int line, const char *cond, int result, const char *expected) {
+ Ut_Suite::printf("%s(%d): error:\n", file, line);
+ Ut_Suite::printf("Value of: %s\n", cond);
+ Ut_Suite::printf(" Actual: %d\n", result);
+ Ut_Suite::printf("Expected: %s\n", expected);
+}
+
+// ----- main ---------------------------------------------------------- MARK: -
// callback whenever the browser value changes
-void UT_BROWSER_CB(Fl_Widget*, void*) {
+void ui_browser_cb(Fl_Widget*, void*) {
for ( int t=1; t<=browser->size(); t++ ) {
UnitTest* ti = (UnitTest*)browser->data(t);
if ( browser->selected(t) ) {
@@ -132,11 +393,57 @@ void UT_BROWSER_CB(Fl_Widget*, void*) {
}
}
+static bool run_core_tests_only = false;
+
+static int arg(int argc, char** argv, int& i) {
+ if ( strcmp(argv[i], "--core") == 0 ) {
+ run_core_tests_only = true;
+ i++;
+ return 1;
+ }
+ if ( strcmp(argv[i], "--color=0") == 0 ) {
+ Ut_Suite::color(0);
+ i++;
+ return 1;
+ }
+ if ( strcmp(argv[i], "--color=1") == 0 ) {
+ Ut_Suite::color(1);
+ i++;
+ return 1;
+ }
+ if ( (strcmp(argv[i], "--help") == 0) || (strcmp(argv[i], "-h") == 0) ) {
+ return 0;
+ }
+ return 0;
+}
// This is the main call. It creates the window and adds all previously
// registered tests to the browser widget.
int main(int argc, char** argv) {
- Fl::args(argc, argv);
+ int i;
+ if ( Fl::args(argc,argv,i,arg) == 0 ) { // unsupported argument found
+ static const char *msg =
+ "usage: %s <switches>\n"
+ " --core : test core functionality only\n"
+ " --color=1, --color=0 : print test output in color or plain text"
+ " --help, -h : print this help page\n";
+ const char *app_name = NULL;
+ if ( (argc > 0) && argv[0] && argv[0][0] )
+ app_name = fl_filename_name(argv[0]);
+ if ( !app_name || !app_name[0])
+ app_name = "unittests";
+#ifdef _MSC_VER
+ fl_message(msg, app_name);
+#else
+ fprintf(stderr, msg, app_name);
+#endif
+ return 1;
+ }
+
+ if (run_core_tests_only) {
+ return RUN_ALL_TESTS();
+ }
+
Fl::get_system_colors();
Fl::scheme(Fl::scheme()); // init scheme before instantiating tests
Fl::visual(FL_RGB);
@@ -146,9 +453,9 @@ int main(int argc, char** argv) {
browser = new Fl_Hold_Browser(UT_BROWSER_X, UT_BROWSER_Y, UT_BROWSER_W, UT_BROWSER_H, "Unit Tests");
browser->align(FL_ALIGN_TOP|FL_ALIGN_LEFT);
browser->when(FL_WHEN_CHANGED);
- browser->callback(UT_BROWSER_CB);
+ browser->callback(ui_browser_cb);
- int i, n = UnitTest::num_tests();
+ int n = UnitTest::num_tests();
for (i=0; i<n; i++) {
UnitTest* t = UnitTest::test(i);
if (t) {
@@ -163,6 +470,6 @@ int main(int argc, char** argv) {
mainwin->show(argc, argv);
// Select first test in browser, and show that test.
browser->select(UT_TEST_ABOUT+1);
- UT_BROWSER_CB(browser, 0);
+ ui_browser_cb(browser, 0);
return Fl::run();
}
diff --git a/test/unittests.h b/test/unittests.h
index 8d04f15ca..cee1dfc3f 100644
--- a/test/unittests.h
+++ b/test/unittests.h
@@ -20,6 +20,10 @@
#include <FL/Fl.H>
#include <FL/Fl_Double_Window.H>
+#include <stdarg.h>
+
+class Fl_Simple_Terminal;
+
// WINDOW/WIDGET SIZES
const int UT_MAINWIN_W = 700; // main window w()
const int UT_MAINWIN_H = 400; // main window h()
@@ -50,7 +54,8 @@ enum {
UT_TEST_VIEWPORT,
UT_TEST_SCROLLBARSIZE,
UT_TEST_SCHEMES,
- UT_TEST_SIMPLE_TERMINAL
+ UT_TEST_SIMPLE_TERMINAL,
+ UT_TEST_CORE,
};
// This class helps to automatically register a new test with the unittest app.
@@ -74,6 +79,173 @@ private:
static UnitTest* test_list_[];
};
+// The following classes and macros implement a subset of the Google Test API
+// without creating any external dependencies.
+//
+// There is nothing to initialise or set up. Just by including these classes,
+// we can create tests anywhere inside the app by simply writing:
+//
+// TEST(Math, Addition) {
+// EXPECT_EQ(3+3, 6);
+// return true;
+// }
+// TEST(Math, Multiplication) {
+// EXPECT_EQ(3*3, 9);
+// return true;
+// }
+// RUN_ALL_TESTS();
+//
+// The test suite must only be run once.
+
+typedef bool (*Ut_Test_Call)();
+
+/**
+ Implement a single test which can in turn contain many EXPECT_* macros.
+ Ut_Test classes are automatically created using the TEST(suite_name, test_name)
+ macro. Tests with identical suite names are grouped into a single suite.
+ */
+class Ut_Test {
+ friend class Ut_Suite;
+ const char *name_;
+ Ut_Test_Call call_;
+ bool failed_;
+ bool done_;
+public:
+ Ut_Test(const char *suitename, const char *testname, Ut_Test_Call call);
+ bool run(const char *suite);
+ void print_failed(const char *suite);
+};
+
+/**
+ Implement test registry and the grouping of tests into a suite. This class
+ holds a number of static elements that register an arbitrary number of tests
+ and groups them into suites via the TEST() macro.
+ */
+class Ut_Suite {
+ static Ut_Suite **suite_list_;
+ static int suite_list_size_;
+ static int num_tests_;
+ static int num_passed_;
+ static int num_failed_;
+
+ Ut_Test **test_list_;
+ int test_list_size_;
+ const char *name_;
+ bool done_;
+ Ut_Suite(const char *name);
+public:
+ void add(Ut_Test *test);
+ int size() { return test_list_size_; }
+ int run();
+ void print_suite_epilog();
+ void print_failed();
+ static Ut_Suite *locate(const char *name);
+ static int run_all_tests();
+ static bool run_next_test();
+ static void printf(const char *format, ...);
+ static void log_bool(const char *file, int line, const char *cond, bool result, bool expected);
+ static void log_string(const char *file, int line, const char *cond, const char *result, const char *expected);
+ static void log_int(const char *file, int line, const char *cond, int result, const char *expected);
+ static void print_prolog();
+ static void print_epilog();
+ static void color(int);
+ static int failed() { return num_failed_; }
+ static const char *red;
+ static const char *green;
+ static const char *normal;
+ static Fl_Simple_Terminal *tty;
+};
+
+#define UT_CONCAT_(prefix, suffix) prefix##suffix
+#define UT_CONCAT(prefix, suffix) UT_CONCAT_(prefix, suffix)
+
+/** Create a test function and register it with the test suites.
+ \param[in] SUITE naming of the test suite for grouping
+ \param[in] CASE name this test
+ */
+#define TEST(SUITE, CASE) \
+ static bool UT_CONCAT(test_call_, __LINE__)(); \
+ Ut_Test UT_CONCAT(test__, __LINE__)(#SUITE, #CASE, UT_CONCAT(test_call_, __LINE__)); \
+ static bool UT_CONCAT(test_call_, __LINE__)()
+
+/** Create a test case where the result is expected to be a boolena with the value true */
+#define EXPECT_TRUE(COND) \
+ bool UT_CONCAT(cond, __LINE__) = COND; \
+ if (UT_CONCAT(cond, __LINE__) != true) { \
+ Ut_Suite::log_bool(__FILE__, __LINE__, #COND, UT_CONCAT(cond, __LINE__), true); \
+ return false; \
+ }
+
+/** Create a test case for string comparison. NULL is ok for both arguments. */
+#define EXPECT_STREQ(A, B) \
+ const char *UT_CONCAT(a, __LINE__) = A; \
+ const char *UT_CONCAT(b, __LINE__) = B; \
+ if ( (UT_CONCAT(a, __LINE__)==NULL && UT_CONCAT(b, __LINE__)!=NULL) \
+ || (UT_CONCAT(a, __LINE__)!=NULL && UT_CONCAT(b, __LINE__)==NULL) \
+ || (UT_CONCAT(b, __LINE__)!=NULL && strcmp(UT_CONCAT(a, __LINE__), UT_CONCAT(b, __LINE__))!=0) ) { \
+ Ut_Suite::log_string(__FILE__, __LINE__, #A, UT_CONCAT(a, __LINE__), #B); \
+ return false; \
+ }
+
+/** Create a test case for integer comparison. */
+#define EXPECT_EQ(A, B) \
+ int UT_CONCAT(a, __LINE__) = A; \
+ int UT_CONCAT(b, __LINE__) = B; \
+ if (UT_CONCAT(a, __LINE__) != UT_CONCAT(b, __LINE__)) { \
+ Ut_Suite::log_int(__FILE__, __LINE__, #A, UT_CONCAT(a, __LINE__), #B); \
+ return false; \
+ }
+
+/** Create a test case for integer comparison. */
+#define EXPECT_NE(A, B) \
+ int UT_CONCAT(a, __LINE__) = A; \
+ int UT_CONCAT(b, __LINE__) = B; \
+ if (UT_CONCAT(a, __LINE__) == UT_CONCAT(b, __LINE__)) { \
+ Ut_Suite::log_int(__FILE__, __LINE__, #A, UT_CONCAT(a, __LINE__), #B); \
+ return false; \
+ }
+
+/** Create a test case for integer comparison. */
+#define EXPECT_LT(A, B) \
+ int UT_CONCAT(a, __LINE__) = A; \
+ int UT_CONCAT(b, __LINE__) = B; \
+ if (UT_CONCAT(a, __LINE__) >= UT_CONCAT(b, __LINE__)) { \
+ Ut_Suite::log_int(__FILE__, __LINE__, #A, UT_CONCAT(a, __LINE__), #B); \
+ return false; \
+ }
+
+/** Create a test case for integer comparison. */
+#define EXPECT_LE(A, B) \
+ int UT_CONCAT(a, __LINE__) = A; \
+ int UT_CONCAT(b, __LINE__) = B; \
+ if (UT_CONCAT(a, __LINE__) > UT_CONCAT(b, __LINE__)) { \
+ Ut_Suite::log_int(__FILE__, __LINE__, #A, UT_CONCAT(a, __LINE__), #B); \
+ return false; \
+ }
+
+/** Create a test case for integer comparison. */
+#define EXPECT_GT(A, B) \
+ int UT_CONCAT(a, __LINE__) = A; \
+ int UT_CONCAT(b, __LINE__) = B; \
+ if (UT_CONCAT(a, __LINE__) <= UT_CONCAT(b, __LINE__)) { \
+ Ut_Suite::log_int(__FILE__, __LINE__, #A, UT_CONCAT(a, __LINE__), #B); \
+ return false; \
+ }
+
+/** Create a test case for integer comparison. */
+#define EXPECT_GE(A, B) \
+ int UT_CONCAT(a, __LINE__) = A; \
+ int UT_CONCAT(b, __LINE__) = B; \
+ if (UT_CONCAT(a, __LINE__) < UT_CONCAT(b, __LINE__)) { \
+ Ut_Suite::log_int(__FILE__, __LINE__, #A, UT_CONCAT(a, __LINE__), #B); \
+ return false; \
+ }
+
+/** Run all registered suits and their tests, and return the number of failed tests. */
+#define RUN_ALL_TESTS() \
+ Ut_Suite::run_all_tests()
+
+
// The main window needs an additional drawing feature in order to support
// the viewport alignment test.
class Ut_Main_Window : public Fl_Double_Window {