// // Fluid Project code for the Fast Light Tool Kit (FLTK). // // Copyright 1998-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 // #include // strerror(errno) #include // free, malloc #include // strdup, strlen, strcmp #include "Project.h" #include "Fluid.h" #include "io/String_Writer.h" #include "nodes/Node.h" #include "panels/settings_panel.h" #include "panels/codeview_panel.h" #include "tools/filename.h" #include #include // ---- project settings /** Initialize a new project. */ fld::Project::Project() : undo(*this), tree(*this), i18n(*this), include_H_from_C(1), use_FL_COMMAND(0), utf8_in_src(0), avoid_early_includes(0), header_file_set(0), code_file_set(0), write_mergeback_data(0), proj_filename(0), header_file_name_(0), code_file_name_(0), include_guard_(0), in_project_dir(0), modflag(0), modflag_c(0), layout(app::default_layout_preset) { app_work_dir_[0] = '\0'; set_header_file_name(".h"); set_code_file_name(".cxx"); set_include_guard(""); } /** Clear all project resources. */ fld::Project::~Project() { if (header_file_name_) free(header_file_name_); if (code_file_name_) free(code_file_name_); if (include_guard_) free(include_guard_); if (proj_filename) free((void*)proj_filename); } /** Reset all project setting to create a new empty project. */ void fld::Project::reset() { ::delete_all(); i18n.reset(); include_H_from_C = 1; use_FL_COMMAND = 0; utf8_in_src = 0; avoid_early_includes = 0; header_file_set = 0; code_file_set = 0; set_header_file_name(".h"); set_code_file_name(".cxx"); set_include_guard(""); write_mergeback_data = 0; } /** Tell the project and i18n tab of the settings dialog to refresh themselves. */ void fld::Project::update_settings_dialog() { if (settings_window) { w_settings_project_tab->do_callback(w_settings_project_tab, LOAD); w_settings_i18n_tab->do_callback(w_settings_i18n_tab, LOAD); } } // Setters for string members void fld::Project::set_header_file_name(const char *name) { if (header_file_name_) free(header_file_name_); header_file_name_ = name ? strdup(name) : 0; } void fld::Project::set_code_file_name(const char *name) { if (code_file_name_) free(code_file_name_); code_file_name_ = name ? strdup(name) : 0; } void fld::Project::set_include_guard(const char *guard) { if (include_guard_) free(include_guard_); include_guard_ = guard ? strdup(guard) : 0; } /** Get the absolute path of the project file, for example `/Users/matt/dev/`. \param[out] buf buffer to store result (ends with '/') \param[in] bufsize size of buffer */ void fld::Project::projectfile_path(char *buf, int bufsize) const { buf[0] = '\0'; if (!proj_filename) return; char path[FL_PATH_MAX]; fl_filename_path(path, FL_PATH_MAX, proj_filename); char abs_path[FL_PATH_MAX]; fl_filename_absolute(abs_path, FL_PATH_MAX, path, Fluid.launch_path()); fld_end_with_slash(buf, bufsize, abs_path); } /** Get the project file name including extension, for example `test.fl`. \return the file name without path */ const char *fld::Project::projectfile_name() const { if (!proj_filename) return 0; return fl_filename_name(proj_filename); } /** Get the absolute path of the generated C++ code file, for example `/Users/matt/dev/src/`. \param[out] buf buffer to store result (ends with '/') \param[in] bufsize size of buffer */ void fld::Project::codefile_path(char *buf, int bufsize) const { buf[0] = '\0'; char path[FL_PATH_MAX]; fl_filename_path(path, FL_PATH_MAX, code_file_name_); char abs_path[FL_PATH_MAX]; if (Fluid.batch_mode) { fl_filename_absolute(abs_path, FL_PATH_MAX, path, Fluid.launch_path()); } else { char proj_path[FL_PATH_MAX]; projectfile_path(proj_path, FL_PATH_MAX); fl_filename_absolute(abs_path, FL_PATH_MAX, path, proj_path); } fld_end_with_slash(buf, bufsize, abs_path); } /** Get the generated C++ code file name including extension, for example `test.cxx`. \param[out] buf buffer to store result \param[in] bufsize size of buffer */ void fld::Project::codefile_name(char *buf, int bufsize) const { buf[0] = '\0'; const char *name = fl_filename_name(code_file_name_); if (!name || !*name) { // No name specified, use project filename with .cxx extension if (!proj_filename) return; strlcpy(buf, fl_filename_name(proj_filename), bufsize); fl_filename_setext(buf, bufsize, ".cxx"); } else if (name[0] == '.') { // Only extension specified, use project filename with this extension if (!proj_filename) return; strlcpy(buf, fl_filename_name(proj_filename), bufsize); fl_filename_setext(buf, bufsize, code_file_name_); } else { strlcpy(buf, name, bufsize); } } /** Get the absolute path of the generated C++ header file, for example `/Users/matt/dev/src/`. \param[out] buf buffer to store result (ends with '/') \param[in] bufsize size of buffer */ void fld::Project::headerfile_path(char *buf, int bufsize) const { buf[0] = '\0'; char path[FL_PATH_MAX]; fl_filename_path(path, FL_PATH_MAX, header_file_name_); char abs_path[FL_PATH_MAX]; if (Fluid.batch_mode) { fl_filename_absolute(abs_path, FL_PATH_MAX, path, Fluid.launch_path()); } else { char proj_path[FL_PATH_MAX]; projectfile_path(proj_path, FL_PATH_MAX); fl_filename_absolute(abs_path, FL_PATH_MAX, path, proj_path); } fld_end_with_slash(buf, bufsize, abs_path); } /** Get the generated C++ header file name including extension, for example `test.h`. \param[out] buf buffer to store result \param[in] bufsize size of buffer */ void fld::Project::headerfile_name(char *buf, int bufsize) const { buf[0] = '\0'; const char *name = fl_filename_name(header_file_name_); if (!name || !*name) { // No name specified, use project filename with .h extension if (!proj_filename) return; strlcpy(buf, fl_filename_name(proj_filename), bufsize); fl_filename_setext(buf, bufsize, ".h"); } else if (name[0] == '.') { // Only extension specified, use project filename with this extension if (!proj_filename) return; strlcpy(buf, fl_filename_name(proj_filename), bufsize); fl_filename_setext(buf, bufsize, header_file_name_); } else { strlcpy(buf, name, bufsize); } } /** Get the absolute path of the generated i18n strings file, for example `/Users/matt/dev/`. \param[out] buf buffer to store result (ends with '/') \param[in] bufsize size of buffer */ void fld::Project::stringsfile_path(char *buf, int bufsize) const { if (Fluid.batch_mode) { strlcpy(buf, Fluid.launch_path(), bufsize); } else { projectfile_path(buf, bufsize); } } /** Get the generated i18n text file name including extension, for example `test.po`. \param[out] buf buffer to store result \param[in] bufsize size of buffer */ void fld::Project::stringsfile_name(char *buf, int bufsize) const { buf[0] = '\0'; if (!proj_filename) return; strlcpy(buf, fl_filename_name(proj_filename), bufsize); switch (i18n.type) { default: fl_filename_setext(buf, bufsize, ".txt"); break; case FLD_I18N_TYPE_GNU: fl_filename_setext(buf, bufsize, ".po"); break; case FLD_I18N_TYPE_POSIX: fl_filename_setext(buf, bufsize, ".msg"); break; } } /** Get the name of the project file without the filename extension. \param[out] buf buffer to store result \param[in] bufsize size of buffer */ void fld::Project::basename(char *buf, int bufsize) const { buf[0] = '\0'; if (!proj_filename) return; strlcpy(buf, fl_filename_name(proj_filename), bufsize); fl_filename_setext(buf, bufsize, ""); } /** Change the current working directory to the .fl project directory. Every call to enter_project_dir() must have a corresponding leave_project_dir() call. Enter and leave calls can be nested. The first call to enter_project_dir() remembers the original directory, usually the launch directory of the application. Nested calls will increment a nesting counter. When the nesting counter is back to 0, leave_project_dir() will return to the original directory. The global variable 'filename' must be set to the current project file with absolute or relative path information. \see leave_project_dir(), pwd, in_project_dir */ void fld::Project::enter_project_dir() { if (in_project_dir < 0) { fprintf(stderr, "** Fluid internal error: enter_project_dir() calls unmatched\n"); return; } in_project_dir++; // check if we are already in the project dir and do nothing if so if (in_project_dir > 1) return; // check if there is an active project, and do nothing if there is none if (!proj_filename || !*proj_filename) { fprintf(stderr, "** Fluid internal error: enter_project_dir() no filename set\n"); return; } // store the current working directory for later fl_getcwd(app_work_dir_, FL_PATH_MAX); // set the current directory to the path of our .fl file char abs_filename[FL_PATH_MAX]; fl_filename_absolute(abs_filename, FL_PATH_MAX, proj_filename); char project_path[FL_PATH_MAX]; fl_filename_path(project_path, FL_PATH_MAX, abs_filename); if (fl_chdir(project_path) == -1) { fprintf(stderr, "** Fluid internal error: enter_project_dir() can't chdir to %s: %s\n", project_path, strerror(errno)); return; } } /** Change the current working directory to the previous directory. \see enter_project_dir(), pwd, in_project_dir */ void fld::Project::leave_project_dir() { if (in_project_dir == 0) { fprintf(stderr, "** Fluid internal error: leave_project_dir() calls unmatched\n"); return; } in_project_dir--; // still nested, stay in the project directory if (in_project_dir > 0) return; // no longer nested, return to the original, usually the application working directory if (fl_chdir(app_work_dir_) < 0) { fprintf(stderr, "** Fluid internal error: leave_project_dir() can't chdir back to %s : %s\n", app_work_dir_, strerror(errno)); } } /** Clear the project filename. */ void fld::Project::clear_filename() { set_filename(0); } /** Set the filename of the current .fl design. \param[in] c the new absolute filename and path (may be NULL to clear) */ void fld::Project::set_filename(const char *c) { if (proj_filename) free((void *)proj_filename); proj_filename = (c && *c) ? strdup(c) : 0; if (proj_filename && !Fluid.batch_mode) Fluid.history.update(proj_filename); set_modflag(modflag); } /** Write the strings that are used in i18n. */ void fld::Project::write_strings() { Fluid.flush_text_widgets(); if (!proj_filename) { Fluid.save_project_file(0); if (!proj_filename) return; } char path[FL_PATH_MAX]; char name[FL_PATH_MAX]; stringsfile_path(path, FL_PATH_MAX); stringsfile_name(name, FL_PATH_MAX); char filename[FL_PATH_MAX]; snprintf(filename, FL_PATH_MAX, "%s%s", path, name); int x = fld::io::write_strings(*this, filename); if (Fluid.batch_mode) { if (x) { fprintf(stderr, "%s : %s\n", filename, strerror(errno)); exit(1); } } else { if (x) { fl_message("Can't write %s: %s", filename, strerror(errno)); } else if (completion_button->value()) { fl_message("Wrote %s", name); } } } /** Set the "modified" flag and update the title of the main window. The first argument sets the modification state of the current design against the corresponding .fl design file. Any change to the widget tree will mark the design 'modified'. Saving the design will mark it clean. The second argument is optional and set the modification state of the current design against the source code and header file. Any change to the tree, including saving the tree, will mark the code 'outdated'. Generating source code and header files will clear this flag until the next modification. \param[in] mf 0 to clear the modflag, 1 to mark the design "modified", -1 to ignore this parameter \param[in] mfc default -1 to let \c mf control \c modflag_c, 0 to mark the code files current, 1 to mark it out of date. -2 to ignore changes to mf. */ void fld::Project::set_modflag(int mf, int mfc) { const char *code_ext = 0; char new_title[FL_PATH_MAX]; // Update the modflag_c to the worst possible condition. We could be a bit // more graceful and compare modification times of the files, but C++ has // no API for that until C++17. if (mf != -1) { modflag = mf; if (mfc == -1 && mf == 1) mfc = mf; } if (mfc >= 0) { modflag_c = mfc; } if (Fluid.main_window) { const char *base; if (!proj_filename) { base = "Untitled.fl"; } else { base = fl_filename_name(proj_filename); } code_ext = fl_filename_ext(code_file_name_); char mod_star = modflag ? '*' : ' '; char mod_c_star = modflag_c ? '*' : ' '; snprintf(new_title, sizeof(new_title), "%s%c %s%c", base, mod_star, code_ext, mod_c_star); const char *old_title = Fluid.main_window->label(); // only update the title if it actually changed if (!old_title || strcmp(old_title, new_title)) Fluid.main_window->copy_label(new_title); } // if the UI was modified in any way, update the Code View panel if (codeview_panel && codeview_panel->visible() && cv_autorefresh->value()) codeview_defer_update(); }