Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/GAP/pkg/browse/lib/   (Algebra von RWTH Aachen Version 4.15.1©)  Datei vom 1.2.2023 mit Größe 283 kB image not shown  

Quelle  browse.gi   Sprache: unbekannt

 
##############################################################################
##
#W  browse.gi             GAP 4 package `browse'                Thomas Breuer
##


#############################################################################
##
DeclareUserPreference( rec(
    name:= "EnableMouseEvents",
    description:= [
"This user preference defines whether mouse events are enabled by default \
in visual mode (value 'true') or not (value 'false', this is the default).  \
During the ⪆ session, \
the value can be changed using 'NCurses.UseMouse'.  \
Inside browse applications based on 'NCurses.BrowseGeneric' \
or 'NCurses.Select', \
the value can be toggled usually by hitting the 'M' key." ],
    default:= false,
    values:= [ true, false ],
    multi:= false,
    ) );

if UserPreference( "Browse", "EnableMouseEvents" ) = true then
  NCurses.UseMouse( true );
fi;


#############################################################################
##
#V  BrowseData
##
BrowseData.actions:= rec();

BrowseData.defaults:= rec(
      work:= rec(

          # global positioning
          windowParameters:= [ 0, 0, 0, 0 ], # full window
          minyx:= [ 0, 0 ], # is replaced by functions below
          align:= "", # default: right and vertically centered

          # table data: five matrices, four separators
          # (no default for `main', the sixth matrix)
          header:= [],
          footer:= [],
          corner:= [],
          labelsCol:= [],
          labelsRow:= [],
          sepLabelsCol:= "",
          sepLabelsRow:= "",
          sepCol:= " ",
          sepRow:= "",

          cacheEntries:= false,

          cornerFormatted:= [],
          labelsColFormatted:= [],
          labelsRowFormatted:= [],
          mainFormatted:= [],

          Main:= false,

          heightRow:= [],
          widthCol:= [],
          heightLabelsCol:= [],
          widthLabelsRow:= [],

          # (at most) mode dependent lengths
          headerLength:= rec(),
          footerLength:= rec(),

          # how to fill missing entries
          emptyCell:= "",      # table cell data object

          # for categories feature
          sepCategories:= "-",
          startCollapsedCategory:= [ [ NCurses.attrs.BOLD, true, "> " ] ],
          startExpandedCategory:= [ [ NCurses.attrs.BOLD, true, "* " ] ],

          # how to mark a selected cell/row/column
          startSelect:= [ NCurses.attrs.STANDOUT, true ],

          # mode info (will be filled below)
          availableModes:= [],

          # customized modes
          customizedModes:= rec(),

          Click:= rec(),
        ),

      dynamic:= rec(

          # for the sort feature, will be filled with the table
          indexRow:= [],
          indexCol:= [],

          # topleft is [i, j, k, l]: entry[i][j] blockposition[k][l]
          topleft := [1, 1, 1, 1],
          isCollapsedCol:= [],
          isCollapsedRow:= [],
          isRejectedCol:= [],
          isRejectedRow:= [],
          isRejectedLabelsCol:= [],
          isRejectedLabelsRow:= [],

          # mode info (will be filled as soon as the `browse' mode is defined)
          activeModes:= [],

          # for selecting a cell or row or column or category
          selectedEntry:= [ 0, 0 ],
          selectedCategory:= [ 0, 0 ],

#T not used anymore, but some applications may rely on its presence
          useMouse:= false,

          # for the search feature
          searchString:= "",
          searchParameters:= [
            [ "case sensitive", [ "yes", "no" ], 2 ],
            [ "mode", [ "substring", "whole entry" ], 1 ],
            [ "search for", [ "any substring", "word", "prefix", "suffix" ],
              1, paras -> ForAny( paras, x -> x[1] = "mode" and x[3] = 1 ) ],
            [ "compare with",
              [ "\"<\"", "\"<=\"", "\"=\"", "\">=\"", "\">\"", "\"<>\"" ], 3,
              paras -> ForAny( paras, x -> x[1] = "mode" and x[3] = 2 ) ],
#T new option: "with wildcards *,?"
            [ "search", [ "forwards", "backwards" ], 1 ],
            [ "search", [ "row by row", "column by column" ], 1 ],
            [ "wrap around", [ "yes", "no" ], 1 ],
            [ "negate", [ "yes", "no" ], 2 ],
          ],

          # for the sort feature
          sortParametersForRows:= [],
          sortParametersForColumns:= [],
          sortParametersForRowsDefault:= [
            [ "direction", [ "ascending", "descending" ], 1 ],
            [ "case sensitive", [ "yes", "no" ], 1 ],
          ],
          sortParametersForColumnsDefault:= [
            [ "direction", [ "ascending", "descending" ], 1 ],
            [ "case sensitive", [ "yes", "no" ], 2 ],
            [ "hide on categorizing", [ "yes", "no" ], 1 ],
            [ "add counter on categorizing", [ "yes", "no" ], 2 ],
            [ "split rows on categorizing", [ "yes", "no" ], 2 ],
          ],
#T other parameters: as strings or numbers or via function,
#T                   secondary sort rows/columns
          sortFunctionsForRows:= [],
          sortFunctionsForColumns:= [],
          sortFunctionForTableDefault:= \<,
          sortFunctionForRowsDefault:= \<,
          sortFunctionForColumnsDefault:= \<,

          # for the categorizing feature
          categories:= [ [], [], [] ],

          # for the filtering feature
          filterParameters:= [
            [ "case sensitive", [ "yes", "no" ], 2 ],
            [ "mode", [ "substring", "whole entry" ], 1 ],
            [ "search for", [ "any substring", "word", "prefix", "suffix" ],
              1, paras -> ForAny( paras, x -> x[1] = "mode" and x[3] = 1 ) ],
            [ "compare with",
              [ "\"<\"", "\"<=\"", "\"=\"", "\">=\"", "\">\"", "\"<>\"" ], 3,
              paras -> ForAny( paras, x -> x[1] = "mode" and x[3] = 2 ) ],
#T new option: "with wildcards *,?"
            [ "negate", [ "yes", "no" ], 2 ],
          ],

          # for the replay feature
          replayDefaults:= rec(
            steps:= [],
            position:= 1,
            replayInterval:= 200,
            quiet:= false,
          ),

          # for the logging feature
          log:= [],
      ),
     );


#############################################################################
##
#F  BrowseData.MinimalWindowHeight( <t> )
#F  BrowseData.MinimalWindowWidth( <t> )
##
BrowseData.MinimalWindowHeight:= function( t )
    local height;

    height:= BrowseData.HeightLabelsColTable( t ) +
             BrowseData.HeightRow( t, 1 ) +
             BrowseData.HeightRow( t, 2 );
    if IsList( t.work.header ) then
      height:= height + Length( t.work.header );
    fi;
    if IsList( t.work.footer ) then
      height:= height + Length( t.work.footer );
    fi;

    return height;
end;

BrowseData.MinimalWindowWidth:= function( t )
    return BrowseData.WidthLabelsRowTable( t ) +
           BrowseData.WidthCol( t, 1 ) +
           BrowseData.WidthCol( t, 2 );
end;

BrowseData.defaults.work.minyx:= [ BrowseData.MinimalWindowHeight,
                                   BrowseData.MinimalWindowWidth ];


#############################################################################
##
#F  BrowseData.IsBrowseTableCellData( <obj> )
##
##  <#GAPDoc Label="IsBrowseTableCellData_man">
##  <ManSection>
##  <Func Name="BrowseData.IsBrowseTableCellData" Arg="obj"/>
##
##  <Returns>
##  <K>true</K> if the argument is a list or a record in a supported format.
##  </Returns>
##
##  <Description>
##  A <E>table cell data object</E> describes the input data for the contents
##  of a cell in a browse table.
##  It is represented by either an attribute line
##  (see <Ref Func="NCurses.IsAttributeLine"/>),
##  for cells of height one, or a list of attribute lines
##  or a record with the components <C>rows</C>, a list of attribute lines,
##  and optionally <C>align</C>, a substring of <C>"bclt"</C>,
##  which describes the alignment of the attribute lines in the table cell
##  -- bottom, horizontally centered, left, and top alignment;
##  the default is right and vertically centered alignment.
##  (Note that the height of a table row and the width of a table column
##  can be larger than the height and width of an individual cell.)
##  <P/>
##  <Example><![CDATA[
##  gap> BrowseData.IsBrowseTableCellData( "abc" );
##  true
##  gap> BrowseData.IsBrowseTableCellData( [ "abc", "def" ] );
##  true
##  gap> BrowseData.IsBrowseTableCellData( rec( rows:= [ "ab", "cd" ],
##  >                                           align:= "tl" ) );
##  true
##  gap> BrowseData.IsBrowseTableCellData( "" );
##  true
##  gap> BrowseData.IsBrowseTableCellData( [] );
##  true
##  ]]></Example>
##  <P/>
##  The <E>empty string</E> is a table cell data object of height one and
##  width zero whereas the <E>empty list</E>
##  (which is not in <Ref Func="IsStringRep" BookName="ref"/>)
##  is a table cell data object of height zero and width zero.
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##
BrowseData.IsBrowseTableCellData:=
    obj -> NCurses.IsAttributeLine( obj ) or
       ( IsDenseList( obj ) and ForAll( obj, NCurses.IsAttributeLine ) ) or
       ( IsRecord( obj ) and IsBound( obj.rows ) and IsDenseList( obj.rows )
                         and ForAll( obj.rows, NCurses.IsAttributeLine ) );


#############################################################################
##
#F  BrowseData.IsModeRecord( <obj> )
##
BrowseData.IsModeRecord:=
    obj -> IsRecord( obj ) and
           IsBound( obj.name ) and IsString( obj.name ) and
           IsBound( obj.flag ) and IsString( obj.flag ) and
           IsBound( obj.ShowTables ) and IsFunction( obj.ShowTables ) and
           IsBound( obj.actions ) and IsList( obj.actions ) and
           ForAll( obj.actions,
             x -> IsList( x ) and Length( x ) = 3
                  and IsList( x[1] )
                  and ForAll( x[1], y -> IsPosInt( y )
                                         or y in NCurses.mouseEvents )
                  and IsRecord( x[2] ) and IsBound( x[2].helplines )
                  and IsList( x[2].helplines )
                  and ForAll( x[2].helplines, NCurses.IsAttributeLine )
                  and IsBound( x[2].action ) and IsFunction( x[2].action )
                  and IsString( x[3] ) and not IsEmpty( x[3] ) );


#############################################################################
##
#F  BrowseData.IsBrowseTable( <obj> )
##
##  <#GAPDoc Label="IsBrowseTable_man">
##  <ManSection>
##  <Func Name="BrowseData.IsBrowseTable" Arg="obj"/>
##
##  <Returns>
##  <K>true</K> if the argument record has the required components
##  and is consistent.
##  </Returns>
##
##  <Description>
##  A <E>browse table</E> is a &GAP; record that can be used as the first
##  argument of the function <Ref Func="NCurses.BrowseGeneric"/>.
##  <P/>
##  The supported components of a browse table are <C>work</C> and
##  <C>dynamic</C>, their values must be records:
##  The components in <C>work</C> describe that part of the data that
##  are not likely to depend on user interactions,
##  such as the table entries and their heights and widths.
##  The components in <C>dynamic</C> describe that part of the data that
##  is intended to change with user interactions,
##  such as the currently shown top-left entry of the table,
##  or the current status w.r.t. sorting.
##  For example, suppose you call <Ref Func="NCurses.BrowseGeneric"/> twice
##  with the same browse table;
##  the second call enters the table in the same status where it was left
##  <E>after</E> the first call if the component <C>dynamic</C> is kept,
##  whereas one has to reset (which usually simply means to unbind) the
##  component <C>dynamic</C> if one wants to start in the same status as
##  <E>before</E> the first call.
##  <P/>
##  The following components are the most important ones for standard browse
##  applications.
##  All these components belong to the <C>work</C> record.
##  For other supported components (of <C>work</C> as well as of
##  <C>dynamic</C>) and for the meaning of the term <Q>mode</Q>,
##  see Section <Ref Sect="sec:modes"/>.
##
##  <List>
##  <Mark><C>main</C></Mark>
##  <Item>
##    is the list of lists of table cell data objects that form the matrix
##    to be shown.
##    There is no default for this component.
##    (It is possible to compute the entries of the main table on demand,
##    see the description of the component <C>Main</C>
##    in Section <Ref Subsect="BrowseData"/>.
##    In this situation, the value of the component <C>main</C> can be an
##    empty list.)
##  </Item>
##  <Mark><C>header</C></Mark>
##  <Item>
##    describes a header that shall be shown above the column labels.
##    The value is either a list of attribute lines (<Q>static header</Q>)
##    or a function or a record whose component names are names of
##    available modes of the browse table (<Q>dynamic header</Q>).
##    In the function case, the function must take the browse table as its
##    only argument, and return a list of attribute lines.
##    In the record case, the values of the components must be such
##    functions.
##    It is assumed that the number of these lines depends at most on the
##    mode.
##    The default is an empty list, i. e., there is no header.
##  </Item>
##  <Mark><C>footer</C></Mark>
##  <Item>
##    describes a footer that shall be shown below the table.
##    The value is analogous to that of <C>footer</C>.
##    The default is an empty list, i. e., there is no footer.
##  </Item>
##  <Mark><C>labelsRow</C></Mark>
##  <Item>
##    is a list of row label rows,
##    each being a list of table cell data objects.
##    These rows are shown to the left of the main table.
##    The default is an empty list, i. e., there are no row labels.
##  </Item>
##  <Mark><C>labelsCol</C></Mark>
##  <Item>
##    is a list of column information rows,
##    each being a list of table cell data objects.
##    These rows are shown between the header and the main table.
##    The default is an empty list, i. e., there are no column labels.
##  </Item>
##  <Mark><C>corner</C></Mark>
##  <Item>
##    is a list of lists of table cell data objects that are printed
##    in the upper left corner, i. e.,
##    to the left of the column label rows and above the row label columns.
##    The default is an empty list.
##  </Item>
##  <Mark><C>sepRow</C></Mark>
##  <Item>
##    describes the separators above and below rows of the main table
##    and of the row labels table.
##    The value is either an attribute line or a (not necessarily dense)
##    list of attribute lines.
##    In the former case, repetitions of the attribute line are used as
##    separators below each row and above the first row of the table;
##    in the latter case, repetitions of the entry at the first position
##    (if bound) are used above the first row, and repetitions of the last
##    bound entry before the <M>(i+2)</M>-th position (if there is such an
##    entry at all) are used below the <M>i</M>-th table row.
##    The default is an empty string,
##    which means that there are no row separators.
##  </Item>
##  <Mark><C>sepCol</C></Mark>
##  <Item>
##    describes the separators in front of and behind columns of the main
##    table and of the column labels table.
##    The format of the value is analogous to that of the component
##    <C>sepRow</C>;
##    the default is the string <C>" "</C> (whitespace of width one).
##  </Item>
##  <Mark><C>sepLabelsCol</C></Mark>
##  <Item>
##    describes the separators above and below rows of the column labels
##    table and of the corner table,
##    analogously to <C>sepRow</C>.
##    The default is an empty string,
##    which means that there are no column label separators.
##  </Item>
##  <Mark><C>sepLabelsRow</C></Mark>
##  <Item>
##    describes the separators in front of and behind columns of the row
##    labels table and of the corner table,
##    analogously to <C>sepCol</C>.
##    The default is an empty string.
##  </Item>
##  </List>
##
##  We give a few examples of standard applications.
##  <P/>
##  The first example defines a small browse table by prescribing only the
##  component <C>work.main</C>,
##  so the defaults for row and column labels (no such labels),
##  and for separators are used.
##  The table cells are given by plain strings, so they have height one.
##  Usually this table will fit on the screen.
##  <P/>
##  <Example><![CDATA[
##  gap> m:= 10;;  n:= 5;;
##  gap> xpl1:= rec( work:= rec(
##  >      main:= List( [ 1 .. m ], i -> List( [ 1 .. n ],
##  >        j -> String( [ i, j ] ) ) ) ) );;
##  gap> BrowseData.IsBrowseTable( xpl1 );
##  true
##  ]]></Example>
##  <P/>
##  In the second example, also row and column labels appear,
##  and different separators are used.
##  The table cells have height three.
##  Also this table will usually fit on the screen.
##  <P/>
##  <Example><![CDATA[
##  gap> m:= 6;;  n:= 5;;
##  gap> xpl2:= rec( work:= rec(
##  >      main:= List( [ 1 .. m ], i -> List( [ 1 .. n ],
##  >        j -> rec( rows:= List( [ -i*j, i*j*1000+j, i-j ], String ),
##  >                  align:= "c" ) ) ),
##  >      labelsRow:= List( [ 1 .. m ], i -> [ String( i ) ] ),
##  >      labelsCol:= [ List( [ 1 .. n ], String ) ],
##  >      sepRow:= "-",
##  >      sepCol:= "|",
##  >  ) );;
##  gap> BrowseData.IsBrowseTable( xpl2 );
##  true
##  ]]></Example>
##  <P/>
##  The third example additionally has a static header and a dynamic footer,
##  and the table cells involve attributes.
##  This table will usually not fit on the screen.
##  Note that <C>NCurses.attrs.ColorPairs</C> is available only if the
##  terminal supports colors, which can be checked using
##  <Ref Var="NCurses.attrs.has_colors"/>.
##  <P/>
##  <Example><![CDATA[
##  gap> m:= 30;;  n:= 25;;
##  gap> xpl3:= rec( work:= rec(
##  >      header:= [ "                    Example 3" ],
##  >      labelsRow:= List( [ 1 .. 30 ], i -> [ String( i ) ] ),
##  >      sepLabelsRow:= " % ",
##  >      sepLabelsCol:= "=",
##  >      sepRow:= "*",
##  >      sepCol:= " |",
##  >      footer:= t -> [ Concatenation( "top-left entry is: ",
##  >                          String( t.dynamic.topleft{ [ 1, 2] } ) ) ],
##  >  ) );;
##  gap> if NCurses.attrs.has_colors then
##  >   xpl3.work.main:= List( [ 1 .. m ], i -> List( [ 1 .. n ],
##  >     j -> rec( rows:= [ String( -i*j ),
##  >                        [ NCurses.attrs.BOLD, true,
##  >                          NCurses.attrs.ColorPairs[56+1], true,
##  >                          String( i*j*1000+j ),
##  >                          NCurses.attrs.NORMAL, true ],
##  >                          String( i-j ) ],
##  >               align:= "c" ) ) );
##  >   xpl3.work.labelsCol:= [ List( [ 1 .. 30 ], i -> [
##  >     NCurses.attrs.ColorPairs[ 56+4 ], true,
##  >     String( i ),
##  >     NCurses.attrs.NORMAL, true ] ) ];
##  > else
##  >   xpl3.work.main:= List( [ 1 .. m ], i -> List( [ 1 .. n ],
##  >     j -> rec( rows:= [ String( -i*j ),
##  >                        [ NCurses.attrs.BOLD, true,
##  >                          String( i*j*1000+j ),
##  >                          NCurses.attrs.NORMAL, true ],
##  >                          String( i-j ) ],
##  >               align:= "c" ) ) );
##  >   xpl3.work.labelsCol:= [ List( [ 1 .. 30 ], i -> [
##  >     NCurses.attrs.BOLD, true,
##  >     String( i ),
##  >     NCurses.attrs.NORMAL, true ] ) ];
##  > fi;
##  gap> BrowseData.IsBrowseTable( xpl3 );
##  true
##  ]]></Example>
##  <P/>
##  The fourth example illustrates that highlighting may not work properly
##  for browse tables containing entries whose attributes are not set with
##  explicit Boolean values, see <Ref Func="NCurses.IsAttributeLine"/>.
##  Call <Ref Func="NCurses.BrowseGeneric"/> with the browse table
##  <C>xpl4</C>, and select an entry (or a column or a row):
##  Only the middle row of each selected cell will be highlighted,
##  because only in this row, the color attribute is switched on with an
##  explicit <K>true</K>.
##  <P/>
##  <Example><![CDATA[
##  gap> xpl4:= rec(
##  >     defc:= NCurses.defaultColors,
##  >     wd:= Maximum( List( ~.defc, Length ) ),
##  >     ca:= NCurses.ColorAttr,
##  >     work:= rec(
##  >       header:= [ "Examples of NCurses.ColorAttr" ],
##  >       main:= List( ~.defc, i -> List( ~.defc,
##  >         j -> [ [ ~.ca( i, j ), String( i, ~.wd ) ],        # no true!
##  >                [ ~.ca( i, j ), true, String( "on", ~.wd ) ],
##  >                [ ~.ca( i, j ), String( j, ~.wd ) ] ] ) ),  # no true!
##  >       labelsRow:= List( ~.defc, i -> [ String( i ) ] ),
##  >       labelsCol:= [ List( ~.defc, String ) ],
##  >       sepRow:= "-",
##  >       sepCol:= [ " |", "|" ],
##  >  ) );;
##  gap> BrowseData.IsBrowseTable( xpl4 );
##  true
##  ]]></Example>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##
BrowseData.IsBrowseTable:= function( obj )
    local result, work, comp, complen, dynamic, l, h, log;

    if not IsRecord( obj ) then
      Print( "#E  <obj> must be a record\n" );
      return false;
    fi;

    result:= true;

    if IsBound( obj.work ) then
      work:= obj.work;
      # windowParameters
      if IsBound( work.windowParameters ) then
        if not ( IsList( work.windowParameters ) and
                 Length( work.windowParameters ) = 4 and
                 ForAll( work.windowParameters,
                         x -> IsInt( x ) and 0 <= x ) ) then
          Print( "#E  <obj>.work.windowParameters must be a list ",
                 "of four nonnegative integers.\n" );
          result:= false;
        fi;
      fi;
      # align
      if IsBound( work.align ) then
        if not ( IsString( work.align ) and
                 IsSubset( "bclrt", Set( work.align ) ) ) then
          Print( "#E  <obj>.work.align must be a subset of \"bclrt\".\n" );
          result:= false;
        fi;
      fi;
      # header and headerLength, footer and footerLength
      for comp in [ "header", "footer" ] do
        if IsBound( work.( comp ) ) and
           not ( ( IsList( work.( comp ) ) and
                   ForAll( work.( comp ), NCurses.IsAttributeLine ) ) or
                 IsFunction( work.( comp ) ) or
                 ( IsRecord( work.( comp ) ) and
                   ForAll( RecNames( work.( comp ) ),
                           n -> IsFunction( work.( comp ).( n ) ) ) ) ) then
          Print( "#E  <obj>.work.", comp,
                 " must be a list of attribute lines\n",
                 "#E  or a function or a record of functions.\n" );
          result:= false;
        fi;
        complen:= Concatenation( comp, "Length" );
        if IsBound( work.( complen ) ) and
           not ( IsRecord( work.( complen ) ) and
                 ForAll( RecNames( work.( complen ) ),
                         n -> IsInt( work.( complen ).( n ) ) ) ) then
          Print( "#E  <obj>.work.", complen, " must be a record with ",
                 "integer values.\n" );
          result:= false;
        fi;
      od;
      # labelsCol, labelsRow, corner, main
      for comp in [ "labelsCol", "labelsRow", "corner", "main" ] do
        if IsBound( work.( comp ) ) and
           not ( IsList( work.( comp ) ) and
                 ForAll( work.( comp ),
                         x -> IsList( x ) and
                              ForAll( x,
                                BrowseData.IsBrowseTableCellData ) ) ) then
          Print( "#E  <obj>.work.", comp, " must be a list of lists ",
                 "of table cell data objects.\n" );
          result:= false;
        fi;
      od;
      # sepRow, sepCol, sepLabelsCol, sepLabelsRow
      for comp in [ "sepRow", "sepCol", "sepLabelsCol", "sepLabelsRow" ] do
        if IsBound( work.( comp ) ) and
           not ( NCurses.IsAttributeLine( work.( comp ) ) or
                 ( IsList( work.( comp ) ) and
                   ForAll( work.( comp ), NCurses.IsAttributeLine ) ) ) then
          Print( "#E  <obj>.work.", comp, " must be an attribute line or ",
                 "a list of attribute lines.\n" );
          result:= false;
        fi;
      od;
      # cacheEntries
      if IsBound( work.cacheEntries ) and
         not IsBool( work.cacheEntries ) then
        Print( "#E  <obj>.work.cacheEntries must be a Boolean.\n" );
        result:= false;
      fi;
      # cornerFormatted, labelsColFormatted, labelsRowFormatted, mainFormatted
      for comp in [ "cornerFormatted", "labelsColFormatted",
                    "labelsRowFormatted", "mainFormatted" ] do
        if IsBound( work.( comp ) ) and
           not ( IsList( work.( comp ) ) and
                 ForAll( work.( comp ), IsList ) and
                 ForAll( work.( comp ),
                   x -> ForAll( x,
                          y -> NCurses.IsAttributeLine( y ) or
                               ( IsList( y ) and
                                 ForAll( y,
                                   NCurses.IsAttributeLine ) ) ) ) ) then
          Print( "#E  <obj>.work.", comp,
                 " must be a matrix of (lists of) attribute lines.\n" );
          result:= false;
        fi;
      od;
      # m0, n0, m, n
      for comp in [ "m0", "n0", "m", "n" ] do
        if IsBound( work.( comp ) ) and
           not ( IsInt( work.( comp ) ) and 0 <= work.( comp ) ) then
          Print( "#E  <obj>.work.", comp,
                 " must be a nonnegative integer.\n" );
          result:= false;
        fi;
      od;
      # heightLabelsCol, widthLabelsRow, heightRow, widthCol
      for comp in [ "heightLabelsCol", "widthLabelsRow", "heightRow",
                    "widthCol" ] do
        if IsBound( work.( comp ) ) and
           not ForAll( work.( comp ), x -> IsInt( x ) and 0 <= x ) then
          Print( "#E  <obj>.work.", comp,
                 " must be a list of nonnegative integers.\n" );
          result:= false;
        fi;
      od;
      # emptyCell
      if IsBound( work.emptyCell ) and
         not BrowseData.IsBrowseTableCellData( work.emptyCell ) then
        Print( "#E  <obj>.work.emptyCell must be a ",
               "table cell data object.\n" );
        result:= false;
      fi;
      # sepCategories, startSelect
      for comp in [ "sepCategories", "startSelect" ] do
        if IsBound( work.( comp ) ) and
           not NCurses.IsAttributeLine( work.( comp ) ) then
          Print( "#E  <obj>.work.", comp, " must be an attribute line.\n" );
          result:= false;
        fi;
      od;
      # startCollapsedCategory, startExpandedCategory
      for comp in [ "startCollapsedCategory", "startExpandedCategory" ] do
        if IsBound( work.( comp ) ) and
           not ( IsList( work.( comp ) ) and
                 ForAll( work.( comp ), NCurses.IsAttributeLine ) ) then
          Print( "#E  <obj>.work.", comp,
                 " must be a list of attribute lines.\n" );
          result:= false;
        fi;
      od;
      # availableModes
      if IsBound( work.availableModes ) and
         not ( IsList( work.availableModes ) and
               ForAll( work.availableModes, BrowseData.IsModeRecord ) ) then
        Print( "#E  <obj>.work.availableModes must be a ",
               "list of mode records.\n" );
        result:= false;
      fi;
    fi;

    if IsBound( obj.dynamic ) then
      dynamic:= obj.dynamic;
      # indexRow, indexCol
      for comp in [ "indexRow", "indexCol" ] do
        if IsBound( dynamic.( comp ) ) then
          l:= dynamic.( comp );
          if not ( IsList( l ) and ForAll( l, IsPosInt ) ) then
            Print( "#E  <obj>.dynamic.", comp,
                   " must be a list of positive integers.\n" );
            result:= false;
          fi;
          if not ForAll( [ 2, 4 .. Length( l ) - 1 ],
                         i -> IsEvenInt( l[i] ) and l[ i+1 ] = l[i] + 1 ) then
            Print( "#E  in <obj>.dynamic.", comp,
                   ", the values at even positions\n",
                   "#E  and at the subsequent odd positions must be ",
                   "consecutive.\n" );
            result:= false;
          fi;
        fi;
      od;
      # topleft
      if IsBound( dynamic.topleft ) and
         not ( IsList( dynamic.topleft ) and
               Length( dynamic.topleft ) = 4 and
               ForAll( dynamic.topleft, IsPosInt ) ) then
        Print( "#E  <obj>.dynamic.topleft must be a list ",
               "of four positive integers.\n" );
        result:= false;
      fi;
      # isCollapsedRow, isCollapsedCol, isRejectedRow, isRejectedCol,
      # isRejectedLabelsRow, isRejectedLabelsCol
      for comp in [ "isCollapsedRow", "isCollapsedCol",
                    "isRejectedRow", "isRejectedCol",
                    "isRejectedLabelsRow", "isRejectedLabelsCol" ] do
        if IsBound( dynamic.( comp ) ) then
          l:= dynamic.( comp );
          if not ( IsList( dynamic.( comp ) ) and
                   ForAll( dynamic.( comp ), IsBool ) ) then
            Print( "#E  <obj>.dynamic.", comp,
                   " must be a list of Booleans.\n" );
            result:= false;
          fi;
          if IsBound( l[1] ) and l[1] then
            Print( "#E  in <obj>.dynamic.", comp,
                   ", the first entry must be `false'\n" );
            result:= false;
          fi;
          if not ForAll( [ 2, 4 .. Length( l ) - 1 ],
                         i -> l[ i+1 ] = l[i] ) then
            Print( "#E  in <obj>.dynamic.", comp,
                   ", the values at even positions\n",
                   "#E  and at the subsequent odd positions must be ",
                   "equal.\n" );
            result:= false;
          fi;
        fi;
      od;
      # activeModes
      if IsBound( dynamic.activeModes ) and
         not ( IsList( dynamic.activeModes ) and
               ForAll( dynamic.activeModes, BrowseData.IsModeRecord ) ) then
        Print( "#E  <obj>.dynamic.activeModes must be a ",
               "list of mode records.\n" );
        result:= false;
      fi;
      # selectedEntry, selectedCategory
      for comp in [ "selectedEntry", "selectedCategory" ] do
        if IsBound( dynamic.( comp ) ) and
           not ( IsList( dynamic.( comp ) ) and
                 Length( dynamic.( comp ) ) = 2 and
                 ForAll( dynamic.( comp ),
                         x -> IsInt( x ) and 0 <= x ) ) then
          Print( "#E  <obj>.dynamic.", comp,
                 " must be a list of two nonnegative integers.\n" );
          result:= false;
        fi;
      od;
      if IsBound( dynamic.selectedEntry ) and
         IsBound( dynamic.selectedCategory ) and
         dynamic.selectedEntry <> [ 0, 0 ] and
         dynamic.selectedCategory <> [ 0, 0 ] then
        Print( "#E  not both <obj>.dynamic.selectedEntry and ",
               "<obj>.dynamic.selectedCategory can be nonzero.\n" );
      fi;
      # searchString
      if IsBound( dynamic.searchString ) and
         not IsString( dynamic.searchString ) then
        Print( "#E  <obj>.dynamic.searchString must be a string.\n" );
      fi;
      # searchParameters (check?)
      # categories
      if IsBound( dynamic.categories ) then
        l:= dynamic.categories;
        if not ( IsDenseList( l ) and Length( l ) = 3
                                  and ForAll( l, IsList )
                                  and Length( l[1] ) = Length( l[2] )
                                  and ForAll( l[3], IsPosInt ) ) then
          Print( "#E  <obj>.dynamic.categories must be a list ",
                 "of three lists, the first two of the same length.\n" );
        elif not IsSortedList( l[1] ) then
          Print( "#E  <obj>.dynamic.categories[1] must be sorted.\n" );
        elif not ForAll( [ 1 .. Length( l[1] ) ],
                         i -> IsPosInt( l[1][i] ) and
                              IsRecord( l[2][i] ) and
                              IsBound( l[2][i].pos ) and
                              l[1][i] = l[2][i].pos and
                              IsBound( l[2][i].level ) and
                              IsPosInt( l[2][i].level ) and
                              IsBound( l[2][i].value ) and
                              NCurses.IsAttributeLine( l[2][i].value ) and
                              IsBound( l[2][i].separator ) and
                              NCurses.IsAttributeLine( l[2][i].separator ) and
                              IsBound( l[2][i].isUnderCollapsedCategory ) and
                              IsBool( l[2][i].isUnderCollapsedCategory ) and
                              IsBound( l[2][i].isRejectedCategory ) and
                              IsBool( l[2][i].isRejectedCategory ) ) then
          Print( "#E  <obj>.dynamic.categories is inconsistent.\n" );
        fi;
      fi;
      # replay
      if IsBound( dynamic.replay ) then
        h:= dynamic.replay;
        if not ( IsRecord( h ) and IsBound( h.logs )
                               and IsBound( h.pointer ) ) then
          Print( "#E  <obj>.dynamic.replay must be a record\n",
                 "#E  with the components `logs' and `pointer'.\n" );
        else
          for log in h.logs do
            if IsBound( log.steps ) and
               not ( IsList( log.steps )
                     and ForAll( log.steps, IsPosInt ) ) then
              Print( "#E  <obj>.dynamic.replay.logs[...].steps must be ",
                     "a list of positive integers.\n" );
            fi;
            if IsBound( log.position ) and not IsPosInt( log.position ) then
              Print( "#E  <obj>.dynamic.replay.logs[...].position must be ",
                     "a positive integer.\n" );
            fi;
            if IsBound( log.replayInterval ) and
               not ( IsInt( log.replayInterval )
                     and 0 <= log.replayInterval ) then
              Print( "#E  <obj>.dynamic.replay.logs[...].replayInterval ",
                     "must be a nonnegative integer.\n" );
            fi;
            if IsBound( log.quiet ) and not IsBool( log.quiet ) then
              Print( "#E  <obj>.dynamic.replay.logs[...].quiet must be ",
                     "a Boolean.\n" );
            fi;
          od;
        fi;
      fi;
      if IsBound( dynamic.log ) then
        if not IsList( dynamic.log ) then
          Print( "#E  <obj>.dynamic.log must be a list.\n" );
        fi;
      fi;
    fi;

    # The object might be a valid input for `NCurses.BrowseGeneric'.
    return result;
end;


#############################################################################
##
#F  BrowseData.HeightWidthWindow( <t> )
##
##  It may happen that there is no component `<t>.dynamic.window';
##  in this case, we use `<t>.work.windowParameters'.
##
BrowseData.HeightWidthWindow:= function( t )
    if t.work.windowParameters{ [ 1, 2 ] } = [ 0, 0 ] then
      return NCurses.getmaxyx( 0 );
    else
      return t.work.windowParameters{ [ 1, 2 ] };
    fi;
end;


#############################################################################
##
#F  BrowseData.SortStableIndirection( <list1>, <list2>, <funs>, <direction> )
##
##  Let <list1> and <list2> be lists of the same length $n$, say,
##  and <funs> be a list of comparison functions (cf. "ref:Sort").
##  `BrowseData.SortStableIndirection' returns the list of length $n$
##  that contains the entries of <list2> in the order that is obtained by
##  sorting <list1> and <list2> in parallel (cf. "ref:SortParallel"),
##  where two entries of <list1> are compared w.r.t. <funcs>,
##  and additionally equal entries in <list1> are compared
##  via the corresponding entries in <list2> (w.r.t. `\<').
##
##  <direction> must be one of `"ascending"' or `"descending"',
##  the latter means that `true' and `false' for the comparison of
##  different values are interchanged.
##
BrowseData.SortStableIndirection:= function( list1, list2, funcs, direction )
    list1:= List( [ 1 .. Length( list1 ) ], i -> [ list1[i], list2[i] ] );
    Sort( list1, function( a, b )
                   local i;

                   for i in [ 1 .. Length( funcs ) ] do
                     if a[1][i] <> b[1][i] then
                       if funcs[i]( a[1][i], b[1][i] ) then
                         return direction[i] = "ascending";
                       else
                         return direction[i] <> "ascending";
                       fi;
                     fi;
                   od;
                   return a[2] < b[2];
                 end );
    return List( list1, x -> x[2] );
end;


#############################################################################
##
#F  BrowseData.IsDoneReplay( <replay> )
##
BrowseData.IsDoneReplay:= function( replay )
    while replay.pointer <= Length( replay.logs ) and
          replay.logs[ replay.pointer ].position
              > Length( replay.logs[ replay.pointer ].steps ) do
      replay.pointer:= replay.pointer + 1;
    od;
    return Length( replay.logs ) < replay.pointer;
end;


#############################################################################
##
#F  BrowseData.IsQuietSession( <replay> )
##
BrowseData.IsQuietSession:= function( replay )
    local pointer;

    if not NCurses.IsStdoutATty() then
      return true;
    fi;
    pointer:= replay.pointer;
    while pointer <= Length( replay.logs ) and
          replay.logs[ pointer ].position
              > Length( replay.logs[ pointer ].steps ) do
      pointer:= pointer + 1;
    od;
    return pointer <= Length( replay.logs ) and replay.logs[ pointer ].quiet;
end;


#############################################################################
##
#F  BrowseData.NextReplay( <replay> )
##
BrowseData.NextReplay:= function( replay )
    local currlog;

    while replay.pointer <= Length( replay.logs ) and
          replay.logs[ replay.pointer ].position
              > Length( replay.logs[ replay.pointer ].steps ) do
      replay.pointer:= replay.pointer + 1;
    od;
    currlog:= replay.logs[ replay.pointer ];
    currlog.position:= currlog.position + 1;
    return currlog.steps[ currlog.position - 1 ];
end;


#############################################################################
##
#T  BrowseData.SetReplay( <data> )
#F  BrowseData.SetReplay( false )
##
##  <#GAPDoc Label="SetReplay_man">
##  <ManSection>
##  <Func Name="BrowseData.SetReplay" Arg="data"/>
##
##  <Description>
##  This function sets and resets the value of
##  <C>BrowseData.defaults.dynamic.replay</C>.
##  <P/>
##  When <Ref Func="BrowseData.SetReplay"/> is called with a list <A>data</A>
##  as its argument then the entries are assumed to describe user inputs for
##  a browse table for which <Ref Func="NCurses.BrowseGeneric"/> will be
##  called afterwards, such that replay of the inputs runs.
##  (Valid input lists can be obtained from the component <C>dynamic.log</C>
##  of the browse table in question.)
##  <P/>
##  When <Ref Func="BrowseData.SetReplay"/> is called with the only argument
##  <K>false</K>,
##  the component is unbound (so replay is disabled, and thus calls to
##  <Ref Func="NCurses.BrowseGeneric"/> will require interactive user input).
##  <P/>
##  The replay feature should be used by initially setting the input list,
##  then running the replay (perhaps several times),
##  and finally unbinding the inputs,
##  such that subsequent uses of other browse tables do not erroneously
##  expect their input in <C>BrowseData.defaults.dynamic.replay</C>.
##  <P/>
##  Note that the value of <C>BrowseData.defaults.dynamic.replay</C> is used
##  in a call to <Ref Func="NCurses.BrowseGeneric"/> only if the browse table
##  in question does not have a component <C>dynamic.replay</C> before the
##  call.
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##
#T Admit more optional arguments, in order to prescribe intervals,
#T and eventually admit also records that are checked for consistency.
##
BrowseData.SetReplay:= function( arg )
    local log, data;

    if Length( arg ) = 1 and arg[1] = false then
      # Remove the stored value.
      Unbind( BrowseData.defaults.dynamic.replay );
    elif Length( arg ) = 1 and IsList( arg[1] ) then
      # Wrap the input list into a replay record.
      data:= rec( logs:= [ rec( steps:= arg[1] ) ], pointer:= 1 );
      BrowseData.defaults.dynamic.replay:= data;
#   elif Length( arg ) = 1 and IsRecord( arg[1] ) then
#     # Validate and set the record.
# Error( "not yet ..." );
    else
      Error( "BrowseData.SetReplay( <data> ) where <data> must be\n",
#            "`false' or a list of input characters/numbers or a record" );
             "`false' or a list of input characters/numbers" );
    fi;
end;


#############################################################################
##
#F  BrowseData.GetCharacter( <t> )
##
##  If <C>BrowseData.IsDoneReplay</C>
##  <Index Key="BrowseData.IsDoneReplay">
##  <C>BrowseData.IsDoneReplay</C></Index>
##  returns <K>true</K> for <A>t</A> then
##  get the next user input interactively,
##  otherwise read one character from <A>t</A><C>.dynamic.replay</C>.
##  <P/>
##  Add this character to the list <A>t</A><C>.dynamic.log</C> if the current
##  list of logs is not the first one.
##
BrowseData.GetCharacter:= function( t )
    local c, replay, currlog;

    if t = fail then
      c:= NCurses.wgetch( 0 );
      if c = NCurses.keys.MOUSE then
        c:= [ c, NCurses.GetMouseEvent() ];
      fi;
      return c;
    fi;
    replay:= t.dynamic.replay;
    if BrowseData.IsDoneReplay( replay ) then
      if not IsBound( t.dynamic.window ) then
        NCurses.SetTerm();
        NCurses.curs_set( 0 );
        t.dynamic.window:= CallFuncList( NCurses.newwin,
                                         t.work.windowParameters );
        t.dynamic.panel:= NCurses.new_panel( t.dynamic.window );
      fi;
      c:= NCurses.wgetch( t.dynamic.window );
      if c = NCurses.keys.MOUSE then
        c:= [ c, NCurses.GetMouseEvent() ];
      fi;
    else
      currlog:= replay.logs[ replay.pointer ];
      c:= currlog.steps[ currlog.position ];
      if IsChar( c ) then
        c:= IntChar( c );
      fi;
      currlog.position:= currlog.position + 1;
      if not currlog.quiet then
        NCurses.napms( currlog.replayInterval );
      fi;
    fi;
    if 1 < replay.pointer then
      Add( t.dynamic.log, c );
    fi;

    return c;
end;


#############################################################################
##
#F  BrowseData.GetPatternEditParameters( <prefix>, <default>, <paras>
#F                                       [, <t>] )
##
##  This is based on the utility <Ref Func="NCurses.GetLineFromUser"/>.
##  In addition to a string, values for given parameters can be chosen.
##  <P/>
##  <A>prefix</A> is an attribute line shown in front of the editable region,
##  <A>default</A> is the default pattern.
##  <A>paras</A> must be a list of triples
##  <C>[ para, values, default ]</C>,
##  where <C>para</C> is a string denoting a parameter,
##  <C>values</C> is the list of admissible values, and
##  <C>default</C> is the position of the default value in this list.
##  <P/>
##  The optional argument <A>t</A> must be a browse table;
##  it is used only for logging/replay.
##  </P>
##  If the user hits the <B>Esc</B> key then the dialog is cancelled,
##  <A>paras</A> is left unchanged, and <K>fail</K> is returned.
##  If the user hits the <B>Enter</B> key then <A>paras</A> is changed
##  in place and the string entered by the user is returned.
##
BrowseData.GetPatternEditParameters:= function( arg )
    local prefix, default, paras, t, localparas, index, win, yx, res, off,
          max, empty, pos, ins, small, currpara, smallheight, bigheight,
          width, winposy, winposx, loop, height, pan, ret, line, row, para,
          i, c, data, pressdata, buttondown;

    if   Length( arg ) = 3 and NCurses.IsAttributeLine( arg[1] )
                           and IsString( arg[2] ) and IsList( arg[3] ) then
      prefix:= arg[1];
      default:= arg[2];
      paras:= arg[3];
      t:= fail;
    elif Length( arg ) = 4 and NCurses.IsAttributeLine( arg[1] )
                           and IsString( arg[2] ) and IsList( arg[3] )
                           and IsRecord( arg[4] ) then
      prefix:= arg[1];
      default:= arg[2];
      paras:= arg[3];
      t:= arg[4];
    else
      Error( "usage: BrowseData.GetPatternEditParameters( <prefix>, ",
             "<default>, <paras>[, <t>] )" );
    fi;
    if t = fail or BrowseData.IsQuietSession( t.dynamic.replay ) then
      win:= 0;
    else
      win:= t.dynamic.window;
    fi;

    localparas:= List( paras, ShallowCopy );
    index:= [];
    for para in localparas do
      if Length( para ) < 4 or para[4]( localparas ) then
        Add( index, Position( localparas, para ) );
      fi;
    od;

    yx:= NCurses.getmaxyx( win );
    res:= ShallowCopy( default );
    off:= NCurses.WidthAttributeLine( prefix );
    max:= yx[2] - 6 - off;
    empty:= RepeatedString( " ", yx[2] - 6 );
    pos:= Length( res ) + 1;
    ins:= true;
    small:= true;
    currpara:= 1;
    if Length( localparas ) = 0 then
      smallheight:= 3;
      bigheight:= 3;
    else
      smallheight:= 4;
      bigheight:= 5;
    fi;
    width:= yx[2] - 4;

    winposy:= yx[1] - bigheight - Length( localparas ) - 1;
    winposx:= 2;

    loop:= function()
      # Create the window.
      height:= smallheight;
      if not small then
        height:= bigheight + Number( localparas,
                                     x -> Length( x ) < 4 or x[4]( localparas ) );
        winposy:= Minimum( winposy, yx[1] - height - 1 );
      fi;
      ret:= false;
      win:= fail;

      repeat

        if win <> fail then
          NCurses.del_panel( pan );
          NCurses.delwin( win );
        fi;
        if t <> fail and not BrowseData.IsQuietSession( t.dynamic.replay )
                     and IsBound( t.dynamic.statuspanel ) then
          NCurses.hide_panel( t.dynamic.statuspanel );
        fi;
        if t = fail or not BrowseData.IsQuietSession( t.dynamic.replay ) then
          win:= NCurses.newwin( height, width, winposy, winposx );
          pan:= NCurses.new_panel( win );
          NCurses.savetty();
          NCurses.SetTerm();
          NCurses.curs_set( 1 );
          NCurses.werase( win );
          if small then
            NCurses.wattrset( win, NCurses.attrs.BOLD );
            NCurses.wborder( win, 0 );
            NCurses.wattrset( win, NCurses.attrs.NORMAL );
          else
            NCurses.GridExt( win, rec(
              trow:= 0,
              brow:= height - 1,
              lcol:= 0,
              rcol:= yx[2] - 5,
              rowinds:= [ 0, 2, height - 1 ],
              colinds:= [ 0, yx[2] - 5 ] ), NCurses.attrs.BOLD );
          fi;

          # Show the dialog box.
          NCurses.PutLine( win, 1, 1, empty );
          line:= NCurses.ConcatenationAttributeLines(
                     [ prefix, [ NCurses.attrs.NORMAL, res ] ] );
          NCurses.PutLine( win, 1, 1, line );
          NCurses.wmove( win, 1, NCurses.WidthAttributeLine( line ) );
          if small then
            if 0 < Length( localparas ) then
              NCurses.PutLine( win, 2, 1, "(down to edit parameters)" );
            fi;
          else
            row:= 3;
            index:= [];
            for para in localparas do
              if Length( para ) < 4 or para[4]( localparas ) then
                Add( index, Position( localparas, para ) );
                line:= ShallowCopy( para[1] );
                for i in [ 1 .. Length( para[2] ) ] do
                  Append( line, "  [" );
                  if para[3] = i then
                    Add( line, 'X' );
                  else
                    Add( line, ' ' );
                  fi;
                  Append( line, "] " );
                  Append( line, para[2][i] );
                od;
                Append( line, RepeatedString( " ", yx[2] - 6 - Length( line ) ) );
#T admit distributing to two lines if necessary?
                if currpara + 2 = row then
                  line:= [ NCurses.attrs.STANDOUT, true, line ];
#T ... if yes then several rows may be marked here!
                fi;
                NCurses.PutLine( win, row, 1, line );
                row:= row + 1;
              fi;
            od;
            NCurses.PutLine( win, height - 2, 1,
         "(up/down to choose a parameter, left/right to change the value)" );
#T admit distributing to two lines if necessary
          fi;
          NCurses.wmove( win, 1, off + pos );
          NCurses.update_panels();
          NCurses.doupdate();
        fi;

        # Get a character and adjust the data.
        c:= BrowseData.GetCharacter( t );
        if   c = NCurses.keys.DOWN then
          if small then
            # Switch to the bigger window.
            ret:= true;
            break;
          elif currpara < Length( index ) then
            # Select the next parameter.
            currpara:= currpara + 1;
          fi;
        elif c = NCurses.keys.UP then
          if not small then
            if currpara > 1 then
              # Select the previous parameter.
              currpara:= currpara - 1;
            else
              # Switch to the small window.
              ret:= true;
              break;
            fi;
          fi;
        elif c = NCurses.keys.RIGHT then
          if small then
            if pos <= Length( res ) and pos < max then
              pos:= pos + 1;
            fi;
          else
            # Select the next value (with wrap around).
            localparas[ index[ currpara ] ][3]:= ( localparas[ index[ currpara ] ][3] mod
                Length( localparas[ index[ currpara ] ][2] ) ) + 1;
          fi;
        elif c = NCurses.keys.LEFT then
          if small then
            if pos > 1 then
              pos:= pos - 1;
            fi;
          else
            # Select the previous value (with wrap around).
            localparas[ index[ currpara ] ][3]:= ( ( localparas[ index[ currpara ] ][3] - 2 ) mod
                Length( localparas[ index[ currpara ] ][2] ) ) + 1;
          fi;
        elif c = NCurses.keys.IC then
          ins:= not ins;
        elif c = NCurses.keys.REPLACE then
          ins:= not ins;
        elif c in [ NCurses.keys.HOME, IntChar( '' ) ] then
          pos:= 1;
        elif c in [ NCurses.keys.END, IntChar( '' ) ] then
          pos:= Length( res ) + 1;
          if pos > max then
            pos:= pos - 1;
          fi;
        elif NCurses.IsBackspace( c ) then
          if pos > 1 then
            pos:= pos - 1;
            RemoveElmList( res, pos );
          fi;
        elif c in [ NCurses.keys.DC, IntChar( '' ) ] then
          if pos <= Length( res ) then
            RemoveElmList( res, pos );
          fi;
        elif IsList( c ) and c[1] = NCurses.keys.MOUSE then
          # If the first button is pressed on this window
          # then we expect that the dialog box shall be moved.
          # If the first button is released somewhere
          # then we move the alert box by the difference.
          data:= c[2];
          if 0 < Length( data ) then
            if   data[1].event = "BUTTON1_PRESSED" and data[1].win = win then
              pressdata:= data[ Length( data ) ];
              buttondown:= true;
            elif data[1].event = "BUTTON1_RELEASED" and buttondown then
              data:= data[ Length( data ) ];
              winposy:= Minimum( Maximum( 0, winposy + data.y - pressdata.y ),
                                 yx[1] - height );
              winposx:= Minimum( Maximum( 0, winposx + data.x - pressdata.x ),
                                 yx[2] - width );
              buttondown:= false;
            fi;
          fi;
        elif not c in [ NCurses.keys.ENTER, 
                        IntChar(NCurses.CTRL( 'M' )), 27 ] then
          if ins and Length( res ) < max then
            InsertElmList( res, pos, CHAR_INT( c mod 256 ) );
            pos:= pos + 1;
          elif not ins and pos <= max then
            res[ pos ]:= CHAR_INT( c mod 256 );
            pos:= pos + 1;
          fi;
        fi;
      until c in [ NCurses.keys.ENTER, IntChar(NCurses.CTRL( 'M' )), 27 ];

      if t <> fail and not BrowseData.IsQuietSession( t.dynamic.replay )
                   and IsBound( t.dynamic.statuspanel ) then
        NCurses.show_panel( t.dynamic.statuspanel );
      fi;

      if t = fail or not BrowseData.IsQuietSession( t.dynamic.replay ) then
        NCurses.del_panel( pan );
        NCurses.delwin( win );
        NCurses.resetty();
#       NCurses.endwin();
#T hier!
        NCurses.update_panels();
        NCurses.doupdate();
      fi;

      return ret;
    end;

    while loop() do
      if 0 < Length( localparas ) then
        small:= not small;
      fi;
    od;

    # Decide whether the user cancelled or submitted.
    if c = 27 then
      return fail;
    else
      for i in [ 1 .. Length( localparas ) ] do
        paras[i]:= localparas[i];
      od;
      return res;
    fi;
end;


#############################################################################
##
#F  BrowseData.CurrentMode( <t> )
##
BrowseData.CurrentMode:= t -> t.dynamic.activeModes[
                                  Length( t.dynamic.activeModes ) ];


#############################################################################
##
#F  BrowseData.PushMode( <t>, <modename> )
##
BrowseData.PushMode:= function( t, modename )
    local mode;

    mode:= First( t.work.availableModes, x -> x.name = modename );
    if mode <> fail then
      Add( t.dynamic.activeModes, mode );
      t.dynamic.changed:= true;
      return true;
    fi;
    return false;
end;


#############################################################################
##
#F  BrowseData.NumberFreeCharacters( <t>, <direction> )
##
##  If <direction> is `"vert"' then the return value is the number of rows in
##  the window of <t>, minus the number of rows needed for the column labels,
##  the header, and the footer;
##  otherwise the return value is the number of columns in the window of <t>,
##  minus the number of columns needed for the row labels.
##
BrowseData.NumberFreeCharacters := function( t, direction )
    local s, mode, header, footer;

    if direction = "vert" then
      s:= BrowseData.HeightWidthWindow( t )[1]
          - BrowseData.HeightLabelsColTable( t );
      mode:= BrowseData.CurrentMode( t ).name;
      header:= t.work.header;
      if   IsList( header ) then
        s:= s - Length( header );
      elif IsFunction( header ) then
        if not IsBound( t.work.headerLength.( mode ) ) then
          t.work.headerLength.( mode ):= Length( header( t ) );
        fi;
        s:= s - t.work.headerLength.( mode );
      elif IsRecord( header ) and IsBound( header.( mode ) ) then
        if not IsBound( t.work.headerLength.( mode ) ) then
          t.work.headerLength.( mode ):= Length( header.( mode )( t ) );
        fi;
        s:= s - t.work.headerLength.( mode );
      fi;
      footer:= t.work.footer;
      if   IsList( footer ) then
        s:= s - Length( footer );
      elif IsFunction( footer ) then
        if not IsBound( t.work.footerLength.( mode ) ) then
          t.work.footerLength.( mode ):= Length( footer( t ) );
        fi;
        s:= s - t.work.footerLength.( mode );
      elif IsRecord( footer ) and IsBound( footer.( mode ) ) then
        if not IsBound( t.work.footerLength.( mode ) ) then
          t.work.footerLength.( mode ):= Length( footer.( mode )( t ) );
        fi;
        s:= s - t.work.footerLength.( mode );
      fi;
    else
      s:= BrowseData.HeightWidthWindow( t )[2]
          - BrowseData.WidthLabelsRowTable( t );
    fi;
    return s;
end;


#############################################################################
##
#F  BrowseData.Separator( <sep>, <i> )
##
BrowseData.Separator:= function( sep, i )
    local result, j;

    if NCurses.IsAttributeLine( sep ) then
      result:= sep;
    else
      # a list of attribute lines
      result:= "";
      j:= i;
      while 0 < j and not IsBound( sep[j] ) do
        j:= j-1;
      od;
      if 0 < j then
        result:= sep[j];
        sep[i]:= result;
      fi;
    fi;
    return result;
end;


#############################################################################
##
#F  BrowseData.HeightCategories( <t>, <i>[, <l>] )
##
##  is the number of rows that are currently needed for the categories of
##  the <i>-th row of (the main matrix in) the browse table <t>,
##  including category separator rows.
##  If the optional argument <l> is given, only categories up to level <l>
##  are considered.
##  Note that collapsed/rejected categories do not count.
##
BrowseData.HeightCategories := function( arg )
    local t, i, l, cat, cats, k, len, entry;

    t:= arg[1];
    i:= arg[2];
    if Length( arg ) = 2 then
      l:= infinity;
    else
      l:= arg[3];
    fi;
    cat:= 0;
    cats:= t.dynamic.categories;
    k:= PositionSorted( cats[1], i );
    cats:= cats[2];
    len:= Length( cats );
    while k <= len do
      entry:= cats[k];
      if i < entry.pos
         or l < entry.level
         or entry.isUnderCollapsedCategory
         or entry.isRejectedCategory then
        break;
      else
        cat:= cat + 1;
        # If there is a category separator and the category is expanded
        # then also the separator counts.
        if NCurses.WidthAttributeLine( entry.separator ) <> 0 then
          if   k = len then
            if ForAny( [ i .. Length( t.dynamic.indexRow ) ],
                   x -> not BrowseData.IsHiddenCell( t, x, "vert" ) ) then
              cat:= cat + 1;
            fi;
          elif i < cats[ k+1 ].pos then
            if ForAny( [ i .. cats[ k+1 ].pos - 1 ],
                   x -> not BrowseData.IsHiddenCell( t, x, "vert" ) ) then
              cat:= cat + 1;
            fi;
          elif not ( cats[ k+1 ].isUnderCollapsedCategory
                     or cats[ k+1 ].isRejectedCategory ) then
            cat:= cat + 1;
          fi;
        fi;
      fi;
      k:= k + 1;
    od;

    return cat;
end;


#############################################################################
##
#F  BrowseData.HeightRow( <t>, <i> )
##
##  is the height of the <i>-th row of the `main' or `mainFormatted' matrix
##  in the browse table <t>.
##  Both row labels and entries of the main matrix are considered.
##  Hidden rows have height zero.
##
BrowseData.HeightRow := function( t, i )
    local result, k, mainfun, isfun, empty, row1, row2, j, entry, w;

    if BrowseData.IsHiddenCell( t, i, "vert" ) then
      return 0;
    fi;
    i:= t.dynamic.indexRow[i];
    if IsBound( t.work.heightRow[i] ) then
      return t.work.heightRow[i];
    fi;
    result:= 0;
    if i mod 2 = 1 then
      # This row is a separator row.
      if NCurses.WidthAttributeLine( BrowseData.Separator(
             t.work.sepRow, ( i + 1 ) / 2  ) ) <> 0 then
        result:= 1;
#T admit arbitrary heights for separators?
      fi;
    else
      k:= i/2;
      mainfun:= t.work.Main;
      isfun:= IsFunction( mainfun );
      empty:= t.work.emptyCell;
      row1:= [];
      row2:= [];
      if IsBound( t.work.mainFormatted[i] ) then
        row1:= t.work.mainFormatted[i];
      fi;
      if IsBound( t.work.main[k] ) then
        row2:= t.work.main[k];
      fi;
      for j in [ 1 .. t.work.n ] do
        if   IsBound( row1[ 2*j ] ) then
          entry:= row1[ 2*j ];
        elif IsBound( row2[j] ) then
          entry:= row2[j];
        elif isfun then
          entry:= mainfun( t, k, j );
        else
          entry:= empty;
        fi;
        w:= BrowseData.HeightEntry( entry );
        if result < w then
          result:= w;
        fi;
      od;
      if IsBound( t.work.labelsRow[k] ) then
        for entry in t.work.labelsRow[k] do
          w:= BrowseData.HeightEntry( entry );
          if result < w then
            result:= w;
          fi;
        od;
      fi;
    fi;
    t.work.heightRow[i]:= result;
    return result;
end;


#############################################################################
##
#F  BrowseData.HeightRowWithCategories( <t>, <i> )
##
BrowseData.HeightRowWithCategories := function( t, i )
    return BrowseData.HeightCategories( t, i )
           + BrowseData.HeightRow( t, i );
end;


#############################################################################
##
#F  BrowseData.HeightLabelsCol( <t>, <i> )
##
##  is the height of the <i>-th row of column labels in the browse table <t>.
##  Both corner rows and entries of the column label matrix are considered.
##  Hidden rows have height zero.
##
BrowseData.HeightLabelsCol := function( t, i )
    local result, k, entry, w;

    if   t.dynamic.isRejectedLabelsCol[i] then
      return 0;
    elif IsBound( t.work.heightLabelsCol[i] ) then
      return t.work.heightLabelsCol[i];
    fi;
    result:= 0;
    if i mod 2 = 1 then
      # This row is a separator row.
      if NCurses.WidthAttributeLine( BrowseData.Separator(
             t.work.sepLabelsCol, ( i + 1 ) / 2 ) ) <> 0 then
        result:= 1;
#T admit arbitrary heights for separators?
      fi;
    else
      k:= i/2;
      if IsBound( t.work.labelsCol[k] ) then
        for entry in t.work.labelsCol[k] do
          w:= BrowseData.HeightEntry( entry );
          if result < w then
            result:= w;
          fi;
        od;
      fi;
      if IsBound( t.work.corner[k] ) then
        for entry in t.work.corner[k] do
          w:= BrowseData.HeightEntry( entry );
          if result < w then
            result:= w;
          fi;
        od;
      fi;
    fi;
    t.work.heightLabelsCol[i]:= result;
    return result;
end;


#############################################################################
##
#F  BrowseData.WidthCol( <t>, <j> )
##
##  is the width of the <j>-th column of the `main' or `mainFormatted'
##  matrix in the browse table <t>.
##  Both column labels and entries of the main matrix are considered.
##  Hidden columns have width zero.
##
BrowseData.WidthCol := function( t, j )
    local result, k, mainfun, isfun, empty, i, row1, row2, entry, w, row;

    if BrowseData.IsHiddenCell( t, j, "horz" ) then
      return 0;
    fi;
    j:= t.dynamic.indexCol[j];
    if IsBound( t.work.widthCol[j] ) then
      return t.work.widthCol[j];
    fi;
    if j mod 2 = 1 then
      # This column is a separator column.
      result:= NCurses.WidthAttributeLine( BrowseData.Separator(
                   t.work.sepCol, ( j + 1 ) / 2 ) );
    else
      result:= 0;
      k:= j/2;
      mainfun:= t.work.Main;
      isfun:= IsFunction( mainfun );
      empty:= t.work.emptyCell;
      for i in [ 1 .. t.work.m ] do
        row1:= [];
        row2:= [];
        if IsBound( t.work.mainFormatted[ 2*i ] ) then
          row1:= t.work.mainFormatted[ 2*i ];
        fi;
        if IsBound( t.work.main[i] ) then
          row2:= t.work.main[i];
        fi;
        if   IsBound( row1[j] ) then
          entry:= row1[j];
        elif IsBound( row2[k] ) then
          entry:= row2[k];
        elif isfun then
          entry:= mainfun( t, i, k );
        else
          entry:= empty;
        fi;
        w:= BrowseData.WidthEntry( entry );
        if result < w then
          result:= w;
        fi;
      od;
      for row in t.work.labelsCol do
        if IsBound( row[k] ) then
          w:= BrowseData.WidthEntry( row[k] );
          if result < w then
            result:= w;
          fi;
        fi;
      od;
    fi;
    t.work.widthCol[j]:= result;
    return result;
end;


#############################################################################
##
#F  BrowseData.WidthLabelsRow( <t>, <j> )
##
##  is the width of the <j>-th column of row labels in the browse table <t>.
##  Both corner columns and entries of the row label matrix are considered.
##  Hidden columns have width zero.
##
BrowseData.WidthLabelsRow := function( t, j )
    local result, k, row, w;

    if   t.dynamic.isRejectedLabelsRow[j] then
      return 0;
    elif IsBound( t.work.widthLabelsRow[j] ) then
      return t.work.widthLabelsRow[j];
    fi;
    if j mod 2 = 1 then
      # This column is a separator column.
      result:= NCurses.WidthAttributeLine( BrowseData.Separator(
                   t.work.sepLabelsRow, ( j + 1 ) / 2 ) );
    else
      result:= 0;
      k:= j/2;
      for row in t.work.labelsRow do
        if IsBound( row[k] ) then
          w:= BrowseData.WidthEntry( row[k] );
          if result < w then
            result:= w;
          fi;
        fi;
      od;
      for row in t.work.corner do
        if IsBound( row[k] ) then
          w:= BrowseData.WidthEntry( row[k] );
          if result < w then
            result:= w;
          fi;
        fi;
      od;
    fi;
    t.work.widthLabelsRow[j]:= result;
    return result;
end;


#############################################################################
##
#F  BrowseData.HeightLabelsColTable( <t> )
#F  BrowseData.WidthLabelsRowTable( <t> )
##
BrowseData.HeightLabelsColTable := function( t )
    local result, i;

    result:= 0;
    for i in [ 1 .. 2 * t.work.m0 + 1 ] do
      result:= result + BrowseData.HeightLabelsCol( t, i );
    od;
    return result;
end;

BrowseData.WidthLabelsRowTable := function( t )
    local result, j;

    result:= 0;
    for j in [ 1 .. 2 * t.work.n0 + 1 ] do
      result:= result + BrowseData.WidthLabelsRow( t, j );
    od;
    return result;
end;


#############################################################################
##
#F  BrowseData.LengthCell( <t>, <i>, <direction> )
##
--> --------------------

--> maximum size reached

--> --------------------

[ Dauer der Verarbeitung: 0.41 Sekunden  (vorverarbeitet)  ]