diff --git a/include/lgi/common/RadioGroup.h b/include/lgi/common/RadioGroup.h --- a/include/lgi/common/RadioGroup.h +++ b/include/lgi/common/RadioGroup.h @@ -1,100 +1,105 @@ /// \file /// \author Matthew Allen /// \brief A radio group view #ifndef _GRADIO_GROUP_H_ #define _GRADIO_GROUP_H_ class LRadioButton; /// A grouping control. All radio buttons that are children of this control will automatically /// have only one option selected. Other controls can be children as well but are ignored in the /// calculation of the groups value. The value of the group is the index into a list of radio buttons /// of the radio button that is on. class LgiClass LRadioGroup : #ifdef WINNATIVE public LControl, #else public LView, #endif public ResObject { class LRadioGroupPrivate *d; void OnCreate() override; public: LRadioGroup(int id, int x, int y, int cx, int cy, const char *name, int Init = 0); ~LRadioGroup(); const char *GetClass() override { return "LRadioGroup"; } /// Returns the index of the set radio button int64 Value() override; /// Sets the 'ith' radio button to on. void Value(int64 i) override; /// Adds a radio button to the group. LRadioButton *Append(int x, int y, const char *name); // Impl int OnNotify(LViewI *Ctrl, LNotification n) override; void OnPaint(LSurface *pDC) override; void OnAttach() override; LMessage::Result OnEvent(LMessage *m) override; bool OnLayout(LViewLayoutInfo &Inf) override; void OnStyleChange(); const char *Name() override { return LView::Name(); } const char16 *NameW() override { return LView::NameW(); } bool Name(const char *n) override; bool NameW(const char16 *n) override; void SetFont(LFont *Fnt, bool OwnIt = false) override; }; /// A radio button control. A radio button is used to select between mutually exclusive options. i.e. /// only one can be valid at any given time. For non-mutually exclusive options see the LCheckBox control. class LgiClass LRadioButton : #if WINNATIVE && !XP_BUTTON public LControl, #else public LView, #endif public ResObject { friend class LRadioGroup; class LRadioButtonPrivate *d; public: LRadioButton(int id, int x, int y, int cx, int cy, const char *name); ~LRadioButton(); const char *GetClass() override { return "LRadioButton"; } + // If the heirarchy of ctrls doesn't allow the radio buttons to find each other automatically, + // this will set the group up so that the only one is "on" at any given time. + // \return true if the group is setup correctly (Ie all ctrl IDs are present) + bool SetGroup(LArray CtrlIds); + // Impl const char *Name() override { return LView::Name(); } const char16 *NameW() override { return LView::NameW(); } bool Name(const char *n) override; bool NameW(const char16 *n) override; int64 Value() override; void Value(int64 i) override; bool OnLayout(LViewLayoutInfo &Inf) override; int OnNotify(LViewI *Ctrl, LNotification n) override; // Events void OnAttach() override; void OnStyleChange(); bool OnKey(LKey &k) override; #if WINNATIVE && !XP_BUTTON - int SysOnNotify(int Msg, int Code); - LMessage::Result OnEvent(LMessage *m) override; + int SysOnNotify(int Msg, int Code); + LMessage::Result OnEvent(LMessage *m) override; #else - void OnMouseClick(LMouse &m) override; - void OnMouseEnter(LMouse &m) override; - void OnMouseExit(LMouse &m) override; - void OnFocus(bool f) override; - void OnPaint(LSurface *pDC) override; - void SetFont(LFont *Fnt, bool OwnIt = false) override; + void OnMouseClick(LMouse &m) override; + void OnMouseEnter(LMouse &m) override; + void OnMouseExit(LMouse &m) override; + void OnFocus(bool f) override; + void OnPaint(LSurface *pDC) override; + void SetFont(LFont *Fnt, bool OwnIt = false) override; #endif }; #endif diff --git a/src/common/Widgets/Editor/RichTextEditPriv.cpp b/src/common/Widgets/Editor/RichTextEditPriv.cpp --- a/src/common/Widgets/Editor/RichTextEditPriv.cpp +++ b/src/common/Widgets/Editor/RichTextEditPriv.cpp @@ -1,2499 +1,2499 @@ #include "lgi/common/Lgi.h" #include "lgi/common/RichTextEdit.h" #include "lgi/common/ScrollBar.h" #include "lgi/common/CssTools.h" #include "lgi/common/Menu.h" #include "RichTextEditPriv.h" /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool Utf16to32(LArray &Out, const uint16_t *In, ssize_t WordLen) { if (WordLen == 0) { Out.Length(0); return true; } // Count the length of utf32 chars... const uint16 *Ptr = In; ssize_t Bytes = sizeof(*In) * WordLen; int Chars = 0; while ( Bytes >= sizeof(*In) && LgiUtf16To32(Ptr, Bytes) > 0) Chars++; // Set the output buffer size.. if (!Out.Length(Chars)) return false; // Convert the string... Ptr = (uint16*)In; Bytes = sizeof(*In) * WordLen; uint32_t *o = &Out[0]; #ifdef _DEBUG uint32_t *e = o + Out.Length(); #endif while ( Bytes >= sizeof(*In)) { *o++ = LgiUtf16To32(Ptr, Bytes); } LAssert(o == e); return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// const char *LRichEditElemContext::GetElement(LRichEditElem *obj) { return obj->Tag; } const char *LRichEditElemContext::GetAttr(LRichEditElem *obj, const char *Attr) { const char *a = NULL; obj->Get(Attr, a); return a; } bool LRichEditElemContext::GetClasses(LString::Array &Classes, LRichEditElem *obj) { const char *c; if (!obj->Get("class", c)) return false; LString cls = c; Classes = cls.Split(" "); return Classes.Length() > 0; } LRichEditElem *LRichEditElemContext::GetParent(LRichEditElem *obj) { return dynamic_cast(obj->Parent); } LArray LRichEditElemContext::GetChildren(LRichEditElem *obj) { LArray a; for (unsigned i=0; iChildren.Length(); i++) a.Add(dynamic_cast(obj->Children[i])); return a; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// LCssCache::LCssCache() { Idx = 1; } LCssCache::~LCssCache() { Styles.DeleteObjects(); } uint32_t LCssCache::GetStyles() { uint32_t c = 0; for (unsigned i=0; iRefCount != 0; } return c; } void LCssCache::ZeroRefCounts() { for (unsigned i=0; iRefCount = 0; } } bool LCssCache::OutputStyles(LStream &s, int TabDepth) { char Tabs[64]; memset(Tabs, '\t', TabDepth); Tabs[TabDepth] = 0; for (unsigned i=0; iRefCount > 0) { s.Print("%s.%s {\n", Tabs, ns->Name.Get()); LAutoString a = ns->ToString(); LString all = a.Get(); LString::Array lines = all.Split("\n"); for (unsigned n=0; n &s) { if (!s) return NULL; // Look through existing styles for a match... for (unsigned i=0; iName.Printf("%sStyle%i", p?p:"", Idx++); *(LCss*)ns = *s.Get(); Styles.Add(ns); #if 0 // _DEBUG LAutoString ss = ns->ToString(); if (ss) LgiTrace("%s = %s\n", ns->Name.Get(), ss.Get()); #endif } return ns; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// BlockCursorState::BlockCursorState(bool cursor, LRichTextPriv::BlockCursor *c) { Cursor = cursor; Offset = c->Offset; LineHint = c->LineHint; BlockUid = c->Blk->GetUid(); } bool BlockCursorState::Apply(LRichTextPriv *Ctx, bool Forward) { LAutoPtr &Bc = Cursor ? Ctx->Cursor : Ctx->Selection; if (!Bc) return false; ssize_t o = Bc->Offset; int lh = Bc->LineHint; int uid = Bc->Blk->GetUid(); int Index; LRichTextPriv::Block *b; if (!Ctx->GetBlockByUid(b, BlockUid, &Index)) return false; if (b != Bc->Blk) Bc.Reset(new LRichTextPriv::BlockCursor(b, Offset, LineHint)); else { Bc->Offset = Offset; Bc->LineHint = LineHint; } Offset = o; LineHint = lh; BlockUid = uid; return true; } /// This is the simplest form of undo, just save the entire block state, and restore it if needed CompleteTextBlockState::CompleteTextBlockState(LRichTextPriv *Ctx, LRichTextPriv::TextBlock *Tb) { if (Ctx->Cursor) Cur.Reset(new BlockCursorState(true, Ctx->Cursor)); if (Ctx->Selection) Sel.Reset(new BlockCursorState(false, Ctx->Selection)); if (Tb) { Uid = Tb->GetUid(); Blk.Reset(new LRichTextPriv::TextBlock(Tb)); } } bool CompleteTextBlockState::Apply(LRichTextPriv *Ctx, bool Forward) { int Index; LRichTextPriv::TextBlock *b; if (!Ctx->GetBlockByUid(b, Uid, &Index)) return false; // Swap the local state with the block in the ctx Blk->UpdateSpellingAndLinks(NULL, LRange(0, Blk->Length())); Ctx->Blocks[Index] = Blk.Release(); Blk.Reset(b); // Update cursors if (Cur) Cur->Apply(Ctx, Forward); if (Sel) Sel->Apply(Ctx, Forward); return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// MultiBlockState::MultiBlockState(LRichTextPriv *ctx, ssize_t Start) { Ctx = ctx; Index = Start; Length = -1; } bool MultiBlockState::Apply(LRichTextPriv *Ctx, bool Forward) { if (Index < 0 || Length < 0) { LAssert(!"Missing parameters"); return false; } // Undo: Swap 'Length' blocks Ctx->Blocks with Blks ssize_t OldLen = Blks.Length(); bool Status = Blks.SwapRange(LRange(0, OldLen), Ctx->Blocks, LRange(Index, Length)); if (Status) Length = OldLen; return Status; } bool MultiBlockState::Copy(ssize_t Idx) { if (!Ctx->Blocks.AddressOf(Idx)) return false; LRichTextPriv::Block *b = Ctx->Blocks[Idx]->Clone(); if (!b) return false; Blks.Add(b); return true; } bool MultiBlockState::Cut(ssize_t Idx) { if (!Ctx->Blocks.AddressOf(Idx)) return false; LRichTextPriv::Block *b = Ctx->Blocks[Idx]; if (!b) return false; Blks.Add(b); return Ctx->Blocks.DeleteAt(Idx, true); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// LRichTextPriv::LRichTextPriv(LRichTextEdit *view, LRichTextPriv **Ptr) : LHtmlParser(view), LFontCache(LSysFont) { if (Ptr) *Ptr = this; BlinkTs = 0; View = view; Log = &LogBuffer; NextUid = 1; UndoPos = 0; UndoPosLock = false; WordSelectMode = false; Dirty = false; ScrollOffsetPx = 0; ShowTools = true; ScrollChange = false; DocumentExtent.x = 0; DocumentExtent.y = 0; SpellCheck = NULL; SpellDictionaryLoaded = false; HtmlLinkAsCid = false; ScrollLinePx = LSysFont->GetHeight(); if (Font.Reset(new LFont)) *Font = *LSysFont; for (unsigned i=0; i DeletedText; LArray *DelTxt = Cut ? &DeletedText : NULL; bool Cf = CursorFirst(); LRichTextPriv::BlockCursor *Start = Cf ? Cursor : Selection; LRichTextPriv::BlockCursor *End = Cf ? Selection : Cursor; if (Start->Blk == End->Blk) { // In the same block... just delete the text ssize_t Len = End->Offset - Start->Offset; LRichTextPriv::Block *NextBlk = Next(Start->Blk); if (Len >= Start->Blk->Length() && NextBlk) { // Delete entire block ssize_t i = Blocks.IndexOf(Start->Blk); LAutoPtr MultiState(new MultiBlockState(this, i)); MultiState->Cut(i); MultiState->Length = 0; Start->Set(NextBlk, 0, 0); Trans->Add(MultiState.Release()); } else { Start->Blk->DeleteAt(Trans, Start->Offset, Len, DelTxt); } } else { // Multi-block delete... ssize_t i = Blocks.IndexOf(Start->Blk); ssize_t e = Blocks.IndexOf(End->Blk); LAutoPtr MultiState(new MultiBlockState(this, i)); // 1) Delete all the content to the end of the first block ssize_t StartLen = Start->Blk->Length(); if (Start->Offset < StartLen) { MultiState->Copy(i++); Start->Blk->DeleteAt(NoTransaction, Start->Offset, StartLen - Start->Offset, DelTxt); } else if (Start->Offset == StartLen) { // This can happen because there is an implied '\n' at the end of each block. The // next block always starts on a new line. We just increment the index 'i' to avoid // the next loop deleting the start block. i++; // If the start and end blocks can be merged, the new line will eventually get removed // then. If not, then... well whatevs. } else { LAssert(0); return Error(_FL, "Cursor outside start block."); } // 2) Delete any blocks between 'Start' and 'End' if (i >= 0) { while (Blocks[i] != End->Blk && i < (int)Blocks.Length()) { LRichTextPriv::Block *b = Blocks[i]; b->CopyAt(0, -1, DelTxt); printf("%s:%i - inter, %i\n", _FL, (int)i); MultiState->Cut(i); e--; } } else { LAssert(0); return Error(_FL, "Start block has no index.");; } if (End->Offset > 0) { // 3) Delete any text up to the Cursor in the 'End' block MultiState->Copy(i); printf("%s:%i - end, %i-%i, %i\n", _FL, (int)0, (int)End->Offset, (int)End->Blk->Length()); End->Blk->DeleteAt(NoTransaction, 0, End->Offset, DelTxt); } // Try and merge the start and end blocks bool MergeOk = Merge(NoTransaction, Start->Blk, End->Blk); MultiState->Length = MergeOk ? 1 : 2; Trans->Add(MultiState.Release()); } // Set the cursor and update the screen Cursor->Set(Start->Blk, Start->Offset, Start->LineHint); Selection.Reset(); View->Invalidate(); if (Cut) { DelTxt->Add(0); *Cut = (char16*)LNewConvertCp(LGI_WideCharset, &DelTxt->First(), "utf-32", DelTxt->Length()*sizeof(uint32_t)); } return true; } LRichTextPriv::Block *LRichTextPriv::Next(Block *b) { ssize_t Idx = Blocks.IndexOf(b); if (Idx < 0) return NULL; if (++Idx >= (int)Blocks.Length()) return NULL; return Blocks[Idx]; } LRichTextPriv::Block *LRichTextPriv::Prev(Block *b) { ssize_t Idx = Blocks.IndexOf(b); if (Idx <= 0) return NULL; return Blocks[--Idx]; } bool LRichTextPriv::AddTrans(LAutoPtr &t) { if (t) { if (UndoPosLock) { LgiTrace("%s:%i - AddTrans failed - UndoPosLocked.\n", _FL); return false; } // Delete any transaction history after 'UndoPos' for (size_t i=UndoPos; i UndoPos) { // Forward in queue Transaction *t = UndoQue[UndoPos]; UndoPosLock = true; if (!t->Apply(this, true)) goto ApplyError; UndoPosLock = false; LAssert(UndoPos == Prev); UndoPos++; } else if (Pos < UndoPos) { // Back in queue Transaction *t = UndoQue[UndoPos-1]; UndoPosLock = true; if (!t->Apply(this, false)) goto ApplyError; UndoPosLock = false; LAssert(UndoPos == Prev); UndoPos--; } else break; // We are done } Dirty = true; InvalidateDoc(NULL); return true; ApplyError: UndoPosLock = false; return false; } bool LRichTextPriv::IsBusy(bool Stop) { for (unsigned i=0; iIsBusy(Stop)) return true; } return false; } bool LRichTextPriv::Error(const char *file, int line, const char *fmt, ...) { va_list Arg; va_start(Arg, fmt); LString s; LPrintf(s, fmt, Arg); va_end(Arg); LString Err; Err.Printf("%s:%i - Error: %s\n", file, line, s.Get()); Log->Write(Err, Err.Length()); Err = LogBuffer.NewLStr(); LgiTrace("%.*s", Err.Length(), Err.Get()); LAssert(0); return false; } void LRichTextPriv::UpdateStyleUI() { if (!Cursor || !Cursor->Blk) { Error(_FL, "Not a valid cursor."); return; } TextBlock *b = dynamic_cast(Cursor->Blk); LArray Styles; if (b) b->GetTextAt(Cursor->Offset, Styles); StyleText *st = Styles.Length() ? Styles.First() : NULL; LFont *f = NULL; if (st) f = GetFont(st->GetStyle()); else if (View) f = View->GetFont(); else if (LAppInst) f = LSysFont; if (f) { Values[LRichTextEdit::FontFamilyBtn] = f->Face(); Values[LRichTextEdit::FontSizeBtn] = f->PointSize(); Values[LRichTextEdit::FontSizeBtn].CastString(); Values[LRichTextEdit::BoldBtn] = f->Bold(); Values[LRichTextEdit::ItalicBtn] = f->Italic(); Values[LRichTextEdit::UnderlineBtn] = f->Underline(); } else { Values[LRichTextEdit::FontFamilyBtn] = "(Unknown)"; } Values[LRichTextEdit::ForegroundColourBtn] = (int64) (st && st->Colours.Fore.IsValid() ? st->Colours.Fore.c32() : TextColour.c32()); Values[LRichTextEdit::BackgroundColourBtn] = (int64) (st && st->Colours.Back.IsValid() ? st->Colours.Back.c32() : 0); if (View) View->Invalidate(Areas + LRichTextEdit::ToolsArea); } void LRichTextPriv::ScrollTo(LRect r) { LRect Content = Areas[LRichTextEdit::ContentArea]; Content.Offset(-Content.x1, ScrollOffsetPx-Content.y1); if (ScrollLinePx > 0) { if (r.y1 < Content.y1) { int OffsetPx = MAX(r.y1, 0); View->SetScrollPos(0, OffsetPx / ScrollLinePx); InvalidateDoc(NULL); } if (r.y2 > Content.y2) { int OffsetPx = r.y2 - Content.Y(); View->SetScrollPos(0, (OffsetPx + ScrollLinePx - 1) / ScrollLinePx); InvalidateDoc(NULL); } } } void LRichTextPriv::InvalidateDoc(LRect *r) { // Transform the coordinates from doc to screen space LRect &c = Areas[LRichTextEdit::ContentArea]; if (r) { LRect t = *r; t.Offset(c.x1, c.y1 - ScrollOffsetPx); View->Invalidate(&t); } else View->Invalidate(&c); } void LRichTextPriv::EmptyDoc() { Block *Def = new TextBlock(this); if (Def) { Blocks.Add(Def); Cursor.Reset(new BlockCursor(Def, 0, 0)); UpdateStyleUI(); } } void LRichTextPriv::Empty() { // Delete cursors first to avoid hanging references Cursor.Reset(); Selection.Reset(); // Clear the block list.. Blocks.DeleteObjects(); } bool LRichTextPriv::Seek(BlockCursor *In, SeekType Dir, bool Select) { if (!In || !In->Blk || Blocks.Length() == 0) return Error(_FL, "Not a valid 'In' cursor, Blks=%i", Blocks.Length()); LAutoPtr c; bool Status = false; switch (Dir) { case SkLineEnd: case SkLineStart: case SkUpLine: case SkDownLine: { if (!c.Reset(new BlockCursor(*In))) break; Block *b = c->Blk; Status = b->Seek(Dir, *c); if (Status) break; if (Dir == SkUpLine) { // No more lines in the current block... // Move to the next block. ssize_t CurIdx = Blocks.IndexOf(b); ssize_t NewIdx = CurIdx - 1; if (NewIdx >= 0) { Block *b = Blocks[NewIdx]; if (!b) return Error(_FL, "No block at %i", NewIdx); c.Reset(new BlockCursor(b, b->Length(), b->GetLines() - 1)); LAssert(c->Offset >= 0); Status = true; } } else if (Dir == SkDownLine) { // No more lines in the current block... // Move to the next block. ssize_t CurIdx = Blocks.IndexOf(b); ssize_t NewIdx = CurIdx + 1; if ((unsigned)NewIdx < Blocks.Length()) { Block *b = Blocks[NewIdx]; if (!b) return Error(_FL, "No block at %i", NewIdx); c.Reset(new BlockCursor(b, 0, 0)); LAssert(c->Offset >= 0); Status = true; } } break; } case SkDocStart: { if (!c.Reset(new BlockCursor(Blocks[0], 0, 0))) break; Status = true; break; } case SkDocEnd: { if (Blocks.Length() == 0) break; Block *l = Blocks.Last(); if (!c.Reset(new BlockCursor(l, l->Length(), -1))) break; Status = true; break; } case SkLeftChar: { if (!c.Reset(new BlockCursor(*In))) break; if (c->Offset > 0) { LArray Ln; c->Blk->OffsetToLine(c->Offset, NULL, &Ln); if (Ln.Length() == 2 && c->LineHint == Ln.Last()) { c->LineHint = Ln.First(); } else { c->Offset--; if (c->Blk->OffsetToLine(c->Offset, NULL, &Ln)) c->LineHint = Ln.First(); } Status = true; } else // Seek to previous block { SeekPrevBlock: ssize_t Idx = Blocks.IndexOf(c->Blk); if (Idx < 0) { LAssert(0); break; } if (Idx == 0) break; // Beginning of document Block *b = Blocks[--Idx]; if (!b) { LAssert(0); break; } if (!c.Reset(new BlockCursor(b, b->Length(), b->GetLines()-1))) break; Status = true; } break; } case SkLeftWord: { if (!c.Reset(new BlockCursor(*In))) break; if (c->Offset > 0) { LArray a; c->Blk->CopyAt(0, c->Offset, &a); ssize_t i = c->Offset; while (i > 0 && IsWordBreakChar(a[i-1])) i--; while (i > 0 && !IsWordBreakChar(a[i-1])) i--; c->Offset = i; LArray Ln; if (c->Blk->OffsetToLine(c->Offset, NULL, &Ln)) c->LineHint = Ln[0]; Status = true; } else // Seek into previous block? { goto SeekPrevBlock; } break; } case SkRightChar: { if (!c.Reset(new BlockCursor(*In))) break; if (c->Offset < c->Blk->Length()) { LArray Ln; if (c->Blk->OffsetToLine(c->Offset, NULL, &Ln) && Ln.Length() == 2 && c->LineHint == Ln.First()) { c->LineHint = Ln.Last(); } else { c->Offset++; if (c->Blk->OffsetToLine(c->Offset, NULL, &Ln)) c->LineHint = Ln.First(); } Status = true; } else // Seek to next block { SeekNextBlock: ssize_t Idx = Blocks.IndexOf(c->Blk); if (Idx < 0) return Error(_FL, "Block ptr index error."); if (Idx >= (int)Blocks.Length() - 1) break; // End of document Block *b = Blocks[++Idx]; if (!b) return Error(_FL, "No block at %i.", Idx); if (!c.Reset(new BlockCursor(b, 0, 0))) break; Status = true; } break; } case SkRightWord: { if (!c.Reset(new BlockCursor(*In))) break; if (c->Offset < c->Blk->Length()) { LArray a; ssize_t RemainingCh = c->Blk->Length() - c->Offset; c->Blk->CopyAt(c->Offset, RemainingCh, &a); int i = 0; while (i < RemainingCh && !IsWordBreakChar(a[i])) i++; while (i < RemainingCh && IsWordBreakChar(a[i])) i++; c->Offset += i; LArray Ln; if (c->Blk->OffsetToLine(c->Offset, NULL, &Ln)) c->LineHint = Ln.Last(); else c->LineHint = -1; Status = true; } else // Seek into next block? { goto SeekNextBlock; } break; } case SkUpPage: { LRect &Content = Areas[LRichTextEdit::ContentArea]; int LineHint = -1; int TargetY = In->Pos.y1 - Content.Y(); ssize_t Idx = HitTest(In->Pos.x1, MAX(TargetY, 0), LineHint); if (Idx >= 0) { ssize_t Offset = -1; Block *b = GetBlockByIndex(Idx, &Offset); if (b) { if (!c.Reset(new BlockCursor(b, Offset, LineHint))) break; Status = true; } } break; } case SkDownPage: { LRect &Content = Areas[LRichTextEdit::ContentArea]; int LineHint = -1; int TargetY = In->Pos.y1 + Content.Y(); ssize_t Idx = HitTest(In->Pos.x1, MIN(TargetY, DocumentExtent.y-1), LineHint); if (Idx >= 0) { ssize_t Offset = -1; int BlkIdx = -1; ssize_t CursorBlkIdx = Blocks.IndexOf(Cursor->Blk); Block *b = GetBlockByIndex(Idx, &Offset, &BlkIdx); if (!b || BlkIdx < CursorBlkIdx || (BlkIdx == CursorBlkIdx && Offset < Cursor->Offset)) { LAssert(!"GetBlockByIndex failed.\n"); LgiTrace("%s:%i - GetBlockByIndex failed.\n", _FL); } if (b) { if (!c.Reset(new BlockCursor(b, Offset, LineHint))) break; Status = true; } } break; } default: { return Error(_FL, "Unknown seek type."); } } if (Status) SetCursor(c, Select); return Status; } bool LRichTextPriv::CursorFirst() { if (!Cursor || !Selection) return true; ssize_t CIdx = Blocks.IndexOf(Cursor->Blk); ssize_t SIdx = Blocks.IndexOf(Selection->Blk); if (CIdx != SIdx) return CIdx < SIdx; return Cursor->Offset < Selection->Offset; } bool LRichTextPriv::SetCursor(LAutoPtr c, bool Select) { LRect InvalidRc(0, 0, -1, -1); if (!c || !c->Blk) return Error(_FL, "Invalid cursor."); if (Select && !Selection) { // Selection starting... save cursor as selection end point if (Cursor) InvalidRc = Cursor->Line; Selection = Cursor; } else if (!Select && Selection) { // Selection ending... invalidate selection region and delete // selection end point LRect r = SelectionRect(); InvalidateDoc(&r); Selection.Reset(); // LgiTrace("Ending selection delete(sel) Idx=%i\n", i); } else if (Select && Cursor) { // Changing selection... InvalidRc = Cursor->Line; } if (Cursor && !Select) { // Just moving cursor InvalidateDoc(&Cursor->Pos); } if (!Cursor) Cursor.Reset(new BlockCursor(*c)); else Cursor = c; // LgiTrace("%s:%i - SetCursor: %i, line: %i\n", _FL, Cursor->Offset, Cursor->LineHint); if (Cursor && Selection && *Cursor == *Selection) Selection.Reset(); Cursor->Blk->GetPosFromIndex(Cursor); UpdateStyleUI(); #if DEBUG_OUTLINE_CUR_DISPLAY_STR || DEBUG_OUTLINE_CUR_STYLE_TEXT InvalidateDoc(NULL); #else if (Select) InvalidRc.Union(&Cursor->Line); else InvalidateDoc(&Cursor->Pos); if (InvalidRc.Valid()) { // Update the screen InvalidateDoc(&InvalidRc); } #endif // Check the cursor is on the visible part of the document. if (Cursor->Pos.Valid()) ScrollTo(Cursor->Pos); return true; } LRect LRichTextPriv::SelectionRect() { LRect SelRc; if (Cursor) { SelRc = Cursor->Line; if (Selection) SelRc.Union(&Selection->Line); } else if (Selection) { SelRc = Selection->Line; } return SelRc; } ssize_t LRichTextPriv::IndexOfCursor(BlockCursor *c) { if (!c || !c->Blk) { Error(_FL, "Invalid cursor param."); return -1; } ssize_t CharPos = 0; for (unsigned i=0; iBlk == b) return CharPos + c->Offset; CharPos += b->Length(); } LAssert(0); return -1; } LPoint LRichTextPriv::ScreenToDoc(int x, int y) { LRect &Content = Areas[LRichTextEdit::ContentArea]; return LPoint(x - Content.x1, y - Content.y1 + ScrollOffsetPx); } LPoint LRichTextPriv::DocToScreen(int x, int y) { LRect &Content = Areas[LRichTextEdit::ContentArea]; return LPoint(x + Content.x1, y + Content.y1 - ScrollOffsetPx); } bool LRichTextPriv::Merge(Transaction *Trans, Block *a, Block *b) { TextBlock *ta = dynamic_cast(a); TextBlock *tb = dynamic_cast(b); if (!ta || !tb) return false; ta->Txt.Add(tb->Txt); ta->LayoutDirty = true; ta->Len += tb->Len; tb->Txt.Length(0); Blocks.Delete(b, true); Dirty = true; return true; } LSurface *LEmojiImage::GetEmojiImage() { if (!EmojiImg) { LString p = LGetSystemPath(LSP_APP_INSTALL); if (!p) { LgiTrace("%s:%i - No app install path.\n", _FL); return NULL; } char File[MAX_PATH_LEN] = ""; LMakePath(File, sizeof(File), p, "..\\src\\common\\Text\\Emoji\\EmojiMap.png"); LString a; if (!LFileExists(File)) a = LFindFile("EmojiMap.png"); EmojiImg.Reset(GdcD->Load(a ? a : File, false)); } return EmojiImg; } ssize_t LRichTextPriv::HitTest(int x, int y, int &LineHint, Block **Blk, ssize_t *BlkOffset) { ssize_t CharPos = 0; HitTestResult r(x, y); if (Blocks.Length() == 0) { Error(_FL, "No blocks."); return -1; } Block *b = Blocks.First(); LRect rc = b->GetPos(); if (y < rc.y1) { if (Blk) *Blk = b; return 0; } for (unsigned i=0; iGetPos(); bool Over = y >= p.y1 && y <= p.y2; if (b->HitTest(r)) { LineHint = r.LineHint; if (Blk) *Blk = b; if (BlkOffset) *BlkOffset = r.Idx; return CharPos + r.Idx; } else if (Over) { Error(_FL, "Block failed to hit, i=%i, pos=%s, y=%i.", i, p.GetStr(), y); } CharPos += b->Length(); } b = Blocks.Last(); rc = b->GetPos(); if (y > rc.y2) { if (Blk) *Blk = b; return CharPos + b->Length(); } return -1; } bool LRichTextPriv::CursorFromPos(int x, int y, LAutoPtr *Cursor, ssize_t *GlobalIdx) { ssize_t CharPos = 0; HitTestResult r(x, y); for (unsigned i=0; iHitTest(r)) { if (Cursor) Cursor->Reset(new BlockCursor(b, r.Idx, r.LineHint)); if (GlobalIdx) *GlobalIdx = CharPos + r.Idx; return true; } CharPos += b->Length(); } return false; } LRichTextPriv::Block *LRichTextPriv::GetBlockByIndex(ssize_t Index, ssize_t *Offset, int *BlockIdx, int *LineCount) { ssize_t CharPos = 0; int Lines = 0; for (unsigned i=0; iLength(); int Ln = b->GetLines(); if (Index >= CharPos && Index < CharPos + Len) { if (BlockIdx) *BlockIdx = i; if (Offset) *Offset = Index - CharPos; return b; } CharPos += b->Length(); Lines += Ln; } Block *b = Blocks.Last(); if (Offset) *Offset = b->Length(); if (BlockIdx) *BlockIdx = (int)Blocks.Length() - 1; if (LineCount) *LineCount = Lines; return b; } bool LRichTextPriv::Layout(LScrollBar *&ScrollY) { Flow f(this); ScrollLinePx = View->GetFont()->GetHeight(); LAssert(ScrollLinePx > 0); if (ScrollLinePx <= 0) ScrollLinePx = 16; LRect Client = Areas[LRichTextEdit::ContentArea]; Client.Offset(-Client.x1, -Client.y1); DocumentExtent.x = Client.X(); LCssTools Ct(this, Font); LRect Content = Ct.ApplyPadding(Client); f.Left = Content.x1; f.Right = Content.x2; f.Top = f.CurY = Content.y1; for (unsigned i=0; iOnLayout(f); if ((f.CurY > Client.Y()) && ScrollY==NULL && !ScrollChange) { // We need to add a scroll bar View->SetScrollBars(false, true); View->Invalidate(); ScrollChange = true; return false; } } DocumentExtent.y = f.CurY + (Client.y2 - Content.y2); if (ScrollY) { int Lines = (f.CurY + ScrollLinePx - 1) / ScrollLinePx; int PageLines = (Client.Y() + ScrollLinePx - 1) / ScrollLinePx; ScrollY->SetPage(PageLines); ScrollY->SetRange(Lines); } if (Cursor) { LAssert(Cursor->Blk != NULL); if (Cursor->Blk) Cursor->Blk->GetPosFromIndex(Cursor); } return true; } void LRichTextPriv::OnStyleChange(LRichTextEdit::RectType t) { LCss s; switch (t) { case LRichTextEdit::FontFamilyBtn: { LCss::StringsDef Fam(Values[t].Str()); s.FontFamily(Fam); if (!ChangeSelectionStyle(&s, true)) StyleDirty.Add(t); break; } case LRichTextEdit::FontSizeBtn: { double Pt = Values[t].CastDouble(); s.FontSize(LCss::Len(LCss::LenPt, (float)Pt)); if (!ChangeSelectionStyle(&s, true)) StyleDirty.Add(t); break; } case LRichTextEdit::BoldBtn: { s.FontWeight(LCss::FontWeightBold); if (!ChangeSelectionStyle(&s, Values[t].CastBool())) StyleDirty.Add(t); break; } case LRichTextEdit::ItalicBtn: { s.FontStyle(LCss::FontStyleItalic); if (!ChangeSelectionStyle(&s, Values[t].CastBool())) StyleDirty.Add(t); break; } case LRichTextEdit::UnderlineBtn: { s.TextDecoration(LCss::TextDecorUnderline); if (ChangeSelectionStyle(&s, Values[t].CastBool())) StyleDirty.Add(t); break; } case LRichTextEdit::ForegroundColourBtn: { s.Color(LCss::ColorDef(LCss::ColorRgb, (uint32_t) Values[t].Value.Int64)); if (!ChangeSelectionStyle(&s, true)) StyleDirty.Add(t); break; } case LRichTextEdit::BackgroundColourBtn: { s.BackgroundColor(LCss::ColorDef(LCss::ColorRgb, (uint32_t) Values[t].Value.Int64)); if (!ChangeSelectionStyle(&s, true)) StyleDirty.Add(t); break; } default: break; } } bool LRichTextPriv::ChangeSelectionStyle(LCss *Style, bool Add) { if (!Selection) return false; LAutoPtr Trans(new Transaction); bool Cf = CursorFirst(); LRichTextPriv::BlockCursor *Start = Cf ? Cursor : Selection; LRichTextPriv::BlockCursor *End = Cf ? Selection : Cursor; if (Start->Blk == End->Blk) { // Change style in the same block... ssize_t Len = End->Offset - Start->Offset; if (!Start->Blk->ChangeStyle(Trans, Start->Offset, Len, Style, Add)) return false; } else { // Multi-block style change... // 1) Change style on the content to the end of the first block Start->Blk->ChangeStyle(Trans, Start->Offset, -1, Style, Add); // 2) Change style on blocks between 'Start' and 'End' ssize_t i = Blocks.IndexOf(Start->Blk); if (i >= 0) { for (++i; Blocks[i] != End->Blk && i < (int)Blocks.Length(); i++) { LRichTextPriv::Block *&b = Blocks[i]; if (!b->ChangeStyle(Trans, 0, -1, Style, Add)) return false; } } else { return Error(_FL, "Start block has no index."); } // 3) Change style up to the Cursor in the 'End' block if (!End->Blk->ChangeStyle(Trans, 0, End->Offset, Style, Add)) return false; } Cursor->Pos.ZOff(-1, -1); InvalidateDoc(NULL); AddTrans(Trans); return true; } void LRichTextPriv::PaintBtn(LSurface *pDC, LRichTextEdit::RectType t) { LRect r = Areas[t]; LVariant &v = Values[t]; bool Down = (v.Type == GV_BOOL && v.Value.Bool) || (BtnState[t].IsPress && BtnState[t].Pressed && BtnState[t].MouseOver); LSysFont->Colour(L_TEXT, BtnState[t].MouseOver ? L_LIGHT : L_MED); LSysFont->Transparent(false); LColour Low(96, 96, 96); pDC->Colour(Down ? LColour::White : Low); pDC->Line(r.x1, r.y2, r.x2, r.y2); pDC->Line(r.x2, r.y1, r.x2, r.y2); pDC->Colour(Down ? Low : LColour::White); pDC->Line(r.x1, r.y1, r.x2, r.y1); pDC->Line(r.x1, r.y1, r.x1, r.y2); r.Inset(1, 1); switch (v.Type) { case GV_STRING: { LDisplayString Ds(LSysFont, v.Str()); Ds.Draw(pDC, r.x1 + ((r.X()-Ds.X())>>1) + Down, r.y1 + ((r.Y()-Ds.Y())>>1) + Down, &r); break; } case GV_INT64: { if (v.Value.Int64) { pDC->Colour((uint32_t)v.Value.Int64, 32); pDC->Rectangle(&r); } else { // Transparent int g[2] = { 128, 192 }; pDC->ClipRgn(&r); for (int y=0; y>1)%2) ^ ((x>>1)%2); pDC->Colour(LColour(g[i],g[i],g[i])); pDC->Rectangle(r.x1+x, r.y1+y, r.x1+x+1, r.y1+y+1); } } pDC->ClipRgn(NULL); } break; } case GV_BOOL: { const char *Label = NULL; switch (t) { case LRichTextEdit::BoldBtn: Label = "B"; break; case LRichTextEdit::ItalicBtn: Label = "I"; break; case LRichTextEdit::UnderlineBtn: Label = "U"; break; default: break; } if (!Label) break; LDisplayString Ds(LSysFont, Label); Ds.Draw(pDC, r.x1 + ((r.X()-Ds.X())>>1) + Down, r.y1 + ((r.Y()-Ds.Y())>>1) + Down, &r); break; } default: break; } } bool LRichTextPriv::MakeLink(TextBlock *tb, ssize_t Offset, ssize_t Len, LString Link) { if (!tb) return false; LArray st; if (!tb->GetTextAt(Offset, st)) return false; LAutoPtr ns(new LNamedStyle); if (ns) { if (st.Last()->GetStyle()) *ns = *st.Last()->GetStyle(); ns->TextDecoration(LCss::TextDecorUnderline); ns->Color(LCss::ColorDef(LCss::ColorRgb, LColour::Blue.c32())); LAutoPtr Trans(new Transaction); tb->ChangeStyle(Trans, Offset, Len, ns, true); if (tb->GetTextAt(Offset + 1, st)) { st.First()->Element = TAG_A; st.First()->Param = Link; } AddTrans(Trans); } return true; } bool LRichTextPriv::ClickBtn(LMouse &m, LRichTextEdit::RectType t) { switch (t) { case LRichTextEdit::FontFamilyBtn: { LString::Array Fonts; if (!LFontSystem::Inst()->EnumerateFonts(Fonts)) return Error(_FL, "EnumerateFonts failed."); bool UseSub = (LSysFont->GetHeight() * Fonts.Length()) > (GdcD->Y() * 0.8); LSubMenu s; LSubMenu *Cur = NULL; int Idx = 1; char Last = 0; for (unsigned i=0; iAppendItem(f, Idx++); else break; } else { s.AppendItem(f, Idx++); } } LPoint p(Areas[t].x1, Areas[t].y2 + 1); View->PointToScreen(p); int Result = s.Float(View, p.x, p.y, true); if (Result) { Values[t] = Fonts[Result-1]; View->Invalidate(Areas+t); OnStyleChange(t); } break; } case LRichTextEdit::FontSizeBtn: { static const char *Sizes[] = { "6", "7", "8", "9", "10", "11", "12", "14", "16", "18", "20", "24", "28", "32", "40", "50", "60", "80", "100", "120", 0 }; LSubMenu s; for (int Idx = 0; Sizes[Idx]; Idx++) s.AppendItem(Sizes[Idx], Idx+1); LPoint p(Areas[t].x1, Areas[t].y2 + 1); View->PointToScreen(p); int Result = s.Float(View, p.x, p.y, true); if (Result) { Values[t] = Sizes[Result-1]; View->Invalidate(Areas+t); OnStyleChange(t); } break; } case LRichTextEdit::BoldBtn: case LRichTextEdit::ItalicBtn: case LRichTextEdit::UnderlineBtn: { Values[t] = !Values[t].CastBool(); View->Invalidate(Areas+t); OnStyleChange(t); break; } case LRichTextEdit::ForegroundColourBtn: case LRichTextEdit::BackgroundColourBtn: { LPoint p(Areas[t].x1, Areas[t].y2 + 1); View->PointToScreen(p); new SelectColour(this, p, t); break; } case LRichTextEdit::MakeLinkBtn: { if (!Cursor || !Cursor->Blk) break; TextBlock *tb = dynamic_cast(Cursor->Blk); if (!tb) break; LArray st; if (!tb->GetTextAt(Cursor->Offset, st)) break; StyleText *a = st.Length() > 1 && st[1]->Element == TAG_A ? st[1] : st.First()->Element == TAG_A ? st[0] : NULL; if (a) { // Edit the existing link... auto i = new LInput(View, a->Param, "Edit link:", "Link"); - i->DoModal([this, i, a](auto dlg, auto ctrlId) + i->DoModal([this, i, a](auto dlg, auto ok) { - if (ctrlId == IDOK) + if (ok) a->Param = i->GetStr(); delete dlg; }); } else if (Selection) { // Turn current selection into link auto i = new LInput(View, NULL, "Edit link:", "Link"); - i->DoModal([this, i, tb](auto dlg, auto ctrlId) + i->DoModal([this, i, tb](auto dlg, auto ok) { - if (ctrlId == IDOK) + if (ok) { BlockCursor *Start = CursorFirst() ? Cursor : Selection; BlockCursor *End = CursorFirst() ? Selection : Cursor; if (!Start || !End) ; else if (Start->Blk != End->Blk) LgiMsg(View, "Selection too large.", "Error"); else MakeLink(tb, Start->Offset, End->Offset - Start->Offset, i->GetStr()); } delete dlg; }); } break; } case LRichTextEdit::RemoveLinkBtn: { if (!Cursor || !Cursor->Blk) break; TextBlock *tb = dynamic_cast(Cursor->Blk); if (!tb) break; LArray st; if (!tb->GetTextAt(Cursor->Offset, st)) break; StyleText *a = st.Length() > 1 && st[1]->Element == TAG_A ? st[1] : st.First()->Element == TAG_A ? st[0] : NULL; if (a) { // Remove the existing link... a->Element = CONTENT; a->Param.Empty(); LAutoPtr s(new LCss); LNamedStyle *Ns = a->GetStyle(); if (Ns) *s = *Ns; if (s->TextDecoration() == LCss::TextDecorUnderline) s->DeleteProp(LCss::PropTextDecoration); if ((LColour)s->Color() == LColour::Blue) s->DeleteProp(LCss::PropColor); Ns = AddStyleToCache(s); a->SetStyle(Ns); tb->LayoutDirty = true; InvalidateDoc(NULL); } break; } case LRichTextEdit::RemoveStyleBtn: { LCss s; ChangeSelectionStyle(&s, false); break; } /* case LRichTextEdit::CapabilityBtn: { View->OnCloseInstaller(); break; } */ case LRichTextEdit::EmojiBtn: { LPoint p(Areas[t].x1, Areas[t].y2 + 1); View->PointToScreen(p); new EmojiMenu(this, p); break; } case LRichTextEdit::HorzRuleBtn: { InsertHorzRule(); break; } default: return false; } return true; } bool LRichTextPriv::InsertHorzRule() { if (!Cursor || !Cursor->Blk) return false; TextBlock *tb = dynamic_cast(Cursor->Blk); if (!tb) return false; LAutoPtr Trans(new Transaction); DeleteSelection(Trans, NULL); ssize_t InsertIdx = Blocks.IndexOf(tb) + 1; LRichTextPriv::Block *After = NULL; if (Cursor->Offset == 0) { InsertIdx--; } else if (Cursor->Offset < tb->Length()) { After = tb->Split(Trans, Cursor->Offset); if (!After) return false; tb->StripLast(Trans); } HorzRuleBlock *Hr = new HorzRuleBlock(this); if (!Hr) return false; Blocks.AddAt(InsertIdx++, Hr); if (After) Blocks.AddAt(InsertIdx++, After); AddTrans(Trans); InvalidateDoc(NULL); LAutoPtr c(new BlockCursor(Hr, 0, 0)); return SetCursor(c); } void LRichTextPriv::Paint(LSurface *pDC, LScrollBar *&ScrollY) { if (Areas[LRichTextEdit::ToolsArea].Valid()) { // Draw tools area... LRect &t = Areas[LRichTextEdit::ToolsArea]; #ifdef WIN32 LDoubleBuffer Buf(pDC, &t); #endif LColour ToolBar = LColour(L_FOCUS_SEL_BACK).Mix(LColour(L_LOW)); pDC->Colour(ToolBar); pDC->Rectangle(&t); LRect r = t; r.Inset(3, 3); #define AllocPx(sz, border) \ LRect(r.x1, r.y1, r.x1 + (int)(sz) - 1, r.y2); r.x1 += (int)(sz) + border Areas[LRichTextEdit::FontFamilyBtn] = AllocPx(130, 6); Areas[LRichTextEdit::FontSizeBtn] = AllocPx(40, 6); Areas[LRichTextEdit::BoldBtn] = AllocPx(r.Y(), 0); Areas[LRichTextEdit::ItalicBtn] = AllocPx(r.Y(), 0); Areas[LRichTextEdit::UnderlineBtn] = AllocPx(r.Y(), 6); Areas[LRichTextEdit::ForegroundColourBtn] = AllocPx(r.Y()*1.5, 0); Areas[LRichTextEdit::BackgroundColourBtn] = AllocPx(r.Y()*1.5, 6); { LDisplayString Ds(LSysFont, TEXT_LINK); Areas[LRichTextEdit::MakeLinkBtn] = AllocPx(Ds.X() + 12, 0); } { LDisplayString Ds(LSysFont, TEXT_REMOVE_LINK); Areas[LRichTextEdit::RemoveLinkBtn] = AllocPx(Ds.X() + 12, 6); } { LDisplayString Ds(LSysFont, TEXT_REMOVE_STYLE); Areas[LRichTextEdit::RemoveStyleBtn] = AllocPx(Ds.X() + 12, 6); } for (unsigned int i=LRichTextEdit::EmojiBtn; iGetColourSpace())) { LAssert(!"MemDC creation failed."); return; } LSurface *pScreen = pDC; pDC = &Mem; r.Offset(-r.x1, -r.y1); #else pDC->GetOrigin(Origin.x, Origin.y); pDC->ClipRgn(&r); #endif ScrollOffsetPx = ScrollY ? (int)(ScrollY->Value() * ScrollLinePx) : 0; pDC->SetOrigin(Origin.x-r.x1, Origin.y-r.y1+ScrollOffsetPx); int DrawPx = ScrollOffsetPx + Areas[LRichTextEdit::ContentArea].Y(); int ExtraPx = DrawPx > DocumentExtent.y ? DrawPx - DocumentExtent.y : 0; r.Set(0, 0, DocumentExtent.x-1, DocumentExtent.y-1); // Fill the padding... LCssTools ct(this, Font); r = ct.PaintPadding(pDC, r); // Fill the background... #if DEBUG_COVERAGE_CHECK pDC->Colour(LColour(255, 0, 255)); #else LCss::ColorDef cBack = BackgroundColor(); // pDC->Colour(cBack.IsValid() ? (LColour)cBack : LColour(LC_WORKSPACE, 24)); #endif pDC->Rectangle(&r); if (ExtraPx) pDC->Rectangle(0, DocumentExtent.y, DocumentExtent.x-1, DocumentExtent.y+ExtraPx); PaintContext Ctx; Ctx.pDC = pDC; Ctx.Cursor = Cursor; Ctx.Select = Selection; Ctx.Colours[Unselected].Fore.Set(L_TEXT); Ctx.Colours[Unselected].Back.Set(L_WORKSPACE); if (View->Focus()) { Ctx.Colours[Selected].Fore.Set(L_FOCUS_SEL_FORE); Ctx.Colours[Selected].Back.Set(L_FOCUS_SEL_BACK); } else { Ctx.Colours[Selected].Fore.Set(L_NON_FOCUS_SEL_FORE); Ctx.Colours[Selected].Back.Set(L_NON_FOCUS_SEL_BACK); } for (unsigned i=0; iOnPaint(Ctx); #if DEBUG_OUTLINE_BLOCKS pDC->Colour(LColour(192, 192, 192)); pDC->LineStyle(LSurface::LineDot); pDC->Box(&b->GetPos()); pDC->LineStyle(LSurface::LineSolid); #endif } } #ifdef _DEBUG pDC->Colour(LColour::Green); for (unsigned i=0; iBox(&DebugRects[i]); } #endif #if 0 // Outline the line the cursor is on if (Cursor) { pDC->Colour(LColour::Blue); pDC->LineStyle(LSurface::LineDot); pDC->Box(&Cursor->Line); } #endif #if defined(WINDOWS) && !DEBUG_NO_DOUBLE_BUF Mem.SetOrigin(0, 0); pScreen->Blt(Areas[LRichTextEdit::ContentArea].x1, Areas[LRichTextEdit::ContentArea].y1, &Mem); #else pDC->ClipRgn(NULL); #endif } LHtmlElement *LRichTextPriv::CreateElement(LHtmlElement *Parent) { return new LRichEditElem(Parent); } bool LRichTextPriv::ToHtml(LArray *Media, BlockCursor *From, BlockCursor *To) { UtfNameCache.Reset(); if (!Blocks.Length()) return false; LStringPipe p(256); p.Print("\n" "\n" "\t\n"); ZeroRefCounts(); ssize_t Start = From ? Blocks.IndexOf(From->Blk) : 0; ssize_t End = To ? Blocks.IndexOf(To->Blk) : Blocks.Length() - 1; ssize_t StartIdx = From ? From->Offset : 0; ssize_t EndIdx = To ? To->Offset : Blocks.Last()->Length(); for (ssize_t i=Start; i<=End; i++) { Blocks[i]->IncAllStyleRefs(); } if (GetStyles()) { p.Print("\t\n"); } p.Print("\n" "\n"); for (ssize_t i=Start; i<=End; i++) { Block *b = Blocks[i]; LRange r; if (i == Start) r.Start = StartIdx; if (i == End) r.Len = EndIdx - r.Start; b->ToHtml(p, Media, r.Valid() ? &r : NULL); } p.Print("\n\n"); LAutoString a(p.NewStr()); UtfNameCache = a; return UtfNameCache.Get() != NULL; } void LRichTextPriv::DumpBlocks() { LgiTrace("LRichTextPriv Blocks=%i\n", Blocks.Length()); for (unsigned i=0; iGetClass(), b->GetStyle(), b->GetStyle() ? b->GetStyle()->Name.Get() : NULL); b->Dump(); LgiTrace("}\n"); } } struct HtmlElementCb : public LCss::ElementCallback { const char *GetElement(LHtmlElement *obj) { return obj->Tag; } const char *GetAttr(LHtmlElement *obj, const char *Attr) { const char *Val = NULL; return obj->Get(Attr, Val) ? Val : NULL; } bool GetClasses(LString::Array &Classes, LHtmlElement *obj) { const char *Cls = NULL; if (!obj->Get("class", Cls)) return false; Classes = LString(Cls).SplitDelimit(); return true; } LHtmlElement *GetParent(LHtmlElement *obj) { return obj->Parent; } LArray GetChildren(LHtmlElement *obj) { return obj->Children; } }; bool LRichTextPriv::FromHtml(LHtmlElement *e, CreateContext &ctx, LCss *ParentStyle, int Depth) { char Sp[48]; int SpLen = MIN(Depth << 1, sizeof(Sp) - 1); memset(Sp, ' ', SpLen); Sp[SpLen] = 0; for (unsigned i = 0; i < e->Children.Length(); i++) { LHtmlElement *c = e->Children[i]; LAutoPtr Style; if (ParentStyle) Style.Reset(new LCss(*ParentStyle)); LCss::SelArray Matches; HtmlElementCb Cb; if (ctx.StyleStore.Match(Matches, &Cb, c) && Matches.Length() > 0 && (Style.Get() || Style.Reset(new LCss))) { for (auto s : Matches) { const char *p = s->Style; Style->Parse(p, LCss::ParseRelaxed); } } // Check to see if the element is block level and end the previous // paragraph if so. c->Info = c->Tag ? LHtmlStatic::Inst->GetTagInfo(c->Tag) : NULL; bool IsBlock = c->Info != NULL && c->Info->Block(); switch (c->TagId) { case TAG_STYLE: { char16 *Style = e->GetText(); if (ValidStrW(Style)) LAssert(!"Impl me."); continue; break; } case TAG_B: { if (!Style) Style.Reset(new LCss); if (Style) Style->FontWeight(LCss::FontWeightBold); break; } case TAG_I: { if (!Style) Style.Reset(new LCss); if (Style) Style->FontStyle(LCss::FontStyleItalic); break; } case TAG_BLOCKQUOTE: { if (!Style) Style.Reset(new LCss); if (Style) { Style->MarginTop(LCss::Len("0.5em")); Style->MarginBottom(LCss::Len("0.5em")); Style->MarginLeft(LCss::Len("1em")); if (ctx.Tb) ctx.Tb->StripLast(NoTransaction); } break; } case TAG_A: { if (!Style) Style.Reset(new LCss); if (Style) { Style->TextDecoration(LCss::TextDecorUnderline); Style->Color(LCss::ColorDef(LCss::ColorRgb, LColour::Blue.c32())); } break; } case TAG_HR: { if (ctx.Tb) ctx.Tb->StripLast(NoTransaction); // Fall through } case TAG_IMG: { ctx.Tb = NULL; IsBlock = true; break; } default: { break; } } const char *Css, *Class; if (c->Get("style", Css)) { if (!Style) Style.Reset(new LCss); if (Style) Style->Parse(Css, ParseRelaxed); } if (c->Get("class", Class)) { LCss::SelArray Selectors; LRichEditElemContext StyleCtx; if (ctx.StyleStore.Match(Selectors, &StyleCtx, dynamic_cast(c))) { for (unsigned n=0; nStyle; if (s) { if (!Style) Style.Reset(new LCss); if (Style) Style->Parse(s, LCss::ParseRelaxed); } } } } } LNamedStyle *CachedStyle = AddStyleToCache(Style); if ( (IsBlock && ctx.LastChar != '\n') || c->TagId == TAG_BR ) { if (!ctx.Tb && c->TagId == TAG_BR) { // Don't do this for IMG and HR layout. Blocks.Add(ctx.Tb = new TextBlock(this)); if (CachedStyle && ctx.Tb) ctx.Tb->SetStyle(CachedStyle); } if (ctx.Tb) { const uint32_t Nl[] = {'\n', 0}; ctx.Tb->AddText(NoTransaction, -1, Nl, 1, NULL); ctx.LastChar = '\n'; ctx.StartOfLine = true; } } bool EndStyleChange = false; if (c->TagId == TAG_IMG) { Blocks.Add(ctx.Ib = new ImageBlock(this)); if (ctx.Ib) { const char *s; if (c->Get("src", s)) ctx.Ib->Source = s; if (c->Get("width", s)) { LCss::Len Sz(s); int Px = Sz.ToPx(); if (Px) ctx.Ib->Size.x = Px; } if (c->Get("height", s)) { LCss::Len Sz(s); int Px = Sz.ToPx(); if (Px) ctx.Ib->Size.y = Px; } if (CachedStyle) ctx.Ib->SetStyle(CachedStyle); ctx.Ib->Load(); } } else if (c->TagId == TAG_HR) { Blocks.Add(ctx.Hrb = new HorzRuleBlock(this)); } else if (c->TagId == TAG_A) { ctx.StartOfLine |= ctx.AddText(CachedStyle, c->GetText()); if (ctx.Tb && ctx.Tb->Txt.Length()) { StyleText *st = ctx.Tb->Txt.Last(); st->Element = TAG_A; const char *Link; if (c->Get("href", Link)) st->Param = Link; } } else { if (IsBlock && ctx.Tb != NULL) { if (CachedStyle != ctx.Tb->GetStyle()) { if (ctx.Tb->Length() == 0) { ctx.Tb->SetStyle(CachedStyle); } else { // Start a new block because the styles are different... EndStyleChange = true; auto Idx = Blocks.IndexOf(ctx.Tb); ctx.Tb = new TextBlock(this); if (Idx >= 0) Blocks.AddAt(Idx+1, ctx.Tb); else Blocks.Add(ctx.Tb); if (CachedStyle) ctx.Tb->SetStyle(CachedStyle); } } } char16 *Txt = c->GetText(); if ( Txt && ( !ctx.StartOfLine || ValidStrW(Txt) ) ) { if (!ctx.Tb) { Blocks.Add(ctx.Tb = new TextBlock(this)); ctx.Tb->SetStyle(CachedStyle); } #ifdef __GTK_H__ for (auto *i = Txt; *i; i++) if (*i == 0xa0) *i = ' '; #endif ctx.AddText(CachedStyle, Txt); ctx.StartOfLine = false; } } if (!FromHtml(c, ctx, Style, Depth + 1)) return false; if (EndStyleChange) ctx.Tb = NULL; if (IsBlock) ctx.StartOfLine = true; } return true; } bool LRichTextPriv::GetSelection(LArray *Text, LAutoString *Html) { if (!Text && !Html) return false; LArray Utf32; bool Cf = CursorFirst(); LRichTextPriv::BlockCursor *Start = Cf ? Cursor : Selection; LRichTextPriv::BlockCursor *End = Cf ? Selection : Cursor; if (Html) { if (ToHtml(NULL, Start, End)) *Html = UtfNameCache; } if (Text) { if (Start->Blk == End->Blk) { // In the same block... just copy ssize_t Len = End->Offset - Start->Offset; Start->Blk->CopyAt(Start->Offset, Len, &Utf32); } else { // Multi-block copy... // 1) Copy the content to the end of the first block Start->Blk->CopyAt(Start->Offset, -1, &Utf32); // 2) Copy any blocks between 'Start' and 'End' ssize_t i = Blocks.IndexOf(Start->Blk); ssize_t EndIdx = Blocks.IndexOf(End->Blk); if (i >= 0 && EndIdx >= i) { for (++i; Blocks[i] != End->Blk && i < (int)Blocks.Length(); i++) { LRichTextPriv::Block *&b = Blocks[i]; b->CopyAt(0, -1, &Utf32); } } else return Error(_FL, "Blocks missing index: %i, %i.", i, EndIdx); // 3) Delete any text up to the Cursor in the 'End' block End->Blk->CopyAt(0, End->Offset, &Utf32); } char16 *w = (char16*)LNewConvertCp(LGI_WideCharset, &Utf32[0], "utf-32", Utf32.Length() * sizeof(uint32_t)); if (!w) return Error(_FL, "Failed to convert %i utf32 to wide.", Utf32.Length()); Text->Add(w, Strlen(w)); Text->Add(0); } return true; } LRichTextEdit::RectType LRichTextPriv::PosToButton(LMouse &m) { if (Areas[LRichTextEdit::ToolsArea].Overlap(m.x, m.y) // || Areas[LRichTextEdit::CapabilityArea].Overlap(m.x, m.y) ) { for (unsigned i=LRichTextEdit::FontFamilyBtn; iOnComponentInstall(Name); } } #ifdef _DEBUG void LRichTextPriv::DumpNodes(LTree *Root) { if (Cursor) { LTreeItem *ti = new LTreeItem; ti->SetText("Cursor"); PrintNode(ti, "Offset=%i", Cursor->Offset); PrintNode(ti, "Pos=%s", Cursor->Pos.GetStr()); PrintNode(ti, "LineHint=%i", Cursor->LineHint); PrintNode(ti, "Blk=%i", Cursor->Blk ? Blocks.IndexOf(Cursor->Blk) : -2); Root->Insert(ti); } if (Selection) { LTreeItem *ti = new LTreeItem; ti->SetText("Selection"); PrintNode(ti, "Offset=%i", Selection->Offset); PrintNode(ti, "Pos=%s", Selection->Pos.GetStr()); PrintNode(ti, "LineHint=%i", Selection->LineHint); PrintNode(ti, "Blk=%i", Selection->Blk ? Blocks.IndexOf(Selection->Blk) : -2); Root->Insert(ti); } for (unsigned i=0; iDumpNodes(ti); LString s; s.Printf("[%i] %s", i, ti->GetText()); ti->SetText(s); Root->Insert(ti); } } LTreeItem *PrintNode(LTreeItem *Parent, const char *Fmt, ...) { LTreeItem *i = new LTreeItem; LString s; va_list Arg; va_start(Arg, Fmt); s.Printf(Arg, Fmt); va_end(Arg); s = s.Replace("\n", "\\n"); i->SetText(s); Parent->Insert(i); return i; } #endif diff --git a/src/common/Widgets/RadioGroup.cpp b/src/common/Widgets/RadioGroup.cpp --- a/src/common/Widgets/RadioGroup.cpp +++ b/src/common/Widgets/RadioGroup.cpp @@ -1,815 +1,861 @@ #if !defined(_WIN32) || (XP_BUTTON != 0) #include #include #include "lgi/common/Lgi.h" #include "lgi/common/SkinEngine.h" #include "lgi/common/RadioGroup.h" #include "lgi/common/CheckBox.h" #include "lgi/common/DisplayString.h" #include "lgi/common/LgiRes.h" #include "lgi/common/StringLayout.h" #define RADIO_GRID 4 /////////////////////////////////////////////////////////////////////////////////////////// // Radio group static int MinYSize = 16; class LRadioGroupPrivate : public LMutex, public LStringLayout { LRadioGroup *Ctrl; public: static int NextId; int Val; int MaxLayoutWidth; LHashTbl,LViewLayoutInfo*> Info; + LArray GroupIDs; LRadioGroupPrivate(LRadioGroup *g) : LMutex("LRadioGroupPrivate"), LStringLayout(LAppInst->GetFontCache()) { Ctrl = g; Val = 0; MaxLayoutWidth = 0; AmpersandToUnderline = true; } ~LRadioGroupPrivate() { Info.DeleteObjects(); } bool PreLayout(int32 &Min, int32 &Max) { if (Lock(_FL)) { DoPreLayout(Min, Max); Unlock(); } else return false; return true; } bool Layout(int Px) { if (Lock(_FL)) { DoLayout(Px, MinYSize); Unlock(); } else return false; return true; } }; int LRadioGroupPrivate::NextId = 10000; LRadioGroup::LRadioGroup(int id, int x, int y, int cx, int cy, const char *name, int Init) : ResObject(Res_Group) { d = new LRadioGroupPrivate(this); Name(name); LRect r(x, y, x+cx, y+cy); SetPos(r); SetId(id); d->Val = Init; LResources::StyleElement(this); } LRadioGroup::~LRadioGroup() { DeleteObj(d); } void LRadioGroup::OnStyleChange() { if (d->Lock(_FL)) { d->Empty(); d->Add(LView::Name(), GetCss()); d->DoLayout(X()); d->Unlock(); Invalidate(); } } bool LRadioGroup::OnLayout(LViewLayoutInfo &Inf) { auto children = IterateViews(); const int BORDER_PX = 2; int MinPx = (RADIO_GRID + BORDER_PX) * 2; if (!Inf.Width.Max) { // Work out the width... d->PreLayout(Inf.Width.Min, Inf.Width.Max); Inf.Width.Min += MinPx; Inf.Width.Max += MinPx; d->Info.DeleteObjects(); // Inf.Width.Min = 16 + TextPx; // Inf.Width.Max = RADIO_GRID + BORDER_PX * 2; for (LViewI *w: children) { LAutoPtr c(new LViewLayoutInfo); if (w->OnLayout(*c)) { // Layout enabled control Inf.Width.Min = MAX(Inf.Width.Min, c->Width.Min + MinPx); Inf.Width.Max += c->Width.Max + RADIO_GRID; d->Info.Add(w, c.Release()); } else { // Non layout enabled control Inf.Width.Min = MAX(Inf.Width.Min, w->X() + (RADIO_GRID << 1)); Inf.Width.Max += w->X() + RADIO_GRID; } } if (Inf.Width.Max < Inf.Width.Min) Inf.Width.Max = Inf.Width.Min; d->MaxLayoutWidth = Inf.Width.Max; } else { d->Layout(Inf.Width.Max); Inf.Height.Min = d->GetMin().y + MinPx; Inf.Height.Max = d->GetMax().y + MinPx; // Working out the height, and positioning the controls // Inf.Height.Min = d->Txt ? d->Txt->Y() : 16; bool Horiz = d->MaxLayoutWidth <= Inf.Width.Max; int Cx = BORDER_PX + RADIO_GRID, Cy = d->GetMin().y; int LastY = 0; for (LViewI *w: children) { LViewLayoutInfo *c = d->Info.Find(w); if (c) { if (w->OnLayout(*c)) { LRect r(Cx, Cy, Cx + c->Width.Max - 1, Cy + c->Height.Max - 1); w->SetPos(r); if (Horiz) // Horizontal layout Cx += r.X() + RADIO_GRID; else // Vertical layout Cy += r.Y() + RADIO_GRID; LastY = MAX(LastY, r.y2); } else LAssert(!"This shouldn't fail."); } else { // Non layout control... just use existing size LRect r = w->GetPos(); r.Offset(Cx - r.x1, Cy - r.y1); w->SetPos(r); if (Horiz) // Horizontal layout Cx += r.X() + RADIO_GRID; else // Vertical layout Cy += r.Y() + RADIO_GRID; LastY = MAX(LastY, r.y2); } } Inf.Height.Min = Inf.Height.Max = LastY + RADIO_GRID * 2 + BORDER_PX; } return true; } void LRadioGroup::OnAttach() { LResources::StyleElement(this); OnStyleChange(); LView::OnAttach(); } LMessage::Result LRadioGroup::OnEvent(LMessage *m) { return LView::OnEvent(m); } bool LRadioGroup::Name(const char *n) { bool Status = false; if (d->Lock(_FL)) { Status = LView::Name(n); d->Empty(); d->Add(n, GetCss()); d->SetBaseFont(GetFont()); d->DoLayout(X()); d->Unlock(); } return Status; } bool LRadioGroup::NameW(const char16 *n) { bool Status = false; if (d->Lock(_FL)) { Status = LView::NameW(n); d->Empty(); d->Add(LBase::Name(), GetCss()); d->SetBaseFont(GetFont()); d->DoLayout(X()); d->Unlock(); } return Status; } void LRadioGroup::SetFont(LFont *Fnt, bool OwnIt) { LAssert(Fnt && Fnt->Handle()); if (d->Lock(_FL)) { LView::SetFont(Fnt, OwnIt); d->Unlock(); } d->Layout(X()); Invalidate(); } void LRadioGroup::OnCreate() { AttachChildren(); Value(d->Val); } int64 LRadioGroup::Value() { int i=0; for (auto w: Children) { LRadioButton *But = dynamic_cast(w); if (But) { if (But->Value()) { d->Val = i; break; } i++; } } return d->Val; } void LRadioGroup::Value(int64 Which) { d->Val = (int)Which; int i=0; for (auto w: Children) { LRadioButton *But = dynamic_cast(w); if (But) { if (i == Which) { But->Value(true); break; } i++; } } } int LRadioGroup::OnNotify(LViewI *Ctrl, LNotification n) { LViewI *v = GetNotify() ? GetNotify() : GetParent(); if (v) { if (dynamic_cast(Ctrl)) { return v->OnNotify(this, n); } else { return v->OnNotify(Ctrl, n); } } return 0; } void LRadioGroup::OnPaint(LSurface *pDC) { if (LApp::SkinEngine && TestFlag(LApp::SkinEngine->GetFeatures(), GSKIN_GROUP)) { LSkinState State; State.pScreen = pDC; State.MouseOver = false; State.aText = d->GetStrs(); LApp::SkinEngine->OnPaint_LRadioGroup(this, &State); } else { // LColour Fore = StyleColour(LCss::PropColor, LC_TEXT); LColour Back = StyleColour(LCss::PropBackgroundColor, LColour(L_MED)); if (!Back.IsTransparent()) { pDC->Colour(Back); pDC->Rectangle(); } int y = d->GetMin().y; LRect b(0, y/2, X()-1, Y()-1); LWideBorder(pDC, b, EdgeXpChisel); LPoint TxtPt(6, 0); LRect TxtRc = d->GetBounds(); TxtRc.Offset(TxtPt.x, TxtPt.y); d->Paint(pDC, TxtPt, Back, TxtRc, Enabled(), false); } } LRadioButton *LRadioGroup::Append(int x, int y, const char *name) { LRadioButton *But = new LRadioButton(d->NextId++, x, y, -1, -1, name); if (But) { Children.Insert(But); } return But; } /////////////////////////////////////////////////////////////////////////////////////////// // Radio button class LRadioButtonPrivate : public LMutex, public LStringLayout { public: LRadioButton *Ctrl; bool Val; bool Over; LRect Btn; LColour BackCol; LRadioButtonPrivate(LRadioButton *c) : LMutex("LRadioButtonPrivate"), LStringLayout(LAppInst->GetFontCache()) { Btn.ZOff(-1,-1); Ctrl = c; Val = 0; Over = 0; AmpersandToUnderline = true; } ~LRadioButtonPrivate() { } LRect GetBtn() { auto CtrlHt = Ctrl->Y(); auto Fnt = Ctrl->GetFont(); int Px = (int) (Fnt->Ascent() + 0.5); if (Px > CtrlHt) Px = CtrlHt; Btn.ZOff(Px-1, Px-1); Btn.Offset(0, (CtrlHt-Btn.Y())>>1); return Btn; } bool PreLayout(int32 &Min, int32 &Max) { if (Lock(_FL)) { DoPreLayout(Min, Max); Unlock(); } else return false; return true; } bool Layout(int Px) { if (Lock(_FL)) { DoLayout(Px); Unlock(); /* if (Min.y < MinYSize) Min.y = MinYSize; if (Max.y < MinYSize) Max.y = MinYSize; */ } else return false; return true; } }; static int PadXPx = 24; // 13px for circle, 11px padding to text. #ifdef MAC static int PadYPx = 6; #else static int PadYPx = 4; #endif LRadioButton::LRadioButton(int id, int x, int y, int cx, int cy, const char *name) : ResObject(Res_RadioBox) { d = new LRadioButtonPrivate(this); Name(name); if (cx < 0) cx = d->GetBounds().X() + PadXPx; if (cy < 0) cy = d->GetBounds().Y() + PadYPx; LRect r(x, y, x+cx, y+cy); SetPos(r); SetId(id); d->Val = false; d->Over = false; SetTabStop(true); #if WINNATIVE SetDlgCode(GetDlgCode() | DLGC_WANTARROWS); #endif } LRadioButton::~LRadioButton() { DeleteObj(d); } +bool LRadioButton::SetGroup(LArray CtrlIds) +{ + auto w = GetWindow(); + if (!w) + return false; + + // This ctrl should be in the ID list. + auto id = GetId(); + if (!CtrlIds.HasItem(id)) + CtrlIds.Add(id); + + for (auto i: CtrlIds) + { + LRadioButton *button; + if (!w->GetViewById(i, button)) + return false; + + button->d->GroupIDs = CtrlIds; + } + + return true; +} + void LRadioButton::OnAttach() { LResources::StyleElement(this); OnStyleChange(); LView::OnAttach(); } void LRadioButton::OnStyleChange() { if (d->Lock(_FL)) { d->Empty(); d->Add(LView::Name(), GetCss()); d->DoLayout(X()); d->Unlock(); Invalidate(); } } bool LRadioButton::Name(const char *n) { bool Status = false; if (d->Lock(_FL)) { Status = LView::Name(n); d->Empty(); d->Add(n, GetCss()); d->SetBaseFont(GetFont()); d->DoLayout(X()); d->Unlock(); } return Status; } bool LRadioButton::NameW(const char16 *n) { bool Status = false; if (d->Lock(_FL)) { Status = LView::NameW(n); d->Empty(); d->Add(LBase::Name(), GetCss()); d->SetBaseFont(GetFont()); d->DoLayout(X()); d->Unlock(); } return Status; } void LRadioButton::SetFont(LFont *Fnt, bool OwnIt) { LAssert(Fnt && Fnt->Handle()); if (d->Lock(_FL)) { LView::SetFont(Fnt, OwnIt); d->Unlock(); } d->Layout(X()); Invalidate(); } bool LRadioButton::OnLayout(LViewLayoutInfo &Inf) { auto Btn = d->GetBtn(); int Pad = Btn.X() + 4; if (!Inf.Width.Max) { d->PreLayout(Inf.Width.Min, Inf.Width.Max); // FIXME: Wrapping labels not supported yet. So use the max width. Inf.Width.Min = Inf.Width.Max; Inf.Width.Min += Pad; Inf.Width.Max += Pad; } else { d->Layout(Inf.Width.Max); Inf.Height.Min = Inf.Height.Max = MAX(d->GetMin().y, Btn.Y()); } return true; } int LRadioButton::OnNotify(LViewI *Ctrl, LNotification n) { if (Ctrl == (LViewI*)this && n.Type == LNotifyActivate) { Value(true); } return 0; } int64 LRadioButton::Value() { return d->Val; } void LRadioButton::Value(int64 i) { if (d->Val != (i != 0)) { if (i) { - // remove the value from the currenly selected radio value - if (auto p = GetParent()) + // remove the value from the currently selected radio value + if (d->GroupIDs.Length()) { - for (auto c: p->IterateViews()) + if (auto w = GetWindow()) { - LRadioButton *b = dynamic_cast(c); - if (b && b != this && b->d->Val) + for (auto id: d->GroupIDs) { - b->d->Val = false; - b->Invalidate(); + if (id == GetId()) + continue; + + LRadioButton *button; + if (w->GetViewById(id, button)) + { + if (button->Value()) + button->Value(false); + } + } + } + } + else + { + // Use the automatic mode... iterate through sibling views. + if (auto p = GetParent()) + { + for (auto c: p->IterateViews()) + { + LRadioButton *b = dynamic_cast(c); + if (b && b != this && b->d->Val) + { + b->d->Val = false; + b->Invalidate(); + } } } } } d->Val = i != 0; Invalidate(); if (i) SendNotify(); } } void LRadioButton::OnMouseClick(LMouse &m) { if (Enabled()) { bool WasCapturing = IsCapturing(); if (m.Down()) Focus(true); Capture(m.Down()); d->Over = m.Down(); LRect r(0, 0, X()-1, Y()-1); if (!m.Down() && r.Overlap(m.x, m.y) && WasCapturing) { Value(true); } else { Invalidate(); } } } void LRadioButton::OnMouseEnter(LMouse &m) { if (Enabled() && IsCapturing()) { d->Over = true; Invalidate(); } } void LRadioButton::OnMouseExit(LMouse &m) { if (Enabled() && IsCapturing()) { d->Over = false; Invalidate(); } } bool LRadioButton::OnKey(LKey &k) { bool Status = false; int Move = 0; switch (k.vkey) { case LK_UP: case LK_LEFT: { if (k.Down()) { Move = -1; } Status = true; break; } case LK_RIGHT: case LK_DOWN: { if (k.Down()) { Move = 1; } Status = true; break; } case LK_SPACE: { if (k.Down()) { Value(1); } return true; } } if (Move) { List Btns; for (LViewI *c: GetParent()->IterateViews()) { LRadioButton *b = dynamic_cast(c); if (b) Btns.Insert(b); } if (Btns.Length() > 1) { ssize_t Index = Btns.IndexOf(this); if (Index >= 0) { LRadioButton *n = Btns[(Index + Move + Btns.Length()) % Btns.Length()]; if (n) { n->Focus(true); } } } } return Status; } void LRadioButton::OnFocus(bool f) { Invalidate(); } void LRadioButton::OnPaint(LSurface *pDC) { if (LApp::SkinEngine && TestFlag(LApp::SkinEngine->GetFeatures(), GSKIN_RADIO)) { LSkinState State; State.pScreen = pDC; State.MouseOver = d->Over; State.aText = d->GetStrs(); State.View = this; State.Rect = d->GetBtn(); LColour Back = StyleColour(LCss::PropBackgroundColor, LColour(L_MED)); State.ForceUpdate = d->BackCol != Back; d->BackCol = Back; LApp::SkinEngine->OnPaint_LRadioButton(this, &State); } else { LRect r(0, 0, X()-1, Y()-1); LRect c(0, 0, 12, 12); // LColour Fore = StyleColour(LCss::PropColor, LC_TEXT, 4); LColour Back = StyleColour(LCss::PropBackgroundColor, LColour(L_MED)); // bool e = Enabled(); LRect fill(c.x2 + 1, r.y1, r.x2, r.x2); LPoint TxtPt(c.x2 + 11, (r.Y() - d->GetBounds().Y()) >> 1); d->Paint(pDC, TxtPt, Back, fill, Enabled(), false); #if defined LGI_CARBON LRect cli = GetClient(); for (LViewI *v = this; v && !v->Handle(); v = v->GetParent()) { LRect p = v->GetPos(); cli.Offset(p.x1, p.y1); } pDC->Colour(Back); pDC->Rectangle(cli.x1, cli.y1, c.x2, cli.y2); LRect rc(c.x1, c.y1 + 4, c.x2 - 1, c.y2 - 1); HIRect Bounds = rc; HIThemeButtonDrawInfo Info; HIRect LabelRect; Info.version = 0; Info.state = d->Val ? kThemeStatePressed : (Enabled() ? kThemeStateActive : kThemeStateInactive); Info.kind = kThemeRadioButton; Info.value = d->Val ? kThemeButtonOn : kThemeButtonOff; Info.adornment = Focus() ? kThemeAdornmentFocus : kThemeAdornmentNone; OSStatus err = HIThemeDrawButton(&Bounds, &Info, pDC->Handle(), kHIThemeOrientationNormal, &LabelRect); if (err) printf("%s:%i - HIThemeDrawButton failed %li\n", _FL, err); #else // Draw border pDC->Colour(L_LOW); pDC->Line(c.x1+1, c.y1+9, c.x1+1, c.y1+10); pDC->Line(c.x1, c.y1+4, c.x1, c.y1+8); pDC->Line(c.x1+1, c.y1+2, c.x1+1, c.y1+3); pDC->Line(c.x1+2, c.y1+1, c.x1+3, c.y1+1); pDC->Line(c.x1+4, c.y1, c.x1+8, c.y1); pDC->Line(c.x1+9, c.y1+1, c.x1+10, c.y1+1); pDC->Colour(L_SHADOW); pDC->Set(c.x1+2, c.y1+9); pDC->Line(c.x1+1, c.y1+4, c.x1+1, c.y1+8); pDC->Line(c.x1+2, c.y1+2, c.x1+2, c.y1+3); pDC->Set(c.x1+3, c.y1+2); pDC->Line(c.x1+4, c.y1+1, c.x1+8, c.y1+1); pDC->Set(c.x1+9, c.y1+2); pDC->Colour(L_LIGHT); pDC->Line(c.x1+11, c.y1+2, c.x1+11, c.y1+3); pDC->Line(c.x1+12, c.y1+4, c.x1+12, c.y1+8); pDC->Line(c.x1+11, c.y1+9, c.x1+11, c.y1+10); pDC->Line(c.x1+9, c.y1+11, c.x1+10, c.y1+11); pDC->Line(c.x1+4, c.y1+12, c.x1+8, c.y1+12); pDC->Line(c.x1+2, c.y1+11, c.x1+3, c.y1+11); /// Draw center bool e = Enabled(); pDC->Colour(d->Over || !e ? L_MED : L_WORKSPACE); pDC->Rectangle(c.x1+2, c.y1+4, c.x1+10, c.y1+8); pDC->Box(c.x1+3, c.y1+3, c.x1+9, c.y1+9); pDC->Box(c.x1+4, c.y1+2, c.x1+8, c.y1+10); // Draw value if (d->Val) { pDC->Colour(e ? L_TEXT : L_LOW); pDC->Rectangle(c.x1+4, c.y1+5, c.x1+8, c.y1+7); pDC->Rectangle(c.x1+5, c.y1+4, c.x1+7, c.y1+8); } #endif } } #endif diff --git a/src/win/Widgets/RadioGroup_Win.cpp b/src/win/Widgets/RadioGroup_Win.cpp --- a/src/win/Widgets/RadioGroup_Win.cpp +++ b/src/win/Widgets/RadioGroup_Win.cpp @@ -1,519 +1,555 @@ // \file // \author Matthew Allen // \brief Native Win32 Radio Group and Button #if !XP_BUTTON #include #include #include "lgi/common/Lgi.h" #include "lgi/common/RadioGroup.h" #include "lgi/common/DisplayString.h" #include "lgi/common/Notifications.h" #include "lgi/common/Css.h" #include "lgi/common/LgiRes.h" #define RADIO_GRID 2 static int PadXPx = 30; #ifdef MAC static int PadYPx = 6; #else static int PadYPx = 4; #endif /////////////////////////////////////////////////////////////////////////////////////////// // Radio group class LRadioGroupPrivate { public: static int NextId; int64 InitVal; int MaxLayoutWidth; LHashTbl,LViewLayoutInfo*> Info; LRadioGroupPrivate() { InitVal = 0; MaxLayoutWidth = 0; } ~LRadioGroupPrivate() { Info.DeleteObjects(); } }; int LRadioGroupPrivate::NextId = 10000; LRadioGroup::LRadioGroup(int id, int x, int y, int cx, int cy, const char *name, int Init) : ResObject(Res_Group) { d = new LRadioGroupPrivate; d->InitVal = Init; Name(name); LRect r(x, y, x+cx, y+cy); SetPos(r); SetId(id); SetTabStop(true); SetStyle(GetStyle() | BS_GROUPBOX | WS_GROUP | WS_CLIPCHILDREN); SetClassW32(GetClass()); if (!SubClass) SubClass = LWindowsClass::Create(GetClass()); if (SubClass) SubClass->SubClass("BUTTON"); else LAssert(!"No subclass?"); } LRadioGroup::~LRadioGroup() { DeleteObj(d); } void LRadioGroup::OnAttach() { } LMessage::Result LRadioGroup::OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case WM_ERASEBKGND: { LScreenDC Dc((HDC)Msg->A(), _View); LColour cBack = StyleColour(LCss::PropBackgroundColor, LColour(L_MED)); Dc.Colour(cBack); Dc.Rectangle(); return true; break; } case WM_DESTROY: { d->InitVal = Value(); break; } } return LControl::OnEvent(Msg); } bool LRadioGroup::Name(const char *n) { return LView::Name(n); } bool LRadioGroup::NameW(const char16 *n) { return LView::NameW(n); } void LRadioGroup::SetFont(LFont *Fnt, bool OwnIt) { LView::SetFont(Fnt); } void LRadioGroup::OnCreate() { SetFont(LSysFont); AttachChildren(); Value(d->InitVal); } int64 LRadioGroup::Value() { if (Handle()) { int n = 0; for (auto c: Children) { LRadioButton *Check = dynamic_cast(c); if (Check) { if (Check->Value()) return n; n++; } } return -1; } else { return d->InitVal; } } void LRadioGroup::Value(int64 Which) { if (Handle()) { int n = 0; for (auto c: Children) { LRadioButton *Check = dynamic_cast(c); if (Check) { Check->Value(n == Which); n++; } } } else { d->InitVal = Which; } } int LRadioGroup::OnNotify(LViewI *Ctrl, LNotification n) { LViewI *v = GetNotify() ? GetNotify() : GetParent(); if (v) { if (dynamic_cast(Ctrl)) { return v->OnNotify(this, n); } else { return v->OnNotify(Ctrl, n); } } return 0; } void LRadioGroup::OnPaint(LSurface *pDC) { } LRadioButton *LRadioGroup::Append(int x, int y, const char *name) { LRadioButton *But = new LRadioButton(d->NextId++, x, y, -1, -1, name); if (But) { Children.Insert(But); } return But; } bool LRadioGroup::OnLayout(LViewLayoutInfo &Inf) { auto it = IterateViews(); const int BORDER_PX = 2; LDisplayString Txt(GetFont(), Name()); if (!Inf.Width.Max) { // Work out the width... int TextPx = Txt.X(); int MinPx = (RADIO_GRID + BORDER_PX) * 2; d->Info.DeleteObjects(); Inf.Width.Min = 16 + TextPx; Inf.Width.Max = RADIO_GRID + BORDER_PX * 2; for (LViewI *w: it) { LAutoPtr c(new LViewLayoutInfo); if (w->OnLayout(*c)) { // Layout enabled control Inf.Width.Min = max(Inf.Width.Min, c->Width.Min + MinPx); Inf.Width.Max += c->Width.Max + RADIO_GRID; d->Info.Add(w, c.Release()); } else { // Non layout enabled control Inf.Width.Min = max(Inf.Width.Min, w->X() + (RADIO_GRID << 1)); Inf.Width.Max += w->X() + RADIO_GRID; } } if (Inf.Width.Max < Inf.Width.Min) Inf.Width.Max = Inf.Width.Min; d->MaxLayoutWidth = Inf.Width.Max; } else { // Working out the height, and positioning the controls Inf.Height.Min = Txt.Y(); bool Horiz = d->MaxLayoutWidth <= Inf.Width.Max; int Cx = BORDER_PX + RADIO_GRID, Cy = Txt.Y(); int LastY = 0; for (LViewI *w: it) { LViewLayoutInfo *c = d->Info.Find(w); if (c) { if (w->OnLayout(*c)) { LRect r(Cx, Cy, Cx + c->Width.Max - 1, Cy + c->Height.Max - 1); w->SetPos(r); if (Horiz) // Horizontal layout Cx += r.X() + RADIO_GRID; else // Vertical layout Cy += r.Y() + RADIO_GRID; LastY = max(LastY, r.y2); } else LAssert(!"This shouldn't fail."); } else { // Non layout control... just use existing size LRect r = w->GetPos(); r.Offset(Cx - r.x1, Cy - r.y1); w->SetPos(r); if (Horiz) // Horizontal layout Cx += r.X() + RADIO_GRID; else // Vertical layout Cy += r.Y() + RADIO_GRID; LastY = max(LastY, r.y2); } } Inf.Height.Min = Inf.Height.Max = LastY + RADIO_GRID * 2 + BORDER_PX; } return true; } /////////////////////////////////////////////////////////////////////////////////////////// // Radio button class LRadioButtonPrivate { public: - DWORD ParentProc; - int64 InitVal; - - LRadioButtonPrivate() - { - ParentProc = 0; - InitVal = 0; - } - - ~LRadioButtonPrivate() - { - } + DWORD ParentProc = 0; + int64 InitVal = 0; + LArray GroupIDs; + bool InValue = false; }; LRadioButton::LRadioButton(int id, int x, int y, int cx, int cy, const char *name) : ResObject(Res_RadioBox) { d = new LRadioButtonPrivate; Name(name); LDisplayString t(LSysFont, name); if (cx < 0) cx = t.X() + 26; if (cy < 0) cy = t.Y() + 4; LRect r(x, y, x+cx, y+cy); SetPos(r); SetId(id); SetTabStop(true); SetStyle(GetStyle() | BS_AUTORADIOBUTTON); SetClassW32(GetClass()); if (!SubClass) SubClass = LWindowsClass::Create(GetClass()); if (SubClass) SubClass->SubClass("BUTTON"); else LAssert(!"No subclass?"); } LRadioButton::~LRadioButton() { DeleteObj(d); } +bool LRadioButton::SetGroup(LArray CtrlIds) +{ + auto w = GetWindow(); + if (!w) + return false; + + // This ctrl should be in the ID list. + auto id = GetId(); + if (!CtrlIds.HasItem(id)) + CtrlIds.Add(id); + + for (auto i: CtrlIds) + { + LRadioButton *button; + if (!w->GetViewById(i, button)) + return false; + + button->d->GroupIDs = CtrlIds; + } + + return true; +} + void LRadioButton::OnAttach() { SetFont(LSysFont); LResources::StyleElement(this); OnStyleChange(); LView::OnAttach(); Value(d->InitVal); } void LRadioButton::OnStyleChange() { } LMessage::Result LRadioButton::OnEvent(LMessage *m) { switch (m->Msg()) { case WM_DESTROY: { // Save the value for anyone needing it. d->InitVal = Value(); break; } } return LControl::OnEvent(m); } int LRadioButton::SysOnNotify(int Msg, int Code) { if (Msg == WM_COMMAND && Code == BN_CLICKED && GetParent()) { Value(true); } return 0; } bool LRadioButton::Name(const char *n) { return LView::Name(n); } bool LRadioButton::NameW(const char16 *n) { return LView::NameW(n); } int64 LRadioButton::Value() { if (Handle()) return SendMessage(Handle(), BM_GETCHECK, 0, 0); return d->InitVal; } void LRadioButton::Value(int64 i) { - static bool InValue = false; - if (!InValue) + if (!d->InValue) { - InValue = true; + d->InValue = true; if (Handle()) { if (i) { // Turn off any other radio buttons in the current group - for (LViewI *c: GetParent()->IterateViews()) + if (d->GroupIDs.Length()) { - LRadioButton *b = dynamic_cast(c); - if (b && - b != this && - b-Value()) + if (auto w = GetWindow()) { - b->Value(false); + for (auto id: d->GroupIDs) + { + if (id == GetId()) + continue; + + LRadioButton *button; + if (w->GetViewById(id, button)) + { + if (button->Value()) + button->Value(false); + } + } + } + } + else + { + for (LViewI *c: GetParent()->IterateViews()) + { + LRadioButton *b = dynamic_cast(c); + if (b && + b != this && + b-Value()) + { + b->Value(false); + } } } } // Change the value of this button SendMessage(Handle(), BM_SETCHECK, i, 0); if (i) { // If gaining selection, tell the parent SendNotify(LNotifyValueChanged); } } else { d->InitVal = i; } - InValue = false; + + d->InValue = false; } } bool LRadioButton::OnLayout(LViewLayoutInfo &Inf) { LDisplayString Txt(GetFont(), Name()); if (!Inf.Width.Max) { Inf.Width.Min = Inf.Width.Max = Txt.X() + PadXPx; } else { int y = Txt.Y() + PadYPx; Inf.Height.Min = max(Inf.Height.Min, y); Inf.Height.Max = max(Inf.Height.Max, y); } return true; } int LRadioButton::OnNotify(LViewI *Ctrl, LNotification n) { if (Ctrl == (LViewI*)this && n.Type == LNotifyActivate) { Value(true); } return 0; } bool LRadioButton::OnKey(LKey &k) { bool Status = false; int Move = 0; switch (k.vkey) { case VK_UP: case VK_LEFT: { if (k.Down()) { Move = -1; } Status = true; break; } case VK_RIGHT: case VK_DOWN: { if (k.Down()) { Move = 1; } Status = true; break; } case ' ': { if (k.Down()) { Value(1); } return true; } } if (Move) { List Btns; auto L = GetParent()->IterateViews(); for (LViewI *c: L) { LRadioButton *b = dynamic_cast(c); if (b) Btns.Insert(b); } if (Btns.Length() > 1) { auto Index = Btns.IndexOf(this); if (Index >= 0) { LRadioButton *n = Btns[(Index + Move + Btns.Length()) % Btns.Length()]; if (n) { n->Focus(true); } } } } return Status; } #endif \ No newline at end of file diff --git a/win/Lgi_vs2019.vcxproj b/win/Lgi_vs2019.vcxproj --- a/win/Lgi_vs2019.vcxproj +++ b/win/Lgi_vs2019.vcxproj @@ -1,630 +1,626 @@  Debug x64 ReleaseNoOptimize x64 Release x64 Lgi {95DF9CA4-6D37-4A85-A648-80C2712E0DA1} 10.0 DynamicLibrary v142 false Unicode DynamicLibrary v142 false Unicode DynamicLibrary v142 false Unicode <_ProjectFileVersion>12.0.30501.0 ..\Lib\ $(Platform)$(Configuration)19\ false $(VC_IncludePath);$(WindowsSDK_IncludePath);..\..\..\CodeLib\libiconv-1.14\include Lgi19x64 ..\Lib\ $(Platform)$(Configuration)19\ true $(VC_IncludePath);$(WindowsSDK_IncludePath);..\..\..\CodeLib\libiconv-1.14\include Lgi19x64d ..\Lib\ $(Platform)$(Configuration)19\ false $(VC_IncludePath);$(WindowsSDK_IncludePath);..\..\..\CodeLib\libiconv-1.14\include Lgi19x64nop NDEBUG;%(PreprocessorDefinitions) true true X64 .\Release/Lgi.tlb MinSpace OnlyExplicitInline ..\include;..\include\lgi\win;..\private\common;..\private\win;..\..\..\..\CodeLib\libiconv\include;..\..\..\scribe\libs\build-x64\libiconv-1.17\include;%(AdditionalIncludeDirectories) WIN64;NDEBUG;WINDOWS;LGI_RES;LGI_LIBRARY;%(PreprocessorDefinitions) true MultiThreadedDLL true true $(IntDir)$(TargetName).pch $(IntDir) $(IntDir) $(IntDir) Level2 true ProgramDatabase Default true NDEBUG;%(PreprocessorDefinitions) 0x0c09 ComCtl32.lib;Ws2_32.lib;UxTheme.lib;imm32.lib;%(AdditionalDependencies) $(OutDir)$(TargetFileName) true true $(OutDir)$(TargetName).pdb Windows true false $(OutDir)$(TargetName).lib MachineX64 _DEBUG;%(PreprocessorDefinitions) true true X64 .\Debug/Lgi.tlb Disabled ..\include;..\include\lgi\win;..\private\common;..\private\win;..\..\..\..\CodeLib\libiconv\include;..\..\..\scribe\libs\build-x64\libiconv-1.17\include;%(AdditionalIncludeDirectories) WIN64;LGI_LIBRARY;_DEBUG;WINDOWS;LGI_RES;%(PreprocessorDefinitions) EnableFastChecks MultiThreadedDebugDLL true $(IntDir)$(TargetName).pch $(IntDir) $(IntDir) $(IntDir) Level3 true ProgramDatabase Default false true _DEBUG;%(PreprocessorDefinitions) 0x0c09 ComCtl32.lib;Ws2_32.lib;UxTheme.lib;imm32.lib;%(AdditionalDependencies) NotSet $(OutDir)$(TargetFileName) true true $(OutDir)$(TargetName).pdb Windows false $(OutDir)$(TargetName).lib MachineX64 NDEBUG;%(PreprocessorDefinitions) true true X64 .\Release/Lgi.tlb MinSpace OnlyExplicitInline ..\include;..\include\lgi\win;..\private\common;..\private\win;..\..\..\..\CodeLib\libiconv\include;..\..\..\scribe\libs\build-x64\libiconv-1.17\include;%(AdditionalIncludeDirectories) WIN64;NDEBUG;WINDOWS;LGI_RES;LGI_LIBRARY;%(PreprocessorDefinitions) true MultiThreadedDLL true true $(IntDir)$(TargetName).pch $(IntDir) $(IntDir) $(IntDir) Level2 true ProgramDatabase Default false true NDEBUG;%(PreprocessorDefinitions) 0x0c09 ComCtl32.lib;Ws2_32.lib;UxTheme.lib;imm32.lib;%(AdditionalDependencies) $(OutDir)$(TargetFileName) true true $(OutDir)$(TargetName).pdb Windows true false $(OutDir)$(TargetName).lib MachineX64 false true true false false false true true true true true true false false false true true true true true true - - - - \ No newline at end of file diff --git a/win/Lgi_vs2019.vcxproj.filters b/win/Lgi_vs2019.vcxproj.filters --- a/win/Lgi_vs2019.vcxproj.filters +++ b/win/Lgi_vs2019.vcxproj.filters @@ -1,1236 +1,1232 @@  {afe8cb77-9ad1-4536-bbdd-3c127e7ed08c} cpp;c;cxx;rc;def;r;odl;idl;hpj;bat {66a64573-871b-4499-ae26-c19e9e2a514a} {3fc23ef0-f144-4f1f-a9b4-18d3392bb63d} {c6cd6d73-d33c-4413-ade1-9dad78e2dc9c} {c8684fc7-2e3c-4f15-8284-9d44b044f6c6} {87b1c801-b9ce-4f6c-97ab-a8f89aee9594} {c06c25f2-2c07-4900-a517-4a6a324069e9} {2c01a737-36cf-4197-bfa1-20395060263f} {1e4cd802-8b94-4328-930e-37bfbfbedff5} {01075698-dde2-4ed0-808f-7dd54414b597} {4a6845a8-e6ec-47d5-8f6c-aa0cfdbc68df} {f567a76b-edd5-434d-b0d9-d51481f34845} {3dee0237-b01c-44e8-a730-08dc661df82f} {bbaaace6-0ac7-4e76-90ae-9e5d5a0cb899} {71e7b970-a070-40a7-a99f-88c215e14e44} {6e115ea1-09fb-492b-82b6-428afe17fed9} {719ef36f-419f-46f9-aef9-2f8158e4d106} {fb221556-3700-4dd8-ba9a-10b5d66bfe54} {8e9f0321-56ae-4485-a338-e87d714c7f50} {c6050f41-574b-4a92-9ce5-860be2719ecf} {a07cd969-801e-4ec9-9394-e34912a3271d} {e067fae0-ef98-4e7a-8434-6f36590ba0e6} {0a3e2151-b13a-407a-8cd9-ccb20f15cacf} {c72248a4-c2f0-43b9-950d-89d09bfddbb3} {dad60202-c763-4f32-9dfb-fe7def4637ee} {532dfa4a-58d3-4133-9ed6-a9fbf7f1556e} {5409aca4-2a55-4b2f-a719-d3db4e3cd7e4} {e3a3aadd-47ef-4723-9bcc-7db1f75a18b0} {c4327acf-78c3-4ef1-b8bc-3aac9ea52b41} {b3c906b8-595e-4641-8eec-8ad03ab13f6f} {baf0a65b-4a9c-4b11-850d-a957c19a22bf} {6e349f5b-36a8-4821-9574-4040a3784204} {ddfdebae-8bcf-4224-8938-2716aba03822} {a126c55a-edee-489f-a852-25cbd1b433b2} {ab5fd4a0-3862-42fd-b4ff-d5d8b0443f53} {048d5e0a-220f-4911-999d-96965eb53691} {258aef64-8cd0-4838-8131-147196656a99} {814a5d81-3fd5-461b-a4a3-cda593ea404b} {5fe450b8-5fa9-440e-9fb0-03860b3548d0} h;hpp;hxx;hm;inl - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files + + Source Files\Core - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files + + Source Files\Core - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files + + Source Files\Dialogs - Source Files + Source Files\General + + + Source Files\Widgets + + + Source Files\Widgets - - Source Files + + Source Files\Widgets + + + Source Files\Widgets\Native Windows - - Source Files + + Source Files\Widgets\Native Windows + + + Source Files\Widgets - - Source Files + + Source Files\Graphics - - Source Files + + Source Files\Widgets + + + Source Files\Text - - Source Files + + Source Files\General\Clipboard + + + Source Files\Graphics - - Source Files + + Source Files\Widgets\Native Windows + + + Source Files\Core\Memory Subsystem - Source Files + Source Files\Widgets\Css - Source Files + Source Files\Widgets\Css + + + Source Files\Core\DateTime + + + Source Files\Widgets\Native Windows + + + Source Files\Graphics\Font + + + Source Files\Core\Drag and Drop - Source Files + Source Files\Core\Drag and Drop + + + Source Files\Core - Source Files + Source Files\Core\Drag and Drop + + + Source Files\Widgets + + + Source Files\Widgets\Native Windows + + + Source Files\General + + + Source Files\Core\File + + + Source Files\Core\File + + + Source Files\Dialogs + + + Source Files\Graphics\Filters - Source Files + Source Files\Dialogs + + + Source Files\Graphics\Font - Source Files + Source Files\Graphics\Font + + + Source Files\Graphics\Font + + + Source Files\Graphics\Font + + + Source Files\Graphics + + + Source Files\Graphics + + + Source Files\Graphics\Gdi Leak + + + Source Files\Core\Skin + + + Source Files\General + + + Source Files\General\Growl - Source Files + Source Files\Graphics - Source Files + Source Files\Dialogs + + + Source Files\Core + + + Source Files\Core + + + Source Files\General + + + Source Files\General + + + Source Files\Core\Resources - Source Files + Source Files\Core\Libraries + + + Source Files\Widgets + + + Source Files\Dialogs + + + Source Files\General\Hash + + + Source Files\General\Hash + + + Source Files\Core\Memory Subsystem + + + Source Files\Graphics\Surfaces + + + Source Files\Graphics\Applicators + + + Source Files\Graphics\Applicators + + + Source Files\Graphics\Applicators + + + Source Files\Graphics\Applicators + + + Source Files\Graphics\Applicators + + + Source Files\Graphics\Applicators + + + Source Files\Graphics\Applicators + + + Source Files\Graphics\Applicators - Source Files + Source Files\Core\Memory Subsystem + + + Source Files\Core\Menus - Source Files + Source Files\Core\Menus + + + Source Files\Network - Source Files + Source Files\Core + + + Source Files\Core\Threads + + + Source Files\Network + + + Source Files\Network - Source Files + Source Files\General - Source Files + Source Files\General + + + Source Files\Widgets + + + Source Files\General + + + Source Files\Graphics + + + Source Files\Widgets + + + Source Files\Graphics\Surfaces + + + Source Files\Core + + + Source Files\Widgets\Native Windows + + + Source Files\Widgets + + + Source Files\Widgets + + + Source Files\Widgets\Native Windows + + + Source Files\General + + + Source Files\Graphics + + + Source Files\Core\Resources + + + Source Files\Graphics\Surfaces + + + Source Files\Widgets + + + Source Files\General\Hash + + + Source Files\Widgets\Native Windows + + + Source Files\Widgets + + + Source Files\Widgets\Native Windows + + + Source Files\Widgets + + + Source Files\Widgets - Source Files + Source Files\Core\Memory Subsystem + + + Source Files\Text + + + Source Files\Graphics\Font - Source Files + Source Files\Core\Process - - Source Files + + Source Files\Graphics\Surfaces - - Source Files + + Source Files\Widgets - - Source Files - - - Source Files + + Source Files\Widgets - - Source Files + + Source Files\Widgets - - Source Files + + Source Files\Widgets - - Source Files + + Source Files\Widgets - - Source Files - - - Source Files + + Source Files\Core\Threads - Source Files + Source Files\Core\Threads - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files + Source Files\Core\Threads - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files + Source Files\Text - Source Files + Source Files\Widgets + + + Source Files\Widgets + + + Source Files\Widgets - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files + Source Files\Widgets - - Source Files - - - Source Files + + Source Files\Graphics\Font - - Source Files - - - Source Files - - - Source Files + + Source Files\Text - - Source Files - - - Source Files + + Source Files\Text - - Source Files - - - Source Files - - - Source Files + + Source Files\Network - - Source Files - - - Source Files + + Source Files\Text - - Source Files - - - Source Files - - - Source Files + + Source Files\General - Source Files + Source Files\Core + + + Source Files\Core - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files + Source Files\Core - - Source Files - - - Source Files - - - Source Files + + Source Files\Core - - Source Files - - - Source Files - - - Source Files + + Source Files\Text Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files Header Files - - - - \ No newline at end of file