1
0
mirror of https://github.com/winscp/winscp.git synced 2026-10-24 02:28:55 +03:00
Files
winscp/source/packages/my/ListViewColProperties.pas
2025-03-26 01:24:53 +01:00

747 lines
21 KiB
ObjectPascal

unit ListViewColProperties;
interface
uses
Classes, ComCtrls, Contnrs;
type
TCustomListViewColProperty = class(TObject)
Alignment: TAlignment;
Caption: string;
Width: Integer;
MaxWidth: Integer;
MinWidth: Integer;
Visible: Boolean;
Order: Integer;
constructor Create(AOrder: Integer);
end;
type
TCustomListViewColProperties = class(TPersistent)
private
FChanged: Boolean;
FOnChange: TNotifyEvent;
FUpdating: Integer;
FProperties: TObjectList;
FCreated: Boolean;
function GetColumns: TListColumns;
function GetCount: Integer;
function GetOrderStr: string;
procedure CheckBounds(Index: Integer);
procedure SetWidthsStr(Value: string; PixelsPerInch: Integer);
function GetWidthsStr: string;
procedure SetOrderStr(Value: string);
protected
FListView: TCustomListView;
FListViewManaged: Boolean;
FConstraintsInitialized: Boolean;
function GetAlignments(Index: Integer): TAlignment;
function GetParamsStr: string; virtual;
function GetVisible(Index: Integer): Boolean;
function GetWidths(Index: Integer): Integer;
procedure SetAlignments(Index: Integer; Value: TAlignment);
procedure SetVisibleInternal(Index: Integer; Value: Boolean; SaveWidth: Boolean);
procedure SetVisible(Index: Integer; Value: Boolean);
procedure SetWidths(Index: Integer; Value: Integer);
function GetCaptions(Index: Integer): string;
procedure Changed; virtual;
procedure SetCaptions(Index: Integer; Value: string); virtual;
procedure SetParamsStr(Value: string); virtual;
procedure UpdateListView;
procedure UpdateFromListView;
procedure UpdateOrderFromListView;
procedure UpdateListViewOrder;
function GetProperties(Index: Integer): TCustomListViewColProperty;
function GetIndexByOrder(Order: Integer): Integer;
function ColumnsExists: Boolean;
procedure SetRuntimeVisible(Index: Integer; Value: Boolean; SaveWidth: Boolean);
function GetColumn(Index: Integer): TListColumn;
procedure CreateProperties(ACount: Integer);
function DefaultConstraint(Value: Integer; Visible: Boolean; Def: Integer): Integer;
property Columns: TListColumns read GetColumns stored False;
public
constructor Create(ListView: TCustomListView; ColCount: Integer);
destructor Destroy; override;
procedure EndUpdate;
procedure BeginUpdate;
procedure ListViewWndCreated;
procedure ListViewWndDestroying;
procedure ListViewWndDestroyed;
procedure ChangeScale(M, D: Integer);
property Count: Integer read GetCount stored False;
property Alignments[Index: Integer]: TAlignment read GetAlignments write SetAlignments;
property Captions[Index: Integer]: string read GetCaptions write SetCaptions;
property Widths[Index: Integer]: Integer read GetWidths write SetWidths;
property Visible[Index: Integer]: Boolean read GetVisible write SetVisible;
procedure RecreateColumns;
property OnChange: TNotifyEvent read FOnChange write FOnChange;
property ParamsStr: string read GetParamsStr write SetParamsStr stored False;
end; { TCustomListViewColProperties }
type
TListViewColProperties = class(TCustomListViewColProperties)
published
end; { TListViewColProperties }
implementation
uses
SysUtils, CommCtrl, Windows, PasTools, Controls, Forms;
const
DefaultListViewMaxWidth = 1000;
DefaultListViewMinWidth = 20;
{ TODO : V ListView zamezit zmenu velikosti neviditelnych sloupecku }
constructor TCustomListViewColProperty.Create(AOrder: Integer);
begin
Alignment := taLeftJustify;
Caption := '';
Width := 50;
Visible := True;
Order := AOrder;
end;
{ TCustomListViewColProperties }
constructor TCustomListViewColProperties.Create(
ListView: TCustomListView; ColCount: Integer);
var
ACount: Integer;
begin
// This contructor (and constructors of descendants)
// is only even called from implementations of
// TCustomNortonLikeListView.NewColProperties
inherited Create;
FConstraintsInitialized := False;
FCreated := False;
FUpdating := 0;
FChanged := False;
// ColCount is not 0 for file panels (TDirView and TCustomUnixDirView).
// It is 0 otherwise.
FListViewManaged := (ColCount = 0);
FListView := ListView;
FProperties := TObjectList.Create;
if FListViewManaged then ACount := GetColumns.Count
else ACount := ColCount;
CreateProperties(ACount);
if not Assigned(FListView) then
raise Exception.Create('NIL ListView pointer.');
end;
destructor TCustomListViewColProperties.Destroy;
begin
inherited;
FProperties.Free;
end;
procedure TCustomListViewColProperties.SetWidthsStr(Value: string; PixelsPerInch: Integer);
var
ColStr: string;
Index: Integer;
NeedInvalidate, NewVisible: Boolean;
NewWidth: Integer;
begin
Index := 0;
NeedInvalidate := False;
BeginUpdate;
try
while (Value <> '') and (Index < Count) do
begin
ColStr := CutToChar(Value, ';', True);
NewWidth := LoadDimension(StrToInt(CutToChar(ColStr, ',', True)), PixelsPerInch, FListView);
Widths[Index] := NewWidth;
NewVisible := Boolean(StrToInt(CutToChar(ColStr, ',', True)));
if Visible[Index] <> NewVisible then
begin
// As we are within BeginUpdate guard, the width set just above is not propadated to WinAPI.
// As reading width retrieves it from WinAPI (if the control is already allocated), we actually save wrong width.
SetVisibleInternal(Index, NewVisible, False);
NeedInvalidate := True;
end;
Inc(Index);
end;
finally
EndUpdate;
end;
// When visibility changes (particularly while reseting layout) redraw is needed
// (Invalidate is called in SetVisible too, but maybe it has no effect there, because it is within BeginUpdate/EndUpdate)
if NeedInvalidate and FListView.HandleAllocated then
FListView.Invalidate;
end;
function TCustomListViewColProperties.GetWidthsStr: string;
var
Index: Integer;
begin
Result := '';
for Index := 0 to Count-1 do
begin
Result := Format('%s;%d,%d', [Result, SaveDimension(Widths[Index]), Integer(Visible[Index])]);
end;
Delete(Result, 1, 1);
end;
procedure TCustomListViewColProperties.BeginUpdate;
begin
Columns.BeginUpdate;
Inc(FUpdating);
end;
procedure TCustomListViewColProperties.EndUpdate;
begin
Columns.EndUpdate;
Dec(FUpdating);
if FUpdating = 0 then
begin
// call Changed() even when FChange is false
Changed;
FChanged := False;
end;
end;
procedure TCustomListViewColProperties.Changed;
begin
if FUpdating > 0 then FChanged := True
else
if Assigned(FOnChange) then FOnChange(Self);
end;
procedure TCustomListViewColProperties.CheckBounds(Index: Integer);
begin
if (Index < 0) or (Index >= Count) then
raise Exception.Create('Index out of bounds.');
end;
function TCustomListViewColProperties.GetProperties(Index: Integer): TCustomListViewColProperty;
begin
Result := TCustomListViewColProperty(FProperties.Items[Index]);
end;
function TCustomListViewColProperties.GetIndexByOrder(Order: Integer): Integer;
var
I: Integer;
begin
for I := 0 to Count - 1 do
begin
if GetProperties(I).Order = Order then
begin
Result := I;
Exit;
end;
end;
raise Exception.Create('Column order out of bounds');
end;
function TCustomListViewColProperties.ColumnsExists: Boolean;
begin
Result := FListView.HandleAllocated;
if Result and (not FCreated) and (not FListViewManaged) then
UpdateListView;
end;
procedure TCustomListViewColProperties.SetAlignments(Index: Integer; Value: TAlignment);
begin
CheckBounds(Index);
if Alignments[Index] <> Value then
begin
GetProperties(Index).Alignment := Value;
if ColumnsExists then GetColumn(Index).Alignment := Value;
Changed;
end;
end;
procedure TCustomListViewColProperties.SetCaptions(Index: Integer; Value: string);
begin
CheckBounds(Index);
if Captions[Index] <> Value then
begin
if ColumnsExists then GetColumn(Index).Caption := Value
else GetProperties(Index).Caption := Value;
Changed;
end;
end;
function TCustomListViewColProperties.GetAlignments(Index: Integer): TAlignment;
begin
CheckBounds(Index);
if ColumnsExists then Result := GetColumn(Index).Alignment
else Result := GetProperties(Index).Alignment;
end;
function TCustomListViewColProperties.GetCaptions(Index: Integer): string;
begin
CheckBounds(Index);
if ColumnsExists then Result := GetColumn(Index).Caption
else Result := GetProperties(Index).Caption;
end;
procedure TCustomListViewColProperties.SetOrderStr(Value: string);
var
Order, Index: Integer;
Properties: TCustomListViewColProperty;
STemp: string;
Phase: Boolean;
begin
BeginUpdate;
try
for Index := 0 to Count - 1 do
GetProperties(Index).Order := -1;
// First order invisible columns (not True), then visible (not not True)
Phase := True;
Order := 0;
repeat
Phase := not Phase;
STemp := Value;
while (STemp <> '') and (Order < Count) do
begin
Index := StrToInt(CutToChar(STemp, ';', True));
Properties := GetProperties(Index);
if (Properties.Visible = Phase) and
(Properties.Order < 0) { robustness } then
begin
Properties.Order := Order;
Inc(Order);
end;
end;
// add missing columns from the same visibility class
for Index := 0 to Count - 1 do
begin
Properties := GetProperties(Index);
if (Properties.Visible = Phase) and
(Properties.Order < 0) then
begin
Properties.Order := Order;
Inc(Order);
end;
end;
until Phase;
if ColumnsExists then
UpdateListViewOrder;
finally
EndUpdate;
end;
end;
procedure TCustomListViewColProperties.SetParamsStr(Value: string);
var
S: string;
WidthsStr: string;
OrderStr: string;
PixelsPerInch: Integer;
begin
// TFileFindDialog uses / as separator of its settings
S := CutToChar(Value, '|', True);
WidthsStr := CutToChar(S, '@', True);
PixelsPerInch := LoadPixelsPerInch(S, FListView);
SetWidthsStr(WidthsStr, PixelsPerInch);
// Have to set order after visibility, otherwise we lost ordering of columns that are invisible by default,
// but visible by configuration (as they would get ordered to the front)
OrderStr := CutToChar(Value, '|', True);
SetOrderStr(OrderStr);
end;
procedure TCustomListViewColProperties.SetVisibleInternal(Index: Integer; Value: Boolean; SaveWidth: Boolean);
var
I: Integer;
Properties: TCustomListViewColProperty;
begin
CheckBounds(Index);
if Visible[Index] <> Value then
begin
Properties := GetProperties(Index);
if ColumnsExists then
UpdateOrderFromListView;
if Value then
begin
// shown column is moved to the back
for I := 0 to Count - 1 do
begin
if GetProperties(I).Order > Properties.Order then
Dec(GetProperties(I).Order);
end;
Properties.Order := Count - 1;
if ColumnsExists then
UpdateListViewOrder;
// show only after reordering column
Properties.Visible := True;
if ColumnsExists then
SetRuntimeVisible(Index, True, SaveWidth);
end
else
begin
// hide before reordering column
Properties.Visible := False;
if ColumnsExists then
SetRuntimeVisible(Index, False, SaveWidth);
// hidden column is moved to the front,
// unless column to the left is not hidden already
// (or unless it is first already, in which case the
// condition in the loop is never satisfied)
if (Properties.Order > 0) and
GetProperties(GetIndexByOrder(Properties.Order - 1)).Visible then
begin
for I := 0 to Count - 1 do
begin
if GetProperties(I).Order < Properties.Order then
Inc(GetProperties(I).Order);
end;
Properties.Order := 0;
end;
if ColumnsExists then
UpdateListViewOrder;
end;
Changed;
// It does not refresh itself at last one on Win7 when DoubleBuffered
if FListView.HandleAllocated then
FListView.Invalidate;
end;
end;
procedure TCustomListViewColProperties.SetVisible(Index: Integer; Value: Boolean);
begin
SetVisibleInternal(Index, Value, True);
end;
procedure TCustomListViewColProperties.SetRuntimeVisible(
Index: Integer; Value: Boolean; SaveWidth: Boolean);
var
Properties: TCustomListViewColProperty;
begin
// This is probably only ever called from file panels (DirViews)
// as other uses ("sychronization checklist" and "file find")
// have FListViewManaged = False and never change Visible property
// (though user can hide some columns manually in configuration storage)
with GetColumn(Index) do
begin
Properties := GetProperties(Index);
if Value then
begin
MaxWidth := Properties.MaxWidth;
MinWidth := Properties.MinWidth;
Width := Properties.Width;
end
else
begin
if SaveWidth then
Properties.Width := Width;
MaxWidth := 1;
MinWidth := 0;
Width := 0
end;
end;
end;
procedure TCustomListViewColProperties.SetWidths(Index: Integer; Value: Integer);
var
Properties: TCustomListViewColProperty;
begin
CheckBounds(Index);
Properties := GetProperties(Index);
if (Properties.MinWidth > 0) and (Value < Properties.MinWidth) then
begin
Value := Properties.MinWidth;
end
else
if (Properties.MaxWidth > 0) and (Value > Properties.MaxWidth) then
begin
Value := Properties.MaxWidth;
end;
if Widths[Index] <> Value then
begin
Properties.Width := Value;
if ColumnsExists and Visible[Index] then GetColumn(Index).Width := Value;
Changed;
end;
end;
function TCustomListViewColProperties.GetColumns: TListColumns;
begin
Result := TListView(FListView).Columns;
end;
function TCustomListViewColProperties.GetColumn(Index: Integer): TListColumn;
begin
Result := Columns[Index];
end;
function TCustomListViewColProperties.GetCount: Integer;
begin
Result := FProperties.Count;
end;
function TCustomListViewColProperties.GetOrderStr: string;
var
Index: Integer;
begin
Result := '';
if ColumnsExists then
UpdateOrderFromListView;
for Index := 0 to Count - 1 do
Result := Format('%s;%d', [Result, GetIndexByOrder(Index)]);
Delete(Result, 1, 1);
end;
function TCustomListViewColProperties.GetParamsStr: string;
begin
// WORKAROUND
// Adding an additional semicolon after the list,
// to ensure that old versions that did not expect the pixels-per-inch part,
// stop at the semicolon, otherwise they try to parse the
// "last-column-width|pixels-per-inch" as integer and throw.
// For the other instance of this hack, see GetListViewStr.
// The new pixels-per-inch part is inserted after the widths part
// as parsing of this was always robust to stop at "count" elements,
// what order part was not (due to its logic of skipping hidden columns)
Result := Format('%s;@%s|%s', [GetWidthsStr, SavePixelsPerInch(FListView), GetOrderStr]);
end;
function TCustomListViewColProperties.GetVisible(Index: Integer): Boolean;
begin
CheckBounds(Index);
Result := GetProperties(Index).Visible;
end;
function TCustomListViewColProperties.GetWidths(Index: Integer): Integer;
begin
CheckBounds(Index);
if ColumnsExists and Visible[Index] then
begin
Result := GetColumn(Index).Width;
end
else
begin
Result := GetProperties(Index).Width;
end;
end;
procedure TCustomListViewColProperties.RecreateColumns;
var
Copy: TListColumns;
begin
Copy := TListColumns.Create(nil);
try
Copy.Assign(Columns);
Columns.Assign(Copy);
finally
Copy.Free;
end;
end;
procedure TCustomListViewColProperties.CreateProperties(ACount: Integer);
var
Index: Integer;
Properties: TCustomListViewColProperty;
begin
for Index := 0 to ACount - 1 do
begin
Properties := TCustomListViewColProperty.Create(Index);
FProperties.Add(Properties);
end;
end;
function TCustomListViewColProperties.DefaultConstraint(Value: Integer; Visible: Boolean; Def: Integer): Integer;
begin
if (Value > 0) and Visible then Result := Value
else Result := Def;
end;
procedure TCustomListViewColProperties.ListViewWndCreated;
var
Index: Integer;
Properties: TCustomListViewColProperty;
Column: TListColumn;
begin
if FListViewManaged then
begin
if (FProperties.Count = 0) and (Columns.Count > 0) then
CreateProperties(Columns.Count);
UpdateFromListView;
end
else
begin
UpdateListView;
end;
Assert(ColumnsExists);
for Index := 0 to Count - 1 do
begin
Column := GetColumn(Index);
Properties := GetProperties(Index);
if not FConstraintsInitialized then
begin
Properties.MaxWidth := DefaultConstraint(Column.MaxWidth, Properties.Visible, DefaultListViewMaxWidth);
Properties.MinWidth := DefaultConstraint(Column.MinWidth, Properties.Visible, DefaultListViewMinWidth);
end;
// To apply the default constraints to columns that do not have their own
if Properties.Visible then
begin
Column.MaxWidth := Properties.MaxWidth;
if Column.Width > Column.MaxWidth then Column.Width := Column.MaxWidth;
Column.MinWidth := Properties.MinWidth;
if Column.Width < Column.MinWidth then Column.Width := Column.MinWidth;
end;
end;
FConstraintsInitialized := True;
end;
procedure TCustomListViewColProperties.ListViewWndDestroying;
begin
UpdateFromListView;
end;
procedure TCustomListViewColProperties.ListViewWndDestroyed;
begin
if not FListViewManaged then
FCreated := False;
end;
procedure TCustomListViewColProperties.ChangeScale(M, D: Integer);
var
Index: Integer;
Properties: TCustomListViewColProperty;
Column: TListColumn;
begin
for Index := 0 to Count - 1 do
begin
Properties := GetProperties(Index);
// This is not perfect, as the constraints apply on re-scaled width before they are re-scaled themselves
if Properties.Visible and ColumnsExists then
begin
Column := GetColumn(Index);
Column.MaxWidth := MulDiv(Column.MaxWidth, M, D);
Column.MinWidth := MulDiv(Column.MinWidth, M, D);
end
else
begin
Properties.MaxWidth := MulDiv(Properties.MaxWidth, M, D);
Properties.MinWidth := MulDiv(Properties.MinWidth, M, D);
end;
end;
end;
procedure TCustomListViewColProperties.UpdateListViewOrder;
var
Index: Integer;
Properties: TCustomListViewColProperty;
Temp: array of Integer;
begin
SetLength(Temp, Count);
// Seemingly useless,
// but probably only because we swallow HDN_ENDDRAG in TCustomIEListView.WMNotify,
// what prevents VLC from actually reordering columns collection
ListView_GetColumnOrderArray(FListView.Handle, Count, PInteger(Temp));
for Index := 0 to Count - 1 do
begin
Properties := GetProperties(Index);
Temp[Properties.Order] := Index;
end;
ListView_SetColumnOrderArray(FListView.Handle, Count, PInteger(Temp));
end;
procedure TCustomListViewColProperties.UpdateListView;
var
Index: Integer;
Column: TListColumn;
Properties: TCustomListViewColProperty;
begin
// Only called when FListViewManaged = False
BeginUpdate;
try
for Index := 0 to Count-1 do
begin
if Index < Columns.Count then
Column := GetColumn(Index)
else
Column := Columns.Add;
Properties := GetProperties(Index);
Column.Alignment := Properties.Alignment;
Column.Caption := Properties.Caption;
SetRuntimeVisible(Index, Properties.Visible, False);
end;
UpdateListViewOrder;
finally
FCreated := True;
EndUpdate;
end;
end;
procedure TCustomListViewColProperties.UpdateOrderFromListView;
var
Index: Integer;
Temp: array of Integer;
begin
SetLength(Temp, Count);
ListView_GetColumnOrderArray(FListView.Handle, Count, PInteger(Temp));
for Index := 0 to Count - 1 do
begin
GetProperties(Temp[Index]).Order := Index;
end;
end;
procedure TCustomListViewColProperties.UpdateFromListView;
var
Index: Integer;
Column: TListColumn;
Properties: TCustomListViewColProperty;
begin
Assert(FProperties.Count = Columns.Count);
for Index := 0 to Count-1 do
begin
Column := GetColumn(Index);
Properties := GetProperties(Index);
Properties.Alignment := Column.Alignment;
Properties.Caption := Column.Caption;
if Properties.Visible then
begin
Properties.Width := Column.Width;
if Column.MaxWidth > 0 then
Properties.MaxWidth := Column.MaxWidth;
if Column.MinWidth > 0 then
Properties.MinWidth := Column.MinWidth;
end;
end;
UpdateOrderFromListView;
end;
end.