diff --git a/Lgi_vs2013.vcxproj b/Lgi_vs2013.vcxproj --- a/Lgi_vs2013.vcxproj +++ b/Lgi_vs2013.vcxproj @@ -1,659 +1,659 @@  Debug Win32 Debug x64 ReleaseNoOptimize Win32 ReleaseNoOptimize x64 Release Win32 Release x64 Lgi {95DF9CA4-6D37-4A85-A648-80C2712E0DA1} DynamicLibrary v120 false Unicode DynamicLibrary v120 false Unicode DynamicLibrary v120 false Unicode DynamicLibrary v120 false Unicode DynamicLibrary v120 false Unicode DynamicLibrary v120 false Unicode <_ProjectFileVersion>12.0.30501.0 .\Lib\ $(Platform)$(Configuration)12\ false $(VC_IncludePath);$(WindowsSDK_IncludePath);..\..\..\CodeLib\libiconv-1.14\include - Lgi12x32 + Lgi12x32_s22 .\Lib\ $(Platform)$(Configuration)12\ false $(VC_IncludePath);$(WindowsSDK_IncludePath);..\..\..\CodeLib\libiconv-1.14\include - Lgi12x64 + Lgi12x64_s22 .\Lib\ $(Platform)$(Configuration)12\ true $(VC_IncludePath);$(WindowsSDK_IncludePath);..\..\..\CodeLib\libiconv-1.14\include - Lgi12x32d + Lgi12x32d_s22 .\Lib\ $(Platform)$(Configuration)12\ true $(VC_IncludePath);$(WindowsSDK_IncludePath);..\..\..\CodeLib\libiconv-1.14\include - Lgi12x64d + Lgi12x64d_s22 .\Lib\ $(Platform)$(Configuration)12\ false $(VC_IncludePath);$(WindowsSDK_IncludePath);..\..\..\CodeLib\libiconv-1.14\include - Lgi12x32nop + Lgi12x32nop_s22 .\Lib\ $(Platform)$(Configuration)12\ false $(VC_IncludePath);$(WindowsSDK_IncludePath);..\..\..\CodeLib\libiconv-1.14\include - Lgi12x64nop + Lgi12x64nop_s22 NDEBUG;%(PreprocessorDefinitions) true true Win32 .\Release/Lgi.tlb MinSpace OnlyExplicitInline include\common;include\win32;..\..\..\CodeLib\libiconv\include;%(AdditionalIncludeDirectories) NDEBUG;WIN32;WINDOWS;LGI_RES;LGI_LIBRARY;%(PreprocessorDefinitions) true MultiThreadedDLL true true $(IntDir)$(TargetName).pch $(IntDir) $(IntDir) $(IntDir) Level2 true ProgramDatabase Default NDEBUG;%(PreprocessorDefinitions) 0x0c09 /MACHINE:I386 %(AdditionalOptions) ComCtl32.lib;Ws2_32.lib;UxTheme.lib;imm32.lib;%(AdditionalDependencies) $(OutDir)$(TargetFileName) true true $(OutDir)$(TargetName).pdb Windows true false $(OutDir)$(TargetName).lib NDEBUG;%(PreprocessorDefinitions) true true X64 .\Release/Lgi.tlb MinSpace OnlyExplicitInline include\common;include\win32;..\..\..\CodeLib\libiconv\include;%(AdditionalIncludeDirectories) WIN64;NDEBUG;WINDOWS;LGI_RES;LGI_LIBRARY;%(PreprocessorDefinitions) true MultiThreadedDLL true true $(IntDir)$(TargetName).pch $(IntDir) $(IntDir) $(IntDir) Level2 true ProgramDatabase Default 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 Win32 .\Debug/Lgi.tlb Disabled include\common;include\win32;..\..\..\CodeLib\libiconv\include;%(AdditionalIncludeDirectories) LGI_LIBRARY;_DEBUG;WIN32;WINDOWS;LGI_RES;%(PreprocessorDefinitions) EnableFastChecks MultiThreadedDebugDLL true $(IntDir)$(TargetName).pch $(IntDir) $(IntDir) $(IntDir) Level2 true ProgramDatabase Default _DEBUG;%(PreprocessorDefinitions) 0x0c09 /MACHINE:I386 %(AdditionalOptions) ComCtl32.lib;Ws2_32.lib;UxTheme.lib;imm32.lib;%(AdditionalDependencies) $(OutDir)$(TargetFileName) true true $(OutDir)$(TargetName).pdb Windows false $(OutDir)$(TargetName).lib _DEBUG;%(PreprocessorDefinitions) true true X64 .\Debug/Lgi.tlb Disabled include\common;include\win32;..\..\..\CodeLib\libiconv\include;%(AdditionalIncludeDirectories) WIN64;LGI_LIBRARY;_DEBUG;WINDOWS;LGI_RES;%(PreprocessorDefinitions) EnableFastChecks MultiThreadedDebugDLL true $(IntDir)$(TargetName).pch $(IntDir) $(IntDir) $(IntDir) Level2 true ProgramDatabase Default _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 Win32 .\Release/Lgi.tlb Disabled OnlyExplicitInline include\common;include\win32;..\..\..\CodeLib\libiconv\include;%(AdditionalIncludeDirectories) NDEBUG;WIN32;WINDOWS;LGI_RES;LGI_LIBRARY;%(PreprocessorDefinitions) true MultiThreadedDLL true true $(IntDir)$(TargetName).pch $(IntDir) $(IntDir) $(IntDir) Level2 true ProgramDatabase Default NDEBUG;%(PreprocessorDefinitions) 0x0c09 /MACHINE:I386 %(AdditionalOptions) ComCtl32.lib;Ws2_32.lib;UxTheme.lib;imm32.lib;%(AdditionalDependencies) $(OutDir)$(TargetFileName) true true $(OutDir)$(TargetName).pdb Windows true false $(OutDir)$(TargetName).lib NDEBUG;%(PreprocessorDefinitions) true true X64 .\Release/Lgi.tlb MinSpace OnlyExplicitInline include\common;include\win32;..\..\..\CodeLib\libiconv\include;%(AdditionalIncludeDirectories) WIN64;NDEBUG;WINDOWS;LGI_RES;LGI_LIBRARY;%(PreprocessorDefinitions) true MultiThreadedDLL true true $(IntDir)$(TargetName).pch $(IntDir) $(IntDir) $(IntDir) Level2 true ProgramDatabase Default 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 true true true true true true true true true true true true true true true true true true true true true true true true true true true true true true \ No newline at end of file diff --git a/include/common/GDocView.h b/include/common/GDocView.h --- a/include/common/GDocView.h +++ b/include/common/GDocView.h @@ -1,516 +1,527 @@ /// \file /// \author Matthew Allen (fret@memecode.com) /// \brief This is the base data and code for all the text controls (inc. HTML) #ifndef __GDOCVIEW_H #define __GDOCVIEW_H #include "GVariant.h" #include "GNotifications.h" // Word wrap /// No word wrapping #define TEXTED_WRAP_NONE 0 /// Dynamically wrap line to editor width #define TEXTED_WRAP_REFLOW 1 // Util macros /// Returns true if 'c' is whitespace #define IsWhiteSpace(c) ((c) < 126 && strchr(GDocView::WhiteSpace, c) != 0) /// Returns true if 'c' is a delimiter #define IsDelimiter(c) ((c) < 126 && strchr(GDocView::Delimiters, c) != 0) /// Returns true if 'c' is a letter or number #define IsText(c) (IsDigit(c) || IsAlpha(c) || (c) == '_') /// Returns true if 'c' is word boundry #define IsWordBoundry(c) (strchr(GDocView::WhiteSpace, c) || strchr(GDocView::Delimiters, c)) /// Returns true if 'c' is alphanumeric or a digit #define AlphaOrDigit(c) (IsDigit(c) || IsAlpha(c)) /// Returns true if 'c' is a valid URL character #define UrlChar(c) ( \ strchr(GDocView::UrlDelim, (c)) || \ AlphaOrDigit((c)) || \ ((c) >= 256) \ ) /// Returns true if 'c' is email address character #define EmailChar(c) (strchr("._-:+", (c)) || AlphaOrDigit((c))) extern char16 *ConvertToCrLf(char16 *Text); /// This class contains infomation about a link. /// \sa LgiDetectLinks struct GLinkInfo { NativeInt Start; NativeInt Len; bool Email; void Set(NativeInt start, NativeInt len, bool email) { Start = start; Len = len; Email = email; } }; // Call back class to handle viewer events class GDocView; /// An environment class to handle requests from the text view to the outside world. class LgiClass GDocumentEnv : public LThreadOwner { GArray Viewers; public: GDocumentEnv(GDocView *v = 0); virtual ~GDocumentEnv(); enum LoadType { LoadError, LoadNotImpl, LoadImmediate, LoadDeferred, }; struct #ifdef MAC LgiClass #endif LoadJob : public LThreadJob { enum PrefFormat { FmtNone, FmtStream, FmtSurface, FmtFilename, }; enum JobStatus { JobInit, JobOk, JobErr_Uri, JobErr_Path, JobErr_FileOpen, JobErr_GetUri, JobErr_NoCachedFile, JobErr_ImageFilter, JobErr_NoMem }; // View data GDocumentEnv *Env; void *UserData; uint32 UserUid; PrefFormat Pref; // Input data GAutoString Uri; GAutoString PostData; // Output data GAutoPtr Stream; GAutoPtr pDC; GString Filename; GString Error; JobStatus Status; LoadJob(LThreadTarget *o) : LThreadJob(o) { Env = NULL; UserUid = 0; UserData = NULL; Pref = FmtNone; Status = JobInit; } GStreamI *GetStream() { if (!Stream && Filename) { GFile *file = new GFile; if (file && file->Open(Filename, O_READ)) Stream.Reset(file); else DeleteObj(file); } return Stream; } }; LoadJob *NewJob() { return new LoadJob(this); } bool AttachView(GDocView *v) { if (!v) return false; if (!Lock(_FL)) return false; LgiAssert(!Viewers.HasItem(v)); Viewers.Add(v); Unlock(); return true; } bool DetachView(GDocView *v) { if (!v) return false; if (!Lock(_FL)) return false; LgiAssert(Viewers.HasItem(v)); Viewers.Delete(v); Unlock(); return true; } /// Creating a context menu, usually when the user right clicks on the /// document. virtual bool AppendItems(GSubMenu *Menu, int Base = 1000) { return false; } /// Do something when the menu items created by GDocumentEnv::AppendItems /// are clicked. virtual bool OnMenu(GDocView *View, int Id, void *Context) { return false; } /// Asks the env to get some data linked from the document, e.g. a css file or an iframe source etc. /// If the GetContent implementation takes ownership of the job pointer then it should set 'j' to NULL. virtual LoadType GetContent(LoadJob *&j) { return LoadNotImpl; } /// After the env's thread loads the resource it calls this to pass it to the doc void OnDone(GAutoPtr j); /// Handle a click on URI virtual bool OnNavigate(GDocView *Parent, const char *Uri) { return false; } /// Handle a form post virtual bool OnPostForm(GDocView *Parent, const char *Uri, const char *Data) { return false; } /// Process dynamic content, returning a dynamically allocated string /// for the result of the executed script. Dynamic content is enclosed /// between <? and ?>. virtual char *OnDynamicContent(GDocView *Parent, const char *Code) { return 0; } /// Some script was received, the owner should compile it virtual bool OnCompileScript(GDocView *Parent, char *Script, const char *Language, const char *MimeType) { return false; } /// Some script needs to be executed, the owner should compile it virtual bool OnExecuteScript(GDocView *Parent, char *Script) { return false; } }; /// Default text view environment /// /// This class defines the default behaviour of the environment, /// However you will need to instantiate this yourself and call /// SetEnv with your instance. i.e. it's not automatic. class LgiClass GDefaultDocumentEnv : public GDocumentEnv { public: LoadType GetContent(LoadJob *&j); bool OnNavigate(GDocView *Parent, const char *Uri); }; /// Find params class GDocFindReplaceParams { public: virtual ~GDocFindReplaceParams() {} }; /// TextView class is a base for all text controls class LgiClass GDocView : public GLayout, virtual public GDom { friend class GDocumentEnv; protected: GDocumentEnv *Environment; GAutoString Charset; public: // Static static const char *WhiteSpace; static const char *Delimiters; static const char *UrlDelim; /////////////////////////////////////////////////////////////////////// // Properties #define _TvMenuProp(Type, Name) \ protected: \ Type Name; \ public: \ virtual void Set##Name(Type i) { Name=i; } \ Type Get##Name() { return Name; } _TvMenuProp(uint16, WrapAtCol) _TvMenuProp(bool, UrlDetect) _TvMenuProp(bool, ReadOnly) _TvMenuProp(uint8, WrapType) _TvMenuProp(uint8, TabSize) _TvMenuProp(uint8, IndentSize) _TvMenuProp(bool, HardTabs) _TvMenuProp(bool, ShowWhiteSpace) _TvMenuProp(bool, ObscurePassword) _TvMenuProp(bool, CrLf) _TvMenuProp(bool, AutoIndent) _TvMenuProp(bool, FixedWidthFont) _TvMenuProp(bool, LoadImages) _TvMenuProp(bool, OverideDocCharset) // This UID is used to match data load events with their source document. // Sometimes data will arrive after the document that asked for it has // already been unloaded. So by assigned each document an UID we can check // the job UID against it and discard old data. _TvMenuProp(int, DocumentUid) #undef _TvMenuProp virtual const char *GetCharset() { return Charset ? Charset.Get() : "utf-8"; } virtual void SetCharset(const char *s) { Charset.Reset(NewStr(s)); } virtual const char *GetMimeType() = 0; /////////////////////////////////////////////////////////////////////// // Object GDocView(GDocumentEnv *e = 0) { WrapAtCol = 0; UrlDetect = true; ReadOnly = false; WrapType = TEXTED_WRAP_REFLOW; TabSize = 4; IndentSize = 4; HardTabs = true; ShowWhiteSpace = false; ObscurePassword = false; CrLf = false; AutoIndent = true; FixedWidthFont = false; LoadImages = false; OverideDocCharset = false; Environment = 0; SetEnv(e); } virtual ~GDocView() { SetEnv(0); } const char *GetClass() { return "GDocView"; } /// Open a file handler virtual bool Open(const char *Name, const char *Cs = 0) { return false; } /// Save a file handler virtual bool Save(const char *Name, const char *Cs = 0) { return false; } /////////////////////////////////////////////////////////////////////// /// Find window handler virtual bool DoFind() { return false; } /// Replace window handler virtual bool DoReplace() { return false; } virtual GDocFindReplaceParams *CreateFindReplaceParams() { return 0; } virtual void SetFindReplaceParams(GDocFindReplaceParams *Params) { } /////////////////////////////////////////////////////////////////////// /// Get the current environment virtual GDocumentEnv *GetEnv() { return Environment; } /// Set the current environment virtual void SetEnv(GDocumentEnv *e) { if (Environment) Environment->DetachView(this); Environment = e; if (Environment) Environment->AttachView(this); } /// When the env has loaded a resource it can pass it to the doc control via this method. /// It MUST be thread safe. Often an environment will call this function directly from /// it's worker thread. virtual void OnContent(GDocumentEnv::LoadJob *Res) {} /////////////////////////////////////////////////////////////////////// // State / Selection /// Set the cursor position, to select an area, move the cursor with Select=false /// then set the other end of the region with Select=true. virtual void SetCaret(size_t i, bool Select, bool ForceFullUpdate = false) {} /// Cursor=false means the other end of the selection if any. The cursor is alwasy /// at one end of the selection. virtual ssize_t GetCaret(bool Cursor = true) { return 0; } /// True if there is a selection virtual bool HasSelection() { return false; } /// Unselect all the text virtual void UnSelectAll() {} /// Select the word from index 'From' virtual void SelectWord(size_t From) {} /// Select all the text in the control virtual void SelectAll() {} /// Get the selection as a dynamicially allocated utf-8 string virtual char *GetSelection() { return 0; } /// Returns the character index at the x,y location virtual ssize_t IndexAt(int x, int y) { return 0; } /// Index=-1 returns the x,y of the cursor, Index >=0 returns the specified x,y virtual bool GetLineColumnAtIndex(GdcPt2 &Pt, int Index = -1) { return false; } /// True if the document has changed virtual bool IsDirty() { return false; } /// Gets the number of lines of text virtual int GetLines() { return 0; } /// Gets the pixels required to display all the text virtual void GetTextExtent(int &x, int &y) {} /////////////////////////////////////////////////////////////////////// /// Cuts the selection from the document and puts it on the clipboard virtual bool Cut() { return false; } /// Copies the selection from the document to the clipboard virtual bool Copy() { return false; } /// Pastes the current contents of the clipboard into the document virtual bool Paste() { return false; } /////////////////////////////////////////////////////////////////////// /// Called when the user hits the escape key virtual void OnEscape(GKey &K) {} /// Called when the user hits the enter key virtual void OnEnter(GKey &k) {} /// Called when the user clicks a URL virtual void OnUrl(char *Url) {} /// Called to add styling to the document virtual void OnAddStyle(const char *MimeType, const char *Styles) {} /////////////////////////////////////////////////////////////////////// struct ContentMedia { GString Id; GString FileName; GString MimeType; GVariant Data; GAutoPtr Stream; + + bool Valid() + { + return MimeType.Get() != NULL && + FileName.Get() != NULL && + ( + (Data.Type == GV_BINARY && Data.Value.Binary.Data != NULL) + || + (Stream.Get() != NULL) + ); + } }; /// Gets the document in format of a desired MIME type virtual bool GetFormattedContent ( /// [In] The desired mime type of the content const char *MimeType, /// [Out] The content in the specified mime type GString &Out, /// [Out/Optional] Any attached media files that the content references GArray *Media = NULL ) { return false; } }; /// Detects links in text, returning their location and type template bool LgiDetectLinks(GArray &Links, T *Text, ssize_t TextCharLen = -1) { if (!Text) return false; if (TextCharLen < 0) TextCharLen = Strlen(Text); T *End = Text + TextCharLen; static T Http[] = {'h', 't', 't', 'p', ':', '/', '/', 0 }; static T Https[] = {'h', 't', 't', 'p', 's', ':', '/', '/', 0}; for (int64 i=0; i= 7 && ( Strnicmp(Text+i, Http, 6) == 0 || Strnicmp(Text+i, Https, 7) == 0 ) ) { // find end T *s = Text + i; T *e = s + 6; for ( ; e < End && UrlChar(*e); e++) ; while ( e > s && ! ( IsAlpha(e[-1]) || IsDigit(e[-1]) || e[-1] == '/' ) ) e--; Links.New().Set(s - Text, e - s, false); i = e - Text; } break; } case '@': { // find start T *s = Text + (MAX(i, 1) - 1); for ( ; s > Text && EmailChar(*s); s--) ; if (s < Text + i) { if (!EmailChar(*s)) s++; bool FoundDot = false; T *Start = Text + i + 1; T *e = Start; for ( ; e < End && EmailChar(*e); e++) { if (*e == '.') FoundDot = true; } while (e > Start && e[-1] == '.') e--; if (FoundDot) { Links.New().Set(s - Text, e - s, true); i = e - Text; } } break; } } } return true; } #endif diff --git a/include/common/GRange.h b/include/common/GRange.h --- a/include/common/GRange.h +++ b/include/common/GRange.h @@ -1,48 +1,91 @@ #ifndef _GRANGE_H_ #define _GRANGE_H_ +#include + #ifndef MAX #define MAX(a,b) ((a) > (b) ? a : b) #endif #ifndef MIN #define MIN(a,b) ((a) < (b) ? a : b) #endif struct GRange { ssize_t Start; ssize_t Len; GRange(ssize_t s = 0, ssize_t l = 0) { Start = s; Len = l; } + GRange &Set(ssize_t s, ssize_t l) + { + Start = s; + Len = l; + return *this; + } + GRange Overlap(const GRange &r) { GRange o; if (r.Start >= End()) return o; if (r.End() <= Start) return o; ssize_t e = MIN(End(), r.End()); o.Start = MAX(r.Start, Start); o.Len = e - o.Start; return o; } bool Overlap(ssize_t Val) { return Val >= Start && Val <= End(); } ssize_t End() const { return Start + Len; } + + bool Valid() const + { + return Start >= 0 && Len > 0; + } + + bool operator ==(const GRange &r) const { return Start == r.Start && Len == r.Len; } + bool operator !=(const GRange &r) const { return Start != r.Start || Len != r.Len; } + bool operator >(const GRange &r) const { return Start > r.Start; } + bool operator >=(const GRange &r) const { return Start >= r.Start; } + bool operator <(const GRange &r) const { return Start < r.Start; } + bool operator <=(const GRange &r) const { return Start < r.Start; } + + GRange &operator -=(const GRange &del) + { + GRange o = Overlap(del); + if (o.Valid()) + { + assert(o.Len <= Len); + Len -= o.Len; + if (del.Start < o.Start) + Start = del.Start; + } + // else nothing happens + + return *this; + } + + GRange &operator =(const GRange &r) + { + this->Start = r.Start; + this->Len = r.Len; + return *this; + } }; #endif \ No newline at end of file diff --git a/include/common/LgiSpellCheck.h b/include/common/LgiSpellCheck.h --- a/include/common/LgiSpellCheck.h +++ b/include/common/LgiSpellCheck.h @@ -1,159 +1,159 @@ #ifndef _LGI_SPELL_CHECK_H_ #define _LGI_SPELL_CHECK_H_ #include "GEventTargetThread.h" #include "GOptionsFile.h" enum SPELL_MSGS { M_CHECK_TEXT = M_USER + 100, M_ENUMERATE_LANGUAGES, M_ENUMERATE_DICTIONARIES, M_SET_DICTIONARY, M_SET_PARAMS, M_ADD_WORD, M_INSTALL_DICTIONARY, }; +enum SpellCheckParams +{ + SpellBlockPtr, +}; + #define SPELL_CHK_VALID_HND(hnd) \ if (hnd < 0) \ { \ LgiAssert(0); \ return false; \ } // Spell check interface class GSpellCheck : public GEventTargetThread { public: static const char Delimiters[]; struct LanguageId { GString LangCode; GString EnglishName; GString NativeName; }; struct DictionaryId { GString Lang; GString Dict; }; struct Params { GOptionsFile::PortableType IsPortable; GString OptionsPath; GString Lang, Dict; GCapabilityTarget *CapTarget; Params() { CapTarget = NULL; } }; - struct SpellingError + struct SpellingError : public GRange { - ssize_t Start, Len; GString::Array Suggestions; - - ssize_t End() { return Start + Len; } }; - struct CheckText + struct CheckText : public GRange { GString Text; - int Len; GArray Errors; // Application specific data - void *UserPtr; - int64 UserInt; + GArray User; CheckText() { - Len = 0; - UserPtr = NULL; - UserInt = 0; } }; GSpellCheck(GString Name) : GEventTargetThread(Name) {} virtual ~GSpellCheck() {} // Impl OnEvent in your subclass: // GMessage::Result OnEvent(GMessage *Msg); /// Sends a M_ENUMERATE_LANGUAGES event to 'ResponseHnd' with a heap /// allocated GArray. bool EnumLanguages(int ResponseHnd) { SPELL_CHK_VALID_HND(ResponseHnd); return PostEvent(M_ENUMERATE_LANGUAGES, (GMessage::Param)ResponseHnd); } /// Sends a M_ENUMERATE_DICTIONARIES event to 'ResponseHnd' with a heap /// allocated GArray. bool EnumDictionaries(int ResponseHnd, const char *Lang) { SPELL_CHK_VALID_HND(ResponseHnd); return PostEvent(M_ENUMERATE_DICTIONARIES, (GMessage::Param)ResponseHnd, (GMessage::Param)new GString(Lang)); } bool SetParams(GAutoPtr p) { return PostObject(GetHandle(), M_SET_PARAMS, p); } bool SetDictionary(int ResponseHnd, const char *Lang, const char *Dictionary = NULL) { SPELL_CHK_VALID_HND(ResponseHnd); GAutoPtr i(new DictionaryId); if (!i) return false; i->Lang = Lang; i->Dict = Dictionary; return PostObject( GetHandle(), M_SET_DICTIONARY, (GMessage::Param)ResponseHnd, i); } - bool Check(int ResponseHnd, GString s, int64 UserInt = 0, void *UserPtr = NULL) + bool Check( int ResponseHnd, + GString s, + ssize_t Start, ssize_t Len, + GArray *User = NULL /* see 'SpellCheckParams' */) { SPELL_CHK_VALID_HND(ResponseHnd); if (s.Length() == 0) return false; GAutoPtr c(new CheckText); - GUtf8Str Utf(s); c->Text = s; - c->Len = Utf.GetChars(); - c->UserInt = UserInt; - c->UserPtr = UserPtr; + c->Start = Start; + c->Len = Len; + if (User) + c->User = *User; return PostObject(GetHandle(), M_CHECK_TEXT, (GMessage::Param)ResponseHnd, c); } bool InstallDictionary() { return PostEvent(M_INSTALL_DICTIONARY); } }; // These are the various implementations of the this object. You have to include the // correct C++ source to get this to link. extern GAutoPtr CreateWindowsSpellCheck(); // Available on Windows 8.0 and greater extern GAutoPtr CreateAppleSpellCheck(); // Available on Mac OS X extern GAutoPtr CreateAspellObject(); // Available anywhere. #endif \ No newline at end of file diff --git a/src/common/INet/OpenSSLSocket.cpp b/src/common/INet/OpenSSLSocket.cpp --- a/src/common/INet/OpenSSLSocket.cpp +++ b/src/common/INet/OpenSSLSocket.cpp @@ -1,1421 +1,1425 @@ /*hdr ** FILE: OpenSSLSocket.cpp ** AUTHOR: Matthew Allen ** DATE: 24/9/2004 ** DESCRIPTION: Open SSL wrapper socket ** ** Copyright (C) 2004-2014, Matthew Allen ** fret@memecode.com ** */ #include #ifdef WINDOWS #pragma comment(lib,"Ws2_32.lib") #else #include #endif #include "Lgi.h" #include "OpenSSLSocket.h" #ifdef WIN32 #include #endif #include "GToken.h" #include "GVariant.h" #include "INet.h" +#if OPENSSL_VERSION_NUMBER >= 0x10100000L +#error "SSL library too new." +#endif + #define PATH_OFFSET "../" #ifdef WIN32 #define SSL_LIBRARY "ssleay32" #define EAY_LIBRARY "libeay32" #else #define SSL_LIBRARY "libssl" #endif #define HasntTimedOut() ((To < 0) || (LgiCurrentTime() - Start < To)) static const char* MinimumVersion = "1.0.1g"; void SSL_locking_function(int mode, int n, const char *file, int line); unsigned long SSL_id_function(); class LibSSL : public GLibrary { public: LibSSL() { char p[MAX_PATH]; #if defined MAC if (LgiGetExeFile(p, sizeof(p))) { LgiMakePath(p, sizeof(p), p, "Contents/MacOS/libssl.1.0.0.dylib"); if (FileExists(p)) { Load(p); } } if (!IsLoaded()) { Load("/opt/local/lib/" SSL_LIBRARY); } #elif defined LINUX if (LgiGetExePath(p, sizeof(p))) { LgiMakePath(p, sizeof(p), p, "libssl.so"); if (FileExists(p)) { LgiTrace("%s:%i - loading SSL library '%s'\n", _FL, p); Load(p); } } #endif if (!IsLoaded()) Load(SSL_LIBRARY); if (!IsLoaded()) { LgiGetExePath(p, sizeof(p)); LgiMakePath(p, sizeof(p), p, PATH_OFFSET "../OpenSSL"); #ifdef WIN32 char old[300]; FileDev->GetCurrentFolder(old, sizeof(old)); FileDev->SetCurrentFolder(p); #endif Load(SSL_LIBRARY); #ifdef WIN32 FileDev->SetCurrentFolder(old); #endif } } #if OPENSSL_VERSION_NUMBER >= 0x10100000L DynFunc0(int, OPENSSL_library_init); DynFunc0(int, OPENSSL_load_error_strings); DynFunc2(int, OPENSSL_init_crypto, uint64_t, opts, const OPENSSL_INIT_SETTINGS *, settings); DynFunc2(int, OPENSSL_init_ssl, uint64_t, opts, const OPENSSL_INIT_SETTINGS *, settings); #else DynFunc0(int, SSL_library_init); DynFunc0(int, SSL_load_error_strings); #endif DynFunc1(int, SSL_open, SSL*, s); DynFunc1(int, SSL_connect, SSL*, s); DynFunc4(long, SSL_ctrl, SSL*, ssl, int, cmd, long, larg, void*, parg); DynFunc1(int, SSL_shutdown, SSL*, s); DynFunc1(int, SSL_free, SSL*, ssl); DynFunc1(int, SSL_get_fd, const SSL *, s); DynFunc2(int, SSL_set_fd, SSL*, s, int, fd); DynFunc1(SSL*, SSL_new, SSL_CTX*, ctx); DynFunc1(BIO*, BIO_new_ssl_connect, SSL_CTX*, ctx); DynFunc1(X509*, SSL_get_peer_certificate, SSL*, s); DynFunc1(int, SSL_set_connect_state, SSL*, s); DynFunc1(int, SSL_set_accept_state, SSL*, s); DynFunc2(int, SSL_get_error, SSL*, s, int, ret_code); DynFunc3(int, SSL_set_bio, SSL*, s, BIO*, rbio, BIO*, wbio); DynFunc3(int, SSL_write, SSL*, ssl, const void*, buf, int, num); DynFunc3(int, SSL_read, SSL*, ssl, const void*, buf, int, num); DynFunc1(int, SSL_pending, SSL*, ssl); DynFunc1(BIO *, SSL_get_rbio, const SSL *, s); DynFunc1(int, SSL_accept, SSL *, ssl); DynFunc0(SSL_METHOD*, SSLv23_client_method); DynFunc0(SSL_METHOD*, SSLv23_server_method); DynFunc1(SSL_CTX*, SSL_CTX_new, SSL_METHOD*, meth); DynFunc3(int, SSL_CTX_load_verify_locations, SSL_CTX*, ctx, const char*, CAfile, const char*, CApath); DynFunc3(int, SSL_CTX_use_certificate_file, SSL_CTX*, ctx, const char*, file, int, type); DynFunc3(int, SSL_CTX_use_PrivateKey_file, SSL_CTX*, ctx, const char*, file, int, type); DynFunc1(int, SSL_CTX_check_private_key, const SSL_CTX*, ctx); DynFunc1(int, SSL_CTX_free, SSL_CTX*, ctx); #ifdef WIN32 // If this is freaking you out then good... openssl-win32 ships // in 2 DLL's and on Linux everything is 1 shared object. Thus // the code reflects that. }; class LibEAY : public GLibrary { public: LibEAY() : GLibrary(EAY_LIBRARY) { if (!IsLoaded()) { char p[300]; LgiGetExePath(p, sizeof(p)); LgiMakePath(p, sizeof(p), p, PATH_OFFSET "../OpenSSL"); #ifdef WIN32 char old[300]; FileDev->GetCurrentFolder(old, sizeof(old)); FileDev->SetCurrentFolder(p); #endif Load("libeay32"); #ifdef WIN32 FileDev->SetCurrentFolder(old); #endif } } #endif typedef void (*locking_callback)(int mode,int type, const char *file,int line); typedef unsigned long (*id_callback)(); DynFunc1(const char *, SSLeay_version, int, type); DynFunc1(BIO*, BIO_new, BIO_METHOD*, type); DynFunc0(BIO_METHOD*, BIO_s_socket); DynFunc0(BIO_METHOD*, BIO_s_mem); DynFunc1(BIO*, BIO_new_connect, char *, host_port); DynFunc4(long, BIO_ctrl, BIO*, bp, int, cmd, long, larg, void*, parg); DynFunc4(long, BIO_int_ctrl, BIO *, bp, int, cmd, long, larg, int, iarg); DynFunc3(int, BIO_read, BIO*, b, void*, data, int, len); DynFunc3(int, BIO_write, BIO*, b, const void*, data, int, len); DynFunc1(int, BIO_free, BIO*, a); DynFunc1(int, BIO_free_all, BIO*, a); DynFunc2(int, BIO_test_flags, const BIO *, b, int, flags); DynFunc0(int, ERR_load_BIO_strings); #if OPENSSL_VERSION_NUMBER < 0x10100000L DynFunc0(int, ERR_free_strings); DynFunc0(int, EVP_cleanup); DynFunc0(int, OPENSSL_add_all_algorithms_noconf); DynFunc1(int, CRYPTO_set_locking_callback, locking_callback, func); DynFunc1(int, CRYPTO_set_id_callback, id_callback, func); DynFunc0(int, CRYPTO_num_locks); #endif DynFunc1(const char *, ERR_lib_error_string, unsigned long, e); DynFunc1(const char *, ERR_func_error_string, unsigned long, e); DynFunc1(const char *, ERR_reason_error_string, unsigned long, e); DynFunc1(int, ERR_print_errors, BIO *, bp); DynFunc3(char*, X509_NAME_oneline, X509_NAME*, a, char*, buf, int, size); DynFunc1(X509_NAME*, X509_get_subject_name, X509*, a); DynFunc2(char*, ERR_error_string, unsigned long, e, char*, buf); DynFunc0(unsigned long, ERR_get_error); }; typedef GArray SslVer; SslVer ParseSslVersion(const char *v) { GToken t(v, "."); SslVer out; for (unsigned i=0; i(SslVer &a, SslVer &b) { return CompareSslVersion(a, b) > 0; } static const char *FileLeaf(const char *f) { const char *l = strrchr(f, DIR_CHAR); return l ? l + 1 : f; } #undef _FL #define _FL FileLeaf(__FILE__), __LINE__ class OpenSSL : #ifdef WINDOWS public LibEAY, #endif public LibSSL { SSL_CTX *Server; public: SSL_CTX *Client; GArray Locks; GAutoString ErrorMsg; bool IsLoaded() { return LibSSL::IsLoaded() #ifdef WINDOWS && LibEAY::IsLoaded() #endif ; } bool InitLibrary(SslSocket *sock) { GStringPipe Err; GArray Ver; GArray MinimumVer = ParseSslVersion(MinimumVersion); GToken t; int Len = 0; const char *v = NULL; if (!IsLoaded()) { Err.Print("%s:%i - SSL libraries missing.\n", _FL); goto OnError; } SSL_library_init(); SSL_load_error_strings(); ERR_load_BIO_strings(); OpenSSL_add_all_algorithms(); Len = CRYPTO_num_locks(); Locks.Length(Len); CRYPTO_set_locking_callback(SSL_locking_function); CRYPTO_set_id_callback(SSL_id_function); v = SSLeay_version(SSLEAY_VERSION); if (!v) { Err.Print("%s:%i - SSLeay_version failed.\n", _FL); goto OnError; } t.Parse(v, " "); if (t.Length() < 2) { Err.Print("%s:%i - SSLeay_version: no version\n", _FL); goto OnError; } Ver = ParseSslVersion(t[1]); if (Ver.Length() < 3) { Err.Print("%s:%i - SSLeay_version: not enough tokens\n", _FL); goto OnError; } if (Ver < MinimumVer) { #if WINDOWS char FileName[MAX_PATH] = ""; DWORD r = GetModuleFileNameA(LibEAY::Handle(), FileName, sizeof(FileName)); #endif Err.Print("%s:%i - SSL version '%s' is too old (minimum '%s')\n" #if WINDOWS "%s\n" #endif , _FL, t[1], MinimumVersion #if WINDOWS ,FileName #endif ); goto OnError; } Client = SSL_CTX_new(SSLv23_client_method()); if (!Client) { long e = ERR_get_error(); char *Msg = ERR_error_string(e, 0); Err.Print("%s:%i - SSL_CTX_new(client) failed with '%s' (%i)\n", _FL, Msg, e); goto OnError; } return true; OnError: ErrorMsg.Reset(Err.NewStr()); if (sock) sock->DebugTrace("%s", ErrorMsg.Get()); return false; } OpenSSL() { Client = NULL; Server = NULL; } ~OpenSSL() { if (Client) { SSL_CTX_free(Client); Client = NULL; } if (Server) { SSL_CTX_free(Server); Server = NULL; } Locks.DeleteObjects(); } SSL_CTX *GetServer(SslSocket *sock, const char *CertFile, const char *KeyFile) { if (!Server) { Server = SSL_CTX_new(SSLv23_server_method()); if (Server) { if (CertFile) SSL_CTX_use_certificate_file(Server, CertFile, SSL_FILETYPE_PEM); if (KeyFile) SSL_CTX_use_PrivateKey_file(Server, KeyFile, SSL_FILETYPE_PEM); if (!SSL_CTX_check_private_key(Server)) { LgiAssert(0); } } else { long e = ERR_get_error(); char *Msg = ERR_error_string(e, 0); GStringPipe p; p.Print("%s:%i - SSL_CTX_new(server) failed with '%s' (%i)\n", _FL, Msg, e); ErrorMsg.Reset(p.NewStr()); sock->DebugTrace("%s", ErrorMsg.Get()); } } return Server; } bool IsOk(SslSocket *sock) { bool Loaded = #ifdef WIN32 LibSSL::IsLoaded() && LibEAY::IsLoaded(); #else IsLoaded(); #endif if (Loaded) return true; // Try and load again... cause the library can be provided by install on demand. #ifdef WIN32 Loaded = LibSSL::Load(SSL_LIBRARY) && LibEAY::Load(EAY_LIBRARY); #else Loaded = Load(SSL_LIBRARY); #endif if (Loaded) InitLibrary(sock); return Loaded; } }; static OpenSSL *Library = 0; #if 0 #define SSL_DEBUG_LOCKING #endif void SSL_locking_function(int mode, int n, const char *file, int line) { LgiAssert(Library != NULL); if (Library) { if (!Library->Locks[n]) { #ifdef SSL_DEBUG_LOCKING LgiTrace("SSL[%i] create\n", n); #endif Library->Locks[n] = new LMutex; } #ifdef SSL_DEBUG_LOCKING LgiTrace("SSL[%i] lock=%i, unlock=%i, re=%i, wr=%i (mode=0x%x, cnt=%i, thr=0x%x, %s:%i)\n", n, TestFlag(mode, CRYPTO_LOCK), TestFlag(mode, CRYPTO_UNLOCK), TestFlag(mode, CRYPTO_READ), TestFlag(mode, CRYPTO_WRITE), mode, Library->Locks[n]->GetCount(), LgiGetCurrentThread(), file, line); #endif if (mode & CRYPTO_LOCK) Library->Locks[n]->Lock((char*)file, line); else if (mode & CRYPTO_UNLOCK) Library->Locks[n]->Unlock(); } } unsigned long SSL_id_function() { return (unsigned long) LgiGetCurrentThread(); } bool StartSSL(GAutoString &ErrorMsg, SslSocket *sock) { static LMutex Lock; if (Lock.Lock(_FL)) { if (!Library) { Library = new OpenSSL; if (Library && !Library->InitLibrary(sock)) { ErrorMsg = Library->ErrorMsg; DeleteObj(Library); } } Lock.Unlock(); } return Library != NULL; } void EndSSL() { DeleteObj(Library); } struct SslSocketPriv { GCapabilityClient *Caps; bool SslOnConnect; bool IsSSL; bool UseSSLrw; int Timeout; bool RawLFCheck; #ifdef _DEBUG bool LastWasCR; #endif bool IsBlocking; // This is just for the UI. GStreamI *Logger; // This is for the connection logging. GAutoString LogFile; GAutoPtr LogStream; int LogFormat; SslSocketPriv() { #ifdef _DEBUG LastWasCR = false; #endif Timeout = 20 * 1000; IsSSL = false; UseSSLrw = false; LogFormat = 0; } }; bool SslSocket::DebugLogging = false; SslSocket::SslSocket(GStreamI *logger, GCapabilityClient *caps, bool sslonconnect, bool RawLFCheck) { d = new SslSocketPriv; Bio = 0; Ssl = 0; d->RawLFCheck = RawLFCheck; d->SslOnConnect = sslonconnect; d->Caps = caps; d->Logger = logger; d->IsBlocking = true; GAutoString ErrMsg; if (StartSSL(ErrMsg, this)) { #ifdef WIN32 if (Library->IsOk(this)) { char n[MAX_PATH]; char s[MAX_PATH]; if (GetModuleFileNameA(Library->LibSSL::Handle(), n, sizeof(n))) { sprintf_s(s, sizeof(s), "Using '%s'", n); OnInformation(s); } if (GetModuleFileNameA(Library->LibEAY::Handle(), n, sizeof(n))) { sprintf_s(s, sizeof(s), "Using '%s'", n); OnInformation(s); } } #endif } else if (caps) { caps->NeedsCapability("openssl", ErrMsg); } else { OnError(0, "Can't load or find OpenSSL library."); } } SslSocket::~SslSocket() { Close(); DeleteObj(d); } GStreamI *SslSocket::Clone() { return new SslSocket(d->Logger, d->Caps, true); } int SslSocket::GetTimeout() { return d->Timeout; } void SslSocket::SetTimeout(int ms) { d->Timeout = ms; } void SslSocket::SetLogger(GStreamI *logger) { d->Logger = logger; } void SslSocket::SetSslOnConnect(bool b) { d->SslOnConnect = b; } GStream *SslSocket::GetLogStream() { if (!d->LogStream && d->LogFile) { if (!d->LogStream.Reset(new GFile)) return NULL; if (!d->LogStream->Open(d->LogFile, O_WRITE)) return NULL; // Seek to the end d->LogStream->SetPos(d->LogStream->GetSize()); } return d->LogStream; } bool SslSocket::GetVariant(const char *Name, GVariant &Val, char *Arr) { if (!Name) return false; if (!_stricmp(Name, "isSsl")) // Type: Bool { Val = true; return true; } return false; } void SslSocket::Log(const char *Str, ssize_t Bytes, SocketMsgType Type) { if (!ValidStr(Str)) return; if (d->Logger) d->Logger->Write(Str, Bytes<0?(int)strlen(Str):Bytes, Type); else if (Type == SocketMsgError) LgiTrace("%.*s", Bytes, Str); } const char *SslSocket::GetErrorString() { return ErrMsg; } void SslSocket::SslError(const char *file, int line, const char *Msg) { char *Part = strrchr((char*)file, DIR_CHAR); #ifndef WIN32 printf("%s:%i - %s\n", file, line, Msg); #endif ErrMsg.Printf("Error: %s:%i - %s\n", Part ? Part + 1 : file, line, Msg); Log(ErrMsg, ErrMsg.Length(), SocketMsgError); } OsSocket SslSocket::Handle(OsSocket Set) { OsSocket h = INVALID_SOCKET; if (Set != INVALID_SOCKET) { long r; bool IsError = false; if (!Ssl) { Ssl = Library->SSL_new(Library->GetServer(this, NULL, NULL)); } if (Ssl) { r = Library->SSL_set_fd(Ssl, Set); Bio = Library->SSL_get_rbio(Ssl); r = Library->SSL_accept(Ssl); if (r <= 0) IsError = true; else if (r == 1) h = Set; } else IsError = true; if (IsError) { long e = Library->ERR_get_error(); char *Msg = Library->ERR_error_string(e, 0); Log(Msg, -1, SocketMsgError); return INVALID_SOCKET; } } else if (Bio) { uint32 hnd = INVALID_SOCKET; Library->BIO_get_fd(Bio, &hnd); h = hnd; } return h; } bool SslSocket::IsOpen() { return Bio != 0; } GString SslGetErrorAsString(OpenSSL *Library) { BIO *bio = Library->BIO_new (Library->BIO_s_mem()); Library->ERR_print_errors (bio); char *buf = NULL; size_t len = Library->BIO_get_mem_data (bio, &buf); GString s(buf, len); Library->BIO_free (bio); return s; } int SslSocket::Open(const char *HostAddr, int Port) { bool Status = false; LMutex::Auto Lck(&Lock, _FL); DebugTrace("%s:%i - SslSocket::Open(%s,%i)\n", _FL, HostAddr, Port); if (Library && Library->IsOk(this) && HostAddr) { char h[256]; sprintf_s(h, sizeof(h), "%s:%i", HostAddr, Port); // Do SSL handshake? if (d->SslOnConnect) { // SSL connection.. d->IsSSL = true; if (Library->Client) { const char *CertDir = "/u/matthew/cert"; long r = Library->SSL_CTX_load_verify_locations(Library->Client, 0, CertDir); DebugTrace("%s:%i - SSL_CTX_load_verify_locations=%i\n", _FL, r); if (r > 0) { Bio = Library->BIO_new_ssl_connect(Library->Client); DebugTrace("%s:%i - BIO_new_ssl_connect=%p\n", _FL, Bio); if (Bio) { Library->BIO_get_ssl(Bio, &Ssl); DebugTrace("%s:%i - BIO_get_ssl=%p\n", _FL, Ssl); if (Ssl) { // SNI setup Library->SSL_set_tlsext_host_name(Ssl, HostAddr); // Library->SSL_CTX_set_timeout() Library->BIO_set_conn_hostname(Bio, HostAddr); #if OPENSSL_VERSION_NUMBER < 0x10100000L Library->BIO_set_conn_int_port(Bio, &Port); #else GString sPort; sPort.Printf("%i"); Library->BIO_set_conn_port(Bio, sPort.Get()); #endif // Do non-block connect uint64 Start = LgiCurrentTime(); int To = GetTimeout(); IsBlocking(false); r = Library->SSL_connect(Ssl); DebugTrace("%s:%i - initial SSL_connect=%i\n", _FL, r); while (r != 1 && !IsCancelled()) { long err = Library->SSL_get_error(Ssl, r); if (err != SSL_ERROR_WANT_CONNECT) { DebugTrace("%s:%i - SSL_get_error=%i\n", _FL, err); } LgiSleep(50); r = Library->SSL_connect(Ssl); DebugTrace("%s:%i - SSL_connect=%i (%i of %i ms)\n", _FL, r, (int)(LgiCurrentTime() - Start), (int)To); bool TimeOut = !HasntTimedOut(); if (TimeOut) { DebugTrace("%s:%i - SSL connect timeout, to=%i\n", _FL, To); SslError(_FL, "Connection timeout."); break; } } DebugTrace("%s:%i - open loop finished, r=%i, Cancelled=%i\n", _FL, r, IsCancelled()); if (r == 1) { IsBlocking(true); Library->SSL_set_mode(Ssl, SSL_MODE_AUTO_RETRY); Status = true; // d->UseSSLrw = true; char m[256]; sprintf_s(m, sizeof(m), "Connected to '%s' using SSL", h); OnInformation(m); } else { GString Err = SslGetErrorAsString(Library).Strip(); if (!Err) Err.Printf("BIO_do_connect(%s:%i) failed.", HostAddr, Port); SslError(_FL, Err); } } else SslError(_FL, "BIO_get_ssl failed."); } else SslError(_FL, "BIO_new_ssl_connect failed."); } else SslError(_FL, "SSL_CTX_load_verify_locations failed."); } else SslError(_FL, "No Ctx."); } else { Bio = Library->BIO_new_connect(h); DebugTrace("%s:%i - BIO_new_connect=%p\n", _FL, Bio); if (Bio) { // Non SSL... go into non-blocking mode so that if ::Close() is called we // can quit out of the connect loop. IsBlocking(false); uint64 Start = LgiCurrentTime(); int To = GetTimeout(); long r = Library->BIO_do_connect(Bio); DebugTrace("%s:%i - BIO_do_connect=%i\n", _FL, r); while (r != 1 && !IsCancelled()) { if (!Library->BIO_should_retry(Bio)) { break; } LgiSleep(50); r = Library->BIO_do_connect(Bio); DebugTrace("%s:%i - BIO_do_connect=%i\n", _FL, r); if (!HasntTimedOut()) { DebugTrace("%s:%i - open timeout, to=%i\n", _FL, To); OnError(0, "Connection timeout."); break; } } DebugTrace("%s:%i - open loop finished=%i\n", _FL, r); if (r == 1) { IsBlocking(true); Status = true; char m[256]; sprintf_s(m, sizeof(m), "Connected to '%s'", h); OnInformation(m); } else SslError(_FL, "BIO_do_connect failed"); } else SslError(_FL, "BIO_new_connect failed"); } } if (!Status) { Close(); } DebugTrace("%s:%i - SslSocket::Open status=%i\n", _FL, Status); return Status; } bool SslSocket::SetVariant(const char *Name, GVariant &Value, char *Arr) { bool Status = false; if (!Library || !Name) return false; if (!_stricmp(Name, SslSocket_LogFile)) { d->LogFile.Reset(Value.ReleaseStr()); } else if (!_stricmp(Name, SslSocket_LogFormat)) { d->LogFormat = Value.CastInt32(); } else if (!_stricmp(Name, GSocket_Protocol)) { char *v = Value.CastString(); if (v && stristr(v, "SSL")) { if (!Bio) { d->SslOnConnect = true; } else { if (!Library->Client) { SslError(_FL, "Library->Client is null."); } else { Ssl = Library->SSL_new(Library->Client); DebugTrace("%s:%i - SSL_new=%p\n", _FL, Ssl); if (!Ssl) { SslError(_FL, "SSL_new failed."); } else { int r = Library->SSL_set_bio(Ssl, Bio, Bio); DebugTrace("%s:%i - SSL_set_bio=%i\n", _FL, r); uint64 Start = LgiCurrentTime(); int To = GetTimeout(); while (HasntTimedOut()) { r = Library->SSL_connect(Ssl); DebugTrace("%s:%i - SSL_connect=%i\n", _FL, r); if (r < 0) LgiSleep(100); else break; } if (r > 0) { Status = d->UseSSLrw = d->IsSSL = true; OnInformation("Session is now using SSL"); X509 *ServerCert = Library->SSL_get_peer_certificate(Ssl); DebugTrace("%s:%i - SSL_get_peer_certificate=%p\n", _FL, ServerCert); if (ServerCert) { char Txt[256] = ""; Library->X509_NAME_oneline(Library->X509_get_subject_name(ServerCert), Txt, sizeof(Txt)); DebugTrace("%s:%i - X509_NAME_oneline=%s\n", _FL, Txt); OnInformation(Txt); } // SSL_get_verify_result } else { SslError(_FL, "SSL_connect failed."); r = Library->SSL_get_error(Ssl, r); char *Msg = Library->ERR_error_string(r, 0); if (Msg) { OnError(r, Msg); } } } } } } } return Status; } int SslSocket::Close() { Cancel(true); LMutex::Auto Lck(&Lock, _FL); if (Library) { if (Ssl) { DebugTrace("%s:%i - SSL_shutdown\n", _FL); int r = 0; if ((r = Library->SSL_shutdown(Ssl)) >= 0) { #ifdef WIN32 closesocket #else close #endif (Library->SSL_get_fd(Ssl)); } Library->SSL_free(Ssl); OnInformation("SSL connection closed."); // I think the Ssl object "owns" the Bio object... // So assume it gets fread by SSL_shutdown } else if (Bio) { DebugTrace("%s:%i - BIO_free\n", _FL); Library->BIO_free(Bio); OnInformation("Connection closed."); } Ssl = 0; Bio = 0; } else return false; return true; } bool SslSocket::Listen(int Port) { return false; } bool SslSocket::IsBlocking() { return d->IsBlocking; } void SslSocket::IsBlocking(bool block) { d->IsBlocking = block; if (Bio) { Library->BIO_set_nbio(Bio, !d->IsBlocking); } } bool SslSocket::IsReadable(int TimeoutMs) { // Assign to local var to avoid a thread changing it // on us between the validity check and the select. // Which is important because a socket value of -1 // (ie invalid) will crash the FD_SET macro. OsSocket s = Handle(); if (ValidSocket(s)) { struct timeval t = {TimeoutMs / 1000, (TimeoutMs % 1000) * 1000}; fd_set r; FD_ZERO(&r); FD_SET(s, &r); int v = select(s+1, &r, 0, 0, &t); if (v > 0 && FD_ISSET(s, &r)) { return true; } else if (v < 0) { // Error(); } } else LgiTrace("%s:%i - Not a valid socket.\n", _FL); return false; } bool SslSocket::IsWritable(int TimeoutMs) { // Assign to local var to avoid a thread changing it // on us between the validity check and the select. // Which is important because a socket value of -1 // (ie invalid) will crash the FD_SET macro. OsSocket s = Handle(); if (ValidSocket(s)) { struct timeval t = {TimeoutMs / 1000, (TimeoutMs % 1000) * 1000}; fd_set w; FD_ZERO(&w); FD_SET(s, &w); int v = select(s+1, &w, 0, 0, &t); if (v > 0 && FD_ISSET(s, &w)) { return true; } else if (v < 0) { // Error(); } } else LgiTrace("%s:%i - Not a valid socket.\n", _FL); return false; } void SslSocket::OnWrite(const char *Data, ssize_t Len) { #ifdef _DEBUG if (d->RawLFCheck) { const char *End = Data + Len; while (Data < End) { LgiAssert(*Data != '\n' || d->LastWasCR); d->LastWasCR = *Data == '\r'; Data++; } } #endif // Log(Data, Len, SocketMsgSend); } void SslSocket::OnRead(char *Data, ssize_t Len) { #ifdef _DEBUG if (d->RawLFCheck) { const char *End = Data + Len; while (Data < End) { LgiAssert(*Data != '\n' || d->LastWasCR); d->LastWasCR = *Data == '\r'; Data++; } } #endif // Log(Data, Len, SocketMsgReceive); } ssize_t SslSocket::Write(const void *Data, ssize_t Len, int Flags) { LMutex::Auto Lck(&Lock, _FL); if (!Library) { DebugTrace("%s:%i - Library is NULL\n", _FL); return -1; } if (!Bio) { DebugTrace("%s:%i - BIO is NULL\n", _FL); return -1; } ssize_t r = 0; if (d->UseSSLrw) { if (Ssl) { uint64 Start = LgiCurrentTime(); int To = GetTimeout(); while (HasntTimedOut()) { r = Library->SSL_write(Ssl, Data, Len); if (r < 0) { LgiSleep(10); } else { DebugTrace("%s:%i - SSL_write(%p,%i)=%i\n", _FL, Data, Len, r); OnWrite((const char*)Data, r); break; } } if (r < 0) { DebugTrace("%s:%i - SSL_write failed (timeout=%i, %ims)\n", _FL, To, (int) (LgiCurrentTime() - Start)); } } else { r = -1; DebugTrace("%s:%i - No SSL\n", _FL); } } else { uint64 Start = LgiCurrentTime(); int To = GetTimeout(); while (HasntTimedOut()) { if (!Library) break; r = Library->BIO_write(Bio, Data, Len); DebugTrace("%s:%i - BIO_write(%p,%i)=%i\n", _FL, Data, Len, r); if (r < 0) { LgiSleep(10); } else { OnWrite((const char*)Data, r); break; } } if (r < 0) { DebugTrace("%s:%i - BIO_write failed (timeout=%i, %ims)\n", _FL, To, (int) (LgiCurrentTime() - Start)); } } if (r > 0) { GStream *l = GetLogStream(); if (l) l->Write(Data, r); } if (Ssl) { if (r < 0) { int Err = Library->SSL_get_error(Ssl, r); char Buf[256] = ""; char *e = Library->ERR_error_string(Err, Buf); DebugTrace("%s:%i - ::Write error %i, %s\n", _FL, Err, e); if (e) { OnError(Err, e); } } if (r <= 0) { DebugTrace("%s:%i - ::Write closing %i\n", _FL, r); Close(); } } return r; } ssize_t SslSocket::Read(void *Data, ssize_t Len, int Flags) { LMutex::Auto Lck(&Lock, _FL); if (!Library) return -1; if (Bio) { int r = 0; if (d->UseSSLrw) { if (Ssl) { uint64 Start = LgiCurrentTime(); int To = GetTimeout(); while (HasntTimedOut()) { r = Library->SSL_read(Ssl, Data, Len); DebugTrace("%s:%i - SSL_read(%p,%i)=%i\n", _FL, Data, Len, r); if (r < 0) LgiSleep(10); else { OnRead((char*)Data, r); break; } } } else { r = -1; } } else { uint64 Start = LgiCurrentTime(); int To = GetTimeout(); while (HasntTimedOut()) { r = Library->BIO_read(Bio, Data, Len); if (r < 0) { if (d->IsBlocking) LgiSleep(10); else break; } else { DebugTrace("%s:%i - BIO_read(%p,%i)=%i\n", _FL, Data, Len, r); OnRead((char*)Data, r); break; } } } if (r > 0) { GStream *l = GetLogStream(); if (l) l->Write(Data, r); } if (Ssl && d->IsBlocking) { if (r < 0) { int Err = Library->SSL_get_error(Ssl, r); char Buf[256]; char *e = Library->ERR_error_string(Err, Buf); if (e) { OnError(Err, e); } Close(); } if (r <= 0) { Close(); } } return r; } return -1; } void SslSocket::OnError(int ErrorCode, const char *ErrorDescription) { DebugTrace("%s:%i - OnError=%i,%s\n", _FL, ErrorCode, ErrorDescription); GString s; s.Printf("Error %i: %s\n", ErrorCode, ErrorDescription); Log(s, s.Length(), SocketMsgError); } void SslSocket::DebugTrace(const char *fmt, ...) { if (DebugLogging) { char Buffer[512]; va_list Arg; va_start(Arg, fmt); int Ch = vsprintf_s(Buffer, sizeof(Buffer), fmt, Arg); va_end(Arg); if (Ch > 0) { // LgiTrace("SSL:%p: %s", this, Buffer); OnInformation(Buffer); } } } void SslSocket::OnInformation(const char *Str) { while (Str && *Str) { GAutoString a; const char *nl = Str; while (*nl && *nl != '\n') nl++; int Len = (int) (nl - Str + 2); a.Reset(new char[Len]); char *o; for (o = a; Str < nl; Str++) { if (*Str != '\r') *o++ = *Str; } *o++ = '\n'; *o++ = 0; LgiAssert((o-a) <= Len); Log(a, -1, SocketMsgInfo); Str = *nl ? nl + 1 : nl; } } diff --git a/src/common/Widgets/Editor/GRichTextEdit.cpp b/src/common/Widgets/Editor/GRichTextEdit.cpp --- a/src/common/Widgets/Editor/GRichTextEdit.cpp +++ b/src/common/Widgets/Editor/GRichTextEdit.cpp @@ -1,2956 +1,2960 @@ #include #include #include #include "Lgi.h" #include "GRichTextEdit.h" #include "GInput.h" #include "GScrollBar.h" #ifdef WIN32 #include #endif #include "GClipBoard.h" #include "GDisplayString.h" #include "GViewPriv.h" #include "GCssTools.h" #include "GFontCache.h" #include "GUnicode.h" #include "GDropFiles.h" #include "GHtmlCommon.h" #include "GHtmlParser.h" #include "LgiRes.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 "GRichTextEditPriv.h" ////////////////////////////////////////////////////////////////////// GRichTextEdit::GRichTextEdit( int Id, int x, int y, int cx, int cy, GFontType *FontType) : ResObject(Res_Custom) { // init vars GView::d->Css.Reset(new GRichTextPriv(this, &d)); // setup window SetId(Id); SetTabStop(true); // default options #if WINNATIVE CrLf = true; SetDlgCode(DLGC_WANTALLKEYS); #else CrLf = false; #endif d->Padding(GCss::Len(GCss::LenPx, 4)); #if 0 d->BackgroundColor(GCss::ColorDef(GColour::Green)); #else d->BackgroundColor(GCss::ColorDef(GCss::ColorRgb, Rgb24To32(LC_WORKSPACE))); #endif SetFont(SysFont); #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 } GRichTextEdit::~GRichTextEdit() { // 'd' is owned by the GView CSS autoptr. } bool GRichTextEdit::SetSpellCheck(GSpellCheck *sp) { if ((d->SpellCheck = sp)) { if (IsAttached()) d->SpellCheck->EnumLanguages(AddDispatch()); // else call that OnCreate } return d->SpellCheck != NULL; } /* bool GRichTextEdit::NeedsCapability(const char *Name, const char *Param) { for (unsigned i=0; iNeedsCap.Length(); i++) { if (d->NeedsCap[i].Name.Equals(Name)) return true; } d->NeedsCap.New().Set(Name, Param); Invalidate(); return true; } void GRichTextEdit::OnInstall(CapsHash *Caps, bool Status) { OnCloseInstaller(); } void GRichTextEdit::OnCloseInstaller() { d->NeedsCap.Length(0); Invalidate(); } */ bool GRichTextEdit::IsDirty() { return d->Dirty; } void GRichTextEdit::IsDirty(bool dirty) { if (d->Dirty ^ dirty) { d->Dirty = dirty; } } void GRichTextEdit::SetFixedWidthFont(bool i) { if (FixedWidthFont ^ i) { if (i) { GFontType Type; if (Type.GetSystemFont("Fixed")) { GDocView::SetFixedWidthFont(i); } } OnFontChange(); Invalidate(); } } void GRichTextEdit::SetReadOnly(bool i) { GDocView::SetReadOnly(i); #if WINNATIVE SetDlgCode(i ? DLGC_WANTARROWS : DLGC_WANTALLKEYS); #endif } GRect GRichTextEdit::GetArea(RectType Type) { return Type >= ContentArea && Type <= MaxArea ? d->Areas[Type] : GRect(0, 0, -1, -1); } bool GRichTextEdit::ShowStyleTools() { return d->ShowTools; } void GRichTextEdit::ShowStyleTools(bool b) { if (d->ShowTools ^ b) { d->ShowTools = b; Invalidate(); } } void GRichTextEdit::SetTabSize(uint8 i) { TabSize = limit(i, 2, 32); OnFontChange(); OnPosChange(); Invalidate(); } void GRichTextEdit::SetWrapType(uint8 i) { GDocView::SetWrapType(i); OnPosChange(); Invalidate(); } GFont *GRichTextEdit::GetFont() { return d->Font; } void GRichTextEdit::SetFont(GFont *f, bool OwnIt) { if (!f) return; if (OwnIt) { d->Font.Reset(f); } else if (d->Font.Reset(new GFont)) { *d->Font = *f; d->Font->Create(NULL, 0, 0); } OnFontChange(); } void GRichTextEdit::OnFontChange() { } void GRichTextEdit::PourText(ssize_t Start, ssize_t Length /* == 0 means it's a delete */) { } void GRichTextEdit::PourStyle(ssize_t Start, ssize_t EditSize) { } bool GRichTextEdit::Insert(int At, char16 *Data, int Len) { return false; } bool GRichTextEdit::Delete(int At, int Len) { return false; } bool GRichTextEdit::DeleteSelection(char16 **Cut) { AutoTrans t(new GRichTextPriv::Transaction); if (!d->DeleteSelection(t, Cut)) return false; return d->AddTrans(t); } int64 GRichTextEdit::Value() { char *n = Name(); #ifdef _MSC_VER return (n) ? _atoi64(n) : 0; #else return (n) ? atoll(n) : 0; #endif } void GRichTextEdit::Value(int64 i) { char Str[32]; sprintf_s(Str, sizeof(Str), LGI_PrintfInt64, i); Name(Str); } bool GRichTextEdit::GetFormattedContent(const char *MimeType, GString &Out, GArray *Media) { if (!MimeType || _stricmp(MimeType, "text/html")) return false; if (!d->ToHtml(Media)) return false; Out = d->UtfNameCache; return true; } char *GRichTextEdit::Name() { d->ToHtml(); return d->UtfNameCache; } const char *GRichTextEdit::GetCharset() { return d->Charset; } void GRichTextEdit::SetCharset(const char *s) { d->Charset = s; } bool GRichTextEdit::GetVariant(const char *Name, GVariant &Value, char *Array) { GDomProperty p = LgiStringToDomProp(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 GRichTextEdit::SetVariant(const char *Name, GVariant &Value, char *Array) { GDomProperty p = LgiStringToDomProp(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 GHtmlElement *FindElement(GHtmlElement *e, HtmlTag TagId) { if (e->TagId == TagId) return e; for (unsigned i = 0; i < e->Children.Length(); i++) { GHtmlElement *c = FindElement(e->Children[i], TagId); if (c) return c; } return NULL; } void GRichTextEdit::OnAddStyle(const char *MimeType, const char *Styles) { if (d->CreationCtx) { d->CreationCtx->StyleStore.Parse(Styles); } } bool GRichTextEdit::Name(const char *s) { d->Empty(); d->OriginalText = s; GHtmlElement Root(NULL); if (!d->CreationCtx.Reset(new GRichTextPriv::CreateContext(d))) return false; if (!d->GHtmlParser::Parse(&Root, s)) return d->Error(_FL, "Failed to parse HTML."); GHtmlElement *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++) { GRichTextPriv::Block *b = d->Blocks[i]; if (b->Length() == 0) { d->Blocks.DeleteAt(i--, true); DeleteObj(b); } } } if (Status) SetCursor(0, false); Invalidate(); return Status; } char16 *GRichTextEdit::NameW() { d->WideNameCache.Reset(Utf8ToWide(Name())); return d->WideNameCache; } bool GRichTextEdit::NameW(const char16 *s) { GAutoString a(WideToUtf8(s)); return Name(a); } char *GRichTextEdit::GetSelection() { if (!HasSelection()) return NULL; GArray Text; if (!d->GetSelection(Text)) return NULL; return WideToUtf8(&Text[0]); } bool GRichTextEdit::HasSelection() { return d->Selection.Get() != NULL; } void GRichTextEdit::SelectAll() { AutoCursor Start(new BlkCursor(d->Blocks.First(), 0, 0)); d->SetCursor(Start); GRichTextPriv::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 GRichTextEdit::UnSelectAll() { bool Update = HasSelection(); if (Update) { d->Selection.Reset(); Invalidate(); } } void GRichTextEdit::SetStylePrefix(GString s) { d->SetPrefix(s); } int GRichTextEdit::GetLines() { uint32 Count = 0; for (unsigned i=0; iBlocks.Length(); i++) { GRichTextPriv::Block *b = d->Blocks[i]; Count += b->GetLines(); } return Count; } int GRichTextEdit::GetLine() { if (!d->Cursor) return -1; ssize_t Idx = d->Blocks.IndexOf(d->Cursor->Blk); if (Idx < 0) { LgiAssert(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 { GArray BlockLine; if (d->Cursor->Blk->OffsetToLine(d->Cursor->Offset, NULL, &BlockLine)) Count += BlockLine.First(); else { // Hmmm... LgiAssert(!"Can't find block line."); return -1; } } return Count; } void GRichTextEdit::SetLine(int i) { int Count = 0; // Count lines in blocks before the cursor... for (int i=0; i<(int)d->Blocks.Length(); i++) { GRichTextPriv::Block *b = d->Blocks[i]; int Lines = b->GetLines(); if (i >= Count && i < Count + Lines) { int BlockLine = i - Count; int Offset = b->LineToOffset(BlockLine); if (Offset >= 0) { AutoCursor c(new BlkCursor(b, Offset, BlockLine)); d->SetCursor(c); break; } } Count += Lines; } } void GRichTextEdit::GetTextExtent(int &x, int &y) { x = d->DocumentExtent.x; y = d->DocumentExtent.y; } bool GRichTextEdit::GetLineColumnAtIndex(GdcPt2 &Pt, int Index) { ssize_t Offset = -1; int BlockLines = -1; GRichTextPriv::Block *b = d->GetBlockByIndex(Index, &Offset, NULL, &BlockLines); if (!b) return false; int Cols; GArray Lines; if (b->OffsetToLine(Offset, &Cols, &Lines)) return false; Pt.x = Cols; Pt.y = BlockLines + Lines.First(); return true; } ssize_t GRichTextEdit::GetCaret(bool Cur) { if (!d->Cursor) return -1; int CharPos = 0; for (unsigned i=0; iBlocks.Length(); i++) { GRichTextPriv::Block *b = d->Blocks[i]; if (d->Cursor->Blk == b) return CharPos + d->Cursor->Offset; CharPos += b->Length(); } LgiAssert(!"Cursor block not found."); return -1; } bool GRichTextEdit::IndexAt(int x, int y, ssize_t &Off, int &LineHint) { GdcPt2 Doc = d->ScreenToDoc(x, y); Off = d->HitTest(Doc.x, Doc.y, LineHint); return Off >= 0; } ssize_t GRichTextEdit::IndexAt(int x, int y) { ssize_t Idx; int Line; if (!IndexAt(x, y, Idx, Line)) return -1; return Idx; } void GRichTextEdit::SetCursor(int i, bool Select, bool ForceFullUpdate) { ssize_t Offset = -1; GRichTextPriv::Block *Blk = d->GetBlockByIndex(i, &Offset); if (Blk) { AutoCursor c(new BlkCursor(Blk, Offset, -1)); if (c) d->SetCursor(c, Select); } } bool GRichTextEdit::Cut() { if (!HasSelection()) return false; char16 *Txt = NULL; if (!DeleteSelection(&Txt)) return false; bool Status = true; if (Txt) { GClipBoard Cb(this); Status = Cb.TextW(Txt); DeleteArray(Txt); } SendNotify(GNotifyDocChanged); return Status; } bool GRichTextEdit::Copy() { if (!HasSelection()) return false; GArray Text; if (!d->GetSelection(Text)) return false; // Put on the clipboard GClipBoard Cb(this); return Cb.TextW(&Text[0]); } bool GRichTextEdit::Paste() { GClipBoard Cb(this); GAutoWString Text(NewStrW(Cb.TextW())); GAutoPtr Img; if (!Text) Img.Reset(Cb.Bitmap()); if (!Text && !Img) return false; if (!d->Cursor || !d->Cursor->Blk) { LgiAssert(0); return false; } AutoTrans Trans(new GRichTextPriv::Transaction); if (HasSelection()) { if (!d->DeleteSelection(Trans, NULL)) return false; } if (Text) { GAutoPtr Utf32((uint32*)LgiNewConvertCp("utf-32", Text, LGI_WideCharset)); ptrdiff_t Len = Strlen(Utf32.Get()); if (!d->Cursor->Blk->AddText(Trans, d->Cursor->Offset, Utf32.Get(), (int)Len)) { LgiAssert(0); return false; } d->Cursor->Offset += Len; d->Cursor->LineHint = -1; } else if (Img) { GRichTextPriv::Block *b = d->Cursor->Blk; ssize_t BlkIdx = d->Blocks.IndexOf(b); GRichTextPriv::Block *After = NULL; ssize_t AddIndex; LgiAssert(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; } GRichTextPriv::ImageBlock *ImgBlk = new GRichTextPriv::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(GNotifyDocChanged); return d->AddTrans(Trans); } bool GRichTextEdit::ClearDirty(bool Ask, char *FileName) { if (1 /*dirty*/) { int Answer = (Ask) ? LgiMsg(this, LgiLoadString(L_TEXTCTRL_ASK_SAVE, "Do you want to save your changes to this document?"), LgiLoadString(L_TEXTCTRL_SAVE, "Save"), MB_YESNOCANCEL) : IDYES; if (Answer == IDYES) { GFileSelect Select; Select.Parent(this); if (!FileName && Select.Save()) { FileName = Select.Name(); } Save(FileName); } else if (Answer == IDCANCEL) { return false; } } return true; } bool GRichTextEdit::Open(const char *Name, const char *CharSet) { bool Status = false; GFile 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 GRichTextEdit::Save(const char *FileName, const char *CharSet) { GFile f; if (!FileName || !f.Open(FileName, O_WRITE)) return false; f.SetSize(0); char *Nm = Name(); if (!Nm) return false; size_t Len = strlen(Nm); return f.Write(Nm, (int)Len) == Len; } void GRichTextEdit::UpdateScrollBars(bool Reset) { if (VScroll) { //GRect Before = GetClient(); } } bool GRichTextEdit::DoCase(bool Upper) { if (!HasSelection()) return false; bool Cf = d->CursorFirst(); GRichTextPriv::BlockCursor *Start = Cf ? d->Cursor : d->Selection; GRichTextPriv::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(); ) { GRichTextPriv::Block *b = d->Blocks[i]; b->DoCase(NoTransaction, 0, -1, Upper); } } else { LgiAssert(0); return false; } // 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(); return true; } bool GRichTextEdit::DoGoto() { GInput Dlg(this, "", LgiLoadString(L_TEXTCTRL_GOTO_LINE, "Goto line:"), "Text"); if (Dlg.DoModal() == IDOK && Dlg.Str) { SetLine(atoi(Dlg.Str)); } return true; } GDocFindReplaceParams *GRichTextEdit::CreateFindReplaceParams() { return new GDocFindReplaceParams3; } void GRichTextEdit::SetFindReplaceParams(GDocFindReplaceParams *Params) { if (Params) { } } bool GRichTextEdit::DoFindNext() { return false; } bool RichText_FindCallback(GFindReplaceCommon *Dlg, bool Replace, void *User) { return ((GRichTextEdit*)User)->OnFind(Dlg); } ////////////////////////////////////////////////////////////////////////////////// FIND bool GRichTextEdit::DoFind() { GArray Sel; if (HasSelection()) d->GetSelection(Sel); GAutoString u(Sel.Length() ? WideToUtf8(&Sel.First()) : NULL); GFindDlg Dlg(this, u, RichText_FindCallback, this); Dlg.DoModal(); Focus(true); return false; } bool GRichTextEdit::OnFind(GFindReplaceCommon *Params) { if (!Params || !d->Cursor) { LgiAssert(0); return false; } GAutoPtr w((uint32*)LgiNewConvertCp("utf-32", Params->Find, "utf-8", Params->Find.Length())); ssize_t Idx = d->Blocks.IndexOf(d->Cursor->Blk); if (Idx < 0) { LgiAssert(0); return false; } for (unsigned n = 0; n < d->Blocks.Length(); n++) { ssize_t i = Idx + n; GRichTextPriv::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 bool GRichTextEdit::DoReplace() { return false; } bool GRichTextEdit::OnReplace(GFindReplaceCommon *Params) { return false; } ////////////////////////////////////////////////////////////////////////////////// void GRichTextEdit::SelectWord(size_t From) { int BlockIdx; ssize_t Start, End; GRichTextPriv::Block *b = d->GetBlockByIndex(From, &Start, &BlockIdx); if (!b) return; GArray 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 GRichTextEdit::OnMultiLineTab(bool In) { return false; } void GRichTextEdit::OnSetHidden(int Hidden) { } void GRichTextEdit::OnPosChange() { static bool Processing = false; if (!Processing) { Processing = true; GLayout::OnPosChange(); // GRect c = GetClient(); Processing = false; } } int GRichTextEdit::WillAccept(List &Formats, GdcPt2 Pt, int KeyState) { const char *Fd = LGI_FileDropFormat; for (char *s = Formats.First(); s; ) { if (!_stricmp(s, Fd) || !_stricmp(s, "UniformResourceLocatorW")) { s = Formats.Next(); } else { // LgiTrace("Ignoring format '%s'\n", s); Formats.Delete(s); DeleteArray(s); s = Formats.Current(); } } return Formats.Length() ? DROPEFFECT_COPY : DROPEFFECT_NONE; } int GRichTextEdit::OnDrop(GArray &Data, GdcPt2 Pt, int KeyState) { int Effect = DROPEFFECT_NONE; for (unsigned i=0; iAreas[ContentArea].Overlap(Pt.x, Pt.y)) { int AddIndex = -1; GdcPt2 TestPt( Pt.x - d->Areas[ContentArea].x1, Pt.y - d->Areas[ContentArea].y1); GDropFiles Df(dd); for (unsigned n=0; nHitTest(TestPt.x, TestPt.y, LineHint); if (Idx >= 0) { ssize_t BlkOffset; int BlkIdx; GRichTextPriv::Block *b = d->GetBlockByIndex(Idx, &BlkOffset, &BlkIdx); if (b) { GRichTextPriv::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; } GRichTextPriv::ImageBlock *ImgBlk = new GRichTextPriv::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(GNotifyDocChanged); } return Effect; } void GRichTextEdit::OnCreate() { SetWindow(this); DropTarget(true); if (Focus()) SetPulse(RTE_PULSE_RATE); if (d->SpellCheck) d->SpellCheck->EnumLanguages(AddDispatch()); } void GRichTextEdit::OnEscape(GKey &K) { } bool GRichTextEdit::OnMouseWheel(double l) { if (VScroll) { VScroll->Value(VScroll->Value() + (int64)l); Invalidate(); } return true; } void GRichTextEdit::OnFocus(bool f) { Invalidate(); SetPulse(f ? RTE_PULSE_RATE : -1); } ssize_t GRichTextEdit::HitTest(int x, int y) { int Line = -1; return d->HitTest(x, y, Line); } void GRichTextEdit::Undo() { if (d->UndoPos > 0) d->SetUndoPos(d->UndoPos - 1); } void GRichTextEdit::Redo() { if (d->UndoPos < (int)d->UndoQue.Length()) d->SetUndoPos(d->UndoPos + 1); } #ifdef _DEBUG class NodeView : public GWindow { public: GTree *Tree; NodeView(GViewI *w) { GRect r(0, 0, 500, 600); SetPos(r); MoveSameScreen(w); Attach(0); if ((Tree = new GTree(100, 0, 0, 100, 100))) { Tree->SetPourLargest(true); Tree->Attach(this); } } }; #endif void GRichTextEdit::DoContextMenu(GMouse &m) { GMenuItem *i; GSubMenu RClick; GAutoString ClipText; { GClipBoard Clip(this); ClipText.Reset(NewStr(Clip.Text())); } GRichTextPriv::Block *Over = NULL; GRect &Content = d->Areas[ContentArea]; GdcPt2 Doc = d->ScreenToDoc(m.x, m.y); // int BlockIndex = -1; ssize_t Offset = -1; if (Content.Overlap(m.x, m.y)) { int LineHint; Offset = d->HitTest(Doc.x, Doc.y, LineHint, &Over); } if (Over) Over->DoContext(RClick, Doc, Offset, true); RClick.AppendItem(LgiLoadString(L_TEXTCTRL_CUT, "Cut"), IDM_RTE_CUT, HasSelection()); RClick.AppendItem(LgiLoadString(L_TEXTCTRL_COPY, "Copy"), IDM_RTE_COPY, HasSelection()); RClick.AppendItem(LgiLoadString(L_TEXTCTRL_PASTE, "Paste"), IDM_RTE_PASTE, ClipText != 0); RClick.AppendSeparator(); RClick.AppendItem(LgiLoadString(L_TEXTCTRL_UNDO, "Undo"), IDM_RTE_UNDO, false /* UndoQue.CanUndo() */); RClick.AppendItem(LgiLoadString(L_TEXTCTRL_REDO, "Redo"), IDM_RTE_REDO, false /* UndoQue.CanRedo() */); RClick.AppendSeparator(); #if 0 i = RClick.AppendItem(LgiLoadString(L_TEXTCTRL_FIXED, "Fixed Width Font"), IDM_FIXED, true); if (i) i->Checked(GetFixedWidthFont()); #endif i = RClick.AppendItem(LgiLoadString(L_TEXTCTRL_AUTO_INDENT, "Auto Indent"), IDM_AUTO_INDENT, true); if (i) i->Checked(AutoIndent); i = RClick.AppendItem(LgiLoadString(L_TEXTCTRL_SHOW_WHITESPACE, "Show Whitespace"), IDM_SHOW_WHITE, true); if (i) i->Checked(ShowWhiteSpace); i = RClick.AppendItem(LgiLoadString(L_TEXTCTRL_HARD_TABS, "Hard Tabs"), IDM_HARD_TABS, true); if (i) i->Checked(HardTabs); RClick.AppendItem(LgiLoadString(L_TEXTCTRL_INDENT_SIZE, "Indent Size"), IDM_INDENT_SIZE, true); RClick.AppendItem(LgiLoadString(L_TEXTCTRL_TAB_SIZE, "Tab Size"), IDM_TAB_SIZE, true); GSubMenu *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, Offset, false); } if (Environment) Environment->AppendItems(&RClick); int Id = 0; m.ToScreen(); switch (Id = RClick.Float(this, m.x, m.y)) { case IDM_FIXED: { SetFixedWidthFont(!GetFixedWidthFont()); SendNotify(GNotifyFixedWidthChanged); 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); GInput i(this, s, "Indent Size:", "Text"); if (i.DoModal()) { IndentSize = atoi(i.Str); } break; } case IDM_TAB_SIZE: { char s[32]; sprintf_s(s, sizeof(s), "%i", TabSize); GInput i(this, s, "Tab Size:", "Text"); if (i.DoModal()) { SetTabSize(atoi(i.Str)); } break; } case IDM_COPY_ORIGINAL: { GClipBoard c(this); c.Text(d->OriginalText); break; } case IDM_COPY_CURRENT: { GClipBoard 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) { GMessage Cmd(M_COMMAND, Id); - Over->OnEvent(&Cmd); + if (Over->OnEvent(&Cmd)) + break; } if (Environment) { Environment->OnMenu(this, Id, 0); } break; } } } void GRichTextEdit::OnMouseClick(GMouse &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)); GdcPt2 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 GRichTextEdit::OnHitTest(int x, int y) { #ifdef WIN32 if (GetClient().Overlap(x, y)) { return HTCLIENT; } #endif return GView::OnHitTest(x, y); } void GRichTextEdit::OnMouseMove(GMouse &m) { GRichTextEdit::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; GdcPt2 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... GArray 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... GArray 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 GRect c = GetClient(); c.Offset(-c.x1, -c.y1); if (c.Overlap(m.x, m.y)) { /* GStyle *s = HitStyle(Hit); TCHAR *c = (s) ? s->GetCursor() : 0; if (!c) c = IDC_IBEAM; ::SetCursor(LoadCursor(0, MAKEINTRESOURCE(c))); */ } #endif } bool GRichTextEdit::OnKey(GKey &k) { if (k.Down() && d->Cursor) d->Cursor->Blink = true; // k.Trace("GRichTextEdit::OnKey"); if (k.IsContextMenu()) { GMouse m; DoContextMenu(m); } else if (k.IsChar) { switch (k.c16) { default: { // process single char input if ( !GetReadOnly() && ( (k.c16 >= ' ' || k.c16 == VK_TAB) && k.c16 != 127 ) ) { if (k.Down() && d->Cursor && d->Cursor->Blk) { // letter/number etc GRichTextPriv::Block *b = d->Cursor->Blk; GNamedStyle *AddStyle = NULL; if (d->StyleDirty.Length() > 0) { GAutoPtr Mod(new GCss); if (Mod) { // Get base styles at the cursor.. GNamedStyle *Base = b->GetStyle(d->Cursor->Offset); if (Base && Mod) *Mod = *Base; // Apply dirty toolbar styles... if (d->StyleDirty.HasItem(FontFamilyBtn)) Mod->FontFamily(GCss::StringsDef(d->Values[FontFamilyBtn].Str())); if (d->StyleDirty.HasItem(FontSizeBtn)) Mod->FontSize(GCss::Len(GCss::LenPt, (float) d->Values[FontSizeBtn].CastDouble())); if (d->StyleDirty.HasItem(BoldBtn)) Mod->FontWeight(d->Values[BoldBtn].CastInt32() ? GCss::FontWeightBold : GCss::FontWeightNormal); if (d->StyleDirty.HasItem(ItalicBtn)) Mod->FontStyle(d->Values[ItalicBtn].CastInt32() ? GCss::FontStyleItalic : GCss::FontStyleNormal); if (d->StyleDirty.HasItem(UnderlineBtn)) Mod->TextDecoration(d->Values[UnderlineBtn].CastInt32() ? GCss::TextDecorUnderline : GCss::TextDecorNone); if (d->StyleDirty.HasItem(ForegroundColourBtn)) Mod->Color(GCss::ColorDef(GCss::ColorRgb, (uint32)d->Values[ForegroundColourBtn].CastInt64())); if (d->StyleDirty.HasItem(BackgroundColourBtn)) Mod->BackgroundColor(GCss::ColorDef(GCss::ColorRgb, (uint32)d->Values[BackgroundColourBtn].CastInt64())); AddStyle = d->AddStyleToCache(Mod); } d->StyleDirty.Length(0); } AutoTrans Trans(new GRichTextPriv::Transaction); d->DeleteSelection(Trans, NULL); uint32 Ch = k.c16; if (b->AddText(Trans, d->Cursor->Offset, &Ch, 1, AddStyle)) { d->Cursor->Set(d->Cursor->Offset + 1); Invalidate(); SendNotify(GNotifyDocChanged); d->AddTrans(Trans); } } return true; } break; } case VK_RETURN: { if (GetReadOnly()) break; if (k.Down() && k.IsChar) OnEnter(k); return true; } case VK_BACKSPACE: { if (GetReadOnly()) break; bool Changed = false; AutoTrans Trans(new GRichTextPriv::Transaction); if (k.Ctrl()) { // Ctrl+H } else if (k.Down()) { GRichTextPriv::Block *b; if (HasSelection()) { 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... GRichTextPriv::Block *n = d->Next(b); if (n) { d->Blocks.Delete(b, true); d->Cursor.Reset(new GRichTextPriv::BlockCursor(n, 0, 0)); } } else { d->Cursor->Set(d->Cursor->Offset - 1); } } } else { // At the start of a block: GRichTextPriv::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(GNotifyDocChanged); } return true; } } } else // not a char { switch (k.vkey) { case VK_TAB: return true; case VK_RETURN: return !GetReadOnly(); case VK_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 LgiAssert(!"Impl backspace by word"); } } return true; } break; } case VK_F3: { if (k.Down()) DoFindNext(); return true; } case VK_LEFT: { if (k.Alt()) return false; if (k.Down()) { if (HasSelection() && !k.Shift()) { GRect 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, k.Ctrl() ? GRichTextPriv::SkLeftWord : GRichTextPriv::SkLeftChar, k.Shift()); } } return true; } case VK_RIGHT: { if (k.Alt()) return false; if (k.Down()) { if (HasSelection() && !k.Shift()) { GRect 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, k.Ctrl() ? GRichTextPriv::SkRightWord : GRichTextPriv::SkRightChar, k.Shift()); } } return true; } case VK_UP: { if (k.Alt()) return false; if (k.Down()) { #ifdef MAC if (k.Ctrl()) goto GTextView4_PageUp; #endif d->Seek(d->Cursor, GRichTextPriv::SkUpLine, k.Shift()); } return true; } case VK_DOWN: { if (k.Alt()) return false; if (k.Down()) { #ifdef MAC if (k.Ctrl()) goto GTextView4_PageDown; #endif d->Seek(d->Cursor, GRichTextPriv::SkDownLine, k.Shift()); } return true; } case VK_END: { if (k.Down()) { #ifdef MAC if (!k.Ctrl()) Jump_EndOfLine: #endif d->Seek(d->Cursor, k.Ctrl() ? GRichTextPriv::SkDocEnd : GRichTextPriv::SkLineEnd, k.Shift()); } return true; } case VK_HOME: { if (k.Down()) { #ifdef MAC if (!k.Ctrl()) Jump_StartOfLine: #endif d->Seek(d->Cursor, k.Ctrl() ? GRichTextPriv::SkDocStart : GRichTextPriv::SkLineStart, k.Shift()); } return true; } case VK_PAGEUP: { #ifdef MAC GTextView4_PageUp: #endif if (k.Down()) { d->Seek(d->Cursor, GRichTextPriv::SkUpPage, k.Shift()); } return true; break; } case VK_PAGEDOWN: { #ifdef MAC GTextView4_PageDown: #endif if (k.Down()) { d->Seek(d->Cursor, GRichTextPriv::SkDownPage, k.Shift()); } return true; break; } case VK_INSERT: { if (k.Down()) { if (k.Ctrl()) { Copy(); } else if (k.Shift()) { if (!GetReadOnly()) { Paste(); } } } return true; break; } case VK_DELETE: { if (GetReadOnly()) break; if (!k.Down()) return true; bool Changed = false; GRichTextPriv::Block *b; AutoTrans Trans(new GRichTextPriv::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. GRichTextPriv::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 GRichTextPriv::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 GRichTextPriv::BlockCursor(b = next, 0, 0)); } } if (!Changed && b->DeleteAt(Trans, d->Cursor->Offset, 1)) { if (b->Length() == 0) { GRichTextPriv::Block *n = d->Next(b); if (n) { d->Blocks.Delete(b, true); d->Cursor.Reset(new GRichTextPriv::BlockCursor(n, 0, 0)); } } Changed = true; } } if (Changed) { Invalidate(); d->AddTrans(Trans); SendNotify(GNotifyDocChanged); } 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 GRichTextPriv::Block *b = d->Cursor->Blk; uint32 Nbsp[] = {0xa0}; if (b->AddText(NoTransaction, d->Cursor->Offset, Nbsp, 1)) { d->Cursor->Set(d->Cursor->Offset + 1); Invalidate(); SendNotify(GNotifyDocChanged); } } break; } if (k.Modifier() && !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 GMouse m; GetMouse(m); d->ClickBtn(m, BoldBtn); } return true; break; } case 'i': case 'I': { if (k.Down()) { // Italic selection GMouse 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(); return true; } case 'g': case 'G': { if (k.Down()) { DoGoto(); } return true; break; } case 'h': case 'H': { if (k.Down()) { DoReplace(); } return true; break; } case 'u': case 'U': { if (!GetReadOnly()) { if (k.Down()) { DoCase(k.Shift()); } return true; } break; } case VK_RETURN: { if (!GetReadOnly() && !k.Shift()) { if (k.Down()) { OnEnter(k); } return true; } break; } } } break; } } } return false; } void GRichTextEdit::OnEnter(GKey &k) { AutoTrans Trans(new GRichTextPriv::Transaction); // Enter key handling bool Changed = false; if (HasSelection()) Changed |= d->DeleteSelection(Trans, NULL); if (d->Cursor && d->Cursor->Blk) { GRichTextPriv::Block *b = d->Cursor->Blk; const uint32 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) { GRichTextPriv::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: { GRichTextPriv::TextBlock *tb = new GRichTextPriv::TextBlock(d); if (tb) { Changed = true; // tb->AddText(Trans, 0, Nl, 1); d->Blocks.AddAt(0, tb); } } } else if (d->Cursor->Offset == b->Length()) { GRichTextPriv::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: { GRichTextPriv::TextBlock *tb = new GRichTextPriv::TextBlock(d); if (tb) { Changed = true; // tb->AddText(Trans, 0, Nl, 1); d->Blocks.Add(tb); } } } } } if (Changed) { Invalidate(); d->AddTrans(Trans); SendNotify(GNotifyDocChanged); } } void GRichTextEdit::OnPaintLeftMargin(GSurface *pDC, GRect &r, GColour &colour) { pDC->Colour(colour); pDC->Rectangle(&r); } void GRichTextEdit::OnPaint(GSurface *pDC) { GRect r = GetClient(); if (!r.Valid()) return; #if 0 pDC->Colour(GColour(255, 0, 255)); pDC->Rectangle(); #endif int FontY = GetFont()->GetHeight(); GCssTools ct(d, d->Font); r = ct.PaintBorder(pDC, r); bool HasSpace = r.Y() > (FontY * 3); /* if (d->NeedsCap.Length() > 0 && HasSpace) { d->Areas[CapabilityArea] = r; d->Areas[CapabilityArea].y2 = d->Areas[CapabilityArea].y1 + 4 + ((FontY + 4) * (int)d->NeedsCap.Length()); r.y1 = d->Areas[CapabilityArea].y2 + 1; d->Areas[CapabilityBtn] = d->Areas[CapabilityArea]; d->Areas[CapabilityBtn].Size(2, 2); d->Areas[CapabilityBtn].x1 = d->Areas[CapabilityBtn].x2 - 30; } else { d->Areas[CapabilityArea].ZOff(-1, -1); d->Areas[CapabilityBtn].ZOff(-1, -1); } */ 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 0 CGAffineTransform t1 = CGContextGetCTM(pDC->Handle()); CGRect rc = CGContextGetClipBoundingBox(pDC->Handle()); LgiTrace("d->Areas[ContentArea]=%s %f,%f,%f,%f\n", d->Areas[ContentArea].GetStr(), rc.origin.x, rc.origin.y, rc.size.width, rc.size.height); if (rc.size.width < 20) { int asd=0; } #endif if (d->Layout(VScroll)) d->Paint(pDC, VScroll); // else the scroll bars changed, wait for re-paint } GMessage::Result GRichTextEdit::OnEvent(GMessage *Msg) { switch (MsgCode(Msg)) { case M_CUT: { Cut(); break; } case M_COPY: { Copy(); break; } case M_PASTE: { Paste(); break; } case M_BLOCK_MSG: { GRichTextPriv::Block *b = (GRichTextPriv::Block*)Msg->A(); GAutoPtr msg((GMessage*)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: { GAutoPtr< GArray > Languages((GArray*)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 (unsigned i=0; iLength(); i++) { GSpellCheck::LanguageId &s = (*Languages)[i]; 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: { GAutoPtr< GArray > Dictionaries((GArray*)Msg->A()); if (!Dictionaries) break; bool Match = false; for (unsigned i=0; iLength(); i++) { GSpellCheck::DictionaryId &s = (*Dictionaries)[i]; if (s.Dict.Equals(d->SpellDict)) { // LgiTrace("%s:%i - M_ENUMERATE_DICTIONARIES: %s, %s\n", _FL, s.Dict.Get(), d->SpellDict.Get()); d->SpellCheck->SetDictionary(AddDispatch(), s.Lang, s.Dict); Match = true; break; } } if (!Match) LgiTrace("%s:%i - No match in M_ENUMERATE_DICTIONARIES: %s\n", _FL, d->SpellDict.Get()); 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 GRichTextPriv::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: { GAutoPtr Ct((GSpellCheck::CheckText*)Msg->A()); - if (!Ct) + if (!Ct || Ct->User.Length() > 1) + { + LgiAssert(0); break; + } - GRichTextPriv::Block *b = (GRichTextPriv::Block*)Ct->UserPtr; + GRichTextPriv::Block *b = (GRichTextPriv::Block*)Ct->User[SpellBlockPtr].CastVoidPtr(); if (!d->Blocks.HasItem(b)) break; - b->SetSpellingErrors(Ct->Errors); + b->SetSpellingErrors(Ct->Errors, *Ct); Invalidate(); break; } #if defined WIN32 case WM_GETTEXTLENGTH: { return 0 /*Size*/; } case WM_GETTEXT: { int Chars = (int)MsgA(Msg); char *Out = (char*)MsgB(Msg); if (Out) { char *In = (char*)LgiNewConvertCp(LgiAnsiToLgiCp(), 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: { GAutoPtr Comp((GString*)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*)LgiNewConvertCp(LGI_WideCharset, Buf, LgiAnsiToLgiCp(), Size); if (Utf) { Insert(Cursor, Utf, StrlenW(Utf)); DeleteArray(Utf); } DeleteArray(Buf); } ImmReleaseContext(Handle(), hIMC); } return 0; } break; } */ #endif } return GLayout::OnEvent(Msg); } int GRichTextEdit::OnNotify(GViewI *Ctrl, int Flags) { if (Ctrl->GetId() == IDC_VSCROLL && VScroll) { Invalidate(d->Areas + ContentArea); } return 0; } void GRichTextEdit::OnPulse() { if (!ReadOnly && d->Cursor) { uint64 n = LgiCurrentTime(); 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 == GRichTextEdit::ContentArea) { GMouse m; GetMouse(m); // Is the mouse outside the content window GRect &r = d->Areas[ContentArea]; if (!r.Overlap(m.x, m.y)) { AutoCursor c(new BlkCursor(NULL, 0, 0)); GdcPt2 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 GRichTextEdit::OnUrl(char *Url) { if (Environment) { Environment->OnNavigate(this, Url); } } bool GRichTextEdit::OnLayout(GViewLayoutInfo &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 GRichTextEdit::DumpNodes(GTree *Root) { d->DumpNodes(Root); } #endif /////////////////////////////////////////////////////////////////////////////// SelectColour::SelectColour(GRichTextPriv *priv, GdcPt2 p, GRichTextEdit::RectType t) : GPopup(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++) { GColour 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); GRect r(0, 0, 12 + (8 * PxSp) - 1, y + 6 - 1); r.Offset(p.x, p.y); SetPos(r); Visible(true); } void SelectColour::OnPaint(GSurface *pDC) { pDC->Colour(LC_MED, 24); pDC->Rectangle(); for (unsigned i=0; iColour(e[i].c); pDC->Rectangle(&e[i].r); } } void SelectColour::OnMouseClick(GMouse &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) { GPopup::Visible(i); if (!i) { d->View->Focus(true); delete this; } } /////////////////////////////////////////////////////////////////////////////// #define EMOJI_PAD 2 #include "Emoji.h" int EmojiMenu::Cur = 0; EmojiMenu::EmojiMenu(GRichTextPriv *priv, GdcPt2 p) : GPopup(priv->View) { d = priv; d->GetEmojiImage(); int MaxIdx = 0; GRange EmojiBlocks[2] = { GRange(0x203c, 0x3299 - 0x203c + 1), GRange(0x1f004, 0x1f6c5 - 0x1f004 + 1) }; GHashTbl Map; for (int b=0; b= 0) { Map.Add(Idx, u); MaxIdx = MAX(MaxIdx, Idx); } } } int Sz = EMOJI_CELL_SIZE - 1; int PaneCount = 5; int PaneSz = Map.Length() / PaneCount; int ImgIdx = 0; int PaneSelectSz = SysFont->GetHeight() * 2; int Rows = (PaneSz + EMOJI_GROUP_X - 1) / EMOJI_GROUP_X; GRect 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 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(GSurface *pDC) { GAutoPtr DblBuf; if (!pDC->SupportsAlphaCompositing()) DblBuf.Reset(new GDoubleBuffer(pDC)); pDC->Colour(LC_MED, 24); pDC->Rectangle(); GSurface *EmojiImg = d->GetEmojiImage(); if (EmojiImg) { pDC->Op(GDC_ALPHA); for (unsigned i=0; iColour(LC_LIGHT, 24); pDC->Rectangle(&p.Btn); } SysFont->Fore(LC_TEXT); SysFont->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 { GRect c = GetClient(); GDisplayString Ds(SysFont, "Loading..."); SysFont->Colour(LC_TEXT, LC_MED); SysFont->Transparent(true); Ds.Draw(pDC, (c.X()-Ds.X())>>1, (c.Y()-Ds.Y())>>1); } } bool EmojiMenu::InsertEmoji(uint32 Ch) { if (!d->Cursor || !d->Cursor->Blk) return false; AutoTrans Trans(new GRichTextPriv::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(GNotifyDocChanged); return true; } void EmojiMenu::OnMouseClick(GMouse &m) { if (m.Down()) { for (unsigned i=0; iView->Focus(true); delete this; } } /////////////////////////////////////////////////////////////////////////////// class GRichTextEdit_Factory : public GViewFactory { GView *NewView(const char *Class, GRect *Pos, const char *Text) { if (_stricmp(Class, "GRichTextEdit") == 0) { return new GRichTextEdit(-1, 0, 0, 2000, 2000); } return 0; } } RichTextEdit_Factory; diff --git a/src/common/Widgets/Editor/GRichTextEditPriv.cpp b/src/common/Widgets/Editor/GRichTextEditPriv.cpp --- a/src/common/Widgets/Editor/GRichTextEditPriv.cpp +++ b/src/common/Widgets/Editor/GRichTextEditPriv.cpp @@ -1,2356 +1,2357 @@ #include "Lgi.h" #include "GRichTextEdit.h" #include "GRichTextEditPriv.h" #include "GScrollBar.h" #include "GCssTools.h" /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool Utf16to32(GArray &Out, const uint16 *In, int Len) { if (Len == 0) { Out.Length(0); return true; } // Count the length of utf32 chars... const uint16 *Ptr = In; ssize_t Bytes = sizeof(*In) * Len; 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) * Len; uint32 *o = &Out[0]; uint32 *e = o + Out.Length(); while ( Bytes >= sizeof(*In)) { *o++ = LgiUtf16To32(Ptr, Bytes); } LgiAssert(o == e); return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// const char *GRichEditElemContext::GetElement(GRichEditElem *obj) { return obj->Tag; } const char *GRichEditElemContext::GetAttr(GRichEditElem *obj, const char *Attr) { const char *a = NULL; obj->Get(Attr, a); return a; } bool GRichEditElemContext::GetClasses(GString::Array &Classes, GRichEditElem *obj) { const char *c; if (!obj->Get("class", c)) return false; GString cls = c; Classes = cls.Split(" "); return Classes.Length() > 0; } GRichEditElem *GRichEditElemContext::GetParent(GRichEditElem *obj) { return dynamic_cast(obj->Parent); } GArray GRichEditElemContext::GetChildren(GRichEditElem *obj) { GArray a; for (unsigned i=0; iChildren.Length(); i++) a.Add(dynamic_cast(obj->Children[i])); return a; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// GCssCache::GCssCache() { Idx = 1; } GCssCache::~GCssCache() { Styles.DeleteObjects(); } uint32 GCssCache::GetStyles() { uint32 c = 0; for (unsigned i=0; iRefCount != 0; } return c; } void GCssCache::ZeroRefCounts() { for (unsigned i=0; iRefCount = 0; } } bool GCssCache::OutputStyles(GStream &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()); GAutoString a = ns->ToString(); GString all = a.Get(); GString::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++); *(GCss*)ns = *s.Get(); Styles.Add(ns); #if 0 // _DEBUG GAutoString ss = ns->ToString(); if (ss) LgiTrace("%s = %s\n", ns->Name.Get(), ss.Get()); #endif } return ns; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// BlockCursorState::BlockCursorState(bool cursor, GRichTextPriv::BlockCursor *c) { Cursor = cursor; Offset = c->Offset; LineHint = c->LineHint; BlockUid = c->Blk->GetUid(); } bool BlockCursorState::Apply(GRichTextPriv *Ctx, bool Forward) { GAutoPtr &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; GRichTextPriv::Block *b; if (!Ctx->GetBlockByUid(b, BlockUid, &Index)) return false; if (b != Bc->Blk) Bc.Reset(new GRichTextPriv::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(GRichTextPriv *Ctx, GRichTextPriv::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 GRichTextPriv::TextBlock(Tb)); } } bool CompleteTextBlockState::Apply(GRichTextPriv *Ctx, bool Forward) { int Index; GRichTextPriv::TextBlock *b; if (!Ctx->GetBlockByUid(b, Uid, &Index)) return false; // Swap the local state with the block in the ctx + Blk->UpdateSpellingAndLinks(NULL, GRange(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(GRichTextPriv *ctx, ssize_t Start) { Ctx = ctx; Index = Start; Length = -1; } bool MultiBlockState::Apply(GRichTextPriv *Ctx, bool Forward) { if (Index < 0 || Length < 0) { LgiAssert(!"Missing parameters"); return false; } // Undo: Swap 'Length' blocks Ctx->Blocks with Blks ssize_t OldLen = Blks.Length(); bool Status = Blks.SwapRange(GRange(0, OldLen), Ctx->Blocks, GRange(Index, Length)); if (Status) Length = OldLen; return Status; } bool MultiBlockState::Copy(ssize_t Idx) { if (!Ctx->Blocks.AddressOf(Idx)) return false; GRichTextPriv::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; GRichTextPriv::Block *b = Ctx->Blocks[Idx]; if (!b) return false; Blks.Add(b); return Ctx->Blocks.DeleteAt(Idx, true); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// GRichTextPriv::GRichTextPriv(GRichTextEdit *view, GRichTextPriv **Ptr) : GHtmlParser(view), GFontCache(SysFont) { if (Ptr) *Ptr = this; BlinkTs = 0; View = view; Log = &LogBuffer; NextUid = 1; UndoPos = 0; WordSelectMode = false; Dirty = false; ScrollOffsetPx = 0; ShowTools = true; ScrollChange = false; DocumentExtent.x = 0; DocumentExtent.y = 0; SpellCheck = NULL; SpellDictionaryLoaded = false; HtmlLinkAsCid = false; ScrollLinePx = SysFont->GetHeight(); if (Font.Reset(new GFont)) *Font = *SysFont; for (unsigned i=0; i 1000) { LgiTrace("%s:%i - Waiting for blocks: %i\n", _FL, (int)(Now-Start)); Msg = Now; } if (Now - Start > 10000) { LgiAssert(0); Start = Now; } } } Empty(); } bool GRichTextPriv::DeleteSelection(Transaction *Trans, char16 **Cut) { if (!Cursor || !Selection) return false; GArray DeletedText; GArray *DelTxt = Cut ? &DeletedText : NULL; bool Cf = CursorFirst(); GRichTextPriv::BlockCursor *Start = Cf ? Cursor : Selection; GRichTextPriv::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; GRichTextPriv::Block *NextBlk = Next(Start->Blk); if (Len >= Start->Blk->Length() && NextBlk) { // Delete entire block ssize_t i = Blocks.IndexOf(Start->Blk); GAutoPtr 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); GAutoPtr 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); } // 2) Delete any blocks between 'Start' and 'End' if (i >= 0) { while (Blocks[i] != End->Blk && i < (int)Blocks.Length()) { GRichTextPriv::Block *b = Blocks[i]; b->CopyAt(0, -1, DelTxt); MultiState->Cut(i); e--; } } else { LgiAssert(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); 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*)LgiNewConvertCp(LGI_WideCharset, &DelTxt->First(), "utf-32", DelTxt->Length()*sizeof(uint32)); } return true; } GRichTextPriv::Block *GRichTextPriv::Next(Block *b) { ssize_t Idx = Blocks.IndexOf(b); if (Idx < 0) return NULL; if (++Idx >= (int)Blocks.Length()) return NULL; return Blocks[Idx]; } GRichTextPriv::Block *GRichTextPriv::Prev(Block *b) { ssize_t Idx = Blocks.IndexOf(b); if (Idx <= 0) return NULL; return Blocks[--Idx]; } bool GRichTextPriv::AddTrans(GAutoPtr &t) { // Delete any transaction history after 'UndoPos' for (ssize_t i=UndoPos; i UndoPos) { // Forward in que Transaction *t = UndoQue[UndoPos]; if (!t->Apply(this, true)) return false; UndoPos++; } else if (Pos < UndoPos) { Transaction *t = UndoQue[UndoPos-1]; if (!t->Apply(this, false)) return false; UndoPos--; } else break; // We are done } Dirty = true; InvalidateDoc(NULL); return true; } bool GRichTextPriv::IsBusy(bool Stop) { for (unsigned i=0; iIsBusy(Stop)) return true; } return false; } bool GRichTextPriv::Error(const char *file, int line, const char *fmt, ...) { va_list Arg; va_start(Arg, fmt); GString s; LgiPrintf(s, fmt, Arg); va_end(Arg); GString Err; Err.Printf("%s:%i - Error: %s\n", file, line, s.Get()); Log->Write(Err, Err.Length()); Err = LogBuffer.NewGStr(); LgiTrace("%.*s", Err.Length(), Err.Get()); LgiAssert(0); return false; } void GRichTextPriv::UpdateStyleUI() { if (!Cursor || !Cursor->Blk) { Error(_FL, "Not a valid cursor."); return; } TextBlock *b = dynamic_cast(Cursor->Blk); GArray Styles; if (b) b->GetTextAt(Cursor->Offset, Styles); StyleText *st = Styles.Length() ? Styles.First() : NULL; GFont *f = NULL; if (st) f = GetFont(st->GetStyle()); else if (View) f = View->GetFont(); else if (LgiApp) f = SysFont; if (f) { Values[GRichTextEdit::FontFamilyBtn] = f->Face(); Values[GRichTextEdit::FontSizeBtn] = f->PointSize(); Values[GRichTextEdit::FontSizeBtn].CastString(); Values[GRichTextEdit::BoldBtn] = f->Bold(); Values[GRichTextEdit::ItalicBtn] = f->Italic(); Values[GRichTextEdit::UnderlineBtn] = f->Underline(); } else { Values[GRichTextEdit::FontFamilyBtn] = "(Unknown)"; } Values[GRichTextEdit::ForegroundColourBtn] = (int64) (st && st->Colours.Fore.IsValid() ? st->Colours.Fore.c32() : TextColour.c32()); Values[GRichTextEdit::BackgroundColourBtn] = (int64) (st && st->Colours.Back.IsValid() ? st->Colours.Back.c32() : 0); if (View) View->Invalidate(Areas + GRichTextEdit::ToolsArea); } void GRichTextPriv::ScrollTo(GRect r) { GRect Content = Areas[GRichTextEdit::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 GRichTextPriv::InvalidateDoc(GRect *r) { // Transform the coordinates from doc to screen space GRect &c = Areas[GRichTextEdit::ContentArea]; if (r) { GRect t = *r; t.Offset(c.x1, c.y1 - ScrollOffsetPx); View->Invalidate(&t); } else View->Invalidate(&c); } void GRichTextPriv::EmptyDoc() { Block *Def = new TextBlock(this); if (Def) { Blocks.Add(Def); Cursor.Reset(new BlockCursor(Def, 0, 0)); UpdateStyleUI(); } } void GRichTextPriv::Empty() { // Delete cursors first to avoid hanging references Cursor.Reset(); Selection.Reset(); // Clear the block list.. Blocks.DeleteObjects(); } bool GRichTextPriv::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()); GAutoPtr 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)); LgiAssert(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)); LgiAssert(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) { GArray 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) { LgiAssert(0); break; } if (Idx == 0) break; // Beginning of document Block *b = Blocks[--Idx]; if (!b) { LgiAssert(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) { GArray 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; GArray 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()) { GArray 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()) { GArray 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; GArray 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: { GRect &Content = Areas[GRichTextEdit::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: { GRect &Content = Areas[GRichTextEdit::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)) { LgiAssert(!"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 GRichTextPriv::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 GRichTextPriv::SetCursor(GAutoPtr c, bool Select) { GRect 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 GRect 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; } GRect GRichTextPriv::SelectionRect() { GRect SelRc; if (Cursor) { SelRc = Cursor->Line; if (Selection) SelRc.Union(&Selection->Line); } else if (Selection) { SelRc = Selection->Line; } return SelRc; } ssize_t GRichTextPriv::IndexOfCursor(BlockCursor *c) { if (!c || !c->Blk) { Error(_FL, "Invalid cursor param."); return -1; } int CharPos = 0; for (unsigned i=0; iBlk == b) return CharPos + c->Offset; CharPos += b->Length(); } LgiAssert(0); return -1; } GdcPt2 GRichTextPriv::ScreenToDoc(int x, int y) { GRect &Content = Areas[GRichTextEdit::ContentArea]; return GdcPt2(x - Content.x1, y - Content.y1 + ScrollOffsetPx); } GdcPt2 GRichTextPriv::DocToScreen(int x, int y) { GRect &Content = Areas[GRichTextEdit::ContentArea]; return GdcPt2(x + Content.x1, y + Content.y1 - ScrollOffsetPx); } bool GRichTextPriv::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; } GSurface *GEmojiContext::GetEmojiImage() { if (!EmojiImg) { GString p = LgiGetSystemPath(LSP_APP_INSTALL); if (!p) { LgiTrace("%s:%i - No app install path.\n", _FL); return NULL; } char File[MAX_PATH] = ""; LgiMakePath(File, sizeof(File), p, "..\\src\\common\\Text\\Emoji\\EmojiMap.png"); GAutoString a; if (!FileExists(File)) a.Reset(LgiFindFile("EmojiMap.png")); EmojiImg.Reset(GdcD->Load(a ? a : File, false)); } return EmojiImg; } ssize_t GRichTextPriv::HitTest(int x, int y, int &LineHint, Block **Blk) { int CharPos = 0; HitTestResult r(x, y); if (Blocks.Length() == 0) { Error(_FL, "No blocks."); return -1; } Block *b = Blocks.First(); GRect 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; 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 GRichTextPriv::CursorFromPos(int x, int y, GAutoPtr *Cursor, ssize_t *GlobalIdx) { int 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; } GRichTextPriv::Block *GRichTextPriv::GetBlockByIndex(ssize_t Index, ssize_t *Offset, int *BlockIdx, int *LineCount) { int 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 GRichTextPriv::Layout(GScrollBar *&ScrollY) { Flow f(this); ScrollLinePx = View->GetFont()->GetHeight(); LgiAssert(ScrollLinePx > 0); if (ScrollLinePx <= 0) ScrollLinePx = 16; GRect Client = Areas[GRichTextEdit::ContentArea]; Client.Offset(-Client.x1, -Client.y1); DocumentExtent.x = Client.X(); GCssTools Ct(this, Font); GRect 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->SetLimits(0, Lines); } if (Cursor) { LgiAssert(Cursor->Blk != NULL); if (Cursor->Blk) Cursor->Blk->GetPosFromIndex(Cursor); } return true; } void GRichTextPriv::OnStyleChange(GRichTextEdit::RectType t) { GCss s; switch (t) { case GRichTextEdit::FontFamilyBtn: { GCss::StringsDef Fam(Values[t].Str()); s.FontFamily(Fam); if (!ChangeSelectionStyle(&s, true)) StyleDirty.Add(t); break; } case GRichTextEdit::FontSizeBtn: { double Pt = Values[t].CastDouble(); s.FontSize(GCss::Len(GCss::LenPt, (float)Pt)); if (!ChangeSelectionStyle(&s, true)) StyleDirty.Add(t); break; } case GRichTextEdit::BoldBtn: { s.FontWeight(GCss::FontWeightBold); if (!ChangeSelectionStyle(&s, Values[t].CastBool())) StyleDirty.Add(t); break; } case GRichTextEdit::ItalicBtn: { s.FontStyle(GCss::FontStyleItalic); if (!ChangeSelectionStyle(&s, Values[t].CastBool())) StyleDirty.Add(t); break; } case GRichTextEdit::UnderlineBtn: { s.TextDecoration(GCss::TextDecorUnderline); if (ChangeSelectionStyle(&s, Values[t].CastBool())) StyleDirty.Add(t); break; } case GRichTextEdit::ForegroundColourBtn: { s.Color(GCss::ColorDef(GCss::ColorRgb, (uint32) Values[t].Value.Int64)); if (!ChangeSelectionStyle(&s, true)) StyleDirty.Add(t); break; } case GRichTextEdit::BackgroundColourBtn: { s.BackgroundColor(GCss::ColorDef(GCss::ColorRgb, (uint32) Values[t].Value.Int64)); if (!ChangeSelectionStyle(&s, true)) StyleDirty.Add(t); break; } default: break; } } bool GRichTextPriv::ChangeSelectionStyle(GCss *Style, bool Add) { if (!Selection) return false; GAutoPtr Trans(new Transaction); bool Cf = CursorFirst(); GRichTextPriv::BlockCursor *Start = Cf ? Cursor : Selection; GRichTextPriv::BlockCursor *End = Cf ? Selection : Cursor; if (Start->Blk == End->Blk) { // Change style in the same block... int 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' int i = Blocks.IndexOf(Start->Blk); if (i >= 0) { for (++i; Blocks[i] != End->Blk && i < (int)Blocks.Length(); i++) { GRichTextPriv::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 GRichTextPriv::PaintBtn(GSurface *pDC, GRichTextEdit::RectType t) { GRect r = Areas[t]; GVariant &v = Values[t]; bool Down = (v.Type == GV_BOOL && v.Value.Bool) || (BtnState[t].IsPress && BtnState[t].Pressed && BtnState[t].MouseOver); SysFont->Colour(LC_TEXT, BtnState[t].MouseOver ? LC_LIGHT : LC_MED); SysFont->Transparent(false); GColour Low(96, 96, 96); pDC->Colour(Down ? GColour::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 : GColour::White); pDC->Line(r.x1, r.y1, r.x2, r.y1); pDC->Line(r.x1, r.y1, r.x1, r.y2); r.Size(1, 1); switch (v.Type) { case GV_STRING: { GDisplayString Ds(SysFont, 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)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(GColour(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 GRichTextEdit::BoldBtn: Label = "B"; break; case GRichTextEdit::ItalicBtn: Label = "I"; break; case GRichTextEdit::UnderlineBtn: Label = "U"; break; default: break; } if (!Label) break; GDisplayString Ds(SysFont, 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 GRichTextPriv::MakeLink(TextBlock *tb, ssize_t Offset, ssize_t Len, GString Link) { if (!tb) return false; GArray st; if (!tb->GetTextAt(Offset, st)) return false; GAutoPtr ns(new GNamedStyle); if (ns) { if (st.Last()->GetStyle()) *ns = *st.Last()->GetStyle(); ns->TextDecoration(GCss::TextDecorUnderline); ns->Color(GCss::ColorDef(GCss::ColorRgb, GColour::Blue.c32())); GAutoPtr 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 GRichTextPriv::ClickBtn(GMouse &m, GRichTextEdit::RectType t) { switch (t) { case GRichTextEdit::FontFamilyBtn: { List Fonts; if (!GFontSystem::Inst()->EnumerateFonts(Fonts)) return Error(_FL, "EnumerateFonts failed."); bool UseSub = (SysFont->GetHeight() * Fonts.Length()) > (GdcD->Y() * 0.8); GSubMenu s; GSubMenu *Cur = NULL; int Idx = 1; char Last = 0; for (const char *f = Fonts.First(); f; ) { if (*f == '@') { Fonts.Delete(f); DeleteArray(f); f = Fonts.Current(); } else if (UseSub) { if (*f != Last || Cur == NULL) { GString str; str.Printf("%c...", Last = *f); Cur = s.AppendSub(str); } if (Cur) Cur->AppendItem(f, Idx++); else break; f = Fonts.Next(); } else { s.AppendItem(f, Idx++); f = Fonts.Next(); } } GdcPt2 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 GRichTextEdit::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 }; GSubMenu s; for (int Idx = 0; Sizes[Idx]; Idx++) s.AppendItem(Sizes[Idx], Idx+1); GdcPt2 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 GRichTextEdit::BoldBtn: case GRichTextEdit::ItalicBtn: case GRichTextEdit::UnderlineBtn: { Values[t] = !Values[t].CastBool(); View->Invalidate(Areas+t); OnStyleChange(t); break; } case GRichTextEdit::ForegroundColourBtn: case GRichTextEdit::BackgroundColourBtn: { GdcPt2 p(Areas[t].x1, Areas[t].y2 + 1); View->PointToScreen(p); new SelectColour(this, p, t); break; } case GRichTextEdit::MakeLinkBtn: { if (!Cursor || !Cursor->Blk) break; TextBlock *tb = dynamic_cast(Cursor->Blk); if (!tb) break; GArray 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... GInput i(View, a->Param, "Edit link:", "Link"); if (i.DoModal()) { a->Param = i.Str; } } else if (Selection) { // Turn current selection into link GInput i(View, NULL, "Edit link:", "Link"); if (i.DoModal()) { BlockCursor *Start = CursorFirst() ? Cursor : Selection; BlockCursor *End = CursorFirst() ? Selection : Cursor; if (!Start || !End) return false; if (Start->Blk != End->Blk) { LgiMsg(View, "Selection too large.", "Error"); return false; } MakeLink(tb, Start->Offset, End->Offset - Start->Offset, i.Str.Get()); } } break; } case GRichTextEdit::RemoveLinkBtn: { if (!Cursor || !Cursor->Blk) break; TextBlock *tb = dynamic_cast(Cursor->Blk); if (!tb) break; GArray 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(); GAutoPtr s(new GCss); GNamedStyle *Ns = a->GetStyle(); if (Ns) *s = *Ns; if (s->TextDecoration() == GCss::TextDecorUnderline) s->DeleteProp(GCss::PropTextDecoration); if ((GColour)s->Color() == GColour::Blue) s->DeleteProp(GCss::PropColor); Ns = AddStyleToCache(s); a->SetStyle(Ns); tb->LayoutDirty = true; InvalidateDoc(NULL); } break; } case GRichTextEdit::RemoveStyleBtn: { GCss s; ChangeSelectionStyle(&s, false); break; } /* case GRichTextEdit::CapabilityBtn: { View->OnCloseInstaller(); break; } */ case GRichTextEdit::EmojiBtn: { GdcPt2 p(Areas[t].x1, Areas[t].y2 + 1); View->PointToScreen(p); new EmojiMenu(this, p); break; } case GRichTextEdit::HorzRuleBtn: { InsertHorzRule(); break; } default: return false; } return true; } bool GRichTextPriv::InsertHorzRule() { if (!Cursor || !Cursor->Blk) return false; TextBlock *tb = dynamic_cast(Cursor->Blk); if (!tb) return false; GAutoPtr Trans(new Transaction); DeleteSelection(Trans, NULL); int InsertIdx = Blocks.IndexOf(tb) + 1; GRichTextPriv::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); GAutoPtr c(new BlockCursor(Hr, 0, 0)); return SetCursor(c); } void GRichTextPriv::Paint(GSurface *pDC, GScrollBar *&ScrollY) { /* if (Areas[GRichTextEdit::CapabilityArea].Valid()) { GRect &t = Areas[GRichTextEdit::CapabilityArea]; pDC->Colour(GColour::Red); pDC->Rectangle(&t); int y = t.y1 + 4; for (unsigned i=0; iTransparent(true); SysFont->Colour(GColour::White, GColour::Red); Ds.Draw(pDC, t.x1 + 4, y); y += Ds.Y() + 4; } PaintBtn(pDC, GRichTextEdit::CapabilityBtn); } */ if (Areas[GRichTextEdit::ToolsArea].Valid()) { // Draw tools area... GRect &t = Areas[GRichTextEdit::ToolsArea]; #ifdef WIN32 GDoubleBuffer Buf(pDC, &t); #endif pDC->Colour(GColour(180, 180, 206)); pDC->Rectangle(&t); GRect r = t; r.Size(3, 3); #define AllocPx(sz, border) \ GRect(r.x1, r.y1, r.x1 + (int)(sz) - 1, r.y2); r.x1 += (int)(sz) + border Areas[GRichTextEdit::FontFamilyBtn] = AllocPx(100, 6); Areas[GRichTextEdit::FontSizeBtn] = AllocPx(40, 6); Areas[GRichTextEdit::BoldBtn] = AllocPx(r.Y(), 0); Areas[GRichTextEdit::ItalicBtn] = AllocPx(r.Y(), 0); Areas[GRichTextEdit::UnderlineBtn] = AllocPx(r.Y(), 6); Areas[GRichTextEdit::ForegroundColourBtn] = AllocPx(r.Y()*1.5, 0); Areas[GRichTextEdit::BackgroundColourBtn] = AllocPx(r.Y()*1.5, 6); { GDisplayString Ds(SysFont, TEXT_LINK); Areas[GRichTextEdit::MakeLinkBtn] = AllocPx(Ds.X() + 12, 0); } { GDisplayString Ds(SysFont, TEXT_REMOVE_LINK); Areas[GRichTextEdit::RemoveLinkBtn] = AllocPx(Ds.X() + 12, 6); } { GDisplayString Ds(SysFont, TEXT_REMOVE_STYLE); Areas[GRichTextEdit::RemoveStyleBtn] = AllocPx(Ds.X() + 12, 6); } for (unsigned int i=GRichTextEdit::EmojiBtn; iGetOrigin(Origin.x, Origin.y); GRect r = Areas[GRichTextEdit::ContentArea]; #if defined(WINDOWS) && !DEBUG_NO_DOUBLE_BUF GMemDC Mem(r.X(), r.Y(), pDC->GetColourSpace()); GSurface *pScreen = pDC; pDC = &Mem; r.Offset(-r.x1, -r.y1); #else 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[GRichTextEdit::ContentArea].Y(); int ExtraPx = DrawPx > DocumentExtent.y ? DrawPx - DocumentExtent.y : 0; r.Set(0, 0, DocumentExtent.x-1, DocumentExtent.y-1); // Fill the padding... GCssTools ct(this, Font); r = ct.PaintPadding(pDC, r); // Fill the background... #if DEBUG_COVERAGE_CHECK pDC->Colour(GColour(255, 0, 255)); #else GCss::ColorDef cBack = BackgroundColor(); // pDC->Colour(cBack.IsValid() ? (GColour)cBack : GColour(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(LC_TEXT, 24); Ctx.Colours[Unselected].Back.Set(LC_WORKSPACE, 24); if (View->Focus()) { Ctx.Colours[Selected].Fore.Set(LC_FOCUS_SEL_FORE, 24); Ctx.Colours[Selected].Back.Set(LC_FOCUS_SEL_BACK, 24); } else { Ctx.Colours[Selected].Fore.Set(LC_NON_FOCUS_SEL_FORE, 24); Ctx.Colours[Selected].Back.Set(LC_NON_FOCUS_SEL_BACK, 24); } for (unsigned i=0; iOnPaint(Ctx); #if DEBUG_OUTLINE_BLOCKS pDC->Colour(GColour(192, 192, 192)); pDC->LineStyle(GSurface::LineDot); pDC->Box(&b->GetPos()); pDC->LineStyle(GSurface::LineSolid); #endif } } #ifdef _DEBUG pDC->Colour(GColour::Green); for (unsigned i=0; iBox(&DebugRects[i]); } #endif #if 0 // Outline the line the cursor is on if (Cursor) { pDC->Colour(GColour::Blue); pDC->LineStyle(GSurface::LineDot); pDC->Box(&Cursor->Line); } #endif #if defined(WINDOWS) && !DEBUG_NO_DOUBLE_BUF Mem.SetOrigin(0, 0); pScreen->Blt(Areas[GRichTextEdit::ContentArea].x1, Areas[GRichTextEdit::ContentArea].y1, &Mem); #endif } GHtmlElement *GRichTextPriv::CreateElement(GHtmlElement *Parent) { return new GRichEditElem(Parent); } bool GRichTextPriv::ToHtml(GArray *Media) { GStringPipe p(256); p.Print("\n" "\n" "\t\n"); ZeroRefCounts(); for (unsigned i=0; iIncAllStyleRefs(); } if (GetStyles()) { p.Print("\t\n"); } p.Print("\n" "\n"); for (unsigned i=0; iToHtml(p, Media); } p.Print("\n\n"); return UtfNameCache.Reset(p.NewStr()); } void GRichTextPriv::DumpBlocks() { LgiTrace("GRichTextPriv Blocks=%i\n", Blocks.Length()); for (unsigned i=0; iGetClass(), b->GetStyle(), b->GetStyle() ? b->GetStyle()->Name.Get() : NULL); b->Dump(); LgiTrace("}\n"); } } bool GRichTextPriv::FromHtml(GHtmlElement *e, CreateContext &ctx, GCss *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++) { GHtmlElement *c = e->Children[i]; GAutoPtr Style; if (ParentStyle) Style.Reset(new GCss(*ParentStyle)); // Check to see if the element is block level and end the previous // paragraph if so. c->Info = c->Tag ? GHtmlStatic::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)) LgiAssert(!"Impl me."); continue; break; } case TAG_B: { if (!Style) Style.Reset(new GCss); if (Style) Style->FontWeight(GCss::FontWeightBold); break; } case TAG_A: { if (!Style) Style.Reset(new GCss); if (Style) { Style->TextDecoration(GCss::TextDecorUnderline); Style->Color(GCss::ColorDef(GCss::ColorRgb, GColour::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 GCss); if (Style) Style->Parse(Css, ParseRelaxed); } if (c->Get("class", Class)) { GCss::SelArray Selectors; GRichEditElemContext StyleCtx; if (ctx.StyleStore.Match(Selectors, &StyleCtx, dynamic_cast(c))) { for (unsigned n=0; nStyle; if (s) { if (!Style) Style.Reset(new GCss); if (Style) Style->Parse(s); } } } } } GNamedStyle *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 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)) { GCss::Len Sz(s); int Px = Sz.ToPx(); if (Px) ctx.Ib->Size.x = Px; } if (c->Get("height", s)) { GCss::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()) { // Start a new block because the styles are different... EndStyleChange = true; Blocks.Add(ctx.Tb = new TextBlock(this)); 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); } 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 GRichTextPriv::GetSelection(GArray &Text) { GArray Utf32; bool Cf = CursorFirst(); GRichTextPriv::BlockCursor *Start = Cf ? Cursor : Selection; GRichTextPriv::BlockCursor *End = Cf ? Selection : Cursor; if (Start->Blk == End->Blk) { // In the same block... just copy int Len = End->Offset - Start->Offset; Start->Blk->CopyAt(Start->Offset, Len, &Utf32); } else { // Multi-block delete... // 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' int i = Blocks.IndexOf(Start->Blk); int EndIdx = Blocks.IndexOf(End->Blk); if (i >= 0 && EndIdx >= i) { for (++i; Blocks[i] != End->Blk && i < (int)Blocks.Length(); i++) { GRichTextPriv::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*)LgiNewConvertCp(LGI_WideCharset, &Utf32[0], "utf-32", Utf32.Length() * sizeof(uint32)); if (!w) return Error(_FL, "Failed to convert %i utf32 to wide.", Utf32.Length()); Text.Add(w, Strlen(w)); Text.Add(0); return true; } GRichTextEdit::RectType GRichTextPriv::PosToButton(GMouse &m) { if (Areas[GRichTextEdit::ToolsArea].Overlap(m.x, m.y) // || Areas[GRichTextEdit::CapabilityArea].Overlap(m.x, m.y) ) { for (unsigned i=GRichTextEdit::FontFamilyBtn; iOnComponentInstall(Name); } } #ifdef _DEBUG void GRichTextPriv::DumpNodes(GTree *Root) { if (Cursor) { GTreeItem *ti = new GTreeItem; 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) { GTreeItem *ti = new GTreeItem; 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); GString s; s.Printf("[%i] %s", i, ti->GetText()); ti->SetText(s); Root->Insert(ti); } } GTreeItem *PrintNode(GTreeItem *Parent, const char *Fmt, ...) { GTreeItem *i = new GTreeItem; GString s; va_list Arg; va_start(Arg, Fmt); s.Printf(Arg, Fmt); va_end(Arg); s = s.Replace("\n", "\\n"); i->SetText(s); Parent->Insert(i); return i; } #endif diff --git a/src/common/Widgets/Editor/GRichTextEditPriv.h b/src/common/Widgets/Editor/GRichTextEditPriv.h --- a/src/common/Widgets/Editor/GRichTextEditPriv.h +++ b/src/common/Widgets/Editor/GRichTextEditPriv.h @@ -1,1415 +1,1363 @@ /* Rich text design notes: - The document is an array of Blocks (Blocks have no hierarchy) - Blocks have a length in characters. New lines are considered as one '\n' char. -- Currently the main type of block is the TextBlock +- The main type of block is the TextBlock - TextBlock contains: - array of StyleText: This is the source text. Each run of text has a style associated with it. This forms the input to the layout algorithm and is what the user is editing. - array of TextLine: Contains all the info needed to render one line of text. Essentially the output of the layout engine. Contains an array of DisplayStr objects. i.e. Characters in the exact same style as each other. It will regularly be deleted and re-flowed from the StyleText objects. - For a plaint text document the entire thing is contained by the one TextBlock. -- There will be an Image block down the track, where the image is treated as one character object. +- There is an Image block, where the image is treated as one character object. +- Also a horizontal rule block. */ #ifndef _RICH_TEXT_EDIT_PRIV_H_ #define _RICH_TEXT_EDIT_PRIV_H_ #include "GHtmlCommon.h" #include "GHtmlParser.h" #include "GFontCache.h" #include "GDisplayString.h" #include "GColourSpace.h" #include "GPopup.h" #include "Emoji.h" #include "LgiSpellCheck.h" #define DEBUG_LOG_CURSOR_COUNT 0 #define DEBUG_OUTLINE_CUR_DISPLAY_STR 0 #define DEBUG_OUTLINE_CUR_STYLE_TEXT 0 #define DEBUG_OUTLINE_BLOCKS 0 #define DEBUG_NO_DOUBLE_BUF 0 #define DEBUG_COVERAGE_CHECK 0 #define DEBUG_NUMBERED_LAYOUTS 0 #if 0 // _DEBUG #define LOG_FN LgiTrace #else #define LOG_FN d->Log->Print #endif #define TEXT_LINK "Link" #define TEXT_REMOVE_LINK "X" #define TEXT_REMOVE_STYLE "Remove Style" #define TEXT_CAP_BTN "Ok" #define TEXT_EMOJI ":)" #define TEXT_HORZRULE "HR" #define RTE_CURSOR_BLINK_RATE 1000 #define RTE_PULSE_RATE 200 #define RICH_TEXT_RESIZED_JPEG_QUALITY 83 // out of 100, high = better quality #define NoTransaction NULL #define IsWordBreakChar(ch) \ ( \ ( \ (ch) == ' ' || (ch) == '\t' || (ch) == '\r' || (ch) == '\n' \ ) \ || \ ( \ EmojiToIconIndex(&(ch), 1) >= 0 \ ) \ ) enum RteCommands { // IDM_OPEN = 10, IDM_NEW = 2000, IDM_RTE_COPY, IDM_RTE_CUT, IDM_RTE_PASTE, IDM_RTE_UNDO, IDM_RTE_REDO, IDM_COPY_URL, IDM_AUTO_INDENT, IDM_UTF8, IDM_PASTE_NO_CONVERT, IDM_FIXED, IDM_SHOW_WHITE, IDM_HARD_TABS, IDM_INDENT_SIZE, IDM_TAB_SIZE, IDM_DUMP, IDM_RTL, IDM_COPY_ORIGINAL, IDM_COPY_CURRENT, IDM_DUMP_NODES, IDM_CLOCKWISE, IDM_ANTI_CLOCKWISE, IDM_X_FLIP, IDM_Y_FLIP, IDM_SCALE_IMAGE, CODEPAGE_BASE = 100, CONVERT_CODEPAGE_BASE = 200, SPELLING_BASE = 300 }; ////////////////////////////////////////////////////////////////////// #define PtrCheckBreak(ptr) if (!ptr) { LgiAssert(!"Invalid ptr"); break; } #undef FixedToInt #define FixedToInt(fixed) ((fixed)>>GDisplayString::FShift) #undef IntToFixed #define IntToFixed(val) ((val)<= e) - break; - if ((*s & 0xfc00) == 0xDC00) - s++; - - c++; - } - else c++; - } - } - - return c; -} -*/ - ////////////////////////////////////////////////////////////////////// #include "GRange.h" class GRichEditElem : public GHtmlElement { GHashTbl Attr; public: GRichEditElem(GHtmlElement *parent) : GHtmlElement(parent) { } bool Get(const char *attr, const char *&val) { if (!attr) return false; GString s = Attr.Find(attr); if (!s) return false; val = s; return true; } void Set(const char *attr, const char *val) { if (!attr) return; Attr.Add(attr, GString(val)); } void SetStyle() { } }; struct GRichEditElemContext : public GCss::ElementCallback { /// Returns the element name const char *GetElement(GRichEditElem *obj); /// Returns the document unque element ID const char *GetAttr(GRichEditElem *obj, const char *Attr); /// Returns the class bool GetClasses(GString::Array &Classes, GRichEditElem *obj); /// Returns the parent object GRichEditElem *GetParent(GRichEditElem *obj); /// Returns an array of child objects GArray GetChildren(GRichEditElem *obj); }; class GDocFindReplaceParams3 : public GDocFindReplaceParams { public: // Find/Replace History char16 *LastFind; char16 *LastReplace; bool MatchCase; bool MatchWord; bool SelectionOnly; GDocFindReplaceParams3() { LastFind = 0; LastReplace = 0; MatchCase = false; MatchWord = false; SelectionOnly = false; } ~GDocFindReplaceParams3() { DeleteArray(LastFind); DeleteArray(LastReplace); } }; struct GNamedStyle : public GCss { int RefCount; GString Name; GNamedStyle() { RefCount = 0; } }; class GCssCache { int Idx; GArray Styles; GString Prefix; public: GCssCache(); ~GCssCache(); void SetPrefix(GString s) { Prefix = s; } uint32 GetStyles(); void ZeroRefCounts(); bool OutputStyles(GStream &s, int TabDepth); GNamedStyle *AddStyleToCache(GAutoPtr &s); }; class GRichTextPriv; class SelectColour : public GPopup { GRichTextPriv *d; GRichTextEdit::RectType Type; struct Entry { GRect r; GColour c; }; GArray e; public: SelectColour(GRichTextPriv *priv, GdcPt2 p, GRichTextEdit::RectType t); const char *GetClass() { return "SelectColour"; } void OnPaint(GSurface *pDC); void OnMouseClick(GMouse &m); void Visible(bool i); }; class EmojiMenu : public GPopup { GRichTextPriv *d; struct Emoji { GRect Src, Dst; uint32 u; }; struct Pane { GRect Btn; GArray e; }; GArray Panes; static int Cur; public: EmojiMenu(GRichTextPriv *priv, GdcPt2 p); void OnPaint(GSurface *pDC); void OnMouseClick(GMouse &m); void Visible(bool i); bool InsertEmoji(uint32 Ch); }; struct CtrlCap { GString Name, Param; void Set(const char *name, const char *param) { Name = name; Param = param; } }; struct ButtonState { uint8 IsMenu : 1; uint8 IsPress : 1; uint8 Pressed : 1; uint8 MouseOver : 1; }; extern bool Utf16to32(GArray &Out, const uint16 *In, int Len); class GEmojiContext { GAutoPtr EmojiImg; public: GSurface *GetEmojiImage(); }; class GRichTextPriv : public GCss, public GHtmlParser, public GHtmlStaticInst, public GCssCache, public GFontCache, public GEmojiContext { GStringPipe LogBuffer; public: enum SelectModeType { Unselected = 0, Selected = 1, }; enum SeekType { SkUnknown, SkLineStart, SkLineEnd, SkDocStart, SkDocEnd, // Horizontal navigation SkLeftChar, SkLeftWord, SkRightChar, SkRightWord, // Vertical navigation SkUpPage, SkUpLine, SkCurrentLine, SkDownLine, SkDownPage, }; struct DisplayStr; struct BlockCursor; class Block; GRichTextEdit *View; GString OriginalText; GAutoWString WideNameCache; GAutoString UtfNameCache; GAutoPtr Font; bool WordSelectMode; bool Dirty; GdcPt2 DocumentExtent; // Px GString Charset; GHtmlStaticInst Inst; int NextUid; GStream *Log; bool HtmlLinkAsCid; uint64 BlinkTs; // Spell check support GSpellCheck *SpellCheck; bool SpellDictionaryLoaded; GString SpellLang, SpellDict; // This is set when the user changes a style without a selection, // indicating that we should start a new run when new text is entered GArray StyleDirty; // Toolbar bool ShowTools; GRichTextEdit::RectType ClickedBtn, OverBtn; ButtonState BtnState[GRichTextEdit::MaxArea]; GRect Areas[GRichTextEdit::MaxArea]; GVariant Values[GRichTextEdit::MaxArea]; // Scrolling int ScrollLinePx; int ScrollOffsetPx; bool ScrollChange; // Capabilities // GArray NeedsCap; // Debug stuff GArray DebugRects; // Constructor GRichTextPriv(GRichTextEdit *view, GRichTextPriv **Ptr); ~GRichTextPriv(); bool Error(const char *file, int line, const char *fmt, ...); bool IsBusy(bool Stop = false); struct Flow { GRichTextPriv *d; GSurface *pDC; // Used for printing. int Left, Right;// Left and right margin positions as measured in px // from the left of the page (controls client area). int Top; int CurY; // Current y position down the page in document co-ords bool Visible; // true if the current block overlaps the visible page // If false, the implementation can take short cuts and // guess various dimensions. Flow(GRichTextPriv *priv) { d = priv; pDC = NULL; Left = 0; Top = 0; Right = 1000; CurY = 0; Visible = true; } int X() { return Right - Left + 1; } GString Describe() { GString s; s.Printf("Left=%i Right=%i CurY=%i", Left, Right, CurY); return s; } }; struct ColourPair { GColour Fore, Back; void Empty() { Fore.Empty(); Back.Empty(); } }; /// This is a run of text, all of the same style class StyleText : public GArray { GNamedStyle *Style; // owned by the CSS cache public: ColourPair Colours; HtmlTag Element; GString Param; bool Emoji; StyleText(const StyleText *St); StyleText(const uint32 *t = NULL, ssize_t Chars = -1, GNamedStyle *style = NULL); uint32 *At(ssize_t i); GNamedStyle *GetStyle(); void SetStyle(GNamedStyle *s); }; struct PaintContext { int Index; GSurface *pDC; SelectModeType Type; ColourPair Colours[2]; BlockCursor *Cursor, *Select; // Cursor stuff int CurEndPoint; GArray EndPoints; PaintContext() { Index = 0; pDC = NULL; Type = Unselected; Cursor = NULL; Select = NULL; CurEndPoint = 0; } GColour &Fore() { return Colours[Type].Fore; } GColour &Back() { return Colours[Type].Back; } void DrawBox(GRect &r, GRect &Edge, GColour &c) { if (Edge.x1 > 0 || Edge.x2 > 0 || Edge.y1 > 0 || Edge.y2 > 0) { pDC->Colour(c); if (Edge.x1) { pDC->Rectangle(r.x1, r.y1, r.x1 + Edge.x1 - 1, r.y2); r.x1 += Edge.x1; } if (Edge.y1) { pDC->Rectangle(r.x1, r.y1, r.x2, r.y1 + Edge.y1 - 1); r.y1 += Edge.y1; } if (Edge.y2) { pDC->Rectangle(r.x1, r.y2 - Edge.y2 + 1, r.x2, r.y2); r.y2 -= Edge.y2; } if (Edge.x2) { pDC->Rectangle(r.x2 - Edge.x2 + 1, r.y1, r.x2, r.y2); r.x2 -= Edge.x2; } } } // This handles calculating the selection stuff for simple "one char" blocks // like images and HR. Call this at the start of the OnPaint. // \return TRUE if the content should be drawn selected. bool SelectBeforePaint(class GRichTextPriv::Block *b) { CurEndPoint = 0; if (b->Cursors > 0 && Select) { // Selection end point checks... if (Cursor && Cursor->Blk == b) EndPoints.Add(Cursor->Offset); if (Select && Select->Blk == b) EndPoints.Add(Select->Offset); // Sort the end points if (EndPoints.Length() > 1 && EndPoints[0] > EndPoints[1]) { ssize_t ep = EndPoints[0]; EndPoints[0] = EndPoints[1]; EndPoints[1] = ep; } } // Before selection end point if (CurEndPoint < (ssize_t)EndPoints.Length() && EndPoints[CurEndPoint] == 0) { Type = Type == Selected ? Unselected : Selected; CurEndPoint++; } return Type == Selected; } // Call this after the OnPaint // \return TRUE if the content after the block is selected. bool SelectAfterPaint(class GRichTextPriv::Block *b) { // After image selection end point if (CurEndPoint < (ssize_t)EndPoints.Length() && EndPoints[CurEndPoint] == 1) { Type = Type == Selected ? Unselected : Selected; CurEndPoint++; } return Type == Selected; } }; struct HitTestResult { GdcPt2 In; Block *Blk; DisplayStr *Ds; ssize_t Idx; int LineHint; bool Near; HitTestResult(int x, int y) { In.x = x; In.y = y; Blk = NULL; Ds = NULL; Idx = -1; LineHint = -1; Near = false; } }; ////////////////////////////////////////////////////////////////////////////////////////////// // Undo structures... struct DocChange { virtual ~DocChange() {} virtual bool Apply(GRichTextPriv *Ctx, bool Forward) = 0; }; class Transaction { public: GArray Changes; ~Transaction() { Changes.DeleteObjects(); } void Add(DocChange *Dc) { Changes.Add(Dc); } bool Apply(GRichTextPriv *Ctx, bool Forward) { for (unsigned i=0; iApply(Ctx, Forward)) return false; } return true; } }; GArray UndoQue; ssize_t UndoPos; bool AddTrans(GAutoPtr &t); bool SetUndoPos(ssize_t Pos); template bool GetBlockByUid(T *&Ptr, int Uid, int *Idx = NULL) { for (unsigned i=0; iGetUid() == Uid) { if (Idx) *Idx = i; return (Ptr = dynamic_cast(b)) != NULL; } } if (Idx) *Idx = -1; return false; } ////////////////////////////////////////////////////////////////////////////////////////////// // A Block is like a DIV in HTML, it's as wide as the page and // always starts and ends on a whole line. class Block : public GEventSinkI, public GEventTargetI { protected: int BlockUid; GRichTextPriv *d; public: /// This is the number of cursors current referencing this Block. int8 Cursors; Block(GRichTextPriv *priv) { d = priv; BlockUid = d->NextUid++; Cursors = 0; } Block(const Block *blk) { d = blk->d; BlockUid = blk->GetUid(); Cursors = 0; } virtual ~Block() { // We must have removed cursors by the time we are deleted // otherwise there will be a hanging pointer in the cursor // object. LgiAssert(Cursors == 0); } // Events bool PostEvent(int Cmd, GMessage::Param a = 0, GMessage::Param b = 0) { bool r = d->View->PostEvent(M_BLOCK_MSG, (GMessage::Param)(Block*)this, (GMessage::Param)new GMessage(Cmd, a, b)); #if defined(_DEBUG) if (!r) LgiTrace("%s:%i - Warning: PostEvent failed..\n", _FL); #endif return r; } + // If this returns non-zero further command processing is aborted. GMessage::Result OnEvent(GMessage *Msg) { - return 0; + return false; } /************************************************ * Get state methods, do not modify the block * ***********************************************/ virtual const char *GetClass() { return "Block"; } virtual GRect GetPos() = 0; virtual ssize_t Length() = 0; virtual bool HitTest(HitTestResult &htr) = 0; virtual bool GetPosFromIndex(BlockCursor *Cursor) = 0; virtual bool OnLayout(Flow &f) = 0; virtual void OnPaint(PaintContext &Ctx) = 0; virtual bool ToHtml(GStream &s, GArray *Media) = 0; virtual bool OffsetToLine(ssize_t Offset, int *ColX, GArray *LineY) = 0; virtual int LineToOffset(int Line) = 0; virtual int GetLines() = 0; virtual ssize_t FindAt(ssize_t StartIdx, const uint32 *Str, GFindReplaceCommon *Params) = 0; - virtual void SetSpellingErrors(GArray &Errors) {} + virtual void SetSpellingErrors(GArray &Errors, GRange r) {} virtual void IncAllStyleRefs() {} virtual void Dump() {} virtual GNamedStyle *GetStyle(ssize_t At = -1) = 0; virtual int GetUid() const { return BlockUid; } virtual bool DoContext(GSubMenu &s, GdcPt2 Doc, ssize_t Offset, bool Spelling) { return false; } #ifdef _DEBUG virtual void DumpNodes(GTreeItem *Ti) = 0; #endif virtual bool IsValid() { return false; } virtual bool IsBusy(bool Stop = false) { return false; } virtual Block *Clone() = 0; virtual void OnComponentInstall(GString Name) {} // Copy some or all of the text out virtual ssize_t CopyAt(ssize_t Offset, ssize_t Chars, GArray *Text) { return false; } /// This method moves a cursor index. /// \returns the new cursor index or -1 on error. virtual bool Seek ( /// [In] true if the next line is needed, false for the previous line SeekType To, /// [In/Out] The starting cursor. BlockCursor &Cursor ) = 0; /************************************************ * Change state methods, require a transaction * ***********************************************/ // Add some text at a given position virtual bool AddText ( /// Current transaction Transaction *Trans, /// The index to add at (-1 = the end) ssize_t AtOffset, /// The text itself const uint32 *Str, /// [Optional] The number of characters ssize_t Chars = -1, /// [Optional] Style to give the text, NULL means "use the existing style" GNamedStyle *Style = NULL ) { return false; } /// Delete some chars /// \returns the number of chars actually removed virtual ssize_t DeleteAt ( Transaction *Trans, ssize_t Offset, ssize_t Chars, GArray *DeletedText = NULL ) { return false; } /// Changes the style of a range of characters virtual bool ChangeStyle ( Transaction *Trans, ssize_t Offset, ssize_t Chars, GCss *Style, bool Add ) { return false; } virtual bool DoCase ( /// Current transaction Transaction *Trans, /// Start index of text to change ssize_t StartIdx, /// Number of chars to change ssize_t Chars, /// True if upper case is desired bool Upper ) { return false; } // Split a block virtual Block *Split ( /// Current transaction Transaction *Trans, /// The index to add at (-1 = the end) ssize_t AtOffset ) { return NULL; } // Event called on dictionary load virtual bool OnDictionary(Transaction *Trans) { return false; } }; struct BlockCursor { // The block the cursor is in. Block *Blk; // This is the character offset of the cursor relative to // the start of 'Blk'. ssize_t Offset; // In wrapped text, a given offset can either be at the end // of one line or the start of the next line. This tells the // text block which line the cursor is actually on. int LineHint; // This is the position on the screen in doc coords. GRect Pos; // This is the position line that the cursor is on. This is // used to calculate the bounds for screen updates. GRect Line; // Cursor is currently blinking on bool Blink; BlockCursor(const BlockCursor &c); BlockCursor(Block *b, ssize_t off, int line); ~BlockCursor(); BlockCursor &operator =(const BlockCursor &c); void Set(ssize_t off); void Set(Block *b, ssize_t off, int line); bool operator ==(const BlockCursor &c) { return Blk == c.Blk && Offset == c.Offset; } #ifdef _DEBUG void DumpNodes(GTreeItem *Ti); #endif }; GAutoPtr Cursor, Selection; /// This is part or all of a Text run struct DisplayStr : public GDisplayString { StyleText *Src; ssize_t Chars; // The number of UTF-32 characters. This can be different to // GDisplayString::Length() in the case that GDisplayString // is using UTF-16 (i.e. Windows). int OffsetY; // Offset of this string from the TextLine's box in the Y axis DisplayStr(StyleText *src, GFont *f, const uint32 *s, ssize_t l = -1, GSurface *pdc = NULL) : GDisplayString(f, #ifndef WINDOWS (char16*) #endif s, l, pdc) { Src = src; OffsetY = 0; #if defined(_MSC_VER) Chars = l < 0 ? Strlen(s) : l; #else Chars = len; #endif } template T *Utf16Seek(T *s, int i) { T *e = s + i; while (s < e) { uint16 n = *s & 0xfc00; if (n == 0xd800) { s++; if (s >= e) break; n = *s & 0xfc00; if (n != 0xdc00) { LgiAssert(!"Unexpected surrogate"); continue; } // else skip over the 2nd surrogate } s++; } return s; } // Make a sub-string of this display string virtual GAutoPtr Clone(ssize_t Start, ssize_t Len = -1) { GAutoPtr c; if (len > 0 && Len != 0) { const char16 *Str = *this; if (Len < 0) Len = len - Start; if (Start >= 0 && Start < (int)len && Start + Len <= (int)len) { #if defined(_MSC_VER) LgiAssert(Str != NULL); const char16 *s = Utf16Seek(Str, Start); const char16 *e = Utf16Seek(s, Len); GArray Tmp; if (Utf16to32(Tmp, (const uint16*)s, e - s)) c.Reset(new DisplayStr(Src, GetFont(), &Tmp[0], Tmp.Length(), pDC)); #else c.Reset(new DisplayStr(Src, GetFont(), (uint32*)Str + Start, Len, pDC)); #endif } } return c; } virtual void Paint(GSurface *pDC, int &FixX, int FixY, GColour &Back) { FDraw(pDC, FixX, FixY); FixX += FX(); } virtual double GetAscent() { return Font->Ascent(); } virtual ssize_t PosToIndex(int x, bool Nearest) { return CharAt(x); } }; struct EmojiDisplayStr : public DisplayStr { GArray SrcRect; GSurface *Img; #if defined(_MSC_VER) GArray Utf32; #endif EmojiDisplayStr(StyleText *src, GSurface *img, GFont *f, const uint32 *s, ssize_t l = -1); GAutoPtr Clone(ssize_t Start, ssize_t Len = -1); void Paint(GSurface *pDC, int &FixX, int FixY, GColour &Back); double GetAscent(); ssize_t PosToIndex(int XPos, bool Nearest); }; /// This structure is a layout of a full line of text. Made up of one or more /// display string objects. struct TextLine { /// This is a position relative to the parent Block GRect PosOff; /// The array of display strings GArray Strs; /// Is '1' for lines that have a new line character at the end. uint8 NewLine; TextLine(int XOffsetPx, int WidthPx, int YOffsetPx); int Length(); /// This runs after the layout line has been filled with display strings. /// It measures the line and works out the right offsets for each strings /// so that their baselines all match up correctly. void LayoutOffsets(int DefaultFontHt); }; class TextBlock : public Block { GNamedStyle *Style; GArray SpellingErrors; int PaintErrIdx, ClickErrIdx; GSpellCheck::SpellingError *SpErr; bool PreEdit(Transaction *Trans); - void UpdateSpellingAndLinks(Transaction *Trans, GRange r); void DrawDisplayString(GSurface *pDC, DisplayStr *Ds, int &FixX, int FixY, GColour &Bk, int &Pos); public: // Runs of characters in the same style: pre-layout. GArray Txt; // Runs of characters (display strings) of potentially different styles on the same line: post-layout. GArray Layout; // True if the 'Layout' data is out of date. bool LayoutDirty; // Size of the edges GRect Margin, Border, Padding; // Default font for the block GFont *Fnt; // Chars in the whole block (sum of all Text lengths) ssize_t Len; // Position in document co-ordinates GRect Pos; TextBlock(GRichTextPriv *priv); TextBlock(const TextBlock *Copy); ~TextBlock(); bool IsValid(); // No state change methods const char *GetClass() { return "TextBlock"; } int GetLines(); bool OffsetToLine(ssize_t Offset, int *ColX, GArray *LineY); int LineToOffset(int Line); GRect GetPos() { return Pos; } void Dump(); GNamedStyle *GetStyle(ssize_t At = -1); void SetStyle(GNamedStyle *s); ssize_t Length(); bool ToHtml(GStream &s, GArray *Media); bool GetPosFromIndex(BlockCursor *Cursor); bool HitTest(HitTestResult &htr); void OnPaint(PaintContext &Ctx); bool OnLayout(Flow &flow); ssize_t GetTextAt(ssize_t Offset, GArray &t); ssize_t CopyAt(ssize_t Offset, ssize_t Chars, GArray *Text); bool Seek(SeekType To, BlockCursor &Cursor); ssize_t FindAt(ssize_t StartIdx, const uint32 *Str, GFindReplaceCommon *Params); void IncAllStyleRefs(); - void SetSpellingErrors(GArray &Errors); + void SetSpellingErrors(GArray &Errors, GRange r); bool DoContext(GSubMenu &s, GdcPt2 Doc, ssize_t Offset, bool Spelling); #ifdef _DEBUG void DumpNodes(GTreeItem *Ti); #endif Block *Clone(); bool IsEmptyLine(BlockCursor *Cursor); + void UpdateSpellingAndLinks(Transaction *Trans, GRange r); // Events GMessage::Result OnEvent(GMessage *Msg); // Transactional changes bool AddText(Transaction *Trans, ssize_t AtOffset, const uint32 *Str, ssize_t Chars = -1, GNamedStyle *Style = NULL); bool ChangeStyle(Transaction *Trans, ssize_t Offset, ssize_t Chars, GCss *Style, bool Add); ssize_t DeleteAt(Transaction *Trans, ssize_t BlkOffset, ssize_t Chars, GArray *DeletedText = NULL); bool DoCase(Transaction *Trans, ssize_t StartIdx, ssize_t Chars, bool Upper); Block *Split(Transaction *Trans, ssize_t AtOffset); bool StripLast(Transaction *Trans, const char *Set = " \t\r\n"); // Strip trailing new line if present.. bool OnDictionary(Transaction *Trans); }; class HorzRuleBlock : public Block { GRect Pos; bool IsDeleted; public: HorzRuleBlock(GRichTextPriv *priv); HorzRuleBlock(const HorzRuleBlock *Copy); ~HorzRuleBlock(); bool IsValid(); // No state change methods const char *GetClass() { return "HorzRuleBlock"; } int GetLines(); bool OffsetToLine(ssize_t Offset, int *ColX, GArray *LineY); int LineToOffset(int Line); GRect GetPos() { return Pos; } void Dump(); GNamedStyle *GetStyle(ssize_t At = -1); void SetStyle(GNamedStyle *s); ssize_t Length(); bool ToHtml(GStream &s, GArray *Media); bool GetPosFromIndex(BlockCursor *Cursor); bool HitTest(HitTestResult &htr); void OnPaint(PaintContext &Ctx); bool OnLayout(Flow &flow); ssize_t GetTextAt(ssize_t Offset, GArray &t); ssize_t CopyAt(ssize_t Offset, ssize_t Chars, GArray *Text); bool Seek(SeekType To, BlockCursor &Cursor); ssize_t FindAt(ssize_t StartIdx, const uint32 *Str, GFindReplaceCommon *Params); void IncAllStyleRefs(); - void SetSpellingErrors(GArray &Errors); bool DoContext(GSubMenu &s, GdcPt2 Doc, ssize_t Offset, bool Spelling); #ifdef _DEBUG void DumpNodes(GTreeItem *Ti); #endif Block *Clone(); // Events GMessage::Result OnEvent(GMessage *Msg); // Transactional changes bool AddText(Transaction *Trans, ssize_t AtOffset, const uint32 *Str, ssize_t Chars = -1, GNamedStyle *Style = NULL); bool ChangeStyle(Transaction *Trans, ssize_t Offset, ssize_t Chars, GCss *Style, bool Add); ssize_t DeleteAt(Transaction *Trans, ssize_t BlkOffset, ssize_t Chars, GArray *DeletedText = NULL); bool DoCase(Transaction *Trans, ssize_t StartIdx, ssize_t Chars, bool Upper); Block *Split(Transaction *Trans, ssize_t AtOffset); }; class ImageBlock : public Block { public: struct ScaleInf { GdcPt2 Sz; GString MimeType; GAutoPtr Compressed; int Percent; ScaleInf() { Sz.x = Sz.y = 0; Percent = 0; } }; int ThreadHnd; protected: GNamedStyle *Style; int Scale; GRect SourceValid; GAutoString FileMimeType; GArray Scales; int ResizeIdx; int ThreadBusy; bool IsDeleted; void UpdateThreadBusy(const char *File, int Line, int Off); int GetThreadHandle(); void UpdateDisplay(int y); void UpdateDisplayImg(); public: GAutoPtr SourceImg, DisplayImg, SelectImg; GRect Margin, Border, Padding; GString Source; GdcPt2 Size; bool LayoutDirty; GRect Pos; // position in document co-ordinates GRect ImgPos; ImageBlock(GRichTextPriv *priv); ImageBlock(const ImageBlock *Copy); ~ImageBlock(); bool IsValid(); bool IsBusy(bool Stop = false); bool Load(const char *Src = NULL); bool SetImage(GAutoPtr Img); // No state change methods int GetLines(); bool OffsetToLine(ssize_t Offset, int *ColX, GArray *LineY); int LineToOffset(int Line); GRect GetPos() { return Pos; } void Dump(); GNamedStyle *GetStyle(ssize_t At = -1); void SetStyle(GNamedStyle *s); ssize_t Length(); bool ToHtml(GStream &s, GArray *Media); bool GetPosFromIndex(BlockCursor *Cursor); bool HitTest(HitTestResult &htr); void OnPaint(PaintContext &Ctx); bool OnLayout(Flow &flow); ssize_t GetTextAt(ssize_t Offset, GArray &t); ssize_t CopyAt(ssize_t Offset, ssize_t Chars, GArray *Text); bool Seek(SeekType To, BlockCursor &Cursor); ssize_t FindAt(ssize_t StartIdx, const uint32 *Str, GFindReplaceCommon *Params); void IncAllStyleRefs(); bool DoContext(GSubMenu &s, GdcPt2 Doc, ssize_t Offset, bool Spelling); #ifdef _DEBUG void DumpNodes(GTreeItem *Ti); #endif Block *Clone(); void OnComponentInstall(GString Name); // Events GMessage::Result OnEvent(GMessage *Msg); // Transactional changes bool AddText(Transaction *Trans, ssize_t AtOffset, const uint32 *Str, ssize_t Chars = -1, GNamedStyle *Style = NULL); bool ChangeStyle(Transaction *Trans, ssize_t Offset, ssize_t Chars, GCss *Style, bool Add); ssize_t DeleteAt(Transaction *Trans, ssize_t BlkOffset, ssize_t Chars, GArray *DeletedText = NULL); bool DoCase(Transaction *Trans, ssize_t StartIdx, ssize_t Chars, bool Upper); }; GArray Blocks; Block *Next(Block *b); Block *Prev(Block *b); void InvalidateDoc(GRect *r); void ScrollTo(GRect r); void UpdateStyleUI(); void EmptyDoc(); void Empty(); bool Seek(BlockCursor *In, SeekType Dir, bool Select); bool CursorFirst(); bool SetCursor(GAutoPtr c, bool Select = false); GRect SelectionRect(); bool GetSelection(GArray &Text); ssize_t IndexOfCursor(BlockCursor *c); ssize_t HitTest(int x, int y, int &LineHint, Block **Blk = NULL); bool CursorFromPos(int x, int y, GAutoPtr *Cursor, ssize_t *GlobalIdx); Block *GetBlockByIndex(ssize_t Index, ssize_t *Offset = NULL, int *BlockIdx = NULL, int *LineCount = NULL); bool Layout(GScrollBar *&ScrollY); void OnStyleChange(GRichTextEdit::RectType t); bool ChangeSelectionStyle(GCss *Style, bool Add); void PaintBtn(GSurface *pDC, GRichTextEdit::RectType t); bool MakeLink(TextBlock *tb, ssize_t Offset, ssize_t Len, GString Link); bool ClickBtn(GMouse &m, GRichTextEdit::RectType t); bool InsertHorzRule(); void Paint(GSurface *pDC, GScrollBar *&ScrollY); GHtmlElement *CreateElement(GHtmlElement *Parent); GdcPt2 ScreenToDoc(int x, int y); GdcPt2 DocToScreen(int x, int y); bool Merge(Transaction *Trans, Block *a, Block *b); bool DeleteSelection(Transaction *t, char16 **Cut); GRichTextEdit::RectType PosToButton(GMouse &m); void OnComponentInstall(GString Name); struct CreateContext { TextBlock *Tb; ImageBlock *Ib; HorzRuleBlock *Hrb; GArray Buf; char16 LastChar; GFontCache *FontCache; GCss::Store StyleStore; bool StartOfLine; CreateContext(GFontCache *fc) { Tb = NULL; Ib = NULL; Hrb = NULL; LastChar = '\n'; FontCache = fc; StartOfLine = true; } bool AddText(GNamedStyle *Style, char16 *Str) { if (!Str || !Tb) return false; int Used = 0; char16 *s = Str; char16 *e = s + StrlenW(s); while (s < e) { if (*s == '\r') { s++; continue; } if (IsWhiteSpace(*s)) { Buf[Used++] = ' '; while (s < e && IsWhiteSpace(*s)) s++; } else { Buf[Used++] = *s++; while (s < e && !IsWhiteSpace(*s)) { Buf[Used++] = *s++; } } } bool Status = false; if (Used > 0) { Status = Tb->AddText(NoTransaction, -1, &Buf[0], Used, Style); LastChar = Buf[Used-1]; } return Status; } }; GAutoPtr CreationCtx; bool ToHtml(GArray *Media = NULL); void DumpBlocks(); bool FromHtml(GHtmlElement *e, CreateContext &ctx, GCss *ParentStyle = NULL, int Depth = 0); #ifdef _DEBUG void DumpNodes(GTree *Root); #endif }; struct BlockCursorState { bool Cursor; ssize_t Offset; int LineHint; int BlockUid; BlockCursorState(bool cursor, GRichTextPriv::BlockCursor *c); bool Apply(GRichTextPriv *Ctx, bool Forward); }; struct CompleteTextBlockState : public GRichTextPriv::DocChange { int Uid; GAutoPtr Cur, Sel; GAutoPtr Blk; CompleteTextBlockState(GRichTextPriv *Ctx, GRichTextPriv::TextBlock *Tb); bool Apply(GRichTextPriv *Ctx, bool Forward); }; struct MultiBlockState : public GRichTextPriv::DocChange { GRichTextPriv *Ctx; ssize_t Index; // Number of blocks before the edit ssize_t Length; // Of the other version currently in the Ctx stack GArray Blks; MultiBlockState(GRichTextPriv *ctx, ssize_t Start); bool Apply(GRichTextPriv *Ctx, bool Forward); bool Copy(ssize_t Idx); bool Cut(ssize_t Idx); }; #ifdef _DEBUG GTreeItem *PrintNode(GTreeItem *Parent, const char *Fmt, ...); #endif typedef GRichTextPriv::BlockCursor BlkCursor; typedef GAutoPtr AutoCursor; typedef GAutoPtr AutoTrans; #endif \ No newline at end of file diff --git a/src/common/Widgets/Editor/HorzRuleBlock.cpp b/src/common/Widgets/Editor/HorzRuleBlock.cpp --- a/src/common/Widgets/Editor/HorzRuleBlock.cpp +++ b/src/common/Widgets/Editor/HorzRuleBlock.cpp @@ -1,252 +1,248 @@ #include "Lgi.h" #include "GRichTextEdit.h" #include "GRichTextEditPriv.h" #include "GDocView.h" GRichTextPriv::HorzRuleBlock::HorzRuleBlock(GRichTextPriv *priv) : Block(priv) { IsDeleted = false; } GRichTextPriv::HorzRuleBlock::HorzRuleBlock(const HorzRuleBlock *Copy) : Block(Copy->d) { IsDeleted = Copy->IsDeleted; } GRichTextPriv::HorzRuleBlock::~HorzRuleBlock() { LgiAssert(Cursors == 0); } bool GRichTextPriv::HorzRuleBlock::IsValid() { return true; } int GRichTextPriv::HorzRuleBlock::GetLines() { return 1; } bool GRichTextPriv::HorzRuleBlock::OffsetToLine(ssize_t Offset, int *ColX, GArray *LineY) { if (ColX) *ColX = Offset > 0; if (LineY) LineY->Add(0); return true; } int GRichTextPriv::HorzRuleBlock::LineToOffset(int Line) { return 0; } void GRichTextPriv::HorzRuleBlock::Dump() { } GNamedStyle *GRichTextPriv::HorzRuleBlock::GetStyle(ssize_t At) { return NULL; } void GRichTextPriv::HorzRuleBlock::SetStyle(GNamedStyle *s) { } ssize_t GRichTextPriv::HorzRuleBlock::Length() { return IsDeleted ? 0 : 1; } bool GRichTextPriv::HorzRuleBlock::ToHtml(GStream &s, GArray *Media) { s.Print("
\n"); return true; } bool GRichTextPriv::HorzRuleBlock::GetPosFromIndex(BlockCursor *Cursor) { if (!Cursor) return d->Error(_FL, "No cursor param."); Cursor->Pos = Pos; Cursor->Line = Pos; if (Cursor->Offset == 0) Cursor->Pos.x2 = Cursor->Pos.x1 + 1; else Cursor->Pos.x1 = Cursor->Pos.x2 - 1; return true; } bool GRichTextPriv::HorzRuleBlock::HitTest(HitTestResult &htr) { if (htr.In.y < Pos.y1 || htr.In.y > Pos.y2) return false; htr.Near = false; htr.LineHint = 0; int Cx = Pos.x1 + (Pos.X() / 2); if (htr.In.x < Cx) htr.Idx = 0; else htr.Idx = 1; return true; } void GRichTextPriv::HorzRuleBlock::OnPaint(PaintContext &Ctx) { Ctx.SelectBeforePaint(this); GColour Fore, Back = Ctx.Back(); Fore = Ctx.Fore().Mix(Back, 0.75f); Ctx.pDC->Colour(Back); Ctx.pDC->Rectangle(&Pos); Ctx.pDC->Colour(Fore); int Cy = Pos.y1 + (Pos.Y() >> 1); Ctx.pDC->Rectangle(Pos.x1, Cy-1, Pos.x2, Cy); if (Ctx.Cursor != NULL && Ctx.Cursor->Blk == (Block*)this && Ctx.Cursor->Blink && d->View->Focus()) { GRect &p = Ctx.Cursor->Pos; Ctx.pDC->Colour(Ctx.Fore()); Ctx.pDC->Rectangle(&p); } Ctx.SelectAfterPaint(this); } bool GRichTextPriv::HorzRuleBlock::OnLayout(Flow &flow) { Pos.x1 = flow.Left; Pos.y1 = flow.CurY; Pos.x2 = flow.Right; Pos.y2 = flow.CurY + 15; // Start with a 16px height. flow.CurY = Pos.y2 + 1; return true; } ssize_t GRichTextPriv::HorzRuleBlock::GetTextAt(ssize_t Offset, GArray &t) { return 0; } ssize_t GRichTextPriv::HorzRuleBlock::CopyAt(ssize_t Offset, ssize_t Chars, GArray *Text) { return 0; } bool GRichTextPriv::HorzRuleBlock::Seek(SeekType To, BlockCursor &Cursor) { switch (To) { case SkLineStart: { Cursor.Offset = 0; Cursor.LineHint = 0; break; } case SkLineEnd: { Cursor.Offset = 1; Cursor.LineHint = 0; break; } case SkLeftChar: { if (Cursor.Offset != 1) return false; Cursor.Offset = 0; Cursor.LineHint = 0; break; } case SkRightChar: { if (Cursor.Offset != 0) return false; Cursor.Offset = 1; Cursor.LineHint = 0; break; } default: { return false; break; } } return true; } ssize_t GRichTextPriv::HorzRuleBlock::FindAt(ssize_t StartIdx, const uint32 *Str, GFindReplaceCommon *Params) { return 0; } void GRichTextPriv::HorzRuleBlock::IncAllStyleRefs() { } -void GRichTextPriv::HorzRuleBlock::SetSpellingErrors(GArray &Errors) -{ -} - bool GRichTextPriv::HorzRuleBlock::DoContext(GSubMenu &s, GdcPt2 Doc, ssize_t Offset, bool Spelling) { return false; } #ifdef _DEBUG void GRichTextPriv::HorzRuleBlock::DumpNodes(GTreeItem *Ti) { Ti->SetText("HorzRuleBlock"); } #endif GRichTextPriv::Block *GRichTextPriv::HorzRuleBlock::Clone() { return new HorzRuleBlock(this); } GMessage::Result GRichTextPriv::HorzRuleBlock::OnEvent(GMessage *Msg) { - return 0; + return false; } bool GRichTextPriv::HorzRuleBlock::AddText(Transaction *Trans, ssize_t AtOffset, const uint32 *Str, ssize_t Chars, GNamedStyle *Style) { return false; } bool GRichTextPriv::HorzRuleBlock::ChangeStyle(Transaction *Trans, ssize_t Offset, ssize_t Chars, GCss *Style, bool Add) { return false; } ssize_t GRichTextPriv::HorzRuleBlock::DeleteAt(Transaction *Trans, ssize_t BlkOffset, ssize_t Chars, GArray *DeletedText) { IsDeleted = BlkOffset == 0; if (IsDeleted) return true; return false; } bool GRichTextPriv::HorzRuleBlock::DoCase(Transaction *Trans, ssize_t StartIdx, ssize_t Chars, bool Upper) { return false; } GRichTextPriv::Block *GRichTextPriv::HorzRuleBlock::Split(Transaction *Trans, ssize_t AtOffset) { return NULL; } diff --git a/src/common/Widgets/Editor/ImageBlock.cpp b/src/common/Widgets/Editor/ImageBlock.cpp --- a/src/common/Widgets/Editor/ImageBlock.cpp +++ b/src/common/Widgets/Editor/ImageBlock.cpp @@ -1,1342 +1,1336 @@ #include "Lgi.h" #include "GRichTextEdit.h" #include "GRichTextEditPriv.h" #include "GdcTools.h" #include "GToken.h" #define LOADER_THREAD_LOGGING 1 #define TIMEOUT_LOAD_PROGRESS 100 // ms int ImgScales[] = { 15, 25, 50, 75, 100 }; class ImageLoader : public GEventTargetThread, public Progress { GString File; GEventSinkI *Sink; GSurface *Img; GAutoPtr Filter; bool SurfaceSent; int64 Ts; GAutoPtr In; public: ImageLoader(GEventSinkI *s) : GEventTargetThread("ImageLoader") { Sink = s; Img = NULL; SurfaceSent = false; Ts = 0; } ~ImageLoader() { Cancel(true); #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - ~ImageLoader\n", _FL); #endif } void Value(int64 v) { Progress::Value(v); if (!SurfaceSent) { SurfaceSent = true; PostSink(M_IMAGE_SET_SURFACE, (GMessage::Param)Img, (GMessage::Param)In.Release()); } int64 Now = LgiCurrentTime(); if (Now - Ts > TIMEOUT_LOAD_PROGRESS) { Ts = Now; PostSink(M_IMAGE_PROGRESS, (GMessage::Param)v); } } bool PostSink(int Cmd, GMessage::Param a = 0, GMessage::Param b = 0) { for (int i=0; i<50; i++) { if (Sink->PostEvent(Cmd, a, b)) return true; LgiSleep(1); } LgiAssert(!"PostSink failed."); return false; } GMessage::Result OnEvent(GMessage *Msg) { switch (Msg->Msg()) { case M_IMAGE_LOAD_FILE: { GAutoPtr Str((GString*)Msg->A()); File = *Str; #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Receive(M_IMAGE_LOAD_FILE): '%s'\n", _FL, File.Get()); #endif if (!Filter.Reset(GFilterFactory::New(File, O_READ, NULL))) { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): no filter\n", _FL); #endif return PostSink(M_IMAGE_ERROR); } if (!In.Reset(new GFile) || !In->Open(File, O_READ)) { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): can't read\n", _FL); #endif return PostSink(M_IMAGE_ERROR); } if (!(Img = new GMemDC)) { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): alloc err\n", _FL); #endif return PostSink(M_IMAGE_ERROR); } Filter->SetProgress(this); Ts = LgiCurrentTime(); GFilter::IoStatus Status = Filter->ReadImage(Img, In); if (Status != GFilter::IoSuccess) { if (Status == GFilter::IoComponentMissing) { GString *s = new GString(Filter->GetComponentName()); #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_COMPONENT_MISSING)\n", _FL); #endif return PostSink(M_IMAGE_COMPONENT_MISSING, (GMessage::Param)s); } #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): Filter::ReadImage err\n", _FL); #endif return PostSink(M_IMAGE_ERROR); } if (!SurfaceSent) { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_SET_SURFACE)\n", _FL); #endif PostSink(M_IMAGE_SET_SURFACE, (GMessage::Param)Img, (GMessage::Param)In.Release()); } #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_FINISHED)\n", _FL); #endif PostSink(M_IMAGE_FINISHED); break; } case M_IMAGE_LOAD_STREAM: { GAutoPtr Stream((GStreamI*)Msg->A()); GAutoPtr FileName((GString*)Msg->B()); #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Receive(M_IMAGE_LOAD_STREAM)\n", _FL); #endif if (!Stream) { LgiAssert(!"No stream."); return PostSink(M_IMAGE_ERROR); } GMemStream Mem(Stream, 0, -1); if (!Filter.Reset(GFilterFactory::New(FileName ? *FileName : 0, O_READ, (const uchar*)Mem.GetBasePtr()))) { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): no filter\n", _FL); #endif return PostSink(M_IMAGE_ERROR); } if (!(Img = new GMemDC)) { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): alloc err\n", _FL); #endif return PostSink(M_IMAGE_ERROR); } Filter->SetProgress(this); Ts = LgiCurrentTime(); GFilter::IoStatus Status = Filter->ReadImage(Img, &Mem); if (Status != GFilter::IoSuccess) { if (Status == GFilter::IoComponentMissing) { GString *s = new GString(Filter->GetComponentName()); #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_COMPONENT_MISSING)\n", _FL); #endif return PostSink(M_IMAGE_COMPONENT_MISSING, (GMessage::Param)s); } #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): Filter::ReadImage err\n", _FL); #endif return PostSink(M_IMAGE_ERROR); } if (!SurfaceSent) { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_SET_SURFACE)\n", _FL); #endif PostSink(M_IMAGE_SET_SURFACE, (GMessage::Param)Img, (GMessage::Param)In.Release()); } #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_FINISHED)\n", _FL); #endif PostSink(M_IMAGE_FINISHED); break; } case M_IMAGE_RESAMPLE: { GSurface *Dst = (GSurface*) Msg->A(); GSurface *Src = (GSurface*) Msg->B(); #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Receive(M_IMAGE_RESAMPLE)\n", _FL); #endif if (Src && Dst) { ResampleDC(Dst, Src); if (PostSink(M_IMAGE_RESAMPLE)) { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_RESAMPLE)\n", _FL); #endif } else LgiTrace("%s:%i - Error sending re-sample msg.\n", _FL); } else { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): ptr err %p %p\n", _FL, Src, Dst); #endif return PostSink(M_IMAGE_ERROR); } break; } case M_IMAGE_COMPRESS: { GSurface *img = (GSurface*)Msg->A(); GRichTextPriv::ImageBlock::ScaleInf *si = (GRichTextPriv::ImageBlock::ScaleInf*)Msg->B(); #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Receive(M_IMAGE_COMPRESS)\n", _FL); #endif if (!img || !si) { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): invalid ptr\n", _FL); #endif PostSink(M_IMAGE_ERROR, (GMessage::Param) new GString("Invalid pointer.")); break; } GAutoPtr f(GFilterFactory::New("a.jpg", O_READ, NULL)); if (!f) { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): No JPEG filter available\n", _FL); #endif PostSink(M_IMAGE_ERROR, (GMessage::Param) new GString("No JPEG filter available.")); break; } GAutoPtr scaled; if (img->X() != si->Sz.x || img->Y() != si->Sz.y) { if (!scaled.Reset(new GMemDC(si->Sz.x, si->Sz.y, img->GetColourSpace()))) break; ResampleDC(scaled, img, NULL, NULL); img = scaled; } GXmlTag Props; f->Props = &Props; Props.SetAttr(LGI_FILTER_QUALITY, RICH_TEXT_RESIZED_JPEG_QUALITY); GAutoPtr jpg(new GMemStream(1024)); if (!f->WriteImage(jpg, img)) { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_ERROR): Image compression failed\n", _FL); #endif PostSink(M_IMAGE_ERROR, (GMessage::Param) new GString("Image compression failed.")); break; } #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_COMPRESS)\n", _FL); #endif PostSink(M_IMAGE_COMPRESS, (GMessage::Param)jpg.Release(), (GMessage::Param)si); break; } case M_IMAGE_ROTATE: { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Receive(M_IMAGE_ROTATE)\n", _FL); #endif GSurface *Img = (GSurface*)Msg->A(); if (!Img) { LgiAssert(!"No image."); break; } RotateDC(Img, Msg->B() == 1 ? 90 : 270); #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_ROTATE)\n", _FL); #endif PostSink(M_IMAGE_ROTATE); break; } case M_IMAGE_FLIP: { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Receive(M_IMAGE_FLIP)\n", _FL); #endif GSurface *Img = (GSurface*)Msg->A(); if (!Img) { LgiAssert(!"No image."); break; } if (Msg->B() == 1) FlipXDC(Img); else FlipYDC(Img); #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Send(M_IMAGE_FLIP)\n", _FL); #endif PostSink(M_IMAGE_FLIP); break; } case M_CLOSE: { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Thread.Receive(M_CLOSE)\n", _FL); #endif EndThread(); break; } } return 0; } }; GRichTextPriv::ImageBlock::ImageBlock(GRichTextPriv *priv) : Block(priv) { ThreadHnd = 0; IsDeleted = false; LayoutDirty = false; Pos.ZOff(-1, -1); Style = NULL; Size.x = 200; Size.y = 64; Scale = 1; SourceValid.ZOff(-1, -1); ResizeIdx = -1; ThreadBusy = 0; Margin.ZOff(0, 0); Border.ZOff(0, 0); Padding.ZOff(0, 0); } GRichTextPriv::ImageBlock::ImageBlock(const ImageBlock *Copy) : Block(Copy->d) { ThreadHnd = 0; ThreadBusy = 0; LayoutDirty = true; SourceImg.Reset(new GMemDC(Copy->SourceImg)); Size = Copy->Size; IsDeleted = false; Margin = Copy->Margin; Border = Copy->Border; Padding = Copy->Padding; } GRichTextPriv::ImageBlock::~ImageBlock() { LgiAssert(ThreadBusy == 0); if (ThreadHnd) PostThreadEvent(ThreadHnd, M_CLOSE); LgiAssert(Cursors == 0); } bool GRichTextPriv::ImageBlock::IsValid() { return true; } bool GRichTextPriv::ImageBlock::IsBusy(bool Stop) { return ThreadBusy != 0; } bool GRichTextPriv::ImageBlock::SetImage(GAutoPtr Img) { SourceImg = Img; if (!SourceImg) return false; Scales.Length(CountOf(ImgScales)); for (int i=0; iX() * ImgScales[i] / 100; si.Sz.y = SourceImg->Y() * ImgScales[i] / 100; si.Percent = ImgScales[i]; if (si.Sz.x == SourceImg->X() && si.Sz.y == SourceImg->Y()) { ResizeIdx = i; } } LayoutDirty = true; UpdateDisplayImg(); if (DisplayImg) { // Update the display image by scaling it from the source... if (PostThreadEvent(GetThreadHandle(), M_IMAGE_RESAMPLE, (GMessage::Param) DisplayImg.Get(), (GMessage::Param) SourceImg.Get())) UpdateThreadBusy(_FL, 1); } else LayoutDirty = true; // Also create a JPG for the current scale (needed before // we save to HTML). if (ResizeIdx >= 0 && ResizeIdx < (int)Scales.Length()) { ScaleInf &si = Scales[ResizeIdx]; if (PostThreadEvent(GetThreadHandle(), M_IMAGE_COMPRESS, (GMessage::Param)SourceImg.Get(), (GMessage::Param)&si)) UpdateThreadBusy(_FL, 1); } else LgiAssert(!"ResizeIdx should be valid."); return true; } bool GRichTextPriv::ImageBlock::Load(const char *Src) { if (Src) Source = Src; GAutoPtr Stream; GString FileName; GString::Array a = Source.Strip().Split(":", 1); if (a.Length() > 1 && a[0].Equals("cid")) { GDocumentEnv *Env = d->View->GetEnv(); if (!Env) return false; GDocumentEnv::LoadJob *j = Env->NewJob(); if (!j) return false; j->Uri.Reset(NewStr(Source)); j->Env = Env; j->Pref = GDocumentEnv::LoadJob::FmtStream; j->UserUid = d->View->GetDocumentUid(); GDocumentEnv::LoadType Result = Env->GetContent(j); if (Result == GDocumentEnv::LoadImmediate) { if (j->Stream) Stream = j->Stream; else if (j->Filename) FileName = j->Filename; else if (j->pDC) { SourceImg = j->pDC; return true; } } else if (Result == GDocumentEnv::LoadDeferred) { LgiAssert(!"Impl me?"); } DeleteObj(j); } else if (FileExists(Source)) { FileName = Source; FileMimeType = LgiApp->GetFileMimeType(Source); } else return false; if (!FileName && !Stream) return false; - ImageLoader *il = new ImageLoader(this); - if (!il) - return false; - ThreadHnd = il->GetHandle(); - LgiAssert(ThreadHnd > 0); - if (Stream) { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Posting M_IMAGE_LOAD_STREAM\n", _FL); #endif - if (PostThreadEvent(ThreadHnd, M_IMAGE_LOAD_STREAM, (GMessage::Param)Stream.Release(), (GMessage::Param) (FileName ? new GString(FileName) : NULL))) + if (PostThreadEvent(GetThreadHandle(), M_IMAGE_LOAD_STREAM, (GMessage::Param)Stream.Release(), (GMessage::Param) (FileName ? new GString(FileName) : NULL))) { UpdateThreadBusy(_FL, 1); return true; } } if (FileName) { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Posting M_IMAGE_LOAD_FILE\n", _FL); #endif - if (PostThreadEvent(ThreadHnd, M_IMAGE_LOAD_FILE, (GMessage::Param)new GString(FileName))) + if (PostThreadEvent(GetThreadHandle(), M_IMAGE_LOAD_FILE, (GMessage::Param)new GString(FileName))) { UpdateThreadBusy(_FL, 1); return true; } } return false; } int GRichTextPriv::ImageBlock::GetLines() { return 1; } bool GRichTextPriv::ImageBlock::OffsetToLine(ssize_t Offset, int *ColX, GArray *LineY) { if (ColX) *ColX = Offset > 0; if (LineY) LineY->Add(0); return true; } int GRichTextPriv::ImageBlock::LineToOffset(int Line) { return 0; } void GRichTextPriv::ImageBlock::Dump() { } GNamedStyle *GRichTextPriv::ImageBlock::GetStyle(ssize_t At) { return Style; } void GRichTextPriv::ImageBlock::SetStyle(GNamedStyle *s) { if ((Style = s)) { GFont *Fnt = d->GetFont(s); LayoutDirty = true; LgiAssert(Fnt != NULL); Margin.x1 = Style->MarginLeft().ToPx(Pos.X(), Fnt); Margin.y1 = Style->MarginTop().ToPx(Pos.Y(), Fnt); Margin.x2 = Style->MarginRight().ToPx(Pos.X(), Fnt); Margin.y2 = Style->MarginBottom().ToPx(Pos.Y(), Fnt); Border.x1 = Style->BorderLeft().ToPx(Pos.X(), Fnt); Border.y1 = Style->BorderTop().ToPx(Pos.Y(), Fnt); Border.x2 = Style->BorderRight().ToPx(Pos.X(), Fnt); Border.y2 = Style->BorderBottom().ToPx(Pos.Y(), Fnt); Padding.x1 = Style->PaddingLeft().ToPx(Pos.X(), Fnt); Padding.y1 = Style->PaddingTop().ToPx(Pos.Y(), Fnt); Padding.x2 = Style->PaddingRight().ToPx(Pos.X(), Fnt); Padding.y2 = Style->PaddingBottom().ToPx(Pos.Y(), Fnt); } } ssize_t GRichTextPriv::ImageBlock::Length() { return IsDeleted ? 0 : 1; } bool GRichTextPriv::ImageBlock::ToHtml(GStream &s, GArray *Media) { if (Media) { bool ValidSourceFile = FileExists(Source); GDocView::ContentMedia &Cm = Media->New(); int Idx = LgiRand() % 10000; Cm.Id.Printf("%u@memecode.com", Idx); GString Style; ScaleInf *Si = ResizeIdx >= 0 && ResizeIdx < (int)Scales.Length() ? &Scales[ResizeIdx] : NULL; if (Si && Si->Compressed) { - // Attach a copy of the resized jpeg... + // Attach a copy of the resized JPEG... Si->Compressed->SetPos(0); Cm.Stream.Reset(new GMemStream(Si->Compressed, 0, -1)); Cm.MimeType = Si->MimeType; if (Cm.MimeType.Equals("image/jpeg")) Cm.FileName.Printf("img%u.jpg", Idx); else if (Cm.MimeType.Equals("image/png")) Cm.FileName.Printf("img%u.png", Idx); else if (Cm.MimeType.Equals("image/tiff")) Cm.FileName.Printf("img%u.tiff", Idx); else if (Cm.MimeType.Equals("image/gif")) Cm.FileName.Printf("img%u.gif", Idx); else if (Cm.MimeType.Equals("image/bmp")) Cm.FileName.Printf("img%u.bmp", Idx); else { LgiAssert(!"Unknown image mime type?"); Cm.FileName.Printf("img%u", Idx); } } else if (ValidSourceFile) { // Attach the original file... GAutoString mt = LgiApp->GetFileMimeType(Source); Cm.MimeType = mt.Get(); Cm.FileName = LgiGetLeaf(Source); GFile *f = new GFile; if (f) { if (f->Open(Source, O_READ)) { Cm.Stream.Reset(f); } else { delete f; LgiTrace("%s:%i - Failed to open link image '%s'.\n", _FL, Source.Get()); } } } else { LgiTrace("%s:%i - No source or JPEG for saving image to HTML.\n", _FL); + LgiAssert(!"No source file or compressed image."); return false; } LgiAssert(Cm.MimeType != NULL); if (DisplayImg && SourceImg && DisplayImg->X() != SourceImg->X()) { int Dx = DisplayImg->X(); - int Sx = SourceImg->X(); - Style.Printf(" style=\"width:%.0f%%\"", (double)Dx * 100 / Sx); + Style.Printf(" style=\"width:%ipx\"", Dx); } if (Cm.Stream) { - s.Print("\n"); + s.Print("\">\n"); + + LgiAssert(Cm.Valid()); return true; } } - s.Print("\n", Source.Get()); + s.Print("\n", Source.Get()); return true; } bool GRichTextPriv::ImageBlock::GetPosFromIndex(BlockCursor *Cursor) { if (!Cursor) return d->Error(_FL, "No cursor param."); if (LayoutDirty) { Cursor->Pos.ZOff(-1, -1); // This is valid behaviour... need to // wait for layout before getting cursor // position. return false; } Cursor->Pos = ImgPos; Cursor->Line = Pos; if (Cursor->Offset == 0) { Cursor->Pos.x2 = Cursor->Pos.x1 + 1; } else if (Cursor->Offset == 1) { Cursor->Pos.x1 = Cursor->Pos.x2 - 1; } return true; } bool GRichTextPriv::ImageBlock::HitTest(HitTestResult &htr) { if (htr.In.y < Pos.y1 || htr.In.y > Pos.y2) return false; htr.Near = false; htr.LineHint = 0; int Cx = ImgPos.x1 + (ImgPos.X() / 2); if (htr.In.x < Cx) htr.Idx = 0; else htr.Idx = 1; return true; } void GRichTextPriv::ImageBlock::OnPaint(PaintContext &Ctx) { bool ImgSelected = Ctx.SelectBeforePaint(this); // Paint margins, borders and padding... GRect r = Pos; r.x1 -= Margin.x1; r.y1 -= Margin.y1; r.x2 -= Margin.x2; r.y2 -= Margin.y2; GCss::ColorDef BorderStyle; if (Style) BorderStyle = Style->BorderLeft().Color; GColour BorderCol(222, 222, 222); if (BorderStyle.Type == GCss::ColorRgb) BorderCol.Set(BorderStyle.Rgb32, 32); Ctx.DrawBox(r, Margin, Ctx.Colours[Unselected].Back); Ctx.DrawBox(r, Border, BorderCol); Ctx.DrawBox(r, Padding, Ctx.Colours[Unselected].Back); if (!DisplayImg && SourceImg && SourceImg->X() > r.X()) { UpdateDisplayImg(); } GSurface *Src = DisplayImg ? DisplayImg : SourceImg; if (Src) { if (SourceValid.Valid()) { GRect Bounds(0, 0, Size.x-1, Size.y-1); Bounds.Offset(r.x1, r.y1); Ctx.pDC->Colour(LC_MED, 24); Ctx.pDC->Box(&Bounds); Bounds.Size(1, 1); Ctx.pDC->Colour(LC_WORKSPACE, 24); Ctx.pDC->Rectangle(&Bounds); GRect rr(0, 0, Src->X()-1, SourceValid.y2 / Scale); Ctx.pDC->Blt(r.x1, r.y1, Src, &rr); } else { if (Ctx.Type == GRichTextPriv::Selected) { if (!SelectImg && SelectImg.Reset(new GMemDC(Src->X(), Src->Y(), System32BitColourSpace))) { SelectImg->Blt(0, 0, Src); int Op = SelectImg->Op(GDC_ALPHA); GColour c = Ctx.Colours[GRichTextPriv::Selected].Back; c.Rgb(c.r(), c.g(), c.b(), 0xa0); SelectImg->Colour(c); SelectImg->Rectangle(); SelectImg->Op(Op); } Ctx.pDC->Blt(r.x1, r.y1, SelectImg); } else { Ctx.pDC->Blt(r.x1, r.y1, Src); } } } else { // Drag missing image... r = ImgPos; GColour cBack(245, 245, 245); Ctx.pDC->Colour(ImgSelected ? cBack.Mix(Ctx.Colours[Selected].Back) : cBack); Ctx.pDC->Rectangle(&r); Ctx.pDC->Colour(LC_LOW, 24); uint Ls = Ctx.pDC->LineStyle(GSurface::LineAlternate); Ctx.pDC->Box(&r); Ctx.pDC->LineStyle(Ls); int Cx = r.x1 + (r.X() >> 1); int Cy = r.y1 + (r.Y() >> 1); Ctx.pDC->Colour(GColour::Red); int Sz = 5; Ctx.pDC->Line(Cx - Sz, Cy - Sz, Cx + Sz, Cy + Sz); Ctx.pDC->Line(Cx - Sz, Cy - Sz + 1, Cx + Sz - 1, Cy + Sz); Ctx.pDC->Line(Cx - Sz + 1, Cy - Sz, Cx + Sz, Cy + Sz - 1); Ctx.pDC->Line(Cx + Sz, Cy - Sz, Cx - Sz, Cy + Sz); Ctx.pDC->Line(Cx + Sz - 1, Cy - Sz, Cx - Sz, Cy + Sz - 1); Ctx.pDC->Line(Cx + Sz, Cy - Sz + 1, Cx - Sz + 1, Cy + Sz); } ImgSelected = Ctx.SelectAfterPaint(this); if (ImgSelected) { Ctx.pDC->Colour(Ctx.Colours[Selected].Back); Ctx.pDC->Rectangle(ImgPos.x2 + 1, ImgPos.y1, ImgPos.x2 + 7, ImgPos.y2); } if (Ctx.Cursor && Ctx.Cursor->Blk == this && Ctx.Cursor->Blink && d->View->Focus()) { Ctx.pDC->Colour(CursorColour); if (Ctx.Cursor->Pos.Valid()) Ctx.pDC->Rectangle(&Ctx.Cursor->Pos); else Ctx.pDC->Rectangle(Pos.x1, Pos.y1, Pos.x1, Pos.y2); } } bool GRichTextPriv::ImageBlock::OnLayout(Flow &flow) { LayoutDirty = false; flow.Left += Margin.x1; flow.Right -= Margin.x2; flow.CurY += Margin.y1; Pos.x1 = flow.Left; Pos.y1 = flow.CurY; Pos.x2 = flow.Right; Pos.y2 = flow.CurY-1; // Start with a 0px height. flow.Left += Border.x1 + Padding.x1; flow.Right -= Border.x2 + Padding.x2; flow.CurY += Border.y1 + Padding.y1; ImgPos.x1 = Pos.x1 + Padding.x1; ImgPos.y1 = Pos.y1 + Padding.y1; ImgPos.x2 = ImgPos.x1 + Size.x - 1; ImgPos.y2 = ImgPos.y1 + Size.y - 1; int Px2 = ImgPos.x2 + Padding.x2; if (Px2 < Pos.x2) Pos.x2 = ImgPos.x2 + Padding.x2; Pos.y2 = ImgPos.y2 + Padding.y2; flow.CurY = Pos.y2 + 1 + Margin.y2 + Border.y2 + Padding.y2; flow.Left -= Margin.x1 + Border.x1 + Padding.x1; flow.Right += Margin.x2 + Border.x2 + Padding.x2; return true; } ssize_t GRichTextPriv::ImageBlock::GetTextAt(ssize_t Offset, GArray &t) { // No text to get return 0; } ssize_t GRichTextPriv::ImageBlock::CopyAt(ssize_t Offset, ssize_t Chars, GArray *Text) { // No text to copy return 0; } bool GRichTextPriv::ImageBlock::Seek(SeekType To, BlockCursor &Cursor) { switch (To) { case SkLineStart: { Cursor.Offset = 0; Cursor.LineHint = 0; break; } case SkLineEnd: { Cursor.Offset = 1; Cursor.LineHint = 0; break; } case SkLeftChar: { if (Cursor.Offset != 1) return false; Cursor.Offset = 0; Cursor.LineHint = 0; break; } case SkRightChar: { if (Cursor.Offset != 0) return false; Cursor.Offset = 1; Cursor.LineHint = 0; break; } default: { return false; break; } } return true; } ssize_t GRichTextPriv::ImageBlock::FindAt(ssize_t StartIdx, const uint32 *Str, GFindReplaceCommon *Params) { // No text to find in return -1; } void GRichTextPriv::ImageBlock::IncAllStyleRefs() { if (Style) Style->RefCount++; } bool GRichTextPriv::ImageBlock::DoContext(GSubMenu &s, GdcPt2 Doc, ssize_t Offset, bool Spelling) { if (SourceImg && !Spelling) { s.AppendSeparator(); GSubMenu *c = s.AppendSub("Transform Image"); if (c) { c->AppendItem("Rotate Clockwise", IDM_CLOCKWISE); c->AppendItem("Rotate Anti-clockwise", IDM_ANTI_CLOCKWISE); c->AppendItem("Horizontal Flip", IDM_X_FLIP); c->AppendItem("Vertical Flip", IDM_Y_FLIP); } c = s.AppendSub("Scale Image"); if (c) { for (unsigned i=0; iX() * ImgScales[i] / 100; si.Sz.y = SourceImg->Y() * ImgScales[i] / 100; si.Percent = ImgScales[i]; m.Printf("%i x %i, %i%% ", si.Sz.x, si.Sz.y, ImgScales[i]); if (si.Compressed) { char Sz[128]; LgiFormatSize(Sz, sizeof(Sz), si.Compressed->GetSize()); GString s; s.Printf(" (%s)", Sz); m += s; } GMenuItem *mi = c->AppendItem(m, IDM_SCALE_IMAGE+i, !IsBusy()); if (mi && ResizeIdx == i) { mi->Checked(true); } } } return true; } return false; } GRichTextPriv::Block *GRichTextPriv::ImageBlock::Clone() { return new ImageBlock(this); } - -void GRichTextPriv::ImageBlock::OnComponentInstall(GString Name) -{ - if (Source && !SourceImg) - { - // Retry the load? - Load(Source); - } -} + +void GRichTextPriv::ImageBlock::OnComponentInstall(GString Name) +{ + if (Source && !SourceImg) + { + // Retry the load? + Load(Source); + } +} void GRichTextPriv::ImageBlock::UpdateDisplay(int yy) { GRect s; if (DisplayImg && !SourceValid.Valid()) { SourceValid = SourceImg->Bounds(); SourceValid.y2 = yy; s = SourceValid; } else { s = SourceValid; s.y1 = s.y2 + 1; s.y2 = SourceValid.y2 = yy; } if (DisplayImg) { GRect d(0, s.y1 / Scale, DisplayImg->X()-1, s.y2 / Scale); // Do a quick and dirty nearest neighbor scale to // show the user some feed back. GSurface *Src = SourceImg; GSurface *Dst = DisplayImg; for (int y=d.y1; y<=d.y2; y++) { int sy = y * Scale; int sx = d.x1 * Scale; for (int x=d.x1; x<=d.x2; x++, sx+=Scale) { COLOUR c = Src->Get(sx, sy); Dst->Colour(c); Dst->Set(x, y); } } } LayoutDirty = true; this->d->InvalidateDoc(NULL); } int GRichTextPriv::ImageBlock::GetThreadHandle() { if (ThreadHnd == 0) { ImageLoader *il = new ImageLoader(this); if (il != NULL) ThreadHnd = il->GetHandle(); } return ThreadHnd; } void GRichTextPriv::ImageBlock::UpdateDisplayImg() { if (!SourceImg) return; Size.x = SourceImg->X(); Size.y = SourceImg->Y(); int ViewX = d->Areas[GRichTextEdit::ContentArea].X(); if (ViewX > 0) { int MaxX = (int) (ViewX * 0.9); if (SourceImg->X() > MaxX) { double Ratio = (double)SourceImg->X() / MAX(1, MaxX); Scale = (int)ceil(Ratio); Size.x = (int)ceil((double)SourceImg->X() / Scale); Size.y = (int)ceil((double)SourceImg->Y() / Scale); if (DisplayImg.Reset(new GMemDC(Size.x, Size.y, SourceImg->GetColourSpace()))) { DisplayImg->Colour(LC_MED, 24); DisplayImg->Rectangle(); } } } } void GRichTextPriv::ImageBlock::UpdateThreadBusy(const char *File, int Line, int Off) { if (ThreadBusy + Off >= 0) { ThreadBusy += Off; #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - ThreadBusy=%i\n", File, Line, ThreadBusy); #endif } else { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Error: ThreadBusy=%i\n", File, Line, ThreadBusy, ThreadBusy + Off); #endif LgiAssert(0); } } GMessage::Result GRichTextPriv::ImageBlock::OnEvent(GMessage *Msg) { switch (Msg->Msg()) { case M_COMMAND: { if (!SourceImg) break; if (Msg->A() >= IDM_SCALE_IMAGE && Msg->A() < IDM_SCALE_IMAGE + CountOf(ImgScales)) { int i = (int)Msg->A() - IDM_SCALE_IMAGE; if (i >= 0 && i < (int)Scales.Length()) { ScaleInf &si = Scales[i]; ResizeIdx = i; #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Posting M_IMAGE_COMPRESS\n", _FL); #endif if (PostThreadEvent(GetThreadHandle(), M_IMAGE_COMPRESS, (GMessage::Param)SourceImg.Get(), (GMessage::Param)&si)) UpdateThreadBusy(_FL, 1); + else + LgiAssert(!"PostThreadEvent failed."); } } else switch (Msg->A()) { case IDM_CLOCKWISE: #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Posting M_IMAGE_ROTATE\n", _FL); #endif if (PostThreadEvent(GetThreadHandle(), M_IMAGE_ROTATE, (GMessage::Param) SourceImg.Get(), 1)) UpdateThreadBusy(_FL, 1); break; case IDM_ANTI_CLOCKWISE: #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Posting M_IMAGE_ROTATE\n", _FL); #endif if (PostThreadEvent(GetThreadHandle(), M_IMAGE_ROTATE, (GMessage::Param) SourceImg.Get(), -1)) UpdateThreadBusy(_FL, 1); break; case IDM_X_FLIP: #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Posting M_IMAGE_FLIP\n", _FL); #endif if (PostThreadEvent(GetThreadHandle(), M_IMAGE_FLIP, (GMessage::Param) SourceImg.Get(), 1)) UpdateThreadBusy(_FL, 1); break; case IDM_Y_FLIP: #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Posting M_IMAGE_FLIP\n", _FL); #endif if (PostThreadEvent(GetThreadHandle(), M_IMAGE_FLIP, (GMessage::Param) SourceImg.Get(), 0)) UpdateThreadBusy(_FL, 1); break; } break; } case M_IMAGE_COMPRESS: { GAutoPtr Jpg((GMemStream*)Msg->A()); ScaleInf *Si = (ScaleInf*)Msg->B(); if (!Jpg || !Si) { LgiAssert(0); #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Error: M_IMAGE_COMPRESS bad arg\n", _FL); #endif break; } #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Received M_IMAGE_COMPRESS\n", _FL); #endif Si->Compressed.Reset(Jpg.Release()); Si->MimeType = "image/jpeg"; UpdateThreadBusy(_FL, -1); break; } case M_IMAGE_ERROR: { GAutoPtr ErrMsg((GString*) Msg->A()); #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Received M_IMAGE_ERROR, posting M_CLOSE\n", _FL); #endif - PostThreadEvent(ThreadHnd, M_CLOSE); - ThreadHnd = 0; UpdateThreadBusy(_FL, -1); break; } case M_IMAGE_COMPONENT_MISSING: { GAutoPtr Component((GString*) Msg->A()); #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Received M_IMAGE_COMPONENT_MISSING, posting M_CLOSE\n", _FL); #endif - PostThreadEvent(ThreadHnd, M_CLOSE); - ThreadHnd = 0; UpdateThreadBusy(_FL, -1); if (Component) { - GToken t(*Component, ","); - for (int i=0; iView->NeedsCapability(t[i]); } else LgiAssert(!"Missing component name."); break; } case M_IMAGE_SET_SURFACE: { GAutoPtr File((GFile*)Msg->B()); #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Received M_IMAGE_SET_SURFACE\n", _FL); #endif if (SourceImg.Reset((GSurface*)Msg->A())) { Scales.Length(CountOf(ImgScales)); for (int i=0; iX() * ImgScales[i] / 100; si.Sz.y = SourceImg->Y() * ImgScales[i] / 100; si.Percent = ImgScales[i]; if (si.Sz.x == SourceImg->X() && si.Sz.y == SourceImg->Y()) { ResizeIdx = i; si.Compressed.Reset(File.Release()); if (FileMimeType) { si.MimeType = FileMimeType.Get(); FileMimeType.Reset(); } } } UpdateDisplayImg(); } break; } case M_IMAGE_PROGRESS: { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Received M_IMAGE_PROGRESS\n", _FL); #endif UpdateDisplay((int)Msg->A()); break; } case M_IMAGE_FINISHED: { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Received M_IMAGE_FINISHED\n", _FL); #endif UpdateDisplay(SourceImg->Y()-1); UpdateThreadBusy(_FL, -1); if (DisplayImg != NULL && PostThreadEvent(GetThreadHandle(), M_IMAGE_RESAMPLE, (GMessage::Param)DisplayImg.Get(), (GMessage::Param)SourceImg.Get())) UpdateThreadBusy(_FL, 1); break; } case M_IMAGE_RESAMPLE: { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Received M_IMAGE_RESAMPLE\n", _FL); #endif LayoutDirty = true; UpdateThreadBusy(_FL, -1); d->InvalidateDoc(NULL); - PostThreadEvent(ThreadHnd, M_CLOSE); - ThreadHnd = 0; SourceValid.ZOff(-1, -1); break; } case M_IMAGE_ROTATE: case M_IMAGE_FLIP: { #if LOADER_THREAD_LOGGING LgiTrace("%s:%i - Received %s\n", _FL, Msg->Msg()==M_IMAGE_ROTATE?"M_IMAGE_ROTATE":"M_IMAGE_FLIP"); #endif GAutoPtr Img = SourceImg; UpdateThreadBusy(_FL, -1); SetImage(Img); break; } + default: + return false; } - return 0; + return true; } bool GRichTextPriv::ImageBlock::AddText(Transaction *Trans, ssize_t AtOffset, const uint32 *Str, ssize_t Chars, GNamedStyle *Style) { // Can't add text to image block return false; } bool GRichTextPriv::ImageBlock::ChangeStyle(Transaction *Trans, ssize_t Offset, ssize_t Chars, GCss *Style, bool Add) { // No styles to change... return false; } ssize_t GRichTextPriv::ImageBlock::DeleteAt(Transaction *Trans, ssize_t BlkOffset, ssize_t Chars, GArray *DeletedText) { // The image is one "character" IsDeleted = BlkOffset == 0; if (IsDeleted) return true; return false; } bool GRichTextPriv::ImageBlock::DoCase(Transaction *Trans, ssize_t StartIdx, ssize_t Chars, bool Upper) { // No text to change case... return false; } #ifdef _DEBUG void GRichTextPriv::ImageBlock::DumpNodes(GTreeItem *Ti) { GString s; s.Printf("ImageBlock style=%s", Style?Style->Name.Get():NULL); Ti->SetText(s); } #endif diff --git a/src/common/Widgets/Editor/TextBlock.cpp b/src/common/Widgets/Editor/TextBlock.cpp --- a/src/common/Widgets/Editor/TextBlock.cpp +++ b/src/common/Widgets/Editor/TextBlock.cpp @@ -1,2447 +1,2510 @@ #include "Lgi.h" #include "GRichTextEdit.h" #include "GRichTextEditPriv.h" #include "Emoji.h" #include "GDocView.h" #define DEBUG_LAYOUT 0 ////////////////////////////////////////////////////////////////////////////////////////////////// GRichTextPriv::StyleText::StyleText(const StyleText *St) { Emoji = St->Emoji; Style = NULL; Element = St->Element; Param = St->Param; if (St->Style) SetStyle(St->Style); Add((uint32*)&St->ItemAt(0), St->Length()); } GRichTextPriv::StyleText::StyleText(const uint32 *t, ssize_t Chars, GNamedStyle *style) { Emoji = false; Style = NULL; Element = CONTENT; if (style) SetStyle(style); if (t) { if (Chars < 0) Chars = Strlen(t); Add((uint32*)t, (int)Chars); } } uint32 *GRichTextPriv::StyleText::At(ssize_t i) { if (i >= 0 && i < (int)Length()) return &(*this)[i]; LgiAssert(0); return NULL; } GNamedStyle *GRichTextPriv::StyleText::GetStyle() { return Style; } void GRichTextPriv::StyleText::SetStyle(GNamedStyle *s) { if (Style != s) { Style = s; Colours.Empty(); if (Style) { GCss::ColorDef c = Style->Color(); if (c.Type == GCss::ColorRgb) Colours.Fore.Set(c.Rgb32, 32); c = Style->BackgroundColor(); if (c.Type == GCss::ColorRgb) Colours.Back.Set(c.Rgb32, 32); } } } ////////////////////////////////////////////////////////////////////////////////////////////////// GRichTextPriv::EmojiDisplayStr::EmojiDisplayStr(StyleText *src, GSurface *img, GFont *f, const uint32 *s, ssize_t l) : DisplayStr(src, NULL, s, l) { Img = img; #if defined(_MSC_VER) Utf16to32(Utf32, (const uint16*) StrCache.Get(), len); uint32 *u = &Utf32[0]; #else LgiAssert(sizeof(char16) == 4); uint32 *u = (uint32*)StrCache.Get(); Chars = Strlen(u); #endif for (int i=0; i= 0); if (Idx >= 0) { int x = Idx % EMOJI_GROUP_X; int y = Idx / EMOJI_GROUP_X; GRect &rc = SrcRect[i]; rc.ZOff(EMOJI_CELL_SIZE-1, EMOJI_CELL_SIZE-1); rc.Offset(x * EMOJI_CELL_SIZE, y * EMOJI_CELL_SIZE); } } x = (int)SrcRect.Length() * EMOJI_CELL_SIZE; y = EMOJI_CELL_SIZE; xf = IntToFixed(x); yf = IntToFixed(y); } GAutoPtr GRichTextPriv::EmojiDisplayStr::Clone(ssize_t Start, ssize_t Len) { if (Len < 0) Len = Chars - Start; #if defined(_MSC_VER) LgiAssert( Start >= 0 && Start < (int)Utf32.Length() && Start + Len <= (int)Utf32.Length()); #endif GAutoPtr s(new EmojiDisplayStr(Src, Img, NULL, #if defined(_MSC_VER) &Utf32[Start] #else (uint32*)(const char16*)(*this) #endif , Len)); return s; } void GRichTextPriv::EmojiDisplayStr::Paint(GSurface *pDC, int &FixX, int FixY, GColour &Back) { GRect f(0, 0, x-1, y-1); f.Offset(FixedToInt(FixX), FixedToInt(FixY)); pDC->Colour(Back); pDC->Rectangle(&f); int Op = pDC->Op(GDC_ALPHA); for (unsigned i=0; iBlt(f.x1, f.y1, Img, &SrcRect[i]); f.x1 += EMOJI_CELL_SIZE; FixX += IntToFixed(EMOJI_CELL_SIZE); } pDC->Op(Op); } double GRichTextPriv::EmojiDisplayStr::GetAscent() { return EMOJI_CELL_SIZE * 0.8; } ssize_t GRichTextPriv::EmojiDisplayStr::PosToIndex(int XPos, bool Nearest) { if (XPos >= (int)x) return Chars; if (XPos <= 0) return 0; return (XPos + (Nearest ? EMOJI_CELL_SIZE >> 1 : 0)) / EMOJI_CELL_SIZE; } ////////////////////////////////////////////////////////////////////////////////////////////////// GRichTextPriv::TextLine::TextLine(int XOffsetPx, int WidthPx, int YOffsetPx) { NewLine = 0; PosOff.ZOff(0, 0); PosOff.Offset(XOffsetPx, YOffsetPx); } int GRichTextPriv::TextLine::Length() { int Len = NewLine; for (unsigned i=0; iChars; return Len; } /// This runs after the layout line has been filled with display strings. /// It measures the line and works out the right offsets for each strings /// so that their baselines all match up correctly. void GRichTextPriv::TextLine::LayoutOffsets(int DefaultFontHt) { double BaseLine = 0.0; int HtPx = 0; for (unsigned i=0; iGetAscent(); BaseLine = MAX(BaseLine, Ascent); HtPx = MAX(HtPx, ds->Y()); } if (Strs.Length() == 0) HtPx = DefaultFontHt; else LgiAssert(HtPx > 0); for (unsigned i=0; iGetAscent(); if (Ascent > 0.0) ds->OffsetY = (int)(BaseLine - Ascent); LgiAssert(ds->OffsetY >= 0); HtPx = MAX(HtPx, ds->OffsetY+ds->Y()); } PosOff.y2 = PosOff.y1 + HtPx - 1; } ////////////////////////////////////////////////////////////////////////////////////////////////// GRichTextPriv::TextBlock::TextBlock(GRichTextPriv *priv) : Block(priv) { LayoutDirty = false; Len = 0; Pos.ZOff(-1, -1); Style = NULL; Fnt = NULL; ClickErrIdx = -1; Margin.ZOff(0, 0); Border.ZOff(0, 0); Padding.ZOff(0, 0); } GRichTextPriv::TextBlock::TextBlock(const TextBlock *Copy) : Block(Copy) { LayoutDirty = true; Len = Copy->Len; Pos = Copy->Pos; Style = Copy->Style; Fnt = Copy->Fnt; Margin = Copy->Margin; Border = Copy->Border; Padding = Copy->Padding; for (unsigned i=0; iTxt.Length(); i++) { Txt.Add(new StyleText(Copy->Txt.ItemAt(i))); } } GRichTextPriv::TextBlock::~TextBlock() { LgiAssert(Cursors == 0); Txt.DeleteObjects(); } void GRichTextPriv::TextBlock::Dump() { LgiTrace(" Txt.Len=%i, margin=%s, border=%s, padding=%s\n", Txt.Length(), Margin.GetStr(), Border.GetStr(), Padding.GetStr()); for (unsigned i=0; iLength() ? #ifndef WINDOWS (char16*) #endif t->At(0) : NULL, t->Length()); s = s.Strip(); LgiTrace(" %p: style=%p/%s, len=%i\n", t, t->GetStyle(), t->GetStyle() ? t->GetStyle()->Name.Get() : NULL, t->Length()); } } GNamedStyle *GRichTextPriv::TextBlock::GetStyle(ssize_t At) { if (At >= 0) { GArray t; if (GetTextAt(At, t)) return t[0]->GetStyle(); } return Style; } void GRichTextPriv::TextBlock::SetStyle(GNamedStyle *s) { if ((Style = s)) { Fnt = d->GetFont(s); LayoutDirty = true; LgiAssert(Fnt != NULL); Margin.x1 = Style->MarginLeft().ToPx(Pos.X(), Fnt); Margin.y1 = Style->MarginTop().ToPx(Pos.Y(), Fnt); Margin.x2 = Style->MarginRight().ToPx(Pos.X(), Fnt); Margin.y2 = Style->MarginBottom().ToPx(Pos.Y(), Fnt); Border.x1 = Style->BorderLeft().ToPx(Pos.X(), Fnt); Border.y1 = Style->BorderTop().ToPx(Pos.Y(), Fnt); Border.x2 = Style->BorderRight().ToPx(Pos.X(), Fnt); Border.y2 = Style->BorderBottom().ToPx(Pos.Y(), Fnt); Padding.x1 = Style->PaddingLeft().ToPx(Pos.X(), Fnt); Padding.y1 = Style->PaddingTop().ToPx(Pos.Y(), Fnt); Padding.x2 = Style->PaddingRight().ToPx(Pos.X(), Fnt); Padding.y2 = Style->PaddingBottom().ToPx(Pos.Y(), Fnt); } } ssize_t GRichTextPriv::TextBlock::Length() { return Len; } HtmlTag IsDefaultStyle(HtmlTag Id, GCss *Css) { if (!Css) return CONTENT; if (Css->Length() == 2) { GCss::ColorDef c = Css->Color(); if ((GColour)c != GColour::Blue) return CONTENT; GCss::TextDecorType td = Css->TextDecoration(); if (td != GCss::TextDecorUnderline) return CONTENT; return TAG_A; } else if (Css->Length() == 1) { GCss::FontWeightType fw = Css->FontWeight(); if (fw == GCss::FontWeightBold || fw == GCss::FontWeightBolder || fw >= GCss::FontWeight700) return TAG_B; GCss::TextDecorType td = Css->TextDecoration(); if (td == GCss::TextDecorUnderline) return TAG_U; GCss::FontStyleType fs = Css->FontStyle(); if (fs == GCss::FontStyleItalic) return TAG_I; } return CONTENT; } bool GRichTextPriv::TextBlock::ToHtml(GStream &s, GArray *Media) { s.Print("

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

\n"); return true; } bool GRichTextPriv::TextBlock::GetPosFromIndex(BlockCursor *Cursor) { if (!Cursor) return d->Error(_FL, "No cursor param."); if (LayoutDirty) { Cursor->Pos.ZOff(-1, -1); // This is valid behaviour... need to // wait for layout before getting cursor // position. return false; } int CharPos = 0; int LastY = 0; for (unsigned i=0; iPosOff; r.Offset(Pos.x1, Pos.y1); int FixX = 0; for (unsigned n=0; nStrs.Length(); n++) { DisplayStr *ds = tl->Strs[n]; ssize_t dsChars = ds->Chars; if ( Cursor->Offset >= CharPos && Cursor->Offset <= CharPos + dsChars && ( Cursor->LineHint < 0 || Cursor->LineHint == i ) ) { ssize_t CharOffset = Cursor->Offset - CharPos; if (CharOffset == 0) { // First char Cursor->Pos.x1 = r.x1 + FixedToInt(FixX); } else if (CharOffset == dsChars) { // Last char Cursor->Pos.x1 = r.x1 + FixedToInt(FixX + ds->FX()); } else { // In the middle somewhere... GAutoPtr Tmp = ds->Clone(0, CharOffset); // GDisplayString Tmp(ds->GetFont(), *ds, CharOffset); if (Tmp) Cursor->Pos.x1 = r.x1 + FixedToInt(FixX + Tmp->FX()); } Cursor->Pos.y1 = r.y1 + ds->OffsetY; Cursor->Pos.y2 = Cursor->Pos.y1 + ds->Y() - 1; Cursor->Pos.x2 = Cursor->Pos.x1 + 1; Cursor->Line.Set(Pos.x1, r.y1, Pos.x2, r.y2); return true; } FixX += ds->FX(); CharPos += ds->Chars; } if ( ( tl->Strs.Length() == 0 || i == Layout.Length() - 1 ) && Cursor->Offset == CharPos ) { // Cursor at the start of empty line. Cursor->Pos.x1 = r.x1; Cursor->Pos.x2 = Cursor->Pos.x1 + 1; Cursor->Pos.y1 = r.y1; Cursor->Pos.y2 = r.y2; Cursor->Line.Set(Pos.x1, r.y1, Pos.x2, r.y2); return true; } CharPos += tl->NewLine; LastY = tl->PosOff.y2; } if (Cursor->Offset == 0 && Len == 0) { Cursor->Pos.x1 = Pos.x1; Cursor->Pos.x2 = Pos.x1 + 1; Cursor->Pos.y1 = Pos.y1; Cursor->Pos.y2 = Pos.y2; Cursor->Line = Pos; return true; } return false; } bool GRichTextPriv::TextBlock::HitTest(HitTestResult &htr) { if (htr.In.y < Pos.y1 || htr.In.y > Pos.y2) return false; int CharPos = 0; for (unsigned i=0; iPosOff; r.Offset(Pos.x1, Pos.y1); bool Over = r.Overlap(htr.In.x, htr.In.y); bool OnThisLine = htr.In.y >= r.y1 && htr.In.y <= r.y2; if (OnThisLine && htr.In.x <= r.x1) { htr.Near = true; htr.Idx = CharPos; htr.LineHint = i; LgiAssert(htr.Idx <= Length()); return true; } int FixX = 0; int InputX = IntToFixed(htr.In.x - Pos.x1 - tl->PosOff.x1); for (unsigned n=0; nStrs.Length(); n++) { DisplayStr *ds = tl->Strs[n]; int dsFixX = ds->FX(); if (Over && InputX >= FixX && InputX < FixX + dsFixX) { int OffFix = InputX - FixX; int OffPx = FixedToInt(OffFix); ssize_t OffChar = ds->PosToIndex(OffPx, true); // d->DebugRects[0].Set(Pos.x1, r.y1, Pos.x1 + InputX+1, r.y2); htr.Blk = this; htr.Ds = ds; htr.Idx = CharPos + OffChar; htr.LineHint = i; LgiAssert(htr.Idx <= Length()); return true; } FixX += ds->FX(); CharPos += ds->Chars; } if (OnThisLine) { htr.Near = true; htr.Idx = CharPos; htr.LineHint = i; LgiAssert(htr.Idx <= Length()); return true; } CharPos += tl->NewLine; } return false; } void DrawDecor(GSurface *pDC, GRichTextPriv::DisplayStr *Ds, int Fx, int Fy, ssize_t Start, ssize_t Len) { // GColour Old = pDC->Colour(GColour::Red); GDisplayString ds1(Ds->GetFont(), (const char16*)(*Ds), Start); GDisplayString ds2(Ds->GetFont(), (const char16*)(*Ds), Start+Len); int x = (Fx >> GDisplayString::FShift); int y = (Fy >> GDisplayString::FShift) + (int)Ds->GetAscent() + 1; int End = x + ds2.X(); x += ds1.X(); pDC->Colour(GColour::Red); while (x < End) { pDC->Set(x, y+(x%2)); x++; } } bool Overlap(GSpellCheck::SpellingError *e, int start, ssize_t len) { if (!e) return false; if (start+len <= e->Start) return false; if (start >= e->End()) return false; return true; } void GRichTextPriv::TextBlock::DrawDisplayString(GSurface *pDC, DisplayStr *Ds, int &FixX, int FixY, GColour &Bk, int &Pos) { int OldX = FixX; // Paint the string itself... Ds->Paint(pDC, FixX, FixY, Bk); // Does the a spelling error overlap this string? ssize_t DsEnd = Pos + Ds->Chars; while (Overlap(SpErr, Pos, Ds->Chars)) { // Yes, work out the region of characters and paint the decor ssize_t Start = MAX(SpErr->Start, Pos); ssize_t Len = MIN(SpErr->End(), Pos + Ds->Chars) - Start; // Draw the decor for the error DrawDecor(pDC, Ds, OldX, FixY, Start - Pos, Len); if (SpErr->End() < DsEnd) { // Are there more errors? SpErr = SpellingErrors.AddressOf(++PaintErrIdx); } else break; } while (SpErr && SpErr->End() < DsEnd) { // Are there more errors? SpErr = SpellingErrors.AddressOf(++PaintErrIdx); } Pos += Ds->Chars; } void GRichTextPriv::TextBlock::OnPaint(PaintContext &Ctx) { int CharPos = 0; int EndPoints = 0; ssize_t EndPoint[2] = {-1, -1}; int CurEndPoint = 0; if (Cursors > 0 && Ctx.Select) { // Selection end point checks... if (Ctx.Cursor && Ctx.Cursor->Blk == this) EndPoint[EndPoints++] = Ctx.Cursor->Offset; if (Ctx.Select && Ctx.Select->Blk == this) EndPoint[EndPoints++] = Ctx.Select->Offset; // Sort the end points if (EndPoints > 1 && EndPoint[0] > EndPoint[1]) { ssize_t ep = EndPoint[0]; EndPoint[0] = EndPoint[1]; EndPoint[1] = ep; } } // Paint margins, borders and padding... GRect r = Pos; r.x1 -= Margin.x1; r.y1 -= Margin.y1; r.x2 -= Margin.x2; r.y2 -= Margin.y2; GCss::ColorDef BorderStyle; if (Style) BorderStyle = Style->BorderLeft().Color; GColour BorderCol(222, 222, 222); if (BorderStyle.Type == GCss::ColorRgb) BorderCol.Set(BorderStyle.Rgb32, 32); Ctx.DrawBox(r, Margin, Ctx.Colours[Unselected].Back); Ctx.DrawBox(r, Border, BorderCol); Ctx.DrawBox(r, Padding, Ctx.Colours[Unselected].Back); int CurY = Pos.y1; PaintErrIdx = 0; SpErr = SpellingErrors.AddressOf(PaintErrIdx); for (unsigned i=0; iPosOff; LinePos.Offset(Pos.x1, Pos.y1); if (Line->PosOff.X() < Pos.X()) { Ctx.pDC->Colour(Ctx.Colours[Unselected].Back); Ctx.pDC->Rectangle(LinePos.x2, LinePos.y1, Pos.x2, LinePos.y2); } int FixX = IntToFixed(LinePos.x1); if (CurY < LinePos.y1) { // Fill padded area... Ctx.pDC->Colour(Ctx.Colours[Unselected].Back); Ctx.pDC->Rectangle(Pos.x1, CurY, Pos.x2, LinePos.y1 - 1); } CurY = LinePos.y1; GFont *Fnt = NULL; #if DEBUG_NUMBERED_LAYOUTS GString s; s.Printf("%i", Ctx.Index); Ctx.Index++; #endif for (unsigned n=0; nStrs.Length(); n++) { DisplayStr *Ds = Line->Strs[n]; GFont *DsFnt = Ds->GetFont(); ColourPair &Cols = Ds->Src->Colours; if (DsFnt && DsFnt != Fnt) { Fnt = DsFnt; Fnt->Transparent(false); } // If the current text part doesn't cover the full line height we have to // fill in the rest here... if (Ds->Y() < Line->PosOff.Y()) { Ctx.pDC->Colour(Ctx.Colours[Unselected].Back); int CurX = FixedToInt(FixX); if (Ds->OffsetY > 0) Ctx.pDC->Rectangle(CurX, CurY, CurX+Ds->X(), CurY+Ds->OffsetY-1); int DsY2 = Ds->OffsetY + Ds->Y(); if (DsY2 < Pos.Y()) Ctx.pDC->Rectangle(CurX, CurY+DsY2, CurX+Ds->X(), Pos.y2); } // Check for selection changes... int FixY = IntToFixed(CurY + Ds->OffsetY); #if DEBUG_OUTLINE_CUR_STYLE_TEXT GRect r(0, 0, -1, -1); if (Ctx.Cursor->Blk == (Block*)this) { GArray CurStyle; if (GetTextAt(Ctx.Cursor->Offset, CurStyle) && Ds->Src == CurStyle.First()) { r.ZOff(Ds->X()-1, Ds->Y()-1); r.Offset(FixedToInt(FixX), FixedToInt(FixY)); } } #endif if (CurEndPoint < EndPoints && EndPoint[CurEndPoint] >= CharPos && EndPoint[CurEndPoint] <= CharPos + Ds->Chars) { // Process string into parts based on the selection boundaries ssize_t Ch = EndPoint[CurEndPoint] - CharPos; int TmpPos = CharPos; GAutoPtr ds1 = Ds->Clone(0, Ch); // First part... GColour Bk = Ctx.Type == Unselected && Cols.Back.IsValid() ? Cols.Back : Ctx.Back(); if (DsFnt) DsFnt->Colour(Ctx.Type == Unselected && Cols.Fore.IsValid() ? Cols.Fore : Ctx.Fore(), Bk); if (ds1) DrawDisplayString(Ctx.pDC, ds1, FixX, FixY, Bk, TmpPos); Ctx.Type = Ctx.Type == Selected ? Unselected : Selected; CurEndPoint++; // Is there 3 parts? // // This happens when the selection starts and end in the one string. // // The alternative is that it starts or ends in the strings but the other // end point is in a different string. In which case there is only 2 strings // to draw. if (CurEndPoint < EndPoints && EndPoint[CurEndPoint] >= CharPos && EndPoint[CurEndPoint] <= CharPos + Ds->Chars) { // Yes.. ssize_t Ch2 = EndPoint[CurEndPoint] - CharPos; // Part 2 GAutoPtr ds2 = Ds->Clone(Ch, Ch2 - Ch); GColour Bk = Ctx.Type == Unselected && Cols.Back.IsValid() ? Cols.Back : Ctx.Back(); if (DsFnt) DsFnt->Colour(Ctx.Type == Unselected && Cols.Fore.IsValid() ? Cols.Fore : Ctx.Fore(), Bk); if (ds2) DrawDisplayString(Ctx.pDC, ds2, FixX, FixY, Bk, TmpPos); Ctx.Type = Ctx.Type == Selected ? Unselected : Selected; CurEndPoint++; // Part 3 if (Ch2 < Ds->Length()) { GAutoPtr ds3 = Ds->Clone(Ch2); Bk = Ctx.Type == Unselected && Cols.Back.IsValid() ? Cols.Back : Ctx.Back(); if (DsFnt) DsFnt->Colour(Ctx.Type == Unselected && Cols.Fore.IsValid() ? Cols.Fore : Ctx.Fore(), Bk); if (ds3) DrawDisplayString(Ctx.pDC, ds3, FixX, FixY, Bk, TmpPos); } } else if (Ch < Ds->Chars) { // No... draw 2nd part GAutoPtr ds2 = Ds->Clone(Ch); GColour Bk = Ctx.Type == Unselected && Cols.Back.IsValid() ? Cols.Back : Ctx.Back(); if (DsFnt) DsFnt->Colour(Ctx.Type == Unselected && Cols.Fore.IsValid() ? Cols.Fore : Ctx.Fore(), Bk); if (ds2) DrawDisplayString(Ctx.pDC, ds2, FixX, FixY, Bk, TmpPos); } } else { // No selection changes... draw the whole string GColour Bk = Ctx.Type == Unselected && Cols.Back.IsValid() ? Cols.Back : Ctx.Back(); if (DsFnt) DsFnt->Colour(Ctx.Type == Unselected && Cols.Fore.IsValid() ? Cols.Fore : Ctx.Fore(), Bk); #if DEBUG_OUTLINE_CUR_DISPLAY_STR int OldFixX = FixX; #endif int TmpPos = CharPos; DrawDisplayString(Ctx.pDC, Ds, FixX, FixY, Bk, TmpPos); #if DEBUG_OUTLINE_CUR_DISPLAY_STR if (Ctx.Cursor->Blk == (Block*)this && Ctx.Cursor->Offset >= CharPos && Ctx.Cursor->Offset < CharPos + Ds->Chars) { GRect r(0, 0, Ds->X()-1, Ds->Y()-1); r.Offset(FixedToInt(OldFixX), FixedToInt(FixY)); Ctx.pDC->Colour(GColour::Red); Ctx.pDC->Box(&r); } #endif } #if DEBUG_OUTLINE_CUR_STYLE_TEXT if (r.Valid()) { Ctx.pDC->Colour(GColour(192, 192, 192)); Ctx.pDC->LineStyle(GSurface::LineDot); Ctx.pDC->Box(&r); Ctx.pDC->LineStyle(GSurface::LineSolid); } #endif CharPos += Ds->Chars; } if (Line->Strs.Length() == 0) { if (CurEndPoint < EndPoints && EndPoint[CurEndPoint] == CharPos) { Ctx.Type = Ctx.Type == Selected ? Unselected : Selected; CurEndPoint++; } } if (Ctx.Type == Selected) { // Draw new line int x1 = FixedToInt(FixX); FixX += IntToFixed(5); int x2 = FixedToInt(FixX); Ctx.pDC->Colour(Ctx.Colours[Selected].Back); Ctx.pDC->Rectangle(x1, LinePos.y1, x2, LinePos.y2); } Ctx.pDC->Colour(Ctx.Colours[Unselected].Back); Ctx.pDC->Rectangle(FixedToInt(FixX), LinePos.y1, Pos.x2, LinePos.y2); #if DEBUG_NUMBERED_LAYOUTS GDisplayString Ds(SysFont, s); SysFont->Colour(GColour::Green, GColour::White); SysFont->Transparent(false); Ds.Draw(Ctx.pDC, LinePos.x1, LinePos.y1); /* Ctx.pDC->Colour(GColour::Blue); Ctx.pDC->Line(LinePos.x1, LinePos.y1,LinePos.x2,LinePos.y2); */ #endif CurY = LinePos.y2 + 1; CharPos += Line->NewLine; } if (CurY < Pos.y2) { // Fill padded area... Ctx.pDC->Colour(Ctx.Colours[Unselected].Back); Ctx.pDC->Rectangle(Pos.x1, CurY, Pos.x2, Pos.y2); } if (Ctx.Cursor && Ctx.Cursor->Blk == this && Ctx.Cursor->Blink && d->View->Focus()) { Ctx.pDC->Colour(CursorColour); if (Ctx.Cursor->Pos.Valid()) Ctx.pDC->Rectangle(&Ctx.Cursor->Pos); else Ctx.pDC->Rectangle(Pos.x1, Pos.y1, Pos.x1, Pos.y2); } #if 0 // def _DEBUG if (Ctx.Select && Ctx.Select->Blk == this) { Ctx.pDC->Colour(GColour(255, 0, 0)); Ctx.pDC->Rectangle(&Ctx.Select->Pos); } #endif } bool GRichTextPriv::TextBlock::OnLayout(Flow &flow) { if (Pos.X() == flow.X() && !LayoutDirty) { // Adjust position to match the flow, even if we are not dirty Pos.Offset(0, flow.CurY - Pos.y1); flow.CurY = Pos.y2 + 1; return true; } LayoutDirty = false; Layout.DeleteObjects(); flow.Left += Margin.x1; flow.Right -= Margin.x2; flow.CurY += Margin.y1; Pos.x1 = flow.Left; Pos.y1 = flow.CurY; Pos.x2 = flow.Right; Pos.y2 = flow.CurY-1; // Start with a 0px height. flow.Left += Border.x1 + Padding.x1; flow.Right -= Border.x2 + Padding.x2; flow.CurY += Border.y1 + Padding.y1; int FixX = 0; // Current x offset (fixed point) on the current line GAutoPtr CurLine(new TextLine(flow.Left - Pos.x1, flow.X(), flow.CurY - Pos.y1)); if (!CurLine) return flow.d->Error(_FL, "alloc failed."); int LayoutSize = 0; int TextSize = 0; for (unsigned i=0; iGetStyle(); LgiAssert(t->Length() >= 0); TextSize += t->Length(); if (t->Length() == 0) continue; int AvailableX = Pos.X() - CurLine->PosOff.x1; if (AvailableX < 0) AvailableX = 1; // Get the font for 't' GFont *f = flow.d->GetFont(t->GetStyle()); if (!f) return flow.d->Error(_FL, "font creation failed."); GCss::WordWrapType WrapType = tstyle ? tstyle->WordWrap() : GCss::WrapNormal; uint32 *sStart = t->At(0); uint32 *sEnd = sStart + t->Length(); for (unsigned Off = 0; Off < t->Length(); ) { // How much of 't' is on the same line? uint32 *s = sStart + Off; #if DEBUG_LAYOUT LgiTrace("Txt[%i][%i]: FixX=%i, Txt='%.*S'\n", i, Off, FixX, t->Length() - Off, s); #endif if (*s == '\n') { // New line handling... Off++; CurLine->PosOff.x2 = CurLine->PosOff.x1 + FixedToInt(FixX) - 1; FixX = 0; CurLine->LayoutOffsets(f->GetHeight()); Pos.y2 = MAX(Pos.y2, Pos.y1 + CurLine->PosOff.y2); CurLine->NewLine = 1; LayoutSize += CurLine->Length(); #if DEBUG_LAYOUT LgiTrace("\tNewLineChar, LayoutSize=%i, TextSize=%i\n", LayoutSize, TextSize); #endif Layout.Add(CurLine.Release()); CurLine.Reset(new TextLine(flow.Left - Pos.x1, flow.X(), Pos.Y())); if (Off == t->Length()) { // Empty line at the end of the StyleText const uint32 Empty[] = {0}; CurLine->Strs.Add(new DisplayStr(t, f, Empty, 0, flow.pDC)); } continue; } uint32 *e = s; /* printf("e=%i sEnd=%i len=%i\n", (int)(e - sStart), (int)(sEnd - sStart), (int)t->Length()); */ while (e < sEnd && *e != '\n') e++; // Add 't' to current line ssize_t Chars = MIN(1024, (int) (e - s)); GAutoPtr Ds ( t->Emoji ? new EmojiDisplayStr(t, d->GetEmojiImage(), f, s, Chars) : new DisplayStr(t, f, s, Chars, flow.pDC) ); if (!Ds) return flow.d->Error(_FL, "display str creation failed."); if (WrapType != GCss::WrapNone && FixX + Ds->FX() > IntToFixed(AvailableX)) { #if DEBUG_LAYOUT LgiTrace("\tNeedToWrap: %i, %i + %i > %i\n", WrapType, FixX, Ds->FX(), IntToFixed(AvailableX)); #endif // Wrap the string onto the line... int AvailablePx = AvailableX - FixedToInt(FixX); ssize_t FitChars = Ds->PosToIndex(AvailablePx, false); if (FitChars < 0) { #if DEBUG_LAYOUT LgiTrace("\tFitChars error: %i\n", FitChars); #endif flow.d->Error(_FL, "PosToIndex(%i) failed.", AvailablePx); LgiAssert(0); } else { // Wind back to the last break opportunity ssize_t ch = 0; for (ch = FitChars; ch > 0; ch--) { if (IsWordBreakChar(s[ch-1])) break; } #if DEBUG_LAYOUT LgiTrace("\tWindBack: %i\n", (int)ch); #endif if (ch == 0) { // One word minimum per line for (ch = 1; ch < Chars; ch++) { if (IsWordBreakChar(s[ch])) break; } Chars = ch; } else if (ch > (FitChars >> 2)) Chars = ch; else Chars = FitChars; // Create a new display string of the right size... if ( ! Ds.Reset ( t->Emoji ? new EmojiDisplayStr(t, d->GetEmojiImage(), f, s, Chars) : new DisplayStr(t, f, s, Chars, flow.pDC) ) ) return flow.d->Error(_FL, "failed to create wrapped display str."); // Finish off line CurLine->PosOff.x2 = CurLine->PosOff.x1 + FixedToInt(FixX + Ds->FX()) - 1; CurLine->Strs.Add(Ds.Release()); CurLine->LayoutOffsets(d->Font->GetHeight()); Pos.y2 = MAX(Pos.y2, Pos.y1 + CurLine->PosOff.y2); LayoutSize += CurLine->Length(); Layout.Add(CurLine.Release()); #if DEBUG_LAYOUT LgiTrace("\tWrap, LayoutSize=%i TextSize=%i\n", LayoutSize, TextSize); #endif // New line... CurLine.Reset(new TextLine(flow.Left - Pos.x1, flow.X(), Pos.Y())); FixX = 0; Off += Chars; continue; } } else { FixX += Ds->FX(); } if (!Ds) break; CurLine->PosOff.x2 = CurLine->PosOff.x1 + FixedToInt(FixX) - 1; CurLine->Strs.Add(Ds.Release()); Off += Chars; } } if (Txt.Length() == 0) { // Empty node case int y = Pos.y1 + flow.d->View->GetFont()->GetHeight() - 1; CurLine->PosOff.y2 = Pos.y2 = MAX(Pos.y2, y); LayoutSize += CurLine->Length(); Layout.Add(CurLine.Release()); } if (CurLine && CurLine->Strs.Length() > 0) { GFont *f = d->View ? d->View->GetFont() : SysFont; CurLine->LayoutOffsets(f->GetHeight()); Pos.y2 = MAX(Pos.y2, Pos.y1 + CurLine->PosOff.y2); LayoutSize += CurLine->Length(); #if DEBUG_LAYOUT LgiTrace("\tRemaining, LayoutSize=%i, TextSize=%i\n", LayoutSize, TextSize); #endif Layout.Add(CurLine.Release()); } LgiAssert(LayoutSize == Len); flow.CurY = Pos.y2 + 1 + Margin.y2 + Border.y2 + Padding.y2; flow.Left -= Margin.x1 + Border.x1 + Padding.x1; flow.Right += Margin.x2 + Border.x2 + Padding.x2; return true; } ssize_t GRichTextPriv::TextBlock::GetTextAt(ssize_t Offset, GArray &Out) { if (Txt.Length() == 0) return 0; StyleText **t = &Txt[0]; StyleText **e = t + Txt.Length(); Out.Length(0); uint32 Pos = 0; while (t < e) { ssize_t Len = (*t)->Length(); if (Offset >= Pos && Offset <= Pos + Len) Out.Add(*t); t++; Pos += Len; } LgiAssert(Pos == Len); return Out.Length(); } bool GRichTextPriv::TextBlock::IsValid() { int TxtLen = 0; for (unsigned i = 0; i < Txt.Length(); i++) { StyleText *t = Txt[i]; TxtLen += t->Length(); for (unsigned n = 0; n < t->Length(); n++) { if ((*t)[n] == 0) { LgiAssert(0); return false; } } } if (Len != TxtLen) return d->Error(_FL, "Txt.Len vs Len mismatch: %i, %i.", TxtLen, Len); return true; } int GRichTextPriv::TextBlock::GetLines() { return (int)Layout.Length(); } bool GRichTextPriv::TextBlock::OffsetToLine(ssize_t Offset, int *ColX, GArray *LineY) { if (LayoutDirty) return false; if (LineY) LineY->Length(0); if (Offset <= 0) { if (ColX) *ColX = 0; if (LineY) LineY->Add(0); return true; } bool Found = false; int Pos = 0; for (unsigned i=0; iLength(); if (Offset >= Pos && Offset <= Pos + Len - tl->NewLine) { if (ColX) *ColX = (int)(Offset - Pos); if (LineY) LineY->Add(i); Found = true; } Pos += Len; } return Found; } int GRichTextPriv::TextBlock::LineToOffset(int Line) { if (LayoutDirty) return -1; if (Line <= 0) return 0; int Pos = 0; for (unsigned i=0; iLength(); if (i == Line) return Pos; Pos = Len; } return (int)Length(); } bool GRichTextPriv::TextBlock::PreEdit(Transaction *Trans) { if (Trans) { bool HasThisBlock = false; for (unsigned i=0; iChanges.Length(); i++) { CompleteTextBlockState *c = dynamic_cast(Trans->Changes[i]); if (c) { if (c->Uid == BlockUid) { HasThisBlock = true; break; } } } if (!HasThisBlock) Trans->Add(new CompleteTextBlockState(d, this)); } return true; } ssize_t GRichTextPriv::TextBlock::DeleteAt(Transaction *Trans, ssize_t BlkOffset, ssize_t Chars, GArray *DeletedText) { ssize_t Pos = 0; ssize_t Deleted = 0; PreEdit(Trans); for (unsigned i=0; i 0; i++) { StyleText *t = Txt[i]; ssize_t TxtOffset = BlkOffset - Pos; if (TxtOffset >= 0 && TxtOffset < (int)t->Length()) { ssize_t MaxChars = t->Length() - TxtOffset; ssize_t Remove = MIN(Chars, MaxChars); if (Remove <= 0) return 0; ssize_t Remaining = MaxChars - Remove; ssize_t NewLen = t->Length() - Remove; if (DeletedText) { DeletedText->Add(t->At(TxtOffset), Remove); } if (Remaining > 0) { // Copy down memmove(&(*t)[TxtOffset], &(*t)[TxtOffset + Remove], Remaining * sizeof(uint32)); (*t)[NewLen] = 0; } // Change length if (NewLen == 0) { // Remove run completely // LgiTrace("DelRun %p/%i '%.*S'\n", t, i, t->Length(), &(*t)[0]); Txt.DeleteAt(i--, true); DeleteObj(t); } else { // Shorten run t->Length(NewLen); // LgiTrace("ShortenRun %p/%i '%.*S'\n", t, i, t->Length(), &(*t)[0]); } LayoutDirty = true; Chars -= Remove; Len -= Remove; Deleted += Remove; } if (t) Pos += t->Length(); } if (Deleted > 0) { + // Adjust start of existing spelling styles + GRange r(BlkOffset, Deleted); + for (unsigned i=0; i r) + { + Err.Start -= Deleted; + } + } + LayoutDirty = true; UpdateSpellingAndLinks(Trans, GRange(BlkOffset, 0)); } IsValid(); return Deleted; } GMessage::Result GRichTextPriv::TextBlock::OnEvent(GMessage *Msg) { switch (Msg->Msg()) { case M_COMMAND: { GSpellCheck::SpellingError *e = SpellingErrors.AddressOf(ClickErrIdx); if (e) { // Replacing text with spell check suggestion: int i = (int)Msg->A() - SPELLING_BASE; if (i >= 0 && i < (int)e->Suggestions.Length()) { + int Start = e->Start; GString s = e->Suggestions[i]; AutoTrans t(new GRichTextPriv::Transaction); // Delete the old text... - DeleteAt(t, e->Start, e->Len); + DeleteAt(t, Start, e->Len); // 'e' might disappear here // Insert the new text.... GAutoPtr u((uint32*)LgiNewConvertCp("utf-32", s, "utf-8")); - AddText(t, e->Start, u.Get(), Strlen(u.Get())); + AddText(t, Start, u.Get(), Strlen(u.Get())); d->AddTrans(t); + return true; } } break; } } - return 0; + return false; } bool GRichTextPriv::TextBlock::AddText(Transaction *Trans, ssize_t AtOffset, const uint32 *InStr, ssize_t InChars, GNamedStyle *Style) { if (!InStr) return d->Error(_FL, "No input text."); if (InChars < 0) InChars = Strlen(InStr); PreEdit(Trans); GArray EmojiIdx; EmojiIdx.Length(InChars); for (int i=0; i= 0 ? AtOffset : Len; int Chars = 0; // Length of run to insert int Pos = 0; // Current character position in this block uint32 TxtIdx = 0; // Index into Txt array for (int i = 0; i < InChars; i += Chars) { // Work out the run of chars that are either // emoji or not emoji... bool IsEmoji = EmojiIdx[i] >= 0; Chars = 1; for (int n = i + 1; n < InChars; n++) { if ( IsEmoji ^ (EmojiIdx[n] >= 0) ) break; Chars++; } // Now process 'Char' chars const uint32 *Str = InStr + i; if (AtOffset >= 0 && Txt.Length() > 0) { // Seek further into block? while ( Pos < AtOffset && TxtIdx < Txt.Length()) { StyleText *t = Txt[TxtIdx]; ssize_t Len = t->Length(); if (AtOffset <= Pos + Len) break; Pos += Len; TxtIdx++; } StyleText *t = TxtIdx >= Txt.Length() ? Txt.Last() : Txt[TxtIdx]; ssize_t TxtLen = t->Length(); if (AtOffset >= Pos && AtOffset <= Pos + TxtLen) { ssize_t StyleOffset = AtOffset - Pos; // Offset into 't' in which we need to potentially break the style // to insert the new content. bool UrlEdge = t->Element == TAG_A && *Str == '\n'; if (!Style && IsEmoji == t->Emoji && !UrlEdge) { // Insert/append to existing text run ssize_t After = t->Length() - StyleOffset; ssize_t NewSz = t->Length() + Chars; t->Length(NewSz); uint32 *c = &t->First(); LOG_FN("TextBlock(%i)::Add(%i,%i,%s)::Append StyleOffset=%i, After=%i\n", GetUid(), AtOffset, InChars, Style?Style->Name.Get():NULL, StyleOffset, After); // Do we need to move characters up to make space? if (After > 0) memmove(c + StyleOffset + Chars, c + StyleOffset, After * sizeof(*c)); // Insert the new string... memcpy(c + StyleOffset, Str, Chars * sizeof(*c)); Len += Chars; AtOffset += Chars; } else { // Break into 2 runs, with the new text in the middle... // Insert the new text+style StyleText *Run = new StyleText(Str, Chars, Style); if (!Run) return false; Run->Emoji = IsEmoji; /* This following code could be wrong. In terms of test cases I fixed this: A) Starting with basic empty email + signature. Insert a URL at the very start. Then hit enter. Buf: \n inserted BEFORE the URL. Changed the condition to 'StyleOffset != 0' rather than 'TxtIdx != 0' Potentially other test cases could exhibit bugs that need to be added here. */ if (StyleOffset) Txt.AddAt(++TxtIdx, Run); else Txt.AddAt(TxtIdx++, Run); //////////////////////////////////// Pos += StyleOffset; // We are skipping over the run at 'TxtIdx', update pos LOG_FN("TextBlock(%i)::Add(%i,%i,%s)::Insert StyleOffset=%i\n", GetUid(), AtOffset, InChars, Style?Style->Name.Get():NULL, StyleOffset); if (StyleOffset < TxtLen) { // Insert the 2nd part of the string Run = new StyleText(t->At(StyleOffset), TxtLen - StyleOffset, t->GetStyle()); if (!Run) return false; Pos += Chars; Txt.AddAt(++TxtIdx, Run); // Now truncate the existing text.. t->Length(StyleOffset); } Len += Chars; AtOffset += Chars; } Str = NULL; } } if (Str) { // At the end StyleText *Last = Txt.Length() > 0 ? Txt.Last() : NULL; if (Last && Last->GetStyle() == Style && IsEmoji == Last->Emoji) { if (Last->Add((uint32*)Str, Chars)) { Len += Chars; if (AtOffset >= 0) AtOffset += Chars; } } else { StyleText *Run = new StyleText(Str, Chars, Style); if (!Run) return false; Run->Emoji = IsEmoji; Txt.Add(Run); Len += Chars; if (AtOffset >= 0) AtOffset += Chars; } } } + // Push existing spelling styles along + for (unsigned i=0; i= InitialOffset) + Err.Start += InChars; + } + + // Update layout and styling LayoutDirty = true; IsValid(); UpdateSpellingAndLinks(Trans, GRange(InitialOffset, InChars)); return true; } bool GRichTextPriv::TextBlock::OnDictionary(Transaction *Trans) { UpdateSpellingAndLinks(Trans, GRange(0, Length())); return true; } #define IsUrlWordChar(t) \ (((t) > ' ') && !strchr("./:", (t))) template bool _ScanWord(Char *&t, Char *e) { if (!IsUrlWordChar(*t)) return false; Char *s = t; while (t < e && IsUrlWordChar(*t)) t++; return t > s; } bool IsBracketed(int s, int e) { if (s == '(' && e == ')') return true; if (s == '[' && e == ']') return true; if (s == '{' && e == '}') return true; if (s == '<' && e == '>') return true; return false; } #define ScanWord() \ if (t >= e || !_ScanWord(t, e)) return false #define ScanChar(ch) \ if (t >= e || *t != ch) \ return false; \ t++ template bool DetectUrl(Char *t, ssize_t &len) { #ifdef _DEBUG GString str(t, len); //char *ss = str; #endif Char *s = t; Char *e = t + len; ScanWord(); // Protocol ScanChar(':'); ScanChar('/'); ScanChar('/'); ScanWord(); // Host name or username.. if (t < e && *t == ':') { t++; _ScanWord(t, e); // Don't return if missing... password optional ScanChar('@'); ScanWord(); // First part of host name... } // Rest of host name while (t < e && *t == '.') { t++; if (t < e && IsUrlWordChar(*t)) ScanWord(); // Second part of host name } if (t < e && *t == ':') // Port number { t++; ScanWord(); } while (t < e && strchr("/.:", *t)) // Path { t++; if (t < e && (IsUrlWordChar(*t) || *t == ':')) ScanWord(); } if (strchr("!.", t[-1])) t--; len = t - s; return true; } +int ErrSort(GSpellCheck::SpellingError *a, GSpellCheck::SpellingError *b) +{ + return (int) (a->Start - b->Start); +} + +void GRichTextPriv::TextBlock::SetSpellingErrors(GArray &Errors, GRange r) +{ + // LgiTrace("%s:%i - SetSpellingErrors " LPrintfSSizeT ", " LPrintfSSizeT ":" LPrintfSSizeT "\n", _FL, Errors.Length(), r.Start, r.End()); + + // Delete any errors overlapping 'r' + for (unsigned i=0; i Text; if (!CopyAt(0, Length(), &Text)) return; // Spelling... if (d->SpellCheck && d->SpellDictionaryLoaded) { - GString s(&Text[0], Text.Length()); - d->SpellCheck->Check(d->View->AddDispatch(), s, r.Start, (GRichTextPriv::Block*)this); + GRange Rgn = r; + while (Rgn.Start > 0 && + IsWordChar(Text[Rgn.Start-1])) + { + Rgn.Start--; + Rgn.Len++; + } + while (Rgn.End() < Len && + IsWordChar(Text[Rgn.End()])) + { + Rgn.Len++; + } + + GString s(Text.AddressOf(Rgn.Start), Rgn.Len); + GArray Params; + Params[SpellBlockPtr] = (Block*)this; + + // LgiTrace("%s:%i - Check(%s) " LPrintfSSizeT ":" LPrintfSSizeT "\n", _FL, s.Get(), Rgn.Start, Rgn.End()); + + d->SpellCheck->Check(d->View->AddDispatch(), s, Rgn.Start, Rgn.Len, &Params); } // Link detection... // Extend the range to include whole words while (r.Start > 0 && !IsWhiteSpace(Text[r.Start])) { r.Start--; r.Len++; } while (r.End() < Text.Length() && !IsWhiteSpace(Text[r.End()])) r.Len++; // Create array of words... GArray Words; bool Ws = true; for (int i = 0; i < r.Len; i++) { bool w = IsWhiteSpace(Text[r.Start + i]); if (w ^ Ws) { Ws = w; if (!w) { GRange &w = Words.New(); w.Start = r.Start + i; // printf("StartWord=%i, %i\n", w.Start, w.Len); } else if (Words.Length() > 0) { GRange &w = Words.Last(); w.Len = r.Start + i - w.Start; // printf("EndWord=%i, %i\n", w.Start, w.Len); } } } if (!Ws && Words.Length() > 0) { GRange &w = Words.Last(); w.Len = r.Start + r.Len - w.Start; // printf("TermWord=%i, %i Words=%i\n", w.Start, w.Len, Words.Length()); } // For each word in the range of text for (unsigned i = 0; iMakeLink(this, w.Start, w.Len, Link); // Also unlink any of the word after the URL if (w.End() < Words[i].End()) { GCss Style; ChangeStyle(Trans, w.End(), Words[i].End() - w.End(), &Style, false); } } } } -int ErrSort(GSpellCheck::SpellingError *a, GSpellCheck::SpellingError *b) -{ - return (int) (a->Start - b->Start); -} - bool GRichTextPriv::TextBlock::StripLast(Transaction *Trans, const char *Set) { if (Txt.Length() == 0) return false; StyleText *l = Txt.Last(); if (!l || l->Length() <= 0) return false; if (!strchr(Set, l->Last())) return false; PreEdit(Trans); if (!l->PopLast()) return false; LayoutDirty = true; Len--; return true; } bool GRichTextPriv::TextBlock::DoContext(GSubMenu &s, GdcPt2 Doc, ssize_t Offset, bool Spelling) { if (Spelling) { // Is there a spelling error at 'Offset'? for (unsigned i=0; i= e.Start && Offset < e.End()) { ClickErrIdx = i; if (e.Suggestions.Length()) { GSubMenu *Sp = s.AppendSub("Spelling"); if (Sp) { s.AppendSeparator(); for (unsigned n=0; nAppendItem(e.Suggestions[n], SPELLING_BASE+n); } // else printf("%s:%i - No sub menu.\n", _FL); } // else printf("%s:%i - No Suggestion.\n", _FL); break; } // else printf("%s:%i - Outside area, Offset=%i e=%i,%i.\n", _FL, Offset, e.Start, e.End()); } } // else printf("%s:%i - No Spelling.\n", _FL); return true; } bool GRichTextPriv::TextBlock::IsEmptyLine(BlockCursor *Cursor) { if (!Cursor) return false; TextLine *Line = Layout.AddressOf(Cursor->LineHint) ? Layout[Cursor->LineHint] : NULL; if (!Line) return false; int LineLen = Line->Length(); return LineLen == 0; } GRichTextPriv::Block *GRichTextPriv::TextBlock::Clone() { return new TextBlock(this); } -void GRichTextPriv::TextBlock::SetSpellingErrors(GArray &Errors) -{ - SpellingErrors = Errors; - SpellingErrors.Sort(ErrSort); -} - ssize_t GRichTextPriv::TextBlock::CopyAt(ssize_t Offset, ssize_t Chars, GArray *Text) { if (!Text) return 0; if (Chars < 0) Chars = Length() - Offset; int Pos = 0; for (unsigned i=0; i= Pos && Offset < Pos + (int)t->Length()) { ssize_t Skip = Offset - Pos; ssize_t Remain = t->Length() - Skip; ssize_t Cp = MIN(Chars, Remain); Text->Add(&(*t)[Skip], Cp); Chars -= Cp; Offset += Cp; } Pos += t->Length(); } return Text->Length(); } ssize_t GRichTextPriv::TextBlock::FindAt(ssize_t StartIdx, const uint32 *Str, GFindReplaceCommon *Params) { if (!Str || !Params) return -1; size_t InLen = Strlen(Str); bool Match; int CharPos = 0; for (unsigned i=0; iFirst(); uint32 *e = s + t->Length(); if (Params->MatchCase) { for (uint32 *c = s; c < e; c++) { if (*c == *Str) { if (c + InLen <= e) Match = !Strncmp(c, Str, InLen); else { GArray tmp; if (CopyAt(CharPos + (c - s), InLen, &tmp) && tmp.Length() == InLen) Match = !Strncmp(&tmp[0], Str, InLen); else Match = false; } if (Match) return CharPos + (c - s); } } } else { uint32 l = ToLower(*Str); for (uint32 *c = s; c < e; c++) { if (ToLower(*c) == l) { if (c + InLen <= e) Match = !Strnicmp(c, Str, InLen); else { GArray tmp; if (CopyAt(CharPos + (c - s), InLen, &tmp) && tmp.Length() == InLen) Match = !Strnicmp(&tmp[0], Str, InLen); else Match = false; } if (Match) return CharPos + (c - s); } } } CharPos += t->Length(); } return -1; } bool GRichTextPriv::TextBlock::DoCase(Transaction *Trans, ssize_t StartIdx, ssize_t Chars, bool Upper) { GRange Blk(0, Len); GRange Inp(StartIdx, Chars < 0 ? Len - StartIdx : Chars); GRange Change = Blk.Overlap(Inp); PreEdit(Trans); GRange Run(0, 0); bool Changed = false; for (unsigned i=0; iLength(); GRange Edit = Run.Overlap(Change); if (Edit.Len > 0) { uint32 *s = st->At(Edit.Start - Run.Start); for (int n=0; n= 'a' && s[n] <= 'z') s[n] = s[n] - 'a' + 'A'; } else { if (s[n] >= 'A' && s[n] <= 'Z') s[n] = s[n] - 'A' + 'a'; } } Changed = true; } Run.Start += Run.Len; } LayoutDirty |= Changed; return Changed; } GRichTextPriv::Block *GRichTextPriv::TextBlock::Split(Transaction *Trans, ssize_t AtOffset) { if (AtOffset < 0 || AtOffset >= Len) return NULL; GRichTextPriv::TextBlock *After = new GRichTextPriv::TextBlock(d); if (!After) { d->Error(_FL, "Alloc Err"); return NULL; } After->SetStyle(GetStyle()); int Pos = 0; unsigned i; for (i=0; iLength(); if (AtOffset >= Pos && AtOffset < Pos + StLen) { ssize_t StOff = AtOffset - Pos; if (StOff > 0) { // Split the text into 2 blocks... uint32 *t = St->At(StOff); ssize_t remaining = St->Length() - StOff; StyleText *AfterText = new StyleText(t, remaining, St->GetStyle()); if (!AfterText) { d->Error(_FL, "Alloc Err"); return NULL; } St->Length(StOff); i++; Len = Pos + StOff; After->Txt.Add(AfterText); After->Len += AfterText->Length(); } else { Len = Pos; } break; } Pos += StLen; } while (i < Txt.Length()) { StyleText *St = Txt[i]; Txt.DeleteAt(i, true); After->Txt.Add(St); After->Len += St->Length(); } LayoutDirty = true; After->LayoutDirty = true; return After; } void GRichTextPriv::TextBlock::IncAllStyleRefs() { if (Style) Style->RefCount++; for (unsigned i=0; iGetStyle(); if (s) s->RefCount++; } } bool GRichTextPriv::TextBlock::ChangeStyle(Transaction *Trans, ssize_t Offset, ssize_t Chars, GCss *Style, bool Add) { if (!Style) return d->Error(_FL, "No style."); if (Offset < 0 || Offset >= Len) return true; if (Chars < 0) Chars = Len; if (Trans) Trans->Add(new CompleteTextBlockState(d, this)); int CharPos = 0; ssize_t RestyleEnd = Offset + Chars; for (unsigned i=0; iLength(); ssize_t End = CharPos + Len; if (End <= Offset || CharPos > RestyleEnd) ; else { ssize_t Before = Offset >= CharPos ? Offset - CharPos : 0; LgiAssert(Before >= 0); ssize_t After = RestyleEnd < End ? End - RestyleEnd : 0; LgiAssert(After >= 0); ssize_t Inside = Len - Before - After; LgiAssert(Inside >= 0); GAutoPtr TmpStyle(new GCss); if (Add) { if (t->GetStyle()) *TmpStyle = *t->GetStyle(); *TmpStyle += *Style; } else if (Style->Length() != 0) { if (t->GetStyle()) *TmpStyle = *t->GetStyle(); *TmpStyle -= *Style; } GNamedStyle *CacheStyle = TmpStyle && TmpStyle->Length() ? d->AddStyleToCache(TmpStyle) : NULL; if (Before && After) { // Split into 3 parts: // |---before----|###restyled###|---after---| StyleText *st = new StyleText(t->At(Before), Inside, CacheStyle); if (st) Txt.AddAt(++i, st); st = new StyleText(t->At(Before + Inside), After, t->GetStyle()); if (st) Txt.AddAt(++i, st); t->Length(Before); LayoutDirty = true; return true; } else if (Before) { // Split into 2 parts: // |---before----|###restyled###| StyleText *st = new StyleText(t->At(Before), Inside, CacheStyle); if (st) Txt.AddAt(++i, st); t->Length(Before); LayoutDirty = true; } else if (After) { // Split into 2 parts: // |###restyled###|---after---| StyleText *st = new StyleText(t->At(0), Inside, CacheStyle); if (st) Txt.AddAt(i, st); memmove(t->At(0), t->At(Inside), After*sizeof(uint32)); t->Length(After); LayoutDirty = true; } else if (Inside) { // Re-style the whole run t->SetStyle(CacheStyle); LayoutDirty = true; } } CharPos += Len; } // Merge any regions of the same style into contiguous sections for (unsigned i=0; iGetStyle() == b->GetStyle() && a->Emoji == b->Emoji) { // Merge... a->Add(b->AddressOf(0), b->Length()); Txt.DeleteAt(i + 1, true); delete b; i--; } } return true; } bool GRichTextPriv::TextBlock::Seek(SeekType To, BlockCursor &Cur) { int XOffset = Cur.Pos.x1 - Pos.x1; int CharPos = 0; GArray LineOffset; GArray LineLen; int CurLine = -1; int CurLineScore = 0; for (unsigned i=0; iLength(); LineOffset[i] = CharPos; LineLen[i] = Len; if (Cur.Offset >= CharPos && Cur.Offset <= CharPos + Len - Line->NewLine) // Minus 'NewLine' is because the cursor can't be // after the '\n' on a line. It's actually on the // next line. { int Score = 1; if (Cur.LineHint >= 0 && i == Cur.LineHint) Score++; if (Score > CurLineScore) { CurLine = i; CurLineScore = Score; } } CharPos += Len; } if (CurLine < 0) { CharPos = 0; d->Log->Print("TextBlock(%i)::Seek, lines=%i\n", GetUid(), Layout.Length()); for (unsigned i=0; iLog->Print("\tLine[%i] @ %i+%i=%i\n", i, CharPos, Line->Length(), CharPos + Line->Length()); CharPos += Line->Length(); } else { d->Log->Print("\tLine[%i] @ %i, is NULL\n", i, CharPos); break; } } return d->Error(_FL, "Index '%i' not in layout lines.", Cur.Offset); } TextLine *Line = NULL; switch (To) { case SkLineStart: { Cur.Offset = LineOffset[CurLine]; Cur.LineHint = CurLine; return true; } case SkLineEnd: { Cur.Offset = LineOffset[CurLine] + LineLen[CurLine] - Layout[CurLine]->NewLine; Cur.LineHint = CurLine; return true; } case SkUpLine: { // Get previous line... if (CurLine == 0) return false; Line = Layout[--CurLine]; if (!Line) return d->Error(_FL, "No line at %i.", CurLine); break; } case SkDownLine: { // Get next line... if (CurLine >= (int)Layout.Length() - 1) return false; Line = Layout[++CurLine]; if (!Line) return d->Error(_FL, "No line at %i.", CurLine); break; } default: { return false; break; } } if (Line) { // Work out where the cursor should be based on the 'XOffset' if (Line->Strs.Length() > 0) { int FixX = 0; int CharOffset = 0; for (unsigned i=0; iStrs.Length(); i++) { DisplayStr *Ds = Line->Strs[i]; PtrCheckBreak(Ds); if (XOffset >= FixedToInt(FixX) && XOffset <= FixedToInt(FixX + Ds->FX())) { // This is the matching string... int Px = XOffset - FixedToInt(FixX) - Line->PosOff.x1; ssize_t Char = Ds->PosToIndex(Px, true); if (Char >= 0) { Cur.Offset = LineOffset[CurLine] + // Character offset of line CharOffset + // Character offset of current string Char; // Offset into current string for 'XOffset' Cur.LineHint = CurLine; return true; } } FixX += Ds->FX(); CharOffset += Ds->Length(); } // Cursor is nearest the end of the string...? Cur.Offset = LineOffset[CurLine] + Line->Length() - Line->NewLine; Cur.LineHint = CurLine; return true; } else if (Line->NewLine) { Cur.Offset = LineOffset[CurLine]; Cur.LineHint = CurLine; return true; } } return false; } #ifdef _DEBUG void GRichTextPriv::TextBlock::DumpNodes(GTreeItem *Ti) { GString s; s.Printf("TextBlock style=%s", Style?Style->Name.Get():NULL); Ti->SetText(s); GTreeItem *TxtRoot = PrintNode(Ti, "Txt(%i)", Txt.Length()); if (TxtRoot) { int Pos = 0; for (unsigned i=0; iLength(); GString u; if (Len) { GStringPipe p(256); uint32 *Str = St->At(0); p.Write("\'", 1); for (int k=0; k= 0x10000) p.Print("&#%i;", Str[k]); else { uint8 utf8[6], *n = utf8; ssize_t utf8len = sizeof(utf8); if (LgiUtf32To8(Str[k], n, utf8len)) p.Write(utf8, sizeof(utf8)-utf8len); } } p.Write("\'", 1); u = p.NewGStr(); } else u = "(Empty)"; PrintNode( TxtRoot, "[%i] range=%i-%i, len=%i, style=%s, %s", i, Pos, Pos + Len - 1, Len, St->GetStyle() ? St->GetStyle()->Name.Get() : NULL, u.Get()); Pos += Len; } } GTreeItem *LayoutRoot = PrintNode(Ti, "Layout(%i)", Layout.Length()); if (LayoutRoot) { int Pos = 0; for (unsigned i=0; iLength() - 1, Tl->Length(), Tl->NewLine, Tl->PosOff.GetStr()); for (unsigned n=0; nStrs.Length(); n++) { DisplayStr *Ds = Tl->Strs[n]; GNamedStyle *Style = Ds->Src ? Ds->Src->GetStyle() : NULL; PrintNode( Elem, "[%i] style=%s len=%i txt='%.20S'", n, Style ? Style->Name.Get() : NULL, Ds->Length(), (const char16*) (*Ds)); } Pos += Tl->Length() + Tl->NewLine; } } } #endif