def __init__(self): super().__init__() self.data = None self._invalidated = False # List of available preprocessors (DescriptionRole : Description) self.preprocessors = QStandardItemModel() def mimeData(indexlist): assert len(indexlist) == 1 index = indexlist[0] qname = index.data(DescriptionRole).qualname m = QMimeData() m.setData("application/x-qwidget-ref", qname.encode("utf-8")) return m # TODO: Fix this (subclass even if just to pass a function # for mimeData delegate) self.preprocessors.mimeData = mimeData box = gui.vBox(self.controlArea, "Preprocessors") self.preprocessorsView = view = QListView( selectionMode=QListView.SingleSelection, dragEnabled=True, dragDropMode=QListView.DragOnly) view.setModel(self.preprocessors) view.activated.connect(self.__activated) box.layout().addWidget(view) #### self._qname2ppdef = {ppdef.qualname: ppdef for ppdef in PREPROCESSORS} # List of 'selected' preprocessors and their parameters. self.preprocessormodel = None self.flow_view = SequenceFlow() self.controler = Controller(self.flow_view, parent=self) self.overlay = OverlayWidget(self) self.overlay.setAttribute(Qt.WA_TransparentForMouseEvents) self.overlay.setWidget(self.flow_view) self.overlay.setLayout(QVBoxLayout()) self.overlay.layout().addWidget( QLabel("Drag items from the list on the left", wordWrap=True)) self.scroll_area = QScrollArea( verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn) self.scroll_area.viewport().setAcceptDrops(True) self.scroll_area.setWidget(self.flow_view) self.scroll_area.setWidgetResizable(True) self.mainArea.layout().addWidget(self.scroll_area) self.flow_view.installEventFilter(self) box = gui.vBox(self.controlArea, "Output") gui.auto_commit(box, self, "autocommit", "Send", box=False) self._initialize()
def __init__(self, parent=None, flags=Qt.Dialog, **kwargs): super().__init__(parent, flags, **kwargs) self.setLayout(QVBoxLayout()) self._options = None self._path = None # Finalizer for opened file handle (in _update_preview) self.__finalizer = None # type: Optional[Callable[[], None]] self._optionswidget = textimport.CSVImportWidget() self._optionswidget.previewReadErrorOccurred.connect( self.__on_preview_error ) self._optionswidget.previewModelReset.connect( self.__on_preview_reset ) self._buttons = buttons = QDialogButtonBox( orientation=Qt.Horizontal, standardButtons=(QDialogButtonBox.Ok | QDialogButtonBox.Cancel | QDialogButtonBox.Reset | QDialogButtonBox.RestoreDefaults), objectName="dialog-button-box", ) # TODO: Help button buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) b = buttons.button(QDialogButtonBox.Reset) b.clicked.connect(self.reset) b = buttons.button(QDialogButtonBox.RestoreDefaults) b.clicked.connect(self.restoreDefaults) self.layout().addWidget(self._optionswidget) self.layout().addWidget(buttons) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self._overlay = OverlayWidget(self) self._overlay.setWidget(self._optionswidget.dataview) self._overlay.setLayout(QVBoxLayout()) self._overlay.layout().addWidget(QLabel(wordWrap=True)) self._overlay.hide()
def __init__(self): super().__init__() self.data = None self._invalidated = False # List of available preprocessors (DescriptionRole : Description) self.preprocessors = QStandardItemModel() def mimeData(indexlist): assert len(indexlist) == 1 index = indexlist[0] qname = index.data(DescriptionRole).qualname m = QMimeData() m.setData("application/x-qwidget-ref", qname.encode("utf-8")) return m # TODO: Fix this (subclass even if just to pass a function # for mimeData delegate) self.preprocessors.mimeData = mimeData box = gui.vBox(self.controlArea, "Preprocessors") gui.rubber(self.controlArea) # we define a class that lets us set the vertical sizeHint # based on the height and number of items in the list # see self.__update_list_sizeHint class ListView(QListView): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.vertical_hint = None def sizeHint(self): sh = super().sizeHint() if self.vertical_hint: return QSize(sh.width(), self.vertical_hint) return sh self.preprocessorsView = view = ListView( selectionMode=QListView.SingleSelection, dragEnabled=True, dragDropMode=QListView.DragOnly) view.setModel(self.preprocessors) view.activated.connect(self.__activated) box.layout().addWidget(view) #### self._qname2ppdef = { ppdef.qualname: ppdef for ppdef in self.PREPROCESSORS } # List of 'selected' preprocessors and their parameters. self.preprocessormodel = None self.flow_view = SequenceFlow() self.controler = self.CONTROLLER(self.flow_view, parent=self) self.overlay = OverlayWidget(self) self.overlay.setAttribute(Qt.WA_TransparentForMouseEvents) self.overlay.setWidget(self.flow_view) self.overlay.setLayout(QVBoxLayout()) self.overlay.layout().addWidget( QLabel("Drag items from the list on the left", wordWrap=True)) self.scroll_area = QScrollArea( verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn) self.scroll_area.viewport().setAcceptDrops(True) self.scroll_area.setWidget(self.flow_view) self.scroll_area.setWidgetResizable(True) self.mainArea.layout().addWidget(self.scroll_area) self.flow_view.installEventFilter(self) gui.auto_apply(self.buttonsArea, self, "autocommit") self._initialize()
class OWPreprocess(widget.OWWidget, openclass=True): name = "Preprocess" description = "Construct a data preprocessing pipeline." icon = "icons/Preprocess.svg" priority = 2105 keywords = ["process"] settings_version = 2 class Inputs: data = Input("Data", Orange.data.Table) class Outputs: preprocessor = Output("Preprocessor", preprocess.preprocess.Preprocess, dynamic=False) preprocessed_data = Output("Preprocessed Data", Orange.data.Table) storedsettings = Setting({}) autocommit = Setting(True) PREPROCESSORS = PREPROCESS_ACTIONS CONTROLLER = Controller def __init__(self): super().__init__() self.data = None self._invalidated = False # List of available preprocessors (DescriptionRole : Description) self.preprocessors = QStandardItemModel() def mimeData(indexlist): assert len(indexlist) == 1 index = indexlist[0] qname = index.data(DescriptionRole).qualname m = QMimeData() m.setData("application/x-qwidget-ref", qname.encode("utf-8")) return m # TODO: Fix this (subclass even if just to pass a function # for mimeData delegate) self.preprocessors.mimeData = mimeData box = gui.vBox(self.controlArea, "Preprocessors") gui.rubber(self.controlArea) # we define a class that lets us set the vertical sizeHint # based on the height and number of items in the list # see self.__update_list_sizeHint class ListView(QListView): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.vertical_hint = None def sizeHint(self): sh = super().sizeHint() if self.vertical_hint: return QSize(sh.width(), self.vertical_hint) return sh self.preprocessorsView = view = ListView( selectionMode=QListView.SingleSelection, dragEnabled=True, dragDropMode=QListView.DragOnly) view.setModel(self.preprocessors) view.activated.connect(self.__activated) box.layout().addWidget(view) #### self._qname2ppdef = { ppdef.qualname: ppdef for ppdef in self.PREPROCESSORS } # List of 'selected' preprocessors and their parameters. self.preprocessormodel = None self.flow_view = SequenceFlow() self.controler = self.CONTROLLER(self.flow_view, parent=self) self.overlay = OverlayWidget(self) self.overlay.setAttribute(Qt.WA_TransparentForMouseEvents) self.overlay.setWidget(self.flow_view) self.overlay.setLayout(QVBoxLayout()) self.overlay.layout().addWidget( QLabel("Drag items from the list on the left", wordWrap=True)) self.scroll_area = QScrollArea( verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn) self.scroll_area.viewport().setAcceptDrops(True) self.scroll_area.setWidget(self.flow_view) self.scroll_area.setWidgetResizable(True) self.mainArea.layout().addWidget(self.scroll_area) self.flow_view.installEventFilter(self) gui.auto_apply(self.buttonsArea, self, "autocommit") self._initialize() def _initialize(self): for pp_def in self.PREPROCESSORS: description = pp_def.description if description.icon: icon = QIcon(description.icon) else: icon = QIcon() item = QStandardItem(icon, description.title) item.setToolTip(description.summary or "") item.setData(pp_def, DescriptionRole) item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled) self.preprocessors.appendRow([item]) self.__update_list_sizeHint() model = self.load(self.storedsettings) self.set_model(model) if not model.rowCount(): # enforce default width constraint if no preprocessors # are instantiated (if the model is not empty the constraints # will be triggered by LayoutRequest event on the `flow_view`) self.__update_size_constraint() self.apply() def __update_list_sizeHint(self): view = self.preprocessorsView h = view.sizeHintForRow(0) n = self.preprocessors.rowCount() view.vertical_hint = n * h + 2 # only on Mac? view.updateGeometry() def load(self, saved): """Load a preprocessor list from a dict.""" preprocessors = saved.get("preprocessors", []) model = StandardItemModel() def dropMimeData(data, action, row, _column, _parent): if data.hasFormat("application/x-qwidget-ref") and \ action == Qt.CopyAction: qname = bytes(data.data("application/x-qwidget-ref")).decode() ppdef = self._qname2ppdef[qname] item = QStandardItem(ppdef.description.title) item.setData({}, ParametersRole) item.setData(ppdef.description.title, Qt.DisplayRole) item.setData(ppdef, DescriptionRole) self.preprocessormodel.insertRow(row, [item]) return True else: return False model.dropMimeData = dropMimeData for qualname, params in preprocessors: pp_def = self._qname2ppdef[qualname] description = pp_def.description item = QStandardItem(description.title) if description.icon: icon = QIcon(description.icon) else: icon = QIcon() item.setIcon(icon) item.setToolTip(description.summary) item.setData(pp_def, DescriptionRole) item.setData(params, ParametersRole) model.appendRow(item) return model @staticmethod def save(model): """Save the preprocessor list to a dict.""" d = {"name": ""} preprocessors = [] for i in range(model.rowCount()): item = model.item(i) pp_def = item.data(DescriptionRole) params = item.data(ParametersRole) preprocessors.append((pp_def.qualname, params)) d["preprocessors"] = preprocessors return d def set_model(self, ppmodel): if self.preprocessormodel: self.preprocessormodel.dataChanged.disconnect( self.__on_modelchanged) self.preprocessormodel.rowsInserted.disconnect( self.__on_modelchanged) self.preprocessormodel.rowsRemoved.disconnect( self.__on_modelchanged) self.preprocessormodel.rowsMoved.disconnect(self.__on_modelchanged) self.preprocessormodel.deleteLater() self.preprocessormodel = ppmodel self.controler.setModel(ppmodel) if ppmodel is not None: self.preprocessormodel.dataChanged.connect(self.__on_modelchanged) self.preprocessormodel.rowsInserted.connect(self.__on_modelchanged) self.preprocessormodel.rowsRemoved.connect(self.__on_modelchanged) self.preprocessormodel.rowsMoved.connect(self.__on_modelchanged) self.__update_overlay() def __update_overlay(self): if self.preprocessormodel is None or \ self.preprocessormodel.rowCount() == 0: self.overlay.setWidget(self.flow_view) self.overlay.show() else: self.overlay.setWidget(None) self.overlay.hide() def __on_modelchanged(self): self.__update_overlay() self.commit() @Inputs.data @check_sql_input def set_data(self, data=None): """Set the input dataset.""" self.data = data def handleNewSignals(self): self.apply() def __activated(self, index): item = self.preprocessors.itemFromIndex(index) action = item.data(DescriptionRole) item = QStandardItem() item.setData({}, ParametersRole) item.setData(action.description.title, Qt.DisplayRole) item.setData(action, DescriptionRole) self.preprocessormodel.appendRow([item]) def buildpreproc(self): plist = [] for i in range(self.preprocessormodel.rowCount()): item = self.preprocessormodel.item(i) desc = item.data(DescriptionRole) params = item.data(ParametersRole) if not isinstance(params, dict): params = {} create = desc.viewclass.createinstance plist.append(create(params)) if len(plist) == 1: return plist[0] else: return preprocess.preprocess.PreprocessorList(plist) def apply(self): # Sync the model into storedsettings on every apply. self.storeSpecificSettings() preprocessor = self.buildpreproc() if self.data is not None: self.error() try: data = preprocessor(self.data) except (ValueError, ZeroDivisionError) as e: self.error(str(e)) return else: data = None self.Outputs.preprocessor.send(preprocessor) self.Outputs.preprocessed_data.send(data) def commit(self): if not self._invalidated: self._invalidated = True QApplication.postEvent(self, QEvent(QEvent.User)) def customEvent(self, event): if event.type() == QEvent.User and self._invalidated: self._invalidated = False self.apply() def eventFilter(self, receiver, event): if receiver is self.flow_view and event.type() == QEvent.LayoutRequest: QTimer.singleShot(0, self.__update_size_constraint) return super().eventFilter(receiver, event) def storeSpecificSettings(self): """Reimplemented.""" self.storedsettings = self.save(self.preprocessormodel) super().storeSpecificSettings() def saveSettings(self): """Reimplemented.""" self.storedsettings = self.save(self.preprocessormodel) super().saveSettings() @classmethod def migrate_settings(cls, settings, version): if version < 2: for action, params in settings["storedsettings"]["preprocessors"]: if action == "orange.preprocess.scale": scale = center = None if "center" in params: center = params.pop("center").name if "scale" in params: scale = params.pop("scale").name migratable = { ("Mean", "NoScaling"): Scale.CenterByMean, ("NoCentering", "Std"): Scale.ScaleBySD, ("Mean", "Std"): Scale.NormalizeBySD, ("NoCentering", "Span"): Scale.NormalizeBySpan_ZeroBased } params["method"] = \ migratable.get((center, scale), Scale.NormalizeBySD) def onDeleteWidget(self): self.data = None self.set_model(None) super().onDeleteWidget() @Slot() def __update_size_constraint(self): # Update minimum width constraint on the scroll area containing # the 'instantiated' preprocessor list (to avoid the horizontal # scroll bar). sh = self.flow_view.minimumSizeHint() scroll_width = self.scroll_area.verticalScrollBar().width() self.scroll_area.setMinimumWidth( min(max(sh.width() + scroll_width + 2, self.controlArea.width()), 520)) def send_report(self): pp = [(self.controler.model().index(i, 0).data(Qt.DisplayRole), w) for i, w in enumerate(self.controler.view.widgets())] if pp: self.report_items("Settings", pp)
def set_basic_layout(self): """Provide the basic widget layout Which parts are created is regulated by class attributes `want_main_area`, `want_control_area`, `want_message_bar` and `buttons_area_orientation`, the presence of method `send_report` and attribute `graph_name`. """ self.setLayout(QVBoxLayout()) self.layout().setContentsMargins(2, 2, 2, 2) if not self.resizing_enabled: self.layout().setSizeConstraint(QVBoxLayout.SetFixedSize) self.want_main_area = self.want_main_area or self.graph_name self._create_default_buttons() self._insert_splitter() if self.want_control_area: self._insert_control_area() if self.want_main_area: self._insert_main_area() if self.want_message_bar: # Use a OverlayWidget for status bar positioning. c = OverlayWidget(self, alignment=Qt.AlignBottom) c.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) c.setWidget(self) c.setLayout(QVBoxLayout()) c.layout().setContentsMargins(0, 0, 0, 0) sb = QStatusBar() sb.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Maximum) sb.setSizeGripEnabled(self.resizing_enabled) c.layout().addWidget(sb) help = self.__help_action icon = QIcon(gui.resource_filename("icons/help.svg")) icon.addFile(gui.resource_filename("icons/help-hover.svg"), mode=QIcon.Active) help_button = SimpleButton( icon=icon, toolTip="Show widget help", visible=help.isVisible(), ) @help.changed.connect def _(): help_button.setVisible(help.isVisible()) help_button.setEnabled(help.isEnabled()) help_button.clicked.connect(help.trigger) sb.addWidget(help_button) if self.graph_name is not None: icon = QIcon(gui.resource_filename("icons/chart.svg")) icon.addFile(gui.resource_filename("icons/chart-hover.svg"), mode=QIcon.Active) b = SimpleButton( icon=icon, toolTip="Save Image", ) b.clicked.connect(self.save_graph) sb.addWidget(b) if hasattr(self, "send_report"): icon = QIcon(gui.resource_filename("icons/report.svg")) icon.addFile(gui.resource_filename("icons/report-hover.svg"), mode=QIcon.Active) b = SimpleButton(icon=icon, toolTip="Report") b.clicked.connect(self.show_report) sb.addWidget(b) self.message_bar = MessagesWidget(self) self.message_bar.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) pb = QProgressBar(maximumWidth=120, minimum=0, maximum=100) pb.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Ignored) pb.setAttribute(Qt.WA_LayoutUsesWidgetRect) pb.setAttribute(Qt.WA_MacMiniSize) pb.hide() sb.addPermanentWidget(pb) sb.addPermanentWidget(self.message_bar) def statechanged(): pb.setVisible(bool(self.processingState) or self.isBlocking()) if self.isBlocking() and not self.processingState: pb.setRange(0, 0) # indeterminate pb elif self.processingState: pb.setRange(0, 100) # determinate pb self.processingStateChanged.connect(statechanged) self.blockingStateChanged.connect(statechanged) @self.progressBarValueChanged.connect def _(val): pb.setValue(int(val)) # Reserve the bottom margins for the status bar margins = self.layout().contentsMargins() margins.setBottom(sb.sizeHint().height()) self.setContentsMargins(margins)
class OWPreprocess(widget.OWWidget): name = "Preprocess" description = "Construct a data preprocessing pipeline." icon = "icons/Preprocess.svg" priority = 2105 inputs = [("Data", Orange.data.Table, "set_data")] outputs = [("Preprocessor", preprocess.preprocess.Preprocess), ("Preprocessed Data", Orange.data.Table)] storedsettings = settings.Setting({}) autocommit = settings.Setting(True) def __init__(self): super().__init__() self.data = None self._invalidated = False # List of available preprocessors (DescriptionRole : Description) self.preprocessors = QStandardItemModel() def mimeData(indexlist): assert len(indexlist) == 1 index = indexlist[0] qname = index.data(DescriptionRole).qualname m = QMimeData() m.setData("application/x-qwidget-ref", qname.encode("utf-8")) return m # TODO: Fix this (subclass even if just to pass a function # for mimeData delegate) self.preprocessors.mimeData = mimeData box = gui.vBox(self.controlArea, "Preprocessors") self.preprocessorsView = view = QListView( selectionMode=QListView.SingleSelection, dragEnabled=True, dragDropMode=QListView.DragOnly) view.setModel(self.preprocessors) view.activated.connect(self.__activated) box.layout().addWidget(view) #### self._qname2ppdef = {ppdef.qualname: ppdef for ppdef in PREPROCESSORS} # List of 'selected' preprocessors and their parameters. self.preprocessormodel = None self.flow_view = SequenceFlow() self.controler = Controller(self.flow_view, parent=self) self.overlay = OverlayWidget(self) self.overlay.setAttribute(Qt.WA_TransparentForMouseEvents) self.overlay.setWidget(self.flow_view) self.overlay.setLayout(QVBoxLayout()) self.overlay.layout().addWidget( QLabel("Drag items from the list on the left", wordWrap=True)) self.scroll_area = QScrollArea( verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn) self.scroll_area.viewport().setAcceptDrops(True) self.scroll_area.setWidget(self.flow_view) self.scroll_area.setWidgetResizable(True) self.mainArea.layout().addWidget(self.scroll_area) self.flow_view.installEventFilter(self) box = gui.vBox(self.controlArea, "Output") gui.auto_commit(box, self, "autocommit", "Send", box=False) self._initialize() def _initialize(self): for pp_def in PREPROCESSORS: description = pp_def.description if description.icon: icon = QIcon(description.icon) else: icon = QIcon() item = QStandardItem(icon, description.title) item.setToolTip(description.summary or "") item.setData(pp_def, DescriptionRole) item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled) self.preprocessors.appendRow([item]) try: model = self.load(self.storedsettings) except Exception: model = self.load({}) self.set_model(model) if not model.rowCount(): # enforce default width constraint if no preprocessors # are instantiated (if the model is not empty the constraints # will be triggered by LayoutRequest event on the `flow_view`) self.__update_size_constraint() self.apply() def load(self, saved): """Load a preprocessor list from a dict.""" name = saved.get("name", "") preprocessors = saved.get("preprocessors", []) model = StandardItemModel() def dropMimeData(data, action, row, column, parent): if data.hasFormat("application/x-qwidget-ref") and \ action == Qt.CopyAction: qname = bytes(data.data("application/x-qwidget-ref")).decode() ppdef = self._qname2ppdef[qname] item = QStandardItem(ppdef.description.title) item.setData({}, ParametersRole) item.setData(ppdef.description.title, Qt.DisplayRole) item.setData(ppdef, DescriptionRole) self.preprocessormodel.insertRow(row, [item]) return True else: return False model.dropMimeData = dropMimeData for qualname, params in preprocessors: pp_def = self._qname2ppdef[qualname] description = pp_def.description item = QStandardItem(description.title) if description.icon: icon = QIcon(description.icon) else: icon = QIcon() item.setIcon(icon) item.setToolTip(description.summary) item.setData(pp_def, DescriptionRole) item.setData(params, ParametersRole) model.appendRow(item) return model def save(self, model): """Save the preprocessor list to a dict.""" d = {"name": ""} preprocessors = [] for i in range(model.rowCount()): item = model.item(i) pp_def = item.data(DescriptionRole) params = item.data(ParametersRole) preprocessors.append((pp_def.qualname, params)) d["preprocessors"] = preprocessors return d def set_model(self, ppmodel): if self.preprocessormodel: self.preprocessormodel.dataChanged.disconnect( self.__on_modelchanged) self.preprocessormodel.rowsInserted.disconnect( self.__on_modelchanged) self.preprocessormodel.rowsRemoved.disconnect( self.__on_modelchanged) self.preprocessormodel.rowsMoved.disconnect(self.__on_modelchanged) self.preprocessormodel.deleteLater() self.preprocessormodel = ppmodel self.controler.setModel(ppmodel) if ppmodel is not None: self.preprocessormodel.dataChanged.connect(self.__on_modelchanged) self.preprocessormodel.rowsInserted.connect(self.__on_modelchanged) self.preprocessormodel.rowsRemoved.connect(self.__on_modelchanged) self.preprocessormodel.rowsMoved.connect(self.__on_modelchanged) self.__update_overlay() def __update_overlay(self): if self.preprocessormodel is None or \ self.preprocessormodel.rowCount() == 0: self.overlay.setWidget(self.flow_view) self.overlay.show() else: self.overlay.setWidget(None) self.overlay.hide() def __on_modelchanged(self): self.__update_overlay() self.commit() @check_sql_input def set_data(self, data=None): """Set the input data set.""" self.data = data def handleNewSignals(self): self.apply() def __activated(self, index): item = self.preprocessors.itemFromIndex(index) action = item.data(DescriptionRole) item = QStandardItem() item.setData({}, ParametersRole) item.setData(action.description.title, Qt.DisplayRole) item.setData(action, DescriptionRole) self.preprocessormodel.appendRow([item]) def buildpreproc(self): plist = [] for i in range(self.preprocessormodel.rowCount()): item = self.preprocessormodel.item(i) desc = item.data(DescriptionRole) params = item.data(ParametersRole) if not isinstance(params, dict): params = {} create = desc.viewclass.createinstance plist.append(create(params)) if len(plist) == 1: return plist[0] else: return preprocess.preprocess.PreprocessorList(plist) def apply(self): # Sync the model into storedsettings on every apply. self.storeSpecificSettings() preprocessor = self.buildpreproc() if self.data is not None: self.error() try: data = preprocessor(self.data) except (ValueError, ZeroDivisionError) as e: self.error(str(e)) return else: data = None self.send("Preprocessor", preprocessor) self.send("Preprocessed Data", data) def commit(self): if not self._invalidated: self._invalidated = True QApplication.postEvent(self, QEvent(QEvent.User)) def customEvent(self, event): if event.type() == QEvent.User and self._invalidated: self._invalidated = False self.apply() def eventFilter(self, receiver, event): if receiver is self.flow_view and event.type() == QEvent.LayoutRequest: QTimer.singleShot(0, self.__update_size_constraint) return super().eventFilter(receiver, event) def storeSpecificSettings(self): """Reimplemented.""" self.storedsettings = self.save(self.preprocessormodel) super().storeSpecificSettings() def saveSettings(self): """Reimplemented.""" self.storedsettings = self.save(self.preprocessormodel) super().saveSettings() def onDeleteWidget(self): self.data = None self.set_model(None) super().onDeleteWidget() @Slot() def __update_size_constraint(self): # Update minimum width constraint on the scroll area containing # the 'instantiated' preprocessor list (to avoid the horizontal # scroll bar). sh = self.flow_view.minimumSizeHint() scroll_width = self.scroll_area.verticalScrollBar().width() self.scroll_area.setMinimumWidth( min(max(sh.width() + scroll_width + 2, self.controlArea.width()), 520)) def sizeHint(self): sh = super().sizeHint() return sh.expandedTo(QSize(sh.width() + 300, 500)) def send_report(self): pp = [(self.controler.model().index(i, 0).data(Qt.DisplayRole), w) for i, w in enumerate(self.controler.view.widgets())] if len(pp): self.report_items("Settings", pp)
class CSVImportDialog(QDialog): """ A dialog for selecting CSV file import options. """ def __init__(self, parent=None, flags=Qt.Dialog, **kwargs): super().__init__(parent, flags, **kwargs) self.setLayout(QVBoxLayout()) self._options = None self._path = None # Finalizer for opened file handle (in _update_preview) self.__finalizer = None # type: Optional[Callable[[], None]] self._optionswidget = textimport.CSVImportWidget() self._optionswidget.previewReadErrorOccurred.connect( self.__on_preview_error ) self._optionswidget.previewModelReset.connect( self.__on_preview_reset ) self._buttons = buttons = QDialogButtonBox( orientation=Qt.Horizontal, standardButtons=(QDialogButtonBox.Ok | QDialogButtonBox.Cancel | QDialogButtonBox.Reset | QDialogButtonBox.RestoreDefaults), objectName="dialog-button-box", ) # TODO: Help button buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) b = buttons.button(QDialogButtonBox.Reset) b.clicked.connect(self.reset) b = buttons.button(QDialogButtonBox.RestoreDefaults) b.clicked.connect(self.restoreDefaults) self.layout().addWidget(self._optionswidget) self.layout().addWidget(buttons) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self._overlay = OverlayWidget(self) self._overlay.setWidget(self._optionswidget.dataview) self._overlay.setLayout(QVBoxLayout()) self._overlay.layout().addWidget(QLabel(wordWrap=True)) self._overlay.hide() def setOptions(self, options): # type: (Options) -> None self._options = options self._optionswidget.setEncoding(options.encoding) self._optionswidget.setDialect(options.dialect) self._optionswidget.setNumbersFormat( options.group_separator, options.decimal_separator) self._optionswidget.setColumnTypeRanges(options.columntypes) self._optionswidget.setRowStates( {i: v for r, v in options.rowspec for i in r} ) def options(self): # type: () -> Options rowspec_ = self._optionswidget.rowStates() rowspec = [(range(i, i + 1), v) for i, v in rowspec_.items()] numformat = self._optionswidget.numbersFormat() return Options( encoding=self._optionswidget.encoding(), dialect=self._optionswidget.dialect(), columntypes=self._optionswidget.columnTypeRanges(), rowspec=rowspec, decimal_separator=numformat["decimal"], group_separator=numformat["group"], ) def setPath(self, path): """ Set the preview path. """ if self._path != path: self._path = path self.__update_preview() def path(self): """Return the preview path""" return self._path def reset(self): """ Reset the options to the state previously set with `setOptions` effectively undoing any user modifications since then. """ self.setOptions(self._options) def restoreDefaults(self): """ Restore the options to default state. """ # preserve `_options` if set by clients (for `reset`). opts = self._options self.setOptions(Options("utf-8", csv.excel())) self._options = opts def __update_preview(self): if not self._path: return try: f = _open(self._path, "rb") except OSError as err: traceback.print_exc(file=sys.stderr) fmt = "".join(traceback.format_exception_only(type(err), err)) self.__set_error(fmt) else: self.__clear_error() self._optionswidget.setSampleContents(f) closeexisting = self.__finalizer if closeexisting is not None: self.destroyed.disconnect(closeexisting) closeexisting() self.__finalizer = weakref.finalize(self, f.close) self.destroyed.connect(self.__finalizer) def __set_error(self, text, format=Qt.PlainText): self._optionswidget.setEnabled(False) label = self._overlay.findChild(QLabel) # type: QLabel label.setText(text) label.setTextFormat(format) self._overlay.show() self._overlay.raise_() dialog_button_box_set_enabled(self._buttons, False) def __clear_error(self): if self._overlay.isVisibleTo(self): self._overlay.hide() self._optionswidget.setEnabled(True) # Enable/disable the accept buttons on the most egregious errors. def __on_preview_error(self): b = self._buttons.button(QDialogButtonBox.Ok) b.setEnabled(False) def __on_preview_reset(self): b = self._buttons.button(QDialogButtonBox.Ok) b.setEnabled(True)
def __init__(self): super().__init__() self.data = None self._invalidated = False # List of available preprocessors (DescriptionRole : Description) self.preprocessors = QStandardItemModel() def mimeData(indexlist): assert len(indexlist) == 1 index = indexlist[0] qname = index.data(DescriptionRole).qualname m = QMimeData() m.setData("application/x-qwidget-ref", qname.encode("utf-8")) return m # TODO: Fix this (subclass even if just to pass a function # for mimeData delegate) self.preprocessors.mimeData = mimeData box = gui.vBox(self.controlArea, "Preprocessors") self.preprocessorsView = view = QListView( selectionMode=QListView.SingleSelection, dragEnabled=True, dragDropMode=QListView.DragOnly ) view.setModel(self.preprocessors) view.activated.connect(self.__activated) box.layout().addWidget(view) #### self._qname2ppdef = {ppdef.qualname: ppdef for ppdef in PREPROCESSORS} # List of 'selected' preprocessors and their parameters. self.preprocessormodel = None self.flow_view = SequenceFlow() self.controler = Controller(self.flow_view, parent=self) self.overlay = OverlayWidget(self) self.overlay.setAttribute(Qt.WA_TransparentForMouseEvents) self.overlay.setWidget(self.flow_view) self.overlay.setLayout(QVBoxLayout()) self.overlay.layout().addWidget( QLabel("Drag items from the list on the left", wordWrap=True)) self.scroll_area = QScrollArea( verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn ) self.scroll_area.viewport().setAcceptDrops(True) self.scroll_area.setWidget(self.flow_view) self.scroll_area.setWidgetResizable(True) self.mainArea.layout().addWidget(self.scroll_area) self.flow_view.installEventFilter(self) box = gui.vBox(self.controlArea, "Output") gui.auto_commit(box, self, "autocommit", "Send", box=False) self._initialize()
class OWPreprocess(widget.OWWidget): name = "Preprocess" description = "Construct a data preprocessing pipeline." icon = "icons/Preprocess.svg" priority = 2105 class Inputs: data = Input("Data", Orange.data.Table) class Outputs: preprocessor = Output("Preprocessor", preprocess.preprocess.Preprocess, dynamic=False) preprocessed_data = Output("Preprocessed Data", Orange.data.Table) storedsettings = settings.Setting({}) autocommit = settings.Setting(True) def __init__(self): super().__init__() self.data = None self._invalidated = False # List of available preprocessors (DescriptionRole : Description) self.preprocessors = QStandardItemModel() def mimeData(indexlist): assert len(indexlist) == 1 index = indexlist[0] qname = index.data(DescriptionRole).qualname m = QMimeData() m.setData("application/x-qwidget-ref", qname.encode("utf-8")) return m # TODO: Fix this (subclass even if just to pass a function # for mimeData delegate) self.preprocessors.mimeData = mimeData box = gui.vBox(self.controlArea, "Preprocessors") self.preprocessorsView = view = QListView( selectionMode=QListView.SingleSelection, dragEnabled=True, dragDropMode=QListView.DragOnly ) view.setModel(self.preprocessors) view.activated.connect(self.__activated) box.layout().addWidget(view) #### self._qname2ppdef = {ppdef.qualname: ppdef for ppdef in PREPROCESSORS} # List of 'selected' preprocessors and their parameters. self.preprocessormodel = None self.flow_view = SequenceFlow() self.controler = Controller(self.flow_view, parent=self) self.overlay = OverlayWidget(self) self.overlay.setAttribute(Qt.WA_TransparentForMouseEvents) self.overlay.setWidget(self.flow_view) self.overlay.setLayout(QVBoxLayout()) self.overlay.layout().addWidget( QLabel("Drag items from the list on the left", wordWrap=True)) self.scroll_area = QScrollArea( verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn ) self.scroll_area.viewport().setAcceptDrops(True) self.scroll_area.setWidget(self.flow_view) self.scroll_area.setWidgetResizable(True) self.mainArea.layout().addWidget(self.scroll_area) self.flow_view.installEventFilter(self) box = gui.vBox(self.controlArea, "Output") gui.auto_commit(box, self, "autocommit", "Send", box=False) self._initialize() def _initialize(self): for pp_def in PREPROCESSORS: description = pp_def.description if description.icon: icon = QIcon(description.icon) else: icon = QIcon() item = QStandardItem(icon, description.title) item.setToolTip(description.summary or "") item.setData(pp_def, DescriptionRole) item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled) self.preprocessors.appendRow([item]) try: model = self.load(self.storedsettings) except Exception: model = self.load({}) self.set_model(model) if not model.rowCount(): # enforce default width constraint if no preprocessors # are instantiated (if the model is not empty the constraints # will be triggered by LayoutRequest event on the `flow_view`) self.__update_size_constraint() self.apply() def load(self, saved): """Load a preprocessor list from a dict.""" name = saved.get("name", "") preprocessors = saved.get("preprocessors", []) model = StandardItemModel() def dropMimeData(data, action, row, column, parent): if data.hasFormat("application/x-qwidget-ref") and \ action == Qt.CopyAction: qname = bytes(data.data("application/x-qwidget-ref")).decode() ppdef = self._qname2ppdef[qname] item = QStandardItem(ppdef.description.title) item.setData({}, ParametersRole) item.setData(ppdef.description.title, Qt.DisplayRole) item.setData(ppdef, DescriptionRole) self.preprocessormodel.insertRow(row, [item]) return True else: return False model.dropMimeData = dropMimeData for qualname, params in preprocessors: pp_def = self._qname2ppdef[qualname] description = pp_def.description item = QStandardItem(description.title) if description.icon: icon = QIcon(description.icon) else: icon = QIcon() item.setIcon(icon) item.setToolTip(description.summary) item.setData(pp_def, DescriptionRole) item.setData(params, ParametersRole) model.appendRow(item) return model def save(self, model): """Save the preprocessor list to a dict.""" d = {"name": ""} preprocessors = [] for i in range(model.rowCount()): item = model.item(i) pp_def = item.data(DescriptionRole) params = item.data(ParametersRole) preprocessors.append((pp_def.qualname, params)) d["preprocessors"] = preprocessors return d def set_model(self, ppmodel): if self.preprocessormodel: self.preprocessormodel.dataChanged.disconnect(self.__on_modelchanged) self.preprocessormodel.rowsInserted.disconnect(self.__on_modelchanged) self.preprocessormodel.rowsRemoved.disconnect(self.__on_modelchanged) self.preprocessormodel.rowsMoved.disconnect(self.__on_modelchanged) self.preprocessormodel.deleteLater() self.preprocessormodel = ppmodel self.controler.setModel(ppmodel) if ppmodel is not None: self.preprocessormodel.dataChanged.connect(self.__on_modelchanged) self.preprocessormodel.rowsInserted.connect(self.__on_modelchanged) self.preprocessormodel.rowsRemoved.connect(self.__on_modelchanged) self.preprocessormodel.rowsMoved.connect(self.__on_modelchanged) self.__update_overlay() def __update_overlay(self): if self.preprocessormodel is None or \ self.preprocessormodel.rowCount() == 0: self.overlay.setWidget(self.flow_view) self.overlay.show() else: self.overlay.setWidget(None) self.overlay.hide() def __on_modelchanged(self): self.__update_overlay() self.commit() @Inputs.data @check_sql_input def set_data(self, data=None): """Set the input data set.""" self.data = data def handleNewSignals(self): self.apply() def __activated(self, index): item = self.preprocessors.itemFromIndex(index) action = item.data(DescriptionRole) item = QStandardItem() item.setData({}, ParametersRole) item.setData(action.description.title, Qt.DisplayRole) item.setData(action, DescriptionRole) self.preprocessormodel.appendRow([item]) def buildpreproc(self): plist = [] for i in range(self.preprocessormodel.rowCount()): item = self.preprocessormodel.item(i) desc = item.data(DescriptionRole) params = item.data(ParametersRole) if not isinstance(params, dict): params = {} create = desc.viewclass.createinstance plist.append(create(params)) if len(plist) == 1: return plist[0] else: return preprocess.preprocess.PreprocessorList(plist) def apply(self): # Sync the model into storedsettings on every apply. self.storeSpecificSettings() preprocessor = self.buildpreproc() if self.data is not None: self.error() try: data = preprocessor(self.data) except (ValueError, ZeroDivisionError) as e: self.error(str(e)) return else: data = None self.Outputs.preprocessor.send(preprocessor) self.Outputs.preprocessed_data.send(data) def commit(self): if not self._invalidated: self._invalidated = True QApplication.postEvent(self, QEvent(QEvent.User)) def customEvent(self, event): if event.type() == QEvent.User and self._invalidated: self._invalidated = False self.apply() def eventFilter(self, receiver, event): if receiver is self.flow_view and event.type() == QEvent.LayoutRequest: QTimer.singleShot(0, self.__update_size_constraint) return super().eventFilter(receiver, event) def storeSpecificSettings(self): """Reimplemented.""" self.storedsettings = self.save(self.preprocessormodel) super().storeSpecificSettings() def saveSettings(self): """Reimplemented.""" self.storedsettings = self.save(self.preprocessormodel) super().saveSettings() def onDeleteWidget(self): self.data = None self.set_model(None) super().onDeleteWidget() @Slot() def __update_size_constraint(self): # Update minimum width constraint on the scroll area containing # the 'instantiated' preprocessor list (to avoid the horizontal # scroll bar). sh = self.flow_view.minimumSizeHint() scroll_width = self.scroll_area.verticalScrollBar().width() self.scroll_area.setMinimumWidth( min(max(sh.width() + scroll_width + 2, self.controlArea.width()), 520)) def sizeHint(self): sh = super().sizeHint() return sh.expandedTo(QSize(sh.width() + 300, 500)) def send_report(self): pp = [(self.controler.model().index(i, 0).data(Qt.DisplayRole), w) for i, w in enumerate(self.controler.view.widgets())] if len(pp): self.report_items("Settings", pp)
def test_layout(self): container = QWidget() container.setLayout(QHBoxLayout()) container1 = QWidget() container.layout().addWidget(container1) container.show() QTest.qWaitForWindowExposed(container) container.resize(600, 600) overlay = OverlayWidget(parent=container) overlay.setWidget(container) overlay.resize(20, 20) overlay.show() center = overlay.geometry().center() self.assertTrue(290 < center.x() < 310) self.assertTrue(290 < center.y() < 310) overlay.setAlignment(Qt.AlignTop | Qt.AlignHCenter) geom = overlay.geometry() self.assertEqual(geom.top(), 0) self.assertTrue(290 < geom.center().x() < 310) overlay.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) geom = overlay.geometry() self.assertEqual(geom.left(), 0) self.assertTrue(290 < geom.center().y() < 310) overlay.setAlignment(Qt.AlignBottom | Qt.AlignRight) geom = overlay.geometry() self.assertEqual(geom.right(), 600 - 1) self.assertEqual(geom.bottom(), 600 - 1) overlay.setWidget(container1) geom = overlay.geometry() self.assertEqual(geom.right(), container1.geometry().right()) self.assertEqual(geom.bottom(), container1.geometry().bottom())
class CSVImportDialog(QDialog): """ A dialog for selecting CSV file import options. """ def __init__(self, parent=None, flags=Qt.Dialog, **kwargs): super().__init__(parent, flags, **kwargs) self.setLayout(QVBoxLayout()) self._options = None self._path = None # Finalizer for opened file handle (in _update_preview) self.__finalizer = None # type: Optional[Callable[[], None]] self._optionswidget = textimport.CSVImportWidget() self._optionswidget.previewReadErrorOccurred.connect( self.__on_preview_error ) self._optionswidget.previewModelReset.connect( self.__on_preview_reset ) self._buttons = buttons = QDialogButtonBox( orientation=Qt.Horizontal, standardButtons=(QDialogButtonBox.Ok | QDialogButtonBox.Cancel | QDialogButtonBox.Reset | QDialogButtonBox.RestoreDefaults), objectName="dialog-button-box", ) # TODO: Help button buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) b = buttons.button(QDialogButtonBox.Reset) b.clicked.connect(self.reset) b = buttons.button(QDialogButtonBox.RestoreDefaults) b.clicked.connect(self.restoreDefaults) self.layout().addWidget(self._optionswidget) self.layout().addWidget(buttons) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self._overlay = OverlayWidget(self) self._overlay.setWidget(self._optionswidget.dataview) self._overlay.setLayout(QVBoxLayout()) self._overlay.layout().addWidget(QLabel(wordWrap=True)) self._overlay.hide() def setOptions(self, options): # type: (Options) -> None self._options = options self._optionswidget.setEncoding(options.encoding) self._optionswidget.setDialect(options.dialect) self._optionswidget.setNumbersFormat( options.group_separator, options.decimal_separator) self._optionswidget.setColumnTypeRanges(options.columntypes) self._optionswidget.setRowStates( {i: v for r, v in options.rowspec for i in r} ) def options(self): # type: () -> Options rowspec_ = self._optionswidget.rowStates() rowspec = [(range(i, i + 1), v) for i, v in rowspec_.items()] numformat = self._optionswidget.numbersFormat() return Options( encoding=self._optionswidget.encoding(), dialect=self._optionswidget.dialect(), columntypes=self._optionswidget.columnTypeRanges(), rowspec=rowspec, decimal_separator=numformat["decimal"], group_separator=numformat["group"], ) def setPath(self, path): """ Set the preview path. """ if self._path != path: self._path = path self.__update_preview() def path(self): """Return the preview path""" return self._path def reset(self): """ Reset the options to the state previously set with `setOptions` effectively undoing any user modifications since then. """ self.setOptions(self._options) def restoreDefaults(self): """ Restore the options to default state. """ # preserve `_options` if set by clients (for `reset`). opts = self._options self.setOptions(Options("utf-8", csv.excel())) self._options = opts def __update_preview(self): if not self._path: return try: f = _open(self._path, "rb") except OSError as err: traceback.print_exc(file=sys.stderr) fmt = "".join(traceback.format_exception_only(type(err), err)) self.__set_error(fmt) else: self.__clear_error() self._optionswidget.setSampleContents(f) closeexisting = self.__finalizer if closeexisting is not None: self.destroyed.disconnect(closeexisting) closeexisting() self.__finalizer = weakref.finalize(self, lambda: f.close()) self.destroyed.connect(self.__finalizer) def __set_error(self, text, format=Qt.PlainText): self._optionswidget.setEnabled(False) label = self._overlay.findChild(QLabel) # type: QLabel label.setText(text) label.setTextFormat(format) self._overlay.show() self._overlay.raise_() dialog_button_box_set_enabled(self._buttons, False) def __clear_error(self): if self._overlay.isVisibleTo(self): self._overlay.hide() self._optionswidget.setEnabled(True) # Enable/disable the accept buttons on the most egregious errors. def __on_preview_error(self): b = self._buttons.button(QDialogButtonBox.Ok) b.setEnabled(False) def __on_preview_reset(self): b = self._buttons.button(QDialogButtonBox.Ok) b.setEnabled(True)
def set_basic_layout(self): """Provide the basic widget layout Which parts are created is regulated by class attributes `want_main_area`, `want_control_area`, `want_message_bar` and `buttons_area_orientation`, the presence of method `send_report` and attribute `graph_name`. """ self.setLayout(QVBoxLayout()) self.layout().setContentsMargins(2, 2, 2, 2) if not self.resizing_enabled: self.layout().setSizeConstraint(QVBoxLayout.SetFixedSize) self.want_main_area = self.want_main_area or self.graph_name self._create_default_buttons() self._insert_splitter() if self.want_control_area: self._insert_control_area() if self.want_main_area: self._insert_main_area() if self.want_message_bar: # Use a OverlayWidget for status bar positioning. c = OverlayWidget(self, alignment=Qt.AlignBottom) c.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) c.setWidget(self) c.setLayout(QVBoxLayout()) c.layout().setContentsMargins(0, 0, 0, 0) sb = QStatusBar() sb.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Maximum) sb.setSizeGripEnabled(self.resizing_enabled) c.layout().addWidget(sb) self.message_bar = MessagesWidget(self) self.message_bar.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) pb = QProgressBar(maximumWidth=120, minimum=0, maximum=100) pb.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Ignored) pb.setAttribute(Qt.WA_LayoutUsesWidgetRect) pb.setAttribute(Qt.WA_MacMiniSize) pb.hide() sb.addPermanentWidget(pb) sb.addPermanentWidget(self.message_bar) def statechanged(): pb.setVisible(bool(self.processingState) or self.isBlocking()) if self.isBlocking() and not self.processingState: pb.setRange(0, 0) # indeterminate pb elif self.processingState: pb.setRange(0, 100) # determinate pb self.processingStateChanged.connect(statechanged) self.blockingStateChanged.connect(statechanged) @self.progressBarValueChanged.connect def _(val): pb.setValue(int(val)) # Reserve the bottom margins for the status bar margins = self.layout().contentsMargins() margins.setBottom(sb.sizeHint().height()) self.setContentsMargins(margins)
def set_basic_layout(self): """Provide the basic widget layout Which parts are created is regulated by class attributes `want_main_area`, `want_control_area`, `want_message_bar` and `buttons_area_orientation`, the presence of method `send_report` and attribute `graph_name`. """ self.setLayout(QVBoxLayout()) self.layout().setContentsMargins(2, 2, 2, 2) if not self.resizing_enabled: self.layout().setSizeConstraint(QVBoxLayout.SetFixedSize) self.want_main_area = self.want_main_area or self.graph_name self._create_default_buttons() self._insert_splitter() if self.want_control_area: self._insert_control_area() if self.want_main_area: self._insert_main_area() if self.want_message_bar: # Use a OverlayWidget for status bar positioning. c = OverlayWidget(self, alignment=Qt.AlignBottom) c.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) c.setWidget(self) c.setLayout(QVBoxLayout()) c.layout().setContentsMargins(0, 0, 0, 0) sb = QStatusBar() sb.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Maximum) sb.setSizeGripEnabled(self.resizing_enabled) c.layout().addWidget(sb) help = self.__help_action help_button = SimpleButton( icon=QIcon(gui.resource_filename("icons/help.svg")), toolTip="Show widget help", visible=help.isVisible(), ) @help.changed.connect def _(): help_button.setVisible(help.isVisible()) help_button.setEnabled(help.isEnabled()) help_button.clicked.connect(help.trigger) sb.addWidget(help_button) if self.graph_name is not None: b = SimpleButton( icon=QIcon(gui.resource_filename("icons/chart.svg")), toolTip="Save Image", ) b.clicked.connect(self.save_graph) sb.addWidget(b) if hasattr(self, "send_report"): b = SimpleButton( icon=QIcon(gui.resource_filename("icons/report.svg")), toolTip="Report" ) b.clicked.connect(self.show_report) sb.addWidget(b) self.message_bar = MessagesWidget(self) self.message_bar.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) pb = QProgressBar(maximumWidth=120, minimum=0, maximum=100) pb.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Ignored) pb.setAttribute(Qt.WA_LayoutUsesWidgetRect) pb.setAttribute(Qt.WA_MacMiniSize) pb.hide() sb.addPermanentWidget(pb) sb.addPermanentWidget(self.message_bar) def statechanged(): pb.setVisible(bool(self.processingState) or self.isBlocking()) if self.isBlocking() and not self.processingState: pb.setRange(0, 0) # indeterminate pb elif self.processingState: pb.setRange(0, 100) # determinate pb self.processingStateChanged.connect(statechanged) self.blockingStateChanged.connect(statechanged) @self.progressBarValueChanged.connect def _(val): pb.setValue(int(val)) # Reserve the bottom margins for the status bar margins = self.layout().contentsMargins() margins.setBottom(sb.sizeHint().height()) self.setContentsMargins(margins)