DVX_GUI/src/apps/kpunch/dvxbasic/ide/ideProject.c

999 lines
29 KiB
C

// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// ideProject.c -- DVX BASIC project file management and project window
//
// The .dbp (DVX BASIC Project) file is INI-format:
//
// [Project]
// Name = MyProject
//
// [Modules]
// Count = 2
// File0 = MAIN.BAS
// File1 = UTILS.BAS
//
// [Forms]
// Count = 1
// File0 = FORM1.FRM
//
// [Settings]
// StartupForm = Form1
//
// All file paths are relative to the directory containing the .dbp file.
// Uses the handle-based dvxPrefs API with a dedicated handle per load/save
// so project files don't interfere with the IDE's own preferences.
#include "ideProject.h"
#include "../basRes.h"
#include "dvxApp.h"
#include "dvxDlg.h"
#include "dvxPrefs.h"
#include "dvxWm.h"
#include "box/box.h"
#include "button/button.h"
#include "dropdown/dropdown.h"
#include "image/image.h"
#include "label/label.h"
#include "textInput/textInpt.h"
#include "treeView/treeView.h"
#include "thirdparty/stb_ds_wrap.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
// ============================================================
// Constants
// ============================================================
#define PRJ_WIN_W 180
#define PRJ_WIN_H 300
#define PPD_WIDTH 380
#define PPD_LABEL_W 96
#define PPD_BTN_W 70
#define PPD_BTN_H 24
#define PPD_DESC_H 60
// ============================================================
// Module state
// ============================================================
static PrjStateT *sPrj = NULL;
static WindowT *sPrjWin = NULL;
static WidgetT *sTree = NULL;
static PrjFileClickFnT sOnClick = NULL;
static PrjSelChangeFnT sOnSelChange = NULL;
static char **sLabels = NULL; // stb_ds array of strdup'd strings
static struct {
bool done;
bool accepted;
WidgetT *name;
WidgetT *author;
WidgetT *publisher;
WidgetT *version;
WidgetT *copyright;
WidgetT *description;
WidgetT *startupForm;
const char **formNames; // stb_ds array of form name strings for startup dropdown
WidgetT *helpFileInput;
WidgetT *iconPreview;
char iconPath[DVX_MAX_PATH];
const char *appPath;
AppContextT *ctx;
PrjStateT *prj;
} sPpd;
// ============================================================
// Prototypes
// ============================================================
static void onPrjWinClose(WindowT *win);
static void onTreeItemDblClick(WidgetT *w);
static void onTreeSelChanged(WidgetT *w);
static WidgetT *ppdAddRow(WidgetT *parent, const char *labelText, const char *value, int32_t maxLen);
static void ppdLoadIconPreview(void);
static void ppdOnBrowseHelp(WidgetT *w);
static void ppdOnBrowseIcon(WidgetT *w);
static void ppdOnCancel(WidgetT *w);
static void ppdOnClose(WindowT *win);
static void ppdOnOk(WidgetT *w);
static bool validateIcon(const char *fullPath, bool showErrors);
static void onPrjWinClose(WindowT *win) {
(void)win;
}
static void onTreeItemDblClick(WidgetT *w) {
if (!sPrj || !sOnClick) {
return;
}
int32_t fileIdx = (int32_t)(intptr_t)w->userData;
if (fileIdx >= 0 && fileIdx < sPrj->fileCount) {
sOnClick(fileIdx, sPrj->files[fileIdx].isForm);
}
}
static void onTreeSelChanged(WidgetT *w) {
(void)w;
if (sOnSelChange) {
sOnSelChange();
}
}
static WidgetT *ppdAddRow(WidgetT *parent, const char *labelText, const char *value, int32_t maxLen) {
WidgetT *row = wgtHBox(parent);
row->spacing = wgtPixels(4);
WidgetT *lbl = wgtLabel(row, labelText);
lbl->minW = wgtPixels(PPD_LABEL_W);
WidgetT *input = wgtTextInput(row, maxLen);
input->weight = WGT_WEIGHT_FILL;
wgtSetText(input, value);
return input;
}
static void ppdLoadIconPreview(void) {
if (!sPpd.iconPreview || !sPpd.ctx || !sPpd.prj) {
return;
}
if (!sPpd.iconPath[0]) {
return;
}
const char *relPath = sPpd.iconPath;
char fullPath[DVX_MAX_PATH * 2];
snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", sPpd.prj->projectDir, relPath);
if (!validateIcon(fullPath, true)) {
sPpd.iconPath[0] = '\0';
return;
}
int32_t w = 0;
int32_t h = 0;
int32_t pitch = 0;
uint8_t *data = dvxLoadImage(sPpd.ctx, fullPath, &w, &h, &pitch);
if (data) {
wgtImageSetData(sPpd.iconPreview, data, w, h, pitch);
}
}
static void ppdOnBrowseHelp(WidgetT *w) {
(void)w;
FileFilterT filters[] = {
{ "Help Files (*.hlp)" },
{ "All Files (*.*)" }
};
char path[DVX_MAX_PATH];
if (!dvxFileDialog(sPpd.ctx, "Select Help File", FD_SAVE, NULL, filters, 2, path, sizeof(path))) {
return;
}
// Convert to project-relative path
const char *relPath = path;
int32_t dirLen = (int32_t)strlen(sPpd.prj->projectDir);
if (strncasecmp(path, sPpd.prj->projectDir, dirLen) == 0 &&
(path[dirLen] == '/' || path[dirLen] == '\\')) {
relPath = path + dirLen + 1;
}
wgtSetText(sPpd.helpFileInput, relPath);
}
static void ppdOnBrowseIcon(WidgetT *w) {
(void)w;
FileFilterT filters[] = {
{ "Images (*.bmp;*.png;*.jpg;*.gif)" },
{ "All Files (*.*)" }
};
char path[DVX_MAX_PATH];
if (dvxFileDialog(sPpd.ctx, "Select Icon", FD_OPEN, NULL, filters, 2, path, sizeof(path))) {
if (!validateIcon(path, true)) {
return;
}
// The icon must be in the project directory so the relative
// path works when the project is reloaded.
const char *relPath = NULL;
int32_t dirLen = (int32_t)strlen(sPpd.prj->projectDir);
if (strncasecmp(path, sPpd.prj->projectDir, dirLen) == 0 &&
(path[dirLen] == '/' || path[dirLen] == '\\')) {
relPath = path + dirLen + 1;
}
if (!relPath) {
int32_t result = dvxMessageBox(sPpd.ctx, "Copy Icon",
"The icon is outside the project directory.\nCopy it to the project?",
MB_YESNO | MB_ICONQUESTION);
if (result != ID_YES) {
return;
}
// Get just the filename
const char *fname = platformPathBaseName(path);
// Check if destination already exists
char destPath[DVX_MAX_PATH * 2];
snprintf(destPath, sizeof(destPath), "%s" DVX_PATH_SEP "%s", sPpd.prj->projectDir, fname);
FILE *existing = fopen(destPath, "rb");
if (existing) {
fclose(existing);
char msg[DVX_MAX_PATH + 32];
snprintf(msg, sizeof(msg), "%s already exists.\nOverwrite it?", fname);
int32_t ow = dvxMessageBox(sPpd.ctx, "Overwrite", msg, MB_YESNO | MB_ICONQUESTION);
if (ow != ID_YES) {
return;
}
}
// Copy the file
FILE *src = fopen(path, "rb");
if (!src) {
dvxErrorBox(sPpd.ctx, NULL, "Could not read source file.");
return;
}
FILE *dst = fopen(destPath, "wb");
if (!dst) {
fclose(src);
dvxErrorBox(sPpd.ctx, NULL, "Could not write to project directory.");
return;
}
char buf[4096];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), src)) > 0) {
fwrite(buf, 1, n, dst);
}
fclose(src);
fclose(dst);
relPath = fname;
}
snprintf(sPpd.iconPath, sizeof(sPpd.iconPath), "%s", relPath);
ppdLoadIconPreview();
}
}
static void ppdOnCancel(WidgetT *w) {
(void)w;
sPpd.accepted = false;
sPpd.done = true;
}
static void ppdOnClose(WindowT *win) {
(void)win;
sPpd.accepted = false;
sPpd.done = true;
}
static void ppdOnOk(WidgetT *w) {
(void)w;
// Validate icon path if set
if (sPpd.iconPath[0] && sPpd.prj) {
char fullPath[DVX_MAX_PATH * 2];
snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", sPpd.prj->projectDir, sPpd.iconPath);
if (!validateIcon(fullPath, true)) {
return;
}
}
sPpd.accepted = true;
sPpd.done = true;
}
int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm) {
PrjFileT entry;
memset(&entry, 0, sizeof(entry));
snprintf(entry.path, sizeof(entry.path), "%s", relativePath);
entry.isForm = isForm;
arrput(prj->files, entry);
prj->fileCount = (int32_t)arrlen(prj->files);
prj->dirty = true;
return prj->fileCount - 1;
}
void prjClose(PrjStateT *prj) {
for (int32_t i = 0; i < prj->fileCount; i++) {
free(prj->files[i].buffer);
}
arrfree(prj->files);
arrfree(prj->sourceMap);
prjInit(prj);
}
WindowT *prjCreateWindow(AppContextT *ctx, PrjStateT *prj, PrjFileClickFnT onClick, PrjSelChangeFnT onSelChange) {
sPrj = prj;
sOnClick = onClick;
sOnSelChange = onSelChange;
sPrjWin = dvxCreateWindow(ctx, "Project", 0, 250, PRJ_WIN_W, PRJ_WIN_H, true);
if (!sPrjWin) {
return NULL;
}
sPrjWin->onClose = onPrjWinClose;
WidgetT *root = wgtInitWindow(ctx, sPrjWin);
sTree = wgtTreeView(root);
sTree->weight = WGT_WEIGHT_FILL;
sTree->onChange = onTreeSelChanged;
prjRebuildTree(prj);
return sPrjWin;
}
void prjDestroyWindow(AppContextT *ctx, WindowT *win) {
if (win) {
dvxDestroyWindow(ctx, win);
}
// Free label strings
if (sLabels) {
for (int32_t i = 0; i < (int32_t)arrlen(sLabels); i++) {
free(sLabels[i]);
}
arrfree(sLabels);
sLabels = NULL;
}
sPrjWin = NULL;
sTree = NULL;
sPrj = NULL;
sOnClick = NULL;
}
void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize) {
if (fileIdx < 0 || fileIdx >= prj->fileCount) {
outPath[0] = '\0';
return;
}
snprintf(outPath, outSize, "%s" DVX_PATH_SEP "%s", prj->projectDir, prj->files[fileIdx].path);
}
int32_t prjGetSelectedFileIdx(void) {
if (!sTree) {
return -1;
}
WidgetT *sel = wgtTreeViewGetSelected(sTree);
if (!sel) {
return -1;
}
return (int32_t)(intptr_t)sel->userData;
}
void prjInit(PrjStateT *prj) {
memset(prj, 0, sizeof(*prj));
prj->activeFileIdx = -1;
}
bool prjLoad(PrjStateT *prj, const char *dbpPath) {
PrefsHandleT *h = prefsLoad(dbpPath);
if (!h) {
return false;
}
prjInit(prj);
snprintf(prj->projectPath, sizeof(prj->projectPath), "%s", dbpPath);
// Derive project directory
snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", dbpPath);
char *sep = platformPathDirEnd(prj->projectDir);
if (sep) {
*sep = '\0';
} else {
prj->projectDir[0] = '.';
prj->projectDir[1] = '\0';
}
// [Project] section
const char *val;
val = prefsGetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_NAME, NULL);
if (val) { snprintf(prj->name, sizeof(prj->name), "%s", val); }
val = prefsGetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_AUTHOR, NULL);
if (val) { snprintf(prj->author, sizeof(prj->author), "%s", val); }
val = prefsGetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_PUBLISHER, NULL);
if (val) { snprintf(prj->publisher, sizeof(prj->publisher), "%s", val); }
val = prefsGetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_VERSION, NULL);
if (val) { snprintf(prj->version, sizeof(prj->version), "%s", val); }
val = prefsGetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_COPYRIGHT, NULL);
if (val) { snprintf(prj->copyright, sizeof(prj->copyright), "%s", val); }
val = prefsGetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_DESCRIPTION, NULL);
if (val) { snprintf(prj->description, sizeof(prj->description), "%s", val); }
val = prefsGetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_ICON, NULL);
if (val) { snprintf(prj->iconPath, sizeof(prj->iconPath), "%s", val); }
val = prefsGetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, NULL);
if (val) { snprintf(prj->helpFile, sizeof(prj->helpFile), "%s", val); }
// [Modules] section -- File0, File1, ... (no cap; loop exits on missing key)
for (int32_t i = 0; ; i++) {
char key[16];
snprintf(key, sizeof(key), "File%d", (int)i);
val = prefsGetString(h, BAS_INI_SECTION_MODULES, key, NULL);
if (!val) {
break;
}
prjAddFile(prj, val, false);
}
// [Forms] section -- File0, File1, ... (no cap; loop exits on missing key)
for (int32_t i = 0; ; i++) {
char key[16];
snprintf(key, sizeof(key), "File%d", (int)i);
val = prefsGetString(h, BAS_INI_SECTION_FORMS, key, NULL);
if (!val) {
break;
}
prjAddFile(prj, val, true);
}
// [Settings] section
val = prefsGetString(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, NULL);
if (val) { snprintf(prj->startupForm, sizeof(prj->startupForm), "%s", val); }
prefsClose(h);
prj->dirty = false;
return true;
}
// prjLoadAllFiles -- read all project files into memory buffers
// and extract form names from .frm files.
void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx) {
for (int32_t i = 0; i < prj->fileCount; i++) {
if (prj->files[i].buffer) {
continue; // already loaded
}
char fullPath[DVX_MAX_PATH];
prjFullPath(prj, i, fullPath, sizeof(fullPath));
int32_t bytesRead = 0;
char *buf = platformReadFile(fullPath, &bytesRead);
if (!buf || bytesRead <= 0) {
free(buf);
continue;
}
prj->files[i].buffer = buf;
// Extract form name from .frm files
if (prj->files[i].isForm) {
const char *pos = buf;
while (*pos) {
pos = dvxSkipWs(pos);
if (strncasecmp(pos, "Begin Form ", 11) == 0) {
const char *np = dvxSkipWs(pos + 11);
int32_t n = 0;
while (*np && *np != ' ' && *np != '\t' && *np != '\r' && *np != '\n' && n < PRJ_MAX_NAME - 1) {
prj->files[i].formName[n++] = *np++;
}
prj->files[i].formName[n] = '\0';
break;
}
while (*pos && *pos != '\n') { pos++; }
if (*pos == '\n') { pos++; }
}
}
// Yield between files to keep the UI responsive
if (ctx) {
dvxUpdate(ctx);
}
}
}
bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, int32_t *outLocalLine) {
for (int32_t i = 0; i < prj->sourceMapCount; i++) {
const PrjSourceMapT *m = &prj->sourceMap[i];
if (concatLine >= m->startLine && concatLine < m->startLine + m->lineCount) {
*outFileIdx = m->fileIdx;
*outLocalLine = concatLine - m->startLine + 1;
return true;
}
}
return false;
}
void prjNew(PrjStateT *prj, const char *name, const char *directory, PrefsHandleT *prefs) {
prjInit(prj);
snprintf(prj->name, sizeof(prj->name), "%s", name);
snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", directory);
snprintf(prj->projectPath, sizeof(prj->projectPath), "%s" DVX_PATH_SEP "%s.dbp", directory, name);
prj->dirty = true;
// Apply defaults from preferences
if (prefs) {
snprintf(prj->author, sizeof(prj->author), "%s", prefsGetString(prefs, "defaults", "author", ""));
snprintf(prj->publisher, sizeof(prj->publisher), "%s", prefsGetString(prefs, "defaults", "publisher", ""));
snprintf(prj->version, sizeof(prj->version), "%s", prefsGetString(prefs, "defaults", "version", "1.0"));
snprintf(prj->copyright, sizeof(prj->copyright), "%s", prefsGetString(prefs, "defaults", "copyright", ""));
snprintf(prj->description, sizeof(prj->description), "%s", prefsGetString(prefs, "defaults", "description", ""));
}
}
bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath) {
if (!ctx || !prj) {
return false;
}
WindowT *win = dvxCreateWindowCentered(ctx, "Project Properties", PPD_WIDTH, 380, false);
if (!win) {
return false;
}
win->modal = true;
win->onClose = ppdOnClose;
win->maxW = win->w;
win->maxH = win->h;
sPpd.done = false;
sPpd.accepted = false;
sPpd.ctx = ctx;
sPpd.prj = prj;
sPpd.appPath = appPath;
WidgetT *root = wgtInitWindow(ctx, win);
if (!root) {
dvxDestroyWindow(ctx, win);
return false;
}
root->spacing = wgtPixels(2);
sPpd.name = ppdAddRow(root, "Name:", prj->name, PRJ_MAX_NAME);
sPpd.author = ppdAddRow(root, "Author:", prj->author, PRJ_MAX_STRING);
sPpd.publisher = ppdAddRow(root, "Publisher:", prj->publisher, PRJ_MAX_STRING);
sPpd.version = ppdAddRow(root, "Version:", prj->version, PRJ_MAX_NAME);
sPpd.copyright = ppdAddRow(root, "Copyright:", prj->copyright, PRJ_MAX_STRING);
// Startup form dropdown
{
WidgetT *sfRow = wgtHBox(root);
sfRow->spacing = wgtPixels(4);
WidgetT *sfLbl = wgtLabel(sfRow, "Startup Form:");
sfLbl->minW = wgtPixels(PPD_LABEL_W);
sPpd.startupForm = wgtDropdown(sfRow);
sPpd.startupForm->weight = WGT_WEIGHT_FILL;
// Populate with form names from the project
sPpd.formNames = NULL;
int32_t selectedIdx = 0;
for (int32_t i = 0; i < prj->fileCount; i++) {
if (!prj->files[i].isForm) {
continue;
}
// Use the cached form object name, fall back to filename
const char *name = prj->files[i].formName;
char fallback[DVX_MAX_PATH];
if (!name[0]) {
snprintf(fallback, sizeof(fallback), "%s", prj->files[i].path);
char *dot = strrchr(fallback, '.');
if (dot) { *dot = '\0'; }
name = fallback;
}
arrput(sPpd.formNames, strdup(name));
if (strcasecmp(name, prj->startupForm) == 0) {
selectedIdx = (int32_t)arrlen(sPpd.formNames) - 1;
}
}
int32_t formCount = (int32_t)arrlen(sPpd.formNames);
wgtDropdownSetItems(sPpd.startupForm, sPpd.formNames, formCount);
if (formCount > 0) {
wgtDropdownSetSelected(sPpd.startupForm, selectedIdx);
}
}
// Icon row: label + preview + Browse button
{
WidgetT *iconRow = wgtHBox(root);
iconRow->spacing = wgtPixels(4);
WidgetT *iconLbl = wgtLabel(iconRow, "Icon:");
iconLbl->minW = wgtPixels(PPD_LABEL_W);
// Load "noicon" placeholder from app resources
int32_t niW = 0;
int32_t niH = 0;
int32_t niP = 0;
uint8_t *noIconData = appPath ? dvxResLoadIcon(ctx, appPath, "noicon", &niW, &niH, &niP) : NULL;
if (noIconData) {
sPpd.iconPreview = wgtImage(iconRow, noIconData, niW, niH, niP);
} else {
uint8_t *placeholder = (uint8_t *)calloc(4, 1);
sPpd.iconPreview = wgtImage(iconRow, placeholder, 1, 1, 4);
}
WidgetT *browseBtn = wgtButton(iconRow, "Browse...");
browseBtn->onClick = ppdOnBrowseIcon;
snprintf(sPpd.iconPath, sizeof(sPpd.iconPath), "%s", prj->iconPath);
ppdLoadIconPreview();
}
// Help file row
{
WidgetT *hlpRow = wgtHBox(root);
hlpRow->spacing = wgtPixels(4);
WidgetT *hlpLbl = wgtLabel(hlpRow, "Help File:");
hlpLbl->minW = wgtPixels(PPD_LABEL_W);
sPpd.helpFileInput = wgtTextInput(hlpRow, DVX_MAX_PATH);
sPpd.helpFileInput->weight = WGT_WEIGHT_FILL;
wgtSetText(sPpd.helpFileInput, prj->helpFile);
WidgetT *hlpBrowse = wgtButton(hlpRow, "Browse...");
hlpBrowse->onClick = ppdOnBrowseHelp;
}
// Description: label above, textarea below (matches Preferences layout)
wgtLabel(root, "Description:");
sPpd.description = wgtTextArea(root, PRJ_MAX_DESC);
sPpd.description->weight = WGT_WEIGHT_FILL;
sPpd.description->minH = wgtPixels(PPD_DESC_H);
wgtSetText(sPpd.description, prj->description);
// OK / Cancel buttons
WidgetT *btnRow = wgtHBox(root);
btnRow->align = AlignCenterE;
WidgetT *okBtn = wgtButton(btnRow, "&OK");
okBtn->minW = wgtPixels(PPD_BTN_W);
okBtn->minH = wgtPixels(PPD_BTN_H);
okBtn->onClick = ppdOnOk;
WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel");
cancelBtn->minW = wgtPixels(PPD_BTN_W);
cancelBtn->minH = wgtPixels(PPD_BTN_H);
cancelBtn->onClick = ppdOnCancel;
dvxFitWindow(ctx, win);
WindowT *prevModal = ctx->modalWindow;
ctx->modalWindow = win;
while (!sPpd.done && ctx->running) {
dvxUpdate(ctx);
}
if (sPpd.accepted) {
const char *s;
s = wgtGetText(sPpd.name);
if (s) { snprintf(prj->name, sizeof(prj->name), "%s", s); }
s = wgtGetText(sPpd.author);
if (s) { snprintf(prj->author, sizeof(prj->author), "%s", s); }
s = wgtGetText(sPpd.publisher);
if (s) { snprintf(prj->publisher, sizeof(prj->publisher), "%s", s); }
s = wgtGetText(sPpd.version);
if (s) { snprintf(prj->version, sizeof(prj->version), "%s", s); }
s = wgtGetText(sPpd.copyright);
if (s) { snprintf(prj->copyright, sizeof(prj->copyright), "%s", s); }
s = wgtGetText(sPpd.description);
if (s) { snprintf(prj->description, sizeof(prj->description), "%s", s); }
snprintf(prj->iconPath, sizeof(prj->iconPath), "%s", sPpd.iconPath);
s = wgtGetText(sPpd.helpFileInput);
if (s) { snprintf(prj->helpFile, sizeof(prj->helpFile), "%s", s); }
// Read startup form from dropdown
if (sPpd.startupForm && sPpd.formNames) {
int32_t sfIdx = wgtDropdownGetSelected(sPpd.startupForm);
if (sfIdx >= 0 && sfIdx < (int32_t)arrlen(sPpd.formNames)) {
snprintf(prj->startupForm, sizeof(prj->startupForm), "%s", sPpd.formNames[sfIdx]);
}
}
prj->dirty = true;
}
// Free form name strings
for (int32_t i = 0; i < (int32_t)arrlen(sPpd.formNames); i++) {
free((char *)sPpd.formNames[i]);
}
arrfree(sPpd.formNames);
sPpd.formNames = NULL;
ctx->modalWindow = prevModal;
dvxDestroyWindow(ctx, win);
return sPpd.accepted;
}
void prjRebuildTree(PrjStateT *prj) {
if (!sTree) {
return;
}
// Clear existing items by removing all children
sTree->firstChild = NULL;
sTree->lastChild = NULL;
// Free old labels
if (sLabels) {
for (int32_t i = 0; i < (int32_t)arrlen(sLabels); i++) {
free(sLabels[i]);
}
arrfree(sLabels);
sLabels = NULL;
}
if (!prj || prj->fileCount == 0) {
return;
}
// Project name as root
char *projLabel = strdup(prj->name[0] ? prj->name : "Project");
arrput(sLabels, projLabel);
WidgetT *projNode = wgtTreeItem(sTree, projLabel);
projNode->userData = (void *)(intptr_t)-1;
wgtTreeItemSetExpanded(projNode, true);
// Forms and Modules groups
char *formsLabel = strdup("Forms");
arrput(sLabels, formsLabel);
WidgetT *formsNode = wgtTreeItem(projNode, formsLabel);
formsNode->userData = (void *)(intptr_t)-1;
wgtTreeItemSetExpanded(formsNode, true);
char *modsLabel = strdup("Modules");
arrput(sLabels, modsLabel);
WidgetT *modsNode = wgtTreeItem(projNode, modsLabel);
modsNode->userData = (void *)(intptr_t)-1;
wgtTreeItemSetExpanded(modsNode, true);
for (int32_t i = 0; i < prj->fileCount; i++) {
char buf[DVX_MAX_PATH + 4];
snprintf(buf, sizeof(buf), "%s%s", prj->files[i].path, prj->files[i].modified ? " *" : "");
char *label = strdup(buf);
arrput(sLabels, label);
WidgetT *item = wgtTreeItem(prj->files[i].isForm ? formsNode : modsNode, label);
item->userData = (void *)(intptr_t)i;
item->onDblClick = onTreeItemDblClick;
}
wgtInvalidate(sTree);
}
void prjRemoveFile(PrjStateT *prj, int32_t idx) {
if (idx < 0 || idx >= prj->fileCount) {
return;
}
free(prj->files[idx].buffer);
arrdel(prj->files, idx);
prj->fileCount = (int32_t)arrlen(prj->files);
// Adjust active file index
if (prj->activeFileIdx == idx) {
prj->activeFileIdx = -1;
} else if (prj->activeFileIdx > idx) {
prj->activeFileIdx--;
}
prj->dirty = true;
}
bool prjSave(const PrjStateT *prj) {
if (prj->projectPath[0] == '\0') {
return false;
}
PrefsHandleT *h = prefsCreate();
if (!h) {
return false;
}
// [Project] section
prefsSetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_NAME, prj->name);
if (prj->author[0]) { prefsSetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_AUTHOR, prj->author); }
if (prj->publisher[0]) { prefsSetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_PUBLISHER, prj->publisher); }
if (prj->version[0]) { prefsSetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_VERSION, prj->version); }
if (prj->copyright[0]) { prefsSetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_COPYRIGHT, prj->copyright); }
if (prj->description[0]) { prefsSetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_DESCRIPTION, prj->description); }
if (prj->iconPath[0]) { prefsSetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_ICON, prj->iconPath); }
if (prj->helpFile[0]) { prefsSetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, prj->helpFile); }
// [Modules] section
int32_t modIdx = 0;
for (int32_t i = 0; i < prj->fileCount; i++) {
if (!prj->files[i].isForm) {
char key[16];
snprintf(key, sizeof(key), "File%d", (int)modIdx++);
prefsSetString(h, BAS_INI_SECTION_MODULES, key, prj->files[i].path);
}
}
prefsSetInt(h, BAS_INI_SECTION_MODULES, "Count", modIdx);
// [Forms] section
int32_t frmIdx = 0;
for (int32_t i = 0; i < prj->fileCount; i++) {
if (prj->files[i].isForm) {
char key[16];
snprintf(key, sizeof(key), "File%d", (int)frmIdx++);
prefsSetString(h, BAS_INI_SECTION_FORMS, key, prj->files[i].path);
}
}
prefsSetInt(h, BAS_INI_SECTION_FORMS, "Count", frmIdx);
// [Settings] section
prefsSetString(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, prj->startupForm);
bool ok = prefsSaveAs(h, prj->projectPath);
prefsClose(h);
return ok;
}
bool prjSaveAs(PrjStateT *prj, const char *dbpPath) {
snprintf(prj->projectPath, sizeof(prj->projectPath), "%s", dbpPath);
// Update project directory
snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", dbpPath);
char *sep = platformPathDirEnd(prj->projectDir);
if (sep) {
*sep = '\0';
} else {
prj->projectDir[0] = '.';
prj->projectDir[1] = '\0';
}
return prjSave(prj);
}
// validateIcon -- check that an image file is a valid 32x32 icon.
// Returns true if valid. Shows an error dialog and returns false if not.
static bool validateIcon(const char *fullPath, bool showErrors) {
int32_t infoW = 0;
int32_t infoH = 0;
if (!dvxImageInfo(fullPath, &infoW, &infoH)) {
if (showErrors) {
dvxErrorBox(sPpd.ctx, "Invalid Icon", "Could not read image file.");
}
return false;
}
if (infoW != 32 || infoH != 32) {
if (showErrors) {
char msg[128];
snprintf(msg, sizeof(msg), "Icon must be 32x32 pixels.\nThis image is %dx%d.", (int)infoW, (int)infoH);
dvxMessageBox(sPpd.ctx, "Invalid Icon", msg, MB_OK | MB_ICONWARNING);
}
return false;
}
return true;
}