1 (* coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, version 3 of the License ONLY.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 {$INCLUDE ../shared/a_modes.inc}
17 {.$DEFINE UI_STYLE_DEBUG_SEARCH}
24 fui_common
, // for TGxRGBA
25 xstreams
, xparser
, utils
, hashtable
;
29 TStyleSection
= class;
31 TStyleValue
= packed record
33 type TType
= (Empty
, Bool
, Int
, Color
, Str
);
36 constructor Create (v
: Boolean);
37 constructor Create (v
: Integer);
38 constructor Create (ar
, ag
, ab
: Integer; aa
: Integer=255);
39 constructor Create (const v
: TGxRGBA
);
40 constructor Create (const v
: AnsiString
);
42 function isEmpty (): Boolean; inline;
44 function toString (): AnsiString
;
45 function asRGBA
: TGxRGBA
; inline;
46 function asRGBADef (const def
: TGxRGBA
): TGxRGBA
; inline;
47 function asInt (const def
: Integer=0): Integer; inline;
48 function asBool (const def
: Boolean=false): Boolean; inline;
49 function asStr (const def
: AnsiString
=''): AnsiString
; inline;
54 TType
.Bool
: (bval
: Boolean);
55 TType
.Int
: (ival
: Integer);
56 TType
.Color
: (r
, g
, b
, a
: Byte);
57 TType
.Str
: (sval
: Pointer); // AnsiString
60 THashStrStyleVal
= specialize THashBase
<AnsiString
, TStyleValue
, THashKeyStrAnsiCI
>;
61 THashStrSection
= specialize THashBase
<AnsiString
, TStyleSection
, THashKeyStrAnsiCI
>;
65 mParent
: TStyleSection
; // for inheritance
66 mInherits
: AnsiString
;
67 mHashName
: AnsiString
; // for this section
68 mCtlName
: AnsiString
; // for this section
69 mVals
: THashStrStyleVal
;
70 mHashes
: THashStrSection
;
71 mCtls
: THashStrSection
;
74 function getTopLevel (): TStyleSection
; inline;
75 // "text-color#inactive@label"
76 function getValue (const path
: AnsiString
): TStyleValue
;
79 constructor Create ();
80 destructor Destroy (); override;
82 function get (name
, hash
, ctl
: AnsiString
): TStyleValue
;
85 property value
[const path
: AnsiString
]: TStyleValue read getValue
; default
;
86 property topLevel
: TStyleSection read getTopLevel
;
91 mId
: AnsiString
; // style name ('default', for example)
95 procedure createMain ();
97 procedure parse (par
: TTextParser
);
99 function getValue (const path
: AnsiString
): TStyleValue
; inline;
102 constructor Create (const aid
: AnsiString
);
103 constructor Create (st
: TStream
); // parse from stream
104 constructor CreateFromFile (const fname
: AnsiString
);
105 destructor Destroy (); override;
107 function get (name
, hash
, ctl
: AnsiString
): TStyleValue
;
110 property id
: AnsiString read mId
;
111 property value
[const path
: AnsiString
]: TStyleValue read getValue
; default
;
115 procedure uiLoadStyles (const fname
: AnsiString
);
116 procedure uiLoadStyles (st
: TStream
);
118 // will return "default" (or raise an exception if there is no "default")
119 function uiFindStyle (const stname
: AnsiString
): TUIStyle
;
129 styles
: array of TUIStyle
;
132 procedure FreeStyles();
142 function createDefaultStyle (): TUIStyle;
147 st := TStringStream.Create(defaultStyleStr);
150 result := TUIStyle.Create(st);
158 function uiFindStyle (const stname
: AnsiString
): TUIStyle
;
162 if (Length(stname
) > 0) then
164 for stl
in styles
do if (strEquCI1251(stl
.mId
, stname
)) then begin result
:= stl
; exit
; end;
166 for stl
in styles
do if (strEquCI1251(stl
.mId
, 'default')) then begin result
:= stl
; exit
; end;
167 raise Exception
.Create('FlexUI FATAL: no "default" style in stylesheet');
169 stl := createDefaultStyle();
170 SetLength(styles, Length(styles)+1);
171 styles[High(styles)] := stl;
177 procedure uiLoadStyles (const fname
: AnsiString
);
181 st
:= fuiOpenFile(fname
);
182 if (st
= nil) then raise Exception
.Create('FlexUI file '''+fname
+''' not found!');
191 procedure uiLoadStyles (st
: TStream
);
197 if (st
= nil) then raise Exception
.Create('cannot load UI styles from nil stream');
198 par
:= TFileTextParser
.Create(st
, false, [par
.TOption
.SignedNumbers
, par
.TOption
.DollarIsId
, par
.TOption
.DashIsId
, par
.TOption
.HtmlColors
]);
201 while (not par
.isEOF
) do
203 stl
:= TUIStyle
.Create('');
205 //writeln('new style: <', stl.mId, '>');
207 while (f
< Length(styles
)) do
209 if (strEquCI1251(styles
[f
].mId
, stl
.mId
)) then
213 if (f
< Length(styles
)) then
215 FreeAndNil(styles
[f
]);
220 SetLength(styles
, f
+1);
229 // we should have "default" style
230 for f
:= 0 to High(styles
) do if (strEquCI1251(styles
[f
].mId
, 'default')) then exit
;
231 raise Exception
.Create('FlexUI FATAL: no "default" style in stylesheet');
233 stl := createDefaultStyle();
234 SetLength(styles, Length(styles)+1);
235 styles[High(styles)] := stl;
240 // ////////////////////////////////////////////////////////////////////////// //
241 procedure freeValueCB (var v
: TStyleValue
); begin
242 if (v
.vtype
= v
.TType
.Str
) then
244 AnsiString(v
.sval
) := '';
246 v
.vtype
:= v
.TType
.Empty
;
249 constructor TStyleValue
.Create (v
: Boolean); begin vtype
:= TType
.Bool
; bval
:= v
; end;
250 constructor TStyleValue
.Create (v
: Integer); begin vtype
:= TType
.Int
; ival
:= v
; end;
251 constructor TStyleValue
.Create (const v
: AnsiString
); begin vtype
:= TType
.Str
; sval
:= Pointer(v
); end;
253 constructor TStyleValue
.Create (ar
, ag
, ab
: Integer; aa
: Integer=255);
255 vtype
:= TType
.Color
;
256 r
:= nmax(0, nmin(ar
, 255));
257 g
:= nmax(0, nmin(ag
, 255));
258 b
:= nmax(0, nmin(ab
, 255));
259 a
:= nmax(0, nmin(aa
, 255));
262 constructor TStyleValue
.Create (const v
: TGxRGBA
);
264 vtype
:= TType
.Color
;
271 function TStyleValue
.isEmpty (): Boolean; inline; begin result
:= (vtype
= TType
.Empty
); end;
272 function TStyleValue
.asRGBA
: TGxRGBA
; inline; begin if (vtype
= TType
.Color
) then result
:= TGxRGBA
.Create(r
, g
, b
, a
) else result
:= TGxRGBA
.Create(0, 0, 0, 0); end;
273 function TStyleValue
.asRGBADef (const def
: TGxRGBA
): TGxRGBA
; inline; begin if (vtype
= TType
.Color
) then result
:= TGxRGBA
.Create(r
, g
, b
, a
) else result
:= def
; end;
274 function TStyleValue
.asInt (const def
: Integer=0): Integer; inline; begin if (vtype
= TType
.Int
) then result
:= ival
else if (vtype
= TType
.Bool
) then begin if (bval
) then result
:= 1 else result
:= 0; end else result
:= def
; end;
275 function TStyleValue
.asBool (const def
: Boolean=false): Boolean; inline; begin if (vtype
= TType
.Bool
) then result
:= bval
else if (vtype
= TType
.Int
) then result
:= (ival
<> 0) else result
:= def
; end;
276 function TStyleValue
.asStr (const def
: AnsiString
=''): AnsiString
; inline; begin if (vtype
= TType
.Str
) then result
:= AnsiString(sval
) else result
:= def
; end;
278 function TStyleValue
.toString (): AnsiString
;
281 TType
.Empty
: result
:= '<empty>';
282 TType
.Bool
: if bval
then result
:= 'true' else result
:= 'false';
283 TType
.Int
: result
:= formatstrf('%s', [ival
]);
284 TType
.Color
: if (a
= 255) then result
:= formatstrf('rgb(%s,%s,%s)', [r
, g
, b
]) else result
:= formatstrf('rgba(%s,%s,%s)', [r
, g
, b
, a
]);
285 else result
:= '<invalid>';
290 // ////////////////////////////////////////////////////////////////////////// //
291 procedure freeSectionCB (var v
: TStyleSection
);
297 function splitPath (const path
: AnsiString
; out name
, hash
, ctl
: AnsiString
): Boolean;
299 hashPos
, atPos
: Integer;
305 hashPos
:= pos('#', path
);
306 atPos
:= pos('@', path
);
310 // has ctl, and (possible) hash
311 if (hashPos
> 0) then
314 if (atPos
< hashPos
) then
317 if (atPos
> 1) then name
:= Copy(path
, 1, atPos
-1);
318 Inc(atPos
); // skip "at"
319 if (atPos
< hashPos
) then ctl
:= Copy(path
, atPos
, hashPos
-atPos
);
320 Inc(hashPos
); // skip hash
321 if (hashPos
<= Length(path
)) then hash
:= Copy(path
, hashPos
, Length(path
)-hashPos
+1);
326 if (hashPos
> 1) then name
:= Copy(path
, 1, hashPos
-1);
327 Inc(hashPos
); // skip hash
328 if (hashPos
< atPos
) then hash
:= Copy(path
, hashPos
, atPos
-hashPos
);
329 Inc(atPos
); // skip "at"
330 if (atPos
<= Length(path
)) then ctl
:= Copy(path
, atPos
, Length(path
)-atPos
+1);
336 if (atPos
> 1) then name
:= Copy(path
, 1, atPos
-1);
337 Inc(atPos
); // skip "at"
338 if (atPos
<= Length(path
)) then ctl
:= Copy(path
, atPos
, Length(path
)-atPos
+1);
341 else if (hashPos
> 0) then
344 if (hashPos
> 1) then name
:= Copy(path
, 1, hashPos
-1);
345 Inc(hashPos
); // skip hash
346 if (hashPos
<= Length(path
)) then hash
:= Copy(path
, hashPos
, Length(path
)-hashPos
+1);
357 // ////////////////////////////////////////////////////////////////////////// //
358 constructor TStyleSection
.Create ();
360 mVals
:= THashStrStyleVal
.Create(freeValueCB
);
361 mHashes
:= THashStrSection
.Create(freeSectionCB
);
362 mCtls
:= THashStrSection
.Create(freeSectionCB
);
366 destructor TStyleSection
.Destroy ();
375 function TStyleSection
.getTopLevel (): TStyleSection
; inline;
378 while Result
.mParent
<> nil do
379 Result
:= Result
.mParent
;
383 function TStyleSection
.get (name
, hash
, ctl
: AnsiString
): TStyleValue
;
386 sect
, s1
, so
: TStyleSection
;
387 jumpsLeft
: Integer = 32; // max inheritance level
388 skipInherits
: Boolean = false;
390 result
.vtype
:= result
.TType
.Empty
;
391 if (Length(name
) = 0) then exit
; // alas
392 {$IFDEF UI_STYLE_DEBUG_SEARCH}writeln('***GET: <', name
, '#', hash
, '@', ctl
, '>');{$ENDIF}
395 if (Length(ctl
) > 0) then
397 if (not strEquCI1251(ctl
, mCtlName
)) then
400 if (not topLevel
.mCtls
.get(ctl
, sect
)) then sect
:= topLevel
;
404 if (Length(hash
) > 0) then
406 if (not strEquCI1251(hash
, sect
.mHashName
)) then
408 if (sect
.mHashes
.get(hash
, s1
)) then sect
:= s1
;
411 // try name, go up with inheritance
412 while (jumpsLeft
> 0) do
414 if (sect
.mVals
.get(name
, result
)) then
416 if (not result
.isEmpty
) then exit
; // i found her!
419 if (skipInherits
) or (Length(sect
.mInherits
) = 0) then
421 skipInherits
:= false;
422 // for hash section: try parent section first
423 if (Length(sect
.mHashName
) > 0) then
425 {$IFDEF UI_STYLE_DEBUG_SEARCH}writeln('search for <#', hash
, '@', ctl
, '> at <#', sect
.mHashName
, '@', sect
.mCtlName
, '>: hash up');{$ENDIF}
426 sect
:= sect
.mParent
;
427 if (sect
= nil) then break
; // alas
428 {$IFDEF UI_STYLE_DEBUG_SEARCH}writeln(' hash up: trying <#', sect
.mHashName
, '@', sect
.mCtlName
, '>');{$ENDIF}
429 if (sect
.mVals
.get(name
, result
)) then
431 if (not result
.isEmpty
) then exit
; // i found her!
433 // move another parent up
434 sect
:= sect
.mParent
;
435 if (sect
= nil) then break
; // alas
436 {$IFDEF UI_STYLE_DEBUG_SEARCH}writeln(' hash up: jumped up twice to <#', sect
.mHashName
, '@', sect
.mCtlName
, '>');{$ENDIF}
441 {$IFDEF UI_STYLE_DEBUG_SEARCH}writeln('search for <#', hash
, '@', ctl
, '> at <#', sect
.mHashName
, '@', sect
.mCtlName
, '>: jump up');{$ENDIF}
442 sect
:= sect
.mParent
;
443 if (sect
= nil) then break
; // alas
445 // here, we should have non-hash section
446 assert(Length(sect
.mHashName
) = 0);
447 // if we want hash, try to find it, otherwise do nothing
448 if (Length(hash
) > 0) then
450 {$IFDEF UI_STYLE_DEBUG_SEARCH}writeln(' search for <#', hash
, '@', ctl
, '> at <#', sect
.mHashName
, '@', sect
.mCtlName
, '>: hash down');{$ENDIF}
451 if (sect
.mHashes
.get(hash
, s1
)) then
454 {$IFDEF UI_STYLE_DEBUG_SEARCH}writeln(' found <#', sect
.mHashName
, '@', sect
.mCtlName
, '>');{$ENDIF}
462 if (jumpsLeft
< 1) then break
; // alas
463 {$IFDEF UI_STYLE_DEBUG_SEARCH}writeln('inherits: <', sect
.mInherits
, '>');{$ENDIF}
464 // parse inherit string
465 if (not splitPath(sect
.mInherits
, tmp
, hash
, ctl
)) then exit
; // alas
466 {$IFDEF UI_STYLE_DEBUG_SEARCH}writeln('inherits: <', hash
, '>:<', ctl
, '>');{$ENDIF}
468 if (Length(ctl
) > 0) then
471 if (strEquCI1251(ctl
, '$main$')) then sect
:= topLevel
472 else if (strEquCI1251(ctl
, '$up$')) then begin if (Length(sect
.mHashName
) <> 0) then sect
:= sect
.mParent
.mParent
else sect
:= sect
.mParent
; end
473 else if (not topLevel
.mCtls
.get(ctl
, sect
)) then sect
:= topLevel
;
474 if (sect
= nil) then break
; // alas
475 if (Length(hash
) > 0) then
477 if (sect
.mHashes
.get(hash
, s1
)) then sect
:= s1
;
483 assert(Length(hash
) > 0);
484 // dummy loop, so i can use `break`
486 // get out of hash section
487 if (Length(sect
.mHashName
) > 0) then
489 {$IFDEF UI_STYLE_DEBUG_SEARCH}writeln('hash-jump-up: <#', sect
.mHashName
, '@', sect
.mCtlName
, '>');{$ENDIF}
490 sect
:= sect
.mParent
;
491 if (sect
= nil) then break
; // alas
492 // check for hash section in parent; use parent if there is no such hash section
493 {$IFDEF UI_STYLE_DEBUG_SEARCH}writeln(' checking parent: <#', sect
.mHashName
, '@', sect
.mCtlName
, '> for <#', hash
, '>');{$ENDIF}
495 if (sect
.mHashes
.get(hash
, s1
)) then
497 if (s1
<> sect
) and (s1
<> so
) then
499 {$IFDEF UI_STYLE_DEBUG_SEARCH}writeln(' found in parent: <#', sect
.mHashName
, '@', sect
.mCtlName
, '> for <#', hash
, '>');{$ENDIF}
506 // we're in parent, try to find hash section
507 {$IFDEF UI_STYLE_DEBUG_SEARCH}writeln(' checking: <#', sect
.mHashName
, '@', sect
.mCtlName
, '> for <#', hash
, '>');{$ENDIF}
508 if (sect
.mHashes
.get(hash
, s1
)) then
510 {$IFDEF UI_STYLE_DEBUG_SEARCH}writeln(' found: <#', sect
.mHashName
, '@', sect
.mCtlName
, '> for <#', hash
, '>');{$ENDIF}
515 // reuse current parent, but don't follow inheritance for it
516 skipInherits
:= true;
520 if (sect
= nil) then break
;
525 result
.vtype
:= result
.TType
.Empty
;
529 // "text-color#inactive@label"
530 function TStyleSection
.getValue (const path
: AnsiString
): TStyleValue
;
532 name
, hash
, ctl
: AnsiString
;
534 result
.vtype
:= result
.TType
.Empty
;
535 if (not splitPath(path
, name
, hash
, ctl
)) then exit
; // alas
536 //writeln('name:<', name, '>; hash:<', hash, '>; ctl:<', ctl, '>');
537 result
:= get(name
, hash
, ctl
);
541 // ////////////////////////////////////////////////////////////////////////// //
542 constructor TUIStyle
.Create (const aid
: AnsiString
);
549 constructor TUIStyle
.Create (st
: TStream
); // parse from stream
555 if (st
= nil) then exit
;
556 par
:= TFileTextParser
.Create(st
, false, [par
.TOption
.SignedNumbers
, par
.TOption
.DollarIsId
, par
.TOption
.DashIsId
, par
.TOption
.HtmlColors
]);
565 constructor TUIStyle
.CreateFromFile (const fname
: AnsiString
);
569 st
:= openDiskFileRO(fname
);
578 destructor TUIStyle
.Destroy ();
586 procedure TUIStyle
.createMain ();
588 mMain
:= TStyleSection
.Create();
589 mMain
.mCtlName
:= '$main$';
593 function TUIStyle
.getValue (const path
: AnsiString
): TStyleValue
; inline;
595 result
:= mMain
[path
];
598 function TUIStyle
.get (name
, hash
, ctl
: AnsiString
): TStyleValue
;
600 result
:= mMain
.get(name
, hash
, ctl
);
604 procedure TUIStyle
.parse (par
: TTextParser
);
605 function getByte (): Byte;
607 if (par
.tokType
<> par
.TTInt
) then par
.expectInt();
608 if (par
.tokInt
< 0) or (par
.tokInt
> 255) then par
.error('invalid byte value');
609 result
:= Byte(par
.tokInt
);
613 procedure parseSection (sect
: TStyleSection
; ctlAllowed
: Boolean; hashAllowed
: Boolean);
616 sc
: TStyleSection
= nil;
619 procedure parseInherit ();
622 if (par
.eatDelim('(')) then
624 if (par
.eatDelim(')')) then par
.error('empty inheritance is not allowed');
625 if (par
.eatDelim('#')) then
628 inh
+= par
.expectId();
630 if (par
.eatDelim('@')) then
633 inh
+= par
.expectId();
635 par
.expectDelim(')');
639 function nib2c (n
: Integer): Byte; inline;
641 if (n
< 0) then result
:= 0
642 else if (n
> 15) then result
:= 255
643 else result
:= Byte(255*n
div 15);
649 par
.expectDelim('{');
650 while (not par
.isDelim('}')) do
652 while (par
.eatDelim(';')) do begin end;
654 if ctlAllowed
and (par
.eatDelim('@')) then
658 par
.eatDelim(':'); // optional
659 if (not sect
.mCtls
.get(s
, sc
)) then
661 // create new section
662 sc
:= TStyleSection
.Create();
667 sect
.mCtls
.put(s
, sc
);
671 assert(sc
.mParent
= sect
);
672 assert(sc
.mHashName
= '');
673 assert(sc
.mCtlName
= s
);
674 if (Length(sc
.mInherits
) <> 0) and (Length(inh
) <> 0) then par
.error('double inheritance');
677 if (not par
.eatDelim(';')) then parseSection(sc
, false, true);
681 if hashAllowed
and (par
.eatDelim('#')) then
685 par
.eatDelim(':'); // optional
686 if (not sect
.mHashes
.get(s
, sc
)) then
688 // create new section
689 sc
:= TStyleSection
.Create();
694 sect
.mHashes
.put(s
, sc
);
698 assert(sc
.mParent
= sect
);
699 assert(sc
.mHashName
= s
);
700 assert(sc
.mCtlName
= '');
701 if (Length(sc
.mInherits
) <> 0) and (Length(inh
) <> 0) then par
.error('double inheritance');
704 if (not par
.eatDelim(';')) then parseSection(sc
, false, false);
709 par
.expectDelim(':');
710 if (par
.eatId('rgb')) or (par
.eatId('rgba')) then
713 par
.expectDelim('(');
714 v
.vtype
:= v
.TType
.Color
;
715 v
.r
:= getByte(); par
.eatDelim(','); // optional
716 v
.g
:= getByte(); par
.eatDelim(','); // optional
717 v
.b
:= getByte(); par
.eatDelim(','); // optional
718 if (par
.tokType
= par
.TTInt
) then
720 v
.a
:= getByte(); par
.eatDelim(','); // optional
724 v
.a
:= 255; // opaque
726 par
.expectDelim(')');
728 else if (par
.isId
) and (par
.tokStr
[1] = '#') then
731 assert((Length(par
.tokStr
) = 4) or (Length(par
.tokStr
) = 7));
732 //writeln('<', par.tokStr, '>; {', par.curChar, '}');
733 v
.vtype
:= v
.TType
.Color
;
734 if (Length(par
.tokStr
) = 4) then
737 v
.r
:= nib2c(digitInBase(par
.tokStr
[2], 16));
738 v
.g
:= nib2c(digitInBase(par
.tokStr
[3], 16));
739 v
.b
:= nib2c(digitInBase(par
.tokStr
[4], 16));
744 v
.r
:= Byte(digitInBase(par
.tokStr
[2], 16)*16+digitInBase(par
.tokStr
[3], 16));
745 v
.g
:= Byte(digitInBase(par
.tokStr
[4], 16)*16+digitInBase(par
.tokStr
[5], 16));
746 v
.b
:= Byte(digitInBase(par
.tokStr
[6], 16)*16+digitInBase(par
.tokStr
[7], 16));
749 //writeln(' r=', v.r, '; g=', v.g, '; b=', v.b);
752 else if (par
.eatId('true')) or (par
.eatId('yes')) then
754 v
.vtype
:= v
.TType
.Bool
;
757 else if (par
.eatId('false')) or (par
.eatId('no')) then
759 v
.vtype
:= v
.TType
.Bool
;
762 else if (par
.isStr
) then
765 v
:= TStyleValue
.Create(par
.tokStr
);
768 else if (par
.eatId('inherit')) then
770 v
.vtype
:= v
.TType
.Empty
;
775 v
.vtype
:= v
.TType
.Int
;
776 v
.ival
:= par
.expectInt();
778 par
.expectDelim(';');
779 sect
.mVals
.put(s
, v
);
781 par
.expectDelim('}');
786 if (not par
.isIdOrStr
) then
788 if (Length(mId
) = 0) then par
.error('style name expected');
794 if (Length(mId
) = 0) then mId
:= 'default';
796 if (not par
.eatDelim(';')) then parseSection(mMain
, true, true);