Find/Replace

This commit is contained in:
Scott Duensing 2026-04-03 16:11:24 -05:00
parent 289adb8c47
commit 0ef46ff6a0
5 changed files with 731 additions and 31 deletions

View file

@ -17,8 +17,10 @@
#include "dvxWm.h"
#include "shellApp.h"
#include "widgetBox.h"
#include "widgetCheckbox.h"
#include "widgetImageButton.h"
#include "widgetLabel.h"
#include "widgetRadio.h"
#include "widgetTextInput.h"
#include "widgetDropdown.h"
#include "widgetButton.h"
@ -87,6 +89,9 @@
#define CMD_PRJ_PROPS 138
#define CMD_WIN_PROJECT 137
#define CMD_HELP_ABOUT 140
#define CMD_FIND 141
#define CMD_REPLACE 142
#define CMD_FIND_NEXT 143
#define IDE_MAX_IMM 1024
#define IDE_DESIGN_W 400
#define IDE_DESIGN_H 300
@ -145,6 +150,9 @@ static void onFormWinClose(WindowT *win);
static void onFormWinResize(WindowT *win, int32_t newW, int32_t newH);
static void onProjectWinClose(WindowT *win);
static WindowT *getLastFocusWin(void);
static void closeFindDialog(void);
static bool findInProject(const char *needle, bool caseSensitive);
static void openFindDialog(bool showReplace);
static void handleEditCmd(int32_t cmd);
static void handleFileCmd(int32_t cmd);
static void handleProjectCmd(int32_t cmd);
@ -224,6 +232,20 @@ static char **sProcBufs = NULL; // stb_ds array: one buffer per procedure
static int32_t sCurProcIdx = -2; // which buffer is in the editor (-1=General, -2=none)
static int32_t sEditorFileIdx = -1; // which project file owns sProcBufs (-1=none)
// Find/Replace state
static char sFindText[256] = "";
static char sReplaceText[256] = "";
// Find/Replace dialog state (modeless)
static WindowT *sFindWin = NULL;
static WidgetT *sFindInput = NULL;
static WidgetT *sReplInput = NULL;
static WidgetT *sReplCheck = NULL;
static WidgetT *sBtnReplace = NULL;
static WidgetT *sBtnReplAll = NULL;
static WidgetT *sCaseCheck = NULL;
static WidgetT *sScopeGroup = NULL; // radio group: 0=Func, 1=Obj, 2=File, 3=Proj
static WidgetT *sDirGroup = NULL; // radio group: 0=Fwd, 1=Back
// Procedure table for Object/Event dropdowns
typedef struct {
char objName[64];
@ -403,6 +425,8 @@ static void activateFile(int32_t fileIdx, IdeViewModeE view) {
sEditorFileIdx = fileIdx;
sProject.activeFileIdx = fileIdx;
}
updateDirtyIndicators();
}
@ -453,8 +477,10 @@ int32_t appMain(DxeAppContextT *ctx) {
sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick);
if (sProjectWin) {
sProjectWin->y = toolbarBottom() + 25;
sProjectWin->onClose = onProjectWinClose;
sProjectWin->y = toolbarBottom() + 25;
sProjectWin->onClose = onProjectWinClose;
sProjectWin->onMenu = onMenu;
sProjectWin->accelTable = sWin ? sWin->accelTable : NULL;
}
char title[300];
@ -536,6 +562,10 @@ static void buildWindow(void) {
wmAddMenuItem(editMenu, "Select &All\tCtrl+A", CMD_SELECT_ALL);
wmAddMenuSeparator(editMenu);
wmAddMenuItem(editMenu, "&Delete\tDel", CMD_DELETE);
wmAddMenuSeparator(editMenu);
wmAddMenuItem(editMenu, "&Find...\tCtrl+F", CMD_FIND);
wmAddMenuItem(editMenu, "Find &Next\tF3", CMD_FIND_NEXT);
wmAddMenuItem(editMenu, "&Replace...\tCtrl+H", CMD_REPLACE);
MenuT *runMenu = wmAddMenu(menuBar, "&Run");
wmAddMenuItem(runMenu, "&Run\tF5", CMD_RUN);
@ -573,6 +603,9 @@ static void buildWindow(void) {
dvxAddAccel(accel, KEY_F7, 0, CMD_VIEW_CODE);
dvxAddAccel(accel, KEY_F7, ACCEL_SHIFT, CMD_VIEW_DESIGN);
dvxAddAccel(accel, 0x1B, 0, CMD_STOP);
dvxAddAccel(accel, 'F', ACCEL_CTRL, CMD_FIND);
dvxAddAccel(accel, 'H', ACCEL_CTRL, CMD_REPLACE);
dvxAddAccel(accel, KEY_F3, 0, CMD_FIND_NEXT);
sWin->accelTable = accel;
WidgetT *tbRoot = wgtInitWindow(sAc, sWin);
@ -1129,6 +1162,8 @@ static void runCached(void) {
static void runModule(BasModuleT *mod) {
setStatus("Running...");
closeFindDialog();
// Hide IDE windows while the program runs
bool hadFormWin = sFormWin && sFormWin->visible;
bool hadToolbox = sToolboxWin && sToolboxWin->visible;
@ -1674,8 +1709,10 @@ static void newProject(void) {
sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick);
if (sProjectWin) {
sProjectWin->y = toolbarBottom() + 25;
sProjectWin->onClose = onProjectWinClose;
sProjectWin->y = toolbarBottom() + 25;
sProjectWin->onClose = onProjectWinClose;
sProjectWin->onMenu = onMenu;
sProjectWin->accelTable = sWin ? sWin->accelTable : NULL;
}
} else {
prjRebuildTree(&sProject);
@ -1720,8 +1757,10 @@ static void openProject(void) {
sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick);
if (sProjectWin) {
sProjectWin->y = toolbarBottom() + 25;
sProjectWin->onClose = onProjectWinClose;
sProjectWin->y = toolbarBottom() + 25;
sProjectWin->onClose = onProjectWinClose;
sProjectWin->onMenu = onMenu;
sProjectWin->accelTable = sWin ? sWin->accelTable : NULL;
}
} else {
prjRebuildTree(&sProject);
@ -1753,6 +1792,8 @@ static void closeProject(void) {
return;
}
closeFindDialog();
if (sProject.dirty) {
prjSave(&sProject);
}
@ -2040,6 +2081,8 @@ static void onCodeWinClose(WindowT *win) {
if (sLastFocusWin == win) {
sLastFocusWin = NULL;
}
updateProjectMenuState();
}
@ -2342,6 +2385,466 @@ static void handleFileCmd(int32_t cmd) {
}
// ============================================================
// Find/Replace dialog (modeless)
// ============================================================
typedef enum {
ScopeFuncE,
ScopeObjE,
ScopeFileE,
ScopeProjE
} FindScopeE;
static FindScopeE getFindScope(void) {
if (!sScopeGroup) {
return ScopeProjE;
}
int32_t idx = wgtRadioGetIndex(sScopeGroup);
switch (idx) {
case 0: return ScopeFuncE;
case 1: return ScopeObjE;
case 2: return ScopeFileE;
default: return ScopeProjE;
}
}
static bool getFindMatchCase(void) {
return sCaseCheck && wgtCheckboxIsChecked(sCaseCheck);
}
static bool getFindForward(void) {
if (!sDirGroup) {
return true;
}
return wgtRadioGetIndex(sDirGroup) == 0;
}
static bool isReplaceEnabled(void) {
return sReplCheck && wgtCheckboxIsChecked(sReplCheck);
}
static void onReplCheckChange(WidgetT *w) {
(void)w;
bool show = isReplaceEnabled();
if (sReplInput) { sReplInput->enabled = show; }
if (sBtnReplace) { sBtnReplace->enabled = show; }
if (sBtnReplAll) { sBtnReplAll->enabled = show; }
if (sFindWin) {
dvxInvalidateWindow(sAc, sFindWin);
}
}
static void onFindNext(WidgetT *w) {
(void)w;
if (!sFindInput) {
return;
}
const char *needle = wgtGetText(sFindInput);
if (!needle || !needle[0]) {
return;
}
snprintf(sFindText, sizeof(sFindText), "%s", needle);
FindScopeE scope = getFindScope();
bool caseSens = getFindMatchCase();
bool forward = getFindForward();
if (scope == ScopeFuncE && sEditor) {
// Search current procedure only
if (!wgtTextAreaFindNext(sEditor, sFindText, caseSens, forward)) {
setStatus("Not found.");
}
} else if (scope == ScopeObjE || scope == ScopeFileE || scope == ScopeProjE) {
if (!findInProject(sFindText, caseSens)) {
setStatus("Not found.");
}
}
}
static void onReplace(WidgetT *w) {
(void)w;
if (!sEditor || !sFindInput || !sReplInput || !isReplaceEnabled()) {
return;
}
const char *needle = wgtGetText(sFindInput);
const char *repl = wgtGetText(sReplInput);
if (!needle || !needle[0]) {
return;
}
snprintf(sFindText, sizeof(sFindText), "%s", needle);
snprintf(sReplaceText, sizeof(sReplaceText), "%s", repl ? repl : "");
// If text is selected and matches the search, replace it, then find next
const char *edText = wgtGetText(sEditor);
if (edText) {
// TODO: replace current selection if it matches, then find next
// For now, just do find next
onFindNext(w);
}
}
static void onReplaceAll(WidgetT *w) {
(void)w;
if (!sFindInput || !sReplInput || !isReplaceEnabled()) {
return;
}
const char *needle = wgtGetText(sFindInput);
const char *repl = wgtGetText(sReplInput);
if (!needle || !needle[0]) {
return;
}
snprintf(sFindText, sizeof(sFindText), "%s", needle);
snprintf(sReplaceText, sizeof(sReplaceText), "%s", repl ? repl : "");
bool caseSens = getFindMatchCase();
FindScopeE scope = getFindScope();
int32_t totalCount = 0;
if (scope == ScopeFuncE && sEditor) {
totalCount = wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens);
} else if (scope == ScopeProjE) {
stashCurrentFile();
for (int32_t i = 0; i < sProject.fileCount; i++) {
activateFile(i, sProject.files[i].isForm ? ViewCodeE : ViewAutoE);
if (sEditor) {
totalCount += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens);
}
}
} else if (scope == ScopeFileE && sEditor) {
// Replace in all procs of current file
int32_t procCount = (int32_t)arrlen(sProcBufs);
for (int32_t p = -1; p < procCount; p++) {
showProc(p);
if (sEditor) {
totalCount += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens);
}
}
} else if (scope == ScopeObjE && sEditor && sObjDropdown) {
// Replace in all procs belonging to current object
int32_t objIdx = wgtDropdownGetSelected(sObjDropdown);
if (objIdx >= 0 && objIdx < (int32_t)arrlen(sObjItems)) {
const char *objName = sObjItems[objIdx];
int32_t procCount = (int32_t)arrlen(sProcTable);
for (int32_t p = 0; p < procCount; p++) {
if (strcasecmp(sProcTable[p].objName, objName) == 0) {
showProc(p);
if (sEditor) {
totalCount += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens);
}
}
}
}
}
char statusBuf[64];
snprintf(statusBuf, sizeof(statusBuf), "%d replacement(s) made.", (int)totalCount);
setStatus(statusBuf);
}
static void onFindClose(WindowT *win) {
(void)win;
closeFindDialog();
}
static void onFindCloseBtn(WidgetT *w) {
(void)w;
closeFindDialog();
}
static void closeFindDialog(void) {
if (sFindWin) {
dvxDestroyWindow(sAc, sFindWin);
sFindWin = NULL;
sFindInput = NULL;
sReplInput = NULL;
sReplCheck = NULL;
sBtnReplace = NULL;
sBtnReplAll = NULL;
sCaseCheck = NULL;
sScopeGroup = NULL;
sDirGroup = NULL;
}
}
static void openFindDialog(bool showReplace) {
if (sFindWin) {
// Already open — just toggle replace mode and raise
if (sReplCheck) {
wgtCheckboxSetChecked(sReplCheck, showReplace);
onReplCheckChange(sReplCheck);
}
dvxRaiseWindow(sAc, sFindWin);
return;
}
sFindWin = dvxCreateWindowCentered(sAc, "Find / Replace", 320, 210, false);
if (!sFindWin) {
return;
}
sFindWin->onClose = onFindClose;
sFindWin->onMenu = onMenu;
sFindWin->accelTable = sWin ? sWin->accelTable : NULL;
WidgetT *root = wgtInitWindow(sAc, sFindWin);
root->spacing = wgtPixels(3);
// Find row
WidgetT *findRow = wgtHBox(root);
findRow->spacing = wgtPixels(4);
wgtLabel(findRow, "Find:");
sFindInput = wgtTextInput(findRow, 256);
sFindInput->weight = 100;
wgtSetText(sFindInput, sFindText);
// Replace checkbox + input
WidgetT *replRow = wgtHBox(root);
replRow->spacing = wgtPixels(4);
sReplCheck = wgtCheckbox(replRow, "Replace:");
wgtCheckboxSetChecked(sReplCheck, showReplace);
sReplCheck->onChange = onReplCheckChange;
sReplInput = wgtTextInput(replRow, 256);
sReplInput->weight = 100;
wgtSetText(sReplInput, sReplaceText);
// Options row: scope + direction + case
WidgetT *optRow = wgtHBox(root);
optRow->spacing = wgtPixels(8);
// Scope
WidgetT *scopeFrame = wgtFrame(optRow, "Scope");
WidgetT *scopeBox = wgtVBox(scopeFrame);
sScopeGroup = wgtRadioGroup(scopeBox);
wgtRadio(sScopeGroup, "Function");
wgtRadio(sScopeGroup, "Object");
wgtRadio(sScopeGroup, "File");
wgtRadio(sScopeGroup, "Project");
wgtRadioGroupSetSelected(sScopeGroup, 3); // Project
// Direction
WidgetT *dirFrame = wgtFrame(optRow, "Direction");
WidgetT *dirBox = wgtVBox(dirFrame);
sDirGroup = wgtRadioGroup(dirBox);
wgtRadio(sDirGroup, "Forward");
wgtRadio(sDirGroup, "Backward");
wgtRadioGroupSetSelected(sDirGroup, 0); // Forward
// Match Case
WidgetT *caseBox = wgtVBox(optRow);
sCaseCheck = wgtCheckbox(caseBox, "Match Case");
// Buttons
WidgetT *btnRow = wgtHBox(root);
btnRow->spacing = wgtPixels(4);
btnRow->align = AlignEndE;
WidgetT *btnFind = wgtButton(btnRow, "Find Next");
btnFind->onClick = onFindNext;
sBtnReplace = wgtButton(btnRow, "Replace");
sBtnReplace->onClick = onReplace;
sBtnReplAll = wgtButton(btnRow, "Replace All");
sBtnReplAll->onClick = onReplaceAll;
WidgetT *btnClose = wgtButton(btnRow, "Close");
btnClose->onClick = onFindCloseBtn;
// Set initial replace enable state
onReplCheckChange(sReplCheck);
dvxFitWindow(sAc, sFindWin);
}
// findInProject -- search all project files for a text match.
// Starts from the current editor position in the current file,
// then continues through subsequent files, wrapping around.
// Opens the file and selects the match when found.
// showProcAndFind -- switch to a procedure, sync the dropdowns, and
// select the search match in the editor.
static bool showProcAndFind(int32_t procIdx, const char *needle, bool caseSensitive) {
showProc(procIdx);
// Sync the Object/Event dropdowns to match
if (procIdx >= 0 && procIdx < (int32_t)arrlen(sProcTable)) {
selectDropdowns(sProcTable[procIdx].objName, sProcTable[procIdx].evtName);
} else if (procIdx == -1 && sObjDropdown) {
wgtDropdownSetSelected(sObjDropdown, 0); // (General)
}
if (sEditor) {
return wgtTextAreaFindNext(sEditor, needle, caseSensitive, true);
}
return false;
}
// Case-insensitive strstr replacement (strcasestr is a GNU extension)
static const char *findSubstrNoCase(const char *haystack, const char *needle, int32_t needleLen) {
for (; *haystack; haystack++) {
if (strncasecmp(haystack, needle, needleLen) == 0) {
return haystack;
}
}
return NULL;
}
static bool findInProject(const char *needle, bool caseSensitive) {
if (!needle || !needle[0] || sProject.fileCount == 0) {
return false;
}
int32_t needleLen = (int32_t)strlen(needle);
// Stash current editor state so all buffers are up-to-date
stashCurrentFile();
// Start from the active file, searching from after the current selection
int32_t startFile = sProject.activeFileIdx >= 0 ? sProject.activeFileIdx : 0;
int32_t startPos = 0;
// If the editor is open on the current file, try the current proc
// first (no wrap — returns false if no more matches ahead).
if (sEditor && sEditorFileIdx == startFile) {
if (wgtTextAreaFindNext(sEditor, needle, caseSensitive, true)) {
return true;
}
// No more matches in current proc — search remaining procs
int32_t procCount = (int32_t)arrlen(sProcBufs);
for (int32_t p = sCurProcIdx + 1; p < procCount; p++) {
if (!sProcBufs[p]) {
continue;
}
const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen);
if (found) {
showProcAndFind(p, needle, caseSensitive);
return true;
}
}
// Search General section if we started in a proc
if (sCurProcIdx >= 0 && sGeneralBuf) {
const char *found = caseSensitive ? strstr(sGeneralBuf, needle) : findSubstrNoCase(sGeneralBuf, needle, needleLen);
if (found) {
showProcAndFind(-1, needle, caseSensitive);
return true;
}
}
// Search procs before the current one (wrap within file)
for (int32_t p = 0; p < sCurProcIdx && p < procCount; p++) {
if (!sProcBufs[p]) {
continue;
}
const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen);
if (found) {
showProcAndFind(p, needle, caseSensitive);
return true;
}
}
// Move to next file
startFile = (startFile + 1) % sProject.fileCount;
}
// Search remaining files, proc by proc.
// startFile was advanced past the current file if we already searched it.
int32_t filesToSearch = sProject.fileCount;
if (sEditor && sEditorFileIdx >= 0) {
filesToSearch--; // skip the file we already searched above
}
for (int32_t attempt = 0; attempt < filesToSearch; attempt++) {
int32_t fileIdx = (startFile + attempt) % sProject.fileCount;
// Activate the file to load its proc buffers
activateFile(fileIdx, sProject.files[fileIdx].isForm ? ViewCodeE : ViewAutoE);
// Search General section
if (sGeneralBuf) {
const char *found = caseSensitive ? strstr(sGeneralBuf, needle) : findSubstrNoCase(sGeneralBuf, needle, needleLen);
if (found) {
showProcAndFind(-1, needle, caseSensitive);
return true;
}
}
// Search each procedure
int32_t procCount = (int32_t)arrlen(sProcBufs);
for (int32_t p = 0; p < procCount; p++) {
if (!sProcBufs[p]) {
continue;
}
const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen);
if (found) {
showProcAndFind(p, needle, caseSensitive);
return true;
}
}
}
return false;
}
static void handleEditCmd(int32_t cmd) {
switch (cmd) {
case CMD_CUT:
@ -2372,6 +2875,22 @@ static void handleEditCmd(int32_t cmd) {
}
}
break;
case CMD_FIND:
openFindDialog(false);
break;
case CMD_FIND_NEXT:
if (sFindText[0]) {
onFindNext(NULL);
} else {
openFindDialog(false);
}
break;
case CMD_REPLACE:
openFindDialog(true);
break;
}
}
@ -2464,7 +2983,9 @@ static void handleWindowCmd(int32_t cmd) {
sToolboxWin = tbxCreate(sAc, &sDesigner);
if (sToolboxWin) {
sToolboxWin->y = toolbarBottom();
sToolboxWin->y = toolbarBottom();
sToolboxWin->onMenu = onMenu;
sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL;
}
}
break;
@ -2474,7 +2995,9 @@ static void handleWindowCmd(int32_t cmd) {
sPropsWin = prpCreate(sAc, &sDesigner);
if (sPropsWin) {
sPropsWin->y = toolbarBottom();
sPropsWin->y = toolbarBottom();
sPropsWin->onMenu = onMenu;
sPropsWin->accelTable = sWin ? sWin->accelTable : NULL;
}
}
break;
@ -3430,7 +3953,9 @@ static void switchToDesign(void) {
sToolboxWin = tbxCreate(sAc, &sDesigner);
if (sToolboxWin) {
sToolboxWin->y = toolbarBottom();
sToolboxWin->y = toolbarBottom();
sToolboxWin->onMenu = onMenu;
sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL;
}
}
@ -3438,7 +3963,9 @@ static void switchToDesign(void) {
sPropsWin = prpCreate(sAc, &sDesigner);
if (sPropsWin) {
sPropsWin->y = toolbarBottom();
sPropsWin->y = toolbarBottom();
sPropsWin->onMenu = onMenu;
sPropsWin->accelTable = sWin ? sWin->accelTable : NULL;
}
}
@ -3502,11 +4029,15 @@ static void showCodeWindow(void) {
WidgetT *dropdownRow = wgtHBox(codeRoot);
dropdownRow->spacing = wgtPixels(4);
wgtLabel(dropdownRow, "Object:");
sObjDropdown = wgtDropdown(dropdownRow);
sObjDropdown->weight = 100;
sObjDropdown->onChange = onObjDropdownChange;
wgtDropdownSetItems(sObjDropdown, NULL, 0);
wgtLabel(dropdownRow, "Function:");
sEvtDropdown = wgtDropdown(dropdownRow);
sEvtDropdown->weight = 100;
sEvtDropdown->onChange = onEvtDropdownChange;
@ -3523,6 +4054,8 @@ static void showCodeWindow(void) {
// onChange is set after initial content is loaded by the caller
// (navigateToEventSub, onPrjFileClick, etc.) to prevent false dirty marking.
updateProjectMenuState();
}
}
@ -3645,6 +4178,10 @@ static void updateProjectMenuState(void) {
wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_REMOVE, hasProject);
wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE, hasProject);
wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE_ALL, hasProject);
wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND, hasProject);
wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND_NEXT, hasProject);
wmMenuItemSetEnabled(sWin->menuBar, CMD_REPLACE, hasProject);
}
@ -3681,11 +4218,19 @@ static void updateDirtyIndicators(void) {
if (sCodeWin) {
bool codeDirty = false;
if (sProject.activeFileIdx >= 0) {
const char *codeFile = "";
if (sEditorFileIdx >= 0 && sEditorFileIdx < sProject.fileCount) {
codeDirty = sProject.files[sEditorFileIdx].modified;
codeFile = sProject.files[sEditorFileIdx].path;
} else if (sProject.activeFileIdx >= 0) {
codeDirty = sProject.files[sProject.activeFileIdx].modified;
codeFile = sProject.files[sProject.activeFileIdx].path;
}
dvxSetTitle(sAc, sCodeWin, codeDirty ? "Code *" : "Code");
char codeTitle[DVX_MAX_PATH + 16];
snprintf(codeTitle, sizeof(codeTitle), "Code - %s%s", codeFile, codeDirty ? " *" : "");
dvxSetTitle(sAc, sCodeWin, codeTitle);
}
// Design window title

View file

@ -2279,10 +2279,6 @@ static void openSysMenu(AppContextT *ctx, WindowT *win) {
ctx->sysMenu.popupX = win->x + CHROME_BORDER_WIDTH;
ctx->sysMenu.popupY = win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT;
if (win->menuBar) {
ctx->sysMenu.popupY += CHROME_MENU_HEIGHT;
}
int32_t maxW = 0;
for (int32_t i = 0; i < ctx->sysMenu.itemCount; i++) {

View file

@ -1473,20 +1473,11 @@ static void int9Handler(void) {
void platformKeyUpInit(void) {
if (sKeyUpInstalled) {
return;
}
_go32_dpmi_get_protected_mode_interrupt_vector(9, &sOldInt9);
sNewInt9.pm_offset = (unsigned long)int9Handler;
sNewInt9.pm_selector = _go32_my_cs();
// Chain: our handler runs first, then DJGPP automatically
// calls the original handler via an IRET wrapper.
_go32_dpmi_chain_protected_mode_interrupt_vector(9, &sNewInt9);
sKeyUpInstalled = true;
// INT 9 hook disabled pending investigation of keyboard corruption.
// Key-up events are not available until this is fixed.
(void)sOldInt9;
(void)sNewInt9;
(void)int9Handler;
}

View file

@ -116,6 +116,7 @@ typedef struct {
} TextAreaDataT;
#include <ctype.h>
#include <strings.h>
#include <time.h>
#define TEXTAREA_BORDER 2
@ -165,6 +166,8 @@ static void widgetTextAreaDragSelect(WidgetT *w, WidgetT *root, int32_t vx, int3
static void widgetTextAreaOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
static int32_t wordBoundaryLeft(const char *buf, int32_t pos);
static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos);
bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, bool forward);
int32_t wgtTextAreaReplaceAll(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive);
WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen);
// sCursorBlinkOn is defined in widgetCore.c (shared state)
@ -2674,6 +2677,163 @@ void wgtTextAreaSetUseTabChar(WidgetT *w, bool useChar) {
}
bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, bool forward) {
if (!w || w->type != sTextAreaTypeId || !needle || !needle[0]) {
return false;
}
TextAreaDataT *ta = (TextAreaDataT *)w->data;
int32_t needleLen = strlen(needle);
if (needleLen > ta->len) {
return false;
}
// Get current cursor byte position
int32_t cursorByte = textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol);
// Search forward from cursor+1 (or backward from cursor-1).
// No wrap-around: returns false if the end/start of the buffer
// is reached without finding a match.
int32_t startPos = forward ? cursorByte + 1 : cursorByte - 1;
int32_t searchLen = ta->len - needleLen + 1;
if (searchLen <= 0) {
return false;
}
int32_t count = forward ? (searchLen - startPos) : (startPos + 1);
if (count <= 0) {
return false;
}
for (int32_t attempt = 0; attempt < count; attempt++) {
int32_t pos;
if (forward) {
pos = startPos + attempt;
} else {
pos = startPos - attempt;
}
if (pos < 0 || pos >= searchLen) {
break;
}
bool match;
if (caseSensitive) {
match = (memcmp(ta->buf + pos, needle, needleLen) == 0);
} else {
match = (strncasecmp(ta->buf + pos, needle, needleLen) == 0);
}
if (match) {
// Select the found text
ta->selAnchor = pos;
ta->selCursor = pos + needleLen;
// Move cursor to start of match (forward) or end of match (backward)
int32_t cursorOff = forward ? pos : pos + needleLen;
textAreaOffToRowCol(ta->buf, cursorOff, &ta->cursorRow, &ta->cursorCol);
ta->desiredCol = ta->cursorCol;
// Scroll to show the match
AppContextT *ctx = wgtGetContext(w);
if (ctx) {
const BitmapFontT *font = &ctx->font;
int32_t gutterW = textAreaGutterWidth(w, font);
int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW;
int32_t visCols = innerW / font->charWidth;
int32_t innerH = w->h - TEXTAREA_BORDER * 2;
int32_t visRows = innerH / font->charHeight;
if (visCols < 1) { visCols = 1; }
if (visRows < 1) { visRows = 1; }
textAreaEnsureVisible(w, visRows, visCols);
}
wgtInvalidatePaint(w);
return true;
}
}
return false;
}
int32_t wgtTextAreaReplaceAll(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive) {
if (!w || w->type != sTextAreaTypeId || !needle || !needle[0] || !replacement) {
return 0;
}
TextAreaDataT *ta = (TextAreaDataT *)w->data;
int32_t needleLen = strlen(needle);
int32_t replLen = strlen(replacement);
int32_t delta = replLen - needleLen;
int32_t count = 0;
if (needleLen > ta->len) {
return 0;
}
// Save undo before any modifications
textEditSaveUndo(ta->buf, ta->len, textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol), ta->undoBuf, &ta->undoLen, &ta->undoCursor, ta->bufSize);
int32_t pos = 0;
while (pos + needleLen <= ta->len) {
bool match;
if (caseSensitive) {
match = (memcmp(ta->buf + pos, needle, needleLen) == 0);
} else {
match = (strncasecmp(ta->buf + pos, needle, needleLen) == 0);
}
if (match) {
// Check if replacement fits in buffer
if (ta->len + delta >= ta->bufSize) {
break;
}
// Shift text after match to make room (or close gap)
if (delta != 0) {
memmove(ta->buf + pos + replLen, ta->buf + pos + needleLen, ta->len - pos - needleLen);
}
// Copy replacement in
memcpy(ta->buf + pos, replacement, replLen);
ta->len += delta;
ta->buf[ta->len] = '\0';
count++;
pos += replLen;
} else {
pos++;
}
}
if (count > 0) {
// Clear selection
ta->selAnchor = -1;
ta->selCursor = -1;
// Clamp cursor if it's past the end
int32_t cursorOff = textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol);
if (cursorOff > ta->len) {
textAreaOffToRowCol(ta->buf, ta->len, &ta->cursorRow, &ta->cursorCol);
}
ta->desiredCol = ta->cursorCol;
textAreaDirtyCache(w);
wgtInvalidatePaint(w);
if (w->onChange) {
w->onChange(w);
}
}
return count;
}
// ============================================================
// DXE registration
// ============================================================
@ -2691,6 +2851,8 @@ static const struct {
void (*setCaptureTabs)(WidgetT *w, bool capture);
void (*setTabWidth)(WidgetT *w, int32_t width);
void (*setUseTabChar)(WidgetT *w, bool useChar);
bool (*findNext)(WidgetT *w, const char *needle, bool caseSensitive, bool forward);
int32_t (*replaceAll)(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive);
} sApi = {
.create = wgtTextInput,
.password = wgtPasswordInput,
@ -2702,7 +2864,9 @@ static const struct {
.setShowLineNumbers = wgtTextAreaSetShowLineNumbers,
.setCaptureTabs = wgtTextAreaSetCaptureTabs,
.setTabWidth = wgtTextAreaSetTabWidth,
.setUseTabChar = wgtTextAreaSetUseTabChar
.setUseTabChar = wgtTextAreaSetUseTabChar,
.findNext = wgtTextAreaFindNext,
.replaceAll = wgtTextAreaReplaceAll
};
// Per-type APIs for the designer

View file

@ -25,6 +25,8 @@ typedef struct {
void (*setCaptureTabs)(WidgetT *w, bool capture);
void (*setTabWidth)(WidgetT *w, int32_t width);
void (*setUseTabChar)(WidgetT *w, bool useChar);
bool (*findNext)(WidgetT *w, const char *needle, bool caseSensitive, bool forward);
int32_t (*replaceAll)(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive);
} TextInputApiT;
static inline const TextInputApiT *dvxTextInputApi(void) {
@ -44,5 +46,7 @@ static inline const TextInputApiT *dvxTextInputApi(void) {
#define wgtTextAreaSetCaptureTabs(w, capture) dvxTextInputApi()->setCaptureTabs(w, capture)
#define wgtTextAreaSetTabWidth(w, width) dvxTextInputApi()->setTabWidth(w, width)
#define wgtTextAreaSetUseTabChar(w, useChar) dvxTextInputApi()->setUseTabChar(w, useChar)
#define wgtTextAreaFindNext(w, needle, caseSens, fwd) dvxTextInputApi()->findNext(w, needle, caseSens, fwd)
#define wgtTextAreaReplaceAll(w, needle, repl, caseSens) dvxTextInputApi()->replaceAll(w, needle, repl, caseSens)
#endif // WIDGET_TEXTINPUT_H