#############################################################################
##
#W  ctbldisp.g            GAP 4 package `browse'                Thomas Breuer
##
##  <#GAPDoc Label="Ctbl_section">
##  <Section Label="sec:ctbldisp">
##  <Heading>Character Table Display</Heading>
##
##  The &GAP; library provides a <Ref Oper="Display" BookName="ref"/> method
##  for character tables
##  that breaks the table into columns fitting on the screen.
##  &Browse; provides an alternative,
##  using the standard facilities of the function
##  <Ref Func="NCurses.BrowseGeneric"/>, i.&nbsp;e.,
##  one can scroll in the matrix of character values,
##  searching and sorting are provided etc.
##  <P/>
##  The <Ref Oper="Browse"/> method for character tables can be called
##  instead of <Ref Oper="Display" BookName="ref"/>.
##  For convenience, one can additionally make this function the default
##  <Ref Oper="Display" BookName="ref"/> method for character tables,
##  by assigning it to the <C>Display</C> component in the global record
##  <C>CharacterTableDisplayDefaults.User</C>,
##  see&nbsp;<Ref Sect="Printing Character Tables" BookName="ref"/>;
##  for example, one can do this in one's <F>gaprc</F> file,
##  see&nbsp;<Ref Sect="The gap.ini and gaprc files" BookName="ref"/>.
##  (This can be undone by unbinding the component
##  <C>CharacterTableDisplayDefaults.User.Display</C>.)
##  <P/>
##  The function <Ref Func="BrowseDecompositionMatrix"/> can be used to
##  display decomposition matrices for Brauer character tables.
##
##  <#Include Label="Browse_Ctbl">
##  <#Include Label="BrowseDecompositionMatrix">
##
##  </Section>
##  <#/GAPDoc>
##


#############################################################################
##
#M  Browse( <tbl>[, <options>] )
##
##  <#GAPDoc Label="Browse_Ctbl">
##  <ManSection>
##  <Meth Name="Browse" Arg="tbl[, options]" Label="for character tables"/>
##
##  <Description>
##  This method displays the character table <A>tbl</A> in a window.
##  The optional record <A>options</A> describes what shall be displayed,
##  the supported components and the default values are described
##  in&nbsp;<Ref Sect="Printing Character Tables" BookName="ref"/>.
##  <P/>
##  The full functionality of the function
##  <Ref Func="NCurses.BrowseGeneric"/> is available.
##  <P/>
##  <Example><![CDATA[
##  gap> if TestPackageAvailability( "CTblLib" ) = true then
##  >      BrowseData.SetReplay( Concatenation(
##  >         # scroll in the table
##  >         "DRULdddddrrrrrlluu",
##  >         # select an entry and move it around
##  >         "seddrruuuddlll",
##  >         # search for the pattern 135 (six times)
##  >         "/135", [ NCurses.keys.ENTER ], "nnnnn",
##  >         # deselect the entry, select the first column
##  >         "qLsc",
##  >         # sort and categorize by this column
##  >         "sc",
##  >         # select the first row, move down the selection
##  >         "srdddd",
##  >         # expand the selected category, scroll the selection down
##  >         "xd",
##  >         # and quit the application
##  >         "Q" ) );
##  >      Browse( CharacterTable( "HN" ) );
##  >      BrowseData.SetReplay( false );
##  > fi;
##  ]]></Example>
##  <P/>
##  <E>Implementation remarks</E>:
##  The first part of the code in the <Ref Oper="Browse"/> method
##  for character tables is almost identical with the code for extracting
##  the data to be displayed from the input data in the &GAP; library
##  function <C>CharacterTableDisplayDefault</C>.
##  The second part of the code transforms these data into a browse table.
##  Character names and (if applicable) indicator values are used as row
##  labels,
##  and centralizer orders, power maps, and class names are used as column
##  labels.
##  The identifier of the table is used as the static header.
##  When an irrational entry is selected, a description of this entry is
##  shown in the dynamic footer.
##  <P/>
##  The standard modes in <Ref Var="BrowseData"/> (except the <C>help</C>
##  mode) have been extended by three new actions.
##  The first two of them open pagers giving an overview of all
##  irrationalities in the table, or of all those irrationalities that have
##  been shown on the screen in the current call, respectively.
##  The corresponding user inputs are the <B>I</B> and the <B>i</B> key.
##  (The names assigned to the irrationalities are generated column-wise.
##  If one just scrolls through the table, without jumping,
##  then these names coincide with the names generated by the default
##  <Ref Oper="Display" BookName="ref"/> method for character tables;
##  this is in general <E>not</E> the case,
##  for example when a row-wise search in the table is performed.)
##  The third new action, which is associated with the <B>p</B> key,
##  toggles the visibility status of the column label rows for centralizer
##  orders and power maps.
##  <P/>
##  An individual <C>minyx</C> function does not only check whether the
##  desired table fits into the window but also whether a table with too high
##  column labels (centralizer orders and power maps) would fit if these
##  labels get collapsed via the <B>p</B> key.
##  In this case, the labels are automatically collapsed, and the <B>p</B>
##  key is disabled.
##  <P/>
##  In order to keep the required space small also for large character
##  tables, caching of formatted matrix entries is disabled,
##  and the strings to be displayed are computed on demand with a
##  <C>Main</C> function in the <C>work</C> component of the browse table.
##  For the same reason, the constant height one for all table rows is set
##  in advance, so one need not inspect a whole character if only a few
##  values of it shall be shown.
##  <P/>
##  Special functions are provided for
##  sorting (concerning the comparison of character values,
##  which can be integers or irrationalities) and
##  categorizing the table by a column (the value in the category row
##  involves the class name of the column in question).
##  <P/>
##  The code can be found in the file <F>app/ctbldisp.g</F> of the package.
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##
if not IsBound( CambridgeMaps ) then
  CambridgeMaps:= "dummy";
fi;

InstallMethod( Browse,
    [ "IsNearlyCharacterTable" ],
    function( tbl )
    if   HasDisplayOptions( tbl ) then
      Browse( tbl, DisplayOptions( tbl ) );
    elif IsBound( CharacterTableDisplayDefaults.User ) then
      Browse( tbl, CharacterTableDisplayDefaults.User );
    else
      Browse( tbl, CharacterTableDisplayDefaults.Global );
    fi;
    end );


InstallOtherMethod( Browse,
    [ "IsNearlyCharacterTable", "IsList" ],
    function( tbl, chars )
    Browse( tbl, rec( chars:= chars ) );
    end );


InstallMethod( Browse,
    [ "IsNearlyCharacterTable", "IsRecord" ],
    function( tbl, options )
    local log, replay, record, stringEntry, stringEntryData, legend,
          cletter, centralizers, chars_from_irr, cnr, chars, charnames,
          classes, tbl_powermap, powermap, field_degrees, indicator, OD, nam,
          indic, i, oddata, ind2, entry, heights, modes, table, ind, p,
          degrees, j, list, hidecollabelspos, mode, pos;

    # Use only the argument record for the history.
    if IsBound( options.log ) and IsBound( options.replay ) then
      log:= options.log;
      replay:= options.replay;
    else
      log:= fail;
    fi;

    # Prepare a list of the available options records.
    options:= [ options ];
    if HasDisplayOptions( tbl ) and
       not IsIdenticalObj( options[1], DisplayOptions( tbl ) ) then
      Add( options, DisplayOptions( tbl ) );
    fi;
    if IsBound( CharacterTableDisplayDefaults.User ) and
       not IsIdenticalObj( options[1],
               CharacterTableDisplayDefaults.User ) then
      Add( options, CharacterTableDisplayDefaults.User );
    fi;
    if not IsIdenticalObj( options[1],
                CharacterTableDisplayDefaults.Global ) then
      Add( options, CharacterTableDisplayDefaults.Global );
    fi;

    # Get the options that are in at least one record.
    for record in options do
      if IsBound( record.StringEntry ) then
        stringEntry:= record.StringEntry;
        break;
      fi;
    od;
    for record in options do
      if IsBound( record.StringEntryData ) then
        stringEntryData:= record.StringEntryData( tbl );
        break;
      fi;
    od;
    for record in options do
      if IsBound( record.Legend ) then
        legend:= record.Legend;
        break;
      fi;
    od;
    for record in options do
      if IsBound( record.letter ) and Length( record.letter ) = 1 then
        cletter:= record.letter;
        break;
      fi;
    od;
    for record in options do
      if IsBound( record.centralizers ) then
        centralizers:= record.centralizers;
        break;
      fi;
    od;

    # Get the options that have no global default.
    # choice of characters and character names
    chars_from_irr:= true;
    for record in options do
      if IsBound( record.chars ) then
        if IsList( record.chars ) and ForAll( record.chars, IsPosInt ) then
          cnr:= record.chars;
          chars:= List( Irr( tbl ){ cnr }, ValuesOfClassFunction );
        elif IsInt( record.chars ) then
          cnr:= [ record.chars ];
          chars:= List( Irr( tbl ){ cnr }, ValuesOfClassFunction );
        elif IsHomogeneousList( record.chars ) then
          chars:= record.chars;
          cnr:= [ 1 .. Length( chars ) ];
          chars_from_irr:= false;
          if not IsBound( cletter ) then
            cletter:= "Y";
          fi;
        else
          cnr:= [];
          chars:= [];
        fi;
        break;
      fi;
    od;
    if not IsBound( chars ) then
      # Perhaps the irreducibles have to be computed here,
      # so we do not use this before evaluating the options.
      chars:= List( Irr( tbl ), ValuesOfClassFunction );
      cnr:= [ 1 .. Length( chars ) ];
      if HasCharacterNames( tbl ) then
        charnames:= CharacterNames( tbl );
      fi;
    fi;
    if not IsBound( cletter ) then
      cletter:= "X";
    fi;
    for record in options do
      if IsBound( record.charnames ) and IsList( record.charnames ) then
        charnames:= record.charnames;
        break;
      fi;
    od;
    if not IsBound( charnames ) then
      charnames:= List( cnr,
          i -> Concatenation( cletter, ".", String( i ) ) );
    fi;

    # choice of classes
    classes:= [ 1 .. NrConjugacyClasses( tbl ) ];
    for record in options do
      if IsBound( record.classes ) then
        if IsInt( record.classes ) then
          classes:= [ record.classes ];
        else
          classes:= record.classes;
        fi;
        break;
      fi;
    od;

    # choice of power maps
    tbl_powermap:= ComputedPowerMaps( tbl );
    powermap:= Filtered( [ 2 .. Length( tbl_powermap ) ],
                         x -> IsBound( tbl_powermap[x] ) );
    for record in options do
      if IsBound( record.powermap ) then
        if IsInt( record.powermap ) then
          IntersectSet( powermap, [ record.powermap ] );
        elif record.powermap = "ATLAS" and IsBound( CambridgeMaps ) then
          powermap:= "ATLAS";
          powermap:= CambridgeMaps( tbl );
        elif IsList( record.powermap ) then
          IntersectSet( powermap, record.powermap );
        elif record.powermap = false then
          powermap:= [];
        fi;
        break;
      fi;
    od;

    # print degrees of character fields?
    field_degrees:= false;
    for record in options do
      if IsBound( record.characterField ) then
        if record.characterField = true then
          field_degrees:= true;
        fi;
        break;
      fi;
    od;

    # print Frobenius-Schur indicators?
    indicator:= [];
    for record in options do
      if IsBound( record.indicator ) then
        if record.indicator = true then
          indicator:= [ 2 ];
        elif IsList( record.indicator ) then
          indicator:= Set( Filtered( record.indicator, IsPosInt ) );
        fi;
        break;
      fi;
    od;

    # print orthogonal discriminants
    OD:= false;
    if chars_from_irr then
      for record in options do
        if IsBound( record.OD ) then
          if record.OD = true then
            OD:= true;
          fi;
          break;
        fi;
      od;
    fi;

    # (end of options handling)

    # prepare class names
    if powermap = "ATLAS" then
      nam:= ClassNames( tbl, "ATLAS" );
    else
      nam:= ClassNames( tbl );
      for record in options do
        if IsBound( record.classnames ) and IsList( record.classnames ) then
          nam:= record.classnames;
          break;
        fi;
      od;
    fi;

    # prepare indicator and OD
    indic:= [];
    if OD then
      if not 2 in indicator then
        indicator:= Concatenation( [ 2 ], indicator );
      fi;
      indicator:= Concatenation( [ "OD" ], indicator );
    fi;
    if indicator <> [] then
      for i in [ 1 .. Length( indicator ) ] do
        if indicator[i] = "OD" then
          indic[1]:= ListWithIdenticalEntries( Length( cnr ), "" );
          if IsBoundGlobal( "OrthogonalDiscriminants" ) then
            oddata:= ValueGlobal( "OrthogonalDiscriminants" )( tbl );
            for j in [ 1 .. Length( cnr ) ] do
              if IsBound( oddata[ cnr[j] ] ) then
                indic[1][j]:= oddata[ cnr[j] ];
              fi;
            od;
          else
            # Show '?' and empty strings.
            ind2:= Indicator( tbl, Irr( tbl ){ cnr }, 2 );
            for j in [ 1 .. Length( cnr ) ] do
              if ( not ind2[ cnr[j] ] in [ -1, 0 ] ) and
                 Irr( tbl )[ cnr[j] ][1] mod 2 = 0 then
                indic[1][j]:= "?";
              fi;
            od;
          fi;
        else
          if chars_from_irr and
             IsBound( ComputedIndicators( tbl )[ indicator[i] ] ) then
            indic[i]:= ComputedIndicators( tbl )[ indicator[i] ]{ cnr };
          else
            indic[i]:= Indicator( tbl, chars, indicator[i] );
          fi;
        fi;
      od;
    fi;

    heights:= ListWithIdenticalEntries( 2 * Length( chars ) + 1, 0 );
    for i in [ 2, 4 .. 2 * Length( chars ) ] do
      heights[i]:= 1;
    od;

    # Construct the extended modes if necessary.
    if not IsBound( BrowseData.defaults.work.customizedModes.ctbldisp ) then
      # Create a shallow copy of each default mode for `Browse',
      # and add new actions to all available modes (except the help mode):
      # - i: Show a pager with the irrationalities that have been shown.
      # - I: Show a pager with all irrationalities in the table.
      # - p: Toggle power maps and centralizer orders (but not class names).
      modes:= List( BrowseData.defaults.work.availableModes,
                    BrowseData.ShallowCopyMode );
      BrowseData.defaults.work.customizedModes.ctbldisp:= modes;
      for mode in modes do
        if mode.name <> "help" then
          BrowseData.SetActions( mode, [
            [ [ "i" ], rec( helplines := [
                 "show an overview of those irrationalities in the table",
                 "that have been displayed in the current call" ],
              action := function( t )
                local str;

                str:= legend( t.work.legend );
                if IsEmpty( str ) then
                  str:= "(no irrationalities yet in this table)";
                fi;
                if BrowseData.IsDoneReplay( t.dynamic.replay ) then
                  NCurses.Pager( str );
                  NCurses.update_panels();
                  NCurses.doupdate();
                  NCurses.curs_set( 0 );
                fi;
                return t.dynamic.changed;
              end ) ],
            [ [ "I" ], rec( helplines := [
                 "show an overview of all irrationalities in the table" ],
              action := function( t )
                local j, i, str;

                # We proceed column-wise, like the traditional `Display'.
                for j in [ 1 .. t.work.n ] do
                  for i in [ 1 .. t.work.m ] do
                    stringEntry( t.dynamic.chars[i][ classes[j] ],
                                 t.work.legend );
                  od;
                od;
                str:= legend( t.work.legend );
                if IsEmpty( str ) then
                  str:= "(no irrationalities in this table)";
                fi;
                if BrowseData.IsDoneReplay( t.dynamic.replay ) then
                  NCurses.Pager( str );
                  NCurses.update_panels();
                  NCurses.doupdate();
                  NCurses.curs_set( 0 );
                fi;
                return t.dynamic.changed;
              end ) ],
            [ [ "p" ], rec( helplines := [
                 "toggle power maps and centralizer orders" ],
              action := function( t )
                local i;

                if t.dynamic.togglePowerMaps then
                  for i in t.dynamic.hidecollabelspos do
                    t.dynamic.isRejectedLabelsCol[i]:=
                        not t.dynamic.isRejectedLabelsCol[i];
                  od;
                  t.dynamic.changed:= true;
                else
                  BrowseData.AlertWithReplay( t,
                      "The screen is too small for expanding power maps.",
                      NCurses.attrs.BOLD );
                fi;
                return t.dynamic.changed;
              end ) ],
          ] );
        fi;
      od;
    else
      modes:= BrowseData.defaults.work.customizedModes.ctbldisp;
    fi;

    # Store the positions of centralizer orders and power maps.
    hidecollabelspos:= [];

    # Construct the browse table.
    table:= rec(
      work:= rec(
        align:= "ct",
        header:= [ Identifier( tbl ) ],
        footer:= rec(
          # Show a description of a selected irrational value.
          select_entry:= function( t )
            local entry, result, pos, q;

            if t.dynamic.selectedEntry = [ 0, 0 ] then
              entry:= First( t.dynamic.categories[2],
                x -> [ x.pos, x.level ] = t.dynamic.selectedCategory ).value;
              pos:= PositionSublist( entry, " = " );
              entry:= entry{ [ pos+3 .. Length( entry ) ] };
            else
              entry:= t.work.Main( t,
                t.dynamic.indexRow[ t.dynamic.selectedEntry[1] ] / 2,
                t.dynamic.indexCol[ t.dynamic.selectedEntry[2] ] / 2 );
            fi;
            result:= [ "", "", "" ];
            if Int( entry ) = fail then
              while 0 < Length( entry ) and entry[1] in "-/*" do
                entry:= entry{ [ 2 .. Length( entry ) ] };
              od;
              pos:= Position( t.work.legend.irrnames, entry );
              if pos <> fail then
                result[2]:= Concatenation( t.work.legend.irrnames[ pos ],
                    " = ", String( t.work.legend.irrstack[ pos ] ) );
                q:= Quadratic( t.work.legend.irrstack[ pos ] );
                if q = fail then
                  result[3]:= "";
                else
                  result[3]:= Concatenation( RepeatedString( " ",
                        Length( t.work.legend.irrnames[ pos ] ) ), " = ",
                      q.display, " = ", q.ATLAS );
                fi;
              fi;
            fi;
            return result;
          end ),
        CategoryValues:= function( t, i, j )
          return [ Concatenation( nam[ classes[ j/2 ] ], " = ",
                       t.work.Main( t, i/2, j/2 ) ) ];
          end,
        availableModes:= modes,

        # Avoid computing strings for all character values in advance.
        main:= [],
        Main:= function( t, i, j )
          return stringEntry( t.dynamic.chars[i][ classes[j] ],
                              t.work.legend );
        end,
        m:= Length( chars ),
        n:= Length( classes ),

        # The labels lists will be filled below.
        labelsRow:= [ List( charnames,
                        x -> rec( rows:= [ String( x ) ], align:= "l" ) ) ],
        labelsCol:= [],
        sepLabelsRow:= [ "" ],
        sepLabelsCol:= [],
        sepCol:= [ " " ],
        corner:= [],

        # Avoid computing all entries in a row for determining the heights.
        heightRow:= heights,

        legend:= stringEntryData,
      ),
      dynamic:= rec(
        activeModes:= [ First( modes, x -> x.name = "browse" ) ],
        sortFunctionForTableDefault:= BrowseData.CompareCharacterValues,
        sortFunctionForColumnsDefault:= BrowseData.CompareCharacterValues,
        sortFunctionForRowsDefault:= BrowseData.CompareCharacterValues,

        chars:= chars,

        # Store the indices relevant for hiding power maps.
        hidecollabelspos:= hidecollabelspos,

        # On small screens, it may be forbidden to expand power maps.
        togglePowerMaps:= true,
      ),
    );

    # Provide indicator columns.
    ind:= ListWithIdenticalEntries( Maximum( 1, Length( indicator ) ), "" );

    for i in [ 1 .. Length( indicator ) ] do
      Add( table.work.sepLabelsRow, " " );
      if indicator[i] = 2 then
        list:= [];
        for j in cnr do
          if   indic[i][j] = 0 then
            Add( list, "o" );
          elif indic[i][j] = 1 then
            Add( list, "+" );
          elif indic[i][j] = -1 then
            Add( list, "-" );
          else
            Add( list, "?" );
          fi;
        od;
        Add( table.work.labelsRow, list );
      else
        Add( table.work.labelsRow, List( indic[i]{ cnr }, String ) );
      fi;
    od;

    # Provide a column for the degrees of the character fields
    # as the first column after the character labels, with label "d".
    if field_degrees then
      Add( indicator, "d", 1 );
      Add( ind, " ", 1 );
      Add( table.work.sepLabelsRow, " " );
      p:= UnderlyingCharacteristic( tbl );
      if p = 0 then
        degrees:= List( chars, x -> Dimension( Field( Rationals, x ) ) );
      else
        degrees:= List( chars, x -> Length( Factors( SizeOfFieldOfDefinition(
          ClassFunction( tbl, x ), p ) ) ) );
      fi;
      Add( table.work.labelsRow, List( degrees, String ), 2 );
    fi;

    table.work.labelsRow:= TransposedMat( table.work.labelsRow );
    if indicator = [] then
      Append( table.work.sepLabelsRow, [ "", "" ] );
    else
      Append( table.work.sepLabelsRow, [ "" ] );
    fi;

    if centralizers = "ATLAS" then
      # Add centralizers themselves as column labels.
      Add( table.work.sepLabelsCol, " " );
      Add( table.work.corner, [ "" ] );
      Add( table.work.labelsCol,
           List( SizesCentralizers( tbl ){ classes }, String ) );
#T admit splitting into two rows!
      Append( hidecollabelspos,
              2 * Length( table.work.labelsCol ) + [ 0, 1 ] );
    elif centralizers = true then
      # Add factored centralizer orders to the column labels.
      Add( table.work.sepLabelsCol, " " );
      for p in PrimeDivisors( Size( tbl ) ) do
        Add( table.work.labelsCol, List( SizesCentralizers( tbl ){ classes },
               x -> stringEntry( Number( Factors( x ), y -> y = p ),
                                 stringEntryData ) ) );
        Append( hidecollabelspos,
                2 * Length( table.work.labelsCol ) + [ 0, 1 ] );
        Add( table.work.corner,
             Concatenation( ind, [ stringEntry( p, stringEntryData ) ] ) );
        Add( table.work.sepLabelsCol, "" );
      od;
      Unbind( table.work.sepLabelsCol[ Length( table.work.sepLabelsCol ) ] );
    fi;

    # class names and power maps
    if IsRecord( powermap ) then

      # three lines: power maps, p' part, and class names
      Append( table.work.sepLabelsCol, [ " ", "", "", " " ] );
      Append( hidecollabelspos,
              2 * Length( table.work.labelsCol ) + [ 2 .. 7 ] );
      Append( table.work.labelsCol, [ powermap.power{ classes },
                                      powermap.prime{ classes },
                                      powermap.names{ classes } ] );
      Append( table.work.corner, [ Concatenation( ind, [ "p " ] ),
                                   Concatenation( ind, [ "p'" ] ) ] );
      if   indicator = [] then
        Add( table.work.corner, [ "", "" ] );
      elif indicator = [ 2 ] then
        Add( table.work.corner, [ "", "ind" ] );
      else
        Add( table.work.corner,
             Concatenation( [ "ind" ], List( indicator, String ) ) );
      fi;

    else

      # first class names, then a row for each power map
      Add( table.work.sepLabelsCol, " " );
      Add( table.work.labelsCol, List( nam{ classes }, String ) );
      Add( table.work.corner, Concatenation( ind, [ "" ] ) );

      for p in powermap do
        list:= CompositionMaps( nam, tbl_powermap[p] ){ classes };
        for i in [ 1 .. Length( list ) ] do
          if not IsString( list[i] ) then
            list[i]:= "?";
          fi;
        od;
        Add( table.work.labelsCol, list );
        Append( hidecollabelspos,
                2 * Length( table.work.labelsCol ) + [ 0, 1 ] );
        Add( table.work.corner,
             Concatenation( ind, [ Concatenation( String( p ), "P" ) ] ) );
        Add( table.work.sepLabelsCol, "" );
      od;

      # Add a data row showing the indicator header (may be empty).
      Add( table.work.sepLabelsCol, "" );
      Add( table.work.sepLabelsCol, "" );
      Add( table.work.corner,
           Concatenation( [ "" ], List( indicator, String ) ) );

    fi;

    if indicator <> [] and powermap <> [] then
      # Replace the separator before the last indicator by empty
      # if its column is only one character wide
      # and if power maps are shown.
      pos:= Length( indicator ) + 1;
      if indicator[ Length( indicator ) ] < 10 and
         ForAll( table.work.labelsRow, x -> Length( x[ pos ] ) <= 1 ) then
        table.work.sepLabelsRow[ pos ]:= "";
      fi;
    fi;

    # Check whether at least one character and at least the degree column
    # fit into the window (the footer needs at most three lines).
    # If the window is not high enough for the column labels then hide them
    # and forbid expanding them.
    table.work.minyx:= [ function( t )
      local maxyx, minyx;

      maxyx:= NCurses.getmaxyx( 0 );
      minyx:= BrowseData.MinimalWindowHeight( t ) + 3;
      if maxyx[1] < minyx then
        # Try to collapse power maps.
        if t.dynamic.togglePowerMaps then
          for i in t.dynamic.hidecollabelspos do
            t.dynamic.isRejectedLabelsCol[i]:= true;
          od;
          t.dynamic.togglePowerMaps:= false;
          minyx:= BrowseData.MinimalWindowHeight( t ) + 3;
        fi;
      fi;

      return minyx;
      end,
      BrowseData.MinimalWindowWidth ];

    # Enter the history if available.
    if log <> fail then
      table.dynamic.log:= log;
      table.dynamic.replay:= replay;
    fi;

    # Show the table.
    NCurses.BrowseGeneric( table );
end );

if CambridgeMaps = "dummy" then
  Unbind( CambridgeMaps );
fi;


#############################################################################
##
#F  BrowseDecompositionMatrix( <modtbl>[, <b>][, <options>] )
##
##  <#GAPDoc Label="BrowseDecompositionMatrix">
##  <ManSection>
##  <Func Name="BrowseDecompositionMatrix" Arg="modtbl[, b][, options]"/>
##
##  <Description>
##  This method displays the decomposition matrix of (the <A>b</A>-th block
##  of) the Brauer character table <A>modtbl</A> in a window.
##  The arguments are the same as for
##  <Ref Func="LaTeXStringDecompositionMatrix" BookName="ref"/>.
##  <P/>
##  The positions of the ordinary and modular irreducible characters
##  are shown in the labels of the rows and columns, respectively,
##  that are indexed by these characters.
##  When an entry in the decomposition matrix is selected then information
##  about the degrees of these characters is shown in the table footer.
##  <P/>
##  The full functionality of the function
##  <Ref Func="NCurses.BrowseGeneric"/> is available.
##  <P/>
##  <Example><![CDATA[
##  gap> BrowseData.SetReplay( Concatenation(
##  >         # select the first entry
##  >         "se",
##  >         # scroll in the table
##  >         "drrrr",
##  >         # keep the table open for a while
##  >         [ 14, 14, 14, 14, 14 ],
##  >         # and quit the application
##  >         "Q" ) );
##  gap> BrowseDecompositionMatrix( CharacterTable( "J1" ) mod 2 );
##  gap> BrowseData.SetReplay( false );
##  ]]></Example>
##  <P/>
##  The code can be found in the file <F>app/ctbldisp.g</F> of the package.
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##
BindGlobal( "BrowseDecompositionMatrix", function( arg )
    local options,       # record with labels, optional third argument
          blocknr,       # number of the block, optional second argument
          modtbl,        # Brauer character table, first argument
          decmat,        # decomposition matrix
          r, j,
          phi,           # string used for Brauer characters
          chi,           # string used for ordinary irreducibles
          collabels,     # indices of Brauer characters
          rowlabels,     # indices of ordinary characters
          block,         # block information on `modtbl'
          ordtbl, p, irr, orddegrees, moddegrees, extrowlabels, extcollabels,
          header, table;

    # Get and check the arguments.
    if   Length( arg ) = 2 and IsBrauerTable( arg[1] )
                           and IsRecord( arg[2] ) then
      options := arg[2];
    elif Length( arg ) = 2 and IsBrauerTable( arg[1] )
                           and IsInt( arg[2] ) then
      blocknr := arg[2];
      options := rec();
    elif Length( arg ) = 3 and IsBrauerTable( arg[1] )
                           and IsInt( arg[2] )
                           and IsRecord( arg[3] ) then
      blocknr := arg[2];
      options := arg[3];
    elif Length( arg ) = 1 and IsBrauerTable( arg[1] ) then
      options := rec();
    else
      Error( "usage: BrowseDecompositionMatrix(",
             " <modtbl>[, <blocknr>][, <options>] )" );
    fi;

    # Compute the decomposition matrix.
    modtbl:= arg[1];
    if IsBound( options.decmat ) then
      decmat:= options.decmat;
    elif IsBound( blocknr ) then
      decmat:= DecompositionMatrix( modtbl, blocknr );
    else
      decmat:= DecompositionMatrix( modtbl );
    fi;
    decmat:= List( decmat, ShallowCopy );
    for r in decmat do
      for j in [ 1 .. Length( r ) ] do
        if r[j] = 0 then
          r[j]:= ".";
        else
          r[j]:= String( r[j] );
        fi;
      od;
    od;

    # Choose default labels if necessary.
    phi:= "Y";
    chi:= "X";

    # Construct the labels if necessary.
    if IsBound( options.phi ) then
      phi:= options.phi;
    fi;
    if IsBound( options.chi ) then
      chi:= options.chi;
    fi;
    if IsBound( options.collabels ) then
      collabels:= options.collabels;
      if ForAll( collabels, IsInt ) then
        collabels:= List( collabels,
            i -> Concatenation( phi, "_", String(i) ) );
      fi;
    fi;
    if IsBound( options.rowlabels ) then
      rowlabels:= options.rowlabels;
      if ForAll( rowlabels, IsInt ) then
        rowlabels:= List( rowlabels,
            i -> Concatenation( chi, "_", String(i) ) );
      fi;
    fi;

    # Construct the labels if they are still missing.
    if not IsBound( collabels ) then
      if IsBound( blocknr ) then
        block     := BlocksInfo( modtbl )[ blocknr ];
        collabels := List( block.modchars, String );
      else
        collabels := List( [ 1 .. Length( decmat[1] ) ], String );
      fi;
      collabels:= List( collabels, i -> Concatenation( phi, "_", i ) );
    fi;
    if not IsBound( rowlabels ) then
      if IsBound( blocknr ) then
        block     := BlocksInfo( modtbl )[ blocknr ];
        rowlabels := List( block.ordchars, String );
      else
        rowlabels := List( [ 1 .. Length( decmat ) ], String );
      fi;
      rowlabels:= List( rowlabels, i -> Concatenation( chi, "_", i ) );
    fi;

    # Construct the information to be shown in the footer.
    ordtbl:= OrdinaryCharacterTable( modtbl );
    p:= UnderlyingCharacteristic( modtbl );
    irr:= List( Irr( ordtbl ), x -> x[1] );
    orddegrees:= List( [ 1 .. Length( irr ) ],
                       i -> Concatenation( String( irr[i] ), "_",
                                String( Number( [ 1 .. i ],
                                              x -> irr[x] = irr[i] ) ) ) );
    irr:= List( Irr( modtbl ), x -> x[1] );
    moddegrees:= List( [ 1 .. Length( irr ) ],
                       i -> Concatenation( String( irr[i] ), "_",
                                String( Number( [ 1 .. i ],
                                              x -> irr[x] = irr[i] ) ) ) );
    if IsBound( blocknr ) then
      orddegrees:= orddegrees{ block.ordchars };
      moddegrees:= moddegrees{ block.modchars };
    fi;
    extcollabels:= List( [ 1 .. Length( collabels ) ],
        i -> Concatenation( collabels[i], " = ", moddegrees[i] ) );
    extrowlabels:= List( [ 1 .. Length( rowlabels ) ],
        i -> Concatenation( rowlabels[i], " = ", orddegrees[i] ) );

    header:= Concatenation( Identifier( ordtbl ), " mod ", String( p ) );
    if IsBound( blocknr ) then
      Append( header, ", block " );
      Append( header, String( blocknr ) );
      Append( header, ", defect " );
      Append( header, String( PrimeBlocks( ordtbl, p ).defect[ blocknr ] ) );
    fi;

    # Construct the browse table.
    table:= rec(
      work:= rec(
        align:= "ct",
        header:= [ header, "" ],
        footer:= rec(
          # Show the degrees of the two irreducibles involved.
          select_entry:= function( t )
            local pos;

            if t.dynamic.selectedEntry = [ 0, 0 ] then
              return [ "", "", "" ];
            else
              pos:= [ t.dynamic.indexRow[ t.dynamic.selectedEntry[1] ] / 2,
                      t.dynamic.indexCol[ t.dynamic.selectedEntry[2] ] / 2 ];
              return [ "", extrowlabels[ pos[1] ], extcollabels[ pos[2] ] ];
            fi;
          end ),
        main:= decmat,
        labelsRow:= List( rowlabels,
                          x -> [ rec( rows:= [ x ], align:= "l" ) ] ),
        labelsCol:= [ List( collabels,
                            x -> rec( rows:= [ x ], align:= "l" ) ) ],
        sepLabelsRow:= [ "", "|" ],
        sepLabelsCol:= [ "", "-" ],
        sepCol:= [ " " ],
        corner:= [],
        SpecialGrid:= BrowseData.SpecialGridLineDrawPlus,
      ),
      dynamic:= rec(
      ),
    );

    # Enter the history if available.
    if IsBound( options.log ) and IsBound( options.replay ) then
      table.dynamic.log:= options.log;
      table.dynamic.replay:= options.replay;
    fi;

    NCurses.BrowseGeneric( table );
end );


#############################################################################
##
#E

