diff --git a/developer/debug/test/application/apptest.c b/developer/debug/test/application/apptest.c new file mode 100644 index 00000000000..ef6c703871e --- /dev/null +++ b/developer/debug/test/application/apptest.c @@ -0,0 +1,970 @@ +/* + Copyright (C) 2026, The AROS Development Team. All rights reserved. + + Desc: application.library test. Prints one PASS/FAIL line per check; + with WAITBACK=n it then waits n seconds for the pop-up's + double-click message. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +struct Library *ApplicationBase; +struct RxsLib *RexxSysBase; + +static int passed, failed; + +static void check(BOOL ok, const char *what) +{ + printf("%s %s\n", ok ? "PASS" : "FAIL", what); + if (ok) + passed++; + else + failed++; +} + +/* Take every message waiting on a port; return how many had 'type' (and, + if from is not ~0, that sender). */ +static int drainfrom(struct MsgPort *port, ULONG type, ULONG from, char *text, int textsize) +{ + struct ApplicationMsg *am; + int n = 0; + + while ((am = (struct ApplicationMsg *)GetMsg(port))) + { + if (am->type == type && (from == (ULONG)~0 || am->senderAppID == from)) + { + n++; + if (text && type == APPLIBMT_CustomMsg) + { + strncpy(text, ((struct ApplicationCustomMsg *)am)->customMsg, textsize - 1); + text[textsize - 1] = '\0'; + } + } + ReplyMsg(&am->msg); + } + return n; +} + +static int drain(struct MsgPort *port, ULONG type, char *text, int textsize) +{ + return drainfrom(port, type, (ULONG)~0, text, textsize); +} + +/* + * Send one command to an ARexx port the way Hollywood's SendRexxCommand() + * does: a RexxMsg built with rexxsyslib, no interpreter involved. + * Returns rm_Result1, or -1 if the port is missing; rc2 gets rm_Result2. + */ +static LONG rexx(CONST_STRPTR portname, CONST_STRPTR command, LONG *rc2) +{ + struct MsgPort *reply, *port; + struct RexxMsg *rm; + LONG rc = -1; + + if (!RexxSysBase || !(reply = CreateMsgPort())) + return -1; + if ((rm = CreateRexxMsg(reply, NULL, NULL))) + { + rm->rm_Args[0] = (IPTR)CreateArgstring((STRPTR)command, strlen(command)); + rm->rm_Action = RXCOMM; + Forbid(); + if ((port = FindPort(portname))) + PutMsg(port, &rm->rm_Node); + Permit(); + if (port) + { + WaitPort(reply); + GetMsg(reply); + rc = rm->rm_Result1; + if (rc2) + *rc2 = rm->rm_Result2; + } + DeleteArgstring((UBYTE *)rm->rm_Args[0]); + DeleteRexxMsg(rm); + } + DeleteMsgPort(reply); + return rc; +} + +/* A memory stream for ALPO_Serialize / ALPO_Deserialize. */ +struct MemIO +{ + char buf[4096]; + LONG len, pos, back; +}; + +static void mem_Put(LONG c, struct ALPOPutProcInfo *pi) +{ + struct MemIO *m = pi->handlePtr; + + if (m->len < (LONG)sizeof(m->buf) - 1) + m->buf[m->len++] = c; + m->buf[m->len] = '\0'; +} + +static LONG mem_Get(struct ALPOGetProcInfo *pi) +{ + struct MemIO *m = pi->handlePtr; + + if (m->back >= 0) + { + LONG c = m->back; + + m->back = -1; + return c; + } + return m->pos < m->len ? (UBYTE)m->buf[m->pos++] : -1; +} + +static LONG mem_UnGet(LONG c, struct ALPOGetProcInfo *pi) +{ + ((struct MemIO *)pi->handlePtr)->back = c; + return c; +} + +/* Read 'xml' into a new dictionary; the error goes to *err, the dictionary + back (or NULL when the type was not a dictionary). */ +static PrefsObject *fromxml(struct MemIO *mem, CONST_STRPTR xml, ULONG *err) +{ + struct ALPOGetProcInfo gpi; + TEXT otag[TAGBUFFER_SIZE]; + PrefsObject *dict = NULL; + + if ((const char *)xml != mem->buf) + { + memset(mem, 0, sizeof(*mem)); + strncpy(mem->buf, xml, sizeof(mem->buf) - 1); + } + mem->len = strlen(mem->buf); + mem->pos = 0; + mem->back = -1; + memset(&gpi, 0, sizeof(gpi)); + gpi.getChProc = mem_Get; + gpi.unGetChProc = mem_UnGet; + gpi.handlePtr = mem; + gpi.key_otag = otag; + gpi.encoding = ALPOENC_UTF8; + *err = 0; + if (BeginDeserialization(&gpi) == ALPOT_Dictionary) + { + dict = PrefsDictionary(NULL, NULL, ALPO_Alloc, 0, TAG_DONE); + PrefsDictionary(dict, err, ALPO_Deserialize, (IPTR)&gpi, TAG_DONE); + } + else + *err = ALPOEF_WRONG_OTAG; + return dict; +} + +static double realof(PrefsObject *dict, CONST_STRPTR key) +{ + double d = 0; + PrefsObject *o = DictGetObjectForKey(dict, key); + + if (o) + PrefsNumber(o, NULL, ALPONUM_GetDouble, (IPTR)&d, TAG_DONE); + return d; +} + +static PrefsObject *num(LONG v) +{ + return PrefsNumber(NULL, NULL, ALPONUM_AllocSetLong, v, TAG_DONE); +} + +static PrefsObject *str(CONST_STRPTR s) +{ + return PrefsString(NULL, NULL, ALPOSTR_AllocSetString, (IPTR)s, TAG_DONE); +} + +static void release(PrefsObject *o) +{ + PrefsBaseObject(o, NULL, ALPO_Release, 0, TAG_DONE); +} + +static BOOL env_exists(CONST_STRPTR path) +{ + BPTR lock = Lock(path, SHARED_LOCK); + + if (lock) + UnLock(lock); + return lock != 0; +} + +/* A dictionary with one of everything. */ +static PrefsObject *sample(void) +{ + static UBYTE bytes[5] = { 0, 1, 0xfe, 0xff, 'x' }; + struct ALPOBinData bd = { bytes, sizeof(bytes) }; + double d = -3.25; + PrefsObject *dict, *arr; + + dict = PrefsDictionary(NULL, NULL, ALPO_Alloc, 0, TAG_DONE); + DictSetObjectForKey(dict, str("a & \"quotes\" caf\xe9"), "String"); + DictSetObjectForKey(dict, num(-42), "Integer"); + DictSetObjectForKey(dict, PrefsNumber(NULL, NULL, ALPONUM_AllocSetBool, TRUE, TAG_DONE), "Switch"); + DictSetObjectForKey(dict, PrefsNumber(NULL, NULL, ALPONUM_AllocSetDouble, (IPTR)&d, TAG_DONE), "Real"); + DictSetObjectForKey(dict, PrefsDate(NULL, NULL, ALPODAT_AllocSetCTime, 1760000000, TAG_DONE), "When"); + DictSetObjectForKey(dict, PrefsBinary(NULL, NULL, ALPOBIN_AllocWithData, (IPTR)&bd, TAG_DONE), "Blob"); + DictSetObjectForKey(dict, str("SecondOption"), "Choice"); + arr = PrefsArray(NULL, NULL, ALPO_Alloc, 0, + ALPOARR_AddObj, (IPTR)num(1), + ALPOARR_AddObj, (IPTR)num(2), + ALPOARR_AddObj, (IPTR)num(3), + TAG_DONE); + DictSetObjectForKey(dict, arr, "this is an array"); + return dict; +} + +/* TRUE if dict holds what sample() put in. */ +static BOOL same(PrefsObject *dict) +{ + static CONST_STRPTR options[] = { "FirstOption", "SecondOption", "ThirdOption", NULL }; + PrefsObject *o, *arr; + struct ALPOObjIndex oi; + struct ALPOBinData bd = { NULL, 0 }; + double d = 0; + ULONG secs = 0, type = 0; + LONG count = 0; + + if (strcmp(DictGetStringForKey(dict, "String", ""), "a & \"quotes\" caf\xe9") != 0) + return FALSE; + if (DictGetIntegerForKey(dict, "Integer", 0) != -42 || !DictGetBoolForKey(dict, "Switch", FALSE)) + return FALSE; + if (!(o = DictGetObjectForKey(dict, "Real"))) + return FALSE; + PrefsNumber(o, NULL, ALPONUM_GetDouble, (IPTR)&d, TAG_DONE); + if (d != -3.25) + return FALSE; + if (!(o = DictGetObjectForKey(dict, "When"))) + return FALSE; + PrefsDate(o, NULL, ALPODAT_GetSecsSince1978, (IPTR)&secs, TAG_DONE); + if (secs != 1760000000UL - 2922UL * 86400UL) + return FALSE; + if (!(o = DictGetObjectForKey(dict, "Blob"))) + return FALSE; + PrefsBinary(o, NULL, ALPOBIN_GetDataStruct, (IPTR)&bd, TAG_DONE); + if (bd.size != 5 || bd.data[0] != 0 || bd.data[2] != 0xfe || bd.data[4] != 'x') + return FALSE; + if (DictGetOptionForKey(dict, "Choice", options, -1) != 1) + return FALSE; + arr = DictGetObjectForKey(dict, "this is an array"); + PrefsBaseObject(arr, NULL, ALPO_Identify, (IPTR)&type, TAG_DONE); + PrefsArray(arr, NULL, ALPOARR_GetCount, (IPTR)&count, TAG_DONE); + oi.index = 2; + PrefsArray(arr, NULL, ALPOARR_GetObjAtIndex, (IPTR)&oi, TAG_DONE); + return type == ALPOT_Array && count == 3 && oi.obj + && PrefsNumber(oi.obj, NULL, ALPONUM_GetLong, (IPTR)&count, TAG_DONE) && count == 3; +} + +static void test_prefsobjects(void) +{ + PrefsObject *dict, *copy, *back, *o; + struct ALPOPutProcInfo ppi; + struct ALPOGetProcInfo gpi; + struct ALPOObjKeyIndex oki; + struct ALPOObjIndex oi; + struct MemIO *mem; + TEXT otag[TAGBUFFER_SIZE]; + ULONG err = 0, type; + LONG count = -1, rc; + + dict = sample(); + check(dict && same(dict), "PrefsObjects: every type stored and read back"); + + copy = PrefsDictionary(dict, &err, ALPO_Copy, 0, TAG_DONE); + DictSetObjectForKey(copy, num(7), "Integer"); + check(err == 0 && copy != dict && DictGetIntegerForKey(dict, "Integer", 0) == -42 + && DictGetIntegerForKey(copy, "Integer", 0) == 7, "ALPO_Copy is deep"); + release(copy); + + o = DictGetObjectForKey(dict, "String"); + PrefsBaseObject(o, NULL, ALPO_Retain, 0, TAG_DONE); + PrefsDictionary(dict, NULL, ALPODICT_RemoveObjForKey, (IPTR)"String", TAG_DONE); + count = 0; + PrefsBaseObject(o, NULL, ALPO_GetRetainCount, (IPTR)&count, TAG_DONE); + check(count == 1 && DictGetObjectForKey(dict, "String") == NULL, "retained object survives its removal"); + DictSetObjectForKey(dict, o, "String"); + + err = 0; + PrefsString(DictGetObjectForKey(dict, "Integer"), &err, ALPOSTR_SetString, (IPTR)"x", TAG_DONE); + check(err & ALPOEF_WRONG_OBJECTTYPE, "a method of the wrong type is refused"); + + oki.obj = num(1); + oki.key = "Integer"; + oki.index = 0; + err = 0; + PrefsDictionary(dict, &err, ALPODICT_InsertObjAtIndex, (IPTR)&oki, TAG_DONE); + check(err & ALPOEF_DUPLICATE, "inserting a key twice is refused"); + release(oki.obj); + + PrefsDictionary(dict, NULL, ALPODICT_SortAscending, 0, TAG_DONE); + oki.index = 0; + PrefsDictionary(dict, NULL, ALPODICT_GetObjAndKeyAtIndex, (IPTR)&oki, TAG_DONE); + check(oki.key && strcmp(oki.key, "Blob") == 0, "dictionary sorted by key"); + + o = DictGetObjectForKey(dict, "this is an array"); + oi.obj = num(0); + oi.index = 0; + PrefsArray(o, NULL, ALPOARR_InsertObjAtIndex, (IPTR)&oi, TAG_DONE); + oi.index = -1; + PrefsArray(o, NULL, ALPOARR_GetIndexForObj, (IPTR)&oi, TAG_DONE); + rc = oi.index; + PrefsArray(o, NULL, ALPOARR_RemoveObj, (IPTR)oi.obj, ALPOARR_GetCount, (IPTR)&count, TAG_DONE); + check(rc == 0 && count == 3, "array insert, find and remove"); + + /* Serialize to memory and read it back by hand */ + mem = AllocVec(sizeof(*mem), MEMF_CLEAR); + memset(&ppi, 0, sizeof(ppi)); + ppi.putChProc = mem_Put; + ppi.handlePtr = mem; + ppi.encoding = ALPOENC_UTF8; + err = 0; + PrefsDictionary(dict, &err, ALPO_Serialize, (IPTR)&ppi, TAG_DONE); + check(err == 0 && strstr(mem->buf, "Integer") && strstr(mem->buf, "<tag> &") + && strstr(mem->buf, "caf\xc3\xa9"), "ALPO_Serialize writes escaped UTF-8 XML"); + + memset(&gpi, 0, sizeof(gpi)); + mem->back = -1; + gpi.getChProc = mem_Get; + gpi.unGetChProc = mem_UnGet; + gpi.handlePtr = mem; + gpi.key_otag = otag; + gpi.encoding = ALPOENC_UTF8; + type = BeginDeserialization(&gpi); + back = PrefsDictionary(NULL, NULL, ALPO_Alloc, 0, TAG_DONE); + err = 0; + PrefsDictionary(back, &err, ALPO_Deserialize, (IPTR)&gpi, TAG_DONE); + check(type == ALPOT_Dictionary && err == 0 && same(back), "BeginDeserialization + ALPO_Deserialize"); + release(back); + FreeVec(mem); + + /* Files */ + err = WritePrefs(dict, WRITEPREFS_FileName, (IPTR)"RAM:apptest-prefs.xml", TAG_DONE); + back = PrefsDictionary(NULL, NULL, ALPO_Alloc, 0, TAG_DONE); + DictSetObjectForKey(back, num(1), "Stale"); + rc = ReadPrefs(back, READPREFS_FileName, (IPTR)"RAM:apptest-prefs.xml", TAG_DONE); + check(err == 0 && rc == 0 && same(back) && !DictGetObjectForKey(back, "Stale"), + "WritePrefs + ReadPrefs round trip (dictionary emptied first)"); + /* only the ".new" copy left, as after a reset between delete and rename */ + Rename("RAM:apptest-prefs.xml", "RAM:apptest-prefs.xml.new"); + rc = ReadPrefs(back, READPREFS_FileName, (IPTR)"RAM:apptest-prefs.xml", TAG_DONE); + check(rc == 0 && same(back), "ReadPrefs falls back to name.new when name is missing"); + DeleteFile("RAM:apptest-prefs.xml.new"); + DeleteFile("RAM:apptest-prefs.xml"); + + /* the rename at the end of a write fails when a drawer is in the way: + the new copy must survive for the reader's fallback */ + { + BPTR lock = CreateDir("RAM:apptest-drawer"); + BPTR fh = Open("RAM:apptest-drawer/keep", MODE_NEWFILE); + + if (lock) + UnLock(lock); + if (fh) + Close(fh); + err = WritePrefs(dict, WRITEPREFS_FileName, (IPTR)"RAM:apptest-drawer", TAG_DONE); + check((err & ALPOEF_FILEERROR) && env_exists("RAM:apptest-drawer.new"), + "a failed rename reports the error and keeps name.new"); + rc = ReadPrefs(back, READPREFS_FileName, (IPTR)"RAM:apptest-drawer", TAG_DONE); + check(rc == 0 && same(back), "ReadPrefs() reads name.new after a failed rename"); + DeleteFile("RAM:apptest-drawer.new"); + DeleteFile("RAM:apptest-drawer/keep"); + DeleteFile("RAM:apptest-drawer"); + } + rc = ReadPrefs(back, READPREFS_FileName, (IPTR)"RAM:no-such-prefs.xml", TAG_DONE); + count = -1; + PrefsDictionary(back, NULL, ALPODICT_GetCount, (IPTR)&count, TAG_DONE); + check(rc != 0 && count == 0, "ReadPrefs of a missing file fails and leaves it empty"); + release(back); + + check(DictGetIntegerForKey(NULL, "x", 5) == 5 && DictGetStringForKey(NULL, "x", NULL) == NULL, + "Dict getters on NULL give the default"); + + /* Reals of any size survive a save and reload */ + { + double big = 3e9, tiny = 1e-12, neg = -123456.789, third = 1.0 / 3.0; + + PrefsDictionary(dict, NULL, ALPODICT_Clear, 0, TAG_DONE); + DictSetObjectForKey(dict, PrefsNumber(NULL, NULL, ALPONUM_AllocSetDouble, (IPTR)&big, TAG_DONE), "big"); + DictSetObjectForKey(dict, PrefsNumber(NULL, NULL, ALPONUM_AllocSetDouble, (IPTR)&tiny, TAG_DONE), "tiny"); + DictSetObjectForKey(dict, PrefsNumber(NULL, NULL, ALPONUM_AllocSetDouble, (IPTR)&neg, TAG_DONE), "neg"); + DictSetObjectForKey(dict, PrefsNumber(NULL, NULL, ALPONUM_AllocSetDouble, (IPTR)&third, TAG_DONE), "third"); + mem = AllocVec(sizeof(*mem), MEMF_CLEAR); + memset(&ppi, 0, sizeof(ppi)); + ppi.putChProc = mem_Put; + ppi.handlePtr = mem; + ppi.encoding = ALPOENC_UTF8; + err = 0; + PrefsDictionary(dict, &err, ALPO_Serialize, (IPTR)&ppi, TAG_DONE); + back = err ? NULL : fromxml(mem, mem->buf, &err); + check(back && err == 0 && realof(back, "big") == 3e9 && realof(back, "tiny") == 1e-12 + && realof(back, "neg") == -123456.789 && realof(back, "third") == 1.0 / 3.0, + "reals 3e9, 1e-12, -123456.789 and 1/3 round-trip exactly"); + if (!back || err || realof(back, "third") != 1.0 / 3.0) + printf(" err 0x%lx, %.*s\n", (unsigned long)err, mem->len > 400 ? 400 : (int)mem->len, mem->buf); + if (back) + release(back); + FreeVec(mem); + } + + /* A hostile file: deep nesting, a huge exponent, a comment ending in --->, the same object stored again */ + { + static const char deep[] = + "a" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + LONG retain = 0; + + mem = AllocVec(sizeof(*mem), MEMF_CLEAR); + back = fromxml(mem, deep, &err); + check((err & ALPOEF_XMLERROR) != 0, "forty nested arrays are refused"); + if (!(err & ALPOEF_XMLERROR)) + printf(" err 0x%lx\n", (unsigned long)err); + if (back) + release(back); + + back = fromxml(mem, "r1e2000000000i1e2000000000" + "m-1e-2000000000", &err); + check(back && err == 0 && realof(back, "r") == realof(back, "r") && realof(back, "r") <= 1.7976931348623157e308 + && realof(back, "r") > 1e308 && DictGetIntegerForKey(back, "i", 0) == 0x7fffffff + && realof(back, "m") == 0, "a huge exponent gives a finite clamp, not a hang"); + if (back) + release(back); + + back = fromxml(mem, "k5", + &err); + check(back && err == 0 && DictGetIntegerForKey(back, "k", 0) == 5, "a comment ending in ---> is skipped"); + if (back) + { + struct ALPOObjKey ok2; + + o = DictGetObjectForKey(back, "k"); + ok2.obj = o; + ok2.key = "k"; + PrefsBaseObject(o, NULL, ALPO_Retain, 0, TAG_DONE); + PrefsDictionary(back, NULL, ALPODICT_ReplaceObjForKey, (IPTR)&ok2, TAG_DONE); + PrefsBaseObject(o, NULL, ALPO_GetRetainCount, (IPTR)&retain, TAG_DONE); + check(retain == 1 && DictGetObjectForKey(back, "k") == o, + "storing the object a key already holds keeps one reference"); + /* the same without retaining first, as a careless caller would */ + ok2.obj = DictGetObjectForKey(back, "k"); + PrefsDictionary(back, NULL, ALPODICT_ReplaceObjForKey, (IPTR)&ok2, TAG_DONE); + retain = 0; + PrefsBaseObject(ok2.obj, NULL, ALPO_GetRetainCount, (IPTR)&retain, TAG_DONE); + check(retain == 1 && DictGetObjectForKey(back, "k") == ok2.obj && DictGetIntegerForKey(back, "k", 0) == 5, + "storing a borrowed object over itself does not free it"); + release(back); + } + FreeVec(mem); + } + + /* a whole-number real stays a real through its text form */ + { + struct ALPOString as; + TEXT text[40]; + double big = 3e9, got = 0; + LONG type = 0; + + o = PrefsNumber(NULL, NULL, ALPONUM_AllocSetDouble, (IPTR)&big, TAG_DONE); + as.string = text; + as.length = sizeof(text); + PrefsBaseObject(o, NULL, ALPO_GetAsString, (IPTR)&as, TAG_DONE); + release(o); + o = num(0); + PrefsBaseObject(o, NULL, ALPO_SetWithString, (IPTR)text, TAG_DONE); + PrefsNumber(o, NULL, ALPONUM_GetType, (IPTR)&type, ALPONUM_GetDouble, (IPTR)&got, TAG_DONE); + check(type == ALPONUMT_Double && got == 3e9, "3e9 as text reads back as a real, not a saturated LONG"); + release(o); + } + release(dict); +} + +static BOOL envarc_writable(void) +{ + BPTR fh = Open("ENVARC:AppTest.writable", MODE_NEWFILE); + + if (fh) + { + Close(fh); + DeleteFile("ENVARC:AppTest.writable"); + } + return fh != BNULL; +} + +static BOOL comment_is(CONST_STRPTR path, CONST_STRPTR comment) +{ + struct FileInfoBlock *fib = AllocDosObject(DOS_FIB, NULL); + BPTR lock = Lock(path, SHARED_LOCK); + BOOL is = fib && lock && Examine(lock, fib) && strcmp(fib->fib_Comment, comment) == 0; + + if (lock) + UnLock(lock); + FreeDosObject(DOS_FIB, fib); + return is; +} + +static void test_appprefs(void) +{ + PrefsObject *p = NULL; + ULONG id; + BOOL marked; + + DeleteFile("ENV:AppTest/PrefsApp.aros.org.xml"); + DeleteFile("ENVARC:AppTest/PrefsApp.aros.org.xml"); + + id = RegisterApplication("PrefsApp", REGAPP_URLIdentifier, (IPTR)"aros.org", + REGAPP_LoadPrefs, TRUE, REGAPP_SavePrefs, TRUE, + REGAPP_ENVDir, (IPTR)"AppTest", TAG_DONE); + GetApplicationAttrs(id, APPATTR_MainPrefsDict, (IPTR)&p, TAG_DONE); + check(id && p && DictGetIntegerForKey(p, "Runs", 0) == 0, "REGAPP_LoadPrefs without a file: empty dictionary"); + DictSetObjectForKey(p, num(1), "Runs"); + SetApplicationAttrs(id, APPATTR_FlushPrefs, FALSE, TAG_DONE); + check(!env_exists("ENV:AppTest/PrefsApp.aros.org.xml"), "APPATTR_FlushPrefs FALSE writes nothing"); + /* FALSE back when ENVARC: cannot be written, ENV: is written all the same */ + check(SetApplicationAttrs(id, APPATTR_FlushPrefs, TRUE, TAG_DONE) == envarc_writable() + && env_exists("ENV:AppTest/PrefsApp.aros.org.xml"), "APPATTR_FlushPrefs writes ENV:[ENVDir/]ident.xml"); + DictSetObjectForKey(p, num(2), "Runs"); + /* the lists in ENV: are current, so unregistering leaves the file alone; + a rewrite would lose the comment */ + marked = SetComment("ENV:Sys/application.xml", "apptest"); + UnregisterApplication(id, NULL); + if (envarc_writable()) + check(env_exists("ENVARC:AppTest/PrefsApp.aros.org.xml"), "REGAPP_SavePrefs writes on unregister, to ENVARC: too"); + else + printf("SKIP REGAPP_SavePrefs to ENVARC: (ENVARC: is not writable)\n"); + if (marked) + { + check(comment_is("ENV:Sys/application.xml", "apptest"), "unregistering does not rewrite the lists in ENV:"); + SetComment("ENV:Sys/application.xml", ""); + } + + id = RegisterApplication("PrefsApp", REGAPP_URLIdentifier, (IPTR)"aros.org", + REGAPP_LoadPrefs, TRUE, REGAPP_ENVDir, (IPTR)"AppTest", TAG_DONE); + p = NULL; + GetApplicationAttrs(id, APPATTR_MainPrefsDict, (IPTR)&p, TAG_DONE); + check(DictGetIntegerForKey(p, "Runs", 0) == 2, "the next run reads them back"); + p = NULL; + check(ReadPrefs(PrefsDictionary(NULL, NULL, ALPO_Alloc, 0, TAG_DONE), READPREFS_AppID, id, TAG_DONE) == 0, + "ReadPrefs by READPREFS_AppID"); + DeleteFile("ENV:AppTest/PrefsApp.aros.org.xml"); + SetApplicationAttrs(id, APPATTR_FlushPrefs, TRUE, TAG_DONE); + check(env_exists("ENV:AppTest/PrefsApp.aros.org.xml"), "APPATTR_FlushPrefs writes without REGAPP_SavePrefs too"); + UnregisterApplication(id, NULL); + + DeleteFile("ENV:AppTest/PrefsApp.aros.org.xml"); + DeleteFile("ENVARC:AppTest/PrefsApp.aros.org.xml"); + DeleteFile("ENV:AppTest"); + DeleteFile("ENVARC:AppTest"); +} + +static void test_applib(ULONG self, struct MsgPort *selfport) +{ + struct ApplicationIconInfo aii = { APPICONT_CustomIcon, NULL }; + PrefsObject *arr = NULL; + struct ALPOObjIndex oi; + struct MsgPort *wport = NULL, *bport = NULL; + ULONG watcher, blanker, other, allowed = 2, gm = 2, max = 0; + LONG count = 0; + BOOL found = FALSE; + + /* PrefsApp registered last, in test_appprefs(); AppTest before it */ + GetAppLibAttrs(APPLIBATTR_LastUsedAppsArray, (IPTR)&arr, TAG_DONE); + PrefsArray(arr, NULL, ALPOARR_GetCount, (IPTR)&count, TAG_DONE); + oi.index = 0; + if (PrefsArray(arr, NULL, ALPOARR_GetObjAtIndex, (IPTR)&oi, TAG_DONE) && oi.obj) + found = strcmp(DictGetStringForKey(oi.obj, LUA_APPNAME_KEY, ""), "PrefsApp") == 0; + for (oi.index = 1; found && oi.index < count; oi.index++) + { + PrefsArray(arr, NULL, ALPOARR_GetObjAtIndex, (IPTR)&oi, TAG_DONE); + if (strcmp(DictGetStringForKey(oi.obj, LUA_APPIDENTIFIER_KEY, ""), "AppTest.aros.org") == 0) + break; + } + check(arr && found && oi.index < count, "last used applications: most recent first, AppTest listed"); + release(arr); + + GetAppLibAttrs(APPLIBATTR_LastUsedDocsMaxCount, (IPTR)&max, TAG_DONE); + SetAppLibAttrs(0, APPLIBATTR_LastUsedDocsMaxCount, 3, TAG_DONE); + SetApplicationAttrs(self, APPATTR_AppOpenedDocument, (IPTR)"RAM:d1", TAG_DONE); + SetApplicationAttrs(self, APPATTR_AppOpenedDocument, (IPTR)"RAM:d2", TAG_DONE); + SetApplicationAttrs(self, APPATTR_AppOpenedDocument, (IPTR)"RAM:d3", TAG_DONE); + SetApplicationAttrs(self, APPATTR_AppOpenedDocument, (IPTR)"RAM:d4", TAG_DONE); + SetApplicationAttrs(self, APPATTR_AppOpenedDocument, (IPTR)"RAM:d2", TAG_DONE); + arr = NULL; + GetApplicationAttrs(self, APPATTR_LastUsedDocsArray, (IPTR)&arr, TAG_DONE); + count = 0; + PrefsArray(arr, NULL, ALPOARR_GetCount, (IPTR)&count, TAG_DONE); + oi.index = 0; + PrefsArray(arr, NULL, ALPOARR_GetObjAtIndex, (IPTR)&oi, TAG_DONE); + check(count == 3 && oi.obj && strcmp(DictGetStringForKey(oi.obj, LUD_DOCPATH_KEY, ""), "RAM:d2") == 0, + "last used documents: most recent first, capped by LastUsedDocsMaxCount"); + release(arr); + SetApplicationAttrs(self, APPATTR_ClearLastUsedDocs, TRUE, TAG_DONE); + SetAppLibAttrs(0, APPLIBATTR_LastUsedDocsMaxCount, max ? max : 10, TAG_DONE); + + watcher = RegisterApplication("Watcher", REGAPP_AppNotifications, TRUE, REGAPP_NoIcon, TRUE, TAG_DONE); + blanker = RegisterApplication("Blanker", REGAPP_BlankerNotifications, TRUE, REGAPP_NoIcon, TRUE, TAG_DONE); + GetApplicationAttrs(watcher, APPATTR_Port, (IPTR)&wport, TAG_DONE); + GetApplicationAttrs(blanker, APPATTR_Port, (IPTR)&bport, TAG_DONE); + drain(wport, 0, NULL, 0); + drain(bport, 0, NULL, 0); + + other = RegisterApplication("Other", TAG_DONE); + check(drainfrom(wport, APPLIBMT_AppRegister, other, NULL, 0) == 1, "AppNotifications: AppRegister"); + + SetApplicationAttrs(other, APPATTR_Hidden, TRUE, TAG_DONE); + check(drainfrom(wport, APPLIBMT_AppDidChangeHidden, other, NULL, 0) == 1, "AppNotifications: AppDidChangeHidden"); + + GetAppLibAttrs(APPLIBATTR_BlankerIsAllowed, (IPTR)&allowed, TAG_DONE); + check(allowed == TRUE, "blanker allowed"); + SetApplicationAttrs(other, APPATTR_AllowsBlanker, FALSE, TAG_DONE); + GetAppLibAttrs(APPLIBATTR_BlankerIsAllowed, (IPTR)&allowed, TAG_DONE); + check(allowed == FALSE && drain(bport, APPLIBMT_BlankerDisallow, NULL, 0) == 1, + "an application vetoes the blanker; blankers told"); + SendApplicationMsg(self, 0, NULL, APPLIBMT_BlankerBlank); + check(drain(bport, APPLIBMT_BlankerBlank, NULL, 0) == 1 && drain(wport, APPLIBMT_BlankerBlank, NULL, 0) == 0 + && drain(selfport, APPLIBMT_BlankerBlank, NULL, 0) == 0, "blanker messages reach only blankers"); + + SetApplicationAttrs(other, APPATTR_NeedsGameMode, TRUE, TAG_DONE); + GetAppLibAttrs(APPLIBATTR_GameModeIsActive, (IPTR)&gm, TAG_DONE); + check(gm == TRUE && Notify(self, APPNOTIFY_Text, (IPTR)"not now", TAG_DONE) == APPNOTIFY_ERROR_GAMEMODEACTIVE, + "game mode: Notify() refuses"); + if (env_exists("SYS:System/Notifications")) + check(Notify(self, APPNOTIFY_Text, (IPTR)"logged during game mode", APPNOTIFY_LogOnly, TRUE, TAG_DONE) + == APPNOTIFY_OK_MSGQUEUED, "game mode: APPNOTIFY_LogOnly still logs"); + drain(selfport, 0, NULL, 0); + + check(LockApplicationIcon(other), "LockApplicationIcon"); + check(!SetApplicationAttrs(other, APPATTR_IconType, (IPTR)&aii, TAG_DONE) + && drainfrom(wport, APPLIBMT_AppWantsChangeIconType, other, NULL, 0) == 1, + "icon type locked: refused, docks asked to unlock"); + check(!UnregisterApplication(other, UNREGAPP_WaitForUnlock, FALSE, TAG_DONE), + "UNREGAPP_WaitForUnlock FALSE fails while locked"); + UnlockApplicationIcon(other); + check(SetApplicationAttrs(other, APPATTR_IconType, (IPTR)&aii, TAG_DONE) + && drainfrom(wport, APPLIBMT_AppDidChangeIconType, other, NULL, 0) == 1, + "unlocked: icon type changes, docks told"); + + UnregisterApplication(other, NULL); + GetAppLibAttrs(APPLIBATTR_BlankerIsAllowed, (IPTR)&allowed, APPLIBATTR_GameModeIsActive, (IPTR)&gm, TAG_DONE); + check(drainfrom(wport, APPLIBMT_AppUnregister, other, NULL, 0) == 1 && allowed && !gm + && drain(bport, APPLIBMT_BlankerAllow, NULL, 0) == 1, + "unregister undoes game mode and the veto, docks told"); + drain(selfport, 0, NULL, 0); + + /* a dock that never unlocks: unregister goes ahead after five seconds, + the icon information it read stays readable until its unlock */ + { + struct ApplicationIconInfo *held = NULL; + struct DateStamp before, after; + ULONG id = RegisterApplication("Held", REGAPP_URLIdentifier, (IPTR)"aros.org", + REGAPP_LoadPrefs, TRUE, REGAPP_SavePrefs, TRUE, + REGAPP_ENVDir, (IPTR)"AppTest", TAG_DONE); + PrefsObject *p = NULL; + LONG ticks; + BOOL unreg; + + DeleteFile("ENV:AppTest/Held.aros.org.xml"); + GetApplicationAttrs(id, APPATTR_MainPrefsDict, (IPTR)&p, TAG_DONE); + if (p) + DictSetObjectForKey(p, num(3), "Runs"); + check(id && LockApplicationIcon(id) + && GetApplicationAttrs(id, APPATTR_IconType, (IPTR)&held, TAG_DONE) && held + && held->iconType == APPICONT_ProgramIcon, "icon locked and read by a dock"); + DateStamp(&before); + unreg = UnregisterApplication(id, NULL); + DateStamp(&after); + ticks = (after.ds_Days - before.ds_Days) * 24 * 60 * TICKS_PER_SECOND * 60 + + (after.ds_Minute - before.ds_Minute) * 60 * TICKS_PER_SECOND + + (after.ds_Tick - before.ds_Tick); + check(unreg && ticks >= 4 * TICKS_PER_SECOND && ticks < 8 * TICKS_PER_SECOND + && FindApplication(FINDAPP_Name, (IPTR)"Held", TAG_DONE) == 0, + "unregister with the icon still locked goes ahead after five seconds"); + check(held && held->iconType == APPICONT_ProgramIcon, "the icon information is still readable"); + check(env_exists("ENV:AppTest/Held.aros.org.xml"), "its prefs were saved all the same"); + UnlockApplicationIcon(id); + drainfrom(wport, APPLIBMT_AppUnregister, id, NULL, 0); + DeleteFile("ENV:AppTest/Held.aros.org.xml"); + DeleteFile("ENVARC:AppTest/Held.aros.org.xml"); + DeleteFile("ENV:AppTest"); + DeleteFile("ENVARC:AppTest"); + } + + UnregisterApplication(blanker, NULL); + UnregisterApplication(watcher, NULL); +} + +static void test_rexx(void) +{ + LONG rc2 = -1; + + RexxSysBase = (struct RxsLib *)OpenLibrary("rexxsyslib.library", 0); + check(rexx("RINGHIO", "REGISTERAPP APP=RexxTest", NULL) == 0, "ARexx REGISTERAPP at the RINGHIO port"); + check(FindApplication(FINDAPP_Name, (IPTR)"RexxTest", TAG_DONE) != 0, "ARexx registration is a real application"); + check(rexx("RINGHIO", "RINGHIO APP=RexxTest CLOSEONDC SCREEN=\"FRONT\" TITLE=\"ARexx\" IMGVALIGN=1 " + "TEXT=Hello from the RINGHIO port, as Hollywood sends it", NULL) == 0, + "ARexx RINGHIO pop-up"); + check(rexx("RINGHIO", "NOTIFY APP=RexxTest PRI=2 TITLE=\"ARexx NOTIFY\" TEXT=The OS4 NOTIFY command", NULL) == 0, + "ARexx NOTIFY"); + check(rexx("RINGHIO", "NOTIFY APP=NobodyHere TEXT=x", &rc2) == RC_ERROR && rc2 == APPNOTIFY_ERROR_APPNOTREGISTERED, + "ARexx NOTIFY for an unregistered app: RC2 = APPNOTIFY_ERROR_APPNOTREGISTERED"); + check(rexx("RINGHIO", "UNREGISTERAPP APP=RexxTest", NULL) == 0 + && FindApplication(FINDAPP_Name, (IPTR)"RexxTest", TAG_DONE) == 0, "ARexx UNREGISTERAPP"); + check(rexx("RINGHIO", "NOSUCHCOMMAND", NULL) != 0, "ARexx unknown command fails"); + if (RexxSysBase) + CloseLibrary((struct Library *)RexxSysBase); +} + +static void test_blocked(void) +{ + PrefsObject *saved, *dict, *apps; + ULONG id; + BOOL had; + + /* Block one application in the server's settings, as Prefs/Notifications would. */ + saved = PrefsDictionary(NULL, NULL, ALPO_Alloc, 0, TAG_DONE); + had = ReadPrefs(saved, READPREFS_FileName, (IPTR)"Sys/Notifications.xml", READPREFS_ReadENVARC, FALSE, + TAG_DONE) == 0; + dict = PrefsDictionary(saved, NULL, ALPO_Copy, 0, TAG_DONE); + apps = PrefsDictionary(NULL, NULL, ALPO_Alloc, 0, TAG_DONE); + DictSetObjectForKey(apps, PrefsNumber(NULL, NULL, ALPONUM_AllocSetBool, FALSE, TAG_DONE), "Blocked"); + DictSetObjectForKey(dict, apps, "Applications"); + WritePrefs(dict, WRITEPREFS_FileName, (IPTR)"Sys/Notifications.xml", TAG_DONE); + release(dict); + Delay(25); + + id = RegisterApplication("Blocked", TAG_DONE); + check(Notify(id, APPNOTIFY_Text, (IPTR)"should not show", TAG_DONE) == APPNOTIFY_ERROR_APPNOTALLOWED, + "an application switched off in the preferences: APPNOTIFY_ERROR_APPNOTALLOWED"); + UnregisterApplication(id, NULL); + + if (had) + WritePrefs(saved, WRITEPREFS_FileName, (IPTR)"Sys/Notifications.xml", TAG_DONE); + else + DeleteFile("ENV:Sys/Notifications.xml"); + release(saved); + Delay(25); +} + +static BOOL log_has(CONST_STRPTR what) +{ + char line[512]; + BOOL found = FALSE; + BPTR fh = Open("T:Notifications.log", MODE_OLDFILE); + + if (!fh) + return FALSE; + while (!found && FGets(fh, line, sizeof(line))) + found = strstr(line, what) != NULL; + Close(fh); + return found; +} + +int main(void) +{ + struct RDArgs *rda; + IPTR args[2] = { 0, 0 }; + LONG waitback = 0; + STRPTR image = NULL; + BOOL server; + ULONG id1, id2, id3, id4, counter = 9; + struct MsgPort *p1 = NULL, *p2 = NULL, *p3 = NULL; + STRPTR ident = NULL; + struct ApplicationCustomMsg cm; + struct MinList *list; + struct ApplicationNode *an; + char text[64]; + int count; + + if (!(rda = ReadArgs("WAITBACK/K/N,IMAGE/K", args, NULL))) + { + PrintFault(IoErr(), "AppTest"); + return RETURN_FAIL; + } + if (args[0]) + waitback = *(LONG *)args[0]; + image = (STRPTR)args[1]; + + if (!(ApplicationBase = OpenLibrary("application.library", 1))) + { + printf("FAIL cannot open application.library\n"); + return RETURN_FAIL; + } + + id1 = RegisterApplication("AppTest", REGAPP_URLIdentifier, (IPTR)"www.aros.org", + REGAPP_Description, (IPTR)"application.library test", TAG_DONE); + check(id1 != 0, "register first instance"); + GetApplicationAttrs(id1, APPATTR_AppIdentifier, (IPTR)&ident, APPATTR_Port, (IPTR)&p1, TAG_DONE); + check(ident && strcmp(ident, "AppTest.aros.org") == 0, "identifier name.url, www. stripped"); + check(p1 != NULL, "first instance has a port"); + + id2 = RegisterApplication("AppTest", REGAPP_URLIdentifier, (IPTR)"aros.org", TAG_DONE); + ident = NULL; + GetApplicationAttrs(id2, APPATTR_AppIdentifier, (IPTR)&ident, APPATTR_Port, (IPTR)&p2, + APPATTR_AppIdentifierCounter, (IPTR)&counter, TAG_DONE); + check(id2 && id2 != id1 && ident && strcmp(ident, "AppTest_1.aros.org") == 0 && counter == 1, + "second instance is AppTest_1.aros.org"); + check(!RegisterApplication("AppTest", REGAPP_URLIdentifier, (IPTR)"aros.org", + REGAPP_IdenticalNameCounter, FALSE, TAG_DONE), + "REGAPP_IdenticalNameCounter FALSE: no third instance"); + + check(FindApplication(FINDAPP_Name, (IPTR)"apptest", TAG_DONE) == id1, "find by name (first instance)"); + check(FindApplication(FINDAPP_AppIdentifier, (IPTR)"AppTest_1.aros.org", TAG_DONE) == id2, + "find by identifier"); + check(FindApplication(FINDAPP_Name, (IPTR)"NoSuchApp", TAG_DONE) == 0, "find unknown is 0"); + + memset(&cm, 0, sizeof(cm)); + strcpy(text, "hello"); + cm.customMsg = text; + check(SendApplicationMsg(id1, id2, &cm.almsg, APPLIBMT_CustomMsg), "send custom message"); + strcpy(text, "clobbered"); /* the library must have copied it */ + text[0] = '\0'; + count = drain(p2, APPLIBMT_CustomMsg, text, sizeof(text)); + check(count == 1 && strcmp(text, "hello") == 0, "custom message arrives, string copied"); + + check(SendApplicationMsg(id1, 0, NULL, APPLIBMT_ToFront), "broadcast"); + check(drain(p2, APPLIBMT_ToFront, NULL, 0) == 1 && drain(p1, APPLIBMT_ToFront, NULL, 0) == 0, + "broadcast skips the sender"); + + id3 = RegisterApplication("UniqApp", REGAPP_UniqueApplication, TRUE, TAG_DONE); + GetApplicationAttrs(id3, APPATTR_Port, (IPTR)&p3, TAG_DONE); + id4 = RegisterApplication("UniqApp", TAG_DONE); + check(id3 && !id4, "second instance of a unique application refused"); + check(p3 && drain(p3, APPLIBMT_Unique, NULL, 0) == 1, "running instance told APPLIBMT_Unique"); + + SetApplicationAttrs(id1, APPATTR_NeedsGameMode, TRUE, TAG_DONE); + check(drain(p2, APPLIBMT_GameModeEntered, NULL, 0) == 1 && drain(p3, APPLIBMT_GameModeEntered, NULL, 0) == 1, + "game mode broadcast to the others"); + SetApplicationAttrs(id1, APPATTR_NeedsGameMode, FALSE, TAG_DONE); + drain(p2, APPLIBMT_GameModeLeft, NULL, 0); + drain(p3, APPLIBMT_GameModeLeft, NULL, 0); + + list = GetApplicationList(); + count = 0; + if (list) + ForeachNode(list, an) + { + if ((strcmp(an->identifier, "AppTest.aros.org") == 0 && strcmp(an->name, "AppTest") == 0 && an->appID == id1) + || strcmp(an->identifier, "AppTest_1.aros.org") == 0 || strcmp(an->name, "UniqApp") == 0) + count++; + } + FreeApplicationList(list); + check(count == 3, "application list has all three"); + + test_prefsobjects(); + test_appprefs(); + test_applib(id1, p1); + + server = env_exists("SYS:System/Notifications"); + if (server) + check(Notify(id1, + APPNOTIFY_Title, (IPTR)"application.library", + APPNOTIFY_Text, (IPTR)"Notifications work. Double-click to send a message back.", + APPNOTIFY_BackMsg, (IPTR)"back-from-popup", + APPNOTIFY_CloseOnDC, TRUE, + APPNOTIFY_ConfirmClose, TRUE, + APPNOTIFY_Pri, waitback > 0 ? 10 : 0, + TAG_DONE) == APPNOTIFY_OK_MSGQUEUED, "Notify() shows a pop-up (server started on demand)"); + check(Notify(id1, APPNOTIFY_Pri, 3, TAG_DONE) == APPNOTIFY_ERROR_TAGS, "Notify() without title or text: _TAGS"); + check(Notify(0x7fffffff, APPNOTIFY_Text, (IPTR)"x", TAG_DONE) == APPNOTIFY_ERROR_APPNOTREGISTERED, + "Notify() for no application: _APPNOTREGISTERED"); + if (!server) + { + struct DateStamp before, after; + ULONG rc; + LONG ticks; + + DateStamp(&before); + rc = Notify(id1, APPNOTIFY_Text, (IPTR)"nobody is listening", TAG_DONE); + DateStamp(&after); + ticks = (after.ds_Days - before.ds_Days) * 24 * 60 * TICKS_PER_SECOND * 60 + + (after.ds_Minute - before.ds_Minute) * 60 * TICKS_PER_SECOND + + (after.ds_Tick - before.ds_Tick); + check(rc == APPNOTIFY_ERROR_SERVERNOTRUNNING && ticks < TICKS_PER_SECOND, + "Notify() without SYS:System/Notifications: _SERVERNOTRUNNING within a second"); + } + if (server) + check(log_has("Notifications work.") && log_has("logged during game mode"), + "every notification goes to T:Notifications.log"); + + if (server && waitback > 0) + { + ULONG left = waitback * 50; + BOOL got = FALSE, closed = FALSE; + + text[0] = '\0'; + while (left-- && !(got && closed)) + { + struct ApplicationMsg *am; + + Delay(1); + while ((am = (struct ApplicationMsg *)GetMsg(p1))) + { + if (am->type == APPLIBMT_CustomMsg) + { + strncpy(text, ((struct ApplicationCustomMsg *)am)->customMsg, sizeof(text) - 1); + got = TRUE; + } + else if (am->type == APPLIBMT_ConfirmClose) + closed = TRUE; + ReplyMsg(&am->msg); + } + } + check(got && strcmp(text, "back-from-popup") == 0, "double-click sent APPNOTIFY_BackMsg back"); + check(closed, "closing it sent APPLIBMT_ConfirmClose"); + } + + if (server) + { + test_rexx(); + test_blocked(); + } + else + printf("SKIP pop-ups, ARexx and the settings (SYS:System/Notifications is not installed)\n"); + + check(UnregisterApplication(id3, NULL) && UnregisterApplication(id2, NULL) + && UnregisterApplication(id1, NULL), "unregister"); + check(FindApplication(FINDAPP_Name, (IPTR)"AppTest", TAG_DONE) == 0, "gone after unregister"); + check(!UnregisterApplication(id1, NULL), "unregister twice is refused"); + + if (server && image) + { + ULONG id5 = RegisterApplication("AppTestImage", TAG_DONE); + + check(Notify(id5, APPNOTIFY_Title, (IPTR)"With an image", + APPNOTIFY_Text, (IPTR)"The picture is scaled to 32x32 and drawn with its alpha.", + APPNOTIFY_ImageFile, (IPTR)image, APPNOTIFY_ImageVertAlign, 1, TAG_DONE) + == APPNOTIFY_OK_MSGQUEUED, "pop-up with an image"); + check(Notify(id5, APPNOTIFY_Title, (IPTR)"From an icon", + APPNOTIFY_Text, (IPTR)"APPNOTIFY_IconFile takes a Workbench icon.", + APPNOTIFY_IconFile, (IPTR)"SYS:Prefs/Asl.info", TAG_DONE) + == APPNOTIFY_OK_MSGQUEUED, "pop-up with an icon"); + check(Notify(id5, APPNOTIFY_Title, (IPTR)"Urgent", + APPNOTIFY_Text, (IPTR)"Priority 10 stays until you close it.", + APPNOTIFY_Pri, 10, TAG_DONE) + == APPNOTIFY_OK_MSGQUEUED, "priority 10 pop-up"); + UnregisterApplication(id5, NULL); + } + + printf("APPTEST %s %d passed, %d failed\n", failed ? "FAILED" : "OK", passed, failed); + + CloseLibrary(ApplicationBase); + FreeArgs(rda); + return failed ? RETURN_ERROR : RETURN_OK; +} diff --git a/developer/debug/test/application/mmakefile.src b/developer/debug/test/application/mmakefile.src new file mode 100644 index 00000000000..decd18afedf --- /dev/null +++ b/developer/debug/test/application/mmakefile.src @@ -0,0 +1,18 @@ +# Copyright (C) 2026, The AROS Development Team. All rights reserved. + +include $(SRCDIR)/config/aros.cfg + +EXEDIR := $(AROS_TESTS)/application + +FILES := \ + apptest + +#MM- test : test-application +#MM- test-quick : test-application-quick + +#MM test-application : includes linklibs workbench-libs-application + +%build_progs mmake=test-application \ + files=$(FILES) targetdir=$(EXEDIR) + +%common diff --git a/workbench/libs/application/application.conf b/workbench/libs/application/application.conf new file mode 100644 index 00000000000..adb24e89704 --- /dev/null +++ b/workbench/libs/application/application.conf @@ -0,0 +1,46 @@ +##begin config +version 1.0 +basename Application +libbasetype struct ApplicationBase +residentpri 5 +##end config + +##begin cdef +#include +##end cdef + +##begin cdefprivate +#include "application_intern.h" +##end cdefprivate + +##begin functionlist +ULONG RegisterApplicationA(CONST_STRPTR name, struct TagItem *tags) (A0,A1) +BOOL UnregisterApplicationA(ULONG appID, struct TagItem *tags) (D0,A0) +BOOL SetApplicationAttrsA(ULONG appID, struct TagItem *tags) (D0,A0) +BOOL GetApplicationAttrsA(ULONG appID, struct TagItem *tags) (D0,A0) +ULONG FindApplicationA(struct TagItem *tags) (A0) +BOOL SendApplicationMsg(ULONG senderAppID, ULONG receiverAppID, struct ApplicationMsg *msg, ULONG type) (D0,D1,A0,D2) +ULONG NotifyA(ULONG appID, struct TagItem *tags) (D0,A0) +struct MinList *GetApplicationList() () +VOID FreeApplicationList(struct MinList *list) (A0) +BOOL GetAppLibAttrsA(struct TagItem *tags) (A0) +BOOL SetAppLibAttrsA(ULONG dummy, struct TagItem *tags) (D0,A0) +BOOL LockApplicationIcon(ULONG appID) (D0) +VOID UnlockApplicationIcon(ULONG appID) (D0) +PrefsObject *PrefsBaseObjectA(PrefsObject *obj, ULONG *error, struct TagItem *tags) (A0,A1,A2) +PrefsObject *PrefsStringA(PrefsObject *obj, ULONG *error, struct TagItem *tags) (A0,A1,A2) +PrefsObject *PrefsNumberA(PrefsObject *obj, ULONG *error, struct TagItem *tags) (A0,A1,A2) +PrefsObject *PrefsDateA(PrefsObject *obj, ULONG *error, struct TagItem *tags) (A0,A1,A2) +PrefsObject *PrefsBinaryA(PrefsObject *obj, ULONG *error, struct TagItem *tags) (A0,A1,A2) +PrefsObject *PrefsArrayA(PrefsObject *obj, ULONG *error, struct TagItem *tags) (A0,A1,A2) +PrefsObject *PrefsDictionaryA(PrefsObject *obj, ULONG *error, struct TagItem *tags) (A0,A1,A2) +PrefsObject *DictGetObjectForKey(PrefsObject *dict, CONST_STRPTR key) (A0,A1) +BOOL DictGetBoolForKey(PrefsObject *dict, CONST_STRPTR key, BOOL defBool) (A0,A1,D0) +LONG DictGetIntegerForKey(PrefsObject *dict, CONST_STRPTR key, LONG defInt) (A0,A1,D0) +CONST_STRPTR DictGetStringForKey(PrefsObject *dict, CONST_STRPTR key, CONST_STRPTR defStr) (A0,A1,A2) +LONG DictGetOptionForKey(PrefsObject *dict, CONST_STRPTR key, CONST_STRPTR *optionsTable, LONG defaultOption) (A0,A1,A2,D0) +BOOL DictSetObjectForKey(PrefsObject *dict, PrefsObject *obj, CONST_STRPTR key) (A0,A1,A2) +ULONG ReadPrefsA(PrefsObject *dict, struct TagItem *tags) (A0,A1) +ULONG WritePrefsA(PrefsObject *dict, struct TagItem *tags) (A0,A1) +ULONG BeginDeserialization(struct ALPOGetProcInfo *info) (A0) +##end functionlist diff --git a/workbench/libs/application/application_applib.c b/workbench/libs/application/application_applib.c new file mode 100644 index 00000000000..9094f024381 --- /dev/null +++ b/workbench/libs/application/application_applib.c @@ -0,0 +1,569 @@ +/* + Copyright (C) 2026, The AROS Development Team. All rights reserved. + + Desc: Library-wide state: the last used applications and documents, + the blanker and game mode, and application icon locks. +*/ + +#include +#include +#include +#include +#include + +#include + +#include "application_intern.h" +#include LC_LIBDEFS_FILE + +#define LU_APPSMAX_KEY "LastUsedAppsMaxCount" +#define LU_DOCSMAX_KEY "LastUsedDocsMaxCount" +#define LU_DEFAULTMAX 10 + +/* Callers hold ab_Lock; the file was normally read by app_PreloadGlobal() + already, so this only makes the empty dictionary. */ +void app_LoadGlobal(struct ApplicationBase *AppBase) +{ + if (AppBase->ab_GlobalLoaded || !app_IsProcess()) + return; + if (!AppBase->ab_Global && !(AppBase->ab_Global = po_NewDict())) + return; + prefs_Read(AppBase, AppBase->ab_Global, APPLIB_GLOBALPREFS, TRUE, TRUE); + AppBase->ab_GlobalLoaded = TRUE; +} + +void app_PreloadGlobal(struct ApplicationBase *AppBase) +{ + PrefsObject *tmp; + + if (AppBase->ab_GlobalLoaded || !app_IsProcess() || !(tmp = po_NewDict())) + return; + prefs_Read(AppBase, tmp, APPLIB_GLOBALPREFS, TRUE, TRUE); + + ObtainSemaphore(&AppBase->ab_Lock); + if (!AppBase->ab_GlobalLoaded) + { + if (AppBase->ab_Global) + po_MoveContents(AppBase->ab_Global, tmp); + else + { + AppBase->ab_Global = tmp; + tmp = NULL; + } + AppBase->ab_GlobalLoaded = TRUE; + } + ReleaseSemaphore(&AppBase->ab_Lock); + po_Release(tmp); +} + +static LONG app_Minute(void) +{ + struct DateStamp ds; + + DateStamp(&ds); + return ds.ds_Days * 1440 + ds.ds_Minute; +} + +/* One save at a time, so the last writer writes the newest state. */ +void app_SaveGlobal(struct ApplicationBase *AppBase, BOOL env, BOOL envarc) +{ + PrefsObject *copy; + + if (!app_IsProcess()) + return; + ObtainSemaphore(&AppBase->ab_SaveLock); + ObtainSemaphore(&AppBase->ab_Lock); + copy = AppBase->ab_GlobalLoaded ? po_Copy(AppBase->ab_Global) : NULL; + if (copy) + { + AppBase->ab_GlobalStale = !envarc; + if (envarc) + AppBase->ab_ArcMinute = app_Minute(); + } + ReleaseSemaphore(&AppBase->ab_Lock); + if (copy) + { + prefs_Write(AppBase, copy, APPLIB_GLOBALPREFS, env, envarc); + po_Release(copy); + } + ReleaseSemaphore(&AppBase->ab_SaveLock); +} + +/* ENVARC: is written when the last application has gone or at most once a + minute. */ +BOOL app_ArcDue(struct ApplicationBase *AppBase, BOOL last) +{ + return last || AppBase->ab_ArcMinute < 0 || app_Minute() - AppBase->ab_ArcMinute >= 1; +} + +/* The last used lists changed: ENV: now, ENVARC: when due. */ +void app_SaveLists(struct ApplicationBase *AppBase) +{ + BOOL envarc; + + if (!app_IsProcess()) + return; + ObtainSemaphore(&AppBase->ab_Lock); + envarc = app_ArcDue(AppBase, FALSE); + ReleaseSemaphore(&AppBase->ab_Lock); + app_SaveGlobal(AppBase, TRUE, envarc); +} + +static LONG lu_Max(struct ApplicationBase *AppBase, CONST_STRPTR key) +{ + LONG max = po_DictLong(AppBase->ab_Global, key, LU_DEFAULTMAX); + + if (max < 0) + max = 0; + if (max > APPLIB_LASTUSED_MAX) + max = APPLIB_LASTUSED_MAX; + return max; +} + +/* The array for key in the global dictionary, made if missing. */ +static PrefsObject *lu_Array(struct ApplicationBase *AppBase, CONST_STRPTR key) +{ + PrefsObject *arr; + + if (!AppBase->ab_Global) + return NULL; + if (po_Type(arr = po_DictGet(AppBase->ab_Global, key)) == ALPOT_Array) + return arr; + if ((arr = po_NewArray()) && !po_DictSet(AppBase->ab_Global, key, arr)) + { + po_Release(arr); + arr = NULL; + } + return arr; +} + +static void lu_Trim(PrefsObject *arr, LONG max) +{ + while (po_Count(arr) > max) + po_ArrayRemove(arr, po_Count(arr) - 1); +} + +/* Put entry first, after removing entries whose matchkey has the same value. */ +static void lu_Push(PrefsObject *arr, PrefsObject *entry, CONST_STRPTR matchkey, CONST_STRPTR matchval, + LONG max) +{ + LONG i; + + for (i = po_Count(arr) - 1; i >= 0; i--) + { + CONST_STRPTR v = po_DictString(po_At(arr, i), matchkey, NULL); + + if (v && Stricmp(v, matchval) == 0) + po_ArrayRemove(arr, i); + } + if (!po_ArrayInsert(arr, entry, 0)) + po_Release(entry); + lu_Trim(arr, max); +} + +static BOOL lu_SetString(PrefsObject *dict, CONST_STRPTR key, CONST_STRPTR value) +{ + PrefsObject *s; + + if (!value) + return TRUE; + if (!(s = po_NewString(value))) + return FALSE; + if (!po_DictSet(dict, key, s)) + { + po_Release(s); + return FALSE; + } + return TRUE; +} + +static PrefsObject *lu_Entry(CONST_STRPTR k1, CONST_STRPTR v1, CONST_STRPTR k2, CONST_STRPTR v2, + CONST_STRPTR k3, CONST_STRPTR v3) +{ + PrefsObject *d, *date; + + if (!(d = po_NewDict())) + return NULL; + if ((date = po_NewNow()) && !po_DictSet(d, LUA_OPENDATE_KEY, date)) + po_Release(date); + if (!lu_SetString(d, k1, v1) || !lu_SetString(d, k2, v2) || !lu_SetString(d, k3, v3)) + { + po_Release(d); + return NULL; + } + return d; +} + +/* TRUE when the list changed and the caller has to save it. */ +BOOL app_NoteAppStarted(struct ApplicationBase *AppBase, struct AppEntry *ae) +{ + PrefsObject *arr, *entry; + + app_LoadGlobal(AppBase); + if (!(arr = lu_Array(AppBase, LUA_BASE_KEY))) + return FALSE; + if (!(entry = lu_Entry(LUA_APPIDENTIFIER_KEY, ae->ae_BaseIdent, LUA_APPNAME_KEY, ae->ae_Name, + LUA_APPPATH_KEY, ae->ae_FileName))) + return FALSE; + lu_Push(arr, entry, LUA_APPIDENTIFIER_KEY, ae->ae_BaseIdent, lu_Max(AppBase, LU_APPSMAX_KEY)); + app_BroadcastTo(AppBase, APPF_APPNOTIFY, 0, APPLIBMT_LastAppDocChange); + return TRUE; +} + +BOOL app_NoteDocOpened(struct ApplicationBase *AppBase, struct AppEntry *ae, CONST_STRPTR path) +{ + PrefsObject *arr, *entry; + + if (!path || !*path) + return FALSE; + app_LoadGlobal(AppBase); + if (!(arr = lu_Array(AppBase, LUD_BASE_KEY))) + return FALSE; + if (!(entry = lu_Entry(LUD_DOCPATH_KEY, path, LUD_APPIDENTIFIER_KEY, ae->ae_BaseIdent, + LUD_APPPATH_KEY, ae->ae_FileName))) + return FALSE; + lu_Push(arr, entry, LUD_DOCPATH_KEY, path, lu_Max(AppBase, LU_DOCSMAX_KEY)); + app_BroadcastTo(AppBase, APPF_APPNOTIFY, 0, APPLIBMT_LastAppDocChange); + return TRUE; +} + +/* A copy of the global document entries of one application. */ +PrefsObject *app_DocsOfApp(struct ApplicationBase *AppBase, struct AppEntry *ae) +{ + PrefsObject *all, *mine; + LONG i; + + app_LoadGlobal(AppBase); + if (!(mine = po_NewArray())) + return NULL; + all = po_DictGet(AppBase->ab_Global, LUD_BASE_KEY); + for (i = 0; i < po_Count(all); i++) + { + PrefsObject *e = po_At(all, i), *c; + CONST_STRPTR id = po_DictString(e, LUD_APPIDENTIFIER_KEY, NULL); + + if (id && Stricmp(id, ae->ae_BaseIdent) == 0 && (c = po_Copy(e))) + { + if (!po_ArrayInsert(mine, c, -1)) + po_Release(c); + } + } + return mine; +} + +BOOL app_ClearDocsOfApp(struct ApplicationBase *AppBase, struct AppEntry *ae) +{ + PrefsObject *all; + BOOL changed = FALSE; + LONG i; + + app_LoadGlobal(AppBase); + all = po_DictGet(AppBase->ab_Global, LUD_BASE_KEY); + for (i = po_Count(all) - 1; i >= 0; i--) + { + CONST_STRPTR id = po_DictString(po_At(all, i), LUD_APPIDENTIFIER_KEY, NULL); + + if (id && Stricmp(id, ae->ae_BaseIdent) == 0) + { + po_ArrayRemove(all, i); + changed = TRUE; + } + } + if (changed) + app_BroadcastTo(AppBase, APPF_APPNOTIFY, 0, APPLIBMT_LastAppDocChange); + return changed; +} + +BOOL app_GameModeActive(struct ApplicationBase *AppBase) +{ + struct AppEntry *ae; + + ForeachNode(&AppBase->ab_Apps, ae) + { + if (ae->ae_Flags & APPF_GAMEMODE) + return TRUE; + } + return FALSE; +} + +static BOOL app_BlankerAllowed(struct ApplicationBase *AppBase) +{ + struct AppEntry *ae; + + ForeachNode(&AppBase->ab_Apps, ae) + { + if ((ae->ae_Flags & APPF_GAMEMODE) || !(ae->ae_Flags & APPF_ALLOWBLANKER)) + return FALSE; + } + return TRUE; +} + +/* Tell the blankers when the answer to "may the screen blank?" changes. */ +void app_UpdateBlanker(struct ApplicationBase *AppBase) +{ + BOOL allowed = app_BlankerAllowed(AppBase); + + if (allowed != AppBase->ab_BlankerAllowed) + { + AppBase->ab_BlankerAllowed = allowed; + app_BroadcastTo(AppBase, APPF_BLANKNOTIFY, 0, + allowed ? APPLIBMT_BlankerAllow : APPLIBMT_BlankerDisallow); + } +} + +/***************************************************************************** + + NAME */ +#include + + AROS_LH1(BOOL, GetAppLibAttrsA, + +/* SYNOPSIS */ + AROS_LHA(struct TagItem *, tags, A0), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 14, Application) + +/* FUNCTION + Read library-wide attributes, APPLIBATTR_ tags marked (G). + The arrays are copies: ALPO_Release them when done. Their entries + are dictionaries with the LUA_ or LUD_ keys, most recent first. + + RESULT + FALSE if a tag could not be read. + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + struct TagItem *tstate = tags, *tag; + BOOL ok = TRUE; + + app_PreloadGlobal(AppBase); + ObtainSemaphore(&AppBase->ab_Lock); + app_Reap(AppBase); + app_LoadGlobal(AppBase); + + while ((tag = NextTagItem(&tstate))) + { + if (!tag->ti_Data) + continue; + switch (tag->ti_Tag) + { + case APPLIBATTR_LastUsedAppsArray: + case APPLIBATTR_LastUsedDocsArray: + { + PrefsObject *arr = lu_Array(AppBase, tag->ti_Tag == APPLIBATTR_LastUsedAppsArray + ? LUA_BASE_KEY : LUD_BASE_KEY); + + if (!(*(PrefsObject **)tag->ti_Data = arr ? po_Copy(arr) : po_NewArray())) + ok = FALSE; + } + break; + case APPLIBATTR_LastUsedAppsMaxCount: + *(ULONG *)tag->ti_Data = lu_Max(AppBase, LU_APPSMAX_KEY); + break; + case APPLIBATTR_LastUsedDocsMaxCount: + *(ULONG *)tag->ti_Data = lu_Max(AppBase, LU_DOCSMAX_KEY); + break; + case APPLIBATTR_BlankerIsAllowed: + *(ULONG *)tag->ti_Data = app_BlankerAllowed(AppBase); + break; + case APPLIBATTR_GameModeIsActive: + *(ULONG *)tag->ti_Data = app_GameModeActive(AppBase); + break; + default: + ok = FALSE; + break; + } + } + ReleaseSemaphore(&AppBase->ab_Lock); + + return ok; + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH2(BOOL, SetAppLibAttrsA, + +/* SYNOPSIS */ + AROS_LHA(ULONG, dummy, D0), + AROS_LHA(struct TagItem *, tags, A0), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 15, Application) + +/* FUNCTION + Change library-wide attributes, APPLIBATTR_ tags marked (S). The + settings are saved at once to ENV:Sys/application.xml and + ENVARC:Sys/application.xml. + + INPUTS + dummy - 0. + + RESULT + FALSE if a tag could not be set. + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + struct TagItem *tstate = tags, *tag; + BOOL ok = TRUE, changed = FALSE; + + (void)dummy; + app_PreloadGlobal(AppBase); + ObtainSemaphore(&AppBase->ab_Lock); + app_Reap(AppBase); + app_LoadGlobal(AppBase); + + while ((tag = NextTagItem(&tstate))) + { + switch (tag->ti_Tag) + { + case APPLIBATTR_LastUsedAppsMaxCount: + case APPLIBATTR_LastUsedDocsMaxCount: + { + BOOL apps = (tag->ti_Tag == APPLIBATTR_LastUsedAppsMaxCount); + PrefsObject *n; + + if (tag->ti_Data > APPLIB_LASTUSED_MAX || !AppBase->ab_Global + || !(n = po_NewLong(tag->ti_Data))) + { + ok = FALSE; + break; + } + if (!po_DictSet(AppBase->ab_Global, apps ? LU_APPSMAX_KEY : LU_DOCSMAX_KEY, n)) + { + po_Release(n); + ok = FALSE; + break; + } + lu_Trim(lu_Array(AppBase, apps ? LUA_BASE_KEY : LUD_BASE_KEY), tag->ti_Data); + changed = TRUE; + } + break; + case APPLIBATTR_ClearLastUsedApps: + case APPLIBATTR_ClearLastUsedDocs: + if (tag->ti_Data) + { + po_Clear(lu_Array(AppBase, tag->ti_Tag == APPLIBATTR_ClearLastUsedApps + ? LUA_BASE_KEY : LUD_BASE_KEY)); + changed = TRUE; + } + break; + default: + ok = FALSE; + break; + } + } + + if (changed) + app_BroadcastTo(AppBase, APPF_APPNOTIFY, 0, APPLIBMT_LastAppDocChange); + ReleaseSemaphore(&AppBase->ab_Lock); + + if (changed) + app_SaveGlobal(AppBase, TRUE, TRUE); + + return ok; + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH1(BOOL, LockApplicationIcon, + +/* SYNOPSIS */ + AROS_LHA(ULONG, appID, D0), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 16, Application) + +/* FUNCTION + Lock an application's icon so that APPATTR_IconType can be read and + the icon or docky used. While it is locked the application cannot + change its icon type, and UnregisterApplication() waits for up to + five seconds; after that the application is gone but its icon + information stays valid until the last unlock. Locks nest. + + A dock must unlock the icon as soon as it is sent + APPLIBMT_AppWantsChangeIconType or APPLIBMT_AppUnregister. + + RESULT + FALSE if there is no such application or it is unregistering. + + SEE ALSO + UnlockApplicationIcon() + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + struct AppEntry *ae; + BOOL ok = FALSE; + + ObtainSemaphore(&AppBase->ab_Lock); + if ((ae = app_Find(AppBase, appID)) && !(ae->ae_Flags & APPF_LEAVING)) + { + ae->ae_IconLocks++; + ok = TRUE; + } + ReleaseSemaphore(&AppBase->ab_Lock); + + return ok; + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH1(VOID, UnlockApplicationIcon, + +/* SYNOPSIS */ + AROS_LHA(ULONG, appID, D0), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 17, Application) + +/* FUNCTION + Undo one LockApplicationIcon(). + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + struct AppEntry *ae, *gone = NULL; + + ObtainSemaphore(&AppBase->ab_Lock); + if ((ae = app_Find(AppBase, appID))) + { + if (ae->ae_IconLocks) + ae->ae_IconLocks--; + } + else + { + ForeachNode(&AppBase->ab_Gone, ae) + { + if (ae->ae_ID == appID) + { + if (ae->ae_IconLocks && !--ae->ae_IconLocks) + { + REMOVE(ae); + gone = ae; + } + break; + } + } + } + ReleaseSemaphore(&AppBase->ab_Lock); + if (gone) + app_FreeEntry(gone); + + AROS_LIBFUNC_EXIT +} diff --git a/workbench/libs/application/application_apps.c b/workbench/libs/application/application_apps.c new file mode 100644 index 00000000000..eac81d1fbcb --- /dev/null +++ b/workbench/libs/application/application_apps.c @@ -0,0 +1,920 @@ +/* + Copyright (C) 2026, The AROS Development Team. All rights reserved. + + Desc: Registering applications and messaging between them. +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "application_intern.h" +#include "application_po.h" +#include LC_LIBDEFS_FILE + +/* Delay(2) steps while a dock still holds the icon lock: five seconds */ +#define APP_UNLOCK_TRIES 125 + +static void app_CopyStr(STRPTR dst, CONST_STRPTR src, ULONG size) +{ + if (!src) + src = ""; + strncpy(dst, src, size - 1); + dst[size - 1] = '\0'; +} + +/* The program's path, from whichever of the three tags the caller gave. */ +static STRPTR app_ProgramPath(struct TagItem *tags) +{ + struct WBStartup *wbs; + STRPTR path; + BPTR lock; + CONST_STRPTR name = NULL; + + if ((name = (CONST_STRPTR)GetTagData(REGAPP_FileName, 0, tags))) + return app_StrDup(name); + + if (!(path = AllocVec(1024, MEMF_ANY | MEMF_CLEAR))) + return NULL; + + if ((lock = (BPTR)GetTagData(REGAPP_FileLock, 0, tags))) + { + if (NameFromLock(lock, path, 1024)) + return path; + } + else if ((wbs = (struct WBStartup *)GetTagData(REGAPP_WBStartup, 0, tags)) + && wbs->sm_NumArgs > 0) + { + if (NameFromLock(wbs->sm_ArgList[0].wa_Lock, path, 1024) + && AddPart(path, wbs->sm_ArgList[0].wa_Name, 1024)) + return path; + } + else + { + struct Process *me = (struct Process *)FindTask(NULL); + TEXT prog[256]; + + if (me->pr_Task.tc_Node.ln_Type == NT_PROCESS && me->pr_HomeDir + && GetProgramName(prog, sizeof(prog)) + && NameFromLock(me->pr_HomeDir, path, 1024) + && AddPart(path, FilePart(prog), 1024)) + return path; + } + + FreeVec(path); + return NULL; +} + + +static BOOL app_SameApp(struct AppEntry *ae, CONST_STRPTR name, CONST_STRPTR url) +{ + return (Stricmp(ae->ae_Name, name) == 0) && (Stricmp(ae->ae_URL, url) == 0); +} + +static BOOL app_IdentTaken(struct ApplicationBase *AppBase, CONST_STRPTR ident) +{ + struct AppEntry *ae; + + ForeachNode(&AppBase->ab_Apps, ae) + { + if (Stricmp(ae->ae_Ident, ident) == 0) + return TRUE; + } + return FALSE; +} + +static void app_BuildIdent(STRPTR dst, struct AppEntry *ae, ULONG n) +{ + TEXT inst[16], *p = inst + sizeof(inst) - 1; + ULONG v = n; + + *p = '\0'; + if (n) + { + do + *--p = '0' + (v % 10); + while (v /= 10); + *--p = '_'; + } + strcpy(dst, ae->ae_Name); + strcat(dst, p); + if (ae->ae_URL[0]) + { + strcat(dst, "."); + strncat(dst, ae->ae_URL, APP_IDENTMAX - strlen(dst) - 1); + } +} + +/* "name.url", then "name_1.url", "name_2.url", ... for later instances. */ +static BOOL app_MakeIdent(struct ApplicationBase *AppBase, struct AppEntry *ae) +{ + ULONG n; + + app_BuildIdent(ae->ae_BaseIdent, ae, 0); + for (n = 0; ; n++) + { + app_BuildIdent(ae->ae_Ident, ae, n); + if (!app_IdentTaken(AppBase, ae->ae_Ident)) + { + ae->ae_Counter = n; + return TRUE; + } + if (ae->ae_Flags & APPF_NOCOUNTER) + return FALSE; + } +} + +/* [ENVDir/]basename.xml below ENV:, or the custom file name. */ +static STRPTR app_PrefsName(struct AppEntry *ae, struct TagItem *tags) +{ + CONST_STRPTR custom = (CONST_STRPTR)GetTagData(REGAPP_CustomPrefsFileName, 0, tags); + CONST_STRPTR dir = (CONST_STRPTR)GetTagData(REGAPP_ENVDir, 0, tags); + CONST_STRPTR base = (CONST_STRPTR)GetTagData(REGAPP_CustomPrefsBaseName, 0, tags); + STRPTR name; + + if (custom) + return app_StrDup(custom); + if (!base) + base = ae->ae_BaseIdent; + if (!(name = AllocVec(APP_PATHMAX, MEMF_ANY | MEMF_CLEAR))) + return NULL; + if (dir && *dir) + { + strncpy(name, dir, APP_PATHMAX - 1); + if (name[strlen(name) - 1] != '/' && name[strlen(name) - 1] != ':') + strncat(name, "/", APP_PATHMAX - strlen(name) - 1); + } + strncat(name, base, APP_PATHMAX - strlen(name) - 1); + strncat(name, ".xml", APP_PATHMAX - strlen(name) - 1); + return name; +} + +static ULONG app_FlagFor(Tag tag) +{ + switch (tag) + { + case REGAPP_UniqueApplication: return APPF_UNIQUE; + case REGAPP_HasIconifyFeature: + case APPATTR_HasIconifyFeature: return APPF_ICONIFY; + case REGAPP_HasPrefsWindow: + case APPATTR_HasPrefsWindow: return APPF_PREFSWINDOW; + case REGAPP_CanCreateNewDocs: + case APPATTR_CanCreateNewDocs: return APPF_NEWDOCS; + case REGAPP_CanPrintDocs: + case APPATTR_CanPrintDocs: return APPF_PRINTDOCS; + case REGAPP_AppNotifications: + case APPATTR_AppNotifications: return APPF_APPNOTIFY; + case REGAPP_BlankerNotifications: + case APPATTR_BlankerNotifications: return APPF_BLANKNOTIFY; + case REGAPP_AllowsBlanker: + case APPATTR_AllowsBlanker: return APPF_ALLOWBLANKER; + case REGAPP_SavePrefs: + case APPATTR_SavePrefs: return APPF_SAVEPREFS; + case REGAPP_Hidden: + case APPATTR_Hidden: return APPF_HIDDEN; + case REGAPP_NeedsGameMode: + case APPATTR_NeedsGameMode: return APPF_GAMEMODE; + case REGAPP_NoIcon: + case APPATTR_NoIcon: return APPF_NOICON; + } + return 0; +} + +void app_FreeEntry(struct AppEntry *ae) +{ + po_Release(ae->ae_Prefs); + FreeVec(ae->ae_PrefsName); + FreeVec(ae->ae_Description); + FreeVec(ae->ae_FileName); + FreeVec(ae); +} + +/***************************************************************************** + + NAME */ +#include + + AROS_LH2(ULONG, RegisterApplicationA, + +/* SYNOPSIS */ + AROS_LHA(CONST_STRPTR, name, A0), + AROS_LHA(struct TagItem *, tags, A1), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 5, Application) + +/* FUNCTION + Register the calling program as an application. It gets an ID, a + unique identifier ("name.urlidentifier", with "_1", "_2" ... added + for further instances) and a message port, see APPATTR_Port. + + INPUTS + name - application name, without spaces. + tags - REGAPP_ tags. With REGAPP_LoadPrefs the application's prefs + file, ENV:[ENVDir/]name.urlidentifier.xml (or ENVARC: if + that is missing), is read into APPATTR_MainPrefsDict. + + RESULT + The application ID, or 0. With REGAPP_UniqueApplication, 0 is also + returned when the application is already running; that instance is + sent an APPLIBMT_Unique message. + + NOTES + Call UnregisterApplication() from the same task before exiting. + + SEE ALSO + UnregisterApplicationA(), GetApplicationAttrsA() + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + struct TagItem *tstate = tags, *tag; + struct ApplicationIconInfo *aii; + struct AppEntry *ae, *other; + CONST_STRPTR url; + BOOL loadprefs, noted; + ULONG id = 0; + + if (!name || !*name || strchr(name, ' ') || strlen(name) >= APP_NAMEMAX) + return 0; + + if (!(ae = AllocVec(sizeof(struct AppEntry), MEMF_ANY | MEMF_CLEAR))) + return 0; + + app_CopyStr(ae->ae_Name, name, sizeof(ae->ae_Name)); + url = (CONST_STRPTR)GetTagData(REGAPP_URLIdentifier, (IPTR)"", tags); + if (!url) + url = ""; + if (Strnicmp(url, "www.", 4) == 0) + url += 4; + app_CopyStr(ae->ae_URL, url, sizeof(ae->ae_URL)); + + ae->ae_Flags = APPF_ALLOWBLANKER; + while ((tag = NextTagItem(&tstate))) + { + ULONG flag = app_FlagFor(tag->ti_Tag); + + if (flag) + { + if (tag->ti_Data) + ae->ae_Flags |= flag; + else + ae->ae_Flags &= ~flag; + } + } + if (!GetTagData(REGAPP_IdenticalNameCounter, TRUE, tags)) + ae->ae_Flags |= APPF_NOCOUNTER; + + ae->ae_Icon.iconType = APPICONT_ProgramIcon; + if ((aii = (struct ApplicationIconInfo *)GetTagData(REGAPP_AppIconInfo, 0, tags))) + ae->ae_Icon = *aii; + if (ae->ae_Flags & APPF_NOICON) + ae->ae_Icon.iconType = APPICONT_None; + + loadprefs = GetTagData(REGAPP_LoadPrefs, FALSE, tags) + || GetTagData(REGAPP_CustomPrefsFileName, 0, tags) + || GetTagData(REGAPP_CustomPrefsBaseName, 0, tags); + + ae->ae_Description = app_StrDup((CONST_STRPTR)GetTagData(REGAPP_Description, 0, tags)); + ae->ae_FileName = app_ProgramPath(tags); + ae->ae_Owner = FindTask(NULL); + + if (!(ae->ae_Port = CreateMsgPort())) + goto fail; + + /* The prefs file name depends on the base identifier only, so the file + is read before the lock is taken. */ + app_BuildIdent(ae->ae_BaseIdent, ae, 0); + if (!(ae->ae_PrefsName = app_PrefsName(ae, tags))) + goto fail; + if (loadprefs && (ae->ae_Prefs = po_NewDict())) + prefs_Read(AppBase, ae->ae_Prefs, ae->ae_PrefsName, TRUE, TRUE); + if (!(ae->ae_Flags & APPF_NOICON)) + app_PreloadGlobal(AppBase); + + ObtainSemaphore(&AppBase->ab_Lock); + app_Reap(AppBase); + + ForeachNode(&AppBase->ab_Apps, other) + { + if (app_SameApp(other, ae->ae_Name, ae->ae_URL) + && ((other->ae_Flags | ae->ae_Flags) & APPF_UNIQUE)) + { + app_Post(AppBase, other, 0, APPLIBMT_Unique, NULL); + ReleaseSemaphore(&AppBase->ab_Lock); + goto fail; + } + } + + if (!app_MakeIdent(AppBase, ae)) + { + ReleaseSemaphore(&AppBase->ab_Lock); + goto fail; + } + + ae->ae_ID = id = AppBase->ab_NextID++; + if (!AppBase->ab_NextID) + AppBase->ab_NextID = 1; + ADDTAIL((struct List *)&AppBase->ab_Apps, ae); + + app_BroadcastTo(AppBase, APPF_APPNOTIFY, id, APPLIBMT_AppRegister); + if (ae->ae_Flags & APPF_GAMEMODE) + app_Broadcast(AppBase, id, APPLIBMT_GameModeEntered); + app_UpdateBlanker(AppBase); + noted = !(ae->ae_Flags & APPF_NOICON) && app_NoteAppStarted(AppBase, ae); + + ReleaseSemaphore(&AppBase->ab_Lock); + + if (noted) + app_SaveLists(AppBase); + + D(bug("[application] registered %s as %lu\n", ae->ae_Ident, (unsigned long)id)); + return id; + +fail: + if (ae->ae_Port) + DeleteMsgPort(ae->ae_Port); + app_FreeEntry(ae); + return 0; + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH2(BOOL, UnregisterApplicationA, + +/* SYNOPSIS */ + AROS_LHA(ULONG, appID, D0), + AROS_LHA(struct TagItem *, tags, A0), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 6, Application) + +/* FUNCTION + Remove an application registered with RegisterApplication(). + Messages still waiting on its port are replied, game mode and a + blanker veto are undone, and with REGAPP_SavePrefs the prefs are + written to ENV: and ENVARC:. + + Docks that locked the application's icon are sent + APPLIBMT_AppUnregister; until they unlock it, or for at most five + seconds, the application stays. After that it is gone, but the + icon information a dock read stays valid until its last + UnlockApplicationIcon(). + + INPUTS + appID - the application ID. + tags - UNREGAPP_WaitForUnlock: FALSE to fail at once while the + icon is locked, instead of waiting. NULL is fine. + + RESULT + FALSE if there was no such application, or its icon is locked and + UNREGAPP_WaitForUnlock is FALSE. + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + BOOL wait = GetTagData(UNREGAPP_WaitForUnlock, TRUE, tags); + struct AppEntry *ae; + struct Message *msg; + PrefsObject *prefs = NULL; + STRPTR prefsname = NULL; + BOOL gone, arc; + LONG tries; + + ObtainSemaphore(&AppBase->ab_Lock); + app_Reap(AppBase); + if (!(ae = app_Find(AppBase, appID)) || (ae->ae_Flags & APPF_LEAVING)) + { + ReleaseSemaphore(&AppBase->ab_Lock); + return FALSE; + } + + if (ae->ae_IconLocks && !wait) + { + app_BroadcastTo(AppBase, APPF_APPNOTIFY, appID, APPLIBMT_AppWantsChangeIconType); + ReleaseSemaphore(&AppBase->ab_Lock); + return FALSE; + } + + ae->ae_Flags |= APPF_LEAVING; + app_BroadcastTo(AppBase, APPF_APPNOTIFY, appID, APPLIBMT_AppUnregister); + for (tries = 0; ae->ae_IconLocks && tries < APP_UNLOCK_TRIES; tries++) + { + ReleaseSemaphore(&AppBase->ab_Lock); + Delay(2); + ObtainSemaphore(&AppBase->ab_Lock); + app_Reap(AppBase); + } + REMOVE(ae); + /* A dock that still holds the icon lock may be reading the icon + information: the entry stays until its last unlock. */ + gone = ae->ae_IconLocks != 0; + if (gone) + ADDTAIL((struct List *)&AppBase->ab_Gone, ae); + + if (ae->ae_Flags & APPF_GAMEMODE) + app_Broadcast(AppBase, ae->ae_ID, APPLIBMT_GameModeLeft); + app_UpdateBlanker(AppBase); + + while ((msg = GetMsg(ae->ae_Port))) + ReplyMsg(msg); + app_Reap(AppBase); + + /* + * The port's signal belongs to the task that registered. From any other + * task, freeing it would release that task's signal bit here instead, + * so the port is left allocated. + */ + if (FindTask(NULL) == ae->ae_Owner) + DeleteMsgPort(ae->ae_Port); + ae->ae_Port = NULL; + + /* Taken over: nothing else reaches the entry, a dock reads only the icon. */ + if ((ae->ae_Flags & APPF_SAVEPREFS) && ae->ae_Prefs && ae->ae_PrefsName) + { + prefs = ae->ae_Prefs; + prefsname = ae->ae_PrefsName; + ae->ae_Prefs = NULL; + ae->ae_PrefsName = NULL; + } + arc = AppBase->ab_GlobalStale + && app_ArcDue(AppBase, IsListEmpty((struct List *)&AppBase->ab_Apps)); + ReleaseSemaphore(&AppBase->ab_Lock); + + if (prefs) + { + prefs_Write(AppBase, prefs, prefsname, TRUE, TRUE); + po_Release(prefs); + FreeVec(prefsname); + } + if (!gone) + app_FreeEntry(ae); + if (arc) + app_SaveGlobal(AppBase, FALSE, TRUE); + return TRUE; + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH2(BOOL, SetApplicationAttrsA, + +/* SYNOPSIS */ + AROS_LHA(ULONG, appID, D0), + AROS_LHA(struct TagItem *, tags, A0), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 7, Application) + +/* FUNCTION + Change the attributes of an application, see the APPATTR_ tags + marked (S) in . + + APPATTR_IconType fails while a dock has the icon locked; the docks + are asked to unlock it, so try again a little later. + APPATTR_MainPrefsDict hands the dictionary to the application, + which releases the one it had. APPATTR_FlushPrefs TRUE writes the + dictionary to ENV: and ENVARC: now, whether or not the application + registered with REGAPP_SavePrefs. + + RESULT + FALSE if there was no such application or a tag could not be set. + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + struct TagItem *tstate = tags, *tag; + struct AppEntry *ae; + PrefsObject *flush = NULL; + STRPTR flushname = NULL; + BOOL ok = TRUE, saveglobal = FALSE; + + app_PreloadGlobal(AppBase); + ObtainSemaphore(&AppBase->ab_Lock); + app_Reap(AppBase); + if (!(ae = app_Find(AppBase, appID))) + { + ReleaseSemaphore(&AppBase->ab_Lock); + return FALSE; + } + + while ((tag = NextTagItem(&tstate))) + { + ULONG flag; + + switch (tag->ti_Tag) + { + case APPATTR_NeedsGameMode: + if (!tag->ti_Data != !(ae->ae_Flags & APPF_GAMEMODE)) + { + ae->ae_Flags ^= APPF_GAMEMODE; + app_Broadcast(AppBase, ae->ae_ID, + tag->ti_Data ? APPLIBMT_GameModeEntered : APPLIBMT_GameModeLeft); + app_UpdateBlanker(AppBase); + } + break; + + case APPATTR_AllowsBlanker: + if (tag->ti_Data) + ae->ae_Flags |= APPF_ALLOWBLANKER; + else + ae->ae_Flags &= ~APPF_ALLOWBLANKER; + app_UpdateBlanker(AppBase); + break; + + case APPATTR_Hidden: + if (!tag->ti_Data != !(ae->ae_Flags & APPF_HIDDEN)) + { + ae->ae_Flags ^= APPF_HIDDEN; + app_BroadcastTo(AppBase, APPF_APPNOTIFY, ae->ae_ID, APPLIBMT_AppDidChangeHidden); + } + break; + + case APPATTR_IconType: + if (!tag->ti_Data) + ok = FALSE; + else if (ae->ae_IconLocks) + { + app_BroadcastTo(AppBase, APPF_APPNOTIFY, ae->ae_ID, APPLIBMT_AppWantsChangeIconType); + ok = FALSE; + } + else + { + ae->ae_Icon = *(struct ApplicationIconInfo *)tag->ti_Data; + app_BroadcastTo(AppBase, APPF_APPNOTIFY, ae->ae_ID, APPLIBMT_AppDidChangeIconType); + } + break; + + case APPATTR_AppOpenedDocument: + if (app_NoteDocOpened(AppBase, ae, (CONST_STRPTR)tag->ti_Data)) + saveglobal = TRUE; + break; + + case APPATTR_ClearLastUsedDocs: + if (tag->ti_Data && app_ClearDocsOfApp(AppBase, ae)) + saveglobal = TRUE; + break; + + case APPATTR_MainPrefsDict: + if (po_Type((PrefsObject *)tag->ti_Data) != ALPOT_Dictionary) + ok = FALSE; + else if ((PrefsObject *)tag->ti_Data != ae->ae_Prefs) + { + po_Release(ae->ae_Prefs); + ae->ae_Prefs = (PrefsObject *)tag->ti_Data; + } + break; + + case APPATTR_FlushPrefs: + if (!tag->ti_Data) + break; + if (!ae->ae_Prefs || !(flushname = app_StrDup(ae->ae_PrefsName)) + || !(flush = po_Copy(ae->ae_Prefs))) + ok = FALSE; + break; + + default: + flag = app_FlagFor(tag->ti_Tag); + if (flag && flag != APPF_UNIQUE && flag != APPF_NOICON) + { + if (tag->ti_Data) + ae->ae_Flags |= flag; + else + ae->ae_Flags &= ~flag; + } + else + ok = FALSE; + break; + } + } + ReleaseSemaphore(&AppBase->ab_Lock); + + /* The file I/O, with the lock released: snapshots of the global lists + and of the application's own dictionary. */ + if (saveglobal) + app_SaveLists(AppBase); + if (flush) + { + if (prefs_Write(AppBase, flush, flushname, TRUE, TRUE)) + ok = FALSE; + po_Release(flush); + } + FreeVec(flushname); + + return ok; + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH2(BOOL, GetApplicationAttrsA, + +/* SYNOPSIS */ + AROS_LHA(ULONG, appID, D0), + AROS_LHA(struct TagItem *, tags, A0), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 8, Application) + +/* FUNCTION + Read attributes of an application. ti_Data points to the storage: + an APTR or STRPTR for pointer attributes, a ULONG for the others. + Strings stay valid while the application is registered. + + APPATTR_MainPrefsDict is the application's own dictionary, made + empty if it had none; APPATTR_LastUsedDocsArray is a copy, to be + ALPO_Release'd. APPATTR_IconType may only be used while the icon + is locked with LockApplicationIcon(). + + RESULT + FALSE if there was no such application or a tag is unknown. + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + struct TagItem *tstate = tags, *tag; + struct AppEntry *ae; + BOOL ok = TRUE; + + if (FindTagItem(APPATTR_LastUsedDocsArray, tags)) + app_PreloadGlobal(AppBase); + ObtainSemaphore(&AppBase->ab_Lock); + app_Reap(AppBase); + if (!(ae = app_Find(AppBase, appID))) + { + ReleaseSemaphore(&AppBase->ab_Lock); + return FALSE; + } + + while ((tag = NextTagItem(&tstate))) + { + APTR *pstore = (APTR *)tag->ti_Data; + ULONG *lstore = (ULONG *)tag->ti_Data; + ULONG flag; + + if (!tag->ti_Data) + continue; + + switch (tag->ti_Tag) + { + case APPATTR_Port: *pstore = ae->ae_Port; break; + case APPATTR_Name: *pstore = ae->ae_Name; break; + case APPATTR_AppIdentifier: *pstore = ae->ae_Ident; break; + case APPATTR_URLIdentifier: *pstore = ae->ae_URL; break; + case APPATTR_Description: *pstore = ae->ae_Description; break; + case APPATTR_FileName: *pstore = ae->ae_FileName; break; + case APPATTR_IconType: *pstore = &ae->ae_Icon; break; + case APPATTR_AppIdentifierCounter: *lstore = ae->ae_Counter; break; + case APPATTR_UniqueApplication: *lstore = (ae->ae_Flags & APPF_UNIQUE) != 0; break; + case APPATTR_LastUsedDocsArray: + if (!(*pstore = app_DocsOfApp(AppBase, ae))) + ok = FALSE; + break; + case APPATTR_MainPrefsDict: + if (!ae->ae_Prefs) + ae->ae_Prefs = po_NewDict(); + if (!(*pstore = ae->ae_Prefs)) + ok = FALSE; + break; + default: + if ((flag = app_FlagFor(tag->ti_Tag))) + *lstore = (ae->ae_Flags & flag) != 0; + else + ok = FALSE; + break; + } + } + ReleaseSemaphore(&AppBase->ab_Lock); + + return ok; + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH1(ULONG, FindApplicationA, + +/* SYNOPSIS */ + AROS_LHA(struct TagItem *, tags, A0), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 9, Application) + +/* FUNCTION + Find a registered application by FINDAPP_Name (the first instance), + FINDAPP_AppIdentifier (one instance) or FINDAPP_FileName. + + RESULT + The application ID, or 0. + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + CONST_STRPTR name = (CONST_STRPTR)GetTagData(FINDAPP_Name, 0, tags); + CONST_STRPTR ident = (CONST_STRPTR)GetTagData(FINDAPP_AppIdentifier, 0, tags); + CONST_STRPTR file = (CONST_STRPTR)GetTagData(FINDAPP_FileName, 0, tags); + struct AppEntry *ae; + ULONG id = 0; + + ObtainSemaphore(&AppBase->ab_Lock); + ForeachNode(&AppBase->ab_Apps, ae) + { + if ((name && Stricmp(ae->ae_Name, name) == 0) + || (ident && Stricmp(ae->ae_Ident, ident) == 0) + || (file && ae->ae_FileName && Stricmp(ae->ae_FileName, file) == 0)) + { + id = ae->ae_ID; + break; + } + } + ReleaseSemaphore(&AppBase->ab_Lock); + + return id; + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH4(BOOL, SendApplicationMsg, + +/* SYNOPSIS */ + AROS_LHA(ULONG, senderAppID, D0), + AROS_LHA(ULONG, receiverAppID, D1), + AROS_LHA(struct ApplicationMsg *, msg, A0), + AROS_LHA(ULONG, type, D2), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 10, Application) + +/* FUNCTION + Send a message to an application, or with receiverAppID 0 to every + application except the sender. The library sends a copy: msg, and + the string of an APPLIBMT_CustomMsg, APPLIBMT_OpenDoc or + APPLIBMT_PrintDoc, can be reused at once. msg may be NULL for the + other types. + + The APPLIBMT_App... types always go to every application with + AppNotifications set, the APPLIBMT_Blanker... types to every one + with BlankerNotifications set, whatever receiverAppID is. + + RESULT + TRUE if at least one application was sent the message. + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + CONST_STRPTR text = NULL; + struct AppEntry *ae; + BOOL sent = FALSE, always; + ULONG flag = 0; + + if (msg) + { + if (type == APPLIBMT_CustomMsg || type == APPLIBMT_ConfirmClose) + text = ((struct ApplicationCustomMsg *)msg)->customMsg; + else if (type == APPLIBMT_OpenDoc || type == APPLIBMT_PrintDoc) + text = ((struct ApplicationOpenPrintDocMsg *)msg)->fileName; + } + always = app_IsAlwaysBroadcast(type, &flag); + + ObtainSemaphore(&AppBase->ab_Lock); + app_Reap(AppBase); + ForeachNode(&AppBase->ab_Apps, ae) + { + if (always) + { + if (ae->ae_ID == senderAppID || !(ae->ae_Flags & flag)) + continue; + } + else if ((receiverAppID && ae->ae_ID != receiverAppID) + || (!receiverAppID && ae->ae_ID == senderAppID)) + continue; + if (app_Post(AppBase, ae, senderAppID, type, text)) + sent = TRUE; + } + ReleaseSemaphore(&AppBase->ab_Lock); + + return sent; + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH0(struct MinList *, GetApplicationList, + +/* SYNOPSIS */ + +/* LOCATION */ + struct ApplicationBase *, AppBase, 12, Application) + +/* FUNCTION + A snapshot of the registered applications, as a list of + struct ApplicationNode. Free it with FreeApplicationList(). + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + struct MinList *list; + struct AppEntry *ae; + + if (!(list = AllocVec(sizeof(struct MinList), MEMF_ANY))) + return NULL; + NEWLIST((struct List *)list); + + ObtainSemaphore(&AppBase->ab_Lock); + ForeachNode(&AppBase->ab_Apps, ae) + { + struct ApplicationNode *an; + ULONG il = strlen(ae->ae_Ident) + 1, nl = strlen(ae->ae_Name) + 1; + ULONG dl = ae->ae_Description ? strlen(ae->ae_Description) + 1 : 0; + ULONG fl = ae->ae_FileName ? strlen(ae->ae_FileName) + 1 : 0; + STRPTR p; + + if (ae->ae_Flags & APPF_LEAVING) + continue; + if (!(an = AllocVec(sizeof(*an) + il + nl + dl + fl, MEMF_ANY | MEMF_CLEAR))) + break; + p = (STRPTR)(an + 1); + an->identifier = p; + CopyMem(ae->ae_Ident, p, il); + p += il; + an->name = p; + CopyMem(ae->ae_Name, p, nl); + p += nl; + if (dl) + { + an->description = p; + CopyMem(ae->ae_Description, p, dl); + p += dl; + } + if (fl) + { + an->fileName = p; + CopyMem(ae->ae_FileName, p, fl); + } + an->appID = ae->ae_ID; + an->hidden = (ae->ae_Flags & APPF_HIDDEN) != 0; + ADDTAIL((struct List *)list, (struct Node *)an); + } + ReleaseSemaphore(&AppBase->ab_Lock); + + return list; + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH1(VOID, FreeApplicationList, + +/* SYNOPSIS */ + AROS_LHA(struct MinList *, list, A0), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 13, Application) + +/* FUNCTION + Free a list from GetApplicationList(). + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + struct Node *node; + + (void)AppBase; + if (!list) + return; + while ((node = REMHEAD((struct List *)list))) + FreeVec(node); + FreeVec(list); + + AROS_LIBFUNC_EXIT +} diff --git a/workbench/libs/application/application_init.c b/workbench/libs/application/application_init.c new file mode 100644 index 00000000000..6b10b284493 --- /dev/null +++ b/workbench/libs/application/application_init.c @@ -0,0 +1,219 @@ +/* + Copyright (C) 2026, The AROS Development Team. All rights reserved. + + Desc: application.library init, expunge and shared helpers. +*/ + +#include +#include +#include +#include +#include + +#include + +#include "application_intern.h" +#include LC_LIBDEFS_FILE + +struct DosLibrary *DOSBase = NULL; +struct UtilityBase *UtilityBase = NULL; + +static int Application_Init(LIBBASETYPEPTR AppBase) +{ + InitSemaphore(&AppBase->ab_Lock); + InitSemaphore(&AppBase->ab_SaveLock); + NEWLIST((struct List *)&AppBase->ab_Apps); + NEWLIST((struct List *)&AppBase->ab_Gone); + AppBase->ab_NextID = 1; + AppBase->ab_ArcMinute = -1; + AppBase->ab_BlankerAllowed = TRUE; + + AppBase->ab_Reaper.mp_Node.ln_Type = NT_MSGPORT; + AppBase->ab_Reaper.mp_Flags = PA_IGNORE; + NEWLIST(&AppBase->ab_Reaper.mp_MsgList); + + if (!(DOSBase = (struct DosLibrary *)OpenLibrary("dos.library", 36))) + return FALSE; + if (!(UtilityBase = (struct UtilityBase *)OpenLibrary("utility.library", 36))) + { + CloseLibrary((struct Library *)DOSBase); + DOSBase = NULL; + return FALSE; + } + return TRUE; +} + +static int Application_Expunge(LIBBASETYPEPTR AppBase) +{ + BOOL idle; + + ObtainSemaphore(&AppBase->ab_Lock); + app_Reap(AppBase); + idle = IsListEmpty((struct List *)&AppBase->ab_Apps) && IsListEmpty((struct List *)&AppBase->ab_Gone) + && !AppBase->ab_Outstanding; + ReleaseSemaphore(&AppBase->ab_Lock); + + /* Messages still out with receivers point back at ab_Reaper, and a dock + may still hold an icon lock. Nothing is written here: expunge runs + from the low memory handler, under Forbid(). */ + if (!idle) + return FALSE; + + po_Release(AppBase->ab_Global); + AppBase->ab_Global = NULL; + AppBase->ab_GlobalLoaded = FALSE; + + CloseLibrary((struct Library *)UtilityBase); + CloseLibrary((struct Library *)DOSBase); + UtilityBase = NULL; + DOSBase = NULL; + return TRUE; +} + +ADD2INITLIB(Application_Init, 0) +ADD2EXPUNGELIB(Application_Expunge, 0) + +STRPTR app_StrDup(CONST_STRPTR s) +{ + STRPTR d; + ULONG len; + + if (!s) + return NULL; + len = strlen(s) + 1; + if ((d = AllocVec(len, MEMF_ANY))) + CopyMem((APTR)s, d, len); + return d; +} + +struct AppEntry *app_Find(struct ApplicationBase *AppBase, ULONG appID) +{ + struct AppEntry *ae; + + if (!appID) + return NULL; + ForeachNode(&AppBase->ab_Apps, ae) + { + if (ae->ae_ID == appID) + return ae; + } + return NULL; +} + +void app_Reap(struct ApplicationBase *AppBase) +{ + struct Message *msg; + + while ((msg = GetMsg(&AppBase->ab_Reaper))) + { + FreeVec(msg); + AppBase->ab_Outstanding--; + } +} + +/* + * Send a copy of a message to one application. The copy carries its own + * string, so the sender's buffers can go away at once. + */ +BOOL app_Post(struct ApplicationBase *AppBase, struct AppEntry *to, ULONG sender, + ULONG type, CONST_STRPTR text) +{ + struct ApplicationMsg *am; + ULONG size, textlen = text ? strlen(text) + 1 : 0; + + if (!to || !to->ae_Port) + return FALSE; + + switch (type) + { + case APPLIBMT_CustomMsg: + case APPLIBMT_ConfirmClose: + size = sizeof(struct ApplicationCustomMsg); + break; + case APPLIBMT_OpenDoc: + case APPLIBMT_PrintDoc: + size = sizeof(struct ApplicationOpenPrintDocMsg); + break; + default: + size = sizeof(struct ApplicationMsg); + textlen = 0; + break; + } + + if (!(am = AllocVec(size + textlen, MEMF_PUBLIC | MEMF_CLEAR))) + return FALSE; + + am->msg.mn_Node.ln_Type = NT_MESSAGE; + am->msg.mn_Length = size; + am->msg.mn_ReplyPort = &AppBase->ab_Reaper; + am->senderAppID = sender; + am->type = type; + + if (textlen) + { + STRPTR copy = (STRPTR)am + size; + + CopyMem((APTR)text, copy, textlen); + if (type == APPLIBMT_CustomMsg || type == APPLIBMT_ConfirmClose) + ((struct ApplicationCustomMsg *)am)->customMsg = copy; + else + ((struct ApplicationOpenPrintDocMsg *)am)->fileName = copy; + } + + AppBase->ab_Outstanding++; + PutMsg(to->ae_Port, &am->msg); + return TRUE; +} + +void app_Broadcast(struct ApplicationBase *AppBase, ULONG sender, ULONG type) +{ + struct AppEntry *ae; + + ForeachNode(&AppBase->ab_Apps, ae) + { + if (ae->ae_ID != sender) + app_Post(AppBase, ae, sender, type, NULL); + } +} + +/* Every application whose flags include 'flag', except the sender. */ +void app_BroadcastTo(struct ApplicationBase *AppBase, ULONG flag, ULONG sender, ULONG type) +{ + struct AppEntry *ae; + + ForeachNode(&AppBase->ab_Apps, ae) + { + if (ae->ae_ID != sender && (ae->ae_Flags & flag) && !(ae->ae_Flags & APPF_LEAVING)) + app_Post(AppBase, ae, sender, type, NULL); + } +} + +/* The types that only go to applications which asked for them. */ +BOOL app_IsAlwaysBroadcast(ULONG type, ULONG *flag) +{ + switch (type) + { + case APPLIBMT_AppRegister: + case APPLIBMT_AppUnregister: + case APPLIBMT_AppWantsChangeIconType: + case APPLIBMT_AppDidChangeIconType: + case APPLIBMT_AppDidChangeHidden: + case APPLIBMT_LastAppDocChange: + *flag = APPF_APPNOTIFY; + return TRUE; + case APPLIBMT_BlankerAllow: + case APPLIBMT_BlankerDisallow: + case APPLIBMT_BlankerBlank: + case APPLIBMT_BlankerMonitorOff: + case APPLIBMT_BlankerUnBlank: + *flag = APPF_BLANKNOTIFY; + return TRUE; + } + return FALSE; +} + +/* File access needs a process. */ +BOOL app_IsProcess(void) +{ + return FindTask(NULL)->tc_Node.ln_Type == NT_PROCESS; +} diff --git a/workbench/libs/application/application_intern.h b/workbench/libs/application/application_intern.h new file mode 100644 index 00000000000..931bcdba2b1 --- /dev/null +++ b/workbench/libs/application/application_intern.h @@ -0,0 +1,143 @@ +#ifndef APPLICATION_INTERN_H +#define APPLICATION_INTERN_H + +/* + Copyright (C) 2026, The AROS Development Team. All rights reserved. + + Desc: application.library private state. +*/ + +#include +#include +#include +#include +#include +#include + +#define APP_NAMEMAX 64 +#define APP_IDENTMAX 200 +#define APP_PATHMAX 512 + +#define APPF_UNIQUE (1 << 0) +#define APPF_ICONIFY (1 << 1) +#define APPF_PREFSWINDOW (1 << 2) +#define APPF_NEWDOCS (1 << 3) +#define APPF_PRINTDOCS (1 << 4) +#define APPF_APPNOTIFY (1 << 5) +#define APPF_BLANKNOTIFY (1 << 6) +#define APPF_ALLOWBLANKER (1 << 7) +#define APPF_SAVEPREFS (1 << 8) +#define APPF_HIDDEN (1 << 9) +#define APPF_GAMEMODE (1 << 10) +#define APPF_NOICON (1 << 11) +#define APPF_LEAVING (1 << 12) /* unregistering, waiting for icon locks */ +#define APPF_NOCOUNTER (1 << 13) /* REGAPP_IdenticalNameCounter FALSE */ + +struct AppEntry +{ + struct MinNode ae_Node; + ULONG ae_ID; + ULONG ae_Flags; + ULONG ae_Counter; /* the _n of the identifier */ + struct MsgPort *ae_Port; /* created in, and owned by, ae_Owner */ + struct Task *ae_Owner; + STRPTR ae_Description; + STRPTR ae_FileName; + STRPTR ae_PrefsName; /* below ENV:, or a full path */ + PrefsObject *ae_Prefs; /* main prefs dictionary, or NULL */ + ULONG ae_IconLocks; + struct ApplicationIconInfo ae_Icon; + TEXT ae_Name[APP_NAMEMAX]; + TEXT ae_URL[APP_NAMEMAX * 2]; + TEXT ae_Ident[APP_IDENTMAX]; + TEXT ae_BaseIdent[APP_IDENTMAX]; /* without the _n */ +}; + +struct ApplicationBase +{ + struct Library ab_Lib; + struct SignalSemaphore ab_Lock; + struct MinList ab_Apps; + struct MinList ab_Gone; /* unregistered, icon still locked by a dock */ + ULONG ab_NextID; + /* + * Messages the library sends come back here when the receiver replies. + * The port never signals; replied messages are freed on the next call. + */ + struct MsgPort ab_Reaper; + ULONG ab_Outstanding; /* sent, not yet freed */ + + /* Global settings and the last used lists, ENV:Sys/application.xml */ + PrefsObject *ab_Global; + BOOL ab_GlobalLoaded; + BOOL ab_GlobalStale; /* ENVARC: behind ENV: */ + LONG ab_ArcMinute; /* when ENVARC: was last written, -1 never */ + struct SignalSemaphore ab_SaveLock; /* one global save at a time */ + BOOL ab_BlankerAllowed; /* as last broadcast */ +}; + +extern struct DosLibrary *DOSBase; +extern struct UtilityBase *UtilityBase; + +#define APPLIB_GLOBALPREFS "Sys/application.xml" + +/* Callers hold ab_Lock. */ +struct AppEntry *app_Find(struct ApplicationBase *AppBase, ULONG appID); +void app_Reap(struct ApplicationBase *AppBase); +BOOL app_Post(struct ApplicationBase *AppBase, struct AppEntry *to, ULONG sender, + ULONG type, CONST_STRPTR text); +void app_Broadcast(struct ApplicationBase *AppBase, ULONG sender, ULONG type); +void app_BroadcastTo(struct ApplicationBase *AppBase, ULONG flag, ULONG sender, ULONG type); +BOOL app_IsAlwaysBroadcast(ULONG type, ULONG *flag); +void app_UpdateBlanker(struct ApplicationBase *AppBase); +BOOL app_GameModeActive(struct ApplicationBase *AppBase); +void app_LoadGlobal(struct ApplicationBase *AppBase); +BOOL app_NoteAppStarted(struct ApplicationBase *AppBase, struct AppEntry *ae); +BOOL app_NoteDocOpened(struct ApplicationBase *AppBase, struct AppEntry *ae, CONST_STRPTR path); +PrefsObject *app_DocsOfApp(struct ApplicationBase *AppBase, struct AppEntry *ae); +BOOL app_ClearDocsOfApp(struct ApplicationBase *AppBase, struct AppEntry *ae); +BOOL app_ArcDue(struct ApplicationBase *AppBase, BOOL last); + +void app_FreeEntry(struct AppEntry *ae); + +/* Callers must not hold ab_Lock: these do the file I/O. */ +void app_PreloadGlobal(struct ApplicationBase *AppBase); +void app_SaveGlobal(struct ApplicationBase *AppBase, BOOL env, BOOL envarc); +void app_SaveLists(struct ApplicationBase *AppBase); +STRPTR app_StrDup(CONST_STRPTR s); +BOOL app_IsProcess(void); + +/* PrefsObjects, for use inside the library */ +PrefsObject *po_NewDict(void); +PrefsObject *po_NewArray(void); +PrefsObject *po_NewString(CONST_STRPTR s); +PrefsObject *po_NewLong(LONG v); +PrefsObject *po_NewNow(void); +void po_Release(PrefsObject *obj); +PrefsObject *po_Copy(PrefsObject *obj); +ULONG po_Type(PrefsObject *obj); +LONG po_Count(PrefsObject *obj); +PrefsObject *po_At(PrefsObject *obj, LONG index); +BOOL po_ArrayInsert(PrefsObject *arr, PrefsObject *item, LONG index); +void po_ArrayRemove(PrefsObject *arr, LONG index); +void po_Clear(PrefsObject *obj); +BOOL po_DictSet(PrefsObject *dict, CONST_STRPTR key, PrefsObject *item); +PrefsObject *po_DictGet(PrefsObject *dict, CONST_STRPTR key); +CONST_STRPTR po_DictString(PrefsObject *dict, CONST_STRPTR key, CONST_STRPTR def); +LONG po_DictLong(PrefsObject *dict, CONST_STRPTR key, LONG def); +BOOL po_MoveContents(PrefsObject *to, PrefsObject *from); + +/* Serialization, in application_xml.c */ +ULONG xml_Serialize(PrefsObject *obj, struct ALPOPutProcInfo *pi); +ULONG xml_Deserialize(PrefsObject *obj, struct ALPOGetProcInfo *pi); +ULONG xml_Begin(struct ALPOGetProcInfo *pi); +ULONG xml_ReadStream(PrefsObject *dict, struct ALPOGetProcInfo *pi); +ULONG xml_WriteStream(PrefsObject *dict, struct ALPOPutProcInfo *pi); +ULONG xml_ReadFile(PrefsObject *dict, CONST_STRPTR path); +ULONG xml_WriteFile(PrefsObject *dict, CONST_STRPTR path); +ULONG prefs_Read(struct ApplicationBase *AppBase, PrefsObject *dict, CONST_STRPTR name, + BOOL env, BOOL envarc); +ULONG prefs_Write(struct ApplicationBase *AppBase, PrefsObject *dict, CONST_STRPTR name, + BOOL env, BOOL envarc); + +#endif /* APPLICATION_INTERN_H */ diff --git a/workbench/libs/application/application_notify.c b/workbench/libs/application/application_notify.c new file mode 100644 index 00000000000..90601b92f82 --- /dev/null +++ b/workbench/libs/application/application_notify.c @@ -0,0 +1,216 @@ +/* + Copyright (C) 2026, The AROS Development Team. All rights reserved. + + Desc: Notify() - hand a pop-up to the notification server. +*/ + +#include +#include +#include +#include +#include +#include + +#include + +#include "application_intern.h" +#include "application_server.h" +#include LC_LIBDEFS_FILE + +static void ntf_Copy(STRPTR dst, CONST_STRPTR src, ULONG size) +{ + if (!src) + src = ""; + strncpy(dst, src, size - 1); + dst[size - 1] = '\0'; +} + +/* Start the server if it is installed but not running, and wait up to + ~3 s for its port. SystemTagList() with SYS_Asynch does not report a + missing program, so its presence is checked first. */ +static BOOL ntf_StartServer(void) +{ + struct TagItem tags[] = + { + { SYS_Asynch, TRUE }, + { SYS_Input, 0 }, + { SYS_Output, 0 }, + { NP_Name, (IPTR)"Notifications" }, + { TAG_DONE, 0 } + }; + BPTR in, out, lock; + LONG tries; + + if (FindPort(NOTIFYSERVER_PORTNAME)) + return TRUE; + if (!(lock = Lock(NOTIFYSERVER_PATH, SHARED_LOCK))) + return FALSE; + UnLock(lock); + + in = Open("NIL:", MODE_OLDFILE); + out = Open("NIL:", MODE_NEWFILE); + if (!in || !out) + { + if (in) + Close(in); + if (out) + Close(out); + return FALSE; + } + tags[1].ti_Data = (IPTR)in; + tags[2].ti_Data = (IPTR)out; + + /* With SYS_Asynch the new process owns the handles from here on. */ + if (SystemTagList(NOTIFYSERVER_PATH, tags) == -1) + { + Close(in); + Close(out); + return FALSE; + } + + for (tries = 0; tries < 60; tries++) + { + if (FindPort(NOTIFYSERVER_PORTNAME)) + return TRUE; + Delay(3); + } + return FALSE; +} + +/***************************************************************************** + + NAME */ +#include + + AROS_LH2(ULONG, NotifyA, + +/* SYNOPSIS */ + AROS_LHA(ULONG, appID, D0), + AROS_LHA(struct TagItem *, tags, A0), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 11, Application) + +/* FUNCTION + Show a pop-up notification for a registered application, and add it + to the notification log. It closes by itself after a few seconds + (a priority 10 one stays until closed) and asks nothing of the user. + + Double-clicking it sends APPNOTIFY_BackMsg to the application as an + APPLIBMT_CustomMsg; a BackMsg starting with "URL:" opens the URL in + the default browser instead. + + The notification server, SYS:System/Notifications, is started if + it is not running. How pop-ups look and which applications may show + them is set in the Notifications preferences. + + INPUTS + appID - the application ID from RegisterApplication(). + tags - APPNOTIFY_ tags; at least a title or a text, unless + APPNOTIFY_Withdraw takes back earlier pop-ups. + + RESULT + APPNOTIFY_OK_MSGQUEUED (0), or an APPNOTIFY_ERROR_ code: + _APPNOTREGISTERED, _TAGS, _GAMEMODEACTIVE (nothing is shown while + an application is in game mode), _APPNOTALLOWED (switched off in + the preferences), _TOOMANYMESSAGESQUEUED, _SERVERNOTRUNNING or + _OUTOFMEM. + + NOTES + Use notifications sparingly, keep them short, and never use one for + an error or a question: the user may never see it. + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + struct NotifyServerMsg *nsm; + struct MsgPort *reply, *server; + struct AppEntry *ae; + CONST_STRPTR text = (CONST_STRPTR)GetTagData(APPNOTIFY_Text, 0, tags); + CONST_STRPTR title = (CONST_STRPTR)GetTagData(APPNOTIFY_Title, 0, tags); + BOOL logonly = GetTagData(APPNOTIFY_LogOnly, FALSE, tags) ? TRUE : FALSE; + ULONG pri = GetTagData(APPNOTIFY_Pri, 0, tags); + BOOL gamemode; + ULONG result = APPNOTIFY_ERROR_SERVERNOTRUNNING; + + CONST_STRPTR withdraw = (CONST_STRPTR)GetTagData(APPNOTIFY_Withdraw, 0, tags); + + if (!text && !title && !withdraw) + return APPNOTIFY_ERROR_TAGS; + + if (!(nsm = AllocVec(sizeof(*nsm), MEMF_PUBLIC | MEMF_CLEAR))) + return APPNOTIFY_ERROR_OUTOFMEM; + + ObtainSemaphore(&AppBase->ab_Lock); + app_Reap(AppBase); + if ((ae = app_Find(AppBase, appID))) + ntf_Copy(nsm->nsm_AppName, ae->ae_Name, sizeof(nsm->nsm_AppName)); + gamemode = app_GameModeActive(AppBase); + ReleaseSemaphore(&AppBase->ab_Lock); + + if (!ae) + { + FreeVec(nsm); + return APPNOTIFY_ERROR_APPNOTREGISTERED; + } + if (gamemode && !logonly && !withdraw) + { + FreeVec(nsm); + return APPNOTIFY_ERROR_GAMEMODEACTIVE; + } + + nsm->nsm_Version = NOTIFYSERVER_VERSION; + nsm->nsm_AppID = appID; + nsm->nsm_CloseOnDC = GetTagData(APPNOTIFY_CloseOnDC, FALSE, tags) ? TRUE : FALSE; + nsm->nsm_Update = GetTagData(APPNOTIFY_Update, FALSE, tags) ? TRUE : FALSE; + nsm->nsm_ConfirmClose = GetTagData(APPNOTIFY_ConfirmClose, FALSE, tags) ? TRUE : FALSE; + nsm->nsm_LogOnly = logonly; + nsm->nsm_Pri = pri > 10 ? 10 : pri; + nsm->nsm_ImageVertAlign = GetTagData(APPNOTIFY_ImageVertAlign, 0, tags); + ntf_Copy(nsm->nsm_Title, title ? title : (CONST_STRPTR)nsm->nsm_AppName, sizeof(nsm->nsm_Title)); + ntf_Copy(nsm->nsm_Text, text, sizeof(nsm->nsm_Text)); + ntf_Copy(nsm->nsm_PubScreen, (CONST_STRPTR)GetTagData(APPNOTIFY_PubScreenName, 0, tags), + sizeof(nsm->nsm_PubScreen)); + ntf_Copy(nsm->nsm_ImageFile, (CONST_STRPTR)GetTagData(APPNOTIFY_ImageFile, 0, tags), + sizeof(nsm->nsm_ImageFile)); + ntf_Copy(nsm->nsm_IconFile, (CONST_STRPTR)GetTagData(APPNOTIFY_IconFile, 0, tags), + sizeof(nsm->nsm_IconFile)); + ntf_Copy(nsm->nsm_SoundFile, (CONST_STRPTR)GetTagData(APPNOTIFY_SoundFile, 0, tags), + sizeof(nsm->nsm_SoundFile)); + ntf_Copy(nsm->nsm_BackMsg, withdraw ? withdraw : (CONST_STRPTR)GetTagData(APPNOTIFY_BackMsg, 0, tags), + sizeof(nsm->nsm_BackMsg)); + nsm->nsm_Withdraw = withdraw ? TRUE : FALSE; + ntf_Copy(nsm->nsm_Gauges, (CONST_STRPTR)GetTagData(APPNOTIFY_Gauges, 0, tags), sizeof(nsm->nsm_Gauges)); + + if (ntf_StartServer()) + { + if (!(reply = CreateMsgPort())) + result = APPNOTIFY_ERROR_OUTOFMEM; + else + { + nsm->nsm_Msg.mn_Node.ln_Type = NT_MESSAGE; + nsm->nsm_Msg.mn_Length = sizeof(*nsm); + nsm->nsm_Msg.mn_ReplyPort = reply; + + /* The server may be quitting; hold the port still while posting. */ + Forbid(); + if ((server = FindPort(NOTIFYSERVER_PORTNAME))) + PutMsg(server, &nsm->nsm_Msg); + Permit(); + + if (server) + { + WaitPort(reply); + GetMsg(reply); + result = nsm->nsm_Result; + } + DeleteMsgPort(reply); + } + } + + FreeVec(nsm); + return result; + + AROS_LIBFUNC_EXIT +} diff --git a/workbench/libs/application/application_po.h b/workbench/libs/application/application_po.h new file mode 100644 index 00000000000..4f6da51c74c --- /dev/null +++ b/workbench/libs/application/application_po.h @@ -0,0 +1,36 @@ +#ifndef APPLICATION_PO_H +#define APPLICATION_PO_H + +/* + Copyright (C) 2026, The AROS Development Team. All rights reserved. + + Desc: The PrefsObject behind the opaque PrefsObject pointer. +*/ + +#include + +struct PObj +{ + ULONG po_Type; /* ALPOT_ */ + LONG po_Retain; + union + { + struct { STRPTR s; ULONG len; } str; + struct { ULONG type; LONG l; double d; } num; /* l for Long and Bool */ + struct { ULONG secs; ULONG micro; } date; /* since 1978 */ + struct { UBYTE *data; LONG size; } bin; + struct { struct PObj **items; STRPTR *keys; LONG count, cap; } con; + } u; +}; + +#define PO(x) ((struct PObj *)(x)) + +PrefsObject *po_New(ULONG type); +PrefsObject *po_Dispatch(ULONG want, PrefsObject *obj, ULONG *error, struct TagItem *tags); +void po_FormatLong(STRPTR buf, LONG v); +void po_FormatDouble(STRPTR buf, double d); +void po_FormatDate(STRPTR buf, ULONG secs); +BOOL po_ParseNumber(CONST_STRPTR s, LONG *l, double *d, BOOL *isdouble); +BOOL po_ParseDate(CONST_STRPTR s, ULONG *secs); + +#endif /* APPLICATION_PO_H */ diff --git a/workbench/libs/application/application_pofuncs.c b/workbench/libs/application/application_pofuncs.c new file mode 100644 index 00000000000..1a96be874a4 --- /dev/null +++ b/workbench/libs/application/application_pofuncs.c @@ -0,0 +1,658 @@ +/* + Copyright (C) 2026, The AROS Development Team. All rights reserved. + + Desc: PrefsObjects functions. +*/ + +#include +#include +#include +#include + +#include + +#include "application_intern.h" +#include "application_po.h" +#include LC_LIBDEFS_FILE + +/***************************************************************************** + + NAME */ +#include + + AROS_LH3(PrefsObject *, PrefsBaseObjectA, + +/* SYNOPSIS */ + AROS_LHA(PrefsObject *, obj, A0), + AROS_LHA(ULONG *, error, A1), + AROS_LHA(struct TagItem *, tags, A2), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 18, Application) + +/* FUNCTION + Run methods on any PrefsObject. Only the ALPO_ methods common to all + types apply; it cannot allocate. + + INPUTS + obj - the object, or NULL for the ALPO_Alloc methods. + error - NULL, or where to store the ALPOEF_ flags of the methods + that failed; 0 if all succeeded. + tags - the methods, run in order. Methods without a parameter + take 0. + + RESULT + obj, or the object an Alloc or ALPO_Copy method made; NULL after + ALPO_Release freed it. + + SEE ALSO + + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + (void)AppBase; + return po_Dispatch(ALPOT_None, obj, error, tags); + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH3(PrefsObject *, PrefsStringA, + +/* SYNOPSIS */ + AROS_LHA(PrefsObject *, obj, A0), + AROS_LHA(ULONG *, error, A1), + AROS_LHA(struct TagItem *, tags, A2), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 19, Application) + +/* FUNCTION + Run methods on a string: ALPO_ and ALPOSTR_ methods. + + INPUTS + obj - the object, or NULL for the ALPO_Alloc methods. + error - NULL, or where to store the ALPOEF_ flags of the methods + that failed; 0 if all succeeded. + tags - the methods, run in order. Methods without a parameter + take 0. + + RESULT + obj, or the object an Alloc or ALPO_Copy method made; NULL after + ALPO_Release freed it. + + SEE ALSO + + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + (void)AppBase; + return po_Dispatch(ALPOT_String, obj, error, tags); + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH3(PrefsObject *, PrefsNumberA, + +/* SYNOPSIS */ + AROS_LHA(PrefsObject *, obj, A0), + AROS_LHA(ULONG *, error, A1), + AROS_LHA(struct TagItem *, tags, A2), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 20, Application) + +/* FUNCTION + Run methods on a number (LONG, BOOL or double): ALPO_ and ALPONUM_ + methods. + + INPUTS + obj - the object, or NULL for the ALPO_Alloc methods. + error - NULL, or where to store the ALPOEF_ flags of the methods + that failed; 0 if all succeeded. + tags - the methods, run in order. Methods without a parameter + take 0. + + RESULT + obj, or the object an Alloc or ALPO_Copy method made; NULL after + ALPO_Release freed it. + + SEE ALSO + + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + (void)AppBase; + return po_Dispatch(ALPOT_Number, obj, error, tags); + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH3(PrefsObject *, PrefsDateA, + +/* SYNOPSIS */ + AROS_LHA(PrefsObject *, obj, A0), + AROS_LHA(ULONG *, error, A1), + AROS_LHA(struct TagItem *, tags, A2), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 21, Application) + +/* FUNCTION + Run methods on a date: ALPO_ and ALPODAT_ methods. + + INPUTS + obj - the object, or NULL for the ALPO_Alloc methods. + error - NULL, or where to store the ALPOEF_ flags of the methods + that failed; 0 if all succeeded. + tags - the methods, run in order. Methods without a parameter + take 0. + + RESULT + obj, or the object an Alloc or ALPO_Copy method made; NULL after + ALPO_Release freed it. + + SEE ALSO + + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + (void)AppBase; + return po_Dispatch(ALPOT_Date, obj, error, tags); + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH3(PrefsObject *, PrefsBinaryA, + +/* SYNOPSIS */ + AROS_LHA(PrefsObject *, obj, A0), + AROS_LHA(ULONG *, error, A1), + AROS_LHA(struct TagItem *, tags, A2), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 22, Application) + +/* FUNCTION + Run methods on binary data: ALPO_ and ALPOBIN_ methods. Prefer the + other types, which can be extended later. + + INPUTS + obj - the object, or NULL for the ALPO_Alloc methods. + error - NULL, or where to store the ALPOEF_ flags of the methods + that failed; 0 if all succeeded. + tags - the methods, run in order. Methods without a parameter + take 0. + + RESULT + obj, or the object an Alloc or ALPO_Copy method made; NULL after + ALPO_Release freed it. + + SEE ALSO + + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + (void)AppBase; + return po_Dispatch(ALPOT_Binary, obj, error, tags); + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH3(PrefsObject *, PrefsArrayA, + +/* SYNOPSIS */ + AROS_LHA(PrefsObject *, obj, A0), + AROS_LHA(ULONG *, error, A1), + AROS_LHA(struct TagItem *, tags, A2), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 23, Application) + +/* FUNCTION + Run methods on an array, objects by index: ALPO_ and ALPOARR_ + methods. + + INPUTS + obj - the object, or NULL for the ALPO_Alloc methods. + error - NULL, or where to store the ALPOEF_ flags of the methods + that failed; 0 if all succeeded. + tags - the methods, run in order. Methods without a parameter + take 0. + + RESULT + obj, or the object an Alloc or ALPO_Copy method made; NULL after + ALPO_Release freed it. + + NOTES + The Add, Insert and Replace methods take over the caller's + reference. + Storing the object already held at the index drops one reference only if + the object has more than one, since a borrowed pointer and a retained + one cannot be told apart: an object also held elsewhere must be + retained before it is stored over itself. + + SEE ALSO + + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + (void)AppBase; + return po_Dispatch(ALPOT_Array, obj, error, tags); + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH3(PrefsObject *, PrefsDictionaryA, + +/* SYNOPSIS */ + AROS_LHA(PrefsObject *, obj, A0), + AROS_LHA(ULONG *, error, A1), + AROS_LHA(struct TagItem *, tags, A2), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 24, Application) + +/* FUNCTION + Run methods on a dictionary, objects by key: ALPO_ and ALPODICT_ + methods. + + INPUTS + obj - the object, or NULL for the ALPO_Alloc methods. + error - NULL, or where to store the ALPOEF_ flags of the methods + that failed; 0 if all succeeded. + tags - the methods, run in order. Methods without a parameter + take 0. + + RESULT + obj, or the object an Alloc or ALPO_Copy method made; NULL after + ALPO_Release freed it. + + NOTES + The Set, Insert and Replace methods take over the caller's + reference. + Storing the object already held for key drops one reference only if + the object has more than one, since a borrowed pointer and a retained + one cannot be told apart: an object also held elsewhere must be + retained before it is stored over itself. + + SEE ALSO + + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + (void)AppBase; + return po_Dispatch(ALPOT_Dictionary, obj, error, tags); + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH2(PrefsObject *, DictGetObjectForKey, + +/* SYNOPSIS */ + AROS_LHA(PrefsObject *, dict, A0), + AROS_LHA(CONST_STRPTR, key, A1), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 25, Application) + +/* FUNCTION + The object stored in a dictionary for a key, or NULL. The + dictionary keeps owning it. + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + (void)AppBase; + if (po_Type(dict) != ALPOT_Dictionary) + return NULL; + return po_DictGet(dict, key); + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH3(BOOL, DictGetBoolForKey, + +/* SYNOPSIS */ + AROS_LHA(PrefsObject *, dict, A0), + AROS_LHA(CONST_STRPTR, key, A1), + AROS_LHA(BOOL, defBool, D0), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 26, Application) + +/* FUNCTION + The number stored for a key as a BOOL, or defBool if dict is NULL + or holds no number for key. + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + struct PObj *o; + + (void)AppBase; + if (po_Type(dict) != ALPOT_Dictionary || po_Type((PrefsObject *)(o = PO(po_DictGet(dict, key)))) != ALPOT_Number) + return defBool; + return (o->u.num.type == ALPONUMT_Double) ? (o->u.num.d != 0) : (o->u.num.l != 0); + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH3(LONG, DictGetIntegerForKey, + +/* SYNOPSIS */ + AROS_LHA(PrefsObject *, dict, A0), + AROS_LHA(CONST_STRPTR, key, A1), + AROS_LHA(LONG, defInt, D0), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 27, Application) + +/* FUNCTION + The number stored for a key as a LONG, or defInt if dict is NULL + or holds no number for key. + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + (void)AppBase; + if (po_Type(dict) != ALPOT_Dictionary) + return defInt; + return po_DictLong(dict, key, defInt); + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH3(CONST_STRPTR, DictGetStringForKey, + +/* SYNOPSIS */ + AROS_LHA(PrefsObject *, dict, A0), + AROS_LHA(CONST_STRPTR, key, A1), + AROS_LHA(CONST_STRPTR, defStr, A2), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 28, Application) + +/* FUNCTION + The string stored for a key, or defStr if dict is NULL or holds no + string for key. The string belongs to the dictionary. + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + (void)AppBase; + if (po_Type(dict) != ALPOT_Dictionary) + return defStr; + return po_DictString(dict, key, defStr); + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH4(LONG, DictGetOptionForKey, + +/* SYNOPSIS */ + AROS_LHA(PrefsObject *, dict, A0), + AROS_LHA(CONST_STRPTR, key, A1), + AROS_LHA(CONST_STRPTR *, optionsTable, A2), + AROS_LHA(LONG, defaultOption, D0), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 29, Application) + +/* FUNCTION + Look the string stored for key up in a NULL-terminated table, + ignoring case. + + RESULT + Its index in the table, or defaultOption if dict, the table or the + string is missing or the string is not in the table. + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + CONST_STRPTR s; + LONG i; + + (void)AppBase; + if (!optionsTable || po_Type(dict) != ALPOT_Dictionary || !(s = po_DictString(dict, key, NULL))) + return defaultOption; + for (i = 0; optionsTable[i]; i++) + { + if (Stricmp(optionsTable[i], s) == 0) + return i; + } + return defaultOption; + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH3(BOOL, DictSetObjectForKey, + +/* SYNOPSIS */ + AROS_LHA(PrefsObject *, dict, A0), + AROS_LHA(PrefsObject *, obj, A1), + AROS_LHA(CONST_STRPTR, key, A2), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 30, Application) + +/* FUNCTION + Store obj in the dictionary for key, releasing what was stored for + key before. The dictionary takes over the caller's reference. + + RESULT + FALSE if dict, obj or key is NULL or memory ran out; obj then + still belongs to the caller. + + NOTES + Storing the object already held for key drops one reference only if + the object has more than one, since a borrowed pointer and a retained + one cannot be told apart: an object also held elsewhere must be + retained before it is stored over itself. + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + (void)AppBase; + if (po_Type(dict) != ALPOT_Dictionary) + return FALSE; + return po_DictSet(dict, key, obj); + + AROS_LIBFUNC_EXIT +} + +/* The prefs file of an application, or NULL. */ +static STRPTR pf_AppFile(struct ApplicationBase *AppBase, ULONG appID, STRPTR buf) +{ + struct AppEntry *ae; + STRPTR name = NULL; + + ObtainSemaphore(&AppBase->ab_Lock); + if ((ae = app_Find(AppBase, appID)) && ae->ae_PrefsName) + { + strncpy(buf, ae->ae_PrefsName, APP_PATHMAX - 1); + buf[APP_PATHMAX - 1] = '\0'; + name = buf; + } + ReleaseSemaphore(&AppBase->ab_Lock); + return name; +} + +/***************************************************************************** + + NAME */ + AROS_LH2(ULONG, ReadPrefsA, + +/* SYNOPSIS */ + AROS_LHA(PrefsObject *, dict, A0), + AROS_LHA(struct TagItem *, tags, A1), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 31, Application) + +/* FUNCTION + Empty a dictionary and fill it from a prefs file or a stream. + + INPUTS + dict - the dictionary. + tags - READPREFS_AppID (that application's prefs file), + READPREFS_FileName (a name below ENV:, or a full path) or + READPREFS_GetProcInfo (a stream). A file is read from ENV:, + or from ENVARC: if that fails; READPREFS_ReadENV and + READPREFS_ReadENVARC switch either off. + + RESULT + 0, or ALPOEF_ flags. dict is left empty on failure. + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + struct ALPOGetProcInfo *pi = (struct ALPOGetProcInfo *)GetTagData(READPREFS_GetProcInfo, 0, tags); + CONST_STRPTR name = (CONST_STRPTR)GetTagData(READPREFS_FileName, 0, tags); + ULONG appID = GetTagData(READPREFS_AppID, 0, tags); + TEXT buf[APP_PATHMAX]; + + if (pi) + return xml_ReadStream(dict, pi); + if (!name && appID && !(name = pf_AppFile(AppBase, appID, buf))) + return ALPOEF_NOTFOUND; + return prefs_Read(AppBase, dict, name, + GetTagData(READPREFS_ReadENV, TRUE, tags), + GetTagData(READPREFS_ReadENVARC, TRUE, tags)); + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH2(ULONG, WritePrefsA, + +/* SYNOPSIS */ + AROS_LHA(PrefsObject *, dict, A0), + AROS_LHA(struct TagItem *, tags, A1), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 32, Application) + +/* FUNCTION + Write a dictionary to a prefs file or a stream. + + INPUTS + dict - the dictionary. + tags - WRITEPREFS_AppID, WRITEPREFS_FileName or + WRITEPREFS_PutProcInfo, as for ReadPrefsA(). A file is + written to ENV: (WRITEPREFS_WriteENV, default TRUE) and to + ENVARC: (WRITEPREFS_WriteENVARC, default FALSE). + + RESULT + 0, or ALPOEF_ flags. + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + struct ALPOPutProcInfo *pi = (struct ALPOPutProcInfo *)GetTagData(WRITEPREFS_PutProcInfo, 0, tags); + CONST_STRPTR name = (CONST_STRPTR)GetTagData(WRITEPREFS_FileName, 0, tags); + ULONG appID = GetTagData(WRITEPREFS_AppID, 0, tags); + TEXT buf[APP_PATHMAX]; + + if (pi) + return xml_WriteStream(dict, pi); + if (!name && appID && !(name = pf_AppFile(AppBase, appID, buf))) + return ALPOEF_NOTFOUND; + return prefs_Write(AppBase, dict, name, + GetTagData(WRITEPREFS_WriteENV, TRUE, tags), + GetTagData(WRITEPREFS_WriteENVARC, FALSE, tags)); + + AROS_LIBFUNC_EXIT +} + +/***************************************************************************** + + NAME */ + AROS_LH1(ULONG, BeginDeserialization, + +/* SYNOPSIS */ + AROS_LHA(struct ALPOGetProcInfo *, info, A0), + +/* LOCATION */ + struct ApplicationBase *, AppBase, 33, Application) + +/* FUNCTION + Read a stream up to its first object's open tag, for reading + PrefsObjects XML by hand: allocate an object of the type returned + and ALPO_Deserialize it with the same info. + + INPUTS + info - getChProc, unGetChProc, encoding, and key_otag pointing to + TAGBUFFER_SIZE bytes. + + RESULT + The ALPOT_ type of the first object, or ALPOT_None. + +*****************************************************************************/ +{ + AROS_LIBFUNC_INIT + + (void)AppBase; + return xml_Begin(info); + + AROS_LIBFUNC_EXIT +} diff --git a/workbench/libs/application/application_prefs.c b/workbench/libs/application/application_prefs.c new file mode 100644 index 00000000000..3a5576a615f --- /dev/null +++ b/workbench/libs/application/application_prefs.c @@ -0,0 +1,1434 @@ +/* + Copyright (C) 2026, The AROS Development Team. All rights reserved. + + Desc: PrefsObjects - strings, numbers, dates, binary data, arrays and + dictionaries, reference counted, for application preferences. +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "application_intern.h" +#include "application_po.h" +#include LC_LIBDEFS_FILE + +/* ---------------------------------------------------------------------- */ +/* Objects */ + +static struct PObj *po_Alloc(ULONG type) +{ + struct PObj *o; + + if (!(o = AllocVec(sizeof(*o), MEMF_ANY | MEMF_CLEAR))) + return NULL; + o->po_Type = type; + o->po_Retain = 1; + if (type == ALPOT_String) + { + if (!(o->u.str.s = AllocVec(1, MEMF_ANY | MEMF_CLEAR))) + { + FreeVec(o); + return NULL; + } + } + return o; +} + +static void po_FreeContents(struct PObj *o) +{ + LONG i; + + switch (o->po_Type) + { + case ALPOT_String: + FreeVec(o->u.str.s); + o->u.str.s = NULL; + o->u.str.len = 0; + break; + case ALPOT_Binary: + FreeVec(o->u.bin.data); + o->u.bin.data = NULL; + o->u.bin.size = 0; + break; + case ALPOT_Array: + case ALPOT_Dictionary: + for (i = 0; i < o->u.con.count; i++) + { + po_Release((PrefsObject *)o->u.con.items[i]); + if (o->u.con.keys) + FreeVec(o->u.con.keys[i]); + } + o->u.con.count = 0; + break; + } +} + +void po_Release(PrefsObject *obj) +{ + struct PObj *o = PO(obj); + + if (!o || --o->po_Retain > 0) + return; + po_FreeContents(o); + if (o->po_Type == ALPOT_Array || o->po_Type == ALPOT_Dictionary) + { + FreeVec(o->u.con.items); + FreeVec(o->u.con.keys); + } + FreeVec(o); +} + +ULONG po_Type(PrefsObject *obj) +{ + return obj ? PO(obj)->po_Type : ALPOT_None; +} + +static BOOL po_IsContainer(struct PObj *o) +{ + return o && (o->po_Type == ALPOT_Array || o->po_Type == ALPOT_Dictionary); +} + +LONG po_Count(PrefsObject *obj) +{ + return po_IsContainer(PO(obj)) ? PO(obj)->u.con.count : 0; +} + +PrefsObject *po_At(PrefsObject *obj, LONG index) +{ + struct PObj *o = PO(obj); + + if (!po_IsContainer(o) || index < 0 || index >= o->u.con.count) + return NULL; + return (PrefsObject *)o->u.con.items[index]; +} + +static BOOL po_Grow(struct PObj *o) +{ + struct PObj **items; + STRPTR *keys = NULL; + LONG cap; + + if (o->u.con.count < o->u.con.cap) + return TRUE; + cap = o->u.con.cap ? o->u.con.cap * 2 : 8; + if (!(items = AllocVec(cap * sizeof(*items), MEMF_ANY))) + return FALSE; + if (o->po_Type == ALPOT_Dictionary && !(keys = AllocVec(cap * sizeof(*keys), MEMF_ANY))) + { + FreeVec(items); + return FALSE; + } + if (o->u.con.count) + { + CopyMem(o->u.con.items, items, o->u.con.count * sizeof(*items)); + if (keys) + CopyMem(o->u.con.keys, keys, o->u.con.count * sizeof(*keys)); + } + FreeVec(o->u.con.items); + FreeVec(o->u.con.keys); + o->u.con.items = items; + o->u.con.keys = keys; + o->u.con.cap = cap; + return TRUE; +} + +/* Takes over the caller's reference to item; key is copied. */ +static ULONG po_Insert(struct PObj *o, LONG index, struct PObj *item, CONST_STRPTR key) +{ + STRPTR kcopy = NULL; + LONG i; + + if (!item) + return ALPOEF_MISSING_OBJECT; + if (index < 0 || index > o->u.con.count) + return ALPOEF_ILLEGAL_PARAMETER; + if (o->po_Type == ALPOT_Dictionary) + { + if (!key) + return ALPOEF_ILLEGAL_PARAMETER; + if (!(kcopy = app_StrDup(key))) + return ALPOEF_MEMORY; + } + if (!po_Grow(o)) + { + FreeVec(kcopy); + return ALPOEF_MEMORY; + } + for (i = o->u.con.count; i > index; i--) + { + o->u.con.items[i] = o->u.con.items[i - 1]; + if (o->u.con.keys) + o->u.con.keys[i] = o->u.con.keys[i - 1]; + } + o->u.con.items[index] = item; + if (o->u.con.keys) + o->u.con.keys[index] = kcopy; + o->u.con.count++; + return 0; +} + +/* Slot index takes over the caller's reference to item. The object already + there may be item itself: then the extra reference is dropped, unless it + is the container's only one, which a caller passing a borrowed pointer + did not have. */ +static void po_Replace(struct PObj *o, LONG index, struct PObj *item) +{ + if (o->u.con.items[index] == item) + { + if (item->po_Retain > 1) + item->po_Retain--; + return; + } + po_Release((PrefsObject *)o->u.con.items[index]); + o->u.con.items[index] = item; +} + +static void po_RemoveAt(struct PObj *o, LONG index) +{ + LONG i; + + po_Release((PrefsObject *)o->u.con.items[index]); + if (o->u.con.keys) + FreeVec(o->u.con.keys[index]); + for (i = index; i < o->u.con.count - 1; i++) + { + o->u.con.items[i] = o->u.con.items[i + 1]; + if (o->u.con.keys) + o->u.con.keys[i] = o->u.con.keys[i + 1]; + } + o->u.con.count--; +} + +static LONG po_KeyIndex(struct PObj *o, CONST_STRPTR key) +{ + LONG i; + + if (!key || o->po_Type != ALPOT_Dictionary) + return -1; + for (i = 0; i < o->u.con.count; i++) + { + if (strcmp(o->u.con.keys[i], key) == 0) + return i; + } + return -1; +} + +static LONG po_ObjIndex(struct PObj *o, struct PObj *item) +{ + LONG i; + + for (i = 0; i < o->u.con.count; i++) + { + if (o->u.con.items[i] == item) + return i; + } + return -1; +} + +static ULONG po_SetString(struct PObj *o, CONST_STRPTR s, LONG len) +{ + STRPTR copy; + + if (!s) + s = ""; + if (len < 0) + len = strlen(s); + if (!(copy = AllocVec(len + 1, MEMF_ANY))) + return ALPOEF_MEMORY; + CopyMem((APTR)s, copy, len); + copy[len] = '\0'; + FreeVec(o->u.str.s); + o->u.str.s = copy; + o->u.str.len = len; + return 0; +} + +static ULONG po_SetBinary(struct PObj *o, CONST UBYTE *data, LONG size) +{ + UBYTE *copy = NULL; + + if (size < 0) + return ALPOEF_ILLEGAL_PARAMETER; + if (size && !(copy = AllocVec(size, MEMF_ANY | MEMF_CLEAR))) + return ALPOEF_MEMORY; + if (copy && data) + CopyMem((APTR)data, copy, size); + FreeVec(o->u.bin.data); + o->u.bin.data = copy; + o->u.bin.size = size; + return 0; +} + +PrefsObject *po_Copy(PrefsObject *obj) +{ + struct PObj *o = PO(obj), *c; + LONG i; + + if (!o || !(c = po_Alloc(o->po_Type))) + return NULL; + + switch (o->po_Type) + { + case ALPOT_String: + if (po_SetString(c, o->u.str.s, o->u.str.len)) + goto fail; + break; + case ALPOT_Number: + c->u.num = o->u.num; + break; + case ALPOT_Date: + c->u.date = o->u.date; + break; + case ALPOT_Binary: + if (po_SetBinary(c, o->u.bin.data, o->u.bin.size)) + goto fail; + break; + case ALPOT_Array: + case ALPOT_Dictionary: + for (i = 0; i < o->u.con.count; i++) + { + struct PObj *item = PO(po_Copy((PrefsObject *)o->u.con.items[i])); + + if (!item) + goto fail; + if (po_Insert(c, i, item, o->u.con.keys ? o->u.con.keys[i] : NULL)) + { + po_Release((PrefsObject *)item); + goto fail; + } + } + break; + } + return (PrefsObject *)c; + +fail: + po_Release((PrefsObject *)c); + return NULL; +} + +/* ---------------------------------------------------------------------- */ +/* Helpers used inside the library */ + +PrefsObject *po_New(ULONG type) { return (PrefsObject *)po_Alloc(type); } +PrefsObject *po_NewDict(void) { return (PrefsObject *)po_Alloc(ALPOT_Dictionary); } +PrefsObject *po_NewArray(void) { return (PrefsObject *)po_Alloc(ALPOT_Array); } + +PrefsObject *po_NewString(CONST_STRPTR s) +{ + struct PObj *o = po_Alloc(ALPOT_String); + + if (o && po_SetString(o, s, -1)) + { + po_Release((PrefsObject *)o); + o = NULL; + } + return (PrefsObject *)o; +} + +PrefsObject *po_NewLong(LONG v) +{ + struct PObj *o = po_Alloc(ALPOT_Number); + + if (o) + { + o->u.num.type = ALPONUMT_Long; + o->u.num.l = v; + } + return (PrefsObject *)o; +} + +static void po_SetNow(struct PObj *o) +{ + struct DateStamp ds; + + DateStamp(&ds); + o->u.date.secs = ds.ds_Days * 86400 + ds.ds_Minute * 60 + ds.ds_Tick / TICKS_PER_SECOND; + o->u.date.micro = (ds.ds_Tick % TICKS_PER_SECOND) * (1000000 / TICKS_PER_SECOND); +} + +PrefsObject *po_NewNow(void) +{ + struct PObj *o = po_Alloc(ALPOT_Date); + + if (o) + po_SetNow(o); + return (PrefsObject *)o; +} + +BOOL po_ArrayInsert(PrefsObject *arr, PrefsObject *item, LONG index) +{ + struct PObj *o = PO(arr); + + if (!o || o->po_Type != ALPOT_Array) + return FALSE; + if (index < 0 || index > o->u.con.count) + index = o->u.con.count; + return po_Insert(o, index, PO(item), NULL) == 0; +} + +void po_ArrayRemove(PrefsObject *arr, LONG index) +{ + struct PObj *o = PO(arr); + + if (po_IsContainer(o) && index >= 0 && index < o->u.con.count) + po_RemoveAt(o, index); +} + +void po_Clear(PrefsObject *obj) +{ + if (po_IsContainer(PO(obj))) + po_FreeContents(PO(obj)); +} + +BOOL po_DictSet(PrefsObject *dict, CONST_STRPTR key, PrefsObject *item) +{ + struct PObj *o = PO(dict); + LONG i; + + if (!o || o->po_Type != ALPOT_Dictionary || !key || !item) + return FALSE; + if ((i = po_KeyIndex(o, key)) >= 0) + { + po_Replace(o, i, PO(item)); + return TRUE; + } + return po_Insert(o, o->u.con.count, PO(item), key) == 0; +} + +PrefsObject *po_DictGet(PrefsObject *dict, CONST_STRPTR key) +{ + struct PObj *o = PO(dict); + LONG i; + + if (!o || (i = po_KeyIndex(o, key)) < 0) + return NULL; + return (PrefsObject *)o->u.con.items[i]; +} + +CONST_STRPTR po_DictString(PrefsObject *dict, CONST_STRPTR key, CONST_STRPTR def) +{ + struct PObj *o = PO(po_DictGet(dict, key)); + + return (o && o->po_Type == ALPOT_String) ? o->u.str.s : def; +} + +/* a double as a LONG, saturated instead of undefined out of range */ +static LONG po_DoubleToLong(double d) +{ + if (d >= 2147483647.0) + return 0x7fffffff; + if (d <= -2147483648.0) + return (LONG)0x80000000; + return (LONG)d; +} + +static BOOL po_NumberLong(struct PObj *o, LONG *v) +{ + if (!o || o->po_Type != ALPOT_Number) + return FALSE; + *v = (o->u.num.type == ALPONUMT_Double) ? po_DoubleToLong(o->u.num.d) : o->u.num.l; + return TRUE; +} + +LONG po_DictLong(PrefsObject *dict, CONST_STRPTR key, LONG def) +{ + LONG v; + + return po_NumberLong(PO(po_DictGet(dict, key)), &v) ? v : def; +} + +/* Swap the contents of two dictionaries, then empty 'from'. */ +BOOL po_MoveContents(PrefsObject *to, PrefsObject *from) +{ + struct PObj *t = PO(to), *f = PO(from), tmp; + + if (!t || !f || t->po_Type != f->po_Type || !po_IsContainer(t)) + return FALSE; + tmp.u.con = t->u.con; + t->u.con = f->u.con; + f->u.con = tmp.u.con; + po_FreeContents(f); + return TRUE; +} + +/* ---------------------------------------------------------------------- */ +/* Numbers and dates as text */ + +void po_FormatLong(STRPTR buf, LONG v) +{ + TEXT tmp[16], *p = tmp + sizeof(tmp) - 1; + ULONG u = v < 0 ? -(ULONG)v : (ULONG)v; + + *p = '\0'; + do + *--p = '0' + (u % 10); + while (u /= 10); + if (v < 0) + *--p = '-'; + strcpy(buf, p); +} + +#define PO_DBL_MAX 1.7976931348623157e308 +#define PO_EXP_MAX 400 /* beyond any double, after the digits */ + +/* v times 10^e in the widest type there is: with the x87's 64-bit + mantissa a 19-digit integer and the powers up to 1e27 are exact, so the + result rounds once; elsewhere long double may be a plain double. */ +static long double po_Scale(long double v, LONG e) +{ + static const long double p10[] = { 1e0L, 1e1L, 1e2L, 1e3L, 1e4L, 1e5L, 1e6L, 1e7L, 1e8L, + 1e9L, 1e10L, 1e11L, 1e12L, 1e13L, 1e14L, 1e15L, 1e16L, + 1e17L, 1e18L, 1e19L, 1e20L, 1e21L, 1e22L, 1e23L, 1e24L, + 1e25L, 1e26L, 1e27L }; + + while (e > 27) + { + v *= 1e27L; + e -= 27; + } + while (e < -27) + { + v /= 1e27L; + e += 27; + } + return e < 0 ? v / p10[-e] : v * p10[e]; +} + +static CONST_STRPTR po_SkipSpace(CONST_STRPTR s) +{ + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') + s++; + return s; +} + +/* A decimal number, optionally with a fraction and exponent. Up to 19 + digits are gathered exactly in an integer and scaled once; an exponent + beyond any double is clamped, so the value is finite and the loop + short. */ +BOOL po_ParseNumber(CONST_STRPTR s, LONG *l, double *d, BOOL *isdouble) +{ + UQUAD q = 0; + double v; + BOOL neg = FALSE, any = FALSE, frac = FALSE; + LONG ex = 0, shift = 0, digits = 0; + + s = po_SkipSpace(s); + if (*s == '-' || *s == '+') + neg = (*s++ == '-'); + for (;;) + { + if (*s == '.' && !frac) + { + frac = TRUE; + s++; + continue; + } + if (*s < '0' || *s > '9') + break; + any = TRUE; + if (digits < 19) + { + q = q * 10 + (*s - '0'); + if (q) + digits++; + if (frac) + shift--; + } + else if (!frac) + shift++; + s++; + } + if (any && (*s == 'e' || *s == 'E')) + { + BOOL exneg = FALSE; + + s++; + frac = TRUE; + if (*s == '-' || *s == '+') + exneg = (*s++ == '-'); + while (*s >= '0' && *s <= '9') + { + if (ex < PO_EXP_MAX) + ex = ex * 10 + (*s - '0'); + s++; + } + if (exneg) + ex = -ex; + } + if (!any || *po_SkipSpace(s)) + return FALSE; + ex += shift; + if (ex > PO_EXP_MAX) + ex = PO_EXP_MAX; + if (ex < -PO_EXP_MAX) + ex = -PO_EXP_MAX; + v = q ? (double)po_Scale((long double)q, ex) : 0; + if (v > PO_DBL_MAX) + v = PO_DBL_MAX; + if (neg) + v = -v; + *d = v; + *l = po_DoubleToLong(v); + *isdouble = frac; + return TRUE; +} + +/* digits[0..n) with the decimal point after position e+1 (plain notation + for -5 <= e < 17, else d.ddde), as po_ParseNumber() reads it; a + whole number keeps a ".0" so that the text stays a real */ +static void po_PutDigits(STRPTR buf, const TEXT *digits, LONG n, LONG e) +{ + LONG i, point; + + if (e >= -5 && e < 17) + { + point = e + 1; + if (e < 0) + { + *buf++ = '0'; + *buf++ = '.'; + for (i = -1; i > e; i--) + *buf++ = '0'; + point = 0; + } + else + { + for (i = 0; i < point; i++) + *buf++ = i < n ? digits[i] : '0'; + *buf++ = '.'; + if (n <= point) + *buf++ = '0'; + } + for (i = point; i < n; i++) + *buf++ = digits[i]; + *buf = '\0'; + return; + } + *buf++ = digits[0]; + if (n > 1) + { + *buf++ = '.'; + for (i = 1; i < n; i++) + *buf++ = digits[i]; + } + *buf++ = 'e'; + po_FormatLong(buf, e); +} + +/* The shortest of 15, 16 and 17 significant digits that po_ParseNumber() + reads back as the same double, then the 17-digit neighbours; the plain + 17 digits when none does. */ +void po_FormatDouble(STRPTR buf, double d) +{ + static const LONG step[] = { 0, 1, -1, 2, -2 }; + UQUAD q, r; + LONG e = 0, ee, n, i, prec, try; + TEXT digits[18]; + STRPTR out = buf; + double m, back; + BOOL isd; + LONG dummy; + + if (d != d) + { + strcpy(buf, "0"); + return; + } + if (d < 0) + { + *out++ = '-'; + d = -d; + } + if (d > PO_DBL_MAX) + d = PO_DBL_MAX; + if (d == 0) + { + strcpy(buf, "0"); + return; + } + m = d; + while (m >= 10) + { + m = (double)po_Scale(m, -1); + e++; + } + while (m < 1) + { + m = (double)po_Scale(m, 1); + e--; + } + q = (UQUAD)(po_Scale(d, 16 - e) + 0.5L); + if (q >= (UQUAD)100000000000000000ULL) + { + q /= 10; + e++; + } + else if (q < (UQUAD)10000000000000000ULL) + { + q *= 10; + e--; + } + for (try = 0; try < 7; try++) + { + UQUAD div = 1; + + /* 15, 16 and 17 digits, then 17 digits one or two units off */ + prec = try < 2 ? 15 + try : 17; + ee = e; + for (i = prec; i < 17; i++) + div *= 10; + r = (q + div / 2) / div; + for (i = prec; i < 17; i++) + r *= 10; + if (try >= 2) + r += step[try - 2]; + if (r >= (UQUAD)100000000000000000ULL) + { + r /= 10; + ee++; + } + else if (r < (UQUAD)10000000000000000ULL) + { + r *= 10; + ee--; + } + for (i = 16; i >= 0; i--) + { + digits[i] = '0' + (r % 10); + r /= 10; + } + for (n = 17; n > 1 && digits[n - 1] == '0'; n--) + ; + po_PutDigits(out, digits, n, ee); + if (po_ParseNumber(out, &dummy, &back, &isd) && back == d) + return; + } + /* none read back exactly: the plain 17 digits */ + for (r = q, i = 16; i >= 0; i--) + { + digits[i] = '0' + (r % 10); + r /= 10; + } + po_PutDigits(out, digits, 17, e); +} + +/* "YYYY-MM-DD HH:MM:SS", local time like the rest of AmigaDOS */ +void po_FormatDate(STRPTR buf, ULONG secs) +{ + struct ClockData cd; + ULONG v[6]; + LONG i, w[6] = { 4, 2, 2, 2, 2, 2 }; + TEXT sep[6] = { '-', '-', ' ', ':', ':', '\0' }; + + Amiga2Date(secs, &cd); + v[0] = cd.year; v[1] = cd.month; v[2] = cd.mday; + v[3] = cd.hour; v[4] = cd.min; v[5] = cd.sec; + for (i = 0; i < 6; i++) + { + LONG j; + + for (j = w[i] - 1; j >= 0; j--) + { + buf[j] = '0' + v[i] % 10; + v[i] /= 10; + } + buf += w[i]; + *buf++ = sep[i]; + } +} + +BOOL po_ParseDate(CONST_STRPTR s, ULONG *secs) +{ + struct ClockData cd; + ULONG v[6] = { 0 }; + LONG i, n; + + s = po_SkipSpace(s); + for (i = 0; i < 6; i++) + { + for (n = 0; *s >= '0' && *s <= '9'; n++) + v[i] = v[i] * 10 + (*s++ - '0'); + if (!n) + break; + if (*s == '-' || *s == ':' || *s == ' ' || *s == 'T') + s++; + } + if (i == 1) + { + /* a bare number: seconds since 1978 */ + *secs = v[0]; + return TRUE; + } + if (i < 3) + return FALSE; + cd.year = v[0]; cd.month = v[1]; cd.mday = v[2]; + cd.hour = v[3]; cd.min = v[4]; cd.sec = v[5]; + cd.wday = 0; + if (!(*secs = CheckDate(&cd)) && !(cd.year == 1978 && cd.month == 1 && cd.mday == 1)) + return FALSE; + *secs = Date2Amiga(&cd); + return TRUE; +} + +/* ---------------------------------------------------------------------- */ +/* Methods */ + +static ULONG po_SetWithString(struct PObj *o, CONST_STRPTR s) +{ + LONG l; + double d; + BOOL isd; + ULONG secs; + + if (!s) + return ALPOEF_ILLEGAL_PARAMETER; + switch (o->po_Type) + { + case ALPOT_String: + return po_SetString(o, s, -1); + case ALPOT_Number: + if (Stricmp(s, "TRUE") == 0 || Stricmp(s, "FALSE") == 0) + { + o->u.num.type = ALPONUMT_Bool; + o->u.num.l = (Stricmp(s, "TRUE") == 0); + return 0; + } + if (!po_ParseNumber(s, &l, &d, &isd)) + return ALPOEF_ILLEGAL_PARAMETER; + o->u.num.type = isd ? ALPONUMT_Double : ALPONUMT_Long; + o->u.num.l = l; + o->u.num.d = d; + return 0; + case ALPOT_Date: + if (!po_ParseDate(s, &secs)) + return ALPOEF_ILLEGAL_PARAMETER; + o->u.date.secs = secs; + o->u.date.micro = 0; + return 0; + } + return ALPOEF_IMPOSSIBLE_OP; +} + +static ULONG po_GetAsString(struct PObj *o, struct ALPOString *as) +{ + TEXT buf[40]; + CONST_STRPTR src = buf; + ULONG len; + + if (!as || !as->string || !as->length) + return ALPOEF_ILLEGAL_PARAMETER; + switch (o->po_Type) + { + case ALPOT_String: + src = o->u.str.s; + break; + case ALPOT_Number: + if (o->u.num.type == ALPONUMT_Bool) + strcpy(buf, o->u.num.l ? "TRUE" : "FALSE"); + else if (o->u.num.type == ALPONUMT_Double) + po_FormatDouble(buf, o->u.num.d); + else + po_FormatLong(buf, o->u.num.l); + break; + case ALPOT_Date: + po_FormatDate(buf, o->u.date.secs); + break; + default: + return ALPOEF_IMPOSSIBLE_OP; + } + len = strlen(src); + if (len > as->length - 1) + len = as->length - 1; + CopyMem((APTR)src, as->string, len); + as->string[len] = '\0'; + as->length = len; + return 0; +} + +static ULONG po_StringMethod(struct PObj *o, struct TagItem *tag) +{ + struct ALPOUniString *us = (struct ALPOUniString *)tag->ti_Data; + ULONG i, err; + STRPTR tmp; + + switch (tag->ti_Tag) + { + case ALPOSTR_Clear: + return po_SetString(o, "", 0); + case ALPOSTR_GetLength: + if (tag->ti_Data) + *(LONG *)tag->ti_Data = o->u.str.len; + return 0; + case ALPOSTR_SetString: + return po_SetString(o, (CONST_STRPTR)tag->ti_Data, -1); + case ALPOSTR_GetString: + if (tag->ti_Data) + *(STRPTR *)tag->ti_Data = o->u.str.s; + return 0; + case ALPOSTR_SetUniString: + /* Kept as 8 bits per character; others become '?'. */ + if (!us || (!us->string && us->length)) + return ALPOEF_ILLEGAL_PARAMETER; + if (!(tmp = AllocVec(us->length + 1, MEMF_ANY))) + return ALPOEF_MEMORY; + for (i = 0; i < us->length; i++) + tmp[i] = us->string[i] < 256 ? us->string[i] : '?'; + err = po_SetString(o, tmp, us->length); + FreeVec(tmp); + return err; + case ALPOSTR_GetUniString: + if (!us || !us->string) + return ALPOEF_ILLEGAL_PARAMETER; + for (i = 0; i < o->u.str.len && i < us->length; i++) + us->string[i] = (UBYTE)o->u.str.s[i]; + us->length = i; + return 0; + } + return ALPOEF_UNKNOWN_METHOD; +} + +static ULONG po_NumberMethod(struct PObj *o, struct TagItem *tag) +{ + IPTR v = tag->ti_Data; + + switch (tag->ti_Tag) + { + case ALPONUM_GetType: + if (v) + *(LONG *)v = o->u.num.type; + return 0; + case ALPONUM_ConvertToType: + if (v == ALPONUMT_Double && o->u.num.type != ALPONUMT_Double) + o->u.num.d = o->u.num.l; + else if (v == ALPONUMT_Long && o->u.num.type == ALPONUMT_Double) + o->u.num.l = po_DoubleToLong(o->u.num.d); + else if (v == ALPONUMT_Bool) + o->u.num.l = (o->u.num.type == ALPONUMT_Double) ? (o->u.num.d != 0) : (o->u.num.l != 0); + else if (v != ALPONUMT_Long && v != ALPONUMT_Double) + return ALPOEF_ILLEGAL_PARAMETER; + o->u.num.type = v; + return 0; + case ALPONUM_SetLong: + o->u.num.type = ALPONUMT_Long; + o->u.num.l = (LONG)v; + return 0; + case ALPONUM_SetBool: + o->u.num.type = ALPONUMT_Bool; + o->u.num.l = v ? TRUE : FALSE; + return 0; + case ALPONUM_SetDouble: + if (!v) + return ALPOEF_ILLEGAL_PARAMETER; + o->u.num.type = ALPONUMT_Double; + o->u.num.d = *(double *)v; + return 0; + case ALPONUM_GetLong: + if (v) + po_NumberLong(o, (LONG *)v); + return 0; + case ALPONUM_GetBool: + if (v) + *(BOOL *)v = (o->u.num.type == ALPONUMT_Double) ? (o->u.num.d != 0) : (o->u.num.l != 0); + return 0; + case ALPONUM_GetDouble: + if (v) + *(double *)v = (o->u.num.type == ALPONUMT_Double) ? o->u.num.d : (double)o->u.num.l; + return 0; + } + return ALPOEF_UNKNOWN_METHOD; +} + +static ULONG po_DateMethod(struct PObj *o, struct TagItem *tag) +{ + APTR v = (APTR)tag->ti_Data; + + if (tag->ti_Tag != ALPODAT_SetCurrentTime && tag->ti_Tag != ALPODAT_SetSecsSince1978 && !v) + return ALPOEF_ILLEGAL_PARAMETER; + + switch (tag->ti_Tag) + { + case ALPODAT_SetCurrentTime: + po_SetNow(o); + return 0; + case ALPODAT_SetTimeVal: + o->u.date.secs = ((struct timeval *)v)->tv_secs; + o->u.date.micro = ((struct timeval *)v)->tv_micro; + return 0; + case ALPODAT_GetTimeVal: + ((struct timeval *)v)->tv_secs = o->u.date.secs; + ((struct timeval *)v)->tv_micro = o->u.date.micro; + return 0; + case ALPODAT_SetSecsSince1978: + o->u.date.secs = (ULONG)tag->ti_Data; + o->u.date.micro = 0; + return 0; + case ALPODAT_GetSecsSince1978: + *(ULONG *)v = o->u.date.secs; + return 0; + case ALPODAT_SetClockData: + o->u.date.secs = Date2Amiga((struct ClockData *)v); + o->u.date.micro = 0; + return 0; + case ALPODAT_GetClockData: + Amiga2Date(o->u.date.secs, (struct ClockData *)v); + return 0; + case ALPODAT_SetDateStamp: + o->u.date.secs = ((struct DateStamp *)v)->ds_Days * 86400 + + ((struct DateStamp *)v)->ds_Minute * 60 + + ((struct DateStamp *)v)->ds_Tick / TICKS_PER_SECOND; + o->u.date.micro = (((struct DateStamp *)v)->ds_Tick % TICKS_PER_SECOND) + * (1000000 / TICKS_PER_SECOND); + return 0; + case ALPODAT_GetDateStamp: + ((struct DateStamp *)v)->ds_Days = o->u.date.secs / 86400; + ((struct DateStamp *)v)->ds_Minute = (o->u.date.secs % 86400) / 60; + ((struct DateStamp *)v)->ds_Tick = (o->u.date.secs % 60) * TICKS_PER_SECOND + + o->u.date.micro / (1000000 / TICKS_PER_SECOND); + return 0; + } + return ALPOEF_UNKNOWN_METHOD; +} + +static ULONG po_BinaryMethod(struct PObj *o, struct TagItem *tag) +{ + struct ALPOBinData *bd = (struct ALPOBinData *)tag->ti_Data; + + switch (tag->ti_Tag) + { + case ALPOBIN_ClearData: + return po_SetBinary(o, NULL, 0); + case ALPOBIN_GetData: + if (tag->ti_Data) + *(UBYTE **)tag->ti_Data = o->u.bin.data; + return 0; + case ALPOBIN_GetSize: + if (tag->ti_Data) + *(LONG *)tag->ti_Data = o->u.bin.size; + return 0; + case ALPOBIN_GetDataStruct: + if (!bd) + return ALPOEF_ILLEGAL_PARAMETER; + bd->data = o->u.bin.data; + bd->size = o->u.bin.size; + return 0; + case ALPOBIN_SetData: + if (!bd) + return ALPOEF_ILLEGAL_PARAMETER; + return po_SetBinary(o, bd->data, bd->size); + } + return ALPOEF_UNKNOWN_METHOD; +} + +static ULONG po_ArrayMethod(struct PObj *o, struct TagItem *tag) +{ + struct ALPOObjIndex *oi = (struct ALPOObjIndex *)tag->ti_Data; + LONG i; + + switch (tag->ti_Tag) + { + case ALPOARR_Clear: + po_FreeContents(o); + return 0; + case ALPOARR_GetCount: + if (tag->ti_Data) + *(LONG *)tag->ti_Data = o->u.con.count; + return 0; + case ALPOARR_AddObj: + return po_Insert(o, o->u.con.count, PO(tag->ti_Data), NULL); + case ALPOARR_RemoveObjAtIndex: + i = (LONG)tag->ti_Data; + if (i < 0 || i >= o->u.con.count) + return ALPOEF_NOTFOUND; + po_RemoveAt(o, i); + return 0; + case ALPOARR_RemoveObj: + if ((i = po_ObjIndex(o, PO(tag->ti_Data))) < 0) + return ALPOEF_NOTFOUND; + po_RemoveAt(o, i); + return 0; + } + + if (!oi) + return ALPOEF_ILLEGAL_PARAMETER; + + switch (tag->ti_Tag) + { + case ALPOARR_InsertObjAtIndex: + return po_Insert(o, oi->index, PO(oi->obj), NULL); + case ALPOARR_GetObjAtIndex: + if (oi->index < 0 || oi->index >= o->u.con.count) + { + oi->obj = NULL; + return ALPOEF_NOTFOUND; + } + oi->obj = (PrefsObject *)o->u.con.items[oi->index]; + return 0; + case ALPOARR_ReplaceObjAtIndex: + if (!oi->obj) + return ALPOEF_MISSING_OBJECT; + if (oi->index < 0 || oi->index >= o->u.con.count) + return ALPOEF_NOTFOUND; + po_Replace(o, oi->index, PO(oi->obj)); + return 0; + case ALPOARR_GetIndexForObj: + oi->index = po_ObjIndex(o, PO(oi->obj)); + return oi->index < 0 ? ALPOEF_NOTFOUND : 0; + } + return ALPOEF_UNKNOWN_METHOD; +} + +static LONG po_KeyCmp(struct PObj *o, LONG a, LONG b) +{ + return strcmp(o->u.con.keys[a], o->u.con.keys[b]); +} + +static void po_SortDict(struct PObj *o, BOOL descending) +{ + LONG i, j; + + for (i = 1; i < o->u.con.count; i++) + { + for (j = i; j > 0; j--) + { + LONG c = po_KeyCmp(o, j - 1, j); + struct PObj *ti; + STRPTR tk; + + if (descending ? c >= 0 : c <= 0) + break; + ti = o->u.con.items[j]; + o->u.con.items[j] = o->u.con.items[j - 1]; + o->u.con.items[j - 1] = ti; + tk = o->u.con.keys[j]; + o->u.con.keys[j] = o->u.con.keys[j - 1]; + o->u.con.keys[j - 1] = tk; + } + } +} + +static ULONG po_DictMethod(struct PObj *o, struct TagItem *tag) +{ + struct ALPOObjKey *ok = (struct ALPOObjKey *)tag->ti_Data; + struct ALPOObjIndex *oi = (struct ALPOObjIndex *)tag->ti_Data; + struct ALPOObjKeyIndex *oki = (struct ALPOObjKeyIndex *)tag->ti_Data; + LONG i; + + switch (tag->ti_Tag) + { + case ALPODICT_Clear: + po_FreeContents(o); + return 0; + case ALPODICT_SortAscending: + case ALPODICT_SortDescending: + po_SortDict(o, tag->ti_Tag == ALPODICT_SortDescending); + return 0; + case ALPODICT_GetCount: + if (tag->ti_Data) + *(LONG *)tag->ti_Data = o->u.con.count; + return 0; + case ALPODICT_RemoveObjForKey: + if ((i = po_KeyIndex(o, (CONST_STRPTR)tag->ti_Data)) < 0) + return ALPOEF_NOTFOUND; + po_RemoveAt(o, i); + return 0; + case ALPODICT_RemoveObjAtIndex: + i = (LONG)tag->ti_Data; + if (i < 0 || i >= o->u.con.count) + return ALPOEF_NOTFOUND; + po_RemoveAt(o, i); + return 0; + case ALPODICT_RemoveObj: + if ((i = po_ObjIndex(o, PO(tag->ti_Data))) < 0) + return ALPOEF_NOTFOUND; + po_RemoveAt(o, i); + return 0; + } + + if (!tag->ti_Data) + return ALPOEF_ILLEGAL_PARAMETER; + + switch (tag->ti_Tag) + { + case ALPODICT_SetObjForKey: + if (!ok->obj) + return ALPOEF_MISSING_OBJECT; + if (!ok->key) + return ALPOEF_ILLEGAL_PARAMETER; + return po_DictSet((PrefsObject *)o, ok->key, ok->obj) ? 0 : ALPOEF_MEMORY; + case ALPODICT_GetObjForKey: + ok->obj = po_DictGet((PrefsObject *)o, ok->key); + return ok->obj ? 0 : ALPOEF_NOTFOUND; + case ALPODICT_ReplaceObjForKey: + if (!ok->obj) + return ALPOEF_MISSING_OBJECT; + if ((i = po_KeyIndex(o, ok->key)) < 0) + return ALPOEF_NOTFOUND; + po_Replace(o, i, PO(ok->obj)); + return 0; + case ALPODICT_GetObjAndIndexForKey: + if ((oki->index = po_KeyIndex(o, oki->key)) < 0) + { + oki->obj = NULL; + return ALPOEF_NOTFOUND; + } + oki->obj = (PrefsObject *)o->u.con.items[oki->index]; + return 0; + case ALPODICT_GetObjAtIndex: + if (oi->index < 0 || oi->index >= o->u.con.count) + { + oi->obj = NULL; + return ALPOEF_NOTFOUND; + } + oi->obj = (PrefsObject *)o->u.con.items[oi->index]; + return 0; + case ALPODICT_ReplaceObjAtIndex: + if (!oi->obj) + return ALPOEF_MISSING_OBJECT; + if (oi->index < 0 || oi->index >= o->u.con.count) + return ALPOEF_NOTFOUND; + po_Replace(o, oi->index, PO(oi->obj)); + return 0; + case ALPODICT_GetObjAndKeyAtIndex: + if (oki->index < 0 || oki->index >= o->u.con.count) + { + oki->obj = NULL; + oki->key = NULL; + return ALPOEF_NOTFOUND; + } + oki->obj = (PrefsObject *)o->u.con.items[oki->index]; + oki->key = o->u.con.keys[oki->index]; + return 0; + case ALPODICT_InsertObjAtIndex: + if (po_KeyIndex(o, oki->key) >= 0) + return ALPOEF_DUPLICATE; + return po_Insert(o, oki->index, PO(oki->obj), oki->key); + case ALPODICT_GetKeyAndIndexForObj: + if ((oki->index = po_ObjIndex(o, PO(oki->obj))) < 0) + { + oki->key = NULL; + return ALPOEF_NOTFOUND; + } + oki->key = o->u.con.keys[oki->index]; + return 0; + } + return ALPOEF_UNKNOWN_METHOD; +} + +/* The Alloc methods that take a parameter, per type. */ +static struct PObj *po_AllocWith(ULONG type, struct TagItem *tag, ULONG *err) +{ + struct PObj *o; + struct TagItem set; + + if (tag->ti_Tag != ALPO_AllocWithString && (tag->ti_Tag - ALPO_Dummy) / 10000 != 3) + return NULL; + + if (!(o = po_Alloc(type))) + { + *err |= ALPOEF_MEMORY; + return NULL; + } + + if (tag->ti_Tag == ALPO_AllocWithString) + { + ULONG e = po_SetWithString(o, (CONST_STRPTR)tag->ti_Data); + + if (e) + { + *err |= e; + po_Release((PrefsObject *)o); + return NULL; + } + return o; + } + + set.ti_Data = tag->ti_Data; + switch (type) + { + case ALPOT_String: + if (tag->ti_Tag == ALPOSTR_AllocSetString) + set.ti_Tag = ALPOSTR_SetString; + else if (tag->ti_Tag == ALPOSTR_AllocSetUniString) + set.ti_Tag = ALPOSTR_SetUniString; + else + goto unknown; + if ((*err |= po_StringMethod(o, &set)) == 0) + return o; + break; + case ALPOT_Number: + if (tag->ti_Tag == ALPONUM_AllocSetLong) + set.ti_Tag = ALPONUM_SetLong; + else if (tag->ti_Tag == ALPONUM_AllocSetBool) + set.ti_Tag = ALPONUM_SetBool; + else if (tag->ti_Tag == ALPONUM_AllocSetDouble) + set.ti_Tag = ALPONUM_SetDouble; + else + goto unknown; + if ((*err |= po_NumberMethod(o, &set)) == 0) + return o; + break; + case ALPOT_Date: + if (tag->ti_Tag == ALPODAT_AllocSetTimeVal) + set.ti_Tag = ALPODAT_SetTimeVal; + else if (tag->ti_Tag == ALPODAT_AllocSetCTime) + { + /* 1970 to 1978 is 2922 days */ + set.ti_Tag = ALPODAT_SetSecsSince1978; + set.ti_Data = tag->ti_Data >= 2922 * 86400 ? tag->ti_Data - 2922 * 86400 : 0; + } + else + goto unknown; + if ((*err |= po_DateMethod(o, &set)) == 0) + return o; + break; + case ALPOT_Binary: + if (tag->ti_Tag == ALPOBIN_AllocWithSize) + { + if ((*err |= po_SetBinary(o, NULL, (LONG)tag->ti_Data)) == 0) + return o; + break; + } + if (tag->ti_Tag != ALPOBIN_AllocWithData) + goto unknown; + set.ti_Tag = ALPOBIN_SetData; + if ((*err |= po_BinaryMethod(o, &set)) == 0) + return o; + break; + default: + goto unknown; + } + po_Release((PrefsObject *)o); + return NULL; + +unknown: + *err |= ALPOEF_UNKNOWN_METHOD; + po_Release((PrefsObject *)o); + return NULL; +} + +/* + * Run the methods in tags. want is the type the access function is for, + * or ALPOT_None for PrefsBaseObjectA(), which only knows the base methods. + */ +PrefsObject *po_Dispatch(ULONG want, PrefsObject *obj, ULONG *error, struct TagItem *tags) +{ + struct TagItem *tstate = tags, *tag; + struct PObj *o = PO(obj); + ULONG err = 0; + + while ((tag = NextTagItem(&tstate))) + { + ULONG e = 0; + + if (tag->ti_Tag < ALPO_Dummy || tag->ti_Tag > ALPO_Dummy + 49999) + { + err |= ALPOEF_UNKNOWN_METHOD; + continue; + } + + if (tag->ti_Tag == ALPO_Alloc || tag->ti_Tag == ALPO_AllocWithString + || (tag->ti_Tag - ALPO_Dummy) / 10000 == 3) + { + struct PObj *n; + + if (want == ALPOT_None) + { + err |= ALPOEF_IMPOSSIBLE_OP; + continue; + } + if (tag->ti_Tag == ALPO_Alloc) + { + if (!(n = po_Alloc(want))) + err |= ALPOEF_MEMORY; + } + else + n = po_AllocWith(want, tag, &err); + if (n) + o = n; + continue; + } + + if (!o) + { + err |= ALPOEF_MISSING_OBJECT; + continue; + } + if (want != ALPOT_None && o->po_Type != want) + { + err |= ALPOEF_WRONG_OBJECTTYPE; + continue; + } + + switch (tag->ti_Tag) + { + case ALPO_Release: + if (o->po_Retain <= 1) + { + po_Release((PrefsObject *)o); + o = NULL; + } + else + o->po_Retain--; + break; + case ALPO_Copy: + { + struct PObj *c = PO(po_Copy((PrefsObject *)o)); + + if (c) + o = c; + else + e = ALPOEF_MEMORY; + } + break; + case ALPO_Retain: + o->po_Retain++; + break; + case ALPO_Identify: + if (tag->ti_Data) + *(ULONG *)tag->ti_Data = o->po_Type; + break; + case ALPO_GetRetainCount: + if (tag->ti_Data) + *(LONG *)tag->ti_Data = o->po_Retain; + break; + case ALPO_Serialize: + e = tag->ti_Data ? xml_Serialize((PrefsObject *)o, (struct ALPOPutProcInfo *)tag->ti_Data) + : ALPOEF_ILLEGAL_PARAMETER; + break; + case ALPO_Deserialize: + e = tag->ti_Data ? xml_Deserialize((PrefsObject *)o, (struct ALPOGetProcInfo *)tag->ti_Data) + : ALPOEF_ILLEGAL_PARAMETER; + break; + case ALPO_SetWithString: + e = po_SetWithString(o, (CONST_STRPTR)tag->ti_Data); + break; + case ALPO_GetAsString: + e = po_GetAsString(o, (struct ALPOString *)tag->ti_Data); + break; + default: + switch (o->po_Type) + { + case ALPOT_String: e = po_StringMethod(o, tag); break; + case ALPOT_Number: e = po_NumberMethod(o, tag); break; + case ALPOT_Date: e = po_DateMethod(o, tag); break; + case ALPOT_Binary: e = po_BinaryMethod(o, tag); break; + case ALPOT_Array: e = po_ArrayMethod(o, tag); break; + case ALPOT_Dictionary: e = po_DictMethod(o, tag); break; + default: e = ALPOEF_INTERNAL_ERROR; break; + } + break; + } + err |= e; + } + + if (error) + *error = err; + return (PrefsObject *)o; +} diff --git a/workbench/libs/application/application_server.h b/workbench/libs/application/application_server.h new file mode 100644 index 00000000000..57bee09d178 --- /dev/null +++ b/workbench/libs/application/application_server.h @@ -0,0 +1,47 @@ +#ifndef APPLICATION_SERVER_H +#define APPLICATION_SERVER_H + +/* + Copyright (C) 2026, The AROS Development Team. All rights reserved. + + Desc: Request format between application.library and the notification + server (SYS:System/Notifications). Private to the two of them. +*/ + +#include +#include + +#define NOTIFYSERVER_PORTNAME "AROS.Notifications" +#define NOTIFYSERVER_PATH "SYS:System/Notifications" +#define NOTIFYSERVER_VERSION 3 + +#define NOTIFYSERVER_SCREENMAX 128 +#define NOTIFYSERVER_PATHMAX 512 +#define NOTIFYSERVER_BACKMAX 512 +#define NOTIFYSERVER_GAUGESMAX 512 + +struct NotifyServerMsg +{ + struct Message nsm_Msg; + ULONG nsm_Version; + ULONG nsm_AppID; + ULONG nsm_Result; /* set by the server: APPNOTIFY_ result */ + BOOL nsm_CloseOnDC; + BOOL nsm_Update; + BOOL nsm_LogOnly; + BOOL nsm_ConfirmClose; + BOOL nsm_Withdraw; /* nsm_BackMsg: take those back */ + UBYTE nsm_ImageVertAlign; + UBYTE nsm_Pri; + TEXT nsm_AppName[APPNOTIFY_TITLE_MAX + 1]; + TEXT nsm_Title[APPNOTIFY_TITLE_MAX + 1]; + TEXT nsm_Text[APPNOTIFY_TEXT_MAX + 1]; + TEXT nsm_PubScreen[NOTIFYSERVER_SCREENMAX]; + TEXT nsm_ImageFile[NOTIFYSERVER_PATHMAX]; + TEXT nsm_IconFile[NOTIFYSERVER_PATHMAX]; + TEXT nsm_SoundFile[NOTIFYSERVER_PATHMAX]; + TEXT nsm_BackMsg[NOTIFYSERVER_BACKMAX]; + TEXT nsm_Gauges[NOTIFYSERVER_GAUGESMAX]; +}; + +#endif /* APPLICATION_SERVER_H */ diff --git a/workbench/libs/application/application_xml.c b/workbench/libs/application/application_xml.c new file mode 100644 index 00000000000..45183b91f72 --- /dev/null +++ b/workbench/libs/application/application_xml.c @@ -0,0 +1,935 @@ +/* + Copyright (C) 2026, The AROS Development Team. All rights reserved. + + Desc: PrefsObjects as XML, and prefs files in ENV: and ENVARC:. + + + + + Width + 640 + ... + + + + Strings hold 8-bit characters; in a UTF-8 stream each byte is one + character, so any byte string reads back as it was written. +*/ + +#include +#include +#include +#include +#include +#include + +#include + +#include "application_intern.h" +#include "application_po.h" + +/* ---------------------------------------------------------------------- */ +/* Writing */ + +static void w_Ch(struct ALPOPutProcInfo *pi, LONG c) +{ + pi->putChProc(c, pi); +} + +static void w_Str(struct ALPOPutProcInfo *pi, CONST_STRPTR s) +{ + while (*s) + w_Ch(pi, (UBYTE)*s++); +} + +static void w_Indent(struct ALPOPutProcInfo *pi) +{ + LONG i; + + for (i = 0; i < pi->tabDepth; i++) + w_Ch(pi, '\t'); +} + +/* Text content: markup characters escaped, 8-bit characters as UTF-8. */ +static void w_Text(struct ALPOPutProcInfo *pi, CONST_STRPTR s, ULONG len) +{ + ULONG i; + + for (i = 0; i < len; i++) + { + UBYTE c = s[i]; + + switch (c) + { + case '<': w_Str(pi, "<"); break; + case '>': w_Str(pi, ">"); break; + case '&': w_Str(pi, "&"); break; + default: + if (c < 0x20 && c != '\t' && c != '\n' && c != '\r') + { + TEXT num[12]; + + po_FormatLong(num, c); + w_Str(pi, "&#"); + w_Str(pi, num); + w_Ch(pi, ';'); + } + else if (c >= 0x80 && pi->encoding == ALPOENC_UTF8) + { + w_Ch(pi, 0xC0 | (c >> 6)); + w_Ch(pi, 0x80 | (c & 0x3F)); + } + else + w_Ch(pi, c); + break; + } + } +} + +static void w_Element(struct ALPOPutProcInfo *pi, CONST_STRPTR name, CONST_STRPTR text, ULONG len) +{ + w_Indent(pi); + w_Ch(pi, '<'); + w_Str(pi, name); + w_Ch(pi, '>'); + w_Text(pi, text, len); + w_Str(pi, "\n"); +} + +static void w_Base64(struct ALPOPutProcInfo *pi, CONST UBYTE *data, LONG size) +{ + static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + LONG i; + + for (i = 0; i < size; i += 3) + { + ULONG v = data[i] << 16; + + if (i + 1 < size) + v |= data[i + 1] << 8; + if (i + 2 < size) + v |= data[i + 2]; + w_Ch(pi, b64[(v >> 18) & 63]); + w_Ch(pi, b64[(v >> 12) & 63]); + w_Ch(pi, i + 1 < size ? b64[(v >> 6) & 63] : '='); + w_Ch(pi, i + 2 < size ? b64[v & 63] : '='); + } +} + +ULONG xml_Serialize(PrefsObject *obj, struct ALPOPutProcInfo *pi) +{ + struct PObj *o = PO(obj); + TEXT buf[40]; + LONG i; + + if (!o || !pi->putChProc) + return ALPOEF_ILLEGAL_PARAMETER; + + switch (o->po_Type) + { + case ALPOT_String: + w_Element(pi, "string", o->u.str.s, o->u.str.len); + break; + + case ALPOT_Number: + if (o->u.num.type == ALPONUMT_Bool) + w_Element(pi, "bool", o->u.num.l ? "TRUE" : "FALSE", o->u.num.l ? 4 : 5); + else if (o->u.num.type == ALPONUMT_Double) + { + po_FormatDouble(buf, o->u.num.d); + w_Element(pi, "real", buf, strlen(buf)); + } + else + { + po_FormatLong(buf, o->u.num.l); + w_Element(pi, "integer", buf, strlen(buf)); + } + break; + + case ALPOT_Date: + po_FormatDate(buf, o->u.date.secs); + w_Element(pi, "date", buf, strlen(buf)); + break; + + case ALPOT_Binary: + w_Indent(pi); + w_Str(pi, ""); + w_Base64(pi, o->u.bin.data, o->u.bin.size); + w_Str(pi, "\n"); + break; + + case ALPOT_Array: + case ALPOT_Dictionary: + { + CONST_STRPTR name = o->po_Type == ALPOT_Array ? "array" : "dict"; + + w_Indent(pi); + w_Ch(pi, '<'); + w_Str(pi, name); + w_Str(pi, ">\n"); + pi->tabDepth++; + for (i = 0; i < o->u.con.count; i++) + { + if (o->u.con.keys) + w_Element(pi, "key", o->u.con.keys[i], strlen(o->u.con.keys[i])); + xml_Serialize((PrefsObject *)o->u.con.items[i], pi); + } + pi->tabDepth--; + w_Indent(pi); + w_Str(pi, "\n"); + } + break; + + default: + return ALPOEF_WRONG_OBJECT; + } + return 0; +} + +static void w_Document(PrefsObject *dict, struct ALPOPutProcInfo *pi, ULONG *err) +{ + LONG depth = pi->tabDepth; + + w_Str(pi, pi->encoding == ALPOENC_UTF8 + ? "\n" + : "\n"); + w_Str(pi, "\n"); + *err |= xml_Serialize(dict, pi); + w_Str(pi, "\n"); + pi->tabDepth = depth; +} + +/* ---------------------------------------------------------------------- */ +/* Reading */ + +static LONG r_Ch(struct ALPOGetProcInfo *pi) +{ + return pi->getChProc(pi); +} + +static void r_UnCh(struct ALPOGetProcInfo *pi, LONG c) +{ + if (c >= 0) + pi->unGetChProc(c, pi); +} + +static LONG r_SkipSpace(struct ALPOGetProcInfo *pi) +{ + LONG c; + + do + c = r_Ch(pi); + while (c == ' ' || c == '\t' || c == '\n' || c == '\r'); + return c; +} + +/* Skip to just after 'end', a short terminator like "?>" or "-->": the + last strlen(end) characters read are compared, so "--->" ends too. */ +static BOOL r_SkipPast(struct ALPOGetProcInfo *pi, CONST_STRPTR end) +{ + ULONG len = strlen(end), n = 0; + TEXT last[8]; + LONG c; + + if (len == 0 || len > sizeof(last)) + return FALSE; + while ((c = r_Ch(pi)) >= 0) + { + if (n < len) + last[n++] = c; + else + { + memmove(last, last + 1, len - 1); + last[len - 1] = c; + } + if (n == len && memcmp(last, end, len) == 0) + return TRUE; + } + return FALSE; +} + +/* + * Read the next tag into buf: "name" for an open tag, "/name" for a close + * tag, "name/" for an empty element. Declarations, comments and attributes + * are skipped. Text before the tag must be blank. + */ +static ULONG r_Tag(struct ALPOGetProcInfo *pi, STRPTR buf) +{ + LONG c, n; + + for (;;) + { + c = r_SkipSpace(pi); + if (c != '<') + return c < 0 ? ALPOEF_READTAG : ALPOEF_XMLERROR; + + c = r_Ch(pi); + if (c == '?') + { + if (!r_SkipPast(pi, "?>")) + return ALPOEF_XMLERROR; + continue; + } + if (c == '!') + { + LONG c2 = r_Ch(pi); + + if (c2 == '-') + { + if (r_Ch(pi) != '-' || !r_SkipPast(pi, "-->")) + return ALPOEF_XMLERROR; + } + else if (!r_SkipPast(pi, ">")) + return ALPOEF_XMLERROR; + continue; + } + break; + } + + n = 0; + if (c == '/') + { + buf[n++] = '/'; + c = r_Ch(pi); + } + while (c >= 0 && c != '>' && c != '/' && c != ' ' && c != '\t' && c != '\n' && c != '\r') + { + if (n < TAGBUFFER_SIZE - 2) + buf[n++] = c; + c = r_Ch(pi); + } + /* attributes */ + while (c >= 0 && c != '>' && c != '/') + { + if (c == '"' || c == '\'') + { + LONG q = c; + + while ((c = r_Ch(pi)) >= 0 && c != q) + ; + } + c = r_Ch(pi); + } + if (c == '/') + { + buf[n++] = '/'; + c = r_Ch(pi); + } + buf[n] = '\0'; + if (c != '>' || n == 0) + return ALPOEF_XMLERROR; + return 0; +} + +struct TextBuf +{ + STRPTR tb_Buf; + ULONG tb_Len, tb_Size; +}; + +static BOOL tb_Add(struct TextBuf *tb, UBYTE c) +{ + if (tb->tb_Len + 1 >= tb->tb_Size) + { + ULONG size = tb->tb_Size ? tb->tb_Size * 2 : 64; + STRPTR n = AllocVec(size, MEMF_ANY); + + if (!n) + return FALSE; + if (tb->tb_Len) + CopyMem(tb->tb_Buf, n, tb->tb_Len); + FreeVec(tb->tb_Buf); + tb->tb_Buf = n; + tb->tb_Size = size; + } + tb->tb_Buf[tb->tb_Len++] = c; + tb->tb_Buf[tb->tb_Len] = '\0'; + return TRUE; +} + +/* A character above 255 cannot be held in an 8-bit string. */ +static BOOL tb_AddCode(struct TextBuf *tb, ULONG code) +{ + return tb_Add(tb, code < 256 ? code : '?'); +} + +static ULONG r_Entity(struct ALPOGetProcInfo *pi, struct TextBuf *tb) +{ + TEXT name[12]; + LONG c, n = 0; + ULONG code = 0; + + while ((c = r_Ch(pi)) >= 0 && c != ';' && n < (LONG)sizeof(name) - 1) + name[n++] = c; + name[n] = '\0'; + if (c != ';') + return ALPOEF_XMLERROR; + + if (strcmp(name, "lt") == 0) code = '<'; + else if (strcmp(name, "gt") == 0) code = '>'; + else if (strcmp(name, "amp") == 0) code = '&'; + else if (strcmp(name, "quot") == 0) code = '"'; + else if (strcmp(name, "apos") == 0) code = '\''; + else if (name[0] == '#') + { + STRPTR p = name + 1; + BOOL hex = (*p == 'x' || *p == 'X'); + + if (hex) + p++; + if (!*p) + return ALPOEF_XMLERROR; + for (; *p; p++) + { + LONG d; + + if (*p >= '0' && *p <= '9') + d = *p - '0'; + else if (hex && (*p | 0x20) >= 'a' && (*p | 0x20) <= 'f') + d = (*p | 0x20) - 'a' + 10; + else + return ALPOEF_XMLERROR; + code = code * (hex ? 16 : 10) + d; + } + } + else + return ALPOEF_XMLERROR; + + return tb_AddCode(tb, code) ? 0 : ALPOEF_MEMORY; +} + +/* Text up to the next '<', entities and UTF-8 decoded. */ +static ULONG r_Text(struct ALPOGetProcInfo *pi, struct TextBuf *tb) +{ + LONG c; + + tb->tb_Len = 0; + if (!tb_Add(tb, 0)) + return ALPOEF_MEMORY; + tb->tb_Len = 0; + + while ((c = r_Ch(pi)) >= 0 && c != '<') + { + ULONG err = 0; + + if (c == '&') + err = r_Entity(pi, tb); + else if (c >= 0xC0 && pi->encoding == ALPOENC_UTF8) + { + ULONG code, more; + LONG c2; + + if (c >= 0xF0) { code = c & 0x07; more = 3; } + else if (c >= 0xE0) { code = c & 0x0F; more = 2; } + else { code = c & 0x1F; more = 1; } + while (more--) + { + if ((c2 = r_Ch(pi)) < 0 || (c2 & 0xC0) != 0x80) + return ALPOEF_READDATA; + code = (code << 6) | (c2 & 0x3F); + } + err = tb_AddCode(tb, code) ? 0 : ALPOEF_MEMORY; + } + else + err = tb_Add(tb, c) ? 0 : ALPOEF_MEMORY; + if (err) + return err; + } + if (c < 0) + return ALPOEF_READDATA; + r_UnCh(pi, c); + return 0; +} + +static ULONG r_CloseTag(struct ALPOGetProcInfo *pi, CONST_STRPTR name) +{ + ULONG err = r_Tag(pi, pi->key_otag); + + if (err) + return err; + if (pi->key_otag[0] != '/' || strcmp(pi->key_otag + 1, name) != 0) + return ALPOEF_WRONG_CTAG; + return 0; +} + +static ULONG xml_TypeOf(CONST_STRPTR tag) +{ + TEXT name[16]; + ULONG len = strlen(tag); + + if (len >= sizeof(name)) + return ALPOT_None; + strcpy(name, tag); + if (len && name[len - 1] == '/') + name[len - 1] = '\0'; + + if (strcmp(name, "string") == 0) + return ALPOT_String; + if (strcmp(name, "integer") == 0 || strcmp(name, "real") == 0 || strcmp(name, "bool") == 0 + || strcmp(name, "true") == 0 || strcmp(name, "false") == 0) + return ALPOT_Number; + if (strcmp(name, "date") == 0) + return ALPOT_Date; + if (strcmp(name, "data") == 0) + return ALPOT_Binary; + if (strcmp(name, "array") == 0) + return ALPOT_Array; + if (strcmp(name, "dict") == 0) + return ALPOT_Dictionary; + return ALPOT_None; +} + +static LONG b64_Value(UBYTE c) +{ + if (c >= 'A' && c <= 'Z') return c - 'A'; + if (c >= 'a' && c <= 'z') return c - 'a' + 26; + if (c >= '0' && c <= '9') return c - '0' + 52; + if (c == '+') return 62; + if (c == '/') return 63; + return -1; +} + +static ULONG r_Base64(struct PObj *o, struct TextBuf *tb) +{ + UBYTE *data; + ULONG i, n = 0, bits = 0, acc = 0; + + if (!(data = AllocVec(tb->tb_Len / 4 * 3 + 3, MEMF_ANY | MEMF_CLEAR))) + return ALPOEF_MEMORY; + for (i = 0; i < tb->tb_Len; i++) + { + LONG v = b64_Value(tb->tb_Buf[i]); + + if (v < 0) + continue; + acc = (acc << 6) | v; + bits += 6; + if (bits >= 8) + { + bits -= 8; + data[n++] = (acc >> bits) & 0xFF; + } + } + FreeVec(o->u.bin.data); + o->u.bin.data = data; + o->u.bin.size = n; + return 0; +} + +/* + * The open tag has been read, into pi->key_otag. Fill obj from the content + * and read the close tag. + */ +/* Containers inside containers, from the object being read: a file from + ENV: must not be able to run the reader out of stack. */ +#define XML_MAXDEPTH 32 + +static ULONG xml_DeserializeDepth(PrefsObject *obj, struct ALPOGetProcInfo *pi, ULONG depth) +{ + struct PObj *o = PO(obj); + struct TextBuf tb = { NULL, 0, 0 }; + TEXT tag[16]; + ULONG len, err = 0; + BOOL empty; + + if (!o || !pi->getChProc || !pi->unGetChProc || !pi->key_otag) + return ALPOEF_ILLEGAL_PARAMETER; + if (depth > XML_MAXDEPTH) + return ALPOEF_XMLERROR | ALPOEF_DESERIALIZATION; + + len = strlen(pi->key_otag); + if (xml_TypeOf(pi->key_otag) != o->po_Type || len >= sizeof(tag)) + return ALPOEF_WRONG_OTAG; + strcpy(tag, pi->key_otag); + empty = (tag[len - 1] == '/'); + if (empty) + tag[len - 1] = '\0'; + + if (o->po_Type == ALPOT_Array || o->po_Type == ALPOT_Dictionary) + { + STRPTR key = NULL; + + while (!empty) + { + struct PObj *item; + ULONG type; + + if ((err = r_Tag(pi, pi->key_otag))) + break; + if (pi->key_otag[0] == '/') + { + if (strcmp(pi->key_otag + 1, tag) != 0) + err = ALPOEF_WRONG_CTAG; + break; + } + if (o->po_Type == ALPOT_Dictionary) + { + if (strcmp(pi->key_otag, "key") != 0) + { + err = ALPOEF_WRONG_OTAG; + break; + } + if ((err = r_Text(pi, &tb)) || (err = r_CloseTag(pi, "key"))) + break; + FreeVec(key); + if (!(key = app_StrDup(tb.tb_Buf))) + { + err = ALPOEF_MEMORY; + break; + } + if ((err = r_Tag(pi, pi->key_otag))) + break; + } + if ((type = xml_TypeOf(pi->key_otag)) == ALPOT_None) + { + err = ALPOEF_WRONG_OTAG; + break; + } + if (!(item = PO(po_New(type)))) + { + err = ALPOEF_MEMORY; + break; + } + if ((err = xml_DeserializeDepth((PrefsObject *)item, pi, depth + 1))) + { + po_Release((PrefsObject *)item); + break; + } + if (o->po_Type == ALPOT_Dictionary) + { + if (!po_DictSet((PrefsObject *)o, key, (PrefsObject *)item)) + err = ALPOEF_MEMORY; + } + else if (!po_ArrayInsert((PrefsObject *)o, (PrefsObject *)item, -1)) + err = ALPOEF_MEMORY; + if (err) + { + po_Release((PrefsObject *)item); + break; + } + } + FreeVec(key); + FreeVec(tb.tb_Buf); + return err ? (err | ALPOEF_DESERIALIZATION) : 0; + } + + if (!empty) + { + if ((err = r_Text(pi, &tb)) || (err = r_CloseTag(pi, tag))) + { + FreeVec(tb.tb_Buf); + return err | ALPOEF_DESERIALIZATION; + } + } + else + { + if (!tb_Add(&tb, 0)) + return ALPOEF_MEMORY; + tb.tb_Len = 0; + } + + switch (o->po_Type) + { + case ALPOT_String: + FreeVec(o->u.str.s); + o->u.str.s = tb.tb_Buf; + o->u.str.len = tb.tb_Len; + tb.tb_Buf = NULL; + break; + + case ALPOT_Number: + if (strcmp(tag, "true") == 0 || strcmp(tag, "false") == 0) + { + o->u.num.type = ALPONUMT_Bool; + o->u.num.l = (tag[0] == 't'); + } + else if (strcmp(tag, "bool") == 0) + { + CONST_STRPTR s = tb.tb_Buf; + + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') + s++; + o->u.num.type = ALPONUMT_Bool; + o->u.num.l = (Strnicmp(s, "TRUE", 4) == 0 || Strnicmp(s, "YES", 3) == 0 || *s == '1'); + } + else + { + LONG l; + double d; + BOOL isd; + + if (!po_ParseNumber(tb.tb_Buf, &l, &d, &isd)) + err = ALPOEF_READDATA; + else if (tag[0] == 'r') + { + o->u.num.type = ALPONUMT_Double; + o->u.num.d = d; + } + else + { + o->u.num.type = ALPONUMT_Long; + o->u.num.l = l; + } + } + break; + + case ALPOT_Date: + if (!po_ParseDate(tb.tb_Buf, &o->u.date.secs)) + err = ALPOEF_READDATA; + o->u.date.micro = 0; + break; + + case ALPOT_Binary: + err = r_Base64(o, &tb); + break; + } + + FreeVec(tb.tb_Buf); + return err ? (err | ALPOEF_DESERIALIZATION) : 0; +} + +ULONG xml_Deserialize(PrefsObject *obj, struct ALPOGetProcInfo *pi) +{ + return xml_DeserializeDepth(obj, pi, 0); +} + +/* Read up to the first object's open tag, past a wrapper. */ +ULONG xml_Begin(struct ALPOGetProcInfo *pi) +{ + if (!pi || !pi->getChProc || !pi->unGetChProc || !pi->key_otag) + return ALPOT_None; + if (r_Tag(pi, pi->key_otag)) + return ALPOT_None; + if (strcmp(pi->key_otag, "pobjects") == 0 && r_Tag(pi, pi->key_otag)) + return ALPOT_None; + return xml_TypeOf(pi->key_otag); +} + +static ULONG xml_ReadDict(PrefsObject *dict, struct ALPOGetProcInfo *pi) +{ + PrefsObject *tmp; + ULONG err; + + if (xml_Begin(pi) != ALPOT_Dictionary) + return ALPOEF_DESERIALIZATION | ALPOEF_WRONG_OTAG; + if (!(tmp = po_NewDict())) + return ALPOEF_MEMORY; + if (!(err = xml_Deserialize(tmp, pi))) + po_MoveContents(dict, tmp); + po_Release(tmp); + return err; +} + +/* ---------------------------------------------------------------------- */ +/* Files */ + +struct FileIO +{ + BPTR fio_File; + LONG fio_Back; /* pushed back character, or -1 */ + BOOL fio_Error; +}; + +static LONG fio_Get(struct ALPOGetProcInfo *pi) +{ + struct FileIO *fio = pi->handlePtr; + LONG c; + + if (fio->fio_Back >= 0) + { + c = fio->fio_Back; + fio->fio_Back = -1; + return c; + } + c = FGetC(fio->fio_File); + return c < 0 ? -1 : c; +} + +static LONG fio_UnGet(LONG c, struct ALPOGetProcInfo *pi) +{ + ((struct FileIO *)pi->handlePtr)->fio_Back = c; + return c; +} + +static void fio_Put(LONG c, struct ALPOPutProcInfo *pi) +{ + struct FileIO *fio = pi->handlePtr; + + if (FPutC(fio->fio_File, c) < 0) + fio->fio_Error = TRUE; +} + +ULONG xml_ReadFile(PrefsObject *dict, CONST_STRPTR path) +{ + struct FileIO fio = { 0, -1, FALSE }; + struct ALPOGetProcInfo pi; + TEXT otag[TAGBUFFER_SIZE]; + ULONG err; + + if (!(fio.fio_File = Open(path, MODE_OLDFILE))) + { + /* a reset between xml_WriteFile()'s delete and rename left only + the new copy */ + TEXT tmp[APP_PATHMAX]; + + if (strlen(path) + 5 > sizeof(tmp)) + return ALPOEF_FILEERROR; + strcpy(tmp, path); + strcat(tmp, ".new"); + if (!(fio.fio_File = Open(tmp, MODE_OLDFILE))) + return ALPOEF_FILEERROR; + } + memset(&pi, 0, sizeof(pi)); + pi.getChProc = fio_Get; + pi.unGetChProc = fio_UnGet; + pi.handlePtr = &fio; + pi.key_otag = otag; + pi.encoding = ALPOENC_UTF8; + err = xml_ReadDict(dict, &pi); + Close(fio.fio_File); + return err; +} + +/* Make the drawers above a file, as for ENV:Sys/x.xml on a fresh RAM: */ +static void xml_MakeDrawers(CONST_STRPTR path) +{ + TEXT buf[APP_PATHMAX]; + STRPTR p; + + if (strlen(path) >= sizeof(buf)) + return; + strcpy(buf, path); + for (p = buf; *p; p++) + { + if (*p == '/') + { + BPTR lock; + + *p = '\0'; + if ((lock = Lock(buf, SHARED_LOCK)) || (lock = CreateDir(buf))) + UnLock(lock); + *p = '/'; + } + } +} + +ULONG xml_WriteFile(PrefsObject *dict, CONST_STRPTR path) +{ + struct FileIO fio = { 0, -1, FALSE }; + struct ALPOPutProcInfo pi; + TEXT tmp[APP_PATHMAX]; + ULONG err = 0; + + if (strlen(path) + 5 > sizeof(tmp)) + return ALPOEF_ILLEGAL_PARAMETER; + strcpy(tmp, path); + strcat(tmp, ".new"); + + xml_MakeDrawers(path); + if (!(fio.fio_File = Open(tmp, MODE_NEWFILE))) + return ALPOEF_FILEERROR; + memset(&pi, 0, sizeof(pi)); + pi.putChProc = fio_Put; + pi.handlePtr = &fio; + pi.encoding = ALPOENC_UTF8; + w_Document(dict, &pi, &err); + if (!Close(fio.fio_File) || fio.fio_Error) + err |= ALPOEF_FILEERROR; + if (err) + DeleteFile(tmp); + else + { + DeleteFile(path); + if (!Rename(tmp, path)) + err |= ALPOEF_FILEERROR; + } + return err; +} + +static BOOL prefs_Path(STRPTR buf, CONST_STRPTR dir, CONST_STRPTR name) +{ + if (strlen(dir) + strlen(name) >= APP_PATHMAX) + return FALSE; + strcpy(buf, dir); + strcat(buf, name); + return TRUE; +} + +/* name is below ENV:/ENVARC:, or a full path when it has a ':' */ +ULONG prefs_Read(struct ApplicationBase *AppBase, PrefsObject *dict, CONST_STRPTR name, + BOOL env, BOOL envarc) +{ + TEXT path[APP_PATHMAX]; + ULONG err = ALPOEF_FILEERROR; + + (void)AppBase; + if (po_Type(dict) != ALPOT_Dictionary) + return ALPOEF_WRONG_OBJECTTYPE; + po_Clear(dict); + if (!name || !app_IsProcess()) + return ALPOEF_ILLEGAL_PARAMETER; + + if (strchr(name, ':')) + return xml_ReadFile(dict, name); + if (env && prefs_Path(path, "ENV:", name)) + err = xml_ReadFile(dict, path); + if (err && envarc && prefs_Path(path, "ENVARC:", name)) + err = xml_ReadFile(dict, path); + return err; +} + +ULONG prefs_Write(struct ApplicationBase *AppBase, PrefsObject *dict, CONST_STRPTR name, + BOOL env, BOOL envarc) +{ + TEXT path[APP_PATHMAX]; + ULONG err = 0; + + (void)AppBase; + if (po_Type(dict) != ALPOT_Dictionary) + return ALPOEF_WRONG_OBJECTTYPE; + if (!name || !app_IsProcess()) + return ALPOEF_ILLEGAL_PARAMETER; + + if (strchr(name, ':')) + return xml_WriteFile(dict, name); + if (env) + err |= prefs_Path(path, "ENV:", name) ? xml_WriteFile(dict, path) : ALPOEF_ILLEGAL_PARAMETER; + if (envarc) + err |= prefs_Path(path, "ENVARC:", name) ? xml_WriteFile(dict, path) : ALPOEF_ILLEGAL_PARAMETER; + return err; +} + +/* ---------------------------------------------------------------------- */ +/* Streams given by the caller */ + +ULONG xml_ReadStream(PrefsObject *dict, struct ALPOGetProcInfo *pi) +{ + TEXT otag[TAGBUFFER_SIZE]; + STRPTR saved = pi->key_otag; + ULONG err; + + if (po_Type(dict) != ALPOT_Dictionary) + return ALPOEF_WRONG_OBJECTTYPE; + po_Clear(dict); + if (!saved) + pi->key_otag = otag; + err = xml_ReadDict(dict, pi); + pi->key_otag = saved; + return err; +} + +ULONG xml_WriteStream(PrefsObject *dict, struct ALPOPutProcInfo *pi) +{ + ULONG err = 0; + + if (po_Type(dict) != ALPOT_Dictionary) + return ALPOEF_WRONG_OBJECTTYPE; + if (!pi->putChProc) + return ALPOEF_ILLEGAL_PARAMETER; + w_Document(dict, pi, &err); + return err; +} diff --git a/workbench/libs/application/include/application.h b/workbench/libs/application/include/application.h new file mode 100644 index 00000000000..583c0e770c2 --- /dev/null +++ b/workbench/libs/application/include/application.h @@ -0,0 +1,461 @@ +#ifndef LIBRARIES_APPLICATION_H +#define LIBRARIES_APPLICATION_H + +/* + Copyright (C) 2026, The AROS Development Team. All rights reserved. + + Desc: application.library - registered applications, messages between + them, pop-up notifications and PrefsObjects preferences. + + The names follow the AmigaOS 4 application.library, so its programs + build unchanged. The tag and message type VALUES are AROS's own. +*/ + +#include +#include +#include +#include + +#define APPLICATIONNAME "application.library" + +/* An opaque PrefsObjects object; see the PrefsObjects section below. */ +typedef ULONG PrefsObject; + +/* struct ApplicationIconInfo.iconType */ +#define APPICONT_None 0 /* no icon, "invisible" application */ +#define APPICONT_ProgramIcon 1 /* the program's .info */ +#define APPICONT_CustomIcon 2 /* info.customIcon */ +#define APPICONT_Docky 3 /* info.dockyBase */ + +struct ApplicationIconInfo +{ + LONG iconType; + union + { + struct DiskObject *customIcon; + struct Library *dockyBase; + IPTR reserved[8]; + } info; +}; + +/* RegisterApplication() */ +#define REGAPP_Dummy (TAG_USER + 0x4A0000) +#define REGAPP_URLIdentifier (REGAPP_Dummy + 1) /* STRPTR: "supercoders.com" */ +#define REGAPP_WBStartup (REGAPP_Dummy + 2) /* struct WBStartup * */ +#define REGAPP_FileName (REGAPP_Dummy + 3) /* STRPTR: program path */ +#define REGAPP_FileLock (REGAPP_Dummy + 4) /* BPTR: lock on the program */ +#define REGAPP_Description (REGAPP_Dummy + 5) /* STRPTR, fixed once set */ +#define REGAPP_UniqueApplication (REGAPP_Dummy + 6) /* BOOL, fixed once set */ +#define REGAPP_HasIconifyFeature (REGAPP_Dummy + 7) /* BOOL */ +#define REGAPP_HasPrefsWindow (REGAPP_Dummy + 8) /* BOOL */ +#define REGAPP_CanCreateNewDocs (REGAPP_Dummy + 9) /* BOOL */ +#define REGAPP_CanPrintDocs (REGAPP_Dummy + 10) /* BOOL */ +#define REGAPP_AppNotifications (REGAPP_Dummy + 11) /* BOOL */ +#define REGAPP_BlankerNotifications (REGAPP_Dummy + 12) /* BOOL */ +#define REGAPP_AllowsBlanker (REGAPP_Dummy + 13) /* BOOL, default TRUE */ +#define REGAPP_SavePrefs (REGAPP_Dummy + 14) /* BOOL: write prefs on unregister */ +#define REGAPP_LoadPrefs (REGAPP_Dummy + 15) /* BOOL: read prefs on register */ +#define REGAPP_CustomPrefsFileName (REGAPP_Dummy + 16) /* STRPTR: prefs file instead of */ + /* ENV:[ENVDir/]ident.xml */ +#define REGAPP_ENVDir (REGAPP_Dummy + 17) /* STRPTR: drawer below ENV:/ENVARC: */ +#define REGAPP_CustomPrefsBaseName (REGAPP_Dummy + 18) /* STRPTR: base name instead of ident*/ +#define REGAPP_NoIcon (REGAPP_Dummy + 19) /* BOOL */ +#define REGAPP_Hidden (REGAPP_Dummy + 20) /* BOOL: starts iconified */ +#define REGAPP_AppIconInfo (REGAPP_Dummy + 21) /* struct ApplicationIconInfo * */ +#define REGAPP_NeedsGameMode (REGAPP_Dummy + 22) /* BOOL */ +#define REGAPP_IdenticalNameCounter (REGAPP_Dummy + 23) /* BOOL, default TRUE */ + +/* UnregisterApplication() */ +#define UNREGAPP_Dummy (TAG_USER + 0x4A0800) +#define UNREGAPP_WaitForUnlock (UNREGAPP_Dummy + 1) /* BOOL, default TRUE */ + +/* Get/SetApplicationAttrs(). Pointer values are read into an APTR or + STRPTR, everything else into a ULONG. */ +#define APPATTR_Dummy (TAG_USER + 0x4A1000) +#define APPATTR_Port (APPATTR_Dummy + 1) /* (G) struct MsgPort * */ +#define APPATTR_Name (APPATTR_Dummy + 2) /* (G) STRPTR */ +#define APPATTR_AppIdentifier (APPATTR_Dummy + 3) /* (G) STRPTR */ +#define APPATTR_URLIdentifier (APPATTR_Dummy + 4) /* (G) STRPTR */ +#define APPATTR_Description (APPATTR_Dummy + 5) /* (G) STRPTR */ +#define APPATTR_FileName (APPATTR_Dummy + 6) /* (G) STRPTR */ +#define APPATTR_UniqueApplication (APPATTR_Dummy + 7) /* (G) BOOL */ +#define APPATTR_Hidden (APPATTR_Dummy + 8) /* (SG) BOOL */ +#define APPATTR_HasIconifyFeature (APPATTR_Dummy + 9) /* (SG) BOOL */ +#define APPATTR_HasPrefsWindow (APPATTR_Dummy + 10) /* (SG) BOOL */ +#define APPATTR_CanCreateNewDocs (APPATTR_Dummy + 11) /* (SG) BOOL */ +#define APPATTR_CanPrintDocs (APPATTR_Dummy + 12) /* (SG) BOOL */ +#define APPATTR_AppNotifications (APPATTR_Dummy + 13) /* (SG) BOOL */ +#define APPATTR_BlankerNotifications (APPATTR_Dummy + 14) /* (SG) BOOL */ +#define APPATTR_AllowsBlanker (APPATTR_Dummy + 15) /* (SG) BOOL */ +#define APPATTR_NeedsGameMode (APPATTR_Dummy + 16) /* (SG) BOOL */ +#define APPATTR_SavePrefs (APPATTR_Dummy + 17) /* (SG) BOOL */ +#define APPATTR_AppOpenedDocument (APPATTR_Dummy + 18) /* (S) STRPTR: document path */ +#define APPATTR_ClearLastUsedDocs (APPATTR_Dummy + 19) /* (S) BOOL */ +#define APPATTR_LastUsedDocsArray (APPATTR_Dummy + 20) /* (G) PrefsObject *: a copy, */ + /* ALPO_Release it */ +#define APPATTR_FlushPrefs (APPATTR_Dummy + 21) /* (S) write the prefs now */ +#define APPATTR_IconType (APPATTR_Dummy + 22) /* (SG) struct ApplicationIconInfo * */ + /* (G only while locked) */ +#define APPATTR_MainPrefsDict (APPATTR_Dummy + 23) /* (SG) PrefsObject * */ +#define APPATTR_AppIdentifierCounter (APPATTR_Dummy + 24) /* (G) ULONG: the _n, 0 = first */ +#define APPATTR_NoIcon (APPATTR_Dummy + 25) /* (G) BOOL */ + +/* Get/SetAppLibAttrs() */ +#define APPLIBATTR_Dummy (TAG_USER + 0x4A1800) +#define APPLIBATTR_LastUsedAppsArray (APPLIBATTR_Dummy + 1) /* (G) PrefsObject *: a copy */ +#define APPLIBATTR_LastUsedAppsMaxCount (APPLIBATTR_Dummy + 2) /* (SG) ULONG, 0..25 */ +#define APPLIBATTR_ClearLastUsedApps (APPLIBATTR_Dummy + 3) /* (S) BOOL */ +#define APPLIBATTR_LastUsedDocsArray (APPLIBATTR_Dummy + 4) /* (G) PrefsObject *: a copy */ +#define APPLIBATTR_LastUsedDocsMaxCount (APPLIBATTR_Dummy + 5) /* (SG) ULONG, 0..25 */ +#define APPLIBATTR_ClearLastUsedDocs (APPLIBATTR_Dummy + 6) /* (S) BOOL */ +#define APPLIBATTR_BlankerIsAllowed (APPLIBATTR_Dummy + 7) /* (G) BOOL */ +#define APPLIBATTR_GameModeIsActive (APPLIBATTR_Dummy + 8) /* (G) BOOL */ + +#define APPLIB_LASTUSED_MAX 25 + +/* Keys of the dictionaries in the last used applications/documents arrays */ +#define LUA_BASE_KEY "LastUsedApplications" +#define LUA_OPENDATE_KEY "openDate" +#define LUA_APPIDENTIFIER_KEY "appIdentifier" +#define LUA_APPNAME_KEY "appName" +#define LUA_APPPATH_KEY "appPath" +#define LUD_BASE_KEY "LastUsedDocuments" +#define LUD_OPENDATE_KEY "openDate" +#define LUD_DOCPATH_KEY "docPath" +#define LUD_APPIDENTIFIER_KEY "appIdentifier" +#define LUD_APPPATH_KEY "appPath" + +/* FindApplication() */ +#define FINDAPP_Dummy (TAG_USER + 0x4A2000) +#define FINDAPP_Name (FINDAPP_Dummy + 1) /* "AudioMonster" */ +#define FINDAPP_AppIdentifier (FINDAPP_Dummy + 2) /* "AudioMonster_1.supercoders.com" */ +#define FINDAPP_FileName (FINDAPP_Dummy + 3) /* "Work:Utils/AudioMonster" */ + +/* Notify() */ +#define APPNOTIFY_Dummy (TAG_USER + 0x4A3000) +#define APPNOTIFY_Title (APPNOTIFY_Dummy + 1) /* STRPTR, up to 64 chars */ +#define APPNOTIFY_Text (APPNOTIFY_Dummy + 2) /* STRPTR, up to 160 chars */ +#define APPNOTIFY_PubScreenName (APPNOTIFY_Dummy + 3) /* STRPTR, "FRONT" = frontmost */ +#define APPNOTIFY_ImageFile (APPNOTIFY_Dummy + 4) /* STRPTR, full path, 32x32 */ +#define APPNOTIFY_BackMsg (APPNOTIFY_Dummy + 5) /* STRPTR, "URL:..." opens it */ +#define APPNOTIFY_CloseOnDC (APPNOTIFY_Dummy + 6) /* BOOL */ +#define APPNOTIFY_ImageVertAlign (APPNOTIFY_Dummy + 7) /* ULONG 0 top, 1 middle, 2 bottom */ +#define APPNOTIFY_Update (APPNOTIFY_Dummy + 8) /* BOOL: replace this app's others */ +#define APPNOTIFY_Pri (APPNOTIFY_Dummy + 9) /* ULONG 0..10: >8 beeps, 10 stays */ +#define APPNOTIFY_LogOnly (APPNOTIFY_Dummy + 10) /* BOOL: log, show nothing */ +#define APPNOTIFY_SoundFile (APPNOTIFY_Dummy + 11) /* STRPTR: sound to play */ +#define APPNOTIFY_ConfirmClose (APPNOTIFY_Dummy + 12) /* BOOL: APPLIBMT_ConfirmClose */ + /* when the pop-up closes */ +#define APPNOTIFY_IconFile (APPNOTIFY_Dummy + 13) /* STRPTR: icon, if no ImageFile */ +/* AROS: take back this application's pop-ups whose BackMsg is this string, + shown or waiting; nothing new is shown. */ +#define APPNOTIFY_Withdraw (APPNOTIFY_Dummy + 14) /* STRPTR */ +/* AROS: bars under the text, one per line: ";#RRGGBB;