/* * JoeyDev * Copyright (C) 2018-2023 Scott Duensing * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * 3. This notice may not be removed or altered from any source distribution. */ #include "common.h" #include "editor.h" #include "utils.h" #include "scintillaHeaders.h" #define MARKER_ERROR_ARROW 0 #define MARKER_ERROR_HIGHLIGHT 1 typedef struct EditorDataS { WindowDataT windowData; GtkWidget *boxForEditor; GtkWidget *statusBar; GtkWidget *editor; ScintillaObject *sci; void *pLexer; int id; int statusBarId; } EditorDataT; static int _nextEditorId = 0; static void clearEditor(EditorDataT *self); EVENT void editorEditorNotify(GtkWidget *sciWidget, gint ctrlID, struct SCNotification *notifyData, gpointer userData); static void loadEditor(EditorDataT *self); static void loadEditorConfig(char *lexer, EditorDataT *self); EVENT void menuEditorEditCopy(GtkWidget *object, gpointer userData); EVENT void menuEditorEditCut(GtkWidget *object, gpointer userData); EVENT void menuEditorEditDelete(GtkWidget *object, gpointer userData); EVENT void menuEditorEditPaste(GtkWidget *object, gpointer userData); EVENT void menuEditorFileClose(GtkWidget *object, gpointer userData); EVENT void menuEditorFileNew(GtkWidget *object, gpointer userData); EVENT void menuEditorFileOpen(GtkWidget *object, gpointer userData); EVENT void menuEditorFileSave(GtkWidget *object, gpointer userData); EVENT void menuEditorFileSaveAs(GtkWidget *object, gpointer userData); EVENT void menuEditorHelpEditor(GtkWidget *object, gpointer userData); static void status(EditorDataT *self, char *message); EVENT gboolean winEditorClose(GtkWidget *object, gpointer userData); static void winEditorDelete(gpointer userData); static void writeEditorConfig(char *lexer, EditorDataT *self); static void clearEditor(EditorDataT *self) { (void)self; // Clear editor. SSM(SCI_CLEARALL, 0, 0); // Clear error markers. SSM(SCI_MARKERDELETEALL, MARKER_ERROR_ARROW, 0); SSM(SCI_MARKERDELETEALL, MARKER_ERROR_HIGHLIGHT, 0); } EVENT void editorEditorNotify(GtkWidget *sciWidget, gint ctrlID, struct SCNotification *notifyData, gpointer userData) { EditorDataT *self = (EditorDataT *)userData; int lineNumber = (int)SSM(SCI_LINEFROMPOSITION, (uptr_t)notifyData->position, (sptr_t)0); (void)sciWidget; (void)ctrlID; switch (notifyData->nmhdr.code) { case SCN_MODIFIED: if (notifyData->modificationType & SC_MOD_INSERTTEXT || notifyData->modificationType & SC_MOD_DELETETEXT) { // Mark text dirty. SCN_SAVEPOINTLEFT isn't being reliable. utilSetDirty((WindowDataT *)self, TRUE); } break; case SCN_MARGINCLICK: switch (notifyData->margin) { case MARGIN_SCRIPT_FOLD_INDEX: SSM(SCI_TOGGLEFOLD, lineNumber, (sptr_t)0); break; } break; } } static void loadEditor(EditorDataT *self) { FILE *in = NULL; char *line = NULL; size_t len = 0; in = fopen(self->windowData.filename, "rt"); if (in != NULL) { utilEnsureBufferSize((unsigned char **)&line, (int *)&len, 1024); // Not technically needed, but fixes a pointer warning from memmaker. while (getline(&line, &len, in) != -1) { SSM(SCI_ADDTEXT, strlen(line), (sptr_t)line); } fclose(in); DEL(line); } // Do again - loading text marks us dirty. utilSetDirty((WindowDataT *)self, FALSE); } static void loadEditorConfig(char *lexer, EditorDataT *self) { char *config = NULL; FILE *in = NULL; char *line = NULL; size_t len = 0; char *c = NULL; char *type = NULL; char *name = NULL; unsigned int number; unsigned int integer; config = utilCreateString("%s%cjoeydev%ceditor-%s.conf", g_get_user_config_dir(), UTIL_PATH_CHAR, UTIL_PATH_CHAR, lexer); if (!utilFileExists(config)) { // Nothing to load. DEL(config); return; } in = fopen(config, "rt"); DEL(config); if (in) { // Load config. utilEnsureBufferSize((unsigned char **)&line, (int *)&len, 4096); // Not technically needed, but fixes a pointer warning from memmaker. while (getline(&line, &len, in) != -1) { if (strlen(line) > 0) line[strlen(line) - 1] = 0; c = utilGetToken(line, " ", "\"", "\""); utilDequote(c); // Is this a 'style' line? if (strcasecmp(c, "style") == 0) { // style number "tags" "description" ... (style keywords) c = utilGetToken(NULL, " ", "\"", "\""); number = strtol(c, NULL, 10); c = utilGetToken(NULL, " ", "\"", "\""); c = utilGetToken(NULL, " ", "\"", "\""); c = utilGetToken(NULL, " ", "\"", "\""); do { if (strcmp(c, "fore") == 0) { c = utilGetToken(NULL, " ", "\"", "\""); integer = strtol(c, NULL, 16); SSM(SCI_STYLESETFORE, number, integer); } if (strcmp(c, "back") == 0) { c = utilGetToken(NULL, " ", "\"", "\""); integer = strtol(c, NULL, 16); SSM(SCI_STYLESETBACK, number, integer); } if (strcmp(c, "bold") == 0) { SSM(SCI_STYLESETBOLD, number, 1); } if (strcmp(c, "italic") == 0) { SSM(SCI_STYLESETITALIC, number, 1); } c = utilGetToken(NULL, " ", "\"", "\""); } while (c != NULL); continue; } // Is this a 'property' line? if (strcasecmp(c, "property") == 0) { // property type name "description" value c = utilGetToken(NULL, " ", "\"", "\""); type = strdup(c); c = utilGetToken(NULL, " ", "\"", "\""); name = strdup(c); c = utilGetToken(NULL, " ", "\"", "\""); c = utilGetToken(NULL, " ", "\"", "\""); utilDequote(c); SSM(SCI_SETPROPERTY, (sptr_t)name, (sptr_t)c); DEL(name); DEL(type); continue; } // Is this a 'keywords' line? if (strcasecmp(c, "keywords") == 0) { // keywords number "description" wordlist c = utilGetToken(NULL, " ", "\"", "\""); number = strtol(c, NULL, 10); c = utilGetToken(NULL, " ", "\"", "\""); c = utilGetToken(NULL, "\n", "\"", "\""); if (c && strlen(c) > 0) SSM(SCI_SETKEYWORDS, number, (sptr_t)c); continue; } } DEL(line); fclose(in); } } EVENT void menuEditorEditCopy(GtkWidget *object, gpointer userData) { EditorDataT *self = (EditorDataT *)userData; (void)object; SSM(SCI_COPY, 0, 0); } EVENT void menuEditorEditCut(GtkWidget *object, gpointer userData) { EditorDataT *self = (EditorDataT *)userData; (void)object; SSM(SCI_CUT, 0, 0); } EVENT void menuEditorEditDelete(GtkWidget *object, gpointer userData) { EditorDataT *self = (EditorDataT *)userData; (void)object; SSM(SCI_CLEAR, 0, 0); } EVENT void menuEditorEditPaste(GtkWidget *object, gpointer userData) { EditorDataT *self = (EditorDataT *)userData; (void)object; SSM(SCI_PASTE, 0, 0); } EVENT void menuEditorFileClose(GtkWidget *object, gpointer userData) { EditorDataT *self = (EditorDataT *)userData; (void)object; gtk_window_close(GTK_WINDOW(self->windowData.window)); } EVENT void menuEditorFileNew(GtkWidget *object, gpointer userData) { EditorDataT *self = (EditorDataT *)userData; (void)object; if (self->windowData.isDirty == TRUE) { if (!utilQuestionDialog(self->windowData.window, "New", "You have unsaved changes. Start new?")) { return; } status(self, "New image."); } clearEditor(self); // Clear any filename. DEL(self->windowData.filename); // Mark clean. utilSetDirty((WindowDataT *)self, FALSE); } EVENT void menuEditorFileOpen(GtkWidget *object, gpointer userData) { EditorDataT *self = (EditorDataT *)userData; (void)object; if (utilFileOpen((WindowDataT *)userData, "*.*", "Code")) { clearEditor(self); loadEditor(self); } } EVENT void menuEditorFileSave(GtkWidget *object, gpointer userData) { EditorDataT *self = (EditorDataT *)userData; int length = SSM(SCI_GETLENGTH, 0, 0); FILE *out = NULL; char *code = NULL; // Do we need to save? if (self->windowData.isDirty == TRUE) { // Do we have a filename? If not, kick 'em to SaveAs. if (self->windowData.filename == NULL) { menuEditorFileSaveAs(object, userData); return; } // Allocate space to fetch code from editor. code = (char *)malloc(length + 1); if (!code) { //***TODO*** Something bad happened. return; } // Fetch code. SSM(SCI_GETTEXT, length, (sptr_t)code); out = fopen(self->windowData.filename, "wt"); if (out != NULL) { // Save! fprintf(out, "%s\n", code); fclose(out); status(self, "Saved."); // We're clean now. utilSetDirty((WindowDataT *)self, FALSE); } else { //***TODO*** Something bad happened. } // Release code. DEL(code); } } EVENT void menuEditorFileSaveAs(GtkWidget *object, gpointer userData) { (void)object; if (utilFileSaveAs((WindowDataT *)userData, "*.*", "Code")) { menuEditorFileSave(object, (EditorDataT *)userData); } } EVENT void menuEditorHelpEditor(GtkWidget *object, gpointer userData) { (void)object; (void)userData; gtk_show_uri_on_window(NULL, "https://skunkworks.kangaroopunch.com/skunkworks/joeydev/-/wikis/Code-Editor", GDK_CURRENT_TIME, NULL); } static void status(EditorDataT *self, char *message) { gtk_statusbar_remove_all(GTK_STATUSBAR(self->statusBar), self->statusBarId); gtk_statusbar_push(GTK_STATUSBAR(self->statusBar), self->statusBarId, message); } EVENT gboolean winEditorClose(GtkWidget *object, gpointer userData) { // userData is not reliable due to menuVectorFileClose and util indirectly calling us. EditorDataT *self = (EditorDataT *)utilGetWindowData(object); (void)userData; if (self->windowData.isDirty == TRUE) { if (utilQuestionDialog(self->windowData.window, "Exit", "You have unsaved changes. Exit?")) { winEditorDelete(self); return FALSE; } return TRUE; } winEditorDelete(self); return FALSE; } void winEditorCreate(char *filename) { EditorDataT *self; char *widgetNames[] = { "winEditor", "boxEditorForEditor", "statusEditor", NULL }; GtkWidget **widgets[] = { NULL, NULL, NULL }; // Set up instance data. self = NEW(EditorDataT); self->windowData.closeWindow = winEditorClose; // Load widgets from XML. widgets[0] = &self->windowData.window; widgets[1] = &self->boxForEditor; widgets[2] = &self->statusBar; utilGetWidgetsFromMemory("/com/kangaroopunch/joeydev/Editor.glade", widgetNames, widgets, self); // Register window. utilWindowRegister(self); // Get status bar context ID. self->statusBarId = gtk_statusbar_get_context_id(GTK_STATUSBAR(self->statusBar), "JoeyDev"); // Create Scintilla editor. self->editor = scintilla_new(); self->sci = SCINTILLA(self->editor); self->id = _nextEditorId++; scintilla_set_id(self->sci, self->id); gtk_widget_set_halign(self->editor, GTK_ALIGN_FILL); gtk_widget_set_valign(self->editor, GTK_ALIGN_FILL); gtk_widget_set_hexpand(self->editor, TRUE); gtk_widget_set_vexpand(self->editor, TRUE); gtk_box_set_child_packing(GTK_BOX(self->boxForEditor), self->editor, TRUE, TRUE, 0, GTK_PACK_START); gtk_container_add(GTK_CONTAINER(self->boxForEditor), self->editor); // Configure editor. SSM(SCI_SETCODEPAGE, SC_CP_UTF8, 0); SSM(SCI_SETIMEINTERACTION, SC_IME_WINDOWED, 0); SSM(SCI_STYLESETCHARACTERSET, STYLE_DEFAULT, SC_CHARSET_DEFAULT); SSM(SCI_STYLESETFONT, STYLE_DEFAULT, (sptr_t)"Monospace"); SSM(SCI_STYLESETSIZEFRACTIONAL, STYLE_DEFAULT, 11 * SC_FONT_SIZE_MULTIPLIER); SSM(SCI_STYLESETFORE, STYLE_DEFAULT, 0xFFFFFF); SSM(SCI_STYLESETBACK, STYLE_DEFAULT, 0); SSM(SCI_STYLECLEARALL, 0, 0); SSM(SCI_SETTABWIDTH, 3, 0); //SSM(SCI_SETEOLMODE, SC_EOL_CR, 0); SSM(SCI_SETMARGINWIDTHN, 0, (int)SSM(SCI_TEXTWIDTH, STYLE_LINENUMBER, (sptr_t)"_99999")); SSM(SCI_SETMARGINWIDTHN, 1, 16); SSM(SCI_SETWRAPMODE, SC_WRAP_NONE, 0); SSM(SCI_SETCARETSTYLE, CARETSTYLE_BLOCK | CARETSTYLE_OVERSTRIKE_BLOCK, 0); SSM(SCI_SETCARETFORE, 0x00ffff, 0); SSM(SCI_STYLESETBACK, STYLE_LINENUMBER, 0x222222); SSM(SCI_SETFOLDMARGINCOLOUR, 1, 0x222222); SSM(SCI_SETFOLDMARGINHICOLOUR, 1, 0x222222); SSM(SCI_SETMARGINTYPEN, MARGIN_SCRIPT_FOLD_INDEX, SC_MARGIN_SYMBOL); SSM(SCI_SETMARGINMASKN, MARGIN_SCRIPT_FOLD_INDEX, SC_MASK_FOLDERS); SSM(SCI_SETMARGINWIDTHN, MARGIN_SCRIPT_FOLD_INDEX, 20); SSM(SCI_SETMARGINSENSITIVEN, MARGIN_SCRIPT_FOLD_INDEX, 1); SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDEROPEN, SC_MARK_BOXMINUS); SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDER, SC_MARK_BOXPLUS); SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDERSUB, SC_MARK_VLINE); SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDERTAIL, SC_MARK_LCORNER); SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDEREND, SC_MARK_BOXPLUSCONNECTED); SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDEROPENMID, SC_MARK_BOXMINUSCONNECTED); SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDERMIDTAIL, SC_MARK_TCORNER); SSM(SCI_SETFOLDFLAGS, SC_FOLDFLAG_LINEAFTER_CONTRACTED , 0); // Margin markers. SSM(SCI_MARKERDEFINE, MARKER_ERROR_ARROW, SC_MARK_SHORTARROW); // Error SSM(SCI_MARKERSETBACK, MARKER_ERROR_ARROW, 255 | (0 << 8) | (0 << 16)); // RGB SSM(SCI_MARKERDEFINE, MARKER_ERROR_HIGHLIGHT, SC_MARK_BACKGROUND); // Error SSM(SCI_MARKERSETBACK, MARKER_ERROR_HIGHLIGHT, 127 | (0 << 8) | (0 << 16)); // RGB // Add lexer for language support. self->pLexer = CreateLexer("cpp"); SSM(SCI_SETILEXER, 0, (sptr_t)self->pLexer); writeEditorConfig("cpp", self); loadEditorConfig("cpp", self); // Connect editor to our code. g_signal_connect(G_OBJECT(self->editor), "sci-notify", G_CALLBACK(editorEditorNotify), self); // Show window. gtk_widget_show_all(self->windowData.window); if (filename != NULL) { self->windowData.filename = strdup(filename); loadEditor(self); } } static void winEditorDelete(gpointer userData) { EditorDataT *self = (EditorDataT *)userData; // Scintilla keeps sending events after we delete things it expects to still exist. Prevent that. g_signal_handlers_disconnect_by_func(G_OBJECT(self->editor), G_CALLBACK(editorEditorNotify), self); utilWindowUnRegister(userData); DEL(self); } static void writeEditorConfig(char *lexer, EditorDataT *self) { char *name = NULL; char *tags = NULL; char *description = NULL; char *config = NULL; FILE *out = NULL; int result = -1; int size = -1; int x; config = utilCreateString("%s%cjoeydev%ceditor-%s.conf", g_get_user_config_dir(), UTIL_PATH_CHAR, UTIL_PATH_CHAR, lexer); if (utilFileExists(config)) { // Don't clobber existing files. DEL(config); return; } out = fopen(config, "wt"); DEL(config); if (out) { // Header fprintf(out, "%s\n", LEXER_VERSION); fprintf(out, "------------------------------------------------------------------------------\n"); // Styles result = SSM(SCI_GETNAMEDSTYLES, 0, 0); if (result > 0) { for (x = 0; x < result; x++) { size = SSM(SCI_NAMEOFSTYLE, x, 0); name = (char *)malloc(size + 1); SSM(SCI_NAMEOFSTYLE, x, (sptr_t)name); size = SSM(SCI_TAGSOFSTYLE, x, 0); tags = (char *)malloc(size + 1); SSM(SCI_TAGSOFSTYLE, x, (sptr_t)tags); size = SSM(SCI_DESCRIPTIONOFSTYLE, x, 0); description = (char *)malloc(size + 1); SSM(SCI_DESCRIPTIONOFSTYLE, x, (sptr_t)description); if (strlen(tags) > 0) { fprintf(out, "style %d \"%s\" \"%s\" fore 0xffffff\n", x, tags, description); } DEL(description); DEL(tags); DEL(name); } } fprintf(out, "\n"); // Properties size = SSM(SCI_PROPERTYNAMES, 0, 0); name = (char *)malloc(size + 1); SSM(SCI_PROPERTYNAMES, 0, (sptr_t)name); tags = strtok(name, "\n"); do { size = SSM(SCI_DESCRIBEPROPERTY, (sptr_t)tags, 0); description = (char *)malloc(size + 1); SSM(SCI_DESCRIBEPROPERTY, (sptr_t)tags, (sptr_t)description); result = SSM(SCI_PROPERTYTYPE, (sptr_t)tags, 0); fprintf(out, "property "); switch (result) { case SC_TYPE_BOOLEAN: fprintf(out, "boolean"); size = SSM(SCI_GETPROPERTYINT, (sptr_t)tags, 0); config = utilCreateString("%d", size); break; case SC_TYPE_STRING: fprintf(out, "string"); //***TODO*** This doesn't appear to be working. size = SSM(SCI_GETPROPERTY, (sptr_t)tags, 0); config = (char *)malloc(size + 1); SSM(SCI_GETPROPERTY, (sptr_t)tags, (sptr_t)config); break; case SC_TYPE_INTEGER: fprintf(out, "integer"); size = SSM(SCI_GETPROPERTYINT, (sptr_t)tags, 0); config = utilCreateString("%d", size); break; } fprintf(out, " %s \"%s\" %s\n", tags, description, config); DEL(config); DEL(description); tags = strtok(NULL, "\n"); } while (tags != NULL); DEL(name); fprintf(out, "\n"); // Keyword Sets size = SSM(SCI_DESCRIBEKEYWORDSETS, 0, 0); name = (char *)malloc(size + 1); SSM(SCI_DESCRIBEKEYWORDSETS, 0, (sptr_t)name); tags = strtok(name, "\n"); size = 0; do { fprintf(out, "keywords %d \"%s\"\n", size, tags); size++; tags = strtok(NULL, "\n"); } while (tags != NULL); DEL(name); fclose(out); } }