1206 lines
31 KiB
C++
1206 lines
31 KiB
C++
// this file may be easier to customize if included directly from other code. Some things that can be defined to hook:
|
|
//
|
|
// #define WDL_LOCALIZE_HOOK_ALLOW_CACHE (ismainthread())
|
|
// #define WDL_LOCALIZE_HOOK_DLGPROC if (is_modal) return __localDlgProcModalPos; // can return a temporary dlgproc override
|
|
// #define WDL_LOCALIZE_HOOK_XLATE(str,subctx,flags,newptr) // can be used to override/tweak translations (newptr str is the original string, newptr is the translated string (or NULL) )
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#else
|
|
#include "../swell/swell.h"
|
|
#endif
|
|
|
|
#ifndef LOCALIZE_NO_DIALOG_MENU_REDEF
|
|
#define LOCALIZE_NO_DIALOG_MENU_REDEF
|
|
#endif
|
|
#include "localize.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include "../assocarray.h"
|
|
#include "../ptrlist.h"
|
|
#include "../chunkalloc.h"
|
|
#include "../fnv64.h"
|
|
#include "../win32_utf8.h"
|
|
#include "../wdlcstring.h"
|
|
|
|
|
|
#define LANGPACK_SCALE_CONSTANT WDL_UINT64_CONST(0x5CA1E00000000000)
|
|
static WDL_StringKeyedArray< WDL_AssocArray<WDL_UINT64, char *> * > g_translations;
|
|
static WDL_AssocArray<WDL_UINT64, char *> *g_translations_commonsec;
|
|
|
|
static bool isPrintfModifier(char c)
|
|
{
|
|
switch (c)
|
|
{
|
|
case '.':
|
|
case '-':
|
|
case '+':
|
|
case '#':
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
case ' ':
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static const char *next_format(const char *p)
|
|
{
|
|
for (;;)
|
|
{
|
|
switch (p[0])
|
|
{
|
|
case 0: return p;
|
|
case '%':
|
|
if (p[1] != '%') return p;
|
|
p++;
|
|
WDL_FALLTHROUGH;
|
|
default:
|
|
p++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool validateStrs(const char *stock, const char *newstr, int flags)
|
|
{
|
|
if (!(flags&LOCALIZE_FLAG_VERIFY_FMTS)) return true;
|
|
|
|
for (;;)
|
|
{
|
|
newstr = next_format(newstr);
|
|
if (!newstr[0]) return true; // not enough formats in new string, safe
|
|
stock = next_format(stock);
|
|
if (!stock[0]) return false; // not enough formats in original string, unsafe
|
|
|
|
// compare formats
|
|
newstr++;
|
|
stock++;
|
|
|
|
do
|
|
{
|
|
while (isPrintfModifier(*newstr)) newstr++;
|
|
while (isPrintfModifier(*stock)) stock++;
|
|
if (*newstr != *stock) return false;
|
|
newstr++;
|
|
stock++;
|
|
} while (newstr[-1] == '*' || newstr[-1] == 'h' || newstr[-1] == 'l' || newstr[-1] == 'L');
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static char *ChunkAlloc(int len)
|
|
{
|
|
#ifdef _DEBUG
|
|
static WDL_ChunkAlloc backing(15000); // will free on exit (for debug mode)
|
|
return (char*)backing.Alloc(len);
|
|
#else
|
|
static WDL_ChunkAlloc *backing;
|
|
if (!backing) backing = new WDL_ChunkAlloc(15000);
|
|
return (char*)backing->Alloc(len);
|
|
#endif
|
|
}
|
|
|
|
#ifdef _DEBUG
|
|
bool g_debug_langpack_has_loaded;
|
|
|
|
class localizeValidatePtrCacheRec {
|
|
public:
|
|
localizeValidatePtrCacheRec(const char *subctx, const char *str, const char *ctx)
|
|
{
|
|
//printf("saving pointers for validation for %s (%s)\n",str,ctx);
|
|
m_str_copy=strdup(m_str=str);
|
|
m_sub_copy=strdup(m_sub=subctx);
|
|
m_ctx=ctx;
|
|
}
|
|
~localizeValidatePtrCacheRec()
|
|
{
|
|
//printf("validating string %s\n",m_str_copy);
|
|
WDL_ASSERT(!strcmp(m_sub,m_sub_copy) /* if this triggers, then a caller called __LOCALIZE without NOCACHE on a non-static or unloaded string */ );
|
|
WDL_ASSERT(!strcmp(m_str,m_str_copy) /* if this triggers, then a caller called __LOCALIZE without NOCACHE on a non-static or unloaded string */ );
|
|
}
|
|
char *m_sub_copy, *m_str_copy;
|
|
const char *m_sub, *m_str, *m_ctx;
|
|
};
|
|
static WDL_PtrList_DeleteOnDestroy<localizeValidatePtrCacheRec> s_debug_validateptrcache;
|
|
#define LANGPACK_DEBUG_SAVE_POINTERS_FOR_VALIDATION(subctx,str,ctx) \
|
|
s_debug_validateptrcache.Add(new localizeValidatePtrCacheRec(subctx,str,ctx));
|
|
#else
|
|
#define LANGPACK_DEBUG_SAVE_POINTERS_FOR_VALIDATION(subctx,str,ctx)
|
|
#endif
|
|
|
|
const char *__localizeFunc(const char *str, const char *subctx, int flags)
|
|
{
|
|
#ifdef _DEBUG
|
|
WDL_ASSERT(g_debug_langpack_has_loaded != false);
|
|
#endif
|
|
if (WDL_NOT_NORMALLY(!str || !subctx || !subctx[0])) return str;
|
|
if (!g_translations.GetSize())
|
|
{
|
|
#ifdef WDL_LOCALIZE_HOOK_XLATE
|
|
char *newptr=NULL;
|
|
WDL_LOCALIZE_HOOK_XLATE(str,subctx,flags,newptr)
|
|
if (newptr) return newptr;
|
|
#endif
|
|
return str;
|
|
}
|
|
|
|
#ifdef WDL_LOCALIZE_HOOK_ALLOW_CACHE
|
|
const bool cache_lookups = !(flags&LOCALIZE_FLAG_NOCACHE) && WDL_LOCALIZE_HOOK_ALLOW_CACHE;
|
|
#else
|
|
const bool cache_lookups = !(flags&LOCALIZE_FLAG_NOCACHE);
|
|
#endif
|
|
|
|
#ifdef BENCHMARK_I8N
|
|
static int stats[2];
|
|
static double sumtimes[2]; // [cached]
|
|
double startTime=time_precise();
|
|
#endif
|
|
|
|
static WDL_PtrKeyedArray< WDL_PtrKeyedArray<char *> * > ptrcache;
|
|
WDL_PtrKeyedArray<char *> *sc = NULL;
|
|
|
|
if (cache_lookups)
|
|
{
|
|
sc = ptrcache.Get((INT_PTR)subctx);
|
|
if (sc)
|
|
{
|
|
const char *ret = sc->Get((INT_PTR)str);
|
|
if (ret)
|
|
{
|
|
#ifdef BENCHMARK_I8N
|
|
stats[1]++;
|
|
startTime = time_precise()-startTime;
|
|
sumtimes[1]+=startTime;
|
|
if (!(stats[1]%100))
|
|
{
|
|
wdl_log("cached, avg was %fuS\n",sumtimes[1]*10000.0);
|
|
sumtimes[1]=0;
|
|
}
|
|
#endif
|
|
|
|
if (ret == (const char *)(INT_PTR)1) return str;
|
|
return ret;
|
|
}
|
|
}
|
|
}
|
|
|
|
char *newptr = NULL;
|
|
|
|
int trycnt;
|
|
size_t len = strlen(str)+1;
|
|
|
|
if (flags & LOCALIZE_FLAG_DOUBLENULL)
|
|
{
|
|
// need to test this
|
|
for (;;)
|
|
{
|
|
size_t a = strlen(str+len);
|
|
if (!a) break;
|
|
len += a+1;
|
|
}
|
|
len++;
|
|
}
|
|
else if (flags & LOCALIZE_FLAG_PAIR)
|
|
{
|
|
len += strlen(str + len) + 1;
|
|
}
|
|
|
|
WDL_UINT64 hash;
|
|
if ((flags & LOCALIZE_FLAG_PAIR) && len == 18 && !memcmp(str,"__LOCALIZE_SCALE\0",18))
|
|
hash = WDL_UINT64_CONST(0x5CA1E00000000000);
|
|
else
|
|
hash = WDL_FNV64(WDL_FNV64_IV,(const unsigned char *)str,(int)len);
|
|
|
|
for (trycnt=0;trycnt<2 && !newptr;trycnt++)
|
|
{
|
|
WDL_AssocArray<WDL_UINT64, char *> *section = trycnt == 1 ? g_translations_commonsec : g_translations.Get(subctx);
|
|
|
|
if (section)
|
|
{
|
|
newptr = section->Get(hash,0);
|
|
if (newptr && !validateStrs(str,newptr,flags)) newptr=NULL;
|
|
}
|
|
}
|
|
|
|
#ifdef WDL_LOCALIZE_HOOK_XLATE
|
|
WDL_LOCALIZE_HOOK_XLATE(str,subctx,flags,newptr)
|
|
#endif
|
|
|
|
if (cache_lookups)
|
|
{
|
|
if (!sc)
|
|
{
|
|
sc = new WDL_PtrKeyedArray<char *>;
|
|
ptrcache.Insert((INT_PTR)subctx,sc);
|
|
}
|
|
sc->Insert((INT_PTR)str,newptr ? newptr : (char*)(INT_PTR)1); // update pointer cache for fast lookup
|
|
LANGPACK_DEBUG_SAVE_POINTERS_FOR_VALIDATION(subctx,str,"")
|
|
}
|
|
#ifdef BENCHMARK_I8N
|
|
stats[0]++;
|
|
startTime = time_precise()-startTime;
|
|
sumtimes[0]+=startTime;
|
|
if (!(stats[0]%100))
|
|
{
|
|
wdl_log("uncached , avg was %f\n",sumtimes[0]*10000.0);
|
|
sumtimes[0]=0;
|
|
}
|
|
#endif
|
|
|
|
return newptr?newptr:str;
|
|
}
|
|
|
|
static void __localProcMenu(HMENU menu, WDL_AssocArray<WDL_UINT64, char *> *s)
|
|
{
|
|
int n = GetMenuItemCount(menu);
|
|
int x;
|
|
char buf[4096];
|
|
for(x=0;x<n;x++)
|
|
{
|
|
buf[0]=0;
|
|
MENUITEMINFO mii={sizeof(mii),MIIM_TYPE|MIIM_SUBMENU,0,0,0,0,0,0,0,buf,sizeof(buf)};
|
|
if (GetMenuItemInfo(menu,x,TRUE,&mii))
|
|
{
|
|
if (mii.fType == MFT_STRING)
|
|
{
|
|
buf[sizeof(buf)-1]=0;
|
|
|
|
#ifdef _WIN32
|
|
char mod[512];
|
|
mod[0]=0;
|
|
tryagain:
|
|
#endif
|
|
WDL_UINT64 hash = WDL_FNV64(WDL_FNV64_IV,(const unsigned char *)buf,(int)strlen(buf)+1);
|
|
const char *newptr = s ? s->Get(hash,0) : NULL;
|
|
if (!newptr && g_translations_commonsec) newptr = g_translations_commonsec->Get(hash,0);
|
|
|
|
if (!newptr)
|
|
{
|
|
#ifdef _WIN32
|
|
char *p =buf;
|
|
while (*p && *p != '\t') p++;
|
|
lstrcpyn(mod,p,sizeof(mod));
|
|
if (*p)
|
|
{
|
|
*p++=0;
|
|
goto tryagain;
|
|
}
|
|
#endif
|
|
|
|
}
|
|
if (newptr)
|
|
{
|
|
#ifdef _WIN32
|
|
if (mod[0])
|
|
{
|
|
lstrcpyn(buf,newptr,3500);
|
|
strcat(buf,mod);
|
|
newptr=buf;
|
|
}
|
|
#endif
|
|
mii.fMask = MIIM_TYPE;
|
|
#ifdef __APPLE__
|
|
mii.fMask |= MIIM_SWELL_DO_NOT_CALC_MODIFIERS;
|
|
#endif
|
|
mii.dwTypeData = (char*)newptr;
|
|
SetMenuItemInfo(menu,x,TRUE,&mii);
|
|
}
|
|
}
|
|
|
|
if (mii.hSubMenu) __localProcMenu(mii.hSubMenu,s);
|
|
}
|
|
}
|
|
}
|
|
|
|
void __localizeMenu(const char *rescat, HMENU hMenu, LPCSTR lpMenuName)
|
|
{
|
|
#ifdef _DEBUG
|
|
WDL_ASSERT(g_debug_langpack_has_loaded != false);
|
|
#endif
|
|
INT_PTR a = (INT_PTR) lpMenuName;
|
|
if (hMenu && a>0&&a<65536)
|
|
{
|
|
char buf[128];
|
|
sprintf(buf,"%sMENU_%d",rescat?rescat:"",(int)a);
|
|
WDL_AssocArray<WDL_UINT64, char *> *s = g_translations.Get(buf);
|
|
if (s)
|
|
{
|
|
__localProcMenu(hMenu,s);
|
|
}
|
|
}
|
|
}
|
|
|
|
HMENU __localizeLoadMenu(HINSTANCE hInstance, LPCSTR lpMenuName)
|
|
{
|
|
HMENU ret = LoadMenu(hInstance,lpMenuName);
|
|
if (ret) __localizeMenu(NULL,ret,lpMenuName);
|
|
return ret;
|
|
}
|
|
|
|
struct windowReorgEnt
|
|
{
|
|
enum windowReorgEntType
|
|
{
|
|
WRET_GROUP=0,
|
|
WRET_SIZEADJ,
|
|
WRET_MISC, // dont analyze for size changes, but move around
|
|
|
|
};
|
|
windowReorgEnt(HWND _hwnd, RECT _r, int wc)
|
|
{
|
|
hwnd=_hwnd;
|
|
orig_r=r=_r;
|
|
mode=WRET_MISC;
|
|
move_amt=0;
|
|
wantsizeincrease=0;
|
|
scaled_width_change = wc;
|
|
}
|
|
~windowReorgEnt() { }
|
|
|
|
HWND hwnd;
|
|
RECT r,orig_r;
|
|
windowReorgEntType mode;
|
|
int move_amt;
|
|
int wantsizeincrease;
|
|
int scaled_width_change;
|
|
|
|
static int Sort(const void *_a, const void *_b)
|
|
{
|
|
const windowReorgEnt *a = (const windowReorgEnt*)_a;
|
|
const windowReorgEnt *b = (const windowReorgEnt*)_b;
|
|
|
|
if (a->r.left < b->r.left) return -1;
|
|
if (a->r.left > b->r.left) return 1;
|
|
|
|
if (a->r.top < b->r.top) return -1;
|
|
if (a->r.top > b->r.top) return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
};
|
|
class windowReorgState
|
|
{
|
|
public:
|
|
|
|
windowReorgState(HWND _par, float _scx, float _scy) : par(_par), scx(_scx), scy(_scy)
|
|
{
|
|
RECT cr;
|
|
GetClientRect(_par,&cr);
|
|
|
|
par_cr = cr;
|
|
if (scx != 1.0f) par_cr.right = (int) (par_cr.right * scx + 0.5);
|
|
if (scy != 1.0f) par_cr.bottom = (int) (par_cr.bottom * scy + 0.5);
|
|
|
|
has_sc = (cr.right != par_cr.right || cr.bottom != par_cr.bottom);
|
|
|
|
if (has_sc)
|
|
{
|
|
RECT wr;
|
|
if (GetWindowLong(_par,GWL_STYLE)&WS_CHILD) wr=cr;
|
|
else GetWindowRect(_par,&wr);
|
|
|
|
SetWindowPos(_par,NULL,0,0,
|
|
(par_cr.right - cr.right) + (wr.right-wr.left),
|
|
(par_cr.bottom - cr.bottom) + (wr.bottom-wr.top),
|
|
SWP_NOMOVE|SWP_NOACTIVATE|SWP_NOZORDER);
|
|
}
|
|
}
|
|
~windowReorgState() { }
|
|
|
|
WDL_TypedBuf<windowReorgEnt> cws;
|
|
WDL_IntKeyedArray<int> columns; // map l|(r<<16) -> maps to size increase
|
|
RECT par_cr;
|
|
|
|
HWND par;
|
|
float scx,scy;
|
|
bool has_sc;
|
|
};
|
|
|
|
static const char *xlateWindow(HWND hwnd, WDL_AssocArray<WDL_UINT64, char *> *s, char *buf, int bufsz, bool prefix_handling)
|
|
{
|
|
buf[0]=0;
|
|
GetWindowText(hwnd,buf,bufsz);
|
|
if (buf[0])
|
|
{
|
|
buf[bufsz-1]=0;
|
|
WDL_UINT64 hash = WDL_FNV64(WDL_FNV64_IV,(const unsigned char *)buf,(int)strlen(buf)+1);
|
|
const char *newptr = s ? s->Get(hash,0) : NULL;
|
|
if (!newptr && g_translations_commonsec) newptr = g_translations_commonsec->Get(hash,0);
|
|
|
|
#ifdef __APPLE__
|
|
bool filter_prefix = false;
|
|
if (!newptr && prefix_handling)
|
|
{
|
|
extern const char *SWELL_GetRecentPrefixRemoval(const char *p);
|
|
const char *p = SWELL_GetRecentPrefixRemoval(buf);
|
|
if (p)
|
|
{
|
|
hash = WDL_FNV64(WDL_FNV64_IV,(const unsigned char *)p,(int)strlen(p)+1);
|
|
newptr = s ? s->Get(hash,0) : NULL;
|
|
if (!newptr && g_translations_commonsec) newptr = g_translations_commonsec->Get(hash,0);
|
|
filter_prefix = true;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (newptr && strcmp(newptr,buf))
|
|
{
|
|
#ifdef __APPLE__
|
|
if (filter_prefix)
|
|
{
|
|
const char *rd=newptr;
|
|
int widx=0;
|
|
while (widx < bufsz-1)
|
|
{
|
|
if (*rd == '&') rd++;
|
|
if (!*rd) break;
|
|
buf[widx++]=*rd++;
|
|
}
|
|
buf[widx]=0;
|
|
SetWindowText(hwnd,buf);
|
|
return newptr;
|
|
}
|
|
#endif
|
|
SetWindowText(hwnd,newptr);
|
|
return newptr;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static BOOL CALLBACK xlateGetRects(HWND hwnd, LPARAM lParam)
|
|
{
|
|
windowReorgState *s=(windowReorgState*)lParam;
|
|
|
|
if (GetParent(hwnd) != s->par) return TRUE;
|
|
|
|
RECT r;
|
|
GetWindowRect(hwnd,&r);
|
|
ScreenToClient(s->par,(LPPOINT)&r);
|
|
ScreenToClient(s->par,((LPPOINT)&r)+1);
|
|
int width_change = 0;
|
|
|
|
if (s->has_sc) // scaling happens before all of the ripple-code
|
|
{
|
|
if (r.top > r.bottom) { const int t = r.top; r.top = r.bottom; r.bottom = t; }
|
|
|
|
width_change = r.right-r.left;
|
|
r.left = (int) (r.left * s->scx + 0.5);
|
|
r.top = (int) (r.top * s->scy + 0.5);
|
|
r.right = (int) (r.right * s->scx + 0.5);
|
|
r.bottom = (int) (r.bottom * s->scy + 0.5);
|
|
SetWindowPos(hwnd,NULL, r.left,r.top, r.right-r.left, r.bottom-r.top, SWP_NOACTIVATE|SWP_NOZORDER);
|
|
width_change = (r.right-r.left) - width_change;
|
|
}
|
|
|
|
windowReorgEnt t(hwnd,r,width_change);
|
|
|
|
#ifdef _WIN32
|
|
char buf[128];
|
|
buf[0]=0;
|
|
GetClassName(hwnd,buf,sizeof(buf));
|
|
if (!strcmp(buf,"Button"))
|
|
{
|
|
LONG style=GetWindowLong(hwnd,GWL_STYLE);
|
|
if (LOWORD(style)==BS_GROUPBOX) t.mode = windowReorgEnt::WRET_GROUP;
|
|
else t.mode = windowReorgEnt::WRET_SIZEADJ;
|
|
}
|
|
else if (!strcmp(buf,"Static"))
|
|
{
|
|
t.mode = windowReorgEnt::WRET_SIZEADJ;
|
|
}
|
|
#else
|
|
if (SWELL_IsGroupBox(hwnd)) t.mode = windowReorgEnt::WRET_GROUP;
|
|
else if (SWELL_IsButton(hwnd)||SWELL_IsStaticText(hwnd)) t.mode = windowReorgEnt::WRET_SIZEADJ;
|
|
#endif
|
|
|
|
if (t.mode == windowReorgEnt::WRET_GROUP)
|
|
{
|
|
// first copy is the left side of the group, with hwnd
|
|
int a = t.r.right;
|
|
t.r.right = t.r.left+4;
|
|
t.orig_r=t.r;
|
|
s->cws.Add(t);
|
|
|
|
// add a second copy, which is the right side but no hwnd
|
|
t.r.right = a;
|
|
t.r.left = a-4;
|
|
t.hwnd=0;
|
|
}
|
|
t.orig_r=t.r;
|
|
s->cws.Add(t);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static int rippleControlsRight(HWND hwnd, const RECT *srcR, windowReorgEnt *ent, int ent_cnt, int dSize, int clientw)
|
|
{
|
|
// limit first to client rectangle
|
|
int space = clientw - 6 - srcR->right;
|
|
if (dSize>space) dSize=space;
|
|
|
|
while (ent_cnt>0 && dSize>0)
|
|
{
|
|
// if the two items overlap by more than 1px initially, dont make them ripple (it is assumed they are allowed, maybe one is hidden)
|
|
#define LOCALIZE_ISECT_PAIRS(x1,x2,y1,y2) \
|
|
((x1 >= y1 && x1 < y2) || (x2 >= y1 && x2 < y2) || \
|
|
(y1 >= x1 && y1 < x2) || (y2 >= x1 && y2 < x2))
|
|
if (ent->r.left >= srcR->right-1 && LOCALIZE_ISECT_PAIRS(srcR->top,srcR->bottom,ent->r.top,ent->r.bottom))
|
|
{
|
|
space = ent->r.left - srcR->right;
|
|
|
|
if (ent->mode == windowReorgEnt::WRET_GROUP)
|
|
{
|
|
if (dSize>space) dSize=space;
|
|
}
|
|
else
|
|
{
|
|
int needAmt = dSize - (space-2 /* border */);
|
|
if (needAmt>0)
|
|
{
|
|
int got = rippleControlsRight(ent->hwnd,&ent->r,ent+1,ent_cnt-1,needAmt,clientw);
|
|
dSize -= needAmt - got;
|
|
if (ent->move_amt < got) ent->move_amt=got;
|
|
}
|
|
}
|
|
}
|
|
|
|
ent_cnt--;
|
|
ent++;
|
|
}
|
|
return dSize;
|
|
}
|
|
|
|
static void localize_dialog(HWND hwnd, WDL_AssocArray<WDL_UINT64, char *> *sec)
|
|
{
|
|
#ifdef _DEBUG
|
|
WDL_ASSERT(g_debug_langpack_has_loaded != false);
|
|
#endif
|
|
const char *sc_str = sec->Get(LANGPACK_SCALE_CONSTANT);
|
|
/* commented [common] scaling fallback: the langpack author has to set it for each dialog that needs it
|
|
if (!sc_str && g_translations_commonsec)
|
|
{
|
|
sc_str = g_translations_commonsec->Get(LANGPACK_SCALE_CONSTANT);
|
|
}
|
|
*/
|
|
float scx = 1.0, scy = 1.0;
|
|
if (sc_str)
|
|
{
|
|
while (*sc_str && (*sc_str == ' ' || *sc_str == '\t')) sc_str++;
|
|
|
|
#ifdef WDL_HOOK_LOCALIZE_ATOF
|
|
float v = (float) WDL_HOOK_LOCALIZE_ATOF(sc_str);
|
|
#else
|
|
float v = (float) atof(sc_str);
|
|
#endif
|
|
if (v > 0.1 && v < 8.0)
|
|
{
|
|
scx=v;
|
|
while (*sc_str && *sc_str != ' ' && *sc_str != '\t') sc_str++;
|
|
if (*sc_str)
|
|
{
|
|
#ifdef WDL_HOOK_LOCALIZE_ATOF
|
|
v = (float)WDL_HOOK_LOCALIZE_ATOF(sc_str);
|
|
#else
|
|
v = (float)atof(sc_str);
|
|
#endif
|
|
if (v > 0.1 && v < 8.0)
|
|
scy = v;
|
|
}
|
|
}
|
|
}
|
|
|
|
windowReorgState s(hwnd,scx,scy);
|
|
|
|
char buf[8192];
|
|
xlateWindow(hwnd,sec,buf,sizeof(buf),false); // translate window title
|
|
EnumChildWindows(hwnd,xlateGetRects,(LPARAM)&s);
|
|
|
|
#ifdef _WIN32
|
|
HDC hdc=GetDC(hwnd);
|
|
HGDIOBJ oldFont=0;
|
|
HFONT font = (HFONT)SendMessage(hwnd,WM_GETFONT,0,0);
|
|
if (font) oldFont=SelectObject(hdc,font);
|
|
#endif
|
|
|
|
int x;
|
|
const bool do_columns = true;
|
|
for(x=0;x<s.cws.GetSize();x++)
|
|
{
|
|
windowReorgEnt *rec=s.cws.Get()+x;
|
|
if (rec->hwnd)
|
|
{
|
|
const char *newText=xlateWindow(rec->hwnd,sec,buf,sizeof(buf), rec->mode != windowReorgEnt::WRET_MISC);
|
|
if (newText && rec->mode == windowReorgEnt::WRET_SIZEADJ)
|
|
{
|
|
RECT r1={0},r2={0};
|
|
#ifdef _WIN32
|
|
DrawText(hdc,buf,-1,&r1,DT_CALCRECT);
|
|
DrawText(hdc,newText,-1,&r2,DT_CALCRECT);
|
|
r1.right += rec->scaled_width_change;
|
|
#else
|
|
GetClientRect(rec->hwnd,&r1);
|
|
SWELL_GetDesiredControlSize(rec->hwnd,&r2);
|
|
#endif
|
|
int dSize=r2.right-r1.right;
|
|
if (dSize>0)
|
|
{
|
|
rec->wantsizeincrease = ++dSize;
|
|
|
|
if (do_columns)
|
|
{
|
|
const int v = (rec->r.right<<16) | (rec->r.left&0xffff);
|
|
int *diff = s.columns.GetPtr(v);
|
|
if (!diff) s.columns.Insert(v,dSize);
|
|
else if (dSize > *diff) *diff = dSize;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (do_columns) for(x=0;x<s.cws.GetSize();x++)
|
|
{
|
|
windowReorgEnt *rec=s.cws.Get()+x;
|
|
if (rec->hwnd && rec->mode == windowReorgEnt::WRET_SIZEADJ)
|
|
{
|
|
const int v = (rec->r.right<<16) | (rec->r.left&0xffff);
|
|
const int *diff = s.columns.GetPtr(v);
|
|
if (diff) rec->wantsizeincrease = *diff;
|
|
}
|
|
}
|
|
|
|
int swSizeInc=0;
|
|
|
|
do
|
|
{
|
|
qsort(s.cws.Get(),s.cws.GetSize(),sizeof(*s.cws.Get()),windowReorgEnt::Sort);
|
|
for(x=0;x<s.cws.GetSize();x++)
|
|
{
|
|
windowReorgEnt *trec=s.cws.Get()+x;
|
|
if (trec->wantsizeincrease>0)
|
|
{
|
|
int amt = rippleControlsRight(trec->hwnd,&trec->r,trec+1,s.cws.GetSize() - (x+1),trec->wantsizeincrease,s.par_cr.right);
|
|
if (amt>0)
|
|
{
|
|
trec->wantsizeincrease -= amt;
|
|
trec->r.right += amt;
|
|
}
|
|
int y;
|
|
for(y=x+1;y<s.cws.GetSize();y++)
|
|
{
|
|
windowReorgEnt *rec=s.cws.Get()+y;
|
|
int a = min(amt,rec->move_amt);
|
|
if (a>0)
|
|
{
|
|
rec->r.left += a;
|
|
rec->r.right += a;
|
|
}
|
|
rec->move_amt=0;
|
|
}
|
|
if (!swSizeInc && trec->wantsizeincrease>0) swSizeInc=1;
|
|
}
|
|
}
|
|
|
|
if (swSizeInc++)
|
|
{
|
|
// flip everything
|
|
int w=s.par_cr.right;
|
|
for(x=0;x<s.cws.GetSize();x++)
|
|
{
|
|
windowReorgEnt *rec=s.cws.Get()+x;
|
|
int a = w-1-rec->r.left;
|
|
int b = w-1-rec->r.right;
|
|
rec->r.left = b;
|
|
rec->r.right = a;
|
|
}
|
|
}
|
|
} while (swSizeInc == 2);
|
|
|
|
|
|
for(x=0;x<s.cws.GetSize();x++)
|
|
{
|
|
windowReorgEnt *rec=s.cws.Get()+x;
|
|
if (rec->hwnd)
|
|
{
|
|
bool wantMove = rec->r.left != rec->orig_r.left;
|
|
bool wantSize = (rec->r.right-rec->r.left) != (rec->orig_r.right-rec->orig_r.left);
|
|
if (wantMove||wantSize)
|
|
{
|
|
SetWindowPos(rec->hwnd,NULL,rec->r.left,rec->r.top,rec->r.right-rec->r.left,rec->r.bottom-rec->r.top,
|
|
(wantSize?0:SWP_NOSIZE)|(wantMove?0:SWP_NOMOVE)|SWP_NOZORDER|SWP_NOACTIVATE);
|
|
}
|
|
}
|
|
}
|
|
#ifdef _WIN32
|
|
if (oldFont) SelectObject(hdc,oldFont);
|
|
ReleaseDC(hwnd,hdc);
|
|
#endif
|
|
}
|
|
|
|
void __localizeInitializeDialog(HWND hwnd, const char *desc)
|
|
{
|
|
if (!desc || !hwnd || !*desc) return;
|
|
WDL_AssocArray<WDL_UINT64, char *> *s = g_translations.Get(desc);
|
|
if (s) localize_dialog(hwnd,s);
|
|
}
|
|
|
|
void (*localizePreInitDialogHook)(HWND hwndDlg);
|
|
|
|
static WDL_DLGRET __localDlgProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
|
|
{
|
|
switch (uMsg)
|
|
{
|
|
case WM_INITDIALOG:
|
|
{
|
|
void **l = (void **)lParam;
|
|
|
|
if (localizePreInitDialogHook)
|
|
localizePreInitDialogHook(hwnd);
|
|
|
|
if (l[2])
|
|
localize_dialog(hwnd,(WDL_AssocArray<WDL_UINT64, char *> *)l[2]);
|
|
|
|
#ifdef _WIN32
|
|
{
|
|
// if not a child window and has a menu set, localize our menu
|
|
WDL_AssocArray<WDL_UINT64, char *> *s = (WDL_AssocArray<WDL_UINT64, char *> *)l[3];
|
|
if (s && !(GetWindowLong(hwnd,GWL_STYLE)&WS_CHILD))
|
|
{
|
|
HMENU hmenu = GetMenu(hwnd);
|
|
if (hmenu) __localProcMenu(hmenu,s);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
DLGPROC newproc = (DLGPROC) l[0];
|
|
SetWindowLongPtr(hwnd,DWLP_DLGPROC,(LRESULT) newproc);
|
|
|
|
return newproc(hwnd,uMsg,wParam,(LPARAM)l[1]);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
static WORD __getMenuIdFromDlgResource(HINSTANCE hInstance, const char * lpTemplate)
|
|
{
|
|
HRSRC h=FindResource(hInstance,lpTemplate,RT_DIALOG);
|
|
if (!h) return 0;
|
|
DWORD sz = SizeofResource(hInstance, h);
|
|
if (sz<30) return 0;
|
|
|
|
HGLOBAL hg=LoadResource(hInstance,h);
|
|
if (!hg) return 0;
|
|
WORD *p = (WORD *)LockResource(hg);
|
|
if (!p) return 0;
|
|
|
|
if (p[0]!=1 || p[1] != 0xffff) return 0; // not extended dialog template
|
|
if (p[13]==0) return 0;
|
|
if (p[13]==0xffff) return p[14];
|
|
|
|
//otherwise it is a string, but we dont support(or use) that
|
|
return 0;
|
|
|
|
}
|
|
#endif
|
|
|
|
DLGPROC __localizePrepareDialog(const char *rescat, HINSTANCE hInstance, const char *lpTemplate, DLGPROC dlgProc, LPARAM lParam, void **ptrs, int nptrs)
|
|
{
|
|
INT_PTR a = (INT_PTR) lpTemplate;
|
|
if (WDL_NOT_NORMALLY(nptrs<4)) return NULL;
|
|
|
|
WDL_AssocArray<WDL_UINT64, char *> *s = NULL, *s2 = NULL;
|
|
if (a>0&&a<65536)
|
|
{
|
|
char buf[128];
|
|
snprintf(buf,sizeof(buf),"%sDLG_%d",rescat?rescat:"",(int)a);
|
|
s = g_translations.Get(buf);
|
|
#ifdef _WIN32
|
|
int menuid = __getMenuIdFromDlgResource(hInstance,lpTemplate);
|
|
if (menuid)
|
|
{
|
|
snprintf(buf,sizeof(buf),"%sMENU_%d",rescat?rescat:"",menuid);
|
|
s2=g_translations.Get(buf);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
ptrs[0] = (void*)dlgProc;
|
|
ptrs[1] = (void*)(INT_PTR)lParam;
|
|
ptrs[2] = s;
|
|
ptrs[3] = s2;
|
|
#ifdef WDL_LOCALIZE_HOOK_DLGPROC
|
|
WDL_LOCALIZE_HOOK_DLGPROC
|
|
#endif
|
|
return (s||s2||(a>0 && localizePreInitDialogHook)) ? __localDlgProc : NULL;
|
|
}
|
|
|
|
HWND __localizeDialog(HINSTANCE hInstance, const char *lpTemplate, HWND hwndParent, DLGPROC dlgProc, LPARAM lParam, int mode)
|
|
{
|
|
void *p[5];
|
|
char tmp[256];
|
|
if (mode == 1)
|
|
{
|
|
p[4] = tmp;
|
|
snprintf(tmp,sizeof(tmp),"DLG%d",(int)(INT_PTR)lpTemplate);
|
|
}
|
|
else
|
|
p[4] = NULL;
|
|
DLGPROC newDlg = __localizePrepareDialog(NULL,hInstance,lpTemplate,dlgProc,lParam,p,sizeof(p)/sizeof(p[0]));
|
|
if (newDlg)
|
|
{
|
|
dlgProc = newDlg;
|
|
lParam = (LPARAM)(INT_PTR)p;
|
|
}
|
|
switch (mode)
|
|
{
|
|
case 0: return CreateDialogParam(hInstance,lpTemplate,hwndParent,dlgProc,lParam);
|
|
case 1: return (HWND) (INT_PTR)DialogBoxParam(hInstance,lpTemplate,hwndParent,dlgProc,lParam);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int uint64cmpfunc(WDL_UINT64 *a, WDL_UINT64 *b)
|
|
{
|
|
if (*a < *b) return -1;
|
|
if (*a > *b) return 1;
|
|
return 0;
|
|
}
|
|
|
|
// call at start of file with format_flag=-1
|
|
// expect possible blank (\r or \n) lines, too, ignore them
|
|
// format flag will get set to: 0=utf8, 1=utf16le, 2=utf16be, 3=ansi
|
|
void WDL_fgets_as_utf8(char *linebuf, int linebuf_size, FILE *fp, int *format_flag)
|
|
{
|
|
linebuf[0]=0;
|
|
if (*format_flag>0)
|
|
{
|
|
int sz=0;
|
|
while (sz < linebuf_size-8)
|
|
{
|
|
int a = fgetc(fp);
|
|
int b = *format_flag==3 ? 0 : fgetc(fp);
|
|
if (a<0 || b<0) break;
|
|
if (*format_flag==2) a = (a<<8)+b;
|
|
else a += b<<8;
|
|
|
|
|
|
again:
|
|
if (a >= 0xD800 && a < 0xDC00) // UTF-16 supplementary planes
|
|
{
|
|
int aa = fgetc(fp);
|
|
int bb = fgetc(fp);
|
|
if (aa < 0 || bb < 0) break;
|
|
|
|
if (*format_flag==2) aa = (aa<<8)+bb;
|
|
else aa += bb<<8;
|
|
|
|
if (aa >= 0xDC00 && aa < 0xE000)
|
|
{
|
|
a = 0x10000 + ((a&0x3FF)<<10) + (aa&0x3FF);
|
|
}
|
|
else
|
|
{
|
|
a=aa;
|
|
goto again;
|
|
}
|
|
}
|
|
|
|
|
|
if (a < 0x80) linebuf[sz++] = a;
|
|
else
|
|
{
|
|
if (a<0x800) // 11 bits (5+6 bits)
|
|
{
|
|
linebuf[sz++] = 0xC0 + ((a>>6)&31);
|
|
}
|
|
else
|
|
{
|
|
if (a < 0x10000) // 16 bits (4+6+6 bits)
|
|
{
|
|
linebuf[sz++] = 0xE0 + ((a>>12)&15); // 4 bits
|
|
}
|
|
else // 21 bits yow
|
|
{
|
|
linebuf[sz++] = 0xF0 + ((a>>18)&7); // 3 bits
|
|
linebuf[sz++] = 0x80 + ((a>>12)&63); // 6 bits
|
|
}
|
|
linebuf[sz++] = 0x80 + ((a>>6)&63); // 6 bits
|
|
}
|
|
linebuf[sz++] = 0x80 + (a&63); // 6 bits
|
|
}
|
|
|
|
if (a == '\n') break;
|
|
}
|
|
linebuf[sz]=0;
|
|
}
|
|
else
|
|
{
|
|
fgets(linebuf,linebuf_size,fp);
|
|
}
|
|
|
|
if (linebuf[0] && *format_flag<0)
|
|
{
|
|
unsigned char *p=(unsigned char *)linebuf;
|
|
if (p[0] == 0xEF && p[1] == 0xBB && p[2] == 0xBf)
|
|
{
|
|
*format_flag=0;
|
|
memmove(linebuf,linebuf+3,strlen(linebuf+3)+1); // skip UTF-8 BOM
|
|
}
|
|
else if ((p[0] == 0xFF && p[1] == 0xFE) || (p[0] == 0xFE && p[1] == 0xFF))
|
|
{
|
|
fseek(fp,2,SEEK_SET);
|
|
*format_flag=p[0] == 0xFE ? 2 : 1;
|
|
WDL_fgets_as_utf8(linebuf,linebuf_size,fp,format_flag);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
for (;;)
|
|
{
|
|
const unsigned char *str=(unsigned char *)linebuf;
|
|
while (*str)
|
|
{
|
|
unsigned char c = *str++;
|
|
if (c >= 0xC0)
|
|
{
|
|
if (c <= 0xDF && str[0] >=0x80 && str[0] <= 0xBF) str++;
|
|
else if (c <= 0xEF && str[0] >=0x80 && str[0] <= 0xBF && str[1] >=0x80 && str[1] <= 0xBF) str+=2;
|
|
else if (c <= 0xF7 &&
|
|
str[0] >=0x80 && str[0] <= 0xBF &&
|
|
str[1] >=0x80 && str[1] <= 0xBF &&
|
|
str[2] >=0x80 && str[2] <= 0xBF) str+=3;
|
|
else break;
|
|
}
|
|
else if (c >= 128) break;
|
|
}
|
|
if (*str) break;
|
|
|
|
linebuf[0]=0;
|
|
fgets(linebuf,linebuf_size,fp);
|
|
if (!linebuf[0]) break;
|
|
}
|
|
*format_flag = linebuf[0] ? 3 : 0; // if scanned the whole file without an invalid UTF8 pair, then UTF-8 (0), otherwise ansi (3)
|
|
fseek(fp,0,SEEK_SET);
|
|
WDL_fgets_as_utf8(linebuf,linebuf_size,fp,format_flag);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
WDL_AssocArray<WDL_UINT64, char *> *WDL_GetLangpackSection(const char *sec)
|
|
{
|
|
return g_translations.Get(sec);
|
|
}
|
|
|
|
WDL_AssocArray<WDL_UINT64, char *> *WDL_LoadLanguagePackInternal(const char *fn,
|
|
WDL_StringKeyedArray< WDL_AssocArray<WDL_UINT64, char *> * > *dest,
|
|
const char *onlySec_name,
|
|
bool include_commented_lines,
|
|
bool no_escape_strings,
|
|
WDL_StringKeyedArray<char *> *extra_metadata
|
|
)
|
|
{
|
|
#ifdef _DEBUG
|
|
g_debug_langpack_has_loaded = true;
|
|
#endif
|
|
WDL_AssocArray<WDL_UINT64, char *> *rv=NULL;
|
|
FILE *fp = fopenUTF8(fn,"r");
|
|
if (!fp) return rv;
|
|
|
|
WDL_AssocArray<WDL_UINT64, char *> *cursec=NULL;
|
|
|
|
int format_flag=-1;
|
|
|
|
WDL_TypedBuf<char> procbuf;
|
|
char linebuf[16384];
|
|
int ic_lines = 0;
|
|
for (;;)
|
|
{
|
|
WDL_fgets_as_utf8(linebuf,sizeof(linebuf),fp,&format_flag);
|
|
if (!linebuf[0]) break;
|
|
|
|
char *p=linebuf;
|
|
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
|
|
char *lbstart = p;
|
|
while (*p) p++;
|
|
p--;
|
|
while (p >= lbstart && (*p == '\t' || *p == '\n' || *p == '\r')) p--;
|
|
p++;
|
|
*p=0;
|
|
|
|
if (include_commented_lines)
|
|
{
|
|
if (*lbstart == ';')
|
|
{
|
|
int x, offs = (lbstart[1] == '^') ? 2 : 1;
|
|
for (x = 0; x < 16; x ++)
|
|
{
|
|
char c = lbstart[offs+x];
|
|
if (c >= 'A' && c <= 'F') { }
|
|
else if (c >= '0' && c <= '9') { }
|
|
else break;
|
|
}
|
|
if (x == 16 && lbstart[offs+16] == '=')
|
|
lbstart += offs;
|
|
}
|
|
}
|
|
if (!*lbstart || *lbstart == ';' || *lbstart == '#')
|
|
{
|
|
if (ic_lines >= 0 && extra_metadata)
|
|
{
|
|
char tmp[128];
|
|
snprintf(tmp,sizeof(tmp),"_initial_comment_%d",ic_lines++);
|
|
extra_metadata->Insert(tmp,strdup(linebuf));
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (*lbstart == '[')
|
|
{
|
|
ic_lines = -1;
|
|
if (cursec) cursec->Resort();
|
|
|
|
lbstart++;
|
|
{
|
|
char *tmp = lbstart;
|
|
while (*tmp && *tmp != ']') tmp++;
|
|
*tmp++=0;
|
|
if (extra_metadata)
|
|
{
|
|
while (*tmp == ' ') tmp++;
|
|
if (*tmp)
|
|
extra_metadata->Insert(lbstart,strdup(tmp));
|
|
}
|
|
}
|
|
|
|
if (onlySec_name)
|
|
{
|
|
cursec=NULL;
|
|
if (!strcmp(lbstart,onlySec_name))
|
|
{
|
|
if (!rv) rv=new WDL_AssocArray<WDL_UINT64, char *>(uint64cmpfunc);
|
|
cursec=rv;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
cursec = dest->Get(lbstart);
|
|
if (!cursec)
|
|
{
|
|
cursec = new WDL_AssocArray<WDL_UINT64, char *>(uint64cmpfunc);
|
|
dest->Insert(lbstart,cursec);
|
|
}
|
|
}
|
|
}
|
|
else if (cursec)
|
|
{
|
|
char *eq = strstr(lbstart,"=");
|
|
if (eq)
|
|
{
|
|
*eq++ = 0;
|
|
if (strlen(lbstart) == 16)
|
|
{
|
|
WDL_UINT64 v=0;
|
|
int x;
|
|
for (x=0;x<16;x++)
|
|
{
|
|
int a = lbstart[x];
|
|
if (a>='0' && a<='9') a-='0';
|
|
else if (a>='A' && a<='F') a-='A' - 10;
|
|
else break;
|
|
|
|
v<<=4;
|
|
v+=a;
|
|
}
|
|
if (x==16)
|
|
{
|
|
if (strstr(eq,"\\") && !no_escape_strings)
|
|
{
|
|
procbuf.Resize(0,false);
|
|
while (*eq)
|
|
{
|
|
if (*eq == '\\')
|
|
{
|
|
eq++;
|
|
if (*eq == '\\' || *eq == '\'' || *eq == '"') procbuf.Add(*eq);
|
|
else if (*eq == 't'||*eq=='T') procbuf.Add('\t');
|
|
else if (*eq == 'r'||*eq=='R') procbuf.Add('\r');
|
|
else if (*eq == 'n'||*eq=='N') procbuf.Add('\n');
|
|
else if (*eq == '0') procbuf.Add(0);
|
|
else procbuf.Add(*eq);
|
|
}
|
|
else procbuf.Add(*eq);
|
|
|
|
eq++;
|
|
}
|
|
procbuf.Add(0);
|
|
procbuf.Add(0);
|
|
char *pc = (char *)ChunkAlloc(procbuf.GetSize());
|
|
if (pc)
|
|
{
|
|
memcpy(pc,procbuf.Get(),procbuf.GetSize());
|
|
cursec->AddUnsorted(v,pc);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int eqlen = (int)strlen(eq);
|
|
char *pc = (char *)ChunkAlloc(eqlen+2);
|
|
if (pc)
|
|
{
|
|
memcpy(pc,eq,eqlen+1);
|
|
pc[eqlen+1]=0; // doublenull terminate to be safe, in case the caller requested LOCALIZE_FLAG_NULLPAIR
|
|
cursec->AddUnsorted(v,pc);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (cursec)
|
|
cursec->Resort();
|
|
|
|
fclose(fp);
|
|
|
|
return rv;
|
|
}
|
|
|
|
WDL_AssocArray<WDL_UINT64, char *> *WDL_LoadLanguagePack(const char *fn, const char *onlySec_name)
|
|
{
|
|
WDL_AssocArray<WDL_UINT64, char *> *rv = WDL_LoadLanguagePackInternal(fn,&g_translations, onlySec_name,false,false, NULL);
|
|
if (!onlySec_name)
|
|
g_translations_commonsec = g_translations.Get("common");
|
|
return rv;
|
|
}
|
|
|
|
void WDL_SetLangpackFallbackEntry(const char *src_sec, WDL_UINT64 src_v, const char *dest_sec, WDL_UINT64 dest_v)
|
|
{
|
|
WDL_AssocArray<WDL_UINT64, char *> *sec = g_translations.Get(src_sec);
|
|
char *v = sec ? sec->Get(src_v) : NULL;
|
|
if (!v) return;
|
|
sec = g_translations.Get(dest_sec);
|
|
if (!sec)
|
|
{
|
|
sec = new WDL_AssocArray<WDL_UINT64, char *>(uint64cmpfunc);
|
|
g_translations.Insert(dest_sec,sec);
|
|
}
|
|
else if (sec->Get(dest_v)) return;
|
|
|
|
sec->Insert(dest_v,v);
|
|
}
|
|
|
|
|