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; i