2024-12-02 03:14:39 -06:00

607 lines
17 KiB
ObjectPascal

unit NewTabSet;
{
Inno Setup
Copyright (C) 1997-2024 Jordan Russell
Portions by Martijn Laan
For conditions of distribution and use, see LICENSE.TXT.
TNewTabSet - modern VS-style tabs with theme support
}
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Math, Generics.Collections,
ModernColors, NewUxTheme;
type
TTabPosition = (tpTop, tpBottom);
TBoolList = TList<Boolean>;
TCloseButtonClickEvent = procedure(Sender: TObject; Index: Integer) of object;
TNewTabSet = class(TCustomControl)
private
FCloseButtons: TBoolList;
FHints: TStrings;
FMenuThemeData: HTHEME;
FOnCloseButtonClick: TCloseButtonClickEvent;
FTabs: TStrings;
FTabIndex: Integer;
FTabPosition: TTabPosition;
FTabsOffset: Integer;
FTheme: TTheme;
FThemeDark: Boolean;
FHotIndex: Integer;
procedure EnsureCurrentTabIsFullyVisible;
function GetTabRect(const Index: Integer; const ApplyTabsOffset: Boolean = True): TRect;
function GetCloseButtonRect(const TabRect: TRect): TRect;
procedure InvalidateTab(Index: Integer);
procedure CloseButtonsListChanged(Sender: TObject; const Item: Boolean;
Action: TCollectionNotification);
procedure TabsListChanged(Sender: TObject);
procedure HintsListChanged(Sender: TObject);
procedure SetCloseButtons(Value: TBoolList);
procedure SetTabs(Value: TStrings);
procedure SetTabIndex(Value: Integer);
procedure SetTabPosition(Value: TTabPosition);
procedure SetTheme(Value: TTheme);
procedure SetHints(const Value: TStrings);
function ToCurrentPPI(const XY: Integer): Integer;
procedure UpdateThemeData(const Open: Boolean);
procedure CMFontChanged(var Message: TMessage); message CM_FONTCHANGED;
procedure CMHintShow(var Message: TCMHintShow); message CM_HINTSHOW;
procedure CMMouseLeave(var Message: TMessage); message CM_MOUSELEAVE;
procedure WMMouseMove(var Message: TWMMouseMove); message WM_MOUSEMOVE;
procedure WMThemeChanged(var Message: TMessage); message WM_THEMECHANGED;
protected
function CanAutoSize(var NewWidth, NewHeight: Integer): Boolean; override;
procedure CreateParams(var Params: TCreateParams); override;
procedure CreateWnd; override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
procedure UpdateHotIndex(NewHotIndex: Integer);
procedure Paint; override;
procedure Resize; override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
property CloseButtons: TBoolList read FCloseButtons write SetCloseButtons;
property Theme: TTheme read FTheme write SetTheme;
published
property Align;
property AutoSize default True;
property Font;
property Hints: TStrings read FHints write SetHints;
property ParentFont;
property TabIndex: Integer read FTabIndex write SetTabIndex;
property Tabs: TStrings read FTabs write SetTabs;
property TabPosition: TTabPosition read FTabPosition write SetTabPosition default tpBottom;
property PopupMenu;
property OnClick;
property OnCloseButtonClick: TCloseButtonClickEvent read FOnCloseButtonClick write FOnCloseButtonClick;
end;
procedure Register;
implementation
uses
Types;
procedure Register;
begin
RegisterComponents('JR', [TNewTabSet]);
end;
procedure RGBToHSV(const R, G, B: Integer; var H, S: Double; var V: Integer);
var
Max, Min, C: Integer;
begin
Max := R;
if G > Max then Max := G;
if B > Max then Max := B;
Min := R;
if G < Min then Min := G;
if B < Min then Min := B;
C := Max - Min;
if C = 0 then begin
H := 0;
S := 0;
end
else begin
if Max = R then
H := (60 * (G - B)) / C
else if Max = G then
H := (60 * (B - R)) / C + 120
else if Max = B then
H := (60 * (R - G)) / C + 240;
if H < 0 then
H := H + 360;
S := C / Max;
end;
V := Max;
end;
procedure HSVtoRGB(const H, S: Double; const V: Integer; var R, G, B: Integer);
var
I, P, Q, T: Integer;
F: Double;
begin
I := Trunc(H / 60);
F := Frac(H / 60);
P := Round(V * (1.0 - S));
Q := Round(V * (1.0 - S * F));
T := Round(V * (1.0 - S * (1.0 - F)));
case I of
0: begin R := V; G := t; B := p; end;
1: begin R := q; G := V; B := p; end;
2: begin R := p; G := V; B := t; end;
3: begin R := p; G := q; B := V; end;
4: begin R := t; G := p; B := V; end;
5: begin R := V; G := p; B := q; end;
else
{ Should only get here with bogus input }
R := 0; G := 0; B := 0;
end;
end;
function LightenColor(const Color: TColorRef; const Amount: Integer): TColorRef;
var
H, S: Double;
V, R, G, B: Integer;
begin
RGBtoHSV(Byte(Color), Byte(Color shr 8), Byte(Color shr 16), H, S, V);
Inc(V, Amount);
if V > 255 then
V := 255;
if V < 0 then
V := 0;
HSVtoRGB(H, S, V, R, G, B);
Result := R or (G shl 8) or (B shl 16);
end;
{ TNewTabSet }
const
TabSetMarginX = 4;
TabPaddingX = 5;
TabPaddingY = 3;
CloseButtonSizeX = 12;
constructor TNewTabSet.Create(AOwner: TComponent);
begin
inherited;
FCloseButtons := TBoolList.Create;
FCloseButtons.OnNotify := CloseButtonsListChanged;
FTabs := TStringList.Create;
TStringList(FTabs).OnChange := TabsListChanged;
FTabPosition := tpBottom;
FHints := TStringList.Create;
TStringList(FHints).OnChange := HintsListChanged;
FHotIndex := -1;
ControlStyle := ControlStyle + [csOpaque];
Width := 129;
Height := 21;
AutoSize := True;
end;
procedure TNewTabSet.CreateParams(var Params: TCreateParams);
begin
inherited;
with Params.WindowClass do
style := style and not CS_HREDRAW;
end;
procedure TNewTabSet.CreateWnd;
begin
inherited;
UpdateThemeData(True);
end;
destructor TNewTabSet.Destroy;
begin
UpdateThemeData(False);
FHints.Free;
FTabs.Free;
FCloseButtons.Free;
inherited;
end;
procedure TNewTabSet.CMFontChanged(var Message: TMessage);
begin
inherited;
if AutoSize then
AdjustSize;
end;
procedure TNewTabSet.CMHintShow(var Message: TCMHintShow);
var
I: Integer;
R: TRect;
begin
inherited;
if Message.HintInfo.HintControl = Self then begin
for I := 0 to FTabs.Count-1 do begin
if I >= FHints.Count then
Break;
R := GetTabRect(I);
if PtInRect(R, Message.HintInfo.CursorPos) then begin
Message.HintInfo.HintStr := FHints[I];
Message.HintInfo.CursorRect := R;
Break;
end;
end;
end;
end;
procedure TNewTabSet.WMMouseMove(var Message: TWMMouseMove);
begin
var Pos := SmallPointToPoint(Message.Pos);
var NewHotIndex := -1;
for var I := 0 to FTabs.Count-1 do begin
if I <> TabIndex then begin
var R := GetTabRect(I);
if PtInRect(R, TPoint.Create(Pos.X, Pos.Y)) then begin
NewHotIndex := I;
Break;
end;
end;
end;
UpdateHotIndex(NewHotIndex);
end;
procedure TNewTabSet.WMThemeChanged(var Message: TMessage);
begin
{ Don't Run to Cursor into this function, it will interrupt up the theme change }
UpdateThemeData(True);
inherited;
end;
procedure TNewTabSet.EnsureCurrentTabIsFullyVisible;
begin
const AdjacentTabVisiblePixels = ToCurrentPPI(30);
const CR = ClientRect;
const R = GetTabRect(FTabIndex, False);
var Offset := FTabsOffset;
{ If the tab is overflowing to the right, scroll right }
var Overflow := R.Right - Offset - CR.Right + AdjacentTabVisiblePixels;
if Overflow > 0 then
Inc(Offset, Overflow);
{ If there's extra space after the last tab, scroll left if possible }
const LastTabRight = GetTabRect(FTabs.Count-1, False).Right +
ToCurrentPPI(TabSetMarginX);
Offset := Min(Offset, Max(0, LastTabRight - CR.Right));
{ If the tab is overflowing to the left, scroll left }
Overflow := Offset - R.Left + AdjacentTabVisiblePixels;
if Overflow > 0 then
Offset := Max(0, Offset - Overflow);
if FTabsOffset <> Offset then begin
FTabsOffset := Offset;
Invalidate;
end;
end;
function TNewTabSet.CanAutoSize(var NewWidth, NewHeight: Integer): Boolean;
begin
{ We need to manage our own height for correct results with non-default PPI }
Canvas.Font.Assign(Font);
NewHeight := Canvas.TextHeight('0') + (ToCurrentPPI(TabPaddingY) * 2) +
ToCurrentPPI(2);
Result := True;
end;
function TNewTabSet.GetTabRect(const Index: Integer;
const ApplyTabsOffset: Boolean = True): TRect;
var
CR: TRect;
I, SizeX, SizeY: Integer;
Size: TSize;
begin
CR := ClientRect;
Canvas.Font.Assign(Font);
if FTabPosition = tpBottom then
Result.Top := 0;
Result.Right := ToCurrentPPI(TabSetMarginX);
if ApplyTabsOffset then
Dec(Result.Right, FTabsOffset);
for I := 0 to FTabs.Count-1 do begin
Size := Canvas.TextExtent(FTabs[I]);
SizeX := Size.cx + (ToCurrentPPI(TabPaddingX) * 2);
if (I < FCloseButtons.Count) and FCloseButtons[I] then
Inc(SizeX, ToCurrentPPI(CloseButtonSizeX));
SizeY := Size.cy + (ToCurrentPPI(TabPaddingY) * 2);
if FTabPosition = tpTop then
Result.Top := CR.Bottom - SizeY;
Result := Bounds(Result.Right, Result.Top, SizeX, SizeY);
if Index = I then
Exit;
end;
SetRectEmpty(Result);
end;
function TNewTabSet.GetCloseButtonRect(const TabRect: TRect): TRect;
begin
Result := TRect.Create(TabRect.Right - ToCurrentPPI(CloseButtonSizeX) - ToCurrentPPI(TabPaddingX) div 2,
TabRect.Top, TabRect.Right - ToCurrentPPI(TabPaddingX) div 2, TabRect.Bottom);
end;
procedure TNewTabSet.InvalidateTab(Index: Integer);
var
R: TRect;
begin
if HandleAllocated and (Index >= 0) and (Index < FTabs.Count) then begin
R := GetTabRect(Index);
InvalidateRect(Handle, @R, False);
end;
end;
procedure TNewTabSet.CloseButtonsListChanged(Sender: TObject; const Item: Boolean;
Action: TCollectionNotification);
begin
FHotIndex := -1;
Invalidate;
end;
procedure TNewTabSet.TabsListChanged(Sender: TObject);
begin
FHotIndex := -1;
Invalidate;
end;
procedure TNewTabSet.HintsListChanged(Sender: TObject);
begin
ShowHint := FHints.Count > 0;
end;
procedure TNewTabSet.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
var
I: Integer;
R: TRect;
begin
if Button = mbLeft then begin
for I := 0 to FTabs.Count-1 do begin
R := GetTabRect(I);
if (X >= R.Left) and (X < R.Right) then begin
if ((I = TabIndex) or (I = FHotIndex)) and (I < FCloseButtons.Count) and FCloseButtons[I] then begin
var R2 := GetCloseButtonRect(R);
if PtInRect(R2, TPoint.Create(X, Y)) then begin
if Assigned(OnCloseButtonClick) then
OnCloseButtonClick(Self, I);
Break;
end;
end;
TabIndex := I;
Break;
end;
end;
end;
end;
procedure TNewTabSet.UpdateHotIndex(NewHotIndex: Integer);
begin
var OldHotIndex := FHotIndex;
if NewHotIndex <> OldHotIndex then begin
FHotIndex := NewHotIndex;
if OldHotIndex <> -1 then
InvalidateTab(OldHotIndex);
if NewHotIndex <> -1 then
InvalidateTab(NewHotIndex);
end;
end;
procedure TNewTabSet.CMMouseLeave(var Message: TMessage);
begin
UpdateHotIndex(-1);
inherited;
end;
procedure TNewTabSet.Paint;
var
HighColorMode: Boolean;
procedure DrawCloseButton(const TabRect: TRect; const TabIndex: Integer);
const
MENU_SYSTEMCLOSE = 17;
MSYSC_NORMAL = 1;
begin
if (TabIndex < FCloseButtons.Count) and FCloseButtons[TabIndex] then begin
var R := GetCloseButtonRect(TabRect);
if FMenuThemeData <> 0 then begin
var Offset := ToCurrentPPI(1);
Inc(R.Left, Offset);
Inc(R.Top, Offset);
DrawThemeBackground(FMenuThemeData, Canvas.Handle, MENU_SYSTEMCLOSE, MSYSC_NORMAL, R, nil);
end else begin
InflateRect(R, -ToCurrentPPI(3), -ToCurrentPPI(6));
Canvas.Pen.Color := Canvas.Font.Color;
Canvas.MoveTo(R.Left, R.Top);
Canvas.LineTo(R.Right, R.Bottom);
Canvas.MoveTo(R.Left, R.Bottom-1);
Canvas.LineTo(R.Right, R.Top-1);
end;
end;
end;
procedure DrawTabs(const SelectedTab: Boolean);
var
I: Integer;
R: TRect;
begin
for I := 0 to FTabs.Count-1 do begin
R := GetTabRect(I);
if SelectedTab and (FTabIndex = I) then begin
if FTheme <> nil then
Canvas.Brush.Color := FTheme.Colors[tcBack]
else
Canvas.Brush.Color := clBtnFace;
Canvas.FillRect(R);
if FTheme <> nil then
Canvas.Font.Color := FTheme.Colors[tcFore]
else
Canvas.Font.Color := clBtnText;
Canvas.TextOut(R.Left + ToCurrentPPI(TabPaddingX), R.Top + ToCurrentPPI(TabPaddingY), FTabs[I]);
DrawCloseButton(R, I);
ExcludeClipRect(Canvas.Handle, R.Left, R.Top, R.Right, R.Bottom);
Break;
end;
if not SelectedTab and (FTabIndex <> I) then begin
if FHotIndex = I then begin
if FTheme <> nil then
Canvas.Font.Color := FTheme.Colors[tcFore]
else
Canvas.Font.Color := clBtnText;
end else if FTheme <> nil then
Canvas.Font.Color := FTheme.Colors[tcMarginFore]
else if HighColorMode and (ColorToRGB(clBtnFace) <> clBlack) then
Canvas.Font.Color := LightenColor(ColorToRGB(clBtnShadow), -43)
else begin
{ If the button face color is black, or if running in low color mode,
use plain clBtnHighlight as the text color }
Canvas.Font.Color := clBtnHighlight;
end;
Canvas.TextOut(R.Left + ToCurrentPPI(TabPaddingX), R.Top + ToCurrentPPI(TabPaddingY), FTabs[I]);
if FHotIndex = I then
DrawCloseButton(R, I);
end;
end;
end;
var
CR: TRect;
begin
Canvas.Font.Assign(Font);
HighColorMode := (GetDeviceCaps(Canvas.Handle, BITSPIXEL) *
GetDeviceCaps(Canvas.Handle, PLANES)) >= 15;
CR := ClientRect;
{ Work around an apparent NT 4.0/2000/??? bug. If the width of the DC is
greater than the width of the screen, then any call to ExcludeClipRect
inexplicably shrinks the DC's clipping rectangle to the screen width.
Calling IntersectClipRect first with the entire client area as the
rectangle solves this (don't ask me why). }
IntersectClipRect(Canvas.Handle, CR.Left, CR.Top, CR.Right, CR.Bottom);
{ Selected tab }
DrawTabs(True);
{ Top or bottom line }
if FTheme <> nil then
Canvas.Brush.Color := FTheme.Colors[tcBack]
else
Canvas.Brush.Color := clBtnFace;
const LineRectHeight = ToCurrentPPI(1);
var LineRect := CR;
if FTabPosition = tpBottom then
LineRect.Bottom := LineRect.Top + LineRectHeight
else
LineRect.Top := LineRect.Bottom - LineRectHeight;
Canvas.FillRect(LineRect);
{ Background fill }
if FTheme <> nil then
Canvas.Brush.Color := FTheme.Colors[tcMarginBack]
else if HighColorMode then
Canvas.Brush.Color := LightenColor(ColorToRGB(clBtnFace), 35)
else
Canvas.Brush.Color := clBtnShadow;
if FTabPosition = tpBottom then
Inc(CR.Top, LineRectHeight)
else
Dec(CR.Bottom, LineRectHeight);
Canvas.FillRect(CR);
{ Non-selected tabs }
DrawTabs(False);
end;
procedure TNewTabSet.Resize;
begin
EnsureCurrentTabIsFullyVisible;
inherited;
end;
procedure TNewTabSet.SetCloseButtons(Value: TBoolList);
begin
FCloseButtons.Clear;
for var V in Value do
FCloseButtons.Add(V);
end;
procedure TNewTabSet.SetHints(const Value: TStrings);
begin
FHints.Assign(Value);
end;
procedure TNewTabSet.SetTabIndex(Value: Integer);
begin
if FTabIndex <> Value then begin
InvalidateTab(FTabIndex);
FTabIndex := Value;
InvalidateTab(Value);
EnsureCurrentTabIsFullyVisible;
Click;
end;
end;
procedure TNewTabSet.SetTabPosition(Value: TTabPosition);
begin
if FTabPosition <> Value then begin
FTabPosition := Value;
Invalidate;
end;
end;
procedure TNewTabSet.SetTabs(Value: TStrings);
begin
FTabs.Assign(Value);
if FTabIndex >= FTabs.Count then
SetTabIndex(FTabs.Count-1);
end;
procedure TNewTabSet.SetTheme(Value: TTheme);
begin
if FTheme <> Value then begin
FTheme := Value;
var NewThemeDark := (FTheme <> nil) and FTheme.Dark;
if FThemeDark <> NewThemeDark then
UpdateThemeData(True);
FThemeDark := NewThemeDark;
Invalidate;
end;
end;
function TNewTabSet.ToCurrentPPI(const XY: Integer): Integer;
begin
Result := MulDiv(XY, CurrentPPI, 96);
end;
procedure TNewTabSet.UpdateThemeData(const Open: Boolean);
begin
if FMenuThemeData <> 0 then begin
CloseThemeData(FMenuThemeData);
FMenuThemeData := 0;
end;
if Open and UseThemes then begin
if (FTheme <> nil) and FTheme.Dark then
FMenuThemeData := OpenThemeData(Handle, 'DarkMode::Menu');
if FMenuThemeData = 0 then
FMenuThemeData := OpenThemeData(Handle, 'Menu');
end;
end;
end.