unit fViewOperations;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, StdCtrls, ExtCtrls, LCLType,
  ComCtrls, Buttons, LCLIntf, Menus, uFileSourceOperation, uOperationsManager,
  Themes;

type

  TViewBaseItem = class;
  TViewBaseItemClick = procedure(Item: TViewBaseItem; Button: TMouseButton; Shift: TShiftState; const Pt: TPoint) of object;
  TViewBaseItemContextMenu = procedure(Item: TViewBaseItem; const Point: TPoint) of object;
  TViewBaseItemSelected = procedure(Item: TViewBaseItem) of object;

  TViewOperationsStatusIcon = (vosiPlay, vosiPause, vosiHourglass);

  { TViewBaseItem }

  TViewBaseItem = class
  private
    FOnClick: TViewBaseItemClick;
    FOnContextMenu: TViewBaseItemContextMenu;
    FOnSelected: TViewBaseItemSelected;
    FTreeNode: TTreeNode;
    FText: String;
    FTextRect: TRect;
    procedure CalculateSizes(Canvas: TCanvas; NeedsStatusIcon, NeedsProgress: Boolean);
    procedure DrawProgress(Canvas: TCanvas; NodeRect: TRect; Progress: Double);
    procedure DrawStatusIcon(Canvas: TCanvas; NodeRect: TRect; Icon: TViewOperationsStatusIcon);
    procedure DrawThemedBackground(Canvas: TCanvas; Element: TThemedTreeview; ARect: TRect);
    procedure DrawThemedText(Canvas: TCanvas; Element: TThemedTreeview; NodeRect: TRect; Center: Boolean; AText: String);
    function GetStatusIconRect(NodeRect: TRect): TRect;
    function GetTextIndent: Integer; virtual; abstract;
    procedure UpdateView(Canvas: TCanvas); virtual; abstract;
  public
    constructor Create(ANode: TTreeNode); virtual;
    procedure Click(const Pt: TPoint; Button: TMouseButton; Shift: TShiftState); virtual;
    procedure Draw(Canvas: TCanvas; NodeRect: TRect); virtual; abstract;
    function GetBackgroundColor: TColor; virtual; abstract;
    procedure KeyDown(var Key: Word; Shift: TShiftState); virtual;
    procedure Selected; virtual;
    procedure StartPause; virtual; abstract;
    procedure Stop; virtual; abstract;
    property OnClick: TViewBaseItemClick read FOnClick write FOnClick;
    property OnContextMenu: TViewBaseItemContextMenu read FOnContextMenu write FOnContextMenu;
    property OnSelected: TViewBaseItemSelected read FOnSelected write FOnSelected;
  end;

  { TViewQueueItem }

  TViewQueueItem = class(TViewBaseItem)
  private
    FQueueIdentifier: TOperationsManagerQueueIdentifier;
    function GetTextIndent: Integer; override;
    procedure UpdateView(Canvas: TCanvas); override;
  public
    constructor Create(ANode: TTreeNode; AQueueId: TOperationsManagerQueueIdentifier); reintroduce;
    procedure Click(const Pt: TPoint; Button: TMouseButton; Shift: TShiftState); override;
    procedure Draw(Canvas: TCanvas; NodeRect: TRect); override;
    function GetBackgroundColor: TColor; override;
    procedure StartPause; override;
    procedure Stop; override;
  end;

  TViewOperationItem = class;

  { TViewOperationItem }

  TViewOperationItem = class(TViewBaseItem)
  private
    FOperationHandle: TOperationHandle;
    FProgress: Double;
    function GetTextIndent: Integer; override;
    procedure UpdateView(Canvas: TCanvas); override;
  public
    constructor Create(ANode: TTreeNode; AOperationHandle: TOperationHandle); reintroduce;
    procedure Click(const Pt: TPoint; Button: TMouseButton; Shift: TShiftState); override;
    procedure Draw(Canvas: TCanvas; NodeRect: TRect); override;
    function GetBackgroundColor: TColor; override;
    procedure StartPause; override;
    procedure Stop; override;
  end;

  { TfrmViewOperations }

  TfrmViewOperations = class(TForm)
    btnStop: TBitBtn;
    btnStartPause: TBitBtn;
    cbAlwaysOnTop: TCheckBox;
    lblUseDragDrop: TLabel;
    mnuCancelQueue: TMenuItem;
    mnuNewQueue: TMenuItem;
    mnuCancelOperation: TMenuItem;
    mnuPutFirstInQueue: TMenuItem;
    mnuPutLastInQueue: TMenuItem;
    mnuOperationShowDetached: TMenuItem;
    mnuQueue2: TMenuItem;
    mnuQueue3: TMenuItem;
    mnuQueue5: TMenuItem;
    mnuQueue4: TMenuItem;
    mnuQueue1: TMenuItem;
    mnuQueue0: TMenuItem;
    mnuQueue: TMenuItem;
    mnuQueueShowDetached: TMenuItem;
    pnlHeader: TPanel;
    pmQueuePopup: TPopupMenu;
    pnlTopHeader: TPanel;
    pmOperationPopup: TPopupMenu;
    tbPauseAll: TToggleBox;
    tvOperations: TTreeView;
    UpdateTimer: TTimer;

    procedure btnStopClick(Sender: TObject);
    procedure cbAlwaysOnTopChange(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure mnuCancelOperationClick(Sender: TObject);
    procedure mnuCancelQueueClick(Sender: TObject);
    procedure mnuNewQueueClick(Sender: TObject);
    procedure mnuPutFirstInQueueClick(Sender: TObject);
    procedure mnuPutLastInQueueClick(Sender: TObject);
    procedure mnuOperationShowDetachedClick(Sender: TObject);
    procedure mnuQueueShowDetachedClick(Sender: TObject);
    procedure OnOperationItemContextMenu(Item: TViewBaseItem; const Point: TPoint);
    procedure OnOperationItemSelected(Item: TViewBaseItem);
    procedure OnQueueContextMenu(Item: TViewBaseItem; const Point: TPoint);
    procedure OnQueueItemSelected(Item: TViewBaseItem);
    procedure OnUpdateTimer(Sender: TObject);
    procedure btnStartPauseClick(Sender: TObject);
    procedure mnuQueueNumberClick(Sender: TObject);
    procedure tbPauseAllChange(Sender: TObject);
    procedure tvOperationsCustomDrawItem(Sender: TCustomTreeView; Node: TTreeNode; State: TCustomDrawState; var DefaultDraw: Boolean);
    procedure tvOperationsDeletion(Sender: TObject; Node: TTreeNode);
    procedure tvOperationsDragDrop(Sender, Source: TObject; X, Y: Integer);
    procedure tvOperationsDragOver(Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean);
    procedure tvOperationsKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure tvOperationsMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
    procedure tvOperationsResize(Sender: TObject);
    procedure tvOperationsSelectionChanged(Sender: TObject);
  private
    FDraggedOperation: TOperationHandle;
    FMenuOperation: TOperationHandle;
    FMenuQueueIdentifier: TOperationsManagerQueueIdentifier;
    procedure CreateNodes;
    function GetFocusedItem: TViewBaseItem;
    procedure MoveWithinQueue(MoveToTop: Boolean);
    procedure SetFocusItem(AOperationHandle: TOperationHandle);
    procedure SetFocusItem(AQueueIdentifier: TOperationsManagerQueueIdentifier);
    procedure SetNewQueue(Item: TViewOperationItem; NewQueue: TOperationsManagerQueueIdentifier);
    procedure SetStartPauseCaption(SetPause: Boolean);
    procedure UpdateView(Item: TOperationsManagerItem; Event: TOperationManagerEvent);
    procedure UpdateSizes;
  end;

procedure ShowOperationsViewer;
procedure ShowOperationsViewer(AOperationHandle: TOperationHandle);
procedure ShowOperationsViewer(AQueueIdentifier: TOperationsManagerQueueIdentifier);

implementation

{$R *.lfm}

uses
  GraphMath, GraphType, Math,
  fFileOpDlg, uLng, uGlobs, uFileSourceOperationMisc;

const
  ExpandSignSize = 9;
  StatusIconPlay: array[0..2] of TPoint =
    ((x: 5; y: 1), (x: 13; y: 9), (x: 5; y: 17));
  StatusIconPause1: array[0..3] of TPoint =
    ((x: 3; y: 2), (x: 3; y: 16), (x: 7; y: 16), (x: 7; y: 2));
  StatusIconPause2: array[0..3] of TPoint =
    ((x: 10; y: 2), (x: 10; y: 16), (x: 14; y: 16), (x: 14; y: 2));
  StatusIconHourglass1: array[0..2] of TPoint =
    ((x: 3; y: 4), (x: 13; y: 4), (x: 8; y: 10));
  StatusIconHourglass2: array[0..2] of TPoint =
    ((x: 8; y: 10), (x: 13; y: 15), (x: 3; y: 15));
  StatusIconFrame: TRect = (Left: 0; Top: 0; Right: 18; Bottom: 19);
  StatusIconRightMargin = 5;
  ProgressHeight = 16;
  ProgressWidth = 150;
  ProgressHorizontalMargin = 5;
  FreeOperationTextIndent = 3;
  MarginTopBottom = 2;

var
  frmViewOperations: TfrmViewOperations = nil;
  ProgressRight: Integer;

procedure ShowOperationsViewer;
begin
  if not Assigned(frmViewOperations) then
    frmViewOperations := TfrmViewOperations.Create(Application);
  frmViewOperations.ShowOnTop;
end;

procedure ShowOperationsViewer(AOperationHandle: TOperationHandle);
begin
  ShowOperationsViewer;
  if AOperationHandle <> InvalidOperationHandle then
    frmViewOperations.SetFocusItem(AOperationHandle);
end;

procedure ShowOperationsViewer(AQueueIdentifier: TOperationsManagerQueueIdentifier);
begin
  ShowOperationsViewer;
  frmViewOperations.SetFocusItem(AQueueIdentifier);
end;

procedure ApplyProgress(var ARect: TRect; Progress: Double);
begin
  ARect.Right := ARect.Left + Round((ARect.Right - ARect.Left) * Progress);
end;

function MoveRect(aRect: TRect; DeltaX, DeltaY: Integer): TRect;
begin
  Result.Left := aRect.Left + DeltaX;
  Result.Right := aRect.Right + DeltaX;
  Result.Top := aRect.Top + DeltaY;
  Result.Bottom := aRect.Bottom + DeltaY;
end;

procedure DrawMovePolygon(Canvas: TCanvas; const Points: array of TPoint; DeltaX, DeltaY: Integer);
var
  CopyPoints: PPoint;
  i: Integer;
begin
  CopyPoints := GetMem(SizeOf(TPoint) * Length(Points));
  for i := 0 to Length(Points) - 1 do
  begin
    CopyPoints[i].x := Points[i].x + DeltaX;
    CopyPoints[i].y := Points[i].y + DeltaY;
  end;
  Canvas.Polygon(CopyPoints, Length(Points), True);
  FreeMem(CopyPoints);
end;

{ TViewBaseItem }

procedure TViewBaseItem.DrawThemedBackground(Canvas: TCanvas; Element: TThemedTreeview; ARect: TRect);
var
  Details: TThemedElementDetails;
begin
  Details := ThemeServices.GetElementDetails(Element);
  if ThemeServices.HasTransparentParts(Details) then
  begin
    Canvas.Brush.Color := GetBackgroundColor;
    Canvas.FillRect(ARect);
  end;
  ThemeServices.DrawElement(Canvas.Handle, Details, ARect, nil);
end;

procedure TViewBaseItem.DrawProgress(Canvas: TCanvas; NodeRect: TRect; Progress: Double);
var
  Details: TThemedElementDetails;
begin
  if Progress > 0 then
  begin
    NodeRect.Right  := NodeRect.Right - ProgressRight;
    NodeRect.Left   := NodeRect.Right - ProgressWidth;
    NodeRect.Top    := NodeRect.Top + (NodeRect.Bottom - NodeRect.Top - ProgressHeight) div 2;
    NodeRect.Bottom := NodeRect.Top + ProgressHeight;

    if ThemeServices.ThemesEnabled then
    begin
      Details := ThemeServices.GetElementDetails(tpBar);
      ThemeServices.DrawElement(Canvas.Handle, Details, NodeRect, nil);
      Details := ThemeServices.GetElementDetails(tpChunk);
      InflateRect(NodeRect, -2, -2);
      ApplyProgress(NodeRect, Progress);
      ThemeServices.DrawElement(Canvas.Handle, Details, NodeRect, nil);
    end
    else
    begin
      Canvas.Pen.Color := clWindowText;
      Canvas.Brush.Color := clForm;
      Canvas.RoundRect(NodeRect, 3, 3);
      Canvas.Brush.Color := clHighlight;
      ApplyProgress(NodeRect, Progress);
      Canvas.RoundRect(NodeRect, 3, 3);
    end;
  end;
end;

procedure TViewBaseItem.DrawStatusIcon(Canvas: TCanvas; NodeRect: TRect; Icon: TViewOperationsStatusIcon);
var
  IconRect: TRect;
begin
  Canvas.Brush.Color := GetBackgroundColor;
  Canvas.Pen.Color := clWindowText;

  IconRect := MoveRect(GetStatusIconRect(NodeRect), NodeRect.Left, NodeRect.Top);
  Canvas.Rectangle(IconRect);

  case Icon of
    vosiPlay:     // Paint "Play" triangle
      begin
        Canvas.Brush.Color := RGBToColor(0, 200, 0);
        DrawMovePolygon(Canvas, StatusIconPlay, IconRect.Left, IconRect.Top);
      end;
    vosiPause:    // Paint "Pause" double line
      begin
        Canvas.Brush.Color := RGBToColor(0, 0, 200);
        DrawMovePolygon(Canvas, StatusIconPause1, IconRect.Left, IconRect.Top);
        DrawMovePolygon(Canvas, StatusIconPause2, IconRect.Left, IconRect.Top);
      end;
    else          // Paint "Hourglass"
      begin
        Canvas.Brush.Color := RGBToColor(255, 255, 255);
        DrawMovePolygon(Canvas, StatusIconHourglass1, IconRect.Left, IconRect.Top);
        DrawMovePolygon(Canvas, StatusIconHourglass2, IconRect.Left, IconRect.Top);
      end;
  end;
end;

procedure TViewBaseItem.DrawThemedText(Canvas: TCanvas; Element: TThemedTreeview; NodeRect: TRect; Center: Boolean; AText: String);
var
  Details: TThemedElementDetails;
  Flags: Cardinal = DT_WORDBREAK or DT_NOPREFIX;
begin
  Details := ThemeServices.GetElementDetails(Element);
  if Center then
    Flags := Flags + DT_VCENTER;
  ThemeServices.DrawText(Canvas, Details, AText, NodeRect, Flags, 0);
end;

function TViewBaseItem.GetStatusIconRect(NodeRect: TRect): TRect;
begin
  Result := MoveRect(StatusIconFrame,
                     (NodeRect.Right - NodeRect.Left) - (StatusIconFrame.Right - StatusIconFrame.Left) - StatusIconRightMargin,
                     ((NodeRect.Bottom - NodeRect.Top) - (StatusIconFrame.Bottom - StatusIconFrame.Top)) div 2);
end;

constructor TViewBaseItem.Create(ANode: TTreeNode);
begin
  FTreeNode := ANode;
end;

procedure TViewBaseItem.CalculateSizes(Canvas: TCanvas; NeedsStatusIcon, NeedsProgress: Boolean);
var
  NodeRect: TRect;
  NeededHeight: Integer;
begin
  // Calculate available width for text.
  NodeRect := FTreeNode.DisplayRect(False);
  FTextRect := Rect(0, 0, 0, 0);
  FTextRect.Right := (NodeRect.Right - NodeRect.Left) - GetTextIndent -
                     (ProgressRight + ProgressWidth + ProgressHorizontalMargin);

  // Calculate text height.
  DrawText(Canvas.Handle, PChar(FText), Length(FText), FTextRect, DT_NOPREFIX + DT_CALCRECT + DT_WORDBREAK);

  // Take max of text, progress and status icon.
  NeededHeight := FTextRect.Bottom - FTextRect.Top;
  if NeedsProgress then
    NeededHeight := Max(NeededHeight, ProgressHeight);
  if NeedsStatusIcon then
    NeededHeight := Max(NeededHeight, StatusIconFrame.Bottom - StatusIconFrame.Top);
  Inc(NeededHeight, 2 * MarginTopBottom);

  FTextRect := MoveRect(FTextRect, NodeRect.Left + GetTextIndent, NodeRect.Top);
  FTreeNode.Height := NeededHeight;
end;

procedure TViewBaseItem.Click(const Pt: TPoint; Button: TMouseButton; Shift: TShiftState);
var
  Handled: Boolean = False;
begin
  case Button of
    mbRight:
      if Assigned(FOnContextMenu) then
      begin
        OnContextMenu(Self, Pt);
        Handled := True;
      end;
  end;
  if not Handled and Assigned(FOnClick) then
    FOnClick(Self, Button, Shift, Pt);
end;

procedure TViewBaseItem.KeyDown(var Key: Word; Shift: TShiftState);
var
  Rect: TRect;
  Point: TPoint;
begin
  case Key of
    VK_APPS:
      if Assigned(FOnContextMenu) then
      begin
        Rect := FTreeNode.DisplayRect(False);
        Point.x := Rect.Left + (Rect.Right - Rect.Left) div 2;
        Point.y := Rect.Top + (Rect.Bottom - Rect.Top) div 2;
        OnContextMenu(Self, Point);
        Key := 0;
      end;
    VK_SPACE:
      begin
        StartPause;
        Key := 0;
      end;
    VK_DELETE, VK_BACK:
      begin
        Stop;
        Key := 0;
      end;
  end;
end;

procedure TViewBaseItem.Selected;
begin
  if Assigned(FOnSelected) then
    FOnSelected(Self);
end;

{ TViewOperationItem }

constructor TViewOperationItem.Create(ANode: TTreeNode; AOperationHandle: TOperationHandle);
begin
  FOperationHandle := AOperationHandle;
  inherited Create(ANode);
end;

procedure TViewOperationItem.Click(const Pt: TPoint; Button: TMouseButton; Shift: TShiftState);
var
  Handled: Boolean = False;
  OpManItem: TOperationsManagerItem;
  NodeRect: TRect;
begin
  OpManItem := OperationsManager.GetItemByHandle(FOperationHandle);
  if Assigned(OpManItem) then
  begin
    case Button of
      mbLeft:
        if OpManItem.Queue.IsFree then
        begin
          NodeRect := FTreeNode.DisplayRect(False);
          if ((ssDouble in Shift) or PtInRect(GetStatusIconRect(NodeRect), Pt)) then
          begin
            StartPause;
            Handled := True;
          end;
        end;
    end;
  end;
  if not Handled then
    inherited Click(Pt, Button, Shift);
end;

procedure TViewOperationItem.Draw(Canvas: TCanvas; NodeRect: TRect);
var
  OpManItem: TOperationsManagerItem;
  Element: TThemedTreeview;
  Icon: TViewOperationsStatusIcon;
begin
  if FTreeNode.Selected then
    Element := ttItemSelected
  else
    Element := ttItemNormal;

  DrawThemedBackground(Canvas, Element, NodeRect);

  OpManItem := OperationsManager.GetItemByHandle(FOperationHandle);
  if Assigned(OpManItem) then
  begin
    DrawThemedText(Canvas, Element, FTextRect, True, FText);

    if OpManItem.Queue.IsFree then
    begin
      case OpManItem.Operation.State of
        fsosRunning:
          Icon := vosiPlay;
        fsosPaused:
          Icon := vosiPause;
        else
          Icon := vosiHourglass;
      end;
      DrawStatusIcon(Canvas, NodeRect, Icon);
    end;

    DrawProgress(Canvas, NodeRect, FProgress);
  end;
end;

function TViewOperationItem.GetBackgroundColor: TColor;
begin
  Result := FTreeNode.TreeView.BackgroundColor;
end;

procedure TViewOperationItem.StartPause;
var
  OpManItem: TOperationsManagerItem;
begin
  OpManItem := OperationsManager.GetItemByHandle(FOperationHandle);
  if Assigned(OpManItem) and OpManItem.Queue.IsFree then
    OpManItem.Operation.TogglePause;
end;

procedure TViewOperationItem.Stop;
var
  OpManItem: TOperationsManagerItem;
begin
  OpManItem := OperationsManager.GetItemByHandle(FOperationHandle);
  if Assigned(OpManItem) then
    OpManItem.Operation.Stop;
end;

procedure TViewOperationItem.UpdateView(Canvas: TCanvas);
var
  OpManItem: TOperationsManagerItem;
begin
  OpManItem := OperationsManager.GetItemByHandle(FOperationHandle);
  if Assigned(OpManItem) then
  begin
    FText := IntToStr(OpManItem.Handle) + ': ' +
             OpManItem.Operation.GetDescription(fsoddJobAndTarget);
    FProgress := OpManItem.Operation.Progress;
    if FProgress > 0 then
      FText := FText + ' - ' + GetProgressString(FProgress);
    FText := FText + GetOperationStateString(OpManItem.Operation.State);

    CalculateSizes(Canvas, OpManItem.Queue.IsFree, FProgress > 0);
  end
  else
  begin
    FText := '';
    FProgress := 0;
    FTreeNode.Height := 10;
  end;
end;

function TViewOperationItem.GetTextIndent: Integer;
begin
  Result := FTreeNode.DisplayExpandSignLeft;
  if FTreeNode.Level = 0 then
    Inc(Result, FreeOperationTextIndent)
  else
    Dec(Result, TTreeView(FTreeNode.TreeView).Indent * (FTreeNode.Level - 1));
end;

{ TViewQueueItem }

procedure TViewQueueItem.Click(const Pt: TPoint; Button: TMouseButton; Shift: TShiftState);
var
  Handled: Boolean = False;
  NodeRect: TRect;
begin
  case Button of
    mbLeft:
      begin
        NodeRect := FTreeNode.DisplayRect(False);
        if (ssDouble in Shift) or PtInRect(GetStatusIconRect(NodeRect), Pt) then
        begin
          StartPause;
          Handled := True;
        end;
      end;
  end;
  if not Handled then
    inherited Click(Pt, Button, Shift);
end;

constructor TViewQueueItem.Create(ANode: TTreeNode; AQueueId: TOperationsManagerQueueIdentifier);
begin
  FQueueIdentifier := AQueueId;
  inherited Create(ANode);
end;

procedure TViewQueueItem.Draw(Canvas: TCanvas; NodeRect: TRect);
var
  Element: TThemedTreeview;
  Queue: TOperationsManagerQueue;
  Icon: TViewOperationsStatusIcon;
begin
  if FTreeNode.Selected then
    Element := ttItemSelected
  else
    Element := ttItemSelectedNotFocus;
  DrawThemedBackground(Canvas, Element, NodeRect);

  Queue := OperationsManager.QueueByIdentifier[FQueueIdentifier];
  if Assigned(Queue) then
  begin
    DrawThemedText(Canvas, Element, FTextRect, True, FText);

    if Queue.Paused then
      Icon := vosiPause
    else
      Icon := vosiPlay;
    DrawStatusIcon(Canvas, NodeRect, Icon);
  end;
end;

function TViewQueueItem.GetBackgroundColor: TColor;
begin
  Result := FTreeNode.TreeView.BackgroundColor;
end;

function TViewQueueItem.GetTextIndent: Integer;
var
  ATreeView: TCustomTreeView;
begin
  Result := FTreeNode.DisplayExpandSignLeft;
  ATreeView := FTreeNode.TreeView;
  if ATreeView is TTreeView then
    Inc(Result, TTreeView(ATreeView).Indent);
end;

procedure TViewQueueItem.StartPause;
var
  Queue: TOperationsManagerQueue;
begin
  Queue := OperationsManager.QueueByIdentifier[FQueueIdentifier];
  if Assigned(Queue) then
    Queue.TogglePause;
end;

procedure TViewQueueItem.Stop;
var
  Queue: TOperationsManagerQueue;
begin
  Queue := OperationsManager.QueueByIdentifier[FQueueIdentifier];
  if Assigned(Queue) then
  begin
    Queue.Stop;
  end;
end;

procedure TViewQueueItem.UpdateView(Canvas: TCanvas);
var
  Queue: TOperationsManagerQueue;
begin
  Queue := OperationsManager.QueueByIdentifier[FQueueIdentifier];
  if Assigned(Queue) then
  begin
    FText := Queue.GetDescription(False);
    if Queue.Paused then
      FText := FText + GetOperationStateString(fsosPaused);

    CalculateSizes(Canvas, True, False);
  end
  else
  begin
    FText := '';
    FTreeNode.Height := 10;
  end;
end;

{ TfrmViewOperations }

procedure TfrmViewOperations.FormCreate(Sender: TObject);
begin
  InitPropStorage(Self);
  cbAlwaysOnTopChange(nil);

  FMenuOperation := InvalidOperationHandle;
  tvOperations.DoubleBuffered := True;
  DoubleBuffered := True;

  CreateNodes;

  OperationsManager.AddEventsListener(
    [omevOperationAdded, omevOperationRemoved, omevOperationMoved],
    @UpdateView);

  tvOperations.OnResize := @tvOperationsResize;
end;

procedure TfrmViewOperations.btnStopClick(Sender: TObject);
var
  Item: TViewBaseItem;
begin
  Item := GetFocusedItem;
  if Assigned(Item) then
    Item.Stop;
end;

procedure TfrmViewOperations.cbAlwaysOnTopChange(Sender: TObject);
begin
  if cbAlwaysOnTop.Checked then
  begin
    FormStyle := fsStayOnTop;
    ShowInTaskBar := stDefault;
  end
  else
  begin
    FormStyle := fsNormal;
    ShowInTaskBar := stAlways;
  end;
end;

procedure TfrmViewOperations.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
  CloseAction := caFree;
  frmViewOperations := nil;
end;

procedure TfrmViewOperations.FormDestroy(Sender: TObject);
begin
  OperationsManager.RemoveEventsListener(
    [omevOperationAdded, omevOperationRemoved, omevOperationMoved],
    @UpdateView);
end;

procedure TfrmViewOperations.mnuNewQueueClick(Sender: TObject);
var
  Item: TViewBaseItem;
  OpManItem: TOperationsManagerItem;
begin
  Item := GetFocusedItem;
  if Assigned(Item) and (Item is TViewOperationItem) then
  begin
    OpManItem := OperationsManager.GetItemByHandle(TViewOperationItem(Item).FOperationHandle);
    if Assigned(OpManItem) then
      OpManItem.MoveToNewQueue;
  end;
end;

procedure TfrmViewOperations.mnuPutFirstInQueueClick(Sender: TObject);
begin
  MoveWithinQueue(True);
end;

procedure TfrmViewOperations.mnuPutLastInQueueClick(Sender: TObject);
begin
  MoveWithinQueue(False);
end;

procedure TfrmViewOperations.mnuOperationShowDetachedClick(Sender: TObject);
var
  OpManItem: TOperationsManagerItem;
begin
  OpManItem := OperationsManager.GetItemByHandle(FMenuOperation);
  if Assigned(OpManItem) and OpManItem.Queue.IsFree then
    TfrmFileOp.ShowFor(OpManItem.Handle, [opwoIfExistsBringToFront]);
end;

procedure TfrmViewOperations.OnOperationItemContextMenu(Item: TViewBaseItem; const Point: TPoint);
var
  i: Integer;
  PopupPoint: TPoint;
  OpManItem: TOperationsManagerItem;
begin
  FMenuOperation := (Item as TViewOperationItem).FOperationHandle;
  OpManItem := OperationsManager.GetItemByHandle(FMenuOperation);
  if Assigned(OpManItem) then
  begin
    for i := 0 to mnuQueue.Count - 1 do
      if i = OpManItem.Queue.Identifier then
        mnuQueue.Items[i].Checked := True
      else
        mnuQueue.Items[i].Checked := False;

    mnuOperationShowDetached.Enabled := OpManItem.Queue.IsFree;
    PopupPoint := tvOperations.ClientToScreen(Point);
    pmOperationPopup.PopUp(PopupPoint.x, PopupPoint.y);
  end;
end;

procedure TfrmViewOperations.OnOperationItemSelected(Item: TViewBaseItem);
var
  OpManItem: TOperationsManagerItem;
begin
  OpManItem := OperationsManager.GetItemByHandle(TViewOperationItem(Item).FOperationHandle);
  if Assigned(OpManItem) then
    begin
      SetStartPauseCaption(OpManItem.Operation.State in [fsosStarting, fsosRunning, fsosWaitingForConnection]);
      btnStartPause.Enabled := OpManItem.Queue.IsFree;
      btnStop.Enabled := True;
    end
  else
    begin
      btnStartPause.Enabled := False;
      btnStop.Enabled := False;
    end;
end;

procedure TfrmViewOperations.OnQueueContextMenu(Item: TViewBaseItem; const Point: TPoint);
var
  PopupPoint: TPoint;
begin
  FMenuQueueIdentifier := (Item as TViewQueueItem).FQueueIdentifier;
  PopupPoint := tvOperations.ClientToScreen(Point);
  pmQueuePopup.PopUp(PopupPoint.x, PopupPoint.y);
end;

procedure TfrmViewOperations.OnQueueItemSelected(Item: TViewBaseItem);
var
  Queue: TOperationsManagerQueue;
begin
  Queue := OperationsManager.QueueByIdentifier[TViewQueueItem(Item).FQueueIdentifier];
  if Assigned(Queue) then
    begin
      SetStartPauseCaption(not Queue.Paused);
      btnStartPause.Enabled := True;
    end
  else
    begin
      btnStartPause.Enabled := False;
    end;
  btnStop.Enabled := btnStartPause.Enabled;
end;

procedure TfrmViewOperations.OnUpdateTimer(Sender: TObject);
begin
  UpdateSizes;
  tvOperationsSelectionChanged(tvOperations);
  tvOperations.Invalidate;
end;

procedure TfrmViewOperations.mnuQueueNumberClick(Sender: TObject);
var
  NewQueueNumber: integer;
  Item: TViewBaseItem;
begin
  if TryStrToInt(Copy((Sender as TMenuItem).Name, 9, 1), NewQueueNumber) then
  begin
    Item := GetFocusedItem;
    if Assigned(Item) and (Item is TViewOperationItem) then
      SetNewQueue(TViewOperationItem(Item), NewQueueNumber);
  end;
end;

procedure TfrmViewOperations.mnuQueueShowDetachedClick(Sender: TObject);
var
  Queue: TOperationsManagerQueue;
begin
  Queue := OperationsManager.QueueByIdentifier[FMenuQueueIdentifier];
  if Assigned(Queue) then
    TfrmFileOp.ShowFor(Queue.Identifier, [opwoIfExistsBringToFront]);
end;

procedure TfrmViewOperations.tbPauseAllChange(Sender: TObject);
begin
  if tbPauseAll.State = cbChecked then
    OperationsManager.PauseAll
  else
    OperationsManager.UnPauseAll;
end;

procedure TfrmViewOperations.tvOperationsCustomDrawItem(Sender: TCustomTreeView; Node: TTreeNode; State: TCustomDrawState; var DefaultDraw: Boolean);
var
  Item: TViewBaseItem;
  NodeRect: TRect;

  procedure DrawExpandSign(MidX, MidY: integer; CollapseSign: boolean);
  const
    ExpandSignColor = clWindowText;
  var
    HalfSize, ALeft, ATop, ARight, ABottom: integer;
    Points: array [0..2] of TPoint;
    R: TRect;
  begin
    with Sender.Canvas do
    begin
      Brush.Color := clWindow;
      Pen.Color := ExpandSignColor;
      Pen.Style := psSolid;

      HalfSize := ExpandSignSize shr 1;
      if not Odd(ExpandSignSize) then
        dec(HalfSize);
      ALeft := MidX - HalfSize;
      ATop := MidY - HalfSize;
      ARight := ALeft + ExpandSignSize;
      ABottom := ATop + ExpandSignSize;

      // draw an arrow. down for collapse and right for expand
      R := Rect(ALeft, ATop, ARight, ABottom);
      if CollapseSign then
      begin
        // draw an arrow down
        Points[0] := Point(R.Left, MidY);
        Points[1] := Point(R.Right - 1, MidY);
        Points[2] := Point(MidX, R.Bottom - 1);
      end else
      begin
        // draw an arrow right
        Points[0] := Point(MidX - 1, ATop);
        Points[1] := Point(R.Right - 2, MidY);
        Points[2] := Point(MidX - 1, R.Bottom - 1);
      end;
      Polygon(Points, False);
    end;
  end;
var
  VertMid: Integer;
begin
  if not Assigned(Node.Data) then
    Exit;

  Item := TViewBaseItem(Node.Data);
  NodeRect := Node.DisplayRect(False);

  Item.Draw(Sender.Canvas, NodeRect);

  if tvOperations.ShowButtons and Node.HasChildren and ((tvoShowRoot in tvOperations.Options) or (Node.Parent <> nil)) then
  begin
    VertMid := (NodeRect.Top + NodeRect.Bottom) div 2;
    DrawExpandSign(Node.DisplayExpandSignLeft + tvOperations.Indent shr 1, VertMid, Node.Expanded);
  end;

  // draw separator
  if (tvoShowSeparators in tvOperations.Options) then
  begin
    Sender.Canvas.Pen.Color:=tvOperations.SeparatorColor;
    Sender.Canvas.MoveTo(NodeRect.Left,NodeRect.Bottom-1);
    Sender.Canvas.LineTo(NodeRect.Right,NodeRect.Bottom-1);
  end;

  DefaultDraw := False;
end;

procedure TfrmViewOperations.tvOperationsDeletion(Sender: TObject; Node: TTreeNode);
var
  Item: TViewBaseItem;
begin
  Item := TViewBaseItem(Node.Data);
  Node.Data := nil;
  Item.Free;
end;

procedure TfrmViewOperations.tvOperationsDragDrop(Sender, Source: TObject; X, Y: Integer);
var
  TargetNode: TTreeNode;
  NodeRect: TRect;
  TargetItem: TViewBaseItem;
  QueueItem: TViewQueueItem;
  OperItem: TViewOperationItem;
  SourceOpManItem, TargetOpManItem: TOperationsManagerItem;
  TargetQueue: TOperationsManagerQueue;
  TargetQueueId: TOperationsManagerQueueIdentifier;
  HitTopPart: Boolean;
begin
  if Source = tvOperations then
  begin
    SourceOpManItem := OperationsManager.GetItemByHandle(FDraggedOperation);
    if Assigned(SourceOpManItem) then
    begin
      TargetNode := tvOperations.GetNodeAt(X, Y);
      if not Assigned(TargetNode) then
      begin
        SourceOpManItem.MoveToNewQueue;
      end
      else
      begin
        NodeRect := TargetNode.DisplayRect(False);
        TargetItem := TViewBaseItem(TargetNode.Data);
        HitTopPart := Y - NodeRect.Top < (NodeRect.Bottom - NodeRect.Top) div 2;

        if TargetItem is TViewQueueItem then
        begin
          QueueItem := TViewQueueItem(TargetItem);
          if HitTopPart and
             (TargetNode = tvOperations.Items.GetFirstNode) and
             (QueueItem.FQueueIdentifier <> FreeOperationsQueueId) then
          begin
            // There are no free operations and item was dropped at the top of the list
            // on some queue. Create a free operations queue and move to it.
            TargetQueueId := FreeOperationsQueueId;
            TargetQueue := OperationsManager.GetOrCreateQueue(TargetQueueId);
          end
          else
          begin
            TargetQueueId := QueueItem.FQueueIdentifier;
            TargetQueue := OperationsManager.QueueByIdentifier[TargetQueueId];
          end;
          SourceOpManItem.SetQueue(TargetQueue);
        end
        else if (TargetItem is TViewOperationItem) and
                (FDraggedOperation <> TViewOperationItem(TargetItem).FOperationHandle) then
        begin
          OperItem := TViewOperationItem(TargetItem);
          TargetOpManItem := OperationsManager.GetItemByHandle(OperItem.FOperationHandle);
          if Assigned(TargetOpManItem) then
            SourceOpManItem.Move(TargetOpManItem.Handle, HitTopPart);
        end;
      end;
    end;
  end;
end;

procedure TfrmViewOperations.tvOperationsDragOver(Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean);
begin
  Accept := True;
end;

procedure TfrmViewOperations.tvOperationsKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
var
  Item: TViewBaseItem;
begin
  Item := GetFocusedItem;
  if Assigned(Item) then
    Item.KeyDown(Key, Shift);
end;

procedure TfrmViewOperations.tvOperationsMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  Node: TTreeNode;
  NodeRect: TRect;
begin
  FDraggedOperation := InvalidOperationHandle;
  Node := tvOperations.GetNodeAt(X, Y);
  if Assigned(Node) then
  begin
    NodeRect := Node.DisplayRect(False);
    TViewBaseItem(Node.Data).Click(Point(X - NodeRect.Left, Y - NodeRect.Top), Button, Shift);
    if TViewBaseItem(Node.Data) is TViewOperationItem then
      FDraggedOperation := TViewOperationItem(Node.Data).FOperationHandle;
  end;
end;

procedure TfrmViewOperations.tvOperationsResize(Sender: TObject);
begin
  UpdateSizes;
end;

procedure TfrmViewOperations.tvOperationsSelectionChanged(Sender: TObject);
var
  Node: TTreeNode;
begin
  Node := tvOperations.Selected;
  if Assigned(Node) then
    TViewBaseItem(Node.Data).Selected;
end;

function TfrmViewOperations.GetFocusedItem: TViewBaseItem;
var
  Node: TTreeNode;
begin
  Node := tvOperations.Selected;
  if Assigned(Node) then
    Result := TViewBaseItem(Node.Data)
  else
    Result := nil;
end;

procedure TfrmViewOperations.mnuCancelOperationClick(Sender: TObject);
var
  OpManItem: TOperationsManagerItem;
begin
  OpManItem := OperationsManager.GetItemByHandle(FMenuOperation);
  if Assigned(OpManItem) then
    OpManItem.Operation.Stop;
end;

procedure TfrmViewOperations.mnuCancelQueueClick(Sender: TObject);
var
  Queue: TOperationsManagerQueue;
begin
  Queue := OperationsManager.QueueByIdentifier[FMenuQueueIdentifier];
  if Assigned(Queue) then
    Queue.Stop;
end;

procedure TfrmViewOperations.MoveWithinQueue(MoveToTop: Boolean);
var
  Item: TViewBaseItem;
  OpManItem: TOperationsManagerItem;
begin
  Item := GetFocusedItem;
  if Assigned(Item) and (Item is TViewOperationItem) then
  begin
    OpManItem := OperationsManager.GetItemByHandle(TViewOperationItem(Item).FOperationHandle);
    if Assigned(OpManItem) then
    begin
      if OpManItem.Queue.Identifier <> FreeOperationsQueueId then
      begin
        if MoveToTop then
          OpManItem.MoveToTop
        else
          OpManItem.MoveToBottom;
      end;
    end;
  end;
end;

procedure TfrmViewOperations.SetFocusItem(AOperationHandle: TOperationHandle);
var
  Node: TTreeNode;
begin
  for Node in tvOperations.Items do
  begin
    if (TViewBaseItem(Node.Data) is TViewOperationItem) and
       (TViewOperationItem(Node.Data).FOperationHandle = AOperationHandle) then
    begin
      Node.Selected := True;
      Exit;
    end;
  end;
end;

procedure TfrmViewOperations.SetFocusItem(AQueueIdentifier: TOperationsManagerQueueIdentifier);
var
  Node: TTreeNode;
begin
  for Node in tvOperations.Items do
  begin
    if (TViewBaseItem(Node.Data) is TViewQueueItem) and
       (TViewQueueItem(Node.Data).FQueueIdentifier = AQueueIdentifier) then
    begin
      Node.Selected := True;
      Exit;
    end;
  end;
end;

procedure TfrmViewOperations.SetNewQueue(Item: TViewOperationItem; NewQueue: TOperationsManagerQueueIdentifier);
var
  OpManItem: TOperationsManagerItem;
begin
  OpManItem := OperationsManager.GetItemByHandle(Item.FOperationHandle);
  if Assigned(OpManItem) then
    OpManItem.SetQueue(OperationsManager.GetOrCreateQueue(NewQueue));
end;

procedure TfrmViewOperations.SetStartPauseCaption(SetPause: Boolean);
begin
  if SetPause then
    btnStartPause.Caption := rsDlgOpPause
  else
    btnStartPause.Caption := rsDlgOpStart;
end;

procedure TfrmViewOperations.btnStartPauseClick(Sender: TObject);
var
  Item: TViewBaseItem;
begin
  Item := GetFocusedItem;
  if Assigned(Item) then
    Item.StartPause;
end;

procedure TfrmViewOperations.CreateNodes;
  procedure AddOperations(Queue: TOperationsManagerQueue; QueueNode: TTreeNode);
  var
    OperIndex: Integer;
    OpManItem: TOperationsManagerItem;
    OperNode: TTreeNode;
    Item: TViewBaseItem;
  begin
    for OperIndex := 0 to Queue.Count - 1 do
    begin
      OpManItem := Queue.Items[OperIndex];
      OperNode := tvOperations.Items.AddChild(QueueNode, '');
      Item := TViewOperationItem.Create(OperNode, OpManItem.Handle);
      OperNode.Data := Item;
      Item.UpdateView(tvOperations.Canvas);
      Item.OnContextMenu := @OnOperationItemContextMenu;
      Item.OnSelected := @OnOperationItemSelected;
    end;
  end;
var
  QueueIndex: Integer;
  Queue: TOperationsManagerQueue;
  QueueNode: TTreeNode;
  Item: TViewBaseItem;
begin
  tvOperations.Items.Clear;

  // First add all free operations.
  Queue := OperationsManager.QueueByIdentifier[FreeOperationsQueueId];
  if Assigned(Queue) then
    AddOperations(Queue, nil);

  for QueueIndex := 0 to OperationsManager.QueuesCount - 1 do
  begin
    Queue := OperationsManager.QueueByIndex[QueueIndex];
    if Queue.Identifier <> FreeOperationsQueueId then
    begin
      QueueNode := tvOperations.Items.AddChild(nil, '');
      Item := TViewQueueItem.Create(QueueNode, Queue.Identifier);
      QueueNode.Data := Item;
      Item.UpdateView(tvOperations.Canvas);
      Item.OnContextMenu := @OnQueueContextMenu;
      Item.OnSelected := @OnQueueItemSelected;
      AddOperations(Queue, QueueNode);
    end;
  end;
end;

procedure TfrmViewOperations.UpdateSizes;
var
  Node: TTreeNode;
begin
  Node := tvOperations.Items.GetFirstNode;
  while Assigned(Node) do
  begin
    if Assigned(Node.Data) then
      TViewBaseItem(Node.Data).UpdateView(tvOperations.Canvas);
    Node := Node.GetNext;
  end;
end;

procedure TfrmViewOperations.UpdateView(Item: TOperationsManagerItem; Event: TOperationManagerEvent);
begin
  CreateNodes;
  tvOperations.Invalidate;
end;

initialization
  ProgressRight := ProgressHorizontalMargin + StatusIconRightMargin + (StatusIconFrame.Right - StatusIconFrame.Left);

end.

