예제 #1
0
 def test_runJavaScript(self):
     w = WebviewWidget()
     w.runJavaScript('2;')
     retvals = []
     w.runJavaScript('3;', lambda retval: retvals.append(retval))
     wait(until=lambda: retvals)
     self.assertEqual(retvals[0], 3)
예제 #2
0
 def test_escape_hides(self):
     window = QDialog()
     w = WebviewWidget(window)
     window.show()
     w.setFocus(Qt.OtherFocusReason)
     self.assertFalse(window.isHidden())
     QTest.keyClick(w, Qt.Key_Escape)
     self.assertTrue(window.isHidden())
예제 #3
0
    def _setup_ui_(self):
        self.table_model = ReportItemModel(0, len(Column.__members__))
        self.table = ReportTable(self.controlArea)
        self.table.setModel(self.table_model)
        self.table.setShowGrid(False)
        self.table.setSelectionBehavior(QTableView.SelectRows)
        self.table.setSelectionMode(QTableView.SingleSelection)
        self.table.setWordWrap(False)
        self.table.setMouseTracking(True)
        self.table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed)
        self.table.verticalHeader().setDefaultSectionSize(20)
        self.table.verticalHeader().setVisible(False)
        self.table.horizontalHeader().setVisible(False)
        self.table.setFixedWidth(250)
        self.table.setColumnWidth(Column.item, 200)
        self.table.setColumnWidth(Column.remove, 23)
        self.table.setColumnWidth(Column.scheme, 25)
        self.table.clicked.connect(self._table_clicked)
        self.table.selectionModel().selectionChanged.connect(
            self._table_selection_changed)
        self.controlArea.layout().addWidget(self.table)

        self.last_scheme = None
        self.scheme_button = gui.button(
            self.controlArea,
            self,
            "Back to Last Scheme",
            callback=self._show_last_scheme,
        )
        box = gui.hBox(self.controlArea)
        box.setContentsMargins(-6, 0, -6, 0)
        self.save_button = gui.button(box,
                                      self,
                                      "Save",
                                      callback=self.save_report)
        self.print_button = gui.button(box,
                                       self,
                                       "Print",
                                       callback=self._print_report)

        class PyBridge(QObject):
            @pyqtSlot(str)
            def _select_item(myself, item_id):
                item = self.table_model.get_item_by_id(item_id)
                self.table.selectRow(
                    self.table_model.indexFromItem(item).row())
                self._change_selected_item(item)

            @pyqtSlot(str, str)
            def _add_comment(myself, item_id, value):
                item = self.table_model.get_item_by_id(item_id)
                item.comment = value
                self.report_changed = True

        self.report_view = WebviewWidget(self.mainArea, bridge=PyBridge(self))
        self.mainArea.layout().addWidget(self.report_view)
예제 #4
0
 def test_escape_hides(self):
     # NOTE: This test doesn't work as it is supposed to.
     window = QDialog()
     w = WebviewWidget(window)
     window.show()
     w.setFocus(Qt.OtherFocusReason)
     self.assertFalse(window.isHidden())
     # This event is sent to the wrong widget. Should be sent to the
     # inner HTML view as focused, but no amount of clicking/ focusing
     # helped, neither did invoking JS handler directly. I'll live with it.
     QTest.keyClick(w, Qt.Key_Escape)
     self.assertTrue(window.isHidden())
예제 #5
0
 def test_escape_hides(self):
     # NOTE: This test doesn't work as it is supposed to.
     window = QDialog()
     w = WebviewWidget(window)
     window.show()
     w.setFocus(Qt.OtherFocusReason)
     self.assertFalse(window.isHidden())
     # This event is sent to the wrong widget. Should be sent to the
     # inner HTML view as focused, but no amount of clicking/ focusing
     # helped, neither did invoking JS handler directly. I'll live with it.
     QTest.keyClick(w, Qt.Key_Escape)
     self.assertTrue(window.isHidden())
예제 #6
0
    def test_base(self):
        w = WebviewWidget()
        w.evalJS('document.write("foo");')
        SVG = '<svg xmlns:dc="...">asd</svg>'
        w.onloadJS('''document.write('{}');'''.format(SVG))
        w.setUrl(SOME_URL)
        svg = self.process_events(lambda: w.svg())

        self.assertEqual(svg, SVG)
        self.assertEqual(
            w.html(),
            '<html><head></head><body>foo<svg xmlns:dc="...">asd</svg></body></html>'
        )
예제 #7
0
    def test_base(self):
        w = WebviewWidget()
        w.evalJS('document.write("foo");')
        SVG = '<svg xmlns:dc="...">asd</svg>'
        w.onloadJS('''document.write('{}');'''.format(SVG))
        w.setUrl(SOME_URL)
        svg = self.process_events(lambda: w.svg())

        self.assertEqual(svg, SVG)
        self.assertEqual(
            w.html(), '<html><head></head><body>foo<svg xmlns:dc="...">asd</svg></body></html>')
예제 #8
0
    def test_base(self):
        w = WebviewWidget()
        w.evalJS('document.write("foo");')
        SVG = '<svg xmlns:dc="...">asd</svg>'
        w.onloadJS('''document.write('{}');'''.format(SVG))
        w.setUrl(SOME_URL)

        svg = None
        while svg is None:
            try:
                svg = w.svg()
                break
            except ValueError:
                qApp.processEvents()
        self.assertEqual(svg, SVG)
        self.assertEqual(
            w.html(), '<html><head></head><body>foo<svg xmlns:dc="...">asd</svg></body></html>')
예제 #9
0
    def test_base(self):
        w = WebviewWidget()
        w.evalJS('document.write("foo");')
        SVG = '<svg xmlns:dc="...">asd</svg>'
        w.onloadJS('''document.write('{}');'''.format(SVG))
        w.setUrl(SOME_URL)

        svg = self.process_events(lambda: w.svg())
        self.assertEqual(svg, SVG)

        self.process_events(until=lambda: 'foo' in w.html())
        html = '<svg xmlns:dc="...">asd</svg>'
        self.assertEqual(
            w.html(),
            '<html><head></head><body>{}</body></html>'.format(
                # WebKit evaluates first document.write first, whereas
                # WebEngine evaluates onloadJS first
                'foo' + html if HAVE_WEBKIT else html + 'foo'
            ))
예제 #10
0
    def test_base(self):
        w = WebviewWidget()
        w.evalJS('document.write("foo");')
        SVG = '<svg xmlns:dc="...">asd</svg>'
        w.onloadJS('''document.write('{}');'''.format(SVG))
        w.setUrl(SOME_URL)

        svg = None
        while svg is None:
            try:
                svg = w.svg()
                break
            except ValueError:
                qApp.processEvents()
        self.assertEqual(svg, SVG)
        self.assertEqual(
            w.html(), '<html><head></head><body>foo<svg xmlns:dc="...">asd</svg></body></html>')
예제 #11
0
 def test_runJavaScript(self):
     w = WebviewWidget()
     w.runJavaScript('2;')
     retvals = []
     w.runJavaScript('3;', lambda retval: retvals.append(retval))
     wait(until=lambda: retvals)
     self.assertEqual(retvals[0], 3)
예제 #12
0
파일: owreport.py 프로젝트: benzei/orange3
    def _setup_ui_(self):
        self.table_model = ReportItemModel(0, len(Column.__members__))
        self.table = ReportTable(self.controlArea)
        self.table.setModel(self.table_model)
        self.table.setShowGrid(False)
        self.table.setSelectionBehavior(QTableView.SelectRows)
        self.table.setSelectionMode(QTableView.SingleSelection)
        self.table.setWordWrap(False)
        self.table.setMouseTracking(True)
        self.table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed)
        self.table.verticalHeader().setDefaultSectionSize(20)
        self.table.verticalHeader().setVisible(False)
        self.table.horizontalHeader().setVisible(False)
        self.table.setFixedWidth(250)
        self.table.setColumnWidth(Column.item, 200)
        self.table.setColumnWidth(Column.remove, 23)
        self.table.setColumnWidth(Column.scheme, 25)
        self.table.clicked.connect(self._table_clicked)
        self.table.selectionModel().selectionChanged.connect(
            self._table_selection_changed)
        self.controlArea.layout().addWidget(self.table)

        self.last_scheme = None
        self.scheme_button = gui.button(
            self.controlArea, self, "Back to Last Scheme",
            callback=self._show_last_scheme
        )
        box = gui.hBox(self.controlArea)
        box.setContentsMargins(-6, 0, -6, 0)
        self.save_button = gui.button(
            box, self, "Save", callback=self.save_report
        )
        self.print_button = gui.button(
            box, self, "Print", callback=self._print_report
        )

        class PyBridge(QObject):
            @pyqtSlot(str)
            def _select_item(myself, item_id):
                item = self.table_model.get_item_by_id(item_id)
                self.table.selectRow(self.table_model.indexFromItem(item).row())
                self._change_selected_item(item)

            @pyqtSlot(str, str)
            def _add_comment(myself, item_id, value):
                item = self.table_model.get_item_by_id(item_id)
                item.comment = value
                self.report_changed = True

        self.report_view = WebviewWidget(self.mainArea, bridge=PyBridge(self))
        self.mainArea.layout().addWidget(self.report_view)
예제 #13
0
    def test_exposeObject(self):
        test = self
        OBJ = dict(a=[1, 2], b='c')
        done = False

        class Bridge(QObject):
            @pyqtSlot('QVariantMap')
            def check_object(self, obj):
                nonlocal test, done, OBJ
                done = True
                test.assertEqual(obj, OBJ)

        w = WebviewWidget(bridge=Bridge())
        w.setUrl(SOME_URL)
        w.exposeObject('obj', OBJ)
        w.evalJS('''pybridge.check_object(window.obj);''')
        self.process_events(lambda: done)

        self.assertRaises(ValueError, w.exposeObject, 'obj', QDialog())
예제 #14
0
    def test_exposeObject(self):
        test = self
        OBJ = dict(a=[1, 2], b='c')
        done = False

        class Bridge(QObject):
            @pyqtSlot('QVariantMap')
            def check_object(self, obj):
                nonlocal test, done, OBJ
                done = True
                test.assertEqual(obj, OBJ)

        w = WebviewWidget(bridge=Bridge())
        w.setUrl(SOME_URL)
        w.exposeObject('obj', OBJ)
        w.evalJS('''pybridge.check_object(window.obj);''')
        self.process_events(lambda: done)

        self.assertRaises(ValueError, w.exposeObject, 'obj', QDialog())
예제 #15
0
class OWReport(OWWidget):
    name = "Report"
    save_dir = Setting("")
    open_dir = Setting("")

    def __init__(self):
        super().__init__()
        self._setup_ui_()
        self.report_changed = False

        index_file = pkg_resources.resource_filename(__name__, "index.html")
        with open(index_file, "r") as f:
            self.report_html_template = f.read()

    def _setup_ui_(self):
        self.table_model = ReportItemModel(0, len(Column.__members__))
        self.table = ReportTable(self.controlArea)
        self.table.setModel(self.table_model)
        self.table.setShowGrid(False)
        self.table.setSelectionBehavior(QTableView.SelectRows)
        self.table.setSelectionMode(QTableView.SingleSelection)
        self.table.setWordWrap(False)
        self.table.setMouseTracking(True)
        self.table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed)
        self.table.verticalHeader().setDefaultSectionSize(20)
        self.table.verticalHeader().setVisible(False)
        self.table.horizontalHeader().setVisible(False)
        self.table.setFixedWidth(250)
        self.table.setColumnWidth(Column.item, 200)
        self.table.setColumnWidth(Column.remove, 23)
        self.table.setColumnWidth(Column.scheme, 25)
        self.table.clicked.connect(self._table_clicked)
        self.table.selectionModel().selectionChanged.connect(
            self._table_selection_changed)
        self.controlArea.layout().addWidget(self.table)

        self.last_scheme = None
        self.scheme_button = gui.button(self.controlArea,
                                        self,
                                        "Back to Last Scheme",
                                        callback=self._show_last_scheme)
        box = gui.hBox(self.controlArea)
        box.setContentsMargins(-6, 0, -6, 0)
        self.save_button = gui.button(box,
                                      self,
                                      "Save",
                                      callback=self.save_report,
                                      disabled=True)
        self.print_button = gui.button(box,
                                       self,
                                       "Print",
                                       callback=self._print_report,
                                       disabled=True)

        class PyBridge(QObject):
            @pyqtSlot(str)
            def _select_item(myself, item_id):
                item = self.table_model.get_item_by_id(item_id)
                self.table.selectRow(
                    self.table_model.indexFromItem(item).row())
                self._change_selected_item(item)

            @pyqtSlot(str, str)
            def _add_comment(myself, item_id, value):
                item = self.table_model.get_item_by_id(item_id)
                item.comment = value
                self.report_changed = True

        self.report_view = WebviewWidget(self.mainArea, bridge=PyBridge(self))
        self.mainArea.layout().addWidget(self.report_view)

    @deprecated("Widgets should not be pickled")
    def __getstate__(self):
        rep_dict = self.__dict__.copy()
        for key in ('_OWWidget__env', 'controlArea', 'mainArea', 'report_view',
                    'table', 'table_model'):
            del rep_dict[key]
        items_len = self.table_model.rowCount()
        return rep_dict, [self.table_model.item(i) for i in range(items_len)]

    @deprecated("Widgets should not be pickled")
    def __setstate__(self, state):
        rep_dict, items = state
        self.__dict__.update(rep_dict)
        self._setup_ui_()
        for i in range(len(items)):
            item = items[i]
            self.table_model.add_item(
                ReportItem(item.name, item.html, item.scheme, item.module,
                           item.icon_name, item.comment))

    def _table_clicked(self, index):
        if index.column() == Column.remove:
            self._remove_item(index.row())
            indexes = self.table.selectionModel().selectedIndexes()
            if indexes:
                item = self.table_model.item(indexes[0].row())
                self._scroll_to_item(item)
                self._change_selected_item(item)
        if index.column() == Column.scheme:
            self._show_scheme(index.row())

    def _table_selection_changed(self, new_selection, _):
        if new_selection.indexes():
            item = self.table_model.item(new_selection.indexes()[0].row())
            self._scroll_to_item(item)
            self._change_selected_item(item)

    def _remove_item(self, row):
        self.table_model.removeRow(row)
        self._empty_report()
        self.report_changed = True
        self._build_html()

    def clear(self):
        self.table_model.clear()
        self._empty_report()
        self.report_changed = True
        self._build_html()

    def _add_item(self, widget):
        name = widget.get_widget_name_extension()
        name = "{} - {}".format(widget.name, name) if name else widget.name
        item = ReportItem(name, widget.report_html, self._get_scheme(),
                          widget.__module__, widget.icon)
        self.table_model.add_item(item)
        self._empty_report()
        self.report_changed = True
        return item

    def _empty_report(self):
        # disable save and print if no reports
        self.save_button.setEnabled(self.table_model.rowCount())
        self.print_button.setEnabled(self.table_model.rowCount())

    def _build_html(self):
        html = self.report_html_template
        html += "<body>"
        for i in range(self.table_model.rowCount()):
            item = self.table_model.item(i)
            html += "<div id='{}' class='normal' " \
                    "onClick='pybridge._select_item(this.id)'>{}<div " \
                    "class='textwrapper'><textarea " \
                    "placeholder='Write a comment...'" \
                    "onInput='this.innerHTML = this.value;" \
                    "pybridge._add_comment(this.parentNode.parentNode.id, this.value);'" \
                    ">{}</textarea></div>" \
                    "</div>".format(item.id, item.html, item.comment)
        html += "</body></html>"
        self.report_view.setHtml(html)

    def _scroll_to_item(self, item):
        self.report_view.evalJS(
            "document.getElementById('{}').scrollIntoView();".format(item.id))

    def _change_selected_item(self, item):
        self.report_view.evalJS(
            "var sel_el = document.getElementsByClassName('selected')[0]; "
            "if (sel_el.id != {}) "
            "   sel_el.className = 'normal';".format(item.id))
        self.report_view.evalJS(
            "document.getElementById('{}').className = 'selected';".format(
                item.id))
        self.report_changed = True

    def make_report(self, widget):
        item = self._add_item(widget)
        self._build_html()
        self._scroll_to_item(item)
        self.table.selectRow(self.table_model.rowCount() - 1)

    def _get_scheme(self):
        canvas = self.get_canvas_instance()
        return canvas.get_scheme_xml() if canvas else None

    def _show_scheme(self, row):
        scheme = self.table_model.item(row).scheme
        canvas = self.get_canvas_instance()
        if canvas:
            document = canvas.current_document()
            if document.isModifiedStrict():
                self.last_scheme = canvas.get_scheme_xml()
            self._load_scheme(scheme)

    def _show_last_scheme(self):
        if self.last_scheme:
            self._load_scheme(self.last_scheme)

    def _load_scheme(self, contents):
        # forcibly load the contents into the associated CanvasMainWindow
        # instance if one exists. Preserve `self` as the designated report.
        canvas = self.get_canvas_instance()
        if canvas is not None:
            document = canvas.current_document()
            old = document.scheme()
            if old.has_report() and old.report_view() is self:
                # remove self so it is not closed
                old.set_report_view(None)
            canvas.load_scheme_xml(contents)
            scheme = canvas.current_document().scheme()
            scheme.set_report_view(self)

    def save_report(self):
        """Save report"""
        formats = OrderedDict(
            (('HTML (*.html)', '.html'), ('PDF (*.pdf)', '.pdf'),
             ('Report (*.report)', '.report')))

        filename, selected_format = QFileDialog.getSaveFileName(
            self, "Save Report", self.save_dir, ';;'.join(formats.keys()))
        if not filename:
            return QDialog.Rejected

        # Set appropriate extension if not set by the user
        expect_ext = formats[selected_format]
        if not filename.endswith(expect_ext):
            filename += expect_ext

        self.save_dir = os.path.dirname(filename)
        self.saveSettings()
        _, extension = os.path.splitext(filename)
        if extension == ".pdf":
            printer = QPrinter()
            printer.setPageSize(QPrinter.A4)
            printer.setOutputFormat(QPrinter.PdfFormat)
            printer.setOutputFileName(filename)
            self._print_to_printer(printer)
        elif extension == ".report":
            self.save(filename)
        else:

            def save_html(contents):
                try:
                    with open(filename, "w", encoding="utf-8") as f:
                        f.write(contents)
                except PermissionError:
                    self.permission_error(filename)

            save_html(self.report_view.html())
        self.report_changed = False
        return QDialog.Accepted

    def _print_to_printer(self, printer):
        filename = printer.outputFileName()
        if filename:
            try:
                # QtWebEngine
                return self.report_view.page().printToPdf(filename)
            except AttributeError:
                try:
                    # QtWebKit
                    return self.report_view.print_(printer)
                except AttributeError:
                    # QtWebEngine 5.6
                    pass
        # Fallback to printing widget as an image
        self.report_view.render(printer)

    def _print_report(self):
        printer = QPrinter()
        print_dialog = QPrintDialog(printer, self)
        print_dialog.setWindowTitle("Print report")
        if print_dialog.exec_() != QDialog.Accepted:
            return
        self._print_to_printer(printer)

    def save(self, filename):
        attributes = {}
        for key in ('last_scheme', 'open_dir'):
            attributes[key] = getattr(self, key, None)
        items = [
            self.table_model.item(i)
            for i in range(self.table_model.rowCount())
        ]
        report = dict(__version__=1, attributes=attributes, items=items)

        try:
            with open(filename, 'wb') as f:
                pickle.dump(report, f)
        except PermissionError:
            self.permission_error(filename)

    @classmethod
    def load(cls, filename):
        with open(filename, 'rb') as f:
            report = pickle.load(f)

        if not isinstance(report, dict):
            return report

        self = cls()
        self.__dict__.update(report['attributes'])
        for item in report['items']:
            self.table_model.add_item(
                ReportItem(item.name, item.html, item.scheme, item.module,
                           item.icon_name, item.comment))
        return self

    def permission_error(self, filename):
        message_critical(
            self.tr("Permission error when trying to write report."),
            title=self.tr("Error"),
            informative_text=self.tr("Permission error occurred "
                                     "while saving '{}'.").format(filename),
            exc_info=True,
            parent=self)
        log.error("PermissionError when trying to write report.",
                  exc_info=True)

    def is_empty(self):
        return not self.table_model.rowCount()

    def is_changed(self):
        return self.report_changed

    @staticmethod
    def set_instance(report):
        warnings.warn("OWReport.set_instance is deprecated",
                      DeprecationWarning,
                      stacklevel=2)
        app_inst = QApplication.instance()
        app_inst._report_window = report

    @staticmethod
    def get_instance():
        warnings.warn("OWReport.get_instance is deprecated",
                      DeprecationWarning,
                      stacklevel=2)
        app_inst = QApplication.instance()
        if not hasattr(app_inst, "_report_window"):
            report = OWReport()
            app_inst._report_window = report
        return app_inst._report_window

    def get_canvas_instance(self):
        # type: () -> Optional[CanvasMainWindow]
        """
        Return a CanvasMainWindow instance to which this report is attached.

        Return None if not associated with any window.

        Returns
        -------
        window : Optional[CanvasMainWindow]
        """
        # Run up the parent/window chain
        parent = self.parent()
        if parent is not None:
            window = parent.window()
            if isinstance(window, CanvasMainWindow):
                return window
        return None

    def copy_to_clipboard(self):
        self.report_view.triggerPageAction(self.report_view.page().Copy)
예제 #16
0
    def __init__(self):
        super().__init__()
        self.table = None
        self._html = None

        def _loadFinished(is_ok):
            if is_ok:
                QTimer.singleShot(1, lambda: setattr(self, '_html', self.webview.html()))

        self.webview = WebviewWidget(loadFinished=_loadFinished)

        vb = gui.vBox(self.controlArea, 'Import Data')
        hb = gui.hBox(vb)
        self.combo = combo = URLComboBox(
            hb, self.recent, editable=True, minimumWidth=400,
            insertPolicy=QComboBox.InsertAtTop,
            toolTip='Format: ' + VALID_URL_HELP,
            editTextChanged=self.is_valid_url,
            # Indirect via QTimer because calling wait() -> processEvents,
            # while our currentIndexChanged event hadn't yet finished.
            # Avoids calling handler twice.
            currentIndexChanged=lambda: QTimer.singleShot(1, self.load_url))
        hb.layout().addWidget(QLabel('Public link URL:', hb))
        hb.layout().addWidget(combo)
        hb.layout().setStretch(1, 2)

        RELOAD_TIMES = (
            ('No reload',),
            ('5 s', 5000),
            ('10 s', 10000),
            ('30 s', 30000),
            ('1 min', 60*1000),
            ('2 min', 2*60*1000),
            ('5 min', 5*60*1000),
        )

        reload_timer = QTimer(self, timeout=lambda: self.load_url(from_reload=True))

        def _on_reload_changed():
            if self.reload_idx == 0:
                reload_timer.stop()
                return
            reload_timer.start(RELOAD_TIMES[self.reload_idx][1])

        gui.comboBox(vb, self, 'reload_idx', label='Reload every:',
                     orientation=Qt.Horizontal,
                     items=[i[0] for i in RELOAD_TIMES],
                     callback=_on_reload_changed)

        box = gui.widgetBox(self.controlArea, "Columns (Double-click to edit)")
        self.domain_editor = DomainEditor(self)
        editor_model = self.domain_editor.model()

        def editorDataChanged():
            self.apply_domain_edit()
            self.commit()

        editor_model.dataChanged.connect(editorDataChanged)
        box.layout().addWidget(self.domain_editor)

        box = gui.widgetBox(self.controlArea, "Info", addSpace=True)
        info = self.data_info = gui.widgetLabel(box, '')
        info.setWordWrap(True)

        self.controlArea.layout().addStretch(1)
        gui.auto_commit(self.controlArea, self, 'autocommit', label='Commit')

        self.set_info()
예제 #17
0
파일: owreport.py 프로젝트: benzei/orange3
class OWReport(OWWidget):
    name = "Report"
    save_dir = Setting("")
    open_dir = Setting("")

    def __init__(self):
        super().__init__()
        self._setup_ui_()
        self.report_changed = False

        index_file = pkg_resources.resource_filename(__name__, "index.html")
        with open(index_file, "r") as f:
            self.report_html_template = f.read()

    def _setup_ui_(self):
        self.table_model = ReportItemModel(0, len(Column.__members__))
        self.table = ReportTable(self.controlArea)
        self.table.setModel(self.table_model)
        self.table.setShowGrid(False)
        self.table.setSelectionBehavior(QTableView.SelectRows)
        self.table.setSelectionMode(QTableView.SingleSelection)
        self.table.setWordWrap(False)
        self.table.setMouseTracking(True)
        self.table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed)
        self.table.verticalHeader().setDefaultSectionSize(20)
        self.table.verticalHeader().setVisible(False)
        self.table.horizontalHeader().setVisible(False)
        self.table.setFixedWidth(250)
        self.table.setColumnWidth(Column.item, 200)
        self.table.setColumnWidth(Column.remove, 23)
        self.table.setColumnWidth(Column.scheme, 25)
        self.table.clicked.connect(self._table_clicked)
        self.table.selectionModel().selectionChanged.connect(
            self._table_selection_changed)
        self.controlArea.layout().addWidget(self.table)

        self.last_scheme = None
        self.scheme_button = gui.button(
            self.controlArea, self, "Back to Last Scheme",
            callback=self._show_last_scheme
        )
        box = gui.hBox(self.controlArea)
        box.setContentsMargins(-6, 0, -6, 0)
        self.save_button = gui.button(
            box, self, "Save", callback=self.save_report
        )
        self.print_button = gui.button(
            box, self, "Print", callback=self._print_report
        )

        class PyBridge(QObject):
            @pyqtSlot(str)
            def _select_item(myself, item_id):
                item = self.table_model.get_item_by_id(item_id)
                self.table.selectRow(self.table_model.indexFromItem(item).row())
                self._change_selected_item(item)

            @pyqtSlot(str, str)
            def _add_comment(myself, item_id, value):
                item = self.table_model.get_item_by_id(item_id)
                item.comment = value
                self.report_changed = True

        self.report_view = WebviewWidget(self.mainArea, bridge=PyBridge(self))
        self.mainArea.layout().addWidget(self.report_view)

    @deprecated("Widgets should not be pickled")
    def __getstate__(self):
        rep_dict = self.__dict__.copy()
        for key in ('_OWWidget__env', 'controlArea', 'mainArea',
                    'report_view', 'table', 'table_model'):
            del rep_dict[key]
        items_len = self.table_model.rowCount()
        return rep_dict, [self.table_model.item(i) for i in range(items_len)]

    @deprecated("Widgets should not be pickled")
    def __setstate__(self, state):
        rep_dict, items = state
        self.__dict__.update(rep_dict)
        self._setup_ui_()
        for i in range(len(items)):
            item = items[i]
            self.table_model.add_item(
                ReportItem(item.name, item.html, item.scheme,
                           item.module, item.icon_name, item.comment)
            )

    def _table_clicked(self, index):
        if index.column() == Column.remove:
            self._remove_item(index.row())
            indexes = self.table.selectionModel().selectedIndexes()
            if indexes:
                item = self.table_model.item(indexes[0].row())
                self._scroll_to_item(item)
                self._change_selected_item(item)
        if index.column() == Column.scheme:
            self._show_scheme(index.row())

    def _table_selection_changed(self, new_selection, _):
        if new_selection.indexes():
            item = self.table_model.item(new_selection.indexes()[0].row())
            self._scroll_to_item(item)
            self._change_selected_item(item)

    def _remove_item(self, row):
        self.table_model.removeRow(row)
        self.report_changed = True
        self._build_html()

    def clear(self):
        self.table_model.clear()
        self.report_changed = True
        self._build_html()

    def _add_item(self, widget):
        name = widget.get_widget_name_extension()
        name = "{} - {}".format(widget.name, name) if name else widget.name
        item = ReportItem(name, widget.report_html, self._get_scheme(),
                          widget.__module__, widget.icon)
        self.table_model.add_item(item)
        self.report_changed = True
        return item

    def _build_html(self):
        html = self.report_html_template
        html += "<body>"
        for i in range(self.table_model.rowCount()):
            item = self.table_model.item(i)
            html += "<div id='{}' class='normal' " \
                    "onClick='pybridge._select_item(this.id)'>{}<div " \
                    "class='textwrapper'><textarea " \
                    "placeholder='Write a comment...'" \
                    "onInput='this.innerHTML = this.value;" \
                    "pybridge._add_comment(this.parentNode.parentNode.id, this.value);'" \
                    ">{}</textarea></div>" \
                    "</div>".format(item.id, item.html, item.comment)
        html += "</body></html>"
        self.report_view.setHtml(html)

    def _scroll_to_item(self, item):
        self.report_view.evalJS(
            "document.getElementById('{}').scrollIntoView();".format(item.id)
        )

    def _change_selected_item(self, item):
        self.report_view.evalJS(
            "var sel_el = document.getElementsByClassName('selected')[0]; "
            "if (sel_el.id != {}) "
            "   sel_el.className = 'normal';".format(item.id))
        self.report_view.evalJS(
            "document.getElementById('{}').className = 'selected';"
            .format(item.id))
        self.report_changed = True

    def make_report(self, widget):
        item = self._add_item(widget)
        self._build_html()
        self._scroll_to_item(item)
        self.table.selectRow(self.table_model.rowCount() - 1)

    def _get_scheme(self):
        canvas = self.get_canvas_instance()
        return canvas.get_scheme_xml() if canvas else None

    def _show_scheme(self, row):
        scheme = self.table_model.item(row).scheme
        canvas = self.get_canvas_instance()
        if canvas:
            document = canvas.current_document()
            if document.isModifiedStrict():
                self.last_scheme = canvas.get_scheme_xml()
            canvas.load_scheme_xml(scheme)

    def _show_last_scheme(self):
        if self.last_scheme:
            canvas = self.get_canvas_instance()
            if canvas:
                canvas.load_scheme_xml(self.last_scheme)

    def save_report(self):
        """Save report"""
        filename, _ = QFileDialog.getSaveFileName(
            self, "Save Report", self.save_dir,
            "HTML (*.html);;PDF (*.pdf);;Report (*.report)")
        if not filename:
            return QDialog.Rejected

        self.save_dir = os.path.dirname(filename)
        self.saveSettings()
        _, extension = os.path.splitext(filename)
        if extension == ".pdf":
            printer = QPrinter()
            printer.setPageSize(QPrinter.A4)
            printer.setOutputFormat(QPrinter.PdfFormat)
            printer.setOutputFileName(filename)
            self.report_view.print_(printer)
        elif extension == ".report":
            self.save(filename)
        else:
            def save_html(contents):
                try:
                    with open(filename, "w", encoding="utf-8") as f:
                        f.write(contents)
                except PermissionError:
                    self.permission_error(filename)

            save_html(self.report_view.html())
        self.report_changed = False
        return QDialog.Accepted

    def _print_report(self):
        printer = QPrinter()
        print_dialog = QPrintDialog(printer, self)
        print_dialog.setWindowTitle("Print report")
        if print_dialog.exec_() != QDialog.Accepted:
            return
        self.report_view.print_(printer)

    def open_report(self):
        filename, _ = QFileDialog.getOpenFileName(
            self, "Open Report", self.open_dir, "Report (*.report)")
        if not filename:
            return

        self.report_changed = False
        self.open_dir = os.path.dirname(filename)
        self.saveSettings()

        try:
            report = self.load(filename)
        except (IOError, AttributeError, pickle.UnpicklingError) as e:
            message_critical(
                 self.tr("Could not load an Orange Report file"),
                 title=self.tr("Error"),
                 informative_text=self.tr("Error occurred "
                                          "while loading '{}'.").format(filename),
                 exc_info=True,
                 parent=self)
            log.error(str(e), exc_info=True)
            return
        self.set_instance(report)
        self = report
        self._build_html()
        self.table.selectRow(0)
        self.show()
        self.raise_()

    def save(self, filename):
        attributes = {}
        for key in ('last_scheme', 'open_dir'):
            attributes[key] = getattr(self, key, None)
        items = [self.table_model.item(i)
                 for i in range(self.table_model.rowCount())]
        report = dict(__version__=1,
                      attributes=attributes,
                      items=items)

        try:
            with open(filename, 'wb') as f:
                pickle.dump(report, f)
        except PermissionError:
            self.permission_error(filename)

    @classmethod
    def load(cls, filename):
        with open(filename, 'rb') as f:
            report = pickle.load(f)

        if not isinstance(report, dict):
            return report

        self = cls()
        self.__dict__.update(report['attributes'])
        for item in report['items']:
            self.table_model.add_item(
                ReportItem(item.name, item.html, item.scheme,
                           item.module, item.icon_name, item.comment)
            )
        return self

    def permission_error(self, filename):
        message_critical(
            self.tr("Permission error when trying to write report."),
            title=self.tr("Error"),
            informative_text=self.tr("Permission error occurred "
                                     "while saving '{}'.").format(filename),
            exc_info=True,
            parent=self)
        log.error("PermissionError when trying to write report.", exc_info=True)

    def is_empty(self):
        return not self.table_model.rowCount()

    def is_changed(self):
        return self.report_changed

    @staticmethod
    def set_instance(report):
        app_inst = QApplication.instance()
        app_inst._report_window = report

    @staticmethod
    def get_instance():
        app_inst = QApplication.instance()
        if not hasattr(app_inst, "_report_window"):
            report = OWReport()
            app_inst._report_window = report
        return app_inst._report_window

    @staticmethod
    def get_canvas_instance():
        for widget in QApplication.topLevelWidgets():
            if isinstance(widget, CanvasMainWindow):
                return widget
예제 #18
0
    def test_base(self):
        w = WebviewWidget()
        w.evalJS('document.write("foo");')
        SVG = '<svg xmlns:dc="...">asd</svg>'
        w.onloadJS('''document.write('{}');'''.format(SVG))
        w.setUrl(SOME_URL)

        svg = self.process_events(lambda: w.svg())
        self.assertEqual(svg, SVG)

        self.process_events(until=lambda: 'foo' in w.html())
        html = '<svg xmlns:dc="...">asd</svg>'
        self.assertEqual(
            w.html(),
            '<html><head></head><body>{}</body></html>'.format(
                # WebKit evaluates first document.write first, whereas
                # WebEngine evaluates onloadJS first
                'foo' + html if HAVE_WEBKIT else html + 'foo'))
예제 #19
0
from os.path import dirname
from unittest import skip

from AnyQt.QtCore import Qt, QObject, pyqtSlot
from AnyQt.QtWidgets import QDialog
from AnyQt.QtTest import QTest

from Orange.widgets.tests.base import WidgetTest
from Orange.widgets.utils.webview import WebviewWidget, HAVE_WEBKIT, wait

SOME_URL = WebviewWidget.toFileURL(dirname(__file__))


@skip("Times out on Travis")
class WebviewWidgetTest(WidgetTest):
    def test_base(self):
        w = WebviewWidget()
        w.evalJS('document.write("foo");')
        SVG = '<svg xmlns:dc="...">asd</svg>'
        w.onloadJS('''document.write('{}');'''.format(SVG))
        w.setUrl(SOME_URL)

        svg = self.process_events(lambda: w.svg())
        self.assertEqual(svg, SVG)

        self.process_events(until=lambda: 'foo' in w.html())
        html = '<svg xmlns:dc="...">asd</svg>'
        self.assertEqual(
            w.html(),
            '<html><head></head><body>{}</body></html>'.format(
                # WebKit evaluates first document.write first, whereas
예제 #20
0
class OW1ka(widget.OWWidget):
    name = "EnKlik Anketa"
    description = "Import data from EnKlikAnketa (1ka.si) public URL."
    icon = "icons/1ka.svg"
    priority = 30
    outputs = [("Data", Table)]

    want_main_area = False
    resizing_enabled = False

    settingsHandler = settings.PerfectDomainContextHandler(
        match_values=settings.PerfectDomainContextHandler.MATCH_VALUES_ALL
    )

    recent = settings.Setting([])
    reload_idx = settings.Setting(0)
    autocommit = settings.Setting(True)
    domain_editor = settings.SettingProvider(DomainEditor)

    UserAdviceMessages = [
        widget.Message(
            'You can import data from public links to 1ka surveys results. '
            'Click to learn more on how to get a shareable public link URL for '
            '1ka surveys that you manage.',
            'public-link',
            icon=widget.Message.Information,
            moreurl='http://english.1ka.si/db/24/468/Guides/Public_link_to_access_data_and_analysis/'
        ),
    ]

    class Error(widget.OWWidget.Error):
        net_error = widget.Msg("Couldn't load data: {}. Ensure network connection, firewall ...")
        parse_error = widget.Msg("Couldn't parse data: {}. Ensure well-formatted data or submit a bug report.")
        invalid_url = widget.Msg('Invalid URL. Public shareable link should match: ' + VALID_URL_HELP)
        data_is_anal = widget.Msg("The provided URL is a public link to 'Analysis'. Need public link to 'Data'.")

    class Information(widget.OWWidget.Information):
        response_data_empty = widget.Msg('Response data is empty. Get some responses first.')

    def __init__(self):
        super().__init__()
        self.table = None
        self._html = None

        def _loadFinished(is_ok):
            if is_ok:
                QTimer.singleShot(1, lambda: setattr(self, '_html', self.webview.html()))

        self.webview = WebviewWidget(loadFinished=_loadFinished)

        vb = gui.vBox(self.controlArea, 'Import Data')
        hb = gui.hBox(vb)
        self.combo = combo = URLComboBox(
            hb, self.recent, editable=True, minimumWidth=400,
            insertPolicy=QComboBox.InsertAtTop,
            toolTip='Format: ' + VALID_URL_HELP,
            editTextChanged=self.is_valid_url,
            # Indirect via QTimer because calling wait() -> processEvents,
            # while our currentIndexChanged event hadn't yet finished.
            # Avoids calling handler twice.
            currentIndexChanged=lambda: QTimer.singleShot(1, self.load_url))
        hb.layout().addWidget(QLabel('Public link URL:', hb))
        hb.layout().addWidget(combo)
        hb.layout().setStretch(1, 2)

        RELOAD_TIMES = (
            ('No reload',),
            ('5 s', 5000),
            ('10 s', 10000),
            ('30 s', 30000),
            ('1 min', 60*1000),
            ('2 min', 2*60*1000),
            ('5 min', 5*60*1000),
        )

        reload_timer = QTimer(self, timeout=lambda: self.load_url(from_reload=True))

        def _on_reload_changed():
            if self.reload_idx == 0:
                reload_timer.stop()
                return
            reload_timer.start(RELOAD_TIMES[self.reload_idx][1])

        gui.comboBox(vb, self, 'reload_idx', label='Reload every:',
                     orientation=Qt.Horizontal,
                     items=[i[0] for i in RELOAD_TIMES],
                     callback=_on_reload_changed)

        box = gui.widgetBox(self.controlArea, "Columns (Double-click to edit)")
        self.domain_editor = DomainEditor(self)
        editor_model = self.domain_editor.model()

        def editorDataChanged():
            self.apply_domain_edit()
            self.commit()

        editor_model.dataChanged.connect(editorDataChanged)
        box.layout().addWidget(self.domain_editor)

        box = gui.widgetBox(self.controlArea, "Info", addSpace=True)
        info = self.data_info = gui.widgetLabel(box, '')
        info.setWordWrap(True)

        self.controlArea.layout().addStretch(1)
        gui.auto_commit(self.controlArea, self, 'autocommit', label='Commit')

        self.set_info()

    def set_combo_items(self):
        self.combo.clear()
        for sheet in self.recent:
            self.combo.addItem(sheet.name, sheet.url)

    def commit(self):
        self.send('Data', self.table)

    def is_valid_url(self, url):
        if is_valid_url(url):
            self.Error.invalid_url.clear()
            return True
        self.Error.invalid_url()
        QToolTip.showText(self.combo.mapToGlobal(QPoint(0, 0)), self.combo.toolTip())

    def load_url(self, from_reload=False):
        self.closeContext()
        self.domain_editor.set_domain(None)

        url = self.combo.currentText()
        if not self.is_valid_url(url):
            self.table = None
            self.commit()
            return

        if url not in self.recent:
            self.recent.insert(0, url)

        prev_table = self.table
        with self.progressBar(3) as progress:
            try:
                self._html = None
                self.webview.setUrl(url)
                wait(until=lambda: self._html is not None)
                progress.advance()
                # Wait some seconds for discrete labels to have loaded via AJAX,
                # then re-query HTML.
                # *Webview.loadFinished doesn't guarantee it sufficiently
                try:
                    wait(until=lambda: False, timeout=1200)
                except TimeoutError:
                    pass
                progress.advance()
                html = self.webview.html()
            except Exception as e:
                log.exception("Couldn't load data from: %s", url)
                self.Error.net_error(try_(lambda: e.args[0], ''))
                self.table = None
            else:
                self.Error.clear()
                self.Information.clear()
                self.table = None
                try:
                    table = self.table = self.table_from_html(html)
                except DataEmptyError:
                    self.Information.response_data_empty()
                except DataIsAnalError:
                    self.Error.data_is_anal()
                except Exception as e:
                    log.exception('Parsing error: %s', url)
                    self.Error.parse_error(try_(lambda: e.args[0], ''))
                else:
                    self.openContext(table.domain)
                    self.combo.setTitleFor(self.combo.currentIndex(), table.name)

        def _equal(data1, data2):
            NAN = float('nan')
            return (try_(lambda: data1.checksum(), NAN) ==
                    try_(lambda: data2.checksum(), NAN))

        self._orig_table = self.table
        self.apply_domain_edit()

        if not (from_reload and _equal(prev_table, self.table)):
            self.commit()

    def apply_domain_edit(self):
        data = self._orig_table
        if data is None:
            self.set_info()
            return

        domain, cols = self.domain_editor.get_domain(data.domain, data)

        # Copied verbatim from OWFile
        if not (domain.variables or domain.metas):
            table = None
        else:
            X, y, m = cols
            table = Table.from_numpy(domain, X, y, m, data.W)
            table.name = data.name
            table.ids = np.array(data.ids)
            table.attributes = getattr(data, 'attributes', {})

        self.table = table
        self.set_info()

    DATETIME_VAR = 'Paradata (insert)'

    def table_from_html(self, html):
        soup = BeautifulSoup(html, 'html.parser')
        try:
            html_table = soup.find_all('table')[-1]
        except IndexError:
            raise DataEmptyError

        if '<h2>Anal' in html or 'div_analiza_' in html:
            raise DataIsAnalError

        def _header_row_strings(row):
            return chain.from_iterable(
                repeat(th.get_text(), int(th.get('colspan') or 1))
                for th in html_table.select('thead tr:nth-of-type(%d) th[title]' % row))

        # self.DATETIME_VAR (available when Paradata is enabled in 1ka UI)
        # should match this variable name format
        header = [th1.rstrip(':') + ('' if th3 == th1 else ' ({})').format(th3.rstrip(':'))
                  for th1, th3 in zip(_header_row_strings(1),
                                      _header_row_strings(3))]
        values = [[(# If no span, feature is a number or a text field
                    td.get_text() if td.span is None else
                    # If have span, it's a number, but if negative, replace with NaN
                    '' if td.contents[0].strip().startswith('-') else
                    # Else if span, the number is its code, but we want its value
                    td.span.get_text()[1:-1])
                   for td in tr.select('td')
                   if 'data_uid' not in td.get('class', ())]
                  for tr in html_table.select('tbody tr')]

        # Save parsed values into in-mem file for default values processing
        buffer = StringIO()
        writer = csv.writer(buffer, delimiter='\t')
        writer.writerow(header)
        writer.writerows(values)
        buffer.flush()
        buffer.seek(0)

        data = TabReader(buffer).read()

        title = soup.select('body h2:nth-of-type(1)')[0].get_text().split(': ', maxsplit=1)[-1]
        data.name = title

        return data

    def set_info(self):
        data = self.table
        if data is None:
            self.data_info.setText('No spreadsheet loaded.')
            return
        text = "{}\n\n{} instance(s), {} feature(s), {} meta attribute(s)\n".format(
            data.name, len(data), len(data.domain.attributes), len(data.domain.metas))
        text += try_(lambda: '\nFirst entry: {}'
                             '\nLast entry: {}'.format(data[0, self.DATETIME_VAR],
                                                       data[-1, self.DATETIME_VAR]), '')
        self.data_info.setText(text)
예제 #21
0
from os.path import dirname

from AnyQt.QtCore import Qt, QObject, pyqtSlot
from AnyQt.QtWidgets import QDialog, qApp
from AnyQt.QtTest import QTest

from Orange.widgets.tests.base import WidgetTest
from Orange.widgets.utils.webview import WebviewWidget

SOME_URL = WebviewWidget.toFileURL(dirname(__file__))


class WebviewWidgetTest(WidgetTest):
    def test_base(self):
        w = WebviewWidget()
        w.evalJS('document.write("foo");')
        SVG = '<svg xmlns:dc="...">asd</svg>'
        w.onloadJS('''document.write('{}');'''.format(SVG))
        w.setUrl(SOME_URL)

        svg = None
        while svg is None:
            try:
                svg = w.svg()
                break
            except ValueError:
                qApp.processEvents()
        self.assertEqual(svg, SVG)
        self.assertEqual(
            w.html(), '<html><head></head><body>foo<svg xmlns:dc="...">asd</svg></body></html>')
예제 #22
0
class OW1ka(widget.OWWidget):
    name = "EnKlik Anketa"
    description = "Import data from EnKlikAnketa (1ka.si) public URL."
    icon = "icons/1ka.svg"
    priority = 200

    class Outputs:
        data = Output("Data", Table)

    want_main_area = False
    resizing_enabled = False

    settingsHandler = settings.PerfectDomainContextHandler(
        match_values=settings.PerfectDomainContextHandler.MATCH_VALUES_ALL)

    recent = settings.Setting([])
    reload_idx = settings.Setting(0)
    autocommit = settings.Setting(True)
    domain_editor = settings.SettingProvider(DomainEditor)

    UserAdviceMessages = [
        widget.Message(
            'You can import data from public links to 1ka surveys results. '
            'Click to learn more on how to get a shareable public link URL for '
            '1ka surveys that you manage.',
            'public-link',
            icon=widget.Message.Information,
            moreurl=
            'http://english.1ka.si/db/24/468/Guides/Public_link_to_access_data_and_analysis/'
        ),
    ]

    class Error(widget.OWWidget.Error):
        net_error = widget.Msg(
            "Couldn't load data: {}. Ensure network connection, firewall ...")
        parse_error = widget.Msg(
            "Couldn't parse data: {}. Ensure well-formatted data or submit a bug report."
        )
        invalid_url = widget.Msg(
            'Invalid URL. Public shareable link should match: ' +
            VALID_URL_HELP)
        data_is_anal = widget.Msg(
            "The provided URL is a public link to 'Analysis'. Need public link to 'Data'."
        )

    class Information(widget.OWWidget.Information):
        response_data_empty = widget.Msg(
            'Response data is empty. Get some responses first.')

    def __init__(self):
        super().__init__()
        self.table = None
        self._html = None

        def _loadFinished(is_ok):
            if is_ok:
                QTimer.singleShot(
                    1, lambda: setattr(self, '_html', self.webview.html()))

        self.webview = WebviewWidget(loadFinished=_loadFinished)

        vb = gui.vBox(self.controlArea, 'Import Data')
        hb = gui.hBox(vb)
        self.combo = combo = URLComboBox(
            hb,
            self.recent,
            editable=True,
            minimumWidth=400,
            insertPolicy=QComboBox.InsertAtTop,
            toolTip='Format: ' + VALID_URL_HELP,
            editTextChanged=self.is_valid_url,
            # Indirect via QTimer because calling wait() -> processEvents,
            # while our currentIndexChanged event hadn't yet finished.
            # Avoids calling handler twice.
            currentIndexChanged=lambda: QTimer.singleShot(1, self.load_url))
        hb.layout().addWidget(QLabel('Public link URL:', hb))
        hb.layout().addWidget(combo)
        hb.layout().setStretch(1, 2)

        RELOAD_TIMES = (
            ('No reload', ),
            ('5 s', 5000),
            ('10 s', 10000),
            ('30 s', 30000),
            ('1 min', 60 * 1000),
            ('2 min', 2 * 60 * 1000),
            ('5 min', 5 * 60 * 1000),
        )

        reload_timer = QTimer(self,
                              timeout=lambda: self.load_url(from_reload=True))

        def _on_reload_changed():
            if self.reload_idx == 0:
                reload_timer.stop()
                return
            reload_timer.start(RELOAD_TIMES[self.reload_idx][1])

        gui.comboBox(vb,
                     self,
                     'reload_idx',
                     label='Reload every:',
                     orientation=Qt.Horizontal,
                     items=[i[0] for i in RELOAD_TIMES],
                     callback=_on_reload_changed)

        box = gui.widgetBox(self.controlArea, "Columns (Double-click to edit)")
        self.domain_editor = DomainEditor(self)
        editor_model = self.domain_editor.model()

        def editorDataChanged():
            self.apply_domain_edit()
            self.commit()

        editor_model.dataChanged.connect(editorDataChanged)
        box.layout().addWidget(self.domain_editor)

        box = gui.widgetBox(self.controlArea, "Info", addSpace=True)
        info = self.data_info = gui.widgetLabel(box, '')
        info.setWordWrap(True)

        self.controlArea.layout().addStretch(1)
        gui.auto_commit(self.controlArea, self, 'autocommit', label='Commit')

        self.set_info()

    def set_combo_items(self):
        self.combo.clear()
        for sheet in self.recent:
            self.combo.addItem(sheet.name, sheet.url)

    def commit(self):
        self.Outputs.data.send(self.table)

    def is_valid_url(self, url):
        if is_valid_url(url):
            self.Error.invalid_url.clear()
            return True
        self.Error.invalid_url()
        QToolTip.showText(self.combo.mapToGlobal(QPoint(0, 0)),
                          self.combo.toolTip())

    def load_url(self, from_reload=False):
        self.closeContext()
        self.domain_editor.set_domain(None)

        url = self.combo.currentText()
        if not self.is_valid_url(url):
            self.table = None
            self.commit()
            return

        if url not in self.recent:
            self.recent.insert(0, url)

        prev_table = self.table
        with self.progressBar(3) as progress:
            try:
                self._html = None
                self.webview.setUrl(url)
                wait(until=lambda: self._html is not None)
                progress.advance()
                # Wait some seconds for discrete labels to have loaded via AJAX,
                # then re-query HTML.
                # *Webview.loadFinished doesn't guarantee it sufficiently
                try:
                    wait(until=lambda: False, timeout=1200)
                except TimeoutError:
                    pass
                progress.advance()
                html = self.webview.html()
            except Exception as e:
                log.exception("Couldn't load data from: %s", url)
                self.Error.net_error(try_(lambda: e.args[0], ''))
                self.table = None
            else:
                self.Error.clear()
                self.Information.clear()
                self.table = None
                try:
                    table = self.table = self.table_from_html(html)
                except DataEmptyError:
                    self.Information.response_data_empty()
                except DataIsAnalError:
                    self.Error.data_is_anal()
                except Exception as e:
                    log.exception('Parsing error: %s', url)
                    self.Error.parse_error(try_(lambda: e.args[0], ''))
                else:
                    self.openContext(table.domain)
                    self.combo.setTitleFor(self.combo.currentIndex(),
                                           table.name)

        def _equal(data1, data2):
            NAN = float('nan')
            return (try_(lambda: data1.checksum(),
                         NAN) == try_(lambda: data2.checksum(), NAN))

        self._orig_table = self.table
        self.apply_domain_edit()

        if not (from_reload and _equal(prev_table, self.table)):
            self.commit()

    def apply_domain_edit(self):
        data = self._orig_table
        if data is None:
            self.set_info()
            return

        domain, cols = self.domain_editor.get_domain(data.domain, data)

        # Copied verbatim from OWFile
        if not (domain.variables or domain.metas):
            table = None
        else:
            X, y, m = cols
            table = Table.from_numpy(domain, X, y, m, data.W)
            table.name = data.name
            table.ids = np.array(data.ids)
            table.attributes = getattr(data, 'attributes', {})

        self.table = table
        self.set_info()

    DATETIME_VAR = 'Paradata (insert)'

    def table_from_html(self, html):
        soup = BeautifulSoup(html, 'html.parser')
        try:
            html_table = soup.find_all('table')[-1]
        except IndexError:
            raise DataEmptyError

        if '<h2>Anal' in html or 'div_analiza_' in html:
            raise DataIsAnalError

        def _header_row_strings(row):
            return chain.from_iterable(
                repeat(th.get_text(), int(th.get('colspan') or 1)) for th in
                html_table.select('thead tr:nth-of-type(%d) th[title]' % row))

        # self.DATETIME_VAR (available when Paradata is enabled in 1ka UI)
        # should match this variable name format
        header = [
            th1.rstrip(':') +
            ('' if th3 == th1 else ' ({})').format(th3.rstrip(':'))
            for th1, th3 in zip(_header_row_strings(1), _header_row_strings(3))
        ]
        values = [
            [
                (  # If no span, feature is a number or a text field
                    td.get_text() if td.span is None else
                    # If have span, it's a number, but if negative, replace with NaN
                    '' if td.contents[0].strip().startswith('-') else
                    # Else if span, the number is its code, but we want its value
                    td.span.get_text()[1:-1]) for td in tr.select('td')
                if 'data_uid' not in td.get('class', ())
            ] for tr in html_table.select('tbody tr')
        ]

        # Save parsed values into in-mem file for default values processing
        buffer = StringIO()
        writer = csv.writer(buffer, delimiter='\t')
        writer.writerow(header)
        writer.writerows(values)
        buffer.flush()
        buffer.seek(0)

        data = TabReader(buffer).read()

        title = soup.select('body h2:nth-of-type(1)')[0].get_text().split(
            ': ', maxsplit=1)[-1]
        data.name = title

        return data

    def set_info(self):
        data = self.table
        if data is None:
            self.data_info.setText('No spreadsheet loaded.')
            return
        text = "{}\n\n{} instance(s), {} feature(s), {} meta attribute(s)\n".format(
            data.name, len(data), len(data.domain.attributes),
            len(data.domain.metas))
        text += try_(
            lambda: '\nFirst entry: {}'
            '\nLast entry: {}'.format(data[0, self.DATETIME_VAR], data[
                -1, self.DATETIME_VAR]), '')
        self.data_info.setText(text)
예제 #23
0
    def __init__(self):
        super().__init__()
        self.table = None
        self._html = None

        def _loadFinished(is_ok):
            if is_ok:
                QTimer.singleShot(
                    1, lambda: setattr(self, '_html', self.webview.html()))

        self.webview = WebviewWidget(loadFinished=_loadFinished)

        vb = gui.vBox(self.controlArea, 'Import Data')
        hb = gui.hBox(vb)
        self.combo = combo = URLComboBox(
            hb,
            self.recent,
            editable=True,
            minimumWidth=400,
            insertPolicy=QComboBox.InsertAtTop,
            toolTip='Format: ' + VALID_URL_HELP,
            editTextChanged=self.is_valid_url,
            # Indirect via QTimer because calling wait() -> processEvents,
            # while our currentIndexChanged event hadn't yet finished.
            # Avoids calling handler twice.
            currentIndexChanged=lambda: QTimer.singleShot(1, self.load_url))
        hb.layout().addWidget(QLabel('Public link URL:', hb))
        hb.layout().addWidget(combo)
        hb.layout().setStretch(1, 2)

        RELOAD_TIMES = (
            ('No reload', ),
            ('5 s', 5000),
            ('10 s', 10000),
            ('30 s', 30000),
            ('1 min', 60 * 1000),
            ('2 min', 2 * 60 * 1000),
            ('5 min', 5 * 60 * 1000),
        )

        reload_timer = QTimer(self,
                              timeout=lambda: self.load_url(from_reload=True))

        def _on_reload_changed():
            if self.reload_idx == 0:
                reload_timer.stop()
                return
            reload_timer.start(RELOAD_TIMES[self.reload_idx][1])

        gui.comboBox(vb,
                     self,
                     'reload_idx',
                     label='Reload every:',
                     orientation=Qt.Horizontal,
                     items=[i[0] for i in RELOAD_TIMES],
                     callback=_on_reload_changed)

        box = gui.widgetBox(self.controlArea, "Columns (Double-click to edit)")
        self.domain_editor = DomainEditor(self)
        editor_model = self.domain_editor.model()

        def editorDataChanged():
            self.apply_domain_edit()
            self.commit()

        editor_model.dataChanged.connect(editorDataChanged)
        box.layout().addWidget(self.domain_editor)

        box = gui.widgetBox(self.controlArea, "Info", addSpace=True)
        info = self.data_info = gui.widgetLabel(box, '')
        info.setWordWrap(True)

        self.controlArea.layout().addStretch(1)
        gui.auto_commit(self.controlArea, self, 'autocommit', label='Commit')

        self.set_info()
class OWReport(OWWidget):
    name = "Report"
    save_dir = Setting("")
    open_dir = Setting("")

    def __init__(self):
        super().__init__()
        self._setup_ui_()
        self.report_changed = False

        index_file = pkg_resources.resource_filename(__name__, "index.html")
        with open(index_file, "r") as f:
            self.report_html_template = f.read()

    def _setup_ui_(self):
        self.table_model = ReportItemModel(0, len(Column.__members__))
        self.table = ReportTable(self.controlArea)
        self.table.setModel(self.table_model)
        self.table.setShowGrid(False)
        self.table.setSelectionBehavior(QTableView.SelectRows)
        self.table.setSelectionMode(QTableView.SingleSelection)
        self.table.setWordWrap(False)
        self.table.setMouseTracking(True)
        self.table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed)
        self.table.verticalHeader().setDefaultSectionSize(20)
        self.table.verticalHeader().setVisible(False)
        self.table.horizontalHeader().setVisible(False)
        self.table.setFixedWidth(250)
        self.table.setColumnWidth(Column.item, 200)
        self.table.setColumnWidth(Column.remove, 23)
        self.table.setColumnWidth(Column.scheme, 25)
        self.table.clicked.connect(self._table_clicked)
        self.table.selectionModel().selectionChanged.connect(
            self._table_selection_changed)
        self.controlArea.layout().addWidget(self.table)

        self.last_scheme = None
        self.scheme_button = gui.button(self.controlArea,
                                        self,
                                        "Back to Last Scheme",
                                        callback=self._show_last_scheme)
        box = gui.hBox(self.controlArea)
        box.setContentsMargins(-6, 0, -6, 0)
        self.save_button = gui.button(box,
                                      self,
                                      "Save",
                                      callback=self.save_report)
        self.print_button = gui.button(box,
                                       self,
                                       "Print",
                                       callback=self._print_report)

        class PyBridge(QObject):
            @pyqtSlot(str)
            def _select_item(myself, item_id):
                item = self.table_model.get_item_by_id(item_id)
                self.table.selectRow(
                    self.table_model.indexFromItem(item).row())
                self._change_selected_item(item)

            @pyqtSlot(str, str)
            def _add_comment(myself, item_id, value):
                item = self.table_model.get_item_by_id(item_id)
                item.comment = value
                self.report_changed = True

        self.report_view = WebviewWidget(self.mainArea, bridge=PyBridge(self))
        self.mainArea.layout().addWidget(self.report_view)

    @deprecated("Widgets should not be pickled")
    def __getstate__(self):
        rep_dict = self.__dict__.copy()
        for key in ('_OWWidget__env', 'controlArea', 'mainArea', 'report_view',
                    'table', 'table_model'):
            del rep_dict[key]
        items_len = self.table_model.rowCount()
        return rep_dict, [self.table_model.item(i) for i in range(items_len)]

    @deprecated("Widgets should not be pickled")
    def __setstate__(self, state):
        rep_dict, items = state
        self.__dict__.update(rep_dict)
        self._setup_ui_()
        for i in range(len(items)):
            item = items[i]
            self.table_model.add_item(
                ReportItem(item.name, item.html, item.scheme, item.module,
                           item.icon_name, item.comment))

    def _table_clicked(self, index):
        if index.column() == Column.remove:
            self._remove_item(index.row())
            indexes = self.table.selectionModel().selectedIndexes()
            if indexes:
                item = self.table_model.item(indexes[0].row())
                self._scroll_to_item(item)
                self._change_selected_item(item)
        if index.column() == Column.scheme:
            self._show_scheme(index.row())

    def _table_selection_changed(self, new_selection, _):
        if new_selection.indexes():
            item = self.table_model.item(new_selection.indexes()[0].row())
            self._scroll_to_item(item)
            self._change_selected_item(item)

    def _remove_item(self, row):
        self.table_model.removeRow(row)
        self.report_changed = True
        self._build_html()

    def clear(self):
        self.table_model.clear()
        self.report_changed = True
        self._build_html()

    def _add_item(self, widget):
        name = widget.get_widget_name_extension()
        name = "{} - {}".format(widget.name, name) if name else widget.name
        item = ReportItem(name, widget.report_html, self._get_scheme(),
                          widget.__module__, widget.icon)
        self.table_model.add_item(item)
        self.report_changed = True
        return item

    def _build_html(self):
        html = self.report_html_template
        html += "<body>"
        for i in range(self.table_model.rowCount()):
            item = self.table_model.item(i)
            html += "<div id='{}' class='normal' " \
                    "onClick='pybridge._select_item(this.id)'>{}<div " \
                    "class='textwrapper'><textarea " \
                    "placeholder='Write a comment...'" \
                    "onInput='this.innerHTML = this.value;" \
                    "pybridge._add_comment(this.parentNode.parentNode.id, this.value);'" \
                    ">{}</textarea></div>" \
                    "</div>".format(item.id, item.html, item.comment)
        html += "</body></html>"
        self.report_view.setHtml(html)

    def _scroll_to_item(self, item):
        self.report_view.evalJS(
            "document.getElementById('{}').scrollIntoView();".format(item.id))

    def _change_selected_item(self, item):
        self.report_view.evalJS(
            "var sel_el = document.getElementsByClassName('selected')[0]; "
            "if (sel_el.id != {}) "
            "   sel_el.className = 'normal';".format(item.id))
        self.report_view.evalJS(
            "document.getElementById('{}').className = 'selected';".format(
                item.id))
        self.report_changed = True

    def make_report(self, widget):
        item = self._add_item(widget)
        self._build_html()
        self._scroll_to_item(item)
        self.table.selectRow(self.table_model.rowCount() - 1)

    def _get_scheme(self):
        canvas = self.get_canvas_instance()
        return canvas.get_scheme_xml() if canvas else None

    def _show_scheme(self, row):
        scheme = self.table_model.item(row).scheme
        canvas = self.get_canvas_instance()
        if canvas:
            document = canvas.current_document()
            if document.isModifiedStrict():
                self.last_scheme = canvas.get_scheme_xml()
            canvas.load_scheme_xml(scheme)

    def _show_last_scheme(self):
        if self.last_scheme:
            canvas = self.get_canvas_instance()
            if canvas:
                canvas.load_scheme_xml(self.last_scheme)

    def save_report(self):
        """Save report"""
        filename, _ = QFileDialog.getSaveFileName(
            self, "Save Report", self.save_dir,
            "HTML (*.html);;PDF (*.pdf);;Report (*.report)")
        if not filename:
            return QDialog.Rejected

        self.save_dir = os.path.dirname(filename)
        self.saveSettings()
        _, extension = os.path.splitext(filename)
        if extension == ".pdf":
            printer = QPrinter()
            printer.setPageSize(QPrinter.A4)
            printer.setOutputFormat(QPrinter.PdfFormat)
            printer.setOutputFileName(filename)
            self.report_view.print_(printer)
        elif extension == ".report":
            self.save(filename)
        else:

            def save_html(contents):
                try:
                    with open(filename, "w", encoding="utf-8") as f:
                        f.write(contents)
                except PermissionError:
                    self.permission_error(filename)

            save_html(self.report_view.html())
        self.report_changed = False
        return QDialog.Accepted

    def _print_report(self):
        printer = QPrinter()
        print_dialog = QPrintDialog(printer, self)
        print_dialog.setWindowTitle("Print report")
        if print_dialog.exec_() != QDialog.Accepted:
            return
        self.report_view.print_(printer)

    def open_report(self):
        filename, _ = QFileDialog.getOpenFileName(self, "Open Report",
                                                  self.open_dir,
                                                  "Report (*.report)")
        if not filename:
            return

        self.report_changed = False
        self.open_dir = os.path.dirname(filename)
        self.saveSettings()

        try:
            report = self.load(filename)
        except (IOError, AttributeError, pickle.UnpicklingError) as e:
            message_critical(self.tr("Could not load an Orange Report file"),
                             title=self.tr("Error"),
                             informative_text=self.tr(
                                 "Error occurred "
                                 "while loading '{}'.").format(filename),
                             exc_info=True,
                             parent=self)
            log.error(str(e), exc_info=True)
            return
        self.set_instance(report)
        self = report
        self._build_html()
        self.table.selectRow(0)
        self.show()
        self.raise_()

    def save(self, filename):
        attributes = {}
        for key in ('last_scheme', 'open_dir'):
            attributes[key] = getattr(self, key, None)
        items = [
            self.table_model.item(i)
            for i in range(self.table_model.rowCount())
        ]
        report = dict(__version__=1, attributes=attributes, items=items)

        try:
            with open(filename, 'wb') as f:
                pickle.dump(report, f)
        except PermissionError:
            self.permission_error(filename)

    @classmethod
    def load(cls, filename):
        with open(filename, 'rb') as f:
            report = pickle.load(f)

        if not isinstance(report, dict):
            return report

        self = cls()
        self.__dict__.update(report['attributes'])
        for item in report['items']:
            self.table_model.add_item(
                ReportItem(item.name, item.html, item.scheme, item.module,
                           item.icon_name, item.comment))
        return self

    def permission_error(self, filename):
        message_critical(
            self.tr("Permission error when trying to write report."),
            title=self.tr("Error"),
            informative_text=self.tr("Permission error occurred "
                                     "while saving '{}'.").format(filename),
            exc_info=True,
            parent=self)
        log.error("PermissionError when trying to write report.",
                  exc_info=True)

    def is_empty(self):
        return not self.table_model.rowCount()

    def is_changed(self):
        return self.report_changed

    @staticmethod
    def set_instance(report):
        app_inst = QApplication.instance()
        app_inst._report_window = report

    @staticmethod
    def get_instance():
        app_inst = QApplication.instance()
        if not hasattr(app_inst, "_report_window"):
            report = OWReport()
            app_inst._report_window = report
        return app_inst._report_window

    @staticmethod
    def get_canvas_instance():
        for widget in QApplication.topLevelWidgets():
            if isinstance(widget, CanvasMainWindow):
                return widget
예제 #25
0
class OWReport(OWWidget):
    name = "Report"
    save_dir = Setting("")
    open_dir = Setting("")

    def __init__(self):
        super().__init__()
        self._setup_ui_()
        self.report_changed = False

        index_file = pkg_resources.resource_filename(__name__, "index.html")
        with open(index_file, "r") as f:
            self.report_html_template = f.read()

    def _setup_ui_(self):
        self.table_model = ReportItemModel(0, len(Column.__members__))
        self.table = ReportTable(self.controlArea)
        self.table.setModel(self.table_model)
        self.table.setShowGrid(False)
        self.table.setSelectionBehavior(QTableView.SelectRows)
        self.table.setSelectionMode(QTableView.SingleSelection)
        self.table.setWordWrap(False)
        self.table.setMouseTracking(True)
        self.table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed)
        self.table.verticalHeader().setDefaultSectionSize(20)
        self.table.verticalHeader().setVisible(False)
        self.table.horizontalHeader().setVisible(False)
        self.table.setFixedWidth(250)
        self.table.setColumnWidth(Column.item, 200)
        self.table.setColumnWidth(Column.remove, 23)
        self.table.setColumnWidth(Column.scheme, 25)
        self.table.clicked.connect(self._table_clicked)
        self.table.selectionModel().selectionChanged.connect(
            self._table_selection_changed)
        self.controlArea.layout().addWidget(self.table)

        self.last_scheme = None
        self.scheme_button = gui.button(
            self.controlArea, self, "Back to Last Scheme",
            callback=self._show_last_scheme
        )
        box = gui.hBox(self.controlArea)
        box.setContentsMargins(-6, 0, -6, 0)
        self.save_button = gui.button(
            box, self, "Save", callback=self.save_report, disabled=True
        )
        self.print_button = gui.button(
            box, self, "Print", callback=self._print_report, disabled=True
        )

        class PyBridge(QObject):
            @pyqtSlot(str)
            def _select_item(myself, item_id):
                item = self.table_model.get_item_by_id(item_id)
                self.table.selectRow(self.table_model.indexFromItem(item).row())
                self._change_selected_item(item)

            @pyqtSlot(str, str)
            def _add_comment(myself, item_id, value):
                item = self.table_model.get_item_by_id(item_id)
                item.comment = value
                self.report_changed = True

        self.report_view = WebviewWidget(self.mainArea, bridge=PyBridge(self))
        self.mainArea.layout().addWidget(self.report_view)

    @deprecated("Widgets should not be pickled")
    def __getstate__(self):
        rep_dict = self.__dict__.copy()
        for key in ('_OWWidget__env', 'controlArea', 'mainArea',
                    'report_view', 'table', 'table_model'):
            del rep_dict[key]
        items_len = self.table_model.rowCount()
        return rep_dict, [self.table_model.item(i) for i in range(items_len)]

    @deprecated("Widgets should not be pickled")
    def __setstate__(self, state):
        rep_dict, items = state
        self.__dict__.update(rep_dict)
        self._setup_ui_()
        for i in range(len(items)):
            item = items[i]
            self.table_model.add_item(
                ReportItem(item.name, item.html, item.scheme,
                           item.module, item.icon_name, item.comment)
            )

    def _table_clicked(self, index):
        if index.column() == Column.remove:
            self._remove_item(index.row())
            indexes = self.table.selectionModel().selectedIndexes()
            if indexes:
                item = self.table_model.item(indexes[0].row())
                self._scroll_to_item(item)
                self._change_selected_item(item)
        if index.column() == Column.scheme:
            self._show_scheme(index.row())

    def _table_selection_changed(self, new_selection, _):
        if new_selection.indexes():
            item = self.table_model.item(new_selection.indexes()[0].row())
            self._scroll_to_item(item)
            self._change_selected_item(item)

    def _remove_item(self, row):
        self.table_model.removeRow(row)
        self._empty_report()
        self.report_changed = True
        self._build_html()

    def clear(self):
        self.table_model.clear()
        self._empty_report()
        self.report_changed = True
        self._build_html()

    def _add_item(self, widget):
        name = widget.get_widget_name_extension()
        name = "{} - {}".format(widget.name, name) if name else widget.name
        item = ReportItem(name, widget.report_html, self._get_scheme(),
                          widget.__module__, widget.icon)
        self.table_model.add_item(item)
        self._empty_report()
        self.report_changed = True
        return item

    def _empty_report(self):
        # disable save and print if no reports
        self.save_button.setEnabled(self.table_model.rowCount())
        self.print_button.setEnabled(self.table_model.rowCount())

    def _build_html(self):
        html = self.report_html_template
        html += "<body>"
        for i in range(self.table_model.rowCount()):
            item = self.table_model.item(i)
            html += "<div id='{}' class='normal' " \
                    "onClick='pybridge._select_item(this.id)'>{}<div " \
                    "class='textwrapper'><textarea " \
                    "placeholder='Write a comment...'" \
                    "onInput='this.innerHTML = this.value;" \
                    "pybridge._add_comment(this.parentNode.parentNode.id, this.value);'" \
                    ">{}</textarea></div>" \
                    "</div>".format(item.id, item.html, item.comment)
        html += "</body></html>"
        self.report_view.setHtml(html)

    def _scroll_to_item(self, item):
        self.report_view.evalJS(
            "document.getElementById('{}').scrollIntoView();".format(item.id)
        )

    def _change_selected_item(self, item):
        self.report_view.evalJS(
            "var sel_el = document.getElementsByClassName('selected')[0]; "
            "if (sel_el.id != {}) "
            "   sel_el.className = 'normal';".format(item.id))
        self.report_view.evalJS(
            "document.getElementById('{}').className = 'selected';"
            .format(item.id))
        self.report_changed = True

    def make_report(self, widget):
        item = self._add_item(widget)
        self._build_html()
        self._scroll_to_item(item)
        self.table.selectRow(self.table_model.rowCount() - 1)

    def _get_scheme(self):
        canvas = self.get_canvas_instance()
        return canvas.get_scheme_xml() if canvas else None

    def _show_scheme(self, row):
        scheme = self.table_model.item(row).scheme
        canvas = self.get_canvas_instance()
        if canvas:
            document = canvas.current_document()
            if document.isModifiedStrict():
                self.last_scheme = canvas.get_scheme_xml()
            self._load_scheme(scheme)

    def _show_last_scheme(self):
        if self.last_scheme:
            self._load_scheme(self.last_scheme)

    def _load_scheme(self, contents):
        # forcibly load the contents into the associated CanvasMainWindow
        # instance if one exists. Preserve `self` as the designated report.
        canvas = self.get_canvas_instance()
        if canvas is not None:
            document = canvas.current_document()
            old = document.scheme()
            if old.has_report() and old.report_view() is self:
                # remove self so it is not closed
                old.set_report_view(None)
            canvas.load_scheme_xml(contents)
            scheme = canvas.current_document().scheme()
            scheme.set_report_view(self)

    def save_report(self):
        """Save report"""
        formats = OrderedDict((('HTML (*.html)', '.html'),
                               ('PDF (*.pdf)', '.pdf'),
                               ('Report (*.report)', '.report')))

        filename, selected_format = QFileDialog.getSaveFileName(
            self, "Save Report", self.save_dir, ';;'.join(formats.keys()))
        if not filename:
            return QDialog.Rejected

        # Set appropriate extension if not set by the user
        expect_ext = formats[selected_format]
        if not filename.endswith(expect_ext):
            filename += expect_ext

        self.save_dir = os.path.dirname(filename)
        self.saveSettings()
        _, extension = os.path.splitext(filename)
        if extension == ".pdf":
            printer = QPrinter()
            printer.setPageSize(QPrinter.A4)
            printer.setOutputFormat(QPrinter.PdfFormat)
            printer.setOutputFileName(filename)
            self._print_to_printer(printer)
        elif extension == ".report":
            self.save(filename)
        else:
            def save_html(contents):
                try:
                    with open(filename, "w", encoding="utf-8") as f:
                        f.write(contents)
                except PermissionError:
                    self.permission_error(filename)

            save_html(self.report_view.html())
        self.report_changed = False
        return QDialog.Accepted

    def _print_to_printer(self, printer):
        filename = printer.outputFileName()
        if filename:
            try:
                # QtWebEngine
                return self.report_view.page().printToPdf(filename)
            except AttributeError:
                try:
                    # QtWebKit
                    return self.report_view.print_(printer)
                except AttributeError:
                    # QtWebEngine 5.6
                    pass
        # Fallback to printing widget as an image
        self.report_view.render(printer)

    def _print_report(self):
        printer = QPrinter()
        print_dialog = QPrintDialog(printer, self)
        print_dialog.setWindowTitle("Print report")
        if print_dialog.exec_() != QDialog.Accepted:
            return
        self._print_to_printer(printer)

    def save(self, filename):
        attributes = {}
        for key in ('last_scheme', 'open_dir'):
            attributes[key] = getattr(self, key, None)
        items = [self.table_model.item(i)
                 for i in range(self.table_model.rowCount())]
        report = dict(__version__=1,
                      attributes=attributes,
                      items=items)

        try:
            with open(filename, 'wb') as f:
                pickle.dump(report, f)
        except PermissionError:
            self.permission_error(filename)

    @classmethod
    def load(cls, filename):
        with open(filename, 'rb') as f:
            report = pickle.load(f)

        if not isinstance(report, dict):
            return report

        self = cls()
        self.__dict__.update(report['attributes'])
        for item in report['items']:
            self.table_model.add_item(
                ReportItem(item.name, item.html, item.scheme,
                           item.module, item.icon_name, item.comment)
            )
        return self

    def permission_error(self, filename):
        message_critical(
            self.tr("Permission error when trying to write report."),
            title=self.tr("Error"),
            informative_text=self.tr("Permission error occurred "
                                     "while saving '{}'.").format(filename),
            exc_info=True,
            parent=self)
        log.error("PermissionError when trying to write report.", exc_info=True)

    def is_empty(self):
        return not self.table_model.rowCount()

    def is_changed(self):
        return self.report_changed

    @staticmethod
    def set_instance(report):
        warnings.warn(
            "OWReport.set_instance is deprecated",
            DeprecationWarning, stacklevel=2
        )
        app_inst = QApplication.instance()
        app_inst._report_window = report

    @staticmethod
    def get_instance():
        warnings.warn(
            "OWReport.get_instance is deprecated",
            DeprecationWarning, stacklevel=2
        )
        app_inst = QApplication.instance()
        if not hasattr(app_inst, "_report_window"):
            report = OWReport()
            app_inst._report_window = report
        return app_inst._report_window

    def get_canvas_instance(self):
        # type: () -> Optional[CanvasMainWindow]
        """
        Return a CanvasMainWindow instance to which this report is attached.

        Return None if not associated with any window.

        Returns
        -------
        window : Optional[CanvasMainWindow]
        """
        # Run up the parent/window chain
        parent = self.parent()
        if parent is not None:
            window = parent.window()
            if isinstance(window, CanvasMainWindow):
                return window
        return None

    def copy_to_clipboard(self):
        self.report_view.triggerPageAction(self.report_view.page().Copy)