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"