Beispiel #1
0
 def at_exit(workers_pool: QThreadPool, logging_worker: LoggingWorker,
             name: str):
     """
     The instance deconstruction handler is meant to be used with weakref.finalize() conforming with the requirement
     to have no reference to the target object (so it doesn't contain any instance reference and also is decorated as
     'staticmethod')
     """
     # Wait forever for all the jobs to complete. Currently, we cannot abort them gracefully
     workers_pool.waitForDone(msecs=-1)
     logging_worker.stopped.set(
     )  # post the event in the logging worker to inform it...
     logging_worker.thread.wait()  # ...and wait for it to exit, too
     module_logger.debug(f"destroyed {name}")
Beispiel #2
0
class QThreadData(QThread):
    def __init__(self, target: list):
        QThread.__init__(self)
        self.target = target
        self.pool = QThreadPool()

    def __del__(self):
        self.wait()

    def run(self):
        for item in self.target:
            logger.debug('start ' + item.__name__)
            worker = Worker(item)
            self.pool.start(worker)
        self.pool.waitForDone()
        logger.debug('thread finished')
Beispiel #3
0
class View(QWidget):
    previous = Signal()
    next = Signal()

    def __init__(self, window):
        super(View, self).__init__(window)

        self.setFocusPolicy(Qt.StrongFocus)
        self.shiftKey = False
        self.ctrlKey = False
        self.lastMousePos = QPoint()
        self.lastTabletPos = QPoint()
        self.mode = 'add'
        self.maskOnly = False

        self.refresh = QTimer(self)
        self.refresh.setSingleShot(True)
        self.refresh.timeout.connect(self.repaint)

        self.addCursor = makeCursor('images/cursor-add.png',
                                    QColor.fromRgbF(0.5, 0.5, 1.0))
        self.delCursor = makeCursor('images/cursor-del.png',
                                    QColor.fromRgbF(1.0, 0.5, 0.5))
        self.setCursor(self.addCursor)

        self.imagefile = None
        self.maskfile = None
        self.image = QImage()
        self.mask = QImage(self.image.size(), QImage.Format_RGB32)
        self.mask.fill(Qt.black)
        self.changed = False
        self.update()

        self.path = list()

        self.load_threads = QThreadPool()
        self.load_threads.setMaxThreadCount(4)

    def load(self, filename):
        self.load_threads.start(LoadTask(self, filename))

    def save(self):
        if self.maskfile and self.changed:
            self.load_threads.waitForDone()
        if self.maskfile and self.changed:
            bitmap = self.mask.createMaskFromColor(
                QColor.fromRgbF(1.0, 0.0, 1.0).rgb())
            bitmap.save(str(self.maskfile), "PNG")
            self.changed = False

    def update(self):
        widgetRatio = self.width() / self.height()
        aspectRatio = self.image.width() / max(1, self.image.height())
        if aspectRatio >= widgetRatio:
            width = self.width()
            height = self.width() / aspectRatio
        else:
            width = self.height() * aspectRatio
            height = self.height()
        self.rc = QRectF((self.width() - width) / 2.0,
                         (self.height() - height) / 2.0, width, height)
        self.repaint()

    def resizeEvent(self, event):
        self.update()

    def paintEvent(self, event):
        p = QPainter(self.mask)
        for (mode, p1, p2, weight) in self.path:
            if mode == 'add':
                p.setPen(
                    QPen(QColor.fromRgbF(1.0, 0.0, 1.0), (weight * 10.0)**2,
                         Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
            else:
                p.setPen(
                    QPen(QColor.fromRgbF(0.0, 0.0, 0.0), (weight * 10.0)**2,
                         Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
            p.drawLine(realCoords(p1, self.mask.rect()),
                       realCoords(p2, self.mask.rect()))
            self.changed = True
        self.path = list()
        p.end()

        p = QPainter(self)
        p.setCompositionMode(QPainter.CompositionMode_SourceOver)
        if not self.maskOnly:
            p.drawImage(self.rc, self.image)
            p.setCompositionMode(QPainter.CompositionMode_Plus)
        p.drawImage(self.rc, self.mask)
        p.end()

    def closeEvent(self, event):
        self.refresh.stop()
        event.accept()

    def enterEvent(self, event):
        self.setFocus(Qt.OtherFocusReason)

    def keyPressEvent(self, event):
        k = event.key()
        if k == Qt.Key_Shift:
            self.shiftKey = True
        if k == Qt.Key_Control:
            self.ctrlKey = True

        if k == Qt.Key_Space:
            self.maskOnly = not self.maskOnly
            self.repaint()

    def keyReleaseEvent(self, event):
        k = event.key()
        mod = event.modifiers()
        if k == Qt.Key_Shift:
            self.shiftKey = False
        if k == Qt.Key_Control:
            self.ctrlKey = False

    def mousePressEvent(self, event):
        x = event.x()
        y = event.y()
        self.lastMousePos = event.pos()

        if event.button() == Qt.ExtraButton1:
            if self.mode == 'add':
                self.mode = 'del'
                self.setCursor(self.delCursor)
            else:
                self.mode = 'add'
                self.setCursor(self.addCursor)
        elif event.button() == Qt.ExtraButton2:
            self.maskOnly = not self.maskOnly
            self.repaint()
        elif event.button() == Qt.ExtraButton3:
            self.previous.emit()
        elif event.button() == Qt.ExtraButton4:
            self.next.emit()

    def mouseMoveEvent(self, event):
        x = event.x()
        y = event.y()
        dx = x - self.lastMousePos.x()
        dy = y - self.lastMousePos.y()
        self.lastMousePos = event.pos()

        # if event.buttons() & Qt.LeftButton:
        # elif event.buttons() & Qt.MiddleButton:
        # elif event.buttons() & Qt.RightButton:

    def wheelEvent(self, event):
        dx = event.angleDelta().x() / 8
        dy = event.angleDelta().y() / 8
        # self.cameraZoom.emit(dy / 15)

    def tabletEvent(self, event):
        if event.device() == QTabletEvent.Stylus and event.pointerType(
        ) == QTabletEvent.Pen:
            if event.type() == QEvent.TabletPress:
                self.tabletPressEvent(event)
            elif event.type() == QEvent.TabletRelease:
                self.tabletReleaseEvent(event)
            elif event.type() == QEvent.TabletMove:
                if event.pressure() > 0.0:
                    self.tabletMoveEvent(event)
            else:
                print('tabletEvent', event.device(), event.type(),
                      event.pointerType())
        else:
            print('tabletEvent', event.device(), event.type(),
                  event.pointerType())

    def tabletPressEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            self.lastTabletPos = normalizeCoords(event.posF(), self.rc)
        if event.buttons() & Qt.MiddleButton:
            if self.mode == 'add':
                self.mode = 'del'
                self.setCursor(self.delCursor)
            else:
                self.mode = 'add'
                self.setCursor(self.addCursor)
        if event.buttons() & Qt.RightButton:
            self.next.emit()

    def tabletReleaseEvent(self, event):
        self.lastTabletPos = normalizeCoords(event.posF(), self.rc)

    def tabletMoveEvent(self, event):
        self.path.append((self.mode, self.lastTabletPos,
                          normalizeCoords(event.posF(),
                                          self.rc), event.pressure()))
        self.lastTabletPos = normalizeCoords(event.posF(), self.rc)
        if not self.refresh.isActive():
            self.refresh.start(50)
Beispiel #4
0
class ViolinGUI(QMainWindow):
    """Main Window Widget for ViolinGUI."""
    style = {
        'title': 'QLabel {font-size: 18pt; font-weight: 600}',
        'header': 'QLabel {font-size: 12pt; font-weight: 520}',
        'label': 'QLabel {font-size: 10pt}',
        'button': 'QPushButton {font-size: 10pt}',
        'run button': 'QPushButton {font-size: 18pt; font-weight: 600}',
        'line edit': 'QLineEdit {font-size: 10pt}',
        'checkbox': 'QCheckBox {font-size: 10pt}',
        'drop down': 'QComboBox {font-size: 10pt}'
    }

    def __init__(self) -> None:
        """ViolinGUI Constructor. Defines all aspects of the GUI."""
        # ## Setup section
        # Inherits from QMainWindow
        super().__init__()
        self.rootdir = get_project_root()
        # QMainWindow basic properties
        self.setWindowTitle("SCOUTS - Violins")
        self.setWindowIcon(
            QIcon(
                os.path.abspath(os.path.join(self.rootdir, 'src',
                                             'scouts.ico'))))
        # Creates QWidget as QMainWindow's central widget
        self.page = QWidget(self)
        self.setCentralWidget(self.page)
        # Miscellaneous initialization values
        self.threadpool = QThreadPool()  # Threadpool for workers
        self.population_df = None  # DataFrame of whole population (raw data)
        self.summary_df = None  # DataFrame indicating which SCOUTS output corresponds to which rule
        self.summary_path = None  # path to all DataFrames generated by SCOUTS

        self.main_layout = QVBoxLayout(self.page)

        # Title section
        # Title
        self.title = QLabel(self.page)
        self.title.setText('SCOUTS - Violins')
        self.title.setStyleSheet(self.style['title'])
        self.title.adjustSize()
        self.main_layout.addWidget(self.title)

        # ## Input section
        # Input header
        self.input_header = QLabel(self.page)
        self.input_header.setText('Load data')
        self.input_header.setStyleSheet(self.style['header'])
        self.input_header.adjustSize()
        self.main_layout.addWidget(self.input_header)
        # Input/Output frame
        self.input_frame = QFrame(self.page)
        self.input_frame.setFrameShape(QFrame.StyledPanel)
        self.input_frame.setLayout(QFormLayout())
        self.main_layout.addWidget(self.input_frame)
        # Raw data button
        self.input_button = QPushButton(self.page)
        self.input_button.setStyleSheet(self.style['button'])
        self.set_icon(self.input_button, 'x-office-spreadsheet')
        self.input_button.setObjectName('file')
        self.input_button.setText(' Load raw data file')
        self.input_button.setToolTip(
            'Load raw data file (the file given to SCOUTS as the input file)')
        self.input_button.clicked.connect(self.get_path)
        # SCOUTS results button
        self.output_button = QPushButton(self.page)
        self.output_button.setStyleSheet(self.style['button'])
        self.set_icon(self.output_button, 'folder')
        self.output_button.setObjectName('folder')
        self.output_button.setText(' Load SCOUTS results')
        self.output_button.setToolTip(
            'Load data from SCOUTS analysis '
            '(the folder given to SCOUTS as the output folder)')
        self.output_button.clicked.connect(self.get_path)
        # Add widgets above to input frame Layout
        self.input_frame.layout().addRow(self.input_button)
        self.input_frame.layout().addRow(self.output_button)

        # ## Samples section
        # Samples header
        self.samples_header = QLabel(self.page)
        self.samples_header.setText('Select sample names')
        self.samples_header.setStyleSheet(self.style['header'])
        self.samples_header.adjustSize()
        self.main_layout.addWidget(self.samples_header)
        # Samples frame
        self.samples_frame = QFrame(self.page)
        self.samples_frame.setFrameShape(QFrame.StyledPanel)
        self.samples_frame.setLayout(QFormLayout())
        self.main_layout.addWidget(self.samples_frame)
        # Samples label
        self.samples_label = QLabel(self.page)
        self.samples_label.setText(
            'Write sample names delimited by semicolons below.\nEx: Control;Treat_01;Pac-03'
        )
        self.samples_label.setStyleSheet(self.style['label'])
        # Sample names line edit
        self.sample_names = QLineEdit(self.page)
        self.sample_names.setStyleSheet(self.style['line edit'])
        # Add widgets above to samples frame Layout
        self.samples_frame.layout().addRow(self.samples_label)
        self.samples_frame.layout().addRow(self.sample_names)

        # ## Analysis section
        # Analysis header
        self.analysis_header = QLabel(self.page)
        self.analysis_header.setText('Plot parameters')
        self.analysis_header.setStyleSheet(self.style['header'])
        self.analysis_header.adjustSize()
        self.main_layout.addWidget(self.analysis_header)
        # Analysis frame
        self.analysis_frame = QFrame(self.page)
        self.analysis_frame.setFrameShape(QFrame.StyledPanel)
        self.analysis_frame.setLayout(QFormLayout())
        self.main_layout.addWidget(self.analysis_frame)
        # Analysis labels
        self.analysis_label_01 = QLabel(self.page)
        self.analysis_label_01.setText('Compare')
        self.analysis_label_01.setStyleSheet(self.style['label'])
        self.analysis_label_02 = QLabel(self.page)
        self.analysis_label_02.setText('with')
        self.analysis_label_02.setStyleSheet(self.style['label'])
        self.analysis_label_03 = QLabel(self.page)
        self.analysis_label_03.setText('for marker')
        self.analysis_label_03.setStyleSheet(self.style['label'])
        self.analysis_label_04 = QLabel(self.page)
        self.analysis_label_04.setText('Outlier type')
        self.analysis_label_04.setStyleSheet(self.style['label'])
        # Analysis drop-down boxes
        self.drop_down_01 = QComboBox(self.page)
        self.drop_down_01.addItems([
            'whole population', 'non-outliers', 'top outliers',
            'bottom outliers', 'none'
        ])
        self.drop_down_01.setStyleSheet(self.style['drop down'])
        self.drop_down_01.setCurrentIndex(2)
        self.drop_down_02 = QComboBox(self.page)
        self.drop_down_02.addItems([
            'whole population', 'non-outliers', 'top outliers',
            'bottom outliers', 'none'
        ])
        self.drop_down_02.setStyleSheet(self.style['drop down'])
        self.drop_down_02.setCurrentIndex(0)
        self.drop_down_03 = QComboBox(self.page)
        self.drop_down_03.setStyleSheet(self.style['drop down'])
        self.drop_down_04 = QComboBox(self.page)
        self.drop_down_04.addItems(['OutS', 'OutR'])
        self.drop_down_04.setStyleSheet(self.style['drop down'])
        # Add widgets above to samples frame Layout
        self.analysis_frame.layout().addRow(self.analysis_label_01,
                                            self.drop_down_01)
        self.analysis_frame.layout().addRow(self.analysis_label_02,
                                            self.drop_down_02)
        self.analysis_frame.layout().addRow(self.analysis_label_03,
                                            self.drop_down_03)
        self.analysis_frame.layout().addRow(self.analysis_label_04,
                                            self.drop_down_04)

        self.legend_checkbox = QCheckBox(self.page)
        self.legend_checkbox.setText('Add legend to the plot')
        self.legend_checkbox.setStyleSheet(self.style['checkbox'])
        self.main_layout.addWidget(self.legend_checkbox)

        # Plot button (stand-alone)
        self.plot_button = QPushButton(self.page)
        self.set_icon(self.plot_button, 'system-run')
        self.plot_button.setText(' Plot')
        self.plot_button.setToolTip(
            'Plot data after loading the input data and selecting parameters')
        self.plot_button.setStyleSheet(self.style['run button'])
        self.plot_button.setEnabled(False)
        self.plot_button.clicked.connect(self.run_plot)
        self.main_layout.addWidget(self.plot_button)

        # ## Secondary Window
        # This is used to plot the violins only
        self.secondary_window = QMainWindow(self)
        self.secondary_window.resize(720, 720)
        self.dynamic_canvas = DynamicCanvas(self.secondary_window,
                                            width=6,
                                            height=6,
                                            dpi=120)
        self.secondary_window.setCentralWidget(self.dynamic_canvas)
        self.secondary_window.addToolBar(
            NavBar(self.dynamic_canvas, self.secondary_window))

    def set_icon(self, widget: QWidget, icon: str) -> None:
        """Associates an icon to a widget."""
        i = QIcon()
        i.addPixmap(
            QPixmap(
                os.path.abspath(
                    os.path.join(self.rootdir, 'src', 'default_icons',
                                 f'{icon}.svg'))))
        widget.setIcon(QIcon.fromTheme(icon, i))

    def get_path(self) -> None:
        """Opens a dialog box and loads the corresponding data into memory, depending on the caller widget."""
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        query = None
        func = None
        if self.sender().objectName() == 'file':
            query, _ = QFileDialog.getOpenFileName(self,
                                                   "Select file",
                                                   "",
                                                   "All Files (*)",
                                                   options=options)
            func = self.load_scouts_input_data
        elif self.sender().objectName() == 'folder':
            query = QFileDialog.getExistingDirectory(self,
                                                     "Select Directory",
                                                     options=options)
            func = self.load_scouts_results
        if query:
            self.load_data(query, func)

    def load_data(self, query: str, func: Callable) -> None:
        """Loads input data into memory, while displaying a loading message as a separate worker."""
        worker = Worker(func=func, query=query)
        message = self.loading_message()
        worker.signals.started.connect(message.show)
        worker.signals.started.connect(self.page.setDisabled)
        worker.signals.error.connect(self.generic_error_message)
        worker.signals.error.connect(message.destroy)
        worker.signals.failed.connect(self.plot_button.setDisabled)
        worker.signals.success.connect(message.destroy)
        worker.signals.success.connect(self.enable_plot)
        worker.signals.finished.connect(self.page.setEnabled)
        self.threadpool.start(worker)

    def loading_message(self) -> QDialog:
        """Returns the message box to be displayed while the user waits for the input data to load."""
        message = QDialog(self)
        message.setWindowTitle('Loading')
        message.resize(300, 50)
        label = QLabel('loading DataFrame into memory...', message)
        label.setStyleSheet(self.style['label'])
        label.adjustSize()
        label.setAlignment(Qt.AlignCenter)
        label.move(int((message.width() - label.width()) / 2),
                   int((message.height() - label.height()) / 2))
        return message

    def load_scouts_input_data(self, query: str) -> None:
        """Loads data for whole population prior to SCOUTS into memory (used for plotting the whole population)."""
        try:
            self.population_df = pd.read_excel(query, index_col=0)
        except XLRDError:
            self.population_df = pd.read_csv(query, index_col=0)
        self.drop_down_03.clear()
        self.drop_down_03.addItems(list(self.population_df.columns))
        self.drop_down_03.setCurrentIndex(0)

    def load_scouts_results(self, query: str) -> None:
        """Loads the SCOUTS summary file into memory, in order to dynamically locate SCOUTS output files later when
        the user chooses which data to plot."""
        self.summary_df = pd.read_excel(os.path.join(query, 'summary.xlsx'),
                                        index_col=None)
        self.summary_path = query

    def enable_plot(self) -> None:
        """Enables plot button if all necessary files are placed in memory."""
        if isinstance(self.summary_df, pd.DataFrame) and isinstance(
                self.population_df, pd.DataFrame):
            self.plot_button.setEnabled(True)

    def run_plot(self) -> None:
        """Sets and starts the plot worker."""
        worker = Worker(func=self.plot)
        worker.signals.error.connect(self.generic_error_message)
        worker.signals.success.connect(self.secondary_window.show)
        self.threadpool.start(worker)

    def plot(self) -> None:
        """Logic for plotting data based on user selection of populations, markers, etc."""
        # Clear figure currently on plot
        self.dynamic_canvas.axes.cla()
        # Initialize values and get parameters from GUI
        columns = ['sample', 'marker', 'population', 'expression']
        samples = self.parse_sample_names()
        pop_01 = self.drop_down_01.currentText()
        pop_02 = self.drop_down_02.currentText()
        pops_to_analyse = [pop_01, pop_02]
        marker = self.drop_down_03.currentText()
        cutoff_from_reference = True if self.drop_down_04.currentText(
        ) == 'OutR' else False
        violin_df = pd.DataFrame(columns=columns)
        # Start fetching data from files
        # Whole population
        for pop in pops_to_analyse:
            if pop == 'whole population':
                for partial_df in self.yield_violin_values(
                        df=self.population_df,
                        population='whole population',
                        samples=samples,
                        marker=marker,
                        columns=columns):
                    violin_df = violin_df.append(partial_df)
        # Other comparisons
            elif pop != 'none':
                for file_number in self.yield_selected_file_numbers(
                        summary_df=self.summary_df,
                        population=pop,
                        cutoff_from_reference=cutoff_from_reference,
                        marker=marker):
                    df_path = os.path.join(self.summary_path, 'data',
                                           f'{"%04d" % file_number}.')
                    try:
                        sample_df = pd.read_excel(df_path + 'xlsx',
                                                  index_col=0)
                    except FileNotFoundError:
                        sample_df = pd.read_csv(df_path + 'csv', index_col=0)
                    if not sample_df.empty:
                        for partial_df in self.yield_violin_values(
                                df=sample_df,
                                population=pop,
                                samples=samples,
                                marker=marker,
                                columns=columns):
                            violin_df = violin_df.append(partial_df)
        # Plot data
        pops_to_analyse = [p for p in pops_to_analyse if p != 'none']
        violin_df = violin_df[violin_df['marker'] == marker]
        for pop in pops_to_analyse:
            pop_subset = violin_df.loc[violin_df['population'] == pop]
            for sample in samples:
                sample_subset = pop_subset.loc[pop_subset['sample'] == sample]
                sat = 1.0 - samples.index(sample) / (len(samples) + 1)
                self.dynamic_canvas.update_figure(
                    subset_by_sample=sample_subset,
                    pop=pop,
                    sat=sat,
                    samples=samples)
        # Draw plotted data on canvas
        if self.legend_checkbox.isChecked():
            self.dynamic_canvas.add_legend()
        self.dynamic_canvas.axes.set_title(
            f'{marker} expression - {self.drop_down_04.currentText()}')
        self.dynamic_canvas.fig.canvas.draw()

    def parse_sample_names(self) -> List[str]:
        """Parse sample names from the QLineEdit Widget."""
        return self.sample_names.text().split(';')

    def generic_error_message(self, error: Tuple[Exception, str]) -> None:
        """Error message box used to display any error message (including traceback) for any uncaught errors."""
        name, trace = error
        QMessageBox.critical(
            self, 'An error occurred!',
            f"Error: {str(name)}\n\nfull traceback:\n{trace}")

    def closeEvent(self, event: QEvent) -> None:
        """Defines the message box for when the user wants to quit ViolinGUI."""
        title = 'Quit Application'
        mes = "Are you sure you want to quit?"
        reply = QMessageBox.question(self, title, mes,
                                     QMessageBox.Yes | QMessageBox.No,
                                     QMessageBox.No)
        if reply == QMessageBox.Yes:
            self.setEnabled(False)
            self.threadpool.waitForDone()
            event.accept()
        else:
            event.ignore()

    @staticmethod
    def yield_violin_values(df: pd.DataFrame, population: str,
                            samples: List[str], marker: str,
                            columns: List[str]) -> pd.DataFrame:
        """Returns a DataFrame from expression values, along with information of sample, marker and population. This
        DataFrame is appended to the violin plot DataFrame in order to simplify plotting the violins afterwards."""
        for sample in samples:
            series = df.loc[df.index.str.contains(sample)].loc[:, marker]
            yield pd.DataFrame(
                {
                    'sample': sample,
                    'marker': marker,
                    'population': population,
                    'expression': series
                },
                columns=columns)

    @staticmethod
    def yield_selected_file_numbers(
            summary_df: pd.DataFrame, population: str,
            cutoff_from_reference: bool,
            marker: str) -> Generator[pd.DataFrame, None, None]:
        """Yields file numbers from DataFrames resulting from SCOUTS analysis. DataFrames are yielded based on
        global values, i.e. the comparisons the user wants to perform."""
        cutoff = 'sample'
        if cutoff_from_reference is True:
            cutoff = 'reference'
        for index, (file_number, cutoff_from, reference, outliers_for,
                    category) in summary_df.iterrows():
            if cutoff_from == cutoff and outliers_for == marker and category == population:
                yield file_number
Beispiel #5
0
class TabDisplays(QTabWidget):
    def __init__(self, parent=None):
        super(TabDisplays, self).__init__(parent)

        # Initialize logging
        logging_conf_file = os.path.join(os.path.dirname(__file__),
                                         'cfg/aecgviewer_aecg_logging.conf')
        logging.config.fileConfig(logging_conf_file)
        self.logger = logging.getLogger(__name__)

        self.studyindex_info = aecg.tools.indexer.StudyInfo()

        self.validator = QWidget()

        self.studyinfo = QWidget()

        self.statistics = QWidget()

        self.waveforms = QWidget()

        self.waveforms.setAccessibleName("Waveforms")

        self.scatterplot = QWidget()

        self.histogram = QWidget()

        self.trends = QWidget()

        self.xmlviewer = QWidget()
        self.xml_display = QTextEdit(self.xmlviewer)

        self.options = QWidget()

        self.aecg_display_area = QScrollArea()
        self.cbECGLayout = QComboBox()
        self.aecg_display = EcgDisplayWidget(self.aecg_display_area)
        self.aecg_display_area.setWidget(self.aecg_display)

        self.addTab(self.validator, "Study information")
        self.addTab(self.waveforms, "Waveforms")
        self.addTab(self.xmlviewer, "XML")
        self.addTab(self.options, "Options")

        self.setup_validator()
        self.setup_waveforms()
        self.setup_xmlviewer()
        self.setup_options()

        size = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        size.setHeightForWidth(False)
        self.setSizePolicy(size)

        # Initialized a threpool with 2 threads 1 for the GUI, 1 for long
        # tasks, so GUI remains responsive
        self.threadpool = QThreadPool()
        self.threadpool.setMaxThreadCount(2)
        self.validator_worker = None
        self.indexing_timer = QElapsedTimer()

    def setup_validator(self):
        self.directory_indexer = None  # aecg.indexing.DirectoryIndexer()
        self.validator_layout_container = QWidget()
        self.validator_layout = QFormLayout()
        self.validator_form_layout = QFormLayout(
            self.validator_layout_container)
        self.validator_grid_layout = QGridLayout()
        self.study_info_file = QLineEdit()
        self.study_info_file.setToolTip("Study index file")
        self.study_info_description = QLineEdit()
        self.study_info_description.setToolTip("Description")
        self.app_type = QLineEdit()
        self.app_type.setToolTip("Application type (e.g., NDA, IND, BLA, IDE)")
        self.app_num = QLineEdit()
        self.app_num.setToolTip("Six-digit application number")
        self.app_num.setValidator(QIntValidator(self.app_num))
        self.study_id = QLineEdit()
        self.study_id.setToolTip("Study identifier")
        self.study_sponsor = QLineEdit()
        self.study_sponsor.setToolTip("Sponsor of the study")

        self.study_annotation_aecg_cb = QComboBox()
        self.study_annotation_aecg_cb.addItems(
            ["Rhythm", "Derived beat", "Holter-rhythm", "Holter-derived"])
        self.study_annotation_aecg_cb.setToolTip(
            "Waveforms used to perform the ECG measurements (i.e., "
            "annotations)\n"
            "\tRhythm: annotations in a rhythm strip or discrete ECG "
            "extraction (e.g., 10-s strips)\n"
            "\tDerived beat: annotations in a representative beat derived "
            "from a rhythm strip\n"
            "\tHolter-rhythm: annotations in a the analysis window of a "
            "continuous recording\n"
            "\tHolter-derived: annotations in a representative beat derived "
            "from analysis window of a continuous recording\n")
        self.study_annotation_lead_cb = QComboBox()
        self.ui_leads = ["GLOBAL"] + aecg.STD_LEADS[0:12] +\
            [aecg.KNOWN_NON_STD_LEADS[1]] + aecg.STD_LEADS[12:15] + ["Other"]
        self.study_annotation_lead_cb.addItems(self.ui_leads)
        self.study_annotation_lead_cb.setToolTip(
            "Primary analysis lead annotated per protocol. There could be "
            "annotations in other leads also, but only the primary lead should"
            " be selected here.\n"
            "Select global if all leads were used at the "
            "same time (e.g., superimposed on screen).\n"
            "Select other if the primary lead used is not in the list.")

        self.study_numsubjects = QLineEdit()
        self.study_numsubjects.setToolTip(
            "Number of subjects with ECGs in the study")
        self.study_numsubjects.setValidator(
            QIntValidator(self.study_numsubjects))

        self.study_aecgpersubject = QLineEdit()
        self.study_aecgpersubject.setToolTip(
            "Number of scheduled ECGs (or analysis windows) per subject as "
            "specified in the study protocol.\n"
            "Enter average number of ECGs "
            "per subject if the protocol does not specify a fixed number of "
            "ECGs per subject.")
        self.study_aecgpersubject.setValidator(
            QIntValidator(self.study_aecgpersubject))

        self.study_numaecg = QLineEdit()
        self.study_numaecg.setToolTip(
            "Total number of aECG XML files in the study")
        self.study_numaecg.setValidator(QIntValidator(self.study_numaecg))

        self.study_annotation_numbeats = QLineEdit()
        self.study_annotation_numbeats.setToolTip(
            "Minimum number of beats annotated in each ECG or analysis window"
            ".\nEnter 1 if annotations were done in the derived beat.")
        self.study_annotation_numbeats.setValidator(
            QIntValidator(self.study_annotation_numbeats))

        self.aecg_numsubjects = QLineEdit()
        self.aecg_numsubjects.setToolTip(
            "Number of subjects found across the provided aECG XML files")
        self.aecg_numsubjects.setReadOnly(True)

        self.aecg_aecgpersubject = QLineEdit()
        self.aecg_aecgpersubject.setToolTip(
            "Average number of ECGs per subject found across the provided "
            "aECG XML files")
        self.aecg_aecgpersubject.setReadOnly(True)

        self.aecg_numaecg = QLineEdit()
        self.aecg_numaecg.setToolTip(
            "Number of aECG XML files found in the study aECG directory")
        self.aecg_numaecg.setReadOnly(True)

        self.subjects_less_aecgs = QLineEdit()
        self.subjects_less_aecgs.setToolTip(
            "Percentage of subjects with less aECGs than specified per "
            "protocol")
        self.subjects_less_aecgs.setReadOnly(True)

        self.subjects_more_aecgs = QLineEdit()
        self.subjects_more_aecgs.setToolTip(
            "Percentage of subjects with more aECGs than specified per "
            "protocol")
        self.subjects_more_aecgs.setReadOnly(True)

        self.aecgs_no_annotations = QLineEdit()
        self.aecgs_no_annotations.setToolTip(
            "Percentage of aECGs with no annotations")
        self.aecgs_no_annotations.setReadOnly(True)

        self.aecgs_less_qt_in_primary_lead = QLineEdit()
        self.aecgs_less_qt_in_primary_lead.setToolTip(
            "Percentage of aECGs with less QT intervals in the primary lead "
            "than specified per protocol")
        self.aecgs_less_qt_in_primary_lead.setReadOnly(True)

        self.aecgs_less_qts = QLineEdit()
        self.aecgs_less_qts.setToolTip(
            "Percentage of aECGs with less QT intervals than specified per "
            "protocol")
        self.aecgs_less_qts.setReadOnly(True)

        self.aecgs_annotations_multiple_leads = QLineEdit()
        self.aecgs_annotations_multiple_leads.setToolTip(
            "Percentage of aECGs with QT annotations in multiple leads")
        self.aecgs_annotations_multiple_leads.setReadOnly(True)

        self.aecgs_annotations_no_primary_lead = QLineEdit()
        self.aecgs_annotations_no_primary_lead.setToolTip(
            "Percentage of aECGs with QT annotations not in the primary lead")
        self.aecgs_annotations_no_primary_lead.setReadOnly(True)

        self.aecgs_with_errors = QLineEdit()
        self.aecgs_with_errors.setToolTip("Number of aECG files with errors")
        self.aecgs_with_errors.setReadOnly(True)

        self.aecgs_potentially_digitized = QLineEdit()
        self.aecgs_potentially_digitized.setToolTip(
            "Number of aECG files potentially digitized (i.e., with more than "
            "5% of samples missing)")
        self.aecgs_potentially_digitized.setReadOnly(True)

        self.study_dir = QLineEdit()
        self.study_dir.setToolTip("Directory containing the aECG files")
        self.study_dir_button = QPushButton("...")
        self.study_dir_button.clicked.connect(self.select_study_dir)
        self.study_dir_button.setToolTip("Open select directory dialog")

        self.validator_form_layout.addRow("Application Type", self.app_type)
        self.validator_form_layout.addRow("Application Number", self.app_num)
        self.validator_form_layout.addRow("Study name/ID", self.study_id)
        self.validator_form_layout.addRow("Sponsor", self.study_sponsor)
        self.validator_form_layout.addRow("Study description",
                                          self.study_info_description)

        self.validator_form_layout.addRow("Annotations in",
                                          self.study_annotation_aecg_cb)
        self.validator_form_layout.addRow("Annotations primary lead",
                                          self.study_annotation_lead_cb)

        self.validator_grid_layout.addWidget(QLabel(""), 0, 0)
        self.validator_grid_layout.addWidget(
            QLabel("Per study protocol or report"), 0, 1)
        self.validator_grid_layout.addWidget(QLabel("Found in aECG files"), 0,
                                             2)

        self.validator_grid_layout.addWidget(QLabel("Number of subjects"), 1,
                                             0)
        self.validator_grid_layout.addWidget(self.study_numsubjects, 1, 1)
        self.validator_grid_layout.addWidget(self.aecg_numsubjects, 1, 2)

        self.validator_grid_layout.addWidget(
            QLabel("Number of aECG per subject"), 2, 0)
        self.validator_grid_layout.addWidget(self.study_aecgpersubject, 2, 1)
        self.validator_grid_layout.addWidget(self.aecg_aecgpersubject, 2, 2)

        self.validator_grid_layout.addWidget(QLabel("Total number of aECG"), 3,
                                             0)
        self.validator_grid_layout.addWidget(self.study_numaecg, 3, 1)
        self.validator_grid_layout.addWidget(self.aecg_numaecg, 3, 2)

        self.validator_grid_layout.addWidget(
            QLabel("Number of beats per aECG"), 4, 0)
        self.validator_grid_layout.addWidget(self.study_annotation_numbeats, 4,
                                             1)

        self.validator_grid_layout.addWidget(
            QLabel("Subjects with fewer ECGs"), 5, 1)
        self.validator_grid_layout.addWidget(self.subjects_less_aecgs, 5, 2)
        self.validator_grid_layout.addWidget(QLabel("Subjects with more ECGs"),
                                             6, 1)
        self.validator_grid_layout.addWidget(self.subjects_more_aecgs, 6, 2)
        self.validator_grid_layout.addWidget(
            QLabel("aECGs without annotations"), 7, 1)
        self.validator_grid_layout.addWidget(self.aecgs_no_annotations, 7, 2)
        self.validator_grid_layout.addWidget(
            QLabel("aECGs without expected number of QTs in primary lead"), 8,
            1)
        self.validator_grid_layout.addWidget(
            self.aecgs_less_qt_in_primary_lead, 8, 2)
        self.validator_grid_layout.addWidget(
            QLabel("aECGs without expected number of QTs"), 9, 1)
        self.validator_grid_layout.addWidget(self.aecgs_less_qts, 9, 2)

        self.validator_grid_layout.addWidget(
            QLabel("aECGs annotated in multiple leads"), 10, 1)
        self.validator_grid_layout.addWidget(
            self.aecgs_annotations_multiple_leads, 10, 2)
        self.validator_grid_layout.addWidget(
            QLabel("aECGs with annotations not in primary lead"), 11, 1)
        self.validator_grid_layout.addWidget(
            self.aecgs_annotations_no_primary_lead, 11, 2)
        self.validator_grid_layout.addWidget(QLabel("aECGs with errors"), 12,
                                             1)
        self.validator_grid_layout.addWidget(self.aecgs_with_errors, 12, 2)

        self.validator_grid_layout.addWidget(
            QLabel("Potentially digitized aECGs"), 13, 1)
        self.validator_grid_layout.addWidget(self.aecgs_potentially_digitized,
                                             13, 2)

        self.validator_form_layout.addRow(self.validator_grid_layout)

        tmp = QHBoxLayout()
        tmp.addWidget(self.study_dir)
        tmp.addWidget(self.study_dir_button)
        self.validator_form_layout.addRow("Study aECGs directory", tmp)

        self.validator_form_layout.addRow("Study index file",
                                          self.study_info_file)

        self.validator_layout.addWidget(self.validator_layout_container)
        self.validator_effective_dirs = QLabel("")
        self.validator_effective_dirs.setWordWrap(True)
        self.validator_layout.addWidget(self.validator_effective_dirs)

        self.val_button = QPushButton("Generate/update study index")
        self.val_button.clicked.connect(self.importstudy_dialog)
        self.validator_layout.addWidget(self.val_button)
        self.cancel_val_button = QPushButton("Cancel study index generation")
        self.cancel_val_button.clicked.connect(self.cancel_validator)
        self.cancel_val_button.setEnabled(False)
        self.validator_layout.addWidget(self.cancel_val_button)
        self.validator_pl = QLabel("")
        self.validator_layout.addWidget(self.validator_pl)
        self.validator_pb = QProgressBar()
        self.validator_layout.addWidget(self.validator_pb)
        self.validator.setLayout(self.validator_layout)
        self.stop_indexing = False

        self.lastindexing_starttime = None

        self.update_validator_effective_dirs()

    def effective_aecgs_dir(self, navwidget, silent=False):
        aecgs_effective_dir = self.study_dir.text()
        if navwidget.project_loaded != '':
            # Path specified in the GUI
            potential_aecgs_dirs = [self.study_dir.text()]
            # StudyDir path from current working directory
            potential_aecgs_dirs += [self.studyindex_info.StudyDir]
            # StudyDir path from directory where the index is located
            potential_aecgs_dirs += [
                os.path.join(os.path.dirname(navwidget.project_loaded),
                             self.studyindex_info.StudyDir)
            ]
            # StudyDir replaced with the directory where the index is located
            potential_aecgs_dirs += [os.path.dirname(navwidget.project_loaded)]
            dir_found = False
            # Get xml and zip filenames from first element in the index
            aecg_xml_file = navwidget.data_index["AECGXML"][0]
            zipfile = ""
            if aecg_xml_file != "":
                zipfile = navwidget.data_index["ZIPFILE"][0]
            for p in potential_aecgs_dirs:
                testfn = os.path.join(p, aecg_xml_file)
                if zipfile != "":
                    testfn = os.path.join(p, zipfile)
                if os.path.isfile(testfn):
                    dir_found = True
                    aecgs_effective_dir = p
                    break
            if not silent:
                if not dir_found:
                    QMessageBox.warning(
                        self, f"Study aECGs directory not found",
                        f"The following paths were checked:"
                        f"{[','.join(p) for p in potential_aecgs_dirs]} and "
                        f"none of them is valid")
                elif p != self.study_dir.text():
                    QMessageBox.warning(
                        self, f"Study aECGs directory not found",
                        f"The path specified in the study aECGs directory is "
                        f"not valid and {p} is being used instead. Check and "
                        f"update the path in Study aECGs directory textbox if "
                        f"the suggested path is not the adequate path")
        return aecgs_effective_dir

    def update_validator_effective_dirs(self):
        msg = f"Working directory: {os.getcwd()}"
        if self.parent() is not None:
            if isinstance(self.parent(), QSplitter):
                navwidget = self.parent().parent()
            else:  # Tabs widget has not been allocated the QSplitter yet
                navwidget = self.parent()
            project_loaded = navwidget.project_loaded
            if project_loaded != '':
                msg = f"{msg}\nLoaded project index: "\
                      f"{navwidget.project_loaded}"
                effective_aecgs_path = self.effective_aecgs_dir(navwidget)
                msg = f"{msg}\nEffective study aECGs directory: "\
                      f"{effective_aecgs_path}"
            else:
                msg = f"{msg}\nLoaded project index: None"
        else:
            msg = f"{msg}\nLoaded project index: None"
        self.validator_effective_dirs.setText(msg)

    def load_study_info(self, fileName):
        self.study_info_file.setText(fileName)
        try:
            study_info = pd.read_excel(fileName, sheet_name="Info")
            self.studyindex_info = aecg.tools.indexer.StudyInfo()
            self.studyindex_info.__dict__.update(
                study_info.set_index("Property").transpose().reset_index(
                    drop=True).to_dict('index')[0])
            sponsor = ""
            description = ""
            if self.studyindex_info.Sponsor is not None and\
                    isinstance(self.studyindex_info.Sponsor, str):
                sponsor = self.studyindex_info.Sponsor
            if self.studyindex_info.Description is not None and\
                    isinstance(self.studyindex_info.Description, str):
                description = self.studyindex_info.Description
            self.study_sponsor.setText(sponsor)
            self.study_info_description.setText(description)
            self.app_type.setText(self.studyindex_info.AppType)
            self.app_num.setText(f"{int(self.studyindex_info.AppNum):06d}")
            self.study_id.setText(self.studyindex_info.StudyID)
            self.study_numsubjects.setText(str(self.studyindex_info.NumSubj))
            self.study_aecgpersubject.setText(
                str(self.studyindex_info.NECGSubj))
            self.study_numaecg.setText(str(self.studyindex_info.TotalECGs))

            anns_in = self.studyindex_info.AnMethod.upper()
            idx = 0
            if anns_in == "RHYTHM":
                idx = 0
            elif anns_in == "DERIVED":
                idx = 1
            elif anns_in == "HOLTER_RHYTHM":
                idx = 2
            elif anns_in == "HOLTER_MEDIAN_BEAT":
                idx = 3
            else:
                idx = int(anns_in) - 1
            self.study_annotation_aecg_cb.setCurrentIndex(idx)

            the_lead = self.studyindex_info.AnLead
            idx = self.study_annotation_lead_cb.findText(str(the_lead))
            if idx == -1:
                idx = self.study_annotation_lead_cb.findText("MDC_ECG_LEAD_" +
                                                             str(the_lead))
            if idx == -1:
                idx = int(the_lead)
            self.study_annotation_lead_cb.setCurrentIndex(idx)

            self.study_annotation_numbeats.setText(
                str(self.studyindex_info.AnNbeats))
            if self.studyindex_info.StudyDir == "":
                self.studyindex_info.StudyDir = os.path.dirname(fileName)
            self.study_dir.setText(self.studyindex_info.StudyDir)

            self.update_validator_effective_dirs()
            self.setCurrentWidget(self.validator)

        except Exception as ex:
            QMessageBox.critical(
                self, "Import study error",
                "Error reading the study information file: '" + fileName + "'")

    def setup_waveforms(self):
        wflayout = QVBoxLayout()

        # ECG plot layout selection box
        self.cbECGLayout.addItems(
            ['12-lead stacked', '3x4 + lead II rhythm', 'Superimposed'])
        self.cbECGLayout.currentIndexChanged.connect(
            self.ecgplotlayout_changed)

        # Zoom buttons
        blayout = QHBoxLayout()

        pb_zoomin = QPushButton()
        pb_zoomin.setText("Zoom +")
        pb_zoomin.clicked.connect(self.zoom_in)

        pb_zoomreset = QPushButton()
        pb_zoomreset.setText("Zoom 1:1")
        pb_zoomreset.clicked.connect(self.zoom_reset)

        pb_zoomout = QPushButton()
        pb_zoomout.setText("Zoom -")
        pb_zoomout.clicked.connect(self.zoom_out)

        blayout.addWidget(self.cbECGLayout)
        blayout.addWidget(pb_zoomout)
        blayout.addWidget(pb_zoomreset)
        blayout.addWidget(pb_zoomin)

        wflayout.addLayout(blayout)

        # Add QScrollArea to main layout of waveforms tab
        self.aecg_display_area.setWidgetResizable(False)
        wflayout.addWidget(self.aecg_display_area)
        self.waveforms.setLayout(wflayout)
        size = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
        size.setHeightForWidth(False)
        self.aecg_display_area.setSizePolicy(size)
        self.waveforms.setSizePolicy(size)

    def setup_xmlviewer(self):
        wf_layout = QHBoxLayout()
        wf_layout.addWidget(self.xml_display)
        self.xmlviewer.setLayout(wf_layout)

    def setup_options(self):
        self.options_layout = QFormLayout()
        self.aecg_schema_filename = QLineEdit(aecg.get_aecg_schema_location())
        self.options_layout.addRow("aECG XML schema path",
                                   self.aecg_schema_filename)

        self.save_index_every_n_aecgs = QSpinBox()
        self.save_index_every_n_aecgs.setMinimum(0)
        self.save_index_every_n_aecgs.setMaximum(50000)
        self.save_index_every_n_aecgs.setValue(0)
        self.save_index_every_n_aecgs.setSingleStep(100)
        self.save_index_every_n_aecgs.setSuffix(" aECGs")
        self.save_index_every_n_aecgs.setToolTip(
            "Set o 0 to save the study index file only after its generation "
            "is completed.\nOtherwise, the file is saved everytime the "
            " specified number of ECGs have been appended to the index.")
        self.options_layout.addRow("Save index every ",
                                   self.save_index_every_n_aecgs)

        self.save_all_intervals_cb = QCheckBox("")
        self.save_all_intervals_cb.setChecked(False)
        self.options_layout.addRow("Save individual beat intervals",
                                   self.save_all_intervals_cb)

        self.parallel_processing_cb = QCheckBox("")
        self.parallel_processing_cb.setChecked(True)
        self.options_layout.addRow("Parallel processing of files",
                                   self.parallel_processing_cb)

        self.options.setLayout(self.options_layout)

    def zoom_in(self):
        self.aecg_display.apply_zoom(self.aecg_display.zoom_factor + 0.1)

    def zoom_out(self):
        self.aecg_display.apply_zoom(self.aecg_display.zoom_factor - 0.1)

    def zoom_reset(self):
        self.aecg_display.apply_zoom(1.0)

    def ecgplotlayout_changed(self, i):
        self.aecg_display.update_aecg_plot(
            ecg_layout=aecg.utils.ECG_plot_layout(i + 1))

    def update_search_progress(self, i, n):
        self.validator_pl.setText(
            f"Searching aECGs in directory ({n} XML files found)")

    def update_progress(self, i, n):
        j = i
        m = n
        if i <= 1:
            j = 1
            if self.validator_pb.value() > 0:
                j = self.validator_pb.value() + 1
            m = self.validator_pb.maximum()
        running_time = self.indexing_timer.elapsed() * 1e-3  # in seconds
        time_per_item = running_time / j
        # reamining = seconds per item so far * total pending items to process
        remaining_time = time_per_item * (m - j)
        eta = datetime.datetime.now() +\
            datetime.timedelta(seconds=round(remaining_time, 0))
        self.validator_pl.setText(
            f"Validating aECG {j}/{m} | "
            f"Execution time: "
            f"{str(datetime.timedelta(0,seconds=round(running_time)))} | "
            f"{round(1/time_per_item,2)} aECGs per second | "
            f"ETA: {eta.isoformat(timespec='seconds')}")
        self.validator_pb.setValue(j)
        if self.save_index_every_n_aecgs.value() > 0 and\
                len(self.directory_indexer.studyindex) % \
                self.save_index_every_n_aecgs.value() == 0:
            self.save_validator_results(
                pd.concat(self.directory_indexer.studyindex,
                          ignore_index=True))

    def save_validator_results(self, res):
        if res.shape[0] > 0:
            self.studyindex_info = aecg.tools.indexer.StudyInfo()
            self.studyindex_info.StudyDir = self.study_dir.text()
            self.studyindex_info.IndexFile = self.study_info_file.text()
            self.studyindex_info.Sponsor = self.study_sponsor.text()
            self.studyindex_info.Description =\
                self.study_info_description.text()
            self.studyindex_info.Date = self.lastindexing_starttime.isoformat()
            self.studyindex_info.End_date = datetime.datetime.now().isoformat()
            self.studyindex_info.Version = aecg.__version__
            self.studyindex_info.AppType = self.app_type.text()
            self.studyindex_info.AppNum = f"{int(self.app_num.text()):06d}"
            self.studyindex_info.StudyID = self.study_id.text()
            self.studyindex_info.NumSubj = int(self.study_numsubjects.text())
            self.studyindex_info.NECGSubj = int(
                self.study_aecgpersubject.text())
            self.studyindex_info.TotalECGs = int(self.study_numaecg.text())
            anmethod = aecg.tools.indexer.AnnotationMethod(
                self.study_annotation_aecg_cb.currentIndex())
            self.studyindex_info.AnMethod = anmethod.name
            self.studyindex_info.AnLead =\
                self.study_annotation_lead_cb.currentText()
            self.studyindex_info.AnNbeats = int(
                self.study_annotation_numbeats.text())

            # Calculate stats
            study_stats = aecg.tools.indexer.StudyStats(
                self.studyindex_info, res)

            # Save to file
            aecg.tools.indexer.save_study_index(self.studyindex_info, res,
                                                study_stats)

    validator_data_ready = Signal()

    def save_validator_results_and_load_index(self, res):
        self.save_validator_results(res)
        self.validator_data_ready.emit()

    def indexer_validator_results(self, res):
        self.studyindex_df = pd.concat([self.studyindex_df, res],
                                       ignore_index=True)

    def subindex_thread_complete(self):
        return

    def index_directory_thread_complete(self):
        tmp = self.validator_pl.text().replace("ETA:", "Completed: ").replace(
            "Validating", "Validated")
        self.validator_pl.setText(tmp)
        self.val_button.setEnabled(True)
        self.cancel_val_button.setEnabled(False)
        self.validator_layout_container.setEnabled(True)

    def index_directory(self, progress_callback):
        self.lastindexing_starttime = datetime.datetime.now()
        self.indexing_timer.start()

        studyindex_df = []
        n_cores = os.cpu_count()
        aecg_schema = None
        if self.aecg_schema_filename.text() != "":
            aecg_schema = self.aecg_schema_filename.text()
        if self.parallel_processing_cb.isChecked():
            studyindex_df = self.directory_indexer.index_directory(
                self.save_all_intervals_cb.isChecked(), aecg_schema, n_cores,
                progress_callback)
        else:
            studyindex_df = self.directory_indexer.index_directory(
                self.save_all_intervals_cb.isChecked(), aecg_schema, 1,
                progress_callback)

        return studyindex_df

    def importstudy_dialog(self):
        dirName = os.path.normpath(self.study_dir.text())
        if dirName != "":
            if os.path.exists(dirName):
                self.directory_indexer = aecg.indexing.DirectoryIndexer()
                self.directory_indexer.set_aecg_dir(
                    dirName, self.update_search_progress)
                self.validator_pb.setMaximum(self.directory_indexer.num_files)
                self.validator_pb.reset()
                self.stop_indexing = False
                self.validator_layout_container.setEnabled(False)
                self.val_button.setEnabled(False)
                self.cancel_val_button.setEnabled(True)
                self.validator_worker = Worker(self.index_directory)
                self.validator_worker.signals.result.connect(
                    self.save_validator_results_and_load_index)
                self.validator_worker.signals.finished.connect(
                    self.index_directory_thread_complete)
                self.validator_worker.signals.progress.connect(
                    self.update_progress)

                # Execute
                self.threadpool.start(self.validator_worker)
            else:
                QMessageBox.critical(
                    self, "Directory not found",
                    f"Specified study directory not found:\n{dirName}")
        else:
            QMessageBox.critical(self, "Import study error",
                                 "Study directory cannot be empty")

    def cancel_validator(self):
        self.cancel_val_button.setEnabled(False)
        self.stop_indexing = True
        self.directory_indexer.cancel_indexing = True
        self.threadpool.waitForDone(3000)
        self.val_button.setEnabled(True)

    def select_study_dir(self):
        cd = self.study_dir.text()
        if cd == "":
            cd = "."
        dir = QFileDialog.getExistingDirectory(
            self, "Open Directory", cd,
            QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks)
        if dir != "":
            self.study_dir.setText(dir)
Beispiel #6
0
class MainWindow(QObject):
    def __init__(self, ui_file, parent=None):
        super(MainWindow, self).__init__(parent)
        ui_file = QFile(ui_file)
        ui_file.open(QFile.ReadOnly)

        loader = QUiLoader()
        self.window = loader.load(ui_file)
        ui_file.close()

        dir_btn: QPushButton = self.window.findChild(QPushButton,
                                                     "dir_choose_btn")
        dir_btn.clicked.connect(self.dir_btn_handler)

        self.dirPath: QLineEdit = self.window.findChild(QLineEdit, "dirPath")

        self.repeatFiles_dir: QLineEdit = self.window.findChild(
            QLineEdit, "repeatFiles_dir")
        self.singleFiles_dir: QLineEdit = self.window.findChild(
            QLineEdit, "singleFiles_dir")

        self.moveSingleFiles: QCheckBox = self.window.findChild(
            QCheckBox, "moveSingleFiles")
        self.moveRepeatFiles: QCheckBox = self.window.findChild(
            QCheckBox, "moveRepeatFiles")

        self.sortBar: QProgressBar = self.window.findChild(
            QProgressBar, "sortBar")
        self.sortBar.setValue(0)
        self.moveBar: QProgressBar = self.window.findChild(
            QProgressBar, "moveBar")
        self.moveBar.setValue(0)

        self.startButton: QPushButton = self.window.findChild(
            QPushButton, "startButton")
        self.startButton.clicked.connect(self.start_handler)
        self.stopButton: QPushButton = self.window.findChild(
            QPushButton, "stopButton")
        self.stopButton.clicked.connect(self.stop_handler)

        self.worker: Worker = None
        self.running = False

        self.thread_pool = QThreadPool()
        print("Multi threading with maximum %d threads" %
              self.thread_pool.maxThreadCount())

        self.msg_box = QMessageBox(parent=self.window)
        self.msg_box.setIcon(QMessageBox.Critical)
        self.msg_box.setWindowTitle("Некорректные настройки")

        self.window.destroyed.connect(self.stop_handler)
        self.window.show()

    def start_handler(self):
        if self.thread_pool.activeThreadCount() > 0:
            return
        if self.check_inputs():
            self.worker = Worker(self)
            self.worker.signals.updated.connect(self.worker_upd_handler)
            self.worker.running = True
            self.thread_pool.start(self.worker)
        return

    def stop_handler(self):
        self.worker.running = False
        self.thread_pool.waitForDone(1000)
        return

    def worker_upd_handler(self, data):
        if "sortBar" in data.keys():
            self.sortBar.setValue(data["sortBar"])
        if "moveBar" in data.keys():
            self.moveBar.setValue(data["moveBar"])

    def dir_btn_handler(self):
        dialog = QFileDialog()
        dirname = dialog.getExistingDirectory()
        self.dirPath.setText(dirname)

    def check_inputs(self):
        errors = []
        if not os.path.exists(self.dirPath.text()):
            errors.append("Выбранная папка не существует")
        if self.moveRepeatFiles.isChecked() and len(
                self.repeatFiles_dir.text()) < 1:
            errors.append(
                "Введите название папки для переноса повторяющихся файлов")
        if self.moveSingleFiles.isChecked() and len(
                self.singleFiles_dir.text()) < 1:
            errors.append(
                "Введите название папки для переноса одиночных файлов")
        if errors:
            self.msg_box.setText("\n".join(errors))
            self.msg_box.exec()
            return False
        return True
Beispiel #7
0
class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self, settings):
        # mandatory
        QMainWindow.__init__(self)
        Ui_MainWindow.__init__(self)
        self.trading_session_state = "TBD"
        self.est = timezone('US/Eastern')
        self.settingsWindow = SettingsWindow()
        self.setupUi(self)
        self.settings = settings
        self.ibkrworker = IBKRWorker(self.settings)
        self.threadpool = QThreadPool()
        self.setWindowTitle("Algo Traider v 2.0")

        sys.stderr = open('LOG/errorLog.txt', 'w')

        self.create_open_positions_grid()

        # setting a timer for Worker

        self.uiTimer = QTimer()
        self.uiTimer.timeout.connect(self.update_ui)

        self.workerTimer = QTimer()
        self.workerTimer.timeout.connect(self.run_worker)

        self.server_timer = QTimer()
        self.server_timer.timeout.connect(self.report_to_server)
        self.server_timer.start(int(self.settings.INTERVALSERVER) * 1000)

        # connecting a buttons
        self.chbxProcess.stateChanged.connect(self.process_checked)
        self.btnSettings.pressed.connect(self.show_settings)

        self.statusbar.showMessage("Ready")

        stock_names = [o.ticker for o in self.settings.CANDIDATES]
        self.ibkrworker.stocks_data_from_server = get_market_data_from_server(
            self.settings, stock_names)
        self.update_console("Market data for " + str(len(stock_names)) +
                            " Candidates received from Server")
        self.start_updating_candidates_and_connect()

        StyleSheet = '''
        #lcdPNLgreen {
            border: 3px solid green;
        }
        #lcdPNLred {
            border: 3px solid red;
        }
        '''
        self.setStyleSheet(StyleSheet)

    def update_candidates_info(self, status_callback, notification_callback):
        today_dt = date.today()
        for c in self.ibkrworker.stocks_data_from_server:
            updated_dt = c['tiprank_updated'].date()
            if today_dt != updated_dt:
                # yahoo
                notification_callback.emit('Update for ' + c['ticker'] +
                                           ' needed:')
                notification_callback.emit('Checking for Yahoo statisticks...')
                drop, change = get_yahoo_stats_for_candidate(
                    c['ticker'], notification_callback)
                c['yahoo_avdropP'] = drop
                c['yahoo_avspreadP'] = change
                notification_callback.emit('Got ' + str(drop) +
                                           ' average daily drop and ' +
                                           str(change) +
                                           ' average daily change.')
                # tipranks
                notification_callback.emit('Checking for Tiprank...')
                rank = get_tiprank_rating_for_ticker(
                    c['ticker'], self.settings.PATHTOWEBDRIVER)
                notification_callback.emit('Got rank of :' + str(rank))
                c['tipranks'] = rank
                c['tiprank_updated'] = today_dt
            else:
                notification_callback.emit(
                    'Data for ' + c['ticker'] +
                    ' is up to the date,no update needed')
        report_market_data_to_server(self.settings,
                                     self.ibkrworker.stocks_data_from_server)

        return 'done'

    def start_updating_candidates_and_connect(self):

        cand = Worker(self.update_candidates_info)
        cand.signals.result.connect(self.connect_to_ibkr)
        # connector.signals.status.connect(self.update_status)
        cand.signals.notification.connect(self.update_console)
        # Execute
        self.threadpool.start(cand)

    def connect_to_ibkr(self):
        """
Starts the connection to the IBKR terminal in separate thread
        """

        self.update_console("Reporting connection to the server...")
        print("Reporting connection to the server...")
        result = report_login_to_server(self.settings)
        self.update_console(result)
        connector = Worker(self.ibkrworker.prepare_and_connect)
        connector.signals.result.connect(self.connection_done)
        connector.signals.status.connect(self.update_status)
        connector.signals.notification.connect(self.update_console)
        # Execute
        self.threadpool.start(connector)

    def process_checked(self):
        """
Starts the Timer with interval from Config file
        """
        if self.chbxProcess.isChecked():
            self.run_worker()
            self.workerTimer.start(int(self.settings.INTERVALWORKER) * 1000)
        else:
            self.workerTimer.stop()

    # noinspection PyUnresolvedReferences
    def run_worker(self):
        """
Executed the Worker in separate thread
        """

        # exec(open('restarter.py').read())
        # sys.exit()
        self.update_session_state()
        currentTime = QTime().currentTime()
        fromTime = QTime(int(self.settings.TECHFROMHOUR),
                         int(self.settings.TECHFROMMIN))
        toTime = QTime(int(self.settings.TECHTOHOUR),
                       int(self.settings.TECHTOMIN))
        sessionState = self.lblMarket.text()

        if fromTime < currentTime < toTime:
            print("Worker skept-Technical break : ",
                  fromTime.toString("hh:mm"), " to ", toTime.toString("hh:mm"))
            self.update_console("Technical break untill " +
                                toTime.toString("hh:mm"))

        else:
            self.update_console("Starting Worker- UI Paused")
            self.uiTimer.stop(
            )  # to not cause an errors when lists will be resetted
            worker = Worker(
                self.ibkrworker.process_positions_candidates
            )  # Any other args, kwargs are passed to the run function
            worker.signals.result.connect(self.update_ui)
            worker.signals.status.connect(self.update_status)
            worker.signals.notification.connect(self.update_console)
            # Execute
            self.threadpool.start(worker)

    def connection_done(self):
        # add processing
        self.update_ui()
        if self.settings.AUTOSTART:
            self.chbxProcess.setChecked(True)

        # # report market data to server
        # if self.settings.USESERVER:
        #     print("Reporting market data to the server...")
        #     result = report_market_data_to_server(self.settings, self.ibkrworker.app.candidatesLive)
        #     self.update_console(result)

    def report_to_server(self):
        """
       reports to the server
        """

        if self.settings.USESERVER:
            net_liquidation = self.ibkrworker.app.netLiquidation
            if hasattr(self.ibkrworker.app, 'smaWithSafety'):
                remaining_sma_with_safety = self.ibkrworker.app.smaWithSafety
            else:
                remaining_sma_with_safety = self.ibkrworker.app.sMa
            excess_liquidity = self.ibkrworker.app.excessLiquidity
            remaining_trades = self.ibkrworker.app.tradesRemaining
            all_positions_value = 0
            open_positions = self.ibkrworker.app.openPositions
            open_orders = self.ibkrworker.app.openOrders
            dailyPnl = self.ibkrworker.app.dailyPnl
            tradinng_session_state = self.trading_session_state
            data_for_report = [
                self.settings, net_liquidation, remaining_sma_with_safety,
                remaining_trades, all_positions_value, open_positions,
                open_orders, dailyPnl,
                self.ibkrworker.last_worker_execution_time,
                datetime.now(self.est), self.trading_session_state,
                excess_liquidity
            ]

            worker = Worker(report_snapshot_to_server, self.settings,
                            data_for_report)

            worker.signals.result.connect(self.process_server_response)
            # Execute
            self.threadpool.start(worker)

    def process_server_response(self, r):
        # trying to restart
        if '$restart$' in r:
            restart()
        self.update_console(r)

    def update_ui(self):
        """
Updates UI after connection/worker execution
        """
        # main data
        self.lAcc.setText(self.settings.ACCOUNT)
        # self.lExcessLiquidity.setText(str(self.ibkrworker.app.excessLiquidity))
        # self.lSma.setText(str(self.ibkrworker.app.sMa))
        if hasattr(self.ibkrworker.app, 'smaWithSafety'):
            self.lSma.setText(str(round(self.ibkrworker.app.smaWithSafety, 1)))
        else:
            self.lSma.setText(str(round(self.ibkrworker.app.sMa, 1)))
        self.lMarketValue.setText(str(self.ibkrworker.app.netLiquidation))
        self.lblAvailTrades.setText(str(self.ibkrworker.app.tradesRemaining))
        self.lcdPNL.display(self.ibkrworker.app.dailyPnl)
        if self.ibkrworker.app.dailyPnl > 0:
            palette = self.lcdPNL.palette()
            palette.setColor(palette.WindowText, QtGui.QColor(51, 153, 51))
            self.lcdPNL.setPalette(palette)
        elif self.ibkrworker.app.dailyPnl < 0:
            palette = self.lcdPNL.palette()
            palette.setColor(palette.WindowText, QtGui.QColor(255, 0, 0))
            self.lcdPNL.setPalette(palette)

        total_positions_value = 0
        for p in self.ibkrworker.app.openPositions.values():
            if hasattr(p, 'Value'):
                total_positions_value += p["Value"]
        self.lPositionsTotalValue.setText(str(round(total_positions_value, 1)))

        self.update_open_positions()
        self.update_live_candidates()
        self.update_open_orders()

        # everything disabled for safety - is now enabled
        self.chbxProcess.setEnabled(True)
        self.btnSettings.setEnabled(True)

        self.update_session_state()

        if not self.uiTimer.isActive():
            self.update_console("UI resumed.")
        self.uiTimer.start(int(self.settings.INTERVALUI) *
                           1000)  # reset the ui timer

    def update_session_state(self):
        fmt = '%Y-%m-%d %H:%M:%S'
        self.est_dateTime = datetime.now(self.est)
        self.est_current_time = QTime(self.est_dateTime.hour,
                                      self.est_dateTime.minute,
                                      self.est_dateTime.second)
        self.lblTime.setText(self.est_current_time.toString())
        dStart = QTime(4, 00)
        dEnd = QTime(20, 00)
        tStart = QTime(9, 30)
        tEnd = QTime(16, 0)
        self.ibkrworker.check_if_holiday()
        if not self.ibkrworker.trading_session_holiday:
            if self.est_current_time > dStart and self.est_current_time <= tStart:
                self.ibkrworker.trading_session_state = "Pre Market"
                self.lblMarket.setText("Pre Market")
            elif self.est_current_time > tStart and self.est_current_time <= tEnd:
                self.ibkrworker.trading_session_state = "Open"
                self.lblMarket.setText("Open")
            elif self.est_current_time > tEnd and self.est_current_time <= dEnd:
                self.ibkrworker.trading_session_state = "After Market"
                self.lblMarket.setText("After Market")
            else:
                self.ibkrworker.trading_session_state = "Closed"
                self.lblMarket.setText("Closed")
        else:
            self.ibkrworker.trading_session_state = "Holiday"
            self.lblMarket.setText("Holiday")
        self.trading_session_state = self.ibkrworker.trading_session_state

    def progress_fn(self, n):
        msgBox = QMessageBox()
        msgBox.setText(str(n))
        retval = msgBox.exec_()

    def update_status(self, s):
        """
Updates StatusBar on event of Status
        :param s:
        """
        self.statusbar.showMessage(s)

    def update_console(self, n):
        """
Adds Message to console- upon event
        :param n:
        """
        self.consoleOut.append(n)
        self.log_message(n)

    def log_message(self, message):
        """
Adds message to the standard log
        :param message:
        """
        with open(LOGFILE, "a") as f:
            currentDt = datetime.now().strftime("%d-%b-%Y (%H:%M:%S.%f)")
            message = "\n" + currentDt + '---' + message
            f.write(message)

    def update_live_candidates(self):
        """
Updates Candidates table
        """

        liveCandidates = self.ibkrworker.app.candidatesLive
        try:
            line = 0
            self.tCandidates.setRowCount(len(liveCandidates))
            for k, v in liveCandidates.items():
                self.tCandidates.setItem(line, 0, QTableWidgetItem(v['Stock']))
                self.tCandidates.setItem(line, 1,
                                         QTableWidgetItem(str(v['Open'])))
                self.tCandidates.setItem(line, 2,
                                         QTableWidgetItem(str(v['Close'])))
                self.tCandidates.setItem(line, 3,
                                         QTableWidgetItem(str(v['Bid'])))
                self.tCandidates.setItem(line, 4,
                                         QTableWidgetItem(str(v['Ask'])))
                if v['Ask'] < v['target_price'] and v['Ask'] != -1:
                    self.tCandidates.item(line, 4).setBackground(
                        QtGui.QColor(0, 255, 0))
                if v['target_price'] is float:
                    self.tCandidates.setItem(
                        line, 5,
                        QTableWidgetItem(str(round(v['target_price'], 2))))
                else:
                    self.tCandidates.setItem(
                        line, 5, QTableWidgetItem(str(v['target_price'])))
                self.tCandidates.setItem(
                    line, 6,
                    QTableWidgetItem(str(round(v['averagePriceDropP'], 2))))
                if v['tipranksRank'] == '':
                    v['tipranksRank'] = 0
                self.tCandidates.setItem(
                    line, 7, QTableWidgetItem(str(v['tipranksRank'])))
                if int(v['tipranksRank']) > 7:
                    self.tCandidates.item(line, 7).setBackground(
                        QtGui.QColor(0, 255, 0))
                self.tCandidates.setItem(
                    line, 8, QTableWidgetItem(str(v['LastUpdate'])))

                line += 1
        except Exception as e:
            if hasattr(e, 'message'):
                self.update_console("Error in updating Candidates: " +
                                    str(e.message))
            else:
                self.update_console("Error in updating Candidates: " + str(e))

    def update_open_positions(self):
        """
Updates Positions grid
        """
        open_positions = self.ibkrworker.app.openPositions
        allKeys = [*open_positions]
        lastUpdatedWidget = 0
        try:
            for i in range(len(open_positions)):  # Update positions Panels

                widget = self.gp.itemAt(i).widget()
                key = allKeys[i]
                values = open_positions[key]
                if 'stocks' in values.keys():
                    if values['stocks'] != 0:
                        candidate = next((x for x in self.settings.CANDIDATES
                                          if x.ticker == key), None)
                        reason_of_candidate = "Bought manually"
                        if candidate is not None:
                            reason_of_candidate = candidate.reason
                        widget.update_view(key, values, reason_of_candidate)
                        widget.show()
                        lastUpdatedWidget = i
                    else:
                        widgetToRemove = self.gp.itemAt(i).widget()
                        widgetToRemove.hide()
                else:
                    print("value not yet received")

            for i in range(self.gp.count()):  # Hide the rest of the panels
                if i > lastUpdatedWidget:
                    widgetToRemove = self.gp.itemAt(i).widget()
                    widgetToRemove.hide()
        except Exception as e:
            if hasattr(e, 'message'):
                self.update_console("Error in refreshing Positions: " +
                                    str(e.message))
            else:
                self.update_console("Error in refreshing Positions: " + str(e))

    def create_open_positions_grid(self):
        """
Creates Open positions grid with 99 Positions widgets
        """

        counter = 0
        col = 0
        row = 0

        for i in range(0, 99):
            if counter % 3 == 0:
                col = 0
                row += 1
            self.gp.addWidget(PositionPanel(), row, col)
            counter += 1
            col += 1

    def update_open_orders(self):
        """
Updates Positions table
        """
        openOrders = self.ibkrworker.app.openOrders
        try:
            line = 0
            self.tOrders.setRowCount(len(openOrders))
            for k, v in openOrders.items():
                self.tOrders.setItem(line, 0, QTableWidgetItem(k))
                self.tOrders.setItem(line, 1, QTableWidgetItem(v['Action']))
                self.tOrders.setItem(line, 2, QTableWidgetItem(v['Type']))
                line += 1
        except Exception as e:
            if hasattr(e, 'message'):
                self.update_console("Error in Updating open Orders : " +
                                    str(e.message))
            else:
                self.update_console("Error in Updating open Orders : " +
                                    str(e))

    def thread_complete(self):
        """
After threaded task finished
        """
        print("TREAD COMPLETE (good or bad)!")

    def show_settings(self):

        # self.settingsWindow = SettingsWindowO(self.settings)
        # self.settingsWindow.show()  # Показываем окно
        # # maybe not needed
        # self.settingsWindow.changedSettings = False
        self.settingsWindow.existingSettings = copy.deepcopy(self.settings)
        self.settingsWindow.changedSettings = False
        self.settingsWindow.ibkrClient = self.ibkrworker
        if self.settingsWindow.exec_():
            self.settings = self.settingsWindow.existingSettings
            self.settings.write_config()
            self.restart_all()
        else:
            print("Settings window Canceled")
        self.settingsWindow = SettingsWindow()

    def restart_all(self):
        """
Restarts everything after Save
        """
        self.threadpool.waitForDone()
        self.update_console("UI paused- for restart")
        self.uiTimer.stop()

        self.workerTimer.stop()
        self.update_console("Configuration changed - restarting everything")
        self.chbxProcess.setEnabled(False)
        self.chbxProcess.setChecked(False)
        self.btnSettings.setEnabled(False)
        self.ibkrworker.app.disconnect()
        while self.ibkrworker.app.isConnected():
            print("waiting for disconnect")
            time.sleep(1)

        self.ibkrworker = None
        self.ibkrworker = IBKRWorker(self.settings)
        self.connect_to_ibkr()

        i = 4
Beispiel #8
0
class ProjectListItem(QObject):
    """
    The core functionality class - GUI representation of the Stm32pio project
    """

    nameChanged = Signal()  # properties notifiers
    stateChanged = Signal()
    stageChanged = Signal()

    logAdded = Signal(str, int,
                      arguments=['message', 'level'
                                 ])  # send the log message to the front-end
    actionDone = Signal(str, bool,
                        arguments=['action', 'success'
                                   ])  # emit when the action has executed

    def __init__(self,
                 project_args: list = None,
                 project_kwargs: dict = None,
                 parent: QObject = None):
        super().__init__(parent=parent)

        if project_args is None:
            project_args = []
        if project_kwargs is None:
            project_kwargs = {}

        self.logger = logging.getLogger(f"{stm32pio.lib.__name__}.{id(self)}")
        self.logger.setLevel(
            logging.DEBUG if settings.get('verbose') else logging.INFO)
        self.logging_worker = LoggingWorker(self.logger)
        self.logging_worker.sendLog.connect(self.logAdded)

        # QThreadPool can automatically queue new incoming tasks if a number of them are larger than maxThreadCount
        self.workers_pool = QThreadPool()
        self.workers_pool.setMaxThreadCount(1)
        self.workers_pool.setExpiryTimeout(
            -1)  # tasks forever wait for the available spot

        # These values are valid till the Stm32pio project does not initialize itself (or failed to)
        self.project = None
        self._name = 'Loading...'
        self._state = {'LOADING': True}
        self._current_stage = 'Loading...'

        self.qml_ready = threading.Event(
        )  # the front and the back both should know when each other is initialized

        self._finalizer = weakref.finalize(
            self, self.at_exit)  # register some kind of deconstruction handler

        if project_args is not None:
            if 'instance_options' not in project_kwargs:
                project_kwargs['instance_options'] = {'logger': self.logger}
            elif 'logger' not in project_kwargs['instance_options']:
                project_kwargs['instance_options']['logger'] = self.logger

            # Start the Stm32pio part initialization right after. It can take some time so we schedule it in a dedicated
            # thread
            self.init_thread = threading.Thread(target=self.init_project,
                                                args=project_args,
                                                kwargs=project_kwargs)
            self.init_thread.start()

    def init_project(self, *args, **kwargs) -> None:
        """
        Initialize the underlying Stm32pio project.

        Args:
            *args: positional arguments of the Stm32pio constructor
            **kwargs: keyword arguments of the Stm32pio constructor
        """
        try:
            self.project = stm32pio.lib.Stm32pio(*args, **kwargs)
        except Exception as e:
            # Error during the initialization
            self.logger.exception(e,
                                  exc_info=self.logger.isEnabledFor(
                                      logging.DEBUG))
            if len(args):
                self._name = args[
                    0]  # use project path string (probably) as a name
            else:
                self._name = 'No name'
            self._state = {'INIT_ERROR': True}
            self._current_stage = 'Initializing error'
        else:
            self._name = 'Project'  # successful initialization. These values should not be used anymore
            self._state = {}
            self._current_stage = 'Initialized'
        finally:
            self.qml_ready.wait()  # wait for the GUI to initialized
            self.nameChanged.emit(
            )  # in any case we should notify the GUI part about the initialization ending
            self.stageChanged.emit()
            self.stateChanged.emit()

    def at_exit(self):
        module_logger.info(f"destroy {self.project}")
        self.workers_pool.waitForDone(
            msecs=-1
        )  # wait for all jobs to complete. Currently, we cannot abort them gracefully
        self.logging_worker.stopped.set(
        )  # post the event in the logging worker to inform it...
        self.logging_worker.thread.wait()  # ...and wait for it to exit

    @Property(str, notify=nameChanged)
    def name(self):
        if self.project is not None:
            return self.project.path.name
        else:
            return self._name

    @Property('QVariant', notify=stateChanged)
    def state(self):
        if self.project is not None:
            # Convert to normal dict (JavaScript object) and exclude UNDEFINED key
            return {
                stage.name: value
                for stage, value in self.project.state.items()
                if stage != stm32pio.lib.ProjectStage.UNDEFINED
            }
        else:
            return self._state

    @Property(str, notify=stageChanged)
    def current_stage(self):
        if self.project is not None:
            return str(self.project.state.current_stage)
        else:
            return self._current_stage

    @Slot()
    def qmlLoaded(self):
        """
        Event signaling the complete loading of needed frontend components.
        """
        self.qml_ready.set()
        self.logging_worker.can_flush_log.set()

    @Slot(str, 'QVariantList')
    def run(self, action: str, args: list):
        """
        Asynchronously perform Stm32pio actions (generate, build, etc.) (dispatch all business logic).

        Args:
            action: method name of the corresponding Stm32pio action
            args: list of positional arguments for the action
        """

        worker = ProjectActionWorker(getattr(self.project, action), args,
                                     self.logger)
        worker.actionDone.connect(self.stateChanged)
        worker.actionDone.connect(self.stageChanged)
        worker.actionDone.connect(self.actionDone)

        self.workers_pool.start(
            worker)  # will automatically place to the queue