diff --git a/Ide/Code/DebugContext.cpp b/Ide/Code/DebugContext.cpp --- a/Ide/Code/DebugContext.cpp +++ b/Ide/Code/DebugContext.cpp @@ -1,777 +1,777 @@ #include "lgi/common/Lgi.h" #include "lgi/common/TextLog.h" #include "lgi/common/List.h" #include "LgiIde.h" #include "IdeProject.h" enum DebugMessages { M_RUN_STATE = M_USER + 100, M_ON_CRASH, M_FILE_LINE, }; class LDebugContextPriv : public LMutex { public: LDebugContext *Ctx; AppWnd *App; IdeProject *Proj; bool InDebugging; LAutoPtr Db; LString Exe, Args; LString SeekFile; int SeekLine; bool SeekCurrentIp; LString MemDumpAddr; NativeInt MemDumpStart; LArray MemDump; LDebugContextPriv(LDebugContext *ctx) : LMutex("LDebugContextPriv") { Ctx = ctx; MemDumpStart = 0; App = NULL; Proj = NULL; InDebugging = false; SeekLine = 0; SeekCurrentIp = false; } ~LDebugContextPriv() { #if DEBUG_SESSION_LOGGING LgiTrace("~LDebugContextPriv freeing debugger...\n"); #endif Db.Reset(); #if DEBUG_SESSION_LOGGING LgiTrace("...done.\n"); #endif } void UpdateThreads() { if (!Db || !Ctx->Threads || !InDebugging) { LgiTrace("%s:%i - No debugger.\n", _FL); return; } LArray Threads; int CurrentThread = -1; if (!Db->GetThreads(Threads, &CurrentThread)) { LgiTrace("%s:%i - Failed to get threads from debugger.\n", _FL); return; } Ctx->Threads->Empty(); for (unsigned i=0; iSetText(f, 0); it->SetText(Sp, 1); Ctx->Threads->Insert(it); it->Select(ThreadId == CurrentThread); } } } Ctx->Threads->SendNotify(); } void UpdateCallStack() { if (Db && Ctx->CallStack && InDebugging) { LArray Stack; if (Db->GetCallStack(Stack)) { Ctx->CallStack->Empty(); for (int i=0; iSetText(f, 0); it->SetText(Sp, 1); } else { it->SetText(Stack[i], 1); } } else { it->SetText(Stack[i], 1); } Ctx->CallStack->Insert(it); } Ctx->CallStack->SendNotify(); } } } void Log(const char *Fmt, ...) { if (Ctx->DebuggerLog) { va_list Arg; va_start(Arg, Fmt); LStreamPrintf(Ctx->DebuggerLog, 0, Fmt, Arg); va_end(Arg); } } int Main() { return 0; } }; LDebugContext::LDebugContext(AppWnd *App, IdeProject *Proj, const char *Exe, const char *Args, bool RunAsAdmin, const char *Env, const char *InitDir) { Watch = NULL; Locals = NULL; DebuggerLog = NULL; ObjectDump = NULL; Registers = NULL; Threads = NULL; d = new LDebugContextPriv(this); d->App = App; d->Proj = Proj; d->Exe = Exe; #ifdef HAIKU LAssert(!"No GDB"); #else if (d->Db.Reset(CreateGdbDebugger(App->GetDebugLog()))) { LFile::Path p; if (InitDir) { p = InitDir; } else { p = Exe; p--; } if (!d->Db->Load(this, Exe, Args, RunAsAdmin, p, Env)) { d->Log("Failed to load '%s' into debugger.\n", d->Exe.Get()); d->Db.Reset(); } } #endif } LDebugContext::~LDebugContext() { DeleteObj(d); } LMessage::Param LDebugContext::OnEvent(LMessage *m) { switch (m->Msg()) { case M_ON_CRASH: { #if DEBUG_SESSION_LOGGING LgiTrace("LDebugContext::OnEvent(M_ON_CRASH)\n"); #endif d->UpdateCallStack(); break; } case M_FILE_LINE: { #if DEBUG_SESSION_LOGGING LgiTrace("LDebugContext::OnEvent(M_FILE_LINE)\n"); #endif LString File; { LMutex::Auto a(d, _FL); File = d->SeekFile; } // printf("%s:%i - %s %i\n", _FL, File.Get(), d->SeekLine); if (File && d->SeekLine > 0) d->App->GotoReference(File, d->SeekLine, d->SeekCurrentIp); break; } } return 0; } bool LDebugContext::SetFrame(int Frame) { return d->Db ? d->Db->SetFrame(Frame) : false; } bool LDebugContext::DumpObject(const char *Var, const char *Val) { if (!d->Db || !Var || !ObjectDump || !d->InDebugging) return false; ObjectDump->Name(NULL); if (!d->Db->PrintObject(Var, ObjectDump)) return false; ObjectDump->SetCaret(0); return true; } bool LDebugContext::UpdateRegisters() { if (!d->Db || !Registers || !d->InDebugging) return false; return d->Db->GetRegisters(Registers); } bool LDebugContext::UpdateLocals() { if (!Locals || !d->Db || !d->InDebugging) return false; LArray Vars; if (!d->Db->GetVariables(true, Vars, false)) return false; Locals->Empty(); for (int i=0; iSetText("local", 0); break; case LDebugger::Variable::Global: it->SetText("global", 0); break; case LDebugger::Variable::Arg: it->SetText("arg", 0); break; } char s[256]; switch (v.Value.Type) { case GV_BOOL: { it->SetText(v.Type ? v.Type.Get() : "bool", 1); sprintf_s(s, sizeof(s), "%s", v.Value.Value.Bool ? "true" : "false"); break; } case GV_INT32: { it->SetText(v.Type ? v.Type.Get() : "int32", 1); sprintf_s(s, sizeof(s), "%i (0x%x)", v.Value.Value.Int, v.Value.Value.Int); break; } case GV_INT64: { it->SetText(v.Type ? v.Type.Get() : "int64", 1); sprintf_s(s, sizeof(s), LPrintfInt64, v.Value.Value.Int64); break; } case GV_DOUBLE: { it->SetText(v.Type ? v.Type.Get() : "double", 1); sprintf_s(s, sizeof(s), "%g", v.Value.Value.Dbl); break; } case GV_STRING: { it->SetText(v.Type ? v.Type.Get() : "string", 1); sprintf_s(s, sizeof(s), "%s", v.Value.Value.String); break; } case GV_WSTRING: { it->SetText(v.Type ? v.Type.Get() : "wstring", 1); #ifdef MAC LAutoString tmp(WideToUtf8(v.Value.Value.WString)); sprintf_s(s, sizeof(s), "%s", tmp.Get()); #else sprintf_s(s, sizeof(s), "%S", v.Value.Value.WString); #endif break; } case GV_VOID_PTR: { it->SetText(v.Type ? v.Type.Get() : "void*", 1); sprintf_s(s, sizeof(s), "%p", v.Value.Value.Ptr); break; } default: { sprintf_s(s, sizeof(s), "notimp(%s)", LVariant::TypeToString(v.Value.Type)); it->SetText(v.Type ? v.Type : s, 1); s[0] = 0; break; } } it->SetText(v.Name, 2); it->SetText(s, 3); Locals->Insert(it); } } Locals->ResizeColumnsToContent(); return true; } bool LDebugContext::UpdateWatches() { LArray Vars; for (LTreeItem *i = Watch->GetChild(); i; i = i->GetNext()) { LDebugger::Variable &v = Vars.New(); v.Name = i->GetText(0); v.Type = i->GetText(1); } printf("Update watches %i\n", (int)Vars.Length()); if (!d->Db->GetVariables(false, Vars, false)) return false; int Idx = 0; for (LTreeItem *i = Watch->GetChild(); i; i = i->GetNext(), Idx++) { LDebugger::Variable &v = Vars[Idx]; WatchItem *wi = dynamic_cast(i); if (!wi) { LgiTrace("%s:%i - Error: not watch item.\n", _FL); continue; } if (v.Name == (const char*)i->GetText(0)) { i->SetText(v.Type, 1); wi->SetValue(v.Value); } else { LgiTrace("%s:%i - Error: Not the same name.\n", _FL); } } return true; } void LDebugContext::UpdateCallStack() { d->UpdateCallStack(); } void LDebugContext::UpdateThreads() { d->UpdateThreads(); } bool LDebugContext::SelectThread(int ThreadId) { if (!d->Db) { LgiTrace("%s:%i - No debugger.\n", _FL); return false; } return d->Db->SetCurrentThread(ThreadId); } bool LDebugContext::ParseFrameReference(const char *Frame, LAutoString &File, int &Line) { if (!Frame) return false; const char *At = NULL, *s = Frame; while ((s = stristr(s, "at"))) { At = s; s += 2; } if (!At) return false; At += 3; if (!File.Reset(LTokStr(At))) return false; char *Colon = strchr(File, ':'); if (!Colon) return false; *Colon++ = 0; Line = atoi(Colon); return Line > 0; } bool LDebugContext::OnCommand(int Cmd) { #if DEBUG_SESSION_LOGGING LgiTrace("LDebugContext::OnCommand(%i)\n", Cmd); #endif switch (Cmd) { case IDM_START_DEBUG: { if (d->Db) { d->App->LoadBreakPoints(d->Db); d->Db->SetRunning(true); } break; } case IDM_CONTINUE: { if (d->Db) d->Db->SetRunning(true); break; } case IDM_ATTACH_TO_PROCESS: { break; } case IDM_STOP_DEBUG: { if (d->Db) return d->Db->Unload(); else return false; break; } case IDM_PAUSE_DEBUG: { if (d->Db) d->Db->SetRunning(false); break; } case IDM_RESTART_DEBUGGING: { if (d->Db) d->Db->Restart(); break; } case IDM_RUN_TO: { break; } case IDM_STEP_INTO: { if (d->Db) d->Db->StepInto(); break; } case IDM_STEP_OVER: { if (d->Db) d->Db->StepOver(); break; } case IDM_STEP_OUT: { if (d->Db) d->Db->StepOut(); break; } case IDM_TOGGLE_BREAKPOINT: { break; } default: return false; } return true; } void LDebugContext::OnUserCommand(const char *Cmd) { if (d->Db) d->Db->UserCommand(Cmd); } void NonPrintable(uint64 ch, uint8_t *&out, ssize_t &len) { if (ch == '\r') { if (len > 1) { *out++ = '\\'; *out++ = 'r'; len -= 2; } else LAssert(0); } else if (ch == '\n') { if (len > 1) { *out++ = '\\'; *out++ = 'n'; len -= 2; } else LAssert(0); } else if (len > 0) { *out++ = '.'; len--; } } void LDebugContext::FormatMemoryDump(int WordSize, int Width, bool InHex) { if (!MemoryDump) { LgiTrace("%s:%i - No MemoryDump.\n", _FL); return; } if (d->MemDump.Length() == 0) { MemoryDump->Name("No data."); return; } if (!Width) Width = 16 / WordSize; if (!WordSize) WordSize = 1; // Format output to the mem dump window LStringPipe p; int LineBytes = WordSize * Width; LPointer ptr; ptr.u8 = &d->MemDump[0]; for (NativeInt i = 0; i < d->MemDump.Length(); i += LineBytes) { // LPointer Start = ptr; ssize_t DisplayBytes = MIN(d->MemDump.Length() - i, LineBytes); ssize_t DisplayWords = DisplayBytes / WordSize; NativeInt iAddr = d->MemDumpStart + i; p.Print("%p ", iAddr); char Char[256] = ""; uint8_t *ChPtr = (uint8_t*)Char; ssize_t Len = sizeof(Char); for (int n=0; nName(p.NewGStr()); + MemoryDump->Name(p.NewLStr()); } void LDebugContext::OnMemoryDump(const char *Addr, int WordSize, int Width, bool IsHex) { if (MemoryDump && d->Db) { MemoryDump->Name(NULL); LString ErrMsg; if (d->Db->ReadMemory(d->MemDumpAddr = Addr, 1024, d->MemDump, &ErrMsg)) { d->MemDumpStart = (int)d->MemDumpAddr.Int(16); FormatMemoryDump(WordSize, Width, IsHex); } else { MemoryDump->Name(ErrMsg ? ErrMsg : "ReadMemory failed."); } } } void LDebugContext::OnState(bool Debugging, bool Running) { #if DEBUG_SESSION_LOGGING LgiTrace("LDebugContext::OnState(%i, %i)\n", Debugging, Running); #endif if (d->InDebugging != Debugging && d->Db) { d->InDebugging = Debugging; } if (d->App) { d->App->OnDebugState(Debugging, Running); } #if DEBUG_SESSION_LOGGING LgiTrace("LDebugContext::OnState(%i, %i) ###ENDED###\n", Debugging, Running); #endif // This object may be deleted at this point... don't access anything. } void LDebugContext::OnFileLine(const char *File, int Line, bool CurrentIp) { if (!d->App) { printf("%s:%i - No app.\n", _FL); return; } if (!File || Line < 1) { LgiTrace("%s:%i - Error: No File or Line... one or both must be valid.\n", _FL); LAssert(!"Invalid Param"); return; } if (d->App->InThread()) { d->App->GotoReference(File, Line, CurrentIp); } else { { LMutex::Auto a(d, _FL); if (File) d->SeekFile = File; d->SeekLine = Line; d->SeekCurrentIp = CurrentIp; // printf("%s:%i - %s %i, %i\n", _FL, d->SeekFile.Get(), d->SeekLine, d->SeekCurrentIp); } d->App->PostEvent(M_FILE_LINE); } } ssize_t LDebugContext::Write(const void *Ptr, ssize_t Size, int Flags) { if (DebuggerLog && Ptr) { // LgiTrace("Write '%.*s'\n", Size, Ptr); return DebuggerLog->Write(Ptr, Size, 0); } return -1; } void LDebugContext::OnError(int Code, const char *Str) { if (DebuggerLog) DebuggerLog->Print("Error(%i): %s\n", Code, Str); } void LDebugContext::OnCrash(int Code) { d->App->PostEvent(M_ON_CRASH); } bool LDebugContext::OnBreakPoint(LDebugger::BreakPoint &b, bool Add) { if (!d->Db) { LgiTrace("%s:%i - No debugger loaded.\n", _FL); return false; } if (Add) return d->Db->SetBreakPoint(&b); else return d->Db->RemoveBreakPoint(&b); } \ No newline at end of file diff --git a/Ide/Code/Debugger.cpp b/Ide/Code/Debugger.cpp --- a/Ide/Code/Debugger.cpp +++ b/Ide/Code/Debugger.cpp @@ -1,1565 +1,1565 @@ #ifdef POSIX #include #include #include #include #endif #include "lgi/common/Lgi.h" #include "lgi/common/SubProcess.h" #include "lgi/common/Token.h" #include "lgi/common/DocView.h" #include "lgi/common/StringClass.h" #include "lgi/common/LgiString.h" #include "Debugger.h" #define DEBUG_STOP_ON_GTK_ERROR 0 #define DEBUG_SHOW_GDB_IO 0 #define ECHO_GDB_OUTPUT 0 const char sPrompt[] = "(gdb) "; class Callback { public: virtual LString GetResponse(const char *c) = 0; }; class Visualizer { public: virtual ~Visualizer() {} virtual bool Match(LString s) = 0; virtual bool Transform(LString name, LString val, Callback *Cb, LVariant &Value, LString &Detail) = 0; }; class LStringVis : public Visualizer { public: bool Match(LString s) { return s == "LString"; } bool Transform(LString name, LString val, Callback *Cb, LVariant &Value, LString &Detail) { LString::Array a = val.SplitDelimit("{} \t\r\n"); if (a.Length() == 3 && a[1] == "=") { void *Ptr = (void*)htoi64(a[2].Get()); if (Ptr == NULL) { Value = "NULL"; } else { LString cmd; cmd.Printf("p (char*)%s.Str->Str", name.Get()); LString r = Cb->GetResponse(cmd); auto Pos = r.Find("="); if (Pos >= 0) Value = r(Pos, r.Length()).Strip().Get(); else Value = r.Get(); } } return true; } }; class Gdb : public LDebugger, public LThread, public Callback { LDebugEvents *Events = NULL; LAutoPtr Sp; LString Exe, Args, InitDir; LString ChildEnv; bool RunAsAdmin = false; bool AtPrompt = false; char Line[256], *LinePtr = NULL; int CurFrame = 0; bool SetAsmType = false; bool SetPendingOn = false; LArray BreakPoints; int BreakPointIdx = -1; int ProcessId = -1; bool SuppressNextFileLine = false; LArray Vis; LStream *Log = NULL; LMutex StateMutex; bool DebuggingProcess = false; bool Running = false; // Current location tracking LString CurFile; int CurLine = -1; LString::Array Untagged; // Parse state enum ParseType { ParseNone, ParseBreakPoint, } ParseState = ParseNone; LString::Array BreakInfo; // Various output modes. LStream *OutStream = NULL; LString::Array *OutLines = NULL; enum ThreadState { Init, Looping, Exiting, ProcessError } State; void OnFileLine(const char *File, int Line, bool CurrentIp) { if (SuppressNextFileLine) { // printf("%s:%i - SuppressNextFileLine\n", _FL); SuppressNextFileLine = false; } else if (Events) { if (File) CurFile = File; if (Line > 0) CurLine = Line; if (CurFile && CurLine > 0) Events->OnFileLine(CurFile, CurLine, CurrentIp); /* else printf("%s:%i - Error: Cur loc incomplete: %s %i.\n", _FL, CurFile.Get(), CurLine); */ } } bool ParseLocation(LString::Array &p) { for (int i=0; i 0) { int At = 0; for (; At < a.Length() && stricmp(a[At], "at") != 0; At++) ; if (At < a.Length() - 1) // Found the 'at' { LString::Array ref = a[At+1].Split(":"); if (ref.Length() == 2) { OnFileLine(NativePath(ref[0]), (int)ref[1].Int(), true); return true; } } else { int Line = (int)a[0].Int(); if (Line) { OnFileLine(NULL, Line, true); return true; } } } } return false; } void SetState(bool is_debug, bool is_run) { if (StateMutex.Lock(_FL)) { if (is_debug != DebuggingProcess || is_run != Running) { DebuggingProcess = is_debug; Running = is_run; StateMutex.Unlock(); if (Events) { #if DEBUG_SESSION_LOGGING LgiTrace("Gdb::SetRunState(%i,%i) calling OnState...\n", is_debug, is_run); #endif Events->OnState(DebuggingProcess, Running); #if DEBUG_SESSION_LOGGING LgiTrace("Gdb::SetRunState(%i,%i) OnState returned.\n", is_debug, is_run); #endif } } else { StateMutex.Unlock(); } } } void LogMsg(const char *Fmt, ...) { if (Events) { va_list Arg; va_start(Arg, Fmt); char Buf[512]; int Ch = vsprintf_s(Buf, sizeof(Buf), Fmt, Arg); va_end(Arg); Events->Write(Buf, Ch); } } void OnExit() { SetState(false, false); } char *NativePath(char *p) { for (char *c = p; *c; c++) { if (*c == '/' || *c == '\\') *c = DIR_CHAR; } return p; } void OnBreakPoint(LString f) { if (!f.Get() || ProcessId < 0) { // printf("Error: Param error: %s, %i (%s:%i)\n", f.Get(), ProcessId, _FL); return; } LString File, Line; LString::Array a = f.Split("at"); /* printf("%s:%i - a.len=%i\n", _FL, a.Length()); for (unsigned n=0; n 0) { e++; while (e < k.Length() && IsDigit(k(e))) e++; // printf("%s:%i - e=%i\n", _FL, e); LString::Array b = k(0, e).RSplit(":", 1); // printf("%s:%i - b.len=%i\n", _FL, b.Length()); if (b.Length() == 2) { File = b[0]; Line = b[1]; } } else printf("Error: no ':' in '%s'. (%s:%i)\n", k.Get(), _FL); } } else printf("Error: %i parts (%s:%i).\n", (int)a.Length(), _FL); if (File && Line.Int() > 0) { OnFileLine(NativePath(File), (int)Line.Int(), true); } else { printf("%s:%i - No file='%s' or line='%s'\n%s\n", _FL, File.Get(), Line.Get(), f.Get()); } } void OnLine(const char *Start, int Length) { #if DEBUG_SHOW_GDB_IO LgiTrace("Receive: '%.*s' ParseState=%i, OutLine=%p, OutStream=%p\n", Length-1, Start, ParseState, OutLines, OutStream); #endif // Send output if (OutLines) { OutLines->New().Set(Start, Length - 1); return; } else if (OutStream) { OutStream->Write(Start, Length); return; } else { Untagged.New().Set(Start, Length); #if !ECHO_GDB_OUTPUT Events->Write(Start, Length); #endif } #if ECHO_GDB_OUTPUT Events->Write(Start, Length); #endif if (ParseState == ParseBreakPoint) { if (Length > 0 && IsDigit(*Start)) { // printf("ParsingBp.Parse=%s\n", Start); LString Bp = LString(" ").Join(BreakInfo).Strip(); OnBreakPoint(Bp); ParseState = ParseNone; BreakInfo.Length(0); } else { // printf("ParsingBp.Add=%s\n", Start); BreakInfo.New().Set(Start, Length); } } if (ParseState == ParseNone) { if (stristr(Start, "received signal SIGSEGV")) { Events->OnCrash(0); } else if (*Start == '[') { if (stristr(Start, "Inferior") && stristr(Start, "exited")) { OnExit(); } else if (stristr(Start, "New Thread")) { LString s(Start, Length); LString::Array a = s.SplitDelimit("[] ()"); int ThreadId = -1; for (unsigned i=0; i 0) { // Ok so whats the process ID? #ifdef POSIX int Pid = getpgid(ThreadId); // LogMsg("Pid for Thread %i = %i\n", ThreadId, Pid); if (Pid > 0 && ProcessId < 0) // LgiTrace("Got the thread id: %i, and pid: %i\n", ThreadId, Pid); ProcessId = Pid; #else LAssert(!"Impl me."); #endif } else LgiTrace("%s:%i - No thread id?\n", _FL); } } else if (strncmp(Start, "Breakpoint ", 11) == 0 && IsDigit(Start[11])) { ParseState = ParseBreakPoint; // printf("ParseState=%i\n", ParseState); BreakInfo.New().Set(Start, Length); } else { // Untagged file/line? if (ParseLocation(Untagged)) { Untagged.Length(0); } } } } void OnRead(const char *Ptr, ssize_t Bytes) { // Parse output into lines const char *p = Ptr; const char *End = p + Bytes; char *LineEnd = Line + sizeof(Line) - 2; while (p < End) { if (*p == '\n') { *LinePtr++ = *p; *LinePtr = 0; OnLine(Line, (int) (LinePtr - Line)); LinePtr = Line; } else if (LinePtr < LineEnd) { *LinePtr++ = *p; } p++; } *LinePtr = 0; // Check for prompt auto bytes = LinePtr - Line; if (bytes > 0) { if (bytes == 6) { AtPrompt = !_strnicmp(Line, sPrompt, bytes); // LgiTrace("%I64i: AtPrompt=%i\n", LCurrentTime(), AtPrompt); if (AtPrompt) { if (Running ^ !AtPrompt) { SetState(DebuggingProcess, !AtPrompt); } if (OutStream) OutStream = NULL; if (OutLines) OutLines = NULL; Events->Write(Line, bytes); } } } } int Main() { #ifdef WIN32 const char *Shell = "C:\\Windows\\System32\\cmd.exe"; const char *Path = "C:\\MinGW\\bin\\gdb.exe"; #else const char *Path = "gdb"; #endif LString p; if (RunAsAdmin) p.Printf("pkexec %s --args \"%s\"", Path, Exe.Get()); else p.Printf("%s --args \"%s\"", Path, Exe.Get()); if (Args) { p += " "; p += Args; } LString::Array a = p.Split(" ", 1); printf("Starting Debugger: %s %s\n", a[0].Get(), a[1].Get()); if (!Sp.Reset(new LSubProcess(a[0], a[1]))) return false; if (InitDir) Sp->SetInitFolder(InitDir); if (ChildEnv) { auto p = ChildEnv.Split("\n"); for (auto &v: p) { auto a = v.Strip().Split("=", 1); if (a.Length() == 2) { LogMsg("%s:%i - env %s=%s\n", _FL, a[0].Get(), a[1].Get()); Sp->SetEnvironment(a[0], a[1]); } else LogMsg("%s:%i - Wrong parts %s.", _FL, v.Get()); } } else LogMsg("%s:%i - No env.", _FL); #if DEBUG_STOP_ON_GTK_ERROR Sp->SetEnvironment("G_DEBUG", "fatal-criticals"); #endif LgiTrace("Starting gdb subprocess...\n"); if (!Sp->Start(true, true, false)) { State = ProcessError; LString ErrMsg = LErrorCodeToString(Sp->GetErrorCode()); char s[256]; sprintf_s(s, sizeof(s), "Failed to start gdb, error: 0x%x (%s)\n", Sp->GetErrorCode(), ErrMsg.Get()); Events->OnError(Sp->GetErrorCode(), s); return -1; } #if DEBUG_SESSION_LOGGING LgiTrace("Gdb::Main - entering loop...\n"); #endif State = Looping; char Buf[513]; bool IsRun; while (State == Looping && (IsRun = Sp->IsRunning())) { #ifdef _DEBUG ZeroObj(Buf); #endif auto Rd = Sp->Read(Buf, sizeof(Buf)-1, 50); if (Rd > 0) { #if 0 // DEBUG_SESSION_LOGGING printf("GDB: %.*s\n", Rd, Buf); #endif OnRead(Buf, Rd); } } Break(); Cmd("q"); #if DEBUG_SESSION_LOGGING LgiTrace("Gdb::Main - exited loop.\n"); #endif SetState(false, false); LogMsg("Debugger exited.\n"); return 0; } bool WaitPrompt() { if (State == Init) { uint64 Start = LCurrentTime(); while (State == Init) { uint64 Now = LCurrentTime(); if (Now - Start < 5000) { LSleep(10); } else { LgiTrace("%s:%i - WaitPrompt init wait failed.\n", _FL); return false; } } } uint64 Start = LCurrentTime(); uint64 Now = Start; while (!AtPrompt && Now - Start < 2000 && State == Looping) { Now = LCurrentTime(); LSleep(50); uint64 After = LCurrentTime(); if (After - Now > 65) { printf("Sleep took=%i\n", (int)(After - Now)); } } if (!AtPrompt) { LogMsg("Error: Not at prompt...\n"); return false; } return true; } bool Cmd(const char *c, LStream *Output = NULL, LString::Array *Arr = NULL) { if (!ValidStr(c)) { LgiTrace("%s:%i - Not a valid command.\n", _FL); LAssert(!"Not a valid command."); return false; } if (!WaitPrompt()) { return false; } char str[256]; int ch = sprintf_s(str, sizeof(str), "%s\n", c); #if DEBUG_SHOW_GDB_IO LgiTrace("Send: '%s'\n", c); #endif Events->Write(str, ch); LinePtr = Line; OutStream = Output; OutLines = Arr; AtPrompt = false; // uint64 Start = LCurrentTime(); auto Wr = Sp->Write(str, ch); if (Wr != ch) return false; if (OutStream || OutLines) { /* uint64 Wait0 = LCurrentTime(); */ WaitPrompt(); /* uint64 Wait1 = LCurrentTime(); LgiTrace("Cmd timing "LGI_PrintfInt64" "LGI_PrintfInt64"\n", Wait0-Start, Wait1-Wait0); */ LAssert(OutStream == NULL && OutLines == NULL); } return true; } public: Gdb(LStream *log) : LThread("Gdb"), Log(log), StateMutex("Gdb.StateMutex") { State = Init; LinePtr = Line; Vis.Add(new LStringVis); } ~Gdb() { if (State == Looping) { #if DEBUG_SESSION_LOGGING LgiTrace("Gdb::~Gdb - waiting for thread to exit...\n"); #endif State = Exiting; while (!IsExited()) { LSleep(1); } #if DEBUG_SESSION_LOGGING LgiTrace("Gdb::~Gdb - thread has exited.\n"); #endif } } bool Load(LDebugEvents *EventHandler, const char *exe, const char *args, bool runAsAdmin, const char *initDir, const char *Env) { Events = EventHandler; Exe = exe; Args = args; RunAsAdmin = runAsAdmin; ChildEnv = Env; InitDir = initDir; Running = false; Run(); return true; } bool SetCurrentThread(int ThreadId) { if (ThreadId < 1) return false; LString c; c.Printf("thread %i", ThreadId); if (!Cmd(c)) return false; return true; } bool GetThreads(LArray &Threads, int *pCurrentThread) { LString::Array t; if (!Cmd("info threads", NULL, &t)) return false; LString *Cur = NULL; for (int i=0; iGet(), l); *Cur = s; } } return true; } bool GetCallStack(LArray &Stack) { LString::Array Bt; if (!Cmd("bt", NULL, &Bt)) return false; for (int i=0; i 0) { // Append to the last line.. LAutoString &Prev = Stack.Last(); char *End = Prev + strlen(Prev); while (End > Prev && strchr(WhiteSpace, End[-1])) *(--End) = 0; LString s; s.Printf("%s%s", Prev.Get(), l); Prev.Reset(NewStr(s)); } } return true; } bool GetFrame(int &Frame, LAutoString &File, int &Line) { LAssert(0); return false; } bool SetFrame(int Frame) { if (CurFrame != Frame) { CurFrame = Frame; char c[256]; sprintf_s(c, sizeof(c), "frame %i", Frame); return Cmd(c); } return true; } bool Restart() { if (Running) Break(true); ProcessId = -1; LString a; if (Args) a.Printf("r %s", Args.Get()); else a = "r"; bool Status = Cmd(a); if (Status) { SetState(true, false); } return Status; } bool Unload() { if (Running) Break(true); Cmd("q"); SetState(false, false); State = Exiting; return false; } bool GetRunning() { return Running; } bool SetRunning(bool Run) { if (Run) { if (!SetAsmType) { SetAsmType = true; Cmd("set disassembly-flavor intel"); Cmd("handle SIGTTIN nostop"); Cmd("handle SIGTTOU ignore nostop"); Cmd("handle SIG34 ignore nostop"); Cmd("handle SIGPIPE nostop"); } LString a; if (DebuggingProcess) a = "c"; else if (Args) a.Printf("r %s", Args.Get()); else a = "r"; if (a(0) == 'r' && ProcessId < 0) { BreakPoint bp; bp.Symbol = "main"; if (SetBreakPoint(&bp)) { if (!Cmd(a)) return false; if (!WaitPrompt()) return false; RemoveBreakPoint(&bp); LStringPipe p; #if 0 // For some reason this is returning the wrong PID... WTH gdb... WTH. // Get process info if (Cmd("info inferiors", &p)) { - auto s = p.NewGStr(); + auto s = p.NewLStr(); // LogMsg("%s\n", s.Get()); auto Ln = s.SplitDelimit("\r\n"); if (Ln.Length() >= 2) { LString::Array a = Ln[1].SplitDelimit(" \t"); for (unsigned i=0; i= 0) { LogMsg("%s:%i - ProcessId was %i, now %i (%s)\n", _FL, ProcessId, Id, Ln[1].Get()); ProcessId = Id; } break; } } } #endif // Redetect the process id from the new threads... ProcessId = -1; bool Status = Cmd("c"); // Continue if (Status) SetState(true, true); LogMsg("[ProcessId=%i]\n", ProcessId); return Status; } } if (Cmd(a)) { SetState(true, true); return true; } } else { if (Break()) { return true; } } return false; } bool AddBp(BreakPoint &bp) { bool Ret = false; if (!bp.Added) { if (!SetPendingOn) { Cmd("set breakpoint pending on"); SetPendingOn = true; } char cmd[MAX_PATH_LEN]; char *File = bp.File.Get(); if (File) { char *Last = strrchr(File, DIR_CHAR); sprintf_s(cmd, sizeof(cmd), "break %s:" LPrintfSSizeT, Last ? Last + 1 : File, bp.Line); } else if (bp.Symbol) { sprintf_s(cmd, sizeof(cmd), "break %s", bp.Symbol.Get()); } else return false; BreakPointIdx = 0; LString::Array Lines; Ret = Cmd(cmd, NULL, &Lines); WaitPrompt(); for (unsigned i=0; i= 2 && !_stricmp(p[0], "breakpoint")) { int Idx = (int)p[1].Int(); if (Idx) { bp.Index = Idx; } } } BreakPointIdx = -1; if (Ret) bp.Added = true; } return Ret; } bool SetBreakPoint(BreakPoint *bp) { if (!bp) { LgiTrace("%s:%i - SetBreakPoint failed, param error.\n", _FL); return false; } // Make sure the child 'gdb' is running... uint64 Start = LCurrentTime(); while (State == Init) { LSleep(5); if (LCurrentTime()-Start > 3000) { LgiTrace("%s:%i - SetBreakPoint init wait failed...\n", _FL); return false; } } bp->Added = false; if (Running) { LgiTrace("%s:%i - Can't add break point while running.\n", _FL); return false; } else { if (AddBp(*bp)) { BreakPoint &n = BreakPoints.New(); n = *bp; } } return true; } bool RemoveBreakPoint(BreakPoint *bp) { if (!bp) return false; // Make sure the child 'gdb' is running... uint64 Start = LCurrentTime(); while (State == Init) { LSleep(5); if (LCurrentTime()-Start > 3000) { LgiTrace("%s:%i - SetBreakPoint init wait failed...\n", _FL); return false; } } if (Running) { LgiTrace("%s:%i - Can't add break point while running.\n", _FL); return false; } else { unsigned i; for (i=0; iFile.Get(), bp->Line); return false; } } return true; } bool GetBreakPoints(LArray &bps) { bps = BreakPoints; return false; } void ParseVariables(const char *a, LArray &vars, LDebugger::Variable::ScopeType scope, bool Detailed) { LToken t(a, "\r\n"); LString CurLine; for (unsigned i=0; i 0) { char *val = CurLine.Get() + EqPos + 1; while (*val && strchr(WhiteSpace, *val)) val++; Variable &v = vars.New(); v.Scope = scope; v.Name = CurLine(0, EqPos).Strip(); if (!strnicmp(val, "0x", 2)) { v.Value.Type = GV_VOID_PTR; v.Value.Value.Ptr = (void*) htoi64(val); } else if (IsDigit(*val) || strchr(".-", *val)) { // Is it floating point? auto isFloat = strchr(val, '.') != NULL; printf("numeric type for '%s' is %i\n", val, isFloat); if (isFloat) { double tmp = atof(val); v.Value = tmp; } else { int64 tmp = atoi64(val); if (tmp & 0xffffffff00000000L) v.Value = tmp; else v.Value = (int)tmp; } } else { v.Value.OwnStr(TrimStr(val)); } if (Detailed) { LStringPipe typePipe, valPipe; LString c; // Get the type... c.Printf("whatis %s", v.Name.Get()); Cmd(c, &typePipe); - auto type = typePipe.NewGStr(); + auto type = typePipe.NewLStr(); printf("Type='%s'\n", type.Get()); c.Printf("p %s", v.Name.Get()); Cmd(c, &valPipe); - auto val = valPipe.NewGStr(); + auto val = valPipe.NewLStr(); if (val) { for (char *s = val; s && *s; ) { if (*s == '\"') { char *e = strchr(++s, '\"'); if (!e) break; v.Value.OwnStr(NewStr(s, e - s)); break; } else if (*s == '(' && !v.Type) { char *e = strchr(++s, ')'); if (!e) break; if (strnicmp(s, "gdb", 3)) v.Type.Set(s, e - s); s = e + 1; continue; } s = LSkipDelim(s, WhiteSpace, true); s = LSkipDelim(s, WhiteSpace); } } } } } } bool GetVariables(bool Locals, LArray &vars, bool Detailed) { LStringPipe p(512); if (vars.Length()) { LString c; for (unsigned i=0; iMatch(v.Type)) { if (vs->Transform(v.Name, Val, this, v.Value, v.Detail)) break; } } if (i >= Vis.Length()) v.Value = Val.Get(); } else printf("%s:%i - Cmd failed '%s'\n", _FL, c.Get()); } return true; } else { if (!Cmd("info args", &p)) return false; - auto a = p.NewGStr(); + auto a = p.NewLStr(); ParseVariables(a, vars, Variable::Arg, Detailed); if (!Cmd("info locals", &p)) return false; - a = p.NewGStr(); + a = p.NewLStr(); ParseVariables(a, vars, Variable::Local, Detailed); } return true; } bool GetRegisters(LStream *Out) { if (!Out) return false; return Cmd("info registers", Out); } bool PrintObject(const char *Var, LStream *Output) { if (!Var || !Output) return false; LStringPipe q; char c[256]; // Get type... sprintf_s(c, sizeof(c), "whatis %s", Var); if (!Cmd(c, &q)) return false; - auto Type = q.NewGStr().SplitDelimit("=").Last().Strip(); + auto Type = q.NewLStr().SplitDelimit("=").Last().Strip(); bool IsPtr = Type.Find("*") >= 0; bool IsChar = Type.Find("const char") == 0 || Type.Find("char") == 0; bool IsGString = Type.Find("LString") == 0; #if 1 Output->Print("Type: %s\n", Type.Get()); #else // Debugging Output->Print("Type: %s (IsPtr=%i, IsGString=%i)\n", Type.Get(), IsPtr, IsGString); #endif // Get value... if (IsGString) { if (IsPtr) sprintf_s(c, sizeof(c), "p (char*)%s->Str.Str", Var); else sprintf_s(c, sizeof(c), "p (char*)%s.Str.Str", Var); } else sprintf_s(c, sizeof(c), "p %s%s", IsPtr && !IsChar ? "*" : "", Var); if (!Cmd(c, &q)) { Output->Print("%s:%i - Can't get value.\n", _FL); return false; } - auto val = q.NewGStr(); + auto val = q.NewLStr(); if (!val) { Output->Print("%s:%i - No value.\n", _FL); return false; } // Output->Print("val=%s\n", val.Get()); auto Eq = Strchr(val.Get(), '='); if (Eq) { Eq++; while (Strchr(" \t\r\n", *Eq)) Eq++; } if (Eq && *Eq != '{') { auto s = val.SplitDelimit("=").Last().Strip(); Output->Print("%s\n", s.Get()); } else // Parse object format. { int Depth = 0; char *Start = NULL; char Spaces[256]; memset(Spaces, ' ', sizeof(Spaces)); int IndentShift = 2; #define Emit() \ if (Start) \ { \ auto bytes = s - Start; \ char *last = s-1; while (last > Start && strchr(WhiteSpace, *last)) last--; \ Output->Print("%.*s%.*s%s\n", Depth<Print("%.*s%c\n", Depth<Print("%.*s%c\n", Depth< &OutBuf, LString *ErrorMsg) { if (!BaseAddr) { if (ErrorMsg) *ErrorMsg = "No base address supplied."; return false; } BaseAddr = BaseAddr.Strip(); LString::Array Out; char c[256]; int words = Length >> 2; // int bytes = Length % 4; if (BaseAddr.Find("0x") >= 0) { // Looks like an literal address... LiteralAddr: sprintf_s(c, sizeof(c), "x/%iw %s", words, BaseAddr.Get()); } else { // Maybe it's a ptr variable? LString c; LString::Array r; c.Printf("p %s", BaseAddr.Get()); if (Cmd(c, NULL, &r)) { LString::Array p = r[0].SplitDelimit(" \t"); for (unsigned i=0; i= 0) { BaseAddr = p[i]; goto LiteralAddr; } /* LString Msg; Msg.Printf("%s\n", p[i].Get()); Events->Write(Msg, Msg.Length()); */ } if (ErrorMsg) *ErrorMsg = "No address in variable value response."; return false; } else { if (ErrorMsg) *ErrorMsg = "Couldn't convert variable to address."; return false; } } if (!Cmd(c, NULL, &Out)) { if (ErrorMsg) *ErrorMsg = "Gdb command failed."; return false; } if (!OutBuf.Length(words << 2)) { if (ErrorMsg) ErrorMsg->Printf("Failed to allocate %i bytes.", words << 2); return false; } uint32_t *buf = (uint32_t*) &(OutBuf)[0]; uint32_t *ptr = buf; uint32_t *end = ptr + (OutBuf.Length() / sizeof(*buf)); for (int i=0; i= end) break; } } OutBuf.Length((ptr - buf) << 2); return true; } bool GetLocation(LAutoString &File, int &Line) { LAssert(0); return false; } bool SetLocation(const char *File, int Line) { LAssert(0); return false; } bool StepInto() { bool Status = Cmd("step"); if (Status) SetState(DebuggingProcess, true); return Status; } bool StepOver() { bool Status = Cmd("next"); if (Status) SetState(DebuggingProcess, true); return Status; } bool StepOut() { bool Status = Cmd("finish"); if (Status) SetState(DebuggingProcess, true); return Status; } bool Break(bool SuppressFL = false) { #ifdef POSIX if (ProcessId < 0) { LogMsg("%s:%i - No process ID (yet?).\n", _FL); return false; } SuppressNextFileLine = SuppressFL; // LogMsg("Break: Sending SIGINT to %i(0x%x)...\n", ProcessId, ProcessId); int result = kill(ProcessId, SIGINT); auto ErrNo = errno; // LogMsg("Break: result=%i\n", result); if (!result) { // LogMsg("%s:%i - success... waiting prompt\n", _FL); return WaitPrompt(); } LogMsg("%s:%i - SIGINT failed with %i(0x%x): %s (pid=%i)\n", _FL, ErrNo, ErrNo, LErrorCodeToString(ErrNo).Get(), ProcessId); return false; #else LAssert(!"Impl me"); return false; #endif } bool UserCommand(const char *cmd) { char c[256]; sprintf_s(c, sizeof(c), "%s", cmd); return Cmd(c); } LString GetResponse(const char *c) { LString r; LStringPipe p; if (Cmd(c, &p)) - r = p.NewGStr(); + r = p.NewLStr(); return r; } }; LDebugger *CreateGdbDebugger(LStream *Log) { return new Gdb(Log); } diff --git a/Ide/Code/IdeProject.cpp b/Ide/Code/IdeProject.cpp --- a/Ide/Code/IdeProject.cpp +++ b/Ide/Code/IdeProject.cpp @@ -1,4102 +1,4102 @@ #if defined(WIN32) #include #else #include #endif #include #include "lgi/common/Lgi.h" #include "lgi/common/DragAndDrop.h" #include "lgi/common/Token.h" #include "lgi/common/Combo.h" #include "lgi/common/Net.h" #include "lgi/common/ListItemCheckBox.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/DropFiles.h" #include "lgi/common/SubProcess.h" #include "lgi/common/Css.h" #include "lgi/common/TableLayout.h" #include "lgi/common/TextLabel.h" #include "lgi/common/Button.h" #include "lgi/common/RegKey.h" #include "lgi/common/FileSelect.h" #include "lgi/common/Menu.h" #include "LgiIde.h" #include "resdefs.h" #include "FtpThread.h" #include "ProjectNode.h" #include "WebFldDlg.h" extern const char *Untitled; const char SourcePatterns[] = "*.c;*.h;*.cpp;*.cc;*.java;*.d;*.php;*.html;*.css;*.js"; const char *AddFilesProgress::DefaultExt = "c,cpp,cc,cxx,h,hpp,hxx,html,css,json,js,jsx,txt,png,jpg,jpeg,rc,xml,mk,paths,makefile,py,java,php"; const char *VsBinaries[] = {"devenv.com", "WDExpress.exe"}; #define USE_OPEN_PROGRESS 1 #define STOP_BUILD_TIMEOUT 2000 #ifdef WINDOWS #define LGI_STATIC_LIBRARY_EXT "lib" #else #define LGI_STATIC_LIBRARY_EXT "a" #endif const char *PlatformNames[] = { "Windows", "Linux", "Mac", "Haiku", 0 }; int PlatformCtrlId[] = { IDC_WIN32, IDC_LINUX, IDC_MAC, IDC_HAIKU, 0 }; const char *PlatformDynamicLibraryExt(IdePlatform Platform) { if (Platform == PlatformWin) return "dll"; if (Platform == PlatformMac) return "dylib"; return "so"; } const char *PlatformSharedLibraryExt(IdePlatform Platform) { if (Platform == PlatformWin) return "lib"; return "a"; } const char *PlatformExecutableExt(IdePlatform Platform) { if (Platform == PlatformWin) return ".exe"; return ""; } char *ToUnixPath(char *s) { if (s) { char *c; while ((c = strchr(s, '\\'))) *c = '/'; } return s; } const char *CastEmpty(char *s) { return s ? s : ""; } bool FindInPath(LString &Exe) { LString::Array Path = LString(getenv("PATH")).Split(LGI_PATH_SEPARATOR); for (unsigned i=0; i SubProc; LString::Array BuildConfigs; LString::Array PostBuild; int AppHnd; enum CompilerType { DefaultCompiler, VisualStudio, MingW, Gcc, CrossCompiler, PythonScript, IAR, Nmake, Cygwin, Xcode, } Compiler; enum ArchType { DefaultArch, ArchX32, ArchX64, ArchArm6, ArchArm7, } Arch; public: BuildThread(IdeProject *proj, char *makefile, bool clean, BuildConfig config, bool all, int wordsize); ~BuildThread(); ssize_t Write(const void *Buffer, ssize_t Size, int Flags = 0) override; LString FindExe(); LAutoString WinToMingWPath(const char *path); int Main() override; }; class IdeProjectPrivate { public: AppWnd *App; IdeProject *Project; bool Dirty, UserFileDirty; LString FileName; IdeProject *ParentProject; IdeProjectSettings Settings; LAutoPtr Thread; LHashTbl, ProjectNode*> Nodes; int NextNodeId; // Threads LAutoPtr CreateMakefile; // User info file LString UserFile; LHashTbl,int> UserNodeFlags; IdeProjectPrivate(AppWnd *a, IdeProject *project) : Project(project), Settings(project) { App = a; Dirty = false; UserFileDirty = false; ParentProject = 0; NextNodeId = 1; } void CollectAllFiles(LTreeNode *Base, LArray &Files, bool SubProjects, int Platform); }; LString ToPlatformPath(const char *s, IdePlatform platform) { LString p = s; if (platform == PlatformWin) return p.Replace("/", "\\"); else return p.Replace("\\", "/"); } class MakefileThread : public LThread, public LCancel { IdeProjectPrivate *d; IdeProject *Proj; IdePlatform Platform; LStream *Log; bool BuildAfterwards; bool HasError; public: static int Instances; MakefileThread(IdeProjectPrivate *priv, IdePlatform platform, bool Build) : LThread("MakefileThread") { Instances++; d = priv; Proj = d->Project; Platform = platform; BuildAfterwards = Build; HasError = false; Log = d->App->GetBuildLog(); Run(); } ~MakefileThread() { Cancel(); while (!IsExited()) LSleep(1); Instances--; } void OnError(const char *Fmt, ...) { va_list Arg; va_start(Arg, Fmt); LStreamPrintf(Log, 0, Fmt, Arg); va_end(Arg); HasError = true; } int Main() { const char *PlatformName = PlatformNames[Platform]; const char *PlatformLibraryExt = NULL; const char *PlatformStaticLibExt = NULL; const char *PlatformExeExt = ""; LString LinkerFlags; const char *TargetType = d->Settings.GetStr(ProjTargetType, NULL, Platform); const char *CompilerName = d->Settings.GetStr(ProjCompiler); LString CCompilerBinary = "gcc"; LString CppCompilerBinary = "g++"; LStream *Log = d->App->GetBuildLog(); bool IsExecutableTarget = TargetType && !stricmp(TargetType, "Executable"); bool IsDynamicLibrary = TargetType && !stricmp(TargetType, "DynamicLibrary"); LAssert(Log); if (!Log) return false; Log->Print("CreateMakefile for '%s'...\n", PlatformName); if (Platform == PlatformWin) { LinkerFlags = ",--enable-auto-import"; } else { if (IsDynamicLibrary) { LinkerFlags = ",-soname,$(TargetFile)"; } LinkerFlags += ",-export-dynamic,-R."; } char Buf[256]; auto MakeFile = Proj->GetMakefile(Platform); Proj->CheckExists(MakeFile); if (!MakeFile) MakeFile = "../Makefile"; // LGI_LIBRARY_EXT switch (Platform) { case PlatformWin: PlatformLibraryExt = "dll"; PlatformStaticLibExt = "lib"; PlatformExeExt = ".exe"; break; case PlatformLinux: case PlatformHaiku: PlatformLibraryExt = "so"; PlatformStaticLibExt = "a"; break; case PlatformMac: PlatformLibraryExt = "dylib"; PlatformStaticLibExt = "a"; break; default: LAssert(0); break; } if (CompilerName) { if (!stricmp(CompilerName, "cross")) { LString CBin = d->Settings.GetStr(ProjCCrossCompiler, NULL, Platform); if (CBin && !LFileExists(CBin)) FindInPath(CBin); if (CBin && LFileExists(CBin)) CCompilerBinary = CBin; else Log->Print("%s:%i - Error: C cross compiler '%s' not found.\n", _FL, CBin.Get()); LString CppBin = d->Settings.GetStr(ProjCppCrossCompiler, NULL, Platform); if (CppBin && !LFileExists(CppBin)) FindInPath(CppBin); if (CppBin && LFileExists(CppBin)) CppCompilerBinary = CppBin; else Log->Print("%s:%i - Error: C++ cross compiler '%s' not found.\n", _FL, CppBin.Get()); } } LFile m; if (!m.Open(MakeFile, O_WRITE)) { Log->Print("Error: Failed to open '%s' for writing.\n", MakeFile.Get()); return false; } m.SetSize(0); m.Print("#!/usr/bin/make\n" "#\n" "# This makefile generated by LgiIde\n" "# http://www.memecode.com/lgi.php\n" "#\n" "\n" ".SILENT :\n" "\n" "CC = %s\n" "CPP = %s\n", CCompilerBinary.Get(), CppCompilerBinary.Get()); // Collect all files that require building LArray Files; d->CollectAllFiles ( Proj, Files, false, 1 << Platform ); auto Base = Proj->GetBasePath(); if (IsExecutableTarget) { LString Exe = Proj->GetExecutable(Platform); if (Exe) { if (LIsRelativePath(Exe)) m.Print("Target = %s\n", ToPlatformPath(Exe, Platform).Get()); else { auto RelExe = LMakeRelativePath(Base, Exe); if (Base && RelExe) { m.Print("Target = %s\n", ToPlatformPath(RelExe, Platform).Get()); } else { Log->Print("%s:%i - Error: Missing path (%s, %s).\n", _FL, Base.Get(), RelExe.Get()); return false; } } } else { Log->Print("%s:%i - Error: No executable name specified (%s, %s).\n", _FL, TargetType, d->FileName.Get()); return false; } } else { LString Target = Proj->GetTargetName(Platform); if (Target) m.Print("Target = %s\n", ToPlatformPath(Target, Platform).Get()); else { Log->Print("%s:%i - Error: No target name specified.\n", _FL); return false; } } // Output the build mode, flags and some paths auto BuildMode = d->App->GetBuildMode(); auto BuildModeName = toString(BuildMode); m.Print("ifndef Build\n" " Build = %s\n" "endif\n", BuildModeName); LString sDefines[BuildMax]; LString sLibs[BuildMax]; LString sIncludes[BuildMax]; const char *ExtraLinkFlags = NULL; const char *ExeFlags = NULL; if (Platform == PlatformWin) { ExtraLinkFlags = ""; ExeFlags = " -mwindows"; m.Print("BuildDir = $(Build)\n" "\n" "Flags = -fPIC -w -fno-inline -fpermissive\n"); const char *DefDefs = "-DWIN32 -D_REENTRANT"; sDefines[BuildDebug] = DefDefs; sDefines[BuildRelease] = DefDefs; } else { LString PlatformCap = PlatformName; ExtraLinkFlags = ""; ExeFlags = ""; m.Print("BuildDir = $(Build)\n" "\n" "Flags = -fPIC -w -fno-inline -fpermissive\n" // -fexceptions ); sDefines[0].Printf("-D%s -D_REENTRANT", PlatformCap.Upper().Get()); #ifdef LINUX sDefines[BuildDebug] += " -D_FILE_OFFSET_BITS=64"; // >:-( sDefines[BuildDebug] += " -DPOSIX"; #endif sDefines[BuildRelease] = sDefines[BuildDebug]; } List Deps; Proj->GetChildProjects(Deps); for (int Cfg = BuildDebug; Cfg < BuildMax; Cfg++) { // Set the config auto cfgName = toString((BuildConfig)Cfg); d->Settings.SetCurrentConfig(cfgName); // Get the defines setup auto PDefs = d->Settings.GetStr(ProjDefines, NULL, Platform); if (ValidStr(PDefs)) { LToken Defs(PDefs, " ;,\r\n"); for (int i=0; iSettings.GetStr(ProjLibraryPaths, NULL, Platform); if (ValidStr(PLibPaths)) { LString::Array LibPaths = PLibPaths.Split("\n"); for (auto i: LibPaths) { LString s, in = i.Strip(); if (!in.Length()) continue; if (strchr("`-", in(0))) { s.Printf(" \\\n\t\t%s", in.Get()); } else { LString Rel; if (!LIsRelativePath(in)) Rel = LMakeRelativePath(Base, in); LString Final = Rel ? Rel.Get() : in.Get(); if (!Proj->CheckExists(Final)) OnError("%s:%i - Library path '%s' doesn't exist (from %s).\n", _FL, Final.Get(), Proj->GetFileName()); s.Printf(" \\\n\t\t-L%s", ToUnixPath(Final)); } sLibs[Cfg] += s; } } const char *PLibs = d->Settings.GetStr(ProjLibraries, NULL, Platform); if (ValidStr(PLibs)) { LToken Libs(PLibs, "\r\n"); for (int i=0; iCheckExists(l); s.Printf(" \\\n\t\t-l%s", ToUnixPath(l)); } sLibs[Cfg] += s; } } for (auto dep: Deps) { LString Target = dep->GetTargetName(Platform); if (Target) { char t[MAX_PATH_LEN]; strcpy_s(t, sizeof(t), Target); if (!strnicmp(t, "lib", 3)) memmove(t, t + 3, strlen(t + 3) + 1); char *dot = strrchr(t, '.'); if (dot) *dot = 0; LString s, sTarget = t; Proj->CheckExists(sTarget); s.Printf(" \\\n\t\t-l%s$(Tag)", ToUnixPath(sTarget)); sLibs[Cfg] += s; auto DepBase = dep->GetBasePath(); if (DepBase) { LString DepPath = DepBase.Get(); auto Rel = LMakeRelativePath(Base, DepPath); LString Final = Rel ? Rel.Get() : DepPath.Get(); Proj->CheckExists(Final); s.Printf(" \\\n\t\t-L%s/$(BuildDir)", ToUnixPath(Final.RStrip("/\\"))); sLibs[Cfg] += s; } } } // Includes // Do include paths LHashTbl,bool> Inc; auto ProjIncludes = d->Settings.GetStr(ProjIncludePaths, NULL, Platform); if (ValidStr(ProjIncludes)) { // Add settings include paths. LToken Paths(ProjIncludes, "\r\n"); for (int i=0; iCheckExists(pn)) OnError("%s:%i - Include path '%s' doesn't exist.\n", _FL, pn.Get()); else if (!Inc.Find(pn)) Inc.Add(pn, true); } } const char *SysIncludes = d->Settings.GetStr(ProjSystemIncludes, NULL, Platform); if (ValidStr(SysIncludes)) { // Add settings include paths. LToken Paths(SysIncludes, "\r\n"); for (int i=0; iCheckExists(pn)) OnError("%s:%i - System include path '%s' doesn't exist (from %s).\n", _FL, pn.Get(), Proj->GetFileName()); else if (!Inc.Find(pn)) Inc.Add(pn, true); } } #if 0 /* Currently this code is adding extra paths that are covered by the official 'IncludePaths' in addition to relative paths in the actual #include parameter. e.g. #include "lgi/common/SomeHeader.h" Hence disabling it for the time being. */ // Add paths of headers for (int i=0; iGetFileName()) { char *e = LGetExtension(n->GetFileName()); if (e && stricmp(e, "h") == 0) { LString Fn = n->GetFileName(); for (char *Dir = Fn; *Dir; Dir++) { if (*Dir == '/' || *Dir == '\\') { *Dir = DIR_CHAR; } } char Path[MAX_PATH_LEN]; strcpy_s(Path, sizeof(Path), Fn); LTrimDir(Path); LString Rel; if (!Proj->RelativePath(Rel, Path)) Rel = Path; if (stricmp(Rel, ".") != 0) { LAutoString RelN = ToNativePath(Rel); if (!Proj->CheckExists(RelN)) OnError("Header include path '%s' doesn't exist.\n", RelN.Get()); else if (!Inc.Find(RelN)) Inc.Add(RelN, true); } } } } #endif LString::Array Incs; for (auto i: Inc) Incs.New() = i.key; Incs.Sort(); for (auto i: Incs) { LString s; if (*i == '`') s.Printf(" \\\n\t\t%s", i.Get()); else s.Printf(" \\\n\t\t-I%s", ToUnixPath(i)); sIncludes[Cfg] += s; } } // Output the defs section for Debug and Release // Debug specific m.Print("\n" "ifeq ($(Build),Debug)\n" " Flags += -g -std=c++14\n" " Tag = d\n" " Defs = -D_DEBUG %s\n" " Libs = %s\n" " Inc = %s\n", CastEmpty(sDefines[0].Get()), CastEmpty(sLibs[0].Get()), CastEmpty(sIncludes[0].Get())); // Release specific m.Print("else\n" " Flags += -s -Os -std=c++14\n" " Defs = %s\n" " Libs = %s\n" " Inc = %s\n" "endif\n" "\n", CastEmpty(sDefines[1].Get()), CastEmpty(sLibs[1].Get()), CastEmpty(sIncludes[1].Get())); if (Files.Length()) { LArray IncPaths; if (Proj->BuildIncludePaths(IncPaths, false, false, Platform)) { // Do source code list... m.Print("# Dependencies\n" "Source =\t"); LString::Array SourceFiles; for (auto &n: Files) { if (n->GetType() == NodeSrc) { auto f = n->GetFileName(); auto path = ToPlatformPath(f, Platform); if (path.Find("./") == 0) path = path(2,-1); SourceFiles.Add(path); } } SourceFiles.Sort(); int c = 0; for (auto &src: SourceFiles) { if (c++) m.Print(" \\\n\t\t\t"); m.Print("%s", src.Get()); } m.Print("\n" "\n" "SourceLst := $(patsubst %%.c,%%.o,$(patsubst %%.cpp,%%.o,$(Source)))\n" "Objects := $(addprefix $(BuildDir)/,$(SourceLst))\n" "Deps := $(patsubst %%.o,%%.d,$(Objects))\n" "\n"); // Write out the target stuff m.Print("# Target\n"); LHashTbl,bool> DepFiles; if (TargetType) { if (IsExecutableTarget) { m.Print("# Executable target\n" "$(Target) :"); LStringPipe Rules; IdeProject *Dep; uint64 Last = LCurrentTime(); int Count = 0; auto It = Deps.begin(); for (Dep=*It; Dep && !IsCancelled(); Dep=*(++It), Count++) { // Get dependency to create it's own makefile... Dep->CreateMakefile(Platform, false); // Build a rule to make the dependency if any of the source changes... auto DepBase = Dep->GetBasePath(); auto Base = Proj->GetBasePath(); auto TargetFile = Dep->GetTargetFile(Platform); if (DepBase && Base && TargetFile) { LString Rel; if (!Proj->RelativePath(Rel, DepBase)) Rel = DepBase; ToUnixPath(Rel); // Add tag to target name auto Parts = TargetFile.SplitDelimit("."); if (Parts.Length() == 2) TargetFile.Printf("lib%s$(Tag).%s", Parts[0].Get(), Parts[1].Get()); sprintf(Buf, "%s/$(BuildDir)/%s", Rel.Get(), TargetFile.Get()); m.Print(" %s", Buf); LArray AllDeps; Dep->GetAllDependencies(AllDeps, Platform); LAssert(AllDeps.Length() > 0); AllDeps.Sort(StrSort); Rules.Print("%s : ", Buf); for (int i=0; iRelativePath(DepRel, AllDeps[i]) ? DepRel.Get() : AllDeps[i]; ToUnixPath(f); Rules.Print("%s", f); // Add these dependencies to this makefiles dep list if (!DepFiles.Find(f)) DepFiles.Add(f, true); } AllDeps.DeleteArrays(); Rules.Print("\n\texport Build=$(Build); \\\n" "\t$(MAKE) -C %s", Rel.Get()); auto Mk = Dep->GetMakefile(Platform); // RenameMakefileForPlatform(Mk, Platform); if (Mk) { char *DepMakefile = strrchr(Mk, DIR_CHAR); if (DepMakefile) Rules.Print(" -f %s", DepMakefile + 1); } else { Mk = Dep->GetMakefile(Platform); OnError("%s:%i - No makefile for '%s'\n", _FL, Dep->GetFullPath().Get()); } Rules.Print("\n\n"); } uint64 Now = LCurrentTime(); if (Now - Last > 1000) { Last = Now; Log->Print("Building deps %i%%...\n", (int) (((int64)Count+1)*100/Deps.Length())); } } m.Print(" $(Objects)\n" " mkdir -p $(BuildDir)\n" " @echo Linking $(Target) [$(Build)]...\n" " $(CPP)%s%s %s%s -o \\\n" " $(Target) $(Objects) $(Libs)\n", ExtraLinkFlags, ExeFlags, ValidStr(LinkerFlags) ? "-Wl" : "", LinkerFlags.Get()); if (Platform == PlatformHaiku) { // Is there an application icon configured? const char *AppIcon = d->Settings.GetStr(ProjApplicationIcon, NULL, Platform); if (AppIcon) { m.Print(" addattr -f %s -t \"'VICN'\" \"BEOS:ICON\" $(Target)\n", AppIcon); } } LString PostBuildCmds = d->Settings.GetStr(ProjPostBuildCommands, NULL, Platform); if (ValidStr(PostBuildCmds)) { LString::Array a = PostBuildCmds.Split("\n"); for (unsigned i=0; iGetMakefile(Platform); if (mk) { LAutoString my_base = Proj->GetBasePath(); LAutoString dep_base = d->GetBasePath(); d->CheckExists(dep_base); auto rel_dir = LMakeRelativePath(my_base, dep_base); d->CheckExists(rel_dir); char *mk_leaf = strrchr(mk, DIR_CHAR); m.Print(" +make -C \"%s\" -f \"%s\" clean\n", ToUnixPath(rel_dir ? rel_dir.Get() : dep_base.Get()), ToUnixPath(mk_leaf ? mk_leaf + 1 : mk.Get())); } } m.Print("\n"); } // Shared library else if (!stricmp(TargetType, "DynamicLibrary")) { m.Print("TargetFile = lib$(Target)$(Tag).%s\n" "$(TargetFile) : $(Objects)\n" " mkdir -p $(BuildDir)\n" " @echo Linking $(TargetFile) [$(Build)]...\n" " $(CPP)$s -shared \\\n" " %s%s \\\n" " -o $(BuildDir)/$(TargetFile) \\\n" " $(Objects) \\\n" " $(Libs)\n", PlatformLibraryExt, ValidStr(ExtraLinkFlags) ? "-Wl" : "", ExtraLinkFlags, LinkerFlags.Get()); LString PostBuildCmds = d->Settings.GetStr(ProjPostBuildCommands, NULL, Platform); if (ValidStr(PostBuildCmds)) { LString::Array a = PostBuildCmds.Split("\n"); for (unsigned i=0; iSettings.GetStr(ProjPostBuildCommands, NULL, Platform); if (ValidStr(PostBuildCmds)) { LString::Array a = PostBuildCmds.Split("\n"); for (unsigned i=0; iCheckExists(p); if (p && !strchr(p, '`')) { if (!LIsRelativePath(p)) { auto a = LMakeRelativePath(Base, p); m.Print("\t%s \\\n", ToPlatformPath(a ? a.Get() : p.Get(), Platform).Get()); } else { m.Print("\t%s \\\n", ToPlatformPath(p, Platform).Get()); } } } m.Print("\t$(BuildDir)\n\n"); const char *OtherMakefileRules = d->Settings.GetStr(ProjMakefileRules, NULL, Platform); if (ValidStr(OtherMakefileRules)) { m.Print("\n%s\n", OtherMakefileRules); } } } else { m.Print("# No files require building.\n"); } Log->Print("...Done: '%s'\n", MakeFile.Get()); if (BuildAfterwards) { if (!Proj->GetApp()->PostEvent(M_START_BUILD)) printf("%s:%i - PostEvent(M_START_BUILD) failed.\n", _FL); } return HasError; } void OnAfterMain() { Proj->GetApp()->PostEvent(M_MAKEFILES_CREATED, (LMessage::Param)Proj); } }; ///////////////////////////////////////////////////////////////////////////////////// NodeSource::~NodeSource() { if (nView) { nView->nSrc = 0; } } NodeView::~NodeView() { if (nSrc) { nSrc->nView = 0; } } ////////////////////////////////////////////////////////////////////////////////// int NodeSort(LTreeItem *a, LTreeItem *b, NativeInt d) { ProjectNode *A = dynamic_cast(a); ProjectNode *B = dynamic_cast(b); if (A && B) { if ( (A->GetType() == NodeDir) ^ (B->GetType() == NodeDir) ) { return A->GetType() == NodeDir ? -1 : 1; } else { return Stricmp(a->GetText(0), b->GetText(0)); } } return 0; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool ReadVsProjFile(LString File, LString &Ver, LString::Array &Configs) { const char *Ext = LGetExtension(File); if (!Ext || _stricmp(Ext, "vcxproj")) return false; LFile f; if (!f.Open(File, O_READ)) return false; LXmlTree Io; LXmlTag r; if (Io.Read(&r, &f) && r.IsTag("Project")) { Ver = r.GetAttr("ToolsVersion"); LXmlTag *ItemGroup = r.GetChildTag("ItemGroup"); if (ItemGroup) for (auto c: ItemGroup->Children) { if (c->IsTag("ProjectConfiguration")) { char *s = c->GetAttr("Include"); if (s) Configs.New() = s; } } } else return false; return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// BuildThread::BuildThread(IdeProject *proj, char *makefile, bool clean, BuildConfig config, bool all, int wordsize) : LThread("BuildThread") { Proj = proj; Makefile = makefile; Clean = clean; Config = config; All = all; WordSize = wordsize; Arch = DefaultArch; Compiler = DefaultCompiler; AppHnd = Proj->GetApp()->AddDispatch(); LString Cmds = proj->d->Settings.GetStr(ProjPostBuildCommands, NULL); if (ValidStr(Cmds)) PostBuild = Cmds.SplitDelimit("\r\n"); auto Ext = LGetExtension(Makefile); if (Ext && !_stricmp(Ext, "py")) Compiler = PythonScript; else if (Ext && !_stricmp(Ext, "sln")) Compiler = VisualStudio; else if (Ext && !Stricmp(Ext, "xcodeproj")) Compiler = Xcode; else { auto Comp = Proj->GetSettings()->GetStr(ProjCompiler); if (Comp) { // Use the specified compiler... if (!stricmp(Comp, "VisualStudio")) Compiler = VisualStudio; else if (!stricmp(Comp, "MingW")) Compiler = MingW; else if (!stricmp(Comp, "gcc")) Compiler = Gcc; else if (!stricmp(Comp, "cross")) Compiler = CrossCompiler; else if (!stricmp(Comp, "IAR")) Compiler = IAR; else if (!stricmp(Comp, "Cygwin")) Compiler = Cygwin; else LAssert(!"Unknown compiler."); } } if (Compiler == DefaultCompiler) { // Use default compiler for platform... #ifdef WIN32 Compiler = VisualStudio; #else Compiler = Gcc; #endif } Run(); } BuildThread::~BuildThread() { if (SubProc) { bool b = SubProc->Interrupt(); LgiTrace("%s:%i - Sub process interrupt = %i.\n", _FL, b); } else LgiTrace("%s:%i - No sub process to interrupt.\n", _FL); uint64 Start = LCurrentTime(); bool Killed = false; while (!IsExited()) { LSleep(10); if (LCurrentTime() - Start > STOP_BUILD_TIMEOUT && SubProc) { if (Killed) { // Thread is stuck as well... ok kill that too!!! Argh - kill all the things!!!! Terminate(); LgiTrace("%s:%i - Thread killed.\n", _FL); Proj->GetApp()->PostEvent(M_BUILD_DONE); break; } else { // Kill the sub-process... bool b = SubProc->Kill(); Killed = true; LgiTrace("%s:%i - Sub process killed.\n", _FL, b); Start = LCurrentTime(); } } } } ssize_t BuildThread::Write(const void *Buffer, ssize_t Size, int Flags) { if (Proj->GetApp()) { Proj->GetApp()->PostEvent(M_APPEND_TEXT, (LMessage::Param)NewStr((char*)Buffer, Size), AppWnd::BuildTab); } return Size; } #pragma comment(lib, "version.lib") struct ProjInfo { LString Guid, Name, File; LHashTbl,ssize_t> Configs; }; LString BuildThread::FindExe() { LString::Array p = LGetPath(); if (Compiler == PythonScript) { #if defined(WINDOWS) uint32_t BestVer = 0; #else LString BestVer; #endif static LString Best; if (!Best) { const char *binName[] = {"python3", "python"}; for (int i=0; idwProductVersionMS > BestVer) { BestVer = v->dwProductVersionMS; Best = Path; } } } else if (!Best) { Best = Path; } free(Buf); #else LSubProcess p(Path, "--version"); LStringPipe o; if (p.Start()) p.Communicate(&o); - auto Out = o.NewGStr(); + auto Out = o.NewLStr(); auto Ver = Out.SplitDelimit().Last(); printf("Ver=%s\n", Ver.Get()); if (!BestVer || Stricmp(Ver.Get(), BestVer.Get()) > 0) { Best = Path; BestVer = Ver; } break; #endif } } } } return Best; } else if (Compiler == VisualStudio) { // Find the version we need: double fVer = 0.0; ProjInfo *First = NULL; LHashTbl, ProjInfo*> Projects; const char *Ext = LGetExtension(Makefile); if (Ext && !_stricmp(Ext, "sln")) { LFile f; if (f.Open(Makefile, O_READ)) { LString ProjKey = "Project("; LString StartSection = "GlobalSection("; LString EndSection = "EndGlobalSection"; LString Section; ssize_t Pos; LString::Array Ln = f.Read().SplitDelimit("\r\n"); for (size_t i = 0; i < Ln.Length(); i++) { LString s = Ln[i].Strip(); if ((Pos = s.Find(ProjKey)) >= 0) { LString::Array p = s.SplitDelimit("(),="); if (p.Length() > 5) { ProjInfo *i = new ProjInfo; i->Name = p[3].Strip(" \t\""); i->File = p[4].Strip(" \t\'\""); i->Guid = p[5].Strip(" \t\""); if (LIsRelativePath(i->File)) { char f[MAX_PATH_LEN]; LMakePath(f, sizeof(f), Makefile, ".."); LMakePath(f, sizeof(f), f, i->File); if (LFileExists(f)) i->File = f; /* else LAssert(0); */ } if (!First) First = i; Projects.Add(i->Guid, i); } } else if (s.Find(StartSection) >= 0) { auto p = s.SplitDelimit("() \t"); Section = p[1]; } else if (s.Find(EndSection) >= 0) { Section.Empty(); } else if (Section == "ProjectConfigurationPlatforms") { auto p = s.SplitDelimit(". \t"); auto i = Projects.Find(p[0]); if (i) { if (!i->Configs.Find(p[1])) { auto Idx = i->Configs.Length() + 1; i->Configs.Add(p[1], Idx); } } } else if (Section == "SolutionConfigurationPlatforms") { auto p = s.SplitDelimit(); auto config = p[0]; for (auto &it: Projects) { auto proj = it.value; if (!proj->Configs.Find(config)) proj->Configs.Add(config, proj->Configs.Length()+1); } } } } } else if (Ext && !_stricmp(Ext, "vcxproj")) { // ProjFile = Makefile; } else { if (Arch == DefaultArch) { if (sizeof(size_t) == 4) Arch = ArchX32; else Arch = ArchX64; } #ifdef _MSC_VER // Nmake file.. LString NmakePath; switch (_MSC_VER) { #ifdef _MSC_VER_VS2013 case _MSC_VER_VS2013: { if (Arch == ArchX32) NmakePath = "c:\\Program Files (x86)\\Microsoft Visual Studio 12.0\\VC\\bin\\nmake.exe"; else NmakePath = "c:\\Program Files (x86)\\Microsoft Visual Studio 12.0\\VC\\bin\\amd64\\nmake.exe"; break; } #endif #ifdef _MSC_VER_VS2015 case _MSC_VER_VS2015: { if (Arch == ArchX32) NmakePath = "c:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\VC\\bin\\nmake.exe"; else NmakePath = "c:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\VC\\bin\\amd64\\nmake.exe"; break; } #endif default: { LAssert(!"Impl me."); break; } } if (LFileExists(NmakePath)) { Compiler = Nmake; return NmakePath; } #endif } /* if (ProjFile && LFileExists(ProjFile)) { LString sVer; if (ReadVsProjFile(ProjFile, sVer, BuildConfigs)) { fVer = sVer.Float(); } } */ if (First) { for (auto i: First->Configs) { BuildConfigs[i.value - 1] = i.key; LFile f(First->File, O_READ); LXmlTree t; LXmlTag r; if (t.Read(&r, &f)) { if (r.IsTag("Project")) { auto ToolsVersion = r.GetAttr("ToolsVersion"); if (ToolsVersion) { fVer = atof(ToolsVersion); } } } } } if (fVer > 0.0) { for (int i=0; i LatestVer) { LatestVer = p[2].Float(); Latest = n; } } } if (Latest && LMakePath(p, sizeof(p), p, Latest) && LMakePath(p, sizeof(p), p, "common\\bin\\IarBuild.exe")) { if (LFileExists(p)) return p; } } else if (Compiler == Xcode) { return "/usr/bin/xcodebuild"; } else if (Compiler == Cygwin) { #ifdef WINDOWS LRegKey k(false, "HKEY_CURRENT_USER\\Software\\Cygwin\\Installations"); List n; k.GetValueNames(n); LString s; for (auto i:n) { s = k.GetStr(i); if (s.Find("\\??\\") == 0) s = s(4,-1); if (LDirExists(s)) { CygwinPath = s; break; } } n.DeleteArrays(); LFile::Path p(s, "bin\\make.exe"); if (p.Exists()) return p.GetFull(); #endif } else { if (Compiler == MingW) { // Have a look in the default spot first... const char *Def = "C:\\MinGW\\msys\\1.0\\bin\\make.exe"; if (LFileExists(Def)) { return Def; } } for (int i=0; iGetApp()->GetOptions()->GetValue(OPT_Jobs, Jobs) || Jobs.CastInt32() < 1) Jobs = 2; auto Pos = InitDir.RFind(DIR_STR); if (Pos) InitDir.Length(Pos); LString TmpArgs, Include, Lib, LibPath, Path; if (Compiler == VisualStudio) { // TmpArgs.Printf("\"%s\" /make \"All - Win32 Debug\"", Makefile.Get()); LString BuildConf = "All - Win32 Debug"; if (BuildConfigs.Length()) { auto Key = toString(Config); for (size_t i=0; i= 0) { if (!BuildConf || (c.Find("x64") >= 0 && BuildConf.Find("x64") < 0)) BuildConf = c; } } } TmpArgs.Printf("\"%s\" %s \"%s\"", Makefile.Get(), Clean ? "/Clean" : "/Build", BuildConf.Get()); } else if (Compiler == Nmake) { const char *DefInc[] = { "C:\\Program Files (x86)\\Microsoft Visual Studio 12.0\\VC\\INCLUDE", "C:\\Program Files (x86)\\Microsoft Visual Studio 12.0\\VC\\ATLMFC\\INCLUDE", "C:\\Program Files (x86)\\Windows Kits\\8.1\\include\\shared", "C:\\Program Files (x86)\\Windows Kits\\8.1\\include\\um", "C:\\Program Files (x86)\\Windows Kits\\8.1\\include\\winrt" }; LString f; #define ADD_PATHS(out, in) \ for (unsigned i=0; iGetChildTag("name") : NULL; if (c) Conf = c->GetContent(); } } TmpArgs.Printf("\"%s\" %s %s -log warnings", Makefile.Get(), Clean ? "-clean" : "-make", Conf.Get()); } else if (Compiler == Xcode) { LString::Array Configs; Configs.SetFixedLength(false); LString a; a.Printf("-list -project \"%s\"", Makefile.Get()); LSubProcess Ls(Exe, a); if (Ls.Start()) { LStringPipe o; Ls.Communicate(&o); auto key = "Build Configurations:"; - auto lines = o.NewGStr().SplitDelimit("\n"); + auto lines = o.NewLStr().SplitDelimit("\n"); int inKey = -1; for (auto l: lines) { int ws = 0; for (int i=0; i=0) inKey = ws; else if (inKey>=0) { if (ws>inKey) { Configs.New() = l.Strip(); } else { inKey = -1; } } // printf("%s\n", l.Get()); } } auto Config = Configs.Length() > 0 ? Configs[0] : LString("Debug"); TmpArgs.Printf("-project \"%s\" -configuration %s", Makefile.Get(), Config.Get()); } else { if (Compiler == Cygwin) { LFile::Path p(CygwinPath, "bin"); Path = p.GetFull(); } if (Compiler == MingW) { LString a; char *Dir = strrchr(MakePath, DIR_CHAR); #if 1 TmpArgs.Printf("/C \"%s\"", Exe.Get()); /* As of MSYS v1.0.18 the support for multiple jobs causes make to hang: http://sourceforge.net/p/mingw/bugs/1950/ http://mingw-users.1079350.n2.nabble.com/MSYS-make-freezes-td7579038.html Apparently it'll be "fixed" in v1.0.19. We'll see. >:-( if (Jobs.CastInt32() > 1) a.Print(" -j %i", Jobs.CastInt32()); */ a.Printf(" -f \"%s\"", Dir ? Dir + 1 : MakePath.Get()); TmpArgs += a; #else TmpArgs.Printf("/C set"); #endif Exe = "C:\\Windows\\System32\\cmd.exe"; } else { if (Jobs.CastInt32()) TmpArgs.Printf("-j %i -f \"%s\"", Jobs.CastInt32(), MakePath.Get()); else TmpArgs.Printf("-f \"%s\"", MakePath.Get()); } if (Clean) { if (All) TmpArgs += " cleanall"; else TmpArgs += " clean"; } if (Config == BuildRelease) TmpArgs += " Build=Release"; } PostThreadEvent(AppHnd, M_SELECT_TAB, AppWnd::BuildTab); LString Msg; Msg.Printf("Making: %s\n", MakePath.Get()); Proj->GetApp()->PostEvent(M_APPEND_TEXT, (LMessage::Param)NewStr(Msg), AppWnd::BuildTab); LgiTrace("%s %s\n", Exe.Get(), TmpArgs.Get()); if (SubProc.Reset(new LSubProcess(Exe, TmpArgs))) { SubProc->SetNewGroup(false); SubProc->SetInitFolder(InitDir); if (Include) SubProc->SetEnvironment("INCLUDE", Include); if (Lib) SubProc->SetEnvironment("LIB", Lib); if (LibPath) SubProc->SetEnvironment("LIBPATHS", LibPath); if (Path) { LString Cur = getenv("PATH"); LString New = Cur + LGI_PATH_SEPARATOR + Path; SubProc->SetEnvironment("PATH", New); } // SubProc->SetEnvironment("DLL", "1"); if (Compiler == MingW) SubProc->SetEnvironment("PATH", "c:\\MingW\\bin;C:\\MinGW\\msys\\1.0\\bin;%PATH%"); if ((Status = SubProc->Start(true, false))) { // Read all the output char Buf[256]; ssize_t rd; while ( (rd = SubProc->Read(Buf, sizeof(Buf))) > 0 ) { Write(Buf, rd); } uint32_t ex = SubProc->Wait(); Print("Make exited with %i (0x%x)\n", ex, ex); if (Compiler == IAR && ex == 0 && PostBuild.Length()) { for (auto Cmd : PostBuild) { auto p = Cmd.Split(" ", 1); if (p[0].Equals("cd")) { if (p.Length() > 1) FileDev->SetCurrentFolder(p[1]); else LAssert(!"No folder for cd?"); } else { LSubProcess PostCmd(p[0], p.Length() > 1 ? p[1] : LString()); if (PostCmd.Start(true, false)) { char Buf[256]; ssize_t rd; while ( (rd = PostCmd.Read(Buf, sizeof(Buf))) > 0 ) { Write(Buf, rd); } } } } } } else { // Create a nice error message. LString ErrStr = LErrorCodeToString(SubProc->GetErrorCode()); if (ErrStr) { char *e = ErrStr.Get() + ErrStr.Length(); while (e > ErrStr && strchr(" \t\r\n.", e[-1])) *(--e) = 0; } sprintf_s(ErrBuf, sizeof(ErrBuf), "Running make failed with %i (%s)\n", SubProc->GetErrorCode(), ErrStr.Get()); Err = ErrBuf; } } } else { Err = "Couldn't find program to build makefile."; LgiTrace("%s,%i - %s.\n", _FL, Err); } AppWnd *w = Proj->GetApp(); if (w) { w->PostEvent(M_BUILD_DONE); if (Err) Proj->GetApp()->PostEvent(M_BUILD_ERR, 0, (LMessage::Param)NewStr(Err)); } else LAssert(0); return 0; } ////////////////////////////////////////////////////////////////////////////////// IdeProject::IdeProject(AppWnd *App) : IdeCommon(NULL) { Project = this; d = new IdeProjectPrivate(App, this); Tag = NewStr("Project"); } IdeProject::~IdeProject() { d->App->OnProjectDestroy(this); LXmlTag::Empty(true); DeleteObj(d); } bool IdeProject::OnNode(const char *Path, ProjectNode *Node, bool Add) { if (!Path || !Node) { LAssert(0); return false; } char Full[MAX_PATH_LEN]; if (LIsRelativePath(Path)) { LAutoString Base = GetBasePath(); if (LMakePath(Full, sizeof(Full), Base, Path)) { Path = Full; } } bool Status = false; if (Add) Status = d->Nodes.Add(Path, Node); else Status = d->Nodes.Delete(Path); LString p = Path; if (Status && CheckExists(p)) d->App->OnNode(p, Node, Add ? FindSymbolSystem::FileAdd : FindSymbolSystem::FileRemove); return Status; } void IdeProject::ShowFileProperties(const char *File) { ProjectNode *Node = NULL; // char *fp = FindFullPath(File, &Node); if (Node) { Node->OnProperties(); } } const char *IdeProject::GetFileComment() { return d->Settings.GetStr(ProjCommentFile); } const char *IdeProject::GetFunctionComment() { return d->Settings.GetStr(ProjCommentFunction); } IdeProject *IdeProject::GetParentProject() { return d->ParentProject; } void IdeProject::SetParentProject(IdeProject *p) { d->ParentProject = p; } bool IdeProject::GetChildProjects(List &c) { CollectAllSubProjects(c); return c.Length() > 0; } bool IdeProject::RelativePath(LString &Out, const char *In, bool Debug) { if (!In) return false; auto Base = GetBasePath(); if (!Base) return false; CheckExists(Base); if (Debug) LgiTrace("XmlBase='%s'\n In='%s'\n", Base.Get(), In); LToken b(Base, DIR_STR); LToken i(In, DIR_STR); char out[MAX_PATH_LEN] = ""; if (Debug) LgiTrace("Len %i-%i\n", b.Length(), i.Length()); auto ILen = i.Length() + (LDirExists(In) ? 0 : 1); auto Max = MIN(b.Length(), ILen); int Common = 0; for (; Common < Max; Common++) { #ifdef WIN32 #define StrCompare stricmp #else #define StrCompare strcmp #endif if (Debug) LgiTrace("Cmd '%s'-'%s'\n", b[Common], i[Common]); if (StrCompare(b[Common], i[Common]) != 0) { break; } } if (Debug) LgiTrace("Common=%i\n", Common); if (Common > 0) { if (Common < b.Length()) { out[0] = 0; auto Back = b.Length() - Common; if (Debug) LgiTrace("Back=%i\n", (int)Back); for (int n=0; nSettings.GetStr(ProjExe); LString Exe; if (!PExe) { // Use the default exe name? Exe = GetExecutable(GetCurrentPlatform()); if (Exe) { printf("Exe='%s'\n", Exe.Get()); PExe = Exe; } } if (!PExe) return false; if (LIsRelativePath(PExe)) { auto Base = GetBasePath(); if (Base) LMakePath(Path, Len, Base, PExe); else return false; } else { strcpy_s(Path, Len, PExe); } return true; } LString IdeProject::GetMakefile(IdePlatform Platform) { const char *PMakefile = d->Settings.GetStr(ProjMakefile, NULL, Platform); if (!PMakefile) return LString(); LString Path; if (LIsRelativePath(PMakefile)) { auto Base = GetBasePath(); if (Base) { char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), Base, PMakefile); Path = p; } } else { Path = PMakefile; } return Path; } void IdeProject::Clean(bool All, BuildConfig Config) { if (!d->Thread && d->Settings.GetStr(ProjMakefile)) { auto m = GetMakefile(PlatformCurrent); if (m) { CheckExists(m); d->Thread.Reset(new BuildThread(this, m, true, Config, All, sizeof(ssize_t)*8)); } } } char *QuoteStr(char *s) { LStringPipe p(256); while (s && *s) { if (*s == ' ') { p.Push("\\ ", 2); } else p.Push(s, 1); s++; } return p.NewStr(); } class ExecuteThread : public LThread, public LStream, public LCancel { IdeProject *Proj; LString Exe, Args, Path; int Len; ExeAction Act; int AppHnd; public: ExecuteThread(IdeProject *proj, const char *exe, const char *args, char *path, ExeAction act) : LThread("ExecuteThread") { Len = 32 << 10; Proj = proj; Act = act; Exe = exe; Args = args; Path = path; DeleteOnExit = true; AppHnd = proj->GetApp()->AddDispatch(); Run(); } ~ExecuteThread() { Cancel(); while (!IsExited()) LSleep(1); } int Main() override { PostThreadEvent(AppHnd, M_SELECT_TAB, AppWnd::OutputTab); PostThreadEvent(AppHnd, M_APPEND_TEXT, 0, AppWnd::OutputTab); if (Exe) { if (Act == ExeDebug) { LSubProcess sub("kdbg", Exe); if (Path) sub.SetInitFolder(Path); if (sub.Start()) sub.Communicate(this, NULL, this); } else if (Act == ExeValgrind) { #ifdef LINUX LString ExePath = Proj->GetExecutable(GetCurrentPlatform()); if (ExePath) { char Path[MAX_PATH_LEN]; char *ExeLeaf = LGetLeaf(Exe); strcpy_s(Path, sizeof(Path), ExeLeaf ? ExeLeaf : Exe.Get()); LTrimDir(Path); char *Term = 0; char *WorkDir = 0; char *Execute = 0; switch (LGetWindowManager()) { case WM_Kde: Term = "konsole"; WorkDir = "--workdir "; Execute = "-e"; break; case WM_Gnome: Term = "gnome-terminal"; WorkDir = "--working-directory="; Execute = "-x"; break; } if (Term && WorkDir && Execute) { char *e = QuoteStr(ExePath); char *p = QuoteStr(Path); char *a = Proj->GetExeArgs() ? Proj->GetExeArgs() : (char*)""; char Args[512]; sprintf(Args, "%s%s " "--noclose " "%s valgrind --tool=memcheck --num-callers=20 %s %s", WorkDir, p, Execute, e, a); LExecute(Term, Args); } } #endif } else { LSubProcess sub(Exe, Args); if (Path) sub.SetInitFolder(Path); if (sub.Start()) sub.Communicate(this, NULL, this); } } return 0; } ssize_t Write(const void *Buffer, ssize_t Size, int Flags = 0) override { if (Len <= 0) return 0; PostThreadEvent(AppHnd, M_APPEND_TEXT, (LMessage::Param)NewStr((char*)Buffer, Size), AppWnd::OutputTab); Len -= Size; return Size; } }; LDebugContext *IdeProject::Execute(ExeAction Act, LString *ErrMsg) { auto Base = GetBasePath(); if (!Base) { if (ErrMsg) *ErrMsg = "No base path for project."; return NULL; } char e[MAX_PATH_LEN]; if (!GetExePath(e, sizeof(e))) { if (ErrMsg) *ErrMsg = "GetExePath failed."; return NULL; } if (!LFileExists(e)) { if (ErrMsg) ErrMsg->Printf("Executable '%s' doesn't exist.\n", e); return NULL; } const char *Args = d->Settings.GetStr(ProjArgs); const char *Env = d->Settings.GetStr(ProjEnv); LString InitDir = d->Settings.GetStr(ProjInitDir); int RunAsAdmin = d->Settings.GetInt(ProjDebugAdmin); #ifndef HAIKU if (Act == ExeDebug) { if (InitDir && LIsRelativePath(InitDir)) { LFile::Path p(Base); p += InitDir; InitDir = p.GetFull(); } return new LDebugContext(d->App, this, e, Args, RunAsAdmin != 0, Env, InitDir); } #endif new ExecuteThread( this, e, Args, Base, #if defined(HAIKU) || defined(WINDOWS) ExeRun // No gdb or valgrind #else Act #endif ); return NULL; } bool IdeProject::IsMakefileAScript() { auto m = GetMakefile(PlatformCurrent); if (m) { auto Ext = LGetExtension(m); if (!Stricmp(Ext, "py")) { // Not a makefile but a build script... can't update. return true; } } return false; } bool IdeProject::IsMakefileUpToDate() { if (IsMakefileAScript()) return true; // We can't know if it's up to date... List Proj; GetChildProjects(Proj); Proj.Insert(this); for (auto p: Proj) { // Is the project file modified after the makefile? auto Proj = p->GetFullPath(); uint64 ProjModTime = 0, MakeModTime = 0; LDirectory dir; if (dir.First(Proj)) { ProjModTime = dir.GetLastWriteTime(); dir.Close(); } auto m = p->GetMakefile(PlatformCurrent); if (!m) { d->App->GetBuildLog()->Print("Error: no makefile? (%s:%i)\n", _FL); break; } auto Ext = LGetExtension(m); if (!Stricmp(Ext, "py")) { // Not a makefile but a build script... can't update. return true; } if (dir.First(m)) { MakeModTime = dir.GetLastWriteTime(); dir.Close(); } // printf("Proj=%s - Timestamps " LGI_PrintfInt64 " - " LGI_PrintfInt64 "\n", Proj.Get(), ProjModTime, MakeModTime); if (ProjModTime != 0 && MakeModTime != 0 && ProjModTime > MakeModTime) { // Need to rebuild the makefile... return false; } } return true; } bool IdeProject::FindDuplicateSymbols() { LStream *Log = d->App->GetBuildLog(); Log->Print("FindDuplicateSymbols starting...\n"); List Proj; CollectAllSubProjects(Proj); Proj.Insert(this); int Lines = 0; LHashTbl,int64> Map(200000); int Found = 0; for (auto p: Proj) { LString s = p->GetExecutable(GetCurrentPlatform()); if (s) { LString Args; Args.Printf("--print-size --defined-only -C %s", s.Get()); LSubProcess Nm("nm", Args); if (Nm.Start(true, false)) { char Buf[256]; LStringPipe q; for (ssize_t Rd = 0; (Rd = Nm.Read(Buf, sizeof(Buf))); ) q.Write(Buf, Rd); - LString::Array a = q.NewGStr().SplitDelimit("\r\n"); + LString::Array a = q.NewLStr().SplitDelimit("\r\n"); LHashTbl,bool> Local(200000); for (auto &Ln: a) { LString::Array p = Ln.SplitDelimit(" \t", 3); if (!Local.Find(p.Last())) { Local.Add(p.Last(), true); // const char *Sz = p[1]; int64 Ours = p[1].Int(16); int64 Theirs = Map.Find(p.Last()); if (Theirs >= 0) { if (Ours != Theirs) { if (Found++ < 100) Log->Print(" %s (" LPrintfInt64 " -> " LPrintfInt64 ")\n", p.Last().Get(), Ours, Theirs); } } else if (Ours >= 0) { Map.Add(p.Last(), Ours); } else { printf("Bad line: %s\n", Ln.Get()); } } Lines++; } } } else printf("%s:%i - GetExecutable failed.\n", _FL); } /* char *Sym; for (int Count = Map.First(&Sym); Count; Count = Map.Next(&Sym)) { if (Count > 1) Log->Print(" %i: %s\n", Count, Sym); } */ Log->Print("FindDuplicateSymbols finished (%i lines)\n", Lines); return false; } bool IdeProject::FixMissingFiles() { FixMissingFilesDlg(this); return true; } void IdeProject::Build(bool All, BuildConfig Config) { if (d->Thread) { d->App->GetBuildLog()->Print("Error: Already building (%s:%i)\n", _FL); return; } auto m = GetMakefile(PlatformCurrent); CheckExists(m); if (!m) { d->App->GetBuildLog()->Print("Error: no makefile? (%s:%i)\n", _FL); return; } if (GetApp()) GetApp()->PostEvent(M_APPEND_TEXT, 0, 0); SetClean([&](bool ok) { if (!ok) return; if (!IsMakefileUpToDate()) CreateMakefile(GetCurrentPlatform(), true); else // Start the build thread... d->Thread.Reset ( new BuildThread ( this, m, false, Config, All, sizeof(size_t)*8 ) ); }); } void IdeProject::StopBuild() { d->Thread.Reset(); } bool IdeProject::Serialize(bool Write) { return true; } AppWnd *IdeProject::GetApp() { return d->App; } const char *IdeProject::GetIncludePaths() { return d->Settings.GetStr(ProjIncludePaths); } const char *IdeProject::GetPreDefinedValues() { return d->Settings.GetStr(ProjDefines); } const char *IdeProject::GetExeArgs() { return d->Settings.GetStr(ProjArgs); } LString IdeProject::GetExecutable(IdePlatform Platform) { LString Bin = d->Settings.GetStr(ProjExe, NULL, Platform); auto TargetType = d->Settings.GetStr(ProjTargetType, NULL, Platform); auto Base = GetBasePath(); if (Bin) { if (LIsRelativePath(Bin) && Base) { char p[MAX_PATH_LEN]; if (LMakePath(p, sizeof(p), Base, Bin)) Bin = p; } return Bin; } // Create binary name from target: auto Target = GetTargetName(Platform); if (Target) { bool IsLibrary = Stristr(TargetType, "library"); int BuildMode = d->App->GetBuildMode(); const char *Name = BuildMode ? "Release" : "Debug"; const char *Postfix = BuildMode ? "" : "d"; switch (Platform) { case PlatformWin: { if (IsLibrary) Bin.Printf("%s%s.dll", Target.Get(), Postfix); else Bin = Target; break; } case PlatformMac: { if (IsLibrary) Bin.Printf("lib%s%s.dylib", Target.Get(), Postfix); else Bin = Target; break; } case PlatformLinux: case PlatformHaiku: { if (IsLibrary) Bin.Printf("lib%s%s.so", Target.Get(), Postfix); else Bin = Target; break; } default: { LAssert(0); printf("%s:%i - Unknown platform.\n", _FL); return LString(); } } // Find the actual file... if (!Base) { printf("%s:%i - GetBasePath failed.\n", _FL); return LString(); } char Path[MAX_PATH_LEN]; LMakePath(Path, sizeof(Path), Base, Name); LMakePath(Path, sizeof(Path), Path, Bin); if (LFileExists(Path)) Bin = Path; else printf("%s:%i - '%s' doesn't exist.\n", _FL, Path); return Bin; } return LString(); } const char *IdeProject::GetFileName() { return d->FileName; } int IdeProject::GetPlatforms() { return PLATFORM_ALL; } LAutoString IdeProject::GetFullPath() { LAutoString Status; if (!d->FileName) { // LAssert(!"No path."); return Status; } LArray sections; IdeProject *proj = this; while ( proj && proj->GetFileName() && LIsRelativePath(proj->GetFileName())) { sections.AddAt(0, proj->GetFileName()); proj = proj->GetParentProject(); } if (!proj) { // LAssert(!"All projects have a relative path?"); return Status; // No absolute path in the parent projects? } char p[MAX_PATH_LEN]; strcpy_s(p, sizeof(p), proj->GetFileName()); // Copy the base path if (sections.Length() > 0) LTrimDir(p); // Trim off the filename for (int i=0; iFileName.Empty(); d->UserFile.Empty(); d->App->GetTree()->Insert(this); ProjectNode *f = new ProjectNode(this); if (f) { f->SetName("Source"); f->SetType(NodeDir); InsertTag(f); } f = new ProjectNode(this); if (f) { f->SetName("Headers"); f->SetType(NodeDir); InsertTag(f); } d->Settings.Set(ProjEditorTabSize, 4); d->Settings.Set(ProjEditorIndentSize, 4); d->Settings.Set(ProjEditorUseHardTabs, true); d->Dirty = true; Expanded(true); } ProjectStatus IdeProject::OpenFile(const char *FileName) { auto Log = d->App->GetBuildLog(); LProfile Prof("IdeProject::OpenFile"); Prof.HideResultsIfBelow(1000); Empty(); Prof.Add("Init"); d->UserNodeFlags.Empty(); if (LIsRelativePath(FileName)) { char p[MAX_PATH_LEN]; getcwd(p, sizeof(p)); LMakePath(p, sizeof(p), p, FileName); d->FileName = p; } else { d->FileName = FileName; } d->UserFile = d->FileName + "." + LCurrentUserName(); if (!d->FileName) { Log->Print("%s:%i - No filename.\n", _FL); return OpenError; } Prof.Add("FileOpen"); LFile f; LString FullPath = d->FileName.Get(); if (!CheckExists(FullPath) || !f.Open(FullPath, O_READWRITE)) { Log->Print("%s:%i - Error: Can't open '%s'.\n", _FL, FullPath.Get()); return OpenError; } Prof.Add("Xml"); LXmlTree x; LXmlTag r; if (!x.Read(&r, &f)) { Log->Print("%s:%i - Error: Can't read XML: %s\n", _FL, x.GetErrorMsg()); return OpenError; } Prof.Add("Progress Setup"); #if DEBUG_OPEN_PROGRESS int64 Nodes = r.CountTags(); LProgressDlg Prog(d->App, 1000); Prog.SetDescription("Loading project..."); Prog.SetLimits(0, Nodes); Prog.SetYieldTime(1000); Prog.SetAlwaysOnTop(true); #endif Prof.Add("UserFile"); if (LFileExists(d->UserFile)) { LFile Uf; if (Uf.Open(d->UserFile, O_READ)) { LString::Array Ln = Uf.Read().SplitDelimit("\n"); for (unsigned i=0; iUserNodeFlags.Add((int)p[0].Int(), (int)p[1].Int(16)); } } } if (!r.IsTag("Project")) { Log->Print("%s:%i - No 'Project' tag.\n", _FL); return OpenError; } Prof.Add("OnOpen"); bool Ok = OnOpen( #if DEBUG_OPEN_PROGRESS &Prog, #else NULL, #endif &r); #if DEBUG_OPEN_PROGRESS if (Prog.IsCancelled()) return OpenCancel; else #endif if (!Ok) return OpenError; Prof.Add("Insert"); d->App->GetTree()->Insert(this); Expanded(true); Prof.Add("Serialize"); d->Settings.Serialize(&r, false /* read */); return OpenOk; } bool IdeProject::SaveFile() { auto Full = GetFullPath(); if (ValidStr(Full) && d->Dirty) { LFile f; if (f.Open(Full, O_WRITE)) { f.SetSize(0); LXmlTree x; d->Settings.Serialize(this, true /* write */); if (x.Write(this, &f)) d->Dirty = false; else LgiTrace("%s:%i - Failed to write XML.\n", _FL); } else LgiTrace("%s:%i - Couldn't open '%s' for writing.\n", _FL, Full.Get()); } if (d->UserFileDirty) { LFile f; LAssert(d->UserFile.Get()); if (f.Open(d->UserFile, O_WRITE)) { f.SetSize(0); // Save user file details.. // int Id; // for (int Flags = d->UserNodeFlags.First(&Id); Flags >= 0; Flags = d->UserNodeFlags.Next(&Id)) for (auto i : d->UserNodeFlags) { f.Print("%i,%x\n", i.key, i.value); } d->UserFileDirty = false; } } printf("\tIdeProject::SaveFile %i %i\n", d->Dirty, d->UserFileDirty); return !d->Dirty && !d->UserFileDirty; } void IdeProject::SetDirty() { d->Dirty = true; d->App->OnProjectChange(); } void IdeProject::SetUserFileDirty() { d->UserFileDirty = true; } bool IdeProject::GetExpanded(int Id) { int f = d->UserNodeFlags.Find(Id); return f >= 0 ? f : false; } void IdeProject::SetExpanded(int Id, bool Exp) { if (d->UserNodeFlags.Find(Id) != (int)Exp) { d->UserNodeFlags.Add(Id, Exp); SetUserFileDirty(); } } int IdeProject::AllocateId() { return d->NextNodeId++; } template bool CheckExists(LAutoString Base, T &p, Fn Setter, bool Debug) { LFile::Path Full; bool WasRel = LIsRelativePath(p); if (WasRel) { Full = Base.Get(); Full += p.Get(); } else Full = p.Get(); bool Ret = Full.Exists(); if (!Ret) { // Is the case wrong? for (int i=1; i%s\n", i, Leaf.Get(), dir.GetName()); Leaf = dir.GetName(); Matched = true; break; } } if (!Matched) break; } } if ((Ret = Full.Exists())) { LString Old = p.Get(); if (WasRel) { auto r = LMakeRelativePath(Base, Full); Setter(p, r); } else { Setter(p, Full.GetFull()); } if (Debug) printf("%s -> %s\n", Old.Get(), p.Get()); } } if (Debug) printf("CheckExists '%s' = %i\n", Full.GetFull().Get(), Ret); return Ret; } bool IdeProject::CheckExists(LString &p, bool Debug) { return ::CheckExists(GetBasePath(), p, [](LString &o, const char *i) { o = i; }, Debug); } bool IdeProject::CheckExists(LAutoString &p, bool Debug) { return ::CheckExists(GetBasePath(), p, [](LAutoString &o, const char *i) { o.Reset(NewStr(i)); }, Debug); } bool IdeProject::GetClean() { for (auto i: *this) { ProjectNode *p = dynamic_cast(i); if (p && !p->GetClean()) return false; } return !d->Dirty && !d->UserFileDirty; } void IdeProject::SetClean(std::function OnDone) { auto CleanNodes = [&]() { for (auto i: *this) { ProjectNode *p = dynamic_cast(i); if (!p) break; p->SetClean(); } if (OnDone) OnDone(true); }; if (d->Dirty || d->UserFileDirty) { if (ValidStr(d->FileName)) SaveFile(); else { LFileSelect s; s.Parent(Tree); s.Name("Project.xml"); s.Save([&](auto s, auto ok) { if (!ok) { if (OnDone) OnDone(false); return; } d->FileName = s->Name(); d->UserFile = d->FileName + "." + LCurrentUserName(); d->App->OnFile(d->FileName, true); Update(); CleanNodes(); }); return; } } CleanNodes(); } const char *IdeProject::GetText(int Col) { if (d->FileName) { char *s = strrchr(d->FileName, DIR_CHAR); return s ? s + 1 : d->FileName.Get(); } return Untitled; } int IdeProject::GetImage(int Flags) { return 0; } void IdeProject::Empty() { LXmlTag *t; while (Children.Length() > 0 && (t = Children[0])) { ProjectNode *n = dynamic_cast(t); if (n) { n->Remove(); } DeleteObj(t); } } LXmlTag *IdeProject::Create(char *Tag) { if (!stricmp(Tag, TagSettings)) return NULL; return new ProjectNode(this); } void IdeProject::OnMouseClick(LMouse &m) { if (m.IsContextMenu()) { LSubMenu Sub; Sub.AppendItem("New Folder", IDM_NEW_FOLDER); Sub.AppendItem("New Web Folder", IDM_WEB_FOLDER); Sub.AppendSeparator(); Sub.AppendItem("Build", IDM_BUILD); Sub.AppendItem("Clean Project", IDM_CLEAN_PROJECT); Sub.AppendItem("Clean All", IDM_CLEAN_ALL); Sub.AppendItem("Rebuild Project", IDM_REBUILD_PROJECT); Sub.AppendItem("Rebuild All", IDM_REBUILD_ALL); Sub.AppendSeparator(); Sub.AppendItem("Sort Children", IDM_SORT_CHILDREN); Sub.AppendSeparator(); Sub.AppendItem("Settings", IDM_SETTINGS, true); Sub.AppendItem("Insert Dependency", IDM_INSERT_DEP); m.ToScreen(); auto c = _ScrollPos(); m.x -= c.x; m.y -= c.y; switch (Sub.Float(Tree, m.x, m.y)) { case IDM_NEW_FOLDER: { auto Name = new LInput(Tree, "", "Name:", AppName); Name->DoModal([this, Name](auto d, auto code) { if (code) GetSubFolder(this, Name->GetStr(), true); delete d; }); break; } case IDM_WEB_FOLDER: { WebFldDlg *Dlg = new WebFldDlg(Tree, 0, 0, 0); Dlg->DoModal([&](auto d, auto code) { if (Dlg->Ftp && Dlg->Www) { IdeCommon *f = GetSubFolder(this, Dlg->Name, true); if (f) { f->SetAttr(OPT_Ftp, Dlg->Ftp); f->SetAttr(OPT_Www, Dlg->Www); } } delete Dlg; }); break; } case IDM_BUILD: { StopBuild(); Build(true, d->App->GetBuildMode()); break; } case IDM_CLEAN_PROJECT: { Clean(false, d->App->GetBuildMode()); break; } case IDM_CLEAN_ALL: { Clean(true, d->App->GetBuildMode()); break; } case IDM_REBUILD_PROJECT: { StopBuild(); Clean(false, d->App->GetBuildMode()); Build(false, d->App->GetBuildMode()); break; } case IDM_REBUILD_ALL: { StopBuild(); Clean(true, d->App->GetBuildMode()); Build(true, d->App->GetBuildMode()); break; } case IDM_SORT_CHILDREN: { SortChildren(); Project->SetDirty(); break; } case IDM_SETTINGS: { d->Settings.Edit(Tree, [&]() { SetDirty(); }); break; } case IDM_INSERT_DEP: { LFileSelect *s = new LFileSelect; s->Parent(Tree); s->Type("Project", "*.xml"); s->Open([&](auto s, bool ok) { if (!ok) return; ProjectNode *New = new ProjectNode(this); if (New) { New->SetFileName(s->Name()); New->SetType(NodeDependancy); InsertTag(New); SetDirty(); } delete s; }); break; } } } } char *IdeProject::FindFullPath(const char *File, ProjectNode **Node) { char *Full = 0; for (auto i:*this) { ProjectNode *c = dynamic_cast(i); if (!c) break; ProjectNode *n = c->FindFile(File, &Full); if (n) { if (Node) *Node = n; break; } } return Full; } bool IdeProject::HasNode(ProjectNode *Node) { for (auto i:*this) { ProjectNode *c = dynamic_cast(i); if (!c) break; if (c->HasNode(Node)) return true; } return false; } bool IdeProject::GetAllNodes(LArray &Nodes) { for (auto i:*this) { ProjectNode *c = dynamic_cast(i); if (!c) break; c->AddNodes(Nodes); } return true; } bool IdeProject::InProject(bool FuzzyMatch, const char *Path, bool Open, IdeDoc **Doc) { if (!Path) return false; // Search complete path first... ProjectNode *n = d->Nodes.Find(Path); if (!n && FuzzyMatch) { // No match, do partial matching. const char *Leaf = LGetLeaf(Path); auto PathLen = strlen(Path); auto LeafLen = strlen(Leaf); uint32_t MatchingScore = 0; // Traverse all nodes and try and find the best fit. // const char *p; // for (ProjectNode *Cur = d->Nodes.First(&p); Cur; Cur = d->Nodes.Next(&p)) for (auto Cur : d->Nodes) { int CurPlatform = Cur.value->GetPlatforms(); uint32_t Score = 0; if (stristr(Cur.key, Path)) { Score += PathLen; } else if (stristr(Cur.key, Leaf)) { Score += LeafLen; } const char *pLeaf = LGetLeaf(Cur.key); if (pLeaf && !stricmp(pLeaf, Leaf)) { Score |= 0x40000000; } if (Score && (CurPlatform & PLATFORM_CURRENT) != 0) { Score |= 0x80000000; } if (Score > MatchingScore) { MatchingScore = Score; n = Cur.value; } } } if (n && Doc) { *Doc = n->Open(); } return n != 0; } char *GetQuotedStr(char *Str) { char *s = strchr(Str, '\"'); if (s) { s++; char *e = strchr(s, '\"'); if (e) { return NewStr(s, e - s); } } return 0; } void IdeProject::ImportDsp(const char *File) { if (File && LFileExists(File)) { char Base[256]; strcpy(Base, File); LTrimDir(Base); char *Dsp = LReadTextFile(File); if (Dsp) { LToken Lines(Dsp, "\r\n"); IdeCommon *Current = this; bool IsSource = false; for (int i=0; iGetSubFolder(this, Folder, true); if (Sub) { Current = Sub; } DeleteArray(Folder); } } else if (strnicmp(L, "# End Group", 11) == 0) { IdeCommon *Parent = dynamic_cast(Current->GetParent()); if (Parent) { Current = Parent; } } else if (strnicmp(L, "# Begin Source", 14) == 0) { IsSource = true; } else if (strnicmp(L, "# End Source", 12) == 0) { IsSource = false; } else if (Current && IsSource && strncmp(L, "SOURCE=", 7) == 0) { ProjectNode *New = new ProjectNode(this); if (New) { char *Src = 0; if (strchr(L, '\"')) { Src = GetQuotedStr(L); } else { Src = NewStr(L + 7); } if (Src) { // Make absolute path char Abs[256]; LMakePath(Abs, sizeof(Abs), Base, ToUnixPath(Src)); // Make relitive path New->SetFileName(Src); DeleteArray(Src); } Current->InsertTag(New); SetDirty(); } } } DeleteArray(Dsp); } } } IdeProjectSettings *IdeProject::GetSettings() { return &d->Settings; } bool IdeProject::BuildIncludePaths(LArray &Paths, bool Recurse, bool IncludeSystem, IdePlatform Platform) { List Projects; if (Recurse) GetChildProjects(Projects); Projects.Insert(this, 0); LHashTbl, bool> Map; for (auto p: Projects) { LString ProjInclude = d->Settings.GetStr(ProjIncludePaths, NULL, Platform); LAutoString Base = p->GetBasePath(); const char *Delim = ",;\r\n"; LString::Array In, Out; LString::Array a = ProjInclude.SplitDelimit(Delim); In = a; if (IncludeSystem) { LString SysInclude = d->Settings.GetStr(ProjSystemIncludes, NULL, Platform); a = SysInclude.SplitDelimit(Delim); In.SetFixedLength(false); In.Add(a); } for (unsigned i=0; i 1 ? a[1].Get() : NULL); LStringPipe Buf; if (Proc.Start()) { Proc.Communicate(&Buf); - LString result = Buf.NewGStr(); + LString result = Buf.NewLStr(); a = result.Split(" \t\r\n"); for (int i=0; i Nodes; if (p->GetAllNodes(Nodes)) { auto Base = p->GetFullPath(); if (Base) { LTrimDir(Base); for (auto &n: Nodes) { if (n->GetType() == NodeHeader && // Only look at headers. (n->GetPlatforms() & (1 << Platform)) != 0) // Exclude files not on this platform. { auto f = n->GetFileName(); char p[MAX_PATH_LEN]; if (f && LMakePath(p, sizeof(p), Base, f)) { char *l = strrchr(p, DIR_CHAR); if (l) *l = 0; if (!Map.Find(p)) { Map.Add(p, true); } } } } } } } // char *p; // for (bool b = Map.First(&p); b; b = Map.Next(&p)) for (auto p : Map) Paths.Add(p.key); return true; } void IdeProjectPrivate::CollectAllFiles(LTreeNode *Base, LArray &Files, bool SubProjects, int Platform) { for (auto i:*Base) { IdeProject *Proj = dynamic_cast(i); if (Proj) { if (Proj->GetParentProject() && !SubProjects) { continue; } } else { ProjectNode *p = dynamic_cast(i); if (p) { if (p->GetType() == NodeSrc || p->GetType() == NodeHeader) { if (p->GetPlatforms() & Platform) { Files.Add(p); } } } } CollectAllFiles(i, Files, SubProjects, Platform); } } LString IdeProject::GetTargetName(IdePlatform Platform) { LString Status; const char *t = d->Settings.GetStr(ProjTargetName, NULL, Platform); if (ValidStr(t)) { // Take target name from the settings Status = t; } else { char *s = strrchr(d->FileName, DIR_CHAR); if (s) { // Generate the target executable name char Target[MAX_PATH_LEN]; strcpy_s(Target, sizeof(Target), s + 1); s = strrchr(Target, '.'); if (s) *s = 0; strlwr(Target); s = Target; for (char *i = Target; *i; i++) { if (*i != '.' && *i != ' ') { *s++ = *i; } } *s = 0; Status = Target; } } return Status; } LString IdeProject::GetTargetFile(IdePlatform Platform) { LString Target = GetTargetName(PlatformCurrent); if (!Target) { LAssert(!"No target?"); return LString(); } const char *TargetType = d->Settings.GetStr(ProjTargetType); if (!TargetType) { LAssert(!"Needs a target type."); return LString(); } if (!stricmp(TargetType, "Executable")) { return Target; } else if (!stricmp(TargetType, "DynamicLibrary")) { char t[MAX_PATH_LEN]; auto DefExt = PlatformDynamicLibraryExt(Platform); strcpy_s(t, sizeof(t), Target); char *ext = LGetExtension(t); if (!ext) sprintf(t + strlen(t), ".%s", DefExt); else if (stricmp(ext, DefExt)) strcpy(ext, DefExt); return t; } else if (!stricmp(TargetType, "StaticLibrary")) { LString Ret; Ret.Printf("%s.%s", Target.Get(), PlatformSharedLibraryExt(Platform)); return Ret; } return LString(); } struct ProjDependency { bool Scanned; LAutoString File; ProjDependency(const char *f) { Scanned = false; File.Reset(NewStr(f)); } }; bool IdeProject::GetAllDependencies(LArray &Files, IdePlatform Platform) { if (!GetTree()->Lock(_FL)) return false; LHashTbl, ProjDependency*> Deps; LAutoString Base = GetBasePath(); // Build list of all the source files... LArray Src; CollectAllSource(Src, Platform); // Get all include paths LArray IncPaths; BuildIncludePaths(IncPaths, false, false, Platform); // Add all source to dependencies for (int i=0; i Unscanned; do { // Find all the unscanned dependencies Unscanned.Length(0); // for (ProjDependency *d = Deps.First(); d; d = Deps.Next()) for (auto d : Deps) { if (!d.value->Scanned) Unscanned.Add(d.value); } for (int i=0; iScanned = true; char *Src = d->File; char Full[MAX_PATH_LEN]; if (LIsRelativePath(d->File)) { LMakePath(Full, sizeof(Full), Base, d->File); Src = Full; } LArray SrcDeps; if (GetDependencies(Src, IncPaths, SrcDeps, Platform)) { for (int n=0; n 0); // for (ProjDependency *d = Deps.First(); d; d = Deps.Next()) for (auto d : Deps) { Files.Add(d.value->File.Release()); } Deps.DeleteObjects(); GetTree()->Unlock(); return true; } bool IdeProject::GetDependencies(const char *InSourceFile, LArray &IncPaths, LArray &Files, IdePlatform Platform) { LString SourceFile = InSourceFile; if (!CheckExists(SourceFile)) { LgiTrace("%s:%i - can't read '%s'\n", _FL, SourceFile.Get()); return false; } LAutoString c8(LReadTextFile(SourceFile)); if (!c8) return false; LArray Headers; if (!BuildHeaderList(c8, Headers, IncPaths, false)) return false; for (int n=0; nCreateMakefile) { if (d->CreateMakefile->IsExited()) d->CreateMakefile.Reset(); else { d->App->GetBuildLog()->Print("%s:%i - Makefile thread still running.\n", _FL); return false; } } if (Platform == PlatformCurrent) Platform = GetCurrentPlatform(); return d->CreateMakefile.Reset(new MakefileThread(d, Platform, BuildAfterwards)); } void IdeProject::OnMakefileCreated() { if (d->CreateMakefile) { d->CreateMakefile.Reset(); if (MakefileThread::Instances == 0) GetApp()->PostEvent(M_LAST_MAKEFILE_CREATED); } } //////////////////////////////////////////////////////////////////////////////////////////// IdeTree::IdeTree() : LTree(IDC_PROJECT_TREE, 0, 0, 100, 100) { Hit = 0; MultiSelect(true); } void IdeTree::OnCreate() { SetWindow(this); } void IdeTree::OnDragExit() { SelectDropTarget(0); } int IdeTree::WillAccept(LDragFormats &Formats, LPoint p, int KeyState) { static bool First = true; Formats.SupportsFileDrops(); Formats.Supports(NODE_DROP_FORMAT); First = false; if (Formats.Length() == 0) { LgiTrace("%s:%i - No valid drop formats.\n", _FL); return DROPEFFECT_NONE; } Hit = ItemAtPoint(p.x, p.y); if (!Hit) { SelectDropTarget(NULL); return DROPEFFECT_NONE; } if (Formats.HasFormat(LGI_FileDropFormat)) { SelectDropTarget(Hit); return DROPEFFECT_LINK; } IdeCommon *Src = dynamic_cast(Selection()); IdeCommon *Dst = dynamic_cast(Hit); if (Src && Dst) { // Check this folder is not a child of the src for (IdeCommon *n=Dst; n; n=dynamic_cast(n->GetParent())) { if (n == Src) return DROPEFFECT_NONE; } } // Valid target SelectDropTarget(Hit); return DROPEFFECT_MOVE; } int IdeTree::OnDrop(LArray &Data, LPoint p, int KeyState) { int Ret = DROPEFFECT_NONE; SelectDropTarget(NULL); if (!(Hit = ItemAtPoint(p.x, p.y))) return Ret; for (unsigned n=0; nType == GV_BINARY && Data->Value.Binary.Length == sizeof(ProjectNode*)) { ProjectNode *Src = ((ProjectNode**)Data->Value.Binary.Data)[0]; if (Src) { ProjectNode *Folder = dynamic_cast(Hit); while (Folder && Folder->GetType() != NodeDir) Folder = dynamic_cast(Folder->GetParent()); IdeCommon *Dst = dynamic_cast(Folder?Folder:Hit); if (Dst) { // Check this folder is not a child of the src for (IdeCommon *n=Dst; n; n=dynamic_cast(n->GetParent())) { if (n == Src) return DROPEFFECT_NONE; } // Detach LTreeItem *i = dynamic_cast(Src); i->Detach(); if (Src->LXmlTag::Parent) { LAssert(Src->LXmlTag::Parent->Children.HasItem(Src)); Src->LXmlTag::Parent->Children.Delete(Src); } // Attach Src->LXmlTag::Parent = Dst; Dst->Children.SetFixedLength(false); Dst->Children.Add(Src); Dst->Children.SetFixedLength(true); Dst->Insert(Src); // Dirty Src->GetProject()->SetDirty(); } Ret = DROPEFFECT_MOVE; } } } else if (dd.IsFileDrop()) { ProjectNode *Folder = dynamic_cast(Hit); while (Folder && Folder->GetType() > NodeDir) Folder = dynamic_cast(Folder->GetParent()); IdeCommon *Dst = dynamic_cast(Folder?Folder:Hit); if (Dst) { AddFilesProgress Prog(this); LDropFiles Df(dd); for (int i=0; iAddFiles(&Prog, Df[i])) Ret = DROPEFFECT_LINK; } } } else LgiTrace("%s:%i - Unknown drop format: %s.\n", _FL, dd.Format.Get()); } return Ret; } ///////////////////////////////////////////////////////////////////////////////////////////////// AddFilesProgress::AddFilesProgress(LViewI *par) { v = 0; Cancel = false; Msg = NULL; SetParent(par); Ts = LCurrentTime(); LRect r(0, 0, 140, 100); SetPos(r); MoveSameScreen(par); Name("Importing files..."); LString::Array a = LString(DefaultExt).SplitDelimit(","); for (unsigned i=0; iGetCell(0, 0); c->Add(new LTextLabel(-1, 0, 0, -1, -1, "Loaded:")); c = t->GetCell(1, 0); c->Add(Msg = new LTextLabel(-1, 0, 0, -1, -1, "...")); c = t->GetCell(0, 1, true, 2); c->TextAlign(LCss::Len(LCss::AlignRight)); c->Add(new LButton(IDCANCEL, 0, 0, -1, -1, "Cancel")); } int64 AddFilesProgress::Value() { return v; } void AddFilesProgress::Value(int64 val) { v = val; uint64 Now = LCurrentTime(); if (Visible()) { if (Now - Ts > 200) { if (Msg) { Msg->Value(v); Msg->SendNotify(LNotifyTableLayoutRefresh); } Ts = Now; } } else if (Now - Ts > 1000) { DoModeless(); SetAlwaysOnTop(true); } } int AddFilesProgress::OnNotify(LViewI *c, LNotification n) { if (c->GetId() == IDCANCEL) Cancel = true; return 0; } diff --git a/Ide/Code/LgiIde.cpp b/Ide/Code/LgiIde.cpp --- a/Ide/Code/LgiIde.cpp +++ b/Ide/Code/LgiIde.cpp @@ -1,4742 +1,4742 @@ #include #include #include #include "lgi/common/Lgi.h" #include "lgi/common/Mdi.h" #include "lgi/common/Token.h" #include "lgi/common/XmlTree.h" #include "lgi/common/Panel.h" #include "lgi/common/Button.h" #include "lgi/common/TabView.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/Box.h" #include "lgi/common/TextLog.h" #include "lgi/common/Edit.h" #include "lgi/common/TableLayout.h" #include "lgi/common/TextLabel.h" #include "lgi/common/Combo.h" #include "lgi/common/CheckBox.h" #include "lgi/common/LgiRes.h" #include "lgi/common/Box.h" #include "lgi/common/SubProcess.h" #include "lgi/common/About.h" #include "lgi/common/Menu.h" #include "lgi/common/ToolBar.h" #include "lgi/common/FileSelect.h" #include "lgi/common/SubProcess.h" #include "LgiIde.h" #include "FtpThread.h" #include "FindSymbol.h" #include "Debugger.h" #include "ProjectNode.h" #define IDM_RECENT_FILE 1000 #define IDM_RECENT_PROJECT 1100 #define IDM_WINDOWS 1200 #define IDM_MAKEFILE_BASE 1300 #define USE_HAIKU_PULSE_HACK 0 #define OPT_ENTIRE_SOLUTION "SearchSolution" #define OPT_SPLIT_PX "SplitPos" #define OPT_OUTPUT_PX "OutputPx" #define OPT_FIX_RENAMED "FixRenamed" #define OPT_RENAMED_SYM "RenamedSym" #define OPT_PLATFORM "Platform" #define IsSymbolChar(c) ( IsDigit(c) || IsAlpha(c) || strchr("-_", c) ) ////////////////////////////////////////////////////////////////////////////////////////// class FindInProject : public LDialog { AppWnd *App; LList *Lst; public: FindInProject(AppWnd *app) { Lst = NULL; App = app; if (LoadFromResource(IDC_FIND_PROJECT_FILE)) { MoveSameScreen(App); LViewI *v; if (GetViewById(IDC_TEXT, v)) v->Focus(true); if (!GetViewById(IDC_FILES, Lst)) return; RegisterHook(this, LKeyEvents, 0); } } bool OnViewKey(LView *v, LKey &k) { switch (k.vkey) { case LK_UP: case LK_DOWN: case LK_PAGEDOWN: case LK_PAGEUP: { return Lst->OnKey(k); break; } case LK_RETURN: { if (k.Down()) { LListItem *i = Lst->GetSelected(); if (i) { const char *Ref = i->GetText(0); App->GotoReference(Ref, 1, false); } EndModal(1); return true; } break; } case LK_ESCAPE: { if (k.Down()) { EndModal(0); return true; } break; } } return false; } void Search(const char *s) { IdeProject *p = App->RootProject(); if (!p || !s) return; LArray Matches, Nodes; List All; p->GetChildProjects(All); All.Insert(p); for (auto p: All) { p->GetAllNodes(Nodes); } FilterFiles(Matches, Nodes, s, App->GetPlatform()); Lst->Empty(); for (auto m: Matches) { LListItem *li = new LListItem; LString Fn = m->GetFileName(); #ifdef WINDOWS Fn = Fn.Replace("/","\\"); #else Fn = Fn.Replace("\\","/"); #endif m->GetProject()->CheckExists(Fn); li->SetText(Fn); Lst->Insert(li); } Lst->ResizeColumnsToContent(); } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_FILES: if (n.Type == LNotifyItemDoubleClick) { LListItem *i = Lst->GetSelected(); if (i) { App->GotoReference(i->GetText(0), 1, false); EndModal(1); } } break; case IDC_TEXT: if (n.Type != LNotifyReturnKey) Search(c->Name()); break; case IDCANCEL: EndModal(0); break; } return 0; } }; ////////////////////////////////////////////////////////////////////////////////////////// const char *AppName = "LgiIde"; char *dirchar(char *s, bool rev = false) { if (rev) { char *last = 0; while (s && *s) { if (*s == '/' || *s == '\\') last = s; s++; } return last; } else { while (s && *s) { if (*s == '/' || *s == '\\') return s; s++; } } return 0; } ////////////////////////////////////////////////////////////////////////////////////////// class AppDependency : public LTreeItem { char *File; bool Loaded; LTreeItem *Fake; public: AppDependency(const char *file) { File = NewStr(file); char *d = strrchr(File, DIR_CHAR); Loaded = false; Insert(Fake = new LTreeItem); if (LFileExists(File)) { SetText(d?d+1:File); } else { char s[256]; sprintf(s, "%s (missing)", d?d+1:File); SetText(s); } } ~AppDependency() { DeleteArray(File); } char *GetFile() { return File; } void Copy(LStringPipe &p, int Depth = 0) { { char s[1024]; ZeroObj(s); memset(s, ' ', Depth * 4); sprintf(s+(Depth*4), "[%c] %s\n", Expanded() ? '-' : '+', GetText(0)); p.Push(s); } if (Loaded) { for (LTreeItem *i=GetChild(); i; i=i->GetNext()) { ((AppDependency*)i)->Copy(p, Depth+1); } } } char *Find(const char *Paths, char *e) { LToken Path(Paths, LGI_PATH_SEPARATOR); for (int p=0; pSunken(true); Root = new AppDependency(File); if (Root) { t->Insert(Root); Root->Expanded(true); } Children.Insert(t); Children.Insert(new LButton(IDC_COPY, 10, t->LView::GetPos().y2 + 10, 60, 20, "Copy")); Children.Insert(new LButton(IDOK, 80, t->LView::GetPos().y2 + 10, 60, 20, "Ok")); } DoModal(NULL); } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_COPY: { if (Root) { LStringPipe p; Root->Copy(p); char *s = p.NewStr(); if (s) { LClipBoard c(this); c.Text(s); DeleteArray(s); } break; } break; } case IDOK: { EndModal(0); break; } } return 0; } }; ////////////////////////////////////////////////////////////////////////////////////////// class DebugTextLog : public LTextLog { public: DebugTextLog(int id) : LTextLog(id) { } void PourText(size_t Start, ssize_t Len) override { auto Ts = LCurrentTime(); LTextView3::PourText(Start, Len); auto Dur = LCurrentTime() - Ts; if (Dur > 1500) { // Yo homes, too much text bro... Name(NULL); } else { for (auto l: Line) { char16 *t = Text + l->Start; if (l->Len > 5 && !StrnicmpW(t, L"(gdb)", 5)) { l->c.Rgb(0, 160, 0); } else if (l->Len > 1 && t[0] == '[') { l->c.Rgb(192, 192, 192); } } } } }; WatchItem::WatchItem(IdeOutput *out, const char *Init) { Out = out; Expanded(false); if (Init) SetText(Init); Insert(PlaceHolder = new LTreeItem); } WatchItem::~WatchItem() { } bool WatchItem::SetValue(LVariant &v) { char *Str = v.CastString(); if (ValidStr(Str)) SetText(Str, 2); else LTreeItem::SetText(NULL, 2); return true; } bool WatchItem::SetText(const char *s, int i) { if (ValidStr(s)) { LTreeItem::SetText(s, i); if (i == 0 && Tree && Tree->GetWindow()) { LViewI *Tabs = Tree->GetWindow()->FindControl(IDC_DEBUG_TAB); if (Tabs) Tabs->SendNotify(LNotifyValueChanged); } return true; } if (i == 0) delete this; return false; } void WatchItem::OnExpand(bool b) { if (b && PlaceHolder) { // Do something } } class BuildLog : public LTextLog { public: BuildLog(int id) : LTextLog(id) { } void PourStyle(size_t Start, ssize_t Length) { List::I it = LTextView3::Line.begin(); for (LTextLine *ln = *it; ln; ln = *++it) { if (!ln->c.IsValid()) { char16 *t = Text + ln->Start; char16 *Err = Strnistr(t, L"error", ln->Len); char16 *Undef = Strnistr(t, L"undefined reference", ln->Len); char16 *Warn = Strnistr(t, L"warning", ln->Len); if ( (Err && strchr(":[", Err[5])) || (Undef != NULL) ) ln->c.Rgb(222, 0, 0); else if (Warn && strchr(":[", Warn[7])) ln->c.Rgb(255, 128, 0); else ln->c = LColour(L_TEXT); } } } }; class IdeOutput : public LTabView { public: AppWnd *App; LTabPage *Build; LTabPage *Output; LTabPage *Debug; LTabPage *Find; LTabPage *Ftp; LList *FtpLog; LTextLog *Txt[AppWnd::Channels::ChannelMax]; LArray Buf[AppWnd::Channels::ChannelMax]; LFont Small; LFont Fixed; LTabView *DebugTab; LBox *DebugBox; LBox *DebugLog; LList *Locals, *CallStack, *Threads; LTree *Watch; LTextLog *ObjectDump, *MemoryDump, *Registers; LTableLayout *MemTable; LEdit *DebugEdit; LTextLog *DebuggerLog; IdeOutput(AppWnd *app) { ZeroObj(Txt); App = app; Build = Output = Debug = Find = Ftp = 0; FtpLog = 0; DebugBox = NULL; Locals = NULL; Watch = NULL; DebugLog = NULL; DebugEdit = NULL; DebuggerLog = NULL; CallStack = NULL; ObjectDump = NULL; MemoryDump = NULL; MemTable = NULL; Threads = NULL; Registers = NULL; Small = *LSysFont; Small.PointSize(Small.PointSize()-1); Small.Create(); LAssert(Small.Handle()); LFontType Type; if (Type.GetSystemFont("Fixed")) { Type.SetPointSize(LSysFont->PointSize()-1); Fixed.Create(&Type); } else { Fixed.PointSize(LSysFont->PointSize()-1); Fixed.Face("Courier"); Fixed.Create(); } GetCss(true)->MinHeight("60px"); Build = Append("Build"); Output = Append("Output"); Find = Append("Find"); Ftp = Append("Ftp"); Debug = Append("Debug"); SetFont(&Small); Build->SetFont(&Small); Output->SetFont(&Small); Find->SetFont(&Small); Ftp->SetFont(&Small); Debug->SetFont(&Small); if (Build) Build->Append(Txt[AppWnd::BuildTab] = new BuildLog(IDC_BUILD_LOG)); if (Output) Output->Append(Txt[AppWnd::OutputTab] = new LTextLog(IDC_OUTPUT_LOG)); if (Find) Find->Append(Txt[AppWnd::FindTab] = new LTextLog(IDC_FIND_LOG)); if (Ftp) Ftp->Append(FtpLog = new LList(104, 0, 0, 100, 100)); if (Debug) { Debug->Append(DebugBox = new LBox); if (DebugBox) { DebugBox->SetVertical(false); if ((DebugTab = new LTabView(IDC_DEBUG_TAB))) { DebugTab->GetCss(true)->Padding("0px"); DebugTab->SetFont(&Small); DebugBox->AddView(DebugTab); LTabPage *Page; if ((Page = DebugTab->Append("Locals"))) { Page->SetFont(&Small); if ((Locals = new LList(IDC_LOCALS_LIST, 0, 0, 100, 100, "Locals List"))) { Locals->SetFont(&Small); Locals->AddColumn("", 30); Locals->AddColumn("Type", 50); Locals->AddColumn("Name", 50); Locals->AddColumn("Value", 1000); Locals->SetPourLargest(true); Page->Append(Locals); } } if ((Page = DebugTab->Append("Object"))) { Page->SetFont(&Small); if ((ObjectDump = new LTextLog(IDC_OBJECT_DUMP))) { ObjectDump->SetFont(&Fixed); ObjectDump->SetPourLargest(true); Page->Append(ObjectDump); } } if ((Page = DebugTab->Append("Watch"))) { Page->SetFont(&Small); if ((Watch = new LTree(IDC_WATCH_LIST, 0, 0, 100, 100, "Watch List"))) { Watch->SetFont(&Small); Watch->ShowColumnHeader(true); Watch->AddColumn("Watch", 80); Watch->AddColumn("Type", 100); Watch->AddColumn("Value", 600); Watch->SetPourLargest(true); Page->Append(Watch); LXmlTag *w = App->GetOptions()->LockTag("watches", _FL); if (!w) { App->GetOptions()->CreateTag("watches"); w = App->GetOptions()->LockTag("watches", _FL); } if (w) { for (auto c: w->Children) { if (c->IsTag("watch")) { Watch->Insert(new WatchItem(this, c->GetContent())); } } App->GetOptions()->Unlock(); } } } if ((Page = DebugTab->Append("Memory"))) { Page->SetFont(&Small); if ((MemTable = new LTableLayout(IDC_MEMORY_TABLE))) { LCombo *cbo; LCheckBox *chk; LTextLabel *txt; LEdit *ed; MemTable->SetFont(&Small); int x = 0, y = 0; auto *c = MemTable->GetCell(x++, y); if (c) { c->VerticalAlign(LCss::VerticalMiddle); c->Add(txt = new LTextLabel(IDC_STATIC, 0, 0, -1, -1, "Address:")); txt->SetFont(&Small); } c = MemTable->GetCell(x++, y); if (c) { c->PaddingRight(LCss::Len("1em")); c->Add(ed = new LEdit(IDC_MEM_ADDR, 0, 0, 60, 20)); ed->SetFont(&Small); } c = MemTable->GetCell(x++, y); if (c) { c->PaddingRight(LCss::Len("1em")); c->Add(cbo = new LCombo(IDC_MEM_SIZE, 0, 0, 60, 20)); cbo->SetFont(&Small); cbo->Insert("1 byte"); cbo->Insert("2 bytes"); cbo->Insert("4 bytes"); cbo->Insert("8 bytes"); } c = MemTable->GetCell(x++, y); if (c) { c->VerticalAlign(LCss::VerticalMiddle); c->Add(txt = new LTextLabel(IDC_STATIC, 0, 0, -1, -1, "Page width:")); txt->SetFont(&Small); } c = MemTable->GetCell(x++, y); if (c) { c->PaddingRight(LCss::Len("1em")); c->Add(ed = new LEdit(IDC_MEM_ROW_LEN, 0, 0, 60, 20)); ed->SetFont(&Small); } c = MemTable->GetCell(x++, y); if (c) { c->VerticalAlign(LCss::VerticalMiddle); c->Add(chk = new LCheckBox(IDC_MEM_HEX, 0, 0, -1, -1, "Show Hex")); chk->SetFont(&Small); chk->Value(true); } int cols = x; x = 0; c = MemTable->GetCell(x++, ++y, true, cols); if ((MemoryDump = new LTextLog(IDC_MEMORY_DUMP))) { MemoryDump->SetFont(&Fixed); MemoryDump->SetPourLargest(true); c->Add(MemoryDump); } Page->Append(MemTable); } } if ((Page = DebugTab->Append("Threads"))) { Page->SetFont(&Small); if ((Threads = new LList(IDC_THREADS, 0, 0, 100, 100, "Threads"))) { Threads->SetFont(&Small); Threads->AddColumn("", 20); Threads->AddColumn("Thread", 1000); Threads->SetPourLargest(true); Threads->MultiSelect(false); Page->Append(Threads); } } if ((Page = DebugTab->Append("Call Stack"))) { Page->SetFont(&Small); if ((CallStack = new LList(IDC_CALL_STACK, 0, 0, 100, 100, "Call Stack"))) { CallStack->SetFont(&Small); CallStack->AddColumn("", 20); CallStack->AddColumn("Call Stack", 1000); CallStack->SetPourLargest(true); CallStack->MultiSelect(false); Page->Append(CallStack); } } if ((Page = DebugTab->Append("Registers"))) { Page->SetFont(&Small); if ((Registers = new LTextLog(IDC_REGISTERS))) { Registers->SetFont(&Small); Registers->SetPourLargest(true); Page->Append(Registers); } } } if ((DebugLog = new LBox)) { DebugLog->SetVertical(true); DebugBox->AddView(DebugLog); DebugLog->AddView(DebuggerLog = new DebugTextLog(IDC_DEBUGGER_LOG)); DebuggerLog->SetFont(&Small); DebugLog->AddView(DebugEdit = new LEdit(IDC_DEBUG_EDIT, 0, 0, 60, 20)); DebugEdit->GetCss(true)->Height(LCss::Len(LCss::LenPx, (float)(LSysFont->GetHeight() + 8))); } } } if (FtpLog) { FtpLog->SetPourLargest(true); FtpLog->Sunken(true); FtpLog->AddColumn("Entry", 1000); FtpLog->ShowColumnHeader(false); } for (int n=0; nSetTabSize(8); Txt[n]->Sunken(true); } } ~IdeOutput() { } const char *GetClass() { return "IdeOutput"; } void Save() { if (Watch) { LXmlTag *w = App->GetOptions()->LockTag("watches", _FL); if (!w) { App->GetOptions()->CreateTag("watches"); w = App->GetOptions()->LockTag("watches", _FL); } if (w) { w->EmptyChildren(); for (LTreeItem *ti = Watch->GetChild(); ti; ti = ti->GetNext()) { LXmlTag *t = new LXmlTag("watch"); if (t) { t->SetContent(ti->GetText(0)); w->InsertTag(t); } } App->GetOptions()->Unlock(); } } } void OnCreate() { SetPulse(1000); AttachChildren(); } void RemoveAnsi(LArray &a) { char *s = a.AddressOf(); char *e = s + a.Length(); while (s < e) { if (*s == 0x7) { a.DeleteAt(s - a.AddressOf(), true); s--; } else if ( *s == 0x1b && s[1] >= 0x40 && s[1] <= 0x5f ) { // ANSI seq char *end; if (s[1] == '[' && s[2] == '0' && s[3] == ';') end = s + 4; else { end = s + 2; while (end < e && !IsAlpha(*end)) { end++; } if (*end) end++; } auto len = end - s; memmove(s, end, e - end); a.Length(a.Length() - len); s--; } s++; } } void OnPulse() { int Changed = -1; for (int Channel = 0; Channel w; if (!LIsUtf8(Utf, (ssize_t)Size)) { LgiTrace("Ch %i not utf len=" LPrintfInt64 "\n", Channel, Size); // Clear out the invalid UTF? uint8_t *u = (uint8_t*) Utf, *e = u + Size; ssize_t len = Size; LArray out; while (u < e) { int32 u32 = LgiUtf8To32(u, len); if (u32) { out.Add(u32); } else { out.Add(0xFFFD); u++; } } out.Add(0); w.Reset(out.Release()); } else { RemoveAnsi(Buf[Channel]); w.Reset(Utf8ToWide(Utf, (ssize_t)Size)); } // auto OldText = Txt[Channel]->NameW(); ssize_t OldLen = Txt[Channel]->Length(); auto Cur = Txt[Channel]->GetCaret(); Txt[Channel]->Insert(OldLen, w, StrlenW(w)); if (Cur > OldLen - 1) Txt[Channel]->SetCaret(OldLen + StrlenW(w), false); else printf("Caret move: %i, %i = %i\n", (int)Cur, (int)OldLen, Cur > OldLen - 1); Changed = Channel; Buf[Channel].Length(0); Txt[Channel]->Invalidate(); } } /* if (Changed >= 0) Value(Changed); */ } }; int DocSorter(IdeDoc *a, IdeDoc *b, NativeInt d) { auto A = a->GetFileName(); auto B = b->GetFileName(); if (A && B) { auto Af = strrchr(A, DIR_CHAR); auto Bf = strrchr(B, DIR_CHAR); return stricmp(Af?Af+1:A, Bf?Bf+1:B); } return 0; } struct FileLoc { LAutoString File; int Line; void Set(const char *f, int l) { File.Reset(NewStr(f)); Line = l; } }; class AppWndPrivate { public: AppWnd *App; int Platform = 0; LMdiParent *Mdi; LOptionsFile Options; LBox *HBox, *VBox; List Docs; List Projects; LImageList *Icons; LTree *Tree; IdeOutput *Output; bool Debugging; bool Running; bool Building; bool FixBuildWait = false; int RebuildWait = 0; LSubMenu *WindowsMenu; LSubMenu *CreateMakefileMenu; LAutoPtr FindSym; LArray SystemIncludePaths; LArray BreakPoints; // Debugging LDebugContext *DbgContext; // Cursor history tracking int HistoryLoc; LArray CursorHistory; bool InHistorySeek; void SeekHistory(int Direction) { if (CursorHistory.Length()) { int Loc = HistoryLoc + Direction; if (Loc >= 0 && Loc < CursorHistory.Length()) { HistoryLoc = Loc; FileLoc &Loc = CursorHistory[HistoryLoc]; App->GotoReference(Loc.File, Loc.Line, false, false); App->DumpHistory(); } } } // Find in files LAutoPtr FindParameters; LAutoPtr Finder; int AppHnd; // Mru LString::Array RecentFiles; LSubMenu *RecentFilesMenu = NULL; LString::Array RecentProjects; LSubMenu *RecentProjectsMenu = NULL; // Object AppWndPrivate(AppWnd *a) : Options(LOptionsFile::DesktopMode, AppName), AppHnd(LEventSinkMap::Dispatch.AddSink(a)) { FindSym.Reset(new FindSymbolSystem(AppHnd)); HistoryLoc = 0; InHistorySeek = false; WindowsMenu = 0; App = a; HBox = VBox = NULL; Tree = 0; Mdi = NULL; DbgContext = NULL; Output = 0; Debugging = false; Running = false; Building = false; Icons = LLoadImageList("icons.png", 16, 16); Options.SerializeFile(false); App->SerializeState(&Options, "WndPos", true); SerializeStringList("RecentFiles", &RecentFiles, false); SerializeStringList("RecentProjects", &RecentProjects, false); } ~AppWndPrivate() { FindSym.Reset(); Finder.Reset(); if (Output) Output->Save(); App->SerializeState(&Options, "WndPos", false); SerializeStringList("RecentFiles", &RecentFiles, true); SerializeStringList("RecentProjects", &RecentProjects, true); Options.SerializeFile(true); while (Docs.Length()) { auto len = Docs.Length(); delete Docs[0]; LAssert(Docs.Length() != len); // doc must delete itself... } auto root = App->RootProject(); if (root) { // printf("Deleting proj %s\n", root->GetFileName()); delete root; } LAssert(!Projects.Length()); DeleteObj(Icons); } bool FindSource(LAutoString &Full, char *File, char *Context) { if (!LIsRelativePath(File)) { Full.Reset(NewStr(File)); } char *ContextPath = 0; if (Context && !Full) { char *Dir = strrchr(Context, DIR_CHAR); for (auto p: Projects) { ContextPath = p->FindFullPath(Dir?Dir+1:Context); if (ContextPath) break; } if (ContextPath) { LTrimDir(ContextPath); char p[300]; LMakePath(p, sizeof(p), ContextPath, File); if (LFileExists(p)) { Full.Reset(NewStr(p)); } } else { LgiTrace("%s:%i - Context '%s' not found in project.\n", _FL, Context); } } if (!Full) { List::I Projs = Projects.begin(); for (IdeProject *p=*Projs; p; p=*++Projs) { LAutoString Base = p->GetBasePath(); if (Base) { char Path[MAX_PATH_LEN]; LMakePath(Path, sizeof(Path), Base, File); if (LFileExists(Path)) { Full.Reset(NewStr(Path)); break; } } } } if (!Full) { char *Dir = dirchar(File, true); for (auto p: Projects) { if (Full.Reset(p->FindFullPath(Dir?Dir+1:File))) break; } if (!Full) { if (LFileExists(File)) { Full.Reset(NewStr(File)); } } } return ValidStr(Full); } void ViewMsg(char *File, int Line, char *Context) { LAutoString Full; if (FindSource(Full, File, Context)) { App->GotoReference(Full, Line, false); } } #if 1 #define LOG_SEEK_MSG(...) printf(__VA_ARGS__) #else #define LOG_SEEK_MSG(...) #endif void GetContext(const char16 *Txt, ssize_t &i, char16 *&Context) { static char16 NMsg[] = L"In file included "; static char16 FromMsg[] = L"from "; auto NMsgLen = StrlenW(NMsg); if (Txt[i] != '\n') return; if (StrncmpW(Txt + i + 1, NMsg, NMsgLen)) return; i += NMsgLen + 1; while (Txt[i]) { // Skip whitespace while (Txt[i] && strchr(" \t\r\n", Txt[i])) i++; // Check for 'from' if (StrncmpW(FromMsg, Txt + i, 5)) break; i += 5; auto Start = Txt + i; // Skip to end of doc or line const char16 *Colon = 0; while (Txt[i] && Txt[i] != '\n') { if (Txt[i] == ':' && Txt[i+1] != '\n') { Colon = Txt + i; } i++; } if (Colon) { DeleteArray(Context); Context = NewStrW(Start, Colon-Start); } } } template bool IsTimeStamp(T *s, ssize_t i) { while (i > 0 && s[i-1] != '\n') i--; auto start = i; while (s[i] && (IsDigit(s[i]) || strchr(" :-", s[i]))) i++; LString txt(s + start, i - start); auto parts = txt.SplitDelimit(" :-"); return parts.Length() == 6 && parts[0].Length() == 4; } template bool IsContext(T *s, ssize_t i) { auto key = L"In file included"; auto end = i; while (i > 0 && s[i-1] != '\n') i--; if (Strnistr(s + i, key, end - i)) return true; return false; } #define PossibleLineSep(ch) \ ( (ch) == ':' || (ch) == '(' ) void SeekMsg(int Direction) { LString Comp; IdeProject *p = App->RootProject(); if (p) p ->GetSettings()->GetStr(ProjCompiler); // bool IsIAR = Comp.Equals("IAR"); if (!Output) return; int64 Current = Output->Value(); LTextView3 *o = Current < CountOf(Output->Txt) ? Output->Txt[Current] : 0; if (!o) return; auto Txt = o->NameW(); if (!Txt) return; ssize_t Cur = o->GetCaret(); char16 *Context = NULL; // Scan forward to the end of file for the next filename/line number separator. ssize_t i; for (i=Cur; Txt[i]; i++) { GetContext(Txt, i, Context); if ( PossibleLineSep(Txt[i]) && isdigit(Txt[i+1]) && !IsTimeStamp(Txt, i) && !IsContext(Txt, i) ) { break; } } // If not found then scan from the start of the file for the next filename/line number separator. if (!PossibleLineSep(Txt[i])) { for (i=0; i 0 && !strchr("\n>", Txt[Line-1])) { Line--; } // Store the filename LString File(Txt+Line, i-Line); if (!File) return; #if DIR_CHAR == '\\' File = File.Replace("/", "\\"); #else File = File.Replace("\\", "/"); #endif // Scan over the line number.. auto NumIndex = ++i; while (isdigit(Txt[NumIndex])) NumIndex++; // Store the line number LString NumStr(Txt + i, NumIndex - i); if (!NumStr) return; // Convert it to an integer auto LineNumber = (int)NumStr.Int(); o->SetCaret(Line, false); o->SetCaret(NumIndex + 1, true); LString Context8 = Context; ViewMsg(File, LineNumber, Context8); } void UpdateMenus() { static const char *None = "(none)"; if (!App->GetMenu()) return; // This happens in GTK during window destruction if (RecentFilesMenu) { RecentFilesMenu->Empty(); if (RecentFiles.Length() == 0) RecentFilesMenu->AppendItem(None, 0, false); else { int n=0; char *f; for (auto It = RecentFiles.begin(); (f = *It); f=*(++It)) { for (; f; f=*(++It)) { if (LIsUtf8(f)) RecentFilesMenu->AppendItem(f, IDM_RECENT_FILE+n++, true); else RecentFiles.Delete(It); } } } } if (RecentProjectsMenu) { RecentProjectsMenu->Empty(); if (RecentProjects.Length() == 0) RecentProjectsMenu->AppendItem(None, 0, false); else { int n=0; char *f; for (auto It = RecentProjects.begin(); (f = *It); f=*(++It)) { if (LIsUtf8(f)) RecentProjectsMenu->AppendItem(f, IDM_RECENT_PROJECT+n++, true); else RecentProjects.Delete(It); } } } if (WindowsMenu) { WindowsMenu->Empty(); Docs.Sort(DocSorter); int n=0; for (auto d: Docs) { const char *File = d->GetFileName(); if (!File) File = "(untitled)"; char *Dir = strrchr((char*)File, DIR_CHAR); WindowsMenu->AppendItem(Dir?Dir+1:File, IDM_WINDOWS+n++, true); } if (!Docs.Length()) { WindowsMenu->AppendItem(None, 0, false); } } } void Dump(LString::Array &a) { for (auto i: a) printf(" %s\n", i.Get()); } void OnFile(const char *File, bool IsProject = false) { if (!File) return; auto *Recent = IsProject ? &RecentProjects : &RecentFiles; for (auto &f: *Recent) { if (f && LFileCompare(f, File) == 0) { f = File; UpdateMenus(); return; } } Recent->AddAt(0, File); if (Recent->Length() > 10) Recent->Length(10); UpdateMenus(); } void RemoveRecent(const char *File) { if (File) { LString::Array *Recent[3] = { &RecentProjects, &RecentFiles, 0 }; for (int i=0; Recent[i]; i++) { auto &a = *Recent[i]; for (size_t n=0; nIsFile(File)) { return Doc; } } // LgiTrace("%s:%i - '%s' not found in %i docs.\n", _FL, File, Docs.Length()); return 0; } IdeProject *IsProjectOpen(const char *File) { if (File) { for (auto p: Projects) { if (p->GetFileName() && stricmp(p->GetFileName(), File) == 0) { return p; } } } return 0; } void SerializeStringList(const char *Opt, LString::Array *Lst, bool Write) { LVariant v; LString Sep = OptFileSeparator; if (Write) { if (Lst->Length() > 0) { auto s = Sep.Join(*Lst); Options.SetValue(Opt, v = s.Get()); // printf("Saving '%s' to %s\n", s.Get(), Opt); } else Options.DeleteValue(Opt); } else if (Options.GetValue(Opt, v)) { auto files = LString(v.Str()).Split(Sep); Lst->Length(0); for (auto f: files) if (f.Length() > 0) Lst->Add(f); // printf("Reading '%s' to %s, file.len=%i %s\n", v.Str(), Opt, (int)files.Length(), v.Str()); } // else printf("%s:%i - No option '%s' to read.\n", _FL, Opt); } }; AppWnd::AppWnd() { #ifdef __GTK_H__ LgiGetResObj(true, AppName); #endif LRect r(0, 0, 1000, 760); SetPos(r); MoveToCenter(); d = new AppWndPrivate(this); Name(AppName); SetQuitOnClose(true); #if WINNATIVE SetIcon((char*)MAKEINTRESOURCE(IDI_APP)); #else SetIcon("icon64.png"); #endif if (!Attach(0)) { LgiTrace("%s:%i - Attach failed.\n", _FL); return; } if ((Menu = new LMenu)) { Menu->Attach(this); bool Loaded = Menu->Load(this, "IDM_MENU"); LAssert(Loaded); if (Loaded) { Menu->SetPrefAndAboutItems(IDM_OPTIONS, IDM_ABOUT); d->RecentFilesMenu = Menu->FindSubMenu(IDM_RECENT_FILES); d->RecentProjectsMenu = Menu->FindSubMenu(IDM_RECENT_PROJECTS); d->WindowsMenu = Menu->FindSubMenu(IDM_WINDOW_LST); d->CreateMakefileMenu = Menu->FindSubMenu(IDM_CREATE_MAKEFILE); if (d->CreateMakefileMenu) { d->CreateMakefileMenu->Empty(); for (int i=0; PlatformNames[i]; i++) { d->CreateMakefileMenu->AppendItem(PlatformNames[i], IDM_MAKEFILE_BASE + i); } } else LgiTrace("%s:%i - FindSubMenu failed.\n", _FL); LMenuItem *Debug = GetMenu()->FindItem(IDM_DEBUG_MODE); if (Debug) Debug->Checked(true); else LgiTrace("%s:%i - FindSubMenu failed.\n", _FL); d->UpdateMenus(); } } LToolBar *Tools = NULL; if (GdcD->Y() > 1200) Tools = LgiLoadToolbar(this, "cmds-32px.png", 32, 32); else Tools = LgiLoadToolbar(this, "cmds-16px.png", 16, 16); if (Tools) { Tools->AppendButton("New", IDM_NEW, TBT_PUSH, true, CMD_NEW); Tools->AppendButton("Open", IDM_OPEN, TBT_PUSH, true, CMD_OPEN); Tools->AppendButton("Save", IDM_SAVE_ALL, TBT_PUSH, true, CMD_SAVE_ALL); Tools->AppendSeparator(); Tools->AppendButton("Cut", IDM_CUT, TBT_PUSH, true, CMD_CUT); Tools->AppendButton("Copy", IDM_COPY, TBT_PUSH, true, CMD_COPY); Tools->AppendButton("Paste", IDM_PASTE, TBT_PUSH, true, CMD_PASTE); Tools->AppendSeparator(); Tools->AppendButton("Compile", IDM_COMPILE, TBT_PUSH, true, CMD_COMPILE); Tools->AppendButton("Build", IDM_BUILD, TBT_PUSH, true, CMD_BUILD); Tools->AppendButton("Stop", IDM_STOP_BUILD, TBT_PUSH, true, CMD_STOP_BUILD); // Tools->AppendButton("Execute", IDM_EXECUTE, TBT_PUSH, true, CMD_EXECUTE); Tools->AppendSeparator(); Tools->AppendButton("Debug", IDM_START_DEBUG, TBT_PUSH, true, CMD_DEBUG); Tools->AppendButton("Pause", IDM_PAUSE_DEBUG, TBT_PUSH, true, CMD_PAUSE); Tools->AppendButton("Restart", IDM_RESTART_DEBUGGING, TBT_PUSH, true, CMD_RESTART); Tools->AppendButton("Kill", IDM_STOP_DEBUG, TBT_PUSH, true, CMD_KILL); Tools->AppendButton("Step Into", IDM_STEP_INTO, TBT_PUSH, true, CMD_STEP_INTO); Tools->AppendButton("Step Over", IDM_STEP_OVER, TBT_PUSH, true, CMD_STEP_OVER); Tools->AppendButton("Step Out", IDM_STEP_OUT, TBT_PUSH, true, CMD_STEP_OUT); Tools->AppendButton("Run To", IDM_RUN_TO, TBT_PUSH, true, CMD_RUN_TO); Tools->AppendSeparator(); Tools->AppendButton("Find In Files", IDM_FIND_IN_FILES, TBT_PUSH, true, CMD_FIND_IN_FILES); Tools->GetCss(true)->Padding("4px"); Tools->Attach(this); } else LgiTrace("%s:%i - No tools obj?", _FL); LVariant SplitPx = 270, OutPx = 250, v; d->Options.GetValue(OPT_SPLIT_PX, SplitPx); d->Options.GetValue(OPT_OUTPUT_PX, OutPx); if (d->Options.GetValue(OPT_PLATFORM, v)) SetPlatform(v.CastInt32()); else SetPlatform(PLATFORM_CURRENT); AddView(d->VBox = new LBox); d->VBox->SetVertical(true); d->HBox = new LBox; d->VBox->AddView(d->HBox); d->VBox->AddView(d->Output = new IdeOutput(this)); d->HBox->AddView(d->Tree = new IdeTree); if (d->Tree) { d->Tree->SetImageList(d->Icons, false); d->Tree->Sunken(false); } d->HBox->AddView(d->Mdi = new LMdiParent); if (d->Mdi) { d->Mdi->HasButton(true); } d->HBox->Value(MAX(SplitPx.CastInt32(), 20)); LRect c = GetClient(); if (c.Y() > OutPx.CastInt32()) { auto Px = OutPx.CastInt32(); LCss::Len y(LCss::LenPx, (float)MAX(Px, 120)); d->Output->GetCss(true)->Height(y); } AttachChildren(); OnPosChange(); UpdateState(); Visible(true); DropTarget(true); SetPulse(1000); #ifdef LINUX LFinishXWindowsStartup(this); #endif #if USE_HAIKU_PULSE_HACK if (d->Output) d->Output->SetPulse(1000); #endif OnCommand(IDM_NEW, 0, NULL); } AppWnd::~AppWnd() { LAssert(IsClean()); #ifdef HAIKU WaitThread(); #endif LVariant v; if (d->HBox) d->Options.SetValue(OPT_SPLIT_PX, v = d->HBox->Value()); if (d->Output) d->Options.SetValue(OPT_OUTPUT_PX, v = d->Output->Y()); d->Options.SetValue(OPT_PLATFORM, v = d->Platform); ShutdownFtpThread(); LAppInst->AppWnd = NULL; DeleteObj(d); } void AppWnd::OnPulse() { IdeDoc *Top = TopDoc(); if (Top) Top->OnPulse(); if (d->FixBuildWait) { d->FixBuildWait = false; if (OnFixBuildErrors() > 0) d->RebuildWait = 3; } else if (d->RebuildWait > 0) { if (--d->RebuildWait == 0) Build(); } for (auto n: NeedsPulse) n->OnPulse(); } LDebugContext *AppWnd::GetDebugContext() { return d->DbgContext; } struct DumpBinThread : public LThread { LStream *Out; LString InFile; bool IsLib; public: DumpBinThread(LStream *out, LString file) : LThread("DumpBin.Thread") { Out = out; InFile = file; DeleteOnExit = true; auto Ext = LGetExtension(InFile); IsLib = Ext && !stricmp(Ext, "lib"); Run(); } bool DumpBin(LString Args, LStream *Str) { char Buf[256]; ssize_t Rd; const char *Prog = "c:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\VC\\bin\\dumpbin.exe"; LSubProcess s(Prog, Args); if (!s.Start(true, false)) { Out->Print("%s:%i - '%s' doesn't exist.\n", _FL, Prog); return false; } while ((Rd = s.Read(Buf, sizeof(Buf))) > 0) Str->Write(Buf, Rd); return true; } LString::Array Dependencies(const char *Executable, int Depth = 0) { LString Args; LStringPipe p; Args.Printf("/dependents \"%s\"", Executable); DumpBin(Args, &p); char Spaces[256]; int Len = Depth * 2; memset(Spaces, ' ', Len); Spaces[Len] = 0; LString::Array Files; - auto Parts = p.NewGStr().Replace("\r", "").Split("\n\n"); + auto Parts = p.NewLStr().Replace("\r", "").Split("\n\n"); if (Parts.Length() > 0) { Files = Parts[4].Strip().Split("\n"); auto Path = LGetPath(); for (size_t i=0; i= 0) { auto p = Ln.Strip().Split(Key); if (p.Length() == 2) { Arch = p[1].Strip("()"); Machine = p[0].Int(16); } } } if (Machine == 0x14c) Arch += " 32bit"; else if (Machine == 0x200) Arch += " 64bit Itanium"; else if (Machine == 0x8664) Arch += " 64bit"; return Arch; } LString GetExports() { LString Args; LStringPipe p; /* if (IsLib) Args.Printf("/symbols \"%s\"", InFile.Get()); else */ Args.Printf("/exports \"%s\"", InFile.Get()); DumpBin(Args, &p); LString Exp; - auto Raw = p.NewGStr().Replace("\r", ""); + auto Raw = p.NewLStr().Replace("\r", ""); auto Sect = Raw.Split("\n\n"); #if 0 // Debug output Exp = Raw; #else if (IsLib) { for (auto &s : Sect) { if (s.Find("COFF/PE Dumper") >= 0) continue; auto ln = s.Split("\n"); if (ln.Length() == 1) continue; for (auto &l: ln) l = l.LStrip(); Exp = LString("\n").Join(ln); break; } } else { bool Ord = false; for (auto &s : Sect) { if (s.Strip().Find("ordinal") == 0) Ord = true; else if (Ord) { Exp = s; break; } else Ord = false; } } #endif return Exp; } int Main() { if (!IsLib) { auto Deps = Dependencies(InFile); if (Deps.Length()) Out->Print("Dependencies:\n\t%s\n\n", LString("\n\t").Join(Deps).Get()); } auto Arch = GetArch(); if (Arch) Out->Print("Arch: %s\n\n", Arch.Get()); auto Exp = GetExports(); if (Arch) Out->Print("Exports:\n%s\n\n", Exp.Get()); return 0; } }; void AppWnd::OnReceiveFiles(LArray &Files) { for (int i=0; iSetFileName(Docs, false); new DumpBinThread(Doc, Files[i]); } } else { OpenFile(f); } } Raise(); if (LAppInst->GetOption("createMakeFiles")) { IdeProject *p = RootProject(); if (p) { p->CreateMakefile(PlatformCurrent, false); } } } void AppWnd::OnDebugState(bool Debugging, bool Running) { // Make sure this event is processed in the GUI thread. #if DEBUG_SESSION_LOGGING LgiTrace("AppWnd::OnDebugState(%i,%i) InThread=%i\n", Debugging, Running, InThread()); #endif PostEvent(M_DEBUG_ON_STATE, Debugging, Running); } bool IsVarChar(LString &s, ssize_t pos) { if (pos < 0) return false; if (pos >= s.Length()) return false; char i = s[pos]; return IsAlpha(i) || IsDigit(i) || i == '_'; } bool ReplaceWholeWord(LString &Ln, LString Word, LString NewWord) { ssize_t Pos = 0; bool Status = false; while (Pos >= 0) { Pos = Ln.Find(Word, Pos); if (Pos < 0) return Status; ssize_t End = Pos + Word.Length(); if (!IsVarChar(Ln, Pos-1) && !IsVarChar(Ln, End)) { LString NewLn = Ln(0,Pos) + NewWord + Ln(End,-1); Ln = NewLn; Status = true; } Pos++; } return Status; } struct LFileInfo { LString Path; LString::Array Lines; bool Dirty; LFileInfo() { Dirty = false; } bool Save() { LFile f; if (!f.Open(Path, O_WRITE)) return false; LString NewFile = LString("\n").Join(Lines); f.SetSize(0); f.Write(NewFile); f.Close(); return true; } }; int AppWnd::OnFixBuildErrors() { LHashTbl, LString> Map; LVariant v; if (GetOptions()->GetValue(OPT_RENAMED_SYM, v)) { auto Lines = LString(v.Str()).Split("\n"); for (auto Ln: Lines) { auto p = Ln.SplitDelimit(); if (p.Length() == 2) Map.Add(p[0], p[1]); } } LString Raw = d->Output->Txt[AppWnd::BuildTab]->Name(); LString::Array Lines = Raw.Split("\n"); auto *Log = d->Output->Txt[AppWnd::OutputTab]; Log->Name(NULL); Log->Print("Parsing errors...\n"); int Replacements = 0; LArray Files; LHashTbl,bool> FixHistory; for (int Idx=0; Idx= 0) { #ifdef WINDOWS LString::Array p = Ln.SplitDelimit(">()"); #else LString::Array p = Ln(0, ErrPos).Strip().SplitDelimit(":"); #endif if (p.Length() <= 2) { Log->Print("Error: Only %i parts? '%s'\n", (int)p.Length(), Ln.Get()); } else { #ifdef WINDOWS int Base = p[0].IsNumeric() ? 1 : 0; LString Fn = p[Base]; if (Fn.Find("Program Files") >= 0) { Log->Print("Is prog file\n"); continue; } auto LineNo = p[Base+1].Int(); bool FileNotFound = Ln.Find("Cannot open include file:") > 0; #else LString Fn = p[0]; auto LineNo = p[1].Int(); bool FileNotFound = false; // fixme #endif LAutoString Full; if (!d->FindSource(Full, Fn, NULL)) { Log->Print("Error: Can't find Fn='%s' Line=%i\n", Fn.Get(), (int)LineNo); continue; } LFileInfo *Fi = NULL; for (auto &i: Files) { if (i.Path.Equals(Full)) { Fi = &i; break; } } if (!Fi) { LFile f(Full, O_READ); if (f.IsOpen()) { Fi = &Files.New(); Fi->Path = Full.Get(); auto OldFile = f.Read(); Fi->Lines = OldFile.SplitDelimit("\n", -1, false); } else { Log->Print("Error: Can't open '%s'\n", Full.Get()); } } if (Fi) { LString Loc; Loc.Printf("%s:%i", Full.Get(), (int)LineNo); if (FixHistory.Find(Loc)) { // Log->Print("Already fixed %s\n", Loc.Get()); } else if (LineNo <= Fi->Lines.Length()) { FixHistory.Add(Loc, true); if (FileNotFound) { auto n = p.Last().SplitDelimit("\'"); auto wrongName = n[1]; LFile f(Full, O_READ); auto Lines = f.Read().SplitDelimit("\n", -1, false); f.Close(); if (LineNo <= Lines.Length()) { auto &errLine = Lines[LineNo-1]; auto Pos = errLine.Find(wrongName); /* if (Pos < 0) { for (int i=0; iPrint("[%i]=%s\n", i, Lines[i].Get()); } */ if (Pos > 0) { // Find where it went... LString newPath; for (auto p: d->Projects) { const char *SubStr[] = { ".", "lgi/common" }; LArray IncPaths; if (p->BuildIncludePaths(IncPaths, true, false, PlatformCurrent)) { for (auto &inc: IncPaths) { for (int sub=0; !newPath && subPrint("Already changed '%s'.\n", wrongName.Get()); } else { LString backup = LString(Full.Get()) + ".orig"; if (LFileExists(backup)) FileDev->Delete(backup); LError Err; if (FileDev->Move(Full, backup, &Err)) { errLine = newLine; LString newLines = LString("\n").Join(Lines); LFile out(Full, O_WRITE); out.Write(newLines); Log->Print("Fixed '%s'->'%s' on ln %i in %s\n", wrongName.Get(), newPath.Get(), (int)LineNo, Full.Get()); Replacements++; } else Log->Print("Error: moving '%s' to backup (%s).\n", Full.Get(), Err.GetMsg().Get()); } } else Log->Print("Error: Missing header '%s'.\n", wrongName.Get()); } else { Log->Print("Error: '%s' not found in line %i of '%s' -> '%s'\n", wrongName.Get(), (int)LineNo, Fn.Get(), Full.Get()); // return; } } else Log->Print("Error: Line %i is beyond file lines: %i\n", (int)LineNo, (int)Lines.Length()); } else { auto OldReplacements = Replacements; for (auto i: Map) { for (int Offset = 0; (LineNo + Offset >= 1) && Offset >= -1; Offset--) { LString &s = Fi->Lines[LineNo+Offset-1]; if (ReplaceWholeWord(s, i.key, i.value)) { Log->Print("Renamed '%s' -> '%s' at %s:%i\n", i.key, i.value.Get(), Full.Get(), LineNo+Offset); Fi->Dirty = true; Replacements++; Offset = -2; } } } if (OldReplacements == Replacements && Ln.Find("syntax error: id") > 0) { Log->Print("Unhandled: %s\n", Ln.Get()); } } } else { Log->Print("Error: Invalid line %i\n", (int)LineNo); } } else { Log->Print("Error: Fi is NULL\n"); } } } } for (auto &Fi : Files) { if (Fi.Dirty) Fi.Save(); } Log->Print("%i replacements made.\n", Replacements); if (Replacements > 0) d->Output->Value(AppWnd::OutputTab); return Replacements; } void AppWnd::OnBuildStateChanged(bool NewState) { LVariant v; if (!NewState && GetOptions()->GetValue(OPT_FIX_RENAMED, v) && v.CastInt32()) { d->FixBuildWait = true; } } void AppWnd::UpdateState(int Debugging, int Building) { // printf("UpdateState %i %i\n", Debugging, Building); if (Debugging >= 0) d->Debugging = Debugging; if (Building >= 0) { if (d->Building != (Building != 0)) OnBuildStateChanged(Building); d->Building = Building; } SetCtrlEnabled(IDM_COMPILE, !d->Building); SetCtrlEnabled(IDM_BUILD, !d->Building); SetCtrlEnabled(IDM_STOP_BUILD, d->Building); // SetCtrlEnabled(IDM_RUN, !d->Building); // SetCtrlEnabled(IDM_TOGGLE_BREAKPOINT, !d->Building); SetCtrlEnabled(IDM_START_DEBUG, !d->Debugging && !d->Building); SetCtrlEnabled(IDM_PAUSE_DEBUG, d->Debugging); SetCtrlEnabled(IDM_RESTART_DEBUGGING, d->Debugging); SetCtrlEnabled(IDM_STOP_DEBUG, d->Debugging); SetCtrlEnabled(IDM_STEP_INTO, d->Debugging); SetCtrlEnabled(IDM_STEP_OVER, d->Debugging); SetCtrlEnabled(IDM_STEP_OUT, d->Debugging); SetCtrlEnabled(IDM_RUN_TO, d->Debugging); } void AppWnd::AppendOutput(char *Txt, AppWnd::Channels Channel) { if (!d->Output) { LgiTrace("%s:%i - No output panel.\n", _FL); return; } if (Channel < 0 || Channel >= CountOf(d->Output->Txt)) { LgiTrace("%s:%i - Channel range: %i, %i.\n", _FL, Channel, CountOf(d->Output->Txt)); return; } if (!d->Output->Txt[Channel]) { LgiTrace("%s:%i - No log for channel %i.\n", _FL, Channel); return; } if (Txt) { d->Output->Buf[Channel].Add(Txt, strlen(Txt)); } else { auto Ctrl = d->Output->Txt[Channel]; Ctrl->UnSelectAll(); Ctrl->Name(""); } } int AppWnd::GetPlatform() { return d->Platform; } bool AppWnd::SetPlatform(int p) { if (p == 0) p = PLATFORM_CURRENT; if (d->Platform == p) return false; d->Platform = p; if (auto m = GetMenu()) { bool all = d->Platform == PLATFORM_ALL; #define SET_CHK(id, flag) \ { auto mi = m->FindItem(id); \ if (mi) mi->Checked((d->Platform & flag) && !all); } SET_CHK(IDM_WIN, PLATFORM_WIN32); SET_CHK(IDM_LINUX, PLATFORM_LINUX); SET_CHK(IDM_MAC, PLATFORM_MAC); SET_CHK(IDM_HAIKU, PLATFORM_HAIKU); auto mi = m->FindItem(IDM_ALL_OS); if (mi) mi->Checked(all); } return true; } bool AppWnd::IsClean() { for (auto Doc: d->Docs) { if (!Doc->GetClean()) return false; } for (auto Proj: d->Projects) { if (!Proj->GetClean()) return false; } return true; } struct SaveState { AppWndPrivate *d = NULL; LArray Docs; LArray Projects; std::function Callback; bool Status = true; bool CloseDirty = false; void Iterate() { if (Docs.Length()) { auto doc = Docs[0]; Docs.DeleteAt(0); // printf("Saving doc...\n"); doc->SetClean([this, doc](bool ok) { // printf("SetClean cb ok=%i\n", ok); if (ok) d->OnFile(doc->GetFileName()); else { if (CloseDirty) delete doc; Status = false; } // printf("SetClean cb iter\n", ok); Iterate(); }); } else if (Projects.Length()) { auto proj = Projects[0]; Projects.DeleteAt(0); // printf("Saving proj...\n"); proj->SetClean([this, proj](bool ok) { if (ok) d->OnFile(proj->GetFileName(), true); else { if (CloseDirty) delete proj; Status = false; } Iterate(); }); } else { // printf("Doing callback...\n"); if (Callback) Callback(Status); // printf("Deleting...\n"); delete this; } } }; void AppWnd::SaveAll(std::function Callback, bool CloseDirty) { auto ss = new SaveState; ss->d = d; ss->Callback = Callback; ss->CloseDirty = CloseDirty; for (auto Doc: d->Docs) { if (!Doc->GetClean()) ss->Docs.Add(Doc); } for (auto Proj: d->Projects) { if (!Proj->GetClean()) ss->Projects.Add(Proj); } ss->Iterate(); } void AppWnd::CloseAll() { SaveAll([&](auto status) { if (!status) { LgiTrace("%s:%i - status=%i\n", _FL, status); return; } while (d->Docs[0]) delete d->Docs[0]; IdeProject *p = RootProject(); if (p) DeleteObj(p); while (d->Projects[0]) delete d->Projects[0]; DeleteObj(d->DbgContext); }); } bool AppWnd::OnRequestClose(bool IsOsQuit) { if (!IsClean()) { SaveAll([](bool status) { LCloseApp(); }, true); return false; } else { return LWindow::OnRequestClose(IsOsQuit); } } bool AppWnd::OnBreakPoint(LDebugger::BreakPoint &b, bool Add) { List::I it = d->Docs.begin(); for (IdeDoc *doc = *it; doc; doc = *++it) { auto fn = doc->GetFileName(); bool Match = !Stricmp(fn, b.File.Get()); if (Match) doc->AddBreakPoint(b.Line, Add); } if (d->DbgContext) d->DbgContext->OnBreakPoint(b, Add); return true; } bool AppWnd::LoadBreakPoints(IdeDoc *doc) { if (!doc) return false; auto fn = doc->GetFileName(); for (int i=0; iBreakPoints.Length(); i++) { LDebugger::BreakPoint &b = d->BreakPoints[i]; if (!_stricmp(fn, b.File)) { doc->AddBreakPoint(b.Line, true); } } return true; } bool AppWnd::LoadBreakPoints(LDebugger *db) { if (!db) return false; for (int i=0; iBreakPoints.Length(); i++) { LDebugger::BreakPoint &bp = d->BreakPoints[i]; db->SetBreakPoint(&bp); } return true; } bool AppWnd::ToggleBreakpoint(const char *File, ssize_t Line) { bool DeleteBp = false; for (int i=0; iBreakPoints.Length(); i++) { LDebugger::BreakPoint &b = d->BreakPoints[i]; if (!_stricmp(File, b.File) && b.Line == Line) { OnBreakPoint(b, false); d->BreakPoints.DeleteAt(i); DeleteBp = true; break; } } if (!DeleteBp) { LDebugger::BreakPoint &b = d->BreakPoints.New(); b.File = File; b.Line = Line; OnBreakPoint(b, true); } return true; } void AppWnd::DumpHistory() { #if 0 LgiTrace("History %i of %i\n", d->HistoryLoc, d->CursorHistory.Length()); for (int i=0; iCursorHistory.Length(); i++) { FileLoc &p = d->CursorHistory[i]; LgiTrace(" [%i] = %s, %i %s\n", i, p.File.Get(), p.Line, d->HistoryLoc == i ? "<-----":""); } #endif } /* void CheckHistory(LArray &CursorHistory) { if (CursorHistory.Length() > 0) { FileLoc *loc = &CursorHistory[0]; for (unsigned i=CursorHistory.Length(); iInHistorySeek) { if (d->CursorHistory.Length() > 0) { FileLoc &Last = d->CursorHistory.Last(); if (_stricmp(File, Last.File) == 0 && abs(Last.Line - Line) <= 1) { // Previous or next line... just update line number Last.Line = Line; DumpHistory(); return; } // Add new entry d->HistoryLoc++; FileLoc &loc = d->CursorHistory[d->HistoryLoc]; #ifdef WIN64 if ((NativeInt)loc.File.Get() == 0xcdcdcdcdcdcdcdcd) LAssert(0); // wtf? else #endif loc.Set(File, Line); } else { // Add new entry d->CursorHistory[0].Set(File, Line); } // Destroy any history after the current... d->CursorHistory.Length(d->HistoryLoc+1); DumpHistory(); } } void AppWnd::OnFile(char *File, bool IsProject) { d->OnFile(File, IsProject); } IdeDoc *AppWnd::NewDocWnd(const char *FileName, NodeSource *Src) { IdeDoc *Doc = new IdeDoc(this, Src, 0); if (Doc) { d->Docs.Insert(Doc); LRect p = d->Mdi->NewPos(); Doc->LView::SetPos(p); Doc->Attach(d->Mdi); Doc->Focus(true); Doc->Raise(); if (FileName) d->OnFile(FileName); } return Doc; } IdeDoc *AppWnd::GetCurrentDoc() { if (d->Mdi) return dynamic_cast(d->Mdi->GetTop()); return NULL; } IdeDoc *AppWnd::GotoReference(const char *File, int Line, bool CurIp, bool WithHistory) { if (!WithHistory) d->InHistorySeek = true; IdeDoc *Doc = File ? OpenFile(File) : GetCurrentDoc(); if (Doc) { Doc->SetLine(Line, CurIp); Doc->Focus(true); } if (!WithHistory) d->InHistorySeek = false; return Doc; } IdeDoc *AppWnd::FindOpenFile(char *FileName) { List::I it = d->Docs.begin(); for (IdeDoc *i=*it; i; i=*++it) { auto f = i->GetFileName(); if (f) { IdeProject *p = i->GetProject(); if (p) { LAutoString Base = p->GetBasePath(); if (Base) { char Path[MAX_PATH_LEN]; if (*f == '.') LMakePath(Path, sizeof(Path), Base, f); else strcpy_s(Path, sizeof(Path), f); if (stricmp(Path, FileName) == 0) return i; } } else { if (stricmp(f, FileName) == 0) return i; } } } return 0; } IdeDoc *AppWnd::OpenFile(const char *FileName, NodeSource *Src) { static bool DoingProjectFind = false; IdeDoc *Doc = 0; const char *File = Src ? Src->GetFileName() : FileName; if (!Src && !ValidStr(File)) { LgiTrace("%s:%i - No source or file?\n", _FL); return NULL; } LString FullPath; if (LIsRelativePath(File)) { IdeProject *Proj = Src && Src->GetProject() ? Src->GetProject() : RootProject(); if (Proj) { List Projs; Projs.Insert(Proj); Proj->CollectAllSubProjects(Projs); for (auto p: Projs) { auto ProjPath = p->GetBasePath(); char s[MAX_PATH_LEN]; LMakePath(s, sizeof(s), ProjPath, File); LString Path = s; if (p->CheckExists(Path)) { FullPath = Path; File = FullPath; break; } } } } // Sniff type... bool probablyLgiProj = false; if (!Stricmp(LGetExtension(File), "xml")) { LFile f(File, O_READ); if (f) { char buf[256]; auto rd = f.Read(buf, sizeof(buf)); if (rd > 0) probablyLgiProj = Strnistr(buf, "IsFileOpen(File); if (!Doc) { if (Src) { Doc = NewDocWnd(File, Src); } else if (!DoingProjectFind) { DoingProjectFind = true; List::I Proj = d->Projects.begin(); for (IdeProject *p=*Proj; p && !Doc; p=*++Proj) { p->InProject(LIsRelativePath(File), File, true, &Doc); } DoingProjectFind = false; d->OnFile(File); } } if (!Doc && LFileExists(File)) { Doc = new IdeDoc(this, 0, File); if (Doc) { Doc->OpenFile(File); LRect p = d->Mdi->NewPos(); Doc->LView::SetPos(p); d->Docs.Insert(Doc); d->OnFile(File); } } if (Doc) { Doc->SetEditorParams(4, 4, true, false); if (!Doc->IsAttached()) { Doc->Attach(d->Mdi); } Doc->Focus(true); Doc->Raise(); } return Doc; } IdeProject *AppWnd::RootProject() { for (auto p: d->Projects) if (!p->GetParentProject()) return p; return NULL; } IdeProject *AppWnd::OpenProject(const char *FileName, IdeProject *ParentProj, bool Create, bool Dep) { if (!FileName) { LgiTrace("%s:%i - Error: No filename.\n", _FL); return NULL; } if (d->IsProjectOpen(FileName)) { LgiTrace("%s:%i - Warning: Project already open.\n", _FL); return NULL; } IdeProject *p = new IdeProject(this); if (!p) { LgiTrace("%s:%i - Error: mem alloc.\n", _FL); return NULL; } LString::Array Inc; p->BuildIncludePaths(Inc, false, false, PlatformCurrent); d->FindSym->SetIncludePaths(Inc); p->SetParentProject(ParentProj); ProjectStatus Status = p->OpenFile(FileName); if (Status == OpenOk) { d->Projects.Insert(p); d->OnFile(FileName, true); if (!Dep) { auto d = strrchr(FileName, DIR_CHAR); if (d++) { char n[256]; sprintf(n, "%s [%s]", AppName, d); Name(n); } } } else { LgiTrace("%s:%i - Failed to open '%s'\n", _FL, FileName); DeleteObj(p); if (Status == OpenError) d->RemoveRecent(FileName); } if (!GetTree()->Selection()) { GetTree()->Select(GetTree()->GetChild()); } GetTree()->Focus(true); return p; } LMessage::Result AppWnd::OnEvent(LMessage *m) { switch (m->Msg()) { case M_MAKEFILES_CREATED: { IdeProject *p = (IdeProject*)m->A(); if (p) p->OnMakefileCreated(); break; } case M_LAST_MAKEFILE_CREATED: { if (LAppInst->GetOption("exit")) LCloseApp(); break; } case M_START_BUILD: { IdeProject *p = RootProject(); if (p) p->Build(true, GetBuildMode()); else printf("%s:%i - No root project.\n", _FL); break; } case M_BUILD_DONE: { UpdateState(-1, false); IdeProject *p = RootProject(); if (p) p->StopBuild(); break; } case M_BUILD_ERR: { char *Msg = (char*)m->B(); if (Msg) { d->Output->Txt[AppWnd::BuildTab]->Print("Build Error: %s\n", Msg); DeleteArray(Msg); } break; } case M_APPEND_TEXT: { LAutoString Text((char*) m->A()); Channels Ch = (Channels) m->B(); AppendOutput(Text, Ch); break; } case M_SELECT_TAB: { if (!d->Output) break; d->Output->Value(m->A()); break; } case M_DEBUG_ON_STATE: { bool Debugging = m->A(); bool Running = m->B(); if (d->Running != Running) { bool RunToNotRun = d->Running && !Running; d->Running = Running; if (RunToNotRun && d->Output && d->Output->DebugTab) { d->Output->DebugTab->SendNotify(LNotifyValueChanged); } } if (d->Debugging != Debugging) { d->Debugging = Debugging; if (!Debugging) { IdeDoc::ClearCurrentIp(); IdeDoc *c = GetCurrentDoc(); if (c) c->UpdateControl(); // Shutdown the debug context and free the memory DeleteObj(d->DbgContext); } } SetCtrlEnabled(IDM_START_DEBUG, !Debugging || !Running); SetCtrlEnabled(IDM_PAUSE_DEBUG, Debugging && Running); SetCtrlEnabled(IDM_RESTART_DEBUGGING, Debugging); SetCtrlEnabled(IDM_STOP_DEBUG, Debugging); SetCtrlEnabled(IDM_STEP_INTO, Debugging && !Running); SetCtrlEnabled(IDM_STEP_OVER, Debugging && !Running); SetCtrlEnabled(IDM_STEP_OUT, Debugging && !Running); SetCtrlEnabled(IDM_RUN_TO, Debugging && !Running); break; } default: { if (d->DbgContext) d->DbgContext->OnEvent(m); break; } } return LWindow::OnEvent(m); } bool AppWnd::OnNode(const char *Path, ProjectNode *Node, FindSymbolSystem::SymAction Action) { // This takes care of adding/removing files from the symbol search engine. if (!Path || !Node) return false; if (d->FindSym) d->FindSym->OnFile(Path, Action, Node->GetPlatforms()); return true; } LOptionsFile *AppWnd::GetOptions() { return &d->Options; } class Options : public LDialog { AppWnd *App; LFontType Font; public: Options(AppWnd *a) { SetParent(App = a); if (LoadFromResource(IDD_OPTIONS)) { SetCtrlEnabled(IDC_FONT, false); MoveToCenter(); if (!Font.Serialize(App->GetOptions(), OPT_EditorFont, false)) { Font.GetSystemFont("Fixed"); } char s[256]; if (Font.GetDescription(s, sizeof(s))) { SetCtrlName(IDC_FONT, s); } LVariant v; if (App->GetOptions()->GetValue(OPT_Jobs, v)) SetCtrlValue(IDC_JOBS, v.CastInt32()); else SetCtrlValue(IDC_JOBS, 2); } } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDOK: { LVariant v; Font.Serialize(App->GetOptions(), OPT_EditorFont, true); App->GetOptions()->SetValue(OPT_Jobs, v = GetCtrlValue(IDC_JOBS)); // Fall through.. } case IDCANCEL: { EndModal(c->GetId()); break; } case IDC_SET_FONT: { Font.DoUI(this, [&](auto ui) { char s[256]; if (Font.GetDescription(s, sizeof(s))) { SetCtrlName(IDC_FONT, s); } }); break; } } return 0; } }; void AppWnd::UpdateMemoryDump() { if (d->DbgContext) { const char *sWord = GetCtrlName(IDC_MEM_SIZE); int iWord = sWord ? atoi(sWord) : 1; int64 RowLen = GetCtrlValue(IDC_MEM_ROW_LEN); bool InHex = GetCtrlValue(IDC_MEM_HEX) != 0; d->DbgContext->FormatMemoryDump(iWord, (int)RowLen, InHex); } } int AppWnd::OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDC_PROJECT_TREE: { if (n.Type == LNotifyDeleteKey) { ProjectNode *n = dynamic_cast(d->Tree->Selection()); if (n) n->Delete(); } break; } case IDC_DEBUG_EDIT: { if (n.Type == LNotifyReturnKey && d->DbgContext) { const char *Cmd = Ctrl->Name(); if (Cmd) { d->DbgContext->OnUserCommand(Cmd); Ctrl->Name(NULL); } } break; } case IDC_MEM_ADDR: { if (n.Type == LNotifyReturnKey) { if (d->DbgContext) { const char *s = Ctrl->Name(); if (s) { auto sWord = GetCtrlName(IDC_MEM_SIZE); int iWord = sWord ? atoi(sWord) : 1; d->DbgContext->OnMemoryDump(s, iWord, (int)GetCtrlValue(IDC_MEM_ROW_LEN), GetCtrlValue(IDC_MEM_HEX) != 0); } else if (d->DbgContext->MemoryDump) { d->DbgContext->MemoryDump->Print("No address specified."); } else { LAssert(!"No MemoryDump."); } } else LAssert(!"No debug context."); } break; } case IDC_MEM_ROW_LEN: { if (n.Type == LNotifyReturnKey) UpdateMemoryDump(); break; } case IDC_MEM_HEX: case IDC_MEM_SIZE: { UpdateMemoryDump(); break; } case IDC_DEBUG_TAB: { if (d->DbgContext && n.Type == LNotifyValueChanged) { switch (Ctrl->Value()) { case AppWnd::LocalsTab: { d->DbgContext->UpdateLocals(); break; } case AppWnd::WatchTab: { d->DbgContext->UpdateWatches(); break; } case AppWnd::RegistersTab: { d->DbgContext->UpdateRegisters(); break; } case AppWnd::CallStackTab: { d->DbgContext->UpdateCallStack(); break; } case AppWnd::ThreadsTab: { d->DbgContext->UpdateThreads(); break; } default: break; } } break; } case IDC_LOCALS_LIST: { if (d->Output->Locals && n.Type == LNotifyItemDoubleClick && d->DbgContext) { LListItem *it = d->Output->Locals->GetSelected(); if (it) { const char *Var = it->GetText(2); const char *Val = it->GetText(3); if (Var) { if (d->Output->DebugTab) d->Output->DebugTab->Value(AppWnd::ObjectTab); d->DbgContext->DumpObject(Var, Val); } } } break; } case IDC_CALL_STACK: { if (n.Type == LNotifyValueChanged) { if (d->Output->DebugTab) d->Output->DebugTab->Value(AppWnd::CallStackTab); } else if (n.Type == LNotifyItemSelect) { // This takes the user to a given call stack reference if (d->Output->CallStack && d->DbgContext) { LListItem *item = d->Output->CallStack->GetSelected(); if (item) { LAutoString File; int Line; if (d->DbgContext->ParseFrameReference(item->GetText(1), File, Line)) { LAutoString Full; if (d->FindSource(Full, File, NULL)) { GotoReference(Full, Line, false); const char *sFrame = item->GetText(0); if (sFrame && IsDigit(*sFrame)) d->DbgContext->SetFrame(atoi(sFrame)); } } } } } break; } case IDC_WATCH_LIST: { WatchItem *Edit = NULL; switch (n.Type) { case LNotifyDeleteKey: { LArray Sel; for (LTreeItem *c = d->Output->Watch->GetChild(); c; c = c->GetNext()) { if (c->Select()) Sel.Add(c); } Sel.DeleteObjects(); break; } case LNotifyItemClick: { Edit = dynamic_cast(d->Output->Watch->Selection()); break; } case LNotifyContainerClick: { // Create new watch. Edit = new WatchItem(d->Output); if (Edit) d->Output->Watch->Insert(Edit); break; } default: break; } if (Edit) Edit->EditLabel(0); break; } case IDC_THREADS: { if (n.Type == LNotifyItemSelect) { // This takes the user to a given thread if (d->Output->Threads && d->DbgContext) { LListItem *item = d->Output->Threads->GetSelected(); if (item) { LString sId = item->GetText(0); int ThreadId = (int)sId.Int(); if (ThreadId > 0) { d->DbgContext->SelectThread(ThreadId); } } } } break; } } return 0; } bool AppWnd::Build() { SaveAll([&](bool status) { if (!status) { LgiTrace("%s:%i - status=%i\n", _FL); return; } IdeDoc *Top; IdeProject *p = RootProject(); if (p) { UpdateState(-1, true); p->Build(false, GetBuildMode()); } else if ((Top = TopDoc())) { Top->Build(); } }); return false; } class RenameDlg : public LDialog { AppWnd *App; public: static RenameDlg *Inst; RenameDlg(AppWnd *a) { Inst = this; SetParent(App = a); MoveSameScreen(a); if (LoadFromResource(IDC_RENAME)) { LVariant v; if (App->GetOptions()->GetValue(OPT_FIX_RENAMED, v)) SetCtrlValue(IDC_FIX_RENAMED, v.CastInt32()); if (App->GetOptions()->GetValue(OPT_RENAMED_SYM, v)) SetCtrlName(IDC_SYM, v.Str()); SetAlwaysOnTop(true); DoModeless(); } } ~RenameDlg() { Inst = NULL; } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_APPLY: { LVariant v; App->GetOptions()->SetValue(OPT_RENAMED_SYM, v = GetCtrlName(IDC_SYM)); App->GetOptions()->SetValue(OPT_FIX_RENAMED, v = GetCtrlValue(IDC_FIX_RENAMED)); App->GetOptions()->SerializeFile(true); break; } case IDC_CLOSE: { EndModeless(); break; } } return 0; } }; void AppWnd::SeekHistory(int Offset) { d->SeekHistory(Offset); } RenameDlg *RenameDlg::Inst = NULL; bool AppWnd::ShowInProject(const char *Fn) { if (!Fn) return false; for (auto p: d->Projects) { ProjectNode *Node = NULL; if (p->FindFullPath(Fn, &Node)) { for (LTreeItem *i = Node->GetParent(); i; i = i->GetParent()) { i->Expanded(true); } Node->Select(true); Node->ScrollTo(); return true; } } return false; } int AppWnd::OnCommand(int Cmd, int Event, OsView Wnd) { switch (Cmd) { case IDM_EXIT: { LCloseApp(); break; } case IDM_OPTIONS: { auto dlg = new Options(this); dlg->DoModal(NULL); break; } case IDM_HELP: { LExecute(APP_URL); break; } case IDM_ABOUT: { auto dlg = new LAbout( this, AppName, APP_VER, "\nLGI Integrated Development Environment", "icon128.png", APP_URL, "fret@memecode.com"); dlg->DoModal(NULL); break; } case IDM_NEW: { IdeDoc *Doc; d->Docs.Insert(Doc = new IdeDoc(this, 0, 0)); if (Doc && d->Mdi) { LRect p = d->Mdi->NewPos(); Doc->LView::SetPos(p); Doc->Attach(d->Mdi); Doc->Focus(true); } break; } case IDM_OPEN: { LFileSelect *s = new LFileSelect; s->Parent(this); // printf("File open dlg from thread=%u\n", GetCurrentThreadId()); s->Open([&](auto s, auto ok) { // printf("open handler start... ok=%i thread=%u\n", ok, GetCurrentThreadId()); if (ok) OpenFile(s->Name()); // printf("open handler deleting...\n"); delete s; // printf("open handler deleted...\n"); }); break; } case IDM_SAVE_ALL: { SaveAll(NULL); break; } case IDM_SAVE: { IdeDoc *Top = TopDoc(); if (Top) Top->SetClean(NULL); break; } case IDM_SAVEAS: { IdeDoc *Top = TopDoc(); if (Top) { LFileSelect *s = new LFileSelect; s->Parent(this); s->Save([&](auto s, auto ok) { Top->SetFileName(s->Name(), true); d->OnFile(s->Name()); delete s; }); } break; } case IDM_CLOSE: { IdeDoc *Top = TopDoc(); if (Top) { if (Top->OnRequestClose(false)) { Top->Quit(); } } DeleteObj(d->DbgContext); break; } case IDM_CLOSE_ALL: { CloseAll(); Name(AppName); break; } // // Editor // case IDM_UNDO: { LTextView3 *Doc = FocusEdit(); if (Doc) { Doc->Undo(); } else LgiTrace("%s:%i - No focus doc.\n", _FL); break; } case IDM_REDO: { LTextView3 *Doc = FocusEdit(); if (Doc) { Doc->Redo(); } else LgiTrace("%s:%i - No focus doc.\n", _FL); break; } case IDM_FIND: { LTextView3 *Doc = FocusEdit(); if (Doc) { Doc->DoFind(NULL); } else LgiTrace("%s:%i - No focus doc.\n", _FL); break; } case IDM_FIND_NEXT: { LTextView3 *Doc = FocusEdit(); if (Doc) { Doc->DoFindNext(NULL); } else LgiTrace("%s:%i - No focus doc.\n", _FL); break; } case IDM_REPLACE: { LTextView3 *Doc = FocusEdit(); if (Doc) { Doc->DoReplace(NULL); } else LgiTrace("%s:%i - No focus doc.\n", _FL); break; } case IDM_GOTO: { LTextView3 *Doc = FocusEdit(); if (Doc) Doc->DoGoto(NULL); else { LInput *Inp = new LInput(this, NULL, LLoadString(L_TEXTCTRL_GOTO_LINE, "Goto [file:]line:"), "Goto"); Inp->DoModal([Inp,this](auto dlg, auto code) { LString s = Inp->GetStr(); LString::Array p = s.SplitDelimit(":,"); if (p.Length() == 2) { LString file = p[0]; int line = (int)p[1].Int(); GotoReference(file, line, false, true); } else LgiMsg(this, "Error: Needs a file name as well.", AppName); delete dlg; }); } break; } case IDM_CUT: { LTextView3 *Doc = FocusEdit(); if (Doc) Doc->PostEvent(M_CUT); break; } case IDM_COPY: { LTextView3 *Doc = FocusEdit(); if (Doc) Doc->PostEvent(M_COPY); break; } case IDM_PASTE: { LTextView3 *Doc = FocusEdit(); if (Doc) Doc->PostEvent(M_PASTE); break; } case IDM_FIND_IN_FILES: { if (!d->Finder) { d->Finder.Reset(new FindInFilesThread(d->AppHnd)); } if (d->Finder) { if (!d->FindParameters && d->FindParameters.Reset(new FindParams)) { LVariant var; if (GetOptions()->GetValue(OPT_ENTIRE_SOLUTION, var)) d->FindParameters->Type = var.CastInt32() ? FifSearchSolution : FifSearchDirectory; } auto Dlg = new FindInFiles(this, d->FindParameters); LViewI *Focus = GetFocus(); if (Focus) { LTextView3 *Edit = dynamic_cast(Focus); if (Edit && Edit->HasSelection()) { LAutoString a(Edit->GetSelection()); Dlg->Params->Text = a; } } IdeProject *p = RootProject(); if (p) { LAutoString Base = p->GetBasePath(); if (Base) Dlg->Params->Dir = Base; } Dlg->DoModal([this, Dlg, p](auto dlg, auto code) { if (p && Dlg->Params->Type == FifSearchSolution) { Dlg->Params->ProjectFiles.Length(0); List Projects; Projects.Insert(p); p->GetChildProjects(Projects); LArray Nodes; for (auto p: Projects) p->GetAllNodes(Nodes); for (unsigned i=0; iGetFullPath(); if (s) Dlg->Params->ProjectFiles.Add(s); } } LVariant var = d->FindParameters->Type == FifSearchSolution; GetOptions()->SetValue(OPT_ENTIRE_SOLUTION, var); d->Finder->Stop(); if (d->FindParameters->Text) d->Finder->PostEvent(FindInFilesThread::M_START_SEARCH, (LMessage::Param) new FindParams(d->FindParameters)); delete Dlg; }); } break; } case IDM_FIND_SYMBOL: { IdeDoc *Doc = FocusDoc(); if (Doc) { Doc->GotoSearch(IDC_SYMBOL_SEARCH); } else { d->FindSym->OpenSearchDlg(this, [&](auto r) { if (r.File) GotoReference(r.File, r.Line, false); }); } break; } case IDM_GOTO_SYMBOL: { IdeDoc *Doc = FocusDoc(); if (Doc) { Doc->SearchSymbol(); } break; } case IDM_FIND_PROJECT_FILE: { IdeDoc *Doc = FocusDoc(); if (Doc) { Doc->SearchFile(); } else { auto d = new FindInProject(this); d->DoModal([](auto dlg, auto ctrlId){ delete dlg; }); } break; } case IDM_FIND_REFERENCES: { LViewI *f = LAppInst->GetFocus(); LDocView *doc = dynamic_cast(f); if (!doc) break; ssize_t c = doc->GetCaret(); if (c < 0) break; LString Txt = doc->Name(); char *s = Txt.Get() + c; char *e = s; while ( s > Txt.Get() && IsSymbolChar(s[-1])) s--; while (*e && IsSymbolChar(*e)) e++; if (e <= s) break; LString Word(s, e - s); if (!d->Finder) d->Finder.Reset(new FindInFilesThread(d->AppHnd)); if (!d->Finder) break; IdeProject *p = RootProject(); if (!p) break; List Projects; Projects.Insert(p); p->GetChildProjects(Projects); LArray Nodes; for (auto p: Projects) p->GetAllNodes(Nodes); LAutoPtr Params(new FindParams); Params->Type = FifSearchSolution; Params->MatchWord = true; Params->Text = Word; for (unsigned i = 0; i < Nodes.Length(); i++) { Params->ProjectFiles.New() = Nodes[i]->GetFullPath(); } d->Finder->Stop(); d->Finder->PostEvent(FindInFilesThread::M_START_SEARCH, (LMessage::Param) Params.Release()); break; } case IDM_PREV_LOCATION: { d->SeekHistory(-1); break; } case IDM_NEXT_LOCATION: { d->SeekHistory(1); break; } // // Project // case IDM_NEW_PROJECT: { CloseAll(); IdeProject *p; d->Projects.Insert(p = new IdeProject(this)); if (p) { p->CreateProject(); } break; } case IDM_NEW_PROJECT_TEMPLATE: { NewProjectFromTemplate(this); break; } case IDM_OPEN_PROJECT: { LFileSelect *s = new LFileSelect; s->Parent(this); s->Type("Projects", "*.xml"); s->Open([&](auto s, auto ok) { if (ok) { CloseAll(); OpenProject(s->Name(), NULL, Cmd == IDM_NEW_PROJECT); if (d->Tree) { d->Tree->Focus(true); } } delete s; }); break; } case IDM_IMPORT_DSP: { IdeProject *p = RootProject(); if (p) { LFileSelect *s = new LFileSelect; s->Parent(this); s->Type("Developer Studio Project", "*.dsp"); s->Open([&](auto s, auto ok) { if (ok) p->ImportDsp(s->Name()); delete s; }); } break; } case IDM_RUN: { SaveAll([&](bool status) { if (!status) { LgiTrace("%s:%i - status=%i\n", _FL, status); return; } IdeProject *p = RootProject(); if (p) p->Execute(); }); break; } case IDM_VALGRIND: { SaveAll([&](bool status) { if (!status) { LgiTrace("%s:%i - status=%i\n", _FL, status); return; } IdeProject *p = RootProject(); if (p) p->Execute(ExeValgrind); }); break; } case IDM_FIX_MISSING_FILES: { IdeProject *p = RootProject(); if (p) p->FixMissingFiles(); else LgiMsg(this, "No project loaded.", AppName); break; } case IDM_FIND_DUPE_SYM: { IdeProject *p = RootProject(); if (p) p->FindDuplicateSymbols(); else LgiMsg(this, "No project loaded.", AppName); break; } case IDM_RENAME_SYM: { if (!RenameDlg::Inst) new RenameDlg(this); break; } case IDM_START_DEBUG: { SaveAll([&](bool status) { if (!status) { LgiTrace("%s:%i - status=%i\n", _FL, status); return; } IdeProject *p = RootProject(); if (!p) { LgiMsg(this, "No project loaded.", "Error"); return; } LString ErrMsg; if (d->DbgContext) { d->DbgContext->OnCommand(IDM_CONTINUE); } else if ((d->DbgContext = p->Execute(ExeDebug, &ErrMsg))) { d->DbgContext->DebuggerLog = d->Output->DebuggerLog; d->DbgContext->Watch = d->Output->Watch; d->DbgContext->Locals = d->Output->Locals; d->DbgContext->CallStack = d->Output->CallStack; d->DbgContext->Threads = d->Output->Threads; d->DbgContext->ObjectDump = d->Output->ObjectDump; d->DbgContext->Registers = d->Output->Registers; d->DbgContext->MemoryDump = d->Output->MemoryDump; d->DbgContext->OnCommand(IDM_START_DEBUG); d->Output->Value(AppWnd::DebugTab); d->Output->DebugEdit->Focus(true); } else if (ErrMsg) { LgiMsg(this, "Error: %s", AppName, MB_OK, ErrMsg.Get()); } }); break; } case IDM_TOGGLE_BREAKPOINT: { IdeDoc *Cur = GetCurrentDoc(); if (Cur) ToggleBreakpoint(Cur->GetFileName(), Cur->GetLine()); break; } case IDM_ATTACH_TO_PROCESS: case IDM_PAUSE_DEBUG: case IDM_RESTART_DEBUGGING: case IDM_RUN_TO: case IDM_STEP_INTO: case IDM_STEP_OVER: case IDM_STEP_OUT: { if (d->DbgContext) d->DbgContext->OnCommand(Cmd); break; } case IDM_STOP_DEBUG: { if (d->DbgContext && d->DbgContext->OnCommand(Cmd)) { DeleteObj(d->DbgContext); } break; } case IDM_BUILD: { Build(); break; } case IDM_STOP_BUILD: { IdeProject *p = RootProject(); if (p) p->StopBuild(); break; } case IDM_CLEAN: { SaveAll([&](bool status) { if (!status) { LgiTrace("%s:%i - status=%i\n", _FL, status); return; } IdeProject *p = RootProject(); if (p) p->Clean(true, GetBuildMode()); }); break; } case IDM_NEXT_MSG: { d->SeekMsg(1); break; } case IDM_PREV_MSG: { d->SeekMsg(-1); break; } case IDM_DEBUG_MODE: { LMenuItem *Debug = GetMenu()->FindItem(IDM_DEBUG_MODE); LMenuItem *Release = GetMenu()->FindItem(IDM_RELEASE_MODE); if (Debug && Release) { Debug->Checked(true); Release->Checked(false); } break; } case IDM_RELEASE_MODE: { LMenuItem *Debug = GetMenu()->FindItem(IDM_DEBUG_MODE); LMenuItem *Release = GetMenu()->FindItem(IDM_RELEASE_MODE); if (Debug && Release) { Debug->Checked(false); Release->Checked(true); } break; } // // OS Platform // #define HANDLE_PLATFORM_ITEM(id, flag) \ case id: \ { \ auto mi = GetMenu()->FindItem(id); \ if (!mi) break; \ if (d->Platform == PLATFORM_ALL) d->Platform = 0; \ if (mi->Checked()) SetPlatform(d->Platform & ~flag); \ else SetPlatform(d->Platform | flag); \ break; \ } HANDLE_PLATFORM_ITEM(IDM_WIN, PLATFORM_WIN32) HANDLE_PLATFORM_ITEM(IDM_LINUX, PLATFORM_LINUX) HANDLE_PLATFORM_ITEM(IDM_MAC, PLATFORM_MAC) HANDLE_PLATFORM_ITEM(IDM_HAIKU, PLATFORM_HAIKU) case IDM_ALL_OS: { SetPlatform(PLATFORM_ALL); break; } // // Other // case IDM_LOOKUP_SYMBOLS: { IdeDoc *Cur = GetCurrentDoc(); if (Cur) { // LookupSymbols(Cur->Read()); } break; } case IDM_DEPENDS: { IdeProject *p = RootProject(); if (p) { LString Exe = p->GetExecutable(GetCurrentPlatform()); if (LFileExists(Exe)) { Depends Dlg(this, Exe); } else { LgiMsg(this, "Couldn't find '%s'\n", AppName, MB_OK, Exe ? Exe.Get() : ""); } } break; } case IDM_SP_TO_TAB: { IdeDoc *Doc = FocusDoc(); if (Doc) Doc->ConvertWhiteSpace(true); break; } case IDM_TAB_TO_SP: { IdeDoc *Doc = FocusDoc(); if (Doc) Doc->ConvertWhiteSpace(false); break; } case IDM_ESCAPE: { IdeDoc *Doc = FocusDoc(); if (Doc) Doc->EscapeSelection(true); break; } case IDM_DESCAPE: { IdeDoc *Doc = FocusDoc(); if (Doc) Doc->EscapeSelection(false); break; } case IDM_SPLIT: { IdeDoc *Doc = FocusDoc(); if (!Doc) break; LInput *i = new LInput(this, "", "Separator:", AppName); i->DoModal([&](auto dlg, auto ok) { if (ok) Doc->SplitSelection(i->GetStr()); delete i; }); break; } case IDM_JOIN: { IdeDoc *Doc = FocusDoc(); if (!Doc) break; LInput *i = new LInput(this, "", "Separator:", AppName); i->DoModal([&](auto dlg, auto ok) { if (ok) Doc->JoinSelection(i->GetStr()); delete i; }); break; } case IDM_EOL_LF: { IdeDoc *Doc = FocusDoc(); if (!Doc) break; Doc->SetCrLf(false); break; } case IDM_EOL_CRLF: { IdeDoc *Doc = FocusDoc(); if (!Doc) break; Doc->SetCrLf(true); break; } case IDM_LOAD_MEMDUMP: { NewMemDumpViewer(this); break; } case IDM_SYS_CHAR_SUPPORT: { new SysCharSupport(this); break; } default: { int index = Cmd - IDM_RECENT_FILE; auto r = d->RecentFiles.IdxCheck(index)? d->RecentFiles[index] : LString(); if (r) { auto idx = Cmd - IDM_RECENT_FILE; if (idx > 0) { d->RecentFiles.DeleteAt(idx, true); d->RecentFiles.AddAt(0, r); } IdeDoc *f = d->IsFileOpen(r); if (f) f->Raise(); else OpenFile(r); } index = Cmd - IDM_RECENT_PROJECT; auto p = d->RecentProjects.IdxCheck(index) ? d->RecentProjects[index] : LString(); if (p) { auto idx = Cmd - IDM_RECENT_PROJECT; if (idx > 0) { d->RecentProjects.DeleteAt(idx, true); d->RecentProjects.AddAt(0, p); } CloseAll(); OpenProject(p, NULL, false); if (d->Tree) { d->Tree->Focus(true); } } IdeDoc *Doc = d->Docs[Cmd - IDM_WINDOWS]; if (Doc) { Doc->Raise(); } IdePlatform PlatIdx = (IdePlatform) (Cmd - IDM_MAKEFILE_BASE); const char *Platform = PlatIdx >= 0 && PlatIdx < PlatformMax ? PlatformNames[Cmd - IDM_MAKEFILE_BASE] : NULL; if (Platform) { IdeProject *p = RootProject(); if (p) { p->CreateMakefile(PlatIdx, false); } } break; } } return 0; } LTree *AppWnd::GetTree() { return d->Tree; } IdeDoc *AppWnd::TopDoc() { return d->Mdi ? dynamic_cast(d->Mdi->GetTop()) : NULL; } LTextView3 *AppWnd::FocusEdit() { return dynamic_cast(GetWindow()->GetFocus()); } IdeDoc *AppWnd::FocusDoc() { IdeDoc *Doc = TopDoc(); if (Doc) { if (Doc->HasFocus()) { return Doc; } else { LViewI *f = GetFocus(); LgiTrace("%s:%i - Edit doesn't have focus, f=%p %s doc.edit=%s\n", _FL, f, f ? f->GetClass() : 0, Doc->Name()); } } return 0; } void AppWnd::OnProjectDestroy(IdeProject *Proj) { if (d) { auto locked = Lock(_FL); // printf("OnProjectDestroy(%s) %i\n", Proj->GetFileName(), locked); d->Projects.Delete(Proj); if (locked) Unlock(); } else LAssert(!"No priv"); } void AppWnd::OnProjectChange() { LArray Views; if (d->Mdi->GetChildren(Views)) { for (unsigned i=0; i(Views[i]); if (Doc) Doc->OnProjectChange(); } } } void AppWnd::OnDocDestroy(IdeDoc *Doc) { if (d) { auto locked = Lock(_FL); d->Docs.Delete(Doc); d->UpdateMenus(); if (locked) Unlock(); } else { LAssert(!"OnDocDestroy no priv...\n"); } } BuildConfig AppWnd::GetBuildMode() { LMenuItem *Release = GetMenu()->FindItem(IDM_RELEASE_MODE); if (Release && Release->Checked()) { return BuildRelease; } return BuildDebug; } LList *AppWnd::GetFtpLog() { return d->Output->FtpLog; } LStream *AppWnd::GetOutputLog() { return d->Output->Txt[AppWnd::OutputTab]; } LStream *AppWnd::GetBuildLog() { return d->Output->Txt[AppWnd::BuildTab]; } LStream *AppWnd::GetDebugLog() { return d->Output->Txt[AppWnd::DebugTab]; } void AppWnd::FindSymbol(int ResultsSinkHnd, const char *Sym) { d->FindSym->Search(ResultsSinkHnd, Sym, d->Platform); } bool AppWnd::GetSystemIncludePaths(::LArray &Paths) { if (d->SystemIncludePaths.Length() == 0) { #if !defined(WINNATIVE) // echo | gcc -v -x c++ -E - LSubProcess sp1("echo"); LSubProcess sp2("gcc", "-v -x c++ -E -"); sp1.Connect(&sp2); sp1.Start(true, false); char Buf[256]; ssize_t r; LStringPipe p; while ((r = sp1.Read(Buf, sizeof(Buf))) > 0) { p.Write(Buf, r); } bool InIncludeList = false; while (p.Pop(Buf, sizeof(Buf))) { if (stristr(Buf, "#include")) { InIncludeList = true; } else if (stristr(Buf, "End of search")) { InIncludeList = false; } else if (InIncludeList) { LAutoString a(TrimStr(Buf)); d->SystemIncludePaths.New() = a; } } #else char p[MAX_PATH_LEN]; LGetSystemPath(LSP_USER_DOCUMENTS, p, sizeof(p)); LMakePath(p, sizeof(p), p, "Visual Studio 2008\\Settings\\CurrentSettings.xml"); if (LFileExists(p)) { LFile f; if (f.Open(p, O_READ)) { LXmlTree t; LXmlTag r; if (t.Read(&r, &f)) { LXmlTag *Opts = r.GetChildTag("ToolsOptions"); if (Opts) { LXmlTag *Projects = NULL; char *Name; for (auto c: Opts->Children) { if (c->IsTag("ToolsOptionsCategory") && (Name = c->GetAttr("Name")) && !stricmp(Name, "Projects")) { Projects = c; break; } } LXmlTag *VCDirectories = NULL; if (Projects) for (auto c: Projects->Children) { if (c->IsTag("ToolsOptionsSubCategory") && (Name = c->GetAttr("Name")) && !stricmp(Name, "VCDirectories")) { VCDirectories = c; break; } } if (VCDirectories) for (auto prop: VCDirectories->Children) { if (prop->IsTag("PropertyValue") && (Name = prop->GetAttr("Name")) && !stricmp(Name, "IncludeDirectories")) { char *Bar = strchr(prop->GetContent(), '|'); LToken t(Bar ? Bar + 1 : prop->GetContent(), ";"); for (int i=0; iSystemIncludePaths.New().Reset(NewStr(s)); } } } } } } } #endif } for (int i=0; iSystemIncludePaths.Length(); i++) { Paths.Add(NewStr(d->SystemIncludePaths[i])); } return true; } /* class SocketTest : public LWindow, public LThread { LTextLog *Log; public: SocketTest() : LThread("SocketTest") { Log = new LTextLog(100); SetPos(LRect(200, 200, 900, 800)); Attach(0); Visible(true); Log->Attach(this); Run(); } int Main() { LSocket s; s.SetTimeout(15000); Log->Print("Starting...\n"); auto r = s.Open("192.168.1.30", 7000); Log->Print("Open =%i\n", r); return 0; } }; */ class TestView : public LView { public: TestView() { LRect r(10, 10, 110, 210); SetPos(r); Sunken(true); printf("_BorderSize=%i\n", _BorderSize); } void OnPaint(LSurface *pdc) { auto c = GetClient(); pdc->Colour(LColour::Red); pdc->Line(c.x1, c.y1, c.x2, c.y2); pdc->Ellipse(c.x1+(c.X()/2)+10, c.y1+(c.Y()/2), c.X()/2, c.Y()/2); } }; #include "lgi/common/Tree.h" #include "lgi/common/List.h" class Test : public LWindow { public: Test() { LRect r(100, 100, 800, 700); SetPos(r); Name("Test"); SetQuitOnClose(true); if (Attach(0)) { // AddView(new TestView); // auto t = new LTree(10, 10, 10, 100, 200); auto t = new LTextLabel(10, 10, 10, 100, 35, "Text"); AddView(t); AttachChildren(); Visible(true); } } }; int LgiMain(OsAppArguments &AppArgs) { printf("LgiIde v%s\n", APP_VER); LApp a(AppArgs, "LgiIde"); if (a.IsOk()) { LPoint dpi = LScreenDpi(); a.AppWnd = new AppWnd; // a.AppWnd->_Dump(); a.Run(); } return 0; } diff --git a/Ide/Code/NewProjectFromTemplate.cpp b/Ide/Code/NewProjectFromTemplate.cpp --- a/Ide/Code/NewProjectFromTemplate.cpp +++ b/Ide/Code/NewProjectFromTemplate.cpp @@ -1,177 +1,177 @@ #include "lgi/common/Lgi.h" #include "lgi/common/Tree.h" #include "lgi/common/SubProcess.h" #include "lgi/common/FileSelect.h" #include "LgiIde.h" #include "resdefs.h" static LString TemplatesPath; class NewProjFromTemplate : public LDialog { public: NewProjFromTemplate(LViewI *parent) { SetParent(parent); if (LoadFromResource(IDD_NEW_PROJ_FROM_TEMPLATE)) { MoveSameScreen(parent); LTree *t; if (TemplatesPath && GetViewById(IDC_TEMPLATES, t)) Add(t, TemplatesPath); } } void Add(LTreeNode *t, const char *path) { LDirectory d; for (auto b=d.First(path); b; b=d.Next()) { auto Full = d.FullPath(); if (d.IsDir()) { LTreeItem *c = new LTreeItem; c->SetText(d.GetName()); c->SetText(Full, 1); t->Insert(c); Add(c, Full); } } } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_BROWSE_OUTPUT: { LFileSelect *s = new LFileSelect; s->Parent(this); s->OpenFolder([&](auto s, auto ok) { if (ok) SetCtrlName(IDC_FOLDER, s->Name()); delete s; }); break; } case IDOK: case IDCANCEL: EndModal(c->GetId() == IDOK); break; } return 0; } }; LString GetPython3() { auto Path = LGetPath(); for (auto i: Path) { LFile::Path p(i); p += #ifdef MAC "python3" #else "python" #endif LGI_EXECUTABLE_EXT; if (p.Exists()) { printf("Got python '%s'\n", p.GetFull().Get()); // But what version is it? LSubProcess sp(p, "--version"); if (sp.Start()) { LStringPipe out; sp.Communicate(&out); - auto s = out.NewGStr(); + auto s = out.NewLStr(); // printf("out=%s\n", s.Get()); if (s.Find(" 3.") >= 0) return p.GetFull(); } } } return LString(); } bool CreateProject(const char *Name, const char *Template, const char *Folder) { auto Py3 = GetPython3(); if (!Py3) { LgiTrace("%s:%i - Can't find python3.\n", _FL); return false; } // Make sure output folder exists? if (!LDirExists(Folder)) { if (!FileDev->CreateFolder(Folder)) { LgiTrace("%s:%i - Create folder '%s' failed.\n", _FL, Folder); return false; } } // Copy in script... auto CreateProjectPy = "create_project.py"; LFile::Path ScriptIn(TemplatesPath); ScriptIn += CreateProjectPy; LFile::Path ScriptOut(Folder); ScriptOut += CreateProjectPy; if (!FileDev->Copy(ScriptIn, ScriptOut)) { LgiTrace("%s:%i - Copy '%s' to '%s' failed.\n", _FL, ScriptIn.GetFull().Get(), ScriptOut.GetFull().Get()); return false; } // Call the script LString Args; Args.Printf("\"%s\" \"%s\" \"%s\"", ScriptOut.GetFull().Get(), Template, Name); LSubProcess p(Py3, Args); if (!p.Start()) { LgiTrace("%s:%i - Start process failed.\n", _FL); return false; } LStringPipe Out; p.Communicate(&Out); - LgiTrace("Out=%s\n", Out.NewGStr().Get()); + LgiTrace("Out=%s\n", Out.NewLStr().Get()); FileDev->Delete(ScriptOut); return true; } void NewProjectFromTemplate(LViewI *parent) { LFile::Path p(LSP_APP_INSTALL); p += #ifdef MAC "../../" #endif "../templates"; TemplatesPath = p; NewProjFromTemplate *Dlg = new NewProjFromTemplate(parent); Dlg->DoModal([&](auto dlg, auto code) { LTree *t; if (!Dlg->GetViewById(IDC_TEMPLATES, t)) { LgiTrace("%s:%i - No tree.\n", _FL); return; } auto sel = t->Selection(); if (!sel) { LgiTrace("%s:%i - No selection.\n", _FL); return; } CreateProject(Dlg->GetCtrlName(IDC_PROJ_NAME), sel->GetText(1), Dlg->GetCtrlName(IDC_FOLDER)); delete Dlg; }); } diff --git a/Ide/Code/ProjectNode.cpp b/Ide/Code/ProjectNode.cpp --- a/Ide/Code/ProjectNode.cpp +++ b/Ide/Code/ProjectNode.cpp @@ -1,1583 +1,1583 @@ #include "lgi/common/Lgi.h" #include "lgi/common/Token.h" #include "lgi/common/Combo.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/TableLayout.h" #include "lgi/common/Menu.h" #include "lgi/common/FileSelect.h" #include "lgi/common/Charset.h" #include "LgiIde.h" #include "IdeProject.h" #include "ProjectNode.h" #include "AddFtpFile.h" #include "WebFldDlg.h" #define DEBUG_SHOW_NODE_COUNTS 0 const char *TypeNames[NodeMax] = { "None", "Folder", "Source", "Header", "Dependancy", "Resources", "Graphic", "Web", "Text", "MakeFile", }; ////////////////////////////////////////////////////////////////////////////////// class FileProps : public LDialog { public: NodeType Type; LString Charset; int Platforms; FileProps(LView *p, char *m, NodeType t, int plat, const char *charset) { Platforms = plat; Type = t; SetParent(p); if (LoadFromResource(IDD_FILE_PROPS)) { MoveToCenter(); SetCtrlName(IDC_MSG, m); LCombo *c; if (GetViewById(IDC_TYPE, c)) { for (int i=NodeNone; iInsert(TypeNames[i]); } c->Value(Type); } else LgiTrace("%s:%i - Failed to get Type combo.\n", _FL); if (GetViewById(IDC_CHARSET, c)) { if (!charset) charset = "utf-8"; for (LCharset *cs = LGetCsList(); cs->Charset; cs++) { c->Insert(cs->Charset); if (!Stricmp(charset, cs->Charset)) c->Value(c->Length()-1); } } for (int i=0; PlatformNames[i]; i++) { SetCtrlValue(PlatformCtrlId[i], ((1 << i) & Platforms) != 0); } OnPosChange(); // Make sure the dialog can display the whole table... LTableLayout *t; if (GetViewById(IDC_TABLE, t)) { LRect u = t->GetUsedArea(); u.Inset(-LTableLayout::CellSpacing, -LTableLayout::CellSpacing); LRect p = GetPos(); if (u.X() < p.X() || u.Y() < p.Y()) { p.SetSize(MAX(u.X(), p.X()), MAX(u.Y(), p.Y())); SetPos(p); } } } } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDOK: { int64 t = GetCtrlValue(IDC_TYPE); if (t >= NodeSrc) { Type = (NodeType) t; } Platforms = 0; for (int i=0; PlatformNames[i]; i++) { if (GetCtrlValue(PlatformCtrlId[i])) { Platforms |= 1 << i; } } Charset = GetCtrlName(IDC_CHARSET); if (!Stricmp(Charset.Get(), "utf-8")) Charset.Empty(); // fall thru } case IDCANCEL: case IDC_COPY_PATH: { EndModal(c->GetId()); break; } } return 0; } }; //////////////////////////////////////////////////////////////////// ProjectNode::ProjectNode(IdeProject *p) : IdeCommon(p) { Platforms = PLATFORM_ALL; NodeId = 0; Type = NodeNone; ChildCount = -1; IgnoreExpand = false; Dep = 0; Tag = NewStr("Node"); } ProjectNode::~ProjectNode() { NeedsPulse(false); if (sFile && Project) Project->OnNode(sFile, this, false); if (Dep) DeleteObj(Dep); } void ProjectNode::NeedsPulse(bool yes) { if (!Project) { LAssert(!"No project."); return; } auto app = Project->GetApp(); if (!app) { LAssert(!"No app."); return; } app->NeedsPulse.Delete(this); if (yes) app->NeedsPulse.Add(this); } void ProjectNode::OpenLocalCache(IdeDoc *&Doc) { if (sLocalCache) { Doc = Project->GetApp()->OpenFile(sLocalCache, this); if (Doc) { Doc->SetProject(Project); IdeProjectSettings *Settings = Project->GetSettings(); Doc->SetEditorParams(Settings->GetInt(ProjEditorIndentSize), Settings->GetInt(ProjEditorTabSize), Settings->GetInt(ProjEditorUseHardTabs), Settings->GetInt(ProjEditorShowWhiteSpace)); } else { LgiMsg(Tree, "Couldn't open file '%s'", AppName, MB_OK, sLocalCache.Get()); } } } int64 ProjectNode::CountNodes() { int64 n = 1; for (auto i:*this) { ProjectNode *c = dynamic_cast(i); if (!c) break; n += c->CountNodes(); } return n; } void ProjectNode::OnCmdComplete(FtpCmd *Cmd) { if (!Cmd) return; if (Cmd->Status && Cmd->File) { if (Cmd->Cmd == FtpRead) { sLocalCache = Cmd->File; IdeDoc *Doc; OpenLocalCache(Doc); } else if (Cmd->Cmd == FtpWrite) { Cmd->View->OnSaveComplete(Cmd->Status); } } } int ProjectNode::GetPlatforms() { return Platforms; } const char *ProjectNode::GetLocalCache() { return sLocalCache; } bool ProjectNode::Load(LDocView *Edit, NodeView *Callback) { bool Status = false; if (IsWeb()) { if (sLocalCache) Status = Edit->Open(sLocalCache); else LAssert(!"No LocalCache"); } else { auto Full = GetFullPath(); Status = Edit->Open(Full, Charset); } return Status; } bool ProjectNode::Save(LDocView *Edit, NodeView *Callback) { bool Status = false; if (IsWeb()) { if (sLocalCache) { if (Edit->Save(sLocalCache)) { FtpThread *t = GetFtpThread(); if (t) { FtpCmd *c = new FtpCmd(FtpWrite, this); if (c) { c->View = Callback; c->Watch = Project->GetApp()->GetFtpLog(); c->Uri = NewStr(sFile); c->File = NewStr(sLocalCache); t->Post(c); } } } else LAssert(!"Editbox save failed."); } else LAssert(!"No LocalCache"); } else { auto f = GetFullPath(); if (Project) Project->CheckExists(f); Status = Edit->Save(f, Charset); if (Callback) Callback->OnSaveComplete(Status); } return Status; } int ProjectNode::GetId() { if (!NodeId && Project) NodeId = Project->AllocateId(); return NodeId; } bool ProjectNode::IsWeb() { char *Www = GetAttr(OPT_Www); char *Ftp = GetAttr(OPT_Ftp); if ( Www || Ftp || (sFile && strnicmp(sFile, "ftp://", 6) == 0) ) return true; return false; } bool ProjectNode::HasNode(ProjectNode *Node) { printf("Has %s %s %p\n", sFile.Get(), sName.Get(), Dep); if (this == Node) return true; if (Dep && Dep->HasNode(Node)) return true; for (auto i:*this) { ProjectNode *c = dynamic_cast(i); if (!c) break; if (c->HasNode(Node)) return true; } return false; } void ProjectNode::AddNodes(LArray &Nodes) { Nodes.Add(this); for (auto i:*this) { ProjectNode *c = dynamic_cast(i); if (!c) break; c->AddNodes(Nodes); } } bool ProjectNode::GetClean() { if (Dep) return Dep->GetClean(); return true; } void ProjectNode::SetClean() { auto CleanProj = [&]() { for (auto i: *this) { ProjectNode *p = dynamic_cast(i); if (p) p->SetClean(); } }; if (Dep) Dep->SetClean([&](bool ok) { if (ok) CleanProj(); }); else CleanProj(); } IdeProject *ProjectNode::GetDep() { return Dep; } IdeProject *ProjectNode::GetProject() { return Project; } bool ProjectNode::GetFormats(LDragFormats &Formats) { Formats.Supports(NODE_DROP_FORMAT); return true; } bool ProjectNode::GetData(LArray &Data) { for (unsigned i=0; iOnNode(sFile, this, false); if (Project->RelativePath(Rel, f, true)) sFile = Rel; else sFile = f; if (sFile) { auto FullPath = GetFullPath(); if (Project) Project->OnNode(FullPath, this, true); AutoDetectType(); } Project->SetDirty(); } char *ProjectNode::GetName() { return sName; } void ProjectNode::SetName(const char *f) { sName = f; Type = NodeDir; } NodeType ProjectNode::GetType() { return Type; } void ProjectNode::SetType(NodeType t) { Type = t; } int ProjectNode::GetImage(int f) { if (IsWeb()) { return sFile ? ICON_SOURCE : ICON_WEB; } switch (Type) { default: break; case NodeDir: return ICON_FOLDER; case NodeSrc: return ICON_SOURCE; case NodeDependancy: return ICON_DEPENDANCY; case NodeGraphic: return ICON_GRAPHIC; case NodeResources: return ICON_RESOURCE; } return ICON_HEADER; } const char *ProjectNode::GetText(int c) { if (sFile) { char *d = 0; if (IsWeb()) { d = sFile ? strrchr(sFile, '/') : 0; } else { #ifdef WIN32 char Other = '/'; #else char Other = '\\'; #endif char *s; while ((s = strchr(sFile, Other))) { *s = DIR_CHAR; } d = strrchr(sFile, DIR_CHAR); } if (d) return d + 1; else return sFile; } #if DEBUG_SHOW_NODE_COUNTS if (Type == NodeDir) { if (ChildCount < 0) ChildCount = CountNodes(); Label.Printf("%s ("LGI_PrintfInt64")", Name, ChildCount); return Label; } #endif return sName ? sName.Get() : Untitled; } void ProjectNode::OnExpand(bool b) { if (!IgnoreExpand) { Project->SetExpanded(GetId(), b); } } bool ProjectNode::Serialize(bool Write) { if (!Write && sFile) Project->OnNode(sFile, this, false); SerializeAttr("File", sFile); SerializeAttr("Name", sName); SerializeAttr("Charset", Charset); SerializeAttr("Type", (int&)Type); SerializeAttr("Platforms", (int&)Platforms); if (!Write && sFile) Project->OnNode(sFile, this, true); if (Type == NodeNone) { AutoDetectType(); } if (Type == NodeDir) { if (Write && !NodeId) GetId(); // Make sure we have an ID SerializeAttr("Id", NodeId); if (Write) Project->SetExpanded(GetId(), Expanded()); else { IgnoreExpand = true; Expanded(Project->GetExpanded(GetId())); IgnoreExpand = false; } } else if (Type == NodeDependancy) { SerializeAttr("LinkAgainst", LinkAgainst); if (!Write) { auto Full = GetFullPath(); Dep = Project->GetApp()->OpenProject(Full, Project, false, true); } } else { #if 0 if (!Write) { // Check that file exists. auto p = GetFullPath(); if (p) { if (LFileExists(p)) { if (!LIsRelativePath(File)) { // Try and fix up any non-relative paths that have crept in... char Rel[MAX_PATH_LEN]; if (Project->RelativePath(Rel, File)) { if (File) Project->OnNode(File, this, false); DeleteArray(File); File = NewStr(Rel); Project->SetDirty(); Project->OnNode(File, this, true); } } } else { // File doesn't exist... has it moved??? LAutoString Path = Project->GetBasePath(); if (Path) { // Find the file. char *d = strrchr(p, DIR_CHAR); LArray Files; LArray Ext; Ext.Add(d ? d + 1 : p.Get()); if (LRecursiveFileSearch(Path, &Ext, &Files)) { if (Files.Length()) { LStringPipe Buf; Buf.Print( "The file:\n" "\n" "\t%s\n" "\n" "doesn't exist. Is this the right file:\n" "\n" "\t%s", p.Get(), Files[0]); - auto Msg = Buf.NewGStr(); + auto Msg = Buf.NewLStr(); if (Msg) { auto a = new LAlert(Project->GetApp(), "Missing File", Msg, "Yes", "No", "Browse..."); a->DoModal([this](auto dlg, auto code) { switch (code) { case 1: // Yes { SetFileName(Files[0]); break; } case 2: // No { break; } case 3: // Browse { LFileSelect s; s.Parent(Project->GetApp()); s.Type("Code", SourcePatterns); if (s.Open()) { SetFileName(s.Name()); } break; } } delete dlg; }); } } else { LStringPipe Buf; Buf.Print( "The file:\n" "\n" "\t%s\n" "\n" "doesn't exist.", p.Get()); - auto Msg = Buf.NewGStr(); + auto Msg = Buf.NewLStr(); if (Msg) { auto a = new LAlert(Project->GetApp(), "Missing File", Msg, "Skip", "Delete", "Browse..."); a->DoModal([this](auto dlg, auto code) { switch (code) { case 1: // Skip { break; } case 2: // Delete { Project->SetDirty(); delete this; return false; break; } case 3: // Browse { auto s = new LFileSelect; s->Parent(Project->GetApp()); s->Type("Code", SourcePatterns); s->Open([this](auto s, auto ok) { if (ok) SetFileName(s->Name()); delete s; }); break; } } delete dlg; }); } } } } else { LgiTrace("%s:%i - Project::GetBasePath failed.\n", _FL); } } } } #endif } return true; } LString ProjectNode::GetFullPath() { LString FullPath; if (LIsRelativePath(sFile)) { // Relative path auto Path = Project->GetBasePath(); if (Path) { char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), Path, sFile); FullPath = p; } } else { // Absolute path FullPath = sFile; } return FullPath; } IdeDoc *ProjectNode::Open() { static bool Processing = false; IdeDoc *Doc = 0; if (Processing) { LAssert(!"Not recursive"); } if (!Processing) { Processing = true; if (IsWeb()) { if (sFile) { if (sLocalCache) { OpenLocalCache(Doc); } else { FtpThread *t = GetFtpThread(); if (t) { FtpCmd *c = new FtpCmd(FtpRead, this); if (c) { c->Watch = Project->GetApp()->GetFtpLog(); c->Uri = NewStr(sFile); t->Post(c); } } } } } else { switch (Type) { case NodeDir: { Expanded(true); break; } case NodeResources: { auto FullPath = GetFullPath(); if (FullPath) { char Exe[MAX_PATH_LEN]; LMakePath(Exe, sizeof(Exe), LGetExePath(), ".."); #if defined WIN32 LMakePath(Exe, sizeof(Exe), Exe, "Debug\\LgiRes.exe"); #elif defined LINUX LMakePath(Exe, sizeof(Exe), Exe, "LgiRes/lgires"); #endif if (LFileExists(Exe)) { LExecute(Exe, FullPath); } } else { LgiMsg(Tree, "No Path", AppName); } break; } case NodeGraphic: { auto FullPath = GetFullPath(); if (FullPath) { LExecute(FullPath); } else { LgiMsg(Tree, "No Path", AppName); } break; } default: { auto FullPath = GetFullPath(); if (Project->CheckExists(FullPath)) { Doc = Project->GetApp()->FindOpenFile(FullPath); if (!Doc) { Doc = Project->GetApp()->NewDocWnd(0, this); if (Doc) { if (Doc->OpenFile(FullPath)) { IdeProjectSettings *Settings = Project->GetSettings(); Doc->SetProject(Project); Doc->SetEditorParams(Settings->GetInt(ProjEditorIndentSize), Settings->GetInt(ProjEditorTabSize), Settings->GetInt(ProjEditorUseHardTabs), Settings->GetInt(ProjEditorShowWhiteSpace)); } else { LgiMsg(Tree, "Couldn't open file '%s'", AppName, MB_OK, FullPath.Get()); } } } else { Doc->Raise(); Doc->Focus(true); } } else { LgiMsg(Tree, "No Path", AppName); } break; } } } Processing = false; } return Doc; } void ProjectNode::Delete() { if (Select()) { LTreeItem *s = GetNext(); if (s || (s = GetParent())) s->Select(true); } if (nView) { nView->OnDelete(); nView = NULL; } Project->SetDirty(); LXmlTag::RemoveTag(); delete this; } bool ProjectNode::OnKey(LKey &k) { if (k.Down()) { if (k.vkey == LK_RETURN && k.IsChar) { Open(); return true; } else if (k.vkey == LK_DELETE) { Delete(); return true; } } return false; } void ProjectNode::OnPulse() { auto StartTs = LCurrentTime(); int TimeSlice = 700; //ms auto Start = strlen(ImportPath) + 1; while (ImportFiles.Length()) { LAutoString f(ImportFiles[0]); // Take ownership of the string ImportFiles.DeleteAt(0); if (ImportProg) (*ImportProg)++; LToken p(f + Start, DIR_STR); ProjectNode *Insert = this; // Find sub nodes, and drill into directory heirarchy, // creating the nodes if needed. for (int i=0; Insert && i(it); if (!c) break; if (c->GetType() == NodeDir && c->GetName() && stricmp(c->GetName(), p[i]) == 0) { Insert = c; Found = true; break; } } if (!Found) { // Create the node IdeCommon *Com = Insert->GetSubFolder(Project, p[i], true); Insert = dynamic_cast(Com); LAssert(Insert); } } // Insert the file into the tree... if (Insert) { ProjectNode *New = new ProjectNode(Project); if (New) { New->SetFileName(f); Insert->InsertTag(New); Insert->SortChildren(); Project->SetDirty(); } } if (LCurrentTime() - StartTs >= TimeSlice) break; // Yeild to the message loop } if (ImportFiles.Length() == 0) NeedsPulse(false); } void ProjectNode::OnMouseClick(LMouse &m) { LTreeItem::OnMouseClick(m); if (m.IsContextMenu()) { LSubMenu Sub; Select(true); if (Type == NodeDir) { if (IsWeb()) { Sub.AppendItem("Insert FTP File", IDM_INSERT_FTP, true); } else { Sub.AppendItem("Insert File", IDM_INSERT, true); } Sub.AppendItem("New Folder", IDM_NEW_FOLDER, true); Sub.AppendItem("Import Folder", IDM_IMPORT_FOLDER, true); Sub.AppendSeparator(); if (!IsWeb()) { Sub.AppendItem("Rename", IDM_RENAME, true); } } Sub.AppendItem("Remove", IDM_DELETE, true); Sub.AppendItem("Sort", IDM_SORT_CHILDREN, true); Sub.AppendSeparator(); Sub.AppendItem("Browse Folder", IDM_BROWSE_FOLDER, ValidStr(sFile)); Sub.AppendItem("Open Terminal", IDM_OPEN_TERM, ValidStr(sFile)); Sub.AppendItem("Properties", IDM_PROPERTIES, true); m.ToScreen(); LPoint c = _ScrollPos(); m.x -= c.x; m.y -= c.y; switch (Sub.Float(Tree, m.x, m.y)) { case IDM_INSERT_FTP: { AddFtpFile *Add = new AddFtpFile(Tree, GetAttr(OPT_Ftp)); Add->DoModal([&](auto dlg, auto code) { if (code) { for (int i=0; iUris.Length(); i++) { ProjectNode *New = new ProjectNode(Project); if (New) { New->SetFileName(Add->Uris[i]); InsertTag(New); SortChildren(); Project->SetDirty(); } } } delete Add; }); break; } case IDM_INSERT: { LFileSelect *s = new LFileSelect; s->Parent(Tree); s->Type("Source", SourcePatterns); s->Type("Makefiles", "*makefile"); s->Type("All Files", LGI_ALL_FILES); s->MultiSelect(true); LAutoString Dir = Project->GetBasePath(); if (Dir) { s->InitialDir(Dir); } s->Open([this](auto s, auto ok) { if (ok) { for (int i=0; iLength(); i++) { if (!Project->InProject(false, (*s)[i], false)) { ProjectNode *New = new ProjectNode(Project); if (New) { New->SetFileName((*s)[i]); InsertTag(New); SortChildren(); Project->SetDirty(); } } else { LgiMsg(Tree, "'%s' is already in the project.\n", AppName, MB_OK, (*s)[i]); } } } delete s; }); break; } case IDM_IMPORT_FOLDER: { LFileSelect *s = new LFileSelect; s->Parent(Tree); auto Dir = Project->GetBasePath(); if (Dir) s->InitialDir(Dir); s->OpenFolder([this](auto s, auto ok) { if (ok) { ImportPath = s->Name(); LArray Ext; LToken e(SourcePatterns, ";"); for (auto i: e) { Ext.Add(i); } if (LRecursiveFileSearch(ImportPath, &Ext, &ImportFiles)) { ImportProg.Reset(new LProgressDlg(GetTree(), 300)); ImportProg->SetDescription("Importing..."); ImportProg->SetRange(ImportFiles.Length()); NeedsPulse(true); } } delete s; }); break; } case IDM_SORT_CHILDREN: { SortChildren(); Project->SetDirty(); break; } case IDM_NEW_FOLDER: { LInput *Name = new LInput(Tree, "", "Name:", AppName); Name->DoModal([&](auto dlg, auto ok) { if (ok) GetSubFolder(Project, Name->GetStr(), true); delete Name; }); break; } case IDM_RENAME: { LInput *Name = new LInput(Tree, GetName(), "Name:", AppName); Name->DoModal([&](auto dlg, auto ok) { if (ok) { SetName(Name->GetStr()); Project->SetDirty(); Update(); } delete Name; }); break; } case IDM_DELETE: { Delete(); return; break; } case IDM_BROWSE_FOLDER: { auto Path = GetFullPath(); if (Path) LBrowseToFile(Path); break; } case IDM_OPEN_TERM: { if (Type == NodeDir) { } else { auto Path = GetFullPath(); if (Path) { LTrimDir(Path); #if defined LINUX char *Term = 0; char *Format = 0; switch (LGetWindowManager()) { case WM_Kde: Term = "konsole"; Format = "--workdir "; break; case WM_Gnome: Term = "gnome-terminal"; Format = "--working-directory="; break; } if (Term) { char s[256]; strcpy_s(s, sizeof(s), Format); char *o = s + strlen(s), *i = Path; *o++ = '\"'; while (*i) { if (*i == ' ') { *o++ = '\\'; } *o++ = *i++; } *o++ = '\"'; *o++ = 0; LExecute(Term, s); } #elif defined WIN32 LExecute("cmd", 0, Path); #endif } } break; } case IDM_PROPERTIES: { OnProperties(); break; } } } else if (m.Left()) { if (Type != NodeDir && m.Double()) { if ( ( IsWeb() || Type != NodeDir ) && ValidStr(sFile) ) { Open(); } else { LAssert(!"Unknown file type"); } } } } #if 0 #define LOG_FIND_FILE(...) printf(__VA_ARGS__) #else #define LOG_FIND_FILE(...) #endif ProjectNode *ProjectNode::FindFile(const char *In, char **Full) { LOG_FIND_FILE("%s:%i Node='%s' '%s'\n", _FL, sFile.Get(), sName.Get()); if (sFile) { bool Match = false; const char *AnyDir = "\\/"; if (IsWeb()) { Match = sFile ? stricmp(In, sFile) == 0 : false; } else if (strchr(In, DIR_CHAR)) { // Match partial or full path char Full[MAX_PATH_LEN] = ""; if (LIsRelativePath(sFile)) { auto Base = Project->GetBasePath(); if (Base) LMakePath(Full, sizeof(Full), Base, sFile); else LgiTrace("%s,%i - Couldn't get full IDoc path.\n", _FL); } else { strcpy_s(Full, sizeof(Full), sFile); } LOG_FIND_FILE("%s:%i Full='%s'\n", _FL, Full); LString MyPath(Full); LString::Array MyArr = MyPath.SplitDelimit(AnyDir); LString InPath(In); LString::Array InArr = InPath.SplitDelimit(AnyDir); auto Common = MIN(MyArr.Length(), InArr.Length()); Match = true; for (int i = 0; i < Common; i++) { char *a = MyArr[MyArr.Length()-(i+1)]; char *b = InArr[InArr.Length()-(i+1)]; auto res = _stricmp(a, b); LOG_FIND_FILE("%s:%i cmp '%s','%s'=%i\n", _FL, a, b, res); if (res) { Match = false; break; } } } else { // Match file name only auto p = sFile.SplitDelimit(AnyDir); Match = p.Last().Equals(In); LOG_FIND_FILE("%s:%i cmp '%s','%s'=%i\n", _FL, p.Last().Get(), In, Match); } if (Match) { if (Full) { if (sFile(0) == '.') { LAutoString Base = Project->GetBasePath(); if (Base) { char f[256]; LMakePath(f, sizeof(f), Base, sFile); *Full = NewStr(f); } } else { *Full = NewStr(sFile); } } return this; } } for (auto i:*this) { ProjectNode *c = dynamic_cast(i); if (!c) { LAssert(!"Not a node?"); break; } ProjectNode *n = c->FindFile(In, Full); if (n) { return n; } } return 0; } struct DepDlg : public LDialog { ProjectNode *Node; DepDlg(ProjectNode *node) : Node(node) { auto proj = node->GetProject(); auto app = proj ? proj->GetApp() : NULL; if (!app) LAssert(!"Can't get app ptr."); else { SetParent(app); MoveSameScreen(app); } if (!LoadFromResource(IDD_DEP_NODE)) LAssert(!"Resource missing."); else { auto Path = node->GetFullPath(); if (Path) SetCtrlName(IDC_PATH, Path); SetCtrlValue(IDC_LINK_AGAINST, node->GetLinkAgainst()); } } int OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDOK: { Node->SetLinkAgainst(GetCtrlValue(IDC_LINK_AGAINST)); break; } } return LDialog::OnNotify(Ctrl, n); } }; void ProjectNode::OnProperties() { if (IsWeb()) { bool IsFolder = sFile.IsEmpty(); WebFldDlg *Dlg = new WebFldDlg(Tree, sName, IsFolder ? GetAttr(OPT_Ftp) : sFile.Get(), GetAttr(OPT_Www)); Dlg->DoModal([&](auto dlg, auto ok) { if (ok) { if (IsFolder) { SetName(Dlg->Name); SetAttr(OPT_Ftp, Dlg->Ftp); SetAttr(OPT_Www, Dlg->Www); } else { sFile = Dlg->Ftp; } Project->SetDirty(); Update(); } delete Dlg; }); } else if (Type == NodeDir) { } else if (Type == NodeDependancy) { auto dlg = new DepDlg(this); dlg->DoModal(NULL); } else { auto Path = GetFullPath(); if (Path) { char Size[32]; int64 FSize = LFileSize(Path); LFormatSize(Size, sizeof(Size), FSize); char Msg[512]; sprintf(Msg, "Source Code:\n\n\t%s\n\nSize: %s (%i bytes)", Path.Get(), Size, (int32)FSize); FileProps *Dlg = new FileProps(Tree, Msg, Type, Platforms, Charset); Dlg->DoModal([this, Dlg, Path](auto dlg, auto code) { switch (code) { case IDOK: { if (Type != Dlg->Type) { Type = Dlg->Type; Project->SetDirty(); } if (Platforms != Dlg->Platforms) { Platforms = Dlg->Platforms; Project->SetDirty(); } if (Charset != Dlg->Charset) { Charset = Dlg->Charset; Project->SetDirty(); } Update(); break; } case IDC_COPY_PATH: { LClipBoard Clip(Tree); Clip.Text(Path); break; } } delete Dlg; }); } } } diff --git a/Lvc/Src/Main.cpp b/Lvc/Src/Main.cpp --- a/Lvc/Src/Main.cpp +++ b/Lvc/Src/Main.cpp @@ -1,1695 +1,1695 @@ #include "lgi/common/Lgi.h" #include "lgi/common/TableLayout.h" #include "lgi/common/TextLog.h" #include "lgi/common/Button.h" #include "lgi/common/XmlTreeUi.h" #include "lgi/common/Tree.h" #include "lgi/common/FileSelect.h" #include "lgi/common/StructuredLog.h" #include "Lvc.h" #include "../Resources/resdefs.h" #ifdef WINDOWS #include "../Resources/resource.h" #endif ////////////////////////////////////////////////////////////////// const char *AppName = "Lvc"; #define DEFAULT_BUILD_FIX_MSG "Build fix." #define OPT_Hosts "Hosts" #define OPT_Host "Host" SshConnection *AppPriv::GetConnection(const char *Uri, bool Create) { LUri u(Uri); u.sPath.Empty(); auto s = u.ToString(); auto Conn = Connections.Find(s); if (!Conn && Create) Connections.Add(s, Conn = new SshConnection(Log, s, "matthew@*$ ")); return Conn; } VersionCtrl AppPriv::DetectVcs(VcFolder *Fld) { char p[MAX_PATH_LEN]; LUri u = Fld->GetUri(); if (!u.IsFile() || !u.sPath) { auto c = GetConnection(u.ToString()); if (!c) return VcNone; auto type = c->Types.Find(u.sPath); if (type) return type; c->DetectVcs(Fld); Fld->GetCss(true)->Color(LColour::Blue); Fld->Update(); return VcPending; } auto Path = u.sPath.Get(); #ifdef WINDOWS if (*Path == '/') Path++; #endif if (LMakePath(p, sizeof(p), Path, ".git") && LDirExists(p)) return VcGit; if (LMakePath(p, sizeof(p), Path, ".svn") && LDirExists(p)) return VcSvn; if (LMakePath(p, sizeof(p), Path, ".hg") && LDirExists(p)) return VcHg; if (LMakePath(p, sizeof(p), Path, "CVS") && LDirExists(p)) return VcCvs; return VcNone; } class DiffView : public LTextLog { public: DiffView(int id) : LTextLog(id) { } void PourStyle(size_t Start, ssize_t Length) { for (auto ln : LTextView3::Line) { if (!ln->c.IsValid()) { char16 *t = Text + ln->Start; if (*t == '+') { ln->c = LColour::Green; ln->Back.Rgb(245, 255, 245); } else if (*t == '-') { ln->c = LColour::Red; ln->Back.Rgb(255, 245, 245); } else if (*t == '@') { ln->c.Rgb(128, 128, 128); ln->Back.Rgb(235, 235, 235); } else ln->c = LColour(L_TEXT); } } } }; class ToolBar : public LLayout, public LResourceLoad { public: ToolBar() { LAutoString Name; LRect Pos; if (LoadFromResource(IDD_TOOLBAR, this, &Pos, &Name)) { OnPosChange(); } else LAssert(!"Missing toolbar resource"); } void OnCreate() { AttachChildren(); } void OnPosChange() { LRect Cli = GetClient(); LTableLayout *v; if (GetViewById(IDC_TABLE, v)) { v->SetPos(Cli); v->OnPosChange(); auto r = v->GetUsedArea(); if (r.Y() <= 1) r.Set(0, 0, 30, 30); // printf("Used = %s\n", r.GetStr()); LCss::Len NewSz(LCss::LenPx, (float)r.Y()+3); auto OldSz = GetCss(true)->Height(); if (OldSz != NewSz) { GetCss(true)->Height(NewSz); SendNotify(LNotifyTableLayoutRefresh); } } else LAssert(!"Missing table ctrl"); } void OnPaint(LSurface *pDC) { pDC->Colour(LColour(L_MED)); pDC->Rectangle(); } }; class CommitCtrls : public LLayout, public LResourceLoad { public: CommitCtrls() { LAutoString Name; LRect Pos; if (LoadFromResource(IDD_COMMIT, this, &Pos, &Name)) { LTableLayout *v; if (GetViewById(IDC_COMMIT_TABLE, v)) { v->GetCss(true)->PaddingRight("8px"); LRect r = v->GetPos(); r.Offset(-r.x1, -r.y1); r.x2++; v->SetPos(r); v->OnPosChange(); r = v->GetUsedArea(); if (r.Y() <= 1) r.Set(0, 0, 30, 30); GetCss(true)->Height(LCss::Len(LCss::LenPx, (float)r.Y())); } else LAssert(!"Missing table ctrl"); } else LAssert(!"Missing toolbar resource"); } void OnPosChange() { LTableLayout *v; if (GetViewById(IDC_COMMIT_TABLE, v)) v->SetPos(GetClient()); } void OnCreate() { AttachChildren(); } }; LString::Array GetProgramsInPath(const char *Program) { LString::Array Bin; LString Prog = Program; #ifdef WINDOWS Prog += LGI_EXECUTABLE_EXT; #endif LString::Array a = LGetPath(); for (auto p : a) { LFile::Path c(p, Prog); if (c.Exists()) Bin.New() = c.GetFull(); } return Bin; } class OptionsDlg : public LDialog, public LXmlTreeUi { LOptionsFile &Opts; public: OptionsDlg(LViewI *Parent, LOptionsFile &opts) : Opts(opts) { SetParent(Parent); Map("svn-path", IDC_SVN, GV_STRING); Map("svn-limit", IDC_SVN_LIMIT); Map("git-path", IDC_GIT, GV_STRING); Map("git-limit", IDC_GIT_LIMIT); Map("hg-path", IDC_HG, GV_STRING); Map("hg-limit", IDC_HG_LIMIT); Map("cvs-path", IDC_CVS, GV_STRING); Map("cvs-limit", IDC_CVS_LIMIT); if (LoadFromResource(IDD_OPTIONS)) { MoveSameScreen(Parent); Convert(&Opts, this, true); } } void Browse(int EditId) { auto s = new LFileSelect; s->Parent(this); s->Open([&](auto dlg, auto status) { if (status) SetCtrlName(EditId, s->Name()); delete dlg; }); } void BrowseFiles(LViewI *Ctrl, const char *Bin, int EditId) { LRect Pos = Ctrl->GetPos(); LPoint Pt(Pos.x1, Pos.y2 + 1); PointToScreen(Pt); LSubMenu s; LString::Array Bins = GetProgramsInPath(Bin); for (unsigned i=0; i= 1000) { LString Bin = Bins[Cmd - 1000]; if (Bin) SetCtrlName(EditId, Bin); } break; } } } int OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDC_SVN_BROWSE: BrowseFiles(Ctrl, "svn", IDC_SVN); break; case IDC_GIT_BROWSE: BrowseFiles(Ctrl, "git", IDC_GIT); break; case IDC_HG_BROWSE: BrowseFiles(Ctrl, "hg", IDC_HG); break; case IDC_CVS_BROWSE: BrowseFiles(Ctrl, "cvs", IDC_CVS); break; case IDOK: Convert(&Opts, this, false); // fall case IDCANCEL: { EndModal(Ctrl->GetId() == IDOK); break; } } return LDialog::OnNotify(Ctrl, n); } }; int CommitDataCmp(VcCommit **_a, VcCommit **_b) { auto a = *_a; auto b = *_b; return a->GetTs().Compare(&b->GetTs()); } LString::Array AppPriv::GetCommitRange() { LString::Array r; if (Commits) { LArray Sel; Commits->GetSelection(Sel); if (Sel.Length() > 1) { Sel.Sort(CommitDataCmp); r.Add(Sel[0]->GetRev()); r.Add(Sel.Last()->GetRev()); } else { r.Add(Sel[0]->GetRev()); } } else LAssert(!"No commit list ptr"); return r; } LArray AppPriv::GetRevs(LString::Array &Revs) { LArray a; for (auto i = Commits->begin(); i != Commits->end(); i++) { VcCommit *c = dynamic_cast(*i); if (c) { for (auto r: Revs) { if (r.Equals(c->GetRev())) { a.Add(c); break; } } } } return a; } class CommitList : public LList { public: CommitList(int id) : LList(id, 0, 0, 200, 200) { } void SelectRevisions(LString::Array &Revs, const char *BranchHint = NULL) { VcCommit *Scroll = NULL; LArray Matches; for (auto i: *this) { VcCommit *item = dynamic_cast(i); if (!item) continue; bool IsMatch = false; for (auto r: Revs) { if (item->IsRev(r)) { IsMatch = true; break; } } if (IsMatch) Matches.Add(item); else if (i->Select()) i->Select(false); } for (auto item: Matches) { auto b = item->GetBranch(); if (BranchHint) { if (!b || Stricmp(b, BranchHint)) continue; } else if (b) { continue; } if (!Scroll) Scroll = item; item->Select(true); } if (!Scroll && Matches.Length() > 0) { Scroll = Matches[0]; Scroll->Select(true); } if (Scroll) Scroll->ScrollTo(); } bool OnKey(LKey &k) { switch (k.c16) { case 'p': case 'P': { if (k.Down()) { LArray Sel; GetSelection(Sel); if (Sel.Length()) { auto first = Sel[0]; auto branch = first->GetBranch(); auto p = first->GetParents(); if (p->Length() == 0) break; for (auto c:Sel) c->Select(false); SelectRevisions(*p, branch); } } return true; } case 'c': case 'C': { if (k.Down()) { LArray Sel; GetSelection(Sel); if (Sel.Length()) { LHashTbl,VcCommit*> Map; for (auto s:Sel) Map.Add(s->GetRev(), s); LString::Array n; for (auto it = begin(); it != end(); it++) { VcCommit *c = dynamic_cast(*it); if (c) { for (auto r:*c->GetParents()) { if (Map.Find(r)) { n.Add(c->GetRev()); break; } } } } for (auto c:Sel) c->Select(false); SelectRevisions(n, Sel[0]->GetBranch()); } } return true; } } return LList::OnKey(k); } }; int LstCmp(LListItem *a, LListItem *b, int Col) { VcCommit *A = dynamic_cast(a); VcCommit *B = dynamic_cast(b); if (A == NULL || B == NULL) { return (A ? 1 : -1) - (B ? 1 : -1); } auto f = A->GetFolder(); auto flds = f->GetFields(); if (!flds.Length()) { LgiTrace("%s:%i - No fields?\n", _FL); return 0; } auto fld = flds[Col]; switch (fld) { case LGraph: case LIndex: case LParents: case LRevision: default: return (int) (B->GetIndex() - A->GetIndex()); case LBranch: case LAuthor: case LMessageTxt: return Stricmp(A->GetFieldText(fld), B->GetFieldText(fld)); case LTimeStamp: return B->GetTs().Compare(&A->GetTs()); } return 0; } struct TestThread : public LThread { public: TestThread() : LThread("test") { Run(); } int Main() { auto Path = LGetPath(); LSubProcess p("python", "/Users/matthew/CodeLib/test.py"); auto t = LString(LGI_PATH_SEPARATOR).Join(Path); for (auto s: Path) printf("s: %s\n", s.Get()); p.SetEnvironment("PATH", t); if (p.Start()) { LStringPipe s; p.Communicate(&s); - printf("Test: %s\n", s.NewGStr().Get()); + printf("Test: %s\n", s.NewLStr().Get()); } return 0; } }; class RemoteFolderDlg : public LDialog { class App *app; LTree *tree; struct SshHost *root, *newhost; LXmlTreeUi Ui; public: LString Uri; RemoteFolderDlg(App *application); ~RemoteFolderDlg(); int OnNotify(LViewI *Ctrl, LNotification n); }; class VcDiffFile : public LTreeItem { AppPriv *d; LString File; public: VcDiffFile(AppPriv *priv, LString file) : d(priv), File(file) { } const char *GetText(int i = 0) override { return i ? NULL : File; } LString StripFirst(LString s) { return s.Replace("\\","/").SplitDelimit("/", 1).Last(); } void Select(bool s) override { LTreeItem::Select(s); if (s) { d->Files->Empty(); d->Diff->Name(NULL); LFile in(File, O_READ); LString s = in.Read(); if (!s) return; LString::Array a = s.Replace("\r").Split("\n"); LArray index; LString Diff, oldName, newName; VcFile *f = NULL; bool InPreamble = false; bool InDiff = false; for (unsigned i=0; iSetDiff(Diff); f->Select(false); } Diff.Empty(); oldName.Empty(); newName.Empty(); InDiff = false; InPreamble = true; } else if (!Strnicmp(Ln, "Index", 5)) { if (InPreamble) index = a[i].SplitDelimit(": ", 1).Slice(1); } else if (!strncmp(Ln, "--- ", 4)) { auto p = a[i].SplitDelimit(" \t", 1); if (p.Length() > 1) oldName = p[1]; } else if (!strncmp(Ln, "+++ ", 4)) { auto p = a[i].SplitDelimit(" \t", 1); if (p.Length() > 1) newName = p[1]; if (oldName && newName) { InDiff = true; InPreamble = false; f = d->FindFile(newName); if (!f) f = new VcFile(d, NULL, NULL, false); const char *nullFn = "dev/null"; if (newName.Find(nullFn) >= 0) { // Delete f->SetText(StripFirst(oldName), COL_FILENAME); f->SetText("D", COL_STATE); } else { f->SetText(StripFirst(newName), COL_FILENAME); if (oldName.Find(nullFn) >= 0) // Add f->SetText("A", COL_STATE); else // Modify f->SetText("M", COL_STATE); } f->GetStatus(); d->Files->Insert(f); } } else if (!_strnicmp(Ln, "------", 6)) { InPreamble = !InPreamble; } else if (!_strnicmp(Ln, "======", 6)) { InPreamble = false; InDiff = true; } else if (InDiff) { if (Diff) Diff += "\n"; Diff += a[i]; } } if (f && Diff) { f->SetDiff(Diff); Diff.Empty(); } } } }; class App : public LWindow, public AppPriv { LAutoPtr ImgLst; LBox *FoldersBox = NULL; bool CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) { if (!Stricmp(MethodName, METHOD_GetContext)) { *ReturnValue = (AppPriv*)this; return true; } return false; } public: App() { LString AppRev; AppRev.Printf("%s v%s", AppName, APP_VERSION); Name(AppRev); LRect r(0, 0, 1400, 800); SetPos(r); MoveToCenter(); SetQuitOnClose(true); Opts.SerializeFile(false); SerializeState(&Opts, "WndPos", true); #ifdef WINDOWS SetIcon(MAKEINTRESOURCEA(IDI_ICON1)); #else SetIcon("icon32.png"); #endif ImgLst.Reset(LLoadImageList("image-list.png", 16, 16)); if (Attach(0)) { if ((Menu = new LMenu)) { Menu->SetPrefAndAboutItems(IDM_OPTIONS, IDM_ABOUT); Menu->Attach(this); Menu->Load(this, "IDM_MENU"); } LBox *ToolsBox = new LBox(IDC_TOOLS_BOX, true, "ToolsBox"); FoldersBox = new LBox(IDC_FOLDERS_BOX, false, "FoldersBox"); LBox *CommitsBox = new LBox(IDC_COMMITS_BOX, true, "CommitsBox"); ToolBar *Tools = new ToolBar; ToolsBox->Attach(this); Tools->Attach(ToolsBox); FoldersBox->Attach(ToolsBox); auto FolderLayout = new LTableLayout(IDC_FOLDER_TBL); auto c = FolderLayout->GetCell(0, 0, true, 2); Tree = new LTree(IDC_TREE, 0, 0, 320, 200); Tree->SetImageList(ImgLst, false); Tree->ShowColumnHeader(true); Tree->AddColumn("Folder", 250); Tree->AddColumn("Counts", 50); c->Add(Tree); c = FolderLayout->GetCell(0, 1); c->Add(new LEdit(IDC_FILTER_FOLDERS, 0, 0, -1, -1)); c = FolderLayout->GetCell(1, 1); c->Add(new LButton(IDC_CLEAR_FILTER_FOLDERS, 0, 0, -1, -1, "x")); FolderLayout->Attach(FoldersBox); CommitsBox->Attach(FoldersBox); auto CommitsLayout = new LTableLayout(IDC_COMMITS_TBL); c = CommitsLayout->GetCell(0, 0, true, 2); Commits = new CommitList(IDC_LIST); c->Add(Commits); c = CommitsLayout->GetCell(0, 1); c->Add(new LEdit(IDC_FILTER_COMMITS, 0, 0, -1, -1)); c = CommitsLayout->GetCell(1, 1); c->Add(new LButton(IDC_CLEAR_FILTER_COMMITS, 0, 0, -1, -1, "x")); CommitsLayout->Attach(CommitsBox); CommitsLayout->GetCss(true)->Height("40%"); LBox *FilesBox = new LBox(IDC_FILES_BOX, false); FilesBox->Attach(CommitsBox); auto FilesLayout = new LTableLayout(IDC_FILES_TBL); c = FilesLayout->GetCell(0, 0, true, 2); Files = new LList(IDC_FILES, 0, 0, 200, 200); Files->AddColumn("[ ]", 30); Files->AddColumn("State", 100); Files->AddColumn("Name", 400); c->Add(Files); c = FilesLayout->GetCell(0, 1); c->Add(new LEdit(IDC_FILTER_FILES, 0, 0, -1, -1)); c = FilesLayout->GetCell(1, 1); c->Add(new LButton(IDC_CLEAR_FILTER_FILES, 0, 0, -1, -1, "x")); FilesLayout->GetCss(true)->Width("35%"); FilesLayout->Attach(FilesBox); LBox *MsgBox = new LBox(IDC_MSG_BOX, true); MsgBox->Attach(FilesBox); CommitCtrls *Commit = new CommitCtrls; Commit->Attach(MsgBox); Commit->GetCss(true)->Height("25%"); if (Commit->GetViewById(IDC_MSG, Msg)) { LTextView3 *Tv = dynamic_cast(Msg); if (Tv) { Tv->Sunken(true); Tv->SetWrapType(TEXTED_WRAP_NONE); } } else LAssert(!"No ctrl?"); Tabs = new LTabView(IDC_TAB_VIEW); Tabs->Attach(MsgBox); const char *Style = "Padding: 0px 8px 8px 0px"; Tabs->GetCss(true)->Parse(Style); LTabPage *p = Tabs->Append("Diff"); p->Append(Diff = new DiffView(IDC_TXT)); // Diff->Sunken(true); Diff->SetWrapType(TEXTED_WRAP_NONE); p = Tabs->Append("Log"); p->Append(Log = new LTextLog(IDC_LOG)); // Log->Sunken(true); Log->SetWrapType(TEXTED_WRAP_NONE); SetCtrlValue(IDC_UPDATE, true); AttachChildren(); Visible(true); } LXmlTag *f = Opts.LockTag(OPT_Folders, _FL); if (!f) { Opts.CreateTag(OPT_Folders); f = Opts.LockTag(OPT_Folders, _FL); } if (f) { bool Req[VcMax] = {0}; for (auto c: f->Children) { if (c->IsTag(OPT_Folder)) { auto f = new VcFolder(this, c); Tree->Insert(f); if (!Req[f->GetType()]) { Req[f->GetType()] = true; f->GetVersion(); } } } Opts.Unlock(); LRect Large(0, 0, 2000, 200); Tree->SetPos(Large); Tree->ResizeColumnsToContent(); LItemColumn *c; int i = 0, px = 0; while ((c = Tree->ColumnAt(i++))) { px += c->Width(); } FoldersBox->Value(MAX(320, px + 20)); // new TestThread(); } SetPulse(200); DropTarget(true); } ~App() { SerializeState(&Opts, "WndPos", false); LXmlTag *f = Opts.LockTag(OPT_Folders, _FL); if (f) { f->EmptyChildren(); for (auto i:*Tree) { VcFolder *vcf = dynamic_cast(i); if (vcf) f->InsertTag(vcf->Save()); } Opts.Unlock(); } Opts.SerializeFile(true); } LMessage::Result OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_RESPONSE: { SshConnection::HandleMsg(Msg); break; } case M_HANDLE_CALLBACK: { LAutoPtr Pc((ProcessCallback*)Msg->A()); if (Pc) Pc->OnComplete(); break; } } return LWindow::OnEvent(Msg); } void OnReceiveFiles(LArray &Files) { for (auto f : Files) { if (LDirExists(f)) OpenLocalFolder(f); } } int OnCommand(int Cmd, int Event, OsView Wnd) { switch (Cmd) { case IDM_PATCH_VIEWER: { OpenPatchViewer(this, &Opts); break; } case IDM_OPEN_LOCAL: { OpenLocalFolder(); break; } case IDM_OPEN_REMOTE: { OpenRemoteFolder(); break; } case IDM_OPEN_DIFF: { auto s = new LFileSelect; s->Parent(this); s->Open([&](auto dlg, auto status) { if (status) OpenDiff(dlg->Name()); delete dlg; }); break; } case IDM_OPTIONS: { auto Dlg = new OptionsDlg(this, Opts); Dlg->DoModal([](auto dlg, auto ctrlId) { delete dlg; }); break; } case IDM_FIND: { auto i = new LInput(this, "", "Search string:"); i->DoModal([this, i](auto dlg, auto ctrlId) { if (ctrlId == IDOK) { LString::Array Revs; Revs.Add(i->GetStr()); CommitList *cl; if (GetViewById(IDC_LIST, cl)) cl->SelectRevisions(Revs); } delete dlg; }); break; } case IDM_UNTRACKED: { auto mi = GetMenu()->FindItem(IDM_UNTRACKED); if (!mi) break; mi->Checked(!mi->Checked()); LArray Flds; Tree->GetSelection(Flds); for (auto f : Flds) { f->Refresh(); } break; } case IDM_REFRESH: { LArray Flds; Tree->GetSelection(Flds); for (auto f: Flds) f->Refresh(); break; } case IDM_PULL: { LArray Flds; Tree->GetSelection(Flds); for (auto f: Flds) f->Pull(); break; } case IDM_PUSH: { LArray Flds; Tree->GetSelection(Flds); for (auto f: Flds) f->Push(); break; } case IDM_STATUS: { LArray Flds; Tree->GetSelection(Flds); for (auto f: Flds) f->FolderStatus(); break; } case IDM_UPDATE_SUBS: { LArray Flds; Tree->GetSelection(Flds); for (auto f: Flds) f->UpdateSubs(); break; break; } case IDM_EXIT: { LCloseApp(); break; } } return 0; } void OnPulse() { for (auto i:*Tree) { VcFolder *vcf = dynamic_cast(i); if (vcf) vcf->OnPulse(); } } void OpenLocalFolder(const char *Fld = NULL) { auto Load = [&](const char *Fld) { // Check the folder isn't already loaded... bool Has = false; LArray Folders; Tree->GetAll(Folders); for (auto f: Folders) { if (f->GetUri().IsFile() && !Stricmp(f->LocalPath(), Fld)) { Has = true; break; } } if (!Has) { LUri u; u.SetFile(Fld); Tree->Insert(new VcFolder(this, u.ToString())); } }; if (!Fld) { auto s = new LFileSelect; s->Parent(this); s->OpenFolder([&](auto dlg, auto status) { if (status) Load(dlg->Name()); delete dlg; }); } else Load(Fld); } void OpenRemoteFolder() { auto Dlg = new RemoteFolderDlg(this); Dlg->DoModal([this, Dlg](auto dlg, auto status) { if (status) Tree->Insert(new VcFolder(this, Dlg->Uri)); delete dlg; }); } void OpenDiff(const char *File) { Tree->Insert(new VcDiffFile(this, File)); } void OnFilterFolders() { if (!Tree) return; for (auto i = Tree->GetChild(); i; i = i->GetNext()) { auto n = i->GetText(); bool vis = !FolderFilter || Stristr(n, FolderFilter.Get()) != NULL; i->GetCss(true)->Display(vis ? LCss::DispBlock : LCss::DispNone); } Tree->UpdateAllItems(); Tree->Invalidate(); } void OnFilterCommits() { if (!Commits) return; LArray a; if (!Commits->GetAll(a)) return; auto cols = Commits->GetColumns(); for (auto i: a) { bool vis = !CommitFilter; for (int c=1; !vis && cGetText(c); if (Stristr(txt, CommitFilter.Get())) vis = true; } i->GetCss(true)->Display(vis ? LCss::DispBlock : LCss::DispNone); } Commits->UpdateAllItems(); Commits->Invalidate(); } void OnFilterFiles() { VcFolder *f = dynamic_cast(Tree->Selection()); if (f) f->FilterCurrentFiles(); } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_CLEAR_FILTER_FOLDERS: { SetCtrlName(IDC_FILTER_FOLDERS, NULL); // Fall through } case IDC_FILTER_FOLDERS: { if (n.Type == LNotifyEscapeKey) SetCtrlName(IDC_FILTER_FOLDERS, NULL); LString n = GetCtrlName(IDC_FILTER_FOLDERS); if (n != FolderFilter) { FolderFilter = n; OnFilterFolders(); } break; } case IDC_CLEAR_FILTER_COMMITS: { SetCtrlName(IDC_FILTER_COMMITS, NULL); // Fall through } case IDC_FILTER_COMMITS: { if (n.Type == LNotifyEscapeKey) SetCtrlName(IDC_FILTER_COMMITS, NULL); LString n = GetCtrlName(IDC_FILTER_COMMITS); if (n != CommitFilter) { CommitFilter = n; OnFilterCommits(); } break; } case IDC_CLEAR_FILTER_FILES: { SetCtrlName(IDC_FILTER_FILES, NULL); // Fall through } case IDC_FILTER_FILES: { if (n.Type == LNotifyEscapeKey) SetCtrlName(IDC_FILTER_FILES, NULL); LString n = GetCtrlName(IDC_FILTER_FILES); if (n != FileFilter) { FileFilter = n; OnFilterFiles(); } break; } case IDC_FILES: { switch (n.Type) { case LNotifyItemColumnClicked: { int Col = -1; LMouse m; if (Files->GetColumnClickInfo(Col, m)) { if (Col == 0) { // Select / deselect all check boxes.. List n; if (Files->GetAll(n)) { bool Checked = false; for (auto f: n) Checked |= f->Checked() > 0; for (auto f: n) f->Checked(Checked ? 0 : 1); } } } break; } default: break; } break; } case IDC_OPEN: { OpenLocalFolder(); break; } case IDC_TREE: { switch (n.Type) { case LNotifyContainerClick: { LMouse m; c->GetMouse(m); if (m.Right()) { LSubMenu s; s.AppendItem("Add Local", IDM_ADD_LOCAL); s.AppendItem("Add Remote", IDM_ADD_REMOTE); int Cmd = s.Float(c->GetGView(), m); switch (Cmd) { case IDM_ADD_LOCAL: { OpenLocalFolder(); break; } case IDM_ADD_REMOTE: { OpenRemoteFolder(); break; } } } break; } case (LNotifyType)LvcCommandStart: { SetCtrlEnabled(IDC_PUSH, false); SetCtrlEnabled(IDC_PULL, false); SetCtrlEnabled(IDC_PULL_ALL, false); break; } case (LNotifyType)LvcCommandEnd: { SetCtrlEnabled(IDC_PUSH, true); SetCtrlEnabled(IDC_PULL, true); SetCtrlEnabled(IDC_PULL_ALL, true); break; } default: break; } break; } case IDC_COMMIT_AND_PUSH: case IDC_COMMIT: { auto BuildFix = GetCtrlValue(IDC_BUILD_FIX); const char *Msg = GetCtrlName(IDC_MSG); if (BuildFix || ValidStr(Msg)) { auto Sel = Tree->Selection(); if (Sel) { VcFolder *f = dynamic_cast(Sel); if (!f) { for (auto p = Sel->GetParent(); p; p = p->GetParent()) { f = dynamic_cast(p); if (f) break; } } if (f) { auto Branch = GetCtrlName(IDC_BRANCH); bool AndPush = c->GetId() == IDC_COMMIT_AND_PUSH; f->Commit(BuildFix ? DEFAULT_BUILD_FIX_MSG : Msg, ValidStr(Branch) ? Branch : NULL, AndPush); } } } else LgiMsg(this, "No message for commit.", AppName); break; } case IDC_PUSH: { VcFolder *f = dynamic_cast(Tree->Selection()); if (f) f->Push(); break; } case IDC_PULL: { VcFolder *f = dynamic_cast(Tree->Selection()); if (f) f->Pull(); break; } case IDC_PULL_ALL: { LArray Folders; Tree->GetAll(Folders); bool AndUpdate = GetCtrlValue(IDC_UPDATE) != 0; for (auto f : Folders) { f->Pull(AndUpdate, LogSilo); } break; } case IDC_STATUS: { LArray Folders; Tree->GetAll(Folders); for (auto f : Folders) { f->FolderStatus(); } break; } case IDC_HEADS: { if (n.Type == LNotifyValueChanged) { auto Revs = LString(c->Name()).SplitDelimit(); CommitList *cl; if (GetViewById(IDC_LIST, cl)) cl->SelectRevisions(Revs); } break; } case IDC_LIST: { switch (n.Type) { case LNotifyItemColumnClicked: { int Col = -1; LMouse Ms; Commits->GetColumnClickInfo(Col, Ms); Commits->Sort(LstCmp, Col); break; } case LNotifyItemDoubleClick: { VcFolder *f = dynamic_cast(Tree->Selection()); if (!f) break; LArray s; if (Commits->GetSelection(s) && s.Length() == 1) f->OnUpdate(s[0]->GetRev()); break; } default: break; } break; } } return 0; } }; struct SshHost : public LTreeItem { LXmlTag *t; LString Host, User, Pass; SshHost(LXmlTag *tag = NULL) { t = tag; if (t) { Serialize(false); SetText(Host); } } void Serialize(bool WriteToTag) { if (WriteToTag) { LUri u; u.sProtocol = "ssh"; u.sHost = Host; u.sUser = User; u.sPass = Pass; t->SetContent(u.ToString()); } else { LUri u(t->GetContent()); if (!Stricmp(u.sProtocol.Get(), "ssh")) { Host = u.sHost; User = u.sUser; Pass = u.sPass; } } } }; RemoteFolderDlg::RemoteFolderDlg(App *application) : app(application), root(NULL), newhost(NULL), tree(NULL) { SetParent(app); LoadFromResource(IDD_REMOTE_FOLDER); if (GetViewById(IDC_HOSTS, tree)) { printf("tree=%p\n", tree); tree->Insert(root = new SshHost()); root->SetText("Ssh Hosts"); } else return; LViewI *v; if (GetViewById(IDC_HOSTNAME, v)) v->Focus(true); Ui.Map("Host", IDC_HOSTNAME); Ui.Map("User", IDC_USER); Ui.Map("Password", IDC_PASS); LXmlTag *hosts = app->Opts.LockTag(OPT_Hosts, _FL); if (hosts) { SshHost *h; for (auto c: hosts->Children) if (c->IsTag(OPT_Host) && (h = new SshHost(c))) root->Insert(h); app->Opts.Unlock(); } root->Insert(newhost = new SshHost()); newhost->SetText("New Host"); root->Expanded(true); newhost->Select(true); } RemoteFolderDlg::~RemoteFolderDlg() { } int RemoteFolderDlg::OnNotify(LViewI *Ctrl, LNotification n) { SshHost *cur = tree ? dynamic_cast(tree->Selection()) : NULL; #define CHECK_SPECIAL() \ if (cur == newhost) \ { \ root->Insert(cur = new SshHost()); \ cur->Select(true); \ } \ if (cur == root) \ break; switch (Ctrl->GetId()) { case IDC_HOSTS: { switch (n.Type) { case LNotifyItemSelect: { bool isRoot = cur == root; SetCtrlEnabled(IDC_HOSTNAME, !isRoot); SetCtrlEnabled(IDC_USER, !isRoot); SetCtrlEnabled(IDC_PASS, !isRoot); SetCtrlEnabled(IDC_DELETE, !isRoot && !(cur == newhost)); SetCtrlName(IDC_HOSTNAME, cur ? cur->Host : NULL); SetCtrlName(IDC_USER, cur ? cur->User : NULL); SetCtrlName(IDC_PASS, cur ? cur->Pass : NULL); break; } default: break; } break; } case IDC_HOSTNAME: { CHECK_SPECIAL() if (cur) { cur->Host = Ctrl->Name(); cur->SetText(cur->Host ? cur->Host : ""); } break; } case IDC_DELETE: { auto sel = tree ? dynamic_cast(tree->Selection()) : NULL; if (!sel) break; LXmlTag *hosts = app->Opts.LockTag(OPT_Hosts, _FL); if (!hosts) { LAssert(!"Couldn't lock tag."); break; } if (hosts->Children.HasItem(sel->t)) { sel->t->RemoveTag(); DeleteObj(sel->t); delete sel; } app->Opts.Unlock(); break; } case IDC_USER: { CHECK_SPECIAL() if (cur) cur->User = Ctrl->Name(); break; } case IDC_PASS: { CHECK_SPECIAL() if (cur) cur->Pass = Ctrl->Name(); break; } case IDOK: { LXmlTag *hosts; if (!(hosts = app->Opts.LockTag(OPT_Hosts, _FL))) { if (!(app->Opts.CreateTag(OPT_Hosts) && (hosts = app->Opts.LockTag(OPT_Hosts, _FL)))) break; } LAssert(hosts != NULL); for (auto i = root->GetChild(); i; i = i->GetNext()) { SshHost *h = dynamic_cast(i); if (!h || h == newhost) continue; if (h->t) ; else if ((h->t = new LXmlTag(OPT_Host))) hosts->InsertTag(cur->t); else return false; h->Serialize(true); } app->Opts.Unlock(); LUri u; u.sProtocol = "ssh"; u.sHost = GetCtrlName(IDC_HOSTNAME); u.sUser = GetCtrlName(IDC_USER); u.sPass = GetCtrlName(IDC_PASS); u.sPath = GetCtrlName(IDC_REMOTE_PATH); Uri = u.ToString(); // Fall through } case IDCANCEL: { EndModal(Ctrl->GetId() == IDOK); break; } } return 0; } ////////////////////////////////////////////////////////////////// int LgiMain(OsAppArguments &AppArgs) { LApp a(AppArgs, AppName); if (a.IsOk()) { // LStructuredLog::UnitTest(); a.AppWnd = new App; a.Run(); } LAssert(VcCommit::Instances == 0); return 0; } diff --git a/Lvc/Src/SshConnection.cpp b/Lvc/Src/SshConnection.cpp --- a/Lvc/Src/SshConnection.cpp +++ b/Lvc/Src/SshConnection.cpp @@ -1,475 +1,475 @@ #include "lgi/common/Lgi.h" #include "Lvc.h" #include "SshConnection.h" #define TIMEOUT_PROMPT 1000 #define PROFILE_WaitPrompt 0 #define PROFILE_OnEvent 0 #define DEBUG_SSH_LOGGING 0 #if DEBUG_SSH_LOGGING #define SSH_LOG(...) d->sLog.Log(__VA_ARGS__) #else #define SSH_LOG(...) #endif ////////////////////////////////////////////////////////////////// SshConnection::SshConnection(LTextLog *log, const char *uri, const char *prompt) : LSsh(log), LEventTargetThread("SshConnection") { auto Wnd = log->GetWindow(); GuiHnd = Wnd->AddDispatch(); Prompt = prompt; Host.Set(Uri = uri); d = NULL; LVariant Ret; LArray Args; if (Wnd->CallMethod(METHOD_GetContext, &Ret, Args)) { if (Ret.Type == GV_VOID_PTR) d = (AppPriv*) Ret.Value.Ptr; } } bool SshConnection::DetectVcs(VcFolder *Fld) { LAutoPtr p(new LString(Fld->GetUri().sPath)); TypeNotify.Add(Fld); return PostObject(GetHandle(), M_DETECT_VCS, p); } bool SshConnection::Command(VcFolder *Fld, LString Exe, LString Args, ParseFn Parser, ParseParams *Params) { if (!Fld || !Exe || !Parser) return false; LAutoPtr p(new SshParams(this)); p->f = Fld; p->Exe = Exe; p->Args = Args; p->Parser = Parser; p->Params = Params; p->Path = Fld->GetUri().sPath; return PostObject(GetHandle(), M_RUN_CMD, p); } LStream *SshConnection::GetConsole() { if (!Connected) { auto r = Open(Host.sHost, Host.sUser, Host.sPass, true); Log->Print("Ssh: %s open: %i\n", Host.sHost.Get(), r); } if (Connected && !c) { c = CreateConsole(); WaitPrompt(c); } return c; } class ProgressListItem : public LListItem { int64_t v, maximum; public: ProgressListItem(int64_t mx = 100) : maximum(mx) { v = 0; } int64_t Value() { return v; } void Value(int64_t val) { v = val; Update(); } void OnPaint(LItem::ItemPaintCtx &Ctx) { auto pDC = Ctx.pDC; pDC->Colour(Ctx.Back); pDC->Rectangle(&Ctx); auto Fnt = GetList()->GetFont(); LDisplayString ds(Fnt, LFormatSize(v)); Fnt->Transparent(true); Fnt->Colour(Ctx.Fore, Ctx.Back); ds.Draw(pDC, Ctx.x1 + 10, Ctx.y1 + ((Ctx.Y() - ds.Y()) >> 1)); pDC->Colour(LProgressView::cNormal); int x1 = 120; int prog = Ctx.X() - x1; int x2 = (int) (v * prog / maximum); pDC->Rectangle(Ctx.x1 + x1, Ctx.y1 + 1, Ctx.x1 + x1 + x2, Ctx.y2 - 1); } }; #if PROFILE_WaitPrompt #define PROFILE(name) prof.Add(name) #else #define PROFILE(name) #endif LString LastLine(LString &input) { #define Ws(ch) ( ((ch) == '\r') || ((ch) == '\n') || ((ch) == '\b') ) char *e = input.Get() + input.Length(); while (e > input.Get() && Ws(e[-1])) e--; char *s = e; while (s > input.Get() && !Ws(s[-1])) s--; return LString(s, e - s); } LString LastLine(LStringPipe &input) { #define Ws(ch) ( ((ch) == '\r') || ((ch) == '\n') || ((ch) == '\b') ) LString s, ln; input.Iterate([&s, &ln](auto ptr, auto bytes) { s = LString((char*)ptr, bytes) + s; auto end = s.Get() + s.Length(); for (auto p = end - 1; p >= s.Get(); p--) { if (Ws(*p)) { while (p < end && (*p == 0 || Ws(*p))) p++; ln = p; break; } } return ln.Get() == NULL; }, true); // if (!ln.Get()) // ln = s; LAssert(ln.Find("\n") < 0); DeEscape(ln); return ln; } bool SshConnection::WaitPrompt(LStream *con, LString *Data, const char *Debug) { LStringPipe out(4 << 10); auto Ts = LCurrentTime(); auto LastReadTs = Ts; ProgressListItem *Prog = NULL; #if PROFILE_WaitPrompt LProfile prof("WaitPrompt", 100); #endif size_t BytesRead = 0; bool CheckLast = true; while (!LSsh::Cancel->IsCancelled()) { PROFILE("read"); auto buf = out.GetBuffer(); if (!buf.ptr) { LAssert(!"Alloc failed."); LgiTrace("WaitPrompt.%s alloc failed.\n", Debug); return false; } auto rd = con->Read(buf.ptr, buf.len); if (rd < 0) { // Error case if (Debug) LgiTrace("WaitPrompt.%s rd=%i\n", Debug, rd); return false; } if (rd > 0) { // Got some data... keep asking for more: LString tmp((char*)buf.ptr, rd); SSH_LOG("waitPrompt data:", rd, tmp); BytesRead += rd; buf.Commit(rd); CheckLast = true; LastReadTs = LCurrentTime(); continue; } if (LCurrentTime() - LastReadTs > 4000) { auto sz = out.GetSize(); SSH_LOG("waitPrompt out:", sz, out); auto last = LastLine(out); // Does the buffer end with a ':' on a line by itself? // Various version control CLI's do that to pageinate data. // Obviously we're not going to deal with that directly, // but the developer will need to know that's happened. if (out.GetSize() > 2) { auto last = LastLine(out); if (last == ":") return false; } } if (!CheckLast) { // We've already checked the buffer for the prompt... LSleep(10); // Don't use too much CPU continue; } PROFILE("LastLine"); CheckLast = false; auto last = LastLine(out); LgiTrace("last='%s'\n", last.Get()); PROFILE("matchstr"); auto result = MatchStr(Prompt, last); SSH_LOG("waitPrompt result:", result, Prompt, last); if (Debug) { LgiTrace("WaitPrompt.%s match='%s' with '%s' = %i\n", Debug, Prompt.Get(), last.Get(), result); } if (result) { if (Data) { PROFILE("data process"); - auto response = out.NewGStr(); + auto response = out.NewLStr(); if (response) { DeEscape(response); // Strip first line off the start.. it's the command... // And the last line... it's the prompt auto start = response.Get(); auto end = response.Get() + response.Length(); while (start < end && *start != '\n') start++; while (start < end && (*start == '\n' || *start == 0)) start++; while (end > start && end[-1] != '\n') end--; Data->Set(start, end - start); } SSH_LOG("waitPrompt data:", *Data); } if (Debug) LgiTrace("WaitPrompt.%s Prompt data=%i\n", Debug, Data?(int)Data->Length():0); break; } auto Now = LCurrentTime(); if (Now - Ts >= TIMEOUT_PROMPT) { if (!Prog && d->Commits) { Prog = new ProgressListItem(1 << 20); d->Commits->Insert(Prog); } if (Prog) Prog->Value(out.GetSize()); Log->Print("...reading: %s\n", LFormatSize(BytesRead).Get()); BytesRead = 0; Ts = Now; } } DeleteObj(Prog); return true; } bool SshConnection::HandleMsg(LMessage *m) { if (m->Msg() != M_RESPONSE) return false; LAutoPtr u((SshParams*)m->A()); if (!u || !u->c) return false; SshConnection &c = *u->c; AppPriv *d = c.d; if (!d) return false; if (u->Vcs) // Check the VCS type.. { c.Types.Add(u->Path, u->Vcs); for (auto f: c.TypeNotify) { if (d->Tree->HasItem(f)) f->OnVcsType(u->Output); else LgiTrace("%s:%i - Folder no longer in tree (recently deleted?).\n", _FL); } } else { if (d && d->Tree->HasItem(u->f)) u->f->OnSshCmd(u); else LgiTrace("%s:%i - Folder no longer in tree (recently deleted?).\n", _FL); } return true; } LString PathFilter(LString s) { auto parts = s.SplitDelimit("/"); if ( (parts[0].Equals("~") || parts[0].Equals(".")) && s(0) == '/' ) { return s(1, -1).Replace(" ", "\\ "); } return s.Replace(" ", "\\ "); } #if PROFILE_OnEvent #define PROF(name) prof.Add(name) #else #define PROF(name) #endif LMessage::Result SshConnection::OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_DETECT_VCS: { LAutoPtr p; if (!ReceiveA(p, Msg)) { LAssert(!"Incorrect param."); break; } LAutoPtr r(new SshParams(this)); LString ls, out; LString::Array lines; VersionCtrl Vcs = VcNone; LString path = PathFilter(*p); LStream *con = GetConsole(); if (!con) { r->Output = "Error: Failed to get console."; r->Vcs = VcError; } else { ls.Printf("find %s -maxdepth 1 -printf \"%%f\n\"\n", path.Get()); SSH_LOG("detectVcs:", ls); con->Write(ls, ls.Length()); auto pr = WaitPrompt(con, &out); lines = out.SplitDelimit("\r\n"); for (auto ln: lines) { if (ln.Equals(".svn")) Vcs = VcSvn; else if (ln.Equals("CVS")) Vcs = VcCvs; else if (ln.Equals(".hg")) Vcs = VcHg; else if (ln.Equals(".git")) Vcs = VcGit; } } r->Path = *p; printf("r->Output=%s\n", r->Output.Get()); if (Vcs == VcError) ; else if (Vcs != VcNone) { r->Vcs = Vcs; r->ExitCode = 0; } else { r->Vcs = VcError; r->Output.Printf("Error: no VCS detected.\n%s\n%s", ls.Get(), lines.Length() ? lines.Last().Get() : "#nodata"); } PostObject(GuiHnd, M_RESPONSE, r); break; } case M_RUN_CMD: { #if PROFILE_OnEvent LProfile prof("OnEvent"); #endif LAutoPtr p; if (!ReceiveA(p, Msg)) break; PROF("get console"); LString path = PathFilter(p->Path); LStream *con = GetConsole(); if (!con) break; auto Debug = p->Params && p->Params->Debug; PROF("cd"); LString cmd; cmd.Printf("cd %s\n", path.Get()); SSH_LOG(">>>> cd:", path); auto wr = con->Write(cmd, cmd.Length()); PROF("cd wait"); auto pr = WaitPrompt(con, NULL, Debug?"Cd":NULL); PROF("cmd"); cmd.Printf("%s %s\n", p->Exe.Get(), p->Args.Get()); SSH_LOG(">>>> cmd:", cmd); if (Log) Log->Print("%s", cmd.Get()); wr = con->Write(cmd, cmd.Length()); PROF("cmd wait"); pr = WaitPrompt(con, &p->Output, Debug?"Cmd":NULL); PROF("result"); LString result; cmd = "echo $?\n"; SSH_LOG(">>>> result:", cmd); wr = con->Write(cmd, cmd.Length()); PROF("result wait"); pr = WaitPrompt(con, &result, Debug?"Echo":NULL); if (pr) { p->ExitCode = (int)result.Int(); if (Log) Log->Print("... result=%i\n", p->ExitCode); } else if (Log) Log->Print("... result=failed\n"); PostObject(GuiHnd, M_RESPONSE, p); break; } default: { LAssert(!"Unhandled msg."); break; } } return 0; } diff --git a/Lvc/Src/VcFolder.cpp b/Lvc/Src/VcFolder.cpp --- a/Lvc/Src/VcFolder.cpp +++ b/Lvc/Src/VcFolder.cpp @@ -1,4475 +1,4475 @@ #include "Lvc.h" #include "../Resources/resdefs.h" #include "lgi/common/Combo.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/Json.h" #include "lgi/common/ProgressDlg.h" #ifndef CALL_MEMBER_FN #define CALL_MEMBER_FN(object,ptrToMember) ((object).*(ptrToMember)) #endif #define MAX_AUTO_RESIZE_ITEMS 2000 #define PROFILE_FN 0 #if PROFILE_FN #define PROF(s) Prof.Add(s) #else #define PROF(s) #endif class TmpFile : public LFile { int Status; LString Hint; public: TmpFile(const char *hint = NULL) { Status = 0; if (hint) Hint = hint; else Hint = "_lvc"; } LFile &Create() { LFile::Path p(LSP_TEMP); p += Hint; do { char s[256]; sprintf_s(s, sizeof(s), "../%s%i.tmp", Hint.Get(), LRand()); p += s; } while (p.Exists()); Status = LFile::Open(p.GetFull(), O_READWRITE); return *this; } }; bool TerminalAt(LString Path) { #if defined(MAC) return LExecute("/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal", Path); #elif defined(WINDOWS) TCHAR w[MAX_PATH_LEN]; auto r = GetWindowsDirectory(w, CountOf(w)); if (r > 0) { LFile::Path p = LString(w); p += "system32\\cmd.exe"; FileDev->SetCurrentFolder(Path); return LExecute(p); } #elif defined(LINUX) LExecute("gnome-terminal", NULL, Path); #endif return false; } int Ver2Int(LString v) { auto p = v.Split("."); int i = 0; for (auto s : p) { auto Int = s.Int(); if (Int < 256) { i <<= 8; i |= (uint8_t)Int; } else { LAssert(0); return 0; } } return i; } int ToolVersion[VcMax] = {0}; #define DEBUG_READER_THREAD 0 #if DEBUG_READER_THREAD #define LOG_READER(...) printf(__VA_ARGS__) #else #define LOG_READER(...) #endif ReaderThread::ReaderThread(VersionCtrl vcs, LAutoPtr p, LStream *out) : LThread("ReaderThread") { Vcs = vcs; Process = p; Out = out; Result = -1; FilterCount = 0; // We don't start this thread immediately... because the number of threads is scaled to the system // resources, particularly CPU cores. } ReaderThread::~ReaderThread() { Out = NULL; while (!IsExited()) LSleep(1); } const char *HgFilter = "We\'re removing Mercurial support"; const char *CvsKill = "No such file or directory"; int ReaderThread::OnLine(char *s, ssize_t len) { switch (Vcs) { case VcHg: { if (strnistr(s, HgFilter, len)) FilterCount = 4; if (FilterCount > 0) { FilterCount--; return 0; } else if (LString(s, len).Strip().Equals("remote:")) { return 0; } break; } case VcCvs: { if (strnistr(s, CvsKill, len)) return -1; break; } default: break; } return 1; } bool ReaderThread::OnData(char *Buf, ssize_t &r) { LOG_READER("OnData %i\n", (int)r); #if 1 char *Start = Buf; for (char *c = Buf; c < Buf + r;) { bool nl = *c == '\n'; c++; if (nl) { int Result = OnLine(Start, c - Start); if (Result < 0) { // Kill process and exit thread. Process->Kill(); return false; } if (Result == 0) { ssize_t LineLen = c - Start; ssize_t NextLine = c - Buf; ssize_t Remain = r - NextLine; if (Remain > 0) memmove(Start, Buf + NextLine, Remain); r -= LineLen; c = Start; } else Start = c; } } #endif Out->Write(Buf, r); return true; } int ReaderThread::Main() { bool b = Process->Start(true, false); if (!b) { LString s("Process->Start failed.\n"); Out->Write(s.Get(), s.Length(), ErrSubProcessFailed); return ErrSubProcessFailed; } char Buf[1024]; ssize_t r; LOG_READER("%s:%i - starting reader loop, pid=%i\n", _FL, Process->Handle()); while (Process->IsRunning()) { if (Out) { LOG_READER("%s:%i - starting read.\n", _FL); r = Process->Read(Buf, sizeof(Buf)); LOG_READER("%s:%i - read=%i.\n", _FL, (int)r); if (r > 0) { if (!OnData(Buf, r)) return -1; } } else { Process->Kill(); return -1; break; } } LOG_READER("%s:%i - process loop done.\n", _FL); if (Out) { while ((r = Process->Read(Buf, sizeof(Buf))) > 0) OnData(Buf, r); } LOG_READER("%s:%i - loop done.\n", _FL); Result = (int) Process->GetExitValue(); #if _DEBUG if (Result) printf("%s:%i - Process err: %i 0x%x\n", _FL, Result, Result); #endif return Result; } ///////////////////////////////////////////////////////////////////////////////////////////// int VcFolder::CmdMaxThreads = 0; int VcFolder::CmdActiveThreads = 0; void VcFolder::Init(AppPriv *priv) { if (!CmdMaxThreads) CmdMaxThreads = LAppInst->GetCpuCount(); d = priv; IsCommit = false; IsLogging = false; IsUpdate = false; IsFilesCmd = false; IsWorkingFld = false; CommitListDirty = false; IsUpdatingCounts = false; IsBranches = StatusNone; IsIdent = StatusNone; Unpushed = Unpulled = -1; Type = VcNone; CmdErrors = 0; CurrentCommitIdx = -1; Expanded(false); Insert(Tmp = new LTreeItem); Tmp->SetText("Loading..."); LAssert(d != NULL); } VcFolder::VcFolder(AppPriv *priv, const char *uri) { Init(priv); Uri.Set(uri); GetType(); } VcFolder::VcFolder(AppPriv *priv, LXmlTag *t) { Init(priv); Serialize(t, false); } VcFolder::~VcFolder() { Log.DeleteObjects(); } VersionCtrl VcFolder::GetType() { if (Type == VcNone) Type = d->DetectVcs(this); return Type; } const char *VcFolder::LocalPath() { if (!Uri.IsProtocol("file") || Uri.sPath.IsEmpty()) { LAssert(!"Shouldn't call this if not a file path."); return NULL; } auto c = Uri.sPath.Get(); #ifdef WINDOWS if (*c == '/') c++; #endif return c; } const char *VcFolder::GetText(int Col) { switch (Col) { case 0: { if (Uri.IsFile()) Cache = LocalPath(); else Cache.Printf("%s%s", Uri.sHost.Get(), Uri.sPath.Get()); if (Cmds.Length()) Cache += " (...)"; return Cache; } case 1: { CountCache.Printf("%i/%i", Unpulled, Unpushed); CountCache = CountCache.Replace("-1", "--"); return CountCache; } } return NULL; } bool VcFolder::Serialize(LXmlTag *t, bool Write) { if (Write) t->SetContent(Uri.ToString()); else { LString s = t->GetContent(); bool isUri = s.Find("://") >= 0; if (isUri) Uri.Set(s); else Uri.SetFile(s); } return true; } LXmlTag *VcFolder::Save() { LXmlTag *t = new LXmlTag(OPT_Folder); if (t) Serialize(t, true); return t; } const char *VcFolder::GetVcName() { const char *Def = NULL; switch (GetType()) { case VcGit: Def = "git"; break; case VcSvn: Def = "svn"; break; case VcHg: Def = "hg"; break; case VcCvs: Def = "cvs"; break; default: break; } if (!VcCmd) { LString Opt; Opt.Printf("%s-path", Def); LVariant v; if (d->Opts.GetValue(Opt, v)) VcCmd = v.Str(); } if (!VcCmd) VcCmd = Def; return VcCmd; } bool VcFolder::RunCmd(const char *Args, LoggingType Logging, std::function Callback) { Result Ret; Ret.Code = -1; const char *Exe = GetVcName(); if (!Exe || CmdErrors > 2) return false; if (Uri.IsFile()) { new ProcessCallback(Exe, Args, LocalPath(), Logging == LogNone ? d->Log : NULL, GetTree()->GetWindow(), Callback); } else { LAssert(!"Impl me."); return false; } return true; } bool VcFolder::StartCmd(const char *Args, ParseFn Parser, ParseParams *Params, LoggingType Logging) { const char *Exe = GetVcName(); if (!Exe) return false; if (CmdErrors > 2) return false; if (Uri.IsFile()) { if (d->Log && Logging != LogSilo) d->Log->Print("%s %s\n", Exe, Args); LAutoPtr Process(new LSubProcess(Exe, Args)); if (!Process) return false; Process->SetInitFolder(Params && Params->AltInitPath ? Params->AltInitPath.Get() : LocalPath()); #ifdef MAC // Mac GUI apps don't share the terminal path, so this overrides that and make it work auto Path = LGetPath(); if (Path.Length()) { LString Tmp = LString(LGI_PATH_SEPARATOR).Join(Path); Process->SetEnvironment("PATH", Tmp); } #endif LString::Array Ctx; Ctx.SetFixedLength(false); Ctx.Add(LocalPath()); Ctx.Add(Exe); Ctx.Add(Args); LAutoPtr c(new Cmd(Ctx, Logging, d->Log)); if (!c) return false; c->PostOp = Parser; c->Params.Reset(Params); c->Rd.Reset(new ReaderThread(GetType(), Process, c)); Cmds.Add(c.Release()); } else { auto c = d->GetConnection(Uri.ToString()); if (!c) return false; if (!c->Command(this, Exe, Args, Parser, Params)) return false; } Update(); return true; } int LogDateCmp(LListItem *a, LListItem *b, NativeInt Data) { VcCommit *A = dynamic_cast(a); VcCommit *B = dynamic_cast(b); if ((A != NULL) ^ (B != NULL)) { // This handles keeping the "working folder" list item at the top return (A != NULL) - (B != NULL); } // Sort the by date from most recent to least return -A->GetTs().Compare(&B->GetTs()); } void VcFolder::AddGitName(LString Hash, LString Name) { LString Existing = GitNames.Find(Hash); if (Existing) GitNames.Add(Hash, Existing + "," + Name); else GitNames.Add(Hash, Name); } LString VcFolder::GetGitNames(LString Hash) { LString Short = Hash(0, 11); return GitNames.Find(Short); } bool VcFolder::ParseBranches(int Result, LString s, ParseParams *Params) { switch (GetType()) { case VcGit: { LString::Array a = s.SplitDelimit("\r\n"); for (auto &l: a) { LString::Array c = l.SplitDelimit(" \t"); if (c[0].Equals("*")) { CurrentBranch = c[1]; AddGitName(c[2], CurrentBranch); Branches.Add(CurrentBranch, new VcBranch(CurrentBranch, c[2])); } else { AddGitName(c[1], c[0]); Branches.Add(c[0], new VcBranch(c[0], c[1])); } } break; } case VcHg: { auto a = s.SplitDelimit("\r\n"); Branches.DeleteObjects(); for (auto b: a) { if (!CurrentBranch) CurrentBranch = b; Branches.Add(b, new VcBranch(b)); } if (Params && Params->Str.Equals("CountToTip")) CountToTip(); break; } default: { break; } } IsBranches = Result ? StatusError : StatusNone; OnBranchesChange(); return false; } void VcFolder::OnBranchesChange() { auto *w = d->Tree->GetWindow(); if (!w || !LTreeItem::Select()) return; if (Branches.Length()) { // Set the colours up LString Default; for (auto b: Branches) { if (!stricmp(b.key, "default") || !stricmp(b.key, "trunk")) Default = b.key; /* else printf("Other=%s\n", b.key); */ } int Idx = 1; for (auto b: Branches) { if (!b.value->Colour.IsValid()) { if (Default && !stricmp(b.key, Default)) b.value->Colour = GetPaletteColour(0); else b.value->Colour = GetPaletteColour(Idx++); } } } DropDownBtn *dd; if (w->GetViewById(IDC_BRANCH_DROPDOWN, dd)) { LString::Array a; for (auto b: Branches) a.Add(b.key); dd->SetList(IDC_BRANCH, a); } LViewI *b; if (Branches.Length() > 0 && w->GetViewById(IDC_BRANCH, b)) { if (CurrentBranch) { b->Name(CurrentBranch); } else { auto it = Branches.begin(); if (it != Branches.end()) b->Name((*it).key); } } } void VcFolder::DefaultFields() { if (Fields.Length() == 0) { switch (GetType()) { case VcHg: { Fields.Add(LGraph); Fields.Add(LIndex); Fields.Add(LRevision); Fields.Add(LBranch); Fields.Add(LAuthor); Fields.Add(LTimeStamp); Fields.Add(LMessageTxt); break; } case VcGit: { Fields.Add(LGraph); Fields.Add(LRevision); Fields.Add(LBranch); Fields.Add(LAuthor); Fields.Add(LTimeStamp); Fields.Add(LMessageTxt); break; } default: { Fields.Add(LGraph); Fields.Add(LRevision); Fields.Add(LAuthor); Fields.Add(LTimeStamp); Fields.Add(LMessageTxt); break; } } } } void VcFolder::UpdateColumns() { d->Commits->EmptyColumns(); for (auto c: Fields) { switch (c) { case LGraph: d->Commits->AddColumn("---", 60); break; case LIndex: d->Commits->AddColumn("Index", 60); break; case LBranch: d->Commits->AddColumn("Branch", 60); break; case LRevision: d->Commits->AddColumn("Revision", 60); break; case LAuthor: d->Commits->AddColumn("Author", 240); break; case LTimeStamp: d->Commits->AddColumn("Date", 130); break; case LMessageTxt: d->Commits->AddColumn("Message", 400); break; default: LAssert(0); break; } } } void VcFolder::FilterCurrentFiles() { LArray All; d->Files->GetAll(All); // Update the display property for (auto i: All) { auto fn = i->GetText(COL_FILENAME); bool vis = !d->FileFilter || Stristr(fn, d->FileFilter.Get()); i->GetCss(true)->Display(vis ? LCss::DispBlock : LCss::DispNone); // LgiTrace("Filter '%s' by '%s' = %i\n", fn, d->FileFilter.Get(), vis); } d->Files->Sort(0); d->Files->UpdateAllItems(); d->Files->ResizeColumnsToContent(); } void VcFolder::Select(bool b) { #if PROFILE_FN LProfile Prof("Select"); #endif if (!b) { auto *w = d->Tree->GetWindow(); w->SetCtrlName(IDC_BRANCH, NULL); } PROF("Parent.Select"); LTreeItem::Select(b); if (b) { if (Uri.IsFile() && !LDirExists(LocalPath())) return; PROF("DefaultFields"); DefaultFields(); PROF("Type Change"); if (GetType() != d->PrevType) { d->PrevType = GetType(); UpdateColumns(); } PROF("UpdateCommitList"); if ((Log.Length() == 0 || CommitListDirty) && !IsLogging) { switch (GetType()) { case VcGit: { LVariant Limit; d->Opts.GetValue("git-limit", Limit); LString cmd = "rev-list --all --header --timestamp --author-date-order", s; if (Limit.CastInt32() > 0) { s.Printf(" -n %i", Limit.CastInt32()); cmd += s; } IsLogging = StartCmd(cmd, &VcFolder::ParseRevList); break; } case VcSvn: { LVariant Limit; d->Opts.GetValue("svn-limit", Limit); if (CommitListDirty) { IsLogging = StartCmd("up", &VcFolder::ParsePull, new ParseParams("log")); break; } LString s; if (Limit.CastInt32() > 0) s.Printf("log --limit %i", Limit.CastInt32()); else s = "log"; IsLogging = StartCmd(s, &VcFolder::ParseLog); break; } case VcHg: { IsLogging = StartCmd("log", &VcFolder::ParseLog); StartCmd("resolve -l", &VcFolder::ParseResolveList); break; } case VcPending: { break; } default: { IsLogging = StartCmd("log", &VcFolder::ParseLog); break; } } CommitListDirty = false; } PROF("GetBranches"); if (GetBranches()) OnBranchesChange(); if (d->CurFolder != this) { PROF("RemoveAll"); d->CurFolder = this; d->Commits->RemoveAll(); } PROF("Uncommit"); if (!Uncommit) Uncommit.Reset(new UncommitedItem(d)); d->Commits->Insert(Uncommit, 0); PROF("Log Loop"); int64 CurRev = Atoi(CurrentCommit.Get()); List Ls; for (auto l: Log) { if (CurrentCommit && l->GetRev()) { switch (GetType()) { case VcSvn: { int64 LogRev = Atoi(l->GetRev()); if (CurRev >= 0 && CurRev >= LogRev) { CurRev = -1; l->SetCurrent(true); } else { l->SetCurrent(false); } break; } default: l->SetCurrent(!_stricmp(CurrentCommit, l->GetRev())); break; } } bool Add = !d->CommitFilter; if (d->CommitFilter) { const char *s = l->GetRev(); if (s && strstr(s, d->CommitFilter) != NULL) Add = true; s = l->GetAuthor(); if (s && stristr(s, d->CommitFilter) != NULL) Add = true; s = l->GetMsg(); if (s && stristr(s, d->CommitFilter) != NULL) Add = true; } LList *CurOwner = l->GetList(); if (Add ^ (CurOwner != NULL)) { if (Add) Ls.Insert(l); else d->Commits->Remove(l); } } PROF("Ls Ins"); d->Commits->Insert(Ls); if (d->Resort >= 0) { PROF("Resort"); d->Commits->Sort(LstCmp, d->Resort); d->Resort = -1; } PROF("ColSizing"); if (d->Commits->Length() > MAX_AUTO_RESIZE_ITEMS) { int i = 0; if (GetType() == VcHg && d->Commits->GetColumns() >= 7) { d->Commits->ColumnAt(i++)->Width(60); // LGraph d->Commits->ColumnAt(i++)->Width(40); // LIndex d->Commits->ColumnAt(i++)->Width(100); // LRevision d->Commits->ColumnAt(i++)->Width(60); // LBranch d->Commits->ColumnAt(i++)->Width(240); // LAuthor d->Commits->ColumnAt(i++)->Width(130); // LTimeStamp d->Commits->ColumnAt(i++)->Width(400); // LMessage } else if (d->Commits->GetColumns() >= 5) { d->Commits->ColumnAt(i++)->Width(40); // LGraph d->Commits->ColumnAt(i++)->Width(270); // LRevision d->Commits->ColumnAt(i++)->Width(240); // LAuthor d->Commits->ColumnAt(i++)->Width(130); // LTimeStamp d->Commits->ColumnAt(i++)->Width(400); // LMessage } } else d->Commits->ResizeColumnsToContent(); PROF("UpdateAll"); d->Commits->UpdateAllItems(); PROF("GetCur"); GetCurrentRevision(); } } int CommitRevCmp(VcCommit **a, VcCommit **b) { int64 arev = Atoi((*a)->GetRev()); int64 brev = Atoi((*b)->GetRev()); int64 diff = (int64)brev - arev; if (diff < 0) return -1; return (diff > 0) ? 1 : 0; } int CommitIndexCmp(VcCommit **a, VcCommit **b) { auto ai = (*a)->GetIndex(); auto bi = (*b)->GetIndex(); auto diff = (int64)bi - ai; if (diff < 0) return -1; return (diff > 0) ? 1 : 0; } int CommitDateCmp(VcCommit **a, VcCommit **b) { uint64 ats, bts; (*a)->GetTs().Get(ats); (*b)->GetTs().Get(bts); int64 diff = (int64)bts - ats; if (diff < 0) return -1; return (diff > 0) ? 1 : 0; } void VcFolder::GetCurrentRevision(ParseParams *Params) { if (CurrentCommit || IsIdent != StatusNone) return; switch (GetType()) { case VcGit: if (StartCmd("rev-parse HEAD", &VcFolder::ParseInfo, Params)) IsIdent = StatusActive; break; case VcSvn: if (StartCmd("info", &VcFolder::ParseInfo, Params)) IsIdent = StatusActive; break; case VcHg: if (StartCmd("id -i -n", &VcFolder::ParseInfo, Params)) IsIdent = StatusActive; break; case VcCvs: break; default: break; } } bool VcFolder::GetBranches(ParseParams *Params) { if (Branches.Length() > 0 || IsBranches != StatusNone) return true; switch (GetType()) { case VcGit: if (StartCmd("-P branch -a -v", &VcFolder::ParseBranches, Params)) IsBranches = StatusActive; break; case VcSvn: Branches.Add("trunk", new VcBranch("trunk")); OnBranchesChange(); break; case VcHg: if (StartCmd("branch", &VcFolder::ParseBranches, Params)) IsBranches = StatusActive; break; case VcCvs: break; default: break; } return false; } bool VcFolder::ParseRevList(int Result, LString s, ParseParams *Params) { Log.DeleteObjects(); int Errors = 0; switch (GetType()) { case VcGit: { LString::Array Commits; Commits.SetFixedLength(false); // Split on the NULL chars... char *c = s.Get(); char *e = c + s.Length(); while (c < e) { char *nul = c; while (nul < e && *nul) nul++; if (nul <= c) break; Commits.New().Set(c, nul-c); if (nul >= e) break; c = nul + 1; } for (auto Commit: Commits) { LAutoPtr Rev(new VcCommit(d, this)); if (Rev->GitParse(Commit, true)) { Log.Add(Rev.Release()); } else { LAssert(!"Parse failed."); LgiTrace("%s:%i - Failed:\n%s\n\n", _FL, Commit.Get()); Errors++; } } LinkParents(); break; } default: LAssert(!"Impl me."); break; } IsLogging = false; return Errors == 0; } LString VcFolder::GetFilePart(const char *uri) { LUri u(uri); LString File = u.IsFile() ? u.DecodeStr(u.LocalPath()) : u.sPath(Uri.sPath.Length(), -1).LStrip("/"); return File; } void VcFolder::LogFile(const char *uri) { LString Args; switch (GetType()) { case VcSvn: case VcHg: case VcGit: { LString File = GetFilePart(uri); ParseParams *Params = new ParseParams(uri); Args.Printf("log \"%s\"", File.Get()); IsLogging = StartCmd(Args, &VcFolder::ParseLog, Params, LogNormal); break; } default: LAssert(!"Impl me."); break; } } VcLeaf *VcFolder::FindLeaf(const char *Path, bool OpenTree) { VcLeaf *r = NULL; if (OpenTree) DoExpand(); for (auto n = GetChild(); !r && n; n = n->GetNext()) { auto l = dynamic_cast(n); if (l) r = l->FindLeaf(Path, OpenTree); } return r; } bool VcFolder::ParseLog(int Result, LString s, ParseParams *Params) { LHashTbl, VcCommit*> Map; for (auto pc: Log) Map.Add(pc->GetRev(), pc); int Skipped = 0, Errors = 0; VcLeaf *File = Params ? FindLeaf(Params->Str, true) : NULL; LArray *Out = File ? &File->Log : &Log; if (File) { for (auto Leaf = File; Leaf; Leaf = dynamic_cast(Leaf->GetParent())) Leaf->OnExpand(true); File->Select(true); File->ScrollTo(); } switch (GetType()) { case VcGit: { LString::Array c; c.SetFixedLength(false); char *prev = s.Get(); for (char *i = s.Get(); *i; ) { if (!strnicmp(i, "commit ", 7)) { if (i > prev) { c.New().Set(prev, i - prev); // LgiTrace("commit=%i\n", (int)(i - prev)); } prev = i; } while (*i) { if (*i++ == '\n') break; } } for (unsigned i=0; i Rev(new VcCommit(d, this)); if (Rev->GitParse(c[i], false)) { if (!Map.Find(Rev->GetRev())) Out->Add(Rev.Release()); else Skipped++; } else { LgiTrace("%s:%i - Failed:\n%s\n\n", _FL, c[i].Get()); Errors++; } } Out->Sort(CommitDateCmp); break; } case VcSvn: { LString::Array c = s.Split("------------------------------------------------------------------------"); for (unsigned i=0; i Rev(new VcCommit(d, this)); LString Raw = c[i].Strip(); if (Rev->SvnParse(Raw)) { if (File || !Map.Find(Rev->GetRev())) Out->Add(Rev.Release()); else Skipped++; } else if (Raw) { OnCmdError(Raw, "ParseLog Failed"); Errors++; } } Out->Sort(CommitRevCmp); break; } case VcHg: { LString::Array c = s.Split("\n\n"); LHashTbl, VcCommit*> Idx; for (auto &Commit: c) { LAutoPtr Rev(new VcCommit(d, this)); if (Rev->HgParse(Commit)) { auto Existing = File ? NULL : Map.Find(Rev->GetRev()); if (!Existing) Out->Add(Existing = Rev.Release()); if (Existing->GetIndex() >= 0) Idx.Add(Existing->GetIndex(), Existing); } } if (!File) { // Patch all the trivial parents... for (auto c: Log) { if (c->GetParents()->Length() > 0) continue; auto CIdx = c->GetIndex(); if (CIdx <= 0) continue; auto Par = Idx.Find(CIdx - 1); if (Par) c->GetParents()->Add(Par->GetRev()); } } Out->Sort(CommitIndexCmp); if (!File) LinkParents(); d->Resort = 1; break; } case VcCvs: { if (Result) { OnCmdError(s, "Cvs command failed."); break; } LHashTbl, VcCommit*> Map; LString::Array c = s.Split("============================================================================="); for (auto &Commit: c) { if (Commit.Strip().Length()) { LString Head, File; LString::Array Versions = Commit.Split("----------------------------"); LString::Array Lines = Versions[0].SplitDelimit("\r\n"); for (auto &Line: Lines) { LString::Array p = Line.Split(":", 1); if (p.Length() == 2) { // LgiTrace("Line: %s\n", Line->Get()); LString Var = p[0].Strip().Lower(); LString Val = p[1].Strip(); if (Var.Equals("branch")) { if (Val.Length()) Branches.Add(Val, new VcBranch(Val)); } else if (Var.Equals("head")) { Head = Val; } else if (Var.Equals("rcs file")) { LString::Array f = Val.SplitDelimit(","); File = f.First(); } } } // LgiTrace("%s\n", Commit->Get()); for (unsigned i=1; i= 3) { LString Ver = Lines[0].Split(" ").Last(); LString::Array a = Lines[1].SplitDelimit(";"); LString Date = a[0].Split(":", 1).Last().Strip(); LString Author = a[1].Split(":", 1).Last().Strip(); LString Id = a[2].Split(":", 1).Last().Strip(); LString Msg = Lines[2]; LDateTime Dt; if (Dt.Parse(Date)) { uint64 Ts; if (Dt.Get(Ts)) { VcCommit *Cc = Map.Find(Ts); if (!Cc) { Map.Add(Ts, Cc = new VcCommit(d, this)); Out->Add(Cc); Cc->CvsParse(Dt, Author, Msg); } Cc->Files.Add(File.Get()); } else LAssert(!"NO ts for date."); } else LAssert(!"Date parsing failed."); } } } } break; } default: LAssert(!"Impl me."); break; } if (File) File->ShowLog(); // LgiTrace("%s:%i - ParseLog: Skip=%i, Error=%i\n", _FL, Skipped, Errors); IsLogging = false; return !Result; } void VcFolder::LinkParents() { #if PROFILE_FN LProfile Prof("LinkParents"); #endif LHashTbl,VcCommit*> Map; // Index all the commits int i = 0; for (auto c:Log) { c->Idx = i++; c->NodeIdx = -1; Map.Add(c->GetRev(), c); } // Create all the edges... PROF("Create edges."); for (auto c:Log) { auto *Par = c->GetParents(); for (auto &pRev : *Par) { auto *p = Map.Find(pRev); if (p) new VcEdge(p, c); #if 0 else return; #endif } } // Map the edges to positions PROF("Map edges."); typedef LArray EdgeArr; LArray Active; for (auto c:Log) { for (unsigned i=0; c->NodeIdx<0 && iParent == c) { c->NodeIdx = i; break; } } } // Add starting edges to active set for (auto e:c->Edges) { if (e->Child == c) { if (c->NodeIdx < 0) c->NodeIdx = (int)Active.Length(); e->Idx = c->NodeIdx; c->Pos.Add(e, e->Idx); Active[e->Idx].Add(e); } } // Now for all active edges... assign positions for (unsigned i=0; iLength(); n++) { LAssert(Active.PtrCheck(Edges)); VcEdge *e = (*Edges)[n]; if (c == e->Child || c == e->Parent) { LAssert(c->NodeIdx >= 0); c->Pos.Add(e, c->NodeIdx); } else { // May need to untangle edges with different parents here bool Diff = false; for (auto edge: *Edges) { if (edge != e && edge->Child != c && edge->Parent != e->Parent) { Diff = true; break; } } if (Diff) { int NewIndex = -1; // Look through existing indexes for a parent match for (unsigned ii=0; iiParent? bool Match = true; for (auto ee: Active[ii]) { if (ee->Parent != e->Parent) { Match = false; break; } } if (Match) NewIndex = ii; } if (NewIndex < 0) // Create new index for this parent NewIndex = (int)Active.Length(); Edges->Delete(e); auto &NewEdges = Active[NewIndex]; NewEdges.Add(e); Edges = &Active[i]; // The 'Add' above can invalidate the object 'Edges' refers to e->Idx = NewIndex; c->Pos.Add(e, NewIndex); n--; } else { LAssert(e->Idx == i); c->Pos.Add(e, i); } } } } // Process terminating edges for (auto e: c->Edges) { if (e->Parent == c) { if (e->Idx < 0) { // This happens with out of order commits..? continue; } int i = e->Idx; if (c->NodeIdx < 0) c->NodeIdx = i; if (Active[i].HasItem(e)) Active[i].Delete(e); else LgiTrace("%s:%i - Warning: Active doesn't have 'e'.\n", _FL); } } // Collapse any empty active columns for (unsigned i=0; iIdx > 0); edge->Idx--; c->Pos.Add(edge, edge->Idx); } } i--; } } } // Find all the "heads", i.e. a commit without any children PROF("Find heads."); LCombo *Heads; if (d->Wnd()->GetViewById(IDC_HEADS, Heads)) { Heads->Empty(); for (auto c:Log) { bool Has = false; for (auto e:c->Edges) { if (e->Parent == c) { Has = true; break; } } if (!Has) Heads->Insert(c->GetRev()); } Heads->SendNotify(LNotifyTableLayoutRefresh); } } VcFile *AppPriv::FindFile(const char *Path) { if (!Path) return NULL; LArray files; if (Files->GetAll(files)) { LString p = Path; p = p.Replace(DIR_STR, "/"); for (auto f : files) { auto Fn = f->GetFileName(); if (p.Equals(Fn)) return f; } } return NULL; } VcFile *VcFolder::FindFile(const char *Path) { return d->FindFile(Path); } void VcFolder::OnCmdError(LString Output, const char *Msg) { if (!CmdErrors) { if (Output.Length()) d->Log->Write(Output, Output.Length()); auto vc_name = GetVcName(); if (vc_name) { LString::Array a = GetProgramsInPath(GetVcName()); d->Log->Print("'%s' executables in the path:\n", GetVcName()); for (auto Bin : a) d->Log->Print(" %s\n", Bin.Get()); } else if (Msg) { d->Log->Print("%s\n", Msg); } } CmdErrors++; d->Tabs->Value(1); GetCss(true)->Color(LColour::Red); } void VcFolder::ClearError() { GetCss(true)->Color(LCss::ColorInherit); } bool VcFolder::ParseInfo(int Result, LString s, ParseParams *Params) { switch (GetType()) { case VcGit: case VcHg: { auto p = s.Strip().SplitDelimit(); CurrentCommit = p[0].Strip(" \t\r\n+"); if (p.Length() > 1) CurrentCommitIdx = p[1].Int(); else CurrentCommitIdx = -1; if (Params && Params->Str.Equals("CountToTip")) CountToTip(); break; } case VcSvn: { if (s.Find("client is too old") >= 0) { OnCmdError(s, "Client too old"); break; } LString::Array c = s.Split("\n"); for (unsigned i=0; iIsWorking = true; ParseStatus(Result, s, Params); break; } case VcCvs: { bool Untracked = d->IsMenuChecked(IDM_UNTRACKED); if (Untracked) { auto Lines = s.SplitDelimit("\n"); for (auto Ln: Lines) { auto p = Ln.SplitDelimit(" \t", 1); if (p.Length() > 1) { auto f = new VcFile(d, this, NULL, true); f->SetText(p[0], COL_STATE); f->SetText(p[1], COL_FILENAME); f->GetStatus(); d->Files->Insert(f); } } } // else fall thru } default: { ParseDiffs(s, NULL, true); break; } } IsWorkingFld = false; FilterCurrentFiles(); d->Files->ResizeColumnsToContent(); if (GetType() == VcSvn) { Unpushed = d->Files->Length() > 0 ? 1 : 0; Update(); } return false; } void VcFolder::DiffRange(const char *FromRev, const char *ToRev) { if (!FromRev || !ToRev) return; switch (GetType()) { case VcSvn: { ParseParams *p = new ParseParams; p->IsWorking = false; p->Str = LString(FromRev) + ":" + ToRev; LString a; a.Printf("diff -r%s:%s", FromRev, ToRev); StartCmd(a, &VcFolder::ParseDiff, p); break; } case VcGit: { ParseParams *p = new ParseParams; p->IsWorking = false; p->Str = LString(FromRev) + ":" + ToRev; LString a; a.Printf("-P diff %s..%s", FromRev, ToRev); StartCmd(a, &VcFolder::ParseDiff, p); break; } case VcCvs: case VcHg: default: LAssert(!"Impl me."); break; } } bool VcFolder::ParseDiff(int Result, LString s, ParseParams *Params) { if (Params) ParseDiffs(s, Params->Str, Params->IsWorking); else ParseDiffs(s, NULL, true); return false; } void VcFolder::Diff(VcFile *file) { auto Fn = file->GetFileName(); if (!Fn || !Stricmp(Fn, ".") || !Stricmp(Fn, "..")) return; const char *Prefix = ""; switch (GetType()) { case VcGit: Prefix = "-P "; // fall through case VcHg: { LString a; auto rev = file->GetRevision(); if (rev) a.Printf("%sdiff %s \"%s\"", Prefix, rev, Fn); else a.Printf("%sdiff \"%s\"", Prefix, Fn); StartCmd(a, &VcFolder::ParseDiff); break; } case VcSvn: { LString a; if (file->GetRevision()) a.Printf("diff -r %s \"%s\"", file->GetRevision(), Fn); else a.Printf("diff \"%s\"", Fn); StartCmd(a, &VcFolder::ParseDiff); break; } case VcCvs: break; default: LAssert(!"Impl me."); break; } } bool VcFolder::ParseDiffs(LString s, LString Rev, bool IsWorking) { LAssert(IsWorking || Rev.Get() != NULL); switch (GetType()) { case VcGit: { LString::Array a = s.Split("\n"); LString Diff; VcFile *f = NULL; for (unsigned i=0; iSetDiff(Diff); Diff.Empty(); auto Bits = a[i].SplitDelimit(); LString Fn, State = "M"; if (Bits[1].Equals("--cc")) { Fn = Bits.Last(); State = "C"; } else Fn = Bits.Last()(2,-1); // LgiTrace("%s\n", a[i].Get()); f = FindFile(Fn); if (!f) f = new VcFile(d, this, Rev, IsWorking); f->SetText(State, COL_STATE); f->SetText(Fn.Replace("\\","/"), COL_FILENAME); f->GetStatus(); d->Files->Insert(f); } else if (!_strnicmp(Ln, "new file", 8)) { if (f) f->SetText("A", COL_STATE); } else if (!_strnicmp(Ln, "index", 5) || !_strnicmp(Ln, "commit", 6) || !_strnicmp(Ln, "Author:", 7) || !_strnicmp(Ln, "Date:", 5) || !_strnicmp(Ln, "+++", 3) || !_strnicmp(Ln, "---", 3)) { // Ignore } else { if (Diff) Diff += "\n"; Diff += a[i]; } } if (f && Diff) { f->SetDiff(Diff); Diff.Empty(); } break; } case VcHg: { LString Sep("\n"); LString::Array a = s.Split(Sep); LString::Array Diffs; VcFile *f = NULL; List Files; LProgressDlg Prog(GetTree(), 1000); Prog.SetDescription("Reading diff lines..."); Prog.SetRange(a.Length()); // Prog.SetYieldTime(300); for (unsigned i=0; iSetDiff(Sep.Join(Diffs)); Diffs.Empty(); auto MainParts = a[i].Split(" -r "); auto FileParts = MainParts.Last().Split(" ",1); LString Fn = FileParts.Last(); f = FindFile(Fn); if (!f) f = new VcFile(d, this, Rev, IsWorking); f->SetText(Fn.Replace("\\","/"), COL_FILENAME); // f->SetText(Status, COL_STATE); Files.Insert(f); } else if (!_strnicmp(Ln, "index", 5) || !_strnicmp(Ln, "commit", 6) || !_strnicmp(Ln, "Author:", 7) || !_strnicmp(Ln, "Date:", 5) || !_strnicmp(Ln, "+++", 3) || !_strnicmp(Ln, "---", 3)) { // Ignore } else { Diffs.Add(a[i]); } Prog.Value(i); if (Prog.IsCancelled()) break; } if (f && Diffs.Length()) { f->SetDiff(Sep.Join(Diffs)); Diffs.Empty(); } d->Files->Insert(Files); break; } case VcSvn: { LString::Array a = s.Replace("\r").Split("\n"); LString Diff; VcFile *f = NULL; bool InPreamble = false; bool InDiff = false; for (unsigned i=0; iSetDiff(Diff); f->Select(false); } Diff.Empty(); InDiff = false; InPreamble = false; LString Fn = a[i].Split(":", 1).Last().Strip(); f = FindFile(Fn); if (!f) f = new VcFile(d, this, Rev, IsWorking); f->SetText(Fn.Replace("\\","/"), COL_FILENAME); f->SetText("M", COL_STATE); f->GetStatus(); d->Files->Insert(f); } else if (!_strnicmp(Ln, "------", 6)) { InPreamble = !InPreamble; } else if (!_strnicmp(Ln, "======", 6)) { InPreamble = false; InDiff = true; } else if (InDiff) { if (!strncmp(Ln, "--- ", 4) || !strncmp(Ln, "+++ ", 4)) { } else { if (Diff) Diff += "\n"; Diff += a[i]; } } } if (f && Diff) { f->SetDiff(Diff); Diff.Empty(); } break; } case VcCvs: { break; } default: { LAssert(!"Impl me."); break; } } FilterCurrentFiles(); return true; } bool VcFolder::ParseFiles(int Result, LString s, ParseParams *Params) { d->ClearFiles(); ParseDiffs(s, Params->Str, false); IsFilesCmd = false; FilterCurrentFiles(); return false; } void VcFolder::OnSshCmd(SshParams *p) { if (!p || !p->f) { LAssert(!"Param error."); return; } LString s = p->Output; int Result = p->ExitCode; if (Result == ErrSubProcessFailed) { CmdErrors++; } else if (p->Parser) { bool Reselect = CALL_MEMBER_FN(*this, p->Parser)(Result, s, p->Params); if (Reselect) { if (LTreeItem::Select()) Select(true); } } } void VcFolder::OnPulse() { bool Reselect = false, CmdsChanged = false; static bool Processing = false; if (!Processing) { Processing = true; // Lock out processing, if it puts up a dialog or something... // bad things happen if we try and re-process something. // printf("Cmds.Len=%i\n", (int)Cmds.Length()); for (unsigned i=0; iRd->GetState()=%i\n", c->Rd->GetState()); if (c->Rd->GetState() == LThread::THREAD_INIT) { if (CmdActiveThreads < CmdMaxThreads) { c->Rd->Run(); CmdActiveThreads++; // LgiTrace("CmdActiveThreads++ = %i\n", CmdActiveThreads); } // else printf("Too many active threads."); } else if (c->Rd->IsExited()) { CmdActiveThreads--; // LgiTrace("CmdActiveThreads-- = %i\n", CmdActiveThreads); LString s = c->GetBuf(); int Result = c->Rd->ExitCode(); if (Result == ErrSubProcessFailed) { if (!CmdErrors) d->Log->Print("Error: Can't run '%s'\n", GetVcName()); CmdErrors++; } else if (c->PostOp) { if (s.Length() == 18 && s.Equals("GSUBPROCESS_ERROR\n")) { OnCmdError(s, "Sub process failed."); } else { Reselect |= CALL_MEMBER_FN(*this, c->PostOp)(Result, s, c->Params); } } Cmds.DeleteAt(i--, true); delete c; CmdsChanged = true; } // else printf("Not exited.\n"); } Processing = false; } if (Reselect) { if (LTreeItem::Select()) Select(true); } if (CmdsChanged) { Update(); } if (CmdErrors) { d->Tabs->Value(1); CmdErrors = false; } } void VcFolder::OnRemove() { LXmlTag *t = d->Opts.LockTag(OPT_Folders, _FL); if (t) { Uncommit.Reset(); if (LTreeItem::Select()) { d->Files->Empty(); d->Commits->RemoveAll(); } bool Found = false; auto u = Uri.ToString(); for (auto c: t->Children) { if (c->IsTag(OPT_Folder) && c->GetContent()) { auto Content = c->GetContent(); if (!_stricmp(Content, u)) { c->RemoveTag(); delete c; Found = true; break; } } } LAssert(Found); d->Opts.Unlock(); } } void VcFolder::Empty() { Type = VcNone; IsCommit = false; IsLogging = false; IsUpdate = false; IsFilesCmd = false; IsWorkingFld = false; CommitListDirty = false; IsUpdatingCounts = false; IsBranches = StatusNone; IsIdent = StatusNone; Unpushed = Unpulled = -1; CmdErrors = 0; CurrentCommitIdx = -1; CurrentCommit.Empty(); RepoUrl.Empty(); VcCmd.Empty(); Uncommit.Reset(); Log.DeleteObjects(); d->Commits->Empty(); d->Files->Empty(); if (!Uri.IsFile()) GetCss(true)->Color(LColour::Blue); } void VcFolder::OnMouseClick(LMouse &m) { if (m.IsContextMenu()) { LSubMenu s; s.AppendItem("Browse To", IDM_BROWSE_FOLDER, Uri.IsFile()); s.AppendItem( #ifdef WINDOWS "Command Prompt At", #else "Terminal At", #endif IDM_TERMINAL, Uri.IsFile()); s.AppendItem("Clean", IDM_CLEAN); s.AppendSeparator(); s.AppendItem("Pull", IDM_PULL); s.AppendItem("Status", IDM_STATUS); s.AppendItem("Push", IDM_PUSH); s.AppendItem("Update Subs", IDM_UPDATE_SUBS, GetType() == VcGit); s.AppendSeparator(); s.AppendItem("Remove", IDM_REMOVE); if (!Uri.IsFile()) { s.AppendSeparator(); s.AppendItem("Edit Location", IDM_EDIT); } int Cmd = s.Float(GetTree(), m); switch (Cmd) { case IDM_BROWSE_FOLDER: { LBrowseToFile(LocalPath()); break; } case IDM_TERMINAL: { TerminalAt(LocalPath()); break; } case IDM_CLEAN: { Clean(); break; } case IDM_PULL: { Pull(); break; } case IDM_STATUS: { FolderStatus(); break; } case IDM_PUSH: { Push(); break; } case IDM_UPDATE_SUBS: { UpdateSubs(); break; } case IDM_REMOVE: { OnRemove(); delete this; break; } case IDM_EDIT: { auto Dlg = new LInput(GetTree(), Uri.ToString(), "URI:", "Remote Folder Location"); Dlg->DoModal([this, Dlg](auto dlg, auto ctrlId) { if (ctrlId) { Uri.Set(Dlg->GetStr()); Empty(); Select(true); } delete dlg; }); break; } default: break; } } } void VcFolder::OnUpdate(const char *Rev) { if (!Rev) return; if (!IsUpdate) { LString Args; NewRev = Rev; switch (GetType()) { case VcGit: Args.Printf("checkout %s", Rev); IsUpdate = StartCmd(Args, &VcFolder::ParseUpdate, NULL, LogNormal); break; case VcSvn: Args.Printf("up -r %s", Rev); IsUpdate = StartCmd(Args, &VcFolder::ParseUpdate, NULL, LogNormal); break; case VcHg: Args.Printf("update -r %s", Rev); IsUpdate = StartCmd(Args, &VcFolder::ParseUpdate, NULL, LogNormal); break; default: { LAssert(!"Impl me."); break; } } } } /////////////////////////////////////////////////////////////////////////////////////// int FolderCompare(LTreeItem *a, LTreeItem *b, NativeInt UserData) { VcLeaf *A = dynamic_cast(a); VcLeaf *B = dynamic_cast(b); if (!A || !B) return 0; return A->Compare(B); } struct SshFindEntry { LString Flags, Name, User, Group; uint64_t Size; LDateTime Modified, Access; SshFindEntry &operator =(const LString &s) { auto p = s.SplitDelimit("/"); if (p.Length() == 7) { Flags = p[0]; Group = p[1]; User = p[2]; Access.Set((uint64_t) p[3].Int()); Modified.Set((uint64_t) p[4].Int()); Size = p[5].Int(); Name = p[6]; } return *this; } bool IsDir() { return Flags(0) == 'd'; } bool IsHidden() { return Name(0) == '.'; } const char *GetName() { return Name; } static int Compare(SshFindEntry *a, SshFindEntry *b) { return Stricmp(a->Name.Get(), b->Name.Get()); } }; bool VcFolder::ParseRemoteFind(int Result, LString s, ParseParams *Params) { if (!Params || !s) return false; auto Parent = Params->Leaf ? static_cast(Params->Leaf) : static_cast(this); LUri u(Params->Str); auto Lines = s.SplitDelimit("\r\n"); LArray Entries; for (size_t i=1; iStr, Dir.GetName(), true); } } else if (!Dir.IsHidden()) { char *Ext = LGetExtension(Dir.GetName()); if (!Ext) continue; if (!stricmp(Ext, "c") || !stricmp(Ext, "cpp") || !stricmp(Ext, "h")) { LUri Path = u; Path += Dir.GetName(); new VcLeaf(this, Parent, Params->Str, Dir.GetName(), false); } } } return false; } void VcFolder::ReadDir(LTreeItem *Parent, const char *ReadUri) { LUri u(ReadUri); if (u.IsFile()) { // Read child items LDirectory Dir; for (int b = Dir.First(u.LocalPath()); b; b = Dir.Next()) { auto name = Dir.GetName(); if (Dir.IsHidden()) continue; LUri Path = u; Path += name; new VcLeaf(this, Parent, u.ToString(), name, Dir.IsDir()); } } else { auto c = d->GetConnection(ReadUri); if (!c) return; LString Path = u.sPath(Uri.sPath.Length(), -1).LStrip("/"); LString Args; Args.Printf("\"%s\" -maxdepth 1 -printf \"%%M/%%g/%%u/%%A@/%%T@/%%s/%%P\n\"", Path ? Path.Get() : "."); auto *Params = new ParseParams(ReadUri); Params->Leaf = dynamic_cast(Parent); c->Command(this, "find", Args, &VcFolder::ParseRemoteFind, Params); return; } Parent->Sort(FolderCompare); } void VcFolder::OnVcsType(LString errorMsg) { if (!d) { LAssert(!"No priv instance"); return; } auto c = d->GetConnection(Uri.ToString(), false); if (c) { auto NewType = c->Types.Find(Uri.sPath); if (NewType && NewType != Type) { if (NewType == VcError) { OnCmdError(NULL, errorMsg); } else { Type = NewType; ClearError(); Update(); if (LTreeItem::Select()) Select(true); } } } } void VcFolder::DoExpand() { if (Tmp) { Tmp->Remove(); DeleteObj(Tmp); ReadDir(this, Uri.ToString()); } } void VcFolder::OnExpand(bool b) { if (b) DoExpand(); } void VcFolder::ListCommit(VcCommit *c) { if (!IsFilesCmd) { LString Args; switch (GetType()) { case VcGit: Args.Printf("-P show %s", c->GetRev()); IsFilesCmd = StartCmd(Args, &VcFolder::ParseFiles, new ParseParams(c->GetRev())); break; case VcSvn: Args.Printf("log --verbose --diff -r %s", c->GetRev()); IsFilesCmd = StartCmd(Args, &VcFolder::ParseFiles, new ParseParams(c->GetRev())); break; case VcCvs: { d->ClearFiles(); for (unsigned i=0; iFiles.Length(); i++) { VcFile *f = new VcFile(d, this, c->GetRev(), false); if (f) { f->SetText(c->Files[i], COL_FILENAME); d->Files->Insert(f); } } FilterCurrentFiles(); break; } case VcHg: { Args.Printf("diff --change %s", c->GetRev()); IsFilesCmd = StartCmd(Args, &VcFolder::ParseFiles, new ParseParams(c->GetRev())); break; } default: LAssert(!"Impl me."); break; } if (IsFilesCmd) d->ClearFiles(); } } LString ConvertUPlus(LString s) { LArray c; LUtf8Ptr p(s); int32 ch; while ((ch = p)) { if (ch == '{') { auto n = p.GetPtr(); if (n[1] == 'U' && n[2] == '+') { // Convert unicode code point p += 3; ch = (int32)htoi(p.GetPtr()); c.Add(ch); while ((ch = p) != '}') p++; } else c.Add(ch); } else c.Add(ch); p++; } c.Add(0); #ifdef LINUX return LString((char16*)c.AddressOf()); #else return LString(c.AddressOf()); #endif } bool VcFolder::ParseStatus(int Result, LString s, ParseParams *Params) { bool ShowUntracked = d->Wnd()->GetCtrlValue(IDC_UNTRACKED) != 0; bool IsWorking = Params ? Params->IsWorking : false; List Ins; switch (GetType()) { case VcCvs: { LHashTbl,VcFile*> Map; for (auto i: *d->Files) { VcFile *f = dynamic_cast(i); if (f) Map.Add(f->GetText(COL_FILENAME), f); } #if 0 LFile Tmp("C:\\tmp\\output.txt", O_WRITE); Tmp.Write(s); Tmp.Close(); #endif LString::Array a = s.Split("==================================================================="); for (auto i : a) { LString::Array Lines = i.SplitDelimit("\r\n"); if (Lines.Length() == 0) continue; LString f = Lines[0].Strip(); if (f.Find("File:") == 0) { LString::Array Parts = f.SplitDelimit("\t"); LString File = Parts[0].Split(": ").Last().Strip(); LString Status = Parts[1].Split(": ").Last(); LString WorkingRev; for (auto l : Lines) { LString::Array p = l.Strip().Split(":", 1); if (p.Length() > 1 && p[0].Strip().Equals("Working revision")) { WorkingRev = p[1].Strip(); } } VcFile *f = Map.Find(File); if (!f) { if ((f = new VcFile(d, this, WorkingRev, IsWorking))) Ins.Insert(f); } if (f) { f->SetText(Status, COL_STATE); f->SetText(File, COL_FILENAME); f->Update(); } } else if (f(0) == '?' && ShowUntracked) { LString File = f(2, -1); VcFile *f = Map.Find(File); if (!f) { if ((f = new VcFile(d, this, NULL, IsWorking))) Ins.Insert(f); } if (f) { f->SetText("?", COL_STATE); f->SetText(File, COL_FILENAME); f->Update(); } } } for (auto i: *d->Files) { VcFile *f = dynamic_cast(i); if (f) { if (f->GetStatus() == VcFile::SUnknown) f->SetStatus(VcFile::SUntracked); } } break; } case VcGit: { LString::Array Lines = s.SplitDelimit("\r\n"); int Fmt = ToolVersion[VcGit] >= Ver2Int("2.8.0") ? 2 : 1; for (auto Ln : Lines) { char Type = Ln(0); if (Ln.Lower().Find("error:") >= 0) { } else if (Ln.Find("usage: git") >= 0) { // It's probably complaining about the --porcelain=2 parameter OnCmdError(s, "Args error"); } else if (Type != '?') { VcFile *f = NULL; if (Fmt == 2) { LString::Array p = Ln.SplitDelimit(" ", 8); if (p.Length() < 7) d->Log->Print("%s:%i - Error: not enough tokens: '%s'\n", _FL, Ln.Get()); else { f = new VcFile(d, this, p[6], IsWorking); f->SetText(p[1].Strip("."), COL_STATE); f->SetText(p.Last(), COL_FILENAME); } } else if (Fmt == 1) { LString::Array p = Ln.SplitDelimit(" "); f = new VcFile(d, this, NULL, IsWorking); f->SetText(p[0], COL_STATE); f->SetText(p.Last(), COL_FILENAME); } if (f) Ins.Insert(f); } else if (ShowUntracked) { VcFile *f = new VcFile(d, this, NULL, IsWorking); f->SetText("?", COL_STATE); f->SetText(Ln(2,-1), COL_FILENAME); Ins.Insert(f); } } break; } case VcHg: case VcSvn: { if (s.Find("failed to import") >= 0) { OnCmdError(s, "Tool error."); return false; } LString::Array Lines = s.SplitDelimit("\r\n"); for (auto Ln : Lines) { char Type = Ln(0); if (Ln.Lower().Find("error:") >= 0) { } else if (Ln.Find("client is too old") >= 0) { OnCmdError(s, "Client too old."); return false; } else if (Strchr(" \t", Type) || Ln.Find("Summary of conflicts") >= 0) { // Ignore } else if (Type != '?') { LString::Array p = Ln.SplitDelimit(" ", 1); if (p.Length() == 2) { LString File; if (GetType() == VcSvn) File = ConvertUPlus(p.Last()); else File = p.Last(); if (GetType() == VcSvn && File.Find("+ ") == 0) { File = File(5, -1); } VcFile *f = new VcFile(d, this, NULL, IsWorking); f->SetText(p[0], COL_STATE); f->SetText(File.Replace("\\","/"), COL_FILENAME); f->GetStatus(); Ins.Insert(f); } else LAssert(!"What happen?"); } else if (ShowUntracked) { VcFile *f = new VcFile(d, this, NULL, IsWorking); f->SetText("?", COL_STATE); f->SetText(Ln(2,-1), COL_FILENAME); Ins.Insert(f); } } break; } default: { LAssert(!"Impl me."); break; } } if ((Unpushed = Ins.Length() > 0)) { if (CmdErrors == 0) GetCss(true)->Color(LColour(255, 128, 0)); } else if (Unpulled == 0) { GetCss(true)->Color(LCss::ColorInherit); } Update(); if (LTreeItem::Select()) { d->Files->Insert(Ins); FilterCurrentFiles(); } else { Ins.DeleteObjects(); } if (Params && Params->Leaf) Params->Leaf->AfterBrowse(); return false; // Don't refresh list } // Clone/checkout any sub-repositries. bool VcFolder::UpdateSubs() { LString Arg; switch (GetType()) { default: case VcSvn: case VcHg: case VcCvs: return false; case VcGit: Arg = "submodule update --init"; break; } return StartCmd(Arg, &VcFolder::ParseUpdateSubs, NULL, LogNormal); } bool VcFolder::ParseUpdateSubs(int Result, LString s, ParseParams *Params) { switch (GetType()) { default: case VcSvn: case VcHg: case VcCvs: return false; case VcGit: break; } return false; } void VcFolder::FolderStatus(const char *uri, VcLeaf *Notify) { LUri Uri(uri); if (Uri.IsFile() && Uri.sPath) { LFile::Path p(Uri.sPath(1,-1)); if (!p.IsFolder()) { LAssert(!"Needs to be a folder."); return; } } if (LTreeItem::Select()) d->ClearFiles(); LString Arg; switch (GetType()) { case VcSvn: case VcHg: Arg = "status"; break; case VcCvs: Arg = "status -l"; break; case VcGit: if (!ToolVersion[VcGit]) LAssert(!"Where is the version?"); // What version did =2 become available? It's definitely not in v2.5.4 // Not in v2.7.4 either... if (ToolVersion[VcGit] >= Ver2Int("2.8.0")) Arg = "-P status --porcelain=2"; else Arg = "-P status --porcelain"; break; default: return; } ParseParams *p = new ParseParams; if (uri && Notify) { p->AltInitPath = uri; p->Leaf = Notify; } else { p->IsWorking = true; } StartCmd(Arg, &VcFolder::ParseStatus, p); switch (GetType()) { case VcHg: CountToTip(); break; default: break; } } void VcFolder::CountToTip() { // if (Path.Equals("C:\\Users\\matthew\\Code\\Lgi\\trunk")) { // LgiTrace("%s: CountToTip, br=%s, idx=%i\n", Path.Get(), CurrentBranch.Get(), (int)CurrentCommitIdx); if (!CurrentBranch) GetBranches(new ParseParams("CountToTip")); else if (CurrentCommitIdx < 0) GetCurrentRevision(new ParseParams("CountToTip")); else { LString Arg; Arg.Printf("id -n -r %s", CurrentBranch.Get()); StartCmd(Arg, &VcFolder::ParseCountToTip); } } } bool VcFolder::ParseCountToTip(int Result, LString s, ParseParams *Params) { switch (GetType()) { case VcHg: if (CurrentCommitIdx >= 0) { auto p = s.Strip(); auto idx = p.Int(); if (idx >= CurrentCommitIdx) { Unpulled = (int) (idx - CurrentCommitIdx); Update(); } } break; default: break; } return false; } void VcFolder::ListWorkingFolder() { if (IsWorkingFld) return; d->ClearFiles(); bool Untracked = d->IsMenuChecked(IDM_UNTRACKED); LString Arg; switch (GetType()) { case VcCvs: if (Untracked) Arg = "-qn update"; else Arg = "-q diff --brief"; break; case VcSvn: Arg = "status"; break; case VcGit: Arg = "-P diff --diff-filter=ACDMRTU"; break; case VcHg: Arg = "status -mard"; break; default: return; } IsWorkingFld = StartCmd(Arg, &VcFolder::ParseWorking); } void VcFolder::GitAdd() { if (!PostAdd) return; LString Args; if (PostAdd->Files.Length() == 0) { LString m(PostAdd->Msg); m = m.Replace("\"", "\\\""); Args.Printf("commit -m \"%s\"", m.Get()); IsCommit = StartCmd(Args, &VcFolder::ParseCommit, PostAdd->Param, LogNormal); PostAdd.Reset(); } else { LString Last = PostAdd->Files.Last(); Args.Printf("add \"%s\"", Last.Replace("\"", "\\\"").Replace("/", DIR_STR).Get()); PostAdd->Files.PopLast(); StartCmd(Args, &VcFolder::ParseGitAdd, NULL, LogNormal); } } bool VcFolder::ParseGitAdd(int Result, LString s, ParseParams *Params) { GitAdd(); return false; } bool VcFolder::ParseCommit(int Result, LString s, ParseParams *Params) { if (LTreeItem::Select()) Select(true); CommitListDirty = Result == 0; CurrentCommit.Empty(); IsCommit = false; if (Result) { switch (GetType()) { case VcGit: { if (s.Find("Please tell me who you are") >= 0) { auto i = new LInput(GetTree(), "", "Git user name:", AppName); i->DoModal([this, i](auto dlg, auto ctrlId) { if (ctrlId) { LString Args; Args.Printf("config --global user.name \"%s\"", i->GetStr().Get()); StartCmd(Args); auto inp = new LInput(GetTree(), "", "Git user email:", AppName); i->DoModal([this, inp](auto dlg, auto ctrlId) { if (ctrlId) { LString Args; Args.Printf("config --global user.email \"%s\"", inp->GetStr().Get()); StartCmd(Args); } delete dlg; }); } delete dlg; }); } break; } default: break; } return false; } if (Result == 0 && LTreeItem::Select()) { d->ClearFiles(); auto *w = d->Diff ? d->Diff->GetWindow() : NULL; if (w) w->SetCtrlName(IDC_MSG, NULL); } switch (GetType()) { case VcGit: { Unpushed++; CommitListDirty = true; Update(); if (Params && Params->Str.Find("Push") >= 0) Push(); break; } case VcSvn: { CurrentCommit.Empty(); CommitListDirty = true; GetTree()->SendNotify((LNotifyType)LvcCommandEnd); if (!Result) { Unpushed = 0; Update(); GetCss(true)->Color(LColour::Green); } break; } case VcHg: { CurrentCommit.Empty(); CommitListDirty = true; GetTree()->SendNotify((LNotifyType)LvcCommandEnd); if (!Result) { Unpushed = 0; Update(); if (Params && Params->Str.Find("Push") >= 0) Push(); else GetCss(true)->Color(LColour::Green); } break; } case VcCvs: { CurrentCommit.Empty(); CommitListDirty = true; GetTree()->SendNotify((LNotifyType)LvcCommandEnd); if (!Result) { Unpushed = 0; Update(); GetCss(true)->Color(LColour::Green); } break; } default: { LAssert(!"Impl me."); break; } } return true; } void VcFolder::Commit(const char *Msg, const char *Branch, bool AndPush) { LArray Add; bool Partial = false; for (auto fp: *d->Files) { VcFile *f = dynamic_cast(fp); if (f) { int c = f->Checked(); if (c > 0) Add.Add(f); else Partial = true; } } if (CurrentBranch && Branch && !CurrentBranch.Equals(Branch)) { int Response = LgiMsg(GetTree(), "Do you want to start a new branch?", AppName, MB_YESNO); if (Response != IDYES) return; LJson j; j.Set("Command", "commit"); j.Set("Msg", Msg); j.Set("AndPush", (int64_t)AndPush); StartBranch(Branch, j.GetJson()); return; } if (!IsCommit) { LString Args; ParseParams *Param = AndPush ? new ParseParams("Push") : NULL; switch (GetType()) { case VcGit: { if (Add.Length() == 0) { break; } else if (Partial) { if (PostAdd.Reset(new GitCommit)) { PostAdd->Files.SetFixedLength(false); for (auto f : Add) PostAdd->Files.Add(f->GetFileName()); PostAdd->Msg = Msg; PostAdd->Branch = Branch; PostAdd->Param = Param; GitAdd(); } } else { LString m(Msg); m = m.Replace("\"", "\\\""); Args.Printf("commit -am \"%s\"", m.Get()); IsCommit = StartCmd(Args, &VcFolder::ParseCommit, Param, LogNormal); } break; } case VcSvn: { LString::Array a; a.New().Printf("commit -m \"%s\"", Msg); for (auto pf: Add) { LString s = pf->GetFileName(); if (s.Find(" ") >= 0) a.New().Printf("\"%s\"", s.Get()); else a.New() = s; } Args = LString(" ").Join(a); IsCommit = StartCmd(Args, &VcFolder::ParseCommit, Param, LogNormal); if (d->Tabs && IsCommit) { d->Tabs->Value(1); GetTree()->SendNotify((LNotifyType)LvcCommandStart); } break; } case VcHg: { LString::Array a; LString CommitMsg = Msg; TmpFile Tmp; if (CommitMsg.Find("\n") >= 0) { Tmp.Create().Write(Msg); a.New().Printf("commit -l \"%s\"", Tmp.GetName()); } else { a.New().Printf("commit -m \"%s\"", Msg); } if (Partial) { for (auto pf: Add) { LString s = pf->GetFileName(); if (s.Find(" ") >= 0) a.New().Printf("\"%s\"", s.Get()); else a.New() = s; } } Args = LString(" ").Join(a); IsCommit = StartCmd(Args, &VcFolder::ParseCommit, Param, LogNormal); if (d->Tabs && IsCommit) { d->Tabs->Value(1); GetTree()->SendNotify((LNotifyType)LvcCommandStart); } break; } case VcCvs: { LString a; a.Printf("commit -m \"%s\"", Msg); IsCommit = StartCmd(a, &VcFolder::ParseCommit, NULL, LogNormal); break; } default: { OnCmdError(NULL, "No commit impl for type."); break; } } } } bool VcFolder::ParseStartBranch(int Result, LString s, ParseParams *Params) { switch (GetType()) { case VcHg: { if (Result == 0 && Params && Params->Str) { LJson j(Params->Str); auto cmd = j.Get("Command"); if (cmd.Equals("commit")) { auto Msg = j.Get("Msg"); auto AndPush = j.Get("AndPush").Int(); if (Msg) { Commit(Msg, NULL, AndPush > 0); } } } break; } default: { OnCmdError(NULL, "No commit impl for type."); break; } } return true; } void VcFolder::StartBranch(const char *BranchName, const char *OnCreated) { if (!BranchName) return; switch (GetType()) { case VcHg: { LString a; a.Printf("branch \"%s\"", BranchName); StartCmd(a, &VcFolder::ParseStartBranch, OnCreated ? new ParseParams(OnCreated) : NULL); break; } default: { OnCmdError(NULL, "No commit impl for type."); break; } } } void VcFolder::Push(bool NewBranchOk) { LString Args; bool Working = false; switch (GetType()) { case VcHg: { auto args = NewBranchOk ? "push --new-branch" : "push"; Working = StartCmd(args, &VcFolder::ParsePush, NULL, LogNormal); break; } case VcGit: { Working = StartCmd("push", &VcFolder::ParsePush, NULL, LogNormal); break; } case VcSvn: { // Nothing to do here.. the commit pushed the data already break; } default: { OnCmdError(NULL, "No push impl for type."); break; } } if (d->Tabs && Working) { d->Tabs->Value(1); GetTree()->SendNotify((LNotifyType)LvcCommandStart); } } bool VcFolder::ParsePush(int Result, LString s, ParseParams *Params) { bool Status = false; if (Result) { if (GetType() == VcHg) { if (s.Find("push creates new remote branches") > 0) { if (LgiMsg(GetTree(), "Push will create a new remote branch. Is that ok?", AppName, MB_YESNO) == IDYES) { Push(true); return false; } } } OnCmdError(s, "Push failed."); } else { switch (GetType()) { case VcGit: break; case VcSvn: break; default: break; } Unpushed = 0; GetCss(true)->Color(LColour::Green); Update(); Status = true; } GetTree()->SendNotify((LNotifyType)LvcCommandEnd); return Status; // no reselect } void VcFolder::Pull(int AndUpdate, LoggingType Logging) { bool Status = false; if (AndUpdate < 0) AndUpdate = GetTree()->GetWindow()->GetCtrlValue(IDC_UPDATE) != 0; switch (GetType()) { case VcNone: return; case VcHg: Status = StartCmd(AndUpdate ? "pull -u" : "pull", &VcFolder::ParsePull, NULL, Logging); break; case VcGit: Status = StartCmd(AndUpdate ? "pull" : "fetch", &VcFolder::ParsePull, NULL, Logging); break; case VcSvn: Status = StartCmd("up", &VcFolder::ParsePull, NULL, Logging); break; default: OnCmdError(NULL, "No pull impl for type."); break; } if (d->Tabs && Status) { d->Tabs->Value(1); GetTree()->SendNotify((LNotifyType)LvcCommandStart); } } bool VcFolder::ParsePull(int Result, LString s, ParseParams *Params) { GetTree()->SendNotify((LNotifyType)LvcCommandEnd); if (Result) { OnCmdError(s, "Pull failed."); return false; } else ClearError(); switch (GetType()) { case VcGit: { // Git does a merge by default, so the current commit changes... CurrentCommit.Empty(); break; } case VcHg: { CurrentCommit.Empty(); auto Lines = s.SplitDelimit("\n"); bool HasUpdates = false; for (auto Ln: Lines) { if (Ln.Find("files updated") < 0) continue; auto Parts = Ln.Split(","); for (auto p: Parts) { auto n = p.Strip().Split(" ", 1); if (n.Length() == 2) { if (n[0].Int() > 0) HasUpdates = true; } } } if (HasUpdates) GetCss(true)->Color(LColour::Green); else GetCss(true)->Color(LCss::ColorInherit); break; } case VcSvn: { // Svn also does a merge by default and can update our current position... CurrentCommit.Empty(); LString::Array a = s.SplitDelimit("\r\n"); for (auto &Ln: a) { if (Ln.Find("At revision") >= 0) { LString::Array p = Ln.SplitDelimit(" ."); CurrentCommit = p.Last(); break; } else if (Ln.Find("svn cleanup") >= 0) { OnCmdError(s, "Needs cleanup"); break; } } if (Params && Params->Str.Equals("log")) { LVariant Limit; d->Opts.GetValue("svn-limit", Limit); LString Args; if (Limit.CastInt32() > 0) Args.Printf("log --limit %i", Limit.CastInt32()); else Args = "log"; IsLogging = StartCmd(Args, &VcFolder::ParseLog); return false; } break; } default: break; } CommitListDirty = true; return true; // Yes - reselect and update } void VcFolder::MergeToLocal(LString Rev) { switch (GetType()) { case VcGit: { LString Args; Args.Printf("merge -m \"Merge with %s\" %s", Rev.Get(), Rev.Get()); StartCmd(Args, &VcFolder::ParseMerge, NULL, LogNormal); break; } case VcHg: { LString Args; Args.Printf("merge -r %s", Rev.Get()); StartCmd(Args, &VcFolder::ParseMerge, NULL, LogNormal); break; } default: LgiMsg(GetTree(), "Not implemented.", AppName); break; } } bool VcFolder::ParseMerge(int Result, LString s, ParseParams *Params) { switch (GetType()) { case VcGit: case VcHg: if (Result == 0) CommitListDirty = true; else OnCmdError(s, "Merge failed."); break; default: LAssert(!"Impl me."); break; } return true; } void VcFolder::Refresh() { CommitListDirty = true; CurrentCommit.Empty(); GitNames.Empty(); Branches.DeleteObjects(); if (Uncommit && Uncommit->LListItem::Select()) Uncommit->Select(true); Select(true); } void VcFolder::Clean() { switch (GetType()) { case VcSvn: StartCmd("cleanup", &VcFolder::ParseClean, NULL, LogNormal); break; default: LgiMsg(GetTree(), "Not implemented.", AppName); break; } } bool VcFolder::ParseClean(int Result, LString s, ParseParams *Params) { switch (GetType()) { case VcSvn: if (Result == 0) GetCss(true)->Color(LCss::ColorInherit); break; default: LAssert(!"Impl me."); break; } return false; } LColour VcFolder::BranchColour(const char *Name) { if (!Name) return GetPaletteColour(0); auto b = Branches.Find(Name); if (!b) // Must be a new one? { int i = 1; for (auto b: Branches) { auto &v = b.value; if (!v->Colour.IsValid()) { if (v->Default) v->Colour = GetPaletteColour(0); else v->Colour = GetPaletteColour(i++); } } Branches.Add(Name, b = new VcBranch(Name)); b->Colour = GetPaletteColour((int)Branches.Length()); } return b ? b->Colour : GetPaletteColour(0); } void VcFolder::CurrentRev(std::function Callback) { LString Cmd; Cmd.Printf("id -i"); RunCmd(Cmd, LogNormal, [Callback](auto r) { if (r.Code == 0) Callback(r.Out.Strip()); }); } bool VcFolder::RenameBranch(LString NewName, LArray &Revs) { switch (GetType()) { case VcHg: { // Update to the ancestor of the commits LHashTbl,int> Refs(0, -1); for (auto c: Revs) { for (auto p:*c->GetParents()) if (Refs.Find(p) < 0) Refs.Add(p, 0); if (Refs.Find(c->GetRev()) >= 0) Refs.Add(c->GetRev(), 1); } LString::Array Ans; for (auto i:Refs) { if (i.value == 0) Ans.Add(i.key); } LArray Ancestors = d->GetRevs(Ans); if (Ans.Length() != 1) { // We should only have one ancestor LString s, m; s.Printf("Wrong number of ancestors: " LPrintfInt64 ".\n", Ans.Length()); for (auto i: Ancestors) { m.Printf("\t%s\n", i->GetRev()); s += m; } LgiMsg(GetTree(), s, AppName, MB_OK); break; } LArray Top; for (auto c:Revs) { for (auto p:*c->GetParents()) if (Refs.Find(p) == 0) Top.Add(c); } if (Top.Length() != 1) { d->Log->Print("Error: Can't find top most commit. (%s:%i)\n", _FL); return false; } // Create the new branch... auto First = Ancestors.First(); LString Cmd; Cmd.Printf("update -r " LPrintfInt64, First->GetIndex()); RunCmd(Cmd, LogNormal, [this, &Cmd, NewName, &Top](auto r) { if (r.Code) { d->Log->Print("Error: Cmd '%s' failed. (%s:%i)\n", Cmd.Get(), _FL); return; } Cmd.Printf("branch \"%s\"", NewName.Get()); RunCmd(Cmd, LogNormal, [this, &Cmd, NewName, &Top](auto r) { if (r.Code) { d->Log->Print("Error: Cmd '%s' failed. (%s:%i)\n", Cmd.Get(), _FL); return; } // Commit it to get a revision point to rebase to Cmd.Printf("commit -m \"Branch: %s\"", NewName.Get()); RunCmd(Cmd, LogNormal, [this, &Cmd, NewName, &Top](auto r) { if (r.Code) { d->Log->Print("Error: Cmd '%s' failed. (%s:%i)\n", Cmd.Get(), _FL); return; } CurrentRev([this, &Cmd, NewName, &Top](auto BranchNode) { // Rebase the old tree to this point Cmd.Printf("rebase -s %s -d %s", Top.First()->GetRev(), BranchNode.Get()); RunCmd(Cmd, LogNormal, [this, &Cmd, NewName, Top](auto r) { if (r.Code) { d->Log->Print("Error: Cmd '%s' failed. (%s:%i)\n", Cmd.Get(), _FL); return; } CommitListDirty = true; d->Log->Print("Finished rename.\n", _FL); }); }); }); }); }); break; } default: { LgiMsg(GetTree(), "Not impl for this VCS.", AppName); break; } } return true; } void VcFolder::GetVersion() { auto t = GetType(); switch (t) { case VcGit: case VcSvn: case VcHg: case VcCvs: StartCmd("--version", &VcFolder::ParseVersion, NULL, LogNormal); break; case VcPending: break; default: OnCmdError(NULL, "No version control found."); break; } } bool VcFolder::ParseVersion(int Result, LString s, ParseParams *Params) { if (Result) return false; auto p = s.SplitDelimit(); switch (GetType()) { case VcGit: { if (p.Length() > 2) { ToolVersion[GetType()] = Ver2Int(p[2]); printf("Git version: %s\n", p[2].Get()); } else LAssert(0); break; } case VcSvn: { if (p.Length() > 2) { ToolVersion[GetType()] = Ver2Int(p[2]); printf("Svn version: %s\n", p[2].Get()); } else LAssert(0); break; } case VcHg: { if (p.Length() >= 5) { auto Ver = p[4].Strip("()"); ToolVersion[GetType()] = Ver2Int(Ver); printf("Hg version: %s\n", Ver.Get()); } break; } case VcCvs: { #ifdef _DEBUG for (auto i : p) printf("i='%s'\n", i.Get()); #endif if (p.Length() > 1) { auto Ver = p[2]; ToolVersion[GetType()] = Ver2Int(Ver); printf("Cvs version: %s\n", Ver.Get()); } break; } default: break; } return false; } bool VcFolder::ParseAddFile(int Result, LString s, ParseParams *Params) { switch (GetType()) { case VcCvs: { if (Result) { d->Tabs->Value(1); OnCmdError(s, "Add file failed"); } else ClearError(); break; } default: break; } return false; } bool VcFolder::AddFile(const char *Path, bool AsBinary) { if (!Path) return false; switch (GetType()) { case VcCvs: { auto p = LString(Path).RSplit(DIR_STR, 1); ParseParams *params = NULL; if (p.Length() >= 2) { if ((params = new ParseParams)) params->AltInitPath = p[0]; } LString a; a.Printf("add%s \"%s\"", AsBinary ? " -kb" : "", p.Length() > 1 ? p.Last().Get() : Path); return StartCmd(a, &VcFolder::ParseAddFile, params); break; } default: { LAssert(!"Impl me."); break; } } return false; } bool VcFolder::ParseRevert(int Result, LString s, ParseParams *Params) { if (GetType() == VcSvn) { if (s.Find("Skipped ") >= 0) Result = 1; // Stupid svn... *sigh* } if (Result) { OnCmdError(s, "Error reverting changes."); } ListWorkingFolder(); return false; } bool VcFolder::Revert(LString::Array &Uris, const char *Revision) { if (Uris.Length() == 0) return false; switch (GetType()) { case VcGit: { LStringPipe p; p.Print("checkout"); for (auto u: Uris) { auto Path = GetFilePart(u); p.Print(" \"%s\"", Path.Get()); } - auto a = p.NewGStr(); + auto a = p.NewLStr(); return StartCmd(a, &VcFolder::ParseRevert); break; } case VcHg: case VcSvn: { LStringPipe p; if (Revision) p.Print("up -r %s", Revision); else p.Print("revert"); for (auto u: Uris) { auto Path = GetFilePart(u); p.Print(" \"%s\"", Path.Get()); } - auto a = p.NewGStr(); + auto a = p.NewLStr(); return StartCmd(a, &VcFolder::ParseRevert); break; } default: { LAssert(!"Impl me."); break; } } return false; } bool VcFolder::ParseResolveList(int Result, LString s, ParseParams *Params) { switch (GetType()) { case VcHg: { auto lines = s.Replace("\r").Split("\n"); for (auto &ln: lines) { auto p = ln.Split(" ", 1); if (p.Length() == 2) { if (p[0].Equals("U")) { auto f = new VcFile(d, this, NULL, true); f->SetText(p[0], COL_STATE); f->SetText(p[1], COL_FILENAME); f->GetStatus(); d->Files->Insert(f); } } } break; } default: { LAssert(!"Impl me."); break; } } return true; } bool VcFolder::ParseResolve(int Result, LString s, ParseParams *Params) { switch (GetType()) { case VcGit: { break; } case VcHg: { d->Log->Print("Resolve: %s\n", s.Get()); break; } default: { LAssert(!"Impl me."); break; } } return true; } bool VcFolder::Resolve(const char *Path, LvcResolve Type) { if (!Path) return false; switch (GetType()) { case VcGit: { LString a; a.Printf("add \"%s\"", Path); return StartCmd(a, &VcFolder::ParseResolve, new ParseParams(Path)); } case VcHg: { LString a; auto local = GetFilePart(Path); switch (Type) { case ResolveMark: a.Printf("resolve -m \"%s\"", local.Get()); break; case ResolveUnmark: a.Printf("resolve -u \"%s\"", local.Get()); break; case ResolveLocal: a.Printf("resolve -t internal:local \"%s\"", local.Get()); break; case ResolveIncoming: a.Printf("resolve -t internal:other \"%s\"", local.Get()); break; default: break; } if (a) return StartCmd(a, &VcFolder::ParseResolve, new ParseParams(Path)); break; } case VcSvn: case VcCvs: default: { LAssert(!"Impl me."); break; } } return false; } bool VcFolder::ParseBlame(int Result, LString s, ParseParams *Params) { new BlameUi(d, GetType(), s); return false; } bool VcFolder::Blame(const char *Path) { if (!Path) return false; LUri u(Path); switch (GetType()) { case VcGit: { LString a; a.Printf("-P blame \"%s\"", u.sPath.Get()); return StartCmd(a, &VcFolder::ParseBlame); break; } case VcHg: { LString a; a.Printf("annotate -un \"%s\"", u.sPath.Get()); return StartCmd(a, &VcFolder::ParseBlame); break; } case VcSvn: { LString a; a.Printf("blame \"%s\"", u.sPath.Get()); return StartCmd(a, &VcFolder::ParseBlame); break; } default: { LAssert(!"Impl me."); break; } } return true; } bool VcFolder::SaveFileAs(const char *Path, const char *Revision) { if (!Path || !Revision) return false; return true; } bool VcFolder::ParseSaveAs(int Result, LString s, ParseParams *Params) { return false; } bool VcFolder::ParseCounts(int Result, LString s, ParseParams *Params) { switch (GetType()) { case VcGit: { Unpushed = (int) s.Strip().Split("\n").Length(); break; } case VcSvn: { int64 ServerRev = 0; bool HasUpdate = false; LString::Array c = s.Split("\n"); for (unsigned i=0; i 1 && a[0].Equals("Status")) ServerRev = a.Last().Int(); else if (a[0].Equals("*")) HasUpdate = true; } if (ServerRev > 0 && HasUpdate) { int64 CurRev = CurrentCommit.Int(); Unpulled = (int) (ServerRev - CurRev); } else Unpulled = 0; Update(); break; } default: { LAssert(!"Impl me."); break; } } IsUpdatingCounts = false; Update(); return false; // No re-select } void VcFolder::SetEol(const char *Path, int Type) { if (!Path) return; switch (Type) { case IDM_EOL_LF: { ConvertEol(Path, false); break; } case IDM_EOL_CRLF: { ConvertEol(Path, true); break; } case IDM_EOL_AUTO: { #ifdef WINDOWS ConvertEol(Path, true); #else ConvertEol(Path, false); #endif break; } } } void VcFolder::UncommitedItem::Select(bool b) { LListItem::Select(b); if (b) { LTreeItem *i = d->Tree->Selection(); VcFolder *f = dynamic_cast(i); if (f) f->ListWorkingFolder(); if (d->Msg) { d->Msg->Name(NULL); auto *w = d->Msg->GetWindow(); if (w) { w->SetCtrlEnabled(IDC_COMMIT, true); w->SetCtrlEnabled(IDC_COMMIT_AND_PUSH, true); } } } } void VcFolder::UncommitedItem::OnPaint(LItem::ItemPaintCtx &Ctx) { LFont *f = GetList()->GetFont(); f->Transparent(false); f->Colour(Ctx.Fore, Ctx.Back); LDisplayString ds(f, "(working folder)"); ds.Draw(Ctx.pDC, Ctx.x1 + ((Ctx.X() - ds.X()) / 2), Ctx.y1 + ((Ctx.Y() - ds.Y()) / 2), &Ctx); } ////////////////////////////////////////////////////////////////////////////////////////// VcLeaf::VcLeaf(VcFolder *parent, LTreeItem *Item, LString uri, LString leaf, bool folder) { Parent = parent; d = Parent->GetPriv(); LAssert(uri.Find("://") >= 0); // Is URI Uri.Set(uri); LAssert(Uri); Leaf = leaf; Folder = folder; Tmp = NULL; Item->Insert(this); if (Folder) { Insert(Tmp = new LTreeItem); Tmp->SetText("Loading..."); } } VcLeaf::~VcLeaf() { Log.DeleteObjects(); } LString VcLeaf::Full() { LUri u = Uri; u += Leaf; return u.ToString(); } void VcLeaf::OnBrowse() { auto full = Full(); LList *Files = d->Files; Files->Empty(); LDirectory Dir; for (int b = Dir.First(full); b; b = Dir.Next()) { if (Dir.IsDir()) continue; VcFile *f = new VcFile(d, Parent, NULL, true); if (f) { f->SetUri(LString("file://") + full); f->SetText(Dir.GetName(), COL_FILENAME); Files->Insert(f); } } Files->ResizeColumnsToContent(); if (Folder) Parent->FolderStatus(full, this); } void VcLeaf::AfterBrowse() { } VcLeaf *VcLeaf::FindLeaf(const char *Path, bool OpenTree) { if (!Stricmp(Path, Full().Get())) return this; if (OpenTree) DoExpand(); VcLeaf *r = NULL; for (auto n = GetChild(); !r && n; n = n->GetNext()) { auto l = dynamic_cast(n); if (l) r = l->FindLeaf(Path, OpenTree); } return r; } void VcLeaf::DoExpand() { if (Tmp) { Tmp->Remove(); DeleteObj(Tmp); Parent->ReadDir(this, Full()); } } void VcLeaf::OnExpand(bool b) { if (b) DoExpand(); } const char *VcLeaf::GetText(int Col) { if (Col == 0) return Leaf; return NULL; } int VcLeaf::GetImage(int Flags) { return Folder ? IcoFolder : IcoFile; } int VcLeaf::Compare(VcLeaf *b) { // Sort folders to the top... if (Folder ^ b->Folder) return (int)b->Folder - (int)Folder; // Then alphabetical return Stricmp(Leaf.Get(), b->Leaf.Get()); } bool VcLeaf::Select() { return LTreeItem::Select(); } void VcLeaf::Select(bool b) { LTreeItem::Select(b); if (b) { d->Commits->RemoveAll(); OnBrowse(); ShowLog(); } } void VcLeaf::ShowLog() { if (Log.Length()) { d->Commits->RemoveAll(); Parent->DefaultFields(); Parent->UpdateColumns(); for (auto i: Log) d->Commits->Insert(i); } } void VcLeaf::OnMouseClick(LMouse &m) { if (m.IsContextMenu()) { LSubMenu s; s.AppendItem("Log", IDM_LOG); s.AppendItem("Blame", IDM_BLAME, !Folder); s.AppendSeparator(); s.AppendItem("Browse To", IDM_BROWSE_FOLDER); s.AppendItem("Terminal At", IDM_TERMINAL); int Cmd = s.Float(GetTree(), m - _ScrollPos()); switch (Cmd) { case IDM_LOG: { Parent->LogFile(Full()); break; } case IDM_BLAME: { Parent->Blame(Full()); break; } case IDM_BROWSE_FOLDER: { LBrowseToFile(Full()); break; } case IDM_TERMINAL: { TerminalAt(Full()); break; } } } } ///////////////////////////////////////////////////////////////////////////////////////// ProcessCallback::ProcessCallback(LString exe, LString args, LString localPath, LTextLog *log, LView *view, std::function callback) : Log(log), View(view), Callback(callback), LThread("ProcessCallback.Thread"), LSubProcess(exe, args) { SetInitFolder(localPath); if (Log) Log->Print("%s %s\n", exe.Get(), args.Get()); Run(); } int ProcessCallback::Main() { if (!Start()) { Ret.Out.Printf("Process failed with %i", GetErrorCode()); Callback(Ret); } else { while (IsRunning()) { auto Rd = Read(); if (Rd.Length()) { Ret.Out += Rd; if (Log) Log->Write(Rd.Get(), Rd.Length()); } } auto Rd = Read(); if (Rd.Length()) { Ret.Out += Rd; if (Log) Log->Write(Rd.Get(), Rd.Length()); } Ret.Code = GetExitValue(); } View->PostEvent(M_HANDLE_CALLBACK, (LMessage::Param)this); return 0; } void ProcessCallback::OnComplete() // Called in the GUI thread... { Callback(Ret); } diff --git a/Lvc/Src/VcFolder.h b/Lvc/Src/VcFolder.h --- a/Lvc/Src/VcFolder.h +++ b/Lvc/Src/VcFolder.h @@ -1,373 +1,373 @@ #ifndef _VcFolder_h_ #define _VcFolder_h_ #include "lgi/common/SubProcess.h" #include class VcLeaf; enum LoggingType { LogNone, // No output from cmd LogNormal, // Output appears as it's available LogSilo, // Output appears after cmd finished (keeps it non-interleaved with other log msgs) }; enum LvcError { ErrNone, ErrSubProcessFailed = GSUBPROCESS_ERROR, }; enum LvcStatus { StatusNone, StatusActive, StatusError, }; enum LvcResolve { ResolveNone, //--- ResolveMark, ResolveUnmark, //--- ResolveLocal, ResolveIncoming, ResolveTool, }; class ReaderThread : public LThread { VersionCtrl Vcs; LStream *Out; LAutoPtr Process; int FilterCount; int OnLine(char *s, ssize_t len); bool OnData(char *Buf, ssize_t &r); public: int Result; ReaderThread(VersionCtrl vcs, LAutoPtr p, LStream *out); ~ReaderThread(); int Main(); }; extern int Ver2Int(LString v); extern int ToolVersion[VcMax]; extern int LstCmp(LListItem *a, LListItem *b, int Col); struct Result { int Code; LString Out; }; struct ProcessCallback : public LThread, public LSubProcess { Result Ret; LView *View = NULL; LTextLog *Log = NULL; std::function Callback; public: ProcessCallback(LString exe, LString args, LString localPath, LTextLog *log, LView *view, std::function callback); int Main(); void OnComplete(); }; struct VcBranch : public LString { bool Default; LColour Colour; LString Hash; VcBranch(LString name, LString hash = NULL) { Default = name.Equals("default") || name.Equals("trunk") || name.Equals("main"); Set(name); if (hash) Hash = hash; } }; struct SshParams { SshConnection *c; VcFolder *f; LString Exe, Args, Path, Output; VersionCtrl Vcs; ParseFn Parser; ParseParams *Params; int ExitCode; SshParams(SshConnection *con) : c(con) { f = NULL; Parser = NULL; Params = NULL; Vcs = VcNone; ExitCode = -1; } }; class VcFolder : public LTreeItem { friend class VcCommit; class Cmd : public LStream { LString::Array Context; LStringPipe Buf; public: LoggingType Logging; LStream *Log; LAutoPtr Rd; ParseFn PostOp; LAutoPtr Params; LvcError Err; Cmd(LString::Array &context, LoggingType logging, LStream *log) { Context = context; Logging = logging; Log = log; Err = ErrNone; } ~Cmd() { } LString GetBuf() { - LString s = Buf.NewGStr(); + LString s = Buf.NewLStr(); if (Log && Logging == LogSilo) { LString m; m.Printf("=== %s ===\n\t%s %s\n", Context[0].Get(), Context[1].Get(), Context[2].Get()); Log->Write(m.Get(), m.Length()); auto Lines = s.Split("\n"); for (auto Ln : Lines) Log->Print("\t%s\n", Ln.Get()); } return s; } ssize_t Write(const void *Ptr, ssize_t Size, int Flags = 0) { ssize_t Wr = Buf.Write(Ptr, Size, Flags); if (Log && Logging == LogNormal) Log->Write(Ptr, Size, Flags); if (Flags) Err = (LvcError) Flags; return Wr; } }; class UncommitedItem : public LListItem { AppPriv *d; public: UncommitedItem(AppPriv *priv) { d = priv; } void OnPaint(LItem::ItemPaintCtx &Ctx); void Select(bool b); }; AppPriv *d; VersionCtrl Type; LUri Uri; LString CurrentCommit, RepoUrl, VcCmd; int64 CurrentCommitIdx; LArray Log; LString CurrentBranch; LHashTbl,VcBranch*> Branches; LAutoPtr Uncommit; LString Cache, NewRev; bool CommitListDirty = 0; int Unpushed = 0, Unpulled = 0; LString CountCache; LTreeItem *Tmp = NULL; int CmdErrors = 0; LArray Fields; // Git specific LHashTbl,LString> GitNames; void AddGitName(LString Hash, LString Name); LString GetGitNames(LString Hash); static int CmdMaxThreads; static int CmdActiveThreads; struct GitCommit { LString::Array Files; LString Msg, Branch; ParseParams *Param; GitCommit() { Param = NULL; } }; LAutoPtr PostAdd; void GitAdd(); LArray Cmds; bool IsLogging, IsUpdate, IsFilesCmd, IsWorkingFld, IsCommit, IsUpdatingCounts; LvcStatus IsBranches, IsIdent; void Init(AppPriv *priv); const char *GetVcName(); bool StartCmd(const char *Args, ParseFn Parser = NULL, ParseParams *Params = NULL, LoggingType Logging = LogNone); bool RunCmd(const char *Args, LoggingType Logging, std::function Callback); void OnBranchesChange(); void OnCmdError(LString Output, const char *Msg); void ClearError(); VcFile *FindFile(const char *Path); void LinkParents(); void CurrentRev(std::function Callback); LColour BranchColour(const char *Name); void Empty(); bool ParseDiffs(LString s, LString Rev, bool IsWorking); bool ParseRevList(int Result, LString s, ParseParams *Params); bool ParseLog(int Result, LString s, ParseParams *Params); bool ParseInfo(int Result, LString s, ParseParams *Params); bool ParseFiles(int Result, LString s, ParseParams *Params); bool ParseWorking(int Result, LString s, ParseParams *Params); bool ParseUpdate(int Result, LString s, ParseParams *Params); bool ParseCommit(int Result, LString s, ParseParams *Params); bool ParseGitAdd(int Result, LString s, ParseParams *Params); bool ParsePush(int Result, LString s, ParseParams *Params); bool ParsePull(int Result, LString s, ParseParams *Params); bool ParseCounts(int Result, LString s, ParseParams *Params); bool ParseRevert(int Result, LString s, ParseParams *Params); bool ParseResolveList(int Result, LString s, ParseParams *Params); bool ParseResolve(int Result, LString s, ParseParams *Params); bool ParseBlame(int Result, LString s, ParseParams *Params); bool ParseSaveAs(int Result, LString s, ParseParams *Params); bool ParseBranches(int Result, LString s, ParseParams *Params); bool ParseStatus(int Result, LString s, ParseParams *Params); bool ParseAddFile(int Result, LString s, ParseParams *Params); bool ParseVersion(int Result, LString s, ParseParams *Params); bool ParseClean(int Result, LString s, ParseParams *Params); bool ParseDiff(int Result, LString s, ParseParams *Params); bool ParseMerge(int Result, LString s, ParseParams *Params); bool ParseCountToTip(int Result, LString s, ParseParams *Params); bool ParseUpdateSubs(int Result, LString s, ParseParams *Params); bool ParseRemoteFind(int Result, LString s, ParseParams *Params); bool ParseStartBranch(int Result, LString s, ParseParams *Params); void DoExpand(); public: VcFolder(AppPriv *priv, const char *uri); VcFolder(AppPriv *priv, LXmlTag *t); ~VcFolder(); VersionCtrl GetType(); AppPriv *GetPriv() { return d; } const char *LocalPath(); LUri GetUri() { return Uri; } VcLeaf *FindLeaf(const char *Path, bool OpenTree); void DefaultFields(); void UpdateColumns(); const char *GetText(int Col); LArray &GetFields() { return Fields; } bool Serialize(LXmlTag *t, bool Write); LXmlTag *Save(); void Select(bool b); void ListCommit(VcCommit *c); void ListWorkingFolder(); void FolderStatus(const char *Path = NULL, VcLeaf *Notify = NULL); void Commit(const char *Msg, const char *Branch, bool AndPush); void StartBranch(const char *BranchName, const char *OnCreated = NULL); void Push(bool NewBranchOk = false); void Pull(int AndUpdate = -1, LoggingType Logging = LogNormal); void Clean(); bool Revert(LString::Array &uris, const char *Revision = NULL); bool Resolve(const char *Path, LvcResolve Type); bool AddFile(const char *Path, bool AsBinary = true); bool Blame(const char *Path); bool SaveFileAs(const char *Path, const char *Revision); void ReadDir(LTreeItem *Parent, const char *Uri); void SetEol(const char *Path, int Type); void GetVersion(); void Diff(VcFile *file); void DiffRange(const char *FromRev, const char *ToRev); void MergeToLocal(LString Rev); bool RenameBranch(LString NewName, LArray &Revs); void Refresh(); bool GetBranches(ParseParams *Params = NULL); void GetCurrentRevision(ParseParams *Params = NULL); void CountToTip(); bool UpdateSubs(); // Clone/checkout any sub-repositries. void LogFile(const char *Path); LString GetFilePart(const char *uri); void FilterCurrentFiles(); void OnPulse(); void OnUpdate(const char *Rev); void OnMouseClick(LMouse &m); void OnRemove(); void OnExpand(bool b); void OnVcsType(LString errorMsg); void OnSshCmd(SshParams *p); }; class VcLeaf : public LTreeItem { AppPriv *d; VcFolder *Parent; bool Folder; LUri Uri; LString Leaf; LTreeItem *Tmp; void DoExpand(); public: LArray Log; VcLeaf(VcFolder *parent, LTreeItem *Item, LString uri, LString leaf, bool folder); ~VcLeaf(); LString Full(); VcLeaf *FindLeaf(const char *Path, bool OpenTree); void OnBrowse(); void AfterBrowse(); void OnExpand(bool b); const char *GetText(int Col); int GetImage(int Flags); int Compare(VcLeaf *b); bool Select(); void Select(bool b); void OnMouseClick(LMouse &m); void ShowLog(); }; #endif diff --git a/ResourceEditor/Code/LgiResApp.cpp b/ResourceEditor/Code/LgiResApp.cpp --- a/ResourceEditor/Code/LgiResApp.cpp +++ b/ResourceEditor/Code/LgiResApp.cpp @@ -1,4726 +1,4726 @@ /* ** FILE: LgiRes.cpp ** AUTHOR: Matthew Allen ** DATE: 3/8/99 ** DESCRIPTION: Lgi Resource Editor ** ** Copyright (C) 1999, Matthew Allen ** fret@memecode.com */ #include #include "LgiResEdit.h" #include "LgiRes_Dialog.h" #include "LgiRes_Menu.h" #include "lgi/common/About.h" #include "lgi/common/TextLabel.h" #include "lgi/common/Edit.h" #include "lgi/common/CheckBox.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/TextView3.h" #include "lgi/common/Token.h" #include "lgi/common/DataDlg.h" #include "lgi/common/Button.h" #include "lgi/common/Menu.h" #include "lgi/common/StatusBar.h" #include "resdefs.h" char AppName[] = "Lgi Resource Editor"; char HelpFile[] = "Help.html"; char OptionsFileName[] = "Options.r"; char TranslationStrMagic[] = "LgiRes.String"; #define VIEW_PULSE_RATE 100 #ifndef DIALOG_X #define DIALOG_X 1.56 #define DIALOG_Y 1.85 #define CTRL_X 1.50 #define CTRL_Y 1.64 #endif enum Ctrls { IDC_HBOX = 100, IDC_VBOX, }; const char *TypeNames[] = { "", "Css", "Dialog", "String", "Menu", 0}; ////////////////////////////////////////////////////////////////////////////// ResFileFormat GetFormat(const char *File) { ResFileFormat Format = Lr8File; char *Ext = LGetExtension(File); if (Ext) { if (stricmp(Ext, "lr") == 0) Format = CodepageFile; else if (stricmp(Ext, "xml") == 0) Format = XmlFile; } return Format; } char *EncodeXml(const char *Str, int Len) { char *Ret = 0; if (Str) { LStringPipe p; const char *s = Str; for (const char *e = Str; e && *e && (Len < 0 || ((e-Str) < Len)); ) { switch (*e) { case '<': { p.Push(s, e-s); p.Push("<"); s = ++e; break; } case '>': { p.Push(s, e-s); p.Push(">"); s = ++e; break; } case '&': { p.Push(s, e-s); p.Push("&"); s = ++e; break; } case '\\': { if (e[1] == 'n') { // Newline p.Push(s, e-s); p.Push("\n"); s = (e += 2); break; } // fall thru } case '\'': case '\"': case '/': { // Convert to entity p.Push(s, e-s); char b[32]; sprintf(b, "&#%i;", *e); p.Push(b); s = ++e; break; } default: { // Regular character e++; break; } } } p.Push(s); Ret = p.NewStr(); } return Ret; } char *DecodeXml(const char *Str, int Len) { if (Str) { LStringPipe p; const char *s = Str; for (const char *e = Str; e && *e && (Len < 0 || ((e-Str) < Len)); ) { switch (*e) { case '&': { // Store string up to here p.Push(s, e-s); e++; if (*e == '#') { // Numerical e++; if (*e == 'x' || *e == 'X') { // Hex e++; char16 c = htoi(e); char *c8 = WideToUtf8(&c, 1); if (c8) { p.Push(c8); DeleteArray(c8); } } else if (isdigit(*e)) { // Decimal char16 c = atoi(e); char *c8 = WideToUtf8(&c, 1); if (c8) { p.Push(c8); DeleteArray(c8); } } else { LAssert(0); } while (*e && *e != ';') e++; } else if (isalpha(*e)) { // named entity const char *Name = e; while (*e && *e != ';') e++; auto Len = e - Name; if (Len == 3 && strnicmp(Name, "amp", Len) == 0) { p.Push("&"); } else if (Len == 2 && strnicmp(Name, "gt", Len) == 0) { p.Push(">"); } else if (Len == 2 && strnicmp(Name, "lt", Len) == 0) { p.Push("<"); } else { // Unsupported entity LAssert(0); } } else { LAssert(0); while (*e && *e != ';') e++; } s = ++e; break; } case '\n': { p.Push(s, e-s); p.Push("\\n"); s = ++e; break; } default: { e++; break; } } } p.Push(s); return p.NewStr(); } return 0; } ////////////////////////////////////////////////////////////////////////////// Resource::Resource(AppWnd *w, int t, bool enabled) { AppWindow = w; ResType = t; Item = 0; SysObject = false; LAssert(AppWindow); } Resource::~Resource() { AppWindow->OnResourceDelete(this); if (Item) { Item->Obj = 0; DeleteObj(Item); } } bool Resource::IsSelected() { return Item?Item->Select():false; } bool Resource::Attach(LViewI *Parent) { LView *w = Wnd(); if (w) { return w->Attach(Parent); } return false; } ////////////////////////////////////////////////////////////////////////////// ResFolder::ResFolder(AppWnd *w, int t, bool enabled) : Resource(w, t, enabled) { Wnd()->Name(""); Wnd()->Enabled(enabled); } ////////////////////////////////////////////////////////////////////////////// ObjTreeItem::ObjTreeItem(Resource *Object) { if ((Obj = Object)) { Obj->Item = this; if (dynamic_cast(Object)) SetImage(ICON_FOLDER); else { int t = Object->Type(); switch (t) { case TYPE_CSS: SetImage(ICON_CSS); break; case TYPE_DIALOG: SetImage(ICON_DIALOG); break; case TYPE_STRING: SetImage(ICON_STRING); break; case TYPE_MENU: SetImage(ICON_MENU); break; } } } } ObjTreeItem::~ObjTreeItem() { if (Obj) { Obj->Item = 0; DeleteObj(Obj); } } const char *ObjTreeItem::GetText(int i) { if (Obj) { int Type = Obj->Type(); if (Type > 0) return Obj->Wnd()->Name(); else return TypeNames[-Type]; } return "#NO_OBJ"; } void ObjTreeItem::OnSelect() { if (Obj) { Obj->App()->OnResourceSelect(Obj); } } void ObjTreeItem::OnMouseClick(LMouse &m) { if (!Obj) return; if (m.IsContextMenu()) { Tree->Select(this); LSubMenu RClick; if (Obj->Wnd()->Enabled()) { if (Obj->Type() > 0) { // Resource RClick.AppendItem("Delete", IDM_DELETE, !Obj->SystemObject()); RClick.AppendItem("Rename", IDM_RENAME, !Obj->SystemObject()); } else { // Folder RClick.AppendItem("New", IDM_NEW, true); RClick.AppendSeparator(); auto Insert = RClick.AppendSub("Import from..."); if (Insert) { Insert->AppendItem("Lgi File", IDM_IMPORT, true); Insert->AppendItem("Win32 Resource Script", IDM_IMPORT_WIN32, false); } } // Custom entries if (!Obj->SystemObject()) { Obj->OnRightClick(&RClick); } } else { RClick.AppendItem("Not implemented", 0, false); } if (Tree->GetMouse(m, true)) { int Cmd = 0; switch (Cmd = RClick.Float(Tree, m.x, m.y)) { case IDM_NEW: { SerialiseContext Ctx; Obj->App()->NewObject(Ctx, 0, -Obj->Type()); break; } case IDM_DELETE: { Obj->App()->SetDirty(true); Obj->App()->DelObject(Obj); break; } case IDM_RENAME: { auto Dlg = new LInput(Tree, GetText(), "Enter the name for the object", "Object Name"); Dlg->DoModal([&](auto dlg, auto id) { if (id) { Obj->Wnd()->Name(Dlg->GetStr()); Update(); Obj->App()->SetDirty(true); } delete dlg; }); break; } case IDM_IMPORT: { auto Select = new LFileSelect(Obj->App()); Select->Type("Text", "*.txt"); Select->Open([&](auto dlg, auto status) { if (status) { LFile F; if (F.Open(dlg->Name(), O_READ)) { SerialiseContext Ctx; Resource *Res = Obj->App()->NewObject(Ctx, 0, -Obj->Type()); if (Res) { // TODO // Res->Read(); } } else { LgiMsg(Obj->App(), "Couldn't open file for reading."); } } delete dlg; }); break; } case IDM_IMPORT_WIN32: { /* List l; if (ImportWin32Dialogs(l, MainWnd)) { for (ResDialog *r = l.First(); r; r = l.Next()) { Obj->App()->InsertObject(TYPE_DIALOG, r); } } */ break; } default: { Obj->OnCommand(Cmd); break; } } } } } ////////////////////////////////////////////////////////////////////////////// FieldView::FieldView(AppWnd *app) : Fields(NextId, true) { NextId = 100; App = app; Source = 0; Ignore = true; SetTabStop(true); Sunken(true); #ifdef WIN32 SetExStyle(GetExStyle() | WS_EX_CONTROLPARENT); #endif } FieldView::~FieldView() { } void FieldView::Serialize(bool Write) { if (!Source) return; Ignore = !Write; Fields.SetMode(Write ? FieldTree::UiToObj : FieldTree::ObjToUi); Fields.SetView(this); Source->Serialize(Fields); /* for (DataDlgField *f=Fields.First(); f; f=Fields.Next()) { LViewI *v; if (GetViewById(f->GetCtrl(), v)) { switch (f->GetType()) { case DATA_STR: { if (Write) // Ctrl -> Options { char *s = v->Name(); Options->Set(f->GetOption(), s); } else // Options -> Ctrl { char *s = 0; Options->Get(f->GetOption(), s); v->Name(s?s:(char*)""); } break; } case DATA_BOOL: case DATA_INT: { if (Write) // Ctrl -> Options { char *s = v->Name(); if (s && (s = strchr(s, '\''))) { s++; char *e = strchr(s, '\''); int i = 0; if (e - s == 4) { memcpy(&i, s, 4); i = LgiSwap32(i); Options->Set(f->GetOption(), i); } } else { int i = v->Value(); Options->Set(f->GetOption(), i); } } else // Options -> Ctrl { int i = 0; Options->Get(f->GetOption(), i); if (i != -1 && (i & 0xff000000) != 0) { char m[8]; i = LgiSwap32(i); sprintf(m, "'%04.4s'", &i); v->Name(m); } else { v->Value(i); } } break; } case DATA_FLOAT: case DATA_PASSWORD: case DATA_STR_SYSTEM: default: { LAssert(0); break; } } } else LAssert(0); } */ Ignore = false; } class TextViewEdit : public LTextView3 { public: bool Multiline; TextViewEdit( int Id, int x, int y, int cx, int cy, LFontType *FontInfo = 0) : LTextView3(Id, x, y, cx, cy, FontInfo) { Multiline = false; #ifdef WIN32 SetDlgCode(DLGC_WANTARROWS | DLGC_WANTCHARS); #endif } bool OnKey(LKey &k) { if (!Multiline && (k.c16 == '\t' || k.c16 == LK_RETURN)) { return false; } return LTextView3::OnKey(k); } }; class Hr : public LView { public: Hr(int x1, int y, int x2) { LRect r(x1, y, x2, y+1); SetPos(r); } void OnPaint(LSurface *pDC) { LRect c = GetClient(); LThinBorder(pDC, c, DefaultSunkenEdge); } bool OnLayout(LViewLayoutInfo &Inf) { if (Inf.Width.Min) Inf.Height.Min = Inf.Height.Max = 2; else Inf.Width.Min = Inf.Width.Max = -1; return true; } }; void FieldView::OnDelete(FieldSource *s) { if (Source != NULL && Source == s) { // Clear fields Source->_FieldView = 0; Fields.Empty(); // remove all children LViewI *c; while ((c = Children[0])) { c->Detach(); DeleteObj(c); } Source = NULL; } } void FieldView::OnSelect(FieldSource *s) { Ignore = true; OnDelete(Source); if (Source) { // Clear fields Source->_FieldView = 0; Fields.Empty(); // remove all children LViewI *c; while ((c = Children[0])) { c->Detach(); DeleteObj(c); } Source = 0; } if (s) { // Add new fields Source = s; Source->_FieldView = AddDispatch(); if (Source->GetFields(Fields)) { LFontType Sys; Sys.GetSystemFont("System"); LTableLayout *t = new LTableLayout(IDC_TABLE); int Row = 0; LLayoutCell *Cell; LArray a; Fields.GetAll(a); for (int i=0; iLength(); n++, Row++) { FieldTree::Field *c = (*b)[n]; switch (c->Type) { case DATA_STR: case DATA_FLOAT: case DATA_INT: case DATA_FILENAME: { Cell = t->GetCell(0, Row); Cell->VerticalAlign(LCss::VerticalMiddle); Cell->Add(new LTextLabel(-1, 0, 0, -1, -1, c->Label)); TextViewEdit *Tv; Cell = t->GetCell(1, Row, true, c->Type == DATA_FILENAME ? 1 : 2); Cell->Add(Tv = new TextViewEdit(c->Id, 0, 0, 100, 20, &Sys)); if (Tv) { Tv->Multiline = c->Multiline; Tv->GetCss(true)->Height(LCss::Len(LCss::LenPx, c->Multiline ? LSysFont->GetHeight() * 8 : LSysFont->GetHeight() + 8)); Tv->SetWrapType(TEXTED_WRAP_NONE); Tv->Sunken(true); } if (c->Type == DATA_FILENAME) { Cell = t->GetCell(2, Row); Cell->Add(new LButton(-c->Id, 0, 0, 21, 21, "...")); } break; } case DATA_BOOL: { Cell = t->GetCell(1, Row, true, 2); Cell->Add(new LCheckBox(c->Id, 0, 0, -1, -1, c->Label)); break; } default: LAssert(!"Impl me."); break; } } if (i < a.Length() - 1) { Cell = t->GetCell(0, Row++, true, 3); Cell->Add(new Hr(0, 0, X()-1)); } } AddView(t); OnPosChange(); AttachChildren(); Invalidate(); } Serialize(false); Ignore = false; } } void FieldView::OnPosChange() { LRect c = GetClient(); c.Inset(6, 6); LViewI *v; if (GetViewById(IDC_TABLE, v)) v->SetPos(c); } LMessage::Result FieldView::OnEvent(LMessage *m) { switch (m->Msg()) { case M_OBJECT_CHANGED: { FieldSource *Src = (FieldSource*)m->A(); if (Src == Source) { Fields.SetMode(FieldTree::ObjToUi); Fields.SetView(this); Serialize(false); } else LAssert(0); break; } } return LLayout::OnEvent(m); } int FieldView::OnNotify(LViewI *Ctrl, LNotification n) { if (!Ignore) { LTextView3 *Tv = dynamic_cast(Ctrl); if (Tv && n.Type == LNotifyCursorChanged) { return 0; } LArray a; Fields.GetAll(a); for (int i=0; iLength(); n++) { FieldTree::Field *c = (*b)[n]; if (c->Id == Ctrl->GetId()) { // Write the value back to the objects Fields.SetMode(FieldTree::UiToObj); Fields.SetView(this); Source->Serialize(Fields); return 0; } else if (c->Id == -Ctrl->GetId()) { auto s = new LFileSelect(this); s->Open([&](auto dlg, auto status) { if (status) { auto File = App->GetCurFile(); if (File) { LFile::Path p = File; p--; auto Rel = LMakeRelativePath(p, dlg->Name()); if (Rel) SetCtrlName(c->Id, Rel); else SetCtrlName(c->Id, dlg->Name()); } else SetCtrlName(c->Id, dlg->Name()); Fields.SetMode(FieldTree::UiToObj); Fields.SetView(this); Source->Serialize(Fields); } delete dlg; }); } } } } return 0; } void FieldView::OnPaint(LSurface *pDC) { pDC->Colour(L_MED); pDC->Rectangle(); } ////////////////////////////////////////////////////////////////////////////// ObjContainer::ObjContainer(AppWnd *w) : LTree(100, 0, 0, 100, 100, "LgiResObjTree") { Window = w; Sunken(true); Insert(Style = new ObjTreeItem( new ResFolder(Window, -TYPE_CSS))); Insert(Dialogs = new ObjTreeItem( new ResFolder(Window, -TYPE_DIALOG))); Insert(Strings = new ObjTreeItem( new ResFolder(Window, -TYPE_STRING))); Insert(Menus = new ObjTreeItem( new ResFolder(Window, -TYPE_MENU))); const char *IconFile = "_icons.gif"; auto f = LFindFile(IconFile); if (f) { Images = LLoadImageList(f, 16, 16); if (Images) SetImageList(Images, false); else LgiTrace("%s:%i - failed to load '%s'\n", _FL, IconFile); } } ObjContainer::~ObjContainer() { DeleteObj(Images); } bool ObjContainer::AppendChildren(ObjTreeItem *Res, List &Lst) { bool Status = true; if (Res) { LTreeItem *Item = Res->GetChild(); while (Item) { ObjTreeItem *i = dynamic_cast(Item); if (i) Lst.Insert(i->GetObj()); else Status = false; Item = Item->GetNext(); } } return Status; } Resource *ObjContainer::CurrentResource() { ObjTreeItem *Item = dynamic_cast(Selection()); if (!Item) return NULL; return Item->GetObj(); } bool ObjContainer::ListObjects(List &Lst) { bool Status = AppendChildren(Style, Lst); Status &= AppendChildren(Dialogs, Lst); Status &= AppendChildren(Strings, Lst); Status &= AppendChildren(Menus, Lst); return Status; } ////////////////////////////////////////////////////////////////////////////// #ifdef WIN32 int Icon = IDI_ICON1; #else const char *Icon = "icon64.png"; #endif AppWnd::AppWnd() : LDocApp(AppName, Icon) { LastRes = 0; Fields = 0; ViewMenu = 0; ContentView = NULL; VBox = NULL; HBox = NULL; ShortCuts = 0; CurLang = -1; ShowLanguages.Add("en", true); if (_Create()) { LVariant Langs; if (GetOptions()->GetValue(OPT_ShowLanguages, Langs)) { ShowLanguages.Empty(); LToken L(Langs.Str(), ","); for (int i=0; iEmpty(); _Destroy(); } void AppWnd::OnCreate() { if (_LoadMenu("IDM_MENU")) { if (_FileMenu) { int n = 6; _FileMenu->AppendSeparator(n++); _FileMenu->AppendItem("Import Win32 Script", IDM_IMPORT_WIN32, true, n++); _FileMenu->AppendItem("Import LgiRes Language", IDM_IMPORT_LANG, true, n++); _FileMenu->AppendItem("Compare To File...", IDM_COMPARE, true, n++); _FileMenu->AppendSeparator(n++); _FileMenu->AppendItem("Properties", IDM_PROPERTIES, true, n++); } ViewMenu = Menu->FindSubMenu(IDM_VIEW); LAssert(ViewMenu); } else LgiTrace("%s:%i - _LoadMenu failed.\n", _FL); Status = 0; StatusInfo[0] = StatusInfo[1] = 0; HBox = new LBox(IDC_HBOX); if (HBox) { HBox->GetCss(true)->Padding("5px"); VBox = new LBox(IDC_VBOX, true); if (VBox) { HBox->AddView(VBox); VBox->AddView(Objs = new ObjContainer(this)); if (Objs) { Objs->AskImage(true); Objs->AskText(true); } VBox->AddView(Fields = new FieldView(this)); VBox->Value(200); } HBox->Value(240); HBox->Attach(this); } DropTarget(true); LString Open; if (LAppInst->GetOption("o", Open)) LoadLgi(Open); } void AppWnd::OnLanguagesChange(LLanguageId Lang, bool Add, bool Update) { bool Change = false; if (Lang) { // Update the list.... bool Has = false; for (int i=0; iId, Lang) == 0) { Has = true; if (!Add) { Languages.DeleteAt(i); Change = true; } break; } } if (Add && !Has) { Change = true; Languages.Add(LFindLang(Lang)); } } // Update the menu... if (ViewMenu && (Change || Update)) { // Remove existing language menu items while (ViewMenu->RemoveItem(2)); // Add new ones int n = 0; for (int i=0; iAppendItem(Lang->Name, IDM_LANG_BASE + n, true); if (Item) { if (CurLang == i) { Item->Checked(true); } } } } } } bool AppWnd::ShowLang(LLanguageId Lang) { return ShowLanguages.Find(Lang) != 0; } void AppWnd::ShowLang(LLanguageId Lang, bool Show) { // Apply change if (Show) { OnLanguagesChange(Lang, true); ShowLanguages.Add(Lang, true); } else { ShowLanguages.Delete(Lang); } // Store the setting for next time LStringPipe p; // const char *L; // for (bool i = ShowLanguages.First(&L); i; i = ShowLanguages.Next(&L)) for (auto i : ShowLanguages) { if (p.GetSize()) p.Push(","); p.Push(i.key); } char *Langs = p.NewStr(); if (Langs) { LVariant v; GetOptions()->SetValue(OPT_ShowLanguages, v = Langs); DeleteArray(Langs); } // Update everything List res; if (ListObjects(res)) { for (auto r: res) { r->OnShowLanguages(); } } } LLanguage *AppWnd::GetCurLang() { if (CurLang >= 0 && CurLang < Languages.Length()) return Languages[CurLang]; return LFindLang("en"); } void AppWnd::SetCurLang(LLanguage *L) { for (int i=0; iId == L->Id) { // Set new current CurLang = i; // Update everything List res; if (ListObjects(res)) { for (auto r: res) { r->OnShowLanguages(); } } break; } } } LArray *AppWnd::GetLanguages() { return &Languages; } class Test : public LView { COLOUR c; public: Test(COLOUR col, int x1, int y1, int x2, int y2) { c = col; LRect r(x1, y1, x2, y2); SetPos(r); _BorderSize = 1; Sunken(true); } void OnPaint(LSurface *pDC) { pDC->Colour(c, 24); pDC->Rectangle(); } }; LMessage::Result AppWnd::OnEvent(LMessage *m) { LMru::OnEvent(m); switch (m->Msg()) { case M_CHANGE: { LAutoPtr note((LNotification*)m->B()); return OnNotify((LViewI*) m->A(), *note); } case M_DESCRIBE: { char *Text = (char*) m->A(); if (Text) { SetStatusText(Text, STATUS_NORMAL); } break; } } return LWindow::OnEvent(m); } void _CountGroup(ResStringGroup *Grp, int &Words, int &Multi) { for (auto s: *Grp->GetStrs()) { if (s->Items.Length() > 1) { Multi++; char *e = s->Get("en"); if (e) { LToken t(e, " "); Words += t.Length(); } } } } int AppWnd::OnCommand(int Cmd, int Event, OsView Handle) { SerialiseContext Ctx; switch (Cmd) { case IDM_SHOW_LANG: { auto Dlg = new ShowLanguagesDlg(this); Dlg->DoModal([](auto dlg, auto ctrlId) { delete dlg; }); break; } case IDM_NEW_CSS: { NewObject(Ctx, 0, TYPE_CSS); break; } case IDM_NEW_DIALOG: { NewObject(Ctx, 0, TYPE_DIALOG); break; } case IDM_NEW_STRING_GRP: { NewObject(Ctx, 0, TYPE_STRING); break; } case IDM_NEW_MENU: { NewObject(Ctx, 0, TYPE_MENU); break; } case IDM_CLOSE: { Empty(); break; } case IDM_IMPORT_WIN32: { LoadWin32(); break; } case IDM_IMPORT_LANG: { ImportLang(); break; } case IDM_COMPARE: { Compare(); break; } case IDM_PROPERTIES: { List l; if (Objs->ListObjects(l)) { int Dialogs = 0; int Strings = 0; int Menus = 0; int Words = 0; int MultiLingual = 0; for (auto r: l) { switch (r->Type()) { case TYPE_DIALOG: { Dialogs++; break; } case TYPE_STRING: { ResStringGroup *Grp = dynamic_cast(r); if (Grp) { Strings += Grp->GetStrs()->Length(); _CountGroup(Grp, Words, MultiLingual); } break; } case TYPE_MENU: { Menus++; ResMenu *Menu = dynamic_cast(r); if (Menu) { if (Menu->Group) { Strings += Menu->Group->GetStrs()->Length(); _CountGroup(Menu->Group, Words, MultiLingual); } } break; } } } LgiMsg( this, "This file contains:\n" "\n" " Dialogs: %i\n" " Menus: %i\n" " Strings: %i\n" " Multi-lingual: %i\n" " Words: %i", AppName, MB_OK, Dialogs, Menus, Strings, MultiLingual, Words); } break; } case IDM_EXIT: { LCloseApp(); break; } case IDM_FIND: { auto s = new Search(this); s->DoModal([&](auto dlg, auto id) { if (id) new Results(this, s); delete dlg; }); break; } case IDM_NEXT: { LgiMsg(this, "Not implemented :(", AppName); break; } case IDM_CUT: { auto Focus = LAppInst->GetFocus(); if (Focus) { Focus->PostEvent(M_CUT); } else { Resource *r = Objs->CurrentResource(); if (r) r->Copy(true); } break; } case IDM_COPY: { auto Focus = LAppInst->GetFocus(); if (Focus) { Focus->PostEvent(M_COPY); } else { Resource *r = Objs->CurrentResource(); if (r) r->Copy(false); } break; } case IDM_PASTE: { auto Focus = LAppInst->GetFocus(); if (Focus) { Focus->PostEvent(M_PASTE); } else { Resource *r = Objs->CurrentResource(); if (r) r->Paste(); } break; } case IDM_TABLELAYOUT_TEST: { OpenTableLayoutTest(this); break; } case IDM_HELP: { char ExeName[MAX_PATH_LEN]; sprintf_s(ExeName, sizeof(ExeName), "%s", LGetExePath().Get()); while (strchr(ExeName, DIR_CHAR) && strlen(ExeName) > 3) { char p[256]; LMakePath(p, sizeof(p), ExeName, "index.html"); if (!LFileExists(p)) { LMakePath(p, sizeof(p), ExeName, "help"); LMakePath(p, sizeof(p), p, "index.html"); } if (LFileExists(p)) { LExecute(HelpFile, NULL, ExeName); break; } LTrimDir(ExeName); } break; } case IDM_SHOW_SHORTCUTS: { if (!ShortCuts) ShortCuts = new ShortCutView(this); break; } case IDM_ABOUT: { LAbout Dlg( this, AppName, APP_VER, "\nLgi Resource Editor (lr8 files).", "icon64.png", "http://www.memecode.com/lgi/res", "fret@memecode.com"); break; } default: { int Idx = Cmd - IDM_LANG_BASE; if (Idx >= 0 && Idx < Languages.Length()) { // Deselect the old lang auto Item = ViewMenu ? ViewMenu->ItemAt(CurLang + 2) : 0; if (Item) { Item->Checked(false); } // Set the current CurLang = Idx; // Set the new lang's menu item Item = ViewMenu ? ViewMenu->ItemAt(CurLang + 2) : 0; if (Item) { Item->Checked(true); } // Update everything List res; if (ListObjects(res)) { for (auto r: res) { r->OnShowLanguages(); } } } break; } } return LDocApp::OnCommand(Cmd, Event, Handle); } int AppWnd::OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { default: { break; } } return 0; } void AppWnd::FindStrings(List &Strs, char *Define, int *CtrlId) { if (Objs) { List l; if (Objs->ListObjects(l)) { for (auto r: l) { StringList *s = r->GetStrs(); if (s) { for (auto Str: *s) { if (Define && ValidStr(Str->GetDefine())) { if (strcmp(Define, Str->GetDefine()) == 0) { Strs.Insert(Str); continue; } } if (CtrlId) { if (*CtrlId == Str->GetId()) { Strs.Insert(Str); continue; } } } } } } } } int AppWnd::GetUniqueCtrlId() { int Max = 0; if (Objs) { List l; if (Objs->ListObjects(l)) { LHashTbl, int> t; for (auto r: l) { StringList *sl = r->GetStrs(); if (sl) { for (auto s: *sl) { if (s->GetId() > 0 && !t.Find(s->GetId())) { t.Add(s->GetId(), s->GetId()); } Max = MAX(s->GetId(), Max); } } } int i = 500; while (true) { if (t.Find(i)) { i++; } else { return i; } } } } return Max + 1; } int AppWnd::GetUniqueStrRef(int Start) { if (!Objs) return -1; List l; if (!Objs->ListObjects(l)) return -1; LHashTbl, ResString*> Map; LArray Dupes; for (auto r: l) { ResStringGroup *Grp = r->GetStringGroup(); if (Grp) { List::I it = Grp->GetStrs()->begin(); for (ResString *s = *it; s; s = *++it) { if (s->GetRef()) { ResString *Existing = Map.Find(s->GetRef()); if (Existing) { // These get their ref's reset to a unique value as a side // effect of this function... Dupes.Add(s); } else { Map.Add(s->GetRef(), s); } } else { // auto Idx = Grp->GetStrs()->IndexOf(s); LAssert(!"No string ref?"); } } } } for (int i=Start; true; i++) { if (!Map.Find(i)) { if (Dupes.Length()) { ResString *s = Dupes[0]; Dupes.DeleteAt(0); s->SetRef(i); SetDirty(true); } else { return i; } } } return -1; } ResString *AppWnd::GetStrFromRef(int Ref) { ResString *Str = 0; if (Objs) { List l; if (Objs->ListObjects(l)) { for (auto r: l) { ResStringGroup *Grp = dynamic_cast(r); if (Grp) { if ((Str = Grp->FindRef(Ref))) break; } } } } return Str; } ResStringGroup *AppWnd::GetDialogSymbols() { if (Objs) { List l; if (Objs->ListObjects(l)) { for (auto r: l) { ResStringGroup *Grp = dynamic_cast(r); if (Grp) { auto ObjName = Grp->Wnd()->Name(); if (ObjName && stricmp(ObjName, StrDialogSymbols) == 0) { return Grp; } } } } } return NULL; } void AppWnd::OnReceiveFiles(LArray &Files) { auto f = Files.Length() ? Files[0] : 0; if (f) { _OpenFile(f, false); } } void AppWnd::SetStatusText(char *Text, int Pane) { if (Pane >= 0 && Pane < STATUS_MAX && StatusInfo[Pane]) { StatusInfo[Pane]->Name(Text); } } Resource *AppWnd::NewObject(SerialiseContext ctx, LXmlTag *load, int Type, bool Select) { Resource *r = 0; ObjTreeItem *Dir = 0; switch (Type) { case TYPE_CSS: { r = new ResCss(this); Dir = Objs->Style; break; } case TYPE_DIALOG: { r = new ResDialog(this); Dir = Objs->Dialogs; break; } case TYPE_STRING: { r = new ResStringGroup(this); Dir = Objs->Strings; break; } case TYPE_MENU: { r = new ResMenu(this); Dir = Objs->Menus; break; } } if (r) { ObjTreeItem *Item = new ObjTreeItem(r); if (Item) { Dir->Insert(Item); Dir->Update(); Dir->Expanded(true); if (Select) { Objs->Select(Item); } } r->Create(load, &ctx); if (Item) { Item->Update(); } SetDirty(true); } return r; } bool AppWnd::InsertObject(int Type, Resource *r, bool Select) { bool Status = false; if (r) { ObjTreeItem *Dir = 0; switch (Type) { case TYPE_CSS: { Dir = Objs->Style; break; } case TYPE_DIALOG: { Dir = Objs->Dialogs; break; } case TYPE_STRING: { Dir = Objs->Strings; break; } case TYPE_MENU: { Dir = Objs->Menus; break; } } if (Dir) { ObjTreeItem *Item = new ObjTreeItem(r); if (Item) { const char *Name = Item->GetText(); r->Item = Item; Dir->Insert(Item, (Name && Name[0] == '_') ? 0 : -1); Dir->Update(); Dir->Expanded(true); if (Select) { Objs->Select(Item); } Status = true; } } } return Status; } void AppWnd::DelObject(Resource *r) { OnResourceSelect(0); DeleteObj(r); } ObjTreeItem *GetTreeItem(LTreeItem *ti, Resource *r) { for (LTreeItem *i=ti->GetChild(); i; i=i->GetNext()) { ObjTreeItem *o = dynamic_cast(i); if (o) { if (o->GetObj() == r) return o; } o = GetTreeItem(i, r); if (o) return o; } return 0; } ObjTreeItem *GetTreeItem(LTree *ti, Resource *r) { for (LTreeItem *i=ti->GetChild(); i; i=i->GetNext()) { ObjTreeItem *o = dynamic_cast(i); if (o) { if (o->GetObj() == r) return o; } o = GetTreeItem(i, r); if (o) return o; } return 0; } void AppWnd::GotoObject(ResString *s, ResStringGroup *g, ResDialog *d, ResMenuItem *m, ResDialogCtrl *c) { if (s) { Resource *Res = 0; if (g) { Res = g; } else if (d) { Res = d; } else if (m) { Res = m->GetMenu(); } if (Res) { ObjTreeItem *ti = GetTreeItem(Objs, Res); if (ti) { ti->Select(true); if (g) { s->GetList()->Select(0); s->ScrollTo(); LYield(); s->Select(true); } else if (d) { LYield(); d->SelectCtrl(c); } else if (m) { for (LTreeItem *i=m; i; i=i->GetParent()) { i->Expanded(true); } m->Select(true); m->ScrollTo(); } } else { printf("%s:%i - couldn't find resources tree item\n", _FL); } } } } bool AppWnd::ListObjects(List &Lst) { if (Objs) { return Objs->ListObjects(Lst); } return false; } void AppWnd::OnObjChange(FieldSource *r) { if (Fields) { Fields->Serialize(false); SetDirty(true); } } void AppWnd::OnObjSelect(FieldSource *r) { if (Fields) Fields->OnSelect(r); } void AppWnd::OnObjDelete(FieldSource *r) { if (Fields) { Fields->OnDelete(r); } } void AppWnd::OnResourceDelete(Resource *r) { - auto v = GetShortCutView(); - if (v) - v->OnResource(NULL); + auto v = GetShortCutView(); + if (v) + v->OnResource(NULL); } void AppWnd::OnResourceSelect(Resource *r) { if (LastRes) { OnObjSelect(NULL); if (ContentView) { ContentView->Detach(); DeleteObj(ContentView); } LastRes = NULL; } if (r) { ContentView = r->CreateUI(); if (ContentView) { if (HBox) ContentView->Attach(HBox); LastRes = r; } - auto v = GetShortCutView(); - if (v) - v->OnResource(r); + auto v = GetShortCutView(); + if (v) + v->OnResource(r); } } char *TagName(LXmlTag *t) { static char Buf[1024]; LArray Tags; for (; t; t = t->Parent) { Tags.AddAt(0, t); } Buf[0] = 0; for (int i=0; iGetTag()); } return Buf; } class ResCompare : public LWindow, public LResourceLoad { LList *Lst; public: ResCompare(const char *File1, const char *File2) { Lst = 0; LRect p; LAutoString n; if (LoadFromResource(IDD_COMPARE, this, &p, &n)) { SetPos(p); Name(n); MoveToCenter(); GetViewById(IDC_DIFFS, Lst); if (Attach(0)) { Visible(true); AttachChildren(); LXmlTag *t1 = new LXmlTag; LXmlTag *t2 = new LXmlTag; if (t1 && File1) { LFile f; if (f.Open(File1, O_READ)) { LXmlTree x(GXT_NO_ENTITIES); if (!x.Read(t1, &f, 0)) { DeleteObj(t1); } } else { DeleteObj(t1); } } if (t2 && File2) { LFile f; if (f.Open(File2, O_READ)) { LXmlTree x(GXT_NO_ENTITIES); if (!x.Read(t2, &f, 0)) { DeleteObj(t2); } } else { DeleteObj(t2); } } if (Lst && t1 && t2) { Lst->Enabled(false); Compare(t1, t2); Lst->Enabled(true); } DeleteObj(t1); DeleteObj(t2); } } } void Compare(LXmlTag *t1, LXmlTag *t2) { char s[1024]; if (stricmp(t1->GetTag(), t2->GetTag()) != 0) { sprintf(s, "Different Tag: '%s' <-> '%s'", t1->GetTag(), t2->GetTag()); LListItem *i = new LListItem; if (i) { i->SetText(s); i->SetText(TagName(t1), 1); Lst->Insert(i); } } LHashTbl,LXmlAttr*> a; for (int i=0; iAttr.Length(); i++) { LXmlAttr *a1 = &t1->Attr[i]; a.Add(a1->GetName(), a1); } for (int n=0; nAttr.Length(); n++) { LXmlAttr *a2 = &t2->Attr[n]; LXmlAttr *a1 = (LXmlAttr*) a.Find(a2->GetName()); if (a1) { if (strcmp(a1->GetValue(), a2->GetValue()) != 0) { sprintf(s, "Different Attr Value: '%s' <-> '%s'", a1->GetValue(), a2->GetValue()); LListItem *i = new LListItem; if (i) { i->SetText(s); sprintf(s, "%s.%s", TagName(t1), a1->GetName()); i->SetText(s, 1); Lst->Insert(i); } } a.Delete(a2->GetName()); } else { sprintf(s, "[Right] Missing Attr: '%s' = '%s'", a2->GetName(), a2->GetValue()); LListItem *i = new LListItem; if (i) { i->SetText(s); i->SetText(TagName(t1), 1); Lst->Insert(i); } } } // char *Key; // for (void *v = a.First(&Key); v; v = a.Next(&Key)) for (auto v : a) { LXmlAttr *a1 = v.value; sprintf(s, "[Left] Missing Attr: '%s' = '%s'", a1->GetName(), a1->GetValue()); LListItem *i = new LListItem; if (i) { i->SetText(s); i->SetText(TagName(t1), 1); Lst->Insert(i); } } if (t1->IsTag("string-group")) { LArray r1, r2; for (auto t: t1->Children) { char *Ref; if ((Ref = t->GetAttr("ref"))) { int r = atoi(Ref); if (r) { r1[r] = t; } } } for (auto t: t2->Children) { char *Ref; if ((Ref = t->GetAttr("ref"))) { int r = atoi(Ref); if (r) { r2[r] = t; } } } auto Max = MAX(r1.Length(), r2.Length()); for (int i = 0; iGetAttr("ref"), r1[i]->GetAttr("Define")); LListItem *n = new LListItem; if (n) { n->SetText(s); n->SetText(TagName(r1[i]), 1); Lst->Insert(n); } } else if (r2[i]) { sprintf(s, "[Left] Missing String: Ref=%s, Def=%s", r2[i]->GetAttr("ref"), r2[i]->GetAttr("Define")); LListItem *n = new LListItem; if (n) { n->SetText(s); n->SetText(TagName(r2[i]), 1); Lst->Insert(n); } } } } else { LXmlTag *c1 = t1->Children[0]; LXmlTag *c2 = t2->Children[0]; while (c1 && c2) { Compare(c1, c2); c1 = t1->Children[0]; c2 = t2->Children[0]; } } LYield(); } void OnPosChange() { LRect c = GetClient(); if (Lst) { c.Inset(7, 7); Lst->SetPos(c); } } }; void AppWnd::Compare() { auto s = new LFileSelect(this); s->Type("Lgi Resource", "*.lr8"); s->Open([&](auto dlg, auto status) { if (status) new ResCompare(GetCurFile(), dlg->Name()); delete dlg; }); } void AppWnd::ImportLang() { // open dialog auto Select = new LFileSelect(this); Select->Type("Lgi Resources", "*.lr8;*.xml"); Select->Open([&](auto dlg, auto status) { if (status) { LFile F; if (F.Open(dlg->Name(), O_READ)) { SerialiseContext Ctx; Ctx.Format = GetFormat(dlg->Name()); // convert file to Xml objects LXmlTag *Root = new LXmlTag; if (Root) { LXmlTree Tree(GXT_NO_ENTITIES); if (Tree.Read(Root, &F, 0)) { List Menus; List Groups; for (auto t: Root->Children) { if (t->IsTag("menu")) { ResMenu *Menu = new ResMenu(this); if (Menu && Menu->Read(t, Ctx)) { Menus.Insert(Menu); } else break; } else if (t->IsTag("string-group")) { ResStringGroup *g = new ResStringGroup(this); if (g && g->Read(t, Ctx)) { Groups.Insert(g); } else break; } } Ctx.PostLoad(this); bool HasData = false; for (auto g: Groups) { g->SetLanguages(); if (g->GetStrs()->Length() > 0 && g->GetLanguages() > 0) { HasData = true; } } if (HasData) { List Langs; for (auto g: Groups) { for (int i=0; iGetLanguages(); i++) { LLanguage *Lang = g->GetLanguage(i); if (Lang) { bool Has = false; for (auto l: Langs) { if (stricmp((char*)l, (char*)Lang) == 0) { Has = true; break; } } if (!Has) { Langs.Insert(Lang); } } } } auto Dlg = new LangDlg(this, Langs); Dlg->DoModal([&](auto dlg, auto id) { if (id == IDOK && Dlg->Lang) { LStringPipe Errors; int Matches = 0; int NotFound = 0; int Imported = 0; int Different = 0; for (auto g: Groups) { List::I Strings = g->GetStrs()->begin(); for (ResString *s=*Strings; s; s=*++Strings) { ResString *d = GetStrFromRef(s->GetRef()); if (d) { Matches++; char *Str = s->Get(Dlg->Lang->Id); char *Dst = d->Get(Dlg->Lang->Id); if ( ( Str && Dst && strcmp(Dst, Str) != 0 ) || ( (Str != 0) ^ (Dst != 0) ) ) { Different++; d->Set(Str, Dlg->Lang->Id); Imported++; } } else { NotFound++; char e[256]; sprintf(e, "String ref=%i (%s)\n", s->GetRef(), s->GetDefine()); Errors.Push(e); } } } List Lst; if (ListObjects(Lst)) { for (auto m: Menus) { // find matching menu in our list ResMenu *Match = 0; for (auto r: Lst) { ResMenu *n = dynamic_cast(r); if (n && stricmp(n->Name(), m->Name()) == 0) { Match = n; break; } } if (Match) { // match strings List *Src = m->GetStrs(); List *Dst = Match->GetStrs(); for (auto s: *Src) { bool FoundRef = false; for (auto d: *Dst) { if (s->GetRef() == d->GetRef()) { FoundRef = true; char *Str = s->Get(Dlg->Lang->Id); if (Str) { char *Dst = d->Get(Dlg->Lang->Id); if (!Dst || strcmp(Dst, Str)) { Different++; } d->Set(Str, Dlg->Lang->Id); Imported++; } break; } } if (!FoundRef) { NotFound++; char e[256]; sprintf(e, "MenuString ref=%i (%s)\n", s->GetRef(), s->GetDefine()); Errors.Push(e); } } Match->SetLanguages(); } } for (auto r: Lst) { ResStringGroup *StrRes = dynamic_cast(r); if (StrRes) { StrRes->SetLanguages(); } } } char *ErrorStr = Errors.NewStr(); LgiMsg( this, "Imported: %i\n" "Matched: %i\n" "Not matched: %i\n" "Different: %i\n" "Total: %i\n" "\n" "Import complete.\n" "\n%s", AppName, MB_OK, Imported, Matches, NotFound, Different, Matches + NotFound, ErrorStr?ErrorStr:(char*)""); } delete dlg; }); } else { LgiMsg(this, "No language information to import", AppName, MB_OK); } // Groups.DeleteObjects(); // Menus.DeleteObjects(); } else { LgiMsg(this, "Failed to parse XML from file.\nError: %s", AppName, MB_OK, Tree.GetErrorMsg()); } DeleteObj(Root); } } } delete dlg; }); } bool AppWnd::Empty() { // Delete any existing objects List l; if (ListObjects(l)) { for (auto It = l.begin(); It != l.end(); ) { auto r = *It; if (r->SystemObject()) l.Delete(It); else It++; } for (auto r: l) { DelObject(r); } } return true; } bool AppWnd::OpenFile(const char *FileName, bool Ro) { if (stristr(FileName, ".lr8") || stristr(FileName, ".xml")) { return LoadLgi(FileName); } else if (stristr(FileName, ".rc")) { LoadWin32(FileName); return true; } return false; } bool AppWnd::SaveFile(const char *FileName) { if (stristr(FileName, ".lr8") || stristr(FileName, ".xml")) { return SaveLgi(FileName); } else if (stristr(FileName, ".rc")) { } return false; } void AppWnd::GetFileTypes(LFileSelect *Dlg, bool Write) { Dlg->Type("Lgi Resources", "*.lr8;*.xml"); if (!Write) { Dlg->Type("All Files", LGI_ALL_FILES); } } // Lgi load/save bool AppWnd::TestLgi(bool Quite) { bool Status = true; List l; if (ListObjects(l)) { ErrorCollection Errors; for (auto r: l) { Status &= r->Test(&Errors); } if (Errors.StrErr.Length() > 0) { LStringPipe Sample; for (int i=0; iGetRef(), s->GetDefine(), Errors.StrErr[i].Msg.Get()); } char *Sam = Sample.NewStr(); LgiMsg(this, "%i strings have errors.\n\n%s", AppName, MB_OK, Errors.StrErr.Length(), Sam); DeleteArray(Sam); } else if (!Quite) { LgiMsg(this, "Object are all ok.", AppName); } } return Status; } #define PROFILE_LOAD 0 #if PROFILE_LOAD #define PROF(s) prof.Add(s) #else #define PROF(s) #endif bool AppWnd::LoadLgi(const char *FileName) { #if PROFILE_LOAD LProfile prof("LoadLgi"); #endif Empty(); if (!FileName) return false; PROF("fOpen"); LFile f; if (!f.Open(FileName, O_READ)) return false; PROF("prog"); LAutoPtr Progress(new LProgressDlg(this)); Progress->SetDescription("Initializing..."); Progress->SetType("Tags"); LAutoPtr Root(new LXmlTag); if (!Root) return false; // convert file to Xml objects LXmlTree Xml(0); Progress->SetDescription("Lexing..."); PROF("xml.read"); if (!Xml.Read(Root, &f, 0)) { LgiMsg(this, "Xml read failed: %s", AppName, MB_OK, Xml.GetErrorMsg()); return false; } Progress->SetRange(Root->Children.Length()); PROF("xml to objs"); // convert Xml list into objects SerialiseContext Ctx; for (auto t: Root->Children) { Progress->Value(Root->Children.IndexOf(t)); int RType = 0; if (t->IsTag("dialog")) RType = TYPE_DIALOG; else if (t->IsTag("string-group")) RType = TYPE_STRING; else if (t->IsTag("menu")) RType = TYPE_MENU; else if (t->IsTag("style")) RType = TYPE_CSS; else LAssert(!"Unexpected tag"); if (RType > 0) NewObject(Ctx, t, RType, false); } PROF("postload"); Ctx.PostLoad(this); PROF("sort"); SortDialogs(); PROF("test"); TestLgi(); PROF("scan langs"); // Scan for languages and update the view lang menu Languages.Length(0); LHashTbl, LLanguage*> Langs; if (!ViewMenu) return false; PROF("remove menu items"); // Remove existing language menu items while (ViewMenu->RemoveItem(1)); ViewMenu->AppendSeparator(); PROF("enum objs"); // Enumerate all languages List res; if (ListObjects(res)) { for (auto r: res) { ResStringGroup *Sg = r->IsStringGroup(); if (Sg) { for (int i=0; iGetLanguages(); i++) { LLanguage *Lang = Sg->GetLanguage(i); if (Lang) { Langs.Add(Lang->Id, Lang); } } } } } PROF("update langs"); // Update languages array int n = 0; for (auto i : Langs) { Languages.Add(i.value); auto Item = ViewMenu->AppendItem(i.value->Name, IDM_LANG_BASE + n, true); if (Item && i.value->IsEnglish()) { Item->Checked(true); CurLang = n; } n++; } PROF("none menu"); if (Languages.Length() == 0) ViewMenu->AppendItem("(none)", -1, false); return true; } void SerialiseContext::PostLoad(AppWnd *App) { for (int i=0; iGetUniqueCtrlId(); s->SetId(Id); Log.Print("Repaired CtrlId of string ref %i to %i\n", s->GetRef(), Id); } LAutoString a(Log.NewStr()); if (ValidStr(a)) { LgiMsg(App, "%s", "Load Warnings", MB_OK, a.Get()); } } int DialogNameCompare(ResDialog *a, ResDialog *b, NativeInt Data) { const char *A = (a)?a->Name():0; const char *B = (b)?b->Name():0; if (A && B) return stricmp(A, B); return -1; } void AppWnd::SortDialogs() { List Lst; if (ListObjects(Lst)) { List Dlgs; for (auto r: Lst) { ResDialog *Dlg = dynamic_cast(r); if (Dlg) { Dlgs.Insert(Dlg); Dlg->Item->Remove(); } } Dlgs.Sort(DialogNameCompare); for (auto d: Dlgs) { Objs->Dialogs->Insert(d->Item); } } } class ResTreeNode { public: char *Str; ResTreeNode *a, *b; ResTreeNode(char *s) { a = b = 0; Str = s; } ~ResTreeNode() { DeleteArray(Str); DeleteObj(a); DeleteObj(b); } void Enum(List &l) { if (a) { a->Enum(l); } if (Str) { l.Insert(Str); } if (b) { b->Enum(l); } } bool Add(char *s) { int Comp = (Str && s) ? stricmp(Str, s) : -1; if (Comp == 0) { return false; } if (Comp < 0) { if (a) { return a->Add(s); } else { a = new ResTreeNode(s); } } if (Comp > 0) { if (b) { return b->Add(s); } else { b = new ResTreeNode(s); } } return true; } }; class ResTree { ResTreeNode *Root; public: ResTree() { Root = 0; } ~ResTree() { DeleteObj(Root); } bool Add(char *s) { if (s) { if (!Root) { Root = new ResTreeNode(NewStr(s)); return true; } else { return Root->Add(NewStr(s)); } } return false; } void Enum(List &l) { if (Root) { Root->Enum(l); } } }; const char *HeaderStr = "// This file generated by LgiRes\r\n\r\n"; struct DefinePair { char *Name; int Value; }; int PairCmp(DefinePair *a, DefinePair *b) { return a->Value - b->Value; } bool AppWnd::WriteDefines(LStream &Defs) { bool Status = false; ResTree Tree; // Empty file Defs.Write(HeaderStr, strlen(HeaderStr)); // make a unique list of #define's List Lst; if (ListObjects(Lst)) { LHashTbl,int> Def; LHashTbl,char*> Ident; for (auto r: Lst) { List *StrList = r->GetStrs(); if (StrList) { Status = true; List::I sl = StrList->begin(); for (ResString *s = *sl; s; s = *++sl) { if (ValidStr(s->GetDefine())) { if (stricmp(s->GetDefine(), "IDOK") == 0) { s->SetId(IDOK); } else if (stricmp(s->GetDefine(), "IDCANCEL") == 0) { s->SetId(IDCANCEL); } else if (stricmp(s->GetDefine(), "IDC_STATIC") == 0) { s->SetId(-1); } else if (stricmp(s->GetDefine(), "-1") == 0) { s->SetDefine(0); } else { // Remove dupe ID's char IdStr[32]; sprintf(IdStr, "%i", s->GetId()); char *Define; if ((Define = Ident.Find(IdStr))) { if (strcmp(Define, s->GetDefine())) { List n; FindStrings(n, s->GetDefine()); int NewId = GetUniqueCtrlId(); for (auto Ns: n) { Ns->SetId(NewId); } } } else { Ident.Add(IdStr, s->GetDefine()); } // Make all define's the same int CtrlId; if ((CtrlId = Def.Find(s->GetDefine()))) { // Already there... s->SetId(CtrlId); } else { // Add... LAssert(s->GetId()); if (s->GetId()) Def.Add(s->GetDefine(), s->GetId()); } } } } } } // write the list out LArray Pairs; // char *s = 0; // for (int i = Def.First(&s); i; i = Def.Next(&s)) for (auto i : Def) { if (ValidStr(i.key) && stricmp(i.key, "IDOK") != 0 && stricmp(i.key, "IDCANCEL") != 0 && stricmp(i.key, "IDC_STATIC") != 0 && stricmp(i.key, "-1") != 0) { DefinePair &p = Pairs.New(); p.Name = i.key; p.Value = i.value; } } Pairs.Sort(PairCmp); for (int n=0; n=' ' && (uint8_t)(c) <= 127) if (IsPrintable(s[0]) && IsPrintable(s[1]) && IsPrintable(s[2]) && IsPrintable(s[3])) { #ifndef __BIG_ENDIAN__ int32 i = LgiSwap32(p.Value); memcpy(s, &i, 4); #endif Defs.Print("#define %s%s'%04.4s'\r\n", p.Name, Tab, s); } else Defs.Print("#define %s%s%i\r\n", p.Name, Tab, p.Value); } } return Status; } bool AppWnd::SaveLgi(const char *FileName) { bool Status = false; if (!TestLgi()) { if (LgiMsg(this, "Do you want to save the file with errors?", AppName, MB_YESNO) == IDNO) return false; } // Rename the existing file to 'xxxxxx.bak' if (LFileExists(FileName)) { char Bak[MAX_PATH_LEN]; strcpy_s(Bak, sizeof(Bak), FileName); char *e = LGetExtension(Bak); if (e) { strcpy(e, "bak"); if (LFileExists(Bak)) FileDev->Delete(Bak, false); FileDev->Move(FileName, Bak); } } // Save the file to xml if (FileName) { LFile f; LFile::Path DefsName = FileName; DefsName += "../resdefs.h"; LStringPipe Defs; if (f.Open(FileName, O_WRITE)) { SerialiseContext Ctx; f.SetSize(0); Defs.SetSize(0); Defs.Print("// Generated by LgiRes\r\n\r\n"); List l; if (ListObjects(l)) { // Remove all duplicate symbol Id's from the dialogs for (auto r: l) { ResDialog *Dlg = dynamic_cast(r); if (Dlg) Dlg->CleanSymbols(); } // write defines WriteDefines(Defs); LXmlTag Root("resources"); // Write all string lists out first so that when we load objects // back in again the strings will already be loaded and can // be referenced for (auto r: l) { if (r->Type() == TYPE_STRING) { LXmlTag *c = new LXmlTag; if (c && r->Write(c, Ctx)) { Root.InsertTag(c); } else { LAssert(0); DeleteObj(c); } } } // now write the rest of the objects out for (auto r: l) { if (r->Type() != TYPE_STRING) { LXmlTag *c = new LXmlTag; if (c && r->Write(c, Ctx)) { Root.InsertTag(c); } else { r->Write(c, Ctx); LAssert(0); DeleteObj(c); } } } // Set the offset type. // // Older versions of LgiRes stored the dialog's controls at a fixed // offset (3,17) from where they should've been. That was fixed, but // to differentiate between the 2 systems, we store a tag at the // root element. Root.SetAttr("Offset", 1); LXmlTree Tree(GXT_NO_ENTITIES); Status = Tree.Write(&Root, &f); if (Status) { // Also write the header... but only if it's changed... - auto DefsContent = Defs.NewGStr(); + auto DefsContent = Defs.NewLStr(); LAutoString OldDefsContent(LReadTextFile(DefsName)); if (Strcmp(DefsContent.Get(), OldDefsContent.Get())) { LFile DefsFile; if (!DefsFile.Open(DefsName, O_WRITE)) goto FileErrorMsg; DefsFile.SetSize(0); DefsFile.Write(DefsContent); } } } } else { FileErrorMsg: LgiMsg(this, "Couldn't open these files for output:\n" "\t%s\n" "\t%s\n" "\n" "Maybe they are read only or locked by another application.", AppName, MB_OK, FileName, DefsName.GetFull().Get()); } } return Status; } // Win32 load/save #define ADJUST_CTRLS_X 2 #define ADJUST_CTRLS_Y 12 #define IMP_MODE_SEARCH 0 #define IMP_MODE_DIALOG 1 #define IMP_MODE_DLG_CTRLS 2 #define IMP_MODE_STRINGS 3 #define IMP_MODE_MENU 4 #include "lgi/common/Token.h" class ImportDefine { public: char *Name; char *Value; ImportDefine() { Name = Value = 0; } ImportDefine(char *Line) { Name = Value = 0; if (Line && *Line == '#') { Line++; if (strnicmp(Line, "define", 6) == 0) { Line += 6; Line = LSkipDelim(Line); char *Start = Line; const char *WhiteSpace = " \r\n\t"; while (*Line && !strchr(WhiteSpace, *Line)) { Line++; } Name = NewStr(Start, Line-Start); Line = LSkipDelim(Line); Start = Line; while (*Line && !strchr(WhiteSpace, *Line)) { Line++; } if (Start != Line) { Value = NewStr(Start, Line-Start); } } } } ~ImportDefine() { DeleteArray(Name); DeleteArray(Value); } }; class DefineList : public List { int NestLevel; public: bool Defined; List IncludeDirs; DefineList() { Defined = true; NestLevel = 0; } ~DefineList() { for (auto i: *this) { DeleteObj(i); } for (auto c: IncludeDirs) { DeleteArray(c); } } void DefineSymbol(const char *Name, const char *Value = 0) { ImportDefine *Def = new ImportDefine; if (Def) { Def->Name = NewStr(Name); if (Value) Def->Value = NewStr(Value); Insert(Def); } } ImportDefine *GetDefine(char *Name) { if (Name) { for (auto i: *this) { if (i->Name && stricmp(i->Name, Name) == 0) { return i; } } } return NULL; } void ProcessLine(char *Line) { if (NestLevel > 16) { return; } if (Line && *Line == '#') { Line++; LToken T(Line); if (T.Length() > 0) { if (stricmp(T[0], "define") == 0) // #define { ImportDefine *Def = new ImportDefine(Line-1); if (Def) { if (Def->Name) { Insert(Def); } else { DeleteObj(Def); } } } else if (stricmp(T[0], "include") == 0) // #include { NestLevel++; LFile F; if (T.Length() > 1) { for (auto IncPath: IncludeDirs) { char FullPath[256]; strcpy(FullPath, IncPath); if (FullPath[strlen(FullPath)-1] != DIR_CHAR) { strcat(FullPath, DIR_STR); } strcat(FullPath, T[1]); if (F.Open(FullPath, O_READ)) { char Line[1024]; while (!F.Eof()) { F.ReadStr(Line, sizeof(Line)); char *p = LSkipDelim(Line); if (*p == '#') { ProcessLine(p); } } break; } } } NestLevel--; } else if (stricmp(T[0], "if") == 0) // #if { } else if (stricmp(T[0], "ifdef") == 0) // #if { if (T.Length() > 1) { Defined = GetDefine(T[1]) != 0; } } else if (stricmp(T[0], "endif") == 0) // #endif { Defined = true; } else if (stricmp(T[0], "pragma") == 0) { ImportDefine *Def = new ImportDefine; if (Def) { char *Str = Line + 7; char *First = strchr(Str, '('); char *Second = (First) ? strchr(First+1, ')') : 0; if (First && Second) { Insert(Def); Def->Name = NewStr(Str, First-Str); First++; Def->Value = NewStr(First, Second-First); } else { DeleteObj(Def); } } } else if (stricmp(T[0], "undef") == 0) { } } } // else it's not for us anyway } }; void TokLine(LArray &T, char *Line) { if (Line) { // Exclude comments for (int k=0; Line[k]; k++) { if (Line[k] == '/' && Line[k+1] == '/') { Line[k] = 0; break; } } // Break into tokens for (const char *s = Line; s && *s; ) { while (*s && strchr(" \t", *s)) s++; char *t = LTokStr(s); if (t) T.Add(t); else break; } } } void AppWnd::LoadWin32(const char *FileName) { bool Status = false; LHashTbl,bool> CtrlNames; CtrlNames.Add("LTEXT", true); CtrlNames.Add("EDITTEXT", true); CtrlNames.Add("COMBOBOX", true); CtrlNames.Add("SCROLLBAR", true); CtrlNames.Add("GROUPBOX", true); CtrlNames.Add("PUSHBUTTON", true); CtrlNames.Add("DEFPUSHBUTTON", true); CtrlNames.Add("CONTROL", true); CtrlNames.Add("ICON", true); CtrlNames.Add("LISTBOX", true); Empty(); auto Load = [&](const char *FileName) { if (!FileName) return; LProgressDlg Progress(this); Progress.SetDescription("Initializing..."); Progress.SetType("K"); Progress.SetScale(1.0/1024.0); char *FileTxt = LReadTextFile(FileName); if (FileTxt) { LToken Lines(FileTxt, "\r\n"); DeleteArray(FileTxt); DefineList Defines; ResStringGroup *String = new ResStringGroup(this); int Mode = IMP_MODE_SEARCH; // Language char *Language = 0; LLanguageId LanguageId = 0; // Dialogs List DlLList; CtrlDlg *Dlg = 0; // Menus ResDialog *Dialog = 0; ResMenu *Menu = 0; List Menus; ResMenuItem *MenuItem[32]; int MenuLevel = 0; bool MenuNewLang = false; int MenuNextItem = 0; // Include defines char IncPath[256]; strcpy(IncPath, FileName); LTrimDir(IncPath); Defines.IncludeDirs.Insert(NewStr(IncPath)); Defines.DefineSymbol("_WIN32"); Defines.DefineSymbol("IDC_STATIC", "-1"); DoEvery Ticker(200); Progress.SetDescription("Reading resources..."); Progress.SetRange(Lines.Length()); if (String) { InsertObject(TYPE_STRING, String, false); } for (int CurLine = 0; CurLine < Lines.Length(); CurLine++) { if (Ticker.DoNow()) { Progress.Value(CurLine); LYield(); } // Skip white space char *Line = Lines[CurLine]; char *p = LSkipDelim(Line); Defines.ProcessLine(Line); // Tokenize LArray T; TokLine(T, Line); // Process line if (Defines.Defined) { switch (Mode) { case IMP_MODE_SEARCH: { DeleteObj(Dialog); Dlg = 0; if (*p != '#') { if (T.Length() > 1 && (stricmp(T[1], "DIALOG") == 0 || stricmp(T[1], "DIALOGEX") == 0)) { Mode = IMP_MODE_DIALOG; Dialog = new ResDialog(this); if (Dialog) { Dialog->Create(NULL, NULL); Dialog->Name(T[0]); auto It = Dialog->IterateViews(); Dlg = dynamic_cast(It[0]); if (Dlg) { int Pos[4] = {0, 0, 0, 0}; int i = 0; for (; iResDialogCtrl::SetPos(r); Dlg->GetStr()->SetDefine(T[0]); ImportDefine *Def = Defines.GetDefine(T[0]); if (Def) { Dlg->GetStr()->SetId(atoi(Def->Value)); } } } break; } if (T.Length() > 1 && stricmp(T[1], "MENU") == 0) { ZeroObj(MenuItem); Mode = IMP_MODE_MENU; Menu = 0; // Check for preexisting menu in another language MenuNewLang = false; for (auto m: Menus) { if (stricmp(m->Name(), T[0]) == 0) { MenuNewLang = true; Menu = m; break; } } // If it doesn't preexist then create it if (!Menu) { Menu = new ResMenu(this); if (Menu) { Menus.Insert(Menu); Menu->Create(NULL, NULL); Menu->Name(T[0]); } } break; } if (T.Length() > 0 && stricmp(T[0], "STRINGTABLE") == 0) { Mode = IMP_MODE_STRINGS; if (String) { String->Name("_Win32 Imports_"); } break; } if (T.Length() > 2 && stricmp(T[0], "LANGUAGE") == 0) { LanguageId = 0; DeleteArray(Language); char *Language = NewStr(T[1]); if (Language) { LLanguage *Info = LFindLang(0, Language); if (Info) { LanguageId = Info->Id; ResDialog::AddLanguage(Info->Id); } } break; } } break; } case IMP_MODE_DIALOG: { if (T.Length() > 0 && Dlg) { if (stricmp(T[0], "CAPTION") == 0) { char *Caption = T[1]; if (Caption) { Dlg->GetStr()->Set(Caption, LanguageId); } } else if (stricmp(T[0], "BEGIN") == 0) { Mode = IMP_MODE_DLG_CTRLS; } } break; } case IMP_MODE_DLG_CTRLS: { char *Type = T[0]; if (!Type) break; if (stricmp(Type, "end") != 0) { // Add wrapped content to the token array. char *Next = Lines[CurLine+1]; if (Next) { Next = LSkipDelim(Next); char *NextTok = LTokStr((const char*&)Next); if (NextTok) { if (stricmp(NextTok, "END") != 0 && !CtrlNames.Find(NextTok)) { TokLine(T, Lines[++CurLine]); } DeleteArray(NextTok); } } } // Process controls if (stricmp(Type, "LTEXT") == 0) { if (T.Length() >= 7) { CtrlText *Ctrl = new CtrlText(Dialog, 0); if (Ctrl) { Ctrl->GetStr()->Set(T[1], LanguageId); Ctrl->GetStr()->SetDefine(T[2]); ImportDefine *Def = Defines.GetDefine(T[2]); if (Def) { Ctrl->GetStr()->SetId(atoi(Def->Value)); } LRect r; r.ZOff(atoi(T[5])*CTRL_X, atoi(T[6])*CTRL_Y); r.Offset(atoi(T[3])*CTRL_X+ADJUST_CTRLS_X, atoi(T[4])*CTRL_Y+ADJUST_CTRLS_Y); Ctrl->ResDialogCtrl::SetPos(r); Dlg->AddView(Ctrl); } } } else if (stricmp(Type, "EDITTEXT") == 0) { if (T.Length() >= 7) { CtrlEditbox *Ctrl = new CtrlEditbox(Dialog, 0); if (Ctrl) { Ctrl->GetStr()->SetDefine(T[1]); ImportDefine *Def = Defines.GetDefine(T[1]); if (Def) { Ctrl->GetStr()->SetId(atoi(Def->Value)); } LRect r; r.ZOff(atoi(T[4])*CTRL_X, atoi(T[5])*CTRL_Y); r.Offset(atoi(T[2])*CTRL_X+ADJUST_CTRLS_X, atoi(T[3])*CTRL_Y+ADJUST_CTRLS_Y); Ctrl->ResDialogCtrl::SetPos(r); Dlg->AddView(Ctrl); } } } else if (stricmp(Type, "COMBOBOX") == 0) { if (T.Length() >= 6) { CtrlComboBox *Ctrl = new CtrlComboBox(Dialog, 0); if (Ctrl) { Ctrl->GetStr()->SetDefine(T[1]); ImportDefine *Def = Defines.GetDefine(T[1]); if (Def) { Ctrl->GetStr()->SetId(atoi(Def->Value)); } LRect r; r.ZOff(atoi(T[4])*CTRL_X, atoi(T[5])*CTRL_Y); r.Offset(atoi(T[2])*CTRL_X+ADJUST_CTRLS_X, atoi(T[3])*CTRL_Y+ADJUST_CTRLS_Y); Ctrl->ResDialogCtrl::SetPos(r); Dlg->AddView(Ctrl); } } } else if (stricmp(Type, "SCROLLBAR") == 0) { if (T.Length() == 6) { CtrlScrollBar *Ctrl = new CtrlScrollBar(Dialog, 0); if (Ctrl) { Ctrl->GetStr()->SetDefine(T[1]); ImportDefine *Def = Defines.GetDefine(T[1]); if (Def) { Ctrl->GetStr()->SetId(atoi(Def->Value)); } LRect r; r.ZOff(atoi(T[4])*CTRL_X, atoi(T[5])*CTRL_Y); r.Offset(atoi(T[2])*CTRL_X+ADJUST_CTRLS_X, atoi(T[3])*CTRL_Y+ADJUST_CTRLS_Y); Ctrl->ResDialogCtrl::SetPos(r); Dlg->AddView(Ctrl); } } } else if (stricmp(Type, "GROUPBOX") == 0) { if (T.Length() >= 7) { CtrlGroup *Ctrl = new CtrlGroup(Dialog, 0); if (Ctrl) { Ctrl->GetStr()->Set(T[1], LanguageId); Ctrl->GetStr()->SetDefine(T[2]); ImportDefine *Def = Defines.GetDefine(T[2]); if (Def) { Ctrl->GetStr()->SetId(atoi(Def->Value)); } LRect r; r.ZOff(atoi(T[5])*CTRL_X, atoi(T[6])*CTRL_Y); r.Offset(atoi(T[3])*CTRL_X+ADJUST_CTRLS_X, atoi(T[4])*CTRL_Y+ADJUST_CTRLS_Y); Ctrl->ResDialogCtrl::SetPos(r); Dlg->AddView(Ctrl); } } } else if (stricmp(Type, "PUSHBUTTON") == 0 || stricmp(Type, "DEFPUSHBUTTON") == 0) { if (T.Length() >= 7) { CtrlButton *Ctrl = new CtrlButton(Dialog, 0); if (Ctrl) { Ctrl->GetStr()->Set(T[1], LanguageId); Ctrl->GetStr()->SetDefine(T[2]); ImportDefine *Def = Defines.GetDefine(T[2]); if (Def) { Ctrl->GetStr()->SetId(atoi(Def->Value)); } LRect r; r.ZOff(atoi(T[5])*CTRL_X, atoi(T[6])*CTRL_Y); r.Offset(atoi(T[3])*CTRL_X+ADJUST_CTRLS_X, atoi(T[4])*CTRL_Y+ADJUST_CTRLS_Y); Ctrl->ResDialogCtrl::SetPos(r); Dlg->AddView(Ctrl); } } } else if (stricmp(Type, "CONTROL") == 0) { if (T.Length() >= 7) { char *Caption = T[1]; char *Id = T[2]; char *Type = T[3]; bool Checkbox = false; bool Radio = false; bool Done = false; // loop through styles int i; for (i=4; !Done && iSetPos(r); if (Caption) Ctrl->GetStr()->Set(Caption, LanguageId); if (Id) Ctrl->GetStr()->SetDefine(Id); ImportDefine *Def = Defines.GetDefine(Id); if (Def) { Ctrl->GetStr()->SetId(atoi(Def->Value)); } Dlg->AddView(Ctrl->View()); } } } } else if (stricmp(Type, "ICON") == 0) { if (T.Length() >= 7) { CtrlBitmap *Ctrl = new CtrlBitmap(Dialog, 0); if (Ctrl) { Ctrl->GetStr()->SetDefine(T[1]); ImportDefine *Def = Defines.GetDefine(T[1]); if (Def) { Ctrl->GetStr()->SetId(atoi(Def->Value)); } LRect r; r.ZOff(atoi(T[5])*CTRL_X, atoi(T[6])*CTRL_Y); r.Offset(atoi(T[3])*CTRL_X+ADJUST_CTRLS_X, atoi(T[4])*CTRL_Y+ADJUST_CTRLS_Y); Ctrl->ResDialogCtrl::SetPos(r); Dlg->AddView(Ctrl->View()); } } } else if (stricmp(Type, "LISTBOX") == 0) { if (T.Length() >= 7) { CtrlList *Ctrl = new CtrlList(Dialog, 0); if (Ctrl) { Ctrl->GetStr()->SetDefine(T[1]); ImportDefine *Def = Defines.GetDefine(T[1]); if (Def) { Ctrl->GetStr()->SetId(atoi(Def->Value)); } LRect r; r.ZOff(atoi(T[4])*CTRL_X, atoi(T[5])*CTRL_Y); r.Offset(atoi(T[2])*CTRL_X+ADJUST_CTRLS_X, atoi(T[3])*CTRL_Y+ADJUST_CTRLS_Y); Ctrl->ResDialogCtrl::SetPos(r); Dlg->AddView(Ctrl); } } } else if (stricmp(Type, "END") == 0) { // search for an existing dialog resource in // another language ResDialog *Match = 0; CtrlDlg *MatchObj = 0; for (auto d: DlLList) { auto It = d->IterateViews(); LViewI *Wnd = It[0]; if (Wnd) { CtrlDlg *Obj = dynamic_cast(Wnd); if (Obj) { if (Obj->GetStr()->GetId() == Dlg->GetStr()->GetId()) { MatchObj = Obj; Match = d; break; } } } } if (Match) { // Merge the controls from "Dlg" to "MatchObj" List Old; List New; Dlg->ListChildren(New); MatchObj->ListChildren(Old); // add the language strings for the caption // without clobbering the languages already // present for (auto s: Dlg->GetStr()->Items) { if (!MatchObj->GetStr()->Get(s->GetLang())) { MatchObj->GetStr()->Set(s->GetStr(), s->GetLang()); } } for (auto c: New) { ResDialogCtrl *MatchCtrl = 0; // try matching by Id { for (auto Mc: Old) { if (Mc->GetStr()->GetId() == c->GetStr()->GetId() && Mc->GetStr()->GetId() > 0) { MatchCtrl = Mc; break; } } } // ok no Id match, match by location and type if (!MatchCtrl) { List Overlapping; for (auto Mc: Old) { LRect a = Mc->View()->GetPos(); LRect b = c->View()->GetPos(); LRect c = a; c.Bound(&b); if (c.Valid()) { int Sa = a.X() * a.Y(); int Sb = b.X() * b.Y(); int Sc = c.X() * c.Y(); int Total = Sa + Sb - Sc; double Amount = (double) Sc / (double) Total; if (Amount > 0.5) { // mostly similar in size Overlapping.Insert(Mc); } } } if (Overlapping.Length() == 1) { MatchCtrl = Overlapping[0]; } } if (MatchCtrl) { // woohoo we are cool for (auto s: c->GetStr()->Items) { MatchCtrl->GetStr()->Set(s->GetStr(), s->GetLang()); } } } // Delete the duplicate OnObjSelect(0); DeleteObj(Dialog); } else { // Insert the dialog InsertObject(TYPE_DIALOG, Dialog, false); DlLList.Insert(Dialog); } Dialog = 0; Dlg = 0; Status = true; Mode = IMP_MODE_SEARCH; } break; } case IMP_MODE_STRINGS: { if (stricmp(T[0], "BEGIN") == 0) { } else if (stricmp(T[0], "END") == 0) { Status = true; Mode = IMP_MODE_SEARCH; } else { if (T.Length() > 1) { ResString *Str = String->FindName(T[0]); if (!Str) { Str = String->CreateStr(); if (Str) { ImportDefine *Def = Defines.GetDefine(T[0]); if (Def) { Str->SetId(atoi(Def->Value)); } Str->SetDefine(T[0]); Str->UnDuplicate(); } } if (Str) { // get the language LLanguage *Lang = LFindLang(Language); LLanguageId SLang = (Lang) ? Lang->Id : (char*)"en"; StrLang *s = 0; // look for language present in string object for (auto ss: Str->Items) { if (*ss == SLang) { s = ss; break; } } // if not present then add it if (!s) { s = new StrLang; if (s) { Str->Items.Insert(s); s->SetLang(SLang); } } // set the value if (s) { s->SetStr(T[1]); } } } } break; } case IMP_MODE_MENU: { if (T.Length() >= 1) { if (stricmp(T[0], "BEGIN") == 0) { MenuLevel++; } else if (stricmp(T[0], "END") == 0) { MenuLevel--; if (MenuLevel == 0) { Status = true; Mode = IMP_MODE_SEARCH; Menu->SetLanguages(); if (!MenuNewLang) { InsertObject(TYPE_MENU, Menu, false); } Menu = 0; } } else { ResMenuItem *i = 0; char *Text = T[1]; if (Text) { if (stricmp(T[0], "POPUP") == 0) { if (MenuNewLang) { LTreeItem *Ri = 0; if (MenuItem[MenuLevel]) { Ri = MenuItem[MenuLevel]->GetNext(); } else { if (MenuLevel == 1) { Ri = Menu->ItemAt(0); } else if (MenuItem[MenuLevel-1]) { Ri = MenuItem[MenuLevel-1]->GetChild(); } } if (Ri) { // Seek up to the next submenu while (!Ri->GetChild()) { Ri = Ri->GetNext(); } i = dynamic_cast(Ri); // char *si = i->Str.Get("en"); if (i) { MenuItem[MenuLevel] = i; } } } else { MenuItem[MenuLevel] = i = new ResMenuItem(Menu); } if (i) { LLanguage *Lang = LFindLang(Language); i->GetStr()->Set(Text, (Lang) ? Lang->Id : (char*)"en"); } MenuNextItem = 0; } else if (stricmp(T[0], "MENUITEM") == 0) { if (MenuNewLang) { if (MenuItem[MenuLevel-1] && T.Length() > 2) { ImportDefine *id = Defines.GetDefine(T[2]); if (id) { int Id = atoi(id->Value); int n = 0; for (LTreeItem *o = MenuItem[MenuLevel-1]->GetChild(); o; o = o->GetNext(), n++) { ResMenuItem *Res = dynamic_cast(o); if (Res && Res->GetStr()->GetId() == Id) { i = Res; break; } } } } MenuNextItem++; } else { i = new ResMenuItem(Menu); } if (i) { if (stricmp(Text, "SEPARATOR") == 0) { // Set separator i->Separator(true); } else { if (!MenuNewLang) { // Set Id i->GetStr()->SetDefine(T[2]); if (i->GetStr()->GetDefine()) { ImportDefine *id = Defines.GetDefine(i->GetStr()->GetDefine()); if (id) { i->GetStr()->SetId(atoi(id->Value)); i->GetStr()->UnDuplicate(); } } } // Set Text LLanguage *Lang = LFindLang(Language); i->GetStr()->Set(Text, (Lang) ? Lang->Id : (char*)"en"); } } } } if (i && !MenuNewLang) { if (MenuLevel == 1) { Menu->Insert(i); } else if (MenuItem[MenuLevel-1]) { MenuItem[MenuLevel-1]->Insert(i); } } } } break; } } } T.DeleteArrays(); } DeleteObj(Dialog); if (String->Length() > 0) { String->SetLanguages(); } } Invalidate(); }; if (FileName) Load(FileName); else { auto Select = new LFileSelect(this); Select->Type("Win32 Resource Script", "*.rc"); Select->Open([&](auto dlg, auto status) { if (status) Load(dlg->Name()); delete dlg; }); } } bool AppWnd::SaveWin32() { return false; } ///////////////////////////////////////////////////////////////////////// ResFrame::ResFrame(Resource *child) { Child = child; Name("ResFrame"); } ResFrame::~ResFrame() { if (Child) { Child->App()->OnObjSelect(NULL); Child->Wnd()->Detach(); } } void ResFrame::OnFocus(bool b) { Child->Wnd()->Invalidate(); } bool ResFrame::Attach(LViewI *p) { bool Status = LLayout::Attach(p); if (Status && Child) { Child->Attach(this); Child->Wnd()->Visible(true); } return Status; } bool ResFrame::Pour(LRegion &r) { LRect *Best = FindLargest(r); if (Best) { SetPos(*Best); return true; } return false; } bool ResFrame::OnKey(LKey &k) { bool Status = false; if (k.Down() && Child) { switch (k.c16) { case LK_DELETE: { if (k.Shift()) { Child->Copy(true); } else { Child->Delete(); } Status = true; break; } case 'x': case 'X': { if (k.Ctrl()) { Child->Copy(true); Status = true; } break; } case 'c': case 'C': { if (k.Ctrl()) { Child->Copy(); Status = true; } break; } case LK_INSERT: { if (k.Ctrl()) { Child->Copy(); } else if (k.Shift()) { Child->Paste(); } Status = true; break; } case 'v': case 'V': { if (k.Ctrl()) { Child->Paste(); Status = true; } break; } } } return Child->Wnd()->OnKey(k) || Status; } void ResFrame::OnPaint(LSurface *pDC) { // Draw nice frame LRect r(0, 0, X()-1, Y()-1); LThinBorder(pDC, r, DefaultRaisedEdge); pDC->Colour(L_MED); LFlatBorder(pDC, r, 4); LWideBorder(pDC, r, DefaultSunkenEdge); // Set the child to the client area Child->Wnd()->SetPos(r); // Draw the dialog & controls LView::OnPaint(pDC); } //////////////////////////////////////////////////////////////////// LgiFunc char *_LgiGenLangLookup(); #include "lgi/common/AutoPtr.h" #include "lgi/common/Variant.h" #include "lgi/common/Css.h" #include "lgi/common/TableLayout.h" class Foo : public LLayoutCell { public: Foo() { TextAlign(AlignLeft); } bool Add(LView *v) { return false; } bool Remove(LView *v) { return false; } }; ////////////////////////////////////////////////////////////////////// ShortCutView::ShortCutView(AppWnd *app) { App = app; LRect r(0, 0, 500, 600); SetPos(r); MoveSameScreen(App); Name("Dialog Shortcuts"); if (Attach(0)) { Lst = new LList(100, 0, 0, 100, 100); Lst->Attach(this); Lst->SetPourLargest(true); Lst->AddColumn("Key", 50); Lst->AddColumn("Ref", 80); Lst->AddColumn("CtrlId", 80); Lst->AddColumn("Name", 150); Visible(true); } } ShortCutView::~ShortCutView() { App->OnCloseView(this); } enum ShortCutCol { ColKey, ColRefId, ColCtrlId, ColName }; void FindMenuKeys(LList *out, ResMenu *menu) { if (!out || !menu) return; LArray items; menu->EnumItems(items); for (auto i: items) { auto sc = i->Shortcut(); if (sc) { auto item = new LListItem(sc); LString ref, ctrl; ref.Printf("%i", i->GetStr()->GetRef()); ctrl.Printf("%i", i->GetStr()->GetId()); item->SetText(ref, ColRefId); item->SetText(ctrl, ColCtrlId); item->SetText(i->GetStr()->Get(), ColName); item->_UserPtr = i; out->Insert(item); } } } void FindShortCuts(LList *Out, LViewI *In) { for (auto c: In->IterateViews()) { auto rdc = dynamic_cast(c); if (!rdc || !rdc->GetStr()) continue; auto n = rdc->GetStr()->Get(); if (n) { char *a = strchr(n, '&'); if (a && a[1] != '&') { LListItem *li = new LListItem; LString s(++a, 1); LString ref, ctrl; ref.Printf("%i", rdc->GetStr()->GetRef()); ctrl.Printf("%i", rdc->GetStr()->GetId()); li->SetText(s.Upper(), ColKey); li->SetText(ref, ColRefId); li->SetText(ctrl, ColCtrlId); li->SetText(rdc->GetClass(), ColName); li->_UserPtr = rdc; Out->Insert(li); } } FindShortCuts(Out, c); } } int ShortCutView::OnNotify(LViewI *Ctrl, LNotification n) { if (Ctrl->GetId() == Lst->GetId()) { switch (n.Type) { case LNotifyItemClick: { auto li = Lst->GetSelected(); if (!li) break; LString s = li->GetText(1); ResObject *c = (ResObject*) li->_UserPtr; if (!c) break; if (auto ctrl = dynamic_cast(c)) App->GotoObject(ctrl->GetStr(), NULL, ctrl->GetDlg(), NULL, ctrl); else if (auto mi = dynamic_cast(c)) App->GotoObject(mi->GetStr(), NULL, NULL, mi, NULL); else LAssert(!"Impl me."); break; } default: break; } } return LWindow::OnNotify(Ctrl, n); } void ShortCutView::OnResource(Resource *r) { Lst->Empty(); if (!r) return; if (auto dlg = dynamic_cast(r)) FindShortCuts(Lst, dlg); else if (auto menu = dynamic_cast(r)) FindMenuKeys(Lst, menu); Lst->Sort(0); Lst->ResizeColumnsToContent(); } ShortCutView *AppWnd::GetShortCutView() { return ShortCuts; } void AppWnd::OnCloseView(ShortCutView *v) { if (v == ShortCuts) ShortCuts = NULL; } ////////////////////////////////////////////////////////////////////// void TestFunc() { /* Foo c; uint32 *p = (uint32*)&c; p++; p = (uint32*)(*p); void *method = (void*)p[2]; for (int i=0; i<16; i++) { printf("[%i]=%p\n", i, p[i]); } c.OnChange(LCss::PropBackground); */ } int LgiMain(OsAppArguments &AppArgs) { LApp a(AppArgs, "LgiRes"); if (a.IsOk()) { if ((a.AppWnd = new AppWnd)) { TestFunc(); a.AppWnd->Visible(true); a.Run(); } } return 0; } diff --git a/SlogViewer/src/SlogViewerMain.cpp b/SlogViewer/src/SlogViewerMain.cpp --- a/SlogViewer/src/SlogViewerMain.cpp +++ b/SlogViewer/src/SlogViewerMain.cpp @@ -1,429 +1,429 @@ #define _CRT_SECURE_NO_WARNINGS #include "lgi/common/Lgi.h" #include "lgi/common/OptionsFile.h" #include "lgi/common/DocApp.h" #include "lgi/common/Box.h" #include "lgi/common/List.h" #include "lgi/common/TextView3.h" #include "lgi/common/TabView.h" #include "lgi/common/StructuredLog.h" #include "lgi/common/StatusBar.h" #include "resdefs.h" ////////////////////////////////////////////////////////////////// const char *AppName = "SlogViewer"; enum DisplayMode { DisplayHex, DisplayEscaped }; enum Ctrls { IDC_BOX = 100, IDC_LOG, IDC_HEX, IDC_ESCAPED, IDC_TABS, IDC_STATUS, }; struct Context { LList *log = NULL; LTabView *tabs = NULL; LTextView3 *hex = NULL; LTextView3 *escaped = NULL; }; class Entry : public LListItem { Context *c; struct Value { LVariantType type; LArray data; LString name; }; LArray values; LString cache; public: Entry(Context *ctx) : c(ctx) { } void add(LVariantType type, size_t sz, void *ptr, const char *name) { auto &v = values.New(); v.type = type; v.data.Add((uint8_t*)ptr, sz); v.name = name; } const char *GetText(int i) { if (!cache) { LStringPipe p; for (auto &v: values) { switch (v.type) { case GV_INT32: case GV_INT64: { if (v.data.Length() == 4) p.Print("%i", *(int*)v.data.AddressOf()); else if (v.data.Length() == 8) p.Print(LPrintfInt64, *(int64_t*)v.data.AddressOf()); else LAssert(!"Unknown int size."); break; } case GV_STRING: { if (v.data.Length() >= 32) p.Print(" (...)"); else p.Print("%.*s", (int)v.data.Length(), v.data.AddressOf()); break; } case GV_CUSTOM: { p.Print("Object: %s", v.name.Get()); break; } case GV_VOID_PTR: { p.Print("/Object"); break; } default: { LAssert(!"Unknown type."); break; } } if (p.GetSize() > 64) break; } - cache = p.NewGStr(); + cache = p.NewLStr(); } return cache; } void Select(bool b) override { LListItem::Select(b); auto mode = (DisplayMode)c->tabs->Value(); auto ctrl = mode ? c->escaped : c->hex; if (b) { LStringPipe p; LString Obj; int Idx = 0; auto PrintEscaped = [&](char *s, char *e) { for (auto c = s; c < e; c++) { switch (*c) { case '\\': p.Print("\\\\"); break; case '\t': p.Print("\\t"); break; case '\r': p.Print("\\r"); break; case '\n': p.Print("\\n\n"); break; case 0x07: p.Print("\\b"); break; case 0x1b: p.Print("\\e"); break; default: if (*c < ' ' || *c >= 128) { LString hex; hex.Printf("\\x%x", (uint8_t)*c); p.Write(hex); } else p.Write(c, 1); break; } } }; for (auto &v: values) { auto sType = LVariant::TypeToString(v.type); switch (v.type) { case GV_CUSTOM: { p.Print("object %s {\n\n", v.name.Get()); Obj = v.name; Idx = 0; continue; } case GV_VOID_PTR: { p.Print("}\n"); Obj.Empty(); continue; } case GV_STRING: { if (Obj == "LConsole") { auto s = (char*)v.data.AddressOf(); auto e = s + v.data.Length(); p.Print("[%i]=", Idx++); PrintEscaped(s, e); p.Print("\n"); continue; } break; } } p.Print("%s, %i bytes%s%s:\n", sType, (int)v.data.Length(), v.name ? ", " : "", v.name ? v.name.Get() : ""); switch (v.type) { case GV_INT32: case GV_INT64: { if (v.data.Length() == 4) { auto i = (int*) v.data.AddressOf(); p.Print("%i 0x%x\n", *i, *i); } else if (v.data.Length() == 8) { auto i = (int64_t*) v.data.AddressOf(); p.Print(LPrintfInt64 " 0x" LPrintfHex64 "\n", *i, *i); } else LAssert(!"Impl me."); break; } case GV_STRING: { if (mode == DisplayHex) { char line[300]; int ch = 0; const int rowSize = 16; const int colHex = 10; const int colAscii = colHex + (rowSize * 3) + 2; const int colEnd = colAscii + rowSize; for (size_t addr = 0; addr < v.data.Length() ; addr += rowSize) { ZeroObj(line); sprintf(line, "%08.8x", (int)addr); auto rowBytes = MIN(v.data.Length() - addr, rowSize); LAssert(rowBytes <= rowSize); auto rowPtr = v.data.AddressOf(addr); for (int i=0; i= ' ' && rowPtr[i] < 128 ? rowPtr[i] : '.'; } for (int i=0; i s && e[-1] != '\n') p.Write("\n"); } break; } default: { LAssert(!"Impl me."); break; } } p.Print("\n"); } - ctrl->Name(p.NewGStr()); + ctrl->Name(p.NewLStr()); } } }; class ReaderThread : public LThread { Context *Ctx; LString FileName; LList *Lst = NULL; Progress *Prog = NULL; public: ReaderThread(Context *ctx, const char *filename, LList *lst, Progress *prog) : LThread("ReaderThread") { Ctx = ctx; FileName = filename; Lst = lst; Prog = prog; Run(); } ~ReaderThread() { Prog->Cancel(); WaitForExit(10000); } int Main() { LStructuredLog file(FileName, false); LAutoPtr cur; List items; while (file.Read([this, &cur, &items](auto type, auto size, auto ptr, auto name) { if (type == LStructuredIo::EndRow) { items.Insert(cur.Release()); if (items.Length() > 100) { Lst->Insert(items); items.Empty(); } return; } if (!cur && !cur.Reset(new Entry(Ctx))) return; cur->add(type, size, ptr, name); }, Prog)) ; Lst->Insert(items); return 0; } }; class App : public LDocApp, public Context { LBox *box = NULL; LAutoPtr Reader; LStatusBar *Status = NULL; LProgressStatus *Prog = NULL; public: App() : LDocApp(AppName) { Name(AppName); LRect r(0, 0, 1000, 800); SetPos(r); MoveToCenter(); SetQuitOnClose(true); if (_Create()) { _LoadMenu(); AddView(Status = new LStatusBar(IDC_STATUS)); Status->AppendPane("Some text"); Status->AppendPane(Prog = new LProgressStatus()); Prog->GetCss(true)->Width("200px"); Prog->GetCss()->TextAlign(LCss::AlignRight); AddView(box = new LBox(IDC_BOX)); box->AddView(log = new LList(IDC_LOG, 0, 0, 200, 200)); log->GetCss(true)->Width("40%"); log->ShowColumnHeader(false); log->AddColumn("Items", 1000); box->AddView(tabs = new LTabView(IDC_TABS)); auto tab = tabs->Append("Hex"); tab->Append(hex = new LTextView3(IDC_HEX)); hex->Sunken(true); hex->SetPourLargest(true); tab = tabs->Append("Escaped"); tab->Append(escaped = new LTextView3(IDC_ESCAPED)); escaped->Sunken(true); escaped->SetPourLargest(true); AttachChildren(); Visible(true); } } void OnReceiveFiles(LArray &Files) { if (Files.Length()) OpenFile(Files[0]); } bool OpenFile(const char *FileName, bool ReadOnly = false) { return Reader.Reset(new ReaderThread(this, FileName, log, Prog)); } bool SaveFile(const char *FileName) { return false; } int OnNotify(LViewI *Ctrl, LNotification n) { if (Ctrl->GetId() == IDC_TABS) { auto item = log->GetSelected(); if (item) item->Select(true); } return 0; } }; ////////////////////////////////////////////////////////////////// int LgiMain(OsAppArguments &AppArgs) { LApp a(AppArgs, AppName); if (a.IsOk()) { a.AppWnd = new App; a.Run(); } return 0; } diff --git a/include/lgi/common/Containers.h b/include/lgi/common/Containers.h --- a/include/lgi/common/Containers.h +++ b/include/lgi/common/Containers.h @@ -1,1159 +1,1159 @@ /** \file \author Matthew Allen \date 27/11/1996 \brief Container class header.\n Copyright (C) 1996-2003, Matthew Allen */ #ifndef _CONTAIN_H_ #define _CONTAIN_H_ #include #include "lgi/common/LgiInc.h" #include "LgiOsDefs.h" #include "lgi/common/Stream.h" #include "lgi/common/Profile.h" #include /// Template for using DLinkList with a type safe API. #define ITEM_PTRS 64 #define LGI_LIST_VALIDATION 0 LgiFunc bool UnitTest_ListClass(); #ifdef _DEBUG #define VALIDATE() Validate() #else #define VALIDATE() #endif template class List { struct LstBlk { LstBlk *Next, *Prev; uint8_t Count; T *Ptr[ITEM_PTRS]; LstBlk() { Next = Prev = NULL; Count = 0; ZeroObj(Ptr); } bool Full() { return Count >= ITEM_PTRS; } int Remaining() { return ITEM_PTRS - Count; } }; public: class Iter { public: const List *Lst; LstBlk *i; int Cur, Ver; OsThreadId Thread; bool CheckThread() const { #if 1 return true; #else auto Cur = GetCurrentThreadId(); bool Ok = Thread == Cur; LAssert(Ok); return Ok; #endif } #ifdef _DEBUG #define CHECK_THREAD CheckThread(); #else #define CHECK_THREAD #endif Iter(const List *lst) { Lst = lst; i = 0; Cur = 0; Ver = lst->Ver; Thread = GetCurrentThreadId(); } Iter(const List *lst, LstBlk *item, int c) { Lst = lst; i = item; Cur = c; Ver = lst->Ver; Thread = GetCurrentThreadId(); } bool operator ==(const Iter &it) const { CHECK_THREAD int x = (int)In() + (int)it.In(); if (x == 2) return (i == it.i) && (Cur == it.Cur); return x == 0; } bool operator !=(const Iter &it) const { CHECK_THREAD return !(*this == it); } bool operator <(const Iter &it) const { CHECK_THREAD return Cur < it.Cur; } bool operator <=(const Iter &it) const { CHECK_THREAD return Cur <= it.Cur; } bool operator >(const Iter &it) const { CHECK_THREAD return Cur > it.Cur; } bool operator >=(const Iter &it) const { CHECK_THREAD return Cur >= it.Cur; } bool In() const { CHECK_THREAD if (Ver != Lst->Ver) { if (!Lst->ValidBlock(i)) return false; } return i && Cur >= 0 && Cur < i->Count; } operator T*() const { CHECK_THREAD return In() ? i->Ptr[Cur] : NULL; } T *operator *() const { CHECK_THREAD return In() ? i->Ptr[Cur] : NULL; } Iter &operator =(LstBlk *item) { CHECK_THREAD i = item; if (!i) Cur = 0; return *this; } Iter &operator =(int c) { CHECK_THREAD Cur = c; return *this; } Iter &operator =(Iter *iter) { CHECK_THREAD Lst = iter->Lst; i = iter->i; Cur = iter->Cur; return *this; } int GetIndex(int Base) { CHECK_THREAD if (i) return Base + Cur; return -1; } bool Next() { CHECK_THREAD if (i) { if (!In()) return false; Cur++; if (Cur >= i->Count) { i = i->Next; if (i) { Cur = 0; return i->Count > 0; } } else return true; } return false; } bool Prev() { CHECK_THREAD if (i) { if (!In()) return false; Cur--; if (Cur < 0) { i = i->Prev; if (i && i->Count > 0) { Cur = i->Count - 1; return true; } } else return true; } return false; } bool Delete() { CHECK_THREAD if (i) { LAssert(Lst); i->Delete(Cur, i); return true; } return false; } Iter &operator ++() { Next(); return *this; } Iter &operator --() { Prev(); return *this; } Iter &operator ++(int) { Next(); return *this; } Iter &operator --(int) { Prev(); return *this; } }; typedef Iter I; // typedef int (*CompareFn)(T *a, T *b, NativeInt data); protected: size_t Items; int Ver; LstBlk *FirstObj, *LastObj; bool ValidBlock(LstBlk *b) const { for (LstBlk *i = FirstObj; i; i = i->Next) if (i == b) return true; return false; } LstBlk *NewBlock(LstBlk *Where) { LstBlk *i = new LstBlk; LAssert(i != NULL); if (!i) return NULL; if (Where) { i->Prev = Where; if (i->Prev->Next) { // Insert i->Next = Where->Next; i->Prev->Next = i->Next->Prev = i; } else { // Append i->Prev->Next = i; LAssert(LastObj == Where); LastObj = i; } } else { // First object LAssert(FirstObj == 0); LAssert(LastObj == 0); FirstObj = LastObj = i; } return i; } bool DeleteBlock(LstBlk *i) { if (!i) { LAssert(!"No ptr."); return false; } if (i->Prev != 0 && i->Next != 0) { LAssert(FirstObj != i); LAssert(LastObj != i); } if (i->Prev) { i->Prev->Next = i->Next; } else { LAssert(FirstObj == i); FirstObj = i->Next; } if (i->Next) { i->Next->Prev = i->Prev; } else { LAssert(LastObj == i); LastObj = i->Prev; } delete i; Ver++; // This forces the iterators to recheck their block ptr return true; } bool Insert(LstBlk *i, T *p, ssize_t Index = -1) { if (!i) return false; if (i->Full()) { if (!i->Next) { // Append a new LstBlk if (!NewBlock(i)) return false; } if (Index < 0) return Insert(i->Next, p, Index); // Push last pointer into Next if (i->Next->Full()) NewBlock(i); // Create an empty Next if (!Insert(i->Next, i->Ptr[ITEM_PTRS-1], 0)) return false; i->Count--; Items--; // We moved the item... not inserted it. // Fall through to the local "non-full" insert... } LAssert(!i->Full()); if (Index < 0) Index = i->Count; else if (Index < i->Count) memmove(i->Ptr+Index+1, i->Ptr+Index, (i->Count-Index) * sizeof(p)); i->Ptr[Index] = p; i->Count++; Items++; LAssert(i->Count <= ITEM_PTRS); return true; } Iter GetIndex(size_t Index, size_t *Base = NULL) const { size_t n = 0; for (LstBlk *i = FirstObj; i; i = i->Next) { if (Index >= n && Index < n + i->Count) { if (Base) *Base = n; return Iter(this, i, (int) (Index - n)); } n += i->Count; } if (Base) *Base = 0; return Iter(this); } Iter GetPtr(T *Ptr, size_t *Base = NULL) { size_t n = 0; for (LstBlk *i = FirstObj; i; i = i->Next) { for (int k=0; kCount; k++) { if (i->Ptr[k] == Ptr) { if (Base) *Base = n; return Iter(this, i, k); } } n += i->Count; } if (Base) *Base = 0; return Iter(this); } public: List() { FirstObj = LastObj = NULL; Items = 0; Ver = 0; } ~List() { VALIDATE(); Empty(); } size_t Length() const { return Items; } bool Length(size_t Len) { if (Len == 0) return Empty(); else if (Len == Items) return true; VALIDATE(); bool Status = false; if (Len < Items) { // Decrease list size... size_t Base = 0; Iter i = GetIndex(Len, &Base); if (i.i) { size_t Offset = Len - Base; if (!(Offset <= i.i->Count)) { LAssert(!"Offset error"); } i.i->Count = (uint8_t) (Len - Base); LAssert(i.i->Count >= 0 && i.i->Count < ITEM_PTRS); while (i.i->Next) { DeleteBlock(i.i->Next); } Items = Len; } else LAssert(!"Iterator invalid."); } else { // Increase list size... LAssert(!"Impl me."); } VALIDATE(); return Status; } bool Empty() { VALIDATE(); LstBlk *n; for (LstBlk *i = FirstObj; i; i = n) { n = i->Next; delete i; } FirstObj = LastObj = NULL; Items = 0; Ver++; VALIDATE(); return true; } bool DeleteAt(size_t i) { VALIDATE(); Iter p = GetIndex(i); if (!p.In()) return false; bool Status = Delete(p); VALIDATE(); return Status; } bool Delete(Iter &Pos) { if (!Pos.In()) return false; int &Index = Pos.Cur; LstBlk *&i = Pos.i; if (Index < i->Count-1) memmove(i->Ptr+Index, i->Ptr+Index+1, (i->Count-Index-1) * sizeof(T*)); Items--; if (--i->Count == 0) { // This Item is now empty, remove and reset current // into the next Item LstBlk *n = i->Next; bool Status = DeleteBlock(i); Pos.Cur = 0; Pos.i = n; return Status; } else if (Index >= i->Count) { // Carry current item over to next Item Pos.i = Pos.i->Next; Pos.Cur = 0; } return true; } bool Delete(T *Ptr) { VALIDATE(); Iter It = GetPtr(Ptr); if (!It.In()) return false; bool Status = Delete(It); VALIDATE(); return Status; } bool Insert(T *p, ssize_t Index = -1) { VALIDATE(); if (!LastObj) { LstBlk *b = NewBlock(NULL); if (!b) return false; b->Ptr[b->Count++] = p; Items++; VALIDATE(); return true; } bool Status; size_t Base; Iter Pos(this); if (Index < 0) Status = Insert(LastObj, p, Index); else { Pos = GetIndex(Index, &Base); if (Pos.i) Status = Insert(Pos.i, p, (int) (Index - Base)); else Status = Insert(LastObj, p, -1); } VALIDATE(); LAssert(Status); return Status; } bool Add(T *p) { return Insert(p); } T *operator [](size_t Index) const { VALIDATE(); auto it = GetIndex(Index); VALIDATE(); return it; } ssize_t IndexOf(T *p) { VALIDATE(); size_t Base = -1; auto It = GetPtr(p, &Base); LAssert(Base != -1); ssize_t Idx = It.In() ? Base + It.Cur : -1; VALIDATE(); return Idx; } bool HasItem(T *p) { VALIDATE(); Iter Pos = GetPtr(p); bool Status = Pos.In(); VALIDATE(); return Status; } T *ItemAt(ssize_t i) { VALIDATE(); Iter It = GetIndex(i); VALIDATE(); return It; } /// Sorts the list template void Sort ( /// The callback function used to compare 2 pointers int (*Compare)(T *a, T *b, User data), /// User data that is passed into the callback User Data = 0 ) { if (Items < 1) return; VALIDATE(); // LProfile prof("ListSort"); // Save the List to an Array LArray a; a.Length(Length()); T **p = a.AddressOf(); for (LstBlk *i = FirstObj; i; i = i->Next) for (int n=0; nCount; n++) *p++ = i->Ptr[n]; // prof.Add("Compare"); struct UserData { int (*Compare)(T *a, T *b, User data); User Data; } ud = {Compare, Data}; #if !defined(WINDOWS) && !defined(HAIKU) && !defined(LINUX) #define USER_DATA_FIRST 1 #else #define USER_DATA_FIRST 0 #endif #if defined(WINDOWS) /* _ACRTIMP void __cdecl qsort_s(void* _Base, rsize_t _NumOfElements, rsize_t _SizeOfElements, int (__cdecl* _PtFuncCompare)(void*, void const*, void const*), void* _Context); */ qsort_s #else qsort_r #endif ( a.AddressOf(), a.Length(), sizeof(T*), #if USER_DATA_FIRST &ud, #endif #if defined(HAIKU) || defined(LINUX) // typedef int (*_compare_function_qsort_r)(const void*, const void*, void*); // extern void qsort_r(void* base, size_t numElements, size_t sizeOfElement, _compare_function_qsort_r, void* cookie); [](const void *a, const void *b, void *ud) -> int #else [](void *ud, const void *a, const void *b) -> int #endif { auto *user = (UserData*)ud; return user->Compare(*(T**)a, *(T**)b, user->Data); } #if !USER_DATA_FIRST , &ud #endif ); // prof.Add("Copy"); // Copy back to the List p = a.AddressOf(); for (LstBlk *i = FirstObj; i; i = i->Next) for (int n=0; nCount; n++) i->Ptr[n] = *p++; VALIDATE(); } /// Delete all pointers in the list as dynamically allocated objects void DeleteObjects() { VALIDATE(); LstBlk *n; for (LstBlk *i = FirstObj; i; i = n) { n = i->Next; for (int n=0; nCount; n++) { if (i->Ptr[n]) { #ifdef _DEBUG size_t Objs = Items; #endif delete i->Ptr[n]; #ifdef _DEBUG if (Objs != Items) LAssert(!"Do you have self deleting objects?"); #endif i->Ptr[n] = NULL; } } delete i; } FirstObj = LastObj = NULL; Items = 0; Ver++; VALIDATE(); } /// Delete all pointers in the list as dynamically allocated arrays void DeleteArrays() { VALIDATE(); LstBlk *n; for (LstBlk *i = FirstObj; i; i = n) { n = i->Next; for (int n=0; nCount; n++) { delete [] i->Ptr[n]; i->Ptr[n] = NULL; } delete i; } FirstObj = LastObj = NULL; Items = 0; Ver++; VALIDATE(); } void Swap(List &other) { LSwap(FirstObj, other.FirstObj); LSwap(LastObj, other.LastObj); LSwap(Items, other.Items); LSwap(Ver, other.Ver); } /// Assign the contents of another list to this one #if 0 List &operator=(const List &lst) { Empty(); for (auto i : lst) Add(i); return *this; } #else List &operator =(const List &lst) { VALIDATE(); // Make sure we have enough blocks allocated size_t i = 0; // Set the existing blocks to empty... for (LstBlk *out = FirstObj; out; out = out->Next) { out->Count = 0; i += ITEM_PTRS; } // If we don't have enough, add more... while (i < lst.Length()) { LstBlk *out = NewBlock(LastObj); if (out) i += ITEM_PTRS; else { LAssert(!"Can't allocate enough blocks?"); return *this; } } // If we have too many, free some... while (LastObj && i > lst.Length() + ITEM_PTRS) { DeleteBlock(LastObj); i -= ITEM_PTRS; } // Now copy over the block's contents. LstBlk *out = FirstObj; Items = 0; for (LstBlk *in = lst.FirstObj; in; in = in->Next) { for (int pos = 0; pos < in->Count; ) { if (!out->Remaining()) { out = out->Next; if (!out) { LAssert(!"We should have pre-allocated everything..."); return *this; } } int Cp = MIN(out->Remaining(), in->Count - pos); LAssert(Cp > 0); memcpy(out->Ptr + out->Count, in->Ptr + pos, Cp * sizeof(T*)); out->Count += Cp; pos += Cp; Items += Cp; } } VALIDATE(); return *this; } #endif Iter begin(ssize_t At = 0) { return GetIndex(At); } Iter rbegin(ssize_t At = 0) { return GetIndex(Length()-1); } Iter end() { return Iter(this, NULL, -1); } bool Validate() const { if (FirstObj == NULL && LastObj == NULL && Items == 0) return true; #if LGI_LIST_VALIDATION size_t n = 0; LstBlk *Prev = NULL; for (LstBlk *i = FirstObj; i; i = i->Next) { for (int k=0; kCount; k++) { if (!i->Ptr[k]) { LAssert(!"NULL pointer in LstBlk."); return false; } else { n++; } } if (i == FirstObj) { if (i->Prev) { LAssert(!"First object's 'Prev' should be NULL."); return false; } } else if (i == LastObj) { if (i->Next) { LAssert(!"Last object's 'Next' should be NULL."); return false; } } else { if (i->Prev != Prev) { LAssert(!"Middle LstBlk 'Prev' incorrect."); return false; } } Prev = i; } if (Items != n) { LAssert(!"Item count cache incorrect."); return false; } #endif return true; } }; /// \brief Data storage class. /// /// Allows data to be buffered in separate memory /// blocks and then written to a continuous block for processing. Works /// as a first in first out que. You can (and should) provide a suitable /// PreAlloc size to the constructor. This can reduce the number of blocks /// of memory being used (and their associated alloc/free time and /// tracking overhead) in high volume situations. class LgiClass LMemQueue : public LStream { protected: /// Data block. These can contain a mix of 3 types of data: /// 1) Bytes that have been read (always at the start of the block) /// 2) Bytes that have been written (always in the middle) /// 3) Unwritten buffer space (always at the end) /// /// Initially a block starts out as completely type 3 bytes, just garbage /// data waiting to be written to. Then something writes into the pipe and bytes /// are stored into this free space, and the 'Used' variable is set to show /// how much of the available buffer is used. At this point we have type 2 and /// type 3 bytes in the block. The buffer can fill up completely in which /// case Used == Size and there are no type 3 bytes left. Also at some point /// something can start reading bytes out of the block which causes the 'Next' /// value to be increased, at which point the block starts with 'Next' bytes /// of type 1. Crystal? struct Block { /// Number of bytes in this block that have been read /// Type 1 or 'read' bytes are in [0,Next-1]. int Next = 0; /// Number of bytes that are used in this block include read bytes. /// Type 2 or 'used' bytes are in [Next,Used-1]. int Used = 0; /// Total size of the memory block /// Type 3 or 'unused' bytes are in [Used,Size-1]. int Size = 0; uint8_t *Ptr() { return (uint8_t*) (this + 1); } uint8_t *Start() { return Ptr() + Next; } ssize_t Length() { return Used - Next; } void Free(); }; ssize_t PreAlloc; List Mem; bool _debug = false; public: /// Constructor LMemQueue ( /// Sets the block size, which means allocating ahead and then joining /// together smaller inserts into 1 continuous block. size_t PreAlloc = 0 ); /// Destructor virtual ~LMemQueue(); LMemQueue &operator =(LMemQueue &p); /// Empties the container freeing any memory used. virtual void Empty(); /// Returns a dynamically allocated block that contains all the data in the container /// in a continuous block. virtual void *New ( /// If this is > 0 then the function add's the specified number of bytes containing /// the value 0 on the end of the block. This is useful for NULL terminating strings /// or adding buffer space on the end of the block returned. ssize_t AddBytes = 0 ); /// Reads data from the start of the container without removing it from /// the que. Returns the bytes copied. virtual int64 Peek ( /// Buffer for output uchar *Ptr, /// Bytes to look at ssize_t Size ); /// Gets the total bytes in the container int64 GetSize() override; /// Reads bytes off the start of the container ssize_t Read(void *Buffer, ssize_t Size, int Flags = 0) override; /// Writes bytes to the end of the container ssize_t Write(const void *Buffer, ssize_t Size, int Flags = 0) override; bool Write(const LString &s) { return Write(s.Get(), s.Length()) == s.Length(); } /// Search for a substring and return it's index, or -1 if not found. ssize_t Find(LString str, bool caseSensitive = true); /// Iterate over the data in the container /// Callback should return true to keep iterating... void Iterate(std::function callback, bool reverse = false); /// For processes that read from one source and then write into this container, it is better /// to not have to copy the data into some local buffer and then again into the mem queue. /// So this allows allocating internal blocks and returning them directly to the writer. After /// the source has written into the internal block the client calls Buffer::Commit to tell the /// LMemQueue how much data has been written. class LgiClass Buffer { friend class LMemQueue; LMemQueue *mq = NULL; Block *blk = NULL; public: uint8_t *ptr = NULL; size_t len = 0; Buffer(LMemQueue *memq) : mq(memq) {} // Call this after writing data into the 'ptr' buffer. bool Commit(size_t bytes); }; /// Find a buffer and return it. Call Buffer::Commit after writting data to the start of the block. /// On error Buffer.ptr is NULL. Buffer GetBuffer(); }; /// A version of GBytePipe for strings. Adds some special handling for strings. class LgiClass LStringPipe : public LMemQueue { ssize_t LineChars(); ssize_t SaveToBuffer(char *Str, ssize_t BufSize, ssize_t Chars); public: /// Constructs the object LStringPipe ( /// Number of bytes to allocate per block. int PreAlloc = -1 ) : LMemQueue(PreAlloc) {} ~LStringPipe() {} virtual ssize_t Pop(char *Str, ssize_t Chars); virtual ssize_t Pop(LArray &Buf); virtual LString Pop(); virtual ssize_t Push(const char *Str, ssize_t Chars = -1); virtual ssize_t Push(const char16 *Str, ssize_t Chars = -1); char *NewStr() { return (char*)New(sizeof(char)); } - LString NewGStr(); + LString NewLStr(); char16 *NewStrW() { return (char16*)New(sizeof(char16)); } LStringPipe &operator +=(const LString &s) { Write(s.Get(), s.Length()); return *this; } #ifdef _DEBUG static bool UnitTest(); #endif }; #define LMEMFILE_BLOCKS 8 class LgiClass LMemFile : public LStream { struct Block { size_t Offset; size_t Used; uint8_t Data[1]; }; // The current file pointer uint64 CurPos; /// Number of blocks in use int Blocks; /// Data payload of blocks int BlockSize; /// Some blocks are built into the struct, no memory alloc needed. Block *Local[LMEMFILE_BLOCKS]; // Buf if they run out we can alloc some more. LArray Extra; Block *Get(int Index); bool FreeBlock(Block *b); Block *GetLast() { return Get(Blocks-1); } Block *Create(); int CurBlock() { return (int) (CurPos / BlockSize); } public: LMemFile(int BlkSize = 256); ~LMemFile(); void Empty(); int64 GetSize() override; int64 SetSize(int64 Size) override; int64 GetPos() override; int64 SetPos(int64 Pos) override; ssize_t Read(void *Ptr, ssize_t Size, int Flags = 0) override; ssize_t Write(const void *Ptr, ssize_t Size, int Flags = 0) override; }; #endif diff --git a/include/lgi/common/Json.h b/include/lgi/common/Json.h --- a/include/lgi/common/Json.h +++ b/include/lgi/common/Json.h @@ -1,550 +1,550 @@ #ifndef _LJSON_H_ #define _LJSON_H_ #include "lgi/common/Array.h" #include "lgi/common/StringClass.h" #define SkipWs(s) while (*s && strchr(" \t\r\n", *s)) s++; class LJson { struct Key { LString Name; LString Str; LArray Obj; LArray Array; Key *Get(const char *name, bool create = false) { if (Name.Equals(name)) return this; for (unsigned i=0; iGet(n, Create); } return k; } }; Key Root; const char *Start; bool ParseString(LString &s, const char *&c) { SkipWs(c); if (*c != '\"') return false; c++; const char *e = c; while (*e) { if (*e == '\\') e += 2; else if (*e == '\"') break; else e++; } if (*e != '\"') return false; s.Set(c, e - c); c = e + 1; return true; } bool ParseChar(char ch, const char *&c) { SkipWs(c); if (*c != ch) return false; c++; SkipWs(c); return true; } bool IsNumeric(char s) { return IsDigit(s) || strchr("-.e", s) != NULL; } bool ParseArray(LArray &Array, const char *&c) { if (*c != '[') { LAssert(0); return false; } c++; SkipWs(c); while (*c != ']') { auto Idx = Array.Length(); Key &a = Array[Idx]; if (!Parse(a, c)) { Array.Length(Idx); // Delete failed object return false; } SkipWs(c); if (*c == ',') { c++; SkipWs(c); } } if (*c != ']') { LAssert(!"Unexpected token."); return false; } c++; return true; } // Value parser: can be one of: // - Object // - Array // - String // - Number // - Boolean (true or false) // - NULL bool Parse(Key &k, const char *&c) { SkipWs(c); if (*c == '\"') { // String return ParseString(k.Str, c); } else if (*c == '{') { // Objects c++; SkipWs(c); while (*c && *c != '}') { LString n; if (!ParseString(n, c)) return false; if (!ParseChar(':', c)) return false; auto Idx = k.Obj.Length(); auto &Member = k.Obj[Idx]; Member.Name = n; if (!Parse(Member, c)) { k.Obj.Length(Idx); // Delete incomplete obj. return false; } SkipWs(c); if (*c == ',') { c++; SkipWs(c); } } if (*c == '}') { c++; return true; } } else if (*c == '[') { // Parse array return ParseArray(k.Array, c); } else if (IsNumeric(*c)) { const char *e = c; while (*e && IsNumeric(*e)) e++; k.Str.Set(c, e - c); c = e; return true; } else if (IsAlpha(*c)) { // Boolean? const char *e = c; while (*e && IsAlpha(*e)) e++; k.Str.Set(c, e - c); c = e; return true; } else if (!*c) return true; return false; } Key *Deref(LString Addr, bool Create) { return Root.Deref(Addr, Create); } public: LJson(const char *c = NULL) { Start = NULL; if (c) SetJson(c); } void Empty() { Root.Empty(); } bool SetJson(const char *c) { Empty(); if (!c) return false; Start = c; bool b = Parse(Root, c); if (!b) LgiTrace("%s:%i - Error at char " LPrintfSizeT "\n", _FL, c - Start); return b; } LString GetJson() { LString s = Root.Print(0); #ifdef _DEBUG LAssert(LIsUtf8(s)); #endif return s; } LString Get(LString Addr) { Key *k = Deref(Addr, false); return k ? k->Str : LString(); } struct Pair { LString key; LString value; Pair() { } Pair(LString k, LString v) : key(k), value(v) { } }; class Iter { LJson *j; LArray *a; public: struct IterPos { Iter *It; size_t Pos; IterPos(Iter *i, size_t p) { It = i; Pos = p; } bool operator !=(const IterPos &i) const { bool Eq = It == i.It && Pos == i.Pos; return !Eq; } IterPos &operator *() { return *this; } operator LString() { return (*It->a)[Pos].Str; } IterPos &operator++() { Pos++; return *this; } LString Get(LString Addr) { auto &Arr = *It->a; if (Pos >= Arr.Length()) return LString(); Key &k = Arr[Pos]; Key *v = k.Deref(Addr, false); if (!v) return LString(); return v->Str; } Pair Get() { auto &Arr = *It->a; if (Pos >= Arr.Length()) return Pair(); Key &k = Arr[Pos]; if (k.Name) return Pair(k.Name, k.Str); if (k.Obj.Length()) return Pair(k.Obj[0].Name, k.Obj[0].Str); return Pair(); } Iter GetArray(LString Addr) { Iter a(It->j); auto &Arr = *It->a; if (Pos < Arr.Length()) { Key *k = Arr[Pos].Deref(Addr, false); if (k) a.Set(&k->Array); } return a; } }; Iter(LJson *J = NULL) : j(J) { a = NULL; } void Set(LJson *J) { j = J; } void Set(LArray *arr) { a = arr; } size_t Length() { return a ? a->Length() : 0; } IterPos begin() { return IterPos(this, 0); } IterPos end() { return IterPos(this, a ? a->Length() : 0); } }; Iter GetArray(LString Addr) { Iter a(this); Key *k = Deref(Addr, false); if (k) a.Set(&k->Array); return a; } bool Set(LString Addr, const char *Val) { Key *k = Deref(Addr, true); if (!k) return false; k->Str = Val; return true; } bool Set(LString Addr, int64_t Int) { char s[32]; sprintf_s(s, sizeof(s), LPrintfInt64, Int); return Set(Addr, s); } bool Set(LString Addr, double Dbl) { char s[32]; sprintf_s(s, sizeof(s), "%f", Dbl); return Set(Addr, s); } bool Set(LString Addr, LArray &Array) { Key *k = Deref(Addr, true); if (!k) return false; for (auto &a : Array) k->Array.New().Str = a; return true; } bool Set(LString Addr, LArray &Array) { Key *k = Deref(Addr, true); if (!k) return false; for (auto &in : Array) { auto &out = k->Array.New(); out = in.Root; } return true; } LArray GetKeys(const char *Addr = NULL) { Key *k = Deref(Addr, true); LArray a; if (k) for (auto &i : k->Obj) a.Add(i.Name); return a; } }; #endif \ No newline at end of file diff --git a/include/lgi/common/Ranges.h b/include/lgi/common/Ranges.h --- a/include/lgi/common/Ranges.h +++ b/include/lgi/common/Ranges.h @@ -1,158 +1,158 @@ #ifndef _RANGES_H_ #define _RANGES_H_ /// This class keeps an array of ranges in sorted order, merging /// adjacent ranges when new ranges are added. class LRanges : public LArray { public: LRanges &operator +=(const LRange &r) { // Insert at the right position if (Length() == 0) New() = r; else if (r > Last()) New() = r; else { size_t s = 0, e = Length() - 1; while (s != e) { size_t mid = s + ((e - s + 1) >> 1); auto &mr = (*this)[mid]; if (r < mr) e = mid - 1; else if (r >= mr.End()) s = mid + 1; else { s = e = mid; break; } } auto &cur = (*this)[s]; if (r < cur) AddAt(s, r); else if (r >= cur.End()) AddAt(s + 1, r); else { // Overlapping? auto us = MIN(r.Start, cur.Start); LRange u(us, MAX(r.End(), cur.End()) - us); cur = u; } } return *this; } /// Returns the complete range, start to end, including any gaps. LRange Union() { LRange u; if (Length() > 0) { u.Start = First().Start; u.Len = Last().End() - u.Start; } return u; } // If you edit values or add ranges outside of the += operator this // will fix the data to be consistent. bool Merge() { bool SortChk = true; RestartMerge: for (unsigned i=0; i int { auto Diff = a->Start - b->Start; if (Diff < 0) return -1; return Diff > 1; } ); goto RestartMerge; } if (a.End() >= b.Start) { // Merge segments a.Len = b.End() - a.Start; if (!DeleteAt(i + 1)) return false; } else i++; } return true; } LString ToString() { LStringPipe p; for (auto r : *this) p.Print(LPrintfSSizeT "-" LPrintfSSizeT ",", r.Start, r.End()); - return p.NewGStr(); + return p.NewLStr(); } bool FromString(LString s) { Length(0); auto Rngs = s.SplitDelimit(","); for (auto r : Rngs) { auto a = r.Split("-"); if (a.Length() != 2) return false; auto &n = New(); n.Start = a[0].Int(); n.Len = a[1].Int() - n.Start; } return true; } bool UnitTests() { /* // Out of order merge test FromString("30-50,20-35,45-60"); Merge(); if (Length() != 1) goto Error; auto &r = (*this)[0]; if (r.Start != 20 || r.Len != 40) goto Error; Length(0); */ // Overlapping insert test LRange *r = NULL; FromString("10-20,30-40,50-60,70-80,90-100"); *this += LRange(55,10); if (Length() != 5) goto Error; r = AddressOf(2); if (r->Start != 50 || r->Len != 15) goto Error; Length(0); return true; Error: LAssert(!"Failed."); return false; } }; #endif diff --git a/include/lgi/common/Store3.h b/include/lgi/common/Store3.h --- a/include/lgi/common/Store3.h +++ b/include/lgi/common/Store3.h @@ -1,726 +1,726 @@ /// \file /// \author Matthew Allen, fret@memecode.com #ifndef _MAIL_STORE_H_ #define _MAIL_STORE_H_ #include #include "Mail.h" #include "Store3Defs.h" #undef GetObject /* Handling of attachments in the Store3 API ----------------------------------------- Given the mail object ptr: LDataI *m; Query that the mail for it's root mime segment: LDataI *Seg = dynamic_cast(m->GetObj(FIELD_MIME_SEG)); Query a seg for it's children: auto Children = Seg->GetList(FIELD_MIME_SEG); auto FirstChild = Children->First(); Access segment's charset and mimetype: char *Charset = Seg->GetStr(FIELD_CHARSET); char *MimeType = Seg->GetStr(FIELD_MIME_TYPE); Get/Set the segment for it's content: GAutoStreamI Data = Seg->GetStream(_FL); // get Seg->SetStream(new MyStream(Data)); // set Delete an mime segment: Seg->Delete(); Add a new segment somewhere in the tree, including reparenting it to another segments or mail object, even if the target parent it not attached yet: NewSeg->Save(ParentSeg); */ #include "LgiInterfaces.h" #include "lgi/common/Mime.h" #include "lgi/common/OptionsFile.h" #include "lgi/common/Variant.h" class LDataI; class LDataFolderI; class LDataStoreI; class LDataPropI; typedef LAutoPtr LAutoStreamI; -void ParseIdList(char *In, List &Out); +LString::Array ParseIdList(const char *In); extern const char *Store3ItemTypeToMime(Store3ItemTypes type); /// A storage event /// a = StoreId /// b = (void*)UserParam /// \sa LDataEventsI::Post #define M_STORAGE_EVENT (M_USER+0x500) /// The storage class has this property (positive properties are owned by the app #define FIELD_IS_ONLINE -100 #define FIELD_PROFILE_IMAP_LISTING -101 #define FIELD_PROFILE_IMAP_SELECT -102 #define LDATA_INT32_PROP(name, id) \ int32 Get##name() { return GetObject() ? (int32)GetObject()->GetInt(id) : OnError(_FL); } \ bool Set##name(int32 val) { return GetObject() ? GetObject()->SetInt(id, val) >= Store3Delayed : OnError(_FL); } #define LDATA_INT64_PROP(name, id) \ int64 Get##name() { return GetObject() ? GetObject()->GetInt(id) : OnError(_FL); } \ bool Set##name(int64 val) { return GetObject() ? GetObject()->SetInt(id, val) >= Store3Delayed : OnError(_FL); } #define LDATA_ENUM_PROP(name, id, type) \ type Get##name() { return (type) (GetObject() ? GetObject()->GetInt(id) : OnError(_FL)); } \ bool Set##name(type val) { return GetObject() ? GetObject()->SetInt(id, (int)val) >= Store3Delayed : OnError(_FL); } #define LDATA_STR_PROP(name, id) \ const char *Get##name() { auto o = GetObject(); return o ? o->GetStr(id) : (const char*)OnError(_FL); } \ bool Set##name(const char *val) { return GetObject() ? GetObject()->SetStr(id, val) >= Store3Delayed : OnError(_FL); } #define LDATA_DATE_PROP(name, id) \ const LDateTime *Get##name() { return (GetObject() ? GetObject()->GetDate(id) : (LDateTime*)OnError(_FL)); } \ bool Set##name(const LDateTime *val) { return GetObject() ? GetObject()->SetDate(id, val) >= Store3Delayed : OnError(_FL); } #define LDATA_INT_TYPE_PROP(type, name, id, defaultVal) \ type Get##name() { return (type) (GetObject() ? GetObject()->GetInt(id) : OnError(_FL)); } \ bool Set##name(type val = defaultVal) { return GetObject() ? GetObject()->SetInt(id, val) >= Store3Delayed : OnError(_FL); } /// This class is an interface to a collection of objects (NOT thread-safe). typedef std::function LIteratorProgressFn; template class LDataIterator { public: virtual ~LDataIterator() {} /// \returns an empty object of the right type. virtual T Create(LDataStoreI *Store) = 0; /// \returns the first object (NOT thread-safe) virtual T First() = 0; /// \returns the first object (NOT thread-safe) virtual T Next() = 0; /// \returns the number of items in the collection virtual size_t Length() = 0; /// \returns the 'nth' item in the collection virtual T operator [](size_t idx) = 0; /// \returns the index of the given item in the collection virtual ssize_t IndexOf(T n, bool NoAssert = false) = 0; /// Deletes an item /// \returns true on success virtual bool Delete(T ptr) = 0; /// Inserts an item at 'idx' or the end if not supplied. /// \returns true on success virtual bool Insert(T ptr, ssize_t idx = -1, bool NoAssert = false) = 0; /// Clears list, but doesn't delete objects. /// \returns true on success virtual bool Empty() = 0; /// Deletes all the objects from memory /// \returns true on success virtual bool DeleteObjects() = 0; /// Gets the current loading/loaded state. virtual Store3State GetState() = 0; /// Sets the progress function virtual void SetProgressFn(LIteratorProgressFn cb) = 0; }; typedef LDataIterator *LDataIt; #define EmptyVirtual(t) LAssert(0); return t #define Store3CopyDecl bool CopyProps(LDataPropI &p) override #define Store3CopyImpl(Cls) bool Cls::CopyProps(LDataPropI &p) /// A generic interface for getting / setting properties. class LDataPropI : virtual public LDom { LDataPropI &operator =(LDataPropI &p) = delete; public: virtual ~LDataPropI() {} /// Copy all the values from 'p' over to this object virtual bool CopyProps(LDataPropI &p) { return false; } /// Gets a string property virtual const char *GetStr(int id) { EmptyVirtual(NULL); } /// Sets a string property, it will make a copy of the string, so you /// still retain ownership of the string you're passing in. virtual Store3Status SetStr(int id, const char *str) { EmptyVirtual(Store3Error); } /// Gets an integer property. virtual int64 GetInt(int id) { EmptyVirtual(false); } /// Sets an integer property. virtual Store3Status SetInt(int id, int64 i) { EmptyVirtual(Store3Error); } /// Gets a date property virtual const LDateTime *GetDate(int id) { EmptyVirtual(NULL); } /// Sets a date property virtual Store3Status SetDate(int id, const LDateTime *i) { EmptyVirtual(Store3Error); } /// Gets a variant virtual const LVariant *GetVar(int id) { EmptyVirtual(NULL); } /// Sets a variant property virtual Store3Status SetVar(int id, LVariant *i) { EmptyVirtual(Store3Error); } /// Gets a sub object pointer virtual LDataPropI *GetObj(int id) { EmptyVirtual(NULL); } /// Sets a sub object pointer virtual Store3Status SetObj(int id, LDataPropI *i) { EmptyVirtual(Store3Error); } /// Gets an iterator interface to a list of sub-objects. virtual LDataIt GetList(int id) { EmptyVirtual(NULL); } /// Set the mime segments virtual Store3Status SetRfc822(LStreamI *Rfc822Msg) { LAssert(!"Pretty sure you should be implementing this"); return Store3Error; } }; #pragma warning(default:4263) class LDataUserI { friend class LDataI; LDataI *Object; public: LString SetterRef; LDataUserI(); virtual ~LDataUserI(); LDataI *GetObject(); virtual bool SetObject ( /// The client side object to link with this object. LDataI *o, /// In the special case that 'Object' is being deleted, and is an /// orphaned objects, SetObject must not attempt to delete 'Object' /// a second time. This flag allows for that case. bool InDestuctor, /// The file name of the caller const char *File, /// The line number of the caller int Line ); }; /// This class is an interface between the UI and the back end for things /// like email, contacts, calendar events, groups and filters class LDataI : virtual public LDataPropI { friend class LDataUserI; LDataI &operator =(LDataI &p) = delete; public: LDataUserI *UserData; LDataI() { UserData = NULL; } virtual ~LDataI() { if (UserData) UserData->Object = NULL; } /// Returns the type of object /// \sa MAGIC_MAIL and it's like virtual uint32_t Type() = 0; /// \return true if the object has been written to disk. By default the object /// starts life in memory only. virtual bool IsOnDisk() = 0; /// \return true if the object is owned by some other object... virtual bool IsOrphan() = 0; /// \returns size of object on disk virtual uint64 Size() = 0; /// Saves the object to disk. If this function fails the object /// is deleted, so if it returns false, stop using the ptr you /// have to it. /// \returns true if successful. virtual Store3Status Save(LDataI *Parent = 0) = 0; /// Delete the on disk representation of the object. This will cause LDataEventsI::OnDelete /// to be called after which this object will be freed from heap memory automatically. So /// Once you call this method assume the object pointed at is gone. virtual Store3Status Delete(bool ToTrash = true) = 0; /// Gets the storage that this object belongs to. virtual LDataStoreI *GetStore() = 0; /// \returns a stream to access the data stored at this node. The caller /// is responsible to free the stream when finished with it. /// For Type == MAGIC_ATTACHMENT: the decoded body of the MIME segment. /// For Type == MAGIC_MAIL: is an RFC822 encoded version of the email. /// For other objects the stream is not defined. virtual LAutoStreamI GetStream(const char *file, int line) = 0; /// Sets the stream, which is used during the next call to LDataI::Save, which /// also deletes the object when it's used. The caller loses ownership of the /// object passed into this function. virtual bool SetStream(LAutoStreamI stream) { return false; } /// Parses the headers of the object and updates all the metadata fields virtual bool ParseHeaders() { return false; } }; /// An interface to a folder structure class LDataFolderI : virtual public LDataI { LDataFolderI &operator =(LDataFolderI &p) = delete; public: virtual ~LDataFolderI() {} /// \returns an iterator for the sub-folders. virtual LDataIterator &SubFolders() = 0; /// \returns an iterator for the child objects virtual LDataIterator &Children() = 0; /// \returns an iterator for the fields this folder defines virtual LDataIterator &Fields() = 0; /// Deletes all child objects from disk and memory. /// \return true on success; virtual Store3Status DeleteAllChildren() { return Store3Error; } /// Frees all the memory used by children objects without deleting from disk virtual Store3Status FreeChildren() { return Store3Error; } /// Called when the user selects the folder in the UI virtual void OnSelect(bool s) {} /// Called when the user selects a relevant context menu command virtual void OnCommand(const char *Name) {} }; #pragma warning(error:4263) /// Event callback interface. Calls to these methods may be in a worker /// thread, so make appropriate locking or pass the event off to the GUI /// thread via a message. class LDataEventsI { public: virtual ~LDataEventsI() {} /// This allows the caller to pass source:line info for debugging /// It should be called prior to one of the following functions and /// expires immediately after the function call. virtual void SetContext(const char *file, int line) {} /// Posts something to the GUI thread /// \sa M_STORAGE_EVENT virtual void Post(LDataStoreI *store, void *Param) {} /// \returns the system path virtual bool GetSystemPath(int Folder, LVariant &Path) { return false; } /// \returns the options object virtual LOptionsFile *GetOptions(bool Create = false) { return 0; } /// A new item is available virtual void OnNew(LDataFolderI *parent, LArray &new_items, int pos, bool is_new) = 0; /// When an item is deleted virtual bool OnDelete(LDataFolderI *parent, LArray &items) = 0; /// When an item is moved to a new folder virtual bool OnMove(LDataFolderI *new_parent, LDataFolderI *old_parent, LArray &items) = 0; /// When an item changes virtual bool OnChange(LArray &items, int FieldHint) = 0; /// Notifcation of property change virtual void OnPropChange(LDataStoreI *Store, int Prop, LVariantType Type) {} /// Get the logging stream virtual LStreamI *GetLogger(LDataStoreI *store) { return 0; } /// Search for a object by type and name virtual bool Match(LDataStoreI *store, LDataPropI *Addr, int ObjectType, LArray &Matches) { return 0; } }; /// The virtual mail storage interface from which all mail stores inherit from. /// /// The data store should implement LDataPropI::GetInt and handle these properties: /// - FIELD_STATUS, acceptable returns value are: /// * 0 - mail store is in error. /// * 1 - mail store is ready to use / ok. /// * 2 - mail store requires upgrading to use. /// These are codified in the enum LDataStoreI::DataStoreStatus /// - FIELD_READONLY, return values are: /// * false - mail store is read/write /// * true - mail store is read only /// - FIELD_VERSION, existing return values are: /// * 3 - a 'mail3' Scribe sqlite database. /// * 10 - the Scribe IMAP implementation. /// If you are create a new mail store use, values above 10 for your implementation /// and optionally register them with Memecode for inclusion here. /// - FIELD_IS_ONLINE, optionally returned if mail store is online or not. /// - FIELD_ACCOUNT_ID, optionally return if the mail store is associated with an account. /// /// The data store may optionally implement LDataPropI::SetInt to handle this property: /// - FIELD_IS_ONLINE, acceptable values are: /// * false - take the mail store offline. /// * true - go online. /// This is currently only implemented on the IMAP mail store. class LDataStoreI : virtual public LDataPropI { public: static LHashTbl,LDataStoreI*> Map; int Id; class LDsTransaction { protected: LDataStoreI *Store; public: LDsTransaction(LDataStoreI *s) { Store = s; } virtual ~LDsTransaction() {} }; typedef LAutoPtr StoreTrans; LDataStoreI() { LAssert(LAppInst->InThread()); while (Map.Find(Id = LRand(1000))) ; Map.Add(Id, this); } virtual ~LDataStoreI() { LAssert(LAppInst->InThread()); if (!Map.Delete(Id)) LAssert(!"Delete failed."); } /// \returns size of object on disk virtual uint64 Size() = 0; /// Create a new data object that isn't written to disk yet virtual LDataI *Create(int Type) = 0; /// Get the root folder object virtual LDataFolderI *GetRoot(bool create = false) = 0; /// Move objects into a different folder. /// /// Success: /// 'Items' are owned by 'NewFolder', and not any previous folder. /// Any LDataEventsI interface owned by the data store has it's 'OnMove' method called. /// /// Failure: /// 'Item' is owned by it's previous folder. /// /// \return true on success. virtual Store3Status Move ( /// The folder to move the object to LDataFolderI *NewFolder, /// The object to move LArray &Items ) = 0; /// Deletes items, which results in either the items being moved to the local trash folder /// or the items being completely deleted if there is no local trash. The items should all /// be in the same folder and of the same type. /// /// Success: /// 'Items' are either owned by the local trash and not any previous folder, and /// LDataEventsI::OnMove is called. /// -or- /// 'Items' are completely deleted and removed from it's parent and /// LDataEventsI::OnDelete is called, after which the objects are freed. /// /// Failure: /// 'Items' are owned by it's previous folder. /// /// \return true on success. virtual Store3Status Delete ( /// The object to delete LArray &Items, /// Send to the trash or not... bool ToTrash ) = 0; /// Changes items, which results in either the items properties being adjusted. /// /// Success: /// The items properties are changed, and the LDataEventsI::OnChange callback /// is made. /// /// Failure: /// Items are not changed. No callback is made. /// /// \return true on success. virtual Store3Status Change ( /// The object to change LArray &Items, /// The property to change... int PropId, /// The value to assign /// (GV_INT32/64 -> SetInt, GV_DATETIME -> SetDateTime, GV_STRING -> SetStr) LVariant &Value, /// Optional operator for action LOperator Operator ) = 0; /// Compact the mail store virtual void Compact ( /// The parent window of the UI LViewI *Parent, /// The store should pass information up to the UI via setting various parameters from Store3UiFields LDataPropI *Props, /// The callback to get status, could be called by a worker thread... std::function OnStatus ) = 0; /// Upgrades the mail store to the current version for this build. You should call this in response /// to getting Store3UpgradeRequired back from this->GetInt(FIELD_STATUS). virtual void Upgrade ( /// The parent window of the UI LViewI *Parent, /// The store should pass information up to the UI via setting various parameters from Store3UiFields LDataPropI *Props, /// The callback to get status, could be called by a worker thread... std::function OnStatus ) { if (OnStatus) OnStatus(false); } /// Tries to repair the database. virtual void Repair ( /// The parent window of the UI LViewI *Parent, /// The store should pass information up to the UI via setting various parameters from Store3UiFields LDataPropI *Props, /// The callback to get status, could be called by a worker thread... std::function OnStatus ) { if (OnStatus) OnStatus(false); } /// Set the sub-format virtual bool SetFormat ( /// The parent window of the UI LViewI *Parent, /// The store should pass information up to the UI via setting various parameters from Store3UiFields LDataPropI *Props ) { return false; } /// Called when event posted virtual void OnEvent(void *Param) = 0; /// Called when the application is not receiving messages. /// \returns false to wait for more messages. virtual bool OnIdle() = 0; /// Gets the events interface virtual LDataEventsI *GetEvents() = 0; /// Start a scoped transaction virtual StoreTrans StartTransaction() { return StoreTrans(0); } }; /// Open a mail3 folder /// \return a valid ptr or NULL on failure extern LDataStoreI *OpenMail3 ( /// The file to open const char *Mail3Folder, /// Event interface, LDataEventsI *Callback, /// true if you want to create a new mail3 file. bool Create = false ); /// Open am imap store /// \return a valid ptr or NULL on failure extern LDataStoreI *OpenImap ( /// The host name of the IMAP server char *Host, /// The port to connect to, or <= 0 means use default int Port, /// The user name of the account to connect to char *User, /// [Optional] The password of the user char *Pass, /// Various flags that control the type of connection made: /// \sa #MAIL_SSL, #MAIL_SECURE_AUTH int ConnectFlags, /// Callback interface for various events... LDataEventsI *Callback, /// This allows the IMAP client to request SSL support from the /// parent applications. LCapabilityClient *caps, /// Pointers to the progress info bars, or NULL if not needed. MailProtocolProgress *prog[2], /// The logging stream. LStream *Log, /// The identifier for the account int AccoundId, /// An interface into the persistant storage area. LAutoPtr store ); #ifdef WIN32 /// Open a MAPI store /// \return a valid ptr or NULL on failure extern LDataStoreI *OpenMapiStore ( /// The MAPI profile name const char *Profile, /// The username to login as const char *Username, /// Their password const char *Password, /// The account ID uint64 AccountId, /// Event interface, LDataEventsI *Callback ); #endif ////////////////////////////////////////////////////////////////////////////// // Common implementation of interfaces template class DNullIterator : public LDataIterator { public: T First() { return 0; } T Next() { return 0; } int Length() { return 0; } T operator [](int idx) { return 0; } bool Delete(T ptr) { return 0; } bool Insert(T ptr, int idx = -1, bool NoAssert = false) { return 0; } bool DeleteObjects() { return 0; } bool Empty() { return false; } int IndexOf(T n, bool NoAssert = false) { return -1; } }; template class DIterator : public LDataIterator { int Cur; public: LArray a; Store3State State; LIteratorProgressFn Prog; DIterator() { Cur = -1; State = Store3Unloaded; } Store3State GetState() { return State; } void SetProgressFn(LIteratorProgressFn cb) { Prog = cb; } void Swap(DIterator &di) { LSwap(Cur, di.Cur); LSwap(State, di.State); a.Swap(di.a); } TPub *Create(LDataStoreI *Store) { LAssert(State == Store3Loaded); return new TPriv(dynamic_cast(Store)); } TPub *First() { LAssert(State == Store3Loaded); Cur = 0; return (int)a.Length() > Cur ? a[Cur] : 0; } TPub *Next() { LAssert(State == Store3Loaded); Cur++; return (int)a.Length() > Cur ? a[Cur] : 0; } size_t Length() { return a.Length(); } TPub *operator [](size_t idx) { LAssert(State == Store3Loaded); return a[idx]; } bool Delete(TPub *pub_ptr) { LAssert(State == Store3Loaded); TPriv *priv_ptr = dynamic_cast(pub_ptr); if (!priv_ptr) { LAssert(!"Not the right type of object."); return false; } ssize_t i = a.IndexOf(priv_ptr); if (i < 0) return false; a.DeleteAt(i, true); return true; } bool Insert(TPub *pub_ptr, ssize_t idx = -1, bool NoAssert = false) { if (!NoAssert) LAssert(State == Store3Loaded); TPriv *priv_ptr = dynamic_cast(pub_ptr); if (!priv_ptr) { LAssert(!"Not the right type of object."); return false; } return a.AddAt(idx < 0 ? a.Length() : idx, priv_ptr); } bool Empty() { LAssert(State == Store3Loaded); a.Length(0); return true; } bool DeleteObjects() { a.DeleteObjects(); return true; } ssize_t IndexOf(TPub *pub_ptr, bool NoAssert = false) { if (!NoAssert) LAssert(State == Store3Loaded); TPriv *priv_ptr = dynamic_cast(pub_ptr); if (!priv_ptr) { LAssert(!"Not the right type of object."); return -1; } return a.IndexOf(priv_ptr); } }; #endif diff --git a/include/lgi/common/StructuredLog.h b/include/lgi/common/StructuredLog.h --- a/include/lgi/common/StructuredLog.h +++ b/include/lgi/common/StructuredLog.h @@ -1,223 +1,223 @@ #pragma once #include #include "lgi/common/File.h" #ifndef _STRUCTURED_IO_H_ #include "lgi/common/StructuredIo.h" #endif #define IntIo(type) inline void StructIo(LStructuredIo &io, type i) { io.Int(i); } #define StrIo(type) inline void StructIo(LStructuredIo &io, type i) { io.String(i); } IntIo(char) IntIo(unsigned char) IntIo(short) IntIo(unsigned short) IntIo(int) IntIo(unsigned int) IntIo(int64_t) IntIo(uint64_t) StrIo(char*); StrIo(const char*); StrIo(wchar_t*); StrIo(const wchar_t*); inline void StructIo(LStructuredIo &io, LString &s) { if (io.GetWrite()) io.String(s.Get(), s.Length()); else io.Decode([&s](auto type, auto sz, auto ptr, auto name) { if (type == GV_STRING && ptr && sz > 0) s.Set((char*)ptr, sz); }); } inline void StructIo(LStructuredIo &io, LStringPipe &p) { // auto obj = io.StartObj("LStringPipe"); if (io.GetWrite()) { p.Iterate([&io](auto ptr, auto bytes) { io.String(ptr, bytes); return true; }); } else { io.Decode([&p](auto type, auto sz, auto ptr, auto name) { if (type == GV_STRING && ptr && sz > 0) p.Write(ptr, sz); }); } } inline void StructIo(LStructuredIo &io, LRect &r) { auto obj = io.StartObj("LRect"); io.Int(r.x1, "x1"); io.Int(r.y1, "y1"); io.Int(r.x2, "x2"); io.Int(r.y2, "y2"); } class LStructuredLog { LFile f; LStructuredIo io; template void Store(T &t) { StructIo(io, t); } public: LStructuredLog(const char *FileName, bool write = true) : io(write) { LString fn; if (LIsRelativePath(FileName)) { LFile::Path p(LSP_APP_INSTALL); p += FileName; fn = p.GetFull(); } else fn = FileName; if (f.Open(fn, write ? O_WRITE : O_READ) && write) f.SetSize(0); } virtual ~LStructuredLog() { } // Write objects to the log. Custom types will need to have a StructIo implementation template void Log(Args&&... args) { if (!io.GetWrite()) { LAssert(!"Not open for writing."); return; } int dummy[] = { 0, ( (void) Store(std::forward(args)), 0) ... }; io.Flush(&f); } // Read and decode to string objects. bool Read(std::function callback, Progress *prog = NULL) { if (io.GetWrite()) { LAssert(!"Not open for reading."); return false; } if (io.GetPos() >= io.Length()) { if (!io.Length(f.GetSize())) return false; if (prog) prog->SetRange(f.GetSize()); for (int64_t i=0; iValue(i); if (prog->IsCancelled()) return false; } } } if (prog && prog->IsCancelled()) return false; return io.Decode(callback, prog); } // Read and convert to a string. void Read(std::function callback) { LStringPipe p; while (Read([&p](auto type, auto sz, auto ptr, auto name) { LString prefix; if (type == LStructuredIo::EndRow) return; if (p.GetSize()) prefix = " "; if (name) { prefix += name; prefix += "="; } switch (type) { case GV_STRING: { p.Print("%s%.*s", prefix ? prefix.Get() : "", (int)sz, ptr); break; } case GV_INT64: { if (sz > 4) p.Print("%s" LPrintfInt64, prefix ? prefix.Get() : "", *((int64*)ptr)); else p.Print("%s%i", prefix ? prefix.Get() : "", *((int*)ptr)); break; } case LStructuredIo::StartObject: { p.Print("%s {", name); break; } case LStructuredIo::EndObject: { p.Print(" }"); break; } default: { LAssert(!"Impl me."); break; } } })); - callback(p.NewGStr()); + callback(p.NewLStr()); } static void UnitTest() { auto fn = "my-test-log.struct"; { LStructuredLog Log(fn); LString asd = LString("1234568,"); int ert = 123; LRect rc(10, 10, 200, 100); Log.Log("asd:", asd, rc, ert); } { LStructuredLog Rd(fn, false); Rd.Read([](LString s) { LgiTrace("%s\n", s.Get()); }); } } }; diff --git a/src/common/Gdc2/Font/Charset.cpp b/src/common/Gdc2/Font/Charset.cpp --- a/src/common/Gdc2/Font/Charset.cpp +++ b/src/common/Gdc2/Font/Charset.cpp @@ -1,1711 +1,1711 @@ #include #include #include #include "lgi/common/Lgi.h" #include "lgi/common/Font.h" #include "lgi/common/Charset.h" struct UnicodeMappings { int Unicode; char Ascii; } MissingMaps[] = { {0x2019, '\''}, {0x201C, '\"'}, {0x201D, '\"'}, {0, 0} }; typedef uint32_t iso2022jp_block[16]; iso2022jp_block *iso2022jp_map[128]; iso2022jp_block iso2022jp_blocks[] = { {0,0,0x10000000,0,0,0x53118c,0x800000,0x800000,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0xfffe0000,0xfffe03fb,0x3fb,0}, {0xffff0002,0xffffffff,0x2ffff,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0x33510000,0x80d0063,0,0,0,0,0,0,0x8,0x800,0,0,0xf0000,0,0x140000,0}, {0x6404098d,0x20301f81,0x40000,0xcc3,0xcc,0x20,0,0,0x40000,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0x3999900f,0x99999939,0x804,0,0,0x300c0003,0xc8c0,0x8000}, {0x60,0,0x5,0xa400,0,0,0,0,0,0,0,0,0,0,0,0}, {0x103fffef,0,0xfffffffe,0xffffffff,0x780fffff,0xfffffffe,0xffffffff,0x787fffff,0,0,0,0,0,0,0,0}, {0x43f36f8b,0x9b462442,0xe3e0e82c,0x400a0004,0xdb365f65,0x4497977,0xe3f0ecd7,0x8c56038,0x3403e602,0x35518000,0x7eabe0c8,0x98698200,0x2942a948,0x8060e803,0xad93441c,0x4568c03a}, {0x8656aa60,0x2403f7a,0x14618388,0x21741020,0x7022021,0x40bc3000,0x4462a624,0xa2060a8,0x85740217,0x9c840402,0x14157bfb,0x11e27f24,0x2efb665,0x20ff1f75,0x38403a70,0x676326c3}, {0x20924dd9,0xfc946b0,0x4850bc98,0xa03f8638,0x88162388,0x52323e09,0xe3a422aa,0xc72c00dd,0x26e1a166,0x8f0a840b,0x559e27eb,0x89bbc241,0x85400014,0x8496361,0x8ad07f0c,0x5cfff3e}, {0xa803ff1a,0x7b407a41,0x80024745,0x38eb0500,0x5d851,0x710c9934,0x1000397,0x24046366,0x5180d0,0x430ac000,0x30c89071,0x58000008,0xf7000e99,0x415f80,0x941000b0,0x62800018}, {0x9d00240,0x1568200,0x8015004,0x5101d10,0x1084c1,0x10504025,0x4d8a410f,0xa60d4009,0x914cab19,0x98121c0,0x3c485,0x80000652,0x80b04,0x9041d,0x905c4849,0x16900009}, {0x22200c65,0x24338412,0x47960c03,0x42250a04,0x90880028,0x4f084900,0xd3aa14a2,0x3e87d830,0x1f618604,0x41867ea4,0x5b3c390,0x211857a5,0x2a48241e,0x4a041128,0x161b0a40,0x88400d60}, {0x9502020a,0x10608221,0x4000243,0x80001444,0xc040000,0x70000000,0xc11a06,0xc00024a,0x401a00,0x40451404,0xbdb30029,0x52b0a78,0xbfa0bba9,0x8379407c,0xe81d12fc,0xc5694bf6}, {0x44aeff6,0xff022115,0x402bed63,0x242d033,0x131000,0x59ca1b02,0x20000a0,0x2c41a703,0x8ff24880,0x204,0x10055800,0x489200,0x20011894,0x34805004,0x684c3200,0x68be49ea}, {0x2e42184c,0x21c9a820,0x80b050b9,0xff7c001e,0x14e0849a,0x1e028c1,0xac49870e,0xdddb130f,0x89fbbe1a,0x51a2a2e0,0x32ca5502,0x928b3e46,0x438f1dbf,0x32186703,0x33c03028,0xa9230811}, {0x3a65c000,0x4028fe3,0x86252c4e,0xa1bf3d,0x8cd43a1a,0x317c06c9,0x950a00e0,0xedb018b,0x8c20e34b,0xf0101182,0xa7287d94,0x40fbc9ac,0x6534484,0x44445a90,0x13fc8,0xf5d40048}, {0xec577701,0x891dc442,0x49286b83,0xd2424109,0x59fe061d,0x3a221800,0x3b9fb7e4,0xc0eaf003,0x82021386,0xe4008980,0x10a1b200,0xcc44b80,0x8944d309,0x48341faf,0xc458259,0x450420a}, {0x10c8a040,0x44503140,0x1004004,0x5408280,0x442c0108,0x1a056a30,0x51420a6,0x645690cf,0x31000021,0xcbf09c18,0x63e2a120,0x1b5104c,0x9a83538c,0x3281b8b2,0xa84987a,0xc0233e7}, {0x9018d4cc,0x9070a1a1,0xe0048a1e,0x451c3d4,0x21c2439a,0x53104844,0x36400292,0xf3bd0241,0xe8f0ab09,0xa5d27dc0,0xd24bc242,0xd0afa43f,0x34a11aa0,0x3d88247,0x651bc452,0xc83ad294}, {0x40c8001c,0x33140e06,0xb21b614f,0xc0d00088,0xa898a02a,0x166ba1c5,0x85b42e50,0x604c08b,0x1e04f933,0xa251056e,0x76380400,0x73b8ec07,0x18324406,0xc8164081,0x63097c8a,0xaa042980}, {0xca9c1c24,0x27604e0e,0x83000990,0x81040046,0x10816011,0x908540d,0xcc0a000e,0xc000500,0xa0440430,0x6784008b,0x8a195288,0x8b18865e,0x41602e59,0x9cbe8c10,0x891c6861,0x89800}, {0x89a8100,0x41900018,0xe4a14007,0x640d0505,0xe4d310e,0xff0a4806,0x2aa81632,0xb852e,0xca841800,0x696c0e20,0x16000032,0x3905658,0x1a285120,0x11248000,0x432618e1,0xeaa5d52}, {0xae280fa0,0x4500fa7b,0x89406408,0xc044c880,0xb1419005,0x24c48424,0x603a1a34,0xc1949000,0x3a8246,0xc106180d,0x99100022,0x1511e050,0x824057,0x20a041a,0x8930004f,0x444ad813}, {0xed228a02,0x400510c0,0x1021000,0x31018808,0x2044600,0x708f000,0xa2008900,0x22020000,0x16100200,0x10400042,0x2605200,0x200052f4,0x82308510,0x42021100,0x80b54308,0x9a2070e1}, {0x8012040,0xfc653500,0xab0419c1,0x62140286,0x440087,0x2449085,0xa85405c,0x33803207,0xb8c00400,0xc0d0ce20,0x80c030,0xd250508,0x400a90,0x80c0200,0x40006505,0x41026421}, {0x268,0x847c0024,0xde200002,0x40498619,0x40000808,0x20010084,0x10108400,0x1c742cd,0xd52a7038,0x1d8f1968,0x3e12be50,0x81d92ef5,0x2412cec4,0x732e0828,0x4b3424ac,0xd41d020c}, {0x80002a02,0x8110097,0x114411c4,0x7d451786,0x64949d9,0x87914000,0xd8c4254c,0x491444ba,0xc8001b92,0x15800271,0xc000081,0xc200096a,0x40024800,0xba493021,0x1c802080,0x1008e2ac}, {0x341004,0x841400e1,0x20000020,0x10149800,0x4aa70c2,0x54208688,0x4130c62,0x20109180,0x2064082,0x54001c40,0xe4e90383,0x84802125,0x2000e433,0xe60944c0,0x81260a03,0x80112da}, {0x97906901,0xf8864001,0x81e24d,0xa6510a0e,0x81ec011a,0x8441c600,0xb62cadb8,0x8741a46f,0x4b028d54,0x2681161,0x2057bb60,0x43350a0,0xb7b4a8c0,0x1122402,0x20009ad3,0xc82271}, {0x809e2081,0xe1800c8a,0x8151b009,0x40281031,0x89a52a0e,0x620e69b6,0xd1444425,0x4d548085,0x1fb12c75,0x862dd807,0x4841d87c,0x226e414e,0x9e088200,0xed37f80c,0x75268c80,0x8149313}, {0xc8040e32,0x6ea6484e,0x66702c4a,0xba0126c0,0x185dd30c,0,0,0,0,0x5400000,0x81337020,0x3a54f81,0x641055ec,0x2344c318,0x341462,0x1a090a43}, {0x13a5187b,0xa8480102,0xc5440440,0xe2dd8106,0x2d481af0,0x416b626,0x6e405058,0x31128032,0xc0007e4,0x420a8208,0x803b4840,0x87134860,0x3428850d,0xe5290319,0x870a2345,0x5c1825a9}, {0xd9c577a6,0x3e85e00,0xa7000081,0x41c6cd54,0xa2042800,0x2b0ab860,0xda9e0020,0xe1a08ea,0x11c0427c,0x3768908,0x1058621,0x18a80000,0xc44846a0,0x20220d05,0x91485422,0x28978a01}, {0x87898,0x31221605,0x8804240,0x6a2fa4e,0x92110814,0x9b042002,0x6432e52,0x90105000,0x85ba0041,0x20203042,0x5a04f0b,0x40802708,0x1a930591,0x600df50,0x3021a202,0x4e800630}, {0x4c80cc4,0x8001a004,0xd4316000,0xa020880,0x281c00,0x418e18,0xca106ad0,0x4b00f210,0x1506274d,0x88900220,0x82a85a00,0x81504549,0x80002004,0x2c088804,0x508d1,0x4ac48001}, {0x62e020,0xa42008e,0x6a8c3055,0xe0a5090e,0x42c42906,0x80b34814,0xb330803e,0x731c0102,0x600d1494,0x9400c20,0xc040301a,0xc094a451,0x5c88dca,0xa40c96c2,0x34040001,0x11000c8}, {0xa9c9550d,0x1c5a2428,0x48370142,0x100f7a4d,0x452a32b4,0x9205317b,0x5c44b894,0x458a68d7,0x2ed15097,0x42081943,0x9d40d202,0x20979840,0x64d5409,0,0,0}, {0,0x84800000,0x4215542,0x17001c06,0x61107624,0xb9ddff87,0x5c0a659f,0x3c00245d,0x59adb0,0,0,0x9b28d0,0x2000422,0x44080108,0xac409804,0x90288d0a}, {0xe0018700,0x310400,0x82211794,0x10540019,0x21a2cb2,0x40039c02,0x88043d60,0x7900080c,0xba3c1628,0xcb088640,0x90807274,0x1e,0xd8000000,0x9c87e188,0x4124034,0x2791ae64}, {0xe6fbe86b,0x5366408f,0x537feea6,0xb5e4e32b,0x2869f,0x1228548,0x8004402,0x20a02116,0x2040004,0x52000,0x1547e00,0x1ac162c,0x10852a84,0x5308c14,0xb943fbc3,0x906000ca}, {0x40326000,0x80901200,0x4c810b30,0x40020054,0x1d6a0029,0x2802000,0x48000,0x150c2610,0x7018040,0xc24d94d,0x18502810,0x50205001,0x4d01000,0x2017080,0x21c30108,0x132}, {0x7190088,0x5600802,0x4c0e0012,0xf0a10405,0x2,0,0,0,0,0,0,0x800000,0x35a8e8d,0x5a0421bd,0x11703488,0x26}, {0x10000000,0x8804c502,0xf801b815,0x25ed147c,0x1bb0ed60,0x1bd70589,0x1a627af3,0xac50d0c,0x524ae5d1,0x63050490,0x52440354,0x16122b57,0x1101a872,0x182949,0x10080948,0x886c6000}, {0x58f916e,0x39903012,0x4930f840,0x1b8880,0,0x428500,0x98000058,0x7014ea04,0x611d1628,0x60005113,0xa71a24,0,0x3c00000,0x10187120,0xa9270172,0x89066004}, {0x20cc022,0x40810900,0x8ca0202d,0xe34,0,0x11012100,0xc11a8011,0x892ec4c,0x85000040,0x1806c7ac,0x512e03e,0x108000,0x80ce4008,0x2106d01,0x8568641,0x27011e}, {0x83d3750,0x4e05e032,0x48401c0,0x1400081,0,0,0,0x591aa0,0x882443c8,0xc8001d48,0x72030152,0x4049013,0x4008280,0xd148a10,0x2088056,0x2704a040}, {0x4c000000,0,0,0xa3200000,0xa0ae1902,0xdf002660,0x7b15f010,0x3ad08121,0x284180,0x48001003,0x8014cc00,0xc414cf,0x30202000,0x1,0,0}, {0,0,0,0,0,0,0,0,0xffffdf7a,0xefffffff,0x3fffffff,0,0,0,0,0x2} }; class LgiIso2022Jp { public: LgiIso2022Jp() { int n, o = 0, i = 0; for (n=0; n<3; n++) iso2022jp_map[o++] = &iso2022jp_blocks[i++]; o += 13; for (n=0; n<4; n++) iso2022jp_map[o++] = &iso2022jp_blocks[i++];; o += 4; iso2022jp_map[o++] = &iso2022jp_blocks[i++];; o += 14; for (n=0; n<41; n++) iso2022jp_map[o++] = &iso2022jp_blocks[i++];; o += 47; iso2022jp_map[o++] = &iso2022jp_blocks[i++];; LAssert(o == 128); } bool CanEncode(char16 *s, ssize_t l) { if (s) { if (l < 0) l = StrlenW(s); for (int i=0; i> 9]; if (!*block) { return false; } u &= 0x1ff; if ( ( *block[u >> 5] & (1 << (u & 0x1f)) ) == 0 ) { return false; } } return true; } return false; } } Iso2022Jp; ///////////////////////////////////////////////////////////////////////////////////// bool LIsUtf8(const char *s, ssize_t len) { #define LenCheck(Need) \ if (len >= 0 && (len - (s - Start)) < Need) \ goto Utf8Error; #define TrailCheck() \ if (!IsUtf8_Trail(*s)) \ goto Utf8Error; \ s++; if (!s || *s == 0) return true; const char *Start = s; while ( ( len < 0 || ((s - Start) < len) ) && *s ) { if (IsUtf8_1Byte(*s)) { s++; } else if (IsUtf8_2Byte(*s)) { s++; LenCheck(1); TrailCheck(); } else if (IsUtf8_3Byte(*s)) { s++; LenCheck(2); TrailCheck(); TrailCheck(); } else if (IsUtf8_4Byte(*s)) { s++; LenCheck(3); TrailCheck(); TrailCheck(); TrailCheck(); } else goto Utf8Error; } return true; Utf8Error: #if 1 LgiTrace("%s:%i - Invalid utf @ offset=%i, bytes=", _FL, (int) (s - Start)); auto end = len < 0 ? NULL : Start + len; for (auto i = 0; i < 16; i++) { if ( (end && s >= end) || *s == 0 ) break; LgiTrace("%02.2x,", (uint8_t)*s++); } LgiTrace("\n"); #endif return false; } ///////////////////////////////////////////////////////////////////////////////////// short _gdc_usascii_mapping[128] = { // 0x80 - 0x8f 0xc7, 0xfc, 0xe9, 0xe2, 0xe4, 0xe0, 0xe5, 0xe7, 0xea, 0xeb, 0xe8, 0xef, 0xee, 0xec, 0xc4, 0xc5, // 0x90 - 0x9f 0xc9, 0xe6, 0xc6, 0xf4, 0xf6, 0xf2, 0xfb, 0xf9, 0xff, 0xd6, 0xdc, 0xa2, 0xa3, 0xa5, 0x20a7, 0x192, // 0xa0 - 0xaf 0xe1, 0xed, 0xf3, 0xfa, 0xf1, 0xd1, 0xb2, 0xb0, 0xbf, 0x2310, 0xac, 0xbd, 0xbc, 0xa1, 0xab, 0xbb, // 0xb0 - 0xbf 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255d, 0x255c, 0x255b, 0x2510, // 0xc0 - 0xcf 0x2514, 0x2534, 0x252c, 0x251c, 0x2500, 0x253c, 0x255e, 0x255f, 0x255a, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256c, 0x2567, // 0xd0 - 0xdf 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256b, 0x256a, 0x2518, 0x250c, 0x2588, 0x2584, 0x258c, 0x2590, 0x2580, // 0xe0 - 0xef 0x3b1, 0x3b2, 0x393, 0x3a0, 0x3a3, 0x3c3, 0x3bc, 0x3c4, 0x3a6, 0x398, 0x3a9, 0x3b4, 0x221e, 0xd8, 0x3b6, 0x2229, // 0xf0 - 0xff 0x2261, 0xb1, 0x2265, 0x2264, 0x2320, 0x2321, 0xf7, 0x2248, 0xb0, 0x2022, 0x2219, 0x221a, 0x207f, 178, 0x25a0, 0x25a1 }; // This mapping just NUL's out the characters between 0x80 and 0x9f which aren't defined // in the ISO spec. The rest of the characters map to themselves. short _gdc_ISO_8859_identity_mapping[128] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf, 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff }; short _gdc_ISO_8859_2_mapping[128] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xa0, 0x104, 0x2d8, 0x141, 0xa4, 0x13d, 0x15a, 0xa7, 0xa8, 0x160, 0x15e, 0x164, 0x179, 0xad, 0x17d, 0x17b, 0xb0, 0x105, 0x2db, 0x142, 0xb4, 0x13e, 0x15b, 0x2c7, 0xb8, 0x161, 0x15f, 0x165, 0x17a, 0x2dd, 0x17e, 0x17c, 0x154, 0xc1, 0xc2, 0x102, 0xc4, 0x139, 0x106, 0xc7, 0x10c, 0xc9, 0x118, 0xcb, 0x11a, 0xcd, 0xce, 0x10e, 0x110, 0x143, 0x147, 0xd3, 0xd4, 0x150, 0xd6, 0xd7, 0x158, 0x16e, 0xda, 0x170, 0xdc, 0xdd, 0x162, 0xdf, 0x155, 0xe1, 0xe2, 0x103, 0xe4, 0x13a, 0x107, 0xe7, 0x10d, 0xe9, 0x119, 0xeb, 0x11b, 0xed, 0xee, 0x10f, 0x111, 0x144, 0x148, 0xf3, 0xf4, 0x151, 0xf6, 0xf7, 0x159, 0x16f, 0xfa, 0x171, 0xfc, 0xfd, 0x163, 0x2d9 }; short _gdc_ISO_8859_3_mapping[128] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xa0, 0x126, 0x2d8, 0xa3, 0xa4, 0, 0x124, 0xa7, 0xa8, 0x130, 0x15e, 0x11e, 0x134, 0xad, 0, 0x17b, 0xb0, 0x127, 0xb2, 0xb3, 0xb4, 0xb5, 0x125, 0xb7, 0xb8, 0x131, 0x15f, 0x11f, 0x135, 0xbd, 0, 0x17c, 0xc0, 0xc1, 0xc2, 0, 0xc4, 0x10a, 0x108, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf, 0, 0xd1, 0xd2, 0xd3, 0xd4, 0x120, 0xd6, 0xd7, 0x11c, 0xd9, 0xda, 0xdb, 0xdc, 0x16c, 0x15c, 0xdf, 0xe0, 0xe1, 0xe2, 0, 0xe4, 0x10b, 0x109, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 0, 0xf1, 0xf2, 0xf3, 0xf4, 0x121, 0xf6, 0xf7, 0x11d, 0xf9, 0xfa, 0xfb, 0xfc, 0x16d, 0x15d, 0x2d9 }; short _gdc_ISO_8859_4_mapping[128] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x0A0, 0x104, 0x138, 0x156, 0x0A4, 0x128, 0x13B, 0x0A7, 0x0A8, 0x160, 0x112, 0x122, 0x166, 0x0AD, 0x17D, 0x0AF, 0x0B0, 0x105, 0x2DB, 0x157, 0x0B4, 0x129, 0x13C, 0x2C7, 0x0B8, 0x161, 0x113, 0x123, 0x167, 0x14A, 0x17E, 0x14B, 0x100, 0x0C1, 0x0C2, 0x0C3, 0x0C4, 0x0C5, 0x0C6, 0x12E, 0x10C, 0x0C9, 0x118, 0x0CB, 0x116, 0x0CD, 0x0CE, 0x12A, 0x110, 0x145, 0x14C, 0x136, 0x0D4, 0x0D5, 0x0D6, 0x0D7, 0x0D8, 0x172, 0x0DA, 0x0DB, 0x0DC, 0x168, 0x16A, 0x0DF, 0x101, 0x0E1, 0x0E2, 0x0E3, 0x0E4, 0x0E5, 0x0E6, 0x12F, 0x10D, 0x0E9, 0x119, 0x0EB, 0x117, 0x0ED, 0x0EE, 0x12B, 0x111, 0x146, 0x14D, 0x137, 0x0F4, 0x0F5, 0x0F6, 0x0F7, 0x0F8, 0x173, 0x0FA, 0x0FB, 0x0FC, 0x169, 0x16B, 0x2D9 }; short _gdc_ISO_8859_5_mapping[128] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x0A0, 0x401, 0x402, 0x403, 0x404, 0x405, 0x406, 0x407, 0x408, 0x409, 0x40A, 0x40B, 0x40C, 0x0AD, 0x40E, 0x40F, 0x410, 0x411, 0x412, 0x413, 0x414, 0x415, 0x416, 0x417, 0x418, 0x419, 0x41A, 0x41B, 0x41C, 0x41D, 0x41E, 0x41F, 0x420, 0x421, 0x422, 0x423, 0x424, 0x425, 0x426, 0x427, 0x428, 0x429, 0x42A, 0x42B, 0x42C, 0x42D, 0x42E, 0x42F, 0x430, 0x431, 0x432, 0x433, 0x434, 0x435, 0x436, 0x437, 0x438, 0x439, 0x43A, 0x43B, 0x43C, 0x43D, 0x43E, 0x43F, 0x440, 0x441, 0x442, 0x443, 0x444, 0x445, 0x446, 0x447, 0x448, 0x449, 0x44A, 0x44B, 0x44C, 0x44D, 0x44E, 0x44F, 0x2116, 0x451, 0x452, 0x453, 0x454, 0x455, 0x456, 0x457, 0x458, 0x459, 0x45A, 0x45B, 0x45C, 0x0A7, 0x45E, 0x45F }; short _gdc_ISO_8859_6_mapping[128] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xA0, 0, 0, 0, 0xA4, 0, 0, 0, 0, 0, 0, 0, 0x60C, 0xAD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x61B, 0, 0, 0, 0x61F, 0, 0x621, 0x622, 0x623, 0x624, 0x625, 0x626, 0x627, 0x628, 0x629, 0x62A, 0x62B, 0x62C, 0x62D, 0x62E, 0x62F, 0x630, 0x631, 0x632, 0x633, 0x634, 0x635, 0x636, 0x637, 0x638, 0x639, 0x63A, 0, 0, 0, 0, 0, 0x640, 0x641, 0x642, 0x643, 0x644, 0x645, 0x646, 0x647, 0x648, 0x649, 0x64A, 0x64B, 0x64C, 0x64D, 0x64E, 0x64F, 0x650, 0x651, 0x652, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; short _gdc_ISO_8859_7_mapping[128] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x0A0, 0x2BD, 0x2BC, 0x0A3, 0, 0, 0x0A6, 0x0A7, 0x0A8, 0x0A9, 0, 0x0AB, 0x0AC, 0x0AD, 0, 0x015, 0x0B0, 0x0B1, 0x0B2, 0x0B3, 0x384, 0x385, 0x386, 0x0B7, 0x388, 0x389, 0x38A, 0x0BB, 0x38C, 0x0BD, 0x38E, 0x38F, 0x390, 0x391, 0x392, 0x393, 0x394, 0x395, 0x396, 0x397, 0x398, 0x399, 0x39A, 0x39B, 0x39C, 0x39D, 0x39E, 0x39F, 0x3A0, 0x3A1, 0, 0x3A3, 0x3A4, 0x3A5, 0x3A6, 0x3A7, 0x3A8, 0x3A9, 0x3AA, 0x3AB, 0x3AC, 0x3AD, 0x3AE, 0x3AF, 0x3B0, 0x3B1, 0x3B2, 0x3B3, 0x3B4, 0x3B5, 0x3B6, 0x3B7, 0x3B8, 0x3B9, 0x3BA, 0x3BB, 0x3BC, 0x3BD, 0x3BE, 0x3BF, 0x3C0, 0x3C1, 0x3C2, 0x3C3, 0x3C4, 0x3C5, 0x3C6, 0x3C7, 0x3C8, 0x3C9, 0x3CA, 0x3CB, 0x3CC, 0x3CD, 0x3CE, 0 }; short _gdc_ISO_8859_8_mapping[128] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x0A0, 0, 0x0A2, 0x0A3, 0x0A4, 0x0A5, 0x0A6, 0x0A7, 0x0A8, 0x0A9, 0x0D7, 0x0AB, 0x0AC, 0x0AD, 0x0AE, 0x203E, 0x0B0, 0x0B1, 0x0B2, 0x0B3, 0x0B4, 0x0B5, 0x0B6, 0x0B7, 0x0B8, 0x0B9, 0x0F7, 0x0BB, 0x0BC, 0x0BD, 0x0BE, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x2017, 0x5D0, 0x5D1, 0x5D2, 0x5D3, 0x5D4, 0x5D5, 0x5D6, 0x5D7, 0x5D8, 0x5D9, 0x5DA, 0x5DB, 0x5DC, 0x5DD, 0x5DE, 0x5DF, 0x5E0, 0x5E1, 0x5E2, 0x5E3, 0x5E4, 0x5E5, 0x5E6, 0x5E7, 0x5E8, 0x5E9, 0x5EA, 0, 0, 0, 0, 0 }; short _gdc_ISO_8859_9_mapping[128] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, 0x11E, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0x130, 0x15E, 0xDF, 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, 0x11F, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0x131, 0x15F, 0xFF }; short _gdc_ISO_8859_13_mapping[128] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xA0, 0x201D, 0xA2, 0xA3, 0xA4, 0x201E, 0xA6, 0xA7, 0xD8, 0xA9, 0x156, 0xAB, 0xAC, 0xAD, 0xAE, 0xC6, 0xB0, 0xB1, 0xB2, 0xB3, 0x201C, 0xB5, 0xB6, 0xB7, 0xF8, 0xB9, 0x157, 0xBB, 0xBC, 0xBD, 0xBE, 0xE6, 0x104, 0x12E, 0x100, 0x106, 0xC4, 0xC5, 0x118, 0x112, 0x10C, 0xC9, 0x179, 0x116, 0x122, 0x136, 0x12A, 0x13B, 0x160, 0x143, 0x145, 0xD3, 0x14C, 0xD5, 0xD6, 0xD7, 0x172, 0x141, 0x15A, 0x16A, 0xDC, 0x17B, 0x17D, 0xDF, 0x105, 0x12F, 0x101, 0x107, 0xE4, 0xE5, 0x119, 0x113, 0x10D, 0xE9, 0x17A, 0x117, 0x123, 0x137, 0x12B, 0x13C, 0x161, 0x144, 0x146, 0xF3, 0x14D, 0xF5, 0xF6, 0xF7, 0x173, 0x142, 0x15B, 0x16B, 0xFC, 0x17C, 0x17E, 0x2019 }; short _gdc_ISO_8859_15_mapping[128] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xA0, 0xA1, 0xA2, 0xA3, 0x20AC, 0xA5, 0x160, 0xA7, 0x161, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB0, 0xB1, 0xB2, 0xB3, 0x17D, 0xB5, 0xB6, 0xB7, 0x17E, 0xB9, 0xBA, 0xBB, 0x152, 0x153, 0x178, 0xBF, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF }; short _gdc_win_874_mapping[128] = { 0x20AC, 0, 0, 0, 0, 0x2026, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0, 0, 0, 0, 0, 0, 0, 0, 0xA0, 0xE01, 0xE02, 0xE03, 0xE04, 0xE05, 0xE06, 0xE07, 0xE08, 0xE09, 0xE0A, 0xE0B, 0xE0C, 0xE0D, 0xE0E, 0xE0F, 0xE10, 0xE11, 0xE12, 0xE13, 0xE14, 0xE15, 0xE16, 0xE17, 0xE18, 0xE19, 0xE1A, 0xE1B, 0xE1C, 0xE1D, 0xE1E, 0xE1F, 0xE20, 0xE21, 0xE22, 0xE23, 0xE24, 0xE25, 0xE26, 0xE27, 0xE28, 0xE29, 0xE2A, 0xE2B, 0xE2C, 0xE2D, 0xE2E, 0xE2F, 0xE30, 0xE31, 0xE32, 0xE33, 0xE34, 0xE35, 0xE36, 0xE37, 0xE38, 0xE39, 0xE3A, 0, 0, 0, 0, 0xE3F, 0xE40, 0xE41, 0xE42, 0xE43, 0xE44, 0xE45, 0xE46, 0xE47, 0xE48, 0xE49, 0xE4A, 0xE4B, 0xE4C, 0xE4D, 0xE4E, 0xE4F, 0xE50, 0xE51, 0xE52, 0xE53, 0xE54, 0xE55, 0xE56, 0xE57, 0xE58, 0xE59, 0xE5A, 0xE5B, 0, 0, 0, 0, }; short _gdc_win_1250_mapping[128] = { 0x20AC, 0, 0x201A, 0, 0x201E, 0x2026, 0x2020, 0x2021, 0, 0x2030, 0x0160, 0x2039, 0x015A, 0x0164, 0x017D, 0x0179, 0, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0, 0x2122, 0x0161, 0x203A, 0x015B, 0x0165, 0x017E, 0x017A, 0xA0, 0x02C7, 0x02D8, 0x0141, 0xA4, 0x0104, 0xA6, 0xA7, 0xA8, 0xA9, 0x015E, 0xAB, 0xAC, 0xAD, 0xAE, 0x017B, 0xB0, 0xB1, 0x02DB, 0x0142, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0x0105, 0x015F, 0xBB, 0x013D, 0x02DD, 0x013E, 0x017C, 0x0154, 0xC1, 0xC2, 0x0102, 0xC4, 0x0139, 0x0106, 0xC7, 0x010C, 0xC9, 0x0118, 0xCB, 0x011A, 0xCD, 0xCE, 0x010E, 0x0110, 0x0143, 0x0147, 0xD3, 0xD4, 0x0150, 0xD6, 0xD7, 0x0158, 0x016E, 0xDA, 0x0170, 0xDC, 0xDD, 0x0162, 0xDF, 0x0155, 0xE1, 0xE2, 0x0103, 0xE4, 0x013A, 0x0107, 0xE7, 0x010D, 0xE9, 0x0119, 0xEB, 0x011B, 0xED, 0xEE, 0x010F, 0x0111, 0x0144, 0x0148, 0xF3, 0xF4, 0x0151, 0xF6, 0xF7, 0x0159, 0x016F, 0xFA, 0x0171, 0xFC, 0xFD, 0x0163, 0x02D9 }; short _gdc_win_1251_mapping[128] = { 0x402, 0x403, 0x201A, 0x453, 0x201E, 0x2026, 0x2020, 0x2021, 0x20AC, 0x2030, 0x409, 0x2039, 0x40A, 0x40C, 0x40B, 0x40F, 0x452, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0, 0x2122, 0x459, 0x203A, 0x45A, 0x45C, 0x45B, 0x45F, 0x0A0, 0x40E, 0x45E, 0x408, 0x0A4, 0x490, 0x0A6, 0x0A7, 0x401, 0x0A9, 0x404, 0x0AB, 0x0AC, 0x0AD, 0x0AE, 0x407, 0x0B0, 0x0B1, 0x406, 0x456, 0x491, 0x0B5, 0x0B6, 0x0B7, 0x451, 0x2116, 0x454, 0x0BB, 0x458, 0x405, 0x455, 0x457, 0x410, 0x411, 0x412, 0x413, 0x414, 0x415, 0x416, 0x417, 0x418, 0x419, 0x41A, 0x41B, 0x41C, 0x41D, 0x41E, 0x41F, 0x420, 0x421, 0x422, 0x423, 0x424, 0x425, 0x426, 0x427, 0x428, 0x429, 0x42A, 0x42B, 0x42C, 0x42D, 0x42E, 0x42F, 0x430, 0x431, 0x432, 0x433, 0x434, 0x435, 0x436, 0x437, 0x438, 0x439, 0x43A, 0x43B, 0x43C, 0x43D, 0x43E, 0x43F, 0x440, 0x441, 0x442, 0x443, 0x444, 0x445, 0x446, 0x447, 0x448, 0x449, 0x44A, 0x44B, 0x44C, 0x44D, 0x44E, 0x44F }; short _gdc_win_1252_mapping[128] = { 0x20AC, 0, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0, 0x017D, 0, 0, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0, 0x017E, 0x0178, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF }; short _gdc_win_1253_mapping[128] = { 0x20AC, 0, 0x201A, 0x192, 0x201E, 0x2026, 0x2020, 0x2021, 0, 0x2030, 0, 0x2039, 0, 0, 0, 0, 0, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0, 0x2122, 0, 0x203A, 0, 0, 0, 0, 0xA0, 0x385, 0x386, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0, 0xAB, 0xAC, 0xAD, 0xAE, 0x2015, 0xB0, 0xB1, 0xB2, 0xB3, 0x384, 0xB5, 0xB6, 0xB7, 0x388, 0x389, 0x38A, 0xBB, 0x38C, 0xBD, 0x38E, 0x38F, 0x390, 0x391, 0x392, 0x393, 0x394, 0x395, 0x396, 0x397, 0x398, 0x399, 0x39A, 0x39B, 0x39C, 0x39D, 0x39E, 0x39F, 0x3A0, 0x3A1, 0, 0x3A3, 0x3A4, 0x3A5, 0x3A6, 0x3A7, 0x3A8, 0x3A9, 0x3AA, 0x3AB, 0x3AC, 0x3AD, 0x3AE, 0x3AF, 0x3B0, 0x3B1, 0x3B2, 0x3B3, 0x3B4, 0x3B5, 0x3B6, 0x3B7, 0x3B8, 0x3B9, 0x3BA, 0x3BB, 0x3BC, 0x3BD, 0x3BE, 0x3BF, 0x3C0, 0x3C1, 0x3C2, 0x3C3, 0x3C4, 0x3C5, 0x3C6, 0x3C7, 0x3C8, 0x3C9, 0x3CA, 0x3CB, 0x3CC, 0x3CD, 0x3CE, 0 }; short _gdc_win_1254_mapping[128] = { 0x20AC, 0, 0x201A, 0x192, 0x201E, 0x2026, 0x2020, 0x2021, 0x2C6, 0x2030, 0x160, 0x2039, 0x152, 0, 0, 0, 0, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x2DC, 0x2122, 0x161, 0x203A, 0x153, 0, 0, 0x178, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, 0x11E, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0x130, 0x15E, 0xDF, 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, 0x11F, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0x131, 0x15F, 0xFF, }; short _gdc_win_1255_mapping[128] = { 0x20AC, 0, 0x201A, 0x192, 0x201E, 0x2026, 0x2020, 0x2021, 0x2C6, 0x2030, 0, 0x2039, 0, 0, 0, 0, 0, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x2DC, 0x2122, 0, 0x203A, 0, 0, 0, 0, 0xA0, 0xA1, 0xA2, 0xA3, 0x20AA, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xD7, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xF7, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, 0x5B0, 0x5B1, 0x5B2, 0x5B3, 0x5B4, 0x5B5, 0x5B6, 0x5B7, 0x5B8, 0x5B9, 0, 0x5BB, 0x5BC, 0x5BD, 0x5BE, 0x5BF, 0x5C0, 0x5C1, 0x5C2, 0x5C3, 0x5F0, 0x5F1, 0x5F2, 0x5F3, 0x5F4, 0, 0, 0, 0, 0, 0, 0, 0x5D0, 0x5D1, 0x5D2, 0x5D3, 0x5D4, 0x5D5, 0x5D6, 0x5D7, 0x5D8, 0x5D9, 0x5DA, 0x5DB, 0x5DC, 0x5DD, 0x5DE, 0x5DF, 0x5E0, 0x5E1, 0x5E2, 0x5E3, 0x5E4, 0x5E5, 0x5E6, 0x5E7, 0x5E8, 0x5E9, 0x5EA, 0, 0, 0x200E, 0x200F, 0, }; short _gdc_win_1256_mapping[128] = { 0x20AC, 0x67E, 0x201A, 0x192, 0x201E, 0x2026, 0x2020, 0x2021, 0x2C6, 0x2030, 0x679, 0x2039, 0x152, 0x686, 0x698, 0x688, 0x6AF, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x6A9, 0x2122, 0x691, 0x203A, 0x153, 0x200C, 0x200D, 0x6BA, 0xA0, 0x60C, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0x6BE, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0x61B, 0xBB, 0xBC, 0xBD, 0xBE, 0x61F, 0x6C1, 0x621, 0x622, 0x623, 0x624, 0x625, 0x626, 0x627, 0x628, 0x629, 0x62A, 0x62B, 0x62C, 0x62D, 0x62E, 0x62F, 0x630, 0x631, 0x632, 0x633, 0x634, 0x635, 0x636, 0xD7, 0x637, 0x638, 0x639, 0x63A, 0x640, 0x641, 0x642, 0x643, 0xE0, 0x644, 0xE2, 0x645, 0x646, 0x647, 0x648, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0x649, 0x64A, 0xEE, 0xEF, 0x64B, 0x64C, 0x64D, 0x64E, 0xF4, 0x64F, 0x650, 0xF7, 0x651, 0xF9, 0x652, 0xFB, 0xFC, 0x200E, 0x200F, 0x6D2, }; short _gdc_win_1257_mapping[128] = { 0x20AC, 0, 0x201A, 0, 0x201E, 0x2026, 0x2020, 0x2021, 0, 0x2030, 0, 0x2039, 0, 0xA8, 0x2C7, 0xB8, 0, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0, 0x2122, 0, 0x203A, 0, 0xAF, 0x2DB, 0, 0xA0, 0, 0xA2, 0xA3, 0xA4, 0, 0xA6, 0xA7, 0xD8, 0xA9, 0x156, 0xAB, 0xAC, 0xAD, 0xAE, 0xC6, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xF8, 0xB9, 0x157, 0xBB, 0xBC, 0xBD, 0xBE, 0xE6, 0x104, 0x12E, 0x100, 0x106, 0xC4, 0xC5, 0x118, 0x112, 0x10C, 0xC9, 0x179, 0x116, 0x122, 0x136, 0x12A, 0x13B, 0x160, 0x143, 0x145, 0xD3, 0x14C, 0xD5, 0xD6, 0xD7, 0x172, 0x141, 0x15A, 0x16A, 0xDC, 0x17B, 0x17D, 0xDF, 0x105, 0x12F, 0x101, 0x107, 0xE4, 0xE5, 0x119, 0x113, 0x10D, 0xE9, 0x17A, 0x117, 0x123, 0x137, 0x12B, 0x13C, 0x161, 0x144, 0x146, 0xF3, 0x14D, 0xF5, 0xF6, 0xF7, 0x173, 0x142, 0x15B, 0x16B, 0xFC, 0x17C, 0x17E, 0x2D9, }; short _gdc_win_1258_mapping[128] = { 0x20AC, 0, 0x201A, 0x192, 0x201E, 0x2026, 0x2020, 0x2021, 0x2C6, 0x2030, 0, 0x2039, 0x152, 0, 0, 0, 0, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x2DC, 0x2122, 0, 0x203A, 0x153, 0, 0, 0x178, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, 0xC0, 0xC1, 0xC2, 0x102, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0x300, 0xCD, 0xCE, 0xCF, 0x110, 0xD1, 0x309, 0xD3, 0xD4, 0x1A0, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0x1AF, 0x303, 0xDF, 0xE0, 0xE1, 0xE2, 0x103, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0x301, 0xED, 0xEE, 0xEF, 0x111, 0xF1, 0x323, 0xF3, 0xF4, 0x1A1, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0x1B0, 0x20AB, 0xFF, }; short _gdc_koi8r_mapping[128] = { 0x2500, 0x2502, 0x250C, 0x2510, 0x2514, 0x2518, 0x251C, 0x2524, 0x252C, 0x2534, 0x253C, 0x2580, 0x2584, 0x2588, 0x258C, 0x2590, 0x2591, 0x2592, 0x2593, 0x2320, 0x25A0, 0x2219, 0x221A, 0x2248, 0x2264, 0x2265, 0xA0, 0x2321, 0xB0, 0xB2, 0xB7, 0xF7, 0x2550, 0x2551, 0x2552, 0x451, 0x2553, 0x2554, 0x2555, 0x2556, 0x2557, 0x2558, 0x2559, 0x255A, 0x255B, 0x255C, 0x255D, 0x255E, 0x255F, 0x2560, 0x2561, 0x401, 0x2562, 0x2563, 0x2564, 0x2565, 0x2566, 0x2567, 0x2568, 0x2569, 0x256A, 0x256B, 0x256C, 0xA9, 0x44E, 0x430, 0x431, 0x446, 0x434, 0x435, 0x444, 0x433, 0x445, 0x438, 0x439, 0x43A, 0x43B, 0x43C, 0x43D, 0x43E, 0x43F, 0x44F, 0x440, 0x441, 0x442, 0x443, 0x436, 0x432, 0x44C, 0x44B, 0x437, 0x448, 0x44D, 0x449, 0x447, 0x44A, 0x42E, 0x410, 0x411, 0x426, 0x414, 0x415, 0x424, 0x413, 0x425, 0x418, 0x419, 0x41A, 0x41B, 0x41C, 0x41D, 0x41E, 0x41F, 0x42F, 0x420, 0x421, 0x422, 0x423, 0x416, 0x412, 0x42C, 0x42B, 0x417, 0x428, 0x42D, 0x429, 0x427, 0x42A }; short _gdc_koi8u_mapping[128] = { 0x2500, 0x2502, 0x250C, 0x2510, 0x2514, 0x2518, 0x251C, 0x2524, 0x252C, 0x2534, 0x253C, 0x2580, 0x2584, 0x2588, 0x258C, 0x2590, 0x2591, 0x2592, 0x2593, 0x2320, 0x25A0, 0x2019, 0x221A, 0x2248, 0x2264, 0x2265, 0xA0, 0x2321, 0xB0, 0xB2, 0xB7, 0xF7, 0x2550, 0x2551, 0x2552, 0x451, 0x454, 0x2554, 0x456, 0x457, 0x2557, 0x2558, 0x2559, 0x255A, 0x255B, 0x491, 0x255D, 0x255E, 0x255F, 0x2560, 0x2561, 0x401, 0x404, 0x2563, 0x406, 0x407, 0x2566, 0x2567, 0x2568, 0x2569, 0x256A, 0x490, 0x256C, 0xA9, 0x44E, 0x430, 0x431, 0x446, 0x434, 0x435, 0x444, 0x433, 0x445, 0x438, 0x439, 0x43A, 0x43B, 0x43C, 0x43D, 0x43E, 0x43F, 0x44F, 0x440, 0x441, 0x442, 0x443, 0x436, 0x432, 0x44C, 0x44B, 0x437, 0x448, 0x44D, 0x449, 0x447, 0x44A, 0x42E, 0x410, 0x411, 0x426, 0x414, 0x415, 0x424, 0x413, 0x425, 0x418, 0x419, 0x41A, 0x41B, 0x41C, 0x41D, 0x41E, 0x41F, 0x42F, 0x420, 0x421, 0x422, 0x423, 0x416, 0x412, 0x42C, 0x42B, 0x417, 0x428, 0x42D, 0x429, 0x427, 0x42A }; short _gdc_koi8ru_mapping[128] = { 0x2500, 0x2502, 0x250C, 0x2510, 0x2514, 0x2518, 0x251C, 0x2524, 0x252C, 0x2534, 0x253C, 0x2580, 0x2584, 0x2588, 0x258C, 0x2590, 0x2591, 0x2592, 0x2593, 0x201C, 0x25A0, 0x2219, 0x201D, 0x2014, 0x2116, 0x2122, 0xA0, 0xBB, 0xAE, 0xAB, 0xB7, 0xA4, 0x2550, 0x2551, 0x2552, 0x451, 0x454, 0x2554, 0x456, 0x457, 0x2557, 0x2558, 0x2559, 0x255A, 0x255B, 0x491, 0x45E, 0x255E, 0x255F, 0x2560, 0x2561, 0x401, 0x404, 0x2563, 0x406, 0x407, 0x2566, 0x2567, 0x2568, 0x2569, 0x256A, 0x490, 0x40E, 0xA9, 0x44E, 0x430, 0x431, 0x446, 0x434, 0x435, 0x444, 0x433, 0x445, 0x438, 0x439, 0x43A, 0x43B, 0x43C, 0x43D, 0x43E, 0x43F, 0x44F, 0x440, 0x441, 0x442, 0x443, 0x436, 0x432, 0x44C, 0x44B, 0x437, 0x448, 0x44D, 0x449, 0x447, 0x44A, 0x42E, 0x410, 0x411, 0x426, 0x414, 0x415, 0x424, 0x413, 0x425, 0x418, 0x419, 0x41A, 0x41B, 0x41C, 0x41D, 0x41E, 0x41F, 0x42F, 0x420, 0x421, 0x422, 0x423, 0x416, 0x412, 0x42C, 0x42B, 0x417, 0x428, 0x42D, 0x429, 0x427, 0x42A, }; #if defined WIN32 #define WinDef(d) d // Map the windows codepages to their real names. #define WINDOWS_1250 EASTEUROPE_CHARSET #define WINDOWS_1252 ANSI_CHARSET #define WINDOWS_1251 RUSSIAN_CHARSET #define WINDOWS_1253 GREEK_CHARSET #define WINDOWS_1254 TURKISH_CHARSET #define WINDOWS_1255 HEBREW_CHARSET #define WINDOWS_1256 ARABIC_CHARSET #define WINDOWS_1257 BALTIC_CHARSET #define WINDOWS_932 SHIFTJIS_CHARSET #define WINDOWS_936 GB2313_CHARSET #define WINDOWS_949 HANGEUL_CHARSET #define WINDOWS_950 CHINESEBIG5_CHARSET #else #define WinDef(d) 0 #endif LCharset::LCharset(const char *cp, const char *des, short *map, const char *alt) { Charset = cp; Description = des; UnicodeMap = map; IconvName = 0; AlternateNames = alt; Type = CpNone; if (cp) { if (stricmp(cp, "utf-8") == 0) { Type = CpUtf8; } else if (stricmp(cp, "utf-16") == 0) { Type = CpUtf16; } else if (stricmp(cp, "utf-32") == 0) { Type = CpUtf32; } else if (stricmp(cp, "ucs-2") == 0) { Type = CpUtf16; IconvName = "UCS-2-INTERNAL"; } else if (UnicodeMap) { Type = CpMapped; } #ifdef WIN32 else if (strnicmp(cp, "windows-", 8) == 0) { Type = CpWindowsDb; } #endif else { Type = CpIconv; } } } bool LCharset::IsUnicode() { return (Type == CpUtf8) || (Type == CpUtf16) || (Type == CpUtf32); } const char *LCharset::GetIconvName() { return IconvName ? IconvName : Charset; } bool LCharset::IsAvailable() { if (Type != CpIconv) return true; #ifndef LGI_STATIC LFontSystem *FontSys = LFontSystem::Inst(); if (FontSys) return FontSys->HasIconv(true); #endif // LGI_STATIC return false; } static LCharset LgiCharsets[] = { // Good 'ol ascii LCharset("us-ascii", "ASCII", _gdc_usascii_mapping, "ascii-us,iso-ir-6,ANSI_X3.4-1986,ISO_646.irv,ASCII,ISO646-US,us,IBM367,cp367,csASCII"), // Unicode (up here so they get found first) LCharset("utf-8", "Utf-8"), LCharset("utf-16", "Utf-16"), LCharset("utf-32", "Utf-32"), LCharset("ucs-2", "Ucs-2"), // ISO (get prefered over windows charsets by being first in this list) LCharset("iso-8859-1", "ISO 8859-1 (West Europe)", _gdc_ISO_8859_identity_mapping, "iso-ir-100,ISO_8859-1,latin1,l1,IBM819,CP819,csISOLatin1,iso8859-1"), LCharset("iso-8859-2", "ISO 8859-2 (East Europe)", _gdc_ISO_8859_2_mapping, "iso-ir-101,ISO_8859-2,latin2,l2,csISOLatin2"), LCharset("iso-8859-3", "ISO 8859-3 (Latin Script)", _gdc_ISO_8859_3_mapping, "iso-ir-109,ISO_8859-3,latin3,l3,csISOLatin3"), LCharset("iso-8859-4", "ISO 8859-4 (Baltic)", _gdc_ISO_8859_4_mapping, "iso-ir-110,ISO_8859-4,latin4,l4,csISOLatin4"), LCharset("iso-8859-5", "ISO 8859-5 (Russian)", _gdc_ISO_8859_5_mapping, "iso-ir-144,ISO_8859-5,cyrillic,csISOLatinCyrillic"), LCharset("iso-8859-6", "ISO 8859-6 (Arabic)", _gdc_ISO_8859_6_mapping, "iso-ir-127,ISO_8859-6,ECMA-114,ASMO-708,arabic,csISOLatinArabic"), LCharset("iso-8859-7", "ISO 8859-7 (Greek)", _gdc_ISO_8859_7_mapping, "iso-ir-126,ISO_8859-7,ELOT_928,ECMA-118,greek,greek8,csISOLatinGreek"), LCharset("iso-8859-8", "ISO 8859-8 (Hebrew)", _gdc_ISO_8859_8_mapping, "iso-ir-138,ISO_8859-8,hebrew,csISOLatinHebrew,iso-8859-8-i"), LCharset("iso-8859-9", "ISO 8859-9 (Turkish)", _gdc_ISO_8859_9_mapping, "iso-ir-148,ISO_8859-9,latin5,l5,csISOLatin5"), LCharset("iso-8859-13", "ISO 8859-13 (Baltik)", _gdc_ISO_8859_13_mapping, "ISO_8859-9"), LCharset("iso-8859-15", "ISO 8859-15 (Latic 9)", _gdc_ISO_8859_15_mapping, "ISO_8859-15"), // Windows LCharset("windows-874", "Windows 874 (Thai)", _gdc_win_874_mapping, "iso-8859-11,cp874"), LCharset("windows-932", "Windows 932 (Japanese)"), LCharset("windows-936", "Windows 936 (Chinese)"), LCharset("windows-949", "Windows 949 (Korean)"), LCharset("windows-950", "Windows 950 (Chinese)"), LCharset("windows-1250", "Windows 1250 (Latin 2)", _gdc_win_1250_mapping, "x-cp1250,cp1250"), LCharset("windows-1251", "Windows 1251 (Cyrillic)", _gdc_win_1251_mapping, "x-cp1251,cp1251"), LCharset("windows-1252", "Windows 1252 (Latin 1)", _gdc_win_1252_mapping, "x-cp1252,cp1252"), LCharset("windows-1253", "Windows 1253 (Greek)", _gdc_win_1253_mapping, "x-cp1253,cp1253"), LCharset("windows-1254", "Windows 1254 (Turkish)", _gdc_win_1254_mapping, "x-cp1254,cp1254"), LCharset("windows-1255", "Windows 1255 (Hebrew)", _gdc_win_1255_mapping, "x-cp1255,cp1255"), LCharset("windows-1256", "Windows 1256 (Arabic)", _gdc_win_1256_mapping, "x-cp1256,cp1256"), LCharset("windows-1257", "Windows 1257 (Baltic)", _gdc_win_1257_mapping, "x-cp1257,cp1257"), LCharset("windows-1258", "Windows 1258 (Veitnam)", _gdc_win_1258_mapping, "x-cp1258,cp1258"), // Russian LCharset("koi8-r", "KOI8-R", _gdc_koi8r_mapping, "csKOI8R"), LCharset("koi8-u", "KOI8-U", _gdc_koi8u_mapping, "csKOI8U"), LCharset("koi8-ru", "KOI8-RU", _gdc_koi8ru_mapping, "csKOI8RU"), LCharset("koi8-t", "KOI8-T (Tajik)"), // Codepages LCharset("cp850", "Cp850", 0, "IBM850,850,csPC850Multilingual"), LCharset("cp862", "Cp862", 0, "IBM862,862,csPC862LatinHebrew"), LCharset("cp866", "Cp866", 0, "IBM866,866,csIBM866"), LCharset("cp1133", "Cp1133 (Laotian)"), // Japanese LCharset("euc-jp", "EUC-JP", 0, "csEUCPkdFmtJapanese"), LCharset("shift_jis", "SHIFT_JIS", 0, "MS_Kanji,csShiftJIS"), LCharset("cp932", "cp932", 0, 0), LCharset("iso-2022-jp", "ISO-2022-JP", 0, "csISO2022JP"), LCharset("iso-2022-jp-1", "ISO-2022-JP-1"), LCharset("iso-2022-jp-2", "ISO-2022-JP-2", 0, "csISO2022JP2"), // Chinese LCharset("euc-cn", "EUC-CN (Chinese)"), LCharset("hz-gb-2312", "HZ (Chinese)", 0, "hz"), LCharset("gbk", "GBK (Chinese)", 0, "CP936,MS936,windows-936,x-gbk,gb2312,GB-2312,csGB2312,GB2312_CHARSET"), LCharset("gb18030", "GB18030 (Chinese)"), LCharset("euc-tw", "EUC-TW (Chinese)"), LCharset("big5", "BIG5 (Chinese)", 0, "csBig5"), LCharset("big5-hkscs", "BIG5-HKSCS (Chinese)"), // LCharset("gb2312", "GB-2312 (Chinese)", 0, "GB-2312,csGB2312"), LCharset("iso-2022-cn", "ISO-2022-CN (Chinese)"), LCharset("iso-2022-cn-eXT","ISO-2022-CN-EXT (Chinese)"), // Korean LCharset("euc-kr", "EUC-KR", 0, "csEUCKR"), LCharset("iso-2022-kr", "ISO-2022-KR", 0, "csISO2022KR"), LCharset("johab", "JOHAB"), LCharset("cp949", "CP949", 0, "ks_c_5601-1987,ks_c_5601"), // Armenian // LCharset("armscii-8", "ARMSCII-8 (Armenian)"), // Georgian LCharset("Georgian-Academy","Georgian-Academy"), LCharset("Georgian-PS", " Georgian-PS"), // Thai LCharset("tis-620", "TIS-620 (Thai)"), // Laotian LCharset("mulelao-1", "MuleLao-1"), // Vietnamese LCharset("viscii", "VISCII (Vietnamese)", 0, "csVISCII"), LCharset("tcvn", "TCVN (Vietnamese)"), // EOF marker LCharset() }; static LCharsetSystem CharsetSystem; LCharsetSystem *LCharsetSystem::Inst() { return &CharsetSystem; } LCharset *LGetCpInfo(const char *Cs) { return CharsetSystem.GetCsInfo(Cs); } ///////////////////////////////////////////////////////////////////////////// // Utf-16 conversion int LCpToAnsi(char *cp) { int Ansi = 0; if (cp && strnicmp(cp, "windows-", 8) == 0) { Ansi = atoi(cp+9); } return Ansi; } ssize_t LBufConvertCp(void *Out, const char *OutCp, ssize_t OutLen, const void *&In, const char *InCp, ssize_t &InLen) { int Status = 0; if (Out && OutCp && In && InCp) { LCharset *InInfo = LGetCpInfo(InCp); LCharset *OutInfo = LGetCpInfo(OutCp); if (InInfo && OutInfo) { char *In8 = (char*)In; uchar *Out8 = (uchar*)Out; if (InLen < 0) { switch (InInfo->Type) { case CpMapped: case CpUtf8: case CpIconv: InLen = (int)strlen((char*)In); break; case CpUtf16: case CpWindowsDb: InLen = StringLen((uint16*)In) << 1; break; case CpUtf32: InLen = StringLen((uint32_t*)In) << 2; break; default: LAssert(0); return 0; } } #ifdef WIN32 if (InInfo->Type == CpWindowsDb && OutInfo->Type == CpUtf16) { // mb -> unicode char Cp[32]; sprintf_s(Cp, sizeof(Cp), ".%s", InInfo->Charset + 8); setlocale(LC_ALL, Cp); void *Start = Out; while (OutLen >= sizeof(char16) && InLen > 0) { int s = mbtowc((char16*)Out, (char*)In, min(InLen, MB_CUR_MAX)); if (s > 0) { ((char*&)In) += s; InLen -= s; ((char16*&)Out)++; OutLen -= sizeof(char16); } else break; } return (NativeInt)Out-(NativeInt)Start; } else if (InInfo->Type == CpUtf16 && OutInfo->Type == CpWindowsDb) { // unicode -> mb char Cp[32]; sprintf_s(Cp, sizeof(Cp), ".%s", OutInfo->Charset + 8); setlocale(LC_ALL, Cp); void *Start = Out; while (OutLen >= MB_CUR_MAX && InLen > sizeof(char16) ) { #if 1 int s = 0; errno_t err = wctomb_s(&s, (char*)Out, OutLen, ((char16*)In)[0]); if (err || s == 0) break; #else int s = wctomb((char*)Out, ((char16*)In)[0] ); if (s > 0) #endif { ((char16*&)In)++; InLen -= sizeof(char16); ((char*&)Out) += s; OutLen -= s; } } return (NativeInt)Out-(NativeInt)Start; } else #endif if (InInfo->Type == CpIconv || OutInfo->Type == CpIconv) { #ifndef LGI_STATIC LFontSystem *Fs = LFontSystem::Inst(); if (Fs) return Fs->IconvConvert(OutInfo->GetIconvName(), (char*)Out, OutLen, InInfo->GetIconvName(), (const char*&)In, InLen); #else LAssert(!"No iconv in static build"); #endif } else { // Mapped or Utf conversion uint32_t Utf32 = 0; while (OutLen > 0 && InLen > 0) { char *RewindIn = In8; ptrdiff_t RewindInLen = InLen; // Convert input char to Utf-32 switch (InInfo->Type) { case CpMapped: { if (*In8) { uchar i = (uchar)*In8++; InLen--; if (i & 0x80) { Utf32 = InInfo->UnicodeMap[i - 0x80]; if (!Utf32) Utf32 = '?'; } else { Utf32 = i; } } else { Utf32 = 0; InLen = 0; } break; } case CpUtf8: { Utf32 = LgiUtf8To32((uint8_t *&)In8, InLen); break; } case CpUtf16: { Utf32 = LgiUtf16To32((const uint16_t *&)In8, InLen); if (Utf32 == 0xfeff || Utf32 == 0xfffe) continue; break; } case CpUtf32: { Utf32 = *((uint32_t*&)In8)++; InLen -= 4; break; } default: LAssert(0); break; } if (!Utf32) { break; } // Convert Utf-32 into output format switch (OutInfo->Type) { case CpMapped: { if (Utf32 & ~0x7f) { int n; for (n=0; n<128; n++) { if (OutInfo->UnicodeMap[n] == Utf32) { *Out8++ = 0x80 + n; break; } } if (n >= 128) { for (n=0; MissingMaps[n].Unicode; n++) { if (MissingMaps[n].Unicode == Utf32) { *Out8++ = MissingMaps[n].Ascii; break; } } if (!MissingMaps[n].Unicode) { *Out8++ = '?'; } } } else { *Out8++ = Utf32; } OutLen--; break; } case CpUtf8: { // uchar *PrevOut8 = Out8; if (!LgiUtf32To8(Utf32, (uint8_t*&) Out8, OutLen)) { // Not enough buffer to encode the character In8 = RewindIn; InLen = RewindInLen; OutLen = 0; } break; } case CpUtf16: { LgiUtf32To16(Utf32, (uint16_t*&)Out8, OutLen); break; } case CpUtf32: { *((uint32_t*&)Out8)++ = Utf32; OutLen -= 4; break; } default: { break; } } } In = (void*)In8; Status = (int) (Out8 - (uchar*)Out); } } else { // printf("%s:%i - LBufConvertCp failed '%s' -> '%s'.\n", __FILE__, __LINE__, InCp, OutCp); } } return Status; } template T *DupeString(T *s, ssize_t Len = -1) { if (!s) return NULL; if (Len < 0) { Len = 0; while (s[Len]) Len++; } T *ns = new T[Len+1]; if (!ns) return NULL; memcpy(ns, s, sizeof(T) * Len); ns[Len] = 0; return ns; } LString LStrConvertCp(const char *OutCp, const void *In, const char *InCp, ssize_t InLen) { if (!OutCp || !In || !InCp) return LString(); LCharset *InInfo = LGetCpInfo(InCp); LCharset *OutInfo = LGetCpInfo(OutCp); if (!InInfo || !OutInfo) return LString(); if (InLen < 0) { switch (InInfo->Type) { case CpMapped: case CpUtf8: case CpIconv: InLen = (int)strlen((char*)In); break; case CpUtf16: case CpWindowsDb: InLen = StringLen((uint16*)In) << 1; break; case CpUtf32: InLen = StringLen((uint32_t*)In) << 2; break; default: return LString(); } } switch (OutInfo->Type) { case CpMapped: case CpUtf8: case CpIconv: break; case CpUtf16: case CpWindowsDb: case CpUtf32: default: LAssert(!"LString doesn't >8bit char (yet)."); return LString(); } if (!stricmp(InCp, OutCp)) return LString((char*)In, InLen); LStringPipe b; if (InInfo->Type == CpIconv || OutInfo->Type == CpIconv) { #ifndef LGI_STATIC LFontSystem *Fs = LFontSystem::Inst(); if (Fs) { auto InCs = InInfo->GetIconvName(); auto OutCs = OutInfo->GetIconvName(); if (Fs->IconvConvert(OutCs, &b, InCs, (const char*&)In, InLen)) - return b.NewGStr(); + return b.NewLStr(); InCp = "iso-8859-1"; } #else LAssert(!"No inconv in static build"); #endif } char Buf[2 << 10]; while (InLen > 0) { ssize_t Bytes = LBufConvertCp(Buf, OutCp, sizeof(Buf), In, InCp, InLen); if (Bytes > 0) b.Write((uchar*)Buf, (int)Bytes); else break; } - return b.NewGStr(); + return b.NewLStr(); } void *LNewConvertCp(const char *OutCp, const void *In, const char *InCp, ssize_t InLen) { if (!OutCp || !In || !InCp) return NULL; LCharset *InInfo = LGetCpInfo(InCp); LCharset *OutInfo = LGetCpInfo(OutCp); if (!InInfo || !OutInfo) return NULL; LMemQueue b; if (InLen < 0) { switch (InInfo->Type) { case CpMapped: case CpUtf8: case CpIconv: InLen = (int)strlen((char*)In); break; case CpUtf16: case CpWindowsDb: InLen = StringLen((uint16*)In) << 1; break; case CpUtf32: InLen = StringLen((uint32_t*)In) << 2; break; default: LAssert(0); return NULL; } } int NullSize; switch (OutInfo->Type) { case CpMapped: case CpUtf8: case CpIconv: NullSize = 1; break; case CpUtf16: case CpWindowsDb: NullSize = 2; break; case CpUtf32: NullSize = 4; break; default: LAssert(0); return NULL; } if (!stricmp(InCp, OutCp)) { if (InInfo->Type == CpUtf16) return DupeString((uint16*)In, InLen/sizeof(uint16)); else if (InInfo->Type == CpUtf32) return DupeString((uint32_t*)In, InLen/sizeof(uint32_t)); else return NewStr((char*)In, InLen); } if (InInfo->Type == CpIconv || OutInfo->Type == CpIconv) { #ifndef LGI_STATIC LFontSystem *Fs = LFontSystem::Inst(); if (Fs) { const char *InCs = InInfo->GetIconvName(); const char *OutCs = OutInfo->GetIconvName(); if (!Fs->IconvConvert(OutCs, &b, InCs, (const char*&)In, InLen)) { InCp = "iso-8859-1"; goto BufConvert; } } #else LAssert(!"No inconv in static build"); #endif } else { BufConvert: char Buf[2 << 10]; while (InLen > 0) { ssize_t Bytes = LBufConvertCp(Buf, OutCp, sizeof(Buf), In, InCp, InLen); if (Bytes > 0) { b.Write((uchar*)Buf, (int)Bytes); } else { break; } } } return b.GetSize() ? b.New(NullSize) : 0; } int LCharLen(const void *Str, const char *Cp, int Bytes) { if (Str && Cp) { LCharset *InInfo = LGetCpInfo(Cp); if (InInfo) { switch (InInfo->Type) { default: case CpMapped: { return (int)strlen((char*)Str); } case CpUtf8: { uchar *s = (uchar*)Str; int Len = 0; if (Bytes > 0) { uchar *e = s + Bytes; while (*s && s < e) { LgiNextUtf8((char*&)s); Len++; } } else { while (*s) { LgiNextUtf8((char*&)s); Len++; } } return Len; } case CpUtf16: { return StringLen((uint16*)Str); } case CpUtf32: { return StringLen((uint32_t*)Str); } } } } return 0; } bool LIsCpImplemented(char *Cp) { return LGetCpInfo(Cp) != 0; } const char *LAnsiToLgiCp(int AnsiCodePage) { if (AnsiCodePage < 0) { #ifdef WIN32 AnsiCodePage = GetACP(); #else return "utf-8"; #endif } #define WinCp(i) case i: return "windows-" #i; switch (AnsiCodePage) { WinCp(874) WinCp(932) WinCp(936) WinCp(949) WinCp(950) WinCp(1250) WinCp(1251) WinCp(1252) WinCp(1253) WinCp(1254) WinCp(1255) WinCp(1256) WinCp(1257) WinCp(1258) case 20127: return "us-ascii"; case 28591: return "iso-8859-1"; case 28592: return "iso-8859-2"; case 28593: return "iso-8859-3"; case 28594: return "iso-8859-4"; case 28595: return "iso-8859-5"; case 28596: return "iso-8859-6"; case 28597: return "iso-8859-7"; case 28598: return "iso-8859-8"; case 28599: return "iso-8859-9"; case 28600: return "ISO-8859-10"; case 28605: return "ISO-8859-15"; case 50220: case 50221: return "iso-2022-jp"; case 51932: return "euc-jp"; case 51949: return "euc-kr"; case 65001: return "utf-8"; } #undef WinCp return 0; } char *LSeekUtf8(const char *Ptr, ssize_t D, char *Start) { uchar *p = (uchar*)Ptr; if (p) { if (D >= 0) { for (int i=0; i(uchar*)Start; i++) { p--; while (p>(uchar*)Start && IsUtf8_Trail(*p)) p--; } } else { // You must pass a start point to move backwards in // the utf-8 string, otherwise you can run off the // beginning of the array. LAssert(0); } } return (char*)p; } bool LMatchCharset(short *Map, char16 *Utf, bool &Has8Bit) { if (Map && Utf) { // Test Charset because we have a map of all the chars in it... char16 *c; for (c = Utf; *c; c++) { if (*c > 0x7f) { // Check char Has8Bit = true; int i; for (i=0; i<128; i++) { if (Map[i] == *c) break; } if (i >= 128) { // Char not found return false; } } } if (Has8Bit) { if (!*c) { return true; } } } return false; } const char *LUnicodeToCharset(const char *Utf8, ssize_t Len, List *Prefs) { const char *Status = "utf-8"; // The default.. LAutoWString Utf((char16*)LNewConvertCp(LGI_WideCharset, Utf8, "utf-8", Len)); if (Utf) { if (Prefs) { for (auto p: *Prefs) { LCharset *Cp = CharsetSystem.GetCsInfo(p); if (Cp && stricmp(Cp->Charset, "us-ascii") != 0 && Cp->UnicodeMap) { bool Has8Bit = false; if (LMatchCharset(Cp->UnicodeMap, Utf, Has8Bit)) { return Cp->Charset; } if (!Has8Bit) { return "us-ascii"; } } } } for (LCharset *Cp = LgiCharsets + 1; Cp->Charset; Cp++) { if (Cp->UnicodeMap) { bool Has8Bit = false; if (LMatchCharset(Cp->UnicodeMap, Utf, Has8Bit)) { return Cp->Charset; } if (!Has8Bit) { return "us-ascii"; } } } } return Status; } LString LToNativeCp(const char *In, ssize_t InLen) { const char *Cp = LAnsiToLgiCp(); LString s; #ifdef WIN32 LCharset *CpInfo = LGetCpInfo(Cp); if (!CpInfo || CpInfo->Type == CpWindowsDb) { if (In) { // Double byte charset conversion, don't rely on iconv // being around to do the conversion. setlocale(LC_ALL, ".ACP"); if (InLen < 0) InLen = strlen(In); LAutoWString Wide(Utf8ToWide(In, InLen)); if (Wide) { size_t Converted; auto Len = wcstombs_s(&Converted, NULL, 0, Wide, 0); if (s.Length(Len)) { wcstombs_s(&Converted, s.Get(), Len+1, Wide, Len+1); s.Get()[Len] = 0; } } } } #endif if (!s) s = LStrConvertCp(Cp, In, "utf-8", InLen); return s; } LString LFromNativeCp(const char *In, ssize_t InLen) { const char *Cp = LAnsiToLgiCp(); LString s; #ifdef WIN32 LCharset *CpInfo = LGetCpInfo(Cp); if (!CpInfo || CpInfo->Type == CpWindowsDb) { if (In) { // Double byte charset conversion, don't rely on iconv // being around to do the conversion. setlocale(LC_ALL, ".ACP"); if (InLen < 0) { #ifdef __GNUC__ // FIXME InLen = strlen(In); #else InLen = _mbstrlen(In); #endif } else { // Work out how many chars 'InLen' bytes is ssize_t Bytes = InLen; const char *i = In; int Chars = 0; while (*i && Bytes > 0) { int n = mblen(i, MB_CUR_MAX); if (n > 0) { Chars++; Bytes -= n; i += n; } else break; } InLen = Chars; } size_t Converted; size_t Len = mbstowcs_s(&Converted, NULL, 0, In, 0); if (Len) { LAutoWString Buf(new char16[Len+1]); if (Buf) { mbstowcs_s(&Converted, Buf, Len, In, Len); Buf[Len] = 0; s = Buf; } } } } #endif if (!s) s = LStrConvertCp("utf-8", In, Cp, InLen); return s; } /////////////////////////////////////////////////////////////////////////// struct LCharsetSystemPriv { LCharset *Utf8; LCharset *Utf16; LHashTbl, LCharset*> Charsets; LCharsetSystemPriv() : Charsets(512) { Utf8 = 0; Utf16 = 0; } }; LCharsetSystem::LCharsetSystem() { char l[256]; // Charset setup, store all the charset pointers // in a hash table for O(1) lookup. d = new LCharsetSystemPriv; LAssert(LgiCharsets->Charset != NULL); for (LCharset *Cs = LgiCharsets; Cs->Charset; Cs++) { strcpy_s(l, sizeof(l), Cs->Charset); #ifdef _MSC_VER _strlwr_s(l, sizeof(l)); #else strlwr(l); #endif if (!stricmp(l, "utf-8")) d->Utf8 = Cs; else if (!stricmp(l, "utf-16")) d->Utf16 = Cs; d->Charsets.Add(l, Cs); auto a = LString(Cs->AlternateNames).SplitDelimit(","); for (int n=0; nCharsets.Add(l, Cs); } } } LCharsetSystem::~LCharsetSystem() { DeleteObj(d); } LCharset *LCharsetSystem::GetCsInfo(const char *Cp) { if (Cp && d) { // Lookup the charset in the hash table char l[256]; strcpy_s(l, sizeof(l), Cp); #ifdef _MSC_VER _strlwr_s(l, sizeof(l)); #else strlwr(l); #endif if (!stricmp(l, "utf-8")) return d->Utf8; else if (!stricmp(l, "utf-16")) return d->Utf16; LCharset *Cs = (LCharset*) d->Charsets.Find(l); if (Cs) { return Cs; } else { // printf("%s:%i - No charset '%s' in font sub system.\n", __FILE__, __LINE__, l); // printf("Charsets=%i\n", Charsets->GetUsed()); } } return 0; } LCharset *LGetCsInfo(const char *Cs) { return CharsetSystem.GetCsInfo(Cs); } LCharset *LCharsetSystem::GetCsList() { return LgiCharsets; } LCharset *LGetCsList() { return LgiCharsets; } diff --git a/src/common/Gdc2/Font/Font.cpp b/src/common/Gdc2/Font/Font.cpp --- a/src/common/Gdc2/Font/Font.cpp +++ b/src/common/Gdc2/Font/Font.cpp @@ -1,1406 +1,1406 @@ /*hdr ** FILE: LFont.cpp ** AUTHOR: Matthew Allen ** DATE: 5/5/97 ** DESCRIPTION: Gdc2 Font Support ** ** Copyright (C) 1997-2002, Matthew Allen ** fret@memecode.com */ #ifndef WIN32 #define _WIN32_WINNT 0x500 #endif ////////////////////////////////////////////////////////////////////// // Includes #include #include #include #include #include "lgi/common/Lgi.h" #include "lgi/common/Variant.h" #include "lgi/common/DisplayString.h" #include "lgi/common/GdiLeak.h" #include "lgi/common/Css.h" #include "FontPriv.h" #ifdef FontChange #undef FontChange #endif ////////////////////////////////////////////////////////////////////// // Helpers #if defined(LGI_SDL) class FreetypeLib { FT_Library lib; FT_Error err; public: FreetypeLib() { err = FT_Init_FreeType(&lib); if (err) { LAssert(0); } } ~FreetypeLib() { if (!err) { FT_Done_FreeType(lib); } } FT_Library Handle() { return lib; } LString GetVersion() { FT_Int amajor = 0, aminor = 0, apatch = 0; FT_Library_Version(lib, &amajor, &aminor, &apatch); LString s; s.Printf("%i.%i.%i", amajor, aminor, apatch); return s; } } Freetype2; LString GetFreetypeLibraryVersion() { return Freetype2.GetVersion(); } #elif defined(WIN32) #ifndef __GNUC__ #include #endif int WinPointToHeight(int Pt, HDC hDC) { int Ht = 0; HWND hDestktop = NULL; if (!hDC) hDC = GetDC(hDestktop = GetDesktopWindow()); if (hDC) Ht = -MulDiv(Pt, GetDeviceCaps(hDC, LOGPIXELSY), 72); if (hDestktop) ReleaseDC(hDestktop, hDC); return Ht; } int WinHeightToPoint(int Ht, HDC hDC) { int Pt = 0; HWND hDestktop = NULL; if (!hDC) hDC = GetDC(hDestktop = GetDesktopWindow()); if (hDC) Pt = -MulDiv(Ht, 72, GetDeviceCaps(hDC, LOGPIXELSY)); if (hDestktop) ReleaseDC(hDestktop, hDC); return Pt; } #elif defined(__GTK_H__) #include #elif USE_CORETEXT // CTFontCreateUIFontForLanguage // #include #include #endif //////////////////////////////////////////////////////////////////// #ifdef WINDOWS LAutoPtr LFontPrivate::Gdi32; #endif LFont::LFont(const char *face, LCss::Len size) { d = new LFontPrivate; if (face && size.IsValid()) { Create(face, size); } } LFont::LFont(OsFont Handle) { d = new LFontPrivate; LFontType Type; if (Type.GetFromRef(Handle)) { Create(&Type); } } LFont::LFont(LFontType &Type) { d = new LFontPrivate; Create(&Type); } LFont::LFont(LFont &Fnt) { d = new LFontPrivate; *this = Fnt; } LFont::~LFont() { LAssert(d->WarnOnDelete == false); Destroy(); DeleteObj(d); } bool LFont::CreateFromCss(const char *Css) { if (!Css) return false; LCss c; c.Parse(Css); return CreateFromCss(&c); } bool LFont::CreateFromCss(LCss *Css) { if (!Css) return false; LCss::StringsDef Fam = Css->FontFamily(); if (Fam.Length()) Face(Fam[0]); else Face(LSysFont->Face()); LCss::Len Sz = Css->FontSize(); switch (Sz.Type) { case LCss::SizeSmaller: Size(LCss::Len(LCss::LenPt, (float)LSysFont->PointSize()-1)); break; case LCss::SizeLarger: Size(LCss::Len(LCss::LenPt, (float)LSysFont->PointSize()+1)); break; case LCss::LenInherit: Size(LSysFont->Size()); break; default: Size(Sz); break; } LCss::FontWeightType w = Css->FontWeight(); if (w == LCss::FontWeightBold) Bold(true); LCss::FontStyleType s = Css->FontStyle(); if (s == LCss::FontStyleItalic) Italic(true); LCss::TextDecorType dec = Css->TextDecoration(); if (dec == LCss::TextDecorUnderline) Underline(true); return Create(); } LString LFont::FontToCss() { LCss c; c.FontFamily(Face()); c.FontSize(Size()); if (Bold()) c.FontWeight(LCss::FontWeightBold); if (Italic()) c.FontStyle(LCss::FontStyleItalic); auto aStr = c.ToString(); return aStr.Get(); } void LFont::WarnOnDelete(bool w) { d->WarnOnDelete = w; } bool LFont::Destroy() { bool Status = true; if (d->hFont) { #if LGI_SDL FT_Done_Face(d->hFont); #elif defined(WIN32) DeleteObject(d->hFont); #elif defined __GTK_H__ if (d->PangoCtx) { g_object_unref(d->PangoCtx); d->PangoCtx = NULL; } Gtk::pango_font_description_free(d->hFont); #elif defined MAC #if USE_CORETEXT CFRelease(d->hFont); #else ATSUDisposeStyle(d->hFont); #endif #elif defined(HAIKU) DeleteObj(d->hFont); #else LAssert(0); #endif d->hFont = 0; } DeleteArray(d->GlyphMap); return Status; } #if USE_CORETEXT CFDictionaryRef LFont::GetAttributes() { return d->Attributes; } #endif uchar *LFont::GetGlyphMap() { return d->GlyphMap; } bool LFont::GetOwnerUnderline() { return d->OwnerUnderline; } void LFont::_OnPropChange(bool FontChange) { if (FontChange) { Destroy(); } } OsFont LFont::Handle() { return d->hFont; } int LFont::GetHeight() { if (!d->hFont) { Create(); } // I've decided for the moment to allow zero pt fonts to make a HTML test case render correctly. // LAssert(d->Height != 0); return d->Height; } bool LFont::IsValid() { bool Status = false; // recreate font #ifdef WIN32 if (!d->hFont) { Status = Create(Face(), Size()); } #else if (d->Dirty) { Status = Create(Face(), Size()); d->Dirty = false; } #endif else { Status = true; } return Status; } #ifdef WIN32 #define ENCODING_TABLE_SIZE 8 class type_4_cmap { public: uint16 format; uint16 length; uint16 language; uint16 seg_count_x_2; uint16 search_range; uint16 entry_selector; uint16 range_shift; // uint16 reserved; uint16 arrays[1]; uint16 SegCount() { return seg_count_x_2 / 2; } uint16 *GetIdRangeOffset() { return arrays + 1 + (SegCount()*3); } uint16 *GetStartCount() { return arrays + 1 + SegCount(); } uint16 *GetEndCount() { /* Apparently the reseved spot is not reserved for the end_count array!? */ return arrays; } }; class cmap_encoding_subtable { public: uint16 platform_id; uint16 encoding_id; uint32_t offset; }; #define INT16_SWAP(i) ( ((i)>>8) | (((i)&0xff)<<8) ) #define INT32_SWAP(i) ( ( ((i)&0x000000ff) << 24) | \ ( ((i)&0x0000ff00) << 8 ) | \ ( ((i)&0x00ff0000) >> 8 ) | \ ( ((i)&0xff000000) >> 24) ) #define MAKE_TT_TABLE_NAME(c1, c2, c3, c4) \ (((uint32_t)c4) << 24 | ((uint32_t)c3) << 16 | ((uint32_t)c2) << 8 | ((uint32_t)c1)) #define CMAP (MAKE_TT_TABLE_NAME('c','m','a','p')) #define CMAP_HEADER_SIZE 4 #define APPLE_UNICODE_PLATFORM_ID 0 #define MACINTOSH_PLATFORM_ID 1 #define ISO_PLATFORM_ID 2 #define MICROSOFT_PLATFORM_ID 3 #define SYMBOL_ENCODING_ID 0 #define UNICODE_ENCODING_ID 1 #define UCS4_ENCODING_ID 10 type_4_cmap *GetUnicodeTable(HFONT hFont, uint16_t &Length) { bool Status = false; type_4_cmap *Table = 0; HDC hDC = GetDC(0); if (hDC) { HFONT Old = (HFONT)SelectObject(hDC, hFont); uint16_t Tables = 0; uint32_t Offset = 0; // Get The number of encoding tables, at offset 2 auto Res = GetFontData(hDC, CMAP, 2, &Tables, sizeof(uint16_t)); if (Res == sizeof (uint16_t)) { Tables = INT16_SWAP(Tables); cmap_encoding_subtable *Sub = (cmap_encoding_subtable*)malloc(ENCODING_TABLE_SIZE*Tables); if (Sub) { Res = GetFontData(hDC, CMAP, CMAP_HEADER_SIZE, Sub, ENCODING_TABLE_SIZE*Tables); if (Res == ENCODING_TABLE_SIZE*Tables) { for (int i = 0; i < Tables; i++) { Sub[i].platform_id = INT16_SWAP(Sub[i].platform_id); Sub[i].encoding_id = INT16_SWAP(Sub[i].encoding_id); Sub[i].offset = INT32_SWAP(Sub[i].offset); if (Sub[i].platform_id == MICROSOFT_PLATFORM_ID && Sub[i].encoding_id == UNICODE_ENCODING_ID) { Offset = Sub[i].offset; } } } free(Sub); } } if (Offset) { Length = 0; uint Res = GetFontData(hDC, CMAP, Offset + 2, &Length, sizeof(uint16_t)); if (Res == sizeof(uint16)) { Length = INT16_SWAP(Length); Table = (type_4_cmap*)malloc(Length); Res = GetFontData(hDC, CMAP, Offset, Table, Length); if (Res == Length) { Table->format = INT16_SWAP(Table->format); Table->length = INT16_SWAP(Table->length); Table->language = INT16_SWAP(Table->language); Table->seg_count_x_2 = INT16_SWAP(Table->seg_count_x_2); Table->search_range = INT16_SWAP(Table->search_range); Table->entry_selector = INT16_SWAP(Table->entry_selector); Table->range_shift = INT16_SWAP(Table->range_shift); if (Table->format == 4) { uint16 *tbl_end = (uint16 *)((char *)Table + Length); uint16 *tbl = Table->arrays; while (tbl < tbl_end) { *tbl = INT16_SWAP(*tbl); tbl++; } Status = true; } } } } SelectObject(hDC, Old); ReleaseDC(0, hDC); } if (!Status) { free(Table); Table = 0; } return Table; } #endif LSurface *LFont::GetSurface() { return d->pSurface; } bool LFont::Create(const char *face, LCss::Len size, LSurface *pSurface) { bool FaceChanging = false; bool SizeChanging = false; bool ValidInitFaceSize = ValidStr(Face()) && Size().IsValid(); if (face) { if (!Face() || strcmp(Face(), face) != 0) { FaceChanging = true; } Face(face); } if (size.IsValid()) { SizeChanging = LTypeFace::d->_Size != size; LTypeFace::d->_Size = size; } if ((SizeChanging || FaceChanging) && this == LSysFont && ValidInitFaceSize) { LgiTrace("Warning: Changing sysfont definition.\n"); } if (this == LSysFont) { printf("Setting sysfont up '%s' %i\n", Face(), PointSize()); } #if LGI_SDL LString FaceName; #if defined(WIN32) const char *Ext = "ttf"; LString FontPath = "c:\\Windows\\Fonts"; #elif defined(LINUX) const char *Ext = "ttf"; LString FontPath = "/usr/share/fonts/truetype"; #elif defined(MAC) const char *Ext = "ttc"; LString FontPath = "/System/Library/Fonts"; #else #error "Put your font path here" #endif LFile::Path p = FontPath.Get(); FaceName.Printf("%s.%s", Face(), Ext); p += FaceName; LString Full = p.GetFull(); if (!LFileExists(Full)) { LArray Files; LArray Extensions; LString Pattern; Pattern.Printf("*.%s", Ext); Extensions.Add(Pattern.Get()); LRecursiveFileSearch(FontPath, &Extensions, &Files, NULL, NULL, NULL, NULL); char *Match = NULL; for (unsigned i=0; ihFont); if (error) { LgiTrace("%s:%i - FT_New_Face failed with %i\n", _FL, error); } else { auto Dpi = LScreenDpi(); int PtSize = PointSize(); int PxSize = (int) (PtSize * Dpi.x / 72.0); error = FT_Set_Char_Size( d->hFont, /* handle to face object */ 0, /* char_width in 1/64th of points */ PtSize*64, /* char_height in 1/64th of points */ Dpi, /* horizontal device resolution */ Dpi); if (error) { LgiTrace("%s:%i - FT_Set_Char_Size failed with %i\n", _FL, error); } d->Height = (int) (ceil((double)d->hFont->height * PxSize / d->hFont->units_per_EM) + 0.0001); LTypeFace::d->_Ascent = (double)d->hFont->ascender * PxSize / d->hFont->units_per_EM; LAssert(d->Height > LTypeFace::d->_Ascent); LTypeFace::d->_Descent = d->Height - LTypeFace::d->_Ascent; return true; } #elif WINNATIVE if (d->hFont) { DeleteObject(d->hFont); d->hFont = 0; } d->pSurface = pSurface; HDC hDC = pSurface ? pSurface->Handle() : GetDC(0); auto Sz = Size(); int Win32Height = 0; if (Sz.Type == LCss::LenPt) Win32Height = WinPointToHeight((int)Sz.Value, hDC); else if (Sz.Type == LCss::LenPx) Win32Height = (int)(Sz.Value * 1.2); else LAssert(!"What now?"); LTypeFace::d->IsSymbol = LTypeFace::d->_Face && ( stristr(LTypeFace::d->_Face, "wingdings") || stristr(LTypeFace::d->_Face, "symbol") ); int Cs; if (LTypeFace::d->IsSymbol) Cs = SYMBOL_CHARSET; else Cs = ANSI_CHARSET; d->OwnerUnderline = Face() && stricmp(Face(), "Courier New") == 0 && Size().Type == LCss::LenPt && (PointSize() == 8 || PointSize() == 9) && LTypeFace::d->_Underline; LAutoWString wFace(Utf8ToWide(LTypeFace::d->_Face)); if (Win32Height) d->hFont = ::CreateFont(Win32Height, 0, 0, 0, LTypeFace::d->_Weight, LTypeFace::d->_Italic, d->OwnerUnderline ? false : LTypeFace::d->_Underline, false, Cs, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, LTypeFace::d->_Quality, FF_DONTCARE, wFace); if (d->hFont) { HFONT hFnt = (HFONT) SelectObject(hDC, d->hFont); TEXTMETRIC tm; ZeroObj(tm); if (GetTextMetrics(hDC, &tm)) { d->Height = tm.tmHeight; LTypeFace::d->_Ascent = tm.tmAscent; LTypeFace::d->_Descent = tm.tmDescent; LTypeFace::d->_Leading = tm.tmInternalLeading; } else { SIZE Size = {0, 0}; GetTextExtentPoint32(hDC, L"Ag", 2, &Size); d->Height = Size.cy; } uint Bytes = (MAX_UNICODE + 1) >> 3; if (!d->GlyphMap) { d->GlyphMap = new uchar[Bytes]; } if (d->GlyphMap) { memset(d->GlyphMap, 0, Bytes); LArray OsVer; int OsType = LGetOs(&OsVer); if (OsType == LGI_OS_WIN9X || OsVer[0] < 5) // GetFontUnicodeRanges is supported on >= Win2k { bool HideUnihan = false; LAssert(sizeof(type_4_cmap)==16); uint16 Length = 0; type_4_cmap *t = GetUnicodeTable(Handle(), Length); if (t) { uint16 SegCount = t->seg_count_x_2 / 2; uint16 *EndCount = t->GetEndCount(); uint16 *StartCount = t->GetStartCount(); uint16 *IdRangeOffset = t->GetIdRangeOffset(); for (int i = 0; i= 0x3400 && u <= 0x9FAF) { // APPROXIMATE } else { // EXACT } if ((u >> 3) < Bytes) { d->GlyphMap[u>>3] |= 1 << (u & 7); } } } else { uint16 *End = (uint16*) (((char*)t)+Length); ssize_t IdBytes = End - IdRangeOffset; for (uint u = StartCount[i]; u <= EndCount[i] && IdRangeOffset[i] != 0xffff; u++) { uint id = *(IdRangeOffset[i]/2 + (u - StartCount[i]) + &IdRangeOffset[i]); if (id) { if (HideUnihan && u >= 0x3400 && u <= 0x9FAF) { // APPROXIMATE } else { // EXACT } if ((u >> 3) < Bytes) { d->GlyphMap[u>>3] |= 1 << (u & 7); } } } } } } else { // not a TTF? assume that it can handle 00-ff in the current ansi cp /* char *Cp = LAnsiToLgiCp(); for (int i=0; i<=0x7f; i++) { char16 u; uchar c = i; void *In = &c; int Size = sizeof(c); LBufConvertCp(&u, "ucs-2", sizeof(u), In, Cp, Size); if ((u >> 3) < Bytes) { GlyphMap[u>>3] |= 1 << (u & 7); } } */ } } else { typedef DWORD (WINAPI *Proc_GetFontUnicodeRanges)(HDC, LPGLYPHSET); if (!d->Gdi32) d->Gdi32.Reset(new LLibrary("Gdi32")); if (d->Gdi32) { Proc_GetFontUnicodeRanges GetFontUnicodeRanges = (Proc_GetFontUnicodeRanges)d->Gdi32->GetAddress("GetFontUnicodeRanges"); if (GetFontUnicodeRanges) { DWORD BufSize = GetFontUnicodeRanges(hDC, 0); LPGLYPHSET Set = (LPGLYPHSET) new char[BufSize]; if (Set && GetFontUnicodeRanges(hDC, Set) > 0) { for (DWORD i=0; icRanges; i++) { WCRANGE *Range = Set->ranges + i; for (int n=0; ncGlyphs; n++) { DWORD u = Range->wcLow + n; if (u >> 3 < Bytes) { d->GlyphMap[u>>3] |= 1 << (u & 7); } } } } DeleteArray((char*&)Set); } } if (LTypeFace::d->IsSymbol) { // Lies! It's all Lies! Symbol doesn't support non-breaking space. int u = 0xa0; d->GlyphMap[u >> 3] &= ~(1 << (u & 7)); } } if (d->GlyphMap) { memset(d->GlyphMap, 0xff, 128/8); } } SelectObject(hDC, hFnt); } if (!pSurface) ReleaseDC(0, hDC); return (d->hFont != 0); #elif defined __GTK_H__ Destroy(); auto Dpi = pSurface ? pSurface->GetDpi() : LScreenDpi(); float DpiScale = (float)Dpi.x / 96.0f; d->hFont = Gtk::pango_font_description_new(); if (!d->hFont) printf("%s:%i - pango_font_description_new failed: Face='%s' Size=%i Bold=%i Italic=%i\n", _FL, Face(), PointSize(), Bold(), Italic()); else if (!ValidStr(Face())) printf("%s:%i - No font face.\n", _FL); else if (!Size().IsValid()) printf("%s:%i - Invalid size.\n", _FL); else { auto Sz = Size(); LString sFace = Face(); Gtk::pango_font_description_set_family(d->hFont, sFace); if (Sz.Type == LCss::LenPt) Gtk::pango_font_description_set_size(d->hFont, Sz.Value * DpiScale * PANGO_SCALE); else if (Sz.Type == LCss::LenPx) Gtk::pango_font_description_set_absolute_size(d->hFont, Sz.Value * DpiScale * PANGO_SCALE); else { LAssert(0); return false; } if (Bold()) Gtk::pango_font_description_set_weight(d->hFont, Gtk::PANGO_WEIGHT_BOLD); // Get metrics for this font... Gtk::GtkPrintContext *PrintCtx = pSurface ? pSurface->GetPrintContext() : NULL; Gtk::PangoContext *SysCtx = LFontSystem::Inst()->GetContext(); if (PrintCtx) d->PangoCtx = gtk_print_context_create_pango_context(PrintCtx); auto EffectiveCtx = d->PangoCtx ? d->PangoCtx : SysCtx; Gtk::PangoFontMetrics *m = Gtk::pango_context_get_metrics(d->PangoCtx ? d->PangoCtx : SysCtx, d->hFont, 0); if (!m) printf("pango_font_get_metrics failed.\n"); else { LTypeFace::d->_Ascent = (double)Gtk::pango_font_metrics_get_ascent(m) / PANGO_SCALE; LTypeFace::d->_Descent = (double)Gtk::pango_font_metrics_get_descent(m) / PANGO_SCALE; d->Height = ceil(LTypeFace::d->_Ascent + LTypeFace::d->_Descent + 1/*hack the underscores to work*/); #if 0 if (PrintCtx) { LgiTrace("LFont::Create %s,%f (%i,%i,%i) (%.1f,%.1f,%i)\n", Gtk::pango_font_description_get_family(d->hFont), (double)Gtk::pango_font_description_get_size(d->hFont) / PANGO_SCALE, Gtk::pango_font_metrics_get_ascent(m), Gtk::pango_font_metrics_get_descent(m), PANGO_SCALE, LTypeFace::d->_Ascent, LTypeFace::d->_Descent, d->Height); } #endif Gtk::pango_font_metrics_unref(m); #if 1 Gtk::PangoFont *fnt = pango_font_map_load_font(Gtk::pango_cairo_font_map_get_default(), EffectiveCtx, d->hFont); if (fnt) { Gtk::PangoCoverage *c = Gtk::pango_font_get_coverage(fnt, Gtk::pango_language_get_default()); if (c) { uint Bytes = (MAX_UNICODE + 1) >> 3; if ((d->GlyphMap = new uchar[Bytes])) { memset(d->GlyphMap, 0, Bytes); int Bits = Bytes << 3; for (int i=0; iGlyphMap[i>>3] |= 1 << (i & 0x7); } } Gtk::pango_coverage_unref(c); } g_object_unref(fnt); } #endif return true; } } #elif defined MAC Destroy(); if (this == LSysFont) LgiTrace("%s:%i - WARNING: you are re-creating the system font... this is bad!!!!\n", _FL); if (Face()) { if (d->Attributes) CFRelease(d->Attributes); auto Sz = Size(); LString sFamily(Face()); LString sBold("Bold"); LArray key; LArray values; key.Add(kCTFontFamilyNameAttribute); values.Add(sFamily.CreateStringRef()); if (!values[0]) return false; if (Bold()) { key.Add(kCTFontStyleNameAttribute); values.Add(sBold.CreateStringRef()); } CFDictionaryRef FontAttrD = CFDictionaryCreate( kCFAllocatorDefault, (const void**)key.AddressOf(), (const void**)values.AddressOf(), key.Length(), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); if (FontAttrD) { CTFontDescriptorRef descriptor = CTFontDescriptorCreateWithAttributes(FontAttrD); if (descriptor) { float PtSz = 0.0; if (Sz.Type == LCss::LenPt) PtSz = Sz.Value; else if (Sz.Type == LCss::LenPx) // This seems to give fonts that are too small: // PtSz = Sz.Value * 72.0f / LScreenDpi(); PtSz = Sz.Value; else LAssert(!"Impl me."); d->hFont = CTFontCreateWithFontDescriptor(descriptor, PtSz, NULL); CFRelease(descriptor); } else LAssert(0); CFRelease(FontAttrD); } else { LAssert(0); return false; } for (size_t i=0; ihFont) { LTypeFace::d->_Ascent = CTFontGetAscent(d->hFont); LTypeFace::d->_Descent = CTFontGetDescent(d->hFont); LTypeFace::d->_Leading = CTFontGetLeading(d->hFont); d->Height = ceil(LTypeFace::d->_Ascent + LTypeFace::d->_Descent + LTypeFace::d->_Leading); #if 0 if (Sz.Type == LCss::LenPx) { LStringPipe p; Sz.ToString(p); LgiTrace("%s:%i - LFont::Create(%s,%s) = %f,%f,%f (%i)\n", _FL, - Face(), p.NewGStr().Get(), + Face(), p.NewLStr().Get(), LTypeFace::d->_Ascent, LTypeFace::d->_Descent, LTypeFace::d->_Leading, GetHeight()); } #endif key.Add(kCTFontAttributeName); values.Add(d->hFont); key.Add(kCTForegroundColorFromContextAttributeName); values.Add(kCFBooleanTrue); if (Underline()) { key.Add(kCTUnderlineStyleAttributeName); CTUnderlineStyle u = kCTUnderlineStyleSingle; values.Add(CFNumberCreate(NULL, kCFNumberSInt32Type, &u)); } d->Attributes = CFDictionaryCreate( kCFAllocatorDefault, (const void**)key.AddressOf(), (const void**)values.AddressOf(), key.Length(), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); return true; } for (int i=2; ihFont = new BFont(); d->hFont->SetSize(PointSize()); status_t r = d->hFont->SetFamilyAndStyle(Face(), "Regular"); // printf("SetFamilyAndFace(%s)=%i\n", Face(), r); if (r == B_OK) { font_height height; d->hFont->GetHeight(&height); d->Height = height.ascent + height.descent + height.leading; LTypeFace::d->_Ascent = height.ascent; LTypeFace::d->_Descent = height.descent; LTypeFace::d->_Leading = height.leading; return true; } #endif return false; } char16 *LFont::_ToUnicode(char *In, ssize_t &Len) { char16 *WStr = 0; if (In && Len > 0) { WStr = (char16*)LNewConvertCp(LGI_WideCharset, In, LTypeFace::d->_CodePage, Len); if (WStr) { ssize_t l = StrlenW(WStr); if (l < Len) { Len = l; } } } return WStr; } #if defined WINNATIVE bool LFont::Create(LFontType *LogFont, LSurface *pSurface) { if (d->hFont) { DeleteObject(d->hFont); d->hFont = 0; } if (LogFont) { // set props PointSize(WinHeightToPoint(LogFont->Info.lfHeight)); LString uFace = LogFont->Info.lfFaceName; if (ValidStr(uFace)) { Face(uFace); Quality(LogFont->Info.lfQuality); Bold(LogFont->Info.lfWeight >= FW_BOLD); Italic(LogFont->Info.lfItalic != FALSE); Underline(LogFont->Info.lfUnderline != FALSE); // create the handle Create(0, 0, pSurface); } } return (d->hFont != 0); } #else bool LFont::Create(LFontType *LogFont, LSurface *pSurface) { if (LogFont) { LCss::Len Sz(LCss::LenPt, (float)LogFont->GetPointSize()); return Create(LogFont->GetFace(), Sz, pSurface); } return false; } #endif LFont &LFont::operator =(const LFont &f) { Face(f.Face()); Size(f.Size()); TabSize(f.TabSize()); Quality(f.Quality()); Fore(f.Fore()); Back(f.Back()); SetWeight(f.GetWeight()); Italic(f.Italic()); Underline(f.Underline()); Transparent(f.Transparent()); return *this; } char16 WinSymbolToUnicode[256] = { /* 0 to 15 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 16 to 31 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 32 to 47 */ 32, 9998, 9986, 9985, 0, 0, 0, 0, 9742, 9990, 9993, 9993, 0, 0, 0, 0, /* 48 to 63 */ 0, 0, 0, 0, 0, 0, 8987, 9000, 0, 0, 0, 0, 0, 0, 9991, 9997, /* 64 to 79 */ 9997, 9996, 0, 0, 0, 9756, 9758, 9757, 9759, 0, 9786, 9786, 9785, 0, 9760, 0, /* 80 to 95 */ 0, 9992, 9788, 0, 10052, 10014, 10014, 10013, 10016, 10017, 9770, 9775, 2384, 9784, 9800, 9801, /* 96 to 111 */ 9802, 9803, 9804, 9805, 9806, 9807, 9808, 9809, 9810, 9811, 38, 38, 9679, 10061, 9632, 9633, /* 112 to 127 */ 9633, 10065, 10066, 9674, 9674, 9670, 10070, 9670, 8999, 9043, 8984, 10048, 10047, 10077, 10078, 0, /* 128 to 143 */ 9450, 9312, 9313, 9314, 9315, 9316, 9317, 9318, 9319, 9320, 9321, 0, 10102, 10103, 10104, 10105, /* 144 to 159 */ 10106, 10107, 10108, 10109, 10110, 10111, 10087, 9753, 9753, 10087, 10087, 9753, 9753, 10087, 8226, 9679, /* 160 to 175 */ 160, 9675, 9675, 9675, 9737, 9737, 10061, 9642, 9633, 0, 10022, 9733, 10038, 10039, 10040, 10037, /* 176 to 191 */ 0, 0, 10023, 0, 65533, 10026, 10032, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 192 to 207 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10086, 10086, 10086, /* 208 to 223 */ 10086, 10086, 10086, 10086, 10086, 9003, 8998, 0, 10146, 0, 0, 0, 10162, 0, 0, 0, /* 224 to 239 */ 0, 0, 0, 0, 0, 0, 0, 0, 10132, 0, 0, 0, 0, 0, 0, 8678, /* 240 to 255 */ 8680, 8679, 8681, 8660, 8661, 8662, 8663, 8665, 8664, 0, 0, 10007, 10003, 9746, 9745, 0, }; LAutoString LFont::ConvertToUnicode(char16 *Input, ssize_t Len) { LAutoString a; if (LTypeFace::d->IsSymbol) { // F***ing wingdings. if (Input) { LStringPipe p(256); if (Len < 0) Len = StrlenW(Input); char16 *c = Input, *e = Input + Len; while (c < e) { if (*c < 256 && WinSymbolToUnicode[*c]) { p.Write(WinSymbolToUnicode + *c, sizeof(char16)); } else { p.Write(c, sizeof(char16)); } c++; } LAutoWString w(p.NewStrW()); a.Reset(WideToUtf8(w)); } } else { // Normal utf-8 text... a.Reset(WideToUtf8(Input, Len)); } return a; } #if WINNATIVE #include "lgi/common/FontSelect.h" #include "lgi/common/GdiLeak.h" ///////////////////////////////////////////////////////////////////////////// void LFont::_Measure(int &x, int &y, OsChar *Str, int Len) { if (!Handle()) { x = 0; y = 0; return; } HDC hDC = GetSurface() ? GetSurface()->Handle() : CreateCompatibleDC(0); HFONT hOldFont = (HFONT) SelectObject(hDC, Handle()); SIZE Size; if (GetTextExtentPoint32W(hDC, Str, Len, &Size)) { x = Size.cx; y = Size.cy; } else { x = y = 0; } SelectObject(hDC, hOldFont); if (!GetSurface()) DeleteDC(hDC); } int LFont::_CharAt(int x, OsChar *Str, int Len, LPxToIndexType Type) { if (!Handle()) return -1; INT Fit = 0; HDC hDC = CreateCompatibleDC(GetSurface()?GetSurface()->Handle():0); HFONT hOldFont = (HFONT) SelectObject(hDC, Handle()); if (hOldFont) { SIZE Size = {0, 0}; if (!GetTextExtentExPointW(hDC, Str, Len, x, &Fit, 0, &Size)) { DWORD e = GetLastError(); Fit = -1; } else if (Type == LgiNearest && Fit < Len) { // Check if the next char is nearer... SIZE Prev, Next; if (GetTextExtentPoint32W(hDC, Str, Fit, &Prev) && GetTextExtentPoint32W(hDC, Str, Fit + 1, &Next)) { int PrevDist = abs(Prev.cx - x); int NextDist = abs(Next.cx - x); if (NextDist <= PrevDist) Fit++; } } SelectObject(hDC, hOldFont); } else { DWORD e = GetLastError(); Fit = -1; LAssert(0); } DeleteDC(hDC); return Fit; } void LFont::_Draw(LSurface *pDC, int x, int y, OsChar *Str, int Len, LRect *r, LColour &fore) { if (!Handle()) return; HDC hDC = pDC->StartDC(); HFONT hOldFont = (HFONT) SelectObject(hDC, Handle()); if (hOldFont) { bool IsTransparent = Transparent(); SetTextColor(hDC, fore.GetNative()); if (!IsTransparent) SetBkColor(hDC, Back().GetNative()); SetBkMode(hDC, IsTransparent ? TRANSPARENT : OPAQUE); SIZE Size; if ((!IsTransparent && !r) || (GetOwnerUnderline())) { GetTextExtentPoint32W(hDC, Str, Len, &Size); } if (Transparent() && !r) { TextOutW(hDC, x, y, Str, Len); } else { RECT rc; if (r) rc = *r; else { rc.left = x; rc.top = y; rc.right = x + Size.cx; rc.top = y + Size.cy; } /* Debugging stuff... POINT _ori; auto res = GetWindowOrgEx(hDC, &_ori); RECT _rc; int res2 = GetClipBox(hDC, &_rc); auto Addr = (*pDC)[y - _ori.y + 6] + ((x - _ori.x + 4) * pDC->GetBits() / 8); */ ExtTextOutW(hDC, x, y, ETO_CLIPPED | (Transparent()?0:ETO_OPAQUE), &rc, Str, Len, 0); } if (GetOwnerUnderline()) { pDC->Colour(fore); pDC->Line(x, y + GetHeight() - 1, x + Size.cx + 1, y + GetHeight() - 1); } HANDLE h = SelectObject(hDC, hOldFont); } else { DWORD e = GetLastError(); LAssert(0); } pDC->EndDC(); } #elif defined(HAIKU) void LFont::_Measure(int &x, int &y, OsChar *Str, int Len) { printf("%s:%i - _Measure not impl.\n", _FL); } int LFont::_CharAt(int x, OsChar *Str, int Len, LPxToIndexType Type) { if (!d->hFont) return -1; BString s(Str, Len); d->hFont->TruncateString(&s, B_TRUNCATE_BEGINNING, x); return s.CountChars(); } void LFont::_Draw(LSurface *pDC, int x, int y, OsChar *Str, int Len, LRect *r, LColour &fore) { printf("%s:%i - _Draw not impl.\n", _FL); } #else void LFont::_Measure(int &x, int &y, OsChar *Str, int Len) { } int LFont::_CharAt(int x, OsChar *Str, int Len, LPxToIndexType Type) { return -1; } void LFont::_Draw(LSurface *pDC, int x, int y, OsChar *Str, int Len, LRect *r, LColour &fore) { } #endif diff --git a/src/common/General/Containers.cpp b/src/common/General/Containers.cpp --- a/src/common/General/Containers.cpp +++ b/src/common/General/Containers.cpp @@ -1,852 +1,852 @@ /** * \file * \author Matthew Allen * \date 27/7/1995 * \brief Container classes.\n Copyright (C) 1995-2003, Matthew Allen */ #ifdef LINUX #define _ISOC99_SOURCE 1 #include #endif #include #include #include #include #include "lgi/common/Mem.h" #include "lgi/common/Containers.h" #include "lgi/common/LgiString.h" #include "lgi/common/LgiCommon.h" ///////////////////////////////////////////////////////////////////////////////////////////////// int ACmp(LString *a, LString *b) { return stricmp(*a, *b); } int LCmp(char *a, char *b, NativeInt d) { return stricmp(a, b); } void UnitTest_CreateList(LArray &a, List &l, int sz = 100) { a.Empty(); for (int i=0; i &a, List &l) { if (a.Length() != l.Length()) return UnitTest_Err("Wrong size."); int n = 0; for (auto s : l) { LString t = s; if (t.Equals(a[n])) ;//printf("%i: %s\n", n, s); else return UnitTest_Err("Wrong value."); n++; } return true; } #define CHECK(val) if (!(val)) return false bool UnitTest_ListClass() { LArray a; List l; // Create test.. UnitTest_CreateList(a, l, 100); CHECK(UnitTest_Check(a, l)); // Delete tests.. l.Delete(a[3].Get()); a.DeleteAt(3, true); CHECK(UnitTest_Check(a, l)); while (a.Length() > 60) { a.DeleteAt(60, true); l.DeleteAt(60); } CHECK(UnitTest_Check(a, l)); // Sort test.. a.Sort([](auto a, auto b) { return stricmp(*a, *b); }); l.Sort(LCmp); CHECK(UnitTest_Check(a, l)); return true; } //////////////////////////////////////////////////////////////////////////////////////// #define Q_TRACE(...) if (_debug) LgiTrace(__VA_ARGS__) LMemQueue::LMemQueue(size_t prealloc) { PreAlloc = prealloc; } LMemQueue::~LMemQueue() { Empty(); } void LMemQueue::Block::Free() { free(this); } LMemQueue &LMemQueue::operator=(LMemQueue &p) { Empty(); PreAlloc = p.PreAlloc; for (auto b: p.Mem) { int Alloc = sizeof(Block) + b->Size; Alloc = LGI_ALLOC_ALIGN(Alloc); Block *n = (Block*) malloc(Alloc); if (n) { memcpy(n, b, sizeof(Block) + b->Size); Mem.Insert(n); } else break; } return *this; } void LMemQueue::Empty() { for (auto b: Mem) { Q_TRACE("Empty() b=%p\n", b); b->Free(); } Mem.Empty(); } int64 LMemQueue::GetSize() { int64 Size = 0; for (auto b: Mem) Size += b->Length(); return Size; } int64 LMemQueue::Peek(uchar *Ptr, ssize_t Size) { int64 Status = 0; if (Ptr && Size <= GetSize()) { for (auto b: Mem) { if (Size <= 0) break; auto Copy = MIN(Size, b->Length()); if (Copy > 0) { memcpy(Ptr, b->Start(), Copy); Ptr += Copy; Size -= Copy; Status += Copy; } } } return Status; } void *LMemQueue::New(ssize_t AddBytes) { auto Len = GetSize(); uchar *Data = Len > 0 ? new uchar[Len + AddBytes] : NULL; if (Data) { ssize_t Rd = Read(Data, Len); if (Rd >= 0 && AddBytes) { memset(Data + Len, 0, AddBytes); } } return Data; } ssize_t LMemQueue::Read(void *Ptr, ssize_t Size, int Flags) { ssize_t Status = 0; if (Ptr && Size > 0) { for (auto b: Mem) { if (Size <= 0) break; auto Copy = MIN(Size, b->Length()); // Q_TRACE("Read, copy=%i\n", (int)Copy); if (Copy > 0) { memcpy(Ptr, b->Start(), Copy); ((uchar*&)Ptr) += Copy; Size -= Copy; b->Next += (int)Copy; Status += Copy; } } while (Mem.Length() > 0) { auto it = Mem.begin(); auto b = *it; if (b->Next < b->Used) break; Mem.Delete(b); // Q_TRACE("Read, free=%p\n", b); b->Free(); } } return Status; } ssize_t LMemQueue::Write(const void *_Ptr, ssize_t Size, int Flags) { ssize_t Status = 0; auto Ptr = (const uint8_t *)_Ptr; if (!Ptr || Size <= 0) return Status; // Is there any space in the last block? auto it = Mem.rbegin(); Block *Last = *it; if (Last) { auto Len = MIN(Size, Last->Size - Last->Used); Q_TRACE("Write, last.len=%i\n", (int)Len); if (Len > 0) { memcpy(Last->Ptr() + Last->Used, Ptr, Len); Last->Used += (int)Len; LAssert(Last->Used <= Last->Size); Size -= Len; Ptr += Len; Status += Len; } } if (Size > 0) { ssize_t Bytes = MAX(PreAlloc, Size); ssize_t Alloc = sizeof(Block) + Bytes; Alloc = LGI_ALLOC_ALIGN(Alloc); Block *b = (Block*) malloc(Alloc); if (b) { b->Size = (int)Bytes; b->Used = (int)Size; b->Next = 0; Q_TRACE("Write, ptr=%p size=%i\n", b, (int)Size); memcpy(b->Ptr(), Ptr, Size); Mem.Insert(b); Status += Size; } } return Status; } ssize_t LMemQueue::Find(LString str, bool caseSensitive) { if (!str.Get()) return -1; if (!caseSensitive) str = str.Lower(); ssize_t pos = 0; char *s = str.Get(); ssize_t si = 0; for (auto b: Mem) { auto p = b->Ptr() + b->Next; auto e = b->Ptr() + b->Used; while (p < e) { if ( (caseSensitive && (s[si] == *p)) || (!caseSensitive && (s[si] == ToLower(*p))) ) { si++; if (si == str.Length()) return pos - si + 1; } else si = 0; p++; pos++; } } return -1; } void LMemQueue::Iterate(std::function callback, bool reverse) { if (!callback) { LAssert(!"No callback."); return; } if (reverse) { for (auto it = Mem.rbegin(); it >= Mem.begin(); --it) { auto b = (*it); if (!callback(b->Start(), b->Length())) break; } } else { for (auto b: Mem) { Q_TRACE("Iter b=%p, blk:%p,%i,%i,%i, start=%p, len=%i\n", b, b->Ptr(), b->Next, b->Used, b->Size, b->Start(), b->Length()); if (!callback(b->Start(), b->Length())) break; } } } LMemQueue::Buffer LMemQueue::GetBuffer() { Buffer b(this); if (Mem.Length()) { // Check if last block has space left: auto it = Mem.rbegin(); b.blk = *it; if (b.blk->Used < b.blk->Size) { b.ptr = b.blk->Ptr() + b.blk->Used; b.len = b.blk->Size - b.blk->Used; #ifdef _DEBUG memset(b.ptr, 0xcd, b.len); #endif return b; } } // Otherwise allocate a block auto sz = PreAlloc > 0 ? PreAlloc : 1024/* some reasonable default? */; auto alloc = sizeof(Block) + sz; alloc = LGI_ALLOC_ALIGN(alloc); b.blk = (Block*) malloc(alloc); if (b.blk) { b.blk->Next = 0; b.blk->Used = 0; b.blk->Size = (int)sz; b.ptr = b.blk->Ptr(); b.len = sz; #ifdef _DEBUG memset(b.ptr, 0xcd, b.len); #endif Mem.Insert(b.blk); } return b; } bool LMemQueue::Buffer::Commit(size_t bytes) { if (!blk || !ptr) { LAssert(!"Invalid param."); return false; } if (blk->Used + bytes > blk->Size) { LAssert(!"Wrote too much data?"); blk->Used = blk->Size; return false; } blk->Used += (int)bytes; return true; } ////////////////////////////////////////////////////////////////////////// -LString LStringPipe::NewGStr() +LString LStringPipe::NewLStr() { LString s; int64 Sz = GetSize(); if (Sz > 0) { if (s.Length(Sz)) { ssize_t Rd = Read(s.Get(), s.Length()); if (Rd > 0 && Rd <= Sz) { s.Get()[Rd] = 0; } } else s.Empty(); } return s; } ssize_t LStringPipe::LineChars() { ssize_t Len = -1; for (auto m: Mem) { uint8_t *p = m->Ptr(); for (int i = m->Next; i < m->Used; i++) { Len++; if (p[i] == '\n') return Len; } } return Len; } ssize_t LStringPipe::SaveToBuffer(char *Start, ssize_t BufSize, ssize_t Chars) { char *Str = Start; char *End = Str + BufSize; // Not including NULL auto it = Mem.begin(); Block *m = *it; while (m) { for ( char *MPtr = (char*)m->Ptr(); m->Next < m->Used; m->Next++) { if (Str < End) *Str++ = MPtr[m->Next]; if (MPtr[m->Next] == '\n') { m->Next++; goto EndPop; } } if (m->Next >= m->Used) { Mem.Delete(it); Q_TRACE("SaveToBuffer, free=%p\n", m); m->Free(); it = Mem.begin(); m = *it; } } EndPop: *Str = 0; return Str - Start; } ssize_t LStringPipe::Pop(LArray &Buf) { ssize_t Chars = LineChars(); if (Chars < 0) return 0; Chars++; // For the '\n' if ((ssize_t)Buf.Length() < Chars + 1) if (!Buf.Length(Chars + 1)) return -1; SaveToBuffer(Buf.AddressOf(), Chars, Chars); return Chars; } LString LStringPipe::Pop() { LString s; ssize_t Chars = LineChars(); if (Chars < 0) return s; s.Length(Chars); SaveToBuffer(s.Get(), Chars, Chars); return s; } ssize_t LStringPipe::Pop(char *Str, ssize_t BufSize) { if (!Str) return 0; ssize_t Chars = LineChars(); if (Chars < 0) return 0; return SaveToBuffer(Str, BufSize-1 /* for the NULL */, Chars); } ssize_t LStringPipe::Push(const char *Str, ssize_t Chars) { if (!Str) return 0; if (Chars < 0) Chars = strlen(Str); return Write((void*)Str, Chars); } ssize_t LStringPipe::Push(const char16 *Str, ssize_t Chars) { if (!Str) return 0; if (Chars < 0) Chars = StrlenW(Str); return Write((void*)Str, Chars * sizeof(char16)); } #ifdef _DEBUG bool LStringPipe::UnitTest() { char Buf[16]; memset(Buf, 0x1, sizeof(Buf)); LStringPipe p(8); const char s[] = "1234567890abc\n" "abcdefghijklmn\n"; p.Write(s, sizeof(s)-1); ssize_t rd = p.Pop(Buf, 10); int cmp = memcmp(Buf, "123456789\x00\x01\x01\x01\x01\x01\x01", 16); if (cmp) return false; rd = p.Pop(Buf, 10); cmp = memcmp(Buf, "abcdefghi\x00\x01\x01\x01\x01\x01\x01", 16); if (cmp) return false; p.Empty(); p.Write(s, sizeof(s)-1); LString r; r = p.Pop(); if (!r.Equals("1234567890abc")) return false; r = p.Pop(); if (!r.Equals("abcdefghijklmn")) return false; return true; } #endif ////////////////////////////////////////////////////////////////////////////////////////////////// LMemFile::LMemFile(int BlkSize) { CurPos = 0; Blocks = 0; BlockSize = BlkSize < 16 ? 16 : BlkSize; ZeroObj(Local); } LMemFile::~LMemFile() { Empty(); } LMemFile::Block *LMemFile::Get(int Index) { if (Blocks == 0 || Index < 0) return NULL; if (Index < LMEMFILE_BLOCKS) return Local[Index]; if (Index - LMEMFILE_BLOCKS >= Extra.Length()) { LAssert(!"Index beyond last block"); return NULL; } return Extra[Index - LMEMFILE_BLOCKS]; } LMemFile::Block *LMemFile::Create() { int Alloc = sizeof(Block) + BlockSize - 1; Alloc = LGI_ALLOC_ALIGN(Alloc); Block *b = (Block*) malloc(Alloc); if (!b) return NULL; b->Used = 0; b->Offset = Blocks * BlockSize; if (Blocks < LMEMFILE_BLOCKS) Local[Blocks] = b; else Extra.Add(b); Blocks++; return b; } void LMemFile::Empty() { CurPos = 0; for (int i=0; iUsed; } bool LMemFile::FreeBlock(Block *b) { if (!b) return false; ssize_t Idx = b->Offset / BlockSize; if (Idx < LMEMFILE_BLOCKS) { // Local block if (Local[Idx] != b || Idx < Blocks-1) { LAssert(!"Block index error."); return false; } free(b); Local[Idx] = NULL; Blocks--; return true; } // Extra block ssize_t Off = Idx - LMEMFILE_BLOCKS; if (Off != Extra.Length() - 1) { LAssert(!"Block index error."); return false; } free(b); Extra.DeleteAt(Off, true); Blocks--; return true; } int64 LMemFile::SetSize(int64 Size) { if (Size <= 0) { Empty(); } else { int64 CurSize = GetSize(); if (Size > CurSize) { // Increase size... int64 Diff = Size - CurSize; Block *b = GetLast(); if (b->Used < BlockSize) { // Add size to last incomplete block ssize_t Remaining = BlockSize - b->Used; ssize_t Add = MIN(Diff, Remaining); b->Used += Add; Diff -= Add; } while (Diff > 0) { // Add new blocks to cover the size... ssize_t Add = MIN(BlockSize, Diff); b = Create(); b->Used = Add; Diff -= Add; } } else { // Decrease size... uint64 Diff = CurSize - Size; while (Diff > 0 && Blocks > 0) { Block *b = GetLast(); if (!b) break; ssize_t Sub = MIN(b->Used, Diff); b->Used -= Sub; Diff -= Sub; if (b->Used == 0) FreeBlock(b); } } } return GetSize(); } int64 LMemFile::GetPos() { return CurPos; } int64 LMemFile::SetPos(int64 Pos) { if (Pos <= 0) return CurPos = 0; // Off the start of the structure ssize_t BlockIndex = Pos / BlockSize; if (BlockIndex >= Blocks) return CurPos = GetSize(); // Off the end of the structure if (BlockIndex >= 0 && BlockIndex < Blocks - 1) return CurPos = Pos; // Inside a full block Block *Last = GetLast(); uint64 Offset = Pos - Last->Offset; if (Offset >= Last->Used) return CurPos = Last->Offset + Last->Used; // End of last block return CurPos = Pos; // Inside the last block } ssize_t LMemFile::Read(void *Ptr, ssize_t Size, int Flags) { if (!Ptr || Size < 1) return 0; uint8_t *p = (uint8_t*) Ptr; uint8_t *end = p + Size; while (p < end) { int Cur = CurBlock(); if (Cur >= Blocks) break; Block *b = Get(Cur); // Where are we in the current block? ssize_t BlkOffset = CurPos - b->Offset; LAssert(b && BlkOffset >= 0 && BlkOffset <= (ssize_t)b->Used); ssize_t Remaining = b->Used - BlkOffset; if (Remaining > 0) { ssize_t Common = MIN(Remaining, end - p); memcpy(p, b->Data + BlkOffset, Common); CurPos += Common; p += Common; } else break; LAssert(p <= end); if (p >= end) // End of read buffer reached? break; // Exit loop } return p - (uint8_t*) Ptr; } ssize_t LMemFile::Write(const void *Ptr, ssize_t Size, int Flags) { if (!Ptr || Size < 1) return 0; uint8_t *p = (uint8_t*) Ptr; ssize_t len = Size; Block *b = GetLast(); if (b && b->Used < BlockSize) { // Any more space in the last block? ssize_t Remaining = BlockSize - b->Used; ssize_t Common = MIN(Remaining, Size); if (Common > 0) { memcpy(b->Data + b->Used, p, Common); p += Common; len -= Common; b->Used += Common; } } // Store remaining data into new blocks while (len > 0) { b = Create(); if (!b) break; ssize_t Common = MIN(BlockSize, len); memcpy(b->Data, p, Common); b->Used = Common; p += Common; len -= Common; } return Size - len; } diff --git a/src/common/General/DateTime.cpp b/src/common/General/DateTime.cpp --- a/src/common/General/DateTime.cpp +++ b/src/common/General/DateTime.cpp @@ -1,2187 +1,2187 @@ /* ** FILE: LDateTime.cpp ** AUTHOR: Matthew Allen ** DATE: 11/11/98 ** DESCRIPTION: Scribe Date Time Object ** ** Copyright (C) 1998, Matthew Allen ** fret@memecode.com */ #define _DEFAULT_SOURCE #include #include #include #include #include #if defined(MAC) #include #endif #ifdef WINDOWS #include #endif #include "lgi/common/Lgi.h" #include "lgi/common/DateTime.h" #include "lgi/common/DocView.h" #if !defined(WINDOWS) constexpr const char *LDateTime::WeekdaysShort[]; constexpr const char *LDateTime::WeekdaysLong[]; constexpr const char *LDateTime::MonthsShort[]; constexpr const char *LDateTime::MonthsLong[]; #define MIN_YEAR 1800 #endif ////////////////////////////////////////////////////////////////////////////// uint16 LDateTime::DefaultFormat = GDTF_DEFAULT; char LDateTime::DefaultSeparator = '/'; uint16 LDateTime::GetDefaultFormat() { if (DefaultFormat == GDTF_DEFAULT) { #ifdef WIN32 TCHAR s[80] = _T("1"); GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_IDATE, s, CountOf(s)); switch (_tstoi(s)) { case 0: DefaultFormat = GDTF_MONTH_DAY_YEAR; break; default: case 1: DefaultFormat = GDTF_DAY_MONTH_YEAR; break; case 2: DefaultFormat = GDTF_YEAR_MONTH_DAY; break; } GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_ITIME, s, sizeof(s)); if (_tstoi(s) == 1) { DefaultFormat |= GDTF_24HOUR; } else { DefaultFormat |= GDTF_12HOUR; } if (GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SDATE, s, sizeof(s))) DefaultSeparator = (char)s[0]; if (GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SSHORTDATE, s, sizeof(s))) { char Sep[] = { DefaultSeparator, '/', '\\', '-', '.', 0 }; LString Str = s; auto t = Str.SplitDelimit(Sep); for (int i=0; i= low && (v) <= high) bool LDateTime::IsValid() const { return InRange(_Day, 1, 31) && InRange(_Year, 1600, 2100) && InRange(_Thousands, 0, 999) && InRange(_Month, 1, 12) && InRange(_Seconds, 0, 59) && InRange(_Minutes, 0, 59) && InRange(_Hours, 0, 23) && InRange(_Tz, -780, 780); } void LDateTime::SetTimeZone(int NewTz, bool ConvertTime) { if (ConvertTime && NewTz != _Tz) { // printf("SetTimeZone: %i\n", NewTz - _Tz); AddMinutes(NewTz - _Tz); } _Tz = NewTz; } int LDateTime::SystemTimeZone(bool ForceUpdate) { if (ForceUpdate || CurTz == NO_ZONE) { CurTz = 0; CurTzOff = 0; #ifdef MAC #ifdef LGI_COCOA NSTimeZone *timeZone = [NSTimeZone localTimeZone]; if (timeZone) { NSDate *Now = [NSDate date]; CurTz = (int) [timeZone secondsFromGMTForDate:Now] / 60; CurTzOff = [timeZone daylightSavingTimeOffsetForDate:Now] / 60; CurTz -= CurTzOff; } #elif defined LGI_CARBON CFTimeZoneRef tz = CFTimeZoneCopySystem(); CFAbsoluteTime now = CFAbsoluteTimeGetCurrent(); Boolean dst = CFTimeZoneIsDaylightSavingTime(tz, now); if (dst) { CFAbsoluteTime next = CFTimeZoneGetNextDaylightSavingTimeTransition(tz, now); CurTz = CFTimeZoneGetSecondsFromGMT(tz, next + 100) / 60; } else { CurTz = CFTimeZoneGetSecondsFromGMT(tz, now) / 60; } CurTzOff = CFTimeZoneGetDaylightSavingTimeOffset(tz, now) / 60; CFRelease(tz); #endif #elif defined(WIN32) timeb tbTime; ftime(&tbTime); CurTz = -tbTime.timezone; TIME_ZONE_INFORMATION Tzi; if (GetTimeZoneInformation(&Tzi) == TIME_ZONE_ID_DAYLIGHT) CurTzOff = -Tzi.DaylightBias; #elif defined(LINUX) || defined(HAIKU) int six_months = (365 * 24 * 60 * 60) / 2; time_t now = 0, then = 0; time (&now); then = now - six_months; tm now_tz, then_tz; tm *t = localtime_r(&now, &now_tz); if (t) { localtime_r(&then, &then_tz); CurTz = now_tz.tm_gmtoff / 60; if (now_tz.tm_isdst) { CurTzOff = (now_tz.tm_gmtoff - then_tz.tm_gmtoff) / 60; CurTz = then_tz.tm_gmtoff / 60; } else // This is not DST so there is no offset right? CurTzOff = 0; // (then_tz.tm_gmtoff - now_tz.tm_gmtoff) / 60; } else return NO_ZONE; #else #error "Impl me." #endif } return CurTz + CurTzOff; } int LDateTime::SystemTimeZoneOffset() { if (CurTz == NO_ZONE) SystemTimeZone(); return CurTzOff; } #if defined WIN32 LDateTime ConvertSysTime(SYSTEMTIME &st, int year) { LDateTime n; if (st.wYear) { n.Year(st.wYear); n.Month(st.wMonth); n.Day(st.wDay); } else { n.Year(year); n.Month(st.wMonth); // Find the 'nth' matching weekday, starting from the first day in the month n.Day(1); LDateTime c = n; for (int i=0; iCompare(b); } #elif defined(LINUX) static bool ParseValue(char *s, LAutoString &var, LAutoString &val) { if (!s) return false; char *e = strchr(s, '='); if (!e) return false; *e++ = 0; var.Reset(NewStr(s)); val.Reset(NewStr(e)); *e = '='; return var != 0 && val != 0; } #endif /* Testing code... LDateTime Start, End; LArray Info; Start.Set("1/1/2010"); End.Set("31/12/2014"); LDateTime::GetDaylightSavingsInfo(Info, Start, &End); LStringPipe p; for (int i=0; i,int> { MonthHash() { for (int i=0; i &Info, LDateTime &Start, LDateTime *End) { bool Status = false; #if defined(WIN32) TIME_ZONE_INFORMATION Tzi; auto r = GetTimeZoneInformation(&Tzi); if (r > TIME_ZONE_ID_UNKNOWN) { Info.Length(0); // Find the dates for the previous year from Start. This allows // us to cover the start of the current year. LDateTime s = ConvertSysTime(Tzi.StandardDate, Start.Year() - 1); LDateTime d = ConvertSysTime(Tzi.DaylightDate, Start.Year() - 1); // Create initial Info entry, as the last change in the previous year auto *i = &Info.New(); if (s < d) { // Year is: Daylight->Standard->Daylight LDateTime tmp = d; i->Offset = -(Tzi.Bias + Tzi.DaylightBias); tmp.AddMinutes(-i->Offset); i->UtcTimeStamp = tmp.Ts(); } else { // Year is: Standard->Daylight->Standard LDateTime tmp = s; i->Offset = -(Tzi.Bias + Tzi.StandardBias); tmp.AddMinutes(-i->Offset); i->UtcTimeStamp = tmp.Ts();; } for (auto y=Start.Year(); y<=(End?End->Year():Start.Year()); y++) { if (s < d) { // Cur year, first event: end of DST i = &Info.New(); auto tmp = ConvertSysTime(Tzi.StandardDate, y); i->Offset = -(Tzi.Bias + Tzi.StandardBias); tmp.AddMinutes(-i->Offset); i->UtcTimeStamp = tmp.Ts(); // Cur year, second event: start of DST i = &Info.New(); tmp = ConvertSysTime(Tzi.DaylightDate, y); i->Offset = -(Tzi.Bias + Tzi.DaylightBias); tmp.AddMinutes(-i->Offset); i->UtcTimeStamp = tmp.Ts(); } else { // Cur year, first event: start of DST i = &Info.New(); auto tmp = ConvertSysTime(Tzi.DaylightDate, Start.Year()); i->Offset = -(Tzi.Bias + Tzi.DaylightBias); tmp.AddMinutes(-i->Offset); i->UtcTimeStamp = tmp.Ts(); // Cur year, second event: end of DST i = &Info.New(); tmp = ConvertSysTime(Tzi.StandardDate, Start.Year()); i->Offset = -(Tzi.Bias + Tzi.StandardBias); tmp.AddMinutes(-i->Offset); i->UtcTimeStamp = tmp.Ts(); } } Status = true; } #elif defined(MAC) LDateTime Before = Start; Before.AddMonths(-6); NSTimeZone *tz = [NSTimeZone systemTimeZone]; NSDate *startDate = [[NSDate alloc] initWithTimeIntervalSince1970:(Before.Ts() / Second64Bit) - Offset1800]; for (int n=0; n<6; n++) { NSDate *next = [tz nextDaylightSavingTimeTransitionAfterDate:startDate]; auto &i = Info.New(); i.UtcTimeStamp = ([next timeIntervalSince1970] + Offset1800) * Second64Bit; i.Offset = (int)([tz secondsFromGMTForDate:[next dateByAddingTimeInterval:60]]/60); #if DEBUG_DST_INFO { LDateTime dt; dt.Set(i.UtcTimeStamp); LgiTrace("%s:%i - Ts=%s Off=%i\n", _FL, dt.Get().Get(), i.Offset); } #endif [startDate release]; startDate = next; } #elif defined(LINUX) if (!Zdump.Length()) { FILE *f = popen("zdump -v /etc/localtime", "r"); if (f) { char s[256]; size_t r; LStringPipe p(1024); while ((r = fread(s, 1, sizeof(s), f)) > 0) { p.Write(s, (int)r); } fclose(f); - LString ps = p.NewGStr(); + LString ps = p.NewLStr(); Zdump = ps.Split("\n"); } } MonthHash Lut; LDateTime Prev; int PrevOff = 0; for (auto Line: Zdump) { auto l = Line.SplitDelimit(" \t"); if (l.Length() >= 16 && l[0].Equals("/etc/localtime")) { // /etc/localtime Sat Oct 3 15:59:59 2037 UTC = Sun Oct 4 01:59:59 2037 EST isdst=0 gmtoff=36000 // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #if DEBUG_DST_INFO printf("DST: %s\n", Line); #endif LDateTime Utc; Utc.Year(l[5].Int()); auto Tm = l[4].SplitDelimit(":"); if (Tm.Length() != 3) { #if DEBUG_DST_INFO printf("%s:%i - Tm '%s' has wrong parts: %s\n", _FL, l[4], Line); #endif continue; } Utc.Hours(Tm[0].Int()); Utc.Minutes(Tm[1].Int()); Utc.Seconds(Tm[2].Int()); if (Utc.Minutes() < 0) { #if DEBUG_DST_INFO printf("%s:%i - Mins is zero: %s\n", _FL, l[4]); #endif continue; } int m = Lut.Find(l[2]); if (!m) { #if DEBUG_DST_INFO printf("%s:%i - Unknown month '%s'\n", _FL, l[2]); #endif continue; } Utc.Day(l[3].Int()); Utc.Month(m); LAutoString Var, Val; if (!ParseValue(l[14], Var, Val) || stricmp(Var, "isdst")) { #if DEBUG_DST_INFO printf("%s:%i - Unknown value for isdst\n", _FL); #endif continue; } if (!ParseValue(l[15], Var, Val) || stricmp(Var, "gmtoff")) { #if DEBUG_DST_INFO printf("%s:%i - Unknown value for isdst\n", _FL); #endif continue; } int Off = atoi(Val) / 60; if (Utc.Ts() == 0) continue; if (Prev.Year() && Prev < Start && Start < Utc) { // Emit initial entry for 'start' auto &inf = Info.New(); inf.UtcTimeStamp = Prev; inf.Offset = PrevOff; #if DEBUG_DST_INFO printf("Info: Start=%s %i\n", Prev.Get().Get(), inf.Offset); #endif } if (Utc > Start) { // Emit furthur entries for DST events between start and end. auto &inf = Info.New(); inf.UtcTimeStamp = Utc; inf.Offset = Off; #if DEBUG_DST_INFO printf("Info: Next=%s %i\n", Utc.Get().Get(), inf.Offset); #endif if (End && Utc > *End) { // printf("Utc after end: %s > %s\n", Utc.Get().Get(), End->Get().Get()); break; } } Prev = Utc; PrevOff = Off; } } Status = Info.Length() > 1; #else LAssert(!"Not implemented."); #endif return Status; } bool LDateTime::DstToLocal(LArray &Dst, LDateTime &dt) { if (dt.GetTimeZone()) { LAssert(!"Should be a UTC date."); return true; } // LgiTrace("DstToLocal: %s\n", dt.Get().Get()); LAssert(Dst.Length() > 1); // Needs to have at least 2 entries...? for (size_t i=0; i %s\n", (int)i, start.Get().Get(), end.Get().Get()); if (dt >= start && dt < end) { dt.SetTimeZone(a.Offset, true); return true; } } auto Last = Dst.Last(); LDateTime d; d.Set(Last.UtcTimeStamp); if (dt >= d && dt.Year() == d.Year()) { // If it's after the last DST change but in the same year... it's ok... // Just use the last offset. dt.SetTimeZone(Last.Offset, true); return true; } LAssert(!"No valid DST range for this date."); return false; } int LDateTime::DayOfWeek() const { int Index = 0; int Day = IsLeapYear() ? 29 : 28; switch (_Year / 100) { case 19: { Index = 3; break; } case 20: { Index = 2; break; } } // get year right int y = _Year % 100; int r = y % 12; Index = (Index + (y / 12) + r + (r / 4)) % 7; // get month right if (_Month % 2 == 0) { // even month if (_Month > 2) Day = _Month; } else { // odd month switch (_Month) { case 1: { Day = 31; if (IsLeapYear()) { Index = Index > 0 ? Index - 1 : Index + 6; } break; } case 11: case 3: { Day = 7; break; } case 5: { Day = 9; break; } case 7: { Day = 11; break; } case 9: { Day = 5; break; } } } // get day right int Diff = Index - (Day - _Day); while (Diff < 0) Diff += 7; return Diff % 7; } LDateTime LDateTime::Now() { LDateTime dt; dt.SetNow(); return dt; } LDateTime &LDateTime::SetNow() { #ifdef WIN32 SYSTEMTIME stNow; FILETIME ftNow; GetSystemTime(&stNow); SystemTimeToFileTime(&stNow, &ftNow); uint64 i64 = ((uint64)ftNow.dwHighDateTime << 32) | ftNow.dwLowDateTime; Set(i64); #else time_t now; time(&now); struct tm *time = localtime(&now); if (time) *this = time; #ifndef LGI_STATIC else { LgiTrace("%s:%i - Error: localtime failed, now=%u\n", _FL, now); } #endif #endif return *this; } #define Convert24HrTo12Hr(h) ( (h) == 0 ? 12 : (h) > 12 ? (h) % 12 : (h) ) #define Convert24HrToAmPm(h) ( (h) >= 12 ? "p" : "a" ) LString LDateTime::GetDate() const { char s[32]; int Ch = GetDate(s, sizeof(s)); return LString(s, Ch); } int LDateTime::GetDate(char *Str, size_t SLen) const { int Ch = 0; if (Str && SLen > 0) { switch (_Format & GDTF_DATE_MASK) { case GDTF_MONTH_DAY_YEAR: Ch += sprintf_s(Str+Ch, SLen-Ch, _Format&GDTF_MONTH_LEADINGZ?"%2.2i" :"%i" , _Month); Ch += sprintf_s(Str+Ch, SLen-Ch, _Format&GDTF_DAY_LEADINGZ ?"%c%2.2i":"%c%i", DefaultSeparator, _Day); Ch += sprintf_s(Str+Ch, SLen-Ch, "%c%i", DefaultSeparator, _Year); break; default: case GDTF_DAY_MONTH_YEAR: Ch += sprintf_s(Str+Ch, SLen-Ch, _Format&GDTF_DAY_LEADINGZ ?"%2.2i" :"%i" , _Day); Ch += sprintf_s(Str+Ch, SLen-Ch, _Format&GDTF_MONTH_LEADINGZ?"%c%2.2i":"%c%i", DefaultSeparator, _Month); Ch += sprintf_s(Str+Ch, SLen-Ch, "%c%i", DefaultSeparator, _Year); break; case GDTF_YEAR_MONTH_DAY: Ch += sprintf_s(Str+Ch, SLen-Ch, "%i", _Year); Ch += sprintf_s(Str+Ch, SLen-Ch, _Format&GDTF_MONTH_LEADINGZ?"%c%2.2i":"%c%i", DefaultSeparator, _Month); Ch += sprintf_s(Str+Ch, SLen-Ch, _Format&GDTF_DAY_LEADINGZ ?"%c%2.2i":"%c%i", DefaultSeparator, _Day); break; } } return Ch; } LString LDateTime::GetTime() const { char s[32]; int Ch = GetTime(s, sizeof(s)); return LString(s, Ch); } int LDateTime::GetTime(char *Str, size_t SLen) const { int Ch = 0; if (Str && SLen > 0) { switch (_Format & GDTF_TIME_MASK) { case GDTF_12HOUR: default: { Ch += sprintf_s(Str, SLen, "%i:%2.2i:%2.2i%s", Convert24HrTo12Hr(_Hours), _Minutes, _Seconds, Convert24HrToAmPm(_Hours)); break; } case GDTF_24HOUR: { Ch += sprintf_s(Str, SLen, "%i:%2.2i:%2.2i", _Hours, _Minutes, _Seconds); break; } } } return Ch; } uint64 LDateTime::Ts() const { uint64 ts = 0; Get(ts); return ts; } bool LDateTime::SetUnix(uint64 s) { #if defined(WINDOWS) return Set(s * LDateTime::Second64Bit + 116445168000000000LL); #else return Set((s + Offset1800) * LDateTime::Second64Bit); #endif } bool LDateTime::Set(uint64 s) { #if defined WIN32 FILETIME Utc; SYSTEMTIME System; // Adjust to the desired timezone uint64 u = s + ((int64)_Tz * 60 * Second64Bit); Utc.dwHighDateTime = u >> 32; Utc.dwLowDateTime = u & 0xffffffff; if (!FileTimeToSystemTime(&Utc, &System)) return false; _Year = System.wYear; _Month = System.wMonth; _Day = System.wDay; _Hours = System.wHour; _Minutes = System.wMinute; _Seconds = System.wSecond; _Thousands = System.wMilliseconds; return true; #else time_t t = (time_t) (((int64)(s / Second64Bit)) - Offset1800); Set(t); _Thousands = s % Second64Bit; return true; #endif } bool LDateTime::Set(time_t tt) { struct tm *t; #if !defined(_MSC_VER) || _MSC_VER < _MSC_VER_VS2005 if (_Tz) tt += _Tz * 60; t = gmtime(&tt); if (t) #else struct tm tmp; if (_localtime64_s(t = &tmp, &tt) == 0) #endif { _Year = t->tm_year + 1900; _Month = t->tm_mon + 1; _Day = t->tm_mday; _Hours = t->tm_hour; _Minutes = t->tm_min; _Seconds = t->tm_sec; _Thousands = 0; // _Tz = SystemTimeZone(); return true; } return false; } uint64_t LDateTime::OsTime() const { #ifdef WINDOWS FILETIME Utc; SYSTEMTIME System; System.wYear = _Year; System.wMonth = limit(_Month, 1, 12); System.wDay = limit(_Day, 1, 31); System.wHour = limit(_Hours, 0, 23); System.wMinute = limit(_Minutes, 0, 59); System.wSecond = limit(_Seconds, 0, 59); System.wMilliseconds = limit(_Thousands, 0, 999); System.wDayOfWeek = DayOfWeek(); if (SystemTimeToFileTime(&System, &Utc)) { uint64_t s = ((uint64_t)Utc.dwHighDateTime << 32) | Utc.dwLowDateTime; if (_Tz) // Adjust for timezone s -= (int64)_Tz * 60 * Second64Bit; return s; } else { DWORD Err = GetLastError(); LAssert(!"SystemTimeToFileTime failed."); } #else if (_Year < MIN_YEAR) return 0; struct tm t; ZeroObj(t); t.tm_year = _Year - 1900; t.tm_mon = _Month - 1; t.tm_mday = _Day; t.tm_hour = _Hours; t.tm_min = _Minutes; t.tm_sec = _Seconds; t.tm_isdst = -1; time_t sec = timegm(&t); if (sec == -1) return 0; if (_Tz) { // Adjust the output to UTC from the current timezone. sec -= _Tz * 60; } return sec; #endif return 0; } bool LDateTime::OsTime(uint64_t ts) { return Set((time_t)ts); } bool LDateTime::Get(uint64 &s) const { #ifdef WINDOWS if (!IsValid()) { LAssert(!"Needs a valid date."); return false; } s = OsTime(); if (!s) return false; return true; #else if (_Year < MIN_YEAR) return false; auto sec = OsTime(); s = (uint64)(sec + Offset1800) * Second64Bit + _Thousands; return true; #endif } LString LDateTime::Get() const { char buf[32]; int Ch = GetDate(buf, sizeof(buf)); buf[Ch++] = ' '; Ch += GetTime(buf+Ch, sizeof(buf)-Ch); return LString(buf, Ch); } void LDateTime::Get(char *Str, size_t SLen) const { if (Str) { GetDate(Str, SLen); size_t len = strlen(Str); if (len < SLen - 1) { Str[len++] = ' '; GetTime(Str+len, SLen-len); } } } bool LDateTime::Set(const char *Str) { if (!Str) return false; char Local[256]; strcpy_s(Local, sizeof(Local), Str); char *Sep = strchr(Local, ' '); if (Sep) { *Sep++ = 0; if (!SetTime(Sep)) return false; } if (!SetDate(Local)) return false; return true; } void LDateTime::Month(char *m) { int i = IsMonth(m); if (i >= 0) _Month = i + 1; } int DateComponent(const char *s) { int64 i = Atoi(s); return i ? (int)i : LDateTime::IsMonth(s); } bool LDateTime::SetDate(const char *Str) { bool Status = false; if (Str) { auto T = LString(Str).SplitDelimit("/-.,_\\"); if (T.Length() == 3) { int i[3] = { DateComponent(T[0]), DateComponent(T[1]), DateComponent(T[2]) }; int fmt = _Format & GDTF_DATE_MASK; // Do some guessing / overrides. // Don't let _Format define the format completely. if (i[0] > 1000) { fmt = GDTF_YEAR_MONTH_DAY; } else if (i[2] > 1000) { if (i[0] > 12) fmt = GDTF_DAY_MONTH_YEAR; else if (i[1] > 12) fmt = GDTF_MONTH_DAY_YEAR; } switch (fmt) { case GDTF_MONTH_DAY_YEAR: { _Month = i[0]; _Day = i[1]; _Year = i[2]; break; } case GDTF_DAY_MONTH_YEAR: { _Day = i[0]; _Month = i[1]; _Year = i[2]; break; } case GDTF_YEAR_MONTH_DAY: { _Year = i[0]; _Month = i[1]; _Day = i[2]; break; } default: { _Year = i[2]; if ((DefaultFormat & GDTF_DATE_MASK) == GDTF_MONTH_DAY_YEAR) { // Assume m/d/yyyy _Day = i[1]; _Month = i[0]; } else { // Who knows??? // Assume d/m/yyyy _Day = i[0]; _Month = i[1]; } break; } } if (_Year < 100) { LAssert(_Day < 1000 && _Month < 1000); if (_Year >= 80) _Year += 1900; else _Year += 2000; } Status = true; } else { // Fall back to fuzzy matching auto T = LString(Str).SplitDelimit(" ,"); MonthHash Lut; int FMonth = 0; int FDay = 0; int FYear = 0; for (unsigned i=0; i 0) { if (i >= 1000) { FYear = i; } else if (i < 32) { FDay = i; } } } else { int i = Lut.Find(p); if (i) FMonth = i; } } if (FMonth && FDay) { Day(FDay); Month(FMonth); } if (FYear) { Year(FYear); } else { LDateTime Now; Now.SetNow(); Year(Now.Year()); } } } return Status; } bool LDateTime::SetTime(const char *Str) { if (!Str) return false; auto T = LString(Str).SplitDelimit(":."); if (T.Length() < 2 || T.Length() > 4) return false; _Hours = (int)T[0].Int(); _Minutes = (int)T[1].Int(); if (_Hours < 0 || _Minutes < 0) return false; char *s = T[2]; if (s) _Seconds = atoi(s); else _Seconds = 0; _Thousands = 0; s = T.Last(); if (s) { if (strchr(s, 'p') || strchr(s, 'P')) { if (_Hours != 12) _Hours += 12; } else if (strchr(s, 'a') || strchr(s, 'A')) { if (_Hours == 12) _Hours -= 12; } } if (T.Length() > 3) { LString t = "0."; t += s; _Thousands = (int) (t.Float() * 1000); } return true; } int LDateTime::IsWeekDay(const char *s) { for (unsigned n=0; n= 4) { Year((int)t[0].Int()); Month((int)t[1].Int()); Day((int)t[2].Int()); } else if (t[2].Length() >= 4) { Day((int)t[0].Int()); Month((int)t[1].Int()); Year((int)t[2].Int()); } else { LAssert(!"Unknown date format?"); return false; } } } else if (a[i].Length() == 4) Year((int)a[i].Int()); else if (!Day()) Day((int)a[i].Int()); } else if (IsAlpha(*c)) { int WkDay = IsWeekDay(c); if (WkDay >= 0) continue; int Mnth = IsMonth(c); if (Mnth >= 0) Month(Mnth + 1); } else if (*c == '-' || *c == '+') { c++; if (strlen(c) == 4) { // Timezone.. int64 Tz = a[i].Int(); int Hrs = (int) (Tz / 100); int Min = (int) (Tz % 100); SetTimeZone(Hrs * 60 + Min, false); } } } return IsValid(); } int LDateTime::Sizeof() { return sizeof(int) * 7; } bool LDateTime::Serialize(LFile &f, bool Write) { int32 i; if (Write) { #define wf(fld) i = fld; f << i; wf(_Day); wf(_Month); wf(_Year); wf(_Thousands); wf(_Seconds); wf(_Minutes); wf(_Hours); } else { #define rf(fld) f >> i; fld = i; rf(_Day); rf(_Month); rf(_Year); rf(_Thousands); rf(_Seconds); rf(_Minutes); rf(_Hours); } return true; } /* bool LDateTime::Serialize(ObjProperties *Props, char *Name, bool Write) { #ifndef LGI_STATIC if (Props && Name) { struct _Date { uint8_t Day; uint8_t Month; int16_t Year; uint8_t Hour; uint8_t Minute; uint16_t ThouSec; }; LAssert(sizeof(_Date) == 8); if (Write) { _Date d; d.Day = _Day; d.Month = _Month; d.Year = _Year; d.Hour = _Hours; d.Minute = _Minutes; d.ThouSec = (_Seconds * 1000) + _Thousands; return Props->Set(Name, &d, sizeof(d)); } else // Read { void *Ptr; int Len; if (Props->Get(Name, Ptr, Len) && sizeof(_Date) == Len) { _Date *d = (_Date*) Ptr; _Day = d->Day; _Month = d->Month; _Year = d->Year; _Hours = d->Hour; _Minutes = d->Minute; _Seconds = d->ThouSec / 1000; _Thousands = d->ThouSec % 1000; return true; } } } #endif return false; } */ int LDateTime::Compare(const LDateTime *Date) const { // this - *Date auto ThisTs = IsValid() ? Ts() : 0; auto DateTs = Date->IsValid() ? Date->Ts() : 0; // If these ever fire, the cast to int64_t will overflow LAssert((ThisTs & 0x800000000000000) == 0); LAssert((DateTs & 0x800000000000000) == 0); int64_t Diff = (int64_t)ThisTs - DateTs; if (Diff < 0) return -1; return Diff > 0 ? 1 : 0; } #define DATETIME_OP(op) \ bool LDateTime::operator op(const LDateTime &dt) const \ { \ auto a = Ts(); \ auto b = dt.Ts(); \ return a op b; \ } DATETIME_OP(<) DATETIME_OP(<=) DATETIME_OP(>) DATETIME_OP(>=) bool LDateTime::operator ==(const LDateTime &dt) const { return _Year == dt._Year && _Month == dt._Month && _Day == dt._Day && _Hours == dt._Hours && _Minutes == dt._Minutes && _Seconds == dt._Seconds && _Thousands == dt._Thousands; } bool LDateTime::operator !=(const LDateTime &dt) const { return _Year != dt._Year || _Month != dt._Month || _Day != dt._Day || _Hours != dt._Hours || _Minutes != dt._Minutes || _Seconds != dt._Seconds || _Thousands != dt._Thousands; } int LDateTime::DiffMonths(const LDateTime &dt) { int a = (Year() * 12) + Month(); int b = (dt.Year() * 12) + dt.Month(); return b - a; } LDateTime LDateTime::operator -(const LDateTime &dt) { uint64 a, b; Get(a); dt.Get(b); /// Resolution of a second when using 64 bit timestamps int64 Sec = Second64Bit; int64 Min = 60 * Sec; int64 Hr = 60 * Min; int64 Day = 24 * Hr; int64 d = (int64)a - (int64)b; LDateTime r; r._Day = (int16) (d / Day); d -= r._Day * Day; r._Hours = (int16) (d / Hr); d -= r._Hours * Hr; r._Minutes = (int16) (d / Min); d -= r._Minutes * Min; r._Seconds = (int16) (d / Sec); #ifdef WIN32 d -= r._Seconds * Sec; r._Thousands = (int16) (d / 10000); #else r._Thousands = 0; #endif return r; } LDateTime LDateTime::operator +(const LDateTime &dt) { LDateTime s = *this; s.AddMonths(dt.Month()); s.AddDays(dt.Day()); s.AddHours(dt.Hours()); s.AddMinutes(dt.Minutes()); // s.AddSeconds(dt.Seconds()); return s; } LDateTime &LDateTime::operator =(const LDateTime &t) { _Day = t._Day; _Year = t._Year; _Thousands = t._Thousands; _Month = t._Month; _Seconds = t._Seconds; _Minutes = t._Minutes; _Hours = t._Hours; _Tz = t._Tz; _Format = t._Format; return *this; } LDateTime &LDateTime::operator =(struct tm *time) { if (time) { _Seconds = time->tm_sec; _Minutes = time->tm_min; _Hours = time->tm_hour; _Day = time->tm_mday; _Month = time->tm_mon + 1; _Year = time->tm_year + 1900; } else Empty(); return *this; } bool LDateTime::IsSameDay(LDateTime &d) const { return Day() == d.Day() && Month() == d.Month() && Year() == d.Year(); } bool LDateTime::IsSameMonth(LDateTime &d) const { return Day() == d.Day() && Month() == d.Month(); } bool LDateTime::IsSameYear(LDateTime &d) const { return Year() == d.Year(); } bool LDateTime::IsLeapYear(int Year) const { if (Year < 0) Year = _Year; if (Year % 4 != 0) { return false; } if (Year % 400 == 0) { return true; } if (Year % 100 == 0) { return false; } return true; } int LDateTime::DaysInMonth() const { if (_Month == 2 && IsLeapYear()) { return 29; } short DaysInMonth[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; return _Month >= 1 && _Month <= 12 ? DaysInMonth[_Month-1] : 0; } #define MinutesInDay (60*24) void LDateTime::AddSeconds(int64 Seconds) { uint64 i; if (Get(i)) { i += Seconds * Second64Bit; Set(i); } } void LDateTime::AddMinutes(int64 Minutes) { uint64 i; if (Get(i)) { int64 delta = Minutes * 60 * Second64Bit; uint64 n = i + delta; // printf("AddMin " LPrintfInt64 " + " LPrintfInt64 " = " LPrintfInt64 "\n", i, delta, n); Set(n); #if 0 uint64 i2; Get(i2); int64 diff = (int64)i2-(int64)i; #endif } } void LDateTime::AddHours(int64 Hours) { uint64 i; if (Get(i)) { i += Hours * LDateTime::HourLength * Second64Bit; Set(i); } } bool LDateTime::AddDays(int64 Days) { if (!Days) return true; uint64 Ts; if (!Get(Ts)) return false; Ts += Days * LDateTime::DayLength * Second64Bit; bool b = Set(Ts); return b; } void LDateTime::AddMonths(int64 Months) { int64 m = _Month + Months; do { if (m < 1) { _Year--; m += 12; } else if (m > 12) { _Year++; m -= 12; } else { break; } } while (1); _Month = (int16) m; if (_Day > DaysInMonth()) _Day = DaysInMonth(); } LString LDateTime::DescribePeriod(double seconds) { int mins = (int) (seconds / 60); seconds -= mins * 60; int hrs = mins / 60; mins -= hrs * 60; int days = hrs / 24; hrs -= days * 24; LString s; if (days > 0) s.Printf("%id %ih %im %is", days, hrs, mins, (int)seconds); else if (hrs > 0) s.Printf("%ih %im %is", hrs, mins, (int)seconds); else if (mins > 0) s.Printf("%im %is", mins, (int)seconds); else s.Printf("%is", (int)seconds); return s; } LString LDateTime::DescribePeriod(LDateTime to) { auto ThisTs = Ts(); auto ToTs = to.Ts(); auto diff = ThisTs < ToTs ? ToTs - ThisTs : ThisTs - ToTs; auto seconds = (double)diff / LDateTime::Second64Bit; return DescribePeriod(seconds); } int LDateTime::MonthFromName(const char *Name) { if (Name) { for (int m=0; m<12; m++) { if (strnicmp(Name, MonthsShort[m], strlen(MonthsShort[m])) == 0) { return m + 1; break; } } } return -1; } bool LDateTime::Decode(const char *In) { // Test data: // // Tue, 6 Dec 2005 1:25:32 -0800 Empty(); if (!In) { LAssert(0); return false; } bool Status = false; // Tokenize delimited by whitespace LString::Array T = LString(In).SplitDelimit(", \t\r\n"); if (T.Length() < 2) { if (T[0].IsNumeric()) { // Some sort of timestamp? uint64_t Ts = Atoi(T[0].Get()); if (Ts > 0) { return SetUnix(Ts); } else return false; } else { // What now? return false; } } else { bool GotDate = false; for (unsigned i=0; i 31) { // Y/M/D? Year((int)Date[0].Int()); Day((int)Date[2].Int()); } else if (Date[2].Int() > 31) { // D/M/Y? Day((int)Date[0].Int()); Year((int)Date[2].Int()); } else { // Ambiguous year... bool YrFirst = true; if (Date[0].Length() == 1) YrFirst = false; // else we really can't tell.. just go with year first if (YrFirst) { Year((int)Date[0].Int()); Day((int)Date[2].Int()); } else { Day((int)Date[0].Int()); Year((int)Date[2].Int()); } LDateTime Now; Now.SetNow(); if (Year() + 2000 <= Now.Year()) Year(2000 + Year()); else Year(1900 + Year()); } if (Date[1].IsNumeric()) Month((int)Date[1].Int()); else { int m = MonthFromName(Date[1]); if (m > 0) Month(m); } GotDate = true; Status = true; } else if (s.Find(":") >= 0) { // whole time // Do some validation bool Valid = true; for (char *c = s; *c && Valid; c++) { if (!(IsDigit(*c) || *c == ':')) Valid = false; } if (Valid) { LString::Array Time = s.Split(":"); if (Time.Length() == 2 || Time.Length() == 3) { // Hour int i = (int) Time[0].Int(); if (i >= 0) Hours(i); if (s.Lower().Find("p") >= 0) { if (Hours() < 12) Hours(Hours() + 12); } // Minute i = (int) Time[1].Int(); if (i >= 0) Minutes(i); if (Time.Length() == 3) { // Second i = (int) Time[2].Int(); if (i >= 0) Seconds(i); } Status = true; } } } else if (IsAlpha(s(0))) { // text int m = MonthFromName(s); if (m > 0) Month(m); } else if (strchr("+-", *s)) { // timezone DoTimeZone: LDateTime Now; double OurTmz = (double)Now.SystemTimeZone() / 60; if (s && strchr("-+", *s) && strlen(s) == 5) { #if 1 int i = atoi(s); int hr = i / 100; int min = i % 100; SetTimeZone(hr * 60 + min, false); #else // adjust for timezone char Buf[32]; memcpy(Buf, s, 3); Buf[3] = 0; double TheirTmz = atof(Buf); memcpy(Buf+1, s + 3, 2); TheirTmz += (atof(Buf) / 60); if (Tz) { *Tz = TheirTmz; } double AdjustHours = OurTmz - TheirTmz; AddMinutes((int) (AdjustHours * 60)); #endif } else { // assume GMT AddMinutes((int) (OurTmz * 60)); } } else if (s.IsNumeric()) { int Count = 0; for (char *c = s; *c; c++) { if (!IsDigit(*c)) break; Count++; } if (Count <= 2) { if (Day()) { // We already have a day... so this might be // a 2 digit year... LDateTime Now; Now.SetNow(); int Yr = atoi(s); if (2000 + Yr <= Now.Year()) Year(2000 + Yr); else Year(1900 + Yr); } else { // A day number (hopefully)? Day((int)s.Int()); } } else if (Count == 4) { if (!Year()) { // A year! Year((int)s.Int()); Status = true; } else { goto DoTimeZone; } // My one and only Y2K fix // d.Year((Yr < 100) ? (Yr > 50) ? 1900+Yr : 2000+Yr : Yr); } } } } return Status; } bool LDateTime::GetVariant(const char *Name, LVariant &Dst, char *Array) { LDomProperty p = LStringToDomProp(Name); switch (p) { case DateYear: // Type: Int32 Dst = Year(); break; case DateMonth: // Type: Int32 Dst = Month(); break; case DateDay: // Type: Int32 Dst = Day(); break; case DateHour: // Type: Int32 Dst = Hours(); break; case DateMinute: // Type: Int32 Dst = Minutes(); break; case DateSecond: // Type: Int32 Dst = Seconds(); break; case DateDate: // Type: String { char s[32]; GetDate(s, sizeof(s)); Dst = s; break; } case DateTime: // Type: String { char s[32]; GetTime(s, sizeof(s)); Dst = s; break; } case TypeString: // Type: String case DateDateAndTime: // Type: String { char s[32]; Get(s, sizeof(s)); Dst = s; break; } case TypeInt: // Type: Int64 case DateTimestamp: // Type: Int64 { uint64 i = 0; Get(i); Dst = (int64)i; break; } case DateSecond64Bit: { Dst = Second64Bit; break; } default: { return false; } } return true; } bool LDateTime::SetVariant(const char *Name, LVariant &Value, char *Array) { LDomProperty p = LStringToDomProp(Name); switch (p) { case DateYear: Year(Value.CastInt32()); break; case DateMonth: Month(Value.CastInt32()); break; case DateDay: Day(Value.CastInt32()); break; case DateHour: Hours(Value.CastInt32()); break; case DateMinute: Minutes(Value.CastInt32()); break; case DateSecond: Seconds(Value.CastInt32()); break; case DateDate: SetDate(Value.Str()); break; case DateTime: SetTime(Value.Str()); break; case DateDateAndTime: Set(Value.Str()); break; case DateTimestamp: Set((uint64)Value.CastInt64()); break; default: return false; } return true; } bool LDateTime::CallMethod(const char *Name, LVariant *ReturnValue, LArray &Args) { switch (LStringToDomProp(Name)) { case DateSetNow: SetNow(); if (ReturnValue) *ReturnValue = true; break; case DateSetStr: if (Args.Length() < 1) return false; bool Status; if (Args[0]->Type == GV_INT64) Status = Set((uint64) Args[0]->Value.Int64); else Status = Set(Args[0]->Str()); if (ReturnValue) *ReturnValue = Status; break; case DateGetStr: { char s[256] = ""; Get(s, sizeof(s)); if (ReturnValue) *ReturnValue = s; break; } default: return false; } return true; } #ifdef _DEBUG #define DATE_ASSERT(i) \ if (!(i)) \ { \ LAssert(!"LDateTime unit test failed."); \ return false; \ } bool LDateTime_Test() { // Check 64bit get/set LDateTime t("1/1/2017 0:0:0"); uint64 i; DATE_ASSERT(t.Get(i)); LgiTrace("Get='%s'\n", t.Get().Get()); uint64 i2 = i + (24ULL * 60 * 60 * LDateTime::Second64Bit); LDateTime t2; t2.SetFormat(GDTF_DAY_MONTH_YEAR); t2.Set(i2); LString s = t2.Get(); LgiTrace("Set='%s'\n", s.Get()); DATE_ASSERT(!stricmp(s, "2/1/2017 12:00:00a") || !stricmp(s, "2/01/2017 12:00:00a")); t.SetNow(); LgiTrace("Now.Local=%s Tz=%.2f\n", t.Get().Get(), t.GetTimeZoneHours()); t2 = t; t2.ToUtc(); LgiTrace("Now.Utc=%s Tz=%.2f\n", t2.Get().Get(), t2.GetTimeZoneHours()); t2.ToLocal(); LgiTrace("Now.Local=%s Tz=%.2f\n", t2.Get().Get(), t2.GetTimeZoneHours()); DATE_ASSERT(t == t2); return true; } #endif diff --git a/src/common/General/SoftwareUpdate.cpp b/src/common/General/SoftwareUpdate.cpp --- a/src/common/General/SoftwareUpdate.cpp +++ b/src/common/General/SoftwareUpdate.cpp @@ -1,454 +1,454 @@ #include "lgi/common/Lgi.h" #include "lgi/common/SoftwareUpdate.h" #include "lgi/common/Net.h" #include "lgi/common/Http.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/TextLabel.h" #include "lgi/common/Button.h" #include "lgi/common/LgiRes.h" #include "lgi/common/Thread.h" static char sHttpDownloadFailed[] = "HTTP download failed."; static char sSocketConnectFailed[] = "Socket connect failed."; static char sNoUpdateUri[] = "No update URI."; static char sXmlParsingFailed[] = "XML parsing failed."; static char sUnexpectedXml[] = "Unexpected XML."; static char sUpdateError[] = "Update script error: %s"; struct LSoftwareUpdatePriv { LString Name; LString UpdateUri; LString Proxy; LString Error; LString TempPath; void SetError(int Id, const char *Def = 0) { Error = LLoadString(Id, Def); } class UpdateThread : public LThread { LSoftwareUpdatePriv *d; LSocket *s; LSoftwareUpdate::UpdateInfo *Info; LHttp Http; bool IncBetas; public: bool Status; UpdateThread(LSoftwareUpdatePriv *priv, LSoftwareUpdate::UpdateInfo *info, bool betas) : LThread("SoftwareUpdateThread") { Info = info; d = priv; s = 0; IncBetas = betas; Status = false; Run(); } ~UpdateThread() { LAssert(IsExited()); } void Cancel() { Http.Close(); Info->Cancel = true; } int Main() { if (d->UpdateUri) { LUri Uri(d->UpdateUri); char Dir[256]; int WordSize = sizeof(size_t) << 3; LString OsName = LGetOsName(); int Os = LGetOs(); if (Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64 || Os == LGI_OS_WIN9X) { OsName.Printf("Win%i", WordSize); } sprintf_s(Dir, sizeof(Dir), "%s?name=%s&os=%s&betas=%i", Uri.sPath.Get(), (char*)d->Name, OsName.Get(), IncBetas); Uri.sPath = Dir; LString GetUri = Uri.ToString(); #ifdef _DEBUG LgiTrace("UpdateURI=%s\n", GetUri.Get()); #endif if (d->Proxy) { LUri Proxy(d->Proxy); if (Proxy.sHost) Http.SetProxy(Proxy.sHost, Proxy.Port?Proxy.Port:HTTP_PORT); } LStringPipe RawXml; int ProtocolStatus = 0; LAutoPtr s(new LSocket); if (Http.Open(s, Uri.sHost, Uri.Port)) { LHttp::ContentEncoding Enc; if (Http.Get(GetUri, NULL, &ProtocolStatus, &RawXml, &Enc)) { - auto Xml = RawXml.NewGStr(); + auto Xml = RawXml.NewLStr(); LMemStream XmlStream(Xml.Get(), Xml.Length(), false); LXmlTree Tree; LXmlTag Root; if (Tree.Read(&Root, &XmlStream)) { LXmlTag *StatusCode; if (Root.IsTag("software") && (StatusCode = Root.GetChildTag("status"))) { if (StatusCode->GetContent() && atoi(StatusCode->GetContent()) > 0) { LXmlTag *t; if ((t = Root.GetChildTag("version"))) Info->Version = t->GetContent(); if ((t = Root.GetChildTag("revision"))) Info->Build = t->GetContent(); if ((t = Root.GetChildTag("uri"))) Info->Uri = t->GetContent(); if ((t = Root.GetChildTag("date"))) { Info->Date.SetFormat(GDTF_YEAR_MONTH_DAY); Info->Date.Set(t->GetContent()); } Status = true; } else { LXmlTag *Msg = Root.GetChildTag("msg"); LStringPipe p; p.Print(LLoadString(L_ERROR_UPDATE, sUpdateError), Msg?Msg->GetContent():(char*)"Unknown"); - d->Error = p.NewGStr(); + d->Error = p.NewLStr(); LgiTrace("UpdateURI=%s\n", GetUri.Get()); } } else { d->SetError(L_ERROR_UNEXPECTED_XML, sUnexpectedXml); LgiTrace("%s:%i - Bad XML: %s\n", _FL, Xml.Get()); } } else { d->SetError(L_ERROR_XML_PARSE, sXmlParsingFailed); LgiTrace("%s:%i - Bad XML: %s\n", _FL, Xml.Get()); } } else { d->SetError(L_ERROR_HTTP_FAILED, sHttpDownloadFailed); LgiTrace("%s:%i - Bad URI: %s\n", _FL, GetUri.Get()); } } else { d->SetError(L_ERROR_CONNECT_FAILED, sSocketConnectFailed); LgiTrace("%s:%i - Bad connect: %s:%i\n", _FL, Uri.sHost.Get(), Uri.Port); } } else d->SetError(L_ERROR_NO_URI, sNoUpdateUri); return 0; } }; class Spinner : public LDialog { UpdateThread *Watch; public: Spinner(LViewI *par, UpdateThread *watch) { Watch = watch; SetParent(par); LRect r(0, 0, 330, 400); SetPos(r); Name("Software Update"); LRect c = GetClient(); LTextLabel *t = new LTextLabel( -1, 10, 10, c.X()-20, -1, LLoadString(L_SOFTUP_CHECKING, "Checking for software update...")); if (t) { AddView(t); LButton *btn = new LButton( IDCANCEL, (c.X()-70)/2, t->GetPos().y2 + 10, 70, -1, LLoadString(L_BTN_CANCEL, "Cancel")); if (btn) { AddView(btn); r.y2 = (r.Y()-c.Y()) + 30 + t->Y() + btn->Y(); SetPos(r); MoveToCenter(); } } } void OnCreate() { SetPulse(100); } void OnPulse() { if (Watch->IsExited()) { EndModal(0); } } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDCANCEL: { Watch->Cancel(); break; } } return LDialog::OnNotify(c, n); } }; class UpdateDownload : public LThread, public LProxyStream { const LSoftwareUpdate::UpdateInfo *Info; LUri *Uri; LUri *Proxy; LStream *Local; LString *Err; int *Status; public: int64 Progress, Total; UpdateDownload( const LSoftwareUpdate::UpdateInfo *info, LUri *uri, LUri *proxy, LStream *local, LString *err, int *status) : LThread("UpdateDownload"), LProxyStream(local) { Info = info; Uri = uri; Proxy = proxy; Local = local; Err = err; Status = status; Progress = Total = 0; Run(); } int64 SetSize(int64 Size) override { Total = Size; return s->SetSize(Size); } ssize_t Write(const void *b, ssize_t l, int f = 0) override { auto ret = s->Write(b, l, f); Progress = s->GetPos(); return ret; } int Main() override { LHttp Http; if (Proxy->sHost) Http.SetProxy(Proxy->sHost, Proxy->Port?Proxy->Port:HTTP_PORT); LAutoPtr s(new LSocket); LHttp::ContentEncoding Enc; if (!Http.Open(s, Uri->sHost, Uri->Port)) { *Err = LLoadString(L_ERROR_CONNECT_FAILED, sSocketConnectFailed); } else if (!Http.Get(Info->Uri, 0, Status, this, &Enc)) { *Err = LLoadString(L_ERROR_HTTP_FAILED, sHttpDownloadFailed); } return 0; } }; }; LSoftwareUpdate::LSoftwareUpdate(const char *SoftwareName, const char *UpdateUri, const char *ProxyUri, const char *OptionalTempPath) { d = new LSoftwareUpdatePriv; d->Name = SoftwareName; d->UpdateUri = UpdateUri; d->Proxy = ProxyUri; d->TempPath = OptionalTempPath; } LSoftwareUpdate::~LSoftwareUpdate() { DeleteObj(d); } void LSoftwareUpdate::CheckForUpdate(UpdateInfo &Info, LViewI *WithUi, bool IncBetas, std::function callback) { LSoftwareUpdatePriv::UpdateThread Update(d, &Info, IncBetas); if (WithUi) { auto s = new LSoftwareUpdatePriv::Spinner(WithUi, &Update); s->DoModal(NULL); } else { while (!Update.IsExited()) { LYield(); LSleep(10); } } if (callback) callback(Update.Status, GetErrorMessage()); } bool LSoftwareUpdate::ApplyUpdate(const UpdateInfo &Info, bool DownloadOnly, LViewI *WithUi) { if (!Info.Uri) { d->SetError(L_ERROR_NO_URI, sNoUpdateUri); return false; } LUri Uri(Info.Uri); if (!Uri.sPath) { d->SetError(L_ERROR_URI_ERROR, "No path in URI."); return false; } char *File = strrchr(Uri.sPath, '/'); if (!File) File = Uri.sPath; else File++; char Tmp[MAX_PATH_LEN]; if (d->TempPath) LMakePath(Tmp, sizeof(Tmp), d->TempPath, File); else LMakePath(Tmp, sizeof(Tmp), LGetSystemPath(LSP_TEMP), File); LFile Local; if (!Local.Open(Tmp, O_WRITE)) { d->SetError(L_ERROR_OPENING_TEMP_FILE, "Can't open local temp file."); return false; } Local.SetSize(0); LProgressDlg *Dlg = new LProgressDlg; Dlg->SetDescription(LLoadString(L_SOFTUP_DOWNLOADING, "Downloading...")); Dlg->SetType("KiB"); Dlg->SetScale(1.0 / 1024.0); int HttpStatus = 0; int64 Size = 0; LUri Proxy(d->Proxy); LSoftwareUpdatePriv::UpdateDownload Thread(&Info, &Uri, &Proxy, &Local, &d->Error, &HttpStatus); while (!Thread.IsExited()) { LYield(); LSleep(50); if (!Size) { if (Thread.Total) Dlg->SetRange(Size = Thread.Total); } else { if (Thread.Progress > Dlg->Value()) Dlg->Value(Thread.Progress); } } Local.Close(); Dlg->EndModeless(); if (HttpStatus != 200) { FileDev->Delete(Tmp); d->SetError(L_ERROR_HTTP_FAILED, sHttpDownloadFailed); return false; } if (!DownloadOnly) { char *Ext = LGetExtension(Tmp); if (Ext) { if (!_stricmp(Ext, "exe")) { // Execute to install... LExecute(Tmp); return true; } else { // Calculate the local path... char Path[MAX_PATH_LEN]; LMakePath(Path, sizeof(Path), LGetExePath(), File); if (!_stricmp(Ext, "dll")) { // Copy to local folder... LError Err; if (!FileDev->Copy(Tmp, Path, &Err)) { d->SetError(L_ERROR_COPY_FAILED, "Failed to copy file from temp folder to local folder."); } } else if (!_stricmp(Ext, "gz")) { // Unpack to local folder... } // Cleanup FileDev->Delete(Tmp); } } } return false; } const char *LSoftwareUpdate::GetErrorMessage() { return d->Error; } diff --git a/src/common/Lgi/About.cpp b/src/common/Lgi/About.cpp --- a/src/common/Lgi/About.cpp +++ b/src/common/Lgi/About.cpp @@ -1,118 +1,118 @@ #include #include #include "lgi/common/Lgi.h" #include "lgi/common/About.h" #include "lgi/common/DocView.h" #include "lgi/common/Bitmap.h" #include "lgi/common/Button.h" #include "lgi/common/Css.h" #include "lgi/common/TableLayout.h" ////////////////////////////////////////////////////////////////////////////// enum Ctrls { IDC_WEB = 100, IDC_TABLE, IDC_BMP, IDC_MESSAGE, }; LAbout::LAbout( LView *parent, const char *AppName, const char *Ver, const char *Text, const char *AboutGraphic, const char *Url, const char *Email) { SetParent(parent); if (!AppName) AppName = "Application"; LString n; n.Printf("About %s", AppName); Name(n); #ifdef _DEBUG const char *Build = "Debug"; #else const char *Build = "Release"; #endif LStringPipe p; const char *OsName = LGetOsName(); #if defined(_WIN64) OsName = "Win64"; #elif defined(WIN32) OsName = "Win32"; #endif p.Print("%s v%s (%s %s)\n", AppName, Ver, OsName, Build); p.Print("Build: %s, %s\n", __DATE__, __TIME__); #ifdef __GTK_H__ p.Print("GTK v%i.%i.%i\n", GTK_MAJOR_VERSION, GTK_MINOR_VERSION, GTK_MICRO_VERSION); #endif p.Print("\n"); if (Url) p.Print("Homepage:\n\t%s\n", Url); if (Email) p.Print("Email:\n\t%s\n", Email); if (Text) p.Write((char*)Text, strlen(Text)); LColour cBack(L_MED); LTableLayout *Tbl = new LTableLayout(IDC_TABLE); AddView(Tbl); Tbl->GetCss(true)->Padding("0.5em"); int x = 0; if (AboutGraphic) { LString FileName = LFindFile(AboutGraphic); if (FileName) { auto c = Tbl->GetCell(x++, 0, true); auto Img = new LBitmap(IDC_BMP, 0, 0, FileName, true); c->Add(Img); Img->GetCss(true)->BackgroundColor(cBack); } } LView *Ctrl = LViewFactory::Create("LTextView3"); if (Ctrl) { auto c = Tbl->GetCell(x++, 0, true); c->Add(Ctrl); Ctrl->SetId(IDC_MESSAGE); - Ctrl->Name(p.NewGStr()); + Ctrl->Name(p.NewLStr()); Ctrl->GetCss(true)->BackgroundColor(cBack); Ctrl->SetFont(LSysFont); } auto c = Tbl->GetCell(0, 1, true, x); c->TextAlign(LCss::AlignRight); c->Add(new LButton(IDOK, 0, 0, -1, -1, "Ok")); LRect r(0, 0, 400, 260); SetPos(r); MoveSameScreen(parent); } int LAbout::OnNotify(LViewI *Ctrl, LNotification n) { if (!Ctrl) return 0; switch (Ctrl->GetId()) { case IDC_BMP: { break; } case IDOK: { EndModal(0); break; } } return 0; } diff --git a/src/common/Lgi/DragAndDropCommon.cpp b/src/common/Lgi/DragAndDropCommon.cpp --- a/src/common/Lgi/DragAndDropCommon.cpp +++ b/src/common/Lgi/DragAndDropCommon.cpp @@ -1,72 +1,72 @@ #include "lgi/common/Lgi.h" #include "lgi/common/DragAndDrop.h" /////////////////////////////////////////////////////////////////////////////////////////////// LDragFormats::LDragFormats(bool source) { Source = source; } LString LDragFormats::ToString() { LStringPipe p(256); p.Print("{"); for (auto &f: Formats) p.Print("%s,", f.Get()); p.Print("}"); - return p.NewGStr(); + return p.NewLStr(); } void LDragFormats::SupportsFileDrops() { #ifdef MAC Supports("NSFilenamesPboardType"); #endif Supports(LGI_FileDropFormat); } void LDragFormats::SupportsFileStreams() { Supports(LGI_StreamDropFormat); #ifdef WINDOWS Supports(CFSTR_FILECONTENTS); #endif } bool LDragFormats::HasFormat(const char *Fmt) { for (auto f: Formats) if (f.Equals(Fmt)) return true; return false; } void LDragFormats::Supports(LString Fmt) { if (Source) { // Drag sources just add the formats to the list if (!HasFormat(Fmt)) Formats.New().Set(Fmt); } else { // Drag targets mark the supported formats for (auto &f: Formats) { if (f.Equals(Fmt)) f.Val = true; } } } LString::Array LDragFormats::GetSupported() { LString::Array a; a.SetFixedLength(false); for (auto &f: Formats) if (f.Val) a.Add(f); return a; } diff --git a/src/common/Lgi/LgiCommon.cpp b/src/common/Lgi/LgiCommon.cpp --- a/src/common/Lgi/LgiCommon.cpp +++ b/src/common/Lgi/LgiCommon.cpp @@ -1,2841 +1,2841 @@ // // Cross platform LGI functions // #if LGI_COCOA #import #endif #define _WIN32_WINNT 0x501 #include #include #include #include #ifdef WINDOWS #include #include "lgi/common/RegKey.h" #include #include #else #include #define _getcwd getcwd #endif #include "lgi/common/Lgi.h" #include "lgi/common/Capabilities.h" #if defined(LINUX) && !defined(LGI_SDL) #include "LgiWinManGlue.h" #elif defined(WINDOWS) #include "lgi/common/RegKey.h" #endif #if defined POSIX #include #include #include #include #include "lgi/common/SubProcess.h" #endif #ifdef HAIKU #include #include #else #include "SymLookup.h" #endif #include "lgi/common/Library.h" #include "lgi/common/Net.h" #if defined(__GTK_H__) namespace Gtk { #include "LgiWidget.h" } #endif ////////////////////////////////////////////////////////////////////////// // Misc stuff #if LGI_COCOA || defined(__GTK_H__) LString LgiArgsAppPath; #endif #if defined MAC #import #if defined LGI_CARBON bool _get_path_FSRef(FSRef &fs, LStringPipe &a) { HFSUniStr255 Name; ZeroObj(Name); FSRef Parent; FSCatalogInfo Cat; ZeroObj(Cat); OSErr e = FSGetCatalogInfo(&fs, kFSCatInfoVolume|kFSCatInfoNodeID, &Cat, &Name, NULL, &Parent); if (!e) { if (_get_path_FSRef(Parent, a)) { LAutoString u((char*)LNewConvertCp("utf-8", Name.unicode, "utf-16", Name.length * sizeof(Name.unicode[0]) )); // printf("CatInfo = '%s' %x %x\n", u.Get(), Cat.nodeID, Cat.volume); if (u && Cat.nodeID > 2) { a.Print("%s%s", DIR_STR, u.Get()); } } return true; } return false; } LAutoString FSRefPath(FSRef &fs) { LStringPipe a; if (_get_path_FSRef(fs, a)) { return LAutoString(a.NewStr()); } return LAutoString(); } #endif #endif bool LPostEvent(OsView Wnd, int Event, LMessage::Param a, LMessage::Param b) { #if LGI_SDL SDL_Event e; e.type = SDL_USEREVENT; e.user.code = Event; e.user.data1 = Wnd; e.user.data2 = a || b ? new LMessage::EventParams(a, b) : NULL; /* printf("LPostEvent Wnd=%p, Event=%i, a/b: %i/%i\n", Wnd, Event, (int)a, (int)b); */ return SDL_PushEvent(&e) == 0; #elif WINNATIVE return PostMessage(Wnd, Event, a, b) != 0; #elif defined(__GTK_H__) LAssert(Wnd); LViewI *View = (LViewI*) g_object_get_data(GtkCast(Wnd, g_object, GObject), "LViewI"); if (View) { LMessage m(0); m.Set(Event, a, b); return m.Send(View); } else printf("%s:%i - Error: LPostEvent can't cast OsView to LViewI\n", _FL); #elif defined(MAC) && !LGI_COCOA #if 0 int64 Now = LCurrentTime(); static int64 Last = 0; static int Count = 0; Count++; if (Now > Last + 1000) { printf("Sent %i events in the last %ims\n", Count, (int)(Now-Last)); Last = Now; Count = 0; } #endif EventRef Ev; OSStatus e = CreateEvent(NULL, kEventClassUser, kEventUser, 0, // EventTime kEventAttributeNone, &Ev); if (e) { printf("%s:%i - CreateEvent failed with %i\n", _FL, (int)e); } else { EventTargetRef t = GetControlEventTarget(Wnd); e = SetEventParameter(Ev, kEventParamLgiEvent, typeUInt32, sizeof(Event), &Event); if (e) printf("%s:%i - error %i\n", _FL, (int)e); e = SetEventParameter(Ev, kEventParamLgiA, typeUInt32, sizeof(a), &a); if (e) printf("%s:%i - error %i\n", _FL, (int)e); e = SetEventParameter(Ev, kEventParamLgiB, typeUInt32, sizeof(b), &b); if (e) printf("%s:%i - error %i\n", _FL, (int)e); bool Status = false; EventQueueRef q = GetMainEventQueue(); e = SetEventParameter(Ev, kEventParamPostTarget, typeEventTargetRef, sizeof(t), &t); if (e) printf("%s:%i - error %i\n", _FL, (int)e); e = PostEventToQueue(q, Ev, kEventPriorityStandard); if (e) printf("%s:%i - error %i\n", _FL, (int)e); else Status = true; // printf("PostEventToQueue %i,%i,%i -> %p\n", Event, a, b, q); ReleaseEvent(Ev); return Status; } #else LAssert(!"Not impl."); #endif return false; } void LExitApp() { exit(0); } ////////////////////////////////////////////////////////////////////////// #ifdef WIN32 bool RegisterActiveXControl(char *Dll) { LLibrary Lib(Dll); if (Lib.IsLoaded()) { #ifdef _MSC_VER typedef HRESULT (STDAPICALLTYPE *p_DllRegisterServer)(void); p_DllRegisterServer DllRegisterServer = (p_DllRegisterServer)Lib.GetAddress("DllRegisterServer"); if (DllRegisterServer) { return DllRegisterServer() == S_OK; } #else LAssert(!"Not impl."); #endif } return false; } #endif ////////////////////////////////////////////////////////////////////////// #ifdef WINDOWS #include #pragma comment(lib, "netapi32.lib") #endif /// \brief Returns the operating system that Lgi is running on. /// \sa Returns one of the defines starting with LGI_OS_UNKNOWN in LgiDefs.h int LGetOs ( /// Returns the version of the OS or NULL if you don't care LArray *Ver ) { #if defined(WIN32) || defined(WIN64) static int Os = LGI_OS_UNKNOWN; static int Version = 0, Revision = 0; if (Os == LGI_OS_UNKNOWN) { #if defined(WIN64) BOOL IsWow64 = TRUE; #elif defined(WIN32) BOOL IsWow64 = FALSE; IsWow64Process(GetCurrentProcess(), &IsWow64); #endif SERVER_INFO_101 *v = NULL; auto r = NetServerGetInfo(NULL, 101, (LPBYTE*)&v); if (r == NERR_Success) { Version = v->sv101_version_major; Revision = v->sv101_version_minor; Os = (v->sv101_version_major >= 6) ? #ifdef WIN32 (IsWow64 ? LGI_OS_WIN64 : LGI_OS_WIN32) #else LGI_OS_WIN64 #endif : LGI_OS_WIN9X; NetApiBufferFree(v); } else LAssert(0); } if (Ver) { Ver->Add(Version); Ver->Add(Revision); } return Os; #elif defined LINUX if (Ver) { utsname Buf; if (!uname(&Buf)) { auto t = LString(Buf.release).SplitDelimit("."); for (int i=0; iAdd(atoi(t[i])); } } } return LGI_OS_LINUX; #elif defined MAC #if !defined(__GTK_H__) if (Ver) { NSOperatingSystemVersion v = [[NSProcessInfo processInfo] operatingSystemVersion]; Ver->Add((int)v.majorVersion); Ver->Add((int)v.minorVersion); Ver->Add((int)v.patchVersion); } #endif return LGI_OS_MAC_OS_X; #elif defined HAIKU return LGI_OS_HAIKU; #else #error "Impl Me" return LGI_OS_UNKNOWN; #endif } const char *LGetOsName() { const char *Str[LGI_OS_MAX] = { "Unknown", "Win9x", "Win32", "Win64", "Haiku", "Linux", "MacOSX", }; auto Os = LGetOs(); if (Os > 0 && Os < CountOf(Str)) return Str[Os]; LAssert(!"Invalid OS index."); return "error"; } #ifdef WIN32 #define RecursiveFileSearch_Wildcard "*.*" #else // unix'ish OS #define RecursiveFileSearch_Wildcard "*" #endif bool LRecursiveFileSearch(const char *Root, LArray *Ext, LArray *Files, uint64 *Size, uint64 *Count, std::function Callback, LCancel *Cancel) { // validate args if (!Root) return false; // get directory enumerator LDirectory Dir; bool Status = true; // enumerate the directory contents for (auto Found = Dir.First(Root); Found && (!Cancel || !Cancel->IsCancelled()); Found = Dir.Next()) { char Name[300]; if (!Dir.Path(Name, sizeof(Name))) continue; if (Callback && !Callback(Name, &Dir)) continue; if (Dir.IsDir()) { // dir LRecursiveFileSearch( Name, Ext, Files, Size, Count, Callback, Cancel); } else { // process file bool Match = true; // if no Ext's then default to match if (Ext) { for (int i=0; iLength(); i++) { const char *e = (*Ext)[i]; char *RawFile = strrchr(Name, DIR_CHAR); if (RawFile && (Match = MatchStr(e, RawFile+1))) { break; } } } if (Match) { // file matched... process: if (Files) Files->Add(NewStr(Name)); if (Size) *Size += Dir.GetSize(); if (Count) (*Count)++; Status = true; } } } return Status; } #define LGI_TRACE_TO_FILE // #include #ifndef WIN32 #define _vsnprintf vsnprintf #endif static LStreamI *_LgiTraceStream = NULL; void LgiTraceSetStream(LStreamI *stream) { _LgiTraceStream = stream; } bool LgiTraceGetFilePath(char *LogPath, int BufLen) { if (!LogPath) return false; auto Exe = LGetExeFile(); if (Exe) { #ifdef MAC char *Dir = strrchr(Exe, DIR_CHAR); if (Dir) { char Part[256]; strcpy_s(Part, sizeof(Part), Dir+1); LMakePath(LogPath, BufLen, "~/Library/Logs", Dir+1); strcat_s(LogPath, BufLen, ".txt"); } else #endif { char *Dot = strrchr(Exe, '.'); if (Dot && !strchr(Dot, DIR_CHAR)) sprintf_s(LogPath, BufLen, "%.*s.txt", (int)(Dot - Exe.Get()), Exe.Get()); else sprintf_s(LogPath, BufLen, "%s.txt", Exe.Get()); } LFile f; if (f.Open(LogPath, O_WRITE)) { f.Close(); } else { char Leaf[64]; char *Dir = strrchr(LogPath, DIR_CHAR); if (Dir) { strcpy_s(Leaf, sizeof(Leaf), Dir + 1); LGetSystemPath(LSP_APP_ROOT, LogPath, BufLen); if (!LDirExists(LogPath)) FileDev->CreateFolder(LogPath); LMakePath(LogPath, BufLen, LogPath, Leaf); } else goto OnError; } #if 0 LFile tmp; if (tmp.Open("c:\\temp\\log.txt", O_WRITE)) { tmp.SetSize(0); tmp.Write(LogPath, strlen(LogPath)); } #endif } else { // Well what to do now? I give up OnError: strcpy_s(LogPath, BufLen, "trace.txt"); return false; } return true; } void LgiTrace(const char *Msg, ...) { #if defined _INC_MALLOC && WINNATIVE if (_heapchk() != _HEAPOK) return; #endif if (!Msg) return; #ifdef WIN32 static LMutex Sem("LgiTrace"); Sem.Lock(_FL, true); #endif char Buffer[2049] = ""; #ifdef LGI_TRACE_TO_FILE static LFile f; static char LogPath[MAX_PATH_LEN] = ""; if (!_LgiTraceStream && LogPath[0] == 0) LgiTraceGetFilePath(LogPath, sizeof(LogPath)); #endif va_list Arg; va_start(Arg, Msg); #ifdef LGI_TRACE_TO_FILE int Ch = #endif vsnprintf(Buffer, sizeof(Buffer)-1, Msg, Arg); va_end(Arg); #ifdef LGI_TRACE_TO_FILE LStreamI *Output = NULL; if (_LgiTraceStream) Output = _LgiTraceStream; else { if (!f.IsOpen() && f.Open(LogPath, O_WRITE)) f.Seek(0, SEEK_END); Output = &f; } if (Output && Ch > 0) { Output->ChangeThread(); Output->Write(Buffer, Ch); } if (!_LgiTraceStream) { #ifdef WINDOWS // Windows can take AGES to close a file when there is anti-virus on, like 100ms. // We can't afford to wait here so just keep the file open but flush the // buffers if we can. FlushFileBuffers(f.Handle()); #else f.Close(); #endif } #endif #if defined WIN32 OutputDebugStringA(Buffer); Sem.Unlock(); #else printf("%s", Buffer); #endif } #ifndef LGI_STATIC #define STACK_SIZE 12 void LStackTrace(const char *Msg, ...) { #ifndef HAIKU LSymLookup::Addr Stack[STACK_SIZE]; ZeroObj(Stack); LSymLookup *Lu = LAppInst ? LAppInst->GetSymLookup() : NULL; if (!Lu) { printf("%s:%i - Failed to get sym lookup object.\n", _FL); return; } int Frames = Lu ? Lu->BackTrace(0, 0, Stack, STACK_SIZE) : 0; if (Msg) { #ifdef LGI_TRACE_TO_FILE static LFile f; static char LogPath[MAX_PATH_LEN] = ""; if (!_LgiTraceStream) { if (LogPath[0] == 0) LgiTraceGetFilePath(LogPath, sizeof(LogPath)); f.Open(LogPath, O_WRITE); } #endif va_list Arg; va_start(Arg, Msg); char Buffer[2049] = ""; int Len = vsnprintf(Buffer, sizeof(Buffer)-1, Msg, Arg); va_end(Arg); Lu->Lookup(Buffer+Len, sizeof(Buffer)-Len-1, Stack, Frames); #ifdef LGI_TRACE_TO_FILE if (f.IsOpen()) { f.Seek(0, SEEK_END); f.Write(Buffer, (int)strlen(Buffer)); f.Close(); } #endif #if defined WIN32 OutputDebugStringA(Buffer); #else printf("Trace: %s", Buffer); #endif } #endif } #endif bool LTrimDir(char *Path) { if (!Path) return false; char *p = strrchr(Path, DIR_CHAR); if (!p) return false; if (p[1] == 0) // Trailing DIR_CHAR doesn't count... do it again. { *p = 0; p = strrchr(Path, DIR_CHAR); if (!p) return false; } *p = 0; return true; } LString LMakeRelativePath(const char *Base, const char *Path) { LStringPipe Status; if (Base && Path) { #ifdef WIN32 bool SameNs = strnicmp(Base, Path, 3) == 0; #else bool SameNs = true; #endif if (SameNs) { auto b = LString(Base + 1).SplitDelimit(":\\/"); auto p = LString(Path + 1).SplitDelimit(":\\/"); int Same = 0; while (Same < b.Length() && Same < p.Length() && stricmp(b[Same], p[Same]) == 0) { Same++; } for (int i = Same; i= b.Length()) { Status.Print(".%s", DIR_STR); } for (int n = Same; n Same) { Status.Push(DIR_STR); } Status.Push(p[n]); } } } - return Status.NewGStr(); + return Status.NewLStr(); } bool LIsRelativePath(const char *Path) { if (!Path) return false; if (*Path == '.') return true; #ifdef WIN32 if (IsAlpha(Path[0]) && Path[1] == ':') // Drive letter path return false; #endif if (*Path == DIR_CHAR) return false; if (strstr(Path, "://")) // Protocol def return false; return true; // Correct or not??? } bool LMakePath(char *Str, int StrSize, const char *Path, const char *File) { if (!Str || StrSize <= 0 || !Path || !File) { printf("%s:%i - Invalid LMakePath(%p,%i,%s,%s) param\n", _FL, Str, StrSize, Path, File); return false; } if (StrSize <= 4) { printf("%s:%i - LgiMakeFile buf size=%i?\n", _FL, StrSize); } if (Str && Path && File) { char Dir[] = { '/', '\\', 0 }; if (Path[0] == '~') { auto Parts = LString(Path).SplitDelimit(Dir, 2); char *s = Str, *e = Str + StrSize; for (auto p: Parts) { if (p.Equals("~")) { LGetSystemPath(LSP_HOME, s, e - s); s += strlen(s); } else s += sprintf_s(s, e - s, "%s%s", DIR_STR, p.Get()); } } else if (Str != Path) { strcpy_s(Str, StrSize, Path); } #define EndStr() Str[strlen(Str)-1] #define EndDir() if (!strchr(Dir, EndStr())) strcat(Str, DIR_STR); size_t Len = strlen(Str); char *End = Str + (Len ? Len - 1 : 0); if (strchr(Dir, *End) && End > Str) { *End = 0; } auto T = LString(File).SplitDelimit(Dir); for (int i=0; i= StrSize - 1) return false; Str[Len++] = DIR_CHAR; Str[Len] = 0; } size_t SegLen = strlen(T[i]); if (Len + SegLen + 1 > StrSize) { return false; } strcpy_s(Str + Len, StrSize - Len, T[i]); } } } return true; } bool LgiGetTempPath(char *Dst, int DstSize) { return LGetSystemPath(LSP_TEMP, Dst, DstSize); } bool LGetSystemPath(LSystemPath Which, char *Dst, ssize_t DstSize) { if (!Dst || DstSize <= 0) return false; LFile::Path p; LString s = p.GetSystem(Which, 0); if (!s) return false; strcpy_s(Dst, DstSize, s); return true; } LString LGetSystemPath(LSystemPath Which, int WordSize) { LFile::Path p; return p.GetSystem(Which, WordSize); } LFile::Path::State LFile::Path::Exists() { if (Length() == 0) return TypeNone; #ifdef WINDOWS struct _stat64 s; int r = _stat64(GetFull(), &s); if (r) return TypeNone; if (s.st_mode & _S_IFDIR) return TypeFolder; if (s.st_mode & _S_IFREG) return TypeFile; #else struct stat s; int r = stat(GetFull(), &s); if (r) return TypeNone; if (S_ISDIR(s.st_mode)) return TypeFolder; if (S_ISREG(s.st_mode)) return TypeFile; #endif return TypeNone; } LString LFile::Path::PrintAll() { LStringPipe p; #define _(name) \ p.Print(#name ": '%s'\n", GetSystem(name).Get()); _(LSP_OS) _(LSP_OS_LIB) _(LSP_TEMP) _(LSP_COMMON_APP_DATA) _(LSP_USER_APP_DATA) _(LSP_LOCAL_APP_DATA) _(LSP_DESKTOP) _(LSP_HOME) _(LSP_USER_APPS) _(LSP_EXE) _(LSP_TRASH) _(LSP_APP_INSTALL) _(LSP_APP_ROOT) _(LSP_USER_DOCUMENTS) _(LSP_USER_MUSIC) _(LSP_USER_VIDEO) _(LSP_USER_DOWNLOADS) _(LSP_USER_LINKS) _(LSP_USER_PICTURES) #undef _ #if LGI_COCOA int Domains[] = {NSUserDomainMask, NSSystemDomainMask}; const char *DomainName[] = {"User", "System"}; for (int i=0; i 0) \ { \ LString s = [paths objectAtIndex:0]; \ p.Print("%s." #name ": '%s'\n", DomainName[i], s.Get()); \ } \ else p.Print("%s." #name ": null\n", DomainName[i]); \ } \ } _(NSApplicationDirectory) _(NSDemoApplicationDirectory) _(NSDeveloperApplicationDirectory) _(NSAdminApplicationDirectory) _(NSLibraryDirectory) _(NSDeveloperDirectory) _(NSUserDirectory) _(NSDocumentationDirectory) _(NSDocumentDirectory) _(NSCoreServiceDirectory) _(NSAutosavedInformationDirectory) _(NSDesktopDirectory) _(NSCachesDirectory) _(NSApplicationSupportDirectory) _(NSDownloadsDirectory) _(NSInputMethodsDirectory) _(NSMoviesDirectory) _(NSMusicDirectory) _(NSPicturesDirectory) _(NSPrinterDescriptionDirectory) _(NSSharedPublicDirectory) _(NSPreferencePanesDirectory) _(NSApplicationScriptsDirectory) _(NSItemReplacementDirectory) _(NSAllApplicationsDirectory) _(NSAllLibrariesDirectory) _(NSTrashDirectory) #undef _ } #endif - return p.NewGStr(); + return p.NewLStr(); } LString LFile::Path::GetSystem(LSystemPath Which, int WordSize) { LString Path; #if defined(WIN32) #ifndef CSIDL_PROFILE #define CSIDL_PROFILE 0x0028 #endif #if !defined(CSIDL_MYDOCUMENTS) #define CSIDL_MYDOCUMENTS 0x0005 #endif #if !defined(CSIDL_MYMUSIC) #define CSIDL_MYMUSIC 0x000d #endif #if !defined(CSIDL_MYVIDEO) #define CSIDL_MYVIDEO 0x000e #endif #if !defined(CSIDL_LOCAL_APPDATA) #define CSIDL_LOCAL_APPDATA 0x001c #endif #if !defined(CSIDL_COMMON_APPDATA) #define CSIDL_COMMON_APPDATA 0x0023 #endif #if !defined(CSIDL_APPDATA) #define CSIDL_APPDATA 0x001a #endif #endif /* #if defined(LINUX) && !defined(LGI_SDL) // Ask our window manager add-on if it knows the path LLibrary *WmLib = LAppInst ? LAppInst->GetWindowManagerLib() : NULL; if (WmLib) { Proc_LgiWmGetPath WmGetPath = (Proc_LgiWmGetPath) WmLib->GetAddress("LgiWmGetPath"); char Buf[MAX_PATH_LEN]; if (WmGetPath && WmGetPath(Which, Buf, sizeof(Buf))) { Path = Buf; return Path; } } #endif */ switch (Which) { default: break; case LSP_USER_DOWNLOADS: { #if defined(__GTK_H__) auto p = Gtk::g_get_user_special_dir(Gtk::G_USER_DIRECTORY_DOWNLOAD); Path = p; #elif defined(WIN32) && defined(_MSC_VER) // OMG!!!! Really? #ifndef REFKNOWNFOLDERID typedef GUID KNOWNFOLDERID; #define REFKNOWNFOLDERID const KNOWNFOLDERID & GUID FOLDERID_Downloads = {0x374DE290,0x123F,0x4565,{0x91,0x64,0x39,0xC4,0x92,0x5E,0x46,0x7B}}; #endif LLibrary Shell("Shell32.dll"); typedef HRESULT (STDAPICALLTYPE *pSHGetKnownFolderPath)(REFKNOWNFOLDERID rfid, DWORD dwFlags, HANDLE hToken, PWSTR *ppszPath); pSHGetKnownFolderPath SHGetKnownFolderPath = (pSHGetKnownFolderPath)Shell.GetAddress("SHGetKnownFolderPath"); if (SHGetKnownFolderPath) { PWSTR ptr = NULL; HRESULT r = SHGetKnownFolderPath(FOLDERID_Downloads, 0, NULL, &ptr); if (SUCCEEDED(r)) { LAutoString u8(WideToUtf8(ptr)); if (u8) Path = u8; CoTaskMemFree(ptr); } } if (!Path.Get()) { LRegKey k(false, "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders"); char *p = k.GetStr("{374DE290-123F-4565-9164-39C4925E467B}"); if (LDirExists(p)) Path = p; } if (!Path.Get()) { LString MyDoc = WinGetSpecialFolderPath(CSIDL_MYDOCUMENTS); if (MyDoc) { char Buf[MAX_PATH_LEN]; LMakePath(Buf, sizeof(Buf), MyDoc, "Downloads"); if (LDirExists(Buf)) Path = Buf; } } if (!Path.Get()) { LString Profile = WinGetSpecialFolderPath(CSIDL_PROFILE); if (Profile) { char Buf[MAX_PATH_LEN]; LMakePath(Buf, sizeof(Buf), Profile, "Downloads"); if (LDirExists(Buf)) Path = Buf; } } #elif LGI_COCOA NSArray *paths = NSSearchPathForDirectoriesInDomains( NSDownloadsDirectory, NSUserDomainMask, YES); if (paths) { if ([paths count]) Path = [paths objectAtIndex:0]; } #elif defined(HAIKU) #else LAssert(!"Not implemented"); #endif break; } case LSP_USER_LINKS: { LString Home = LGetSystemPath(LSP_HOME); #if defined(WIN32) char p[MAX_PATH_LEN]; if (LMakePath(p, sizeof(p), Home, "Links")) Path = p; #elif defined(LINUX) char p[MAX_PATH_LEN]; if (LMakePath(p, sizeof(p), Home, ".config/gtk-3.0")) Path = p; #endif break; } case LSP_USER_PICTURES: { #if defined(__GTK_H__) auto p = Gtk::g_get_user_special_dir(Gtk::G_USER_DIRECTORY_DOCUMENTS); Path = p; #elif defined(WIN32) Path = WinGetSpecialFolderPath(CSIDL_MYPICTURES); if (Path) return Path; #elif defined LGI_COCOA NSArray *paths = NSSearchPathForDirectoriesInDomains( NSPicturesDirectory, NSUserDomainMask, YES); if (paths) { if ([paths count]) Path = [paths objectAtIndex:0]; } #endif // Default to ~/Pictures char hm[MAX_PATH_LEN]; LString Home = LGetSystemPath(LSP_HOME); if (LMakePath(hm, sizeof(hm), Home, "Pictures")) Path = hm; break; } case LSP_USER_DOCUMENTS: { char path[MAX_PATH_LEN]; #if defined(__GTK_H__) auto p = Gtk::g_get_user_special_dir(Gtk::G_USER_DIRECTORY_DOCUMENTS); if (p) Path = p; #elif defined(WIN32) && defined(_MSC_VER) Path = WinGetSpecialFolderPath(CSIDL_MYDOCUMENTS); #elif defined LGI_COCOA NSArray *paths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES); if (paths) { if ([paths count]) Path = [paths objectAtIndex:0]; } #elif defined(HAIKU) if( find_directory ( B_SYSTEM_DOCUMENTATION_DIRECTORY, dev_for_path("/boot"), true, path, sizeof(path) ) == B_OK) Path = path; #endif if (!Path) { // Default to ~/Documents if (LMakePath(path, sizeof(path), LGetSystemPath(LSP_HOME), "Documents")) Path = path; } break; } case LSP_USER_MUSIC: { char path[MAX_PATH_LEN]; #if defined WIN32 Path = WinGetSpecialFolderPath(CSIDL_MYMUSIC); #elif defined(__GTK_H__) auto p = Gtk::g_get_user_special_dir(Gtk::G_USER_DIRECTORY_MUSIC); if (p) Path = p; #elif defined LGI_CARBON FSRef Ref; OSErr e = FSFindFolder(kUserDomain, kMusicDocumentsFolderType, kDontCreateFolder, &Ref); if (e) printf("%s:%i - FSFindFolder failed e=%i\n", _FL, e); else { LAutoString a = FSRefPath(Ref); if (a) Path = a.Get(); } #elif LGI_COCOA NSArray *paths = NSSearchPathForDirectoriesInDomains( NSMusicDirectory, NSUserDomainMask, YES); if (paths) { if ([paths count]) Path = [paths objectAtIndex:0]; } #elif defined(HAIKU) if( find_directory ( B_USER_SOUNDS_DIRECTORY, dev_for_path("/boot"), true, path, sizeof(path) ) == B_OK) Path = path; #endif if (!Path) { // Default to ~/Music if (LMakePath(path, sizeof(path), LGetSystemPath(LSP_HOME), "Music")) Path = path; } break; } case LSP_USER_VIDEO: { char path[MAX_PATH_LEN]; #if defined WIN32 Path = WinGetSpecialFolderPath(CSIDL_MYVIDEO); #elif defined(__GTK_H__) auto p = Gtk::g_get_user_special_dir(Gtk::G_USER_DIRECTORY_VIDEOS); if (p) Path = p; #elif defined LGI_CARBON FSRef Ref; OSErr e = FSFindFolder(kUserDomain, kMovieDocumentsFolderType, kDontCreateFolder, &Ref); if (e) printf("%s:%i - FSFindFolder failed e=%i\n", _FL, e); else { LAutoString a = FSRefPath(Ref); if (a) Path = a.Get(); } #elif LGI_COCOA NSArray *paths = NSSearchPathForDirectoriesInDomains( NSMoviesDirectory, NSUserDomainMask, YES); if (paths) { if ([paths count]) Path = [paths objectAtIndex:0]; } #endif if (!Path) { // Default to ~/Video if (LMakePath(path, sizeof(path), LGetSystemPath(LSP_HOME), "Video")) Path = path; } break; } case LSP_USER_APPS: { #if defined WIN32 int Id = #ifdef WIN64 CSIDL_PROGRAM_FILES #else CSIDL_PROGRAM_FILESX86 #endif ; if (WordSize == 32) Id = CSIDL_PROGRAM_FILESX86; else if (WordSize == 64) Id = CSIDL_PROGRAM_FILES; Path = WinGetSpecialFolderPath(Id); #elif defined(HAIKU) char path[MAX_PATH_LEN] = ""; if (find_directory(B_USER_APPS_DIRECTORY, dev_for_path("/boot"), true, path, sizeof(path)) == B_OK) Path = path; #elif LGI_COCOA NSArray *paths = NSSearchPathForDirectoriesInDomains( NSApplicationDirectory, NSSystemDomainMask, YES); if (paths) { if ([paths count]) Path = [paths objectAtIndex:0]; } #elif defined MAC Path = "/Applications"; #elif defined LINUX Path = "/usr/bin"; #else LAssert(!"Impl me."); #endif break; } case LSP_APP_INSTALL: { Path = LGetSystemPath(LSP_EXE); if (Path) { size_t Last = Path.RFind(DIR_STR); if (Last > 0) { LString s = Path(Last, -1); if ( stristr(s, #ifdef _DEBUG "Debug" #else "Release" #endif ) ) Path = Path(0, Last); } } break; } case LSP_APP_ROOT: { #ifndef LGI_STATIC const char *Name = NULL; // Try and get the configured app name: if (LAppInst) Name = LAppInst->LBase::Name(); if (!Name) { // Use the exe name? LString Exe = LGetExeFile(); char *l = LGetLeaf(Exe); if (l) { #ifdef WIN32 char *d = strrchr(l, '.'); *d = NULL; #endif Name = l; // printf("%s:%i - name '%s'\n", _FL, Name); } } if (!Name) { LAssert(0); break; } #if defined MAC #if LGI_COCOA NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); if (paths) Path = [[paths objectAtIndex:0] UTF8String]; #elif LGI_CARBON FSRef Ref; OSErr e = FSFindFolder(kUserDomain, kDomainLibraryFolderType, kDontCreateFolder, &Ref); if (e) { printf("%s:%i - FSFindFolder failed e=%i\n", _FL, e); LAssert(0); } else { LAutoString Base = FSRefPath(Ref); Path = Base.Get(); } #else struct passwd *pw = getpwuid(getuid()); if (!pw) return false; Path.Printf("%s/Library", pw->pw_dir); #endif #elif defined WIN32 Path = WinGetSpecialFolderPath(CSIDL_APPDATA); #elif defined LINUX char Dot[128]; snprintf(Dot, sizeof(Dot), ".%s", Name); Name = Dot; struct passwd *pw = getpwuid(getuid()); if (pw) Path = pw->pw_dir; else LAssert(0); #elif defined(HAIKU) dev_t volume = dev_for_path("/boot"); char path[MAX_PATH_LEN] = ""; if (find_directory(B_USER_DIRECTORY , volume, true, path, sizeof(path)) == B_OK) Path = path; #else LAssert(0); #endif if (Path) { Path += DIR_STR; Path += Name; } #endif break; } case LSP_OS: { #if defined WIN32 char16 p[MAX_PATH_LEN]; if (GetWindowsDirectoryW(p, CountOf(p)) > 0) Path = p; #elif defined(HAIKU) dev_t volume = dev_for_path("/boot"); char path[MAX_PATH_LEN] = ""; if (find_directory(B_SYSTEM_DIRECTORY, volume, true, path, sizeof(path)) == B_OK) Path = path; #elif defined LGI_CARBON FSRef Ref; OSErr e = FSFindFolder(kOnAppropriateDisk, kSystemFolderType, kDontCreateFolder, &Ref); if (e) printf("%s:%i - FSFindFolder failed e=%i\n", __FILE__, __LINE__, e); else { LAutoString u = FSRefPath(Ref); if (u) Path = u.Get(); } #elif defined LGI_COCOA NSArray *paths = NSSearchPathForDirectoriesInDomains( NSLibraryDirectory, NSSystemDomainMask, YES); if (paths) { Path = [paths objectAtIndex:0]; LTrimDir(Path); } #elif defined LINUX Path = "/boot"; // it'll do for now... #endif break; } case LSP_OS_LIB: { #if defined WIN32 char16 p[MAX_PATH_LEN]; if (GetSystemDirectoryW(p, CountOf(p)) > 0) Path = p; #elif defined(HAIKU) dev_t volume = dev_for_path("/boot"); char path[MAX_PATH_LEN] = ""; if (find_directory(B_SYSTEM_LIB_DIRECTORY, volume, true, path, sizeof(path)) == B_OK) Path = path; #elif defined MAC Path = "/Library"; #elif defined LINUX Path = "/lib"; // it'll do for now... #endif break; } case LSP_TEMP: { #if defined WIN32 char16 t[MAX_PATH_LEN]; if (GetTempPathW(CountOf(t), t) > 0) { LAutoString utf(WideToUtf8(t)); if (utf) Path = utf; } #elif defined LGI_CARBON FSRef Ref; OSErr e = FSFindFolder(kUserDomain, kTemporaryFolderType, kCreateFolder, &Ref); if (e) LgiTrace("%s:%i - FSFindFolder failed e=%i\n", _FL, e); else { LAutoString u = FSRefPath(Ref); if (u) { Path = u.Get(); } else LgiTrace("%s:%i - FSRefPath failed.\n", _FL); } #elif defined LGI_COCOA NSString *tempDir = NSTemporaryDirectory(); if (tempDir) Path = tempDir; else LAssert(!"No tmp folder?"); #elif defined LINUX Path = "/tmp"; // it'll do for now... #else LAssert(!"Impl me."); #endif break; } case LSP_COMMON_APP_DATA: { #if defined WIN32 Path = WinGetSpecialFolderPath(CSIDL_COMMON_APPDATA); #elif defined LGI_CARBON FSRef Ref; OSErr e = FSFindFolder(kOnSystemDisk, kDomainLibraryFolderType, kDontCreateFolder, &Ref); if (e) printf("%s:%i - FSFindFolder failed e=%i\n", _FL, e); else { auto u = FSRefPath(Ref); if (u) Path = u.Get(); } #elif defined LGI_COCOA NSArray *paths = NSSearchPathForDirectoriesInDomains( NSLibraryDirectory, NSSystemDomainMask, YES); if (paths) { Path = [paths objectAtIndex:0]; } #elif defined LINUX Path = "/usr"; #else LAssert(!"Impl me."); #endif break; } case LSP_USER_APP_DATA: { #if defined WIN32 Path = WinGetSpecialFolderPath(CSIDL_APPDATA); #elif defined LGI_CARBON FSRef Ref; OSErr e = FSFindFolder(kUserDomain, kDomainLibraryFolderType, kDontCreateFolder, &Ref); if (e) printf("%s:%i - FSFindFolder failed e=%i\n", __FILE__, __LINE__, e); else { auto u = FSRefPath(Ref); if (u) Path = u.Get(); } #elif defined LGI_COCOA NSArray *paths = NSSearchPathForDirectoriesInDomains( NSLibraryDirectory, NSUserDomainMask, YES); if (paths) { Path = [paths objectAtIndex:0]; } #elif defined LINUX Path = "/usr"; #elif defined HAIKU dev_t volume = dev_for_path("/boot"); char path[MAX_PATH_LEN] = ""; if (find_directory(B_USER_SETTINGS_DIRECTORY, volume, true, path, sizeof(path)) == B_OK) Path = path; #else LAssert(!"Impl me."); #endif break; } case LSP_LOCAL_APP_DATA: { #if defined WIN32 Path = WinGetSpecialFolderPath(CSIDL_LOCAL_APPDATA); #elif defined LGI_COCOA NSArray *paths = NSSearchPathForDirectoriesInDomains( NSLibraryDirectory, NSUserDomainMask, YES); if (paths) { Path = [paths objectAtIndex:0]; } #else LAssert(!"Impl me."); #endif break; } case LSP_DESKTOP: { #if defined(WINDOWS) && defined(_MSC_VER) Path = WinGetSpecialFolderPath(CSIDL_DESKTOPDIRECTORY); #elif defined(HAIKU) dev_t volume = dev_for_path("/boot"); char path[MAX_PATH_LEN] = ""; if (find_directory(B_DESKTOP_DIRECTORY, volume, true, path, sizeof(path)) == B_OK) Path = path; #elif defined(__GTK_H__) auto p = Gtk::g_get_user_special_dir(Gtk::G_USER_DIRECTORY_DESKTOP); Path = p; #elif defined LGI_COCOA NSArray *paths = NSSearchPathForDirectoriesInDomains( NSDesktopDirectory, NSUserDomainMask, YES); if (paths) { Path = [paths objectAtIndex:0]; } #elif defined LGI_CARBON FSRef Ref; OSErr e = FSFindFolder(kOnAppropriateDisk, kDesktopFolderType, kDontCreateFolder, &Ref); if (e) printf("%s:%i - FSFindFolder failed e=%i\n", __FILE__, __LINE__, e); else { LAutoString u = FSRefPath(Ref); if (u) Path = u.Get(); } #elif defined POSIX struct passwd *pw = getpwuid(getuid()); if (pw) { #ifdef LINUX WindowManager wm = LGetWindowManager(); if (wm == WM_Gnome) { Path.Printf("%s/.gnome-desktop", pw->pw_dir); } #endif if (!LDirExists(Path)) { Path.Printf("%s/Desktop", pw->pw_dir); } } #else #error "Impl me." #endif break; } case LSP_HOME: { #if defined WIN32 Path = WinGetSpecialFolderPath(CSIDL_PROFILE); #elif defined(HAIKU) dev_t volume = dev_for_path("/boot"); char path[MAX_PATH_LEN] = ""; if (find_directory(B_USER_DIRECTORY, volume, true, path, sizeof(path)) == B_OK) Path = path; #elif defined LGI_COCOA NSString *home = NSHomeDirectory(); if (home) Path = home; else LAssert(!"No home path?"); #elif defined POSIX struct passwd *pw = getpwuid(getuid()); if (pw) Path = pw->pw_dir; #else #error "Impl me." #endif break; } case LSP_EXE: { Path = LGetExeFile(); if (!Path) break; auto p = Path.RFind(DIR_STR); if (p > 0) Path.Length(p); break; } case LSP_TRASH: { #if defined LINUX switch (LGetWindowManager()) { case WM_Kde: { static char KdeTrash[256] = ""; if (!ValidStr(KdeTrash)) { // Ask KDE where the current trash is... LStringPipe o; LSubProcess p("kde-config", "--userpath trash"); if (p.Start()) { p.Communicate(&o); char *s = o.NewStr(); if (s) { // Store it.. strcpy_s(KdeTrash, sizeof(KdeTrash), s); DeleteArray(s); // Clear out any crap at the end... char *e = KdeTrash + strlen(KdeTrash) - 1; while (e > KdeTrash && strchr(" \r\n\t/", *e)) { *e-- = 0; } } else { printf("%s:%i - No output from 'kde-config'.\n", _FL); } } else { printf("%s:%i - Run 'kde-config' failed.\n", _FL); } } if (ValidStr(KdeTrash)) Path = KdeTrash; break; } default: { LString Home = LGetSystemPath(LSP_HOME); if (!Home) { LgiTrace("%s:%i - Can't get LSP_HOME.\n", _FL); break; } char p[MAX_PATH_LEN]; if (!LMakePath(p, sizeof(p), Home, ".local/share/Trash/files") || !LDirExists(p)) { LgiTrace("%s:%i - '%s' doesn't exist.\n", _FL, p); break; } Path = p; break; } } #elif defined(HAIKU) dev_t volume = dev_for_path("/boot"); char path[MAX_PATH_LEN] = ""; if (find_directory(B_TRASH_DIRECTORY, volume, true, path, sizeof(path)) == B_OK) Path = path; #elif defined LGI_CARBON FSRef Ref; OSErr e = FSFindFolder(kUserDomain, kTrashFolderType, kDontCreateFolder, &Ref); if (e) printf("%s:%i - FSFindFolder failed e=%i\n", _FL, e); else { LAutoString u = FSRefPath(Ref); if (u) Path = u.Get(); } #elif defined LGI_COCOA NSArray *paths = NSSearchPathForDirectoriesInDomains( NSTrashDirectory, NSUserDomainMask, YES); if (paths) { Path = [paths objectAtIndex:0]; } #elif defined(WIN32) LAssert(0); #endif break; } } return Path; } LString LGetExeFile() { #if defined WIN32 char16 Exe[MAX_PATH_LEN]; if (GetModuleFileNameW(NULL, Exe, CountOf(Exe)) > 0) { LString e = Exe; if (e) { return e; } else { LgiMsg(0, "LgiFromNativeCp returned 0, ANSI CodePage=%i (%s)", "LgiGetExeFile Error", MB_OK, GetACP(), LAnsiToLgiCp()); return LString(); } } LString m; m.Printf("GetModuleFileName failed err: %08.8X", GetLastError()); MessageBoxA(0, m, "LgiGetExeFile Error", MB_OK); LExitApp(); #elif defined HAIKU // Copy the string so as to not allow callers to change it return LgiArgsAppPath.Get(); #elif defined LINUX static char ExePathCache[MAX_PATH_LEN] = ""; bool Status = false; // this is _REALLY_ lame way to do it... but hey there aren't any // other better ways to get the full path of the running executable if (!ExePathCache[0]) { // First try the self method int Len = readlink("/proc/self/exe", ExePathCache, sizeof(ExePathCache)); Status = LFileExists(ExePathCache); // printf("readlink=%i Status=%i Exe='%s'\n", Len, Status, ExePathCache); if (!Status) { ExePathCache[0] = 0; // Next try the map file method char ProcFile[256]; sprintf_s(ProcFile, sizeof(ProcFile), "/proc/%i/maps", getpid()); int fd = open(ProcFile, O_RDONLY); if (fd >= 0) { int Len = 16 << 10; // is this enough? // no better way of determining the length of proc info? char *Buf = new char[Len+1]; if (Buf) { int r = read(fd, Buf, Len); Buf[r] = 0; char *s = strchr(Buf, '/'); if (s) { char *e = strchr(s, '\n'); if (e) { *e = 0; strcpy_s(ExePathCache, sizeof(ExePathCache), s); Status = true; } } DeleteArray(Buf); } close(fd); } else { // Non proc system (like cygwin for example) // char Cmd[256]; // sprintf_s(Cmd, sizeof(Cmd), "ps | grep \"%i\"", getpid()); LStringPipe Ps; LSubProcess p("ps"); if (p.Start()) { p.Communicate(&Ps); char *PsOutput = Ps.NewStr(); if (PsOutput) { auto n = LString(PsOutput).SplitDelimit("\r\n"); for (int i=0; !Status && i 7) { int LinePid = 0; for (int i=0; iReset(NewStr(Path)); return; } #ifdef WIN32 if (PathLen < sizeof(Path) - 4) { strcat(Path, ".lnk"); if (LResolveShortcut(Path, Path, sizeof(Path))) { if (GStr) *GStr = Path; else if (AStr) AStr->Reset(NewStr(Path)); return; } } #endif } // General search fall back... LArray Ext; LArray Files; Ext.Add((char*)Name); if (LRecursiveFileSearch(Exe, &Ext, &Files) && Files.Length()) { if (GStr) *GStr = Files[0]; else { AStr->Reset(Files[0]); Files.DeleteAt(0); } } Files.DeleteArrays(); } LString LFindFile(const char *Name) { LString s; _LFindFile(Name, &s, NULL); return s; } #if defined WIN32 static LARGE_INTEGER Freq = {0}; static bool CurTimeInit = false; #endif uint64_t LCurrentTime() { #if defined WIN32 if (!CurTimeInit) { CurTimeInit = true; if (!QueryPerformanceFrequency(&Freq)) Freq.QuadPart = 0; } if (Freq.QuadPart) { // Return performance counter in ms LARGE_INTEGER i; if (QueryPerformanceCounter(&i)) { return i.QuadPart * 1000 / Freq.QuadPart; } // Now what?? Give up and go back to tick count I guess. Freq.QuadPart = 0; } // Fall back for systems without a performance counter. return GetTickCount(); #elif defined LGI_CARBON UnsignedWide t; Microseconds(&t); uint64 i = ((uint64)t.hi << 32) | t.lo; return i / 1000; #else timeval tv; gettimeofday(&tv, 0); return (tv.tv_sec * 1000) + (tv.tv_usec / 1000); #endif } uint64_t LgiMicroTime() { #if defined WIN32 if (!CurTimeInit) { CurTimeInit = true; if (!QueryPerformanceFrequency(&Freq)) Freq.QuadPart = 0; } if (Freq.QuadPart) { // Return performance counter in ms LARGE_INTEGER i; if (QueryPerformanceCounter(&i)) { return i.QuadPart * 1000000 / Freq.QuadPart; } } return 0; #elif defined LGI_CARBON UnsignedWide t; Microseconds(&t); return ((uint64)t.hi << 32) | t.lo; #else timeval tv; gettimeofday(&tv, 0); return (tv.tv_sec * 1000000) + tv.tv_usec; #endif } int LIsReleaseBuild() { #ifdef _DEBUG return 0; #else return 1; #endif } bool LIsVolumeRoot(const char *Path) { if (!Path) return false; #ifdef WIN32 if ( IsAlpha(Path[0]) && Path[1] == ':' && ( (Path[2] == 0) || (Path[2] == '\\' && Path[3] == 0) ) ) { return true; } #else auto t = LString(Path).SplitDelimit(DIR_STR); if (t.Length() == 0) return true; #ifdef MAC if (!stricmp(t[0], "Volumes") && t.Length() == 2) return true; #else if (!stricmp(t[0], "mnt") && t.Length() == 2) return true; #endif #endif return false; } LString LFormatSize(int64_t Size) { char Buf[64]; LFormatSize(Buf, sizeof(Buf), Size); return Buf; } void LFormatSize(char *Str, int SLen, int64_t Size) { int64_t K = 1024; int64_t M = K * K; int64_t G = K * M; int64_t T = K * G; if (Size == 1) { strcpy_s(Str, SLen, "1 byte"); } else if (Size < K) { sprintf_s(Str, SLen, "%u bytes", (int)Size); } else if (Size < 10 * K) { double d = (double)Size; sprintf_s(Str, SLen, "%.2f KiB", d / K); } else if (Size < M) { sprintf_s(Str, SLen, "%u KiB", (int) (Size / K)); } else if (Size < G) { double d = (double)Size; sprintf_s(Str, SLen, "%.2f MiB", d / M); } else if (Size < T) { double d = (double)Size; sprintf_s(Str, SLen, "%.2f GiB", d / G); } else { double d = (double)Size; sprintf_s(Str, SLen, "%.2f TiB", d / T); } } char *LTokStr(const char *&s) { char *Status = 0; if (s && *s) { // Skip whitespace static char Delim[] = ", \t\r\n"; while (*s && strchr(Delim, *s)) s++; if (*s) { if (strchr("\'\"", *s)) { char Delim = *s++; const char *e = strchr(s, Delim); if (!e) { // error, no end delimiter e = s; while (*e) e++; } Status = NewStr(s, e - s); s = *e ? e + 1 : e; } else { const char *e = s; while (*e && !strchr(Delim, *e)) e++; Status = NewStr(s, e - s); s = e; } } } return Status; } LString LGetEnv(const char *Var) { #ifdef _MSC_VER char *s = NULL; size_t sz; errno_t err = _dupenv_s(&s, &sz, Var); if (err) return LString(); LString ret(s); free(s); return ret; #else return getenv("PATH"); #endif } LString::Array LGetPath() { LString::Array Paths; #ifdef MAC // OMG, WHY?! Seriously? // // The GUI application path is NOT the same as what is configured for the terminal. // At least in 10.12. And I don't know how to make them the same. This works around // that for the time being. #if 1 LFile EctPaths("/etc/paths", O_READ); Paths = EctPaths.Read().Split("\n"); #else LFile::Path Home(LSP_HOME); Home += ".profile"; if (!Home.Exists()) { Home--; Home += ".zprofile"; } auto Profile = LFile(Home, O_READ).Read().Split("\n"); for (auto Ln : Profile) { auto p = Ln.SplitDelimit(" =", 2); if (p.Length() == 3 && p[0].Equals("export") && p[1].Equals("PATH")) { Paths = p[2].Strip("\"").SplitDelimit(LGI_PATH_SEPARATOR); Paths.SetFixedLength(false); for (auto &p : Paths) { if (p.Equals("$PATH")) { auto SysPath = LGetEnv("PATH").SplitDelimit(LGI_PATH_SEPARATOR); for (unsigned i=0; i 0) { Period = p; } } bool DoEvery::DoNow() { int64 Now = LCurrentTime(); if (LastTime + Period < Now) { LastTime = Now; return true; } return false; } ////////////////////////////////////////////////////////////////////// bool LCapabilityClient::NeedsCapability(const char *Name, const char *Param) { for (int i=0; iNeedsCapability(Name, Param); return Targets.Length() > 0; } LCapabilityClient::~LCapabilityClient() { for (int i=0; iClients.Delete(this); } void LCapabilityClient::Register(LCapabilityTarget *t) { if (t && !Targets.HasItem(t)) { Targets.Add(t); t->Clients.Add(this); } } LCapabilityTarget::~LCapabilityTarget() { for (int i=0; iTargets.Delete(this); } ///////////////////////////////////////////////////////////////////// #define BUF_SIZE (4 << 10) #define PROFILE_MICRO 1 LProfile::LProfile(const char *Name, int HideMs) { MinMs = HideMs; Used = 0; Buf = NULL; Add(Name); } LProfile::~LProfile() { Add("End"); uint64 TotalMs = s.Last().Time - s[0].Time; if (MinMs > 0) { if (TotalMs < MinMs #if PROFILE_MICRO * 1000 #endif ) { return; } } uint64 accum = 0; for (int i=0; i BUF_SIZE - 64) { LAssert(0); return; } char *Name = Buf + Used; Used += sprintf_s(Name, BUF_SIZE - Used, "%s:%i", File, Line) + 1; s.Add(Sample( #if PROFILE_MICRO LgiMicroTime(), #else LCurrentTime(), #endif Name)); } //////////////////////////////////////////////////////////////////////////////////////////////////////////// bool LIsValidEmail(LString Email) { // Local part char buf[321]; char *o = buf; char *e = Email; if (!Email || *e == '.') return false; #define OutputChar() \ if (o - buf >= sizeof(buf) - 1) \ return false; \ *o++ = *e++ // Local part while (*e) { if (strchr("!#$%&\'*+-/=?^_`.{|}~", *e) || IsAlpha((uchar)*e) || IsDigit((uchar)*e)) { OutputChar(); } else if (*e == '\"') { // Quoted string OutputChar(); bool quote = false; while (*e && !quote) { quote = *e == '\"'; OutputChar(); } } else if (*e == '\\') { // Quoted character e++; if (*e < ' ' || *e >= 0x7f) return false; OutputChar(); } else if (*e == '@') { break; } else { // Illegal character return false; } } // Process the '@' if (*e != '@' || o - buf > 64) return false; OutputChar(); // Domain part... if (*e == '[') { // IP addr OutputChar(); // Initial char must by a number if (!IsDigit(*e)) return false; // Check the rest... char *Start = e; while (*e) { if (IsDigit(*e) || *e == '.') { OutputChar(); } else { return false; } } // Not a valid IP if (e - Start > 15) return false; if (*e != ']') return false; OutputChar(); } else { // Hostname, check initial char if (!IsAlpha(*e) && !IsDigit(*e)) return false; // Check the rest. while (*e) { if (IsAlpha(*e) || IsDigit(*e) || strchr(".-", *e)) { OutputChar(); } else { return false; } } } // Remove any trailing dot/dash while (strchr(".-", o[-1])) o--; // Output *o = 0; LAssert(o - buf <= sizeof(buf)); if (strcmp(Email, buf)) Email.Set(buf, o - buf); return true; } ////////////////////////////////////////////////////////////////////////// LString LGetAppForProtocol(const char *Protocol) { LString App; if (!Protocol) return App; #ifdef WINDOWS LRegKey k(false, "HKEY_CLASSES_ROOT\\%s\\shell\\open\\command", Protocol); if (k.IsOk()) { const char *p = k.GetStr(); if (p) { LAutoString a(LTokStr(p)); App = a.Get(); } } #elif defined(LINUX) const char *p = NULL; if (stricmp(Protocol, "mailto")) p = "xdg-email"; else p = "xdg-open"; LString Path = LGetEnv("PATH"); LString::Array a = Path.SplitDelimit(LGI_PATH_SEPARATOR); for (auto i : a) { LFile::Path t(i); t += p; if (t.Exists()) { App = t.GetFull(); break; } } #elif defined(__GTK_H__) LAssert(!"What to do?"); #elif defined(MAC) // Get the handler type LString s; s.Printf("%s://domain/path", Protocol); auto str = s.NsStr(); auto type = [NSURL URLWithString:str]; [str release]; auto handlerUrl = [[NSWorkspace sharedWorkspace] URLForApplicationToOpenURL:type]; [type release]; if (handlerUrl) { // Convert to app path s = [handlerUrl absoluteString]; LUri uri(s); if (uri.sProtocol.Equals("file")) App = uri.sPath.RStrip("/"); else LgiTrace("%s:%i - Error: unknown protocol '%s'\n", _FL, uri.sProtocol.Get()); } [handlerUrl release]; #else #warning "Impl me." #endif return App; } diff --git a/src/common/Lgi/SubProcess.cpp b/src/common/Lgi/SubProcess.cpp --- a/src/common/Lgi/SubProcess.cpp +++ b/src/common/Lgi/SubProcess.cpp @@ -1,1424 +1,1424 @@ /** \file \brief Sub-process wrapper. This class runs one or more sub-processes chained together by pipes. Example: LSubProcess p1("ls", "-l"); LSubProcess p2("grep", "string"); p1.Connect(&p2); p1.Start(true, false); int r; char Buf[256]; while ((r = p1.Read(Buf, sizeof(Buf))) > 0) { // So something with 'Buf' } */ #if defined(MAC) || defined(POSIX) || defined(HAIKU) #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include #include #include #include #include #elif defined(WINDOWS) #define NTDDI_VERSION 0x0A000006 #endif #include "lgi/common/Lgi.h" #include "lgi/common/SubProcess.h" #define DEBUG_SUBPROCESS 0 #define DEBUG_ARGS 0 #if defined(WIN32) #include #include #pragma comment(lib, "netapi32.lib") #define NULL_PIPE NULL #define ClosePipe CloseHandle typedef HRESULT (WINAPI *ProcCreatePseudoConsole)(_In_ COORD size, _In_ HANDLE hInput, _In_ HANDLE hOutput, _In_ DWORD dwFlags, _Out_ HPCON* phPC); #else #define NULL_PIPE -1 #define ClosePipe close #define INVALID_PID -1 #endif #if defined(POSIX) #include #if defined(LINUX) // !mac and !haiku #include #endif #if !defined(MAC) #include #endif #endif #ifdef HAIKU #include #endif LSubProcess::Pipe::Pipe() { Read = Write = NULL_PIPE; } bool LSubProcess::Pipe::Create ( #ifdef WIN32 LPSECURITY_ATTRIBUTES pAttr #else void *UnusedParam #endif ) { #if defined(WIN32) return CreatePipe(&Read, &Write, pAttr, 0) != 0; #else return pipe(Handles) != NULL_PIPE; #endif } void LSubProcess::Pipe::Close() { if (Read != NULL_PIPE) { ClosePipe(Read); Read = NULL_PIPE; } if (Write != NULL_PIPE) { ClosePipe(Write); Write = NULL_PIPE; } } char *ArgTok(const char *&s) { if (!s || !*s) return NULL; while (*s && strchr(WhiteSpace, *s)) s++; if (*s == '\'' || *s == '\"') { char delim = *s++; const char *start = s; while (*s && *s != delim) s++; char *r = NewStr(start, s - start); if (*s) s++; return r; } else { const char *start = s; while (*s && !strchr(WhiteSpace, *s)) s++; return NewStr(start, s - start); } } struct LSubProcessPriv { LString Exe; LString InitialFolder; LArray Args; bool NewGroup; bool PseudoConsole; bool EnvironmentChanged; LArray Environment; uint32_t ErrorCode; LSubProcess::PipeHandle ExternIn, ExternOut; LSubProcess::ProcessId ChildPid; #if defined(POSIX) LSubProcess::Pipe Io; int ExitValue; // was uint32 int UserId = -1; int GrpId = -1; #elif defined(WIN32) HANDLE ChildHnd; DWORD ExitValue; LSubProcess::Pipe ChildOutput, ChildInput; HPCON hConsole; LLibrary Kernel; ProcCreatePseudoConsole CreatePseudoConsole; LString User, Password; #endif LSubProcessPriv(bool pseudoConsole) #ifdef WINDOWS : Kernel("Kernel32") #endif { NewGroup = false; ErrorCode = 0; EnvironmentChanged = false; ExternIn = NULL_PIPE; ExternOut = NULL_PIPE; PseudoConsole = false; #if defined(POSIX) ChildPid = INVALID_PID; ExitValue = -1; #elif defined(WIN32) ChildPid = NULL; ChildHnd = NULL; ExitValue = 0; hConsole = NULL; CreatePseudoConsole = (ProcCreatePseudoConsole)Kernel.GetAddress("CreatePseudoConsole"); PseudoConsole = CreatePseudoConsole != NULL && pseudoConsole; #endif } ~LSubProcessPriv() { Args.DeleteArrays(); } }; LSubProcess::LSubProcess(const char *exe, const char *args, bool pseudoConsole) { d = new LSubProcessPriv(pseudoConsole); Parent = Child = NULL; d->Exe = exe; d->Args.Add(NewStr(d->Exe)); #if DEBUG_SUBPROCESS LgiTrace("%s:%i - %p::LSubProcess('%s','%s')\n", _FL, this, exe, args); #endif char *s; while ((s = ArgTok(args))) { d->Args.Add(s); #if DEBUG_ARGS LgiTrace("a='%s'\n", s); #endif } } LSubProcess::~LSubProcess() { #if defined(POSIX) d->Io.Close(); #endif if (Child) { LAssert(Child->Parent == this); Child->Parent = NULL; } if (Parent) { LAssert(Parent->Child == this); Parent->Child = NULL; } DeleteObj(d); } #ifndef WINDOWS extern char **environ; #endif bool LSubProcess::GetNewGroup() { return d->NewGroup; } void LSubProcess::SetNewGroup(bool ng) { d->NewGroup = ng; } LSubProcess::ProcessId LSubProcess::Handle() { return d->ChildPid; } LSubProcess::Variable *LSubProcess::GetEnvVar(const char *Var, bool Create) { if (d->Environment.Length() == 0) { // Read all variables in #ifdef WINDOWS LPWCH e = GetEnvironmentStringsW(); if (e) { char16 *s = e; while (*s) { char16 *eq = StrchrW(s, '='); if (!eq) break; ptrdiff_t NameChars = eq - s; if (NameChars > 0) { Variable &v = d->Environment.New(); v.Var.SetW(s, eq - s); eq++; v.Val.SetW(eq); } eq += StrlenW(eq); s = eq + 1; } FreeEnvironmentStringsW(e); } #else for (int i=0; environ[i]; i++) { auto p = LString(environ[i]).Split("=", 1); if (p.Length() == 2) { Variable &v = d->Environment.New(); v.Var = p[0]; v.Val = p[1]; } } #endif } for (unsigned i=0; iEnvironment.Length(); i++) { if (d->Environment[i].Var.Equals(Var)) return &d->Environment[i]; } if (Create) { Variable &v = d->Environment.New(); v.Var = Var; return &v; } return NULL; } bool LSubProcess::Dupe(PipeHandle Old, PipeHandle New) { #if defined(POSIX) while ((dup2(Old, New) == -1) && (errno == EINTR)) ; return true; #elif defined(WIN32) return DuplicateHandle( GetCurrentProcess(), Old, GetCurrentProcess(), &New, 0, false, DUPLICATE_SAME_ACCESS) != 0; #else return false; #endif } bool LSubProcess::IsRunning() { #if defined(POSIX) int Status = 0; pid_t r = waitpid(d->ChildPid, &Status, WNOHANG); if (r == d->ChildPid) { d->ChildPid = INVALID_PID; if (WIFEXITED(Status)) d->ExitValue = WEXITSTATUS(Status); else d->ExitValue = 255; } return d->ChildPid != INVALID_PID; #elif defined(WIN32) if (!GetExitCodeProcess(d->ChildHnd, &d->ExitValue)) return false; if (d->ExitValue != STILL_ACTIVE) { d->ChildPid = 0; return false; } else { return true; } #endif } uint32_t LSubProcess::GetErrorCode() { return d->ErrorCode; } int32 LSubProcess::GetExitValue() { #if defined(POSIX) if (d->ChildPid != INVALID_PID) // This will set ExitValue if the process has finished. IsRunning(); #elif defined(WIN32) GetExitCodeProcess(d->ChildHnd, &d->ExitValue); #endif return d->ExitValue; } bool LSubProcess::SetUser(const char *User, const char *Pass) { if (!User || !Pass) { LgiTrace("%s:%i - SetUser param err.\n", _FL); return false; } #if defined(POSIX) auto entry = getpwnam(User); if (!entry) { LgiTrace("%s:%i - SetUser: User '%s' doesn't exist.\n", _FL, User); return false; } if (0 != strcmp(entry->pw_passwd, "x")) { if (strcmp(entry->pw_passwd, crypt(Pass, entry->pw_passwd))) { LgiTrace("%s:%i - SetUser: Wrong password.\n", _FL); return false; } } #if !defined(MAC) else { // password is in shadow file auto shadowEntry = getspnam(User); if (!shadowEntry) { LgiTrace("%s:%i - SetUser: Failed to read shadow entry for user '%s'\n", User); return false; } if (strcmp(shadowEntry->sp_pwdp, crypt(Pass, shadowEntry->sp_pwdp))) { LgiTrace("%s:%i - SetUser: Wrong password.\n", _FL); return false; } } #endif d->UserId = entry->pw_uid; d->GrpId = entry->pw_gid; #elif defined(WINDOWS) LAutoWString u(Utf8ToWide(User)); USER_INFO_0 *info = NULL; auto result = NetUserGetInfo(NULL, u, 0, (LPBYTE*)&info); if (result != NERR_Success) { return false; } d->User = User; d->Password = Pass; #else #error "Impl me." #endif return true; } void LSubProcess::SetInitFolder(const char *f) { d->InitialFolder = f; } const char *LSubProcess::GetEnvironment(const char *Var) { Variable *v = GetEnvVar(Var); return v ? v->Val.Get() : NULL; } bool LSubProcess::SetEnvironment(const char *Var, const char *Value) { Variable *v = GetEnvVar(Var, true); if (!v) return false; bool IsPath = !_stricmp(Var, "PATH"); LStringPipe a; const char *s = Value; while (*s) { char *n = strchr(s, '%'); char *e = n ? strchr(n + 1, '%') : NULL; if (n && e) { a.Write(s, (int) (n-s)); n++; ptrdiff_t bytes = e - n; char Name[128]; if (bytes > sizeof(Name) - 1) bytes = sizeof(Name)-1; memcpy(Name, n, bytes); Name[bytes] = 0; const char *existing = GetEnvironment(Name); if (existing) { a.Write(existing, (int)strlen(existing)); } s = e + 1; } else { a.Write(s, (int)strlen(s)); break; } } - v->Val = a.NewGStr(); + v->Val = a.NewLStr(); if (IsPath) { // Remove missing paths from the list auto t = LString(v->Val).SplitDelimit(LGI_PATH_SEPARATOR); LStringPipe p; for (unsigned i=0; iVal = p.NewGStr(); + v->Val = p.NewLStr(); } d->EnvironmentChanged = true; return true; } bool LSubProcess::GetValue(const char *Var, ::LVariant &Value) { switch (LStringToDomProp(Var)) { case StreamReadable: { #ifdef WINNATIVE char Buf[32] = ""; DWORD lpBytesRead = 0; BOOL b = PeekNamedPipe( d->ChildOutput.Read, Buf, sizeof(Buf), &lpBytesRead, NULL, NULL); Value = b && lpBytesRead > 0; break; #endif } /* case StreamWritable: { break; } */ default: return false; } return true; } void LSubProcess::SetStdin(OsFile Hnd) { d->ExternIn = Hnd; } void LSubProcess::SetStdout(OsFile Hnd) { d->ExternOut = Hnd; } void LSubProcess::Connect(LSubProcess *child) { Child = child; if (Child) { Child->Parent = this; } } bool LSubProcess::Start(bool ReadAccess, bool WriteAccess, bool MapStderrToStdout) { bool Status = false; #if DEBUG_SUBPROCESS LgiTrace("%s:%i - %p::Start(%i,%i,%i)\n", _FL, this, ReadAccess, WriteAccess, MapStderrToStdout); #endif #if USE_SIMPLE_FORK int in[2]; if (pipe(in) == -1) { printf("parent: Failed to create stdin pipe"); return false; } int out[2]; if (pipe(out) == -1) { printf("parent: Failed to create stdout pipe"); return false; } d->ChildPid = fork(); if (d->ChildPid == 0) { // We are in the child process. if (d->InitialFolder) { chdir(d->InitialFolder); } // Child shouldn't write to its stdin. if (close(in[1])) printf("%s:%i - close failed.\n", _FL); // Child shouldn't read from its stdout. if (close(out[0])) printf("%s:%i - close failed.\n", _FL); // Redirect stdin and stdout for the child process. if (dup2(in[0], fileno(stdin)) == -1) { printf("%s:%i - child[pre-exec]: Failed to redirect stdin for child\n", _FL); return false; } if (close(in[0])) printf("%s:%i - close failed.\n", _FL); if (dup2(out[1], fileno(stdout)) == -1) { printf("%s:%i - child[pre-exec]: Failed to redirect stdout for child\n", _FL); return false; } if (dup2(out[1], fileno(stderr)) == -1) { printf("%s:%i - child[pre-exec]: Failed to redirect stderr for child\n", _FL); return false; } close(out[1]); // Execute the child d->Args.Add(NULL); LString::Array Path; if (!LFileExists(d->Exe)) { // Apparently 'execve' doesn't search the path... so we're going to look up the // full executable path ourselves. if (!Path.Length()) Path = LGetPath(); for (auto s: Path) { LFile::Path p(s, d->Exe); if (p.Exists()) { d->Exe = p.GetFull(); break; } } } if (d->UserId >= 0) setuid(d->UserId); if (d->GrpId >= 0) setgid(d->GrpId); if (d->Environment.Length()) { LString::Array Vars; LArray Env; Vars.SetFixedLength(false); for (auto v : d->Environment) { LString &s = Vars.New(); s.Printf("%s=%s", v.Var.Get(), v.Val.Get()); Env.Add(s.Get()); if (v.Var.Equals("PATH")) Path = v.Val.Split(LGI_PATH_SEPARATOR); } Env.Add(NULL); #if DEBUG_SUBPROCESS printf("Exe=%s\n", d->Exe.Get()); printf("Env.Len=%i\n", (int)Env.Length()); for (int i=0; iExe, &d->Args[0], Env.AddressOf()); printf("execve=%i err=%i\n", r, errno); } else { execvp(d->Exe, &d->Args[0]); } // Execution will pass to here if the 'Exe' can't run or doesn't exist // So by exiting with an error the parent process can handle it. printf("GSUBPROCESS_ERROR\n"); exit(GSUBPROCESS_ERROR); } else { // We are in the parent process. if (d->ChildPid == -1) { printf("%s:%i - parent: Failed to create child", _FL); return false; } // Parent shouldn't read from child's stdin. if (close(in[0])) printf("%s:%i - close failed.\n", _FL); // Parent shouldn't write to child's stdout. if (close(out[1])) printf("%s:%i - close failed.\n", _FL); d->Io.Read = out[0]; d->Io.Write = in[1]; #if DEBUG_SUBPROCESS printf("USE_SIMPLE_FORK success.\n"); #endif return true; } #else // Find the end of the process list ::LArray p; for (LSubProcess *s=this; s; s=s->Child) { LAssert(!s->Child || s->Child->Parent == s); p.Add(s); } size_t Kids = p.Length() + 1; #ifdef WIN32 SECURITY_ATTRIBUTES Attr; Attr.nLength = sizeof(SECURITY_ATTRIBUTES); Attr.bInheritHandle = true; Attr.lpSecurityDescriptor = NULL; #else int Attr = 0; #endif #if defined(POSIX) ::LArray Pipes; Pipes.Length(Kids); Pipes[0].Create(&Attr); #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *PARENT* pipe[%i].create %i,%i\n", _FL, 0, Pipes[0].Read, Pipes[0].Write); #endif Status = true; for (int i=1; iChildPid = fork(); if (sp->ChildPid == INVALID_PID) { LgiTrace("%s:%i - fork failed with %i", _FL, errno); exit(1); } else if (sp->ChildPid == 0) { if (InitialFolder) { chdir(InitialFolder); } // Close irrelevant pipes for (int j = 0; j < i-1; j++) { #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *CHILD* pipe[%i].close %i,%i\n", _FL, j, Pipes[j].Read, Pipes[j].Write); #endif Pipes[j].Close(); } // Set up STDIN and STDOUT Pipe &in = Pipes[i-1]; Pipe &out = Pipes[i]; #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *CHILD* %i) Child init %i->'%s'->%i\n", _FL, i, in.Read, sp->Exe.Get(), out.Write); #endif #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *CHILD* Dupe %i->%i\n", _FL, in.Read, STDIN_FILENO); #endif Dupe(in.Read, STDIN_FILENO); #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *CHILD* Close %i\n", _FL, in.Write); #endif close(in.Write); #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *CHILD* Dupe %i->%i\n", _FL, out.Write, STDOUT_FILENO); #endif Dupe(out.Write, STDOUT_FILENO); #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *CHILD* Dupe %i->%i\n", out.Write, STDERR_FILENO); #endif Dupe(out.Write, STDERR_FILENO); #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *CHILD* Close %i\n", _FL, out.Read); #endif close(out.Read); fsync(STDOUT_FILENO); LSleep(100); // Execute the child sp->Args.Add(NULL); execvp(sp->Exe, &sp->Args[0]); LgiTrace("%s:%i - execvp('%s').\n", _FL, sp->Exe.Get()); for (int i=0; iArgs.Length(); i++) LgiTrace("%s:%i - Args[%i]='%s'\n", _FL, i, sp->Args[i]); Status = false; break; } } // Close irrelevant pipes for (int j = 1; j < Kids - 1; j++) { #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *PARENT* pipe[%i].close %i,%i\n", _FL, j, Pipes[j].Read, Pipes[j].Write); #endif Pipes[j].Close(); } #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *PARENT* pipe[0].close %i, pipe[%i].close %i\n", _FL, Pipes[0].Read, Pipes.Length()-1, Pipes.Last().Write); #endif close(Pipes[0].Read); close(Pipes.Last().Write); // Set the input and output pipes for this sub-process. if (WriteAccess) Io.Write = Pipes[0].Write; else { #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *PARENT* pipe[0].close %i\n", _FL, Pipes[0].Write); #endif close(Pipes[0].Write); } if (ReadAccess) Io.Read = Pipes.Last().Read; else { #if DEBUG_SUBPROCESS LgiTrace("%s:%i - *PARENT* pipe[%i].close %i\n", _FL, Pipes.Length()-1, Pipes.Last().Read); #endif close(Pipes.Last().Read); } // LgiTrace("Final Handles %i, %i\n", Io.Read, Io.Write); #elif defined(WIN32) LAutoWString WExe; if (LFileExists(d->Exe)) { WExe.Reset(Utf8ToWide(d->Exe)); } else { char *Ext = LGetExtension(d->Exe); bool HasExt = Ext && _stricmp(Ext, "exe") == 0; #if defined(WIN32) && !defined(PLATFORM_MINGW) LString::Array p; char *sPath = NULL; size_t sSize; errno_t err = _dupenv_s(&sPath, &sSize, "PATH"); if (err == 0) p = LString(sPath).SplitDelimit(LGI_PATH_SEPARATOR); free(sPath); #else LToken p(getenv("PATH"), LGI_PATH_SEPARATOR); #endif for (unsigned i=0; iExe); if (LFileExists(s)) { WExe.Reset(Utf8ToWide(s)); break; } if (!HasExt) { strcat_s(s, sizeof(s), ".exe"); if (LFileExists(s)) { WExe.Reset(Utf8ToWide(s)); break; } } } } #if DEBUG_SUBPROCESS LgiTrace("%s:%i - Exe='%S'\n", _FL, WExe.Get()); #endif LStringPipe Args; for (unsigned i=0; iArgs.Length(); i++) { auto a = d->Args[i]; auto sp = i ? " " : ""; if (strchr(a, ' ')) Args.Print("%s\"%s\"", sp, a); else Args.Print("%s%s", sp, a); } - LAutoWString WArg(Utf8ToWide(Args.NewGStr())); + LAutoWString WArg(Utf8ToWide(Args.NewLStr())); #if DEBUG_SUBPROCESS || DEBUG_ARGS LgiTrace("%s:%i - Args='%S'\n", _FL, WArg.Get()); #endif bool HasExternIn = d->ExternIn != NULL_PIPE; #if DEBUG_SUBPROCESS LgiTrace("%s:%i - HasExternIn=%i\n", _FL, HasExternIn); #endif if (d->ChildOutput.Create(&Attr) && (HasExternIn || d->ChildInput.Create(&Attr))) { if (!SetHandleInformation(d->ChildOutput.Read, HANDLE_FLAG_INHERIT, 0)) LgiTrace("%s:%i - SetHandleInformation failed.\n", _FL); if (!HasExternIn && !SetHandleInformation(d->ChildInput.Write, HANDLE_FLAG_INHERIT, 0)) LgiTrace("%s:%i - SetHandleInformation failed.\n", _FL); #if DEBUG_SUBPROCESS LgiTrace("%s:%i - Output Pipe: rd=%p, wr=%p\n", _FL, d->ChildOutput.Read, d->ChildOutput.Write); if (!HasExternIn) LgiTrace("%s:%i - Input Pipe: rd=%p, wr=%p\n", _FL, d->ChildInput.Read, d->ChildInput.Write); #endif DWORD CreateFlags = CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT; if (d->NewGroup) CreateFlags |= CREATE_NEW_PROCESS_GROUP; STARTUPINFOEXW InfoEx; ZeroObj(InfoEx); auto &Info = InfoEx.StartupInfo; Info.cb = sizeof(InfoEx); PROCESS_INFORMATION ProcInfo; ZeroObj(ProcInfo); Info.dwFlags = STARTF_USESTDHANDLES; if (d->PseudoConsole) { CreateFlags |= EXTENDED_STARTUPINFO_PRESENT; size_t size; InitializeProcThreadAttributeList(NULL, 1, 0, (PSIZE_T)&size); BYTE *attrList = new BYTE[size]; InfoEx.lpAttributeList = reinterpret_cast(attrList); auto b = InitializeProcThreadAttributeList(InfoEx.lpAttributeList, 1, 0, (PSIZE_T)&size); if (!b) { LgiTrace("%s:%s - InitializeProcThreadAttributeList failed.\n", _FL); return false; } COORD dimensions = { 500, 500 }; auto r = d->CreatePseudoConsole(dimensions, HasExternIn ? d->ExternIn : d->ChildInput.Read, d->ChildOutput.Write, 0, &d->hConsole); if (FAILED(r)) { LgiTrace("%s:%s - CreatePseudoConsole failed.\n", _FL); return false; } b = UpdateProcThreadAttribute( InfoEx.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, &d->hConsole, sizeof(HPCON), NULL, NULL); if (!b) { LgiTrace("%s:%s - UpdateProcThreadAttribute failed.\n", _FL); return false; } } else { Info.hStdOutput = d->ChildOutput.Write; Info.hStdInput = HasExternIn ? d->ExternIn : d->ChildInput.Read; if (MapStderrToStdout) Info.hStdError = d->ChildOutput.Write; } LAutoWString WInitialFolder(Utf8ToWide(d->InitialFolder)); #if DEBUG_SUBPROCESS LgiTrace("%s:%i - WInitialFolder=%S\n", _FL, WInitialFolder.Get()); #endif LAutoWString WEnv; if (d->EnvironmentChanged) { LMemQueue q(256); for (unsigned i=0; iEnvironment.Length(); i++) { Variable &v = d->Environment[i]; LAutoWString Var(Utf8ToWide(v.Var)); LAutoWString Val(Utf8ToWide(v.Val)); q.Write(Var, sizeof(char16)*(StrlenW(Var))); q.Write(L"=", sizeof(char16)); q.Write(Val, sizeof(char16)*(StrlenW(Val)+1)); } WEnv.Reset((char16*)q.New(4)); } bool createResult = false; if (d->User) { LAutoWString WUser(Utf8ToWide(d->User)); LAutoWString WPass(Utf8ToWide(d->Password)); createResult = CreateProcessWithLogonW( WUser, NULL, WPass, LOGON_NETCREDENTIALS_ONLY, WExe, WArg, CreateFlags, // dwCreationFlags WEnv, // lpEnvironment WInitialFolder, // lpCurrentDirectory &Info, // lpStartupInfo &ProcInfo) != 0; } else { createResult = CreateProcessW( WExe, WArg, &Attr, // lpProcessAttributes NULL, // lpThreadAttributes TRUE, // bInheritHandles CreateFlags, // dwCreationFlags WEnv, // lpEnvironment WInitialFolder, // lpCurrentDirectory &Info, // lpStartupInfo &ProcInfo) != 0; } if (createResult) { d->ChildPid = ProcInfo.dwProcessId; d->ChildHnd = ProcInfo.hProcess; CloseHandle(ProcInfo.hThread); #if DEBUG_SUBPROCESS LgiTrace("%s:%i - CreateProcessW OK, ChildPid=%p, ChildHnd=%p\n", _FL, d->ChildPid, d->ChildHnd); #endif Status = true; } else { d->ErrorCode = GetLastError(); LgiTrace("%s:%i - CreateProces('%S', '%S'...) failed with %i\n", _FL, WExe.Get(), WArg, d->ErrorCode); } #if DEBUG_SUBPROCESS LgiTrace("%s:%i - Closing handles: %p, %p\n", _FL, d->ChildOutput.Write, d->ChildInput.Read); #endif CloseHandle(d->ChildOutput.Write); CloseHandle(d->ChildInput.Read); if (d->PseudoConsole) DeleteProcThreadAttributeList(InfoEx.lpAttributeList); } #endif #endif return Status; } int32 LSubProcess::Communicate(LStreamI *Out, LStreamI *In, LCancel *Cancel) { char Buf[1024]; ssize_t r; LAssert(In == NULL); // Impl me. #define NOT_CANCELLED (!Cancel || !Cancel->IsCancelled()) while (IsRunning() && NOT_CANCELLED) { r = Read(Buf, sizeof(Buf)); if (r > 0 && Out) Out->Write(Buf, r); } while (NOT_CANCELLED) { r = Read(Buf, sizeof(Buf)); if (r > 0 && Out) Out->Write(Buf, r); else break; } return GetExitValue(); } int LSubProcess::Wait() { int Status = -1; #if defined(POSIX) if (d->ChildPid != INVALID_PID) { int Status = 0; pid_t r = waitpid(d->ChildPid, &Status, 0); if (r == d->ChildPid) { d->ChildPid = INVALID_PID; if (WIFEXITED(Status)) d->ExitValue = WEXITSTATUS(Status); else d->ExitValue = 255; } } #elif defined(WIN32) if (d->ChildHnd) { DWORD r = WaitForSingleObject(d->ChildHnd, INFINITE); if (r == WAIT_OBJECT_0) { if (GetExitCodeProcess(d->ChildHnd, &r)) { Status = r; } } } #endif return Status; } bool LSubProcess::Interrupt() { #if defined(POSIX) return Signal(SIGINT); #elif defined(WIN32) if (!d->ChildHnd) return false; #if 1 // It's impossible to be attached to 2 consoles at the same time, // so release the current one. FreeConsole(); // This does not require the console window to be visible. if (AttachConsole(d->ChildPid)) { // Disable Ctrl-C handling for our program SetConsoleCtrlHandler(NULL, true); BOOL b = GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0); // Must wait here. If we don't and re-enable Ctrl-C // handling below too fast, we might terminate ourselves. LSleep(1000); FreeConsole(); // Re-enable Ctrl-C handling or any subsequently started // programs will inherit the disabled state. SetConsoleCtrlHandler(NULL, false); return b != 0; } #else if (GenerateConsoleCtrlEvent(CTRL_C_EVENT, ChildPid)) return true; auto Err = GetLastError(); LgiTrace("%s:%i - GenerateConsoleCtrlEvent failed with 0x%x\n", _FL, Err); #endif return false; #else return false; #endif } bool LSubProcess::Signal(int which) { #if defined(POSIX) if (d->ChildPid == INVALID_PID) { printf("%s:%i - child pid doesn't exist (%s).\n", _FL, d->Exe.Get()); return false; } if (kill(d->ChildPid, which)) { printf("%s:%i - kill(%i, %i) failed.\n", _FL, d->ChildPid, which); return false; } printf("%s:%i - kill(%i, %i).\n", _FL, d->ChildPid, which); if (which == SIGTERM) d->ChildPid = INVALID_PID; #elif defined(WIN32) if (!d->ChildHnd) { LgiTrace("%s:%i - No child process.\n", _FL); return false; } LAssert(!"Impl me."); #endif return true; } bool LSubProcess::Kill() { #if defined(POSIX) return Signal(SIGTERM); #elif defined(WIN32) if (!d->ChildHnd) { LgiTrace("%s:%i - No child process.\n", _FL); return false; } if (TerminateProcess(d->ChildHnd, -1)) { d->ChildHnd = NULL; } else { LgiTrace("%s:%i - TerminateProcess failed with 0x%x.\n", _FL, GetLastError()); return false; } #endif return true; } LString LSubProcess::Read() { LStringPipe p(512); char Buf[512]; ssize_t Rd; while (Peek()) { Rd = Read(Buf, sizeof(Buf)); if (Rd > 0) p.Write(Buf, Rd); else break; } - return p.NewGStr(); + return p.NewLStr(); } ssize_t LSubProcess::Read(void *Buf, ssize_t Size, int TimeoutMs) { #if defined(POSIX) bool DoRead = true; if (TimeoutMs) { OsSocket s = d->Io.Read; if (ValidSocket(s)) { struct timeval t = {TimeoutMs / 1000, (TimeoutMs % 1000) * 1000}; fd_set r; FD_ZERO(&r); FD_SET(s, &r); int v = select((int)s+1, &r, 0, 0, &t); if (v > 0 && FD_ISSET(s, &r)) { DoRead = true; } else { // printf("SubProc not readable..\n"); return 0; } } else LgiTrace("%s:%i - Invalid socket.\n", _FL); } return (int)read(d->Io.Read, Buf, Size); #else DWORD Rd = -1, Sz; if (!ReadFile(d->ChildOutput.Read, Buf, AssertCast(Sz, Size), &Rd, NULL)) return -1; return Rd; #endif } int LSubProcess::Peek() { #if defined(POSIX) int bytesAvailable = 0; int r = ioctl(d->Io.Read, FIONREAD, &bytesAvailable); return r ? -1 : bytesAvailable; #else DWORD Rd = 0, Avail = 0; char Buf[32]; if (PeekNamedPipe(d->ChildOutput.Read, Buf, sizeof(Buf), &Rd, &Avail, NULL)) return Rd; return 0; #endif } bool LSubProcess::Write(LString s) { auto Wr = Write(s.Get(), s.Length()); return Wr == s.Length(); } ssize_t LSubProcess::Write(const void *Buf, ssize_t Size, int Flags) { #if defined(POSIX) return (int)write(d->Io.Write, Buf, Size); #else DWORD Wr = -1, Sz; if (!WriteFile(d->ChildInput.Write, Buf, AssertCast(Sz, Size), &Wr, NULL)) return -1; return Wr; #endif } //////////////////////////////////////////////////////////////////////////////////// bool LIsProcess(OsProcessId Pid) { bool Status = false; #if defined WIN32 HANDLE hProc = OpenProcess(PROCESS_QUERY_INFORMATION, false, Pid); if (hProc) { DWORD ExitCode = 0; if (GetExitCodeProcess(hProc, &ExitCode) && ExitCode == STILL_ACTIVE) { Status = true; } CloseHandle(hProc); } else LgiTrace("%s:%i - OpenProcess failed with 0x%x\n", _FL, GetLastError()); #elif defined(MAC) #if LGI_COCOA #ifndef __OBJC__ #error "Change this source code to ObjC++" #endif auto app = [NSRunningApplication runningApplicationWithProcessIdentifier: Pid]; return app != nil; #elif LGI_CARBON ProcessSerialNumber psn; OSStatus e = GetProcessForPID(Pid, &psn); return e == 0; #else #warning FIXME #endif #elif defined(LINUX) char ProcPath[256]; sprintf_s(ProcPath, sizeof(ProcPath), "/proc/%i", Pid); Status = LDirExists(ProcPath); #elif defined HAIKU BRoster r; app_info a; if (r.GetRunningAppInfo(Pid, &a) == B_OK) { Status = true; } #else #error Impl me. #endif return Status; } diff --git a/src/common/Net/Ftp.cpp b/src/common/Net/Ftp.cpp --- a/src/common/Net/Ftp.cpp +++ b/src/common/Net/Ftp.cpp @@ -1,1318 +1,1318 @@ #include #include #include #include #include "lgi/common/Gdc2.h" #include "lgi/common/Ftp.h" #include "lgi/common/LgiString.h" #include "lgi/common/LgiCommon.h" #include "lgi/common/OpenSSLSocket.h" #include "lgi/common/Variant.h" #include "FtpListParser.cpp" /////////////////////////////////////////////////////////////////// enum FtpTransferMode { FT_Unknown, FT_Ascii, FT_Binary }; struct IFtpPrivate { char OutBuf[256] = ""; char InBuf[256] = ""; List In; FtpTransferMode CurMode = FT_Unknown; LAutoPtr Listen; // listen for data LAutoPtr Data; // get data LAutoPtr F; LString Charset = DefaultFtpCharset; LString ErrBuf; IFtpCallback *Callback = NULL; bool UseTLS = false; LString Host; int Port = 0; IFtpPrivate(IFtpCallback *cb) : Callback(cb) { } }; /////////////////////////////////////////////////////////////////// int LookupMonth(char *m) { for (int i=0; i<12; i++) { if (m && _stricmp(m, LDateTime::MonthsShort[i]) == 0) return i + 1; } return 1; } /////////////////////////////////////////////////////////////////// IFtpEntry::IFtpEntry() { Attributes = 0; Size = 0; UserData = NULL; } IFtpEntry::IFtpEntry(IFtpEntry *Entry) { UserData = NULL; if (Entry) *this = *Entry; } bool IFtpEntry::PermissionsFromStr(const char *perm) { if (!perm) return false; if (strlen(perm) < 10) return false; Perms.IsWindows = false; Perms.u32 = 0; if (perm[0] == 'd' || perm[0] == 'l') Attributes |= IFTP_DIR; Perms.Unix.UserRead = perm[1] != '-'; Perms.Unix.UserWrite = perm[2] != '-'; Perms.Unix.UserExecute = perm[3] != '-'; Perms.Unix.GroupRead = perm[4] != '-'; Perms.Unix.GroupWrite = perm[5] != '-'; Perms.Unix.GroupExecute = perm[6] != '-'; Perms.Unix.GlobalRead = perm[7] != '-'; Perms.Unix.GlobalWrite = perm[8] != '-'; Perms.Unix.GlobalExecute = perm[9] != '-'; if (perm[0] == 'l') { // link... Attributes |= IFTP_SYM_LINK; if (Name) { ssize_t Arrow = Name.Find("->"); if (Arrow > 0) Name.Length(Arrow-1); } } return true; } IFtpEntry::IFtpEntry(struct ftpparse *Fp, const char *Cs) { UserData = NULL; Attributes = 0; Size = 0; if (Fp) { Name.Set(Fp->name, Fp->namelen); Size = (Fp->sizetype == FTPPARSE_SIZE_BINARY) ? Fp->size : -1; if (Fp->mtimetype != FTPPARSE_MTIME_UNKNOWN) { #ifdef _MSC_VER struct tm local_val, *local = &local_val; auto err = localtime_s(local, &Fp->mtime); #else struct tm *local = localtime(&Fp->mtime); #endif if (local) Date = local; } PermissionsFromStr(Fp->perms); } } IFtpEntry::IFtpEntry(char *Entry, const char *Cs) { UserData = NULL; Attributes = 0; Size = 0; if (Entry) { const char *Ws = " \t"; auto T = LString(Entry).SplitDelimit(Ws); if (T.Length() > 0) { char *_Size = 0, *_Perm = 0; int _Name = 0; if (isdigit(*T[0])) { // M$ format _Size = T[2]; if (_stricmp(_Size, "") == 0) { Attributes |= IFTP_DIR; } _Name = 3; Date.SetDate(T[0]); Date.SetTime(T[1]); } else { // Unix format int SizeElement = T.Length() > 4 && isdigit(*T[4]) ? 4 : 3; _Size = T.Length() > SizeElement ? T[SizeElement] : NULL; _Name = (int)T.Length() - 1; _Perm = T[0]; if (T.Length() > SizeElement-2) User = T[SizeElement-2]; if (T.Length() > SizeElement-1) Group = T[SizeElement-1]; char *MonthStr = T.Length() > SizeElement + 1 ? T[SizeElement+1] : NULL; char *YearOrTime = T.Length() > SizeElement + 3 ? T[SizeElement+3] : NULL; if (MonthStr && YearOrTime) { int Day = T.Length() > SizeElement+2 ? atoi(T[SizeElement+2]) : -1; int Month = LookupMonth(MonthStr); int Year = 0; if (strchr(YearOrTime, ':')) { // It's time Date.SetTime(YearOrTime); // Default the year #if defined(WIN32) && !defined(PLATFORM_MINGW) __time64_t now = _time64(NULL); struct tm ptm; errno_t err = _localtime64_s(&ptm, &now); if (err == 0) Year = 1900 + ptm.tm_year; #else time_t now = time(NULL); tm *ptm = localtime(&now); if (ptm) Year = 1900 + ptm->tm_year; #endif } else { // It's a year Year = atoi(YearOrTime); } char DateStr[64]; sprintf_s(DateStr, sizeof(DateStr), "%i/%i/%i", Day, Month, Year); Date.SetDate(DateStr); } } if (_Name) { char *n = Entry; for (int i=0; i<_Name; i++) { for (; *n && !strchr(Ws, *n); n++); for (; *n && strchr(Ws, *n); n++); } LAutoString Utf((char*) LNewConvertCp("utf-8", n, Cs)); Name = Utf.Get(); } if (_Perm && strlen(_Perm) == 10) PermissionsFromStr(_Perm); if (_Size) { Size = atoi64(_Size); } } } } IFtpEntry::~IFtpEntry() { } IFtpEntry &IFtpEntry::operator =(const IFtpEntry &e) { Attributes = e.Attributes; Perms = e.Perms; Size = e.Size; Date = e.Date; UserData = e.UserData; // Make copies for thread safety Name = e.Name.Get(); Path = e.Path.Get(); User = e.User.Get(); Group = e.Group.Get(); return *this; } /////////////////////////////////////////////////////////////////// // #define VERIFY(i, ret) if (i != ret) { Close(); return 0; } // #define VERIFY_RANGE(i, range) if (((i)/100) != range) { Close(); return 0; } #define Verify(i, ret) { ssize_t Code = i; if (Code != (ret)) throw Code; } #define VerifyRange(i, range) { ssize_t Code = i; if ((Code/100) != range) throw Code; } IFtp::IFtp(IFtpCallback *cb, bool useTLS) { d = new IFtpPrivate(cb); d->UseTLS = useTLS; } IFtp::~IFtp() { DeleteObj(d); } const char *IFtp::GetError() { return d->ErrBuf; } char *IFtp::ToFtpCs(const char *s) { return (char*) LNewConvertCp(GetCharset(), s, "utf-8"); } char *IFtp::FromFtpCs(const char *s) { return (char*) LNewConvertCp("utf-8", s, GetCharset()); } const char *IFtp::GetCharset() { return d->Charset; } void IFtp::SetCharset(const char *cs) { d->Charset = cs ? cs : DefaultFtpCharset; } ssize_t IFtp::WriteLine(char *Msg) { ssize_t Status = 0; if (IsOpen()) { char *b = Msg ? Msg : d->OutBuf; size_t l = strlen(b); Status = Socket->Write(b, l, 0); } return Status; } ssize_t IFtp::ReadLine(char *Msg, ssize_t MsgSize) { ssize_t Status = 0; while (Socket) { // look through the input list for result codes char *s; auto It = d->In.begin(); while ((s = *It)) { int i = 0; if (isdigit(*s)) { char *e = s; while (isdigit(*e)) e++; if (*e != '-') { i = atoi(s); if (Msg) { char *Sp = strchr(s, ' '); if (Sp) { strcpy_s(Msg, MsgSize, Sp + 1); } } } } d->In.Delete(It); d->ErrBuf.Empty(); if (i) { return i; } } // Ok, no result code in the input list so // Read data off socket ssize_t Len = strlen(d->InBuf); ssize_t Read = 0; if (IsOpen()) { Read = Socket->Read(d->InBuf+Len, sizeof(d->InBuf)-Len-1, 0); } if (Read > 0) { // process input into lines char *Eol = 0; d->InBuf[Len+Read] = 0; do { Eol = strstr(d->InBuf, "\r\n"); if (Eol) { // copy line into input list char *New = 0; d->In.Insert(New = NewStr(d->InBuf, Eol-d->InBuf)); Eol += 2; // move data after line up to the start of the buffer ssize_t EolLen = strlen(Eol); if (EolLen > 0) { memmove(d->InBuf, Eol, EolLen+1); } else { d->InBuf[0] = 0; Eol = 0; } } } while (Eol); } else break; // the read failed.... } // loop around to check results return Status; } bool IFtp::IsOpen() { return Socket ? Socket->IsOpen() : false; } void IFtp::GetHost(LString *Host, int *Port) { *Host = d->Host.Get(); *Port = d->Port; } FtpOpenStatus IFtp::Open(LSocketI *S, char *RemoteHost, int Port, char *User, char *Password) { try { SslSocket *SslSock = dynamic_cast(S); if (d->UseTLS && !SslSock) { LAssert(!"Must use SslSocket for TLS based connections."); return FO_Error; } Socket.Reset(S); if (S->GetTimeout() < 0) S->SetTimeout(15 * 1000); Authenticated = false; if (!Port) Port = 21; d->Host = RemoteHost; d->Port = Port; if (!Socket || !Socket->Open(RemoteHost, Port)) return FO_ConnectFailed; if (d->Callback) d->Callback->OnSocketConnect(); Verify(ReadLine(), 220); if (d->UseTLS) { // https://datatracker.ietf.org/doc/html/rfc4217 // https://datatracker.ietf.org/doc/html/rfc2228 sprintf_s(d->OutBuf, sizeof(d->OutBuf), "AUTH TLS\r\n"); if (!WriteLine()) return FO_Error; auto Result = ReadLine(); if (Result != 234) return FO_Error; // Switch on TLS LVariant v = "ssl"; auto IsSsl = SslSock->SetValue(LSocket_Protocol, v); if (!IsSsl) return FO_Error; // Protection buffer size... sprintf_s(d->OutBuf, sizeof(d->OutBuf), "PBSZ 0\r\n"); if (!WriteLine()) return FO_Error; Verify(ReadLine(), 200); // Protected data transfers... sprintf_s(d->OutBuf, sizeof(d->OutBuf), "PROT P\r\n"); if (!WriteLine()) return FO_Error; Verify(ReadLine(), 200); } if (User) { // User/pass sprintf_s(d->OutBuf, sizeof(d->OutBuf), "USER %s\r\n", User); if (!WriteLine()) return FO_Error; Verify(ReadLine(), 331); sprintf_s(d->OutBuf, sizeof(d->OutBuf), "PASS %s\r\n", ValidStr(Password) ? Password : (char*)""); if (!WriteLine()) return FO_Error; Verify(ReadLine(), 230); Authenticated = true; } else { // Anonymous sprintf_s(d->OutBuf, sizeof(d->OutBuf), "USER anonymous\r\n"); if (WriteLine()) { ssize_t Code = ReadLine(); if (Code / 100 > 3) throw Code; sprintf_s(d->OutBuf, sizeof(d->OutBuf), "PASS me@privacy.net\r\n"); // garrenteed to never // be allocated to an IP if (WriteLine()) { Verify(ReadLine(), 230); Authenticated = true; } } } #if 0 sprintf_s(d->OutBuf, sizeof(d->OutBuf), "FEAT\r\n"); if (WriteLine()) { int r = ReadLine(); if (r / 100 == 2) { // Is utf8 there? // FIXME } } #endif if (!Authenticated) return FO_LoginFailed; return FO_Connected; } catch (ssize_t Error) { LgiTrace("%s:%i - Error: " LPrintfSSizeT "\n", _FL, Error); return FO_Error; } } bool IFtp::Close() { if (d->F) d->F->Close(); if (d->Data) d->Data->Close(); if (d->Listen) d->Listen->Close(); if (Socket) Socket->Close(); return true; } void IFtp::Noop() { if (IsOpen()) { sprintf_s(d->OutBuf, sizeof(d->OutBuf), "NOOP\r\n"); if (WriteLine()) { ReadLine(); } } } LString IFtp::GetDir() { LString p; try { if (IsOpen()) { char Temp[256]; sprintf_s(d->OutBuf, sizeof(d->OutBuf), "PWD\r\n"); WriteLine(); Verify(ReadLine(Temp, sizeof(Temp)), 257); char *Start = strchr(Temp, '\"'); char *End = (Start) ? strchr(Start+1, '\"') : 0; if (End) { *End = 0; p = Start + 1; } } } catch (ssize_t Error) { LgiTrace("%s:%i - error: " LPrintfSSizeT "\n", _FL, Error); if (IsOpen()) { LAssert(0); } } return p; } bool IFtp::SetDir(const char *Dir) { bool Status = false; try { if (Dir && IsOpen()) { char *f = ToFtpCs(Dir); if (f) { sprintf_s(d->OutBuf, sizeof(d->OutBuf), "CWD %s\r\n", f); WriteLine(); Verify(ReadLine(), 250); Status = true; DeleteArray(f); } } } catch (ssize_t Error) { LgiTrace("%s:%i - error: " LPrintfSSizeT "\n", _FL, Error); } return Status; } bool IFtp::CreateDir(const char *Dir) { bool Status = false; try { if (IsOpen() && Dir) { char *f = ToFtpCs(Dir); if (f) { sprintf_s(d->OutBuf, sizeof(d->OutBuf), "MKD %s\r\n", f); WriteLine(); VerifyRange(ReadLine(), 2); Status = true; DeleteArray(f); } } } catch (ssize_t Error) { printf("%s:%i - error: " LPrintfSSizeT "\n", _FL, Error); if (IsOpen()) { LAssert(0); } } return Status; } bool IFtp::DeleteDir(const char *Dir) { bool Status = false; if (IsOpen() && Dir) { char *f = ToFtpCs(Dir); if (f) { sprintf_s(d->OutBuf, sizeof(d->OutBuf), "RMD %s\r\n", f); WriteLine(); VerifyRange(ReadLine(), 2); Status = true; DeleteArray(f); } } return Status; } bool IFtp::UpDir() { bool Status = false; try { if (IsOpen()) { sprintf_s(d->OutBuf, sizeof(d->OutBuf), "CDUP\r\n"); WriteLine(); VerifyRange(ReadLine(0), 2); Status = true; } } catch (ssize_t Error) { printf("%s:%i - error: " LPrintfSSizeT "\n", _FL, Error); if (IsOpen()) { LAssert(0); } } return Status; } // #include "LgiCommon.h" // #include #ifdef _INC_MALLOC #define IsHeapOk() (_heapchk() != _HEAPOK) #else #define IsHeapOk() (-1) #endif bool IFtp::ListDir(LArray &Dir) { bool Status = false; try { if (IsOpen() && SetupData(true)) { LStringPipe Buf; // List command strcpy_s(d->OutBuf, sizeof(d->OutBuf), "LIST"); if (LongList || ShowHidden) strcat(d->OutBuf, " -"); if (LongList) strcat(d->OutBuf, "l"); if (ShowHidden) strcat(d->OutBuf, "a"); strcat(d->OutBuf, "\r\n"); WriteLine(); // Accept the data connection if (ConnectData()) { VerifyRange(ReadLine(), 1); if (Meter) { Meter->SetRange(0); Meter->Value(0); } // Read the data uchar Buffer[1024]; ssize_t Len; while ((Len = d->Data->Read((char*) Buffer, sizeof(Buffer), 0)) > 0) { if (Meter) { Meter->Value(Buf.GetSize()); } Buf.Write(Buffer, Len); } if (Meter) { Meter->Value(0); } Status = true; } d->Data.Reset(); d->Listen.Reset(); // Parse the results - LString Text = Buf.NewGStr(); + LString Text = Buf.NewLStr(); if (Status) { if (Text) { #ifdef LOG_LISTINGS LFile F; if (F.Open("c:\\temp\\ftplog.txt", O_WRITE)) { F.Write(Text, Len); F.Close(); } #endif LString::Array Ln = Text.Split("\n"); // Parse lines... for (unsigned i=0; iName) { if (strcmp(e->Name, ".") != 0 && strcmp(e->Name, "..") != 0) { Dir.Add(e); e = 0; } } DeleteObj(e); } VerifyRange(ReadLine(), 2); Status = true; } else { VerifyRange(ReadLine(), 2); Status = true; } } } else printf("%s:%i - SetupData failed.\n", _FL); } catch (ssize_t Error) { printf("%s:%i - error: " LPrintfSSizeT "\n", _FL, Error); d->Data.Reset(); d->Listen.Reset(); } return Status; } bool IFtp::RenameFile(const char *From, const char *To) { bool Status = false; try { if (IsOpen() && From && To) { char *f = ToFtpCs(From); char *t = ToFtpCs(To); if (f && t) { // From sprintf_s(d->OutBuf, sizeof(d->OutBuf), "RNFR %s\r\n", f); WriteLine(); VerifyRange(ReadLine(), 3); // To sprintf_s(d->OutBuf, sizeof(d->OutBuf), "RNTO %s\r\n", t); WriteLine(); VerifyRange(ReadLine(), 2); Status = true; } DeleteArray(f); DeleteArray(t); } } catch (ssize_t Error) { printf("%s:%i - error: " LPrintfSSizeT "\n", _FL, Error); if (IsOpen()) { LAssert(0); } } return Status; } bool IFtp::DeleteFile(const char *Remote) { bool Status = false; if (IsOpen() && Remote) { char *f = ToFtpCs(Remote); if (f) { sprintf_s(d->OutBuf, sizeof(d->OutBuf), "DELE %s\r\n", f); WriteLine(); VerifyRange(ReadLine(), 2); Status = true; DeleteArray(f); } } return Status; } bool IFtp::DownloadFile(const char *Local, IFtpEntry *Remote, bool Binary) { if (Remote && Remote->Name) { return TransferFile(Local, Remote->Name, Remote->Size, false, Binary); } return false; } bool IFtp::UploadFile(const char *Local, const char *Remote, bool Binary) { return TransferFile(Local, Remote, LFileSize(Local), true, Binary); } bool IFtp::ResumeAt(int64 Pos) { sprintf_s(d->OutBuf, sizeof(d->OutBuf), "REST " LPrintfInt64 "\r\n", Pos); WriteLine(); ssize_t FtpStatus = ReadLine(); bool Status = (FtpStatus/100) < 4; RestorePos = (Status) ? Pos : 0; return Status; } bool IFtp::TransferFile(const char *Local, const char *Remote, int64 Size, bool Upload, bool Binary) { bool Status = false; bool Aborted = false; d->F.Reset(new LFile); try { if (d->F && Socket && Local && Remote) { if (SetupData(Binary)) { // Data command char *f = ToFtpCs(Remote); sprintf_s(d->OutBuf, sizeof(d->OutBuf), "%s %s\r\n", (Upload)?"STOR":"RETR", f ? f : Remote); WriteLine(); DeleteArray(f); // Build data connection if (ConnectData()) { ssize_t Result = ReadLine(); ssize_t Range = Result / 100; if (Range != 1 && Range != 2) throw Result; if (!d->F->Open(Local, (Upload)?O_READ:O_WRITE)) { // couldn't open file... char Str[256]; sprintf_s(Str, sizeof(Str), "Error: Couldn't open '%s' for %s", Local, (Upload)?"reading":"writing"); Socket->OnInformation(Str); } else { // do the transfer int TempLen = 64 << 10; uchar *Temp = new uchar[TempLen]; if (Temp) { int64 Processed = 0; ssize_t Len = 0; if (Meter) Meter->SetRange(Size); if (!Upload) Size -= RestorePos; AbortTransfer = false; bool Error = false; while ( Socket && d->Data && (Size < 0 || Processed < Size) && !Error && !AbortTransfer && (!Meter || !Meter->IsCancelled())) { if (Meter) { Meter->Value(0); } if (Upload) { // upload loop d->Data->SetTimeout(15 * 1000); do { Len = d->F->Read(Temp, TempLen); for (ssize_t i=0; iData) WriteLen = d->Data->Write((char*) Temp, Len, 0); else break; // LgiTrace("Transfer %s...\n", LFormatSize(WriteLen).Get()); if (WriteLen > 0) { Processed += WriteLen; i += WriteLen; if (Meter) Meter->Value(Meter->Value() + WriteLen); } else { printf("%s:%i - Data->Write failed, %i of %i bytes written.\n", _FL, (int)WriteLen, (int)Len); Error = true; break; } } if (Meter && Meter->IsCancelled()) { printf("%s:%i - Upload canceled.\n", _FL); break; } } while (Len > 0 && !AbortTransfer && !Error); } else { // restore support d->F->Seek(RestorePos, SEEK_SET); if (Meter && RestorePos > 0) { // Meter->SetParameter(10, (int)RestorePos); LVariant v; Meter->SetValue("RestorePos", v = RestorePos); } RestorePos = 0; // download loop while (d->Data) { if (d->Data) { Len = d->Data->Read((char*) Temp, TempLen, 0); } if (Len <= 0 || AbortTransfer) { break; } d->F->Write(Temp, Len); Processed += Len; if (Meter) { Meter->Value(Meter->Value() + Len); if (Meter->IsCancelled()) break; } } if (Len <= 0) Error = true; } } if (AbortTransfer || (Meter && Meter->IsCancelled())) { // send abort command sprintf_s(d->OutBuf, sizeof(d->OutBuf), "ABOR\r\n"); WriteLine(); if ((ReadLine()/100) == 4) // temp msg { // response... we don't care just read it ReadLine(); } Aborted = true; } if (d->Data) d->Data->Close(); if (Size < 0) Status = true; else Status = Size == Processed; if (Meter) Meter->Value(0); DeleteArray(Temp); } // read eof response if (!Aborted) { VerifyRange(ReadLine(), 2); } } } d->Data.Reset(); d->Listen.Reset(); } } } catch (ssize_t Error) { if (IsOpen()) { d->ErrBuf.Printf("%s:%i - TransferFile(%s) error: " LPrintfSSizeT "\n", _FL, Local, Error); } Status = false; } d->F.Reset(); return Status; } bool IFtp::SetupData(bool Binary) { bool Status = false; try { d->Data.Reset(); if (IsOpen()) { if (Socket) { auto nstream = Socket->Clone(); auto nsock = dynamic_cast(nstream); LAssert(nsock != NULL); d->Data.Reset(nsock); } else { if (d->Data.Reset(new LSocket())) d->Data->SetCancel(Socket->GetCancel()); } } if (!d->Data) { d->ErrBuf.Printf("No data socket, Socket=%p.", Socket.Get()); LgiTrace("%s:%i - %s\n", _FL, d->ErrBuf.Get()); } else { PassiveMode = false; if ( (Binary && d->CurMode != FT_Binary) || (!Binary && d->CurMode != FT_Ascii)) { // Type command sprintf_s(d->OutBuf, sizeof(d->OutBuf), "TYPE %c\r\n", Binary ? 'I' : 'A'); if (WriteLine()) { VerifyRange(ReadLine(), 2); d->CurMode = Binary ? FT_Binary : FT_Ascii; } } if (!ForceActive) { // Try the PASV command first... it's better for // getting out of a firewall to the server sprintf_s(d->OutBuf, sizeof(d->OutBuf), "PASV\r\n"); WriteLine(); char Temp[256]; if (ReadLine(Temp, sizeof(Temp))/100 == 2) { // Ok we have PASV mode // grab the IP and Port char *Start = strchr(Temp, '('); if (Start) { Start++; char *End = strchr(Start, ')'); if (End) { *End = 0; auto T = LString(Start).SplitDelimit(","); if (T.Length() == 6) { sprintf_s(Ip, sizeof(Ip), "%s.%s.%s.%s", T[0].Get(), T[1].Get(), T[2].Get(), T[3].Get()); Port = (int) ((T[4].Int()<<8) | T[5].Int()); PassiveMode = true; return true; } } } } else { printf("\tread failed.\n"); return false; } } // Ok, no PASV so we use the PORT command // Port command // static int p = (4<<8) | 27; bool ListenStatus = false; d->Listen.Reset(new LSocket); // listen if (d->Listen) { int To = Socket->GetTimeout(); d->Listen->SetTimeout(To); if (d->Listen->Listen()) { Socket->GetLocalIp(Ip); // get current IP Port = d->Listen->GetLocalPort(); // get current port ListenStatus = true; } } if (ListenStatus) { // convert IP to PORT's comma delimited format for (char *p = Ip; *p; p++) { if (*p == '.') { *p = ','; } } sprintf_s(d->OutBuf, sizeof(d->OutBuf), "PORT %s,%i,%i\r\n", Ip, Port>>8, Port&0xFF); WriteLine(); VerifyRange(ReadLine(), 2); Status = true; } } } catch (ssize_t Error) { LgiTrace("%s:%i - error: " LPrintfSSizeT "\n", _FL, Error); } return Status; } bool IFtp::ConnectData() { if (PassiveMode) { return d->Data->Open(Ip, Port) != 0; } else { d->Data.Reset(Socket ? dynamic_cast(Socket->Clone()) : new LSocket()); if (d->Data) { int To = Socket->GetTimeout(); d->Data->SetTimeout(To); return d->Listen->Accept(d->Data); } } return false; } bool IFtp::SetPerms(const char *File, LPermissions Perms) { bool Status = false; if (Perms.IsWindows) { LAssert(!"Wrong perms type."); return false; } try { if (IsOpen() && File) { char *f = ToFtpCs(File); if (f) { sprintf_s(d->OutBuf, sizeof(d->OutBuf), "SITE CHMOD %03X %s\r\n", Perms.u32 & 0x777, File); WriteLine(); VerifyRange(ReadLine(), 2); Status = true; DeleteArray(f); } } } catch (ssize_t Error) { printf("%s:%i - error: " LPrintfSSizeT "\n", _FL, Error); if (IsOpen()) { LAssert(0); } } return Status; } diff --git a/src/common/Net/Http.cpp b/src/common/Net/Http.cpp --- a/src/common/Net/Http.cpp +++ b/src/common/Net/Http.cpp @@ -1,936 +1,936 @@ #include #include #include #include "lgi/common/Gdc2.h" #include "lgi/common/Http.h" #include "lgi/common/LgiCommon.h" #include "lgi/common/NetTools.h" #include "lgi/common/LgiString.h" #include "lgi/common/Base64.h" #include "lgi/common/Variant.h" #define DEBUG_LOGGING 0 /////////////////////////////////////////////////////////////////// class ILogProxy : public LSocketI { LSocketI *Dest; public: ILogProxy(LSocketI *dest) { Dest = dest; } void OnError(int ErrorCode, const char *ErrorDescription) { if (Dest && ErrorDescription) { char Str[256]; sprintf_s(Str, sizeof(Str), "[Data] %s", ErrorDescription); Dest->OnInformation(Str); } } void OnInformation(const char *s) { if (Dest && s) { char Str[256]; sprintf_s(Str, sizeof(Str), "[Data] %s", s); Dest->OnInformation(Str); } } void OnDisconnect() { if (Dest) { Dest->OnInformation("[Data] Disconnect"); } } }; /////////////////////////////////////////////////////////////////// LHttp::LHttp() { Meter = 0; ResumeFrom = 0; Proxy = 0; ProxyPort = 0; Headers = 0; NoCache = false; BufferLen = 16 << 10; Buffer = new char[BufferLen]; } LHttp::~LHttp() { Close(); DeleteArray(Proxy); DeleteArray(Headers); DeleteArray(Buffer); } void LHttp::SetProxy(char *p, int Port) { DeleteArray(Proxy); Proxy = NewStr(p); ProxyPort = Port; } void LHttp::SetAuth(char *User, char *Pass) { AuthUser = User; AuthPassword = Pass; } bool LHttp::Open(LAutoPtr S, const char *RemoteHost, int Port) { Close(); Socket = S; if (Proxy) { RemoteHost = Proxy; Port = ProxyPort; } if (RemoteHost) { LUri u; if (stristr(RemoteHost, "://") || strchr(RemoteHost, '/')) u.Set(RemoteHost); else u.sHost = RemoteHost; if (!u.Port && Port) u.Port = Port; if (!Socket) ErrorMsg = "No socket object."; else if (Socket->Open(u.sHost, u.Port > 0 ? u.Port : HTTP_PORT)) return true; else ErrorMsg = Socket->GetErrorString(); } else ErrorMsg = "No remote host."; if (ErrorMsg) LgiTrace("%s:%i - %s.\n", _FL, ErrorMsg.Get()); return false; } bool LHttp::Close() { if (Socket) Socket->Close(); return 0; } bool LHttp::IsOpen() { return Socket != NULL; } enum HttpRequestType { HttpNone, HttpGet, HttpPost, HttpOther, }; bool LHttp::Request ( const char *Type, const char *Uri, int *ProtocolStatus, const char *InHeaders, LStreamI *InBody, LStreamI *Out, LStreamI *OutHeaders, ContentEncoding *OutEncoding ) { // Input validation if (!Socket || !Uri || !Out || !Type) return false; #if DEBUG_LOGGING LStringPipe Log; #endif HttpRequestType ReqType = HttpNone; if (!_stricmp(Type, "GET")) ReqType = HttpGet; else if (!_stricmp(Type, "POST")) ReqType = HttpPost; else ReqType = HttpOther; // Generate the request string LStringPipe Cmd; LUri u(Uri); bool IsHTTPS = u.sProtocol && !_stricmp(u.sProtocol, "https"); LString EncPath = u.EncodeStr(u.sPath.Get() ? u.sPath.Get() : (char*)"/"), Mem; char s[1024]; LHtmlLinePrefix EndHeaders("\r\n"); LStringPipe Headers; if (IsHTTPS && Proxy) { Cmd.Print( "CONNECT %s:%i HTTP/1.1\r\n" "Host: %s\r\n" "\r\n", u.sHost.Get(), u.Port ? u.Port : HTTPS_PORT, u.sHost.Get()); LAutoString c(Cmd.NewStr()); size_t cLen = strlen(c); ssize_t r = Socket->Write(c, cLen); if (r == cLen) { ssize_t Length = 0; while (Out) { ssize_t r = Socket ? Socket->Read(s, sizeof(s)) : -1; if (r > 0) { ssize_t e = EndHeaders.IsEnd(s, r); if (e < 0) { Headers.Write(s, r); } else { e -= Length; Headers.Write(s, e); break; } Length += r; } else break; } LAutoString Hdr(Headers.NewStr()); LVariant v; if (Socket) Socket->SetValue(LSocket_Protocol, v = "SSL"); EndHeaders.Reset(); } else return false; } Cmd.Print("%s %s HTTP/1.1\r\n", Type, (Proxy && !IsHTTPS) ? Uri : EncPath.Get()); Cmd.Print("Host: %s\r\n", u.sHost.Get()); if (InHeaders) Cmd.Write(InHeaders, strlen(InHeaders)); if (AuthUser && AuthPassword) { if (1) { // Basic authentication char Raw[128]; sprintf_s(Raw, sizeof(Raw), "%s:%s", AuthUser.Get(), AuthPassword.Get()); char Base64[128]; ZeroObj(Base64); ConvertBinaryToBase64(Base64, sizeof(Base64)-1, (uchar*)Raw, strlen(Raw)); Cmd.Print("Authorization: Basic %s\r\n", Base64); } else { // Digest authentication // Not implemented yet... LAssert(!"Not impl."); } } Cmd.Push("\r\n"); - auto c = Cmd.NewGStr(); + auto c = Cmd.NewLStr(); #if DEBUG_LOGGING Log.Print("HTTP req.hdrs=%s\n-------------------------------------\nHTTP req.body=", c.Get()); #endif bool Status = false; if (Socket && c) { // Write the headers... bool WriteOk = Socket->Write(c, c.Length()) == c.Length(); if (WriteOk) { // Write any body... if (InBody) { ssize_t r; while ((r = InBody->Read(s, sizeof(s))) > 0) { ssize_t w = Socket->Write(s, r); if (w < r) { return false; } #if DEBUG_LOGGING Log.Print("%.*s", (int)w, s); #endif } } // Read the response ssize_t Total = 0; ssize_t Used = 0; while (Out) { ssize_t r = Socket ? Socket->Read(s, sizeof(s)) : -1; if (r > 0) { ssize_t e = EndHeaders.IsEnd(s, r); if (e < 0) { Headers.Write(s, r); } else { e -= Total; Headers.Write(s, e); if (r > e) { // Move the tailing data down to the start of the buffer memmove(s, s + e, r - e); Used = r - e; } break; } Total += r; } else break; } // Process output LAutoString h(Headers.NewStr()); if (h) { #if DEBUG_LOGGING Log.Print("HTTP res.hdrs=%s\n-------------------------------------\nHTTP res.body=", h); #endif LAutoString sContentLen(InetGetHeaderField(h, "Content-Length")); int64 ContentLen = sContentLen ? atoi64(sContentLen) : -1; bool IsChunked = false; if (ContentLen > 0) Out->SetSize(ContentLen); else { LAutoString sTransferEncoding(InetGetHeaderField(h, "Transfer-Encoding")); IsChunked = sTransferEncoding && !_stricmp(sTransferEncoding, "chunked"); Out->SetSize(0); } LAutoString sContentEncoding(InetGetHeaderField(h, "Content-Encoding")); ContentEncoding Encoding = EncodeRaw; if (sContentEncoding && !_stricmp(sContentEncoding, "gzip")) Encoding = EncodeGZip; if (OutEncoding) *OutEncoding = Encoding; int HttpStatus = 0; if (_strnicmp(h, "HTTP/", 5) == 0) { HttpStatus = atoi(h + 9); if (ProtocolStatus) *ProtocolStatus = HttpStatus; } if (HttpStatus / 100 == 3) { FileLocation.Reset(InetGetHeaderField(h, "Location")); } if (OutHeaders) { OutHeaders->Write(h, strlen(h)); } if (IsChunked) { #ifdef _DEBUG LStringPipe Log; #endif while (true) { // Try and get chunk header char *End = Strnstr(s, "\r\n", Used); if (!End) { ssize_t r = Socket->Read(s + Used, sizeof(s) - Used); if (r < 0) break; Used += r; End = Strnstr(s, "\r\n", Used); if (!End) { LAssert(!"No chunk header"); break; } } // Process the chunk header End += 2; ssize_t HdrLen = End - s; int ChunkSize = htoi(s); if (ChunkSize <= 0) { // End of stream. Status = true; break; } ssize_t ChunkDone = 0; memmove(s, End, Used - HdrLen); Used -= HdrLen; // Loop over the body of the chunk while (Socket && ChunkDone < ChunkSize) { ssize_t Remaining = ChunkSize - ChunkDone; ssize_t Common = MIN(Used, Remaining); #ifdef _DEBUG Log.Print("%s:%i - remaining:%i, common=%i\n", _FL, (int)Remaining, (int)Common); #endif if (Common > 0) { ssize_t w = Out->Write(s, Common); #ifdef _DEBUG Log.Print("%s:%i - w:%i\n", _FL, (int)w); #endif if (w != Common) { LAssert(!"Write failed."); break; } if (Used > Common) { #ifdef _DEBUG Log.Print("%s:%i - common:%i, used=%i\n", _FL, (int)Common, (int)Used); #endif memmove(s, s + Common, Used - Common); } ChunkDone += Common; Used -= Common; if (ChunkDone >= ChunkSize) { #ifdef _DEBUG Log.Print("%s:%i - chunkdone\n", _FL); #endif break; } } ssize_t r = Socket->Read(s + Used, sizeof(s) - Used); #ifdef _DEBUG Log.Print("%s:%i - r:%i\n", _FL, (int)r); #endif if (r < 0) break; Used += r; } // Loop over the CRLF postfix if (Socket && Used < 2) { ssize_t r = Socket->Read(s + Used, sizeof(s) - Used); #ifdef _DEBUG Log.Print("%s:%i - r:%i\n", _FL, (int)r); #endif if (r < 0) break; Used += r; } if (Used < 2 || s[0] != '\r' || s[1] != '\n') { #ifdef _DEBUG - LgiTrace("Log: %s\n", Log.NewGStr().Get()); + LgiTrace("Log: %s\n", Log.NewLStr().Get()); #endif LAssert(!"Post fix missing."); break; } if (Used > 2) memmove(s, s + 2, Used - 2); Used -= 2; } } else { // Non chunked connection. int64 Written = 0; if (Used > 0) { // auto Pos = Out->GetPos(); ssize_t w = Out->Write(s, Used); /* LgiTrace("%s:%i - Write @ " LPrintfInt64 " of " LPrintfSSizeT " = " LPrintfSSizeT " (%x,%x,%x,%x)\n", _FL, Pos, Used, w, (uint8_t)s[0], (uint8_t)s[1], (uint8_t)s[2], (uint8_t)s[3]); */ #if DEBUG_LOGGING Log.Write(s, w); #endif if (w == Used) { Written += w; Used = 0; } else { LAssert(0); Written = -1; } } while ( Socket && Written >= 0 && Written < ContentLen && Socket->IsOpen()) { ssize_t r = Socket->Read(s, sizeof(s)); if (r <= 0) { LgiTrace("%s:%i - Socket read failed.\n", _FL); break; } // auto Pos = Out->GetPos(); ssize_t w = Out->Write(s, r); /* auto NewPos = Out->GetPos(); LgiTrace("%s:%i - Write @ " LPrintfInt64 " of " LPrintfSSizeT " = " LPrintfSSizeT " (%x,%x,%x,%x) " LPrintfSSizeT "\n", _FL, Pos, r, w, (uint8_t)s[0], (uint8_t)s[1], (uint8_t)s[2], (uint8_t)s[3], NewPos); */ #if DEBUG_LOGGING Log.Print("%.*s", (int)w, s); #endif if (w != r) { LgiTrace("%s:%i - File write failed.\n", _FL); break; } Written += w; } Status = Written == ContentLen; if (Written != ContentLen) { LgiTrace("%s:%i - HTTP length not reached.\n", _FL); } #if DEBUG_LOGGING Log.Print("\n---------------------------------------------\n"); #endif } } } } #if DEBUG_LOGGING // if (ProtocolStatus && *ProtocolStatus >= 300) { - auto LogData = Log.NewGStr(); + auto LogData = Log.NewLStr(); if (LogData) { LgiTrace("%.*s\n", (int)LogData.Length(), LogData.Get()); int asd=0; } } #endif return Status; } #if 0 #define HTTP_POST_LOG 1 bool LHttp::Post ( char *File, const char *ContentType, LStreamI *In, int *ProtocolStatus, LStreamI *Out, LStreamI *OutHeaders, char *InHeaders ) { bool Status = false; if (Socket && File && Out) { // Read in the data part of the PUT command, because we need the // Content-Length to put in the headers. LStringPipe Content; while (In) { char s[1024]; int r = In->Read(s, sizeof(s)); if (r > 0) { Content.Write(s, r); } else break; } // Generate the request string LStringPipe Cmd; Cmd.Print("POST %s HTTP/1.0\r\n", File); if (InHeaders) Cmd.Print("%s", InHeaders); if (ContentType) Cmd.Print("Content-Type: %s\r\n", ContentType); Cmd.Print("Content-Length: %i\r\n\r\n", Content.GetSize()); char *s = Cmd.NewStr(); if (s) { #if HTTP_POST_LOG LgiTrace("IHTTP::Post write headers:\n%s\n\n", s); #endif // Write the headers... int SLen = strlen(s); bool WriteOk = Socket->Write(s, SLen) == SLen; DeleteArray(s); #if HTTP_POST_LOG LgiTrace("IHTTP::Post WriteOk=%i\n", WriteOk); #endif if (WriteOk) { // Copy all the 'Content' to the socket char Buf[1024]; while (true) { int r = Content.Read(Buf, sizeof(Buf)); #if HTTP_POST_LOG LgiTrace("IHTTP::Post Content.Read=%i\n", r); #endif if (r > 0) { int w = Socket->Write(Buf, r); #if HTTP_POST_LOG LgiTrace("IHTTP::Post Socket.Write=%i\n", w); #endif if (w <= 0) { break; } } else break; } #if HTTP_POST_LOG LgiTrace("IHTTP::Post Starting on response\n"); #endif // Read the response LHtmlLinePrefix EndHeaders("\r\n"); int Total = 0; LStringPipe Headers; while (Out) { int r = Socket->Read(Buf, sizeof(Buf)); #if HTTP_POST_LOG LgiTrace("IHTTP::Post Read=%i\n", r); #endif if (r > 0) { int e = EndHeaders.IsEnd(Buf, r); #if HTTP_POST_LOG LgiTrace("IHTTP::Post EndHeaders=%i\n", e); #endif if (e < 0) { Headers.Write(Buf, r); } else { e -= Total; Headers.Write(Buf, e); Out->Write(Buf + e, r - e); break; } Total += r; } else break; } // Process output char *h = Headers.NewStr(); #if HTTP_POST_LOG LgiTrace("IHTTP::Post Headers=\n%s\n\n", h); #endif if (h) { if (ProtocolStatus) { char *Eol = strchr(h, '\r'); if (Eol) { LToken Resp(h, " ", true, Eol-h); if (Resp.Length() > 1) *ProtocolStatus = atoi(Resp[1]); } } if (OutHeaders) { OutHeaders->Write(h, strlen(h)); } DeleteArray(h); while (Socket->IsOpen()) { int r = Socket->Read(Buf, sizeof(Buf)); #if HTTP_POST_LOG LgiTrace("IHTTP::Post ResponseBody.Read=%i\n", r); #endif if (r > 0) { Out->Write(Buf, r); } else break; } Status = true; } } } } return Status; } #endif /////////////////////////////////////////////////////////////////////////// #include "lgi/common/OpenSSLSocket.h" #include "lgi/common/Net.h" #define COMP_FUNCTIONS 1 #include "lgi/common/ZlibWrapper.h" void ZLibFree(voidpf opaque, voidpf address) { // Do nothing... the memory is owned by an autoptr } bool LgiGetUri(LCancel *Cancel, LStreamI *Out, LString *OutError, const char *InUri, const char *InHeaders, LUri *InProxy) { if (!InUri || !Out) { if (OutError) OutError->Printf("Parameter missing error (%p,%p)", InUri, Out); return false; } LHttp Http; int RedirectLimit = 10; LAutoString Location; for (int i=0; iIsCancelled()); i++) { LUri u(InUri); bool IsHTTPS = u.sProtocol && !_stricmp(u.sProtocol, "https"); int DefaultPort = IsHTTPS ? HTTPS_PORT : HTTP_PORT; if (InProxy) Http.SetProxy(InProxy->sHost, InProxy->Port ? InProxy->Port : DefaultPort); LAutoPtr s; if (IsHTTPS) { SslSocket *ssl; s.Reset(ssl = new SslSocket()); ssl->SetSslOnConnect(true); } else { s.Reset(new LSocket); } if (!s) { if (OutError) OutError->Printf("Alloc Failed"); return false; } if (Cancel) s->SetCancel(Cancel); s->SetTimeout(10 * 1000); if (!Http.Open(s, InUri, DefaultPort)) { if (OutError) OutError->Printf("Http open failed for '%s:%i'", InUri, DefaultPort); return false; } const char DefaultHeaders[] = "User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:39.0) Gecko/20100101 Firefox/39.0\r\n" "Accept: text/html,application/xhtml+xml,application/xml,image/png,image/*;q=0.9,*/*;q=0.8\r\n" "Accept-Language: en-US,en;q=0.5\r\n" "Accept-Encoding: gzip, deflate\r\n" "Connection: keep-alive\r\n"; LString InputHeaders; if (InHeaders) { auto Hdrs = LString(InHeaders).SplitDelimit("\r\n"); for (auto h: Hdrs) { LString s; s.Printf("%s\r\n", h.Get()); InputHeaders += s; } } else { InputHeaders = DefaultHeaders; } int Status = 0; LHttp::ContentEncoding Enc; LStringPipe OutHeaders; LStringPipe TmpFile(4 << 10); Http.Get(InUri, InHeaders ? InputHeaders : DefaultHeaders, &Status, &TmpFile, &Enc, &OutHeaders); int StatusCatagory = Status / 100; if (StatusCatagory == 3) { - LString Headers = OutHeaders.NewGStr(); + LString Headers = OutHeaders.NewLStr(); LgiTrace("Header=%s\n", Headers.Get()); LAutoString Loc(InetGetHeaderField(Headers, "Location", -1)); if (!Loc) { if (OutError) *OutError = "HTTP redirect doesn't have a location header."; return false; } if (!_stricmp(Loc, InUri)) { if (OutError) *OutError = "HTTP redirect to same URI."; return false; } LgiTrace("HTTP redir(%i) from '%s' to '%s'\n", i, InUri, Loc.Get()); LUri u(Loc); if (!u.sHost) { LUri in(InUri); in.sPath = u.sPath; Location.Reset(NewStr(in.ToString())); } else { Location = Loc; } InUri = Location; continue; } else if (StatusCatagory != 2) { - // auto Hdr = OutHeaders.NewGStr(); + // auto Hdr = OutHeaders.NewLStr(); Enc = LHttp::EncodeRaw; if (OutError) OutError->Printf("Got %i for '%.200s'\n", Status, InUri); return false; } Http.Close(); if (Enc == LHttp::EncodeRaw) { // Copy TmpFile to Out LCopyStreamer Cp; if (!Cp.Copy(&TmpFile, Out)) { if (OutError) *OutError = "Stream copy failed."; return false; } } else if (Enc == LHttp::EncodeGZip) { int64 Len = TmpFile.GetSize(); if (Len <= 0) { if (OutError) *OutError = "No data to ungzip."; return false; } LAutoPtr Data(new uchar[(size_t)Len]); if (!Data) { if (OutError) *OutError = "Alloc Failed"; return false; } TmpFile.Read(Data, (int)Len); LAutoPtr z; if (!z) z.Reset(new Zlib); else if (!z->IsLoaded()) z->Reload(); if (z && z->IsLoaded()) { z_stream Stream; ZeroObj(Stream); Stream.next_in = Data; Stream.avail_in = (uInt) Len; Stream.zfree = ZLibFree; int r = z->inflateInit2_(&Stream, 16+MAX_WBITS, ZLIB_VERSION, sizeof(z_stream)); uchar Buf[4 << 10]; Stream.next_out = Buf; Stream.avail_out = sizeof(Buf); while ( Stream.avail_in > 0 && (r = z->inflate(&Stream, Z_NO_FLUSH)) >= 0) { Out->Write(Buf, sizeof(Buf) - Stream.avail_out); Stream.next_out = Buf; Stream.avail_out = sizeof(Buf); } r = z->inflateEnd(&Stream); } else { GdcD->NeedsCapability("zlib"); if (OutError) OutError->Printf("Gzip decompression not available (needs %s)", z ? z->Name() : "zlib library"); return false; } } break; } return true; } diff --git a/src/common/Net/Mail.cpp b/src/common/Net/Mail.cpp --- a/src/common/Net/Mail.cpp +++ b/src/common/Net/Mail.cpp @@ -1,2709 +1,2709 @@ /*hdr ** FILE: Mail.cpp ** AUTHOR: Matthew Allen ** DATE: 28/5/98 ** DESCRIPTION: Mail app ** ** Copyright (C) 1998, Matthew Allen ** fret@memecode.com */ #include #include #include #include #include "lgi/common/Lgi.h" #include "lgi/common/Mail.h" #include "lgi/common/Base64.h" #include "lgi/common/NetTools.h" #include "lgi/common/DateTime.h" #include "lgi/common/DocView.h" #include "lgi/common/Store3Defs.h" #include "lgi/common/LgiRes.h" #include "lgi/common/TextConvert.h" #include "lgi/common/Mime.h" #include "../Hash/md5/md5.h" const char *sTextPlain = "text/plain"; const char *sTextHtml = "text/html"; const char *sTextXml = "text/xml"; const char *sApplicationInternetExplorer = "application/internet-explorer"; const char sMultipartMixed[] = "multipart/mixed"; const char sMultipartEncrypted[] = "multipart/encrypted"; const char sMultipartSigned[] = "multipart/signed"; const char sMultipartAlternative[] = "multipart/alternative"; const char sMultipartRelated[] = "multipart/related"; const char sAppOctetStream[] = "application/octet-stream"; ////////////////////////////////////////////////////////////////////////////////////////////////// LogEntry::LogEntry(LColour col) { c = col; } bool LogEntry::Add(const char *t, ssize_t len) { if (!t) return false; if (len < 0) len = strlen(t); /* // Strip off any whitespace on the end of the line. while (len > 0 && strchr(" \t\r\n", t[len-1])) len--; */ LAutoWString w(Utf8ToWide(t, len)); if (!w) return false; size_t ch = StrlenW(w); return Txt.Add(w, ch); } bool Base64Str(LString &s) { LString b64; ssize_t Base64Len = BufferLen_BinTo64(s.Length()); if (!b64.Set(NULL, Base64Len)) return false; #ifdef _DEBUG ssize_t Ch = #endif ConvertBinaryToBase64(b64.Get(), b64.Length(), (uchar*)s.Get(), s.Length()); LAssert(Ch == b64.Length()); s = b64; return true; } bool UnBase64Str(LString &s) { LString Bin; ssize_t BinLen = BufferLen_64ToBin(s.Length()); if (!Bin.Set(NULL, BinLen)) return false; ssize_t Ch = ConvertBase64ToBinary((uchar*)Bin.Get(), Bin.Length(), s.Get(), s.Length()); LAssert(Ch <= (int)Bin.Length()); s = Bin; s.Get()[Ch] = 0; return true; } ////////////////////////////////////////////////////////////////////////////////////////////////// // returns the maximum length of the lines contained in the string int MaxLineLen(char *Text) { if (!Text) return false; int Max = 0; int i = 0; for (char *c = Text; *c; c++) { if (*c == '\r') { // return } else if (*c == '\n') { // eol Max = MAX(i, Max); i = 0; } else { // normal char i++; } } return Max; } bool IsDotLined(char *Text) { if (Text) { for (char *l = Text; l && *l; ) { if (l[0] == '.') { if (l[1] == '\n' || l[1] == 0) { return true; } } l = strchr(l, '\n'); if (l) l++; } } return false; } // Is s a valid non-whitespace string? bool ValidNonWSStr(const char *s) { if (s && *s) { while (*s && strchr(" \r\t\n", *s)) { s++; } if (*s) { return true; } } return false; } void TokeniseStrList(char *Str, List &Output, const char *Delim) { if (Str && Delim) { char *s = Str; while (*s) { while (*s && strchr(WhiteSpace, *s)) s++; char *e = s; for (; *e; e++) { if (strchr("\'\"", *e)) { // handle string constant char delim = *e++; e = strchr(e, delim); } else if (*e == '<') { e = strchr(e, '>'); } else { while (*e && *e != '<' && !IsWhiteSpace(*e) && !strchr(Delim, *e)) e++; } if (!e || !*e || strchr(Delim, *e)) { break; } } ssize_t Len = e ? e - s : strlen(s); if (Len > 0) { char *Temp = new char[Len+1]; if (Temp) { memcpy(Temp, s, Len); Temp[Len] = 0; Output.Insert(Temp); } } if (e) { s = e; for (; *s && strchr(Delim, *s); s++); } else break; } } } //////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// void DeNullText(char *in, ssize_t &len) { char *out = in; char *end = in + len; while (in < end) { if (*in) { *out++ = *in; } else { len--; } in++; } } ////////////////////////////////////////////////////////////////////////////// typedef char CharPair[2]; static CharPair Pairs[] = { {'<', '>'}, {'(', ')'}, {'\'', '\''}, {'\"', '\"'}, {0, 0}, }; struct MailAddrPart { LAutoString Part; bool Brackets; bool ValidEmail; LAutoString RemovePairs(char *Str, ssize_t Len, CharPair *Pairs) { char *s = Str; if (Len < 0) Len = strlen(s); while (*s && strchr(WhiteSpace, *s)) { s++; Len--; } if (!*s) return LAutoString(); // Get the end of the string... char *e = s; if (Len < 0) e += strlen(s); else e += Len; // Seek back over any trailing whitespace while (e > s && strchr(WhiteSpace, e[-1])) e--; for (CharPair *p = Pairs; (*p)[0]; p++) { if ((*p)[0] == *s && (*p)[1] == e[-1]) { s++; e--; if (s < e) { // reset search p = Pairs - 1; } else break; } } Len = e - s; if (Len < 0) return LAutoString(); return LAutoString(NewStr(s, Len)); } MailAddrPart(char *s, ssize_t len) { ValidEmail = false; Brackets = false; if (s) { if (len < 0) len = strlen(s); while (strchr(WhiteSpace, *s) && len > 0) { s++; len--; } Brackets = *s == '<'; Part = RemovePairs(s, len, Pairs); // ValidEmail = IsValidEmail(Part); } } int Score() { if (!Part) return 0; return (ValidEmail ? 1 : 0) + (Brackets ? 1 : 0); } }; int PartCmp(LAutoPtr *a, LAutoPtr *b) { return (*b)->Score() - (*a)->Score(); } bool IsAngleBrackets(LString &s) { if (s(0) == '<' && s(-1) == '>') return true; return false; } void DecodeAddrName(const char *Str, std::function Cb, const char *DefaultDomain) { if (!Str) return; LString s = Str; LString non; LString email; LString::Array a; auto startBracket = s.Find("<"); auto endBracket = s.Find(">", startBracket); if (startBracket >= 0 && endBracket >= 0) { // Keep the angle brackets for the time being... a.New() = s(0, startBracket) + s(++endBracket, -1); a.New() = s(startBracket, endBracket); } else a.New() = s; for (unsigned i=0; i"); } else { non += a[i]; } } if (!email) { a = s.SplitDelimit("()"); non.Empty(); for (unsigned i=0; i 0) { const char *ChSet = " \t\r\n\'\"<>"; do { non = non.Strip(ChSet); } while (non.Length() > 0 && strchr(ChSet, non(0))); } Cb(non, email.Strip()); } void DecodeAddrName(const char *Start, LAutoString &Name, LAutoString &Addr, const char *DefaultDomain) { DecodeAddrName(Start, [&Name, &Addr](LString n, LString a){ Name.Reset(NewStr(n)); Addr.Reset(NewStr(a)); }, DefaultDomain); } void DecodeAddrName(const char *Start, LString &Name, LString &Addr, const char *DefaultDomain) { DecodeAddrName(Start, [&Name, &Addr](LString n, LString a){ Name = n; Addr = a; }, DefaultDomain); } #if 0 struct LDecodeAddrNameTest { LDecodeAddrNameTest() { // Testing code char *Input[] = { "\"Sound&Secure@speedytechnical.com\" ", "\"@MM-Social Mailman List\" ", "'Matthew Allen (fret)' ", "Matthew Allen (fret) ", "\"'Matthew Allen'\" ", "Matthew Allen", "fret@memecode.com", "\"\" ", " (fret@memecode.com)", "Matthew Allen ", "\"Matthew, Allen\" (fret@memecode.com)", "Matt'hew Allen ", "john.omalley ", "Bankers' Association (ABA)", "'Amy's Mum' ", "\"Philip Doggett (JIRA)\" ", "\"group name\" ", NULL }; LAutoString Name, Addr; for (char **i = Input; *i; i++) { Name.Reset(); Addr.Reset(); DecodeAddrName(*i, Name, Addr, "name.com"); LgiTrace("N=%-#32s A=%-32s\n", Name, Addr); } int asd=0; } } DecodeAddrNameTest; #endif void StrCopyToEOL(char *d, char *s) { if (d && s) { while (*s && *s != '\r' && *s != '\n') { *d++ = *s++; } *d = 0; } } ////////////////////////////////////////////////////////////////////////////////////////////////// MailTransaction::MailTransaction() { Index = -1; Flags = 0; Status = false; Oversize = false; Stream = 0; UserData = 0; } MailTransaction::~MailTransaction() { } ////////////////////////////////////////////////////////////////////////////////////////////////// FileDescriptor::FileDescriptor() { Embeded = 0; Offset = 0; Size = 0; Data = 0; MimeType = 0; ContentId = 0; Lock = 0; OwnEmbeded = false; } FileDescriptor::FileDescriptor(LStreamI *embed, int64 offset, int64 size, char *name) { Embeded = embed; Offset = offset; Size = size; Data = 0; MimeType = 0; Lock = 0; ContentId = 0; OwnEmbeded = false; if (name) { Name(name); } } FileDescriptor::FileDescriptor(char *name) { Embeded = 0; Offset = 0; Size = 0; Data = 0; MimeType = 0; Lock = 0; ContentId = 0; OwnEmbeded = false; if (name) { Name(name); if (File.Open(name, O_READ)) { Size = File.GetSize(); File.Close(); } } } FileDescriptor::FileDescriptor(char *data, int64 len) { Embeded = 0; Offset = 0; MimeType = 0; Lock = 0; ContentId = 0; Size = len; OwnEmbeded = false; Data = data ? new uchar[(size_t)Size] : 0; if (Data) { memcpy(Data, data, (size_t)Size); } } FileDescriptor::~FileDescriptor() { if (OwnEmbeded) { DeleteObj(Embeded); } DeleteArray(MimeType); DeleteArray(ContentId); DeleteArray(Data); } void FileDescriptor::SetOwnEmbeded(bool i) { OwnEmbeded = i; } void FileDescriptor::SetLock(LMutex *l) { Lock = l; } LMutex *FileDescriptor::GetLock() { return Lock; } LStreamI *FileDescriptor::GotoObject() { if (Embeded) { Embeded->SetPos(Offset); return Embeded; } else if (Name() && File.Open(Name(), O_READ)) { return &File; } else if (Data && Size > 0) { DataStream.Reset(new LMemStream(Data, Size, false)); return DataStream; } return 0; } int FileDescriptor::Sizeof() { return (int)Size; } uchar *FileDescriptor::GetData() { return Data; } bool FileDescriptor::Decode(char *ContentType, char *ContentTransferEncoding, char *MimeData, int MimeDataLen) { bool Status = false; int Content = CONTENT_NONE; if (ContentType && ContentTransferEncoding) { // Content-Type: application/octet-stream; name="Scribe.opt" Content = CONTENT_OCTET_STREAM; if (strnistr(ContentTransferEncoding, "base64", 1000)) { Content = CONTENT_BASE64; } if (strnistr(ContentTransferEncoding, "quoted-printable", 1000)) { Content = CONTENT_QUOTED_PRINTABLE; } if (Content != CONTENT_NONE) { const char *NameKey = "name"; char *n = strnistr(ContentType, NameKey, 1000); if (n) { char *Equal = strchr(n, '='); if (Equal) { Equal++; while (*Equal && *Equal == '\"') { Equal++; } char *End = strchr(Equal, '\"'); if (End) { *End = 0; } Name(Equal); Status = true; } } } } if (Status && MimeData && MimeDataLen > 0 && Content != CONTENT_NONE) { Status = false; char *Base64 = new char[MimeDataLen]; switch (Content) { case CONTENT_OCTET_STREAM: { Size = 0; DeleteObj(Data); Data = new uchar[MimeDataLen]; if (Data) { Size = MimeDataLen; memcpy(Data, MimeData, (size_t)Size); Status = true; } break; } case CONTENT_QUOTED_PRINTABLE: { Size = 0; DeleteObj(Data); Data = new uchar[MimeDataLen+1]; if (Data) { char *Out = (char*) Data; for (int i=0; i= Size - 3; if (Status) { Size = Converted; } else { DeleteArray(Data); Size = 0; } } break; } } } return Status; } ////////////////////////////////////////////////////////////////////////////////////////////////// AddressDescriptor::AddressDescriptor(const AddressDescriptor *Copy) { if (Copy) { Status = Copy->Status; CC = Copy->CC; sAddr = Copy->sAddr; sName = Copy->sName; } } AddressDescriptor::~AddressDescriptor() { _Delete(); } void AddressDescriptor::_Delete() { Status = false; CC = MAIL_ADDR_CC; sName.Empty(); sAddr.Empty(); } LString AddressDescriptor::Print() { LString s; char delim = '\''; if (sName) { bool hasSingle = sName.Find("\'") >= 0; bool hasDouble = sName.Find("\"") >= 0; if (hasSingle && !hasDouble) delim = '\"'; } if (sAddr && sName) s.Printf("%c%s%c <%s>", delim, sAddr.Get(), delim, sName.Get()); else if (sAddr) s.Printf("<%s>", sAddr.Get()); else if (sName) s.Printf("%c%s%c", delim, sName.Get(), delim); return s; } ////////////////////////////////////////////////////////////////////////////////////////////////// MailProtocol::MailProtocol() : SocketLock("MailProtocol") { Buffer[0] = 0; Logger = 0; ErrMsgId = 0; SettingStore = NULL; Items = 0; Transfer = 0; } MailProtocol::~MailProtocol() { CharsetPrefs.DeleteArrays(); } void MailProtocol::Log(const char *Str, LSocketI::SocketMsgType type) { if (Logger && Str) { char s[1024]; char *e = s + sizeof(s) - 2; const char *i = Str; char *o = s; while (*i && o < e) { *o++ = *i++; } while (o > s && (o[-1] == '\r' || o[-1] == '\n')) o--; *o++ = '\n'; *o = 0; Logger->Write(s, o - s, type); } } bool MailProtocol::Error(const char *file, int line, const char *msg, ...) { char s[1024]; va_list a; va_start(a, msg); vsprintf_s(s, sizeof(s), msg, a); va_end(a); Log(s, LSocketI::SocketMsgError); LgiTrace("%s:%i - Error: %s", file, line, s); return false; } bool MailProtocol::Read() { bool Status = false; if (Socket) { Status = Socket->Read(Buffer, sizeof(Buffer), 0) > 0; } return Status; } bool MailProtocol::Write(const char *Buf, bool LogWrite) { bool Status = false; if (Socket) { const char *p = Buf ? Buf : Buffer; Status = Socket->Write(p, strlen(p), 0) > 0; if (LogWrite) { Log(p, LSocketI::SocketMsgSend); } } return Status; } ////////////////////////////////////////////////////////////////////////////////////////////////// #define VERIFY_RET_VAL(Arg) \ { \ if (!Arg) \ { \ LMutex::Auto Lck(&SocketLock, _FL); \ Socket.Reset(0); \ return NULL; \ } \ } #define VERIFY_ONERR(Arg) \ { \ if (!Arg) \ { \ LMutex::Auto Lck(&SocketLock, _FL); \ Socket.Reset(0); \ goto CleanUp; \ } \ } void Reorder(LArray &a, const char *s) { for (unsigned i=0; i 0) { a.DeleteAt(i, true); a.AddAt(0, s); break; } } } MailSmtp::MailSmtp() { } MailSmtp::~MailSmtp() { } bool MailSmtp::Open(LSocketI *S, const char *RemoteHost, const char *LocalDomain, const char *UserName, const char *Password, int Port, int Flags) { char Str[256] = ""; bool Status = false; if (!RemoteHost) Error(_FL, "No remote SMTP host.\n"); else { strcpy_s(Str, sizeof(Str), RemoteHost); char *Colon = strchr(Str, ':'); if (Colon) { *Colon = 0; Colon++; Port = atoi(Colon); } if (Port == 0) { if (Flags & MAIL_SSL) Port = SMTP_SSL_PORT; else Port = SMTP_PORT; } LAutoString Server(TrimStr(Str)); if (Server) { if (SocketLock.Lock(_FL)) { Socket.Reset(S); SocketLock.Unlock(); } Socket->SetTimeout(30 * 1000); char Msg[256]; sprintf_s(Msg, sizeof(Msg), "Connecting to %s:%i...", Server.Get(), Port); Log(Msg, LSocketI::SocketMsgInfo); if (!Socket->Open(Server, Port)) Error(_FL, "Failed to connect socket to %s:%i\n", Server.Get(), Port); else { LStringPipe Str; // receive signon message VERIFY_RET_VAL(ReadReply("220")); // Rfc 2554 ESMTP authentication SmtpHello: sprintf_s(Buffer, sizeof(Buffer), "EHLO %s\r\n", (ValidNonWSStr(LocalDomain)) ? LocalDomain : "default"); VERIFY_RET_VAL(Write(0, true)); /*bool HasSmtpExtensions =*/ ReadReply("250", &Str); bool Authed = false; bool NoAuthTypes = false; bool SupportsStartTLS = false; LArray AuthTypes; // Look through the response for the auth line - LString Response = Str.NewGStr(); + LString Response = Str.NewLStr(); if (Response) { auto Lines = Response.SplitDelimit("\n"); for (auto &l: Lines) { char *AuthStr = stristr(l, "AUTH"); if (AuthStr) { // walk through AUTH types auto Types = LString(AuthStr + 4).SplitDelimit(" ,;"); for (auto &t: Types) AuthTypes.Add(t); } if (stristr(l, "STARTTLS")) SupportsStartTLS = true; } } if (SupportsStartTLS && TestFlag(Flags, MAIL_USE_STARTTLS)) { strcpy_s(Buffer, sizeof(Buffer), "STARTTLS\r\n"); VERIFY_RET_VAL(Write(0, true)); VERIFY_RET_VAL(ReadReply("220", &Str)); LVariant v; if (Socket->SetValue(LSocket_Protocol, v="SSL")) { Flags &= ~MAIL_USE_STARTTLS; goto SmtpHello; } else { // SSL init failed... what to do here? return false; } } if (TestFlag(Flags, MAIL_USE_AUTH)) { if (!ValidStr(UserName)) { // We need a user name in all authentication types. SetError(L_ERROR_ESMTP_NO_USERNAME, "No username for authentication."); return false; } if (AuthTypes.Length() == 0) { // No auth types? huh? if (TestFlag(Flags, MAIL_USE_PLAIN)) // Force plain type AuthTypes.Add("PLAIN"); else if (TestFlag(Flags, MAIL_USE_LOGIN)) // Force login type AuthTypes.Add("LOGIN"); else if (TestFlag(Flags, MAIL_USE_CRAM_MD5)) // Force CRAM MD5 type AuthTypes.Add("CRAM-MD5"); else if (TestFlag(Flags, MAIL_USE_OAUTH2)) // Force OAUTH2 type AuthTypes.Add("XOAUTH2"); else { // Try all AuthTypes.Add("PLAIN"); AuthTypes.Add("LOGIN"); AuthTypes.Add("CRAM-MD5"); AuthTypes.Add("XOAUTH2"); } } else { // Force user preference if (TestFlag(Flags, MAIL_USE_PLAIN)) Reorder(AuthTypes, "PLAIN"); else if (TestFlag(Flags, MAIL_USE_LOGIN)) Reorder(AuthTypes, "LOGIN"); else if (TestFlag(Flags, MAIL_USE_CRAM_MD5)) Reorder(AuthTypes, "CRAM-MD5"); else if (TestFlag(Flags, MAIL_USE_OAUTH2)) Reorder(AuthTypes, "XOAUTH2"); } for (auto Auth : AuthTypes) { // Try all their auth types against our internally support types if (Auth.Equals("LOGIN")) { VERIFY_RET_VAL(Write("AUTH LOGIN\r\n", true)); VERIFY_RET_VAL(ReadReply("334")); ZeroObj(Buffer); ConvertBinaryToBase64(Buffer, sizeof(Buffer), (uchar*)UserName, strlen(UserName)); strcat(Buffer, "\r\n"); VERIFY_RET_VAL(Write(0, true)); if (ReadReply("334") && Password) { ZeroObj(Buffer); ConvertBinaryToBase64(Buffer, sizeof(Buffer), (uchar*)Password, strlen(Password)); strcat(Buffer, "\r\n"); VERIFY_RET_VAL(Write(0, true)); if (ReadReply("235")) Authed = true; } } else if (Auth.Equals("PLAIN")) { char Ascii[512]; int ch = sprintf_s(Ascii, sizeof(Ascii), "%c%s%c%s", 0, UserName, 0, Password); char Base64[512] = {0}; ConvertBinaryToBase64(Base64, sizeof(Base64), (uint8_t*)Ascii, ch); sprintf_s(Buffer, sizeof(Buffer), "AUTH PLAIN %s\r\n", Base64); VERIFY_RET_VAL(Write(0, true)); if (ReadReply("235")) { Authed = true; } } else if (Auth.Equals("CRAM-MD5")) { sprintf_s(Buffer, sizeof(Buffer), "AUTH CRAM-MD5\r\n"); VERIFY_RET_VAL(Write(0, true)); if (ReadReply("334")) { auto Sp = strchr(Buffer, ' '); if (Sp) { Sp++; // Decode the server response: uint8_t Txt[128]; auto InLen = strlen(Sp); ssize_t TxtLen = ConvertBase64ToBinary(Txt, sizeof(Txt), Sp, InLen); // Calc the hash: // https://tools.ietf.org/html/rfc2104 char Key[64] = {0}; memcpy(Key, Password, MIN(strlen(Password), sizeof(Key))); uint8_t iKey[256]; char oKey[256]; for (unsigned i=0; i<64; i++) { iKey[i] = Key[i] ^ 0x36; oKey[i] = Key[i] ^ 0x5c; } memcpy(iKey+64, Txt, TxtLen); md5_state_t md5; md5_init(&md5); md5_append(&md5, iKey, 64 + TxtLen); md5_finish(&md5, oKey + 64); md5_init(&md5); md5_append(&md5, (uint8_t*)oKey, 64 + 16); char digest[16]; md5_finish(&md5, digest); char r[256]; int ch = sprintf_s(r, sizeof(r), "%s ", UserName); for (unsigned i=0; i<16; i++) ch += sprintf_s(r+ch, sizeof(r)-ch, "%02x", (uint8_t)digest[i]); // Base64 encode ssize_t Len = ConvertBinaryToBase64(Buffer, sizeof(Buffer), (uint8_t*)r, ch); Buffer[Len++] = '\r'; Buffer[Len++] = '\n'; Buffer[Len++] = 0; VERIFY_RET_VAL(Write(0, true)); if (ReadReply("235")) Authed = true; } } } else if (Auth.Equals("XOAUTH2")) { auto Log = dynamic_cast(Socket->GetLog()); LOAuth2 Authenticator(OAuth2, UserName, SettingStore, Socket->GetCancel(), Log); auto Tok = Authenticator.GetAccessToken(); if (Tok) { LString s; s.Printf("user=%s\001auth=Bearer %s\001\001\0", UserName, Tok.Get()); Base64Str(s); sprintf_s(Buffer, sizeof(Buffer), "AUTH %s %s\r\n", Auth.Get(), s.Get()); VERIFY_RET_VAL(Write(0, true)); Authed = ReadReply("235"); if (!Authed) { Authenticator.Refresh(); } } } else { LgiTrace("%s:%i - Unsupported auth type '%s'\n", _FL, Auth.Get()); } if (Authed) break; } if (!Authed) { if (NoAuthTypes) SetError(L_ERROR_ESMTP_NO_AUTHS, "The server didn't return the authentication methods it supports."); else { LString p; for (auto i : AuthTypes) { if (p.Get()) p += ", "; p += i; } SetError(L_ERROR_UNSUPPORTED_AUTH, "Authentication failed, types available:\n\t%s", p); } } Status = Authed; } else { Status = true; } } } } return Status; } bool MailSmtp::WriteText(const char *Str) { // we have to convert all strings to CRLF in here bool Status = false; if (Str) { LMemQueue Temp; const char *Start = Str; while (*Str) { if (*Str == '\n') { // send a string ssize_t Size = Str-Start; if (Str[-1] == '\r') Size--; Temp.Write((uchar*) Start, Size); Temp.Write((uchar*) "\r\n", 2); Start = Str + 1; } Str++; } // send the final string ssize_t Size = Str-Start; if (Str[-1] == '\r') Size--; Temp.Write((uchar*) Start, (int)Size); Size = (int)Temp.GetSize(); char *Data = new char[(size_t)Size]; if (Data) { Temp.Read((uchar*) Data, Size); Status = Socket->Write(Data, (int)Size, 0) == Size; DeleteArray(Data); } } return Status; } void StripChars(LString &s) { s = s.Strip("\r\n"); } char *CreateAddressTag(List &l, int Type, List *CharsetPrefs) { char *Result = 0; List Addr; for (auto a: l) { if (a->CC == Type) { Addr.Insert(a); } } if (Addr.Length() > 0) { LStringPipe StrBuf; StrBuf.Push((Type == 0) ? (char*)"To: " : (char*)"Cc: "); for (auto It = Addr.begin(); It != Addr.end(); ) { auto a = *It; AddressDescriptor *NextA = *(++It); char Buffer[256] = ""; StripChars(a->sName); StripChars(a->sAddr); if (a->sAddr && strchr(a->sAddr, ',')) { // Multiple address format auto t = a->sAddr.SplitDelimit(","); for (uint32_t i=0; i", t[i].Get()); if (i < t.Length()-1) strcat(Buffer, ",\r\n\t"); StrBuf.Push(Buffer); Buffer[0] = 0; } } else if (a->sName) { // Name and addr char *Mem = 0; char *Name = a->sName.Get(); if (Is8Bit(Name)) { Name = Mem = EncodeRfc2047(NewStr(Name), 0, CharsetPrefs); } if (strchr(Name, '\"')) sprintf_s(Buffer, sizeof(Buffer), "'%s' <%s>", Name, a->sAddr.Get()); else sprintf_s(Buffer, sizeof(Buffer), "\"%s\" <%s>", Name, a->sAddr.Get()); DeleteArray(Mem); } else if (a->sAddr) { // Just addr sprintf_s(Buffer, sizeof(Buffer), "<%s>", a->sAddr.Get()); } if (NextA) strcat(Buffer, ",\r\n\t"); StrBuf.Push(Buffer); a = NextA; } StrBuf.Push("\r\n"); Result = StrBuf.NewStr(); } return Result; } // This class implements a pipe that writes to a socket class SocketPipe : public LStringPipe { LSocketI *s; MailProtocolProgress *p; public: bool Status; SocketPipe(LSocketI *socket, MailProtocolProgress *progress) { s = socket; p = progress; Status = true; } ssize_t Read(void *Ptr, ssize_t Size, int Flags) { return false; } int64 SetSize(int64 Size) { if (p) { p->Start = LCurrentTime(); p->Range = (int)Size; return Size; } return -1; } ssize_t Write(const void *InPtr, ssize_t Size, int Flags) { char *Ptr = (char*)InPtr; char *e = Ptr + Size; while (Ptr < e) { ssize_t w = s->Write(Ptr, e - Ptr, 0); if (w > 0) { Ptr += w; if (p && p->Range && w > 0) p->Value += w; } else break; } return Ptr - (char*)InPtr; } }; bool MailSmtp::SendToFrom(List &To, AddressDescriptor *From, MailProtocolError *Err) { bool AddrOk = false; if (To.Length() == 0) { ErrMsgId = L_ERROR_ESMTP_NO_RECIPIENT; ErrMsgFmt = "No recipients to send to."; ErrMsgParam.Empty(); LgiTrace("%s:%i - No recipients.\n", _FL); return false; } // send MAIL message if (From && ValidStr(From->sAddr)) { sprintf_s(Buffer, sizeof(Buffer), "MAIL FROM: <%s>\r\n", From->sAddr.Get()); } else { ErrMsgId = L_ERROR_ESMTP_NO_FROM; ErrMsgFmt = "No 'from' address in email."; ErrMsgParam.Empty(); LgiTrace("%s:%i - Invalid from '%s'.\n", _FL, From->sAddr.Get()); return false; } VERIFY_RET_VAL(Write(0, true)); VERIFY_RET_VAL(ReadReply("250", 0, Err)); // send RCPT message AddrOk = true; List::I Recip = To.begin(); for (AddressDescriptor *a = *Recip; a; a = *++Recip) { LString Addr = ValidStr(a->sAddr) ? a->sAddr : a->sName; if (ValidStr(Addr)) { auto Parts = Addr.SplitDelimit(","); for (auto p: Parts) { sprintf_s(Buffer, sizeof(Buffer), "RCPT TO: <%s>\r\n", p.Get()); VERIFY_RET_VAL(Write(0, true)); a->Status = ReadReply("25", 0, Err); AddrOk |= a->Status != 0; // at least one address is ok } } else if (Err) { ErrMsgId = L_ERROR_ESMTP_BAD_RECIPIENT; ErrMsgFmt = "Invalid recipient '%s'."; ErrMsgParam = Addr; } } return AddrOk; } LStringPipe *MailSmtp::SendData(MailProtocolError *Err) { // send DATA message sprintf_s(Buffer, sizeof(Buffer), "DATA\r\n"); VERIFY_RET_VAL(Write(0, true)); VERIFY_RET_VAL(ReadReply("354", 0, Err)); return new SocketPipe(Socket, Transfer); } LStringPipe *MailSmtp::SendStart(List &To, AddressDescriptor *From, MailProtocolError *Err) { return SendToFrom(To, From, Err) ? SendData(Err) : NULL; } bool MailSmtp::SendEnd(LStringPipe *m) { bool Status = false; SocketPipe *Msg = dynamic_cast(m); if (Msg) { // send message terminator and receive reply if (Msg->Status && Msg->Write((void*)"\r\n.\r\n", 5, 0)) { Status = ReadReply("250"); } // else // just close the connection on them // so nothing gets sent } DeleteObj(m); return Status; } /* bool MailSmtp::Send(MailMessage *Msg, bool Mime) { bool Status = false; if (Socket && Msg) { LStringPipe *Sink = SendStart(Msg->To, Msg->From); if (Sink) { // setup a gui progress meter to send the email, // the length is just a guesstimate as we won't know the exact // size until we encode it all, and I don't want it hanging around // in memory at once, so we encode and send on the fly. int Length = 1024 + (Msg->GetBody() ? strlen(Msg->GetBody()) : 0); for (FileDescriptor *f=Msg->FileDesc.First(); f; f=Msg->FileDesc.Next()) { Length += f->Sizeof() * 4 / 3; } // encode and send message for transport Msg->Encode(*Sink, 0, this); Status = SendEnd(Sink); } } return Status; } */ bool MailSmtp::Close() { if (Socket) { // send QUIT message sprintf_s(Buffer, sizeof(Buffer), "QUIT\r\n"); VERIFY_RET_VAL(Write(0, true)); VERIFY_RET_VAL(ReadReply("221")); LMutex::Auto Lock(&SocketLock, _FL); Socket.Reset(0); return true; } return false; } bool MailSmtp::ReadReply(const char *Str, LStringPipe *Pipe, MailProtocolError *Err) { bool Status = false; if (Socket && Str) { ssize_t Pos = 0; char *Start = Buffer; ZeroObj(Buffer); while (Pos < sizeof(Buffer)) { ssize_t Len = Socket->Read(Buffer+Pos, sizeof(Buffer)-Pos, 0); if (Len > 0) { char *Eol = strstr(Start, "\r\n"); while (Eol) { // wipe EOL chars *Eol++ = 0; *Eol++ = 0; // process if (Pipe) { if (Pipe->GetSize()) Pipe->Push("\n"); Pipe->Push(Start); } if (Start[3] == ' ') { // end of response if (!strncmp(Start, Str, strlen(Str))) { Status = true; } if (Err) { Err->Code = atoi(Start); char *Sp = strchr(Start, ' '); Err->ErrMsg = Sp ? Sp + 1 : Start; } // Log Log(Start, atoi(Start) >= 400 ? LSocketI::SocketMsgError : LSocketI::SocketMsgReceive); // exit loop Pos = sizeof(Buffer); break; } else { Log(Start, LSocketI::SocketMsgReceive); // more lines follow Start = Eol; Eol = strstr(Start, "\r\n"); } } Pos += Len; } else break; } if (!Status) { SetError(L_ERROR_GENERIC, "Error: %s", Buffer); } } return Status; } ////////////////////////////////////////////////////////////////////////////////////////////////// class Mail2Folder : public LStringPipe { char File[256]; LFile F; public: Mail2Folder(char *Path, List &To) { do { char n[32]; sprintf_s(n, sizeof(n), "%u.mail", LRand()); LMakePath(File, sizeof(File), Path, n); } while (LFileExists(File)); if (F.Open(File, O_WRITE)) { F.Print("Forward-Path: "); int i = 0; for (auto a: To) { a->Status = true; auto Addrs = a->sAddr.SplitDelimit(","); for (unsigned n=0; n", Addrs[n].Get()); } } F.Print("\r\n"); } } ~Mail2Folder() { F.Close(); } ssize_t Read(void *Buffer, ssize_t Size, int Flags = 0) { return F.Read(Buffer, Size, Flags); } ssize_t Write(const void *Buffer, ssize_t Size, int Flags = 0) { return F.Write(Buffer, Size, Flags); } }; class MailPostFolderPrivate { public: char *Path; MailPostFolderPrivate() { Path = 0; } ~MailPostFolderPrivate() { DeleteArray(Path); } }; MailSendFolder::MailSendFolder(char *Path) { d = new MailPostFolderPrivate; d->Path = NewStr(Path); } MailSendFolder::~MailSendFolder() { DeleteObj(d); } bool MailSendFolder::Open(LSocketI *S, const char *RemoteHost, const char *LocalDomain, const char *UserName, const char *Password, int Port, int Flags) { return LDirExists(d->Path); } bool MailSendFolder::Close() { return true; } LStringPipe *MailSendFolder::SendStart(List &To, AddressDescriptor *From, MailProtocolError *Err) { return new Mail2Folder(d->Path, To); } bool MailSendFolder::SendEnd(LStringPipe *Sink) { DeleteObj(Sink); return true; } ////////////////////////////////////////////////////////////////////////////////////////////////// class MailItem { public: char *File; bool Delete; MailItem(char *f) { File = NewStr(f); Delete = false; } ~MailItem() { DeleteArray(File); } }; class MailReceiveFolderPrivate { public: char *Path; List Mail; MailReceiveFolderPrivate() { Path = 0; } ~MailReceiveFolderPrivate() { DeleteArray(Path); Mail.DeleteObjects(); } void Empty() { for (auto m: Mail) { if (m->Delete) { FileDev->Delete(m->File, false); } } Mail.DeleteObjects(); } }; MailReceiveFolder::MailReceiveFolder(char *Path) { d = new MailReceiveFolderPrivate; d->Path = NewStr(Path); } MailReceiveFolder::~MailReceiveFolder() { DeleteObj(d); } bool MailReceiveFolder::Open(LSocketI *S, const char *RemoteHost, int Port, const char *User, const char *Password, LDom *SettingStore, int Flags) { // We don't use the socket so just free it here... DeleteObj(S); // Argument check if (!LDirExists(d->Path)) return false; LDirectory Dir; // Loop through files, looking for email for (int b = Dir.First(d->Path, LGI_ALL_FILES); b; b = Dir.Next()) { if (!Dir.IsDir()) { if (MatchStr("*.eml", Dir.GetName()) || MatchStr("*.mail", Dir.GetName())) { char p[300]; Dir.Path(p, sizeof(p)); d->Mail.Insert(new MailItem(p)); } } } return true; } bool MailReceiveFolder::Close() { d->Empty(); return true; } ssize_t MailReceiveFolder::GetMessages() { return d->Mail.Length(); } bool MailReceiveFolder::Receive(LArray &Trans, MailCallbacks *Callbacks) { bool Status = false; for (unsigned i=0; iStream) { t->Status = false; MailItem *m = d->Mail[t->Index]; if (m) { LFile i; if (i.Open(m->File, O_READ)) { LCopyStreamer c; if (c.Copy(&i, t->Stream)) { Status = t->Status = true; if (Callbacks && Callbacks->OnReceive) { Callbacks->OnReceive(t, Callbacks->CallbackData); } } } } } } return Status; } bool MailReceiveFolder::Delete(int Message) { MailItem *m = d->Mail[Message]; if (m) { m->Delete = true; return false; } return false; } int MailReceiveFolder::Sizeof(int Message) { MailItem *m = d->Mail[Message]; if (m) { return (int)LFileSize(m->File); } return 0; } bool MailReceiveFolder::GetUid(int Message, char *Id, int IdLen) { if (Id) { MailItem *m = d->Mail[Message]; if (m) { char *s = strrchr(m->File, DIR_CHAR); if (s++) { char *e = strchr(s, '.'); if (!e) e = s + strlen(s); ssize_t Len = e - s; memcpy(Id, s, Len); Id[Len] = 0; return true; } } } return false; } bool MailReceiveFolder::GetUidList(LString::Array &Id) { bool Status = false; for (int i=0; iMail.Length(); i++) { char Uid[256]; if (GetUid(i, Uid, sizeof(Uid))) { Status = true; Id.New() = Uid; } else { Status = false; break; } } return Status; } LString MailReceiveFolder::GetHeaders(int Message) { MailItem *m = d->Mail[Message]; if (!m) return NULL; LFile i; if (!i.Open(m->File, O_READ)) return NULL; LStringPipe o; LCopyStreamer c; LHtmlLinePrefix e("", false); if (!c.Copy(&i, &o, &e)) return NULL; - return o.NewGStr(); + return o.NewLStr(); } ////////////////////////////////////////////////////////////////////////////////////////////////// MailPop3::MailPop3() { End = "\r\n.\r\n"; Marker = End; Messages = -1; } MailPop3::~MailPop3() { } ssize_t MailPop3::GetMessages() { if (Messages < 0) { if (Socket && Socket->IsOpen()) { // see how many messages there are VERIFY_ONERR(Write("STAT\r\n", true)); VERIFY_ONERR(ReadReply()); Messages = GetInt(); } else LAssert(!"No socket to get message count."); } CleanUp: return Messages; } int MailPop3::GetInt() { char Buf[32]; char *Start = strchr(Buffer, ' '); if (Start) { Start++; char *End = strchr(Start, ' '); if (End) { int Len = (int) (End - Start); memcpy(Buf, Start, Len); Buf[Len] = 0; return atoi(Buf); } } return 0; } bool MailPop3::ReadReply() { bool Status = false; if (Socket) { ssize_t Pos = 0; ZeroObj(Buffer); do { ssize_t Result = Socket->Read(Buffer+Pos, sizeof(Buffer)-Pos, 0); if (Result <= 0) // an error? { // Leave the loop... break; } Pos += Result; } while ( !strstr(Buffer, "\r\n") && sizeof(Buffer)-Pos > 0); Status = (Buffer[0] == '+') && strstr(Buffer, "\r\n"); char *Cr = strchr(Buffer, '\r'); if (Cr) *Cr = 0; if (ValidStr(Buffer)) Log(Buffer, (Status) ? LSocketI::SocketMsgReceive : LSocketI::SocketMsgError); if (Cr) *Cr = '\r'; if (!Status) { SetError(L_ERROR_GENERIC, "Error: %s", Buffer); } } return Status; } bool MailPop3::ListCmd(const char *Cmd, LHashTbl, bool> &Results) { sprintf_s(Buffer, sizeof(Buffer), "%s\r\n", Cmd); if (!Write(0, true)) return false; char *b = Buffer; ssize_t r; while ((r = Socket->Read(b, sizeof(Buffer)-(b-Buffer))) > 0) { b += r; if (Strnstr(Buffer, "\r\n.\r\n", b-Buffer)) break; } if (r <= 0) return false; auto Lines = LString(Buffer).SplitDelimit("\r\n"); for (unsigned i=1; iGetValue("IsSSL", IsSsl) && IsSsl.CastInt32()) Port = POP3_SSL_PORT; else Port = POP3_PORT; } strcpy_s(Str, sizeof(Str), RemoteHost); char *Colon = strchr(Str, ':'); if (Colon) { *Colon = 0; Colon++; Port = atoi(Colon); } if (S && User && Password && (Server = TrimStr(Str))) { S->SetTimeout(30 * 1000); ReStartConnection: if (SocketLock.Lock(_FL)) { Socket.Reset(S); SocketLock.Unlock(); } if (Socket && Socket->Open(Server, Port) && ReadReply()) { LVariant NoAPOP = false; if (SettingStore) SettingStore->GetValue(OPT_Pop3NoApop, NoAPOP); if (!NoAPOP.CastInt32()) { char *s = strchr(Buffer + 3, '<'); if (s) { char *e = strchr(s + 1, '>'); if (e) { Apop = NewStr(s, e - s + 1); } } } // login bool Authed = false; char *user = (char*) LNewConvertCp("iso-8859-1", User, "utf-8"); char *pass = (char*) LNewConvertCp("iso-8859-1", Password, "utf-8"); if (user && (pass || SecureAuth)) { bool SecurityError = false; if (TestFlag(Flags, MAIL_USE_STARTTLS)) { strcpy_s(Buffer, sizeof(Buffer), "STARTTLS\r\n"); VERIFY_RET_VAL(Write(0, true)); VERIFY_RET_VAL(ReadReply()); LVariant v; if (Socket->SetValue(LSocket_Protocol, v="SSL")) { Flags &= ~MAIL_USE_STARTTLS; } else { SecurityError = true; } } if (!SecurityError && Apop) // GotKey, not implemented { // using encrypted password unsigned char Digest[16]; char HexDigest[33]; // append password char Key[256]; sprintf_s(Key, sizeof(Key), "%s%s", Apop, pass); ZeroObj(Digest); MDStringToDigest(Digest, Key); for (int i = 0; i < 16; i++) sprintf_s(HexDigest + (i*2), 3, "%2.2x", Digest[i]); HexDigest[32] = 0; sprintf_s(Buffer, sizeof(Buffer), "APOP %s %s\r\n", user, HexDigest); VERIFY_ONERR(Write(0, true)); Authed = ReadReply(); if (!Authed) { DeleteArray(Apop); LVariant NoAPOP = true; if (SettingStore) SettingStore->SetValue(OPT_Pop3NoApop, NoAPOP); S->Close(); goto ReStartConnection; } } if (!SecurityError && SecureAuth) { LHashTbl, bool> AuthTypes, Capabilities; if (ListCmd("AUTH", AuthTypes) && ListCmd("CAPA", Capabilities)) { if (AuthTypes.Find("GSSAPI")) { sprintf_s(Buffer, sizeof(Buffer), "AUTH GSSAPI\r\n"); VERIFY_ONERR(Write(0, true)); VERIFY_ONERR(ReadReply()); // http://www.faqs.org/rfcs/rfc2743.html } } } else if (!SecurityError && !Authed) { // have to use non-key method sprintf_s(Buffer, sizeof(Buffer), "USER %s\r\n", user); VERIFY_ONERR(Write(0, true)); VERIFY_ONERR(ReadReply()); sprintf_s(Buffer, sizeof(Buffer), "PASS %s\r\n", pass); VERIFY_ONERR(Write(0, false)); Log("PASS *******", LSocketI::SocketMsgSend); Authed = ReadReply(); } DeleteArray(user); DeleteArray(pass); } if (Authed) { Status = true; } else { if (SocketLock.Lock(_FL)) { Socket.Reset(0); SocketLock.Unlock(); } LgiTrace("%s:%i - Failed auth.\n", _FL); } } else Error(_FL, "Failed to open socket to %s:%i and read reply.\n", Server, Port); } else Error(_FL, "No user/pass.\n"); } CleanUp: DeleteArray(Apop); DeleteArray(Server); return Status; } bool MailPop3::MailIsEnd(LString &s) { ssize_t Len = s.Length(); for (auto c = s.Get(); c && Len-- > 0; c++) { if (*c != *Marker) { Marker = End; } if (*c == *Marker) { Marker++; if (!*Marker) { return true; } } } return false; } bool MailPop3::Receive(LArray &Trans, MailCallbacks *Callbacks) { bool Status = false; if (Trans.Length() > 0 && Socket) { for (unsigned n = 0; nIndex; LStreamI *Msg = Trans[n]->Stream; if (Msg) { int Size = 0; // Transfer is not null when the caller wants info on the bytes comming in if (Transfer || Callbacks) { // get message size sprintf_s(Buffer, sizeof(Buffer), "LIST %i\r\n", Message + 1); VERIFY_RET_VAL(Write(0, true)); VERIFY_RET_VAL(ReadReply()); char *s = strchr(Buffer, ' '); if (s) { s = strchr(s+1, ' '); if (s) { Size = atoi(s); } } } MailSrcStatus Action = DownloadAll; int TopLines = 100; if (Callbacks && Callbacks->OnSrc) { Action = Callbacks->OnSrc(Trans[n], Size, &TopLines, Callbacks->CallbackData); } if (Action == DownloadAbort) { break; } if (Action == DownloadAll || Action == DownloadTop) { if (Action == DownloadAll) { sprintf_s(Buffer, sizeof(Buffer), "RETR %i\r\n", Message + 1); } else { sprintf_s(Buffer, sizeof(Buffer), "TOP %i %i\r\n", Message + 1, TopLines); } VERIFY_RET_VAL(Write(0, true)); LHtmlLinePrefix End(".\r\n"); if (Transfer) { Transfer->Value = 0; Transfer->Range = Size; Transfer->Start = LCurrentTime(); } // Read status line ZeroObj(Buffer); ssize_t Used = 0; bool Ok = false; bool Finished = false; int64 DataPos = 0; while (Socket->IsOpen()) { ssize_t r = Socket->Read(Buffer+Used, sizeof(Buffer)-Used-1, 0); if (r > 0) { DeNullText(Buffer + Used, r); if (Transfer) { Transfer->Value += r; } char *Eol = strchr(Buffer, '\n'); if (Eol) { Eol++; Ok = Buffer[0] == '+'; if (Ok) { // Log(Buffer, LSocketI::SocketMsgReceive); // The Buffer was zero'd at the beginning garrenteeing // NULL termination size_t Len = strlen(Eol); ssize_t EndPos = End.IsEnd(Eol, Len); if (EndPos >= 0) { Msg->Write(Eol, EndPos - 3); Status = Trans[n]->Status = true; Finished = true; } else { Msg->Write(Eol, Len); DataPos += Len; } } else { Log(Buffer, LSocketI::SocketMsgError); Finished = true; } break; } Used += r; } else break; } if (!Finished) { if (Ok) { // Read rest of message while (Socket->IsOpen()) { ssize_t r = Socket->Read(Buffer, sizeof(Buffer), 0); if (r > 0) { DeNullText(Buffer, r); if (Transfer) { Transfer->Value += r; } ssize_t EndPos = End.IsEnd(Buffer, r); if (EndPos >= 0) { ssize_t Actual = EndPos - DataPos - 3; if (Actual > 0) { #ifdef _DEBUG ssize_t w = #endif Msg->Write(Buffer, Actual); LAssert(w == Actual); } // else the end point was in the last buffer Status = Trans[n]->Status = true; break; } else { #ifdef _DEBUG ssize_t w = #endif Msg->Write(Buffer, r); LAssert(w == r); DataPos += r; } } else { break; } } if (!Status) { LgiTrace("%s:%i - Didn't get end-of-mail marker.\n", _FL); } } else { LgiTrace("%s:%i - Didn't get Ok.\n", _FL); break; } } if (Callbacks && Callbacks->OnReceive) { Callbacks->OnReceive(Trans[n], Callbacks->CallbackData); } if (Transfer) { Transfer->Empty(); } } else { Trans[n]->Oversize = Status = true; } if (Items) { Items->Value++; } } else { LgiTrace("%s:%i - No stream.\n", _FL); } } } else { LgiTrace("%s:%i - Arg check failed, len=%p, sock=%p.\n", _FL, Trans.Length(), Socket.Get()); } return Status; } bool MailPop3::GetSizes(LArray &Sizes) { if (!Socket) return false; strcpy_s(Buffer, sizeof(Buffer), (char*)"LIST\r\n"); VERIFY_RET_VAL(Write(0, true)); auto s = ReadMultiLineReply(); if (!s) return false; for (auto ln: s.SplitDelimit("\r\n")) { auto p = ln.SplitDelimit(); if (p.Length() > 1) Sizes.Add((int)p.Last().Int()); } return Sizes.Length() > 0; } int MailPop3::Sizeof(int Message) { int Size = 0; if (Socket) { sprintf_s(Buffer, sizeof(Buffer), "LIST %i\r\n", Message + 1); VERIFY_RET_VAL(Write(0, true)); VERIFY_RET_VAL(ReadReply()); char *s = strchr(Buffer, ' '); if (s) { s = strchr(s+1, ' '); if (s) { Size = atoi(s); } } } return Size; } bool MailPop3::Delete(int Message) { if (Socket) { sprintf_s(Buffer, sizeof(Buffer), "DELE %i\r\n", Message + 1); VERIFY_RET_VAL(Write(0, true)); VERIFY_RET_VAL(ReadReply()); return true; } return false; } bool MailPop3::GetUid(int Index, char *Id, int IdLen) { if (Socket && Id) { sprintf_s(Buffer, sizeof(Buffer), "UIDL %i\r\n", Index + 1); VERIFY_RET_VAL(Write(0, true)); VERIFY_RET_VAL(ReadReply()); char *Space = strchr(Buffer, ' '); if (Space) { Space = strchr(Space+1, ' '); if (Space) { for (char *s = Space+1; *s; s++) { if (*s == '\r' || *s == '\n') { *s = 0; break; } } strcpy_s(Id, IdLen, Space+1); return true; } } } return false; } bool MailPop3::GetUidList(LString::Array &Id) { if (!Socket) return false; sprintf_s(Buffer, sizeof(Buffer), "UIDL\r\n"); VERIFY_RET_VAL(Write(0, true)); auto Str = ReadMultiLineReply(); if (!Str) return false; auto lines = Str.SplitDelimit("\r\n"); for (auto s: lines) { if (s(0) != '.') { char *Space = strchr(s, ' '); if (Space++) Id.New() = Space; } } return true; } LString MailPop3::GetHeaders(int Message) { if (!Socket) return NULL; sprintf_s(Buffer, sizeof(Buffer), "TOP %i 0\r\n", Message + 1); if (!Write(NULL, true)) return NULL; return ReadMultiLineReply(); } LString MailPop3::ReadMultiLineReply() { if (!Socket) { LAssert(!"No socket."); return false; } LString a; do { auto s = Socket->Read(); if (!s) break; a += s; if (!a || a[0] != '+') return NULL; } while (!MailIsEnd(a)); // Strip off the first line... auto FirstNewLen = a.Find("\n"); return FirstNewLen >= 0 ? a(FirstNewLen, -1) : NULL; } bool MailPop3::Close() { if (Socket) { // logout VERIFY_RET_VAL(Write("QUIT\r\n", true)); // 2 sec timeout, we don't really care about the server's response Socket->SetTimeout(2000); ReadReply(); if (SocketLock.Lock(_FL)) { Socket.Reset(0); SocketLock.Unlock(); } Messages = 0; return true; } return false; } diff --git a/src/common/Net/MailImap.cpp b/src/common/Net/MailImap.cpp --- a/src/common/Net/MailImap.cpp +++ b/src/common/Net/MailImap.cpp @@ -1,3331 +1,3331 @@ #include #ifdef LINUX #include #endif #include "lgi/common/Lgi.h" #include "lgi/common/Mail.h" #include "lgi/common/Base64.h" #include "lgi/common/NetTools.h" #include "lgi/common/DocView.h" #include "lgi/common/Http.h" #include "lgi/common/HttpTools.h" #include "lgi/common/OpenSSLSocket.h" #include "lgi/common/Json.h" #define DEBUG_OAUTH2 1 #ifdef _DEBUG #define DEBUG_FETCH 0 #else #define DEBUG_FETCH 0 #endif #define OPT_ImapOAuth2AccessToken "OAuth2AccessTok" #undef _FL #define _FL LGetLeaf(__FILE__), __LINE__ //////////////////////////////////////////////////////////////////////////// #if GPL_COMPATIBLE #include "AuthNtlm/Ntlm.h" #else #include "../src/common/Net/libntlm-0.4.2/ntlm.h" #endif #if HAS_LIBGSASL #include "gsasl.h" #endif static const char *sRfc822Header = "RFC822.HEADER"; static const char *sRfc822Size = "RFC822.SIZE"; struct TraceLog : public LStream { ssize_t Read(void *Buffer, ssize_t Size, int Flags = 0) { return 0; } ssize_t Write(const void *Buffer, ssize_t Size, int Flags = 0) { LgiTrace("%.*s", (int)Size, Buffer); return Size; } }; /* #define SkipWhiteSpace(s) while (*s && IsWhiteSpace(*s)) s++; bool JsonDecode(LXmlTag &t, const char *s) { if (*s != '{') return false; s++; while (*s) { SkipWhiteSpace(s); if (*s != '\"') break; LAutoString Variable(LTokStr(s)); SkipWhiteSpace(s); if (*s != ':') return false; s++; SkipWhiteSpace(s); LAutoString Value(LTokStr(s)); SkipWhiteSpace(s); t.SetAttr(Variable, Value); if (*s != ',') break; s++; } if (*s != '}') return false; s++; return true; } */ #define SkipWhite(s) while (*s && strchr(WhiteSpace, *s)) s++ #define SkipSpaces(s) while (*s && strchr(" \t", *s)) s++ #define SkipNonWhite(s) while (*s && !strchr(WhiteSpace, *s)) s++; #define ExpectChar(ch) if (*s != ch) return 0; s++ ssize_t MailIMap::ParseImapResponse(char *Buffer, ssize_t BufferLen, LArray &Ranges, int Names) { Ranges.Length(0); if (!*Buffer || *Buffer != '*') return 0; #ifdef _DEBUG char *End = Buffer + BufferLen; #endif char *s = Buffer + 1; char *Start; for (int n=0; nOpen(u.sHost, u.Port?u.Port:443)) return false; ssize_t w = S->Write(Req, ReqLen); if (w != ReqLen) return false; char Buf[256]; LArray Res; ssize_t r; ssize_t ContentLen = 0; ssize_t HdrLen = 0; while ((r = S->Read(Buf, sizeof(Buf))) > 0) { ssize_t Old = Res.Length(); Res.Length(Old + r); memcpy(&Res[Old], Buf, r); if (ContentLen) { if ((ssize_t)Res.Length() >= HdrLen + ContentLen) break; } else { auto Eoh = Strnstr(&Res[0], "\r\n\r\n", Res.Length()); if (Eoh) { HdrLen = Eoh - &Res[0]; LAutoString c(InetGetHeaderField(&Res[0], "Content-Length", HdrLen)); if (c) { ContentLen = atoi(c); } } } } char *Rp = &Res[0]; auto Eoh = Strnstr(Rp, "\r\n\r\n", Res.Length()); if (Eoh) { if (OutHeaders) OutHeaders->Reset(NewStr(Rp, Eoh-Rp)); if (OutBody) OutBody->Reset(NewStr(Eoh + 4, Res.Length() - (Eoh-Rp) - 4)); if (StatusCode) { *StatusCode = 0; char *Eol = strchr(Rp, '\n'); if (Eol) { auto t = LString(Rp, Eol-Rp).SplitDelimit(" \t\r\n"); if (t.Length() > 2) *StatusCode = (int)t[1].Int(); } } } else return false; #ifndef _DEBUG LFile f; if (f.Open("c:\\temp\\http.html", O_WRITE)) { f.SetSize(0); f.Write(&Res[0], Res.Length()); f.Close(); } #endif return true; } LAutoString ImapBasicTokenize(char *&s) { if (s) { while (*s && strchr(WhiteSpace, *s)) s++; char start = 0, end = 0; if (*s == '\'' || *s == '\"') start = end = *s; else if (*s == '[') { start = '['; end = ']'; } else if (*s == '(') { start = '('; end = ')'; } else if (*s == '{') { start = '{'; end = '}'; } if (start && end) { s++; char *e = strchr(s, end); if (e) { char *n = NewStr(s, e - s); s = e + 1; return LAutoString(n); } } else { char *e = s; while (*e && !strchr(WhiteSpace, *e)) e++; if (e > s) { char *n = NewStr(s, e - s); s = e + (*e != 0); return LAutoString(n); } } } s += strlen(s); return LAutoString(); } char *Tok(char *&s) { char *Ret = 0; while (*s && strchr(WhiteSpace, *s)) s++; if (*s == '=' || *s == ',') { Ret = NewStr(s++, 1); } else if (*s == '\'' || *s == '\"') { char d = *s++; char *e = strchr(s, d); if (e) { Ret = NewStr(s, e - s); s = e + 1; } } else if (*s) { char *e; for (e=s; *e && (IsDigit(*e) || IsAlpha(*e) || *e == '-'); e++); Ret = NewStr(s, e - s); s = e; } return Ret; } char *DecodeImapString(const char *s) { LStringPipe p; while (s && *s) { if (*s == '&') { char Escape = *s++; const char *e = s; while (*e && *e != '-') { e++; } ssize_t Len = e - s; if (Len) { char *Base64 = new char[Len + 4]; if (Base64) { memcpy(Base64, s, Len); char *n = Base64 + Len; for (ssize_t i=Len; i%4; i++) *n++ = '='; *n++ = 0; Len = strlen(Base64); ssize_t BinLen = BufferLen_64ToBin(Len); uint16 *Bin = new uint16[(BinLen/2)+1]; if (Bin) { BinLen = ConvertBase64ToBinary((uchar*)Bin, BinLen, Base64, Len); if (BinLen) { ssize_t Chars = BinLen / 2; Bin[Chars] = 0; for (int i=0; i>8) | ((Bin[i]&0xff)<<8); } char *c8 = WideToUtf8((char16*)Bin, BinLen); if (c8) { p.Push(c8); DeleteArray(c8); } } DeleteArray(Bin); } DeleteArray(Base64); } } else { p.Push(&Escape, 1); } s = e + 1; } else { p.Push(s, 1); s++; } } return p.NewStr(); } char *EncodeImapString(const char *s) { LStringPipe p; ssize_t Len = s ? strlen(s) : 0; while (s && *s) { int c = LgiUtf8To32((uint8*&)s, Len); DoNextChar: if ((c >= ' ' && c < 0x80) || c == '\n' || c == '\t' || c == '\r') { // Literal char ch = c; p.Push(&ch, 1); } else { // Encoded LArray Str; Str[0] = c; while ((c = LgiUtf8To32((uint8*&)s, Len))) { if ((c >= ' ' && c < 0x80) || c == '\n' || c == '\t' || c == '\r') { break; } else { Str[Str.Length()] = c; } } for (uint32 i=0; i>8) | ((Str[i]&0xff)<<8); } ssize_t BinLen = Str.Length() << 1; ssize_t BaseLen = BufferLen_BinTo64(BinLen); char *Base64 = new char[BaseLen+1]; if (Base64) { ssize_t Bytes = ConvertBinaryToBase64(Base64, BaseLen, (uchar*)&Str[0], BinLen); while (Bytes > 0 && Base64[Bytes-1] == '=') { Base64[Bytes-1] = 0; Bytes--; } Base64[Bytes] = 0; p.Print("&%s-", Base64); DeleteArray(Base64); } goto DoNextChar; } } return p.NewStr(); } void ChopNewLine(char *Str) { char *End = Str+strlen(Str)-1; if (*End == '\n') { *End-- = 0; } if (*End == '\r') { *End-- = 0; } } MailImapFolder::MailImapFolder() { Sep = '/'; Path = 0; NoSelect = false; NoInferiors = false; Marked = false; Exists = -1; Recent = -1; // UnseenIndex = -1; Deleted = 0; } MailImapFolder::~MailImapFolder() { DeleteArray(Path); } void MailImapFolder::operator =(LHashTbl,int> &v) { int o = v.Find("exists"); if (o >= 0) Exists = o; o = v.Find("recent"); if (o >= 0) Recent = o; } char *MailImapFolder::GetPath() { return Path; } void MailImapFolder::SetPath(const char *s) { char *NewPath = DecodeImapString(s); DeleteArray(Path); Path = NewPath; } char *MailImapFolder::GetName() { if (Path) { char *s = strrchr(Path, Sep); if (s) { return s + 1; } else { return Path; } } return 0; } void MailImapFolder::SetName(const char *s) { if (s) { char Buf[256]; strcpy_s(Buf, sizeof(Buf), Path?Path:(char*)""); DeleteArray(Path); char *Last = strrchr(Buf, Sep); if (Last) { Last++; strcpy_s(Last, sizeof(Buf)-(Last-Buf), s); Path = NewStr(Buf); } else { Path = NewStr(s); } } } ///////////////////////////////////////////// class MailIMapPrivate : public LMutex { public: int NextCmd, IdleCmd; bool Logging; bool ExpungeOnExit; bool ReadDebug; char FolderSep; char *Current; char *Flags; LHashTbl,bool> Capability; LString WebLoginUri; LViewI *ParentWnd; LCancel *Cancel; OsThread InCommand; LString LastWrite; MailIMapPrivate() : LMutex("MailImapSem") { ParentWnd = NULL; FolderSep = '/'; NextCmd = 1; IdleCmd = -1; Logging = true; ExpungeOnExit = true; Current = 0; Flags = 0; InCommand = 0; Cancel = NULL; ReadDebug = false; } ~MailIMapPrivate() { DeleteArray(Current); DeleteArray(Flags); } }; MailIMap::MailIMap() { d = new MailIMapPrivate; Buffer[0] = 0; } MailIMap::~MailIMap() { if (Lock(_FL)) { ClearDialog(); ClearUid(); DeleteObj(d); } } bool MailIMap::Lock(const char *file, int line) { if (!d->Lock(file, line)) return false; return true; } bool MailIMap::LockWithTimeout(int Timeout, const char *file, int line) { if (!d->LockWithTimeout(Timeout, file, line)) return false; return true; } void MailIMap::Unlock() { d->Unlock(); d->InCommand = 0; } void MailIMap::SetCancel(LCancel *Cancel) { d->Cancel = Cancel; } void MailIMap::SetParentWindow(LViewI *wnd) { d->ParentWnd = wnd; } const char *MailIMap::GetWebLoginUri() { return d->WebLoginUri; } bool MailIMap::IsOnline() { return Socket ? Socket->IsOpen() : false; } char MailIMap::GetFolderSep() { return d->FolderSep; } char *MailIMap::GetCurrentPath() { return d->Current; } bool MailIMap::GetExpungeOnExit() { return d->ExpungeOnExit; } void MailIMap::SetExpungeOnExit(bool b) { d->ExpungeOnExit = b; } void MailIMap::ClearUid() { if (Lock(_FL)) { Uid.DeleteArrays(); Unlock(); } } void MailIMap::ClearDialog() { if (Lock(_FL)) { Dialog.DeleteArrays(); Unlock(); } } bool MailIMap::WriteBuf(bool ObsurePass, const char *Buffer, bool Continuation) { if (Socket) { if (!Buffer) Buffer = Buf; ssize_t Len = strlen(Buffer); d->LastWrite = Buffer; if (!Continuation && d->InCommand) { LString Msg; Msg.Printf("%s:%i - WriteBuf failed(%s)\n", LGetLeaf(__FILE__), __LINE__, d->LastWrite.Strip().Get()); Socket->OnInformation(Msg); LAssert(!"Can't be issuing new commands while others are still running."); return false; } /* else { LString Msg; Msg.Printf("%s:%i - WriteBuf ok(%s)\n", LGetLeaf(__FILE__), __LINE__, d->LastWrite.Strip().Get()); Socket->OnInformation(Msg); } */ if (Socket->Write((void*)Buffer, Len, 0) == Len) { if (ObsurePass) { char *Sp = (char*)strrchr(Buffer, ' '); if (Sp) { Sp++; LString s; s.Printf("%.*s********\r\n", Sp - Buffer, Buffer); Log(s.Get(), LSocketI::SocketMsgSend); } } else Log(Buffer, LSocketI::SocketMsgSend); d->InCommand = LGetCurrentThread(); return true; } // else Log("Failed to write data to socket.", LSocketI::SocketMsgError); } else Log("Not connected.", LSocketI::SocketMsgError); return false; } bool MailIMap::Read(LStreamI *Out, int Timeout) { int Lines = 0; while (!Lines && Socket) { ssize_t r = Socket->Read(Buffer, sizeof(Buffer)); if (Timeout > 0 && Socket->IsOpen() && r <= 0) { auto St = LCurrentTime(); auto Rd = Socket->IsReadable(Timeout); auto End = LCurrentTime(); if (Rd) { r = Socket->Read(Buffer, sizeof(Buffer)); if (r < 0) { Socket->Close(); LgiTrace("%s:%i - Wut? IsReadable/Read mismatch.\n", _FL); return false; } #if 0 else if (d->ReadDebug) { LgiTrace("%s:%i - Idle Read '%.*s'\n", _FL, (int)r, Buffer); } #endif } else { if (End - St < Timeout - 20) LgiTrace("%s:%i - IsReadable broken (again)\n", _FL); return false; } } if (r > 0) { ReadBuf.Push(Buffer, r); while (ReadBuf.Pop(Buffer, sizeof(Buffer))) { // Trim trailing whitespace char *e = Buffer + strlen(Buffer) - 1; while (e > Buffer && strchr(WhiteSpace, *e)) *e-- = 0; Lines++; if (Out) { Out->Write(Buffer, strlen(Buffer)); Out->Write((char*)"\r\n", 2); } else { Dialog.Add(NewStr(Buffer)); } } } else break; } return Lines > 0; } bool MailIMap::IsResponse(const char *Buf, int Cmd, bool &Ok) { char Num[8]; int Ch = sprintf_s(Num, sizeof(Num), "A%4.4i ", Cmd); if (!Buf || _strnicmp(Buf, Num, Ch) != 0) return false; Ok = _strnicmp(Buf+Ch, "OK", 2) == 0; if (!Ok) SetError(L_ERROR_GENERIC, "Error: %s", Buf+Ch); return true; } bool MailIMap::ReadResponse(int Cmd, bool Plus) { bool Done = false; bool Status = false; if (Socket) { ssize_t Pos = Dialog.Length(); while (!Done) { if (Read(NULL)) { for (auto It = Dialog.begin(Pos); !Done && It != Dialog.end(); It++) { auto Dlg = *It; Pos++; if (Cmd < 0 || (Plus && *Dlg == '+')) { Status = Done = true; } if (IsResponse(Dlg, Cmd, Status)) Done = true; if (d->Logging) { bool Good = strchr("*+", *Dlg) != NULL || Status; Log(Dlg, Good ? LSocketI::SocketMsgReceive : LSocketI::SocketMsgError); } } } else { // LgiTrace("%s:%i - 'Read' failed.\n", _FL); break; } } } return Status; } void Hex(char *Out, int OutLen, uchar *In, ssize_t InLen = -1) { if (Out && In) { if (InLen < 0) InLen = strlen((char*)In); for (int i=0; i 0) { Out += ch; OutLen -= ch; } else break; } } } void _unpack(void *ptr, int ptrsize, char *b64) { ConvertBase64ToBinary((uchar*) ptr, ptrsize, b64, strlen(b64)); } bool MailIMap::ReadLine() { ssize_t Len = 0; Buf[0] = 0; do { ssize_t r = Socket->Read(Buf+Len, sizeof(Buf)-Len); if (r < 1) return false; Len += r; } while (!stristr(Buf, "\r\n")); Log(Buf, LSocketI::SocketMsgReceive); return true; } #if HAS_LIBGSASL int GsaslCallback(Gsasl *ctx, Gsasl_session *sctx, Gsasl_property prop) { return 0; } #endif class OAuthWebServer : public LThread, public LMutex { bool Loop; int Port; LSocket Listen; LAutoString Req; LString Resp; bool Finished; public: OAuthWebServer(int DesiredPort = 0) : LThread("OAuthWebServerThread"), LMutex("OAuthWebServerMutex") { Loop = false; if (Listen.Listen(DesiredPort)) { Port = Listen.GetLocalPort(); Run(); } else Port = 0; Finished = false; } ~OAuthWebServer() { if (Loop) { Loop = false; while (!IsExited()) LSleep(10); } } int GetPort() { return Port; } LString GetRequest(LCancel *Loop, uint64 TimeoutMs = 0) { LString r; uint64 Start = LCurrentTime(); while (!r && (!Loop || !Loop->IsCancelled())) { if (Lock(_FL)) { if (Req) r = Req; Unlock(); } if (TimeoutMs) { uint64 Now = LCurrentTime(); if (Now - Start >= TimeoutMs) break; } if (!r) LSleep(50); } return r; } void SetResponse(const char *r) { if (Lock(_FL)) { Resp = r; Unlock(); } } bool IsFinished() { return Finished; } int Main() { LAutoPtr s; Loop = true; while (Loop) { if (Listen.CanAccept(100)) { s.Reset(new LSocket); if (!Listen.Accept(s)) s.Reset(); else { LArray Mem; ssize_t r; char buf[512]; do { r = s->Read(buf, sizeof(buf)); if (r > 0) { Mem.Add(buf, r); bool End = Strnstr(&Mem[0], "\r\n\r\n", Mem.Length()) != NULL; if (End) break; } } while (r > 0); if (Lock(_FL)) { Mem.Add(0); Req.Reset(Mem.Release()); Unlock(); } // Wait for the response... LString Response; do { if (Lock(_FL)) { if (Resp) Response = Resp; Unlock(); } if (!Response) LSleep(10); } while (Loop && !Response); if (Response) s->Write(Response, Response.Length()); Loop = false; } } else LSleep(10); } Finished = true; return 0; } }; static void AddIfMissing(LArray &Auths, const char *a, LString *DefaultAuthType = NULL) { for (unsigned i=0; iLastWrite.Strip().Get()); Socket->OnInformation(Msg); */ d->InCommand = 0; d->LastWrite.Empty(); } bool MailIMap::Open(LSocketI *s, const char *RemoteHost, int Port, const char *User, const char *Password, LDom *settingStore, int Flags) { bool Status = false; if (settingStore) SettingStore = settingStore; if (SocketLock.Lock(_FL)) { Socket.Reset(s); SocketLock.Unlock(); } if (Socket && ValidStr(RemoteHost) && ValidStr(User) && ( ValidStr(Password) || OAuth2.IsValid() ) && Lock(_FL)) { // prepare address if (Port < 1) { if (Flags & MAIL_SSL) Port = IMAP_SSL_PORT; else Port = IMAP_PORT; } char Remote[256]; strcpy_s(Remote, sizeof(Remote), RemoteHost); char *Colon = strchr(Remote, ':'); if (Colon) { *Colon++ = 0; Port = atoi(Colon); } // Set SSL mode LVariant v; if (Flags == MAIL_SSL) v = "SSL"; Socket->SetValue(LSocket_Protocol, v); // connect if (Socket->Open(Remote, Port)) { bool IMAP4Server = false; LArray Auths; // check capability int CapCmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i CAPABILITY\r\n", CapCmd); if (WriteBuf()) { bool Rd = ReadResponse(CapCmd); CommandFinished(); if (Rd) { for (auto r: Dialog) { auto T = LString(r).SplitDelimit(" "); if (T.Length() > 1 && _stricmp(T[1], "CAPABILITY") == 0) { for (unsigned i=2; i 0) { Auths.DeleteAt(n); Auths.AddAt(0, DefaultAuthType); break; } } // SSL bool TlsError = false; if (TestFlag(Flags, MAIL_USE_STARTTLS)) { int CapCmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i STARTTLS\r\n", CapCmd); if (WriteBuf()) { bool Rd = ReadResponse(CapCmd); CommandFinished(); if (Rd) { LVariant v; TlsError = !Socket->SetValue(LSocket_Protocol, v="SSL"); } else { TlsError = true; } } else LAssert(0); if (TlsError) { Log("STARTTLS failed", LSocketI::SocketMsgError); } } // login bool LoggedIn = false; char AuthTypeStr[256] = ""; for (unsigned i=0; i 0) { strconcat(AuthTypeStr, ", "); } strconcat(AuthTypeStr, AuthType); } // Do auth #if HAS_LIBGSASL if (!_stricmp(AuthType, "GSSAPI")) { int AuthCmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%04.4i AUTHENTICATE GSSAPI\r\n", AuthCmd); if (WriteBuf() && ReadLine() && Buf[0] == '+') { // Start GSSAPI Gsasl *ctx = NULL; Gsasl_session *sess = NULL; int rc = gsasl_init(&ctx); if (rc == GSASL_OK) { char *mechs; rc = gsasl_client_mechlist(ctx, &mechs); gsasl_callback_set(ctx, GsaslCallback); rc = gsasl_client_start(ctx, AuthType, &sess); if (rc != GSASL_OK) { Log("gsasl_client_start failed", LSocketI::SocketMsgError); } // gsasl_step(ctx, gsasl_done(ctx); } else Log("gsasl_init failed", LSocketI::SocketMsgError); } else Log("AUTHENTICATE GSSAPI failed", LSocketI::SocketMsgError); } else #endif if (_stricmp(AuthType, "LOGIN") == 0 || _stricmp(AuthType, "OTP") == 0) { // clear text authentication int AuthCmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i LOGIN %s %s\r\n", AuthCmd, User, Password); if (WriteBuf(true)) { LoggedIn = ReadResponse(AuthCmd); CommandFinished(); } } else if (_stricmp(AuthType, "PLAIN") == 0) { // plain auth type char s[256]; char *e = s; *e++ = 0; strcpy_s(e, sizeof(s)-(e-s), User); e += strlen(e); e++; strcpy_s(e, sizeof(s)-(e-s), Password); e += strlen(e); *e++ = '\r'; *e++ = '\n'; ssize_t Len = e - s - 2; int AuthCmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i AUTHENTICATE PLAIN\r\n", AuthCmd); if (WriteBuf()) { if (ReadResponse(AuthCmd, true)) { ssize_t b = ConvertBinaryToBase64(Buf, sizeof(Buf), (uchar*)s, Len); strcpy_s(Buf+b, sizeof(Buf)-b, "\r\n"); if (WriteBuf(false, NULL, true)) { bool Rd = ReadResponse(AuthCmd); CommandFinished(); if (Rd) { LoggedIn = true; } else { // Look for WEBALERT from Google for (auto s: Dialog) { char *start = strchr(s, '['); char *end = start ? strrchr(start, ']') : NULL; if (start && end) { start++; if (_strnicmp(start, "WEBALERT", 8) == 0) { start += 8; while (*start && strchr(WhiteSpace, *start)) start++; d->WebLoginUri.Set(start, end - start); } } } } } } } } #if (GPL_COMPATIBLE || defined(_LIBNTLM_H)) && defined(WINNATIVE) else if (_stricmp(AuthType, "NTLM") == 0) { // NT Lan Man authentication OSVERSIONINFO ver; ZeroObj(ver); ver.dwOSVersionInfoSize = sizeof(ver); if (!GetVersionEx(&ver)) { DWORD err = GetLastError(); Log("Couldn't get OS version", LSocketI::SocketMsgError); } else { // Username is in the format: User[@Domain] char UserDom[256]; strcpy_s(UserDom, sizeof(UserDom), User); char *Domain = strchr(UserDom, '@'); if (Domain) *Domain++ = 0; int AuthCmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%04.4i AUTHENTICATE NTLM\r\n", AuthCmd); if (WriteBuf()) { if (ReadResponse(AuthCmd, true)) { tSmbNtlmAuthNegotiate negotiate; tSmbNtlmAuthChallenge challenge; tSmbNtlmAuthResponse response; buildSmbNtlmAuthNegotiate(&negotiate, 0, 0); if (NTLM_VER(&negotiate) == 2) { negotiate.v2.version.major = (uint8) ver.dwMajorVersion; negotiate.v2.version.minor = (uint8) ver.dwMinorVersion; negotiate.v2.version.buildNumber = (uint16) ver.dwBuildNumber; negotiate.v2.version.ntlmRevisionCurrent = 0x0f; } ZeroObj(Buf); auto negotiateLen = SmbLength(&negotiate); auto c = ConvertBinaryToBase64(Buf, sizeof(Buf), (uchar*)&negotiate, negotiateLen); strcpy_s(Buf+c, sizeof(Buf)-c, "\r\n"); WriteBuf(false, NULL, true); /* read challange data from server, convert from base64 */ Buf[0] = 0; ClearDialog(); if (ReadResponse()) { /* buffer should contain the string "+ [base 64 data]" */ #if 1 ZeroObj(challenge); char *Line = Dialog[0]; LAssert(Line != NULL); ChopNewLine(Line); auto LineLen = strlen(Line); auto challengeLen = sizeof(challenge); c = ConvertBase64ToBinary((uchar*) &challenge, sizeof(challenge), Line+2, LineLen-2); if (NTLM_VER(&challenge) == 2) challenge.v2.bufIndex = (uint32)(c - (challenge.v2.buffer-(uint8*)&challenge)); else challenge.v1.bufIndex = (uint32)(c - (challenge.v1.buffer-(uint8*)&challenge)); #endif /* prepare response, convert to base64, send to server */ ZeroObj(response); FILETIME time = {0, 0}; SYSTEMTIME stNow; GetSystemTime(&stNow); SystemTimeToFileTime(&stNow, &time); char HostName[256] = ""; gethostname(HostName, sizeof(HostName)); buildSmbNtlmAuthResponse(&challenge, &response, UserDom, HostName, Domain, Password, (uint8*)&time); if (NTLM_VER(&response) == 2) { response.v2.version.major = (uint8) ver.dwMajorVersion; response.v2.version.minor = (uint8) ver.dwMinorVersion; response.v2.version.buildNumber = (uint16) ver.dwBuildNumber; response.v2.version.ntlmRevisionCurrent = 0x0f; } #if 0 { uint8 *r1 = (uint8*)&response; uint8 *r2 = (uint8*)&response_good; for (int i=0; iNextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i AUTHENTICATE DIGEST-MD5\r\n", AuthCmd); if (WriteBuf()) { if (ReadResponse(AuthCmd)) { char *TestCnonce = 0; #if 0 // Test case strcpy(Buf, "+ cmVhbG09ImVsd29vZC5pbm5vc29mdC5jb20iLG5vbmNlPSJPQTZNRzl0RVFHbTJoaCIscW9wPSJhdXRoIixhbGdvcml0aG09bWQ1LXNlc3MsY2hhcnNldD11dGYtOA=="); RemoteHost = "elwood.innosoft.com"; User = "chris"; Password = "secret"; TestCnonce = "OA6MHXh6VqTrRk"; #endif char *In = (char*)Buf; if (In[0] == '+' && In[1] == ' ') { In += 2; uchar Out[2048]; ssize_t b = ConvertBase64ToBinary(Out, sizeof(Out), In, strlen(In)); Out[b] = 0; LHashTbl, char*> Map; char *s = (char*)Out; while (s && *s) { char *Var = Tok(s); char *Eq = Tok(s); char *Val = Tok(s); char *Comma = Tok(s); if (Var && Eq && Val && strcmp(Eq, "=") == 0) { Map.Add(Var, Val); Val = 0; } DeleteArray(Var); DeleteArray(Eq); DeleteArray(Val); DeleteArray(Comma); } int32 CnonceI[2] = { (int32)LRand(), (int32)LRand() }; char Cnonce[32]; if (TestCnonce) strcpy_s(Cnonce, sizeof(Cnonce), TestCnonce); else Cnonce[ConvertBinaryToBase64(Cnonce, sizeof(Cnonce), (uchar*)&CnonceI, sizeof(CnonceI))] = 0; s = strchr(Cnonce, '='); if (s) *s = 0; int Nc = 1; char *Realm = Map.Find("realm"); char DigestUri[256]; sprintf_s(DigestUri, sizeof(DigestUri), "imap/%s", Realm ? Realm : RemoteHost); LStringPipe p; p.Print("username=\"%s\"", User); p.Print(",nc=%08.8i", Nc); p.Print(",digest-uri=\"%s\"", DigestUri); p.Print(",cnonce=\"%s\"", Cnonce); char *Nonce = Map.Find("nonce"); if (Nonce) { p.Print(",nonce=\"%s\"", Nonce); } if (Realm) { p.Print(",realm=\"%s\"", Realm); } char *Charset = Map.Find("charset"); if (Charset) { p.Print(",charset=%s", Charset); } char *Qop = Map.Find("qop"); if (Qop) { p.Print(",qop=%s", Qop); } // Calculate A1 char a1[256]; uchar md5[16]; sprintf_s(Buf, sizeof(Buf), "%s:%s:%s", User, Realm ? Realm : (char*)"", Password); MDStringToDigest((uchar*)a1, Buf); char *Authzid = Map.Find("authzid"); int ch = 16; if (Authzid) ch += sprintf_s(a1+ch, sizeof(a1)-ch, ":%s:%s:%s", Nonce, Cnonce, Authzid); else ch += sprintf_s(a1+ch, sizeof(a1)-ch, ":%s:%s", Nonce, Cnonce); MDStringToDigest(md5, a1, ch); char a1hex[256]; Hex(a1hex, sizeof(a1hex), (uchar*)md5, sizeof(md5)); // Calculate char a2[256]; if (Qop && (_stricmp(Qop, "auth-int") == 0 || _stricmp(Qop, "auth-conf") == 0)) sprintf_s(a2, sizeof(a2), "AUTHENTICATE:%s:00000000000000000000000000000000", DigestUri); else sprintf_s(a2, sizeof(a2), "AUTHENTICATE:%s", DigestUri); MDStringToDigest(md5, a2); char a2hex[256]; Hex(a2hex, sizeof(a2hex), (uchar*)md5, sizeof(md5)); // Calculate the final response sprintf_s(Buf, sizeof(Buf), "%s:%s:%8.8i:%s:%s:%s", a1hex, Nonce, Nc, Cnonce, Qop, a2hex); MDStringToDigest(md5, Buf); Hex(Buf, sizeof(Buf), (uchar*)md5, sizeof(md5)); p.Print(",response=%s", Buf); if ((s = p.NewStr())) { ssize_t Chars = ConvertBinaryToBase64(Buf, sizeof(Buf) - 4, (uchar*)s, strlen(s)); LAssert(Chars < sizeof(Buf)); strcpy_s(Buf+Chars, sizeof(Buf)-Chars, "\r\n"); if (WriteBuf(false, NULL, true) && Read()) { for (auto Dlg: Dialog) { if (Dlg[0] == '+' && Dlg[1] == ' ') { Log(Dlg, LSocketI::SocketMsgReceive); strcpy_s(Buf, sizeof(Buf), "\r\n"); if (WriteBuf(false, NULL, true)) { LoggedIn = ReadResponse(AuthCmd); } } else { Log(Dlg, LSocketI::SocketMsgError); break; } } } DeleteArray(s); } } } CommandFinished(); } } else if (!_stricmp(AuthType, "XOAUTH2")) { if (stristr(RemoteHost, "office365.com")) { Log("office365.com doesn't support OAUTH2:", LSocketI::SocketMsgInfo); Log("\thttps://stackoverflow.com/questions/29747477/imap-auth-in-office-365-using-oauth2", LSocketI::SocketMsgInfo); Log("\tSo why does it report support in the CAPABILITY response? Don't ask me - fret", LSocketI::SocketMsgInfo); continue; } else if (!OAuth2.IsValid()) { sprintf_s(Buf, sizeof(Buf), "Error: Unknown OAUTH2 server '%s' (ask fret@memecode.com to add)", RemoteHost); Log(Buf, LSocketI::SocketMsgError); continue; } TraceLog TLog; LOAuth2 Auth(OAuth2, User, SettingStore, Socket->GetCancel(), &TLog); auto AccessToken = Auth.GetAccessToken(); if (!AccessToken) { sprintf_s(Buf, sizeof(Buf), "Warning: No OAUTH2 Access Token."); #if DEBUG_OAUTH2 LgiTrace("%s:%i - %s.\n", _FL, Buf); #endif Log(Buf, LSocketI::SocketMsgWarning); break; } // Construct the XOAUTH2 parameter LString s; s.Printf("user=%s\001auth=Bearer %s\001\001", User, AccessToken.Get()); #if DEBUG_OAUTH2 LgiTrace("%s:%i - s=%s.\n", _FL, s.Replace("\001", "%01").Get()); #endif Base64Str(s); // Issue the IMAP command int AuthCmd = d->NextCmd++; LString AuthStr; AuthStr.Printf("A%4.4i AUTHENTICATE XOAUTH2 %s\r\n", AuthCmd, s.Get()); if (WriteBuf(false, AuthStr)) { Dialog.DeleteArrays(); if (Read(NULL)) { for (auto l: Dialog) { if (*l == '+') { l++; while (*l && strchr(WhiteSpace, *l)) l++; s = l; UnBase64Str(s); Log(s.Strip(), LSocketI::SocketMsgError); LJson t(s); auto StatusCode = t.Get("status").Int(); LgiTrace("%s:%i - HTTP status: %" PRIi64 "\n%s\n", _FL, StatusCode, s.Get()); sprintf_s(Buf, sizeof(Buf), "\r\n"); WriteBuf(false, NULL, true); if (StatusCode == 400) { // Refresh the token...? if (Auth.Refresh()) { CommandFinished(); // We need to restart the connection to use the refreshed token // Seems we can't just re-try the authentication command. return false; } } } else if (*l == '*') { Log(l, LSocketI::SocketMsgReceive); } else { if (IsResponse(l, AuthCmd, LoggedIn) && LoggedIn) { Log(l, LSocketI::SocketMsgReceive); if (SettingStore) { // Login successful, so persist the AuthCode for next time LVariant v = AccessToken.Get(); bool b = SettingStore->SetValue(OPT_ImapOAuth2AccessToken, v); if (!b) { Log("Couldn't store access token.", LSocketI::SocketMsgWarning); } } break; } else { Log(l, LSocketI::SocketMsgError); } } } } CommandFinished(); } } else { char s[256]; sprintf_s(s, sizeof(s), "Warning: Unsupported authentication type '%s'", AuthType); Log(s, LSocketI::SocketMsgWarning); } } if (LoggedIn) { Status = true; // Ask server for it's heirarchy (folder) separator. int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i LIST \"\" \"\"\r\n", Cmd); if (WriteBuf()) { ClearDialog(); Buf[0] = 0; if (ReadResponse(Cmd)) { for (auto Dlg: Dialog) { LArray t; char *s = Dlg; while (*s) { LAutoString a = ImapBasicTokenize(s); if (a) t.New() = a; else break; } if (t.Length() >= 5 && strcmp(t[0], "*") == 0 && _stricmp(t[1], "list") == 0) { for (unsigned i=2; iFolderSep = *s; break; } } break; } } } CommandFinished(); } } else { SetError(L_ERROR_UNSUPPORTED_AUTH, "Authentication failed, types available:\n\t%s", ValidStr(AuthTypeStr) ? AuthTypeStr : "(none)"); } } } Unlock(); } return Status; } bool MailIMap::Close() { bool Status = false; if (Socket && Lock(_FL)) { if (d->ExpungeOnExit) { ExpungeFolder(); } int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i LOGOUT\r\n", Cmd); if (WriteBuf()) Status = true; CommandFinished(); Socket->Close(); Unlock(); } return Status; } bool MailIMap::GetCapabilities(LArray &s) { // char *k = 0; // for (bool p=d->Capability.First(&k); p; p=d->Capability.Next(&k)) for (auto i : d->Capability) { s.Add(i.key); } return s.Length() > 0; } bool MailIMap::ServerOption(char *Opt) { return d->Capability.Find(Opt) != 0; } char *MailIMap::GetSelectedFolder() { return d->Current; } bool MailIMap::SelectFolder(const char *Path, StrMap *Values) { bool Status = false; if (!Path) { LAssert(!"Path is missing."); return false; } if (Socket && Lock(_FL)) { int Cmd = d->NextCmd++; char *Enc = EncodePath(Path); sprintf_s(Buf, sizeof(Buf), "A%4.4i SELECT \"%s\"\r\n", Cmd, Enc); DeleteArray(Enc); if (WriteBuf()) { DeleteArray(d->Current); ClearDialog(); if (ReadResponse(Cmd)) { Uid.DeleteArrays(); if (Values) { for (LString Dlg: Dialog) { LString::Array t = Dlg.SplitDelimit(" []"); if (t.Length() > 0 && t[0].Equals("*")) { for (unsigned i=1; iAdd(t[i], t[i-1]); } else if (t[i].Equals("unseen")) { //char *val = t[i+1]; if (t[i+1].IsNumeric()) Values->Add(t[i], t[i+1]); } else if (t[i].Equals("flags")) { ssize_t s = Dlg.Find("("); ssize_t e = Dlg.Find(")", s + 1); if (e >= 0) { LString Val = Dlg(s+1, e); Values->Add(t[i], Val); } } } } } } Status = true; d->Current = NewStr(Path); ClearDialog(); } CommandFinished(); } Unlock(); } return Status; } int MailIMap::GetMessages(const char *Path) { int Status = 0; if (!Path) { LAssert(!"No path."); return 0; } if (Socket && Lock(_FL)) { StrMap f; if (SelectFolder(Path, &f)) { LString Exists = f.Find("exists"); if (Exists && Exists.Int() >= 0) Status = (int)Exists.Int(); else LgiTrace("%s:%i - Failed to get 'exists' value.\n", _FL); } Unlock(); } return Status; } ssize_t MailIMap::GetMessages() { return GetMessages("INBOX"); } char *MailIMap::SequenceToString(LArray *Seq) { if (!Seq) return NewStr("1:*"); LStringPipe p; // int Last = 0; for (unsigned s=0; sLength(); ) { unsigned e = s; while (eLength()-1 && (*Seq)[e] == (*Seq)[e+1]-1) e++; if (s) p.Print(","); if (e == s) p.Print("%i", (*Seq)[s]); else p.Print("%i:%i", (*Seq)[s], (*Seq)[e]); s = e + 1; } return p.NewStr(); } static void RemoveBytes(LArray &a, ssize_t &Used, ssize_t Bytes) { if (Used >= Bytes) { ssize_t Remain = Used - Bytes; if (Remain > 0) memmove(&a[0], &a[Bytes], Remain); Used -= Bytes; } else LAssert(0); } static bool PopLine(LArray &a, ssize_t &Used, LAutoString &Line) { for (ssize_t i=0; iSet(EINVAL); Error->AddNote(_FL, "Invalid arg: %p,%p,%p.", Parts, Callback, Seq); return false; } if (!Lock(_FL)) { LgiTrace("%s:%i - Failed to get lock.\n", _FL); Error->Set(ENOLCK); Error->AddNote(_FL, "Failed to get lock."); return false; } int Status = 0; int Cmd = d->NextCmd++; LStringPipe p(256); p.Print("A%4.4i %sFETCH ", Cmd, ByUid ? "UID " : ""); p.Write(Seq, strlen(Seq)); p.Print(" (%s)\r\n", Parts); LAutoString WrBuf(p.NewStr()); if (!WriteBuf(false, WrBuf)) { Error->Set(EIO); Error->AddNote(_FL, "Write failed."); } else { ClearDialog(); LArray Buf; Buf.Length(1024 + (SizeHint>0?(uint32)SizeHint:0)); ssize_t Used = 0; ssize_t MsgSize; // int64 Start = LCurrentTime(); int64 Bytes = 0; bool Done = false; // uint64 TotalTs = 0; bool Blocking = Socket->IsBlocking(); Socket->IsBlocking(false); #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: Starting loop\n", _FL); #endif uint64 LastActivity = LCurrentTime(); bool Debug = false; while (!Done) { ssize_t r; if (!Socket->IsOpen()) { Error->Set(ENOTSOCK); Error->AddNote(_FL, "Socket closed."); break; } // We don't wait for 'readable' with select here because // a) The socket is in non-blocking mode and // b) For OpenSSL connections 'readable' on the socket != can get bytes from 'read'. // Just try the read first and see if it gives you bytes, if not then 'select' on the socket. while (true) { // Extend the buffer if getting used up if (Buf.Length()-Used <= 256) { Buf.Length(Buf.Length() + (64<<10)); #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: Ext buf: %i\n", _FL, Buf.Length()); #endif } // Try and read bytes from server. r = Socket->Read(Buf.AddressOf(Used), Buf.Length()-Used-1); // -1 for NULL terminator #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: r=%i, used=%i, buf=%i\n", _FL, r, Used, Buf.Length()); #endif if (r > 0) { if (RawCopy) RawCopy->Write(&Buf[Used], r); Used += r; Bytes += r; LastActivity = LCurrentTime(); } else { LSleep(1); // Don't eat the whole CPU... break; } if (Debug) LgiTrace("%s:%i - Recv=%i\n", _FL, r); } // See if we can parse out a single response LArray Ranges; LAssert(Used < (ssize_t)Buf.Length()); Buf[Used] = 0; // NULL terminate before we parse while (true) { MsgSize = ParseImapResponse(&Buf[0], Used, Ranges, 2); #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: MsgSize=%i\n", _FL, MsgSize); #endif if (Debug) LgiTrace("%s:%i - ParseImapResponse=%i\n", _FL, MsgSize); if (!MsgSize) break; if (!Debug) LastActivity = LCurrentTime(); char *b = &Buf[0]; if (MsgSize > Used) { // This is an error... ParseImapResponse should always return <= Used. // If this triggers, ParseImapResponse is skipping a NULL that it shouldn't. #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: Wrong size %i, %i\n", _FL, MsgSize, Used); #endif Ranges.Length(0); LAssert(0); #if _DEBUG ParseImapResponse(&Buf[0], Used, Ranges, 2); #endif Done = true; Error->Set(ENODATA); Error->AddNote(_FL, "Wrong data size."); break; } LAssert(Ranges.Length() >= 2); // Setup strings for callback char *Param = b + Ranges[0].Start; Param[Ranges[0].Len()] = 0; char *Name = b + Ranges[1].Start; Name[Ranges[1].Len()] = 0; if (Stricmp(Name, "FETCH")) { // Not the response we're looking for. #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: Wrong response: %s\n", _FL, Name); #endif } else if (Stricmp(Param, "BYE")) { // Process ranges into a hash table StrMap Parts; for (unsigned i=2; iSet(ECANCELED); Error->AddNote(_FL, "Callback failed."); #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: Callback return FALSE?\n", _FL); #endif } } // Remove this msg from buffer RemoveBytes(Buf, Used, MsgSize); Buf[Used] = 0; // 'Used' changed... so NULL terminate before we parse } // Look for the end marker #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: End, Used=%i, Buf=%.12s\n", _FL, Used, Buf.AddressOf()); #endif if (Used > 0 && Buf[0] != '*') { LAutoString Line; while (PopLine(Buf, Used, Line)) { #if DEBUG_FETCH LgiTrace("%s:%i - Fetch: Line='%s'\n", _FL, Line.Get()); #endif auto t = LString(Line).SplitDelimit(" \r\n"); if (t.Length() >= 2) { char *r = t[0]; if (*r == 'A') { bool IsOk = !_stricmp(t[1], "Ok"); int Response = atoi(r + 1); Log(Line, IsOk ? LSocketI::SocketMsgReceive : LSocketI::SocketMsgError); if (Response == Cmd) { Done = true; break; } } else { Error->Set(EPROTO); Error->AddNote(_FL, "ImapErr: %s", &Buf[0]); Log(&Buf[0], LSocketI::SocketMsgError); } } else { // This is normal behaviour... just don't have the end marker yet. Done = true; break; } } } } Socket->IsBlocking(Blocking); CommandFinished(); #if DEBUG_FETCH LgiTrace("%s:%i - Fetch finished, status=%i\n", _FL, Status); #endif } Unlock(); if (!Status && !Error->GetCode()) { Error->Set(ENODATA); Error->AddNote(_FL, "No records received."); } return Status; } bool IMapHeadersCallback(MailIMap *Imap, uint32_t Msg, MailIMap::StrMap &Parts, void *UserData) { char *s = Parts.Find(sRfc822Header); if (s) { Parts.Delete(sRfc822Header); LString *Hdrs = (LString*)UserData; *Hdrs = s; } return true; } LString MailIMap::GetHeaders(int Message) { LString Text; if (!Lock(_FL)) { char Seq[64]; sprintf_s(Seq, sizeof(Seq), "%i", Message + 1); Fetch( false, Seq, sRfc822Header, IMapHeadersCallback, &Text, NULL); Unlock(); } return Text; } struct ReceiveCallbackState { MailTransaction *Trans; MailCallbacks *Callbacks; }; static bool IMapReceiveCallback(MailIMap *Imap, uint32_t Msg, MailIMap::StrMap &Parts, void *UserData) { ReceiveCallbackState *State = (ReceiveCallbackState*) UserData; char *Flags = Parts.Find("FLAGS"); if (Flags) { State->Trans->Imap.Set(Flags); } char *Hdrs = Parts.Find(sRfc822Header); if (Hdrs) { ssize_t Len = strlen(Hdrs); State->Trans->Stream->Write(Hdrs, Len); } char *Body = Parts.Find("BODY[TEXT]"); if (Body) { ssize_t Len = strlen(Body); State->Trans->Stream->Write(Body, Len); } State->Trans->Status = Hdrs != NULL || Body != NULL; if (Imap->Items) Imap->Items->Value++; Parts.Empty(); if (State->Callbacks) { bool Ret = State->Callbacks->OnReceive(State->Trans, State->Callbacks->CallbackData); if (!Ret) return false; } return true; } bool MailIMap::Receive(LArray &Trans, MailCallbacks *Callbacks) { bool Status = false; if (Lock(_FL)) { int Errors = 0; ReceiveCallbackState State; State.Callbacks = Callbacks; for (unsigned i=0; iStatus = false; char Seq[64]; sprintf_s(Seq, sizeof(Seq), "%i", State.Trans->Index + 1); Fetch ( false, Seq, "FLAGS RFC822.HEADER BODY[TEXT]", IMapReceiveCallback, &State, NULL ); if (State.Trans->Status) { Status = true; } else if (Errors++ > 5) { // Yeah... not feelin' it Status = false; break; } } Unlock(); } return Status; } bool MailIMap::Append(const char *Folder, ImapMailFlags *Flags, const char *Msg, LString &NewUid) { bool Status = false; if (Folder && Msg && Lock(_FL)) { LString Flag; if (Flags) Flag = Flags->Get(); LAutoString Path(EncodePath(Folder)); int Cmd = d->NextCmd++; int Len = 0; for (const char *m = Msg; *m; m++) { if (*m == '\n') { Len += 2; } else if (*m != '\r') { Len++; } } // Append on the end of the mailbox int c = sprintf_s(Buf, sizeof(Buf), "A%4.4i APPEND \"%s\"", Cmd, Path.Get()); if (Flag) c += sprintf_s(Buf+c, sizeof(Buf)-c, " (%s)", Flag.Get()); c += sprintf_s(Buf+c, sizeof(Buf)-c, " {%i}\r\n", Len); if (WriteBuf()) { if (Read()) { bool GotPlus = false; for (auto Dlg: Dialog) { if (Dlg[0] == '+') { Dialog.Delete(Dlg); DeleteArray(Dlg); GotPlus = true; break; } } if (GotPlus) { ssize_t Wrote = 0; for (const char *m = Msg; *m; ) { while (*m == '\r' || *m == '\n') { if (*m == '\n') { Wrote += Socket->Write((char*)"\r\n", 2); } m++; } const char *e = m; while (*e && *e != '\r' && *e != '\n') e++; if (e > m) { Wrote += Socket->Write(m, e-m); m = e; } else break; } LAssert(Wrote == Len); Wrote += Socket->Write((char*)"\r\n", 2); // Read response.. ClearDialog(); if ((Status = ReadResponse(Cmd))) { char Tmp[16]; sprintf_s(Tmp, sizeof(Tmp), "A%4.4i", Cmd); for (auto Line: Dialog) { LAutoString c = ImapBasicTokenize(Line); if (!c) break; if (!strcmp(Tmp, c)) { LAutoString a; while ((a = ImapBasicTokenize(Line)).Get()) { auto t = LString(a).SplitDelimit(" "); if (t.Length() > 2 && !_stricmp(t[0], "APPENDUID")) { NewUid = t[2]; break; } } } } } } } CommandFinished(); } Unlock(); } return Status; } bool MailIMap::Delete(int Message) { bool Status = false; if (Socket && Lock(_FL)) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i STORE %i FLAGS (\\deleted)\r\n", Cmd, Message+1); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); CommandFinished(); } Unlock(); } return Status; } bool MailIMap::Delete(bool ByUid, const char *Seq) { bool Status = false; if (Socket && Lock(_FL)) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i %sSTORE %s FLAGS (\\deleted)\r\n", Cmd, ByUid?"UID ":"", Seq); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); CommandFinished(); } Unlock(); } return Status; } int MailIMap::Sizeof(int Message) { int Status = 0; if (Socket && Lock(_FL)) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i FETCH %i (%s)\r\n", Cmd, Message+1, sRfc822Size); if (WriteBuf()) { ClearDialog(); Buf[0] = 0; if (ReadResponse(Cmd)) { char *d = Dialog[0]; if (d) { char *t = strstr(d, sRfc822Size); if (t) { t += strlen(sRfc822Size) + 1; Status = atoi(t); } } } CommandFinished(); } Unlock(); } return Status; } bool ImapSizeCallback(MailIMap *Imap, uint32_t Msg, MailIMap::StrMap &Parts, void *UserData) { LArray *Sizes = (LArray*) UserData; char *Sz = Parts.Find(sRfc822Size); if (!Sz) return false; (*Sizes)[Msg - 1] = atoi(Sz); return true; } bool MailIMap::GetSizes(LArray &Sizes) { return Fetch(false, "1:*", sRfc822Size, ImapSizeCallback, &Sizes) != 0; } bool MailIMap::GetUid(int Message, char *Id, int IdLen) { bool Status = false; if (Lock(_FL)) { if (FillUidList()) { char *s = Uid.ItemAt(Message); if (s && Id) { strcpy_s(Id, IdLen, s); Status = true; } } Unlock(); } return Status; } bool MailIMap::FillUidList() { bool Status = false; if (Socket && Lock(_FL)) { if (Uid.Length() == 0) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i UID SEARCH ALL\r\n", Cmd); if (WriteBuf()) { ClearDialog(); if (ReadResponse(Cmd)) { for (auto d: Dialog) { auto T = LString(d).SplitDelimit(" "); if (T[1] && strcmp(T[1], "SEARCH") == 0) { for (unsigned i=2; i &Folders) { bool Status = false; if (Socket && Lock(_FL)) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i LIST \"\" \"*\"\r\n", Cmd); if (WriteBuf()) { ClearDialog(); Buf[0] = 0; if (ReadResponse(Cmd)) { char Sep[] = { GetFolderSep(), 0 }; for (auto d: Dialog) { LArray t; char *s; while ((s = LTokStr((const char*&)d))) { t[t.Length()].Reset(s); } if (t.Length() >= 5) { if (strcmp(t[0], "*") == 0 && _stricmp(t[1], "LIST") == 0) { char *Folder = t[t.Length()-1]; MailImapFolder *f = new MailImapFolder(); if (f) { Folders.Add(f); f->Sep = Sep[0]; // Check flags f->NoSelect = stristr(t[2], "NoSelect") != 0; f->NoInferiors = stristr(t[2], "NoInferiors") != 0; // LgiTrace("Imap folder '%s' %s\n", Folder, t[2].Get()); // Alloc name if (Folder[0] == '\"') { char *p = TrimStr(Folder, "\""); f->Path = DecodeImapString(p); DeleteArray(p); } else { f->Path = DecodeImapString(Folder); } } } } } Status = true; ClearDialog(); } CommandFinished(); } Unlock(); } return Status; } bool MailIMap::CreateFolder(MailImapFolder *f) { bool Status = false; // char Dir[2] = { d->FolderSep, 0 }; if (f && f->GetPath() && Lock(_FL)) { int Cmd = d->NextCmd++; char *Enc = EncodePath(f->GetPath()); sprintf_s(Buf, sizeof(Buf), "A%4.4i CREATE \"%s\"\r\n", Cmd, Enc); DeleteArray(Enc); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); if (Status) { char *End = f->Path + strlen(f->Path) - 1; if (*End == GetFolderSep()) { f->NoSelect = true; *End = 0; } else { f->NoInferiors = true; } } CommandFinished(); } Unlock(); } return Status; } char *MailIMap::EncodePath(const char *Path) { if (!Path) return 0; char Sep = GetFolderSep(); char Native[MAX_PATH_LEN], *o = Native, *e = Native + sizeof(Native) - 1; for (const char *i = Path[0] == '/' && Path[1] ? Path + 1 : Path; *i && o < e; i++) { if (*i == '/') *o++ = Sep; else *o++ = *i; } *o++ = 0; return EncodeImapString(Native); } bool MailIMap::DeleteFolder(const char *Path) { bool Status = false; if (Path && Lock(_FL)) { // Close the current folder if required. if (d->Current && _stricmp(Path, d->Current) == 0) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i CLOSE\r\n", Cmd); if (WriteBuf()) { ClearDialog(); ReadResponse(Cmd); DeleteArray(d->Current); CommandFinished(); } } // Delete the folder int Cmd = d->NextCmd++; char *NativePath = EncodePath(Path); sprintf_s(Buf, sizeof(Buf), "A%4.4i DELETE \"%s\"\r\n", Cmd, NativePath); DeleteArray(NativePath); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); CommandFinished(); } Unlock(); } return Status; } bool MailIMap::RenameFolder(const char *From, const char *To) { bool Status = false; if (From && To && Lock(_FL)) { int Cmd = d->NextCmd++; LAutoString f(EncodePath(From)); LAutoString t(EncodePath(To)); sprintf_s(Buf, sizeof(Buf), "A%4.4i RENAME \"%s\" \"%s\"\r\n", Cmd, f.Get(), t.Get()); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); CommandFinished(); } Unlock(); } return Status; } bool MailIMap::SetFolderFlags(MailImapFolder *f) { bool Status = false; if (f && Lock(_FL)) { /* int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i RENAME \"%s\" \"%s\"\r\n", Cmd); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); } */ Unlock(); } return Status; } bool MailIMap::SetFlagsByUid(LArray &Uids, const char *Flags) { bool Status = false; if (Lock(_FL)) { int Cmd = d->NextCmd++; LStringPipe p; p.Print("A%04.4i UID STORE ", Cmd); if (Uids.Length()) { for (unsigned i=0; i &InUids, const char *DestFolder) { bool Status = false; if (Lock(_FL)) { int Cmd = d->NextCmd++; LAutoString Dest(EncodePath(DestFolder)); LStringPipe p(1024); p.Print("A%04.4i UID COPY ", Cmd); for (unsigned i=0; iNextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i EXPUNGE\r\n", Cmd); if (WriteBuf()) { ClearDialog(); Status = ReadResponse(Cmd); CommandFinished(); } Unlock(); } return Status; } bool MailIMap::Search(bool Uids, LArray &SeqNumbers, const char *Filter) { bool Status = false; if (ValidStr(Filter) && Lock(_FL)) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i %sSEARCH %s\r\n", Cmd, Uids?"UID ":"", Filter); if (WriteBuf()) { ClearDialog(); if (ReadResponse(Cmd)) { for (auto d: Dialog) { if (*d != '*') continue; d++; LAutoString s(Tok(d)); if (!s || _stricmp(s, "Search")) continue; while (s.Reset(Tok(d))) { SeqNumbers.New() = s.Get(); Status = true; } } } CommandFinished(); } Unlock(); } return Status; } bool MailIMap::Status(char *Path, int *Recent) { bool Status = false; if (Path && Recent && Lock(_FL)) { LAutoString Dest(EncodePath(Path)); int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i STATUS %s (RECENT)\r\n", Cmd, Dest.Get()); if (WriteBuf()) { ClearDialog(); if (ReadResponse(Cmd)) { for (auto d: Dialog) { if (*d != '*') continue; d++; LAutoString Cmd = ImapBasicTokenize(d); LAutoString Folder = ImapBasicTokenize(d); LAutoString Fields = ImapBasicTokenize(d); if (Cmd && Folder && Fields && !_stricmp(Cmd, "status") && !_stricmp(Folder, Dest)) { char *f = Fields; LAutoString Field = ImapBasicTokenize(f); LAutoString Value = ImapBasicTokenize(f); if (Field && Value && !_stricmp(Field, "recent")) { *Recent = atoi(Value); Status = true; break; } } } } else LgiTrace("%s:%i - STATUS cmd failed.\n", _FL); CommandFinished(); } Unlock(); } return Status; } bool MailIMap::Poll(int *Recent, LArray *New) { bool Status = true; if (Lock(_FL)) { int Cmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i NOOP\r\n", Cmd); if (WriteBuf()) { ClearDialog(); if ((Status = ReadResponse(Cmd))) { int LocalRecent; if (!Recent) Recent = &LocalRecent; *Recent = 0; for (auto Dlg: Dialog) { if (Recent && stristr(Dlg, " RECENT")) { *Recent = atoi(Dlg + 2); } } if (*Recent && New) { Search(false, *New, "new"); } } CommandFinished(); } Unlock(); } return Status; } bool MailIMap::StartIdle() { bool Status = false; if (Lock(_FL)) { d->IdleCmd = d->NextCmd++; sprintf_s(Buf, sizeof(Buf), "A%4.4i IDLE\r\n", d->IdleCmd); Status = WriteBuf(); CommandFinished(); Unlock(); } return Status; } bool MailIMap::OnIdle(int Timeout, LString::Array &Resp) { bool Status = false; if (Lock(_FL)) { auto Blk = Socket->IsBlocking(); Socket->IsBlocking(false); d->ReadDebug = true; Read(NULL, Timeout); d->ReadDebug = false; Socket->IsBlocking(Blk); Resp.SetFixedLength(false); char *Dlg; while ((Dlg = Dialog[0])) { Dialog.Delete(Dlg); Log(Dlg, LSocketI::SocketMsgReceive); if (Dlg[0] == '*' && Dlg[1] == ' ') { Resp.New() = Dlg; Status = true; } DeleteArray(Dlg); } Unlock(); } return Status; } bool MailIMap::FinishIdle() { bool Status = false; if (Lock(_FL)) { if (WriteBuf(false, "DONE\r\n")) { Status = ReadResponse(d->IdleCmd); CommandFinished(); d->IdleCmd = -1; } Unlock(); } return Status; } diff --git a/src/common/Net/OAuth2.cpp b/src/common/Net/OAuth2.cpp --- a/src/common/Net/OAuth2.cpp +++ b/src/common/Net/OAuth2.cpp @@ -1,507 +1,507 @@ #include "lgi/common/Lgi.h" #include "lgi/common/TextLog.h" #include "lgi/common/OpenSSLSocket.h" #include "lgi/common/Base64.h" #include "lgi/common/NetTools.h" #include "lgi/common/OAuth2.h" #include "lgi/common/Json.h" ////////////////////////////////////////////////////////////////// #define LOCALHOST_PORT 54900 #define OPT_AccessToken "AccessToken" #define OPT_RefreshToken "RefreshToken" static LString GetHeaders(LSocketI *s) { char Buf[256]; ssize_t Rd; LString p; while ((Rd = s->Read(Buf, sizeof(Buf))) > 0) { p += LString(Buf, Rd); if (p.Find("\r\n\r\n") >= 0) return p; } s->Close(); return NULL; } ssize_t ChunkSize(ssize_t &Pos, LString &Buf, LString &Body) { static LString Eol("\r\n"); auto End = Buf.Find(Eol, Pos); if (End > Pos) { auto Sz = Buf(Pos, End).Int(16); if (Sz >= 0) { End += Eol.Length(); auto Bytes = End + Sz + Eol.Length(); if (Buf.Length() >= Bytes) { Body += Buf(End, End + Sz); Pos = End + Sz + Eol.Length(); return Sz; } } } return -1; } static bool GetHttp(LSocketI *s, LString &Hdrs, LString &Body, bool IsResponse) { LString Resp = GetHeaders(s); char Buf[512]; ssize_t Rd; auto BodyPos = Resp.Find("\r\n\r\n"); LAutoString Len(InetGetHeaderField(Resp, "Content-Length", BodyPos)); if (Len) { int Bytes = atoi(Len); size_t Total = BodyPos + 4 + Bytes; while (Resp.Length() < Total) { Rd = s->Read(Buf, sizeof(Buf)); if (Rd > 0) { Resp += LString(Buf, Rd); } } } else if (s->IsOpen() && IsResponse) { LAutoString Te(InetGetHeaderField(Resp, "Transfer-Encoding", BodyPos)); bool Chunked = Te && !_stricmp(Te, "chunked"); if (Chunked) { ssize_t Pos = 0; Hdrs = Resp(0, BodyPos); LString Raw = Resp(BodyPos + 4, -1); Body.Empty(); while (s->IsOpen()) { auto Sz = ChunkSize(Pos, Raw, Body); if (Sz == 0) break; if (Sz < 0) { Rd = s->Read(Buf, sizeof(Buf)); if (Rd > 0) Raw += LString(Buf, Rd); else break; } } return true; } else { while ((Rd = s->Read(Buf, sizeof(Buf))) > 0) Resp += LString(Buf, Rd); } } Hdrs = Resp(0, BodyPos); Body = Resp(BodyPos + 4, -1); return true; } static LString UrlFromHeaders(LString Hdrs) { auto Lines = Hdrs.Split("\r\n", 1); auto p = Lines[0].SplitDelimit(); if (p.Length() < 3) { return NULL; } return p[1]; } static bool Write(LSocketI *s, LString b) { for (size_t i = 0; i < b.Length(); ) { auto Wr = s->Write(b.Get() + i, b.Length() - i); if (Wr <= 0) return false; i += Wr; } return true; } static LString FormEncode(const char *s, bool InValue = true) { LStringPipe p; for (auto c = s; *c; c++) { if (isalpha(*c) || isdigit(*c) || *c == '_' || *c == '.' || (!InValue && *c == '+') || *c == '-' || *c == '%') { p.Write(c, 1); } else if (*c == ' ') { p.Write((char*)"+", 1); } else { p.Print("%%%02.2X", *c); } } - return p.NewGStr(); + return p.NewLStr(); } struct LOAuth2Priv { LOAuth2::Params Params; LString Id; LStream *Log; LString Token; LString CodeVerifier; LStringPipe LocalLog; LDom *Store; LCancel *Cancel; LString AccessToken, RefreshToken; int64 ExpiresIn; struct Server : public LSocket { LSocket Listen; LOAuth2Priv *d; LSocket s; public: LHashTbl,LString> Params; LString Body; Server(LOAuth2Priv *cd) : d(cd) { auto Start = LCurrentTime(); while ( !d->Cancel->IsCancelled() && !Listen.Listen(LOCALHOST_PORT) && (LCurrentTime() - Start) < 60000) { d->Log->Print("Error: Can't listen on %i... (%s)\n", LOCALHOST_PORT, Listen.GetErrorString()); LSleep(1000); } } bool GetReq() { while (!d->Cancel->IsCancelled()) { if (Listen.IsReadable(100) && Listen.Accept(&s)) { // Read access code out of response LString Hdrs; if (GetHttp(&s, Hdrs, Body, false)) { auto Url = UrlFromHeaders(Hdrs); auto Vars = Url.Split("?", 1); if (Vars.Length() != 2) { return false; } Vars = Vars[1].Split("&"); for (auto v : Vars) { auto p = v.Split("=", 1); if (p.Length() != 2) continue; Params.Add(p[0], p[1]); } return true; } } } return false; } bool Response(const char *Txt) { LString Msg; Msg.Printf("HTTP/1.0 200 OK\r\n" "\r\n" "\n" "%s\n" "", Txt); return ::Write(&s, Msg); } }; LString Base64(LString s) { LString b; b.Length(BufferLen_BinTo64(s.Length())); ConvertBinaryToBase64(b.Get(), b.Length(), (uchar*)s.Get(), s.Length()); b.Get()[b.Length()] = 0; return b; } LString ToText(LString Bin) { LArray t; for (char i='0'; i<='9'; i++) t.Add(i); for (char i='a'; i<='z'; i++) t.Add(i); for (char i='A'; i<='Z'; i++) t.Add(i); t.Add('-'); t.Add('.'); t.Add('_'); t.Add('~'); LString Txt; Txt.Length(Bin.Length()); int Pos = 0; for (int i=0; iPrint("%s:%i - Uri: %s\n", _FL, Uri.Get()); LExecute(Uri); // Open browser for user to auth if (Svr.GetReq()) { Token = Svr.Params.Find("code"); Svr.Response(Token ? "Ok: Got token. You can close this window/tab now." : "Error: No token."); } return Token != NULL; } bool GetAccess() { if (!AccessToken) { LStringPipe p(1024); LUri u(Params.ApiUri); SslSocket sock(NULL, NULL, true); if (!sock.Open(u.sHost, HTTPS_PORT)) { Log->Print("Error: Can't connect to '%s:%i'\n", u.sHost.Get(), HTTPS_PORT); return NULL; } LString Body, Http; Body.Printf("code=%s&" "client_id=%s&" "redirect_uri=http://localhost:%i&" "code_verifier=%s&" "grant_type=authorization_code", FormEncode(Token).Get(), Params.ClientID.Get(), LOCALHOST_PORT, FormEncode(CodeVerifier).Get()); if (Params.ClientSecret) { Body += "&client_secret="; Body += Params.ClientSecret; } LUri Api(Params.ApiUri); Http.Printf("POST %s HTTP/1.1\r\n" "Host: %s\r\n" "Content-Type: application/x-www-form-urlencoded\r\n" "Content-length: " LPrintfSizeT "\r\n" "\r\n" "%s", Api.sPath.Get(), Api.sHost.Get(), Body.Length(), Body.Get()); if (!Write(&sock, Http)) { Log->Print("%s:%i - Error writing to socket.\n", _FL); return false; } LString Hdrs; if (!GetHttp(&sock, Hdrs, Body, true)) { return false; } // Log->Print("Body=%s\n", Body.Get()); LJson j(Body); AccessToken = j.Get("access_token"); RefreshToken = j.Get("refresh_token"); ExpiresIn = j.Get("expires_in").Int(); if (!AccessToken) Log->Print("Failed to get AccessToken: %s\n", Body.Get()); } return AccessToken.Get() != NULL; } bool Refresh() { if (!RefreshToken) return false; LStringPipe p(1024); LUri u(Params.Scope); SslSocket sock(NULL, NULL, true); if (!sock.Open(u.sHost, HTTPS_PORT)) { Log->Print("Error: Can't connect to '%s:%i'\n", u.sHost.Get(), HTTPS_PORT); return NULL; } LString Body, Http; Body.Printf("refresh_token=%s&" "client_id=%s&" "client_secret=%s&" "grant_type=refresh_token", FormEncode(RefreshToken).Get(), Params.ClientID.Get(), Params.ClientSecret.Get()); LUri Api(Params.ApiUri); Http.Printf("POST %s HTTP/1.1\r\n" "Host: %s\r\n" "Content-Type: application/x-www-form-urlencoded\r\n" "Content-length: " LPrintfSizeT "\r\n" "\r\n" "%s", Api.sPath.Get(), Api.sHost.Get(), Body.Length(), Body.Get()); if (!Write(&sock, Http)) { Log->Print("%s:%i - Error writing to socket.\n", _FL); return false; } LString Hdrs; if (!GetHttp(&sock, Hdrs, Body, true)) { return false; } // Log->Print("Body=%s\n%s\n", Params.ApiUri.Get(), Body.Get()); LJson j(Body); AccessToken = j.Get("access_token"); ExpiresIn = j.Get("expires_in").Int(); return AccessToken.Get() != NULL; } LOAuth2Priv(LOAuth2::Params ¶ms, const char *account, LDom *store, LStream *log, LCancel *cancel) { Params = params; Id = account; Store = store; Cancel = cancel; Log = log ? log : &LocalLog; } bool Serialize(bool Write) { if (!Store) return false; LVariant v; LString Key, kAccTok, kRefreshTok; Key.Printf("%s.%s", Params.Scope.Get(), Id.Get()); auto KeyB64 = Base64(Key); kAccTok.Printf("OAuth2-%s-%s", OPT_AccessToken, KeyB64.Get()); kAccTok = kAccTok.RStrip("="); kRefreshTok.Printf("OAuth2-%s-%s", OPT_RefreshToken, KeyB64.Get()); kRefreshTok= kRefreshTok.RStrip("="); if (Write) { Store->SetValue(kAccTok, v = AccessToken.Get()); Store->SetValue(kRefreshTok, v = RefreshToken.Get()); } else { if (Store->GetValue(kAccTok, v)) AccessToken = v.Str(); else return false; if (Store->GetValue(kRefreshTok, v)) RefreshToken = v.Str(); else return false; } return true; } }; LOAuth2::LOAuth2(LOAuth2::Params ¶ms, const char *account, LDom *store, LCancel *cancel, LStream *log) { d = new LOAuth2Priv(params, account, store, log, cancel); d->Serialize(false); } LOAuth2::~LOAuth2() { d->Serialize(true); delete d; } bool LOAuth2::Refresh() { d->AccessToken.Empty(); d->Serialize(true); return d->Refresh(); } LString LOAuth2::GetAccessToken() { if (d->AccessToken) return d->AccessToken; if (d->GetToken()) { d->Log->Print("Got token.\n"); if (d->GetAccess()) return d->AccessToken; else d->Log->Print("No access.\n"); } else d->Log->Print("No token.\n"); return LString(); } diff --git a/src/common/Net/OpenSSLSocket.cpp b/src/common/Net/OpenSSLSocket.cpp --- a/src/common/Net/OpenSSLSocket.cpp +++ b/src/common/Net/OpenSSLSocket.cpp @@ -1,1481 +1,1481 @@ /*hdr ** FILE: OpenSSLSocket.cpp ** AUTHOR: Matthew Allen ** DATE: 24/9/2004 ** DESCRIPTION: Open SSL wrapper socket ** ** Copyright (C) 2004-2014, Matthew Allen ** fret@memecode.com ** */ #include #ifdef WINDOWS #pragma comment(lib,"Ws2_32.lib") #else #include #endif #include "lgi/common/Lgi.h" #include "lgi/common/OpenSSLSocket.h" #include #ifdef WIN32 #include #endif #include "lgi/common/Variant.h" #include "lgi/common/Net.h" #define PATH_OFFSET "../" LString LibName(const char *Fmt) { LString s; #if defined(HAIKU) s = LString(Fmt).Strip("."); #elif defined(OPENSSL_SHLIB_VERSION) s.Printf(Fmt, OPENSSL_SHLIB_VERSION); #elif defined(MAC) s.Printf(Fmt, 3); #else s.Printf(Fmt, 1); #endif #ifdef _WIN64 s += "-x64"; #endif return s; } #ifdef WIN32 #define SSL_LIBRARY LibName("libssl-%i") #define EAY_LIBRARY LibName("libcrypto-%i") #elif defined(LINUX) // Building openssl on linux: // git clone https://github.com/openssl/openssl.git // cd openssl // ./config // make -j8 #define SSL_LIBRARY LibName("libssl.so.%i") #else // Building openssl on mac: // ./configure darwin64-x86_64-cc -mmacosx-version-min=10.10 #define SSL_LIBRARY LibName("libssl.%i") #endif #define HasntTimedOut() ((To < 0) || (LCurrentTime() - Start < To)) static const char* MinimumVersion = "3.0.1"; void SSL_locking_function(int mode, int n, const char *file, int line); unsigned long SSL_id_function(); class LibSSL : public LLibrary { public: LibSSL() { #if defined MAC char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), LGetExeFile(), LString("Contents/Frameworks/") + SSL_LIBRARY); if (!Load(p)) LgiTrace("%s:%i - Failed to load '%s'\n", _FL, p); #elif defined LINUX char p[MAX_PATH_LEN]; if (LMakePath(p, sizeof(p), LGetExePath(), SSL_LIBRARY)) { if (LFileExists(p)) { auto result = Load(p); LgiTrace("%s:%i - Local SSL '%s' = %i\n", _FL, p, result); } else LgiTrace("%s:%i - No local SSL library '%s'\n", _FL, p); } #endif if (!IsLoaded()) Load(SSL_LIBRARY); if (!IsLoaded()) { #ifdef WIN32 char p[MAX_PATH_LEN], leaf[32]; int bits = sizeof(size_t)*8; sprintf_s(leaf, sizeof(leaf), "OpenSSL-Win%i", bits); LMakePath(p, sizeof(p), LGetSystemPath(LSP_USER_APPS, bits), leaf); auto prev = FileDev->GetCurrentFolder(); FileDev->SetCurrentFolder(p); LMakePath(p, sizeof(p), p, SSL_LIBRARY); auto loaded = Load(p); FileDev->SetCurrentFolder(prev); #endif } } #if OPENSSL_VERSION_NUMBER >= 0x10100000L DynFunc0(int, OPENSSL_library_init); DynFunc0(int, OPENSSL_load_error_strings); DynFunc2(int, OPENSSL_init_ssl, uint64_t, opts, const OPENSSL_INIT_SETTINGS *, settings); DynFunc0(const SSL_METHOD *, TLS_method); DynFunc0(const SSL_METHOD *, TLS_server_method); DynFunc0(const SSL_METHOD *, TLS_client_method); #else DynFunc0(int, SSL_library_init); DynFunc0(int, SSL_load_error_strings); DynFunc0(SSL_METHOD*, SSLv23_client_method); DynFunc0(SSL_METHOD*, SSLv23_server_method); #endif DynFunc1(int, SSL_open, SSL*, s); DynFunc1(int, SSL_connect, SSL*, s); DynFunc4(long, SSL_ctrl, SSL*, ssl, int, cmd, long, larg, void*, parg); DynFunc1(int, SSL_shutdown, SSL*, s); DynFunc1(int, SSL_free, SSL*, ssl); DynFunc1(int, SSL_get_fd, const SSL *, s); DynFunc2(int, SSL_set_fd, SSL*, s, int, fd); DynFunc1(SSL*, SSL_new, SSL_CTX*, ctx); DynFunc1(BIO*, BIO_new_ssl_connect, SSL_CTX*, ctx); DynFunc1(X509*, SSL_get_peer_certificate, SSL*, s); DynFunc1(int, SSL_set_connect_state, SSL*, s); DynFunc1(int, SSL_set_accept_state, SSL*, s); DynFunc2(int, SSL_get_error, SSL*, s, int, ret_code); DynFunc3(int, SSL_set_bio, SSL*, s, BIO*, rbio, BIO*, wbio); DynFunc3(int, SSL_write, SSL*, ssl, const void*, buf, int, num); DynFunc3(int, SSL_read, SSL*, ssl, const void*, buf, int, num); DynFunc1(int, SSL_pending, SSL*, ssl); DynFunc1(BIO *, SSL_get_rbio, const SSL *, s); DynFunc1(int, SSL_accept, SSL *, ssl); DynFunc1(SSL_CTX*, SSL_CTX_new, const SSL_METHOD*, meth); DynFunc3(int, SSL_CTX_load_verify_locations, SSL_CTX*, ctx, const char*, CAfile, const char*, CApath); DynFunc3(int, SSL_CTX_use_certificate_file, SSL_CTX*, ctx, const char*, file, int, type); DynFunc3(int, SSL_CTX_use_PrivateKey_file, SSL_CTX*, ctx, const char*, file, int, type); DynFunc1(int, SSL_CTX_check_private_key, const SSL_CTX*, ctx); DynFunc1(int, SSL_CTX_free, SSL_CTX*, ctx); #ifdef WIN32 // If this is freaking you out then good... openssl-win32 ships // in 2 DLL's and on Linux everything is 1 shared object. Thus // the code reflects that. }; class LibEAY : public LLibrary { public: LibEAY() : LLibrary(EAY_LIBRARY) { if (!IsLoaded()) { #ifdef WIN32 char p[MAX_PATH_LEN], leaf[32]; int bits = sizeof(size_t)*8; sprintf_s(leaf, sizeof(leaf), "OpenSSL-Win%i", bits); LMakePath(p, sizeof(p), LGetSystemPath(LSP_USER_APPS, bits), leaf); auto prev = FileDev->GetCurrentFolder(); FileDev->SetCurrentFolder(p); LMakePath(p, sizeof(p), p, EAY_LIBRARY); auto loaded = Load(p); FileDev->SetCurrentFolder(prev); #endif } } #endif typedef void (*locking_callback)(int mode,int type, const char *file,int line); typedef unsigned long (*id_callback)(); DynFunc1(BIO*, BIO_new, BIO_METHOD*, type); DynFunc0(BIO_METHOD*, BIO_s_socket); DynFunc0(BIO_METHOD*, BIO_s_mem); DynFunc1(BIO*, BIO_new_connect, char *, host_port); DynFunc4(long, BIO_ctrl, BIO*, bp, int, cmd, long, larg, void*, parg); DynFunc4(long, BIO_int_ctrl, BIO *, bp, int, cmd, long, larg, int, iarg); DynFunc3(int, BIO_read, BIO*, b, void*, data, int, len); DynFunc3(int, BIO_write, BIO*, b, const void*, data, int, len); DynFunc1(int, BIO_free, BIO*, a); DynFunc1(int, BIO_free_all, BIO*, a); DynFunc2(int, BIO_test_flags, const BIO *, b, int, flags); DynFunc1(int, BIO_get_retry_reason, BIO *, bio); DynFunc0(int, ERR_load_BIO_strings); #if OPENSSL_VERSION_NUMBER < 0x10100000L DynFunc0(int, ERR_free_strings); DynFunc0(int, EVP_cleanup); DynFunc0(int, OPENSSL_add_all_algorithms_noconf); DynFunc1(int, CRYPTO_set_locking_callback, locking_callback, func); DynFunc1(int, CRYPTO_set_id_callback, id_callback, func); DynFunc0(int, CRYPTO_num_locks); DynFunc1(const char *, SSLeay_version, int, type); #else DynFunc2(int, OPENSSL_init_crypto, uint64_t, opts, const OPENSSL_INIT_SETTINGS *, settings); DynFunc1(const char *, OpenSSL_version, int, type); #endif DynFunc1(const char *, ERR_lib_error_string, unsigned long, e); DynFunc1(const char *, ERR_func_error_string, unsigned long, e); DynFunc1(const char *, ERR_reason_error_string, unsigned long, e); DynFunc1(int, ERR_print_errors, BIO *, bp); DynFunc3(char*, X509_NAME_oneline, X509_NAME*, a, char*, buf, int, size); DynFunc1(X509_NAME*, X509_get_subject_name, X509*, a); DynFunc2(char*, ERR_error_string, unsigned long, e, char*, buf); DynFunc0(unsigned long, ERR_get_error); DynFunc2(int, RAND_bytes, unsigned char *, buf, int, num); }; typedef LArray SslVer; SslVer ParseSslVersion(const char *v) { auto t = LString(v).SplitDelimit("."); SslVer out; for (unsigned i=0; i(SslVer &a, SslVer &b) { return CompareSslVersion(a, b) > 0; } static const char *FileLeaf(const char *f) { const char *l = strrchr(f, DIR_CHAR); return l ? l + 1 : f; } #undef _FL #define _FL FileLeaf(__FILE__), __LINE__ class OpenSSL : #ifdef WINDOWS public LibEAY, #endif public LibSSL { SSL_CTX *Server; public: SSL_CTX *Client; LArray Locks; LString ErrorMsg; bool IsLoaded() { return LibSSL::IsLoaded() #ifdef WINDOWS && LibEAY::IsLoaded() #endif ; } bool InitLibrary(SslSocket *sock) { LStringPipe Err; LArray Ver; LArray MinimumVer = ParseSslVersion(MinimumVersion); LString::Array t; int Len = 0; const char *v = NULL; if (!IsLoaded()) { #ifdef EAY_LIBRARY Err.Print("%s:%i - SSL libraries missing (%s, %s)\n", _FL, SSL_LIBRARY.Get(), EAY_LIBRARY.Get()); #else Err.Print("%s:%i - SSL library missing (%s)\n", _FL, SSL_LIBRARY.Get()); #endif goto OnError; } SSL_library_init(); SSL_load_error_strings(); ERR_load_BIO_strings(); OpenSSL_add_all_algorithms(); Len = CRYPTO_num_locks(); Locks.Length(Len); CRYPTO_set_locking_callback(SSL_locking_function); CRYPTO_set_id_callback(SSL_id_function); v = SSLeay_version(SSLEAY_VERSION); if (!v) { Err.Print("%s:%i - SSLeay_version failed.\n", _FL); goto OnError; } t = LString(v).SplitDelimit(" "); if (t.Length() < 2) { Err.Print("%s:%i - SSLeay_version: no version\n", _FL); goto OnError; } Ver = ParseSslVersion(t[1]); if (Ver.Length() < 3) { Err.Print("%s:%i - SSLeay_version: not enough tokens\n", _FL); goto OnError; } if (Ver < MinimumVer) { #if WINDOWS char FileName[MAX_PATH_LEN] = ""; DWORD r = GetModuleFileNameA(LibEAY::Handle(), FileName, sizeof(FileName)); #endif Err.Print("%s:%i - SSL version '%s' is too old (minimum '%s')\n" #if WINDOWS "%s\n" #endif , _FL, t[1].Get(), MinimumVersion #if WINDOWS ,FileName #endif ); goto OnError; } Client = SSL_CTX_new(SSLv23_client_method()); if (!Client) { long e = ERR_get_error(); char *Msg = ERR_error_string(e, 0); Err.Print("%s:%i - SSL_CTX_new(client) failed with '%s' (%i)\n", _FL, Msg, e); goto OnError; } return true; OnError: - ErrorMsg = Err.NewGStr(); + ErrorMsg = Err.NewLStr(); if (sock) sock->DebugTrace("%s", ErrorMsg.Get()); return false; } OpenSSL() { Client = NULL; Server = NULL; } ~OpenSSL() { if (Client) { SSL_CTX_free(Client); Client = NULL; } if (Server) { SSL_CTX_free(Server); Server = NULL; } Locks.DeleteObjects(); } SSL_CTX *GetServer(SslSocket *sock, const char *CertFile, const char *KeyFile) { if (!Server) { Server = SSL_CTX_new(SSLv23_server_method()); if (Server) { if (CertFile) SSL_CTX_use_certificate_file(Server, CertFile, SSL_FILETYPE_PEM); if (KeyFile) SSL_CTX_use_PrivateKey_file(Server, KeyFile, SSL_FILETYPE_PEM); if (!SSL_CTX_check_private_key(Server)) { LAssert(0); } } else { long e = ERR_get_error(); char *Msg = ERR_error_string(e, 0); LStringPipe p; p.Print("%s:%i - SSL_CTX_new(server) failed with '%s' (%i)\n", _FL, Msg, e); - ErrorMsg = p.NewGStr(); + ErrorMsg = p.NewLStr(); sock->DebugTrace("%s", ErrorMsg.Get()); } } return Server; } bool IsOk(SslSocket *sock) { bool Loaded = #ifdef WIN32 LibSSL::IsLoaded() && LibEAY::IsLoaded(); #else IsLoaded(); #endif if (Loaded) return true; // Try and load again... cause the library can be provided by install on demand. #ifdef WIN32 Loaded = LibSSL::Load(SSL_LIBRARY) && LibEAY::Load(EAY_LIBRARY); #else Loaded = Load(SSL_LIBRARY); #endif if (Loaded) InitLibrary(sock); return Loaded; } }; static OpenSSL *Library = 0; #if 0 #define SSL_DEBUG_LOCKING #endif void SSL_locking_function(int mode, int n, const char *file, int line) { LAssert(Library != NULL); if (Library) { if (!Library->Locks[n]) { #ifdef SSL_DEBUG_LOCKING LgiTrace("SSL[%i] create\n", n); #endif Library->Locks[n] = new LMutex("SSL_locking_function"); } #ifdef SSL_DEBUG_LOCKING LgiTrace("SSL[%i] lock=%i, unlock=%i, re=%i, wr=%i (mode=0x%x, cnt=%i, thr=0x%x, %s:%i)\n", n, TestFlag(mode, CRYPTO_LOCK), TestFlag(mode, CRYPTO_UNLOCK), TestFlag(mode, CRYPTO_READ), TestFlag(mode, CRYPTO_WRITE), mode, Library->Locks[n]->GetCount(), LGetCurrentThread(), file, line); #endif if (mode & CRYPTO_LOCK) Library->Locks[n]->Lock((char*)file, line); else if (mode & CRYPTO_UNLOCK) Library->Locks[n]->Unlock(); } } unsigned long SSL_id_function() { return (unsigned long) GetCurrentThreadId(); } bool StartSSL(LString &ErrorMsg, SslSocket *sock) { static LMutex Lock("StartSSL"); if (Lock.Lock(_FL)) { if (!Library) { Library = new OpenSSL; if (Library && !Library->InitLibrary(sock)) { ErrorMsg = Library->ErrorMsg; DeleteObj(Library); } } Lock.Unlock(); } return Library != NULL; } void EndSSL() { DeleteObj(Library); } struct SslSocketPriv : public LCancel { LCapabilityClient *Caps; bool SslOnConnect; bool IsSSL; bool UseSSLrw; int Timeout; bool RawLFCheck; #ifdef _DEBUG bool LastWasCR; #endif bool IsBlocking; LCancel *Cancel; // This is just for the UI. LStreamI *Logger; // This is for the connection logging. LAutoString LogFile; bool LogFileError = false; LAutoPtr LogStream; int LogFormat; SslSocketPriv() { #ifdef _DEBUG LastWasCR = false; #endif Cancel = this; Timeout = 20 * 1000; IsSSL = false; UseSSLrw = false; LogFormat = 0; } }; bool SslSocket::DebugLogging = false; SslSocket::SslSocket(LStreamI *logger, LCapabilityClient *caps, bool sslonconnect, bool RawLFCheck) : Lock("SslSocket") { d = new SslSocketPriv; Bio = 0; Ssl = 0; d->RawLFCheck = RawLFCheck; d->SslOnConnect = sslonconnect; d->Caps = caps; d->Logger = logger; d->IsBlocking = true; LString ErrMsg; if (StartSSL(ErrMsg, this)) { if (Library->IsOk(this)) { char s[MAX_PATH_LEN]; #ifdef WIN32 char n[MAX_PATH_LEN]; if (GetModuleFileNameA(Library->LibSSL::Handle(), n, sizeof(n))) { sprintf_s(s, sizeof(s), "Using '%s'", n); OnInformation(s); } if (GetModuleFileNameA(Library->LibEAY::Handle(), n, sizeof(n))) { sprintf_s(s, sizeof(s), "Using '%s'", n); OnInformation(s); } #else LString fp = Library->GetFullPath(); if (fp) { sprintf_s(s, sizeof(s), "Using '%s'", fp.Get()); OnInformation(s); } #endif } } else if (caps) { caps->NeedsCapability("openssl", ErrMsg); } else { OnError(0, "Can't load or find OpenSSL library."); } } SslSocket::~SslSocket() { Close(); DeleteObj(d); } LStreamI *SslSocket::Clone() { return new SslSocket(d->Logger, d->Caps, true); } LCancel *SslSocket::GetCancel() { return d->Cancel; } void SslSocket::SetCancel(LCancel *c) { d->Cancel = c; } int SslSocket::GetTimeout() { return d->Timeout; } void SslSocket::SetTimeout(int ms) { d->Timeout = ms; } void SslSocket::SetLogger(LStreamI *logger) { d->Logger = logger; } LStreamI *SslSocket::GetLog() { return d->Logger; } void SslSocket::SetSslOnConnect(bool b) { d->SslOnConnect = b; } LStream *SslSocket::GetLogStream() { if (!d->LogStream && d->LogFile && !d->LogFileError) { if (!d->LogStream.Reset(new LFile)) return NULL; if (!d->LogStream->Open(d->LogFile, O_WRITE)) { d->LogStream.Reset(); d->LogFileError = true; // Stop retrying the open and spamming the trace log. LgiTrace("%s:%i - Can't open '%s' for SSL logging.\n", _FL, d->LogFile.Get()); return NULL; } // Seek to the end d->LogStream->SetPos(d->LogStream->GetSize()); } return d->LogStream; } bool SslSocket::GetVariant(const char *Name, LVariant &Val, const char *Arr) { if (!Name) return false; if (!_stricmp(Name, "isSsl")) // Type: Bool { Val = true; return true; } return false; } void SslSocket::Log(const char *Str, ssize_t Bytes, SocketMsgType Type) { if (!ValidStr(Str)) return; if (d->Logger) d->Logger->Write(Str, Bytes<0?(int)strlen(Str):Bytes, Type); else if (Type == SocketMsgError) LgiTrace("%.*s", Bytes, Str); } const char *SslSocket::GetErrorString() { return ErrMsg; } void SslSocket::SslError(const char *file, int line, const char *Msg) { char *Part = strrchr((char*)file, DIR_CHAR); #ifndef WIN32 printf("%s:%i - %s\n", file, line, Msg); #endif ErrMsg.Printf("Error: %s:%i - %s\n", Part ? Part + 1 : file, line, Msg); Log(ErrMsg, ErrMsg.Length(), SocketMsgError); } OsSocket SslSocket::Handle(OsSocket Set) { OsSocket h = INVALID_SOCKET; if (Set != INVALID_SOCKET) { long r; bool IsError = false; if (!Ssl) { Ssl = Library->SSL_new(Library->GetServer(this, NULL, NULL)); } if (Ssl) { r = Library->SSL_set_fd(Ssl, (int) Set); Bio = Library->SSL_get_rbio(Ssl); r = Library->SSL_accept(Ssl); if (r <= 0) IsError = true; else if (r == 1) h = Set; } else IsError = true; if (IsError) { long e = Library->ERR_get_error(); char *Msg = Library->ERR_error_string(e, 0); Log(Msg, -1, SocketMsgError); return INVALID_SOCKET; } } else if (Bio) { int hnd = (int)INVALID_SOCKET; Library->BIO_get_fd(Bio, &hnd); h = hnd; } return h; } bool SslSocket::IsOpen() { return Bio != 0 && !d->Cancel->IsCancelled(); } LString SslGetErrorAsString(OpenSSL *Library) { BIO *bio = Library->BIO_new (Library->BIO_s_mem()); Library->ERR_print_errors (bio); char *buf = NULL; size_t len = Library->BIO_get_mem_data (bio, &buf); LString s(buf, len); Library->BIO_free (bio); return s; } int SslSocket::Open(const char *HostAddr, int Port) { bool Status = false; LMutex::Auto Lck(&Lock, _FL); DebugTrace("%s:%i - SslSocket::Open(%s,%i)\n", _FL, HostAddr, Port); if (Library && Library->IsOk(this) && HostAddr) { char h[256]; sprintf_s(h, sizeof(h), "%s:%i", HostAddr, Port); // Do SSL handshake? if (d->SslOnConnect) { // SSL connection.. d->IsSSL = true; if (Library->Client) { const char *CertDir = "/u/matthew/cert"; int r = Library->SSL_CTX_load_verify_locations(Library->Client, 0, CertDir); DebugTrace("%s:%i - SSL_CTX_load_verify_locations=%i\n", _FL, r); if (r > 0) { Bio = Library->BIO_new_ssl_connect(Library->Client); DebugTrace("%s:%i - BIO_new_ssl_connect=%p\n", _FL, Bio); if (Bio) { Library->BIO_get_ssl(Bio, &Ssl); DebugTrace("%s:%i - BIO_get_ssl=%p\n", _FL, Ssl); if (Ssl) { // SNI setup Library->SSL_set_tlsext_host_name(Ssl, HostAddr); // Library->SSL_CTX_set_timeout() Library->BIO_set_conn_hostname(Bio, HostAddr); #if OPENSSL_VERSION_NUMBER < 0x10100000L Library->BIO_set_conn_int_port(Bio, &Port); #else LString sPort; sPort.Printf("%i", Port); Library->BIO_set_conn_port(Bio, sPort.Get()); #endif // Do non-block connect uint64 Start = LCurrentTime(); int To = GetTimeout(); IsBlocking(false); r = Library->SSL_connect(Ssl); DebugTrace("%s:%i - initial SSL_connect=%i\n", _FL, r); while (r != 1 && !d->Cancel->IsCancelled()) { int err = Library->SSL_get_error(Ssl, r); if (err != SSL_ERROR_WANT_CONNECT) { DebugTrace("%s:%i - SSL_get_error=%i\n", _FL, err); } LSleep(50); try { r = Library->SSL_connect(Ssl); } catch (...) { r = -1; LgiTrace("%s:%i - SSL_connect crashed.\n", _FL); } DebugTrace("%s:%i - SSL_connect=%i (%i of %i ms)\n", _FL, r, (int)(LCurrentTime() - Start), (int)To); bool TimeOut = !HasntTimedOut(); if (TimeOut) { DebugTrace("%s:%i - SSL connect timeout, to=%i\n", _FL, To); SslError(_FL, "Connection timeout."); break; } } DebugTrace("%s:%i - open loop finished, r=%i, Cancelled=%i\n", _FL, r, d->Cancel->IsCancelled()); if (r == 1) { IsBlocking(true); Library->SSL_set_mode(Ssl, SSL_MODE_AUTO_RETRY); Status = true; // d->UseSSLrw = true; char m[256]; sprintf_s(m, sizeof(m), "Connected to '%s' using SSL", h); OnInformation(m); } else if (!d->Cancel->IsCancelled()) { LString Err = SslGetErrorAsString(Library).Strip(); if (!Err) Err.Printf("BIO_do_connect(%s:%i) failed.", HostAddr, Port); SslError(_FL, Err); } } else SslError(_FL, "BIO_get_ssl failed."); } else SslError(_FL, "BIO_new_ssl_connect failed."); } else SslError(_FL, "SSL_CTX_load_verify_locations failed."); } else SslError(_FL, "No Ctx."); } else { Bio = Library->BIO_new_connect(h); DebugTrace("%s:%i - BIO_new_connect=%p\n", _FL, Bio); if (Bio) { // Non SSL... go into non-blocking mode so that if ::Close() is called we // can quit out of the connect loop. IsBlocking(false); uint64 Start = LCurrentTime(); int To = GetTimeout(); long r = Library->BIO_do_connect(Bio); DebugTrace("%s:%i - BIO_do_connect=%i\n", _FL, r); while (r != 1 && !d->Cancel->IsCancelled()) { if (!Library->BIO_should_retry(Bio)) { break; } LSleep(50); r = Library->BIO_do_connect(Bio); DebugTrace("%s:%i - BIO_do_connect=%i\n", _FL, r); if (!HasntTimedOut()) { DebugTrace("%s:%i - open timeout, to=%i\n", _FL, To); OnError(0, "Connection timeout."); break; } } DebugTrace("%s:%i - open loop finished=%i\n", _FL, r); if (r == 1) { IsBlocking(true); Status = true; char m[256]; sprintf_s(m, sizeof(m), "Connected to '%s'", h); OnInformation(m); } else SslError(_FL, "BIO_do_connect failed"); } else SslError(_FL, "BIO_new_connect failed"); } } if (!Status) { Close(); } DebugTrace("%s:%i - SslSocket::Open status=%i\n", _FL, Status); return Status; } bool SslSocket::SetVariant(const char *Name, LVariant &Value, const char *Arr) { bool Status = false; if (!Library || !Name) return false; if (!_stricmp(Name, SslSocket_LogFile)) { d->LogFile.Reset(Value.ReleaseStr()); } else if (!_stricmp(Name, SslSocket_LogFormat)) { d->LogFormat = Value.CastInt32(); } else if (!_stricmp(Name, LSocket_Protocol)) { char *v = Value.CastString(); if (v && stristr(v, "SSL")) { if (!Bio) { d->SslOnConnect = true; } else { if (!Library->Client) { SslError(_FL, "Library->Client is null."); } else { Ssl = Library->SSL_new(Library->Client); DebugTrace("%s:%i - SSL_new=%p\n", _FL, Ssl); if (!Ssl) { SslError(_FL, "SSL_new failed."); } else { int r = Library->SSL_set_bio(Ssl, Bio, Bio); DebugTrace("%s:%i - SSL_set_bio=%i\n", _FL, r); uint64 Start = LCurrentTime(); int To = GetTimeout(); while (HasntTimedOut()) { r = Library->SSL_connect(Ssl); DebugTrace("%s:%i - SSL_connect=%i\n", _FL, r); if (r < 0) LSleep(100); else break; } if (r > 0) { Status = d->UseSSLrw = d->IsSSL = true; OnInformation("Session is now using SSL"); X509 *ServerCert = Library->SSL_get_peer_certificate(Ssl); DebugTrace("%s:%i - SSL_get_peer_certificate=%p\n", _FL, ServerCert); if (ServerCert) { char Txt[256] = ""; Library->X509_NAME_oneline(Library->X509_get_subject_name(ServerCert), Txt, sizeof(Txt)); DebugTrace("%s:%i - X509_NAME_oneline=%s\n", _FL, Txt); OnInformation(Txt); } // SSL_get_verify_result } else { SslError(_FL, "SSL_connect failed."); r = Library->SSL_get_error(Ssl, r); char *Msg = Library->ERR_error_string(r, 0); if (Msg) { OnError(r, Msg); } } } } } } } return Status; } int SslSocket::Close() { bool Prev = d->Cancel->IsCancelled(); d->Cancel->Cancel(); LMutex::Auto Lck(&Lock, _FL); if (!Library) { LgiTrace("%s:%i - Error: no library.\n", _FL); return false; } if (Bio) { auto result = Library->BIO_free_all(Bio); if (result != 1) printf("%s:%i result =%i\n", _FL, result); } Ssl = NULL; Bio = NULL; d->Cancel->Cancel(Prev); return true; } bool SslSocket::Listen(int Port) { return false; } bool SslSocket::IsBlocking() { return d->IsBlocking; } void SslSocket::IsBlocking(bool block) { d->IsBlocking = block; if (Bio) Library->BIO_set_nbio(Bio, !d->IsBlocking); } bool SslSocket::IsReadable(int TimeoutMs) { // Assign to local var to avoid a thread changing it // on us between the validity check and the select. // Which is important because a socket value of -1 // (ie invalid) will crash the FD_SET macro. OsSocket s = Handle(); if (!d->Cancel->IsCancelled() && ValidSocket(s)) { struct timeval t = {TimeoutMs / 1000, (TimeoutMs % 1000) * 1000}; fd_set r; FD_ZERO(&r); FD_SET(s, &r); int v = select((int)s+1, &r, 0, 0, &t); if (v > 0 && FD_ISSET(s, &r)) { return true; } else if (v < 0) { // Error(); } } else LgiTrace("%s:%i - Not a valid socket.\n", _FL); return false; } bool SslSocket::IsWritable(int TimeoutMs) { // Assign to local var to avoid a thread changing it // on us between the validity check and the select. // Which is important because a socket value of -1 // (ie invalid) will crash the FD_SET macro. OsSocket s = Handle(); if (!d->Cancel->IsCancelled() && ValidSocket(s)) { struct timeval t = {TimeoutMs / 1000, (TimeoutMs % 1000) * 1000}; fd_set w; FD_ZERO(&w); FD_SET(s, &w); int v = select((int)s+1, &w, 0, 0, &t); if (v > 0 && FD_ISSET(s, &w)) { return true; } else if (v < 0) { // Error(); } } else LgiTrace("%s:%i - Not a valid socket.\n", _FL); return false; } void SslSocket::OnWrite(const char *Data, ssize_t Len) { #ifdef _DEBUG if (d->RawLFCheck) { const char *End = Data + Len; while (Data < End) { LAssert(*Data != '\n' || d->LastWasCR); d->LastWasCR = *Data == '\r'; Data++; } } #endif // Log(Data, Len, SocketMsgSend); } void SslSocket::OnRead(char *Data, ssize_t Len) { #ifdef _DEBUG if (d->RawLFCheck) { const char *End = Data + Len; while (Data < End) { LAssert(*Data != '\n' || d->LastWasCR); d->LastWasCR = *Data == '\r'; Data++; } } #endif // Log(Data, Len, SocketMsgReceive); } ssize_t SslSocket::Write(const void *Data, ssize_t Len, int Flags) { LMutex::Auto Lck(&Lock, _FL); if (!Library || d->Cancel->IsCancelled()) return -1; if (!Bio) { DebugTrace("%s:%i - BIO is NULL\n", _FL); return -1; } int r = 0; if (d->UseSSLrw) { if (Ssl) { uint64 Start = LCurrentTime(); int To = GetTimeout(); while (HasntTimedOut()) { r = Library->SSL_write(Ssl, Data, (int)Len); if (r <= 0) { if (!Library->BIO_should_retry(Bio)) break; if (d->IsBlocking) LSleep(1); else break; } else { DebugTrace("%s:%i - SSL_write(%p,%i)=%i\n", _FL, Data, Len, r); OnWrite((const char*)Data, r); break; } } } else { r = -1; DebugTrace("%s:%i - No SSL\n", _FL); } } else { uint64 Start = LCurrentTime(); int To = GetTimeout(); while (HasntTimedOut()) { if (!Library) break; r = Library->BIO_write(Bio, Data, (int)Len); DebugTrace("%s:%i - BIO_write(%p,%i)=%i\n", _FL, Data, Len, r); if (r <= 0) { if (!Library->BIO_should_retry(Bio)) break; if (d->IsBlocking) LSleep(1); else break; } else { OnWrite((const char*)Data, r); break; } } } if (r > 0) { LStream *l = GetLogStream(); if (l) l->Write(Data, r); } else if (Ssl) { auto Err = Library->SSL_get_error(Ssl, r); if (Err == SSL_ERROR_ZERO_RETURN) { DebugTrace("%s:%i - ::Write closing %i\n", _FL, r); Close(); } else if (Err != SSL_ERROR_WANT_WRITE && Err != SSL_ERROR_SYSCALL) { char Buf[256] = ""; char *e = Library->ERR_error_string(Err, Buf); DebugTrace("%s:%i - ::Write error %i, %s\n", _FL, Err, e); OnError(Err, e ? e : "ERR_error_string failed"); } } return r; } ssize_t SslSocket::Read(void *Data, ssize_t Len, int Flags) { LMutex::Auto Lck(&Lock, _FL); if (!Library || d->Cancel->IsCancelled()) return -1; if (Bio) { int r = 0; if (d->UseSSLrw) { if (Ssl) { uint64 Start = LCurrentTime(); int To = GetTimeout(); while (HasntTimedOut()) { r = Library->SSL_read(Ssl, Data, (int)Len); DebugTrace("%s:%i - SSL_read(%p,%i)=%i\n", _FL, Data, Len, r); if (r < 0) { if (!Library->BIO_should_retry(Bio)) { DebugTrace("%s:%i - BIO_should_retry is false\n", _FL); break; } if (d->IsBlocking) LSleep(1); else break; } else { OnRead((char*)Data, r); break; } } } else { DebugTrace("%s:%i - Ssl is NULL\n", _FL); r = -1; } } else { uint64 Start = LCurrentTime(); int To = GetTimeout(); while (HasntTimedOut()) { r = Library->BIO_read(Bio, Data, (int)Len); DebugTrace("%s:%i - BIO_read(%p,%i)=%i\n", _FL, Data, Len, r); if (r < 0) { auto Retry = Library->BIO_should_retry(Bio); DebugTrace("%s:%i - BIO_should_retry=%i IsBlocking=%i\n", _FL, Retry, d->IsBlocking); if (!Retry) { Library->BIO_get_retry_reason(Bio); break; } if (d->IsBlocking) LSleep(1); else break; } else { if (r > 0) OnRead((char*)Data, r); break; } } } if (r > 0) { LStream *l = GetLogStream(); if (l) l->Write(Data, r); } else if (Ssl) { int Err = Library->SSL_get_error(Ssl, r); DebugTrace("%s:%i - SSL_get_error = %i\n", _FL, Err); if (Err == SSL_ERROR_ZERO_RETURN) { DebugTrace("%s:%i - ::Read closing %i\n", _FL, r); Close(); } else if (Err != SSL_ERROR_WANT_READ && Err != SSL_ERROR_SYSCALL) { char Buf[256]; char *e = Library->ERR_error_string(Err, Buf); OnError(Err, e ? e : "ERR_error_string failed"); } } return r; } return -1; } void SslSocket::OnError(int ErrorCode, const char *ErrorDescription) { DebugTrace("%s:%i - OnError=%i,%s\n", _FL, ErrorCode, ErrorDescription); LString s; s.Printf("Error %i: %s\n", ErrorCode, ErrorDescription); Log(s, s.Length(), SocketMsgError); } void SslSocket::DebugTrace(const char *fmt, ...) { if (DebugLogging) { char Buffer[512]; va_list Arg; va_start(Arg, fmt); int Ch = vsprintf_s(Buffer, sizeof(Buffer), fmt, Arg); va_end(Arg); if (Ch > 0) { #if 0 LgiTrace("SSL:%p: %s", this, Buffer); #else OnInformation(Buffer); #endif } } } void SslSocket::OnInformation(const char *Str) { while (Str && *Str) { LAutoString a; const char *nl = Str; while (*nl && *nl != '\n') nl++; int Len = (int) (nl - Str + 2); a.Reset(new char[Len]); char *o; for (o = a; Str < nl; Str++) { if (*Str != '\r') *o++ = *Str; } *o++ = '\n'; *o++ = 0; LAssert((o-a) <= Len); Log(a, -1, SocketMsgInfo); Str = *nl ? nl + 1 : nl; } } LString SslSocket::Random(int Len) { LString s; s.Length(Len); auto r = Library ? Library->RAND_bytes((uint8_t*) s.Get(), Len) : 0; return r ? s : NULL; } diff --git a/src/common/Net/Uri.cpp b/src/common/Net/Uri.cpp --- a/src/common/Net/Uri.cpp +++ b/src/common/Net/Uri.cpp @@ -1,379 +1,379 @@ #include "lgi/common/Lgi.h" #include "lgi/common/Net.h" #include "lgi/common/RegKey.h" ///////////////////////////////////////////////////////////////////////////////// static const char *Ws = " \t\r\n"; #define SkipWs(s) while (*s && strchr(Ws, *s)) s++; LUri::LUri(const char *uri) { Port = 0; if (uri) Set(uri); } LUri::~LUri() { Empty(); } LUri &LUri::operator +=(const char *s) { // Add segment to path if (!sPath.Length() || sPath(-1) != '/') sPath += IsFile() ? DIR_STR : "/"; auto len = sPath.Length(); sPath += s; if (IsFile()) { char *c = sPath.Get(); #if DIR_CHAR == '/' char from = '\\'; #else char from = '/'; #endif for (size_t i=len; c && i 1 && s[1] == '/' && s[2] == '/') { sProtocol.Set(p, s - p); s += 3; } else { // No protocol, so assume it's a host name or path s = p; } // Check for path bool HasPath = false; if ( (s[0] && s[1] == ':') || (s[0] == '/') || (s[0] == '\\') ) { HasPath = true; } else { // Scan over the host name p = s; while ( *s && *s > ' ' && *s < 127 && *s != '/' && *s != '\\') { s++; } sHost.Set(p, s - p); if (sHost) { char *At = strchr(sHost, '@'); if (At) { *At++ = 0; char *Col = strchr(sHost, ':'); if (Col) { *Col++ = 0; sPass = DecodeStr(Col); } sUser = DecodeStr(sHost); sHost = At; } char *Col = strchr(sHost, ':'); if (Col) { Port = atoi(Col+1); sHost.Length(Col-sHost.Get()); } } HasPath = *s == '/'; } if (HasPath) { const char *Start = s; while (*s && *s != '#') s++; if (*s) { sPath.Set(Start, s - Start); sAnchor = s + 1; } else { sPath = Start; } #if 0 // This decodes the path from %## encoding to raw characters. // However sometimes we need the encoded form. So instead of // doing the conversion here the caller has to do it now. char *i = Path, *o = Path; while (*i) { if (*i == '%' && i[1] && i[2]) { char h[3] = {i[1], i[2], 0}; *o++ = htoi(h); i+=2; } else { *o++ = *i; } i++; } *o = 0; #endif } return sHost || sPath; } LString LUri::EncodeStr(const char *s, const char *ExtraCharsToEncode) { LStringPipe p(256); if (s) { while (*s) { if (*s == ' ' || (ExtraCharsToEncode && strchr(ExtraCharsToEncode, *s))) { char h[4]; sprintf_s(h, sizeof(h), "%%%2.2X", (uint32_t)(uchar)*s++); p.Write(h, 3); } else { p.Write(s++, 1); } } } - return p.NewGStr(); + return p.NewLStr(); } LUri::StrMap LUri::Params() { StrMap m; if (sPath) { const char *q = strchr(sPath, '?'); if (q++) { auto Parts = LString(q).SplitDelimit("&"); for (auto p : Parts) { auto Var = p.Split("=", 1); if (Var.Length() == 2) m.Add(Var[0], Var[1]); } } } return m; } LString LUri::DecodeStr(const char *s) { LStringPipe p(256); if (s) { while (*s) { if (s[0] == '%' && s[1] && s[2]) { char h[3] = { s[1], s[2], 0 }; char c = htoi(h); p.Write(&c, 1); s += 3; } else { p.Write(s++, 1); } } } - return p.NewGStr(); + return p.NewLStr(); } #if defined LGI_CARBON int CFNumberRefToInt(CFNumberRef r, int Default = 0) { int i = Default; if (r && CFGetTypeID(r) == CFNumberGetTypeID()) { CFNumberGetValue(r, kCFNumberIntType, &r); } return i; } #endif LProxyUri::LProxyUri() { #if defined(WIN32) LRegKey k(false, "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"); if (k.IsOk()) { uint32_t Enabled = 0; if (k.GetInt("ProxyEnable", Enabled) && Enabled) { char *p = k.GetStr("ProxyServer"); if (p) { Set(p); } } } #elif defined LINUX char *HttpProxy = getenv("http_proxy"); if (HttpProxy) { Set(HttpProxy); } #elif defined MAC // CFDictionaryRef Proxies = SCDynamicStoreCopyProxies(0); // if (!Proxies) // LgiTrace("%s:%i - SCDynamicStoreCopyProxies failed.\n", _FL); // else // { // int enable = CFNumberRefToInt((CFNumberRef) CFDictionaryGetValue(Proxies, kSCPropNetProxiesHTTPEnable)); // if (enable) // { // #ifdef LGI_COCOA // LAssert(!"Fixme"); // #else // Host = CFStringToUtf8((CFStringRef) CFDictionaryGetValue(Proxies, kSCPropNetProxiesHTTPProxy)); // #endif // Port = CFNumberRefToInt((CFNumberRef) CFDictionaryGetValue(Proxies, kSCPropNetProxiesHTTPPort)); // } // // CFRelease(Proxies); // } #else #warning "Impl getting OS proxy here." #endif } diff --git a/src/common/Net/WebDav.cpp b/src/common/Net/WebDav.cpp --- a/src/common/Net/WebDav.cpp +++ b/src/common/Net/WebDav.cpp @@ -1,222 +1,222 @@ #include "lgi/common/Lgi.h" #include "lgi/common/WebDav.h" //////////////////////////////////////////////////////////////////////////////// bool LFindXml(LArray &Results, LXmlTag *t, const char *Name) { if (t->IsTag(Name)) Results.Add(t); for (auto i: t->Children) LFindXml(Results, i, Name); return Results.Length() > 0; } //////////////////////////////////////////////////////////////////////////////// LWebdav::LWebdav(LString endPoint, LString user, LString pass, LCancel *cancel) : Cancel(cancel) { EndPoint = endPoint; User = user; Pass = pass; LUri u(EndPoint); EndPointPath = u.sPath; } void LWebdav::PrettyPrint(LXmlTag &x) { LStringPipe p; LXmlTree t; t.Write(&x, &p); - auto s = p.NewGStr(); + auto s = p.NewLStr(); LgiTrace("Pretty: %s\n", s.Get()); } bool LWebdav::Request(Req &r, const char *Name, LString Resource) { auto sock = GetSocket(); LHttp http; LUri u(EndPoint); if (!http.Open(sock, u.sHost, u.Port ? u.Port : HTTPS_PORT)) { r.Status = false; } else { LStringPipe OutPipe, OutHdrsPipe; LString Delim("/"); auto Path = u.sPath.SplitDelimit(Delim); Path += Resource.SplitDelimit(Delim); auto Res = Delim + Delim.Join(Path); u.sPath = Res; auto Full = u.ToString(); LMemStream In(r.InBody.Get(), r.InBody.Length(), false); http.SetAuth(User, Pass); r.Status = http.Request(Name, Full, &r.ProtocolStatus, r.InHdrs, r.InBody ? &In : NULL, &OutPipe, &OutHdrsPipe, &r.Encoding); - r.OutHdrs = OutHdrsPipe.NewGStr(); - r.OutBody = OutPipe.NewGStr(); + r.OutHdrs = OutHdrsPipe.NewLStr(); + r.OutBody = OutPipe.NewLStr(); // LgiTrace("%s\n%s\n", r.OutHdrs.Get(), r.OutBody.Get()); } return r.Status; } LString::Array LWebdav::GetOptions(LString Resource) { LString::Array opts; Req r; if (Request(r, "OPTIONS", Resource)) { auto Lines = r.OutHdrs.SplitDelimit("\r\n"); for (auto Ln: Lines) { auto p = Ln.Split(":", 1); if (p.Length() < 2) continue; if (p[0].Strip().Equals("Allow")) { auto o = p[1].Split(","); for (auto i: o) opts.Add(i.Strip()); } } } for (auto o: opts) LgiTrace("opt=%s\n", o.Get()); return opts; } bool LWebdav::PropFind(LArray &Files, LString Resource, int Depth) { Req r; r.InBody = "\n" "\n" " \n" ""; r.InHdrs.Printf("Depth: %i\r\n" "Content-Type: text/xml; charset=\"utf-8\"\r\n" "Content-Length: %i\r\n", Depth, (int)r.InBody.Length()); if (!Request(r, "PROPFIND", Resource)) return false; LXmlTag x; LXmlTree t; LMemStream m(r.OutBody.Get(), r.OutBody.Length(), false); if (!t.Read(&x, &m)) return false; // PrettyPrint(&x); LArray Responses; if (!LFindXml(Responses, &x, "d:response")) return false; for (auto r: Responses) { auto Href = r->GetChildTag("d:href"); LXmlTag *FileProps = NULL; LArray Props; if (LFindXml(Props, r, "d:prop")) { for (auto p: Props) { auto Len = p->GetChildTag("d:getcontentlength"); if (Len) FileProps = p; } } if (Href && FileProps) { auto &f = Files.New(); f.Href = Href->GetContent(); if (f.Href.Find(EndPointPath) == 0) { f.Href = f.Href(EndPointPath.Length(),-1); } auto len = FileProps->GetChildTag("d:getcontentlength"); f.Length = Atoi(len->GetContent(), 10, 0); auto lastmodified = FileProps->GetChildTag("d:getlastmodified"); if (lastmodified) f.LastMod = lastmodified->GetContent(); auto etag = FileProps->GetChildTag("d:getetag"); if (etag) { f.Tag = etag->GetContent(); f.Tag = f.Tag.Strip("\""); } auto contentType = FileProps->GetChildTag("d:getcontenttype"); if (contentType) f.ContentType = contentType->GetContent(); } } return true; } bool LWebdav::Get(const char *Resource, LString &Data) { Req r; if (!Request(r, "GET", Resource)) return false; if (r.ProtocolStatus != 200) { LgiTrace("GetFailed: %i\n%s\n%s\n", r.ProtocolStatus, r.OutHdrs.Get(), r.OutBody.Get()); return false; } Data = r.OutBody; // LgiTrace("Webdav.data=%s\n", Data.Get()); return true; } bool LWebdav::Put(const char *Resource, LString &Data) { Req r; r.InBody = Data; r.InHdrs.Printf("Content-Type: text/calendar\r\n" "Content-Length: " LPrintfInt64 "\r\n", Data.Length()); if (!Request(r, "PUT", Resource)) return false; if (r.ProtocolStatus / 100 != 2) { LgiTrace("PutFailed: %i\n%s\n%s\n%s", r.ProtocolStatus, r.OutHdrs.Get(), r.OutBody.Get(), Data.Get()); return false; } Data = r.OutBody; return true; } bool LWebdav::Delete(const char *Resource) { Req r; if (!Request(r, "DELETE", Resource)) return false; if (r.ProtocolStatus / 100 != 2) { LgiTrace("DeleteFailed: %i\n%s\n%s\n", r.ProtocolStatus, r.OutHdrs.Get(), r.OutBody.Get()); return false; } return true; } diff --git a/src/common/Text/Html.cpp b/src/common/Text/Html.cpp --- a/src/common/Text/Html.cpp +++ b/src/common/Text/Html.cpp @@ -1,9498 +1,9498 @@ #include #include #include #include #include #include "lgi/common/Lgi.h" #include "lgi/common/Html.h" #include "lgi/common/ScrollBar.h" #include "lgi/common/Variant.h" #include "lgi/common/FindReplaceDlg.h" #include "lgi/common/Unicode.h" #include "lgi/common/Emoji.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/Button.h" #include "lgi/common/Edit.h" #include "lgi/common/Combo.h" #include "lgi/common/GdcTools.h" #include "lgi/common/DisplayString.h" #include "lgi/common/Palette.h" #include "lgi/common/Path.h" #include "lgi/common/CssTools.h" #include "lgi/common/LgiRes.h" #include "lgi/common/Net.h" #include "lgi/common/Base64.h" #include "lgi/common/Menu.h" #include "lgi/common/FindReplaceDlg.h" #include "lgi/common/Homoglyphs.h" #include "lgi/common/Charset.h" #include "HtmlPriv.h" #define DEBUG_TABLE_LAYOUT 1 #define DEBUG_DRAW_TD 0 #define DEBUG_RESTYLE 0 #define DEBUG_TAG_BY_POS 0 #define DEBUG_SELECTION 0 #define DEBUG_TEXT_AREA 0 #define ENABLE_IMAGE_RESIZING 1 #define DOCUMENT_LOAD_IMAGES 1 #define MAX_RECURSION_DEPTH 300 #define ALLOW_TABLE_GROWTH 1 #define LGI_HTML_MAXPAINT_TIME 350 // ms #define FLOAT_TOLERANCE 0.001 #define CRASH_TRACE 0 #ifdef MAC #define HTML_USE_DOUBLE_BUFFER 0 #else #define HTML_USE_DOUBLE_BUFFER 1 #endif #define GT_TRANSPARENT 0x00000000 #ifndef IDC_HAND #define IDC_HAND MAKEINTRESOURCE(32649) #endif #undef CellSpacing #define DefaultCellSpacing 0 #define DefaultCellPadding 1 #ifdef MAC #define MinimumPointSize 9 #define MinimumBodyFontSize 12 #else #define MinimumPointSize 8 #define MinimumBodyFontSize 11 #endif // #define DefaultFont "font-family: Times; font-size: 16pt;" #define DefaultBodyMargin "5px" #define DefaultImgSize 16 #define DefaultMissingCellColour GT_TRANSPARENT // Rgb32(0xf0,0xf0,0xf0) #define ShowNbsp 0 #define FontPxHeight(fnt) (fnt->GetHeight() - (int)(fnt->Leading() + 0.5)) #if 0 // def _DEBUG #define DefaultTableBorder Rgb32(0xf8, 0xf8, 0xf8) #else #define DefaultTableBorder GT_TRANSPARENT #endif #if defined(_DEBUG) && DEBUG_TABLE_LAYOUT #define DEBUG_LOG(...) if (Table->Debug) LgiTrace(__VA_ARGS__) #else #define DEBUG_LOG(...) #endif #define IsTableCell(id) ( ((id) == TAG_TD) || ((id) == TAG_TH) ) #define IsTableTag() (TagId == TAG_TABLE || TagId == TAG_TR || TagId == TAG_TD || TagId == TAG_TH) #define GetCssLen(a, b) a().Type == LCss::LenInherit ? b() : a() static char WordDelim[] = ".,<>/?[]{}()*&^%$#@!+|\'\""; static char16 WhiteW[] = {' ', '\t', '\r', '\n', 0}; #if 0 static char DefaultCss[] = { "a { color: blue; text-decoration: underline; }" "body { margin: 8px; }" "strong { font-weight: bolder; }" "pre { font-family: monospace }" "h1 { font-size: 2em; margin: .67em 0px; }" "h2 { font-size: 1.5em; margin: .75em 0px; }" "h3 { font-size: 1.17em; margin: .83em 0px; }" "h4, p," "blockquote, ul," "fieldset, form," "ol, dl, dir," "menu { margin: 1.12em 0px; }" "h5 { font-size: .83em; margin: 1.5em 0px; }" "h6 { font-size: .75em; margin: 1.67em 0px; }" "strike, del { text-decoration: line-through; }" "hr { border: 1px inset; }" "center { text-align: center; }" "h1, h2, h3, h4," "h5, h6, b," "strong { font-weight: bolder; }" }; #endif template void RemoveChars(T *str, T *remove_list) { T *i = str, *o = str, *c; while (*i) { for (c = remove_list; *c; c++) { if (*c == *i) break; } if (*c == 0) *o++ = *i; i++; } *o++ = NULL; } ////////////////////////////////////////////////////////////////////// using namespace Html1; namespace Html1 { class LHtmlPrivate { public: LHashTbl, LTag*> Loading; LHtmlStaticInst Inst; bool CursorVis; LRect CursorPos; LPoint Content; bool WordSelectMode; bool LinkDoubleClick; LAutoString OnLoadAnchor; bool DecodeEmoji; LAutoString EmojiImg; int NextCtrlId; uint64 SetScrollTime; int DeferredLoads; bool IsParsing; bool IsLoaded; bool StyleDirty; // Paint time limits... bool MaxPaintTimeout = false; int MaxPaintTime = LGI_HTML_MAXPAINT_TIME; // Find settings LAutoWString FindText; bool MatchCase; LHtmlPrivate() { IsLoaded = false; StyleDirty = false; IsParsing = false; LinkDoubleClick = true; WordSelectMode = false; NextCtrlId = 2000; SetScrollTime = 0; CursorVis = false; CursorPos.ZOff(-1, -1); DeferredLoads = 0; char EmojiPng[MAX_PATH_LEN]; #ifdef MAC LMakePath(EmojiPng, sizeof(EmojiPng), LGetExeFile(), "Contents/Resources/Emoji.png"); #else LGetSystemPath(LSP_APP_INSTALL, EmojiPng, sizeof(EmojiPng)); LMakePath(EmojiPng, sizeof(EmojiPng), EmojiPng, "resources/emoji.png"); #endif if (LFileExists(EmojiPng)) { DecodeEmoji = true; EmojiImg.Reset(NewStr(EmojiPng)); } else DecodeEmoji = false; } ~LHtmlPrivate() { } }; class InputButton : public LButton { LTag *Tag; public: InputButton(LTag *tag, int Id, const char *Label) : LButton(Id, 0, 0, -1, -1, Label) { Tag = tag; } void OnClick(const LMouse &m) { Tag->OnClick(m); } }; class LFontCache { LHtml *Owner; List Fonts; public: LFontCache(LHtml *owner) { Owner = owner; } ~LFontCache() { Fonts.DeleteObjects(); } LFont *FontAt(int i) { return Fonts.ItemAt(i); } LFont *FindMatch(LFont *m) { for (auto f: Fonts) { if (*f == *m) { return f; } } return 0; } LFont *GetFont(LCss *Style) { if (!Style) return NULL; LFont *Default = Owner->GetFont(); LCss::StringsDef Face = Style->FontFamily(); if (Face.Length() < 1 || !ValidStr(Face[0])) { Face.Empty(); const char *DefFace = Default->Face(); LAssert(ValidStr(DefFace)); Face.Add(NewStr(DefFace)); } LAssert(ValidStr(Face[0])); LCss::Len Size = Style->FontSize(); LCss::FontWeightType Weight = Style->FontWeight(); bool IsBold = Weight == LCss::FontWeightBold || Weight == LCss::FontWeightBolder || Weight > LCss::FontWeight400; bool IsItalic = Style->FontStyle() == LCss::FontStyleItalic; bool IsUnderline = Style->TextDecoration() == LCss::TextDecorUnderline; if (Size.Type == LCss::LenInherit || Size.Type == LCss::LenNormal) { Size.Type = LCss::LenPt; Size.Value = (float)Default->PointSize(); } auto Scale = Owner->GetDpiScale(); if (Size.Type == LCss::LenPx) { Size.Value *= (float) Scale.y; int RequestPx = (int) Size.Value; // Look for cached fonts of the right size... for (auto f: Fonts) { if (f->Face() && _stricmp(f->Face(), Face[0]) == 0 && f->Bold() == IsBold && f->Italic() == IsItalic && f->Underline() == IsUnderline) { int Px = FontPxHeight(f); int Diff = Px - RequestPx; if (Diff >= 0 && Diff <= 2) return f; } } } else if (Size.Type == LCss::LenPt) { double Pt = Size.Value; for (auto f: Fonts) { if (!f->Face() || Face.Length() == 0) { LAssert(0); break; } auto FntSz = f->Size(); if (f->Face() && _stricmp(f->Face(), Face[0]) == 0 && FntSz.Type == LCss::LenPt && std::abs(FntSz.Value - Pt) < FLOAT_TOLERANCE && f->Bold() == IsBold && f->Italic() == IsItalic && f->Underline() == IsUnderline) { // Return cached font return f; } } } else if (Size.Type == LCss::LenPercent) { // Most of the percentages will be resolved in the "Apply" stage // of the CSS calculations, any that appear here have no "font-size" // in their parent tree, so we just use the default font size times // the requested percent Size.Type = LCss::LenPt; Size.Value *= Default->PointSize() / 100.0f; if (Size.Value < MinimumPointSize) Size.Value = MinimumPointSize; } else if (Size.Type == LCss::LenEm) { // Most of the relative sizes will be resolved in the "Apply" stage // of the CSS calculations, any that appear here have no "font-size" // in their parent tree, so we just use the default font size times // the requested percent Size.Type = LCss::LenPt; Size.Value *= Default->PointSize(); if (Size.Value < MinimumPointSize) Size.Value = MinimumPointSize; } else if (Size.Type == LCss::SizeXXSmall || Size.Type == LCss::SizeXSmall || Size.Type == LCss::SizeSmall || Size.Type == LCss::SizeMedium || Size.Type == LCss::SizeLarge || Size.Type == LCss::SizeXLarge || Size.Type == LCss::SizeXXLarge) { int Idx = Size.Type-LCss::SizeXXSmall; LAssert(Idx >= 0 && Idx < CountOf(LCss::FontSizeTable)); Size.Type = LCss::LenPt; Size.Value = Default->PointSize() * LCss::FontSizeTable[Idx]; if (Size.Value < MinimumPointSize) Size.Value = MinimumPointSize; } else if (Size.Type == LCss::SizeSmaller) { Size.Type = LCss::LenPt; Size.Value = (float)(Default->PointSize() - 1); } else if (Size.Type == LCss::SizeLarger) { Size.Type = LCss::LenPt; Size.Value = (float)(Default->PointSize() + 1); } else LAssert(!"Not impl."); LFont *f; if ((f = new LFont)) { auto ff = ValidStr(Face[0]) ? Face[0] : Default->Face(); f->Face(ff); f->Size(Size.IsValid() ? Size : Default->Size()); f->Bold(IsBold); f->Italic(IsItalic); f->Underline(IsUnderline); // printf("Add cache font %s,%i %i,%i,%i\n", f->Face(), f->PointSize(), f->Bold(), f->Italic(), f->Underline()); if (std::abs(Size.Value) < FLOAT_TOLERANCE) ; else if (!f->Create((char*)0, 0)) { // Broken font... f->Face(Default->Face()); LFont *DefMatch = FindMatch(f); // printf("Falling back to default face for '%s:%i', DefMatch=%p\n", ff, f->PointSize(), DefMatch); if (DefMatch) { DeleteObj(f); return DefMatch; } else { if (!f->Create((char*)0, 0)) { DeleteObj(f); return Fonts[0]; } } } // Not already cached Fonts.Insert(f); if (!f->Face()) { LAssert(0); } return f; } return 0; } }; class LFlowRegion { LCss::LengthType Align = LCss::LenInherit; List Line; // These pointers aren't owned by the flow region // When the line is finish, all the tag regions // will need to be vertically aligned struct LFlowStack { int LeftAbs; int RightAbs; int TopAbs; }; LArray Stack; public: LHtml *Html; int x1, x2; // Left and right margins int y1; // Current y position int y2; // Maximum used y position int cx; // Current insertion point int my; // How much of the area above y2 was just margin LPoint MAX; // Max dimensions int Inline; int InBody; LFlowRegion(LHtml *html, bool inbody) { Html = html; x1 = x2 = y1 = y2 = cx = my = 0; Inline = 0; InBody = inbody; } LFlowRegion(LHtml *html, LRect r, bool inbody) { Html = html; MAX.x = cx = x1 = r.x1; MAX.y = y1 = y2 = r.y1; x2 = r.x2; my = 0; Inline = 0; InBody = inbody; } LFlowRegion(LFlowRegion &r) { Html = r.Html; x1 = r.x1; x2 = r.x2; y1 = r.y1; MAX.x = cx = r.cx; MAX.y = y2 = r.y2; my = r.my; Inline = r.Inline; InBody = r.InBody; } LString ToString() { LString s; s.Printf("Flow: x=%i(%i)%i y=%i,%i my=%i inline=%i", x1, cx, x2, y1, y2, my, Inline); return s; } int X() { return x2 - cx; } void X(int newx) { x2 = x1 + newx - 1; } int Width() { return x2 - x1 + 1; } LFlowRegion &operator +=(LRect r) { x1 += r.x1; cx += r.x1; x2 -= r.x2; y1 += r.y1; y2 += r.y1; return *this; } LFlowRegion &operator -=(LRect r) { x1 -= r.x1; cx -= r.x1; x2 += r.x2; y1 += r.y2; y2 += r.y2; return *this; } void AlignText(); void FinishLine(bool Margin = false); void EndBlock(); void Insert(LFlowRect *Tr, LCss::LengthType Align); LRect *LineBounds(); void Indent(LTag *Tag, LCss::Len Left, LCss::Len Top, LCss::Len Right, LCss::Len Bottom, bool IsMargin) { LFlowRegion This(*this); LFlowStack &Fs = Stack.New(); Fs.LeftAbs = Left.IsValid() ? ResolveX(Left, Tag, IsMargin) : 0; Fs.RightAbs = Right.IsValid() ? ResolveX(Right, Tag, IsMargin) : 0; Fs.TopAbs = Top.IsValid() ? ResolveY(Top, Tag, IsMargin) : 0; x1 += Fs.LeftAbs; cx += Fs.LeftAbs; x2 -= Fs.RightAbs; y1 += Fs.TopAbs; y2 += Fs.TopAbs; if (IsMargin) my += Fs.TopAbs; } void Indent(LRect &Px, bool IsMargin) { LFlowRegion This(*this); LFlowStack &Fs = Stack.New(); Fs.LeftAbs = Px.x1; Fs.RightAbs = Px.x2; Fs.TopAbs = Px.y1; x1 += Fs.LeftAbs; cx += Fs.LeftAbs; x2 -= Fs.RightAbs; y1 += Fs.TopAbs; y2 += Fs.TopAbs; if (IsMargin) my += Fs.TopAbs; } void Outdent(LRect &Px, bool IsMargin) { LFlowRegion This = *this; ssize_t len = Stack.Length(); if (len > 0) { LFlowStack &Fs = Stack[len-1]; int &BottomAbs = Px.y2; x1 -= Fs.LeftAbs; cx -= Fs.LeftAbs; x2 += Fs.RightAbs; y2 += BottomAbs; if (IsMargin) my += BottomAbs; Stack.Length(len-1); } else LAssert(!"Nothing to pop."); } void Outdent(LTag *Tag, LCss::Len Left, LCss::Len Top, LCss::Len Right, LCss::Len Bottom, bool IsMargin) { LFlowRegion This = *this; ssize_t len = Stack.Length(); if (len > 0) { LFlowStack &Fs = Stack[len-1]; int BottomAbs = Bottom.IsValid() ? ResolveY(Bottom, Tag, IsMargin) : 0; x1 -= Fs.LeftAbs; cx -= Fs.LeftAbs; x2 += Fs.RightAbs; y2 += BottomAbs; if (IsMargin) my += BottomAbs; Stack.Length(len-1); } else LAssert(!"Nothing to pop."); } int ResolveX(LCss::Len l, LTag *t, bool IsMargin) { LFont *f = t->GetFont(); switch (l.Type) { default: case LCss::LenInherit: return IsMargin ? 0 : X(); case LCss::LenPx: // return MIN((int)l.Value, X()); return (int)l.Value; case LCss::LenPt: return (int) (l.Value * LScreenDpi().x / 72.0); case LCss::LenCm: return (int) (l.Value * LScreenDpi().x / 2.54); case LCss::LenEm: { if (!f) { LAssert(!"No font?"); f = LSysFont; } return (int)(l.Value * f->GetHeight()); } case LCss::LenEx: { if (!f) { LAssert(!"No font?"); f = LSysFont; } return (int) (l.Value * f->GetHeight() / 2); // More haha, who uses 'ex' anyway? } case LCss::LenPercent: { int my_x = X(); int px = (int) (l.Value * my_x / 100.0); return px; } case LCss::LenAuto: { if (IsMargin) return 0; else return X(); break; } case LCss::SizeSmall: { return 1; // px } case LCss::SizeMedium: { return 2; // px } case LCss::SizeLarge: { return 3; // px } } return 0; } bool LimitX(int &x, LCss::Len Min, LCss::Len Max, LFont *f) { bool Limited = false; if (Min.IsValid()) { int Px = Min.ToPx(x2 - x1 + 1, f, false); if (Px > x) { x = Px; Limited = true; } } if (Max.IsValid()) { int Px = Max.ToPx(x2 - x1 + 1, f, false); if (Px < x) { x = Px; Limited = true; } } return Limited; } int ResolveY(LCss::Len l, LTag *t, bool IsMargin) { LFont *f = t->GetFont(); switch (l.Type) { case LCss::LenInherit: case LCss::LenAuto: case LCss::LenNormal: case LCss::LenPx: return (int)l.Value; case LCss::LenPt: return (int) (l.Value * LScreenDpi().y / 72.0); case LCss::LenCm: return (int) (l.Value * LScreenDpi().y / 2.54); case LCss::LenEm: { if (!f) { f = LSysFont; LAssert(!"No font"); } return (int) (l.Value * f->GetHeight()); } case LCss::LenEx: { if (!f) { f = LSysFont; LAssert(!"No font"); } return (int) (l.Value * f->GetHeight() / 2); // More haha, who uses 'ex' anyway? } case LCss::LenPercent: { // Walk up tree of tags to find an absolute size... LCss::Len Ab; for (LTag *p = ToTag(t->Parent); p; p = ToTag(p->Parent)) { auto h = p->Height(); if (h.IsValid() && !h.IsDynamic()) { Ab = h; break; } } if (!Ab.IsValid()) { LAssert(Html != NULL); Ab.Type = LCss::LenPx; Ab.Value = (float)Html->Y(); } LCss::Len m = Ab * l; return (int)m.ToPx(0, f);; } case LCss::SizeSmall: { return 1; // px } case LCss::SizeMedium: { return 2; // px } case LCss::SizeLarge: { return 3; // px } case LCss::AlignLeft: case LCss::AlignRight: case LCss::AlignCenter: case LCss::AlignJustify: case LCss::VerticalBaseline: case LCss::VerticalSub: case LCss::VerticalSuper: case LCss::VerticalTop: case LCss::VerticalTextTop: case LCss::VerticalMiddle: case LCss::VerticalBottom: case LCss::VerticalTextBottom: { // Meaningless in this context break; } default: { LAssert(!"Not supported."); break; } } return 0; } bool LimitY(int &y, LCss::Len Min, LCss::Len Max, LFont *f) { bool Limited = false; int TotalY = Html ? Html->Y() : 0; if (Min.IsValid()) { int Px = Min.ToPx(TotalY, f, false); if (Px > y) { y = Px; Limited = true; } } if (Max.IsValid()) { int Px = Max.ToPx(TotalY, f, false); if (Px < y) { y = Px; Limited = true; } } return Limited; } LRect ResolveMargin(LCss *Src, LTag *Tag) { LRect r; r.x1 = ResolveX(Src->MarginLeft(), Tag, true); r.y1 = ResolveY(Src->MarginTop(), Tag, true); r.x2 = ResolveX(Src->MarginRight(), Tag, true); r.y2 = ResolveY(Src->MarginBottom(), Tag, true); return r; } LRect ResolveBorder(LCss *Src, LTag *Tag) { LRect r; r.x1 = ResolveX(Src->BorderLeft(), Tag, true); r.y1 = ResolveY(Src->BorderTop(), Tag, true); r.x2 = ResolveX(Src->BorderRight(), Tag, true); r.y2 = ResolveY(Src->BorderBottom(), Tag, true); return r; } LRect ResolvePadding(LCss *Src, LTag *Tag) { LRect r; r.x1 = ResolveX(Src->PaddingLeft(), Tag, true); r.y1 = ResolveY(Src->PaddingTop(), Tag, true); r.x2 = ResolveX(Src->PaddingRight(), Tag, true); r.y2 = ResolveY(Src->PaddingBottom(), Tag, true); return r; } }; }; ////////////////////////////////////////////////////////////////////// static bool ParseDistance(char *s, float &d, char *units = 0) { if (!s) return false; while (*s && IsWhiteSpace(*s)) s++; if (!IsDigit(*s) && !strchr("-.", *s)) return false; d = (float)atof(s); while (*s && (IsDigit(*s) || strchr("-.", *s))) s++; while (*s && IsWhiteSpace(*s)) s++; char _units[128]; char *o = units = units ? units : _units; while (*s && (IsAlpha(*s) || *s == '%')) { *o++ = *s++; } *o++ = 0; return true; } LHtmlLength::LHtmlLength() { d = 0; PrevAbs = 0; u = LCss::LenInherit; } LHtmlLength::LHtmlLength(char *s) { Set(s); } bool LHtmlLength::IsValid() { return u != LCss::LenInherit; } bool LHtmlLength::IsDynamic() { return u == LCss::LenPercent || d == 0.0; } LHtmlLength::operator float () { return d; } LHtmlLength &LHtmlLength::operator =(float val) { d = val; u = LCss::LenPx; return *this; } LCss::LengthType LHtmlLength::GetUnits() { return u; } void LHtmlLength::Set(char *s) { if (ValidStr(s)) { char Units[256] = ""; if (ParseDistance(s, d, Units)) { if (Units[0]) { if (strchr(Units, '%')) { u = LCss::LenPercent; } else if (stristr(Units, "pt")) { u = LCss::LenPt; } else if (stristr(Units, "em")) { u = LCss::LenEm; } else if (stristr(Units, "ex")) { u = LCss::LenEx; } else { u = LCss::LenPx; } } else { u = LCss::LenPx; } } } } float LHtmlLength::Get(LFlowRegion *Flow, LFont *Font, bool Lock) { switch (u) { default: break; case LCss::LenEm: { return PrevAbs = d * (Font ? Font->GetHeight() : 14); break; } case LCss::LenEx: { return PrevAbs = (Font ? Font->GetHeight() * d : 14) / 2; break; } case LCss::LenPercent: { if (Lock || PrevAbs == 0.0) { return PrevAbs = (Flow->X() * d / 100); } else { return PrevAbs; } break; } } float FlowX = Flow ? Flow->X() : d; return PrevAbs = MIN(FlowX, d); } LHtmlLine::LHtmlLine() { LineStyle = -1; LineReset = 0x80000000; } LHtmlLine::~LHtmlLine() { } LHtmlLine &LHtmlLine::operator =(int i) { d = (float)i; return *this; } void LHtmlLine::Set(char *s) { LToken t(s, " \t"); LineReset = 0x80000000; LineStyle = -1; char *Style = 0; for (unsigned i=0; iColourMap.Find(c) ) { LHtmlParser::ParseColour(c, Colour); } else if (_strnicmp(c, "rgb(", 4) == 0) { char Buf[256]; strcpy_s(Buf, sizeof(Buf), c); while (!strchr(c, ')') && (c = t[++i])) { strcat(Buf, c); } LHtmlParser::ParseColour(Buf, Colour); } else if (IsDigit(*c)) { LHtmlLength::Set(c); } else if (_stricmp(c, "none") == 0) { Style = 0; } else if ( _stricmp(c, "dotted") == 0 || _stricmp(c, "dashed") == 0 || _stricmp(c, "solid") == 0 || _stricmp(c, "float") == 0 || _stricmp(c, "groove") == 0 || _stricmp(c, "ridge") == 0 || _stricmp(c, "inset") == 0 || _stricmp(c, "outse") == 0) { Style = c; } else { // ??? } } if (Style && _stricmp(Style, "dotted") == 0) { switch ((int)d) { case 2: { LineStyle = 0xcccccccc; break; } case 3: { LineStyle = 0xe38e38; LineReset = 0x800000; break; } case 4: { LineStyle = 0xf0f0f0f0; break; } case 5: { LineStyle = 0xf83e0; LineReset = 0x80000; break; } case 6: { LineStyle = 0xfc0fc0; LineReset = 0x800000; break; } case 7: { LineStyle = 0xfe03f80; LineReset = 0x8000000; break; } case 8: { LineStyle = 0xff00ff00; break; } case 9: { LineStyle = 0x3fe00; LineReset = 0x20000; break; } default: { LineStyle = 0xaaaaaaaa; break; } } } } ////////////////////////////////////////////////////////////////////// LRect LTag::GetRect(bool Client) { LRect r(Pos.x, Pos.y, Pos.x + Size.x - 1, Pos.y + Size.y - 1); if (!Client) { for (LTag *p = ToTag(Parent); p; p=ToTag(p->Parent)) { r.Offset(p->Pos.x, p->Pos.y); } } return r; } LCss::LengthType LTag::GetAlign(bool x) { for (LTag *t = this; t; t = ToTag(t->Parent)) { LCss::Len l; if (x) { if (IsTableCell(TagId) && Cell && Cell->XAlign) l.Type = Cell->XAlign; else l = t->TextAlign(); } else { l = t->VerticalAlign(); } if (l.Type != LenInherit) { return l.Type; } if (t->TagId == TAG_TABLE) break; } return LenInherit; } ////////////////////////////////////////////////////////////////////// void LFlowRegion::EndBlock() { if (cx > x1) FinishLine(); } void LFlowRegion::AlignText() { if (Align != LCss::AlignLeft) { int Used = 0; for (auto l : Line) Used += l->X(); int Total = x2 - x1 + 1; if (Used < Total) { int Offset = 0; if (Align == LCss::AlignCenter) Offset = (Total - Used) / 2; else if (Align == LCss::AlignRight) Offset = Total - Used; if (Offset) for (auto l : Line) { if (l->Tag->Display() != LCss::DispInlineBlock) l->Offset(Offset, 0); } } } } void LFlowRegion::FinishLine(bool Margin) { // AlignText(); if (y2 > y1) { my = Margin ? y2 - y1 : 0; y1 = y2; } else { int fy = Html->DefFont()->GetHeight(); my = Margin ? fy : 0; y1 += fy; } cx = x1; y2 = y1; Line.Empty(); } LRect *LFlowRegion::LineBounds() { auto It = Line.begin(); LFlowRect *Prev = *It; LFlowRect *r=Prev; if (r) { LRect b; b = *r; int Ox = r->Tag->AbsX(); int Oy = r->Tag->AbsY(); b.Offset(Ox, Oy); // int Ox = 0, Oy = 0; while ((r = *(++It) )) { LRect c = *r; Ox = r->Tag->AbsX(); Oy = r->Tag->AbsY(); c.Offset(Ox, Oy); /* Ox += r->Tag->Pos.x - Prev->Tag->Pos.x; Oy += r->Tag->Pos.y - Prev->Tag->Pos.y; c.Offset(Ox, Oy); */ b.Union(&c); Prev = r; } static LRect Rgn; Rgn = b; return &Rgn; } return 0; } void LFlowRegion::Insert(LFlowRect *Tr, LCss::LengthType align) { if (Tr) { Align = align; Line.Insert(Tr); } } ////////////////////////////////////////////////////////////////////// LTag::LTag(LHtml *h, LHtmlElement *p) : LHtmlElement(p), Attr(8) { Ctrl = 0; CtrlType = CtrlNone; TipId = 0; Display(DispInline); Html = h; ImageResized = false; Cursor = -1; Selection = -1; Font = 0; LineHeightCache = -1; HtmlId = NULL; // TableBorder = 0; Cell = NULL; TagId = CONTENT; Info = 0; Pos.x = Pos.y = 0; #ifdef _DEBUG Debug = false; #endif } LTag::~LTag() { if (Html->Cursor == this) { Html->Cursor = 0; } if (Html->Selection == this) { Html->Selection = 0; } DeleteObj(Ctrl); Attr.DeleteArrays(); DeleteObj(Cell); } void LTag::OnChange(PropType Prop) { } bool LTag::OnClick(const LMouse &m) { if (!Html->Environment) return false; const char *OnClick = NULL; if (Get("onclick", OnClick)) { Html->Environment->OnExecuteScript(Html, (char*)OnClick); } else { OnNotify(LNotification(m)); } return true; } void LTag::Set(const char *attr, const char *val) { char *existing = Attr.Find(attr); if (existing) DeleteArray(existing); if (val) Attr.Add(attr, NewStr(val)); } bool LTag::GetVariant(const char *Name, LVariant &Value, const char *Array) { LDomProperty Fld = LStringToDomProp(Name); switch (Fld) { case ObjStyle: // Type: LCssStyle { Value = &StyleDom; return true; } case ObjTextContent: // Type: String { Value = Text(); return true; } default: { char *a = Attr.Find(Name); if (a) { Value = a; return true; } break; } } return false; } bool LTag::SetVariant(const char *Name, LVariant &Value, const char *Array) { LDomProperty Fld = LStringToDomProp(Name); switch (Fld) { case ObjStyle: { const char *Defs = Value.Str(); if (!Defs) return false; return Parse(Defs, ParseRelaxed); } case ObjTextContent: { const char *s = Value.Str(); if (s) { LAutoWString w(CleanText(s, strlen(s), "utf-8", true, true)); Txt = w; return true; } break; } case ObjInnerHtml: // Type: String { // Clear out existing tags.. Children.DeleteObjects(); char *Doc = Value.CastString(); if (Doc) { // Create new tags... bool BackOut = false; while (Doc && *Doc) { LTag *t = new LTag(Html, this); if (t) { Doc = Html->ParseHtml(t, Doc, 1, false, &BackOut); if (!Doc) break; } else break; } } else return false; break; } default: { Set(Name, Value.CastString()); SetStyle(); break; } } Html->ViewWidth = -1; return true; } ssize_t LTag::GetTextStart() { if (PreText() && TextPos.Length() > 1) { LFlowRect *t = TextPos[1]; if (t) return t->Text - Text(); } else if (TextPos.Length() > 0) { LFlowRect *t = TextPos[0]; if (t && Text()) { LAssert(t->Text >= Text() && t->Text <= Text()+2); return t->Text - Text(); } } return 0; } static bool TextToStream(LStream &Out, char16 *Text) { if (!Text) return true; uint8_t Buf[256]; uint8_t *s = Buf; ssize_t Len = sizeof(Buf); while (*Text) { #define WriteExistingContent() \ if (s > Buf) \ Out.Write(Buf, (int)(s - Buf)); \ s = Buf; \ Len = sizeof(Buf); \ Buf[0] = 0; if (*Text == '<' || *Text == '>') { WriteExistingContent(); Out.Print("&%ct;", *Text == '<' ? 'l' : 'g'); } else if (*Text == 0xa0) { WriteExistingContent(); Out.Write((char*)" ", 6); } else { LgiUtf32To8(*Text, s, Len); if (Len < 16) { WriteExistingContent(); } } Text++; } if (s > Buf) Out.Write(Buf, s - Buf); return true; } bool LTag::CreateSource(LStringPipe &p, int Depth, bool LastWasBlock) { char *Tabs = new char[Depth+1]; memset(Tabs, '\t', Depth); Tabs[Depth] = 0; if (ValidStr(Tag)) { if (IsBlock()) { p.Print("%s%s<%s", TagId != TAG_HTML ? "\n" : "", Tabs, Tag.Get()); } else { p.Print("<%s", Tag.Get()); } if (Attr.Length()) { // const char *a; // for (char *v = Attr.First(&a); v; v = Attr.Next(&a)) for (auto v : Attr) { if (_stricmp(v.key, "style")) p.Print(" %s=\"%s\"", v.key, v.value); } } if (Props.Length()) { LCss *Css = this; LCss Tmp; #define DelProp(p) \ if (Css == this) { Tmp = *Css; Css = &Tmp; } \ Css->DeleteProp(p); // Clean out any default CSS properties where we can... LHtmlElemInfo *i = LHtmlStatic::Inst->GetTagInfo(Tag); if (i) { if (Props.Find(PropDisplay) && ( (!i->Block() && Display() == DispInline) || (i->Block() && Display() == DispBlock) )) { DelProp(PropDisplay); } switch (TagId) { default: break; case TAG_A: { LCss::ColorDef Blue(LCss::ColorRgb, Rgb32(0, 0, 255)); if (Props.Find(PropColor) && Color() == Blue) DelProp(PropColor); if (Props.Find(PropTextDecoration) && TextDecoration() == LCss::TextDecorUnderline) DelProp(PropTextDecoration) break; } case TAG_BODY: { LCss::Len FivePx(LCss::LenPx, 5.0f); if (Props.Find(PropPaddingLeft) && PaddingLeft() == FivePx) DelProp(PropPaddingLeft) if (Props.Find(PropPaddingTop) && PaddingTop() == FivePx) DelProp(PropPaddingTop) if (Props.Find(PropPaddingRight) && PaddingRight() == FivePx) DelProp(PropPaddingRight) break; } case TAG_B: { if (Props.Find(PropFontWeight) && FontWeight() == LCss::FontWeightBold) DelProp(PropFontWeight); break; } case TAG_U: { if (Props.Find(PropTextDecoration) && TextDecoration() == LCss::TextDecorUnderline) DelProp(PropTextDecoration); break; } case TAG_I: { if (Props.Find(PropFontStyle) && FontStyle() == LCss::FontStyleItalic) DelProp(PropFontStyle); break; } } } // Convert CSS props to a string and emit them... auto s = Css->ToString(); if (ValidStr(s)) { // Clean off any trailing whitespace... char *e = s ? s + strlen(s) : NULL; while (e && strchr(WhiteSpace, e[-1])) *--e = 0; // Print them to the tags attributes... p.Print(" style=\"%s\"", s.Get()); } } } if (Children.Length() || TagId == TAG_STYLE) // { if (Tag) { p.Write((char*)">", 1); TextToStream(p, Text()); } bool Last = IsBlock(); for (unsigned i=0; iCreateSource(p, Parent ? Depth+1 : 0, Last); Last = c->IsBlock(); } if (Tag) { if (IsBlock()) { if (Children.Length()) p.Print("\n%s", Tabs); } p.Print("", Tag.Get()); } } else if (Tag) { if (Text()) { p.Write((char*)">", 1); TextToStream(p, Text()); p.Print("", Tag.Get()); } else { p.Print("/>\n"); } } else { TextToStream(p, Text()); } DeleteArray(Tabs); return true; } void LTag::SetTag(const char *NewTag) { Tag.Reset(NewStr(NewTag)); if (NewTag) { Info = Html->GetTagInfo(Tag); if (Info) { TagId = Info->Id; Display(Info->Flags & LHtmlElemInfo::TI_BLOCK ? LCss::DispBlock : LCss::DispInline); } } else { Info = NULL; TagId = CONTENT; } SetStyle(); } LColour LTag::_Colour(bool f) { for (LTag *t = this; t; t = ToTag(t->Parent)) { ColorDef c = f ? t->Color() : t->BackgroundColor(); if (c.Type != ColorInherit) { return LColour(c.Rgb32, 32); } #if 1 if (!f && t->TagId == TAG_TABLE) break; #else /* This implements some basic level of colour inheritance for background colours. See test case 'cisra-cqs.html'. */ if (!f && t->TagId == TAG_TABLE) break; #endif } return LColour(); } void LTag::CopyClipboard(LMemQueue &p, bool &InSelection) { ssize_t Min = -1; ssize_t Max = -1; if (Cursor >= 0 && Selection >= 0) { Min = MIN(Cursor, Selection); Max = MAX(Cursor, Selection); } else if (InSelection) { Max = MAX(Cursor, Selection); } else { Min = MAX(Cursor, Selection); } ssize_t Off = -1; ssize_t Chars = 0; auto Start = GetTextStart(); if (Min >= 0 && Max >= 0) { Off = Min + Start; Chars = Max - Min; } else if (Min >= 0) { Off = Min + Start; Chars = StrlenW(Text()) - Min; InSelection = true; } else if (Max >= 0) { Off = Start; Chars = Max; InSelection = false; } else if (InSelection) { Off = Start; Chars = StrlenW(Text()); } if (Off >= 0 && Chars > 0) { p.Write((uchar*) (Text() + Off), Chars * sizeof(char16)); } if (InSelection) { switch (TagId) { default: break; case TAG_BR: { char16 NL[] = {'\n', 0}; p.Write((uchar*) NL, sizeof(char16)); break; } case TAG_P: { char16 NL[] = {'\n', '\n', 0}; p.Write((uchar*) NL, sizeof(char16) * 2); break; } } } for (unsigned i=0; iCopyClipboard(p, InSelection); } } static char* _DumpColour(LCss::ColorDef c) { static char Buf[4][32]; #ifdef _MSC_VER static LONG Cur = 0; LONG Idx = InterlockedIncrement(&Cur); #else static int Cur = 0; int Idx = __sync_fetch_and_add(&Cur, 1); #endif char *b = Buf[Idx % 4]; if (c.Type == LCss::ColorInherit) strcpy_s(b, 32, "Inherit"); else sprintf_s(b, 32, "%2.2x,%2.2x,%2.2x(%2.2x)", R32(c.Rgb32),G32(c.Rgb32),B32(c.Rgb32),A32(c.Rgb32)); return b; } void LTag::_Dump(LStringPipe &Buf, int Depth) { LString Tabs; Tabs.Set(NULL, Depth); memset(Tabs.Get(), '\t', Depth); const char *Empty = ""; char *ElementName = TagId == CONTENT ? (char*)"Content" : (TagId == ROOT ? (char*)"Root" : Tag); Buf.Print( "%s%s(%p)%s%s%s (%i) Pos=%i,%i Size=%i,%i Color=%s/%s", Tabs.Get(), ElementName, this, HtmlId ? "#" : Empty, HtmlId ? HtmlId : Empty, #ifdef _DEBUG Debug ? " debug" : Empty, #else Empty, #endif WasClosed, Pos.x, Pos.y, Size.x, Size.y, _DumpColour(Color()), _DumpColour(BackgroundColor())); for (unsigned i=0; iText, Tr->Len)); if (Utf8) { size_t Len = strlen(Utf8); if (Len > 40) { Utf8[40] = 0; } } else if (Tr->Text) { Utf8.Reset(NewStr("")); } Buf.Print("Tr(%i,%i %ix%i '%s') ", Tr->x1, Tr->y1, Tr->X(), Tr->Y(), Utf8.Get()); } Buf.Print("\r\n"); for (unsigned i=0; i_Dump(Buf, Depth+1); } if (Children.Length()) { Buf.Print("%s/%s\r\n", Tabs.Get(), ElementName); } } LAutoWString LTag::DumpW() { LStringPipe Buf; // Buf.Print("Html pos=%s\n", Html?Html->GetPos().GetStr():0); _Dump(Buf, 0); LAutoString a(Buf.NewStr()); LAutoWString w(Utf8ToWide(a)); return w; } LAutoString LTag::DescribeElement() { LStringPipe s(256); s.Print("%s", Tag ? Tag.Get() : "CONTENT"); if (HtmlId) s.Print("#%s", HtmlId); for (unsigned i=0; iDefFont(); } return f; } LFont *LTag::GetFont() { if (!Font) { if (PropAddress(PropFontFamily) != 0 || FontSize().Type != LenInherit || FontStyle() != FontStyleInherit || FontVariant() != FontVariantInherit || FontWeight() != FontWeightInherit || TextDecoration() != TextDecorInherit) { LCss c; LCss::PropMap Map; Map.Add(PropFontFamily, new LCss::PropArray); Map.Add(PropFontSize, new LCss::PropArray); Map.Add(PropFontStyle, new LCss::PropArray); Map.Add(PropFontVariant, new LCss::PropArray); Map.Add(PropFontWeight, new LCss::PropArray); Map.Add(PropTextDecoration, new LCss::PropArray); for (LTag *t = this; t; t = ToTag(t->Parent)) { if (t->TagId == TAG_IFRAME) break; if (!c.InheritCollect(*t, Map)) break; } c.InheritResolve(Map); Map.DeleteObjects(); if ((Font = Html->FontCache->GetFont(&c))) return Font; } else { LTag *t = this; while (!t->Font && t->Parent) { t = ToTag(t->Parent); } if (t->Font) return t->Font; } Font = Html->DefFont(); } return Font; } LTag *LTag::PrevTag() { if (Parent) { ssize_t i = Parent->Children.IndexOf(this); if (i >= 0) { return ToTag(Parent->Children[i - 1]); } } return 0; } void LTag::Invalidate() { LRect p = GetRect(); for (LTag *t=ToTag(Parent); t; t=ToTag(t->Parent)) { p.Offset(t->Pos.x, t->Pos.y); } Html->Invalidate(&p); } LTag *LTag::IsAnchor(LString *Uri) { LTag *a = 0; for (LTag *t = this; t; t = ToTag(t->Parent)) { if (t->TagId == TAG_A) { a = t; break; } } if (a && Uri) { const char *u = 0; if (a->Get("href", u)) { LAutoWString w(CleanText(u, strlen(u), "utf-8")); if (w) { *Uri = w; } } } return a; } bool LTag::OnMouseClick(LMouse &m) { bool Processed = false; if (m.IsContextMenu()) { LString Uri; const char *ImgSrc = NULL; LTag *a = IsAnchor(&Uri); bool IsImg = TagId == TAG_IMG; if (IsImg) Get("src", ImgSrc); bool IsAnchor = a && ValidStr(Uri); if (IsAnchor || IsImg) { LSubMenu RClick; #define IDM_COPY_LINK 100 #define IDM_COPY_IMG 101 if (Html->GetMouse(m, true)) { int Id = 0; if (IsAnchor) RClick.AppendItem(LLoadString(L_COPY_LINK_LOCATION, "&Copy Link Location"), IDM_COPY_LINK, Uri != NULL); if (IsImg) RClick.AppendItem("Copy Image Location", IDM_COPY_IMG, ImgSrc != NULL); if (Html->GetEnv()) Html->GetEnv()->AppendItems(&RClick, Uri); switch (Id = RClick.Float(Html, m.x, m.y)) { case IDM_COPY_LINK: { LClipBoard Clip(Html); Clip.Text(Uri); break; } case IDM_COPY_IMG: { LClipBoard Clip(Html); Clip.Text(ImgSrc); break; } default: { if (Html->GetEnv()) Html->GetEnv()->OnMenu(Html, Id, a); break; } } } Processed = true; } } else if (m.Down() && m.Left()) { #ifdef _DEBUG if (m.Ctrl()) { auto Style = ToString(); LStringPipe p(256); p.Print("Tag: %s\n", Tag ? Tag.Get() : "CONTENT"); if (Class.Length()) { p.Print("Class(es): "); for (unsigned i=0; iParent; t=ToTag(t->Parent)) { LStringPipe Tmp; Tmp.Print(" %s", t->Tag ? t->Tag.Get() : "CONTENT"); if (t->HtmlId) { Tmp.Print("#%s", t->HtmlId); } for (unsigned i=0; iClass.Length(); i++) { Tmp.Print(".%s", t->Class[i].Get()); } LAutoString Txt(Tmp.NewStr()); p.Print("%s", Txt.Get()); LDisplayString Ds(LSysFont, Txt); int Px = 170 - Ds.X(); int Chars = Px / Sp.X(); for (int c=0; cPos.x, t->Pos.y, t->Size.x, t->Size.y); } LAutoString a(p.NewStr()); LgiMsg( Html, "%s", Html->GetClass(), MB_OK, a.Get()); } else #endif { LString Uri; if (Html && Html->Environment) { if (IsAnchor(&Uri)) { if (Uri) { if (!Html->d->LinkDoubleClick || m.Double()) { Html->Environment->OnNavigate(Html, Uri); Processed = true; } } const char *OnClk = NULL; if (!Processed && Get("onclick", OnClk)) { Html->Environment->OnExecuteScript(Html, (char*)OnClk); } } else { Processed = OnClick(m); } } } } return Processed; } LTag *LTag::GetBlockParent(ssize_t *Idx) { if (IsBlock()) { if (Idx) *Idx = 0; return this; } for (LTag *t = this; t; t = ToTag(t->Parent)) { if (ToTag(t->Parent)->IsBlock()) { if (Idx) { *Idx = t->Parent->Children.IndexOf(t); } return ToTag(t->Parent); } } return 0; } LTag *LTag::GetAnchor(char *Name) { if (!Name) return 0; const char *n; if (IsAnchor(0) && Get("name", n) && n && !_stricmp(Name, n)) { return this; } for (unsigned i=0; iGetAnchor(Name); if (Result) return Result; } return 0; } LTag *LTag::GetTagByName(const char *Name) { if (Name) { if (Tag && _stricmp(Tag, Name) == 0) { return this; } for (unsigned i=0; iGetTagByName(Name); if (Result) return Result; } } return 0; } static int IsNearRect(LRect *r, int x, int y) { if (r->Overlap(x, y)) { return 0; } else if (x >= r->x1 && x <= r->x2) { if (y < r->y1) return r->y1 - y; else return y - r->y2; } else if (y >= r->y1 && y <= r->y2) { if (x < r->x1) return r->x1 - x; else return x - r->x2; } int64 dx = 0; int64 dy = 0; if (x < r->x1) { if (y < r->y1) { // top left dx = r->x1 - x; dy = r->y1 - y; } else { // bottom left dx = r->x1 - x; dy = y - r->y2; } } else { if (y < r->y1) { // top right dx = x - r->x2; dy = r->y1 - y; } else { // bottom right dx = x - r->x2; dy = y - r->y2; } } return (int) sqrt( (double) ( (dx * dx) + (dy * dy) ) ); } ssize_t LTag::NearestChar(LFlowRect *Tr, int x, int y) { LFont *f = GetFont(); if (f) { LDisplayString ds(f, Tr->Text, Tr->Len); ssize_t c = ds.CharAt(x - Tr->x1); if (Tr->Text == PreText()) { return 0; } else { char16 *t = Tr->Text + c; size_t Len = StrlenW(Text()); if (t >= Text() && t <= Text() + Len) { return (t - Text()) - GetTextStart(); } else { LgiTrace("%s:%i - Error getting char at position.\n", _FL); } } } return -1; } void LTag::GetTagByPos(LTagHit &TagHit, int x, int y, int Depth, bool InBody, bool DebugLog) { /* InBody: Originally I had this test in the code but it seems that some test cases have actual content after the body. And testing for "InBody" breaks functionality for those cases (see "spam4.html" and the unsubscribe link at the end of the doc). */ if (TagId == TAG_IMG) { LRect img(0, 0, Size.x - 1, Size.y - 1); if (/*InBody &&*/ img.Overlap(x, y)) { TagHit.Direct = this; TagHit.Block = 0; } } else if (/*InBody &&*/ TextPos.Length()) { for (unsigned i=0; i= Tr->y1 && y <= Tr->y2; int Near = IsNearRect(Tr, x, y); if (Near >= 0 && Near < 100) { if ( !TagHit.NearestText || ( SameRow && !TagHit.NearSameRow ) || ( SameRow == TagHit.NearSameRow && Near < TagHit.Near ) ) { TagHit.NearestText = this; TagHit.NearSameRow = SameRow; TagHit.Block = Tr; TagHit.Near = Near; TagHit.Index = NearestChar(Tr, x, y); if (DebugLog) { LgiTrace("%i:GetTagByPos HitText %s #%s, idx=%i, near=%i, txt='%S'\n", Depth, Tag.Get(), HtmlId, TagHit.Index, TagHit.Near, Tr->Text); } if (!TagHit.Near) { TagHit.Direct = this; TagHit.LocalCoords.x = x; TagHit.LocalCoords.y = y; } } } } } else if ( TagId != TAG_TR && Tag && x >= 0 && y >= 0 && x < Size.x && y < Size.y // && InBody ) { // Direct hit TagHit.Direct = this; TagHit.LocalCoords.x = x; TagHit.LocalCoords.y = y; if (DebugLog) { LgiTrace("%i:GetTagByPos DirectHit %s #%s, idx=%i, near=%i\n", Depth, Tag.Get(), HtmlId, TagHit.Index, TagHit.Near); } } if (TagId == TAG_BODY) InBody = true; for (unsigned i=0; iPos.x >= 0 && t->Pos.y >= 0) { t->GetTagByPos(TagHit, x - t->Pos.x, y - t->Pos.y, Depth + 1, InBody, DebugLog); } } } int LTag::OnNotify(LNotification n) { if (!Ctrl || !Html->InThread()) return 0; switch (CtrlType) { case CtrlSubmit: { LTag *Form = this; while (Form && Form->TagId != TAG_FORM) Form = ToTag(Form->Parent); if (Form) Html->OnSubmitForm(Form); break; } default: { CtrlValue = Ctrl->Name(); break; } } return 0; } void LTag::CollectFormValues(LHashTbl,char*> &f) { if (CtrlType != CtrlNone) { const char *Name; if (Get("name", Name)) { char *Existing = f.Find(Name); if (Existing) DeleteArray(Existing); char *Val = CtrlValue.Str(); if (Val) { LStringPipe p(256); for (char *v = Val; *v; v++) { if (*v == ' ') p.Write("+", 1); else if (IsAlpha(*v) || IsDigit(*v) || *v == '_' || *v == '.') p.Write(v, 1); else p.Print("%%%02.2X", *v); } f.Add(Name, p.NewStr()); } else { f.Add(Name, NewStr("")); } } } for (unsigned i=0; iCollectFormValues(f); } } LTag *LTag::FindCtrlId(int Id) { if (Ctrl && Ctrl->GetId() == Id) return this; for (unsigned i=0; iFindCtrlId(Id); if (f) return f; } return NULL; } void LTag::Find(int TagType, LArray &Out) { if (TagId == TagType) { Out.Add(this); } for (unsigned i=0; iFind(TagType, Out); } } void LTag::SetImage(const char *Uri, LSurface *Img) { if (Img) { if (TagId != TAG_IMG) { ImageDef *Def = (ImageDef*)LCss::Props.Find(PropBackgroundImage); if (Def) { Def->Type = ImageOwn; DeleteObj(Def->Img); Def->Img = Img; } } else { if (Img->GetColourSpace() == CsIndex8) { if (Image.Reset(new LMemDC(Img->X(), Img->Y(), System32BitColourSpace))) { Image->Colour(0, 32); Image->Rectangle(); Image->Blt(0, 0, Img); } else LgiTrace("%s:%i - SetImage can't promote 8bit image to 32bit.\n", _FL); } else Image.Reset(Img); LRect r = XSubRect(); if (r.Valid()) { LAutoPtr t(new LMemDC(r.X(), r.Y(), Image->GetColourSpace())); if (t) { t->Blt(0, 0, Image, &r); Image = t; } } } for (unsigned i=0; iCell) { t->Cell->MinContent = 0; t->Cell->MaxContent = 0; } } } else { Html->d->Loading.Add(Uri, this); } } void LTag::LoadImage(const char *Uri) { #if DOCUMENT_LOAD_IMAGES if (!Html->Environment) return; LUri u(Uri); bool LdImg = Html->GetLoadImages(); bool IsRemote = u.sProtocol && ( !_stricmp(u.sProtocol, "http") || !_stricmp(u.sProtocol, "https") || !_stricmp(u.sProtocol, "ftp") ); if (IsRemote && !LdImg) { Html->NeedsCapability("RemoteContent"); return; } else if (u.IsProtocol("data")) { if (!u.sPath) return; const char *s = u.sPath; if (*s++ != '/') return; LAutoString Type(LTokStr(s)); if (*s++ != ',') return; auto p = LString(Type).SplitDelimit(",;:"); if (p.Length() != 2 || !p.Last().Equals("base64")) return; LString Name = LString("name.") + p[0]; auto Filter = LFilterFactory::New(Name, FILTER_CAP_READ, NULL); if (!Filter) return; auto slen = strlen(s); auto blen = BufferLen_64ToBin(slen); LMemStream bin; bin.SetSize(blen); ConvertBase64ToBinary((uint8_t*)bin.GetBasePtr(), blen, s, slen); bin.SetPos(0); if (!Image.Reset(new LMemDC)) return; auto result = Filter->ReadImage(Image, &bin); if (result != LFilter::IoSuccess) Image.Reset(); return; } LDocumentEnv::LoadJob *j = Html->Environment->NewJob(); if (j) { LAssert(Html != NULL); j->Uri.Reset(NewStr(Uri)); j->Env = Html->Environment; j->UserData = this; j->UserUid = Html->GetDocumentUid(); // LgiTrace("%s:%i - new job %p, %p\n", _FL, j, j->UserData); LDocumentEnv::LoadType Result = Html->Environment->GetContent(j); if (Result == LDocumentEnv::LoadImmediate) { SetImage(Uri, j->pDC.Release()); } else if (Result == LDocumentEnv::LoadDeferred) { Html->d->DeferredLoads++; } DeleteObj(j); } #endif } void LTag::LoadImages() { const char *Uri = 0; if (Html->Environment && TagId == TAG_IMG && !Image) { if (Get("src", Uri)) LoadImage(Uri); } for (unsigned i=0; iLoadImages(); } } void LTag::ImageLoaded(char *uri, LSurface *Img, int &Used) { const char *Uri = 0; if (!Image && Get("src", Uri)) { if (strcmp(Uri, uri) == 0) { if (Used == 0) { SetImage(Uri, Img); } else { SetImage(Uri, new LMemDC(Img)); } Used++; } } for (unsigned i=0; iImageLoaded(uri, Img, Used); } } struct LTagElementCallback : public LCss::ElementCallback { const char *Val; const char *GetElement(LTag *obj) { return obj->Tag; } const char *GetAttr(LTag *obj, const char *Attr) { if (obj->Get(Attr, Val)) return Val; return NULL; } bool GetClasses(LString::Array &Classes, LTag *obj) { Classes = obj->Class; return Classes.Length() > 0; } LTag *GetParent(LTag *obj) { return ToTag(obj->Parent); } LArray GetChildren(LTag *obj) { LArray c; for (unsigned i=0; iChildren.Length(); i++) c.Add(ToTag(obj->Children[i])); return c; } }; void LTag::RestyleAll() { Restyle(); for (unsigned i=0; iRestyleAll(); } } // After CSS has changed this function scans through the CSS and applies any rules // that match the current tag. void LTag::Restyle() { // Use the matching built into the LCss Store. LCss::SelArray Styles; LTagElementCallback Context; if (Html->CssStore.Match(Styles, &Context, this)) { for (unsigned i=0; iStyle); } } // Do the element specific styles const char *s; if (Get("style", s)) SetCssStyle(s); #if DEBUG_RESTYLE && defined(_DEBUG) if (Debug) { auto Style = ToString(); LgiTrace(">>>> %s <<<<:\n%s\n\n", Tag.Get(), Style.Get()); } #endif } void LTag::SetStyle() { const static float FntMul[] = { 0.6f, // size=1 0.89f, // size=2 1.0f, // size=3 1.2f, // size=4 1.5f, // size=5 2.0f, // size=6 3.0f // size=7 }; const char *s = 0; #ifdef _DEBUG if (Get("debug", s)) { if ((Debug = atoi(s))) { LgiTrace("Debug Tag: %p '%s'\n", this, Tag ? Tag.Get() : "CONTENT"); } } #endif if (Get("Color", s)) { ColorDef Def; if (LHtmlParser::ParseColour(s, Def)) { Color(Def); } } if (Get("Background", s) || Get("bgcolor", s)) { ColorDef Def; if (LHtmlParser::ParseColour(s, Def)) { BackgroundColor(Def); } else { LCss::ImageDef Img; Img.Type = ImageUri; Img.Uri = s; BackgroundImage(Img); BackgroundRepeat(RepeatBoth); } } switch (TagId) { default: { if (!Stricmp(Tag.Get(), "o:p")) Display(LCss::DispNone); break; } case TAG_LINK: { const char *Type, *Href; if (Html->Environment && Get("type", Type) && Get("href", Href) && !Stricmp(Type, "text/css") && !Html->CssHref.Find(Href)) { LDocumentEnv::LoadJob *j = Html->Environment->NewJob(); if (j) { LAssert(Html != NULL); LTag *t = this; j->Uri.Reset(NewStr(Href)); j->Env = Html->Environment; j->UserData = t; j->UserUid = Html->GetDocumentUid(); LDocumentEnv::LoadType Result = Html->Environment->GetContent(j); if (Result == LDocumentEnv::LoadImmediate) { LStreamI *s = j->GetStream(); if (s) { int Len = (int)s->GetSize(); if (Len > 0) { LAutoString a(new char[Len+1]); ssize_t r = s->Read(a, Len); a[r] = 0; Html->CssHref.Add(Href, true); Html->OnAddStyle("text/css", a); } } } else if (Result == LDocumentEnv::LoadDeferred) { Html->d->DeferredLoads++; } DeleteObj(j); } } break; } case TAG_BLOCKQUOTE: { MarginTop(Len("8px")); MarginBottom(Len("8px")); MarginLeft(Len("16px")); if (Get("Type", s)) { if (_stricmp(s, "cite") == 0) { BorderLeft(BorderDef(this, "1px solid blue")); PaddingLeft(Len("0.5em")); /* ColorDef Def; Def.Type = ColorRgb; Def.Rgb32 = Rgb32(0x80, 0x80, 0x80); Color(Def); */ } } break; } case TAG_P: { MarginBottom(Len("1em")); break; } case TAG_A: { const char *Href; if (Get("href", Href)) { ColorDef c; c.Type = ColorRgb; c.Rgb32 = Rgb32(0, 0, 255); Color(c); TextDecoration(TextDecorUnderline); } break; } case TAG_TABLE: { Len l; if (!Cell) Cell = new TblCell; if (Get("border", s)) { BorderDef b; if (b.Parse(this, s)) { BorderLeft(b); BorderRight(b); BorderTop(b); BorderBottom(b); } } if (Get("cellspacing", s) && l.Parse(s, PropBorderSpacing, ParseRelaxed)) { BorderSpacing(l); } else { // BorderSpacing(LCss::Len(LCss::LenPx, 2.0f)); } if (Get("cellpadding", s) && l.Parse(s, Prop_CellPadding, ParseRelaxed)) { _CellPadding(l); } if (Get("align", s)) { Len l; if (l.Parse(s)) Cell->XAlign = l.Type; } break; } case TAG_TD: case TAG_TH: { if (!Cell) Cell = new TblCell; LTag *Table = GetTable(); if (Table) { Len l = Table->_CellPadding(); if (!l.IsValid()) { l.Type = LCss::LenPx; l.Value = DefaultCellPadding; } PaddingLeft(l); PaddingRight(l); PaddingTop(l); PaddingBottom(l); } if (TagId == TAG_TH) FontWeight(LCss::FontWeightBold); break; } case TAG_BODY: { MarginLeft(Len(Get("leftmargin", s) ? s : DefaultBodyMargin)); MarginTop(Len(Get("topmargin", s) ? s : DefaultBodyMargin)); MarginRight(Len(Get("rightmargin", s) ? s : DefaultBodyMargin)); if (Get("text", s)) { ColorDef c; if (c.Parse(s)) { Color(c); } } break; } case TAG_OL: case TAG_UL: { MarginLeft(Len("16px")); break; } case TAG_STRONG: case TAG_B: { FontWeight(FontWeightBold); break; } case TAG_I: { FontStyle(FontStyleItalic); break; } case TAG_U: { TextDecoration(TextDecorUnderline); break; } case TAG_SUP: { VerticalAlign(VerticalSuper); FontSize(SizeSmaller); break; } case TAG_SUB: { VerticalAlign(VerticalSub); FontSize(SizeSmaller); break; } case TAG_TITLE: { Display(LCss::DispNone); break; } } if (Get("width", s)) { Len l; if (l.Parse(s, PropWidth, ParseRelaxed)) { Width(l); } } if (Get("height", s)) { Len l; if (l.Parse(s, PropHeight, ParseRelaxed)) Height(l); } if (Get("align", s)) { if (_stricmp(s, "left") == 0) TextAlign(Len(AlignLeft)); else if (_stricmp(s, "right") == 0) TextAlign(Len(AlignRight)); else if (_stricmp(s, "center") == 0) TextAlign(Len(AlignCenter)); } if (Get("valign", s)) { if (_stricmp(s, "top") == 0) VerticalAlign(Len(VerticalTop)); else if (_stricmp(s, "middle") == 0) VerticalAlign(Len(VerticalMiddle)); else if (_stricmp(s, "bottom") == 0) VerticalAlign(Len(VerticalBottom)); } Get("id", HtmlId); if (Get("class", s)) { Class = LString(s).SplitDelimit(" \t"); } Restyle(); switch (TagId) { default: break; case TAG_BIG: { LCss::Len l; l.Type = SizeLarger; FontSize(l); break; } /* case TAG_META: { LAutoString Cs; const char *s; if (Get("http-equiv", s) && _stricmp(s, "Content-Type") == 0) { const char *ContentType; if (Get("content", ContentType)) { char *CharSet = stristr(ContentType, "charset="); if (CharSet) { char16 *cs = NULL; Html->ParsePropValue(CharSet + 8, cs); Cs.Reset(WideToUtf8(cs)); DeleteArray(cs); } } } if (Get("name", s) && _stricmp(s, "charset") == 0 && Get("content", s)) { Cs.Reset(NewStr(s)); } else if (Get("charset", s)) { Cs.Reset(NewStr(s)); } if (Cs) { if (Cs && _stricmp(Cs, "utf-16") != 0 && _stricmp(Cs, "utf-32") != 0 && LGetCsInfo(Cs)) { // Html->SetCharset(Cs); } } break; } */ case TAG_BODY: { LCss::ColorDef Bk = BackgroundColor(); if (Bk.Type != ColorInherit) { // Copy the background up to the LHtml wrapper Html->GetCss(true)->BackgroundColor(Bk); } /* LFont *f = GetFont(); if (FontSize().Type == LenInherit) { FontSize(Len(LenPt, (float)f->PointSize())); } */ break; } case TAG_HEAD: { Display(DispNone); break; } case TAG_PRE: { LFontType Type; if (Type.GetSystemFont("Fixed")) { LAssert(ValidStr(Type.GetFace())); FontFamily(StringsDef(Type.GetFace())); } break; } case TAG_TR: break; case TAG_TD: case TAG_TH: { LAssert(Cell != NULL); const char *s; if (Get("colspan", s)) Cell->Span.x = atoi(s); else Cell->Span.x = 1; if (Get("rowspan", s)) Cell->Span.y = atoi(s); else Cell->Span.y = 1; Cell->Span.x = MAX(Cell->Span.x, 1); Cell->Span.y = MAX(Cell->Span.y, 1); if (Display() == DispInline || Display() == DispInlineBlock) { Display(DispBlock); // Inline-block TD??? Nope. } break; } case TAG_IMG: { const char *Uri; if (Html->Environment && Get("src", Uri)) { // printf("Uri: %s\n", Uri); LoadImage(Uri); } break; } case TAG_H1: { char s[32]; sprintf_s(s, sizeof(s), "%ipt", (int)((float)Html->DefFont()->PointSize() * FntMul[5])); FontSize(Len(s)); FontWeight(FontWeightBold); break; } case TAG_H2: { char s[32]; sprintf_s(s, sizeof(s), "%ipt", (int)((float)Html->DefFont()->PointSize() * FntMul[4])); FontSize(Len(s)); FontWeight(FontWeightBold); break; } case TAG_H3: { char s[32]; sprintf_s(s, sizeof(s), "%ipt", (int)((float)Html->DefFont()->PointSize() * FntMul[3])); FontSize(Len(s)); FontWeight(FontWeightBold); break; } case TAG_H4: { char s[32]; sprintf_s(s, sizeof(s), "%ipt", (int)((float)Html->DefFont()->PointSize() * FntMul[2])); FontSize(Len(s)); FontWeight(FontWeightBold); break; } case TAG_H5: { char s[32]; sprintf_s(s, sizeof(s), "%ipt", (int)((float)Html->DefFont()->PointSize() * FntMul[1])); FontSize(Len(s)); FontWeight(FontWeightBold); break; } case TAG_H6: { char s[32]; sprintf_s(s, sizeof(s), "%ipt", (int)((float)Html->DefFont()->PointSize() * FntMul[0])); FontSize(Len(s)); FontWeight(FontWeightBold); break; } case TAG_FONT: { const char *s = 0; if (Get("Face", s)) { char16 *cw = CleanText(s, strlen(s), "utf-8", true); char *c8 = WideToUtf8(cw); DeleteArray(cw); LToken Faces(c8, ","); DeleteArray(c8); char *face = TrimStr(Faces[0]); if (ValidStr(face)) { FontFamily(face); DeleteArray(face); } else { LgiTrace("%s:%i - No face for font tag.\n", __FILE__, __LINE__); } } if (Get("Size", s)) { bool Digit = false, NonW = false; for (auto *c = s; *c; c++) { if (IsDigit(*c) || *c == '-') Digit = true; else if (!IsWhiteSpace(*c)) NonW = true; } if (Digit && !NonW) { auto Sz = atoi(s); switch (Sz) { case 1: FontSize(Len(LCss::LenEm, 0.63f)); break; case 2: FontSize(Len(LCss::LenEm, 0.82f)); break; case 3: FontSize(Len(LCss::LenEm, 1.0f)); break; case 4: FontSize(Len(LCss::LenEm, 1.13f)); break; case 5: FontSize(Len(LCss::LenEm, 1.5f)); break; case 6: FontSize(Len(LCss::LenEm, 2.0f)); break; case 7: FontSize(Len(LCss::LenEm, 3.0f)); break; } } else { FontSize(Len(s)); } } break; } case TAG_SELECT: { if (!Html->InThread()) break; LAssert(!Ctrl); Ctrl = new LCombo(Html->d->NextCtrlId++, 0, 0, 100, LSysFont->GetHeight() + 8, NULL); CtrlType = CtrlSelect; break; } case TAG_INPUT: { if (!Html->InThread()) break; LAssert(!Ctrl); const char *Type, *Value = NULL; Get("value", Value); LAutoWString CleanValue(Value ? CleanText(Value, strlen(Value), "utf-8", true, true) : NULL); if (CleanValue) { CtrlValue = CleanValue; } if (Get("type", Type)) { if (!_stricmp(Type, "password")) CtrlType = CtrlPassword; else if (!_stricmp(Type, "email")) CtrlType = CtrlEmail; else if (!_stricmp(Type, "text")) CtrlType = CtrlText; else if (!_stricmp(Type, "button")) CtrlType = CtrlButton; else if (!_stricmp(Type, "submit")) CtrlType = CtrlSubmit; else if (!_stricmp(Type, "hidden")) CtrlType = CtrlHidden; DeleteObj(Ctrl); if (CtrlType == CtrlEmail || CtrlType == CtrlText || CtrlType == CtrlPassword) { LEdit *Ed; LAutoString UtfCleanValue(WideToUtf8(CleanValue)); Ctrl = Ed = new LEdit(Html->d->NextCtrlId++, 0, 0, 60, LSysFont->GetHeight() + 8, UtfCleanValue); if (Ctrl) { Ed->Sunken(false); Ed->Password(CtrlType == CtrlPassword); } } else if (CtrlType == CtrlButton || CtrlType == CtrlSubmit) { LAutoString UtfCleanValue(WideToUtf8(CleanValue)); if (UtfCleanValue) { Ctrl = new InputButton(this, Html->d->NextCtrlId++, UtfCleanValue); } } } break; } } if (IsBlock()) { LCss::ImageDef bk = BackgroundImage(); if (bk.Type == LCss::ImageUri && ValidStr(bk.Uri) && !bk.Uri.Equals("transparent")) { LoadImage(bk.Uri); } } if (Ctrl) { LFont *f = GetFont(); if (f) Ctrl->SetFont(f, false); } } void LTag::OnStyleChange(const char *name) { if (!Stricmp(name, "display") && Html) { Html->Layout(true); Html->Invalidate(); } } void LTag::SetCssStyle(const char *Style) { if (Style) { // Strip out comments char *Comment = NULL; while ((Comment = strstr((char*)Style, "/*"))) { char *End = strstr(Comment+2, "*/"); if (!End) break; for (char *c = Comment; cDocCharSet && Html->Charset) { DocAndCsTheSame = _stricmp(Html->DocCharSet, Html->Charset) == 0; } if (SourceCs) { t = (char16*) LNewConvertCp(LGI_WideCharset, s, SourceCs, Len); } else if (Html->DocCharSet && Html->Charset && !DocAndCsTheSame && !Html->OverideDocCharset) { char *DocText = (char*)LNewConvertCp(Html->DocCharSet, s, Html->Charset, Len); t = (char16*) LNewConvertCp(LGI_WideCharset, DocText, Html->DocCharSet, -1); DeleteArray(DocText); } else if (Html->DocCharSet) { t = (char16*) LNewConvertCp(LGI_WideCharset, s, Html->DocCharSet, Len); } else { t = (char16*) LNewConvertCp(LGI_WideCharset, s, Html->Charset.Get() ? Html->Charset.Get() : DefaultCs, Len); } if (t && ConversionAllowed) { char16 *o = t; for (char16 *i=t; *i; ) { switch (*i) { case '&': { i++; if (*i == '#') { // Unicode Number char n[32] = "", *p = n; i++; if (*i == 'x' || *i == 'X') { // Hex number i++; while ( *i && ( IsDigit(*i) || (*i >= 'A' && *i <= 'F') || (*i >= 'a' && *i <= 'f') ) && (p - n) < 31) { *p++ = (char)*i++; } } else { // Decimal number while (*i && IsDigit(*i) && (p - n) < 31) { *p++ = (char)*i++; } } *p++ = 0; char16 Ch = atoi(n); if (Ch) { *o++ = Ch; } if (*i && *i != ';') i--; } else { // Named Char char16 *e = i; while (*e && IsAlpha(*e) && *e != ';') { e++; } LAutoWString Var(NewStrW(i, e-i)); char16 Char = LHtmlStatic::Inst->VarMap.Find(Var); if (Char) { *o++ = Char; i = e; } else { i--; *o++ = *i; } } break; } case '\r': { break; } case ' ': case '\t': case '\n': { if (KeepWhiteSpace) { *o++ = *i; } else { *o++ = ' '; // Skip furthur whitespace while (i[1] && IsWhiteSpace(i[1])) { i++; } } break; } default: { // Normal char *o++ = *i; break; } } if (*i) i++; else break; } *o++ = 0; } if (t && !*t) { DeleteArray(t); } return t; } char *LTag::ParseText(char *Doc) { ColorDef c; c.Type = ColorRgb; c.Rgb32 = LColour(L_WORKSPACE).c32(); BackgroundColor(c); TagId = TAG_BODY; Tag.Reset(NewStr("body")); Info = Html->GetTagInfo(Tag); char *OriginalCp = NewStr(Html->Charset); LStringPipe Utf16; char *s = Doc; while (s) { if (*s == '\r') { s++; } else if (*s == '<') { // Process tag char *e = s; e++; while (*e && *e != '>') { if (*e == '\"' || *e == '\'') { char *q = strchr(e + 1, *e); if (q) e = q + 1; else e++; } else e++; } if (*e == '>') e++; // Output tag Html->SetCharset("iso-8859-1"); char16 *t = CleanText(s, e - s, NULL, false); if (t) { Utf16.Push(t); DeleteArray(t); } s = e; } else if (!*s || *s == '\n') { // Output previous line char16 *Line = Utf16.NewStrW(); if (Line) { LTag *t = new LTag(Html, this); if (t) { t->Color(LColour(L_TEXT)); t->Text(Line); } } if (*s == '\n') { s++; LTag *t = new LTag(Html, this); if (t) { t->TagId = TAG_BR; t->Tag.Reset(NewStr("br")); t->Info = Html->GetTagInfo(t->Tag); } } else break; } else { // Seek end of text char *e = s; while (*e && *e != '\r' && *e != '\n' && *e != '<') e++; // Output text Html->SetCharset(OriginalCp); LAutoWString t(CleanText(s, e - s, NULL, false)); if (t) { Utf16.Push(t); } s = e; } } Html->SetCharset(OriginalCp); DeleteArray(OriginalCp); return 0; } bool LTag::ConvertToText(TextConvertState &State) { const static char *Rule = "------------------------------------------------------"; int DepthInc = 0; switch (TagId) { default: break; case TAG_P: if (State.GetPrev()) State.NewLine(); break; case TAG_UL: case TAG_OL: DepthInc = 2; break; } if (ValidStrW(Txt)) { for (int i=0; iConvertToUnicode(Txt); else u.Reset(WideToUtf8(Txt)); if (u) { size_t u_len = strlen(u); State.Write(u, u_len); } } State.Depth += DepthInc; for (unsigned i=0; iConvertToText(State); } State.Depth -= DepthInc; if (IsBlock()) { if (State.CharsOnLine) State.NewLine(); } else { switch (TagId) { case TAG_A: { // Emit the link to the anchor if it's different from the text of the span... const char *Href; if (Get("href", Href) && ValidStrW(Txt)) { if (_strnicmp(Href, "mailto:", 7) == 0) Href += 7; size_t HrefLen = strlen(Href); LAutoWString h(CleanText(Href, HrefLen, "utf-8")); if (h && StrcmpW(h, Txt) != 0) { // Href different from the text of the link State.Write(" (", 2); State.Write(Href, HrefLen); State.Write(")", 1); } } break; } case TAG_HR: { State.Write(Rule, strlen(Rule)); State.NewLine(); break; } case TAG_BR: { State.NewLine(); break; } default: break; } } return true; } char *LTag::NextTag(char *s) { while (s && *s) { char *n = strchr(s, '<'); if (n) { if (!n[1]) return NULL; if (IsAlpha(n[1]) || strchr("!/", n[1]) || n[1] == '?') { return n; } s = n + 1; } else break; } return 0; } void LHtml::CloseTag(LTag *t) { if (!t) return; OpenTags.Delete(t); } bool LTag::OnUnhandledColor(LCss::ColorDef *def, const char *&s) { const char *e = s; while (*e && (IsText(*e) || *e == '_')) e++; char tmp[256]; ssize_t len = e - s; memcpy(tmp, s, len); tmp[len] = 0; int m = LHtmlStatic::Inst->ColourMap.Find(tmp); s = e; if (m >= 0) { def->Type = LCss::ColorRgb; def->Rgb32 = Rgb24To32(m); return true; } return false; } void LTag::ZeroTableElements() { if (TagId == TAG_TABLE || TagId == TAG_TR || IsTableCell(TagId)) { Size.x = 0; Size.y = 0; if (Cell) { Cell->MinContent = 0; Cell->MaxContent = 0; } for (unsigned i=0; iZeroTableElements(); } } } void LTag::ResetCaches() { /* If during the parse process a callback causes a layout to happen then it's possible to have partial information in the LHtmlTableLayout structure, like missing TD cells. Because they haven't been parsed yet. This is called at the end of the parsing to reset all the cached info in LHtmlTableLayout. That way when the first real layout happens all the data is there. */ if (Cell) DeleteObj(Cell->Cells); for (size_t i=0; iResetCaches(); } LPoint LTag::GetTableSize() { LPoint s(0, 0); if (Cell && Cell->Cells) { Cell->Cells->GetSize(s.x, s.y); } return s; } LTag *LTag::GetTableCell(int x, int y) { LTag *t = this; while ( t && !t->Cell && !t->Cell->Cells && t->Parent) { t = ToTag(t->Parent); } if (t && t->Cell && t->Cell->Cells) { return t->Cell->Cells->Get(x, y); } return 0; } // This function gets the largest and smallest piece of content // in this cell and all it's children. bool LTag::GetWidthMetrics(LTag *Table, uint16 &Min, uint16 &Max) { bool Status = true; int MarginPx = 0; int LineWidth = 0; if (Display() == LCss::DispNone) return true; // Break the text into words and measure... if (Text()) { int MinContent = 0; int MaxContent = 0; LFont *f = GetFont(); if (f) { for (char16 *s = Text(); s && *s; ) { // Skip whitespace... while (*s && StrchrW(WhiteW, *s)) s++; // Find end of non-whitespace char16 *e = s; while (*e && !StrchrW(WhiteW, *e)) e++; // Find size of the word ssize_t Len = e - s; if (Len > 0) { LDisplayString ds(f, s, Len); MinContent = MAX(MinContent, ds.X()); } // Move to the next word. s = (*e) ? e + 1 : 0; } LDisplayString ds(f, Text()); LineWidth = MaxContent = ds.X(); } #if 0//def _DEBUG if (Debug) { LgiTrace("GetWidthMetrics Font=%p Sz=%i,%i\n", f, MinContent, MaxContent); } #endif Min = MAX(Min, MinContent); Max = MAX(Max, MaxContent); } // Specific tag handling? switch (TagId) { default: { if (IsBlock()) { MarginPx = (int)(BorderLeft().ToPx() + BorderRight().ToPx() + PaddingLeft().ToPx() + PaddingRight().ToPx()); } break; } case TAG_IMG: { Len w = Width(); if (w.IsValid()) { int x = (int) w.Value; Min = MAX(Min, x); Max = MAX(Max, x); } else if (Image) { Min = Max = Image->X(); } else { Size.x = Size.y = DefaultImgSize; Min = MAX(Min, Size.x); Max = MAX(Max, Size.x); } break; } case TAG_TD: case TAG_TH: { Len w = Width(); if (w.IsValid()) { if (w.IsDynamic()) { Min = MAX(Min, (int)w.Value); Max = MAX(Max, (int)w.Value); } else { Max = w.ToPx(0, GetFont()); } } else { LCss::BorderDef BLeft = BorderLeft(); LCss::BorderDef BRight = BorderRight(); LCss::Len PLeft = PaddingLeft(); LCss::Len PRight = PaddingRight(); MarginPx = (int)(PLeft.ToPx() + PRight.ToPx() + BLeft.ToPx()); if (Table->BorderCollapse() == LCss::CollapseCollapse) MarginPx += BRight.ToPx(); } break; } case TAG_TABLE: { Len w = Width(); if (w.IsValid() && !w.IsDynamic()) { // Fixed width table... int CellSpacing = BorderSpacing().ToPx(Min, GetFont()); int Px = ((int)w.Value) + (CellSpacing << 1); Min = MAX(Min, Px); Max = MAX(Max, Px); return true; } else { LPoint s; LHtmlTableLayout c(this); c.GetSize(s.x, s.y); // Auto layout table LArray ColMin, ColMax; for (int y=0; yGetWidthMetrics(Table, a, b)) { ColMin[x] = MAX(ColMin[x], a); ColMax[x] = MAX(ColMax[x], b); } x += t->Cell->Span.x; } else break; } } int MinSum = 0, MaxSum = 0; for (int i=0; iGetWidthMetrics(Table, Min, TagMax); LineWidth += TagMax; if (c->TagId == TAG_BR || c->TagId == TAG_LI) { Max = MAX(Max, LineWidth); LineWidth = 0; } } Max = MAX(Max, LineWidth); Min += MarginPx; Max += MarginPx; return Status; } static void DistributeSize(LArray &a, int Start, int Span, int Size, int Border) { // Calculate the current size of the cells int Cur = -Border; for (int i=0; i T Sum(LArray &a) { T s = 0; for (unsigned i=0; iCells) { #if defined(_DEBUG) && DEBUG_TABLE_LAYOUT if (Debug) { //int asd=0; } #endif Cell->Cells = new LHtmlTableLayout(this); #if defined(_DEBUG) && DEBUG_TABLE_LAYOUT if (Cell->Cells && Debug) Cell->Cells->Dump(); #endif } if (Cell->Cells) Cell->Cells->LayoutTable(f, Depth); } void LHtmlTableLayout::AllocatePx(int StartCol, int Cols, int MinPx, bool HasToFillAllAvailable) { // Get the existing total size and size of the column set int CurrentTotalX = GetTotalX(); int CurrentSpanX = GetTotalX(StartCol, Cols); int MaxAdditionalPx = AvailableX - CurrentTotalX; if (MaxAdditionalPx <= 0) return; // Calculate the maximum space we have for this column set int AvailPx = (CurrentSpanX + MaxAdditionalPx) - BorderX1 - BorderX2; // Allocate any remaining space... int RemainingPx = MaxAdditionalPx; LArray Growable, NonGrowable, SizeInherit; int GrowablePx = 0; for (int x=StartCol; x 0) { GrowablePx += DiffPx; Growable.Add(x); } else if (MinCol[x] > 0) { NonGrowable.Add(x); } else if (MinCol[x] == 0 && CurrentSpanX < AvailPx) { // Growable.Add(x); } if (SizeCol[x].Type == LCss::LenInherit) SizeInherit.Add(x); } if (GrowablePx < RemainingPx && HasToFillAllAvailable) { if (Growable.Length() == 0) { // Add any suitable non-growable columns as well for (unsigned i=0; i MinCol[Largest]) Largest = i; } Growable.Add(Largest); } } if (Growable.Length()) { // Some growable columns... int Added = 0; // Reasonably increase the size of the columns... for (unsigned i=0; i 0) { AddPx = DiffPx; } else if (DiffPx > 0) { double Ratio = (double)DiffPx / GrowablePx; AddPx = (int) (Ratio * RemainingPx); } else { AddPx = RemainingPx / (int)Growable.Length(); } LAssert(AddPx >= 0); MinCol[x] += AddPx; LAssert(MinCol[x] >= 0); Added += AddPx; } if (Added < RemainingPx && HasToFillAllAvailable) { // Still more to add, so if (SizeInherit.Length()) { Growable = SizeInherit; } else { int Largest = -1; for (unsigned i=0; i MinCol[Largest]) Largest = x; } Growable.Length(1); Growable[0] = Largest; } int AddPx = (RemainingPx - Added) / (int)Growable.Length(); for (unsigned i=0; i= 0); } else { MinCol[x] += AddPx; LAssert(MinCol[x] >= 0); Added += AddPx; } } } } } struct ColInfo { int Large; int Growable; int Idx; int Px; }; int ColInfoCmp(ColInfo *a, ColInfo *b) { int LDiff = b->Large - a->Large; int LGrow = b->Growable - a->Growable; int LSize = b->Px - a->Px; return LDiff + LGrow + LSize; } void LHtmlTableLayout::DeallocatePx(int StartCol, int Cols, int MaxPx) { int TotalPx = GetTotalX(StartCol, Cols); if (TotalPx <= MaxPx || MaxPx == 0) return; int TrimPx = TotalPx - MaxPx; LArray Inf; int HalfMax = MaxPx >> 1; unsigned Interesting = 0; int InterestingPx = 0; for (int x=StartCol; x HalfMax; ci.Growable = MinCol[x] < MaxCol[x]; if (ci.Large || ci.Growable) { Interesting++; InterestingPx += ci.Px; } } Inf.Sort(ColInfoCmp); if (InterestingPx > 0) { for (unsigned i=0; i= 0); } else break; } } } int LHtmlTableLayout::GetTotalX(int StartCol, int Cols) { if (Cols < 0) Cols = s.x; int TotalX = BorderX1 + BorderX2 + CellSpacing; for (int x=StartCol; xZeroTableElements(); MinCol.Length(0); MaxCol.Length(0); MaxRow.Length(0); SizeCol.Length(0); LCss::Len BdrSpacing = Table->BorderSpacing(); CellSpacing = BdrSpacing.IsValid() ? (int)BdrSpacing.Value : 0; // Resolve total table width. TableWidth = Table->Width(); if (TableWidth.IsValid()) AvailableX = f->ResolveX(TableWidth, Table, false); else AvailableX = f->X(); LCss::Len MaxWidth = Table->MaxWidth(); if (MaxWidth.IsValid()) { int Px = f->ResolveX(MaxWidth, Table, false); if (Px < AvailableX) AvailableX = Px; } TableBorder = f->ResolveBorder(Table, Table); if (Table->BorderCollapse() != LCss::CollapseCollapse) TablePadding = f->ResolvePadding(Table, Table); else TablePadding.ZOff(0, 0); BorderX1 = TableBorder.x1 + TablePadding.x1; BorderX2 = TableBorder.x2 + TablePadding.x2; #if defined(_DEBUG) && DEBUG_TABLE_LAYOUT if (Table->Debug) LgiTrace("AvailableX=%i, BorderX1=%i, BorderX2=%i\n", AvailableX, BorderX1, BorderX2); #endif #ifdef _DEBUG if (Table->Debug) { printf("Table Debug\n"); } #endif // Size detection pass int y; for (y=0; yGetFont(); t->Cell->BorderPx = f->ResolveBorder(t, t); t->Cell->PaddingPx = f->ResolvePadding(t, t); if (t->Cell->Pos.x == x && t->Cell->Pos.y == y) { LCss::DisplayType Disp = t->Display(); if (Disp == LCss::DispNone) continue; LCss::Len Content = t->Width(); if (Content.IsValid() && t->Cell->Span.x == 1) { if (SizeCol[x].IsValid()) { int OldPx = f->ResolveX(SizeCol[x], t, false); int NewPx = f->ResolveX(Content, t, false); if (NewPx > OldPx) { SizeCol[x] = Content; } } else { SizeCol[x] = Content; } } if (!t->GetWidthMetrics(Table, t->Cell->MinContent, t->Cell->MaxContent)) { t->Cell->MinContent = 16; t->Cell->MaxContent = 16; } #if defined(_DEBUG) && DEBUG_TABLE_LAYOUT if (Table->Debug) LgiTrace("Content[%i,%i] MIN=%i MAX=%i\n", x, y, t->Cell->MinContent, t->Cell->MaxContent); #endif if (t->Cell->Span.x == 1) { int BoxPx = t->Cell->BorderPx.x1 + t->Cell->BorderPx.x2 + t->Cell->PaddingPx.x1 + t->Cell->PaddingPx.x2; MinCol[x] = MAX(MinCol[x], t->Cell->MinContent + BoxPx); LAssert(MinCol[x] >= 0); MaxCol[x] = MAX(MaxCol[x], t->Cell->MaxContent + BoxPx); } } x += t->Cell->Span.x; } else break; } } // How much space used so far? int TotalX = GetTotalX(); if (TotalX > AvailableX) { // FIXME: // Off -> 'cisra-cqs.html' renders correctly. // On -> 'cisra_outage.html', 'steam1.html' renders correctly. #if 1 DeallocatePx(0, (int)MinCol.Length(), AvailableX); TotalX = GetTotalX(); #endif } #if defined(_DEBUG) && DEBUG_TABLE_LAYOUT #define DumpCols(msg) \ if (Table->Debug) \ { \ LgiTrace("%s Ln%i - TotalX=%i AvailableX=%i\n", msg, __LINE__, TotalX, AvailableX); \ for (unsigned i=0; iDebug) { printf("TableDebug\n"); } #endif // Process spanned cells for (y=0; yCell->Pos.x == x && t->Cell->Pos.y == y) { if (t->Cell->Span.x > 1 || t->Cell->Span.y > 1) { int i; int ColMin = -CellSpacing; int ColMax = -CellSpacing; for (i=0; iCell->Span.x; i++) { ColMin += MinCol[x + i] + CellSpacing; ColMax += MaxCol[x + i] + CellSpacing; } LCss::Len Width = t->Width(); if (Width.IsValid()) { int Px = f->ResolveX(Width, t, false); t->Cell->MinContent = MAX(t->Cell->MinContent, Px); t->Cell->MaxContent = MAX(t->Cell->MaxContent, Px); } #if defined(_DEBUG) && DEBUG_TABLE_LAYOUT if (Table->Debug) LgiTrace("Content[%i,%i] MIN=%i MAX=%i\n", x, y, t->Cell->MinContent, t->Cell->MaxContent); #endif if (t->Cell->MinContent > ColMin) AllocatePx(t->Cell->Pos.x, t->Cell->Span.x, t->Cell->MinContent, false); if (t->Cell->MaxContent > ColMax) DistributeSize(MaxCol, t->Cell->Pos.x, t->Cell->Span.x, t->Cell->MaxContent, CellSpacing); } x += t->Cell->Span.x; } else break; } } TotalX = GetTotalX(); DumpCols("AfterSpannedCells"); // Sometimes the web page specifies too many percentages: // Scale them all. float PercentSum = 0.0f; for (int i=0; i 100.0) { float Ratio = PercentSum / 100.0f; for (int i=0; iResolveX(w, Table, false); if (w.Type == LCss::LenPercent) { MaxCol[x] = Px; } else if (Px > MinCol[x]) { int RemainingPx = AvailableX - TotalX; int AddPx = Px - MinCol[x]; AddPx = MIN(RemainingPx, AddPx); TotalX += AddPx; MinCol[x] += AddPx; LAssert(MinCol[x] >= 0); } } } } TotalX = GetTotalX(); DumpCols("AfterCssNonPercentageSizes"); if (TotalX > AvailableX) { #if !ALLOW_TABLE_GROWTH // Deallocate space if overused // Take some from the largest column int Largest = 0; for (int i=0; i MinCol[Largest]) { Largest = i; } } int Take = TotalX - AvailableX; if (Take < MinCol[Largest]) { MinCol[Largest] = MinCol[Largest] - Take; LAssert(MinCol[Largest] >= 0); TotalX -= Take; } DumpCols("AfterSpaceDealloc"); #endif } else if (TotalX < AvailableX) { AllocatePx(0, s.x, AvailableX, TableWidth.IsValid()); DumpCols("AfterRemainingAlloc"); } // Layout cell horizontally and then flow the contents to get // the height of all the cells LArray RowPad; MaxRow.Length(s.y); for (y=0; yCell->Pos.x == x && t->Cell->Pos.y == y) { t->Pos.x = XPos; t->Size.x = -CellSpacing; XPos -= CellSpacing; RowPad[y].y1 = MAX(RowPad[y].y1, t->Cell->BorderPx.y1 + t->Cell->PaddingPx.y1); RowPad[y].y2 = MAX(RowPad[y].y2, t->Cell->BorderPx.y2 + t->Cell->PaddingPx.y2); LRect Box(0, 0, -CellSpacing, 0); for (int i=0; iCell->Span.x; i++) { int ColSize = MinCol[x + i] + CellSpacing; LAssert(ColSize >= 0); if (ColSize < 0) break; t->Size.x += ColSize; XPos += ColSize; Box.x2 += ColSize; } LCss::Len Ht = t->Height(); LFlowRegion r(Table->Html, Box, true); t->OnFlow(&r, Depth+1); if (r.MAX.y > r.y2) { t->Size.y = MAX(r.MAX.y, t->Size.y); } if (Ht.IsValid() && Ht.Type != LCss::LenPercent) { int h = f->ResolveY(Ht, t, false); t->Size.y = MAX(h, t->Size.y); DistributeSize(MaxRow, y, t->Cell->Span.y, t->Size.y, CellSpacing); } } x += t->Cell->Span.x; } } #if defined(_DEBUG) DEBUG_LOG("%s:%i - AfterCellFlow\n", _FL); for (unsigned i=0; iCell->Pos.x == x && t->Cell->Pos.y == y) { LCss::Len Ht = t->Height(); if (!(Ht.IsValid() && Ht.Type != LCss::LenPercent)) { DistributeSize(MaxRow, y, t->Cell->Span.y, t->Size.y, CellSpacing); } } x += t->Cell->Span.x; } else break; } } // Cell positioning int Cx = BorderX1 + CellSpacing; int Cy = TableBorder.y1 + TablePadding.y1 + CellSpacing; for (y=0; yParent); if (Row && Row->TagId == TAG_TR) { t = new LTag(Table->Html, Row); if (t) { t->TagId = TAG_TD; t->Tag.Reset(NewStr("td")); t->Info = Table->Html->GetTagInfo(t->Tag); if ((t->Cell = new LTag::TblCell)) { t->Cell->Pos.x = x; t->Cell->Pos.y = y; t->Cell->Span.x = 1; t->Cell->Span.y = 1; } t->BackgroundColor(LCss::ColorDef(LCss::ColorRgb, DefaultMissingCellColour)); Set(Table); } else break; } else break; } if (t) { if (t->Cell->Pos.x == x && t->Cell->Pos.y == y) { int RowPadOffset = RowPad[y].y1 - t->Cell->BorderPx.y1 - t->Cell->PaddingPx.y1; t->Pos.x = Cx; t->Pos.y = Cy + RowPadOffset; t->Size.x = -CellSpacing; for (int i=0; iCell->Span.x; i++) { int w = MinCol[x + i] + CellSpacing; t->Size.x += w; Cx += w; } t->Size.y = -CellSpacing; for (int n=0; nCell->Span.y; n++) { t->Size.y += MaxRow[y+n] + CellSpacing; } Table->Size.x = MAX(Cx + BorderX2, Table->Size.x); #if defined(_DEBUG) && DEBUG_TABLE_LAYOUT if (Table->Debug) { LgiTrace("cell(%i,%i) = pos(%i,%i)+size(%i,%i)\n", t->Cell->Pos.x, t->Cell->Pos.y, t->Pos.x, t->Pos.y, t->Size.x, t->Size.y); } #endif } else { Cx += t->Size.x + CellSpacing; } x += t->Cell->Span.x; } else break; Prev = t; } Cx = BorderX1 + CellSpacing; Cy += MaxRow[y] + CellSpacing; } switch (Table->Cell->XAlign ? Table->Cell->XAlign : ToTag(Table->Parent)->GetAlign(true)) { case LCss::AlignCenter: { int fx = f->X(); int Ox = (fx-Table->Size.x) >> 1; Table->Pos.x = f->x1 + MAX(Ox, 0); DEBUG_LOG("%s:%i - AlignCenter fx=%i ox=%i pos.x=%i size.x=%i\n", _FL, fx, Ox, Table->Pos.x, Table->Size.x); break; } case LCss::AlignRight: { Table->Pos.x = f->x2 - Table->Size.x; DEBUG_LOG("%s:%i - AlignRight f->x2=%i size.x=%i pos.x=%i\n", _FL, f->x2, Table->Size.x, Table->Pos.x); break; } default: { Table->Pos.x = f->x1; DEBUG_LOG("%s:%i - AlignLeft f->x1=%i size.x=%i pos.x=%i\n", _FL, f->x2, Table->Size.x, Table->Pos.x); break; } } Table->Pos.y = f->y1; Table->Size.y = Cy + TablePadding.y2 + TableBorder.y2; } LRect LTag::ChildBounds() { LRect b(0, 0, -1, -1); for (unsigned i=0; iGetRect(); b.Union(&c); } else { b = t->GetRect(); } } return b; } LPoint LTag::AbsolutePos() { LPoint p; for (LTag *t=this; t; t=ToTag(t->Parent)) { p += t->Pos; } return p; } void LTag::SetSize(LPoint &s) { Size = s; } LHtmlArea::~LHtmlArea() { DeleteObjects(); } LRect LHtmlArea::Bounds() { LRect n(0, 0, -1, -1); for (unsigned i=0; iLength(); i++) { LRect *r = (*c)[i]; if (!Top || (r && (r->y1 < Top->y1))) { Top = r; } } return Top; } void LHtmlArea::FlowText(LTag *Tag, LFlowRegion *Flow, LFont *Font, int LineHeight, char16 *Text, LCss::LengthType Align) { if (!Flow || !Text || !Font) return; SetFixedLength(false); char16 *Start = Text; size_t FullLen = StrlenW(Text); #if 1 if (!Tag->Html->GetReadOnly() && !*Text) { // Insert a text rect for this tag, even though it's empty. // This allows the user to place the cursor on a blank line. LFlowRect *Tr = new LFlowRect; Tr->Tag = Tag; Tr->Text = Text; Tr->x1 = Flow->cx; Tr->x2 = Tr->x1 + 1; Tr->y1 = Flow->y1; Tr->y2 = Tr->y1 + Font->GetHeight(); LAssert(Tr->y2 >= Tr->y1); Flow->y2 = MAX(Flow->y2, Tr->y2+1); Flow->cx = Tr->x2 + 1; Add(Tr); Flow->Insert(Tr, Align); return; } #endif while (*Text) { LFlowRect *Tr = new LFlowRect; if (!Tr) break; Tr->Tag = Tag; Restart: Tr->x1 = Flow->cx; Tr->y1 = Flow->y1; #if 1 // I removed this at one stage but forget why. // Remove white space at start of line if not in edit mode.. if (Tag->Html->GetReadOnly() && Flow->x1 == Flow->cx && *Text == ' ') { Text++; if (!*Text) { DeleteObj(Tr); break; } } #endif Tr->Text = Text; LDisplayString ds(Font, Text, MIN(1024, FullLen - (Text-Start))); ssize_t Chars = ds.CharAt(Flow->X()); bool Wrap = false; if (Text[Chars]) { // Word wrap // Seek back to the nearest break opportunity ssize_t n = Chars; while (n > 0 && !StrchrW(WhiteW, Text[n])) n--; if (n == 0) { if (Flow->x1 == Flow->cx) { // Already started from the margin and it's too long to // fit across the entire page, just let it hang off the right edge. // Seek to the end of the word for (Tr->Len = Chars; Text[Tr->Len] && !StrchrW(WhiteW, Text[Tr->Len]); Tr->Len++) ; // Wrap... if (*Text == ' ') Text++; } else { // Not at the start of the margin Flow->FinishLine(); goto Restart; } } else { Tr->Len = n; LAssert(Tr->Len > 0); Wrap = true; } } else { // Fits.. Tr->Len = Chars; LAssert(Tr->Len > 0); } LDisplayString ds2(Font, Tr->Text, Tr->Len); Tr->x2 = ds2.X(); Tr->y2 = LineHeight > 0 ? LineHeight - 1 : 0; if (Wrap) { Flow->cx = Flow->x1; Flow->y1 += Tr->y2 + 1; Tr->x2 = Flow->x2 - Tag->RelX(); } else { Tr->x2 += Tr->x1 - 1; Flow->cx = Tr->x2 + 1; } Tr->y2 += Tr->y1; Flow->y2 = MAX(Flow->y2, Tr->y2 + 1); Add(Tr); Flow->Insert(Tr, Align); Text += Tr->Len; if (Wrap) { while (*Text == ' ') Text++; } Tag->Size.x = MAX(Tag->Size.x, Tr->x2 + 1); Tag->Size.y = MAX(Tag->Size.y, Tr->y2 + 1); Flow->MAX.x = MAX(Flow->MAX.x, Tr->x2); Flow->MAX.y = MAX(Flow->MAX.y, Tr->y2); if (Tr->Len == 0) break; } SetFixedLength(true); } char16 htoi(char16 c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; LAssert(0); return 0; } bool LTag::Serialize(LXmlTag *t, bool Write) { LRect pos; if (Write) { // Obj -> Tag if (Tag) t->SetAttr("tag", Tag); pos.ZOff(Size.x, Size.y); pos.Offset(Pos.x, Pos.y); t->SetAttr("pos", pos.GetStr()); t->SetAttr("tagid", TagId); if (Txt) { LStringPipe p(256); for (char16 *c = Txt; *c; c++) { if (*c > ' ' && *c < 127 && !strchr("%<>\'\"", *c)) p.Print("%c", (char)*c); else p.Print("%%%.4x", *c); } LAutoString Tmp(p.NewStr()); t->SetContent(Tmp); } if (Props.Length()) { auto CssStyles = ToString(); LAssert(!strchr(CssStyles, '\"')); t->SetAttr("style", CssStyles); } if (Html->Cursor == this) { LAssert(Cursor >= 0); t->SetAttr("cursor", (int64)Cursor); } else LAssert(Cursor < 0); if (Html->Selection == this) { LAssert(Selection >= 0); t->SetAttr("selection", (int64)Selection); } else LAssert(Selection < 0); for (unsigned i=0; iInsertTag(child); if (!tag->Serialize(child, Write)) { return false; } } } else { // Tag -> Obj Tag.Reset(NewStr(t->GetAttr("tag"))); TagId = (HtmlTag) t->GetAsInt("tagid"); pos.SetStr(t->GetAttr("pos")); if (pos.Valid()) { Pos.x = pos.x1; Pos.y = pos.y1; Size.x = pos.x2; Size.y = pos.y2; } if (ValidStr(t->GetContent())) { LStringPipe p(256); char *c = t->GetContent(); SkipWhiteSpace(c); for (; *c && *c > ' '; c++) { char16 ch; if (*c == '%') { ch = 0; for (int i=0; i<4 && *c; i++) { ch <<= 4; ch |= htoi(*++c); } } else ch = *c; p.Write(&ch, sizeof(ch)); } Txt.Reset(p.NewStrW()); } const char *s = t->GetAttr("style"); if (s) Parse(s, ParseRelaxed); s = t->GetAttr("cursor"); if (s) { LAssert(Html->Cursor == NULL); Html->Cursor = this; Cursor = atoi(s); LAssert(Cursor >= 0); } s = t->GetAttr("selection"); if (s) { LAssert(Html->Selection == NULL); Html->Selection = this; Selection = atoi(s); LAssert(Selection >= 0); } #ifdef _DEBUG s = t->GetAttr("debug"); if (s && atoi(s) != 0) Debug = true; #endif for (int i=0; iChildren.Length(); i++) { LXmlTag *child = t->Children[i]; if (child->IsTag("e")) { LTag *tag = new LTag(Html, NULL); if (!tag) { LAssert(0); return false; } if (!tag->Serialize(child, Write)) { return false; } Attach(tag); } } } return true; } /* /// This method centers the text in the area given to the tag. Used for inline block elements. void LTag::CenterText() { if (!Parent) return; // Find the size of the text elements. int ContentPx = 0; for (unsigned i=0; iX(); } LFont *f = GetFont(); int ParentPx = ToTag(Parent)->Size.x; int AvailPx = Size.x; // Remove the border and padding from the content area AvailPx -= BorderLeft().ToPx(ParentPx, f); AvailPx -= BorderRight().ToPx(ParentPx, f); AvailPx -= PaddingLeft().ToPx(ParentPx, f); AvailPx -= PaddingRight().ToPx(ParentPx, f); if (AvailPx > ContentPx) { // Now offset all the regions to the right int OffPx = (AvailPx - ContentPx) >> 1; for (unsigned i=0; iOffset(OffPx, 0); } } } */ void LTag::OnFlow(LFlowRegion *Flow, uint16 Depth) { if (Depth >= MAX_RECURSION_DEPTH) return; DisplayType Disp = Display(); if (Disp == DispNone) return; LFont *f = GetFont(); LFlowRegion Local(*Flow); bool Restart = true; int BlockFlowWidth = 0; const char *ImgAltText = NULL; Size.x = 0; Size.y = 0; LCssTools Tools(this, f); LRect rc(Flow->X(), Html->Y()); PadPx = Tools.GetPadding(rc); if (TipId) { Html->Tip.DeleteTip(TipId); TipId = 0; } switch (TagId) { default: break; case TAG_BODY: { Flow->InBody++; break; } case TAG_IFRAME: { LFlowRegion Temp = *Flow; Flow->EndBlock(); Flow->Indent(this, MarginLeft(), MarginTop(), MarginRight(), MarginBottom(), true); // Flow children for (unsigned i=0; iOnFlow(&Temp, Depth + 1); if (TagId == TAG_TR) { Temp.x2 -= MIN(t->Size.x, Temp.X()); } } Flow->Outdent(this, MarginLeft(), MarginTop(), MarginRight(), MarginBottom(), true); BoundParents(); return; break; } case TAG_TR: { Size.x = Flow->X(); break; } case TAG_IMG: { Size.x = Size.y = 0; LCss::Len w = Width(); LCss::Len h = Height(); // LCss::Len MinX = MinWidth(); // LCss::Len MaxX = MaxWidth(); LCss::Len MinY = MinHeight(); LCss::Len MaxY = MaxHeight(); LAutoPtr a; int ImgX, ImgY; if (Image) { ImgX = Image->X(); ImgY = Image->Y(); } else if (Get("alt", ImgAltText) && ValidStr(ImgAltText)) { LDisplayString a(f, ImgAltText); ImgX = a.X() + 4; ImgY = a.Y() + 4; } else { ImgX = DefaultImgSize; ImgY = DefaultImgSize; } double AspectRatio = ImgY != 0 ? (double)ImgX / ImgY : 1.0; bool XLimit = false, YLimit = false; double Scale = 1.0; if (w.IsValid() && w.Type != LenAuto) { Size.x = Flow->ResolveX(w, this, false); XLimit = true; } else { int Fx = Flow->x2 - Flow->x1 + 1; if (ImgX > Fx) { Size.x = Fx; // * 0.8; if (Image) Scale = (double) Fx / ImgX; } else { Size.x = ImgX; } } XLimit |= Flow->LimitX(Size.x, MinWidth(), MaxWidth(), f); if (h.IsValid() && h.Type != LenAuto) { Size.y = Flow->ResolveY(h, this, false); YLimit = true; } else { Size.y = (int) (ImgY * Scale); } YLimit |= Flow->LimitY(Size.y, MinHeight(), MaxHeight(), f); if ( (XLimit ^ YLimit) && Image ) { if (XLimit) { Size.y = (int) ceil((double)Size.x / AspectRatio); } else { Size.x = (int) ceil((double)Size.y * AspectRatio); } } if (MinY.IsValid()) { int Px = Flow->ResolveY(MinY, this, false); if (Size.y < Px) Size.y = Px; } if (MaxY.IsValid()) { int Px = Flow->ResolveY(MaxY, this, false); if (Size.y > Px) Size.y = Px; } if (Disp == DispInline || Disp == DispInlineBlock) { Restart = false; if (Flow->cx > Flow->x1 && Size.x > Flow->X()) { Flow->FinishLine(); } Pos.y = Flow->y1; Flow->y2 = MAX(Flow->y1, Pos.y + Size.y - 1); LCss::LengthType a = GetAlign(true); switch (a) { case AlignCenter: { int Fx = Flow->x2 - Flow->x1; Pos.x = Flow->x1 + ((Fx - Size.x) / 2); break; } case AlignRight: { Pos.x = Flow->x2 - Size.x; break; } default: { Pos.x = Flow->cx; break; } } } break; } case TAG_HR: { Flow->FinishLine(); Pos.x = Flow->x1; Pos.y = Flow->y1 + 7; Size.x = Flow->X(); Size.y = 2; Flow->cx ++; Flow->y2 += 16; Flow->FinishLine(); return; break; } case TAG_TABLE: { Flow->EndBlock(); LCss::Len left = GetCssLen(MarginLeft, Margin); LCss::Len top = GetCssLen(MarginTop, Margin); LCss::Len right = GetCssLen(MarginRight, Margin); LCss::Len bottom = GetCssLen(MarginBottom, Margin); Flow->Indent(this, left, top, right, bottom, true); LayoutTable(Flow, Depth + 1); Flow->y1 += Size.y; Flow->y2 = Flow->y1; Flow->cx = Flow->x1; Flow->my = 0; Flow->MAX.y = MAX(Flow->MAX.y, Flow->y2); Flow->Outdent(this, left, top, right, bottom, true); BoundParents(); return; } } if (Disp == DispBlock || Disp == DispInlineBlock) { // This is a block level element, so end the previous non-block elements if (Disp == DispBlock) Flow->EndBlock(); #ifdef _DEBUG if (Debug) LgiTrace("Before %s\n", Flow->ToString().Get()); #endif BlockFlowWidth = Flow->X(); // Indent the margin... LCss::Len left = GetCssLen(MarginLeft, Margin); LCss::Len top = GetCssLen(MarginTop, Margin); LCss::Len right = GetCssLen(MarginRight, Margin); LCss::Len bottom = GetCssLen(MarginBottom, Margin); Flow->Indent(this, left, top, right, bottom, true); // Set the width if any LCss::Len Wid = Width(); if (!IsTableCell(TagId) && Wid.IsValid()) Size.x = Flow->ResolveX(Wid, this, false); else if (TagId != TAG_IMG) { if (Disp == DispInlineBlock) // Flow->Inline) Size.x = 0; // block inside inline-block default to fit the content else Size.x = Flow->X(); } else if (Disp == DispInlineBlock) Size.x = 0; if (MaxWidth().IsValid()) { int Px = Flow->ResolveX(MaxWidth(), this, false); if (Size.x > Px) Size.x = Px; } if (MinWidth().IsValid()) { int Px = Flow->ResolveX(MinWidth(), this, false); if (Size.x < Px) Size.x = Px; } Pos.x = Disp == DispInlineBlock ? Flow->cx : Flow->x1; Pos.y = Flow->y1; Flow->y1 -= Pos.y; Flow->y2 -= Pos.y; if (Disp == DispBlock) { Flow->x1 -= Pos.x; Flow->x2 = Flow->x1 + Size.x; Flow->cx -= Pos.x; Flow->Indent(this, LCss::BorderLeft(), LCss::BorderTop(), LCss::BorderRight(), LCss::BorderBottom(), false); Flow->Indent(PadPx, false); } else { Flow->x2 = Flow->X(); Flow->x1 = Flow->ResolveX(BorderLeft(), this, true) + Flow->ResolveX(PaddingLeft(), this, true); Flow->cx = Flow->x1; Flow->y1 += Flow->ResolveY(BorderTop(), this, true) + Flow->ResolveY(PaddingTop(), this, true); Flow->y2 = Flow->y1; if (!IsTableTag()) Flow->Inline++; } } else { Flow->Indent(PadPx, false); } if (f) { // Clear the previous text layout... TextPos.DeleteObjects(); switch (TagId) { default: break; case TAG_LI: { // Insert the list marker if (!PreText()) { LCss::ListStyleTypes s = Parent->ListStyleType(); if (s == ListInherit) { if (Parent->TagId == TAG_OL) s = ListDecimal; else if (Parent->TagId == TAG_UL) s = ListDisc; } switch (s) { default: break; case ListDecimal: { ssize_t Index = Parent->Children.IndexOf(this); char Txt[32]; sprintf_s(Txt, sizeof(Txt), "%i. ", (int)(Index + 1)); PreText(Utf8ToWide(Txt)); break; } case ListDisc: { PreText(NewStrW(LHtmlListItem)); break; } } } if (PreText()) TextPos.FlowText(this, Flow, f, f->GetHeight(), PreText(), AlignLeft); break; } case TAG_IMG: { if (Disp == DispBlock) { Flow->cx += Size.x; Flow->y2 += Size.y; } break; } } if (Text() && Flow->InBody) { // Setup the line height cache if (LineHeightCache < 0) { LCss::Len LineHt; LFont *LineFnt = GetFont(); for (LTag *t = this; t && !LineHt.IsValid(); t = ToTag(t->Parent)) { LineHt = t->LineHeight(); if (t->TagId == TAG_TABLE) break; } if (LineFnt) { int FontPx = LineFnt->GetHeight(); if (!LineHt.IsValid() || LineHt.Type == LCss::LenAuto || LineHt.Type == LCss::LenNormal) { LineHeightCache = FontPx; // LgiTrace("LineHeight FontPx=%i Px=%i Auto\n", FontPx, LineHeightCache); } else if (LineHt.Type == LCss::LenPx) { auto Scale = Html->GetDpiScale().y; LineHt.Value *= (float)Scale; LineHeightCache = LineHt.ToPx(FontPx, f); // LgiTrace("LineHeight FontPx=%i Px=%i (Scale=%f)\n", FontPx, LineHeightCache, Scale); } else { LineHeightCache = LineHt.ToPx(FontPx, f); // LgiTrace("LineHeight FontPx=%i Px=%i ToPx\n", FontPx, LineHeightCache); } } } // Flow in the rest of the text... char16 *Txt = Text(); LCss::LengthType Align = GetAlign(true); TextPos.FlowText(this, Flow, f, LineHeightCache, Txt, Align); #ifdef _DEBUG if (Debug) LgiTrace("%s:%i - %p.size=%p\n", _FL, this, &Size.x); #endif } } // Flow children PostFlowAlign.Length(0); for (unsigned i=0; iPosition()) { case PosStatic: case PosAbsolute: case PosFixed: { LFlowRegion old = *Flow; t->OnFlow(Flow, Depth + 1); // Try and reset the flow to how it was before... Flow->x1 = old.x1; Flow->x2 = old.x2; Flow->cx = old.cx; Flow->y1 = old.y1; Flow->y2 = old.y2; Flow->MAX.x = MAX(Flow->MAX.x, old.MAX.x); Flow->MAX.y = MAX(Flow->MAX.y, old.MAX.y); break; } default: { t->OnFlow(Flow, Depth + 1); break; } } if (TagId == TAG_TR) { Flow->x2 -= MIN(t->Size.x, Flow->X()); } } LCss::LengthType XAlign = GetAlign(true); int FlowSz = Flow->Width(); // Align the children... for (auto &group: PostFlowAlign) { int MinX = FlowSz, MaxX = 0; for (auto &a: group) { MinX = MIN(MinX, a.t->Pos.x); MaxX = MAX(MaxX, a.t->Pos.x + a.t->Size.x - 1); } int TotalX = MaxX - MinX + 1; int FirstX = group.Length() ? group[0].t->Pos.x : 0; for (auto &a: group) { if (a.XAlign == LCss::AlignCenter) { int OffX = (Size.x - TotalX) >> 1; if (OffX > 0) { a.t->Pos.x += OffX; } } else if (a.XAlign == LCss::AlignRight) { int OffX = FlowSz - FirstX - TotalX; if (OffX > 0) { a.t->Pos.x += OffX; } } } } if (Disp == DispBlock || Disp == DispInlineBlock) { LCss::Len Ht = Height(); LCss::Len MaxHt = MaxHeight(); // I dunno, there should be a better way... :-( if (MarginLeft().Type == LenAuto && MarginRight().Type == LenAuto) { XAlign = LCss::AlignCenter; } bool AcceptHt = !IsTableCell(TagId) && Ht.Type != LenPercent; if (AcceptHt) { if (Ht.IsValid()) { int HtPx = Flow->ResolveY(Ht, this, false); if (HtPx > Flow->y2) Flow->y2 = HtPx; } if (MaxHt.IsValid()) { int MaxHtPx = Flow->ResolveY(MaxHt, this, false); if (MaxHtPx < Flow->y2) { Flow->y2 = MaxHtPx; Flow->MAX.y = MIN(Flow->y2, Flow->MAX.y); } } } if (Disp == DispBlock) { Flow->EndBlock(); int OldFlowSize = Flow->x2 - Flow->x1 + 1; Flow->Outdent(this, PaddingLeft(), PaddingTop(), PaddingRight(), PaddingBottom(), false); Flow->Outdent(this, LCss::BorderLeft(), LCss::BorderTop(), LCss::BorderRight(), LCss::BorderBottom(), false); Size.y = Flow->y2 > 0 ? Flow->y2 : 0; Flow->Outdent(this, MarginLeft(), MarginTop(), MarginRight(), MarginBottom(), true); int NewFlowSize = Flow->x2 - Flow->x1 + 1; int Diff = NewFlowSize - OldFlowSize; if (Diff) Flow->MAX.x += Diff; Flow->y1 = Flow->y2; Flow->x2 = Flow->x1 + BlockFlowWidth; } else { LCss::Len Wid = Width(); int WidPx = Wid.IsValid() ? Flow->ResolveX(Wid, this, true) : 0; Size.x = MAX(WidPx, Size.x); Size.x += Flow->ResolveX(PaddingRight(), this, true); Size.x += Flow->ResolveX(BorderRight(), this, true); int MarginR = Flow->ResolveX(MarginRight(), this, true); int MarginB = Flow->ResolveX(MarginBottom(), this, true); Flow->x1 = Local.x1 - Pos.x; Flow->cx = Local.cx + Size.x + MarginR - Pos.x; Flow->x2 = Local.x2 - Pos.x; if (Height().IsValid()) { Size.y = Flow->ResolveY(Height(), this, false); Flow->y2 = MAX(Flow->y1 + Size.y + MarginB - 1, Flow->y2); } else { Flow->y2 += Flow->ResolveX(PaddingBottom(), this, true); Flow->y2 += Flow->ResolveX(BorderBottom(), this, true); Size.y = Flow->y2; } Flow->y1 = Local.y1 - Pos.y; Flow->y2 = MAX(Local.y2, Flow->y1+Size.y-1); if (!IsTableTag()) Flow->Inline--; } // Can't do alignment here because pos is used to // restart the parents flow region... } else { Flow->Outdent(PadPx, false); switch (TagId) { default: break; case TAG_SELECT: case TAG_INPUT: { if (Html->InThread() && Ctrl) { LRect r = Ctrl->GetPos(); if (Width().IsValid()) Size.x = Flow->ResolveX(Width(), this, false); else Size.x = r.X(); if (Height().IsValid()) Size.y = Flow->ResolveY(Height(), this, false); else Size.y = r.Y(); if (Html->IsAttached() && !Ctrl->IsAttached()) Ctrl->Attach(Html); } Flow->cx += Size.x; Flow->y2 = MAX(Flow->y2, Flow->y1 + Size.y - 1); break; } case TAG_IMG: { Flow->cx += Size.x; Flow->y2 = MAX(Flow->y2, Flow->y1 + Size.y - 1); break; } case TAG_BR: { int OldFlowY2 = Flow->y2; Flow->FinishLine(); Size.y = Flow->y2 - OldFlowY2; Flow->y2 = MAX(Flow->y2, Flow->y1 + Size.y - 1); break; } case TAG_CENTER: { int Px = Flow->X(); for (auto e: Children) { LTag *t = ToTag(e); if (t && t->IsBlock() && t->Size.x < Px) { t->Pos.x = (Px - t->Size.x) >> 1; } } break; } } } BoundParents(); if (Restart) { Flow->x1 += Pos.x; Flow->x2 += Pos.x; Flow->cx += Pos.x; Flow->y1 += Pos.y; Flow->y2 += Pos.y; Flow->MAX.y = MAX(Flow->MAX.y, Flow->y2); } if (Disp == DispBlock || Disp == DispInlineBlock) { if (XAlign == LCss::AlignCenter || XAlign == LCss::AlignRight) { int Match = 0; auto parent = ToTag(Parent); for (auto &grp: parent->PostFlowAlign) { bool Overlaps = false; for (auto &a: grp) { if (a.Overlap(this)) { Overlaps = true; break; } } if (!Overlaps) Match++; } auto &grp = parent->PostFlowAlign[Match]; if (grp.Length() == 0) { grp.x1 = Flow->x1; grp.x2 = Flow->x2; } auto &pf = grp.New(); pf.Disp = Disp; pf.XAlign = XAlign; pf.t = this; } } if (TagId == TAG_BODY && Flow->InBody > 0) { Flow->InBody--; } } bool LTag::PeekTag(char *s, char *tag) { bool Status = false; if (s && tag) { if (*s == '<') { char *t = 0; Html->ParseName(++s, &t); if (t) { Status = _stricmp(t, tag) == 0; } DeleteArray(t); } } return Status; } LTag *LTag::GetTable() { LTag *t = 0; for (t=ToTag(Parent); t && t->TagId != TAG_TABLE; t = ToTag(t->Parent)) ; return t; } void LTag::BoundParents() { if (!Parent) return; LTag *np; for (LTag *n=this; n; n = np) { np = ToTag(n->Parent); if (!np || np->TagId == TAG_IFRAME) break; np->Size.x = MAX(np->Size.x, n->Pos.x + n->Size.x); np->Size.y = MAX(np->Size.y, n->Pos.y + n->Size.y); } } struct DrawBorder { LSurface *pDC; uint32_t LineStyle; uint32_t LineReset; uint32_t OldStyle; DrawBorder(LSurface *pdc, LCss::BorderDef &d) { LineStyle = 0xffffffff; LineReset = 0x80000000; if (d.Style == LCss::BorderDotted) { switch ((int)d.Value) { case 2: { LineStyle = 0xcccccccc; break; } case 3: { LineStyle = 0xe38e38; LineReset = 0x800000; break; } case 4: { LineStyle = 0xf0f0f0f0; break; } case 5: { LineStyle = 0xf83e0; LineReset = 0x80000; break; } case 6: { LineStyle = 0xfc0fc0; LineReset = 0x800000; break; } case 7: { LineStyle = 0xfe03f80; LineReset = 0x8000000; break; } case 8: { LineStyle = 0xff00ff00; break; } case 9: { LineStyle = 0x3fe00; LineReset = 0x20000; break; } default: { LineStyle = 0xaaaaaaaa; break; } } } pDC = pdc; OldStyle = pDC->LineStyle(); } ~DrawBorder() { pDC->LineStyle(OldStyle); } }; void LTag::GetInlineRegion(LRegion &rgn, int ox, int oy) { if (TagId == TAG_IMG) { LRect rc(0, 0, Size.x-1, Size.y-1); rc.Offset(ox + Pos.x, oy + Pos.y); rgn.Union(&rc); } else { for (unsigned i=0; iGetInlineRegion(rgn, ox + Pos.x, oy + Pos.y); } } class CornersImg : public LMemDC { public: int Px, Px2; CornersImg( float RadPx, LRect *BorderPx, LCss::BorderDef **defs, LColour &Back, bool DrawBackground) { Px = 0; Px2 = 0; //Radius.Type != LCss::LenInherit && if (RadPx > 0.0f) { Px = (int)ceil(RadPx); Px2 = Px << 1; if (Create(Px2, Px2, System32BitColourSpace)) { #if 1 Colour(0, 32); #else Colour(LColour(255, 0, 255)); #endif Rectangle(); LPointF ctr(Px, Px); LPointF LeftPt(0.0, Px); LPointF TopPt(Px, 0.0); LPointF RightPt(X(), Px); LPointF BottomPt(Px, Y()); int x_px[4] = {BorderPx->x1, BorderPx->x2, BorderPx->x2, BorderPx->x1}; int y_px[4] = {BorderPx->y1, BorderPx->y1, BorderPx->y2, BorderPx->y2}; LPointF *pts[4] = {&LeftPt, &TopPt, &RightPt, &BottomPt}; // Draw border parts.. for (int i=0; i<4; i++) { int k = (i + 1) % 4; // Setup the stops LBlendStop stops[2] = { {0.0, 0}, {1.0, 0} }; uint32_t iColour = defs[i]->Color.IsValid() ? defs[i]->Color.Rgb32 : Back.c32(); uint32_t kColour = defs[k]->Color.IsValid() ? defs[k]->Color.Rgb32 : Back.c32(); if (defs[i]->IsValid() && defs[k]->IsValid()) { stops[0].c32 = iColour; stops[1].c32 = kColour; } else if (defs[i]->IsValid()) { stops[0].c32 = stops[1].c32 = iColour; } else { stops[0].c32 = stops[1].c32 = kColour; } // Create a brush LLinearBlendBrush br ( *pts[i], *pts[k], 2, stops ); // Setup the clip LRect clip( (int)MIN(pts[i]->x, pts[k]->x), (int)MIN(pts[i]->y, pts[k]->y), (int)MAX(pts[i]->x, pts[k]->x)-1, (int)MAX(pts[i]->y, pts[k]->y)-1); ClipRgn(&clip); // Draw the arc... LPath p; p.Circle(ctr, Px); if (defs[i]->IsValid() || defs[k]->IsValid()) p.Fill(this, br); // Fill the background p.Empty(); p.Ellipse(ctr, Px-x_px[i], Px-y_px[i]); if (DrawBackground) { LSolidBrush br(Back); p.Fill(this, br); } else { LEraseBrush br; p.Fill(this, br); } ClipRgn(NULL); } #ifdef MAC ConvertPreMulAlpha(true); #endif #if 0 static int count = 0; LString file; file.Printf("c:\\temp\\img-%i.bmp", ++count); GdcD->Save(file, Corners); #endif } } } }; void LTag::PaintBorderAndBackground(LSurface *pDC, LColour &Back, LRect *BorderPx) { LArray r; LRect BorderPxRc; bool DrawBackground = !Back.IsTransparent(); #ifdef _DEBUG if (Debug) { //int asd=0; } #endif if (!BorderPx) BorderPx = &BorderPxRc; BorderPx->ZOff(0, 0); // Get all the border info and work out the pixel sizes. LFont *f = GetFont(); #define DoEdge(coord, axis, name) \ BorderDef name = Border##name(); \ BorderPx->coord = name.Style != LCss::BorderNone ? name.ToPx(Size.axis, f) : 0; #define BorderValid(name) \ ((name).IsValid() && (name).Style != LCss::BorderNone) DoEdge(x1, x, Left); DoEdge(y1, y, Top); DoEdge(x2, x, Right); DoEdge(y2, y, Bottom); LCss::BorderDef *defs[4] = {&Left, &Top, &Right, &Bottom}; if (BorderValid(Left) || BorderValid(Right) || BorderValid(Top) || BorderValid(Bottom) || DrawBackground) { // Work out the rectangles switch (Display()) { case DispInlineBlock: case DispBlock: { r[0].ZOff(Size.x-1, Size.y-1); break; } case DispInline: { LRegion rgn; GetInlineRegion(rgn); if (BorderPx) { for (int i=0; ix1 -= BorderPx->x1 + PadPx.x1; r->y1 -= BorderPx->y1 + PadPx.y1; r->x2 += BorderPx->x2 + PadPx.x2; r->y2 += BorderPx->y2 + PadPx.y2; } } r.Length(rgn.Length()); auto p = r.AddressOf(); for (auto i = rgn.First(); i; i = rgn.Next()) *p++ = *i; break; } default: return; } // If we are drawing rounded corners, draw them into a memory context LAutoPtr Corners; int Px = 0, Px2 = 0; LCss::Len Radius = BorderRadius(); float RadPx = Radius.Type == LCss::LenPx ? Radius.Value : Radius.ToPx(Size.x, GetFont()); bool HasRadius = Radius.Type != LCss::LenInherit && RadPx > 0.0f; // Loop over the rectangles and draw everything int Op = pDC->Op(GDC_ALPHA); for (unsigned i=0; i rc.Y()) { Px = rc.Y() / 2; Px2 = Px << 1; } if (!Corners || Corners->Px2 != Px2) { Corners.Reset(new CornersImg((float)Px, BorderPx, defs, Back, DrawBackground)); } // top left LRect r(0, 0, Px-1, Px-1); pDC->Blt(rc.x1, rc.y1, Corners, &r); // top right r.Set(Px, 0, Corners->X()-1, Px-1); pDC->Blt(rc.x2-Px+1, rc.y1, Corners, &r); // bottom left r.Set(0, Px, Px-1, Corners->Y()-1); pDC->Blt(rc.x1, rc.y2-Px+1, Corners, &r); // bottom right r.Set(Px, Px, Corners->X()-1, Corners->Y()-1); pDC->Blt(rc.x2-Px+1, rc.y2-Px+1, Corners, &r); #if 1 pDC->Colour(Back); pDC->Rectangle(rc.x1+Px, rc.y1, rc.x2-Px, rc.y2); pDC->Rectangle(rc.x1, rc.y1+Px, rc.x1+Px-1, rc.y2-Px); pDC->Rectangle(rc.x2-Px+1, rc.y1+Px, rc.x2, rc.y2-Px); #else pDC->Colour(LColour(255, 0, 0, 0x80)); pDC->Rectangle(rc.x1+Px, rc.y1, rc.x2-Px, rc.y2); pDC->Colour(LColour(0, 255, 0, 0x80)); pDC->Rectangle(rc.x1, rc.y1+Px, rc.x1+Px-1, rc.y2-Px); pDC->Colour(LColour(0, 0, 255, 0x80)); pDC->Rectangle(rc.x2-Px+1, rc.y1+Px, rc.x2, rc.y2-Px); #endif } else if (DrawBackground) { pDC->Colour(Back); pDC->Rectangle(&rc); } LCss::BorderDef *b; if ((b = &Left) && BorderValid(*b)) { pDC->Colour(b->Color.Rgb32, 32); DrawBorder db(pDC, *b); for (int i=0; iValue; i++) { pDC->LineStyle(db.LineStyle, db.LineReset); pDC->Line(rc.x1 + i, rc.y1+Px, rc.x1+i, rc.y2-Px); } } if ((b = &Top) && BorderValid(*b)) { pDC->Colour(b->Color.Rgb32, 32); DrawBorder db(pDC, *b); for (int i=0; iValue; i++) { pDC->LineStyle(db.LineStyle, db.LineReset); pDC->Line(rc.x1+Px, rc.y1+i, rc.x2-Px, rc.y1+i); } } if ((b = &Right) && BorderValid(*b)) { pDC->Colour(b->Color.Rgb32, 32); DrawBorder db(pDC, *b); for (int i=0; iValue; i++) { pDC->LineStyle(db.LineStyle, db.LineReset); pDC->Line(rc.x2-i, rc.y1+Px, rc.x2-i, rc.y2-Px); } } if ((b = &Bottom) && BorderValid(*b)) { pDC->Colour(b->Color.Rgb32, 32); DrawBorder db(pDC, *b); for (int i=0; iValue; i++) { pDC->LineStyle(db.LineStyle, db.LineReset); pDC->Line(rc.x1+Px, rc.y2-i, rc.x2-Px, rc.y2-i); } } } pDC->Op(Op); } } static void FillRectWithImage(LSurface *pDC, LRect *r, LSurface *Image, LCss::RepeatType Repeat) { int Px = 0, Py = 0; int Old = pDC->Op(GDC_ALPHA); if (!Image) return; switch (Repeat) { default: case LCss::RepeatBoth: { for (int y=0; yY(); y += Image->Y()) { for (int x=0; xX(); x += Image->X()) { pDC->Blt(Px + x, Py + y, Image); } } break; } case LCss::RepeatX: { for (int x=0; xX(); x += Image->X()) { pDC->Blt(Px + x, Py, Image); } break; } case LCss::RepeatY: { for (int y=0; yY(); y += Image->Y()) { pDC->Blt(Px, Py + y, Image); } break; } case LCss::RepeatNone: { pDC->Blt(Px, Py, Image); break; } } pDC->Op(Old); } void LTag::OnPaint(LSurface *pDC, bool &InSelection, uint16 Depth) { if (Depth >= MAX_RECURSION_DEPTH || Display() == DispNone) return; if ( #ifdef _DEBUG !Html->_Debug && #endif LCurrentTime() - Html->PaintStart > Html->d->MaxPaintTime) { Html->d->MaxPaintTimeout = true; return; } int Px, Py; pDC->GetOrigin(Px, Py); #if 0 if (Debug) { Gtk::cairo_matrix_t mx; Gtk::cairo_get_matrix(pDC->Handle(), &mx); LPoint Offset; Html->WindowVirtualOffset(&Offset); LRect cli; pDC->GetClient(&cli); printf("\tTag paint mx=%g,%g off=%i,%i p=%i,%i Pos=%i,%i cli=%s\n", mx.x0, mx.y0, Offset.x, Offset.y, Px, Py, Pos.x, Pos.y, cli.GetStr()); } #endif switch (TagId) { case TAG_INPUT: case TAG_SELECT: { if (Ctrl) { int64 Sx = 0, Sy = 0; int64 LineY = GetFont()->GetHeight(); Html->GetScrollPos(Sx, Sy); Sx *= LineY; Sy *= LineY; LRect r(0, 0, Size.x-1, Size.y-1), Px; LColour back = _Colour(false); PaintBorderAndBackground(pDC, back, &Px); if (!dynamic_cast(Ctrl)) { r.x1 += Px.x1; r.y1 += Px.y1; r.x2 -= Px.x2; r.y2 -= Px.y2; } r.Offset(AbsX() - (int)Sx, AbsY() - (int)Sy); Ctrl->SetPos(r); } if (TagId == TAG_SELECT) return; break; } case TAG_BODY: { auto b = _Colour(false); if (!b.IsTransparent()) { pDC->Colour(b); pDC->Rectangle(Pos.x, Pos.y, Pos.x+Size.x, Pos.y+Size.y); } if (Image) { LRect r; r.ZOff(Size.x-1, Size.y-1); FillRectWithImage(pDC, &r, Image, BackgroundRepeat()); } break; } case TAG_HEAD: { // Nothing under here to draw. return; } case TAG_HR: { pDC->Colour(L_MED); pDC->Rectangle(0, 0, Size.x - 1, Size.y - 1); break; } case TAG_TR: case TAG_TBODY: case TAG_META: { // Draws nothing... break; } case TAG_IMG: { LRect Clip(0, 0, Size.x-1, Size.y-1); pDC->ClipRgn(&Clip); if (Image) { #if ENABLE_IMAGE_RESIZING if ( !ImageResized && ( Size.x != Image->X() || Size.y != Image->Y() ) ) { ImageResized = true; LColourSpace Cs = Image->GetColourSpace(); if (Cs == CsIndex8 && Image->AlphaDC()) Cs = System32BitColourSpace; LAutoPtr r(new LMemDC(Size.x, Size.y, Cs)); if (r) { if (Cs == CsIndex8) r->Palette(new LPalette(Image->Palette())); ResampleDC(r, Image); Image = r; } } #endif int Old = pDC->Op(GDC_ALPHA); pDC->Blt(0, 0, Image); pDC->Op(Old); } else if (Size.x > 1 && Size.y > 1) { LRect b(0, 0, Size.x-1, Size.y-1); LColour Fill(LColour(L_MED).Mix(LColour(L_LIGHT), 0.2f)); LColour Border(L_MED); // Border pDC->Colour(Border); pDC->Box(&b); b.Inset(1, 1); pDC->Box(&b); b.Inset(1, 1); pDC->Colour(Fill); pDC->Rectangle(&b); const char *Alt; LColour Red(LColour(255, 0, 0).Mix(Fill, 0.3f)); if (Get("alt", Alt) && ValidStr(Alt)) { LDisplayString Ds(Html->GetFont(), Alt); Html->GetFont()->Colour(Red, Fill); Ds.Draw(pDC, 2, 2, &b); } else if (Size.x >= 16 && Size.y >= 16) { // Red 'x' int Cx = b.x1 + (b.X()/2); int Cy = b.y1 + (b.Y()/2); LRect c(Cx-4, Cy-4, Cx+4, Cy+4); pDC->Colour(Red); pDC->Line(c.x1, c.y1, c.x2, c.y2); pDC->Line(c.x1, c.y2, c.x2, c.y1); pDC->Line(c.x1, c.y1 + 1, c.x2 - 1, c.y2); pDC->Line(c.x1 + 1, c.y1, c.x2, c.y2 - 1); pDC->Line(c.x1 + 1, c.y2, c.x2, c.y1 + 1); pDC->Line(c.x1, c.y2 - 1, c.x2 - 1, c.y1); } } pDC->ClipRgn(0); break; } default: { LColour fore = _Colour(true); LColour back = _Colour(false); if (Display() == DispBlock && Html->Environment) { LCss::ImageDef Img = BackgroundImage(); if (Img.Img) { LRect Clip(0, 0, Size.x-1, Size.y-1); pDC->ClipRgn(&Clip); FillRectWithImage(pDC, &Clip, Img.Img, BackgroundRepeat()); pDC->ClipRgn(NULL); back.Empty(); } } PaintBorderAndBackground(pDC, back, NULL); LFont *f = GetFont(); #if DEBUG_TEXT_AREA bool IsEditor = Html ? !Html->GetReadOnly() : false; #else bool IsEditor = false; #endif if (f && TextPos.Length()) { // This is the non-display part of the font bounding box int LeadingPx = (int)(f->Leading() + 0.5); // This is the displayable part of the font int FontPx = f->GetHeight() - LeadingPx; // This is the pixel height we're aiming to fill int EffectiveLineHt = LineHeightCache >= 0 ? MAX(FontPx, LineHeightCache) : FontPx; // This gets added to the y coord of each piece of text int LineHtOff = ((EffectiveLineHt - FontPx + 1) >> 1) - LeadingPx; #define FontColour(InSelection) \ f->Transparent(!InSelection && !IsEditor); \ if (InSelection) \ f->Colour(L_FOCUS_SEL_FORE, L_FOCUS_SEL_BACK); \ else \ { \ LColour bk(back.IsTransparent() ? LColour(L_WORKSPACE) : back); \ LColour fr(fore.IsTransparent() ? LColour(DefaultTextColour) : fore); \ if (IsEditor) \ bk = bk.Mix(LColour::Black, 0.05f); \ f->Colour(fr, bk); \ } if (Html->HasSelection() && (Selection >= 0 || Cursor >= 0) && Selection != Cursor) { ssize_t Min = -1; ssize_t Max = -1; ssize_t Base = GetTextStart(); if (Cursor >= 0 && Selection >= 0) { Min = MIN(Cursor, Selection) + Base; Max = MAX(Cursor, Selection) + Base; } else if (InSelection) { Max = MAX(Cursor, Selection) + Base; } else { Min = MAX(Cursor, Selection) + Base; } LRect CursorPos; CursorPos.ZOff(-1, -1); for (unsigned i=0; iText - Text(); ssize_t Done = 0; int x = Tr->x1; if (Tr->Len == 0) { // Is this a selection edge point? if (!InSelection && Min == 0) { InSelection = !InSelection; } else if (InSelection && Max == 0) { InSelection = !InSelection; } if (Cursor >= 0) { // Is this the cursor, then draw it and save it's position if (Cursor == Start + Done - Base) { Html->d->CursorPos.Set(x, Tr->y1 + LineHtOff, x + 1, Tr->y2 - LineHtOff); if (Html->d->CursorPos.x1 > Tr->x2) Html->d->CursorPos.Offset(Tr->x2 - Html->d->CursorPos.x1, 0); CursorPos = Html->d->CursorPos; Html->d->CursorPos.Offset(AbsX(), AbsY()); } } break; } while (Done < Tr->Len) { ssize_t c = Tr->Len - Done; FontColour(InSelection); // Is this a selection edge point? if ( !InSelection && Min - Start >= Done && Min - Start < Done + Tr->Len) { InSelection = !InSelection; c = Min - Start - Done; } else if ( InSelection && Max - Start >= Done && Max - Start <= Tr->Len) { InSelection = !InSelection; c = Max - Start - Done; } // Draw the text run LDisplayString ds(f, Tr->Text + Done, c); if (IsEditor) { LRect r(x, Tr->y1, x + ds.X() - 1, Tr->y2); ds.Draw(pDC, x, Tr->y1 + LineHtOff, &r); } else { ds.Draw(pDC, x, Tr->y1 + LineHtOff); } x += ds.X(); Done += c; // Is this is end of the tag? if (Tr->Len == Done) { // Is it also a selection edge? if ( !InSelection && Min - Start == Done) { InSelection = !InSelection; } else if ( InSelection && Max - Start == Done) { InSelection = !InSelection; } } if (Cursor >= 0) { // Is this the cursor, then draw it and save it's position if (Cursor == Start + Done - Base) { Html->d->CursorPos.Set(x, Tr->y1 + LineHtOff, x + 1, Tr->y2 - LineHtOff); if (Html->d->CursorPos.x1 > Tr->x2) Html->d->CursorPos.Offset(Tr->x2 - Html->d->CursorPos.x1, 0); CursorPos = Html->d->CursorPos; Html->d->CursorPos.Offset(AbsX(), AbsY()); } } } } if (Html->d->CursorVis && CursorPos.Valid()) { pDC->Colour(L_TEXT); pDC->Rectangle(&CursorPos); } } else if (Cursor >= 0) { FontColour(InSelection); ssize_t Base = GetTextStart(); for (unsigned i=0; iText - Text()) - Base; LAssert(Tr->y2 >= Tr->y1); LDisplayString ds(f, Tr->Text, Tr->Len); ds.Draw(pDC, Tr->x1, Tr->y1 + LineHtOff, IsEditor ? Tr : NULL); if ( ( Tr->Text == PreText() && !ValidStrW(Text()) ) || ( Cursor >= Pos && Cursor <= Pos + Tr->Len ) ) { ssize_t Off = Tr->Text == PreText() ? StrlenW(PreText()) : Cursor - Pos; pDC->Colour(L_TEXT); LRect c; if (Off) { LDisplayString ds(f, Tr->Text, Off); int x = ds.X(); if (x >= Tr->X()) x = Tr->X()-1; c.Set(Tr->x1 + x, Tr->y1, Tr->x1 + x + 1, Tr->y1 + f->GetHeight()); } else { c.Set(Tr->x1, Tr->y1, Tr->x1 + 1, Tr->y1 + f->GetHeight()); } Html->d->CursorPos = c; if (Html->d->CursorVis) pDC->Rectangle(&c); Html->d->CursorPos.Offset(AbsX(), AbsY()); } } } else { FontColour(InSelection); for (auto &Tr: TextPos) { LDisplayString ds(f, Tr->Text, Tr->Len); ds.Draw(pDC, Tr->x1, Tr->y1 + LineHtOff, IsEditor ? Tr : NULL); } } } break; } } #if DEBUG_TABLE_LAYOUT && 0 if (IsTableCell(TagId)) { LTag *Tbl = this; while (Tbl->TagId != TAG_TABLE && Tbl->Parent) Tbl = Tbl->Parent; if (Tbl && Tbl->TagId == TAG_TABLE && Tbl->Debug) { pDC->Colour(LColour(255, 0, 0)); pDC->Box(0, 0, Size.x-1, Size.y-1); } } #endif for (unsigned i=0; iSetOrigin(Px - t->Pos.x, Py - t->Pos.y); t->OnPaint(pDC, InSelection, Depth + 1); pDC->SetOrigin(Px, Py); } #if DEBUG_DRAW_TD if (TagId == TAG_TD) { LTag *Tbl = this; while (Tbl && Tbl->TagId != TAG_TABLE) Tbl = ToTag(Tbl->Parent); if (Tbl && Tbl->Debug) { int Ls = pDC->LineStyle(LSurface::LineDot); pDC->Colour(LColour::Blue); pDC->Box(0, 0, Size.x-1, Size.y-1); pDC->LineStyle(Ls); } } #endif } ////////////////////////////////////////////////////////////////////// LHtml::LHtml(int id, int x, int y, int cx, int cy, LDocumentEnv *e) : LDocView(e), ResObject(Res_Custom), LHtmlParser(NULL) { View = this; d = new LHtmlPrivate; SetReadOnly(true); ViewWidth = -1; SetId(id); LRect r(x, y, x+cx, y+cy); SetPos(r); Cursor = 0; Selection = 0; DocumentUid = 0; _New(); } LHtml::~LHtml() { _Delete(); DeleteObj(d); if (JobSem.Lock(_FL)) { JobSem.Jobs.DeleteObjects(); JobSem.Unlock(); } } void LHtml::_New() { d->StyleDirty = false; d->IsLoaded = false; d->Content.x = d->Content.y = 0; d->DeferredLoads = 0; Tag = 0; DocCharSet.Reset(); IsHtml = true; #ifdef DefaultFont LFont *Def = new LFont; if (Def) { if (Def->CreateFromCss(DefaultFont)) SetFont(Def, true); else DeleteObj(Def); } #endif FontCache = new LFontCache(this); SetScrollBars(false, false); } void LHtml::_Delete() { LAssert(!d->IsParsing); CssStore.Empty(); CssHref.Empty(); OpenTags.Length(0); Source.Reset(); DeleteObj(Tag); DeleteObj(FontCache); } LFont *LHtml::DefFont() { return GetFont(); } void LHtml::OnAddStyle(const char *MimeType, const char *Styles) { if (Styles) { const char *c = Styles; bool Status = CssStore.Parse(c); if (Status) { d->StyleDirty = true; } #if 0 // def _DEBUG bool LogCss = false; if (!Status) { char p[MAX_PATH_LEN]; sprintf_s(p, sizeof(p), "c:\\temp\\css_parse_failure_%i.txt", LRand()); LFile f; if (f.Open(p, O_WRITE)) { f.SetSize(0); if (CssStore.Error) f.Print("Error: %s\n\n", CssStore.Error.Get()); f.Write(Styles, strlen(Styles)); f.Close(); } } if (LogCss) { LStringPipe p; CssStore.Dump(p); LAutoString a(p.NewStr()); LFile f; if (f.Open("C:\\temp\\css.txt", O_WRITE)) { f.Write(a, strlen(a)); f.Close(); } } #endif } } void LHtml::ParseDocument(const char *Doc) { if (!Tag) { Tag = new LTag(this, 0); } if (GetCss()) GetCss()->DeleteProp(LCss::PropBackgroundColor); if (Tag) { Tag->TagId = ROOT; OpenTags.Length(0); if (IsHtml) { Parse(Tag, Doc); // Add body tag if not specified... LTag *Html = Tag->GetTagByName("html"); LTag *Body = Tag->GetTagByName("body"); if (!Html && !Body) { if ((Html = new LTag(this, 0))) Html->SetTag("html"); if ((Body = new LTag(this, Html))) Body->SetTag("body"); Html->Attach(Body); if (Tag->Text()) { LTag *Content = new LTag(this, Body); if (Content) { Content->TagId = CONTENT; Content->Text(NewStrW(Tag->Text())); } } while (Tag->Children.Length()) { LTag *t = ToTag(Tag->Children.First()); Body->Attach(t, Body->Children.Length()); } DeleteObj(Tag); Tag = Html; } else if (!Body) { if ((Body = new LTag(this, Html))) Body->SetTag("body"); for (unsigned i=0; iChildren.Length(); i++) { LTag *t = ToTag(Html->Children[i]); if (t->TagId != TAG_HEAD) { Body->Attach(t); i--; } } Html->Attach(Body); } if (Html && Body) { char16 *t = Tag->Text(); if (t) { if (ValidStrW(t)) { LTag *Content = new LTag(this, 0); if (Content) { Content->Text(NewStrW(Tag->Text())); Body->Attach(Content, 0); } } Tag->Text(0); } #if 0 // Enabling this breaks the test file 'gw2.html'. for (LTag *t = Html->Tags.First(); t; ) { if (t->Tag && t->Tag[0] == '!') { Tag->Attach(t, 0); t = Html->Tags.Current(); } else if (t->TagId != TAG_HEAD && t != Body) { if (t->TagId == TAG_HTML) { LTag *c; while ((c = t->Tags.First())) { Html->Attach(c, 0); } t->Detach(); DeleteObj(t); } else { t->Detach(); Body->Attach(t); } t = Html->Tags.Current(); } else { t = Html->Tags.Next(); } } #endif if (Environment) { const char *OnLoad; if (Body->Get("onload", OnLoad)) { Environment->OnExecuteScript(this, (char*)OnLoad); } } } } else { Tag->ParseText(Source); } } ViewWidth = -1; if (Tag) Tag->ResetCaches(); Invalidate(); } bool LHtml::NameW(const char16 *s) { LAutoPtr utf(WideToUtf8(s)); return Name(utf); } const char16 *LHtml::NameW() { LBase::Name(Source); return LBase::NameW(); } bool LHtml::Name(const char *s) { int Uid = -1; if (Environment) Uid = Environment->NextUid(); if (Uid < 0) Uid = GetDocumentUid() + 1; SetDocumentUid(Uid); _Delete(); _New(); IsHtml = false; // Detect HTML const char *c = s; while ((c = strchr(c, '<'))) { char *t = 0; c = ParseName((char*) ++c, &t); if (t && GetTagInfo(t)) { DeleteArray(t); IsHtml = true; break; } DeleteArray(t); } // Parse d->IsParsing = true; ParseDocument(s); d->IsParsing = false; if (Tag && d->StyleDirty) { d->StyleDirty = false; Tag->RestyleAll(); } if (d->DeferredLoads == 0) { OnLoad(); } Invalidate(); return true; } const char *LHtml::Name() { if (!Source && Tag) { LStringPipe s(1024); Tag->CreateSource(s); Source.Reset(s.NewStr()); } return Source; } LMessage::Result LHtml::OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_COPY: { Copy(); break; } case M_JOBS_LOADED: { bool Update = false; int InitDeferredLoads = d->DeferredLoads; if (JobSem.Lock(_FL)) { for (unsigned i=0; iUserData); if (j->UserUid == MyUid && j->UserData != NULL) { Html1::LTag *r = static_cast(j->UserData); if (d->DeferredLoads > 0) d->DeferredLoads--; // Check the tag is still in our tree... if (Tag->HasChild(r)) { // Process the returned data... if (r->TagId == TAG_IMG) { if (j->pDC) { r->SetImage(j->Uri, j->pDC.Release()); ViewWidth = 0; Update = true; } else if (j->Stream) { LAutoPtr pDC(GdcD->Load(dynamic_cast(j->Stream.Get()))); if (pDC) { r->SetImage(j->Uri, pDC.Release()); ViewWidth = 0; Update = true; } else LgiTrace("%s:%i - Image decode failed for '%s'\n", _FL, j->Uri.Get()); } else if (j->Status == LDocumentEnv::LoadJob::JobOk) LgiTrace("%s:%i - Unexpected job type for '%s'\n", _FL, j->Uri.Get()); } else if (r->TagId == TAG_LINK) { if (!CssHref.Find(j->Uri)) { LStreamI *s = j->GetStream(); if (s) { s->ChangeThread(); int Size = (int)s->GetSize(); LAutoString Style(new char[Size+1]); ssize_t rd = s->Read(Style, Size); if (rd > 0) { Style[rd] = 0; CssHref.Add(j->Uri, true); OnAddStyle("text/css", Style); ViewWidth = 0; Update = true; } } } } else if (r->TagId == TAG_IFRAME) { // Remote IFRAME loading not support for security reasons. } else LgiTrace("%s:%i - Unexpected tag '%s' for URI '%s'\n", _FL, r->Tag.Get(), j->Uri.Get()); } else { /* Html1::LTag *p = ToTag(r->Parent); while (p && p->Parent) p = ToTag(p->Parent); */ LgiTrace("%s:%i - No child tag for job.\n", _FL); } } // else it's from another (historical) HTML control, ignore } JobSem.Jobs.DeleteObjects(); JobSem.Unlock(); } if (InitDeferredLoads > 0 && d->DeferredLoads <= 0) { LAssert(d->DeferredLoads == 0); d->DeferredLoads = 0; OnLoad(); } if (Update) { OnPosChange(); Invalidate(); } break; } } return LDocView::OnEvent(Msg); } int LHtml::OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_VSCROLL: { int LineY = GetFont()->GetHeight(); if (Tag) Tag->ClearToolTips(); if (n.Type == LNotifyScrollBarCreate && VScroll && LineY > 0) { int y = Y(); int p = MAX(y / LineY, 1); int fy = d->Content.y / LineY; VScroll->SetPage(p); VScroll->SetRange(fy); } Invalidate(); break; } default: { LTag *Ctrl = Tag ? Tag->FindCtrlId(c->GetId()) : NULL; if (Ctrl) return Ctrl->OnNotify(n); break; } } return LLayout::OnNotify(c, n); } void LHtml::OnPosChange() { LLayout::OnPosChange(); if (ViewWidth != X()) { Invalidate(); } } bool LHtml::OnLayout(LViewLayoutInfo &Inf) { if (!Inf.Width.Min) { Inf.Width.Min = Inf.FILL; Inf.Width.Max = Inf.FILL; } else { Inf.Height.Min = Inf.FILL; Inf.Height.Max = Inf.FILL; } return true; } LPoint LHtml::Layout(bool ForceLayout) { LRect Client = GetClient(); if (Tag && (ViewWidth != Client.X() || ForceLayout)) { LFlowRegion f(this, Client, false); // Flow text, width is different Tag->OnFlow(&f, 0); ViewWidth = Client.X(); d->Content.x = f.MAX.x + 1; d->Content.y = f.MAX.y + 1; // Set up scroll box bool Sy = f.y2 > Y(); int LineY = GetFont()->GetHeight(); uint64 Now = LCurrentTime(); if (Now - d->SetScrollTime > 100) { d->SetScrollTime = Now; SetScrollBars(false, Sy); if (Sy && VScroll && LineY > 0) { int y = Y(); int p = MAX(y / LineY, 1); int fy = f.y2 / LineY; VScroll->SetPage(p); VScroll->SetRange(fy); } } else { // LgiTrace("%s - Dropping SetScroll, loop detected: %i ms\n", GetClass(), (int)(Now - d->SetScrollTime)); } } return d->Content; } LPointF LHtml::GetDpiScale() { LPointF Scale(1.0, 1.0); auto Wnd = GetWindow(); if (Wnd) Scale = Wnd->GetDpiScale(); return Scale; } void LHtml::OnPaint(LSurface *ScreenDC) { // LProfile Prof("LHtml::OnPaint"); #if HTML_USE_DOUBLE_BUFFER LRect Client = GetClient(); if (ScreenDC->IsScreen()) { if (!MemDC || (MemDC->X() < Client.X() || MemDC->Y() < Client.Y())) { if (MemDC.Reset(new LMemDC)) { int Sx = Client.X() + 10; int Sy = Client.Y() + 10; if (!MemDC->Create(Sx, Sy, System32BitColourSpace)) { MemDC.Reset(); } } } if (MemDC) { MemDC->ClipRgn(NULL); #if 0//def _DEBUG MemDC->Colour(LColour(255, 0, 255)); MemDC->Rectangle(); #endif } } #endif LSurface *pDC = MemDC ? MemDC : ScreenDC; #if 0 Gtk::cairo_matrix_t mx; Gtk::cairo_get_matrix(pDC->Handle(), &mx); LPoint Offset; WindowVirtualOffset(&Offset); printf("\tHtml paint mx=%g,%g off=%i,%i\n", mx.x0, mx.y0, Offset.x, Offset.y); #endif LColour cBack; if (GetCss()) { LCss::ColorDef Bk = GetCss()->BackgroundColor(); if (Bk.Type == LCss::ColorRgb) cBack = Bk; } if (!cBack.IsValid()) cBack = LColour(Enabled() ? L_WORKSPACE : L_MED); pDC->Colour(cBack); pDC->Rectangle(); if (Tag) { Layout(); if (VScroll) { int LineY = GetFont()->GetHeight(); int Vs = (int)VScroll->Value(); pDC->SetOrigin(0, Vs * LineY); } bool InSelection = false; PaintStart = LCurrentTime(); d->MaxPaintTimeout = false; Tag->OnPaint(pDC, InSelection, 0); if (d->MaxPaintTimeout) { LgiTrace("%s:%i - Html max paint time reached: %i ms.\n", _FL, LCurrentTime() - PaintStart); } } #if HTML_USE_DOUBLE_BUFFER if (MemDC) { pDC->SetOrigin(0, 0); ScreenDC->Blt(0, 0, MemDC); } #endif if (d->OnLoadAnchor && VScroll) { LAutoString a = d->OnLoadAnchor; GotoAnchor(a); LAssert(d->OnLoadAnchor == 0); } } bool LHtml::HasSelection() { if (Cursor && Selection) { return Cursor->Cursor >= 0 && Selection->Selection >= 0 && !(Cursor == Selection && Cursor->Cursor == Selection->Selection); } return false; } void LHtml::UnSelectAll() { bool i = false; if (Cursor) { Cursor->Cursor = -1; Cursor = NULL; i = true; } if (Selection) { Selection->Selection = -1; Selection = NULL; i = true; } if (i) { Invalidate(); } } void LHtml::SelectAll() { } LTag *LHtml::GetLastChild(LTag *t) { if (t && t->Children.Length()) { for (LTag *i = ToTag(t->Children.Last()); i; ) { LTag *c = i->Children.Length() ? ToTag(i->Children.Last()) : NULL; if (c) i = c; else return i; } } return 0; } LTag *LHtml::PrevTag(LTag *t) { // This returns the previous tag in the tree as if all the tags were // listed via recursion using "in order". // Walk up the parent chain looking for a prev for (LTag *p = t; p; p = ToTag(p->Parent)) { // Does this tag have a parent? if (p->Parent) { // Prev? LTag *pp = ToTag(p->Parent); ssize_t Idx = pp->Children.IndexOf(p); LTag *Prev = Idx > 0 ? ToTag(pp->Children[Idx - 1]) : NULL; if (Prev) { LTag *Last = GetLastChild(Prev); return Last ? Last : Prev; } else { return ToTag(p->Parent); } } } return 0; } LTag *LHtml::NextTag(LTag *t) { // This returns the next tag in the tree as if all the tags were // listed via recursion using "in order". // Does this have a child tag? if (t->Children.Length() > 0) { return ToTag(t->Children.First()); } else { // Walk up the parent chain for (LTag *p = t; p; p = ToTag(p->Parent)) { // Does this tag have a next? if (p->Parent) { LTag *pp = ToTag(p->Parent); size_t Idx = pp->Children.IndexOf(p); LTag *Next = pp->Children.Length() > Idx + 1 ? ToTag(pp->Children[Idx + 1]) : NULL; if (Next) { return Next; } } } } return 0; } int LHtml::GetTagDepth(LTag *Tag) { // Returns the depth of the tag in the tree. int n = 0; for (LTag *t = Tag; t; t = ToTag(t->Parent)) { n++; } return n; } bool LHtml::IsCursorFirst() { if (!Cursor || !Selection) return false; return CompareTagPos(Cursor, Cursor->Cursor, Selection, Selection->Selection); } bool LHtml::CompareTagPos(LTag *a, ssize_t AIdx, LTag *b, ssize_t BIdx) { // Returns true if the 'a' is before 'b' point. if (!a || !b) return false; if (a == b) { return AIdx < BIdx; } else { LArray ATree, BTree; for (LTag *t = a; t; t = ToTag(t->Parent)) ATree.AddAt(0, t); for (LTag *t = b; t; t = ToTag(t->Parent)) BTree.AddAt(0, t); ssize_t Depth = MIN(ATree.Length(), BTree.Length()); for (int i=0; i 0); LTag *p = ATree[i-1]; LAssert(BTree[i-1] == p); ssize_t ai = p->Children.IndexOf(at); ssize_t bi = p->Children.IndexOf(bt); return ai < bi; } } } return false; } void LHtml::SetLoadImages(bool i) { if (i ^ GetLoadImages()) { LDocView::SetLoadImages(i); SendNotify(LNotifyShowImagesChanged); if (GetLoadImages() && Tag) { Tag->LoadImages(); } } } char *LHtml::GetSelection() { char *s = 0; if (Cursor && Selection) { LMemQueue p; bool InSelection = false; Tag->CopyClipboard(p, InSelection); int Len = (int)p.GetSize(); if (Len > 0) { char16 *t = (char16*)p.New(sizeof(char16)); if (t) { size_t Len = StrlenW(t); for (int i=0; iOnFind(Dlg); } */ void BuildTagList(LArray &t, LTag *Tag) { t.Add(Tag); for (unsigned i=0; iChildren.Length(); i++) { LTag *c = ToTag(Tag->Children[i]); BuildTagList(t, c); } } static void FormEncode(LStringPipe &p, const char *c) { const char *s = c; while (*c) { while (*c && *c != ' ') c++; if (c > s) { p.Write(s, c - s); c = s; } if (*c == ' ') { p.Write("+", 1); s = c; } else break; } } bool LHtml::OnSubmitForm(LTag *Form) { if (!Form || !Environment) { LAssert(!"Bad param"); return false; } const char *Method = NULL; const char *Action = NULL; if (!Form->Get("method", Method) || !Form->Get("action", Action)) { LAssert(!"Missing form action/method"); return false; } LHashTbl,char*> f; Form->CollectFormValues(f); bool Status = false; if (!_stricmp(Method, "post")) { LStringPipe p(256); bool First = true; // const char *Field; // for (char *Val = f.First(&Field); Val; Val = f.Next(&Field)) for (auto v : f) { if (First) First = false; else p.Write("&", 1); FormEncode(p, v.key); p.Write("=", 1); FormEncode(p, v.value); } LAutoPtr Data(p.NewStr()); Status = Environment->OnPostForm(this, Action, Data); } else if (!_stricmp(Method, "get")) { Status = Environment->OnNavigate(this, Action); } else { LAssert(!"Bad form method."); } f.DeleteArrays(); return Status; } bool LHtml::OnFind(LFindReplaceCommon *Params) { bool Status = false; if (Params) { if (!Params->Find) return Status; d->FindText.Reset(Utf8ToWide(Params->Find)); d->MatchCase = Params->MatchCase; } if (!Cursor) Cursor = Tag; if (Cursor && d->FindText) { LArray Tags; BuildTagList(Tags, Tag); ssize_t Start = Tags.IndexOf(Cursor); for (unsigned i=1; iText()) { char16 *Hit; if (d->MatchCase) Hit = StrstrW(s->Text(), d->FindText); else Hit = StristrW(s->Text(), d->FindText); if (Hit) { // found something... UnSelectAll(); Selection = Cursor = s; Cursor->Cursor = Hit - s->Text(); Selection->Selection = Cursor->Cursor + StrlenW(d->FindText); OnCursorChanged(); if (VScroll) { // Scroll the tag into view... int y = s->AbsY(); int LineY = GetFont()->GetHeight(); int Val = y / LineY; SetVScroll(Val); } Invalidate(); Status = true; break; } } } } return Status; } void LHtml::DoFind(std::function Callback) { LFindDlg *Dlg = new LFindDlg(this, [this](auto dlg, auto action) { OnFind(dlg); delete dlg; }); Dlg->DoModal(NULL); } bool LHtml::OnKey(LKey &k) { bool Status = false; if (k.Down()) { int Dy = 0; int LineY = GetFont()->GetHeight(); int Page = GetClient().Y() / LineY; switch (k.vkey) { case LK_F3: { OnFind(NULL); break; } #ifdef WIN32 case LK_INSERT: goto DoCopy; #endif case LK_UP: { Dy = -1; Status = true; break; } case LK_DOWN: { Dy = 1; Status = true; break; } case LK_PAGEUP: { Dy = -Page; Status = true; break; } case LK_PAGEDOWN: { Dy = Page; Status = true; break; } case LK_HOME: { Dy = (int) (VScroll ? -VScroll->Value() : 0); Status = true; break; } case LK_END: { if (VScroll) { LRange r = VScroll->GetRange(); Dy = (int)(r.End() - Page); } Status = true; break; } default: { switch (k.c16) { case 'f': case 'F': { if (k.CtrlCmd()) { DoFind(NULL); Status = true; } break; } case 'c': case 'C': { #ifdef WIN32 DoCopy: #endif if (k.CtrlCmd()) { Copy(); Status = true; } break; } } break; } } if (Dy && VScroll) SetVScroll(VScroll->Value() + Dy); } return Status; } int LHtml::ScrollY() { return GetFont()->GetHeight() * (VScroll ? (int)VScroll->Value() : 0); } void LHtml::OnMouseClick(LMouse &m) { Capture(m.Down()); SetPulse(m.Down() ? 200 : -1); if (m.Down()) { Focus(true); int Offset = ScrollY(); bool TagProcessedClick = false; LTagHit Hit; if (Tag) { Tag->GetTagByPos(Hit, m.x, m.y + Offset, 0, false, DEBUG_TAG_BY_POS); #if DEBUG_TAG_BY_POS Hit.Dump("MouseClick"); #endif } if (m.Left() && !m.IsContextMenu()) { if (m.Double()) { d->WordSelectMode = true; if (Cursor) { // Extend the selection out to the current word's boundaries. Selection = Cursor; Selection->Selection = Cursor->Cursor; if (Cursor->Text()) { ssize_t Base = Cursor->GetTextStart(); char16 *Text = Cursor->Text() + Base; while (Text[Cursor->Cursor]) { char16 c = Text[Cursor->Cursor]; if (strchr(WordDelim, c) || StrchrW(WhiteW, c)) break; Cursor->Cursor++; } } if (Selection->Text()) { ssize_t Base = Selection->GetTextStart(); char16 *Sel = Selection->Text() + Base; while (Selection->Selection > 0) { char16 c = Sel[Selection->Selection - 1]; if (strchr(WordDelim, c) || StrchrW(WhiteW, c)) break; Selection->Selection--; } } Invalidate(); SendNotify(LNotifySelectionChanged); } } else if (Hit.NearestText) { d->WordSelectMode = false; UnSelectAll(); Cursor = Hit.NearestText; Cursor->Cursor = Hit.Index; #if DEBUG_SELECTION LgiTrace("StartSelect Near='%20S' Idx=%i\n", Hit.NearestText->Text(), Hit.Index); #endif OnCursorChanged(); SendNotify(LNotifySelectionChanged); } else { #if DEBUG_SELECTION LgiTrace("StartSelect no text hit %p, %p\n", Cursor, Selection); #endif } } if (Hit.NearestText && Hit.Near == 0) { TagProcessedClick = Hit.NearestText->OnMouseClick(m); } else if (Hit.Direct) { TagProcessedClick = Hit.Direct->OnMouseClick(m); } #ifdef _DEBUG else if (m.Left() && m.Ctrl()) { LgiMsg(this, "No tag under the cursor.", GetClass()); } #endif if (!TagProcessedClick && m.IsContextMenu()) { LSubMenu RClick; enum ContextMenuCmds { IDM_DUMP = 100, IDM_COPY_SRC, IDM_VIEW_SRC, IDM_EXTERNAL, IDM_COPY, IDM_VIEW_IMAGES, }; #define IDM_CHARSET_BASE 10000 RClick.AppendItem (LLoadString(L_TEXTCTRL_COPY, "Copy"), IDM_COPY, HasSelection()); LMenuItem *Vs = RClick.AppendItem (LLoadString(L_VIEW_SOURCE, "View Source"), IDM_VIEW_SRC, Source != 0); RClick.AppendItem (LLoadString(L_COPY_SOURCE, "Copy Source"), IDM_COPY_SRC, Source != 0); LMenuItem *Load = RClick.AppendItem (LLoadString(L_VIEW_IMAGES, "View External Images"), IDM_VIEW_IMAGES, true); if (Load) Load->Checked(GetLoadImages()); RClick.AppendItem (LLoadString(L_VIEW_IN_DEFAULT_BROWSER, "View in Default Browser"), IDM_EXTERNAL, Source != 0); LSubMenu *Cs = RClick.AppendSub (LLoadString(L_CHANGE_CHARSET, "Change Charset")); if (Cs) { int n=0; for (LCharset *c = LGetCsList(); c->Charset; c++, n++) { Cs->AppendItem(c->Charset, IDM_CHARSET_BASE + n, c->IsAvailable()); } } if (!GetReadOnly() || // Is editor #ifdef _DEBUG 1 #else 0 #endif ) { RClick.AppendSeparator(); RClick.AppendItem("Dump Layout", IDM_DUMP, Tag != 0); } if (Vs) { Vs->Checked(!IsHtml); } if (OnContextMenuCreate(Hit, RClick) && GetMouse(m, true)) { int Id = RClick.Float(this, m.x, m.y); switch (Id) { case IDM_COPY: { Copy(); break; } case IDM_VIEW_SRC: { if (Vs) { DeleteObj(Tag); IsHtml = !IsHtml; ParseDocument(Source); } break; } case IDM_COPY_SRC: { if (Source) { LClipBoard c(this); const char *ViewCs = GetCharset(); if (ViewCs) { LAutoWString w((char16*)LNewConvertCp(LGI_WideCharset, Source, ViewCs)); if (w) c.TextW(w); } else c.Text(Source); } break; } case IDM_VIEW_IMAGES: { SetLoadImages(!GetLoadImages()); break; } case IDM_DUMP: { if (Tag) { LAutoWString s = Tag->DumpW(); if (s) { LClipBoard c(this); c.TextW(s); } } break; } case IDM_EXTERNAL: { if (!Source) { LgiTrace("%s:%i - No HTML source code.\n", _FL); break; } char Path[MAX_PATH_LEN]; if (!LGetSystemPath(LSP_TEMP, Path, sizeof(Path))) { LgiTrace("%s:%i - Failed to get the system path.\n", _FL); break; } char f[32]; sprintf_s(f, sizeof(f), "_%i.html", LRand(1000000)); LMakePath(Path, sizeof(Path), Path, f); LFile F; if (!F.Open(Path, O_WRITE)) { LgiTrace("%s:%i - Failed to open '%s' for writing.\n", _FL, Path); break; } LStringPipe Ex; bool Error = false; F.SetSize(0); LAutoWString SrcMem; const char *ViewCs = GetCharset(); if (ViewCs) SrcMem.Reset((char16*)LNewConvertCp(LGI_WideCharset, Source, ViewCs)); else SrcMem.Reset(Utf8ToWide(Source)); for (char16 *s=SrcMem; s && *s;) { char16 *cid = StristrW(s, L"cid:"); while (cid && !strchr("\'\"", cid[-1])) { cid = StristrW(cid+1, L"cid:"); } if (cid) { char16 Delim = cid[-1]; char16 *e = StrchrW(cid, Delim); if (e) { *e = 0; if (StrchrW(cid, '\n')) { *e = Delim; Error = true; break; } else { char File[MAX_PATH_LEN] = ""; if (Environment) { LDocumentEnv::LoadJob *j = Environment->NewJob(); if (j) { j->Uri.Reset(WideToUtf8(cid)); j->Env = Environment; j->Pref = LDocumentEnv::LoadJob::FmtFilename; j->UserUid = GetDocumentUid(); LDocumentEnv::LoadType Result = Environment->GetContent(j); if (Result == LDocumentEnv::LoadImmediate) { if (j->Filename) strcpy_s(File, sizeof(File), j->Filename); } else if (Result == LDocumentEnv::LoadDeferred) { d->DeferredLoads++; } DeleteObj(j); } } *e = Delim; Ex.Push(s, cid - s); if (File[0]) { char *d; while ((d = strchr(File, '\\'))) { *d = '/'; } Ex.Push(L"file:///"); LAutoWString w(Utf8ToWide(File)); Ex.Push(w); } s = e; } } else { Error = true; break; } } else { Ex.Push(s); break; } } if (!Error) { int64 WideChars = Ex.GetSize() / sizeof(char16); LAutoWString w(Ex.NewStrW()); LAutoString u(WideToUtf8(w, WideChars)); if (u) F.Write(u, strlen(u)); F.Close(); LString Err; if (!LExecute(Path, NULL, NULL, &Err)) { LgiMsg( this, "Failed to open '%s'\n%s", LAppInst ? LAppInst->LBase::Name() : GetClass(), MB_OK, Path, Err.Get()); } } break; } default: { if (Id >= IDM_CHARSET_BASE) { LCharset *c = LGetCsList() + (Id - IDM_CHARSET_BASE); if (c->Charset) { Charset = c->Charset; OverideDocCharset = true; char *Src = Source.Release(); _Delete(); _New(); Source.Reset(Src); ParseDocument(Source); Invalidate(); SendNotify(LNotifyCharsetChanged); } } else { OnContextMenuCommand(Hit, Id); } break; } } } } } else // Up Click { if (Selection && Cursor && Selection == Cursor && Selection->Selection == Cursor->Cursor) { Selection->Selection = -1; Selection = 0; SendNotify(LNotifySelectionChanged); #if DEBUG_SELECTION LgiTrace("NoSelect on release\n"); #endif } } } void LHtml::OnLoad() { d->IsLoaded = true; SendNotify(LNotifyDocLoaded); } LTag *LHtml::GetTagByPos(int x, int y, ssize_t *Index, LPoint *LocalCoords, bool DebugLog) { LTag *Status = NULL; if (Tag) { if (DebugLog) LgiTrace("GetTagByPos starting...\n"); LTagHit Hit; Tag->GetTagByPos(Hit, x, y, 0, DebugLog); if (DebugLog) LgiTrace("GetTagByPos Hit=%s, %i, %i...\n\n", Hit.Direct ? Hit.Direct->Tag.Get() : 0, Hit.Index, Hit.Near); Status = Hit.NearestText && Hit.Near == 0 ? Hit.NearestText : Hit.Direct; if (Hit.NearestText && Hit.Near < 30) { if (Index) *Index = Hit.Index; if (LocalCoords) *LocalCoords = Hit.LocalCoords; } } return Status; } void LHtml::SetVScroll(int64 v) { if (!VScroll) return; if (Tag) Tag->ClearToolTips(); VScroll->Value(v); Invalidate(); } bool LHtml::OnMouseWheel(double Lines) { if (VScroll) SetVScroll(VScroll->Value() + (int64)Lines); return true; } LCursor LHtml::GetCursor(int x, int y) { int Offset = ScrollY(); ssize_t Index = -1; LPoint LocalCoords; LTag *Tag = GetTagByPos(x, y + Offset, &Index, &LocalCoords); if (Tag) { LString Uri; if (LocalCoords.x >= 0 && LocalCoords.y >= 0 && Tag->IsAnchor(&Uri)) { LRect c = GetClient(); c.Offset(-c.x1, -c.y1); if (c.Overlap(x, y) && ValidStr(Uri)) { return LCUR_PointingHand; } } } return LCUR_Normal; } void LTag::ClearToolTips() { if (TipId) { Html->Tip.DeleteTip(TipId); TipId = 0; } for (auto c: Children) ToTag(c)->ClearToolTips(); } void LHtml::OnMouseMove(LMouse &m) { if (!Tag) return; int Offset = ScrollY(); LTagHit Hit; Tag->GetTagByPos(Hit, m.x, m.y + Offset, 0, false); if (!Hit.Direct && !Hit.NearestText) return; LString Uri; LTag *HitTag = Hit.NearestText && Hit.Near == 0 ? Hit.NearestText : Hit.Direct; if (HitTag && HitTag->TipId == 0 && Hit.LocalCoords.x >= 0 && Hit.LocalCoords.y >= 0 && HitTag->IsAnchor(&Uri) && Uri) { if (!Tip.GetParent()) { Tip.Attach(this); } LRect r = HitTag->GetRect(false); r.Offset(0, -Offset); if (!HitTag->TipId) HitTag->TipId = Tip.NewTip(Uri, r); // LgiTrace("NewTip: %s @ %s, ID=%i\n", Uri.Get(), r.GetStr(), HitTag->TipId); } if (IsCapturing() && Cursor && Hit.NearestText) { if (!Selection) { Selection = Cursor; Selection->Selection = Cursor->Cursor; Cursor = Hit.NearestText; Cursor->Cursor = Hit.Index; OnCursorChanged(); Invalidate(); SendNotify(LNotifySelectionChanged); #if DEBUG_SELECTION LgiTrace("CreateSelection '%20S' %i\n", Hit.NearestText->Text(), Hit.Index); #endif } else if ((Cursor != Hit.NearestText) || (Cursor->Cursor != Hit.Index)) { // Move the cursor to track the mouse if (Cursor) { Cursor->Cursor = -1; } Cursor = Hit.NearestText; Cursor->Cursor = Hit.Index; #if DEBUG_SELECTION LgiTrace("ExtendSelection '%20S' %i\n", Hit.NearestText->Text(), Hit.Index); #endif if (d->WordSelectMode && Cursor->Text()) { ssize_t Base = Cursor->GetTextStart(); if (IsCursorFirst()) { // Extend the cursor up the document to include the whole word while (Cursor->Cursor > 0) { char16 c = Cursor->Text()[Base + Cursor->Cursor - 1]; if (strchr(WordDelim, c) || StrchrW(WhiteW, c)) break; Cursor->Cursor--; } } else { // Extend the cursor down the document to include the whole word while (Cursor->Text()[Base + Cursor->Cursor]) { char16 c = Cursor->Text()[Base + Cursor->Cursor]; if (strchr(WordDelim, c) || StrchrW(WhiteW, c)) break; Cursor->Cursor++; } } } OnCursorChanged(); Invalidate(); SendNotify(LNotifySelectionChanged); } } } void LHtml::OnPulse() { if (VScroll && IsCapturing()) { int Fy = DefFont() ? DefFont()->GetHeight() : 16; LMouse m; if (GetMouse(m, false)) { LRect c = GetClient(); int Lines = 0; if (m.y < c.y1) { // Scroll up Lines = (c.y1 - m.y + Fy - 1) / -Fy; } else if (m.y > c.y2) { // Scroll down Lines = (m.y - c.y2 + Fy - 1) / Fy; } if (Lines && VScroll) SetVScroll(VScroll->Value() + Lines); } } } LRect *LHtml::GetCursorPos() { return &d->CursorPos; } void LHtml::SetCursorVis(bool b) { if (d->CursorVis ^ b) { d->CursorVis = b; Invalidate(); } } bool LHtml::GetCursorVis() { return d->CursorVis; } LDom *ElementById(LTag *t, char *id) { if (t && id) { const char *i; if (t->Get("id", i) && _stricmp(i, id) == 0) return t; for (unsigned i=0; iChildren.Length(); i++) { LTag *c = ToTag(t->Children[i]); LDom *n = ElementById(c, id); if (n) return n; } } return 0; } LDom *LHtml::getElementById(char *Id) { return ElementById(Tag, Id); } bool LHtml::GetLinkDoubleClick() { return d->LinkDoubleClick; } void LHtml::SetLinkDoubleClick(bool b) { d->LinkDoubleClick = b; } bool LHtml::GetFormattedContent(const char *MimeType, LString &Out, LArray *Media) { if (!MimeType) { LAssert(!"No MIME type for getting formatted content"); return false; } if (!_stricmp(MimeType, "text/html")) { // We can handle this type... LArray Imgs; if (Media) { // Find all the image tags... Tag->Find(TAG_IMG, Imgs); // Give them CID's if they don't already have them for (unsigned i=0; iGet("src", Src) && !Img->Get("cid", Cid)) { char id[256]; sprintf_s(id, sizeof(id), "%x.%x", (unsigned)LCurrentTime(), (unsigned)LRand()); Img->Set("cid", id); Img->Get("cid", Cid); } if (Src && Cid) { LFile *f = new LFile; if (f) { if (f->Open(Src, O_READ)) { // Add the exported image stream to the media array LDocView::ContentMedia &m = Media->New(); m.Id = Cid; m.Stream.Reset(f); } } } } } // Export the HTML, including the CID's from the first step Out = Name(); } else if (!_stricmp(MimeType, "text/plain")) { // Convert DOM tree down to text instead... LStringPipe p(512); if (Tag) { LTag::TextConvertState State(&p); Tag->ConvertToText(State); } - Out = p.NewGStr(); + Out = p.NewLStr(); } return false; } void LHtml::OnContent(LDocumentEnv::LoadJob *Res) { if (JobSem.Lock(_FL)) { JobSem.Jobs.Add(Res); JobSem.Unlock(); PostEvent(M_JOBS_LOADED); } } LHtmlElement *LHtml::CreateElement(LHtmlElement *Parent) { return new LTag(this, Parent); } bool LHtml::GetVariant(const char *Name, LVariant &Value, const char *Array) { if (!_stricmp(Name, "supportLists")) // Type: Bool Value = false; else if (!_stricmp(Name, "vml")) // Type: Bool // Vector Markup Language Value = false; else if (!_stricmp(Name, "mso")) // Type: Bool // mso = Microsoft Office Value = false; else return false; return true; } bool LHtml::EvaluateCondition(const char *Cond) { if (!Cond) return true; // This is a really bad attempt at writing an expression evaluator. // I could of course use the scripting language but that would pull // in a fairly large dependency on the HTML control. However user // apps that already have that could reimplement this virtual function // if they feel like it. LArray Str; for (const char *c = Cond; *c; ) { if (IsAlpha(*c)) { Str.Add(LTokStr(c)); } else if (IsWhiteSpace(*c)) { c++; } else { const char *e = c; while (*e && !IsWhiteSpace(*e) && !IsAlpha(*e)) e++; Str.Add(NewStr(c, e - c)); LAssert(e > c); if (e > c) c = e; else break; } } bool Result = true; bool Not = false; for (unsigned i=0; iGetAnchor(Name); if (a) { if (VScroll) { int LineY = GetFont()->GetHeight(); int Ay = a->AbsY(); int Scr = Ay / LineY; SetVScroll(Scr); VScroll->SendNotify(); } else d->OnLoadAnchor.Reset(NewStr(Name)); } } return false; } bool LHtml::GetEmoji() { return d->DecodeEmoji; } void LHtml::SetEmoji(bool i) { d->DecodeEmoji = i; } void LHtml::SetMaxPaintTime(int Ms) { d->MaxPaintTime = Ms; } bool LHtml::GetMaxPaintTimeout() { return d->MaxPaintTimeout; } //////////////////////////////////////////////////////////////////////// class LHtml_Factory : public LViewFactory { LView *NewView(const char *Class, LRect *Pos, const char *Text) { if (_stricmp(Class, "LHtml") == 0) { return new LHtml(-1, 0, 0, 100, 100, new LDefaultDocumentEnv); } return 0; } } LHtml_Factory; ////////////////////////////////////////////////////////////////////// struct BuildContext { LHtmlTableLayout *Layout; LTag *Table; LTag *TBody; LTag *CurTr; LTag *CurTd; int cx, cy; BuildContext() { Layout = NULL; cx = cy = 0; Table = NULL; TBody = NULL; CurTr = NULL; CurTd = NULL; } bool Build(LTag *t, int Depth) { bool RetReattach = false; switch (t->TagId) { case TAG_TABLE: { if (!Table) Table = t; else return false; break; } case TAG_TBODY: { if (TBody) return false; TBody = t; break; } case TAG_TR: { CurTr = t; break; } case TAG_TD: { CurTd = t; if (t->Parent != CurTr) { if ( !CurTr && (Table || TBody) ) { LTag *p = TBody ? TBody : Table; CurTr = new LTag(p->Html, p); if (CurTr) { CurTr->Tag.Reset(NewStr("tr")); CurTr->TagId = TAG_TR; ssize_t Idx = t->Parent->Children.IndexOf(t); t->Parent->Attach(CurTr, Idx); } } if (CurTr) { CurTr->Attach(t); RetReattach = true; } else { LAssert(0); return false; } } t->Cell->Pos.x = cx; t->Cell->Pos.y = cy; Layout->Set(t); break; } default: { if (CurTd == t->Parent) return false; break; } } for (unsigned n=0; nChildren.Length(); n++) { LTag *c = ToTag(t->Children[n]); bool Reattached = Build(c, Depth+1); if (Reattached) n--; } if (t->TagId == TAG_TR) { CurTr = NULL; cy++; cx = 0; Layout->s.y = cy; } if (t->TagId == TAG_TD) { CurTd = NULL; cx += t->Cell->Span.x; Layout->s.x = MAX(cx, Layout->s.x); } return RetReattach; } }; LHtmlTableLayout::LHtmlTableLayout(LTag *table) { Table = table; if (!Table) return; #if 0 BuildContext Ctx; Ctx.Layout = this; Ctx.Build(table, 0); #else int y = 0; LTag *FakeRow = 0; LTag *FakeCell = 0; LTag *r; for (size_t i=0; iChildren.Length(); i++) { r = ToTag(Table->Children[i]); if (r->Display() == LCss::DispNone) continue; if (r->TagId == TAG_TR) { FakeRow = 0; FakeCell = 0; } else if (r->TagId == TAG_TBODY) { ssize_t Index = Table->Children.IndexOf(r); for (size_t n=0; nChildren.Length(); n++) { LTag *t = ToTag(r->Children[n]); Table->Children.AddAt(++Index, t); t->Parent = Table; /* LgiTrace("Moving '%s'(%p) from TBODY(%p) into '%s'(%p)\n", t->Tag, t, r, t->Parent->Tag, t->Parent); */ } r->Children.Length(0); } else { if (!FakeRow) { if ((FakeRow = new LTag(Table->Html, 0))) { FakeRow->Tag.Reset(NewStr("tr")); FakeRow->TagId = TAG_TR; ssize_t Idx = Table->Children.IndexOf(r); Table->Attach(FakeRow, Idx); } } if (FakeRow) { if (!IsTableCell(r->TagId) && !FakeCell) { if ((FakeCell = new LTag(Table->Html, FakeRow))) { FakeCell->Tag.Reset(NewStr("td")); FakeCell->TagId = TAG_TD; if ((FakeCell->Cell = new LTag::TblCell)) { FakeCell->Cell->Span.x = 1; FakeCell->Cell->Span.y = 1; } } } ssize_t Idx = Table->Children.IndexOf(r); r->Detach(); if (IsTableCell(r->TagId)) { FakeRow->Attach(r); } else { LAssert(FakeCell != NULL); FakeCell->Attach(r); } i = Idx - 1; } } } FakeCell = NULL; for (size_t n=0; nChildren.Length(); n++) { LTag *r = ToTag(Table->Children[n]); if (r->TagId == TAG_TR) { int x = 0; for (size_t i=0; iChildren.Length(); i++) { LTag *cell = ToTag(r->Children[i]); if (!IsTableCell(cell->TagId)) { if (!FakeCell) { // Make a fake TD cell FakeCell = new LTag(Table->Html, NULL); FakeCell->Tag.Reset(NewStr("td")); FakeCell->TagId = TAG_TD; if ((FakeCell->Cell = new LTag::TblCell)) { FakeCell->Cell->Span.x = 1; FakeCell->Cell->Span.y = 1; } // Join the fake TD into the TR r->Children[i] = FakeCell; FakeCell->Parent = r; } else { // Not the first non-TD tag, so delete it from the TR. Only the // fake TD will remain in the TR. r->Children.DeleteAt(i--, true); } // Insert the tag into it as a child FakeCell->Children.Add(cell); cell->Parent = FakeCell; cell = FakeCell; } else { FakeCell = NULL; } if (IsTableCell(cell->TagId)) { if (cell->Display() == LCss::DispNone) continue; while (Get(x, y)) { x++; } cell->Cell->Pos.x = x; cell->Cell->Pos.y = y; Set(cell); x += cell->Cell->Span.x; } } y++; FakeCell = NULL; } } #endif } void LHtmlTableLayout::Dump() { int Sx, Sy; GetSize(Sx, Sy); LgiTrace("Table %i x %i cells.\n", Sx, Sy); for (int x=0; xCell->Pos.x, t->Cell->Pos.y, t->Cell->Span.x, t->Cell->Span.y); LgiTrace("%-10s", s); } LgiTrace("\n"); } LgiTrace("\n"); } void LHtmlTableLayout::GetAll(List &All) { LHashTbl, bool> Added; for (size_t y=0; y= (int) c.Length()) return NULL; CellArray &a = c[y]; if (x >= (int) a.Length()) return NULL; return a[x]; } bool LHtmlTableLayout::Set(LTag *t) { if (!t) return false; for (int y=0; yCell->Span.y; y++) { for (int x=0; xCell->Span.x; x++) { // LAssert(!c[y][x]); c[t->Cell->Pos.y + y][t->Cell->Pos.x + x] = t; } } return true; } void LTagHit::Dump(const char *Desc) { LArray d, n; LTag *t = Direct; unsigned i; for (i=0; i<3 && t; t = ToTag(t->Parent), i++) { d.AddAt(0, t); } t = NearestText; for (i=0; i<3 && t; t = ToTag(t->Parent), i++) { n.AddAt(0, t); } LgiTrace("Hit: %s Direct: ", Desc); for (i=0; i%s", d[i]->Tag ? d[i]->Tag.Get() : "CONTENT"); LgiTrace(" Nearest: "); for (i=0; i%s", n[i]->Tag ? n[i]->Tag.Get() : "CONTENT"); LgiTrace(" Local: %ix%i Index: %i Block: %s '%.10S'\n", LocalCoords.x, LocalCoords.y, Index, Block ? Block->GetStr() : NULL, Block ? Block->Text + Index : NULL); } diff --git a/src/common/Text/TextView3.cpp b/src/common/Text/TextView3.cpp --- a/src/common/Text/TextView3.cpp +++ b/src/common/Text/TextView3.cpp @@ -1,5439 +1,5439 @@ #include #include #include #include #include "lgi/common/Lgi.h" #include "lgi/common/TextView3.h" #include "lgi/common/Input.h" #include "lgi/common/ScrollBar.h" #ifdef WIN32 #include #endif #include "lgi/common/ClipBoard.h" #include "lgi/common/DisplayString.h" #include "lgi/common/CssTools.h" #include "lgi/common/LgiRes.h" #include "lgi/common/Mail.h" #include "lgi/common/FileSelect.h" #include "lgi/common/Menu.h" #include "lgi/common/DropFiles.h" #include "ViewPriv.h" #undef max #ifdef _DEBUG #define FEATURE_HILIGHT_ALL_MATCHES 1 #else #define FEATURE_HILIGHT_ALL_MATCHES 0 #endif #define DefaultCharset "utf-8" #define SubtractPtr(a, b) ((a) - (b)) #define GDCF_UTF8 -1 #define POUR_DEBUG 0 #define PROFILE_POUR 0 #define PROFILE_PAINT 0 #define DRAW_LINE_BOXES 0 #define WRAP_POUR_TIMEOUT 90 // ms #define PULSE_TIMEOUT 500 // ms #define CURSOR_BLINK 1000 // ms #define ALLOC_BLOCK 64 #define IDC_VS 1000 #ifdef WINDOWS #define DOUBLE_BUFFER_PAINT 1 #endif enum Cmds { IDM_COPY_URL = 100, IDM_AUTO_INDENT, IDM_UTF8, IDM_PASTE_NO_CONVERT, IDM_FIXED, IDM_SHOW_WHITE, IDM_HARD_TABS, IDM_INDENT_SIZE, IDM_TAB_SIZE, IDM_DUMP, IDM_RTL, IDM_COPY_ALL, IDM_SELECT_ALL, #ifndef IDM_OPEN IDM_OPEN, #endif #ifndef IDM_NEW IDM_NEW, #endif #ifndef IDM_COPY IDM_COPY, #endif #ifndef IDM_CUT IDM_CUT, #endif #ifndef IDM_PASTE IDM_PASTE, #endif #ifndef IDM_UNDO IDM_UNDO, #endif #ifndef IDM_REDO IDM_REDO, #endif }; #define PAINT_BORDER Back #if DRAW_LINE_BOXES #define PAINT_AFTER_LINE LColour(240, 240, 240) #else #define PAINT_AFTER_LINE Back #endif #define CODEPAGE_BASE 100 #define CONVERT_CODEPAGE_BASE 200 #if !defined(WIN32) && !defined(toupper) #define toupper(c) (((c)>='a'&&(c)<='z') ? (c)-'a'+'A' : (c)) #endif #define THREAD_CHECK() \ if (!InThread()) \ { \ LgiTrace("%s:%i - %s called out of thread.\n", _FL, __FUNCTION__); \ return false; \ } static char SelectWordDelim[] = " \t\n.,()[]<>=?/\\{}\"\';:+=-|!@#$%^&*"; #ifndef WINDOWS static LArray Ctrls; #endif ////////////////////////////////////////////////////////////////////// class LDocFindReplaceParams3 : public LDocFindReplaceParams, public LMutex { public: // Find/Replace History LAutoWString LastFind; LAutoWString LastReplace; bool MatchCase; bool MatchWord; bool SelectionOnly; bool SearchUpwards; LDocFindReplaceParams3() : LMutex("LDocFindReplaceParams3") { MatchCase = false; MatchWord = false; SelectionOnly = false; SearchUpwards = false; } }; class LTextView3Private : public LCss, public LMutex { public: LTextView3 *View; LRect rPadding; int PourX; bool LayoutDirty; ssize_t DirtyStart, DirtyLen; LColour UrlColour; bool CenterCursor; ssize_t WordSelectMode; LString Eol; LString LastError; // If the scroll position is set before we get a scroll bar, store the index // here and set it when the LNotifyScrollBarCreate arrives. ssize_t VScrollCache; // Find/Replace Params bool OwnFindReplaceParams; LDocFindReplaceParams3 *FindReplaceParams; // Map buffer ssize_t MapLen; char16 *MapBuf; // // Thread safe Name(char*) impl LString SetName; // #ifdef _DEBUG LString PourLog; #endif LTextView3Private(LTextView3 *view) : LMutex("LTextView3Private") { View = view; WordSelectMode = -1; PourX = -1; VScrollCache = -1; DirtyStart = DirtyLen = 0; UrlColour.Rgb(0, 0, 255); LColour::GetConfigColour("colour.L_URL", UrlColour); CenterCursor = false; LayoutDirty = true; rPadding.ZOff(0, 0); MapBuf = 0; MapLen = 0; OwnFindReplaceParams = true; FindReplaceParams = new LDocFindReplaceParams3; } ~LTextView3Private() { if (OwnFindReplaceParams) { DeleteObj(FindReplaceParams); } DeleteArray(MapBuf); } void SetDirty(ssize_t Start, ssize_t Len = 0) { LayoutDirty = true; DirtyStart = Start; DirtyLen = Len; } void OnChange(PropType Prop) { if (Prop == LCss::PropPadding || Prop == LCss::PropPaddingLeft || Prop == LCss::PropPaddingRight || Prop == LCss::PropPaddingTop || Prop == LCss::PropPaddingBottom) { LCssTools t(this, View->GetFont()); rPadding.ZOff(0, 0); rPadding = t.ApplyPadding(rPadding); } } }; ////////////////////////////////////////////////////////////////////// enum UndoType { UndoDelete, UndoInsert, UndoChange }; struct Change : public LRange { UndoType Type; LArray Txt; }; struct LTextView3Undo : public LUndoEvent { LTextView3 *View; LArray Changes; LTextView3Undo(LTextView3 *view) { View = view; } void AddChange(ssize_t At, ssize_t Len, UndoType Type) { Change &c = Changes.New(); c.Start = At; c.Len = Len; c.Txt.Add(View->Text + At, Len); c.Type = Type; } void OnChange() { for (auto &c : Changes) { size_t Len = c.Len; if (View->Text) { char16 *t = View->Text + c.Start; for (size_t i=0; id->SetDirty(c.Start, c.Len); } } // LUndoEvent void ApplyChange() { View->UndoOn = false; for (auto &c : Changes) { switch (c.Type) { case UndoInsert: { View->Insert(c.Start, c.Txt.AddressOf(), c.Len); View->Cursor = c.Start + c.Len; break; } case UndoDelete: { View->Delete(c.Start, c.Len); View->Cursor = c.Start; break; } case UndoChange: { OnChange(); break; } } } View->UndoOn = true; View->Invalidate(); } void RemoveChange() { View->UndoOn = false; for (auto &c : Changes) { switch (c.Type) { case UndoInsert: { View->Delete(c.Start, c.Len); break; } case UndoDelete: { View->Insert(c.Start, c.Txt.AddressOf(), c.Len); break; } case UndoChange: { OnChange(); break; } } View->Cursor = c.Start; } View->UndoOn = true; View->Invalidate(); } }; void LTextView3::LStyle::RefreshLayout(size_t Start, ssize_t Len) { View->PourText(Start, Len); View->PourStyle(Start, Len); } ////////////////////////////////////////////////////////////////////// LTextView3::LTextView3( int Id, int x, int y, int cx, int cy, LFontType *FontType) : ResObject(Res_Custom) { // init vars LView::d->Css.Reset(d = new LTextView3Private(this)); TabSize = TAB_SIZE; IndentSize = TAB_SIZE; // setup window SetId(Id); // default options #if WINNATIVE CrLf = true; SetDlgCode(DLGC_WANTALLKEYS); #else #endif d->Padding(LCss::Len(LCss::LenPx, 2)); #ifdef _DEBUG // debug times _PourTime = 0; _StyleTime = 0; _PaintTime = 0; #endif // Data Alloc = ALLOC_BLOCK; Text = new char16[Alloc]; if (Text) *Text = 0; Cursor = 0; Size = 0; // Display if (FontType) Font = FontType->Create(); else { LFontType Type; if (Type.GetSystemFont("Fixed")) Font = Type.Create(); else printf("%s:%i - failed to create font.\n", _FL); } if (Font) { SetTabStop(true); Underline = new LFont; if (Underline) { *Underline = *Font; Underline->Underline(true); if (d->UrlColour.IsValid()) Underline->Fore(d->UrlColour); Underline->Create(); } Bold = new LFont; if (Bold) { *Bold = *Font; Bold->Bold(true); Bold->Create(); } OnFontChange(); } else { LgiTrace("%s:%i - Failed to create font, FontType=%p\n", _FL, FontType); Font = LSysFont; } CursorPos.ZOff(1, LineY-1); CursorPos.Offset(d->rPadding.x1, d->rPadding.y1); LRect r; r.ZOff(cx-1, cy-1); r.Offset(x, y); SetPos(r); LResources::StyleElement(this); } LTextView3::~LTextView3() { #ifndef WINDOWS Ctrls.Delete(this); #endif Line.DeleteObjects(); Style.Empty(); DeleteArray(TextCache); DeleteArray(Text); if (Font != LSysFont) DeleteObj(Font); DeleteObj(FixedFont); DeleteObj(Underline); DeleteObj(Bold); // 'd' is owned by the LView::Css auto ptr } char16 *LTextView3::MapText(char16 *Str, ssize_t Len, bool RtlTrailingSpace) { if (ObscurePassword /*|| ShowWhiteSpace*/ || RtlTrailingSpace) { if (Len > d->MapLen) { DeleteArray(d->MapBuf); d->MapBuf = new char16[Len + RtlTrailingSpace]; d->MapLen = Len; } if (d->MapBuf) { int n = 0; if (RtlTrailingSpace) { d->MapBuf[n++] = ' '; for (int i=0; iMapBuf[n++] = Str[i]; } } else if (ObscurePassword) { for (int i=0; iMapBuf[n++] = '*'; } } /* else if (ShowWhiteSpace) { for (int i=0; iMapBuf[n++] = 0xb7; } else if (Str[i] == '\t') { d->MapBuf[n++] = 0x2192; } else { d->MapBuf[n++] = Str[i]; } } } */ return d->MapBuf; } } return Str; } void LTextView3::SetFixedWidthFont(bool i) { if (FixedWidthFont ^ i) { if (i) { LFontType Type; if (Type.GetSystemFont("Fixed")) { LFont *f = FixedFont; FixedFont = Font; Font = f; if (!Font) { Font = Type.Create(); if (Font) { Font->PointSize(FixedFont->PointSize()); } } LDocView::SetFixedWidthFont(i); } } else if (FixedFont) { LFont *f = FixedFont; FixedFont = Font; Font = f; LDocView::SetFixedWidthFont(i); } OnFontChange(); Invalidate(); } } void LTextView3::SetReadOnly(bool i) { LDocView::SetReadOnly(i); #if WINNATIVE SetDlgCode(i ? DLGC_WANTARROWS : DLGC_WANTALLKEYS); #endif } void LTextView3::SetCrLf(bool crlf) { CrLf = crlf; } void LTextView3::SetTabSize(uint8_t i) { TabSize = limit(i, 2, 32); OnFontChange(); OnPosChange(); Invalidate(); } void LTextView3::SetWrapType(LDocWrapType i) { LDocView::SetWrapType(i); CanScrollX = i != TEXTED_WRAP_REFLOW; OnPosChange(); Invalidate(); } LFont *LTextView3::GetFont() { return Font; } LFont *LTextView3::GetBold() { return Bold; } void LTextView3::SetFont(LFont *f, bool OwnIt) { if (!f) return; if (OwnIt) { if (Font != LSysFont) DeleteObj(Font); Font = f; } else if (!Font || Font == LSysFont) { Font = new LFont(*f); } else { *Font = *f; } if (Font) { if (!Underline) Underline = new LFont; if (Underline) { *Underline = *Font; Underline->Underline(true); Underline->Create(); if (d->UrlColour.IsValid()) Underline->Fore(d->UrlColour); } if (!Bold) Bold = new LFont; if (Bold) { *Bold = *Font; Bold->Bold(true); Bold->Create(); } } OnFontChange(); } void LTextView3::OnFontChange() { if (Font) { // get line height // int OldLineY = LineY; if (!Font->Handle()) Font->Create(); LineY = Font->GetHeight(); if (LineY < 1) LineY = 1; // get tab size char Spaces[32]; memset(Spaces, 'A', TabSize); Spaces[TabSize] = 0; LDisplayString ds(Font, Spaces); Font->TabSize(ds.X()); // repour doc d->SetDirty(0, Size); // validate blue underline font if (Underline) { *Underline = *Font; Underline->Underline(true); Underline->Create(); } #if WINNATIVE // Set the IME font. HIMC hIMC = ImmGetContext(Handle()); if (hIMC) { COMPOSITIONFORM Cf; Cf.dwStyle = CFS_POINT; Cf.ptCurrentPos.x = CursorPos.x1; Cf.ptCurrentPos.y = CursorPos.y1; LOGFONT FontInfo; GetObject(Font->Handle(), sizeof(FontInfo), &FontInfo); ImmSetCompositionFont(hIMC, &FontInfo); ImmReleaseContext(Handle(), hIMC); } #endif } } void LTextView3::LogLines() { int Idx = 0; LgiTrace("DocSize: %i\n", (int)Size); for (auto i : Line) { LgiTrace(" [%i]=%p, %i+%i, %s\n", Idx, i, (int)i->Start, (int)i->Len, i->r.GetStr()); Idx++; } #ifdef _DEBUG if (d->PourLog) LgiTrace("%s", d->PourLog.Get()); #endif } bool LTextView3::ValidateLines(bool CheckBox) { size_t Pos = 0; char16 *c = Text; size_t Idx = 0; LTextLine *Prev = NULL; for (auto i : Line) { LTextLine *l = i; if (l->Start != Pos) { LogLines(); LAssert(!"Incorrect start."); return false; } char16 *e = c; if (WrapType == TEXTED_WRAP_NONE) { while (*e && *e != '\n') e++; } else { char16 *end = Text + l->Start + l->Len; while (*e && *e != '\n' && e < end) e++; } ssize_t Len = e - c; if (l->Len != Len) { LogLines(); LAssert(!"Incorrect length."); return false; } if (CheckBox && Prev && Prev->r.y2 != l->r.y1 - 1) { LogLines(); LAssert(!"Lines not joined vertically"); } if (*e) { if (*e == '\n') e++; else if (WrapType == TEXTED_WRAP_REFLOW) e++; } Pos = e - Text; c = e; Idx++; Prev = l; } if (WrapType == TEXTED_WRAP_NONE && Pos != Size) { LogLines(); LAssert(!"Last line != end of doc"); return false; } return true; } int LTextView3::AdjustStyles(ssize_t Start, ssize_t Diff, bool ExtendStyle) { int Changes = 0; for (auto &s : Style) { if (s.Start == Start) { if (Diff < 0 || ExtendStyle) s.Len += Diff; else s.Start += Diff; Changes++; } else if (s.Start > Start) { s.Start += Diff; Changes++; } } return Changes; } // break array, break out of loop when we hit these chars #define ExitLoop(c) ( (c) == 0 || \ (c) == '\n' || \ (c) == ' ' || \ (c) == '\t' \ ) // extra breaking opportunities #define ExtraBreak(c) ( ( (c) >= 0x3040 && (c) <= 0x30FF ) || \ ( (c) >= 0x3300 && (c) <= 0x9FAF ) \ ) /* Prerequisite: The Line list must have either the objects with the correct Start/Len or be missing the lines altogether... */ void LTextView3::PourText(size_t Start, ssize_t Length /* == 0 means it's a delete */) { #if PROFILE_POUR char _txt[256]; sprintf_s(_txt, sizeof(_txt), "%p::PourText Lines=%i Sz=%i", this, (int)Line.Length(), (int)Size); LProfile Prof(_txt); #endif #if !defined(HAIKU) LAssert(InThread()); #endif LRect Client = GetClient(); int Mx = Client.X() - d->rPadding.x1 - d->rPadding.x2; int Cy = 0; MaxX = 0; ssize_t Idx = -1; LTextLine *Cur = GetTextLine(Start, &Idx); // LgiTrace("Pour %i:%i Cur=%p Idx=%i\n", (int)Start, (int)Length, (int)Cur, (int)Idx); if (!Cur || !Cur->r.Valid()) { // Find the last line that has a valid position... for (auto i = Idx >= 0 ? Line.begin(Idx) : Line.rbegin(); *i; i--, Idx--) { Cur = *i; if (Cur->r.Valid()) { Cy = Cur->r.y1; if (Idx < 0) Idx = Line.IndexOf(Cur); break; } } } if (Cur && !Cur->r.Valid()) Cur = NULL; if (Cur) { Cy = Cur->r.y1; Start = Cur->Start; Length = Size - Start; // LgiTrace("Reset start to %i:%i because Cur!=NULL\n", (int)Start, (int)Length); } else { Idx = 0; Start = 0; Length = Size; } if (!Text || !Font || Mx <= 0) return; // Tracking vars ssize_t e; //int LastX = 0; int WrapCol = GetWrapAtCol(); LDisplayString Sp(Font, " ", 1); int WidthOfSpace = Sp.X(); if (WidthOfSpace < 1) { printf("%s:%i - WidthOfSpace test failed.\n", _FL); return; } // Alright... lets pour! uint64 StartTs = LCurrentTime(); if (WrapType == TEXTED_WRAP_NONE) { // Find the dimensions of each line that is missing a rect #if PROFILE_POUR Prof.Add("NoWrap: ExistingLines"); #endif #ifdef _DEGBUG LStringPipe Log(1024); Log.Printf("Pour: " LPrintfSizeT ", " LPrintfSSizeT ", partial=%i\n", Start, Length, PartialPour); #endif ssize_t Pos = 0; for (auto i = Line.begin(Idx); *i; i++, Idx++) { LTextLine *l = *i; #ifdef _DEGBUG Log.Printf(" [%i] exist: r.val=%i\n", Idx, l->r.Valid()); #endif if (!l->r.Valid()) // If the layout is not valid... { LDisplayString ds(Font, Text + l->Start, l->Len); l->r.x1 = d->rPadding.x1; l->r.x2 = l->r.x1 + ds.X(); MaxX = MAX(MaxX, l->r.X()); } // Adjust the y position anyway... it's free. l->r.y1 = Cy; l->r.y2 = l->r.y1 + LineY - 1; Cy = l->r.y2 + 1; Pos = l->Start + l->Len; if (Text[Pos] == '\n') Pos++; } // Now if we are missing lines as well, create them and lay them out #if PROFILE_POUR Prof.Add("NoWrap: NewLines"); #endif while (Pos < Size) { LTextLine *l = new LTextLine; l->Start = Pos; char16 *c = Text + Pos; char16 *e = c; while (*e && *e != '\n') e++; l->Len = e - c; #ifdef _DEGBUG Log.Printf(" [%i] new: start=" LPrintfSSizeT ", len=" LPrintfSSizeT "\n", Idx, l->Start, l->Len); #endif l->r.x1 = d->rPadding.x1; l->r.y1 = Cy; l->r.y2 = l->r.y1 + LineY - 1; if (l->Len) { LDisplayString ds(Font, Text + l->Start, l->Len); l->r.x2 = l->r.x1 + ds.X(); } else { l->r.x2 = l->r.x1; } Line.Insert(l); if (*e == '\n') e++; MaxX = MAX(MaxX, l->r.X()); Cy = l->r.y2 + 1; Pos = e - Text; Idx++; } #ifdef _DEGBUG - d->PourLog = Log.NewGStr(); + d->PourLog = Log.NewLStr(); #endif PartialPour = false; PartialPourLines = 0; } else // Wrap text { int DisplayStart = ScrollYLine(); int DisplayLines = (Client.Y() + LineY - 1) / LineY; int DisplayEnd = DisplayStart + DisplayLines; // Pouring is split into 2 parts... // 1) pouring to the end of the displayed text. // 2) pouring from there to the end of the document. // potentially taking several goes to complete the full pour // This allows the document to display and edit faster.. bool PourToDisplayEnd = Line.Length() < DisplayEnd; #if 0 LgiTrace("Idx=%i, DisplayStart=%i, DisplayLines=%i, DisplayEnd=%i, PourToDisplayEnd=%i\n", Idx, DisplayStart, DisplayLines, DisplayEnd, PourToDisplayEnd); #endif if ((ssize_t)Line.Length() > Idx) { for (auto i = Line.begin(Idx); *i; i++) delete *i; Line.Length(Idx); Cur = NULL; } int Cx = 0; ssize_t i; for (i=Start; i= Size || Text[e] == '\n' || (e-i) >= WrapCol) { break; } e++; } // Seek back some characters if we are mid word size_t OldE = e; if (e < Size && Text[e] != '\n') { while (e > i) { if (ExitLoop(Text[e]) || ExtraBreak(Text[e])) { break; } e--; } } if (e == i) { // No line break at all, so seek forward instead for (e=OldE; e < Size && Text[e] != '\n'; e++) { if (ExitLoop(Text[e]) || ExtraBreak(Text[e])) break; } } // Calc the width LDisplayString ds(Font, Text + i, e - i); Width = ds.X(); } else { // Wrap to edge of screen ssize_t PrevExitChar = -1; int PrevX = -1; while (true) { if (e >= Size || ExitLoop(Text[e]) || ExtraBreak(Text[e])) { LDisplayString ds(Font, Text + i, e - i); if (ds.X() + Cx > Mx) { if (PrevExitChar > 0) { e = PrevExitChar; Width = PrevX; } else { Width = ds.X(); } break; } else if (e >= Size || Text[e] == '\n') { Width = ds.X(); break; } PrevExitChar = e; PrevX = ds.X(); } e++; } } // Create layout line LTextLine *l = new LTextLine; if (l) { l->Start = i; l->Len = e - i; l->r.x1 = d->rPadding.x1; l->r.x2 = l->r.x1 + Width - 1; l->r.y1 = Cy; l->r.y2 = l->r.y1 + LineY - 1; Line.Insert(l); if (PourToDisplayEnd) { if (Line.Length() > DisplayEnd) { // We have reached the end of the displayed area... so // exit out temporarily to display the layout to the user PartialPour = true; PartialPourLines = std::max(PartialPourLines, Line.Length()); break; } } else { // Otherwise check if we are taking too long... if (Line.Length() % 20 == 0) { uint64 Now = LCurrentTime(); if (Now - StartTs > WRAP_POUR_TIMEOUT) { PartialPour = true; PartialPourLines = std::max(PartialPourLines, Line.Length()); break; } } } MaxX = MAX(MaxX, l->r.X()); Cy += LineY; if (e < Size) e++; } } if (i >= Size) { PartialPour = false; PartialPourLines = 0; } SendNotify(LNotifyCursorChanged); } #ifdef _DEBUG // ValidateLines(true); #endif #if PROFILE_POUR Prof.Add("LastLine"); #endif if (!PartialPour) { auto It = Line.rbegin(); LTextLine *Last = It != Line.end() ? *It : NULL; if (!Last || Last->Start + Last->Len < Size) { LTextLine *l = new LTextLine; if (l) { l->Start = Size; l->Len = 0; l->r.x1 = l->r.x2 = d->rPadding.x1; l->r.y1 = Cy; l->r.y2 = l->r.y1 + LineY - 1; Line.Insert(l); MaxX = MAX(MaxX, l->r.X()); Cy += LineY; } } } bool ScrollYNeeded = Client.Y() < (std::max(PartialPourLines, Line.Length()) * LineY); bool ScrollChange = ScrollYNeeded ^ (VScroll != NULL); d->LayoutDirty = WrapType != TEXTED_WRAP_NONE && ScrollChange; #if PROFILE_POUR static LString _s; _s.Printf("ScrollBars dirty=%i", d->LayoutDirty); Prof.Add(_s); #endif if (ScrollChange) { #if 0 LgiTrace("%s:%i - %p::SetScrollBars(%i) cliy=%i content=%i partial=%i\n", _FL, this, ScrollYNeeded, Client.Y(), (Line.Length() * LineY), PartialPour); #endif SetScrollBars(false, ScrollYNeeded); } UpdateScrollBars(); #if 0 // def _DEBUG if (GetWindow()) { static char s[256]; sprintf_s(s, sizeof(s), "Pour: %.2f sec", (double)_PourTime / 1000); GetWindow()->PostEvent(M_TEXTVIEW_DEBUG_TEXT, (LMessage::Param)s); } #endif #if POUR_DEBUG printf("Lines=%i\n", Line.Length()); int Index = 0; for (LTextLine *l=Line.First(); l; l=Line.Next(), Index++) { printf("\t[%i] %i,%i (%s)\n", Index, l->Start, l->Len, l->r.Describe()); } #endif } bool LTextView3::InsertStyle(LAutoPtr s) { if (!s) return false; LAssert(s->Start >= 0); LAssert(s->Len > 0); ssize_t Last = 0; // int n = 0; // LgiTrace("StartStyle=%i,%i(%i) %s\n", (int)s->Start, (int)s->Len, (int)(s->Start+s->Len), s->Fore.GetStr()); if (Style.Length() > 0) { // Optimize for last in the list auto Last = Style.rbegin(); if (s->Start >= (ssize_t)Last->End()) { Style.Insert(*s); return true; } } for (auto i = Style.begin(); i != Style.end(); i++) { if (s->Overlap(*i)) { if (s->Owner > i->Owner) { // Fail the insert return false; } else { // Replace mode... *i = *s; return true; } } if (s->Start >= Last && s->Start < i->Start) { Style.Insert(*s, i); return true; } } Style.Insert(*s); return true; } LTextView3::LStyle *LTextView3::GetNextStyle(StyleIter &s, ssize_t Where) { if (Where >= 0) s = Style.begin(); else s++; while (s != Style.end()) { // determine whether style is relevant.. // styles in the selected region are ignored ssize_t Min = MIN(SelStart, SelEnd); ssize_t Max = MAX(SelStart, SelEnd); if (SelStart >= 0 && s->Start >= Min && s->Start+s->Len < Max) { // style is completely inside selection: ignore s++; } else if (Where >= 0 && s->Start+s->Len < Where) { s++; } else { return &(*s); } } return NULL; } #if 0 CURSOR_CHAR GetCursor() { #ifdef WIN32 LArray Ver; int Os = LGetOs(&Ver); if ((Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64) && Ver[0] >= 5) { return MAKEINTRESOURCE(32649); // hand } else { return IDC_ARROW; } #endif return 0; } #endif LTextView3::LStyle *LTextView3::HitStyle(ssize_t i) { for (auto &s : Style) { if (i >= s.Start && i < (ssize_t)s.End()) { return &s; } } return NULL; } void LTextView3::PourStyle(size_t Start, ssize_t EditSize) { #ifdef _DEBUG int64 StartTime = LCurrentTime(); #endif LAssert(InThread()); if (!Text || Size < 1) return; ssize_t Length = MAX(EditSize, 0); if ((ssize_t)Start + Length >= Size) Length = Size - Start; // For deletes, this sizes the edit length within bounds. // Expand re-style are to word boundaries before and after the area of change while (Start > 0 && UrlChar(Text[Start-1])) { // Move the start back Start--; Length++; } while ((ssize_t)Start + Length < Size && UrlChar(Text[Start+Length])) { // Move the end back Length++; } // Delete all the styles that we own inside the changed area for (StyleIter s = Style.begin(); s != Style.end();) { if (s->Owner == STYLE_NONE) { if (EditSize > 0) { if (s->Overlap(Start, EditSize < 0 ? -EditSize : EditSize)) { Style.Delete(s); continue; } } else { if (s->Overlap(Start, -EditSize)) { Style.Delete(s); continue; } } } s++; } if (UrlDetect) { LArray Links; LAssert((ssize_t)Start + Length <= Size); if (LDetectLinks(Links, Text + Start, Length)) { for (uint32_t i=0; i Url(new LStyle(STYLE_URL)); if (Url) { Url->View = this; Url->Start = Inf.Start + Start; Url->Len = Inf.Len; // Url->Email = Inf.Email; Url->Font = Underline; Url->Fore = d->UrlColour; InsertStyle(Url); } } } } #ifdef _DEBUG _StyleTime = LCurrentTime() - StartTime; #endif } bool LTextView3::Insert(size_t At, const char16 *Data, ssize_t Len) { LProfile Prof("LTextView3::Insert"); Prof.HideResultsIfBelow(1000); LAssert(InThread()); if (!ReadOnly && Len > 0) { if (!Data) return false; // limit input to valid data At = MIN(Size, (ssize_t)At); // make sure we have enough memory size_t NewAlloc = Size + Len + 1; NewAlloc += ALLOC_BLOCK - (NewAlloc % ALLOC_BLOCK); if (NewAlloc != Alloc) { char16 *NewText = new char16[NewAlloc]; if (NewText) { if (Text) { // copy any existing data across memcpy(NewText, Text, (Size + 1) * sizeof(char16)); } DeleteArray(Text); Text = NewText; Alloc = NewAlloc; } else { // memory allocation error return false; } } Prof.Add("MemChk"); if (Text) { // Insert the data // Move the section after the insert to make space... memmove(Text+(At+Len), Text+At, (Size-At) * sizeof(char16)); Prof.Add("Cpy"); // Copy new data in... memcpy(Text+At, Data, Len * sizeof(char16)); Size += Len; Text[Size] = 0; // NULL terminate Prof.Add("Undo"); // Add the undo object... if (UndoOn) { LAutoPtr Obj(new LTextView3Undo(this)); LTextView3Undo *u = UndoCur ? UndoCur : Obj; if (u) u->AddChange(At, Len, UndoInsert); if (Obj) UndoQue += Obj.Release(); } // Clear layout info for the new text ssize_t Idx = -1; LTextLine *Cur = NULL; if (Line.Length() == 0) { // Empty doc... set up the first line Line.Insert(Cur = new LTextLine); Idx = 0; Cur->Start = 0; } else { Cur = GetTextLine(At, &Idx); } if (Cur) { if (WrapType == TEXTED_WRAP_NONE) { // Clear layout for current line... Cur->r.ZOff(-1, -1); Prof.Add("NoWrap add lines"); // Add any new lines that we need... char16 *e = Text + At + Len; char16 *c; for (c = Text + At; c < e; c++) { if (*c == '\n') { // Set the size of the current line... size_t Pos = c - Text; Cur->Len = Pos - Cur->Start; // Create a new line... Cur = new LTextLine(); if (!Cur) return false; Cur->Start = Pos + 1; Line.Insert(Cur, ++Idx); } } Prof.Add("CalcLen"); // Make sure the last Line's length is set.. Cur->CalcLen(Text); Prof.Add("UpdatePos"); // Now update all the positions of the following lines... for (auto i = Line.begin(++Idx); *i; i++) (*i)->Start += Len; } else { // Clear all lines to the end of the doc... for (auto i = Line.begin(Idx); *i; i++) delete *i; Line.Length(Idx); } } else { // If wrap is on then this can happen when an Insert happens before the // OnPulse event has laid out the new text. Probably not a good thing in // non-wrap mode if (WrapType == TEXTED_WRAP_NONE) { LTextLine *l = *Line.rbegin(); printf("%s:%i - Insert error: no cur, At=%i, Size=%i, Lines=%i, WrapType=%i\n", _FL, (int)At, (int)Size, (int)Line.Length(), (int)WrapType); if (l) printf("Last=%i, %i\n", (int)l->Start, (int)l->Len); } } #ifdef _DEBUG // Prof.Add("Validate"); // ValidateLines(); #endif if (AdjustStylePos) AdjustStyles(At, Len); Dirty = true; if (PourEnabled) { Prof.Add("PourText"); PourText(At, Len); Prof.Add("PourStyle"); auto Start = LCurrentTime(); PourStyle(At, Len); auto End = LCurrentTime(); if (End - Start > 1000) { PourStyle(At, Len); } } SendNotify(LNotifyDocChanged); return true; } } return false; } bool LTextView3::Delete(size_t At, ssize_t Len) { bool Status = false; LAssert(InThread()); if (!ReadOnly) { // limit input At = MAX(At, 0); At = MIN((ssize_t)At, Size); Len = MIN(Size-(ssize_t)At, Len); if (Len > 0) { int HasNewLine = 0; for (int i=0; i Obj(new LTextView3Undo(this)); LTextView3Undo *u = UndoCur ? UndoCur : Obj; if (u) u->AddChange(At, Len, UndoDelete); if (Obj) UndoQue += Obj.Release(); } memmove(Text+At, Text+(At+Len), (Size-At-Len) * sizeof(char16)); Size -= Len; Text[Size] = 0; if (WrapType == TEXTED_WRAP_NONE) { ssize_t Idx = -1; LTextLine *Cur = GetTextLine(At, &Idx); if (Cur) { Cur->r.ZOff(-1, -1); // Delete some lines... for (int i=0; iCalcLen(Text); // Shift all further lines down... for (auto i = Line.begin(Idx + 1); *i; i++) (*i)->Start -= Len; } } else { ssize_t Index; LTextLine *Cur = GetTextLine(At, &Index); if (Cur) { for (auto i = Line.begin(Index); *i; i++) delete *i; Line.Length(Index); } } Dirty = true; Status = true; #ifdef _DEBUG // ValidateLines(); #endif if (AdjustStylePos) AdjustStyles(At, -Len); if (PourEnabled) { PourText(At, -Len); PourStyle(At, -Len); } if (Cursor >= (ssize_t)At && Cursor <= (ssize_t)At + Len) { SetCaret(At, false, HasNewLine != 0); } // Handle repainting in flowed mode, when the line starts change if (WrapType == TEXTED_WRAP_REFLOW) { ssize_t Index; LTextLine *Cur = GetTextLine(At, &Index); if (Cur) { LRect r = Cur->r; r.x2 = GetClient().x2; r.y2 = GetClient().y2; Invalidate(&r); } } SendNotify(LNotifyDocChanged); Status = true; } } return Status; } void LTextView3::DeleteSelection(char16 **Cut) { if (SelStart >= 0) { ssize_t Min = MIN(SelStart, SelEnd); ssize_t Max = MAX(SelStart, SelEnd); if (Cut) { *Cut = NewStrW(Text + Min, Max - Min); } Delete(Min, Max - Min); SetCaret(Min, false, true); } } List::I LTextView3::GetTextLineIt(ssize_t Offset, ssize_t *Index) { int i = 0; for (auto It = Line.begin(); It != Line.end(); It++) { auto l = *It; if (Offset >= l->Start && Offset <= l->Start+l->Len) { if (Index) *Index = i; return It; } i++; } return Line.end(); } int64 LTextView3::Value() { auto n = Name(); #ifdef _MSC_VER return (n) ? _atoi64(n) : 0; #else return (n) ? atoll(n) : 0; #endif } void LTextView3::Value(int64 i) { char Str[32]; sprintf_s(Str, sizeof(Str), LPrintfInt64, i); Name(Str); } LString LTextView3::operator[](ssize_t LineIdx) { if (LineIdx <= 0 || LineIdx > (ssize_t)GetLines()) return LString(); LTextLine *Ln = Line[LineIdx-1]; if (!Ln) return LString(); LString s(Text + Ln->Start, Ln->Len); return s; } const char *LTextView3::Name() { UndoQue.Empty(); DeleteArray(TextCache); TextCache = WideToUtf8(Text); return TextCache; } bool LTextView3::Name(const char *s) { if (InThread()) { UndoQue.Empty(); DeleteArray(TextCache); DeleteArray(Text); Line.DeleteObjects(); Style.Empty(); LAssert(LIsUtf8(s)); Text = Utf8ToWide(s); if (!Text) { Text = new char16[1]; if (Text) *Text = 0; } Size = Text ? StrlenW(Text) : 0; Alloc = Size + 1; Cursor = MIN(Cursor, Size); if (Text) { // Remove '\r's char16 *o = Text; for (char16 *i=Text; *i; i++) { if (*i != '\r') { *o++ = *i; } else Size--; } *o++ = 0; } // update everything else d->SetDirty(0, Size); PourText(0, Size); PourStyle(0, Size); UpdateScrollBars(); Invalidate(); } else if (d->Lock(_FL)) { if (IsAttached()) { d->SetName = s; PostEvent(M_TEXT_UPDATE_NAME); } else LAssert(!"Can't post event to detached/virtual window."); d->Unlock(); } return true; } const char16 *LTextView3::NameW() { return Text; } const char16 *LTextView3::TextAtLine(size_t Index) { if (Index >= Line.Length()) return NULL; auto ln = Line[Index]; return Text + ln->Start; } bool LTextView3::NameW(const char16 *s) { DeleteArray(Text); Size = s ? StrlenW(s) : 0; Alloc = Size + 1; Text = new char16[Alloc]; Cursor = MIN(Cursor, Size); if (Text) { memcpy(Text, s, Size * sizeof(char16)); // remove LF's int In = 0, Out = 0; CrLf = false; for (; InSetDirty(0, Size); PourText(0, Size); PourStyle(0, Size); UpdateScrollBars(); Invalidate(); return true; } LRange LTextView3::GetSelectionRange() { LRange r; if (HasSelection()) { r.Start = MIN(SelStart, SelEnd); ssize_t End = MAX(SelStart, SelEnd); r.Len = End - r.Start; } return r; } char *LTextView3::GetSelection() { LRange s = GetSelectionRange(); if (s.Len > 0) { return (char*)LNewConvertCp("utf-8", Text + s.Start, LGI_WideCharset, s.Len*sizeof(Text[0]) ); } return 0; } bool LTextView3::HasSelection() { return (SelStart >= 0) && (SelStart != SelEnd); } void LTextView3::SelectAll() { SelStart = 0; SelEnd = Size; Invalidate(); } void LTextView3::UnSelectAll() { bool Update = HasSelection(); SelStart = -1; SelEnd = -1; if (Update) { Invalidate(); } } size_t LTextView3::GetLines() { return Line.Length(); } void LTextView3::GetTextExtent(int &x, int &y) { PourText(0, Size); x = MaxX + d->rPadding.x1; y = (int)(Line.Length() * LineY); } bool LTextView3::GetLineColumnAtIndex(LPoint &Pt, ssize_t Index) { ssize_t FromIndex = 0; LTextLine *From = GetTextLine(Index < 0 ? Cursor : Index, &FromIndex); if (!From) return false; Pt.x = (int) (Cursor - From->Start); Pt.y = (int) FromIndex; return true; } ssize_t LTextView3::GetCaret(bool Cur) { if (Cur) { return Cursor; } return 0; } ssize_t LTextView3::IndexAt(int x, int y) { LTextLine *l = Line.ItemAt(y); if (l) { return l->Start + MIN(x, l->Len); } return 0; } bool LTextView3::ScrollToOffset(size_t Off) { bool ForceFullUpdate = false; ssize_t ToIndex = 0; LTextLine *To = GetTextLine(Off, &ToIndex); if (To) { LRect Client = GetClient(); int DisplayLines = Client.Y() / LineY; if (VScroll) { if (ToIndex < VScroll->Value()) { // Above the visible region... if (d->CenterCursor) { ssize_t i = ToIndex - (DisplayLines >> 1); VScroll->Value(MAX(0, i)); } else { VScroll->Value(ToIndex); } ForceFullUpdate = true; } if (ToIndex >= VScroll->Value() + DisplayLines) { int YOff = d->CenterCursor ? DisplayLines >> 1 : DisplayLines; ssize_t v = MIN(ToIndex - YOff + 1, (ssize_t)Line.Length() - DisplayLines); if (v != VScroll->Value()) { // Below the visible region VScroll->Value(v); ForceFullUpdate = true; } } } else { d->VScrollCache = ToIndex; } } return ForceFullUpdate; } void LTextView3::SetCaret(size_t i, bool Select, bool ForceFullUpdate) { // int _Start = LCurrentTime(); Blink = true; // Bound the new cursor position to the document if ((ssize_t)i > Size) i = Size; // Store the old selection and cursor ssize_t s = SelStart, e = SelEnd, c = Cursor; // If there is going to be a selected area if (Select && i != SelStart) { // Then set the start if (SelStart < 0) { // We are starting a new selection SelStart = Cursor; } // And end SelEnd = i; } else { // Clear the selection SelStart = SelEnd = -1; } ssize_t FromIndex = 0; LTextLine *From = GetTextLine(Cursor, &FromIndex); Cursor = i; // check the cursor is on the screen ForceFullUpdate |= ScrollToOffset(Cursor); // check whether we need to update the screen ssize_t ToIndex = 0; LTextLine *To = GetTextLine(Cursor, &ToIndex); if (ForceFullUpdate || !To || !From) { // need full update Invalidate(); } else if ( ( SelStart != s || SelEnd != e ) ) { // Update just the selection bounds LRect Client = GetClient(); size_t Start, End; if (SelStart >= 0 && s >= 0) { // Selection has changed, union the before and after regions Start = MIN(Cursor, c); End = MAX(Cursor, c); } else if (SelStart >= 0) { // Selection created... Start = MIN(SelStart, SelEnd); End = MAX(SelStart, SelEnd); } else if (s >= 0) { // Selection removed... Start = MIN(s, e); End = MAX(s, e); } else return; auto SLine = GetTextLine(Start); auto ELine = GetTextLine(End); LRect u; if (SLine && ELine) { if (SLine->r.Valid()) { u = DocToScreen(SLine->r); } else u.Set(0, 0, Client.X()-1, 1); // Start of visible page LRect b(0, Client.Y()-1, Client.X()-1, Client.Y()-1); if (ELine->r.Valid()) { b = DocToScreen(ELine->r); } else { b.Set(0, Client.Y()-1, Client.X()-1, Client.Y()-1); } u.Union(&b); u.x1 = 0; u.x2 = X(); } else { /* printf("%s,%i - Couldn't get SLine and ELine: %i->%p, %i->%p\n", _FL, (int)Start, SLine, (int)End, ELine); */ u = Client; } Invalidate(&u); } else if (Cursor != c) { // just the cursor has moved // update the line the cursor moved to LRect r = To->r; r.Offset(-ScrollX, d->rPadding.y1-DocOffset); r.x2 = X(); Invalidate(&r); if (To != From) { // update the line the cursor came from, // if it's a different line from the "to" r = From->r; r.Offset(-ScrollX, d->rPadding.y1-DocOffset); r.x2 = X(); Invalidate(&r); } } if (c != Cursor) { // Send off notify SendNotify(LNotifyCursorChanged); } //int _Time = LCurrentTime() - _Start; //printf("Setcursor=%ims\n", _Time); } void LTextView3::SetBorder(int b) { } bool LTextView3::Cut() { bool Status = false; char16 *Txt16 = 0; DeleteSelection(&Txt16); if (Txt16) { #ifdef WIN32 Txt16 = ConvertToCrLf(Txt16); #endif char *Txt8 = (char*)LNewConvertCp(LAnsiToLgiCp(), Txt16, LGI_WideCharset); LClipBoard Clip(this); Clip.Text(Txt8); Status = Clip.TextW(Txt16, false); DeleteArray(Txt8); DeleteArray(Txt16); } return Status; } bool LTextView3::Copy() { bool Status = true; printf("txt copy\n"); if (SelStart >= 0) { ssize_t Min = MIN(SelStart, SelEnd); ssize_t Max = MAX(SelStart, SelEnd); #ifdef WIN32 char16 *Txt16 = NewStrW(Text+Min, Max-Min); Txt16 = ConvertToCrLf(Txt16); char *Txt8 = (char*)LNewConvertCp(LAnsiToLgiCp(), Txt16, LGI_WideCharset); #else char *Txt8 = (char*)LNewConvertCp("utf-8", Text+Min, LGI_WideCharset, (Max-Min)*sizeof(*Text)); #endif LClipBoard Clip(this); Clip.Text(Txt8); #ifdef WIN32 Clip.TextW(Txt16, false); DeleteArray(Txt16); #endif DeleteArray(Txt8); } else LgiTrace("%s:%i - No selection.\n", _FL); return Status; } bool LTextView3::Paste() { LClipBoard Clip(this); LAutoWString Mem; char16 *t = Clip.TextW(); if (!t) // ala Win9x { char *s = Clip.Text(); if (s) { Mem.Reset(Utf8ToWide(s)); t = Mem; } } if (!t) return false; if (SelStart >= 0) { DeleteSelection(); } // remove '\r's char16 *s = t, *d = t; for (; *s; s++) { if (*s != '\r') { *d++ = *s; } } *d++ = 0; // insert text ssize_t Len = StrlenW(t); Insert(Cursor, t, Len); SetCaret(Cursor+Len, false, true); // Multiline return true; } void LTextView3::ClearDirty(std::function OnStatus, bool Ask, const char *FileName) { if (Dirty) { int Answer = (Ask) ? LgiMsg(this, LLoadString(L_TEXTCTRL_ASK_SAVE, "Do you want to save your changes to this document?"), LLoadString(L_TEXTCTRL_SAVE, "Save"), MB_YESNOCANCEL) : IDYES; if (Answer == IDYES) { auto DoSave = [this, OnStatus, FileName=LString(FileName)](bool ok) { Save(FileName); if (OnStatus) OnStatus(ok); }; if (!FileName) { LFileSelect *Select = new LFileSelect; Select->Parent(this); Select->Save([&FileName, &DoSave](auto Select, auto ok) { if (ok) FileName = Select->Name(); DoSave(ok); delete Select; }); } else DoSave(true); } else if (Answer == IDCANCEL) { if (OnStatus) OnStatus(false); return; } } if (OnStatus) OnStatus(true); } bool LTextView3::Open(const char *Name, const char *CharSet) { bool Status = false; LFile f; if (f.Open(Name, O_READ|O_SHARE)) { DeleteArray(Text); int64 Bytes = f.GetSize(); if (Bytes < 0 || Bytes & 0xffff000000000000LL) { LgiTrace("%s:%i - Invalid file size: " LPrintfInt64 "\n", _FL, Bytes); return false; } SetCaret(0, false); Line.DeleteObjects(); char *c8 = new char[Bytes + 4]; if (c8) { if (f.Read(c8, (int)Bytes) == Bytes) { char *DataStart = c8; c8[Bytes] = 0; c8[Bytes+1] = 0; c8[Bytes+2] = 0; c8[Bytes+3] = 0; if ((uchar)c8[0] == 0xff && (uchar)c8[1] == 0xfe) { // utf-16 if (!CharSet) { CharSet = "utf-16"; DataStart += 2; } } // Convert to unicode first.... if (Bytes == 0) { Text = new char16[1]; if (Text) Text[0] = 0; } else { Text = (char16*)LNewConvertCp(LGI_WideCharset, DataStart, CharSet ? CharSet : DefaultCharset); } if (Text) { // Remove LF's char16 *In = Text, *Out = Text; CrLf = false; Size = 0; while (*In) { if (*In >= ' ' || *In == '\t' || *In == '\n') { *Out++ = *In; Size++; } else if (*In == '\r') { CrLf = true; } In++; } Size = (int) (Out - Text); *Out = 0; Alloc = Size + 1; Dirty = false; if (Text && Text[0] == 0xfeff) // unicode byte order mark { memmove(Text, Text+1, Size * sizeof(*Text)); Size--; } PourText(0, Size); PourStyle(0, Size); UpdateScrollBars(true); Status = true; } } DeleteArray(c8); } else { Alloc = Size = 0; } Invalidate(); } return Status; } template bool WriteToStream(LFile &out, T *in, size_t len, bool CrLf) { if (!in) return false; if (CrLf) { int BufLen = 1 << 20; LAutoPtr Buf(new T[BufLen]); T *b = Buf; T *e = Buf + BufLen; T *c = in; T *end = c + len; while (c < end) { if (b > e - 16) { auto Bytes = (b - Buf) * sizeof(T); if (out.Write(Buf, Bytes) != Bytes) return false; b = Buf; } if (*c == '\n') { *b++ = '\r'; *b++ = '\n'; } else { *b++ = *c; } c++; } auto Bytes = (b - Buf) * sizeof(T); if (out.Write(Buf, Bytes) != Bytes) return false; } else { auto Bytes = len * sizeof(T); if (out.Write(in, Bytes) != Bytes) return false; } return true; } bool LTextView3::Save(const char *Name, const char *CharSet) { LFile f; LString TmpName; bool Status = false; d->LastError.Empty(); if (f.Open(Name, O_WRITE)) { if (f.SetSize(0) != 0) { // Can't resize file, fall back to renaming it and // writing a new file... f.Close(); TmpName = Name; TmpName += ".tmp"; if (!FileDev->Move(Name, TmpName)) { LgiTrace("%s:%i - Failed to move '%s'.\n", _FL, Name); return false; } if (!f.Open(Name, O_WRITE)) { LgiTrace("%s:%i - Failed to open '%s' for writing.\n", _FL, Name); return false; } } if (Text) { auto InSize = Size * sizeof(char16); if (CharSet && !Stricmp(CharSet, "utf-16")) { if (sizeof(*Text) == 2) { // No conversion needed... Status = WriteToStream(f, Text, Size, CrLf); } else { // 32->16 convert LAutoPtr c16((uint16_t*)LNewConvertCp(CharSet, Text, LGI_WideCharset, InSize)); if (c16) Status = WriteToStream(f, c16.Get(), Strlen(c16.Get()), CrLf); } } else if (CharSet && !Stricmp(CharSet, "utf-32")) { if (sizeof(*Text) == 4) { // No conversion needed... Status = WriteToStream(f, Text, Size, CrLf); } else { // 16->32 convert LAutoPtr c32((uint32_t*)LNewConvertCp(CharSet, Text, LGI_WideCharset, InSize)); if (c32) Status = WriteToStream(f, c32.Get(), Strlen(c32.Get()), CrLf); } } else { LAutoString c8((char*)LNewConvertCp(CharSet ? CharSet : DefaultCharset, Text, LGI_WideCharset, InSize)); if (c8) Status = WriteToStream(f, c8.Get(), strlen(c8), CrLf); } if (Status) Dirty = false; } } else { int Err = f.GetError(); LString sErr = LErrorCodeToString(Err); d->LastError.Printf("Failed to open '%s' for writing: %i - %s\n", Name, Err, sErr.Get()); } if (TmpName) FileDev->Delete(TmpName); return Status; } const char *LTextView3::GetLastError() { return d->LastError; } void LTextView3::UpdateScrollBars(bool Reset) { if (!VScroll) return; LRect Before = GetClient(); int DisplayLines = Y() / LineY; ssize_t Lines = std::max(PartialPourLines, Line.Length()); VScroll->SetRange(Lines); if (VScroll) { VScroll->SetPage(DisplayLines); ssize_t Max = Lines - DisplayLines + 1; bool Inval = false; if (VScroll->Value() > Max) { VScroll->Value(Max); Inval = true; } if (Reset) { VScroll->Value(0); SelStart = SelEnd = -1; } else if (d->VScrollCache >= 0) { VScroll->Value(d->VScrollCache); d->VScrollCache = -1; SelStart = SelEnd = -1; } LRect After = GetClient(); if (Before != After && GetWrapType()) { d->SetDirty(0, Size); Inval = true; } if (Inval) { Invalidate(); } } } void LTextView3::DoCase(std::function Callback, bool Upper) { if (Text) { ssize_t Min = MIN(SelStart, SelEnd); ssize_t Max = MAX(SelStart, SelEnd); if (Min < Max) { if (UndoOn) { LAutoPtr Obj(new LTextView3Undo(this)); LTextView3Undo *u = UndoCur ? UndoCur : Obj; if (u) u->AddChange(Min, Max - Min, UndoChange); if (Obj) UndoQue += Obj.Release(); } for (ssize_t i=Min; i= 'a' && Text[i] <= 'z') Text[i] = Text[i] - 'a' + 'A'; } else { if (Text[i] >= 'A' && Text[i] <= 'Z') Text[i] = Text[i] - 'A' + 'a'; } } Dirty = true; d->SetDirty(Min, 0); Invalidate(); SendNotify(LNotifyDocChanged); } } if (Callback) Callback(Text != NULL); } ssize_t LTextView3::GetLine() { ssize_t Idx = 0; GetTextLine(Cursor, &Idx); return Idx + 1; } void LTextView3::SetLine(int64_t i, bool select) { LTextLine *l = Line.ItemAt(i - 1); if (l) { d->CenterCursor = true; SetCaret(l->Start, select); d->CenterCursor = false; } } void LTextView3::DoGoto(std::function Callback) { LInput *Dlg = new LInput(this, "", LLoadString(L_TEXTCTRL_GOTO_LINE, "Goto line:"), "Text"); Dlg->DoModal([this, Dlg, Callback](auto d, auto code) { auto ok = code == IDOK && Dlg->GetStr(); if (ok) SetLine(Dlg->GetStr().Int()); if (Callback) Callback(ok); delete Dlg; }); } LDocFindReplaceParams *LTextView3::CreateFindReplaceParams() { return new LDocFindReplaceParams3; } void LTextView3::SetFindReplaceParams(LDocFindReplaceParams *Params) { if (Params) { if (d->OwnFindReplaceParams) { DeleteObj(d->FindReplaceParams); } d->OwnFindReplaceParams = false; d->FindReplaceParams = (LDocFindReplaceParams3*) Params; } } void LTextView3::DoFindNext(std::function OnStatus) { bool Status = false; if (InThread()) { if (d->FindReplaceParams->Lock(_FL)) { if (d->FindReplaceParams->LastFind) Status = OnFind(d->FindReplaceParams->LastFind, d->FindReplaceParams->MatchWord, d->FindReplaceParams->MatchCase, d->FindReplaceParams->SelectionOnly, d->FindReplaceParams->SearchUpwards); d->FindReplaceParams->Unlock(); } } else if (IsAttached()) { Status = PostEvent(M_TEXTVIEW_FIND); } if (OnStatus) OnStatus(Status); } void LTextView3::DoFind(std::function Callback) { LString u; if (HasSelection()) { ssize_t Min = MIN(SelStart, SelEnd); ssize_t Max = MAX(SelStart, SelEnd); u = LString(Text + Min, Max - Min); } else { u = d->FindReplaceParams->LastFind.Get(); } auto Dlg = new LFindDlg(this, [this, Params=d->FindReplaceParams, Callback](auto Dlg, auto Action) { if (Params && Params->Lock(_FL)) { Params->MatchWord = Dlg->MatchWord; Params->MatchCase = Dlg->MatchCase; Params->SelectionOnly = Dlg->SelectionOnly; Params->SearchUpwards = Dlg->SearchUpwards; Params->LastFind.Reset(Utf8ToWide(Dlg->Find)); Params->Unlock(); } DoFindNext([this, Callback](bool ok) { Focus(true); if (Callback) Callback(ok); }); }, u); Dlg->DoModal(NULL); } void LTextView3::DoReplace(std::function Callback) { bool SingleLineSelection = false; SingleLineSelection = HasSelection(); if (SingleLineSelection) { LRange Sel = GetSelectionRange(); for (ssize_t i = Sel.Start; i < Sel.End(); i++) { if (Text[i] == '\n') { SingleLineSelection = false; break; } } } LAutoString LastFind8(SingleLineSelection ? GetSelection() : WideToUtf8(d->FindReplaceParams->LastFind)); LAutoString LastReplace8(WideToUtf8(d->FindReplaceParams->LastReplace)); auto Dlg = new LReplaceDlg(this, [this, LastFind8, LastReplace8](LFindReplaceCommon *Dlg, int Action) { LReplaceDlg *Replace = dynamic_cast(Dlg); LAssert(Replace != NULL); if (Action == IDCANCEL) return; if (d->FindReplaceParams->Lock(_FL)) { d->FindReplaceParams->LastFind.Reset(Utf8ToWide(Replace->Find)); d->FindReplaceParams->LastReplace.Reset(Utf8ToWide(Replace->Replace)); d->FindReplaceParams->MatchWord = Replace->MatchWord; d->FindReplaceParams->MatchCase = Replace->MatchCase; d->FindReplaceParams->SelectionOnly = Replace->SelectionOnly; switch (Action) { case IDC_FR_FIND: { OnFind( d->FindReplaceParams->LastFind, d->FindReplaceParams->MatchWord, d->FindReplaceParams->MatchCase, d->FindReplaceParams->SelectionOnly, d->FindReplaceParams->SearchUpwards); break; } case IDOK: case IDC_FR_REPLACE: { OnReplace( d->FindReplaceParams->LastFind, d->FindReplaceParams->LastReplace, Action == IDOK, d->FindReplaceParams->MatchWord, d->FindReplaceParams->MatchCase, d->FindReplaceParams->SelectionOnly, d->FindReplaceParams->SearchUpwards); break; } } d->FindReplaceParams->Unlock(); } }, LastFind8, LastReplace8); Dlg->MatchWord = d->FindReplaceParams->MatchWord; Dlg->MatchCase = d->FindReplaceParams->MatchCase; Dlg->SelectionOnly = HasSelection(); Dlg->DoModal(NULL); } void LTextView3::SelectWord(size_t From) { for (SelStart = From; SelStart > 0; SelStart--) { if (strchr(SelectWordDelim, Text[SelStart])) { SelStart++; break; } } for (SelEnd = From; SelEnd < Size; SelEnd++) { if (strchr(SelectWordDelim, Text[SelEnd])) { break; } } Invalidate(); } typedef int (*StringCompareFn)(const char16 *a, const char16 *b, ssize_t n); ptrdiff_t LTextView3::MatchText(const char16 *Find, bool MatchWord, bool MatchCase, bool SelectionOnly, bool SearchUpwards) { if (!ValidStrW(Find)) return -1; ssize_t FindLen = StrlenW(Find); // Setup range to search ssize_t Begin, End; if (SelectionOnly && HasSelection()) { Begin = MIN(SelStart, SelEnd); End = MAX(SelStart, SelEnd); } else { Begin = 0; End = Size; } // Look through text... ssize_t i; bool Wrap = false; if (Cursor > End - FindLen) { Wrap = true; if (SearchUpwards) i = End - FindLen; else i = Begin; } else { i = Cursor; } if (i < Begin) i = Begin; if (i > End) i = End; StringCompareFn CmpFn = MatchCase ? StrncmpW : StrnicmpW; char16 FindCh = MatchCase ? Find[0] : toupper(Find[0]); for (; SearchUpwards ? i >= Begin : i <= End; i += SearchUpwards ? -1 : 1) { if ( (MatchCase ? Text[i] : toupper(Text[i])) == FindCh ) { char16 *Possible = Text + i; if (CmpFn(Possible, Find, FindLen) == 0) { if (MatchWord) { // Check boundaries if (Possible > Text) // Check off the start { if (!IsWordBoundry(Possible[-1])) continue; } if (i + FindLen < Size) // Check off the end { if (!IsWordBoundry(Possible[FindLen])) continue; } } /* What was this even supposed to do? LRange r(Possible - Text, FindLen); if (!r.Overlap(Cursor)) */ return i; } } if (!Wrap && (i + 1 > End - FindLen)) { Wrap = true; i = Begin; End = Cursor; } } return -1; } bool LTextView3::OnFind(const char16 *Find, bool MatchWord, bool MatchCase, bool SelectionOnly, bool SearchUpwards) { THREAD_CHECK(); // Not sure what this is doing??? if (HasSelection() && SelEnd < SelStart) { Cursor = SelStart; } #if FEATURE_HILIGHT_ALL_MATCHES // Clear existing styles for matches for (StyleIter s = Style.begin(); s != Style.end(); ) { if (s->Owner == STYLE_FIND_MATCHES) Style.Delete(s); else s++; } ssize_t FindLen = StrlenW(Find); ssize_t FirstLoc = MatchText(Find, MatchWord, MatchCase, false, SearchUpwards), Loc; if (FirstLoc >= 0) { SetCaret(FirstLoc, false); SetCaret(FirstLoc + FindLen, true); } ssize_t Old = Cursor; if (!SearchUpwards) Cursor += FindLen; while ((Loc = MatchText(Find, MatchWord, MatchCase, false, false)) != FirstLoc) { LAutoPtr s(new LStyle(STYLE_FIND_MATCHES)); s->Start = Loc; s->Len = FindLen; s->Fore = LColour(L_FOCUS_SEL_FORE); s->Back = LColour(L_FOCUS_SEL_BACK).Mix(LColour(L_WORKSPACE)); InsertStyle(s); Cursor = Loc + FindLen; } Cursor = Old; ScrollToOffset(Cursor); Invalidate(); #else ssize_t Loc = MatchText(Find, MatchWord, MatchCase, SelectionOnly, SearchUpwards); if (Loc >= 0) { SetCaret(Loc, false); SetCaret(Loc + StrlenW(Find), true); return true; } #endif return false; } bool LTextView3::OnReplace(const char16 *Find, const char16 *Replace, bool All, bool MatchWord, bool MatchCase, bool SelectionOnly, bool SearchUpwards) { THREAD_CHECK(); if (ValidStrW(Find)) { // int Max = -1; ssize_t FindLen = StrlenW(Find); ssize_t ReplaceLen = StrlenW(Replace); // size_t OldCursor = Cursor; ptrdiff_t First = -1; while (true) { ptrdiff_t Loc = MatchText(Find, MatchWord, MatchCase, SelectionOnly, SearchUpwards); if (First < 0) { First = Loc; } else if (Loc == First) { break; } if (Loc >= 0) { ssize_t OldSelStart = SelStart; ssize_t OldSelEnd = SelEnd; Delete(Loc, FindLen); Insert(Loc, Replace, ReplaceLen); SelStart = OldSelStart; SelEnd = OldSelEnd - FindLen + ReplaceLen; Cursor = Loc + ReplaceLen; } if (!All) { return Loc >= 0; } if (Loc < 0) break; } } return false; } ssize_t LTextView3::SeekLine(ssize_t Offset, GTextViewSeek Where) { THREAD_CHECK(); switch (Where) { case PrevLine: { for (; Offset > 0 && Text[Offset] != '\n'; Offset--) ; if (Offset > 0) Offset--; for (; Offset > 0 && Text[Offset] != '\n'; Offset--) ; if (Offset > 0) Offset++; break; } case NextLine: { for (; Offset < Size && Text[Offset] != '\n'; Offset++) ; Offset++; break; } case StartLine: { for (; Offset > 0 && Text[Offset] != '\n'; Offset--) ; if (Offset > 0) Offset++; break; } case EndLine: { for (; Offset < Size && Text[Offset] != '\n'; Offset++) ; break; } default: { LAssert(false); break; } } return Offset; } bool LTextView3::OnMultiLineTab(bool In) { bool Status = false; ssize_t Min = MIN(SelStart, SelEnd); ssize_t Max = MAX(SelStart, SelEnd), i; Min = SeekLine(Min, StartLine); int Ls = 0; LArray p; for (i=Min; i=0; i--) { if (In) { // <- ssize_t n = Indexes[i], Space = 0; for (; Space ssize_t Len = Indexes[i]; for (; Text[Len] != '\n' && Len Indexes[i]) { if (HardTabs) { char16 Tab[] = {'\t', 0}; Insert(Indexes[i], Tab, 1); Max++; } else { char16 *Sp = new char16[IndentSize]; if (Sp) { for (int n=0; nChanges.Length()) { UndoQue += UndoCur; UndoCur = NULL; } else { DeleteObj(UndoCur); } SelStart = Min; SelEnd = Cursor = Max; PourEnabled = true; PourText(Min, Max - Min); PourStyle(Min, Max - Min); d->SetDirty(Min, Max-Min); Invalidate(); Status = true; return Status; } void LTextView3::OnSetHidden(int Hidden) { } void LTextView3::OnPosChange() { static bool Processing = false; if (!Processing) { Processing = true; LLayout::OnPosChange(); LRect c = GetClient(); bool ScrollYNeeded = c.Y() < (std::max(PartialPourLines, Line.Length()) * LineY); bool ScrollChange = ScrollYNeeded ^ (VScroll != NULL); if (ScrollChange) { #if 0 auto Client = GetClient(); LgiTrace("%s:%i - %p::SetScrollBars(%i) cliy=%i content=%i partial=%i\n", _FL, this, ScrollYNeeded, Client.Y(), (Line.Length() * LineY), PartialPour); #endif SetScrollBars(false, ScrollYNeeded); } UpdateScrollBars(); if (GetWrapType() && d->PourX != X()) { d->PourX = X(); d->SetDirty(0, Size); } Processing = false; } } int LTextView3::WillAccept(LDragFormats &Formats, LPoint Pt, int KeyState) { Formats.Supports("text/uri-list"); Formats.Supports("text/html"); Formats.Supports("UniformResourceLocatorW"); return Formats.Length() ? DROPEFFECT_COPY : DROPEFFECT_NONE; } int LTextView3::OnDrop(LArray &Data, LPoint Pt, int KeyState) { int Status = DROPEFFECT_NONE; for (unsigned i=0; iIsBinary()) { OsChar *e = (OsChar*) ((char*)Data->Value.Binary.Data + Data->Value.Binary.Length); OsChar *s = (OsChar*) Data->Value.Binary.Data; int len = 0; while (s < e && s[len]) { len++; } LAutoWString w ( (char16*)LNewConvertCp ( LGI_WideCharset, s, ( sizeof(OsChar) == 1 ? "utf-8" : LGI_WideCharset ), len * sizeof(*s) ) ); Insert(Cursor, w, len); Invalidate(); return DROPEFFECT_COPY; } } else if (dd.IsFileDrop()) { // We don't directly handle file drops... pass up to the parent bool FoundTarget = false; for (LViewI *p = GetParent(); p; p = p->GetParent()) { LDragDropTarget *t = p->DropTarget(); if (t) { Status = t->OnDrop(Data, Pt, KeyState); if (Status != DROPEFFECT_NONE) { FoundTarget = true; break; } } } if (!FoundTarget) { auto Wnd = GetWindow(); if (Wnd) { LDropFiles files(dd); Wnd->OnReceiveFiles(files); } } } } return Status; } void LTextView3::OnCreate() { SetWindow(this); DropTarget(true); #ifndef WINDOWS if (Ctrls.Length() == 0) SetPulse(PULSE_TIMEOUT); Ctrls.Add(this); #else SetPulse(PULSE_TIMEOUT); #endif } void LTextView3::OnEscape(LKey &K) { } bool LTextView3::OnMouseWheel(double l) { if (VScroll) { int64 NewPos = VScroll->Value() + (int)l; NewPos = limit(NewPos, 0, (ssize_t)GetLines()); VScroll->Value(NewPos); Invalidate(); } return true; } void LTextView3::OnFocus(bool f) { Invalidate(); } ssize_t LTextView3::HitText(int x, int y, bool Nearest) { if (!Text) return 0; bool Down = y >= 0; int Y = (VScroll) ? (int)VScroll->Value() : 0; auto It = Line.begin(Y); if (It != Line.end()) y += (*It)->r.y1; while (It != Line.end()) { auto l = *It; if (l->r.Overlap(x, y)) { // Over a line int At = x - l->r.x1; ssize_t Char = 0; LDisplayString Ds(Font, MapText(Text + l->Start, l->Len), l->Len, 0); Char = Ds.CharAt(At, Nearest ? LgiNearest : LgiTruncate); return l->Start + Char; } else if (y >= l->r.y1 && y <= l->r.y2) { // Click horizontally before of after line if (x < l->r.x1) { return l->Start; } else if (x > l->r.x2) { return l->Start + l->Len; } } if (Down) It++; else It--; Y++; } // outside text area if (Down) { It = Line.rbegin(); if (It != Line.end()) { if (y > (*It)->r.y2) { // end of document return Size; } } } return 0; } void LTextView3::Undo() { int Old = UndoQue.GetPos(); UndoQue.Undo(); if (Old && !UndoQue.GetPos()) { Dirty = false; SendNotify(LNotifyDocChanged); } } void LTextView3::Redo() { UndoQue.Redo(); } void LTextView3::DoContextMenu(LMouse &m) { LSubMenu RClick; LAutoString ClipText; { LClipBoard Clip(this); ClipText.Reset(NewStr(Clip.Text())); } LStyle *s = HitStyle(HitText(m.x, m.y, true)); if (s) { if (OnStyleMenu(s, &RClick)) { RClick.AppendSeparator(); } } RClick.AppendItem(LLoadString(L_TEXTCTRL_CUT, "Cut"), IDM_CUT, HasSelection()); RClick.AppendItem(LLoadString(L_TEXTCTRL_COPY, "Copy"), IDM_COPY, HasSelection()); RClick.AppendItem(LLoadString(L_TEXTCTRL_PASTE, "Paste"), IDM_PASTE, ClipText != 0); RClick.AppendSeparator(); RClick.AppendItem("Copy All", IDM_COPY_ALL, true); RClick.AppendItem("Select All", IDM_SELECT_ALL, true); RClick.AppendSeparator(); RClick.AppendItem(LLoadString(L_TEXTCTRL_UNDO, "Undo"), IDM_UNDO, UndoQue.CanUndo()); RClick.AppendItem(LLoadString(L_TEXTCTRL_REDO, "Redo"), IDM_REDO, UndoQue.CanRedo()); RClick.AppendSeparator(); auto i = RClick.AppendItem(LLoadString(L_TEXTCTRL_FIXED, "Fixed Width Font"), IDM_FIXED, true); if (i) i->Checked(GetFixedWidthFont()); i = RClick.AppendItem(LLoadString(L_TEXTCTRL_AUTO_INDENT, "Auto Indent"), IDM_AUTO_INDENT, true); if (i) i->Checked(AutoIndent); i = RClick.AppendItem(LLoadString(L_TEXTCTRL_SHOW_WHITESPACE, "Show Whitespace"), IDM_SHOW_WHITE, true); if (i) i->Checked(ShowWhiteSpace); i = RClick.AppendItem(LLoadString(L_TEXTCTRL_HARD_TABS, "Hard Tabs"), IDM_HARD_TABS, true); if (i) i->Checked(HardTabs); RClick.AppendItem(LLoadString(L_TEXTCTRL_INDENT_SIZE, "Indent Size"), IDM_INDENT_SIZE, true); RClick.AppendItem(LLoadString(L_TEXTCTRL_TAB_SIZE, "Tab Size"), IDM_TAB_SIZE, true); if (Environment) Environment->AppendItems(&RClick, NULL); int Id = 0; m.ToScreen(); switch (Id = RClick.Float(this, m)) { case IDM_FIXED: { SetFixedWidthFont(!GetFixedWidthFont()); SendNotify(LNotifyFixedWidthChanged); break; } case IDM_CUT: { Cut(); break; } case IDM_COPY: { Copy(); break; } case IDM_PASTE: { Paste(); break; } case IDM_COPY_ALL: { SelectAll(); Copy(); break; } case IDM_SELECT_ALL: { SelectAll(); break; } case IDM_UNDO: { Undo(); break; } case IDM_REDO: { Redo(); break; } case IDM_AUTO_INDENT: { AutoIndent = !AutoIndent; break; } case IDM_SHOW_WHITE: { ShowWhiteSpace = !ShowWhiteSpace; Invalidate(); break; } case IDM_HARD_TABS: { HardTabs = !HardTabs; break; } case IDM_INDENT_SIZE: { char s[32]; sprintf_s(s, sizeof(s), "%i", IndentSize); LInput *i = new LInput(this, s, "Indent Size:", "Text"); i->DoModal([this, i](auto dlg, auto code) { if (code) IndentSize = atoi(i->GetStr()); delete i; }); break; } case IDM_TAB_SIZE: { char s[32]; sprintf_s(s, sizeof(s), "%i", TabSize); LInput *i = new LInput(this, s, "Tab Size:", "Text"); i->DoModal([this, i](auto dlg, auto code) { SetTabSize(atoi(i->GetStr())); delete i; }); break; } default: { if (s) { OnStyleMenuClick(s, Id); } if (Environment) { Environment->OnMenu(this, Id, 0); } break; } } } bool LTextView3::OnStyleClick(LStyle *style, LMouse *m) { switch (style->Owner) { case STYLE_URL: { if ( (!m) || (m->Left() && m->Down() && m->Double()) ) { LString s(Text + style->Start, style->Len); if (s) OnUrl(s); return true; } break; } default: break; } return false; } bool LTextView3::OnStyleMenu(LStyle *style, LSubMenu *m) { switch (style->Owner) { case STYLE_URL: { LString s(Text + style->Start, style->Len); if (LIsValidEmail(s)) m->AppendItem(LLoadString(L_TEXTCTRL_EMAIL_TO, "New Email to..."), IDM_NEW, true); else m->AppendItem(LLoadString(L_TEXTCTRL_OPENURL, "Open URL"), IDM_OPEN, true); m->AppendItem(LLoadString(L_TEXTCTRL_COPYLINK, "Copy link location"), IDM_COPY_URL, true); return true; } default: break; } return false; } void LTextView3::OnStyleMenuClick(LStyle *style, int i) { switch (style->Owner) { case STYLE_URL: { LString s(Text + style->Start, style->Len); switch (i) { case IDM_NEW: case IDM_OPEN: { if (s) OnUrl(s); break; } case IDM_COPY_URL: { if (s) { LClipBoard Clip(this); Clip.Text(s); } break; } } break; } default: break; } } void LTextView3::OnMouseClick(LMouse &m) { bool Processed = false; m.x += ScrollX; if (m.Down()) { if (m.IsContextMenu()) { DoContextMenu(m); return; } else if (m.Left()) { Focus(true); ssize_t Hit = HitText(m.x, m.y, true); if (Hit >= 0) { SetCaret(Hit, m.Shift()); LStyle *s = HitStyle(Hit); if (s) Processed = OnStyleClick(s, &m); } if (!Processed && m.Double()) { d->WordSelectMode = Cursor; SelectWord(Cursor); } else { d->WordSelectMode = -1; } } } if (!Processed) { Capture(m.Down()); } } int LTextView3::OnHitTest(int x, int y) { #ifdef WIN32 if (GetClient().Overlap(x, y)) { return HTCLIENT; } #endif return LView::OnHitTest(x, y); } void LTextView3::OnMouseMove(LMouse &m) { m.x += ScrollX; ssize_t Hit = HitText(m.x, m.y, true); if (IsCapturing()) { if (d->WordSelectMode < 0) { SetCaret(Hit, m.Left()); } else { ssize_t Min = Hit < d->WordSelectMode ? Hit : d->WordSelectMode; ssize_t Max = Hit > d->WordSelectMode ? Hit : d->WordSelectMode; for (SelStart = Min; SelStart > 0; SelStart--) { if (strchr(SelectWordDelim, Text[SelStart])) { SelStart++; break; } } for (SelEnd = Max; SelEnd < Size; SelEnd++) { if (strchr(SelectWordDelim, Text[SelEnd])) { break; } } Cursor = SelEnd; Invalidate(); } } } LCursor LTextView3::GetCursor(int x, int y) { LRect c = GetClient(); c.Offset(-c.x1, -c.y1); LStyle *s = NULL; if (c.Overlap(x, y)) { ssize_t Hit = HitText(x, y, true); s = HitStyle(Hit); } return s ? s->Cursor : LCUR_Ibeam; } int LTextView3::GetColumn() { int x = 0; LTextLine *l = GetTextLine(Cursor); if (l) { for (ssize_t i=l->Start; i> 1); m.Target = this; DoContextMenu(m); } else if (k.IsChar) { switch (k.vkey) { default: { // process single char input if ( !GetReadOnly() && ( (k.c16 >= ' ' || k.vkey == LK_TAB) && k.c16 != 127 ) ) { if (k.Down()) { // letter/number etc if (SelStart >= 0) { bool MultiLine = false; if (k.vkey == LK_TAB) { size_t Min = MIN(SelStart, SelEnd), Max = MAX(SelStart, SelEnd); for (size_t i=Min; iLen : 0; if (l && k.vkey == LK_TAB && (!HardTabs || IndentSize != TabSize)) { int x = GetColumn(); int Add = IndentSize - (x % IndentSize); if (HardTabs && ((x + Add) % TabSize) == 0) { int Rx = x; size_t Remove; for (Remove = Cursor; Text[Remove - 1] == ' ' && Rx % TabSize != 0; Remove--, Rx--); ssize_t Chars = (ssize_t)Cursor - Remove; Delete(Remove, Chars); Insert(Remove, &k.c16, 1); Cursor = Remove + 1; Invalidate(); } else { char16 *Sp = new char16[Add]; if (Sp) { for (int n=0; nLen : 0; SetCaret(Cursor + Add, false, Len != NewLen - 1); } DeleteArray(Sp); } } } else { char16 In = k.GetChar(); if (In == '\t' && k.Shift() && Cursor > 0) { l = GetTextLine(Cursor); if (Cursor > l->Start) { if (Text[Cursor-1] == '\t') { Delete(Cursor - 1, 1); SetCaret(Cursor, false, false); } else if (Text[Cursor-1] == ' ') { ssize_t Start = (ssize_t)Cursor - 1; while (Start >= l->Start && strchr(" \t", Text[Start-1])) Start--; int Depth = SpaceDepth(Text + Start, Text + Cursor); int NewDepth = Depth - (Depth % IndentSize); if (NewDepth == Depth && NewDepth > 0) NewDepth -= IndentSize; int Use = 0; while (SpaceDepth(Text + Start, Text + Start + Use + 1) < NewDepth) Use++; Delete(Start + Use, Cursor - Start - Use); SetCaret(Start + Use, false, false); } } } else if (In && Insert(Cursor, &In, 1)) { l = GetTextLine(Cursor); size_t NewLen = (l) ? l->Len : 0; SetCaret(Cursor + 1, false, Len != NewLen - 1); } } } return true; } break; } case LK_RETURN: #if defined MAC case LK_KEYPADENTER: #endif { if (GetReadOnly()) break; if (k.Down() && k.IsChar) { OnEnter(k); } return true; break; } case LK_BACKSPACE: { if (GetReadOnly()) break; if (k.Ctrl()) { // Ctrl+H } else if (k.Down()) { if (SelStart >= 0) { // delete selection DeleteSelection(); } else { char Del = Cursor > 0 ? Text[Cursor-1] : 0; if (Del == ' ' && (!HardTabs || IndentSize != TabSize)) { // Delete soft tab int x = GetColumn(); int Max = x % IndentSize; if (Max == 0) Max = IndentSize; ssize_t i; for (i=Cursor-1; i>=0; i--) { if (Max-- <= 0 || Text[i] != ' ') { i++; break; } } if (i < 0) i = 0; if (i < Cursor - 1) { ssize_t Del = (ssize_t)Cursor - i; Delete(i, Del); // SetCursor(i, false, false); // Invalidate(); break; } } else if (Del == '\t' && HardTabs && IndentSize != TabSize) { int x = GetColumn(); Delete(--Cursor, 1); for (int c=GetColumn(); c 0) { Delete(Cursor - 1, 1); } } } return true; break; } } } else // not a char { switch (k.vkey) { case LK_TAB: return true; case LK_RETURN: { return !GetReadOnly(); } case LK_BACKSPACE: { if (!GetReadOnly()) { if (k.Alt()) { if (k.Down()) { if (k.Ctrl()) { Redo(); } else { Undo(); } } } else if (k.Ctrl()) { if (k.Down()) { ssize_t Start = Cursor; while (IsWhiteSpace(Text[Cursor-1]) && Cursor > 0) Cursor--; while (!IsWhiteSpace(Text[Cursor-1]) && Cursor > 0) Cursor--; Delete(Cursor, Start - Cursor); Invalidate(); } } return true; } break; } case LK_F3: { if (k.Down()) { DoFindNext(NULL); } return true; break; } case LK_LEFT: { if (k.Down()) { if (SelStart >= 0 && !k.Shift()) { SetCaret(MIN(SelStart, SelEnd), false); } else if (Cursor > 0) { ssize_t n = Cursor; #ifdef MAC if (k.System()) { goto Jump_StartOfLine; } else if (k.Alt()) #else if (k.Ctrl()) #endif { // word move/select bool StartWhiteSpace = IsWhiteSpace(Text[n]); bool LeftWhiteSpace = n > 0 && IsWhiteSpace(Text[n-1]); if (!StartWhiteSpace || Text[n] == '\n') { n--; } // Skip ws for (; n > 0 && strchr(" \t", Text[n]); n--) ; if (Text[n] == '\n') { n--; } else if (!StartWhiteSpace || !LeftWhiteSpace) { if (IsDelimiter(Text[n])) { for (; n > 0 && IsDelimiter(Text[n]); n--); } else { for (; n > 0; n--) { //IsWordBoundry(Text[n]) if (IsWhiteSpace(Text[n]) || IsDelimiter(Text[n])) { break; } } } } if (n > 0) n++; } else { // single char n--; } SetCaret(n, k.Shift()); } } return true; break; } case LK_RIGHT: { if (k.Down()) { if (SelStart >= 0 && !k.Shift()) { SetCaret(MAX(SelStart, SelEnd), false); } else if (Cursor < Size) { ssize_t n = Cursor; #ifdef MAC if (k.System()) { goto Jump_EndOfLine; } else if (k.Alt()) #else if (k.Ctrl()) #endif { // word move/select if (IsWhiteSpace(Text[n])) { for (; nStart, Cursor-l->Start); int ScreenX = CurLine.X(); LDisplayString PrevLine(Font, Text + Prev->Start, Prev->Len); ssize_t CharX = PrevLine.CharAt(ScreenX); SetCaret(Prev->Start + MIN(CharX, Prev->Len), k.Shift()); } } } return true; break; } case LK_DOWN: { if (k.Alt()) return false; if (k.Down()) { #ifdef MAC if (k.Ctrl()) goto LTextView3_PageDown; #endif auto It = GetTextLineIt(Cursor); if (It != Line.end()) { auto l = *It; It++; if (It != Line.end()) { LTextLine *Next = *It; LDisplayString CurLine(Font, Text + l->Start, Cursor-l->Start); int ScreenX = CurLine.X(); LDisplayString NextLine(Font, Text + Next->Start, Next->Len); ssize_t CharX = NextLine.CharAt(ScreenX); SetCaret(Next->Start + MIN(CharX, Next->Len), k.Shift()); } } } return true; break; } case LK_END: { if (k.Down()) { if (k.Ctrl()) { SetCaret(Size, k.Shift()); } else { #ifdef MAC Jump_EndOfLine: #endif LTextLine *l = GetTextLine(Cursor); if (l) { SetCaret(l->Start + l->Len, k.Shift()); } } } return true; break; } case LK_HOME: { if (k.Down()) { if (k.Ctrl()) { SetCaret(0, k.Shift()); } else { #ifdef MAC Jump_StartOfLine: #endif LTextLine *l = GetTextLine(Cursor); if (l) { char16 *Line = Text + l->Start; char16 *s; char16 SpTab[] = {' ', '\t', 0}; for (s = Line; (SubtractPtr(s,Line) < l->Len) && StrchrW(SpTab, *s); s++); ssize_t Whitespace = SubtractPtr(s, Line); if (l->Start + Whitespace == Cursor) { SetCaret(l->Start, k.Shift()); } else { SetCaret(l->Start + Whitespace, k.Shift()); } } } } return true; break; } case LK_PAGEUP: { #ifdef MAC LTextView3_PageUp: #endif if (k.Down()) { LTextLine *l = GetTextLine(Cursor); if (l) { int DisplayLines = Y() / LineY; ssize_t CurLine = Line.IndexOf(l); LTextLine *New = Line.ItemAt(MAX(CurLine - DisplayLines, 0)); if (New) { SetCaret(New->Start + MIN(Cursor - l->Start, New->Len), k.Shift()); } } } return true; break; } case LK_PAGEDOWN: { #ifdef MAC LTextView3_PageDown: #endif if (k.Down()) { LTextLine *l = GetTextLine(Cursor); if (l) { int DisplayLines = Y() / LineY; ssize_t CurLine = Line.IndexOf(l); LTextLine *New = Line.ItemAt(MIN(CurLine + DisplayLines, (ssize_t)GetLines()-1)); if (New) { SetCaret(New->Start + MIN(Cursor - l->Start, New->Len), k.Shift()); } } } return true; break; } case LK_INSERT: { if (k.Down()) { if (k.Ctrl()) { Copy(); } else if (k.Shift()) { if (!GetReadOnly()) { Paste(); } } } return true; break; } case LK_DELETE: { if (!GetReadOnly()) { if (k.Down()) { if (SelStart >= 0) { if (k.Shift()) { Cut(); } else { DeleteSelection(); } } else if (Cursor < Size && Delete(Cursor, 1)) { Invalidate(); } } return true; } break; } default: { if (k.c16 == 17) break; if (k.CtrlCmd() && !k.Alt()) { switch (k.GetChar()) { case 0xbd: // Ctrl+'-' { if (k.Down() && Font->PointSize() > 1) { Font->PointSize(Font->PointSize() - 1); OnFontChange(); Invalidate(); } break; } case 0xbb: // Ctrl+'+' { if (k.Down() && Font->PointSize() < 100) { Font->PointSize(Font->PointSize() + 1); OnFontChange(); Invalidate(); } break; } case 'a': case 'A': { if (k.Down()) { // select all SelStart = 0; SelEnd = Size; Invalidate(); } return true; break; } case 'y': case 'Y': { if (!GetReadOnly()) { if (k.Down()) { Redo(); } return true; } break; } case 'z': case 'Z': { if (!GetReadOnly()) { if (k.Down()) { if (k.Shift()) { Redo(); } else { Undo(); } } return true; } break; } case 'x': case 'X': { if (!GetReadOnly()) { if (k.Down()) { Cut(); } return true; } break; } case 'c': case 'C': { if (k.Shift()) return false; if (k.Down()) Copy(); return true; break; } case 'v': case 'V': { if (!k.Shift() && !GetReadOnly()) { if (k.Down()) { Paste(); } return true; } break; } case 'f': { if (k.Down()) { DoFind(NULL); } return true; break; } case 'g': case 'G': { if (k.Down()) { DoGoto(NULL); } return true; break; } case 'h': case 'H': { if (k.Down()) { DoReplace(NULL); } return true; break; } case 'u': case 'U': { if (!GetReadOnly()) { if (k.Down()) { DoCase(NULL, k.Shift()); } return true; } break; } case LK_RETURN: { if (!GetReadOnly() && !k.Shift()) { if (k.Down()) { OnEnter(k); } return true; } break; } } } break; } } } return false; } void LTextView3::OnEnter(LKey &k) { // enter if (SelStart >= 0) { DeleteSelection(); } char16 InsertStr[256] = {'\n', 0}; LTextLine *CurLine = GetTextLine(Cursor); if (CurLine && AutoIndent) { int WsLen = 0; for (; WsLen < CurLine->Len && WsLen < (Cursor - CurLine->Start) && strchr(" \t", Text[CurLine->Start + WsLen]); WsLen++); if (WsLen > 0) { memcpy(InsertStr+1, Text+CurLine->Start, WsLen * sizeof(char16)); InsertStr[WsLen+1] = 0; } } if (Insert(Cursor, InsertStr, StrlenW(InsertStr))) { SetCaret(Cursor + StrlenW(InsertStr), false, true); } } int LTextView3::TextWidth(LFont *f, char16 *s, int Len, int x, int Origin) { int w = x; int Size = f->TabSize(); for (char16 *c = s; SubtractPtr(c, s) < Len; ) { if (*c == 9) { w = ((((w-Origin) + Size) / Size) * Size) + Origin; c++; } else { char16 *e; for (e = c; SubtractPtr(e, s) < Len && *e != 9; e++); LDisplayString ds(f, c, SubtractPtr(e, c)); w += ds.X(); c = e; } } return w - x; } int LTextView3::ScrollYLine() { return (VScroll) ? (int)VScroll->Value() : 0; } int LTextView3::ScrollYPixel() { return ScrollYLine() * LineY; } LRect LTextView3::DocToScreen(LRect r) { r.Offset(0, d->rPadding.y1 - ScrollYPixel()); return r; } void LTextView3::OnPaintLeftMargin(LSurface *pDC, LRect &r, LColour &colour) { pDC->Colour(colour); pDC->Rectangle(&r); } void LTextView3::OnPaint(LSurface *pDC) { #if LGI_EXCEPTIONS try { #endif #if DOUBLE_BUFFER_PAINT LDoubleBuffer MemBuf(pDC); #endif #if PROFILE_PAINT char s[256]; sprintf_s(s, sizeof(s), "%p::OnPaint Lines=%i Sz=%i", this, (int)Line.Length(), (int)Size); LProfile Prof(s); #endif if (d->LayoutDirty) { #if PROFILE_PAINT Prof.Add("PourText"); #endif PourText(d->DirtyStart, d->DirtyLen); #if PROFILE_PAINT Prof.Add("PourStyle"); #endif PourStyle(d->DirtyStart, d->DirtyLen); d->LayoutDirty = false; } #if PROFILE_PAINT Prof.Add("Setup"); #endif LRect r = GetClient(); r.x2 += ScrollX; int Ox, Oy; pDC->GetOrigin(Ox, Oy); pDC->SetOrigin(Ox+ScrollX, Oy); #if 0 // Coverage testing... pDC->Colour(Rgb24(255, 0, 255), 24); pDC->Rectangle(); #endif LSurface *pOut = pDC; bool DrawSel = false; bool HasFocus = Focus(); // printf("%s:%i - HasFocus = %i\n", _FL, HasFocus); LColour SelectedText(HasFocus ? LColour(L_FOCUS_SEL_FORE) : LColour(L_NON_FOCUS_SEL_FORE)); LColour SelectedBack(HasFocus ? LColour(L_FOCUS_SEL_BACK) : LColour(L_NON_FOCUS_SEL_BACK)); LCss::ColorDef ForeDef, BkDef; if (GetCss()) { ForeDef = GetCss()->Color(); BkDef = GetCss()->BackgroundColor(); } LColour Fore(ForeDef.Type == LCss::ColorRgb ? LColour(ForeDef.Rgb32, 32) : LColour(L_TEXT)); LColour Back ( /*!ReadOnly &&*/ BkDef.Type == LCss::ColorRgb ? LColour(BkDef.Rgb32, 32) : Enabled() ? LColour(L_WORKSPACE) : LColour(L_MED) ); // LColour Whitespace = Fore.Mix(Back, 0.85f); if (!Enabled()) { Fore = LColour(L_LOW); Back = LColour(L_MED); } if (Text && Font) { ssize_t SelMin = MIN(SelStart, SelEnd); ssize_t SelMax = MAX(SelStart, SelEnd); // font properties Font->Colour(Fore, Back); // Font->WhitespaceColour(Whitespace); Font->Transparent(false); // draw margins pDC->Colour(PAINT_BORDER); // top margin pDC->Rectangle(0, 0, r.x2, d->rPadding.y1-1); // left margin { LRect LeftMargin(0, d->rPadding.y1, d->rPadding.x1-1, r.y2); OnPaintLeftMargin(pDC, LeftMargin, PAINT_BORDER); } // draw lines of text int k = ScrollYLine(); auto It = Line.begin(k); LTextLine *l = NULL; int Dy = 0; if (It != Line.end()) Dy = -(*It)->r.y1; ssize_t NextSelection = (SelStart != SelEnd) ? SelMin : -1; // offset where selection next changes if (It != Line.end() && (l = *It) && SelStart >= 0 && SelStart < l->Start && SelEnd > l->Start) { // start of visible area is in selection // init to selection colour DrawSel = true; Font->Colour(SelectedText, SelectedBack); NextSelection = SelMax; } StyleIter Si = Style.begin(); LStyle *NextStyle = GetNextStyle(Si, (l) ? l->Start : 0); DocOffset = (l) ? l->r.y1 : 0; #if PROFILE_PAINT Prof.Add("foreach Line loop"); #endif // loop through all visible lines int y = d->rPadding.y1; while ((l = *It) && l->r.y1+Dy < r.Y()) { LRect Tr = l->r; Tr.Offset(0, y - Tr.y1); //LRect OldTr = Tr; // deal with selection change on beginning of line if (NextSelection == l->Start) { // selection change DrawSel = !DrawSel; NextSelection = (NextSelection == SelMin) ? SelMax : -1; } if (DrawSel) { Font->Colour(SelectedText, SelectedBack); } else { LColour fore = l->c.IsValid() ? l->c : Fore; LColour back = l->Back.IsValid() ? l->Back : Back; Font->Colour(fore, back); } // How many chars on this line have we // processed so far: ssize_t Done = 0; bool LineHasSelection = NextSelection >= l->Start && NextSelection < l->Start + l->Len; // Fractional pixels we have moved so far: int MarginF = d->rPadding.x1 << LDisplayString::FShift; int FX = MarginF; int FY = Tr.y1 << LDisplayString::FShift; // loop through all sections of similar text on a line while (Done < l->Len) { // decide how big this block is int RtlTrailingSpace = 0; ssize_t Cur = l->Start + Done; ssize_t Block = l->Len - Done; // check for style change if (NextStyle && (ssize_t)NextStyle->End() <= l->Start) NextStyle = GetNextStyle(Si); if (NextStyle) { // start if (l->Overlap(NextStyle->Start) && NextStyle->Start > Cur && NextStyle->Start - Cur < Block) { Block = NextStyle->Start - Cur; } // end ssize_t StyleEnd = NextStyle->Start + NextStyle->Len; if (l->Overlap(StyleEnd) && StyleEnd > Cur && StyleEnd - Cur < Block) { Block = StyleEnd - Cur; } } // check for next selection change // this may truncate the style if (NextSelection > Cur && NextSelection - Cur < Block) { Block = NextSelection - Cur; } LAssert(Block != 0); // sanity check if (NextStyle && // There is a style (Cur < SelMin || Cur >= SelMax) && // && we're not drawing a selection block Cur >= NextStyle->Start && // && we're inside the styled area Cur < NextStyle->Start+NextStyle->Len) { LFont *Sf = NextStyle->Font ? NextStyle->Font : Font; if (Sf) { // draw styled text if (NextStyle->Fore.IsValid()) Sf->Fore(NextStyle->Fore); if (NextStyle->Back.IsValid()) Sf->Back(NextStyle->Back); else if (l->Back.IsValid()) Sf->Back(l->Back); else Sf->Back(Back); Sf->Transparent(false); LAssert(l->Start + Done >= 0); LDisplayString Ds( Sf, MapText(Text + (l->Start + Done), Block, RtlTrailingSpace != 0), Block + RtlTrailingSpace); Ds.SetDrawOffsetF(FX - MarginF); Ds.ShowVisibleTab(ShowWhiteSpace); Ds.FDraw(pOut, FX, FY, 0, LineHasSelection); if (NextStyle->Decor == LCss::TextDecorSquiggle) { pOut->Colour(NextStyle->DecorColour); int x = FX >> LDisplayString::FShift; int End = x + Ds.X(); while (x < End) { pOut->Set(x, Tr.y2-(x%2)); x++; } } FX += Ds.FX(); LColour fore = l->c.IsValid() ? l->c : Fore; LColour back = l->Back.IsValid() ? l->Back : Back; Sf->Colour(fore, back); } else LAssert(0); } else { // draw a block of normal text LAssert(l->Start + Done >= 0); LDisplayString Ds( Font, MapText(Text + (l->Start + Done), Block, RtlTrailingSpace != 0), Block + RtlTrailingSpace); Ds.SetDrawOffsetF(FX - MarginF); Ds.ShowVisibleTab(ShowWhiteSpace); Ds.FDraw(pOut, FX, FY, 0, LineHasSelection); FX += Ds.FX(); } if (NextStyle && Cur+Block >= NextStyle->Start+NextStyle->Len) { // end of this styled block NextStyle = GetNextStyle(Si); } if (NextSelection == Cur+Block) { // selection change DrawSel = !DrawSel; if (DrawSel) { Font->Colour(SelectedText, SelectedBack); } else { LColour fore = l->c.IsValid() ? l->c : Fore; LColour back = l->Back.IsValid() ? l->Back : Back; Font->Colour(fore, back); } NextSelection = (NextSelection == SelMin) ? SelMax : -1; } Done += Block + RtlTrailingSpace; } // end block loop Tr.x1 = FX >> LDisplayString::FShift; // eol processing ssize_t EndOfLine = l->Start+l->Len; if (EndOfLine >= SelMin && EndOfLine < SelMax) { // draw the '\n' at the end of the line as selected // LColour bk = Font->Back(); pOut->Colour(Font->Back()); pOut->Rectangle(Tr.x2, Tr.y1, Tr.x2+7, Tr.y2); Tr.x2 += 7; } else Tr.x2 = Tr.x1; // draw any space after text pOut->Colour(PAINT_AFTER_LINE); pOut->Rectangle(Tr.x2, Tr.y1, r.x2, Tr.y2); // cursor? if (HasFocus) { // draw the cursor if on this line if (Cursor >= l->Start && Cursor <= l->Start+l->Len) { CursorPos.ZOff(1, LineY-1); ssize_t At = Cursor-l->Start; LDisplayString Ds(Font, MapText(Text+l->Start, At), At); Ds.ShowVisibleTab(ShowWhiteSpace); int CursorX = Ds.X(); CursorPos.Offset(d->rPadding.x1 + CursorX, Tr.y1); if (CanScrollX) { // Cursor on screen check LRect Scr = GetClient(); Scr.Offset(ScrollX, 0); LRect Cur = CursorPos; if (Cur.x2 > Scr.x2 - 5) // right edge check { ScrollX = ScrollX + Cur.x2 - Scr.x2 + 40; Invalidate(); } else if (Cur.x1 < Scr.x1 && ScrollX > 0) { ScrollX = MAX(0, Cur.x1 - 40); Invalidate(); } } if (Blink) { LRect c = CursorPos; pOut->Colour(!ReadOnly ? Fore : LColour(192, 192, 192)); pOut->Rectangle(&c); } #if WINNATIVE HIMC hIMC = ImmGetContext(Handle()); if (hIMC) { COMPOSITIONFORM Cf; Cf.dwStyle = CFS_POINT; Cf.ptCurrentPos.x = CursorPos.x1; Cf.ptCurrentPos.y = CursorPos.y1; ImmSetCompositionWindow(hIMC, &Cf); ImmReleaseContext(Handle(), hIMC); } #endif } } #if DRAW_LINE_BOXES { uint Style = pDC->LineStyle(LSurface::LineAlternate); LColour Old = pDC->Colour(LColour::Red); pDC->Box(&OldTr); pDC->Colour(Old); pDC->LineStyle(Style); LString s; s.Printf("%i, %i", Line.IndexOf(l), l->Start); LDisplayString ds(LSysFont, s); LSysFont->Transparent(true); ds.Draw(pDC, OldTr.x2 + 2, OldTr.y1); } #endif y += LineY; It++; } // end of line loop // draw any space under the lines if (y <= r.y2) { pDC->Colour(Back); // pDC->Colour(LColour(255, 0, 255)); pDC->Rectangle(d->rPadding.x1, y, r.x2, r.y2); } } else { // default drawing: nothing pDC->Colour(Back); pDC->Rectangle(&r); } // _PaintTime = LCurrentTime() - StartTime; #ifdef PAINT_DEBUG if (GetNotify()) { char s[256]; sprintf_s(s, sizeof(s), "Pour:%i Style:%i Paint:%i ms", _PourTime, _StyleTime, _PaintTime); LMessage m = CreateMsg(DEBUG_TIMES_MSG, 0, (int)s); GetNotify()->OnEvent(&m); } #endif // printf("PaintTime: %ims\n", _PaintTime); #if LGI_EXCEPTIONS } catch (...) { LgiMsg(this, "LTextView3::OnPaint crashed.", "Lgi"); } #endif } LMessage::Result LTextView3::OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_TEXT_UPDATE_NAME: { if (d->Lock(_FL)) { Name(d->SetName); d->SetName.Empty(); d->Unlock(); } break; } case M_TEXTVIEW_FIND: { if (InThread()) DoFindNext(NULL); else LgiTrace("%s:%i - Not in thread.\n", _FL); break; } case M_TEXTVIEW_REPLACE: { // DoReplace(); break; } case M_CUT: { Cut(); break; } case M_COPY: { Copy(); break; } case M_PASTE: { Paste(); break; } #if defined WIN32 case WM_GETTEXTLENGTH: { return Size; } case WM_GETTEXT: { int Chars = (int)Msg->A(); char *Out = (char*)Msg->B(); if (Out) { char *In = (char*)LNewConvertCp(LAnsiToLgiCp(), NameW(), LGI_WideCharset, Chars); if (In) { int Len = (int)strlen(In); memcpy(Out, In, Len); DeleteArray(In); return Len; } } return 0; } /* This is broken... the IME returns garbage in the buffer. :( case WM_IME_COMPOSITION: { if (Msg->b & GCS_RESULTSTR) { HIMC hIMC = ImmGetContext(Handle()); if (hIMC) { int Size = ImmGetCompositionString(hIMC, GCS_RESULTSTR, NULL, 0); char *Buf = new char[Size]; if (Buf) { ImmGetCompositionString(hIMC, GCS_RESULTSTR, Buf, Size); char16 *Utf = (char16*)LNewConvertCp(LGI_WideCharset, Buf, LAnsiToLgiCp(), Size); if (Utf) { Insert(Cursor, Utf, StrlenW(Utf)); DeleteArray(Utf); } DeleteArray(Buf); } ImmReleaseContext(Handle(), hIMC); } return 0; } break; } */ #endif } return LLayout::OnEvent(Msg); } int LTextView3::OnNotify(LViewI *Ctrl, LNotification n) { if (Ctrl->GetId() == IDC_VSCROLL && VScroll) { if (n.Type == LNotifyScrollBarCreate) { UpdateScrollBars(); } Invalidate(); } return 0; } void LTextView3::InternalPulse() { if (!ReadOnly) { uint64 Now = LCurrentTime(); if (!BlinkTs) BlinkTs = Now; else if (Now - BlinkTs > CURSOR_BLINK) { Blink = !Blink; LRect p = CursorPos; p.Offset(-ScrollX, 0); Invalidate(&p); BlinkTs = Now; } } if (PartialPour) PourText(Size, 0); } void LTextView3::OnPulse() { #ifdef WINDOWS InternalPulse(); #else for (auto c: Ctrls) c->InternalPulse(); #endif } void LTextView3::OnUrl(char *Url) { if (Environment) Environment->OnNavigate(this, Url); else { LUri u(Url); bool Email = LIsValidEmail(Url); const char *Proto = Email ? "mailto" : u.sProtocol; LString App = LGetAppForProtocol(Proto); if (App) LExecute(App, Url); else LgiMsg(this, "Failed to find application for protocol '%s'", "Error", MB_OK, Proto); } } bool LTextView3::OnLayout(LViewLayoutInfo &Inf) { Inf.Width.Min = 32; Inf.Width.Max = -1; Inf.Height.Min = (Font ? Font->GetHeight() : 18) + 4; Inf.Height.Max = -1; return true; } /////////////////////////////////////////////////////////////////////////////// class LTextView3_Factory : public LViewFactory { LView *NewView(const char *Class, LRect *Pos, const char *Text) { if (_stricmp(Class, "LTextView3") == 0) { return new LTextView3(-1, 0, 0, 2000, 2000); } return 0; } } TextView3_Factory; diff --git a/src/common/Text/TextView4.cpp b/src/common/Text/TextView4.cpp --- a/src/common/Text/TextView4.cpp +++ b/src/common/Text/TextView4.cpp @@ -1,5510 +1,5510 @@ #ifdef WIN32 #include #include #endif #include #include #include #include "lgi/common/Lgi.h" #include "lgi/common/TextView4.h" #include "lgi/common/Input.h" #include "lgi/common/ScrollBar.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/DisplayString.h" #include "lgi/common/CssTools.h" #include "lgi/common/LgiRes.h" #include "lgi/common/Mail.h" #include "lgi/common/FileSelect.h" #include "lgi/common/Menu.h" #include "lgi/common/DropFiles.h" #include "ViewPriv.h" #ifdef _DEBUG #define FEATURE_HILIGHT_ALL_MATCHES 1 #else #define FEATURE_HILIGHT_ALL_MATCHES 0 #endif #define DefaultCharset "utf-8" #define SubtractPtr(a, b) ((a) - (b)) #define GDCF_UTF8 -1 #define POUR_DEBUG 0 #define PROFILE_POUR 0 #define PROFILE_PAINT 0 #define DRAW_LINE_BOXES 0 #define WRAP_POUR_TIMEOUT 90 // ms #define PULSE_TIMEOUT 250 // ms #define CURSOR_BLINK 1000 // ms #define ALLOC_BLOCK 64 #define IDC_VS 1000 enum Cmds { IDM_COPY_URL = 100, IDM_AUTO_INDENT, IDM_UTF8, IDM_PASTE_NO_CONVERT, IDM_FIXED, IDM_SHOW_WHITE, IDM_HARD_TABS, IDM_INDENT_SIZE, IDM_TAB_SIZE, IDM_DUMP, IDM_RTL, IDM_COPY_ALL, IDM_SELECT_ALL, #ifndef IDM_OPEN IDM_OPEN, #endif #ifndef IDM_NEW IDM_NEW, #endif #ifndef IDM_COPY IDM_COPY, #endif #ifndef IDM_CUT IDM_CUT, #endif #ifndef IDM_PASTE IDM_PASTE, #endif #ifndef IDM_UNDO IDM_UNDO, #endif #ifndef IDM_REDO IDM_REDO, #endif }; #define PAINT_BORDER Back #if DRAW_LINE_BOXES #define PAINT_AFTER_LINE LColour(240, 240, 240) #else #define PAINT_AFTER_LINE Back #endif #define CODEPAGE_BASE 100 #define CONVERT_CODEPAGE_BASE 200 #if !defined(WIN32) && !defined(toupper) #define toupper(c) (((c)>='a'&&(c)<='z') ? (c)-'a'+'A' : (c)) #endif #define THREAD_CHECK() \ if (!InThread()) \ { \ LgiTrace("%s:%i - %s called out of thread.\n", _FL, __FUNCTION__); \ return false; \ } static char SelectWordDelim[] = " \t\n.,()[]<>=?/\\{}\"\';:+=-|!@#$%^&*"; #ifndef WINDOWS static LArray Ctrls; #endif ////////////////////////////////////////////////////////////////////// class GDocFindReplaceParams4 : public LDocFindReplaceParams, public LMutex { public: // Find/Replace History LAutoWString LastFind; LAutoWString LastReplace; bool MatchCase; bool MatchWord; bool SelectionOnly; bool SearchUpwards; GDocFindReplaceParams4() : LMutex("GDocFindReplaceParams4") { MatchCase = false; MatchWord = false; SelectionOnly = false; SearchUpwards = false; } }; class LTextView4Private : public LCss, public LMutex { public: LTextView4 *View; LRect rPadding; int PourX; bool LayoutDirty; ssize_t DirtyStart, DirtyLen; LColour UrlColour; bool CenterCursor; ssize_t WordSelectMode; LString Eol; LString LastError; // If the scroll position is set before we get a scroll bar, store the index // here and set it when the LNotifyScrollBarCreate arrives. ssize_t VScrollCache; // Find/Replace Params bool OwnFindReplaceParams; GDocFindReplaceParams4 *FindReplaceParams; // Map buffer ssize_t MapLen; char16 *MapBuf; // // Thread safe Name(char*) impl LString SetName; // #ifdef _DEBUG LString PourLog; #endif LTextView4Private(LTextView4 *view) : LMutex("LTextView4Private") { View = view; WordSelectMode = -1; PourX = -1; VScrollCache = -1; DirtyStart = DirtyLen = 0; UrlColour.Rgb(0, 0, 255); LColour::GetConfigColour("colour.L_URL", UrlColour); CenterCursor = false; LayoutDirty = true; rPadding.ZOff(0, 0); MapBuf = 0; MapLen = 0; OwnFindReplaceParams = true; FindReplaceParams = new GDocFindReplaceParams4; } ~LTextView4Private() { if (OwnFindReplaceParams) { DeleteObj(FindReplaceParams); } DeleteArray(MapBuf); } void SetDirty(ssize_t Start, ssize_t Len = 0) { LayoutDirty = true; DirtyStart = Start; DirtyLen = Len; } void OnChange(PropType Prop) { if (Prop == LCss::PropPadding || Prop == LCss::PropPaddingLeft || Prop == LCss::PropPaddingRight || Prop == LCss::PropPaddingTop || Prop == LCss::PropPaddingBottom) { LCssTools t(this, View->GetFont()); rPadding.ZOff(0, 0); rPadding = t.ApplyPadding(rPadding); } } }; ////////////////////////////////////////////////////////////////////// enum UndoType { UndoDelete, UndoInsert, UndoChange }; struct Change : public LRange { UndoType Type; LArray Txt; }; struct LTextView4Undo : public LUndoEvent { LTextView4 *View; LArray Changes; LTextView4Undo(LTextView4 *view) { View = view; } void AddChange(ssize_t At, ssize_t Len, UndoType Type) { Change &c = Changes.New(); c.Start = At; c.Len = Len; c.Txt.Add(View->Text + At, Len); c.Type = Type; } void OnChange() { for (auto &c : Changes) { size_t Len = c.Len; if (View->Text) { char16 *t = View->Text + c.Start; for (size_t i=0; id->SetDirty(c.Start, c.Len); } } // LUndoEvent void ApplyChange() { View->UndoOn = false; for (auto &c : Changes) { switch (c.Type) { case UndoInsert: { View->Insert(c.Start, c.Txt.AddressOf(), c.Len); View->Cursor = c.Start + c.Len; break; } case UndoDelete: { View->Delete(c.Start, c.Len); View->Cursor = c.Start; break; } case UndoChange: { OnChange(); break; } } } View->UndoOn = true; View->Invalidate(); } void RemoveChange() { View->UndoOn = false; for (auto &c : Changes) { switch (c.Type) { case UndoInsert: { View->Delete(c.Start, c.Len); break; } case UndoDelete: { View->Insert(c.Start, c.Txt.AddressOf(), c.Len); break; } case UndoChange: { OnChange(); break; } } View->Cursor = c.Start; } View->UndoOn = true; View->Invalidate(); } }; void LTextView4::LStyle::RefreshLayout(size_t Start, ssize_t Len) { View->PourText(Start, Len); View->PourStyle(Start, Len); } ////////////////////////////////////////////////////////////////////// LTextView4::LTextView4( int Id, int x, int y, int cx, int cy, LFontType *FontType) : ResObject(Res_Custom) { // init vars LView::d->Css.Reset(d = new LTextView4Private(this)); PourEnabled = true; PartialPour = false; AdjustStylePos = true; BlinkTs = 0; LineY = 1; MaxX = 0; TextCache = 0; UndoOn = true; UndoCur = NULL; Font = 0; FixedWidthFont = false; FixedFont = 0; ShowWhiteSpace = false; ObscurePassword = false; TabSize = TAB_SIZE; IndentSize = TAB_SIZE; HardTabs = true; CanScrollX = false; Blink = true; // setup window SetId(Id); // default options Dirty = false; #if WINNATIVE CrLf = true; SetDlgCode(DLGC_WANTALLKEYS); #else CrLf = false; #endif Underline = NULL; Bold = NULL; d->Padding(LCss::Len(LCss::LenPx, 2)); #ifdef _DEBUG // debug times _PourTime = 0; _StyleTime = 0; _PaintTime = 0; #endif // Data Alloc = ALLOC_BLOCK; Text = new char16[Alloc]; if (Text) *Text = 0; Cursor = 0; Size = 0; // Display SelStart = SelEnd = -1; DocOffset = 0; ScrollX = 0; if (FontType) { Font = FontType->Create(); } else { LFontType Type; if (Type.GetSystemFont("Fixed")) Font = Type.Create(); else printf("%s:%i - failed to create font.\n", _FL); } if (Font) { SetTabStop(true); Underline = new LFont; if (Underline) { *Underline = *Font; Underline->Underline(true); if (d->UrlColour.IsValid()) Underline->Fore(d->UrlColour); Underline->Create(); } Bold = new LFont; if (Bold) { *Bold = *Font; Bold->Bold(true); Bold->Create(); } OnFontChange(); } else { LgiTrace("%s:%i - Failed to create font, FontType=%p\n", _FL, FontType); Font = LSysFont; } CursorPos.ZOff(1, LineY-1); CursorPos.Offset(d->rPadding.x1, d->rPadding.y1); LRect r; r.ZOff(cx-1, cy-1); r.Offset(x, y); SetPos(r); LResources::StyleElement(this); } LTextView4::~LTextView4() { #ifndef WINDOWS Ctrls.Delete(this); #endif Line.DeleteObjects(); Style.Empty(); DeleteArray(TextCache); DeleteArray(Text); if (Font != LSysFont) DeleteObj(Font); DeleteObj(FixedFont); DeleteObj(Underline); DeleteObj(Bold); // 'd' is owned by the LView::Css auto ptr } char16 *LTextView4::MapText(char16 *Str, ssize_t Len, bool RtlTrailingSpace) { if (ObscurePassword /*|| ShowWhiteSpace*/ || RtlTrailingSpace) { if (Len > d->MapLen) { DeleteArray(d->MapBuf); d->MapBuf = new char16[Len + RtlTrailingSpace]; d->MapLen = Len; } if (d->MapBuf) { int n = 0; if (RtlTrailingSpace) { d->MapBuf[n++] = ' '; for (int i=0; iMapBuf[n++] = Str[i]; } } else if (ObscurePassword) { for (int i=0; iMapBuf[n++] = '*'; } } /* else if (ShowWhiteSpace) { for (int i=0; iMapBuf[n++] = 0xb7; } else if (Str[i] == '\t') { d->MapBuf[n++] = 0x2192; } else { d->MapBuf[n++] = Str[i]; } } } */ return d->MapBuf; } } return Str; } void LTextView4::SetFixedWidthFont(bool i) { if (FixedWidthFont ^ i) { if (i) { LFontType Type; if (Type.GetSystemFont("Fixed")) { LFont *f = FixedFont; FixedFont = Font; Font = f; if (!Font) { Font = Type.Create(); if (Font) { Font->PointSize(FixedFont->PointSize()); } } LDocView::SetFixedWidthFont(i); } } else if (FixedFont) { LFont *f = FixedFont; FixedFont = Font; Font = f; LDocView::SetFixedWidthFont(i); } OnFontChange(); Invalidate(); } } void LTextView4::SetReadOnly(bool i) { LDocView::SetReadOnly(i); #if WINNATIVE SetDlgCode(i ? DLGC_WANTARROWS : DLGC_WANTALLKEYS); #endif } void LTextView4::SetCrLf(bool crlf) { CrLf = crlf; } void LTextView4::SetTabSize(uint8_t i) { TabSize = limit(i, 2, 32); OnFontChange(); OnPosChange(); Invalidate(); } void LTextView4::SetWrapType(LDocWrapType i) { LDocView::SetWrapType(i); CanScrollX = i != TEXTED_WRAP_REFLOW; OnPosChange(); Invalidate(); } LFont *LTextView4::GetFont() { return Font; } LFont *LTextView4::GetBold() { return Bold; } void LTextView4::SetFont(LFont *f, bool OwnIt) { if (!f) return; if (OwnIt) { if (Font != LSysFont) DeleteObj(Font); Font = f; } else if (!Font || Font == LSysFont) { Font = new LFont(*f); } else { *Font = *f; } if (Font) { if (!Underline) Underline = new LFont; if (Underline) { *Underline = *Font; Underline->Underline(true); Underline->Create(); if (d->UrlColour.IsValid()) Underline->Fore(d->UrlColour); } if (!Bold) Bold = new LFont; if (Bold) { *Bold = *Font; Bold->Bold(true); Bold->Create(); } } OnFontChange(); } void LTextView4::OnFontChange() { if (Font) { // get line height // int OldLineY = LineY; if (!Font->Handle()) Font->Create(); LineY = Font->GetHeight(); if (LineY < 1) LineY = 1; // get tab size char Spaces[32]; memset(Spaces, 'A', TabSize); Spaces[TabSize] = 0; LDisplayString ds(Font, Spaces); Font->TabSize(ds.X()); // repour doc d->SetDirty(0, Size); // validate blue underline font if (Underline) { *Underline = *Font; Underline->Underline(true); Underline->Create(); } #if WINNATIVE // Set the IME font. HIMC hIMC = ImmGetContext(Handle()); if (hIMC) { COMPOSITIONFORM Cf; Cf.dwStyle = CFS_POINT; Cf.ptCurrentPos.x = CursorPos.x1; Cf.ptCurrentPos.y = CursorPos.y1; LOGFONT FontInfo; GetObject(Font->Handle(), sizeof(FontInfo), &FontInfo); ImmSetCompositionFont(hIMC, &FontInfo); ImmReleaseContext(Handle(), hIMC); } #endif } } void LTextView4::LogLines() { int Idx = 0; LStringPipe p; p.Print("DocSize: %i\n", (int)Size); for (auto i : Line) { p.Print(" [%i]=%p, %i+%i, %s\n", Idx, i, (int)i->Start, (int)i->Len, i->r.GetStr()); Idx++; } - LgiTrace(p.NewGStr()); + LgiTrace(p.NewLStr()); #ifdef _DEBUG if (d->PourLog) LgiTrace("%s", d->PourLog.Get()); #endif } bool LTextView4::ValidateLines(bool CheckBox) { size_t Pos = 0; char16 *c = Text; size_t Idx = 0; LTextLine *Prev = NULL; for (auto l: Line) { if (l->Start != Pos) { LogLines(); LAssert(!"Incorrect start."); return false; } char16 *e = c; if (WrapType == TEXTED_WRAP_NONE) { while (*e && *e != '\n') e++; } else { char16 *end = Text + l->Start + l->Len; while (*e && *e != '\n' && e < end) e++; } ssize_t Len = e - c; if (l->Len != Len) { LogLines(); LAssert(!"Incorrect length."); return false; } if (CheckBox && Prev && Prev->r.y2 != l->r.y1 - 1) { LogLines(); LAssert(!"Lines not joined vertically"); } if (*e) { if (*e == '\n') e++; else if (WrapType == TEXTED_WRAP_REFLOW) e++; } Pos = e - Text; c = e; Idx++; Prev = l; } if (WrapType == TEXTED_WRAP_NONE && Pos != Size) { LogLines(); LAssert(!"Last line != end of doc"); return false; } return true; } int LTextView4::AdjustStyles(ssize_t Start, ssize_t Diff, bool ExtendStyle) { int Changes = 0; for (auto &s : Style) { if (s.Start == Start) { if (Diff < 0 || ExtendStyle) s.Len += Diff; else s.Start += Diff; Changes++; } else if (s.Start > Start) { s.Start += Diff; Changes++; } } return Changes; } // break array, break out of loop when we hit these chars #define ExitLoop(c) ( (c) == 0 || \ (c) == '\n' || \ (c) == ' ' || \ (c) == '\t' \ ) // extra breaking opportunities #define ExtraBreak(c) ( ( (c) >= 0x3040 && (c) <= 0x30FF ) || \ ( (c) >= 0x3300 && (c) <= 0x9FAF ) \ ) /* Prerequisite: The Line list must have either the objects with the correct Start/Len or be missing the lines altogether... */ void LTextView4::PourText(size_t Start, ssize_t Length /* == 0 means it's a delete */) { #if PROFILE_POUR char _txt[256]; sprintf_s(_txt, sizeof(_txt), "%p::PourText Lines=%i Sz=%i", this, (int)Line.Length(), (int)Size); LProfile Prof(_txt); #endif LAssert(InThread()); LRect Client = GetClient(); int Mx = Client.X() - d->rPadding.x1 - d->rPadding.x2; int Cy = 0; MaxX = 0; ssize_t Idx = -1; LTextLine *Cur = GetTextLine(Start, &Idx); if (!Cur || !Cur->r.Valid()) { // Find the last line that has a valid position... for (int64_t mid, s = 0, e = Idx >= 0 ? Idx : Line.Length() - 1; s < e; ) { if (e - s <= 1) { if (Line[e]->r.Valid()) mid = e; else mid = s; Cur = Line[mid]; Cy = Cur->r.y1; Idx = mid; break; } else mid = s + ((e - s) >> 1); auto sItem = Line[mid]; if (sItem->r.Valid()) s = mid + 1; // Search Mid->e else e = mid - 1; // Search s->Mid } } if (Cur && !Cur->r.Valid()) Cur = NULL; if (Cur) { Cy = Cur->r.y1; Start = Cur->Start; Length = Size - Start; } else { Idx = 0; Start = 0; Length = Size; } if (!Text || !Font || Mx <= 0) return; // Tracking vars ssize_t e; int WrapCol = GetWrapAtCol(); LDisplayString Sp(Font, " ", 1); int WidthOfSpace = Sp.X(); if (WidthOfSpace < 1) { printf("%s:%i - WidthOfSpace test failed.\n", _FL); return; } // Alright... lets pour! uint64 StartTs = LCurrentTime(); if (WrapType == TEXTED_WRAP_NONE) { // Find the dimensions of each line that is missing a rect #if PROFILE_POUR Prof.Add("NoWrap: ExistingLines"); #endif #ifdef _DEGBUG LStringPipe Log(1024); Log.Printf("Pour: " LPrintfSizeT ", " LPrintfSSizeT ", partial=%i\n", Start, Length, PartialPour); #endif ssize_t Pos = 0; for (; Idx < (ssize_t)Line.Length(); Idx++) { LTextLine *l = Line[Idx]; #ifdef _DEGBUG Log.Printf(" [%i] exist: r.val=%i\n", Idx, l->r.Valid()); #endif if (!l->r.Valid()) // If the layout is not valid... { LDisplayString ds(Font, Text + l->Start, l->Len); l->r.x1 = d->rPadding.x1; l->r.x2 = l->r.x1 + ds.X(); MaxX = MAX(MaxX, l->r.X()); } // Adjust the y position anyway... it's free. l->r.y1 = Cy; l->r.y2 = l->r.y1 + LineY - 1; Cy = l->r.y2 + 1; Pos = l->Start + l->Len; if (Text[Pos] == '\n') Pos++; } // Now if we are missing lines as well, create them and lay them out #if PROFILE_POUR Prof.Add("NoWrap: NewLines"); #endif while (Pos < Size) { LTextLine *l = new LTextLine; l->Start = Pos; char16 *c = Text + Pos; char16 *e = c; while (*e && *e != '\n') e++; l->Len = e - c; #ifdef _DEGBUG Log.Printf(" [%i] new: start=" LPrintfSSizeT ", len=" LPrintfSSizeT "\n", Idx, l->Start, l->Len); #endif l->r.x1 = d->rPadding.x1; l->r.y1 = Cy; l->r.y2 = l->r.y1 + LineY - 1; if (l->Len) { LDisplayString ds(Font, Text + l->Start, l->Len); l->r.x2 = l->r.x1 + ds.X(); } else { l->r.x2 = l->r.x1; } LAssert(l->Len > 0); Line.Add(l); if (*e == '\n') e++; MaxX = MAX(MaxX, l->r.X()); Cy = l->r.y2 + 1; Pos = e - Text; Idx++; } #ifdef _DEGBUG - d->PourLog = Log.NewGStr(); + d->PourLog = Log.NewLStr(); #endif PartialPour = false; } else // Wrap text { int DisplayStart = ScrollYLine(); int DisplayLines = (Client.Y() + LineY - 1) / LineY; int DisplayEnd = DisplayStart + DisplayLines; // Pouring is split into 2 parts... // 1) pouring to the end of the displayed text. // 2) pouring from there to the end of the document. // potentially taking several goes to complete the full pour // This allows the document to display and edit faster.. bool PourToDisplayEnd = Line.Length() < DisplayEnd; #if 0 LgiTrace("Idx=%i, DisplayStart=%i, DisplayLines=%i, DisplayEnd=%i, PourToDisplayEnd=%i\n", Idx, DisplayStart, DisplayLines, DisplayEnd, PourToDisplayEnd); #endif if ((ssize_t)Line.Length() > Idx) { for (size_t i=Idx; i= Size || Text[e] == '\n' || (e-i) >= WrapCol) { break; } e++; } // Seek back some characters if we are mid word size_t OldE = e; if (e < Size && Text[e] != '\n') { while (e > i) { if (ExitLoop(Text[e]) || ExtraBreak(Text[e])) { break; } e--; } } if (e == i) { // No line break at all, so seek forward instead for (e=OldE; e < Size && Text[e] != '\n'; e++) { if (ExitLoop(Text[e]) || ExtraBreak(Text[e])) break; } } // Calc the width LDisplayString ds(Font, Text + i, e - i); Width = ds.X(); } else { // Wrap to edge of screen ssize_t PrevExitChar = -1; int PrevX = -1; while (true) { if (e >= Size || ExitLoop(Text[e]) || ExtraBreak(Text[e])) { LDisplayString ds(Font, Text + i, e - i); if (ds.X() + Cx > Mx) { if (PrevExitChar > 0) { e = PrevExitChar; Width = PrevX; } else { Width = ds.X(); } break; } else if (e >= Size || Text[e] == '\n') { Width = ds.X(); break; } PrevExitChar = e; PrevX = ds.X(); } e++; } } // Create layout line LTextLine *l = new LTextLine; if (l) { l->Start = i; l->Len = e - i; l->r.x1 = d->rPadding.x1; l->r.x2 = l->r.x1 + Width - 1; l->r.y1 = Cy; l->r.y2 = l->r.y1 + LineY - 1; LAssert(l->Len > 0); Line.Add(l); if (PourToDisplayEnd) { if (Line.Length() > DisplayEnd) { // We have reached the end of the displayed area... so // exit out temporarily to display the layout to the user PartialPour = true; break; } } else { // Otherwise check if we are taking too long... if (Line.Length() % 20 == 0) { uint64 Now = LCurrentTime(); if (Now - StartTs > WRAP_POUR_TIMEOUT) { PartialPour = true; // LgiTrace("Pour timeout...\n"); break; } } } MaxX = MAX(MaxX, l->r.X()); Cy += LineY; if (e < Size) e++; } } if (i >= Size) PartialPour = false; SendNotify(LNotifyCursorChanged); } #ifdef _DEBUG ValidateLines(true); #endif #if PROFILE_POUR Prof.Add("LastLine"); #endif if (!PartialPour) { auto It = Line.rbegin(); LTextLine *Last = It != Line.end() ? *It : NULL; if (!Last || Last->Start + Last->Len < Size) { auto LastEnd = Last ? Last->End() : 0; LTextLine *l = new LTextLine; if (l) { l->Start = LastEnd; l->Len = Size - LastEnd; l->r.x1 = l->r.x2 = d->rPadding.x1; l->r.y1 = Cy; l->r.y2 = l->r.y1 + LineY - 1; Line.Add(l); MaxX = MAX(MaxX, l->r.X()); Cy += LineY; } } } bool ScrollYNeeded = Client.Y() < (Line.Length() * LineY); bool ScrollChange = ScrollYNeeded ^ (VScroll != NULL); d->LayoutDirty = WrapType != TEXTED_WRAP_NONE && ScrollChange; #if PROFILE_POUR static LString _s; _s.Printf("ScrollBars dirty=%i", d->LayoutDirty); Prof.Add(_s); #endif if (ScrollChange) { // LgiTrace("%s:%i - SetScrollBars(%i) %i %i\n", _FL, ScrollYNeeded, Client.Y(), (Line.Length() * LineY)); SetScrollBars(false, ScrollYNeeded); } UpdateScrollBars(); #if 0 // def _DEBUG if (GetWindow()) { static char s[256]; sprintf_s(s, sizeof(s), "Pour: %.2f sec", (double)_PourTime / 1000); GetWindow()->PostEvent(M_TEXTVIEW_DEBUG_TEXT, (LMessage::Param)s); } #endif #if POUR_DEBUG printf("Lines=%i\n", Line.Length()); int Index = 0; for (LTextLine *l=Line.First(); l; l=Line.Next(), Index++) { printf("\t[%i] %i,%i (%s)\n", Index, l->Start, l->Len, l->r.Describe()); } #endif } bool LTextView4::InsertStyle(LAutoPtr s) { if (!s) return false; LAssert(s->Start >= 0); LAssert(s->Len > 0); ssize_t Last = 0; // int n = 0; // LgiTrace("StartStyle=%i,%i(%i) %s\n", (int)s->Start, (int)s->Len, (int)(s->Start+s->Len), s->Fore.GetStr()); if (Style.Length() > 0) { // Optimize for last in the list auto Last = Style.rbegin(); if (s->Start >= (ssize_t)Last->End()) { Style.Insert(*s); return true; } } for (auto i = Style.begin(); i != Style.end(); i++) { if (s->Overlap(*i)) { if (s->Owner > i->Owner) { // Fail the insert return false; } else { // Replace mode... *i = *s; return true; } } if (s->Start >= Last && s->Start < i->Start) { Style.Insert(*s, i); return true; } } Style.Insert(*s); return true; } LTextView4::LStyle *LTextView4::GetNextStyle(StyleIter &s, ssize_t Where) { if (Where >= 0) s = Style.begin(); else s++; while (s != Style.end()) { // determine whether style is relevant.. // styles in the selected region are ignored ssize_t Min = MIN(SelStart, SelEnd); ssize_t Max = MAX(SelStart, SelEnd); if (SelStart >= 0 && s->Start >= Min && s->Start+s->Len < Max) { // style is completely inside selection: ignore s++; } else if (Where >= 0 && s->Start+s->Len < Where) { s++; } else { return &(*s); } } return NULL; } #if 0 CURSOR_CHAR GetCursor() { #ifdef WIN32 LArray Ver; int Os = LGetOs(&Ver); if ((Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64) && Ver[0] >= 5) { return MAKEINTRESOURCE(32649); // hand } else { return IDC_ARROW; } #endif return 0; } #endif LTextView4::LStyle *LTextView4::HitStyle(ssize_t i) { for (auto &s : Style) { if (i >= s.Start && i < (ssize_t)s.End()) { return &s; } } return NULL; } void LTextView4::PourStyle(size_t Start, ssize_t EditSize) { #ifdef _DEBUG int64 StartTime = LCurrentTime(); #endif LAssert(InThread()); if (!Text || Size < 1) return; ssize_t Length = MAX(EditSize, 0); if ((ssize_t)Start + Length >= Size) Length = Size - Start; // For deletes, this sizes the edit length within bounds. // Expand re-style are to word boundaries before and after the area of change while (Start > 0 && UrlChar(Text[Start-1])) { // Move the start back Start--; Length++; } while ((ssize_t)Start + Length < Size && UrlChar(Text[Start+Length])) { // Move the end back Length++; } // Delete all the styles that we own inside the changed area for (StyleIter s = Style.begin(); s != Style.end();) { if (s->Owner == STYLE_NONE) { if (EditSize > 0) { if (s->Overlap(Start, EditSize < 0 ? -EditSize : EditSize)) { Style.Delete(s); continue; } } else { if (s->Overlap(Start, -EditSize)) { Style.Delete(s); continue; } } } s++; } if (UrlDetect) { LArray Links; LAssert((ssize_t)Start + Length <= Size); if (LDetectLinks(Links, Text + Start, Length)) { for (uint32_t i=0; i Url(new LStyle(STYLE_URL)); if (Url) { Url->View = this; Url->Start = Inf.Start + Start; Url->Len = Inf.Len; Url->Font = Underline; Url->Fore = d->UrlColour; InsertStyle(Url); } } } } #ifdef _DEBUG _StyleTime = LCurrentTime() - StartTime; #endif } bool LTextView4::Insert(size_t At, const char16 *Data, ssize_t Len) { static int count = -1; count++; LProfile Prof("LTextView4::Insert"); Prof.HideResultsIfBelow(1000); LAssert(InThread()); if (!ReadOnly && Len > 0) { if (!Data) return false; // limit input to valid data At = MIN(Size, (ssize_t)At); // make sure we have enough memory size_t NewAlloc = Size + Len + 1; NewAlloc += ALLOC_BLOCK - (NewAlloc % ALLOC_BLOCK); if (NewAlloc != Alloc) { char16 *NewText = new char16[NewAlloc]; if (NewText) { if (Text) { // copy any existing data across memcpy(NewText, Text, (Size + 1) * sizeof(char16)); } DeleteArray(Text); Text = NewText; Alloc = NewAlloc; } else { // memory allocation error return false; } } Prof.Add("MemChk"); if (Text) { // Insert the data // Move the section after the insert to make space... memmove(Text+(At+Len), Text+At, (Size-At) * sizeof(char16)); Prof.Add("Cpy"); // Copy new data in... memcpy(Text+At, Data, Len * sizeof(char16)); Size += Len; Text[Size] = 0; // NULL terminate Prof.Add("Undo"); // Add the undo object... if (UndoOn) { LAutoPtr Obj; if (!UndoCur) Obj.Reset(new LTextView4Undo(this)); auto u = UndoCur ? UndoCur : Obj; if (u) u->AddChange(At, Len, UndoInsert); else LAssert(!"No undo obj?"); if (Obj) UndoQue += Obj.Release(); } // Clear layout info for the new text ssize_t Idx = -1; LTextLine *Cur = NULL; if (Line.Length() == 0) { // Empty doc... set up the first line Line.Add(Cur = new LTextLine); Idx = 0; Cur->Start = 0; } else { Cur = GetTextLine(At, &Idx); } if (Cur) { if (WrapType == TEXTED_WRAP_NONE) { // Clear layout for current line... Cur->r.ZOff(-1, -1); Prof.Add("NoWrap add lines"); // Add any new lines that we need... char16 *e = Text + At + Len; char16 *c; for (c = Text + At; c < e; c++) { if (*c == '\n') { // Set the size of the current line... size_t Pos = c - Text; Cur->Len = Pos - Cur->Start; // Create a new line... Cur = new LTextLine(); if (!Cur) return false; Cur->Start = Pos + 1; Line.AddAt(++Idx, Cur); } } Prof.Add("CalcLen"); // Make sure the last Line's length is set.. Cur->CalcLen(Text); Prof.Add("UpdatePos"); // Now update all the positions of the following lines... for (size_t i = ++Idx; i < Line.Length(); i++) Line[i]->Start += Len; } else { // Clear all lines to the end of the doc... LgiTrace("ClearLines %i\n", (int)Idx+1); for (size_t i = ++Idx; i < Line.Length(); i++) delete Line[i]; Line.Length(Idx); } } else { // If wrap is on then this can happen when an Insert happens before the // OnPulse event has laid out the new text. Probably not a good thing in // non-wrap mode if (WrapType == TEXTED_WRAP_NONE) { LTextLine *l = *Line.rbegin(); printf("%s:%i - Insert error: no cur, At=%i, Size=%i, Lines=%i, WrapType=%i\n", _FL, (int)At, (int)Size, (int)Line.Length(), (int)WrapType); if (l) printf("Last=%i, %i\n", (int)l->Start, (int)l->Len); } } #ifdef _DEBUG // Prof.Add("Validate"); // ValidateLines(); #endif if (AdjustStylePos) AdjustStyles(At, Len); Dirty = true; if (PourEnabled) { Prof.Add("PourText"); PourText(At, Len); Prof.Add("PourStyle"); auto Start = LCurrentTime(); PourStyle(At, Len); auto End = LCurrentTime(); if (End - Start > 1000) { PourStyle(At, Len); } } SendNotify(LNotifyDocChanged); return true; } } return false; } bool LTextView4::Delete(size_t At, ssize_t Len) { bool Status = false; LAssert(InThread()); if (!ReadOnly) { // limit input At = MAX(At, 0); At = MIN((ssize_t)At, Size); Len = MIN(Size-(ssize_t)At, Len); if (Len > 0) { int HasNewLine = 0; for (int i=0; i Obj(new LTextView4Undo(this)); LTextView4Undo *u = UndoCur ? UndoCur : Obj; if (u) u->AddChange(At, Len, UndoDelete); if (Obj) UndoQue += Obj.Release(); } memmove(Text+At, Text+(At+Len), (Size-At-Len) * sizeof(char16)); Size -= Len; Text[Size] = 0; if (WrapType == TEXTED_WRAP_NONE) { ssize_t Idx = -1; LTextLine *Cur = GetTextLine(At, &Idx); if (Cur) { Cur->r.ZOff(-1, -1); // Delete some lines... for (int i=0; iCalcLen(Text); // Shift all further lines down... for (size_t i = Idx + 1; i < Line.Length(); i++) Line[i]->Start -= Len; } } else { ssize_t Index; LTextLine *Cur = GetTextLine(At, &Index); if (Cur) { for (size_t i = Index; i < Line.Length(); i++) delete Line[i]; Line.Length(Index); } } Dirty = true; Status = true; #ifdef _DEBUG ValidateLines(); #endif if (AdjustStylePos) AdjustStyles(At, -Len); if (PourEnabled) { PourText(At, -Len); PourStyle(At, -Len); } if (Cursor >= (ssize_t)At && Cursor <= (ssize_t)At + Len) { SetCaret(At, false, HasNewLine != 0); } // Handle repainting in flowed mode, when the line starts change if (WrapType == TEXTED_WRAP_REFLOW) { ssize_t Index; LTextLine *Cur = GetTextLine(At, &Index); if (Cur) { LRect r = Cur->r; r.x2 = GetClient().x2; r.y2 = GetClient().y2; Invalidate(&r); } } SendNotify(LNotifyDocChanged); Status = true; } } return Status; } void LTextView4::DeleteSelection(char16 **Cut) { if (SelStart >= 0) { ssize_t Min = MIN(SelStart, SelEnd); ssize_t Max = MAX(SelStart, SelEnd); if (Cut) { *Cut = NewStrW(Text + Min, Max - Min); } Delete(Min, Max - Min); SetCaret(Min, false, true); } } LArray::I LTextView4::GetTextLineIt(ssize_t Offset, ssize_t *Index) { if (Line.Length() == 0) { if (Index) *Index = 0; return Line.end(); } if (Offset <= 0) { if (Index) *Index = 0; return Line.begin(); } else if (Line.Length()) { auto l = Line.Last(); if (Offset > l->End()) { if (Index) *Index = Line.Length() - 1; return Line.begin(Line.Length()-1); } } size_t mid = 0, s = 0, e = Line.Length() - 1; while (s < e) { if (e - s <= 1) { if (Line[s]->Overlap(Offset)) mid = s; else if (Line[e]->Overlap(Offset)) mid = e; else { LAssert(!"Needs to match Line s or e..."); break; } } else mid = s + ((e - s) >> 1); auto l = Line[mid]; auto end = l->End(); if (Offset < l->Start) e = mid - 1; else if (Offset > end) s = mid + 1; else { LAssert(Line[mid]->Overlap(Offset)); if (Index) *Index = mid; return Line.begin(mid); } } LAssert(Line[s]->Overlap(Offset)); if (Index) *Index = s; return Line.begin(s); } int64 LTextView4::Value() { auto n = Name(); #ifdef _MSC_VER return (n) ? _atoi64(n) : 0; #else return (n) ? atoll(n) : 0; #endif } void LTextView4::Value(int64 i) { char Str[32]; sprintf_s(Str, sizeof(Str), LPrintfInt64, i); Name(Str); } LString LTextView4::operator[](ssize_t LineIdx) { if (LineIdx <= 0 || LineIdx > (ssize_t)GetLines()) return LString(); LTextLine *Ln = Line[LineIdx-1]; if (!Ln) return LString(); LString s(Text + Ln->Start, Ln->Len); return s; } const char *LTextView4::Name() { UndoQue.Empty(); DeleteArray(TextCache); TextCache = WideToUtf8(Text); return TextCache; } bool LTextView4::Name(const char *s) { if (InThread()) { UndoQue.Empty(); DeleteArray(TextCache); DeleteArray(Text); Line.DeleteObjects(); Style.Empty(); LAssert(LIsUtf8(s)); Text = Utf8ToWide(s); if (!Text) { Text = new char16[1]; if (Text) *Text = 0; } Size = Text ? StrlenW(Text) : 0; Alloc = Size + 1; Cursor = MIN(Cursor, Size); if (Text) { // Remove '\r's char16 *o = Text; for (char16 *i=Text; *i; i++) { if (*i != '\r') { *o++ = *i; } else Size--; } *o++ = 0; } // update everything else d->SetDirty(0, Size); PourText(0, Size); PourStyle(0, Size); UpdateScrollBars(); Invalidate(); } else if (d->Lock(_FL)) { if (IsAttached()) { d->SetName = s; PostEvent(M_TEXT_UPDATE_NAME); } else LAssert(!"Can't post event to detached/virtual window."); d->Unlock(); } return true; } const char16 *LTextView4::NameW() { return Text; } bool LTextView4::NameW(const char16 *s) { DeleteArray(Text); Size = s ? StrlenW(s) : 0; Alloc = Size + 1; Text = new char16[Alloc]; Cursor = MIN(Cursor, Size); if (Text) { memcpy(Text, s, Size * sizeof(char16)); // remove LF's int In = 0, Out = 0; CrLf = false; for (; InSetDirty(0, Size); PourText(0, Size); PourStyle(0, Size); UpdateScrollBars(); Invalidate(); return true; } LRange LTextView4::GetSelectionRange() { LRange r; if (HasSelection()) { r.Start = MIN(SelStart, SelEnd); ssize_t End = MAX(SelStart, SelEnd); r.Len = End - r.Start; } return r; } char *LTextView4::GetSelection() { LRange s = GetSelectionRange(); if (s.Len > 0) { return (char*)LNewConvertCp("utf-8", Text + s.Start, LGI_WideCharset, s.Len*sizeof(Text[0]) ); } return 0; } bool LTextView4::HasSelection() { return (SelStart >= 0) && (SelStart != SelEnd); } void LTextView4::SelectAll() { SelStart = 0; SelEnd = Size; Invalidate(); } void LTextView4::UnSelectAll() { bool Update = HasSelection(); SelStart = -1; SelEnd = -1; if (Update) { Invalidate(); } } size_t LTextView4::GetLines() { return Line.Length(); } void LTextView4::GetTextExtent(int &x, int &y) { PourText(0, Size); x = MaxX + d->rPadding.x1; y = (int)(Line.Length() * LineY); } bool LTextView4::GetLineColumnAtIndex(LPoint &Pt, ssize_t Index) { ssize_t FromIndex = 0; LTextLine *From = GetTextLine(Index < 0 ? Cursor : Index, &FromIndex); if (!From) return false; Pt.x = (int) (Cursor - From->Start); Pt.y = (int) FromIndex; return true; } ssize_t LTextView4::GetCaret(bool Cur) { if (Cur) { return Cursor; } return 0; } ssize_t LTextView4::IndexAt(int x, int y) { LTextLine *l = Line.ItemAt(y); if (l) { return l->Start + MIN(x, l->Len); } return 0; } bool LTextView4::ScrollToOffset(size_t Off) { bool ForceFullUpdate = false; ssize_t ToIndex = 0; LTextLine *To = GetTextLine(Off, &ToIndex); if (To) { LRect Client = GetClient(); int DisplayLines = Client.Y() / LineY; if (VScroll) { if (ToIndex < VScroll->Value()) { // Above the visible region... if (d->CenterCursor) { ssize_t i = ToIndex - (DisplayLines >> 1); VScroll->Value(MAX(0, i)); } else { VScroll->Value(ToIndex); } ForceFullUpdate = true; } if (ToIndex >= VScroll->Value() + DisplayLines) { int YOff = d->CenterCursor ? DisplayLines >> 1 : DisplayLines; ssize_t v = MIN(ToIndex - YOff + 1, (ssize_t)Line.Length() - DisplayLines); if (v != VScroll->Value()) { // Below the visible region VScroll->Value(v); ForceFullUpdate = true; } } } else { d->VScrollCache = ToIndex; } } return ForceFullUpdate; } void LTextView4::SetCaret(size_t i, bool Select, bool ForceFullUpdate) { // int _Start = LCurrentTime(); Blink = true; // Bound the new cursor position to the document if ((ssize_t)i > Size) i = Size; // Store the old selection and cursor ssize_t s = SelStart, e = SelEnd, c = Cursor; // If there is going to be a selected area if (Select && i != SelStart) { // Then set the start if (SelStart < 0) { // We are starting a new selection SelStart = Cursor; } // And end SelEnd = i; } else { // Clear the selection SelStart = SelEnd = -1; } ssize_t FromIndex = 0; LTextLine *From = GetTextLine(Cursor, &FromIndex); Cursor = i; // check the cursor is on the screen ForceFullUpdate |= ScrollToOffset(Cursor); // check whether we need to update the screen ssize_t ToIndex = 0; LTextLine *To = GetTextLine(Cursor, &ToIndex); if (ForceFullUpdate || !To || !From) { // need full update Invalidate(); } else if ( ( SelStart != s || SelEnd != e ) ) { // Update just the selection bounds LRect Client = GetClient(); size_t Start, End; if (SelStart >= 0 && s >= 0) { // Selection has changed, union the before and after regions Start = MIN(Cursor, c); End = MAX(Cursor, c); } else if (SelStart >= 0) { // Selection created... Start = MIN(SelStart, SelEnd); End = MAX(SelStart, SelEnd); } else if (s >= 0) { // Selection removed... Start = MIN(s, e); End = MAX(s, e); } else return; LTextLine *SLine = GetTextLine(Start); LTextLine *ELine = GetTextLine(End); LRect u; if (SLine && ELine) { if (SLine->r.Valid()) { u = DocToScreen(SLine->r); } else u.Set(0, 0, Client.X()-1, 1); // Start of visible page LRect b(0, Client.Y()-1, Client.X()-1, Client.Y()-1); if (ELine->r.Valid()) { b = DocToScreen(ELine->r); } else { b.Set(0, Client.Y()-1, Client.X()-1, Client.Y()-1); } u.Union(&b); u.x1 = 0; u.x2 = X(); } else { /* printf("%s,%i - Couldn't get SLine and ELine: %i->%p, %i->%p\n", _FL, (int)Start, SLine, (int)End, ELine); */ u = Client; } Invalidate(&u); } else if (Cursor != c) { // just the cursor has moved // update the line the cursor moved to LRect r = To->r; r.Offset(-ScrollX, d->rPadding.y1-DocOffset); r.x2 = X(); Invalidate(&r); if (To != From) { // update the line the cursor came from, // if it's a different line from the "to" r = From->r; r.Offset(-ScrollX, d->rPadding.y1-DocOffset); r.x2 = X(); Invalidate(&r); } } if (c != Cursor) { // Send off notify SendNotify(LNotifyCursorChanged); } //int _Time = LCurrentTime() - _Start; //printf("Setcursor=%ims\n", _Time); } void LTextView4::SetBorder(int b) { } bool LTextView4::Cut() { bool Status = false; char16 *Txt16 = 0; DeleteSelection(&Txt16); if (Txt16) { #ifdef WIN32 Txt16 = ConvertToCrLf(Txt16); #endif char *Txt8 = (char*)LNewConvertCp(LAnsiToLgiCp(), Txt16, LGI_WideCharset); LClipBoard Clip(this); Clip.Text(Txt8); Status = Clip.TextW(Txt16, false); DeleteArray(Txt8); DeleteArray(Txt16); } return Status; } bool LTextView4::Copy() { bool Status = true; if (SelStart >= 0) { ssize_t Min = MIN(SelStart, SelEnd); ssize_t Max = MAX(SelStart, SelEnd); #ifdef WIN32 char16 *Txt16 = NewStrW(Text+Min, Max-Min); Txt16 = ConvertToCrLf(Txt16); char *Txt8 = (char*)LNewConvertCp(LAnsiToLgiCp(), Txt16, LGI_WideCharset); #else char *Txt8 = (char*)LNewConvertCp("utf-8", Text+Min, LGI_WideCharset, (Max-Min)*sizeof(*Text)); #endif LClipBoard Clip(this); Clip.Text(Txt8); #ifdef WIN32 Clip.TextW(Txt16, false); DeleteArray(Txt16); #endif DeleteArray(Txt8); } else LgiTrace("%s:%i - No selection.\n", _FL); return Status; } bool LTextView4::Paste() { LClipBoard Clip(this); LAutoWString Mem; char16 *t = Clip.TextW(); if (!t) // ala Win9x { char *s = Clip.Text(); if (s) { Mem.Reset(Utf8ToWide(s)); t = Mem; } } if (!t) return false; if (SelStart >= 0) { DeleteSelection(); } // remove '\r's char16 *s = t, *d = t; for (; *s; s++) { if (*s != '\r') { *d++ = *s; } } *d++ = 0; // insert text ssize_t Len = StrlenW(t); Insert(Cursor, t, Len); SetCaret(Cursor+Len, false, true); // Multiline return true; } void LTextView4::ClearDirty(std::function OnStatus, bool Ask, const char *FileName) { if (Dirty) { int Answer = (Ask) ? LgiMsg(this, LLoadString(L_TEXTCTRL_ASK_SAVE, "Do you want to save your changes to this document?"), LLoadString(L_TEXTCTRL_SAVE, "Save"), MB_YESNOCANCEL) : IDYES; if (Answer == IDYES) { auto DoSave = [this, OnStatus, FileName=LString(FileName)](bool ok) { Save(FileName); if (OnStatus) OnStatus(ok); }; if (!FileName) { LFileSelect *Select = new LFileSelect; Select->Parent(this); Select->Save([&FileName, &DoSave](auto Select, auto ok) { if (ok) FileName = Select->Name(); DoSave(ok); delete Select; }); } else DoSave(true); } else if (Answer == IDCANCEL) { if (OnStatus) OnStatus(false); return; } } if (OnStatus) OnStatus(true); } bool LTextView4::Open(const char *Name, const char *CharSet) { bool Status = false; LFile f; if (f.Open(Name, O_READ|O_SHARE)) { DeleteArray(Text); int64 Bytes = f.GetSize(); if (Bytes < 0 || Bytes & 0xffff000000000000LL) { LgiTrace("%s:%i - Invalid file size: " LPrintfInt64 "\n", _FL, Bytes); return false; } SetCaret(0, false); Line.DeleteObjects(); char *c8 = new char[Bytes + 4]; if (c8) { if (f.Read(c8, (int)Bytes) == Bytes) { char *DataStart = c8; c8[Bytes] = 0; c8[Bytes+1] = 0; c8[Bytes+2] = 0; c8[Bytes+3] = 0; if ((uchar)c8[0] == 0xff && (uchar)c8[1] == 0xfe) { // utf-16 if (!CharSet) { CharSet = "utf-16"; DataStart += 2; } } // Convert to unicode first.... if (Bytes == 0) { Text = new char16[1]; if (Text) Text[0] = 0; } else { Text = (char16*)LNewConvertCp(LGI_WideCharset, DataStart, CharSet ? CharSet : DefaultCharset); } if (Text) { // Remove LF's char16 *In = Text, *Out = Text; CrLf = false; Size = 0; while (*In) { if (*In >= ' ' || *In == '\t' || *In == '\n') { *Out++ = *In; Size++; } else if (*In == '\r') { CrLf = true; } In++; } Size = (int) (Out - Text); *Out = 0; Alloc = Size + 1; Dirty = false; if (Text && Text[0] == 0xfeff) // unicode byte order mark { memmove(Text, Text+1, Size * sizeof(*Text)); Size--; } PourText(0, Size); PourStyle(0, Size); UpdateScrollBars(true); Status = true; } } DeleteArray(c8); } else { Alloc = Size = 0; } Invalidate(); } return Status; } template bool WriteToStream(LFile &out, T *in, size_t len, bool CrLf) { if (!in) return false; if (CrLf) { int BufLen = 1 << 20; LAutoPtr Buf(new T[BufLen]); T *b = Buf; T *e = Buf + BufLen; T *c = in; T *end = c + len; while (c < end) { if (b > e - 16) { auto Bytes = (b - Buf) * sizeof(T); if (out.Write(Buf, Bytes) != Bytes) return false; b = Buf; } if (*c == '\n') { *b++ = '\r'; *b++ = '\n'; } else { *b++ = *c; } c++; } auto Bytes = (b - Buf) * sizeof(T); if (out.Write(Buf, Bytes) != Bytes) return false; } else { auto Bytes = len * sizeof(T); if (out.Write(in, Bytes) != Bytes) return false; } return true; } bool LTextView4::Save(const char *Name, const char *CharSet) { LFile f; LString TmpName; bool Status = false; d->LastError.Empty(); if (f.Open(Name, O_WRITE)) { if (f.SetSize(0) != 0) { // Can't resize file, fall back to renaming it and // writing a new file... f.Close(); TmpName = Name; TmpName += ".tmp"; if (!FileDev->Move(Name, TmpName)) { LgiTrace("%s:%i - Failed to move '%s'.\n", _FL, Name); return false; } if (!f.Open(Name, O_WRITE)) { LgiTrace("%s:%i - Failed to open '%s' for writing.\n", _FL, Name); return false; } } if (Text) { auto InSize = Size * sizeof(char16); if (CharSet && !Stricmp(CharSet, "utf-16")) { if (sizeof(*Text) == 2) { // No conversion needed... Status = WriteToStream(f, Text, Size, CrLf); } else { // 32->16 convert LAutoPtr c16((uint16_t*)LNewConvertCp(CharSet, Text, LGI_WideCharset, InSize)); if (c16) Status = WriteToStream(f, c16.Get(), Strlen(c16.Get()), CrLf); } } else if (CharSet && !Stricmp(CharSet, "utf-32")) { if (sizeof(*Text) == 4) { // No conversion needed... Status = WriteToStream(f, Text, Size, CrLf); } else { // 16->32 convert LAutoPtr c32((uint32_t*)LNewConvertCp(CharSet, Text, LGI_WideCharset, InSize)); if (c32) Status = WriteToStream(f, c32.Get(), Strlen(c32.Get()), CrLf); } } else { LAutoString c8((char*)LNewConvertCp(CharSet ? CharSet : DefaultCharset, Text, LGI_WideCharset, InSize)); if (c8) Status = WriteToStream(f, c8.Get(), strlen(c8), CrLf); } if (Status) Dirty = false; } } else { int Err = f.GetError(); LString sErr = LErrorCodeToString(Err); d->LastError.Printf("Failed to open '%s' for writing: %i - %s\n", Name, Err, sErr.Get()); } if (TmpName) FileDev->Delete(TmpName); return Status; } const char *LTextView4::GetLastError() { return d->LastError; } void LTextView4::UpdateScrollBars(bool Reset) { if (VScroll) { LRect Before = GetClient(); int DisplayLines = Y() / LineY; ssize_t Lines = GetLines(); VScroll->SetRange(Lines); if (VScroll) { VScroll->SetPage(DisplayLines); ssize_t Max = Lines - DisplayLines + 1; bool Inval = false; if (VScroll->Value() > Max) { VScroll->Value(Max); Inval = true; } if (Reset) { VScroll->Value(0); SelStart = SelEnd = -1; } else if (d->VScrollCache >= 0) { VScroll->Value(d->VScrollCache); d->VScrollCache = -1; SelStart = SelEnd = -1; } LRect After = GetClient(); if (Before != After && GetWrapType()) { d->SetDirty(0, Size); Inval = true; } if (Inval) { Invalidate(); } } } } void LTextView4::DoCase(std::function Callback, bool Upper) { if (Text) { ssize_t Min = MIN(SelStart, SelEnd); ssize_t Max = MAX(SelStart, SelEnd); if (Min < Max) { if (UndoOn) { LAutoPtr Obj(new LTextView4Undo(this)); LTextView4Undo *u = UndoCur ? UndoCur : Obj; if (u) u->AddChange(Min, Max - Min, UndoChange); if (Obj) UndoQue += Obj.Release(); } for (ssize_t i=Min; i= 'a' && Text[i] <= 'z') Text[i] = Text[i] - 'a' + 'A'; } else { if (Text[i] >= 'A' && Text[i] <= 'Z') Text[i] = Text[i] - 'A' + 'a'; } } Dirty = true; d->SetDirty(Min, 0); Invalidate(); SendNotify(LNotifyDocChanged); } } if (Callback) Callback(Text != NULL); } ssize_t LTextView4::GetLine() { ssize_t Idx = 0; GetTextLine(Cursor, &Idx); return Idx + 1; } void LTextView4::SetLine(int64_t i) { LTextLine *l = Line.ItemAt(i - 1); if (l) { d->CenterCursor = true; SetCaret(l->Start, false); d->CenterCursor = false; } } void LTextView4::DoGoto(std::function Callback) { LInput *Dlg = new LInput(this, "", LLoadString(L_TEXTCTRL_GOTO_LINE, "Goto line:"), "Text"); Dlg->DoModal([this, Dlg, Callback](auto d, auto code) { auto ok = code == IDOK && Dlg->GetStr(); if (ok) SetLine(Dlg->GetStr().Int()); if (Callback) Callback(ok); delete Dlg; }); } LDocFindReplaceParams *LTextView4::CreateFindReplaceParams() { return new GDocFindReplaceParams4; } void LTextView4::SetFindReplaceParams(LDocFindReplaceParams *Params) { if (Params) { if (d->OwnFindReplaceParams) { DeleteObj(d->FindReplaceParams); } d->OwnFindReplaceParams = false; d->FindReplaceParams = (GDocFindReplaceParams4*) Params; } } void LTextView4::DoFindNext(std::function Callback) { bool Status = false; if (InThread()) { if (d->FindReplaceParams->Lock(_FL)) { if (d->FindReplaceParams->LastFind) Status = OnFind(d->FindReplaceParams->LastFind, d->FindReplaceParams->MatchWord, d->FindReplaceParams->MatchCase, d->FindReplaceParams->SelectionOnly, d->FindReplaceParams->SearchUpwards); d->FindReplaceParams->Unlock(); } } else if (IsAttached()) { Status = PostEvent(M_TEXTVIEW_FIND); } if (Callback) Callback(Status); } void LTextView4::DoFind(std::function Callback) { LString u; if (HasSelection()) { ssize_t Min = MIN(SelStart, SelEnd); ssize_t Max = MAX(SelStart, SelEnd); u = LString(Text + Min, Max - Min); } else { u = d->FindReplaceParams->LastFind.Get(); } auto Dlg = new LFindDlg(this, [this, Params=d->FindReplaceParams, Callback](auto Dlg, auto Action) { if (Params && Params->Lock(_FL)) { Params->MatchWord = Dlg->MatchWord; Params->MatchCase = Dlg->MatchCase; Params->SelectionOnly = Dlg->SelectionOnly; Params->SearchUpwards = Dlg->SearchUpwards; Params->LastFind.Reset(Utf8ToWide(Dlg->Find)); Params->Unlock(); } DoFindNext([this, Callback](bool ok) { Focus(true); if (Callback) Callback(ok); }); }, u); Dlg->DoModal(NULL); } void LTextView4::DoReplace(std::function Callback) { bool SingleLineSelection = false; SingleLineSelection = HasSelection(); if (SingleLineSelection) { LRange Sel = GetSelectionRange(); for (ssize_t i = Sel.Start; i < Sel.End(); i++) { if (Text[i] == '\n') { SingleLineSelection = false; break; } } } LAutoString LastFind8(SingleLineSelection ? GetSelection() : WideToUtf8(d->FindReplaceParams->LastFind)); LAutoString LastReplace8(WideToUtf8(d->FindReplaceParams->LastReplace)); auto Dlg = new LReplaceDlg(this, [this](auto Dlg, auto Action) { LReplaceDlg *Replace = dynamic_cast(Dlg); LAssert(Replace != NULL); if (Action == IDCANCEL) return; if (d->FindReplaceParams->Lock(_FL)) { d->FindReplaceParams->LastFind.Reset(Utf8ToWide(Replace->Find)); d->FindReplaceParams->LastReplace.Reset(Utf8ToWide(Replace->Replace)); d->FindReplaceParams->MatchWord = Replace->MatchWord; d->FindReplaceParams->MatchCase = Replace->MatchCase; d->FindReplaceParams->SelectionOnly = Replace->SelectionOnly; switch (Action) { case IDC_FR_FIND: { OnFind( d->FindReplaceParams->LastFind, d->FindReplaceParams->MatchWord, d->FindReplaceParams->MatchCase, d->FindReplaceParams->SelectionOnly, d->FindReplaceParams->SearchUpwards); break; } case IDOK: case IDC_FR_REPLACE: { OnReplace( d->FindReplaceParams->LastFind, d->FindReplaceParams->LastReplace, Action == IDOK, d->FindReplaceParams->MatchWord, d->FindReplaceParams->MatchCase, d->FindReplaceParams->SelectionOnly, d->FindReplaceParams->SearchUpwards); break; } } d->FindReplaceParams->Unlock(); } }, LastFind8, LastReplace8); Dlg->MatchWord = d->FindReplaceParams->MatchWord; Dlg->MatchCase = d->FindReplaceParams->MatchCase; Dlg->SelectionOnly = HasSelection(); Dlg->DoModal(NULL); } void LTextView4::SelectWord(size_t From) { for (SelStart = From; SelStart > 0; SelStart--) { if (strchr(SelectWordDelim, Text[SelStart])) { SelStart++; break; } } for (SelEnd = From; SelEnd < Size; SelEnd++) { if (strchr(SelectWordDelim, Text[SelEnd])) { break; } } Invalidate(); } typedef int (*StringCompareFn)(const char16 *a, const char16 *b, ssize_t n); ptrdiff_t LTextView4::MatchText(const char16 *Find, bool MatchWord, bool MatchCase, bool SelectionOnly, bool SearchUpwards) { if (!ValidStrW(Find)) return -1; ssize_t FindLen = StrlenW(Find); // Setup range to search ssize_t Begin, End; if (SelectionOnly && HasSelection()) { Begin = MIN(SelStart, SelEnd); End = MAX(SelStart, SelEnd); } else { Begin = 0; End = Size; } // Look through text... ssize_t i; bool Wrap = false; if (Cursor > End - FindLen) { Wrap = true; if (SearchUpwards) i = End - FindLen; else i = Begin; } else { i = Cursor; } if (i < Begin) i = Begin; if (i > End) i = End; StringCompareFn CmpFn = MatchCase ? StrncmpW : StrnicmpW; char16 FindCh = MatchCase ? Find[0] : toupper(Find[0]); for (; SearchUpwards ? i >= Begin : i <= End - FindLen; i += SearchUpwards ? -1 : 1) { if ( (MatchCase ? Text[i] : toupper(Text[i])) == FindCh ) { char16 *Possible = Text + i; if (CmpFn(Possible, Find, FindLen) == 0) { if (MatchWord) { // Check boundaries if (Possible > Text) // Check off the start { if (!IsWordBoundry(Possible[-1])) continue; } if (i + FindLen < Size) // Check off the end { if (!IsWordBoundry(Possible[FindLen])) continue; } } LRange r(Possible - Text, FindLen); if (!r.Overlap(Cursor)) return r.Start; } } if (!Wrap && (i + 1 > End - FindLen)) { Wrap = true; i = Begin; End = Cursor; } } return -1; } bool LTextView4::OnFind(const char16 *Find, bool MatchWord, bool MatchCase, bool SelectionOnly, bool SearchUpwards) { THREAD_CHECK(); // Not sure what this is doing??? if (HasSelection() && SelEnd < SelStart) { Cursor = SelStart; } #if FEATURE_HILIGHT_ALL_MATCHES // Clear existing styles for matches for (StyleIter s = Style.begin(); s != Style.end(); ) { if (s->Owner == STYLE_FIND_MATCHES) Style.Delete(s); else s++; } ssize_t FindLen = StrlenW(Find); ssize_t FirstLoc = MatchText(Find, MatchWord, MatchCase, false, SearchUpwards), Loc; if (FirstLoc >= 0) { SetCaret(FirstLoc, false); SetCaret(FirstLoc + FindLen, true); } ssize_t Old = Cursor; if (!SearchUpwards) Cursor += FindLen; while ((Loc = MatchText(Find, MatchWord, MatchCase, false, false)) != FirstLoc) { LAutoPtr s(new LStyle(STYLE_FIND_MATCHES)); s->Start = Loc; s->Len = FindLen; s->Fore = LColour(L_FOCUS_SEL_FORE); s->Back = LColour(L_FOCUS_SEL_BACK).Mix(LColour(L_WORKSPACE)); InsertStyle(s); Cursor = Loc + FindLen; } Cursor = Old; ScrollToOffset(Cursor); Invalidate(); #else ssize_t Loc = MatchText(Find, MatchWord, MatchCase, SelectionOnly, SearchUpwards); if (Loc >= 0) { SetCaret(Loc, false); SetCaret(Loc + StrlenW(Find), true); return true; } #endif return false; } bool LTextView4::OnReplace(const char16 *Find, const char16 *Replace, bool All, bool MatchWord, bool MatchCase, bool SelectionOnly, bool SearchUpwards) { THREAD_CHECK(); if (ValidStrW(Find)) { // int Max = -1; ssize_t FindLen = StrlenW(Find); ssize_t ReplaceLen = StrlenW(Replace); // size_t OldCursor = Cursor; ptrdiff_t First = -1; while (true) { ptrdiff_t Loc = MatchText(Find, MatchWord, MatchCase, SelectionOnly, SearchUpwards); if (First < 0) { First = Loc; } else if (Loc == First) { break; } if (Loc >= 0) { ssize_t OldSelStart = SelStart; ssize_t OldSelEnd = SelEnd; Delete(Loc, FindLen); Insert(Loc, Replace, ReplaceLen); SelStart = OldSelStart; SelEnd = OldSelEnd - FindLen + ReplaceLen; Cursor = Loc + ReplaceLen; } if (!All) { return Loc >= 0; } if (Loc < 0) break; } } return false; } ssize_t LTextView4::SeekLine(ssize_t Offset, GTextViewSeek Where) { THREAD_CHECK(); switch (Where) { case PrevLine: { for (; Offset > 0 && Text[Offset] != '\n'; Offset--) ; if (Offset > 0) Offset--; for (; Offset > 0 && Text[Offset] != '\n'; Offset--) ; if (Offset > 0) Offset++; break; } case NextLine: { for (; Offset < Size && Text[Offset] != '\n'; Offset++) ; Offset++; break; } case StartLine: { for (; Offset > 0 && Text[Offset] != '\n'; Offset--) ; if (Offset > 0) Offset++; break; } case EndLine: { for (; Offset < Size && Text[Offset] != '\n'; Offset++) ; break; } default: { LAssert(false); break; } } return Offset; } bool LTextView4::OnMultiLineTab(bool In) { bool Status = false; ssize_t Min = MIN(SelStart, SelEnd); ssize_t Max = MAX(SelStart, SelEnd), i; Min = SeekLine(Min, StartLine); int Ls = 0; LArray p; for (i=Min; i=0; i--) { if (In) { // <- ssize_t n = Indexes[i], Space = 0; for (; Space ssize_t Len = Indexes[i]; for (; Text[Len] != '\n' && Len Indexes[i]) { if (HardTabs) { char16 Tab[] = {'\t', 0}; Insert(Indexes[i], Tab, 1); Max++; } else { char16 *Sp = new char16[IndentSize]; if (Sp) { for (int n=0; nChanges.Length()) { UndoQue += UndoCur; UndoCur = NULL; } else { DeleteObj(UndoCur); } SelStart = Min; SelEnd = Cursor = Max; PourEnabled = true; PourText(Min, Max - Min); PourStyle(Min, Max - Min); d->SetDirty(Min, Max-Min); Invalidate(); Status = true; return Status; } void LTextView4::OnSetHidden(int Hidden) { } void LTextView4::OnPosChange() { static bool Processing = false; if (!Processing) { Processing = true; LLayout::OnPosChange(); LRect c = GetClient(); bool ScrollYNeeded = c.Y() < (Line.Length() * LineY); bool ScrollChange = ScrollYNeeded ^ (VScroll != NULL); if (ScrollChange) { // printf("%s:%i - SetScrollBars(%i)\n", _FL, ScrollYNeeded); SetScrollBars(false, ScrollYNeeded); } UpdateScrollBars(); if (GetWrapType() && d->PourX != X()) { d->PourX = X(); d->SetDirty(0, Size); } Processing = false; } } int LTextView4::WillAccept(LDragFormats &Formats, LPoint Pt, int KeyState) { Formats.Supports("text/uri-list"); Formats.Supports("text/html"); Formats.Supports("UniformResourceLocatorW"); return Formats.Length() ? DROPEFFECT_COPY : DROPEFFECT_NONE; } int LTextView4::OnDrop(LArray &Data, LPoint Pt, int KeyState) { int Status = DROPEFFECT_NONE; for (unsigned i=0; iIsBinary()) { OsChar *e = (OsChar*) ((char*)Data->Value.Binary.Data + Data->Value.Binary.Length); OsChar *s = (OsChar*) Data->Value.Binary.Data; int len = 0; while (s < e && s[len]) { len++; } LAutoWString w ( (char16*)LNewConvertCp ( LGI_WideCharset, s, ( sizeof(OsChar) == 1 ? "utf-8" : LGI_WideCharset ), len * sizeof(*s) ) ); Insert(Cursor, w, len); Invalidate(); return DROPEFFECT_COPY; } } else if (dd.IsFileDrop()) { // We don't directly handle file drops... pass up to the parent bool FoundTarget = false; for (LViewI *p = GetParent(); p; p = p->GetParent()) { LDragDropTarget *t = p->DropTarget(); if (t) { Status = t->OnDrop(Data, Pt, KeyState); if (Status != DROPEFFECT_NONE) { FoundTarget = true; break; } } } if (!FoundTarget) { auto Wnd = GetWindow(); if (Wnd) { LDropFiles files(dd); Wnd->OnReceiveFiles(files); } } } } return Status; } void LTextView4::OnCreate() { SetWindow(this); DropTarget(true); #ifndef WINDOWS if (Ctrls.Length() == 0) #endif SetPulse(PULSE_TIMEOUT); #ifndef WINDOWS Ctrls.Add(this); #endif } void LTextView4::OnEscape(LKey &K) { } bool LTextView4::OnMouseWheel(double l) { if (VScroll) { int64 NewPos = VScroll->Value() + (int)l; NewPos = limit(NewPos, 0, (ssize_t)GetLines()); VScroll->Value(NewPos); Invalidate(); } return true; } void LTextView4::OnFocus(bool f) { Invalidate(); } ssize_t LTextView4::HitText(int x, int y, bool Nearest) { if (!Text) return 0; bool Down = y >= 0; auto Y = VScroll ? VScroll->Value() : 0; if (Y < (ssize_t)Line.Length()) y += Line[Y]->r.y1; while (Y>=0 && Y<(ssize_t)Line.Length()) { auto l = Line[Y]; if (l->r.Overlap(x, y)) { // Over a line int At = x - l->r.x1; ssize_t Char = 0; LDisplayString Ds(Font, MapText(Text + l->Start, l->Len), l->Len, 0); Char = Ds.CharAt(At, Nearest ? LgiNearest : LgiTruncate); return l->Start + Char; } else if (y >= l->r.y1 && y <= l->r.y2) { // Click horizontally before of after line if (x < l->r.x1) return l->Start; else if (x > l->r.x2) return l->Start + l->Len; } if (Down) Y++; else Y--; } // outside text area if (Down) { if (Line.Length()) { if (y > Line.Last()->r.y2) { // end of document return Size; } } } return 0; } void LTextView4::Undo() { int Old = UndoQue.GetPos(); UndoQue.Undo(); if (Old && !UndoQue.GetPos()) { Dirty = false; SendNotify(LNotifyDocChanged); } } void LTextView4::Redo() { UndoQue.Redo(); } void LTextView4::DoContextMenu(LMouse &m) { LSubMenu RClick; LAutoString ClipText; { LClipBoard Clip(this); ClipText.Reset(NewStr(Clip.Text())); } LStyle *s = HitStyle(HitText(m.x, m.y, true)); if (s) { if (OnStyleMenu(s, &RClick)) { RClick.AppendSeparator(); } } RClick.AppendItem(LLoadString(L_TEXTCTRL_CUT, "Cut"), IDM_CUT, HasSelection()); RClick.AppendItem(LLoadString(L_TEXTCTRL_COPY, "Copy"), IDM_COPY, HasSelection()); RClick.AppendItem(LLoadString(L_TEXTCTRL_PASTE, "Paste"), IDM_PASTE, ClipText != 0); RClick.AppendSeparator(); RClick.AppendItem("Copy All", IDM_COPY_ALL, true); RClick.AppendItem("Select All", IDM_SELECT_ALL, true); RClick.AppendSeparator(); RClick.AppendItem(LLoadString(L_TEXTCTRL_UNDO, "Undo"), IDM_UNDO, UndoQue.CanUndo()); RClick.AppendItem(LLoadString(L_TEXTCTRL_REDO, "Redo"), IDM_REDO, UndoQue.CanRedo()); RClick.AppendSeparator(); auto i = RClick.AppendItem(LLoadString(L_TEXTCTRL_FIXED, "Fixed Width Font"), IDM_FIXED, true); if (i) i->Checked(GetFixedWidthFont()); i = RClick.AppendItem(LLoadString(L_TEXTCTRL_AUTO_INDENT, "Auto Indent"), IDM_AUTO_INDENT, true); if (i) i->Checked(AutoIndent); i = RClick.AppendItem(LLoadString(L_TEXTCTRL_SHOW_WHITESPACE, "Show Whitespace"), IDM_SHOW_WHITE, true); if (i) i->Checked(ShowWhiteSpace); i = RClick.AppendItem(LLoadString(L_TEXTCTRL_HARD_TABS, "Hard Tabs"), IDM_HARD_TABS, true); if (i) i->Checked(HardTabs); RClick.AppendItem(LLoadString(L_TEXTCTRL_INDENT_SIZE, "Indent Size"), IDM_INDENT_SIZE, true); RClick.AppendItem(LLoadString(L_TEXTCTRL_TAB_SIZE, "Tab Size"), IDM_TAB_SIZE, true); if (Environment) Environment->AppendItems(&RClick, NULL); int Id = 0; m.ToScreen(); switch (Id = RClick.Float(this, m)) { case IDM_FIXED: { SetFixedWidthFont(!GetFixedWidthFont()); SendNotify(LNotifyFixedWidthChanged); break; } case IDM_CUT: { Cut(); break; } case IDM_COPY: { Copy(); break; } case IDM_PASTE: { Paste(); break; } case IDM_COPY_ALL: { SelectAll(); Copy(); break; } case IDM_SELECT_ALL: { SelectAll(); break; } case IDM_UNDO: { Undo(); break; } case IDM_REDO: { Redo(); break; } case IDM_AUTO_INDENT: { AutoIndent = !AutoIndent; break; } case IDM_SHOW_WHITE: { ShowWhiteSpace = !ShowWhiteSpace; Invalidate(); break; } case IDM_HARD_TABS: { HardTabs = !HardTabs; break; } case IDM_INDENT_SIZE: { char s[32]; sprintf_s(s, sizeof(s), "%i", IndentSize); LInput *i = new LInput(this, s, "Indent Size:", "Text"); i->DoModal([this, i](auto dlg, auto code) { if (code) IndentSize = atoi(i->GetStr()); delete i; }); break; } case IDM_TAB_SIZE: { char s[32]; sprintf_s(s, sizeof(s), "%i", TabSize); LInput *i = new LInput(this, s, "Tab Size:", "Text"); i->DoModal([this, i](auto dlg, auto code) { SetTabSize(atoi(i->GetStr())); delete i; }); break; } default: { if (s) { OnStyleMenuClick(s, Id); } if (Environment) { Environment->OnMenu(this, Id, 0); } break; } } } bool LTextView4::OnStyleClick(LStyle *style, LMouse *m) { switch (style->Owner) { case STYLE_URL: { if ( (!m) || (m->Left() && m->Down() && m->Double()) ) { LString s(Text + style->Start, style->Len); if (s) OnUrl(s); return true; } break; } default: break; } return false; } bool LTextView4::OnStyleMenu(LStyle *style, LSubMenu *m) { switch (style->Owner) { case STYLE_URL: { LString s(Text + style->Start, style->Len); if (LIsValidEmail(s)) m->AppendItem(LLoadString(L_TEXTCTRL_EMAIL_TO, "New Email to..."), IDM_NEW, true); else m->AppendItem(LLoadString(L_TEXTCTRL_OPENURL, "Open URL"), IDM_OPEN, true); m->AppendItem(LLoadString(L_TEXTCTRL_COPYLINK, "Copy link location"), IDM_COPY_URL, true); return true; } default: break; } return false; } void LTextView4::OnStyleMenuClick(LStyle *style, int i) { switch (style->Owner) { case STYLE_URL: { LString s(Text + style->Start, style->Len); switch (i) { case IDM_NEW: case IDM_OPEN: { if (s) OnUrl(s); break; } case IDM_COPY_URL: { if (s) { LClipBoard Clip(this); Clip.Text(s); } break; } } break; } default: break; } } void LTextView4::OnMouseClick(LMouse &m) { bool Processed = false; m.x += ScrollX; if (m.Down()) { if (!m.IsContextMenu()) { Focus(true); ssize_t Hit = HitText(m.x, m.y, true); if (Hit >= 0) { SetCaret(Hit, m.Shift()); LStyle *s = HitStyle(Hit); if (s) Processed = OnStyleClick(s, &m); } if (!Processed && m.Double()) { d->WordSelectMode = Cursor; SelectWord(Cursor); } else { d->WordSelectMode = -1; } } else { DoContextMenu(m); return; } } if (!Processed) { Capture(m.Down()); } } int LTextView4::OnHitTest(int x, int y) { #ifdef WIN32 if (GetClient().Overlap(x, y)) { return HTCLIENT; } #endif return LView::OnHitTest(x, y); } void LTextView4::OnMouseMove(LMouse &m) { m.x += ScrollX; ssize_t Hit = HitText(m.x, m.y, true); if (IsCapturing()) { if (d->WordSelectMode < 0) { SetCaret(Hit, m.Left()); } else { ssize_t Min = Hit < d->WordSelectMode ? Hit : d->WordSelectMode; ssize_t Max = Hit > d->WordSelectMode ? Hit : d->WordSelectMode; for (SelStart = Min; SelStart > 0; SelStart--) { if (strchr(SelectWordDelim, Text[SelStart])) { SelStart++; break; } } for (SelEnd = Max; SelEnd < Size; SelEnd++) { if (strchr(SelectWordDelim, Text[SelEnd])) { break; } } Cursor = SelEnd; Invalidate(); } } } LCursor LTextView4::GetCursor(int x, int y) { LRect c = GetClient(); c.Offset(-c.x1, -c.y1); LStyle *s = NULL; if (c.Overlap(x, y)) { ssize_t Hit = HitText(x, y, true); s = HitStyle(Hit); } return s ? s->Cursor : LCUR_Ibeam; } int LTextView4::GetColumn() { int x = 0; LTextLine *l = GetTextLine(Cursor); if (l) { for (ssize_t i=l->Start; i> 1); m.Target = this; DoContextMenu(m); } else if (k.IsChar) { switch (k.vkey) { default: { // process single char input if ( !GetReadOnly() && ( (k.c16 >= ' ' || k.vkey == LK_TAB) && k.c16 != 127 ) ) { if (k.Down()) { // letter/number etc if (SelStart >= 0) { bool MultiLine = false; if (k.vkey == LK_TAB) { size_t Min = MIN(SelStart, SelEnd), Max = MAX(SelStart, SelEnd); for (size_t i=Min; iLen : 0; if (l && k.vkey == LK_TAB && (!HardTabs || IndentSize != TabSize)) { int x = GetColumn(); int Add = IndentSize - (x % IndentSize); if (HardTabs && ((x + Add) % TabSize) == 0) { int Rx = x; size_t Remove; for (Remove = Cursor; Text[Remove - 1] == ' ' && Rx % TabSize != 0; Remove--, Rx--); ssize_t Chars = (ssize_t)Cursor - Remove; Delete(Remove, Chars); Insert(Remove, &k.c16, 1); Cursor = Remove + 1; Invalidate(); } else { char16 *Sp = new char16[Add]; if (Sp) { for (int n=0; nLen : 0; SetCaret(Cursor + Add, false, Len != NewLen - 1); } DeleteArray(Sp); } } } else { char16 In = k.GetChar(); if (In == '\t' && k.Shift() && Cursor > 0) { l = GetTextLine(Cursor); if (Cursor > l->Start) { if (Text[Cursor-1] == '\t') { Delete(Cursor - 1, 1); SetCaret(Cursor, false, false); } else if (Text[Cursor-1] == ' ') { ssize_t Start = (ssize_t)Cursor - 1; while (Start >= l->Start && strchr(" \t", Text[Start-1])) Start--; int Depth = SpaceDepth(Text + Start, Text + Cursor); int NewDepth = Depth - (Depth % IndentSize); if (NewDepth == Depth && NewDepth > 0) NewDepth -= IndentSize; int Use = 0; while (SpaceDepth(Text + Start, Text + Start + Use + 1) < NewDepth) Use++; Delete(Start + Use, Cursor - Start - Use); SetCaret(Start + Use, false, false); } } } else if (In && Insert(Cursor, &In, 1)) { l = GetTextLine(Cursor); size_t NewLen = (l) ? l->Len : 0; SetCaret(Cursor + 1, false, Len != NewLen - 1); } } } return true; } break; } case LK_RETURN: #if defined MAC case LK_KEYPADENTER: #endif { if (GetReadOnly()) break; if (k.Down() && k.IsChar) { OnEnter(k); } return true; break; } case LK_BACKSPACE: { if (GetReadOnly()) break; if (k.Ctrl()) { // Ctrl+H } else if (k.Down()) { if (SelStart >= 0) { // delete selection DeleteSelection(); } else { char Del = Cursor > 0 ? Text[Cursor-1] : 0; if (Del == ' ' && (!HardTabs || IndentSize != TabSize)) { // Delete soft tab int x = GetColumn(); int Max = x % IndentSize; if (Max == 0) Max = IndentSize; ssize_t i; for (i=Cursor-1; i>=0; i--) { if (Max-- <= 0 || Text[i] != ' ') { i++; break; } } if (i < 0) i = 0; if (i < Cursor - 1) { ssize_t Del = (ssize_t)Cursor - i; Delete(i, Del); // SetCursor(i, false, false); // Invalidate(); break; } } else if (Del == '\t' && HardTabs && IndentSize != TabSize) { int x = GetColumn(); Delete(--Cursor, 1); for (int c=GetColumn(); c 0) { Delete(Cursor - 1, 1); } } } return true; break; } } } else // not a char { switch (k.vkey) { case LK_TAB: return true; case LK_RETURN: { return !GetReadOnly(); } case LK_BACKSPACE: { if (!GetReadOnly()) { if (k.Alt()) { if (k.Down()) { if (k.Ctrl()) { Redo(); } else { Undo(); } } } else if (k.Ctrl()) { if (k.Down()) { ssize_t Start = Cursor; while (IsWhiteSpace(Text[Cursor-1]) && Cursor > 0) Cursor--; while (!IsWhiteSpace(Text[Cursor-1]) && Cursor > 0) Cursor--; Delete(Cursor, Start - Cursor); Invalidate(); } } return true; } break; } case LK_F3: { if (k.Down()) { DoFindNext(NULL); } return true; break; } case LK_LEFT: { if (k.Down()) { if (SelStart >= 0 && !k.Shift()) { SetCaret(MIN(SelStart, SelEnd), false); } else if (Cursor > 0) { ssize_t n = Cursor; #ifdef MAC if (k.System()) { goto Jump_StartOfLine; } else if (k.Alt()) #else if (k.Ctrl()) #endif { // word move/select bool StartWhiteSpace = IsWhiteSpace(Text[n]); bool LeftWhiteSpace = n > 0 && IsWhiteSpace(Text[n-1]); if (!StartWhiteSpace || Text[n] == '\n') { n--; } // Skip ws for (; n > 0 && strchr(" \t", Text[n]); n--) ; if (Text[n] == '\n') { n--; } else if (!StartWhiteSpace || !LeftWhiteSpace) { if (IsDelimiter(Text[n])) { for (; n > 0 && IsDelimiter(Text[n]); n--); } else { for (; n > 0; n--) { //IsWordBoundry(Text[n]) if (IsWhiteSpace(Text[n]) || IsDelimiter(Text[n])) { break; } } } } if (n > 0) n++; } else { // single char n--; } SetCaret(n, k.Shift()); } } return true; break; } case LK_RIGHT: { if (k.Down()) { if (SelStart >= 0 && !k.Shift()) { SetCaret(MAX(SelStart, SelEnd), false); } else if (Cursor < Size) { ssize_t n = Cursor; #ifdef MAC if (k.System()) { goto Jump_EndOfLine; } else if (k.Alt()) #else if (k.Ctrl()) #endif { // word move/select if (IsWhiteSpace(Text[n])) { for (; nStart, Cursor-l->Start); int ScreenX = CurLine.X(); LDisplayString PrevLine(Font, Text + Prev->Start, Prev->Len); ssize_t CharX = PrevLine.CharAt(ScreenX); SetCaret(Prev->Start + MIN(CharX, Prev->Len), k.Shift()); } } } return true; break; } case LK_DOWN: { if (k.Alt()) return false; if (k.Down()) { #ifdef MAC if (k.Ctrl()) goto LTextView4_PageDown; #endif auto It = GetTextLineIt(Cursor); if (It != Line.end()) { auto l = *It; It++; if (It != Line.end()) { LTextLine *Next = *It; LDisplayString CurLine(Font, Text + l->Start, Cursor-l->Start); int ScreenX = CurLine.X(); LDisplayString NextLine(Font, Text + Next->Start, Next->Len); ssize_t CharX = NextLine.CharAt(ScreenX); SetCaret(Next->Start + MIN(CharX, Next->Len), k.Shift()); } } } return true; break; } case LK_END: { if (k.Down()) { if (k.Ctrl()) { SetCaret(Size, k.Shift()); } else { #ifdef MAC Jump_EndOfLine: #endif LTextLine *l = GetTextLine(Cursor); if (l) { SetCaret(l->Start + l->Len, k.Shift()); } } } return true; break; } case LK_HOME: { if (k.Down()) { if (k.Ctrl()) { SetCaret(0, k.Shift()); } else { #ifdef MAC Jump_StartOfLine: #endif LTextLine *l = GetTextLine(Cursor); if (l) { char16 *Line = Text + l->Start; char16 *s; char16 SpTab[] = {' ', '\t', 0}; for (s = Line; (SubtractPtr(s,Line) < l->Len) && StrchrW(SpTab, *s); s++); ssize_t Whitespace = SubtractPtr(s, Line); if (l->Start + Whitespace == Cursor) { SetCaret(l->Start, k.Shift()); } else { SetCaret(l->Start + Whitespace, k.Shift()); } } } } return true; break; } case LK_PAGEUP: { #ifdef MAC LTextView4_PageUp: #endif if (k.Down()) { LTextLine *l = GetTextLine(Cursor); if (l) { int DisplayLines = Y() / LineY; ssize_t CurLine = Line.IndexOf(l); LTextLine *New = Line.ItemAt(MAX(CurLine - DisplayLines, 0)); if (New) { SetCaret(New->Start + MIN(Cursor - l->Start, New->Len), k.Shift()); } } } return true; break; } case LK_PAGEDOWN: { #ifdef MAC LTextView4_PageDown: #endif if (k.Down()) { LTextLine *l = GetTextLine(Cursor); if (l) { int DisplayLines = Y() / LineY; ssize_t CurLine = Line.IndexOf(l); LTextLine *New = Line.ItemAt(MIN(CurLine + DisplayLines, (ssize_t)GetLines()-1)); if (New) { SetCaret(New->Start + MIN(Cursor - l->Start, New->Len), k.Shift()); } } } return true; break; } case LK_INSERT: { if (k.Down()) { if (k.Ctrl()) { Copy(); } else if (k.Shift()) { if (!GetReadOnly()) { Paste(); } } } return true; break; } case LK_DELETE: { if (!GetReadOnly()) { if (k.Down()) { if (SelStart >= 0) { if (k.Shift()) { Cut(); } else { DeleteSelection(); } } else if (Cursor < Size && Delete(Cursor, 1)) { Invalidate(); } } return true; } break; } default: { if (k.c16 == 17) break; if (k.CtrlCmd() && !k.Alt()) { switch (k.GetChar()) { case 0xbd: // Ctrl+'-' { if (k.Down() && Font->PointSize() > 1) { Font->PointSize(Font->PointSize() - 1); OnFontChange(); Invalidate(); } break; } case 0xbb: // Ctrl+'+' { if (k.Down() && Font->PointSize() < 100) { Font->PointSize(Font->PointSize() + 1); OnFontChange(); Invalidate(); } break; } case 'a': case 'A': { if (k.Down()) { // select all SelStart = 0; SelEnd = Size; Invalidate(); } return true; break; } case 'y': case 'Y': { if (!GetReadOnly()) { if (k.Down()) { Redo(); } return true; } break; } case 'z': case 'Z': { if (!GetReadOnly()) { if (k.Down()) { if (k.Shift()) { Redo(); } else { Undo(); } } return true; } break; } case 'x': case 'X': { if (!GetReadOnly()) { if (k.Down()) { Cut(); } return true; } break; } case 'c': case 'C': { if (k.Shift()) return false; if (k.Down()) Copy(); return true; break; } case 'v': case 'V': { if (!k.Shift() && !GetReadOnly()) { if (k.Down()) { Paste(); } return true; } break; } case 'f': { if (k.Down()) { DoFind(NULL); } return true; break; } case 'g': case 'G': { if (k.Down()) { DoGoto(NULL); } return true; break; } case 'h': case 'H': { if (k.Down()) { DoReplace(NULL); } return true; break; } case 'u': case 'U': { if (!GetReadOnly()) { if (k.Down()) { DoCase(NULL, k.Shift()); } return true; } break; } case LK_RETURN: { if (!GetReadOnly() && !k.Shift()) { if (k.Down()) { OnEnter(k); } return true; } break; } } } break; } } } return false; } void LTextView4::OnEnter(LKey &k) { // enter if (SelStart >= 0) { DeleteSelection(); } char16 InsertStr[256] = {'\n', 0}; LTextLine *CurLine = GetTextLine(Cursor); if (CurLine && AutoIndent) { int WsLen = 0; for (; WsLen < CurLine->Len && WsLen < (Cursor - CurLine->Start) && strchr(" \t", Text[CurLine->Start + WsLen]); WsLen++); if (WsLen > 0) { memcpy(InsertStr+1, Text+CurLine->Start, WsLen * sizeof(char16)); InsertStr[WsLen+1] = 0; } } if (Insert(Cursor, InsertStr, StrlenW(InsertStr))) { SetCaret(Cursor + StrlenW(InsertStr), false, true); } } int LTextView4::TextWidth(LFont *f, char16 *s, int Len, int x, int Origin) { int w = x; int Size = f->TabSize(); for (char16 *c = s; SubtractPtr(c, s) < Len; ) { if (*c == 9) { w = ((((w-Origin) + Size) / Size) * Size) + Origin; c++; } else { char16 *e; for (e = c; SubtractPtr(e, s) < Len && *e != 9; e++); LDisplayString ds(f, c, SubtractPtr(e, c)); w += ds.X(); c = e; } } return w - x; } int LTextView4::ScrollYLine() { return (VScroll) ? (int)VScroll->Value() : 0; } int LTextView4::ScrollYPixel() { return ScrollYLine() * LineY; } LRect LTextView4::DocToScreen(LRect r) { r.Offset(0, d->rPadding.y1 - ScrollYPixel()); return r; } void LTextView4::OnPaintLeftMargin(LSurface *pDC, LRect &r, LColour &colour) { pDC->Colour(colour); pDC->Rectangle(&r); } void LTextView4::OnPaint(LSurface *pDC) { #if LGI_EXCEPTIONS try { #endif #if PROFILE_PAINT char s[256]; sprintf_s(s, sizeof(s), "%p::OnPaint Lines=%i Sz=%i", this, (int)Line.Length(), (int)Size); LProfile Prof(s); #endif if (d->LayoutDirty) { #if PROFILE_PAINT Prof.Add("PourText"); #endif PourText(d->DirtyStart, d->DirtyLen); #if PROFILE_PAINT Prof.Add("PourStyle"); #endif PourStyle(d->DirtyStart, d->DirtyLen); d->LayoutDirty = false; } #if PROFILE_PAINT Prof.Add("Setup"); #endif LRect r = GetClient(); r.x2 += ScrollX; int Ox, Oy; pDC->GetOrigin(Ox, Oy); pDC->SetOrigin(Ox+ScrollX, Oy); #if 0 // Coverage testing... pDC->Colour(Rgb24(255, 0, 255), 24); pDC->Rectangle(); #endif LSurface *pOut = pDC; bool DrawSel = false; bool HasFocus = Focus(); // printf("%s:%i - HasFocus = %i\n", _FL, HasFocus); LColour SelectedText(HasFocus ? LColour(L_FOCUS_SEL_FORE) : LColour(L_NON_FOCUS_SEL_FORE)); LColour SelectedBack(HasFocus ? LColour(L_FOCUS_SEL_BACK) : LColour(L_NON_FOCUS_SEL_BACK)); LCss::ColorDef ForeDef, BkDef; if (GetCss()) { ForeDef = GetCss()->Color(); BkDef = GetCss()->BackgroundColor(); } LColour Fore(ForeDef.Type == LCss::ColorRgb ? LColour(ForeDef.Rgb32, 32) : LColour(L_TEXT)); LColour Back ( /*!ReadOnly &&*/ BkDef.Type == LCss::ColorRgb ? LColour(BkDef.Rgb32, 32) : Enabled() ? LColour(L_WORKSPACE) : LColour(L_MED) ); // LColour Whitespace = Fore.Mix(Back, 0.85f); if (!Enabled()) { Fore = LColour(L_LOW); Back = LColour(L_MED); } #ifdef DOUBLE_BUFFER_PAINT LMemDC *pMem = new LMemDC; pOut = pMem; #endif if (Text && Font #ifdef DOUBLE_BUFFER_PAINT && pMem && pMem->Create(r.X()-d->rPadding.x1, LineY, GdcD->GetBits()) #endif ) { ssize_t SelMin = MIN(SelStart, SelEnd); ssize_t SelMax = MAX(SelStart, SelEnd); // font properties Font->Colour(Fore, Back); // Font->WhitespaceColour(Whitespace); Font->Transparent(false); // draw margins pDC->Colour(PAINT_BORDER); // top margin pDC->Rectangle(0, 0, r.x2, d->rPadding.y1-1); // left margin { LRect LeftMargin(0, d->rPadding.y1, d->rPadding.x1-1, r.y2); OnPaintLeftMargin(pDC, LeftMargin, PAINT_BORDER); } // draw lines of text int k = ScrollYLine(); LTextLine *l = NULL; int Dy = 0; if (k < Line.Length()) Dy = -Line[k]->r.y1; ssize_t NextSelection = (SelStart != SelEnd) ? SelMin : -1; // offset where selection next changes if (k < Line.Length() && (l = Line[k]) && SelStart >= 0 && SelStart < l->Start && SelEnd > l->Start) { // start of visible area is in selection // init to selection colour DrawSel = true; Font->Colour(SelectedText, SelectedBack); NextSelection = SelMax; } StyleIter Si = Style.begin(); LStyle *NextStyle = GetNextStyle(Si, (l) ? l->Start : 0); DocOffset = (l) ? l->r.y1 : 0; #if PROFILE_PAINT Prof.Add("foreach Line loop"); #endif // loop through all visible lines int y = d->rPadding.y1; while ( k < Line.Length() && (l = Line[k]) && l->r.y1+Dy < r.Y()) { LRect Tr = l->r; #ifdef DOUBLE_BUFFER_PAINT Tr.Offset(-Tr.x1, -Tr.y1); #else Tr.Offset(0, y - Tr.y1); #endif //LRect OldTr = Tr; // deal with selection change on beginning of line if (NextSelection == l->Start) { // selection change DrawSel = !DrawSel; NextSelection = (NextSelection == SelMin) ? SelMax : -1; } if (DrawSel) { Font->Colour(SelectedText, SelectedBack); } else { LColour fore = l->c.IsValid() ? l->c : Fore; LColour back = l->Back.IsValid() ? l->Back : Back; Font->Colour(fore, back); } // How many chars on this line have we // processed so far: ssize_t Done = 0; bool LineHasSelection = NextSelection >= l->Start && NextSelection < l->Start + l->Len; // Fractional pixels we have moved so far: int MarginF = d->rPadding.x1 << LDisplayString::FShift; int FX = MarginF; int FY = Tr.y1 << LDisplayString::FShift; // loop through all sections of similar text on a line while (Done < l->Len) { // decide how big this block is int RtlTrailingSpace = 0; ssize_t Cur = l->Start + Done; ssize_t Block = l->Len - Done; // check for style change if (NextStyle && (ssize_t)NextStyle->End() <= l->Start) NextStyle = GetNextStyle(Si); if (NextStyle) { // start if (l->Overlap(NextStyle->Start) && NextStyle->Start > Cur && NextStyle->Start - Cur < Block) { Block = NextStyle->Start - Cur; } // end ssize_t StyleEnd = NextStyle->Start + NextStyle->Len; if (l->Overlap(StyleEnd) && StyleEnd > Cur && StyleEnd - Cur < Block) { Block = StyleEnd - Cur; } } // check for next selection change // this may truncate the style if (NextSelection > Cur && NextSelection - Cur < Block) { Block = NextSelection - Cur; } LAssert(Block != 0); // sanity check if (NextStyle && // There is a style (Cur < SelMin || Cur >= SelMax) && // && we're not drawing a selection block Cur >= NextStyle->Start && // && we're inside the styled area Cur < NextStyle->Start+NextStyle->Len) { LFont *Sf = NextStyle->Font ? NextStyle->Font : Font; if (Sf) { // draw styled text if (NextStyle->Fore.IsValid()) Sf->Fore(NextStyle->Fore); if (NextStyle->Back.IsValid()) Sf->Back(NextStyle->Back); else if (l->Back.IsValid()) Sf->Back(l->Back); else Sf->Back(Back); Sf->Transparent(false); LAssert(l->Start + Done >= 0); LDisplayString Ds( Sf, MapText(Text + (l->Start + Done), Block, RtlTrailingSpace != 0), Block + RtlTrailingSpace); Ds.SetDrawOffsetF(FX - MarginF); Ds.ShowVisibleTab(ShowWhiteSpace); Ds.FDraw(pOut, FX, FY, 0, LineHasSelection); if (NextStyle->Decor == LCss::TextDecorSquiggle) { pOut->Colour(NextStyle->DecorColour); int x = FX >> LDisplayString::FShift; int End = x + Ds.X(); while (x < End) { pOut->Set(x, Tr.y2-(x%2)); x++; } } FX += Ds.FX(); LColour fore = l->c.IsValid() ? l->c : Fore; LColour back = l->Back.IsValid() ? l->Back : Back; Sf->Colour(fore, back); } else LAssert(0); } else { // draw a block of normal text LAssert(l->Start + Done >= 0); LDisplayString Ds( Font, MapText(Text + (l->Start + Done), Block, RtlTrailingSpace != 0), Block + RtlTrailingSpace); Ds.SetDrawOffsetF(FX - MarginF); Ds.ShowVisibleTab(ShowWhiteSpace); Ds.FDraw(pOut, FX, FY, 0, LineHasSelection); FX += Ds.FX(); } if (NextStyle && Cur+Block >= NextStyle->Start+NextStyle->Len) { // end of this styled block NextStyle = GetNextStyle(Si); } if (NextSelection == Cur+Block) { // selection change DrawSel = !DrawSel; if (DrawSel) { Font->Colour(SelectedText, SelectedBack); } else { LColour fore = l->c.IsValid() ? l->c : Fore; LColour back = l->Back.IsValid() ? l->Back : Back; Font->Colour(fore, back); } NextSelection = (NextSelection == SelMin) ? SelMax : -1; } Done += Block + RtlTrailingSpace; } // end block loop Tr.x1 = FX >> LDisplayString::FShift; // eol processing ssize_t EndOfLine = l->Start+l->Len; if (EndOfLine >= SelMin && EndOfLine < SelMax) { // draw the '\n' at the end of the line as selected // LColour bk = Font->Back(); pOut->Colour(Font->Back()); pOut->Rectangle(Tr.x2, Tr.y1, Tr.x2+7, Tr.y2); Tr.x2 += 7; } else Tr.x2 = Tr.x1; // draw any space after text pOut->Colour(PAINT_AFTER_LINE); pOut->Rectangle(Tr.x2, Tr.y1, r.x2, Tr.y2); // cursor? if (HasFocus) { // draw the cursor if on this line if (Cursor >= l->Start && Cursor <= l->Start+l->Len) { CursorPos.ZOff(1, LineY-1); ssize_t At = Cursor-l->Start; LDisplayString Ds(Font, MapText(Text+l->Start, At), At); Ds.ShowVisibleTab(ShowWhiteSpace); int CursorX = Ds.X(); CursorPos.Offset(d->rPadding.x1 + CursorX, Tr.y1); if (CanScrollX) { // Cursor on screen check LRect Scr = GetClient(); Scr.Offset(ScrollX, 0); LRect Cur = CursorPos; if (Cur.x2 > Scr.x2 - 5) // right edge check { ScrollX = ScrollX + Cur.x2 - Scr.x2 + 40; Invalidate(); } else if (Cur.x1 < Scr.x1 && ScrollX > 0) { ScrollX = MAX(0, Cur.x1 - 40); Invalidate(); } } if (Blink) { LRect c = CursorPos; #ifdef DOUBLE_BUFFER_PAINT c.Offset(-d->rPadding.x1, -y); #endif pOut->Colour(!ReadOnly ? Fore : LColour(192, 192, 192)); pOut->Rectangle(&c); } #if WINNATIVE HIMC hIMC = ImmGetContext(Handle()); if (hIMC) { COMPOSITIONFORM Cf; Cf.dwStyle = CFS_POINT; Cf.ptCurrentPos.x = CursorPos.x1; Cf.ptCurrentPos.y = CursorPos.y1; ImmSetCompositionWindow(hIMC, &Cf); ImmReleaseContext(Handle(), hIMC); } #endif } } #if DRAW_LINE_BOXES { uint Style = pDC->LineStyle(LSurface::LineAlternate); LColour Old = pDC->Colour(LColour::Red); pDC->Box(&OldTr); pDC->Colour(Old); pDC->LineStyle(Style); LString s; s.Printf("%i, %i", Line.IndexOf(l), l->Start); LDisplayString ds(LSysFont, s); LSysFont->Transparent(true); ds.Draw(pDC, OldTr.x2 + 2, OldTr.y1); } #endif #ifdef DOUBLE_BUFFER_PAINT // dump to screen pDC->Blt(d->rPadding.x1, y, pOut); #endif y += LineY; k++; } // end of line loop // draw any space under the lines if (y <= r.y2) { pDC->Colour(Back); // pDC->Colour(LColour(255, 0, 255)); pDC->Rectangle(d->rPadding.x1, y, r.x2, r.y2); } #ifdef DOUBLE_BUFFER_PAINT DeleteObj(pMem); #endif } else { // default drawing: nothing pDC->Colour(Back); pDC->Rectangle(&r); } // _PaintTime = LCurrentTime() - StartTime; #ifdef PAINT_DEBUG if (GetNotify()) { char s[256]; sprintf_s(s, sizeof(s), "Pour:%i Style:%i Paint:%i ms", _PourTime, _StyleTime, _PaintTime); LMessage m = CreateMsg(DEBUG_TIMES_MSG, 0, (int)s); GetNotify()->OnEvent(&m); } #endif // printf("PaintTime: %ims\n", _PaintTime); #if LGI_EXCEPTIONS } catch (...) { LgiMsg(this, "LTextView4::OnPaint crashed.", "Lgi"); } #endif } LMessage::Result LTextView4::OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_TEXT_UPDATE_NAME: { if (d->Lock(_FL)) { Name(d->SetName); d->SetName.Empty(); d->Unlock(); } break; } case M_TEXTVIEW_FIND: { if (InThread()) DoFindNext(NULL); else LgiTrace("%s:%i - Not in thread.\n", _FL); break; } case M_TEXTVIEW_REPLACE: { // DoReplace(); break; } case M_CUT: { Cut(); break; } case M_COPY: { Copy(); break; } case M_PASTE: { Paste(); break; } #if defined WIN32 case WM_GETTEXTLENGTH: { return Size; } case WM_GETTEXT: { int Chars = (int)Msg->A(); char *Out = (char*)Msg->B(); if (Out) { char *In = (char*)LNewConvertCp(LAnsiToLgiCp(), NameW(), LGI_WideCharset, Chars); if (In) { int Len = (int)strlen(In); memcpy(Out, In, Len); DeleteArray(In); return Len; } } return 0; } /* This is broken... the IME returns garbage in the buffer. :( case WM_IME_COMPOSITION: { if (Msg->b & GCS_RESULTSTR) { HIMC hIMC = ImmGetContext(Handle()); if (hIMC) { int Size = ImmGetCompositionString(hIMC, GCS_RESULTSTR, NULL, 0); char *Buf = new char[Size]; if (Buf) { ImmGetCompositionString(hIMC, GCS_RESULTSTR, Buf, Size); char16 *Utf = (char16*)LNewConvertCp(LGI_WideCharset, Buf, LAnsiToLgiCp(), Size); if (Utf) { Insert(Cursor, Utf, StrlenW(Utf)); DeleteArray(Utf); } DeleteArray(Buf); } ImmReleaseContext(Handle(), hIMC); } return 0; } break; } */ #endif } return LLayout::OnEvent(Msg); } int LTextView4::OnNotify(LViewI *Ctrl, LNotification n) { if (Ctrl->GetId() == IDC_VSCROLL && VScroll) { if (n.Type == LNotifyScrollBarCreate) { UpdateScrollBars(); } Invalidate(); } return 0; } void LTextView4::InternalPulse() { if (!ReadOnly) { uint64 Now = LCurrentTime(); if (!BlinkTs) BlinkTs = Now; else if (Now - BlinkTs > CURSOR_BLINK) { Blink = !Blink; LRect p = CursorPos; p.Offset(-ScrollX, 0); Invalidate(&p); BlinkTs = Now; } } if (PartialPour) PourText(Size, 0); } void LTextView4::OnPulse() { #ifdef WINDOWS InternalPulse(); #else for (auto c: Ctrls) c->InternalPulse(); #endif } void LTextView4::OnUrl(char *Url) { if (Environment) Environment->OnNavigate(this, Url); else { LUri u(Url); bool Email = LIsValidEmail(Url); const char *Proto = Email ? "mailto" : u.sProtocol; LString App = LGetAppForProtocol(Proto); if (App) LExecute(App, Url); else LgiMsg(this, "Failed to find application for protocol '%s'", "Error", MB_OK, Proto); } } bool LTextView4::OnLayout(LViewLayoutInfo &Inf) { Inf.Width.Min = 32; Inf.Width.Max = -1; Inf.Height.Min = (Font ? Font->GetHeight() : 18) + 4; Inf.Height.Max = -1; return true; } /////////////////////////////////////////////////////////////////////////////// class LTextView4_Factory : public LViewFactory { LView *NewView(const char *Class, LRect *Pos, const char *Text) { if (_stricmp(Class, "LTextView4") == 0) { return new LTextView4(-1, 0, 0, 2000, 2000); } return 0; } } TextView4_Factory; diff --git a/src/common/Widgets/Box.cpp b/src/common/Widgets/Box.cpp --- a/src/common/Widgets/Box.cpp +++ b/src/common/Widgets/Box.cpp @@ -1,926 +1,926 @@ #include #include "lgi/common/Lgi.h" #include "lgi/common/Box.h" #include "lgi/common/CssTools.h" #include "lgi/common/LgiRes.h" #include "lgi/common/Popup.h" #include "lgi/common/Notifications.h" #undef max #define DEFAULT_MINIMUM_SIZE_PX 5 #define ACTIVE_SPACER_SIZE_PX 9 #if 0 //def _DEBUG #define LOG(...) if (_Debug) LgiTrace(__VA_ARGS__) #else #define LOG(...) #endif static int DefaultSpacerPx() { static int px = -1; if (px < 0) { auto dpi = LScreenDpi(); px = std::max(dpi.x / 17, 5); } return px; }; #define DEFAULT_SPACER_PX DefaultSpacerPx() enum LBoxMessages { M_CHILDREN_CHANGED = M_USER + 0x2000 }; struct LBoxViewInfo { LViewI *View = NULL; LCss::Len Size; // Original size before layout }; struct LBoxPriv { public: bool Vertical = false; bool Dirty = false; LArray Spacers; LBox::Spacer *Dragging = NULL; LPoint DragOffset; LArray Info; LBoxPriv() { } LBoxViewInfo *GetInfo(LViewI *v) { for (auto &i: Info) if (i.View == v) return &i; auto &i = Info.New(); i.View = v; return &i; } int GetBox(LRect &r) { return Vertical ? r.Y() : r.X(); } LBox::Spacer *HitTest(int x, int y) { for (int i=0; iUnregisterHook(this); DeleteObj(d); } bool LBox::IsVertical() { return d->Vertical; } void LBox::SetVertical(bool v) { if (d->Vertical != v) { d->Vertical = v; OnPosChange(); } } LBox::Spacer *LBox::GetSpacer(int idx) { if (Children.Length()) { while (d->Spacers.Length() < Children.Length() - 1) { Spacer &s = d->Spacers.New(); s.SizePx = DEFAULT_SPACER_PX; // s.Colour.c24(DEFAULT_SPACER_COLOUR24); } } return idx >= 0 && idx < d->Spacers.Length() ? &d->Spacers[idx] : NULL; } LViewI *LBox::GetViewAt(int i) { return Children[i]; } bool LBox::SetViewAt(uint32_t i, LViewI *v) { if (!v || i > Children.Length()) { return false; } if (v->GetParent()) v->Detach(); v->Visible(true); bool Status; if (i < Children.Length()) { // Remove existing view.. LViewI *existing = Children[i]; if (existing == v) return true; if (existing) existing->Detach(); Status = AddView(v, i); } else { Status = AddView(v); } if (Status) { AttachChildren(); } return Status; } void LBox::OnCreate() { AttachChildren(); OnPosChange(); LWindow *Wnd = GetWindow(); if (Wnd) Wnd->RegisterHook(this, LMouseEvents); } bool LBox::OnViewMouse(LView *v, LMouse &m) { // This hook allows the LBox to catch clicks nearby the splits even if the splits are too small // to grab normally. Consider the case of a split that is 1px wide. The active region needs to // be a little larger than that, however a normal click would go through to the child windows // on either side of the split rather than to the LBox. if (!m.IsMove() && m.Down()) { // Convert click to the local coordinates of this view LMouse Local = m; while (v && v != (LView*)this && v->GetParent()) { if (dynamic_cast(v)) return true; LRect p = v->GetPos(); Local.x += p.x1; Local.y += p.y1; LViewI *vi = v->GetParent(); v = vi ? vi->GetGView() : NULL; } if (v == (LView*)this) { // Is the click over our spacers? Spacer *s = d->HitTest(Local.x, Local.y); if (s) { // Pass the click to ourselves and prevent the normal view from getting it. OnMouseClick(Local); return false; } } } return true; } bool LBox::Pour(LRegion &r) { LRect *p = FindLargest(r); if (!p) return false; SetPos(*p); return true; } void LBox::OnPaint(LSurface *pDC) { if (d->Dirty) { d->Dirty = false; OnPosChange(); } LRect cli = GetClient(); LCssTools tools(GetCss(), GetFont()); cli = tools.PaintBorderAndPadding(pDC, cli); LColour cBack = StyleColour(LCss::PropBackgroundColor, LColour(L_MED)); size_t ChildViews = Children.Length(); if (ChildViews == 0) { pDC->Colour(cBack); pDC->Rectangle(&cli); } else { #if 0 // coverage check... pDC->Colour(LColour(255, 0, 255)); pDC->Rectangle(&cli); #endif LRegion Painted(cli); for (int i=0; iSpacers.Length(); i++) { Spacer &s = d->Spacers[i]; if (s.Colour.IsValid()) pDC->Colour(s.Colour); else pDC->Colour(cBack); pDC->Rectangle(&s.Pos); Painted.Subtract(&s.Pos); } for (auto c : Children) Painted.Subtract(&c->GetPos()); for (auto r = Painted.First(); r; r = Painted.Next()) { pDC->Colour(cBack); pDC->Rectangle(r); } } } struct BoxRange { int MinPx, MaxPx; LCss::Len Size, Min, Max; LViewI *View; BoxRange &Init() { MinPx = MaxPx = DEFAULT_MINIMUM_SIZE_PX; View = NULL; return *this; } LString toString(const char *label, LCss::Len &l) { if (!l.IsValid()) return ""; LStringPipe p; l.ToString(p); - return LString(", ") + label + "=" + p.NewGStr(); + return LString(", ") + label + "=" + p.NewLStr(); } LString toString() { LStringPipe p; Size.ToString(p); LString s; s.Printf("%s/%p, %i->%i%s%s%s", View?View->GetClass():NULL, View, MinPx, MaxPx, toString("sz", Size).Get(), toString("min", Min).Get(), toString("max", Max).Get()); return s; } }; void LBox::OnPosChange() { LCssTools tools(GetCss(), GetFont()); LRect client = GetClient(); if (!client.Valid()) return; LRect content = tools.ApplyBorder(client); content = tools.ApplyPadding(content); GetSpacer(0); auto views = IterateViews(); int Cur = content.x1, Idx = 0; int AvailablePx = d->GetBox(content); if (AvailablePx <= 0) { LOG("%s:%i - No available px.\n", _FL); return; } LArray Sizes; int SpacerPx = 0; int FixedPx = 0; LArray FixedChildren; int MinPx = 0; int PercentPx = 0; LArray PercentChildren; float PercentCount = 0.0f; LArray AutoChildren; LOG("%s:%i - %i views, %i avail px\n", _FL, (int)views.Length(), AvailablePx); // Do first pass over children and find their sizes for (LViewI *c: views) { LCss *css = c->GetCss(); BoxRange &box = Sizes[Idx].Init(); auto vi = d->GetInfo(c); box.View = c; // Get any available CSS size if (css) { if (IsVertical()) { box.Size = css->Height(); box.Min = css->MinHeight(); box.Max = css->MaxHeight(); } else { box.Size = css->Width(); box.Min = css->MinWidth(); box.Max = css->MaxWidth(); } } // Work out some min and max values if (box.Size.IsValid()) { if (!vi->Size.IsValid()) vi->Size = box.Size; if (box.Size.Type == LCss::LenPercent) { box.MaxPx = box.Size.ToPx(AvailablePx, GetFont()); PercentPx += box.MaxPx; PercentCount += box.Size.Value; PercentChildren.Add(Idx); } else if (box.Size.IsDynamic()) { AutoChildren.Add(Idx); } else { // Fixed children get first crack at the space box.MaxPx = box.Size.ToPx(AvailablePx, GetFont()); FixedPx += box.MaxPx; FixedChildren.Add(Idx); } } else { AutoChildren.Add(Idx); if (vi) { if (IsVertical()) vi->Size = LCss::Len(LCss::LenPx, c->Y()); else vi->Size = LCss::Len(LCss::LenPx, c->X()); } } MinPx += box.MinPx; LOG(" %s\n", box.toString().Get()); // Allocate area for spacers in the Fixed portion if (Idx < Children.Length() - 1) { Spacer &s = d->Spacers[Idx]; SpacerPx += s.SizePx; } Idx++; } LOG(" FixedChildren=" LPrintfSizeT " AutoChildren=" LPrintfSizeT "\n", FixedChildren.Length(), AutoChildren.Length()); // Convert all the percentage sizes to px int RemainingPx = AvailablePx - SpacerPx - FixedPx; LOG(" RemainingPx=%i (%i - %i - %i)\n", RemainingPx, AvailablePx, SpacerPx, FixedPx); if (RemainingPx < 0) { // De-allocate space from the fixed size views... int FitPx = AvailablePx - SpacerPx - MinPx; double Ratio = (double)FitPx / FixedPx; LOG(" Dealloc FitPx=%i Ratio=%.2f\n", FitPx, Ratio); for (size_t i=0; iGetCss(); BoxRange &box = Sizes[i]; if (css) { if (!box.Size.IsDynamic()) { int OldPx = box.MaxPx; int NewPx = (int)(OldPx * Ratio); LAssert(NewPx == 0 || NewPx < OldPx); box.MaxPx = NewPx; box.Size = LCss::Len(LCss::LenPx, NewPx); LOG(" %s: px=%i->%i\n", c->GetClass(), OldPx, NewPx); FixedPx -= OldPx - NewPx; } } } RemainingPx = AvailablePx - SpacerPx - FixedPx; } LOG(" RemainingPx=%i (%i - %i - %i)\n", RemainingPx, AvailablePx, SpacerPx, FixedPx); for (int i=0; i %i\n", box.toString().Get(), PercentPx, RemainingPx); if (PercentPx > RemainingPx) { if (AutoChildren.Length() > 0 || PercentChildren.Length() > 1) { // Well... ah... we better leave _some_ space for them. auto AutoPx = 16 * AutoChildren.Length(); float Ratio = ((float)RemainingPx - AutoPx) / PercentPx; int Px = (int) (box.MaxPx * Ratio); box.Size.Type = LCss::LenPx; box.Size.Value = (float) Px; RemainingPx -= Px; } else { // We can just take all the space... box.Size.Type = LCss::LenPx; box.Size.Value = (float) RemainingPx; RemainingPx = 0; } } else { box.Size.Type = LCss::LenPx; box.Size.Value = (float) box.MaxPx; RemainingPx -= box.MaxPx; } } } // Convert auto children to px auto AutoPx = AutoChildren.Length() > 0 ? RemainingPx / AutoChildren.Length() : 0; LOG(" AutoPx=%i, %i / " LPrintfSizeT "\n", AutoPx, RemainingPx, AutoChildren.Length()); while (AutoChildren.Length()) { auto i = AutoChildren[0]; BoxRange &box = Sizes[i]; AutoChildren.DeleteAt(0, true); LOG(" Auto: %s\n", box.toString().Get()); box.Size.Type = LCss::LenPx; if (AutoChildren.Length() > 0) { box.Size.Type = LCss::LenPx; box.Size.Value = (float) AutoPx; RemainingPx -= (int)AutoPx; } else { box.Size.Type = LCss::LenPx; box.Size.Value = (float) RemainingPx; RemainingPx = 0; } LOG(" AutoAlloc: %s\n", box.toString().Get()); } auto Fnt = GetFont(); for (int i=0; iVertical) { viewPos.y1 = Cur; viewPos.y2 = Cur + Px - 1; } else { viewPos.x1 = Cur; viewPos.x2 = Cur + Px - 1; } box.View->SetPos(viewPos); LOG(" View[%i] = %s.\n", i, viewPos.GetStr()); #ifdef WIN32 // This forces the update, otherwise the child's display lags till the // mouse is released *rolls eyes* box.View->Invalidate((LRect*)NULL, true); #endif Cur += Px; // Allocate area for spacer if (i < Sizes.Length() - 1) { Spacer &s = d->Spacers[i]; s.Pos = content; if (d->Vertical) { s.Pos.y1 = Cur; s.Pos.y2 = Cur + s.SizePx - 1; } else { s.Pos.x1 = Cur; s.Pos.x2 = Cur + s.SizePx - 1; } Cur += s.SizePx; } } } void LBox::OnMouseClick(LMouse &m) { #if 0 { LString::Array a; for (LViewI *p = this; p; p = p->GetParent()) a.New() = p->GetClass(); m.Trace(LString("LBox::OnMouseClick-") + LString(".").Join(a)); } #endif // m.Trace("Lbox click."); if (m.Down()) { d->Dragging = d->HitTest(m.x, m.y); Capture(d->Dragging != NULL); if (d->Dragging) { d->DragOffset.x = m.x - d->Dragging->Pos.x1; d->DragOffset.y = m.y - d->Dragging->Pos.y1; } } else if (IsCapturing()) { Capture(false); d->Dragging = NULL; } } bool IsValidLen(LCss *c, LCss::PropType p) { if (!c || c->GetType(p) != LCss::TypeLen) return false; LCss::Len *l = (LCss::Len*)c->PropAddress(p); if (!l) return false; return l->IsValid(); } void LBox::OnMouseMove(LMouse &m) { // m.Trace("Lbox move"); if (!d->Dragging || !IsCapturing()) { return; } #if 0 { LString::Array a; for (LViewI *p = this; p; p = p->GetParent()) a.New().Printf("%s/%p", p->GetClass(), p); m.Trace(LString("LBox::OnMouseMove-") + LString(".").Join(a)); } #endif if (!m.Down()) { // Something else got the up click? printf("No button down... so uncapturing..\n"); Capture(false); return; } int DragIndex = (int) (d->Dragging - &d->Spacers[0]); if (DragIndex < 0 || DragIndex >= d->Spacers.Length()) { LAssert(0); return; } LViewI *Prev = Children[DragIndex]; if (!Prev) { LAssert(0); return; } LViewI *Next = DragIndex < Children.Length() ? Children[DragIndex+1] : NULL; LCssTools tools(GetCss(), GetFont()); LRect Content = tools.ApplyMargin(GetClient()); int ContentPx = d->GetBox(Content); LRect SplitPos = d->Dragging->Pos; LCss *PrevStyle = Prev->GetCss(); LCss::PropType Style = d->Vertical ? LCss::PropHeight : LCss::PropWidth; bool EditPrev = !Next || IsValidLen(PrevStyle, Style); LViewI *Edit = EditPrev ? Prev : Next; LAssert(Edit != NULL); LRect ViewPos = Edit->GetPos(); auto *EditCss = Edit->GetCss(true); if (d->Vertical) { // Work out the minimum height of the view LCss::Len MinHeight = EditCss->MinHeight(); int MinPx = MinHeight.IsValid() ? MinHeight.ToPx(ViewPos.Y(), Edit->GetFont()) : DEFAULT_MINIMUM_SIZE_PX; int Offset = m.y - d->DragOffset.y - SplitPos.y1; if (Offset) { // Slide up and down the Y axis // Limit to the min size LRect r = ViewPos; if (EditPrev) { r.y2 += Offset; if (r.Y() < MinPx) { int Diff = MinPx - r.Y(); Offset += Diff; r.y2 += Diff; } } else { r.y1 += Offset; if (r.Y() < MinPx) { int Diff = MinPx - r.Y(); Offset -= Diff; r.y1 -= Diff; } } if (Offset) { SplitPos.Offset(0, Offset); // Save the new height of the view LCss::Len Ht = EditCss->Height(); if (Ht.Type == LCss::LenPercent && ContentPx > 0) { Ht.Value = (float)r.Y() * 100 / ContentPx; } else { Ht.Type = LCss::LenPx; Ht.Value = (float)r.Y(); } EditCss->Height(Ht); } } } else { // Work out the minimum width of the view LCss::Len MinWidth = EditCss->MinWidth(); int MinPx = MinWidth.IsValid() ? MinWidth.ToPx(ViewPos.X(), Edit->GetFont()) : DEFAULT_MINIMUM_SIZE_PX; int Offset = m.x - d->DragOffset.x - SplitPos.x1; if (Offset) { // Slide along the X axis // Limit to the min size LRect r = ViewPos; if (EditPrev) { r.x2 += Offset; int rx = r.X(); if (r.X() < MinPx) { int Diff = MinPx - rx; Offset += Diff; r.x2 += Diff; } } else { r.x1 += Offset; int rx = r.X(); if (r.X() < MinPx) { int Diff = MinPx - rx; Offset -= Diff; r.x1 -= Diff; } } if (Offset) { SplitPos.Offset(Offset, 0); // Save the new height of the view LCss::Len Wid = EditCss->Width(); if (Wid.Type == LCss::LenPercent && ContentPx > 0) { Wid.Value = (float)r.X() * 100 / ContentPx; } else { Wid.Type = LCss::LenPx; Wid.Value = (float)r.X(); } EditCss->Width(Wid); } } } OnPosChange(); Invalidate((LRect*)NULL, true); } int LBox::OnNotify(LViewI *Ctrl, LNotification n) { if (n.Type == LNotifyTableLayoutRefresh) { d->Dirty = true; #if LGI_VIEW_HANDLE if (Handle()) #endif PostEvent(M_CHILDREN_CHANGED); } return LView::OnNotify(Ctrl, n); } void LBox::OnChildrenChanged(LViewI *Wnd, bool Attaching) { #if 0 LgiTrace("LBox(%s)::OnChildrenChanged(%s, %i)\n", Name(), Wnd ? Wnd->GetClass() : NULL, Attaching); for (int i=0; iGetClass(), Children[i]->Handle(), Children[i]->Visible()); #endif d->Dirty = true; #if LGI_VIEW_HANDLE if (Handle()) #endif PostEvent(M_CHILDREN_CHANGED); } int64 LBox::Value() { LViewI *v = Children.Length() ? Children[0] : NULL; if (!v) return 0; LCss *css = v->GetCss(); if (!css) return 0; LCss::Len l = d->Vertical ? css->Height() : css->Width(); if (l.Type != LCss::LenPx) return 0; return (int64)l.Value; } void LBox::Value(int64 i) { LViewI *v = Children.Length() ? Children[0] : NULL; if (!v) return; LCss *css = v->GetCss(true); if (!css) return; if (d->Vertical) css->Height(LCss::Len(LCss::LenPx, (float)i)); else css->Width(LCss::Len(LCss::LenPx, (float)i)); OnPosChange(); } LCursor LBox::GetCursor(int x, int y) { Spacer *Over = d->HitTest(x, y); if (Over) return (d->Vertical) ? LCUR_SizeVer : LCUR_SizeHor; else return LCUR_Normal; } bool LBox::OnLayout(LViewLayoutInfo &Inf) { Inf.Width.Min = -1; Inf.Width.Max = -1; Inf.Height.Min = -1; Inf.Height.Max = -1; return true; } bool LBox::Serialize(LDom *Dom, const char *OptName, bool Write) { if (Write) { } else { } LAssert(0); return false; } bool LBox::SetSize(int ViewIndex, LCss::Len Size) { LViewI *v = Children[ViewIndex]; if (!v) return false; LCss *c = v->GetCss(true); if (!c) return false; c->Width(Size); return true; } LMessage::Result LBox::OnEvent(LMessage *Msg) { if (Msg->Msg() == M_CHILDREN_CHANGED) { if (d->Dirty) { d->Dirty = false; OnPosChange(); } } return LView::OnEvent(Msg); } diff --git a/src/common/Widgets/Editor/RichTextEditPriv.cpp b/src/common/Widgets/Editor/RichTextEditPriv.cpp --- a/src/common/Widgets/Editor/RichTextEditPriv.cpp +++ b/src/common/Widgets/Editor/RichTextEditPriv.cpp @@ -1,2499 +1,2499 @@ #include "lgi/common/Lgi.h" #include "lgi/common/RichTextEdit.h" #include "lgi/common/ScrollBar.h" #include "lgi/common/CssTools.h" #include "lgi/common/Menu.h" #include "RichTextEditPriv.h" /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool Utf16to32(LArray &Out, const uint16_t *In, ssize_t WordLen) { if (WordLen == 0) { Out.Length(0); return true; } // Count the length of utf32 chars... const uint16 *Ptr = In; ssize_t Bytes = sizeof(*In) * WordLen; int Chars = 0; while ( Bytes >= sizeof(*In) && LgiUtf16To32(Ptr, Bytes) > 0) Chars++; // Set the output buffer size.. if (!Out.Length(Chars)) return false; // Convert the string... Ptr = (uint16*)In; Bytes = sizeof(*In) * WordLen; uint32_t *o = &Out[0]; #ifdef _DEBUG uint32_t *e = o + Out.Length(); #endif while ( Bytes >= sizeof(*In)) { *o++ = LgiUtf16To32(Ptr, Bytes); } LAssert(o == e); return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// const char *LRichEditElemContext::GetElement(LRichEditElem *obj) { return obj->Tag; } const char *LRichEditElemContext::GetAttr(LRichEditElem *obj, const char *Attr) { const char *a = NULL; obj->Get(Attr, a); return a; } bool LRichEditElemContext::GetClasses(LString::Array &Classes, LRichEditElem *obj) { const char *c; if (!obj->Get("class", c)) return false; LString cls = c; Classes = cls.Split(" "); return Classes.Length() > 0; } LRichEditElem *LRichEditElemContext::GetParent(LRichEditElem *obj) { return dynamic_cast(obj->Parent); } LArray LRichEditElemContext::GetChildren(LRichEditElem *obj) { LArray a; for (unsigned i=0; iChildren.Length(); i++) a.Add(dynamic_cast(obj->Children[i])); return a; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// LCssCache::LCssCache() { Idx = 1; } LCssCache::~LCssCache() { Styles.DeleteObjects(); } uint32_t LCssCache::GetStyles() { uint32_t c = 0; for (unsigned i=0; iRefCount != 0; } return c; } void LCssCache::ZeroRefCounts() { for (unsigned i=0; iRefCount = 0; } } bool LCssCache::OutputStyles(LStream &s, int TabDepth) { char Tabs[64]; memset(Tabs, '\t', TabDepth); Tabs[TabDepth] = 0; for (unsigned i=0; iRefCount > 0) { s.Print("%s.%s {\n", Tabs, ns->Name.Get()); LAutoString a = ns->ToString(); LString all = a.Get(); LString::Array lines = all.Split("\n"); for (unsigned n=0; n &s) { if (!s) return NULL; // Look through existing styles for a match... for (unsigned i=0; iName.Printf("%sStyle%i", p?p:"", Idx++); *(LCss*)ns = *s.Get(); Styles.Add(ns); #if 0 // _DEBUG LAutoString ss = ns->ToString(); if (ss) LgiTrace("%s = %s\n", ns->Name.Get(), ss.Get()); #endif } return ns; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// BlockCursorState::BlockCursorState(bool cursor, LRichTextPriv::BlockCursor *c) { Cursor = cursor; Offset = c->Offset; LineHint = c->LineHint; BlockUid = c->Blk->GetUid(); } bool BlockCursorState::Apply(LRichTextPriv *Ctx, bool Forward) { LAutoPtr &Bc = Cursor ? Ctx->Cursor : Ctx->Selection; if (!Bc) return false; ssize_t o = Bc->Offset; int lh = Bc->LineHint; int uid = Bc->Blk->GetUid(); int Index; LRichTextPriv::Block *b; if (!Ctx->GetBlockByUid(b, BlockUid, &Index)) return false; if (b != Bc->Blk) Bc.Reset(new LRichTextPriv::BlockCursor(b, Offset, LineHint)); else { Bc->Offset = Offset; Bc->LineHint = LineHint; } Offset = o; LineHint = lh; BlockUid = uid; return true; } /// This is the simplest form of undo, just save the entire block state, and restore it if needed CompleteTextBlockState::CompleteTextBlockState(LRichTextPriv *Ctx, LRichTextPriv::TextBlock *Tb) { if (Ctx->Cursor) Cur.Reset(new BlockCursorState(true, Ctx->Cursor)); if (Ctx->Selection) Sel.Reset(new BlockCursorState(false, Ctx->Selection)); if (Tb) { Uid = Tb->GetUid(); Blk.Reset(new LRichTextPriv::TextBlock(Tb)); } } bool CompleteTextBlockState::Apply(LRichTextPriv *Ctx, bool Forward) { int Index; LRichTextPriv::TextBlock *b; if (!Ctx->GetBlockByUid(b, Uid, &Index)) return false; // Swap the local state with the block in the ctx Blk->UpdateSpellingAndLinks(NULL, LRange(0, Blk->Length())); Ctx->Blocks[Index] = Blk.Release(); Blk.Reset(b); // Update cursors if (Cur) Cur->Apply(Ctx, Forward); if (Sel) Sel->Apply(Ctx, Forward); return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// MultiBlockState::MultiBlockState(LRichTextPriv *ctx, ssize_t Start) { Ctx = ctx; Index = Start; Length = -1; } bool MultiBlockState::Apply(LRichTextPriv *Ctx, bool Forward) { if (Index < 0 || Length < 0) { LAssert(!"Missing parameters"); return false; } // Undo: Swap 'Length' blocks Ctx->Blocks with Blks ssize_t OldLen = Blks.Length(); bool Status = Blks.SwapRange(LRange(0, OldLen), Ctx->Blocks, LRange(Index, Length)); if (Status) Length = OldLen; return Status; } bool MultiBlockState::Copy(ssize_t Idx) { if (!Ctx->Blocks.AddressOf(Idx)) return false; LRichTextPriv::Block *b = Ctx->Blocks[Idx]->Clone(); if (!b) return false; Blks.Add(b); return true; } bool MultiBlockState::Cut(ssize_t Idx) { if (!Ctx->Blocks.AddressOf(Idx)) return false; LRichTextPriv::Block *b = Ctx->Blocks[Idx]; if (!b) return false; Blks.Add(b); return Ctx->Blocks.DeleteAt(Idx, true); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// LRichTextPriv::LRichTextPriv(LRichTextEdit *view, LRichTextPriv **Ptr) : LHtmlParser(view), LFontCache(LSysFont) { if (Ptr) *Ptr = this; BlinkTs = 0; View = view; Log = &LogBuffer; NextUid = 1; UndoPos = 0; UndoPosLock = false; WordSelectMode = false; Dirty = false; ScrollOffsetPx = 0; ShowTools = true; ScrollChange = false; DocumentExtent.x = 0; DocumentExtent.y = 0; SpellCheck = NULL; SpellDictionaryLoaded = false; HtmlLinkAsCid = false; ScrollLinePx = LSysFont->GetHeight(); if (Font.Reset(new LFont)) *Font = *LSysFont; for (unsigned i=0; i DeletedText; LArray *DelTxt = Cut ? &DeletedText : NULL; bool Cf = CursorFirst(); LRichTextPriv::BlockCursor *Start = Cf ? Cursor : Selection; LRichTextPriv::BlockCursor *End = Cf ? Selection : Cursor; if (Start->Blk == End->Blk) { // In the same block... just delete the text ssize_t Len = End->Offset - Start->Offset; LRichTextPriv::Block *NextBlk = Next(Start->Blk); if (Len >= Start->Blk->Length() && NextBlk) { // Delete entire block ssize_t i = Blocks.IndexOf(Start->Blk); LAutoPtr MultiState(new MultiBlockState(this, i)); MultiState->Cut(i); MultiState->Length = 0; Start->Set(NextBlk, 0, 0); Trans->Add(MultiState.Release()); } else { Start->Blk->DeleteAt(Trans, Start->Offset, Len, DelTxt); } } else { // Multi-block delete... ssize_t i = Blocks.IndexOf(Start->Blk); ssize_t e = Blocks.IndexOf(End->Blk); LAutoPtr MultiState(new MultiBlockState(this, i)); // 1) Delete all the content to the end of the first block ssize_t StartLen = Start->Blk->Length(); if (Start->Offset < StartLen) { MultiState->Copy(i++); Start->Blk->DeleteAt(NoTransaction, Start->Offset, StartLen - Start->Offset, DelTxt); } else if (Start->Offset == StartLen) { // This can happen because there is an implied '\n' at the end of each block. The // next block always starts on a new line. We just increment the index 'i' to avoid // the next loop deleting the start block. i++; // If the start and end blocks can be merged, the new line will eventually get removed // then. If not, then... well whatevs. } else { LAssert(0); return Error(_FL, "Cursor outside start block."); } // 2) Delete any blocks between 'Start' and 'End' if (i >= 0) { while (Blocks[i] != End->Blk && i < (int)Blocks.Length()) { LRichTextPriv::Block *b = Blocks[i]; b->CopyAt(0, -1, DelTxt); printf("%s:%i - inter, %i\n", _FL, (int)i); MultiState->Cut(i); e--; } } else { LAssert(0); return Error(_FL, "Start block has no index.");; } if (End->Offset > 0) { // 3) Delete any text up to the Cursor in the 'End' block MultiState->Copy(i); printf("%s:%i - end, %i-%i, %i\n", _FL, (int)0, (int)End->Offset, (int)End->Blk->Length()); End->Blk->DeleteAt(NoTransaction, 0, End->Offset, DelTxt); } // Try and merge the start and end blocks bool MergeOk = Merge(NoTransaction, Start->Blk, End->Blk); MultiState->Length = MergeOk ? 1 : 2; Trans->Add(MultiState.Release()); } // Set the cursor and update the screen Cursor->Set(Start->Blk, Start->Offset, Start->LineHint); Selection.Reset(); View->Invalidate(); if (Cut) { DelTxt->Add(0); *Cut = (char16*)LNewConvertCp(LGI_WideCharset, &DelTxt->First(), "utf-32", DelTxt->Length()*sizeof(uint32_t)); } return true; } LRichTextPriv::Block *LRichTextPriv::Next(Block *b) { ssize_t Idx = Blocks.IndexOf(b); if (Idx < 0) return NULL; if (++Idx >= (int)Blocks.Length()) return NULL; return Blocks[Idx]; } LRichTextPriv::Block *LRichTextPriv::Prev(Block *b) { ssize_t Idx = Blocks.IndexOf(b); if (Idx <= 0) return NULL; return Blocks[--Idx]; } bool LRichTextPriv::AddTrans(LAutoPtr &t) { if (t) { if (UndoPosLock) { LgiTrace("%s:%i - AddTrans failed - UndoPosLocked.\n", _FL); return false; } // Delete any transaction history after 'UndoPos' for (size_t i=UndoPos; i UndoPos) { // Forward in queue Transaction *t = UndoQue[UndoPos]; UndoPosLock = true; if (!t->Apply(this, true)) goto ApplyError; UndoPosLock = false; LAssert(UndoPos == Prev); UndoPos++; } else if (Pos < UndoPos) { // Back in queue Transaction *t = UndoQue[UndoPos-1]; UndoPosLock = true; if (!t->Apply(this, false)) goto ApplyError; UndoPosLock = false; LAssert(UndoPos == Prev); UndoPos--; } else break; // We are done } Dirty = true; InvalidateDoc(NULL); return true; ApplyError: UndoPosLock = false; return false; } bool LRichTextPriv::IsBusy(bool Stop) { for (unsigned i=0; iIsBusy(Stop)) return true; } return false; } bool LRichTextPriv::Error(const char *file, int line, const char *fmt, ...) { va_list Arg; va_start(Arg, fmt); LString s; LPrintf(s, fmt, Arg); va_end(Arg); LString Err; Err.Printf("%s:%i - Error: %s\n", file, line, s.Get()); Log->Write(Err, Err.Length()); - Err = LogBuffer.NewGStr(); + Err = LogBuffer.NewLStr(); LgiTrace("%.*s", Err.Length(), Err.Get()); LAssert(0); return false; } void LRichTextPriv::UpdateStyleUI() { if (!Cursor || !Cursor->Blk) { Error(_FL, "Not a valid cursor."); return; } TextBlock *b = dynamic_cast(Cursor->Blk); LArray Styles; if (b) b->GetTextAt(Cursor->Offset, Styles); StyleText *st = Styles.Length() ? Styles.First() : NULL; LFont *f = NULL; if (st) f = GetFont(st->GetStyle()); else if (View) f = View->GetFont(); else if (LAppInst) f = LSysFont; if (f) { Values[LRichTextEdit::FontFamilyBtn] = f->Face(); Values[LRichTextEdit::FontSizeBtn] = f->PointSize(); Values[LRichTextEdit::FontSizeBtn].CastString(); Values[LRichTextEdit::BoldBtn] = f->Bold(); Values[LRichTextEdit::ItalicBtn] = f->Italic(); Values[LRichTextEdit::UnderlineBtn] = f->Underline(); } else { Values[LRichTextEdit::FontFamilyBtn] = "(Unknown)"; } Values[LRichTextEdit::ForegroundColourBtn] = (int64) (st && st->Colours.Fore.IsValid() ? st->Colours.Fore.c32() : TextColour.c32()); Values[LRichTextEdit::BackgroundColourBtn] = (int64) (st && st->Colours.Back.IsValid() ? st->Colours.Back.c32() : 0); if (View) View->Invalidate(Areas + LRichTextEdit::ToolsArea); } void LRichTextPriv::ScrollTo(LRect r) { LRect Content = Areas[LRichTextEdit::ContentArea]; Content.Offset(-Content.x1, ScrollOffsetPx-Content.y1); if (ScrollLinePx > 0) { if (r.y1 < Content.y1) { int OffsetPx = MAX(r.y1, 0); View->SetScrollPos(0, OffsetPx / ScrollLinePx); InvalidateDoc(NULL); } if (r.y2 > Content.y2) { int OffsetPx = r.y2 - Content.Y(); View->SetScrollPos(0, (OffsetPx + ScrollLinePx - 1) / ScrollLinePx); InvalidateDoc(NULL); } } } void LRichTextPriv::InvalidateDoc(LRect *r) { // Transform the coordinates from doc to screen space LRect &c = Areas[LRichTextEdit::ContentArea]; if (r) { LRect t = *r; t.Offset(c.x1, c.y1 - ScrollOffsetPx); View->Invalidate(&t); } else View->Invalidate(&c); } void LRichTextPriv::EmptyDoc() { Block *Def = new TextBlock(this); if (Def) { Blocks.Add(Def); Cursor.Reset(new BlockCursor(Def, 0, 0)); UpdateStyleUI(); } } void LRichTextPriv::Empty() { // Delete cursors first to avoid hanging references Cursor.Reset(); Selection.Reset(); // Clear the block list.. Blocks.DeleteObjects(); } bool LRichTextPriv::Seek(BlockCursor *In, SeekType Dir, bool Select) { if (!In || !In->Blk || Blocks.Length() == 0) return Error(_FL, "Not a valid 'In' cursor, Blks=%i", Blocks.Length()); LAutoPtr c; bool Status = false; switch (Dir) { case SkLineEnd: case SkLineStart: case SkUpLine: case SkDownLine: { if (!c.Reset(new BlockCursor(*In))) break; Block *b = c->Blk; Status = b->Seek(Dir, *c); if (Status) break; if (Dir == SkUpLine) { // No more lines in the current block... // Move to the next block. ssize_t CurIdx = Blocks.IndexOf(b); ssize_t NewIdx = CurIdx - 1; if (NewIdx >= 0) { Block *b = Blocks[NewIdx]; if (!b) return Error(_FL, "No block at %i", NewIdx); c.Reset(new BlockCursor(b, b->Length(), b->GetLines() - 1)); LAssert(c->Offset >= 0); Status = true; } } else if (Dir == SkDownLine) { // No more lines in the current block... // Move to the next block. ssize_t CurIdx = Blocks.IndexOf(b); ssize_t NewIdx = CurIdx + 1; if ((unsigned)NewIdx < Blocks.Length()) { Block *b = Blocks[NewIdx]; if (!b) return Error(_FL, "No block at %i", NewIdx); c.Reset(new BlockCursor(b, 0, 0)); LAssert(c->Offset >= 0); Status = true; } } break; } case SkDocStart: { if (!c.Reset(new BlockCursor(Blocks[0], 0, 0))) break; Status = true; break; } case SkDocEnd: { if (Blocks.Length() == 0) break; Block *l = Blocks.Last(); if (!c.Reset(new BlockCursor(l, l->Length(), -1))) break; Status = true; break; } case SkLeftChar: { if (!c.Reset(new BlockCursor(*In))) break; if (c->Offset > 0) { LArray Ln; c->Blk->OffsetToLine(c->Offset, NULL, &Ln); if (Ln.Length() == 2 && c->LineHint == Ln.Last()) { c->LineHint = Ln.First(); } else { c->Offset--; if (c->Blk->OffsetToLine(c->Offset, NULL, &Ln)) c->LineHint = Ln.First(); } Status = true; } else // Seek to previous block { SeekPrevBlock: ssize_t Idx = Blocks.IndexOf(c->Blk); if (Idx < 0) { LAssert(0); break; } if (Idx == 0) break; // Beginning of document Block *b = Blocks[--Idx]; if (!b) { LAssert(0); break; } if (!c.Reset(new BlockCursor(b, b->Length(), b->GetLines()-1))) break; Status = true; } break; } case SkLeftWord: { if (!c.Reset(new BlockCursor(*In))) break; if (c->Offset > 0) { LArray a; c->Blk->CopyAt(0, c->Offset, &a); ssize_t i = c->Offset; while (i > 0 && IsWordBreakChar(a[i-1])) i--; while (i > 0 && !IsWordBreakChar(a[i-1])) i--; c->Offset = i; LArray Ln; if (c->Blk->OffsetToLine(c->Offset, NULL, &Ln)) c->LineHint = Ln[0]; Status = true; } else // Seek into previous block? { goto SeekPrevBlock; } break; } case SkRightChar: { if (!c.Reset(new BlockCursor(*In))) break; if (c->Offset < c->Blk->Length()) { LArray Ln; if (c->Blk->OffsetToLine(c->Offset, NULL, &Ln) && Ln.Length() == 2 && c->LineHint == Ln.First()) { c->LineHint = Ln.Last(); } else { c->Offset++; if (c->Blk->OffsetToLine(c->Offset, NULL, &Ln)) c->LineHint = Ln.First(); } Status = true; } else // Seek to next block { SeekNextBlock: ssize_t Idx = Blocks.IndexOf(c->Blk); if (Idx < 0) return Error(_FL, "Block ptr index error."); if (Idx >= (int)Blocks.Length() - 1) break; // End of document Block *b = Blocks[++Idx]; if (!b) return Error(_FL, "No block at %i.", Idx); if (!c.Reset(new BlockCursor(b, 0, 0))) break; Status = true; } break; } case SkRightWord: { if (!c.Reset(new BlockCursor(*In))) break; if (c->Offset < c->Blk->Length()) { LArray a; ssize_t RemainingCh = c->Blk->Length() - c->Offset; c->Blk->CopyAt(c->Offset, RemainingCh, &a); int i = 0; while (i < RemainingCh && !IsWordBreakChar(a[i])) i++; while (i < RemainingCh && IsWordBreakChar(a[i])) i++; c->Offset += i; LArray Ln; if (c->Blk->OffsetToLine(c->Offset, NULL, &Ln)) c->LineHint = Ln.Last(); else c->LineHint = -1; Status = true; } else // Seek into next block? { goto SeekNextBlock; } break; } case SkUpPage: { LRect &Content = Areas[LRichTextEdit::ContentArea]; int LineHint = -1; int TargetY = In->Pos.y1 - Content.Y(); ssize_t Idx = HitTest(In->Pos.x1, MAX(TargetY, 0), LineHint); if (Idx >= 0) { ssize_t Offset = -1; Block *b = GetBlockByIndex(Idx, &Offset); if (b) { if (!c.Reset(new BlockCursor(b, Offset, LineHint))) break; Status = true; } } break; } case SkDownPage: { LRect &Content = Areas[LRichTextEdit::ContentArea]; int LineHint = -1; int TargetY = In->Pos.y1 + Content.Y(); ssize_t Idx = HitTest(In->Pos.x1, MIN(TargetY, DocumentExtent.y-1), LineHint); if (Idx >= 0) { ssize_t Offset = -1; int BlkIdx = -1; ssize_t CursorBlkIdx = Blocks.IndexOf(Cursor->Blk); Block *b = GetBlockByIndex(Idx, &Offset, &BlkIdx); if (!b || BlkIdx < CursorBlkIdx || (BlkIdx == CursorBlkIdx && Offset < Cursor->Offset)) { LAssert(!"GetBlockByIndex failed.\n"); LgiTrace("%s:%i - GetBlockByIndex failed.\n", _FL); } if (b) { if (!c.Reset(new BlockCursor(b, Offset, LineHint))) break; Status = true; } } break; } default: { return Error(_FL, "Unknown seek type."); } } if (Status) SetCursor(c, Select); return Status; } bool LRichTextPriv::CursorFirst() { if (!Cursor || !Selection) return true; ssize_t CIdx = Blocks.IndexOf(Cursor->Blk); ssize_t SIdx = Blocks.IndexOf(Selection->Blk); if (CIdx != SIdx) return CIdx < SIdx; return Cursor->Offset < Selection->Offset; } bool LRichTextPriv::SetCursor(LAutoPtr c, bool Select) { LRect InvalidRc(0, 0, -1, -1); if (!c || !c->Blk) return Error(_FL, "Invalid cursor."); if (Select && !Selection) { // Selection starting... save cursor as selection end point if (Cursor) InvalidRc = Cursor->Line; Selection = Cursor; } else if (!Select && Selection) { // Selection ending... invalidate selection region and delete // selection end point LRect r = SelectionRect(); InvalidateDoc(&r); Selection.Reset(); // LgiTrace("Ending selection delete(sel) Idx=%i\n", i); } else if (Select && Cursor) { // Changing selection... InvalidRc = Cursor->Line; } if (Cursor && !Select) { // Just moving cursor InvalidateDoc(&Cursor->Pos); } if (!Cursor) Cursor.Reset(new BlockCursor(*c)); else Cursor = c; // LgiTrace("%s:%i - SetCursor: %i, line: %i\n", _FL, Cursor->Offset, Cursor->LineHint); if (Cursor && Selection && *Cursor == *Selection) Selection.Reset(); Cursor->Blk->GetPosFromIndex(Cursor); UpdateStyleUI(); #if DEBUG_OUTLINE_CUR_DISPLAY_STR || DEBUG_OUTLINE_CUR_STYLE_TEXT InvalidateDoc(NULL); #else if (Select) InvalidRc.Union(&Cursor->Line); else InvalidateDoc(&Cursor->Pos); if (InvalidRc.Valid()) { // Update the screen InvalidateDoc(&InvalidRc); } #endif // Check the cursor is on the visible part of the document. if (Cursor->Pos.Valid()) ScrollTo(Cursor->Pos); return true; } LRect LRichTextPriv::SelectionRect() { LRect SelRc; if (Cursor) { SelRc = Cursor->Line; if (Selection) SelRc.Union(&Selection->Line); } else if (Selection) { SelRc = Selection->Line; } return SelRc; } ssize_t LRichTextPriv::IndexOfCursor(BlockCursor *c) { if (!c || !c->Blk) { Error(_FL, "Invalid cursor param."); return -1; } ssize_t CharPos = 0; for (unsigned i=0; iBlk == b) return CharPos + c->Offset; CharPos += b->Length(); } LAssert(0); return -1; } LPoint LRichTextPriv::ScreenToDoc(int x, int y) { LRect &Content = Areas[LRichTextEdit::ContentArea]; return LPoint(x - Content.x1, y - Content.y1 + ScrollOffsetPx); } LPoint LRichTextPriv::DocToScreen(int x, int y) { LRect &Content = Areas[LRichTextEdit::ContentArea]; return LPoint(x + Content.x1, y + Content.y1 - ScrollOffsetPx); } bool LRichTextPriv::Merge(Transaction *Trans, Block *a, Block *b) { TextBlock *ta = dynamic_cast(a); TextBlock *tb = dynamic_cast(b); if (!ta || !tb) return false; ta->Txt.Add(tb->Txt); ta->LayoutDirty = true; ta->Len += tb->Len; tb->Txt.Length(0); Blocks.Delete(b, true); Dirty = true; return true; } LSurface *LEmojiImage::GetEmojiImage() { if (!EmojiImg) { LString p = LGetSystemPath(LSP_APP_INSTALL); if (!p) { LgiTrace("%s:%i - No app install path.\n", _FL); return NULL; } char File[MAX_PATH_LEN] = ""; LMakePath(File, sizeof(File), p, "..\\src\\common\\Text\\Emoji\\EmojiMap.png"); LString a; if (!LFileExists(File)) a = LFindFile("EmojiMap.png"); EmojiImg.Reset(GdcD->Load(a ? a : File, false)); } return EmojiImg; } ssize_t LRichTextPriv::HitTest(int x, int y, int &LineHint, Block **Blk, ssize_t *BlkOffset) { ssize_t CharPos = 0; HitTestResult r(x, y); if (Blocks.Length() == 0) { Error(_FL, "No blocks."); return -1; } Block *b = Blocks.First(); LRect rc = b->GetPos(); if (y < rc.y1) { if (Blk) *Blk = b; return 0; } for (unsigned i=0; iGetPos(); bool Over = y >= p.y1 && y <= p.y2; if (b->HitTest(r)) { LineHint = r.LineHint; if (Blk) *Blk = b; if (BlkOffset) *BlkOffset = r.Idx; return CharPos + r.Idx; } else if (Over) { Error(_FL, "Block failed to hit, i=%i, pos=%s, y=%i.", i, p.GetStr(), y); } CharPos += b->Length(); } b = Blocks.Last(); rc = b->GetPos(); if (y > rc.y2) { if (Blk) *Blk = b; return CharPos + b->Length(); } return -1; } bool LRichTextPriv::CursorFromPos(int x, int y, LAutoPtr *Cursor, ssize_t *GlobalIdx) { ssize_t CharPos = 0; HitTestResult r(x, y); for (unsigned i=0; iHitTest(r)) { if (Cursor) Cursor->Reset(new BlockCursor(b, r.Idx, r.LineHint)); if (GlobalIdx) *GlobalIdx = CharPos + r.Idx; return true; } CharPos += b->Length(); } return false; } LRichTextPriv::Block *LRichTextPriv::GetBlockByIndex(ssize_t Index, ssize_t *Offset, int *BlockIdx, int *LineCount) { ssize_t CharPos = 0; int Lines = 0; for (unsigned i=0; iLength(); int Ln = b->GetLines(); if (Index >= CharPos && Index < CharPos + Len) { if (BlockIdx) *BlockIdx = i; if (Offset) *Offset = Index - CharPos; return b; } CharPos += b->Length(); Lines += Ln; } Block *b = Blocks.Last(); if (Offset) *Offset = b->Length(); if (BlockIdx) *BlockIdx = (int)Blocks.Length() - 1; if (LineCount) *LineCount = Lines; return b; } bool LRichTextPriv::Layout(LScrollBar *&ScrollY) { Flow f(this); ScrollLinePx = View->GetFont()->GetHeight(); LAssert(ScrollLinePx > 0); if (ScrollLinePx <= 0) ScrollLinePx = 16; LRect Client = Areas[LRichTextEdit::ContentArea]; Client.Offset(-Client.x1, -Client.y1); DocumentExtent.x = Client.X(); LCssTools Ct(this, Font); LRect Content = Ct.ApplyPadding(Client); f.Left = Content.x1; f.Right = Content.x2; f.Top = f.CurY = Content.y1; for (unsigned i=0; iOnLayout(f); if ((f.CurY > Client.Y()) && ScrollY==NULL && !ScrollChange) { // We need to add a scroll bar View->SetScrollBars(false, true); View->Invalidate(); ScrollChange = true; return false; } } DocumentExtent.y = f.CurY + (Client.y2 - Content.y2); if (ScrollY) { int Lines = (f.CurY + ScrollLinePx - 1) / ScrollLinePx; int PageLines = (Client.Y() + ScrollLinePx - 1) / ScrollLinePx; ScrollY->SetPage(PageLines); ScrollY->SetRange(Lines); } if (Cursor) { LAssert(Cursor->Blk != NULL); if (Cursor->Blk) Cursor->Blk->GetPosFromIndex(Cursor); } return true; } void LRichTextPriv::OnStyleChange(LRichTextEdit::RectType t) { LCss s; switch (t) { case LRichTextEdit::FontFamilyBtn: { LCss::StringsDef Fam(Values[t].Str()); s.FontFamily(Fam); if (!ChangeSelectionStyle(&s, true)) StyleDirty.Add(t); break; } case LRichTextEdit::FontSizeBtn: { double Pt = Values[t].CastDouble(); s.FontSize(LCss::Len(LCss::LenPt, (float)Pt)); if (!ChangeSelectionStyle(&s, true)) StyleDirty.Add(t); break; } case LRichTextEdit::BoldBtn: { s.FontWeight(LCss::FontWeightBold); if (!ChangeSelectionStyle(&s, Values[t].CastBool())) StyleDirty.Add(t); break; } case LRichTextEdit::ItalicBtn: { s.FontStyle(LCss::FontStyleItalic); if (!ChangeSelectionStyle(&s, Values[t].CastBool())) StyleDirty.Add(t); break; } case LRichTextEdit::UnderlineBtn: { s.TextDecoration(LCss::TextDecorUnderline); if (ChangeSelectionStyle(&s, Values[t].CastBool())) StyleDirty.Add(t); break; } case LRichTextEdit::ForegroundColourBtn: { s.Color(LCss::ColorDef(LCss::ColorRgb, (uint32_t) Values[t].Value.Int64)); if (!ChangeSelectionStyle(&s, true)) StyleDirty.Add(t); break; } case LRichTextEdit::BackgroundColourBtn: { s.BackgroundColor(LCss::ColorDef(LCss::ColorRgb, (uint32_t) Values[t].Value.Int64)); if (!ChangeSelectionStyle(&s, true)) StyleDirty.Add(t); break; } default: break; } } bool LRichTextPriv::ChangeSelectionStyle(LCss *Style, bool Add) { if (!Selection) return false; LAutoPtr Trans(new Transaction); bool Cf = CursorFirst(); LRichTextPriv::BlockCursor *Start = Cf ? Cursor : Selection; LRichTextPriv::BlockCursor *End = Cf ? Selection : Cursor; if (Start->Blk == End->Blk) { // Change style in the same block... ssize_t Len = End->Offset - Start->Offset; if (!Start->Blk->ChangeStyle(Trans, Start->Offset, Len, Style, Add)) return false; } else { // Multi-block style change... // 1) Change style on the content to the end of the first block Start->Blk->ChangeStyle(Trans, Start->Offset, -1, Style, Add); // 2) Change style on blocks between 'Start' and 'End' ssize_t i = Blocks.IndexOf(Start->Blk); if (i >= 0) { for (++i; Blocks[i] != End->Blk && i < (int)Blocks.Length(); i++) { LRichTextPriv::Block *&b = Blocks[i]; if (!b->ChangeStyle(Trans, 0, -1, Style, Add)) return false; } } else { return Error(_FL, "Start block has no index."); } // 3) Change style up to the Cursor in the 'End' block if (!End->Blk->ChangeStyle(Trans, 0, End->Offset, Style, Add)) return false; } Cursor->Pos.ZOff(-1, -1); InvalidateDoc(NULL); AddTrans(Trans); return true; } void LRichTextPriv::PaintBtn(LSurface *pDC, LRichTextEdit::RectType t) { LRect r = Areas[t]; LVariant &v = Values[t]; bool Down = (v.Type == GV_BOOL && v.Value.Bool) || (BtnState[t].IsPress && BtnState[t].Pressed && BtnState[t].MouseOver); LSysFont->Colour(L_TEXT, BtnState[t].MouseOver ? L_LIGHT : L_MED); LSysFont->Transparent(false); LColour Low(96, 96, 96); pDC->Colour(Down ? LColour::White : Low); pDC->Line(r.x1, r.y2, r.x2, r.y2); pDC->Line(r.x2, r.y1, r.x2, r.y2); pDC->Colour(Down ? Low : LColour::White); pDC->Line(r.x1, r.y1, r.x2, r.y1); pDC->Line(r.x1, r.y1, r.x1, r.y2); r.Inset(1, 1); switch (v.Type) { case GV_STRING: { LDisplayString Ds(LSysFont, v.Str()); Ds.Draw(pDC, r.x1 + ((r.X()-Ds.X())>>1) + Down, r.y1 + ((r.Y()-Ds.Y())>>1) + Down, &r); break; } case GV_INT64: { if (v.Value.Int64) { pDC->Colour((uint32_t)v.Value.Int64, 32); pDC->Rectangle(&r); } else { // Transparent int g[2] = { 128, 192 }; pDC->ClipRgn(&r); for (int y=0; y>1)%2) ^ ((x>>1)%2); pDC->Colour(LColour(g[i],g[i],g[i])); pDC->Rectangle(r.x1+x, r.y1+y, r.x1+x+1, r.y1+y+1); } } pDC->ClipRgn(NULL); } break; } case GV_BOOL: { const char *Label = NULL; switch (t) { case LRichTextEdit::BoldBtn: Label = "B"; break; case LRichTextEdit::ItalicBtn: Label = "I"; break; case LRichTextEdit::UnderlineBtn: Label = "U"; break; default: break; } if (!Label) break; LDisplayString Ds(LSysFont, Label); Ds.Draw(pDC, r.x1 + ((r.X()-Ds.X())>>1) + Down, r.y1 + ((r.Y()-Ds.Y())>>1) + Down, &r); break; } default: break; } } bool LRichTextPriv::MakeLink(TextBlock *tb, ssize_t Offset, ssize_t Len, LString Link) { if (!tb) return false; LArray st; if (!tb->GetTextAt(Offset, st)) return false; LAutoPtr ns(new LNamedStyle); if (ns) { if (st.Last()->GetStyle()) *ns = *st.Last()->GetStyle(); ns->TextDecoration(LCss::TextDecorUnderline); ns->Color(LCss::ColorDef(LCss::ColorRgb, LColour::Blue.c32())); LAutoPtr Trans(new Transaction); tb->ChangeStyle(Trans, Offset, Len, ns, true); if (tb->GetTextAt(Offset + 1, st)) { st.First()->Element = TAG_A; st.First()->Param = Link; } AddTrans(Trans); } return true; } bool LRichTextPriv::ClickBtn(LMouse &m, LRichTextEdit::RectType t) { switch (t) { case LRichTextEdit::FontFamilyBtn: { LString::Array Fonts; if (!LFontSystem::Inst()->EnumerateFonts(Fonts)) return Error(_FL, "EnumerateFonts failed."); bool UseSub = (LSysFont->GetHeight() * Fonts.Length()) > (GdcD->Y() * 0.8); LSubMenu s; LSubMenu *Cur = NULL; int Idx = 1; char Last = 0; for (unsigned i=0; iAppendItem(f, Idx++); else break; } else { s.AppendItem(f, Idx++); } } LPoint p(Areas[t].x1, Areas[t].y2 + 1); View->PointToScreen(p); int Result = s.Float(View, p.x, p.y, true); if (Result) { Values[t] = Fonts[Result-1]; View->Invalidate(Areas+t); OnStyleChange(t); } break; } case LRichTextEdit::FontSizeBtn: { static const char *Sizes[] = { "6", "7", "8", "9", "10", "11", "12", "14", "16", "18", "20", "24", "28", "32", "40", "50", "60", "80", "100", "120", 0 }; LSubMenu s; for (int Idx = 0; Sizes[Idx]; Idx++) s.AppendItem(Sizes[Idx], Idx+1); LPoint p(Areas[t].x1, Areas[t].y2 + 1); View->PointToScreen(p); int Result = s.Float(View, p.x, p.y, true); if (Result) { Values[t] = Sizes[Result-1]; View->Invalidate(Areas+t); OnStyleChange(t); } break; } case LRichTextEdit::BoldBtn: case LRichTextEdit::ItalicBtn: case LRichTextEdit::UnderlineBtn: { Values[t] = !Values[t].CastBool(); View->Invalidate(Areas+t); OnStyleChange(t); break; } case LRichTextEdit::ForegroundColourBtn: case LRichTextEdit::BackgroundColourBtn: { LPoint p(Areas[t].x1, Areas[t].y2 + 1); View->PointToScreen(p); new SelectColour(this, p, t); break; } case LRichTextEdit::MakeLinkBtn: { if (!Cursor || !Cursor->Blk) break; TextBlock *tb = dynamic_cast(Cursor->Blk); if (!tb) break; LArray st; if (!tb->GetTextAt(Cursor->Offset, st)) break; StyleText *a = st.Length() > 1 && st[1]->Element == TAG_A ? st[1] : st.First()->Element == TAG_A ? st[0] : NULL; if (a) { // Edit the existing link... auto i = new LInput(View, a->Param, "Edit link:", "Link"); i->DoModal([this, i, a](auto dlg, auto ctrlId) { if (ctrlId == IDOK) a->Param = i->GetStr(); delete dlg; }); } else if (Selection) { // Turn current selection into link auto i = new LInput(View, NULL, "Edit link:", "Link"); i->DoModal([this, i, tb](auto dlg, auto ctrlId) { if (ctrlId == IDOK) { BlockCursor *Start = CursorFirst() ? Cursor : Selection; BlockCursor *End = CursorFirst() ? Selection : Cursor; if (!Start || !End) ; else if (Start->Blk != End->Blk) LgiMsg(View, "Selection too large.", "Error"); else MakeLink(tb, Start->Offset, End->Offset - Start->Offset, i->GetStr()); } delete dlg; }); } break; } case LRichTextEdit::RemoveLinkBtn: { if (!Cursor || !Cursor->Blk) break; TextBlock *tb = dynamic_cast(Cursor->Blk); if (!tb) break; LArray st; if (!tb->GetTextAt(Cursor->Offset, st)) break; StyleText *a = st.Length() > 1 && st[1]->Element == TAG_A ? st[1] : st.First()->Element == TAG_A ? st[0] : NULL; if (a) { // Remove the existing link... a->Element = CONTENT; a->Param.Empty(); LAutoPtr s(new LCss); LNamedStyle *Ns = a->GetStyle(); if (Ns) *s = *Ns; if (s->TextDecoration() == LCss::TextDecorUnderline) s->DeleteProp(LCss::PropTextDecoration); if ((LColour)s->Color() == LColour::Blue) s->DeleteProp(LCss::PropColor); Ns = AddStyleToCache(s); a->SetStyle(Ns); tb->LayoutDirty = true; InvalidateDoc(NULL); } break; } case LRichTextEdit::RemoveStyleBtn: { LCss s; ChangeSelectionStyle(&s, false); break; } /* case LRichTextEdit::CapabilityBtn: { View->OnCloseInstaller(); break; } */ case LRichTextEdit::EmojiBtn: { LPoint p(Areas[t].x1, Areas[t].y2 + 1); View->PointToScreen(p); new EmojiMenu(this, p); break; } case LRichTextEdit::HorzRuleBtn: { InsertHorzRule(); break; } default: return false; } return true; } bool LRichTextPriv::InsertHorzRule() { if (!Cursor || !Cursor->Blk) return false; TextBlock *tb = dynamic_cast(Cursor->Blk); if (!tb) return false; LAutoPtr Trans(new Transaction); DeleteSelection(Trans, NULL); ssize_t InsertIdx = Blocks.IndexOf(tb) + 1; LRichTextPriv::Block *After = NULL; if (Cursor->Offset == 0) { InsertIdx--; } else if (Cursor->Offset < tb->Length()) { After = tb->Split(Trans, Cursor->Offset); if (!After) return false; tb->StripLast(Trans); } HorzRuleBlock *Hr = new HorzRuleBlock(this); if (!Hr) return false; Blocks.AddAt(InsertIdx++, Hr); if (After) Blocks.AddAt(InsertIdx++, After); AddTrans(Trans); InvalidateDoc(NULL); LAutoPtr c(new BlockCursor(Hr, 0, 0)); return SetCursor(c); } void LRichTextPriv::Paint(LSurface *pDC, LScrollBar *&ScrollY) { if (Areas[LRichTextEdit::ToolsArea].Valid()) { // Draw tools area... LRect &t = Areas[LRichTextEdit::ToolsArea]; #ifdef WIN32 LDoubleBuffer Buf(pDC, &t); #endif LColour ToolBar = LColour(L_FOCUS_SEL_BACK).Mix(LColour(L_LOW)); pDC->Colour(ToolBar); pDC->Rectangle(&t); LRect r = t; r.Inset(3, 3); #define AllocPx(sz, border) \ LRect(r.x1, r.y1, r.x1 + (int)(sz) - 1, r.y2); r.x1 += (int)(sz) + border Areas[LRichTextEdit::FontFamilyBtn] = AllocPx(130, 6); Areas[LRichTextEdit::FontSizeBtn] = AllocPx(40, 6); Areas[LRichTextEdit::BoldBtn] = AllocPx(r.Y(), 0); Areas[LRichTextEdit::ItalicBtn] = AllocPx(r.Y(), 0); Areas[LRichTextEdit::UnderlineBtn] = AllocPx(r.Y(), 6); Areas[LRichTextEdit::ForegroundColourBtn] = AllocPx(r.Y()*1.5, 0); Areas[LRichTextEdit::BackgroundColourBtn] = AllocPx(r.Y()*1.5, 6); { LDisplayString Ds(LSysFont, TEXT_LINK); Areas[LRichTextEdit::MakeLinkBtn] = AllocPx(Ds.X() + 12, 0); } { LDisplayString Ds(LSysFont, TEXT_REMOVE_LINK); Areas[LRichTextEdit::RemoveLinkBtn] = AllocPx(Ds.X() + 12, 6); } { LDisplayString Ds(LSysFont, TEXT_REMOVE_STYLE); Areas[LRichTextEdit::RemoveStyleBtn] = AllocPx(Ds.X() + 12, 6); } for (unsigned int i=LRichTextEdit::EmojiBtn; iGetColourSpace())) { LAssert(!"MemDC creation failed."); return; } LSurface *pScreen = pDC; pDC = &Mem; r.Offset(-r.x1, -r.y1); #else pDC->GetOrigin(Origin.x, Origin.y); pDC->ClipRgn(&r); #endif ScrollOffsetPx = ScrollY ? (int)(ScrollY->Value() * ScrollLinePx) : 0; pDC->SetOrigin(Origin.x-r.x1, Origin.y-r.y1+ScrollOffsetPx); int DrawPx = ScrollOffsetPx + Areas[LRichTextEdit::ContentArea].Y(); int ExtraPx = DrawPx > DocumentExtent.y ? DrawPx - DocumentExtent.y : 0; r.Set(0, 0, DocumentExtent.x-1, DocumentExtent.y-1); // Fill the padding... LCssTools ct(this, Font); r = ct.PaintPadding(pDC, r); // Fill the background... #if DEBUG_COVERAGE_CHECK pDC->Colour(LColour(255, 0, 255)); #else LCss::ColorDef cBack = BackgroundColor(); // pDC->Colour(cBack.IsValid() ? (LColour)cBack : LColour(LC_WORKSPACE, 24)); #endif pDC->Rectangle(&r); if (ExtraPx) pDC->Rectangle(0, DocumentExtent.y, DocumentExtent.x-1, DocumentExtent.y+ExtraPx); PaintContext Ctx; Ctx.pDC = pDC; Ctx.Cursor = Cursor; Ctx.Select = Selection; Ctx.Colours[Unselected].Fore.Set(L_TEXT); Ctx.Colours[Unselected].Back.Set(L_WORKSPACE); if (View->Focus()) { Ctx.Colours[Selected].Fore.Set(L_FOCUS_SEL_FORE); Ctx.Colours[Selected].Back.Set(L_FOCUS_SEL_BACK); } else { Ctx.Colours[Selected].Fore.Set(L_NON_FOCUS_SEL_FORE); Ctx.Colours[Selected].Back.Set(L_NON_FOCUS_SEL_BACK); } for (unsigned i=0; iOnPaint(Ctx); #if DEBUG_OUTLINE_BLOCKS pDC->Colour(LColour(192, 192, 192)); pDC->LineStyle(LSurface::LineDot); pDC->Box(&b->GetPos()); pDC->LineStyle(LSurface::LineSolid); #endif } } #ifdef _DEBUG pDC->Colour(LColour::Green); for (unsigned i=0; iBox(&DebugRects[i]); } #endif #if 0 // Outline the line the cursor is on if (Cursor) { pDC->Colour(LColour::Blue); pDC->LineStyle(LSurface::LineDot); pDC->Box(&Cursor->Line); } #endif #if defined(WINDOWS) && !DEBUG_NO_DOUBLE_BUF Mem.SetOrigin(0, 0); pScreen->Blt(Areas[LRichTextEdit::ContentArea].x1, Areas[LRichTextEdit::ContentArea].y1, &Mem); #else pDC->ClipRgn(NULL); #endif } LHtmlElement *LRichTextPriv::CreateElement(LHtmlElement *Parent) { return new LRichEditElem(Parent); } bool LRichTextPriv::ToHtml(LArray *Media, BlockCursor *From, BlockCursor *To) { UtfNameCache.Reset(); if (!Blocks.Length()) return false; LStringPipe p(256); p.Print("\n" "\n" "\t\n"); ZeroRefCounts(); ssize_t Start = From ? Blocks.IndexOf(From->Blk) : 0; ssize_t End = To ? Blocks.IndexOf(To->Blk) : Blocks.Length() - 1; ssize_t StartIdx = From ? From->Offset : 0; ssize_t EndIdx = To ? To->Offset : Blocks.Last()->Length(); for (ssize_t i=Start; i<=End; i++) { Blocks[i]->IncAllStyleRefs(); } if (GetStyles()) { p.Print("\t\n"); } p.Print("\n" "\n"); for (ssize_t i=Start; i<=End; i++) { Block *b = Blocks[i]; LRange r; if (i == Start) r.Start = StartIdx; if (i == End) r.Len = EndIdx - r.Start; b->ToHtml(p, Media, r.Valid() ? &r : NULL); } p.Print("\n\n"); LAutoString a(p.NewStr()); UtfNameCache = a; return UtfNameCache.Get() != NULL; } void LRichTextPriv::DumpBlocks() { LgiTrace("LRichTextPriv Blocks=%i\n", Blocks.Length()); for (unsigned i=0; iGetClass(), b->GetStyle(), b->GetStyle() ? b->GetStyle()->Name.Get() : NULL); b->Dump(); LgiTrace("}\n"); } } struct HtmlElementCb : public LCss::ElementCallback { const char *GetElement(LHtmlElement *obj) { return obj->Tag; } const char *GetAttr(LHtmlElement *obj, const char *Attr) { const char *Val = NULL; return obj->Get(Attr, Val) ? Val : NULL; } bool GetClasses(LString::Array &Classes, LHtmlElement *obj) { const char *Cls = NULL; if (!obj->Get("class", Cls)) return false; Classes = LString(Cls).SplitDelimit(); return true; } LHtmlElement *GetParent(LHtmlElement *obj) { return obj->Parent; } LArray GetChildren(LHtmlElement *obj) { return obj->Children; } }; bool LRichTextPriv::FromHtml(LHtmlElement *e, CreateContext &ctx, LCss *ParentStyle, int Depth) { char Sp[48]; int SpLen = MIN(Depth << 1, sizeof(Sp) - 1); memset(Sp, ' ', SpLen); Sp[SpLen] = 0; for (unsigned i = 0; i < e->Children.Length(); i++) { LHtmlElement *c = e->Children[i]; LAutoPtr Style; if (ParentStyle) Style.Reset(new LCss(*ParentStyle)); LCss::SelArray Matches; HtmlElementCb Cb; if (ctx.StyleStore.Match(Matches, &Cb, c) && Matches.Length() > 0 && (Style.Get() || Style.Reset(new LCss))) { for (auto s : Matches) { const char *p = s->Style; Style->Parse(p, LCss::ParseRelaxed); } } // Check to see if the element is block level and end the previous // paragraph if so. c->Info = c->Tag ? LHtmlStatic::Inst->GetTagInfo(c->Tag) : NULL; bool IsBlock = c->Info != NULL && c->Info->Block(); switch (c->TagId) { case TAG_STYLE: { char16 *Style = e->GetText(); if (ValidStrW(Style)) LAssert(!"Impl me."); continue; break; } case TAG_B: { if (!Style) Style.Reset(new LCss); if (Style) Style->FontWeight(LCss::FontWeightBold); break; } case TAG_I: { if (!Style) Style.Reset(new LCss); if (Style) Style->FontStyle(LCss::FontStyleItalic); break; } case TAG_BLOCKQUOTE: { if (!Style) Style.Reset(new LCss); if (Style) { Style->MarginTop(LCss::Len("0.5em")); Style->MarginBottom(LCss::Len("0.5em")); Style->MarginLeft(LCss::Len("1em")); if (ctx.Tb) ctx.Tb->StripLast(NoTransaction); } break; } case TAG_A: { if (!Style) Style.Reset(new LCss); if (Style) { Style->TextDecoration(LCss::TextDecorUnderline); Style->Color(LCss::ColorDef(LCss::ColorRgb, LColour::Blue.c32())); } break; } case TAG_HR: { if (ctx.Tb) ctx.Tb->StripLast(NoTransaction); // Fall through } case TAG_IMG: { ctx.Tb = NULL; IsBlock = true; break; } default: { break; } } const char *Css, *Class; if (c->Get("style", Css)) { if (!Style) Style.Reset(new LCss); if (Style) Style->Parse(Css, ParseRelaxed); } if (c->Get("class", Class)) { LCss::SelArray Selectors; LRichEditElemContext StyleCtx; if (ctx.StyleStore.Match(Selectors, &StyleCtx, dynamic_cast(c))) { for (unsigned n=0; nStyle; if (s) { if (!Style) Style.Reset(new LCss); if (Style) Style->Parse(s, LCss::ParseRelaxed); } } } } } LNamedStyle *CachedStyle = AddStyleToCache(Style); if ( (IsBlock && ctx.LastChar != '\n') || c->TagId == TAG_BR ) { if (!ctx.Tb && c->TagId == TAG_BR) { // Don't do this for IMG and HR layout. Blocks.Add(ctx.Tb = new TextBlock(this)); if (CachedStyle && ctx.Tb) ctx.Tb->SetStyle(CachedStyle); } if (ctx.Tb) { const uint32_t Nl[] = {'\n', 0}; ctx.Tb->AddText(NoTransaction, -1, Nl, 1, NULL); ctx.LastChar = '\n'; ctx.StartOfLine = true; } } bool EndStyleChange = false; if (c->TagId == TAG_IMG) { Blocks.Add(ctx.Ib = new ImageBlock(this)); if (ctx.Ib) { const char *s; if (c->Get("src", s)) ctx.Ib->Source = s; if (c->Get("width", s)) { LCss::Len Sz(s); int Px = Sz.ToPx(); if (Px) ctx.Ib->Size.x = Px; } if (c->Get("height", s)) { LCss::Len Sz(s); int Px = Sz.ToPx(); if (Px) ctx.Ib->Size.y = Px; } if (CachedStyle) ctx.Ib->SetStyle(CachedStyle); ctx.Ib->Load(); } } else if (c->TagId == TAG_HR) { Blocks.Add(ctx.Hrb = new HorzRuleBlock(this)); } else if (c->TagId == TAG_A) { ctx.StartOfLine |= ctx.AddText(CachedStyle, c->GetText()); if (ctx.Tb && ctx.Tb->Txt.Length()) { StyleText *st = ctx.Tb->Txt.Last(); st->Element = TAG_A; const char *Link; if (c->Get("href", Link)) st->Param = Link; } } else { if (IsBlock && ctx.Tb != NULL) { if (CachedStyle != ctx.Tb->GetStyle()) { if (ctx.Tb->Length() == 0) { ctx.Tb->SetStyle(CachedStyle); } else { // Start a new block because the styles are different... EndStyleChange = true; auto Idx = Blocks.IndexOf(ctx.Tb); ctx.Tb = new TextBlock(this); if (Idx >= 0) Blocks.AddAt(Idx+1, ctx.Tb); else Blocks.Add(ctx.Tb); if (CachedStyle) ctx.Tb->SetStyle(CachedStyle); } } } char16 *Txt = c->GetText(); if ( Txt && ( !ctx.StartOfLine || ValidStrW(Txt) ) ) { if (!ctx.Tb) { Blocks.Add(ctx.Tb = new TextBlock(this)); ctx.Tb->SetStyle(CachedStyle); } #ifdef __GTK_H__ for (auto *i = Txt; *i; i++) if (*i == 0xa0) *i = ' '; #endif ctx.AddText(CachedStyle, Txt); ctx.StartOfLine = false; } } if (!FromHtml(c, ctx, Style, Depth + 1)) return false; if (EndStyleChange) ctx.Tb = NULL; if (IsBlock) ctx.StartOfLine = true; } return true; } bool LRichTextPriv::GetSelection(LArray *Text, LAutoString *Html) { if (!Text && !Html) return false; LArray Utf32; bool Cf = CursorFirst(); LRichTextPriv::BlockCursor *Start = Cf ? Cursor : Selection; LRichTextPriv::BlockCursor *End = Cf ? Selection : Cursor; if (Html) { if (ToHtml(NULL, Start, End)) *Html = UtfNameCache; } if (Text) { if (Start->Blk == End->Blk) { // In the same block... just copy ssize_t Len = End->Offset - Start->Offset; Start->Blk->CopyAt(Start->Offset, Len, &Utf32); } else { // Multi-block copy... // 1) Copy the content to the end of the first block Start->Blk->CopyAt(Start->Offset, -1, &Utf32); // 2) Copy any blocks between 'Start' and 'End' ssize_t i = Blocks.IndexOf(Start->Blk); ssize_t EndIdx = Blocks.IndexOf(End->Blk); if (i >= 0 && EndIdx >= i) { for (++i; Blocks[i] != End->Blk && i < (int)Blocks.Length(); i++) { LRichTextPriv::Block *&b = Blocks[i]; b->CopyAt(0, -1, &Utf32); } } else return Error(_FL, "Blocks missing index: %i, %i.", i, EndIdx); // 3) Delete any text up to the Cursor in the 'End' block End->Blk->CopyAt(0, End->Offset, &Utf32); } char16 *w = (char16*)LNewConvertCp(LGI_WideCharset, &Utf32[0], "utf-32", Utf32.Length() * sizeof(uint32_t)); if (!w) return Error(_FL, "Failed to convert %i utf32 to wide.", Utf32.Length()); Text->Add(w, Strlen(w)); Text->Add(0); } return true; } LRichTextEdit::RectType LRichTextPriv::PosToButton(LMouse &m) { if (Areas[LRichTextEdit::ToolsArea].Overlap(m.x, m.y) // || Areas[LRichTextEdit::CapabilityArea].Overlap(m.x, m.y) ) { for (unsigned i=LRichTextEdit::FontFamilyBtn; iOnComponentInstall(Name); } } #ifdef _DEBUG void LRichTextPriv::DumpNodes(LTree *Root) { if (Cursor) { LTreeItem *ti = new LTreeItem; ti->SetText("Cursor"); PrintNode(ti, "Offset=%i", Cursor->Offset); PrintNode(ti, "Pos=%s", Cursor->Pos.GetStr()); PrintNode(ti, "LineHint=%i", Cursor->LineHint); PrintNode(ti, "Blk=%i", Cursor->Blk ? Blocks.IndexOf(Cursor->Blk) : -2); Root->Insert(ti); } if (Selection) { LTreeItem *ti = new LTreeItem; ti->SetText("Selection"); PrintNode(ti, "Offset=%i", Selection->Offset); PrintNode(ti, "Pos=%s", Selection->Pos.GetStr()); PrintNode(ti, "LineHint=%i", Selection->LineHint); PrintNode(ti, "Blk=%i", Selection->Blk ? Blocks.IndexOf(Selection->Blk) : -2); Root->Insert(ti); } for (unsigned i=0; iDumpNodes(ti); LString s; s.Printf("[%i] %s", i, ti->GetText()); ti->SetText(s); Root->Insert(ti); } } LTreeItem *PrintNode(LTreeItem *Parent, const char *Fmt, ...) { LTreeItem *i = new LTreeItem; LString s; va_list Arg; va_start(Arg, Fmt); s.Printf(Arg, Fmt); va_end(Arg); s = s.Replace("\n", "\\n"); i->SetText(s); Parent->Insert(i); return i; } #endif diff --git a/src/common/Widgets/Editor/TextBlock.cpp b/src/common/Widgets/Editor/TextBlock.cpp --- a/src/common/Widgets/Editor/TextBlock.cpp +++ b/src/common/Widgets/Editor/TextBlock.cpp @@ -1,2616 +1,2616 @@ #include "lgi/common/Lgi.h" #include "lgi/common/RichTextEdit.h" #include "RichTextEditPriv.h" #include "lgi/common/Emoji.h" #include "lgi/common/DocView.h" #include "lgi/common/Menu.h" #include "lgi/common/GdcTools.h" #define DEBUG_LAYOUT 0 ////////////////////////////////////////////////////////////////////////////////////////////////// LRichTextPriv::StyleText::StyleText(const StyleText *St) { Emoji = St->Emoji; Style = NULL; Element = St->Element; Param = St->Param; if (St->Style) SetStyle(St->Style); Add((uint32_t*)&St->ItemAt(0), St->Length()); } LRichTextPriv::StyleText::StyleText(const uint32_t *t, ssize_t Chars, LNamedStyle *style) { Emoji = false; Style = NULL; Element = CONTENT; if (style) SetStyle(style); if (t) { if (Chars < 0) Chars = Strlen(t); Add((uint32_t*)t, (int)Chars); } } uint32_t *LRichTextPriv::StyleText::At(ssize_t i) { if (i >= 0 && i < (int)Length()) return &(*this)[i]; LAssert(0); return NULL; } LNamedStyle *LRichTextPriv::StyleText::GetStyle() { return Style; } void LRichTextPriv::StyleText::SetStyle(LNamedStyle *s) { if (Style != s) { Style = s; Colours.Empty(); if (Style) { LCss::ColorDef c = Style->Color(); if (c.Type == LCss::ColorRgb) Colours.Fore.Set(c.Rgb32, 32); c = Style->BackgroundColor(); if (c.Type == LCss::ColorRgb) Colours.Back.Set(c.Rgb32, 32); } } } ////////////////////////////////////////////////////////////////////////////////////////////////// LRichTextPriv::EmojiDisplayStr::EmojiDisplayStr(StyleText *src, LSurface *img, LCss::Len &fntSize, const uint32_t *s, ssize_t l) : DisplayStr(src, NULL, s, l) { Img = img; #if defined(_MSC_VER) Utf32.Add(s, l); uint32_t *u = &Utf32[0]; #else LAssert(sizeof(char16) == 4); uint32_t *u = (uint32_t*)Wide; Chars = Strlen(u); #endif CharPx = EMOJI_CELL_SIZE; Size = fntSize; if (Size.IsValid()) { int Px = Size.ToPx(); if (Px > 0) CharPx = (int)(Px / 0.8); } for (int i=0; i= 0); if (Emoji.Index >= 0) { int x = Emoji.Index % EMOJI_GROUP_X; int y = Emoji.Index / EMOJI_GROUP_X; LRect &rc = SrcRect[i]; rc.ZOff(EMOJI_CELL_SIZE-1, EMOJI_CELL_SIZE-1); rc.Offset(x * EMOJI_CELL_SIZE, y * EMOJI_CELL_SIZE); i += Emoji.Size; } else { LAssert(!"Not an emoji."); break; } } x = (int)SrcRect.Length() * CharPx; y = CharPx; xf = IntToFixed(x); yf = IntToFixed(y); } LAutoPtr LRichTextPriv::EmojiDisplayStr::Clone(ssize_t Start, ssize_t Len) { if (Len < 0) Len = Chars - Start; #if defined(_MSC_VER) LAssert( Start >= 0 && Start < (int)Utf32.Length() && Start + Len <= (int)Utf32.Length()); #endif LAutoPtr s(new EmojiDisplayStr(Src, Img, Size, #if defined(_MSC_VER) &Utf32[Start] #else (uint32_t*)(const char16*)(*this) #endif , Len)); return s; } void LRichTextPriv::EmojiDisplayStr::Paint(LSurface *pDC, int &FixX, int FixY, LColour &Back) { LRect f(0, 0, x-1, y-1); f.Offset(FixedToInt(FixX), FixedToInt(FixY)); pDC->Colour(Back); pDC->Rectangle(&f); int Op = pDC->Op(GDC_ALPHA); for (unsigned i=0; iBlt(f.x1, f.y1, &mem); } else { pDC->Blt(f.x1, f.y1, Img, &SrcRect[i]); } f.x1 += CharPx; FixX += IntToFixed(CharPx); } pDC->Op(Op); } double LRichTextPriv::EmojiDisplayStr::GetAscent() { return CharPx * 0.8; } ssize_t LRichTextPriv::EmojiDisplayStr::PosToIndex(int XPos, bool Nearest) { if (XPos >= (int)x) return Chars; if (XPos <= 0) return 0; return (XPos + (Nearest ? CharPx >> 1 : 0)) / CharPx; } ////////////////////////////////////////////////////////////////////////////////////////////////// LRichTextPriv::TextLine::TextLine(int XOffsetPx, int WidthPx, int YOffsetPx) { NewLine = 0; PosOff.ZOff(0, 0); PosOff.Offset(XOffsetPx, YOffsetPx); } ssize_t LRichTextPriv::TextLine::Length() { ssize_t Len = NewLine; for (unsigned i=0; iChars; return Len; } /// This runs after the layout line has been filled with display strings. /// It measures the line and works out the right offsets for each strings /// so that their baselines all match up correctly. void LRichTextPriv::TextLine::LayoutOffsets(int DefaultFontHt) { double BaseLine = 0.0; int HtPx = 0; for (unsigned i=0; iGetAscent(); BaseLine = MAX(BaseLine, Ascent); HtPx = MAX(HtPx, ds->Y()); } if (Strs.Length() == 0) HtPx = DefaultFontHt; else LAssert(HtPx > 0); for (unsigned i=0; iGetAscent(); if (Ascent > 0.0) ds->OffsetY = (int)(BaseLine - Ascent); LAssert(ds->OffsetY >= 0); HtPx = MAX(HtPx, ds->OffsetY+ds->Y()); } PosOff.y2 = PosOff.y1 + HtPx - 1; } ////////////////////////////////////////////////////////////////////////////////////////////////// LRichTextPriv::TextBlock::TextBlock(LRichTextPriv *priv) : Block(priv) { LayoutDirty = false; Len = 0; Pos.ZOff(-1, -1); Style = NULL; Fnt = NULL; ClickErrIdx = -1; Margin.ZOff(0, 0); Border.ZOff(0, 0); Padding.ZOff(0, 0); } LRichTextPriv::TextBlock::TextBlock(const TextBlock *Copy) : Block(Copy) { LayoutDirty = true; Len = Copy->Len; Pos = Copy->Pos; Style = Copy->Style; Fnt = Copy->Fnt; Margin = Copy->Margin; Border = Copy->Border; Padding = Copy->Padding; for (unsigned i=0; iTxt.Length(); i++) { Txt.Add(new StyleText(Copy->Txt.ItemAt(i))); } } LRichTextPriv::TextBlock::~TextBlock() { LAssert(Cursors == 0); Txt.DeleteObjects(); } void LRichTextPriv::TextBlock::Dump() { LgiTrace(" Txt.Len=%i, margin=%s, border=%s, padding=%s\n", Txt.Length(), Margin.GetStr(), Border.GetStr(), Padding.GetStr()); for (unsigned i=0; iLength() ? #ifndef WINDOWS (char16*) #endif t->At(0) : NULL, t->Length()); s = s.Strip(); LgiTrace(" %p: style=%p/%s, len=%i\n", t, t->GetStyle(), t->GetStyle() ? t->GetStyle()->Name.Get() : NULL, t->Length()); } } LNamedStyle *LRichTextPriv::TextBlock::GetStyle(ssize_t At) { if (At >= 0) { LArray t; if (GetTextAt(At, t)) return t[0]->GetStyle(); } return Style; } void LRichTextPriv::TextBlock::SetStyle(LNamedStyle *s) { if ((Style = s)) { Fnt = d->GetFont(s); LayoutDirty = true; LAssert(Fnt != NULL); Margin.x1 = Style->MarginLeft().ToPx(Pos.X(), Fnt); Margin.y1 = Style->MarginTop().ToPx(Pos.Y(), Fnt); Margin.x2 = Style->MarginRight().ToPx(Pos.X(), Fnt); Margin.y2 = Style->MarginBottom().ToPx(Pos.Y(), Fnt); Border.x1 = Style->BorderLeft().ToPx(Pos.X(), Fnt); Border.y1 = Style->BorderTop().ToPx(Pos.Y(), Fnt); Border.x2 = Style->BorderRight().ToPx(Pos.X(), Fnt); Border.y2 = Style->BorderBottom().ToPx(Pos.Y(), Fnt); Padding.x1 = Style->PaddingLeft().ToPx(Pos.X(), Fnt); Padding.y1 = Style->PaddingTop().ToPx(Pos.Y(), Fnt); Padding.x2 = Style->PaddingRight().ToPx(Pos.X(), Fnt); Padding.y2 = Style->PaddingBottom().ToPx(Pos.Y(), Fnt); } } ssize_t LRichTextPriv::TextBlock::Length() { return Len; } HtmlTag IsDefaultStyle(HtmlTag Id, LCss *Css) { if (!Css) return CONTENT; if (Css->Length() == 2) { LCss::ColorDef c = Css->Color(); if ((LColour)c != LColour::Blue) return CONTENT; LCss::TextDecorType td = Css->TextDecoration(); if (td != LCss::TextDecorUnderline) return CONTENT; return TAG_A; } else if (Css->Length() == 1) { LCss::FontWeightType fw = Css->FontWeight(); if (fw == LCss::FontWeightBold || fw == LCss::FontWeightBolder || fw >= LCss::FontWeight700) return TAG_B; LCss::TextDecorType td = Css->TextDecoration(); if (td == LCss::TextDecorUnderline) return TAG_U; LCss::FontStyleType fs = Css->FontStyle(); if (fs == LCss::FontStyleItalic) return TAG_I; } return CONTENT; } bool LRichTextPriv::TextBlock::ToHtml(LStream &s, LArray *Media, LRange *Rng) { s.Print("

"); LRange All(0, Length()); if (!Rng) Rng = &All; size_t Pos = 0; for (unsigned i=0; iGetStyle(); ssize_t tlen = t->Length(); if (!tlen) continue; LRange TxtRange(Pos, tlen); LRange Common = TxtRange.Overlap(*Rng); if (Common.Valid()) { LString utf( #ifndef WINDOWS (char16*) #endif t->At(Common.Start - Pos), Common.Len); char *str = utf; const char *ElemName = NULL; if (t->Element != CONTENT) { LHtmlElemInfo *e = d->Inst.Static->GetTagInfo(t->Element); if (!e) return false; ElemName = e->Tag; if (style) { HtmlTag tag = IsDefaultStyle(t->Element, style); if (tag == t->Element) style = NULL; } } else { HtmlTag tag = IsDefaultStyle(t->Element, style); if (tag != CONTENT) { LHtmlElemInfo *e = d->Inst.Static->GetTagInfo(tag); if (e) { ElemName = e->Tag; style = NULL; } } } if (style && !ElemName) ElemName = "span"; if (ElemName) s.Print("<%s", ElemName); if (style) s.Print(" class='%s'", style->Name.Get()); if (t->Element == TAG_A && t->Param) s.Print(" href='%s'", t->Param.Get()); if (ElemName) s.Print(">"); // Encode entities... LUtf8Ptr last(str); LUtf8Ptr cur(str); LUtf8Ptr end(str + utf.Length()); while (cur < end) { int32 ch = cur; switch (ch) { case '<': s.Print("%.*s<", cur - last, last.GetPtr()); last = ++cur; break; case '>': s.Print("%.*s>", cur - last, last.GetPtr()); last = ++cur; break; case '\n': s.Print("%.*s
\n", cur - last, last.GetPtr()); last = ++cur; break; case '&': s.Print("%.*s&", cur - last, last.GetPtr()); last = ++cur; break; case 0xa0: s.Print("%.*s ", cur - last, last.GetPtr()); last = ++cur; break; default: cur++; break; } } s.Print("%.*s", cur - last, last.GetPtr()); if (ElemName) s.Print("", ElemName); } Pos += tlen; } s.Print("

\n"); return true; } bool LRichTextPriv::TextBlock::GetPosFromIndex(BlockCursor *Cursor) { if (!Cursor) return d->Error(_FL, "No cursor param."); if (LayoutDirty) { Cursor->Pos.ZOff(-1, -1); // This is valid behaviour... need to // wait for layout before getting cursor // position. return false; } ssize_t CharPos = 0; int LastY = 0; for (unsigned i=0; iPosOff; r.Offset(Pos.x1, Pos.y1); int FixX = 0; for (unsigned n=0; nStrs.Length(); n++) { DisplayStr *ds = tl->Strs[n]; ssize_t dsChars = ds->Chars; if ( Cursor->Offset >= CharPos && Cursor->Offset <= CharPos + dsChars && ( Cursor->LineHint < 0 || Cursor->LineHint == i ) ) { ssize_t CharOffset = Cursor->Offset - CharPos; if (CharOffset == 0) { // First char Cursor->Pos.x1 = r.x1 + FixedToInt(FixX); } else if (CharOffset == dsChars) { // Last char Cursor->Pos.x1 = r.x1 + FixedToInt(FixX + ds->FX()); } else { // In the middle somewhere... LAutoPtr Tmp = ds->Clone(0, CharOffset); // LDisplayString Tmp(ds->GetFont(), *ds, CharOffset); if (Tmp) Cursor->Pos.x1 = r.x1 + FixedToInt(FixX + Tmp->FX()); } Cursor->Pos.y1 = r.y1 + ds->OffsetY; Cursor->Pos.y2 = Cursor->Pos.y1 + ds->Y() - 1; Cursor->Pos.x2 = Cursor->Pos.x1 + 1; Cursor->Line.Set(Pos.x1, r.y1, Pos.x2, r.y2); return true; } FixX += ds->FX(); CharPos += ds->Chars; } if ( ( tl->Strs.Length() == 0 || i == Layout.Length() - 1 ) && Cursor->Offset == CharPos ) { // Cursor at the start of empty line. Cursor->Pos.x1 = r.x1; Cursor->Pos.x2 = Cursor->Pos.x1 + 1; Cursor->Pos.y1 = r.y1; Cursor->Pos.y2 = r.y2; Cursor->Line.Set(Pos.x1, r.y1, Pos.x2, r.y2); return true; } CharPos += tl->NewLine; LastY = tl->PosOff.y2; } if (Cursor->Offset == 0 && Len == 0) { Cursor->Pos.x1 = Pos.x1; Cursor->Pos.x2 = Pos.x1 + 1; Cursor->Pos.y1 = Pos.y1; Cursor->Pos.y2 = Pos.y2; Cursor->Line = Pos; return true; } return false; } bool LRichTextPriv::TextBlock::HitTest(HitTestResult &htr) { if (htr.In.y < Pos.y1 || htr.In.y > Pos.y2) return false; ssize_t CharPos = 0; for (unsigned i=0; iPosOff; r.Offset(Pos.x1, Pos.y1); bool Over = r.Overlap(htr.In.x, htr.In.y); bool OnThisLine = htr.In.y >= r.y1 && htr.In.y <= r.y2; if (OnThisLine && htr.In.x <= r.x1) { htr.Near = true; htr.Idx = CharPos; htr.LineHint = i; LAssert(htr.Idx <= Length()); return true; } int FixX = 0; int InputX = IntToFixed(htr.In.x - Pos.x1 - tl->PosOff.x1); for (unsigned n=0; nStrs.Length(); n++) { DisplayStr *ds = tl->Strs[n]; int dsFixX = ds->FX(); if (Over && InputX >= FixX && InputX < FixX + dsFixX) { int OffFix = InputX - FixX; int OffPx = FixedToInt(OffFix); ssize_t OffChar = ds->PosToIndex(OffPx, true); // d->DebugRects[0].Set(Pos.x1, r.y1, Pos.x1 + InputX+1, r.y2); htr.Blk = this; htr.Ds = ds; htr.Idx = CharPos + OffChar; htr.LineHint = i; LAssert(htr.Idx <= Length()); return true; } FixX += ds->FX(); CharPos += ds->Chars; } if (OnThisLine) { htr.Near = true; htr.Idx = CharPos; htr.LineHint = i; LAssert(htr.Idx <= Length()); return true; } CharPos += tl->NewLine; } return false; } void DrawDecor(LSurface *pDC, LRichTextPriv::DisplayStr *Ds, int Fx, int Fy, ssize_t Start, ssize_t Len) { // LColour Old = pDC->Colour(LColour::Red); LDisplayString ds1(Ds->GetFont(), (const char16*)(*Ds), Start); LDisplayString ds2(Ds->GetFont(), (const char16*)(*Ds), Start+Len); int x = (Fx >> LDisplayString::FShift); int y = (Fy >> LDisplayString::FShift) + (int)Ds->GetAscent() + 1; int End = x + ds2.X(); x += ds1.X(); pDC->Colour(LColour::Red); while (x < End) { pDC->Set(x, y+(x%2)); x++; } } bool Overlap(LSpellCheck::SpellingError *e, ssize_t start, ssize_t len) { if (!e) return false; if (start+len <= e->Start) return false; if (start >= e->End()) return false; return true; } void LRichTextPriv::TextBlock::DrawDisplayString(LSurface *pDC, DisplayStr *Ds, int &FixX, int FixY, LColour &Bk, ssize_t &Pos) { int OldX = FixX; // Paint the string itself... Ds->Paint(pDC, FixX, FixY, Bk); // Does the a spelling error overlap this string? ssize_t DsEnd = Pos + Ds->Chars; while (Overlap(SpErr, Pos, Ds->Chars)) { // Yes, work out the region of characters and paint the decor ssize_t Start = MAX(SpErr->Start, Pos); ssize_t Len = MIN(SpErr->End(), Pos + Ds->Chars) - Start; // Draw the decor for the error DrawDecor(pDC, Ds, OldX, FixY, Start - Pos, Len); if (SpErr->End() < DsEnd) { // Are there more errors? SpErr = SpellingErrors.AddressOf(++PaintErrIdx); } else break; } while (SpErr && SpErr->End() < DsEnd) { // Are there more errors? SpErr = SpellingErrors.AddressOf(++PaintErrIdx); } Pos += Ds->Chars; } void LRichTextPriv::TextBlock::OnPaint(PaintContext &Ctx) { ssize_t CharPos = 0; ssize_t EndPoints = 0; ssize_t EndPoint[2] = {-1, -1}; ssize_t CurEndPoint = 0; if (Cursors > 0 && Ctx.Select) { // Selection end point checks... if (Ctx.Cursor && Ctx.Cursor->Blk == this) EndPoint[EndPoints++] = Ctx.Cursor->Offset; if (Ctx.Select && Ctx.Select->Blk == this) EndPoint[EndPoints++] = Ctx.Select->Offset; // Sort the end points if (EndPoints > 1 && EndPoint[0] > EndPoint[1]) { ssize_t ep = EndPoint[0]; EndPoint[0] = EndPoint[1]; EndPoint[1] = ep; } } // Paint margins, borders and padding... LRect r = Pos; r.x1 -= Margin.x1; r.y1 -= Margin.y1; r.x2 -= Margin.x2; r.y2 -= Margin.y2; LCss::ColorDef BorderStyle; if (Style) BorderStyle = Style->BorderLeft().Color; LColour BorderCol(222, 222, 222); if (BorderStyle.Type == LCss::ColorRgb) BorderCol.Set(BorderStyle.Rgb32, 32); Ctx.DrawBox(r, Margin, Ctx.Colours[Unselected].Back); Ctx.DrawBox(r, Border, BorderCol); Ctx.DrawBox(r, Padding, Ctx.Colours[Unselected].Back); int CurY = Pos.y1; PaintErrIdx = 0; SpErr = SpellingErrors.AddressOf(PaintErrIdx); for (unsigned i=0; iPosOff; LinePos.Offset(Pos.x1, Pos.y1); if (Line->PosOff.X() < Pos.X()) { Ctx.pDC->Colour(Ctx.Colours[Unselected].Back); Ctx.pDC->Rectangle(LinePos.x2, LinePos.y1, Pos.x2, LinePos.y2); } int FixX = IntToFixed(LinePos.x1); if (CurY < LinePos.y1) { // Fill padded area... Ctx.pDC->Colour(Ctx.Colours[Unselected].Back); Ctx.pDC->Rectangle(Pos.x1, CurY, Pos.x2, LinePos.y1 - 1); } CurY = LinePos.y1; LFont *Fnt = NULL; #if DEBUG_NUMBERED_LAYOUTS LString s; s.Printf("%i", Ctx.Index); Ctx.Index++; #endif for (unsigned n=0; nStrs.Length(); n++) { DisplayStr *Ds = Line->Strs[n]; LFont *DsFnt = Ds->GetFont(); ColourPair &Cols = Ds->Src->Colours; if (DsFnt && DsFnt != Fnt) { Fnt = DsFnt; Fnt->Transparent(false); } // If the current text part doesn't cover the full line height we have to // fill in the rest here... if (Ds->Y() < Line->PosOff.Y()) { Ctx.pDC->Colour(Ctx.Colours[Unselected].Back); int CurX = FixedToInt(FixX); if (Ds->OffsetY > 0) Ctx.pDC->Rectangle(CurX, CurY, CurX+Ds->X(), CurY+Ds->OffsetY-1); int DsY2 = Ds->OffsetY + Ds->Y(); if (DsY2 < Pos.Y()) Ctx.pDC->Rectangle(CurX, CurY+DsY2, CurX+Ds->X(), Pos.y2); } // Check for selection changes... int FixY = IntToFixed(CurY + Ds->OffsetY); #if DEBUG_OUTLINE_CUR_STYLE_TEXT LRect r(0, 0, -1, -1); if (Ctx.Cursor->Blk == (Block*)this) { LArray CurStyle; if (GetTextAt(Ctx.Cursor->Offset, CurStyle) && Ds->Src == CurStyle.First()) { r.ZOff(Ds->X()-1, Ds->Y()-1); r.Offset(FixedToInt(FixX), FixedToInt(FixY)); } } #endif if (CurEndPoint < EndPoints && EndPoint[CurEndPoint] >= CharPos && EndPoint[CurEndPoint] <= CharPos + Ds->Chars) { // Process string into parts based on the selection boundaries ssize_t Ch = EndPoint[CurEndPoint] - CharPos; ssize_t TmpPos = CharPos; LAutoPtr ds1 = Ds->Clone(0, Ch); // First part... LColour Bk = Ctx.Type == Unselected && Cols.Back.IsValid() ? Cols.Back : Ctx.Back(); if (DsFnt) DsFnt->Colour(Ctx.Type == Unselected && Cols.Fore.IsValid() ? Cols.Fore : Ctx.Fore(), Bk); if (ds1) DrawDisplayString(Ctx.pDC, ds1, FixX, FixY, Bk, TmpPos); Ctx.Type = Ctx.Type == Selected ? Unselected : Selected; CurEndPoint++; // Is there 3 parts? // // This happens when the selection starts and end in the one string. // // The alternative is that it starts or ends in the strings but the other // end point is in a different string. In which case there is only 2 strings // to draw. if (CurEndPoint < EndPoints && EndPoint[CurEndPoint] >= CharPos && EndPoint[CurEndPoint] <= CharPos + Ds->Chars) { // Yes.. ssize_t Ch2 = EndPoint[CurEndPoint] - CharPos; // Part 2 LAutoPtr ds2 = Ds->Clone(Ch, Ch2 - Ch); LColour Bk = Ctx.Type == Unselected && Cols.Back.IsValid() ? Cols.Back : Ctx.Back(); if (DsFnt) DsFnt->Colour(Ctx.Type == Unselected && Cols.Fore.IsValid() ? Cols.Fore : Ctx.Fore(), Bk); if (ds2) DrawDisplayString(Ctx.pDC, ds2, FixX, FixY, Bk, TmpPos); Ctx.Type = Ctx.Type == Selected ? Unselected : Selected; CurEndPoint++; // Part 3 if (Ch2 < Ds->Length()) { LAutoPtr ds3 = Ds->Clone(Ch2); Bk = Ctx.Type == Unselected && Cols.Back.IsValid() ? Cols.Back : Ctx.Back(); if (DsFnt) DsFnt->Colour(Ctx.Type == Unselected && Cols.Fore.IsValid() ? Cols.Fore : Ctx.Fore(), Bk); if (ds3) DrawDisplayString(Ctx.pDC, ds3, FixX, FixY, Bk, TmpPos); } } else if (Ch < Ds->Chars) { // No... draw 2nd part LAutoPtr ds2 = Ds->Clone(Ch); LColour Bk = Ctx.Type == Unselected && Cols.Back.IsValid() ? Cols.Back : Ctx.Back(); if (DsFnt) DsFnt->Colour(Ctx.Type == Unselected && Cols.Fore.IsValid() ? Cols.Fore : Ctx.Fore(), Bk); if (ds2) DrawDisplayString(Ctx.pDC, ds2, FixX, FixY, Bk, TmpPos); } } else { // No selection changes... draw the whole string LColour Bk = Ctx.Type == Unselected && Cols.Back.IsValid() ? Cols.Back : Ctx.Back(); if (DsFnt) DsFnt->Colour(Ctx.Type == Unselected && Cols.Fore.IsValid() ? Cols.Fore : Ctx.Fore(), Bk); #if DEBUG_OUTLINE_CUR_DISPLAY_STR int OldFixX = FixX; #endif auto TmpPos = CharPos; DrawDisplayString(Ctx.pDC, Ds, FixX, FixY, Bk, TmpPos); #if DEBUG_OUTLINE_CUR_DISPLAY_STR if (Ctx.Cursor->Blk == (Block*)this && Ctx.Cursor->Offset >= CharPos && Ctx.Cursor->Offset < CharPos + Ds->Chars) { LRect r(0, 0, Ds->X()-1, Ds->Y()-1); r.Offset(FixedToInt(OldFixX), FixedToInt(FixY)); Ctx.pDC->Colour(LColour::Red); Ctx.pDC->Box(&r); } #endif } #if DEBUG_OUTLINE_CUR_STYLE_TEXT if (r.Valid()) { Ctx.pDC->Colour(LColour(192, 192, 192)); Ctx.pDC->LineStyle(LSurface::LineDot); Ctx.pDC->Box(&r); Ctx.pDC->LineStyle(LSurface::LineSolid); } #endif CharPos += Ds->Chars; } if (Line->Strs.Length() == 0) { if (CurEndPoint < EndPoints && EndPoint[CurEndPoint] == CharPos) { Ctx.Type = Ctx.Type == Selected ? Unselected : Selected; CurEndPoint++; } } if (Ctx.Type == Selected) { // Draw new line int x1 = FixedToInt(FixX); FixX += IntToFixed(5); int x2 = FixedToInt(FixX); Ctx.pDC->Colour(Ctx.Colours[Selected].Back); Ctx.pDC->Rectangle(x1, LinePos.y1, x2, LinePos.y2); } Ctx.pDC->Colour(Ctx.Colours[Unselected].Back); Ctx.pDC->Rectangle(FixedToInt(FixX), LinePos.y1, Pos.x2, LinePos.y2); #if DEBUG_NUMBERED_LAYOUTS LDisplayString Ds(LSysFont, s); LSysFont->Colour(LColour::Green, LColour::White); LSysFont->Transparent(false); Ds.Draw(Ctx.pDC, LinePos.x1, LinePos.y1); /* Ctx.pDC->Colour(LColour::Blue); Ctx.pDC->Line(LinePos.x1, LinePos.y1,LinePos.x2,LinePos.y2); */ #endif CurY = LinePos.y2 + 1; CharPos += Line->NewLine; } if (CurY < Pos.y2) { // Fill padded area... Ctx.pDC->Colour(Ctx.Colours[Unselected].Back); Ctx.pDC->Rectangle(Pos.x1, CurY, Pos.x2, Pos.y2); } if (Ctx.Cursor && Ctx.Cursor->Blk == this && Ctx.Cursor->Blink && d->View->Focus()) { Ctx.pDC->Colour(CursorColour); if (Ctx.Cursor->Pos.Valid()) Ctx.pDC->Rectangle(&Ctx.Cursor->Pos); else Ctx.pDC->Rectangle(Pos.x1, Pos.y1, Pos.x1, Pos.y2); } #if 0 // def _DEBUG if (Ctx.Select && Ctx.Select->Blk == this) { Ctx.pDC->Colour(LColour(255, 0, 0)); Ctx.pDC->Rectangle(&Ctx.Select->Pos); } #endif } bool LRichTextPriv::TextBlock::OnLayout(Flow &flow) { if (Pos.X() == flow.X() && !LayoutDirty) { // Adjust position to match the flow, even if we are not dirty Pos.Offset(0, flow.CurY - Pos.y1); flow.CurY = Pos.y2 + 1; return true; } static int Count = 0; Count++; LayoutDirty = false; Layout.DeleteObjects(); flow.Left += Margin.x1; flow.Right -= Margin.x2; flow.CurY += Margin.y1; Pos.x1 = flow.Left; Pos.y1 = flow.CurY; Pos.x2 = flow.Right; Pos.y2 = flow.CurY-1; // Start with a 0px height. flow.Left += Border.x1 + Padding.x1; flow.Right -= Border.x2 + Padding.x2; flow.CurY += Border.y1 + Padding.y1; int FixX = 0; // Current x offset (fixed point) on the current line LAutoPtr CurLine(new TextLine(flow.Left - Pos.x1, flow.X(), flow.CurY - Pos.y1)); if (!CurLine) return flow.d->Error(_FL, "alloc failed."); ssize_t LayoutSize = 0; ssize_t TextSize = 0; for (unsigned i=0; iGetStyle(); LAssert(t->Length() >= 0); TextSize += t->Length(); if (t->Length() == 0) continue; int AvailableX = Pos.X() - CurLine->PosOff.x1; if (AvailableX < 0) AvailableX = 1; // Get the font for 't' LFont *f = flow.d->GetFont(t->GetStyle()); if (!f) return flow.d->Error(_FL, "font creation failed."); LCss::WordWrapType WrapType = tstyle ? tstyle->WordWrap() : LCss::WrapNormal; uint32_t *sStart = t->At(0); uint32_t *sEnd = sStart + t->Length(); for (ssize_t Off = 0; Off < (ssize_t)t->Length(); ) { // How much of 't' is on the same line? uint32_t *s = sStart + Off; #if DEBUG_LAYOUT LgiTrace("Txt[%i][%i]: FixX=%i, Txt='%.*S'\n", i, Off, FixX, t->Length() - Off, s); #endif if (*s == '\n') { // New line handling... Off++; CurLine->PosOff.x2 = CurLine->PosOff.x1 + FixedToInt(FixX) - 1; FixX = 0; CurLine->LayoutOffsets(f->GetHeight()); Pos.y2 = MAX(Pos.y2, Pos.y1 + CurLine->PosOff.y2); CurLine->NewLine = 1; LayoutSize += CurLine->Length(); #if DEBUG_LAYOUT LgiTrace("\tNewLineChar, LayoutSize=%i, TextSize=%i\n", LayoutSize, TextSize); #endif Layout.Add(CurLine.Release()); CurLine.Reset(new TextLine(flow.Left - Pos.x1, flow.X(), Pos.Y())); if (Off == t->Length()) { // Empty line at the end of the StyleText const uint32_t Empty[] = {0}; CurLine->Strs.Add(new DisplayStr(t, f, Empty, 0, flow.pDC)); } continue; } uint32_t *e = s; /* printf("e=%i sEnd=%i len=%i\n", (int)(e - sStart), (int)(sEnd - sStart), (int)t->Length()); */ while (e < sEnd && *e != '\n') e++; // Add 't' to current line LCss::Len fntSize; if (f) fntSize = f->Size(); ssize_t Chars = MIN(1024, e - s); LAutoPtr Ds ( t->Emoji ? new EmojiDisplayStr(t, d->GetEmojiImage(), fntSize, s, Chars) : new DisplayStr(t, f, s, Chars, flow.pDC) ); if (!Ds) return flow.d->Error(_FL, "display str creation failed."); if (WrapType != LCss::WrapNone && FixX + Ds->FX() > IntToFixed(AvailableX)) { #if DEBUG_LAYOUT LgiTrace("\tNeedToWrap: %i, %i + %i > %i\n", WrapType, FixX, Ds->FX(), IntToFixed(AvailableX)); #endif // Wrap the string onto the line... int AvailablePx = AvailableX - FixedToInt(FixX); ssize_t FitChars = Ds->PosToIndex(AvailablePx, false); if (FitChars < 0) { #if DEBUG_LAYOUT LgiTrace("\tFitChars error: %i\n", FitChars); #endif flow.d->Error(_FL, "PosToIndex(%i) failed.", AvailablePx); LAssert(0); } else { // Wind back to the last break opportunity ssize_t ch = 0; for (ch = FitChars; ch > 0; ch--) { if (IsWordBreakChar(s[ch-1])) break; } #if DEBUG_LAYOUT LgiTrace("\tWindBack: %i\n", (int)ch); #endif if (ch == 0) { // One word minimum per line for (ch = 1; ch < Chars; ch++) { if (IsWordBreakChar(s[ch])) break; } Chars = ch; } else if (ch > (FitChars >> 2)) Chars = ch; else Chars = FitChars; // Create a new display string of the right size... LCss::Len fntSize; if (f) fntSize = f->Size(); if ( ! Ds.Reset ( t->Emoji ? new EmojiDisplayStr(t, d->GetEmojiImage(), fntSize, s, Chars) : new DisplayStr(t, f, s, Chars, flow.pDC) ) ) return flow.d->Error(_FL, "failed to create wrapped display str."); // Finish off line CurLine->PosOff.x2 = CurLine->PosOff.x1 + FixedToInt(FixX + Ds->FX()) - 1; CurLine->Strs.Add(Ds.Release()); CurLine->LayoutOffsets(d->Font->GetHeight()); Pos.y2 = MAX(Pos.y2, Pos.y1 + CurLine->PosOff.y2); LayoutSize += CurLine->Length(); Layout.Add(CurLine.Release()); #if DEBUG_LAYOUT LgiTrace("\tWrap, LayoutSize=%i TextSize=%i\n", LayoutSize, TextSize); #endif // New line... CurLine.Reset(new TextLine(flow.Left - Pos.x1, flow.X(), Pos.Y())); FixX = 0; Off += Chars; continue; } } else { FixX += Ds->FX(); } if (!Ds) break; CurLine->PosOff.x2 = CurLine->PosOff.x1 + FixedToInt(FixX) - 1; CurLine->Strs.Add(Ds.Release()); Off += Chars; } } if (Txt.Length() == 0) { // Empty node case int y = Pos.y1 + flow.d->View->GetFont()->GetHeight() - 1; CurLine->PosOff.y2 = Pos.y2 = MAX(Pos.y2, y); LayoutSize += CurLine->Length(); Layout.Add(CurLine.Release()); } if (CurLine && CurLine->Strs.Length() > 0) { LFont *f = d->View ? d->View->GetFont() : LSysFont; CurLine->LayoutOffsets(f->GetHeight()); Pos.y2 = MAX(Pos.y2, Pos.y1 + CurLine->PosOff.y2); LayoutSize += CurLine->Length(); #if DEBUG_LAYOUT LgiTrace("\tRemaining, LayoutSize=%i, TextSize=%i\n", LayoutSize, TextSize); #endif Layout.Add(CurLine.Release()); } LAssert(LayoutSize == Len); flow.CurY = Pos.y2 + 1 + Margin.y2 + Border.y2 + Padding.y2; flow.Left -= Margin.x1 + Border.x1 + Padding.x1; flow.Right += Margin.x2 + Border.x2 + Padding.x2; return true; } ssize_t LRichTextPriv::TextBlock::GetTextAt(ssize_t Offset, LArray &Out) { if (Txt.Length() == 0) return 0; StyleText **t = &Txt[0]; StyleText **e = t + Txt.Length(); Out.Length(0); ssize_t Pos = 0; while (t < e) { ssize_t Len = (*t)->Length(); if (Offset >= Pos && Offset <= Pos + Len) Out.Add(*t); t++; Pos += Len; } LAssert(Pos == Len); return Out.Length(); } bool LRichTextPriv::TextBlock::IsValid() { ssize_t TxtLen = 0; for (unsigned i = 0; i < Txt.Length(); i++) { StyleText *t = Txt[i]; TxtLen += t->Length(); for (unsigned n = 0; n < t->Length(); n++) { if ((*t)[n] == 0) { LAssert(0); return false; } } } if (Len != TxtLen) return d->Error(_FL, "Txt.Len vs Len mismatch: %i, %i.", TxtLen, Len); return true; } int LRichTextPriv::TextBlock::GetLines() { return (int)Layout.Length(); } bool LRichTextPriv::TextBlock::OffsetToLine(ssize_t Offset, int *ColX, LArray *LineY) { if (LayoutDirty) return false; if (LineY) LineY->Length(0); if (Offset <= 0) { if (ColX) *ColX = 0; if (LineY) LineY->Add(0); return true; } bool Found = false; ssize_t Pos = 0; for (unsigned i=0; iLength(); if (Offset >= Pos && Offset <= Pos + Len - tl->NewLine) { if (ColX) *ColX = (int)(Offset - Pos); if (LineY) LineY->Add(i); Found = true; } Pos += Len; } return Found; } ssize_t LRichTextPriv::TextBlock::LineToOffset(ssize_t Line) { if (LayoutDirty) return -1; if (Line <= 0) return 0; ssize_t Pos = 0; for (size_t i=0; iLength(); if (i == Line) return Pos; Pos = Len; } return (int)Length(); } bool LRichTextPriv::TextBlock::PreEdit(Transaction *Trans) { if (Trans) { bool HasThisBlock = false; for (unsigned i=0; iChanges.Length(); i++) { CompleteTextBlockState *c = dynamic_cast(Trans->Changes[i]); if (c) { if (c->Uid == BlockUid) { HasThisBlock = true; break; } } } if (!HasThisBlock) Trans->Add(new CompleteTextBlockState(d, this)); } return true; } ssize_t LRichTextPriv::TextBlock::DeleteAt(Transaction *Trans, ssize_t BlkOffset, ssize_t Chars, LArray *DeletedText) { ssize_t Pos = 0; ssize_t Deleted = 0; PreEdit(Trans); for (unsigned i=0; i 0; i++) { StyleText *t = Txt[i]; ssize_t TxtOffset = BlkOffset - Pos; if (TxtOffset >= 0 && TxtOffset < (int)t->Length()) { ssize_t MaxChars = t->Length() - TxtOffset; ssize_t Remove = MIN(Chars, MaxChars); if (Remove <= 0) return 0; ssize_t Remaining = MaxChars - Remove; ssize_t NewLen = t->Length() - Remove; if (DeletedText) { DeletedText->Add(t->At(TxtOffset), Remove); } if (Remaining > 0) { // Copy down memmove(&(*t)[TxtOffset], &(*t)[TxtOffset + Remove], Remaining * sizeof(uint32_t)); (*t)[NewLen] = 0; } // Change length if (NewLen == 0) { // Remove run completely // LgiTrace("DelRun %p/%i '%.*S'\n", t, i, t->Length(), &(*t)[0]); Txt.DeleteAt(i--, true); DeleteObj(t); } else { // Shorten run t->Length(NewLen); // LgiTrace("ShortenRun %p/%i '%.*S'\n", t, i, t->Length(), &(*t)[0]); } LayoutDirty = true; Chars -= Remove; Len -= Remove; Deleted += Remove; } if (t) Pos += t->Length(); } if (Deleted > 0) { // Adjust start of existing spelling styles LRange r(BlkOffset, Deleted); for (auto &Err : SpellingErrors) { if (Err.Overlap(r).Valid()) { Err -= r; } else if (Err > r) { Err.Start -= Deleted; } } LayoutDirty = true; UpdateSpellingAndLinks(Trans, LRange(BlkOffset, 0)); } IsValid(); return Deleted; } bool LRichTextPriv::TextBlock::AddText(Transaction *Trans, ssize_t AtOffset, const uint32_t *InStr, ssize_t InChars, LNamedStyle *Style) { if (!InStr) return d->Error(_FL, "No input text."); if (InChars < 0) InChars = Strlen(InStr); PreEdit(Trans); LArray EmojiIdx; for (int i=0; i= 0) i += e.Size; else i++; } ssize_t InitialOffset = AtOffset >= 0 ? AtOffset : Len; ssize_t Chars = 0; // Length of run to insert ssize_t Pos = 0; // Current character position in this block uint32_t TxtIdx = 0; // Index into Txt array for (ssize_t i = 0; i < InChars; i += Chars) { // Work out the run of chars that are either // Emoji or not Emoji... bool IsEmoji = EmojiIdx[i].Index >= 0; Chars = 0; for (auto n = i; n < InChars;) { if ( IsEmoji ^ (EmojiIdx[n].Index >= 0) ) break; if (IsEmoji) { auto &e = EmojiIdx[n]; if (e.Index >= 0 && e.Size > 0) { Chars += e.Size; n += e.Size; } else { LAssert(!"Something went wrong."); Chars++; n++; } } else { Chars++; n++; } } // Now process 'Char' chars const uint32_t *Str = InStr + i; if (AtOffset >= 0 && Txt.Length() > 0) { // Seek further into block? while ( Pos < AtOffset && TxtIdx < Txt.Length()) { StyleText *t = Txt[TxtIdx]; ssize_t Len = t->Length(); if (AtOffset <= Pos + Len) break; Pos += Len; TxtIdx++; } StyleText *t = TxtIdx >= Txt.Length() ? Txt.Last() : Txt[TxtIdx]; ssize_t TxtLen = t->Length(); if (AtOffset >= Pos && AtOffset <= Pos + TxtLen) { ssize_t StyleOffset = AtOffset - Pos; // Offset into 't' in which we need to potentially break the style // to insert the new content. bool UrlEdge = t->Element == TAG_A && *Str == '\n'; if (!Style && IsEmoji == t->Emoji && !UrlEdge) { // Insert/append to existing text run ssize_t After = t->Length() - StyleOffset; ssize_t NewSz = t->Length() + Chars; t->Length(NewSz); uint32_t *c = &t->First(); LOG_FN("TextBlock(%i)::Add(%i,%i,%s)::Append StyleOffset=%i, After=%i\n", GetUid(), AtOffset, InChars, Style?Style->Name.Get():NULL, StyleOffset, After); // Do we need to move characters up to make space? if (After > 0) memmove(c + StyleOffset + Chars, c + StyleOffset, After * sizeof(*c)); // Insert the new string... memcpy(c + StyleOffset, Str, Chars * sizeof(*c)); Len += Chars; AtOffset += Chars; } else { // Break into 2 runs, with the new text in the middle... // Insert the new text+style StyleText *Run = new StyleText(Str, Chars, Style); if (!Run) return false; Run->Emoji = IsEmoji; /* This following code could be wrong. In terms of test cases I fixed this: A) Starting with basic empty email + signature. Insert a URL at the very start. Then hit enter. Buf: \n inserted BEFORE the URL. Changed the condition to 'StyleOffset != 0' rather than 'TxtIdx != 0' Potentially other test cases could exhibit bugs that need to be added here. */ if (StyleOffset) Txt.AddAt(++TxtIdx, Run); else Txt.AddAt(TxtIdx++, Run); //////////////////////////////////// Pos += StyleOffset; // We are skipping over the run at 'TxtIdx', update pos LOG_FN("TextBlock(%i)::Add(%i,%i,%s)::Insert StyleOffset=%i\n", GetUid(), AtOffset, InChars, Style?Style->Name.Get():NULL, StyleOffset); if (StyleOffset < TxtLen) { // Insert the 2nd part of the string Run = new StyleText(t->At(StyleOffset), TxtLen - StyleOffset, t->GetStyle()); if (!Run) return false; Pos += Chars; Txt.AddAt(++TxtIdx, Run); // Now truncate the existing text.. t->Length(StyleOffset); } Len += Chars; AtOffset += Chars; } Str = NULL; } } if (Str) { // At the end StyleText *Last = Txt.Length() > 0 ? Txt.Last() : NULL; if (Last && Last->GetStyle() == Style && IsEmoji == Last->Emoji) { if (Last->Add((uint32_t*)Str, Chars)) { Len += Chars; if (AtOffset >= 0) AtOffset += Chars; } } else { StyleText *Run = new StyleText(Str, Chars, Style); if (!Run) return false; Run->Emoji = IsEmoji; Txt.Add(Run); Len += Chars; if (AtOffset >= 0) AtOffset += Chars; } } } // Push existing spelling styles along for (auto &Err : SpellingErrors) { if (Err.Start >= InitialOffset) Err.Start += InChars; } // Update layout and styling LayoutDirty = true; IsValid(); UpdateSpellingAndLinks(Trans, LRange(InitialOffset, InChars)); return true; } bool LRichTextPriv::TextBlock::OnDictionary(Transaction *Trans) { UpdateSpellingAndLinks(Trans, LRange(0, Length())); return true; } #define IsUrlWordChar(t) \ (((t) > ' ') && !strchr("./:", (t))) template bool _ScanWord(Char *&t, Char *e) { if (!IsUrlWordChar(*t)) return false; Char *s = t; while (t < e && IsUrlWordChar(*t)) t++; return t > s; } bool IsBracketed(int s, int e) { if (s == '(' && e == ')') return true; if (s == '[' && e == ']') return true; if (s == '{' && e == '}') return true; if (s == '<' && e == '>') return true; return false; } #define ScanWord() \ if (t >= e || !_ScanWord(t, e)) return false #define ScanChar(ch) \ if (t >= e || *t != ch) \ return false; \ t++ template bool DetectUrl(Char *t, ssize_t &len) { #ifdef _DEBUG LString str(t, len); //char *ss = str; #endif Char *s = t; Char *e = t + len; ScanWord(); // Protocol ScanChar(':'); ScanChar('/'); ScanChar('/'); ScanWord(); // Host name or username.. if (t < e && *t == ':') { t++; _ScanWord(t, e); // Don't return if missing... password optional ScanChar('@'); ScanWord(); // First part of host name... } // Rest of host name while (t < e && *t == '.') { t++; if (t < e && IsUrlWordChar(*t)) ScanWord(); // Second part of host name } if (t < e && *t == ':') // Port number { t++; ScanWord(); } while (t < e && strchr("/.:", *t)) // Path { t++; if (t < e && (IsUrlWordChar(*t) || *t == ':')) ScanWord(); } if (strchr("!.", t[-1])) t--; len = t - s; return true; } int ErrSort(LSpellCheck::SpellingError *a, LSpellCheck::SpellingError *b) { return (int) (a->Start - b->Start); } void LRichTextPriv::TextBlock::SetSpellingErrors(LArray &Errors, LRange r) { // LgiTrace("%s:%i - SetSpellingErrors " LPrintfSSizeT ", " LPrintfSSizeT ":" LPrintfSSizeT "\n", _FL, Errors.Length(), r.Start, r.End()); // Delete any errors overlapping 'r' for (unsigned i=0; i Text; if (!CopyAt(0, Length(), &Text)) return; // Spelling... if (d->SpellCheck && d->SpellDictionaryLoaded) { LRange Rgn = r; while (Rgn.Start > 0 && IsWordChar(Text[Rgn.Start-1])) { Rgn.Start--; Rgn.Len++; } while (Rgn.End() < Len && IsWordChar(Text[Rgn.End()])) { Rgn.Len++; } LString s(Text.AddressOf(Rgn.Start), Rgn.Len); LArray Params; Params[SpellBlockPtr] = (Block*)this; // LgiTrace("%s:%i - Check(%s) " LPrintfSSizeT ":" LPrintfSSizeT "\n", _FL, s.Get(), Rgn.Start, Rgn.End()); d->SpellCheck->Check(d->View->AddDispatch(), s, Rgn.Start, Rgn.Len, &Params); } // Link detection... // Extend the range to include whole words while (r.Start > 0 && !IsWhiteSpace(Text[r.Start])) { r.Start--; r.Len++; } while (r.End() < (ssize_t)Text.Length() && !IsWhiteSpace(Text[r.End()])) r.Len++; // Create array of words... LArray Words; bool Ws = true; for (int i = 0; i < r.Len; i++) { bool w = IsWhiteSpace(Text[r.Start + i]); if (w ^ Ws) { Ws = w; if (!w) { LRange &w = Words.New(); w.Start = r.Start + i; // printf("StartWord=%i, %i\n", w.Start, w.Len); } else if (Words.Length() > 0) { LRange &w = Words.Last(); w.Len = r.Start + i - w.Start; // printf("EndWord=%i, %i\n", w.Start, w.Len); } } } if (!Ws && Words.Length() > 0) { LRange &w = Words.Last(); w.Len = r.Start + r.Len - w.Start; // printf("TermWord=%i, %i Words=%i\n", w.Start, w.Len, Words.Length()); } // For each word in the range of text for (unsigned i = 0; iMakeLink(this, w.Start, w.Len, Link); // Also unlink any of the word after the URL if (w.End() < Words[i].End()) { LCss Style; ChangeStyle(Trans, w.End(), Words[i].End() - w.End(), &Style, false); } } } } bool LRichTextPriv::TextBlock::StripLast(Transaction *Trans, const char *Set) { if (Txt.Length() == 0) return false; StyleText *l = Txt.Last(); if (!l || l->Length() <= 0) return false; if (!strchr(Set, l->Last())) return false; PreEdit(Trans); if (!l->PopLast()) return false; LayoutDirty = true; Len--; return true; } bool LRichTextPriv::TextBlock::DoContext(LSubMenu &s, LPoint Doc, ssize_t Offset, bool TopOfMenu) { if (!TopOfMenu) return false; // Is there a spelling error at 'Offset'? for (unsigned i=0; i= e.Start && Offset < e.End()) { ClickErrIdx = i; if (e.Suggestions.Length()) { auto Sp = s.AppendSub("Spelling"); if (Sp) { s.AppendSeparator(); for (unsigned n=0; nAppendItem(e.Suggestions[n], SPELLING_BASE+n); } // else printf("%s:%i - No sub menu.\n", _FL); } // else printf("%s:%i - No Suggestion.\n", _FL); break; } // else printf("%s:%i - Outside area, Offset=%i e=%i,%i.\n", _FL, Offset, e.Start, e.End()); } // Check for URLs under the cursor? ssize_t pos = 0; for (auto t: Txt) { // LgiTrace("t: %i %i\n", (int)pos, t->Element); if (t->Element == TAG_A) { // LgiTrace("off: %i %i %i\n", (int)Offset, (int)pos, (int)pos + (int)t->Length()); if (Offset >= pos && Offset < pos + (ssize_t)t->Length()) { // Is over a link... s.AppendItem("Open URL", IDM_OPEN_URL); ClickedUri = t->Param; break; } } pos += t->Length(); } return true; } LMessage::Result LRichTextPriv::TextBlock::OnEvent(LMessage* Msg) { switch (Msg->Msg()) { case M_COMMAND: { if (Msg->A() == IDM_OPEN_URL) { if (ClickedUri) LExecute(ClickedUri); } else if (auto e = SpellingErrors.AddressOf(ClickErrIdx)) { // Replacing text with spell check suggestion: int i = (int)Msg->A() - SPELLING_BASE; if (i >= 0 && i < (int)e->Suggestions.Length()) { auto Start = e->Start; LString s = e->Suggestions[i]; AutoTrans t(new LRichTextPriv::Transaction); // Delete the old text... DeleteAt(t, Start, e->Len); // 'e' might disappear here // Insert the new text.... LAutoPtr u((uint32_t*)LNewConvertCp("utf-32", s, "utf-8")); AddText(t, Start, u.Get(), Strlen(u.Get())); d->AddTrans(t); return true; } } break; } } return false; } bool LRichTextPriv::TextBlock::IsEmptyLine(BlockCursor *Cursor) { if (!Cursor) return false; TextLine *Line = Layout.AddressOf(Cursor->LineHint) ? Layout[Cursor->LineHint] : NULL; if (!Line) return false; auto LineLen = Line->Length(); return LineLen == 0; } LRichTextPriv::Block *LRichTextPriv::TextBlock::Clone() { return new TextBlock(this); } ssize_t LRichTextPriv::TextBlock::CopyAt(ssize_t Offset, ssize_t Chars, LArray *Text) { if (!Text) return 0; if (Chars < 0) Chars = Length() - Offset; ssize_t Pos = 0; for (size_t i=0; i= Pos && Offset < Pos + (int)t->Length()) { ssize_t Skip = Offset - Pos; ssize_t Remain = t->Length() - Skip; ssize_t Cp = MIN(Chars, Remain); Text->Add(&(*t)[Skip], Cp); Chars -= Cp; Offset += Cp; } Pos += t->Length(); } return Text->Length(); } ssize_t LRichTextPriv::TextBlock::FindAt(ssize_t StartIdx, const uint32_t *Str, LFindReplaceCommon *Params) { if (!Str || !Params) return -1; size_t InLen = Strlen(Str); bool Match; ssize_t CharPos = 0; for (size_t i=0; iFirst(); uint32_t *e = s + t->Length(); if (Params->MatchCase) { for (uint32_t *c = s; c < e; c++) { if (*c == *Str) { if (c + InLen <= e) Match = !Strncmp(c, Str, InLen); else { LArray tmp; if (CopyAt(CharPos + (c - s), InLen, &tmp) && tmp.Length() == InLen) Match = !Strncmp(&tmp[0], Str, InLen); else Match = false; } if (Match) return CharPos + (c - s); } } } else { uint32_t l = ToLower(*Str); for (uint32_t *c = s; c < e; c++) { if (ToLower(*c) == l) { if (c + InLen <= e) Match = !Strnicmp(c, Str, InLen); else { LArray tmp; if (CopyAt(CharPos + (c - s), InLen, &tmp) && tmp.Length() == InLen) Match = !Strnicmp(&tmp[0], Str, InLen); else Match = false; } if (Match) return CharPos + (c - s); } } } CharPos += t->Length(); } return -1; } bool LRichTextPriv::TextBlock::DoCase(Transaction *Trans, ssize_t StartIdx, ssize_t Chars, bool Upper) { LRange Blk(0, Len); LRange Inp(StartIdx, Chars < 0 ? Len - StartIdx : Chars); LRange Change = Blk.Overlap(Inp); PreEdit(Trans); LRange Run(0, 0); bool Changed = false; for (unsigned i=0; iLength(); LRange Edit = Run.Overlap(Change); if (Edit.Len > 0) { uint32_t *s = st->At(Edit.Start - Run.Start); for (int n=0; n= 'a' && s[n] <= 'z') s[n] = s[n] - 'a' + 'A'; } else { if (s[n] >= 'A' && s[n] <= 'Z') s[n] = s[n] - 'A' + 'a'; } } Changed = true; } Run.Start += Run.Len; } LayoutDirty |= Changed; return Changed; } LRichTextPriv::Block *LRichTextPriv::TextBlock::Split(Transaction *Trans, ssize_t AtOffset) { if (AtOffset < 0 || AtOffset >= Len) return NULL; LRichTextPriv::TextBlock *After = new LRichTextPriv::TextBlock(d); if (!After) { d->Error(_FL, "Alloc Err"); return NULL; } After->SetStyle(GetStyle()); ssize_t Pos = 0; unsigned i; for (i=0; iLength(); if (AtOffset >= Pos && AtOffset < Pos + StLen) { ssize_t StOff = AtOffset - Pos; if (StOff > 0) { // Split the text into 2 blocks... uint32_t *t = St->At(StOff); ssize_t remaining = St->Length() - StOff; StyleText *AfterText = new StyleText(t, remaining, St->GetStyle()); if (!AfterText) { d->Error(_FL, "Alloc Err"); return NULL; } St->Length(StOff); i++; Len = Pos + StOff; After->Txt.Add(AfterText); After->Len += AfterText->Length(); } else { Len = Pos; } break; } Pos += StLen; } while (i < Txt.Length()) { StyleText *St = Txt[i]; Txt.DeleteAt(i, true); After->Txt.Add(St); After->Len += St->Length(); } LayoutDirty = true; After->LayoutDirty = true; return After; } void LRichTextPriv::TextBlock::IncAllStyleRefs() { if (Style) Style->RefCount++; for (unsigned i=0; iGetStyle(); if (s) s->RefCount++; } } bool LRichTextPriv::TextBlock::ChangeStyle(Transaction *Trans, ssize_t Offset, ssize_t Chars, LCss *Style, bool Add) { if (!Style) return d->Error(_FL, "No style."); if (Offset < 0 || Offset >= Len) return true; if (Chars < 0) Chars = Len; if (Trans) Trans->Add(new CompleteTextBlockState(d, this)); ssize_t CharPos = 0; ssize_t RestyleEnd = Offset + Chars; for (unsigned i=0; iLength(); ssize_t End = CharPos + Len; if (End <= Offset || CharPos > RestyleEnd) ; else { ssize_t Before = Offset >= CharPos ? Offset - CharPos : 0; LAssert(Before >= 0); ssize_t After = RestyleEnd < End ? End - RestyleEnd : 0; LAssert(After >= 0); ssize_t Inside = Len - Before - After; LAssert(Inside >= 0); LAutoPtr TmpStyle(new LCss); if (Add) { if (t->GetStyle()) *TmpStyle = *t->GetStyle(); *TmpStyle += *Style; } else if (Style->Length() != 0) { if (t->GetStyle()) *TmpStyle = *t->GetStyle(); *TmpStyle -= *Style; } LNamedStyle *CacheStyle = TmpStyle && TmpStyle->Length() ? d->AddStyleToCache(TmpStyle) : NULL; if (Before && After) { // Split into 3 parts: // |---before----|###restyled###|---after---| StyleText *st = new StyleText(t->At(Before), Inside, CacheStyle); if (st) Txt.AddAt(++i, st); st = new StyleText(t->At(Before + Inside), After, t->GetStyle()); if (st) Txt.AddAt(++i, st); t->Length(Before); LayoutDirty = true; return true; } else if (Before) { // Split into 2 parts: // |---before----|###restyled###| StyleText *st = new StyleText(t->At(Before), Inside, CacheStyle); if (st) Txt.AddAt(++i, st); t->Length(Before); LayoutDirty = true; } else if (After) { // Split into 2 parts: // |###restyled###|---after---| StyleText *st = new StyleText(t->At(0), Inside, CacheStyle); if (st) Txt.AddAt(i, st); memmove(t->At(0), t->At(Inside), After*sizeof(uint32_t)); t->Length(After); LayoutDirty = true; } else if (Inside) { // Re-style the whole run t->SetStyle(CacheStyle); LayoutDirty = true; } } CharPos += Len; } // Merge any regions of the same style into contiguous sections for (unsigned i=0; iGetStyle() == b->GetStyle() && a->Emoji == b->Emoji) { // Merge... a->Add(b->AddressOf(0), b->Length()); Txt.DeleteAt(i + 1, true); delete b; i--; } } return true; } bool LRichTextPriv::TextBlock::Seek(SeekType To, BlockCursor &Cur) { int XOffset = Cur.Pos.x1 - Pos.x1; ssize_t CharPos = 0; LArray LineOffset; LArray LineLen; int CurLine = -1; int CurLineScore = 0; for (unsigned i=0; iLength(); LineOffset[i] = CharPos; LineLen[i] = Len; if (Cur.Offset >= CharPos && Cur.Offset <= CharPos + Len - Line->NewLine) // Minus 'NewLine' is because the cursor can't be // after the '\n' on a line. It's actually on the // next line. { int Score = 1; if (Cur.LineHint >= 0 && i == Cur.LineHint) Score++; if (Score > CurLineScore) { CurLine = i; CurLineScore = Score; } } CharPos += Len; } if (CurLine < 0) { CharPos = 0; d->Log->Print("TextBlock(%i)::Seek, lines=%i\n", GetUid(), Layout.Length()); for (unsigned i=0; iLog->Print("\tLine[%i] @ %i+%i=%i\n", i, CharPos, Line->Length(), CharPos + Line->Length()); CharPos += Line->Length(); } else { d->Log->Print("\tLine[%i] @ %i, is NULL\n", i, CharPos); break; } } return d->Error(_FL, "Index '%i' not in layout lines.", Cur.Offset); } TextLine *Line = NULL; switch (To) { case SkLineStart: { Cur.Offset = LineOffset[CurLine]; Cur.LineHint = CurLine; return true; } case SkLineEnd: { Cur.Offset = LineOffset[CurLine] + LineLen[CurLine] - Layout[CurLine]->NewLine; Cur.LineHint = CurLine; return true; } case SkUpLine: { // Get previous line... if (CurLine == 0) return false; Line = Layout[--CurLine]; if (!Line) return d->Error(_FL, "No line at %i.", CurLine); break; } case SkDownLine: { // Get next line... if (CurLine >= (int)Layout.Length() - 1) return false; Line = Layout[++CurLine]; if (!Line) return d->Error(_FL, "No line at %i.", CurLine); break; } default: { return false; break; } } if (Line) { // Work out where the cursor should be based on the 'XOffset' if (Line->Strs.Length() > 0) { int FixX = 0; ssize_t CharOffset = 0; for (unsigned i=0; iStrs.Length(); i++) { DisplayStr *Ds = Line->Strs[i]; PtrCheckBreak(Ds); if (XOffset >= FixedToInt(FixX) && XOffset <= FixedToInt(FixX + Ds->FX())) { // This is the matching string... int Px = XOffset - FixedToInt(FixX) - Line->PosOff.x1; ssize_t Char = Ds->PosToIndex(Px, true); if (Char >= 0) { Cur.Offset = LineOffset[CurLine] + // Character offset of line CharOffset + // Character offset of current string Char; // Offset into current string for 'XOffset' Cur.LineHint = CurLine; return true; } } FixX += Ds->FX(); CharOffset += Ds->Length(); } // Cursor is nearest the end of the string...? Cur.Offset = LineOffset[CurLine] + Line->Length() - Line->NewLine; Cur.LineHint = CurLine; return true; } else if (Line->NewLine) { Cur.Offset = LineOffset[CurLine]; Cur.LineHint = CurLine; return true; } } return false; } #ifdef _DEBUG void LRichTextPriv::TextBlock::DumpNodes(LTreeItem *Ti) { LString s; s.Printf("TextBlock, style=%s, pos=%s, ptr=%p", Style?Style->Name.Get():NULL, Pos.GetStr(), this); Ti->SetText(s); LTreeItem *TxtRoot = PrintNode(Ti, "Txt(%i)", Txt.Length()); if (TxtRoot) { ssize_t Pos = 0; for (size_t i=0; iLength(); LString u; if (Len) { LStringPipe p(256); uint32_t *Str = St->At(0); p.Write("\'", 1); for (int k=0; k= 0x10000) p.Print("&#%i;", Str[k]); else { uint8_t utf8[6], *n = utf8; ssize_t utf8len = sizeof(utf8); if (LgiUtf32To8(Str[k], n, utf8len)) p.Write(utf8, sizeof(utf8)-utf8len); } } p.Write("\'", 1); - u = p.NewGStr(); + u = p.NewLStr(); } else u = "(Empty)"; PrintNode( TxtRoot, "[%i] range=%i-%i, len=%i, style=%s, %s", i, Pos, Pos + Len - 1, Len, St->GetStyle() ? St->GetStyle()->Name.Get() : NULL, u.Get()); Pos += Len; } } LTreeItem *LayoutRoot = PrintNode(Ti, "Layout(%i)", Layout.Length()); if (LayoutRoot) { ssize_t Pos = 0; for (size_t i=0; iLength() - 1, Tl->Length(), Tl->NewLine, Tl->PosOff.GetStr()); for (unsigned n=0; nStrs.Length(); n++) { DisplayStr *Ds = Tl->Strs[n]; LNamedStyle *Style = Ds->Src ? Ds->Src->GetStyle() : NULL; PrintNode( Elem, "[%i] style=%s len=%i txt='%.20S'", n, Style ? Style->Name.Get() : NULL, Ds->Length(), (const char16*) (*Ds)); } Pos += Tl->Length() + Tl->NewLine; } } } #endif diff --git a/src/linux/Lgi/App.cpp b/src/linux/Lgi/App.cpp --- a/src/linux/Lgi/App.cpp +++ b/src/linux/Lgi/App.cpp @@ -1,1476 +1,1476 @@ #include #include #include #include #ifndef WIN32 #include #include #endif #include #define _GNU_SOURCE #include #include "lgi/common/Lgi.h" #include "lgi/common/SkinEngine.h" #include "lgi/common/Array.h" #include "lgi/common/Variant.h" #include "lgi/common/Token.h" #include "lgi/common/FontCache.h" #include "AppPriv.h" #define DEBUG_MSG_TYPES 0 #define DEBUG_HND_WARNINGS 0 #define IDLE_ALWAYS 0 using namespace Gtk; bool GlibWidgetSearch(GtkWidget *p, GtkWidget *w, bool Debug, int depth = 0); //////////////////////////////////////////////////////////////// struct OsAppArgumentsPriv { ::LArray Ptr; ~OsAppArgumentsPriv() { Ptr.DeleteArrays(); } }; OsAppArguments::OsAppArguments(int args, const char **arg) { d = new OsAppArgumentsPriv; for (int i=0; iPtr.Add(NewStr(arg[i])); } Args = d->Ptr.Length(); Arg = &d->Ptr[0]; } OsAppArguments::~OsAppArguments() { DeleteObj(d); } bool OsAppArguments::Get(const char *Name, const char **Val) { if (!Name) return false; for (int i=0; iPtr.DeleteArrays(); if (!CmdLine) return; for (char *s = CmdLine; *s; ) { while (*s && strchr(WhiteSpace, *s)) s++; if (*s == '\'' || *s == '\"') { char delim = *s++; char *e = strchr(s, delim); if (e) d->Ptr.Add(NewStr(s, e - s)); else break; s = e + 1; } else { char *e = s; while (*e && !strchr(WhiteSpace, *e)) e++; d->Ptr.Add(NewStr(s, e-s)); s = e; } } Args = d->Ptr.Length(); Arg = &d->Ptr[0]; } OsAppArguments &OsAppArguments::operator =(OsAppArguments &a) { d->Ptr.DeleteArrays(); for (int i=0; iPtr.Add(NewStr(a.Arg[i])); } Args = d->Ptr.Length(); Arg = &d->Ptr[0]; return *this; } //////////////////////////////////////////////////////////////// #if HAS_SHARED_MIME #include "GFilterUtils.h" #include "mime-types.h" class LSharedMime : public LLibrary { public: LSharedMime() : #ifdef _DEBUG LLibrary("libsharedmime1d") #else LLibrary("libsharedmime1") #endif { } DynFunc0(int, mimetypes_init); DynFunc1(const char*, mimetypes_set_default_type, const char *, default_type); DynFunc2(const char*, mimetypes_get_file_type, const char*, pathname, mimetypes_flags, flags); DynFunc2(const char*, mimetypes_get_data_type, const void*, data, int, length); DynFunc3(bool, mimetypes_decode, const char *, type, char **, media_type, char **, sub_type); DynFunc2(char *, mimetypes_convert_filename, const char *, pathname, const char *, mime_type); DynFunc3(bool, mimetypes_add_mime_dir, const char *, path, bool, high_priority, bool, rescan); DynFunc2(const char *, mimetypes_get_type_info, const char *, mime_type, const char *, lang); }; #endif ///////////////////////////////////////////////////////////////////////////// // // Attempts to cleanup and call drkonqi to process the crash // static LString CrashHandlerApp; void LgiCrashHandler(int Sig) { // Don't get into an infinite loop signal(SIGSEGV, SIG_DFL); #ifndef _MSC_VER // Our pid LString pid; pid.Printf("%i", getpid()); LgiTrace("LgiCrashHandler trigger pid=%s\n", pid.Get()); auto child = fork(); if (!child) { LFile::Path workingDir = CrashHandlerApp; workingDir--; chdir(workingDir); const char *args[] = { CrashHandlerApp, "--pid", pid, NULL }; execvp(CrashHandlerApp, args); exit(0); } if (LAppInst->InThread()) { LgiTrace("LgiCrashHandler showing dlg\n"); LgiMsg(NULL, "Application crashed... dumping details.", "Crash"); } else { LgiTrace("LgiCrashHandler called from worker thread.\n"); LSleep(10000); // Wait for the crash handler to do it's thing... } #endif exit(-1); } ///////////////////////////////////////////////////////////////////////////// #ifndef XK_Num_Lock #define XK_Num_Lock 0xff7f #endif #ifndef XK_Shift_Lock #define XK_Shift_Lock 0xffe6 #endif #ifndef XK_Caps_Lock #define XK_Caps_Lock 0xffe5 #endif struct Msg { LViewI *v; int m; LMessage::Param a, b; void Set(LViewI *V, int M, LMessage::Param A, LMessage::Param B) { v = V; m = M; a = A; b = B; } }; // Out of thread messages... must lock before access. class LMessageQue : public LMutex { public: typedef LArray MsgArray; LMessageQue() : LMutex("LMessageQue") { } MsgArray *Lock(const char *file, int line) { if (!LMutex::Lock(file, line)) return NULL; return &q; } operator bool() { return q.Length() > 0; } private: MsgArray q; } MsgQue; ///////////////////////////////////////////////////////////////////////////// LSkinEngine *LApp::SkinEngine = 0; LApp *TheApp = 0; LMouseHook *LApp::MouseHook = 0; LApp::LApp(OsAppArguments &AppArgs, const char *name, LAppArguments *Args) : OsApplication(AppArgs.Args, AppArgs.Arg) { TheApp = this; d = new LAppPrivate(this); Name(name); LgiArgsAppPath = AppArgs.Arg[0]; if (LIsRelativePath(LgiArgsAppPath)) { char Cwd[MAX_PATH_LEN]; getcwd(Cwd, sizeof(Cwd)); LMakePath(Cwd, sizeof(Cwd), Cwd, LgiArgsAppPath); LgiArgsAppPath = Cwd; } int WCharSz = sizeof(wchar_t); #if defined(_MSC_VER) LAssert(WCharSz == 2); ::LFile::Path Dlls(LgiArgsAppPath); Dlls--; SetDllDirectoryA(Dlls); #else LAssert(WCharSz == 4); #endif #ifdef _MSC_VER SetEnvironmentVariable(_T("GTK_CSD"), _T("0")); #else setenv("GTK_CSD", "0", true); #endif // We want our printf's NOW! setvbuf(stdout, (char*)NULL,_IONBF, 0); // print mesgs immediately. // Setup the file and graphics sub-systems d->FileSystem = new LFileSystem; d->GdcSystem = new GdcDevice; SetAppArgs(AppArgs); srand(LCurrentTime()); LColour::OnChange(); Gtk::gchar id[256]; sprintf_s(id, sizeof(id), "com.memecode.%s", name); d->App = Gtk::gtk_application_new(id, Gtk::G_APPLICATION_FLAGS_NONE); LAssert(d->App != NULL); MouseHook = new LMouseHook; // Setup the SIGSEGV signal to call the crash handler if (!GetOption("nch")) { auto programName = "crash-handler"; LFile::Path p(LSP_APP_INSTALL); p += programName; if (!p.Exists()) { // Check alternate location for development builds Dl_info dlInfo; dladdr(LgiCrashHandler, &dlInfo); if (dlInfo.dli_sname != NULL && dlInfo.dli_saddr != NULL) { p = dlInfo.dli_fname; p += "../../src/linux/CrashHandler"; p += programName; printf("Alternative path %s: %s\n", p.Exists() ? "found" : "missing", p.GetFull().Get()); } } if (p.Exists()) { CrashHandlerApp = p; signal(SIGSEGV, LgiCrashHandler); LgiTrace("Crash handler: '%s' installed.\n", CrashHandlerApp.Get()); } else { LgiTrace("Crash handler: No crash handler '%s' found, SIGSEGV handler not installed.\n", p.GetFull().Get()); } } else { LgiTrace("Crash handler: disabled.\n"); } d->GetConfig(); // System font setup LFontType SysFontType; Gtk::PangoFontMap *fm = Gtk::pango_cairo_font_map_get_default(); if (fm) { using namespace Gtk; auto cfm = PANGO_CAIRO_FONT_MAP(fm); #ifdef MAC double Dpi = 80.0; #else double Dpi = 96.0; #endif ::LFile::Path p(LSP_APP_ROOT); p += "lgi-conf.json"; if (p.IsFile()) { ::LFile f(p, O_READ); LJson j(f.Read()); auto sDpi = j.Get("DPI"); if (sDpi) Dpi = sDpi.Float(); } pango_cairo_font_map_set_resolution(cfm, Dpi); } if (SysFontType.GetSystemFont("System")) { SystemNormal = SysFontType.Create(); if (SystemNormal) SystemNormal->Transparent(true); SystemBold = SysFontType.Create(); if (SystemBold) { SystemBold->Bold(true); SystemBold->Transparent(true); SystemBold->Create(); } } else { printf("%s:%i - Couldn't get system font setting.\n", __FILE__, __LINE__); } if (!SystemNormal) { LgiMsg(0, "Error: Couldn't create system font.", "Lgi Error: LApp::LApp", MB_OK); LExitApp(); } if (!GetOption("noskin")) { extern LSkinEngine *CreateSkinEngine(LApp *App); SkinEngine = CreateSkinEngine(this); } } LApp::~LApp() { DeleteObj(AppWnd); DeleteObj(SystemNormal); DeleteObj(SystemBold); DeleteObj(SkinEngine); DeleteObj(MouseHook); DeleteObj(d->FileSystem); DeleteObj(d->GdcSystem); DeleteObj(LFontSystem::Me); DeleteObj(d); TheApp = 0; } LApp *LApp::ObjInstance() { return TheApp; } bool LApp::IsOk() { bool Status = #ifndef __clang__ (this != 0) && #endif (d != 0); LAssert(Status); return Status; } LMouseHook *LApp::GetMouseHook() { return MouseHook; } int LApp::GetMetric(LSystemMetric Metric) { switch (Metric) { case LGI_MET_DECOR_X: return 8; case LGI_MET_DECOR_Y: return 8 + 19; case LGI_MET_DECOR_CAPTION: return 19; default: break; } return 0; } LViewI *LApp::GetFocus() { // GtkWidget *w = gtk_window_get_focus(GtkWindow *window); return NULL; } OsThread LApp::_GetGuiThread() { return d->GuiThread; } OsThreadId LApp::GetGuiThreadId() { return d->GuiThreadId; } OsProcessId LApp::GetProcessId() { #ifdef WIN32 return GetCurrentProcessId(); #else return getpid(); #endif } OsAppArguments *LApp::GetAppArgs() { return IsOk() ? &d->Args : 0; } void LApp::SetAppArgs(OsAppArguments &AppArgs) { if (IsOk()) { d->Args = AppArgs; } } bool LApp::InThread() { OsThreadId Me = GetCurrentThreadId(); OsThreadId Gui = GetGuiThreadId(); // printf("Me=%i Gui=%i\n", Me, Gui); return Gui == Me; } struct GtkIdle { LAppPrivate *d; LAppI::OnIdleProc cb; void *param; }; Gtk::gboolean IdleWrapper(Gtk::gpointer data) { #if 0 static int64 ts = LCurrentTime(); static int count = 0; int64 now = LCurrentTime(); if (now - ts > 300) { printf("IdleWrapper = %i\n", count); count = 0; ts = now; } else count++; #endif GtkIdle *i = (GtkIdle*) data; if (i->cb) i->cb(i->param); LMessageQue::MsgArray *Msgs; if (MsgQue && (Msgs = MsgQue.Lock(_FL))) { // printf("IdleWrapper start %i\n", (int)Msgs->Length()); // Copy the messages out of the locked structure.. // This allows new messages to arrive independent // of us processing them here... LMessageQue::MsgArray q = *Msgs; Msgs->Empty(); MsgQue.Unlock(); for (auto m : q) { if (!LView::LockHandler(m.v, LView::OpExists)) { // LgiTrace("%s:%i - Invalid view: %p.\n", _FL, m.v); } else { LMessage Msg(m.m, m.a, m.b); // LgiTrace("%s::OnEvent %i,%i,%i\n", m.v->GetClass(), m.m, m.a, m.b); m.v->OnEvent(&Msg); } } } else { // printf("IdleWrapper start no lock\n"); } // printf("IdleWrapper end\n"); return i->cb != NULL; // return false; } static GtkIdle idle = {0}; bool LApp::Run(OnIdleProc IdleCallback, void *IdleParam) { if (!InThread()) { printf("%s:%i - Error: Out of thread.\n", _FL); return false; } if (!idle.d) { idle.d = d; idle.cb = IdleCallback; idle.param = IdleParam; } static bool CmdLineDone = false; if (!CmdLineDone) { CmdLineDone = true; OnCommandLine(); } Gtk::gtk_main(); return true; } bool LApp::Yield() { Gtk::gtk_main_iteration_do(false); return true; } void LApp::Exit(int Code) { if (Code) { // hard exit ::exit(Code); } else { // soft exit Gtk::gtk_main_quit(); if (d->IdleId.Length() > 0) { size_t Last = d->IdleId.Length() - 1; Gtk::g_source_remove(d->IdleId[Last]); d->IdleId.Length(Last); } } } bool LApp::PostEvent(LViewI *View, int Msg, LMessage::Param a, LMessage::Param b) { auto q = MsgQue.Lock(_FL); if (!q) { printf("%s:%i - Couldn't lock app.\n", _FL); return false; } q->New().Set(View, Msg, a, b); #if 1 // defined(_DEBUG) if (q->Length() > 200 && (q->Length()%20)==0) { static uint64 prev = 0; auto now = LCurrentTime(); if (now - prev >= 1000) { prev = now; #if defined(WIN32) char s[256]; sprintf_s(s, sizeof(s), #else printf( #endif "PostEvent Que=" LPrintfSizeT " (msg=%i)\n", q->Length(), Msg); #if defined(WIN32) OutputDebugStringA(s); #endif #ifdef LINUX LHashTbl, size_t> MsgCounts; for (auto &msg: *q) MsgCounts.Add(msg.m, MsgCounts.Find(msg.m) + 1); for (auto c: MsgCounts) printf(" %i->%i\n", c.key, c.value); if (Msg == 916) { int asd=0; } #endif } } #endif MsgQue.Unlock(); // g_idle_add((GSourceFunc)IdleWrapper, &idle); g_idle_add_full(G_PRIORITY_HIGH_IDLE, (GSourceFunc)IdleWrapper, &idle, NULL); return true; } void LApp::OnUrl(const char *Url) { if (AppWnd) AppWnd->OnUrl(Url); } void LApp::OnReceiveFiles(::LArray &Files) { if (AppWnd) AppWnd->OnReceiveFiles(Files); else LAssert(!"You probably want to set 'AppWnd' before calling LApp::Run... maybe."); } const char *LApp::KeyModFlags::FlagName(int Flag) { #define CHECK(f) if (Flag & f) return #f; CHECK(GDK_SHIFT_MASK) CHECK(GDK_LOCK_MASK) CHECK(GDK_CONTROL_MASK) CHECK(GDK_MOD1_MASK) CHECK(GDK_MOD2_MASK) CHECK(GDK_MOD3_MASK) CHECK(GDK_MOD4_MASK) CHECK(GDK_MOD5_MASK) CHECK(GDK_BUTTON1_MASK) CHECK(GDK_BUTTON2_MASK) CHECK(GDK_BUTTON3_MASK) CHECK(GDK_BUTTON4_MASK) CHECK(GDK_BUTTON5_MASK) CHECK(GDK_SUPER_MASK) CHECK(GDK_HYPER_MASK) CHECK(GDK_META_MASK) CHECK(GDK_RELEASE_MASK) #undef CHECK return NULL; } int LApp::KeyModFlags::FlagValue(const char *Name) { #define CHECK(f) if (!Stricmp(Name, #f)) return f; CHECK(GDK_SHIFT_MASK) CHECK(GDK_LOCK_MASK) CHECK(GDK_CONTROL_MASK) CHECK(GDK_MOD1_MASK) CHECK(GDK_MOD2_MASK) CHECK(GDK_MOD3_MASK) CHECK(GDK_MOD4_MASK) CHECK(GDK_MOD5_MASK) CHECK(GDK_BUTTON1_MASK) CHECK(GDK_BUTTON2_MASK) CHECK(GDK_BUTTON3_MASK) CHECK(GDK_BUTTON4_MASK) CHECK(GDK_BUTTON5_MASK) CHECK(GDK_SUPER_MASK) CHECK(GDK_HYPER_MASK) CHECK(GDK_META_MASK) CHECK(GDK_RELEASE_MASK) #undef CHECK return 0; } ::LString LApp::KeyModFlags::FlagsToString(int s) { ::LString::Array a; for (int i=0; i<32; i++) { if (((1 << i) & s) != 0) a.New() = FlagName(1 << i); } return ::LString(",").Join(a); } LApp::KeyModFlags *LApp::GetKeyModFlags() { return d->GetModFlags(); } const char *LApp::GetArgumentAt(int n) { return n >= 0 && n < d->Args.Args ? NewStr(d->Args.Arg[n]) : 0; } bool LApp::GetOption(const char *Option, char *Dest, int DestLen) { ::LString Buf; if (GetOption(Option, Buf)) { if (Dest) { if (DestLen > 0) { strcpy_s(Dest, DestLen, Buf); } else return false; } return true; } return false; } bool LApp::GetOption(const char *Option, ::LString &Buf) { if (IsOk() && Option) { int OptLen = strlen(Option); for (int i=1; iArgs.Args; i++) { char *a = d->Args.Arg[i]; if (!a) continue; if (strchr("-/\\", a[0])) { if (strnicmp(a+1, Option, OptLen) == 0) { char *Arg = 0; if (strlen(a+1+OptLen) > 0) { Arg = a + 1 + OptLen; } else if (i < d->Args.Args - 1) { Arg = d->Args.Arg[i + 1]; } if (Arg) { if (strchr("\'\"", *Arg)) { char Delim = *Arg++; char *End = strchr(Arg, Delim); if (End) { auto Len = End-Arg; if (Len > 0) Buf.Set(Arg, Len); else return false; } else return false; } else { Buf = Arg; } } return true; } } } } return false; } void LApp::OnCommandLine() { ::LArray Files; for (int i=1; iArgs; i++) { char *a = GetAppArgs()->Arg[i]; if (LFileExists(a)) { Files.Add(NewStr(a)); } } // call app if (Files.Length() > 0) { OnReceiveFiles(Files); } // clear up Files.DeleteArrays(); } ::LString LApp::GetFileMimeType(const char *File) { ::LString Status; char Full[MAX_PATH_LEN] = ""; if (!LFileExists(File)) { // Look in the path LToken p(getenv("PATH"), LGI_PATH_SEPARATOR); for (int i=0; i 0) Status = s.SplitDelimit(":").Last().Strip(); } return Status; } } #endif #if HAS_LIB_MAGIC static bool MagicError = false; if (d->MagicLock.Lock(_FL)) { if (!d->hMagic && !MagicError) { d->hMagic = magic_open(MAGIC_MIME_TYPE); if (d->hMagic) { if (magic_load(d->hMagic, NULL) != 0) { printf("%s:%i - magic_load failed - %s\n", _FL, magic_error(d->hMagic)); magic_close(d->hMagic); d->hMagic = NULL; MagicError = true; } } else { printf("%s:%i - magic_open failed.\n", _FL); MagicError = true; } } if (d->hMagic && !MagicError) { const char *mt = magic_file(d->hMagic, File); if (mt) { Status = mt; } } d->MagicLock.Unlock(); if (Status) return Status; } #endif #if HAS_SHARED_MIME // freedesktop.org rocks... if (!d->Sm) { // Not loaded, go and try to load it... d->Sm = new LSharedMime; if (d->Sm && d->Sm->IsLoaded()) { d->Sm->mimetypes_init(); } } if (d->Sm && d->Sm->IsLoaded()) { // Loaded... char *m = (char*)d->Sm->mimetypes_get_file_type(File, MIMETYPES_CHECK_ALL); if (m) { #if HAS_FILE_CMD if (stricmp(m, "application/x-not-recognised") != 0) #endif { strcpy(Mime, m); return true; } } else { printf("%s:%i - mimetypes_get_file_type failed for '%s'\n", __FILE__, __LINE__, File); } } else { printf("%s:%i - Shared Mime not loaded!!!\n", __FILE__, __LINE__); } #endif #if HAS_FILE_CMD // doh! not installed... :( LStringPipe Output; char Args[256]; sprintf(Args, "-i \"%s\"", File); LSubProcess p("file", Args); if (p.Start()) { p.Communicate(&Output); char *Out = Output.NewStr(); if (Out) { char *s = strchr(Out, ':'); if (s && strchr(Out, '/')) { s += 2; char *e = s; while ( *e && ( IsAlpha(*e) || IsDigit(*e) || strchr(".-_/", *e) ) ) e++; *e = 0; Status.Reset(NewStr(s)); } DeleteArray(Out); } } #endif return Status; } bool LApp::GetAppsForMimeType(const char *Mime, LArray<::LAppInfo> &Apps) { // Find alternative version of the MIME type (e.g. x-type and type). char AltMime[256]; strcpy(AltMime, Mime); char *s = strchr(AltMime, '/'); if (s) { s++; int Len = strlen(s) + 1; if (strnicmp(s, "x-", 2) == 0) { memmove(s, s+2, Len - 2); } else { memmove(s+2, s, Len); s[0] = 'x'; s[1] = '-'; } } LGetAppsForMimeType(Mime, Apps); LGetAppsForMimeType(AltMime, Apps); return Apps.Length() > 0; } #if defined(LINUX) LLibrary *LApp::GetWindowManagerLib() { if (this != NULL && !d->WmLib) { char Lib[32]; WindowManager Wm = LGetWindowManager(); switch (Wm) { case WM_Kde: strcpy(Lib, "liblgikde3"); break; case WM_Gnome: strcpy(Lib, "liblgignome2"); break; default: strcpy(Lib, "liblgiother"); break; } #ifdef _DEBUG strcat(Lib, "d"); #endif d->WmLib = new LLibrary(Lib, true); if (d->WmLib) { if (d->WmLib->IsLoaded()) { Proc_LgiWmInit WmInit = (Proc_LgiWmInit) d->WmLib->GetAddress("LgiWmInit"); if (WmInit) { WmInitParams Params; // Params.Dsp = XObject::XDisplay(); Params.Args = d->Args.Args; Params.Arg = d->Args.Arg; WmInit(&Params); } // else printf("%s:%i - Failed to find method 'LgiWmInit' in WmLib.\n", __FILE__, __LINE__); } // else printf("%s:%i - couldn't load '%s.so'\n", __FILE__, __LINE__, Lib); } // else printf("%s:%i - alloc error\n", __FILE__, __LINE__); } return d->WmLib && d->WmLib->IsLoaded() ? d->WmLib : 0; } void LApp::DeleteMeLater(LViewI *v) { d->DeleteLater.Add(v); } void LApp::SetClipBoardContent(OsView Hnd, ::LVariant &v) { // Store the clipboard data we will serve d->ClipData = v; } bool LApp::GetClipBoardContent(OsView Hnd, ::LVariant &v, ::LArray &Types) { return false; } #endif LSymLookup *LApp::GetSymLookup() { return d; } bool LApp::IsElevated() { #ifdef WIN32 LAssert(!"What API works here?"); return false; #else return geteuid() == 0; #endif } int LApp::GetCpuCount() { return 1; } LFontCache *LApp::GetFontCache() { if (!d->FontCache) d->FontCache.Reset(new LFontCache(SystemNormal)); return d->FontCache; } #ifdef LINUX LApp::DesktopInfo::DesktopInfo(const char *file) { File = file; Dirty = false; if (File) Serialize(false); } bool LApp::DesktopInfo::Serialize(bool Write) { ::LFile f; if (Write) { ::LFile::Path p(File); p--; if (!p.Exists()) return false; } else if (!LFileExists(File)) return false; if (!f.Open(File, Write?O_WRITE:O_READ)) { LgiTrace("%s:%i - Failed to open '%s'\n", _FL, File.Get()); return false; } if (Write) { f.SetSize(0); for (unsigned i=0; i= 0) { int e = l.Find("]", ++s); if (e >= 0) { Cur = &Data.New(); Cur->Name = l(s, e - s + 1); } } else if ((s = l.Find("=")) >= 0) { if (!Cur) Cur = &Data.New(); KeyPair &kp = Cur->Values.New(); kp.Key = l(0, s).Strip(); kp.Value = l(++s, -1).Strip(); // printf("Read '%s': '%s'='%s'\n", Cur->Name.Get(), kp.Key.Get(), kp.Value.Get()); } } } return true; } LApp::DesktopInfo::Section *LApp::DesktopInfo::GetSection(const char *Name, bool Create) { for (unsigned i=0; iGet(Field, false, Dirty); if (kp) { return kp->Value; } } } return ::LString(); } bool LApp::DesktopInfo::Set(const char *Field, const char *Value, const char *Sect) { if (!Field) return false; Section *s = GetSection(Sect ? Sect : DefaultSection, true); if (!s) return false; KeyPair *kp = s->Get(Field, true, Dirty); if (!kp) return false; if (kp->Value != Value) { kp->Value = Value; Dirty = true; } return true; } LApp::DesktopInfo *LApp::GetDesktopInfo() { auto sExe = LGetExeFile(); ::LFile::Path Exe(sExe); ::LFile::Path Desktop(LSP_HOME); ::LString Leaf; Leaf.Printf("%s.desktop", Exe.Last().Get()); Desktop += ".local/share/applications"; Desktop += Leaf; const char *Ex = Exe; const char *Fn = Desktop; if (d->DesktopInfo.Reset(new DesktopInfo(Desktop))) { // Do a sanity check... ::LString s = d->DesktopInfo->Get("Name"); if (!s && Name()) d->DesktopInfo->Set("Name", Name()); s = d->DesktopInfo->Get("Exec"); if (!s || s != (const char*)sExe) d->DesktopInfo->Set("Exec", sExe); s = d->DesktopInfo->Get("Type"); if (!s) d->DesktopInfo->Set("Type", "Application"); s = d->DesktopInfo->Get("Categories"); if (!s) d->DesktopInfo->Set("Categories", "Application;"); s = d->DesktopInfo->Get("Terminal"); if (!s) d->DesktopInfo->Set("Terminal", "false"); d->DesktopInfo->Update(); } return d->DesktopInfo; } bool LApp::SetApplicationIcon(const char *FileName) { DesktopInfo *di = GetDesktopInfo(); if (!di) return false; ::LString IcoPath = di->Get("Icon"); if (IcoPath == FileName) return true; di->Set("Icon", FileName); return di->Update(); } #endif //////////////////////////////////////////////////////////////// OsApplication *OsApplication::Inst = 0; class OsApplicationPriv { public: OsApplicationPriv() { } }; OsApplication::OsApplication(int Args, char **Arg) { Inst = this; d = new OsApplicationPriv; } OsApplication::~OsApplication() { DeleteObj(d); Inst = 0; } //////////////////////////////////////////////////////////////// void LMessage::Set(int Msg, Param ParamA, Param ParamB) { m = Msg; a = ParamA; b = ParamB; } struct GlibEventParams : public LMessage { GtkWidget *w; }; bool GlibWidgetSearch(GtkWidget *p, GtkWidget *w, bool Debug, int depth) { char indent[256] = ""; if (Debug) { int ch = depth * 2; memset(indent, ' ', ch); indent[ch] = 0; printf("%sGlibWidgetSearch: %p, %p\n", indent, p, w); } if (p == w) return true; if (GTK_IS_CONTAINER(p)) { int n = 0; Gtk::GList *top = gtk_container_get_children(GTK_CONTAINER(p)); Gtk::GList *i = top; while (i) { if (Debug) printf("%s[%i]=%s\n", indent, n, gtk_widget_get_name((GtkWidget*)i->data)); if (i->data == w) return true; else if (GlibWidgetSearch((GtkWidget*)i->data, w, Debug, depth + 1)) return true; i = i->next; n++; } g_list_free(top); } else if (GTK_IS_BIN(p)) { GtkWidget *child = gtk_bin_get_child(GTK_BIN(p)); if (child) { if (Debug) printf("%schild=%s\n", indent, gtk_widget_get_name(child)); if (child == w) return true; else if (GlibWidgetSearch(child, w, Debug, depth + 1)) return true; } } else if (Debug) { printf("%sUnknown=%s\n", indent, gtk_widget_get_name(p)); } return false; } void LApp::OnDetach(LViewI *View) { LMessageQue::MsgArray *q = MsgQue.Lock(_FL); if (!q) { printf("%s:%i - Couldn't lock app.\n", _FL); return; } MsgQue.Unlock(); } bool LMessage::Send(LViewI *View) { if (!View) { LAssert(!"No view"); return false; } return LAppInst->PostEvent(View, m, a, b); } diff --git a/src/win/Lgi/General.cpp b/src/win/Lgi/General.cpp --- a/src/win/Lgi/General.cpp +++ b/src/win/Lgi/General.cpp @@ -1,1159 +1,1159 @@ #define _WIN32_WINNT 0x500 // Win32 Implementation of General LGI functions #include #include #include #include #include "lgi/common/Lgi.h" #include "lgi/common/RegKey.h" #define DEBUG_LOG_WRITES 1 #if DEBUG_LOG_WRITES #define LOG_WRITE(...) LgiTrace(__VA_ARGS__) #else #define LOG_WRITE(...) #endif //////////////////////////////////////////////////////////////// // Implementations void LSleep(DWORD i) { ::Sleep(i); } LString LCurrentUserName() { TCHAR username[256]; DWORD username_len = sizeof(username); GetUserName(username, &username_len); return username; } bool LGetMimeTypeExtensions(const char *Mime, LArray &Ext) { auto Start = Ext.Length(); char *e; LRegKey t(false, "HKEY_LOCAL_MACHINE\\SOFTWARE\\Classes\\MIME\\Database\\Content Type\\%s", Mime); if (t.IsOk() && (e = t.GetStr("Extension"))) { if (*e == '.') e++; Ext.Add(e); } else { #define HardCodeExtention(mime, Ext1, Ext2) \ else if (!stricmp(Mime, mime)) \ { if (Ext1) Ext.Add(Ext1); \ if (Ext2) Ext.Add(Ext2); } const char *Null = NULL; if (!Mime); HardCodeExtention("text/calendar", "ics", Null) HardCodeExtention("text/x-vcard", "vcf", Null) HardCodeExtention("text/mbox", "mbx", "mbox") HardCodeExtention("text/html", "html", Null) HardCodeExtention("text/plain", "txt", Null) HardCodeExtention("message/rfc822", "eml", Null) HardCodeExtention("audio/mpeg", "mp3", Null) HardCodeExtention("application/msword", "doc", Null) HardCodeExtention("application/pdf", "pdf", Null) } return Ext.Length() > Start; } LString LGetFileMimeType(const char *File) { if (File) { char *Dot = strrchr((char*)File, '.'); if (Dot) { bool AssertOnError = LRegKey::AssertOnError; LRegKey::AssertOnError = false; LRegKey Key(false, "HKEY_CLASSES_ROOT\\%s", Dot); if (Key.IsOk()) { char *Ct = Key.GetStr("Content Type"); if (Ct && !stricmp(Dot, ".dsw") == 0 && !stricmp(Dot, ".dsp") == 0) { return Ct; } else { // Search mime type DB. LRegKey Db(false, "HKEY_LOCAL_MACHINE\\SOFTWARE\\Classes\\MIME\\Database\\Content Type"); List Sub; Db.GetKeyNames(Sub); for (auto k: Sub) { LRegKey Type(false, "HKEY_LOCAL_MACHINE\\SOFTWARE\\Classes\\MIME\\Database\\Content Type\\%s", k); char *Ext = Type.GetStr("Extension"); if (Ext && stricmp(Ext, Dot) == 0) { return k; } } Sub.DeleteArrays(); // This is a hack to get around file types without a MIME database entry // but do have a .ext entry. LGetAppsForMimeType knows about the hack too. LString MimeType; MimeType.Printf("application/%s", Dot); return MimeType; } } LRegKey::AssertOnError = AssertOnError; } // no extension? // no registry entry for file type? return "application/octet-stream"; } return LString(); } bool _GetApps_Add(LArray &Apps, char *In) { LAutoString Path; if (!In) return false; while (*In && strchr(WhiteSpace, *In)) In++; if (!*In) return false; for (char *i = In; true; i++) { if (*i == '\'' || *i == '\"') { char delim = *i++; char *end = strchr(i, delim); if (!end) end = i + strlen(i); Path.Reset(NewStr(i, end-i)); In = end + (*end != 0); break; } else if (!*i || strchr(WhiteSpace, *i)) { char old = *i; *i = 0; if (LFileExists(In)) { Path.Reset(NewStr(In)); } *i = old; if (Path) { In = i + (*i != 0); break; } } if (!*i) break; } if (Path) { LStringPipe p; char *RootVar = "%SystemRoot%"; char *SysRoot = stristr(Path, RootVar); if (SysRoot) { // correct path for variables TCHAR Temp[256]; UINT Ch = GetWindowsDirectory(Temp, CountOf(Temp)); LString Tmp = Temp; if (Tmp(-1) != DIR_CHAR) Tmp += DIR_STR; p.Push(Tmp); char *End = SysRoot + strlen(RootVar); p.Push(*End == DIR_CHAR ? End + 1 : End); } else { p.Push(Path); } auto &a = Apps.New(); a.Params = LString(In).Strip(); - a.Path = p.NewGStr(); + a.Path = p.NewLStr(); if (a.Path) { char e[MAX_PATH_LEN]; char *d = strrchr(a.Path, DIR_CHAR); if (d) strcpy_s(e, sizeof(e), d + 1); else strcpy_s(e, sizeof(e), a.Path); d = strchr(e, '.'); if (d) *d = 0; e[0] = toupper(e[0]); a.Name = e; if (ValidStr(a.Name)) { bool AllCaps = true; for (char *s=a.Name; *s; s++) { if (islower(*s)) { AllCaps = false; break; } } if (AllCaps) { Strlwr(a.Name.Get() + 1); } } } return true; } return false; } bool LGetAppsForMimeType(const char *Mime, LArray &Apps, int Limit) { bool Status = false; if (Mime) { if (stricmp(Mime, "application/email") == 0) { // get email app LRegKey Key(false, "HKEY_CLASSES_ROOT\\mailto\\shell\\open\\command"); if (Key.IsOk()) { // get app path char *Str = Key.GetStr(); // if (RegQueryValueEx(hKey, 0, 0, &Type, (uchar*)Str, &StrLen) == ERROR_SUCCESS) if (Str) { Status = _GetApps_Add(Apps, Str); } } } else if (!stricmp(Mime, "application/browser")) { // get default browser char *Keys[] = { "HKCU", "HKLM" }; char Base[] = "SOFTWARE\\Clients\\StartMenuInternet"; for (int i=0; i Keys; if (Shell.GetKeyNames(Keys)) { LRegKey First(false, "HKEY_CLASSES_ROOT\\Applications\\%s\\shell\\%s\\command", Application, Keys[0]); char *Path; if (Path = First.GetStr()) { Status |= _GetApps_Add(Apps, Path); } } Keys.DeleteArrays(); } } DeleteArray(Mru); } if (!Status) { // Explorers file extensions LRegKey FileExt(false, "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\%s", Ext); char *Application; if (Application = FileExt.GetStr("Application")) { LRegKey App(false, "HKEY_CLASSES_ROOT\\Applications\\%s\\shell\\open\\command", Application); char *Path; if (Path = App.GetStr()) { Status = _GetApps_Add(Apps, Path); } } } if (!Status) { // get classes location LRegKey ExtEntry(false, "HKEY_CLASSES_ROOT\\%s", Ext); LRegKey TypeEntry(false, "HKEY_CLASSES_ROOT\\%s\\shell\\open\\command", ExtEntry.GetStr()); char *Path = TypeEntry.GetStr(); if (Path) { const char *c = Path; char *Part = LTokStr(c); if (Part) { char AppPath[256]; _snprintf_s(AppPath, sizeof(AppPath), "\"%s\"", Part); Status = _GetApps_Add(Apps, AppPath); DeleteArray(Part); } else { Status = _GetApps_Add(Apps, Path); } } } } } } } return Status; } LString LGetAppForMimeType(const char *Mime) { LString App; LArray Apps; if (LGetAppsForMimeType(Mime, Apps, 1)) App = Apps[0].Path.Get(); return App; } int LRand(int i) { return (rand() % i); } bool LPlaySound(const char *FileName, int Flags) { bool Status = false; HMODULE hDll = LoadLibrary(_T("winmm.dll")); if (hDll) { typedef BOOL (__stdcall *Proc_sndPlaySound)(LPCSTR pszSound, UINT fuSound); Proc_sndPlaySound psndPlaySound = (Proc_sndPlaySound)GetProcAddress(hDll, "sndPlaySoundA"); if (psndPlaySound) { if (LGetOs() == LGI_OS_WIN9X) { // async broken on 98? Flags = 0; } Status = psndPlaySound(FileName, Flags) != 0; } FreeLibrary(hDll); } return Status; } #include LString LErrorCodeToString(uint32_t ErrorCode) { LString Str; HMODULE hModule = NULL; LPSTR MessageBuffer = NULL; DWORD dwBufferLength; DWORD dwFormatFlags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_FROM_SYSTEM ; if (ErrorCode >= NERR_BASE && ErrorCode <= MAX_NERR) { hModule = LoadLibraryEx( TEXT("netmsg.dll"), NULL, LOAD_LIBRARY_AS_DATAFILE); if (hModule != NULL) dwFormatFlags |= FORMAT_MESSAGE_FROM_HMODULE; } if (dwBufferLength = FormatMessageA(dwFormatFlags, hModule, // module to get message from (NULL == system) ErrorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // default language (LPSTR) &MessageBuffer, 0, NULL)) { Str.Set(MessageBuffer, dwBufferLength); LocalFree(MessageBuffer); } if (hModule != NULL) FreeLibrary(hModule); return Str; } bool LExecute(const char *File, const char *Arguments, const char *Dir, LString *ErrorMsg) { int Error = 0; HINSTANCE Status = NULL; if (!File) return false; uint64 Now = LCurrentTime(); if (LGetOs() == LGI_OS_WIN9X) { auto f = LToNativeCp(File); auto a = LToNativeCp(Arguments); auto d = LToNativeCp(Dir); if (f) { Status = ShellExecuteA(NULL, "open", f, a, d, 5); if ((size_t)Status <= 32) Error = GetLastError(); } } else { LAutoWString f(Utf8ToWide(File)); LAutoWString a(Utf8ToWide(Arguments)); LAutoWString d(Utf8ToWide(Dir)); if (f) { Status = ShellExecuteW(NULL, L"open", f, a, d, 5); if ((size_t)Status <= 32) Error = GetLastError(); } } #ifdef _DEBUG if ((size_t)Status <= 32) LgiTrace("ShellExecuteW failed with %p (LastErr=0x%x)\n", Status, Error); if (LCurrentTime() - Now > 1000) LgiTrace("ShellExecuteW took %I64i\n", LCurrentTime() - Now); #endif if (ErrorMsg) *ErrorMsg = LErrorCodeToString(Error); return (size_t)Status > 32; } //////////////////////////////////////////////////////////////////////////////////// HKEY GetRootKey(char *s) { HKEY Root = 0; #define TestKey(Name) \ if (strncmp(s, #Name, strlen(#Name)) == 0) \ { \ Root = Name; \ } TestKey(HKEY_CLASSES_ROOT) else TestKey(HKEY_CURRENT_CONFIG) else TestKey(HKEY_CURRENT_USER) else TestKey(HKEY_LOCAL_MACHINE) else TestKey(HKEY_USERS) #undef TestKey return Root; } bool LRegKey::AssertOnError = true; LRegKey::LRegKey(bool WriteAccess, char *Key, ...) { char Buffer[1025]; k = 0; s[0] = 0; Root = (HKEY)-1; va_list Arg; va_start(Arg, Key); vsprintf_s(Buffer, Key, Arg); va_end(Arg); KeyName = Buffer; if (KeyName) { size_t Len = 0; char *SubKey = 0; #define TestKey(Long, Short) \ if (!strnicmp(KeyName, #Long, Len = strlen(#Long)) || \ !strnicmp(KeyName, #Short, Len = strlen(#Short))) \ { \ Root = Long; \ SubKey = KeyName.Get()[Len] ? KeyName.Get() + Len + 1 : 0; \ } TestKey(HKEY_CLASSES_ROOT, HKCR) else TestKey(HKEY_CURRENT_CONFIG, HKCC) else TestKey(HKEY_CURRENT_USER, HKCU) else TestKey(HKEY_LOCAL_MACHINE, HKLM) else TestKey(HKEY_USERS, HKU) else return; LONG ret = RegOpenKeyExA(Root, SubKey, 0, WriteAccess ? KEY_ALL_ACCESS : KEY_READ, &k); if (ret != ERROR_SUCCESS && ret != ERROR_FILE_NOT_FOUND) { DWORD err = GetLastError(); if (AssertOnError) LAssert(!"RegOpenKeyEx failed"); } } } LRegKey::~LRegKey() { if (k) RegCloseKey(k); } bool LRegKey::IsOk() { return k != NULL; } bool LRegKey::Create() { bool Status = false; if (!k && KeyName) { char *Sub = strchr(KeyName, '\\'); if (Sub) { LONG Ret = RegCreateKeyA(Root, Sub+1, &k); if (Ret == ERROR_SUCCESS) { Status = IsOk(); } else { DWORD err = GetLastError(); if (AssertOnError) LAssert(!"RegCreateKey failed"); } } } return Status; } char *LRegKey::Name() { return KeyName; } bool LRegKey::DeleteValue(char *Name) { if (k) { if (RegDeleteValueA(k, Name) == ERROR_SUCCESS) { return true; } else { DWORD Err = GetLastError(); LAssert(!"RegDeleteValue failed"); } } return false; } bool LRegKey::DeleteKey() { bool Status = false; if (k) { char *n = strchr(KeyName, '\\'); if (n++) { RegCloseKey(k); k = 0; HKEY Root = GetRootKey(KeyName); int Ret = RegDeleteKeyA(Root, n); Status = Ret == ERROR_SUCCESS; if (!Status) { if (AssertOnError) LAssert(!"RegDeleteKey failed."); } KeyName.Empty(); } } return false; } char *LRegKey::GetStr(const char *Name) { if (!k) { LAssert(!"No key to read from."); return NULL; } DWORD Size = sizeof(s), Type; LONG Ret = RegQueryValueExA(k, Name, 0, &Type, (uchar*)s, &Size); if (Ret != ERROR_SUCCESS) { if (AssertOnError) LAssert(!"RegQueryValueEx failed."); return NULL; } return s; } bool LRegKey::GetStr(const char *Name, LString &Str) { if (!k) { if (AssertOnError) LAssert(!"No key to read from."); return false; } DWORD Size = 0, Type; LONG Ret = RegQueryValueExA(k, Name, 0, &Type, NULL, &Size); if (Ret != ERROR_SUCCESS) goto OnError; { LString Tmp((char*)NULL, Size); Ret = RegQueryValueExA(k, Name, 0, &Type, (LPBYTE)Tmp.Get(), &Size); if (Ret != ERROR_SUCCESS) goto OnError; Str = Tmp; return true; } OnError: if (AssertOnError) LAssert(!"RegQueryValueEx failed."); return false; } bool LRegKey::SetStr(const char *Name, const char *Value) { if (!k) { LAssert(!"No key open."); return false; } auto r = RegSetValueExA(k, Name, 0, REG_SZ, (uchar*)Value, Value ? (DWORD)strlen(Value) : 0); LOG_WRITE("RegSetValueExA(%s,%s,'%s')=%i\n", KeyName.Get(), Name, Value, r); if (r != ERROR_SUCCESS) { if (AssertOnError) LAssert(!"RegSetValueEx failed."); return false; } return true; } bool LRegKey::GetInt(const char *Name, uint32_t &Value) { if (!k) return false; DWORD Size = sizeof(Value), Type; LSTATUS r = RegQueryValueExA(k, Name, 0, &Type, (uchar*)&Value, &Size); return r == ERROR_SUCCESS; } bool LRegKey::SetInt(const char *Name, uint32_t Value) { if (!k) { LgiTrace("%s:%i - No key name.\n", _FL); return false; } auto r = RegSetValueExA(k, Name, 0, REG_DWORD, (uchar*)&Value, sizeof(Value)); LOG_WRITE("RegSetValueExA(%s,%s,%i)=%i\n", KeyName.Get(), Name, Value, r); if (r == ERROR_SUCCESS) return true; LgiTrace("%s:%i - RegSetValueExA(%s) returned error: %x.\n", _FL, Name, r); return false; } bool LRegKey::GetBinary(char *Name, void *&Ptr, int &Len) { DWORD Size = 0, Type; if (k && RegQueryValueExA(k, Name, 0, &Type, 0, &Size) == ERROR_SUCCESS) { Len = Size; Ptr = new uchar[Len]; return RegQueryValueExA(k, Name, 0, &Type, (uchar*)Ptr, &Size) == ERROR_SUCCESS; } return false; } bool LRegKey::SetBinary(char *Name, void *Ptr, int Len) { LAssert(!"Not impl."); return false; } bool LRegKey::GetKeyNames(List &n) { FILETIME t; TCHAR Buf[256]; DWORD Size = CountOf(Buf), i = 0; LSTATUS Status; while ((Status = RegEnumKeyEx(k, i++, Buf, &Size, 0, 0, 0, &t)) == ERROR_SUCCESS) { n.Insert(WideToUtf8(Buf)); Size = sizeof(Buf); } return n.Length() > 0; } bool LRegKey::GetValueNames(List &n) { TCHAR Buf[256]; DWORD Type, Size = CountOf(Buf), i = 0; while (RegEnumValue(k, i++, Buf, &Size, 0, &Type, 0, 0) == ERROR_SUCCESS) { n.Insert(WideToUtf8(Buf)); Size = sizeof(Buf); } return n.Length() > 0; } ////////////////////////////////////////////////////////////////////////////////////// LString WinGetSpecialFolderPath(int Id) { LLibrary Shell("Shell32"); LString s; char16 wp[MAX_PATH_LEN] = { 0 }; pSHGetSpecialFolderPathW w = (pSHGetSpecialFolderPathW) Shell.GetAddress("SHGetSpecialFolderPathW"); if (w) { BOOL result = w(0, wp, Id, false); if (result && ValidStrW(wp)) { LAutoString Tmp(WideToUtf8(wp)); s = Tmp; } else { DWORD e = GetLastError(); LAssert(!"Error getting system folder."); } } return s; } ////////////////////////////////////////////////////////////////////// #ifndef LGI_STATIC void LAssertDlg(LString Msg, std::function Callback) { auto a = new LAlert(LAppInst ? LAppInst->AppWnd : NULL, "Assert Failed", Msg, "Abort", "Debug", "Ignore"); a->SetAppModal(); a->DoModal([Callback](auto d, auto code) { if (Callback) Callback(code); delete d; }); } #endif void _lgi_assert(bool b, const char *test, const char *file, int line) { static bool Asserting = false; if (!b) { #ifdef LGI_STATIC assert(b); #else if (Asserting || !LAppInst || !LSysFont) { // Woah boy... LgiTrace("%s:%i - Assert: %s failed.\n", file, line, test); } else { Asserting = true; printf("%s:%i - Assert failed:\n%s\n", file, line, test); #ifdef _DEBUG LString Msg; Msg.Printf("Assert failed, file: %s, line: %i\n%s", file, line, test); int Result = 0; if (LAppInst->InThread()) { // We are in the GUI thread, show the dialog inline LAssertDlg(Msg, [](auto Result) { switch (Result) { case 1: { exit(-1); break; } case 2: { // Bring up the debugger... #if defined(_WIN64) || !defined(_MSC_VER) assert(0); #else _asm int 3 #endif break; } default: case 3: { break; } } }); } else { // Fall back to windows UI assert(0); } #endif Asserting = false; } #endif } } ////////////////////////////////////////////////////////////////////// // The following code is from: // Web: http://www.codeproject.com/Articles/28071/Toggle-hardware-data-read-execute-breakpoints-prog // License: http://www.codeproject.com/info/cpol10.aspx struct HWBRK { void *a; HANDLE hT; HWBRK_TYPE Type; HWBRK_SIZE Size; HANDLE hEv; int iReg; int Opr; bool SUCC; HWBRK() { Opr = 0; a = 0; hT = 0; hEv = 0; iReg = 0; SUCC = false; } }; static void SetBits(DWORD_PTR& dw, int lowBit, int bits, int newValue) { DWORD_PTR mask = (1 << bits) - 1; dw = (dw & ~(mask << lowBit)) | (newValue << lowBit); } static DWORD WINAPI BreakpointThread(LPVOID lpParameter) { HWBRK* h = (HWBRK*)lpParameter; int j = 0; int y = 0; j = SuspendThread(h->hT); y = GetLastError(); CONTEXT ct = {0}; ct.ContextFlags = CONTEXT_DEBUG_REGISTERS; j = GetThreadContext(h->hT,&ct); y = GetLastError(); int FlagBit = 0; bool Dr0Busy = false; bool Dr1Busy = false; bool Dr2Busy = false; bool Dr3Busy = false; if (ct.Dr7 & 1) Dr0Busy = true; if (ct.Dr7 & 4) Dr1Busy = true; if (ct.Dr7 & 16) Dr2Busy = true; if (ct.Dr7 & 64) Dr3Busy = true; if (h->Opr == 1) { // Remove if (h->iReg == 0) { FlagBit = 0; ct.Dr0 = 0; Dr0Busy = false; } if (h->iReg == 1) { FlagBit = 2; ct.Dr1 = 0; Dr1Busy = false; } if (h->iReg == 2) { FlagBit = 4; ct.Dr2 = 0; Dr2Busy = false; } if (h->iReg == 3) { FlagBit = 6; ct.Dr3 = 0; Dr3Busy = false; } ct.Dr7 &= ~(1 << FlagBit); } else { if (!Dr0Busy) { h->iReg = 0; ct.Dr0 = (DWORD_PTR)h->a; Dr0Busy = true; } else if (!Dr1Busy) { h->iReg = 1; ct.Dr1 = (DWORD_PTR)h->a; Dr1Busy = true; } else if (!Dr2Busy) { h->iReg = 2; ct.Dr2 = (DWORD_PTR)h->a; Dr2Busy = true; } else if (!Dr3Busy) { h->iReg = 3; ct.Dr3 = (DWORD_PTR)h->a; Dr3Busy = true; } else { h->SUCC = false; j = ResumeThread(h->hT); y = GetLastError(); SetEvent(h->hEv); return 0; } ct.Dr6 = 0; int st = 0; if (h->Type == HWBRK_TYPE_CODE) st = 0; if (h->Type == HWBRK_TYPE_READWRITE) st = 3; if (h->Type == HWBRK_TYPE_WRITE) st = 1; int le = 0; if (h->Size == HWBRK_SIZE_1) le = 0; if (h->Size == HWBRK_SIZE_2) le = 1; if (h->Size == HWBRK_SIZE_4) le = 3; if (h->Size == HWBRK_SIZE_8) le = 2; SetBits(ct.Dr7, 16 + h->iReg*4, 2, st); SetBits(ct.Dr7, 18 + h->iReg*4, 2, le); SetBits(ct.Dr7, h->iReg*2,1,1); } ct.ContextFlags = CONTEXT_DEBUG_REGISTERS; j = SetThreadContext(h->hT,&ct); y = GetLastError(); ct.ContextFlags = CONTEXT_DEBUG_REGISTERS; j = GetThreadContext(h->hT,&ct); y = GetLastError(); j = ResumeThread(h->hT); y = GetLastError(); h->SUCC = true; SetEvent(h->hEv); return 0; } HANDLE SetHardwareBreakpoint(HANDLE hThread, HWBRK_TYPE Type, HWBRK_SIZE Size, void *s) { HWBRK *h = new HWBRK; h->a = s; h->Size = Size; h->Type = Type; h->hT = hThread; if (hThread == GetCurrentThread()) { DWORD pid = GetCurrentThreadId(); h->hT = OpenThread(THREAD_ALL_ACCESS,0,pid); } h->hEv = CreateEvent(0, 0, 0, 0); h->Opr = 0; // Set Break HANDLE hY = CreateThread(0, 0, BreakpointThread, (LPVOID)h, 0, 0); WaitForSingleObject(h->hEv, INFINITE); CloseHandle(h->hEv); h->hEv = 0; if (hThread == GetCurrentThread()) { CloseHandle(h->hT); } h->hT = hThread; if (!h->SUCC) { delete h; return 0; } return (HANDLE)h; } bool RemoveHardwareBreakpoint(HANDLE hBrk) { HWBRK* h = (HWBRK*)hBrk; if (!h) return false; bool C = false; if (h->hT == GetCurrentThread()) { DWORD pid = GetCurrentThreadId(); h->hT = OpenThread(THREAD_ALL_ACCESS,0,pid); C = true; } h->hEv = CreateEvent(0,0,0,0); h->Opr = 1; // Remove Break HANDLE hY = CreateThread(0,0,BreakpointThread,(LPVOID)h,0,0); WaitForSingleObject(h->hEv,INFINITE); CloseHandle(h->hEv); h->hEv = 0; if (C) { CloseHandle(h->hT); } delete h; return true; }