{*********************************************************}
{*                  OVCTSELL.PAS 4.06                    *}
{*********************************************************}

{* ***** BEGIN LICENSE BLOCK *****                                            *}
{* Version: MPL 1.1                                                           *}
{*                                                                            *}
{* The contents of this file are subject to the Mozilla Public License        *}
{* Version 1.1 (the "License"); you may not use this file except in           *}
{* compliance with the License. You may obtain a copy of the License at       *}
{* http://www.mozilla.org/MPL/                                                *}
{*                                                                            *}
{* Software distributed under the License is distributed on an "AS IS" basis, *}
{* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License   *}
{* for the specific language governing rights and limitations under the       *}
{* License.                                                                   *}
{*                                                                            *}
{* The Original Code is TurboPower Orpheus                                    *}
{*                                                                            *}
{* The Initial Developer of the Original Code is TurboPower Software          *}
{*                                                                            *}
{* Portions created by TurboPower Software Inc. are Copyright (C)1995-2002    *}
{* TurboPower Software Inc. All Rights Reserved.                              *}
{*                                                                            *}
{* Contributor(s):                                                            *}
{*                                                                            *}
{* ***** END LICENSE BLOCK *****                                              *}

{$I OVC.INC}

{$B-} {Complete Boolean Evaluation}
{$I+} {Input/Output-Checking}
{$P+} {Open Parameters}
{$T-} {Typed @ Operator}
{.W-} {Windows Stack Frame}
{$X+} {Extended Syntax}

unit ovctsell;
  {-Table cell selection list class}

{Notes:

  The TOvcSelectionList class implements a data structure that stores which
  cells in a table are selected. The structure is implemented as an array
  of (sub)arrays, each element of the outer array pertaining to a single
  column. The items in each subarray are ranges of rows that are selected.
  Thus if the cells (in (row,col) format) (1,1)..(2,3) are all selected,
  the structure will look like this:
             Column   Value
             0        nil
             1        array with 1 element: range row 1 to row 2
             2        array with 1 element: range row 1 to row 2
             3        array with 1 element: range row 1 to row 2
             4        nil
             ...
  Adding a new selected cell at (4,2) would add an element to the third
  column's subarray: a range from row 4 to row 4.

  To check whether a cell is selected or not, get the element of the array
  pertaining to the column. If nil, the cell is not selected. If not,
  search through the ranges sequentially until the cell's row falls into a
  range.

  Adding a selected cell (or cells) will generally add a new row range
  element to the relevant column array, but it might cause row range
  mergings. Deselecting a cell (or cells) might cause range splittings and
  mergings.

  To aid in dynamic selection of cells, the class remembers the 'current
  range'. The table using this class will set the anchor cell address and
  then periodically set the new active cell; the difference between these
  is the current range. The class will move the current range to the above
  data structure when the anchor cell is set. IsCellSelected and
  HaveSelection will both look at this current range as well as the data
  structure. Note that the current range could be for selecting as well as
  deselecting cells: there's a flag to define which.

  One important assumption has been made. This is that generally there are
  'few' selections (obviously one can imagine the fanatical user who is
  determined to see 1000 separate disjoint selections, but normally there
  will be one selection to maybe half a dozen). In other words it will be
  more efficient to code a sequential search internally rather than a
  binary one, and other similar types of speed improvements have not been
  used.
}

interface

uses
  {$IFNDEF LCL} Windows, {$ELSE} LclIntf, {$ENDIF} SysUtils, OvcTCmmn;

type
  TOvcSelRowRange = packed record            {a row range}
    L, H : TRowNum;
  end;

  POvcSelRRArray = ^TOvcSelRRArray;   {an array of row ranges}
  TOvcSelRRArray = packed record
    RRCount : integer;
    RRTotal : integer;
    RRs     : array [0..(MaxInt div sizeof(TOvcSelRowRange))-2] of TOvcSelRowRange;
  end;

  POvcSelColArray = ^TOvcSelColArray; {an array of arrays of row ranges}
  TOvcSelColArray = array [0..(MaxInt div sizeof(POvcSelRRArray))-1] of POvcSelRRArray;

  TOvcSelectionList = class           {class to manage list of selected cells}
    protected {private}
      {.Z+}
      {even sized}
      slArray : POvcSelColArray;      {array of arrays of row ranges}
      slColCount : TColNum;           {number of columns in slArray}
      slColWithSelCount : TColNum;    {num of columns with at least 1 selected cell}
      slActiveCol : TColNum;          {current active cell-column}
      slActiveRow : TColNum;          {                   -row}
      slAnchorCol : TColNum;          {current anchor-column}
      slAnchorRow : TColNum;          {              -row}
      slRowCount : TRowNum;           {number of rows in slArray}
      slColMin : TColNum;             {current range-minimum column}
      slColMax : TColNum;             {             -maximum column}
      slRowMin : TRowNum;             {             -minimum row}
      slRowMax : TRowNum;             {             -maximum row}
      {odd sized}
      slSelecting : boolean;          {current range is for selection}
      slEmptyRange : boolean;         {current range is empty}
      {.Z-}
    protected
      {.Z+}
      procedure slDeselectCellRangeInCol(Row1, Row2 : TRowNum; ColNum : TColNum);
      procedure slSelectCellRangeInCol(Row1, Row2 : TRowNum; ColNum : TColNum);
      {.Z-}
    public
      constructor Create(RowCount : TRowNum; ColCount : TColNum);
        {-Create a new instance for RowCount rows & ColCount columns}
      destructor Destroy; override;
        {-Destroy the instance}

      procedure DeselectAll;
        {-Deselect all cells}
      procedure DeselectCell(RowNum : TRowNum; ColNum : TColNum);
        {-Deselect a single cell}
      procedure DeselectCellRange(FromRow : TRowNum; FromCol : TColNum;
                                  ToRow : TRowNum; ToCol : TColNum);
        {-Deselect a range of cells}
      procedure ExtendRange(RowNum : TRowNum; ColNum : TColNum;
                            IsSelecting : boolean);
        {-Extend/shrink the current range to RowNum, ColNum}
      function HaveSelection : boolean;
        {-Return true if at least one cell is selected}
      function IsCellSelected(RowNum : TRowNum; ColNum : TColNum) : boolean;
        {-Return true if specified cell is selected}
      procedure Iterate(SI : TSelectionIterator; ExtraData : pointer);
        {-Iterate through all the selection ranges calling SI for each}
      procedure SelectAll;
        {-Select all cells}
      procedure SelectCell(RowNum : TRowNum; ColNum : TColNum);
        {-Mark a single cell as selected}
      procedure SelectCellRange(FromRow : TRowNum; FromCol : TColNum;
                                ToRow : TRowNum; ToCol : TColNum);
        {-Mark a range of cells as selected}
      procedure SetColCount(ColCount : TColNum);
        {-Change the number of columns}
      procedure SetRangeAnchor(RowNum : TRowNum; ColNum : TColNum;
                               Action : TOvcTblSelectionType);
        {-Set the anchor cell; if Action is tstAdditional the current
          selection is stored, if not all DeselectAll is called}
      procedure SetRowCount(RowCount : TRowNum);
        {-Change the number of rows}
  end;

implementation

const
  RRArrayInc = 16;
  RRElemSize = sizeof(TOvcSelRowRange);

{===Helper routines==================================================}
function CalcRRArraySize(ElementCount : integer) : integer;
  {-Given a number of elements, calcs the memory block size}
  begin
    Result := (ElementCount * RRElemSize) + (2 * sizeof(integer));
  end;
{--------}
procedure AllocRRArray(var RRA : POvcSelRRArray);
  {-Allocates/grows a row range array}
  var
    NewTotal : integer;
    NewArray: POvcSelRRArray;
  begin
    {are we growing a current allocation?}
    if Assigned(RRA) then
      begin
        NewTotal := RRA^.RRTotal + RRArrayInc;
        NewArray := AllocMem(CalcRRArraySize(NewTotal));
        NewArray^.RRTotal := NewTotal;
        NewArray^.RRCount := RRA^.RRCount;
        Move(RRA^.RRs, NewArray^.RRs, RRA^.RRCount * RRElemSize);
        FreeMem(RRA, CalcRRArraySize(RRA^.RRTotal));
        RRA := NewArray;
      end
    {otherwise this is a new allocation}
    else
      begin
        RRA := AllocMem(CalcRRArraySize(RRArrayInc));
        RRA^.RRTotal := RRArrayInc;
      end;
  end;
{--------}
procedure FreeRRArray(var RRA : POvcSelRRArray);
  {-Frees a row range array}
  begin
    {Note: assumes RRA is not nil}
    FreeMem(RRA, CalcRRArraySize(RRA^.RRTotal));
    RRA := nil;
  end;
{--------}
procedure ReallocColArray(var CA : POvcSelColArray; OldCC, NewCC : TColNum);
  {-Reallocates (ie allocs or frees or grows) a column array}
  var
    NewArray : POvcSelColArray;
    i        : integer;
  begin
    {if there's no change, forget it}
    if (NewCC = OldCC) then
      Exit;
    {if the new array size is greater then just copy over the
     old array's contents after clearing the new array}
    if (NewCC > OldCC) then
      begin
        NewArray := AllocMem(NewCC * sizeof(pointer));
        if (OldCC > 0) then
          Move(CA^, NewArray^, OldCC * sizeof(pointer));
      end
    {if the new array size is smaller then we have to dispose of
     the subarrays that will no longer be used, then copy over
     the remaining elements (if any).}
    else
      begin
        for i := NewCC to pred(OldCC) do
          if Assigned(CA^[i]) then
            FreeRRArray(CA^[i]);
        if (NewCC = 0) then
          NewArray := nil
        else
          begin
            GetMem(NewArray, NewCC * sizeof(pointer));
            Move(CA^, NewArray^, NewCC * sizeof(pointer));
          end;
      end;
    {dispose of the old array, return the new one}
    if (OldCC > 0) then
      FreeMem(CA, OldCC * sizeof(pointer));
    CA := NewArray;
  end;
{====================================================================}


{===TOvcSelectionList================================================}
constructor TOvcSelectionList.Create(RowCount : TRowNum; ColCount : TColNum);
  begin
    {inherited Create;}
    SetRowCount(RowCount);
    SetColCount(ColCount);
  end;
{--------}
destructor TOvcSelectionList.Destroy;
  begin
    SetColCount(0);
    {inherited Destroy;}
  end;
{--------}
procedure TOvcSelectionList.DeselectAll;
  var
    ColNum : TColNum;
  begin
    if (slColWithSelCount > 0) then
      begin
        for ColNum := 0 to pred(slColCount) do
          if Assigned(slArray^[ColNum]) then
            FreeRRArray(slArray^[ColNum]);
        slColWithSelCount := 0;
      end;
    slEmptyRange := true;
  end;
{--------}
procedure TOvcSelectionList.DeselectCell(RowNum : TRowNum; ColNum : TColNum);
  begin
    {sanity checks}
    if (RowNum < 0) or (RowNum >= slRowCount) or
       (ColNum < 0) or (ColNum >= slColCount) then
      Exit;
    {do it}
    slDeselectCellRangeInCol(RowNum, RowNum, ColNum);
  end;
{--------}
procedure TOvcSelectionList.DeselectCellRange(FromRow : TRowNum; FromCol : TColNum;
                                              ToRow : TRowNum; ToCol : TColNum);
  var
    ColNum : TColNum;
    SwapTemp : longint;
  begin
    {save the caller from himself: sort the rows/cols into ascending order}
    if FromRow > ToRow then
      begin
        SwapTemp := FromRow;
        FromRow := ToRow;
        ToRow := SwapTemp;
      end;
    if FromCol > ToCol then
      begin
        SwapTemp := FromCol;
        FromCol := ToCol;
        ToCol := SwapTemp;
      end;
    {sanity checks}
    if (FromRow < 0) or (FromRow >= slRowCount) or
       (ToRow < 0) or (ToRow >= slRowCount) or
       (FromCol < 0) or (FromCol >= slColCount) or
       (ToCol < 0) or (ToCol >= slColCount) then
      Exit;
    {for each column, deselect cells in that column}
    for ColNum := FromCol to ToCol do
      slDeselectCellRangeInCol(FromRow, ToRow, ColNum);
  end;
{--------}
{$IFDEF SuppressWarnings}
{$WARNINGS OFF}
{$ENDIF}
procedure TOvcSelectionList.slDeselectCellRangeInCol(Row1, Row2 : TRowNum;
                                                     ColNum : TColNum);
  var
    Inx : integer;
    i   : integer;
    RRA : POvcSelRRArray;
    MustDelete : boolean;
    StillGoing : boolean;
  begin
    Inx := 0;
    {take care of the simple special case first: there are no
     selections in the column at all}
    if (not Assigned(slArray^[ColNum])) then
      Exit;
    {make sure the array has at least one spare element: we could
     be splitting a range}
    RRA := slArray^[ColNum];
    if (RRA^.RRCount = RRA^.RRTotal) then
      begin
        AllocRRArray(RRA);
        slArray^[ColNum] := RRA;
      end;
    {with this array}
    with RRA^ do
      begin
        {search for the place to delete from}
        MustDelete := false;
        for i := 0 to pred(RRCount) do
          if (Row1 < RRs[i].L) then
            begin
              MustDelete := true;
              Inx := i;
              Break;{out of for loop}
            end
          else if (Row1 <= RRs[i].H) then
            begin
              MustDelete := true;
              Inx := succ(i);
              Break;{out of for loop}
            end;

        {if the range to deselect appears after all other
         ranges, just exit, nothing to do}
        if not MustDelete then
          Exit;
        {walk through the array starting at pred(Inx)
         and split/remove as we go}
        if (Inx > 0) then
          dec(Inx);
        StillGoing := true;
        while StillGoing and (Inx < RRCount) do
          if (RRs[Inx].L < Row1) then
            if (RRs[Inx].H < Row1) then
              inc(Inx)
            else {H >= Row1} if (RRs[Inx].H > Row2) then
              begin
                {split, the deselect range is entirely within this range}
                Move(RRs[Inx], RRs[succ(Inx)], (RRCount-Inx)*RRElemSize);
                inc(RRCount);
                RRs[Inx].H := pred(Row1);
                RRs[succ(Inx)].L := succ(Row2);
                StillGoing := false;
              end
            else {H >= Row1 and <= Row2}
              begin
                RRs[Inx].H := pred(Row1);
                inc(Inx);
              end
          else {L >= Row1} if (RRs[Inx].L <= Row2) then
            if (RRs[Inx].H > Row2) then
              begin
                RRs[Inx].L := succ(Row2);
                StillGoing := false;
              end
            else {H <= Row2}
              begin
                {delete the range completely}
                dec(RRCount);
                Move(RRs[succ(Inx)], RRs[Inx], (RRCount-Inx)*RRElemSize);
              end
          else {L >= Row1 and > Row2}
            StillGoing := false;
      end;
    {check to see whether we've managed to deselect every cell, if so
     free the row range array}
    if (RRA^.RRCount = 0) then
      begin
        FreeRRArray(slArray^[ColNum]);
        dec(slColWithSelCount);
      end;
  end;
{$IFDEF SuppressWarnings}
{$WARNINGS ON}
{$ENDIF}
{--------}
procedure TOvcSelectionList.ExtendRange(RowNum : TRowNum; ColNum : TColNum;
                                        IsSelecting : boolean);
  begin
    if (RowNum < 0) or (RowNum >= slRowCount) or
       (ColNum < 0) or (ColNum >= slColCount) then
      Exit;
    slSelecting := IsSelecting;
    slActiveRow := RowNum;
    slActiveCol := ColNum;
    slEmptyRange := (slAnchorRow = RowNum) and (slAnchorCol = ColNum);
    if not slEmptyRange then
      begin
        slColMin := MinL(slAnchorCol, ColNum);
        slColMax := MaxL(slAnchorCol, ColNum);
        slRowMin := MinL(slAnchorRow, RowNum);
        slRowMax := MaxL(slAnchorRow, RowNum);
      end;
  end;
{--------}
function TOvcSelectionList.HaveSelection : boolean;
  begin
    Result := (not slEmptyRange) or (slColWithSelCount <> 0);
  end;
{--------}
function TOvcSelectionList.IsCellSelected(RowNum : TRowNum; ColNum : TColNum) : boolean;
  var
    i : integer;
  begin
    {assume false, the cell is not selected}
    Result := false;
    {sanity checks}
    if (RowNum < 0) or (RowNum >= slRowCount) or
       (ColNum < 0) or (ColNum >= slColCount) then
      Exit;
    {check in current range}
    if (not slEmptyRange) then
      if (slColMin <= ColNum) and (ColNum <= slColMax) and
         (slRowMin <= RowNum) and (RowNum <= slRowMax) then
        begin
          Result := slSelecting;
          Exit;
        end;
    {if the column array exists, search through it; note we use a
     sequential search: it'll be faster than a binary search for a
     'few' elements, and generally there will be 'few' elements}
    if Assigned(slArray^[ColNum]) then with slArray^[ColNum]^ do
      for i := 0 to pred(RRCount) do
        if (RRs[i].L <= RowNum) and (RowNum <= RRs[i].H) then
          begin
            Result := true;
            Exit;
          end;
  end;
{--------}
procedure TOvcSelectionList.Iterate(SI : TSelectionIterator; ExtraData : pointer);
  var
    ColNum   : TColNum;
    RangeNum : integer;
  begin
    {fix the current range}
    if not slEmptyRange then
      begin
        if slSelecting then
          SelectCellRange(slRowMin, slColMin, slRowMax, slColMax)
        else
          DeselectCellRange(slRowMin, slColMin, slRowMax, slColMax);
        slEmptyRange := true;
      end;
    {iterate through the ranges}
    for ColNum := 0 to pred(slColCount) do
      if Assigned(slArray^[ColNum]) then
        with slArray^[ColNum]^ do
          for RangeNum := 0 to pred(RRCount) do
            if not SI(RRs[RangeNum].L, ColNum, RRs[RangeNum].H, ColNum, ExtraData) then
              Exit;
  end;
{--------}
procedure TOvcSelectionList.SelectAll;
  var
    ColNum : TColNum;
  begin
    for ColNum := 0 to pred(slColCount) do
      begin
        if not Assigned(slArray^[ColNum]) then
          AllocRRArray(slArray^[ColNum]);
        with slArray^[ColNum]^ do
          begin
            RRCount := 1;
            RRs[0].L := 0;
            RRs[0].H := pred(slRowCount);
          end;
      end;
    slColWithSelCount := slColCount;
  end;
{--------}
procedure TOvcSelectionList.SelectCell(RowNum : TRowNum; ColNum : TColNum);
  begin
    {sanity checks}
    if (RowNum < 0) or (RowNum >= slRowCount) or
       (ColNum < 0) or (ColNum >= slColCount) then
      Exit;
    {do it}
    slSelectCellRangeInCol(RowNum, RowNum, ColNum);
  end;
{--------}
procedure TOvcSelectionList.SelectCellRange(FromRow : TRowNum; FromCol : TColNum;
                                            ToRow : TRowNum; ToCol : TColNum);
  var
    ColNum : TColNum;
    SwapTemp : longint;
  begin
    {save the caller from himself: sort the rows/cols into ascending order}
    if FromRow > ToRow then
      begin
        SwapTemp := FromRow;
        FromRow := ToRow;
        ToRow := SwapTemp;
      end;
    if FromCol > ToCol then
      begin
        SwapTemp := FromCol;
        FromCol := ToCol;
        ToCol := SwapTemp;
      end;
    {sanity checks}
    if (FromRow < 0) or (FromRow >= slRowCount) or
       (ToRow < 0) or (ToRow >= slRowCount) or
       (FromCol < 0) or (FromCol >= slColCount) or
       (ToCol < 0) or (ToCol >= slColCount) then
      Exit;
    {for each column, select cells in that column}
    for ColNum := FromCol to ToCol do
      slSelectCellRangeInCol(FromRow, ToRow, ColNum);
  end;
{--------}
{$IFDEF SuppressWarnings}
{$WARNINGS OFF}
{$ENDIF}
procedure TOvcSelectionList.slSelectCellRangeInCol(Row1, Row2 : TRowNum;
                                                   ColNum : TColNum);
  var
    i       : integer;
    Inx     : integer;
    NextInx : integer;
    RRA     : POvcSelRRArray;
    MustInsert   : boolean;
    StillGoing   : boolean;
    AlreadyMerged: boolean;
  begin
    Inx := 0;
    {take care of the simple special case first: there are no
     selections in the column as yet}
    if (not Assigned(slArray^[ColNum])) then
      begin
        AllocRRArray(slArray^[ColNum]);
        inc(slColWithSelCount);
        with slArray^[ColNum]^ do
          begin
            RRCount := 1;
            RRs[0].L := Row1;
            RRs[0].H := Row2;
          end;
        Exit;
      end;
    {make sure the array has at least one spare element}
    RRA := slArray^[ColNum];
    if (RRA^.RRCount = RRA^.RRTotal) then
      begin
        AllocRRArray(RRA);
        slArray^[ColNum] := RRA;
      end;
    {with this array}
    with RRA^ do
      begin
        {search for the place to insert/merge}
        MustInsert := false;
        for i := 0 to pred(RRCount) do
          if (Row1 < RRs[i].L) then
            begin
              MustInsert := true;
              Inx := i;
              Break;{out of for loop}
            end;
        {if the new range appears after all the other ranges, add it
         to the end of the list; check to be able to merge it first}
        if not MustInsert then
          begin
            if (Row1 <= succ(RRs[pred(RRCount)].H)) then
              RRs[pred(RRCount)].H := MaxL(Row2, RRs[pred(RRCount)].H)
            else
              begin
                RRs[RRCount].L := Row1;
                RRs[RRCount].H := Row2;
                inc(RRCount);
              end;
            Exit;
          end;
        {otherwise we must insert; first insert the new range}
        Move(RRs[Inx], RRs[succ(Inx)], (RRCount-Inx) * RRElemSize);
        RRs[Inx].L := Row1;
        RRs[Inx].H := Row2;
        inc(RRCount);
        {now walk through the array starting at pred(Inx) and merge
         ranges as we move forward}
        if (Inx > 0) then
          dec(Inx);
        NextInx := succ(Inx);
        AlreadyMerged := false;
        StillGoing := true;
        while StillGoing and (NextInx < RRCount) do
          begin
            if (succ(RRs[Inx].H) >= RRs[NextInx].L) then
              begin
                RRs[Inx].H := MaxL(RRs[Inx].H, RRs[NextInx].H);
                inc(NextInx);
                AlreadyMerged := true;
              end
            else if AlreadyMerged then
              StillGoing := false
            else
              begin
                inc(Inx);
                inc(NextInx);
                AlreadyMerged := true;
              end;
          end;
        {by this point we know we must get rid of the elements
         in between Inx and NextInx -- they've been merged into
         other ranges}
        if ((NextInx - Inx) > 1) then
          begin
            if (NextInx < RRCount) then
              Move(RRs[NextInx], RRs[succ(Inx)], (RRCount-NextInx)*RRElemSize);
            dec(RRCount, NextInx - Inx - 1);
          end;
      end;
  end;
{$IFDEF SuppressWarnings}
{$WARNINGS ON}
{$ENDIF}
{--------}
procedure TOvcSelectionList.SetRangeAnchor(RowNum : TRowNum; ColNum : TColNum;
                                           Action : TOvcTblSelectionType);
  begin
    {sanity checks}
    if (RowNum < 0) or (RowNum >= slRowCount) or
       (ColNum < 0) or (ColNum >= slColCount) then
      Exit;
    {what's happening? deselecting all, or adding a new range}
    if (Action = tstDeselectAll) then
      DeselectAll
    else {an additional range is being set up}
      if not slEmptyRange then
        if slSelecting then
          SelectCellRange(slRowMin, slColMin, slRowMax, slColMax)
        else
          DeselectCellRange(slRowMin, slColMin, slRowMax, slColMax);
    slAnchorRow := RowNum;
    slAnchorCol := ColNum;
    slActiveRow := RowNum;
    slActiveCol := ColNum;
    slEmptyRange := true;
  end;
{--------}
procedure TOvcSelectionList.SetColCount(ColCount : TColNum);
  begin
    if (ColCount >= 0) then
      begin
        ReallocColArray(slArray, slColCount, ColCount);
        slColCount := ColCount;
      end;
  end;
{--------}
procedure TOvcSelectionList.SetRowCount(RowCount : TRowNum);
  begin
    if (RowCount >= 0) then
      slRowCount := RowCount;
  end;
{====================================================================}

end.
