from collections import defaultdict
from functools import partial
from typing import List
import inflect
from schrodinger.models import parameters
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.ui.qt import mapperwidgets
from schrodinger.ui.qt import propertyselector
from schrodinger.ui.qt.appframework2 import wizards
from schrodinger.ui.qt.widgetmixins import panelmixins
from . import constants
from . import data_classes
from . import entry_types as ets
from . import export_map_manager
from . import export_models
from . import export_tasks
from . import export_ui
from . import ld_folder_tree
from . import ld_utils
from . import live_report_widget
from . import login
from . import panel_components
from . import pose_name_panel
from . import summary
NOT_DEFINED = '(not defined)'
MORE = 'Show More'
LESS = 'Show Less'
MatchCompoundsBy = export_models.MatchCompoundsBy
RefreshResult = export_models.RefreshResult
PROPNAME_CORP_ID = constants.PROPNAME_CORP_ID
LD_DATA_3D = export_models.LD_DATA_3D
WARNING_COLOR = QtGui.QColor('firebrick')
SUCCESS_COLOR = QtGui.QColor('green')
# Mapping for user-passed string arguments to custom LD data items. Keys should
# be lowercase strings.
CUSTOM_STR_ITEM_MAP = {'3d': LD_DATA_3D}
[docs]class ExportModel(export_models.LDClientModelMixin, parameters.CompoundParam):
    ld_client: object
    ld_models: object
    entry_data: ets.BaseEntryData = None
    export_text: str
    select_text: str
    entry_type: object = ets.Ligands
    entry_type_description: str
    entry_type_name: str
    input_summary: str
    ld_destination: export_models.LDDestination
    match_compounds_by: MatchCompoundsBy
    match_prop_user_name: str = NOT_DEFINED
    match_prop_data_name: str
    more_columns_visible: bool = False
    task_rl_map: object
    export_task: export_tasks.MasterExportTask
    summary_model: export_models.SummaryModel
    pose_name_text: str = NOT_DEFINED
    pose_name_model: export_models.PoseNameEditModel
    lr_widget_model: live_report_widget.LiveReportModel
 
[docs]class LDExportPanelMixin:
    """
    Mixin for LD Export GUI panels. Provides a standardized window title that
    includes a custom string and the host name.
    Subclasses must:
        1. Inherit from `mappers.MapperMixin`
        2. Have a model that includes a `export_models.LDDestination` parameter
            called `ld_destination`
    :cvar TITLE_BASE: the standard string to include at the beginning of the
        window's title
    :type TITLE_BASE: str
    """
    TITLE_BASE = NotImplemented
[docs]    def getSignalsAndSlots(self, model):
        return super().getSignalsAndSlots(model) + [
            (model.ld_destination.hostChanged, self._updatePanelTitle)
        ] # yapf: disable 
    def _updatePanelTitle(self):
        """
        Update the panel title.
        """
        host = self.model.ld_destination.host
        window_title = self.TITLE_BASE
        if host:
            window_title += f' ({host})'
        self.setWindowTitle(window_title) 
[docs]class AbstractExportPanel(LDExportPanelMixin, panelmixins.TaskPanelMixin,
                          wizards.BaseWizardPanel):
    """
    :cvar allow_add_live_reports: whether users should be presented with the
        option to create a new live report from this panel
    :vartype allow_add_live_reports: bool
    """
    model_class = ExportModel
    PANEL_TASKS = [model_class.export_task]
    ui_module = export_ui
    EXPORT_MAP_MANAGER_CLASS = export_map_manager.ExportMapManager
    SELECT_TEXT = 'Maestro properties:'
    EXPORT_TEXT = 'Map Maestro properties to LiveDesign properties:'
    MAP_SELECT_TEXT = 'Select Map...'
    TABLE_MODEL_CLASS = panel_components.ExportTableModel
    TABLE_VIEW_CLASS = panel_components.ExportTableView
    SHOW_POSE_PAGE = 0
    HIDE_POSE_PAGE = 1
    DISABLED_TEXTS = {'', NOT_DEFINED}
    SAVE_MAPPING_TEXT = 'Save Mapping...'
    LOAD_MAPPING_TEXT = 'Load Mapping'
    DELETE_MAPPING_TEXT = 'Delete'
    DELETE_MAPPING_PREFKEY = 'export_delete_mapping_prefkey'
    INVALID_MAP_NAME_MSG = 'Please enter a valid name for saving a new map.'
    MAP_ERROR_MSG = ('Error: The selected mapping could not be successfully '
                     'applied. One or more data rows in the export table '
                     'may be incorrect.')
    MAP_EXISTS_MSG = ('The following map name already exists: {0}\nPlease enter'
                      ' a new name.')
    allow_add_live_reports = True
[docs]    def initSetOptions(self):
        super().initSetOptions()
        self._default_ld_items_checked = False
        self._user_default_items = [] 
[docs]    def initSetUp(self):
        super().initSetUp()
        self.table_model = self.TABLE_MODEL_CLASS()
        self.table_model.set3DDataSpecMap(self._get3DDataSpecMap())
        self.table_model.setFFCDataSpecMap(self._getFFCDataSpecMap())
        self.table_view = self.TABLE_VIEW_CLASS(self)
        self.table_view.setModel(self.table_model)
        self.lr_widget = live_report_widget.LiveReportWidget(
            self, allow_add_live_reports=self.allow_add_live_reports)
        self.ld_data_tree = panel_components.LDDataTree(self)
        self.summary_panel = summary.LiveDesignExportSummaryPanel(parent=self)
        self.pose_name_panel = pose_name_panel.PoseNameEditPanel(parent=self)
        self.setDownstreamWidgetsEnabled(False)
        self.match_compounds_btn_grp = mapperwidgets.MappableButtonGroup({
            self.ui.match_by_structure_rb: MatchCompoundsBy.structure,
            self.ui.match_by_corp_id_rb: MatchCompoundsBy.corp_id,
        })
        self.ui.select_property_btn.clicked.connect(self._selectProperty)
        self.ui.show_more_btn.clicked.connect(self._onShowColumns)
        self.ui.pose_name_btn.clicked.connect(self.pose_name_panel.run)
        self.ld_data_tree.dataChanged.connect(self._onLDDataSelectionChanged)
        self.ui.clear_props_btn.clicked.connect(self.ld_data_tree.uncheckAll)
        self.ui.use_pose_name_cb.clicked.connect(self._setPoseWidgetsEnabled)
        # Initialize initial pose widgets
        self._setPoseWidgetsEnabled(self.ui.use_pose_name_cb.isChecked())
        self.publish_btn_grp = mapperwidgets.MappableButtonGroup({
            self.ui.unpublish_data_rb: False,
            self.ui.publish_data_rb: True,
        })
        self.save_mapping_dlg = export_map_manager.SaveMappingDialog(
            parent=self)
        self.save_mapping_dlg.saveMappingRequested.connect(self.saveExportMap)
        # TODO: Hiding the "Show more/less" button for now, since the
        # functionality afforded by those columns is not yet implemented
        self.ui.show_more_btn.setVisible(False)
        # No "Next" or "Cancel" buttons required for this panel
        self.next_btn.hide()
        self.cancel_btn.hide() 
[docs]    def initLayOut(self):
        super().initLayOut()
        self.ui.lr_widget_layout.addWidget(self.lr_widget)
        self.ui.export_table_layout.addWidget(self.table_view)
        self.ui.ld_data_select_layout.addWidget(self.ld_data_tree) 
[docs]    def initSetDefaults(self):
        """
        Override `mappers.MapperMixin` to avoid resetting the entire model.
        """
        self._resetPropNameValues()
        self.updateExportableData()
        self._setDefaultLDDataItemsChecked() 
[docs]    def initFinalize(self):
        super().initFinalize()
        self._onMatchCompoundMethodChanged()
        self._onMatchPropertyChanged()
        self._onPoseNameValueChanged() 
[docs]    def processPrevPanel(self, state):
        super().processPrevPanel(state)
        if not self._default_ld_items_checked:
            self.initSetDefaults()
            self._default_ld_items_checked = True 
[docs]    def setModel(self, model):
        super().setModel(model)
        self.summary_panel.setModel(self.model.summary_model)
        self.lr_widget.setModel(self.model.lr_widget_model)
        self.pose_name_panel.setModel(self.model.pose_name_model)
        self.model.export_text = self.EXPORT_TEXT
        self.model.select_text = self.SELECT_TEXT
        self.model.ld_destination.host = login.get_host() 
[docs]    def defineMappings(self):
        M = self.model_class
        ui = self.ui
        return super().defineMappings() + [
            (ui.ld_data_select_lbl, M.select_text),
            (ui.ld_data_export_lbl, M.export_text),
            (self.match_compounds_btn_grp, M.match_compounds_by),
            (ui.selected_property_lbl, M.match_prop_user_name),
            (ui.pose_name_value_lbl, M.pose_name_text),
            (self.publish_btn_grp, M.export_task.input.publish_data),
            (ui.input_summary_lbl, M.input_summary),
        ] # yapf: disable 
[docs]    def getSignalsAndSlots(self, model):
        lr_widget = self.lr_widget
        pn_model = model.pose_name_model
        return super().getSignalsAndSlots(model) + [
            (model.match_compounds_byChanged,
                self._onMatchCompoundMethodChanged),
            (model.match_prop_data_nameChanged, self._onMatchPropertyChanged),
            (model.pose_name_textChanged, self._onPoseNameValueChanged),
            (model.entry_dataChanged, self.updateExportableData),
            (model.entry_dataChanged, self._resetPropNameValues),
            (model.ld_clientChanged, self._onLDClientChanged),
            (model.more_columns_visibleChanged,
                self._onColumnVisibilityChanged),
            (lr_widget.liveReportSelected, self._onLRSelectionChanged),
            (lr_widget.newLiveReportSelected, self._onLRSelectionChanged),
            (lr_widget.liveReportDefaultsApplied, self._onLRSelectionChanged),
            (lr_widget.disconnected, self._showDisconnectedError),
            (pn_model.custom_text_finalChanged, self._updatePoseNameLabel),
            (pn_model.property_name_finalChanged, self._updatePoseNameLabel)
        ] # yapf: disable 
[docs]    def defineTaskPreprocessors(self, model):
        task = model.export_task
        return super().defineTaskPreprocessors(model) + [
            (task, self._checkConnection, constants.ORDER_VALIDATE),
            (task, self._checkFields, constants.ORDER_VALIDATE),
            (task, self._checkFFCs, constants.ORDER_VALIDATE),
            (task, self._copyRLMap, constants.ORDER_COPY_RL_MAP),
            (task, self._populateTaskParams, constants.ORDER_POPULATE_TASK),
            (task, self._showSummary, constants.ORDER_SHOW_SUMMARY),
            (task, self._connectTaskSignals, constants.ORDER_CONNECT_SIGNALS),
            (task, self._setWaitingStatus, constants.ORDER_POST_SUMMARY)
        ] # yapf: disable 
[docs]    def setDefaultCheckedItems(self, item_strs: List[str]):
        """
        Specify which LD data items will be checked by default.
        :param item_strs: string representations of the desired checked data
                item. In most cases, this is the structure property name.
        """
        self._default_ld_items_checked = False
        self._user_default_items = []
        unrecognized_item_strs = []
        for item_str in item_strs:
            lddata = CUSTOM_STR_ITEM_MAP.get(item_str.lower())
            if lddata is None:
                try:
                    lddata = data_classes.LDData(data_name=item_str)
                except:
                    pass
            if lddata is not None:
                self._user_default_items.append(lddata)
            else:
                unrecognized_item_strs.append(item_str)
        if unrecognized_item_strs:
            print(f'Unrecognized item strings: {unrecognized_item_strs}') 
    def _getDefaultLDDataItems(self) -> List[data_classes.LDData]:
        """
        :return: `LDData` items to be checked by default when first launching
                the GUI
        """
        return list(self._user_default_items)
    def _setDefaultLDDataItemsChecked(self):
        """
        Set the desired LD data items to be checked by default.
        """
        default_ldd_items = self._getDefaultLDDataItems()
        self.setCheckedLDData(default_ldd_items)
    def _checkConnection(self):
        """
        Determine whether the `LDClient` instance is still connected to the
        LiveDesign server and attempt to reconnect; if not possible, warn user.
        """
        is_connected = self.model.refreshLDClient() != RefreshResult.failure
        if not is_connected:
            self._showDisconnectedError()
        return is_connected
    def _showDisconnectedError(self):
        """
        Present the user with an error dialog warning them that they are no
        longer connected to a LiveDesign server.
        """
        msg = ('The connection to the LiveDesign server has been lost. Try'
               ' restarting the panel and re-entering your credentials to'
               ' restore it.')
        self.error(msg)
    def _getMissingFields(self):
        """
        Return a list of strings that identify fields in the export table that
        the user must fill out prior to export.
        :return: a list of strings describing fields that are required for
            export but have not been specified
        :rtype: list[str]
        """
        missing_fields = []
        model = self.model
        if not model.ld_destination.proj_name:
            missing_fields.append('Project')
        if not model.ld_destination.lr_name:
            missing_fields.append('LiveReport')
        if (model.match_compounds_by == MatchCompoundsBy.corp_id and
                not model.match_prop_data_name):
            missing_fields.append('Match Existing Compounds by Property')
        for spec in self.table_model.getExportSpecMap().values():
            missing_table_fields = []
            if spec.ld_model == panel_components.MODEL_OR_ASSAY_MISSING:
                missing_table_fields.append('Model/Assay Column')
            if spec.ld_endpoint == panel_components.ENDPOINT_MISSING:
                missing_table_fields.append('Endpoint Column')
            if missing_table_fields:
                missing_fields += missing_table_fields
                break
        return missing_fields
    def _checkFields(self):
        """
        Checks that all the needed fields for export are specified. If fields
        are missing, returns False and an error message detailing the missing
        fields, otherwise returns True
        """
        missing_fields = self._getMissingFields()
        if not missing_fields:
            return True
        self.table_model.highlight_missing_fields = True
        msg = 'Export failed. The following fields have missing values:'
        msg += _get_bulleted_string_from_list(missing_fields)
        self.error(msg)
        return False
    def _checkFFCs(self):
        """
        Prevent the user from exporting multiple FFC items that share the same
        model/assay name, as this will cause an export failure (PANEL-14681).
        """
        name_to_export_data = defaultdict(list)
        for spec in self.table_model.getFFCExportSpecMap().values():
            name_to_export_data[(spec.ld_model, spec.ld_endpoint)].append(spec)
        bad_item_strs = []
        for model_name, export_data_list in name_to_export_data.items():
            if len(export_data_list) > 1:
                for ed in export_data_list:
                    item_str = f'{model_name}: {ed.name}, {ed.endpoint}'
                    bad_item_strs.append(item_str)
        if not bad_item_strs:
            return True
        msg = ('Export failed. The following data items must not share model/'
               'assay names and endpoints:')
        msg += _get_bulleted_string_from_list(bad_item_strs)
        self.error(msg)
        return False
    def _copyRLMap(self):
        """
        Copy the RL map in anticipation for modification prior to export.
        """
        self.model.task_rl_map = self.model.entry_data.getRLMap()
    def _populateTaskParams(self):
        """
        Process data from the panel into export task input parameters.
        """
        task_input = self.model.export_task.input
        table_model = self.table_model
        rl_map = self.model.task_rl_map
        prop_specs = table_model.getPropertyExportSpecMap().values()
        task_input.property_export_specs = list(prop_specs)
        task_input.three_d_export_items = []
        # Prepare data for standard 3D export, if requested
        corp_id_match_prop = None
        if self.model.match_compounds_by == MatchCompoundsBy.corp_id:
            corp_id_match_prop = self.model.match_prop_data_name
        for spec in table_model.get3DExportSpecMap().values():
            spec.addDataToExportTask(self.model.export_task, rl_map,
                                     corp_id_match_prop)
        # Prepare data for standard 2D export. Note that redundancy between
        # these compounds and the 3D export items will be removed during the
        # master export task if necessary.
        compounds = list(set(rl_map.ligands))
        if corp_id_match_prop is not None:
            for st in compounds:
                corp_id = st.property.get(corp_id_match_prop)
                # Corporate ID when available should only be a string value.
                corp_id = None if corp_id is None else str(corp_id)
                ld_utils.safely_set_property(st, PROPNAME_CORP_ID, corp_id)
        task_input.structures_for_2d_export = compounds
        # Prepare data for freeform column export, if requested
        ffc_specs = list(table_model.getFFCExportSpecMap().values())
        for spec in ffc_specs:
            spec.getAttachmentData(self.model)
        task_input.ffc_export_specs = ffc_specs
    def _showSummary(self):
        """
        Present the user with a summary of data to be exported.
        """
        return self.summary_panel.run(blocking=True, modal=True)
    def _connectTaskSignals(self):
        """
        Connect signals associated with the task before it is run.
        This preprocessor should be run last so that there is no chance of
        connecting a signal/slot pair more than once.
        """
        self.model.export_task.taskDone.connect(self._onExportFinished)
        self.model.export_task.exportFailed.connect(self._onExportFailed)
    def _setWaitingStatus(self):
        """
        Notify the user that the export is in progress.
        """
        task = self.model.export_task
        msg = f'{task.name}: waiting for confirmation from LiveDesign server.'
        self.setStatus(msg)
    def _onExportFinished(self):
        """
        Present the user with a dialog with the results of the export process.
        """
        task = self.sender()
        output = task.output
        num_attempt = output.num_success + output.num_failure
        if output.num_success == 0 or output.num_failure > 0:
            # A failure occurred during the export process
            if num_attempt == 0:
                status_msg = 'Something went wrong during the export process.'
            else:
                status_msg = (f'{task.name}: {output.num_failure} out of'
                              f' {num_attempt} export processes failed.')
            lr_url_txt = f'LiveReport URL: {output.lr_url}'
            result_url_txt = 'Result URLs:' + '\n'.join(output.result_urls)
            msg = ('\nPlease contact Technical Support at help@schrodinger.com'
                   f' with the following information:\n{lr_url_txt}\n'
                   f'{result_url_txt}')
            self.setStatus(status_msg, color=WARNING_COLOR)
            self.error(status_msg + msg)
            return
        if output.unexported_items:
            # No failure occurred, but some 3D items did not get exported
            num_items = len(output.unexported_items)
            item_str = inflect.engine().plural('item', num_items)
            process_str = inflect.engine().plural('process', output.num_success)
            status_msg = (f'{task.name}: {num_items} {item_str} failed to'
                          f' export during {output.num_success} successful'
                          f' {process_str}.')
            status_color = WARNING_COLOR
        else:
            # Set the active LiveReport to the one to which a successful export
            # was just performed
            lr_id = task.input.ld_destination.lr_id
            if lr_id:
                self.lr_widget.setLiveReport(lr_id)
            status_msg = (f'{task.name}: {output.num_success} export'
                          f' processes were successful.')
            status_color = SUCCESS_COLOR
        self.setStatus(status_msg, color=status_color)
        msg = (f'Structures have been added to the LiveReport <a href='
               f'"{output.lr_url}">{output.lr_url}</a>')
        self.info(msg)
    def _getProperties(self):
        """
        :return: a set of structure property names for all ligands/complexes in
            the current system
        :rtype: set[str]
        """
        data_names = set()
        if not self.model.entry_data:
            return data_names
        for ligand in self.model.entry_data.getRLMap().ligands:
            data_names |= set(ligand.property.keys())
        return data_names
    def _selectProperty(self):
        """
        Launch a dialog prompting the user to select a structure property that
        contains corporate IDs.
        """
        # Show only string and integer properties, we will further convert
        # these properties to a string value before exporting.
        prop_sel_dialog = propertyselector.PropertySelectorDialog(
            parent=self,
            type_filter=['s', 'i'],
            title='Select Corporate ID Property',
            accept_text='OK',
            show_alpha_toggle=True,
            show_filter_field=True)
        data_names = self._getProperties()
        properties = prop_sel_dialog.chooseFromList(data_names)
        if properties:
            selected_prop = properties[0]
            self.model.match_prop_user_name = selected_prop.userName()
            self.model.match_prop_data_name = selected_prop.dataName()
    def _onMatchCompoundMethodChanged(self):
        """
        Update property selector widget visibility when the user changes how
        compounds should be matched in LD.
        """
        if self.model.match_compounds_by == MatchCompoundsBy.structure:
            page = self.HIDE_POSE_PAGE
            M = self.model_class
            for param in [M.match_prop_data_name, M.match_prop_user_name]:
                self.model.reset(param)
        else:
            page = self.SHOW_POSE_PAGE
        self.ui.match_corp_id_stack.setCurrentIndex(page)
    def _onMatchPropertyChanged(self):
        """
        Update the match property label color when its text is changed.
        """
        enable = self.model.match_prop_data_name not in self.DISABLED_TEXTS
        self._colorLabel(self.ui.selected_property_lbl, enable)
    def _setPoseWidgetsEnabled(self, enable):
        """
        Show or hide pose widgets.  Reset on hide.
        """
        self.ui.pose_name_value_lbl.setVisible(enable)
        self.ui.pose_name_btn.setEnabled(enable)
        if not enable and self.model is not None:
            self.model.reset(self.model_class.pose_name_text)
    def _onPoseNameValueChanged(self):
        pose_name = self.model.pose_name_text
        enable = pose_name not in self.DISABLED_TEXTS
        self._colorLabel(self.ui.pose_name_value_lbl, enable)
    def _colorLabel(self, label, enable: bool):
        """
        Colors label based on enable value.
        Disabled labels are disabled, enabled labels are bolded
        """
        stylesheet = 'font: bold' if enable else ''
        label.setEnabled(enable)
        label.setStyleSheet(stylesheet)
    def _onLRSelectionChanged(self):
        """
        Respond to the live report ID changing.
        """
        if self.model.refreshLDClient() == RefreshResult.failure:
            self._showDisconnectedError()
            lr_defined = False
        else:
            lr_defined = bool(self.model.ld_destination.lr_name)
            self._loadLDFolderTree()
        self.table_model.disable_lr_columns = not lr_defined
        self.setDownstreamWidgetsEnabled(lr_defined)
        self.generateMappingsMenu()
    def _loadLDFolderTree(self):
        """
        Add the assay names to the assay column tree model.
        """
        proj_id = self.model.ld_destination.proj_id
        tree = ld_folder_tree.LDFolderTree(proj_id)
        if proj_id:
            tree.fillFolderTree()
        endpoints = dict(tree.endpoints)
        self.table_model.loadAssayData(endpoints, tree.favorite_endpoints)
        self.table_model.loadEndpointData(endpoints)
        # activate the mapping manager
        host = self.model.ld_destination.host
        self.export_map_manager = self.EXPORT_MAP_MANAGER_CLASS(host, proj_id)
        self.generateMappingsMenu()
    def _getExportableData(self):
        """
        Return a set of data available for the user to select for export to
        LiveDesign.
        :return: a list of exportable data
        :rtype: list[data_classes.LDData]
        """
        ld_data_props = [
            data_classes.LDData(data_name=data_name)
            for data_name in self._getProperties()
        ]
        return ld_data_props
    def _get3DDataSpecMap(self):
        """
        This method associates `LDData` instances with 3D export specs.
        If a subclass wants to add a non-standard 3D export data to the panel,
        it must be included here.
        :return: a dictionary that maps 3D `LDData` instances with their
            associated export spec classes
        :rtype: dict[data_classes.LDData, export_models.Base3DExportSpec]
        """
        return {export_models.LD_DATA_3D: export_models.Standard3DExportSpec}
    def _getFFCDataSpecMap(self):
        """
        This method associates `LDData` instances with FFC export specs.
        If a subclass wants to add a FFC export data to the panel, it must be
        included here.
        :return: a dictionary that maps FFC `LDData` instances with their
            associated export spec classes
        :rtype: dict[data_classes.LDData, export_models.FFCExportSpec]
        """
        return {}
[docs]    def updateExportableData(self):
        """
        Update the selection and state of the items that can be exported from
        this panel to LiveDesign.
        """
        _3d_ld_data = self._get3DDataSpecMap().keys()
        ffc_ld_data = self._getFFCDataSpecMap().keys()
        ld_data_list = self._getExportableData()
        ld_data_list += list(_3d_ld_data) + list(ffc_ld_data)
        self.ld_data_tree.loadData(ld_data_list)
        self.updateEnabledExportableData()
        self._onLDDataSelectionChanged() 
    def _resetPropNameValues(self):
        """
        Resets values for selected property names
        """
        M = self.model_class
        params_to_reset = [
            M.match_compounds_by, M.match_prop_user_name, M.match_prop_data_name
        ]
        for param in params_to_reset:
            self.model.reset(param)
[docs]    def updateEnabledExportableData(self):
        """
        Enable or disable exportable data items.
        Should be overridden in subclasses.
        """
        pass 
    def _onLDDataSelectionChanged(self):
        """
        Called when LD data selection is changed.
        """
        ld_data_list = self.ld_data_tree.getCheckedData()
        self.table_model.loadData(ld_data_list)
[docs]    def openSaveMappingDialog(self):
        self.save_mapping_dlg.show()
        self.save_mapping_dlg.raise_() 
[docs]    def saveExportMap(self, map_name):
        """
        Save the current export table's state as a map file.
        """
        manager = self.export_map_manager
        if not map_name:
            self.warning(self.INVALID_MAP_NAME_MSG)
            return
        if map_name in manager.getAvailableMappings():
            self.warning(self.MAP_EXISTS_MSG.format(map_name))
            return
        # TODO: Currently, mapping only supports structure property rows. This
        # can be expanded if necessary to include all non-3D rows
        mappable_rows = self.table_model.getMappableRows()
        manager.saveNewMapping(map_name, mappable_rows)
        self.generateMappingsMenu() 
[docs]    def openSelectedExportMap(self, map_name):
        """
        Slot connected to selecting a mapping. Open the selected map and
        use the mappings to populate the export table.
        This method resets the property selection tree along with the export
        table.
        """
        file_path = self.export_map_manager.getMapFilePath(map_name)
        self.openExportMap(file_path)
        self.generateMappingsMenu() 
[docs]    def openExportMap(self, map_file):
        """
        Open the map_file and use the mappings to populate the export table.
        This method resets the property selection tree along with the export
        table. This function is used in KNIME to restore the Properties table
        contents from a map.json file
        :param map_file: Path to a json map file
        :type map_file: str
        """
        try:
            map_rows = self.export_map_manager.openMapping(map_file)
        except export_map_manager.ExportMapTypeError as exc:
            # The mapping will fail if the host and project from the map do not
            # match the current host and project
            self.warning(str(exc))
            return
        self.setExportItemData(map_rows) 
[docs]    def deleteExportMap(self, map_name):
        """
        Delete the given export map.
        """
        text = (f'Remove "{map_name}" from the list?'
                '\nThis will cause the corresponding file to be deleted.')
        if not self.question(title='Warning',
                             text=text,
                             yes_text='Delete',
                             no_text=None,
                             add_cancel_btn=True,
                             icon=QtWidgets.QMessageBox.Warning,
                             save_response_key=self.DELETE_MAPPING_PREFKEY):
            return
        self.export_map_manager.deleteMapping(map_name)
        self.generateMappingsMenu() 
[docs]    def setExportItemData(self, export_rows):
        """
        Update the panel with the specified export item data.
        Export data not available in the LD item tree will be ignored. If such
        data is supplied, the user will be presented with a warning message.
        :param export_rows: export row data
        :type export_rows: list[]
        """
        ld_data_list = [row.ld_data for row in export_rows]
        self.setCheckedLDData(ld_data_list)
        success = self.table_model.loadMappings(export_rows)
        if not success:
            self.warning(self.MAP_ERROR_MSG) 
[docs]    def setCheckedLDData(self, ld_data_list):
        """
        Assign a check state to the boxes in the data selection tree.
        May be overridden in subclasses that require more complex behavior when
        changing the LD data tree state.
        :param ld_data_list: check boxes associated with these `LDData` objects,
                and uncheck all other boxes
        :type ld_data_list: list[data_classes.LDData]
        """
        self.ld_data_tree.setCheckedData(ld_data_list) 
    def _onLDClientChanged(self):
        """
        Respond to the selection of a new LD client instance.
        """
        version = login.get_LD_version(self.model.ld_client)
        visible = version >= login.LD_VERSION_CORP_ID_MATCHING
        self.ui.match_compounds_widget.setVisible(visible)
        visible = version >= login.LD_VERSION_POSE_NAMES
        self.ui.pose_name_widget.setVisible(visible)
    def _onColumnVisibilityChanged(self):
        """
        Update panel state in response to extra column visibility changes.
        """
        text = LESS if self.model.more_columns_visible else MORE
        self.ui.show_more_btn.setText(text)
    def _onShowColumns(self):
        """
        Slot invoked when 'Show More' or 'Show Less' button is clicked.
        """
        visible = self.model.more_columns_visible
        self.table_view.setExtraColumnsVisible(visible)
    def _updatePoseNameLabel(self):
        model = self.model.pose_name_model
        propname = model.property_name
        user_name = propname.userName() if propname else ''
        pose_name_text = model.custom_text
        if user_name:
            pose_name_text += f'<b><{user_name}></b>'
        if pose_name_text:
            self.model.pose_name_text = pose_name_text
        else:
            self.model.reset(self.model_class.pose_name_text)
    def _onExportFailed(self, error_str):
        """
        Notify the users when the export fails.
        :param error_str: a message with more details about the failure
        :type error_str: str
        """
        self.setStatus(error_str, color=WARNING_COLOR)
        msg = f'An error occurred during the export process. {error_str}'
        self.error(msg)
[docs]    def setStatus(self, message, timeout=0, color=None):
        """
        Set the status bar text with optional timeout and text color.
        :param message: the message to display
        :type message: str
        :param timeout: time to show message in ms. If set to 0 (default) the
                message is not cleared.
        :type timeout: int
        :param color: text color for message. Default is black.
        :type color: QtGui.QColor
        """
        color = color or QtGui.QColor(Qt.black)
        sheet_txt = (f'QStatusBar{{color: rgb({color.red()},{color.green()},'
                     f'{color.blue()}); font:bold}}')
        self.status_bar.setStyleSheet(sheet_txt)
        self.status_bar.showMessage(message, timeout)  
def _get_bulleted_string_from_list(items):
    """
    Return a HTML-formatted string with a bulleted list of the supplied items.
    :param items: a list of items to include in the bulleted list string
    :type items: list(str)
    :return: a HTML-formatted bulleted list string
    :type: str
    """
    bulleted_string = ''
    for item in items:
        bulleted_string += f'<li>{item}</li>'
    return f'<ul>{bulleted_string}</ul>'