### Navigation

- index
- modules |
- SamSifter 0.5.4 documentation »
- Module code »

# Source code for samsifter.samsifter

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    """SamSifter helps you create filter workflows for NGS data.

    It is primarily used to process SAM files generated by MALT prior to
    metagenomic analysis in MEGAN.

    .. moduleauthor:: Florian Aldehoff <faldehoff@student.uni-tuebingen.de>
    """
    import signal
    import sys
    if not (sys.version_info[0] >= 3):
        print("Error, I need python 3.x or newer")
        exit(1)

    import platform
    import argparse
    import logging as log
    from functools import partial
    from os.path import basename, dirname, expanduser

    """ Qt4 """
    from PyQt4.QtGui import (
        QMainWindow, QHBoxLayout, QTreeView, QFrame, QIcon, QAction, QStandardItem,
        QDockWidget, QApplication, QLabel, QFileDialog, QVBoxLayout,
        QAbstractItemView, QMessageBox, QTextEdit, QColor, QKeySequence,
        QDesktopServices
    )
    from PyQt4.QtCore import (
        Qt, QSize, QProcess, QFileInfo, QPoint, QSettings, QTimer, QFile,
        PYQT_VERSION_STR, QT_VERSION_STR, QProcessEnvironment, QUrl
    )

    """ custom libraries """
    from samsifter.gui.widgets import InputWidget, OutputWidget
    from samsifter.gui.dialogs import (
        BashOptionsDialog, RmaOptionsDialog, RmaOptions
    )
    # Add your new tools/filter to this import list!
    from samsifter.tools import (
        filter_read_list, filter_read_pmds, filter_read_identity,
        filter_read_conservation, filter_ref_list, filter_ref_identity,
        filter_ref_pmds, filter_ref_coverage, filter_taxon_list, filter_taxon_pmds,
        calculate_pmds, sort_by_coordinates, sort_by_names, remove_duplicates,
        sam_2_bam, bam_2_sam, compress, decompress, count_taxon_reads,
        better_remove_duplicates
    )
    from samsifter.models.filter_model import FilterTreeModel
    from samsifter.views.filterviews import FilterListView
    from samsifter.models.workflow import Workflow
    from samsifter.util.validation import WorkflowValidator
    from samsifter.models.filter import FilterItem
    from samsifter.resources import resources
    assert resources  # silence pyflakes
    from samsifter.version import samsifter_version

    __version__ = samsifter_version

    """ globals """
    VERSION = __version__
    FILE_FORMATS = ["*.ssx", "*.SSX"]
    TITLE = "SamSifter"
    DESC = ("SamSifter helps you create filter workflows for next-generation "
            "sequencing data. It is primarily used to process SAM files generated "
            "by MALT prior to metagenomic analysis in MEGAN.")
    COPYRIGHT = ("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."
                 "<p>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."
                 "<p>You should have received a copy of the GNU General Public "
                 "License along with this program. If not, see "
                 '<a href="http://www.gnu.org/licenses/">'
                 "http://www.gnu.org/licenses/</a>.")
    GREETING = ("<h3>Welcome to %s version %s!</h3><p>%s<p>To start working add "
                "filters to the workflow above. You can add filters by "
                "doubleclicking available  entries in the righthand dock or by "
                "selecting an entry from the menu <b>[Edit > Add Filter...]</b>. "
                "Similarly, you can doubleclick filters in the workflow to edit "
                "their settings and parameters. The lefthand toolbar is used to "
                "move and delete filter steps."
                "<p>You can save your workflow to a file, execute it right now "
                "by clicking the <b>[Run]</b> button and even export it to a Bash "
                "script to run it on another workstation. Any errors and "
                "messages will be reported to you right here. Feel free to "
                "re-arrange these docks to make yourself comfortable - %s will "
                "remember your settings. Accidentally closed docks can always "
                "be re-opened from the menu <b>[View]<b>."
                % (TITLE, __version__, DESC, TITLE))
    RIGHT_DOCK_MIN_HEIGHT = 220
    RIGHT_DOCK_MIN_WIDTH = 300
    BOTTOM_DOCK_MIN_HEIGHT = 200
    RECENT_FILES_MAX_LENGTH = 9
    STATUS_DELAY = 5000

    class MainWindow(QMainWindow):
        """Main window of SamSifter GUI interface.

        Provides a central list view of filter items and freely arrangeable docks
        for filter settings, toolbox and log messages.
        """

        def __init__(self, verbose=False, debug=False):
            """Initialize main window.

            Parameters
            ----------
            verbose : bool, optional
                Enable printing of additional information to STDERR, defaults to
                False.
            debug : bool, optional
                Enable debug menu with additional options to analyze errors,
                defaults to False.
            """
            super(MainWindow, self).__init__()
            self.verbose = verbose
            self.debug = debug

            """ data models """

            self.recent_files = []

            self.filters = {}
            self.populate_filters()

            self.tree_model = FilterTreeModel(self)
            root = self.tree_model.invisibleRootItem()
            for category in self.filters:
                branch = QStandardItem(category)
                branch.setSelectable(False)
                branch.setEditable(False)
                root.appendRow(branch)
                for fltr in self.filters[category]:
                    leaf = fltr
                    branch.appendRow(leaf)
            # increase GUI haptics by making sure elements are always in sam
            self.tree_model.sort(0)

            self.wf = Workflow(self)
            self.wf.validity_changed.connect(self.on_validity_change)
            self.wf.changed.connect(self.update_status)

            # QProcess object for external app
            self.process = None
            self.pid = -1

            """ viewers and controllers """

            self.init_ui()

            """ previous settings """

            settings = QSettings()
            if settings.value("RecentFiles") is not None:
                self.recent_files = settings.value("RecentFiles")
            self.resize(settings.value("MainWindow/Size", QSize(800, 800)))
            self.move(settings.value("MainWindow/Position", QPoint(0, 0)))
            if settings.value("MainWindow/State") is not None:
                self.restoreState(settings.value("MainWindow/State"))

            self.rma_options = RmaOptions()
            if settings.value("SAM2RMA/TopPercent") is not None:
                self.rma_options.set_top_percent(
                    float(settings.value("SAM2RMA/TopPercent"))
                )
            if settings.value("SAM2RMA/MaxExpected") is not None:
                self.rma_options.set_max_expected(
                    float(settings.value("SAM2RMA/MaxExpected"))
                )
            if settings.value("SAM2RMA/MinScore") is not None:
                self.rma_options.set_min_score(
                    float(settings.value("SAM2RMA/MinScore"))
                )
            if settings.value("SAM2RMA/MinSupportPercent") is not None:
                self.rma_options.set_min_support_percent(
                    float(settings.value("SAM2RMA/MinSupportPercent"))
                )
            if settings.value("SAM2RMA/Sam2RmaPath") is not None:
                self.rma_options.set_sam2rma_path(
                    settings.value("SAM2RMA/Sam2RmaPath")
                )

            self.populate_file_menu()
            QTimer.singleShot(0, self.load_initial_file)

            self.show()

        def populate_filters(self):
            """Compiles available filters to group them into named categories.

            Note
            ----
            To add a new filter or tool to the menu of available items simply
            choose a category and append your
            :py:class:`samsifter.models.filter.FilterItem`
            to the corresponding list. This is usually done by calling the
            ``item()`` method of your module in package :py:mod:`samsifter.tools`.
            Entries will be sorted by category name and item name prior to display
            in the GUI so the order within the list does not matter.

            In order to call the ``item()`` method of your new module you will
            also have to import it in the header of this module.
            """

            """ analyzers """
            analyzers = []
            analyzers.append(calculate_pmds.item())
            analyzers.append(count_taxon_reads.item())
            self.filters["Analyzers"] = analyzers

            """ converters """
            converters = []
            converters.append(sam_2_bam.item())
            converters.append(bam_2_sam.item())
            converters.append(compress.item())
            converters.append(decompress.item())
            self.filters["Format Converters"] = converters

            """ sorters """
            sorters = []
            sorters.append(sort_by_coordinates.item())
            sorters.append(sort_by_names.item())
            self.filters["File Sorters"] = sorters

            """ read filters """
            read_filters = []
            read_filters.append(filter_read_list.item())
            read_filters.append(filter_read_pmds.item())
            read_filters.append(filter_read_identity.item())
            read_filters.append(filter_read_conservation.item())
            read_filters.append(remove_duplicates.item())
            read_filters.append(better_remove_duplicates.item())
            self.filters["Read Filters"] = read_filters

            """ reference filters """
            reference_filters = []
            reference_filters.append(filter_ref_list.item())
            reference_filters.append(filter_ref_identity.item())
            reference_filters.append(filter_ref_pmds.item())
            reference_filters.append(filter_ref_coverage.item())
            self.filters["Reference Filters"] = reference_filters

            """ taxon filters """
            taxon_filters = []
            taxon_filters.append(filter_taxon_list.item())
            taxon_filters.append(filter_taxon_pmds.item())
            self.filters["Taxon Filters"] = taxon_filters

        def init_ui(self):
            """Initialize GUI components (widgets, actions, menus and toolbars)."""
            self.setWindowTitle(TITLE)
            self.setDockOptions(QMainWindow.AnimatedDocks
                                | QMainWindow.AllowNestedDocks)

            """ central workflow widget """

            self.input_widget = InputWidget(self)
            self.input_widget.file_entry.setText(self.wf.get_in_filename())
            self.wf.input_changed.connect(self.input_widget.set_filename)
            self.input_widget.file_entry.textChanged.connect(
                self.wf.set_in_filename
            )

            self.output_widget = OutputWidget(self)
            self.output_widget.file_entry.setText(self.wf.get_out_filename())
            self.wf.output_changed.connect(self.output_widget.set_filename)
            self.output_widget.file_entry.textChanged.connect(
                self.wf.set_out_filename
            )

            self.output_widget.compile_box.stateChanged.connect(
                self.wf.set_run_compile_stats
            )

            self.output_widget.sam2rma_box.stateChanged.connect(
                self.wf.set_run_sam2rma
            )
            self.output_widget.sam2rma_btn.clicked.connect(self.configure_sam2rma)

            self.list_view = FilterListView(self)
            self.list_view.setModel(self.wf.getModel())
            self.list_view.selectionModel().currentRowChanged.connect(
                self.update_settings_view)
            self.list_view.doubleClicked.connect(self.on_list_item_doubleclick)

            list_layout = QVBoxLayout()
            list_layout.setMargin(0)
            list_layout.addWidget(self.input_widget)
            list_layout.addWidget(self.list_view)
            list_layout.addWidget(self.output_widget)

            list_frame = QFrame(self)
            list_frame.setObjectName('ListFrame')
            list_frame.setFrameShape(QFrame.StyledPanel)
            list_frame.setLayout(list_layout)

            """ available filters dock """

            tree_view = QTreeView(self)
            tree_view.setDragEnabled(False)
            tree_view.setHeaderHidden(True)
            tree_view.setSelectionMode(QAbstractItemView.SingleSelection)
            tree_view.setModel(self.tree_model)
            tree_view.expandAll()
            tree_view.doubleClicked.connect(self.on_tree_item_doubleclick)

            tree_layout = QHBoxLayout()
            tree_layout.setMargin(0)
            tree_layout.addWidget(tree_view)

            tree_frame = QFrame(self)
            tree_frame.setFrameShape(QFrame.StyledPanel)
            tree_frame.setLayout(tree_layout)

            self.tree_dock = QDockWidget("Filters and Tools")
            self.tree_dock.setObjectName('TreeDock')
            self.tree_dock.setMinimumWidth(RIGHT_DOCK_MIN_WIDTH)
            self.tree_dock.setFeatures(QDockWidget.DockWidgetMovable
                                       | QDockWidget.DockWidgetFloatable
                                       | QDockWidget.DockWidgetClosable)
            self.tree_dock.setAllowedAreas(Qt.LeftDockWidgetArea
                                           | Qt.RightDockWidgetArea)
            self.tree_dock.setWidget(tree_frame)
            self.tree_dock.setVisible(True)

            """ settings dock """

            self.placeholder = QLabel(self)
            self.placeholder.setAlignment(Qt.AlignCenter)
            self.placeholder.setText("filter settings shown here")
            self.placeholder.setPixmap(
                QIcon.fromTheme('view-filter').pixmap(QSize(128, 128),
                                                      QIcon.Disabled,
                                                      QIcon.Off))

            self.current_editor = self.placeholder
            self.fset_layout = QVBoxLayout()
            self.fset_layout.setMargin(0)
            self.fset_layout.addWidget(self.current_editor)

            fset_frame = QFrame(self)
            fset_frame.setLayout(self.fset_layout)

            self.fset_dock = QDockWidget("Filter Settings")
            self.fset_dock.setObjectName('FilterSettingsDock')
            self.fset_dock.setMinimumWidth(RIGHT_DOCK_MIN_WIDTH)
            self.fset_dock.setMinimumHeight(RIGHT_DOCK_MIN_HEIGHT)
            self.fset_dock.setFeatures(
                QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable
                | QDockWidget.DockWidgetClosable
            )
            self.fset_dock.setAllowedAreas(
                Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea
                | Qt.RightDockWidgetArea
            )
            self.fset_dock.setWidget(fset_frame)
            self.fset_dock.setVisible(False)

            """ progress dock """

            self.output = QTextEdit(self)
            self.output.setReadOnly(True)
            self.output.setFontPointSize(8)
            self.output.setFontFamily('Monospace')

            progress_layout = QVBoxLayout()
            progress_layout.setMargin(0)
            progress_layout.addWidget(self.output)

            progress_frame = QFrame(self)
            progress_frame.setLayout(progress_layout)

            self.progress_dock = QDockWidget("Messages")
            self.progress_dock.setObjectName('ProgressDock')
            self.progress_dock.setMinimumWidth(BOTTOM_DOCK_MIN_HEIGHT)
            self.progress_dock.setFeatures(
                QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable
                | QDockWidget.DockWidgetClosable
            )
            self.progress_dock.setAllowedAreas(
                Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea
                | Qt.RightDockWidgetArea
            )

            self.progress_dock.setWidget(progress_frame)
            self.progress_dock.setVisible(False)

            """ main frame """

            self.addDockWidget(Qt.RightDockWidgetArea, self.tree_dock)
            self.addDockWidget(Qt.RightDockWidgetArea, self.fset_dock)
            self.addDockWidget(Qt.BottomDockWidgetArea, self.progress_dock)
            self.setCentralWidget(list_frame)

            """ actions """

            new_action_help = "Create a new workflow"
            new_action = QAction(QIcon.fromTheme('document-new'), "&New...", self)
            new_action.setShortcut(QKeySequence.New)
            new_action.setToolTip(new_action_help)
            new_action.setStatusTip(new_action_help)
            new_action.triggered.connect(self.new_file)

            open_action_help = "Open workflow from file"
            open_action = QAction(QIcon.fromTheme('document-open'), "&Open...",
                                  self)
            open_action.setShortcut(QKeySequence.Open)
            open_action.setToolTip(open_action_help)
            open_action.setStatusTip(open_action_help)
            open_action.triggered.connect(self.open_file)

            save_action_help = "Save workflow to file"
            save_action = QAction(QIcon.fromTheme('document-save'), "&Save", self)
            save_action.setShortcut(QKeySequence.Save)
            save_action.setToolTip(save_action_help)
            save_action.setStatusTip(save_action_help)
            save_action.triggered.connect(self.save_file)

            save_as_action = QAction(QIcon.fromTheme('document-save-as'),
                                     '&Save as...', self)
            save_as_action.setShortcut('Ctrl+Shift+S')
            save_as_action.setStatusTip('Save workflow to new file')
            save_as_action.triggered.connect(self.save_file_as)

            quit_action = QAction(QIcon.fromTheme('application-exit'),
                                  '&Quit', self)
            quit_action.setShortcut(QKeySequence.Quit)
            quit_action.setStatusTip('Exit application')
    #        quit_action.triggered.connect(qApp.quit)
            quit_action.triggered.connect(self.close)

            self.run_action = QAction(QIcon.fromTheme('system-run'), '&Run', self)
            self.run_action.setShortcut('Ctrl+R')
            self.run_action.setStatusTip('Run workflow')
            self.run_action.triggered.connect(self.run_workflow_command)

            self.stopAction = QAction(QIcon.fromTheme('process-stop'), '&Stop',
                                      self)
            self.stopAction.setShortcut('Ctrl+K')
            self.stopAction.setStatusTip('Stop workflow')
            self.stopAction.setEnabled(False)
            self.stopAction.triggered.connect(self.stop_command)

            exportAction = QAction(QIcon.fromTheme('utilities-terminal'),
                                   '&Export to Bash', self)
            exportAction.setShortcut('Ctrl+E')
            exportAction.setStatusTip('Export workflow to Bash script')
            exportAction.triggered.connect(self.configure_bash)

            moveUpAction = QAction(QIcon.fromTheme('go-up'), 'Move up', self)
            moveUpAction.setShortcut('Ctrl+Up')
            moveUpAction.setStatusTip('Move filter up')
            moveUpAction.triggered.connect(self.move_current_up)

            moveDownAction = QAction(QIcon.fromTheme('go-down'), 'Move down', self)
            moveDownAction.setShortcut('Ctrl+Down')
            moveDownAction.setStatusTip('Move filter down')
            moveDownAction.triggered.connect(self.move_current_down)

            removeAction = QAction(QIcon.fromTheme('edit-delete'), 'Remove', self)
            removeAction.setShortcut('Del')
            removeAction.setStatusTip('Remove filter')
            removeAction.triggered.connect(self.delete_item)

            """ debug actions BEGIN """
            validate_action = QAction(QIcon.fromTheme('dialog-information'),
                                      'Validate workflow',
                                      self)
            validate_action.setShortcut('F8')
            validate_action.setStatusTip('Validate workflow')
            validate_action.triggered.connect(self.validate_wf)

            print_cmd_action = QAction(QIcon.fromTheme('dialog-information'),
                                       'Print bash command',
                                       self)
            print_cmd_action.setShortcut('F9')
            print_cmd_action.setStatusTip('Print bash command')
            print_cmd_action.triggered.connect(self.print_command)

            print_xml_action = QAction(QIcon.fromTheme('dialog-information'),
                                       'Print XML',
                                       self)
            print_xml_action.setShortcut('F10')
            print_xml_action.setStatusTip('Print XML')
            print_xml_action.triggered.connect(self.print_xml)

            print_wf_action = QAction(QIcon.fromTheme('dialog-information'),
                                      'Print workflow',
                                      self)
            print_wf_action.setShortcut('F11')
            print_wf_action.setStatusTip('Print workflow')
            print_wf_action.triggered.connect(self.print_wf)
            """ debug actions END """

            self.showTreeAction = QAction('Available Filters', self)
            self.showTreeAction.setShortcut('F2')
            self.showTreeAction.setCheckable(True)
            self.showTreeAction.setChecked(self.tree_dock.isVisible())
            self.showTreeAction.setStatusTip('Toggle available filters dock')
    #        self.showTreeAction.triggered.connect(self.toggle_tree_dock)
            self.showTreeAction.toggled.connect(self.tree_dock.setVisible)
            self.tree_dock.visibilityChanged.connect(
                self.showTreeAction.setChecked)

            self.showProgressAction = QAction('Workflow Progress', self)
            self.showProgressAction.setShortcut('F3')
            self.showProgressAction.setCheckable(True)
            self.showProgressAction.setChecked(self.fset_dock.isVisible())
            self.showProgressAction.setStatusTip('Toggle progress dock')
    #        self.showProgressAction.triggered.connect(self.toggle_progress_dock)
            self.showProgressAction.toggled.connect(self.progress_dock.setVisible)
            self.progress_dock.visibilityChanged.connect(
                self.showProgressAction.setChecked)

            self.showSettingsAction = QAction('Filter Settings', self)
            self.showSettingsAction.setShortcut('F4')
            self.showSettingsAction.setCheckable(True)
            self.showSettingsAction.setChecked(self.fset_dock.isVisible())
            self.showSettingsAction.setStatusTip('Toggle filter settings dock')
    #        self.showSettingsAction.triggered.connect(self.toggle_settings_dock)
            self.showSettingsAction.toggled.connect(self.fset_dock.setVisible)
            self.fset_dock.visibilityChanged.connect(
                self.showSettingsAction.setChecked)

            about_action = QAction(QIcon.fromTheme('help-about'),
                                   'About %s' % TITLE, self)
            about_action.setShortcut('Ctrl+?')
            about_action.setStatusTip('Show information about this program.')
            about_action.triggered.connect(self.show_about)

            help_action = QAction(QIcon.fromTheme('help-contents'),
                                  '%s Help' % TITLE, self)
            help_action.setShortcut('F1')
            help_action.setStatusTip('Launch browser to view the help pages.')
            help_action.triggered.connect(self.show_help)

            """ menus """

            menubar = self.menuBar()

            self.file_menu = menubar.addMenu('&File')
            self.file_menu_actions = (new_action,
                                      None,
                                      open_action, save_action, save_as_action,
                                      None,
                                      quit_action)
            self.file_menu.aboutToShow.connect(self.populate_file_menu)

            run_menu = menubar.addMenu('&Run')
            run_menu.addAction(self.run_action)
            run_menu.addAction(self.stopAction)
            run_menu.addAction(exportAction)

            edit_menu = menubar.addMenu('&Edit')
            self.filter_menu = edit_menu.addMenu(QIcon.fromTheme('list-add'),
                                                 '&Add Filter...')
            self.populate_filter_menu()
            edit_menu.addSeparator()
            edit_menu.addAction(moveUpAction)
            edit_menu.addAction(removeAction)
            edit_menu.addAction(moveDownAction)

            view_menu = menubar.addMenu('&View')
            view_menu.addAction(self.showTreeAction)
            view_menu.addAction(self.showProgressAction)
            view_menu.addAction(self.showSettingsAction)

            if self.debug:
                debug_menu = menubar.addMenu('&Debug')
                debug_menu.addAction(validate_action)
                debug_menu.addAction(print_cmd_action)
                debug_menu.addAction(print_xml_action)
                debug_menu.addAction(print_wf_action)

            help_menu = menubar.addMenu('&Help')
            help_menu.addAction(help_action)
            help_menu.addAction(about_action)

            """ toolbars """

            wf_tools = self.addToolBar('Workflow Toolbar')
            wf_tools.setObjectName('WorkflowTools')
            wf_tools.addAction(new_action)
            wf_tools.addAction(open_action)
            wf_tools.addAction(save_action)
    #        wf_tools.addAction(save_as_action)
            wf_tools.addSeparator()
            wf_tools.addAction(exportAction)
            wf_tools.addAction(self.run_action)
            wf_tools.addAction(self.stopAction)

            f_tools = self.addToolBar('Filter Toolbar')
            f_tools.setObjectName('FilterTools')
            f_tools.addAction(moveUpAction)
            f_tools.addAction(removeAction)
            f_tools.addAction(moveDownAction)
            self.addToolBar(Qt.LeftToolBarArea, f_tools)

            """ status bar """

            self.steps_lbl = QLabel("%i steps" % self.wf.getModel().rowCount())
            self.steps_lbl.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken)

            self.statusBar().setSizeGripEnabled(False)
            self.statusBar().addPermanentWidget(self.steps_lbl)
            self.statusBar().showMessage('Ready', STATUS_DELAY)

        def addActions(self, target, actions):
            """Adds actions in list to target menu.

            Overrides Qt4 base method to also insert separators for 'None'
            entries.

            Parameters
            ----------
            target : QMenu
                Target menu.
            actions : list of QAction
                List of actions to be inserted into menu.
            """
            for action in actions:
                if action is None:
                    target.addSeparator()
                else:
                    target.addAction(action)

        """ view event handlers """

        def update_settings_view(self, current_idx, previous_idx):
            """Updates filter settings view upon change of focus in workflow.

            Parameters
            ----------
            current_idx : QModelIndex
                Model index of currently focussed filter item.
            previous_idx: QModelIndex
                Model index of previously focussed filter item.
            """
            current_item = self.wf.getModel().data(current_idx, role=Qt.UserRole)

            self.current_editor.setParent(None)
            if current_item is None:
                self.fset_layout.addWidget(self.placeholder)
                self.placeholder.show()
            else:
                self.placeholder.hide()
                self.current_editor = current_item.make_widget()
                self.current_editor.value_changed.connect(self.on_settings_change)
                self.fset_layout.addWidget(self.current_editor)

        def on_settings_change(self):
            """Handle changes in the settings dialog.

            Indicates unsaved settings and re-validates the workflow.
            """
            self.wf.validator.validate()
            self.wf.set_dirty()

        def closeEvent(self, event):
            """Confirm before exiting and save settings for next launch.

            Overrides parent QT class method to ask for confirmation and save
            window settings, unsaved files and preferences.

            Parameters
            ----------
            event : QEvent
                Close event triggered by closing the main window or kernel signals.
            """
            if self.ok_to_continue():
                settings = QSettings()
                # save window settings
                settings.setValue("LastFile", self.wf.get_filename())
                settings.setValue("RecentFiles", self.recent_files)
                settings.setValue("MainWindow/Size", self.size())
                settings.setValue("MainWindow/Position", self.pos())
                settings.setValue("MainWindow/State", self.saveState())
                # save SAM2RMA settings
                settings.setValue(
                    "SAM2RMA/TopPercent", self.rma_options.get_top_percent()
                )
                settings.setValue(
                    "SAM2RMA/MaxExpected", self.rma_options.get_max_expected()
                )
                settings.setValue(
                    "SAM2RMA/MinScore", self.rma_options.get_min_score()
                )
                settings.setValue(
                    "SAM2RMA/MinSupportPercent",
                    self.rma_options.get_min_support_percent()
                )
                settings.setValue(
                    "SAM2RMA/Sam2RmaPath", self.rma_options.get_sam2rma_path()
                )
            else:
                event.ignore()

        def new_file(self):
            """Handles pressing of the 'New' button."""
            if not self.ok_to_continue():
                return

            self.add_recent_file(self.wf.get_filename())
            self.wf.clear()

        def open_file(self):
            """Handles pressing of the 'Open' button."""
            if not self.ok_to_continue():
                return

            current_name = self.wf.get_filename()
            if current_name is None:
                filedir = expanduser("~")
            else:
                filedir = dirname(current_name)

            next_name = QFileDialog.getOpenFileName(
                self,
                "Choose workflow",
                filedir,
                "SamSifter XML files (%s);;All files (*)" % " ".join(FILE_FORMATS)
            )
            if next_name:
                self.load_file(next_name)

        def show_about(self):
            """Show an informative About dialog."""
            QMessageBox.about(
                self,
                "About %s" % TITLE,
                """<b>%s</b> v%s
                <p>Copyright &copy; 2015 Florian Aldehoff.
                <p>%s
                <p>%s
                <p>Python %s - Qt %s - PyQt %s on %s"""
                % (TITLE, __version__, DESC, COPYRIGHT, platform.python_version(),
                   QT_VERSION_STR, PYQT_VERSION_STR, platform.system())
            )

        def show_help(self):
            """Open browser to show the online help."""
            QDesktopServices.openUrl(QUrl(
                "http://www.biohazardous.de/samsifter/docs/help.html"
            ))

        def print_command(self):
            """Print current workflow command to console (debugging)."""
            self.output.clear()
            self.output.append(
                self.wf.commandline(hyphenated=False, multiline=True)
            )
            log.info(self.wf)

        def print_xml(self):
            """Print current workflow XML to console (debugging)."""
            xml = self.wf.to_xml_string()
            self.output.clear()
            self.output.append(xml)
            log.info(xml)

        def print_wf(self):
            """Print current workflow to console (debugging)."""
            self.output.clear()
            self.output.append(repr(self.wf))
            log.info(repr(self.wf))

        def validate_wf(self):
            """Validate current workflow (debugging)."""
            self.output.clear()
            validator = WorkflowValidator(self.wf)
            validator.validate()

        def run_workflow_command(self):
            """Runs the workflow."""
            cl = self.wf.commandline(hyphenated=False, multiline=False)
            self.progress_dock.setVisible(True)
            self.output.clear()

            self.process = QProcess()

            # QProcess emits `readyRead` when there is data to be read
            self.process.readyReadStandardError.connect(self.on_stderr)
            self.process.readyReadStandardOutput.connect(self.on_stdout)
            self.process.finished.connect(self.on_workflow_finished)

            # prevent accidentally running multiple processes
            self.process.started.connect(lambda: self.run_action.setEnabled(False))
            self.process.finished.connect(lambda: self.run_action.setEnabled(True))
            self.process.started.connect(lambda: self.stopAction.setEnabled(True))
            self.process.finished.connect(
                lambda: self.stopAction.setEnabled(False))

            workdir = dirname(self.wf.get_out_filename())
            self.process.setWorkingDirectory(workdir)
            log.info("setting working directory: %s" % workdir)

            # environment variable is used to set prefix of temporary stats files
            prefix = basename(self.wf.get_in_filename())
            prefix = 'reads_per_taxon'
            env = QProcessEnvironment.systemEnvironment()
            env.insert('filename', prefix)
            self.process.setProcessEnvironment(env)
            log.info("setting filename variable: %s" % prefix)

            self.process.start("bash", ['-e', '-c', cl])
            self.pid = self.process.pid()
            self.output.setTextColor(QColor("black"))
            self.output.append("PROCESS %i STARTED" % self.pid)
            log.info("started PID %i" % self.pid)

        def run_compile_stats_command(self):
            """Runs command to compile temp statistics files into one CSV file."""
            self.process = QProcess(self)

            # QProcess emits `readyRead` when there is data to be read
            self.process.readyReadStandardError.connect(self.on_stderr)
            self.process.readyReadStandardOutput.connect(self.on_stdout_stats)
            self.process.finished.connect(self.on_compilation_finished)

            # prevent accidentally running multiple processes
            self.process.started.connect(lambda: self.run_action.setEnabled(False))
            self.process.finished.connect(lambda: self.run_action.setEnabled(True))
            self.process.started.connect(lambda: self.stopAction.setEnabled(True))
            self.process.finished.connect(
                lambda: self.stopAction.setEnabled(False))

            # all statistics should be located with output file
            workdir = dirname(self.wf.get_out_filename())
            self.process.setWorkingDirectory(workdir)
            log.info("setting working directory: %s" % workdir)

            self.process.start('compile_stats', ['--remove', '--verbose'])
            self.pid = self.process.pid()

        def run_sam2rma_command(self):
            """Runs command to create RMA file from (zipped) SAM file."""
            self.process = QProcess(self)

            self.process.readyReadStandardError.connect(self.on_stderr)
            self.process.readyReadStandardOutput.connect(self.on_stdout)
            self.process.finished.connect(self.on_postprocessing_finished)

            # prevent accidentally running multiple processes
            self.process.started.connect(lambda: self.run_action.setEnabled(False))
            self.process.finished.connect(lambda: self.run_action.setEnabled(True))
            self.process.started.connect(lambda: self.stopAction.setEnabled(True))
            self.process.finished.connect(
                lambda: self.stopAction.setEnabled(False))

            # RMA should be located with output file
            workdir = dirname(self.wf.get_out_filename())
            self.process.setWorkingDirectory(workdir)
            log.info("setting working directory: %s" % workdir)
            log.info(self.rma_options.get_sam2rma_path())

            self.process.start(
                self.rma_options.get_sam2rma_path(),
                [
                    '--minScore %.1f' % self.rma_options.get_min_score(),
                    '--maxExpected %.2f' % self.rma_options.get_max_expected(),
                    '--topPercent %.1f' % self.rma_options.get_top_percent(),
                    '--minSupportPercent %.3f'
                    % self.rma_options.get_min_support_percent(),
                    '--in %s' % self.wf.get_out_filename(),
                    '--out %s' % workdir
                ]
            )
            self.pid = self.process.pid()

        def stop_command(self):
            """Terminates current process and all child processes.

            Default termination of QProcess won't work here because bash spawns
            independent children that will continue to run.
            TODO: Use POSIX command 'kill $(ps -o pid= --ppid $PID)' where pkill
            is not available
            """
            killer = QProcess(self)
            killer.start('bash', ['-c', 'pkill -TERM -P ' + str(self.pid)])
    #        killer.readyReadStandardError.connect(self.on_stderr)
    #        killer.readyReadStandardOutput.connect(self.on_stdout)
            self.output.setTextColor(QColor("darkred"))
            self.output.append("PROCESS %i TERMINATING" % (self.pid, ))
            log.info("terminating PID %i" % self.pid)
            self.pid = -1

        def add_filter(self, item):
            """Insert cloned copy of filter item into the list model.

            Note
            ----
            C++ QWidgets do NOT support pythonic copy/deepcopy/pickle! Thus they
            can not be easily encoded to/decoded from MIME for standard Qt4
            drag&drop support and have to be cloned instead.
            """
            if isinstance(item, FilterItem):
                current_idx = self.list_view.currentIndex()
                if current_idx.row() == -1:
                    # insert at end of list
                    target_row = self.wf.getModel().rowCount()
                else:
                    # insert after current item
                    target_row = current_idx.row() + 1
                self.wf.getModel().insertItem(item.clone(), target_row)
                # put focus on new item
                new_idx = self.wf.getModel().index(target_row)
                self.list_view.setCurrentIndex(new_idx)
                self.update_status("Added filter step [%s] to workflow"
                                   % item.text())

            else:
                log.error("not a FilterItem but a %s" % type(item))

        def on_tree_item_doubleclick(self):
            """Clone doubleclicked item in tree view into the list."""
            cur_tree_idx = self.sender().currentIndex()
            item = self.sender().model().itemFromIndex(cur_tree_idx)
            self.add_filter(item)

        def on_list_item_doubleclick(self):
            """Toggle settings dock to inspect settings of doubleclicked item."""
            if not self.fset_dock.isVisible():
                self.fset_dock.setVisible(True)

        def move_current(self, positions):
            """Move item in model.

            Parameters
            ----------
            positions : int
                number of positions to move currently selected item (negative = up,
                positive = down)

            Returns
            -------
            bool
                Success of moving operation.
            """
            current_idx = self.list_view.currentIndex()
            if not current_idx.isValid():
                return False

            model = self.wf.getModel()
            row = current_idx.row()
            if row + positions >= 0 and row + positions < model.rowCount():
                item = model.takeItem(row)
                model.insertItem(item, row + positions)
                mdlidx = model.index(row + positions)
                self.list_view.setCurrentIndex(mdlidx)
                return True
            else:
                return False

        def move_current_up(self):
            """Move currently selected item up by one position.

            Returns
            -------
            bool
                Success of moving operation.
            """
            return self.move_current(-1)

        def move_current_down(self):
            """Move currently selected item down by one position.

            Returns
            -------
            bool
                Success of moving operation.
            """
            return self.move_current(1)

        def delete_item(self):
            """Delete currently selected item from model.

            Returns
            -------
            bool
                Success of delete operation.
            """
            current_idx = self.list_view.currentIndex()
            if not current_idx.isValid():
                return False

            row = current_idx.row()
            return self.wf.getModel().removeRow(row)

        """ process event handlers """

        def on_stdout(self):
            """Handles STDOUT output from subprocess."""
            self.output.setTextColor(Qt.black)
            self.output.append(str(self.process.readAllStandardOutput(),
                                   encoding='utf-8'))

        def on_stdout_stats(self):
            """Handles STDOUT output from statistics file compilation."""
            with open('%s.csv' % self.wf.get_out_filename(), 'a') as stats:
                print(
                    str(self.process.readAllStandardOutput(), encoding='utf-8'),
                    file=stats
                )

        def on_stderr(self):
            """Handles STDERR output from subprocess."""
            # convert QT4 binary array back into string
            message = str(self.process.readAllStandardError(), encoding='utf-8')
            message = message.rstrip()

            if message.startswith("INFO:"):
                self.output.setTextColor(QColor('darkgreen'))
            elif message.startswith("WARNING:"):
                self.output.setTextColor(QColor('orange'))
            elif message.startswith("ERROR:"):
                self.output.setTextColor(QColor('darkred'))
            elif "error:" in message:
                self.output.setTextColor(QColor('darkred'))
                self.stop_command()
            else:
                self.output.setTextColor(Qt.black)

            self.output.append(message)

        def on_workflow_finished(self):
            """Starts post-processing after end of workflow process."""
            self.output.setTextColor(QColor('black'))
            self.output.append("PROCESS %i FINISHED" % self.pid)
            log.info("finished PID %i" % self.pid)
            self.process = None
            self.pid = -1

            if self.wf.get_run_compile_stats() or self.wf.get_run_sam2rma():
                self.output.append("POST-PROCESSING")
            # always compile any temporary statistics files
            self.run_compile_stats_command()

        def on_compilation_finished(self):
            """Handles signal when statistics compilation has finished."""
            self.pid = -1
            if not self.wf.get_run_sam2rma():
                self.on_postprocessing_finished()
            else:
                # generate RMA file
                self.run_sam2rma_command()

        def on_postprocessing_finished(self):
            """Handles signal when post-processing has finished."""
            self.output.setTextColor(Qt.black)
            self.output.append("DONE")
            # reset command buttons
            self.stopAction.setEnabled(False)
            self.run_action.setEnabled(True)
            self.process = None
            self.pid = -1

        """ model event handlers """

        def update_status(self, message=None):
            """Updates window title and status bar.

            Parameters
            ----------
            message : str, optional
                Message to be displayed in status bar.
            """
            if message is not None:
                log.info(message)
                self.statusBar().showMessage(message, STATUS_DELAY)
            self.steps_lbl.setText("%i steps" % (self.wf.getModel().rowCount()))
            filename = self.wf.get_filename()
            if filename is None:
                self.setWindowTitle("untitled[*] - %s" % TITLE)
            else:
                self.setWindowTitle("%s[*] - %s" % (basename(filename), TITLE))
            self.setWindowModified(self.wf.is_dirty())

        def on_validity_change(self, message=None):
            """Show hints to resolve errors and prevent running invalid workflows.

            Parameters
            ----------
            message : str, optional
                Message to be displayed in message dock.
            """
            if message is not None:
                if message != "":
                    log.info(message)
                self.output.clear()
                self.output.append(message)

            # prevent execution of invalid workflow
            self.run_action.setEnabled(self.wf.is_valid() and self.pid == -1)

            # set highlighting and tooltips of input/output widgets
            self.input_widget.highlight(not self.wf.infile_is_valid())
            self.input_widget.setToolTip(self.wf.validator.get_input_errors())
            self.output_widget.highlight(not self.wf.outfile_is_valid())
            self.output_widget.setToolTip(self.wf.validator.get_output_errors())

        """ file operations """

        def load_initial_file(self):
            """Restore workflow from previous session, greet user on first use."""
            settings = QSettings()
            fname = settings.value("LastFile")
            if fname and QFile.exists(fname):
                self.load_file(fname)
            else:
                self.update_status("Welcome to SamSifter!")
                self.progress_dock.setVisible(True)
                self.output.append(GREETING)

        def load_file(self, filename=None):
            """Open workflow data from file.

            Retrieves filename from user data of QAction when called from list of
            recently used files (signal emits a boolean).

            Parameters
            ----------
            filename : str, optional
                Path of file to be opened.
            """
            if filename is None or filename is False:
                action = self.sender()
                if isinstance(action, QAction):
                    filename = action.data()
                    if not self.ok_to_continue():
                        return
                else:
                    return

            if filename:
                self.wf.set_filename(None)

                # actual deserialization is handled by workflow container
                (loaded, message) = self.wf.load(filename)
                if loaded:
                    self.add_recent_file(filename)
    #            self.update_status(message)

        def save_file(self):
            """Save workflow under current filename."""
            # save only if there are changes (commented out to always save)
    #        if not self.wf.is_dirty():
    #            return

            filename = self.wf.get_filename()
            if filename is None:
                self.save_file_as()
            else:
                # actual serialization is handled by workflow container
                (saved, message) = self.wf.save()
                self.update_status(message)

        def save_file_as(self):
            """Saves workflow under a new filename."""
            current_name = self.wf.get_filename()
            if current_name is not None:
                filedir = dirname(current_name)
            else:
                filedir = expanduser("~")

            new_name = QFileDialog.getSaveFileName(self, "Save workflow",
                                                   filedir,
                                                   "SamSifter XML files (%s);;"
                                                   "All files (*)"
                                                   % (" ".join(FILE_FORMATS), ))
            if new_name:
                # append missing extension
                if "." not in new_name:
                    new_name += FILE_FORMATS[0].lstrip('*')
                self.add_recent_file(new_name)
                self.wf.set_filename(new_name)
                self.save_file()

        def add_recent_file(self, filename):
            """Adds a filename to the list of recently used files.

            Parameters
            ----------
            filename : str
                Path of file to be added to list.
            """
            if filename is None:
                return
            if filename not in self.recent_files:
                self.recent_files.insert(0, filename)
                while len(self.recent_files) > RECENT_FILES_MAX_LENGTH:
                    self.recent_files.pop()

        def configure_bash(self):
            """Opens modal dialog to set options for exported bash script."""
            dialog = BashOptionsDialog()
            options = dialog.get_options()
            if options:
                self.export_bash(options)

        def configure_sam2rma(self):
            """Opens modal dialog to set options for sam2rma conversion."""
            dialog = RmaOptionsDialog(self.rma_options)
            options = dialog.get_options()
            if options:
                self.rma_options = options

        def export_bash(self, options):
            """Opens 'Save as...' dialogue and exports workflow to bash script."""
            dialog = QFileDialog()
            dialog.setDefaultSuffix('Bash script (*.sh *.SH)')
            fname = dialog.getSaveFileName(self, 'Export as',
                                           expanduser("~"),
                                           "Bash script (*.sh *.SH);;All files (*)"
                                           )
            if fname:
                # force script extension also on Unity/Gnome systems
                if not fname.endswith(".sh"):
                    fname += ".sh"
                self.wf.to_bash(fname, options, self.rma_options)

        def ok_to_continue(self):
            """Asks user for confirmation to save any unsaved changes.

            Returns
            -------
            bool
                True if ok to continue, False if unsaved changes still exist.
            """
            if self.wf.is_dirty():
                reply = QMessageBox.question(
                    self, "Unsaved Changes",
                    ("There are unsaved changes to the current workflow that will "
                     "be lost. Would you like to save them?"),
                    QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
                )
                if reply == QMessageBox.Cancel:
                    return False
                elif reply == QMessageBox.Yes:
                    self.save_file()
            return True

    def sigint_handler(*args):
        """Handler for the SIGINT signal (Ctrl+C)."""
        sys.stderr.write('\r')
        if QMessageBox.question(
            None, '', "Are you sure you want to quit?",
            QMessageBox.Yes | QMessageBox.No, QMessageBox.No
        ) == QMessageBox.Yes:
            QApplication.quit()

    def main():
        """Executable for SamSifter GUI.

        See ``--help`` for details on expected arguments. Takes no input from STDIN
        but logs errors and messages to STDERR.
        """
        # parse arguments
        parser = argparse.ArgumentParser(description=DESC)
        parser.add_argument('-v', '--verbose',
                            required=False,
                            action='store_true',
                            help='print additional information to stderr')
        parser.add_argument('-d', '--debug',
                            required=False,
                            action='store_true',
                            help='show debug options in menu')
        (args, remainArgs) = parser.parse_known_args()

        # configure logging
        if args.verbose:
            log.basicConfig(format="%(levelname)s: %(message)s", level=log.DEBUG)
        else:
            log.basicConfig(format="%(levelname)s: %(message)s")

        signal.signal(signal.SIGINT, sigint_handler)

        app = QApplication(remainArgs)

        timer = QTimer()
        timer.start(500)                     # You may change this if you wish.
        timer.timeout.connect(lambda: None)  # Let the interpreter run each 500 ms.

        app.setOrganizationName("Biohazardous")
        app.setOrganizationDomain("biohazardous.de")
        app.setApplicationVersion(__version__)
        app.setApplicationName(TITLE)
        app.setWindowIcon(QIcon(":/icon.png"))
        mw = MainWindow(args.verbose, args.debug)
        mw.show()
        sys.exit(app.exec_())

    if __name__ == '__main__':
        main()

### Navigation

- index
- modules |
- SamSifter 0.5.4 documentation »
- Module code »