{
   Double Commander
   -------------------------------------------------------------------------
   Control that shows drives list and allows selecting a drive.

   Copyright (C) 2009-2018  Alexander Koblov (alexx2000@mail.ru)
   Copyright (C) 2009-2011  Przemyslaw Nagay (cobines@gmail.com)

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, 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 General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program. If not, see <http://www.gnu.org/licenses/>.
}

unit uDrivesList;

{$mode objfpc}{$H+}

{$IFDEF MSWINDOWS}
    {$DEFINE ForceVirtualKeysShortcuts}
    {$DEFINE FileCaseInsensitive}
{$ENDIF}

{$IFDEF DARWIN}
    {$DEFINE FileCaseInsensitive}
{$ENDIF}

interface

uses
  Classes, SysUtils, Grids, Controls, LCLType,
  uFilePanelSelect, uDrive;

type
  TDriveSelected = procedure (Sender: TObject; ADriveIndex: Integer;
    APanel: TFilePanelSelect) of object;

  { TDrivesListPopup }

  TDrivesListPopup = class(TStringGrid)
  private
    FDriveIconSize: Integer;
    FDrivesList: TDrivesList;
    FPanel: TFilePanelSelect;
    FShortCuts: array of TUTF8Char;
    FAllowSelectDummyRow: Boolean;
    FOnDriveSelected: TDriveSelected;
    FOnClose: TNotifyEvent;

    {en
       @param(ARow
              Row nr in the grid (LowestRow..HighestRow).)
    }
    function GetDriveIndexByRow(ARow: Integer): Integer;
    function GetDrivesCount: Integer;
    function GetLowestRow: Integer;
    function GetHighestRow: Integer;

    procedure PrepareCanvasEvent(Sender: TObject; aCol, {%H-}aRow: Integer;
      {%H-}aState: TGridDrawState);
    procedure SelectCellEvent(Sender: TObject; {%H-}aCol, aRow: Integer;
      var CanSelect: Boolean);
    procedure EnterEvent(Sender: TObject);
    procedure ExitEvent(Sender: TObject);
    procedure KeyDownEvent(Sender: TObject; var Key: Word; {%H-}Shift: TShiftState);
    procedure KeyPressEvent(Sender: TObject; var Key: Char);
{$IFNDEF ForceVirtualKeysShortcuts}
    procedure UTF8KeyPressEvent(Sender: TObject; var UTF8Key: TUTF8Char);
{$ENDIF}
    procedure SelectDrive(ADriveIndex: Integer);
    procedure DoDriveSelected(ADriveIndex: Integer);
    procedure ShowContextMenu(ADriveIndex: Integer; X, Y: Integer);
    procedure ContextMenuClosed(Sender: TObject);

    {en
       Checks if the given shortcut is assigned to a drive.
       If it is then that drive is selected.
       @returns(@true if shortcut found, @false otherwise.)
    }
    function CheckShortcut(AShortcut: TUTF8Char): Boolean;

    procedure Close;
    procedure UpdateCells;
    procedure UpdateSize;

    property LowestRow: Integer read GetLowestRow;
    property HighestRow: Integer read GetHighestRow;

  protected
    procedure DrawCell(aCol, aRow: Integer; aRect: TRect;
       aState: TGridDrawState); override;
    procedure MouseDown(Button: TMouseButton; {%H-}Shift: TShiftState;
      X, Y: Integer); override;
    procedure MouseMove({%H-}Shift: TShiftState; X, Y: Integer); override;
    procedure MouseUp({%H-}Button: TMouseButton; {%H-}Shift: TShiftState;
      X, Y: Integer); override;
    procedure Paint; override;

  public
    constructor Create(AOwner: TComponent; AParent: TWinControl); reintroduce;

    procedure UpdateDrivesList(ADrivesList: TDrivesList);
    procedure UpdateView;

    {en
       Shows the drive list.
       @param(AtPoint
              Position where to show the list.)
       @param(APanel
              For which panel the list is to be shown.)
       @param(ASelectedDriveIndex
              Which drive to pre-select (0..DrivesCount-1).)
    }
    procedure Show(AtPoint: TPoint; APanel: TFilePanelSelect;
                   ASelectedDriveIndex: Integer = -1);

    procedure SetFocus; override;

    property Panel: TFilePanelSelect read FPanel;
    property DrivesCount: Integer read GetDrivesCount;

    property OnDriveSelected: TDriveSelected read FOnDriveSelected write FOnDriveSelected;
    property OnClose: TNotifyEvent read FOnClose write FOnClose;
  end;

implementation

uses
  StdCtrls, Graphics, LCLProc, LazUTF8,
  uPixMapManager, uOSUtils, uDCUtils, uOSForms, uGlobs;

const
  DriveIconSize = 16;
  // One dummy row is added, which is not displayed and cannot be selected.
  // It is used to simulate having no selection in the grid, because the
  // TCustomGrid forces at least one row/cell to be selected or focused.
  DummyRows = 1;

constructor TDrivesListPopup.Create(AOwner: TComponent; AParent: TWinControl);
begin
  inherited Create(AOwner);

  ControlStyle := ControlStyle + [csNoFocus];
  Parent := AParent;

  FDrivesList := nil;
  FShortCuts := nil;
  FAllowSelectDummyRow := False;
  FOnDriveSelected := nil;
  FOnClose := nil;

  AllowOutboundEvents := False;
  AutoFillColumns := False;
  BorderStyle := bsNone;
  BorderWidth := 0;
  ExtendedSelect := False;
  Flat := False;
  FocusRectVisible := False;
  MouseWheelOption := mwGrid;
  Options := [goRowSelect, goThumbTracking];
  ScrollBars := ssNone;
  Visible := False;

  while Columns.Count < 5 do
    Columns.Add;
  RowCount := 0 + DummyRows;
  FixedCols := 0;
  FixedRows := 0;
  if DummyRows > 0 then
    RowHeights[FixedRows] := 1; // Every row must have Height > 0

  Color := clBtnFace;
  Font.Color := clWindowText;
  FDriveIconSize := AdjustIconSize(DriveIconSize, 96);

  OnPrepareCanvas := @PrepareCanvasEvent;
  OnSelectCell    := @SelectCellEvent;
  OnEnter         := @EnterEvent;
  OnExit          := @ExitEvent;
  OnKeyDown       := @KeyDownEvent;
  OnKeyPress      := @KeyPressEvent;
{$IFNDEF ForceVirtualKeysShortcuts}
  OnUTF8KeyPress  := @UTF8KeyPressEvent;
{$ENDIF}
end;

procedure TDrivesListPopup.UpdateDrivesList(ADrivesList: TDrivesList);
begin
  FDrivesList := ADrivesList;

  RowCount := LowestRow + ADrivesList.Count;
  Clean;
  SetLength(FShortCuts, ADrivesList.Count);

  // If currently visible update the grid.
  if IsVisible then
  begin
    UpdateCells;
    UpdateSize;
  end;
end;

procedure TDrivesListPopup.UpdateView;
begin
  Columns.Items[2].Visible := dlbShowLabel in gDrivesListButtonOptions;
  Columns.Items[3].Visible := dlbShowFileSystem in gDrivesListButtonOptions;
  Columns.Items[4].Visible := dlbShowFreeSpace in gDrivesListButtonOptions;
end;

procedure TDrivesListPopup.Show(AtPoint: TPoint; APanel: TFilePanelSelect;
                                ASelectedDriveIndex: Integer = -1);
begin
  UpdateCells;
  UpdateSize;

  FPanel := APanel;

  Left := AtPoint.X;
  Top := AtPoint.Y;
  Visible := True;

  ASelectedDriveIndex := LowestRow + ASelectedDriveIndex;
  if (ASelectedDriveIndex >= LowestRow) and (ASelectedDriveIndex <= HighestRow) then
    Row := ASelectedDriveIndex
  else
  begin
    FAllowSelectDummyRow := True;
    Row := FixedRows; // Select dummy row to clear selection
    FAllowSelectDummyRow := False;
  end;

  // Set focus using parent procedure.
  inherited SetFocus;
end;

procedure TDrivesListPopup.SetFocus;
begin
  // Empty - don't allow setting focus.
end;

procedure TDrivesListPopup.PrepareCanvasEvent(Sender: TObject; aCol, aRow: Integer;
  aState: TGridDrawState);
var
  ts: TTextStyle;
begin
  if aCol = 4 then
  begin
    // Right-align free space text in third column.
    ts := Canvas.TextStyle;
    ts.Alignment := taRightJustify;
    Canvas.TextStyle := ts;
  end
  else if aCol > 0 then
  begin
    // Left-align other columns (except column 0 which shows the icon).
    ts := Canvas.TextStyle;
    ts.Alignment := taLeftJustify;
    Canvas.TextStyle := ts;
  end;
end;

function TDrivesListPopup.GetDriveIndexByRow(ARow: Integer): Integer;
begin
  if (ARow >= LowestRow) and (ARow <= HighestRow) then
    Result := ARow - LowestRow
  else
    Result := -1;
end;

function TDrivesListPopup.GetDrivesCount: Integer;
begin
  Result := HighestRow - LowestRow + 1;
end;

function TDrivesListPopup.GetLowestRow: Integer;
begin
  Result := FixedRows + DummyRows;
end;

function TDrivesListPopup.GetHighestRow: Integer;
begin
  Result := RowCount - 1;
end;

procedure TDrivesListPopup.DrawCell(aCol, aRow: Integer; aRect: TRect;
  aState: TGridDrawState);
var
  Drive: PDrive;
  BitmapTmp: TBitmap;
begin
  if (aRow = FixedRows) and (DummyRows > 0) then
    // Don't draw the dummy row.
    Exit
  else if (aCol = 0) and (aRow >= LowestRow) then
  begin
    inherited;

    // Draw drive icon in the first column.

    Drive := FDrivesList.Items[GetDriveIndexByRow(aRow)];

    // get disk icon
    BitmapTmp := PixMapManager.GetDriveIcon(Drive, FDriveIconSize, Self.Color);

    if Assigned(BitmapTmp) then
    begin
      // Center icon in the cell.
      aRect.Left := aRect.Left + (ColWidths[aCol] - FDriveIconSize) div 2;
      aRect.Top := aRect.Top + (RowHeights[aRow] - FDriveIconSize) div 2;
      aRect.Width := FDriveIconSize;
      aRect.Height := FDriveIconSize;
      Canvas.StretchDraw(aRect, BitmapTmp);
      FreeAndNil(BitmapTmp);
    end;
  end
  else
  begin
    inherited;

    // Draw vertical lines separating cells, but only in columns other than first.
    with Canvas, aRect do
    begin
      MoveTo(Right - 1, Top);
      LineTo(Right - 1, Bottom);
    end;
  end;
end;

procedure TDrivesListPopup.MouseDown(Button: TMouseButton; Shift: TShiftState;
  X, Y: Integer);
var
  ACol, ARow: Integer;
begin
  // Totally override MouseDown (don't call inherited).

  if (X < 0) or (Y < 0) or (X >= Width) or (Y >= Height) then
    Close
  else
  begin
    MouseToCell(X, Y, ACol, ARow);
    if (ACol < 0) or (ARow < 0) then
      Close
    else
    begin
      case Button of
        mbLeft:
          SelectDrive(GetDriveIndexByRow(ARow));
        mbRight:
          ShowContextMenu(GetDriveIndexByRow(ARow), X, Y);
      end;
    end;
  end;
end;

procedure TDrivesListPopup.MouseMove(Shift: TShiftState; X, Y: Integer);
var
  ACol, ARow: Integer;
begin
  // Totally override MouseMove (don't call inherited).

  if (X < 0) or (Y < 0) or (X >= Width) or (Y >= Height) then
    Exit;

  MouseToCell(X, Y, ACol, ARow);
  if (ACol >= 0) and (ARow >= 0) then
    Row := ARow;
end;

procedure TDrivesListPopup.MouseUp(Button: TMouseButton; Shift: TShiftState;
  X, Y: Integer);
var
  ACol, ARow: Integer;
begin
  // Totally override MouseUp (don't call inherited).

  MouseToCell(X, Y, ACol, ARow);
  if (X < 0) or (Y < 0) or (X >= Width) or (Y >= Height) or (ACol < 0) or (ARow < 0) then
    Close;
end;

procedure TDrivesListPopup.Paint;
var
  ARect: TRect;
begin
{$IFDEF LCLQT}
  // In QT Frame3d draws filled rectangle, so it must be drawn before
  // or it would overwrite all the painting done below.
  ARect := Classes.Rect(0, 0, Width, Height);
  Canvas.Frame3d(ARect, 1, bvRaised);
{$ENDIF}

  inherited Paint;

{$IFNDEF LCLQT}
  // This draws empty frame rectangle.
  ARect := Classes.Rect(0, 0, Width, Height);
  Canvas.Frame3d(ARect, 1, bvRaised);
{$ENDIF}
end;

procedure TDrivesListPopup.SelectCellEvent(Sender: TObject; aCol, aRow: Integer;
  var CanSelect: Boolean);
begin
  // Don't allow selecting dummy row.
  if (not FAllowSelectDummyRow) and (DummyRows > 0) then
    CanSelect := aRow > FixedRows
  else
    CanSelect := True;
end;

procedure TDrivesListPopup.EnterEvent(Sender: TObject);
begin
  // Mouse capture is needed for detecting when mouse is clicked outside the control.
  // This also recaptures mouse if user switched to another application and back.
  MouseCapture := True;
end;

procedure TDrivesListPopup.ExitEvent(Sender: TObject);
begin
  Close;
end;

procedure TDrivesListPopup.KeyDownEvent(Sender: TObject; var Key: Word;
  Shift: TShiftState);
var
  Rect: TRect;
begin
  case Key of
    VK_HOME, VK_PRIOR:
      begin
        Row := LowestRow;
        Key := 0;
      end;
    VK_END, VK_NEXT:
      begin
        Row := HighestRow;
        Key := 0;
      end;
    VK_UP, VK_LEFT:
      begin
        if Row > LowestRow then
          Row := Row - 1
        // If dummy row selected then select the last row.
        else if Row = FixedRows then
          Row := HighestRow;
        Key := 0;
      end;
    VK_DOWN, VK_RIGHT:
      begin
        if Row < HighestRow then
          Row := Row + 1;
        Key := 0;
      end;
    VK_RETURN, VK_SELECT, VK_SPACE:
      begin
        SelectDrive(GetDriveIndexByRow(Row));
        Key := 0;
      end;
    VK_ESCAPE:
      begin
        Close;
        Key := 0;
      end;
    VK_APPS:
      begin
        Rect := CellRect(2, Row);
        ShowContextMenu(GetDriveIndexByRow(Row), Rect.Left, Rect.Top);
        Key := 0;
      end;
{$IFDEF ForceVirtualKeysShortcuts}
    VK_0..VK_9, VK_A..VK_Z:
      begin
        if (CheckShortcut(TUTF8Char(Char(Key)))) then
          Key := 0;
      end;
{$ENDIF}
  end;
end;

procedure TDrivesListPopup.KeyPressEvent(Sender: TObject; var Key: Char);
begin
  if CheckShortcut(TUTF8Char(Key)) then
    Key := #0;
end;

{$IFNDEF ForceVirtualKeysShortcuts}
procedure TDrivesListPopup.UTF8KeyPressEvent(Sender: TObject; var UTF8Key: TUTF8Char);
begin
  if CheckShortcut(UTF8Key) then
    UTF8Key := '';
end;
{$ENDIF}

procedure TDrivesListPopup.SelectDrive(ADriveIndex: Integer);
begin
  if (ADriveIndex >= 0) and (ADriveIndex < DrivesCount) then
  begin
    MouseCapture := False;
    DoDriveSelected(ADriveIndex);
    Close;
  end;
end;

procedure TDrivesListPopup.DoDriveSelected(ADriveIndex: Integer);
begin
  if Assigned(FOnDriveSelected) then
    FOnDriveSelected(Self, ADriveIndex, FPanel);
end;

procedure TDrivesListPopup.ShowContextMenu(ADriveIndex: Integer; X, Y: Integer);
var
  pt: TPoint;
begin
  if (ADriveIndex >= 0) and (ADriveIndex < FDrivesList.Count) then
  begin
    pt.X := X;
    pt.Y := Y;
    pt := ClientToScreen(pt);

    // Context menu usually captures mouse so we have to disable ours.
    MouseCapture := False;
    ShowDriveContextMenu(Self, FDrivesList[ADriveIndex], pt.X, pt.Y, @ContextMenuClosed);
  end;
end;

procedure TDrivesListPopup.ContextMenuClosed(Sender: TObject);
begin
  MouseCapture := True;
end;

function TDrivesListPopup.CheckShortcut(AShortcut: TUTF8Char): Boolean;
var
  i: Integer;
begin
{$IFDEF FileCaseInsensitive}
  AShortCut := UpperCase(AShortcut);
{$ENDIF}
  for i := 0 to Length(FShortCuts) - 1 do
  begin
    if FShortCuts[i] = AShortcut then
    begin
      SelectDrive(i);
      Exit(True);
    end;
  end;
  Result := False;
end;

procedure TDrivesListPopup.Close;
begin
  MouseCapture := False;
  Visible := False;

  if Assigned(FOnClose) then
    FOnClose(Self);
end;

procedure TDrivesListPopup.UpdateCells;
var
  I, RowNr : Integer;
  FreeSize, TotalSize: Int64;
  Drive: PDrive;
begin
  for I := 0 to FDrivesList.Count - 1 do
    begin
      Drive := FDrivesList[I];
      RowNr := LowestRow + I;

      if Length(Drive^.DisplayName) > 0 then
      begin
        Cells[1, RowNr] := Drive^.DisplayName;
{$IFDEF FileCaseInsensitive}
        FShortCuts[I] := UTF8Copy(UpperCase(Drive^.DisplayName), 1, 1);
{$ELSE}
        FShortCuts[I] := UTF8Copy(Drive^.DisplayName, 1, 1);
{$ENDIF}
      end
      else
      begin
        Cells[1, RowNr] := Drive^.Path;
        FShortCuts[I] := '';
      end;

      Cells[2, RowNr] := GetDriveLabelOrStatus(Drive);
      Cells[3, RowNr] := Drive^.FileSystem;

      // Display free space only for some drives
      // (removable, network, etc. may be slow).
      if (Drive^.DriveType in [dtHardDisk, dtOptical, dtRamDisk, dtRemovableUsb]) and
         IsAvailable(Drive, False) and
         GetDiskFreeSpace(Drive^.Path, FreeSize, TotalSize) then
      begin
        Cells[4, RowNr] :=
          Format('%s/%s', [cnvFormatFileSize(FreeSize, uoscHeader),
                           cnvFormatFileSize(TotalSize, uoscHeader)])
      end
      else if (Drive^.DriveSize > 0) then
      begin
        Cells[4, RowNr] := cnvFormatFileSize(Drive^.DriveSize, uoscHeader);
      end
    end; // for
end;

procedure TDrivesListPopup.UpdateSize;
var
  I : Integer;
  w, h: Integer;
begin
  // Needed for autosizing to work before the control is visible.
  HandleNeeded;

  AutoSizeColumns;

  // Add some space to the icon column.
  ColWidths[0] := FDriveIconSize + 8;

  // Add some space to other columns.
  for I := 1 to ColCount - 1 do
    ColWidths[I] := ColWidths[I] + 4;

  w := GridWidth;
  h := GridHeight;

  if DummyRows > 0 then
    Inc(h, RowHeights[FixedRows] + GridLineWidth);

  Width := w;
  Height := h;
end;

end.

