1057 lines
30 KiB
C
1057 lines
30 KiB
C
// 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 "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 PRJ_MAX_FILES 256
|
|
|
|
// ============================================================
|
|
// 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
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
static void onPrjWinClose(WindowT *win);
|
|
static void onTreeItemDblClick(WidgetT *w);
|
|
static void onTreeSelChanged(WidgetT *w);
|
|
static bool validateIcon(const char *fullPath, bool showErrors);
|
|
|
|
// ============================================================
|
|
// prjInit
|
|
// ============================================================
|
|
|
|
void prjInit(PrjStateT *prj) {
|
|
memset(prj, 0, sizeof(*prj));
|
|
prj->activeFileIdx = -1;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prjClose
|
|
// ============================================================
|
|
|
|
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);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prjLoad
|
|
// ============================================================
|
|
|
|
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 = strrchr(prj->projectDir, '/');
|
|
char *sep2 = strrchr(prj->projectDir, '\\');
|
|
|
|
if (sep2 > sep) {
|
|
sep = sep2;
|
|
}
|
|
|
|
if (sep) {
|
|
*sep = '\0';
|
|
} else {
|
|
prj->projectDir[0] = '.';
|
|
prj->projectDir[1] = '\0';
|
|
}
|
|
|
|
// [Project] section
|
|
const char *val;
|
|
|
|
val = prefsGetString(h, "Project", "Name", NULL);
|
|
if (val) { snprintf(prj->name, sizeof(prj->name), "%s", val); }
|
|
|
|
val = prefsGetString(h, "Project", "Author", NULL);
|
|
if (val) { snprintf(prj->author, sizeof(prj->author), "%s", val); }
|
|
|
|
val = prefsGetString(h, "Project", "Company", NULL);
|
|
if (val) { snprintf(prj->company, sizeof(prj->company), "%s", val); }
|
|
|
|
val = prefsGetString(h, "Project", "Version", NULL);
|
|
if (val) { snprintf(prj->version, sizeof(prj->version), "%s", val); }
|
|
|
|
val = prefsGetString(h, "Project", "Copyright", NULL);
|
|
if (val) { snprintf(prj->copyright, sizeof(prj->copyright), "%s", val); }
|
|
|
|
val = prefsGetString(h, "Project", "Description", NULL);
|
|
if (val) { snprintf(prj->description, sizeof(prj->description), "%s", val); }
|
|
|
|
val = prefsGetString(h, "Project", "Icon", NULL);
|
|
if (val) { snprintf(prj->iconPath, sizeof(prj->iconPath), "%s", val); }
|
|
|
|
val = prefsGetString(h, "Project", "HelpFile", NULL);
|
|
if (val) { snprintf(prj->helpFile, sizeof(prj->helpFile), "%s", val); }
|
|
|
|
// [Modules] section -- File0, File1, ...
|
|
for (int32_t i = 0; i < PRJ_MAX_FILES; i++) {
|
|
char key[16];
|
|
snprintf(key, sizeof(key), "File%d", (int)i);
|
|
val = prefsGetString(h, "Modules", key, NULL);
|
|
|
|
if (!val) {
|
|
break;
|
|
}
|
|
|
|
prjAddFile(prj, val, false);
|
|
}
|
|
|
|
// [Forms] section -- File0, File1, ...
|
|
for (int32_t i = 0; i < PRJ_MAX_FILES; i++) {
|
|
char key[16];
|
|
snprintf(key, sizeof(key), "File%d", (int)i);
|
|
val = prefsGetString(h, "Forms", key, NULL);
|
|
|
|
if (!val) {
|
|
break;
|
|
}
|
|
|
|
prjAddFile(prj, val, true);
|
|
}
|
|
|
|
// [Settings] section
|
|
val = prefsGetString(h, "Settings", "StartupForm", NULL);
|
|
if (val) { snprintf(prj->startupForm, sizeof(prj->startupForm), "%s", val); }
|
|
|
|
prefsClose(h);
|
|
prj->dirty = false;
|
|
return true;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prjSave
|
|
// ============================================================
|
|
|
|
bool prjSave(const PrjStateT *prj) {
|
|
if (prj->projectPath[0] == '\0') {
|
|
return false;
|
|
}
|
|
|
|
PrefsHandleT *h = prefsCreate();
|
|
|
|
if (!h) {
|
|
return false;
|
|
}
|
|
|
|
// [Project] section
|
|
prefsSetString(h, "Project", "Name", prj->name);
|
|
|
|
if (prj->author[0]) { prefsSetString(h, "Project", "Author", prj->author); }
|
|
if (prj->company[0]) { prefsSetString(h, "Project", "Company", prj->company); }
|
|
if (prj->version[0]) { prefsSetString(h, "Project", "Version", prj->version); }
|
|
if (prj->copyright[0]) { prefsSetString(h, "Project", "Copyright", prj->copyright); }
|
|
if (prj->description[0]) { prefsSetString(h, "Project", "Description", prj->description); }
|
|
if (prj->iconPath[0]) { prefsSetString(h, "Project", "Icon", prj->iconPath); }
|
|
if (prj->helpFile[0]) { prefsSetString(h, "Project", "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, "Modules", key, prj->files[i].path);
|
|
}
|
|
}
|
|
|
|
prefsSetInt(h, "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, "Forms", key, prj->files[i].path);
|
|
}
|
|
}
|
|
|
|
prefsSetInt(h, "Forms", "Count", frmIdx);
|
|
|
|
// [Settings] section
|
|
prefsSetString(h, "Settings", "StartupForm", prj->startupForm);
|
|
|
|
bool ok = prefsSaveAs(h, prj->projectPath);
|
|
prefsClose(h);
|
|
return ok;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prjSaveAs
|
|
// ============================================================
|
|
|
|
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 = strrchr(prj->projectDir, '/');
|
|
char *sep2 = strrchr(prj->projectDir, '\\');
|
|
|
|
if (sep2 > sep) {
|
|
sep = sep2;
|
|
}
|
|
|
|
if (sep) {
|
|
*sep = '\0';
|
|
} else {
|
|
prj->projectDir[0] = '.';
|
|
prj->projectDir[1] = '\0';
|
|
}
|
|
|
|
return prjSave(prj);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prjNew
|
|
// ============================================================
|
|
|
|
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%c%s.dbp", directory, DVX_PATH_SEP, name);
|
|
prj->dirty = true;
|
|
|
|
// Apply defaults from preferences
|
|
if (prefs) {
|
|
snprintf(prj->author, sizeof(prj->author), "%s", prefsGetString(prefs, "defaults", "author", ""));
|
|
snprintf(prj->company, sizeof(prj->company), "%s", prefsGetString(prefs, "defaults", "company", ""));
|
|
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", ""));
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prjAddFile
|
|
// ============================================================
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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));
|
|
|
|
FILE *f = fopen(fullPath, "r");
|
|
|
|
if (!f) {
|
|
continue;
|
|
}
|
|
|
|
fseek(f, 0, SEEK_END);
|
|
long size = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
if (size <= 0) {
|
|
fclose(f);
|
|
continue;
|
|
}
|
|
|
|
char *buf = (char *)malloc(size + 1);
|
|
|
|
if (!buf) {
|
|
fclose(f);
|
|
continue;
|
|
}
|
|
|
|
int32_t bytesRead = (int32_t)fread(buf, 1, size, f);
|
|
fclose(f);
|
|
buf[bytesRead] = '\0';
|
|
|
|
prj->files[i].buffer = buf;
|
|
|
|
// Extract form name from .frm files
|
|
if (prj->files[i].isForm) {
|
|
const char *pos = buf;
|
|
|
|
while (*pos) {
|
|
while (*pos == ' ' || *pos == '\t') { pos++; }
|
|
|
|
if (strncasecmp(pos, "Begin Form ", 11) == 0) {
|
|
const char *np = pos + 11;
|
|
while (*np == ' ' || *np == '\t') { np++; }
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prjRemoveFile
|
|
// ============================================================
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prjFullPath
|
|
// ============================================================
|
|
|
|
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%c%s", prj->projectDir, DVX_PATH_SEP, prj->files[fileIdx].path);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prjMapLine
|
|
// ============================================================
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Project window callbacks
|
|
// ============================================================
|
|
|
|
static void onPrjWinClose(WindowT *win) {
|
|
(void)win;
|
|
}
|
|
|
|
|
|
int32_t prjGetSelectedFileIdx(void) {
|
|
if (!sTree) {
|
|
return -1;
|
|
}
|
|
|
|
WidgetT *sel = wgtTreeViewGetSelected(sTree);
|
|
|
|
if (!sel) {
|
|
return -1;
|
|
}
|
|
|
|
return (int32_t)(intptr_t)sel->userData;
|
|
}
|
|
|
|
|
|
static void onTreeSelChanged(WidgetT *w) {
|
|
(void)w;
|
|
|
|
if (sOnSelChange) {
|
|
sOnSelChange();
|
|
}
|
|
}
|
|
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prjCreateWindow
|
|
// ============================================================
|
|
|
|
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 = 100;
|
|
sTree->onChange = onTreeSelChanged;
|
|
|
|
prjRebuildTree(prj);
|
|
return sPrjWin;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prjDestroyWindow
|
|
// ============================================================
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prjRebuildTree
|
|
// ============================================================
|
|
|
|
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);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Project properties dialog
|
|
// ============================================================
|
|
|
|
#define PPD_WIDTH 380
|
|
#define PPD_LABEL_W 96
|
|
#define PPD_BTN_W 70
|
|
#define PPD_BTN_H 24
|
|
#define PPD_DESC_H 60
|
|
|
|
static struct {
|
|
bool done;
|
|
bool accepted;
|
|
WidgetT *name;
|
|
WidgetT *author;
|
|
WidgetT *company;
|
|
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;
|
|
|
|
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%c%s", sPpd.prj->projectDir, DVX_PATH_SEP, sPpd.iconPath);
|
|
|
|
if (!validateIcon(fullPath, true)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
sPpd.accepted = true;
|
|
sPpd.done = true;
|
|
}
|
|
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; }
|
|
|
|
|
|
// 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) {
|
|
dvxMessageBox(sPpd.ctx, "Invalid Icon", "Could not read image file.", MB_OK | MB_ICONERROR);
|
|
}
|
|
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;
|
|
}
|
|
|
|
|
|
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%c%s", sPpd.prj->projectDir, DVX_PATH_SEP, 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 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 = strrchr(path, '/');
|
|
const char *fname2 = strrchr(path, '\\');
|
|
|
|
if (fname2 > fname) {
|
|
fname = fname2;
|
|
}
|
|
|
|
fname = fname ? fname + 1 : path;
|
|
|
|
// Check if destination already exists
|
|
char destPath[DVX_MAX_PATH * 2];
|
|
snprintf(destPath, sizeof(destPath), "%s%c%s", sPpd.prj->projectDir, DVX_PATH_SEP, 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) {
|
|
dvxMessageBox(sPpd.ctx, "Error", "Could not read source file.", MB_OK | MB_ICONERROR);
|
|
return;
|
|
}
|
|
|
|
FILE *dst = fopen(destPath, "wb");
|
|
|
|
if (!dst) {
|
|
fclose(src);
|
|
dvxMessageBox(sPpd.ctx, "Error", "Could not write to project directory.", MB_OK | MB_ICONERROR);
|
|
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 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 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 = 100;
|
|
wgtSetText(input, value);
|
|
|
|
return input;
|
|
}
|
|
|
|
|
|
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.company = ppdAddRow(root, "Company:", prj->company, 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 = 100;
|
|
|
|
// 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 = 100;
|
|
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 = 100;
|
|
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.company);
|
|
if (s) { snprintf(prj->company, sizeof(prj->company), "%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;
|
|
}
|