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,3097 +1,3099 @@ #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() { } void LRichTextEdit::PourText(ssize_t Start, ssize_t Length /* == 0 means it's a delete */) { } void LRichTextEdit::PourStyle(ssize_t Start, ssize_t EditSize) { } bool LRichTextEdit::Insert(int At, char16 *Data, int Len) { return false; } bool LRichTextEdit::Delete(int At, int Len) { 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); - return Cb.Bitmap([this](auto bmp, auto str) + 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); 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; } else { // Insert before.. AddIndex = BlkIdx; } LRichTextPriv::ImageBlock *ImgBlk = new LRichTextPriv::ImageBlock(d); if (ImgBlk) { 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) { 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::StringsDef(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::StringsDef(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(); 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/win/Lgi/ClipBoard.cpp b/src/win/Lgi/ClipBoard.cpp --- a/src/win/Lgi/ClipBoard.cpp +++ b/src/win/Lgi/ClipBoard.cpp @@ -1,892 +1,873 @@ #include "lgi/common/Lgi.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/Palette.h" #include "lgi/common/Com.h" class LClipBoardPriv { public: LString Utf8; LAutoWString Wide; }; #if 0 class LFileEnum : public LUnknownImpl { int Idx; LArray Types; public: LFileEnum(LClipBoard::FormatType type) { Idx = 0; // Types.Add(type); Types.Add(CF_HDROP); AddInterface(IID_IEnumFORMATETC, (IEnumFORMATETC*)this); } ~LFileEnum() { LgiTrace("%s:%i - ~LFileEnum()\n", _FL); } HRESULT STDMETHODCALLTYPE Next(ULONG celt, FORMATETC *fmt, ULONG *pceltFetched) { if (!fmt || Idx >= Types.Length()) return S_FALSE; fmt->cfFormat = Types[Idx]; fmt->dwAspect = DVASPECT_CONTENT; fmt->lindex = -1; fmt->ptd = NULL; fmt->tymed = TYMED_HGLOBAL; if (pceltFetched) *pceltFetched = 1; Idx += celt; LgiTrace("%s:%i - Next(%i) returned '%s'\n", _FL, celt, LClipBoard::FmtToStr(fmt->cfFormat).Get()); return S_OK; } HRESULT STDMETHODCALLTYPE Skip(ULONG celt) { return S_OK; } HRESULT STDMETHODCALLTYPE Reset() { return S_OK; } HRESULT STDMETHODCALLTYPE Clone(IEnumFORMATETC **ppenum) { return E_NOTIMPL; } }; struct LFileName : public LUnknownImpl { char16 *w; LFileName(STGMEDIUM *Med, const char *u) { Med->tymed = TYMED_FILE; Med->lpszFileName = w = Utf8ToWide(u); Med->pUnkForRelease = this; } ~LFileName() { DeleteArray(w); } }; class LFileData : public LUnknownImpl { int Cur; LClipBoard::FormatType Type, PrefDrop, ShellIdList; public: LString::Array Files; LFileData(LString::Array &files) : Files(files) { // TraceRefs = true; Type = LClipBoard::StrToFmt(CFSTR_FILENAMEA); PrefDrop = LClipBoard::StrToFmt(CFSTR_PREFERREDDROPEFFECT); ShellIdList = LClipBoard::StrToFmt(CFSTR_SHELLIDLIST); Cur = 0; AddInterface(IID_IDataObject, (IDataObject*)this); LgiTrace("%s:%i - LFileData() = %p\n", _FL, this); } ~LFileData() { LgiTrace("%s:%i - ~LFileData()\n", _FL); } HRESULT STDMETHODCALLTYPE GetData(FORMATETC *Fmt, STGMEDIUM *Med) { LString sFmt = LClipBoard::FmtToStr(Fmt->cfFormat); LgiTrace("%s:%i - GetData(%s) starting...\n", _FL, sFmt.Get()); if (!Med) return E_INVALIDARG; if (Fmt->cfFormat == PrefDrop) { Med->tymed = TYMED_HGLOBAL; Med->hGlobal = GlobalAlloc(GHND, sizeof(DWORD)); DWORD* data = (DWORD*)GlobalLock(Med->hGlobal); *data = DROPEFFECT_COPY; GlobalUnlock(Med->hGlobal); Med->pUnkForRelease = NULL; } else if (Fmt->cfFormat == Type) { new LFileName(Med, Files[Cur++]); } else if (Fmt->cfFormat == CF_HDROP) { LDragDropSource Src; LDragData Data; LMouse m; if (!Src.CreateFileDrop(&Data, m, Files)) { LgiTrace("%s:%i - CreateFileDrop failed.\n", _FL); return E_FAIL; } LVariant &d = Data.Data[0]; Med->tymed = TYMED_HGLOBAL; Med->hGlobal = GlobalAlloc(GHND, d.Value.Binary.Length); if (Med->hGlobal == NULL) { LgiTrace("%s:%i - GlobalAlloc failed.\n", _FL); return E_FAIL; } char* data = (char*)GlobalLock(Med->hGlobal); memcpy(data, d.Value.Binary.Data, d.Value.Binary.Length); GlobalUnlock(Med->hGlobal); Med->pUnkForRelease = NULL; } else if (Fmt->cfFormat == ShellIdList) { LgiTrace("%s:%i - GetData ShellIdList not supported.\n", _FL); return E_NOTIMPL; /* LPIDA Data = NULL; ITEMIDLIST *IdList = NULL; size_t Size = sizeof(CIDA) + (Files.Length() * sizeof(UINT)) + (Files.Length() * sizeof(ITEMIDLIST)); Med->hGlobal = GlobalAlloc(GHND, Size); if (Med->hGlobal == NULL) return E_FAIL; Data = (LPIDA) GlobalLock(Med->hGlobal); Data->cidl = Files.Length(); LPITEMIDLIST *Parent = (LPITEMIDLIST) (Data + 1); Data->aoffset[0] = (char*)Parent - (char*)Data; LPointer p; p.vp = Parent + 1; for (unsigned i=0; iaoffset[i+1] } GlobalUnlock(Med->hGlobal); Med->pUnkForRelease = NULL; */ } else { LgiTrace("%s:%i - GetData(%s) not supported.\n", _FL, sFmt.Get()); return DV_E_FORMATETC; } LgiTrace("%s:%i - GetData(%s) OK.\n", _FL, sFmt.Get()); return S_OK; } HRESULT STDMETHODCALLTYPE GetDataHere(FORMATETC *pformatetc, STGMEDIUM *pmedium) { LgiTrace("%s:%i - GetDataHere not impl\n", _FL); return E_NOTIMPL; } HRESULT STDMETHODCALLTYPE QueryGetData(FORMATETC *Fmt) { if (Fmt->cfFormat == Type || Fmt->cfFormat == CF_HDROP) return S_OK; LString sFmt = LClipBoard::FmtToStr(Fmt->cfFormat); LgiTrace("%s:%i - QueryGetData(%s) not supported.\n", _FL, sFmt.Get()); return DV_E_FORMATETC; } HRESULT STDMETHODCALLTYPE GetCanonicalFormatEtc(FORMATETC *Fmt, FORMATETC *pformatetcOut) { LgiTrace("%s:%i - GetCanonicalFormatEtc not impl\n", _FL); return E_NOTIMPL; } HRESULT STDMETHODCALLTYPE SetData(FORMATETC *Fmt, STGMEDIUM *pmedium, BOOL fRelease) { LString sFmt = LClipBoard::FmtToStr(Fmt->cfFormat); LgiTrace("%s:%i - SetData(%s)\n", _FL, sFmt.Get()); return S_OK; } HRESULT STDMETHODCALLTYPE EnumFormatEtc(DWORD dir, IEnumFORMATETC **enumFmt) { if (dir == DATADIR_GET) { if (*enumFmt = new LFileEnum(Type)) (*enumFmt)->AddRef(); LgiTrace("%s:%i - Returning LFileEnum obj.\n", _FL); return S_OK; } else if (dir == DATADIR_SET) { } LgiTrace("%s:%i - EnumFormatEtc error\n", _FL); return E_NOTIMPL; } HRESULT STDMETHODCALLTYPE DAdvise(FORMATETC *pformatetc, DWORD advf, IAdviseSink *pAdvSink, DWORD *pdwConnection) { LgiTrace("%s:%i - DAdvise not impl\n", _FL); return E_NOTIMPL; } HRESULT STDMETHODCALLTYPE DUnadvise(DWORD dwConnection) { LgiTrace("%s:%i - DUnadvise not impl\n", _FL); return E_NOTIMPL; } HRESULT STDMETHODCALLTYPE EnumDAdvise(IEnumSTATDATA **ppenumAdvise) { LgiTrace("%s:%i - EnumDAdvise not impl\n", _FL); return E_NOTIMPL; } }; #endif -LString::Array LClipBoard::Files() +void LClipBoard::Files(FilesCb Callback) { LString::Array f; + LString errMsg; if (Open) { CloseClipboard(); Open = FALSE; } LPDATAOBJECT pObj = NULL; auto r = OleGetClipboard(&pObj); - if (SUCCEEDED(r)) + if (FAILED(r)) + { + errMsg = "OleGetClipboard failed."; + + LArray Fmts; + if (EnumFormats(Fmts)) + { + for (auto f : Fmts) + { + auto s = FmtToStr(f); + LgiTrace("ClipFmt: %s\n", s.Get()); + } + } + } + else { LArray Fmts; IEnumFORMATETC *pEnum = NULL; r = pObj->EnumFormatEtc(DATADIR_GET, &pEnum); - if (SUCCEEDED(r)) + if (FAILED(r)) + { + errMsg = "EnumFormatEtc failed."; + } + else { FORMATETC Fmt; r = pEnum->Next(1, &Fmt, NULL); while (r == S_OK) { Fmts.Add(Fmt.cfFormat); LgiTrace("Got format: 0x%x = %s\n", Fmt.cfFormat, FmtToStr(Fmt.cfFormat).Get()); if (Fmt.cfFormat == CF_HDROP) { STGMEDIUM Med; auto Res = pObj->GetData(&Fmt, &Med); if (Res == S_OK) { switch (Med.tymed) { case TYMED_HGLOBAL: { auto Sz = GlobalSize(Med.hGlobal); DROPFILES *p = (DROPFILES*)GlobalLock(Med.hGlobal); if (p) { LPointer End; End.c = (char*)p + Sz; if (p->fWide) { wchar_t *w = (wchar_t*) ((char*)p + p->pFiles); while (w < End.w && *w) { f.Add(w); w += Strlen(w) + 1; } } else { char *n = (char*)p + p->pFiles; while (n < End.c && *n) { auto u = LFromNativeCp(n); f.Add(u.Get()); n += Strlen(n) + 1; } } } /* int Count = DragQueryFileW(hDrop, -1, NULL, 0); for (int i=0; i 0) { FileNames.Add(WideToUtf8(FileName)); } } */ GlobalUnlock(Med.hGlobal); break; } } } } r = pEnum->Next(1, &Fmt, NULL); } pEnum->Release(); } pObj->Release(); } - else - { - LArray Fmts; - if (EnumFormats(Fmts)) - { - for (auto f : Fmts) - { - auto s = FmtToStr(f); - LgiTrace("ClipFmt: %s\n", s.Get()); - } - } - } - return f; + if (Callback) + Callback(f, errMsg); } bool LClipBoard::Files(LString::Array &Paths, bool AutoEmpty) { LDragDropSource Src; LDragData Output; LMouse m; if (Owner) Owner->GetMouse(m, true); if (!Src.CreateFileDrop(&Output, m, Paths)) return false; LVariant &v = Output.Data[0]; if (v.Type != GV_BINARY) return false; HGLOBAL hMem = GlobalAlloc(GMEM_ZEROINIT|GMEM_MOVEABLE|GMEM_DDESHARE, v.Value.Binary.Length); auto *p = GlobalLock(hMem); CopyMemory(p, v.Value.Binary.Data, v.Value.Binary.Length); GlobalUnlock(hMem); OpenClipboard(NULL); if (AutoEmpty) EmptyClipboard(); auto r = SetClipboardData(CF_HDROP, hMem); CloseClipboard(); return r != NULL; } /////////////////////////////////////////////////////////////////////////////////////////////// LString LClipBoard::FmtToStr(FormatType Fmt) { TCHAR n[256] = {0}; int r = GetClipboardFormatName(Fmt, n, CountOf(n)); if (!r) { switch (Fmt) { case CF_TEXT: return "CF_TEXT"; case CF_BITMAP: return "CF_BITMAP"; case CF_HDROP: return "CF_HDROP"; case CF_UNICODETEXT: return "CF_UNICODETEXT"; default: LAssert(!"Not impl."); break; } } return n; } LClipBoard::FormatType LClipBoard::StrToFmt(LString Fmt) { return RegisterClipboardFormatA(Fmt); } /////////////////////////////////////////////////////////////////////////////////////////////// LClipBoard::LClipBoard(LView *o) { d = new LClipBoardPriv; Open = false; Owner = o; if (Owner) Open = OpenClipboard(Owner->Handle()) != 0; } LClipBoard::~LClipBoard() { if (Open) { CloseClipboard(); Open = FALSE; } DeleteObj(d); } LClipBoard &LClipBoard::operator =(LClipBoard &c) { LAssert(0); return *this; } bool LClipBoard::EnumFormats(LArray &Formats) { UINT Idx = 0; UINT Fmt; while (Fmt = EnumClipboardFormats(Idx)) { Formats.Add(Fmt); Idx++; } return Formats.Length() > 0; } bool LClipBoard::Empty() { d->Utf8.Empty(); d->Wide.Reset(); return EmptyClipboard() != 0; } // Text bool LClipBoard::Text(const char *Str, bool AutoEmpty) { bool Status = false; if (Str) { auto Native = LToNativeCp(Str); if (Native) { Status = Binary(CF_TEXT, (uchar*)Native.Get(), strlen(Native)+1, AutoEmpty); } else LgiTrace("%s:%i - Conversion to native cs failed.\n", _FL); } else LgiTrace("%s:%i - No text.\n", _FL); return Status; } char *LClipBoard::Text() { - ssize_t Len = 0; - LAutoPtr Str; - if (Binary(CF_TEXT, Str, &Len)) + Binary(CF_TEXT, [this](auto str, auto err) { - d->Utf8 = LFromNativeCp((char*)Str.Get()); - return d->Utf8; - } + d->Utf8 = LFromNativeCp(str); + }); - return NULL; + return d->Utf8; } bool LClipBoard::TextW(const char16 *Str, bool AutoEmpty) { if (Str) { auto Len = StrlenW(Str); return Binary(CF_UNICODETEXT, (uchar*) Str, (Len+1) * sizeof(ushort), AutoEmpty); } return false; } char16 *LClipBoard::TextW() { - ssize_t Len = 0; - LAutoPtr Str; - if (Binary(CF_UNICODETEXT, Str, &Len)) + Binary(CF_UNICODETEXT, [this](auto str, auto err) { - d->Wide.Reset(NewStrW((char16*) Str.Get(), Len / 2)); - return d->Wide; - } + d->Wide.Reset(NewStrW((char16*) str.Get(), str.Length() / 2)); + }); - return NULL; + return d->Wide; } #ifndef CF_HTML #define CF_HTML RegisterClipboardFormatA("HTML Format") #endif bool LClipBoard::Html(const char *Doc, bool AutoEmpty) { if (!Doc) return false; LString s; s.Printf("Version:0.9\n" "StartHTML:000000\n" "EndHTML:000000\n" ""); auto Start = s.Length(); s += Doc; auto End = s.Length(); auto p = s.Split("000000", 2); if (p.Length() != 3) return false; LString n; n.Printf("%06i", (int)Start); s = p[0] + n; n.Printf("%06i", (int)End); s += p[1] + n + p[2]; auto Len = Strlen(Doc); return Binary(CF_HTML, (uchar*) s.Get(), s.Length(), AutoEmpty); } LString LClipBoard::Html() { - LAutoPtr Buf; - ssize_t Len; - if (!Binary(CF_HTML, Buf, &Len)) + LString Txt; + Binary(CF_HTML, [&Txt](auto str, auto err) + { + Txt = str; + }); + + if (!Txt) return NULL; - LString Txt((char*)Buf.Get(), Len); auto Ln = Txt.Split("\n", 20); ssize_t Start = -1, End = -1; for (auto l : Ln) { auto p = l.Strip().Split(":", 1); if (p.Length() == 0) ; else if (p[0].Equals("StartHTML")) Start = (ssize_t)p[1].Int(); else if (p[0].Equals("EndHTML")) End = (ssize_t)p[1].Int(); } if (Start <= 0 || End <= 0) return false; return Txt(Start, End).Strip(); } // Bitmap bool LClipBoard::Bitmap(LSurface *pDC, bool AutoEmpty) { bool Status = FALSE; Empty(); if (pDC) { int Bytes = BMPWIDTH(pDC->X() * pDC->GetBits()); int Colours = (pDC->GetBits()>8) ? ((pDC->GetBits() != 24) ? 3 : 0) : 1 << pDC->GetBits(); int HeaderSize = sizeof(BITMAPINFOHEADER); int PaletteSize = Colours * sizeof(RGBQUAD); int MapSize = Bytes * pDC->Y(); int TotalSize = HeaderSize + PaletteSize + MapSize; HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, TotalSize); if (hMem) { BITMAPINFO *Info = (BITMAPINFO*) GlobalLock(hMem); if (Info) { // Header Info->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); Info->bmiHeader.biWidth = pDC->X(); Info->bmiHeader.biHeight = pDC->Y(); Info->bmiHeader.biPlanes = 1; Info->bmiHeader.biBitCount = pDC->GetBits(); if (pDC->GetBits() == 16 || pDC->GetBits() == 32) { Info->bmiHeader.biCompression = BI_BITFIELDS; } else { Info->bmiHeader.biCompression = BI_RGB; } Info->bmiHeader.biSizeImage = MapSize; Info->bmiHeader.biXPelsPerMeter = 0; Info->bmiHeader.biYPelsPerMeter = 0; Info->bmiHeader.biClrUsed = 0; Info->bmiHeader.biClrImportant = 0; if (pDC->GetBits() <= 8) { // Palette LPalette *Pal = pDC->Palette(); RGBQUAD *Rgb = Info->bmiColors; if (Pal) { GdcRGB *p = (*Pal)[0]; if (p) { for (int i=0; irgbRed = p->r; Rgb->rgbGreen = p->g; Rgb->rgbBlue = p->b; Rgb->rgbReserved = p->a; } } } else { memset(Rgb, 0, Colours * sizeof(RGBQUAD)); } } else { int *Primaries = (int*) Info->bmiColors; switch (pDC->GetBits()) { case 16: { Primaries[0] = Rgb16(255, 0, 0); Primaries[1] = Rgb16(0, 255, 0); Primaries[2] = Rgb16(0, 0, 255); break; } case 32: { Primaries[0] = Rgba32(255, 0, 0, 0); Primaries[1] = Rgba32(0, 255, 0, 0); Primaries[2] = Rgba32(0, 0, 255, 0); break; } } } // Bits uchar *Dest = ((uchar*)Info) + HeaderSize + PaletteSize; for (int y=pDC->Y()-1; y>=0; y--) { uchar *s = (*pDC)[y]; if (s) { memcpy(Dest, s, Bytes); } Dest += Bytes; } #if 0 LFile f; if (f.Open("c:\\tmp\\out.bmp", O_WRITE)) { f.SetSize(0); f.Write(Info, TotalSize); f.Close(); } #endif Status = SetClipboardData(CF_DIB, hMem) != 0; } else { GlobalFree(hMem); } } } return Status; } LAutoPtr LClipBoard::ConvertFromPtr(void *Ptr) { LAutoPtr pDC; BITMAPINFO *Info = (BITMAPINFO*) Ptr; if ( Info && pDC.Reset(new LMemDC) && (Info->bmiHeader.biCompression == BI_RGB || Info->bmiHeader.biCompression == BI_BITFIELDS) ) { if ( pDC->Create ( Info->bmiHeader.biWidth, Info->bmiHeader.biHeight, LBitsToColourSpace(max(Info->bmiHeader.biPlanes * Info->bmiHeader.biBitCount, 8)) ) ) { int Colours = 0; char *Source = (char*) Info->bmiColors; // do palette if (pDC->GetBits() <= 8) { if (Info->bmiHeader.biClrUsed > 0) Colours = Info->bmiHeader.biClrUsed; else Colours = 1 << pDC->GetBits(); LPalette *Pal = new LPalette(NULL, Colours); if (Pal) { GdcRGB *d = (*Pal)[0]; RGBQUAD *s = (RGBQUAD*) Source; if (d) { for (int i=0; ir = s->rgbRed; d->g = s->rgbGreen; d->b = s->rgbBlue; d->a = s->rgbReserved; } } Source = (char*) s; pDC->Palette(Pal); } } if (Info->bmiHeader.biCompression == BI_BITFIELDS) Source += sizeof(DWORD) * 3; // do pixels int Bytes = BMPWIDTH(pDC->X() * pDC->GetBits()); for (int y=pDC->Y()-1; y>=0; y--) { uchar *d = (*pDC)[y]; if (d) memcpy(d, Source, Bytes); Source += Bytes; } } } return pDC; } -bool LClipBoard::Bitmap(BitmapCb Callback) +void LClipBoard::Bitmap(BitmapCb Callback) { if (!Callback) - return false; + return; HGLOBAL hMem = GetClipboardData(CF_DIB); LAutoPtr pDC; if (void *Ptr = GlobalLock(hMem)) { pDC = ConvertFromPtr(Ptr); GlobalUnlock(hMem); } Callback(pDC, pDC ? NULL : "No bitmap on clipboard."); - return true; -} - -LAutoPtr LClipBoard::Bitmap() -{ - HGLOBAL hMem = GetClipboardData(CF_DIB); - LAutoPtr pDC; - if (void *Ptr = GlobalLock(hMem)) - { - #if 0 - SIZE_T TotalSize = GlobalSize(hMem); - LFile f; - if (f.Open("c:\\tmp\\in.bmp", O_WRITE)) - { - f.SetSize(0); - f.Write(Ptr, TotalSize); - f.Close(); - } - #endif - - pDC = ConvertFromPtr(Ptr); - GlobalUnlock(hMem); - } - - return pDC; } bool LClipBoard::Binary(FormatType Format, uchar *Ptr, ssize_t Len, bool AutoEmpty) { bool Status = FALSE; if (AutoEmpty) { Empty(); } if (Ptr && Len > 0) { HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, Len); if (hMem) { char *Data = (char*) GlobalLock(hMem); if (Data) { memcpy(Data, Ptr, Len); GlobalUnlock(hMem); Status = SetClipboardData(Format, hMem) != 0; if (!Status) LgiTrace("%s:%i - SetClipboardData failed.\n", _FL); } else { GlobalFree(hMem); } } else LgiTrace("%s:%i - Alloc failed.\n", _FL); } else LgiTrace("%s:%i - No data to set.\n", _FL); return Status; } -bool LClipBoard::Binary(FormatType Format, LAutoPtr &Ptr, ssize_t *Length) +void LClipBoard::Binary(FormatType Format, BinaryCb Callback) { - bool Status = false; + if (!Callback) + return; HGLOBAL hMem = GetClipboardData(Format); - if (hMem) + if (!hMem) { - auto Len = GlobalSize(hMem); - if (Length) - *Length = Len; - - uchar *Data = (uchar*) GlobalLock(hMem); - if (Data) - { - if (Ptr.Reset(new uchar[Len+sizeof(char16)])) - { - memcpy(Ptr, Data, Len); - - // String termination - memset(Ptr + Len, 0, sizeof(char16)); - - Status = true; - } - } - - GlobalUnlock(hMem); + Callback(LString(), "GetClipboardData failed."); + return; } - return Status; + auto Len = GlobalSize(hMem); + uchar *Data = (uchar*) GlobalLock(hMem); + if (Data) + { + LString Ptr; + if (Ptr.Length(Len + sizeof(char16))) + { + memcpy(Ptr.Get(), Data, Len); + memset(Ptr.Get() + Len, 0, sizeof(char16)); // terminate with zeros + Ptr.Length(Len); + + Callback(Ptr, LString()); + } + else Callback(LString(), "Alloc failed."); + } + else Callback(LString(), "GlobalLock failed."); + + GlobalUnlock(hMem); }