Exemplo n.º 1
0
class ScheduleEditorWindow(QMainWindow):
    """
    Class describing the main window of the program.
    """
    def __init__(self):
        super().__init__()

        self._schedule = Schedule()
        self._indexes_ref = self._schedule.indexes()
        self._file = None

        # window settings
        self.setWindowTitle(self.tr("Schedule Editor"))
        self.setMinimumSize(800, 600)

        # central widget settings
        self.table_widget = ScheduleTableWidget()
        self.table_widget.set_schedule(self._schedule)
        self.table_widget.setVerticalHeader(
            CustomHeaderView(Qt.Vertical, self._indexes_ref))
        self.table_widget.setHorizontalHeader(CustomHeaderView(Qt.Horizontal))
        self.table_widget.setWordWrap(True)
        self.table_widget.setContextMenuPolicy(Qt.CustomContextMenu)
        self.table_widget.setSelectionMode(QAbstractItemView.SingleSelection)
        self.table_widget.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.table_widget.horizontalHeader().setSectionResizeMode(
            QHeaderView.Stretch)
        self.table_widget.horizontalHeader().setMaximumHeight(25)
        self.table_widget.horizontalHeader().setStretchLastSection(True)
        self.table_widget.verticalHeader().setSectionResizeMode(
            QHeaderView.Fixed)
        self.table_widget.verticalHeader().setMaximumWidth(25)
        self.table_widget.verticalHeader().setStretchLastSection(True)
        self.table_widget.setSizePolicy(QSizePolicy.MinimumExpanding,
                                        QSizePolicy.MinimumExpanding)
        self.table_widget.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.table_widget.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

        self.table_widget.setColumnCount(self._schedule.columns())
        for i in range(8):
            item = QTableWidgetItem(TimePair.time_start_end(i))
            self.table_widget.setHorizontalHeaderItem(i, item)

        self.table_widget.setRowCount(self._schedule.rows())
        for i, day in enumerate(DaysOfWeek.to_list()):
            item = QTableWidgetItem(day)
            self.table_widget.setVerticalHeaderItem(i, item)

        self.setCentralWidget(self.table_widget)

        # menu bar settings
        self.menu_bar = self.menuBar()
        self.menu_file = self.menu_bar.addMenu(self.tr("&File"))

        self.action_new_file = QAction(QIcon.fromTheme("document-new"),
                                       self.tr("&New file"), self)
        self.action_new_file.setShortcut("Ctrl+N")
        self.menu_file.addAction(self.action_new_file)

        self.action_open = QAction(QIcon.fromTheme("document-open"),
                                   self.tr("&Open"), self)
        self.action_open.setShortcut("Ctrl+O")
        self.menu_file.addAction(self.action_open)

        self.action_save = QAction(QIcon.fromTheme("document-save"),
                                   self.tr("&Save"), self)
        self.action_save.setShortcut("Ctrl+S")
        self.menu_file.addAction(self.action_save)

        self.action_save_as = QAction(QIcon.fromTheme("document-save-as"),
                                      self.tr("Save as..."), self)
        self.action_save_as.setShortcut("Ctrl+S+A")
        self.menu_file.addAction(self.action_save_as)

        self.action_export = QAction(self.tr("Export"), self)
        self.action_export.setShortcut("Ctrl+E")
        self.menu_file.addAction(self.action_export)

        self.action_import = QAction(self.tr("Import"), self)
        self.action_import.setShortcut("Ctrl+I")
        self.menu_file.addAction(self.action_import)

        self.menu_file.addSeparator()

        self.action_settings = QAction(self.tr("Settings"), self)
        self.menu_file.addAction(self.action_settings)

        self.action_about = QAction(self.tr("About"), self)
        self.menu_file.addAction(self.action_about)

        self.action_exit = QAction(self.tr("&Quit"), self)
        self.action_exit.setShortcut("Ctrl+Q")
        self.menu_file.addAction(self.action_exit)

        # status bar settings
        self.statusBar().showMessage(self.tr("Ready!"))

        # connection
        self.action_new_file.triggered.connect(self.action_new_file_clicked)
        self.action_open.triggered.connect(self.action_open_clicked)
        self.action_save.triggered.connect(self.action_save_clicked)
        self.action_save_as.triggered.connect(self.action_save_as_clicked)
        self.action_export.triggered.connect(self.action_export_clicked)
        self.action_import.triggered.connect(self.action_import_clicked)
        self.action_settings.triggered.connect(self.action_settings_clicked)
        self.action_about.triggered.connect(self.action_about_clicked)
        self.action_exit.triggered.connect(self.close)

        # self.table_widget.clicked.connect(self.test)
        self.table_widget.doubleClicked.connect(self.cell_clicked)
        self.table_widget.customContextMenuRequested.connect(
            self.context_menu_requested)

    # def test(self) -> None:
    #     """
    #     Method for tests.
    #     """
    #     item = self.table_widget.currentItem()
    #     if item is not None:
    #         print(item.row(),
    #               item.column(),
    #               self.table_widget.rowSpan(item.row(), item.column()),
    #               self.table_widget.columnSpan(item.row(), item.column()))

    def changeEvent(self, event: QEvent) -> None:
        if event.type() == QEvent.LanguageChange:
            self.setWindowTitle(self.tr("Schedule Editor"))
            self.menu_file.setTitle(self.tr("&File"))
            self.action_new_file.setText(self.tr("&New file"))
            self.action_open.setText(self.tr("&Open"))
            self.action_save.setText(self.tr("&Save"))
            self.action_save_as.setText(self.tr("Save as..."))
            self.action_export.setText(self.tr("Export"))
            self.action_import.setText(self.tr("Import"))
            self.action_settings.setText(self.tr("Settings"))
            self.action_about.setText(self.tr("About"))
            self.action_exit.setText(self.tr("&Quit"))

            for i, day in enumerate(DaysOfWeek.to_list()):
                item = QTableWidgetItem(day)
                self.table_widget.setVerticalHeaderItem(i, item)

            self.table_widget.update_schedule()
        else:
            super().changeEvent(event)

    def resizeEvent(self, event: QResizeEvent) -> None:
        self.table_widget.resize_table()
        super().resizeEvent(event)

    def context_menu_requested(self, pos: QPoint) -> None:
        """
        Create a context menu to edit a table cell.

        :param pos: Menu call position
        """
        menu = QMenu(self)

        action_edit = QAction(QIcon.fromTheme("accessories-text-editor"),
                              self.tr("Edit cell"), self)
        action_edit.triggered.connect(self.cell_clicked)
        menu.addAction(action_edit)

        menu.popup(self.table_widget.viewport().mapToGlobal(pos))

    def action_new_file_clicked(self) -> bool:
        """
        Slot to handle file save, after changes.
        If the user has agreed to save / not save, then True is returned, otherwise False.
        """
        if self._schedule.is_change():
            answer = QMessageBox.warning(
                self, self.tr("The document has been modified"),
                self.tr("Do you want to save the changes you made?\n"
                        "You changes will be lost if you don't save them"),
                QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel,
                QMessageBox.Save)
            if answer == QMessageBox.Save:
                self.action_save_clicked()
            elif answer == QMessageBox.Cancel:
                return False

        self._schedule.clear()
        self.table_widget.update_schedule()
        self.setWindowTitle(self.tr("Schedule Editor"))
        return True

    def action_open_clicked(self) -> None:
        """
        Slot to handle file upload.
        """
        if self._file is not None:
            if not self.action_new_file_clicked():
                return

        path = QFileDialog.getOpenFileName(
            self, self.tr("Open schedule from JSON file"), ".",
            "JSON file (*.json)")[0]
        if path == "":
            return

        try:
            self._schedule.load(path)
        except AlongTwoPairsException as ex:
            QMessageBox.critical(self, self.tr("AlongTwoPairsException!"),
                                 str(ex))
            return
        except Exception as ex:
            QMessageBox.critical(self, self.tr("Unknown error!"), str(ex))
            return

        self._file = QFileInfo(path)
        self.statusBar().showMessage(self.tr("Load file: ") + path, 5000)
        self.setWindowTitle(
            self.tr("Schedule Editor [{}]").format(
                self._file.absoluteFilePath()))
        self.table_widget.update_schedule()

    def action_save_clicked(self) -> None:
        """
        Slot to handle file saving.
        """
        if self._file is None:
            self.action_save_as_clicked()
        else:
            self._schedule.save(self._file.absoluteFilePath())
            self.setWindowTitle(
                self.tr("Schedule Editor [{}]").format(
                    self._file.absoluteFilePath()))
            self.statusBar().showMessage(
                self.tr("Save file: ") + self._file.absoluteFilePath(), 5000)

    def action_save_as_clicked(self) -> None:
        """
        Slot to save the file if it has not been saved before.
        """
        path = QFileDialog.getSaveFileName(
            self, self.tr("Save schedule as JSON file"), "./examples",
            "JSON file (*.json)")[0]
        if path == "":
            return

        if not path.endswith(".json"):
            path += ".json"

        self._schedule.save(path)
        self._file = QFileInfo(path)
        self.setWindowTitle(
            self.tr("Schedule Editor [{}]").format(
                self._file.absoluteFilePath()))
        self.statusBar().showMessage(
            self.tr("Save file: ") + self._file.absoluteFilePath(), 5000)

    def action_export_clicked(self) -> None:
        """
        Slot for schedule exports to PDF.
         """
        exporter = ExportWindow(self._schedule, self)
        exporter.exec_()

    def action_import_clicked(self) -> None:
        """
        Slot for schedule imports from PDF.
        """
        importer = ImportWindow(self)
        importer.exec_()

    def action_settings_clicked(self) -> None:
        """
        Slot for calling up the settings window.
        """
        settings = SettingsWindow(self)
        settings.exec_()

        self.table_widget.update_schedule()

    def action_about_clicked(self) -> None:
        """
        Slot display window: "About program".
        """
        QMessageBox.information(
            self, self.tr("About program"),
            self.tr("""
                                    <b>Stankin Schedule Editor</b>
                                    <p>
                                        The project is designed to create a weekly
                                        schedule in the form of pdf-files.
                                    <p>
                                    <p>
                                        <b>Author</b>: Nick Vereshchagin<br>
                                        <b>GitHub</b>:
                                        <a href='https://github.com/Nikololoshka/StankinScheduleEditor'>
                                            https://github.com/Nikololoshka/StankinScheduleEditor
                                        </a>
                                    </p>
                                """))

    def cell_clicked(self) -> None:
        """
        Processes the action to change a table cell.
        """
        day = self.table_widget.currentRow()
        number = self.table_widget.currentColumn()

        item = self.table_widget.currentItem()
        if item is not None:
            duration = item.data(Qt.UserRole)
        else:
            duration = self.table_widget.columnSpan(day, number)

        day, number, duration = self._schedule.normalize_index(
            day, number, duration)

        selector = PairSelectorWindow(self._schedule, day, number, duration,
                                      self)
        selector.pairsListChanged.connect(self.table_widget.update_schedule)
        selector.exec_()

        self.table_widget.update_schedule()
def import_from_pdf(process_id, manager: ImportManager) -> None:
    tesseract = TesseractWrapper(tesseract_path=manager.tesseract_path)
    while not manager.queue.empty():
        try:
            file_path = manager.queue.get(True, 1)
            file = QFileInfo(file_path)

            # convert from pdf to PIL image
            img_pdf = pdf2image.convert_from_path(
                file.absoluteFilePath(),
                dpi=manager.dpi,
                poppler_path=manager.poppler_path)
            img_pdf = img_pdf[0].convert('RGB')

            # convert to NumPy array
            img = np.array(img_pdf)
            img = img[:, :, ::-1].copy()

            # set mask
            grey = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
            thresh = cv.threshold(grey, 127, 255, 0)[1]

            if manager.flags["stop"]:
                break

            # found contours
            contours = cv.findContours(thresh, cv.RETR_TREE,
                                       cv.CHAIN_APPROX_SIMPLE)[0]

            height, width = img.shape[:2]
            # max and min area of rect
            max_area = height * width / 20
            min_area = max_area / 40

            cells = []
            time_cells = dict()

            contours_number = len(contours)
            number = 0

            for k, contour in enumerate(contours, 1):
                rect = cv.minAreaRect(contour)
                area = int(rect[1][0] * rect[1][1])
                if min_area < area < max_area:
                    if manager.flags["stop"]:
                        break

                    x, y, w, h = cv.boundingRect(contour)
                    crop_img = img[int(y):int(y + h), int(x):int(x + w)]

                    txt = tesseract.to_string(crop_img)

                    found = False
                    for i in range(8):
                        if TimePair.time_starts(
                        )[i] in txt and TimePair.time_ends()[i]:
                            time_cells[i] = (x, x + w)
                            found = True
                            break

                    if not found:
                        cells.append((x, x + w, " ".join(txt.split())))

                    # draw debug rect with number
                    if manager.debug_image:
                        box = cv.boxPoints(rect)
                        box = np.int0(box)
                        blue_color = (255, 0, 0)
                        center = (int(rect[0][0]), int(rect[0][1]))

                        cv.drawContours(img, [box], 0, blue_color, 2)
                        cv.putText(img, str(number),
                                   (center[0] - 100, center[1] - 40),
                                   cv.FONT_HERSHEY_SIMPLEX, 3, blue_color, 12)

                    number += 1
                    process = int(k / contours_number * 70)
                    manager.progress_value_list[process_id] = process
                    manager.progress_text_list[process_id] = "{} {}%".format(
                        file.baseName(), process)
            if manager.debug_image:
                cv.imwrite(file_path[0:-4] + "-debug.jpg", img)

            if manager.flags["stop"]:
                break

            schedule = Schedule()
            cells_number = len(cells)

            for k, cell in enumerate(cells):
                if manager.flags["stop"]:
                    break

                start_x, end_x, text = cell
                first_start_time, first_end_time = time_cells[0]

                if not abs(end_x - first_start_time) < abs(start_x -
                                                           first_start_time):
                    text = "\n".join(re.findall(r".*?\]", text))
                    while True:
                        try:
                            pairs = parse_pair(manager, text)
                            break
                        except InvalidDatePair as ex:
                            text = confuse_loop(
                                process_id, manager,
                                ConfuseSituationException(
                                    file.absoluteFilePath(),
                                    text,
                                    confuse=str(ex)))
                        except ConfuseSituationException as ex:
                            ex.filename = file.absoluteFilePath(
                            )[0:-4] + "-debug.jpg"
                            ex.cell = k
                            ex.context = text
                            if ex.maybe_answer == "":
                                ex.maybe_answer = text

                            text = confuse_loop(process_id, manager, ex)

                    if len(pairs) != 0:
                        diff_start = abs(start_x - first_start_time)
                        diff_end = abs(end_x - first_end_time)
                        start, end = 0, 0

                        for number, (start_time,
                                     end_time) in time_cells.items():
                            diff = abs(start_x - start_time)
                            if diff < diff_start:
                                diff_start = diff
                                start = number

                            diff = abs(end_x - end_time)
                            if diff < diff_end:
                                diff_end = diff
                                end = number

                        for pair in pairs:
                            pair["time"].set_time(
                                TimePair.time_starts()[start],
                                TimePair.time_ends()[end])
                            schedule.add_pair(pair)

                    process = int(70 + k / cells_number * 30)
                    manager.progress_value_list[process_id] = process
                    manager.progress_text_list[process_id] = "{} {}%".format(
                        file.baseName(), process)

            schedule.save(file.absoluteFilePath()[0:-4] + ".json")
            print(file.absoluteFilePath()[0:-4] + ".json")

            if manager.flags["stop"]:
                break

            if manager.weekly:
                export_weeks_to_pdf(
                    schedule, file.baseName(), True,
                    file.absoluteFilePath()[0:-4] + "-weekly.pdf",
                    manager.font_name, manager.font_path, manager.encoding,
                    manager.start, manager.end, manager.color_a,
                    manager.color_b)

            if manager.full:
                export_full_to_pdf(schedule, file.baseName(),
                                   file.absoluteFilePath()[0:-4] + "-full.pdf",
                                   manager.font_name, manager.font_path,
                                   manager.encoding)

        except Exception as ex:
            print("Exception, process:", process_id, "is:", ex)
            traceback.print_exc()

    manager.progress_value_list[process_id] = 100
    manager.progress_text_list[process_id] = "Work complete"