diff --git a/include/common/GFontCache.h b/include/common/GFontCache.h --- a/include/common/GFontCache.h +++ b/include/common/GFontCache.h @@ -1,140 +1,152 @@ #ifndef _GFONTCACHE_H_ #define _GFONTCACHE_H_ class GFontCache { GFont *DefaultFont; GArray Fonts; LHashTbl, GString> FontName; public: /// Constructor for font cache GFontCache ( /// This is an externally owned default font... or optionally /// NULL if there is no default. GFont *DefFnt = NULL ) { DefaultFont = DefFnt; } ~GFontCache() { Fonts.DeleteObjects(); } GFont *GetDefaultFont() { return DefaultFont; } void SetDefaultFont(GFont *Def) { DefaultFont = Def; } /// This defines a text label that links to a font-face. On multi-platform /// software sometimes you need to have one CSS font Label that links to /// different available fonts. void DefineFontName(const char *Label, const char *FontFace) { GString s = FontName.Find(Label); if (!s.Get()) FontName.Add(Label, GString(FontFace)); } GFont *AddFont( const char *Face, GCss::Len Size, GCss::FontWeightType Weight, GCss::FontStyleType Style, GCss::TextDecorType Decor) { // Matching existing fonts... for (unsigned i=0; iFace() && Face && !_stricmp(f->Face(), Face) && f->Size() == Size && f->Bold() == (Weight == GCss::FontWeightBold) && f->Italic() == (Style == GCss::FontStyleItalic) && f->Underline() == (Decor == GCss::TextDecorUnderline) ) return f; } // No matching font... create a new one GFont *f = new GFont; if (f) { f->Bold(Weight == GCss::FontWeightBold); f->Italic(Style == GCss::FontStyleItalic); f->Underline(Decor == GCss::TextDecorUnderline); - if (!f->Create(Face, Size)) + auto Sz = Size; + if (Sz.Type == GCss::SizeLarger) + { + Sz = SysFont->Size(); + Sz.Value++; + } + else if (Sz.Type == GCss::SizeSmaller) + { + Sz = SysFont->Size(); + Sz.Value--; + } + + if (!f->Create(Face, Sz)) { LgiAssert(0); DeleteObj(f); return NULL; } Fonts.Add(f); } return f; } GFont *GetFont(GCss *Style) { if (!Style || !DefaultFont) return DefaultFont; GCss::StringsDef Fam = Style->FontFamily(); bool FamHasDefFace = false; for (unsigned i=0; iFace(), Fam[i]); } } if (!FamHasDefFace) Fam.Add(NewStr(DefaultFont->Face())); GCss::Len Sz = Style->FontSize(); if (!Sz.IsValid()) Sz = DefaultFont->Size(); GCss::FontWeightType Weight = Style->FontWeight(); GCss::FontWeightType DefaultWeight = DefaultFont && DefaultFont->Bold() ? GCss::FontWeightBold : GCss::FontWeightNormal; GCss::FontStyleType FontStyle = Style->FontStyle(); GCss::TextDecorType Decor = Style->TextDecoration(); GFont *f = NULL; for (unsigned i = 0; !f && i // sub-system headers #include "LgiOsDefs.h" // Platform specific #include "LgiInc.h" #include "LgiClass.h" #include "Progress.h" // Xp #include "GFile.h" // Platform specific #include "GMem.h" // Platform specific #include "Core.h" // Platform specific #include "GContainers.h" #include "GCapabilities.h" #include "GRefCount.h" #include "GPalette.h" // Alpha Blting #ifdef WIN32 #include "wingdi.h" #endif #ifndef AC_SRC_OVER #define AC_SRC_OVER 0 #endif #ifndef AC_SRC_ALPHA #define AC_SRC_ALPHA 1 #endif #include "GLibrary.h" // Defines /// The default gamma curve (none) used by the gamma LUT GdcDevice::GetGamma #define LGI_DEFAULT_GAMMA 1.0 #ifndef LGI_PI /// The value of PI #define LGI_PI 3.141592654 #endif /// Converts degrees to radians #define LGI_DegToRad(i) ((i)*LGI_PI/180) /// Converts radians to degrees #define LGI_RadToDeg(i) ((i)*180/LGI_PI) #if defined(WIN32) && !defined(_WIN64) /// Use intel assembly instructions, comment out for porting #define GDC_USE_ASM #endif /// Blending mode: overwrite #define GDC_SET 0 /// Blending mode: bitwise AND with background #define GDC_AND 1 /// Blending mode: bitwise OR with background #define GDC_OR 2 /// Blending mode: bitwise XOR with background #define GDC_XOR 3 /// Blending mode: alpha blend with background #define GDC_ALPHA 4 #define GDC_REMAP 5 #define GDC_MAXOP 6 #define GDC_CACHE_SIZE 4 // Channel types #define GDCC_MONO 0 #define GDCC_GREY 1 #define GDCC_INDEX 2 #define GDCC_R 3 #define GDCC_G 4 #define GDCC_B 5 #define GDCC_ALPHA 6 // Native data formats #define GDC_8BIT 0 #define GDC_16BIT 1 #define GDC_24BIT 2 #define GDC_32BIT 3 #define GDC_MAXFMT 4 // Colour spaces #define GDC_8I 0 // 8 bit paletted #define GDC_5R6G5B 1 // 16 bit #define GDC_8B8G8B 2 // 24 bit #define GDC_8A8R8G8B 3 // 32 bit #define GDC_MAXSPACE 4 // Update types #define GDC_PAL_CHANGE 0x1 #define GDC_BITS_CHANGE 0x2 // Flood fill types /// GSurface::FloodFill to a different colour #define GDC_FILL_TO_DIFFERENT 0 /// GSurface::FloodFill to a certain colour #define GDC_FILL_TO_BORDER 1 /// GSurface::FloodFill while colour is near to the seed colour #define GDC_FILL_NEAR 2 // Gdc options /// Used in GdcApp8Set::Blt when doing colour depth reduction to 8 bit. #define GDC_REDUCE_TYPE 0 /// No conversion #define REDUCE_NONE 0 /// Nearest colour pixels #define REDUCE_NEAREST 1 /// Halftone the pixels #define REDUCE_HALFTONE 2 /// Error diffuse the pixels #define REDUCE_ERROR_DIFFUSION 3 /// Not used. #define REDUCE_DL1 4 /// Not used. #define GDC_HALFTONE_BASE_INDEX 1 /// When in 8-bit defined the behaviour of GdcDevice::GetColour #define GDC_PALETTE_TYPE 2 /// Allocate colours from the palette #define PALTYPE_ALLOC 0 /// Use an RGB cube #define PALTYPE_RGB_CUBE 1 /// Use a HSL palette #define PALTYPE_HSL 2 /// Converts images to the specified bit-depth on load, does nothing if 0 #define GDC_PROMOTE_ON_LOAD 3 #define GDC_MAX_OPTION 4 // GSurface Flags #define GDC_ON_SCREEN 0x0002 #define GDC_ALPHA_CHANNEL 0x0004 #define GDC_UPDATED_PALETTE 0x0008 #define GDC_CAPTURE_CURSOR 0x0010 #define GDC_OWN_APPLICATOR 0x0020 #define GDC_CACHED_APPLICATOR 0x0040 #define GDC_OWN_PALETTE 0x0080 #define GDC_DRAW_ON_ALPHA 0x0100 // Region types #define GDC_RGN_NONE 0 // No clipping #define GDC_RGN_SIMPLE 1 // Single rectangle #define GDC_RGN_COMPLEX 2 // Many rectangles // Error codes #define GDCERR_NONE 0 #define GDCERR_ERROR 1 #define GDCERR_CANT_SET_SCAN_WIDTH 2 #define GDC_INVALIDMODE -1 // Font display flags #define GDCFNT_TRANSPARENT 0x0001 // not set - SOLID #define GDCFNT_UNDERLINE 0x0002 // Palette file types #define GDCPAL_JASC 1 #define GDCPAL_MICROSOFT 2 // Misc #define BMPWIDTH(bits) ((((bits)+31)/32)<<2) #include "GColourSpace.h" // Look up tables #define Div255Lut (GdcDevice::GetInst()->GetDiv255()) // Classes class GFilter; class GSurface; #include "GRect.h" // #include "GFont.h" #include "GPoint.h" #include "GColour.h" class LgiClass GBmpMem { public: enum GdcMemFlags { BmpOwnMemory = 0x1, BmpPreMulAlpha = 0x2, }; uchar *Base; int x, y; ssize_t Line; GColourSpace Cs; int Flags; GBmpMem(); ~GBmpMem(); bool PreMul() { return (Flags & BmpPreMulAlpha) != 0; } bool PreMul(bool set) { if (set) Flags |= BmpPreMulAlpha; else Flags &= ~BmpPreMulAlpha; return PreMul(); } bool OwnMem() { return (Flags & BmpOwnMemory) != 0; } bool OwnMem(bool set) { if (set) Flags |= BmpOwnMemory; else Flags &= ~BmpOwnMemory; return OwnMem(); } int GetBits() { return GColourSpaceToBits(Cs); } void GetMemoryExtents(uchar *&Start, uchar *&End) { if (Line < 0) { Start = Base + (y * Line); End = Base + Line; } else { Start = Base; End = Base + (y * Line); } } bool Overlap(GBmpMem *Mem) { uchar *ThisStart, *ThisEnd; GetMemoryExtents(ThisStart, ThisEnd); uchar *MemStart, *MemEnd; GetMemoryExtents(MemStart, MemEnd); if (ThisEnd < MemStart) return false; if (ThisStart > MemEnd) return false; return true; } }; #define GAPP_ALPHA_A 1 #define GAPP_ALPHA_PAL 2 #define GAPP_BACKGROUND 3 #define GAPP_ANGLE 4 #define GAPP_BOUNDS 5 /// \brief Class to draw onto a memory bitmap /// /// This class assumes that all clipping is done by the layer above. /// It can then implement very simple loops to do the work of filling /// pixels class LgiClass GApplicator { protected: GBmpMem *Dest; GBmpMem *Alpha; GPalette *Pal; int Op; public: union { COLOUR c; // main colour System24BitPixel p24; System32BitPixel p32; }; GApplicator() { c = 0; Dest = NULL; Alpha = NULL; Pal = NULL; } GApplicator(COLOUR Colour) { c = Colour; } virtual ~GApplicator() { } virtual const char *GetClass() { return "GApplicator"; } /// Get a parameter virtual int GetVar(int Var) { return 0; } /// Set a parameter virtual int SetVar(int Var, NativeInt Value) { return 0; } GColourSpace GetColourSpace() { return Dest ? Dest->Cs : CsNone; } /// Sets the operator void SetOp(int o, int Param = -1) { Op = o; } /// Gets the operator int GetOp() { return Op; } /// Gets the bit depth int GetBits() { return (Dest) ? GColourSpaceToBits(Dest->Cs) : 0; } /// Gets the flags in operation int GetFlags() { return (Dest) ? Dest->Flags : 0; } /// Gets the palette GPalette *GetPal() { return Pal; } /// Sets the bitmap to write onto virtual bool SetSurface(GBmpMem *d, GPalette *p = 0, GBmpMem *a = 0) = 0; // sets Dest, returns FALSE on error /// Sets the current position to an x,y virtual void SetPtr(int x, int y) = 0; // calculates Ptr from x, y /// Moves the current position one pixel left virtual void IncX() = 0; /// Moves the current position one scanline down virtual void IncY() = 0; /// Offset the current position virtual void IncPtr(int X, int Y) = 0; /// Sets the pixel at the current location with the current colour virtual void Set() = 0; /// Gets the colour of the pixel at the current location virtual COLOUR Get() = 0; /// Draws a vertical line from the current position down 'height' scanlines virtual void VLine(int height) = 0; /// Draws a rectangle starting from the current position, 'x' pixels across and 'y' pixels down virtual void Rectangle(int x, int y) = 0; /// Copies bitmap data to the current position virtual bool Blt(GBmpMem *Src, GPalette *SPal, GBmpMem *SrcAlpha = 0) = 0; }; /// Creates applications from parameters. class LgiClass GApplicatorFactory { public: GApplicatorFactory(); virtual ~GApplicatorFactory(); /// Find the application factory and create the appropriate object. static GApplicator *NewApp(GColourSpace Cs, int Op); virtual GApplicator *Create(GColourSpace Cs, int Op) = 0; }; class LgiClass GApp15 : public GApplicatorFactory { public: GApplicator *Create(GColourSpace Cs, int Op); }; class LgiClass GApp16 : public GApplicatorFactory { public: GApplicator *Create(GColourSpace Cs, int Op); }; class LgiClass GApp24 : public GApplicatorFactory { public: GApplicator *Create(GColourSpace Cs, int Op); }; class LgiClass GApp32 : public GApplicatorFactory { public: GApplicator *Create(GColourSpace Cs, int Op); }; class LgiClass GApp8 : public GApplicatorFactory { public: GApplicator *Create(GColourSpace Cs, int Op); }; class GAlphaFactory : public GApplicatorFactory { public: GApplicator *Create(GColourSpace Cs, int Op); }; #define OrgX(x) x -= OriginX #define OrgY(y) y -= OriginY #define OrgXy(x, y) x -= OriginX; y -= OriginY #define OrgPt(p) p.x -= OriginX; p.y -= OriginY #define OrgRgn(r) r.Offset(-OriginX, -OriginY) /// Base class API for graphics operations class LgiClass GSurface : public GRefCount, public GDom { friend class GFilter; friend class GView; friend class GWindow; friend class GVariant; friend class GRegionClipDC; void Init(); protected: int Flags; int PrevOp; GRect Clip; GColourSpace ColourSpace; GBmpMem *pMem; GSurface *pAlphaDC; GPalette *pPalette; GApplicator *pApp; GApplicator *pAppCache[GDC_CACHE_SIZE]; int OriginX, OriginY; // Protected functions GApplicator *CreateApplicator(int Op, GColourSpace Cs = CsNone); uint32_t LineBits; uint32_t LineMask; uint32_t LineReset; #if WINNATIVE OsPainter hDC; OsBitmap hBmp; #elif defined __GTK_H__ OsPainter Cairo; #endif public: GSurface(); GSurface(GSurface *pDC); virtual ~GSurface(); // Win32 #if defined(__GTK_H__) /// Gets the drawable size, regardless of clipping or client rect virtual GdcPt2 GetSize() { GdcPt2 p; return p; } virtual Gtk::GtkPrintContext *GetPrintContext() { return NULL; } #elif defined(WINNATIVE) virtual HDC StartDC() { return hDC; } virtual void EndDC() {} #elif defined MAC #ifdef COCOA #else virtual CGColorSpaceRef GetColourSpaceRef() { return 0; } #endif #endif virtual OsBitmap GetBitmap(); virtual OsPainter Handle(); virtual void SetClient(GRect *c) {} virtual bool GetClient(GRect *c) { return false; } // Creation enum SurfaceCreateFlags { SurfaceCreateNone, SurfaceRequireNative, SurfaceRequireExactCs, }; virtual bool Create(int x, int y, GColourSpace Cs, int Flags = SurfaceCreateNone) { return false; } virtual void Update(int Flags) {} // Alpha channel /// Returns true if this Surface has an alpha channel virtual bool HasAlpha() { return pAlphaDC != 0; } /// Creates or destroys the alpha channel for this surface virtual bool HasAlpha(bool b); /// Returns true if we are drawing on the alpha channel bool DrawOnAlpha() { return ((Flags & GDC_DRAW_ON_ALPHA) != 0); } /// True if you want to edit the alpha channel rather than the colour bits bool DrawOnAlpha(bool Draw); /// Returns the surface of the alpha channel. GSurface *AlphaDC() { return pAlphaDC; } /// Lowers the alpha of the whole image to Alpha/255.0. /// Only works on bitmaps with an alpha channel (i.e. CsRgba32 or it's variants) bool SetConstantAlpha(uint8_t Alpha); // Create sub-images (that reference the memory of this object) GSurface *SubImage(GRect r); GSurface *SubImage(int x1, int y1, int x2, int y2) { GRect r(x1, y1, x2, y2); return SubImage(r); } // Applicator virtual bool Applicator(GApplicator *pApp); virtual GApplicator *Applicator(); // Palette virtual GPalette *Palette(); virtual void Palette(GPalette *pPal, bool bOwnIt = true); // Clip region virtual GRect ClipRgn(GRect *Rgn); virtual GRect ClipRgn(); /// Gets the current colour virtual COLOUR Colour() { return pApp->c; } /// Sets the current colour virtual COLOUR Colour ( /// The new colour COLOUR c, /// The bit depth of the new colour or 0 to indicate the depth is the same as the current Surface int Bits = 0 ); /// Sets the current colour virtual GColour Colour ( /// The new colour GColour c ); /// Gets the current blending mode in operation virtual int Op() { return (pApp) ? pApp->GetOp() : GDC_SET; } /// Sets the current blending mode in operation /// \sa GDC_SET, GDC_AND, GDC_OR, GDC_XOR and GDC_ALPHA virtual int Op(int Op, NativeInt Param = -1); /// Gets the width in pixels virtual int X() { return (pMem) ? pMem->x : 0; } /// Gets the height in pixels virtual int Y() { return (pMem) ? pMem->y : 0; } /// Gets the bounds of the image as a GRect GRect Bounds() { return GRect(0, 0, X()-1, Y()-1); } /// Gets the length of a scanline in bytes virtual ssize_t GetRowStep() { return (pMem) ? pMem->Line : 0; } /// Returns the horizontal resolution of the device virtual int DpiX() { return 100; } /// Returns the vertical resolution of the device virtual int DpiY() { return 100; } /// Gets the bits per pixel virtual int GetBits() { return (pMem) ? GColourSpaceToBits(pMem->Cs) : 0; } /// Gets the colour space of the pixels virtual GColourSpace GetColourSpace() { return ColourSpace; } /// Gets any flags associated with the surface virtual int GetFlags() { return Flags; } /// Returns true if the surface is on the screen virtual class GScreenDC *IsScreen() { return 0; } /// Returns true if the surface is for printing virtual bool IsPrint() { return false; } /// Returns a pointer to the start of a scanline, or NULL if not available virtual uchar *operator[](int y); /// Returns true if this surface supports alpha compositing when using Blt virtual bool SupportsAlphaCompositing() { return false; } /// \returns whether if pixel data is pre-multiplied alpha virtual bool IsPreMultipliedAlpha(); /// Converts the pixel data between pre-mul alpha or non-pre-mul alpha virtual bool ConvertPreMulAlpha(bool ToPreMul); /// Makes the alpha channel opaque virtual bool MakeOpaque(); /// Gets the surface origin virtual void GetOrigin(int &x, int &y) { x = OriginX; y = OriginY; } /// Sets the surface origin virtual void SetOrigin(int x, int y) { OriginX = x; OriginY = y; } /// Sets a pixel with the current colour virtual void Set(int x, int y); /// Gets a pixel (doesn't work on some types of image, i.e. GScreenDC) virtual COLOUR Get(int x, int y); // Line /// Draw a horizontal line in the current colour virtual void HLine(int x1, int x2, int y); /// Draw a vertical line in the current colour virtual void VLine(int x, int y1, int y2); /// Draw a line in the current colour virtual void Line(int x1, int y1, int x2, int y2); /// Some surfaces only support specific line styles (e.g. GDI/Win) enum LineStyles { LineNone = 0x0, LineSolid = 0xffffffff, LineAlternate = 0xaaaaaaaa, LineDash = 0xf0f0f0f0, LineDot = 0xcccccccc, LineDashDot = 0xF33CCF30, LineDashDotDot = 0xf0ccf0cc, }; virtual uint LineStyle(uint32_t Bits, uint32_t Reset = 0x80000000) { uint32_t B = LineBits; LineBits = Bits; LineMask = LineReset = Reset; return B; } virtual uint LineStyle() { return LineBits; } // Curve /// Stroke a circle in the current colour virtual void Circle(double cx, double cy, double radius); /// Fill a circle in the current colour virtual void FilledCircle(double cx, double cy, double radius); /// Stroke an arc in the current colour virtual void Arc(double cx, double cy, double radius, double start, double end); /// Fill an arc in the current colour virtual void FilledArc(double cx, double cy, double radius, double start, double end); /// Stroke an ellipse in the current colour virtual void Ellipse(double cx, double cy, double x, double y); /// Fill an ellipse in the current colour virtual void FilledEllipse(double cx, double cy, double x, double y); // Rectangular /// Stroke a rectangle in the current colour virtual void Box(int x1, int y1, int x2, int y2); /// Stroke a rectangle in the current colour virtual void Box ( /// The rectangle, or NULL to stroke the edge of the entire surface GRect *a = NULL ); /// Fill a rectangle in the current colour virtual void Rectangle(int x1, int y1, int x2, int y2); /// Fill a rectangle in the current colour virtual void Rectangle ( /// The rectangle, or NULL to fill the entire surface GRect *a = NULL ); /// Copy an image onto the surface virtual void Blt ( /// The destination x coord int x, /// The destination y coord int y, /// The source surface GSurface *Src, /// The optional area of the source to use, if not specified the whole source is used GRect *a = NULL ); void Blt(int x, int y, GSurface *Src, GRect a) { Blt(x, y, Src, &a); } /// Not implemented virtual void StretchBlt(GRect *d, GSurface *Src, GRect *s); // Other /// Fill a polygon in the current colour virtual void Polygon(int Points, GdcPt2 *Data); /// Stroke a bezier in the current colour virtual void Bezier(int Threshold, GdcPt2 *Pt); /// Flood fill in the current colour (doesn't work on a GScreenDC) virtual void FloodFill ( /// Start x coordinate int x, /// Start y coordinate int y, /// Use #GDC_FILL_TO_DIFFERENT, #GDC_FILL_TO_BORDER or #GDC_FILL_NEAR int Mode, /// Fill colour COLOUR Border = 0, /// The bounds of the filled area or NULL if you don't care GRect *Bounds = NULL ); // GDom interface bool GetVariant(const char *Name, GVariant &Value, char *Array = NULL); bool SetVariant(const char *Name, GVariant &Value, char *Array = NULL); bool CallMethod(const char *Name, GVariant *ReturnValue, GArray &Args); }; #ifdef MAC struct GPrintDcParams { #ifdef COCOA #else PMRect Page; CGContextRef Ctx; PMResolution Dpi; #endif }; #endif /// \brief An implemenation of GSurface to draw onto the screen. /// /// This is the class given to GView::OnPaint() most of the time. Which most of /// the time doesn't matter unless your doing something unusual. class LgiClass GScreenDC : public GSurface { class GScreenPrivate *d; public: GScreenDC(); virtual ~GScreenDC(); // OS Sepcific #if WINNATIVE GScreenDC(GViewI *view); GScreenDC(HWND hwnd); GScreenDC(HDC hdc, HWND hwnd, bool Release = false); GScreenDC(HBITMAP hBmp, int Sx, int Sy); bool CreateFromHandle(HDC hdc); void SetSize(int x, int y); #else /// Construct a wrapper to draw on a window GScreenDC(GView *view, void *Param = 0); #if defined(LGI_SDL) #elif defined(MAC) GScreenDC(GWindow *wnd, void *Param = 0); GScreenDC(GPrintDcParams *Params); // Used by GPrintDC GRect GetPos(); void PushState(); void PopState(); #elif defined(__GTK_H__) /// Constructs a server size pixmap GScreenDC(int x, int y, int bits); /// Constructs a wrapper around a drawable GScreenDC(Gtk::GdkDrawable *Drawable); /// Constructs a DC for drawing on a window ///GScreenDC(OsView View); // Gtk::cairo_surface_t *GetSurface(bool Render); GdcPt2 GetSize(); #elif defined(BEOS) GScreenDC(BView *view); #endif OsPainter Handle(); GView *GetView(); int GetFlags(); GRect *GetClient(); #endif // Properties bool GetClient(GRect *c); void SetClient(GRect *c); int X(); int Y(); GPalette *Palette(); void Palette(GPalette *pPal, bool bOwnIt = true); uint LineStyle(); uint LineStyle(uint Bits, uint32_t Reset = 0x80000000); int GetBits(); GScreenDC *IsScreen() { return this; } bool SupportsAlphaCompositing(); #ifndef LGI_SDL uchar *operator[](int y) { return NULL; } void GetOrigin(int &x, int &y); void SetOrigin(int x, int y); GRect ClipRgn(); GRect ClipRgn(GRect *Rgn); COLOUR Colour(); COLOUR Colour(COLOUR c, int Bits = 0); GColour Colour(GColour c); int Op(); int Op(int Op, NativeInt Param = -1); // Primitives void Set(int x, int y); COLOUR Get(int x, int y); void HLine(int x1, int x2, int y); void VLine(int x, int y1, int y2); void Line(int x1, int y1, int x2, int y2); void Circle(double cx, double cy, double radius); void FilledCircle(double cx, double cy, double radius); void Arc(double cx, double cy, double radius, double start, double end); void FilledArc(double cx, double cy, double radius, double start, double end); void Ellipse(double cx, double cy, double x, double y); void FilledEllipse(double cx, double cy, double x, double y); void Box(int x1, int y1, int x2, int y2); void Box(GRect *a); void Rectangle(int x1, int y1, int x2, int y2); void Rectangle(GRect *a = NULL); void Blt(int x, int y, GSurface *Src, GRect *a = NULL); void StretchBlt(GRect *d, GSurface *Src, GRect *s = NULL); void Polygon(int Points, GdcPt2 *Data); void Bezier(int Threshold, GdcPt2 *Pt); void FloodFill(int x, int y, int Mode, COLOUR Border = 0, GRect *Bounds = NULL); #endif }; /// \brief Blitting region helper class, can calculate the right source and dest rectangles /// for a blt operation including propagating clipping back to the source rect. class GBlitRegions { // Raw image bounds GRect SrcBounds; GRect DstBounds; // Unclipped blit regions GRect SrcBlt; GRect DstBlt; public: /// Clipped blit region in destination co-ords GRect SrcClip; /// Clipped blit region in source co-ords GRect DstClip; /// Calculate the rectangles. GBlitRegions ( /// Destination surface GSurface *Dst, /// Destination blt x offset int x1, /// Destination blt y offset int y1, /// Source surface GSurface *Src, /// [Optional] Crop the source surface first, else whole surface is blt GRect *SrcRc = 0 ) { // Calc full image bounds if (Src) SrcBounds.Set(0, 0, Src->X()-1, Src->Y()-1); else SrcBounds.ZOff(-1, -1); if (Dst) DstBounds.Set(0, 0, Dst->X()-1, Dst->Y()-1); else DstBounds.ZOff(-1, -1); // Calc full sized blt regions if (SrcRc) { SrcBlt = *SrcRc; SrcBlt.Bound(&SrcBounds); } else SrcBlt = SrcBounds; DstBlt = SrcBlt; DstBlt.Offset(x1-DstBlt.x1, y1-DstBlt.y1); // Dest clipped to dest bounds DstClip = DstBlt; DstClip.Bound(&DstBounds); // Now map the dest clipping back to the source SrcClip = SrcBlt; SrcClip.x1 += DstClip.x1 - DstBlt.x1; SrcClip.y1 += DstClip.y1 - DstBlt.y1; SrcClip.x2 -= DstBlt.x2 - DstClip.x2; SrcClip.y2 -= DstBlt.y2 - DstClip.y2; } /// Returns non-zero if both clipped rectangles are valid. bool Valid() { return DstClip.Valid() && SrcClip.Valid(); } void Dump() { printf("SrcBounds: %s\n", SrcBounds.GetStr()); printf("DstBounds: %s\n", DstBounds.GetStr()); printf("SrcBlt: %s\n", SrcBlt.GetStr()); printf("DstBlt: %s\n", DstBlt.GetStr()); printf("SrcClip: %s\n", SrcClip.GetStr()); printf("DstClip: %s\n", DstClip.GetStr()); } }; #if defined(MAC) class CGImg { class CGImgPriv *d; void Create(int x, int y, int Bits, ssize_t Line, uchar *data, uchar *palette, GRect *r); public: CGImg(int x, int y, int Bits, ssize_t Line, uchar *data, uchar *palette, GRect *r); CGImg(GSurface *pDC); ~CGImg(); operator CGImageRef(); }; #endif /// \brief An implemenation of GSurface to draw into a memory bitmap. /// /// This class uses a block of memory to represent an image. You have direct /// pixel access as well as higher level functions to manipulate the bits. class LgiClass GMemDC : public GSurface { protected: class GMemDCPrivate *d; #if defined WINNATIVE PBITMAPINFO GetInfo(); #endif // This is called between capturing the screen and overlaying the // cursor in GMemDC::Blt(x, y, ScreenDC, Src). It can be used to // overlay effects between the screen and cursor layers. virtual void OnCaptureScreen() {} public: /// Creates a memory bitmap GMemDC ( /// The width int x = 0, /// The height int y = 0, /// The colour space to use. CsNone will default to the /// current screen colour space. GColourSpace cs = CsNone, /// Optional creation flags int Flags = SurfaceCreateNone ); GMemDC(GSurface *pDC); virtual ~GMemDC(); #if WINNATIVE HDC StartDC(); void EndDC(); void Update(int Flags); void UpsideDown(bool upsidedown); #else GRect ClipRgn() { return Clip; } #if defined MAC OsBitmap GetBitmap(); #if !defined(LGI_SDL) CGColorSpaceRef GetColourSpaceRef(); CGImg *GetImg(GRect *Sub = 0); #endif #elif defined(__GTK_H__) Gtk::GdkImage *GetImage(); GdcPt2 GetSize(); Gtk::cairo_surface_t *GetSurface(GRect &r); GColourSpace GetCreateCs(); #elif defined(BEOS) || defined(LGI_SDL) OsBitmap GetBitmap(); #endif OsPainter Handle(); #endif // Set new clipping region GRect ClipRgn(GRect *Rgn); void SetClient(GRect *c); /// Locks the bits for access. GMemDC's start in the locked state. bool Lock(); /// Unlocks the bits to optimize for display. While the bitmap is unlocked you /// can't access the data for read or write. On linux this converts the XImage /// to pixmap. On other systems it doesn't do much. As a general rule if you /// don't need access to a bitmap after creating / loading it then unlock it. bool Unlock(); void SetOrigin(int x, int y); void Empty(); bool SupportsAlphaCompositing(); bool Create(int x, int y, GColourSpace Cs, int Flags = SurfaceCreateNone); void Blt(int x, int y, GSurface *Src, GRect *a = NULL); void StretchBlt(GRect *d, GSurface *Src, GRect *s = NULL); void HorzLine(int x1, int x2, int y, COLOUR a, COLOUR b); void VertLine(int x, int y1, int y2, COLOUR a, COLOUR b); }; /// \brief An implemenation of GSurface to print to a printer. /// /// This class redirects standard graphics calls to print a page. /// /// \sa GPrinter class LgiClass GPrintDC #if defined(WIN32) || defined(MAC) : public GScreenDC #else : public GSurface #endif { class GPrintDCPrivate *d; public: GPrintDC(void *Handle, const char *PrintJobName, const char *PrinterName = NULL); ~GPrintDC(); bool IsPrint() { return true; } const char *GetOutputFileName(); int X(); int Y(); int GetBits(); /// Returns the horizontal DPI of the printer or 0 on error int DpiX(); /// Returns the vertical DPI of the printer or 0 on error int DpiY(); #if defined __GTK_H__ Gtk::GtkPrintContext *GetPrintContext(); int Op() { return GDC_SET; } int Op(int Op, NativeInt Param = -1) { return GDC_SET; } GRect ClipRgn(GRect *Rgn); GRect ClipRgn(); COLOUR Colour(); COLOUR Colour(COLOUR c, int Bits = 0); GColour Colour(GColour c); void Set(int x, int y); void HLine(int x1, int x2, int y); void VLine(int x, int y1, int y2); void Line(int x1, int y1, int x2, int y2); void Circle(double cx, double cy, double radius); void FilledCircle(double cx, double cy, double radius); void Arc(double cx, double cy, double radius, double start, double end); void FilledArc(double cx, double cy, double radius, double start, double end); void Ellipse(double cx, double cy, double x, double y); void FilledEllipse(double cx, double cy, double x, double y); void Box(int x1, int y1, int x2, int y2); void Box(GRect *a = NULL); void Rectangle(int x1, int y1, int x2, int y2); void Rectangle(GRect *a = NULL); void Blt(int x, int y, GSurface *Src, GRect *a = NULL); void StretchBlt(GRect *d, GSurface *Src, GRect *s); void Polygon(int Points, GdcPt2 *Data); void Bezier(int Threshold, GdcPt2 *Pt); #endif }; ////////////////////////////////////////////////////////////////////////////// class LgiClass GGlobalColour { class GGlobalColourPrivate *d; public: GGlobalColour(); ~GGlobalColour(); // Add all the colours first COLOUR AddColour(COLOUR c24); bool AddBitmap(GSurface *pDC); bool AddBitmap(GImageList *il); // Then call this bool MakeGlobalPalette(); // Which will give you a palette that // includes everything GPalette *GetPalette(); // Convert a bitmap to the global palette COLOUR GetColour(COLOUR c24); bool RemapBitmap(GSurface *pDC); }; /// This class is useful for double buffering in an OnPaint handler... class GDoubleBuffer { GSurface **In; GSurface *Screen; GMemDC Mem; GRect Rgn; bool Valid; public: GDoubleBuffer(GSurface *&pDC, GRect *Sub = NULL) : In(&pDC) { Rgn = Sub ? *Sub : pDC->Bounds(); Screen = pDC; Valid = pDC && Mem.Create(Rgn.X(), Rgn.Y(), pDC->GetColourSpace()); if (Valid) { *In = &Mem; if (Sub) pDC->SetOrigin(Sub->x1, Sub->y1); } } ~GDoubleBuffer() { if (Valid) { Mem.SetOrigin(0, 0); Screen->Blt(Rgn.x1, Rgn.y1, &Mem); } // Restore state *In = Screen; } }; #ifdef WIN32 typedef int (__stdcall *MsImg32_AlphaBlend)(HDC,int,int,int,int,HDC,int,int,int,int,BLENDFUNCTION); #endif /// Main singleton graphics device class. Holds all global data for graphics rendering. class LgiClass GdcDevice : public GCapabilityClient { friend class GScreenDC; friend class GMemDC; friend class GImageList; static GdcDevice *pInstance; class GdcDevicePrivate *d; #ifdef WIN32 MsImg32_AlphaBlend AlphaBlend; #endif public: GdcDevice(); ~GdcDevice(); static GdcDevice *GetInst() { return pInstance; } /// Returns the colour space of the screen GColourSpace GetColourSpace(); /// Returns the current screen bit depth int GetBits(); /// Returns the current screen width int X(); /// Returns the current screen height int Y(); /// Returns the size of the screen as a rectangle. GRect Bounds() { return GRect(0, 0, X()-1, Y()-1); } GGlobalColour *GetGlobalColour(); /// Set a global graphics option int GetOption(int Opt); /// Get a global graphics option int SetOption(int Opt, int Value); /// 256 lut for squares ulong *GetCharSquares(); /// Divide by 255 lut, 64k entries long. uchar *GetDiv255(); // Palette/Colour void SetGamma(double Gamma); double GetGamma(); // Palette void SetSystemPalette(int Start, int Size, GPalette *Pal); GPalette *GetSystemPalette(); void SetColourPaletteType(int Type); // Type = PALTYPE_xxx define COLOUR GetColour(COLOUR Rgb24, GSurface *pDC = NULL); // File I/O /// \brief Loads a image from a file /// /// This function uses the compiled in codecs, some of which require external /// shared libraries / DLL's to function. Other just need the right source to /// be compiled in. /// /// Lgi comes with the following image codecs: ///
    ///
  • Windows or OS/2 Bitmap: GdcBmp (GFilter.cpp) ///
  • PCX: GdcPcx (Pcx.cpp) ///
  • GIF: GdcGif (Gif.cpp and Lzw.cpp) ///
  • JPEG: GdcJpeg (Jpeg.cpp + libjpeg library) ///
  • PNG: GdcPng (Png.cpp + libpng library) ///
/// GSurface *Load ( /// The full path of the file const char *FileName, /// [Optional] Enable OS based loaders bool UseOSLoader = true ); /// The stream version of the file loader... GSurface *Load ( /// The full path of the file GStream *In, /// [Optional] File name hint for selecting a filter const char *Name = NULL, /// [Optional] Enable OS based loaders bool UseOSLoader = true ); + /// Save an image to a stream. + bool Save + ( + /// The file to write to + GStream *Out, + /// The pixels to store + GSurface *In, + /// Dummy file name to determine the file type, eg: "img.jpg" + const char *FileType + ); + /// Save an image to a file. bool Save ( /// The file to write to const char *Name, /// The pixels to store GSurface *pDC ); #if LGI_SDL SDL_Surface *Handle(); #endif }; /// \brief Defines a bitmap inline in C++ code. /// /// The easiest way I know of create the raw data for an GInlineBmp /// is to use i.Mage to /// load a file or create a image and then use the Edit->Copy As Code /// menu. Then paste into your C++ and put a uint32 array declaration /// around it. Then point the Data member to the uint32 array. Just be /// sure to get the dimensions right. /// /// I use this for embeding resource images directly into the code so /// that a) they load instantly and b) they can't get lost as a separate file. class LgiClass GInlineBmp { public: /// The width of the image. int X; /// The height of the image. int Y; /// The bitdepth of the image (8, 15, 16, 24, 32). int Bits; /// Pointer to the raw data. uint32_t *Data; /// Creates a memory DC of the image. GSurface *Create(uint32_t TransparentPx = 0xffffffff); }; // file filter support #include "GFilter.h" // globals #define GdcD GdcDevice::GetInst() /// Converts a context to a different bit depth LgiFunc GSurface *ConvertDC ( /// The source image GSurface *pDC, /// The destination bit depth int Bits ); /// Wrapper around GdcDevice::Load /// \deprecated Use GdcDevice::Load directly in new code. LgiFunc GSurface *LoadDC(const char *Name, bool UseOSLoader = true) DEPRECATED_POST; /// Wrapper around GdcDevice::Save /// \deprecated Use GdcDevice::Save directly in new code. LgiFunc bool WriteDC(const char *Name, GSurface *pDC) DEPRECATED_POST; /// Converts a colour to a different bit depth LgiFunc COLOUR CBit(int DstBits, COLOUR c, int SrcBits = 24, GPalette *Pal = 0); #ifdef __cplusplus /// blends 2 colours by the amount specified LgiClass GColour GdcMixColour(GColour a, GColour b, float HowMuchA = 0.5); #endif /// blends 2 24bit colours by the amount specified LgiFunc COLOUR GdcMixColour(COLOUR a, COLOUR b, float HowMuchA = 0.5); /// Turns a colour into an 8 bit grey scale representation LgiFunc COLOUR GdcGreyScale(COLOUR c, int Bits = 24); /// Colour reduction option to define what palette to go to enum GColourReducePalette { CR_PAL_NONE = -1, CR_PAL_CUBE = 0, CR_PAL_OPT, CR_PAL_FILE }; /// Colour reduction option to define how to deal with reduction error enum GColourReduceMatch { CR_MATCH_NONE = -1, CR_MATCH_NEAR = 0, CR_MATCH_HALFTONE, CR_MATCH_ERROR }; /// Colour reduction options class GReduceOptions { public: /// Type of palette GColourReducePalette PalType; /// Reduction error handling GColourReduceMatch MatchType; /// 1-256 int Colours; /// Specific palette to reduce to GPalette *Palette; GReduceOptions() { Palette = 0; Colours = 256; PalType = CR_PAL_NONE; MatchType = CR_MATCH_NONE; } }; /// Reduces a images colour depth LgiFunc bool GReduceBitDepth(GSurface *pDC, int Bits, GPalette *Pal = 0, GReduceOptions *Reduce = 0); struct GColourStop { COLOUR Colour; float Pos; }; /// Draws a horizontal or vertical gradient LgiFunc void LgiFillGradient(GSurface *pDC, GRect &r, bool Vert, GArray &Stops); #ifdef WIN32 /// Draws a windows HICON onto a surface at Dx, Dy LgiFunc void LgiDrawIcon(GSurface *pDC, int Dx, int Dy, HICON ico); #endif /// Row copy operator for full RGB (8 bit components) LgiFunc bool LgiRopRgb ( // Pointer to destination pixel buffer uint8_t *Dst, // Destination colour space (must be 8bit components) GColourSpace DstCs, // Pointer to source pixel buffer (if this overlaps 'Dst', set 'Overlap' to true) uint8_t *Src, // Source colour space (must be 8bit components) GColourSpace SrcCs, // Number of pixels to convert int Px, // Whether to composite using alpha or copy blt bool Composite ); /// Universal bit blt method LgiFunc bool LgiRopUniversal(GBmpMem *Dst, GBmpMem *Src, bool Composite); /// Gets the screens DPI LgiFunc int LgiScreenDpi(); /// Find the bounds of an image. /// \return true if there is some non-transparent image in 'rc' LgiFunc bool LgiFindBounds ( /// [in] The image GSurface *pDC, /// [in/out] Starts off as the initial bounds to search. /// Returns the non-background area. GRect *rc ); #if defined(LGI_SDL) LgiFunc GColourSpace PixelFormat2ColourSpace(SDL_PixelFormat *pf); #elif defined(BEOS) LgiFunc GColourSpace BeosColourSpaceToLgi(color_space cs); #endif #endif diff --git a/src/common/Gdc2/Filters/GFilter.cpp b/src/common/Gdc2/Filters/GFilter.cpp --- a/src/common/Gdc2/Filters/GFilter.cpp +++ b/src/common/Gdc2/Filters/GFilter.cpp @@ -1,2218 +1,2224 @@ /** \file \author Matthew Allen \date 25/3/97 \brief Graphics file filters */ #if WINDOWS #include #include #include "Lgi.h" #ifdef _MSC_VER #include #endif #else #define BI_RGB 0L #define BI_RLE8 1L #define BI_RLE4 2L #define BI_BITFIELDS 3L #endif #include #include #include #include #include "Lgi.h" #include "GString.h" #include "GVariant.h" #include "GClipBoard.h" #include "GToken.h" #include "GPalette.h" int FindHeader(int Offset, const char *Str, GStream *f) { int i = 0; if (Offset >= 0) { f->SetPos(Offset); } while (true) { char c; if (!f->Read(&c, 1)) break; if (Str[i] == c || Str[i] == '?') { i++; if (!Str[i]) { return true; } } else { i = 0; } } return false; } /// Windows and OS/2 BMP file filter class GdcBmp : public GFilter { int ActualBits; int ScanSize; public: int GetCapabilites() { return FILTER_CAP_READ | FILTER_CAP_WRITE; } Format GetFormat() { return FmtBmp; } /// Reads a BMP file IoStatus ReadImage(GSurface *Out, GStream *In); /// Writes a Windows BMP file IoStatus WriteImage(GStream *Out, GSurface *In); bool GetVariant(const char *n, GVariant &v, char *a) { if (!stricmp(n, LGI_FILTER_TYPE)) { v = "Windows or OS/2 Bitmap"; } else if (!stricmp(n, LGI_FILTER_EXTENSIONS)) { v = "BMP"; } else return false; return true; } }; class GdcBmpFactory : public GFilterFactory { bool CheckFile(const char *File, int Access, const uchar *Hint) { if (File) { const char *Ext = LgiGetExtension((char*)File); if (Ext && !_stricmp(Ext, "bmp")) return true; } if (Hint) { if (Hint[0] == 'B' && Hint[1] == 'M') return true; } return false; } GFilter *NewObject() { return new GdcBmp; } } BmpFactory; #define BMP_ID 0x4D42 #ifdef WIN32 #pragma pack(push, before_pack) #pragma pack(1) #endif class BMP_FILE { public: char Type[2]; uint32_t Size; uint16 Reserved1; uint16 Reserved2; uint32_t OffsetToData; }; class BMP_WININFO { public: // Win2 hdr (12 bytes) int32 Size; int32 Sx; int32 Sy; uint16 Planes; uint16 Bits; // Win3 hdr (40 bytes) uint32_t Compression; uint32_t DataSize; int32 XPels; int32 YPels; uint32_t ColoursUsed; uint32_t ColourImportant; // Win4 hdr (108 bytes) uint32_t RedMask; /* Mask identifying bits of red component */ uint32_t GreenMask; /* Mask identifying bits of green component */ uint32_t BlueMask; /* Mask identifying bits of blue component */ uint32_t AlphaMask; /* Mask identifying bits of alpha component */ uint32_t CSType; /* Color space type */ uint32_t RedX; /* X coordinate of red endpoint */ uint32_t RedY; /* Y coordinate of red endpoint */ uint32_t RedZ; /* Z coordinate of red endpoint */ uint32_t GreenX; /* X coordinate of green endpoint */ uint32_t GreenY; /* Y coordinate of green endpoint */ uint32_t GreenZ; /* Z coordinate of green endpoint */ uint32_t BlueX; /* X coordinate of blue endpoint */ uint32_t BlueY; /* Y coordinate of blue endpoint */ uint32_t BlueZ; /* Z coordinate of blue endpoint */ uint32_t GammaRed; /* Gamma red coordinate scale value */ uint32_t GammaGreen; /* Gamma green coordinate scale value */ uint32_t GammaBlue; /* Gamma blue coordinate scale value */ bool Read(GStream &f) { ZeroObj(*this); int64 Start = f.GetPos(); #define Rd(var) if (f.Read(&var, sizeof(var)) != sizeof(var)) \ { LgiTrace("Bmp.Read(%i) failed\n", (int)sizeof(var)); return false; } Rd(Size); // 4 Rd(Sx); // 4 Rd(Sy); // 4 Rd(Planes); // 2 Rd(Bits); // 2 // = 16 if (Size >= 40) { Rd(Compression); // 4 Rd(DataSize); // 4 Rd(XPels); // 4 Rd(YPels); // 4 Rd(ColoursUsed); // 4 Rd(ColourImportant); // 4 // = 24 + 16 = 40 } if (Size >= 52) { Rd(RedMask); // 4 Rd(GreenMask); // 4 Rd(BlueMask); // 4 // 12 + 40 = 52 } if (Size >= 56) { Rd(AlphaMask); // 4 // = 4 + 52 = 56 } if (Size >= 108) { Rd(CSType); // 4 Rd(RedX); // 4 Rd(RedY); // 4 Rd(RedZ); // 4 Rd(GreenX); // 4 Rd(GreenY); // 4 Rd(GreenZ); // 4 Rd(BlueX); // 4 Rd(BlueY); // 4 Rd(BlueZ); // 4 Rd(GammaRed); // 4 Rd(GammaGreen); // 4 Rd(GammaBlue); // 4 // = 52 + 56 = 108 } int64 End = f.GetPos(); int64 Bytes = End - Start; return Bytes >= 12; } }; #ifdef WIN32 #pragma pack(pop, before_pack) #endif static int CountSetBits(uint32_t b) { int Count = 0; for (int i=0; iMask < a->Mask ? 1 : -1; } int DownSort(MaskComp *a, MaskComp *b) { return b->Mask > a->Mask ? 1 : -1; } GFilter::IoStatus GdcBmp::ReadImage(GSurface *pDC, GStream *In) { if (!pDC || !In) return GFilter::IoError; ActualBits = 0; ScanSize = 0; BMP_FILE File; BMP_WININFO Info; GRect Cr; Read(In, &File.Type, sizeof(File.Type)); Read(In, &File.Size, sizeof(File.Size)); Read(In, &File.Reserved1, sizeof(File.Reserved1)); Read(In, &File.Reserved2, sizeof(File.Reserved2)); Read(In, &File.OffsetToData, sizeof(File.OffsetToData)); if (!Info.Read(*In)) { LgiTrace("%s:%i - BmpHdr read failed.\n", _FL); return GFilter::IoError; } if (File.Type[0] != 'B' || File.Type[1] != 'M') { LgiTrace("%s:%i - Bmp file id failed: '%.2s'.\n", _FL, File.Type); return GFilter::IoUnsupportedFormat; } ActualBits = Info.Bits; ScanSize = BMPWIDTH(Info.Sx * Info.Bits); int MemBits = MAX(Info.Bits, 8); if (!pDC->Create(Info.Sx, Info.Sy, GBitsToColourSpace(MemBits), ScanSize)) { LgiTrace("%s:%i - MemDC(%i,%i,%i) failed.\n", _FL, Info.Sx, Info.Sy, MAX(Info.Bits, 8)); return GFilter::IoError; } if (pDC->GetBits() <= 8) { int Colours = 1 << ActualBits; GPalette *Palette = new GPalette; if (Palette) { Palette->SetSize(Colours); GdcRGB *Start = (*Palette)[0]; if (Start) { In->Read(Start, sizeof(GdcRGB)*Colours); Palette->SwapRAndB(); } Palette->Update(); pDC->Palette(Palette); } } GBmpMem *pMem = GetSurface(pDC); In->SetPos(File.OffsetToData); GFilter::IoStatus Status = IoSuccess; if (Info.Compression == BI_RLE8) { // 8 bit RLE compressed image int64 Remaining = In->GetSize() - In->GetPos(); uchar *Data = new uchar[Remaining]; if (Data) { if (In->Read(Data, (int)Remaining) == Remaining) { int x=0, y=pDC->Y()-1; uchar *p = Data; bool Done = false; while (!Done && p < Data + Remaining) { uchar Length = *p++; uchar Colour = *p++; if (Length == 0) { switch (Colour) { case 0: { x = 0; y--; break; } case 1: { Done = true; break; } case 2: { x += *p++; y -= *p++; break; } default: { // absolute mode uchar *Pixel = (*pDC)[y]; if (Pixel && y >= 0 && y < pDC->Y()) { int Len = MIN(Colour, pDC->X() - x); if (Len > 0) { memcpy(Pixel + x, p, Len); x += Colour; p += Colour; } } else { p += Colour; } if ((NativeInt) p & 1) p++; break; } } } else { // encoded mode uchar *Pixel = (*pDC)[y]; if (Pixel && y >= 0 && y < pDC->Y()) { int Len = MIN(Length, pDC->X() - x); if (Len > 0) { memset(Pixel + x, Colour, Len); x += Length; } } } } } } } else if (Info.Compression == BI_RLE4) { // 4 bit RLE compressed image // not implemented LgiTrace("%s:%i - BI_RLE4 not implemented.\n", _FL); } else { // Progress if (Meter) { Meter->SetDescription("scanlines"); Meter->SetLimits(0, pMem->y-1); } GColourSpace SrcCs = CsNone; switch (ActualBits) { case 8: SrcCs = CsIndex8; break; case 15: SrcCs = CsRgb15; break; case 16: SrcCs = CsRgb16; break; case 24: SrcCs = CsBgr24; break; case 32: SrcCs = CsBgra32; break; } #if 1 if (Info.Compression == BI_BITFIELDS) { // Should we try and create a colour space from these fields? GArray Comps; Comps.New().Set(CtRed, Info.RedMask); Comps.New().Set(CtGreen, Info.GreenMask); Comps.New().Set(CtBlue, Info.BlueMask); if (Info.AlphaMask) Comps.New().Set(CtAlpha, Info.AlphaMask); Comps.Sort(ActualBits == 16 ? DownSort : UpSort); GColourSpaceBits Cs; Cs.All = 0; for (int i=0; iy-1; y>=0; y--) { if (In->Read(Buffer, ScanSize) == ScanSize) { uchar Mask = 0x80; uchar *d = Buffer; for (int x=0; xX(); x++) { pDC->Colour((*d & Mask) ? 1 : 0); pDC->Set(x, y); Mask >>= 1; if (!Mask) { Mask = 0x80; d++; } } } else { Status = IoError; break; } if (Meter) Meter->Value(pMem->y-1-y); } DeleteArray(Buffer); } break; } case 4: { uchar *Buffer = new uchar[ScanSize]; if (Buffer) { for (int y=pMem->y-1; y>=0; y--) { if (In->Read(Buffer, ScanSize) != ScanSize) { Status = IoError; break; } uchar *d = Buffer; for (int x=0; xX(); x++) { if (x & 1) { pDC->Colour(*d & 0xf); d++; } else { pDC->Colour(*d >> 4); } pDC->Set(x, y); } if (Meter) Meter->Value(pMem->y-1-y); } DeleteArray(Buffer); } break; } default: { GColourSpace DstCs = pDC->GetColourSpace(); for (int i=pMem->y-1; i>=0; i--) { uint8_t *Ptr = pMem->Base + (pMem->Line * i); ssize_t r = In->Read(Ptr, ScanSize); if (r != ScanSize) { Status = IoError; LgiTrace("%s:%i - Bmp read err, wanted %i, got %i.\n", _FL, ScanSize, r); break; } if (DstCs != SrcCs) { if (!LgiRopRgb(Ptr, DstCs, Ptr, SrcCs, pMem->x, false)) { Status = IoUnsupportedFormat; LgiTrace("%s:%i - Bmp had unsupported bit depth.\n", _FL); break; } } if (Meter) Meter->Value(pMem->y-1-i); } break; } } } Cr.ZOff(pDC->X()-1, pDC->Y()-1); pDC->ClipRgn(&Cr); pDC->Update(GDC_BITS_CHANGE|GDC_PAL_CHANGE); return Status; } template ssize_t SwapWrite(GStream *Out, T v) { #if 0 // __ORDER_BIG_ENDIAN__ uint8 *s = (uint8*)&v; uint8 *e = s + sizeof(v) - 1; while (s < e) { uint8 tmp = *s; *s++ = *e; *e-- = tmp; } #endif return Out->Write(&v, sizeof(v)); } GFilter::IoStatus GdcBmp::WriteImage(GStream *Out, GSurface *pDC) { GFilter::IoStatus Status = IoError; if (!pDC || !Out) return GFilter::IoError; BMP_FILE File; BMP_WININFO Info; GBmpMem *pMem = GetSurface(pDC); int Colours = (pMem->Cs == CsIndex8) ? 1 << 8 : 0; int UsedBits = GColourSpaceToBits(pMem->Cs); GPalette *Palette = pDC->Palette(); if (pMem->Cs == CsIndex8 && Palette) { int Size = Palette->GetSize(); int Bits = 8; for (int c=256; c; c>>=1, Bits--) { if (c & Size) { Colours = c; UsedBits = Bits; break; } } } if (!pMem || !pMem->x || !pMem->y) { return GFilter::IoError; } Out->SetSize(0); #define Wr(var) SwapWrite(Out, var) Info.Compression = UsedBits == 16 || UsedBits == 32 ? BI_BITFIELDS : BI_RGB; int BitFieldSize = Info.Compression == BI_BITFIELDS ? 16 : 0; Info.Size = 40 + BitFieldSize; File.Type[0] = 'B'; File.Type[1] = 'M'; File.OffsetToData = 14 + Info.Size; File.Size = File.OffsetToData + (int)(ABS(pMem->Line) * pMem->y); File.Reserved1 = 0; File.Reserved2 = 0; Info.Sx = pMem->x; Info.Sy = pMem->y; Info.Planes = 1; Info.Bits = UsedBits; Info.DataSize = 0; Info.XPels = 3000; Info.YPels = 3000; Info.ColoursUsed = Colours; Info.ColourImportant = Colours; bool Written = Out->Write(File.Type, 2) && Wr(File.Size) && Wr(File.Reserved1) && Wr(File.Reserved2) && Wr(File.OffsetToData); Written = Wr(Info.Size) && Wr(Info.Sx) && Wr(Info.Sy) && Wr(Info.Planes) && Wr(Info.Bits) && Wr(Info.Compression) && Wr(Info.DataSize) && Wr(Info.XPels) && Wr(Info.YPels) && Wr(Info.ColoursUsed) && Wr(Info.ColourImportant); if (Written) { if (pMem->Cs == CsIndex8) { int Done = 0; if (Palette) { GdcRGB *Start = (*Palette)[0]; if (Start) { Palette->SwapRAndB(); Out->Write(Start, sizeof(GdcRGB)*Palette->GetSize()); Palette->SwapRAndB(); Done += Palette->GetSize(); } } char Grey[4]; Grey[3] = 0; while (Done < Colours) { int C = Done * 256 / Colours; Grey[0] = C; Grey[1] = C; Grey[2] = C; Out->Write(Grey, 4); Done++; } } // Progress if (Meter) { Meter->SetDescription("scanlines"); Meter->SetLimits(0, pMem->y-1); } int Bytes = BMPWIDTH(pMem->x * UsedBits); Status = GFilter::IoSuccess; switch (UsedBits) { case 1: { uchar *Buffer = new uchar[Bytes]; if (Buffer) { for (int i=pMem->y-1; i>=0; i--) { uchar *Src = pMem->Base + (i * pMem->Line); // assemble the bits memset(Buffer, 0, Bytes); for (int x=0; xx; x++) { Buffer[x>>3] |= (Src[x] & 1) << (x & 3); } // write the bits if (Out->Write(Buffer, Bytes) != Bytes) { Status = IoError; break; } // update status if (Meter) Meter->Value(pMem->y-1-i); } DeleteArray(Buffer); } break; } case 4: { uchar *Buffer = new uchar[Bytes]; if (Buffer) { for (int i=pMem->y-1; i>=0; i--) { uchar *Src = pMem->Base + (i * pMem->Line); // assemble the nibbles for (int x=0; xx; x+=2) { Buffer[x>>1] = (Src[x]<<4) | (Src[x+1]&0x0f); } // write the line if (Out->Write(Buffer, Bytes) != Bytes) { Status = IoError; break; } // update status if (Meter) Meter->Value(pMem->y-1-i); } DeleteArray(Buffer); } break; } default: { if (UsedBits == 16) { System16BitPixel px[8]; ZeroObj(px); px[0].r = 0x1f; px[2].g = 0x3f; px[4].b = 0x1f; Out->Write(px, sizeof(px)); } else if (UsedBits == 32) { System32BitPixel px[4]; ZeroObj(px); px[0].r = 0xff; px[1].g = 0xff; px[2].b = 0xff; px[3].a = 0xff; Out->Write(px, sizeof(px)); } for (int i=pMem->y-1; i>=0; i--) { ssize_t w = Out->Write(pMem->Base + (i * pMem->Line), Bytes); if (w != Bytes) { Status = IoError; break; } if (Meter) Meter->Value(pMem->y-1-i); } break; } } } Out->Close(); return Status; } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // ICO file format class GdcIco : public GFilter { int TruncSize(int i) { if (i >= 64) return 64; if (i >= 32) return 32; return 16; } public: GdcIco(); Format GetFormat() { return FmtIco; } int GetCapabilites() { return FILTER_CAP_READ | FILTER_CAP_WRITE; } int GetImages() { return 1; } IoStatus ReadImage(GSurface *pDC, GStream *In); IoStatus WriteImage(GStream *Out, GSurface *pDC); bool GetVariant(const char *n, GVariant &v, char *a); }; class GdcIcoFactory : public GFilterFactory { bool CheckFile(const char *File, int Access, const uchar *Hint) { return (File) ? stristr(File, ".ico") != 0 : false; } GFilter *NewObject() { return new GdcIco; } } IcoFactory; GdcIco::GdcIco() { } bool GdcIco::GetVariant(const char *n, GVariant &v, char *a) { if (!stricmp(n, LGI_FILTER_TYPE)) { v = "Icon file"; } else if (!stricmp(n, LGI_FILTER_EXTENSIONS)) { v = "ICO"; } else return false; return true; } GFilter::IoStatus GdcIco::ReadImage(GSurface *pDC, GStream *In) { GFilter::IoStatus Status = IoError; int MyBits = 0; int16 Reserved_1; int16 Type; int16 Count; Read(In, &Reserved_1, sizeof(Reserved_1)); Read(In, &Type, sizeof(Type)); Read(In, &Count, sizeof(Count)); for (int Image = 0; Image < Count; Image++) { int8 Width; int8 Height; int8 ColorCount; int8 Reserved_2; int16 Planes; int16 BitCount; int32 BytesInRes; int32 ImageOffset; Read(In, &Width, sizeof(Width)); Read(In, &Height, sizeof(Height)); Read(In, &ColorCount, sizeof(ColorCount)); Read(In, &Reserved_2, sizeof(Reserved_2)); Read(In, &Planes, sizeof(Planes)); Read(In, &BitCount, sizeof(BitCount)); Read(In, &BytesInRes, sizeof(BytesInRes)); Read(In, &ImageOffset, sizeof(ImageOffset)); int64 BytesLeft = BytesInRes; int64 CurrentPos = In->GetPos(); In->SetPos(ImageOffset); BMP_WININFO Header; GdcRGB *Colours; uchar *XorBytes = 0; uchar *AndBytes = 0; int64 StartHdrPos = In->GetPos(); if (!Header.Read(*In)) return GFilter::IoError; BytesLeft -= In->GetPos() - StartHdrPos; if (!Header.Sx) Header.Sx = Width; if (!Header.Sy) Header.Sy = Height; if (!Header.Bits) { if (BitCount) Header.Bits = BitCount; else if (ColorCount) { for (int i=1; i<=8; i++) { if (1 << i >= ColorCount) { BitCount = Header.Bits = i; break; } } } } Colours = new GdcRGB[ColorCount]; if (Colours) { In->Read(Colours, sizeof(GdcRGB) * ColorCount); BytesLeft -= sizeof(GdcRGB) * ColorCount; } Header.Sy >>= 1; int XorSize = BMPWIDTH(Header.Sx * Header.Bits) * Height; int AndSize = BMPWIDTH(Header.Sx) * Height; if (BytesLeft >= XorSize) { XorBytes = new uchar[XorSize]; if (XorBytes) { In->Read(XorBytes, XorSize); BytesLeft -= XorSize; } } if (BytesLeft >= AndSize) { AndBytes = new uchar[AndSize]; if (AndBytes) { In->Read(AndBytes, AndSize); BytesLeft -= AndSize; } } /* LgiTrace("BytesInRes=%i, Xor=%i, And=%i, Pal=%i, BytesLeft=%i\n", BytesInRes, XorSize, AndSize, sizeof(GdcRGB) * Cols, BytesLeft); */ if (Colours && XorBytes && (Header.Bits > MyBits || Width > pDC->X() || Height > pDC->Y()) && pDC->Create(Width, Height, GBitsToColourSpace(MAX(8, Header.Bits)) )) { MyBits = Header.Bits; pDC->Colour(0, 24); pDC->Rectangle(); GPalette *Pal = new GPalette; if (Pal) { Pal->SetSize(ColorCount); memcpy((*Pal)[0], Colours, sizeof(GdcRGB) * ColorCount); Pal->SwapRAndB(); pDC->Palette(Pal, true); } if (AndBytes) { pDC->HasAlpha(true); } GSurface *pAlpha = pDC->AlphaDC(); int XorLine = XorSize / Height; int AndLine = AndSize / Height; for (int y=0; y>3] & (0x80 >> (x&7))) ? 0 : 0xff; } if (Src[x/8] & m) { Dest[x] = 1; } else { Dest[x] = 0; } m >>= 1; if (!m) m = 0x80; } Status = IoSuccess; break; } case 4: { for (int x=0; x>3] & (0x80 >> (x&7))) ? 0 : 0xff; } if (x & 1) { Dest[x] = Src[x>>1] & 0xf; } else { Dest[x] = Src[x>>1] >> 4; } } Status = IoSuccess; break; } case 8: { for (int x=0; x>3] & (0x80 >> (x&7))) ? 0 : 0xff; } Dest[x] = Src[x]; } Status = IoSuccess; break; } } } } else { LgiTrace("%s:%i - Header size error: %i != %i + %i, Img: %ix%i @ %i bits\n", _FL, Header.DataSize, XorSize, AndSize, Header.Sx, Header.Sy, Header.Bits); } DeleteArray(Colours); DeleteArray(XorBytes); DeleteArray(AndBytes); In->SetPos(CurrentPos); } return Status; } GFilter::IoStatus GdcIco::WriteImage(GStream *Out, GSurface *pDC) { GFilter::IoStatus Status = IoError; if (!pDC || pDC->GetBits() > 8 || !Out) return GFilter::IoError; Out->SetSize(0); int ActualBits = pDC->GetBits(); GPalette *Pal = pDC->Palette(); if (Pal) { if (Pal->GetSize() <= 2) { ActualBits = 1; } else if (Pal->GetSize() <= 16) { ActualBits = 4; } } // write file header int Colours = 1 << ActualBits; int16 Reserved_1 = 0; int16 Type = 1; int16 Count = 1; Write(Out, &Reserved_1, sizeof(Reserved_1)); Write(Out, &Type, sizeof(Type)); Write(Out, &Count, sizeof(Count)); // write directory list int8 Width = TruncSize(pDC->X()); int8 Height = TruncSize(pDC->Y()); int8 ColorCount = Colours; int8 Reserved_2 = 0; int16 Planes = 0; int16 BitCount = 0; int Line = (ActualBits * Width) / 8; int MaskLine = BMPWIDTH(Width/8); int32 BytesInRes = sizeof(BMP_WININFO) + (sizeof(GdcRGB) * Colours) + (Line * Height) + (MaskLine * Height); Write(Out, &Width, sizeof(Width)); Write(Out, &Height, sizeof(Height)); Write(Out, &ColorCount, sizeof(ColorCount)); Write(Out, &Reserved_2, sizeof(Reserved_2)); Write(Out, &Planes, sizeof(Planes)); Write(Out, &BitCount, sizeof(BitCount)); Write(Out, &BytesInRes, sizeof(BytesInRes)); int32 ImageOffset = (int32)(Out->GetPos() + sizeof(ImageOffset)); Write(Out, &ImageOffset, sizeof(ImageOffset)); // Write icon itself BMP_WININFO Header; LgiAssert(sizeof(Header) == 40); Header.Size = sizeof(Header); Header.Sx = Width; Header.Sy = Height * 2; Header.Planes = 1; Header.Bits = ActualBits; Header.Compression = 0; // BI_RGB; Header.DataSize = (Line * Height) + (MaskLine * Height); Header.XPels = 0; Header.YPels = 0; Header.ColoursUsed = 0; Header.ColourImportant = 0; Out->Write(&Header, sizeof(Header)); // Write palette if (Pal) { Pal->SwapRAndB(); Out->Write((*Pal)[0], sizeof(GdcRGB) * (int)(1 << ActualBits)); Pal->SwapRAndB(); } // Get background colour COLOUR Back = 0xffffffff; if (Props) { GVariant v; if (Props->GetValue(LGI_FILTER_BACKGROUND, v)) Back = v.CastInt32(); } // Write "Colour" bits int y; for (y=Height-1; y>=0; y--) { uchar *Src = (*pDC)[y]; // xor bits (colour) switch (ActualBits) { case 4: { uchar Dest = 0; for (int x=0; x=0; y--) { uchar *Src = (*pDC)[y]; uchar Dest = 0; uchar Mask = 0x80; int x; for (x = 0; x < Width; x++) { if (Src[x] == Back) Dest |= Mask; Mask >>= 1; if (!Mask) { Write(Out, &Dest, sizeof(Dest)); Dest = 0; Mask = 0x80; } } x = Width / 8; while (x < MaskLine) { uchar c = 0; Write(Out, &c, sizeof(c)); x++; } Status = IoSuccess; } return Status; } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // Run length encoded DC GdcRleDC::GdcRleDC() { Flags = GDC_RLE_COLOUR | GDC_RLE_READONLY; Length = 0; Alloc = 0; Data = 0; ScanLine = 0; Key = 0; pMem = new GBmpMem; if (pMem) { pMem->Base = 0; pMem->x = 0; pMem->y = 0; pMem->Cs = CsNone; pMem->Line = 0; pMem->Flags = 0; } } GdcRleDC::~GdcRleDC() { Empty(); } void GdcRleDC::Empty() { Length = 0; Alloc = 0; DeleteArray(Data); DeleteArray(ScanLine); } bool GdcRleDC::Create(int x, int y, GColourSpace cs, int flags) { bool Status = GMemDC::Create(x, y, cs, flags); if (Status) { Flags &= ~GDC_RLE_READONLY; } return Status; } bool GdcRleDC::CreateInfo(int x, int y, GColourSpace cs) { bool Status = false; DeleteObj(pMem); pMem = new GBmpMem; if (pMem) { pMem->x = x; pMem->y = y; pMem->Cs = cs; pMem->Base = 0; pMem->Line = 0; pMem->Flags = 0; Flags |= GDC_RLE_READONLY; if (cs == CsIndex8) { Flags |= GDC_RLE_MONO; } } return Status; } void GdcRleDC::ReadOnly(bool Read) { if (Read) { Flags |= GDC_RLE_READONLY; } else { if (pMem) { pMem->Base = new uchar[pMem->Line * pMem->y]; } else { // just in case... shouldn't ever happen // am I paranoid? // 'course :) Create(1, 1, System24BitColourSpace); } Flags &= ~GDC_RLE_READONLY; } } bool GdcRleDC::ReadOnly() { return (Flags & GDC_RLE_READONLY) != 0; } void GdcRleDC::Mono(bool Mono) { if (Mono) { Flags |= GDC_RLE_MONO; Flags &= ~GDC_RLE_COLOUR; } else { Flags &= ~GDC_RLE_MONO; Flags |= GDC_RLE_COLOUR; } } bool GdcRleDC::Mono() { return (Flags & GDC_RLE_MONO) != 0; } bool GdcRleDC::SetLength(ssize_t Len) { bool Status = true; if (Len + 1 > Alloc) { int NewAlloc = (Len + 0x4000) & (~0x3FFF); uchar *Temp = new uchar[NewAlloc]; if (Temp) { if (NewAlloc > Length) { memset(Temp + Length, 0, NewAlloc - Length); } memcpy(Temp, Data, Length); DeleteArray(Data); DeleteArray(ScanLine); Data = Temp; Length = Len; Alloc = NewAlloc; } else { Status = false; } } else { // TODO: resize Data to be smaller if theres a // considerable size change Length = Len; } return Status; } void GdcRleDC::Update(int UpdateFlags) { bool Error = false; if ( (UpdateFlags & GDC_BITS_CHANGE) && !(Flags & GDC_RLE_READONLY)) { Empty(); COLOUR Key = Get(0, 0); ssize_t Pos = 0; ulong PixelSize = GetBits() / 8; for (int y=0; !Error && y Length) { Error = true; } } } if (Error) { DeleteArray(ScanLine); } else { Status = true; } } return Status; } void GdcRleDC::Draw(GSurface *Dest, int Ox, int Oy) { int OriX, OriY; Dest->GetOrigin(OriX, OriY); Ox += OriX; Oy += OriY; if (Dest && Data) { bool ReBuildScanInfo = false; if (!ScanLine) ReBuildScanInfo = true; if (ScanLine && Data && ScanLine[0] != Data) ReBuildScanInfo = true; if (ReBuildScanInfo) { // need to rebuild scan line info if (!FindScanLines()) { // couldn't create scan line info so quit return; } } GRect S; S.ZOff(X()-1, Y()-1); GRect D; D = S; D.Offset(Ox, Oy); GRect DClip = D; GRect Temp = Dest->ClipRgn(); D.Bound(&Temp); if (DClip == D && (*Dest)[0]) { // no clipping needed int PixLen = Dest->GetBits() / 8; if (Flags & GDC_RLE_COLOUR) { if (Dest->GetBits() == GetBits()) { for (int y=0; yApplicator(); for (int y=0; ySetPtr(Ox+x, Oy+y); pDApp->Rectangle((int)Pixels, 1); x += Pixels; } } } } else if (DClip.Valid()) { // clip int PixLen = Dest->GetBits() / 8; if (Flags & GDC_RLE_COLOUR) { if (Dest->GetBits() == GetBits()) { for (int y=0; y= Temp.y1 && Fy < Temp.y2) // clip y { uchar *s = ScanLine[y]; uchar *d = (*Dest)[Fy]; for (int x=0; x 0) // clip x { memcpy( d + ((Fx - PreClipPixels) * PixLen), s + (PreClipPixels * PixLen), PixelsLeft * PixLen); } x += Pixels; s += Pixels * PixLen; } } } } } /* else if (Flags & GDC_RLE_MONO) { GApplicator *pDApp = Dest->Applicator(); for (int y=0; ySetPtr(Ox+x, Oy+y); pDApp->Rectangle(Pixels, 1); x += Pixels; } } } */ } } } ssize_t GdcRleDC::SizeOf() { return (sizeof(int) * 5) + Length; } bool GdcRleDC::Read(GFile &F) { Empty(); int32 Len = 0, x = 0, y = 0, bits = 0; int8 Monochrome; F >> Len; if (SetLength(Len)) { F >> Key; F >> x; F >> y; F >> bits; F >> Monochrome; Mono(Monochrome != 0); CreateInfo(x, y, GBitsToColourSpace(bits)); F.Read(Data, Len); return !F.GetStatus(); } return false; } bool GdcRleDC::Write(GFile &F) { if (Data) { char Monochrome = Mono(); F << Length; F << Key; F << X(); F << Y(); F << GetBits(); F << Monochrome; F.Write(Data, Length); return !F.GetStatus(); } return false; } void GdcRleDC::Move(GdcRleDC *pDC) { if (pDC) { Key = pDC->Key; Flags = pDC->Flags; Length = pDC->Length; Alloc = pDC->Alloc; Data = pDC->Data; ScanLine = pDC->ScanLine; pMem = pDC->pMem; pPalette = pDC->pPalette; pDC->pPalette = 0; pDC->pMem = 0; pDC->Length = 0; pDC->Alloc = 0; pDC->Data = 0; pDC->ScanLine = 0; } } //////////////////////////////////////////////////////////////////// GFilterFactory *GFilterFactory::First = 0; GFilterFactory::GFilterFactory() { // add this filter to the global list Next = First; First = this; } GFilterFactory::~GFilterFactory() { // delete from the global list if (First == this) { First = First->Next; } else { GFilterFactory *i = First; while (i->Next && i->Next != this) { i = i->Next; } if (i->Next == this) { i->Next = Next; } else { // we aren't in the list // thats bad LgiAssert(0); } } } GFilter *GFilterFactory::New(const char *File, int Access, const uchar *Hint) { GFilterFactory *i = First; while (i) { if (i->CheckFile(File, Access, Hint)) { return i->NewObject(); } i = i->Next; } return 0; } GFilter *GFilterFactory::NewAt(int n) { int Status = 0; GFilterFactory *i = First; while (i) { if (Status++ == n) { return i->NewObject(); } i = i->Next; } return 0; } int GFilterFactory::GetItems() { int Status = 0; GFilterFactory *i = First; while (i) { Status++; i = i->Next; } return Status; } ////////////////////////////////////////////////////////////////////////// /// Legacy wrapper that calls the new method GSurface *LoadDC(const char *Name, bool UseOSLoader) { return GdcD->Load(Name, UseOSLoader); } GSurface *GdcDevice::Load(const char *Name, bool UseOSLoader) { if (!FileExists(Name)) { // Loading non-file resource... #if WINNATIVE GAutoWString WName(Utf8ToWide(Name)); // a resource... lock and load gentlemen HRSRC hRsrc = FindResource(NULL, WName, RT_BITMAP); if (hRsrc) { int Size = SizeofResource(NULL, hRsrc); HGLOBAL hMem = LoadResource(NULL, hRsrc); if (hMem) { uchar *Ptr = (uchar*) LockResource(hMem); if (Ptr) { GClipBoard Clip(NULL); GSurface *pDC = Clip.ConvertFromPtr(Ptr); GlobalUnlock(hMem); return pDC; } } } #endif return NULL; } GFile File; if (!File.Open(Name, O_READ)) { LgiTrace("%s:%i - Couldn't open '%s' for reading.\n", _FL, Name); return NULL; } return Load(&File, Name, UseOSLoader); } GSurface *GdcDevice::Load(GStream *In, const char *Name, bool UseOSLoader) { if (!In) return NULL; GFilter::IoStatus FilterStatus = GFilter::IoError; int64 Size = In->GetSize(); if (Size <= 0) { return NULL; } GAutoPtr Hint(new uchar[16]); memset(Hint, 0, 16); if (In->Read(Hint, 16) == 0) { Hint.Reset(0); } int64 SeekResult = In->SetPos(0); if (SeekResult != 0) { LgiTrace("%s:%i - Seek failed after reading hint.\n", _FL); return NULL; } GAutoPtr Filter(GFilterFactory::New(Name, FILTER_CAP_READ, Hint)); GAutoPtr pDC; if (Filter && pDC.Reset(new GMemDC)) { FilterStatus = Filter->ReadImage(pDC, In); if (FilterStatus != GFilter::IoSuccess) { pDC.Reset(); LgiTrace("%s:%i - Filter couldn't cope with '%s'.\n", _FL, Name); } } if (UseOSLoader && !pDC) { #if LGI_SDL #elif defined MAC && !defined COCOA CGImageRef Img = NULL; if (FileExists(Name)) { CFURLRef FileUrl = CFURLCreateFromFileSystemRepresentation(0, (const UInt8*)Name, strlen(Name), false); if (!FileUrl) LgiTrace("%s:%i - CFURLCreateFromFileSystemRepresentation failed.\n", _FL); else { CGImageSourceRef Src = CGImageSourceCreateWithURL(FileUrl, 0); if (!Src) LgiTrace("%s:%i - CGImageSourceCreateWithURL failed.\n", _FL); else { Img = CGImageSourceCreateImageAtIndex(Src, 0, 0); if (!Img) LgiTrace("%s:%i - CGImageSourceCreateImageAtIndex failed.\n", _FL); CFRelease(Src); } CFRelease(FileUrl); } } else { GMemStream ms(In, 0, -1); CFDataRef data = CFDataCreate(NULL, (const UInt8 *)ms.GetBasePtr(), ms.GetSize()); CGImageSourceRef Src = CGImageSourceCreateWithData(data, NULL); if (!Src) LgiTrace("%s:%i - CGImageSourceCreateWithURL failed.\n", _FL); else { Img = CGImageSourceCreateImageAtIndex(Src, 0, 0); if (!Img) LgiTrace("%s:%i - CGImageSourceCreateImageAtIndex failed.\n", _FL); CFRelease(Src); } } if (Img) { size_t x = CGImageGetWidth(Img); size_t y = CGImageGetHeight(Img); // size_t bits = CGImageGetBitsPerPixel(Img); if (pDC.Reset(new GMemDC) && pDC->Create(x, y, System32BitColourSpace)) { pDC->Colour(0); pDC->Rectangle(); CGRect r = {{0, 0}, {(CGFloat)x, (CGFloat)y}}; CGContextDrawImage(pDC->Handle(), r, Img); } else { LgiTrace("%s:%i - pMemDC->Create failed.\n", _FL); pDC.Reset(); } CGImageRelease(Img); } #elif WINNATIVE && defined(_MSC_VER) /* char *Ext = LgiGetExtension((char*)Name); if (Ext && stricmp(Ext, "gif") && stricmp(Ext, "png")) */ { IImgCtx *Ctx = 0; HRESULT hr = CoCreateInstance(CLSID_IImgCtx, NULL, CLSCTX_INPROC_SERVER, IID_IImgCtx, (LPVOID*)&Ctx); if (SUCCEEDED(hr)) { GVariant Fn = Name; hr = Ctx->Load(Fn.WStr(), 0); if (SUCCEEDED(hr)) { SIZE Size = { -1, -1 }; ULONG State = 0; int64 Start = LgiCurrentTime(); while (LgiCurrentTime() - Start < 3000) // just in case it gets stuck.... { hr = Ctx->GetStateInfo(&State, &Size, false); if (SUCCEEDED(hr)) { if ((State & IMGLOAD_COMPLETE) || (State & IMGLOAD_STOPPED) || (State & IMGLOAD_ERROR)) { break; } else { LgiSleep(10); } } else break; } if (Size.cx > 0 && Size.cy > 0 && pDC.Reset(new GMemDC)) { if (pDC->Create(Size.cx, Size.cy, System24BitColourSpace)) { HDC hDC = pDC->StartDC(); if (hDC) { RECT rc = { 0, 0, pDC->X(), pDC->Y() }; Ctx->Draw(hDC, &rc); pDC->EndDC(); } if (pDC->GetBits() == 32) { // Make opaque int Op = pDC->Op(GDC_OR); pDC->Colour(Rgba32(0, 0, 0, 255), 32); pDC->Rectangle(); pDC->Op(GDC_SET); } } else pDC.Reset(); } } Ctx->Release(); } } #endif if (pDC) return pDC.Release(); } if (FilterStatus == GFilter::IoComponentMissing) { const char *c = Filter->GetComponentName(); LgiAssert(c != NULL); if (c) { GToken t(c, ","); for (int i=0; iGetOption(GDC_PROMOTE_ON_LOAD); if (PromoteTo > 0 && PromoteTo != pDC->GetBits()) { GAutoPtr pOld;; // GPalette *pPal = pDC->Palette(); pOld = pDC; pDC.Reset(new GMemDC); if (pOld && pDC && pDC->Create(pOld->X(), pOld->Y(), GBitsToColourSpace(PromoteTo))) { pDC->Blt(0, 0, pOld); pOld.Reset(); } } */ #ifdef BEOS else if (pDC->GetBits() == 8) { // Create remap table color_map *Map = (color_map*) system_colors(); GPalette *Palette = pDC->Palette(); if (Map && Palette) { char ReMap[256]; for (int i=0; i<256; i++) { GdcRGB *p = (*Palette)[i]; if (p) { COLOUR c = Rgb15(p->r, p->g, p->b); ReMap[i] = Map->index_map[c]; p->r = Map->color_list[i].red; p->g = Map->color_list[i].green; p->b = Map->color_list[i].blue; } else { ReMap[i] = 0; } } // Remap colours to BeOS palette for (int y=0; yY(); y++) { uchar *d = (*pDC)[y]; if (d) { for (int x=0; xX(); x++) { d[x] = ReMap[d[x]]; } } } } } #endif } #endif return pDC.Release(); } bool WriteDC(const char *Name, GSurface *pDC) { return GdcD->Save(Name, pDC); } +bool GdcDevice::Save(GStream *Out, GSurface *In, const char *FileType) +{ + if (!Out || !In || !FileType) + return false; + + GAutoPtr F(GFilterFactory::New(FileType, FILTER_CAP_WRITE, 0)); + if (!F) + { + LgiTrace("%s:%i - No filter for '%s'\n", _FL, FileType); + return false; + } + + return F->WriteImage(Out, In) == GFilter::IoSuccess; +} + bool GdcDevice::Save(const char *Name, GSurface *pDC) { - bool Status = false; + if (!Name || !pDC) + return false; - if (Name && pDC) + GFile File; + if (!File.Open(Name, O_WRITE)) { - GAutoPtr F(GFilterFactory::New(Name, FILTER_CAP_WRITE, 0)); - if (F) - { - GFile File; - if (File.Open(Name, O_WRITE)) - { - Status = F->WriteImage(&File, pDC) == GFilter::IoSuccess; - } - } - else - { - LgiTrace("No filter to write '%s'\n", Name); - } + LgiTrace("%s:%i - Can't open '%s'\n", _FL, Name); + return false; } - return Status; + return Save(&File, pDC, Name); } diff --git a/src/common/INet/libntlm-0.4.2/build-win32/stdint.h b/src/common/INet/libntlm-0.4.2/build-win32/_stdint.h rename from src/common/INet/libntlm-0.4.2/build-win32/stdint.h rename to src/common/INet/libntlm-0.4.2/build-win32/_stdint.h diff --git a/src/common/INet/libntlm-0.4.2/lib/md4.h b/src/common/INet/libntlm-0.4.2/lib/md4.h --- a/src/common/INet/libntlm-0.4.2/lib/md4.h +++ b/src/common/INet/libntlm-0.4.2/lib/md4.h @@ -1,82 +1,86 @@ /* Declarations of functions and data types used for MD4 sum library functions. Copyright (C) 2000, 2001, 2003, 2005, 2008 Free Software Foundation, Inc. This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef MD4_H # define MD4_H 1 # include # include # define MD4_DIGEST_SIZE 16 +#ifdef _WIN32 + #define inline +#endif + /* Structure to save state of computation between the single steps. */ struct md4_ctx { uint32_t A; uint32_t B; uint32_t C; uint32_t D; uint32_t total[2]; uint32_t buflen; uint32_t buffer[32]; }; /* Initialize structure containing state of computation. */ extern void md4_init_ctx (struct md4_ctx *ctx); /* Starting with the result of former calls of this function (or the initialization function update the context for the next LEN bytes starting at BUFFER. It is necessary that LEN is a multiple of 64!!! */ extern void md4_process_block (const void *buffer, size_t len, struct md4_ctx *ctx); /* Starting with the result of former calls of this function (or the initialization function update the context for the next LEN bytes starting at BUFFER. It is NOT required that LEN is a multiple of 64. */ extern void md4_process_bytes (const void *buffer, size_t len, struct md4_ctx *ctx); /* Process the remaining bytes in the buffer and put result from CTX in first 16 bytes following RESBUF. The result is always in little endian byte order, so that a byte-wise output yields to the wanted ASCII representation of the message digest. */ extern void *md4_finish_ctx (struct md4_ctx *ctx, void *resbuf); /* Put result from CTX in first 16 bytes following RESBUF. The result is always in little endian byte order, so that a byte-wise output yields to the wanted ASCII representation of the message digest. */ extern void *md4_read_ctx (const struct md4_ctx *ctx, void *resbuf); /* Compute MD4 message digest for bytes read from STREAM. The resulting message digest number will be written into the 16 bytes beginning at RESBLOCK. */ extern int md4_stream (FILE * stream, void *resblock); /* Compute MD4 message digest for LEN bytes beginning at BUFFER. The result is always in little endian byte order, so that a byte-wise output yields to the wanted ASCII representation of the message digest. */ extern void *md4_buffer (const char *buffer, size_t len, void *resblock); #endif diff --git a/src/common/Text/Emoji/EmojiTools.cpp b/src/common/Text/Emoji/EmojiTools.cpp --- a/src/common/Text/Emoji/EmojiTools.cpp +++ b/src/common/Text/Emoji/EmojiTools.cpp @@ -1,254 +1,254 @@ // http://code.iamcal.com/php/emoji/ #include #include "Lgi.h" #include "Emoji.h" #include "GVariant.h" #include "GDocView.h" #ifdef WIN32 -typedef uint32 WChar; +typedef uint32_t WChar; #else typedef wchar_t WChar; #endif bool HasEmoji(char *Txt) { if (!Txt) return false; GUtf8Ptr p(Txt); WChar u; while ((u = p++)) { int IcoIdx = EmojiToIconIndex((uint32_t*)&u, 1); if (IcoIdx >= 0) return true; } return false; } bool HasEmoji(uint32_t *Txt) { if (!Txt) return false; for (uint32_t *s = Txt; *s; s++) { int IcoIdx = EmojiToIconIndex(s, 2); if (IcoIdx >= 0) return true; } return false; } /* #ifdef WIN32 #define snwprintf _snwprintf #else #include */ template ssize_t my_snwprintf(T *ptr, int ptr_size, const char16 *fmt, ...) { T *start = ptr; T *end = ptr + ptr_size - 1; va_list ap; va_start(ap, fmt); // int a = 1; while (ptr < end && *fmt) { if (*fmt == '%') { int len = -1; fmt++; if (*fmt == '.') fmt++; if (*fmt == '*') { len = va_arg(ap, int); fmt++; } if (*fmt == 's') { T *in = va_arg(ap, T*); if (len >= 0) while (ptr < end && in && len-- > 0) *ptr++ = *in++; else while (ptr < end && in && *in) *ptr++ = *in++; } else if (*fmt == 'S') { char *in = va_arg(ap, char*); if (len >= 0) while (ptr < end && in && len-- > 0) *ptr++ = *in++; else while (ptr < end && in && *in) *ptr++ = *in++; } else if (*fmt == 'i') { char tmp[32]; int n = 0; int in = va_arg(ap, int); if (in) while (in) { tmp[n++] = '0' + (in % 10); in /= 10; } else tmp[n++] = '0'; while (ptr < end && n > 0) *ptr++ = tmp[--n]; } else LgiAssert(!"Unknown format specifier"); fmt++; } else *ptr++ = *fmt++; } *ptr++ = 0; va_end(ap); return ptr - start - 1; } // #endif #define BUF_SIZE 256 const char16 *h1 = L"\n"; const char16 *h2 = L"\n"; const char16 *newline = L"
\n"; const char16 *mail_link = L"%.*s"; const char16 *anchor = L"%.*s"; const char16 *img = L""; struct EmojiMemQ : GMemQueue { EmojiMemQ() : GMemQueue(1024) { } #ifdef WINDOWS int WriteWide(const char16 *s, ssize_t bytes) { - GAutoPtr c((uint32*)LgiNewConvertCp("utf-32", s, LGI_WideCharset, bytes)); + GAutoPtr c((uint32_t*)LgiNewConvertCp("utf-32", s, LGI_WideCharset, bytes)); int len = Strlen(c.Get()); - return GMemQueue::Write(c, len * sizeof(uint32)); + return GMemQueue::Write(c, len * sizeof(uint32_t)); } #endif ssize_t WriteWide(const WChar *s, ssize_t bytes) { return GMemQueue::Write(s, bytes); } }; GAutoWString TextToEmoji(uint32_t *Txt, bool IsHtml) { EmojiMemQ p; GArray Links; int Lnk = 0; ssize_t Ch; WChar Buf[BUF_SIZE]; char EmojiPng[MAX_PATH]; #ifdef MAC LgiGetExeFile(EmojiPng, sizeof(EmojiPng)); LgiMakePath(EmojiPng, sizeof(EmojiPng), EmojiPng, "Contents/Resources/EmojiMap.png"); #else LGetSystemPath(LSP_APP_INSTALL, EmojiPng, sizeof(EmojiPng)); LgiMakePath(EmojiPng, sizeof(EmojiPng), EmojiPng, "resources/EmojiMap.png"); #endif LgiAssert(sizeof(WChar) == sizeof(uint32_t)); if (!IsHtml) { LgiDetectLinks(Links, Txt); Ch = my_snwprintf(Buf, BUF_SIZE, h1); if (Ch > 0) p.Write(Buf, (int) (Ch * sizeof(*Buf))); } WChar *Start = (WChar*)Txt; WChar *s = (WChar*)Txt; for (; *s; s++) { if (Lnk < (int)Links.Length() && s - (WChar*)Txt == Links[Lnk].Start) { GLinkInfo &l = Links[Lnk]; // Start of embedded link, convert into if (s > Start) p.Write(Start, (int) ((s - Start) * sizeof(*s))); if (l.Email) Ch = my_snwprintf(Buf, BUF_SIZE, mail_link, l.Len, s, l.Len, s); else Ch = my_snwprintf(Buf, BUF_SIZE, anchor, l.Len, s, l.Len, s); if (Ch > 0) p.Write(Buf, (int) (Ch * sizeof(*Buf))); Start = s + l.Len; Lnk++; } else if (!IsHtml && *s == '\n') { // Eol if (s > Start) p.Write(Start, (int) ((s - Start) * sizeof(*s))); Ch = my_snwprintf(Buf, BUF_SIZE, newline); if (Ch > 0) p.Write(Buf, (int) (Ch * sizeof(*Buf))); Start = s + 1; } else { int IcoIdx = EmojiToIconIndex((uint32_t*)s, 2); if (IcoIdx >= 0) { // Emoji character, convert to if (s > Start) p.Write(Start, (int) ((s - Start) * sizeof(*s))); int XChar = IcoIdx % EMOJI_GROUP_X; int YChar = IcoIdx / EMOJI_GROUP_X; GRect rc; rc.ZOff(EMOJI_CELL_SIZE - 1, EMOJI_CELL_SIZE - 1); rc.Offset(XChar * EMOJI_CELL_SIZE, YChar * EMOJI_CELL_SIZE); Ch = my_snwprintf(Buf, BUF_SIZE, img, EmojiPng, rc.GetStr()); if (Ch > 0) p.Write(Buf, (int) (Ch * sizeof(*Buf))); Start = s + 1; if (*Start == 0xfe0f) { s++; Start++; } } } } if (s > Start) p.Write(Start, (int) ((s - Start) * sizeof(*s))); if (!IsHtml) { Ch = my_snwprintf(Buf, BUF_SIZE, h2); if (Ch > 0) p.Write(Buf, (int) (Ch * sizeof(*Buf))); } GAutoPtr WideVer( (WChar*)p.New(sizeof(*s)) ); GAutoWString Final( (char16*)LgiNewConvertCp(LGI_WideCharset, WideVer, "utf-32") ); return Final; } diff --git a/src/common/Text/GHtml.cpp b/src/common/Text/GHtml.cpp --- a/src/common/Text/GHtml.cpp +++ b/src/common/Text/GHtml.cpp @@ -1,9193 +1,9188 @@ #include #include #include #include #include "Lgi.h" #include "GHtml.h" #include "GHtmlPriv.h" #include "GToken.h" #include "GScrollBar.h" #include "GVariant.h" #include "GFindReplaceDlg.h" #include "GUnicode.h" #include "Emoji.h" #include "GClipBoard.h" #include "GButton.h" #include "GEdit.h" #include "GCombo.h" #include "GdcTools.h" #include "GDisplayString.h" #include "GPalette.h" #include "GPath.h" #include "GCssTools.h" #include "LgiRes.h" #include "INet.h" #define DEBUG_TABLE_LAYOUT 0 #define DEBUG_DRAW_TD 0 #define DEBUG_RESTYLE 0 #define DEBUG_TAG_BY_POS 0 #define DEBUG_SELECTION 0 #define DEBUG_TEXT_AREA 0 #define ENABLE_IMAGE_RESIZING 1 #define DOCUMENT_LOAD_IMAGES 1 #define MAX_RECURSION_DEPTH 300 #define ALLOW_TABLE_GROWTH 1 #define LUIS_DEBUG 0 #define CRASH_TRACE 0 #ifdef MAC #define GHTML_USE_DOUBLE_BUFFER 0 #else #define GHTML_USE_DOUBLE_BUFFER 1 #endif #define GT_TRANSPARENT 0x00000000 #ifndef IDC_HAND #define IDC_HAND MAKEINTRESOURCE(32649) #endif #define M_JOBS_LOADED (M_USER+4000) #undef CellSpacing #define DefaultCellSpacing 0 #define DefaultCellPadding 1 #ifdef MAC #define MinimumPointSize 9 #define MinimumBodyFontSize 12 #else #define MinimumPointSize 8 #define MinimumBodyFontSize 11 #endif // #define DefaultFont "font-family: Times; font-size: 16pt;" #define DefaultBodyMargin "5px" #define DefaultImgSize 16 #define DefaultMissingCellColour GT_TRANSPARENT // Rgb32(0xf0,0xf0,0xf0) #define ShowNbsp 0 #define FontPxHeight(fnt) (fnt->GetHeight() - (int)(fnt->Leading() + 0.5)) #if 0 // def _DEBUG #define DefaultTableBorder Rgb32(0xf8, 0xf8, 0xf8) #else #define DefaultTableBorder GT_TRANSPARENT #endif #define IsTableCell(id) ( ((id) == TAG_TD) || ((id) == TAG_TH) ) #define IsTableTag() (TagId == TAG_TABLE || TagId == TAG_TR || TagId == TAG_TD || TagId == TAG_TH) #define GetCssLen(a, b) a().Type == GCss::LenInherit ? b() : a() static char WordDelim[] = ".,<>/?[]{}()*&^%$#@!+|\'\""; static char16 WhiteW[] = {' ', '\t', '\r', '\n', 0}; #if 0 static char DefaultCss[] = { "a { color: blue; text-decoration: underline; }" "body { margin: 8px; }" "strong { font-weight: bolder; }" "pre { font-family: monospace }" "h1 { font-size: 2em; margin: .67em 0px; }" "h2 { font-size: 1.5em; margin: .75em 0px; }" "h3 { font-size: 1.17em; margin: .83em 0px; }" "h4, p," "blockquote, ul," "fieldset, form," "ol, dl, dir," "menu { margin: 1.12em 0px; }" "h5 { font-size: .83em; margin: 1.5em 0px; }" "h6 { font-size: .75em; margin: 1.67em 0px; }" "strike, del { text-decoration: line-through; }" "hr { border: 1px inset; }" "center { text-align: center; }" "h1, h2, h3, h4," "h5, h6, b," "strong { font-weight: bolder; }" }; #endif template void RemoveChars(T *str, T *remove_list) { T *i = str, *o = str, *c; while (*i) { for (c = remove_list; *c; c++) { if (*c == *i) break; } if (*c == 0) *o++ = *i; i++; } *o++ = NULL; } ////////////////////////////////////////////////////////////////////// using namespace Html1; namespace Html1 { class GHtmlPrivate { public: LHashTbl, GTag*> Loading; GHtmlStaticInst Inst; bool CursorVis; GRect CursorPos; GdcPt2 Content; bool WordSelectMode; bool LinkDoubleClick; GAutoString OnLoadAnchor; bool DecodeEmoji; GAutoString EmojiImg; int NextCtrlId; uint64 SetScrollTime; int DeferredLoads; bool IsParsing; bool IsLoaded; bool StyleDirty; // Find settings GAutoWString FindText; bool MatchCase; GHtmlPrivate() { IsLoaded = false; StyleDirty = false; IsParsing = false; LinkDoubleClick = true; WordSelectMode = false; NextCtrlId = 2000; SetScrollTime = 0; CursorVis = false; CursorPos.ZOff(-1, -1); DeferredLoads = 0; char EmojiPng[MAX_PATH]; #ifdef MAC LgiGetExeFile(EmojiPng, sizeof(EmojiPng)); LgiMakePath(EmojiPng, sizeof(EmojiPng), EmojiPng, "Contents/Resources/Emoji.png"); #else LGetSystemPath(LSP_APP_INSTALL, EmojiPng, sizeof(EmojiPng)); LgiMakePath(EmojiPng, sizeof(EmojiPng), EmojiPng, "resources/emoji.png"); #endif if (FileExists(EmojiPng)) { DecodeEmoji = true; EmojiImg.Reset(NewStr(EmojiPng)); } else DecodeEmoji = false; } ~GHtmlPrivate() { } }; }; ////////////////////////////////////////////////////////////////////// namespace Html1 { class InputButton : public GButton { GTag *Tag; public: InputButton(GTag *tag, int Id, const char *Label) : GButton(Id, 0, 0, -1, -1, Label) { Tag = tag; } void OnClick() { Tag->OnClick(); } }; class GFontCache { GHtml *Owner; List Fonts; public: GFontCache(GHtml *owner) { Owner = owner; } ~GFontCache() { Fonts.DeleteObjects(); } GFont *FontAt(int i) { return Fonts.ItemAt(i); } GFont *FindMatch(GFont *m) { for (GFont *f = Fonts.First(); f; f = Fonts.Next()) { if (*f == *m) { return f; } } return 0; } GFont *GetFont(GCss *Style) { if (!Style) return NULL; GFont *Default = Owner->GetFont(); GCss::StringsDef Face = Style->FontFamily(); if (Face.Length() < 1 || !ValidStr(Face[0])) { Face.Empty(); const char *DefFace = Default->Face(); LgiAssert(ValidStr(DefFace)); Face.Add(NewStr(DefFace)); } LgiAssert(ValidStr(Face[0])); GCss::Len Size = Style->FontSize(); GCss::FontWeightType Weight = Style->FontWeight(); bool IsBold = Weight == GCss::FontWeightBold || Weight == GCss::FontWeightBolder || Weight > GCss::FontWeight400; bool IsItalic = Style->FontStyle() == GCss::FontStyleItalic; bool IsUnderline = Style->TextDecoration() == GCss::TextDecorUnderline; if (Size.Type == GCss::LenInherit || Size.Type == GCss::LenNormal) { Size.Type = GCss::LenPt; Size.Value = (float)Default->PointSize(); } GFont *f = 0; if (Size.Type == GCss::LenPx) { int RequestPx = (int)Size.Value; // Look for cached fonts of the right size... for (f=Fonts.First(); f; f=Fonts.Next()) { if (f->Face() && _stricmp(f->Face(), Face[0]) == 0 && f->Bold() == IsBold && f->Italic() == IsItalic && f->Underline() == IsUnderline) { int Px = FontPxHeight(f); if (abs(Px - RequestPx) < 2) return f; } } } else if (Size.Type == GCss::LenPt) { double Pt = MAX(MinimumPointSize, Size.Value); for (f=Fonts.First(); f; f=Fonts.Next()) { if (!f->Face() || Face.Length() == 0) { LgiAssert(0); break; } auto FntSz = f->Size(); if (f->Face() && _stricmp(f->Face(), Face[0]) == 0 && FntSz.Type == GCss::LenPt && abs(FntSz.Value - Pt) < 0.001 && f->Bold() == IsBold && f->Italic() == IsItalic && f->Underline() == IsUnderline) { // Return cached font return f; } } } else if (Size.Type == GCss::LenPercent) { // Most of the percentages will be resolved in the "Apply" stage // of the CSS calculations, any that appear here have no "font-size" // in their parent tree, so we just use the default font size times // the requested percent Size.Type = GCss::LenPt; Size.Value *= Default->PointSize() / 100.0; if (Size.Value < MinimumPointSize) Size.Value = MinimumPointSize; } else if (Size.Type == GCss::LenEm) { // Most of the relative sizes will be resolved in the "Apply" stage // of the CSS calculations, any that appear here have no "font-size" // in their parent tree, so we just use the default font size times // the requested percent Size.Type = GCss::LenPt; Size.Value *= Default->PointSize(); if (Size.Value < MinimumPointSize) Size.Value = MinimumPointSize; } else if (Size.Type == GCss::SizeXXSmall || Size.Type == GCss::SizeXSmall || Size.Type == GCss::SizeSmall || Size.Type == GCss::SizeMedium || Size.Type == GCss::SizeLarge || Size.Type == GCss::SizeXLarge || Size.Type == GCss::SizeXXLarge) { int Idx = Size.Type-GCss::SizeXXSmall; LgiAssert(Idx >= 0 && Idx < CountOf(GCss::FontSizeTable)); Size.Type = GCss::LenPt; Size.Value = Default->PointSize() * GCss::FontSizeTable[Idx]; if (Size.Value < MinimumPointSize) Size.Value = MinimumPointSize; } else if (Size.Type == GCss::SizeSmaller) { Size.Type = GCss::LenPt; Size.Value = (float)(Default->PointSize() - 1); } else if (Size.Type == GCss::SizeLarger) { Size.Type = GCss::LenPt; Size.Value = (float)(Default->PointSize() + 1); } else LgiAssert(!"Not impl."); if ((f = new GFont)) { char *ff = ValidStr(Face[0]) ? Face[0] : Default->Face(); f->Face(ff); f->Size(Size.IsValid() ? Size : Default->Size()); f->Bold(IsBold); f->Italic(IsItalic); f->Underline(IsUnderline); // printf("Add cache font %s,%i %i,%i,%i\n", f->Face(), f->PointSize(), f->Bold(), f->Italic(), f->Underline()); if (!f->Create((char*)0, 0)) { // Broken font... f->Face(Default->Face()); GFont *DefMatch = FindMatch(f); // printf("Falling back to default face for '%s:%i', DefMatch=%p\n", ff, f->PointSize(), DefMatch); if (DefMatch) { DeleteObj(f); return DefMatch; } else { if (!f->Create((char*)0, 0)) { DeleteObj(f); return Fonts.First(); } } } // Not already cached Fonts.Insert(f); if (!f->Face()) { LgiAssert(0); } return f; } return 0; } }; class GFlowRegion { List Line; // These pointers aren't owned by the flow region // When the line is finish, all the tag regions // will need to be vertically aligned struct GFlowStack { int LeftAbs; int RightAbs; int TopAbs; }; GArray Stack; public: GHtml *Html; int x1, x2; // Left and right margins int y1; // Current y position int y2; // Maximum used y position int cx; // Current insertion point int my; // How much of the area above y2 was just margin GdcPt2 MAX; // Max dimensions int Inline; int InBody; GFlowRegion(GHtml *html, bool inbody) { Html = html; x1 = x2 = y1 = y2 = cx = my = 0; Inline = 0; InBody = inbody; } GFlowRegion(GHtml *html, GRect r, bool inbody) { Html = html; MAX.x = cx = x1 = r.x1; MAX.y = y1 = y2 = r.y1; x2 = r.x2; my = 0; Inline = 0; InBody = inbody; } GFlowRegion(GFlowRegion &r) { Html = r.Html; x1 = r.x1; x2 = r.x2; y1 = r.y1; MAX.x = cx = r.cx; MAX.y = y2 = r.y2; my = r.my; Inline = r.Inline; InBody = r.InBody; } GString ToString() { GString s; s.Printf("Flow: x=%i(%i)%i y=%i,%i my=%i inline=%i", x1, cx, x2, y1, y2, my, Inline); return s; } int X() { return x2 - cx; } void X(int newx) { x2 = x1 + newx - 1; } GFlowRegion &operator +=(GRect r) { x1 += r.x1; cx += r.x1; x2 -= r.x2; y1 += r.y1; y2 += r.y1; return *this; } GFlowRegion &operator -=(GRect r) { x1 -= r.x1; cx -= r.x1; x2 += r.x2; y1 += r.y2; y2 += r.y2; return *this; } void FinishLine(GCss::LengthType Align, bool Margin = false); void EndBlock(GCss::LengthType Align); void Insert(GFlowRect *Tr); GRect *LineBounds(); void Indent(GTag *Tag, GCss::Len Left, GCss::Len Top, GCss::Len Right, GCss::Len Bottom, bool IsMargin) { GFlowRegion This(*this); GFlowStack &Fs = Stack.New(); Fs.LeftAbs = Left.IsValid() ? ResolveX(Left, Tag, IsMargin) : 0; Fs.RightAbs = Right.IsValid() ? ResolveX(Right, Tag, IsMargin) : 0; Fs.TopAbs = Top.IsValid() ? ResolveY(Top, Tag, IsMargin) : 0; x1 += Fs.LeftAbs; cx += Fs.LeftAbs; x2 -= Fs.RightAbs; y1 += Fs.TopAbs; y2 += Fs.TopAbs; if (IsMargin) my += Fs.TopAbs; } void Indent(GRect &Px, bool IsMargin) { GFlowRegion This(*this); GFlowStack &Fs = Stack.New(); Fs.LeftAbs = Px.x1; Fs.RightAbs = Px.x2; Fs.TopAbs = Px.y1; x1 += Fs.LeftAbs; cx += Fs.LeftAbs; x2 -= Fs.RightAbs; y1 += Fs.TopAbs; y2 += Fs.TopAbs; if (IsMargin) my += Fs.TopAbs; } void Outdent(GRect &Px, bool IsMargin) { GFlowRegion This = *this; ssize_t len = Stack.Length(); if (len > 0) { GFlowStack &Fs = Stack[len-1]; int &BottomAbs = Px.y2; x1 -= Fs.LeftAbs; cx -= Fs.LeftAbs; x2 += Fs.RightAbs; y2 += BottomAbs; if (IsMargin) my += BottomAbs; Stack.Length(len-1); } else LgiAssert(!"Nothing to pop."); } void Outdent(GTag *Tag, GCss::Len Left, GCss::Len Top, GCss::Len Right, GCss::Len Bottom, bool IsMargin) { GFlowRegion This = *this; ssize_t len = Stack.Length(); if (len > 0) { GFlowStack &Fs = Stack[len-1]; int BottomAbs = Bottom.IsValid() ? ResolveY(Bottom, Tag, IsMargin) : 0; x1 -= Fs.LeftAbs; cx -= Fs.LeftAbs; x2 += Fs.RightAbs; y2 += BottomAbs; if (IsMargin) my += BottomAbs; Stack.Length(len-1); } else LgiAssert(!"Nothing to pop."); } int ResolveX(GCss::Len l, GTag *t, bool IsMargin) { GFont *f = t->GetFont(); switch (l.Type) { default: case GCss::LenInherit: return IsMargin ? 0 : X(); case GCss::LenPx: // return MIN((int)l.Value, X()); return (int)l.Value; case GCss::LenPt: return (int) (l.Value * LgiScreenDpi() / 72.0); case GCss::LenCm: return (int) (l.Value * LgiScreenDpi() / 2.54); case GCss::LenEm: { if (!f) { LgiAssert(!"No font?"); f = SysFont; } return (int)(l.Value * f->GetHeight()); } case GCss::LenEx: { if (!f) { LgiAssert(!"No font?"); f = SysFont; } return (int) (l.Value * f->GetHeight() / 2); // More haha, who uses 'ex' anyway? } case GCss::LenPercent: { int my_x = X(); int px = (int) (l.Value * my_x / 100.0); return px; } case GCss::LenAuto: { if (IsMargin) return 0; else return X(); break; } case GCss::SizeSmall: { return 1; // px } case GCss::SizeMedium: { return 2; // px } case GCss::SizeLarge: { return 3; // px } } return 0; } bool LimitX(int &x, GCss::Len Min, GCss::Len Max, GFont *f) { bool Limited = false; if (Min.IsValid()) { int Px = Min.ToPx(x2 - x1 + 1, f, false); if (Px > x) { x = Px; Limited = true; } } if (Max.IsValid()) { int Px = Max.ToPx(x2 - x1 + 1, f, false); if (Px < x) { x = Px; Limited = true; } } return Limited; } int ResolveY(GCss::Len l, GTag *t, bool IsMargin) { GFont *f = t->GetFont(); switch (l.Type) { case GCss::LenInherit: case GCss::LenAuto: case GCss::LenNormal: case GCss::LenPx: return (int)l.Value; case GCss::LenPt: return (int) (l.Value * LgiScreenDpi() / 72.0); case GCss::LenCm: return (int) (l.Value * LgiScreenDpi() / 2.54); case GCss::LenEm: { if (!f) { f = SysFont; LgiAssert(!"No font"); } return (int) (l.Value * f->GetHeight()); } case GCss::LenEx: { if (!f) { f = SysFont; LgiAssert(!"No font"); } return (int) (l.Value * f->GetHeight() / 2); // More haha, who uses 'ex' anyway? } case GCss::LenPercent: { // Walk up tree of tags to find an absolute size... GCss::Len Ab; for (GTag *p = ToTag(t->Parent); p; p = ToTag(p->Parent)) { auto h = p->Height(); if (h.IsValid() && !h.IsDynamic()) { Ab = h; break; } } if (!Ab.IsValid()) { LgiAssert(Html != NULL); Ab.Type = GCss::LenPx; Ab.Value = Html->Y(); } GCss::Len m = Ab * l; return (int)m.ToPx(0, f);; } case GCss::SizeSmall: { return 1; // px } case GCss::SizeMedium: { return 2; // px } case GCss::SizeLarge: { return 3; // px } case GCss::AlignLeft: case GCss::AlignRight: case GCss::AlignCenter: case GCss::AlignJustify: case GCss::VerticalBaseline: case GCss::VerticalSub: case GCss::VerticalSuper: case GCss::VerticalTop: case GCss::VerticalTextTop: case GCss::VerticalMiddle: case GCss::VerticalBottom: case GCss::VerticalTextBottom: { // Meaningless in this context break; } default: { LgiAssert(!"Not supported."); break; } } return 0; } bool LimitY(int &y, GCss::Len Min, GCss::Len Max, GFont *f) { bool Limited = false; int TotalY = Html ? Html->Y() : 0; if (Min.IsValid()) { int Px = Min.ToPx(TotalY, f, false); if (Px > y) { y = Px; Limited = true; } } if (Max.IsValid()) { int Px = Max.ToPx(TotalY, f, false); if (Px < y) { y = Px; Limited = true; } } return Limited; } GRect ResolveMargin(GCss *Src, GTag *Tag) { GRect r; r.x1 = ResolveX(Src->MarginLeft(), Tag, true); r.y1 = ResolveY(Src->MarginTop(), Tag, true); r.x2 = ResolveX(Src->MarginRight(), Tag, true); r.y2 = ResolveY(Src->MarginBottom(), Tag, true); return r; } GRect ResolveBorder(GCss *Src, GTag *Tag) { GRect r; r.x1 = ResolveX(Src->BorderLeft(), Tag, true); r.y1 = ResolveY(Src->BorderTop(), Tag, true); r.x2 = ResolveX(Src->BorderRight(), Tag, true); r.y2 = ResolveY(Src->BorderBottom(), Tag, true); return r; } GRect ResolvePadding(GCss *Src, GTag *Tag) { GRect r; r.x1 = ResolveX(Src->PaddingLeft(), Tag, true); r.y1 = ResolveY(Src->PaddingTop(), Tag, true); r.x2 = ResolveX(Src->PaddingRight(), Tag, true); r.y2 = ResolveY(Src->PaddingBottom(), Tag, true); return r; } }; }; ////////////////////////////////////////////////////////////////////// static bool ParseDistance(char *s, float &d, char *units = 0) { if (!s) return false; while (*s && IsWhiteSpace(*s)) s++; if (!IsDigit(*s) && !strchr("-.", *s)) return false; d = (float)atof(s); while (*s && (IsDigit(*s) || strchr("-.", *s))) s++; while (*s && IsWhiteSpace(*s)) s++; char _units[128]; char *o = units = units ? units : _units; while (*s && (IsAlpha(*s) || *s == '%')) { *o++ = *s++; } *o++ = 0; return true; } GLength::GLength() { d = 0; PrevAbs = 0; u = GCss::LenInherit; } GLength::GLength(char *s) { Set(s); } bool GLength::IsValid() { return u != GCss::LenInherit; } bool GLength::IsDynamic() { return u == GCss::LenPercent || d == 0.0; } GLength::operator float () { return d; } GLength &GLength::operator =(float val) { d = val; u = GCss::LenPx; return *this; } GCss::LengthType GLength::GetUnits() { return u; } void GLength::Set(char *s) { if (ValidStr(s)) { char Units[256] = ""; if (ParseDistance(s, d, Units)) { if (Units[0]) { if (strchr(Units, '%')) { u = GCss::LenPercent; } else if (stristr(Units, "pt")) { u = GCss::LenPt; } else if (stristr(Units, "em")) { u = GCss::LenEm; } else if (stristr(Units, "ex")) { u = GCss::LenEx; } else { u = GCss::LenPx; } } else { u = GCss::LenPx; } } } } float GLength::Get(GFlowRegion *Flow, GFont *Font, bool Lock) { switch (u) { default: break; case GCss::LenEm: { return PrevAbs = d * (Font ? Font->GetHeight() : 14); break; } case GCss::LenEx: { return PrevAbs = (Font ? Font->GetHeight() * d : 14) / 2; break; } case GCss::LenPercent: { if (Lock || PrevAbs == 0.0) { return PrevAbs = (Flow->X() * d / 100); } else { return PrevAbs; } break; } } float FlowX = Flow ? Flow->X() : d; return PrevAbs = MIN(FlowX, d); } GLine::GLine() { LineStyle = -1; LineReset = 0x80000000; } GLine::~GLine() { } GLine &GLine::operator =(int i) { d = (float)i; return *this; } void GLine::Set(char *s) { GToken t(s, " \t"); LineReset = 0x80000000; LineStyle = -1; char *Style = 0; for (unsigned i=0; iColourMap.Find(c) ) { GHtmlParser::ParseColour(c, Colour); } else if (_strnicmp(c, "rgb(", 4) == 0) { char Buf[256]; strcpy_s(Buf, sizeof(Buf), c); while (!strchr(c, ')') && (c = t[++i])) { strcat(Buf, c); } GHtmlParser::ParseColour(Buf, Colour); } else if (IsDigit(*c)) { GLength::Set(c); } else if (_stricmp(c, "none") == 0) { Style = 0; } else if ( _stricmp(c, "dotted") == 0 || _stricmp(c, "dashed") == 0 || _stricmp(c, "solid") == 0 || _stricmp(c, "float") == 0 || _stricmp(c, "groove") == 0 || _stricmp(c, "ridge") == 0 || _stricmp(c, "inset") == 0 || _stricmp(c, "outse") == 0) { Style = c; } else { // ??? } } if (Style && _stricmp(Style, "dotted") == 0) { switch ((int)d) { case 2: { LineStyle = 0xcccccccc; break; } case 3: { LineStyle = 0xe38e38; LineReset = 0x800000; break; } case 4: { LineStyle = 0xf0f0f0f0; break; } case 5: { LineStyle = 0xf83e0; LineReset = 0x80000; break; } case 6: { LineStyle = 0xfc0fc0; LineReset = 0x800000; break; } case 7: { LineStyle = 0xfe03f80; LineReset = 0x8000000; break; } case 8: { LineStyle = 0xff00ff00; break; } case 9: { LineStyle = 0x3fe00; LineReset = 0x20000; break; } default: { LineStyle = 0xaaaaaaaa; break; } } } } ////////////////////////////////////////////////////////////////////// GRect GTag::GetRect(bool Client) { GRect r(Pos.x, Pos.y, Pos.x + Size.x - 1, Pos.y + Size.y - 1); if (!Client) { for (GTag *p = ToTag(Parent); p; p=ToTag(p->Parent)) { r.Offset(p->Pos.x, p->Pos.y); } } return r; } GCss::LengthType GTag::GetAlign(bool x) { for (GTag *t = this; t; t = ToTag(t->Parent)) { GCss::Len l; if (x) { if (IsTableCell(TagId) && Cell && Cell->XAlign) l.Type = Cell->XAlign; else l = t->TextAlign(); } else { l = t->VerticalAlign(); } if (l.Type != LenInherit) { return l.Type; } if (t->TagId == TAG_TABLE) break; } return LenInherit; } ////////////////////////////////////////////////////////////////////// void GFlowRegion::EndBlock(GCss::LengthType Align) { if (cx > x1) { FinishLine(Align); } } void GFlowRegion::FinishLine(GCss::LengthType Align, bool Margin) { if (Align != GCss::AlignLeft) { int Used = 0; for (auto l : Line) Used += l->X(); int Total = x2 - x1 + 1; if (Used < Total) { int Offset = 0; if (Align == GCss::AlignCenter) Offset = (Total - Used) / 2; else if (Align == GCss::AlignRight) Offset = Total - Used; if (Offset) for (auto l : Line) { if (l->Tag->Display() != GCss::DispInlineBlock) l->Offset(Offset, 0); } } } if (y2 > y1) { my = Margin ? y2 - y1 : 0; y1 = y2; } else { int fy = Html->DefFont()->GetHeight(); my = Margin ? fy : 0; y1 += fy; } cx = x1; y2 = y1; Line.Empty(); } GRect *GFlowRegion::LineBounds() { GFlowRect *Prev = Line.First(); GFlowRect *r=Prev; if (r) { GRect b; b = *r; int Ox = r->Tag->AbsX(); int Oy = r->Tag->AbsY(); b.Offset(Ox, Oy); // int Ox = 0, Oy = 0; while ((r = Line.Next())) { GRect c = *r; Ox = r->Tag->AbsX(); Oy = r->Tag->AbsY(); c.Offset(Ox, Oy); /* Ox += r->Tag->Pos.x - Prev->Tag->Pos.x; Oy += r->Tag->Pos.y - Prev->Tag->Pos.y; c.Offset(Ox, Oy); */ b.Union(&c); Prev = r; } static GRect Rgn; Rgn = b; return &Rgn; } return 0; } void GFlowRegion::Insert(GFlowRect *Tr) { if (Tr) Line.Insert(Tr); } ////////////////////////////////////////////////////////////////////// GTag::GTag(GHtml *h, GHtmlElement *p) : GHtmlElement(p), Attr(8) { Ctrl = 0; CtrlType = CtrlNone; TipId = 0; Display(DispInline); Html = h; ImageResized = false; Cursor = -1; Selection = -1; Font = 0; LineHeightCache = -1; HtmlId = NULL; // TableBorder = 0; Cell = NULL; TagId = CONTENT; Info = 0; Pos.x = Pos.y = 0; #ifdef _DEBUG Debug = false; #endif } GTag::~GTag() { if (Html->Cursor == this) { Html->Cursor = 0; } if (Html->Selection == this) { Html->Selection = 0; } DeleteObj(Ctrl); Attr.DeleteArrays(); DeleteObj(Cell); } void GTag::OnChange(PropType Prop) { } bool GTag::OnClick() { if (!Html->Environment) return false; const char *OnClick = NULL; if (Get("onclick", OnClick)) { Html->Environment->OnExecuteScript(Html, (char*)OnClick); } else { OnNotify(0); } return true; } void GTag::Set(const char *attr, const char *val) { char *existing = Attr.Find(attr); if (existing) DeleteArray(existing); if (val) Attr.Add(attr, NewStr(val)); } bool GTag::GetVariant(const char *Name, GVariant &Value, char *Array) { GDomProperty Fld = LgiStringToDomProp(Name); switch (Fld) { case ObjStyle: // Type: GCssStyle { Value = &StyleDom; return true; } case ObjTextContent: // Type: String { Value = Text(); return true; } default: { char *a = Attr.Find(Name); if (a) { Value = a; return true; } break; } } return false; } bool GTag::SetVariant(const char *Name, GVariant &Value, char *Array) { GDomProperty Fld = LgiStringToDomProp(Name); switch (Fld) { case ObjStyle: { const char *Defs = Value.Str(); if (!Defs) return false; return Parse(Defs, ParseRelaxed); } case ObjTextContent: { const char *s = Value.Str(); if (s) { GAutoWString w(CleanText(s, strlen(s), "utf-8", true, true)); Txt = w; return true; } break; } case ObjInnerHtml: // Type: String { // Clear out existing tags.. Children.DeleteObjects(); char *Doc = Value.CastString(); if (Doc) { // Create new tags... bool BackOut = false; while (Doc && *Doc) { GTag *t = new GTag(Html, this); if (t) { Doc = Html->ParseHtml(t, Doc, 1, false, &BackOut); if (!Doc) break; } else break; } } else return false; break; } default: { Set(Name, Value.CastString()); SetStyle(); break; } } Html->ViewWidth = -1; return true; } ssize_t GTag::GetTextStart() { if (PreText()) { GFlowRect *t = TextPos[1]; if (t) return t->Text - Text(); } else { GFlowRect *t = TextPos[0]; if (t) { LgiAssert(t->Text >= Text() && t->Text <= Text()+2); return t->Text - Text(); } } return 0; } static bool TextToStream(GStream &Out, char16 *Text) { if (!Text) return true; uint8_t Buf[256]; uint8_t *s = Buf; ssize_t Len = sizeof(Buf); while (*Text) { #define WriteExistingContent() \ if (s > Buf) \ Out.Write(Buf, (int)(s - Buf)); \ s = Buf; \ Len = sizeof(Buf); \ Buf[0] = 0; if (*Text == '<' || *Text == '>') { WriteExistingContent(); Out.Print("&%ct;", *Text == '<' ? 'l' : 'g'); } else if (*Text == 0xa0) { WriteExistingContent(); Out.Write((char*)" ", 6); } else { LgiUtf32To8(*Text, s, Len); if (Len < 16) { WriteExistingContent(); } } Text++; } if (s > Buf) Out.Write(Buf, s - Buf); return true; } bool GTag::CreateSource(GStringPipe &p, int Depth, bool LastWasBlock) { char *Tabs = new char[Depth+1]; memset(Tabs, '\t', Depth); Tabs[Depth] = 0; if (ValidStr(Tag)) { if (IsBlock()) { p.Print("%s%s<%s", TagId != TAG_HTML ? "\n" : "", Tabs, Tag.Get()); } else { p.Print("<%s", Tag.Get()); } if (Attr.Length()) { // const char *a; // for (char *v = Attr.First(&a); v; v = Attr.Next(&a)) for (auto v : Attr) { if (_stricmp(v.key, "style")) p.Print(" %s=\"%s\"", v.key, v.value); } } if (Props.Length()) { GCss *Css = this; GCss Tmp; #define DelProp(p) \ if (Css == this) { Tmp = *Css; Css = &Tmp; } \ Css->DeleteProp(p); // Clean out any default CSS properties where we can... GHtmlElemInfo *i = GHtmlStatic::Inst->GetTagInfo(Tag); if (i) { if (Props.Find(PropDisplay) && ( (!i->Block() && Display() == DispInline) || (i->Block() && Display() == DispBlock) )) { DelProp(PropDisplay); } switch (TagId) { default: break; case TAG_A: { GCss::ColorDef Blue(GCss::ColorRgb, Rgb32(0, 0, 255)); if (Props.Find(PropColor) && Color() == Blue) DelProp(PropColor); if (Props.Find(PropTextDecoration) && TextDecoration() == GCss::TextDecorUnderline) DelProp(PropTextDecoration) break; } case TAG_BODY: { GCss::Len FivePx(GCss::LenPx, 5.0f); if (Props.Find(PropPaddingLeft) && PaddingLeft() == FivePx) DelProp(PropPaddingLeft) if (Props.Find(PropPaddingTop) && PaddingTop() == FivePx) DelProp(PropPaddingTop) if (Props.Find(PropPaddingRight) && PaddingRight() == FivePx) DelProp(PropPaddingRight) break; } case TAG_B: { if (Props.Find(PropFontWeight) && FontWeight() == GCss::FontWeightBold) DelProp(PropFontWeight); break; } case TAG_U: { if (Props.Find(PropTextDecoration) && TextDecoration() == GCss::TextDecorUnderline) DelProp(PropTextDecoration); break; } case TAG_I: { if (Props.Find(PropFontStyle) && FontStyle() == GCss::FontStyleItalic) DelProp(PropFontStyle); break; } } } // Convert CSS props to a string and emit them... GAutoString s = Css->ToString(); if (ValidStr(s)) { // Clean off any trailing whitespace... char *e = s ? s + strlen(s) : NULL; while (e && strchr(WhiteSpace, e[-1])) *--e = 0; // Print them to the tags attributes... p.Print(" style=\"%s\"", s.Get()); } } } if (Children.Length() || TagId == TAG_STYLE) // { if (Tag) { p.Write((char*)">", 1); TextToStream(p, Text()); } bool Last = IsBlock(); for (unsigned i=0; iCreateSource(p, Parent ? Depth+1 : 0, Last); Last = c->IsBlock(); } if (Tag) { if (IsBlock()) { if (Children.Length()) p.Print("\n%s", Tabs); } p.Print("", Tag.Get()); } } else if (Tag) { if (Text()) { p.Write((char*)">", 1); TextToStream(p, Text()); p.Print("", Tag.Get()); } else { p.Print("/>\n"); } } else { TextToStream(p, Text()); } DeleteArray(Tabs); return true; } void GTag::SetTag(const char *NewTag) { Tag.Reset(NewStr(NewTag)); if (NewTag) { Info = Html->GetTagInfo(Tag); if (Info) { TagId = Info->Id; Display(Info->Flags & GHtmlElemInfo::TI_BLOCK ? GCss::DispBlock : GCss::DispInline); } } else { Info = NULL; TagId = CONTENT; } SetStyle(); } GColour GTag::_Colour(bool f) { for (GTag *t = this; t; t = ToTag(t->Parent)) { ColorDef c = f ? t->Color() : t->BackgroundColor(); if (c.Type != ColorInherit) { return GColour(c.Rgb32, 32); } #if 1 if (!f && t->TagId == TAG_TABLE) break; #else /* This implements some basic level of colour inheritance for background colours. See test case 'cisra-cqs.html'. */ if (!f && t->TagId == TAG_TABLE) break; #endif } return GColour(); } void GTag::CopyClipboard(GMemQueue &p, bool &InSelection) { ssize_t Min = -1; ssize_t Max = -1; if (Cursor >= 0 && Selection >= 0) { Min = MIN(Cursor, Selection); Max = MAX(Cursor, Selection); } else if (InSelection) { Max = MAX(Cursor, Selection); } else { Min = MAX(Cursor, Selection); } ssize_t Off = -1; ssize_t Chars = 0; if (Min >= 0 && Max >= 0) { Off = Min; Chars = Max - Min; } else if (Min >= 0) { Off = Min; Chars = StrlenW(Text()) - Min; InSelection = true; } else if (Max >= 0) { Off = 0; Chars = Max; InSelection = false; } else if (InSelection) { Off = 0; Chars = StrlenW(Text()); } if (Off >= 0 && Chars > 0) { p.Write((uchar*) (Text() + Off), Chars * sizeof(char16)); } if (InSelection) { switch (TagId) { default: break; case TAG_BR: { char16 NL[] = {'\n', 0}; p.Write((uchar*) NL, sizeof(char16)); break; } case TAG_P: { char16 NL[] = {'\n', '\n', 0}; p.Write((uchar*) NL, sizeof(char16) * 2); break; } } } for (unsigned i=0; iCopyClipboard(p, InSelection); } } static char* _DumpColour(GCss::ColorDef c) { static char Buf[4][32]; #ifdef _MSC_VER static LONG Cur = 0; LONG Idx = InterlockedIncrement(&Cur); #else static int Cur = 0; int Idx = __sync_fetch_and_add(&Cur, 1); #endif char *b = Buf[Idx % 4]; if (c.Type == GCss::ColorInherit) strcpy_s(b, 32, "Inherit"); else sprintf_s(b, 32, "%2.2x,%2.2x,%2.2x(%2.2x)", R32(c.Rgb32),G32(c.Rgb32),B32(c.Rgb32),A32(c.Rgb32)); return b; } void GTag::_Dump(GStringPipe &Buf, int Depth) { GString Tabs; Tabs.Set(NULL, Depth); memset(Tabs.Get(), '\t', Depth); const char *Empty = ""; char *ElementName = TagId == CONTENT ? (char*)"Content" : (TagId == ROOT ? (char*)"Root" : Tag); Buf.Print( "%s%s(%p)%s%s%s (%i) Pos=%i,%i Size=%i,%i Color=%s/%s", Tabs.Get(), ElementName, this, HtmlId ? "#" : Empty, HtmlId ? HtmlId : Empty, #ifdef _DEBUG Debug ? " debug" : Empty, #else Empty, #endif WasClosed, Pos.x, Pos.y, Size.x, Size.y, _DumpColour(Color()), _DumpColour(BackgroundColor())); for (unsigned i=0; iText, Tr->Len)); if (Utf8) { size_t Len = strlen(Utf8); if (Len > 40) { Utf8[40] = 0; } } else if (Tr->Text) { Utf8.Reset(NewStr("")); } Buf.Print("Tr(%i,%i %ix%i '%s') ", Tr->x1, Tr->y1, Tr->X(), Tr->Y(), Utf8.Get()); } Buf.Print("\r\n"); for (unsigned i=0; i_Dump(Buf, Depth+1); } if (Children.Length()) { Buf.Print("%s/%s\r\n", Tabs.Get(), ElementName); } } GAutoWString GTag::DumpW() { GStringPipe Buf; // Buf.Print("Html pos=%s\n", Html?Html->GetPos().GetStr():0); _Dump(Buf, 0); GAutoString a(Buf.NewStr()); GAutoWString w(Utf8ToWide(a)); return w; } GAutoString GTag::DescribeElement() { GStringPipe s(256); s.Print("%s", Tag ? Tag.Get() : "CONTENT"); if (HtmlId) s.Print("#%s", HtmlId); for (unsigned i=0; iDefFont(); } return f; } GFont *GTag::GetFont() { if (!Font) { if (PropAddress(PropFontFamily) != 0 || FontSize().Type != LenInherit || FontStyle() != FontStyleInherit || FontVariant() != FontVariantInherit || FontWeight() != FontWeightInherit || TextDecoration() != TextDecorInherit) { GCss c; GCss::PropMap Map; Map.Add(PropFontFamily, new GCss::PropArray); Map.Add(PropFontSize, new GCss::PropArray); Map.Add(PropFontStyle, new GCss::PropArray); Map.Add(PropFontVariant, new GCss::PropArray); Map.Add(PropFontWeight, new GCss::PropArray); Map.Add(PropTextDecoration, new GCss::PropArray); for (GTag *t = this; t; t = ToTag(t->Parent)) { if (!c.InheritCollect(*t, Map)) break; } c.InheritResolve(Map); Map.DeleteObjects(); if ((Font = Html->FontCache->GetFont(&c))) return Font; } else { GTag *t = this; while (!t->Font && t->Parent) { t = ToTag(t->Parent); } if (t->Font) return t->Font; } Font = Html->DefFont(); } return Font; } GTag *GTag::PrevTag() { if (Parent) { ssize_t i = Parent->Children.IndexOf(this); if (i >= 0) { return ToTag(Parent->Children[i - 1]); } } return 0; } void GTag::Invalidate() { GRect p = GetRect(); for (GTag *t=ToTag(Parent); t; t=ToTag(t->Parent)) { p.Offset(t->Pos.x, t->Pos.y); } Html->Invalidate(&p); } GTag *GTag::IsAnchor(GAutoString *Uri) { GTag *a = 0; for (GTag *t = this; t; t = ToTag(t->Parent)) { if (t->TagId == TAG_A) { a = t; break; } } if (a && Uri) { const char *u = 0; if (a->Get("href", u)) { GAutoWString w(CleanText(u, strlen(u), "utf-8")); if (w) { Uri->Reset(WideToUtf8(w)); } } } return a; } bool GTag::OnMouseClick(GMouse &m) { bool Processed = false; if (m.IsContextMenu()) { GAutoString Uri; GTag *a = IsAnchor(&Uri); if (a && ValidStr(Uri)) { GSubMenu RClick; #define IDM_COPY_LINK 100 if (Html->GetMouse(m, true)) { int Id = 0; RClick.AppendItem(LgiLoadString(L_COPY_LINK_LOCATION, "&Copy Link Location"), IDM_COPY_LINK, Uri != 0); if (Html->GetEnv()) Html->GetEnv()->AppendItems(&RClick); switch (Id = RClick.Float(Html, m.x, m.y)) { case IDM_COPY_LINK: { GClipBoard Clip(Html); Clip.Text(Uri); break; } default: { if (Html->GetEnv()) { Html->GetEnv()->OnMenu(Html, Id, a); } break; } } } Processed = true; } } else if (m.Down() && m.Left()) { #ifdef _DEBUG if (m.Ctrl()) { GAutoString Style = ToString(); GStringPipe p(256); p.Print("Tag: %s\n", Tag ? Tag.Get() : "CONTENT"); if (Class.Length()) { p.Print("Class(es): "); for (unsigned i=0; iParent; t=ToTag(t->Parent)) { GStringPipe Tmp; Tmp.Print(" %s", t->Tag ? t->Tag.Get() : "CONTENT"); if (t->HtmlId) { Tmp.Print("#%s", t->HtmlId); } for (unsigned i=0; iClass.Length(); i++) { Tmp.Print(".%s", t->Class[i].Get()); } GAutoString Txt(Tmp.NewStr()); p.Print("%s", Txt.Get()); GDisplayString Ds(SysFont, Txt); int Px = 170 - Ds.X(); int Chars = Px / Sp.X(); for (int c=0; cPos.x, t->Pos.y, t->Size.x, t->Size.y); } GAutoString a(p.NewStr()); LgiMsg( Html, "%s", Html->GetClass(), MB_OK, a.Get()); } else #endif { GAutoString Uri; if (Html && Html->Environment) { if (IsAnchor(&Uri)) { if (Uri) { if (!Html->d->LinkDoubleClick || m.Double()) { Html->Environment->OnNavigate(Html, Uri); Processed = true; } } const char *OnClk = NULL; if (!Processed && Get("onclick", OnClk)) { Html->Environment->OnExecuteScript(Html, (char*)OnClk); } } else { Processed = OnClick(); } } } } return Processed; } GTag *GTag::GetBlockParent(ssize_t *Idx) { if (IsBlock()) { if (Idx) *Idx = 0; return this; } for (GTag *t = this; t; t = ToTag(t->Parent)) { if (ToTag(t->Parent)->IsBlock()) { if (Idx) { *Idx = t->Parent->Children.IndexOf(t); } return ToTag(t->Parent); } } return 0; } GTag *GTag::GetAnchor(char *Name) { if (!Name) return 0; const char *n; if (IsAnchor(0) && Get("name", n) && n && !_stricmp(Name, n)) { return this; } for (unsigned i=0; iGetAnchor(Name); if (Result) return Result; } return 0; } GTag *GTag::GetTagByName(const char *Name) { if (Name) { if (Tag && _stricmp(Tag, Name) == 0) { return this; } for (unsigned i=0; iGetTagByName(Name); if (Result) return Result; } } return 0; } static int IsNearRect(GRect *r, int x, int y) { if (r->Overlap(x, y)) { return 0; } else if (x >= r->x1 && x <= r->x2) { if (y < r->y1) return r->y1 - y; else return y - r->y2; } else if (y >= r->y1 && y <= r->y2) { if (x < r->x1) return r->x1 - x; else return x - r->x2; } int64 dx = 0; int64 dy = 0; if (x < r->x1) { if (y < r->y1) { // top left dx = r->x1 - x; dy = r->y1 - y; } else { // bottom left dx = r->x1 - x; dy = y - r->y2; } } else { if (y < r->y1) { // top right dx = x - r->x2; dy = r->y1 - y; } else { // bottom right dx = x - r->x2; dy = y - r->y2; } } return (int) sqrt( (double) ( (dx * dx) + (dy * dy) ) ); } ssize_t GTag::NearestChar(GFlowRect *Tr, int x, int y) { GFont *f = GetFont(); if (f) { GDisplayString ds(f, Tr->Text, Tr->Len); ssize_t c = ds.CharAt(x - Tr->x1); if (Tr->Text == PreText()) { return 0; } else { char16 *t = Tr->Text + c; size_t Len = StrlenW(Text()); if (t >= Text() && t <= Text() + Len) { return (t - Text()) - GetTextStart(); } else { LgiTrace("%s:%i - Error getting char at position.\n", _FL); } } } return -1; } void GTag::GetTagByPos(GTagHit &TagHit, int x, int y, int Depth, bool InBody, bool DebugLog) { /* InBody: Originally I had this test in the code but it seems that some test cases have actual content after the body. And testing for "InBody" breaks functionality for those cases (see "spam4.html" and the unsubscribe link at the end of the doc). */ if (TagId == TAG_IMG) { GRect img(0, 0, Size.x - 1, Size.y - 1); if (/*InBody &&*/ img.Overlap(x, y)) { TagHit.Direct = this; TagHit.Block = 0; } } else if (/*InBody &&*/ TextPos.Length()) { for (unsigned i=0; i= Tr->y1 && y <= Tr->y2; int Near = IsNearRect(Tr, x, y); if (Near >= 0 && Near < 100) { if ( !TagHit.NearestText || ( SameRow && !TagHit.NearSameRow ) || ( SameRow == TagHit.NearSameRow && Near < TagHit.Near ) ) { TagHit.NearestText = this; TagHit.NearSameRow = SameRow; TagHit.Block = Tr; TagHit.Near = Near; TagHit.Index = NearestChar(Tr, x, y); if (DebugLog) { LgiTrace("%i:GetTagByPos HitText %s #%s, idx=%i, near=%i, txt='%S'\n", Depth, Tag.Get(), HtmlId, TagHit.Index, TagHit.Near, Tr->Text); } if (!TagHit.Near) { TagHit.Direct = this; TagHit.LocalCoords.x = x; TagHit.LocalCoords.y = y; } } } } } else if ( TagId != TAG_TR && Tag && x >= 0 && y >= 0 && x < Size.x && y < Size.y // && InBody ) { // Direct hit TagHit.Direct = this; TagHit.LocalCoords.x = x; TagHit.LocalCoords.y = y; if (DebugLog) { LgiTrace("%i:GetTagByPos DirectHit %s #%s, idx=%i, near=%i\n", Depth, Tag.Get(), HtmlId, TagHit.Index, TagHit.Near); } } if (TagId == TAG_BODY) InBody = true; for (unsigned i=0; iPos.x >= 0 && t->Pos.y >= 0) { t->GetTagByPos(TagHit, x - t->Pos.x, y - t->Pos.y, Depth + 1, InBody, DebugLog); } } } int GTag::OnNotify(int f) { if (!Ctrl || !Html->InThread()) return 0; switch (CtrlType) { case CtrlSubmit: { GTag *Form = this; while (Form && Form->TagId != TAG_FORM) Form = ToTag(Form->Parent); if (Form) Html->OnSubmitForm(Form); break; } default: { CtrlValue = Ctrl->Name(); break; } } return 0; } void GTag::CollectFormValues(LHashTbl,char*> &f) { if (CtrlType != CtrlNone) { const char *Name; if (Get("name", Name)) { char *Existing = f.Find(Name); if (Existing) DeleteArray(Existing); char *Val = CtrlValue.Str(); if (Val) { GStringPipe p(256); for (char *v = Val; *v; v++) { if (*v == ' ') p.Write("+", 1); else if (IsAlpha(*v) || IsDigit(*v) || *v == '_' || *v == '.') p.Write(v, 1); else p.Print("%%%02.2X", *v); } f.Add(Name, p.NewStr()); } else { f.Add(Name, NewStr("")); } } } for (unsigned i=0; iCollectFormValues(f); } } GTag *GTag::FindCtrlId(int Id) { if (Ctrl && Ctrl->GetId() == Id) return this; for (unsigned i=0; iFindCtrlId(Id); if (f) return f; } return NULL; } void GTag::Find(int TagType, GArray &Out) { if (TagId == TagType) { Out.Add(this); } for (unsigned i=0; iFind(TagType, Out); } } void GTag::SetImage(const char *Uri, GSurface *Img) { if (Img) { if (TagId != TAG_IMG) { ImageDef *Def = (ImageDef*)GCss::Props.Find(PropBackgroundImage); if (Def) { Def->Type = ImageOwn; DeleteObj(Def->Img); Def->Img = Img; } } else { Image.Reset(Img); GRect r = XSubRect(); if (r.Valid()) { GAutoPtr t(new GMemDC(r.X(), r.Y(), Image->GetColourSpace())); if (t) { t->Blt(0, 0, Image, &r); Image = t; } } } for (unsigned i=0; iCell) { t->Cell->MinContent = 0; t->Cell->MaxContent = 0; } } } else { Html->d->Loading.Add(Uri, this); } } void GTag::LoadImage(const char *Uri) { #if DOCUMENT_LOAD_IMAGES if (!Html->Environment) return; GUri u(Uri); bool LdImg = Html->GetLoadImages(); bool IsRemote = u.Protocol && ( !_stricmp(u.Protocol, "http") || !_stricmp(u.Protocol, "https") || !_stricmp(u.Protocol, "ftp") ); if (IsRemote && !LdImg) { Html->NeedsCapability("RemoteContent"); return; } GDocumentEnv::LoadJob *j = Html->Environment->NewJob(); if (j) { LgiAssert(Html != NULL); j->Uri.Reset(NewStr(Uri)); j->Env = Html->Environment; j->UserData = this; j->UserUid = Html->GetDocumentUid(); GDocumentEnv::LoadType Result = Html->Environment->GetContent(j); if (Result == GDocumentEnv::LoadImmediate) { SetImage(Uri, j->pDC.Release()); } else if (Result == GDocumentEnv::LoadDeferred) { Html->d->DeferredLoads++; } DeleteObj(j); } #endif } void GTag::LoadImages() { const char *Uri = 0; if (Html->Environment && TagId == TAG_IMG && !Image && Get("src", Uri)) { LoadImage(Uri); } for (unsigned i=0; iLoadImages(); } } void GTag::ImageLoaded(char *uri, GSurface *Img, int &Used) { const char *Uri = 0; if (!Image && Get("src", Uri)) { if (strcmp(Uri, uri) == 0) { if (Used == 0) { SetImage(Uri, Img); } else { SetImage(Uri, new GMemDC(Img)); } Used++; } } for (unsigned i=0; iImageLoaded(uri, Img, Used); } } struct GTagElementCallback : public GCss::ElementCallback { const char *Val; const char *GetElement(GTag *obj) { return obj->Tag; } const char *GetAttr(GTag *obj, const char *Attr) { if (obj->Get(Attr, Val)) return Val; return NULL; } bool GetClasses(GString::Array &Classes, GTag *obj) { Classes = obj->Class; return Classes.Length() > 0; } GTag *GetParent(GTag *obj) { return ToTag(obj->Parent); } GArray GetChildren(GTag *obj) { GArray c; for (unsigned i=0; iChildren.Length(); i++) c.Add(ToTag(obj->Children[i])); return c; } }; void GTag::RestyleAll() { Restyle(); for (unsigned i=0; iRestyleAll(); } } // After CSS has changed this function scans through the CSS and applies any rules // that match the current tag. void GTag::Restyle() { // Use the matching built into the GCss Store. GCss::SelArray Styles; GTagElementCallback Context; if (Html->CssStore.Match(Styles, &Context, this)) { for (unsigned i=0; iStyle); } } // Do the element specific styles const char *s; if (Get("style", s)) SetCssStyle(s); #if DEBUG_RESTYLE && defined(_DEBUG) if (Debug) { GAutoString Style = ToString(); LgiTrace(">>>> %s <<<<:\n%s\n\n", Tag.Get(), Style.Get()); } #endif } void GTag::SetStyle() { const static float FntMul[] = { 0.6f, // size=1 0.89f, // size=2 1.0f, // size=3 1.2f, // size=4 1.5f, // size=5 2.0f, // size=6 3.0f // size=7 }; const char *s = 0; #ifdef _DEBUG if (Get("debug", s)) { if ((Debug = atoi(s))) { LgiTrace("Debug Tag: %p '%s'\n", this, Tag ? Tag.Get() : "CONTENT"); } } #endif if (Get("Color", s)) { ColorDef Def; if (GHtmlParser::ParseColour(s, Def)) { Color(Def); } } if (Get("Background", s) || Get("bgcolor", s)) { ColorDef Def; if (GHtmlParser::ParseColour(s, Def)) { BackgroundColor(Def); } else { GCss::ImageDef Img; Img.Type = ImageUri; Img.Uri = s; BackgroundImage(Img); BackgroundRepeat(RepeatBoth); } } switch (TagId) { default: break; case TAG_LINK: { const char *Type, *Href; if (Html->Environment && Get("type", Type) && Get("href", Href) && !_stricmp(Type, "text/css") && !Html->CssHref.Find(Href)) { GDocumentEnv::LoadJob *j = Html->Environment->NewJob(); if (j) { LgiAssert(Html != NULL); GTag *t = this; j->Uri.Reset(NewStr(Href)); j->Env = Html->Environment; j->UserData = t; j->UserUid = Html->GetDocumentUid(); GDocumentEnv::LoadType Result = Html->Environment->GetContent(j); if (Result == GDocumentEnv::LoadImmediate) { GStreamI *s = j->GetStream(); if (s) { int Len = (int)s->GetSize(); if (Len > 0) { GAutoString a(new char[Len+1]); ssize_t r = s->Read(a, Len); a[r] = 0; Html->CssHref.Add(Href, true); Html->OnAddStyle("text/css", a); } } } else if (Result == GDocumentEnv::LoadDeferred) { Html->d->DeferredLoads++; } DeleteObj(j); } } break; } case TAG_BLOCKQUOTE: { MarginTop(Len("8px")); MarginBottom(Len("8px")); MarginLeft(Len("16px")); if (Get("Type", s)) { if (_stricmp(s, "cite") == 0) { BorderLeft(BorderDef(this, "1px solid blue")); PaddingLeft(Len("0.5em")); /* ColorDef Def; Def.Type = ColorRgb; Def.Rgb32 = Rgb32(0x80, 0x80, 0x80); Color(Def); */ } } break; } case TAG_P: { MarginBottom(Len("1em")); break; } case TAG_A: { const char *Href; if (Get("href", Href)) { ColorDef c; c.Type = ColorRgb; c.Rgb32 = Rgb32(0, 0, 255); Color(c); TextDecoration(TextDecorUnderline); } break; } case TAG_TABLE: { Len l; if (!Cell) Cell = new TblCell; if (Get("border", s)) { BorderDef b; if (b.Parse(this, s)) { BorderLeft(b); BorderRight(b); BorderTop(b); BorderBottom(b); } } if (Get("cellspacing", s) && l.Parse(s, PropBorderSpacing, ParseRelaxed)) { BorderSpacing(l); } else { // BorderSpacing(GCss::Len(GCss::LenPx, 2.0f)); } if (Get("cellpadding", s) && l.Parse(s, Prop_CellPadding, ParseRelaxed)) { _CellPadding(l); } if (Get("align", s)) { Len l; if (l.Parse(s)) Cell->XAlign = l.Type; } break; } case TAG_TD: case TAG_TH: { if (!Cell) Cell = new TblCell; GTag *Table = GetTable(); if (Table) { Len l = Table->_CellPadding(); if (!l.IsValid()) { l.Type = GCss::LenPx; l.Value = DefaultCellPadding; } PaddingLeft(l); PaddingRight(l); PaddingTop(l); PaddingBottom(l); } if (TagId == TAG_TH) FontWeight(GCss::FontWeightBold); break; } case TAG_BODY: { MarginLeft(Len(Get("leftmargin", s) ? s : DefaultBodyMargin)); MarginTop(Len(Get("topmargin", s) ? s : DefaultBodyMargin)); MarginRight(Len(Get("rightmargin", s) ? s : DefaultBodyMargin)); if (Get("text", s)) { ColorDef c; if (c.Parse(s)) { Color(c); } } break; } case TAG_OL: case TAG_UL: { MarginLeft(Len("16px")); break; } case TAG_STRONG: case TAG_B: { FontWeight(FontWeightBold); break; } case TAG_I: { FontStyle(FontStyleItalic); break; } case TAG_U: { TextDecoration(TextDecorUnderline); break; } } if (Get("width", s)) { Len l; if (l.Parse(s, PropWidth, ParseRelaxed)) { Width(l); } } if (Get("height", s)) { Len l; if (l.Parse(s, PropHeight, ParseRelaxed)) Height(l); } if (Get("align", s)) { if (_stricmp(s, "left") == 0) TextAlign(Len(AlignLeft)); else if (_stricmp(s, "right") == 0) TextAlign(Len(AlignRight)); else if (_stricmp(s, "center") == 0) TextAlign(Len(AlignCenter)); } if (Get("valign", s)) { if (_stricmp(s, "top") == 0) VerticalAlign(Len(VerticalTop)); else if (_stricmp(s, "middle") == 0) VerticalAlign(Len(VerticalMiddle)); else if (_stricmp(s, "bottom") == 0) VerticalAlign(Len(VerticalBottom)); } Get("id", HtmlId); if (Get("class", s)) { Class = GString(s).SplitDelimit(" \t"); } Restyle(); switch (TagId) { default: break; case TAG_BIG: { GCss::Len l; l.Type = SizeLarger; FontSize(l); break; } /* case TAG_META: { GAutoString Cs; const char *s; if (Get("http-equiv", s) && _stricmp(s, "Content-Type") == 0) { const char *ContentType; if (Get("content", ContentType)) { char *CharSet = stristr(ContentType, "charset="); if (CharSet) { char16 *cs = NULL; Html->ParsePropValue(CharSet + 8, cs); Cs.Reset(WideToUtf8(cs)); DeleteArray(cs); } } } if (Get("name", s) && _stricmp(s, "charset") == 0 && Get("content", s)) { Cs.Reset(NewStr(s)); } else if (Get("charset", s)) { Cs.Reset(NewStr(s)); } if (Cs) { if (Cs && _stricmp(Cs, "utf-16") != 0 && _stricmp(Cs, "utf-32") != 0 && LgiGetCsInfo(Cs)) { // Html->SetCharset(Cs); } } break; } */ case TAG_BODY: { GCss::ColorDef Bk = BackgroundColor(); if (Bk.Type != ColorInherit) { // Copy the background up to the GHtml wrapper Html->GetCss(true)->BackgroundColor(Bk); } /* GFont *f = GetFont(); if (FontSize().Type == LenInherit) { FontSize(Len(LenPt, (float)f->PointSize())); } */ break; } case TAG_HEAD: { Display(DispNone); break; } case TAG_PRE: { GFontType Type; if (Type.GetSystemFont("Fixed")) { LgiAssert(ValidStr(Type.GetFace())); FontFamily(StringsDef(Type.GetFace())); } break; } case TAG_TR: break; case TAG_TD: case TAG_TH: { LgiAssert(Cell != NULL); const char *s; if (Get("colspan", s)) Cell->Span.x = atoi(s); else Cell->Span.x = 1; if (Get("rowspan", s)) Cell->Span.y = atoi(s); else Cell->Span.y = 1; Cell->Span.x = MAX(Cell->Span.x, 1); Cell->Span.y = MAX(Cell->Span.y, 1); if (Display() == DispInline || Display() == DispInlineBlock) { Display(DispBlock); // Inline-block TD??? Nope. } break; } case TAG_IMG: { const char *Uri; if (Html->Environment && Get("src", Uri)) { LoadImage(Uri); } break; } case TAG_H1: { char s[32]; sprintf_s(s, sizeof(s), "%ipt", (int)((float)Html->DefFont()->PointSize() * FntMul[5])); FontSize(Len(s)); FontWeight(FontWeightBold); break; } case TAG_H2: { char s[32]; sprintf_s(s, sizeof(s), "%ipt", (int)((float)Html->DefFont()->PointSize() * FntMul[4])); FontSize(Len(s)); FontWeight(FontWeightBold); break; } case TAG_H3: { char s[32]; sprintf_s(s, sizeof(s), "%ipt", (int)((float)Html->DefFont()->PointSize() * FntMul[3])); FontSize(Len(s)); FontWeight(FontWeightBold); break; } case TAG_H4: { char s[32]; sprintf_s(s, sizeof(s), "%ipt", (int)((float)Html->DefFont()->PointSize() * FntMul[2])); FontSize(Len(s)); FontWeight(FontWeightBold); break; } case TAG_H5: { char s[32]; sprintf_s(s, sizeof(s), "%ipt", (int)((float)Html->DefFont()->PointSize() * FntMul[1])); FontSize(Len(s)); FontWeight(FontWeightBold); break; } case TAG_H6: { char s[32]; sprintf_s(s, sizeof(s), "%ipt", (int)((float)Html->DefFont()->PointSize() * FntMul[0])); FontSize(Len(s)); FontWeight(FontWeightBold); break; } case TAG_FONT: { const char *s = 0; if (Get("Face", s)) { char16 *cw = CleanText(s, strlen(s), "utf-8", true); char *c8 = WideToUtf8(cw); DeleteArray(cw); GToken Faces(c8, ","); DeleteArray(c8); char *face = TrimStr(Faces[0]); if (ValidStr(face)) { FontFamily(face); DeleteArray(face); } else { LgiTrace("%s:%i - No face for font tag.\n", __FILE__, __LINE__); } } if (Get("Size", s)) { bool Digit = false, NonW = false; for (auto *c = s; *c; c++) { if (IsDigit(*c) || *c == '-') Digit = true; else if (!IsWhiteSpace(*c)) NonW = true; } if (Digit && !NonW) { auto Sz = atoi(s); switch (Sz) { case 1: FontSize(Len(GCss::LenEm, 0.63f)); break; case 2: FontSize(Len(GCss::LenEm, 0.82f)); break; case 3: FontSize(Len(GCss::LenEm, 1.0f)); break; case 4: FontSize(Len(GCss::LenEm, 1.13f)); break; case 5: FontSize(Len(GCss::LenEm, 1.5f)); break; case 6: FontSize(Len(GCss::LenEm, 2.0f)); break; case 7: FontSize(Len(GCss::LenEm, 3.0f)); break; } } else { FontSize(Len(s)); } } break; } case TAG_SELECT: { if (!Html->InThread()) break; LgiAssert(!Ctrl); Ctrl = new GCombo(Html->d->NextCtrlId++, 0, 0, 100, SysFont->GetHeight() + 8, NULL); CtrlType = CtrlSelect; break; } case TAG_INPUT: { if (!Html->InThread()) break; LgiAssert(!Ctrl); const char *Type, *Value = NULL; Get("value", Value); GAutoWString CleanValue(Value ? CleanText(Value, strlen(Value), "utf-8", true, true) : NULL); if (CleanValue) { CtrlValue = CleanValue; } if (Get("type", Type)) { if (!_stricmp(Type, "password")) CtrlType = CtrlPassword; else if (!_stricmp(Type, "email")) CtrlType = CtrlEmail; else if (!_stricmp(Type, "text")) CtrlType = CtrlText; else if (!_stricmp(Type, "button")) CtrlType = CtrlButton; else if (!_stricmp(Type, "submit")) CtrlType = CtrlSubmit; else if (!_stricmp(Type, "hidden")) CtrlType = CtrlHidden; DeleteObj(Ctrl); if (CtrlType == CtrlEmail || CtrlType == CtrlText || CtrlType == CtrlPassword) { GEdit *Ed; GAutoString UtfCleanValue(WideToUtf8(CleanValue)); Ctrl = Ed = new GEdit(Html->d->NextCtrlId++, 0, 0, 60, SysFont->GetHeight() + 8, UtfCleanValue); if (Ctrl) { Ed->Sunken(false); Ed->Password(CtrlType == CtrlPassword); } } else if (CtrlType == CtrlButton || CtrlType == CtrlSubmit) { GAutoString UtfCleanValue(WideToUtf8(CleanValue)); if (UtfCleanValue) { Ctrl = new InputButton(this, Html->d->NextCtrlId++, UtfCleanValue); } } } break; } } if (IsBlock()) { GCss::ImageDef bk = BackgroundImage(); if (bk.Type == GCss::ImageUri && ValidStr(bk.Uri)) { LoadImage(bk.Uri); } } if (Ctrl) { GFont *f = GetFont(); if (f) Ctrl->SetFont(f, false); } if (Display() == DispBlock && Html->Environment) { GCss::ImageDef Img = BackgroundImage(); if (Img.Type == ImageUri) { LoadImage(Img.Uri); } } } void GTag::SetCssStyle(const char *Style) { if (Style) { // Strip out comments char *Comment = 0; while ((Comment = strstr((char*)Style, "/*"))) { char *End = strstr(Comment+2, "*/"); if (!End) break; for (char *c = Comment; c<=End+2; c++) *c = ' '; } // Parse CSS const char *Ptr = Style; GCss::Parse(Ptr, GCss::ParseRelaxed); } } char16 *GTag::CleanText(const char *s, ssize_t Len, const char *SourceCs, bool ConversionAllowed, bool KeepWhiteSpace) { if (!s || Len <= 0) return NULL; static const char *DefaultCs = "iso-8859-1"; char16 *t = 0; bool DocAndCsTheSame = false; if (Html->DocCharSet && Html->Charset) { DocAndCsTheSame = _stricmp(Html->DocCharSet, Html->Charset) == 0; } if (SourceCs) { t = (char16*) LgiNewConvertCp(LGI_WideCharset, s, SourceCs, Len); } else if (Html->DocCharSet && Html->Charset && !DocAndCsTheSame && !Html->OverideDocCharset) { char *DocText = (char*)LgiNewConvertCp(Html->DocCharSet, s, Html->Charset, Len); t = (char16*) LgiNewConvertCp(LGI_WideCharset, DocText, Html->DocCharSet, -1); DeleteArray(DocText); } else if (Html->DocCharSet) { t = (char16*) LgiNewConvertCp(LGI_WideCharset, s, Html->DocCharSet, Len); } else { t = (char16*) LgiNewConvertCp(LGI_WideCharset, s, Html->Charset.Get() ? Html->Charset.Get() : DefaultCs, Len); } if (t && ConversionAllowed) { char16 *o = t; for (char16 *i=t; *i; ) { switch (*i) { case '&': { i++; if (*i == '#') { // Unicode Number char n[32] = "", *p = n; i++; if (*i == 'x' || *i == 'X') { // Hex number i++; while ( *i && ( IsDigit(*i) || (*i >= 'A' && *i <= 'F') || (*i >= 'a' && *i <= 'f') ) && (p - n) < 31) { *p++ = (char)*i++; } } else { // Decimal number while (*i && IsDigit(*i) && (p - n) < 31) { *p++ = (char)*i++; } } *p++ = 0; char16 Ch = atoi(n); if (Ch) { *o++ = Ch; } if (*i && *i != ';') i--; } else { // Named Char char16 *e = i; while (*e && IsAlpha(*e) && *e != ';') { e++; } GAutoWString Var(NewStrW(i, e-i)); char16 Char = GHtmlStatic::Inst->VarMap.Find(Var); if (Char) { *o++ = Char; i = e; } else { i--; *o++ = *i; } } break; } case '\r': { break; } case ' ': case '\t': case '\n': { if (KeepWhiteSpace) { *o++ = *i; } else { *o++ = ' '; // Skip furthur whitespace while (i[1] && IsWhiteSpace(i[1])) { i++; } } break; } default: { // Normal char *o++ = *i; break; } } if (*i) i++; else break; } *o++ = 0; } if (t && !*t) { DeleteArray(t); } return t; } char *GTag::ParseText(char *Doc) { ColorDef c; c.Type = ColorRgb; c.Rgb32 = LC_WORKSPACE; BackgroundColor(c); TagId = TAG_BODY; Tag.Reset(NewStr("body")); Info = Html->GetTagInfo(Tag); char *OriginalCp = NewStr(Html->Charset); GStringPipe Utf16; char *s = Doc; while (s) { if (*s == '\r') { s++; } else if (*s == '<') { // Process tag char *e = s; e++; while (*e && *e != '>') { if (*e == '\"' || *e == '\'') { char *q = strchr(e + 1, *e); if (q) e = q + 1; else e++; } else e++; } if (*e == '>') e++; // Output tag Html->SetCharset("iso-8859-1"); char16 *t = CleanText(s, e - s, NULL, false); if (t) { Utf16.Push(t); DeleteArray(t); } s = e; } else if (!*s || *s == '\n') { // Output previous line char16 *Line = Utf16.NewStrW(); if (Line) { GTag *t = new GTag(Html, this); if (t) { t->Color(ColorDef(ColorRgb, Rgb24To32(LC_TEXT))); t->Text(Line); } } if (*s == '\n') { s++; GTag *t = new GTag(Html, this); if (t) { t->TagId = TAG_BR; t->Tag.Reset(NewStr("br")); t->Info = Html->GetTagInfo(t->Tag); } } else break; } else { // Seek end of text char *e = s; while (*e && *e != '\r' && *e != '\n' && *e != '<') e++; // Output text Html->SetCharset(OriginalCp); GAutoWString t(CleanText(s, e - s, NULL, false)); if (t) { Utf16.Push(t); } s = e; } } Html->SetCharset(OriginalCp); DeleteArray(OriginalCp); return 0; } bool GTag::ConvertToText(TextConvertState &State) { const static char *Rule = "------------------------------------------------------"; int DepthInc = 0; switch (TagId) { default: break; case TAG_P: if (State.GetPrev()) State.NewLine(); break; case TAG_UL: case TAG_OL: DepthInc = 2; break; } if (ValidStrW(Txt)) { for (int i=0; iConvertToUnicode(Txt); else u.Reset(WideToUtf8(Txt)); if (u) { size_t u_len = strlen(u); State.Write(u, u_len); } } State.Depth += DepthInc; for (unsigned i=0; iConvertToText(State); } State.Depth -= DepthInc; if (IsBlock()) { if (State.CharsOnLine) State.NewLine(); } else { switch (TagId) { case TAG_A: { // Emit the link to the anchor if it's different from the text of the span... const char *Href; if (Get("href", Href) && ValidStrW(Txt)) { if (_strnicmp(Href, "mailto:", 7) == 0) Href += 7; size_t HrefLen = strlen(Href); GAutoWString h(CleanText(Href, HrefLen, "utf-8")); if (h && StrcmpW(h, Txt) != 0) { // Href different from the text of the link State.Write(" (", 2); State.Write(Href, HrefLen); State.Write(")", 1); } } break; } case TAG_HR: { State.Write(Rule, strlen(Rule)); State.NewLine(); break; } case TAG_BR: { State.NewLine(); break; } default: break; } } return true; } char *GTag::NextTag(char *s) { while (s && *s) { char *n = strchr(s, '<'); if (n) { if (!n[1]) return NULL; if (IsAlpha(n[1]) || strchr("!/", n[1]) || n[1] == '?') { return n; } s = n + 1; } else break; } return 0; } void GHtml::CloseTag(GTag *t) { if (!t) return; OpenTags.Delete(t); } bool GTag::OnUnhandledColor(GCss::ColorDef *def, const char *&s) { const char *e = s; while (*e && (IsText(*e) || *e == '_')) e++; char tmp[256]; ssize_t len = e - s; memcpy(tmp, s, len); tmp[len] = 0; int m = GHtmlStatic::Inst->ColourMap.Find(tmp); s = e; if (m >= 0) { def->Type = GCss::ColorRgb; def->Rgb32 = Rgb24To32(m); return true; } return false; } void GTag::ZeroTableElements() { if (TagId == TAG_TABLE || TagId == TAG_TR || IsTableCell(TagId)) { Size.x = 0; Size.y = 0; if (Cell) { Cell->MinContent = 0; Cell->MaxContent = 0; } for (unsigned i=0; iZeroTableElements(); } } } void GTag::ResetCaches() { /* If during the parse process a callback causes a layout to happen then it's possible to have partial information in the GHtmlTableLayout structure, like missing TD cells. Because they haven't been parsed yet. This is called at the end of the parsing to reset all the cached info in GHtmlTableLayout. That way when the first real layout happens all the data is there. */ if (Cell) DeleteObj(Cell->Cells); for (size_t i=0; iResetCaches(); } GdcPt2 GTag::GetTableSize() { GdcPt2 s(0, 0); if (Cell && Cell->Cells) { Cell->Cells->GetSize(s.x, s.y); } return s; } GTag *GTag::GetTableCell(int x, int y) { GTag *t = this; while ( t && !t->Cell && !t->Cell->Cells && t->Parent) { t = ToTag(t->Parent); } if (t && t->Cell && t->Cell->Cells) { return t->Cell->Cells->Get(x, y); } return 0; } // This function gets the largest and smallest piece of content // in this cell and all it's children. bool GTag::GetWidthMetrics(GTag *Table, uint16 &Min, uint16 &Max) { bool Status = true; int MarginPx = 0; int LineWidth = 0; if (Display() == GCss::DispNone) return true; // Break the text into words and measure... if (Text()) { int MinContent = 0; int MaxContent = 0; GFont *f = GetFont(); if (f) { for (char16 *s = Text(); s && *s; ) { // Skip whitespace... while (*s && StrchrW(WhiteW, *s)) s++; // Find end of non-whitespace char16 *e = s; while (*e && !StrchrW(WhiteW, *e)) e++; // Find size of the word ssize_t Len = e - s; if (Len > 0) { GDisplayString ds(f, s, Len); MinContent = MAX(MinContent, ds.X()); } // Move to the next word. s = (*e) ? e + 1 : 0; } GDisplayString ds(f, Text()); LineWidth = MaxContent = ds.X(); } #ifdef _DEBUG if (Debug) { LgiTrace("GetWidthMetrics Font=%p Sz=%i,%i\n", f, MinContent, MaxContent); } #endif Min = MAX(Min, MinContent); Max = MAX(Max, MaxContent); } // Specific tag handling? switch (TagId) { default: { if (IsBlock()) { MarginPx = (int)(BorderLeft().ToPx() + BorderRight().ToPx() + PaddingLeft().ToPx() + PaddingRight().ToPx()); } break; } case TAG_IMG: { Len w = Width(); if (w.IsValid()) { int x = (int) w.Value; Min = MAX(Min, x); Max = MAX(Max, x); } else if (Image) { Min = Max = Image->X(); } else { Size.x = Size.y = DefaultImgSize; Min = MAX(Min, Size.x); Max = MAX(Max, Size.x); } break; } case TAG_TD: case TAG_TH: { Len w = Width(); if (w.IsValid()) { if (w.IsDynamic()) { Min = MAX(Min, (int)w.Value); Max = MAX(Max, (int)w.Value); } else { Max = w.ToPx(0, GetFont()); } } else { GCss::BorderDef BLeft = BorderLeft(); GCss::BorderDef BRight = BorderRight(); GCss::Len PLeft = PaddingLeft(); GCss::Len PRight = PaddingRight(); MarginPx = (int)(PLeft.ToPx() + PRight.ToPx() + BLeft.ToPx()); if (Table->BorderCollapse() == GCss::CollapseCollapse) MarginPx += BRight.ToPx(); } break; } case TAG_TABLE: { Len w = Width(); if (w.IsValid() && !w.IsDynamic()) { // Fixed width table... int CellSpacing = BorderSpacing().ToPx(Min, GetFont()); int Px = ((int)w.Value) + (CellSpacing << 1); Min = MAX(Min, Px); Max = MAX(Max, Px); return true; } else { GdcPt2 s; GHtmlTableLayout c(this); c.GetSize(s.x, s.y); // Auto layout table GArray ColMin, ColMax; for (int y=0; yGetWidthMetrics(Table, a, b)) { ColMin[x] = MAX(ColMin[x], a); ColMax[x] = MAX(ColMax[x], b); } x += t->Cell->Span.x; } else break; } } int MinSum = 0, MaxSum = 0; for (int i=0; iGetWidthMetrics(Table, Min, TagMax); LineWidth += TagMax; if (c->TagId == TAG_BR || c->TagId == TAG_LI) { Max = MAX(Max, LineWidth); LineWidth = 0; } } Max = MAX(Max, LineWidth); Min += MarginPx; Max += MarginPx; return Status; } static void DistributeSize(GArray &a, int Start, int Span, int Size, int Border) { // Calculate the current size of the cells int Cur = -Border; for (int i=0; i T Sum(GArray &a) { T s = 0; for (unsigned i=0; iCells) { #if defined(_DEBUG) && DEBUG_TABLE_LAYOUT if (Debug) { //int asd=0; } #endif Cell->Cells = new GHtmlTableLayout(this); #if defined(_DEBUG) && DEBUG_TABLE_LAYOUT if (Cell->Cells && Debug) Cell->Cells->Dump(); #endif } if (Cell->Cells) Cell->Cells->LayoutTable(f, Depth); } void GHtmlTableLayout::AllocatePx(int StartCol, int Cols, int MinPx, bool HasToFillAllAvailable) { // Get the existing total size and size of the column set int CurrentTotalX = GetTotalX(); int CurrentSpanX = GetTotalX(StartCol, Cols); int MaxAdditionalPx = AvailableX - CurrentTotalX; if (MaxAdditionalPx <= 0) return; // Calculate the maximum space we have for this column set int AvailPx = (CurrentSpanX + MaxAdditionalPx) - BorderX1 - BorderX2; // Allocate any remaining space... int RemainingPx = MaxAdditionalPx; GArray Growable, NonGrowable, SizeInherit; int GrowablePx = 0; for (int x=StartCol; x 0) { GrowablePx += DiffPx; Growable.Add(x); } else if (MinCol[x] > 0) { NonGrowable.Add(x); } else if (MinCol[x] == 0 && CurrentSpanX < AvailPx) { // Growable.Add(x); } if (SizeCol[x].Type == GCss::LenInherit) SizeInherit.Add(x); } if (GrowablePx < RemainingPx && HasToFillAllAvailable) { if (Growable.Length() == 0) { // Add any suitable non-growable columns as well for (unsigned i=0; i MinCol[Largest]) Largest = i; } Growable.Add(Largest); } } if (Growable.Length()) { // Some growable columns... int Added = 0; // Reasonably increase the size of the columns... for (unsigned i=0; i 0) { AddPx = DiffPx; } else if (DiffPx > 0) { double Ratio = (double)DiffPx / GrowablePx; AddPx = (int) (Ratio * RemainingPx); } else { AddPx = RemainingPx / Growable.Length(); } LgiAssert(AddPx >= 0); MinCol[x] += AddPx; Added += AddPx; } if (Added < RemainingPx && HasToFillAllAvailable) { // Still more to add, so if (SizeInherit.Length()) { Growable = SizeInherit; } else { int Largest = -1; for (unsigned i=0; i MinCol[Largest]) Largest = x; } Growable.Length(1); Growable[0] = Largest; } int AddPx = (RemainingPx - Added) / Growable.Length(); for (unsigned i=0; i= 0); } } } } struct ColInfo { int Large; int Growable; int Idx; int Px; }; int ColInfoCmp(ColInfo *a, ColInfo *b) { int LDiff = b->Large - a->Large; int LGrow = b->Growable - a->Growable; int LSize = b->Px - a->Px; return LDiff + LGrow + LSize; } void GHtmlTableLayout::DeallocatePx(int StartCol, int Cols, int MaxPx) { int TotalPx = GetTotalX(StartCol, Cols); if (TotalPx <= MaxPx || MaxPx == 0) return; int TrimPx = TotalPx - MaxPx; GArray Inf; int HalfMax = MaxPx >> 1; unsigned Interesting = 0; int InterestingPx = 0; for (int x=StartCol; x HalfMax; ci.Growable = MinCol[x] < MaxCol[x]; if (ci.Large || ci.Growable) { Interesting++; InterestingPx += ci.Px; } } Inf.Sort(ColInfoCmp); for (unsigned i=0; iGetFont(); Table->ZeroTableElements(); MinCol.Length(0); MaxCol.Length(0); MaxRow.Length(0); SizeCol.Length(0); GCss::Len BdrSpacing = Table->BorderSpacing(); CellSpacing = BdrSpacing.IsValid() ? (int)BdrSpacing.Value : 0; // Resolve total table width. TableWidth = Table->Width(); if (TableWidth.IsValid()) AvailableX = f->ResolveX(TableWidth, Table, false); else AvailableX = f->X(); GCss::Len MaxWidth = Table->MaxWidth(); if (MaxWidth.IsValid()) { int Px = f->ResolveX(MaxWidth, Table, false); if (Px < AvailableX) AvailableX = Px; } TableBorder = f->ResolveBorder(Table, Table); if (Table->BorderCollapse() != GCss::CollapseCollapse) TablePadding = f->ResolvePadding(Table, Table); else TablePadding.ZOff(0, 0); BorderX1 = TableBorder.x1 + TablePadding.x1; BorderX2 = TableBorder.x2 + TablePadding.x2; #if defined(_DEBUG) && DEBUG_TABLE_LAYOUT if (Table->Debug) LgiTrace("AvailableX=%i, BorderX1=%i, BorderX2=%i\n", AvailableX, BorderX1, BorderX2); #endif #ifdef _DEBUG if (Table->Debug) { printf("Table Debug\n"); } #endif // Size detection pass int y; for (y=0; yGetFont(); t->Cell->BorderPx = f->ResolveBorder(t, t); t->Cell->PaddingPx = f->ResolvePadding(t, t); if (t->Cell->Pos.x == x && t->Cell->Pos.y == y) { GCss::DisplayType Disp = t->Display(); if (Disp == GCss::DispNone) continue; GCss::Len Content = t->Width(); if (Content.IsValid() && t->Cell->Span.x == 1) { if (SizeCol[x].IsValid()) { int OldPx = f->ResolveX(SizeCol[x], t, false); int NewPx = f->ResolveX(Content, t, false); if (NewPx > OldPx) { SizeCol[x] = Content; } } else { SizeCol[x] = Content; } } if (!t->GetWidthMetrics(Table, t->Cell->MinContent, t->Cell->MaxContent)) { t->Cell->MinContent = 16; t->Cell->MaxContent = 16; } #if defined(_DEBUG) && DEBUG_TABLE_LAYOUT if (Table->Debug) LgiTrace("Content[%i,%i] MIN=%i MAX=%i\n", x, y, t->Cell->MinContent, t->Cell->MaxContent); #endif if (t->Cell->Span.x == 1) { int BoxPx = t->Cell->BorderPx.x1 + t->Cell->BorderPx.x2 + t->Cell->PaddingPx.x1 + t->Cell->PaddingPx.x2; MinCol[x] = MAX(MinCol[x], t->Cell->MinContent + BoxPx); MaxCol[x] = MAX(MaxCol[x], t->Cell->MaxContent + BoxPx); } } x += t->Cell->Span.x; } else break; } } // How much space used so far? int TotalX = GetTotalX(); if (TotalX > AvailableX) { // FIXME: // Off -> 'cisra-cqs.html' renders correctly. // On -> 'cisra_outage.html' renders correctly. #if 0 DeallocatePx(0, MinCol.Length(), AvailableX); TotalX = GetTotalX(); #endif } #if defined(_DEBUG) && DEBUG_TABLE_LAYOUT #define DumpCols(msg) \ if (Table->Debug) \ { \ LgiTrace("%s Ln%i - TotalX=%i AvailableX=%i\n", msg, __LINE__, TotalX, AvailableX); \ for (unsigned i=0; iDebug) { printf("TableDebug\n"); } #endif // Process spanned cells for (y=0; yCell->Pos.x == x && t->Cell->Pos.y == y) { if (t->Cell->Span.x > 1 || t->Cell->Span.y > 1) { int i; int ColMin = -CellSpacing; int ColMax = -CellSpacing; for (i=0; iCell->Span.x; i++) { ColMin += MinCol[x + i] + CellSpacing; ColMax += MaxCol[x + i] + CellSpacing; } GCss::Len Width = t->Width(); if (Width.IsValid()) { int Px = f->ResolveX(Width, t, false); t->Cell->MinContent = MAX(t->Cell->MinContent, Px); t->Cell->MaxContent = MAX(t->Cell->MaxContent, Px); } #if defined(_DEBUG) && DEBUG_TABLE_LAYOUT if (Table->Debug) LgiTrace("Content[%i,%i] MIN=%i MAX=%i\n", x, y, t->Cell->MinContent, t->Cell->MaxContent); #endif if (t->Cell->MinContent > ColMin) AllocatePx(t->Cell->Pos.x, t->Cell->Span.x, t->Cell->MinContent, false); if (t->Cell->MaxContent > ColMax) DistributeSize(MaxCol, t->Cell->Pos.x, t->Cell->Span.x, t->Cell->MaxContent, CellSpacing); } x += t->Cell->Span.x; } else break; } } TotalX = GetTotalX(); DumpCols("AfterSpannedCells"); // Sometimes the web page specifies too many percentages: // Scale them all. float PercentSum = 0.0f; for (int i=0; i 100.0) { float Ratio = PercentSum / 100.0f; for (int i=0; iResolveX(w, Table, false); if (w.Type == GCss::LenPercent) { MaxCol[x] = Px; } else if (Px > MinCol[x]) { int RemainingPx = AvailableX - TotalX; int AddPx = Px - MinCol[x]; AddPx = MIN(RemainingPx, AddPx); TotalX += AddPx; MinCol[x] += AddPx; } } } } TotalX = GetTotalX(); DumpCols("AfterCssNonPercentageSizes"); if (TotalX > AvailableX) { #if !ALLOW_TABLE_GROWTH // Deallocate space if overused // Take some from the largest column int Largest = 0; for (int i=0; i MinCol[Largest]) { Largest = i; } } int Take = TotalX - AvailableX; if (Take < MinCol[Largest]) { MinCol[Largest] = MinCol[Largest] - Take; TotalX -= Take; } DumpCols("AfterSpaceDealloc"); #endif } else if (TotalX < AvailableX) { AllocatePx(0, s.x, AvailableX, TableWidth.IsValid()); DumpCols("AfterRemainingAlloc"); } // Layout cell horizontally and then flow the contents to get // the height of all the cells GArray RowPad; MaxRow.Length(s.y); for (y=0; yCell->Pos.x == x && t->Cell->Pos.y == y) { t->Pos.x = XPos; t->Size.x = -CellSpacing; XPos -= CellSpacing; RowPad[y].y1 = MAX(RowPad[y].y1, t->Cell->BorderPx.y1 + t->Cell->PaddingPx.y1); RowPad[y].y2 = MAX(RowPad[y].y2, t->Cell->BorderPx.y2 + t->Cell->PaddingPx.y2); GRect Box(0, 0, -CellSpacing, 0); for (int i=0; iCell->Span.x; i++) { int ColSize = MinCol[x + i] + CellSpacing; LgiAssert(ColSize >= 0); if (ColSize < 0) break; t->Size.x += ColSize; XPos += ColSize; Box.x2 += ColSize; } GCss::Len Ht = t->Height(); GFlowRegion r(Table->Html, Box, true); t->OnFlow(&r, Depth+1); if (r.MAX.y > r.y2) { t->Size.y = MAX(r.MAX.y, t->Size.y); } if (Ht.IsValid() && Ht.Type != GCss::LenPercent) { int h = f->ResolveY(Ht, t, false); t->Size.y = MAX(h, t->Size.y); DistributeSize(MaxRow, y, t->Cell->Span.y, t->Size.y, CellSpacing); } } x += t->Cell->Span.x; } else break; } } #if defined(_DEBUG) && DEBUG_TABLE_LAYOUT if (Table->Debug) { LgiTrace("%s:%i - AfterCellFlow\n", _FL); for (unsigned i=0; iCell->Pos.x == x && t->Cell->Pos.y == y) { GCss::Len Ht = t->Height(); if (!(Ht.IsValid() && Ht.Type != GCss::LenPercent)) { DistributeSize(MaxRow, y, t->Cell->Span.y, t->Size.y, CellSpacing); } } x += t->Cell->Span.x; } else break; } } // Cell positioning int Cx = BorderX1 + CellSpacing; int Cy = TableBorder.y1 + TablePadding.y1 + CellSpacing; for (y=0; yParent); if (Row && Row->TagId == TAG_TR) { t = new GTag(Table->Html, Row); if (t) { t->TagId = TAG_TD; t->Tag.Reset(NewStr("td")); t->Info = Table->Html->GetTagInfo(t->Tag); if ((t->Cell = new GTag::TblCell)) { t->Cell->Pos.x = x; t->Cell->Pos.y = y; t->Cell->Span.x = 1; t->Cell->Span.y = 1; } t->BackgroundColor(GCss::ColorDef(GCss::ColorRgb, DefaultMissingCellColour)); Set(Table); } else break; } else break; } if (t) { if (t->Cell->Pos.x == x && t->Cell->Pos.y == y) { int RowPadOffset = RowPad[y].y1 - t->Cell->BorderPx.y1 - t->Cell->PaddingPx.y1; t->Pos.x = Cx; t->Pos.y = Cy + RowPadOffset; t->Size.x = -CellSpacing; for (int i=0; iCell->Span.x; i++) { int w = MinCol[x + i] + CellSpacing; t->Size.x += w; Cx += w; } t->Size.y = -CellSpacing; for (int n=0; nCell->Span.y; n++) { t->Size.y += MaxRow[y+n] + CellSpacing; } Table->Size.x = MAX(Cx + BorderX2, Table->Size.x); #if defined(_DEBUG) && DEBUG_TABLE_LAYOUT if (Table->Debug) { LgiTrace("cell(%i,%i) = pos(%i,%i)+size(%i,%i)\n", t->Cell->Pos.x, t->Cell->Pos.y, t->Pos.x, t->Pos.y, t->Size.x, t->Size.y); } #endif } else { Cx += t->Size.x + CellSpacing; } x += t->Cell->Span.x; } else break; Prev = t; } Cx = BorderX1 + CellSpacing; Cy += MaxRow[y] + CellSpacing; } switch (Table->Cell->XAlign ? Table->Cell->XAlign : ToTag(Table->Parent)->GetAlign(true)) { case GCss::AlignCenter: { int fx = f->X(); int Ox = (fx-Table->Size.x) >> 1; Table->Pos.x = f->x1 + MAX(Ox, 0); break; } case GCss::AlignRight: { Table->Pos.x = f->x2 - Table->Size.x; break; } default: { Table->Pos.x = f->x1; break; } } Table->Pos.y = f->y1; Table->Size.y = Cy + TablePadding.y2 + TableBorder.y2; } GRect GTag::ChildBounds() { GRect b(0, 0, -1, -1); for (unsigned i=0; iGetRect(); b.Union(&c); } else { b = t->GetRect(); } } return b; } GdcPt2 GTag::AbsolutePos() { GdcPt2 p; for (GTag *t=this; t; t=ToTag(t->Parent)) { p += t->Pos; } return p; } void GTag::SetSize(GdcPt2 &s) { Size = s; } GArea::~GArea() { DeleteObjects(); } GRect GArea::Bounds() { GRect n(0, 0, -1, -1); for (unsigned i=0; iLength(); i++) { GRect *r = (*c)[i]; if (!Top || (r && (r->y1 < Top->y1))) { Top = r; } } return Top; } void GArea::FlowText(GTag *Tag, GFlowRegion *Flow, GFont *Font, int LineHeight, char16 *Text, GCss::LengthType Align) { if (!Flow || !Text || !Font) return; char16 *Start = Text; size_t FullLen = StrlenW(Text); #if 1 if (!Tag->Html->GetReadOnly() && !*Text) { // Insert a text rect for this tag, even though it's empty. // This allows the user to place the cursor on a blank line. GFlowRect *Tr = new GFlowRect; Tr->Tag = Tag; Tr->Text = Text; Tr->x1 = Flow->cx; Tr->x2 = Tr->x1 + 1; Tr->y1 = Flow->y1; Tr->y2 = Tr->y1 + Font->GetHeight(); LgiAssert(Tr->y2 >= Tr->y1); Flow->y2 = MAX(Flow->y2, Tr->y2+1); Flow->cx = Tr->x2 + 1; Add(Tr); Flow->Insert(Tr); return; } #endif while (*Text) { GFlowRect *Tr = new GFlowRect; if (!Tr) break; Tr->Tag = Tag; Restart: Tr->x1 = Flow->cx; Tr->y1 = Flow->y1; #if 1 // I removed this at one stage but forget why. // Remove white space at start of line if not in edit mode.. if (Tag->Html->GetReadOnly() && Flow->x1 == Flow->cx && *Text == ' ') { Text++; if (!*Text) { DeleteObj(Tr); break; } } #endif Tr->Text = Text; GDisplayString ds(Font, Text, MIN(1024, FullLen - (Text-Start))); ssize_t Chars = ds.CharAt(Flow->X()); bool Wrap = false; if (Text[Chars]) { // Word wrap // Seek back to the nearest break opportunity ssize_t n = Chars; while (n > 0 && !StrchrW(WhiteW, Text[n])) n--; if (n == 0) { if (Flow->x1 == Flow->cx) { // Already started from the margin and it's too long to // fit across the entire page, just let it hang off the right edge. // Seek to the end of the word for (Tr->Len = Chars; Text[Tr->Len] && !StrchrW(WhiteW, Text[Tr->Len]); Tr->Len++) ; // Wrap... if (*Text == ' ') Text++; } else { // Not at the start of the margin Flow->FinishLine(Align); goto Restart; } } else { Tr->Len = n; LgiAssert(Tr->Len > 0); Wrap = true; } } else { // Fits.. Tr->Len = Chars; LgiAssert(Tr->Len > 0); } GDisplayString ds2(Font, Tr->Text, Tr->Len); Tr->x2 = ds2.X(); Tr->y2 = LineHeight > 0 ? LineHeight - 1 : 0; if (Wrap) { Flow->cx = Flow->x1; Flow->y1 += Tr->y2 + 1; Tr->x2 = Flow->x2 - Tag->RelX(); } else { Tr->x2 += Tr->x1 - 1; Flow->cx = Tr->x2 + 1; } Tr->y2 += Tr->y1; Flow->y2 = MAX(Flow->y2, Tr->y2 + 1); Add(Tr); Flow->Insert(Tr); Text += Tr->Len; if (Wrap) { while (*Text == ' ') Text++; } Tag->Size.x = MAX(Tag->Size.x, Tr->x2 + 1); Tag->Size.y = MAX(Tag->Size.y, Tr->y2 + 1); Flow->MAX.x = MAX(Flow->MAX.x, Tr->x2); Flow->MAX.y = MAX(Flow->MAX.y, Tr->y2); if (Tr->Len == 0) break; } } char16 htoi(char16 c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; LgiAssert(0); return 0; } bool GTag::Serialize(GXmlTag *t, bool Write) { GRect pos; if (Write) { // Obj -> Tag if (Tag) t->SetAttr("tag", Tag); pos.ZOff(Size.x, Size.y); pos.Offset(Pos.x, Pos.y); t->SetAttr("pos", pos.GetStr()); t->SetAttr("tagid", TagId); if (Txt) { GStringPipe p(256); for (char16 *c = Txt; *c; c++) { if (*c > ' ' && *c < 127 && !strchr("%<>\'\"", *c)) p.Print("%c", (char)*c); else p.Print("%%%.4x", *c); } GAutoString Tmp(p.NewStr()); t->SetContent(Tmp); } if (Props.Length()) { GAutoString CssStyles = ToString(); LgiAssert(!strchr(CssStyles, '\"')); t->SetAttr("style", CssStyles); } if (Html->Cursor == this) { LgiAssert(Cursor >= 0); t->SetAttr("cursor", (int64)Cursor); } else LgiAssert(Cursor < 0); if (Html->Selection == this) { LgiAssert(Selection >= 0); t->SetAttr("selection", (int64)Selection); } else LgiAssert(Selection < 0); for (unsigned i=0; iInsertTag(child); if (!tag->Serialize(child, Write)) { return false; } } } else { // Tag -> Obj Tag.Reset(NewStr(t->GetAttr("tag"))); TagId = (HtmlTag) t->GetAsInt("tagid"); pos.SetStr(t->GetAttr("pos")); if (pos.Valid()) { Pos.x = pos.x1; Pos.y = pos.y1; Size.x = pos.x2; Size.y = pos.y2; } if (ValidStr(t->GetContent())) { GStringPipe p(256); char *c = t->GetContent(); SkipWhiteSpace(c); for (; *c && *c > ' '; c++) { char16 ch; if (*c == '%') { ch = 0; for (int i=0; i<4 && *c; i++) { ch <<= 4; ch |= htoi(*++c); } } else ch = *c; p.Write(&ch, sizeof(ch)); } Txt.Reset(p.NewStrW()); } const char *s = t->GetAttr("style"); if (s) Parse(s, ParseRelaxed); s = t->GetAttr("cursor"); if (s) { LgiAssert(Html->Cursor == NULL); Html->Cursor = this; Cursor = atoi(s); LgiAssert(Cursor >= 0); } s = t->GetAttr("selection"); if (s) { LgiAssert(Html->Selection == NULL); Html->Selection = this; Selection = atoi(s); LgiAssert(Selection >= 0); } #ifdef _DEBUG s = t->GetAttr("debug"); if (s && atoi(s) != 0) Debug = true; #endif for (int i=0; iChildren.Length(); i++) { GXmlTag *child = t->Children[i]; if (child->IsTag("e")) { GTag *tag = new GTag(Html, NULL); if (!tag) { LgiAssert(0); return false; } if (!tag->Serialize(child, Write)) { return false; } Attach(tag); } } } return true; } /// This method centers the text in the area given to the tag. Used for inline block elements. void GTag::CenterText() { if (!Parent) return; // Find the size of the text elements. int ContentPx = 0; for (unsigned i=0; iX(); } GFont *f = GetFont(); int ParentPx = ToTag(Parent)->Size.x; int AvailPx = Size.x; // Remove the border and padding from the content area AvailPx -= BorderLeft().ToPx(ParentPx, f); AvailPx -= BorderRight().ToPx(ParentPx, f); AvailPx -= PaddingLeft().ToPx(ParentPx, f); AvailPx -= PaddingRight().ToPx(ParentPx, f); if (AvailPx > ContentPx) { // Now offset all the regions to the right int OffPx = (AvailPx - ContentPx) >> 1; for (unsigned i=0; iOffset(OffPx, 0); } } } void GTag::OnFlow(GFlowRegion *Flow, uint16 Depth) { if (Depth >= MAX_RECURSION_DEPTH) return; DisplayType Disp = Display(); if (Disp == DispNone) return; - if (Debug) - { - int asd=0; - } - GFont *f = GetFont(); GFlowRegion Local(*Flow); bool Restart = true; int BlockFlowWidth = 0; const char *ImgAltText = NULL; Size.x = 0; Size.y = 0; GCssTools Tools(this, f); GRect rc(Flow->X(), Html->Y()); PadPx = Tools.GetPadding(rc); switch (TagId) { default: break; case TAG_BODY: { Flow->InBody++; break; } case TAG_IFRAME: { GFlowRegion Temp = *Flow; Flow->EndBlock(GetAlign(true)); Flow->Indent(this, MarginLeft(), MarginTop(), MarginRight(), MarginBottom(), true); // Flow children for (unsigned i=0; iOnFlow(&Temp, Depth + 1); if (TagId == TAG_TR) { Temp.x2 -= MIN(t->Size.x, Temp.X()); } } Flow->Outdent(this, MarginLeft(), MarginTop(), MarginRight(), MarginBottom(), true); BoundParents(); return; break; } case TAG_TR: { Size.x = Flow->X(); break; } case TAG_IMG: { Size.x = Size.y = 0; GCss::Len w = Width(); GCss::Len h = Height(); // GCss::Len MinX = MinWidth(); // GCss::Len MaxX = MaxWidth(); GCss::Len MinY = MinHeight(); GCss::Len MaxY = MaxHeight(); GAutoPtr a; int ImgX, ImgY; if (Image) { ImgX = Image->X(); ImgY = Image->Y(); } else if (Get("alt", ImgAltText) && ValidStr(ImgAltText)) { GDisplayString a(f, ImgAltText); ImgX = a.X() + 4; ImgY = a.Y() + 4; } else { ImgX = DefaultImgSize; ImgY = DefaultImgSize; } double AspectRatio = ImgY != 0 ? (double)ImgX / ImgY : 1.0; bool XLimit = false, YLimit = false; double Scale = 1.0; if (w.IsValid() && w.Type != LenAuto) { Size.x = Flow->ResolveX(w, this, false); XLimit = true; } else { int Fx = Flow->x2 - Flow->x1 + 1; if (ImgX > Fx) { Size.x = Fx; // * 0.8; if (Image) Scale = (double) Fx / ImgX; } else { Size.x = ImgX; } } XLimit |= Flow->LimitX(Size.x, MinWidth(), MaxWidth(), f); if (h.IsValid() && h.Type != LenAuto) { Size.y = Flow->ResolveY(h, this, false); YLimit = true; } else { Size.y = (int) (ImgY * Scale); } YLimit |= Flow->LimitY(Size.y, MinHeight(), MaxHeight(), f); if ( (XLimit ^ YLimit) && Image ) { if (XLimit) { Size.y = (int) ceil((double)Size.x / AspectRatio); } else { Size.x = (int) ceil((double)Size.y * AspectRatio); } } if (MinY.IsValid()) { int Px = Flow->ResolveY(MinY, this, false); if (Size.y < Px) Size.y = Px; } if (MaxY.IsValid()) { int Px = Flow->ResolveY(MaxY, this, false); if (Size.y > Px) Size.y = Px; } if (Disp == DispInline || Disp == DispInlineBlock) { Restart = false; if (Flow->cx > Flow->x1 && Size.x > Flow->X()) { Flow->FinishLine(GetAlign(true)); } Pos.y = Flow->y1; Flow->y2 = MAX(Flow->y1, Pos.y + Size.y - 1); GCss::LengthType a = GetAlign(true); switch (a) { case AlignCenter: { int Fx = Flow->x2 - Flow->x1; Pos.x = Flow->x1 + ((Fx - Size.x) / 2); break; } case AlignRight: { Pos.x = Flow->x2 - Size.x; break; } default: { Pos.x = Flow->cx; break; } } } break; } case TAG_HR: { Flow->FinishLine(GetAlign(true)); Pos.x = Flow->x1; Pos.y = Flow->y1 + 7; Size.x = Flow->X(); Size.y = 2; Flow->cx ++; Flow->y2 += 16; Flow->FinishLine(GetAlign(true)); return; break; } case TAG_TABLE: { Flow->EndBlock(GetAlign(true)); GCss::Len left = GetCssLen(MarginLeft, Margin); GCss::Len top = GetCssLen(MarginTop, Margin); GCss::Len right = GetCssLen(MarginRight, Margin); GCss::Len bottom = GetCssLen(MarginBottom, Margin); Flow->Indent(this, left, top, right, bottom, true); LayoutTable(Flow, Depth + 1); Flow->y1 += Size.y; Flow->y2 = Flow->y1; Flow->cx = Flow->x1; Flow->my = 0; Flow->MAX.y = MAX(Flow->MAX.y, Flow->y2); Flow->Outdent(this, left, top, right, bottom, true); BoundParents(); return; } } if (Disp == DispBlock || Disp == DispInlineBlock) { // This is a block level element, so end the previous non-block elements if (Disp == DispBlock) { Flow->EndBlock(GetAlign(true)); } /* if (Debug) LgiTrace("Before %s\n", Flow->ToString().Get()); */ BlockFlowWidth = Flow->X(); // Indent the margin... GCss::Len left = GetCssLen(MarginLeft, Margin); GCss::Len top = GetCssLen(MarginTop, Margin); GCss::Len right = GetCssLen(MarginRight, Margin); GCss::Len bottom = GetCssLen(MarginBottom, Margin); Flow->Indent(this, left, top, right, bottom, true); // Set the width if any if (Disp == DispBlock) { GCss::Len Wid = Width(); if (!IsTableCell(TagId) && Wid.IsValid()) Size.x = Flow->ResolveX(Wid, this, false); else if (TagId != TAG_IMG) { if (Flow->Inline) Size.x = 0; // block inside inline-block default to fit the content else Size.x = Flow->X(); } if (MaxWidth().IsValid()) { int Px = Flow->ResolveX(MaxWidth(), this, false); if (Size.x > Px) Size.x = Px; } Pos.x = Flow->x1; } else { Size.x = 0; // Child content should expand this to fit Pos.x = Flow->cx; } Pos.y = Flow->y1; Flow->y1 -= Pos.y; Flow->y2 -= Pos.y; if (Disp == DispBlock) { Flow->x1 -= Pos.x; Flow->x2 = Flow->x1 + Size.x; Flow->cx -= Pos.x; Flow->Indent(this, GCss::BorderLeft(), GCss::BorderTop(), GCss::BorderRight(), GCss::BorderBottom(), false); Flow->Indent(PadPx, false); } else { Flow->x2 = Flow->X(); Flow->x1 = Flow->ResolveX(BorderLeft(), this, true) + Flow->ResolveX(PaddingLeft(), this, true); Flow->cx = Flow->x1; Flow->y1 += Flow->ResolveY(BorderTop(), this, true) + Flow->ResolveY(PaddingTop(), this, true); Flow->y2 = Flow->y1; if (!IsTableTag()) Flow->Inline++; } } else { Flow->Indent(PadPx, false); } if (f) { // Clear the previous text layout... TextPos.DeleteObjects(); switch (TagId) { default: break; case TAG_LI: { // Insert the list marker if (!PreText()) { GCss::ListStyleTypes s = Parent->ListStyleType(); if (s == ListInherit) { if (Parent->TagId == TAG_OL) s = ListDecimal; else if (Parent->TagId == TAG_UL) s = ListDisc; } switch (s) { default: break; case ListDecimal: { ssize_t Index = Parent->Children.IndexOf(this); char Txt[32]; sprintf_s(Txt, sizeof(Txt), "%i. ", (int)(Index + 1)); PreText(Utf8ToWide(Txt)); break; } case ListDisc: { PreText(NewStrW(GHtmlListItem)); break; } } } if (PreText()) TextPos.FlowText(this, Flow, f, f->GetHeight(), PreText(), AlignLeft); break; } case TAG_IMG: { if (Disp == DispBlock) { Flow->cx += Size.x; Flow->y2 += Size.y; } break; } } if (Text() && Flow->InBody) { // Setup the line height cache if (LineHeightCache < 0) { GCss::PropMap Map; GCss Final; Map.Add(PropLineHeight, new GCss::PropArray); for (GTag *t = this; t; t = t->TagId == TAG_TABLE ? NULL : ToTag(t->Parent)) { if (!Final.InheritCollect(*t, Map)) break; } Final.InheritResolve(Map); Map.DeleteObjects(); GCss::Len CssLineHeight = Final.LineHeight(); if (f) { int FontPx = FontPxHeight(f); if (!CssLineHeight.IsValid() || CssLineHeight.Type == GCss::LenAuto || CssLineHeight.Type == GCss::LenNormal) { LineHeightCache = f->GetHeight(); } else { LineHeightCache = CssLineHeight.ToPx(FontPx, f); } } } // Flow in the rest of the text... char16 *Txt = Text(); GCss::LengthType Align = GetAlign(true); TextPos.FlowText(this, Flow, f, LineHeightCache, Txt, Align); } } // Flow children for (unsigned i=0; iPosition()) { case PosStatic: case PosAbsolute: case PosFixed: { GFlowRegion old = *Flow; t->OnFlow(Flow, Depth + 1); // Try and reset the flow to how it was before... Flow->x1 = old.x1; Flow->x2 = old.x2; Flow->cx = old.cx; Flow->y1 = old.y1; Flow->y2 = old.y2; Flow->MAX.x = MAX(Flow->MAX.x, old.MAX.x); Flow->MAX.y = MAX(Flow->MAX.y, old.MAX.y); break; } default: { t->OnFlow(Flow, Depth + 1); break; } } if (TagId == TAG_TR) { Flow->x2 -= MIN(t->Size.x, Flow->X()); } } GCss::LengthType XAlign = GetAlign(true); if (Disp == DispBlock || Disp == DispInlineBlock) { GCss::Len Ht = Height(); GCss::Len MaxHt = MaxHeight(); // I dunno, there should be a better way... :-( if (MarginLeft().Type == LenAuto && MarginRight().Type == LenAuto) { XAlign = GCss::AlignCenter; } bool AcceptHt = !IsTableCell(TagId) && Ht.Type != LenPercent; if (AcceptHt) { if (Ht.IsValid()) { int HtPx = Flow->ResolveY(Ht, this, false); if (HtPx > Flow->y2) Flow->y2 = HtPx; } if (MaxHt.IsValid()) { int MaxHtPx = Flow->ResolveY(MaxHt, this, false); if (MaxHtPx < Flow->y2) Flow->y2 = MaxHtPx; } } if (Disp == DispBlock) { Flow->EndBlock(XAlign); int OldFlowSize = Flow->x2 - Flow->x1 + 1; Flow->Outdent(this, PaddingLeft(), PaddingTop(), PaddingRight(), PaddingBottom(), false); Flow->Outdent(this, GCss::BorderLeft(), GCss::BorderTop(), GCss::BorderRight(), GCss::BorderBottom(), false); Size.y = Flow->y2 > 0 ? Flow->y2 : 0; Flow->Outdent(this, MarginLeft(), MarginTop(), MarginRight(), MarginBottom(), true); int NewFlowSize = Flow->x2 - Flow->x1 + 1; int Diff = NewFlowSize - OldFlowSize; if (Diff) Flow->MAX.x += Diff; Flow->y1 = Flow->y2; Flow->x2 = Flow->x1 + BlockFlowWidth; } else { GCss::Len Wid = Width(); int WidPx = Wid.IsValid() ? Flow->ResolveX(Wid, this, true) : 0; Size.x = MAX(WidPx, Size.x); Size.x += Flow->ResolveX(PaddingRight(), this, true); Size.x += Flow->ResolveX(BorderRight(), this, true); int MarginR = Flow->ResolveX(MarginRight(), this, true); int MarginB = Flow->ResolveX(MarginBottom(), this, true); Flow->x1 = Local.x1 - Pos.x; Flow->cx = Local.cx + Size.x + MarginR - Pos.x; Flow->x2 = Local.x2 - Pos.x; if (Height().IsValid()) { Size.y = Flow->ResolveY(Height(), this, false); Flow->y2 = MAX(Flow->y1 + Size.y + MarginB - 1, Flow->y2); } else { Flow->y2 += Flow->ResolveX(PaddingBottom(), this, true); Flow->y2 += Flow->ResolveX(BorderBottom(), this, true); Size.y = Flow->y2; } Flow->y1 = Local.y1; Flow->y2 = MAX(Local.y2, Local.y1 + Size.y); if (!IsTableTag()) Flow->Inline--; CenterText(); } // Can't do alignment here because pos is used to // restart the parents flow region... } else { Flow->Outdent(PadPx, false); switch (TagId) { default: break; case TAG_SELECT: case TAG_INPUT: { if (Html->InThread() && Ctrl) { GRect r = Ctrl->GetPos(); if (Width().IsValid()) Size.x = Flow->ResolveX(Width(), this, false); else Size.x = r.X(); if (Height().IsValid()) Size.y = Flow->ResolveY(Height(), this, false); else Size.y = r.Y(); if (Html->IsAttached() && !Ctrl->IsAttached()) Ctrl->Attach(Html); } Flow->cx += Size.x; Flow->y2 = MAX(Flow->y2, Flow->y1 + Size.y - 1); break; } case TAG_IMG: { Flow->cx += Size.x; Flow->y2 = MAX(Flow->y2, Flow->y1 + Size.y - 1); break; } case TAG_BR: { int OldFlowY2 = Flow->y2; Flow->FinishLine(GetAlign(true)); Size.y = Flow->y2 - OldFlowY2; Flow->y2 = MAX(Flow->y2, Flow->y1 + Size.y - 1); break; } case TAG_CENTER: { int Px = Flow->X(); for (GHtmlElement **e = NULL; Children.Iterate(e); ) { GTag *t = ToTag(*e); if (t->IsBlock()) { if (t->Size.x < Px) { t->Pos.x = (Px - t->Size.x) >> 1; } } } break; } } } BoundParents(); if (Restart) { Flow->x1 += Pos.x; Flow->x2 += Pos.x; Flow->cx += Pos.x; Flow->y1 += Pos.y; Flow->y2 += Pos.y; Flow->MAX.y = MAX(Flow->MAX.y, Flow->y2); } if (Disp == DispBlock || Disp == DispInlineBlock) { if (XAlign == GCss::AlignCenter) { int OffX = (Flow->x2 - Flow->x1 - Size.x) >> 1; if (OffX > 0) { Pos.x += OffX; } } else if (XAlign == GCss::AlignRight) { int OffX = Flow->x2 - Flow->x1 - Size.x; if (OffX > 0) { Pos.x += OffX; } } } if (TagId == TAG_BODY && Flow->InBody > 0) { Flow->InBody--; } } bool GTag::PeekTag(char *s, char *tag) { bool Status = false; if (s && tag) { if (*s == '<') { char *t = 0; Html->ParseName(++s, &t); if (t) { Status = _stricmp(t, tag) == 0; } DeleteArray(t); } } return Status; } GTag *GTag::GetTable() { GTag *t = 0; for (t=ToTag(Parent); t && t->TagId != TAG_TABLE; t = ToTag(t->Parent)) ; return t; } void GTag::BoundParents() { if (!Parent) return; GTag *np; for (GTag *n=this; n; n = np) { np = ToTag(n->Parent); if (!np || n->Parent->TagId == TAG_IFRAME) break; np->Size.x = MAX(np->Size.x, n->Pos.x + n->Size.x); np->Size.y = MAX(np->Size.y, n->Pos.y + n->Size.y); } } struct DrawBorder { GSurface *pDC; uint32_t LineStyle; uint32_t LineReset; uint32_t OldStyle; DrawBorder(GSurface *pdc, GCss::BorderDef &d) { LineStyle = 0xffffffff; LineReset = 0x80000000; if (d.Style == GCss::BorderDotted) { switch ((int)d.Value) { case 2: { LineStyle = 0xcccccccc; break; } case 3: { LineStyle = 0xe38e38; LineReset = 0x800000; break; } case 4: { LineStyle = 0xf0f0f0f0; break; } case 5: { LineStyle = 0xf83e0; LineReset = 0x80000; break; } case 6: { LineStyle = 0xfc0fc0; LineReset = 0x800000; break; } case 7: { LineStyle = 0xfe03f80; LineReset = 0x8000000; break; } case 8: { LineStyle = 0xff00ff00; break; } case 9: { LineStyle = 0x3fe00; LineReset = 0x20000; break; } default: { LineStyle = 0xaaaaaaaa; break; } } } pDC = pdc; OldStyle = pDC->LineStyle(); } ~DrawBorder() { pDC->LineStyle(OldStyle); } }; void GTag::GetInlineRegion(GRegion &rgn) { if (TagId == TAG_IMG) { GRect rc(0, 0, Size.x-1, Size.y-1); rc.Offset(Pos.x, Pos.y); rgn.Union(&rc); } else { for (unsigned i=0; iGetInlineRegion(rgn); } } class CornersImg : public GMemDC { public: int Px, Px2; CornersImg( float RadPx, GRect *BorderPx, GCss::BorderDef **defs, GColour &Back, bool DrawBackground) { Px = 0; Px2 = 0; //Radius.Type != GCss::LenInherit && if (RadPx > 0.0f) { Px = (int)ceil(RadPx); Px2 = Px << 1; if (Create(Px2, Px2, System32BitColourSpace)) { #if 1 Colour(0, 32); #else Colour(GColour(255, 0, 255)); #endif Rectangle(); GPointF ctr(Px, Px); GPointF LeftPt(0.0, Px); GPointF TopPt(Px, 0.0); GPointF RightPt(X(), Px); GPointF BottomPt(Px, Y()); int x_px[4] = {BorderPx->x1, BorderPx->x2, BorderPx->x2, BorderPx->x1}; int y_px[4] = {BorderPx->y1, BorderPx->y1, BorderPx->y2, BorderPx->y2}; GPointF *pts[4] = {&LeftPt, &TopPt, &RightPt, &BottomPt}; // Draw border parts.. for (int i=0; i<4; i++) { int k = (i + 1) % 4; // Setup the stops GBlendStop stops[2] = { {0.0, 0}, {1.0, 0} }; uint32_t iColour = defs[i]->Color.IsValid() ? defs[i]->Color.Rgb32 : Back.c32(); uint32_t kColour = defs[k]->Color.IsValid() ? defs[k]->Color.Rgb32 : Back.c32(); if (defs[i]->IsValid() && defs[k]->IsValid()) { stops[0].c32 = iColour; stops[1].c32 = kColour; } else if (defs[i]->IsValid()) { stops[0].c32 = stops[1].c32 = iColour; } else { stops[0].c32 = stops[1].c32 = kColour; } // Create a brush GLinearBlendBrush br ( *pts[i], *pts[k], 2, stops ); // Setup the clip GRect clip( (int)MIN(pts[i]->x, pts[k]->x), (int)MIN(pts[i]->y, pts[k]->y), (int)MAX(pts[i]->x, pts[k]->x)-1, (int)MAX(pts[i]->y, pts[k]->y)-1); ClipRgn(&clip); // Draw the arc... GPath p; p.Circle(ctr, Px); if (defs[i]->IsValid() || defs[k]->IsValid()) p.Fill(this, br); // Fill the background p.Empty(); p.Ellipse(ctr, Px-x_px[i], Px-y_px[i]); if (DrawBackground) { GSolidBrush br(Back); p.Fill(this, br); } else { GEraseBrush br; p.Fill(this, br); } ClipRgn(NULL); } #ifdef MAC ConvertPreMulAlpha(true); #endif #if 0 static int count = 0; GString file; file.Printf("c:\\temp\\img-%i.bmp", ++count); GdcD->Save(file, Corners); #endif } } } }; void GTag::PaintBorderAndBackground(GSurface *pDC, GColour &Back, GRect *BorderPx) { GArray r; GRect BorderPxRc; bool DrawBackground = !Back.IsTransparent(); #ifdef _DEBUG if (Debug) { //int asd=0; } #endif if (!BorderPx) BorderPx = &BorderPxRc; BorderPx->ZOff(0, 0); // Get all the border info and work out the pixel sizes. GFont *f = GetFont(); BorderDef Left = BorderLeft(); BorderPx->x1 = Left.ToPx(Size.x, f); BorderDef Top = BorderTop(); BorderPx->y1 = Top.ToPx(Size.x, f); BorderDef Right = BorderRight(); BorderPx->x2 = Right.ToPx(Size.x, f); BorderDef Bottom = BorderBottom(); BorderPx->y2 = Bottom.ToPx(Size.x, f); GCss::BorderDef *defs[4] = {&Left, &Top, &Right, &Bottom}; // Work out the rectangles switch (Display()) { case DispInlineBlock: case DispBlock: { r[0].ZOff(Size.x-1, Size.y-1); break; } case DispInline: { GRegion rgn; GetInlineRegion(rgn); // rgn.Simplify(false); for (int i=0; ix1 + PadPx.x1; rc.y1 -= BorderPx->y1 + PadPx.y1; rc.x2 += BorderPx->x2 + PadPx.x2; rc.y2 += BorderPx->y2 + PadPx.y2; } r.New() = rc; } break; } default: return; } // If we are drawing rounded corners, draw them into a memory context GAutoPtr Corners; int Px = 0, Px2 = 0; GCss::Len Radius = BorderRadius(); float RadPx = Radius.Type == GCss::LenPx ? Radius.Value : Radius.ToPx(Size.x, GetFont()); bool HasRadius = Radius.Type != GCss::LenInherit && RadPx > 0.0f; // Loop over the rectangles and draw everything int Op = pDC->Op(GDC_ALPHA); for (unsigned i=0; i rc.Y()) { Px = rc.Y() / 2; Px2 = Px << 1; } if (!Corners || Corners->Px2 != Px2) { Corners.Reset(new CornersImg((float)Px, BorderPx, defs, Back, DrawBackground)); } // top left GRect r(0, 0, Px-1, Px-1); pDC->Blt(rc.x1, rc.y1, Corners, &r); // top right r.Set(Px, 0, Corners->X()-1, Px-1); pDC->Blt(rc.x2-Px+1, rc.y1, Corners, &r); // bottom left r.Set(0, Px, Px-1, Corners->Y()-1); pDC->Blt(rc.x1, rc.y2-Px+1, Corners, &r); // bottom right r.Set(Px, Px, Corners->X()-1, Corners->Y()-1); pDC->Blt(rc.x2-Px+1, rc.y2-Px+1, Corners, &r); #if 1 pDC->Colour(Back); pDC->Rectangle(rc.x1+Px, rc.y1, rc.x2-Px, rc.y2); pDC->Rectangle(rc.x1, rc.y1+Px, rc.x1+Px-1, rc.y2-Px); pDC->Rectangle(rc.x2-Px+1, rc.y1+Px, rc.x2, rc.y2-Px); #else pDC->Colour(GColour(255, 0, 0, 0x80)); pDC->Rectangle(rc.x1+Px, rc.y1, rc.x2-Px, rc.y2); pDC->Colour(GColour(0, 255, 0, 0x80)); pDC->Rectangle(rc.x1, rc.y1+Px, rc.x1+Px-1, rc.y2-Px); pDC->Colour(GColour(0, 0, 255, 0x80)); pDC->Rectangle(rc.x2-Px+1, rc.y1+Px, rc.x2, rc.y2-Px); #endif } else if (DrawBackground) { pDC->Colour(Back); pDC->Rectangle(&rc); /* if (Debug) { pDC->Colour(GColour(255, 0, 0)); pDC->Box(&rc); } */ } GCss::BorderDef *b; if ((b = &Left)->IsValid()) { pDC->Colour(b->Color.Rgb32, 32); DrawBorder db(pDC, *b); for (int i=0; iValue; i++) { pDC->LineStyle(db.LineStyle, db.LineReset); pDC->Line(rc.x1 + i, rc.y1+Px, rc.x1+i, rc.y2-Px); } } if ((b = &Top)->IsValid()) { pDC->Colour(b->Color.Rgb32, 32); DrawBorder db(pDC, *b); for (int i=0; iValue; i++) { pDC->LineStyle(db.LineStyle, db.LineReset); pDC->Line(rc.x1+Px, rc.y1+i, rc.x2-Px, rc.y1+i); } } if ((b = &Right)->IsValid()) { pDC->Colour(b->Color.Rgb32, 32); DrawBorder db(pDC, *b); for (int i=0; iValue; i++) { pDC->LineStyle(db.LineStyle, db.LineReset); pDC->Line(rc.x2-i, rc.y1+Px, rc.x2-i, rc.y2-Px); } } if ((b = &Bottom)->IsValid()) { pDC->Colour(b->Color.Rgb32, 32); DrawBorder db(pDC, *b); for (int i=0; iValue; i++) { pDC->LineStyle(db.LineStyle, db.LineReset); pDC->Line(rc.x1+Px, rc.y2-i, rc.x2-Px, rc.y2-i); } } } pDC->Op(Op); } static void FillRectWithImage(GSurface *pDC, GRect *r, GSurface *Image, GCss::RepeatType Repeat) { int Px = 0, Py = 0; int Old = pDC->Op(GDC_ALPHA); if (!Image) return; switch (Repeat) { default: case GCss::RepeatBoth: { for (int y=0; yY(); y += Image->Y()) { for (int x=0; xX(); x += Image->X()) { pDC->Blt(Px + x, Py + y, Image); } } break; } case GCss::RepeatX: { for (int x=0; xX(); x += Image->X()) { pDC->Blt(Px + x, Py, Image); } break; } case GCss::RepeatY: { for (int y=0; yY(); y += Image->Y()) { pDC->Blt(Px, Py + y, Image); } break; } case GCss::RepeatNone: { pDC->Blt(Px, Py, Image); break; } } pDC->Op(Old); } void GTag::OnPaint(GSurface *pDC, bool &InSelection, uint16 Depth) { if (Depth >= MAX_RECURSION_DEPTH || Display() == DispNone) return; int Px, Py; pDC->GetOrigin(Px, Py); switch (TagId) { case TAG_INPUT: case TAG_SELECT: { if (Ctrl) { int Sx = 0, Sy = 0; int LineY = GetFont()->GetHeight(); Html->GetScrollPos(Sx, Sy); Sx *= LineY; Sy *= LineY; GRect r(0, 0, Size.x-1, Size.y-1), Px; GColour back = _Colour(false); PaintBorderAndBackground(pDC, back, &Px); if (!dynamic_cast(Ctrl)) { r.x1 += Px.x1; r.y1 += Px.y1; r.x2 -= Px.x2; r.y2 -= Px.y2; } r.Offset(AbsX() - Sx, AbsY() - Sy); Ctrl->SetPos(r); } if (TagId == TAG_SELECT) return; break; } case TAG_BODY: { COLOUR b = GetBack(); if (b != GT_TRANSPARENT) { pDC->Colour(b, 32); pDC->Rectangle(Pos.x, Pos.y, Pos.x+Size.x, Pos.y+Size.y); } if (Image) { GRect r; r.ZOff(Size.x-1, Size.y-1); FillRectWithImage(pDC, &r, Image, BackgroundRepeat()); } break; } case TAG_HEAD: { // Nothing under here to draw. return; } case TAG_HR: { pDC->Colour(LC_MED, 24); pDC->Rectangle(0, 0, Size.x - 1, Size.y - 1); break; } case TAG_TR: case TAG_TBODY: case TAG_META: { // Draws nothing... break; } case TAG_IMG: { GRect Clip(0, 0, Size.x-1, Size.y-1); pDC->ClipRgn(&Clip); if (Image) { #if ENABLE_IMAGE_RESIZING if ( !ImageResized && ( Size.x != Image->X() || Size.y != Image->Y() ) ) { ImageResized = true; GColourSpace Cs = Image->GetColourSpace(); if (Cs == CsIndex8 && Image->AlphaDC()) Cs = System32BitColourSpace; GAutoPtr r(new GMemDC(Size.x, Size.y, Cs)); if (r) { if (Cs == CsIndex8) r->Palette(new GPalette(Image->Palette())); ResampleDC(r, Image); Image = r; } } #endif int Old = pDC->Op(GDC_ALPHA); pDC->Blt(0, 0, Image); pDC->Op(Old); } else if (Size.x > 1 && Size.y > 1) { GRect b(0, 0, Size.x-1, Size.y-1); GColour Fill(GdcMixColour(LC_MED, LC_LIGHT, 0.2f), 24); GColour Border(LC_MED, 24); // Border pDC->Colour(Border); pDC->Box(&b); b.Size(1, 1); pDC->Box(&b); b.Size(1, 1); pDC->Colour(Fill); pDC->Rectangle(&b); const char *Alt; GColour Red(GdcMixColour(Rgb24(255, 0, 0), Fill.c24(), 0.3f), 24); if (Get("alt", Alt) && ValidStr(Alt)) { GDisplayString Ds(Html->GetFont(), Alt); Html->GetFont()->Colour(Red, Fill); Ds.Draw(pDC, 2, 2, &b); } else if (Size.x >= 16 && Size.y >= 16) { // Red 'x' int Cx = b.x1 + (b.X()/2); int Cy = b.y1 + (b.Y()/2); GRect c(Cx-4, Cy-4, Cx+4, Cy+4); pDC->Colour(Red); pDC->Line(c.x1, c.y1, c.x2, c.y2); pDC->Line(c.x1, c.y2, c.x2, c.y1); pDC->Line(c.x1, c.y1 + 1, c.x2 - 1, c.y2); pDC->Line(c.x1 + 1, c.y1, c.x2, c.y2 - 1); pDC->Line(c.x1 + 1, c.y2, c.x2, c.y1 + 1); pDC->Line(c.x1, c.y2 - 1, c.x2 - 1, c.y1); } } pDC->ClipRgn(0); break; } default: { GColour fore = _Colour(true); GColour back = _Colour(false); if (Display() == DispBlock && Html->Environment) { GCss::ImageDef Img = BackgroundImage(); if (Img.Img) { GRect Clip(0, 0, Size.x-1, Size.y-1); pDC->ClipRgn(&Clip); FillRectWithImage(pDC, &Clip, Img.Img, BackgroundRepeat()); pDC->ClipRgn(NULL); back.Empty(); } } PaintBorderAndBackground(pDC, back, NULL); GFont *f = GetFont(); #if DEBUG_TEXT_AREA bool IsEditor = Html ? !Html->GetReadOnly() : false; #else bool IsEditor = false; #endif if (f && TextPos.Length()) { // This is the non-display part of the font bounding box int LeadingPx = (int)(f->Leading() + 0.5); // This is the displayable part of the font int FontPx = f->GetHeight() - LeadingPx; // This is the pixel height we're aiming to fill int EffectiveLineHt = LineHeightCache >= 0 ? MAX(FontPx, LineHeightCache) : FontPx; // This gets added to the y coord of each peice of text int LineHtOff = ((EffectiveLineHt - FontPx + 1) >> 1) - LeadingPx; #define FontColour(InSelection) \ f->Transparent(!InSelection && !IsEditor); \ if (InSelection) \ f->Colour(LC_FOCUS_SEL_FORE, LC_FOCUS_SEL_BACK); \ else \ { \ GColour bk(back.IsTransparent() ? GColour(LC_WORKSPACE, 24) : back); \ GColour fr(fore.IsTransparent() ? GColour(DefaultTextColour, 32) : fore); \ if (IsEditor) \ bk = bk.Mix(GColour(0, 0, 0), 0.05f); \ f->Colour(fr, bk); \ } if (Html->HasSelection() && (Selection >= 0 || Cursor >= 0) && Selection != Cursor) { ssize_t Min = -1; ssize_t Max = -1; ssize_t Base = GetTextStart(); if (Cursor >= 0 && Selection >= 0) { Min = MIN(Cursor, Selection) + Base; Max = MAX(Cursor, Selection) + Base; } else if (InSelection) { Max = MAX(Cursor, Selection) + Base; } else { Min = MAX(Cursor, Selection) + Base; } GRect CursorPos; CursorPos.ZOff(-1, -1); for (unsigned i=0; iText - Text(); ssize_t Done = 0; int x = Tr->x1; if (Tr->Len == 0) { // Is this a selection edge point? if (!InSelection && Min == 0) { InSelection = !InSelection; } else if (InSelection && Max == 0) { InSelection = !InSelection; } if (Cursor >= 0) { // Is this the cursor, then draw it and save it's position if (Cursor == Start + Done - Base) { Html->d->CursorPos.Set(x, Tr->y1 + LineHtOff, x + 1, Tr->y2 - LineHtOff); if (Html->d->CursorPos.x1 > Tr->x2) Html->d->CursorPos.Offset(Tr->x2 - Html->d->CursorPos.x1, 0); CursorPos = Html->d->CursorPos; Html->d->CursorPos.Offset(AbsX(), AbsY()); } } break; } while (Done < Tr->Len) { ssize_t c = Tr->Len - Done; FontColour(InSelection); // Is this a selection edge point? if ( !InSelection && Min - Start >= Done && Min - Start < Done + Tr->Len) { InSelection = !InSelection; c = Min - Start - Done; } else if ( InSelection && Max - Start >= Done && Max - Start <= Tr->Len) { InSelection = !InSelection; c = Max - Start - Done; } // Draw the text run GDisplayString ds(f, Tr->Text + Done, c); if (IsEditor) { GRect r(x, Tr->y1, x + ds.X() - 1, Tr->y2); ds.Draw(pDC, x, Tr->y1 + LineHtOff, &r); } else { ds.Draw(pDC, x, Tr->y1 + LineHtOff); } x += ds.X(); Done += c; // Is this is end of the tag? if (Tr->Len == Done) { // Is it also a selection edge? if ( !InSelection && Min - Start == Done) { InSelection = !InSelection; } else if ( InSelection && Max - Start == Done) { InSelection = !InSelection; } } if (Cursor >= 0) { // Is this the cursor, then draw it and save it's position if (Cursor == Start + Done - Base) { Html->d->CursorPos.Set(x, Tr->y1 + LineHtOff, x + 1, Tr->y2 - LineHtOff); if (Html->d->CursorPos.x1 > Tr->x2) Html->d->CursorPos.Offset(Tr->x2 - Html->d->CursorPos.x1, 0); CursorPos = Html->d->CursorPos; Html->d->CursorPos.Offset(AbsX(), AbsY()); } } } } if (Html->d->CursorVis && CursorPos.Valid()) { pDC->Colour(LC_TEXT, 24); pDC->Rectangle(&CursorPos); } } else if (Cursor >= 0) { FontColour(InSelection); ssize_t Base = GetTextStart(); for (unsigned i=0; iText - Text()) - Base; LgiAssert(Tr->y2 >= Tr->y1); GDisplayString ds(f, Tr->Text, Tr->Len); ds.Draw(pDC, Tr->x1, Tr->y1 + LineHtOff, IsEditor ? Tr : NULL); if ( ( Tr->Text == PreText() && !ValidStrW(Text()) ) || ( Cursor >= Pos && Cursor <= Pos + Tr->Len ) ) { ssize_t Off = Tr->Text == PreText() ? StrlenW(PreText()) : Cursor - Pos; pDC->Colour(LC_TEXT, 24); GRect c; if (Off) { GDisplayString ds(f, Tr->Text, Off); int x = ds.X(); if (x >= Tr->X()) x = Tr->X()-1; c.Set(Tr->x1 + x, Tr->y1, Tr->x1 + x + 1, Tr->y1 + f->GetHeight()); } else { c.Set(Tr->x1, Tr->y1, Tr->x1 + 1, Tr->y1 + f->GetHeight()); } Html->d->CursorPos = c; if (Html->d->CursorVis) pDC->Rectangle(&c); Html->d->CursorPos.Offset(AbsX(), AbsY()); } } } else { FontColour(InSelection); for (unsigned i=0; iText, Tr->Len); ds.Draw(pDC, Tr->x1, Tr->y1 + LineHtOff, IsEditor ? Tr : NULL); } } } break; } } #if DEBUG_TABLE_LAYOUT && 0 if (IsTableCell(TagId)) { GTag *Tbl = this; while (Tbl->TagId != TAG_TABLE && Tbl->Parent) Tbl = Tbl->Parent; if (Tbl && Tbl->TagId == TAG_TABLE && Tbl->Debug) { pDC->Colour(GColour(255, 0, 0)); pDC->Box(0, 0, Size.x-1, Size.y-1); } } #endif for (unsigned i=0; iSetOrigin(Px - t->Pos.x, Py - t->Pos.y); t->OnPaint(pDC, InSelection, Depth + 1); pDC->SetOrigin(Px, Py); } #if DEBUG_DRAW_TD if (TagId == TAG_TD) { GTag *Tbl = this; while (Tbl && Tbl->TagId != TAG_TABLE) Tbl = ToTag(Tbl->Parent); if (Tbl && Tbl->Debug) { int Ls = pDC->LineStyle(GSurface::LineDot); pDC->Colour(GColour::Blue); pDC->Box(0, 0, Size.x-1, Size.y-1); pDC->LineStyle(Ls); } } #endif } ////////////////////////////////////////////////////////////////////// GHtml::GHtml(int id, int x, int y, int cx, int cy, GDocumentEnv *e) : GDocView(e), ResObject(Res_Custom), GHtmlParser(NULL) { View = this; d = new GHtmlPrivate; SetReadOnly(true); ViewWidth = -1; SetId(id); GRect r(x, y, x+cx, y+cy); SetPos(r); Cursor = 0; Selection = 0; DocumentUid = 0; _New(); } GHtml::~GHtml() { _Delete(); DeleteObj(d); if (JobSem.Lock(_FL)) { JobSem.Jobs.DeleteObjects(); JobSem.Unlock(); } } void GHtml::_New() { d->StyleDirty = false; d->IsLoaded = false; d->Content.x = d->Content.y = 0; d->DeferredLoads = 0; Tag = 0; DocCharSet.Reset(); IsHtml = true; #ifdef DefaultFont GFont *Def = new GFont; if (Def) { if (Def->CreateFromCss(DefaultFont)) SetFont(Def, true); else DeleteObj(Def); } #endif FontCache = new GFontCache(this); SetScrollBars(false, false); } void GHtml::_Delete() { LgiAssert(!d->IsParsing); CssStore.Empty(); CssHref.Empty(); OpenTags.Length(0); Source.Reset(); DeleteObj(Tag); DeleteObj(FontCache); } GFont *GHtml::DefFont() { return GetFont(); } void GHtml::OnAddStyle(const char *MimeType, const char *Styles) { if (Styles) { const char *c = Styles; bool Status = CssStore.Parse(c); if (Status) { d->StyleDirty = true; } #if 0 // def _DEBUG bool LogCss = false; if (!Status) { char p[MAX_PATH]; sprintf_s(p, sizeof(p), "c:\\temp\\css_parse_failure_%i.txt", LgiRand()); GFile f; if (f.Open(p, O_WRITE)) { f.SetSize(0); if (CssStore.Error) f.Print("Error: %s\n\n", CssStore.Error.Get()); f.Write(Styles, strlen(Styles)); f.Close(); } } if (LogCss) { GStringPipe p; CssStore.Dump(p); GAutoString a(p.NewStr()); GFile f; if (f.Open("C:\\temp\\css.txt", O_WRITE)) { f.Write(a, strlen(a)); f.Close(); } } #endif } } void GHtml::ParseDocument(const char *Doc) { if (!Tag) { Tag = new GTag(this, 0); } if (GetCss()) GetCss()->DeleteProp(GCss::PropBackgroundColor); if (Tag) { Tag->TagId = ROOT; OpenTags.Length(0); if (IsHtml) { Parse(Tag, Doc); // Add body tag if not specified... GTag *Html = Tag->GetTagByName("html"); GTag *Body = Tag->GetTagByName("body"); if (!Html && !Body) { if ((Html = new GTag(this, 0))) Html->SetTag("html"); if ((Body = new GTag(this, Html))) Body->SetTag("body"); Html->Attach(Body); if (Tag->Text()) { GTag *Content = new GTag(this, Body); if (Content) { Content->TagId = CONTENT; Content->Text(NewStrW(Tag->Text())); } } while (Tag->Children.Length()) { GTag *t = ToTag(Tag->Children.First()); Body->Attach(t, Body->Children.Length()); } DeleteObj(Tag); Tag = Html; } else if (!Body) { if ((Body = new GTag(this, Html))) Body->SetTag("body"); for (unsigned i=0; iChildren.Length(); i++) { GTag *t = ToTag(Html->Children[i]); if (t->TagId != TAG_HEAD) { Body->Attach(t); i--; } } Html->Attach(Body); } if (Html && Body) { char16 *t = Tag->Text(); if (t) { if (ValidStrW(t)) { GTag *Content = new GTag(this, 0); if (Content) { Content->Text(NewStrW(Tag->Text())); Body->Attach(Content, 0); } } Tag->Text(0); } #if 0 // Enabling this breaks the test file 'gw2.html'. for (GTag *t = Html->Tags.First(); t; ) { if (t->Tag && t->Tag[0] == '!') { Tag->Attach(t, 0); t = Html->Tags.Current(); } else if (t->TagId != TAG_HEAD && t != Body) { if (t->TagId == TAG_HTML) { GTag *c; while ((c = t->Tags.First())) { Html->Attach(c, 0); } t->Detach(); DeleteObj(t); } else { t->Detach(); Body->Attach(t); } t = Html->Tags.Current(); } else { t = Html->Tags.Next(); } } #endif if (Environment) { const char *OnLoad; if (Body->Get("onload", OnLoad)) { Environment->OnExecuteScript(this, (char*)OnLoad); } } } } else { Tag->ParseText(Source); } } ViewWidth = -1; if (Tag) Tag->ResetCaches(); Invalidate(); } bool GHtml::NameW(const char16 *s) { GAutoPtr utf(WideToUtf8(s)); return Name(utf); } char16 *GHtml::NameW() { GBase::Name(Source); return GBase::NameW(); } bool GHtml::Name(const char *s) { int Uid = -1; if (Environment) Uid = Environment->NextUid(); if (Uid < 0) Uid = GetDocumentUid() + 1; SetDocumentUid(Uid); _Delete(); _New(); IsHtml = false; // Detect HTML const char *c = s; while ((c = strchr(c, '<'))) { char *t = 0; c = ParseName((char*) ++c, &t); if (t && GetTagInfo(t)) { DeleteArray(t); IsHtml = true; break; } DeleteArray(t); } // Parse d->IsParsing = true; ParseDocument(s); d->IsParsing = false; if (Tag && d->StyleDirty) { d->StyleDirty = false; Tag->RestyleAll(); } if (d->DeferredLoads == 0) { OnLoad(); } Invalidate(); return true; } char *GHtml::Name() { #if LUIS_DEBUG LgiTrace("%s:%i html(%p).src(%p)='%30.30s'\n", _FL, this, Source, Source); #endif if (!Source && Tag) { GStringPipe s(1024); Tag->CreateSource(s); Source.Reset(s.NewStr()); } return Source; } GMessage::Result GHtml::OnEvent(GMessage *Msg) { switch (MsgCode(Msg)) { case M_COPY: { Copy(); break; } case M_JOBS_LOADED: { bool Update = false; int InitDeferredLoads = d->DeferredLoads; if (JobSem.Lock(_FL)) { for (unsigned i=0; iUserUid == MyUid && j->UserData != NULL) { Html1::GTag *r = static_cast(j->UserData); if (d->DeferredLoads > 0) d->DeferredLoads--; // Check the tag is still in our tree... if (Tag->HasChild(r)) { // Process the returned data... if (j->pDC) { r->SetImage(j->Uri, j->pDC.Release()); ViewWidth = 0; Update = true; } else if (r->TagId == TAG_LINK) { if (!CssHref.Find(j->Uri)) { GStreamI *s = j->GetStream(); if (s) { int Size = (int)s->GetSize(); GAutoString Style(new char[Size+1]); ssize_t rd = s->Read(Style, Size); if (rd > 0) { Style[rd] = 0; CssHref.Add(j->Uri, true); OnAddStyle("text/css", Style); ViewWidth = 0; Update = true; } } } } } else { Html1::GTag *p = ToTag(r->Parent); while (p->Parent) p = ToTag(p->Parent); int asd=0; } } // else it's from another (historical) HTML control, ignore } JobSem.Jobs.DeleteObjects(); JobSem.Unlock(); } if (InitDeferredLoads > 0 && d->DeferredLoads <= 0) { LgiAssert(d->DeferredLoads == 0); d->DeferredLoads = 0; OnLoad(); } if (Update) { OnPosChange(); Invalidate(); } break; } } return GDocView::OnEvent(Msg); } int GHtml::OnNotify(GViewI *c, int f) { switch (c->GetId()) { case IDC_VSCROLL: { int LineY = GetFont()->GetHeight(); if (f == GNotifyScrollBar_Create && VScroll && LineY > 0) { int y = Y(); int p = MAX(y / LineY, 1); int fy = d->Content.y / LineY; VScroll->SetPage(p); VScroll->SetLimits(0, fy); } Invalidate(); break; } default: { GTag *Ctrl = Tag ? Tag->FindCtrlId(c->GetId()) : NULL; if (Ctrl) return Ctrl->OnNotify(f); break; } } return GLayout::OnNotify(c, f); } void GHtml::OnPosChange() { GLayout::OnPosChange(); if (ViewWidth != X()) { Invalidate(); } } GdcPt2 GHtml::Layout(bool ForceLayout) { GRect Client = GetClient(); if (Tag && (ViewWidth != Client.X() || ForceLayout)) { GFlowRegion f(this, Client, false); // Flow text, width is different Tag->OnFlow(&f, 0); ViewWidth = Client.X(); d->Content.x = f.MAX.x + 1; d->Content.y = f.MAX.y + 1; // Set up scroll box bool Sy = f.y2 > Y(); int LineY = GetFont()->GetHeight(); uint64 Now = LgiCurrentTime(); if (Now - d->SetScrollTime > 100) { d->SetScrollTime = Now; SetScrollBars(false, Sy); if (Sy && VScroll && LineY > 0) { int y = Y(); int p = MAX(y / LineY, 1); int fy = f.y2 / LineY; VScroll->SetPage(p); VScroll->SetLimits(0, fy); } } else { // LgiTrace("%s - Dropping SetScroll, loop detected: %i ms\n", GetClass(), (int)(Now - d->SetScrollTime)); } } return d->Content; } void GHtml::OnPaint(GSurface *ScreenDC) { #if LGI_EXCEPTIONS try { #endif #if GHTML_USE_DOUBLE_BUFFER GRect Client = GetClient(); if (!MemDC || (MemDC->X() < Client.X() || MemDC->Y() < Client.Y())) { if (MemDC.Reset(new GMemDC)) { int Sx = Client.X() + 10; int Sy = Client.Y() + 10; if (!MemDC->Create(Sx, Sy, System32BitColourSpace)) { MemDC.Reset(); } } } if (MemDC) { MemDC->ClipRgn(NULL); #if 0//def _DEBUG MemDC->Colour(GColour(255, 0, 255)); MemDC->Rectangle(); #endif } #endif GSurface *pDC = MemDC ? MemDC : ScreenDC; GColour cBack; if (GetCss()) { GCss::ColorDef Bk = GetCss()->BackgroundColor(); if (Bk.Type == GCss::ColorRgb) cBack = Bk; } if (!cBack.IsValid()) cBack.Set(Enabled() ? LC_WORKSPACE : LC_MED, 24); pDC->Colour(cBack); pDC->Rectangle(); if (Tag) { Layout(); if (VScroll) { int LineY = GetFont()->GetHeight(); int Vs = (int)VScroll->Value(); pDC->SetOrigin(0, Vs * LineY); } bool InSelection = false; Tag->OnPaint(pDC, InSelection, 0); } #if GHTML_USE_DOUBLE_BUFFER if (MemDC) { pDC->SetOrigin(0, 0); #if 0 pDC->Colour(Rgb24(255, 0, 0), 24); pDC->Line(0, 0, X()-1, Y()-1); pDC->Line(X()-1, 0, 0, Y()-1); #endif ScreenDC->Blt(0, 0, MemDC); } #endif #if LGI_EXCEPTIONS } catch (...) { LgiTrace("GHtml paint crash\n"); } #endif if (d->OnLoadAnchor && VScroll) { GAutoString a = d->OnLoadAnchor; GotoAnchor(a); LgiAssert(d->OnLoadAnchor == 0); } } bool GHtml::HasSelection() { if (Cursor && Selection) { return Cursor->Cursor >= 0 && Selection->Selection >= 0 && !(Cursor == Selection && Cursor->Cursor == Selection->Selection); } return false; } void GHtml::UnSelectAll() { bool i = false; if (Cursor) { Cursor->Cursor = -1; Cursor = NULL; i = true; } if (Selection) { Selection->Selection = -1; Selection = NULL; i = true; } if (i) { Invalidate(); } } void GHtml::SelectAll() { } GTag *GHtml::GetLastChild(GTag *t) { if (t && t->Children.Length()) { for (GTag *i = ToTag(t->Children.Last()); i; ) { GTag *c = i->Children.Length() ? ToTag(i->Children.Last()) : NULL; if (c) i = c; else return i; } } return 0; } GTag *GHtml::PrevTag(GTag *t) { // This returns the previous tag in the tree as if all the tags were // listed via recursion using "in order". // Walk up the parent chain looking for a prev for (GTag *p = t; p; p = ToTag(p->Parent)) { // Does this tag have a parent? if (p->Parent) { // Prev? GTag *pp = ToTag(p->Parent); ssize_t Idx = pp->Children.IndexOf(p); GTag *Prev = Idx > 0 ? ToTag(pp->Children[Idx - 1]) : NULL; if (Prev) { GTag *Last = GetLastChild(Prev); return Last ? Last : Prev; } else { return ToTag(p->Parent); } } } return 0; } GTag *GHtml::NextTag(GTag *t) { // This returns the next tag in the tree as if all the tags were // listed via recursion using "in order". // Does this have a child tag? if (t->Children.Length() > 0) { return ToTag(t->Children.First()); } else { // Walk up the parent chain for (GTag *p = t; p; p = ToTag(p->Parent)) { // Does this tag have a next? if (p->Parent) { GTag *pp = ToTag(p->Parent); size_t Idx = pp->Children.IndexOf(p); GTag *Next = pp->Children.Length() > Idx + 1 ? ToTag(pp->Children[Idx + 1]) : NULL; if (Next) { return Next; } } } } return 0; } int GHtml::GetTagDepth(GTag *Tag) { // Returns the depth of the tag in the tree. int n = 0; for (GTag *t = Tag; t; t = ToTag(t->Parent)) { n++; } return n; } bool GHtml::IsCursorFirst() { if (!Cursor || !Selection) return false; return CompareTagPos(Cursor, Cursor->Cursor, Selection, Selection->Selection); } bool GHtml::CompareTagPos(GTag *a, ssize_t AIdx, GTag *b, ssize_t BIdx) { // Returns true if the 'a' is before 'b' point. if (!a || !b) return false; if (a == b) { return AIdx < BIdx; } else { GArray ATree, BTree; for (GTag *t = a; t; t = ToTag(t->Parent)) ATree.AddAt(0, t); for (GTag *t = b; t; t = ToTag(t->Parent)) BTree.AddAt(0, t); ssize_t Depth = MIN(ATree.Length(), BTree.Length()); for (int i=0; i 0); GTag *p = ATree[i-1]; LgiAssert(BTree[i-1] == p); ssize_t ai = p->Children.IndexOf(at); ssize_t bi = p->Children.IndexOf(bt); return ai < bi; } } } return false; } void GHtml::SetLoadImages(bool i) { if (i ^ GetLoadImages()) { GDocView::SetLoadImages(i); SendNotify(GNotifyShowImagesChanged); if (GetLoadImages() && Tag) { Tag->LoadImages(); } } } char *GHtml::GetSelection() { char *s = 0; if (Cursor && Selection) { GMemQueue p; bool InSelection = false; Tag->CopyClipboard(p, InSelection); int Len = (int)p.GetSize(); if (Len > 0) { char16 *t = (char16*)p.New(sizeof(char16)); if (t) { size_t Len = StrlenW(t); for (int i=0; iOnFind(Dlg); } void BuildTagList(GArray &t, GTag *Tag) { t.Add(Tag); for (unsigned i=0; iChildren.Length(); i++) { GTag *c = ToTag(Tag->Children[i]); BuildTagList(t, c); } } static void FormEncode(GStringPipe &p, const char *c) { const char *s = c; while (*c) { while (*c && *c != ' ') c++; if (c > s) { p.Write(s, c - s); c = s; } if (*c == ' ') { p.Write("+", 1); s = c; } else break; } } bool GHtml::OnSubmitForm(GTag *Form) { if (!Form || !Environment) { LgiAssert(!"Bad param"); return false; } const char *Method = NULL; const char *Action = NULL; if (!Form->Get("method", Method) || !Form->Get("action", Action)) { LgiAssert(!"Missing form action/method"); return false; } LHashTbl,char*> f; Form->CollectFormValues(f); bool Status = false; if (!_stricmp(Method, "post")) { GStringPipe p(256); bool First = true; // const char *Field; // for (char *Val = f.First(&Field); Val; Val = f.Next(&Field)) for (auto v : f) { if (First) First = false; else p.Write("&", 1); FormEncode(p, v.key); p.Write("=", 1); FormEncode(p, v.value); } GAutoPtr Data(p.NewStr()); Status = Environment->OnPostForm(this, Action, Data); } else if (!_stricmp(Method, "get")) { Status = Environment->OnNavigate(this, Action); } else { LgiAssert(!"Bad form method."); } f.DeleteArrays(); return Status; } bool GHtml::OnFind(GFindReplaceCommon *Params) { bool Status = false; if (Params) { if (!Params->Find) return Status; d->FindText.Reset(Utf8ToWide(Params->Find)); d->MatchCase = Params->MatchCase; } if (!Cursor) Cursor = Tag; if (Cursor && d->FindText) { GArray Tags; BuildTagList(Tags, Tag); ssize_t Start = Tags.IndexOf(Cursor); for (unsigned i=1; iText()) { char16 *Hit; if (d->MatchCase) Hit = StrstrW(s->Text(), d->FindText); else Hit = StristrW(s->Text(), d->FindText); if (Hit) { // found something... UnSelectAll(); Selection = Cursor = s; Cursor->Cursor = Hit - s->Text(); Selection->Selection = Cursor->Cursor + StrlenW(d->FindText); OnCursorChanged(); if (VScroll) { // Scroll the tag into view... int y = s->AbsY(); int LineY = GetFont()->GetHeight(); int Val = y / LineY; VScroll->Value(Val); } Invalidate(); Status = true; break; } } } } return Status; } bool GHtml::OnKey(GKey &k) { bool Status = false; if (k.Down()) { int Dy = 0; int LineY = GetFont()->GetHeight(); int Page = GetClient().Y() / LineY; switch (k.c16) { case 'f': case 'F': { if (k.Modifier()) { GFindDlg Dlg(this, 0, FindCallback, this); Dlg.DoModal(); Status = true; } break; } case VK_F3: { OnFind(NULL); break; } case 'c': case 'C': #ifdef WIN32 case VK_INSERT: #endif { printf("Got 'c', mod=%i\n", k.Modifier()); if (k.Modifier()) { Copy(); Status = true; } break; } case VK_UP: { Dy = -1; Status = true; break; } case VK_DOWN: { Dy = 1; Status = true; break; } case VK_PAGEUP: { Dy = -Page; Status = true; break; } case VK_PAGEDOWN: { Dy = Page; Status = true; break; } case VK_HOME: { Dy = (int) (VScroll ? -VScroll->Value() : 0); Status = true; break; } case VK_END: { if (VScroll) { int64 Low, High; VScroll->Limits(Low, High); Dy = (int) ((High - Page) - VScroll->Value()); } Status = true; break; } } if (Dy && VScroll) { VScroll->Value(VScroll->Value() + Dy); Invalidate(); } } return Status; } int GHtml::ScrollY() { return GetFont()->GetHeight() * (VScroll ? (int)VScroll->Value() : 0); } void GHtml::OnMouseClick(GMouse &m) { Capture(m.Down()); SetPulse(m.Down() ? 200 : -1); if (m.Down()) { Focus(true); int Offset = ScrollY(); bool TagProcessedClick = false; GTagHit Hit; if (Tag) { Tag->GetTagByPos(Hit, m.x, m.y + Offset, 0, false, DEBUG_TAG_BY_POS); #if DEBUG_TAG_BY_POS Hit.Dump("MouseClick"); #endif } if (m.Left() && !m.IsContextMenu()) { if (m.Double()) { d->WordSelectMode = true; if (Cursor) { // Extend the selection out to the current word's boundaries. Selection = Cursor; Selection->Selection = Cursor->Cursor; if (Cursor->Text()) { ssize_t Base = Cursor->GetTextStart(); char16 *Text = Cursor->Text() + Base; while (Text[Cursor->Cursor]) { char16 c = Text[Cursor->Cursor]; if (strchr(WordDelim, c) || StrchrW(WhiteW, c)) break; Cursor->Cursor++; } } if (Selection->Text()) { ssize_t Base = Selection->GetTextStart(); char16 *Sel = Selection->Text() + Base; while (Selection->Selection > 0) { char16 c = Sel[Selection->Selection - 1]; if (strchr(WordDelim, c) || StrchrW(WhiteW, c)) break; Selection->Selection--; } } Invalidate(); SendNotify(GNotifySelectionChanged); } } else if (Hit.NearestText) { d->WordSelectMode = false; UnSelectAll(); Cursor = Hit.NearestText; Cursor->Cursor = Hit.Index; #if DEBUG_SELECTION LgiTrace("StartSelect Near='%20S' Idx=%i\n", Hit.NearestText->Text(), Hit.Index); #endif OnCursorChanged(); SendNotify(GNotifySelectionChanged); } else { #if DEBUG_SELECTION LgiTrace("StartSelect no text hit %p, %p\n", Cursor, Selection); #endif } } if (Hit.NearestText && Hit.Near == 0) { TagProcessedClick = Hit.NearestText->OnMouseClick(m); } else if (Hit.Direct) { TagProcessedClick = Hit.Direct->OnMouseClick(m); } #ifdef _DEBUG else if (m.Left() && m.Ctrl()) { LgiMsg(this, "No tag under the cursor.", GetClass()); } #endif if (!TagProcessedClick && m.IsContextMenu()) { GSubMenu RClick; enum ContextMenuCmds { IDM_DUMP = 100, IDM_COPY_SRC, IDM_VIEW_SRC, IDM_EXTERNAL, IDM_COPY, IDM_VIEW_IMAGES, }; #define IDM_CHARSET_BASE 10000 RClick.AppendItem (LgiLoadString(L_TEXTCTRL_COPY, "Copy"), IDM_COPY, HasSelection()); GMenuItem *Vs = RClick.AppendItem (LgiLoadString(L_VIEW_SOURCE, "View Source"), IDM_VIEW_SRC, Source != 0); RClick.AppendItem (LgiLoadString(L_COPY_SOURCE, "Copy Source"), IDM_COPY_SRC, Source != 0); GMenuItem *Load = RClick.AppendItem (LgiLoadString(L_VIEW_IMAGES, "View External Images"), IDM_VIEW_IMAGES, true); if (Load) Load->Checked(GetLoadImages()); RClick.AppendItem (LgiLoadString(L_VIEW_IN_DEFAULT_BROWSER, "View in Default Browser"), IDM_EXTERNAL, Source != 0); GSubMenu *Cs = RClick.AppendSub (LgiLoadString(L_CHANGE_CHARSET, "Change Charset")); if (Cs) { int n=0; for (GCharset *c = LgiGetCsList(); c->Charset; c++, n++) { Cs->AppendItem(c->Charset, IDM_CHARSET_BASE + n, c->IsAvailable()); } } if (!GetReadOnly() || // Is editor #ifdef _DEBUG 1 #else 0 #endif ) { RClick.AppendSeparator(); RClick.AppendItem("Dump Layout", IDM_DUMP, Tag != 0); } if (Vs) { Vs->Checked(!IsHtml); } if (OnContextMenuCreate(Hit, RClick) && GetMouse(m, true)) { int Id = RClick.Float(this, m.x, m.y); switch (Id) { case IDM_COPY: { Copy(); break; } case IDM_VIEW_SRC: { if (Vs) { DeleteObj(Tag); IsHtml = !IsHtml; ParseDocument(Source); } break; } case IDM_COPY_SRC: { if (Source) { GClipBoard c(this); const char *ViewCs = GetCharset(); if (ViewCs) { GAutoWString w((char16*)LgiNewConvertCp(LGI_WideCharset, Source, ViewCs)); if (w) c.TextW(w); } else c.Text(Source); } break; } case IDM_VIEW_IMAGES: { SetLoadImages(!GetLoadImages()); break; } case IDM_DUMP: { if (Tag) { GAutoWString s = Tag->DumpW(); if (s) { GClipBoard c(this); c.TextW(s); } } break; } case IDM_EXTERNAL: { if (!Source) { LgiTrace("%s:%i - No HTML source code.\n", _FL); break; } char Path[MAX_PATH]; if (!LGetSystemPath(LSP_TEMP, Path, sizeof(Path))) { LgiTrace("%s:%i - Failed to get the system path.\n", _FL); break; } char f[32]; sprintf_s(f, sizeof(f), "_%i.html", LgiRand(1000000)); LgiMakePath(Path, sizeof(Path), Path, f); GFile F; if (!F.Open(Path, O_WRITE)) { LgiTrace("%s:%i - Failed to open '%s' for writing.\n", _FL, Path); break; } GStringPipe Ex; bool Error = false; F.SetSize(0); GAutoWString SrcMem; const char *ViewCs = GetCharset(); if (ViewCs) SrcMem.Reset((char16*)LgiNewConvertCp(LGI_WideCharset, Source, ViewCs)); else SrcMem.Reset(Utf8ToWide(Source)); for (char16 *s=SrcMem; s && *s;) { char16 *cid = StristrW(s, L"cid:"); while (cid && !strchr("\'\"", cid[-1])) { cid = StristrW(cid+1, L"cid:"); } if (cid) { char16 Delim = cid[-1]; char16 *e = StrchrW(cid, Delim); if (e) { *e = 0; if (StrchrW(cid, '\n')) { *e = Delim; Error = true; break; } else { char File[MAX_PATH] = ""; if (Environment) { GDocumentEnv::LoadJob *j = Environment->NewJob(); if (j) { j->Uri.Reset(WideToUtf8(cid)); j->Env = Environment; j->Pref = GDocumentEnv::LoadJob::FmtFilename; j->UserUid = GetDocumentUid(); GDocumentEnv::LoadType Result = Environment->GetContent(j); if (Result == GDocumentEnv::LoadImmediate) { if (j->Filename) strcpy_s(File, sizeof(File), j->Filename); } else if (Result == GDocumentEnv::LoadDeferred) { d->DeferredLoads++; } DeleteObj(j); } } *e = Delim; Ex.Push(s, cid - s); if (File[0]) { char *d; while ((d = strchr(File, '\\'))) { *d = '/'; } Ex.Push(L"file:///"); GAutoWString w(Utf8ToWide(File)); Ex.Push(w); } s = e; } } else { Error = true; break; } } else { Ex.Push(s); break; } } if (!Error) { int64 WideChars = Ex.GetSize() / sizeof(char16); GAutoWString w(Ex.NewStrW()); GAutoString u(WideToUtf8(w, WideChars)); if (u) F.Write(u, strlen(u)); F.Close(); GAutoString Err; if (!LgiExecute(Path, NULL, NULL, &Err)) { LgiMsg( this, "Failed to open '%s'\n%s", LgiApp ? LgiApp->GBase::Name() : GetClass(), MB_OK, Path, Err.Get()); } } break; } default: { if (Id >= IDM_CHARSET_BASE) { GCharset *c = LgiGetCsList() + (Id - IDM_CHARSET_BASE); if (c->Charset) { Charset = c->Charset; OverideDocCharset = true; char *Src = Source.Release(); _Delete(); _New(); Source.Reset(Src); ParseDocument(Source); Invalidate(); SendNotify(GNotifyCharsetChanged); } } else { OnContextMenuCommand(Hit, Id); } break; } } } } } else // Up Click { if (Selection && Cursor && Selection == Cursor && Selection->Selection == Cursor->Cursor) { Selection->Selection = -1; Selection = 0; SendNotify(GNotifySelectionChanged); #if DEBUG_SELECTION LgiTrace("NoSelect on release\n"); #endif } } } void GHtml::OnLoad() { d->IsLoaded = true; SendNotify(GNotifyDocLoaded); } GTag *GHtml::GetTagByPos(int x, int y, ssize_t *Index, GdcPt2 *LocalCoords, bool DebugLog) { GTag *Status = NULL; if (Tag) { if (DebugLog) LgiTrace("GetTagByPos starting...\n"); GTagHit Hit; Tag->GetTagByPos(Hit, x, y, 0, DebugLog); if (DebugLog) LgiTrace("GetTagByPos Hit=%s, %i, %i...\n\n", Hit.Direct ? Hit.Direct->Tag.Get() : 0, Hit.Index, Hit.Near); Status = Hit.NearestText && Hit.Near == 0 ? Hit.NearestText : Hit.Direct; if (Hit.NearestText && Hit.Near < 30) { if (Index) *Index = Hit.Index; if (LocalCoords) *LocalCoords = Hit.LocalCoords; } } return Status; } bool GHtml::OnMouseWheel(double Lines) { if (VScroll) { VScroll->Value(VScroll->Value() + (int)Lines); Invalidate(); } return true; } LgiCursor GHtml::GetCursor(int x, int y) { int Offset = ScrollY(); ssize_t Index = -1; GdcPt2 LocalCoords; GTag *Tag = GetTagByPos(x, y + Offset, &Index, &LocalCoords); if (Tag) { GAutoString Uri; if (LocalCoords.x >= 0 && LocalCoords.y >= 0 && Tag->IsAnchor(&Uri)) { GRect c = GetClient(); c.Offset(-c.x1, -c.y1); if (c.Overlap(x, y) && ValidStr(Uri)) { return LCUR_PointingHand; } } } return LCUR_Normal; } void GHtml::OnMouseMove(GMouse &m) { if (!Tag) return; int Offset = ScrollY(); GTagHit Hit; Tag->GetTagByPos(Hit, m.x, m.y + Offset, 0, false); if (!Hit.Direct && !Hit.NearestText) return; GAutoString Uri; GTag *HitTag = Hit.NearestText && Hit.Near == 0 ? Hit.NearestText : Hit.Direct; if (HitTag && HitTag->TipId == 0 && Hit.LocalCoords.x >= 0 && Hit.LocalCoords.y >= 0 && HitTag->IsAnchor(&Uri) && Uri) { if (!Tip.GetParent()) { Tip.Attach(this); } GRect r = HitTag->GetRect(false); r.Offset(0, -Offset); HitTag->TipId = Tip.NewTip(Uri, r); // LgiTrace("NewTip: %s @ %s, ID=%i\n", Uri.Get(), r.GetStr(), HitTag->TipId); } if (IsCapturing() && Cursor && Hit.NearestText) { if (!Selection) { Selection = Cursor; Selection->Selection = Cursor->Cursor; Cursor = Hit.NearestText; Cursor->Cursor = Hit.Index; OnCursorChanged(); Invalidate(); SendNotify(GNotifySelectionChanged); #if DEBUG_SELECTION LgiTrace("CreateSelection '%20S' %i\n", Hit.NearestText->Text(), Hit.Index); #endif } else if ((Cursor != Hit.NearestText) || (Cursor->Cursor != Hit.Index)) { // Move the cursor to track the mouse if (Cursor) { Cursor->Cursor = -1; } Cursor = Hit.NearestText; Cursor->Cursor = Hit.Index; #if DEBUG_SELECTION LgiTrace("ExtendSelection '%20S' %i\n", Hit.NearestText->Text(), Hit.Index); #endif if (d->WordSelectMode && Cursor->Text()) { ssize_t Base = Cursor->GetTextStart(); if (IsCursorFirst()) { // Extend the cursor up the document to include the whole word while (Cursor->Cursor > 0) { char16 c = Cursor->Text()[Base + Cursor->Cursor - 1]; if (strchr(WordDelim, c) || StrchrW(WhiteW, c)) break; Cursor->Cursor--; } } else { // Extend the cursor down the document to include the whole word while (Cursor->Text()[Base + Cursor->Cursor]) { char16 c = Cursor->Text()[Base + Cursor->Cursor]; if (strchr(WordDelim, c) || StrchrW(WhiteW, c)) break; Cursor->Cursor++; } } } OnCursorChanged(); Invalidate(); SendNotify(GNotifySelectionChanged); } } } void GHtml::OnPulse() { if (VScroll && IsCapturing()) { int Fy = DefFont() ? DefFont()->GetHeight() : 16; GMouse m; if (GetMouse(m, false)) { GRect c = GetClient(); int Lines = 0; if (m.y < c.y1) { // Scroll up Lines = (c.y1 - m.y + Fy - 1) / -Fy; } else if (m.y > c.y2) { // Scroll down Lines = (m.y - c.y2 + Fy - 1) / Fy; } if (Lines) { VScroll->Value(VScroll->Value() + Lines); Invalidate(); } } } } GRect *GHtml::GetCursorPos() { return &d->CursorPos; } void GHtml::SetCursorVis(bool b) { if (d->CursorVis ^ b) { d->CursorVis = b; Invalidate(); } } bool GHtml::GetCursorVis() { return d->CursorVis; } GDom *ElementById(GTag *t, char *id) { if (t && id) { const char *i; if (t->Get("id", i) && _stricmp(i, id) == 0) return t; for (unsigned i=0; iChildren.Length(); i++) { GTag *c = ToTag(t->Children[i]); GDom *n = ElementById(c, id); if (n) return n; } } return 0; } GDom *GHtml::getElementById(char *Id) { return ElementById(Tag, Id); } bool GHtml::GetLinkDoubleClick() { return d->LinkDoubleClick; } void GHtml::SetLinkDoubleClick(bool b) { d->LinkDoubleClick = b; } bool GHtml::GetFormattedContent(const char *MimeType, GString &Out, GArray *Media) { if (!MimeType) { LgiAssert(!"No MIME type for getting formatted content"); return false; } if (!_stricmp(MimeType, "text/html")) { // We can handle this type... GArray Imgs; if (Media) { // Find all the image tags... Tag->Find(TAG_IMG, Imgs); // Give them CID's if they don't already have them for (unsigned i=0; iGet("src", Src) && !Img->Get("cid", Cid)) { char id[256]; sprintf_s(id, sizeof(id), "%x.%x", (unsigned)LgiCurrentTime(), (unsigned)LgiRand()); Img->Set("cid", id); Img->Get("cid", Cid); } if (Src && Cid) { GFile *f = new GFile; if (f) { if (f->Open(Src, O_READ)) { // Add the exported image stream to the media array GDocView::ContentMedia &m = Media->New(); m.Id = Cid; m.Stream.Reset(f); } } } } } // Export the HTML, including the CID's from the first step Out = Name(); } else if (!_stricmp(MimeType, "text/plain")) { // Convert DOM tree down to text instead... GStringPipe p(512); if (Tag) { GTag::TextConvertState State(&p); Tag->ConvertToText(State); } Out = p.NewGStr(); } return false; } void GHtml::OnContent(GDocumentEnv::LoadJob *Res) { if (JobSem.Lock(_FL)) { JobSem.Jobs.Add(Res); JobSem.Unlock(); PostEvent(M_JOBS_LOADED); } } GHtmlElement *GHtml::CreateElement(GHtmlElement *Parent) { return new GTag(this, Parent); } bool GHtml::GetVariant(const char *Name, GVariant &Value, char *Array) { if (!_stricmp(Name, "supportLists")) // Type: Bool Value = false; else if (!_stricmp(Name, "vml")) // Type: Bool // Vector Markup Language Value = false; else if (!_stricmp(Name, "mso")) // Type: Bool // mso = Microsoft Office Value = false; else return false; return true; } bool GHtml::EvaluateCondition(const char *Cond) { if (!Cond) return true; // This is a really bad attempt at writing an expression evaluator. // I could of course use the scripting language but that would pull // in a fairly large dependency on the HTML control. However user // apps that already have that could reimplement this virtual function // if they feel like it. GArray Str; for (const char *c = Cond; *c; ) { if (IsAlpha(*c)) { Str.Add(LgiTokStr(c)); } else if (IsWhiteSpace(*c)) { c++; } else { const char *e = c; while (*e && !IsWhiteSpace(*e) && !IsAlpha(*e)) e++; Str.Add(NewStr(c, e - c)); LgiAssert(e > c); if (e > c) c = e; else break; } } bool Result = true; bool Not = false; for (unsigned i=0; iGetAnchor(Name); if (a) { if (VScroll) { int LineY = GetFont()->GetHeight(); int Ay = a->AbsY(); int Scr = Ay / LineY; VScroll->Value(Scr); VScroll->SendNotify(); } else d->OnLoadAnchor.Reset(NewStr(Name)); } } return false; } bool GHtml::GetEmoji() { return d->DecodeEmoji; } void GHtml::SetEmoji(bool i) { d->DecodeEmoji = i; } //////////////////////////////////////////////////////////////////////// class GHtml_Factory : public GViewFactory { GView *NewView(const char *Class, GRect *Pos, const char *Text) { if (_stricmp(Class, "GHtml") == 0) { return new GHtml(-1, 0, 0, 100, 100, new GDefaultDocumentEnv); } return 0; } } GHtml_Factory; ////////////////////////////////////////////////////////////////////// struct BuildContext { GHtmlTableLayout *Layout; GTag *Table; GTag *TBody; GTag *CurTr; GTag *CurTd; int cx, cy; BuildContext() { Layout = NULL; cx = cy = 0; Table = NULL; TBody = NULL; CurTr = NULL; CurTd = NULL; } bool Build(GTag *t, int Depth) { bool RetReattach = false; switch (t->TagId) { case TAG_TABLE: { if (!Table) Table = t; else return false; break; } case TAG_TBODY: { if (TBody) return false; TBody = t; break; } case TAG_TR: { CurTr = t; break; } case TAG_TD: { CurTd = t; if (t->Parent != CurTr) { if ( !CurTr && (Table || TBody) ) { GTag *p = TBody ? TBody : Table; CurTr = new GTag(p->Html, p); if (CurTr) { CurTr->Tag.Reset(NewStr("tr")); CurTr->TagId = TAG_TR; ssize_t Idx = t->Parent->Children.IndexOf(t); t->Parent->Attach(CurTr, Idx); } } if (CurTr) { CurTr->Attach(t); RetReattach = true; } else { LgiAssert(0); return false; } } t->Cell->Pos.x = cx; t->Cell->Pos.y = cy; Layout->Set(t); break; } default: { if (CurTd == t->Parent) return false; break; } } for (unsigned n=0; nChildren.Length(); n++) { GTag *c = ToTag(t->Children[n]); bool Reattached = Build(c, Depth+1); if (Reattached) n--; } if (t->TagId == TAG_TR) { CurTr = NULL; cy++; cx = 0; Layout->s.y = cy; } if (t->TagId == TAG_TD) { CurTd = NULL; cx += t->Cell->Span.x; Layout->s.x = MAX(cx, Layout->s.x); } return RetReattach; } }; GHtmlTableLayout::GHtmlTableLayout(GTag *table) { Table = table; if (!Table) return; #if 0 BuildContext Ctx; Ctx.Layout = this; Ctx.Build(table, 0); #else int y = 0; GTag *FakeRow = 0; GTag *FakeCell = 0; GTag *r; for (size_t i=0; iChildren.Length(); i++) { r = ToTag(Table->Children[i]); if (r->Display() == GCss::DispNone) continue; if (r->TagId == TAG_TR) { FakeRow = 0; FakeCell = 0; } else if (r->TagId == TAG_TBODY) { ssize_t Index = Table->Children.IndexOf(r); for (size_t n=0; nChildren.Length(); n++) { GTag *t = ToTag(r->Children[n]); Table->Children.AddAt(++Index, t); t->Parent = Table; /* LgiTrace("Moving '%s'(%p) from TBODY(%p) into '%s'(%p)\n", t->Tag, t, r, t->Parent->Tag, t->Parent); */ } r->Children.Length(0); } else { if (!FakeRow) { if ((FakeRow = new GTag(Table->Html, 0))) { FakeRow->Tag.Reset(NewStr("tr")); FakeRow->TagId = TAG_TR; ssize_t Idx = Table->Children.IndexOf(r); Table->Attach(FakeRow, Idx); } } if (FakeRow) { if (!IsTableCell(r->TagId) && !FakeCell) { if ((FakeCell = new GTag(Table->Html, FakeRow))) { FakeCell->Tag.Reset(NewStr("td")); FakeCell->TagId = TAG_TD; if ((FakeCell->Cell = new GTag::TblCell)) { FakeCell->Cell->Span.x = 1; FakeCell->Cell->Span.y = 1; } } } ssize_t Idx = Table->Children.IndexOf(r); r->Detach(); if (IsTableCell(r->TagId)) { FakeRow->Attach(r); } else { LgiAssert(FakeCell != NULL); FakeCell->Attach(r); } i = Idx - 1; } } } FakeCell = NULL; for (size_t n=0; nChildren.Length(); n++) { GTag *r = ToTag(Table->Children[n]); if (r->TagId == TAG_TR) { int x = 0; for (size_t i=0; iChildren.Length(); i++) { GTag *cell = ToTag(r->Children[i]); if (!IsTableCell(cell->TagId)) { if (!FakeCell) { // Make a fake TD cell FakeCell = new GTag(Table->Html, NULL); FakeCell->Tag.Reset(NewStr("td")); FakeCell->TagId = TAG_TD; if ((FakeCell->Cell = new GTag::TblCell)) { FakeCell->Cell->Span.x = 1; FakeCell->Cell->Span.y = 1; } // Join the fake TD into the TR r->Children[i] = FakeCell; FakeCell->Parent = r; } else { // Not the first non-TD tag, so delete it from the TR. Only the // fake TD will remain in the TR. r->Children.DeleteAt(i--, true); } // Insert the tag into it as a child FakeCell->Children.Add(cell); cell->Parent = FakeCell; cell = FakeCell; } else { FakeCell = NULL; } if (IsTableCell(cell->TagId)) { if (cell->Display() == GCss::DispNone) continue; while (Get(x, y)) { x++; } cell->Cell->Pos.x = x; cell->Cell->Pos.y = y; Set(cell); x += cell->Cell->Span.x; } } y++; FakeCell = NULL; } } #endif } void GHtmlTableLayout::Dump() { int Sx, Sy; GetSize(Sx, Sy); LgiTrace("Table %i x %i cells.\n", Sx, Sy); for (int x=0; xCell->Pos.x, t->Cell->Pos.y, t->Cell->Span.x, t->Cell->Span.y); LgiTrace("%-10s", s); } LgiTrace("\n"); } LgiTrace("\n"); } void GHtmlTableLayout::GetAll(List &All) { LHashTbl, bool> Added; for (size_t y=0; y= (int) c.Length()) return NULL; CellArray &a = c[y]; if (x >= (int) a.Length()) return NULL; return a[x]; } bool GHtmlTableLayout::Set(GTag *t) { if (!t) return false; for (int y=0; yCell->Span.y; y++) { for (int x=0; xCell->Span.x; x++) { // LgiAssert(!c[y][x]); c[t->Cell->Pos.y + y][t->Cell->Pos.x + x] = t; } } return true; } void GTagHit::Dump(const char *Desc) { GArray d, n; GTag *t = Direct; unsigned i; for (i=0; i<3 && t; t = ToTag(t->Parent), i++) { d.AddAt(0, t); } t = NearestText; for (i=0; i<3 && t; t = ToTag(t->Parent), i++) { n.AddAt(0, t); } LgiTrace("Hit: %s Direct: ", Desc); for (i=0; i%s", d[i]->Tag ? d[i]->Tag.Get() : "CONTENT"); LgiTrace(" Nearest: "); for (i=0; i%s", n[i]->Tag ? n[i]->Tag.Get() : "CONTENT"); LgiTrace(" Local: %ix%i Index: %i Block: %s '%.10S'\n", LocalCoords.x, LocalCoords.y, Index, Block ? Block->GetStr() : NULL, Block ? Block->Text + Index : NULL); } //////////////////////////////////////////////////////////////////////// bool GCssStyle::GetVariant(const char *Name, GVariant &Value, char *Array) { if (!Name) return false; if (!_stricmp(Name, "Display")) // Type: String { Value = Css->ToString(Css->Display()); return Value.Str() != NULL; } else LgiAssert(!"Impl me."); return false; } bool GCssStyle::SetVariant(const char *Name, GVariant &Value, char *Array) { if (!Name) return false; if (!_stricmp(Name, "display")) { const char *d = Value.Str(); if (Css->ParseDisplayType(d)) { GTag *t = dynamic_cast(Css); if (t) { t->Html->Layout(true); t->Html->Invalidate(); } return true; } } else LgiAssert(!"Impl me."); return false; } 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,1380 +1,1380 @@ /* Rich text design notes: - The document is an array of Blocks (Blocks have no hierarchy) - Blocks have a length in characters. New lines are considered as one '\n' char. - The main type of block is the TextBlock - TextBlock contains: - array of StyleText: This is the source text. Each run of text has a style associated with it. This forms the input to the layout algorithm and is what the user is editing. - array of TextLine: Contains all the info needed to render one line of text. Essentially the output of the layout engine. Contains an array of DisplayStr objects. i.e. Characters in the exact same style as each other. It will regularly be deleted and re-flowed from the StyleText objects. - For a plaint text document the entire thing is contained by the one TextBlock. - There is an Image block, where the image is treated as one character object. - Also a horizontal rule block. */ #ifndef _RICH_TEXT_EDIT_PRIV_H_ #define _RICH_TEXT_EDIT_PRIV_H_ #include "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)<, GString> 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_t 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_t 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_t Ch); }; struct CtrlCap { GString Name, Param; void Set(const char *name, const char *param) { Name = name; Param = param; } }; struct ButtonState { uint8_t IsMenu : 1; uint8_t IsPress : 1; uint8_t Pressed : 1; uint8_t MouseOver : 1; }; extern bool Utf16to32(GArray &Out, const uint16_t *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 *t = NULL, ssize_t Chars = -1, GNamedStyle *style = NULL); uint32_t *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; /// Draw debug selection bool DrawDebug; Block(GRichTextPriv *priv) { d = priv; DrawDebug = false; BlockUid = d->NextUid++; Cursors = 0; } Block(const Block *blk) { d = blk->d; DrawDebug = false; BlockUid = blk->GetUid(); Cursors = 0; } virtual ~Block() { // We must have removed cursors by the time we are deleted // otherwise there will be a hanging pointer in the cursor // object. 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 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, GRange *Rgn) = 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_t *Str, GFindReplaceCommon *Params) = 0; 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_t *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_t *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)) + GArray Tmp; + if (Utf16to32(Tmp, (const uint16_t*)s, e - s)) c.Reset(new DisplayStr(Src, GetFont(), &Tmp[0], Tmp.Length(), pDC)); #else c.Reset(new DisplayStr(Src, GetFont(), (uint32_t*)Str + Start, Len, pDC)); #endif } } return c; } virtual void Paint(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; + GArray Utf32; #endif EmojiDisplayStr(StyleText *src, GSurface *img, GFont *f, const uint32_t *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_t 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 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, GRange *Rng); 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_t *Str, GFindReplaceCommon *Params); void IncAllStyleRefs(); 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_t *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, GRange *Rng); 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_t *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(); // Events GMessage::Result OnEvent(GMessage *Msg); // Transactional changes bool AddText(Transaction *Trans, ssize_t AtOffset, const uint32_t *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; GString FileName; GString ContentId; GString StreamMimeType; 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, GRange *Rng); 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_t *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_t *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, GAutoString *Html); 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 { #ifdef WINDOWS ssize_t Len = s[0] && s[1] ? 4 : (s[0] ? 2 : 0); Buf[Used++] = LgiUtf16To32((const uint16 *&)s, Len); #else Buf[Used++] = *s++; #endif while (s < e && !IsWhiteSpace(*s)) { #ifdef WINDOWS Len = s[0] && s[1] ? 4 : (s[0] ? 2 : 0); Buf[Used++] = LgiUtf16To32((const uint16 *&)s, Len); #else Buf[Used++] = *s++; #endif } } } bool Status = false; if (Used > 0) { Status = Tb->AddText(NoTransaction, -1, &Buf[0], Used, Style); LastChar = Buf[Used-1]; } return Status; } }; GAutoPtr CreationCtx; bool ToHtml(GArray *Media = NULL, BlockCursor *From = NULL, BlockCursor *To = 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 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,2519 +1,2519 @@ #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_t*)&St->ItemAt(0), St->Length()); } GRichTextPriv::StyleText::StyleText(const uint32_t *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*)t, (int)Chars); } } uint32_t *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_t *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]; + Utf16to32(Utf32, (const uint16_t*) StrCache.Get(), len); + uint32_t *u = &Utf32[0]; #else LgiAssert(sizeof(char16) == 4); uint32_t *u = (uint32_t*)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_t*)(const char16*)(*this) #endif , Len)); return s; } void GRichTextPriv::EmojiDisplayStr::Paint(GSurface *pDC, int &FixX, int FixY, GColour &Back) { GRect f(0, 0, x-1, y-1); f.Offset(FixedToInt(FixX), FixedToInt(FixY)); pDC->Colour(Back); pDC->Rectangle(&f); int Op = pDC->Op(GDC_ALPHA); for (unsigned i=0; iBlt(f.x1, f.y1, Img, &SrcRect[i]); f.x1 += EMOJI_CELL_SIZE; FixX += IntToFixed(EMOJI_CELL_SIZE); } pDC->Op(Op); } double GRichTextPriv::EmojiDisplayStr::GetAscent() { return EMOJI_CELL_SIZE * 0.8; } ssize_t GRichTextPriv::EmojiDisplayStr::PosToIndex(int XPos, bool Nearest) { if (XPos >= (int)x) return Chars; if (XPos <= 0) return 0; return (XPos + (Nearest ? EMOJI_CELL_SIZE >> 1 : 0)) / EMOJI_CELL_SIZE; } ////////////////////////////////////////////////////////////////////////////////////////////////// GRichTextPriv::TextLine::TextLine(int XOffsetPx, int WidthPx, int YOffsetPx) { NewLine = 0; PosOff.ZOff(0, 0); PosOff.Offset(XOffsetPx, YOffsetPx); } int GRichTextPriv::TextLine::Length() { int Len = NewLine; for (unsigned i=0; iChars; return Len; } /// This runs after the layout line has been filled with display strings. /// It measures the line and works out the right offsets for each strings /// so that their baselines all match up correctly. void GRichTextPriv::TextLine::LayoutOffsets(int DefaultFontHt) { double BaseLine = 0.0; int HtPx = 0; for (unsigned i=0; iGetAscent(); BaseLine = MAX(BaseLine, Ascent); HtPx = MAX(HtPx, ds->Y()); } if (Strs.Length() == 0) HtPx = DefaultFontHt; else LgiAssert(HtPx > 0); for (unsigned i=0; iGetAscent(); if (Ascent > 0.0) ds->OffsetY = (int)(BaseLine - Ascent); LgiAssert(ds->OffsetY >= 0); HtPx = MAX(HtPx, ds->OffsetY+ds->Y()); } PosOff.y2 = PosOff.y1 + HtPx - 1; } ////////////////////////////////////////////////////////////////////////////////////////////////// GRichTextPriv::TextBlock::TextBlock(GRichTextPriv *priv) : Block(priv) { LayoutDirty = false; Len = 0; Pos.ZOff(-1, -1); Style = NULL; Fnt = NULL; ClickErrIdx = -1; Margin.ZOff(0, 0); Border.ZOff(0, 0); Padding.ZOff(0, 0); } GRichTextPriv::TextBlock::TextBlock(const TextBlock *Copy) : Block(Copy) { LayoutDirty = true; Len = Copy->Len; Pos = Copy->Pos; Style = Copy->Style; Fnt = Copy->Fnt; Margin = Copy->Margin; Border = Copy->Border; Padding = Copy->Padding; for (unsigned i=0; iTxt.Length(); i++) { Txt.Add(new StyleText(Copy->Txt.ItemAt(i))); } } GRichTextPriv::TextBlock::~TextBlock() { LgiAssert(Cursors == 0); Txt.DeleteObjects(); } void GRichTextPriv::TextBlock::Dump() { LgiTrace(" Txt.Len=%i, margin=%s, border=%s, padding=%s\n", Txt.Length(), Margin.GetStr(), Border.GetStr(), Padding.GetStr()); for (unsigned i=0; iLength() ? #ifndef WINDOWS (char16*) #endif t->At(0) : NULL, t->Length()); s = s.Strip(); LgiTrace(" %p: style=%p/%s, len=%i\n", t, t->GetStyle(), t->GetStyle() ? t->GetStyle()->Name.Get() : NULL, t->Length()); } } GNamedStyle *GRichTextPriv::TextBlock::GetStyle(ssize_t At) { if (At >= 0) { GArray t; if (GetTextAt(At, t)) return t[0]->GetStyle(); } return Style; } void GRichTextPriv::TextBlock::SetStyle(GNamedStyle *s) { if ((Style = s)) { Fnt = d->GetFont(s); LayoutDirty = true; LgiAssert(Fnt != NULL); Margin.x1 = Style->MarginLeft().ToPx(Pos.X(), Fnt); Margin.y1 = Style->MarginTop().ToPx(Pos.Y(), Fnt); Margin.x2 = Style->MarginRight().ToPx(Pos.X(), Fnt); Margin.y2 = Style->MarginBottom().ToPx(Pos.Y(), Fnt); Border.x1 = Style->BorderLeft().ToPx(Pos.X(), Fnt); Border.y1 = Style->BorderTop().ToPx(Pos.Y(), Fnt); Border.x2 = Style->BorderRight().ToPx(Pos.X(), Fnt); Border.y2 = Style->BorderBottom().ToPx(Pos.Y(), Fnt); Padding.x1 = Style->PaddingLeft().ToPx(Pos.X(), Fnt); Padding.y1 = Style->PaddingTop().ToPx(Pos.Y(), Fnt); Padding.x2 = Style->PaddingRight().ToPx(Pos.X(), Fnt); Padding.y2 = Style->PaddingBottom().ToPx(Pos.Y(), Fnt); } } ssize_t GRichTextPriv::TextBlock::Length() { return Len; } HtmlTag IsDefaultStyle(HtmlTag Id, GCss *Css) { if (!Css) return CONTENT; if (Css->Length() == 2) { GCss::ColorDef c = Css->Color(); if ((GColour)c != GColour::Blue) return CONTENT; GCss::TextDecorType td = Css->TextDecoration(); if (td != GCss::TextDecorUnderline) return CONTENT; return TAG_A; } else if (Css->Length() == 1) { GCss::FontWeightType fw = Css->FontWeight(); if (fw == GCss::FontWeightBold || fw == GCss::FontWeightBolder || fw >= GCss::FontWeight700) return TAG_B; GCss::TextDecorType td = Css->TextDecoration(); if (td == GCss::TextDecorUnderline) return TAG_U; GCss::FontStyleType fs = Css->FontStyle(); if (fs == GCss::FontStyleItalic) return TAG_I; } return CONTENT; } bool GRichTextPriv::TextBlock::ToHtml(GStream &s, GArray *Media, GRange *Rng) { s.Print("

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

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