class UserInteraction(QObject): """ Base class for user interaction handlers. Parameters ---------- document : :class:`~.SchemeEditWidget` An scheme editor instance with which the user is interacting. parent : :class:`QObject`, optional A parent QObject deleteOnEnd : bool, optional Should the UserInteraction be deleted when it finishes (``True`` by default). """ # Cancel reason flags #: No specified reason NoReason = 0 #: User canceled the operation (e.g. pressing ESC) UserCancelReason = 1 #: Another interaction was set InteractionOverrideReason = 3 #: An internal error occurred ErrorReason = 4 #: Other (unspecified) reason OtherReason = 5 #: Emitted when the interaction is set on the scene. started = Signal() #: Emitted when the interaction finishes successfully. finished = Signal() #: Emitted when the interaction ends (canceled or finished) ended = Signal() #: Emitted when the interaction is canceled. canceled = Signal([], [int]) def __init__(self, document, parent=None, deleteOnEnd=True): QObject.__init__(self, parent) self.document = document self.scene = document.scene() self.scheme = document.scheme() self.suggestions = document.suggestions() self.deleteOnEnd = deleteOnEnd self.cancelOnEsc = False self.__finished = False self.__canceled = False self.__cancelReason = self.NoReason def start(self): """ Start the interaction. This is called by the :class:`CanvasScene` when the interaction is installed. .. note:: Must be called from subclass implementations. """ self.started.emit() def end(self): """ Finish the interaction. Restore any leftover state in this method. .. note:: This gets called from the default :func:`cancel` implementation. """ self.__finished = True if self.scene.user_interaction_handler is self: self.scene.set_user_interaction_handler(None) if self.__canceled: self.canceled.emit() self.canceled[int].emit(self.__cancelReason) else: self.finished.emit() self.ended.emit() if self.deleteOnEnd: self.deleteLater() def cancel(self, reason=OtherReason): """ Cancel the interaction with `reason`. """ self.__canceled = True self.__cancelReason = reason self.end() def isFinished(self): """ Is the interaction finished. """ return self.__finished def isCanceled(self): """ Was the interaction canceled. """ return self.__canceled def cancelReason(self): """ Return the reason the interaction was canceled. """ return self.__cancelReason def mousePressEvent(self, event): """ Handle a `QGraphicsScene.mousePressEvent`. """ return False def mouseMoveEvent(self, event): """ Handle a `GraphicsScene.mouseMoveEvent`. """ return False def mouseReleaseEvent(self, event): """ Handle a `QGraphicsScene.mouseReleaseEvent`. """ return False def mouseDoubleClickEvent(self, event): """ Handle a `QGraphicsScene.mouseDoubleClickEvent`. """ return False def keyPressEvent(self, event): """ Handle a `QGraphicsScene.keyPressEvent` """ if self.cancelOnEsc and event.key() == Qt.Key_Escape: self.cancel(self.UserCancelReason) return False def keyReleaseEvent(self, event): """ Handle a `QGraphicsScene.keyPressEvent` """ return False def contextMenuEvent(self, event): """ Handle a `QGraphicsScene.contextMenuEvent` """ return False
class VizRankDialog(QDialog, ProgressBarMixin, WidgetMessagesMixin): """ Base class for VizRank dialogs, providing a GUI with a table and a button, and the skeleton for managing the evaluation of visualizations. Derived classes must provide methods - `iterate_states` for generating combinations (e.g. pairs of attritutes), - `compute_score(state)` for computing the score of a combination, - `row_for_state(state)` that returns a list of items inserted into the table for the given state. and, optionally, - `state_count` that returns the number of combinations (used for progress bar) - `on_selection_changed` that handles event triggered when the user selects a table row. The method should emit signal `VizRankDialog.selectionChanged(object)`. - `bar_length` returns the length of the bar corresponding to the score. The class provides a table and a button. A widget constructs a single instance of this dialog in its `__init__`, like (in Sieve) by using a convenience method :obj:`add_vizrank`:: self.vizrank, self.vizrank_button = SieveRank.add_vizrank( box, self, "Score Combinations", self.set_attr) When the widget receives new data, it must call the VizRankDialog's method :obj:`VizRankDialog.initialize()` to clear the GUI and reset the state. Clicking the Start button calls method `run` (and renames the button to Pause). Run sets up a progress bar by getting the number of combinations from :obj:`VizRankDialog.state_count()`. It restores the paused state (if any) and calls generator :obj:`VizRankDialog.iterate_states()`. For each generated state, it calls :obj:`VizRankDialog.score(state)`, which must return the score (lower is better) for this state. If the returned state is not `None`, the data returned by `row_for_state` is inserted at the appropriate place in the table. Args: master (Orange.widget.OWWidget): widget to which the dialog belongs Attributes: master (Orange.widget.OWWidget): widget to which the dialog belongs captionTitle (str): the caption for the dialog. This can be a class attribute. `captionTitle` is used by the `ProgressBarMixin`. """ captionTitle = "" processingStateChanged = Signal(int) progressBarValueChanged = Signal(float) messageActivated = Signal(Msg) messageDeactivated = Signal(Msg) selectionChanged = Signal(object) class Information(WidgetMessagesMixin.Information): nothing_to_rank = Msg("There is nothing to rank.") def __init__(self, master): """Initialize the attributes and set up the interface""" QDialog.__init__(self, master, windowTitle=self.captionTitle) WidgetMessagesMixin.__init__(self) self.setLayout(QVBoxLayout()) self.insert_message_bar() self.layout().insertWidget(0, self.message_bar) self.master = master self.keep_running = False self.scheduled_call = None self.saved_state = None self.saved_progress = 0 self.scores = [] self.add_to_model = queue.Queue() self.update_timer = QTimer(self) self.update_timer.timeout.connect(self._update) self.update_timer.setInterval(200) self._thread = None self._worker = None self.filter = QLineEdit() self.filter.setPlaceholderText("过滤...") self.filter.textChanged.connect(self.filter_changed) self.layout().addWidget(self.filter) # Remove focus from line edit self.setFocus(Qt.ActiveWindowFocusReason) self.rank_model = QStandardItemModel(self) self.model_proxy = QSortFilterProxyModel(self, filterCaseSensitivity=False) self.model_proxy.setSourceModel(self.rank_model) self.rank_table = view = QTableView( selectionBehavior=QTableView.SelectRows, selectionMode=QTableView.SingleSelection, showGrid=False, editTriggers=gui.TableView.NoEditTriggers) if self._has_bars: view.setItemDelegate(TableBarItem()) else: view.setItemDelegate(HorizontalGridDelegate()) view.setModel(self.model_proxy) view.selectionModel().selectionChanged.connect( self.on_selection_changed) view.horizontalHeader().setStretchLastSection(True) view.horizontalHeader().hide() self.layout().addWidget(view) self.button = gui.button(self, self, "Start", callback=self.toggle, default=True) @property def _has_bars(self): return type(self).bar_length is not VizRankDialog.bar_length @classmethod def add_vizrank(cls, widget, master, button_label, set_attr_callback): """ Equip the widget with VizRank button and dialog, and monkey patch the widget's `closeEvent` and `hideEvent` to close/hide the vizrank, too. Args: widget (QWidget): the widget into whose layout to insert the button master (Orange.widgets.widget.OWWidget): the master widget button_label: the label for the button set_attr_callback: the callback for setting the projection chosen in the vizrank Returns: tuple with Vizrank dialog instance and push button """ # Monkey patching could be avoided by mixing-in the class (not # necessarily a good idea since we can make a mess of multiple # defined/derived closeEvent and hideEvent methods). Furthermore, # per-class patching would be better than per-instance, but we don't # want to mess with meta-classes either. vizrank = cls(master) button = gui.button(widget, master, button_label, callback=vizrank.reshow, enabled=False) vizrank.selectionChanged.connect(lambda args: set_attr_callback(*args)) master_close_event = master.closeEvent master_hide_event = master.hideEvent master_delete_event = master.onDeleteWidget def closeEvent(event): vizrank.close() master_close_event(event) def hideEvent(event): vizrank.hide() master_hide_event(event) def deleteEvent(): vizrank.keep_running = False if vizrank._thread is not None and vizrank._thread.isRunning(): vizrank._thread.quit() vizrank._thread.wait() master_delete_event() master.closeEvent = closeEvent master.hideEvent = hideEvent master.onDeleteWidget = deleteEvent return vizrank, button def reshow(self): """Put the widget on top of all windows """ self.show() self.raise_() self.activateWindow() def initialize(self): """ Clear and initialize the dialog. This method must be called by the widget when the data is reset, e.g. from `set_data` handler. """ if self._thread is not None and self._thread.isRunning(): self.keep_running = False self._thread.quit() self._thread.wait() self.keep_running = False self.scheduled_call = None self.saved_state = None self.saved_progress = 0 self.update_timer.stop() self.progressBarFinished() self.scores = [] self._update_model() # empty queue self.rank_model.clear() self.button.setText("Start") self.button.setEnabled(self.check_preconditions()) self._thread = QThread(self) self._worker = Worker(self) self._worker.moveToThread(self._thread) self._worker.stopped.connect(self._thread.quit) self._worker.stopped.connect(self._select_first_if_none) self._worker.stopped.connect(self._stopped) self._worker.done.connect(self._done) self._thread.started.connect(self._worker.do_work) def filter_changed(self, text): self.model_proxy.setFilterFixedString(text) def stop_and_reset(self, reset_method=None): if self.keep_running: self.scheduled_call = reset_method or self.initialize self.keep_running = False else: self.initialize() def check_preconditions(self): """Check whether there is sufficient data for ranking.""" return True def on_selection_changed(self, selected, deselected): """ Set the new visualization in the widget when the user select a row in the table. If derived class does not reimplement this, the table gives the information but the user can't click it to select the visualization. Args: selected: the index of the selected item deselected: the index of the previously selected item """ pass def iterate_states(self, initial_state): """ Generate all possible states (e.g. attribute combinations) for the given data. The content of the generated states is specific to the visualization. This method must be defined in the derived classes. Args: initial_state: initial state; None if this is the first call """ raise NotImplementedError def state_count(self): """ Return the number of states for the progress bar. Derived classes should implement this to ensure the proper behaviour of the progress bar""" return 0 def compute_score(self, state): """ Abstract method for computing the score for the given state. Smaller scores are better. Args: state: the state, e.g. the combination of attributes as generated by :obj:`state_count`. """ raise NotImplementedError def bar_length(self, score): """Compute the bar length (between 0 and 1) corresponding to the score. Return `None` if the score cannot be normalized. """ return None def row_for_state(self, score, state): """ Abstract method that return the items that are inserted into the table. Args: score: score, computed by :obj:`compute_score` state: the state, e.g. combination of attributes """ raise NotImplementedError def _select_first_if_none(self): if not self.rank_table.selectedIndexes(): self.rank_table.selectRow(0) def _done(self): self.button.setText("Finished") self.button.setEnabled(False) self.keep_running = False self.saved_state = None def _stopped(self): self.update_timer.stop() self.progressBarFinished() self._update_model() self.stopped() if self.scheduled_call: self.scheduled_call() def _update(self): self._update_model() self._update_progress() def _update_progress(self): self.progressBarSet( int(self.saved_progress * 100 / max(1, self.state_count()))) def _update_model(self): try: while True: pos, row_items = self.add_to_model.get_nowait() self.rank_model.insertRow(pos, row_items) except queue.Empty: pass def toggle(self): """Start or pause the computation.""" self.keep_running = not self.keep_running if self.keep_running: self.button.setText("Pause") self.progressBarInit() self.update_timer.start() self.before_running() self._thread.start() else: self.button.setText("Continue") self._thread.quit() # Need to sync state (the worker must read the keep_running # state and stop) for reliable restart. self._thread.wait() def before_running(self): """Code that is run before running vizrank in its own thread""" pass def stopped(self): """Code that is run after stopping the vizrank thread""" pass
class OWScatterPlot(OWDataProjectionWidget): """Scatterplot visualization with explorative analysis and intelligent data visualization enhancements.""" name = 'Scatter Plot' description = "Interactive scatter plot visualization with " \ "intelligent data visualization enhancements." icon = "icons/ScatterPlot.svg" priority = 140 keywords = [] class Inputs(OWDataProjectionWidget.Inputs): features = Input("Features", AttributeList) class Outputs(OWDataProjectionWidget.Outputs): features = Output("Features", AttributeList, dynamic=False) settings_version = 4 auto_sample = Setting(True) attr_x = ContextSetting(None) attr_y = ContextSetting(None) tooltip_shows_all = Setting(True) GRAPH_CLASS = OWScatterPlotGraph graph = SettingProvider(OWScatterPlotGraph) embedding_variables_names = None xy_changed_manually = Signal(Variable, Variable) class Warning(OWDataProjectionWidget.Warning): missing_coords = Msg("Plot cannot be displayed because '{}' or '{}' " "is missing for all data points") no_continuous_vars = Msg("Data has no continuous variables") class Information(OWDataProjectionWidget.Information): sampled_sql = Msg("Large SQL table; showing a sample.") missing_coords = Msg( "Points with missing '{}' or '{}' are not displayed") def __init__(self): self.sql_data = None # Orange.data.sql.table.SqlTable self.attribute_selection_list = None # list of Orange.data.Variable self.__timer = QTimer(self, interval=1200) self.__timer.timeout.connect(self.add_data) super().__init__() # manually register Matplotlib file writers self.graph_writers = self.graph_writers.copy() for w in [MatplotlibFormat, MatplotlibPDFFormat]: self.graph_writers.append(w) def _add_controls(self): self._add_controls_axis() self._add_controls_sampling() super()._add_controls() self.gui.add_widgets([ self.gui.ShowGridLines, self.gui.ToolTipShowsAll, self.gui.RegressionLine ], self._plot_box) gui.checkBox( gui.indentedBox(self._plot_box), self, value="graph.orthonormal_regression", label="Treat variables as independent", callback=self.graph.update_regression_line, tooltip= "If checked, fit line to group (minimize distance from points);\n" "otherwise fit y as a function of x (minimize vertical distances)") def _add_controls_axis(self): common_options = dict(labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str, contentsLength=14) box = gui.vBox(self.controlArea, True) dmod = DomainModel self.xy_model = DomainModel(dmod.MIXED, valid_types=ContinuousVariable) self.cb_attr_x = gui.comboBox(box, self, "attr_x", label="Axis x:", callback=self.set_attr_from_combo, model=self.xy_model, **common_options) self.cb_attr_y = gui.comboBox(box, self, "attr_y", label="Axis y:", callback=self.set_attr_from_combo, model=self.xy_model, **common_options) vizrank_box = gui.hBox(box) self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank( vizrank_box, self, "Find Informative Projections", self.set_attr) def _add_controls_sampling(self): self.sampling = gui.auto_commit(self.controlArea, self, "auto_sample", "Sample", box="Sampling", callback=self.switch_sampling, commit=lambda: self.add_data(1)) self.sampling.setVisible(False) @property def effective_variables(self): return [self.attr_x, self.attr_y] def _vizrank_color_change(self): self.vizrank.initialize() is_enabled = self.data is not None and not self.data.is_sparse() and \ len(self.xy_model) > 2 and len(self.data[self.valid_data]) > 1 \ and np.all(np.nan_to_num(np.nanstd(self.data.X, 0)) != 0) self.vizrank_button.setEnabled( is_enabled and self.attr_color is not None and not np.isnan( self.data.get_column_view( self.attr_color)[0].astype(float)).all()) text = "Color variable has to be selected." \ if is_enabled and self.attr_color is None else "" self.vizrank_button.setToolTip(text) def set_data(self, data): super().set_data(data) def findvar(name, iterable): """Find a Orange.data.Variable in `iterable` by name""" for el in iterable: if isinstance(el, Variable) and el.name == name: return el return None # handle restored settings from < 3.3.9 when attr_* were stored # by name if isinstance(self.attr_x, str): self.attr_x = findvar(self.attr_x, self.xy_model) if isinstance(self.attr_y, str): self.attr_y = findvar(self.attr_y, self.xy_model) if isinstance(self.attr_label, str): self.attr_label = findvar(self.attr_label, self.gui.label_model) if isinstance(self.attr_color, str): self.attr_color = findvar(self.attr_color, self.gui.color_model) if isinstance(self.attr_shape, str): self.attr_shape = findvar(self.attr_shape, self.gui.shape_model) if isinstance(self.attr_size, str): self.attr_size = findvar(self.attr_size, self.gui.size_model) def check_data(self): super().check_data() self.__timer.stop() self.sampling.setVisible(False) self.sql_data = None if isinstance(self.data, SqlTable): if self.data.approx_len() < 4000: self.data = Table(self.data) else: self.Information.sampled_sql() self.sql_data = self.data data_sample = self.data.sample_time(0.8, no_cache=True) data_sample.download_data(2000, partial=True) self.data = Table(data_sample) self.sampling.setVisible(True) if self.auto_sample: self.__timer.start() if self.data is not None: if not self.data.domain.has_continuous_attributes(True, True): self.Warning.no_continuous_vars() self.data = None if self.data is not None and (len(self.data) == 0 or len(self.data.domain) == 0): self.data = None def get_embedding(self): self.valid_data = None if self.data is None: return None x_data = self.get_column(self.attr_x, filter_valid=False) y_data = self.get_column(self.attr_y, filter_valid=False) if x_data is None or y_data is None: return None self.Warning.missing_coords.clear() self.Information.missing_coords.clear() self.valid_data = np.isfinite(x_data) & np.isfinite(y_data) if self.valid_data is not None and not np.all(self.valid_data): msg = self.Information if np.any(self.valid_data) else self.Warning msg.missing_coords(self.attr_x.name, self.attr_y.name) return np.vstack((x_data, y_data)).T # Tooltip def _point_tooltip(self, point_id, skip_attrs=()): point_data = self.data[point_id] xy_attrs = (self.attr_x, self.attr_y) text = "<br/>".join( escape('{} = {}'.format(var.name, point_data[var])) for var in xy_attrs) if self.tooltip_shows_all: others = super()._point_tooltip(point_id, skip_attrs=xy_attrs) if others: text = "<b>{}</b><br/><br/>{}".format(text, others) return text def add_data(self, time=0.4): if self.data and len(self.data) > 2000: self.__timer.stop() return data_sample = self.sql_data.sample_time(time, no_cache=True) if data_sample: data_sample.download_data(2000, partial=True) data = Table(data_sample) self.data = Table.concatenate((self.data, data), axis=0) self.handleNewSignals() def init_attr_values(self): super().init_attr_values() data = self.data domain = data.domain if data and len(data) else None self.xy_model.set_domain(domain) self.attr_x = self.xy_model[0] if self.xy_model else None self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \ else self.attr_x def switch_sampling(self): self.__timer.stop() if self.auto_sample and self.sql_data: self.add_data() self.__timer.start() def set_subset_data(self, subset_data): self.warning() if isinstance(subset_data, SqlTable): if subset_data.approx_len() < AUTO_DL_LIMIT: subset_data = Table(subset_data) else: self.warning("Data subset does not support large Sql tables") subset_data = None super().set_subset_data(subset_data) # called when all signals are received, so the graph is updated only once def handleNewSignals(self): if self.attribute_selection_list and self.data is not None and \ self.data.domain is not None and \ all(attr in self.data.domain for attr in self.attribute_selection_list): self.attr_x, self.attr_y = self.attribute_selection_list[:2] self.attribute_selection_list = None super().handleNewSignals() if self._domain_invalidated: self.graph.update_axes() self._domain_invalidated = False self._vizrank_color_change() @Inputs.features def set_shown_attributes(self, attributes): if attributes and len(attributes) >= 2: self.attribute_selection_list = attributes[:2] self._invalidated = self._invalidated \ or self.attr_x != attributes[0] \ or self.attr_y != attributes[1] else: self.attribute_selection_list = None def set_attr(self, attr_x, attr_y): if attr_x != self.attr_x or attr_y != self.attr_y: self.attr_x, self.attr_y = attr_x, attr_y self.attr_changed() def set_attr_from_combo(self): self.attr_changed() self.xy_changed_manually.emit(self.attr_x, self.attr_y) def attr_changed(self): self.setup_plot() self.commit() def get_axes(self): return {"bottom": self.attr_x, "left": self.attr_y} def colors_changed(self): super().colors_changed() self._vizrank_color_change() def commit(self): super().commit() self.send_features() def send_features(self): features = [attr for attr in [self.attr_x, self.attr_y] if attr] self.Outputs.features.send(features or None) def get_widget_name_extension(self): if self.data is not None: return "{} vs {}".format(self.attr_x.name, self.attr_y.name) return None @classmethod def migrate_settings(cls, settings, version): if version < 2 and "selection" in settings and settings["selection"]: settings["selection_group"] = [(a, 1) for a in settings["selection"]] if version < 3: if "auto_send_selection" in settings: settings["auto_commit"] = settings["auto_send_selection"] if "selection_group" in settings: settings["selection"] = settings["selection_group"] @classmethod def migrate_context(cls, context, version): values = context.values if version < 3: values["attr_color"] = values["graph"]["attr_color"] values["attr_size"] = values["graph"]["attr_size"] values["attr_shape"] = values["graph"]["attr_shape"] values["attr_label"] = values["graph"]["attr_label"] if version < 4: if values["attr_x"][1] % 100 == 1 or values["attr_y"][1] % 100 == 1: raise IncompatibleContext()
class OWChoropleth(OWWidget): """ This is to `OWDataProjectionWidget` what `OWChoroplethPlotGraph` is to `OWScatterPlotBase`. """ name = 'Choropleth Map' description = 'A thematic map in which areas are shaded in proportion ' \ 'to the measurement of the statistical variable being displayed.' icon = "icons/Choropleth.svg" priority = 120 class Inputs: data = Input("Data", Table, default=True) class Outputs: selected_data = Output("Selected Data", Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) settings_version = 2 settingsHandler = DomainContextHandler() selection = Setting(None, schema_only=True) auto_commit = Setting(True) attr_lat = ContextSetting(None) attr_lon = ContextSetting(None) agg_attr = ContextSetting(None) agg_func = ContextSetting(DEFAULT_AGG_FUNC) admin_level = Setting(0) binning_index = Setting(0) GRAPH_CLASS = OWChoroplethPlotMapGraph graph = SettingProvider(OWChoroplethPlotMapGraph) graph_name = "graph.plot_widget.plotItem" input_changed = Signal(object) output_changed = Signal(object) class Warning(OWWidget.Warning): no_lat_lon_vars = Msg("Data has no latitude and longitude variables.") no_region = Msg("{} points are not in any region.") def __init__(self): super().__init__() self.data = None self.data_ids = None # type: Optional[np.ndarray] self.agg_data = None # type: Optional[np.ndarray] self.region_ids = None # type: Optional[np.ndarray] self.choropleth_regions = [] self.binnings = [] self.input_changed.connect(self.set_input_summary) self.output_changed.connect(self.set_output_summary) self.setup_gui() def setup_gui(self): self._add_graph() self._add_controls() self.input_changed.emit(None) self.output_changed.emit(None) def _add_graph(self): box = gui.vBox(self.mainArea, True, margin=0) self.graph = self.GRAPH_CLASS(self, box) box.layout().addWidget(self.graph.plot_widget) def _add_controls(self): options = dict( labelWidth=75, orientation=Qt.Horizontal, sendSelectedValue=True, contentsLength=14 ) lat_lon_box = gui.vBox(self.controlArea, True) self.lat_lon_model = DomainModel(DomainModel.MIXED, valid_types=(ContinuousVariable,)) gui.comboBox(lat_lon_box, self, 'attr_lon', label='Longitude:', callback=self.setup_plot, model=self.lat_lon_model, **options) gui.comboBox(lat_lon_box, self, 'attr_lat', label='Latitude:', callback=self.setup_plot, model=self.lat_lon_model, **options) agg_box = gui.vBox(self.controlArea, True) self.agg_attr_model = DomainModel(valid_types=(ContinuousVariable, DiscreteVariable)) gui.comboBox(agg_box, self, 'agg_attr', label='Attribute:', callback=self.update_agg, model=self.agg_attr_model, **options) self.agg_func_combo = gui.comboBox(agg_box, self, 'agg_func', label='Agg.:', items=[DEFAULT_AGG_FUNC], callback=self.graph.update_colors, **options) a_slider = gui.hSlider(agg_box, self, 'admin_level', minValue=0, maxValue=2, step=1, label='Detail:', createLabel=False, callback=self.setup_plot) a_slider.setFixedWidth(176) visualization_box = gui.vBox(self.controlArea, True) b_slider = gui.hSlider(visualization_box, self, "binning_index", label="Bin width:", minValue=0, maxValue=max(1, len(self.binnings) - 1), createLabel=False, callback=self.graph.update_colors) b_slider.setFixedWidth(176) av_slider = gui.hSlider(visualization_box, self, "graph.alpha_value", minValue=0, maxValue=255, step=10, label="Opacity:", createLabel=False, callback=self.graph.update_colors) av_slider.setFixedWidth(176) gui.checkBox(visualization_box, self, "graph.show_legend", "Show legend", callback=self.graph.update_legend_visibility) self.controlArea.layout().addStretch(100) plot_gui = OWPlotGUI(self) plot_gui.box_zoom_select(self.controlArea) gui.auto_send(self.controlArea, self, "auto_commit") @property def effective_variables(self): return [self.attr_lat, self.attr_lon] \ if self.attr_lat and self.attr_lon else [] @property def effective_data(self): return self.data.transform(Domain(self.effective_variables)) # Input @Inputs.data @check_sql_input def set_data(self, data): data_existed = self.data is not None effective_data = self.effective_data if data_existed else None self.closeContext() self.data = data self.agg_func = DEFAULT_AGG_FUNC self.Warning.no_region.clear() self.Warning.no_lat_lon_vars.clear() self.init_attr_values() self.openContext(self.data) if not (data_existed and self.data is not None and array_equal(effective_data.X, self.effective_data.X)): self.clear(cache=True) self.input_changed.emit(data) self.setup_plot() self.update_agg() self.apply_selection() self.unconditional_commit() def init_attr_values(self): domain = self.data.domain if self.data else None self.lat_lon_model.set_domain(domain) self.agg_attr_model.set_domain(domain) self.agg_attr = None self.attr_lat, self.attr_lon = None, None if self.data: self.agg_attr = self.data.domain.class_var attr_lat, attr_lon = find_lat_lon(self.data, filter_hidden=True) if attr_lat is None or attr_lon is None: # we either find both or none self.Warning.no_lat_lon_vars() else: self.attr_lat, self.attr_lon = attr_lat, attr_lon def set_input_summary(self, data): summary = str(len(data)) if data else self.info.NoInput self.info.set_input_summary(summary) def set_output_summary(self, data): summary = str(len(data)) if data else self.info.NoOutput self.info.set_output_summary(summary) def update_agg(self): current_agg = self.agg_func self.agg_func_combo.clear() new_aggs = list(AGG_FUNCS) if self.agg_attr is not None: if self.agg_attr.is_discrete: new_aggs = [agg for agg in AGG_FUNCS if AGG_FUNCS[agg].disc] elif self.agg_attr.is_time: new_aggs = [agg for agg in AGG_FUNCS if AGG_FUNCS[agg].time] self.agg_func_combo.addItems(new_aggs) if current_agg in new_aggs: self.agg_func = current_agg else: self.agg_func = DEFAULT_AGG_FUNC self.graph.update_colors() def setup_plot(self): self.controls.binning_index.setEnabled(not self.is_mode()) self.clear() self.graph.reset_graph() def is_valid(self): return self.attr_lat is not None and \ (self.agg_attr is not None or self.agg_func == "Count") def apply_selection(self): if self.data is not None and self.selection is not None: index_group = np.array(self.selection).T selection = np.zeros(self.graph.n_ids, dtype=np.uint8) selection[index_group[0]] = index_group[1] self.graph.selection = selection self.graph.update_selection_colors() def selection_changed(self): sel = None if self.data and isinstance(self.data, SqlTable) \ else self.graph.selection self.selection = [(i, x) for i, x in enumerate(sel) if x] \ if sel is not None else None self.commit() def commit(self): self.send_data() def send_data(self): data, graph_sel = self.data, self.graph.get_selection() group_sel, selected_data, ann_data = None, None, None if data is not None and len(data) and self.region_ids is not None: # we get selection by region ids so we have to map it to points group_sel = np.zeros(len(data), dtype=int) for id, s in zip(self.region_ids, graph_sel): if s == 0: continue id_indices = np.where(self.data_ids == id)[0] group_sel[id_indices] = s if np.sum(graph_sel) > 0: selected_data = create_groups_table(data, group_sel, False, "Group") if data is not None: if np.max(graph_sel) > 1: ann_data = create_groups_table(data, group_sel) else: ann_data = create_annotated_table(data, group_sel.astype(bool)) self.output_changed.emit(selected_data) self.Outputs.selected_data.send(selected_data) self.Outputs.annotated_data.send(ann_data) def recompute_binnings(self): if self.is_mode(): return if self.is_time(): self.binnings = time_binnings(self.agg_data, min_unique=3, min_bins=3, max_bins=15) else: self.binnings = decimal_binnings(self.agg_data, min_unique=3, min_bins=3, max_bins=15) max_bins = len(self.binnings) - 1 self.controls.binning_index.setMaximum(max_bins) self.binning_index = min(max_bins, self.binning_index) def get_binning(self) -> BinDefinition: return self.binnings[self.binning_index] def get_palette(self): if self.agg_func in ('Count', 'Count defined'): return DefaultContinuousPalette elif self.is_mode(): return LimitedDiscretePalette(MAX_COLORS) else: return self.agg_attr.palette def get_color_data(self): return self.get_reduced_agg_data() def get_color_labels(self): if self.is_mode(): return self.get_reduced_agg_data(return_labels=True) elif self.is_time(): return self.agg_attr.str_val def get_reduced_agg_data(self, return_labels=False): """ This returns agg data or its labels. It also merges infrequent data. """ needs_merging = self.is_mode() \ and len(self.agg_attr.values) >= MAX_COLORS if return_labels and not needs_merging: return self.agg_attr.values if not needs_merging: return self.agg_data dist = bincount(self.agg_data, max_val=len(self.agg_attr.values) - 1)[0] infrequent = np.zeros(len(self.agg_attr.values), dtype=bool) infrequent[np.argsort(dist)[:-(MAX_COLORS - 1)]] = True if return_labels: return [value for value, infreq in zip(self.agg_attr.values, infrequent) if not infreq] + ["Other"] else: result = self.agg_data.copy() freq_vals = [i for i, f in enumerate(infrequent) if not f] for i, infreq in enumerate(infrequent): if infreq: result[self.agg_data == i] = MAX_COLORS - 1 else: result[self.agg_data == i] = freq_vals.index(i) return result def is_mode(self): return self.agg_attr is not None and \ self.agg_attr.is_discrete and \ self.agg_func == 'Mode' def is_time(self): return self.agg_attr is not None and \ self.agg_attr.is_time and \ self.agg_func not in ('Count', 'Count defined') @memoize_method(3) def get_regions(self, lat_attr, lon_attr, admin): """ Map points to regions and get regions information. Returns: ndarray of ids corresponding to points, dict of region ids matched to their additional info, dict of region ids matched to their polygon """ latlon = np.c_[self.data.get_column_view(lat_attr)[0], self.data.get_column_view(lon_attr)[0]] region_info = latlon2region(latlon, admin) ids = np.array([region.get('_id') for region in region_info]) region_info = {info.get('_id'): info for info in region_info} self.data_ids = np.array(ids) no_region = np.sum(self.data_ids == None) if no_region: self.Warning.no_region(no_region) unique_ids = list(set(ids) - {None}) polygons = {_id: poly for _id, poly in zip(unique_ids, get_shape(unique_ids))} return ids, region_info, polygons def get_grouped(self, lat_attr, lon_attr, admin, attr, agg_func): """ Get aggregation value for points grouped by regions. Returns: Series of aggregated values """ if attr is not None: data = self.data.get_column_view(attr)[0] else: data = np.ones(len(self.data)) ids, _, _ = self.get_regions(lat_attr, lon_attr, admin) result = pd.Series(data, dtype=float)\ .groupby(ids)\ .agg(AGG_FUNCS[agg_func].transform) return result def get_agg_data(self) -> np.ndarray: result = self.get_grouped(self.attr_lat, self.attr_lon, self.admin_level, self.agg_attr, self.agg_func) self.agg_data = np.array(result.values) self.region_ids = np.array(result.index) arg_region_sort = np.argsort(self.region_ids) self.region_ids = self.region_ids[arg_region_sort] self.agg_data = self.agg_data[arg_region_sort] self.recompute_binnings() return self.agg_data def _repr_val(self, value): if self.agg_func in ('Count', 'Count defined'): return f"{value:d}" else: return self.agg_attr.repr_val(value) def get_choropleth_regions(self) -> List[_ChoroplethRegion]: """Recalculate regions""" if not self.is_valid(): return [] _, region_info, polygons = self.get_regions(self.attr_lat, self.attr_lon, self.admin_level) regions = [] for _id in polygons: if isinstance(polygons[_id], MultiPolygon): # some regions consist of multiple polygons polys = list(polygons[_id].geoms) else: polys = [polygons[_id]] qpolys = [self.poly2qpoly(transform(self.deg2canvas, poly)) for poly in polys] regions.append(_ChoroplethRegion(id=_id, info=region_info[_id], qpolys=qpolys)) self.choropleth_regions = sorted(regions, key=lambda cr: cr.id) self.get_agg_data() return self.choropleth_regions @staticmethod def poly2qpoly(poly: Polygon) -> QPolygonF: return QPolygonF([QPointF(x, y) for x, y in poly.exterior.coords]) @staticmethod def deg2canvas(x, y): x, y = deg2norm(x, y) y = 1 - y return x, y def clear(self, cache=False): self.choropleth_regions = [] if cache: self.get_regions.cache_clear() def send_report(self): if self.data is None: return self.report_plot() def sizeHint(self): return QSize(1132, 708) def onDeleteWidget(self): super().onDeleteWidget() self.graph.plot_widget.getViewBox().deleteLater() self.graph.plot_widget.clear() self.graph.clear() def keyPressEvent(self, event): """Update the tip about using the modifier keys when selecting""" super().keyPressEvent(event) self.graph.update_tooltip(event.modifiers()) def keyReleaseEvent(self, event): """Update the tip about using the modifier keys when selecting""" super().keyReleaseEvent(event) self.graph.update_tooltip(event.modifiers()) def showEvent(self, ev): super().showEvent(ev) # reset the map on show event since before that we didn't know the # right resolution self.graph.update_view_range() def resizeEvent(self, ev): super().resizeEvent(ev) # when resizing we need to constantly reset the map so that new # portions are drawn self.graph.update_view_range(match_data=False) @classmethod def migrate_settings(cls, settings, version): if version < 2: settings["graph"] = {} rename_setting(settings, "admin", "admin_level") rename_setting(settings, "autocommit", "auto_commit") settings["graph"]["alpha_value"] = \ round(settings["opacity"] * 2.55) settings["graph"]["show_legend"] = settings["show_legend"] @classmethod def migrate_context(cls, context, version): if version < 2: migrate_str_to_variable(context, names="lat_attr", none_placeholder="") migrate_str_to_variable(context, names="lon_attr", none_placeholder="") migrate_str_to_variable(context, names="attr", none_placeholder="") rename_setting(context, "lat_attr", "attr_lat") rename_setting(context, "lon_attr", "attr_lon") rename_setting(context, "attr", "agg_attr") # old selection will not be ported rename_setting(context, "selection", "old_selection") if context.values["agg_func"][0] == "Max": context.values["agg_func"] = ("Maximal", context.values["agg_func"][1]) elif context.values["agg_func"][0] == "Min": context.values["agg_func"] = ("Minimal", context.values["agg_func"][1]) elif context.values["agg_func"][0] == "Std": context.values["agg_func"] = ("Std.", context.values["agg_func"][1])
class Installer(QObject): installStatusChanged = Signal(str) started = Signal() finished = Signal() error = Signal(str, object, int, list) def __init__(self, parent=None, steps=[], user_install=False): QObject.__init__(self, parent) self.__interupt = False self.__queue = deque(steps) self.__user_install = user_install def start(self): QTimer.singleShot(0, self._next) def interupt(self): self.__interupt = True def setStatusMessage(self, message): self.__statusMessage = message self.installStatusChanged.emit(message) @Slot() def _next(self): def fmt_cmd(cmd): return "Command failed: python " + " ".join(map(shlex.quote, cmd)) command, pkg = self.__queue.popleft() if command == Install: inst = pkg.installable inst_name = inst.name if inst.package_url.startswith( "http://") else inst.package_url self.setStatusMessage("Installing {}".format(inst.name)) cmd = (["-m", "pip", "install"] + (["--user"] if self.__user_install else []) + [inst_name]) process = python_process(cmd, bufsize=-1, universal_newlines=True, env=_env_with_proxies()) retcode, output = self.__subprocessrun(process) if retcode != 0: self.error.emit(fmt_cmd(cmd), pkg, retcode, output) return elif command == Upgrade: inst = pkg.installable inst_name = inst.name if inst.package_url.startswith( "http://") else inst.package_url self.setStatusMessage("Upgrading {}".format(inst.name)) cmd = (["-m", "pip", "install", "--upgrade", "--no-deps"] + (["--user"] if self.__user_install else []) + [inst_name]) process = python_process(cmd, bufsize=-1, universal_newlines=True, env=_env_with_proxies()) retcode, output = self.__subprocessrun(process) if retcode != 0: self.error.emit(fmt_cmd(cmd), pkg, retcode, output) return # Why is this here twice?? cmd = (["-m", "pip", "install"] + (["--user"] if self.__user_install else []) + [inst_name]) process = python_process(cmd, bufsize=-1, universal_newlines=True, env=_env_with_proxies()) retcode, output = self.__subprocessrun(process) if retcode != 0: self.error.emit(fmt_cmd(cmd), pkg, retcode, output) return elif command == Uninstall: dist = pkg.local self.setStatusMessage("Uninstalling {}".format(dist.project_name)) cmd = ["-m", "pip", "uninstall", "--yes", dist.project_name] process = python_process(cmd, bufsize=-1, universal_newlines=True, env=_env_with_proxies()) retcode, output = self.__subprocessrun(process) if self.__user_install: # Remove the package forcefully; pip doesn't (yet) uninstall # --user packages (or any package outside sys.prefix?) # google: pip "Not uninstalling ?" "outside environment" install_path = os.path.join( USER_SITE, re.sub('[^\w]', '_', dist.project_name)) pip_record = next(iglob(install_path + '*.dist-info/RECORD'), None) if pip_record: with open(pip_record) as f: files = [line.rsplit(',', 2)[0] for line in f] else: files = [ os.path.join(USER_SITE, 'orangecontrib', dist.project_name.split('-')[-1].lower()), ] for match in itertools.chain(files, iglob(install_path + '*')): print('rm -rf', match) if os.path.isdir(match): shutil.rmtree(match) elif os.path.exists(match): os.unlink(match) if retcode != 0: self.error.emit(fmt_cmd(cmd), pkg, retcode, output) return if self.__queue: QTimer.singleShot(0, self._next) else: self.finished.emit() def __subprocessrun(self, process): output = [] while process.poll() is None: try: line = process.stdout.readline() except IOError as ex: if ex.errno != errno.EINTR: raise else: output.append(line) print(line, end="") # Read remaining output if any line = process.stdout.read() if line: output.append(line) print(line, end="") return process.returncode, output
class Installer(QObject): installStatusChanged = Signal(str) started = Signal() finished = Signal() error = Signal(str, object, int, list) def __init__(self, parent=None, steps=[]): QObject.__init__(self, parent) self.__interupt = False self.__queue = deque(steps) self.__statusMessage = "" def start(self): QTimer.singleShot(0, self._next) def interupt(self): self.__interupt = True def setStatusMessage(self, message): if self.__statusMessage != message: self.__statusMessage = message self.installStatusChanged.emit(message) @Slot() def _next(self): def fmt_cmd(cmd): return "python " + (" ".join(map(shlex.quote, cmd))) command, pkg = self.__queue.popleft() if command == Install: inst = pkg.installable self.setStatusMessage("Installing {}".format(inst.name)) links = [] cmd = ["-m", "pip", "install"] + links + [inst.name] process = python_process(cmd, bufsize=-1, universal_newlines=True, env=_env_with_proxies()) retcode, output = self.__subprocessrun(process) if retcode != 0: self.error.emit(fmt_cmd(cmd), pkg, retcode, output) return elif command == Upgrade: inst = pkg.installable self.setStatusMessage("Upgrading {}".format(inst.name)) cmd = ["-m", "pip", "install", "--upgrade", "--no-deps", inst.name] process = python_process(cmd, bufsize=-1, universal_newlines=True, env=_env_with_proxies()) retcode, output = self.__subprocessrun(process) if retcode != 0: self.error.emit(fmt_cmd(cmd), pkg, retcode, output) return cmd = ["-m", "pip", "install", inst.name] process = python_process(cmd, bufsize=-1, universal_newlines=True, env=_env_with_proxies()) retcode, output = self.__subprocessrun(process) if retcode != 0: self.error.emit(fmt_cmd(cmd), pkg, retcode, output) return elif command == Uninstall: dist = pkg.local self.setStatusMessage("Uninstalling {}".format(dist.project_name)) cmd = ["-m", "pip", "uninstall", "--yes", dist.project_name] process = python_process(cmd, bufsize=-1, universal_newlines=True, env=_env_with_proxies()) retcode, output = self.__subprocessrun(process) if retcode != 0: self.error.emit(fmt_cmd(cmd), pkg, retcode, output) return if self.__queue: QTimer.singleShot(0, self._next) else: self.finished.emit() def __subprocessrun(self, process): output = [] while process.poll() is None: try: line = process.stdout.readline() except IOError as ex: if ex.errno != errno.EINTR: raise else: output.append(line) print(line, end="") # Read remaining output if any line = process.stdout.read() if line: output.append(line) print(line, end="") return process.returncode, output
class ScoreTable(OWComponent, QObject): shown_scores = \ Setting(set(chain(*BUILTIN_SCORERS_ORDER.values()))) shownScoresChanged = Signal() class ItemDelegate(QStyledItemDelegate): def sizeHint(self, *args): size = super().sizeHint(*args) return QSize(size.width(), size.height() + 6) def displayText(self, value, locale): if isinstance(value, float): return f"{value:.3f}" else: return super().displayText(value, locale) def __init__(self, master): QObject.__init__(self) OWComponent.__init__(self, master) self.view = gui.TableView(wordWrap=True, editTriggers=gui.TableView.NoEditTriggers) header = self.view.horizontalHeader() header.setSectionResizeMode(QHeaderView.ResizeToContents) header.setDefaultAlignment(Qt.AlignCenter) header.setStretchLastSection(False) header.setContextMenuPolicy(Qt.CustomContextMenu) header.customContextMenuRequested.connect(self.show_column_chooser) self.model = QStandardItemModel(master) self.model.setHorizontalHeaderLabels(["Method"]) self.sorted_model = ScoreModel() self.sorted_model.setSourceModel(self.model) self.view.setModel(self.sorted_model) self.view.setItemDelegate(self.ItemDelegate()) def _column_names(self): return (self.model.horizontalHeaderItem(section).data(Qt.DisplayRole) for section in range(1, self.model.columnCount())) def show_column_chooser(self, pos): # pylint doesn't know that self.shown_scores is a set, not a Setting # pylint: disable=unsupported-membership-test def update(col_name, checked): if checked: self.shown_scores.add(col_name) else: self.shown_scores.remove(col_name) self._update_shown_columns() menu = QMenu() header = self.view.horizontalHeader() for col_name in self._column_names(): action = menu.addAction(col_name) action.setCheckable(True) action.setChecked(col_name in self.shown_scores) action.triggered.connect(partial(update, col_name)) menu.exec(header.mapToGlobal(pos)) def _update_shown_columns(self): # pylint doesn't know that self.shown_scores is a set, not a Setting # pylint: disable=unsupported-membership-test header = self.view.horizontalHeader() for section, col_name in enumerate(self._column_names(), start=1): header.setSectionHidden(section, col_name not in self.shown_scores) self.view.resizeColumnsToContents() self.shownScoresChanged.emit() def update_header(self, scorers): # Set the correct horizontal header labels on the results_model. self.model.setColumnCount(1 + len(scorers)) self.model.setHorizontalHeaderItem(0, QStandardItem("Model")) for col, score in enumerate(scorers, start=1): item = QStandardItem(score.name) item.setToolTip(score.long_name) self.model.setHorizontalHeaderItem(col, item) self._update_shown_columns()
class EditableTreeView(QWidget): dataChanged = Signal() selectionChanged = Signal() def __init__(self, parent=None): super().__init__(parent=parent) self.__stack: List = [] self.__stack_index: int = -1 def push_on_data_changed(_, __, roles): if Qt.EditRole in roles: self._push_data() self.__model = QStandardItemModel() self.__model.dataChanged.connect(self.dataChanged) self.__model.dataChanged.connect(push_on_data_changed) self.__root: QStandardItem = self.__model.invisibleRootItem() self.__tree = TreeView(self.dataChanged) self.__tree.drop_finished.connect(self.dataChanged) self.__tree.drop_finished.connect(self._push_data) self.__tree.setModel(self.__model) self.__tree.selectionModel().selectionChanged.connect( self.selectionChanged) actions_widget = ModelActionsWidget() actions_widget.layout().setSpacing(1) action = QAction("+", self, toolTip="Add a new word") action.triggered.connect(self.__on_add) actions_widget.addAction(action) action = QAction("\N{MINUS SIGN}", self, toolTip="Remove word") action.triggered.connect(self.__on_remove) actions_widget.addAction(action) action = QAction("\N{MINUS SIGN}R", self, toolTip="Remove word recursively (incl. children)") action.triggered.connect(self.__on_remove_recursive) actions_widget.addAction(action) gui.rubber(actions_widget) self.__undo_action = action = QAction("Undo", self, toolTip="Undo") action.triggered.connect(self.__on_undo) actions_widget.addAction(action) self.__redo_action = action = QAction("Redo", self, toolTip="Redo") action.triggered.connect(self.__on_redo) actions_widget.addAction(action) self._enable_undo_redo() layout = QVBoxLayout() layout.setSpacing(1) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.__tree) layout.addWidget(actions_widget) self.setLayout(layout) def __on_add(self): parent: QStandardItem = self.__root selection: List = self.__tree.selectionModel().selectedIndexes() if selection: sel_index: QModelIndex = selection[0] parent: QStandardItem = self.__model.itemFromIndex(sel_index) item = QStandardItem("") parent.appendRow(item) index: QModelIndex = item.index() with disconnected(self.__model.dataChanged, self.dataChanged): self.__model.setItemData(index, {Qt.EditRole: ""}) self.__tree.setCurrentIndex(index) self.__tree.edit(index) def __on_remove_recursive(self): sel_model: QItemSelectionModel = self.__tree.selectionModel() if len(sel_model.selectedIndexes()): while sel_model.selectedIndexes(): index: QModelIndex = sel_model.selectedIndexes()[0] self.__model.removeRow(index.row(), index.parent()) self._push_data() self.dataChanged.emit() def __on_remove(self): sel_model: QItemSelectionModel = self.__tree.selectionModel() if len(sel_model.selectedIndexes()): while sel_model.selectedIndexes(): index: QModelIndex = sel_model.selectedIndexes()[0] # move children to item's parent item: QStandardItem = self.__model.itemFromIndex(index) children = [item.takeChild(i) for i in range(item.rowCount())] parent = item.parent() or self.__root self.__model.removeRow(index.row(), index.parent()) for child in children[::-1]: parent.insertRow(index.row(), child) self.__tree.expandAll() self._push_data() self.dataChanged.emit() def __on_undo(self): self.__stack_index -= 1 self._set_from_stack() def __on_redo(self): self.__stack_index += 1 self._set_from_stack() def get_words(self) -> List: return _model_to_words(self.__root) def get_selected_words(self) -> Set: return set(self.__model.itemFromIndex(index).text() for index in self.__tree.selectionModel().selectedIndexes()) def get_selected_words_with_children(self) -> Set: words = set() for index in self.__tree.selectionModel().selectedIndexes(): item: QStandardItem = self.__model.itemFromIndex(index) words.update(_model_to_words(item)) return words def get_data(self, with_selection=False) -> Union[Dict, OntoType]: selection = self.__tree.selectionModel().selectedIndexes() return _model_to_tree(self.__root, selection, with_selection) def set_data(self, data: Dict, keep_history: bool = False): if not keep_history: self.__stack = [] self.__stack_index = -1 self._set_data(data) self._push_data() def _set_data(self, data: Dict): self.clear() _tree_to_model(data, self.__root, self.__tree.selectionModel()) self.__tree.expandAll() def clear(self): if self.__model.hasChildren(): self.__model.removeRows(0, self.__model.rowCount()) def _enable_undo_redo(self): index = self.__stack_index self.__undo_action.setEnabled(index >= 1) self.__redo_action.setEnabled(index < len(self.__stack) - 1) def _push_data(self): self.__stack_index += 1 self.__stack = self.__stack[:self.__stack_index] self.__stack.append(self.get_data()) self._enable_undo_redo() def _set_from_stack(self): assert self.__stack_index < len(self.__stack) assert self.__stack_index >= 0 self._set_data(self.__stack[self.__stack_index]) self._enable_undo_redo() self.dataChanged.emit()
class DendrogramWidget(QGraphicsWidget): """A Graphics Widget displaying a dendrogram.""" class ClusterGraphicsItem(QGraphicsPathItem): #: The untransformed source path in 'dendrogram' logical coordinate #: system sourcePath = QPainterPath() # type: QPainterPath sourceAreaShape = QPainterPath() # type: QPainterPath __shape = None # type: Optional[QPainterPath] __boundingRect = None # type: Optional[QRectF] #: An extended path describing the full mouse hit area #: (extends all the way to the base of the dendrogram) __mouseAreaShape = QPainterPath() # type: QPainterPath def setGeometryData(self, path, hitArea): # type: (QPainterPath, QPainterPath) -> None """ Set the geometry (path) and the mouse hit area (hitArea) for this item. """ super().setPath(path) self.prepareGeometryChange() self.__boundingRect = self.__shape = None self.__mouseAreaShape = hitArea def shape(self): # type: () -> QPainterPath if self.__shape is None: path = super().shape() # type: QPainterPath self.__shape = path.united(self.__mouseAreaShape) return self.__shape def boundingRect(self): # type: () -> QRectF if self.__boundingRect is None: sh = self.shape() pw = self.pen().widthF() / 2.0 self.__boundingRect = sh.boundingRect().adjusted( -pw, -pw, pw, pw) return self.__boundingRect class _SelectionItem(QGraphicsItemGroup): def __init__(self, parent, path, unscaled_path, label=""): super().__init__(parent) self.path = QGraphicsPathItem(path, self) self.path.setPen(make_pen(width=1, cosmetic=True)) self.addToGroup(self.path) self.label = QGraphicsSimpleTextItem(label) self._update_label_pos() self.addToGroup(self.label) self.unscaled_path = unscaled_path def set_path(self, path): self.path.setPath(path) self._update_label_pos() def set_label(self, label): self.label.setText(label) self.label.setBrush(Qt.blue) self._update_label_pos() def set_color(self, color): self.path.setBrush(QColor(color)) def _update_label_pos(self): path = self.path.path() elements = (path.elementAt(i) for i in range(path.elementCount())) points = ((p.x, p.y) for p in elements) p1, p2, *rest = sorted(points) x, y = p1[0], (p1[1] + p2[1]) / 2 brect = self.label.boundingRect() # leaf nodes' paths are 4 pixels higher; leafs are `len(rest) == 3` self.label.setPos(x - brect.width() - 4, y - brect.height() + 4 * (len(rest) == 3)) #: Orientation Left, Top, Right, Bottom = 1, 2, 3, 4 #: Selection flags NoSelection, SingleSelection, ExtendedSelection = 0, 1, 2 #: Emitted when a user clicks on the cluster item. itemClicked = Signal(ClusterGraphicsItem) #: Signal emitted when the selection changes. selectionChanged = Signal() #: Signal emitted when the selection was changed by the user. selectionEdited = Signal() def __init__(self, parent=None, root=None, orientation=Left, hoverHighlightEnabled=True, selectionMode=ExtendedSelection, **kwargs): super().__init__(None, **kwargs) # Filter all events from children (`ClusterGraphicsItem`s) self.setFiltersChildEvents(True) self.orientation = orientation self._root = None #: A tree with dendrogram geometry self._layout = None self._highlighted_item = None #: a list of selected items self._selection = OrderedDict() #: a {node: item} mapping self._items = { } # type: Dict[Tree, DendrogramWidget.ClusterGraphicsItem] #: container for all cluster items. self._itemgroup = QGraphicsWidget(self) self._itemgroup.setGeometry(self.contentsRect()) #: Transform mapping from 'dendrogram' to widget local coordinate #: system self._transform = QTransform() self._cluster_parent = {} self.__hoverHighlightEnabled = hoverHighlightEnabled self.__selectionMode = selectionMode self.setContentsMargins(0, 0, 0, 0) self.setRoot(root) if parent is not None: self.setParentItem(parent) def setSelectionMode(self, mode): """ Set the selection mode. """ assert mode in [ DendrogramWidget.NoSelection, DendrogramWidget.SingleSelection, DendrogramWidget.ExtendedSelection ] if self.__selectionMode != mode: self.__selectionMode = mode if self.__selectionMode == DendrogramWidget.NoSelection and \ self._selection: self.setSelectedClusters([]) elif self.__selectionMode == DendrogramWidget.SingleSelection and \ len(self._selection) > 1: self.setSelectedClusters([self.selected_nodes()[-1]]) def selectionMode(self): """ Return the current selection mode. """ return self.__selectionMode def setHoverHighlightEnabled(self, enabled): if self.__hoverHighlightEnabled != bool(enabled): self.__hoverHighlightEnabled = bool(enabled) if self._highlighted_item is not None: self._set_hover_item(None) def isHoverHighlightEnabled(self): return self.__hoverHighlightEnabled def clear(self): """ Clear the widget. """ scene = self.scene() if scene is not None: scene.removeItem(self._itemgroup) else: self._itemgroup.setParentItem(None) self._itemgroup = QGraphicsWidget(self) self._itemgroup.setGeometry(self.contentsRect()) self._items.clear() for item in self._selection.values(): if scene is not None: scene.removeItem(item) else: item.setParentItem(None) self._root = None self._items = {} self._selection = OrderedDict() self._highlighted_item = None self._cluster_parent = {} self.updateGeometry() def setRoot(self, root): # type: (Tree) -> None """ Set the root cluster tree node for display. Parameters ---------- root : Tree The tree root node. """ self.clear() self._root = root if root is not None: pen = make_pen(Qt.blue, width=1, cosmetic=True, join_style=Qt.MiterJoin) for node in postorder(root): item = DendrogramWidget.ClusterGraphicsItem(self._itemgroup) item.setAcceptHoverEvents(True) item.setPen(pen) item.node = node for branch in node.branches: assert branch in self._items self._cluster_parent[branch] = node self._items[node] = item self._relayout() self._rescale() self.updateGeometry() set_root = setRoot def root(self): # type: () -> Tree """ Return the cluster tree root node. Returns ------- root : Tree """ return self._root def item(self, node): # type: (Tree) -> DendrogramWidget.ClusterGraphicsItem """ Return the ClusterGraphicsItem instance representing the cluster `node`. """ return self._items.get(node) def heightAt(self, point): # type: (QPointF) -> float """ Return the cluster height at the point in widget local coordinates. """ if not self._root: return 0 tinv, ok = self._transform.inverted() if not ok: return 0 tpoint = tinv.map(point) if self.orientation in [self.Left, self.Right]: height = tpoint.x() else: height = tpoint.y() # Undo geometry prescaling base = self._root.value.height scale = self._height_scale_factor() # Use better better precision then double provides. Fr = fractions.Fraction if scale > 0: height = Fr(height) / Fr(scale) else: height = 0 if self.orientation in [self.Left, self.Bottom]: height = Fr(base) - Fr(height) return float(height) height_at = heightAt def posAtHeight(self, height): # type: (float) -> float """ Return a point in local coordinates for `height` (in cluster """ if not self._root: return QPointF() scale = self._height_scale_factor() base = self._root.value.height height = scale * height if self.orientation in [self.Left, self.Bottom]: height = scale * base - height if self.orientation in [self.Left, self.Right]: p = QPointF(height, 0) else: p = QPointF(0, height) return self._transform.map(p) pos_at_height = posAtHeight def _set_hover_item(self, item): """Set the currently highlighted item.""" if self._highlighted_item is item: return def branches(item): return [self._items[ch] for ch in item.node.branches] if self._highlighted_item: pen = make_pen(Qt.blue, width=1, cosmetic=True) for it in postorder(self._highlighted_item, branches): it.setPen(pen) self._highlighted_item = item if item: hpen = make_pen(Qt.blue, width=2, cosmetic=True) for it in postorder(item, branches): it.setPen(hpen) def leafItems(self): """Iterate over the dendrogram leaf items (:class:`QGraphicsItem`). """ if self._root: return (self._items[leaf] for leaf in leaves(self._root)) else: return iter(()) leaf_items = leafItems def leafAnchors(self): """Iterate over the dendrogram leaf anchor points (:class:`QPointF`). The points are in the widget local coordinates. """ for item in self.leafItems(): anchor = QPointF(item.element.anchor) yield self.mapFromItem(item, anchor) leaf_anchors = leafAnchors def selectedNodes(self): """ Return the selected cluster nodes. """ return [item.node for item in self._selection] selected_nodes = selectedNodes def setSelectedItems(self, items: List[ClusterGraphicsItem]): """Set the item selection.""" to_remove = set(self._selection) - set(items) to_add = set(items) - set(self._selection) for sel in to_remove: self._remove_selection(sel) for sel in to_add: self._add_selection(sel) if to_add or to_remove: self._re_enumerate_selections() self.selectionChanged.emit() set_selected_items = setSelectedItems def setSelectedClusters(self, clusters: List[Tree]) -> None: """Set the selected clusters. """ self.setSelectedItems(list(map(self.item, clusters))) set_selected_clusters = setSelectedClusters def isItemSelected(self, item: ClusterGraphicsItem) -> bool: """Is `item` selected (is a root of a selection).""" return item in self._selection def isItemIncludedInSelection(self, item: ClusterGraphicsItem) -> bool: """Is item included in any selection.""" return self._selected_super_item(item) is not None is_included = isItemIncludedInSelection def setItemSelected(self, item, state): # type: (ClusterGraphicsItem, bool) -> None """Set the `item`s selection state to `state`.""" if state is False and item not in self._selection or \ state is True and item in self._selection: return # State unchanged if item in self._selection: if state is False: self._remove_selection(item) self._re_enumerate_selections() self.selectionChanged.emit() else: # If item is already inside another selected item, # remove that selection super_selection = self._selected_super_item(item) if super_selection: self._remove_selection(super_selection) # Remove selections this selection will override. sub_selections = self._selected_sub_items(item) for sub in sub_selections: self._remove_selection(sub) if state: self._add_selection(item) elif item in self._selection: self._remove_selection(item) self._re_enumerate_selections() self.selectionChanged.emit() select_item = setItemSelected @staticmethod def _create_path(item, path): ppath = QPainterPath() if item.node.is_leaf: ppath.addRect(path.boundingRect().adjusted(-8, -4, 0, 4)) else: ppath.addPolygon(path) ppath = path_outline(ppath, width=-8) return ppath @staticmethod def _create_label(i): return f"C{i + 1}" def _add_selection(self, item): """Add selection rooted at item """ outline = self._selection_poly(item) path = self._transform.map(outline) ppath = self._create_path(item, path) label = self._create_label(len(self._selection)) selection_item = self._SelectionItem(self, ppath, outline, label) selection_item.setPos(self.contentsRect().topLeft()) self._selection[item] = selection_item def _remove_selection(self, item): """Remove selection rooted at item.""" selection_item = self._selection[item] selection_item.hide() selection_item.setParentItem(None) if self.scene(): self.scene().removeItem(selection_item) del self._selection[item] def _selected_sub_items(self, item): """Return all selected subclusters under item.""" def branches(item): return [self._items[ch] for ch in item.node.branches] res = [] for item in list(preorder(item, branches))[1:]: if item in self._selection: res.append(item) return res def _selected_super_item(self, item): """Return the selected super item if it exists.""" def branches(item): return [self._items[ch] for ch in item.node.branches] for selected_item in self._selection: if item in set(preorder(selected_item, branches)): return selected_item return None def _re_enumerate_selections(self): """Re enumerate the selection items and update the colors.""" # Order the clusters items = sorted(self._selection.items(), key=lambda item: item[0].node.value.first) palette = colorpalettes.LimitedDiscretePalette(len(items)) for i, (item, selection_item) in enumerate(items): # delete and then reinsert to update the ordering del self._selection[item] self._selection[item] = selection_item selection_item.set_label(self._create_label(i)) color = palette[i] color.setAlpha(150) selection_item.set_color(color) def _selection_poly(self, item): # type: (Tree) -> QPolygonF """ Return an selection geometry covering item and all its children. """ def left(item): return [self._items[ch] for ch in item.node.branches[:1]] def right(item): return [self._items[ch] for ch in item.node.branches[-1:]] itemsleft = list(preorder(item, left))[::-1] itemsright = list(preorder(item, right)) # itemsleft + itemsright walks from the leftmost leaf up to the root # and down to the rightmost leaf assert itemsleft[0].node.is_leaf assert itemsright[-1].node.is_leaf if item.node.is_leaf: # a single anchor point vert = [itemsleft[0].element.anchor] else: vert = [] for it in itemsleft[1:]: vert.extend([ it.element.path[0], it.element.path[1], it.element.anchor ]) for it in itemsright[:-1]: vert.extend([ it.element.anchor, it.element.path[-2], it.element.path[-1] ]) # close the polygon vert.append(vert[0]) def isclose(a, b, rel_tol=1e-6): return abs(a - b) < rel_tol * max(abs(a), abs(b)) def isclose_p(p1, p2, rel_tol=1e-6): return isclose(p1.x, p2.x, rel_tol) and \ isclose(p1.y, p2.y, rel_tol) # merge consecutive vertices that are (too) close acc = [vert[0]] for v in vert[1:]: if not isclose_p(v, acc[-1]): acc.append(v) vert = acc return QPolygonF([QPointF(*p) for p in vert]) def _update_selection_items(self): """Update the shapes of selection items after a scale change. """ transform = self._transform for item, selection in self._selection.items(): path = transform.map(selection.unscaled_path) ppath = self._create_path(item, path) selection.set_path(ppath) def _height_scale_factor(self): # Internal dendrogram height scale factor. The dendrogram geometry is # scaled by this factor to better condition the geometry if self._root is None: return 1 base = self._root.value.height # implicitly scale the geometry to 0..1 scale or flush to 0 for fuzz if base >= np.finfo(base).eps: return 1 / base else: return 0 def _relayout(self): if self._root is None: return scale = self._height_scale_factor() base = scale * self._root.value.height self._layout = dendrogram_path(self._root, self.orientation, scaleh=scale) for node_geom in postorder(self._layout): node, geom = node_geom.value item = self._items[node] item.element = geom # the untransformed source path item.sourcePath = path_toQtPath(geom) r = item.sourcePath.boundingRect() if self.orientation == Left: r.setRight(base) elif self.orientation == Right: r.setLeft(0) elif self.orientation == Top: r.setBottom(base) else: r.setTop(0) hitarea = QPainterPath() hitarea.addRect(r) item.sourceAreaShape = hitarea item.setGeometryData(item.sourcePath, item.sourceAreaShape) item.setZValue(-node.value.height) def _rescale(self): if self._root is None: return scale = self._height_scale_factor() base = scale * self._root.value.height crect = self.contentsRect() leaf_count = len(list(leaves(self._root))) if self.orientation in [Left, Right]: drect = QSizeF(base, leaf_count) else: drect = QSizeF(leaf_count, base) eps = np.finfo(np.float64).eps if abs(drect.width()) < eps: sx = 1.0 else: sx = crect.width() / drect.width() if abs(drect.height()) < eps: sy = 1.0 else: sy = crect.height() / drect.height() transform = QTransform().scale(sx, sy) self._transform = transform self._itemgroup.setPos(crect.topLeft()) self._itemgroup.setGeometry(crect) for node_geom in postorder(self._layout): node, _ = node_geom.value item = self._items[node] item.setGeometryData(transform.map(item.sourcePath), transform.map(item.sourceAreaShape)) self._selection_items = None self._update_selection_items() def sizeHint(self, which: Qt.SizeHint, constraint=QSizeF()) -> QRectF: # reimplemented fm = QFontMetrics(self.font()) spacing = fm.lineSpacing() mleft, mtop, mright, mbottom = self.getContentsMargins() if self._root and which == Qt.PreferredSize: nleaves = len( [node for node in self._items.keys() if not node.branches]) base = max(10, min(spacing * 16, 250)) if self.orientation in [self.Left, self.Right]: return QSizeF(base, spacing * nleaves + mleft + mright) else: return QSizeF(spacing * nleaves + mtop + mbottom, base) elif which == Qt.MinimumSize: return QSizeF(mleft + mright + 10, mtop + mbottom + 10) else: return QSizeF() def sceneEventFilter(self, obj, event): if isinstance(obj, DendrogramWidget.ClusterGraphicsItem): if event.type() == QEvent.GraphicsSceneHoverEnter and \ self.__hoverHighlightEnabled: self._set_hover_item(obj) event.accept() return True elif event.type() == QEvent.GraphicsSceneMousePress and \ event.button() == Qt.LeftButton: is_selected = self.isItemSelected(obj) is_included = self.is_included(obj) current_selection = list(self._selection) if self.__selectionMode == DendrogramWidget.SingleSelection: if event.modifiers() & Qt.ControlModifier: self.setSelectedItems([obj] if not is_selected else []) elif event.modifiers() & Qt.AltModifier: self.setSelectedItems([]) elif event.modifiers() & Qt.ShiftModifier: if not is_included: self.setSelectedItems([obj]) elif current_selection != [obj]: self.setSelectedItems([obj]) elif self.__selectionMode == DendrogramWidget.ExtendedSelection: if event.modifiers() & Qt.ControlModifier: self.setItemSelected(obj, not is_selected) elif event.modifiers() & Qt.AltModifier: self.setItemSelected(self._selected_super_item(obj), False) elif event.modifiers() & Qt.ShiftModifier: if not is_included: self.setItemSelected(obj, True) elif current_selection != [obj]: self.setSelectedItems([obj]) if current_selection != self._selection: self.selectionEdited.emit() self.itemClicked.emit(obj) event.accept() return True if event.type() == QEvent.GraphicsSceneHoverLeave: self._set_hover_item(None) return super().sceneEventFilter(obj, event) def changeEvent(self, event): # reimplemented super().changeEvent(event) if event.type() == QEvent.FontChange: self.updateGeometry() # QEvent.ContentsRectChange is missing in PyQt4 <= 4.11.3 if event.type() == 178: # QEvent.ContentsRectChange: self._rescale() def resizeEvent(self, event): # reimplemented super().resizeEvent(event) self._rescale() def mousePressEvent(self, event): # reimplemented super().mousePressEvent(event) # A mouse press on an empty widget part if event.modifiers() == Qt.NoModifier and self._selection: self.set_selected_clusters([])
class AsyncUpdateLoop(QObject): """ Run/drive an coroutine from the event loop. This is a utility class which can be used for implementing asynchronous update loops. I.e. coroutines which periodically yield control back to the Qt event loop. """ Next = QEvent.registerEventType() #: State flags Idle, Running, Cancelled, Finished = 0, 1, 2, 3 #: The coroutine has yielded control to the caller (with `object`) yielded = Signal(object) #: The coroutine has finished/exited (either with an exception #: or with a return statement) finished = Signal() #: The coroutine has returned (normal return statement / StopIteration) returned = Signal(object) #: The coroutine has exited with with an exception. raised = Signal(object) #: The coroutine was cancelled/closed. cancelled = Signal() def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self.__coroutine = None self.__next_pending = False # Flag for compressing scheduled events self.__in_next = False self.__state = AsyncUpdateLoop.Idle @Slot(object) def setCoroutine(self, loop): """ Set the coroutine. The coroutine will be resumed (repeatedly) from the event queue. If there is an existing coroutine set it is first closed/cancelled. Raises an RuntimeError if the current coroutine is running. """ if self.__coroutine is not None: self.__coroutine.close() self.__coroutine = None self.__state = AsyncUpdateLoop.Cancelled self.cancelled.emit() self.finished.emit() if loop is not None: self.__coroutine = loop self.__state = AsyncUpdateLoop.Running self.__schedule_next() @Slot() def cancel(self): """ Cancel/close the current coroutine. Raises an RuntimeError if the current coroutine is running. """ self.setCoroutine(None) def state(self): """ Return the current state. """ return self.__state def isRunning(self): return self.__state == AsyncUpdateLoop.Running def __schedule_next(self): if not self.__next_pending: self.__next_pending = True QTimer.singleShot(10, self.__on_timeout) def __next(self): if self.__coroutine is not None: try: rval = next(self.__coroutine) except StopIteration as stop: self.__state = AsyncUpdateLoop.Finished self.returned.emit(stop.value) self.finished.emit() self.__coroutine = None except BaseException as er: self.__state = AsyncUpdateLoop.Finished self.raised.emit(er) self.finished.emit() self.__coroutine = None else: self.yielded.emit(rval) self.__schedule_next() @Slot() def __on_timeout(self): assert self.__next_pending self.__next_pending = False if not self.__in_next: self.__in_next = True try: self.__next() finally: self.__in_next = False else: # warn self.__schedule_next() def customEvent(self, event): if event.type() == AsyncUpdateLoop.Next: self.__on_timeout() else: super().customEvent(event)
class TreeView(QTreeView): Style = f""" QTreeView::branch {{ background: palette(base); }} QTreeView::branch:has-siblings:!adjoins-item {{ border-image: url({resources_path}/vline.png) 0; }} QTreeView::branch:has-siblings:adjoins-item {{ border-image: url({resources_path}/branch-more.png) 0; }} QTreeView::branch:!has-children:!has-siblings:adjoins-item {{ border-image: url({resources_path}/branch-end.png) 0; }} QTreeView::branch:has-children:!has-siblings:closed, QTreeView::branch:closed:has-children:has-siblings {{ border-image: none; image: url({resources_path}/branch-closed.png); }} QTreeView::branch:open:has-children:!has-siblings, QTreeView::branch:open:has-children:has-siblings {{ border-image: none; image: url({resources_path}/branch-open.png); }} """ drop_finished = Signal() def __init__(self, data_changed_cb: Callable): self.__data_changed_cb = data_changed_cb edit_triggers = QTreeView.DoubleClicked | QTreeView.EditKeyPressed super().__init__( editTriggers=int(edit_triggers), selectionMode=QTreeView.ExtendedSelection, dragEnabled=True, acceptDrops=True, defaultDropAction=Qt.MoveAction ) self.setHeaderHidden(True) self.setDropIndicatorShown(True) self.setStyleSheet(self.Style) self.__disconnected = False def startDrag(self, actions: Qt.DropActions): with disconnected(self.model().dataChanged, self.__data_changed_cb): super().startDrag(actions) self.drop_finished.emit() def dragEnterEvent(self, event: QDragEnterEvent): if event.source() != self: self.__disconnected = True self.model().dataChanged.disconnect(self.__data_changed_cb) super().dragEnterEvent(event) def dragLeaveEvent(self, event: QDragLeaveEvent): super().dragLeaveEvent(event) if self.__disconnected: self.__disconnected = False self.model().dataChanged.connect(self.__data_changed_cb) def dropEvent(self, event: QDropEvent): super().dropEvent(event) self.expandAll() if self.__disconnected: self.__disconnected = False self.model().dataChanged.connect(self.__data_changed_cb) self.drop_finished.emit()
class ToolBox(QFrame): """ A tool box widget. """ # Signal emitted when a tab is toggled. tabToggled = Signal(int, bool) __exclusive = False # type: bool def setExclusive(self, exclusive): # type: (bool) -> None """ Set exclusive tabs (only one tab can be open at a time). """ if self.__exclusive != exclusive: self.__exclusive = exclusive self.__tabActionGroup.setExclusive(exclusive) checked = self.__tabActionGroup.checkedAction() if checked is None: # The action group can be out of sync with the actions state # when switching between exclusive states. actions_checked = [ page.action for page in self.__pages if page.action.isChecked() ] if actions_checked: checked = actions_checked[0] # Trigger/toggle remaining open pages if exclusive and checked is not None: for page in self.__pages: if checked != page.action and page.action.isChecked(): page.action.trigger() def exclusive(self): # type: () -> bool """ Are the tabs in the toolbox exclusive. """ return self.__exclusive exclusive_ = Property(bool, fget=exclusive, fset=setExclusive, designable=True, doc="Exclusive tabs") def __init__(self, parent=None, **kwargs): # type: (Optional[QWidget], Any)-> None super().__init__(parent, **kwargs) self.__pages = [] # type: List[_ToolBoxPage] self.__tabButtonHeight = -1 self.__tabIconSize = QSize() self.__exclusive = False layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) # Scroll area for the contents. self.__scrollArea = QScrollArea( self, objectName="toolbox-scroll-area", sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding), verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn, horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff, widgetResizable=True, ) self.__scrollArea.setFrameStyle(QScrollArea.NoFrame) # A widget with all of the contents. # The tabs/contents are placed in the layout inside this widget self.__contents = QWidget(self.__scrollArea, objectName="toolbox-contents") self.__contentsLayout = _ToolBoxLayout( sizeConstraint=_ToolBoxLayout.SetMinAndMaxSize, spacing=0) self.__contentsLayout.setContentsMargins(0, 0, 0, 0) self.__contents.setLayout(self.__contentsLayout) self.__scrollArea.setWidget(self.__contents) layout.addWidget(self.__scrollArea) self.setLayout(layout) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) self.__tabActionGroup = QActionGroup( self, objectName="toolbox-tab-action-group", exclusive=self.__exclusive) self.__actionMapper = QSignalMapper(self) self.__actionMapper.mapped[QObject].connect(self.__onTabActionToggled) def setTabButtonHeight(self, height): # type: (int) -> None """ Set the tab button height. """ if self.__tabButtonHeight != height: self.__tabButtonHeight = height for page in self.__pages: page.button.setFixedHeight(height) def tabButtonHeight(self): # type: () -> int """ Return the tab button height. """ return self.__tabButtonHeight def setTabIconSize(self, size): # type: (QSize) -> None """ Set the tab button icon size. """ if self.__tabIconSize != size: self.__tabIconSize = QSize(size) for page in self.__pages: page.button.setIconSize(size) def tabIconSize(self): # type: () -> QSize """ Return the tab icon size. """ return QSize(self.__tabIconSize) def tabButton(self, index): # type: (int) -> QAbstractButton """ Return the tab button at `index` """ return self.__pages[index].button def tabAction(self, index): # type: (int) -> QAction """ Return open/close action for the tab at `index`. """ return self.__pages[index].action def addItem(self, widget, text, icon=QIcon(), toolTip=""): # type: (QWidget, str, QIcon, str) -> int """ Append the `widget` in a new tab and return its index. Parameters ---------- widget : QWidget A widget to be inserted. The toolbox takes ownership of the widget. text : str Name/title of the new tab. icon : QIcon An icon for the tab button. toolTip : str Tool tip for the tab button. Returns ------- index : int Index of the inserted tab """ return self.insertItem(self.count(), widget, text, icon, toolTip) def insertItem(self, index, widget, text, icon=QIcon(), toolTip=""): # type: (int, QWidget, str, QIcon, str) -> int """ Insert the `widget` in a new tab at position `index`. See also -------- ToolBox.addItem """ button = self.createTabButton(widget, text, icon, toolTip) self.__contentsLayout.insertWidget(index * 2, button) self.__contentsLayout.insertWidget(index * 2 + 1, widget) widget.hide() page = _ToolBoxPage(index, widget, button.defaultAction(), button) self.__pages.insert(index, page) # update the indices __pages list for i in range(index + 1, self.count()): self.__pages[i] = self.__pages[i]._replace(index=i) self.__updatePositions() # Show (open) the first tab. if self.count() == 1 and index == 0: page.action.trigger() self.__updateSelected() self.updateGeometry() return index def removeItem(self, index): # type: (int) -> None """ Remove the widget at `index`. Note ---- The widget is hidden but is is not deleted. It is up to the caller to delete it. """ self.__contentsLayout.takeAt(2 * index + 1) self.__contentsLayout.takeAt(2 * index) page = self.__pages.pop(index) # Update the page indexes for i in range(index, self.count()): self.__pages[i] = self.__pages[i]._replace(index=i) page.button.deleteLater() # Hide the widget and reparent to self # This follows QToolBox.removeItem page.widget.hide() page.widget.setParent(self) self.__updatePositions() self.__updateSelected() self.updateGeometry() def count(self): # type: () -> int """ Return the number of widgets inserted in the toolbox. """ return len(self.__pages) def widget(self, index): # type: (int) -> QWidget """ Return the widget at `index`. """ return self.__pages[index].widget def createTabButton(self, widget, text, icon=QIcon(), toolTip=""): # type: (QWidget, str, QIcon, str) -> QAbstractButton """ Create the tab button for `widget`. """ action = QAction(text, self) action.setCheckable(True) if icon: action.setIcon(icon) if toolTip: action.setToolTip(toolTip) self.__tabActionGroup.addAction(action) self.__actionMapper.setMapping(action, action) action.toggled.connect(self.__actionMapper.map) button = ToolBoxTabButton(self, objectName="toolbox-tab-button") button.setDefaultAction(action) button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) button.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) if self.__tabIconSize.isValid(): button.setIconSize(self.__tabIconSize) if self.__tabButtonHeight > 0: button.setFixedHeight(self.__tabButtonHeight) return button def ensureWidgetVisible(self, child, xmargin=50, ymargin=50): # type: (QWidget, int, int) -> None """ Scroll the contents so child widget instance is visible inside the viewport. """ self.__scrollArea.ensureWidgetVisible(child, xmargin, ymargin) def sizeHint(self): # type: () -> QSize """ Reimplemented. """ hint = self.__contentsLayout.sizeHint() if self.count(): # Compute max width of hidden widgets also. scroll = self.__scrollArea scroll_w = scroll.verticalScrollBar().sizeHint().width() frame_w = self.frameWidth() * 2 + scroll.frameWidth() * 2 max_w = max([p.widget.sizeHint().width() for p in self.__pages]) hint = QSize( max(max_w, hint.width()) + scroll_w + frame_w, hint.height()) return QSize(200, 200).expandedTo(hint) def __onTabActionToggled(self, action): # type: (QAction) -> None page = find(self.__pages, action, key=attrgetter("action")) on = action.isChecked() page.widget.setVisible(on) index = page.index if index > 0: # Update the `previous` tab buttons style hints previous = self.__pages[index - 1].button previous.selected = set_flag(previous.selected, QStyleOptionToolBox.NextIsSelected, on) previous.update() if index < self.count() - 1: next = self.__pages[index + 1].button next.selected = set_flag(next.selected, QStyleOptionToolBox.PreviousIsSelected, on) next.update() self.tabToggled.emit(index, on) self.__contentsLayout.invalidate() def __updateSelected(self): # type: () -> None """Update the tab buttons selected style flags. """ if self.count() == 0: return def update(button, next_sel, prev_sel): # type: (ToolBoxTabButton, bool, bool) -> None button.selected = set_flag(button.selected, QStyleOptionToolBox.NextIsSelected, next_sel) button.selected = set_flag(button.selected, QStyleOptionToolBox.PreviousIsSelected, prev_sel) button.update() if self.count() == 1: update(self.__pages[0].button, False, False) elif self.count() >= 2: pages = self.__pages for i in range(1, self.count() - 1): update(pages[i].button, pages[i + 1].action.isChecked(), pages[i - 1].action.isChecked()) def __updatePositions(self): # type: () -> None """Update the tab buttons position style flags. """ if self.count() == 0: return elif self.count() == 1: self.__pages[0].button.position = QStyleOptionToolBox.OnlyOneTab else: self.__pages[0].button.position = QStyleOptionToolBox.Beginning self.__pages[-1].button.position = QStyleOptionToolBox.End for p in self.__pages[1:-1]: p.button.position = QStyleOptionToolBox.Middle for p in self.__pages: p.button.update()
class CanvasApplication(QApplication): fileOpenRequest = Signal(QUrl) __args = None def __init__(self, argv): fix_qt_plugins_path() if hasattr(Qt, "AA_EnableHighDpiScaling"): # Turn on HighDPI support when available QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) CanvasApplication.__args, argv_ = self.parse_style_arguments(argv) if self.__args.style: argv_ = argv_ + ["-style", self.__args.style] super().__init__(argv_) # Make sure there is an asyncio event loop that runs on the # Qt event loop. _ = get_event_loop() argv[:] = argv_ self.setAttribute(Qt.AA_DontShowIconsInMenus, True) if hasattr(self, "styleHints"): sh = self.styleHints() if hasattr(sh, 'setShowShortcutsInContextMenus'): # PyQt5.13 and up sh.setShowShortcutsInContextMenus(True) self.configureStyle() def event(self, event): if event.type() == QEvent.FileOpen: self.fileOpenRequest.emit(event.url()) elif event.type() == QEvent.PolishRequest: self.configureStyle() return super().event(event) @staticmethod def parse_style_arguments(argv): parser = argparse.ArgumentParser() parser.add_argument("-style", type=str, default=None) parser.add_argument("-colortheme", type=str, default=None) ns, rest = parser.parse_known_args(argv) if ns.style is not None: if ":" in ns.style: ns.style, colortheme = ns.style.split(":", 1) if ns.colortheme is None: ns.colortheme = colortheme return ns, rest @staticmethod def configureStyle(): from orangecanvas import styles args = CanvasApplication.__args settings = QSettings() settings.beginGroup("application-style") name = settings.value("style-name", "", type=str) if args is not None and args.style: # command line params take precedence name = args.style if name != "": inst = QApplication.instance() if inst is not None: if inst.style().objectName().lower() != name.lower(): QApplication.setStyle(name) theme = settings.value("palette", "", type=str) if args is not None and args.colortheme: theme = args.colortheme if theme and theme in styles.colorthemes: palette = styles.colorthemes[theme]() QApplication.setPalette(palette)
class DataTool(QObject): """ A base class for data tools that operate on PaintViewBox. """ #: Tool mouse cursor has changed cursorChanged = Signal(QCursor) #: User started an editing operation. editingStarted = Signal() #: User ended an editing operation. editingFinished = Signal() #: Emits a data transformation command issueCommand = Signal(object) # Makes for a checkable push-button checkable = True # The tool only works if (at least) two dimensions only2d = True def __init__(self, parent, plot): super().__init__(parent) self._cursor = Qt.ArrowCursor self._plot = plot def cursor(self): return QCursor(self._cursor) def setCursor(self, cursor): if self._cursor != cursor: self._cursor = QCursor(cursor) self.cursorChanged.emit() # pylint: disable=unused-argument,no-self-use,unnecessary-pass def mousePressEvent(self, event): return False def mouseMoveEvent(self, event): return False def mouseReleaseEvent(self, event): return False def mouseClickEvent(self, event): return False def mouseDragEvent(self, event): return False def hoverEnterEvent(self, event): return False def hoverLeaveEvent(self, event): return False def mapToPlot(self, point): """Map a point in ViewBox local coordinates into plot coordinates. """ box = self._plot.getViewBox() return box.mapToView(point) def activate(self, ): """Activate the tool""" pass def deactivate(self, ): """Deactivate a tool""" pass
class CorrelationRank(VizRankDialogAttrPair): """ Correlations rank widget. """ threadStopped = Signal() PValRole = next(gui.OrangeUserRole) def __init__(self, *args): super().__init__(*args) self.heuristic = None self.use_heuristic = False self.sel_feature_index = None def initialize(self): super().initialize() data = self.master.cont_data self.attrs = data and data.domain.attributes self.model_proxy.setFilterKeyColumn(-1) self.heuristic = None self.use_heuristic = False if self.master.feature is not None: self.sel_feature_index = data.domain.index(self.master.feature) else: self.sel_feature_index = None if data: # use heuristic if data is too big self.use_heuristic = len(data) * len(self.attrs) ** 2 > SIZE_LIMIT \ and self.sel_feature_index is None if self.use_heuristic: self.heuristic = KMeansCorrelationHeuristic(data) def compute_score(self, state): (attr1, attr2), corr_type = state, self.master.correlation_type data = self.master.cont_data.X corr = pearsonr if corr_type == CorrelationType.PEARSON else spearmanr r, p_value = corr(data[:, attr1], data[:, attr2]) return -abs(r) if not np.isnan(r) else NAN, r, p_value def row_for_state(self, score, state): attrs = sorted((self.attrs[x] for x in state), key=attrgetter("name")) attr_items = [] for attr in attrs: item = QStandardItem(attr.name) item.setData(attrs, self._AttrRole) item.setData(Qt.AlignLeft + Qt.AlignTop, Qt.TextAlignmentRole) item.setToolTip(attr.name) attr_items.append(item) correlation_item = QStandardItem("{:+.3f}".format(score[1])) correlation_item.setData(score[2], self.PValRole) correlation_item.setData(attrs, self._AttrRole) correlation_item.setData( self.NEGATIVE_COLOR if score[1] < 0 else self.POSITIVE_COLOR, gui.TableBarItem.BarColorRole) return [correlation_item] + attr_items def check_preconditions(self): return self.master.cont_data is not None def iterate_states(self, initial_state): if self.sel_feature_index is not None: return self.iterate_states_by_feature() elif self.use_heuristic: return self.heuristic.get_states(initial_state) else: return super().iterate_states(initial_state) def iterate_states_by_feature(self): for j in range(len(self.attrs)): if j != self.sel_feature_index: yield self.sel_feature_index, j def state_count(self): n = len(self.attrs) return n * (n - 1) / 2 if self.sel_feature_index is None else n - 1 @staticmethod def bar_length(score): return abs(score[1]) def stopped(self): self.threadStopped.emit() header = self.rank_table.horizontalHeader() header.setSectionResizeMode(1, QHeaderView.Stretch) def start(self, task, *args, **kwargs): self._set_empty_status() super().start(task, *args, **kwargs) self.__set_state_busy() def cancel(self): super().cancel() self.__set_state_ready() def _connect_signals(self, state): super()._connect_signals(state) state.progress_changed.connect(self.master.progressBarSet) state.status_changed.connect(self.master.setStatusMessage) def _disconnect_signals(self, state): super()._disconnect_signals(state) state.progress_changed.disconnect(self.master.progressBarSet) state.status_changed.disconnect(self.master.setStatusMessage) def _on_task_done(self, future): super()._on_task_done(future) self.__set_state_ready() def __set_state_ready(self): self._set_empty_status() self.master.setBlocking(False) def __set_state_busy(self): self.master.progressBarInit() self.master.setBlocking(True) def _set_empty_status(self): self.master.progressBarFinished() self.master.setStatusMessage("")
class VolcanoGraph(pg.PlotWidget): selectionChanged = Signal() #: Selection Modes NoSelection, SymetricSelection, RectSelection = 0, 1, 2 def __init__(self, parent=None, symbolSize=5, **kwargs): pg.PlotWidget.__init__(self, parent, **kwargs) self.getAxis("bottom").setLabel("log<sub>2</sub> (ratio)") self.getAxis("left").setLabel("-log<sub>10</sub> (P_value)") # Absolute cutoff values for symmetric selection mode. self.cutoffX = 2.0 self.cutoffY = 2.0 # maximum absolute x, y values. self.maxX, self.maxY = 10, 10 self.symbolSize = symbolSize self.__selectionMode = VolcanoGraph.NoSelection self.__selectiondelegate = None self._item = None self._selitem = None self.plotData = numpy.empty((0, 2)) self._stylebrush = numpy.array([ pg.mkBrush((0, 0, 255, 100)), # normal points pg.mkBrush((255, 0, 0, 100)) # selected points ]) def setSelectionMode(self, mode): if mode != self.__selectionMode: viewbox = self.getViewBox() viewbox.setAcceptHoverEvents(True) if self.__selectiondelegate is not None: viewbox.removeEventFilter(self.__selectiondelegate) self.__selectiondelegate.deleteLater() self.__selectiondelegate = None self.__selectionMode = mode if mode == VolcanoGraph.SymetricSelection: self.__selectiondelegate = SymetricSelections( viewbox, x=min(self.maxX, self.cutoffX), y=min(self.maxY, self.cutoffY)) viewbox.installEventFilter(self.__selectiondelegate) elif mode == VolcanoGraph.RectSelection: self.__selectiondelegate = GraphSelections(viewbox) viewbox.installEventFilter(self.__selectiondelegate) else: pass if self.__selectiondelegate is not None: self.__selectiondelegate.selectionGeometryChanged.connect( self.__on_selectionChanged) if self.plotData.size: self.updateSelectionArea() def selectionMode(self): return self.__selectionMode def __on_selectionChanged(self): if self.__selectionMode == VolcanoGraph.SymetricSelection: self.cutoffX, self.cutoffY = self.__selectiondelegate.cuttoff() self.updateSelectionArea() self.selectionChanged.emit() def selectionMask(self): if self.__selectiondelegate is not None: return self.__selectiondelegate.testSelection(self.plotData) else: return numpy.zeros((len(self.plotData), ), dtype=bool) def setPlotData(self, data): self.plotData = numpy.asarray(data) if self.plotData.size: self.maxX = numpy.max(numpy.abs(self.plotData[:, 0])) self.maxY = numpy.max(self.plotData[:, 1]) else: self.maxX, self.maxY = 10, 10 self.replot_() self.selectionChanged.emit() def setSymbolSize(self, size): if size != self.symbolSize: self.symbolSize = size if self._item is not None: self._item.setSize(size) def updateSelectionArea(self): mask = self.selectionMask() brush = self._stylebrush[mask.astype(int)] self._item.setBrush(brush) if self._selitem is not None: self.removeItem(self._selitem) self._selitem = None if self.__selectiondelegate is not None: self._selitem = QGraphicsItemGroup() self._selitem.dataBounds = \ lambda axis, frac=1.0, orthoRange=None: None for p1, p2 in self.__selectiondelegate.selection: r = QRectF(p1, p2).normalized() ritem = QGraphicsRectItem(r, self._selitem) ritem.setBrush(QBrush(Qt.NoBrush)) ritem.setPen(QPen(Qt.red, 0)) self.addItem(self._selitem) def replot_(self): if self.plotData.size: if self._item is not None: self.removeItem(self._item) mask = self.selectionMask() brush = self._stylebrush[mask.astype(int)] self._item = ScatterPlotItem(x=self.plotData[:, 0], y=self.plotData[:, 1], brush=brush, size=self.symbolSize, antialias=True, data=numpy.arange(len(self.plotData))) self.addItem(self._item) self.updateSelectionArea() else: self.removeItem(self._item) self._item = None def clear(self): self._item = None self._selitem = None self.plotData = numpy.empty((0, 2)) super().clear() self.selectionChanged.emit() def sizeHint(self): return QSize(500, 500)
class RadvizVizRank(VizRankDialog, OWComponent): captionTitle = "Score Plots" n_attrs = Setting(3) minK = 10 attrsSelected = Signal([]) _AttrRole = next(gui.OrangeUserRole) percent_data_used = Setting(100) def __init__(self, master): """Add the spin box for maximal number of attributes""" VizRankDialog.__init__(self, master) OWComponent.__init__(self, master) self.master = master self.n_neighbors = 10 box = gui.hBox(self) max_n_attrs = min(MAX_DISPLAYED_VARS, len(master.model_selected)) self.n_attrs_spin = gui.spin(box, self, "n_attrs", 3, max_n_attrs, label="Maximum number of variables: ", controlWidth=50, alignment=Qt.AlignRight, callback=self._n_attrs_changed) gui.rubber(box) self.last_run_n_attrs = None self.attr_color = master.attr_color self.attr_ordering = None self.data = None self.valid_data = None self.rank_table.clicked.connect(self.on_row_clicked) self.rank_table.verticalHeader().sectionClicked.connect( self.on_header_clicked) def initialize(self): super().initialize() self.attr_color = self.master.attr_color def _compute_attr_order(self): """ used by VizRank to evaluate attributes """ master = self.master attrs = [ v for v in master.primitive_variables if v is not self.attr_color ] data = self.master.data.transform( Domain(attributes=attrs, class_vars=self.attr_color)) self.data = data self.valid_data = np.hstack( (~np.isnan(data.X), ~np.isnan(data.Y.reshape(len(data.Y), 1)))) relief = ReliefF if self.attr_color.is_discrete else RReliefF weights = relief(n_iterations=100, k_nearest=self.minK)(data) attrs = sorted(zip(weights, attrs), key=lambda x: (-x[0], x[1].name)) self.attr_ordering = attr_ordering = [a for _, a in attrs] return attr_ordering def _evaluate_projection(self, x, y): """ kNNEvaluate - evaluate class separation in the given projection using a k-NN method Parameters ---------- x - variables to evaluate y - class Returns ------- scores """ if self.percent_data_used != 100: rand = np.random.choice(len(x), int(len(x) * self.percent_data_used / 100), replace=False) x = x[rand] y = y[rand] neigh = KNeighborsClassifier(n_neighbors=3) if self.attr_color.is_discrete else \ KNeighborsRegressor(n_neighbors=3) assert ~(np.isnan(x).any(axis=None) | np.isnan(x).any(axis=None)) neigh.fit(x, y) with warnings.catch_warnings(): warnings.simplefilter("ignore", category=UserWarning) scores = cross_val_score(neigh, x, y, cv=3) return scores.mean() def _n_attrs_changed(self): """ Change the button label when the number of attributes changes. The method does not reset anything so the user can still see the results until actually restarting the search. """ if self.n_attrs != self.last_run_n_attrs or self.saved_state is None: self.button.setText("Start") else: self.button.setText("Continue") self.button.setEnabled(self.check_preconditions()) def progressBarSet(self, value): self.setWindowTitle(self.captionTitle + " Evaluated {} permutations".format(value)) def check_preconditions(self): master = self.master if not super().check_preconditions(): return False elif not master.btn_vizrank.isEnabled(): return False self.n_attrs_spin.setMaximum( min(MAX_DISPLAYED_VARS, len(master.model_selected))) return True def on_selection_changed(self, selected, _): self.on_row_clicked(selected.indexes()[0]) def on_row_clicked(self, index): self.selectionChanged.emit(index.data(self._AttrRole)) def on_header_clicked(self, section): self.on_row_clicked(self.rank_model.index(section, 0)) def iterate_states(self, state): if state is None: # on the first call, compute order self.attrs = self._compute_attr_order() state = list(range(3)) else: state = list(state) def combinations(n, s): while True: yield s for up, _ in enumerate(s): s[up] += 1 if up + 1 == len(s) or s[up] < s[up + 1]: break s[up] = up if s[-1] == n: if len(s) < self.n_attrs: s = list(range(len(s) + 1)) else: break for c in combinations(len(self.attrs), state): for p in islice(permutations(c[1:]), factorial(len(c) - 1) // 2): yield (c[0], ) + p def compute_score(self, state): attrs = [self.attrs[i] for i in state] domain = Domain(attributes=attrs, class_vars=[self.attr_color]) data = self.data.transform(domain) projector = RadViz() projection = projector(data) radviz_xy = projection(data).X y = projector.preprocess(data).Y return -self._evaluate_projection(radviz_xy, y) def bar_length(self, score): return -score def row_for_state(self, score, state): attrs = [self.attrs[s] for s in state] item = QStandardItem("[{:0.6f}] ".format(-score) + ", ".join(a.name for a in attrs)) item.setData(attrs, self._AttrRole) return [item] def _update_progress(self): self.progressBarSet(int(self.saved_progress)) def before_running(self): """ Disable the spin for number of attributes before running and enable afterwards. Also, if the number of attributes is different than in the last run, reset the saved state (if it was paused). """ if self.n_attrs != self.last_run_n_attrs: self.saved_state = None self.saved_progress = 0 if self.saved_state is None: self.scores = [] self.rank_model.clear() self.last_run_n_attrs = self.n_attrs self.n_attrs_spin.setDisabled(True) def stopped(self): self.n_attrs_spin.setDisabled(False)
class GraphSelections(QObject): """ Selection manager using a union of rectangle areas. """ selectionStarted = Signal() selectionGeometryChanged = Signal() selectionFinished = Signal() def __init__(self, parent): QObject.__init__(self, parent) self.selection = [] def mapToView(self, pos): """ Map the `pos` in viewbox local into the view (plot) coordinates. """ viewbox = self.parent() return viewbox.mapToView(pos) def start(self, event): pos = self.mapToView(event.pos()) if event.modifiers() & Qt.ControlModifier: self.selection.append((pos, pos)) else: self.selection = [(pos, pos)] self.selectionStarted.emit() self.selectionGeometryChanged.emit() def update(self, event): pos = self.mapToView(event.pos()) self.selection[-1] = self.selection[-1][:-1] + (pos, ) self.selectionGeometryChanged.emit() def end(self, event): self.update(event) self.selectionFinished.emit() def testSelection(self, data): """ Given a Parameters ---------- data : (N, 2) array Point coordinates """ if len(data) == 0: return numpy.zeros(0, dtype=bool) def contained(a, left, top, right, bottom): assert left <= right and bottom <= top x, y = a.T return (x >= left) & (x <= right) & (y <= top) & (y >= bottom) data = numpy.asarray(data) selected = numpy.zeros(len(data), dtype=bool) for p1, p2 in self.selection: r = QRectF(p1, p2).normalized() # Note the inverted top/bottom (Qt coordinate system) selected |= contained(data, r.left(), r.bottom(), r.right(), r.top()) return selected def mousePressEvent(self, event): """ Handle the intercepted mouse event. Return True if the event has been handled, False otherwise (let the viewbox handle the event). """ if event.button() == Qt.LeftButton: self.start(event) event.accept() return True else: return False def mouseMoveEvent(self, event): """ Handle the intercepted mouse event. Return True if the event has been handled, False otherwise (let the viewbox handle the event). """ if event.buttons() & Qt.LeftButton: self.update(event) event.accept() return True else: return False def mouseReleaseEvent(self, event): """ Handle the intercepted mouse event. Return True if the event has been handled, False otherwise (let the viewbox handle the event). """ if event.button() == Qt.LeftButton: self.update(event) self.end(event) event.accept() return True else: return False def eventFilter(self, obj, event): if obj is self.parent(): if event.type() == QEvent.GraphicsSceneMousePress: return self.mousePressEvent(event) elif event.type() == QEvent.GraphicsSceneMouseMove: return self.mouseMoveEvent(event) elif event.type() == QEvent.GraphicsSceneMouseRelease: return self.mouseReleaseEvent(event) return QObject.eventFilter(self, obj, event)
class CorrelationRank(VizRankDialogAttrPair): """ Correlations rank widget. """ threadStopped = Signal() PValRole = next(gui.OrangeUserRole) def __init__(self, *args): super().__init__(*args) self.heuristic = None self.use_heuristic = False self.sel_feature_index = None def initialize(self): super().initialize() data = self.master.cont_data self.attrs = data and data.domain.attributes self.model_proxy.setFilterKeyColumn(-1) self.heuristic = None self.use_heuristic = False if self.master.feature is not None: self.sel_feature_index = data.domain.index(self.master.feature) else: self.sel_feature_index = None if data: # use heuristic if data is too big n_attrs = len(self.attrs) use_heuristic = n_attrs > KMeansCorrelationHeuristic.n_clusters self.use_heuristic = use_heuristic and \ len(data) * n_attrs ** 2 > SIZE_LIMIT and \ self.sel_feature_index is None if self.use_heuristic: self.heuristic = KMeansCorrelationHeuristic(data) def compute_score(self, state): (attr1, attr2), corr_type = state, self.master.correlation_type data = self.master.cont_data.X corr = pearsonr if corr_type == CorrelationType.PEARSON else spearmanr r, p_value = corr(data[:, attr1], data[:, attr2]) return -abs(r) if not np.isnan(r) else NAN, r, p_value def row_for_state(self, score, state): attrs = sorted((self.attrs[x] for x in state), key=attrgetter("name")) attr_items = [] for attr in attrs: item = QStandardItem(attr.name) item.setData(attrs, self._AttrRole) item.setData(Qt.AlignLeft + Qt.AlignTop, Qt.TextAlignmentRole) item.setToolTip(attr.name) attr_items.append(item) correlation_item = QStandardItem("{:+.3f}".format(score[1])) correlation_item.setData(score[2], self.PValRole) correlation_item.setData(attrs, self._AttrRole) correlation_item.setData( self.NEGATIVE_COLOR if score[1] < 0 else self.POSITIVE_COLOR, gui.TableBarItem.BarColorRole) return [correlation_item] + attr_items def check_preconditions(self): return self.master.cont_data is not None def iterate_states(self, initial_state): if self.sel_feature_index is not None: return self.iterate_states_by_feature() elif self.use_heuristic: return self.heuristic.get_states(initial_state) else: return super().iterate_states(initial_state) def iterate_states_by_feature(self): for j in range(len(self.attrs)): if j != self.sel_feature_index: yield self.sel_feature_index, j def state_count(self): if self.sel_feature_index is not None: return len(self.attrs) - 1 elif self.use_heuristic: n_clusters = KMeansCorrelationHeuristic.n_clusters n_avg_attrs = len(self.attrs) / n_clusters return n_clusters * n_avg_attrs * (n_avg_attrs - 1) / 2 else: n_attrs = len(self.attrs) return n_attrs * (n_attrs - 1) / 2 @staticmethod def bar_length(score): return abs(score[1]) def stopped(self): self.threadStopped.emit() header = self.rank_table.horizontalHeader() header.setSectionResizeMode(1, QHeaderView.Stretch)
class TaskState(QObject): status_changed = Signal(str) _p_status_changed = Signal(str) progress_changed = Signal(float) _p_progress_changed = Signal(float) partial_result_ready = Signal(object) _p_partial_result_ready = Signal(object) def __init__(self, *args): super().__init__(*args) self.__future = None self.watcher = FutureWatcher() self.__interuption_requested = False self.__progress = 0 # Helpers to route the signal emits via a this object's queue. # This ensures 'atomic' disconnect from signals for targets/slots # in the same thread. Requires that the event loop is running in this # object's thread. self._p_status_changed.connect(self.status_changed, Qt.QueuedConnection) self._p_progress_changed.connect(self.progress_changed, Qt.QueuedConnection) self._p_partial_result_ready.connect(self.partial_result_ready, Qt.QueuedConnection) @property def future(self): # type: () -> Future return self.__future def set_status(self, text): self._p_status_changed.emit(text) def set_progress_value(self, value): if round(value, 1) > round(self.__progress, 1): # Only emit progress when it has changed sufficiently self._p_progress_changed.emit(value) self.__progress = value def set_partial_results(self, value): self._p_partial_result_ready.emit(value) def is_interuption_requested(self): return self.__interuption_requested def start(self, executor, func=None): # type: (futures.Executor, Callable[[], Any]) -> Future assert self.future is None assert not self.__interuption_requested self.__future = executor.submit(func) self.watcher.setFuture(self.future) return self.future def cancel(self): assert not self.__interuption_requested self.__interuption_requested = True if self.future is not None: rval = self.future.cancel() else: # not even scheduled yet rval = True return rval
class ChoroplethItem(pg.GraphicsObject): """ GraphicsObject that represents regions. Regions can consist of multiple disjoint polygons. """ itemClicked = Signal(str) # send region id def __init__(self, region: _ChoroplethRegion, pen: QPen, brush: QBrush): pg.GraphicsObject.__init__(self) self.region = region self.agg_value = None self.pen = pen self.brush = brush self._region_info = self._get_region_info(self.region) self._bounding_rect = reduce( lambda br1, br2: br1.united(br2), (qpoly.boundingRect() for qpoly in self.region.qpolys) ) @staticmethod def _get_region_info(region: _ChoroplethRegion): region_text = "<br/>".join(escape('{} = {}'.format(k, v)) for k, v in region.info.items()) return "<b>Region info:</b><br/>" + region_text def tooltip(self): return f"<b>Agg. value = {self.agg_value}</b><hr/>{self._region_info}" def setPen(self, pen): self.pen = pen self.update() def setBrush(self, brush): self.brush = brush self.update() def paint(self, p: QPainter, *args): p.setBrush(self.brush) p.setPen(self.pen) for qpoly in self.region.qpolys: p.drawPolygon(qpoly) def boundingRect(self) -> QRectF: return self._bounding_rect def contains(self, point: QPointF) -> bool: return any(qpoly.containsPoint(point, Qt.OddEvenFill) for qpoly in self.region.qpolys) def intersects(self, poly: QPolygonF) -> bool: return any(not qpoly.intersected(poly).isEmpty() for qpoly in self.region.qpolys) def mouseClickEvent(self, ev): if ev.button() == Qt.LeftButton and self.contains(ev.pos()): self.itemClicked.emit(self.region.id) ev.accept() else: ev.ignore()
class ViolinPlot(pg.PlotItem): """ A violin plot item with interactive data boundary selection. """ #: Emitted when the selection boundary has changed selectionChanged = Signal() #: Emitted when the selection boundary has been edited by the user #: (by dragging the boundary lines) selectionEdited = Signal() #: Selection Flags NoSelection, Low, High = 0, 1, 2 def __init__(self, *args, enableMenu=False, axisItems=None, **kwargs): if axisItems is None: axisItems = {} for position in ("left", 'right', 'top', 'bottom'): axisItems.setdefault(position, AxisItem(position)) super().__init__(*args, enableMenu=enableMenu, axisItems=axisItems, **kwargs) self.__data = None #: min/max cutoff line positions self.__min = 0 self.__max = 0 self.__dataPointsVisible = True self.__selectionEnabled = True self.__selectionMode = ViolinPlot.High | ViolinPlot.Low self._plotitems = None def setData(self, data, nsamples, sample_range=None, color=Qt.magenta): assert np.all(np.isfinite(data)) if data.size > 0: xmin, xmax = np.min(data), np.max(data) else: xmin = xmax = 0.0 if sample_range is None: xrange = xmax - xmin sample_min = xmin - xrange * 0.025 sample_max = xmax + xrange * 0.025 else: sample_min, sample_max = sample_range sample = np.linspace(sample_min, sample_max, nsamples) if data.size < 2: est = np.full(sample.size, 1. / sample.size, ) else: try: density = stats.gaussian_kde(data) est = density.evaluate(sample) except np.linalg.LinAlgError: est = np.zeros(sample.size) item = QGraphicsPathItem(violin_shape(sample, est)) color = QColor(color) color.setAlphaF(0.5) item.setBrush(QBrush(color)) pen = QPen(self.palette().color(QPalette.Shadow)) pen.setCosmetic(True) item.setPen(pen) est_max = np.max(est) x = np.random.RandomState(0xD06F00D).uniform( -est_max, est_max, size=data.size ) dots = ScatterPlotItem( x=x, y=data, size=3, ) dots.setVisible(self.__dataPointsVisible) pen = QPen(self.palette().color(QPalette.Shadow), 1) hoverPen = QPen(self.palette().color(QPalette.Highlight), 1.5) cmax = SelectionLine( angle=0, pos=xmax, movable=True, bounds=(sample_min, sample_max), pen=pen, hoverPen=hoverPen ) cmin = SelectionLine( angle=0, pos=xmin, movable=True, bounds=(sample_min, sample_max), pen=pen, hoverPen=hoverPen ) cmax.setCursor(Qt.SizeVerCursor) cmin.setCursor(Qt.SizeVerCursor) selection_item = QGraphicsRectItem( QRectF(-est_max, xmin, est_max * 2, xmax - xmin) ) selection_item.setPen(QPen(Qt.NoPen)) selection_item.setBrush(QColor(0, 250, 0, 50)) def update_selection_rect(): mode = self.__selectionMode p = selection_item.parentItem() # type: Optional[QGraphicsItem] while p is not None and not isinstance(p, pg.ViewBox): p = p.parentItem() if p is not None: viewbox = p # type: pg.ViewBox else: viewbox = None rect = selection_item.rect() # type: QRectF if mode & ViolinPlot.High: rect.setTop(cmax.value()) elif viewbox is not None: rect.setTop(viewbox.viewRect().bottom()) else: rect.setTop(cmax.maxRange[1]) if mode & ViolinPlot.Low: rect.setBottom(cmin.value()) elif viewbox is not None: rect.setBottom(viewbox.viewRect().top()) else: rect.setBottom(cmin.maxRange[0]) selection_item.setRect(rect.normalized()) cmax.sigPositionChanged.connect(update_selection_rect) cmin.sigPositionChanged.connect(update_selection_rect) cmax.visibleChanged.connect(update_selection_rect) cmin.visibleChanged.connect(update_selection_rect) def setupper(line): ebound = self.__effectiveBoundary() elower, eupper = ebound mode = self.__selectionMode if not mode & ViolinPlot.High: return upper = line.value() lower = min(elower, upper) if lower != elower and mode & ViolinPlot.Low: self.__min = lower cmin.setValue(lower) if upper != eupper: self.__max = upper if ebound != self.__effectiveBoundary(): self.selectionEdited.emit() self.selectionChanged.emit() def setlower(line): ebound = self.__effectiveBoundary() elower, eupper = ebound mode = self.__selectionMode if not mode & ViolinPlot.Low: return lower = line.value() upper = max(eupper, lower) if upper != eupper and mode & ViolinPlot.High: self.__max = upper cmax.setValue(upper) if lower != elower: self.__min = lower if ebound != self.__effectiveBoundary(): self.selectionEdited.emit() self.selectionChanged.emit() cmax.sigPositionChanged.connect(setupper) cmin.sigPositionChanged.connect(setlower) selmode = self.__selectionMode cmax.setVisible(selmode & ViolinPlot.High) cmin.setVisible(selmode & ViolinPlot.Low) selection_item.setVisible(selmode) self.addItem(dots) self.addItem(item) self.addItem(cmax) self.addItem(cmin) self.addItem(selection_item) self.setRange( QRectF(-est_max, np.min(sample), est_max * 2, np.ptp(sample)) ) self._plotitems = SimpleNamespace( pointsitem=dots, densityitem=item, cmax=cmax, cmin=cmin, selection_item=selection_item ) self.__min = xmin self.__max = xmax def setDataPointsVisible(self, visible): self.__dataPointsVisible = visible if self._plotitems is not None: self._plotitems.pointsitem.setVisible(visible) def setSelectionMode(self, mode): oldlower, oldupper = self.__effectiveBoundary() oldmode = self.__selectionMode mode = mode & 0b11 if self.__selectionMode == mode: return self.__selectionMode = mode if self._plotitems is None: return cmin = self._plotitems.cmin cmax = self._plotitems.cmax selitem = self._plotitems.selection_item cmin.setVisible(mode & ViolinPlot.Low) cmax.setVisible(mode & ViolinPlot.High) selitem.setVisible(bool(mode)) lower, upper = self.__effectiveBoundary() # The recorded values are not bounded by each other on gui interactions # when one is disabled. Rectify this now. if (oldmode ^ mode) & ViolinPlot.Low and mode & ViolinPlot.High: # Lower activated and High enabled lower = min(lower, upper) if (oldmode ^ mode) & ViolinPlot.High and mode & ViolinPlot.Low: # High activated and Low enabled upper = max(lower, upper) with block_signals(self): if lower != oldlower and mode & ViolinPlot.Low: cmin.setValue(lower) if upper != oldupper and mode & ViolinPlot.High: cmax.setValue(upper) self.selectionChanged.emit() def setBoundary(self, low, high): """ Set the lower and upper selection boundary value. """ changed = 0 mode = self.__selectionMode if self.__min != low: self.__min = low changed |= mode & ViolinPlot.Low if self.__max != high: self.__max = high changed |= mode & ViolinPlot.High if changed: if self._plotitems: with block_signals(self): if changed & ViolinPlot.Low: self._plotitems.cmin.setValue(low) if changed & ViolinPlot.High: self._plotitems.cmax.setValue(high) self.selectionChanged.emit() def boundary(self): """ Return the current lower and upper selection boundary values. """ return self.__min, self.__max def __effectiveBoundary(self): # effective boundary, masked by selection mode low, high = -np.inf, np.inf if self.__selectionMode & ViolinPlot.Low: low = self.__min if self.__selectionMode & ViolinPlot.High: high = self.__max return low, high def clear(self): super().clear() self._plotitems = None def mouseDragEvent(self, event): mode = self.__selectionMode if mode != ViolinPlot.NoSelection and event.buttons() & Qt.LeftButton: start = event.buttonDownScenePos(Qt.LeftButton) # type: QPointF pos = event.scenePos() # type: QPointF cmin, cmax = self._plotitems.cmin, self._plotitems.cmax assert cmin.parentItem() is cmax.parentItem() pos = self.mapToItem(cmin.parentItem(), pos) start = self.mapToItem(cmin.parentItem(), start) if mode & ViolinPlot.Low and mode & ViolinPlot.High: lower, upper = min(pos.y(), start.y()), max(pos.y(), start.y()) cmin.setValue(lower) cmax.setValue(upper) elif mode & ViolinPlot.Low: lower = pos.y() cmin.setValue(lower) elif mode & ViolinPlot.High: upper = pos.y() cmax.setValue(upper) event.accept()
class AddonManagerWidget(QWidget): statechanged = Signal() def __init__(self, parent=None, **kwargs): super(AddonManagerWidget, self).__init__(parent, **kwargs) self.__items = [] self.setLayout(QVBoxLayout()) self.__header = QLabel(wordWrap=True, textFormat=Qt.RichText) self.__search = QLineEdit(placeholderText=self.tr("Filter")) self.layout().addWidget(self.__search) self.__view = view = QTreeView(rootIsDecorated=False, editTriggers=QTreeView.NoEditTriggers, selectionMode=QTreeView.SingleSelection, alternatingRowColors=True) self.__view.setItemDelegateForColumn(0, TristateCheckItemDelegate()) self.layout().addWidget(view) self.__model = model = QStandardItemModel() model.setHorizontalHeaderLabels(["", "Name", "Version", "Action"]) model.dataChanged.connect(self.__data_changed) proxy = QSortFilterProxyModel(filterKeyColumn=1, filterCaseSensitivity=Qt.CaseInsensitive) proxy.setSourceModel(model) self.__search.textChanged.connect(proxy.setFilterFixedString) view.setModel(proxy) view.selectionModel().selectionChanged.connect(self.__update_details) header = self.__view.header() header.setSectionResizeMode(0, QHeaderView.Fixed) header.setSectionResizeMode(2, QHeaderView.ResizeToContents) self.__details = QTextBrowser( frameShape=QTextBrowser.NoFrame, readOnly=True, lineWrapMode=QTextBrowser.WidgetWidth, openExternalLinks=True, ) self.__details.setWordWrapMode(QTextOption.WordWrap) palette = QPalette(self.palette()) palette.setColor(QPalette.Base, Qt.transparent) self.__details.setPalette(palette) self.layout().addWidget(self.__details) def set_items(self, items): self.__items = items model = self.__model model.clear() model.setHorizontalHeaderLabels(["", "Name", "Version", "Action"]) for item in items: if isinstance(item, Installed): installed = True ins, dist = item name = dist.project_name summary = get_dist_meta(dist).get("Summary", "") version = ins.version if ins is not None else dist.version else: installed = False (ins, ) = item dist = None name = ins.name summary = ins.summary version = ins.version updatable = is_updatable(item) item1 = QStandardItem() item1.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | (Qt.ItemIsTristate if updatable else 0)) if installed and updatable: item1.setCheckState(Qt.PartiallyChecked) elif installed: item1.setCheckState(Qt.Checked) else: item1.setCheckState(Qt.Unchecked) item2 = QStandardItem(cleanup(name)) item2.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) item2.setToolTip(summary) item2.setData(item, Qt.UserRole) if updatable: version = "{} < {}".format(dist.version, ins.version) item3 = QStandardItem(version) item3.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) item4 = QStandardItem() item4.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) model.appendRow([item1, item2, item3, item4]) self.__view.resizeColumnToContents(0) self.__view.setColumnWidth(1, max(150, self.__view.sizeHintForColumn(1))) self.__view.setColumnWidth(2, max(150, self.__view.sizeHintForColumn(2))) if self.__items: self.__view.selectionModel().select( self.__view.model().index(0, 0), QItemSelectionModel.Select | QItemSelectionModel.Rows) def item_state(self): steps = [] for i, item in enumerate(self.__items): modelitem = self.__model.item(i, 0) state = modelitem.checkState() if modelitem.flags() & Qt.ItemIsTristate and state == Qt.Checked: steps.append((Upgrade, item)) elif isinstance(item, Available) and state == Qt.Checked: steps.append((Install, item)) elif isinstance(item, Installed) and state == Qt.Unchecked: steps.append((Uninstall, item)) return steps def __selected_row(self): indices = self.__view.selectedIndexes() if indices: proxy = self.__view.model() indices = [proxy.mapToSource(index) for index in indices] return indices[0].row() else: return -1 def set_install_projects(self, names): """Mark for installation the add-ons that match any of names""" model = self.__model for row in range(model.rowCount()): item = model.item(row, 1) if item.text() in names: model.item(row, 0).setCheckState(Qt.Checked) def __data_changed(self, topleft, bottomright): rows = range(topleft.row(), bottomright.row() + 1) for i in rows: modelitem = self.__model.item(i, 0) actionitem = self.__model.item(i, 3) item = self.__items[i] state = modelitem.checkState() flags = modelitem.flags() if flags & Qt.ItemIsTristate and state == Qt.Checked: actionitem.setText("Update") elif isinstance(item, Available) and state == Qt.Checked: actionitem.setText("Install") elif isinstance(item, Installed) and state == Qt.Unchecked: actionitem.setText("Uninstall") else: actionitem.setText("") self.statechanged.emit() def __update_details(self): index = self.__selected_row() if index == -1: self.__details.setText("") else: item = self.__model.item(index, 1) item = item.data(Qt.UserRole) assert isinstance(item, (Installed, Available)) # if isinstance(item, Available): # self.__installed_label.setText("") # self.__available_label.setText(str(item.available.version)) # elif item.installable is not None: # self.__installed_label.setText(str(item.local.version)) # self.__available_label.setText(str(item.available.version)) # else: # self.__installed_label.setText(str(item.local.version)) # self.__available_label.setText("") text = self._detailed_text(item) self.__details.setText(text) def _detailed_text(self, item): if isinstance(item, Installed): remote, dist = item if remote is None: meta = get_dist_meta(dist) description = meta.get("Description") or meta.get('Summary') else: description = remote.description else: description = item[0].description if docutils is not None: try: html = docutils.core.publish_string( trim(description), writer_name="html", settings_overrides={ "output-encoding": "utf-8", # "embed-stylesheet": False, # "stylesheet": [], # "stylesheet_path": [] }).decode("utf-8") except docutils.utils.SystemMessage: html = "<pre>{}<pre>".format(escape(description)) except Exception: html = "<pre>{}<pre>".format(escape(description)) else: html = "<pre>{}<pre>".format(escape(description)) return html def sizeHint(self): return QSize(480, 420)
class LinearProjectionVizRank(VizRankDialog, OWComponent): captionTitle = "Score Plots" n_attrs = Setting(3) minK = 10 attrsSelected = Signal([]) _AttrRole = next(gui.OrangeUserRole) def __init__(self, master): # Add the spin box for a number of attributes to take into account. VizRankDialog.__init__(self, master) OWComponent.__init__(self, master) box = gui.hBox(self) max_n_attrs = len(master.model_selected) self.n_attrs_spin = gui.spin(box, self, "n_attrs", 3, max_n_attrs, label="Number of variables: ", controlWidth=50, alignment=Qt.AlignRight, callback=self._n_attrs_changed) gui.rubber(box) self.last_run_n_attrs = None self.attr_color = master.attr_color self.attrs = [] def initialize(self): super().initialize() self.attr_color = self.master.attr_color def before_running(self): """ Disable the spin for number of attributes before running and enable afterwards. Also, if the number of attributes is different than in the last run, reset the saved state (if it was paused). """ if self.n_attrs != self.last_run_n_attrs: self.saved_state = None self.saved_progress = 0 if self.saved_state is None: self.scores = [] self.rank_model.clear() self.last_run_n_attrs = self.n_attrs self.n_attrs_spin.setDisabled(True) def stopped(self): self.n_attrs_spin.setDisabled(False) def check_preconditions(self): master = self.master if not super().check_preconditions(): return False elif not master.btn_vizrank.isEnabled(): return False n_cont_var = len([ v for v in master.continuous_variables if v is not master.attr_color ]) self.n_attrs_spin.setMaximum(n_cont_var) return True def state_count(self): n_all_attrs = len(self.attrs) if not n_all_attrs: return 0 n_attrs = self.n_attrs return factorial(n_all_attrs) // ( 2 * factorial(n_all_attrs - n_attrs) * n_attrs) def iterate_states(self, state): if state is None: # on the first call, compute order self.attrs = self._score_heuristic() state = list(range(self.n_attrs)) else: state = list(state) def combinations(n, s): while True: yield s for up, _ in enumerate(s): s[up] += 1 if up + 1 == len(s) or s[up] < s[up + 1]: break s[up] = up if s[-1] == n: break for c in combinations(len(self.attrs), state): for p in islice(permutations(c[1:]), factorial(len(c) - 1) // 2): yield (c[0], ) + p def compute_score(self, state): master = self.master data = master.data domain = Domain([self.attrs[i] for i in state], data.domain.class_vars) projection = master.projector(data.transform(domain)) ec = projection(data).X y = column_data(data, self.attr_color, dtype=float) if ec.shape[0] < self.minK: return None n_neighbors = min(self.minK, len(ec) - 1) knn = NearestNeighbors(n_neighbors=n_neighbors).fit(ec) ind = knn.kneighbors(return_distance=False) # pylint: disable=invalid-unary-operand-type if self.attr_color.is_discrete: return -np.sum(y[ind] == y.reshape(-1, 1)) / n_neighbors / len(y) return -r2_score(y, np.mean(y[ind], axis=1)) * (len(y) / len(data)) def bar_length(self, score): return max(0, -score) def _score_heuristic(self): def normalized(a): span = np.max(a, axis=0) - np.min(a, axis=0) span[span == 0] = 1 return (a - np.mean(a, axis=0)) / span domain = self.master.data.domain attr_color = self.master.attr_color domain = Domain(attributes=[ v for v in chain(domain.variables, domain.metas) if v.is_continuous and v is not attr_color ], class_vars=attr_color) data = self.master.data.transform(domain) data.X = normalized(data.X) relief = ReliefF if attr_color.is_discrete else RReliefF weights = relief(n_iterations=100, k_nearest=self.minK)(data) results = sorted(zip(weights, domain.attributes), key=lambda x: (-x[0], x[1].name)) return [attr for _, attr in results] def row_for_state(self, score, state): attrs = [self.attrs[i] for i in state] item = QStandardItem(", ".join(a.name for a in attrs)) item.setData(attrs, self._AttrRole) return [item] def on_selection_changed(self, selected, deselected): if not selected.indexes(): return attrs = selected.indexes()[0].data(self._AttrRole) self.selectionChanged.emit([attrs]) def _n_attrs_changed(self): if self.n_attrs != self.last_run_n_attrs or self.saved_state is None: self.button.setText("Start") else: self.button.setText("Continue") self.button.setEnabled(self.check_preconditions())
class progress(QObject): advance = Signal()
class LinksEditWidget(QGraphicsWidget): """ A Graphics Widget for editing the links between two nodes. """ def __init__(self, *args, **kwargs): QGraphicsWidget.__init__(self, *args, **kwargs) self.setAcceptedMouseButtons(Qt.LeftButton | Qt.RightButton) self.source = None self.sink = None # QGraphicsWidget/Items in the scene. self.sourceNodeWidget = None self.sourceNodeTitle = None self.sinkNodeWidget = None self.sinkNodeTitle = None self.__links = [] self.__textItems = [] self.__iconItems = [] self.__tmpLine = None self.__dragStartItem = None self.setLayout(QGraphicsLinearLayout(Qt.Vertical)) self.layout().setContentsMargins(0, 0, 0, 0) def removeItems(self, items): """ Remove child items from the widget and scene. """ scene = self.scene() for item in items: item.setParentItem(None) if scene is not None: scene.removeItem(item) def clear(self): """ Clear the editor state (source and sink nodes, channels ...). """ if self.layout().count(): widget = self.layout().takeAt(0).graphicsItem() self.removeItems([widget]) self.source = None self.sink = None def setNodes(self, source, sink): """ Set the source/sink nodes (:class:`SchemeNode` instances) between which to edit the links. .. note:: Call this before :func:`setLinks`. """ self.clear() self.source = source self.sink = sink self.__updateState() def setLinks(self, links): """ Set a list of links to display between the source and sink nodes. `links` must be a list of (`OutputSignal`, `InputSignal`) tuples where the first element refers to the source node and the second to the sink node (as set by `setNodes`). """ self.clearLinks() for output, input in links: self.addLink(output, input) def links(self): """ Return the links between the source and sink node. """ return [(link.output, link.input) for link in self.__links] def mousePressEvent(self, event): if event.button() == Qt.LeftButton: startItem = find_item_at(self.scene(), event.pos(), type=ChannelAnchor) if startItem is not None: # Start a connection line drag. self.__dragStartItem = startItem self.__tmpLine = None event.accept() return lineItem = find_item_at(self.scene(), event.scenePos(), type=QGraphicsLineItem) if lineItem is not None: # Remove a connection under the mouse for link in self.__links: if link.lineItem == lineItem: self.removeLink(link.output, link.input) event.accept() return QGraphicsWidget.mousePressEvent(self, event) def mouseMoveEvent(self, event): if event.buttons() & Qt.LeftButton: downPos = event.buttonDownPos(Qt.LeftButton) if not self.__tmpLine and self.__dragStartItem and \ (downPos - event.pos()).manhattanLength() > \ QApplication.instance().startDragDistance(): # Start a line drag line = QGraphicsLineItem(self) start = self.__dragStartItem.boundingRect().center() start = self.mapFromItem(self.__dragStartItem, start) line.setLine(start.x(), start.y(), event.pos().x(), event.pos().y()) pen = QPen(self.palette().color(QPalette.Foreground), 4) pen.setCapStyle(Qt.RoundCap) line.setPen(pen) line.show() self.__tmpLine = line if self.__tmpLine: # Update the temp line line = self.__tmpLine.line() line.setP2(event.pos()) self.__tmpLine.setLine(line) QGraphicsWidget.mouseMoveEvent(self, event) def mouseReleaseEvent(self, event): if event.button() == Qt.LeftButton and self.__tmpLine: endItem = find_item_at(self.scene(), event.scenePos(), type=ChannelAnchor) if endItem is not None: startItem = self.__dragStartItem startChannel = startItem.channel() endChannel = endItem.channel() possible = False # Make sure the drag was from input to output (or reversed) and # not between input -> input or output -> output if type(startChannel) != type(endChannel): if isinstance(startChannel, InputSignal): startChannel, endChannel = endChannel, startChannel possible = compatible_channels(startChannel, endChannel) if possible: self.addLink(startChannel, endChannel) self.scene().removeItem(self.__tmpLine) self.__tmpLine = None self.__dragStartItem = None QGraphicsWidget.mouseReleaseEvent(self, event) def addLink(self, output, input): """ Add a link between `output` (:class:`OutputSignal`) and `input` (:class:`InputSignal`). """ if not compatible_channels(output, input): return if output not in self.source.output_channels(): raise ValueError("%r is not an output channel of %r" % \ (output, self.source)) if input not in self.sink.input_channels(): raise ValueError("%r is not an input channel of %r" % \ (input, self.sink)) if input.single: # Remove existing link if it exists. for s1, s2, _ in self.__links: if s2 == input: self.removeLink(s1, s2) line = QGraphicsLineItem(self) source_anchor = self.sourceNodeWidget.anchor(output) sink_anchor = self.sinkNodeWidget.anchor(input) source_pos = source_anchor.boundingRect().center() source_pos = self.mapFromItem(source_anchor, source_pos) sink_pos = sink_anchor.boundingRect().center() sink_pos = self.mapFromItem(sink_anchor, sink_pos) line.setLine(source_pos.x(), source_pos.y(), sink_pos.x(), sink_pos.y()) pen = QPen(self.palette().color(QPalette.Foreground), 4) pen.setCapStyle(Qt.RoundCap) line.setPen(pen) self.__links.append(_Link(output, input, line)) def removeLink(self, output, input): """ Remove a link between the `output` and `input` channels. """ for link in list(self.__links): if link.output == output and link.input == input: self.scene().removeItem(link.lineItem) self.__links.remove(link) break else: raise ValueError("No such link {0.name!r} -> {1.name!r}." \ .format(output, input)) def clearLinks(self): """ Clear (remove) all the links. """ for output, input, _ in list(self.__links): self.removeLink(output, input) def __updateState(self): """ Update the widget with the new source/sink node signal descriptions. """ widget = QGraphicsWidget() widget.setLayout(QGraphicsGridLayout()) # Space between left and right anchors widget.layout().setHorizontalSpacing(50) left_node = EditLinksNode(self, direction=Qt.LeftToRight, node=self.source) left_node.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) right_node = EditLinksNode(self, direction=Qt.RightToLeft, node=self.sink) right_node.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) left_node.setMinimumWidth(150) right_node.setMinimumWidth(150) widget.layout().addItem( left_node, 0, 0, ) widget.layout().addItem( right_node, 0, 1, ) title_template = "<center><b>{0}<b></center>" left_title = GraphicsTextWidget(self) left_title.setHtml(title_template.format(escape(self.source.title))) left_title.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) right_title = GraphicsTextWidget(self) right_title.setHtml(title_template.format(escape(self.sink.title))) right_title.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) widget.layout().addItem(left_title, 1, 0, alignment=Qt.AlignHCenter | Qt.AlignTop) widget.layout().addItem(right_title, 1, 1, alignment=Qt.AlignHCenter | Qt.AlignTop) widget.setParentItem(self) max_w = max( left_node.sizeHint(Qt.PreferredSize).width(), right_node.sizeHint(Qt.PreferredSize).width()) # fix same size left_node.setMinimumWidth(max_w) right_node.setMinimumWidth(max_w) left_title.setMinimumWidth(max_w) right_title.setMinimumWidth(max_w) self.layout().addItem(widget) self.layout().activate() self.sourceNodeWidget = left_node self.sinkNodeWidget = right_node self.sourceNodeTitle = left_title self.sinkNodeTitle = right_title if QT_VERSION < 0x40700: geometryChanged = Signal() def setGeometry(self, rect): QGraphicsWidget.setGeometry(self, rect) self.geometryChanged.emit()
class VizRankDialogAttrPair(VizRankDialog): """ VizRank dialog for pairs of attributes. The class provides most of the needed methods, except for `initialize` which is expected to store a list of `Variable` instances to `self.attrs`, and method `compute_score(state)` for scoring the combinations. The state is a pair of indices into `self.attrs`. When the user selects a pair, the dialog emits signal `selectionChanged` with a tuple of variables as parameter. """ pairSelected = Signal(Variable, Variable) _AttrRole = next(gui.OrangeUserRole) def __init__(self, master): VizRankDialog.__init__(self, master) self.resize(320, 512) self.attrs = [] manual_change_signal = getattr(master, "xy_changed_manually", None) if manual_change_signal: manual_change_signal.connect(self.on_manual_change) def sizeHint(self): """Assuming two columns in the table, return `QSize(320, 512)` as a reasonable default size.""" return QSize(320, 512) def check_preconditions(self): """Refuse ranking if there are less than two feature or instances.""" can_rank = self.master.data is not None and \ len(self.master.data.domain.attributes) >= 2 and \ len(self.master.data) >= 2 self.Information.nothing_to_rank(shown=not can_rank) return can_rank def on_selection_changed(self, selected, deselected): selection = selected.indexes() if not selection: return attrs = selected.indexes()[0].data(self._AttrRole) self.selectionChanged.emit(attrs) def on_manual_change(self, attr1, attr2): model = self.rank_model self.rank_table.selectionModel().clear() for row in range(model.rowCount()): a1, a2 = model.data(model.index(row, 0), self._AttrRole) if a1 is attr1 and a2 is attr2: self.rank_table.selectRow(row) return def state_count(self): n_attrs = len(self.attrs) return n_attrs * (n_attrs - 1) / 2 def iterate_states(self, initial_state): si, sj = initial_state or (0, 0) for i in range(si, len(self.attrs)): for j in range(sj, i): yield i, j sj = 0 def row_for_state(self, score, state): attrs = sorted((self.attrs[x] for x in state), key=attrgetter("name")) item = QStandardItem(", ".join(a.name for a in attrs)) item.setData(attrs, self._AttrRole) return [item]
class OWWidget(QDialog, OWComponent, Report, ProgressBarMixin, WidgetMessagesMixin, WidgetSignalsMixin, metaclass=WidgetMetaClass): """Base widget class""" # Global widget count widget_id = 0 # Widget Meta Description # ----------------------- #: Widget name (:class:`str`) as presented in the Canvas name = None id = None category = None version = None #: Short widget description (:class:`str` optional), displayed in #: canvas help tooltips. description = "" #: Widget icon path relative to the defining module icon = "icons/Unknown.png" #: Widget priority used for sorting within a category #: (default ``sys.maxsize``). priority = sys.maxsize help = None help_ref = None url = None keywords = [] background = None replaces = None #: A list of published input definitions inputs = [] #: A list of published output definitions outputs = [] # Default widget GUI layout settings # ---------------------------------- #: Should the widget have basic layout #: (If this flag is false then the `want_main_area` and #: `want_control_area` are ignored). want_basic_layout = True #: Should the widget construct a `mainArea` (this is a resizable #: area to the right of the `controlArea`). want_main_area = True #: Should the widget construct a `controlArea`. want_control_area = True #: Orientation of the buttonsArea box; valid only if #: `want_control_area` is `True`. Possible values are Qt.Horizontal, #: Qt.Vertical and None for no buttons area buttons_area_orientation = Qt.Horizontal #: Specify whether the default message bar widget should be created #: and placed into the default layout. If False then clients are #: responsible for displaying messages within the widget in an #: appropriate manner. want_message_bar = True #: Widget painted by `Save graph` button graph_name = None graph_writers = FileFormat.img_writers save_position = True #: If false the widget will receive fixed size constraint #: (derived from it's layout). Use for widgets which have simple #: static size contents. resizing_enabled = True blockingStateChanged = Signal(bool) processingStateChanged = Signal(int) # Signals have to be class attributes and cannot be inherited, # say from a mixin. This has something to do with the way PyQt binds them progressBarValueChanged = Signal(float) messageActivated = Signal(Msg) messageDeactivated = Signal(Msg) settingsHandler = None """:type: SettingsHandler""" #: Version of the settings representation #: Subclasses should increase this number when they make breaking #: changes to settings representation (a settings that used to store #: int now stores string) and handle migrations in migrate and #: migrate_context settings. settings_version = 1 savedWidgetGeometry = settings.Setting(None) #: A list of advice messages (:class:`Message`) to display to the user. #: When a widget is first shown a message from this list is selected #: for display. If a user accepts (clicks 'Ok. Got it') the choice is #: recorded and the message is never shown again (closing the message #: will not mark it as seen). Messages can be displayed again by pressing #: Shift + F1 #: #: :type: list of :class:`Message` UserAdviceMessages = [] contextAboutToBeOpened = Signal(object) contextOpened = Signal() contextClosed = Signal() def __new__(cls, *args, captionTitle=None, **kwargs): self = super().__new__(cls, None, cls.get_flags()) QDialog.__init__(self, None, self.get_flags()) OWComponent.__init__(self) WidgetMessagesMixin.__init__(self) WidgetSignalsMixin.__init__(self) stored_settings = kwargs.get('stored_settings', None) if self.settingsHandler: self.settingsHandler.initialize(self, stored_settings) self.signalManager = kwargs.get('signal_manager', None) self.__env = _asmappingproxy(kwargs.get("env", {})) self.graphButton = None self.report_button = None OWWidget.widget_id += 1 self.widget_id = OWWidget.widget_id captionTitle = self.name if captionTitle is None else captionTitle # must be set without invoking setCaption self.captionTitle = captionTitle self.setWindowTitle(captionTitle) self.setFocusPolicy(Qt.StrongFocus) self.__blocking = False # flag indicating if the widget's position was already restored self.__was_restored = False self.__statusMessage = "" self.__msgwidget = None self.__msgchoice = 0 self.left_side = None self.controlArea = self.mainArea = self.buttonsArea = None self.splitter = None if self.want_basic_layout: self.set_basic_layout() sc = QShortcut(QKeySequence(Qt.ShiftModifier | Qt.Key_F1), self) sc.activated.connect(self.__quicktip) sc = QShortcut(QKeySequence.Copy, self) sc.activated.connect(self.copy_to_clipboard) if self.controlArea is not None: # Otherwise, the first control has focus self.controlArea.setFocus(Qt.ActiveWindowFocusReason) return self # pylint: disable=super-init-not-called def __init__(self, *args, **kwargs): """__init__s are called in __new__; don't call them from here""" @classmethod def get_widget_description(cls): if not cls.name: return properties = { name: getattr(cls, name) for name in ("name", "icon", "description", "priority", "keywords", "help", "help_ref", "url", "version", "background", "replaces") } properties["id"] = cls.id or cls.__module__ properties["inputs"] = cls.get_signals("inputs") properties["outputs"] = cls.get_signals("outputs") properties["qualified_name"] = \ "{}.{}".format(cls.__module__, cls.__name__) return properties @classmethod def get_flags(cls): return (Qt.Window if cls.resizing_enabled else Qt.Dialog | Qt.MSWindowsFixedSizeDialogHint) class _Splitter(QSplitter): def createHandle(self): """Create splitter handle""" return self._Handle(self.orientation(), self, cursor=Qt.PointingHandCursor) class _Handle(QSplitterHandle): def mouseReleaseEvent(self, event): """Resize on left button""" if event.button() == Qt.LeftButton: splitter = self.splitter() splitter.setSizes([int(splitter.sizes()[0] == 0), 100000]) super().mouseReleaseEvent(event) def mouseMoveEvent(self, event): """Prevent moving; just show/hide""" return def _insert_splitter(self): self.splitter = self._Splitter(Qt.Horizontal, self) self.layout().addWidget(self.splitter) def _insert_control_area(self): self.left_side = gui.vBox(self.splitter, spacing=0) self.splitter.setSizes([1]) # Smallest size allowed by policy if self.buttons_area_orientation is not None: self.controlArea = gui.vBox(self.left_side, addSpace=0) self._insert_buttons_area() else: self.controlArea = self.left_side if self.want_main_area: self.controlArea.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) m = 0 else: m = 4 self.controlArea.layout().setContentsMargins(m, m, m, m) def _insert_buttons_area(self): self.buttonsArea = gui.widgetBox( self.left_side, addSpace=0, spacing=9, orientation=self.buttons_area_orientation) if self.graphButton is not None: self.buttonsArea.layout().addWidget(self.graphButton) if self.report_button is not None: self.buttonsArea.layout().addWidget(self.report_button) def _insert_main_area(self): self.mainArea = gui.vBox(self.splitter, margin=4, sizePolicy=QSizePolicy( QSizePolicy.Expanding, QSizePolicy.Expanding)) self.splitter.addWidget(self.mainArea) self.splitter.setCollapsible(self.splitter.indexOf(self.mainArea), False) self.mainArea.layout().setContentsMargins( 0 if self.want_control_area else 4, 4, 4, 4) def _create_default_buttons(self): # These buttons are inserted in buttons_area, if it exists # Otherwise it is up to the widget to add them to some layout if self.graph_name is not None: self.graphButton = QPushButton("&Save Image", autoDefault=False) self.graphButton.clicked.connect(self.save_graph) if hasattr(self, "send_report"): self.report_button = QPushButton("&Report", autoDefault=False) self.report_button.clicked.connect(self.show_report) 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 save_graph(self): """Save the graph with the name given in class attribute `graph_name`. The method is called by the *Save graph* button, which is created automatically if the `graph_name` is defined. """ graph_obj = getdeepattr(self, self.graph_name, None) if graph_obj is None: return saveplot.save_plot(graph_obj, self.graph_writers) def copy_to_clipboard(self): if self.graph_name: graph_obj = getdeepattr(self, self.graph_name, None) if graph_obj is None: return ClipboardFormat.write_image(None, graph_obj) def __restoreWidgetGeometry(self): def _fullscreen_to_maximized(geometry): """Don't restore windows into full screen mode because it loses decorations and can't be de-fullscreened at least on some platforms. Use Maximized state insted.""" w = QWidget(visible=False) w.restoreGeometry(QByteArray(geometry)) if w.isFullScreen(): w.setWindowState(w.windowState() & ~Qt.WindowFullScreen | Qt.WindowMaximized) return w.saveGeometry() restored = False if self.save_position: geometry = self.savedWidgetGeometry if geometry is not None: geometry = _fullscreen_to_maximized(geometry) restored = self.restoreGeometry(geometry) if restored and not self.windowState() & \ (Qt.WindowMaximized | Qt.WindowFullScreen): space = QApplication.desktop().availableGeometry(self) frame, geometry = self.frameGeometry(), self.geometry() #Fix the widget size to fit inside the available space width = space.width() - (frame.width() - geometry.width()) width = min(width, geometry.width()) height = space.height() - (frame.height() - geometry.height()) height = min(height, geometry.height()) self.resize(width, height) # Move the widget to the center of available space if it is # currently outside it if not space.contains(self.frameGeometry()): x = max(0, space.width() / 2 - width / 2) y = max(0, space.height() / 2 - height / 2) self.move(x, y) # Mark as explicitly moved/resized if not already. QDialog would # otherwise adjust position/size on subsequent hide/show # (move/resize events coming from the window manager do not set # these flags). if not self.testAttribute(Qt.WA_Moved): self.setAttribute(Qt.WA_Moved) if not self.testAttribute(Qt.WA_Resized): self.setAttribute(Qt.WA_Resized) return restored def __updateSavedGeometry(self): if self.__was_restored and self.isVisible(): # Update the saved geometry only between explicit show/hide # events (i.e. changes initiated by the user not by Qt's default # window management). # Note: This should always be stored as bytes and not QByteArray. self.savedWidgetGeometry = bytes(self.saveGeometry()) # when widget is resized, save the new width and height def resizeEvent(self, event): """Overloaded to save the geometry (width and height) when the widget is resized. """ QDialog.resizeEvent(self, event) # Don't store geometry if the widget is not visible # (the widget receives a resizeEvent (with the default sizeHint) # before first showEvent and we must not overwrite the the # savedGeometry with it) if self.save_position and self.isVisible(): self.__updateSavedGeometry() def moveEvent(self, event): """Overloaded to save the geometry when the widget is moved """ QDialog.moveEvent(self, event) if self.save_position and self.isVisible(): self.__updateSavedGeometry() def hideEvent(self, event): """Overloaded to save the geometry when the widget is hidden """ if self.save_position: self.__updateSavedGeometry() QDialog.hideEvent(self, event) def closeEvent(self, event): """Overloaded to save the geometry when the widget is closed """ if self.save_position and self.isVisible(): self.__updateSavedGeometry() QDialog.closeEvent(self, event) def showEvent(self, event): """Overloaded to restore the geometry when the widget is shown """ QDialog.showEvent(self, event) if self.save_position and not self.__was_restored: # Restore saved geometry on (first) show self.__restoreWidgetGeometry() self.__was_restored = True self.__quicktipOnce() def wheelEvent(self, event): """Silently accept the wheel event. This is to ensure combo boxes and other controls that have focus don't receive this event unless the cursor is over them. """ event.accept() def setCaption(self, caption): # save caption title in case progressbar will change it self.captionTitle = str(caption) self.setWindowTitle(caption) def reshow(self): """Put the widget on top of all windows """ self.show() self.raise_() self.activateWindow() def openContext(self, *a): """Open a new context corresponding to the given data. The settings handler first checks the stored context for a suitable match. If one is found, it becomes the current contexts and the widgets settings are initialized accordingly. If no suitable context exists, a new context is created and data is copied from the widget's settings into the new context. Widgets that have context settings must call this method after reinitializing the user interface (e.g. combo boxes) with the new data. The arguments given to this method are passed to the context handler. Their type depends upon the handler. For instance, `DomainContextHandler` expects `Orange.data.Table` or `Orange.data.Domain`. """ self.contextAboutToBeOpened.emit(a) self.settingsHandler.open_context(self, *a) self.contextOpened.emit() def closeContext(self): """Save the current settings and close the current context. Widgets that have context settings must call this method before reinitializing the user interface (e.g. combo boxes) with the new data. """ self.settingsHandler.close_context(self) self.contextClosed.emit() def retrieveSpecificSettings(self): """ Retrieve data that is not registered as setting. This method is called by `Orange.widgets.settings.ContextHandler.settings_to_widget`. Widgets may define it to retrieve any data that is not stored in widget attributes. See :obj:`Orange.widgets.data.owcolor.OWColor` for an example. """ pass def storeSpecificSettings(self): """ Store data that is not registered as setting. This method is called by `Orange.widgets.settings.ContextHandler.settings_from_widget`. Widgets may define it to store any data that is not stored in widget attributes. See :obj:`Orange.widgets.data.owcolor.OWColor` for an example. """ pass def saveSettings(self): """ Writes widget instance's settings to class defaults. Usually called when the widget is deleted. """ self.settingsHandler.update_defaults(self) def onDeleteWidget(self): """ Invoked by the canvas to notify the widget it has been deleted from the workflow. If possible, subclasses should gracefully cancel any currently executing tasks. """ pass def handleNewSignals(self): """ Invoked by the workflow signal propagation manager after all signals handlers have been called. Reimplement this method in order to coalesce updates from multiple updated inputs. """ pass #: Widget's status message has changed. statusMessageChanged = Signal(str) def setStatusMessage(self, text): """ Set widget's status message. This is a short status string to be displayed inline next to the instantiated widget icon in the canvas. """ if self.__statusMessage != text: self.__statusMessage = text self.statusMessageChanged.emit(text) def statusMessage(self): """ Return the widget's status message. """ return self.__statusMessage def keyPressEvent(self, e): """Handle default key actions or pass the event to the inherited method """ if (int(e.modifiers()), e.key()) in OWWidget.defaultKeyActions: OWWidget.defaultKeyActions[int(e.modifiers()), e.key()](self) else: QDialog.keyPressEvent(self, e) defaultKeyActions = {} if sys.platform == "darwin": defaultKeyActions = { (Qt.ControlModifier, Qt.Key_M): lambda self: self.showMaximized if self.isMinimized() else self.showMinimized(), (Qt.ControlModifier, Qt.Key_W): lambda self: self.setVisible(not self.isVisible()) } def setBlocking(self, state=True): """ Set blocking flag for this widget. While this flag is set this widget and all its descendants will not receive any new signals from the workflow signal manager. This is useful for instance if the widget does it's work in a separate thread or schedules processing from the event queue. In this case it can set the blocking flag in it's processNewSignals method schedule the task and return immediately. After the task has completed the widget can clear the flag and send the updated outputs. .. note:: Failure to clear this flag will block dependent nodes forever. """ if self.__blocking != state: self.__blocking = state self.blockingStateChanged.emit(state) def isBlocking(self): """Is this widget blocking signal processing.""" return self.__blocking def resetSettings(self): """Reset the widget settings to default""" self.settingsHandler.reset_settings(self) def workflowEnv(self): """ Return (a view to) the workflow runtime environment. Returns ------- env : types.MappingProxyType """ return self.__env def workflowEnvChanged(self, key, value, oldvalue): """ A workflow environment variable `key` has changed to value. Called by the canvas framework to notify widget of a change in the workflow runtime environment. The default implementation does nothing. """ pass def __showMessage(self, message): if self.__msgwidget is not None: self.__msgwidget.hide() self.__msgwidget.deleteLater() self.__msgwidget = None if message is None: return buttons = MessageOverlayWidget.Ok | MessageOverlayWidget.Close if message.moreurl is not None: buttons |= MessageOverlayWidget.Help if message.icon is not None: icon = message.icon else: icon = Message.Information self.__msgwidget = MessageOverlayWidget(parent=self, text=message.text, icon=icon, wordWrap=True, standardButtons=buttons) btn = self.__msgwidget.button(MessageOverlayWidget.Ok) btn.setText("Ok, got it") self.__msgwidget.setStyleSheet(""" MessageOverlayWidget { background: qlineargradient( x1: 0, y1: 0, x2: 0, y2: 1, stop:0 #666, stop:0.3 #6D6D6D, stop:1 #666) } MessageOverlayWidget QLabel#text-label { color: white; }""") if message.moreurl is not None: helpbutton = self.__msgwidget.button(MessageOverlayWidget.Help) helpbutton.setText("Learn more\N{HORIZONTAL ELLIPSIS}") self.__msgwidget.helpRequested.connect( lambda: QDesktopServices.openUrl(QUrl(message.moreurl))) self.__msgwidget.setWidget(self) self.__msgwidget.show() def __quicktip(self): messages = list(self.UserAdviceMessages) if messages: message = messages[self.__msgchoice % len(messages)] self.__msgchoice += 1 self.__showMessage(message) def __quicktipOnce(self): filename = os.path.join(settings.widget_settings_dir(), "user-session-state.ini") namespace = ( "user-message-history/{0.__module__}.{0.__qualname__}".format( type(self))) session_hist = QSettings(filename, QSettings.IniFormat) session_hist.beginGroup(namespace) messages = self.UserAdviceMessages def _ispending(msg): return not session_hist.value("{}/confirmed".format( msg.persistent_id), defaultValue=False, type=bool) messages = [msg for msg in messages if _ispending(msg)] if not messages: return message = messages[self.__msgchoice % len(messages)] self.__msgchoice += 1 self.__showMessage(message) def _userconfirmed(): session_hist = QSettings(filename, QSettings.IniFormat) session_hist.beginGroup(namespace) session_hist.setValue("{}/confirmed".format(message.persistent_id), True) session_hist.sync() self.__msgwidget.accepted.connect(_userconfirmed) @classmethod def migrate_settings(cls, settings, version): """Fix settings to work with the current version of widgets Parameters ---------- settings : dict dict of name - value mappings version : Optional[int] version of the saved settings or None if settings were created before migrations """ @classmethod def migrate_context(cls, context, version): """Fix contexts to work with the current version of widgets
class Histogram(pg.PlotWidget): """ A histogram plot with interactive 'tail' selection """ #: Emitted when the selection boundary has changed selectionChanged = Signal() #: Emitted when the selection boundary has been edited by the user #: (by dragging the boundary lines) selectionEdited = Signal() #: Selection mode NoSelection, Low, High, TwoSided, Middle = 0, 1, 2, 3, 4 def __init__(self, parent=None, **kwargs): pg.PlotWidget.__init__(self, parent, **kwargs) self.getAxis("bottom").setLabel("Score") self.getAxis("left").setLabel("Counts") self.__data = None self.__histcurve = None self.__mode = Histogram.NoSelection self.__min = 0 self.__max = 0 def makeline(pos): pen = QPen(Qt.darkGray, 1) pen.setCosmetic(True) line = InfiniteLine(angle=90, pos=pos, pen=pen, movable=True) line.setCursor(Qt.SizeHorCursor) return line self.__cuthigh = makeline(self.__max) self.__cuthigh.sigPositionChanged.connect(self.__on_cuthigh_changed) self.__cuthigh.sigPositionChangeFinished.connect(self.selectionEdited) self.__cutlow = makeline(self.__min) self.__cutlow.sigPositionChanged.connect(self.__on_cutlow_changed) self.__cutlow.sigPositionChangeFinished.connect(self.selectionEdited) brush = pg.mkBrush((200, 200, 200, 180)) self.__taillow = pg.PlotCurveItem(fillLevel=0, brush=brush, pen=QPen(Qt.NoPen)) self.__taillow.setVisible(False) self.__tailhigh = pg.PlotCurveItem(fillLevel=0, brush=brush, pen=QPen(Qt.NoPen)) self.__tailhigh.setVisible(False) def setData(self, hist, bins=None): """ Set the histogram data """ if bins is None: bins = np.arange(len(hist)) self.__data = (hist, bins) if self.__histcurve is None: self.__histcurve = pg.PlotCurveItem(x=bins, y=hist, stepMode=True) else: self.__histcurve.setData(x=bins, y=hist, stepMode=True) self.__update() def setHistogramCurve(self, curveitem): """ Set the histogram plot curve. """ if self.__histcurve is curveitem: return if self.__histcurve is not None: self.removeItem(self.__histcurve) self.__histcurve = None self.__data = None if curveitem is not None: if not curveitem.opts["stepMode"]: raise ValueError("The curve must have `stepMode == True`") self.addItem(curveitem) self.__histcurve = curveitem self.__data = (curveitem.yData, curveitem.xData) self.__update() def histogramCurve(self): """ Return the histogram plot curve. """ return self.__histcurve def setSelectionMode(self, mode): """ Set selection mode """ if self.__mode != mode: self.__mode = mode self.__update_cutlines() self.__update_tails() def setLower(self, value): """ Set the lower boundary value. """ if self.__min != value: self.__min = value self.__update_cutlines() self.__update_tails() self.selectionChanged.emit() def setUpper(self, value): """ Set the upper boundary value. """ if self.__max != value: self.__max = value self.__update_cutlines() self.__update_tails() self.selectionChanged.emit() def setBoundary(self, lower, upper): """ Set lower and upper boundary value. """ changed = False if self.__min != lower: self.__min = lower changed = True if self.__max != upper: self.__max = upper changed = True if changed: self.__update_cutlines() self.__update_tails() self.selectionChanged.emit() def boundary(self): """ Return the lower and upper boundary values. """ return (self.__min, self.__max) def clear(self): """ Clear the plot. """ self.__data = None self.__histcurve = None super().clear() def __update(self): def additem(item): if item.scene() is not self.scene(): self.addItem(item) def removeitem(item): if item.scene() is self.scene(): self.removeItem(item) if self.__data is not None: additem(self.__cuthigh) additem(self.__cutlow) additem(self.__tailhigh) additem(self.__taillow) _, edges = self.__data # Update the allowable cutoff line bounds minx, maxx = np.min(edges), np.max(edges) span = maxx - minx bounds = minx - span * 0.005, maxx + span * 0.005 self.__cuthigh.setBounds(bounds) self.__cutlow.setBounds(bounds) self.__update_cutlines() self.__update_tails() else: removeitem(self.__cuthigh) removeitem(self.__cutlow) removeitem(self.__tailhigh) removeitem(self.__taillow) def __update_cutlines(self): self.__cuthigh.setVisible(self.__mode & Histogram.High) self.__cuthigh.setValue(self.__max) self.__cutlow.setVisible(self.__mode & Histogram.Low) self.__cutlow.setValue(self.__min) def __update_tails(self): if self.__mode == Histogram.NoSelection: return if self.__data is None: return hist, edges = self.__data self.__taillow.setVisible(self.__mode & Histogram.Low) if self.__min > edges[0]: datalow = histogram_cut(hist, edges, edges[0], self.__min) self.__taillow.setData(*datalow, fillLevel=0, stepMode=True) else: self.__taillow.clear() self.__tailhigh.setVisible(self.__mode & Histogram.High) if self.__max < edges[-1]: datahigh = histogram_cut(hist, edges, self.__max, edges[-1]) self.__tailhigh.setData(*datahigh, fillLevel=0, stepMode=True) else: self.__tailhigh.clear() def __on_cuthigh_changed(self): self.setUpper(self.__cuthigh.value()) def __on_cutlow_changed(self): self.setLower(self.__cutlow.value())
class CanvasScene(QGraphicsScene): """ A Graphics Scene for displaying an :class:`~.scheme.Scheme` instance. """ #: Signal emitted when a :class:`NodeItem` has been added to the scene. node_item_added = Signal(object) #: Signal emitted when a :class:`NodeItem` has been removed from the #: scene. node_item_removed = Signal(object) #: Signal emitted when a new :class:`LinkItem` has been added to the #: scene. link_item_added = Signal(object) #: Signal emitted when a :class:`LinkItem` has been removed. link_item_removed = Signal(object) #: Signal emitted when a :class:`Annotation` item has been added. annotation_added = Signal(object) #: Signal emitted when a :class:`Annotation` item has been removed. annotation_removed = Signal(object) #: Signal emitted when the position of a :class:`NodeItem` has changed. node_item_position_changed = Signal(object, QPointF) #: Signal emitted when an :class:`NodeItem` has been double clicked. node_item_double_clicked = Signal(object) #: An node item has been activated (clicked) node_item_activated = Signal(object) #: An node item has been hovered node_item_hovered = Signal(object) #: Link item has been hovered link_item_hovered = Signal(object) def __init__(self, *args, **kwargs): QGraphicsScene.__init__(self, *args, **kwargs) self.scheme = None self.registry = None # All node items self.__node_items = [] # Mapping from SchemeNodes to canvas items self.__item_for_node = {} # All link items self.__link_items = [] # Mapping from SchemeLinks to canvas items. self.__item_for_link = {} # All annotation items self.__annotation_items = [] # Mapping from SchemeAnnotations to canvas items. self.__item_for_annotation = {} # Is the scene editable self.editable = True # Anchor Layout self.__anchor_layout = AnchorLayout() self.addItem(self.__anchor_layout) self.__channel_names_visible = True self.__node_animation_enabled = True self.user_interaction_handler = None self.activated_mapper = QSignalMapper(self) self.activated_mapper.mapped[QObject].connect( lambda node: self.node_item_activated.emit(node)) self.hovered_mapper = QSignalMapper(self) self.hovered_mapper.mapped[QObject].connect( lambda node: self.node_item_hovered.emit(node)) self.position_change_mapper = QSignalMapper(self) self.position_change_mapper.mapped[QObject].connect( self._on_position_change) log.info("'%s' intitialized." % self) def clear_scene(self): """ Clear (reset) the scene. """ if self.scheme is not None: self.scheme.node_added.disconnect(self.add_node) self.scheme.node_removed.disconnect(self.remove_node) self.scheme.link_added.disconnect(self.add_link) self.scheme.link_removed.disconnect(self.remove_link) self.scheme.annotation_added.disconnect(self.add_annotation) self.scheme.annotation_removed.disconnect(self.remove_annotation) self.scheme.node_state_changed.disconnect( self.on_widget_state_change) self.scheme.channel_state_changed.disconnect( self.on_link_state_change) # Remove all items to make sure all signals from scheme items # to canvas items are disconnected. for annot in self.scheme.annotations: if annot in self.__item_for_annotation: self.remove_annotation(annot) for link in self.scheme.links: if link in self.__item_for_link: self.remove_link(link) for node in self.scheme.nodes: if node in self.__item_for_node: self.remove_node(node) self.scheme = None self.__node_items = [] self.__item_for_node = {} self.__link_items = [] self.__item_for_link = {} self.__annotation_items = [] self.__item_for_annotation = {} self.__anchor_layout.deleteLater() self.user_interaction_handler = None self.clear() log.info("'%s' cleared." % self) def set_scheme(self, scheme): """ Set the scheme to display. Populates the scene with nodes and links already in the scheme. Any further change to the scheme will be reflected in the scene. Parameters ---------- scheme : :class:`~.scheme.Scheme` """ if self.scheme is not None: # Clear the old scheme self.clear_scene() log.info("Setting scheme '%s' on '%s'" % (scheme, self)) self.scheme = scheme if self.scheme is not None: self.scheme.node_added.connect(self.add_node) self.scheme.node_removed.connect(self.remove_node) self.scheme.link_added.connect(self.add_link) self.scheme.link_removed.connect(self.remove_link) self.scheme.annotation_added.connect(self.add_annotation) self.scheme.annotation_removed.connect(self.remove_annotation) self.scheme.node_state_changed.connect(self.on_widget_state_change) self.scheme.channel_state_changed.connect( self.on_link_state_change) self.scheme.topology_changed.connect(self.on_scheme_change) for node in scheme.nodes: self.add_node(node) for link in scheme.links: self.add_link(link) for annot in scheme.annotations: self.add_annotation(annot) def set_registry(self, registry): """ Set the widget registry. """ # TODO: Remove/Deprecate. Is used only to get the category/background # color. That should be part of the SchemeNode/WidgetDescription. log.info("Setting registry '%s on '%s'." % (registry, self)) self.registry = registry def set_anchor_layout(self, layout): """ Set an :class:`~.layout.AnchorLayout` """ if self.__anchor_layout != layout: if self.__anchor_layout: self.__anchor_layout.deleteLater() self.__anchor_layout = None self.__anchor_layout = layout def anchor_layout(self): """ Return the anchor layout instance. """ return self.__anchor_layout def set_channel_names_visible(self, visible): """ Set the channel names visibility. """ self.__channel_names_visible = visible for link in self.__link_items: link.setChannelNamesVisible(visible) def channel_names_visible(self): """ Return the channel names visibility state. """ return self.__channel_names_visible def set_node_animation_enabled(self, enabled): """ Set node animation enabled state. """ if self.__node_animation_enabled != enabled: self.__node_animation_enabled = enabled for node in self.__node_items: node.setAnimationEnabled(enabled) def add_node_item(self, item): """ Add a :class:`.NodeItem` instance to the scene. """ if item in self.__node_items: raise ValueError("%r is already in the scene." % item) if item.pos().isNull(): if self.__node_items: pos = self.__node_items[-1].pos() + QPointF(150, 0) else: pos = QPointF(150, 150) item.setPos(pos) item.setFont(self.font()) # Set signal mappings self.activated_mapper.setMapping(item, item) item.activated.connect(self.activated_mapper.map) self.hovered_mapper.setMapping(item, item) item.hovered.connect(self.hovered_mapper.map) self.position_change_mapper.setMapping(item, item) item.positionChanged.connect(self.position_change_mapper.map) self.addItem(item) self.__node_items.append(item) self.node_item_added.emit(item) log.info("Added item '%s' to '%s'" % (item, self)) return item def add_node(self, node): """ Add and return a default constructed :class:`.NodeItem` for a :class:`SchemeNode` instance `node`. If the `node` is already in the scene do nothing and just return its item. """ if node in self.__item_for_node: # Already added return self.__item_for_node[node] item = self.new_node_item(node.description) if node.position: pos = QPointF(*node.position) item.setPos(pos) item.setTitle(node.title) item.setProcessingState(node.processing_state) item.setProgress(node.progress) for message in node.state_messages(): item.setStateMessage(message) item.setStatusMessage(node.status_message()) self.__item_for_node[node] = item node.position_changed.connect(self.__on_node_pos_changed) node.title_changed.connect(item.setTitle) node.progress_changed.connect(item.setProgress) node.processing_state_changed.connect(item.setProcessingState) node.state_message_changed.connect(item.setStateMessage) node.status_message_changed.connect(item.setStatusMessage) return self.add_node_item(item) def new_node_item(self, widget_desc, category_desc=None): """ Construct an new :class:`.NodeItem` from a `WidgetDescription`. Optionally also set `CategoryDescription`. """ item = items.NodeItem() item.setWidgetDescription(widget_desc) if category_desc is None and self.registry and widget_desc.category: category_desc = self.registry.category(widget_desc.category) if category_desc is None and self.registry is not None: try: category_desc = self.registry.category(widget_desc.category) except KeyError: pass if category_desc is not None: item.setWidgetCategory(category_desc) item.setAnimationEnabled(self.__node_animation_enabled) return item def remove_node_item(self, item): """ Remove `item` (:class:`.NodeItem`) from the scene. """ self.activated_mapper.removeMappings(item) self.hovered_mapper.removeMappings(item) self.position_change_mapper.removeMappings(item) item.hide() self.removeItem(item) self.__node_items.remove(item) self.node_item_removed.emit(item) log.info("Removed item '%s' from '%s'" % (item, self)) def remove_node(self, node): """ Remove the :class:`.NodeItem` instance that was previously constructed for a :class:`SchemeNode` `node` using the `add_node` method. """ item = self.__item_for_node.pop(node) node.position_changed.disconnect(self.__on_node_pos_changed) node.title_changed.disconnect(item.setTitle) node.progress_changed.disconnect(item.setProgress) node.processing_state_changed.disconnect(item.setProcessingState) node.state_message_changed.disconnect(item.setStateMessage) self.remove_node_item(item) def node_items(self): """ Return all :class:`.NodeItem` instances in the scene. """ return list(self.__node_items) def add_link_item(self, item): """ Add a link (:class:`.LinkItem`) to the scene. """ if item.scene() is not self: self.addItem(item) item.setFont(self.font()) self.__link_items.append(item) self.link_item_added.emit(item) log.info("Added link %r -> %r to '%s'" % (item.sourceItem.title(), item.sinkItem.title(), self)) self.__anchor_layout.invalidateLink(item) return item def add_link(self, scheme_link): """ Create and add a :class:`.LinkItem` instance for a :class:`SchemeLink` instance. If the link is already in the scene do nothing and just return its :class:`.LinkItem`. """ if scheme_link in self.__item_for_link: return self.__item_for_link[scheme_link] source = self.__item_for_node[scheme_link.source_node] sink = self.__item_for_node[scheme_link.sink_node] item = self.new_link_item(source, scheme_link.source_channel, sink, scheme_link.sink_channel) item.setEnabled(scheme_link.enabled) scheme_link.enabled_changed.connect(item.setEnabled) if scheme_link.is_dynamic(): item.setDynamic(True) item.setDynamicEnabled(scheme_link.dynamic_enabled) scheme_link.dynamic_enabled_changed.connect(item.setDynamicEnabled) item.setRuntimeState(scheme_link.runtime_state()) scheme_link.state_changed.connect(item.setRuntimeState) self.add_link_item(item) self.__item_for_link[scheme_link] = item return item def new_link_item(self, source_item, source_channel, sink_item, sink_channel): """ Construct and return a new :class:`.LinkItem` """ item = items.LinkItem() item.setSourceItem(source_item) item.setSinkItem(sink_item) def channel_name(channel): if isinstance(channel, str): return channel else: return channel.name source_name = channel_name(source_channel) sink_name = channel_name(sink_channel) fmt = "<b>{0}</b> \u2192 <b>{1}</b>" item.setToolTip(fmt.format(escape(source_name), escape(sink_name))) item.setSourceName(source_name) item.setSinkName(sink_name) item.setChannelNamesVisible(self.__channel_names_visible) return item def remove_link_item(self, item): """ Remove a link (:class:`.LinkItem`) from the scene. """ # Invalidate the anchor layout. self.__anchor_layout.invalidateAnchorItem( item.sourceItem.outputAnchorItem) self.__anchor_layout.invalidateAnchorItem( item.sinkItem.inputAnchorItem) self.__link_items.remove(item) # Remove the anchor points. item.removeLink() self.removeItem(item) self.link_item_removed.emit(item) log.info("Removed link '%s' from '%s'" % (item, self)) return item def remove_link(self, scheme_link): """ Remove a :class:`.LinkItem` instance that was previously constructed for a :class:`SchemeLink` instance `link` using the `add_link` method. """ item = self.__item_for_link.pop(scheme_link) scheme_link.enabled_changed.disconnect(item.setEnabled) if scheme_link.is_dynamic(): scheme_link.dynamic_enabled_changed.disconnect( item.setDynamicEnabled) scheme_link.state_changed.disconnect(item.setRuntimeState) self.remove_link_item(item) def link_items(self): """ Return all :class:`.LinkItem`\s in the scene. """ return list(self.__link_items) def add_annotation_item(self, annotation): """ Add an :class:`.Annotation` item to the scene. """ self.__annotation_items.append(annotation) self.addItem(annotation) self.annotation_added.emit(annotation) return annotation def add_annotation(self, scheme_annot): """ Create a new item for :class:`SchemeAnnotation` and add it to the scene. If the `scheme_annot` is already in the scene do nothing and just return its item. """ if scheme_annot in self.__item_for_annotation: # Already added return self.__item_for_annotation[scheme_annot] if isinstance(scheme_annot, scheme.SchemeTextAnnotation): item = items.TextAnnotation() x, y, w, h = scheme_annot.rect item.setPos(x, y) item.resize(w, h) item.setTextInteractionFlags(Qt.TextEditorInteraction) font = font_from_dict(scheme_annot.font, item.font()) item.setFont(font) item.setContent(scheme_annot.content, scheme_annot.content_type) scheme_annot.content_changed.connect(item.setContent) elif isinstance(scheme_annot, scheme.SchemeArrowAnnotation): item = items.ArrowAnnotation() start, end = scheme_annot.start_pos, scheme_annot.end_pos item.setLine(QLineF(QPointF(*start), QPointF(*end))) item.setColor(QColor(scheme_annot.color)) scheme_annot.geometry_changed.connect( self.__on_scheme_annot_geometry_change) self.add_annotation_item(item) self.__item_for_annotation[scheme_annot] = item return item def remove_annotation_item(self, annotation): """ Remove an :class:`.Annotation` instance from the scene. """ self.__annotation_items.remove(annotation) self.removeItem(annotation) self.annotation_removed.emit(annotation) def remove_annotation(self, scheme_annotation): """ Remove an :class:`.Annotation` instance that was previously added using :func:`add_anotation`. """ item = self.__item_for_annotation.pop(scheme_annotation) scheme_annotation.geometry_changed.disconnect( self.__on_scheme_annot_geometry_change) if isinstance(scheme_annotation, scheme.SchemeTextAnnotation): scheme_annotation.content_changed.disconnect(item.setContent) self.remove_annotation_item(item) def annotation_items(self): """ Return all :class:`.Annotation` items in the scene. """ return self.__annotation_items def item_for_annotation(self, scheme_annotation): return self.__item_for_annotation[scheme_annotation] def annotation_for_item(self, item): rev = dict( reversed(item) for item in self.__item_for_annotation.items()) return rev[item] def commit_scheme_node(self, node): """ Commit the `node` into the scheme. """ if not self.editable: raise Exception("Scheme not editable.") if node not in self.__item_for_node: raise ValueError("No 'NodeItem' for node.") item = self.__item_for_node[node] try: self.scheme.add_node(node) except Exception: log.error("An error occurred while committing node '%s'", node, exc_info=True) # Cleanup (remove the node item) self.remove_node_item(item) raise log.info("Commited node '%s' from '%s' to '%s'" % (node, self, self.scheme)) def commit_scheme_link(self, link): """ Commit a scheme link. """ if not self.editable: raise Exception("Scheme not editable") if link not in self.__item_for_link: raise ValueError("No 'LinkItem' for link.") self.scheme.add_link(link) log.info("Commited link '%s' from '%s' to '%s'" % (link, self, self.scheme)) def node_for_item(self, item): """ Return the `SchemeNode` for the `item`. """ rev = dict([(v, k) for k, v in self.__item_for_node.items()]) return rev[item] def item_for_node(self, node): """ Return the :class:`NodeItem` instance for a :class:`SchemeNode`. """ return self.__item_for_node[node] def link_for_item(self, item): """ Return the `SchemeLink for `item` (:class:`LinkItem`). """ rev = dict([(v, k) for k, v in self.__item_for_link.items()]) return rev[item] def item_for_link(self, link): """ Return the :class:`LinkItem` for a :class:`SchemeLink` """ return self.__item_for_link[link] def selected_node_items(self): """ Return the selected :class:`NodeItem`'s. """ return [item for item in self.__node_items if item.isSelected()] def selected_annotation_items(self): """ Return the selected :class:`Annotation`'s """ return [item for item in self.__annotation_items if item.isSelected()] def node_links(self, node_item): """ Return all links from the `node_item` (:class:`NodeItem`). """ return self.node_output_links(node_item) + self.node_input_links( node_item) def node_output_links(self, node_item): """ Return a list of all output links from `node_item`. """ return [ link for link in self.__link_items if link.sourceItem == node_item ] def node_input_links(self, node_item): """ Return a list of all input links for `node_item`. """ return [ link for link in self.__link_items if link.sinkItem == node_item ] def neighbor_nodes(self, node_item): """ Return a list of `node_item`'s (class:`NodeItem`) neighbor nodes. """ neighbors = list( map(attrgetter("sourceItem"), self.node_input_links(node_item))) neighbors.extend( map(attrgetter("sinkItem"), self.node_output_links(node_item))) return neighbors def on_widget_state_change(self, widget, state): pass def on_link_state_change(self, link, state): pass def on_scheme_change(self, ): pass def _on_position_change(self, item): # Invalidate the anchor point layout and schedule a layout. self.__anchor_layout.invalidateNode(item) self.node_item_position_changed.emit(item, item.pos()) def __on_node_pos_changed(self, pos): node = self.sender() item = self.__item_for_node[node] item.setPos(*pos) def __on_scheme_annot_geometry_change(self): annot = self.sender() item = self.__item_for_annotation[annot] if isinstance(annot, scheme.SchemeTextAnnotation): item.setGeometry(QRectF(*annot.rect)) elif isinstance(annot, scheme.SchemeArrowAnnotation): p1 = item.mapFromScene(QPointF(*annot.start_pos)) p2 = item.mapFromScene(QPointF(*annot.end_pos)) item.setLine(QLineF(p1, p2)) else: pass def item_at(self, pos, type_or_tuple=None, buttons=0): """Return the item at `pos` that is an instance of the specified type (`type_or_tuple`). If `buttons` (`Qt.MouseButtons`) is given only return the item if it is the top level item that would accept any of the buttons (`QGraphicsItem.acceptedMouseButtons`). """ rect = QRectF(pos, QSizeF(1, 1)) items = self.items(rect) if buttons: items = itertools.dropwhile( lambda item: not item.acceptedMouseButtons() & buttons, items) items = list(items)[:1] if type_or_tuple: items = [i for i in items if isinstance(i, type_or_tuple)] return items[0] if items else None if USE_PYQT and PYQT_VERSION < 0x40900: # For QGraphicsObject subclasses items, itemAt ... return a # QGraphicsItem wrapper instance and not the actual class instance. def itemAt(self, *args, **kwargs): item = QGraphicsScene.itemAt(self, *args, **kwargs) return toGraphicsObjectIfPossible(item) def items(self, *args, **kwargs): items = QGraphicsScene.items(self, *args, **kwargs) return list(map(toGraphicsObjectIfPossible, items)) def selectedItems(self, *args, **kwargs): return list( map( toGraphicsObjectIfPossible, QGraphicsScene.selectedItems(self, *args, **kwargs), )) def collidingItems(self, *args, **kwargs): return list( map( toGraphicsObjectIfPossible, QGraphicsScene.collidingItems(self, *args, **kwargs), )) def focusItem(self, *args, **kwargs): item = QGraphicsScene.focusItem(self, *args, **kwargs) return toGraphicsObjectIfPossible(item) def mouseGrabberItem(self, *args, **kwargs): item = QGraphicsScene.mouseGrabberItem(self, *args, **kwargs) return toGraphicsObjectIfPossible(item) def mousePressEvent(self, event): if (self.user_interaction_handler and self.user_interaction_handler.mousePressEvent(event)): return # Right (context) click on the node item. If the widget is not # in the current selection then select the widget (only the widget). # Else simply return and let customContextMenuRequested signal # handle it shape_item = self.item_at(event.scenePos(), items.NodeItem) if (shape_item and event.button() == Qt.RightButton and shape_item.flags() & QGraphicsItem.ItemIsSelectable): if not shape_item.isSelected(): self.clearSelection() shape_item.setSelected(True) return QGraphicsScene.mousePressEvent(self, event) def mouseMoveEvent(self, event): if (self.user_interaction_handler and self.user_interaction_handler.mouseMoveEvent(event)): return return QGraphicsScene.mouseMoveEvent(self, event) def mouseReleaseEvent(self, event): if (self.user_interaction_handler and self.user_interaction_handler.mouseReleaseEvent(event)): return return QGraphicsScene.mouseReleaseEvent(self, event) def mouseDoubleClickEvent(self, event): if (self.user_interaction_handler and self.user_interaction_handler.mouseDoubleClickEvent(event)): return return QGraphicsScene.mouseDoubleClickEvent(self, event) def keyPressEvent(self, event): if (self.user_interaction_handler and self.user_interaction_handler.keyPressEvent(event)): return return QGraphicsScene.keyPressEvent(self, event) def keyReleaseEvent(self, event): if (self.user_interaction_handler and self.user_interaction_handler.keyReleaseEvent(event)): return return QGraphicsScene.keyReleaseEvent(self, event) def contextMenuEvent(self, event): if (self.user_interaction_handler and self.user_interaction_handler.contextMenuEvent(event)): return super().contextMenuEvent(event) def set_user_interaction_handler(self, handler): if (self.user_interaction_handler and not self.user_interaction_handler.isFinished()): self.user_interaction_handler.cancel() log.info("Setting interaction '%s' to '%s'" % (handler, self)) self.user_interaction_handler = handler if handler: handler.start() def __str__(self): return "%s(objectName=%r, ...)" % (type(self).__name__, str(self.objectName()))