diff --git a/include/lgi/common/RichTextEdit.h b/include/lgi/common/RichTextEdit.h --- a/include/lgi/common/RichTextEdit.h +++ b/include/lgi/common/RichTextEdit.h @@ -1,238 +1,247 @@ /// \file /// \author Matthew Allen /// \brief A unicode text editor #ifndef _RICH_TEXT_EDIT_H_ #define _RICH_TEXT_EDIT_H_ #include "lgi/common/DocView.h" #include "lgi/common/Undo.h" #include "lgi/common/DragAndDrop.h" #include "lgi/common/Capabilities.h" #include "lgi/common/FindReplaceDlg.h" #if _DEBUG #include "lgi/common/Tree.h" #endif enum RichEditMsgs { M_BLOCK_MSG = M_USER + 0x1000, M_IMAGE_LOAD_FILE, M_IMAGE_SET_SURFACE, M_IMAGE_ERROR, M_IMAGE_COMPONENT_MISSING, M_IMAGE_PROGRESS, M_IMAGE_RESAMPLE, M_IMAGE_FINISHED, M_IMAGE_COMPRESS, M_IMAGE_ROTATE, M_IMAGE_FLIP, M_IMAGE_LOAD_STREAM, M_COMPONENT_INSTALLED, // A = LString *ComponentName }; extern char Delimiters[]; /// Styled unicode text editor control. class #if defined(MAC) LgiClass #endif LRichTextEdit : public LDocView, public ResObject, public LDragDropTarget, public LCapabilityClient { friend bool RichText_FindCallback(LFindReplaceCommon *Dlg, bool Replace, void *User); public: enum LTextViewSeek { PrevLine, NextLine, StartLine, EndLine }; protected: class LRichTextPriv *d; friend class LRichTextPriv; bool IndexAt(int x, int y, ssize_t &Off, int &LineHint); // Overridables virtual void PourText(ssize_t Start, ssize_t Length); virtual void PourStyle(ssize_t Start, ssize_t Length); virtual void OnFontChange(); virtual void OnPaintLeftMargin(LSurface *pDC, LRect &r, LColour &colour); public: // Construction LRichTextEdit( int Id, int x = 0, int y = 0, int cx = 100, int cy = 100, LFontType *FontInfo = NULL); ~LRichTextEdit(); const char *GetClass() override { return "LRichTextEdit"; } // Data const char *Name() override; bool Name(const char *s) override; const char16 *NameW() override; bool NameW(const char16 *s) override; int64 Value() override; void Value(int64 i) override; const char *GetMimeType() override { return "text/html"; } int GetSize(); const char *GetCharset() override; void SetCharset(const char *s) override; ssize_t HitTest(int x, int y); bool DeleteSelection(char16 **Cut = NULL); bool SetSpellCheck(class LSpellCheck *sp); bool GetFormattedContent(const char *MimeType, LString &Out, LArray *Media = NULL) override; // Dom bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool SetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; // Font LFont *GetFont() override; void SetFont(LFont *f, bool OwnIt = false) override; void SetFixedWidthFont(bool i) override; // Options void SetTabSize(uint8_t i) override; void SetReadOnly(bool i) override; bool ShowStyleTools(); void ShowStyleTools(bool b); enum RectType { ContentArea, ToolsArea, // CapabilityArea, // CapabilityBtn, FontFamilyBtn, FontSizeBtn, BoldBtn, ItalicBtn, UnderlineBtn, ForegroundColourBtn, BackgroundColourBtn, MakeLinkBtn, RemoveLinkBtn, RemoveStyleBtn, EmojiBtn, HorzRuleBtn, MaxArea }; LRect GetArea(RectType Type); /// Sets the wrapping on the control, use #L_WRAP_NONE or #L_WRAP_REFLOW void SetWrapType(LDocWrapType i) override; // State / Selection void SetCursor(int i, bool Select, bool ForceFullUpdate = false); ssize_t IndexAt(int x, int y) override; bool IsDirty() override; void IsDirty(bool d); bool HasSelection() override; void UnSelectAll() override; void SelectWord(size_t From) override; void SelectAll() override; ssize_t GetCaret(bool Cursor = true) override; bool GetLineColumnAtIndex(LPoint &Pt, ssize_t Index = -1) override; size_t GetLines() override; void GetTextExtent(int &x, int &y) override; char *GetSelection() override; void SetStylePrefix(LString s); bool IsBusy(bool Stop = false); // File IO bool Open(const char *Name, const char *Cs = NULL) override; bool Save(const char *Name, const char *Cs = NULL) override; // Clipboard IO bool Cut() override; bool Copy() override; bool Paste() override; // Undo/Redo void Undo(); void Redo(); bool GetUndoOn(); void SetUndoOn(bool b); // Action UI virtual void DoGoto(std::function Callback); virtual void DoCase(std::function Callback, bool Upper); virtual void DoFind(std::function Callback) override; virtual void DoFindNext(std::function Callback); virtual void DoReplace(std::function Callback) override; // Action Processing bool ClearDirty(bool Ask, const char *FileName = NULL); void UpdateScrollBars(bool Reset = false); int GetLine(); void SetLine(int Line); LDocFindReplaceParams *CreateFindReplaceParams() override; void SetFindReplaceParams(LDocFindReplaceParams *Params) override; void OnAddStyle(const char *MimeType, const char *Styles) override; // Object Events bool OnFind(LFindReplaceCommon *Params); bool OnReplace(LFindReplaceCommon *Params); bool OnMultiLineTab(bool In); void OnSetHidden(int Hidden); void OnPosChange() override; void OnCreate() override; void OnEscape(LKey &K) override; bool OnMouseWheel(double Lines) override; // Capability target stuff // bool NeedsCapability(const char *Name, const char *Param = NULL); // void OnInstall(CapsHash *Caps, bool Status); // void OnCloseInstaller(); // Window Events void OnFocus(bool f) override; void OnMouseClick(LMouse &m) override; void OnMouseMove(LMouse &m) override; bool OnKey(LKey &k) override; void OnPaint(LSurface *pDC) override; LMessage::Result OnEvent(LMessage *Msg) override; int OnNotify(LViewI *Ctrl, LNotification n) override; void OnPulse() override; int OnHitTest(int x, int y) override; bool OnLayout(LViewLayoutInfo &Inf) override; // D'n'd target int WillAccept(LDragFormats &Formats, LPoint Pt, int KeyState) override; int OnDrop(LArray &Data, LPoint Pt, int KeyState) override; // Virtuals bool Insert(size_t At, const char16 *Data, ssize_t Len) override; bool Delete(size_t At, ssize_t Len) override; virtual void OnEnter(LKey &k) override; virtual void OnUrl(char *Url) override; virtual void DoContextMenu(LMouse &m); + + struct ImgParams + { + LPoint Sz; + size_t Bytes = 0; + int JpegQuality = 0; // suggested... + }; + /// \returns true if the image should be resized to be smaller + virtual bool MaxImageFilter(ImgParams ¶ms) { return false; } #if _DEBUG void DumpNodes(LTree *Root); void SelectNode(LString Param); #endif }; #endif diff --git a/lvc/src/VcFolder.cpp b/lvc/src/VcFolder.cpp --- a/lvc/src/VcFolder.cpp +++ b/lvc/src/VcFolder.cpp @@ -1,5190 +1,5193 @@ #include "Lvc.h" #include "lgi/common/Combo.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/Json.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/IniFile.h" #include "resdefs.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) const char *Locations[] = { "/System/Applications/Utilities/Terminal.app", "/Applications/Utilities/Terminal.app", NULL }; for (size_t i=0; Locations[i]; i++) { if (LFileExists(Locations[i])) { LString term; term.Printf("%s/Contents/MacOS/Terminal", Locations[i]); return LExecute(term, 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; 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() { if (d->CurFolder == this) d->CurFolder = NULL; Log.DeleteObjects(); } VersionCtrl VcFolder::GetType() { if (Type == VcNone) Type = d->DetectVcs(this); return Type; } bool VcFolder::IsLocal() { return Uri.IsProtocol("file"); } LString VcFolder::LocalPath() { if (!Uri.IsProtocol("file") || Uri.sPath.IsEmpty()) { LAssert(!"Shouldn't call this if not a file path."); return LString(); } return Uri.LocalPath(); } 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() { auto t = new LXmlTag(OPT_Folder); if (t) Serialize(t, true); return t; } const char *VcFolder::GetVcName() { if (!VcCmd) VcCmd = d->GetVcName(GetType()); return VcCmd; } char VcFolder::GetPathSep() { if (Uri.IsFile()) return DIR_CHAR; return '/'; // FIXME: Assumption is that the remote system is unix based. } 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; } #if HAS_LIBSSH SshConnection::LoggingType Convert(LoggingType t) { switch (t) { case LogNormal: case LogSilo: return SshConnection::LogInfo; case LogDebug: return SshConnection::LogDebug; } return SshConnection::LogNone; } #endif 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()); #if 0//def 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); printf("Tmp='%s'\n", Tmp.Get()); 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 { #if HAS_LIBSSH auto c = d->GetConnection(Uri.ToString()); if (!c) return false; if (!c->Command(this, Exe, Args, Parser, Params, Convert(Logging))) return false; #endif } Update(); return true; } int LogDateCmp(LListItem *a, LListItem *b, NativeInt Data) { auto A = dynamic_cast(a); auto 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) { if (!Hash || !Name) { LAssert(!"Param error"); return; } 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; char *s = l.Get(); while (*s && IsWhite(*s)) s++; bool IsCur = *s == '*'; if (IsCur) s++; while (*s && IsWhite(*s)) s++; if (*s == '(') { s++; auto e = strchr(s, ')'); if (e) { c.New().Set(s, e - s); e++; c += LString(e).SplitDelimit(" \t"); } } else { c = LString(s).SplitDelimit(" \t"); } if (c.Length() < 1) { d->Log->Print("%s:%i - Too few parts in line '%s'\n", _FL, l.Get()); continue; } if (IsCur) SetCurrentBranch(c[0]); AddGitName(c[1], c[0]); Branches.Add(c[0], new VcBranch(c[0], c[1])); } break; } case VcHg: { auto a = s.SplitDelimit("\r\n"); for (auto b: a) { if (b.Find("inactive") > 0) continue; auto name = b(0, 28).Strip(); auto refs = b(28, -1).SplitDelimit()[0].SplitDelimit(":"); auto branch = Branches.Find(name); if (branch) branch->Hash = refs.Last(); else Branches.Add(name, new VcBranch(name, refs.Last())); } if (Params && Params->Str.Equals("CountToTip")) CountToTip(); break; } default: { break; } } IsBranches = Result ? StatusError : StatusNone; OnBranchesChange(); return false; } void VcFolder::GetRemoteUrl(std::function Callback) { LAutoPtr p(new ParseParams); p->Callback = Callback; switch (GetType()) { case VcGit: { StartCmd("config --get remote.origin.url", NULL, p.Release()); break; } case VcSvn: { StartCmd("info --show-item=url", NULL, p.Release()); break; } case VcHg: { StartCmd("paths default", NULL, p.Release()); break; } default: break; } } void VcFolder::SelectCommit(LWindow *Parent, LString Commit, LString Path) { bool requireFullMatch = true; if (GetType() == VcGit) requireFullMatch = false; // This function find the given commit and selects it such that the diffs are displayed in the file list VcCommit *ExistingMatch = NULL; for (auto c: Log) { char *rev = c->GetRev(); bool match = requireFullMatch ? Commit.Equals(rev) : Strstr(rev, Commit.Get()) != NULL; if (match) { ExistingMatch = c; break; } } FileToSelect = Path; if (ExistingMatch) { ExistingMatch->Select(true); } else { // If the commit isn't there, it's likely that the log item limit was reached before the commit was // found. In which case we should go get just that commit and add it: d->Files->Empty(); // Diff just that ref: LString a; switch (GetType()) { case VcGit: { a.Printf("diff %s~ %s", Commit.Get(), Commit.Get()); StartCmd(a, &VcFolder::ParseSelectCommit); break; } case VcHg: { a.Printf("log -p -r %s", Commit.Get()); StartCmd(a, &VcFolder::ParseSelectCommit); break; } default: { NoImplementation(_FL); break; } } // if (Parent) LgiMsg(Parent, "The commit '%s' wasn't found", AppName, MB_OK, Commit.Get()); } } bool VcFolder::ParseSelectCommit(int Result, LString s, ParseParams *Params) { switch (GetType()) { case VcGit: case VcHg: case VcSvn: case VcCvs: { ParseDiff(Result, s, Params); break; } default: { NoImplementation(_FL); break; } } 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++); } } } UpdateBranchUi(); } 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(LTime); Fields.Add(LMessageTxt); break; } case VcGit: { Fields.Add(LGraph); Fields.Add(LRevision); Fields.Add(LBranch); Fields.Add(LAuthor); Fields.Add(LTime); Fields.Add(LMessageTxt); break; } default: { Fields.Add(LGraph); Fields.Add(LRevision); Fields.Add(LAuthor); Fields.Add(LTime); Fields.Add(LMessageTxt); break; } } } } int VcFolder::IndexOfCommitField(CommitField fld) { return (int)Fields.IndexOf(fld); } void VcFolder::UpdateColumns(LList *lst) { if (!lst) lst = d->Commits; lst->EmptyColumns(); for (auto c: Fields) { switch (c) { case LGraph: lst->AddColumn("---", 60); break; case LIndex: lst->AddColumn("Index", 60); break; case LBranch: lst->AddColumn("Branch", 60); break; case LRevision: lst->AddColumn("Revision", 60); break; case LAuthor: lst->AddColumn("Author", 240); break; case LTime: lst->AddColumn("Date", 130); break; case LMessageTxt: lst->AddColumn("Message", 700); 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::UpdateAuthorUi() { if (AuthorLocal) d->Wnd()->SetCtrlName(IDC_AUTHOR, AuthorLocal.ToString()); else if (AuthorGlobal) d->Wnd()->SetCtrlName(IDC_AUTHOR, AuthorGlobal.ToString()); } LString VcFolder::GetConfigFile(bool local) { switch (GetType()) { case VcHg: { - LFile::Path p; - if (local) - { - p = LFile::Path(LocalPath()) / ".hg" / "hgrc"; - } - else + if (Uri.IsFile()) { - p = LFile::Path(LSP_HOME) / ".hgrc"; - if (!p.Exists()) - p = LFile::Path(LSP_HOME) / "mercurial.ini"; + LFile::Path p; + if (local) + { + p = LFile::Path(LocalPath()) / ".hg" / "hgrc"; + } + else + { + p = LFile::Path(LSP_HOME) / ".hgrc"; + if (!p.Exists()) + p = LFile::Path(LSP_HOME) / "mercurial.ini"; + } + + d->Log->Print("%s: %i\n", p.GetFull().Get(), p.Exists()); + if (p.Exists()) + return p.GetFull(); } - - d->Log->Print("%s: %i\n", p.GetFull().Get(), p.Exists()); - if (p.Exists()) - return p.GetFull(); break; } default: { NoImplementation(_FL); return false; } } return LString(); } bool VcFolder::GetAuthor(bool local, std::function callback) { auto scope = local ? "--local" : "--global"; auto target = local ? &AuthorLocal : &AuthorGlobal; switch (GetType()) { case VcGit: { if (target->InProgress) return true; auto params = new ParseParams; params->Callback = [this, callback, target](auto code, auto s) { for (auto ln: s.Strip().SplitDelimit("\r\n")) { auto parts = ln.SplitDelimit("=", 1); if (parts.Length() == 2) { if (parts[0].Equals("user.email")) target->email = parts[1]; else if (parts[0].Equals("user.name")) target->name = parts[1]; } } target->InProgress = false; if (callback) callback(target->name, target->email); }; auto args = LString::Fmt("-P config -l %s", scope); target->InProgress = StartCmd(args, NULL, params); break; } case VcHg: { auto config = GetConfigFile(local); if (!config) return false; LIniFile data(config); auto author = data.Get("ui", "username"); auto start = author.Find("<"); auto end = author.Find(">", start); if (start >= 0 && end >= start) { target->name = author(0, start).Strip(); target->email = author(start + 1, end).Strip(); } IsGettingAuthor = false; callback(target->name, target->email); break; } default: { NoImplementation(_FL); return false; } } return true; } bool VcFolder::SetAuthor(bool local, LString name, LString email) { auto scope = local ? "--local" : "--global"; auto target = local ? &AuthorLocal : &AuthorGlobal; target->name = name; target->email = email; switch (GetType()) { case VcGit: { auto args = LString::Fmt("config %s user.name \"%s\"", scope, name.Get()); StartCmd(args); args = LString::Fmt("config %s user.email \"%s\"", scope, email.Get()); StartCmd(args); break; } case VcHg: { auto config = GetConfigFile(local); if (!config) return false; LString author; author.Printf("%s <%s>", name.Get(), email.Get()); LIniFile data(config); data.Set("ui", "username", author); return data.Write(); } default: { NoImplementation(_FL); return false; } } return true; } 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(); if (!AuthorLocal) GetAuthor(true, [this](auto name, auto email) { UpdateAuthorUi(); }); if (!AuthorGlobal) GetAuthor(false, [this](auto name, auto email) { UpdateAuthorUi(); }); UpdateAuthorUi(); 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; } } LList *CurOwner = l->GetList(); if (!CurOwner) Ls.Insert(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) { LTimeStamp ats, bts; (*a)->GetTs().Get(ats); (*b)->GetTs().Get(bts); int64 diff = (int64)bts.Get() - ats.Get(); 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 -v", &VcFolder::ParseBranches, Params)) IsBranches = StatusActive; break; case VcSvn: Branches.Add("trunk", new VcBranch("trunk")); OnBranchesChange(); break; case VcHg: { if (StartCmd("branches", &VcFolder::ParseBranches, Params)) IsBranches = StatusActive; auto p = new ParseParams; p->Callback = [this](auto code, auto str) { SetCurrentBranch(str.Strip()); }; StartCmd("branch", NULL, p); 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::ClearLog() { Uncommit.Reset(); Log.DeleteObjects(); } void VcFolder::LogFilter(const char *Filter) { if (!Filter) { LAssert(!"No filter."); return; } switch (GetType()) { case VcGit: { // See if 'Filter' is a commit id? LString args; args.Printf("-P show %s", Filter); ParseParams *params = new ParseParams; params->Callback = [this, Filter=LString(Filter)](auto code, auto str) { ClearLog(); if (code == 0 && str.Find(Filter) >= 0) { // Found the commit... d->Commits->Empty(); CurrentCommit.Empty(); ParseLog(code, str, NULL); d->Commits->Insert(Log); } else { // Not a commit ref...? LString args; args.Printf("log --grep \"%s\"", Filter.Get()); IsLogging = StartCmd(args, &VcFolder::ParseLog); } }; StartCmd(args, NULL, params); break; } default: { NoImplementation(_FL); break; } } } void VcFolder::LogFile(const char *uri) { LString Args; if (IsLogging) { d->Log->Print("%s:%i - already logging.\n", _FL); return; } const char *Page = ""; switch (GetType()) { case VcGit: Page = "-P "; // fall through case VcSvn: case VcHg: { FileToSelect = GetFilePart(uri); if (IsLocal() && !LFileExists(FileToSelect)) { LFile::Path Abs(LocalPath()); Abs += FileToSelect; if (Abs.Exists()) FileToSelect = Abs; } ParseParams *Params = new ParseParams(uri); Args.Printf("%slog \"%s\"", Page, FileToSelect.Get()); IsLogging = StartCmd(Args, &VcFolder::ParseLog, Params, LogNormal); break; } default: NoImplementation(_FL); 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) { int Skipped = 0, Errors = 0; bool LoggingFile = Params ? Params->Str != NULL : false; VcLeaf *File = LoggingFile ? FindLeaf(Params->Str, true) : NULL; // This may be NULL even if we are logging a file... LArray *Out, BrowseLog; if (File) Out = &File->Log; else if (LoggingFile) Out = &BrowseLog; else Out = &Log; LHashTbl, VcCommit*> Map; for (auto pc: *Out) Map.Add(pc->GetRev(), pc); 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(); #if 0 LFile::Path outPath("~/code/dump.txt"); LFile out(outPath.Absolute(), O_WRITE); out.Write(s); #endif if (!s) { OnCmdError(s, "No output from command."); return false; } char *i = s.Get(); while (*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; } } if (prev && i > prev) { // Last one... c.New().Set(prev, i - prev); } for (auto txt: c) { LAutoPtr Rev(new VcCommit(d, this)); if (Rev->GitParse(txt, false)) { if (!Map.Find(Rev->GetRev())) Out->Add(Rev.Release()); else Skipped++; } else { LgiTrace("%s:%i - Failed:\n%s\n\n", _FL, txt.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)) { LTimeStamp Ts; if (Dt.Get(Ts)) { VcCommit *Cc = Map.Find(Ts.Get()); if (!Cc) { Map.Add(Ts.Get(), 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(); } else if (LoggingFile) { if (auto ui = new BrowseUi(BrowseUi::TLog, d, this, Params->Str)) ui->ParseLog(BrowseLog, s); } // 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--; } } } } void VcFolder::UpdateBranchUi() { auto w = d->Wnd(); 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); } } LCombo *Cbo; if (w->GetViewById(IDC_BRANCHES, Cbo)) { Cbo->Empty(); int64 select = -1; for (auto b: Branches) { if (CurrentBranch && CurrentBranch == b.key) select = Cbo->Length(); Cbo->Insert(b.key); } if (select >= 0) Cbo->Value(select); Cbo->SendNotify(LNotifyTableLayoutRefresh); // LgiTrace("%s:%i - Branches len=%i->%i\n", _FL, (int)Branches.Length(), (int)Cbo->Length()); } } 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::NoImplementation(const char* file, int line) { LString s; s.Printf("%s, uri=%s, type=%s (%s:%i)", LLoadString(IDS_ERR_NO_IMPL_FOR_TYPE), Uri.ToString().Get(), toString(GetType()), file, line); OnCmdError(LString(), s); } 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); Update(); } 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; iStr.Equals("Branch")) SetCurrentBranch(NewRev); else CurrentCommit = NewRev; } NewRev.Empty(); IsUpdate = false; return true; } bool VcFolder::ParseWorking(int Result, LString s, ParseParams *Params) { IsListingWorking = false; switch (GetType()) { case VcSvn: case VcHg: { ParseParams Local; if (!Params) Params = &Local; Params->IsWorking = 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, LString(), 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, LString(), true); break; } } 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, LString(), 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; } } void VcFolder::InsertFiles(List &files) { d->Files->Insert(files); if (FileToSelect) { LListItem *scroll = NULL; for (auto f: files) { // Convert to an absolute path: bool match = false; auto relPath = f->GetText(COL_FILENAME); if (IsLocal()) { LFile::Path p(LocalPath()); p += relPath; match = p.GetFull().Equals(FileToSelect); } else { match = !Stricmp(FileToSelect.Get(), relPath); } f->Select(match); if (match) scroll = f; } if (scroll) scroll->ScrollTo(); } } bool VcFolder::ParseDiffs(LString s, LString Rev, bool IsWorking) { LAssert(IsWorking || Rev.Get() != NULL); switch (GetType()) { case VcGit: { List Files; 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(); Files.Insert(f); } else if (!_strnicmp(Ln, "new file", 8)) { if (f) f->SetText("A", COL_STATE); } else if (!_strnicmp(Ln, "deleted file", 12)) { if (f) f->SetText("D", 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(); } InsertFiles(Files); 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(); } InsertFiles(Files); break; } case VcSvn: { List Files; 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(); 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]; } } } InsertFiles(Files); 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; } #if HAS_LIBSSH 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); } } if (p->Params && p->Params->Callback) { p->Params->Callback(Result, s); } } #endif 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()); if (c->Rd->GetState() == LThread::THREAD_INIT) { if (CmdActiveThreads < CmdMaxThreads) { c->Rd->Run(); CmdActiveThreads++; // printf("CmdActiveThreads++ = %i\n", CmdActiveThreads); } // else printf("Too many active threads.\n"); } else if (c->Rd->IsExited()) { CmdActiveThreads--; // printf("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("LSUBPROCESS_ERROR\n")) { OnCmdError(s, "Sub process failed."); } else { Reselect |= CALL_MEMBER_FN(*this, c->PostOp)(Result, s, c->Params); } } if (c->Params && c->Params->Callback) { c->Params->Callback(Result, s); } 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)) printf("%s:%i - Wrong tag: %s, %s\n", _FL, c->GetTag(), OPT_Folder); else if (!c->GetContent()) printf("%s:%i - No content.\n", _FL); else { 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; 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); s.AppendItem("Remote URL", IDM_REMOTE_URL); if (!Uri.IsFile()) { s.AppendSeparator(); s.AppendItem("Edit Location", IDM_EDIT); } m -= GetTree()->ScrollPxPos(); auto Cmd = s.Float(GetTree(), m); switch (Cmd) { case IDM_BROWSE_FOLDER: { LBrowseToFile(LocalPath()); break; } case IDM_TERMINAL: { auto p = LocalPath(); TerminalAt(p); 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; } case IDM_REMOTE_URL: { GetRemoteUrl([this](auto code, auto str) { LString Url = str.Strip(); if (Url) { auto a = new LAlert(GetTree(), "Remote Url", Url, "Copy", "Ok"); a->DoModal([this, Url](auto dlg, auto code) { if (code == 1) { LClipBoard c(GetTree()); c.Text(Url); } delete dlg; }); } }); break; } default: break; } } } LString &VcFolder::GetCurrentBranch() { return CurrentBranch; } void VcFolder::SetCurrentBranch(LString name) { if (CurrentBranch != name) { CurrentBranch = name; UpdateBranchUi(); } } void VcFolder::Checkout(const char *Rev, bool isBranch) { if (!Rev || IsUpdate) return; LString Args; LAutoPtr params(new ParseParams(isBranch ? "Branch" : "Rev")); NewRev = Rev; switch (GetType()) { case VcGit: Args.Printf("checkout %s", Rev); IsUpdate = StartCmd(Args, &VcFolder::ParseCheckout, params.Release(), LogNormal); break; case VcSvn: Args.Printf("up -r %s", Rev); IsUpdate = StartCmd(Args, &VcFolder::ParseCheckout, params.Release(), LogNormal); break; case VcHg: Args.Printf("update -r %s", Rev); IsUpdate = StartCmd(Args, &VcFolder::ParseCheckout, params.Release(), LogNormal); break; default: { NoImplementation(_FL); break; } } } bool VcFolder::ParseDelete(int Result, LString s, ParseParams *Params) { switch (GetType()) { case VcHg: { break; } } return true; } void VcFolder::Delete(const char *Path, bool KeepLocal) { switch (GetType()) { case VcHg: { LString args; if (KeepLocal) args.Printf("forget \"%s\"", Path); else args.Printf("remove \"%s\"", Path); StartCmd(args, &VcFolder::ParseDelete); break; } default: { NoImplementation(_FL); 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()); } } #if HAS_LIBSSH 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, SshConnection::LogNone); return; } #endif Parent->Sort(FolderCompare); } void VcFolder::OnVcsType(LString errorMsg) { if (!d) { LAssert(!"No priv instance"); return; } #if HAS_LIBSSH auto c = d->GetConnection(Uri.ToString(), false); if (c) { auto NewType = c->Types.Find(Uri.sPath); if (NewType && NewType != Type) { if (NewType == VcError) { OnCmdError(LString(), errorMsg); } else { Type = NewType; ClearError(); Update(); if (LTreeItem::Select()) Select(true); for (auto &e: OnVcsTypeEvents) e(); OnVcsTypeEvents.Empty(); } } } #endif } 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^..%s", c->GetRev(), 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 auto a = s.Split("==================================================================="); for (auto i : a) { auto Lines = i.SplitDelimit("\r\n"); if (Lines.Length() == 0) continue; auto f = Lines[0].Strip(); if (f.Find("File:") == 0) { auto Parts = f.SplitDelimit("\t"); auto File = Parts[0].Split(": ").Last().Strip(); auto Status = Parts[1].Split(": ").Last(); LString WorkingRev; for (auto l : Lines) { auto p = l.Strip().Split(":", 1); if (p.Length() > 1 && p[0].Strip().Equals("Working revision")) { WorkingRev = p[1].Strip(); } } auto 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, LString(), 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: { auto Lines = s.SplitDelimit("\r\n"); int Fmt = ToolVersion[VcGit] >= Ver2Int("2.8.0") ? 2 : 1; for (auto Ln : Lines) { auto 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 { auto path = p[6]; f = new VcFile(d, this, path, IsWorking); auto state = p[1].Strip("."); auto pos = p[1].Find(state); d->Log->Print("%s state='%s' pos=%i\n", path.Get(), state.Get(), (int)pos); f->SetText(state, COL_STATE); f->SetText(p.Last(), COL_FILENAME); f->SetStaged(pos == 0); } } else if (Fmt == 1) { LString::Array p = Ln.SplitDelimit(" "); f = new VcFile(d, this, LString(), 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, LString(), 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, LString(), 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, LString(), 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 --recursive"; 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 (IsListingWorking) return; d->ClearFiles(); bool Untracked = d->IsMenuChecked(IDM_UNTRACKED); LString Arg; switch (GetType()) { case VcPending: OnVcsTypeEvents.Add([this]() { ListWorkingFolder(); }); break; case VcCvs: if (Untracked) Arg = "-qn update"; else Arg = "-q diff --brief"; break; case VcSvn: Arg = "status"; break; case VcGit: #if 1 Arg = "-P status -vv"; #else Arg = "-P diff --diff-filter=CMRTU --cached"; #endif break; case VcHg: Arg = "status -mard"; break; default: return; } IsListingWorking = 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 { char NativeSep[] = {GetPathSep(), 0}; LString Last = PostAdd->Files.Last(); Args.Printf("add \"%s\"", Last.Replace("\"", "\\\"").Replace("/", NativeSep).Get()); PostAdd->Files.PopLast(); StartCmd(Args, &VcFolder::ParseGitAdd, NULL, LogNormal); } } bool VcFolder::ParseGitAdd(int Result, LString s, ParseParams *Params) { if (Result) { OnCmdError(s, "add failed."); } else { 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(LString(), "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(LString(), "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: { NoImplementation(_FL); 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: { LString args; if (NewBranchOk) { if (CurrentBranch) { args.Printf("push --set-upstream origin %s", CurrentBranch.Get()); } else { OnCmdError(LString(), "Don't have the current branch?"); return; } } else { args = "push"; } Working = StartCmd(args, &VcFolder::ParsePush, NULL, LogNormal); break; } case VcSvn: { // Nothing to do here.. the commit pushed the data already break; } default: { OnCmdError(LString(), "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) { bool needsNewBranchPerm = false; switch (GetType()) { case VcHg: { needsNewBranchPerm = s.Find("push creates new remote branches") >= 0; break; } case VcGit: { needsNewBranchPerm = s.Find("The current branch") >= 0 && s.Find("has no upstream branch") >= 0; break; } } if (needsNewBranchPerm && LgiMsg(GetTree(), LLoadString(IDS_CREATE_NEW_BRANCH), 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; case VcPending: OnVcsTypeEvents.New() = [this, AndUpdate, Logging]() { Pull(AndUpdate, Logging); }; break; default: NoImplementation(_FL); 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(), LLoadString(IDS_ERR_NO_IMPL_FOR_TYPE), 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, LLoadString(IDS_ERR_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(), LLoadString(IDS_ERR_NO_IMPL_FOR_TYPE), 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(), LLoadString(IDS_ERR_NO_IMPL_FOR_TYPE), AppName); break; } } return true; } bool VcFolder::ParseAddFile(int Result, LString s, ParseParams *Params) { switch (GetType()) { case VcCvs: { if (Result) { d->Tabs->Value(1); OnCmdError(s, LLoadString(IDS_ERR_ADD_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: { NoImplementation(_FL); 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, LLoadString(IDS_ERR_REVERT_FAILED)); } ListWorkingFolder(); return false; } bool VcFolder::Revert(LString::Array &Uris, const char *Revision) { if (Uris.Length() == 0) return false; switch (GetType()) { case VcGit: { LStringPipe cmd, paths; LAutoPtr params; if (Revision) { cmd.Print("checkout %s", Revision); } else { // Unstage the file... cmd.Print("reset"); } for (auto u: Uris) { auto Path = GetFilePart(u); paths.Print(" \"%s\"", Path.Get()); } auto p = paths.NewLStr(); cmd.Write(p); if (!Revision) { if (params.Reset(new ParseParams)) { params->Callback = [this, p](auto code, auto str) { LString c; c.Printf("checkout %s", p.Get()); StartCmd(c, &VcFolder::ParseRevert); }; } } return StartCmd(cmd.NewLStr(), &VcFolder::ParseRevert, params.Release()); 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.NewLStr(); return StartCmd(a, &VcFolder::ParseRevert); break; } default: { NoImplementation(_FL); 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, LString(), true); f->SetText(p[0], COL_STATE); f->SetText(p[1], COL_FILENAME); f->GetStatus(); d->Files->Insert(f); } } } break; } default: { NoImplementation(_FL); 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: { NoImplementation(_FL); break; } } return true; } bool VcFolder::Resolve(const char *Path, LvcResolve Type) { if (!Path) return false; switch (GetType()) { case VcGit: { LString a; auto local = GetFilePart(Path); LAutoPtr params(new ParseParams(Path)); switch (Type) { case ResolveIncoming: a.Printf("checkout --theirs \"%s\"", local.Get()); break; case ResolveLocal: a.Printf("checkout --ours \"%s\"", local.Get()); break; case ResolveMark: a.Printf("add \"%s\"", local.Get()); break; default: OnCmdError(Path, "No resolve type implemented."); return false; } if (Type == ResolveIncoming || Type == ResolveLocal) { // Add the file after the resolution: params->Callback = [this, local](auto code, auto str) { LString a; a.Printf("add \"%s\"", local.Get()); StartCmd(a, &VcFolder::ParseAddFile); Refresh(); }; } return StartCmd(a, &VcFolder::ParseResolve, params.Release()); } 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: { NoImplementation(_FL); break; } } return false; } bool BlameLine::Parse(VersionCtrl type, LArray &out, LString in) { auto lines = in.SplitDelimit("\n", -1, false); switch (type) { case VcGit: { for (auto &ln: lines) { auto s = ln.Get(); auto open = ln.Find("("); auto close = ln.Find(")", open); if (open > 0 && close > open) { auto eRef = ln(0, open-1); auto fields = ln(open + 1, close); auto parts = fields.SplitDelimit(); auto &o = out.New(); o.ref = eRef; o.line = parts.Last(); parts.PopLast(); LString::Array name; LDateTime dt; for (auto p: parts) { auto first = p(0); if (IsDigit(first)) { if (p.Find("-") > 0) dt.SetDate(p); else if (p.Find(":") > 0) dt.SetTime(p); } else if (first == '+') dt.SetTimeZone((int)p.Int(), false); else name.Add(p); } o.user = LString(" ").Join(name); o.date = dt.Get(); o.src = ln(close + 1, -1); } else if (ln.Length() > 0) { int asd=0; } } break; } case VcHg: { for (auto &ln: lines) { auto s = ln.Get(); auto eUser = strchr(s, ' '); if (!eUser) continue; auto eRef = strchr(eUser, ':'); if (!eRef) continue; auto &o = out.New(); o.user.Set(s, eUser++ - s); o.ref.Set(eUser, eRef - eUser); o.src = eRef + 1; } break; } /* case VcSvn: { break; } */ default: { LAssert(0); return false; } } return true; } bool VcFolder::ParseBlame(int Result, LString s, ParseParams *Params) { if (!Params) { LAssert(!"Need the path in the params."); return false; } LArray lines; if (BlameLine::Parse(GetType(), lines, s)) { if (auto ui = new BrowseUi(BrowseUi::TBlame, d, this, Params->Str)) ui->ParseBlame(lines, s); } else NoImplementation(_FL); return false; } bool VcFolder::Blame(const char *Path) { if (!Path) return false; auto file = GetFilePart(Path); LAutoPtr Params(new ParseParams(file)); LUri u(Path); switch (GetType()) { case VcGit: { LString a; a.Printf("-P blame \"%s\"", file.Get()); return StartCmd(a, &VcFolder::ParseBlame, Params.Release()); break; } case VcHg: { LString a; a.Printf("annotate -un \"%s\"", file.Get()); return StartCmd(a, &VcFolder::ParseBlame, Params.Release()); break; } case VcSvn: { LString a; a.Printf("blame \"%s\"", file.Get()); return StartCmd(a, &VcFolder::ParseBlame, Params.Release()); break; } default: { NoImplementation(_FL); 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; Item->Insert(this); if (Folder) { Insert(Tmp = new LTreeItem); Tmp->SetText("Loading..."); } } VcLeaf::~VcLeaf() { for (auto l: Log) { if (!l->GetList()) delete l; } } LString VcLeaf::Full() { LUri u = Uri; u += Leaf; return u.ToString(); } void VcLeaf::OnBrowse() { LUri full(Full()); LList *Files = d->Files; Files->Empty(); LDirectory Dir; for (int b = Dir.First(full.LocalPath()); b; b = Dir.Next()) { if (Dir.IsDir()) continue; VcFile *f = new VcFile(d, Parent, LString(), true); if (f) { f->SetUri(LString("file://") + full); f->SetText(Dir.GetName(), COL_FILENAME); Files->Insert(f); } } Files->ResizeColumnsToContent(); if (Folder) Parent->FolderStatus(full.ToString(), 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()) return; d->Commits->RemoveAll(); Parent->DefaultFields(); Parent->UpdateColumns(); for (auto i: Log) // We make a copy of the commit here so that the LList owns the copied object, // and this object still owns 'i'. d->Commits->Insert(new VcCommit(*i), -1, false); d->Commits->UpdateAllItems(); d->Commits->ResizeColumnsToContent(); } 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/src/common/Widgets/Editor/ImageBlock.cpp b/src/common/Widgets/Editor/ImageBlock.cpp --- a/src/common/Widgets/Editor/ImageBlock.cpp +++ b/src/common/Widgets/Editor/ImageBlock.cpp @@ -1,1390 +1,1345 @@ #include "lgi/common/Lgi.h" #include "lgi/common/RichTextEdit.h" #include "lgi/common/GdcTools.h" #include "lgi/common/Menu.h" #include "lgi/common/Net.h" #include "lgi/common/Uri.h" #include "RichTextEditPriv.h" #define LOADER_THREAD_LOGGING 1 +#if LOADER_THREAD_LOGGING +#define LOADER_LOG(...) LgiTrace(__VA_ARGS__) +#else +#define LOADER_LOG(...) ; +#endif + #define TIMEOUT_LOAD_PROGRESS 100 // ms int ImgScales[] = { 15, 25, 50, 75, 100 }; class ImageLoader : public LEventTargetThread, public Progress { LString File; LEventSinkI *Sink = NULL; LSurface *Img = NULL; LAutoPtr Filter; bool SurfaceSent = false; int64 Ts = 0; LAutoPtr In; public: ImageLoader(LEventSinkI *s) : LEventTargetThread("ImageLoader"), Sink(s) { } ~ImageLoader() { Progress::Cancel(true); - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - ~ImageLoader\n", _FL); - #endif + LOADER_LOG("%s:%i - ~ImageLoader\n", _FL); } const char *GetClass() override { return "ImageLoader"; } void Value(int64 v) override { Progress::Value(v); if (!SurfaceSent) { SurfaceSent = true; PostSink(M_IMAGE_SET_SURFACE, (LMessage::Param)Img, (LMessage::Param)In.Release()); } int64 Now = LCurrentTime(); if (Now - Ts > TIMEOUT_LOAD_PROGRESS) { Ts = Now; PostSink(M_IMAGE_PROGRESS, (LMessage::Param)v); } } bool PostSink(int Cmd, LMessage::Param a = 0, LMessage::Param b = 0) { for (int i=0; i<50; i++) { if (Sink->PostEvent(Cmd, a, b)) return true; LSleep(1); } LAssert(!"PostSink failed."); return false; } LMessage::Result OnEvent(LMessage *Msg) override { switch (Msg->Msg()) { case M_IMAGE_LOAD_FILE: { LAutoPtr Str((LString*)Msg->A()); File = *Str; - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Receive(M_IMAGE_LOAD_FILE): '%s'\n", _FL, File.Get()); - #endif + LOADER_LOG("%s:%i - Thread.Receive(M_IMAGE_LOAD_FILE): '%s'\n", _FL, File.Get()); Filter = LFilterFactory::New(File, O_READ, NULL); if (!Filter) { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): no filter\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_ERROR): no filter\n", _FL); return PostSink(M_IMAGE_ERROR); } if (!In.Reset(new LFile) || !In->Open(File, O_READ)) { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): can't read\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_ERROR): can't read\n", _FL); return PostSink(M_IMAGE_ERROR); } if (!(Img = new LMemDC)) { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): alloc err\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_ERROR): alloc err\n", _FL); return PostSink(M_IMAGE_ERROR); } Filter->SetProgress(this); Ts = LCurrentTime(); LFilter::IoStatus Status = Filter->ReadImage(Img, In); if (Status != LFilter::IoSuccess) { if (Status == LFilter::IoComponentMissing) { LString *s = new LString(Filter->GetComponentName()); - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_COMPONENT_MISSING)\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_COMPONENT_MISSING)\n", _FL); return PostSink(M_IMAGE_COMPONENT_MISSING, (LMessage::Param)s); } - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): Filter::ReadImage err\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_ERROR): Filter::ReadImage err\n", _FL); return PostSink(M_IMAGE_ERROR); } if (!SurfaceSent) { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_SET_SURFACE)\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_SET_SURFACE)\n", _FL); PostSink(M_IMAGE_SET_SURFACE, (LMessage::Param)Img, (LMessage::Param)In.Release()); } - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_FINISHED)\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_FINISHED)\n", _FL); PostSink(M_IMAGE_FINISHED); break; } case M_IMAGE_LOAD_STREAM: { LAutoPtr Stream((LStreamI*)Msg->A()); LAutoPtr FileName((LString*)Msg->B()); - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Receive(M_IMAGE_LOAD_STREAM)\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Receive(M_IMAGE_LOAD_STREAM)\n", _FL); if (!Stream) { LAssert(!"No stream."); return PostSink(M_IMAGE_ERROR); } LMemStream *Mem = new LMemStream(Stream, 0, -1); In.Reset(Mem); Filter = LFilterFactory::New(FileName ? *FileName : 0, O_READ, (const uchar*)Mem->GetBasePtr()); if (!Filter) { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): no filter\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_ERROR): no filter\n", _FL); return PostSink(M_IMAGE_ERROR); } if (!(Img = new LMemDC)) { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): alloc err\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_ERROR): alloc err\n", _FL); return PostSink(M_IMAGE_ERROR); } Filter->SetProgress(this); Ts = LCurrentTime(); LFilter::IoStatus Status = Filter->ReadImage(Img, Mem); if (Status != LFilter::IoSuccess) { if (Status == LFilter::IoComponentMissing) { LString *s = new LString(Filter->GetComponentName()); - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_COMPONENT_MISSING)\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_COMPONENT_MISSING)\n", _FL); return PostSink(M_IMAGE_COMPONENT_MISSING, (LMessage::Param)s); } - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): Filter::ReadImage err\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_ERROR): Filter::ReadImage err\n", _FL); return PostSink(M_IMAGE_ERROR); } if (!SurfaceSent) { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_SET_SURFACE)\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_SET_SURFACE)\n", _FL); PostSink(M_IMAGE_SET_SURFACE, (LMessage::Param)Img, (LMessage::Param)In.Release()); } - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_FINISHED)\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_FINISHED)\n", _FL); PostSink(M_IMAGE_FINISHED); break; } case M_IMAGE_RESAMPLE: { - LSurface *Dst = (LSurface*) Msg->A(); - LSurface *Src = (LSurface*) Msg->B(); - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Receive(M_IMAGE_RESAMPLE)\n", _FL); - #endif + auto Dst = (LSurface*) Msg->A(); + auto Src = (LSurface*) Msg->B(); + LOADER_LOG("%s:%i - Thread.Receive(M_IMAGE_RESAMPLE)\n", _FL); if (Src && Dst) { ResampleDC(Dst, Src); if (PostSink(M_IMAGE_RESAMPLE)) - { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_RESAMPLE)\n", _FL); - #endif - } - else LgiTrace("%s:%i - Error sending re-sample msg.\n", _FL); + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_RESAMPLE)\n", _FL); + else + LgiTrace("%s:%i - Error sending re-sample msg.\n", _FL); } else { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): ptr err %p %p\n", _FL, Src, Dst); - #endif + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_ERROR): ptr err %p %p\n", _FL, Src, Dst); return PostSink(M_IMAGE_ERROR); } break; } case M_IMAGE_COMPRESS: { LSurface *img = (LSurface*)Msg->A(); - LRichTextPriv::ImageBlock::ScaleInf *si = (LRichTextPriv::ImageBlock::ScaleInf*)Msg->B(); - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Receive(M_IMAGE_COMPRESS)\n", _FL); - #endif + auto si = (LRichTextPriv::ImageBlock::ScaleInf*)Msg->B(); + LOADER_LOG("%s:%i - Thread.Receive(M_IMAGE_COMPRESS) si: %s\n", _FL, si->ToString().Get()); if (!img || !si) { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): invalid ptr\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_ERROR): invalid ptr\n", _FL); PostSink(M_IMAGE_ERROR, (LMessage::Param) new LString("Invalid pointer.")); break; } auto f = LFilterFactory::New("a.jpg", O_READ, NULL); if (!f) { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): No JPEG filter available\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_ERROR): No JPEG filter available\n", _FL); PostSink(M_IMAGE_ERROR, (LMessage::Param) new LString("No JPEG filter available.")); break; } LAutoPtr scaled; if (img->X() != si->Sz.x || img->Y() != si->Sz.y) { if (!scaled.Reset(new LMemDC(si->Sz.x, si->Sz.y, img->GetColourSpace()))) break; ResampleDC(scaled, img, NULL, NULL); img = scaled; } LXmlTag Props; f->Props = &Props; Props.SetAttr(LGI_FILTER_QUALITY, RICH_TEXT_RESIZED_JPEG_QUALITY); LAutoPtr jpg(new LMemStream(1024)); if (!f->WriteImage(jpg, img)) { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): Image compression failed\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_ERROR): Image compression failed\n", _FL); PostSink(M_IMAGE_ERROR, (LMessage::Param) new LString("Image compression failed.")); break; } - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_COMPRESS)\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_COMPRESS)\n", _FL); PostSink(M_IMAGE_COMPRESS, (LMessage::Param)jpg.Release(), (LMessage::Param)si); break; } case M_IMAGE_ROTATE: { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Receive(M_IMAGE_ROTATE)\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Receive(M_IMAGE_ROTATE)\n", _FL); LSurface *Img = (LSurface*)Msg->A(); if (!Img) { LAssert(!"No image."); break; } RotateDC(Img, Msg->B() == 1 ? 90 : 270); - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_ROTATE)\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_ROTATE)\n", _FL); PostSink(M_IMAGE_ROTATE); break; } case M_IMAGE_FLIP: { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Receive(M_IMAGE_FLIP)\n", _FL); - #endif - LSurface *Img = (LSurface*)Msg->A(); + LOADER_LOG("%s:%i - Thread.Receive(M_IMAGE_FLIP)\n", _FL); + auto Img = (LSurface*)Msg->A(); if (!Img) { LAssert(!"No image."); break; } if (Msg->B() == 1) FlipXDC(Img); else FlipYDC(Img); - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Send(M_IMAGE_FLIP)\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Send(M_IMAGE_FLIP)\n", _FL); PostSink(M_IMAGE_FLIP); break; } case M_CLOSE: { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Thread.Receive(M_CLOSE)\n", _FL); - #endif + LOADER_LOG("%s:%i - Thread.Receive(M_CLOSE)\n", _FL); EndThread(); break; } } return 0; } }; LRichTextPriv::ImageBlock::ImageBlock(LRichTextPriv *priv) : Block(priv) { - ThreadHnd = 0; - IsDeleted = false; - LayoutDirty = false; Pos.ZOff(-1, -1); - Style = NULL; Size.x = 200; Size.y = 64; - Scale = 1; SourceValid.ZOff(-1, -1); - ResizeIdx = -1; - ThreadBusy = 0; Margin.ZOff(0, 0); Border.ZOff(0, 0); Padding.ZOff(0, 0); } LRichTextPriv::ImageBlock::ImageBlock(const ImageBlock *Copy) : Block(Copy->d) { - ThreadHnd = 0; - ThreadBusy = 0; LayoutDirty = true; SourceImg.Reset(new LMemDC(Copy->SourceImg)); Size = Copy->Size; - IsDeleted = false; Margin = Copy->Margin; Border = Copy->Border; Padding = Copy->Padding; } LRichTextPriv::ImageBlock::~ImageBlock() { LAssert(ThreadBusy == 0); if (ThreadHnd) PostThreadEvent(ThreadHnd, M_CLOSE); LAssert(Cursors == 0); } bool LRichTextPriv::ImageBlock::IsValid() { return true; } bool LRichTextPriv::ImageBlock::IsBusy(bool Stop) { return ThreadBusy != 0; } -bool LRichTextPriv::ImageBlock::SetImage(LAutoPtr Img) +void LRichTextPriv::ImageBlock::OnDimensions() { - SourceImg = Img; - if (!SourceImg) - return false; - Scales.Length(CountOf(ImgScales)); for (int i=0; iX() * ImgScales[i] / 100; si.Sz.y = SourceImg->Y() * ImgScales[i] / 100; si.Percent = ImgScales[i]; if (si.Sz.x == SourceImg->X() && - si.Sz.y == SourceImg->Y()) + si.Sz.y == SourceImg->Y() && + ResizeIdx < 0) { ResizeIdx = i; + ResizeSrc = SourceDefault; + } + } +} + +void LRichTextPriv::ImageBlock::GetCompressedSize() +{ + // Also create a JPG for the current scale (needed before + // we save to HTML). + if (Scales.IdxCheck(ResizeIdx)) + { + ScaleInf &si = Scales[ResizeIdx]; + if (!si.Compressing && !si.Compressed) + { + si.Compressing = true; + LOADER_LOG("%s:%i post M_IMAGE_COMPRESS %s\n", _FL, si.ToString().Get()); + if (PostThreadEvent(GetThreadHandle(), M_IMAGE_COMPRESS, (LMessage::Param)SourceImg.Get(), (LMessage::Param)&si)) + UpdateThreadBusy(_FL, 1); } } + else LAssert(!"ResizeIdx should be valid."); +} +void LRichTextPriv::ImageBlock::MaxImageFilter() +{ + LgiTrace("MaxImageFilter: ResizeIdx=%i ResizeSrc=%s\n", ResizeIdx, ToString(ResizeSrc)); + for (int i=0; iGetSize(); + if (d->View->MaxImageFilter(params)) + { + if (ResizeIdx <= 0) + LOADER_LOG("%s:%i no more resize steps available?\n", _FL); + else if (ResizeSrc == SourceUser) + LOADER_LOG("%s:%i we won't override the user's request.\n", _FL); + else + { + ResizeIdx--; + LOADER_LOG("%s:%i dec ResizeIdx=%i\n", _FL, ResizeIdx); + GetCompressedSize(); + } + } + } +} + +bool LRichTextPriv::ImageBlock::SetImage(LAutoPtr Img) +{ + SourceImg = Img; + if (!SourceImg) + return false; + + OnDimensions(); LayoutDirty = true; UpdateDisplayImg(); if (DisplayImg) { // Update the display image by scaling it from the source... if (PostThreadEvent(GetThreadHandle(), M_IMAGE_RESAMPLE, (LMessage::Param) DisplayImg.Get(), (LMessage::Param) SourceImg.Get())) UpdateThreadBusy(_FL, 1); } else LayoutDirty = true; - // Also create a JPG for the current scale (needed before - // we save to HTML). - if (ResizeIdx >= 0 && ResizeIdx < (int)Scales.Length()) - { - ScaleInf &si = Scales[ResizeIdx]; - if (PostThreadEvent(GetThreadHandle(), M_IMAGE_COMPRESS, (LMessage::Param)SourceImg.Get(), (LMessage::Param)&si)) - UpdateThreadBusy(_FL, 1); - } - else LAssert(!"ResizeIdx should be valid."); + GetCompressedSize(); return true; } bool LRichTextPriv::ImageBlock::Load(const char *Src) { if (Src) Source = Src; LAutoPtr Stream; - LString::Array a = Source.Strip().Split(":", 1); + auto a = Source.Strip().Split(":", 1); if (a.Length() > 1 && a[0].Equals("cid")) { - LDocumentEnv *Env = d->View->GetEnv(); + auto Env = d->View->GetEnv(); if (!Env) return false; auto j = Env->NewJob(); if (!j) return false; j->Uri.Reset(NewStr(Source)); j->Env = Env; j->Pref = LDocumentEnv::LoadJob::FmtStream; j->UserUid = d->View->GetDocumentUid(); auto Result = Env->GetContent(j); if (Result == LDocumentEnv::LoadImmediate) { StreamMimeType = j->MimeType; ContentId = j->ContentId.Strip("<>"); FileName = j->Filename; if (j->Stream) { Stream = j->Stream; } else if (j->pDC) { SourceImg = j->pDC; return true; } } else if (Result == LDocumentEnv::LoadDeferred) { LAssert(!"Impl me?"); } } else if (LFileExists(Source)) { FileName = Source; FileMimeType = LAppInst->GetFileMimeType(Source); } else return false; if (!FileName && !Stream) return false; if (Stream) { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Posting M_IMAGE_LOAD_STREAM\n", _FL); - #endif + LOADER_LOG("%s:%i - Posting M_IMAGE_LOAD_STREAM\n", _FL); if (PostThreadEvent(GetThreadHandle(), M_IMAGE_LOAD_STREAM, (LMessage::Param)Stream.Release(), (LMessage::Param) (FileName ? new LString(FileName) : NULL))) { UpdateThreadBusy(_FL, 1); return true; } } if (FileName) { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Posting M_IMAGE_LOAD_FILE\n", _FL); - #endif + LOADER_LOG("%s:%i - Posting M_IMAGE_LOAD_FILE\n", _FL); if (PostThreadEvent(GetThreadHandle(), M_IMAGE_LOAD_FILE, (LMessage::Param)new LString(FileName))) { UpdateThreadBusy(_FL, 1); return true; } } return false; } int LRichTextPriv::ImageBlock::GetLines() { return 1; } bool LRichTextPriv::ImageBlock::OffsetToLine(ssize_t Offset, int *ColX, LArray *LineY) { if (ColX) *ColX = Offset > 0; if (LineY) LineY->Add(0); return true; } ssize_t LRichTextPriv::ImageBlock::LineToOffset(ssize_t Line) { return 0; } void LRichTextPriv::ImageBlock::Dump() { } LNamedStyle *LRichTextPriv::ImageBlock::GetStyle(ssize_t At) { return Style; } void LRichTextPriv::ImageBlock::SetStyle(LNamedStyle *s) { if ((Style = s)) { LFont *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::ImageBlock::Length() { return IsDeleted ? 0 : 1; } bool LRichTextPriv::ImageBlock::ToHtml(LStream &s, LArray *Media, LRange *Rng) { LUri uri(Source); if (uri.IsProtocol("http") || uri.IsProtocol("https") || uri.IsProtocol("ftp")) { // Nothing to do...? } else if (Media) { bool ValidSourceFile = LFileExists(Source); LDocView::ContentMedia &Cm = Media->New(); int Idx = LRand() % 10000; if (!ContentId) ContentId.Printf("%u@memecode.com", Idx); Cm.Id = ContentId; LString Style; - ScaleInf *Si = ResizeIdx >= 0 && ResizeIdx < (int)Scales.Length() ? &Scales[ResizeIdx] : NULL; + auto Si = ResizeIdx >= 0 && ResizeIdx < (int)Scales.Length() ? &Scales[ResizeIdx] : NULL; if (Si && Si->Compressed) { // Attach a copy of the resized JPEG... Si->Compressed->SetPos(0); Cm.Stream.Reset(new LMemStream(Si->Compressed, 0, -1)); Cm.MimeType = Si->MimeType; if (FileName) Cm.FileName = LGetLeaf(FileName); else if (Cm.MimeType.Equals("image/jpeg")) Cm.FileName.Printf("img%u.jpg", Idx); else if (Cm.MimeType.Equals("image/png")) Cm.FileName.Printf("img%u.png", Idx); else if (Cm.MimeType.Equals("image/tiff")) Cm.FileName.Printf("img%u.tiff", Idx); else if (Cm.MimeType.Equals("image/gif")) Cm.FileName.Printf("img%u.gif", Idx); else if (Cm.MimeType.Equals("image/bmp")) Cm.FileName.Printf("img%u.bmp", Idx); else { LAssert(!"Unknown image mime type?"); Cm.FileName.Printf("img%u", Idx); } } else if (ValidSourceFile) { // Attach the original file... Cm.MimeType = LAppInst->GetFileMimeType(Source); Cm.FileName = LGetLeaf(Source); - LFile *f = new LFile; + LAutoPtr f(new LFile); if (f) { if (f->Open(Source, O_READ)) - { - Cm.Stream.Reset(f); - } + Cm.Stream.Reset(f.Release()); else - { - delete f; LgiTrace("%s:%i - Failed to open link image '%s'.\n", _FL, Source.Get()); - } } } else { LAssert(!"No valid source."); return false; } LAssert(Cm.MimeType != NULL); if (DisplayImg && SourceImg && DisplayImg->X() != SourceImg->X()) { int Dx = DisplayImg->X(); Style.Printf(" style=\"width:%ipx\"", Dx); } if (Cm.Stream) { s.Print("HtmlLinkAsCid) s.Print("cid:%s", Cm.Id.Get()); else s.Print("%s", Cm.FileName.Get()); s.Print("\">\n"); LAssert(Cm.Valid()); return true; } } s.Print("\n", Source.Get()); return true; } bool LRichTextPriv::ImageBlock::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; } Cursor->Pos = ImgPos; Cursor->Line = Pos; if (Cursor->Offset == 0) { Cursor->Pos.x2 = Cursor->Pos.x1 + 1; } else if (Cursor->Offset == 1) { Cursor->Pos.x1 = Cursor->Pos.x2 - 1; } return true; } bool LRichTextPriv::ImageBlock::HitTest(HitTestResult &htr) { if (htr.In.y < Pos.y1 || htr.In.y > Pos.y2) return false; htr.Near = false; htr.LineHint = 0; int Cx = ImgPos.x1 + (ImgPos.X() / 2); if (htr.In.x < Cx) htr.Idx = 0; else htr.Idx = 1; return true; } void LRichTextPriv::ImageBlock::OnPaint(PaintContext &Ctx) { bool ImgSelected = Ctx.SelectBeforePaint(this); // 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); LRegion unpainted(r); if (!DisplayImg && SourceImg && SourceImg->X() > r.X()) { UpdateDisplayImg(); } LSurface *Src = DisplayImg ? DisplayImg : SourceImg; if (Src) { if (SourceValid.Valid()) { LRect Bounds(0, 0, Size.x-1, Size.y-1); Bounds.Offset(r.x1, r.y1); Ctx.pDC->Colour(L_MED); Ctx.pDC->Box(&Bounds); Bounds.Inset(1, 1); Ctx.pDC->Colour(L_WORKSPACE); Ctx.pDC->Rectangle(&Bounds); LRect rr(0, 0, Src->X()-1, SourceValid.y2 / Scale); Ctx.pDC->Blt(ImgPos.x1, ImgPos.y1, Src, &rr); rr.Offset(ImgPos.x1, ImgPos.y1); unpainted.Subtract(&rr); } else { LRect rr; if (Ctx.Type == LRichTextPriv::Selected) { if (!SelectImg && SelectImg.Reset(new LMemDC(Src->X(), Src->Y(), System32BitColourSpace))) { SelectImg->Blt(0, 0, Src); int Op = SelectImg->Op(GDC_ALPHA); LColour c = Ctx.Colours[LRichTextPriv::Selected].Back; c.Rgb(c.r(), c.g(), c.b(), 0xa0); SelectImg->Colour(c); SelectImg->Rectangle(); SelectImg->Op(Op); } rr = SelectImg->Bounds(); Ctx.pDC->Blt(ImgPos.x1, ImgPos.y1, SelectImg); } else { rr = Src->Bounds(); Ctx.pDC->Blt(ImgPos.x1, ImgPos.y1, Src); } rr.Offset(ImgPos.x1, ImgPos.y1); unpainted.Subtract(&rr); } } else { // Drag missing image... r = ImgPos; LColour cBack(245, 245, 245); Ctx.pDC->Colour(ImgSelected ? cBack.Mix(Ctx.Colours[Selected].Back) : cBack); Ctx.pDC->Rectangle(&r); Ctx.pDC->Colour(L_LOW); uint Ls = Ctx.pDC->LineStyle(LSurface::LineAlternate); Ctx.pDC->Box(&r); Ctx.pDC->LineStyle(Ls); int Cx = r.x1 + (r.X() >> 1); int Cy = r.y1 + (r.Y() >> 1); Ctx.pDC->Colour(LColour::Red); int Sz = 5; Ctx.pDC->Line(Cx - Sz, Cy - Sz, Cx + Sz, Cy + Sz); Ctx.pDC->Line(Cx - Sz, Cy - Sz + 1, Cx + Sz - 1, Cy + Sz); Ctx.pDC->Line(Cx - Sz + 1, Cy - Sz, Cx + Sz, Cy + Sz - 1); Ctx.pDC->Line(Cx + Sz, Cy - Sz, Cx - Sz, Cy + Sz); Ctx.pDC->Line(Cx + Sz - 1, Cy - Sz, Cx - Sz, Cy + Sz - 1); Ctx.pDC->Line(Cx + Sz, Cy - Sz + 1, Cx - Sz + 1, Cy + Sz); unpainted.Subtract(&ImgPos); } ImgSelected = Ctx.SelectAfterPaint(this); if (ImgSelected) { Ctx.pDC->Colour(Ctx.Colours[Selected].Back); Ctx.pDC->Rectangle(ImgPos.x2 + 1, ImgPos.y1, ImgPos.x2 + 7, ImgPos.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); } } bool LRichTextPriv::ImageBlock::OnLayout(Flow &flow) { LayoutDirty = false; 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; ImgPos.x1 = Pos.x1 + Padding.x1; ImgPos.y1 = Pos.y1 + Padding.y1; ImgPos.x2 = ImgPos.x1 + Size.x - 1; ImgPos.y2 = ImgPos.y1 + Size.y - 1; int Px2 = ImgPos.x2 + Padding.x2; if (Px2 < Pos.x2) Pos.x2 = ImgPos.x2 + Padding.x2; Pos.y2 = ImgPos.y2 + Padding.y2; 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::ImageBlock::GetTextAt(ssize_t Offset, LArray &t) { // No text to get return 0; } ssize_t LRichTextPriv::ImageBlock::CopyAt(ssize_t Offset, ssize_t Chars, LArray *Text) { // No text to copy return 0; } bool LRichTextPriv::ImageBlock::Seek(SeekType To, BlockCursor &Cursor) { switch (To) { case SkLineStart: { Cursor.Offset = 0; Cursor.LineHint = 0; break; } case SkLineEnd: { Cursor.Offset = 1; Cursor.LineHint = 0; break; } case SkLeftChar: { if (Cursor.Offset != 1) return false; Cursor.Offset = 0; Cursor.LineHint = 0; break; } case SkRightChar: { if (Cursor.Offset != 0) return false; Cursor.Offset = 1; Cursor.LineHint = 0; break; } default: { return false; break; } } return true; } ssize_t LRichTextPriv::ImageBlock::FindAt(ssize_t StartIdx, const uint32_t *Str, LFindReplaceCommon *Params) { // No text to find in return -1; } void LRichTextPriv::ImageBlock::IncAllStyleRefs() { if (Style) Style->RefCount++; } bool LRichTextPriv::ImageBlock::DoContext(LSubMenu &s, LPoint Doc, ssize_t Offset, bool TopOfMenu) { if (SourceImg && !TopOfMenu) { s.AppendSeparator(); LSubMenu *c = s.AppendSub("Transform Image"); if (c) { c->AppendItem("Rotate Clockwise", IDM_CLOCKWISE); c->AppendItem("Rotate Anti-clockwise", IDM_ANTI_CLOCKWISE); c->AppendItem("Horizontal Flip", IDM_X_FLIP); c->AppendItem("Vertical Flip", IDM_Y_FLIP); } c = s.AppendSub("Scale Image"); if (c) { for (unsigned i=0; iX() * ImgScales[i] / 100; si.Sz.y = SourceImg->Y() * ImgScales[i] / 100; si.Percent = ImgScales[i]; m.Printf("%i x %i, %i%% ", si.Sz.x, si.Sz.y, ImgScales[i]); if (si.Compressed) { char Sz[128]; LFormatSize(Sz, sizeof(Sz), si.Compressed->GetSize()); LString s; s.Printf(" (%s)", Sz); m += s; } LMenuItem *mi = c->AppendItem(m, IDM_SCALE_IMAGE+i, !IsBusy()); if (mi && ResizeIdx == i) { mi->Checked(true); } } } return true; } return false; } LRichTextPriv::Block *LRichTextPriv::ImageBlock::Clone() { return new ImageBlock(this); } void LRichTextPriv::ImageBlock::OnComponentInstall(LString Name) { if (Source && !SourceImg) { // Retry the load? Load(Source); } } void LRichTextPriv::ImageBlock::UpdateDisplay(int yy) { LRect s; if (DisplayImg && !SourceValid.Valid()) { SourceValid = SourceImg->Bounds(); SourceValid.y2 = yy; s = SourceValid; } else { s = SourceValid; s.y1 = s.y2 + 1; s.y2 = SourceValid.y2 = yy; } + /* This seems to be broken... if (DisplayImg) { LRect d(0, s.y1 / Scale, DisplayImg->X()-1, s.y2 / Scale); // Do a quick and dirty nearest neighbor scale to // show the user some feed back. LSurface *Src = SourceImg; LSurface *Dst = DisplayImg; for (int y=d.y1; y<=d.y2; y++) { int sy = y * Scale; int sx = d.x1 * Scale; for (int x=d.x1; x<=d.x2; x++, sx+=Scale) { COLOUR c = Src->Get(sx, sy); Dst->Colour(c); Dst->Set(x, y); } } } + */ LayoutDirty = true; this->d->InvalidateDoc(NULL); } int LRichTextPriv::ImageBlock::GetThreadHandle() { if (ThreadHnd == 0) { ImageLoader *il = new ImageLoader(this); if (il != NULL) ThreadHnd = il->GetHandle(); } return ThreadHnd; } void LRichTextPriv::ImageBlock::UpdateDisplayImg() { if (!SourceImg) return; Size.x = SourceImg->X(); Size.y = SourceImg->Y(); int ViewX = d->Areas[LRichTextEdit::ContentArea].X(); if (ViewX > 0) { int MaxX = (int) (ViewX * 0.9); if (SourceImg->X() > MaxX) { double Ratio = (double)SourceImg->X() / MAX(1, MaxX); Scale = (int)ceil(Ratio); Size.x = (int)ceil((double)SourceImg->X() / Scale); Size.y = (int)ceil((double)SourceImg->Y() / Scale); if (DisplayImg.Reset(new LMemDC(Size.x, Size.y, SourceImg->GetColourSpace()))) { DisplayImg->Colour(L_MED); DisplayImg->Rectangle(); if (PostThreadEvent(GetThreadHandle(), M_IMAGE_RESAMPLE, (LMessage::Param)DisplayImg.Get(), (LMessage::Param)SourceImg.Get())) { UpdateThreadBusy(_FL, 1); } } } } } void LRichTextPriv::ImageBlock::UpdateThreadBusy(const char *File, int Line, int Off) { if (ThreadBusy + Off >= 0) { ThreadBusy += Off; - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - ThreadBusy=%i\n", File, Line, ThreadBusy); - #endif + LOADER_LOG("%s:%i - ThreadBusy=%i\n", File, Line, ThreadBusy); } else { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Error: ThreadBusy=%i\n", File, Line, ThreadBusy, ThreadBusy + Off); - #endif + LOADER_LOG("%s:%i - Error: ThreadBusy=%i\n", File, Line, ThreadBusy, ThreadBusy + Off); LAssert(0); } } LMessage::Result LRichTextPriv::ImageBlock::OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_COMMAND: { if (!SourceImg) break; if (Msg->A() >= IDM_SCALE_IMAGE && - Msg->A() < IDM_SCALE_IMAGE + CountOf(ImgScales)) + Msg->A() < IDM_SCALE_IMAGE + CountOf(ImgScales)) { int i = (int)Msg->A() - IDM_SCALE_IMAGE; if (i >= 0 && i < (int)Scales.Length()) { ScaleInf &si = Scales[i]; ResizeIdx = i; + ResizeSrc = SourceUser; + LgiTrace("%s:%i - user setting image scale to %i\n", _FL, ResizeIdx); - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Posting M_IMAGE_COMPRESS\n", _FL); - #endif + LOADER_LOG("%s:%i - Posting M_IMAGE_COMPRESS\n", _FL); if (PostThreadEvent(GetThreadHandle(), M_IMAGE_COMPRESS, (LMessage::Param)SourceImg.Get(), (LMessage::Param)&si)) UpdateThreadBusy(_FL, 1); else LAssert(!"PostThreadEvent failed."); } } else switch (Msg->A()) { case IDM_CLOCKWISE: - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Posting M_IMAGE_ROTATE\n", _FL); - #endif + LOADER_LOG("%s:%i - Posting M_IMAGE_ROTATE\n", _FL); if (PostThreadEvent(GetThreadHandle(), M_IMAGE_ROTATE, (LMessage::Param) SourceImg.Get(), 1)) UpdateThreadBusy(_FL, 1); break; case IDM_ANTI_CLOCKWISE: - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Posting M_IMAGE_ROTATE\n", _FL); - #endif + LOADER_LOG("%s:%i - Posting M_IMAGE_ROTATE\n", _FL); if (PostThreadEvent(GetThreadHandle(), M_IMAGE_ROTATE, (LMessage::Param) SourceImg.Get(), -1)) UpdateThreadBusy(_FL, 1); break; case IDM_X_FLIP: - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Posting M_IMAGE_FLIP\n", _FL); - #endif + LOADER_LOG("%s:%i - Posting M_IMAGE_FLIP\n", _FL); if (PostThreadEvent(GetThreadHandle(), M_IMAGE_FLIP, (LMessage::Param) SourceImg.Get(), 1)) UpdateThreadBusy(_FL, 1); break; case IDM_Y_FLIP: - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Posting M_IMAGE_FLIP\n", _FL); - #endif + LOADER_LOG("%s:%i - Posting M_IMAGE_FLIP\n", _FL); if (PostThreadEvent(GetThreadHandle(), M_IMAGE_FLIP, (LMessage::Param) SourceImg.Get(), 0)) UpdateThreadBusy(_FL, 1); break; } break; } case M_IMAGE_COMPRESS: { - LAutoPtr Jpg((LMemStream*)Msg->A()); - ScaleInf *Si = (ScaleInf*)Msg->B(); + auto Jpg = Msg->AutoA(); + auto Si = (ScaleInf*)Msg->B(); if (!Jpg || !Si) { LAssert(0); - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Error: M_IMAGE_COMPRESS bad arg\n", _FL); - #endif + LOADER_LOG("%s:%i - Error: M_IMAGE_COMPRESS bad arg\n", _FL); break; } - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Received M_IMAGE_COMPRESS\n", _FL); - #endif + Si->Compressing = false; Si->Compressed.Reset(Jpg.Release()); Si->MimeType = "image/jpeg"; + LOADER_LOG("%s:%i - Received M_IMAGE_COMPRESS: %s\n", _FL, Si->ToString().Get()); UpdateThreadBusy(_FL, -1); // Change the doc to dirty d->Dirty = true; d->View->SendNotify(LNotifyDocChanged); + + MaxImageFilter(); break; } case M_IMAGE_ERROR: { LAutoPtr ErrMsg((LString*) Msg->A()); - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Received M_IMAGE_ERROR, posting M_CLOSE\n", _FL); - #endif + LOADER_LOG("%s:%i - Received M_IMAGE_ERROR, posting M_CLOSE\n", _FL); UpdateThreadBusy(_FL, -1); break; } case M_IMAGE_COMPONENT_MISSING: { LAutoPtr Component((LString*) Msg->A()); - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Received M_IMAGE_COMPONENT_MISSING, posting M_CLOSE\n", _FL); - #endif + LOADER_LOG("%s:%i - Received M_IMAGE_COMPONENT_MISSING, posting M_CLOSE\n", _FL); UpdateThreadBusy(_FL, -1); if (Component) { auto t = LString(*Component).SplitDelimit(","); for (int i=0; iView->NeedsCapability(t[i]); } else LAssert(!"Missing component name."); break; } case M_IMAGE_SET_SURFACE: { - LAutoPtr File((LStream*)Msg->B()); - - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Received M_IMAGE_SET_SURFACE\n", _FL); - #endif - + auto File = Msg->AutoB(); + LOADER_LOG("%s:%i - Received M_IMAGE_SET_SURFACE\n", _FL); if (SourceImg.Reset((LSurface*)Msg->A())) { - Scales.Length(CountOf(ImgScales)); + OnDimensions(); for (int i=0; iX() * ImgScales[i] / 100; - si.Sz.y = SourceImg->Y() * ImgScales[i] / 100; - si.Percent = ImgScales[i]; - if (si.Sz.x == SourceImg->X() && si.Sz.y == SourceImg->Y()) { - ResizeIdx = i; si.Compressed.Reset(File.Release()); - if (StreamMimeType) { si.MimeType = StreamMimeType; } else if (FileMimeType) { si.MimeType = FileMimeType.Get(); FileMimeType.Empty(); } } } + LAssert(File.Get() == NULL); // One of the scales should have claimed the data UpdateDisplayImg(); + MaxImageFilter(); } break; } case M_IMAGE_PROGRESS: { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Received M_IMAGE_PROGRESS\n", _FL); - #endif - + LOADER_LOG("%s:%i - Received M_IMAGE_PROGRESS\n", _FL); + UpdateDisplay((int)Msg->A()); break; } case M_IMAGE_FINISHED: { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Received M_IMAGE_FINISHED\n", _FL); - #endif + LOADER_LOG("%s:%i - Received M_IMAGE_FINISHED\n", _FL); UpdateThreadBusy(_FL, -1); if (SourceImg) { + OnDimensions(); UpdateDisplay(SourceImg->Y()-1); + if ( DisplayImg != NULL && PostThreadEvent(GetThreadHandle(), M_IMAGE_RESAMPLE, (LMessage::Param)DisplayImg.Get(), (LMessage::Param)SourceImg.Get()) ) { UpdateThreadBusy(_FL, 1); } + + GetCompressedSize(); } break; } case M_IMAGE_RESAMPLE: { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Received M_IMAGE_RESAMPLE\n", _FL); - #endif - + LOADER_LOG("%s:%i - Received M_IMAGE_RESAMPLE\n", _FL); + LayoutDirty = true; UpdateThreadBusy(_FL, -1); d->InvalidateDoc(NULL); SourceValid.ZOff(-1, -1); break; } case M_IMAGE_ROTATE: case M_IMAGE_FLIP: { - #if LOADER_THREAD_LOGGING - LgiTrace("%s:%i - Received %s\n", _FL, Msg->Msg()==M_IMAGE_ROTATE?"M_IMAGE_ROTATE":"M_IMAGE_FLIP"); - #endif - + LOADER_LOG("%s:%i - Received %s\n", _FL, Msg->Msg()==M_IMAGE_ROTATE?"M_IMAGE_ROTATE":"M_IMAGE_FLIP"); + LAutoPtr Img = SourceImg; UpdateThreadBusy(_FL, -1); SetImage(Img); break; } default: return false; } return true; } bool LRichTextPriv::ImageBlock::AddText(Transaction *Trans, ssize_t AtOffset, const uint32_t *Str, ssize_t Chars, LNamedStyle *Style) { // Can't add text to image block return false; } bool LRichTextPriv::ImageBlock::ChangeStyle(Transaction *Trans, ssize_t Offset, ssize_t Chars, LCss *Style, bool Add) { // No styles to change... return false; } ssize_t LRichTextPriv::ImageBlock::DeleteAt(Transaction *Trans, ssize_t BlkOffset, ssize_t Chars, LArray *DeletedText) { // The image is one "character" IsDeleted = BlkOffset == 0; if (IsDeleted) return true; return false; } bool LRichTextPriv::ImageBlock::DoCase(Transaction *Trans, ssize_t StartIdx, ssize_t Chars, bool Upper) { // No text to change case... return false; } #ifdef _DEBUG void LRichTextPriv::ImageBlock::DumpNodes(LTreeItem *Ti) { LString s; s.Printf("ImageBlock style=%s", Style?Style->Name.Get():NULL); Ti->SetText(s); } #endif diff --git a/src/common/Widgets/Editor/RichTextEdit.cpp b/src/common/Widgets/Editor/RichTextEdit.cpp --- a/src/common/Widgets/Editor/RichTextEdit.cpp +++ b/src/common/Widgets/Editor/RichTextEdit.cpp @@ -1,3135 +1,3133 @@ #include #include #include #include "lgi/common/Lgi.h" #include "lgi/common/RichTextEdit.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/FontCache.h" #include "lgi/common/Unicode.h" #include "lgi/common/DropFiles.h" #include "lgi/common/HtmlCommon.h" #include "lgi/common/HtmlParser.h" #include "lgi/common/LgiRes.h" #include "lgi/common/FileSelect.h" #include "lgi/common/Menu.h" #include "lgi/common/Homoglyphs.h" // If this is not found add $lgi/private/common to your include paths #include "ViewPriv.h" #define DefaultCharset "utf-8" #define GDCF_UTF8 -1 #define POUR_DEBUG 0 #define PROFILE_POUR 0 #define ALLOC_BLOCK 64 #define IDC_VS 1000 #define PAINT_BORDER Back #define PAINT_AFTER_LINE Back #if !defined(WIN32) && !defined(toupper) #define toupper(c) (((c)>='a'&&(c)<='z') ? (c)-'a'+'A' : (c)) #endif // static char SelectWordDelim[] = " \t\n.,()[]<>=?/\\{}\"\';:+=-|!@#$%^&*"; #include "RichTextEditPriv.h" ////////////////////////////////////////////////////////////////////// LRichTextEdit::LRichTextEdit( int Id, int x, int y, int cx, int cy, LFontType *FontType) : ResObject(Res_Custom) { // init vars LView::d->Css.Reset(new LRichTextPriv(this, &d)); // setup window SetId(Id); SetTabStop(true); // default options #if WINNATIVE CrLf = true; SetDlgCode(DLGC_WANTALLKEYS); #else CrLf = false; #endif d->Padding(LCss::Len(LCss::LenPx, 4)); #if 0 d->BackgroundColor(LCss::ColorDef(LColour::Green)); #else d->BackgroundColor(LColour(L_WORKSPACE)); #endif SetFont(LSysFont); #if 0 // def _DEBUG Name("\n" "\n" " This is some bold text to test with.
\n" " A second line of text for testing.\n" "\n" "\n"); #endif } LRichTextEdit::~LRichTextEdit() { // 'd' is owned by the LView CSS autoptr. } bool LRichTextEdit::SetSpellCheck(LSpellCheck *sp) { if ((d->SpellCheck = sp)) { if (IsAttached()) d->SpellCheck->EnumLanguages(AddDispatch()); // else call that OnCreate } return d->SpellCheck != NULL; } bool LRichTextEdit::IsDirty() { return d->Dirty; } void LRichTextEdit::IsDirty(bool dirty) { if (d->Dirty ^ dirty) { d->Dirty = dirty; } } void LRichTextEdit::SetFixedWidthFont(bool i) { if (FixedWidthFont ^ i) { if (i) { LFontType Type; if (Type.GetSystemFont("Fixed")) { LDocView::SetFixedWidthFont(i); } } OnFontChange(); Invalidate(); } } void LRichTextEdit::SetReadOnly(bool i) { LDocView::SetReadOnly(i); #if WINNATIVE SetDlgCode(i ? DLGC_WANTARROWS : DLGC_WANTALLKEYS); #endif } LRect LRichTextEdit::GetArea(RectType Type) { return Type >= ContentArea && Type <= MaxArea ? d->Areas[Type] : LRect(0, 0, -1, -1); } bool LRichTextEdit::ShowStyleTools() { return d->ShowTools; } void LRichTextEdit::ShowStyleTools(bool b) { if (d->ShowTools ^ b) { d->ShowTools = b; Invalidate(); } } void LRichTextEdit::SetTabSize(uint8_t i) { TabSize = limit(i, 2, 32); OnFontChange(); OnPosChange(); Invalidate(); } void LRichTextEdit::SetWrapType(LDocWrapType i) { LDocView::SetWrapType(i); OnPosChange(); Invalidate(); } LFont *LRichTextEdit::GetFont() { return d->Font; } void LRichTextEdit::SetFont(LFont *f, bool OwnIt) { if (!f) return; if (OwnIt) { d->Font.Reset(f); } else if (d->Font.Reset(new LFont)) { *d->Font = *f; d->Font->Create(NULL, 0, 0); } OnFontChange(); } void LRichTextEdit::OnFontChange() { LgiTrace("%s:%i - LRichTextEdit::OnFontChange not impl.\n", _FL); } void LRichTextEdit::PourText(ssize_t Start, ssize_t Length /* == 0 means it's a delete */) { LAssert(!"Not impl."); } void LRichTextEdit::PourStyle(ssize_t Start, ssize_t EditSize) { LAssert(!"Not impl."); } bool LRichTextEdit::Insert(size_t At, const char16 *Data, ssize_t Len) { if (!d->Cursor || !d->Cursor->Blk) { LAssert(!"No cursor block."); return false; } auto b = d->Cursor->Blk; AutoTrans Trans(new LRichTextPriv::Transaction); #ifdef WINDOWS // UCS2 -> UTF32 LAutoPtr utf32((uint32_t*)LNewConvertCp("utf-32", Data, LGI_WideCharset, Len*sizeof(*Data))); uint32_t *u32 = utf32.Get(); auto len = Strlen(u32); #else LAssert(sizeof(char16) == sizeof(uint32_t)); uint32_t *u32 = (uint32_t*)Data; auto len = Len; #endif if (!b->AddText(Trans, d->Cursor->Offset, u32, len, NULL)) { LAssert(!"AddText failed."); return false; } d->Cursor->Set(d->Cursor->Offset + 1); Invalidate(); SendNotify(LNotifyDocChanged); d->AddTrans(Trans); return true; } bool LRichTextEdit::Delete(size_t At, ssize_t Len) { LAssert(!"Not impl."); return false; } bool LRichTextEdit::DeleteSelection(char16 **Cut) { AutoTrans t(new LRichTextPriv::Transaction); if (!d->DeleteSelection(t, Cut)) return false; return d->AddTrans(t); } int64 LRichTextEdit::Value() { const char *n = Name(); #ifdef _MSC_VER return (n) ? _atoi64(n) : 0; #else return (n) ? atoll(n) : 0; #endif } void LRichTextEdit::Value(int64 i) { char Str[32]; sprintf_s(Str, sizeof(Str), LPrintfInt64, i); Name(Str); } bool LRichTextEdit::GetFormattedContent(const char *MimeType, LString &Out, LArray *Media) { if (!MimeType || _stricmp(MimeType, "text/html")) return false; if (!d->ToHtml(Media)) return false; Out = d->UtfNameCache; return true; } const char *LRichTextEdit::Name() { d->ToHtml(); return d->UtfNameCache; } const char *LRichTextEdit::GetCharset() { return d->Charset; } void LRichTextEdit::SetCharset(const char *s) { d->Charset = s; } bool LRichTextEdit::GetVariant(const char *Name, LVariant &Value, const char *Array) { LDomProperty p = LStringToDomProp(Name); switch (p) { case HtmlImagesLinkCid: { Value = d->HtmlLinkAsCid; break; } case SpellCheckLanguage: { Value = d->SpellLang.Get(); break; } case SpellCheckDictionary: { Value = d->SpellDict.Get(); break; } default: return false; } return true; } bool LRichTextEdit::SetVariant(const char *Name, LVariant &Value, const char *Array) { LDomProperty p = LStringToDomProp(Name); switch (p) { case HtmlImagesLinkCid: { d->HtmlLinkAsCid = Value.CastInt32() != 0; break; } case SpellCheckLanguage: { d->SpellLang = Value.Str(); break; } case SpellCheckDictionary: { d->SpellDict = Value.Str(); break; } default: return false; } return true; } static LHtmlElement *FindElement(LHtmlElement *e, HtmlTag TagId) { if (e->TagId == TagId) return e; for (unsigned i = 0; i < e->Children.Length(); i++) { LHtmlElement *c = FindElement(e->Children[i], TagId); if (c) return c; } return NULL; } void LRichTextEdit::OnAddStyle(const char *MimeType, const char *Styles) { if (d->CreationCtx) { d->CreationCtx->StyleStore.Parse(Styles); } } bool LRichTextEdit::Name(const char *s) { d->Empty(); d->OriginalText = s; LHtmlElement Root(NULL); if (!d->CreationCtx.Reset(new LRichTextPriv::CreateContext(d))) return false; if (!d->LHtmlParser::Parse(&Root, s)) return d->Error(_FL, "Failed to parse HTML."); LHtmlElement *Body = FindElement(&Root, TAG_BODY); if (!Body) Body = &Root; bool Status = d->FromHtml(Body, *d->CreationCtx); // d->DumpBlocks(); if (!d->Blocks.Length()) { d->EmptyDoc(); } else { // Clear out any zero length blocks. for (unsigned i=0; iBlocks.Length(); i++) { LRichTextPriv::Block *b = d->Blocks[i]; if (b->Length() == 0) { d->Blocks.DeleteAt(i--, true); DeleteObj(b); } } } if (Status) SetCursor(0, false); Invalidate(); return Status; } const char16 *LRichTextEdit::NameW() { d->WideNameCache.Reset(Utf8ToWide(Name())); return d->WideNameCache; } bool LRichTextEdit::NameW(const char16 *s) { LAutoString a(WideToUtf8(s)); return Name(a); } char *LRichTextEdit::GetSelection() { if (!HasSelection()) return NULL; LArray Text; if (!d->GetSelection(&Text, NULL)) return NULL; return WideToUtf8(&Text[0]); } bool LRichTextEdit::HasSelection() { return d->Selection.Get() != NULL; } void LRichTextEdit::SelectAll() { AutoCursor Start(new BlkCursor(d->Blocks.First(), 0, 0)); d->SetCursor(Start); LRichTextPriv::Block *Last = d->Blocks.Length() ? d->Blocks.Last() : NULL; if (Last) { AutoCursor End(new BlkCursor(Last, Last->Length(), Last->GetLines()-1)); d->SetCursor(End, true); } else d->Selection.Reset(); Invalidate(); } void LRichTextEdit::UnSelectAll() { bool Update = HasSelection(); if (Update) { d->Selection.Reset(); Invalidate(); } } void LRichTextEdit::SetStylePrefix(LString s) { d->SetPrefix(s); } bool LRichTextEdit::IsBusy(bool Stop) { return d->IsBusy(Stop); } size_t LRichTextEdit::GetLines() { uint32_t Count = 0; for (size_t i=0; iBlocks.Length(); i++) { LRichTextPriv::Block *b = d->Blocks[i]; Count += b->GetLines(); } return Count; } int LRichTextEdit::GetLine() { if (!d->Cursor) return -1; ssize_t Idx = d->Blocks.IndexOf(d->Cursor->Blk); if (Idx < 0) { LAssert(0); return -1; } int Count = 0; // Count lines in blocks before the cursor... for (int i=0; iBlocks[i]; Count += b->GetLines(); } // Add the lines in the cursor's block... if (d->Cursor->LineHint) { Count += d->Cursor->LineHint; } else { LArray BlockLine; if (d->Cursor->Blk->OffsetToLine(d->Cursor->Offset, NULL, &BlockLine)) Count += BlockLine.First(); else { // Hmmm... LAssert(!"Can't find block line."); return -1; } } return Count; } void LRichTextEdit::SetLine(int Line) { int Count = 0; // Count lines in blocks before the cursor... for (int i=0; i<(int)d->Blocks.Length(); i++) { LRichTextPriv::Block *b = d->Blocks[i]; int Lines = b->GetLines(); if (Line >= Count && Line < Count + Lines) { auto BlockLine = Line - Count; auto Offset = b->LineToOffset(BlockLine); if (Offset >= 0) { AutoCursor c(new BlkCursor(b, Offset, BlockLine)); d->SetCursor(c); break; } } Count += Lines; } } void LRichTextEdit::GetTextExtent(int &x, int &y) { x = d->DocumentExtent.x; y = d->DocumentExtent.y; } bool LRichTextEdit::GetLineColumnAtIndex(LPoint &Pt, ssize_t Index) { ssize_t Offset = -1; int BlockLines = -1; LRichTextPriv::Block *b = d->GetBlockByIndex(Index, &Offset, NULL, &BlockLines); if (!b) return false; int Cols; LArray Lines; if (b->OffsetToLine(Offset, &Cols, &Lines)) return false; Pt.x = Cols; Pt.y = BlockLines + Lines.First(); return true; } ssize_t LRichTextEdit::GetCaret(bool Cur) { if (!d->Cursor) return -1; ssize_t CharPos = 0; for (ssize_t i=0; i<(ssize_t)d->Blocks.Length(); i++) { LRichTextPriv::Block *b = d->Blocks[i]; if (d->Cursor->Blk == b) return CharPos + d->Cursor->Offset; CharPos += b->Length(); } LAssert(!"Cursor block not found."); return -1; } bool LRichTextEdit::IndexAt(int x, int y, ssize_t &Off, int &LineHint) { LPoint Doc = d->ScreenToDoc(x, y); Off = d->HitTest(Doc.x, Doc.y, LineHint); return Off >= 0; } ssize_t LRichTextEdit::IndexAt(int x, int y) { ssize_t Idx; int Line; if (!IndexAt(x, y, Idx, Line)) return -1; return Idx; } void LRichTextEdit::SetCursor(int i, bool Select, bool ForceFullUpdate) { ssize_t Offset = -1; LRichTextPriv::Block *Blk = d->GetBlockByIndex(i, &Offset); if (Blk) { AutoCursor c(new BlkCursor(Blk, Offset, -1)); if (c) d->SetCursor(c, Select); } } bool LRichTextEdit::Cut() { if (!HasSelection()) return false; char16 *Txt = NULL; if (!DeleteSelection(&Txt)) return false; bool Status = true; if (Txt) { LClipBoard Cb(this); Status = Cb.TextW(Txt); DeleteArray(Txt); } SendNotify(LNotifyDocChanged); return Status; } bool LRichTextEdit::Copy() { if (!HasSelection()) return false; LArray PlainText; LAutoString Html; if (!d->GetSelection(&PlainText, &Html)) return false; LString PlainUtf8 = PlainText.AddressOf(); if (HasHomoglyphs(PlainUtf8, PlainUtf8.Length())) { if (LgiMsg( this, LLoadString(L_TEXTCTRL_HOMOGLYPH_WARNING, "Text contains homoglyph characters that maybe a phishing attack.\n" "Do you really want to copy it?"), LLoadString(L_TEXTCTRL_WARNING, "Warning"), MB_YESNO) == IDNO) return false; } // Put on the clipboard LClipBoard Cb(this); bool Status = Cb.TextW(PlainText.AddressOf()); Cb.Html(Html, false); return Status; } bool LRichTextEdit::Paste() { LClipBoard Cb(this); Cb.Bitmap([this](auto bmp, auto str) { LString Html; LAutoWString Text; LAutoPtr Img; Img = bmp; if (!Img) { LClipBoard Cb(this); Html = Cb.Html(); if (!Html) Text.Reset(NewStrW(Cb.TextW())); } if (!Html && !Text && !Img) return false; if (!d->Cursor || !d->Cursor->Blk) { LAssert(0); return false; } AutoTrans Trans(new LRichTextPriv::Transaction); if (HasSelection()) { if (!d->DeleteSelection(Trans, NULL)) return false; } if (Html) { LHtmlElement Root(NULL); if (!d->CreationCtx.Reset(new LRichTextPriv::CreateContext(d))) return false; if (!d->LHtmlParser::Parse(&Root, Html)) return d->Error(_FL, "Failed to parse HTML."); LHtmlElement *Body = FindElement(&Root, TAG_BODY); if (!Body) Body = &Root; if (d->Cursor) { auto *b = d->Cursor->Blk; ssize_t BlkIdx = d->Blocks.IndexOf(b); LRichTextPriv::Block *After = NULL; ssize_t AddIndex = BlkIdx;; // Split 'b' to make room for pasted objects if (d->Cursor->Offset > 0) { After = b->Split(Trans, d->Cursor->Offset); AddIndex = BlkIdx+1; } // else Insert before cursor block auto *PastePoint = new LRichTextPriv::TextBlock(d); if (PastePoint) { d->Blocks.AddAt(AddIndex++, PastePoint); if (After) d->Blocks.AddAt(AddIndex++, After); d->CreationCtx->Tb = PastePoint; d->FromHtml(Body, *d->CreationCtx); } } } else if (Text) { LAutoPtr Utf32((uint32_t*)LNewConvertCp("utf-32", Text, LGI_WideCharset)); ptrdiff_t Len = Strlen(Utf32.Get()); if (!d->Cursor->Blk->AddText(Trans, d->Cursor->Offset, Utf32.Get(), (int)Len)) { LAssert(0); return false; } d->Cursor->Offset += Len; d->Cursor->LineHint = -1; } else if (Img) { - LRichTextPriv::Block *b = d->Cursor->Blk; - ssize_t BlkIdx = d->Blocks.IndexOf(b); + auto b = d->Cursor->Blk; + auto BlkIdx = d->Blocks.IndexOf(b); LRichTextPriv::Block *After = NULL; ssize_t AddIndex; LAssert(BlkIdx >= 0); // Split 'b' to make room for the image if (d->Cursor->Offset > 0) { After = b->Split(Trans, d->Cursor->Offset); - AddIndex = BlkIdx+1; + AddIndex = BlkIdx + 1; } else { // Insert before.. AddIndex = BlkIdx; } - LRichTextPriv::ImageBlock *ImgBlk = new LRichTextPriv::ImageBlock(d); - if (ImgBlk) + if (auto ImgBlk = new LRichTextPriv::ImageBlock(d)) { d->Blocks.AddAt(AddIndex++, ImgBlk); if (After) d->Blocks.AddAt(AddIndex++, After); Img->MakeOpaque(); ImgBlk->SetImage(Img); AutoCursor c(new BlkCursor(ImgBlk, 1, -1)); d->SetCursor(c); } } Invalidate(); SendNotify(LNotifyDocChanged); return d->AddTrans(Trans); }); return true; } bool LRichTextEdit::ClearDirty(bool Ask, const char *FileName) { if (1 /*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) { if (FileName) Save(FileName); else { LFileSelect *Select = new LFileSelect; Select->Parent(this); Select->Save([this](auto dlg, auto status) { if (status) Save(dlg->Name()); delete dlg; }); } } else if (Answer == IDCANCEL) { return false; } } return true; } bool LRichTextEdit::Open(const char *Name, const char *CharSet) { bool Status = false; LFile f; if (f.Open(Name, O_READ|O_SHARE)) { size_t Bytes = (size_t)f.GetSize(); SetCursor(0, false); 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; } } } DeleteArray(c8); } else { } Invalidate(); } return Status; } bool LRichTextEdit::Save(const char *FileName, const char *CharSet) { LFile f; if (!FileName || !f.Open(FileName, O_WRITE)) return false; f.SetSize(0); const char *Nm = Name(); if (!Nm) return false; size_t Len = strlen(Nm); return f.Write(Nm, (int)Len) == Len; } void LRichTextEdit::UpdateScrollBars(bool Reset) { if (VScroll) { //LRect Before = GetClient(); } } void LRichTextEdit::DoCase(std::function Callback, bool Upper) { if (!HasSelection()) { if (Callback) Callback(false); return; } bool Cf = d->CursorFirst(); LRichTextPriv::BlockCursor *Start = Cf ? d->Cursor : d->Selection; LRichTextPriv::BlockCursor *End = Cf ? d->Selection : d->Cursor; if (Start->Blk == End->Blk) { // In the same block... ssize_t Len = End->Offset - Start->Offset; Start->Blk->DoCase(NoTransaction, Start->Offset, Len, Upper); } else { // Multi-block delete... // 1) Delete all the content to the end of the first block ssize_t StartLen = Start->Blk->Length(); if (Start->Offset < StartLen) Start->Blk->DoCase(NoTransaction, Start->Offset, StartLen - Start->Offset, Upper); // 2) Delete any blocks between 'Start' and 'End' ssize_t i = d->Blocks.IndexOf(Start->Blk); if (i >= 0) { for (++i; d->Blocks[i] != End->Blk && i < (int)d->Blocks.Length(); ) { LRichTextPriv::Block *b = d->Blocks[i]; b->DoCase(NoTransaction, 0, -1, Upper); } } else { LAssert(0); if (Callback) Callback(false); return; } // 3) Delete any text up to the Cursor in the 'End' block End->Blk->DoCase(NoTransaction, 0, End->Offset, Upper); } // Update the screen d->Dirty = true; Invalidate(); if (Callback) Callback(true); } void LRichTextEdit::DoGoto(std::function Callback) { auto input = new LInput(this, "", LLoadString(L_TEXTCTRL_GOTO_LINE, "Goto line:"), "Text"); input->DoModal([this, input, Callback](auto dlg, auto ok) { if (ok) { auto i = input->GetStr().Int(); if (i >= 0) { SetLine((int)i); if (Callback) Callback(true); return; } } if (Callback) Callback(false); delete dlg; }); } LDocFindReplaceParams *LRichTextEdit::CreateFindReplaceParams() { return new LDocFindReplaceParams3; } void LRichTextEdit::SetFindReplaceParams(LDocFindReplaceParams *Params) { if (Params) { } } void LRichTextEdit::DoFindNext(std::function Callback) { if (Callback) Callback(false); } ////////////////////////////////////////////////////////////////////////////////// FIND void LRichTextEdit::DoFind(std::function Callback) { LArray Sel; if (HasSelection()) d->GetSelection(&Sel, NULL); LAutoString u(Sel.Length() ? WideToUtf8(&Sel.First()) : NULL); auto Dlg = new LFindDlg(this, [this](auto dlg, auto ctrlId) { return OnFind(dlg); }, u); Dlg->DoModal([this, Dlg, Callback](auto dlg, auto ctrlId) { if (Callback) Callback(ctrlId != IDCANCEL); Focus(true); delete dlg; }); } bool LRichTextEdit::OnFind(LFindReplaceCommon *Params) { if (!Params || !d->Cursor) { LAssert(0); return false; } LAutoPtr w((uint32_t*)LNewConvertCp("utf-32", Params->Find, "utf-8", Params->Find.Length())); ssize_t Idx = d->Blocks.IndexOf(d->Cursor->Blk); if (Idx < 0) { LAssert(0); return false; } for (unsigned n = 0; n < d->Blocks.Length(); n++) { ssize_t i = Idx + n; LRichTextPriv::Block *b = d->Blocks[i % d->Blocks.Length()]; ssize_t At = n ? 0 : d->Cursor->Offset; ssize_t Result = b->FindAt(At, w, Params); if (Result >= At) { ptrdiff_t Len = Strlen(w.Get()); AutoCursor Sel(new BlkCursor(b, Result, -1)); d->SetCursor(Sel, false); AutoCursor Cur(new BlkCursor(b, Result + Len, -1)); return d->SetCursor(Cur, true); } } return false; } ////////////////////////////////////////////////////////////////////////////////// REPLACE void LRichTextEdit::DoReplace(std::function Callback) { if (Callback) Callback(false); } bool LRichTextEdit::OnReplace(LFindReplaceCommon *Params) { return false; } ////////////////////////////////////////////////////////////////////////////////// void LRichTextEdit::SelectWord(size_t From) { int BlockIdx; ssize_t Start, End; LRichTextPriv::Block *b = d->GetBlockByIndex(From, &Start, &BlockIdx); if (!b) return; LArray Txt; if (!b->CopyAt(0, b->Length(), &Txt)) return; End = Start; while (Start > 0 && !IsWordBreakChar(Txt[Start-1])) Start--; while ( End < b->Length() && ( End == Txt.Length() || !IsWordBreakChar(Txt[End]) ) ) End++; AutoCursor c(new BlkCursor(b, Start, -1)); d->SetCursor(c); c.Reset(new BlkCursor(b, End, -1)); d->SetCursor(c, true); } bool LRichTextEdit::OnMultiLineTab(bool In) { return false; } void LRichTextEdit::OnSetHidden(int Hidden) { } void LRichTextEdit::OnPosChange() { static bool Processing = false; if (!Processing) { Processing = true; LLayout::OnPosChange(); // LRect c = GetClient(); Processing = false; } LLayout::OnPosChange(); } int LRichTextEdit::WillAccept(LDragFormats &Formats, LPoint Pt, int KeyState) { Formats.SupportsFileDrops(); #ifdef WINDOWS Formats.Supports("UniformResourceLocatorW"); #endif return Formats.Length() ? DROPEFFECT_COPY : DROPEFFECT_NONE; } int LRichTextEdit::OnDrop(LArray &Data, LPoint Pt, int KeyState) { int Effect = DROPEFFECT_NONE; for (unsigned i=0; iAreas[ContentArea].Overlap(Pt.x, Pt.y)) { int AddIndex = -1; LPoint TestPt( Pt.x - d->Areas[ContentArea].x1, Pt.y - d->Areas[ContentArea].y1); if (VScroll) TestPt.y += (int)(VScroll->Value() * d->ScrollLinePx); LDropFiles Df(dd); for (unsigned n=0; nHitTest(TestPt.x, TestPt.y, LineHint); if (Idx >= 0) { ssize_t BlkOffset; int BlkIdx; LRichTextPriv::Block *b = d->GetBlockByIndex(Idx, &BlkOffset, &BlkIdx); if (b) { LRichTextPriv::Block *After = NULL; // Split 'b' to make room for the image if (BlkOffset > 0) { After = b->Split(NoTransaction, BlkOffset); AddIndex = BlkIdx+1; } else { // Insert before.. AddIndex = BlkIdx; } - LRichTextPriv::ImageBlock *ImgBlk = new LRichTextPriv::ImageBlock(d); - if (ImgBlk) + if (auto ImgBlk = new LRichTextPriv::ImageBlock(d)) { d->Blocks.AddAt(AddIndex++, ImgBlk); if (After) d->Blocks.AddAt(AddIndex++, After); ImgBlk->Load(f); Effect = DROPEFFECT_COPY; } } } } } } break; } } if (Effect != DROPEFFECT_NONE) { Invalidate(); SendNotify(LNotifyDocChanged); } return Effect; } void LRichTextEdit::OnCreate() { SetWindow(this); DropTarget(true); if (Focus()) SetPulse(RTE_PULSE_RATE); if (d->SpellCheck) d->SpellCheck->EnumLanguages(AddDispatch()); } void LRichTextEdit::OnEscape(LKey &K) { } bool LRichTextEdit::OnMouseWheel(double l) { if (VScroll) { VScroll->Value(VScroll->Value() + (int64)l); Invalidate(); } return true; } void LRichTextEdit::OnFocus(bool f) { Invalidate(); SetPulse(f ? RTE_PULSE_RATE : -1); } ssize_t LRichTextEdit::HitTest(int x, int y) { int Line = -1; return d->HitTest(x, y, Line); } void LRichTextEdit::Undo() { if (d->UndoPos > 0) d->SetUndoPos(d->UndoPos - 1); } void LRichTextEdit::Redo() { if (d->UndoPos < (int)d->UndoQue.Length()) d->SetUndoPos(d->UndoPos + 1); } #ifdef _DEBUG class NodeView : public LWindow { public: LTree *Tree; NodeView(LViewI *w) { LRect r(0, 0, 500, 600); SetPos(r); MoveSameScreen(w); Attach(0); if ((Tree = new LTree(100, 0, 0, 100, 100))) { Tree->SetPourLargest(true); Tree->Attach(this); } } }; #endif void LRichTextEdit::DoContextMenu(LMouse &m) { LMenuItem *i; LSubMenu RClick; LAutoString ClipText; { LClipBoard Clip(this); ClipText.Reset(NewStr(Clip.Text())); } LRichTextPriv::Block *Over = NULL; LRect &Content = d->Areas[ContentArea]; LPoint Doc = d->ScreenToDoc(m.x, m.y); ssize_t Offset = -1, BlkOffset = -1; if (Content.Overlap(m.x, m.y)) { int LineHint; Offset = d->HitTest(Doc.x, Doc.y, LineHint, &Over, &BlkOffset); } if (Over) Over->DoContext(RClick, Doc, BlkOffset, true); RClick.AppendItem(LLoadString(L_TEXTCTRL_CUT, "Cut"), IDM_RTE_CUT, HasSelection()); RClick.AppendItem(LLoadString(L_TEXTCTRL_COPY, "Copy"), IDM_RTE_COPY, HasSelection()); RClick.AppendItem(LLoadString(L_TEXTCTRL_PASTE, "Paste"), IDM_RTE_PASTE, ClipText != 0); RClick.AppendSeparator(); RClick.AppendItem(LLoadString(L_TEXTCTRL_UNDO, "Undo"), IDM_RTE_UNDO, false /* UndoQue.CanUndo() */); RClick.AppendItem(LLoadString(L_TEXTCTRL_REDO, "Redo"), IDM_RTE_REDO, false /* UndoQue.CanRedo() */); RClick.AppendSeparator(); #if 0 i = RClick.AppendItem(LLoadString(L_TEXTCTRL_FIXED, "Fixed Width Font"), IDM_FIXED, true); if (i) i->Checked(GetFixedWidthFont()); #endif 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); LSubMenu *Src = RClick.AppendSub("Source"); if (Src) { Src->AppendItem("Copy Original", IDM_COPY_ORIGINAL, d->OriginalText.Get() != NULL); Src->AppendItem("Copy Current", IDM_COPY_CURRENT); #ifdef _DEBUG Src->AppendItem("Dump Nodes", IDM_DUMP_NODES); // Edit->DumpNodes(Tree); #endif } if (Over) { #ifdef _DEBUG // RClick.AppendItem(Over->GetClass(), -1, false); #endif Over->DoContext(RClick, Doc, BlkOffset, false); } if (Environment) Environment->AppendItems(&RClick, NULL); int Id = 0; m.ToScreen(); switch (Id = RClick.Float(this, m.x, m.y)) { case IDM_FIXED: { SetFixedWidthFont(!GetFixedWidthFont()); SendNotify(LNotifyFixedWidthChanged); break; } case IDM_RTE_CUT: { Cut(); break; } case IDM_RTE_COPY: { Copy(); break; } case IDM_RTE_PASTE: { Paste(); break; } case IDM_RTE_UNDO: { Undo(); break; } case IDM_RTE_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); auto i = new LInput(this, s, "Indent Size:", "Text"); i->DoModal([this, i](auto dlg, auto ctrlId) { if (ctrlId == IDOK) { IndentSize = (uint8_t)i->GetStr().Int(); Invalidate(); } delete dlg; }); break; } case IDM_TAB_SIZE: { char s[32]; sprintf_s(s, sizeof(s), "%i", TabSize); auto i = new LInput(this, s, "Tab Size:", "Text"); i->DoModal([this, i](auto dlg, auto ok) { if (ok) SetTabSize((uint8_t)i->GetStr().Int()); delete dlg; }); break; } case IDM_COPY_ORIGINAL: { LClipBoard c(this); c.Text(d->OriginalText); break; } case IDM_COPY_CURRENT: { LClipBoard c(this); c.Text(Name()); break; } case IDM_DUMP_NODES: { #ifdef _DEBUG NodeView *nv = new NodeView(GetWindow()); DumpNodes(nv->Tree); nv->Visible(true); #endif break; } default: { if (Over) { LMessage Cmd(M_COMMAND, Id); if (Over->OnEvent(&Cmd)) break; } if (Environment) Environment->OnMenu(this, Id, 0); break; } } } void LRichTextEdit::OnMouseClick(LMouse &m) { bool Processed = false; RectType Clicked = d->PosToButton(m); if (m.Down()) { Focus(true); if (m.IsContextMenu()) { DoContextMenu(m); return; } else { Focus(true); if (d->Areas[ToolsArea].Overlap(m.x, m.y) // || d->Areas[CapabilityArea].Overlap(m.x, m.y) ) { if (Clicked != MaxArea) { if (d->BtnState[Clicked].IsPress) { d->BtnState[d->ClickedBtn = Clicked].Pressed = true; Invalidate(d->Areas + Clicked); Capture(true); } else { Processed |= d->ClickBtn(m, Clicked); } } } else { d->WordSelectMode = !Processed && m.Double(); AutoCursor c(new BlkCursor(NULL, 0, 0)); LPoint Doc = d->ScreenToDoc(m.x, m.y); ssize_t Idx = -1; if (d->CursorFromPos(Doc.x, Doc.y, &c, &Idx)) { d->ClickedBtn = ContentArea; d->SetCursor(c, m.Shift()); if (d->WordSelectMode) SelectWord(Idx); } } } } else if (IsCapturing()) { Capture(false); if (d->ClickedBtn != MaxArea) { d->BtnState[d->ClickedBtn].Pressed = false; Invalidate(d->Areas + d->ClickedBtn); Processed |= d->ClickBtn(m, Clicked); } d->ClickedBtn = MaxArea; } if (!Processed) { Capture(m.Down()); } } int LRichTextEdit::OnHitTest(int x, int y) { #ifdef WIN32 if (GetClient().Overlap(x, y)) { return HTCLIENT; } #endif return LView::OnHitTest(x, y); } void LRichTextEdit::OnMouseMove(LMouse &m) { LRichTextEdit::RectType OverBtn = d->PosToButton(m); if (d->OverBtn != OverBtn) { if (d->OverBtn < MaxArea) { d->BtnState[d->OverBtn].MouseOver = false; Invalidate(&d->Areas[d->OverBtn]); } d->OverBtn = OverBtn; if (d->OverBtn < MaxArea) { d->BtnState[d->OverBtn].MouseOver = true; Invalidate(&d->Areas[d->OverBtn]); } } if (IsCapturing()) { if (d->ClickedBtn == ContentArea) { AutoCursor c; LPoint Doc = d->ScreenToDoc(m.x, m.y); ssize_t Idx = -1; if (d->CursorFromPos(Doc.x, Doc.y, &c, &Idx) && c) { if (d->WordSelectMode && d->Selection) { // Extend the selection to include the whole word if (!d->CursorFirst()) { // Extend towards the end of the doc... LArray Txt; if (c->Blk->CopyAt(0, c->Blk->Length(), &Txt)) { while ( c->Offset < (int)Txt.Length() && !IsWordBreakChar(Txt[c->Offset]) ) c->Offset++; } } else { // Extend towards the start of the doc... LArray Txt; if (c->Blk->CopyAt(0, c->Blk->Length(), &Txt)) { while ( c->Offset > 0 && !IsWordBreakChar(Txt[c->Offset-1]) ) c->Offset--; } } } d->SetCursor(c, m.Left()); } } } #ifdef WIN32 LRect c = GetClient(); c.Offset(-c.x1, -c.y1); if (c.Overlap(m.x, m.y)) { /* LStyle *s = HitStyle(Hit); TCHAR *c = (s) ? s->GetCursor() : 0; if (!c) c = IDC_IBEAM; ::SetCursor(LoadCursor(0, MAKEINTRESOURCE(c))); */ } #endif } bool LRichTextEdit::OnKey(LKey &k) { if (k.Down() && d->Cursor) d->Cursor->Blink = true; if (k.c16 == 17) return false; #ifdef WINDOWS // Wtf is this? // Weeeelll, windows likes to send a LK_TAB after a Ctrl+I doesn't it? // And this just takes care of that TAB before it can overwrite your // selection. if (ToLower(k.c16) == 'i' && k.Ctrl()) { d->EatVkeys.Add(LK_TAB); } else if (d->EatVkeys.Length()) { auto Idx = d->EatVkeys.IndexOf(k.vkey); if (Idx >= 0) { // Yum yum d->EatVkeys.DeleteAt(Idx); return true; } } #endif // k.Trace("LRichTextEdit::OnKey"); if (k.IsContextMenu()) { LMouse m; 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() && d->Cursor && d->Cursor->Blk) { // letter/number etc LRichTextPriv::Block *b = d->Cursor->Blk; AutoTrans Trans(new LRichTextPriv::Transaction); d->DeleteSelection(Trans, NULL); LNamedStyle *AddStyle = NULL; if (d->StyleDirty.Length() > 0) { LAutoPtr Mod(new LCss); if (Mod) { // Get base styles at the cursor.. LNamedStyle *Base = b->GetStyle(d->Cursor->Offset); if (Base) *Mod = *Base; // Apply dirty toolbar styles... if (d->StyleDirty.HasItem(FontFamilyBtn)) Mod->FontFamily(LCss::FontFamilies(d->Values[FontFamilyBtn].Str())); if (d->StyleDirty.HasItem(FontSizeBtn)) Mod->FontSize(LCss::Len(LCss::LenPt, (float) d->Values[FontSizeBtn].CastDouble())); if (d->StyleDirty.HasItem(BoldBtn)) Mod->FontWeight(d->Values[BoldBtn].CastInt32() ? LCss::FontWeightBold : LCss::FontWeightNormal); if (d->StyleDirty.HasItem(ItalicBtn)) Mod->FontStyle(d->Values[ItalicBtn].CastInt32() ? LCss::FontStyleItalic : LCss::FontStyleNormal); if (d->StyleDirty.HasItem(UnderlineBtn)) Mod->TextDecoration(d->Values[UnderlineBtn].CastInt32() ? LCss::TextDecorUnderline : LCss::TextDecorNone); if (d->StyleDirty.HasItem(ForegroundColourBtn)) Mod->Color(LCss::ColorDef(LCss::ColorRgb, (uint32_t)d->Values[ForegroundColourBtn].CastInt64())); if (d->StyleDirty.HasItem(BackgroundColourBtn)) Mod->BackgroundColor(LCss::ColorDef(LCss::ColorRgb, (uint32_t)d->Values[BackgroundColourBtn].CastInt64())); AddStyle = d->AddStyleToCache(Mod); } d->StyleDirty.Length(0); } else if (b->Length() == 0) { // We have no existing style to modify, so create one from scratch. LAutoPtr Mod(new LCss); if (Mod) { // Apply dirty toolbar styles... Mod->FontFamily(LCss::FontFamilies(d->Values[FontFamilyBtn].Str())); Mod->FontSize(LCss::Len(LCss::LenPt, (float)d->Values[FontSizeBtn].CastDouble())); Mod->FontWeight(d->Values[BoldBtn].CastInt32() ? LCss::FontWeightBold : LCss::FontWeightNormal); Mod->FontStyle(d->Values[ItalicBtn].CastInt32() ? LCss::FontStyleItalic : LCss::FontStyleNormal); Mod->TextDecoration(d->Values[UnderlineBtn].CastInt32() ? LCss::TextDecorUnderline : LCss::TextDecorNone); Mod->Color(LCss::ColorDef(LCss::ColorRgb, (uint32_t)d->Values[ForegroundColourBtn].CastInt64())); auto Bk = d->Values[BackgroundColourBtn].CastInt64(); if (Bk > 0) Mod->BackgroundColor(LCss::ColorDef(LCss::ColorRgb, (uint32_t)Bk)); AddStyle = d->AddStyleToCache(Mod); } } uint32_t Ch = k.c16; if (b->AddText(Trans, d->Cursor->Offset, &Ch, 1, AddStyle)) { d->Cursor->Set(d->Cursor->Offset + 1); Invalidate(); SendNotify(LNotifyDocChanged); d->AddTrans(Trans); } } return true; } break; } case LK_RETURN: { if (GetReadOnly()) break; if (k.Down() && k.IsChar) { OnEnter(k); SendNotify(LNotifyDocChanged); } return true; } case LK_BACKSPACE: { if (GetReadOnly()) break; bool Changed = false; AutoTrans Trans(new LRichTextPriv::Transaction); if (k.Ctrl()) { // Ctrl+H } else if (k.Down()) { LRichTextPriv::Block *b; if (HasSelection()) { Changed = d->DeleteSelection(Trans, NULL); } else if (d->Cursor && (b = d->Cursor->Blk)) { if (d->Cursor->Offset > 0) { Changed = b->DeleteAt(Trans, d->Cursor->Offset-1, 1) > 0; if (Changed) { // Has block size reached 0? if (b->Length() == 0) { // Then delete it... LRichTextPriv::Block *n = d->Next(b); if (n) { d->Blocks.Delete(b, true); d->Cursor.Reset(new LRichTextPriv::BlockCursor(n, 0, 0)); } else { // No other block to go to, so leave this empty block at the end // of the documnent but set the cursor correctly. d->Cursor->Set(0); } } else { d->Cursor->Set(d->Cursor->Offset - 1); } } } else { // At the start of a block: LRichTextPriv::Block *Prev = d->Prev(d->Cursor->Blk); if (Prev) { // Try and merge the two blocks... ssize_t Len = Prev->Length(); d->Merge(Trans, Prev, d->Cursor->Blk); AutoCursor c(new BlkCursor(Prev, Len, -1)); d->SetCursor(c); } else // at the start of the doc... { // Don't send the doc changed... return true; } } } } if (Changed) { Invalidate(); d->AddTrans(Trans); SendNotify(LNotifyDocChanged); } return true; } } } 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()) { // Implement delete by word LAssert(!"Impl backspace by word"); } } return true; } break; } case LK_F3: { if (k.Down()) DoFindNext(NULL); return true; } case LK_LEFT: { #ifdef MAC if (k.Ctrl()) #else if (k.Alt()) #endif return false; if (k.Down()) { if (HasSelection() && !k.Shift()) { LRect r = d->SelectionRect(); Invalidate(&r); AutoCursor c(new BlkCursor(d->CursorFirst() ? *d->Cursor : *d->Selection)); d->SetCursor(c); } else { #ifdef MAC if (k.System()) goto Jump_StartOfLine; else #endif d->Seek(d->Cursor, #ifdef MAC k.Alt() ? #else k.Ctrl() ? #endif LRichTextPriv::SkLeftWord : LRichTextPriv::SkLeftChar, k.Shift()); } } return true; } case LK_RIGHT: { #ifdef MAC if (k.Ctrl()) #else if (k.Alt()) #endif return false; if (k.Down()) { if (HasSelection() && !k.Shift()) { LRect r = d->SelectionRect(); Invalidate(&r); AutoCursor c(new BlkCursor(d->CursorFirst() ? *d->Selection : *d->Cursor)); d->SetCursor(c); } else { #ifdef MAC if (k.System()) goto Jump_EndOfLine; #endif d->Seek(d->Cursor, #ifdef MAC k.Alt() ? #else k.Ctrl() ? #endif LRichTextPriv::SkRightWord : LRichTextPriv::SkRightChar, k.Shift()); } } return true; } case LK_UP: { if (k.Alt()) return false; if (k.Down()) { #ifdef MAC if (k.Ctrl()) goto GTextView4_PageUp; #endif d->Seek(d->Cursor, LRichTextPriv::SkUpLine, k.Shift()); } return true; } case LK_DOWN: { if (k.Alt()) return false; if (k.Down()) { #ifdef MAC if (k.Ctrl()) goto GTextView4_PageDown; #endif d->Seek(d->Cursor, LRichTextPriv::SkDownLine, k.Shift()); } return true; } case LK_END: { if (k.Down()) { #ifdef MAC if (!k.Ctrl()) Jump_EndOfLine: #endif d->Seek(d->Cursor, k.Ctrl() ? LRichTextPriv::SkDocEnd : LRichTextPriv::SkLineEnd, k.Shift()); } return true; } case LK_HOME: { if (k.Down()) { #ifdef MAC if (!k.Ctrl()) Jump_StartOfLine: #endif d->Seek(d->Cursor, k.Ctrl() ? LRichTextPriv::SkDocStart : LRichTextPriv::SkLineStart, k.Shift()); } return true; } case LK_PAGEUP: { #ifdef MAC GTextView4_PageUp: #endif if (k.Down()) { d->Seek(d->Cursor, LRichTextPriv::SkUpPage, k.Shift()); } return true; break; } case LK_PAGEDOWN: { #ifdef MAC GTextView4_PageDown: #endif if (k.Down()) { d->Seek(d->Cursor, LRichTextPriv::SkDownPage, 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()) break; if (!k.Down()) return true; bool Changed = false; LRichTextPriv::Block *b; AutoTrans Trans(new LRichTextPriv::Transaction); if (HasSelection()) { if (k.Shift()) Changed |= Cut(); else Changed |= d->DeleteSelection(Trans, NULL); } else if (d->Cursor && (b = d->Cursor->Blk)) { if (d->Cursor->Offset >= b->Length()) { // Cursor is at the end of this block, pull the styles // from the next block into this one. LRichTextPriv::Block *next = d->Next(b); if (!next) { // No next block, therefor nothing to delete break; } // Try and merge the blocks if (d->Merge(Trans, b, next)) Changed = true; else { // If the cursor is on the last empty line of a text block, // we should delete that '\n' first LRichTextPriv::TextBlock *tb = dynamic_cast(b); if (tb && tb->IsEmptyLine(d->Cursor)) Changed = tb->StripLast(Trans); // move the cursor to the next block d->Cursor.Reset(new LRichTextPriv::BlockCursor(b = next, 0, 0)); } } if (!Changed && b->DeleteAt(Trans, d->Cursor->Offset, 1)) { if (b->Length() == 0) { LRichTextPriv::Block *n = d->Next(b); if (n) { d->Blocks.Delete(b, true); d->Cursor.Reset(new LRichTextPriv::BlockCursor(n, 0, 0)); } } Changed = true; } } if (Changed) { Invalidate(); d->AddTrans(Trans); SendNotify(LNotifyDocChanged); } return true; } default: { if (k.c16 == 17) break; if (k.c16 == ' ' && k.Ctrl() && k.Alt() && d->Cursor && d->Cursor->Blk) { if (k.Down()) { // letter/number etc LRichTextPriv::Block *b = d->Cursor->Blk; uint32_t Nbsp[] = {0xa0}; if (b->AddText(NoTransaction, d->Cursor->Offset, Nbsp, 1)) { d->Cursor->Set(d->Cursor->Offset + 1); Invalidate(); SendNotify(LNotifyDocChanged); } } 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 SelectAll(); } return true; break; } case 'b': case 'B': { if (k.Down()) { // Bold selection LMouse m; GetMouse(m); d->ClickBtn(m, BoldBtn); } return true; break; } case 'l': case 'L': { if (k.Down()) { // Underline selection LMouse m; GetMouse(m); d->ClickBtn(m, UnderlineBtn); } return true; break; } case 'i': case 'I': { if (k.Down()) { // Italic selection LMouse m; GetMouse(m); d->ClickBtn(m, ItalicBtn); } 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; } 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 LRichTextEdit::OnEnter(LKey &k) { AutoTrans Trans(new LRichTextPriv::Transaction); // Enter key handling bool Changed = false; if (HasSelection()) Changed |= d->DeleteSelection(Trans, NULL); if (d->Cursor && d->Cursor->Blk) { LRichTextPriv::Block *b = d->Cursor->Blk; const uint32_t Nl[] = {'\n'}; if (b->AddText(Trans, d->Cursor->Offset, Nl, 1)) { d->Cursor->Set(d->Cursor->Offset + 1); Changed = true; } else { // Some blocks don't take text. However a new block can be created or // the text added to the start of the next block if (d->Cursor->Offset == 0) { LRichTextPriv::Block *Prev = d->Prev(b); if (Prev) Changed = Prev->AddText(Trans, Prev->Length(), Nl, 1); else // No previous... must by first block... create new block: { LRichTextPriv::TextBlock *tb = new LRichTextPriv::TextBlock(d); if (tb) { Changed = true; // tb->AddText(Trans, 0, Nl, 1); d->Blocks.AddAt(0, tb); } } } else if (d->Cursor->Offset == b->Length()) { LRichTextPriv::Block *Next = d->Next(b); if (Next) { if ((Changed = Next->AddText(Trans, 0, Nl, 1))) d->Cursor->Set(Next, 0, -1); } else // No next block. Create one: { LRichTextPriv::TextBlock *tb = new LRichTextPriv::TextBlock(d); if (tb) { Changed = true; // tb->AddText(Trans, 0, Nl, 1); d->Blocks.Add(tb); } } } } } if (Changed) { Invalidate(); d->AddTrans(Trans); SendNotify(LNotifyDocChanged); } } void LRichTextEdit::OnPaintLeftMargin(LSurface *pDC, LRect &r, LColour &colour) { pDC->Colour(colour); pDC->Rectangle(&r); } void LRichTextEdit::OnPaint(LSurface *pDC) { LRect r = GetClient(); if (!r.Valid()) return; #if 0 pDC->Colour(LColour(255, 0, 255)); pDC->Rectangle(); #endif int FontY = GetFont()->GetHeight(); LCssTools ct(d, d->Font); r = ct.PaintBorder(pDC, r); bool HasSpace = r.Y() > (FontY * 3); if (d->ShowTools && HasSpace) { d->Areas[ToolsArea] = r; d->Areas[ToolsArea].y2 = d->Areas[ToolsArea].y1 + (FontY + 8) - 1; r.y1 = d->Areas[ToolsArea].y2 + 1; } else { d->Areas[ToolsArea].ZOff(-1, -1); } d->Areas[ContentArea] = r; if (d->Layout(VScroll)) d->Paint(pDC, VScroll); // else the scroll bars changed, wait for re-paint } LMessage::Result LRichTextEdit::OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_CUT: { Cut(); break; } case M_COPY: { Copy(); break; } case M_PASTE: { Paste(); break; } case M_BLOCK_MSG: { - LRichTextPriv::Block *b = (LRichTextPriv::Block*)Msg->A(); + auto b = (LRichTextPriv::Block*)Msg->A(); LAutoPtr msg((LMessage*)Msg->B()); if (d->Blocks.HasItem(b) && msg) { b->OnEvent(msg); } else printf("%s:%i - No block to receive M_BLOCK_MSG.\n", _FL); break; } case M_ENUMERATE_LANGUAGES: { LAutoPtr< LArray > Languages((LArray*)Msg->A()); if (!Languages) { LgiTrace("%s:%i - M_ENUMERATE_LANGUAGES no param\n", _FL); break; } // LgiTrace("%s:%i - Got M_ENUMERATE_LANGUAGES %s\n", _FL, d->SpellLang.Get()); bool Match = false; for (auto &s: *Languages) { if (s.LangCode.Equals(d->SpellLang) || s.EnglishName.Equals(d->SpellLang)) { // LgiTrace("%s:%i - EnumDict called %s\n", _FL, s.LangCode.Get()); d->SpellCheck->EnumDictionaries(AddDispatch(), s.LangCode); Match = true; break; } } if (!Match) LgiTrace("%s:%i - EnumDict not called %s\n", _FL, d->SpellLang.Get()); break; } case M_ENUMERATE_DICTIONARIES: { LAutoPtr< LArray > Dictionaries((LArray*)Msg->A()); if (!Dictionaries) break; bool Match = false; for (auto &s: *Dictionaries) { // LgiTrace("%s:%i - M_ENUMERATE_DICTIONARIES: %s, %s\n", _FL, s.Dict.Get(), d->SpellDict.Get()); if (s.Dict.Equals(d->SpellDict)) { d->SpellCheck->SetDictionary(AddDispatch(), s.Lang, s.Dict); Match = true; break; } } if (!Match) d->SpellCheck->SetDictionary(AddDispatch(), d->SpellLang, NULL); break; } case M_SET_DICTIONARY: { d->SpellDictionaryLoaded = Msg->A() != 0; // LgiTrace("%s:%i - M_SET_DICTIONARY=%i\n", _FL, d->SpellDictionaryLoaded); if (d->SpellDictionaryLoaded) { AutoTrans Trans(new LRichTextPriv::Transaction); // Get any loaded text blocks to check their spelling bool Status = false; for (unsigned i=0; iBlocks.Length(); i++) { Status |= d->Blocks[i]->OnDictionary(Trans); } if (Status) d->AddTrans(Trans); } break; } case M_CHECK_TEXT: { LAutoPtr Ct((LSpellCheck::CheckText*)Msg->A()); if (!Ct || Ct->User.Length() > 1) { LAssert(0); break; } LRichTextPriv::Block *b = (LRichTextPriv::Block*)Ct->User[SpellBlockPtr].CastVoidPtr(); if (!d->Blocks.HasItem(b)) break; b->SetSpellingErrors(Ct->Errors, *Ct); Invalidate(); break; } #if defined WIN32 case WM_GETTEXTLENGTH: { return 0 /*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; } case M_COMPONENT_INSTALLED: { LAutoPtr Comp((LString*)Msg->A()); if (Comp) d->OnComponentInstall(*Comp); break; } /* 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 LRichTextEdit::OnNotify(LViewI *Ctrl, LNotification n) { if (Ctrl->GetId() == IDC_VSCROLL && VScroll) { Invalidate(d->Areas + ContentArea); } return 0; } void LRichTextEdit::OnPulse() { if (!ReadOnly && d->Cursor) { uint64 n = LCurrentTime(); if (d->BlinkTs - n >= RTE_CURSOR_BLINK_RATE) { d->BlinkTs = n; d->Cursor->Blink = !d->Cursor->Blink; d->InvalidateDoc(&d->Cursor->Pos); } // Do autoscroll while the user has clicked and dragged off the control: if (VScroll && IsCapturing() && d->ClickedBtn == LRichTextEdit::ContentArea) { LMouse m; GetMouse(m); // Is the mouse outside the content window LRect &r = d->Areas[ContentArea]; if (!r.Overlap(m.x, m.y)) { AutoCursor c(new BlkCursor(NULL, 0, 0)); LPoint Doc = d->ScreenToDoc(m.x, m.y); ssize_t Idx = -1; if (d->CursorFromPos(Doc.x, Doc.y, &c, &Idx)) { d->SetCursor(c, true); if (d->WordSelectMode) SelectWord(Idx); } // Update the screen. d->InvalidateDoc(NULL); } } } } void LRichTextEdit::OnUrl(char *Url) { if (Environment) { Environment->OnNavigate(this, Url); } } bool LRichTextEdit::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; } #if _DEBUG void LRichTextEdit::SelectNode(LString Param) { LRichTextPriv::Block *b = (LRichTextPriv::Block*) Param.Int(16); bool Valid = false; for (auto i : d->Blocks) { if (i == b) Valid = true; i->DrawDebug = false; } if (Valid) { b->DrawDebug = true; Invalidate(); } } void LRichTextEdit::DumpNodes(LTree *Root) { d->DumpNodes(Root); } #endif /////////////////////////////////////////////////////////////////////////////// SelectColour::SelectColour(LRichTextPriv *priv, LPoint p, LRichTextEdit::RectType t) : LPopup(priv->View) { d = priv; Type = t; int Px = 16; int PxSp = Px + 2; int x = 6; int y = 6; // Do grey ramp for (int i=0; i<8; i++) { Entry &en = e.New(); int Grey = i * 255 / 7; en.r.ZOff(Px-1, Px-1); en.r.Offset(x + (i * PxSp), y); en.c.Rgb(Grey, Grey, Grey); } // Do colours y += PxSp + 4; int SatRange = 255 - 64; int SatStart = 255 - 32; int HueStep = 360 / 8; for (int sat=0; sat<8; sat++) { for (int hue=0; hue<8; hue++) { LColour c; c.SetHLS(hue * HueStep, SatStart - ((sat * SatRange) / 7), 255); c.ToRGB(); Entry &en = e.New(); en.r.ZOff(Px-1, Px-1); en.r.Offset(x + (hue * PxSp), y); en.c = c; } y += PxSp; } SetParent(d->View); LRect r(0, 0, 12 + (8 * PxSp) - 1, y + 6 - 1); r.Offset(p.x, p.y); SetPos(r); Visible(true); } void SelectColour::OnPaint(LSurface *pDC) { pDC->Colour(L_MED); pDC->Rectangle(); for (unsigned i=0; iColour(e[i].c); pDC->Rectangle(&e[i].r); } } void SelectColour::OnMouseClick(LMouse &m) { if (m.Down()) { for (unsigned i=0; iValues[Type] = (int64)e[i].c.c32(); d->View->Invalidate(d->Areas + Type); d->OnStyleChange(Type); Visible(false); break; } } } } void SelectColour::Visible(bool i) { LPopup::Visible(i); if (!i) { d->View->Focus(true); delete this; } } /////////////////////////////////////////////////////////////////////////////// #define EMOJI_PAD 2 #include "lgi/common/Emoji.h" int EmojiMenu::Cur = 0; EmojiMenu::EmojiMenu(LRichTextPriv *priv, LPoint p) : LPopup(priv->View) { d = priv; d->GetEmojiImage(); int MaxIdx = 0; LHashTbl, int> Map; for (int b=0; b= 0) { Map.Add(Emoji.Index, u); MaxIdx = MAX(MaxIdx, Emoji.Index); } } } int Sz = EMOJI_CELL_SIZE - 1; int PaneCount = 5; int PaneSz = (int)(Map.Length() / PaneCount); int ImgIdx = 0; int PaneSelectSz = LSysFont->GetHeight() * 2; int Rows = (PaneSz + EMOJI_GROUP_X - 1) / EMOJI_GROUP_X; LRect r(0, 0, (EMOJI_CELL_SIZE + EMOJI_PAD) * EMOJI_GROUP_X + EMOJI_PAD, (EMOJI_CELL_SIZE + EMOJI_PAD) * Rows + EMOJI_PAD + PaneSelectSz); r.Offset(p.x, p.y); SetPos(r); for (int pi = 0; pi < PaneCount; pi++) { Pane &p = Panes[pi]; int Wid = X() - (EMOJI_PAD*2); p.Btn.x1 = EMOJI_PAD + (pi * Wid / PaneCount); p.Btn.y1 = EMOJI_PAD; p.Btn.x2 = EMOJI_PAD + ((pi + 1) * Wid / PaneCount) - 1; p.Btn.y2 = EMOJI_PAD + PaneSelectSz; int Dx = EMOJI_PAD; int Dy = p.Btn.y2 + 1; while ((int)p.e.Length() < PaneSz && ImgIdx <= MaxIdx) { uint32_t u = Map.Find(ImgIdx); if (u) { Emoji &Ch = p.e.New(); Ch.u = u; int Sx = ImgIdx % EMOJI_GROUP_X; int Sy = ImgIdx / EMOJI_GROUP_X; Ch.Src.ZOff(Sz, Sz); Ch.Src.Offset(Sx * EMOJI_CELL_SIZE, Sy * EMOJI_CELL_SIZE); Ch.Dst.ZOff(Sz, Sz); Ch.Dst.Offset(Dx, Dy); Dx += EMOJI_PAD + EMOJI_CELL_SIZE; if (Dx + EMOJI_PAD + EMOJI_CELL_SIZE >= r.X()) { Dx = EMOJI_PAD; Dy += EMOJI_PAD + EMOJI_CELL_SIZE; } } ImgIdx++; } } SetParent(d->View); Visible(true); } void EmojiMenu::OnPaint(LSurface *pDC) { LAutoPtr DblBuf; if (!pDC->SupportsAlphaCompositing()) DblBuf.Reset(new LDoubleBuffer(pDC)); pDC->Colour(L_MED); pDC->Rectangle(); LSurface *EmojiImg = d->GetEmojiImage(); if (EmojiImg) { pDC->Op(GDC_ALPHA); for (unsigned i=0; iColour(L_LIGHT); pDC->Rectangle(&p.Btn); } LSysFont->Fore(L_TEXT); LSysFont->Transparent(true); Ds.Draw(pDC, p.Btn.x1 + ((p.Btn.X()-Ds.X())>>1), p.Btn.y1 + ((p.Btn.Y()-Ds.Y())>>1)); } Pane &p = Panes[Cur]; for (unsigned i=0; iBlt(g.Dst.x1, g.Dst.y1, EmojiImg, &g.Src); } } else { LRect c = GetClient(); LDisplayString Ds(LSysFont, "Loading..."); LSysFont->Colour(L_TEXT, L_MED); LSysFont->Transparent(true); Ds.Draw(pDC, (c.X()-Ds.X())>>1, (c.Y()-Ds.Y())>>1); } } bool EmojiMenu::InsertEmoji(uint32_t Ch) { if (!d->Cursor || !d->Cursor->Blk) return false; AutoTrans Trans(new LRichTextPriv::Transaction); if (!d->Cursor->Blk->AddText(NoTransaction, d->Cursor->Offset, &Ch, 1, NULL)) return false; AutoCursor c(new BlkCursor(*d->Cursor)); c->Offset++; d->SetCursor(c); d->AddTrans(Trans); d->Dirty = true; d->InvalidateDoc(NULL); d->View->SendNotify(LNotifyDocChanged); return true; } void EmojiMenu::OnMouseClick(LMouse &m) { if (m.Down()) { for (unsigned i=0; iView->Focus(true); delete this; } } /////////////////////////////////////////////////////////////////////////////// class LRichTextEdit_Factory : public LViewFactory { LView *NewView(const char *Class, LRect *Pos, const char *Text) { if (_stricmp(Class, "LRichTextEdit") == 0) { return new LRichTextEdit(-1, 0, 0, 2000, 2000); } return 0; } } RichTextEdit_Factory; diff --git a/src/common/Widgets/Editor/RichTextEditPriv.h b/src/common/Widgets/Editor/RichTextEditPriv.h --- a/src/common/Widgets/Editor/RichTextEditPriv.h +++ b/src/common/Widgets/Editor/RichTextEditPriv.h @@ -1,1392 +1,1420 @@ /* Rich text design notes: - The document is an array of Blocks (Blocks have no hierarchy) - Blocks have a length in characters. New lines are considered as one '\n' char. - The main type of block is the TextBlock - TextBlock contains: - array of StyleText: This is the source text. Each run of text has a style associated with it. This forms the input to the layout algorithm and is what the user is editing. - array of TextLine: Contains all the info needed to render one line of text. Essentially the output of the layout engine. Contains an array of DisplayStr objects. i.e. Characters in the exact same style as each other. It will regularly be deleted and re-flowed from the StyleText objects. - For a plaint text document the entire thing is contained by the one TextBlock. - There is an Image block, where the image is treated as one character object. - Also a horizontal rule block. */ #ifndef _RICH_TEXT_EDIT_PRIV_H_ #define _RICH_TEXT_EDIT_PRIV_H_ #include "lgi/common/HtmlCommon.h" #include "lgi/common/HtmlParser.h" #include "lgi/common/FontCache.h" #include "lgi/common/DisplayString.h" #include "lgi/common/ColourSpace.h" #include "lgi/common/Popup.h" #include "lgi/common/Emoji.h" #include "lgi/common/SpellCheck.h" #define DEBUG_LOG_CURSOR_COUNT 0 #define DEBUG_OUTLINE_CUR_DISPLAY_STR 0 #define DEBUG_OUTLINE_CUR_STYLE_TEXT 0 #define DEBUG_OUTLINE_BLOCKS 0 #define DEBUG_NO_DOUBLE_BUF 0 #define DEBUG_COVERAGE_CHECK 0 #define DEBUG_NUMBERED_LAYOUTS 0 #if 0 // _DEBUG #define LOG_FN LgiTrace #else #define LOG_FN d->Log->Print #endif #define TEXT_LINK "Link" #define TEXT_REMOVE_LINK "X" #define TEXT_REMOVE_STYLE "Remove Style" #define TEXT_CAP_BTN "Ok" #define TEXT_EMOJI ":)" #define TEXT_HORZRULE "HR" #define RTE_CURSOR_BLINK_RATE 1000 #define RTE_PULSE_RATE 200 #define RICH_TEXT_RESIZED_JPEG_QUALITY 83 // out of 100, high = better quality #define NoTransaction NULL #define IsWordBreakChar(ch) \ ( \ ( \ (ch) == ' ' || (ch) == '\t' || (ch) == '\r' || (ch) == '\n' \ ) \ || \ ( \ EmojiToIconIndex(&(ch), 1).Index >= 0 \ ) \ ) enum RteCommands { // IDM_OPEN = 10, IDM_NEW = 2000, IDM_RTE_COPY, IDM_RTE_CUT, IDM_RTE_PASTE, IDM_RTE_UNDO, IDM_RTE_REDO, IDM_COPY_URL, 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_ORIGINAL, IDM_COPY_CURRENT, IDM_DUMP_NODES, IDM_CLOCKWISE, IDM_ANTI_CLOCKWISE, IDM_X_FLIP, IDM_Y_FLIP, IDM_SCALE_IMAGE, IDM_OPEN_URL, CODEPAGE_BASE = 100, CONVERT_CODEPAGE_BASE = 200, SPELLING_BASE = 300 }; ////////////////////////////////////////////////////////////////////// #define PtrCheckBreak(ptr) if (!ptr) { LAssert(!"Invalid ptr"); break; } #undef FixedToInt #define FixedToInt(fixed) ((fixed)>>LDisplayString::FShift) #undef IntToFixed #define IntToFixed(val) ((val)<, LString> Attr; public: LRichEditElem(LHtmlElement *parent) : LHtmlElement(parent) { } bool Get(const char *attr, const char *&val) { if (!attr) return false; LString s = Attr.Find(attr); if (!s) return false; val = s; return true; } void Set(const char *attr, const char *val) { if (!attr) return; Attr.Add(attr, LString(val)); } void SetStyle() { } }; struct LRichEditElemContext : public LCss::ElementCallback { /// Returns the element name const char *GetElement(LRichEditElem *obj); /// Returns the document unque element ID const char *GetAttr(LRichEditElem *obj, const char *Attr); /// Returns the class bool GetClasses(LString::Array &Classes, LRichEditElem *obj); /// Returns the parent object LRichEditElem *GetParent(LRichEditElem *obj); /// Returns an array of child objects LArray GetChildren(LRichEditElem *obj); }; class LDocFindReplaceParams3 : public LDocFindReplaceParams { public: // Find/Replace History char16 *LastFind; char16 *LastReplace; bool MatchCase; bool MatchWord; bool SelectionOnly; LDocFindReplaceParams3() { LastFind = 0; LastReplace = 0; MatchCase = false; MatchWord = false; SelectionOnly = false; } ~LDocFindReplaceParams3() { DeleteArray(LastFind); DeleteArray(LastReplace); } }; struct LNamedStyle : public LCss { int RefCount = 0; LString Name; }; class LCssCache { int Idx; LArray Styles; LString Prefix; public: LCssCache(); ~LCssCache(); void SetPrefix(LString s) { Prefix = s; } uint32_t GetStyles(); void ZeroRefCounts(); bool OutputStyles(LStream &s, int TabDepth); LNamedStyle *AddStyleToCache(LAutoPtr &s); }; class LRichTextPriv; class SelectColour : public LPopup { LRichTextPriv *d; LRichTextEdit::RectType Type; struct Entry { LRect r; LColour c; }; LArray e; public: SelectColour(LRichTextPriv *priv, LPoint p, LRichTextEdit::RectType t); const char *GetClass() { return "SelectColour"; } void OnPaint(LSurface *pDC); void OnMouseClick(LMouse &m); void Visible(bool i); }; class EmojiMenu : public LPopup { LRichTextPriv *d; struct Emoji { LRect Src, Dst; uint32_t u; }; struct Pane { LRect Btn; LArray e; }; LArray Panes; static int Cur; public: EmojiMenu(LRichTextPriv *priv, LPoint p); void OnPaint(LSurface *pDC); void OnMouseClick(LMouse &m); void Visible(bool i); bool InsertEmoji(uint32_t Ch); }; struct CtrlCap { LString Name, Param; void Set(const char *name, const char *param) { Name = name; Param = param; } }; struct ButtonState { uint8_t IsMenu : 1; uint8_t IsPress : 1; uint8_t Pressed : 1; uint8_t MouseOver : 1; }; extern bool Utf16to32(LArray &Out, const uint16_t *In, ssize_t Len); class LEmojiImage { LAutoPtr EmojiImg; public: LSurface *GetEmojiImage(); }; class LRichTextPriv : public LCss, public LHtmlParser, public LHtmlStaticInst, public LCssCache, public LFontCache, public LEmojiImage { LStringPipe LogBuffer; public: enum SelectModeType { Unselected = 0, Selected = 1, }; enum SeekType { SkUnknown, SkLineStart, SkLineEnd, SkDocStart, SkDocEnd, // Horizontal navigation SkLeftChar, SkLeftWord, SkRightChar, SkRightWord, // Vertical navigation SkUpPage, SkUpLine, SkCurrentLine, SkDownLine, SkDownPage, }; struct DisplayStr; struct BlockCursor; class Block; LRichTextEdit *View; LString OriginalText; LAutoWString WideNameCache; LAutoString UtfNameCache; LAutoPtr Font; bool WordSelectMode; bool Dirty; LPoint DocumentExtent; // Px LString Charset; LHtmlStaticInst Inst; int NextUid; LStream *Log; bool HtmlLinkAsCid; uint64 BlinkTs; // Spell check support LSpellCheck *SpellCheck; bool SpellDictionaryLoaded; LString SpellLang, SpellDict; // This is set when the user changes a style without a selection, // indicating that we should start a new run when new text is entered LArray StyleDirty; // Toolbar bool ShowTools; LRichTextEdit::RectType ClickedBtn, OverBtn; ButtonState BtnState[LRichTextEdit::MaxArea]; LRect Areas[LRichTextEdit::MaxArea]; LVariant Values[LRichTextEdit::MaxArea]; // Scrolling int ScrollLinePx; int ScrollOffsetPx; bool ScrollChange; // Eat keys (OS bug work arounds) LArray EatVkeys; // Debug stuff LArray DebugRects; // Constructor LRichTextPriv(LRichTextEdit *view, LRichTextPriv **Ptr); ~LRichTextPriv(); bool Error(const char *file, int line, const char *fmt, ...); bool IsBusy(bool Stop = false); struct Flow { LRichTextPriv *d; LSurface *pDC; // Used for printing. int Left, Right;// Left and right margin positions as measured in px // from the left of the page (controls client area). int Top; int CurY; // Current y position down the page in document co-ords bool Visible; // true if the current block overlaps the visible page // If false, the implementation can take short cuts and // guess various dimensions. Flow(LRichTextPriv *priv) { d = priv; pDC = NULL; Left = 0; Top = 0; Right = 1000; CurY = 0; Visible = true; } int X() { return Right - Left + 1; } LString Describe() { LString s; s.Printf("Left=%i Right=%i CurY=%i", Left, Right, CurY); return s; } }; struct ColourPair { LColour Fore, Back; void Empty() { Fore.Empty(); Back.Empty(); } }; /// This is a run of text, all of the same style class StyleText : public LArray { LNamedStyle *Style = NULL; // owned by the CSS cache public: ColourPair Colours; HtmlTag Element; LString Param; bool Emoji; StyleText(const StyleText *St); StyleText(const uint32_t *t = NULL, ssize_t Chars = -1, LNamedStyle *style = NULL); uint32_t *At(ssize_t i); LNamedStyle *GetStyle(); void SetStyle(LNamedStyle *s); }; struct PaintContext { int Index; LSurface *pDC; SelectModeType Type; ColourPair Colours[2]; BlockCursor *Cursor, *Select; // Cursor stuff int CurEndPoint; LArray EndPoints; PaintContext() { Index = 0; pDC = NULL; Type = Unselected; Cursor = NULL; Select = NULL; CurEndPoint = 0; } LColour &Fore() { return Colours[Type].Fore; } LColour &Back() { return Colours[Type].Back; } void DrawBox(LRect &r, LRect &Edge, LColour &c) { if (Edge.x1 > 0 || Edge.x2 > 0 || Edge.y1 > 0 || Edge.y2 > 0) { pDC->Colour(c); if (Edge.x1) { pDC->Rectangle(r.x1, r.y1, r.x1 + Edge.x1 - 1, r.y2); r.x1 += Edge.x1; } if (Edge.y1) { pDC->Rectangle(r.x1, r.y1, r.x2, r.y1 + Edge.y1 - 1); r.y1 += Edge.y1; } if (Edge.y2) { pDC->Rectangle(r.x1, r.y2 - Edge.y2 + 1, r.x2, r.y2); r.y2 -= Edge.y2; } if (Edge.x2) { pDC->Rectangle(r.x2 - Edge.x2 + 1, r.y1, r.x2, r.y2); r.x2 -= Edge.x2; } } } // This handles calculating the selection stuff for simple "one char" blocks // like images and HR. Call this at the start of the OnPaint. // \return TRUE if the content should be drawn selected. bool SelectBeforePaint(class LRichTextPriv::Block *b) { CurEndPoint = 0; if (b->Cursors > 0 && Select) { // Selection end point checks... if (Cursor && Cursor->Blk == b) EndPoints.Add(Cursor->Offset); if (Select && Select->Blk == b) EndPoints.Add(Select->Offset); // Sort the end points if (EndPoints.Length() > 1 && EndPoints[0] > EndPoints[1]) { ssize_t ep = EndPoints[0]; EndPoints[0] = EndPoints[1]; EndPoints[1] = ep; } } // Before selection end point if (CurEndPoint < (ssize_t)EndPoints.Length() && EndPoints[CurEndPoint] == 0) { Type = Type == Selected ? Unselected : Selected; CurEndPoint++; } return Type == Selected; } // Call this after the OnPaint // \return TRUE if the content after the block is selected. bool SelectAfterPaint(class LRichTextPriv::Block *b) { // After image selection end point if (CurEndPoint < (ssize_t)EndPoints.Length() && EndPoints[CurEndPoint] == 1) { Type = Type == Selected ? Unselected : Selected; CurEndPoint++; } return Type == Selected; } }; struct HitTestResult { LPoint In; Block *Blk; DisplayStr *Ds; ssize_t Idx; int LineHint; bool Near; HitTestResult(int x, int y) { In.x = x; In.y = y; Blk = NULL; Ds = NULL; Idx = -1; LineHint = -1; Near = false; } }; ////////////////////////////////////////////////////////////////////////////////////////////// // Undo structures... struct DocChange { virtual ~DocChange() {} virtual bool Apply(LRichTextPriv *Ctx, bool Forward) = 0; }; class Transaction { public: LArray Changes; ~Transaction() { Changes.DeleteObjects(); } void Add(DocChange *Dc) { Changes.Add(Dc); } bool Apply(LRichTextPriv *Ctx, bool Forward) { for (unsigned i=0; iApply(Ctx, Forward)) return false; } return true; } }; LArray UndoQue; ssize_t UndoPos; bool UndoPosLock; bool AddTrans(LAutoPtr &t); bool SetUndoPos(ssize_t Pos); template bool GetBlockByUid(T *&Ptr, int Uid, int *Idx = NULL) { for (unsigned i=0; iGetUid() == Uid) { if (Idx) *Idx = i; return (Ptr = dynamic_cast(b)) != NULL; } } if (Idx) *Idx = -1; return false; } ////////////////////////////////////////////////////////////////////////////////////////////// // A Block is like a DIV in HTML, it's as wide as the page and // always starts and ends on a whole line. class Block : public LEventSinkI, public LEventTargetI { protected: int BlockUid; LRichTextPriv *d; public: /// This is the number of cursors current referencing this Block. int8 Cursors; /// Draw debug selection bool DrawDebug; Block(LRichTextPriv *priv) { d = priv; DrawDebug = false; BlockUid = d->NextUid++; Cursors = 0; } Block(const Block *blk) { d = blk->d; DrawDebug = false; BlockUid = blk->GetUid(); Cursors = 0; } virtual ~Block() { // We must have removed cursors by the time we are deleted // otherwise there will be a hanging pointer in the cursor // object. LAssert(Cursors == 0); } // Events bool PostEvent(int Cmd, LMessage::Param a = 0, LMessage::Param b = 0, int64_t TimeoutMs = -1) { bool r = d->View->PostEvent(M_BLOCK_MSG, (LMessage::Param)(Block*)this, (LMessage::Param)new LMessage(Cmd, a, b)); #if defined(_DEBUG) if (!r) LgiTrace("%s:%i - Warning: PostEvent failed..\n", _FL); #endif return r; } // If this returns non-zero further command processing is aborted. LMessage::Result OnEvent(LMessage *Msg) { return false; } /************************************************ * Get state methods, do not modify the block * ***********************************************/ virtual const char *GetClass() { return "Block"; } virtual LRect GetPos() = 0; virtual ssize_t Length() = 0; virtual bool HitTest(HitTestResult &htr) = 0; virtual bool GetPosFromIndex(BlockCursor *Cursor) = 0; virtual bool OnLayout(Flow &f) = 0; virtual void OnPaint(PaintContext &Ctx) = 0; virtual bool ToHtml(LStream &s, LArray *Media, LRange *Rgn) = 0; virtual bool OffsetToLine(ssize_t Offset, int *ColX, LArray *LineY) = 0; virtual ssize_t LineToOffset(ssize_t Line) = 0; virtual int GetLines() = 0; virtual ssize_t FindAt(ssize_t StartIdx, const uint32_t *Str, LFindReplaceCommon *Params) = 0; virtual void SetSpellingErrors(LArray &Errors, LRange r) {} virtual void IncAllStyleRefs() {} virtual void Dump() {} virtual LNamedStyle *GetStyle(ssize_t At = -1) = 0; virtual int GetUid() const { return BlockUid; } virtual bool DoContext(LSubMenu &s, LPoint Doc, ssize_t Offset /* internal to this block, not the whole doc. */, bool TopOfMenu) { return false; } #ifdef _DEBUG virtual void DumpNodes(LTreeItem *Ti) = 0; #endif virtual bool IsValid() { return false; } virtual bool IsBusy(bool Stop = false) { return false; } virtual Block *Clone() = 0; virtual void OnComponentInstall(LString Name) {} // Copy some or all of the text out virtual ssize_t CopyAt(ssize_t Offset, ssize_t Chars, LArray *Text) { return false; } /// This method moves a cursor index. /// \returns the new cursor index or -1 on error. virtual bool Seek ( /// [In] true if the next line is needed, false for the previous line SeekType To, /// [In/Out] The starting cursor. BlockCursor &Cursor ) = 0; /************************************************ * Change state methods, require a transaction * ***********************************************/ // Add some text at a given position virtual bool AddText ( /// Current transaction Transaction *Trans, /// The index to add at (-1 = the end) ssize_t AtOffset, /// The text itself const uint32_t *Str, /// [Optional] The number of characters ssize_t Chars = -1, /// [Optional] Style to give the text, NULL means "use the existing style" LNamedStyle *Style = NULL ) { return false; } /// Delete some chars /// \returns the number of chars actually removed virtual ssize_t DeleteAt ( Transaction *Trans, ssize_t Offset, ssize_t Chars, LArray *DeletedText = NULL ) { return false; } /// Changes the style of a range of characters virtual bool ChangeStyle ( Transaction *Trans, ssize_t Offset, ssize_t Chars, LCss *Style, bool Add ) { return false; } virtual bool DoCase ( /// Current transaction Transaction *Trans, /// Start index of text to change ssize_t StartIdx, /// Number of chars to change ssize_t Chars, /// True if upper case is desired bool Upper ) { return false; } // Split a block virtual Block *Split ( /// Current transaction Transaction *Trans, /// The index to add at (-1 = the end) ssize_t AtOffset ) { return NULL; } // Event called on dictionary load virtual bool OnDictionary(Transaction *Trans) { return false; } }; struct BlockCursor { // The block the cursor is in. Block *Blk; // This is the character offset of the cursor relative to // the start of 'Blk'. ssize_t Offset; // In wrapped text, a given offset can either be at the end // of one line or the start of the next line. This tells the // text block which line the cursor is actually on. int LineHint; // This is the position on the screen in doc coords. LRect Pos; // This is the position line that the cursor is on. This is // used to calculate the bounds for screen updates. LRect Line; // Cursor is currently blinking on bool Blink; BlockCursor(const BlockCursor &c); BlockCursor(Block *b, ssize_t off, int line); ~BlockCursor(); BlockCursor &operator =(const BlockCursor &c); void Set(ssize_t off); void Set(Block *b, ssize_t off, int line); bool operator ==(const BlockCursor &c) { return Blk == c.Blk && Offset == c.Offset; } #ifdef _DEBUG void DumpNodes(LTreeItem *Ti); #endif }; LAutoPtr Cursor, Selection; /// This is part or all of a Text run struct DisplayStr : public LDisplayString { StyleText *Src; ssize_t Chars; // The number of UTF-32 characters. This can be different to // LDisplayString::Length() in the case that LDisplayString // is using UTF-16 (i.e. Windows). int OffsetY; // Offset of this string from the TextLine's box in the Y axis DisplayStr(StyleText *src, LFont *f, const uint32_t *s, ssize_t l = -1, LSurface *pdc = NULL) : LDisplayString(f, #ifndef WINDOWS (char16*) #endif s, l, pdc) { Src = src; OffsetY = 0; #if defined(_MSC_VER) Chars = l < 0 ? Strlen(s) : l; #else Chars = WideWords; #endif // LAssert(l == 0 || FX() > 0); } template T *Utf16Seek(T *s, ssize_t i) { T *e = s + i; while (s < e) { uint16 n = *s & 0xfc00; if (n == 0xd800) { s++; if (s >= e) break; n = *s & 0xfc00; if (n != 0xdc00) { LAssert(!"Unexpected surrogate"); continue; } // else skip over the 2nd surrogate } s++; } return s; } ssize_t WideLen() { #if defined(LGI_DSP_STR_CACHE) return WideWords; #else return StrWords; #endif } // Make a sub-string of this display string virtual LAutoPtr Clone(ssize_t Start, ssize_t Len = -1) { LAutoPtr c; auto WideW = WideLen(); if (WideW > 0 && Len != 0) { const char16 *Str = *this; if (Len < 0) Len = WideW - Start; if (Start >= 0 && Start < (int)WideW && Start + Len <= (int)WideW) { #if defined(_MSC_VER) LAssert(Str != NULL); const char16 *s = Utf16Seek(Str, Start); const char16 *e = Utf16Seek(s, Len); LArray Tmp; if (Utf16to32(Tmp, (const uint16_t*)s, e - s)) c.Reset(new DisplayStr(Src, GetFont(), &Tmp[0], Tmp.Length(), pDC)); #else c.Reset(new DisplayStr(Src, GetFont(), (uint32_t*)Str + Start, Len, pDC)); #endif } } return c; } virtual void Paint(LSurface *pDC, int &FixX, int FixY, LColour &Back) { FDraw(pDC, FixX, FixY); FixX += FX(); } virtual double GetAscent() { return Font->Ascent(); } virtual ssize_t PosToIndex(int x, bool Nearest) { return CharAt(x); } }; struct EmojiDisplayStr : public DisplayStr { LArray SrcRect; LSurface *Img = NULL; LCss::Len Size; int CharPx = 0; #if defined(_MSC_VER) LArray Utf32; #endif EmojiDisplayStr(StyleText *src, LSurface *img, LCss::Len &fntSize, const uint32_t *s, ssize_t l = -1); LAutoPtr Clone(ssize_t Start, ssize_t Len = -1); void Paint(LSurface *pDC, int &FixX, int FixY, LColour &Back); double GetAscent(); ssize_t PosToIndex(int XPos, bool Nearest); }; /// This structure is a layout of a full line of text. Made up of one or more /// display string objects. struct TextLine { /// This is a position relative to the parent Block LRect PosOff; /// The array of display strings LArray Strs; /// Is '1' for lines that have a new line character at the end. uint8_t NewLine; TextLine(int XOffsetPx, int WidthPx, int YOffsetPx); ssize_t Length(); /// 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 LayoutOffsets(int DefaultFontHt); }; class TextBlock : public Block { LNamedStyle *Style; LArray SpellingErrors; int PaintErrIdx, ClickErrIdx; LSpellCheck::SpellingError *SpErr; LString ClickedUri; bool PreEdit(Transaction *Trans); void DrawDisplayString(LSurface *pDC, DisplayStr *Ds, int &FixX, int FixY, LColour &Bk, ssize_t &Pos); public: // Runs of characters in the same style: pre-layout. LArray Txt; // Runs of characters (display strings) of potentially different styles on the same line: post-layout. LArray Layout; // True if the 'Layout' data is out of date. bool LayoutDirty; // Size of the edges LRect Margin, Border, Padding; // Default font for the block LFont *Fnt; // Chars in the whole block (sum of all Text lengths) ssize_t Len; // Position in document co-ordinates LRect Pos; TextBlock(LRichTextPriv *priv); TextBlock(const TextBlock *Copy); ~TextBlock(); bool IsValid(); // No state change methods const char *GetClass() { return "TextBlock"; } int GetLines(); bool OffsetToLine(ssize_t Offset, int *ColX, LArray *LineY); ssize_t LineToOffset(ssize_t Line); LRect GetPos() { return Pos; } void Dump(); LNamedStyle *GetStyle(ssize_t At = -1); void SetStyle(LNamedStyle *s); ssize_t Length(); bool ToHtml(LStream &s, LArray *Media, LRange *Rng); bool GetPosFromIndex(BlockCursor *Cursor); bool HitTest(HitTestResult &htr); void OnPaint(PaintContext &Ctx); bool OnLayout(Flow &flow); ssize_t GetTextAt(ssize_t Offset, LArray &t); ssize_t CopyAt(ssize_t Offset, ssize_t Chars, LArray *Text); bool Seek(SeekType To, BlockCursor &Cursor); ssize_t FindAt(ssize_t StartIdx, const uint32_t *Str, LFindReplaceCommon *Params); void IncAllStyleRefs(); void SetSpellingErrors(LArray &Errors, LRange r); bool DoContext(LSubMenu &s, LPoint Doc, ssize_t Offset, bool Spelling); #ifdef _DEBUG void DumpNodes(LTreeItem *Ti); #endif Block *Clone(); bool IsEmptyLine(BlockCursor *Cursor); void UpdateSpellingAndLinks(Transaction *Trans, LRange r); // Events LMessage::Result OnEvent(LMessage *Msg); // Transactional changes bool AddText(Transaction *Trans, ssize_t AtOffset, const uint32_t *Str, ssize_t Chars = -1, LNamedStyle *Style = NULL); bool ChangeStyle(Transaction *Trans, ssize_t Offset, ssize_t Chars, LCss *Style, bool Add); ssize_t DeleteAt(Transaction *Trans, ssize_t BlkOffset, ssize_t Chars, LArray *DeletedText = NULL); bool DoCase(Transaction *Trans, ssize_t StartIdx, ssize_t Chars, bool Upper); Block *Split(Transaction *Trans, ssize_t AtOffset); bool StripLast(Transaction *Trans, const char *Set = " \t\r\n"); // Strip trailing new line if present.. bool OnDictionary(Transaction *Trans); }; class HorzRuleBlock : public Block { LRect Pos; bool IsDeleted; public: HorzRuleBlock(LRichTextPriv *priv); HorzRuleBlock(const HorzRuleBlock *Copy); ~HorzRuleBlock(); bool IsValid(); // No state change methods const char *GetClass() { return "HorzRuleBlock"; } int GetLines(); bool OffsetToLine(ssize_t Offset, int *ColX, LArray *LineY); ssize_t LineToOffset(ssize_t Line); LRect GetPos() { return Pos; } void Dump(); LNamedStyle *GetStyle(ssize_t At = -1); void SetStyle(LNamedStyle *s); ssize_t Length(); bool ToHtml(LStream &s, LArray *Media, LRange *Rng); bool GetPosFromIndex(BlockCursor *Cursor); bool HitTest(HitTestResult &htr); void OnPaint(PaintContext &Ctx); bool OnLayout(Flow &flow); ssize_t GetTextAt(ssize_t Offset, LArray &t); ssize_t CopyAt(ssize_t Offset, ssize_t Chars, LArray *Text); bool Seek(SeekType To, BlockCursor &Cursor); ssize_t FindAt(ssize_t StartIdx, const uint32_t *Str, LFindReplaceCommon *Params); void IncAllStyleRefs(); bool DoContext(LSubMenu &s, LPoint Doc, ssize_t Offset, bool Spelling); #ifdef _DEBUG void DumpNodes(LTreeItem *Ti); #endif Block *Clone(); // Events LMessage::Result OnEvent(LMessage *Msg); // Transactional changes bool AddText(Transaction *Trans, ssize_t AtOffset, const uint32_t *Str, ssize_t Chars = -1, LNamedStyle *Style = NULL); bool ChangeStyle(Transaction *Trans, ssize_t Offset, ssize_t Chars, LCss *Style, bool Add); ssize_t DeleteAt(Transaction *Trans, ssize_t BlkOffset, ssize_t Chars, LArray *DeletedText = NULL); bool DoCase(Transaction *Trans, ssize_t StartIdx, ssize_t Chars, bool Upper); Block *Split(Transaction *Trans, ssize_t AtOffset); }; class ImageBlock : public Block { public: struct ScaleInf { LPoint Sz; LString MimeType; LAutoPtr Compressed; - int Percent; + int Percent = 0; + bool Compressing = false; - ScaleInf() + LString ToString() { - Sz.x = Sz.y = 0; - Percent = 0; + return LString::Fmt("sz=%s, mime=%s, comp=%s, pc=%i", + Sz.GetStr().Get(), + MimeType.Get(), + LFormatSize(Compressed ? Compressed->GetSize() : 0).Get(), + Percent); } }; - int ThreadHnd; + int ThreadHnd = 0; protected: - LNamedStyle *Style; - int Scale; + LNamedStyle *Style = NULL; + int Scale = 1; LRect SourceValid; LString FileName; LString ContentId; LString StreamMimeType; LString FileMimeType; + enum ResizeIdxSource + { + SourceNone, + SourceDefault, + SourceUser, + SourceMaxImage, + }; + const char *ToString(ResizeIdxSource s) + { + switch (s) + { + case SourceNone: return "SourceNone"; + case SourceDefault: return "SourceDefault"; + case SourceUser: return "SourceUser"; + case SourceMaxImage: return "SourceMaxImage"; + } + return NULL; + } + + int ResizeIdx = -1; + ResizeIdxSource ResizeSrc = SourceNone; + LArray Scales; - int ResizeIdx; - int ThreadBusy; - bool IsDeleted; + int ThreadBusy = 0; + bool IsDeleted = false; void UpdateThreadBusy(const char *File, int Line, int Off); int GetThreadHandle(); void UpdateDisplay(int y); void UpdateDisplayImg(); public: LAutoPtr SourceImg, DisplayImg, SelectImg; LRect Margin, Border, Padding; LString Source; LPoint Size; - bool LayoutDirty; + bool LayoutDirty = false; LRect Pos; // position in document co-ordinates LRect ImgPos; ImageBlock(LRichTextPriv *priv); ImageBlock(const ImageBlock *Copy); ~ImageBlock(); bool IsValid(); bool IsBusy(bool Stop = false); bool Load(const char *Src = NULL); bool SetImage(LAutoPtr Img); + void OnDimensions(); + void GetCompressedSize(); + void MaxImageFilter(); // No state change methods int GetLines(); bool OffsetToLine(ssize_t Offset, int *ColX, LArray *LineY); ssize_t LineToOffset(ssize_t Line); LRect GetPos() { return Pos; } void Dump(); LNamedStyle *GetStyle(ssize_t At = -1); void SetStyle(LNamedStyle *s); ssize_t Length(); bool ToHtml(LStream &s, LArray *Media, LRange *Rng); bool GetPosFromIndex(BlockCursor *Cursor); bool HitTest(HitTestResult &htr); void OnPaint(PaintContext &Ctx); bool OnLayout(Flow &flow); ssize_t GetTextAt(ssize_t Offset, LArray &t); ssize_t CopyAt(ssize_t Offset, ssize_t Chars, LArray *Text); bool Seek(SeekType To, BlockCursor &Cursor); ssize_t FindAt(ssize_t StartIdx, const uint32_t *Str, LFindReplaceCommon *Params); void IncAllStyleRefs(); bool DoContext(LSubMenu &s, LPoint Doc, ssize_t Offset, bool Spelling); #ifdef _DEBUG void DumpNodes(LTreeItem *Ti); #endif Block *Clone(); void OnComponentInstall(LString Name); // Events LMessage::Result OnEvent(LMessage *Msg); // Transactional changes bool AddText(Transaction *Trans, ssize_t AtOffset, const uint32_t *Str, ssize_t Chars = -1, LNamedStyle *Style = NULL); bool ChangeStyle(Transaction *Trans, ssize_t Offset, ssize_t Chars, LCss *Style, bool Add); ssize_t DeleteAt(Transaction *Trans, ssize_t BlkOffset, ssize_t Chars, LArray *DeletedText = NULL); bool DoCase(Transaction *Trans, ssize_t StartIdx, ssize_t Chars, bool Upper); }; LArray Blocks; Block *Next(Block *b); Block *Prev(Block *b); void InvalidateDoc(LRect *r); void ScrollTo(LRect r); void UpdateStyleUI(); void EmptyDoc(); void Empty(); bool Seek(BlockCursor *In, SeekType Dir, bool Select); bool CursorFirst(); bool SetCursor(LAutoPtr c, bool Select = false); LRect SelectionRect(); bool GetSelection(LArray *Text, LAutoString *Html); ssize_t IndexOfCursor(BlockCursor *c); ssize_t HitTest(int x, int y, int &LineHint, Block **Blk = NULL, ssize_t *BlkOffset = NULL); bool CursorFromPos(int x, int y, LAutoPtr *Cursor, ssize_t *GlobalIdx); Block *GetBlockByIndex(ssize_t Index, ssize_t *Offset = NULL, int *BlockIdx = NULL, int *LineCount = NULL); bool Layout(LScrollBar *&ScrollY); void OnStyleChange(LRichTextEdit::RectType t); bool ChangeSelectionStyle(LCss *Style, bool Add); void PaintBtn(LSurface *pDC, LRichTextEdit::RectType t); bool MakeLink(TextBlock *tb, ssize_t Offset, ssize_t Len, LString Link); bool ClickBtn(LMouse &m, LRichTextEdit::RectType t); bool InsertHorzRule(); void Paint(LSurface *pDC, LScrollBar *&ScrollY); LHtmlElement *CreateElement(LHtmlElement *Parent); LPoint ScreenToDoc(int x, int y); LPoint DocToScreen(int x, int y); bool Merge(Transaction *Trans, Block *a, Block *b); bool DeleteSelection(Transaction *t, char16 **Cut); LRichTextEdit::RectType PosToButton(LMouse &m); void OnComponentInstall(LString Name); struct CreateContext { TextBlock *Tb; ImageBlock *Ib; HorzRuleBlock *Hrb; LArray Buf; uint32_t LastChar; LFontCache *FontCache; LCss::Store StyleStore; bool StartOfLine; CreateContext(LFontCache *fc) { Tb = NULL; Ib = NULL; Hrb = NULL; LastChar = '\n'; FontCache = fc; StartOfLine = true; } bool AddText(LNamedStyle *Style, char16 *Str) { if (!Str || !Tb) return false; int Used = 0; char16 *s = Str; char16 *e = s + StrlenW(s); while (s < e) { if (*s == '\r') { s++; continue; } if (IsWhite(*s)) { Buf[Used++] = ' '; while (s < e && IsWhite(*s)) s++; } else { #ifdef WINDOWS ssize_t Len = s[0] && s[1] ? 4 : (s[0] ? 2 : 0); Buf[Used++] = LgiUtf16To32((const uint16 *&)s, Len); #else Buf[Used++] = *s++; #endif while (s < e && !IsWhite(*s)) { #ifdef WINDOWS Len = s[0] && s[1] ? 4 : (s[0] ? 2 : 0); Buf[Used++] = LgiUtf16To32((const uint16 *&)s, Len); #else Buf[Used++] = *s++; #endif } } } bool Status = false; if (Used > 0) { Status = Tb->AddText(NoTransaction, -1, &Buf[0], Used, Style); LastChar = Buf[Used-1]; } return Status; } }; LAutoPtr CreationCtx; bool ToHtml(LArray *Media = NULL, BlockCursor *From = NULL, BlockCursor *To = NULL); void DumpBlocks(); bool FromHtml(LHtmlElement *e, CreateContext &ctx, LCss *ParentStyle = NULL, int Depth = 0); #ifdef _DEBUG void DumpNodes(LTree *Root); #endif }; struct BlockCursorState { bool Cursor; ssize_t Offset; int LineHint; int BlockUid; BlockCursorState(bool cursor, LRichTextPriv::BlockCursor *c); bool Apply(LRichTextPriv *Ctx, bool Forward); }; struct CompleteTextBlockState : public LRichTextPriv::DocChange { int Uid; LAutoPtr Cur, Sel; LAutoPtr Blk; CompleteTextBlockState(LRichTextPriv *Ctx, LRichTextPriv::TextBlock *Tb); bool Apply(LRichTextPriv *Ctx, bool Forward); }; struct MultiBlockState : public LRichTextPriv::DocChange { LRichTextPriv *Ctx; ssize_t Index; // Number of blocks before the edit ssize_t Length; // Of the other version currently in the Ctx stack LArray Blks; MultiBlockState(LRichTextPriv *ctx, ssize_t Start); bool Apply(LRichTextPriv *Ctx, bool Forward); bool Copy(ssize_t Idx); bool Cut(ssize_t Idx); }; #ifdef _DEBUG LTreeItem *PrintNode(LTreeItem *Parent, const char *Fmt, ...); #endif typedef LRichTextPriv::BlockCursor BlkCursor; typedef LAutoPtr AutoCursor; typedef LAutoPtr AutoTrans; #endif