#!/usr/bin/env python
# 
# Displays a table and calls coot.
#
# Author       : Chris Weichenberger
# Created      : 2011-10-26
# License
# Twilight is a GUI for displaying ligands incompatible with electron density.
#    Copyright (C) 2012  Christian X. Weichenberger, Edwin Pozharski, and
#    Bernhard Rupp.
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Modifications:
# 2011-12-18 (CW) First release to the public. (v1.0)
# 2011-12-22 (EP) Added coot executable. (v1.1)
# 2012-02-08 (CW) Added sigma contour level as a parameter. (v1.2)
# 2012-02-10 (CW) Added journal name and PMID (v2.0).
# 2012-02-14 (CW) Added versioned file names (v2.1).
# 2012-02-15 (CW) Branched development to PyGtk. 
# 2012-02-24 (CW) Major rewrite. End of almost all command line params. Config
#   file instead. Preferences dialog based on config file started.
# 2012-02-29 (CW) ED edits. Coot window prefs in config, Save as... dialog and
#   user feedback on currently edited file name in window title (v0.91).
# 2012-03-02 (CW) Table load by menu bar action. Removed legacy parameters and
#   refactored code a bit. (v.0.92)
# 2012-03-07 (CW) All config file parameters now fully configurable from the
#   preferences menu. Coot temporary directory became a config file parameter.
#   (v0.93).
# 2012-03-08 (CW) Tooltips for Preferences window. (v0.94)
# 2012-03-14 (CW) Added Python/Scheme radiobutton and Ed's coot script. (v0.95)
# 2012-03-15 (CW) Sorted list of het groups all over, facilitates orientation.
#   (v0.96)
# 2012-03-23 (CW) Partial undo of latest sort function: Keep order as in table
#   for jumping forth and back in coot. (v0.97)
# 2012-03-27 (CW) Removed parameter class. (v0.98)
# 2012-05-02 (CW) Added GPLv3 license and About dialog (v0.99)
# 2012-05-02 (CW) Added main table mouse overs, first version that appears to
#   be done. (v1.00)
# 2012-05-09 (CW) Removed bug resulting from a sorting order of a non-Rank
#   column. Altered Assign menu entry names. (v1.01)
# 2012-05-14 (CW) Added release date and refinement software columns. Made
#   columns resizable. Software column fixed width. Border width unified.
#   (v1.02)
# 2012-05-21 (CW) Smarter adding of user stamp. If the comment already starts
#   with the user's abbreviated name, it is not prepended. (v1.03)
# 2012-06-06 (CW) Some flags for figure preparation mode (v1.04).
# 2012-06-14 (CW) Fixed journal column to show full journal name. (v1.05).
# 2012-06-24 (CW) Baptized it Twilight (v1.06).
# 2012-07-27 (CW) Manual loading of arbitrary PDB entries with ligands. (v1.07)
# 2012-07-30 (CW) Bug removed when importing PDB entries on Rank sorted column.
# 2012-07-31 (CW) <return> key loads structure in import widget.
# 2012-08-01 (CW) PdbFile instance always kept in sync with the config. (v1.08)
# 2012-08-02 (CW) Bug for Rank sorted columns and import remained. Fix. (v1.09)
# 2012-10-07 (CW) Empty table at startup and New in File menu. Error handling
#   when writing to a non-writable file. 
# 2012-10-10 (CW) More informative error messages when retrieving entries.
# 2012-10-14 (CW) Rewrote meaning of bad ligands. Good and bad ones, both can
#   now be shown in the viewer, though only exclusively. Group sort by
#   Combined Score or RSCC. Good and bad RSCC coloring of ligands. Support for
#   color vision deficiencies. (v1.10)  
# 2012-10-23 (CW) Highlight the selected ligand in the ligand table,
#   irrespective of good/bad viewing mode. (v1.11)
#
# known bugs:
#
#


###########
###########
### TOO DOO
###########
###########

# Switch to showing only valid ligands in Ed's scheme coot script.

import os
import sys
import gzip
import argparse
import subprocess
import webbrowser
try:
    import pygtk
    pygtk.require('2.0')
    import gtk
    import gobject;
except ImportError:
    sys.stderr.write("Could not import PyGtk. Please check installation.\n");
    sys.exit(1);

from misc import CalcCombinedScore, EMPTYFN, UNTITLEDFN, NONLIGANDS,\
     RSCCBADTH, RSCCGOODTH;
from edstable import CEDSTableData;
from pdbfile import CPDBFile, PDB_OK, PDB_NO_CONNECTION,\
     PDB_PDBENTRY_NOT_EXISTS, PDB_EDSENTRY_NOT_EXISTS,\
     PDB_EDS_INVALID_STATLIS,PDB_NO_LIGANDS_FOUND;

from files import GetVersionedInOutFileNames;
from optconf import CDysConfigurer;

# Program version
__version__         = "1.11";

# Main window title
MAIN_WINDOW_TITLE = "Ligand Dysplayer";
# Preferences window title.
PREF_WINDOW_TITLE = "Preferences";
# Import EDS and PDB window.
IMPORTEDSPDB_WINDOW_TITLE = "Import EDS and PDB";
# Load dialog
LOAD_TITLE = "Load table...";
# Save as... dialog
SAVE_AS_TITLE = "Save table as...";
# File choosing dialog for coot window title.
COOT_FILE_SEL_WINDOW_TITLE = "Choose coot executable";
# File choosing dialog for coot window title.
PDB_DIR_SEL_WINDOW_TITLE = "Select PDB mirror directory";
# Same for PyMOL
PYMOL_FILE_SEL_WINDOW_TITLE = "Choose PyMOL executable";

# File is not saved dialog.
TABLE_MODIFIED_CONTINUE_TEXT = "Table has been modified. Continue anyway?";
# Invalid input file format
INVALID_FILE_FORMAT_TEXT = "Invalid file format. No data has been read.";
# No electron density data found on EDS server.
NO_EDS_DATA_AVAILABLE_TEXT = "No EDS entry available for PDB code ``%s''.";
# There is no ligand in the PDB file.
NO_LIGAND_DATA_AVAILABLE_TEXT = "PDB code ``%s'' contains no ligands.";
# PDB entry is not available on PDB server, ie. invalid PDB code.
INVALID_PDBENTRY_TEXT = "PDB entry ``%s'' does not exist on RCSB PDB server (Invalid PDB code)."
# (coot) file is not executable.
FILE_NOT_EXECTUTABLE_TEXT="File is not executable."
# Somehow this file is not there.
FILE_NOT_EXIST_TEXT="File does not exist."
# Not a directory has been selected.
NOT_A_DIRECTORY_TEXT="Not a directory."
# Directory cannot be cd'd.
CANNOT_CHG2DIRECTORY_TEXT="Cannot change to directory."
# Can't write to dir.
NO_WRITE_PERMS_TEXT= "No write permission on directory."
# Can't write to file.
COULD_NOT_WRITE_FILE_TEXT = "Could not write file ``%s''. Permissions ok?";
# URL could not be opened.
CONNECTION_FAILED_TEXT = "Connection problem. Could not fetch data from server.";

# File menu entries
FILE_MNU_FETCH_EDS_TEXT = "Fetch EDS and PDB...";

# The license text as it appears in the About window.
LICENSE_TEXT = """Twilight %s

Twilight is a GUI for displaying ligands incompatible with electron density.

Copyright (C) 2012  Christian X. Weichenberger, Edwin Pozharski, and Bernhard Rupp.

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

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

# Tooltip texts
# These are for the Preferences window.
CONTOUR_2FOFC_TTEXT = "Contour level in units of standard deviations the \
2FoFc map is drawn on coot startup.";
CONTOUR_FOFC_TTEXT = "Contour level in units of standard deviations the \
FoFc map is drawn on coot startup."
COOT_WIN_WIDTH_TTEXT = "Coot window width after launching."
COOT_WIN_HEIGHT_TTEXT = "Coot window height after launching."
COOT_BINARY_TTEXT = "Full path file name of coot executable, in case it is \
not in the default search path. If it is, simply put in the command line \
executable name in here. This is either 'coot' on Unix-based systems or \
'runwincoot.bat' on MS-Windows.";
PDB_MIRROR_DIR_TTEXT = "Directory name of a local PDB mirror for retrieval \
of coordinate files without connecting to the PDB server. This directory \
name is the root of a tree that usually is generated by rsyncing with the \
PDB server."
TEMP_FILES_DIR_TTEXT = "Directory for storing downloaded coordinate and \
electron density files. This directory needs to have write permissions. \
Files are kept in between sessions. Structure files are loaded from here \
before connecting to the PDB or the EDS server for file retrieval.";
USER_ID_TTEXT = "Short name of the annotator. This will automatically \
be prefixed to the annotation text."
SCRIPT_PYTHON_TTEXT = "Use coot's Python scripting engine for exchanging \
data (recommended).";
SCRIPT_SCHEME_TTEXT = "Use coot's Scheme scripting engine for exchanging \
data (currently does not implement full functionality.";
CONFIG_FILENAME_TTEXT = "File name where all configuration options entered \
in this window are saved to. You can manually edit this file, especially \
if you want to rephrase or extend the annotation texts.";
# These are for the Import from EDS dialog.
EDS_PDB_ENTRY_TTEXT = "PDB four letter code for retrieval of a structure \
and its corresponding electron density from EDS.";

RANK_TTEXT = "Rank shows highest Score values first and lists below all other \
ligands from the same PDB entry that have passed the filter criteria.";
PDBID_TTEXT = "PDB four letter code.";
LIGNM_TTEXT = "Ligand three letter residue name.";
RESNR_TTEXT = "Chain name, residue number and insertion code.";
RSCC_TTEXT = "Real space correlation coefficient.";
OWAB_TTEXT = "Occupancy-weighted average B-factor.";
MOLWT_TTEXT = "Molecular weight of ligand.";
CHARGE_TTEXT = "Overall charge of ligand.";
NRHEAVY_TTEXT = "Number of heavy atoms of ligand.";
NRHYDRO_TTEXT = "Number of hydrogen atoms of ligand.";
NRATOMS_TTEXT = "Overall number of ligand atoms, heavy atoms and hydrogens.";
RELDAT_TTEXT = "Deposit date of PDB entry.";
RESOL_TTEXT = "Experimental resolution of the structure."
RWORK_TTEXT = "R-value of the working set.";
RFREE_TTEXT = "R-value of the testing set.";
ROBS_TTEXT = "R-value of both the test and working sets.";
SW_TTEXT = "Software used during refinement of crystal structure.";
JRNL_TTEXT = "Abbreviation of the journal the structure was published in.";
PMID_TTEXT = "Numeric PubMed identfier for the publication, if available. \
Otherwise, a hyphen (-) indicates that there is no publication.";
CLASS_TTEXT = "Classification code of the ligand. 'B' signifies it is a \
frequently used buffer molecule.";
VALID_TTEXT = "Currently this column is always set to yes (Y).";
SCORE_TTEXT = "Score combining Resol and RSCC into a single number.";
COMMENT_TTEXT = "Author's annotation.";

MAIN_TABLE_TOOLTIP_TEXTS = [PDBID_TTEXT, LIGNM_TTEXT, RESNR_TTEXT,
                            RSCC_TTEXT, OWAB_TTEXT, MOLWT_TTEXT, CHARGE_TTEXT,
                            NRHEAVY_TTEXT, NRHYDRO_TTEXT, NRATOMS_TTEXT,
                            RELDAT_TTEXT, RESOL_TTEXT, RWORK_TTEXT,
                            RFREE_TTEXT, ROBS_TTEXT, SW_TTEXT, JRNL_TTEXT,
                            PMID_TTEXT, CLASS_TTEXT, VALID_TTEXT, SCORE_TTEXT,
                            COMMENT_TTEXT]; 
# Switch for manuscript figure display settings
PREPARE_FIGURE_MODE = False;

# Number of columns for the ligand array
LIG_TABLE_MAX_COL = 10;
# Font size (point) of the monospace fixed font for the two tables.
TABLES_FONT_SIZE_PTS = 8;

# Border width of the main views EDS Table, Ligand detail viewer and Status line
BORDER_WIDTH   = 7;
# Minimal width of any resizable column
MIN_COL_WIDTH  = TABLES_FONT_SIZE_PTS*3;
# Width of the Softwre column, too large by default.
SW_COL_WIDTH   = TABLES_FONT_SIZE_PTS*11;
# Width of the Jrnl column, too large by default.
JRNL_COL_WIDTH = TABLES_FONT_SIZE_PTS*10;


# Colors for hetgroup display table.
LOW_RSCC_COL     = "#f1641d";
IN_FOCUS_BG_COL  = "#cccccc";
HIGHLIGHT_FG_COL = "#ffffff";
HIGHLIGHT_BG_COL = "#4b6983";
BLACK_COL        = "#000000";
HIGH_RSCC_COL    = "#00a277";
DEFAULT_COL      = None;

# Colors for distinguishing manually added structures from those in the
# precompiled table.
VALID_ROW_COL          = None;
MANUALLY_ADDED_ROW_COL = "#d0e1ff";

# Status messages.
STAT_FETCHING_PDB_ENTRY = "Fetching PDB entry from RCSB PDB server.";
STAT_FETCHING_EDS_DATA  = "Fetching electron density data from EDS server.";
STAT_LAUNCHING_COOT     = "Launching coot...";
STAT_SAVING_FILE        = "Saving file ``%s''...";
STAT_LOADING_FILE       = "Loading file ``%s''..."

# Timeout for clearing status line (ms)
CLR_TIMEOUT = 1500;

# Callback globals
MNU_SEND_TO_PDB     = "sendPDB";
MNU_SEND_TO_EDS     = "sendEDS";
MNU_SEND_TO_PMED    = "sendPubMed";
MNU_SEND_TO_PDBREDO = "sendPDB_REDO";

# URLs for looking up data from a table row.
URL_PDB     = "http://www.pdb.org/pdb/explore/explore.do?structureId=%s"
URL_EDS     = "http://eds.bmc.uu.se/cgi-bin/eds/uusfs?pdbCode=%s"
URL_PDBREDO = "http://www.cmbi.ru.nl/pdb_redo/%s/%s/index.html"
URL_PUBMED  = "http://www.ncbi.nlm.nih.gov/pubmed/%s"

class CGtkDysplayer:
    """GUI for analyzing ligands not compatible with their respective electron
    density.
    """
    def __init__(s, tableFN, configFN, sortS=True, badLigands=True):
        """Constructor.
        
        @param tableFN  EDS table file name, EMPTYFN indicates empty table.
        @param configFN  Configuration file name.
        @param sortS  @c True, if we complex sort by score S, @c if we sort by
         RSCC only.
        @param badLigands: @c True, show the list of edns-incompatible
         ligands, else show the highly compatible ones.
        """
        s._EDSInTableFN = tableFN;
        if( configFN!="" ):
            # have a config file name to load or create other than the default.
            dirNm, confFN = os.path.split(configFN);
            s._DysConfig = CDysConfigurer(defaultarget=dirNm,
                                          cfgname=confFN);
        else:
            # default config name and location.
            s._DysConfig = CDysConfigurer();
        cfg = s._DysConfig.GetConfig();
        s._Table = CEDSTableData();
        if( sortS ):
            s._Table.SetSortingScheme(s._Table.SCOREIDX, False);
        else:
            s._Table.SetSortingScheme(s._Table.RSCCIDX, True);
        if( badLigands ):
            s._Table.SetFocusOnBad();
        else:
            s._Table.SetFocusOnGood();
        s._PdbFile = CPDBFile();
        s._UpdateFromConfig();
        s.populatedPDBID = "";
        s.tableModified = False;
        s.__BuildWindow();
        s._UpdateListViewWithTableFile(s._EDSInTableFN)
        s.preferencesWin = CDysPreferences(s._DysConfig);
    
    def _GetTableFileName(s):
        """Get the 'translated' file name for the table. This takes care of
        the empty table and its associated Untitled file name.
        """
        if( s._EDSInTableFN==EMPTYFN ):
            return UNTITLEDFN;
        else:
            return s._EDSInTableFN;
    
    def _UpdateFromConfig(s):
        """Update the internal PdbFile instance with the current setting from
        the config.
        """
        cfg   = s._DysConfig.GetConfig();
        s._PdbFile.SetPDBDir(cfg.get("program", "pdbDir"));
        s._PdbFile.SetCootTmpDir(cfg.get("program", "tempDir"));
        s._PdbFile.SetCootExec(cfg.get("program", "cootExec"));
        
    def __BuildWindow(s):
        """Constructs main window.
        """
        # Miscellaneous stuff for setting up the framework of the main
        # application window.
        s.window = gtk.Window(gtk.WINDOW_TOPLEVEL);
        s.window.connect("destroy", s._QuitCB);
        s.window.connect("delete_event", s._QuitCB);
        s.window.set_title(s.GetMainWindowTitle());
        s.window.set_border_width(0);
        s.window.set_size_request(1000, 750);
        
        # We pack everything into a vertical box.
        s.mainBox = gtk.VBox(False, 0);
        s.window.add(s.mainBox);

        # First the menu bar.
        s._BuildMenuBar();
        s._BuildPopupMenu();
        s.mainBox.pack_start(s.menuBar, False, False, 0);
        
        # Create the data storage for the ligand table.
        # Number of columns in the stored EDS table object s._Table.
        nrTableColumns = len(s._Table.ColumnTypes);
        # the large table with all ligand data.
        s.edsTableWin = gtk.ScrolledWindow();
        s.edsTableWin.set_border_width(BORDER_WIDTH);
        s.mainBox.pack_start(s.edsTableWin, True, True, 0);
        # Column types:
        # * display data
        # * data in natural data types for sorting
        # * control data
        columnTypes = len(s._Table.ColumnTypes)*[str ,] + \
                      list(s._Table.ColumnTypes) + [int, str]
        # these are the two locations where the comments go.
        s.commentIdcs = (s._Table.COMMENTIDX,
                         nrTableColumns+s._Table.COMMENTIDX);
        s.edsLS = gtk.ListStore(*columnTypes);
        s.edsLS.clear();
        
        # Create the actual gtk GUI element that renders the table. Connect it
        # with the ListStore object.
        s.edsView = gtk.TreeView(s.edsLS);
        s.edsView.connect("button-press-event", s._TableButtonPressCB);
        # Two types of columns. One general and another that allows input for
        # the editing process.
        s.rendTxt = gtk.CellRendererText();
        s.rendTxt.set_property("family", "monospace");
        s.rendTxt.set_property("ypad", 0);
        s.rendTxt.set_property("size-points", TABLES_FONT_SIZE_PTS);
        # for the figure in the manuscript:
        if( PREPARE_FIGURE_MODE ):
            s.rendTxt.set_property("font", "monospace bold 13");
        s.editTxt = gtk.CellRendererText();
        s.editTxt.set_property("family", "monospace");
        s.editTxt.set_property("ypad", 0);
        s.editTxt.set_property("size-points", TABLES_FONT_SIZE_PTS);
        s.editTxt.set_property("editable", True);
        if( PREPARE_FIGURE_MODE ):
            s.editTxt.set_property("font", "monospace bold 13");
        s.editTxt.connect('edited', s._CommentCB)
        
        # add_with_viewport would scroll the table header.
        s.edsTableWin.add(s.edsView);
        
        # All ligands per PDB entry TreeView, a table with the color coded
        # ligand residue numbers.
        s.ligTableWin = gtk.ScrolledWindow();
        # Allow arbitrary width, but limit the height to 120px
        s.ligTableWin.set_size_request(-1, 120);
        s.ligTableWin.set_border_width(BORDER_WIDTH);
        # Do not take extra space when enlarging window.
        s.mainBox.pack_start(s.ligTableWin, False, False, 0);
        # these are the raw strings for the hetgroup names in the ListStore.
        columnTypes  =  LIG_TABLE_MAX_COL*[str ,];
        # One time for the foreground color, one time for the background.
        columnTypes += 2*LIG_TABLE_MAX_COL*[str, ];
        s.ligLS = gtk.ListStore(*columnTypes);
        s.ligLS.clear();
        s.ligView = gtk.TreeView(s.ligLS);
        # Get the selection object and disable selection on this table. We do
        # not want to have highlighted rows in the ligand display.
        s.ligView.get_selection().set_mode(gtk.SELECTION_NONE);
        s.ligView.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_BOTH);
        # This time, we do not want to display table headers.
        s.ligView.set_headers_visible(False);
        for tabLoc in range(LIG_TABLE_MAX_COL):
            # Columns are nameless, as we disabled column headers
            col = gtk.TreeViewColumn("", s.rendTxt, text=tabLoc,
                                     foreground=LIG_TABLE_MAX_COL+tabLoc,
                                     background=2*LIG_TABLE_MAX_COL+tabLoc);
            s.ligView.append_column(col);
        s.ligTableWin.add(s.ligView);

        # Status bar
        s.statusBox = gtk.HBox(False, 0);
        s.statusBox.set_border_width(BORDER_WIDTH);
        s.statusLabel = gtk.Label("Status: ");
        s.statusEntry = gtk.Entry();
        s.statusEntry.set_text("");
        s.statusEntry.set_editable(False);
        s.statusBox.pack_start(s.statusLabel, False, False);
        s.statusBox.pack_start(s.statusEntry, True, True);
        s.mainBox.pack_start(s.statusBox, False, True, 0);
        
        s.window.show_all(); # instead of single widget .show() methods

    def _GetListViewRowColor(s, idx):
        """Determine row color of an entry in the main table.
        @param idx  Index into the EDS table, s._Table.
        """
        rowColor = VALID_ROW_COL;
        row      = s._Table[idx];
        if( row[s._Table.VALIDIDX]=="A" ):
            rowColor = MANUALLY_ADDED_ROW_COL;
        return rowColor;
        
    def _UpdateListViewWithTableFile(s, filename):
        """Update the ListStore and TreeView with new data from another EDS
        table file.
        @param filename  File name of table to read. If set to the emtpy
         string, the default template for an empty table will be read.
        """
        if( filename=="" ):
            s._Table.ReadTable(EMPTYFN);
        else:
            s._Table.ReadTable(filename);
            
        nrTableColumns = len(s._Table.ColumnTypes);
        s.edsLS.clear();
        # The List Store has the following columns:
        # All columns from the EDS table object as strings             THEN
        # the same columns but converted to natural types for sorting  THEN
        # columns used for controlling the table attributes:
        #  o  Original sorting order by a rank integer column.
        for i, row in enumerate(s._Table):
            rowColor = s._GetListViewRowColor(i);
            #       string   natural types         controls
            lsRow = row + s._Table.ConvertRow(i) + [i+1, rowColor];
            s.edsLS.append(lsRow);

        # Empty the view first.
        for column in s.edsView.get_columns():
            s.edsView.remove_column(column);
        # we added hidden control columns at the end of the data store, let's
        # remember its index in the list store.
        s.rankColIdx = 2*len(s._Table.ColumnTypes);
        s.rowColorIdx= s.rankColIdx + 1;
        # We now pack the first visible column, the rank. This column is not
        # included in the EDS table file and therefore is generated here in
        # order to have a way back for the original ranking, which is too
        # complex to redo on the fly.
        s.rankCol = gtk.TreeViewColumn("", s.rendTxt, text=s.rankColIdx,
                                       background=s.rowColorIdx);
        rankLabel = gtk.Label("Rank");
        rankLabel.show();
        rankLabel.set_tooltip_text(RANK_TTEXT);
        s.rankCol.set_widget(rankLabel);
        s.rankCol.set_sort_column_id(s.rankColIdx);
        s.edsView.append_column(s.rankCol);
        # now pack all columns as they appear in the EDS input table, using
        # strings to display since float data types are displayed with too many
        # digits and waste space.
        toolTips = gtk.Tooltips();
        for tabLoc, colName in enumerate(s._Table.GetHeader()):
            cellRenderer = s.rendTxt if tabLoc!=s._Table.COMMENTIDX else \
                           s.editTxt;
            col = gtk.TreeViewColumn("", cellRenderer, text=tabLoc,
                                     background=s.rowColorIdx);
            # sorting is done through an offset column with the same data but
            # converted to its natural data type.
            if( not PREPARE_FIGURE_MODE ):
                # we want sortable columns. But in figure preparation the sort
                # order arrow space is wasted and renders the table too
                # large.
                col.set_sort_column_id(tabLoc + nrTableColumns);
            col.set_resizable(True);
            col.set_min_width(MIN_COL_WIDTH);
            # The software column is too wide. Narrow manually here.
            if(   tabLoc==s._Table.SWIDX ):
                col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED);
                col.set_fixed_width(SW_COL_WIDTH);
            elif( tabLoc==s._Table.JRNLIDX ):
                col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED);
                col.set_fixed_width(JRNL_COL_WIDTH);
                
            colLabel = gtk.Label(colName);
            colLabel.show();
            colLabel.set_tooltip_text(MAIN_TABLE_TOOLTIP_TEXTS[tabLoc]);
            col.set_widget(colLabel);
            s.edsView.append_column(col);

        # Data has been loaded, therefore, it is ok to save now, too.
        s._EnableSaveMenus()
        
        
    def _ShowPDBEDSErrorMessage(s, errorCode, pdbID=""):
        """Given an error message from the @c pdbfile module, pop up an
        informative text box.
        @param errorCode  Error code as defined in pdbfile.py
        @param pdbID  PDB entry if available. Will be printed for certain msgs.
        """
        if(   errorCode==PDB_NO_CONNECTION ):
            # Connection to the internet could not be established.
            SimpleOKDialog(CONNECTION_FAILED_TEXT);
        elif( errorCode==PDB_PDBENTRY_NOT_EXISTS ):
            # Could not fetch PDB file (this is an invalid PDB code then)
            SimpleOKDialog(INVALID_PDBENTRY_TEXT % pdbID);
        elif( errorCode==PDB_EDSENTRY_NOT_EXISTS ):
            # The PDB file was fetched, but the MAPMAN statistics list file
            # has not been retrieved from the EDS server. This means that
            # EDS has not generated an electron density for this PDB entry.
            SimpleOKDialog(NO_EDS_DATA_AVAILABLE_TEXT % pdbID);
        elif( errorCode==PDB_NO_LIGANDS_FOUND ):
            # Everything fine, but there was no ligand in the PDB file.
            SimpleOKDialog(NO_LIGAND_DATA_AVAILABLE_TEXT % pdbID);
        elif( errorCode==PDB_EDS_INVALID_STATLIS ):
            sys.stderr.write("Tried to fetch EDS data for another PDB file (internal error)");
    
    
    def _PopulateLigandTable(s, tableIdx):
        """Fill the second table with ligand data and color it.
        @param tableIdx  Index into the Gtk liststore object. Not to be
         confused with the EDS table object.
        @return @c True, ok. @c False, something went wrong. Error message has
         been displayed. 
        """
        # The rank column is our save way back to the s._Table indices.
        rowIdx    = s.edsLS[tableIdx][s.rankColIdx] - 1;
        # this could also be retrieved directly from the eds ListStore...
        pdbID     = s._Table.GetPDBID(rowIdx);
        ligRnr    = s._Table.GetResNr(rowIdx);
        # but for this here, we need to row index in s._Table anyway!
        validRnrs = s._Table.GetAllDisplayLigands(rowIdx);
        focRnrs   = s._Table.GetAllRSCCFocusLigands(rowIdx);
        s._Table.ToggleFocus();
        altFocRnrs= s._Table.GetAllRSCCFocusLigands(rowIdx);
        s._Table.ToggleFocus();
        # Get the latest content of the config and apply it to PdbFile.
        s._UpdateFromConfig();
        s.SetStatusText(STAT_FETCHING_PDB_ENTRY);
        status = s._PdbFile.FetchPDB(pdbID);
        s.DelayedStatusClear();
        if( status!=PDB_OK ):
            s._ShowPDBEDSErrorMessage(status, pdbID=pdbID);
            return False;
        s.populatedPDBID = pdbID;
        s.ligLS.clear();
        if( s._Table.IsFocusOnBad() ):
            focCol, altFocCol = LOW_RSCC_COL, HIGH_RSCC_COL;
        else:
            focCol, altFocCol = HIGH_RSCC_COL, LOW_RSCC_COL;
        row, fgCols, bgCols = [], [], [];
        for rnr, ligand in s._PdbFile.GetAllHetGroups():
            resNm = ligand.Get3LetterName();
            # do not show HOH et al.
            if( resNm in NONLIGANDS ):
                continue ;
            fgCol = bgCol = DEFAULT_COL;
            # Coloring has become quite complex. First, if the entry has been
            # imported manually, the rules are different. Color the good and
            # the bads, do not use bg colors, as everything is displayed
            # anyway. 
            # If the entry is present in the table and has not been imported,
            # it makes sense to color by presence in the table, which has the
            # meaning that the present ligands are either good or bad.
            # In either case, highlight the selected ligand.
            if( s._Table.IsManuallyImported(rowIdx) ):
                if( rnr in focRnrs ):
                    fgCol, bgCol = focCol, DEFAULT_COL;
                if( rnr in altFocRnrs ):
                    fgCol, bgCol = altFocCol, DEFAULT_COL;
            else:
                # A ligands can be both in focus AND be valid (ie. G/Y).
                # Therefore, assign first the color for focus and later it may
                # be overridden by a valid ligand.
                if( rnr in focRnrs ):
                    fgCol, bgCol = focCol, DEFAULT_COL;
                if( rnr in validRnrs ):
                    fgCol, bgCol = focCol, IN_FOCUS_BG_COL;
                if( rnr in altFocRnrs ):
                    fgCol, bgCol = altFocCol, DEFAULT_COL;
            if( rnr==ligRnr ):
                fgCol, bgCol = HIGHLIGHT_FG_COL, HIGHLIGHT_BG_COL;
            row.append("%s %s" % (resNm, rnr));
            fgCols.append(fgCol);
            bgCols.append(bgCol);
            if( len(row)==LIG_TABLE_MAX_COL ):
                s.ligLS.append(row+fgCols+bgCols);
                row, fgCols, bgCols = [], [], [];
        if( row!=[] ):
            filler = (LIG_TABLE_MAX_COL-len(row))*[DEFAULT_COL,];
            row    = row + filler;
            fgCols = fgCols + filler;
            bgCols = bgCols + filler;
            s.ligLS.append(row+fgCols+bgCols);
        return True;
        
    def _CommentCB(s, cell, path, newText):
        """Callback function for editing the Comment column.
        'cell' argument is ignored!"""
        # we update both the original column used for display and the mirrored
        # column with the natural data type, which happens to be the same
        # (str) for the Comment column.
        # EP: newText is prepended with user stamp
        for colIdx in s.commentIdcs:
            ustamp = s._DysConfig.GetConfig().get("program", "ustamp").strip();
            if( ustamp!="" and newText.strip().find(ustamp)!=0 ):
                newText = ustamp + " " + newText;
            s.edsLS[path][colIdx] = newText;
        s.SetModifiedContent();

    def _SaveFile(s, outFFN):
        """Saves the currently displayed data into a file.
        The file name has been determined at startup. This method retrieves
        the comments, transfers them back to the EDSTable object and writes
        out the modified content.
        
        @param outFFN  Full path output file name.
        @return @c True, everything fine. @c False, some error during
         writing. An info dialog has already been displayed.
        """
        for row in s.edsLS:
            # the index in the originally read table is given by the rank
            # column, which is displayed as the first column, but was appended
            # to the table in the ListStore.
            origIdx = row[s.rankColIdx] - 1;
            # this text comes from the edits.
            comment = row[s._Table.COMMENTIDX];
            s._Table[origIdx][s._Table.COMMENTIDX] = comment;
        ok = s._Table.WriteTSV(outFFN, verbose=True);
        if( not ok ):
            OkInfoDialog(COULD_NOT_WRITE_FILE_TEXT % outFFN);
        return ok;

    def _NewCB(s, widget, data=None):
        """Callback for creating an empty table."""
        s._LoadCB(widget, data=data, empty=True);
    
    def _LoadCB(s, widget, data=None, empty=False):
        "Callback for Load table dialog."
        if( s.tableModified ):
            yesPressed = YesNoInfoDialog(TABLE_MODIFIED_CONTINUE_TEXT);
            if( not yesPressed ):
                # ok, user is afraid, let's not load anything this time.
                return ;
        
        if( not empty ):
            inFFN = HandleFileChooser(LOAD_TITLE, gtk.FILE_CHOOSER_ACTION_OPEN);
        else:
            # Empty table template file name.
            inFFN = EMPTYFN;
        if( inFFN!=None ):
            # First, let's check if this stuff we are being fed is a valid EDS
            # table...
            if s._Table.IsValidTableFile(inFFN):
                s.SetStatusText(STAT_LOADING_FILE % inFFN);
                s._UpdateListViewWithTableFile(inFFN);
                s._EDSInTableFN = inFFN;
                s.DelayedStatusClear();
                s.ClearModifiedContent();
                s._EnableFetchEDSnPDB();
            else:
                # no it isn't valid, complain and leave it.
                OkInfoDialog(INVALID_FILE_FORMAT_TEXT);
        
    def _LoadSfCB(s, widget, data=None):
        """Callback for the Fetch EDS and PDB dialog."""
        importDlg = CImportEDSnPDB();
        retVal    = importDlg.run();
        pdbID     = importDlg.GetPDBCode();
        importDlg.destroy();
        if( retVal!=gtk.RESPONSE_OK ):
            # Cancel or window closed. Do not proceed.
            return ;
        # Get the latest content of the config and apply it to PdbFile.
        s._UpdateFromConfig();
        s.SetBusyCursor()
        s.SetStatusText(STAT_FETCHING_PDB_ENTRY);
        status = s._PdbFile.FetchPDBnStatLis(pdbID);
        s.DelayedStatusClear();
        s.SetNormalCursor();
        if( status!=PDB_OK ):
            s._ShowPDBEDSErrorMessage(status, pdbID=pdbID);
            return ;
        
        # these are the ligands we need to add.
        ligandsToAdd = s._PdbFile.GetAllHetGroups();
        nrToAdd      = len(ligandsToAdd);
        # Compute the combined score and sort by it. This reflects the
        # behavior as expected from the group sorting.
        for rnr, ligand in ligandsToAdd:
            score = CalcCombinedScore(ligand.GetRSCC(),
                                      s._PdbFile.GetResolution());
            ligand.SetCombinedScore(score);
        # Sort descending by combined score.
        ligandsToAdd.sort(lambda x,y: -(x[1].GetCombinedScore()<y[1].GetCombinedScore()));
        # Since the list store reflects the sort order of the tree view, we
        # need to compute the new ranks. Fortunately, we are adding new
        # entries to the begin of the list, so we know that the current ranks
        # need to be incremented by the number of entries added.
        #
        # Word of warning: Do not enumerate(s.edsLS) and manipulate rows at
        # the same time. For example, this code goes into an infinite loop
        # when enumerating and altering the rank column if the table is sorted
        # by this column!
        #
        # It's getting worse than that: If sorting by Rank is on, and sorting
        # is ascending, it will not work with directly indexing into the list
        # store, either. It seems the list store is immediately resorting its 
        # content once an element has been modified! If sorting is on the Rank
        # column, modification of the Rank cells has to be executed such that
        # the sorting will not be overthrown.
        # Example
        # rank = [10,11,12,13]
        # Adding the integer to the top entry in the list (10) will yield
        # rank = [11,12,12,13] after '10' has been added by '2', since the list
        # store immediately restores sort order.
        # Adding from the bottom will not destroy the order:
        # (add 2 to position 3 where integer 13 is): [10,11,12,15]
        # (add 2 to position 2 where integer 12 is): [10,11,14,15]
        # (add 2 to position 1 where integer 11 is): [10,13,14,15]
        # (add 2 to position 0 where integer 10 is): [12,13,14,15]
        #
        # It's easy to see that the same argument holds if the list is sorted
        # descending when accessing the elements from top to bottom.
        #
        # Solution: Always increment from the point of the list where there is
        # the largest element. If this is increased, it will not change
        # position. (If the list store is not sorted by Rank, no position
        # change will happen at all, since we are not operating on the sort
        # column.)
        #
        # We do not care if the list store is currently sorted by the Rank
        # column, we just have a look in which direction the sort order could
        # be and then choose the right direction for accessing the elements of
        # the list store.
        if( len(s.edsLS)>0 ):
            order = range(len(s.edsLS));
            if( s.edsLS[0][s.rankColIdx]<s.edsLS[1][s.rankColIdx] ):
                # If sorted by Rank, it is an ascending order. The highest
                # Rank is at the bottom of the table, that's where we want to
                # start then. We traverse the elements for updating from the
                # bottom to the top.
                order.reverse();
            for i in order:
                s.edsLS[i][s.rankColIdx] = s.edsLS[i][s.rankColIdx] + nrToAdd;
        # Add each new ligand as a single new row and also set the rank.
        index = nrToAdd;
        for rnr, ligand in ligandsToAdd:
            # Add it to the EDS table object.
            s._Table.AddLigand(pdbID, s._PdbFile, ligand);
            # Determine the color of the freshly added row.
            rowColor = rowColor = s._GetListViewRowColor(0);
            # And once there, simply take it and add it to the list store.
            lsRow  = s._Table[0] + s._Table.ConvertRow(0) + [index, rowColor];
            s.edsLS.prepend(lsRow);
            index -= 1;
        # New data has been added, time to think about saving that stuff.
        s.SetModifiedContent();
        # This is a bit inconsistent: Jump to the top of the table,
        # independent of the sorting order. If the sorting order is by
        # combined score/rank, this will result in the newly loaded
        # structure.
        s.edsView.scroll_to_cell(0);
        
        
    def _PrefCB(s, widget):
        "Callback for Preferences dialog."
        s.preferencesWin.show_all();
    
    def _SaveCB(s, widget, data=None):
        """Callback for saving the table contents."""
        saveFFN = s._GetTableFileName();
        s.SetStatusText(STAT_SAVING_FILE % saveFFN);
        ok = s._SaveFile(saveFFN);
        if( ok ):
            s.DelayedStatusClear();
            s.ClearModifiedContent();

    def _SaveAsCB(s, widget, data=None):
        "Callback function for Save as..."
        curFFN = s._GetTableFileName();
        edsInFN, edsOutFN = GetVersionedInOutFileNames(curFFN);
        outFFN = HandleFileChooser(SAVE_AS_TITLE, gtk.FILE_CHOOSER_ACTION_SAVE,
                                   suggestedFN=edsOutFN);
        if( outFFN!=None ):
            s.SetStatusText(STAT_SAVING_FILE % outFFN);
            s._SaveFile(outFFN);
            # This is a trick: The new input file is the chosen save file. By
            # this, save will default to this new file. Also, the window title
            # adjusts.
            s._EDSInTableFN = outFFN;
            s.DelayedStatusClear();
            s.ClearModifiedContent();

    def _AboutCB(s, widget):
        """Callback function for About..."""
        SimpleOKDialog(LICENSE_TEXT);
        
    def _QuitCB(s, widget, data=None):
        """Callback for application shutdown. Check if table has been
        saved.
        """
        yesPressed = True;
        if( s.tableModified ):
            yesPressed = YesNoInfoDialog(TABLE_MODIFIED_CONTINUE_TEXT);
        if( yesPressed ):
            gtk.main_quit();
        # We need to return True here since:
        # a) if we did not quit the application, the framework needs to know
        #    that we have taken care of the delete event.
        # b) in case we quit the app, this dialog should not show up again.
        return True;
    
    def _TableMenuCB(s, token):
        """Callback for selected menu item in the table."""
        if( token in (MNU_SEND_TO_PDB, MNU_SEND_TO_EDS, MNU_SEND_TO_PMED,
                      MNU_SEND_TO_PDBREDO) ):

            pdbID = s.edsLS[s.__tmpPath][s._Table.PDBIDIDX];
            pmID  = s.edsLS[s.__tmpPath][s._Table.PMIDIDX];
            mid   = pdbID[1:3];
            if(   token==MNU_SEND_TO_PDB ):
                url = URL_PDB % pdbID;
            elif( token==MNU_SEND_TO_EDS ):
                url = URL_EDS % pdbID;
            elif( token==MNU_SEND_TO_PMED ):
                url = URL_PUBMED % pmID;
            elif( token==MNU_SEND_TO_PDBREDO ):
                url = URL_PDBREDO % (mid,pdbID);
            webbrowser.open(url);
        else:
            txt = s._DysConfig.GetConfig().get("annotation", token);
            txt = txt.split("-")[1];
            txt = txt.strip();
            s._CommentCB(None, s.__tmpPath, txt);
        
    def _BuildMenuBar(s):
        """Build and pack the (small) menu bar."""
        aclGrp = gtk.AccelGroup();
        s.window.add_accel_group(aclGrp);
        
        s.menuBar      = gtk.MenuBar();
        s.fileMBMenu   = gtk.Menu();
        s.fileMBItem     = gtk.MenuItem("_File");
        s.menuBar.append(s.fileMBItem);
        s.fileMBItem.set_submenu(s.fileMBMenu);
        s.fileMnNewItem  = gtk.ImageMenuItem(gtk.STOCK_NEW);
        s.fileMnLoadItem = gtk.ImageMenuItem(gtk.STOCK_OPEN);
        s.fileMnLdSfItem = gtk.ImageMenuItem(gtk.STOCK_OPEN);
        s.fileMnSaveItem = gtk.ImageMenuItem(gtk.STOCK_SAVE);
        s.fileMnSvAsItem = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS);
        s.fileMnPrefItem = gtk.ImageMenuItem(gtk.STOCK_PREFERENCES);
        s.fileMnAbotItem = gtk.ImageMenuItem(gtk.STOCK_ABOUT);
        s.fileMnQuitItem = gtk.ImageMenuItem(gtk.STOCK_QUIT);
        s.fileMnNewItem.connect("activate", s._NewCB);
        s.fileMnLoadItem.connect("activate", s._LoadCB);
        s.fileMnLdSfItem.connect("activate", s._LoadSfCB);
        s.fileMnSaveItem.connect("activate", s._SaveCB);
        s.fileMnSvAsItem.connect("activate", s._SaveAsCB);
        s.fileMnPrefItem.connect("activate", s._PrefCB);
        s.fileMnAbotItem.connect("activate", s._AboutCB);
        s.fileMnQuitItem.connect("activate", s._QuitCB);
        s.fileMnLdSfItem.set_label(FILE_MNU_FETCH_EDS_TEXT);
        key, mod = gtk.accelerator_parse("<Control>N");
        s.fileMnNewItem.add_accelerator("activate", aclGrp, key, 
                                         mod, gtk.ACCEL_VISIBLE);
        key, mod = gtk.accelerator_parse("<Control>O");
        s.fileMnLoadItem.add_accelerator("activate", aclGrp, key, 
                                         mod, gtk.ACCEL_VISIBLE);
        key, mod = gtk.accelerator_parse("<Shift><Control>O");
        s.fileMnLdSfItem.add_accelerator("activate", aclGrp, key, 
                                         mod, gtk.ACCEL_VISIBLE);
        key, mod = gtk.accelerator_parse("<Control>S");
        s.fileMnSaveItem.add_accelerator("activate", aclGrp, key, 
                                         mod, gtk.ACCEL_VISIBLE);
        key, mod = gtk.accelerator_parse("<Shift><Control>S");
        s.fileMnSvAsItem.add_accelerator("activate", aclGrp, key, 
                                         mod, gtk.ACCEL_VISIBLE);
        key, mod = gtk.accelerator_parse("<Control>P");
        s.fileMnPrefItem.add_accelerator("activate", aclGrp, key, 
                                         mod, gtk.ACCEL_VISIBLE);

        key, mod = gtk.accelerator_parse("<Control>Q");
        s.fileMnQuitItem.add_accelerator("activate", aclGrp, key, 
                                         mod, gtk.ACCEL_VISIBLE);
        for menuItem in (s.fileMnNewItem, s.fileMnLoadItem,
                         s.fileMnLdSfItem, s.fileMnSaveItem,
                         s.fileMnSvAsItem, s.fileMnPrefItem,
                         gtk.SeparatorMenuItem(), s.fileMnAbotItem,
                         gtk.SeparatorMenuItem(), s.fileMnQuitItem):
            s.fileMBMenu.append(menuItem);
        
    def _DisableFetchEDSnPDB(s):
        """Grey out the Fetch EDS and PDB menu.
        """
        s.fileMnLdSfItem.set_sensitive(False);
        
    def _DisableSaveMenus(s):
        "Grey out Save menus."
        s.fileMnSaveItem.set_sensitive(False);
        s.fileMnSvAsItem.set_sensitive(False);

    def  _EnableSaveMenus(s):
        "Allow Save menus be selected."
        s.fileMnSaveItem.set_sensitive(True);
        s.fileMnSvAsItem.set_sensitive(True);
    
    def _EnableFetchEDSnPDB(s):
        """Allow choosing of Fetch EDS and PDB menu.
        """
        s.fileMnLdSfItem.set_sensitive(True);
        
    def _BuildPopupMenu(s):
        """Build the popup menu structure for the data table."""
        s.tableMenu          = gtk.Menu();
        s.tableMnPDBItem     = gtk.MenuItem("PDB web site");
        s.tableMnEDSItem     = gtk.MenuItem("EDS web site");
        s.tableMnPDBREDOItem = gtk.MenuItem("PDB_REDO web site");
        s.tableMnPMIDItem    = gtk.MenuItem("Pubmed");
        for menuItem, token in ((s.tableMnPMIDItem, MNU_SEND_TO_PMED),
                                (s.tableMnPDBItem, MNU_SEND_TO_PDB),
                                (s.tableMnEDSItem, MNU_SEND_TO_EDS),
                                (s.tableMnPDBREDOItem, MNU_SEND_TO_PDBREDO) ):
            # we need to call show(), otherwise nothing appears when popup()
            # is called.
            menuItem.show();
            menuItem.connect_object("activate", s._TableMenuCB, token);
            s.tableMenu.append(menuItem);
        # sub menu for predefined annotation texts.
        s.tableAsgnSubMenu = gtk.Menu();
        for token, menuDesc in s._DysConfig.GetConfig().items("annotation"):
            menuText = menuDesc.split("-")[0];
            menuText.strip();
            menuItem = gtk.MenuItem(menuText, token);
            menuItem.connect_object("activate", s._TableMenuCB, token);
            menuItem.show();
            s.tableAsgnSubMenu.append(menuItem);
        s.tableAssignItem = gtk.MenuItem("Assign");
        s.tableAssignItem.set_submenu(s.tableAsgnSubMenu);
        s.tableMenu.append(s.tableAssignItem);
        s.tableAssignItem.show()

    def _TableButtonPressCB(s, treeView, event):
        """General callback for all mouse click events happening on the table
        view."""
        # Where did we get this mouse click?
        pthinfo = treeView.get_path_at_pos(int(event.x), int(event.y))
        if( pthinfo is not None ):
            # ok, we really got it somewhere inside the table view.
            path, colObj, cellX, cellY = pthinfo
            pdbID = s.edsLS[path][s._Table.PDBIDIDX];
            cfg   = s._DysConfig.GetConfig();
            # Get the latest content of the config and apply it to PdbFile.
            s._UpdateFromConfig();
            if(   event.button==1 and event.type==gtk.gdk._2BUTTON_PRESS ):
                # this is a left mouse double click, fire up coot.
                resNr = s.edsLS[path][s._Table.RESNRIDX];
                s.SetBusyCursor();
                s.SetStatusText(STAT_FETCHING_PDB_ENTRY);
                # All parameters might have changed since the last double
                # click. Therefore, retrive them from the config and set them
                # in the PDBFile instance.
                status = s._PdbFile.FetchPDB(pdbID);
                if( status!=PDB_OK ):
                    s.SetNormalCursor();
                    s._ShowPDBEDSErrorMessage(status, pdbID=pdbID);
                    return ;
                ok = s._PopulateLigandTable(path[0]);
                if( not ok ):
                    s.SetNormalCursor();
                    return ;
                s.SetStatusText(STAT_FETCHING_EDS_DATA);
                status = s._PdbFile.FetchMTZ(pdbID);
                s.SetNormalCursor();
                s.SetStatusText(STAT_LAUNCHING_COOT);
                s.DelayedStatusClear();
                if( status!=PDB_OK ):
                    s._ShowPDBEDSErrorMessage(status, pdbID=pdbID);
                    return ;
                # this is the trick to get back to the EDS table index when
                # having a different sorting than the default. We use the
                # hidden column which is used by Gtk to define the sorting of
                # the Rank column. Rank numbering starts with 1.
                edsTabIdx = s.edsLS[path][s.rankColIdx] - 1;
                if( s._Table.IsManuallyImported(edsTabIdx) ):
                    displayRnrs = s._Table.GetAllDisplayLigands(edsTabIdx);
                else:
                    displayRnrs = s._Table.GetAllDisplayRSCCFocusLigands(edsTabIdx,
                                                                         False);
                s._PdbFile.SetCootScriptingHost(cfg.get("program",
                                                        "scripting-host"));
                s._PdbFile.WriteCootScript(pdbID, resNr,
                                       cfg.getint("program", "coot-width"),
                                       cfg.getint("program", "coot-height"),
                                       cfg.getfloat("program", "2FoFc"),
                                       cfg.getfloat("program", "FoFc"),
                                       displayRnrs);
                script = s._PdbFile.WriteShellScript(pdbID);
                proc = subprocess.Popen([script,], shell=True, );
            elif( event.button==1 and event.type==gtk.gdk.BUTTON_PRESS ):
                # if we move to a different row, see if the PDB id is still
                # the one we are showing the ligands for. If not, clear the
                # ligand view.
                if( s.populatedPDBID!=pdbID ):
                    s.ligLS.clear();
                    s.populatedPDBID = "";
            elif( event.button==2 and event.type==gtk.gdk.BUTTON_PRESS ):
                # middle mouse button single press, populate the het group
                # data table.
                s.edsView.grab_focus();
                s.edsView.set_cursor(path, colObj, 0);
                # path is [rowNr,]
                s.SetBusyCursor();
                s._PopulateLigandTable(path[0]);
                s.SetNormalCursor();
            elif( event.button==3 and event.type==gtk.gdk.BUTTON_PRESS ):
                # right mouse button single click, build menu.
                activated = s.edsLS[path][s._Table.PMIDIDX]!="";
                s.tableMnPMIDItem.set_sensitive(activated);
                # the table menu callback method will not be furnished with
                # table associated data.
                s.__tmpPath = path;
                # Show the context sensitive menu.
                s.tableMenu.popup(None, None, None, event.button, event.time);

    def GetMainWindowTitle(s):
        "Compose the string for the main window's title."
        ffn = s._GetTableFileName();
        return MAIN_WINDOW_TITLE + ": " + os.path.basename(ffn);
        
    def SetStatusText(s, text):
        """Set a status message in the status bar at the end of the window."""
        s.statusEntry.set_text(text);
        while gtk.events_pending():
            gtk.main_iteration_do(False)

    def DelayedStatusClear(s):
        """Wait a while, then clear the Status line."""
        gobject.timeout_add(CLR_TIMEOUT, lambda self=s:self.SetStatusText(""));
        
    def SetModifiedContent(s):
        """Signify with an asterisk that the table has changed and not yet
        been saved."""
        s.tableModified = True;
        s.window.set_title(s.GetMainWindowTitle() + "*");

    def ClearModifiedContent(s):
        """Signify that the table has not been changed since the last save."""
        s.tableModified = False;
        s.window.set_title(s.GetMainWindowTitle());

    def SetBusyCursor(s):
        """Show an hourglass cursor to indicate work in progress."""
        watch = gtk.gdk.Cursor(gtk.gdk.WATCH);
        s.window.window.set_cursor(watch);

    def SetNormalCursor(s):
        """Set back to cursor to normal."""
        s.window.window.set_cursor(None);


class CDysPreferences(gtk.Window):
    """Handle setting of preferences.

    This window is implemented such that its content can be queried from
    outside. However, as long as the window is mapped, it will report the
    values before its invocation. Only if this window has been closed by
    pressing the OK button, values will be transfered and will be visible for
    the outside caller.
    """
    def __init__(s, dysConfig):
        gtk.Window.__init__(s);
        s.dysConfig = dysConfig;
        s.set_border_width(0);
        s.set_title(PREF_WINDOW_TITLE);
        s.connect("destroy", s._QuitCB);
        s.connect("delete_event", s._QuitCB);
        s.__BuildWindow();

    def __BuildWindow(s):
        # We pack everything into a vertical box.
        s.mainBox = gtk.VBox(homogeneous=False, spacing=5);
        s.add(s.mainBox);
        
        cfg = s.dysConfig.GetConfig();

        # Table row and columns are adjusted to their max element.
        s.Tab1 = gtk.Table(9, 4, homogeneous=False);
        s.sigma2FoFcLabel = gtk.Label("2FoFc contour (sigma): ");
        adj = gtk.Adjustment(cfg.getfloat("program", "2FoFc"),
                             0.00, 50.00, 0.1, -0.1);
        s.sigma2FoFcSpin = gtk.SpinButton(adj, digits=2);
        s.sigma2FoFcSpin.set_numeric(True);
        s.sigmaFoFcLabel = gtk.Label("FoFc contour (sigma): ");
        adj = gtk.Adjustment(cfg.getfloat("program", "FoFc"),
                             0.00, 50.00, 0.1, -0.1);
        s.sigmaFoFcSpin = gtk.SpinButton(adj, digits=2);
        s.sigmaFoFcSpin.set_numeric(True);
        
        for i, widget in enumerate( (s.sigma2FoFcLabel, s.sigma2FoFcSpin,
                                     s.sigmaFoFcLabel,  s.sigmaFoFcSpin) ):
            s.Tab1.attach(widget, i, i+1, 0, 1, xpadding=5, ypadding=5);

        s.cootWinWidthLabel = gtk.Label("Coot window width: ");
        adj = gtk.Adjustment(cfg.getint("program", "coot-width"),
                             100, 2000.00, 10, -10);
        s.cootWinWidthSpin = gtk.SpinButton(adj, digits=0);
        s.cootWinWidthSpin.set_numeric(True);
        s.cootWinHeightLabel = gtk.Label("Coot window height: ");
        adj = gtk.Adjustment(cfg.getint("program", "coot-height"),
                             100, 2000.00, 10, -10);
        s.cootWinHeightSpin = gtk.SpinButton(adj, digits=0);
        s.cootWinHeightSpin.set_numeric(True);
        
        for i, widget in enumerate( (s.cootWinWidthLabel,s.cootWinWidthSpin,
                                     s.cootWinHeightLabel,s.cootWinHeightSpin)):
            s.Tab1.attach(widget, i, i+1, 1, 2, xpadding=5, ypadding=5);

        s.cootLocLabel = gtk.Label("Coot binary location: ");
        s.cootLocEntry = gtk.Entry();
        s.cootLocEntry.set_max_length(0);
        s.cootLocEntry.set_editable(True);
        s.cootLocEntry.set_property("xalign", 0.0);
        s.cootLocEntry.set_text(cfg.get("program", "cootExec"));
        s.cootLocButton = gtk.Button(stock=gtk.STOCK_OPEN);
        s.cootLocButton.connect("clicked", s._CootLocCB);
        s.Tab1.attach(s.cootLocLabel, 0, 1, 2, 3, xpadding=5, ypadding=5);
        s.Tab1.attach(s.cootLocEntry, 1, 3, 2, 3, xpadding=5, ypadding=5);
        s.Tab1.attach(s.cootLocButton, 3, 4, 2, 3, xpadding=5, ypadding=5);

        s.pdbDirLabel = gtk.Label("PDB mirror directory: ");
        s.pdbDirEntry = gtk.Entry();
        s.pdbDirEntry.set_max_length(0);
        s.pdbDirEntry.set_editable(False);
        s.pdbDirEntry.set_property("xalign", 0.0);
        s.pdbDirEntry.set_text(cfg.get("program", "pdbDir"));
        s.pdbDirButton = gtk.Button(stock=gtk.STOCK_OPEN);
        s.pdbDirButton.connect("clicked", s._PDBDirLocCB);
        s.Tab1.attach(s.pdbDirLabel, 0, 1, 3, 4, xpadding=5, ypadding=5);
        s.Tab1.attach(s.pdbDirEntry, 1, 3, 3, 4, xpadding=5, ypadding=5);
        s.Tab1.attach(s.pdbDirButton, 3, 4, 3, 4, xpadding=5, ypadding=5);
        
        s.tempDirLabel = gtk.Label("Temporary files directory: ");
        s.tempDirEntry = gtk.Entry();
        s.tempDirEntry.set_max_length(0);
        s.tempDirEntry.set_editable(False);
        s.tempDirEntry.set_property("xalign", 0.0);
        s.tempDirEntry.set_text(cfg.get("program", "tempDir"));
        s.tempDirButton = gtk.Button(stock=gtk.STOCK_OPEN);
        s.tempDirButton.connect("clicked", s._TempDirLocCB);
        s.Tab1.attach(s.tempDirLabel, 0, 1, 4, 5, xpadding=5, ypadding=5);
        s.Tab1.attach(s.tempDirEntry, 1, 3, 4, 5, xpadding=5, ypadding=5);
        s.Tab1.attach(s.tempDirButton, 3, 4, 4, 5, xpadding=5, ypadding=5);

        s.ustampLabel = gtk.Label("User ID (abbreviation): ");
        s.ustampEntry = gtk.Entry();
        s.ustampEntry.set_max_length(0);
        s.ustampEntry.set_editable(True);
        s.ustampEntry.set_text(cfg.get("program", "ustamp"));
        s.Tab1.attach(s.ustampLabel, 0, 1, 5, 6, xpadding=5, ypadding=5);
        s.Tab1.attach(s.ustampEntry, 1, 2, 5, 6, xpadding=5, ypadding=5);
        
        s.cootScrHost = gtk.Label("Coot scripting language: ");
        s.cootScrPyBut= gtk.RadioButton(group=None, label="Python",
                                        use_underline=False);
        # these two buttons define a group.
        s.cootScrScmBut= gtk.RadioButton(group=s.cootScrPyBut, label="Scheme",
                                        use_underline=False);
        # line the two radiobuttons in a column, a little bit of waste of
        # space, but it looks better.
        s.Tab1.attach(s.cootScrHost, 0, 1, 6, 7, xpadding=5, ypadding=5);
        s.Tab1.attach(s.cootScrPyBut, 1, 2, 6, 7, xpadding=5, ypadding=5);
        s.Tab1.attach(s.cootScrScmBut, 1, 2, 7, 8, xpadding=5, ypadding=0);
        # choose the right button to be active.
        active = s.cootScrPyBut if cfg.get("program", "scripting-host")=="python"\
                 else s.cootScrScmBut;
        active.set_active(True);
        
        s.cfgLocLabel = gtk.Label("Configuration file: ");
        s.cfgFNLabel = gtk.Label(s.dysConfig.GetCurrentConfigFullFileName());
        # User should be able to copy and paste this path...
        s.cfgFNLabel.set_selectable(True);
        s.Tab1.attach(s.cfgLocLabel, 0, 1, 8, 9, xpadding=5, ypadding=5);
        s.Tab1.attach(s.cfgFNLabel, 1, 4, 8, 9, xpadding=5, ypadding=5);
        
        s.__MakeToolTips();
        
        # make all labels in this window left aligned. Default was centered.
        for label in (s.sigma2FoFcLabel, s.sigmaFoFcLabel, s.cootWinWidthLabel,
                      s.cootWinHeightLabel, s.cootLocLabel, s.pdbDirLabel,
                      s.tempDirLabel, s.ustampLabel, s.cfgLocLabel,
                      s.cootScrHost, s.cfgFNLabel):
            label.set_alignment(xalign=0.0, yalign=0.5);
        s.mainBox.pack_start(s.Tab1, False, False, padding=5);
        
        s.commitBox = gtk.HBox(False, 0);
        s.okButton = gtk.Button(stock=gtk.STOCK_OK);
        s.cancelButton = gtk.Button(stock=gtk.STOCK_CANCEL);
        s.okButton.connect("clicked", s._CommitCB, True);
        s.cancelButton.connect("clicked", s._CommitCB, False);
        s.commitBox.pack_start(s.cancelButton, True, False);
        s.commitBox.pack_start(s.okButton, True, False);
        s.mainBox.pack_start(s.commitBox, padding=5);

    def __MakeToolTips(s):
        "Make the little mouse over texts."
        toolTips = (
            (s.sigma2FoFcSpin, CONTOUR_2FOFC_TTEXT),
            (s.sigmaFoFcSpin, CONTOUR_FOFC_TTEXT),
            (s.cootWinWidthSpin, COOT_WIN_WIDTH_TTEXT),
            (s.cootWinHeightSpin, COOT_WIN_HEIGHT_TTEXT),
            (s.cootLocEntry, COOT_BINARY_TTEXT),
            (s.pdbDirEntry, PDB_MIRROR_DIR_TTEXT),
            (s.tempDirEntry, TEMP_FILES_DIR_TTEXT),
            (s.ustampEntry, USER_ID_TTEXT),
            (s.cootScrScmBut, SCRIPT_SCHEME_TTEXT),
            (s.cootScrPyBut, SCRIPT_PYTHON_TTEXT),
            (s.cfgFNLabel, CONFIG_FILENAME_TTEXT));
        for widget, text in toolTips:
            widget.set_tooltip_text(text);
    
    def _QuitCB(s, widget, data=None):
        """Callback for window delete/destroy events. Treat like Cancel."""
        s._CommitCB(widget, data=False);
        
    def _CommitCB(s, widget, data=False):
        """Callback for cancel/ok button pressed & window destroy.
        """
        cfg = s.dysConfig.GetConfig();
        if( data ):
            # we pressed the OK button, need to store strings in the config.
            cfg.set("program", "2FoFc", str(s.sigma2FoFcSpin.get_value()));
            cfg.set("program", "FoFc", str(s.sigmaFoFcSpin.get_value()));
            cfg.set("program", "coot-width",
                    str(int(s.cootWinWidthSpin.get_value())));
            cfg.set("program", "coot-height",
                    str(int(s.cootWinHeightSpin.get_value())));
            cfg.set("program", "cootExec", s.cootLocEntry.get_text());
            cfg.set("program", "pdbDir", s.pdbDirEntry.get_text());
            cfg.set("program", "tempDir", s.tempDirEntry.get_text());
            cfg.set("program", "ustamp", s.ustampEntry.get_text());
            scriptLang = "python" if s.cootScrPyBut.get_active() else "scheme";
            cfg.set("program", "scripting-host", scriptLang);
            s.dysConfig.WriteConfiguration();
        else:
            # Cancel button pressed. Restore to the previous default values.
            s.sigma2FoFcSpin.set_value(cfg.getfloat("program", "2FoFc"))
            s.sigmaFoFcSpin.set_value(cfg.getfloat("program", "FoFc"))
            s.cootWinWidthSpin.set_value(cfg.getint("program", "coot-width"));
            s.cootWinHeightSpin.set_value(cfg.getint("program", "coot-height"));
            s.cootLocEntry.set_text(cfg.get("program", "cootExec"));
            s.pdbDirEntry.set_text(cfg.get("program", "pdbDir"));
            s.tempDirEntry.set_text(cfg.get("program", "tempDir"));
            s.ustampEntry.set_text(cfg.get("program", "ustamp"));
            scriptLang = cfg.get("program", "scripting-host");
            active = s.cootScrPyBut if scriptLang=="python" else\
                     s.cootScrScmBut;
            active.set_active(True);
        s.hide();
        
    def _CootLocCB(s, widget, data=None):
        "Callback function for coot executable file location."
        cfg = s.dysConfig.GetConfig();
        fn = HandleFileChooser(COOT_FILE_SEL_WINDOW_TITLE,
                               gtk.FILE_CHOOSER_ACTION_OPEN,
                               suggestedFN=cfg.get("program", "cootExec"));
        if( fn!=None ):
            if(   not os.path.exists(fn) ):
                OkInfoDialog(FILE_NOT_EXIST_TEXT);
                return ;
            elif( not os.access(fn, os.X_OK) ):
                OkInfoDialog(FILE_NOT_EXECTUTABLE_TEXT);
                return ;
            # integrity check passed.
            s.cootLocEntry.set_text(fn);

    def _PDBDirLocCB(s, widget, data=None):
        cfg = s.dysConfig.GetConfig();
        fn = HandleFileChooser(PDB_DIR_SEL_WINDOW_TITLE,
                               gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
                               suggestedFN=cfg.get("program", "pdbDir"));
        if( fn!=None ):
            if(   not os.path.isdir(fn) ):
                # This should *never* happen...
                OkInfoDialog(NOT_A_DIRECTORY_TEXT);
                return ;
            elif( not os.access(fn, os.X_OK) ):
                OkInfoDialog(CANNOT_CHG2DIRECTORY_TEXT);
            # integrity check passed.
            s.pdbDirEntry.set_text(fn);
    
    def _TempDirLocCB(s, widget, data=None):
        cfg = s.dysConfig.GetConfig();
        fn = HandleFileChooser(PDB_DIR_SEL_WINDOW_TITLE,
                               gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
                               suggestedFN=cfg.get("program", "tempDir"));
        if( fn!=None ):
            if(   not os.path.isdir(fn) ):
                # This should *never* happen...
                OkInfoDialog(NOT_A_DIRECTORY_TEXT);
                return ;
            elif( not os.access(fn, os.X_OK) ):
                OkInfoDialog(CANNOT_CHG2DIRECTORY_TEXT);
                return ;
            elif( not os.access(fn, os.W_OK) ):
                OkInfoDialog(NO_WRITE_PERMS_TEXT);
                return ;
                
            # integrity check passed.
            s.tempDirEntry.set_text(fn);
            
    def _PymolLocCB(s, widget, data=None):
        fn = HandleFileChooser(PYMOL_FILE_SEL_WINDOW_TITLE,
                               gtk.FILE_CHOOSER_ACTION_OPEN);
        if( fn!=None ):
            s.pymolExecFFN = fn;
            s.pymolLocEntry.set_text(s.pymolExecFFN);
    
            
class CImportEDSnPDB(gtk.Dialog):
    """Import widget.

    Upon run() invocation, this widget returns 'gtk.RESPONSE_OK' if the OK
    button was pressed. 
    """
    def __init__(s):
        gtk.Dialog.__init__(s, title=IMPORTEDSPDB_WINDOW_TITLE, 
                   parent=None, flags=gtk.DIALOG_MODAL,
                   buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                            gtk.STOCK_OK, gtk.RESPONSE_OK));
        s._PdbCode = None;
        s.__BuildWindow();
        # This call here alone is futile. The entry widget needs to be enabled
        # for default actions, that is: entry.set_activates_default(True), see
        # below in __BuildWindow.
        s.set_default_response(gtk.RESPONSE_OK);
        s.show_all();

    def __BuildWindow(s):
        # There is already a vbox in this class.
        # Table row and columns are adjusted to their max element.
        s.Tab = gtk.Table(1, 2, homogeneous=False);
        s.importRemoteLabel = gtk.Label("PDB 4 letter code: ");
        s.importRemoteEntry = gtk.Entry();
        s.importRemoteEntry.set_max_length(0);
        s.importRemoteEntry.set_editable(True);
        s.importRemoteEntry.set_property("xalign", 0.0);
        s.importRemoteEntry.set_tooltip_text(EDS_PDB_ENTRY_TTEXT);
        # Allow default actions for this input widget.
        s.importRemoteEntry.set_activates_default(True);
        s.Tab.attach(s.importRemoteLabel, 0, 1, 0, 1, xpadding=5, ypadding=5);
        s.Tab.attach(s.importRemoteEntry, 1, 2, 0, 1, xpadding=5, ypadding=5);
        s.vbox.pack_start(s.Tab, False, False, padding=5);

    def GetPDBCode(s):
        """Get the PDB code entered in the dialog box.
        @return pdbCode  Either the 4 letter code or @c None to indicate that
         the process was canceled.
        """
        return s.importRemoteEntry.get_text();
    

def YesNoInfoDialog(msgText):
    """Pop up modal yes/no info dialog with text 'msgText'.
    Return True if 'Yes' was chosen, else False.
    """
    dlg  = gtk.MessageDialog(buttons=gtk.BUTTONS_YES_NO,
                             type=gtk.MESSAGE_WARNING, flags=gtk.DIALOG_MODAL);
    dlg.set_property("text", msgText);
    dlg.set_title("Information")
    retVal = dlg.run();
    dlg.destroy();
    return (retVal not in  (gtk.RESPONSE_NONE, gtk.RESPONSE_NO));

def OkInfoDialog(msgText):
    """Pop up modal info dialog with text 'msgText'.
    """
    dlg  = gtk.MessageDialog(buttons=gtk.BUTTONS_OK,
                             type=gtk.MESSAGE_WARNING, flags=gtk.DIALOG_MODAL);
    dlg.set_property("text", msgText);
    dlg.set_title("Information")
    retVal = dlg.run();
    dlg.destroy();

def SimpleOKDialog(msgText):
    """Pop up info dialog with text 'msgText'.
    """
    dlg  = gtk.MessageDialog(buttons=gtk.BUTTONS_OK)
    dlg.set_property("text", msgText);
    dlg.set_title("Information")
    retVal = dlg.run();
    dlg.destroy();

def HandleFileChooser(title, action, suggestedFN=""):
    """Helper function for file reading/writing and directory choosing dialog.
    - title: Window title.
    - action: One of gtk.FILE_CHOOSER_ACTION_OPEN,
      gtk.FILE_CHOOSER_ACTION_SAVE, gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER or
      gtk.FILE_CHOOSER_ACTION_CREATE_FOLDER
    - suggested: Suggested file/directory name.
    return None if nothing chosen, else the name of the chosen file.
    """
    if(   action==gtk.FILE_CHOOSER_ACTION_OPEN ):
        buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                   gtk.STOCK_OPEN, gtk.RESPONSE_OK);
    elif( action==gtk.FILE_CHOOSER_ACTION_SAVE ):
        buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                   gtk.STOCK_SAVE_AS, gtk.RESPONSE_OK);
    elif( action==gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER ):
        buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                   gtk.STOCK_OPEN, gtk.RESPONSE_OK);
    dlg = gtk.FileChooserDialog(title, None, action,buttons=buttons);
    dlg.set_default_response(gtk.RESPONSE_OK);
    dlg.set_select_multiple(False);
    if( suggestedFN!="" and action==gtk.FILE_CHOOSER_ACTION_SAVE ):
        dlg.set_current_name(suggestedFN);
    elif( suggestedFN!="" and action==gtk.FILE_CHOOSER_ACTION_OPEN ):
        # This is mainly to switch to the directory the file name belongs to.
        dlg.set_filename(suggestedFN);
    response = dlg.run();
    chosenName = None;
    if( response==gtk.RESPONSE_OK ):
        chosenName = dlg.get_filename();
    dlg.hide();
    dlg.destroy();
    return chosenName;


if( __name__=="__main__" ):
    ap=argparse.ArgumentParser(description="""This program displays the table
    of EDS ligand data and allows to edit comments. Coot is started for a
    selected line.""", epilog="""When launched without arguments, eg. from a
    GUI, the program displays an empty table. Data can be loaded from a file
    menu.""");
    ap.add_argument("-v", "--version",
                    help="Print version information and exit.",
                    action="version", version=__version__);
    ap.add_argument("--latest", dest="loadLatest",
                    help="""Just provide the base name without version number,
                    and the program will search for the highest version number
                    of the table and load it. The succeeding version number
                    will be used to save the file. It is granted that this
                    version number file does not yet exist. """, 
                    action="store_true");
    ap.add_argument("--config", type=str, 
                    help="""File name of alternative config file to load. If
                    it does not yet exist, it will be created and filled with
                    default values. This file can then be edited.""",
                    default="");
    ap.add_argument("--read", type=str, help="Input file with EDS table.",
                    dest="edstable", metavar="FILE", default=EMPTYFN);
    ap.add_argument("--show-hq-rscc", dest="goodLigands", action="store_true",
                    help="""Instead of viewing the table of ligands with low
                    RSCC (<%3.2f) values, show a table with very high RSCC
                    values (>=%3.2f).""" % (RSCCBADTH, RSCCGOODTH));
    ap.add_argument("--sort-rscc", dest="sortRSCC", action="store_true",
                    help="""Group sort the table by RSCC value only, instead
                    of combined score S.""");
    
    todo=ap.parse_args();
    edsInFN, edsOutFN    = GetVersionedInOutFileNames(todo.edstable);
    edsTableFN  = edsInFN if todo.loadLatest else todo.edstable;
    
    app = CGtkDysplayer(edsTableFN, todo.config, sortS=not todo.sortRSCC,
                        badLigands=not todo.goodLigands);
    gtk.main();

