/* * Copyright (c) 2010-2023 Julien Nadeau Carriere * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE * USE OF THIS SOFTWARE EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * Graphical user interface. */ #include #include #include #include #include #include AG_Object sgVfsRoot; /* General-purpose VFS */ static AG_WindowQ sgEditorWindows; /* Editor windows */ /* * Object classes users can create and edit independently of a specific * scene and scene view. */ const char *sgEditableClasses[] = { "SG:*", "SG_Script:*", "SG_Texture:*", NULL }; /* Save (or discard) the object and exit. */ static void CloseObject(AG_Event *_Nonnull event) { AG_Window *wMain = AG_WINDOW_PTR(1); AG_Object *obj = AG_OBJECT_PTR(2); const int save = AG_INT(3); /* 0 = Discard */ AG_Event ev; if (save) { if (!AG_Defined(obj, "archive-path")) { AG_EventArgs(&ev, "%p,%p,%i", wMain, obj, 1); SG_GUI_SaveAsDlg(&ev); return; } if (AG_ObjectSave(obj) == -1) { AG_TextMsgFromError(); /* TODO suggest "save as" */ return; } } TAILQ_REMOVE(&sgEditorWindows, wMain, user); AG_ObjectDetach(wMain); // AG_ObjectDelete(obj); if (TAILQ_EMPTY(&sgEditorWindows)) AG_Terminate(0); } static void WindowClose(AG_Event *_Nonnull event) { AG_Window *win = AG_WINDOW_SELF(); AG_Object *obj = AG_OBJECT_PTR(1); AG_Event ev; AG_Window *wOther; TAILQ_FOREACH(wOther, &sgEditorWindows, user) { if (wOther != win && AG_GetPointer(wOther, "object") == obj) break; } if (wOther == NULL && /* Last editor window for this object */ AG_ObjectChanged(obj)) { AG_Window *wDlg; AG_Label *lbl; AG_Button *btn; AG_Box *box; if ((wDlg = AG_WindowNew(AG_WINDOW_MODAL | AG_WINDOW_NOTITLE | AG_WINDOW_NORESIZE)) == NULL) { goto out; } wDlg->wmType = AG_WINDOW_WM_DIALOG; AG_WindowSetCaptionS(wDlg, _("Save changes before closing?")); AG_WindowSetPosition(wDlg, AG_WINDOW_CENTER, 0); lbl = AG_LabelNew(wDlg, 0, _("Save changes to " AGSI_BOLD "%s" AGSI_RST " ?"), obj->name); AG_SetFontSize(lbl, "110%"); AG_SetPadding(lbl, "10"); box = AG_BoxNewHoriz(wDlg, 0); AG_SetPadding(box, "5"); { AG_ButtonNewFn(box, AG_BUTTON_VFILL, _("Save As"), CloseObject, "%p,%p,%i", win, obj, 1); btn = AG_ButtonNewFn(box, AG_BUTTON_VFILL, _("Cancel"), AGWINDETACH(wDlg)); AG_WidgetFocus(btn); AG_ButtonNewFn(box, AG_BUTTON_VFILL, _("Discard Changes"), CloseObject, "%p,%p,%i", win, obj, 0); } AG_WindowAttach(win, wDlg); AG_WindowShow(wDlg); return; } out: AG_EventArgs(&ev, "%p,%p,%i", win, obj, 0); CloseObject(&ev); } /* Open a Agar-SG object for edition. */ AG_Window * SG_GUI_OpenObject(void *p) { AG_Object *obj = p; AG_Window *win = NULL; AG_Widget *wEdit; /* Invoke edit(), which may return a Window or some other Widget. */ if ((wEdit = obj->cls->edit(obj)) == NULL) { AG_SetError("%s no edit()", obj->cls->name); return (NULL); } if (AG_WINDOW_ISA(wEdit)) { Debug(NULL, "SG_GUI: %s->edit() returned Window %s\n", obj->name, wEdit ? OBJECT(wEdit)->name : "NULL"); win = (AG_Window *)wEdit; } else if (AG_WIDGET_ISA(wEdit)) { Debug(NULL, "SG_GUI: %s->edit() returned Widget %s\n", obj->name, wEdit ? OBJECT(wEdit)->name : "NULL"); if ((win = AG_WindowNew(AG_WINDOW_MAIN)) == NULL) { return (NULL); } AG_ObjectAttach(win, wEdit); } else { AG_SetError("%s: edit() illegal object", obj->cls->name); return (NULL); } AG_WindowSetCaptionS(win, AG_Defined(obj,"archive-path") ? AG_GetStringP(obj,"archive-path") : obj->name); AG_SetEvent(win, "window-close", WindowClose, "%p", obj); AG_SetPointer(win, "object", obj); AG_PostEvent(obj, "edit-open", NULL); TAILQ_INSERT_TAIL(&sgEditorWindows, win, user); AG_WindowShow(win); return (win); } /* Create a new instance of a native Agar-SG object class. */ void SG_GUI_NewObject(AG_Event *event) { AG_ObjectClass *cl = AG_PTR(1); AG_Object *obj; if (cl == &sgClass) { obj = (AG_Object *)SG_New(&sgVfsRoot, NULL, 0); } else { obj = AG_ObjectNew(&sgVfsRoot, NULL, cl); } if (obj == NULL || SG_GUI_OpenObject(obj) == NULL) { goto fail; } return; fail: AG_TextError(_("Failed to create object: %s"), AG_GetError()); if (obj != NULL) { AG_ObjectDelete(obj); } } /* Load a native Agar-SG object file. */ AG_Object * SG_GUI_LoadObject(AG_ObjectClass *cls, const char *path) { AG_Object *obj; if ((obj = AG_ObjectNew(&sgVfsRoot, NULL, cls)) == NULL) { return (NULL); } if (AG_ObjectLoadFromFile(obj, path) == -1) { AG_SetError("%s: %s", AG_ShortFilename(path), AG_GetError()); goto fail; } AG_SetString(obj, "archive-path", path); AG_ObjectSetNameS(obj, AG_ShortFilename(path)); if (SG_GUI_OpenObject(obj) == NULL) { goto fail; } return (obj); fail: AG_ObjectDelete(obj); return (NULL); } /* Load a native Agar-SG object file. */ static void LoadObject(AG_Event *_Nonnull event) { AG_ObjectClass *cls = AG_PTR(1); const char *path = AG_STRING(2); if (SG_GUI_LoadObject(cls, path) == NULL) AG_TextMsgFromError(); } /* "Open..." dialog */ void SG_GUI_OpenDlg(AG_Event *event) { AG_Window *wMain = AG_WINDOW_PTR(1); AG_Window *win; AG_FileDlg *fd; int j; if ((win = AG_WindowNew(0)) == NULL) { return; } AG_WindowSetCaptionS(win, _("Open...")); fd = AG_FileDlgNewMRU(win, "sg-objs", AG_FILEDLG_LOAD | AG_FILEDLG_CLOSEWIN | AG_FILEDLG_EXPAND); AG_FileDlgSetOptionContainer(fd, AG_BoxNewVert(win, AG_BOX_HFILL)); for (j = 0; j < agFileExtCount; j++) { const AG_FileExtMapping *me = &agFileExtMap[j]; char lbl[64], ext[16]; Snprintf(lbl, sizeof(lbl), _("Agar-SG %s file"), me->descr); Strlcpy(ext, "*", sizeof(ext)); Strlcat(ext, me->ext, sizeof(ext)); AG_FileDlgAddType(fd, lbl, ext, LoadObject, "%p", me->cls); } AG_WindowAttach(wMain, win); AG_WindowShow(win); } /* Save an object file in native Agar-SG format. */ static void SaveObject(AG_Event *_Nonnull event) { AG_Window *wMain = AG_WINDOW_PTR(1); AG_Object *obj = AG_OBJECT_PTR(2); const int saveAndClose = AG_INT(3); const char *path = AG_STRING(4); const char *pathShort; AG_Event ev; pathShort = AG_ShortFilename(path); if (AG_ObjectSaveToFile(obj, path) == -1) { AG_TextError("%s: %s", pathShort, AG_GetError()); return; } if (saveAndClose) { AG_EventArgs(&ev, "%p,%p,%i", wMain, obj, 0); CloseObject(&ev); } else { AG_SetString(obj, "archive-path", path); AG_ObjectSetNameS(obj, pathShort); AG_WindowSetCaptionS(wMain, pathShort); } AG_TextTmsg(AG_MSG_INFO, 2000, _("Object saved to %s"), pathShort); } /* "Save as..." dialog. */ void SG_GUI_SaveAsDlg(AG_Event *event) { AG_Window *wMain = AG_WINDOW_PTR(1); AG_Object *obj = AG_OBJECT_PTR(2); int saveAndClose = AG_INT(3); char defDir[AG_PATHNAME_MAX]; AG_Window *win; AG_FileDlg *fd; int j; if (obj == NULL) { AG_TextError(_("No document is selected for saving.")); return; } if ((win = AG_WindowNew(0)) == NULL) { return; } AG_WindowSetCaption(win, _("Save %s as..."), obj->name); fd = AG_FileDlgNew(win, AG_FILEDLG_SAVE | AG_FILEDLG_CLOSEWIN | AG_FILEDLG_EXPAND); AG_FileDlgSetOptionContainer(fd, AG_BoxNewVert(win, AG_BOX_HFILL)); AG_ConfigGetPath(AG_CONFIG_PATH_DATA, 0, defDir, sizeof(defDir)); AG_FileDlgSetDirectoryMRU(fd, "agar-sg.mru.files", defDir); for (j = 0; j < agFileExtCount; j++) { const AG_FileExtMapping *me = &agFileExtMap[j]; AG_ObjectClass *cls = (AG_ObjectClass *)me->cls; char lbl[64], ext[16]; if (AG_OfClass(obj, cls->hier)) { Snprintf(lbl, sizeof(lbl), _("Agar-SG %s file"), me->descr); Strlcpy(ext, "*", sizeof(ext)); Strlcat(ext, me->ext, sizeof(ext)); AG_FileDlgAddType(fd, lbl, ext, SaveObject, "%p,%p,%i", wMain, obj, saveAndClose); } } AG_WindowAttach(wMain, win); AG_WindowShow(win); } /* "Save" action */ void SG_GUI_Save(AG_Event *event) { AG_Window *wMain = AG_WINDOW_PTR(1); AG_Object *obj = AG_OBJECT_PTR(2); const int saveAndClose = AG_INT(3); char *archivePath; AG_Event ev; if (obj == NULL) { AG_TextError(_("No document is selected for saving.")); return; } if (!AG_Defined(obj,"archive-path") || (archivePath = AG_GetStringP(obj,"archive-path")) == NULL || archivePath[0] == '\0') { AG_EventArgs(&ev, "%p,%p,%i", wMain, obj, saveAndClose); SG_GUI_SaveAsDlg(&ev); return; } if (AG_ObjectSave(obj) == -1) { AG_TextError(_("Error saving object: %s"), AG_GetError()); } else { AG_TextTmsg(AG_MSG_INFO, 1250, _("Saved %s to %s"), obj->name, archivePath); } } /* Undo last action. */ void SG_GUI_Undo(AG_Event *event) { /* TODO */ } /* Redo last undone action. */ void SG_GUI_Redo(AG_Event *event) { /* TODO */ } /* Standard Edit / Preferences dialog. */ void SG_GUI_EditPreferences(AG_Event *event) { } static void SelectedFont(AG_Event *_Nonnull event) { AG_Window *win = AG_WINDOW_PTR(1); Strlcpy(agConfig->fontFace, OBJECT(agDefaultFont)->name, sizeof(agConfig->fontFace)); agConfig->fontSize = agDefaultFont->spec.size; agConfig->fontFlags = agDefaultFont->flags; (void)AG_ConfigSave(); AG_TextWarning("default-font-changed", _("The default font has been changed.\n" "Please restart application for this change to take effect.")); AG_ObjectDetach(win); } /* "Select font" dialog */ void SG_GUI_SelectFontDlg(AG_Event *event) { AG_Window *wMain = AG_WINDOW_PTR(1); AG_Window *win; AG_FontSelector *fs; AG_Box *hBox; if ((win = AG_WindowNew(0)) == NULL) { return; } AG_WindowSetCaptionS(win, _("Font selection")); fs = AG_FontSelectorNew(win, AG_FONTSELECTOR_EXPAND); AG_BindPointer(fs, "font", (void *)&agDefaultFont); hBox = AG_BoxNewHoriz(win, AG_BOX_HFILL|AG_BOX_HOMOGENOUS); AG_ButtonNewFn(hBox, 0, _("OK"), SelectedFont, "%p", win); AG_ButtonNewFn(hBox, 0, _("Cancel"), AGWINCLOSE(win)); AG_SeparatorNewHoriz(win); #ifdef __APPLE__ AG_LabelNewS(win, 0, _("Note: Use [Command] and [-] or [=] to zoom current window.")); #else AG_LabelNewS(win, 0, _("Note: Use [Ctrl] and [-] or [=] to zoom current window.")); #endif AG_WindowAttach(wMain, win); AG_WindowShow(win); } /* "Create new" dialog (used by sgedit(1) when called without arguments) */ void SG_GUI_CreateNewDlg(AG_Event *event) { AG_Window *win; const AG_FileExtMapping *me; AG_Label *lbl; int i; if ((win = AG_WindowNew(AG_WINDOW_MAIN)) == NULL) { return; } AG_WindowSetCaptionS(win, "sgedit"); AG_SetPadding(win, "20"); lbl = AG_LabelNewS(win, AG_LABEL_HFILL, "sgedit"); AG_LabelJustify(lbl, AG_TEXT_CENTER); AG_SetFontSize(lbl, "250%"); AG_SetTextColor(lbl, "#aaeeff"); AG_SetFontWeight(lbl, "bold"); AG_LabelNewS(win, 0, _("Create New:")); for (i = 0; i < agFileExtCount; i++) { me = &agFileExtMap[i]; if (!me->editDirect) { continue; } AG_ButtonNewFn(win, AG_BUTTON_HFILL, me->descr, SG_GUI_NewObject, "%p", me->cls); } AG_SeparatorNewHoriz(win); AG_ButtonNewFn(win, AG_BUTTON_HFILL, _("Quit"), AGWINCLOSE(win)); AG_WindowSetPosition(win, AG_WINDOW_MC, 0); AG_WindowShow(win); } /* Create a read-only view for a scene. */ void SG_GUI_CreateNewView(AG_Event *event) { char name[AG_OBJECT_NAME_MAX]; AG_Window *wMain = AG_WINDOW_PTR(1); SG_View *svOrig = SG_VIEW_PTR(2), *sv; const int shareCam = AG_INT(3); SG *sg = svOrig->sg; AG_Window *win; int num = 0; if ((win = AG_WindowNew(0)) == NULL) { return; } AG_WindowSetCaptionS(win, OBJECT(sg)->name); sv = SG_ViewNew(win, sg, SG_VIEW_EXPAND); tryname: AG_Snprintf(name, sizeof(name), "cView%d", num++); if (SG_FindNode(sg, name) != NULL) { goto tryname; } sv->cam = shareCam ? svOrig->cam : SG_CameraNewDuplicate(sg->root, name, svOrig->cam); AG_RedrawOnTick(sv, 16); AG_WindowSetGeometryAlignedPct(win, AG_WINDOW_TR, 40, 30); AG_WindowAttach(wMain, win); AG_WindowShow(win); } static void CloseEditor(AG_Event *_Nonnull event) { AG_Menu *m = AG_MENU_SELF(); AG_Window *win = AG_ParentWindow(m); if (win) AG_PostEvent(win, "window-close", NULL); } /* Build a generic "File" menu. */ void SG_FileMenu(AG_MenuItem *m, void *obj, AG_Window *wMain) { const AG_FileExtMapping *me = NULL; int j; for (j = 0; j < agFileExtCount; j++) { me = &agFileExtMap[j]; if (!me->editDirect) { continue; } AG_MenuAction(m, AG_Printf("New %s...", me->descr), agIconDoc.s, SG_GUI_NewObject, "%p", me->cls); } AG_MenuSeparator(m); AG_MenuActionKb(m, _("Open..."), agIconLoad.s, AG_KEY_O, AG_KEYMOD_CTRL, SG_GUI_OpenDlg, "%p", wMain); AG_MenuActionKb(m, _("Save"), agIconSave.s, AG_KEY_S, AG_KEYMOD_CTRL, SG_GUI_Save, "%p,%p,%i", wMain, obj, 0); AG_MenuAction(m, _("Save as..."), agIconSave.s, SG_GUI_SaveAsDlg, "%p,%p,%i", wMain, obj, 0); AG_MenuSeparator(m); AG_MenuAction(m, _("Close"), agIconClose.s, CloseEditor, "%p", obj); } /* Build a generic "Edit" menu. */ void SG_EditMenu(AG_MenuItem *m, void *obj, AG_Window *wMain) { AG_MenuActionKb(m, _("Undo"), agIconUp.s, AG_KEY_Z, AG_KEYMOD_CTRL, SG_GUI_Undo, "%p", obj); AG_MenuActionKb(m, _("Redo"), agIconDown.s, AG_KEY_R, AG_KEYMOD_CTRL, SG_GUI_Redo, "%p", obj); AG_MenuSeparator(m); AG_MenuAction(m, _("Font preferences..."), agIconMagnifier.s, SG_GUI_SelectFontDlg, "%p", wMain); } /* Build a generic "View" menu. */ void SG_ViewMenu(AG_MenuItem *m, void *obj, AG_Window *wMain, SG_View *sv) { AG_MenuAction(m, _("New view..."), NULL, SG_GUI_CreateNewView, "%p,%p,%i", wMain, sv, 0); AG_MenuAction(m, _("New view (share camera)..."), NULL, SG_GUI_CreateNewView, "%p,%p,%i", wMain, sv, 1); // AG_MenuSeparator(m); // AG_MenuAction(m, _("Switch camera..."), NULL, // SG_GUI_SwitchCameraDlg, "%p", sv); } /* Initialize Agar-SG GUI globals. */ void SG_InitGUI(void) { if (agGUI) { AG_RegisterClass(&sgViewClass); sgIcon_Init(); } AG_ObjectInit(&sgVfsRoot, NULL); sgVfsRoot.flags |= AG_OBJECT_STATIC; AG_ObjectSetName(&sgVfsRoot, "Agar-SG VFS"); TAILQ_INIT(&sgEditorWindows); } /* Release Agar-SG GUI globals. */ void SG_DestroyGUI(void) { if (agGUI) { AG_UnregisterClass(&sgViewClass); } AG_ObjectDestroy(&sgVfsRoot); }