示例#1
0
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
示例#2
0
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)
示例#3
0
    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
示例#4
0
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)
示例#5
0
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/')