// 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 "dvxDialog.h" #include "dvxPrefs.h" #include "dvxWm.h" #include "widgetBox.h" #include "widgetButton.h" #include "widgetImage.h" #include "widgetLabel.h" #include "widgetTextInput.h" #include "widgetTreeView.h" #include "thirdparty/stb_ds_wrap.h" #include #include #include #include // ============================================================ // 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 char **sLabels = NULL; // stb_ds array of strdup'd strings // ============================================================ // Prototypes // ============================================================ static void onPrjWinClose(WindowT *win); static void onTreeItemDblClick(WidgetT *w); // ============================================================ // 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); } // [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); } // [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) { 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/%s.dbp", directory, name); prj->dirty = true; } // ============================================================ // 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; } // ============================================================ // 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/%s", prj->projectDir, 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; } 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) { sPrj = prj; sOnClick = onClick; 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; 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 group char *formsLabel = strdup("Forms"); arrput(sLabels, formsLabel); WidgetT *formsNode = wgtTreeItem(projNode, formsLabel); formsNode->userData = (void *)(intptr_t)-1; wgtTreeItemSetExpanded(formsNode, true); for (int32_t i = 0; i < prj->fileCount; i++) { if (prj->files[i].isForm) { char *label = strdup(prj->files[i].path); arrput(sLabels, label); WidgetT *item = wgtTreeItem(formsNode, label); item->userData = (void *)(intptr_t)i; item->onDblClick = onTreeItemDblClick; } } // Modules group 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++) { if (!prj->files[i].isForm) { char *label = strdup(prj->files[i].path); arrput(sLabels, label); WidgetT *item = wgtTreeItem(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 *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) { const char *iconText = sPpd.iconPath; char fullPath[DVX_MAX_PATH * 2]; snprintf(fullPath, sizeof(fullPath), "%s/%s", sPpd.prj->projectDir, iconText); int32_t infoW = 0; int32_t infoH = 0; if (!dvxImageInfo(fullPath, &infoW, &infoH)) { dvxMessageBox(sPpd.ctx, "Invalid Icon", "Could not read image file.", MB_OK | MB_ICONERROR); return; } if (infoW != 32 || infoH != 32) { 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; } } 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; } 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/%s", sPpd.prj->projectDir, relPath); // Verify the image is 32x32 before loading int32_t infoW = 0; int32_t infoH = 0; if (!dvxImageInfo(fullPath, &infoW, &infoH)) { return; } if (infoW != 32 || infoH != 32) { 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); 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)", "*.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))) { // Validate size using the full path before accepting int32_t infoW = 0; int32_t infoH = 0; if (!dvxImageInfo(path, &infoW, &infoH)) { dvxMessageBox(sPpd.ctx, "Invalid Icon", "Could not read image file.", MB_OK | MB_ICONERROR); return; } if (infoW != 32 || infoH != 32) { 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; } // 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/%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) { 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 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); // 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(); } // Description gets a taller text area WidgetT *descRow = wgtHBox(root); descRow->spacing = wgtPixels(4); WidgetT *descLbl = wgtLabel(descRow, "Description:"); descLbl->minW = wgtPixels(PPD_LABEL_W); sPpd.description = wgtTextArea(descRow, 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); prj->dirty = true; } ctx->modalWindow = prevModal; dvxDestroyWindow(ctx, win); return sPpd.accepted; }