def create_clickable_dock_widget(widget: QDockWidget): class Filter(QObject): clicked = pyqtSignal(QObject) def eventFilter(self, obj: QObject, event: QEvent): # print('obj: %s, event: %s' % (obj, event)) if obj == widget or obj == widget.widget(): if event.type() == QEvent.MouseButtonRelease: self.clicked.emit(obj, widget) return True return False _filter = Filter(widget) widget.installEventFilter(_filter) return _filter.clicked
class MainWindow(QMainWindow): """Pyspread main window :application: QApplication :args: Command line arguments object from argparse :unit_test: If True then the application runs in unit_test mode :type unit_test: bool, defaults to False """ gui_update = pyqtSignal(dict) def __init__(self, application, args, unit_test=False): super().__init__() self._loading = True self.application = application self.unit_test = unit_test self.settings = Settings(self) self.workflows = Workflows(self) self.undo_stack = QUndoStack(self) self.refresh_timer = QTimer() self._init_widgets() self.main_window_actions = MainWindowActions(self) self._init_window() self._init_toolbars() self.settings.restore() if self.settings.signature_key is None: self.settings.signature_key = genkey() # Update recent files in the file menu self.menuBar().file_menu.history_submenu.update() if not self.unit_test: self.show() self._update_action_toggles() # Update the GUI so that everything matches the model cell_attributes = self.grid.model.code_array.cell_attributes attributes = cell_attributes[self.grid.current] self.on_gui_update(attributes) self._loading = False self._previous_window_state = self.windowState() # Open initial file if provided by the command line if args.file is not None: if self.workflows.filepath_open(args.file): self.workflows.update_main_window_title() else: msg = "File '{}' could not be opened.".format(args.file) self.statusBar().showMessage(msg) def _init_window(self): """Initialize main window components""" self.setWindowTitle(APP_NAME) self.setWindowIcon(Icon.pyspread) self.safe_mode_widget = QSvgWidget(str(IconPath.warning), self) msg = "%s is in safe mode.\nExpressions are not evaluated." % APP_NAME self.safe_mode_widget.setToolTip(msg) self.statusBar().addPermanentWidget(self.safe_mode_widget) self.safe_mode_widget.hide() # Disable the approve fiel menu button self.main_window_actions.approve.setEnabled(False) self.setMenuBar(MenuBar(self)) def resizeEvent(self, event): super(MainWindow, self).resizeEvent(event) if self._loading: return def closeEvent(self, event=None): """Overloaded close event, allows saving changes or canceling close""" if event: event.ignore() self.workflows.file_quit() # has @handle_changed_since_save decorator def _init_widgets(self): """Initialize widgets""" self.widgets = Widgets(self) self.entry_line = Entryline(self) self.grid = Grid(self) self.macro_panel = MacroPanel(self, self.grid.model.code_array) self.main_splitter = QSplitter(Qt.Vertical, self) self.setCentralWidget(self.main_splitter) self.main_splitter.addWidget(self.entry_line) self.main_splitter.addWidget(self.grid) self.main_splitter.addWidget(self.grid.table_choice) self.main_splitter.setSizes( [self.entry_line.minimumHeight(), 9999, 20]) self.macro_dock = QDockWidget("Macros", self) self.macro_dock.setObjectName("Macro Panel") self.macro_dock.setWidget(self.macro_panel) self.addDockWidget(Qt.RightDockWidgetArea, self.macro_dock) self.macro_dock.installEventFilter(self) self.gui_update.connect(self.on_gui_update) self.refresh_timer.timeout.connect(self.on_refresh_timer) def eventFilter(self, source, event): """Event filter for handling QDockWidget close events Updates the menu if the macro panel is closed. """ if event.type() == QEvent.Close \ and isinstance(source, QDockWidget) \ and source.windowTitle() == "Macros": self.main_window_actions.toggle_macro_panel.setChecked(False) return super().eventFilter(source, event) def _init_toolbars(self): """Initialize the main window toolbars""" self.main_toolbar = MainToolBar(self) self.macro_toolbar = MacroToolbar(self) self.find_toolbar = FindToolbar(self) self.format_toolbar = FormatToolbar(self) self.addToolBar(self.main_toolbar) self.addToolBar(self.macro_toolbar) self.addToolBar(self.find_toolbar) self.addToolBarBreak() self.addToolBar(self.format_toolbar) def _update_action_toggles(self): """Updates the toggle menu check states""" self.main_window_actions.toggle_main_toolbar.setChecked( self.main_toolbar.isVisible()) self.main_window_actions.toggle_macro_toolbar.setChecked( self.macro_toolbar.isVisible()) self.main_window_actions.toggle_format_toolbar.setChecked( self.format_toolbar.isVisible()) self.main_window_actions.toggle_find_toolbar.setChecked( self.find_toolbar.isVisible()) self.main_window_actions.toggle_entry_line.setChecked( self.entry_line.isVisible()) self.main_window_actions.toggle_macro_panel.setChecked( self.macro_dock.isVisible()) @property def safe_mode(self): """Returns safe_mode state. In safe_mode cells are not evaluated.""" return self.grid.model.code_array.safe_mode @safe_mode.setter def safe_mode(self, value): """Sets safe mode. This triggers the safe_mode icon in the statusbar. If safe_mode changes from True to False then caches are cleared and macros are executed. """ if self.grid.model.code_array.safe_mode == bool(value): return self.grid.model.code_array.safe_mode = bool(value) if value: # Safe mode entered self.safe_mode_widget.show() # Enable approval menu entry self.main_window_actions.approve.setEnabled(True) else: # Safe_mode disabled self.safe_mode_widget.hide() # Disable approval menu entry self.main_window_actions.approve.setEnabled(False) # Clear result cache self.grid.model.code_array.result_cache.clear() # Execute macros self.macro_panel.on_apply() def on_print(self): """Print event handler""" # Create printer printer = QPrinter(mode=QPrinter.HighResolution) # Get print area self.print_area = PrintAreaDialog(self, self.grid).area if self.print_area is None: return # Create print dialog dialog = QPrintDialog(printer, self) if dialog.exec_() == QPrintDialog.Accepted: self.on_paint_request(printer) def on_preview(self): """Print preview event handler""" # Create printer printer = QPrinter(mode=QPrinter.HighResolution) # Get print area self.print_area = PrintAreaDialog(self, self.grid).area if self.print_area is None: return # Create print preview dialog dialog = PrintPreviewDialog(printer) dialog.paintRequested.connect(self.on_paint_request) dialog.exec_() def on_paint_request(self, printer): """Paints to printer""" painter = QPainter(printer) option = QStyleOptionViewItem() painter.setRenderHints(QPainter.SmoothPixmapTransform | QPainter.SmoothPixmapTransform) page_rect = printer.pageRect() rows = list(self.workflows.get_paint_rows(self.print_area)) columns = list(self.workflows.get_paint_columns(self.print_area)) if not rows or not columns: return zeroidx = self.grid.model.index(0, 0) zeroidx_rect = self.grid.visualRect(zeroidx) minidx = self.grid.model.index(min(rows), min(columns)) minidx_rect = self.grid.visualRect(minidx) maxidx = self.grid.model.index(max(rows), max(columns)) maxidx_rect = self.grid.visualRect(maxidx) grid_width = maxidx_rect.x() + maxidx_rect.width() - minidx_rect.x() grid_height = maxidx_rect.y() + maxidx_rect.height() - minidx_rect.y() grid_rect = QRectF(minidx_rect.x() - zeroidx_rect.x(), minidx_rect.y() - zeroidx_rect.y(), grid_width, grid_height) self.settings.print_zoom = min(page_rect.width() / grid_width, page_rect.height() / grid_height) with self.grid.delegate.painter_save(painter): painter.scale(self.settings.print_zoom, self.settings.print_zoom) # Translate so that the grid starts at upper left paper edge painter.translate(zeroidx_rect.x() - minidx_rect.x(), zeroidx_rect.y() - minidx_rect.y()) # Draw grid cells self.workflows.paint(painter, option, grid_rect, rows, columns) self.settings.print_zoom = None def on_fullscreen(self): """Fullscreen toggle event handler""" if self.windowState() == Qt.WindowFullScreen: self.setWindowState(self._previous_window_state) else: self._previous_window_state = self.windowState() self.setWindowState(Qt.WindowFullScreen) def on_approve(self): """Approve event handler""" if ApproveWarningDialog(self).choice: self.safe_mode = False def on_clear_globals(self): """Clear globals event handler""" self.grid.model.code_array.result_cache.clear() # Clear globals self.grid.model.code_array.clear_globals() self.grid.model.code_array.reload_modules() def on_preferences(self): """Preferences event handler (:class:`dialogs.PreferencesDialog`) """ data = PreferencesDialog(self).data if data is not None: max_file_history_changed = \ self.settings.max_file_history != data['max_file_history'] # Dialog has been approved --> Store data to settings for key in data: if key == "signature_key" and not data[key]: data[key] = genkey() self.settings.__setattr__(key, data[key]) # Immediately adjust file history in menu if max_file_history_changed: self.menuBar().file_menu.history_submenu.update() def on_dependencies(self): """Dependancies installer (:class:`installer.InstallerDialog`) """ dial = DependenciesDialog(self) dial.exec_() def on_undo(self): """Undo event handler""" self.undo_stack.undo() def on_redo(self): """Undo event handler""" self.undo_stack.redo() def on_toggle_refresh_timer(self, toggled): """Toggles periodic timer for frozen cells""" if toggled: self.refresh_timer.start(self.settings.refresh_timeout) else: self.refresh_timer.stop() def on_refresh_timer(self): """Event handler for self.refresh_timer.timeout Called for periodic updates of frozen cells. Does nothing if either the entry_line or a cell editor is active. """ if not self.entry_line.hasFocus() \ and self.grid.state() != self.grid.EditingState: self.grid.refresh_frozen_cells() def _toggle_widget(self, widget, action_name, toggled): """Toggles widget visibility and updates toggle actions""" if toggled: widget.show() else: widget.hide() self.main_window_actions[action_name].setChecked(widget.isVisible()) def on_toggle_main_toolbar(self, toggled): """Main toolbar toggle event handler""" self._toggle_widget(self.main_toolbar, "toggle_main_toolbar", toggled) def on_toggle_macro_toolbar(self, toggled): """Macro toolbar toggle event handler""" self._toggle_widget(self.macro_toolbar, "toggle_macro_toolbar", toggled) def on_toggle_format_toolbar(self, toggled): """Format toolbar toggle event handler""" self._toggle_widget(self.format_toolbar, "toggle_format_toolbar", toggled) def on_toggle_find_toolbar(self, toggled): """Find toolbar toggle event handler""" self._toggle_widget(self.find_toolbar, "toggle_find_toolbar", toggled) def on_toggle_entry_line(self, toggled): """Entryline toggle event handler""" self._toggle_widget(self.entry_line, "toggle_entry_line", toggled) def on_toggle_macro_panel(self, toggled): """Macro panel toggle event handler""" self._toggle_widget(self.macro_dock, "toggle_macro_panel", toggled) def on_manual(self): """Show manual browser""" dialog = ManualDialog(self) dialog.show() def on_tutorial(self): """Show tutorial browser""" dialog = TutorialDialog(self) dialog.show() def on_about(self): """Show about message box""" about_msg_template = "<p>".join(( "<b>%s</b>" % APP_NAME, "A non-traditional Python spreadsheet application", "Version {version}", "Created by:<br>{devs}", "Documented by:<br>{doc_devs}", "Copyright:<br>Martin Manns", "License:<br>{license}", '<a href="https://pyspread.gitlab.io">pyspread.gitlab.io</a>', )) devs = "Martin Manns, Jason Sexauer<br>Vova Kolobok, mgunyho, " \ "Pete Morgan" doc_devs = "Martin Manns, Bosko Markovic, Pete Morgan" about_msg = about_msg_template.format(version=VERSION, license=LICENSE, devs=devs, doc_devs=doc_devs) QMessageBox.about(self, "About %s" % APP_NAME, about_msg) def on_gui_update(self, attributes): """GUI update event handler. Emitted on cell change. Attributes contains current cell_attributes. """ widgets = self.widgets menubar = self.menuBar() is_bold = attributes["fontweight"] == QFont.Bold self.main_window_actions.bold.setChecked(is_bold) is_italic = attributes["fontstyle"] == QFont.StyleItalic self.main_window_actions.italics.setChecked(is_italic) underline_action = self.main_window_actions.underline underline_action.setChecked(attributes["underline"]) strikethrough_action = self.main_window_actions.strikethrough strikethrough_action.setChecked(attributes["strikethrough"]) renderer = attributes["renderer"] widgets.renderer_button.set_current_action(renderer) widgets.renderer_button.set_menu_checked(renderer) freeze_action = self.main_window_actions.freeze_cell freeze_action.setChecked(attributes["frozen"]) lock_action = self.main_window_actions.lock_cell lock_action.setChecked(attributes["locked"]) self.entry_line.setReadOnly(attributes["locked"]) button_action = self.main_window_actions.button_cell button_action.setChecked(attributes["button_cell"] is not False) rotation = "rotate_{angle}".format(angle=int(attributes["angle"])) widgets.rotate_button.set_current_action(rotation) widgets.rotate_button.set_menu_checked(rotation) widgets.justify_button.set_current_action(attributes["justification"]) widgets.justify_button.set_menu_checked(attributes["justification"]) widgets.align_button.set_current_action(attributes["vertical_align"]) widgets.align_button.set_menu_checked(attributes["vertical_align"]) border_action = self.main_window_actions.border_group.checkedAction() if border_action is not None: icon = border_action.icon() menubar.format_menu.border_submenu.setIcon(icon) self.format_toolbar.border_menu_button.setIcon(icon) border_width_action = \ self.main_window_actions.border_width_group.checkedAction() if border_width_action is not None: icon = border_width_action.icon() menubar.format_menu.line_width_submenu.setIcon(icon) self.format_toolbar.line_width_button.setIcon(icon) if attributes["textcolor"] is None: text_color = self.grid.palette().color(QPalette.Text) else: text_color = QColor(*attributes["textcolor"]) widgets.text_color_button.color = text_color if attributes["bgcolor"] is None: bgcolor = self.grid.palette().color(QPalette.Base) else: bgcolor = QColor(*attributes["bgcolor"]) widgets.background_color_button.color = bgcolor if attributes["textfont"] is None: widgets.font_combo.font = QFont().family() else: widgets.font_combo.font = attributes["textfont"] widgets.font_size_combo.size = attributes["pointsize"] merge_cells_action = self.main_window_actions.merge_cells merge_cells_action.setChecked(attributes["merge_area"] is not None)
def __init__(self, name, content, plot_labels, plot_name, plot_worker, plot_type, layout, *args, parent=None, **kwargs): """ Initialisation of the PlotContainer widget. content - Content for the plotcontainer plot_lables - Labels of the plot widget plot_name - Name of the associated software parent - Parent widget (default None) Returns: None """ super(PlotContainer, self).__init__(parent) self.parent_layout = layout self.parent = parent self.setCentralWidget(None) self.setTabPosition(Qt.TopDockWidgetArea, QTabWidget.North) self.plot_name = plot_name self.name = name self.worker = plot_worker self.content = [] self.dock_widgets = [] if self.name == 'Overview': plot_labels = [[content]] for label in plot_labels: label = label[0] if label == 'mic_number': continue elif label == 'file_name': continue elif label == 'image' and content != 'image': continue elif label != 'image' and content == 'image': continue elif label == 'object': continue else: pass dock_widget = QDockWidget(label, self) custom_title = QWidget(dock_widget) layout_custom_title = QHBoxLayout(custom_title) layout_custom_title.setContentsMargins(0, 0, 0, 0) layout_custom_title.addWidget( QLabel('{} - {}'.format(self.plot_name, label), dock_widget)) layout_custom_title.addStretch(1) button = QPushButton(dock_widget) button.my_docker = dock_widget button.setStyleSheet( 'color: rgba(0, 0, 0 ,0); background-color: rgba(0, 0, 0, 0)') icon = dock_widget.style().standardIcon( QStyle.SP_TitleBarNormalButton) button.setIcon(icon) button.clicked.connect(self.set_floating) layout_custom_title.addWidget(button) dock_widget.setTitleBarWidget(custom_title) if label == 'overview': widget = TwinContainer(dock_widget=dock_widget, parent=self) else: if label == 'image': twin_container = None else: twin_container = self.parent.content[layout].widget( 0).content[0] widget = PlotWidget(label=label, plot_typ=content, dock_widget=dock_widget, twin_container=twin_container, parent=self) self.content.append(widget) dock_widget.setWidget(widget) dock_widget.installEventFilter(self) dock_widget.setFeatures(QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetMovable) self.dock_widgets.append(dock_widget) self.addDockWidget(Qt.BottomDockWidgetArea, dock_widget, Qt.Horizontal) for idx in range(1, len(self.dock_widgets)): self.tabifyDockWidget(self.dock_widgets[0], self.dock_widgets[idx]) self.tabifiedDockWidgetActivated.connect(self.synchronize_tabs) self.parent.content[self.parent_layout].enable_tab(False) self._is_visible = False
class MainWindow(QMainWindow): """Pyspread main window""" gui_update = pyqtSignal(dict) def __init__(self, application): super().__init__() self._loading = True self.application = application self.settings = Settings(self) self.workflows = Workflows(self) self.undo_stack = QUndoStack(self) self.refresh_timer = QTimer() self._init_widgets() self.main_window_actions = MainWindowActions(self) self._init_window() self._init_toolbars() self.settings.restore() if self.settings.signature_key is None: self.settings.signature_key = genkey() self.show() self._update_action_toggles() # Update the GUI so that everything matches the model cell_attributes = self.grid.model.code_array.cell_attributes attributes = cell_attributes[self.grid.current] self.on_gui_update(attributes) self._loading = False self._previous_window_state = self.windowState() def _init_window(self): """Initialize main window components""" self.setWindowTitle(APP_NAME) self.setWindowIcon(Icon.pyspread) self.safe_mode_widget = QSvgWidget(str(IconPath.warning), self) msg = "%s is in safe mode.\nExpressions are not evaluated." % APP_NAME self.safe_mode_widget.setToolTip(msg) self.statusBar().addPermanentWidget(self.safe_mode_widget) self.safe_mode_widget.hide() self.setMenuBar(MenuBar(self)) def resizeEvent(self, event): super(MainWindow, self).resizeEvent(event) if self._loading: return def closeEvent(self, event=None): """Overloaded close event, allows saving changes or canceling close""" self.workflows.file_quit() # has @handle_changed_since_save decorator self.settings.save() if event: event.ignore() # Maybe a warn of closing sys.exit() def _init_widgets(self): """Initialize widgets""" self.widgets = Widgets(self) self.entry_line = Entryline(self) self.grid = Grid(self) self.macro_panel = MacroPanel(self, self.grid.model.code_array) self.main_splitter = QSplitter(Qt.Vertical, self) self.setCentralWidget(self.main_splitter) self.main_splitter.addWidget(self.entry_line) self.main_splitter.addWidget(self.grid) self.main_splitter.addWidget(self.grid.table_choice) self.main_splitter.setSizes([self.entry_line.minimumHeight(), 9999, 20]) self.macro_dock = QDockWidget("Macros", self) self.macro_dock.setObjectName("Macro Panel") self.macro_dock.setWidget(self.macro_panel) self.addDockWidget(Qt.RightDockWidgetArea, self.macro_dock) self.macro_dock.installEventFilter(self) self.gui_update.connect(self.on_gui_update) self.refresh_timer.timeout.connect(self.on_refresh_timer) def eventFilter(self, source, event): """Event filter for handling QDockWidget close events Updates the menu if the macro panel is closed. """ if event.type() == QEvent.Close \ and isinstance(source, QDockWidget) \ and source.windowTitle() == "Macros": self.main_window_actions.toggle_macro_panel.setChecked(False) return super().eventFilter(source, event) def _init_toolbars(self): """Initialize the main window toolbars""" self.main_toolbar = MainToolBar(self) self.find_toolbar = FindToolbar(self) self.format_toolbar = FormatToolbar(self) self.macro_toolbar = MacroToolbar(self) self.widget_toolbar = WidgetToolbar(self) self.addToolBar(self.main_toolbar) self.addToolBar(self.find_toolbar) self.addToolBarBreak() self.addToolBar(self.format_toolbar) self.addToolBar(self.macro_toolbar) self.addToolBar(self.widget_toolbar) def _update_action_toggles(self): """Updates the toggle menu check states""" self.main_window_actions.toggle_main_toolbar.setChecked( self.main_toolbar.isVisible()) self.main_window_actions.toggle_macro_toolbar.setChecked( self.macro_toolbar.isVisible()) self.main_window_actions.toggle_widget_toolbar.setChecked( self.widget_toolbar.isVisible()) self.main_window_actions.toggle_format_toolbar.setChecked( self.format_toolbar.isVisible()) self.main_window_actions.toggle_find_toolbar.setChecked( self.find_toolbar.isVisible()) self.main_window_actions.toggle_entry_line.setChecked( self.entry_line.isVisible()) self.main_window_actions.toggle_macro_panel.setChecked( self.macro_dock.isVisible()) @property def safe_mode(self): """Returns safe_mode state. In safe_mode cells are not evaluated.""" return self.grid.model.code_array.safe_mode @safe_mode.setter def safe_mode(self, value): """Sets safe mode. This triggers the safe_mode icon in the statusbar. If safe_mode changes from True to False then caches are cleared and macros are executed. """ if self.grid.model.code_array.safe_mode == bool(value): return self.grid.model.code_array.safe_mode = bool(value) if value: # Safe mode entered self.safe_mode_widget.show() else: # Safe_mode disabled self.safe_mode_widget.hide() # Clear result cache self.grid.model.code_array.result_cache.clear() # Execute macros self.grid.model.code_array.execute_macros() def on_nothing(self): """Dummy action that does nothing""" sender = self.sender() print("on_nothing > ", sender.text(), sender) def on_fullscreen(self): """Fullscreen toggle event handler""" if self.windowState() == Qt.WindowFullScreen: self.setWindowState(self._previous_window_state) else: self._previous_window_state = self.windowState() self.setWindowState(Qt.WindowFullScreen) def on_approve(self): """Approve event handler""" if ApproveWarningDialog(self).choice: self.safe_mode = False def on_preferences(self): """Preferences event handler (:class:`dialogs.PreferencesDialog`) """ data = PreferencesDialog(self).data if data is not None: # Dialog has not been approved --> Store data to settings for key in data: if key == "signature_key" and not data[key]: data[key] = genkey() self.settings.__setattr__(key, data[key]) def on_undo(self): """Undo event handler""" self.undo_stack.undo() def on_redo(self): """Undo event handler""" self.undo_stack.redo() def on_toggle_refresh_timer(self, toggled): """Toggles periodic timer for frozen cells""" if toggled: self.refresh_timer.start(self.settings.refresh_timeout) else: self.refresh_timer.stop() def on_refresh_timer(self): """Event handler for self.refresh_timer.timeout Called for periodic updates of frozen cells. Does nothing if either the entry_line or a cell editor is active. """ if not self.entry_line.hasFocus() \ and self.grid.state() != self.grid.EditingState: self.grid.refresh_frozen_cells() def _toggle_widget(self, widget, action_name, toggled): """Toggles widget visibility and updates toggle actions""" if toggled: widget.show() else: widget.hide() self.main_window_actions[action_name].setChecked(widget.isVisible()) def on_toggle_main_toolbar(self, toggled): """Main toolbar toggle event handler""" self._toggle_widget(self.main_toolbar, "toggle_main_toolbar", toggled) def on_toggle_macro_toolbar(self, toggled): """Macro toolbar toggle event handler""" self._toggle_widget(self.macro_toolbar, "toggle_macro_toolbar", toggled) def on_toggle_widget_toolbar(self, toggled): """Widget toolbar toggle event handler""" self._toggle_widget(self.widget_toolbar, "toggle_widget_toolbar", toggled) def on_toggle_format_toolbar(self, toggled): """Format toolbar toggle event handler""" self._toggle_widget(self.format_toolbar, "toggle_format_toolbar", toggled) def on_toggle_find_toolbar(self, toggled): """Find toolbar toggle event handler""" self._toggle_widget(self.find_toolbar, "toggle_find_toolbar", toggled) def on_toggle_entry_line(self, toggled): """Entryline toggle event handler""" self._toggle_widget(self.entry_line, "toggle_entry_line", toggled) def on_toggle_macro_panel(self, toggled): """Macro panel toggle event handler""" self._toggle_widget(self.macro_dock, "toggle_macro_panel", toggled) def on_about(self): """Show about message box""" about_msg_template = "<p>".join(( "<b>%s</b>" % APP_NAME, "A non-traditional Python spreadsheet application", "Version {version}", "Created by:<br>{devs}", "Documented by:<br>{doc_devs}", "Copyright:<br>Martin Manns", "License:<br>{license}", '<a href="https://pyspread.gitlab.io">pyspread.gitlab.io</a>', )) devs = "Martin Manns, Jason Sexauer<br>Vova Kolobok, mgunyho, " \ "Pete Morgan" doc_devs = "Martin Manns, Bosko Markovic, Pete Morgan" about_msg = about_msg_template.format( version=VERSION, license=LICENSE, devs=devs, doc_devs=doc_devs) QMessageBox.about(self, "About %s" % APP_NAME, about_msg) def on_gui_update(self, attributes): """GUI update event handler. Emitted on cell change. Attributes contains current cell_attributes. """ widgets = self.widgets is_bold = attributes["fontweight"] == QFont.Bold self.main_window_actions.bold.setChecked(is_bold) is_italic = attributes["fontstyle"] == QFont.StyleItalic self.main_window_actions.italics.setChecked(is_italic) underline_action = self.main_window_actions.underline underline_action.setChecked(attributes["underline"]) strikethrough_action = self.main_window_actions.strikethrough strikethrough_action.setChecked(attributes["strikethrough"]) renderer = attributes["renderer"] widgets.renderer_button.set_current_action(renderer) widgets.renderer_button.set_menu_checked(renderer) freeze_action = self.main_window_actions.freeze_cell freeze_action.setChecked(attributes["frozen"]) lock_action = self.main_window_actions.lock_cell lock_action.setChecked(attributes["locked"]) self.entry_line.setReadOnly(attributes["locked"]) rotation = "rotate_{angle}".format(angle=int(attributes["angle"])) widgets.rotate_button.set_current_action(rotation) widgets.rotate_button.set_menu_checked(rotation) widgets.justify_button.set_current_action(attributes["justification"]) widgets.justify_button.set_menu_checked(attributes["justification"]) widgets.align_button.set_current_action(attributes["vertical_align"]) widgets.align_button.set_menu_checked(attributes["vertical_align"]) border_action = self.main_window_actions.border_group.checkedAction() if border_action is not None: icon = border_action.icon() self.menuBar().border_submenu.setIcon(icon) self.format_toolbar.border_menu_button.setIcon(icon) border_width_action = \ self.main_window_actions.border_width_group.checkedAction() if border_width_action is not None: icon = border_width_action.icon() self.menuBar().line_width_submenu.setIcon(icon) self.format_toolbar.line_width_button.setIcon(icon) if attributes["textcolor"] is None: text_color = self.grid.palette().color(QPalette.Text) else: text_color = QColor(*attributes["textcolor"]) widgets.text_color_button.color = text_color if attributes["bgcolor"] is None: bgcolor = self.grid.palette().color(QPalette.Base) else: bgcolor = QColor(*attributes["bgcolor"]) widgets.background_color_button.color = bgcolor if attributes["textfont"] is None: widgets.font_combo.font = QFont().family() else: widgets.font_combo.font = attributes["textfont"] widgets.font_size_combo.size = attributes["pointsize"] merge_cells_action = self.main_window_actions.merge_cells merge_cells_action.setChecked(attributes["merge_area"] is not None)
class InvoiceX(QMainWindow): def __init__(self): super().__init__() self.mainWindowLeft = 300 self.mainWindowTop = 300 self.mainWindowWidth = 680 self.mainWindowHeight = 480 self.fileLoaded = False self.dialog = None self.initUI() def initUI(self): # StatusBar self.statusBar() self.setStatusTip('Select a PDF to get started') self.set_menu_bar() self.set_dockview_fields() self.set_center_widget() self.set_toolbar() self.setGeometry(self.mainWindowLeft, self.mainWindowTop, self.mainWindowWidth, self.mainWindowHeight) self.setWindowTitle('Invoice-X') self.setWindowIcon( QIcon(os.path.join(os.path.dirname(__file__), 'icons/logo.ico'))) self.show() if not spawn.find_executable('convert'): QMessageBox.critical(self, 'Import Error', "Imagemagick is not installed", QMessageBox.Ok) self.close() if sys.platform[:3] == 'win': if not spawn.find_executable('magick'): QMessageBox.critical( self, 'Import Error', "Imagemagick and GhostScript are not installed", QMessageBox.Ok) self.close() def set_toolbar(self): toolbar = self.addToolBar('File') toolbar.addAction(self.openFile) toolbar.addAction(self.saveFile) toolbar.addAction(self.validateMetadata) toolbar.addAction(self.editFields) def set_center_widget(self): self.square = QLabel(self) self.square.setAlignment(Qt.AlignCenter) self.setCentralWidget(self.square) def set_dockview_fields(self): self.fields = QDockWidget("Fields", self) self.fields.installEventFilter(self) self.fieldsQWidget = QWidget() self.fieldsScrollArea = QScrollArea() self.fieldsScrollArea.setWidgetResizable(True) self.fieldsScrollArea.setWidget(self.fieldsQWidget) self.layout = QGridLayout() self.fieldsQWidget.setLayout(self.layout) self.fields.setWidget(self.fieldsScrollArea) self.fields.setFloating(False) self.fields.setMinimumWidth(360) self.fields.setStyleSheet("QWidget { background-color: #AAB2BD}") self.addDockWidget(Qt.RightDockWidgetArea, self.fields) def set_menu_bar(self): self.exitAct = QAction( QIcon(os.path.join(os.path.dirname(__file__), 'icons/exit.png')), 'Exit', self) self.exitAct.setShortcut('Ctrl+Q') self.exitAct.setStatusTip('Exit application') self.exitAct.triggered.connect(self.close) self.openFile = QAction( QIcon(os.path.join(os.path.dirname(__file__), 'icons/pdf.png')), 'Open', self) self.openFile.setShortcut('Ctrl+O') self.openFile.setStatusTip('Open new File') self.openFile.triggered.connect(self.show_file_dialog) self.saveFile = QAction( QIcon(os.path.join(os.path.dirname(__file__), 'icons/save.png')), 'Save', self) self.saveFile.setShortcut('Ctrl+S') self.saveFile.setStatusTip('Save File') self.saveFile.triggered.connect(self.save_file_dialog) self.saveAsFile = QAction('Save As', self) self.saveAsFile.setStatusTip('Save File as a new File') self.saveAsFile.triggered.connect(self.show_save_as_dialog) self.viewDock = QAction('View Fields', self, checkable=True) self.viewDock.setStatusTip('View Fields') self.viewDock.setChecked(True) self.viewDock.triggered.connect(self.view_dock_field_toggle) extractFields = QAction('Extract Fields', self) extractFields.setStatusTip('Extract Fields from PDF and add to XML') extractFields.triggered.connect(self.extract_fields_from_pdf) jsonFormat = QAction('JSON', self) jsonFormat.setStatusTip('Export file to JSON') jsonFormat.triggered.connect(lambda: self.export_fields('json')) xmlFormat = QAction('XML', self) xmlFormat.setStatusTip('Export file to XML') xmlFormat.triggered.connect(lambda: self.export_fields('xml')) ymlFormat = QAction('YML', self) ymlFormat.setStatusTip('Export file to YML') ymlFormat.triggered.connect(lambda: self.export_fields('yml')) self.validateMetadata = QAction( QIcon(os.path.join(os.path.dirname(__file__), 'icons/validate.png')), 'Validate', self) self.validateMetadata.setStatusTip('Validate XML') self.validateMetadata.triggered.connect(self.validate_xml) addMetadata = QAction('Add Metadata', self) addMetadata.setStatusTip('Add metadata to PDF') self.editFields = QAction( QIcon(os.path.join(os.path.dirname(__file__), 'icons/edit.png')), 'Edit Metadata', self) self.editFields.setStatusTip('Edit Metadata in XML') self.editFields.triggered.connect(self.edit_fields_dialog) documentation = QAction('Documentation', self) documentation.setStatusTip('Open Documentation for Invoice-X') documentation.triggered.connect(self.documentation_menubar) aboutApp = QAction('About', self) aboutApp.setStatusTip('Know about Invoice-X') aboutApp.triggered.connect(self.about_app_menubar) menubar = self.menuBar() fileMenu = menubar.addMenu('&File') fileMenu.addAction(self.openFile) fileMenu.addAction(self.saveFile) fileMenu.addAction(self.saveAsFile) fileMenu.addAction(self.viewDock) fileMenu.addAction(self.exitAct) commandMenu = menubar.addMenu('&Command') exportMetadata = commandMenu.addMenu('&Export Metadata') exportMetadata.addAction(jsonFormat) exportMetadata.addAction(xmlFormat) exportMetadata.addAction(ymlFormat) commandMenu.addAction(self.validateMetadata) commandMenu.addAction(self.editFields) commandMenu.addAction(addMetadata) commandMenu.addAction(extractFields) helpMenu = menubar.addMenu('&Help') helpMenu.addAction(documentation) helpMenu.addAction(aboutApp) def view_dock_field_toggle(self, state): if state: self.fields.show() else: self.fields.hide() def validate_xml(self): try: if self.factx.is_valid(): QMessageBox.information(self, 'Valid XML', "The XML is Valid", QMessageBox.Ok) else: QMessageBox.critical(self, 'Invalid XML', "The XML is invalid", QMessageBox.Ok) except AttributeError: QMessageBox.critical(self, 'File Not Found', "Load a PDF first", QMessageBox.Ok) def set_pdf_preview(self): # print(str(fileName[0])) if not os.path.exists('.load'): os.mkdir('.load') if sys.platform[:3] == 'win': convert = [ 'magick', self.fileName[0], '-flatten', '.load/preview.jpg' ] else: convert = [ 'convert', '-verbose', '-density', '150', '-trim', self.fileName[0], '-quality', '100', '-flatten', '-sharpen', '0x1.0', '.load/preview.jpg' ] subprocess.call(convert) self.pdfPreviewImage = '.load/preview.jpg' self.fileLoaded = True self.square.setPixmap( QPixmap(self.pdfPreviewImage).scaled(self.square.size().width(), self.square.size().height(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) def edit_fields_dialog(self): try: self.dialog = EditFieldsClass(self, self.factx, self.fieldsDict, self.metadata_field) self.dialog.installEventFilter(self) # self.dialog.show() except AttributeError: QMessageBox.critical(self, 'File Not Found', "Load a PDF first", QMessageBox.Ok) def update_dock_fields(self): self.factx.write_json('.load/output.json') with open('.load/output.json') as jsonFile: self.fieldsDict = json.load(jsonFile) os.remove('.load/output.json') # print(self.fieldsDict) i = 0 self.metadata_field = { 'amount_tax': 'Amount Tax', 'amount_total': 'Amount Total', 'amount_untaxed': 'Amount Untaxed', 'buyer': 'Buyer', 'currency': 'Currency', 'date': 'Date', 'date_due': 'Date Due', 'invoice_number': 'Invoice Number', 'name': 'Name', 'notes': 'Notes', 'seller': 'Seller', 'type': 'Type', 'version': 'Version' } for key in sorted(self.fieldsDict): i += 1 try: self.factx[key] except IndexError: self.fieldsDict[key] = "Field Not Specified" except TypeError: pass fieldKey = QLabel(self.metadata_field[key] + ": ") if self.fieldsDict[key] is None: fieldValue = QLabel("NA") else: if key[:4] == "date" and \ self.fieldsDict[key] != "Field Not Specified": self.fieldsDict[key] = self.fieldsDict[key][:4] \ + "/" + self.fieldsDict[key][4:6] \ + "/" + self.fieldsDict[key][6:8] if self.fieldsDict[key] == "Field Not Specified": fieldValue = QLabel(self.fieldsDict[key]) fieldValue.setStyleSheet("QLabel { color: #666666}") else: fieldValue = QLabel(self.fieldsDict[key]) # fieldValue.setFrameShape(QFrame.Panel) # fieldValue.setFrameShadow(QFrame.Plain) # fieldValue.setLineWidth(3) self.layout.addWidget(fieldKey, i, 0) self.layout.addWidget(fieldValue, i, 1) def show_file_dialog(self): self.fileName = QFileDialog.getOpenFileName(self, 'Open file', os.path.expanduser("~"), "pdf (*.pdf)") self.load_pdf_file() def load_pdf_file(self): if self.fileName[0]: if self.check_xml_for_pdf() is None: self.standard = None self.level = None self.choose_standard_level() if self.standard is not None: self.factx = FacturX(self.fileName[0], self.standard, self.level) else: self.factx = FacturX(self.fileName[0]) if hasattr(self, 'factx'): self.set_pdf_preview() self.update_dock_fields() self.setStatusTip("PDF is Ready") def choose_standard_level(self): self.chooseStandardDialog = QDialog() layout = QGridLayout() noXMLLabel = QLabel("No XML found", self) layout.addWidget(noXMLLabel, 0, 0) chooseStandardLabel = QLabel("Standard", self) chooseStandardCombo = QComboBox(self) chooseStandardCombo.addItem("Factur-X") chooseStandardCombo.addItem("Zugferd") chooseStandardCombo.addItem("UBL") chooseStandardCombo.model().item(2).setEnabled(False) chooseStandardCombo.activated[str].connect(self.on_select_level) chooseLevelLabel = QLabel("Level", self) self.chooseLevelCombo = QComboBox(self) self.chooseLevelCombo.addItem("Minimum") self.chooseLevelCombo.addItem("Basic WL") self.chooseLevelCombo.addItem("Basic") self.chooseLevelCombo.addItem("EN16931") self.chooseLevelCombo.activated[str].connect(self.set_level) applyStandard = QPushButton("Apply") applyStandard.clicked.connect(self.set_standard_level) discardStandard = QPushButton("Cancel") discardStandard.clicked.connect(self.discard_standard_level) layout.addWidget(chooseStandardLabel, 1, 0) layout.addWidget(chooseStandardCombo, 1, 1) layout.addWidget(chooseLevelLabel, 2, 0) layout.addWidget(self.chooseLevelCombo, 2, 1) layout.addWidget(discardStandard, 3, 0) layout.addWidget(applyStandard, 3, 1) self.chooseStandardDialog.setLayout(layout) self.chooseStandardDialog.setWindowTitle("Choose Standard") self.chooseStandardDialog.setWindowModality(Qt.ApplicationModal) self.chooseStandardDialog.exec_() def set_standard_level(self): try: self.standard = self.standard_temp self.level = self.level_temp except AttributeError: self.standard = 'factur-x' self.level = 'minimum' self.chooseStandardDialog.close() def discard_standard_level(self): self.chooseStandardDialog.close() def on_select_level(self, text): if text == "Factur-X": self.chooseLevelCombo.clear() self.chooseLevelCombo.addItem("Minimum") self.chooseLevelCombo.addItem("Basic WL") self.chooseLevelCombo.addItem("Basic") self.chooseLevelCombo.addItem("EN16931") elif text == "Zugferd": self.chooseLevelCombo.clear() self.chooseLevelCombo.addItem("Basic") self.chooseLevelCombo.addItem("Comfort") elif text == "UBL": self.chooseLevelCombo.clear() self.chooseLevelCombo.addItem("UBL 2.0") self.chooseLevelCombo.addItem("UBL 2.1") standard_dict = { 'Factur-X': ['factur-x', 'minimum'], 'Zugferd': ['zugferd', 'basic'], } self.standard_temp = standard_dict[text][0] self.level_temp = standard_dict[text][1] def set_level(self, text): level_dict = { 'Minimum': 'minimum', 'Basic WL': 'basicwl', 'Basic': 'basic', 'EN16931': 'en16931', 'Comfort': 'comfort' } self.level_temp = level_dict[text] def check_xml_for_pdf(self): pdf = PdfFileReader(self.fileName[0]) pdf_root = pdf.trailer['/Root'] if '/Names' not in pdf_root or '/EmbeddedFiles' not in \ pdf_root['/Names']: return None for file in pdf_root['/Names']['/EmbeddedFiles']['/Names']: if isinstance(file, IndirectObject): obj = file.getObject() if obj['/F'] in xml_flavor.valid_xmp_filenames(): xml_root = etree.fromstring(obj['/EF']['/F'].getData()) xml_content = xml_root return xml_content def save_file_dialog(self): if self.fileLoaded: if self.confirm_save_dialog(): try: self.factx.write_pdf(self.fileName[0]) except TypeError: QMessageBox.critical(self, 'Type Error', "Some field value(s) are invalid", QMessageBox.Ok) else: QMessageBox.critical(self, 'File Not Found', "Load a PDF first", QMessageBox.Ok) def confirm_save_dialog(self): reply = QMessageBox.question( self, 'Message', "Do you want to save? This cannot be undone", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: return True else: return False def show_save_as_dialog(self): if self.fileLoaded: try: self.saveFileName = QFileDialog.getSaveFileName( self, 'Save file', os.path.expanduser("~"), "pdf (*.pdf)") if self.saveFileName[0]: if self.saveFileName[0].endswith('.pdf'): fileName = self.saveFileName[0] else: fileName = self.saveFileName[0] + '.pdf' self.factx.write_pdf(fileName) except TypeError: QMessageBox.critical(self, 'Type Error', "Some field value(s) are not valid", QMessageBox.Ok) else: QMessageBox.critical(self, 'File Not Found', "Load a PDF first", QMessageBox.Ok) def extract_fields_from_pdf(self): if self.fileLoaded: self.populate = PopulateFieldClass(self, self.factx, self.fieldsDict, self.metadata_field) else: QMessageBox.critical(self, 'File Not Found', "Load a PDF first", QMessageBox.Ok) def documentation_menubar(self): pass def about_app_menubar(self): pass def export_fields(self, outputformat): if self.fileLoaded: self.exportFileName = QFileDialog.getSaveFileName( self, 'Export file', os.path.expanduser("~") + '/output.%s' % outputformat, "%s (*.%s)" % (outputformat, outputformat)) if self.exportFileName[0]: if outputformat is "json": self.pdf_write_json(self.exportFileName[0]) elif outputformat is "xml": self.pdf_write_xml(self.exportFileName[0]) elif outputformat is "yml": self.pdf_write_yml(self.exportFileName[0]) else: QMessageBox.critical(self, 'File Not Found', "Load a PDF first", QMessageBox.Ok) def pdf_write_json(self, fileName): self.factx.write_json(fileName) def pdf_write_xml(self, fileName): self.factx.write_xml(fileName) def pdf_write_yml(self, fileName): self.factx.write_yml(fileName) def resizeEvent(self, event): if self.fileLoaded: self.square.setPixmap( QPixmap(self.pdfPreviewImage).scaled( self.square.size().width(), self.square.size().height(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) self.square.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Ignored) QMainWindow.resizeEvent(self, event) def eventFilter(self, source, event): if event.type() == QEvent.Close and source is self.fields: self.viewDock.setChecked(False) return QMainWindow.eventFilter(self, source, event) def closeEvent(self, event): if os.path.isdir('.load'): shutil.rmtree('.load/')