class SingleLinkPage(QFrame): """ An simple (overly) large image with a external link """ def __init__(self, *args, image=QImage(), heading="", link=QUrl(), **kwargs): super().__init__(*args, **kwargs) self.__link = QUrl() layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) self.__heading = QLabel() self.__content = QLabel() self.layout().addWidget(self.__heading) self.layout().addWidget(self.__content, 10, Qt.AlignCenter) self.__shadow = DropShadowFrame() self.__shadow.setWidget(self.__content) self.setImage(image) self.setHeading(heading) self.setLink(link) def setHeading(self, heading): self.__heading.setText("<h2>{}</h2>".format(heading)) def setImage(self, image): pm = pixmap_from_image(image) self.__content.setPixmap(pm) def setLink(self, url): self.__link = QUrl(url) if not self.__link.isEmpty(): self.__content.setCursor(Qt.PointingHandCursor) else: self.__content.unsetCursor() self.__content.setToolTip(self.__link.toString()) def mousePressEvent(self, event): if self.__content.geometry().contains(event.pos()) and \ not self.__link.isEmpty(): QDesktopServices.openUrl(self.__link) event.accept() else: super().mousePressEvent(event)
def ShowInfo(self): dialog = QDialog(self) dialog.setModal(False) dialog.setLayout(QVBoxLayout()) label = QLabel(dialog) label.setText("Ontology:\n" + self.ontology.header if self.ontology else "Ontology not loaded!") dialog.layout().addWidget(label) label = QLabel(dialog) label.setText("Annotations:\n" + self.annotations.header.replace("!", "") if self.annotations else "Annotations not loaded!") dialog.layout().addWidget(label) dialog.show()
def show_tip(widget: QWidget, pos: QPoint, text: str, timeout=-1, textFormat=Qt.AutoText, wordWrap=None): propname = __name__ + "::show_tip_qlabel" if timeout < 0: timeout = widget.toolTipDuration() if timeout < 0: timeout = 5000 + 40 * max(0, len(text) - 100) tip = widget.property(propname) if not text and tip is None: return def hide(): w = tip.parent() w.setProperty(propname, None) tip.timer.stop() tip.close() tip.deleteLater() if not isinstance(tip, QLabel): tip = QLabel(objectName="tip-label", focusPolicy=Qt.NoFocus) tip.setBackgroundRole(QPalette.ToolTipBase) tip.setForegroundRole(QPalette.ToolTipText) tip.setPalette(QToolTip.palette()) tip.setFont(QApplication.font("QTipLabel")) tip.timer = QTimer(tip, singleShot=True, objectName="hide-timer") tip.timer.timeout.connect(hide) widget.setProperty(propname, tip) tip.setParent(widget, Qt.ToolTip) tip.setText(text) tip.setTextFormat(textFormat) if wordWrap is None: wordWrap = textFormat != Qt.PlainText tip.setWordWrap(wordWrap) if not text: hide() else: tip.timer.start(timeout) tip.show() tip.move(pos)
class DummyOWWidget(widget.OWWidget): """ Dummy OWWidget used to report import/init errors in the canvas. """ name = "Placeholder" # Fake settings handler that preserves the settings class DummySettingsHandler(settings.SettingsHandler): def pack_data(self, widget): return getattr(widget, "_settings", {}) def initialize(self, widget, data=None): widget._settings = data settings.SettingsHandler.initialize(self, widget, None) # specifically disable persistent global defaults def write_defaults(self): pass def read_defaults(self): pass settingsHandler = DummySettingsHandler() want_main_area = False def __init__(self, parent=None): super().__init__(parent) self.errorLabel = QLabel( textInteractionFlags=Qt.TextSelectableByMouse, wordWrap=True, ) self.errorLabel.setSizePolicy( QSizePolicy.Expanding, QSizePolicy.Expanding ) self.controlArea.layout().addWidget(self.errorLabel) def setErrorMessage(self, message): self.errorLabel.setText(message) self.error(message)
def mousePressEvent(self, event): if event.button() == Qt.LeftButton: if self.__popuptext: popup = QMenu(self) label = QLabel( self, textInteractionFlags=Qt.TextBrowserInteraction, openExternalLinks=self.__openExternalLinks, ) label.setText(self.__styled(self.__defaultStyleSheet, self.__popuptext)) label.linkActivated.connect(self.linkActivated) label.linkHovered.connect(self.linkHovered) action = QWidgetAction(popup) action.setDefaultWidget(label) popup.addAction(action) popup.popup(event.globalPos(), action) event.accept() return else: super().mousePressEvent(event)
class OWImportImages(widget.OWWidget): name = "Import Images" description = "Import images from a directory(s)" icon = "icons/ImportImages.svg" priority = 110 outputs = [("Data", Orange.data.Table)] #: list of recent paths recent_paths = settings.Setting([]) # type: List[RecentPath] want_main_area = False resizing_enabled = False Modality = Qt.ApplicationModal # Modality = Qt.WindowModal MaxRecentItems = 20 def __init__(self): super().__init__() #: widget's runtime state self.__state = State.NoState self.data = None self._n_image_categories = 0 self._n_image_data = 0 self._n_skipped = 0 self.__invalidated = False self.__pendingTask = None vbox = gui.vBox(self.controlArea) hbox = gui.hBox(vbox) self.recent_cb = QComboBox( sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon, minimumContentsLength=16, acceptDrops=True ) self.recent_cb.installEventFilter(self) self.recent_cb.activated[int].connect(self.__onRecentActivated) icons = standard_icons(self) browseaction = QAction( "Open/Load Images", self, iconText="\N{HORIZONTAL ELLIPSIS}", icon=icons.dir_open_icon, toolTip="Select a directory from which to load the images" ) browseaction.triggered.connect(self.__runOpenDialog) reloadaction = QAction( "Reload", self, icon=icons.reload_icon, toolTip="Reload current image set" ) reloadaction.triggered.connect(self.reload) self.__actions = namespace( browse=browseaction, reload=reloadaction, ) browsebutton = QPushButton( browseaction.iconText(), icon=browseaction.icon(), toolTip=browseaction.toolTip(), clicked=browseaction.trigger ) reloadbutton = QPushButton( reloadaction.iconText(), icon=reloadaction.icon(), clicked=reloadaction.trigger, default=True, ) hbox.layout().addWidget(self.recent_cb) hbox.layout().addWidget(browsebutton) hbox.layout().addWidget(reloadbutton) self.addActions([browseaction, reloadaction]) reloadaction.changed.connect( lambda: reloadbutton.setEnabled(reloadaction.isEnabled()) ) box = gui.vBox(vbox, "Info") self.infostack = QStackedWidget() self.info_area = QLabel( text="No image set selected", wordWrap=True ) self.progress_widget = QProgressBar( minimum=0, maximum=0 ) self.cancel_button = QPushButton( "Cancel", icon=icons.cancel_icon, ) self.cancel_button.clicked.connect(self.cancel) w = QWidget() vlayout = QVBoxLayout() vlayout.setContentsMargins(0, 0, 0, 0) hlayout = QHBoxLayout() hlayout.setContentsMargins(0, 0, 0, 0) hlayout.addWidget(self.progress_widget) hlayout.addWidget(self.cancel_button) vlayout.addLayout(hlayout) self.pathlabel = TextLabel() self.pathlabel.setTextElideMode(Qt.ElideMiddle) self.pathlabel.setAttribute(Qt.WA_MacSmallSize) vlayout.addWidget(self.pathlabel) w.setLayout(vlayout) self.infostack.addWidget(self.info_area) self.infostack.addWidget(w) box.layout().addWidget(self.infostack) self.__initRecentItemsModel() self.__invalidated = True self.__executor = ThreadExecutor(self) QApplication.postEvent(self, QEvent(RuntimeEvent.Init)) def __initRecentItemsModel(self): self._relocate_recent_files() recent_paths = [] for item in self.recent_paths: recent_paths.append(item) recent_paths = recent_paths[:OWImportImages.MaxRecentItems] recent_model = self.recent_cb.model() recent_model.clear() for pathitem in recent_paths: item = RecentPath_asqstandarditem(pathitem) recent_model.appendRow(item) self.recent_paths = recent_paths if self.recent_paths and os.path.isdir(self.recent_paths[0].abspath): self.recent_cb.setCurrentIndex(0) self.__actions.reload.setEnabled(True) else: self.recent_cb.setCurrentIndex(-1) self.__actions.reload.setEnabled(False) def customEvent(self, event): """Reimplemented.""" if event.type() == RuntimeEvent.Init: if self.__invalidated: try: self.start() finally: self.__invalidated = False super().customEvent(event) def __runOpenDialog(self): startdir = os.path.expanduser("~/") if self.recent_paths: startdir = os.path.dirname(self.recent_paths[0].abspath) if OWImportImages.Modality == Qt.WindowModal: dlg = QFileDialog( self, "Select Top Level Directory", startdir, acceptMode=QFileDialog.AcceptOpen, modal=True, ) dlg.setFileMode(QFileDialog.Directory) dlg.setOption(QFileDialog.ShowDirsOnly) dlg.setDirectory(startdir) dlg.setAttribute(Qt.WA_DeleteOnClose) @dlg.accepted.connect def on_accepted(): dirpath = dlg.selectedFiles() if dirpath: self.setCurrentPath(dirpath[0]) self.start() dlg.open() else: dirpath = QFileDialog.getExistingDirectory( self, "Select Top Level Directory", startdir ) if dirpath: self.setCurrentPath(dirpath) self.start() def __onRecentActivated(self, index): item = self.recent_cb.itemData(index) if item is None: return assert isinstance(item, RecentPath) self.setCurrentPath(item.abspath) self.start() def __updateInfo(self): if self.__state == State.NoState: text = "No image set selected" elif self.__state == State.Processing: text = "Processing" elif self.__state == State.Done: nvalid = self._n_image_data ncategories = self._n_image_categories n_skipped = self._n_skipped if ncategories < 2: text = "{} image{}".format(nvalid, "s" if nvalid != 1 else "") else: text = "{} images / {} categories".format(nvalid, ncategories) if n_skipped > 0: text = text + ", {} skipped".format(n_skipped) elif self.__state == State.Cancelled: text = "Cancelled" elif self.__state == State.Error: text = "Error state" else: assert False self.info_area.setText(text) if self.__state == State.Processing: self.infostack.setCurrentIndex(1) else: self.infostack.setCurrentIndex(0) def setCurrentPath(self, path): """ Set the current root image path to path If the path does not exists or is not a directory the current path is left unchanged Parameters ---------- path : str New root import path. Returns ------- status : bool True if the current root import path was successfully changed to path. """ if self.recent_paths and path is not None and \ os.path.isdir(self.recent_paths[0].abspath) and os.path.isdir(path) \ and os.path.samefile(os.path.isdir(self.recent_paths[0].abspath), path): return True success = True error = None if path is not None: if not os.path.exists(path): error = "'{}' does not exist".format(path) path = None success = False elif not os.path.isdir(path): error = "'{}' is not a directory".format(path) path = None success = False if error is not None: self.error(error) warnings.warn(error, UserWarning, stacklevel=3) else: self.error() if path is not None: newindex = self.addRecentPath(path) self.recent_cb.setCurrentIndex(newindex) self.__actions.reload.setEnabled(len(self.recent_paths) > 0) if self.__state == State.Processing: self.cancel() return success def _search_paths(self): basedir = self.workflowEnv().get("basedir", None) if basedir is None: return [] return [("basedir", basedir)] def addRecentPath(self, path): """ Prepend a path entry to the list of recent paths If an entry with the same path already exists in the recent path list it is moved to the first place Parameters ---------- path : str """ existing = None for pathitem in self.recent_paths: try: if os.path.samefile(pathitem.abspath, path): existing = pathitem break except FileNotFoundError: # file not found if the `pathitem.abspath` no longer exists pass model = self.recent_cb.model() if existing is not None: selected_index = self.recent_paths.index(existing) assert model.item(selected_index).data(Qt.UserRole) is existing self.recent_paths.remove(existing) row = model.takeRow(selected_index) self.recent_paths.insert(0, existing) model.insertRow(0, row) else: item = RecentPath.create(path, self._search_paths()) self.recent_paths.insert(0, item) model.insertRow(0, RecentPath_asqstandarditem(item)) return 0 def __setRuntimeState(self, state): assert state in State self.setBlocking(state == State.Processing) message = "" if state == State.Processing: assert self.__state in [State.Done, State.NoState, State.Error, State.Cancelled] message = "Processing" elif state == State.Done: assert self.__state == State.Processing elif state == State.Cancelled: assert self.__state == State.Processing message = "Cancelled" elif state == State.Error: message = "Error during processing" elif state == State.NoState: message = "" else: assert False self.__state = state if self.__state == State.Processing: self.infostack.setCurrentIndex(1) else: self.infostack.setCurrentIndex(0) self.setStatusMessage(message) self.__updateInfo() def reload(self): """ Restart the image scan task """ if self.__state == State.Processing: self.cancel() self.data = None self.start() def start(self): """ Start/execute the image indexing operation """ self.error() self.__invalidated = False if not self.recent_paths: return if self.__state == State.Processing: assert self.__pendingTask is not None log.info("Starting a new task while one is in progress. " "Cancel the existing task (dir:'{}')" .format(self.__pendingTask.startdir)) self.cancel() startdir = self.recent_paths[0].abspath self.__setRuntimeState(State.Processing) report_progress = methodinvoke( self, "__onReportProgress", (object,)) task = ImportImages(report_progress=report_progress) # collect the task state in one convenient place self.__pendingTask = taskstate = namespace( task=task, startdir=startdir, future=None, watcher=None, cancelled=False, cancel=None, ) def cancel(): # Cancel the task and disconnect if taskstate.future.cancel(): pass else: taskstate.task.cancelled = True taskstate.cancelled = True try: taskstate.future.result(timeout=3) except UserInterruptError: pass except TimeoutError: log.info("The task did not stop in in a timely manner") taskstate.watcher.finished.disconnect(self.__onRunFinished) taskstate.cancel = cancel def run_image_scan_task_interupt(): try: return task(startdir) except UserInterruptError: # Suppress interrupt errors, so they are not logged return taskstate.future = self.__executor.submit(run_image_scan_task_interupt) taskstate.watcher = FutureWatcher(taskstate.future) taskstate.watcher.finished.connect(self.__onRunFinished) @Slot() def __onRunFinished(self): assert QThread.currentThread() is self.thread() assert self.__state == State.Processing assert self.__pendingTask is not None assert self.sender() is self.__pendingTask.watcher assert self.__pendingTask.future.done() task = self.__pendingTask self.__pendingTask = None try: data, n_skipped = task.future.result() except Exception: sys.excepthook(*sys.exc_info()) state = State.Error data = None n_skipped = 0 self.error(traceback.format_exc()) else: state = State.Done self.error() if data: self._n_image_data = len(data) self._n_image_categories = len(data.domain.class_var.values)\ if data.domain.class_var else 0 else: self._n_image_data, self._n_image_categories = 0, 0 self.data = data self._n_skipped = n_skipped self.__setRuntimeState(state) self.commit() def cancel(self): """ Cancel current pending task (if any). """ if self.__state == State.Processing: assert self.__pendingTask is not None self.__pendingTask.cancel() self.__pendingTask = None self.__setRuntimeState(State.Cancelled) @Slot(object) def __onReportProgress(self, arg): # report on scan progress from a worker thread # arg must be a namespace(count: int, lastpath: str) assert QThread.currentThread() is self.thread() if self.__state == State.Processing: self.pathlabel.setText(prettyfypath(arg.lastpath)) def commit(self): """ Commit a Table from the collected image meta data. """ self.send("Data", self.data) def onDeleteWidget(self): self.cancel() self.__executor.shutdown(wait=True) self.__invalidated = False def eventFilter(self, receiver, event): # re-implemented from QWidget # intercept and process drag drop events on the recent directory # selection combo box def dirpath(event): # type: (QDropEvent) -> Optional[str] """Return the directory from a QDropEvent.""" data = event.mimeData() urls = data.urls() if len(urls) == 1: url = urls[0] path = url.toLocalFile() if path.endswith("/"): path = path[:-1] # remove last / if os.path.isdir(path): return path return None if receiver is self.recent_cb and \ event.type() in {QEvent.DragEnter, QEvent.DragMove, QEvent.Drop}: assert isinstance(event, QDropEvent) path = dirpath(event) if path is not None and event.possibleActions() & Qt.LinkAction: event.setDropAction(Qt.LinkAction) event.accept() if event.type() == QEvent.Drop: self.setCurrentPath(path) self.start() else: event.ignore() return True return super().eventFilter(receiver, event) def _relocate_recent_files(self): search_paths = self._search_paths() rec = [] for recent in self.recent_paths: kwargs = dict( title=recent.title, sheet=recent.sheet, file_format=recent.file_format) resolved = recent.resolve(search_paths) if resolved is not None: rec.append( RecentPath.create(resolved.abspath, search_paths, **kwargs)) else: rec.append(recent) # change the list in-place for the case the widgets wraps this list self.recent_paths[:] = rec def workflowEnvChanged(self, key, value, oldvalue): """ Function called when environment changes (e.g. while saving the scheme) It make sure that all environment connected values are modified (e.g. relative file paths are changed) """ self.__initRecentItemsModel()
class OWNNLearner(OWBaseLearner): name = "Neural Network" description = "A multi-layer perceptron (MLP) algorithm with " \ "backpropagation." icon = "icons/NN.svg" priority = 90 keywords = ["mlp"] LEARNER = NNLearner activation = ["identity", "logistic", "tanh", "relu"] act_lbl = ["Identity", "Logistic", "tanh", "ReLu"] solver = ["lbfgs", "sgd", "adam"] solv_lbl = ["L-BFGS-B", "SGD", "Adam"] learner_name = Setting("Neural Network") hidden_layers_input = Setting("100,") activation_index = Setting(3) solver_index = Setting(2) max_iterations = Setting(200) alpha_index = Setting(0) settings_version = 1 alphas = list( chain([x / 10000 for x in range(1, 10)], [x / 1000 for x in range(1, 10)], [x / 100 for x in range(1, 10)], [x / 10 for x in range(1, 10)], range(1, 10), range(10, 100, 5), range(100, 200, 10), range(100, 1001, 50))) def add_main_layout(self): form = QFormLayout() form.setFieldGrowthPolicy(form.AllNonFixedFieldsGrow) form.setVerticalSpacing(25) gui.widgetBox(self.controlArea, True, orientation=form) form.addRow( "Neurons in hidden layers:", gui.lineEdit( None, self, "hidden_layers_input", orientation=Qt.Horizontal, callback=self.settings_changed, tooltip="A list of integers defining neurons. Length of list " "defines the number of layers. E.g. 4, 2, 2, 3.", placeholderText="e.g. 100,")) form.addRow( "Activation:", gui.comboBox(None, self, "activation_index", orientation=Qt.Horizontal, label="Activation:", items=[i for i in self.act_lbl], callback=self.settings_changed)) form.addRow(" ", gui.separator(None, 16)) form.addRow( "Solver:", gui.comboBox(None, self, "solver_index", orientation=Qt.Horizontal, label="Solver:", items=[i for i in self.solv_lbl], callback=self.settings_changed)) self.reg_label = QLabel() slider = gui.hSlider(None, self, "alpha_index", minValue=0, maxValue=len(self.alphas) - 1, callback=lambda: (self.set_alpha(), self.settings_changed()), createLabel=False) form.addRow(self.reg_label, slider) self.set_alpha() form.addRow( "Maximal number of iterations:", gui.spin(None, self, "max_iterations", 10, 10000, step=10, label="Max iterations:", orientation=Qt.Horizontal, alignment=Qt.AlignRight, callback=self.settings_changed)) def set_alpha(self): self.strength_C = self.alphas[self.alpha_index] self.reg_label.setText("Regularization, α={}:".format(self.strength_C)) @property def alpha(self): return self.alphas[self.alpha_index] def setup_layout(self): super().setup_layout() self._task = None # type: Optional[Task] self._executor = ThreadExecutor() # just a test cancel button gui.button(self.apply_button, self, "Cancel", callback=self.cancel) def create_learner(self): return self.LEARNER(hidden_layer_sizes=self.get_hidden_layers(), activation=self.activation[self.activation_index], solver=self.solver[self.solver_index], alpha=self.alpha, max_iter=self.max_iterations, preprocessors=self.preprocessors) def get_learner_parameters(self): return (("Hidden layers", ', '.join(map(str, self.get_hidden_layers()))), ("Activation", self.act_lbl[self.activation_index]), ("Solver", self.solv_lbl[self.solver_index]), ("Alpha", self.alpha), ("Max iterations", self.max_iterations)) def get_hidden_layers(self): layers = tuple(map(int, re.findall(r'\d+', self.hidden_layers_input))) if not layers: layers = (100, ) self.hidden_layers_input = "100," return layers def update_model(self): self.show_fitting_failed(None) self.model = None if self.check_data(): self.__update() else: self.Outputs.model.send(self.model) @Slot(float) def setProgressValue(self, value): assert self.thread() is QThread.currentThread() self.progressBarSet(value) def __update(self): if self._task is not None: # First make sure any pending tasks are cancelled. self.cancel() assert self._task is None max_iter = self.learner.kwargs["max_iter"] # Setup the task state task = Task() lastemitted = 0. def callback(iteration): nonlocal task # type: Task nonlocal lastemitted if task.isInterruptionRequested(): raise CancelTaskException() progress = round(iteration / max_iter * 100) if progress != lastemitted: task.emitProgressUpdate(progress) lastemitted = progress # copy to set the callback so that the learner output is not modified # (currently we can not pass callbacks to learners __call__) learner = copy.copy(self.learner) learner.callback = callback def build_model(data, learner): try: return learner(data) except CancelTaskException: return None build_model_func = partial(build_model, self.data, learner) task.setFuture(self._executor.submit(build_model_func)) task.done.connect(self._task_finished) task.progressChanged.connect(self.setProgressValue) self._task = task self.progressBarInit() self.setBlocking(True) @Slot(concurrent.futures.Future) def _task_finished(self, f): """ Parameters ---------- f : Future The future instance holding the built model """ assert self.thread() is QThread.currentThread() assert self._task is not None assert self._task.future is f assert f.done() self._task.deleteLater() self._task = None self.setBlocking(False) self.progressBarFinished() try: self.model = f.result() except Exception as ex: # pylint: disable=broad-except # Log the exception with a traceback log = logging.getLogger() log.exception(__name__, exc_info=True) self.model = None self.show_fitting_failed(ex) else: self.model.name = self.learner_name self.model.instances = self.data self.model.skl_model.orange_callback = None # remove unpicklable callback self.Outputs.model.send(self.model) def cancel(self): """ Cancel the current task (if any). """ if self._task is not None: self._task.cancel() assert self._task.future.done() # disconnect from the task self._task.done.disconnect(self._task_finished) self._task.progressChanged.disconnect(self.setProgressValue) self._task.deleteLater() self._task = None self.progressBarFinished() self.setBlocking(False) def onDeleteWidget(self): self.cancel() super().onDeleteWidget() @classmethod def migrate_settings(cls, settings, version): if not version: alpha = settings.pop("alpha", None) if alpha is not None: settings["alpha_index"] = \ np.argmin(np.abs(np.array(cls.alphas) - alpha))
class ControlCombo(ControlBase, QWidget): """This class represents a wrapper to the combo box""" def __init__(self, label='', default=None, helptext=None): QWidget.__init__(self) ControlBase.__init__(self, label, default, helptext) ########################################################################## ############ Functions ################################################### ########################################################################## def init_form(self): self._layout = QHBoxLayout() self._combo = QComboBox(self.form) if self._label is not None: self._combolabel = QLabel(self.form) self._layout.addWidget(self._combolabel) self._combolabel.setAccessibleName('ControlCombo-label') self.label = self._label else: self._combolabel = None self._layout.addWidget(self._combo) self.form.setLayout(self._layout) self._combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self._layout.setContentsMargins(0, 0, 0, 0) self.form.setContentsMargins(0, 0, 0, 0) self.form.setMinimumHeight(38) self.form.setMaximumHeight(38) self.form.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) self._combo.currentIndexChanged.connect(self._currentIndexChanged) self._combo.activated.connect(self._activated) self._combo.highlighted.connect(self._highlighted) self._combo.editTextChanged.connect(self._editTextChanged) self._items = {} self._addingItem = False def clear(self): self._items = {} self._value = None self._combo.clear() def add_item(self, label, value=None): self._addingItem = True if value is not None: if not (value in self._items.values()): self._combo.addItem(label) else: if not (label in self._items.keys()): self._combo.addItem(label) firstValue = False if self._items == {}: firstValue = True if value is None: self._items[str(label)] = label else: self._items[str(label)] = value self._addingItem = False if firstValue: self.value = self._items[label] def __add__(self, val): if isinstance(val, tuple): self.add_item(val[0], val[1]) else: self.add_item(val) return self def get_item_index_by_name(self, item_name): """ Returns the index of the item containing the given name :param item_name: item name in combo box :type item_name: string """ return self._combo.findText(item_name) def count(self): return self._combo.count() def show(self): """ Show the control """ QWidget.show(self) def hide(self): """ Hide the control """ QWidget.hide(self) ########################################################################## ############ Events ###################################################### ########################################################################## def current_index_changed_event(self, index): """Called when the user chooses an item in the combobox and the selected choice is different from the last one selected. @index: item's index """ pass def activated_event(self, index): """Called when the user chooses an item in the combobox. Note that this signal happens even when the choice is not changed @index: item's index """ pass def highlighted_event(self, index): pass def edittext_changed_event(self, text): pass ########################################################################## ############ PROPERTIES ################################################## ########################################################################## @property def form(self): return self @property def current_index(self): return self._combo.currentIndex() @current_index.setter def current_index(self, value): self._combo.setCurrentIndex(value) @property def items(self): return self._items.items() @property def value(self): return self._value @value.setter def value(self, value): for key, val in self.items: if value == val: index = self._combo.findText(key) self._combo.setCurrentIndex(index) if self._value != value: self.changed_event() self._value = val @property def text(self): return str(self._combo.currentText()) @text.setter def text(self, value): for key, val in self.items: if value == key: self.value = val break @property def label(self): if self._combolabel: return self._combolabel.text() else: return None @label.setter def label(self, value): """ Label of the control, if applies @type value: string """ if self._combolabel: self._combolabel.setText(value) ########################################################################## ############ Private functions ########################################### ########################################################################## def _activated(self, index): if not self._addingItem: item = self._combo.currentText() if len(item) >= 1: ControlBase.value.fset(self, self._items[str(item)]) self.activated_event(index) def _highlighted(self, index): """Called when an item in the combobox popup list is highlighted by the user. @index: item's index """ self.highlighted_event(index) def _editTextChanged(self, text): self.edittext_changed_event(text) def _currentIndexChanged(self, index): if not self._addingItem: item = self._combo.currentText() if len(item) >= 1: ControlBase.value.fset(self, self._items[str(item)]) self.current_index_changed_event(index)
class MessageWidget(QWidget): """ A widget displaying a simple message to the user. This is an alternative to a full QMessageBox intended for inline modeless messages. [[icon] {Message text} (Ok) (Cancel)] """ #: Emitted when a button with the AcceptRole is clicked accepted = Signal() #: Emitted when a button with the RejectRole is clicked rejected = Signal() #: Emitted when a button with the HelpRole is clicked helpRequested = Signal() #: Emitted when a button is clicked clicked = Signal(QAbstractButton) class StandardButton(enum.IntEnum): NoButton, Ok, Close, Help = 0x0, 0x1, 0x2, 0x4 NoButton, Ok, Close, Help = list(StandardButton) class ButtonRole(enum.IntEnum): InvalidRole, AcceptRole, RejectRole, HelpRole = 0, 1, 2, 3 InvalidRole, AcceptRole, RejectRole, HelpRole = list(ButtonRole) _Button = namedtuple("_Button", ["button", "role", "stdbutton"]) def __init__(self, parent=None, icon=QIcon(), text="", wordWrap=False, textFormat=Qt.AutoText, standardButtons=NoButton, **kwargs): super().__init__(parent, **kwargs) self.__text = text self.__icon = QIcon() self.__wordWrap = wordWrap self.__standardButtons = MessageWidget.NoButton self._buttons = [] layout = QHBoxLayout() layout.setContentsMargins(8, 0, 8, 0) self.__iconlabel = QLabel(objectName="icon-label") self.__iconlabel.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.__textlabel = QLabel(objectName="text-label", text=text, wordWrap=wordWrap, textFormat=textFormat) if sys.platform == "darwin": self.__textlabel.setAttribute(Qt.WA_MacSmallSize) layout.addWidget(self.__iconlabel) layout.addWidget(self.__textlabel) self.setLayout(layout) self.setIcon(icon) self.setStandardButtons(standardButtons) def setText(self, text): """ Set the current message text. :type message: str """ if self.__text != text: self.__text = text self.__textlabel.setText(text) def text(self): """ Return the current message text. :rtype: str """ return self.__text def setIcon(self, icon): """ Set the message icon. :type icon: QIcon | QPixmap | QString | QStyle.StandardPixmap """ if isinstance(icon, QStyle.StandardPixmap): icon = self.style().standardIcon(icon) else: icon = QIcon(icon) if self.__icon != icon: self.__icon = QIcon(icon) if not self.__icon.isNull(): size = self.style().pixelMetric(QStyle.PM_SmallIconSize, None, self) pm = self.__icon.pixmap(QSize(size, size)) else: pm = QPixmap() self.__iconlabel.setPixmap(pm) self.__iconlabel.setVisible(not pm.isNull()) def icon(self): """ Return the current icon. :rtype: QIcon """ return QIcon(self.__icon) def setWordWrap(self, wordWrap): """ Set the message text wrap property :type wordWrap: bool """ if self.__wordWrap != wordWrap: self.__wordWrap = wordWrap self.__textlabel.setWordWrap(wordWrap) def wordWrap(self): """ Return the message text wrap property. :rtype: bool """ return self.__wordWrap def setTextFormat(self, textFormat): """ Set message text format :type textFormat: Qt.TextFormat """ self.__textlabel.setTextFormat(textFormat) def textFormat(self): """ Return the message text format. :rtype: Qt.TextFormat """ return self.__textlabel.textFormat() def changeEvent(self, event): # reimplemented if event.type() == 177: # QEvent.MacSizeChange: ... super().changeEvent(event) def setStandardButtons(self, buttons): for button in MessageWidget.StandardButton: existing = self.button(button) if button & buttons and existing is None: self.addButton(button) elif existing is not None: self.removeButton(existing) def standardButtons(self): return functools.reduce( operator.ior, (slot.stdbutton for slot in self._buttons if slot.stdbutton is not None), MessageWidget.NoButton) def addButton(self, button, *rolearg): """ addButton(QAbstractButton, ButtonRole) addButton(str, ButtonRole) addButton(StandardButton) Add and return a button """ stdbutton = None if isinstance(button, QAbstractButton): if len(rolearg) != 1: raise TypeError("Wrong number of arguments for " "addButton(QAbstractButton, role)") role = rolearg[0] elif isinstance(button, MessageWidget.StandardButton): if len(rolearg) != 0: raise TypeError("Wrong number of arguments for " "addButton(StandardButton)") stdbutton = button if button == MessageWidget.Ok: role = MessageWidget.AcceptRole button = QPushButton("Ok", default=False, autoDefault=False) elif button == MessageWidget.Close: role = MessageWidget.RejectRole # button = QPushButton( # default=False, autoDefault=False, flat=True, # icon=QIcon(self.style().standardIcon( # QStyle.SP_TitleBarCloseButton))) button = SimpleButton(icon=QIcon(self.style().standardIcon( QStyle.SP_TitleBarCloseButton))) elif button == MessageWidget.Help: role = MessageWidget.HelpRole button = QPushButton("Help", default=False, autoDefault=False) elif isinstance(button, str): if len(rolearg) != 1: raise TypeError("Wrong number of arguments for " "addButton(str, ButtonRole)") role = rolearg[0] button = QPushButton(button, default=False, autoDefault=False) if sys.platform == "darwin": button.setAttribute(Qt.WA_MacSmallSize) self._buttons.append(MessageWidget._Button(button, role, stdbutton)) button.clicked.connect(self._button_clicked) self._relayout() return button def removeButton(self, button): """ Remove a `button`. :type button: QAbstractButton """ slot = [s for s in self._buttons if s.button is button] if slot: slot = slot[0] self._buttons.remove(slot) self.layout().removeWidget(slot.button) slot.button.setParent(None) def buttonRole(self, button): """ Return the ButtonRole for button :type button: QAbstractButton """ for slot in self._buttons: if slot.button is button: return slot.role else: return MessageWidget.InvalidRole def button(self, standardButton): """ Return the button for the StandardButton. :type standardButton: StandardButton """ for slot in self._buttons: if slot.stdbutton == standardButton: return slot.button else: return None def _button_clicked(self): button = self.sender() role = self.buttonRole(button) self.clicked.emit(button) if role == MessageWidget.AcceptRole: self.accepted.emit() self.close() elif role == MessageWidget.RejectRole: self.rejected.emit() self.close() elif role == MessageWidget.HelpRole: self.helpRequested.emit() def _relayout(self): for slot in self._buttons: self.layout().removeWidget(slot.button) order = { MessageOverlayWidget.HelpRole: 0, MessageOverlayWidget.AcceptRole: 2, MessageOverlayWidget.RejectRole: 3, } orderd = sorted(self._buttons, key=lambda slot: order.get(slot.role, -1)) prev = self.__textlabel for slot in orderd: self.layout().addWidget(slot.button) QWidget.setTabOrder(prev, slot.button)
class OWFilter(widget.OWWidget): name = "Filter" icon = 'icons/Filter.svg' description = "Filter cells/genes" priority = 210 class Inputs: data = widget.Input("Data", Orange.data.Table) class Outputs: data = widget.Output("Data", Orange.data.Table) class Warning(widget.OWWidget.Warning): sampling_in_effect = widget.Msg("Too many data points to display.\n" "Sampling {} of {} data points.") class Error(widget.OWWidget.Error): invalid_range = widget.Msg( "Negative values in input data.\n" "This filter is only defined for non-negative values.") invalid_domain = widget.Msg("Invalid domain\n" "Domain contains non numeric columns.") #: Filter mode. #: Filter out rows/columns or 'zap' data values in range. Cells, Genes, Data = Cells, Genes, Data settings_version = 3 #: The selected filter mode selected_filter_type = settings.Setting(Cells) # type: int #: Selected filter statistics / QC measure indexed by filter_type selected_filter_metric = settings.Setting(TotalCounts) # type: int #: Augment the violin plot with a dot plot (strip plot) of the (non-zero) #: measurement counts in Cells/Genes mode or data matrix values in Data #: mode. display_dotplot = settings.Setting(True) # type: bool #: Is min/max range selection enable limit_lower_enabled = settings.Setting(True) # type: bool limit_upper_enabled = settings.Setting(True) # type: bool #: The lower and upper selection limit for each filter type thresholds = settings.Setting({ (Cells, DetectionCount): (0, 2**31 - 1), (Cells, TotalCounts): (0, 2**31 - 1), (Genes, DetectionCount): (0, 2**31 - 1), (Genes, TotalCounts): (0, 2**31 - 1), (Data, -1): (0.0, 2.0**31 - 1) }) # type: Dict[Tuple[int, int], Tuple[float, float]] #: Plot scale: 'Linear' or 'Log1p' scale = settings.Setting(Scale.Linear.name) # type: str auto_commit = settings.Setting(True) # type: bool def __init__(self): super().__init__() self.data = None # type: Optional[Orange.data.Table] self._state = None # type: Optional[_FilterData] box = gui.widgetBox(self.controlArea, "Info") self._info = QLabel(box, wordWrap=True) self._info.setText("No data in input\n") box.layout().addWidget(self._info) box = gui.widgetBox(self.controlArea, "Filter Type", spacing=-1) rbg = QButtonGroup(box, exclusive=True) layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) for id_ in [Cells, Genes, Data]: name, _, tip = FilterInfo[id_] b = QRadioButton(name, toolTip=tip, checked=id_ == self.selected_filter_type) rbg.addButton(b, id_) layout.addWidget(b, stretch=10, alignment=Qt.AlignCenter) box.layout().addLayout(layout) rbg.buttonClicked[int].connect(self.set_filter_type) self.filter_metric_cb = gui.comboBox( box, self, "selected_filter_metric", callback=self._update_metric, enabled=self.selected_filter_type != Data) for id_ in [DetectionCount, TotalCounts]: text, ttip = MeasureInfo[id_] self.filter_metric_cb.addItem(text) idx = self.filter_metric_cb.count() - 1 self.filter_metric_cb.setItemData(idx, ttip, Qt.ToolTipRole) self.filter_metric_cb.setCurrentIndex(self.selected_filter_metric) form = QFormLayout(labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft, fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow) self._filter_box = box = gui.widgetBox( self.controlArea, "Filter", orientation=form) # type: QGroupBox self.threshold_stacks = ( QStackedWidget(enabled=self.limit_lower_enabled), QStackedWidget(enabled=self.limit_upper_enabled), ) finfo = np.finfo(np.float64) for filter_ in [Cells, Genes, Data]: if filter_ in {Cells, Genes}: minimum = 0.0 ndecimals = 1 metric = self.selected_filter_metric else: minimum = finfo.min ndecimals = 3 metric = -1 spinlower = QDoubleSpinBox( self, minimum=minimum, maximum=finfo.max, decimals=ndecimals, keyboardTracking=False, ) spinupper = QDoubleSpinBox( self, minimum=minimum, maximum=finfo.max, decimals=ndecimals, keyboardTracking=False, ) lower, upper = self.thresholds.get((filter_, metric), (0, 0)) spinlower.setValue(lower) spinupper.setValue(upper) self.threshold_stacks[0].addWidget(spinlower) self.threshold_stacks[1].addWidget(spinupper) spinlower.valueChanged.connect(self._limitchanged) spinupper.valueChanged.connect(self._limitchanged) self.threshold_stacks[0].setCurrentIndex(self.selected_filter_type) self.threshold_stacks[1].setCurrentIndex(self.selected_filter_type) self.limit_lower_enabled_cb = cb = QCheckBox( "Min", checked=self.limit_lower_enabled) cb.toggled.connect(self.set_lower_limit_enabled) cb.setAttribute(Qt.WA_LayoutUsesWidgetRect, True) form.addRow(cb, self.threshold_stacks[0]) self.limit_upper_enabled_cb = cb = QCheckBox( "Max", checked=self.limit_upper_enabled) cb.toggled.connect(self.set_upper_limit_enabled) cb.setAttribute(Qt.WA_LayoutUsesWidgetRect, True) form.addRow(cb, self.threshold_stacks[1]) box = gui.widgetBox(self.controlArea, "Plot Options") self._showpoints = gui.checkBox(box, self, "display_dotplot", "Show data points", callback=self._update_dotplot) self.log_scale_cb = QCheckBox("Log scale", checked=self.scale == Scale.Log1p.name) self.log_scale_cb.toggled[bool].connect( lambda state: self.set_filter_scale(Scale.Log1p if state else Scale.Linear)) box.layout().addWidget(self.log_scale_cb) self.controlArea.layout().addStretch(10) gui.auto_commit(self.controlArea, self, "auto_commit", "Commit") self._view = pg.GraphicsView() self._view.enableMouse(False) self._view.setAntialiasing(True) self._plot = plot = ViolinPlot() self._plot.setDataPointsVisible(self.display_dotplot) self._plot.setSelectionMode( (ViolinPlot.Low if self.limit_lower_enabled else 0) | (ViolinPlot.High if self.limit_upper_enabled else 0)) self._plot.setRange(QRectF(-1., 0., 2., 1.)) self._plot.selectionEdited.connect(self._limitchanged_plot) self._view.setCentralWidget(self._plot) bottom = self._plot.getAxis("bottom") # type: pg.AxisItem bottom.hide() plot.setMouseEnabled(False, False) plot.hideButtons() self.mainArea.layout().addWidget(self._view) # Coalescing commit timer self._committimer = QTimer(self, singleShot=True) self._committimer.timeout.connect(self.commit) self.addAction( QAction("Select All", self, shortcut=QKeySequence.SelectAll, triggered=self._select_all)) self._setup_axes() def sizeHint(self): sh = super().sizeHint() # type: QSize return sh.expandedTo(QSize(800, 600)) def set_filter_type(self, type_): if self.selected_filter_type != type_: assert type_ in (Cells, Genes, Data), str(type_) self.selected_filter_type = type_ self.threshold_stacks[0].setCurrentIndex(type_) self.threshold_stacks[1].setCurrentIndex(type_) self.filter_metric_cb.setEnabled(type_ != Data) self._setup_axes() if self.data is not None: self._setup(self.data, type_) self._schedule_commit() def filter_type(self): return self.selected_filter_type def set_filter_scale(self, scale): # type: (Scale) -> None if self.scale != scale: self.scale = scale.name self.log_scale_cb.setChecked(scale == Scale.Log1p) self._update_scale() def filter_scale(self): return Scale[self.scale] def _update_metric(self): self._update_scale() if self.data is not None: self._setup( self.data, self.selected_filter_type, ) def set_upper_limit_enabled(self, enabled): if enabled != self.limit_upper_enabled: self.limit_upper_enabled = enabled self.threshold_stacks[1].setEnabled(enabled) self.limit_upper_enabled_cb.setChecked(enabled) self._update_filter() self._schedule_commit() def set_lower_limit_enabled(self, enabled): if enabled != self.limit_lower_enabled: self.limit_lower_enabled = enabled self.threshold_stacks[0].setEnabled(enabled) self.limit_lower_enabled_cb.setChecked(enabled) self._update_filter() self._schedule_commit() def _update_filter(self): mode = 0 if self.limit_lower_enabled: mode |= ViolinPlot.Low if self.limit_upper_enabled: mode |= ViolinPlot.High self._plot.setSelectionMode(mode) self._update_info() self._schedule_commit() def _is_filter_enabled(self): return self.limit_lower_enabled or self.limit_upper_enabled @Inputs.data def set_data(self, data): # type: (Optional[Orange.data.Table]) -> None self.clear() if data is not None and \ any(type(v) is not Orange.data.ContinuousVariable for v in data.domain.attributes): self.Error.invalid_domain() data = None if data is not None and np.any(data.X < 0): self.Error.invalid_range() data = None self.data = data if data is not None: self._setup(data, self.filter_type()) self.unconditional_commit() def clear(self): self.data = None self._state = None self._plot.clear() # reset the plot range self._plot.setRange(QRectF(-1., 0., 2., 1.)) self._update_info() self.Warning.clear() self.Error.clear() def _update_info(self): text = [] if self.data is None: text += ["No data on input.\n"] else: N, M = len(self.data), len(self.data.domain.attributes) text = [] text += [ "Data with {N} cell{Np} and {M} gene{Mp}".format( N=N, Np="s" if N != 1 else "", M=M, Mp="s" if N != 1 else "") ] if self._is_filter_enabled() and \ self.filter_type() in [Cells, Genes]: counts = self._state.x mask = np.ones(counts.shape, dtype=bool) if self.limit_lower_enabled: mask &= self.limit_lower <= counts if self.limit_upper_enabled: mask &= counts <= self.limit_upper n = np.count_nonzero(mask) subject = "cell" if self.filter_type() == Cells else "gene" if n == 0: text += ["All {}s filtered out".format(subject)] else: text += [ "{} {subject}{s} in selection".format( n, subject=subject, s="s" if n != 1 else "") ] else: text += [""] self._info.setText("\n".join(text)) def _select_all(self): self.limit_lower = 0 self.limit_upper = 2**31 - 1 self._limitchanged() def _setup_axes(self): # Setup the plot axes and title filter_type = self.filter_type() info = FilterInfo[filter_type] _, title, _, *_ = info if filter_type in [Cells, Genes]: measure = self.selected_filter_metric else: measure = None if filter_type == Cells and measure == TotalCounts: axis_label = "Total counts (library size)" elif filter_type == Cells and measure == DetectionCount: axis_label = "Number of expressed genes" elif filter_type == Genes and measure == TotalCounts: axis_label = "Total counts" elif filter_type == Genes and measure == DetectionCount: # TODO: Too long axis_label = "Number of cells a gene is expressed in" elif filter_type == Data: axis_label = "Gene Expression" ax = self._plot.getAxis("left") if self.filter_scale() == Scale.Log1p: axis_label = "1 + '{}' <i>(in log scale)</i>".format(axis_label) ax.setLabel(axis_label) ax.setLogMode(True) else: ax.setLogMode(False) ax.setLabel(axis_label) # Reset the tick text area width ax.textWidth = 30 ax.setWidth(None) self._plot.setTitle(title) def _setup(self, data, filter_type): self._plot.clear() self._state = None self._setup_axes() span = -1.0 # data span measure = self.selected_filter_metric if filter_type != Data else None state = _FilterData() if filter_type in [Cells, Genes]: if filter_type == Cells: axis = 1 else: axis = 0 if measure == TotalCounts: counts = np.nansum(data.X, axis=axis) else: mask = (data.X != 0) & (np.isfinite(data.X)) counts = np.count_nonzero(mask, axis=axis) x = counts self.Warning.sampling_in_effect.clear() elif filter_type == Data: x = data.X.ravel() x = x[np.isfinite(x)] x = x[x != 0] MAX_DISPLAY_SIZE = 20000 if x.size > MAX_DISPLAY_SIZE: self.Warning.sampling_in_effect(MAX_DISPLAY_SIZE, x.size) # tails to preserve exactly tails = 1 assert x.flags.owndata x.partition(tails - 1) xrest = x[tails:] xrest.partition(xrest.size - tails) x1, x2, x3 = x[:tails], x[tails:x.size - tails], x[x.size - tails:] assert x1.size + x2.size + x3.size == x.size x2 = np.random.RandomState(0x667).choice( x2, size=MAX_DISPLAY_SIZE - 2 * tails, replace=False, ) x = np.r_[x1, x2, x3] else: self.Warning.sampling_in_effect.clear() else: assert False state.x = x scale = self.filter_scale() if scale == Scale.Log1p: scale_transform = log1p scale_transform_inv = expm1 else: scale_transform = lambda x: x scale_transform_inv = scale_transform state.transform = scale_transform state.transform_inv = scale_transform_inv if x.size: xmin, xmax = np.min(x), np.max(x) else: xmin = xmax = 0., 1. state.xmin, state.xmax = xmin, xmax xs = scale_transform(x) xs = xs[np.isfinite(xs)] state.xt = xs if xs.size: xsmin, xsmax = np.min(xs), np.max(xs) # find effective xmin, xmax (valid in both original and transformed # space xmin_, xmax_ = scale_transform_inv([xsmin, xsmax]) xmin, xmax = max(xmin, xmin_), min(xmax, xmax_) lower = np.clip(self.limit_lower, xmin, xmax) upper = np.clip(self.limit_upper, xmin, xmax) else: xmin, xmax = 0., 1. lower, upper = 0., 1. state.xtmin, state.xtmax = xsmin, xsmax spinlow = self.threshold_stacks[0].widget(filter_type) spinhigh = self.threshold_stacks[1].widget(filter_type) if filter_type == Data or measure == TotalCounts: span = xmax - xmin if span > 0: ndecimals = max(4 - int(np.floor(np.log10(span))), 1) else: ndecimals = 1 else: ndecimals = 1 # Round effective bounds (spin <=> plot cut lines) lower = round(lower, ndecimals) upper = round(upper, ndecimals) if xs.size > 0: # TODO: Need correction for lower bounded distribution (counts) # Use reflection around 0, but gaussian_kde does not provide # sufficient flexibility w.r.t bandwidth selection. self._plot.setData(xs, 1000) self._plot.setBoundary(*scale_transform([lower, upper])) spinlow.setDecimals(ndecimals) self.limit_lower = lower spinhigh.setDecimals(ndecimals) self.limit_upper = upper self._state = state self._update_info() def _update_dotplot(self): self._plot.setDataPointsVisible(self.display_dotplot) def current_filter_thresholds(self): if self.selected_filter_type in {Cells, Genes}: metric = self.selected_filter_metric else: metric = -1 return self.thresholds[self.selected_filter_type, metric] def set_current_filter_thesholds(self, lower, upper): if self.selected_filter_type in {Cells, Genes}: metric = self.selected_filter_metric else: metric = -1 self.thresholds[self.selected_filter_type, metric] = (lower, upper) def _update_scale(self): self._setup_axes() if self.data is not None: self._setup(self.data, self.filter_type()) @property def limit_lower(self): return self.current_filter_thresholds()[0] @limit_lower.setter def limit_lower(self, value): _, upper = self.current_filter_thresholds() self.set_current_filter_thesholds(value, upper) stacklower, _ = self.threshold_stacks sb = stacklower.widget(self.selected_filter_type) # prevent changes due to spin box rounding sb.setValue(value) @property def limit_upper(self): return self.current_filter_thresholds()[1] @limit_upper.setter def limit_upper(self, value): lower, _ = self.current_filter_thresholds() self.set_current_filter_thesholds(lower, value) _, stackupper = self.threshold_stacks sb = stackupper.widget(self.selected_filter_type) sb.setValue(value) @Slot() def _limitchanged(self): # Low/high limit changed via the spin boxes stacklow, stackhigh = self.threshold_stacks filter_ = self.selected_filter_type lower = stacklow.widget(filter_).value() upper = stackhigh.widget(filter_).value() self.set_current_filter_thesholds(lower, upper) state = self._state if state is not None and state.x.size: xmin, xmax = state.xmin, state.xmax lower = np.clip(lower, xmin, xmax) upper = np.clip(upper, xmin, xmax) lower, upper = state.transform([lower, upper]) self._plot.setBoundary(lower, upper) # TODO: Only when the actual selection/filter mask changes self._schedule_commit() self._update_info() def _limitchanged_plot(self): # Low/high limit changed via the plot if self._state is not None: state = self._state newlower_, newupper_ = self._plot.boundary() newlower, newupper = state.transform_inv([newlower_, newupper_]) filter_ = self.selected_filter_type lower, upper = self.current_filter_thresholds() stacklow, stackhigh = self.threshold_stacks spin_lower = stacklow.widget(filter_) spin_upper = stackhigh.widget(filter_) # do rounding to match the spin box's precision if self.limit_lower_enabled: newlower = round(newlower, spin_lower.decimals()) else: newlower = lower if self.limit_upper_enabled: newupper = round(newupper, spin_upper.decimals()) else: newupper = upper if self.limit_lower_enabled and newlower != lower: self.limit_lower = newlower if self.limit_upper_enabled and newupper != upper: self.limit_upper = newupper newlower_, newupper_ = state.transform([newlower, newupper]) self._plot.setBoundary(newlower_, newupper_) # TODO: Only when the actual selection/filter mask changes self._schedule_commit() self._update_info() def _schedule_commit(self): self._committimer.start() def commit(self): self._committimer.stop() data = self.data if data is not None and self._is_filter_enabled(): if self.filter_type() in [Cells, Genes]: state = self._state assert state is not None counts = state.x cmax = self.limit_upper cmin = self.limit_lower mask = np.ones(counts.shape, dtype=bool) if self.limit_lower_enabled: mask &= cmin <= counts if self.limit_upper_enabled: mask &= counts <= cmax if self.filter_type() == Cells: assert counts.size == len(data) data = data[mask] else: assert counts.size == len(data.domain.attributes) atts = [ v for v, m in zip(data.domain.attributes, mask) if m ] data = data.from_table( Orange.data.Domain(atts, data.domain.class_vars, data.domain.metas), data) if len(data) == 0 or \ len(data.domain) + len(data.domain.metas) == 0: data = None elif self.filter_type() == Data: dmin, dmax = self.limit_lower, self.limit_upper data = data.copy() assert data.X.base is None mask = None if self.limit_lower_enabled: mask = data.X < dmin if self.limit_upper_enabled: if mask is not None: mask |= data.X > dmax else: mask = data.X < dmax data.X[mask] = 0.0 else: assert False self.Outputs.data.send(data) def onDeleteWidget(self): self.clear() self._plot.close() super().onDeleteWidget() @classmethod def migrate_settings(cls, settings, version): if (version is None or version < 2) and \ ("limit_lower" in settings and "limit_upper" in settings): # v2 changed limit_lower, limit_upper to per filter limits stored # in a single dict lower = settings.pop("limit_lower") upper = settings.pop("limit_upper") settings["thresholds"] = { (Cells, TotalCounts): (lower, upper), (Cells, DetectionCount): (lower, upper), (Genes, TotalCounts): (lower, upper), (Genes, DetectionCount): (lower, upper), (Data, -1): (lower, upper), } if version == 2: thresholds = settings["thresholds"] c = thresholds.pop(Cells) g = thresholds.pop(Genes) d = thresholds.pop(Data) thresholds = { (Cells, TotalCounts): c, (Cells, DetectionCount): c, (Genes, TotalCounts): g, (Genes, DetectionCount): g, (Data, -1): d, } settings["thresholds"] = thresholds
class OWDataSets(OWWidget): name = "Datasets" description = "Load a dataset from an online repository" icon = "icons/DataSets.svg" priority = 20 replaces = ["orangecontrib.prototypes.widgets.owdatasets.OWDataSets"] keywords = ["online"] # The following constants can be overridden in a subclass # to reuse this widget for a different repository # Take care when refactoring! (used in e.g. single-cell) INDEX_URL = "https://datasets.biolab.si/" DATASET_DIR = "datasets" # override HEADER_SCHEMA to define new columns # if schema is changed override methods: self.assign_delegates and # self.create_model HEADER_SCHEMA = [['islocal', { 'label': '' }], ['title', { 'label': 'Title' }], ['size', { 'label': 'Size' }], ['instances', { 'label': 'Instances' }], ['variables', { 'label': 'Variables' }], ['target', { 'label': 'Target' }], ['tags', { 'label': 'Tags' }]] # type: List[str, dict] class Error(OWWidget.Error): no_remote_datasets = Msg("Could not fetch dataset list") class Warning(OWWidget.Warning): only_local_datasets = Msg("Could not fetch datasets list, only local " "cached datasets are shown") class Outputs: data = Output("Data", Orange.data.Table) #: Selected dataset id selected_id = settings.Setting(None) # type: Optional[str] auto_commit = settings.Setting(False) # type: bool #: main area splitter state splitter_state = settings.Setting(b'') # type: bytes header_state = settings.Setting(b'') # type: bytes def __init__(self): super().__init__() self.allinfo_local = {} self.allinfo_remote = {} self.local_cache_path = os.path.join(data_dir(), self.DATASET_DIR) self._header_labels = [ header['label'] for _, header in self.HEADER_SCHEMA ] self._header_index = namedtuple( '_header_index', [info_tag for info_tag, _ in self.HEADER_SCHEMA]) self.Header = self._header_index( *[index for index, _ in enumerate(self._header_labels)]) self.__awaiting_state = None # type: Optional[_FetchState] box = gui.widgetBox(self.controlArea, "Info") self.infolabel = QLabel(text="Initializing...\n\n") box.layout().addWidget(self.infolabel) gui.widgetLabel(self.mainArea, "Filter") self.filterLineEdit = QLineEdit(textChanged=self.filter) self.mainArea.layout().addWidget(self.filterLineEdit) self.splitter = QSplitter(orientation=Qt.Vertical) self.view = QTreeView( sortingEnabled=True, selectionMode=QTreeView.SingleSelection, alternatingRowColors=True, rootIsDecorated=False, editTriggers=QTreeView.NoEditTriggers, uniformRowHeights=True, ) # the method doesn't exists yet, pylint: disable=unnecessary-lambda self.view.doubleClicked.connect(lambda: self.unconditional_commit()) box = gui.widgetBox(self.splitter, "Description", addToLayout=False) self.descriptionlabel = QLabel( wordWrap=True, textFormat=Qt.RichText, ) self.descriptionlabel = QTextBrowser( openExternalLinks=True, textInteractionFlags=(Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse)) self.descriptionlabel.setFrameStyle(QTextBrowser.NoFrame) # no (white) text background self.descriptionlabel.viewport().setAutoFillBackground(False) box.layout().addWidget(self.descriptionlabel) self.splitter.addWidget(self.view) self.splitter.addWidget(box) self.splitter.setSizes([300, 200]) self.splitter.splitterMoved.connect(lambda: setattr( self, "splitter_state", bytes(self.splitter.saveState()))) self.mainArea.layout().addWidget(self.splitter) self.controlArea.layout().addStretch(10) gui.auto_commit(self.controlArea, self, "auto_commit", "Send Data") proxy = QSortFilterProxyModel() proxy.setFilterKeyColumn(-1) proxy.setFilterCaseSensitivity(False) self.view.setModel(proxy) if self.splitter_state: self.splitter.restoreState(self.splitter_state) self.assign_delegates() self.setBlocking(True) self.setStatusMessage("Initializing") self._executor = ThreadPoolExecutor(max_workers=1) f = self._executor.submit(self.list_remote) w = FutureWatcher(f, parent=self) w.done.connect(self.__set_index) def assign_delegates(self): # NOTE: All columns must have size hinting delegates. # QTreeView queries only the columns displayed in the viewport so # the layout would be different depending in the horizontal scroll # position self.view.setItemDelegate(UniformHeightDelegate(self)) self.view.setItemDelegateForColumn( self.Header.islocal, UniformHeightIndicatorDelegate(self, role=Qt.DisplayRole)) self.view.setItemDelegateForColumn(self.Header.size, SizeDelegate(self)) self.view.setItemDelegateForColumn(self.Header.instances, NumericalDelegate(self)) self.view.setItemDelegateForColumn(self.Header.variables, NumericalDelegate(self)) self.view.resizeColumnToContents(self.Header.islocal) def _parse_info(self, file_path): if file_path in self.allinfo_remote: info = self.allinfo_remote[file_path] else: info = self.allinfo_local[file_path] islocal = file_path in self.allinfo_local isremote = file_path in self.allinfo_remote outdated = islocal and isremote and ( self.allinfo_remote[file_path].get('version', '') != self.allinfo_local[file_path].get('version', '')) islocal &= not outdated prefix = os.path.join('', *file_path[:-1]) filename = file_path[-1] return Namespace(file_path=file_path, prefix=prefix, filename=filename, islocal=islocal, outdated=outdated, **info) def create_model(self): allkeys = set(self.allinfo_local) | set(self.allinfo_remote) allkeys = sorted(allkeys) model = QStandardItemModel(self) model.setHorizontalHeaderLabels(self._header_labels) current_index = -1 for i, file_path in enumerate(allkeys): datainfo = self._parse_info(file_path) item1 = QStandardItem() item1.setData(" " if datainfo.islocal else "", Qt.DisplayRole) item1.setData(datainfo, Qt.UserRole) item2 = QStandardItem(datainfo.title) item3 = QStandardItem() item3.setData(datainfo.size, Qt.DisplayRole) item4 = QStandardItem() item4.setData(datainfo.instances, Qt.DisplayRole) item5 = QStandardItem() item5.setData(datainfo.variables, Qt.DisplayRole) item6 = QStandardItem() item6.setData(datainfo.target, Qt.DisplayRole) if datainfo.target: item6.setIcon(variable_icon(datainfo.target)) item7 = QStandardItem() item7.setData(", ".join(datainfo.tags) if datainfo.tags else "", Qt.DisplayRole) row = [item1, item2, item3, item4, item5, item6, item7] model.appendRow(row) if os.path.join(*file_path) == self.selected_id: current_index = i return model, current_index @Slot(object) def __set_index(self, f): # type: (Future) -> None # set results from `list_remote` query. assert QThread.currentThread() is self.thread() assert f.done() self.setBlocking(False) self.setStatusMessage("") self.allinfo_local = self.list_local() try: self.allinfo_remote = f.result() except Exception: # anytying can happen, pylint: disable=broad-except log.exception("Error while fetching updated index") if not self.allinfo_local: self.Error.no_remote_datasets() else: self.Warning.only_local_datasets() self.allinfo_remote = {} model, current_index = self.create_model() self.view.model().setSourceModel(model) self.view.selectionModel().selectionChanged.connect( self.__on_selection) self.view.resizeColumnToContents(0) self.view.setColumnWidth( 1, min(self.view.sizeHintForColumn(1), self.view.fontMetrics().width("X" * 37))) header = self.view.header() header.restoreState(self.header_state) # Update the info text self.infolabel.setText( format_info(model.rowCount(), len(self.allinfo_local))) if current_index != -1: selmodel = self.view.selectionModel() selmodel.select( self.view.model().mapFromSource(model.index(current_index, 0)), QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows) def __update_cached_state(self): model = self.view.model().sourceModel() localinfo = self.list_local() assert isinstance(model, QStandardItemModel) allinfo = [] for i in range(model.rowCount()): item = model.item(i, 0) info = item.data(Qt.UserRole) info.islocal = info.file_path in localinfo item.setData(" " if info.islocal else "", Qt.DisplayRole) allinfo.append(info) self.infolabel.setText( format_info(model.rowCount(), sum(info.islocal for info in allinfo))) def selected_dataset(self): """ Return the current selected dataset info or None if not selected Returns ------- info : Optional[Namespace] """ rows = self.view.selectionModel().selectedRows(0) assert 0 <= len(rows) <= 1 current = rows[0] if rows else None # type: Optional[QModelIndex] if current is not None: info = current.data(Qt.UserRole) assert isinstance(info, Namespace) else: info = None return info def filter(self): filter_string = self.filterLineEdit.text().strip() proxyModel = self.view.model() if proxyModel: proxyModel.setFilterFixedString(filter_string) def __on_selection(self): # Main datasets view selection has changed rows = self.view.selectionModel().selectedRows(0) assert 0 <= len(rows) <= 1 current = rows[0] if rows else None # type: Optional[QModelIndex] if current is not None: current = self.view.model().mapToSource(current) di = current.data(Qt.UserRole) text = description_html(di) self.descriptionlabel.setText(text) self.selected_id = os.path.join(di.prefix, di.filename) else: self.descriptionlabel.setText("") self.selected_id = None self.commit() def commit(self): """ Commit a dataset to the output immediately (if available locally) or schedule download background and an eventual send. During the download the widget is in blocking state (OWWidget.isBlocking) """ di = self.selected_dataset() if di is not None: self.Error.clear() if self.__awaiting_state is not None: # disconnect from the __commit_complete self.__awaiting_state.watcher.done.disconnect( self.__commit_complete) # .. and connect to update_cached_state # self.__awaiting_state.watcher.done.connect( # self.__update_cached_state) # TODO: There are possible pending __progress_advance queued self.__awaiting_state.pb.advance.disconnect( self.__progress_advance) self.progressBarFinished(processEvents=None) self.__awaiting_state = None if not di.islocal: pr = progress() callback = lambda pr=pr: pr.advance.emit() pr.advance.connect(self.__progress_advance, Qt.QueuedConnection) self.progressBarInit(processEvents=None) self.setStatusMessage("Fetching...") self.setBlocking(True) f = self._executor.submit(ensure_local, self.INDEX_URL, di.file_path, self.local_cache_path, force=di.outdated, progress_advance=callback) w = FutureWatcher(f, parent=self) w.done.connect(self.__commit_complete) self.__awaiting_state = _FetchState(f, w, pr) else: self.setStatusMessage("") self.setBlocking(False) self.commit_cached(di.file_path) else: self.Outputs.data.send(None) @Slot(object) def __commit_complete(self, f): # complete the commit operation after the required file has been # downloaded assert QThread.currentThread() is self.thread() assert self.__awaiting_state is not None assert self.__awaiting_state.future is f if self.isBlocking(): self.progressBarFinished(processEvents=None) self.setBlocking(False) self.setStatusMessage("") self.__awaiting_state = None try: path = f.result() # anything can happen here, pylint: disable=broad-except except Exception as ex: log.exception("Error:") self.error(format_exception(ex)) path = None self.__update_cached_state() if path is not None: data = self.load_data(path) else: data = None self.Outputs.data.send(data) def commit_cached(self, file_path): path = LocalFiles(self.local_cache_path).localpath(*file_path) self.Outputs.data.send(self.load_data(path)) @Slot() def __progress_advance(self): assert QThread.currentThread() is self.thread() self.progressBarAdvance(1, processEvents=None) def onDeleteWidget(self): super().onDeleteWidget() if self.__awaiting_state is not None: self.__awaiting_state.watcher.done.disconnect( self.__commit_complete) self.__awaiting_state.pb.advance.disconnect( self.__progress_advance) self.__awaiting_state = None @staticmethod def sizeHint(): return QSize(1100, 500) def closeEvent(self, event): self.splitter_state = bytes(self.splitter.saveState()) self.header_state = bytes(self.view.header().saveState()) super().closeEvent(event) def load_data(self, path): # pylint: disable=no-self-use return Orange.data.Table(path) def list_remote(self): # type: () -> Dict[Tuple[str, ...], dict] client = ServerFiles(server=self.INDEX_URL) return client.allinfo() def list_local(self): # type: () -> Dict[Tuple[str, ...], dict] return LocalFiles(self.local_cache_path).allinfo()
class OWDataSets(OWWidget): name = "Datasets" description = "Load a dataset from an online repository" icon = "icons/DataSets.svg" priority = 20 replaces = ["orangecontrib.prototypes.widgets.owdatasets.OWDataSets"] keywords = ["online"] # The following constants can be overridden in a subclass # to reuse this widget for a different repository # Take care when refactoring! (used in e.g. single-cell) INDEX_URL = "https://datasets.biolab.si/" DATASET_DIR = "datasets" # override HEADER_SCHEMA to define new columns # if schema is changed override methods: self.assign_delegates and # self.create_model HEADER_SCHEMA = [ ['islocal', {'label': ''}], ['title', {'label': 'Title'}], ['size', {'label': 'Size'}], ['instances', {'label': 'Instances'}], ['variables', {'label': 'Variables'}], ['target', {'label': 'Target'}], ['tags', {'label': 'Tags'}] ] # type: List[str, dict] class Error(OWWidget.Error): no_remote_datasets = Msg("Could not fetch dataset list") class Warning(OWWidget.Warning): only_local_datasets = Msg("Could not fetch datasets list, only local " "cached datasets are shown") class Outputs: data = Output("Data", Orange.data.Table) #: Selected dataset id selected_id = settings.Setting(None) # type: Optional[str] auto_commit = settings.Setting(False) # type: bool #: main area splitter state splitter_state = settings.Setting(b'') # type: bytes header_state = settings.Setting(b'') # type: bytes def __init__(self): super().__init__() self.allinfo_local = {} self.allinfo_remote = {} self.local_cache_path = os.path.join(data_dir(), self.DATASET_DIR) self._header_labels = [ header['label'] for _, header in self.HEADER_SCHEMA] self._header_index = namedtuple( '_header_index', [info_tag for info_tag, _ in self.HEADER_SCHEMA]) self.Header = self._header_index( *[index for index, _ in enumerate(self._header_labels)]) self.__awaiting_state = None # type: Optional[_FetchState] box = gui.widgetBox(self.controlArea, "Info") self.infolabel = QLabel(text="Initializing...\n\n") box.layout().addWidget(self.infolabel) gui.widgetLabel(self.mainArea, "Filter") self.filterLineEdit = QLineEdit( textChanged=self.filter ) self.mainArea.layout().addWidget(self.filterLineEdit) self.splitter = QSplitter(orientation=Qt.Vertical) self.view = QTreeView( sortingEnabled=True, selectionMode=QTreeView.SingleSelection, alternatingRowColors=True, rootIsDecorated=False, editTriggers=QTreeView.NoEditTriggers, uniformRowHeights=True, ) # the method doesn't exists yet, pylint: disable=unnecessary-lambda self.view.doubleClicked.connect(lambda: self.unconditional_commit()) box = gui.widgetBox(self.splitter, "Description", addToLayout=False) self.descriptionlabel = QLabel( wordWrap=True, textFormat=Qt.RichText, ) self.descriptionlabel = QTextBrowser( openExternalLinks=True, textInteractionFlags=(Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse) ) self.descriptionlabel.setFrameStyle(QTextBrowser.NoFrame) # no (white) text background self.descriptionlabel.viewport().setAutoFillBackground(False) box.layout().addWidget(self.descriptionlabel) self.splitter.addWidget(self.view) self.splitter.addWidget(box) self.splitter.setSizes([300, 200]) self.splitter.splitterMoved.connect( lambda: setattr(self, "splitter_state", bytes(self.splitter.saveState())) ) self.mainArea.layout().addWidget(self.splitter) self.controlArea.layout().addStretch(10) gui.auto_commit(self.controlArea, self, "auto_commit", "Send Data") proxy = QSortFilterProxyModel() proxy.setFilterKeyColumn(-1) proxy.setFilterCaseSensitivity(False) self.view.setModel(proxy) if self.splitter_state: self.splitter.restoreState(self.splitter_state) self.assign_delegates() self.setBlocking(True) self.setStatusMessage("Initializing") self._executor = ThreadPoolExecutor(max_workers=1) f = self._executor.submit(self.list_remote) w = FutureWatcher(f, parent=self) w.done.connect(self.__set_index) def assign_delegates(self): # NOTE: All columns must have size hinting delegates. # QTreeView queries only the columns displayed in the viewport so # the layout would be different depending in the horizontal scroll # position self.view.setItemDelegate(UniformHeightDelegate(self)) self.view.setItemDelegateForColumn( self.Header.islocal, UniformHeightIndicatorDelegate(self, role=Qt.DisplayRole) ) self.view.setItemDelegateForColumn( self.Header.size, SizeDelegate(self) ) self.view.setItemDelegateForColumn( self.Header.instances, NumericalDelegate(self) ) self.view.setItemDelegateForColumn( self.Header.variables, NumericalDelegate(self) ) self.view.resizeColumnToContents(self.Header.islocal) def _parse_info(self, file_path): if file_path in self.allinfo_remote: info = self.allinfo_remote[file_path] else: info = self.allinfo_local[file_path] islocal = file_path in self.allinfo_local isremote = file_path in self.allinfo_remote outdated = islocal and isremote and ( self.allinfo_remote[file_path].get('version', '') != self.allinfo_local[file_path].get('version', '') ) islocal &= not outdated prefix = os.path.join('', *file_path[:-1]) filename = file_path[-1] return Namespace(file_path=file_path, prefix=prefix, filename=filename, islocal=islocal, outdated=outdated, **info) def create_model(self): allkeys = set(self.allinfo_local) | set(self.allinfo_remote) allkeys = sorted(allkeys) model = QStandardItemModel(self) model.setHorizontalHeaderLabels(self._header_labels) current_index = -1 for i, file_path in enumerate(allkeys): datainfo = self._parse_info(file_path) item1 = QStandardItem() item1.setData(" " if datainfo.islocal else "", Qt.DisplayRole) item1.setData(datainfo, Qt.UserRole) item2 = QStandardItem(datainfo.title) item3 = QStandardItem() item3.setData(datainfo.size, Qt.DisplayRole) item4 = QStandardItem() item4.setData(datainfo.instances, Qt.DisplayRole) item5 = QStandardItem() item5.setData(datainfo.variables, Qt.DisplayRole) item6 = QStandardItem() item6.setData(datainfo.target, Qt.DisplayRole) if datainfo.target: item6.setIcon(variable_icon(datainfo.target)) item7 = QStandardItem() item7.setData(", ".join(datainfo.tags) if datainfo.tags else "", Qt.DisplayRole) row = [item1, item2, item3, item4, item5, item6, item7] model.appendRow(row) if os.path.join(*file_path) == self.selected_id: current_index = i return model, current_index @Slot(object) def __set_index(self, f): # type: (Future) -> None # set results from `list_remote` query. assert QThread.currentThread() is self.thread() assert f.done() self.setBlocking(False) self.setStatusMessage("") self.allinfo_local = self.list_local() try: self.allinfo_remote = f.result() except Exception: # anytying can happen, pylint: disable=broad-except log.exception("Error while fetching updated index") if not self.allinfo_local: self.Error.no_remote_datasets() else: self.Warning.only_local_datasets() self.allinfo_remote = {} model, current_index = self.create_model() self.view.model().setSourceModel(model) self.view.selectionModel().selectionChanged.connect( self.__on_selection ) self.view.resizeColumnToContents(0) self.view.setColumnWidth( 1, min(self.view.sizeHintForColumn(1), self.view.fontMetrics().width("X" * 24))) header = self.view.header() header.restoreState(self.header_state) # Update the info text self.infolabel.setText( format_info(model.rowCount(), len(self.allinfo_local))) if current_index != -1: selmodel = self.view.selectionModel() selmodel.select( self.view.model().mapFromSource(model.index(current_index, 0)), QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows) def __update_cached_state(self): model = self.view.model().sourceModel() localinfo = self.list_local() assert isinstance(model, QStandardItemModel) allinfo = [] for i in range(model.rowCount()): item = model.item(i, 0) info = item.data(Qt.UserRole) info.islocal = info.file_path in localinfo item.setData(" " if info.islocal else "", Qt.DisplayRole) allinfo.append(info) self.infolabel.setText(format_info( model.rowCount(), sum(info.islocal for info in allinfo))) def selected_dataset(self): """ Return the current selected dataset info or None if not selected Returns ------- info : Optional[Namespace] """ rows = self.view.selectionModel().selectedRows(0) assert 0 <= len(rows) <= 1 current = rows[0] if rows else None # type: Optional[QModelIndex] if current is not None: info = current.data(Qt.UserRole) assert isinstance(info, Namespace) else: info = None return info def filter(self): filter_string = self.filterLineEdit.text().strip() proxyModel = self.view.model() if proxyModel: proxyModel.setFilterFixedString(filter_string) def __on_selection(self): # Main datasets view selection has changed rows = self.view.selectionModel().selectedRows(0) assert 0 <= len(rows) <= 1 current = rows[0] if rows else None # type: Optional[QModelIndex] if current is not None: current = self.view.model().mapToSource(current) di = current.data(Qt.UserRole) text = description_html(di) self.descriptionlabel.setText(text) self.selected_id = os.path.join(di.prefix, di.filename) else: self.descriptionlabel.setText("") self.selected_id = None self.commit() def commit(self): """ Commit a dataset to the output immediately (if available locally) or schedule download background and an eventual send. During the download the widget is in blocking state (OWWidget.isBlocking) """ di = self.selected_dataset() if di is not None: self.Error.clear() if self.__awaiting_state is not None: # disconnect from the __commit_complete self.__awaiting_state.watcher.done.disconnect( self.__commit_complete) # .. and connect to update_cached_state # self.__awaiting_state.watcher.done.connect( # self.__update_cached_state) # TODO: There are possible pending __progress_advance queued self.__awaiting_state.pb.advance.disconnect( self.__progress_advance) self.progressBarFinished(processEvents=None) self.__awaiting_state = None if not di.islocal: pr = progress() callback = lambda pr=pr: pr.advance.emit() pr.advance.connect(self.__progress_advance, Qt.QueuedConnection) self.progressBarInit(processEvents=None) self.setStatusMessage("Fetching...") self.setBlocking(True) f = self._executor.submit( ensure_local, self.INDEX_URL, di.file_path, self.local_cache_path, force=di.outdated, progress_advance=callback) w = FutureWatcher(f, parent=self) w.done.connect(self.__commit_complete) self.__awaiting_state = _FetchState(f, w, pr) else: self.setStatusMessage("") self.setBlocking(False) self.commit_cached(di.file_path) else: self.Outputs.data.send(None) @Slot(object) def __commit_complete(self, f): # complete the commit operation after the required file has been # downloaded assert QThread.currentThread() is self.thread() assert self.__awaiting_state is not None assert self.__awaiting_state.future is f if self.isBlocking(): self.progressBarFinished(processEvents=None) self.setBlocking(False) self.setStatusMessage("") self.__awaiting_state = None try: path = f.result() # anything can happen here, pylint: disable=broad-except except Exception as ex: log.exception("Error:") self.error(format_exception(ex)) path = None self.__update_cached_state() if path is not None: data = self.load_data(path) else: data = None self.Outputs.data.send(data) def commit_cached(self, file_path): path = LocalFiles(self.local_cache_path).localpath(*file_path) self.Outputs.data.send(self.load_data(path)) @Slot() def __progress_advance(self): assert QThread.currentThread() is self.thread() self.progressBarAdvance(1, processEvents=None) def onDeleteWidget(self): super().onDeleteWidget() if self.__awaiting_state is not None: self.__awaiting_state.watcher.done.disconnect(self.__commit_complete) self.__awaiting_state.pb.advance.disconnect(self.__progress_advance) self.__awaiting_state = None @staticmethod def sizeHint(): return QSize(900, 600) def closeEvent(self, event): self.splitter_state = bytes(self.splitter.saveState()) self.header_state = bytes(self.view.header().saveState()) super().closeEvent(event) def load_data(self, path): # pylint: disable=no-self-use return Orange.data.Table(path) def list_remote(self): # type: () -> Dict[Tuple[str, ...], dict] client = ServerFiles(server=self.INDEX_URL) return client.allinfo() def list_local(self): # type: () -> Dict[Tuple[str, ...], dict] return LocalFiles(self.local_cache_path).allinfo()
class MessagesWidget(QWidget): """ An iconified multiple message display area. `MessagesWidget` displays a short message along with an icon. If there are multiple messages they are summarized. The user can click on the widget to display the full message text in a popup view. """ #: Signal emitted when an embedded html link is clicked #: (if `openExternalLinks` is `False`). linkActivated = Signal(str) #: Signal emitted when an embedded html link is hovered. linkHovered = Signal(str) Severity = Severity #: General informative message. Information = Severity.Information #: A warning message severity. Warning = Severity.Warning #: An error message severity. Error = Severity.Error Message = Message def __init__(self, parent=None, openExternalLinks=False, defaultStyleSheet="", **kwargs): kwargs.setdefault( "sizePolicy", QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) ) super().__init__(parent, **kwargs) self.__openExternalLinks = openExternalLinks # type: bool self.__messages = OrderedDict() # type: Dict[Hashable, Message] #: The full (joined all messages text - rendered as html), displayed #: in a tooltip. self.__fulltext = "" #: The full text displayed in a popup. Is empty if the message is #: short self.__popuptext = "" #: Leading icon self.__iconwidget = IconWidget( sizePolicy=QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) ) #: Inline message text self.__textlabel = QLabel( wordWrap=False, textInteractionFlags=Qt.LinksAccessibleByMouse, openExternalLinks=self.__openExternalLinks, sizePolicy=QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum) ) #: Indicator that extended contents are accessible with a click on the #: widget. self.__popupicon = QLabel( sizePolicy=QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum), text="\N{VERTICAL ELLIPSIS}", visible=False, ) self.__textlabel.linkActivated.connect(self.linkActivated) self.__textlabel.linkHovered.connect(self.linkHovered) self.setLayout(QHBoxLayout()) self.layout().setContentsMargins(2, 1, 2, 1) self.layout().setSpacing(0) self.layout().addWidget(self.__iconwidget) self.layout().addSpacing(4) self.layout().addWidget(self.__textlabel) self.layout().addWidget(self.__popupicon) self.__textlabel.setAttribute(Qt.WA_MacSmallSize) self.__defaultStyleSheet = defaultStyleSheet def sizeHint(self): sh = super().sizeHint() h = self.style().pixelMetric(QStyle.PM_SmallIconSize) if all(m.isEmpty() for m in self.messages()): sh.setWidth(0) return sh.expandedTo(QSize(0, h + 2)) def minimumSizeHint(self): msh = super().minimumSizeHint() h = self.style().pixelMetric(QStyle.PM_SmallIconSize) if all(m.isEmpty() for m in self.messages()): msh.setWidth(0) else: msh.setWidth(h + 2) return msh.expandedTo(QSize(0, h + 2)) def setOpenExternalLinks(self, state): # type: (bool) -> None """ If `True` then `linkActivated` signal will be emitted when the user clicks on an html link in a message, otherwise links are opened using `QDesktopServices.openUrl` """ # TODO: update popup if open self.__openExternalLinks = state self.__textlabel.setOpenExternalLinks(state) def openExternalLinks(self): # type: () -> bool """ """ return self.__openExternalLinks def setDefaultStyleSheet(self, css): # type: (str) -> None """ Set a default css to apply to the rendered text. Parameters ---------- css : str A css style sheet as supported by Qt's Rich Text support. Note ---- Not to be confused with `QWidget.styleSheet` See Also -------- `Supported HTML Subset`_ .. _`Supported HTML Subset`: http://doc.qt.io/qt-5/richtext-html-subset.html """ if self.__defaultStyleSheet != css: self.__defaultStyleSheet = css self.__update() def defaultStyleSheet(self): """ Returns ------- css : str The current style sheet """ return self.__defaultStyleSheet def setMessage(self, message_id, message): # type: (Hashable, Message) -> None """ Add a `message` for `message_id` to the current display. Note ---- Set an empty `Message` instance to clear the message display but retain the relative ordering in the display should a message for `message_id` reactivate. """ self.__messages[message_id] = message self.__update() def removeMessage(self, message_id): # type: (Hashable) -> None """ Remove message for `message_id` from the display. Note ---- Setting an empty `Message` instance will also clear the display, however the relative ordering of the messages will be retained, should the `message_id` 'reactivate'. """ del self.__messages[message_id] self.__update() def setMessages(self, messages): # type: (Union[Iterable[Tuple[Hashable, Message]], Dict[Hashable, Message]]) -> None """ Set multiple messages in a single call. """ messages = OrderedDict(messages) self.__messages.update(messages) self.__update() def clear(self): # type: () -> None """ Clear all messages. """ self.__messages.clear() self.__update() def messages(self): # type: () -> List[Message] """ Return all set messages. Returns ------- messages: `List[Message]` """ return list(self.__messages.values()) def summarize(self): # type: () -> Message """ Summarize all the messages into a single message. """ messages = [m for m in self.__messages.values() if not m.isEmpty()] if messages: return summarize(messages) else: return Message() @staticmethod def __styled(css, html): # Prepend css style sheet before a html fragment. if css.strip(): return "<style>\n" + escape(css) + "\n</style>\n" + html else: return html def __update(self): """ Update the current display state. """ self.ensurePolished() summary = self.summarize() icon = message_icon(summary) self.__iconwidget.setIcon(icon) self.__iconwidget.setVisible(not (summary.isEmpty() or icon.isNull())) self.__textlabel.setTextFormat(summary.textFormat) self.__textlabel.setText(summary.text) self.__textlabel.setVisible(bool(summary.text)) messages = [m for m in self.__messages.values() if not m.isEmpty()] if messages: messages = sorted(messages, key=attrgetter("severity"), reverse=True) fulltext = "<hr/>".join(m.asHtml() for m in messages) else: fulltext = "" self.__fulltext = fulltext self.setToolTip(self.__styled(self.__defaultStyleSheet, fulltext)) def is_short(m): return not (m.informativeText or m.detailedText) if not messages or len(messages) == 1 and is_short(messages[0]): self.__popuptext = "" else: self.__popuptext = fulltext self.__popupicon.setVisible(bool(self.__popuptext)) self.layout().activate() def mousePressEvent(self, event): if event.button() == Qt.LeftButton: if self.__popuptext: popup = QMenu(self) label = QLabel( self, textInteractionFlags=Qt.TextBrowserInteraction, openExternalLinks=self.__openExternalLinks, ) label.setText(self.__styled(self.__defaultStyleSheet, self.__popuptext)) label.linkActivated.connect(self.linkActivated) label.linkHovered.connect(self.linkHovered) action = QWidgetAction(popup) action.setDefaultWidget(label) popup.addAction(action) popup.popup(event.globalPos(), action) event.accept() return else: super().mousePressEvent(event) def enterEvent(self, event): super().enterEvent(event) self.update() def leaveEvent(self, event): super().leaveEvent(event) self.update() def changeEvent(self, event): super().changeEvent(event) self.update() def paintEvent(self, event): opt = QStyleOption() opt.initFrom(self) if not self.__popupicon.isVisible(): return if not (opt.state & QStyle.State_MouseOver or opt.state & QStyle.State_HasFocus): return palette = opt.palette # type: QPalette if opt.state & QStyle.State_HasFocus: pen = QPen(palette.color(QPalette.Highlight)) else: pen = QPen(palette.color(QPalette.Dark)) if self.__fulltext and \ opt.state & QStyle.State_MouseOver and \ opt.state & QStyle.State_Active: g = QLinearGradient() g.setCoordinateMode(QLinearGradient.ObjectBoundingMode) base = palette.color(QPalette.Window) base.setAlpha(90) g.setColorAt(0, base.lighter(200)) g.setColorAt(0.6, base) g.setColorAt(1.0, base.lighter(200)) brush = QBrush(g) else: brush = QBrush(Qt.NoBrush) p = QPainter(self) p.setBrush(brush) p.setPen(pen) p.drawRect(opt.rect.adjusted(0, 0, -1, -1))
class PreviewBrowser(QWidget): """A Preview Browser for recent/premade scheme selection. """ # Emitted when the current previewed item changes currentIndexChanged = Signal(int) # Emitted when an item is double clicked in the preview list. activated = Signal(int) def __init__(self, *args, heading="", previewMargins=12, **kwargs): super().__init__(*args) self.__model = None self.__currentIndex = -1 self.__template = DESCRIPTION_TEMPLATE self.__margin = previewMargins self.__setupUi() self.setHeading(heading) def __setupUi(self): vlayout = QVBoxLayout() vlayout.setContentsMargins(0, 0, 0, 0) top_layout = QVBoxLayout(objectName="top-layout") margin = self.__margin top_layout.setContentsMargins(margin, margin, margin, margin) # Optional heading label self.__heading = QLabel( self, objectName="heading", visible=False ) # Horizontal row with full text description and a large preview # image. hlayout = QHBoxLayout() hlayout.setContentsMargins(0, 0, 0, 0) self.__label = QLabel( self, objectName="description-label", wordWrap=True, alignment=Qt.AlignTop | Qt.AlignLeft ) self.__label.setWordWrap(True) self.__label.setFixedSize(220, PREVIEW_SIZE[1]) self.__label.setMinimumWidth(PREVIEW_SIZE[0] // 2) self.__label.setMaximumHeight(PREVIEW_SIZE[1]) self.__image = QSvgWidget(self, objectName="preview-image") self.__image.setFixedSize(*PREVIEW_SIZE) self.__imageFrame = DropShadowFrame(self) self.__imageFrame.setWidget(self.__image) hlayout.addWidget(self.__label) hlayout.addWidget(self.__image) # Path text below the description and image path_layout = QHBoxLayout() path_layout.setContentsMargins(0, 0, 0, 0) path_label = QLabel("<b>{0!s}</b>".format(self.tr("Path:")), self, objectName="path-label") self.__path = TextLabel(self, objectName="path-text") path_layout.addWidget(path_label) path_layout.addWidget(self.__path) top_layout.addWidget(self.__heading) top_layout.addLayout(hlayout) top_layout.addLayout(path_layout) vlayout.addLayout(top_layout) # An list view with small preview icons. self.__previewList = LinearIconView( objectName="preview-list-view", wordWrap=True ) self.__previewList.doubleClicked.connect(self.__onDoubleClicked) vlayout.addWidget(self.__previewList) self.setLayout(vlayout) def setHeading(self, text): self.__heading.setVisible(bool(text)) self.__heading.setText(text) def setPreviewMargins(self, margin): # type: (int) -> None """ Set the left, top and right margins of the top widget part (heading and description) Parameters ---------- margin : int Margin """ if margin != self.__margin: layout = self.layout().itemAt(0).layout() assert isinstance(layout, QVBoxLayout) assert layout.objectName() == "top-layout" layout.setContentsMargins(margin, margin, margin, 0) def setModel(self, model): """Set the item model for preview. """ if self.__model != model: if self.__model: s_model = self.__previewList.selectionModel() s_model.selectionChanged.disconnect(self.__onSelectionChanged) self.__model.dataChanged.disconnect(self.__onDataChanged) self.__model = model self.__previewList.setModel(model) if model: s_model = self.__previewList.selectionModel() s_model.selectionChanged.connect(self.__onSelectionChanged) self.__model.dataChanged.connect(self.__onDataChanged) if model and model.rowCount(): self.setCurrentIndex(0) def model(self): """Return the item model. """ return self.__model def setPreviewDelegate(self, delegate): """Set the delegate to render the preview images. """ raise NotImplementedError def setDescriptionTemplate(self, template): self.__template = template self.__update() def setCurrentIndex(self, index): """Set the selected preview item index. """ if self.__model is not None and self.__model.rowCount(): index = min(index, self.__model.rowCount() - 1) index = self.__model.index(index, 0) sel_model = self.__previewList.selectionModel() # This emits selectionChanged signal and triggers # __onSelectionChanged, currentIndex is updated there. sel_model.select(index, sel_model.ClearAndSelect) elif self.__currentIndex != -1: self.__currentIndex = -1 self.__update() self.currentIndexChanged.emit(-1) def currentIndex(self): """Return the current selected index. """ return self.__currentIndex def __onSelectionChanged(self, *args): """Selected item in the preview list has changed. Set the new description and large preview image. """ rows = self.__previewList.selectedIndexes() if rows: index = rows[0] self.__currentIndex = index.row() else: index = QModelIndex() self.__currentIndex = -1 self.__update() self.currentIndexChanged.emit(self.__currentIndex) def __onDataChanged(self, topleft, bottomRight): """Data changed, update the preview if current index in the changed range. """ if self.__currentIndex <= topleft.row() and \ self.__currentIndex >= bottomRight.row(): self.__update() def __onDoubleClicked(self, index): """Double click on an item in the preview item list. """ self.activated.emit(index.row()) def __update(self): """Update the current description. """ if self.__currentIndex != -1: index = self.model().index(self.__currentIndex, 0) else: index = QModelIndex() if not index.isValid(): description = "" name = "" path = "" svg = NO_PREVIEW_SVG else: description = index.data(Qt.WhatsThisRole) if description: description = description else: description = "No description." description = escape(description) description = description.replace("\n", "<br/>") name = index.data(Qt.DisplayRole) if name: name = name else: name = "Untitled" name = escape(name) path = str(index.data(Qt.StatusTipRole)) svg = str(index.data(previewmodel.ThumbnailSVGRole)) desc_text = self.__template.format(description=description, name=name) self.__label.setText(desc_text) self.__path.setText(contractuser(path)) if not svg: svg = NO_PREVIEW_SVG if svg: self.__image.load(QByteArray(svg.encode("utf-8")))
class MessageWidget(QWidget): """ A widget displaying a simple message to the user. This is an alternative to a full QMessageBox intended for inline modeless messages. [[icon] {Message text} (Ok) (Cancel)] """ #: Emitted when a button with the AcceptRole is clicked accepted = Signal() #: Emitted when a button with the RejectRole is clicked rejected = Signal() #: Emitted when a button with the HelpRole is clicked helpRequested = Signal() #: Emitted when a button is clicked clicked = Signal(QAbstractButton) class StandardButton(enum.IntEnum): NoButton, Ok, Close, Help = 0x0, 0x1, 0x2, 0x4 NoButton, Ok, Close, Help = list(StandardButton) class ButtonRole(enum.IntEnum): InvalidRole, AcceptRole, RejectRole, HelpRole = 0, 1, 2, 3 InvalidRole, AcceptRole, RejectRole, HelpRole = list(ButtonRole) _Button = namedtuple("_Button", ["button", "role", "stdbutton"]) def __init__(self, parent=None, icon=QIcon(), text="", wordWrap=False, textFormat=Qt.AutoText, standardButtons=NoButton, **kwargs): super().__init__(parent, **kwargs) self.__text = text self.__icon = QIcon() self.__wordWrap = wordWrap self.__standardButtons = MessageWidget.NoButton self.__buttons = [] layout = QHBoxLayout() layout.setContentsMargins(8, 0, 8, 0) self.__iconlabel = QLabel(objectName="icon-label") self.__iconlabel.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.__textlabel = QLabel(objectName="text-label", text=text, wordWrap=wordWrap, textFormat=textFormat) if sys.platform == "darwin": self.__textlabel.setAttribute(Qt.WA_MacSmallSize) layout.addWidget(self.__iconlabel) layout.addWidget(self.__textlabel) self.setLayout(layout) self.setIcon(icon) self.setStandardButtons(standardButtons) def setText(self, text): """ Set the current message text. :type message: str """ if self.__text != text: self.__text = text self.__textlabel.setText(text) def text(self): """ Return the current message text. :rtype: str """ return self.__text def setIcon(self, icon): """ Set the message icon. :type icon: QIcon | QPixmap | QString | QStyle.StandardPixmap """ if isinstance(icon, QStyle.StandardPixmap): icon = self.style().standardIcon(icon) else: icon = QIcon(icon) if self.__icon != icon: self.__icon = QIcon(icon) if not self.__icon.isNull(): size = self.style().pixelMetric( QStyle.PM_SmallIconSize, None, self) pm = self.__icon.pixmap(QSize(size, size)) else: pm = QPixmap() self.__iconlabel.setPixmap(pm) self.__iconlabel.setVisible(not pm.isNull()) def icon(self): """ Return the current icon. :rtype: QIcon """ return QIcon(self.__icon) def setWordWrap(self, wordWrap): """ Set the message text wrap property :type wordWrap: bool """ if self.__wordWrap != wordWrap: self.__wordWrap = wordWrap self.__textlabel.setWordWrap(wordWrap) def wordWrap(self): """ Return the message text wrap property. :rtype: bool """ return self.__wordWrap def setTextFormat(self, textFormat): """ Set message text format :type textFormat: Qt.TextFormat """ self.__textlabel.setTextFormat(textFormat) def textFormat(self): """ Return the message text format. :rtype: Qt.TextFormat """ return self.__textlabel.textFormat() def changeEvent(self, event): # reimplemented if event.type() == 177: # QEvent.MacSizeChange: ... super().changeEvent(event) def setStandardButtons(self, buttons): for button in MessageWidget.StandardButton: existing = self.button(button) if button & buttons and existing is None: self.addButton(button) elif existing is not None: self.removeButton(existing) def standardButtons(self): return functools.reduce( operator.ior, (slot.stdbutton for slot in self.__buttons if slot.stdbutton is not None), MessageWidget.NoButton) def addButton(self, button, *rolearg): """ addButton(QAbstractButton, ButtonRole) addButton(str, ButtonRole) addButton(StandardButton) Add and return a button """ stdbutton = None if isinstance(button, QAbstractButton): if len(rolearg) != 1: raise TypeError("Wrong number of arguments for " "addButton(QAbstractButton, role)") role = rolearg[0] elif isinstance(button, MessageWidget.StandardButton): if len(rolearg) != 0: raise TypeError("Wrong number of arguments for " "addButton(StandardButton)") stdbutton = button if button == MessageWidget.Ok: role = MessageWidget.AcceptRole button = QPushButton("Ok", default=False, autoDefault=False) elif button == MessageWidget.Close: role = MessageWidget.RejectRole # button = QPushButton( # default=False, autoDefault=False, flat=True, # icon=QIcon(self.style().standardIcon( # QStyle.SP_TitleBarCloseButton))) button = SimpleButton( icon=QIcon(self.style().standardIcon( QStyle.SP_TitleBarCloseButton))) elif button == MessageWidget.Help: role = MessageWidget.HelpRole button = QPushButton("Help", default=False, autoDefault=False) elif isinstance(button, str): if len(rolearg) != 1: raise TypeError("Wrong number of arguments for " "addButton(str, ButtonRole)") role = rolearg[0] button = QPushButton(button, default=False, autoDefault=False) if sys.platform == "darwin": button.setAttribute(Qt.WA_MacSmallSize) self.__buttons.append(MessageWidget._Button(button, role, stdbutton)) button.clicked.connect(self.__button_clicked) self.__relayout() return button def removeButton(self, button): """ Remove a `button`. :type button: QAbstractButton """ slot = [s for s in self.__buttons if s.button is button] if slot: slot = slot[0] self.__buttons.remove(slot) self.layout().removeWidget(slot.button) slot.button.setParent(None) def buttonRole(self, button): """ Return the ButtonRole for button :type button: QAbsstractButton """ for slot in self.__buttons: if slot.button is button: return slot.role else: return MessageWidget.InvalidRole def button(self, standardButton): """ Return the button for the StandardButton. :type standardButton: StandardButton """ for slot in self.__buttons: if slot.stdbutton == standardButton: return slot.button else: return None def __button_clicked(self): button = self.sender() role = self.buttonRole(button) self.clicked.emit(button) if role == MessageWidget.AcceptRole: self.accepted.emit() self.close() elif role == MessageWidget.RejectRole: self.rejected.emit() self.close() elif role == MessageWidget.HelpRole: self.helpRequested.emit() def __relayout(self): for slot in self.__buttons: self.layout().removeWidget(slot.button) order = { MessageOverlayWidget.HelpRole: 0, MessageOverlayWidget.AcceptRole: 2, MessageOverlayWidget.RejectRole: 3, } orderd = sorted(self.__buttons, key=lambda slot: order.get(slot.role, -1)) prev = self.__textlabel for slot in orderd: self.layout().addWidget(slot.button) QWidget.setTabOrder(prev, slot.button)
class PreviewDialog(QDialog): """A Dialog for selecting an item from a PreviewItem. """ currentIndexChanged = Signal(int) def __init__(self, parent=None, flags=Qt.WindowFlags(0), model=None, **kwargs): QDialog.__init__(self, parent, flags, **kwargs) self.__setupUi() if model is not None: self.setModel(model) def __setupUi(self): layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) self.setContentsMargins(0, 0, 0, 0) heading = self.tr("Preview") heading = "<h3>{0}</h3>".format(heading) self.__heading = QLabel(heading, self, objectName="heading") self.__heading.setContentsMargins(12, 12, 12, 0) self.__browser = previewbrowser.PreviewBrowser(self) self.__buttons = QDialogButtonBox(QDialogButtonBox.Open | \ QDialogButtonBox.Cancel, Qt.Horizontal,) self.__buttons.button(QDialogButtonBox.Open).setAutoDefault(True) # Set the Open dialog as disabled until the current index changes self.__buttons.button(QDialogButtonBox.Open).setEnabled(False) # The QDialogButtonsWidget messes with the layout if it is # contained directly in the QDialog. So we create an extra # layer of indirection. buttons = QWidget(objectName="button-container") buttons_l = QVBoxLayout() buttons_l.setContentsMargins(12, 0, 12, 12) buttons.setLayout(buttons_l) buttons_l.addWidget(self.__buttons) layout.addWidget(self.__heading) layout.addWidget(self.__browser) layout.addWidget(buttons) self.__buttons.accepted.connect(self.accept) self.__buttons.rejected.connect(self.reject) self.__browser.currentIndexChanged.connect( self.__on_currentIndexChanged ) self.__browser.activated.connect(self.__on_activated) layout.setSizeConstraint(QVBoxLayout.SetFixedSize) self.setLayout(layout) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) def setItems(self, items): """Set the items (a list of strings) for preview/selection. """ model = QStringListModel(items) self.setModel(model) def setModel(self, model): """Set the model for preview/selection. """ self.__browser.setModel(model) def model(self): """Return the model. """ return self.__browser.model() def currentIndex(self): return self.__browser.currentIndex() def setCurrentIndex(self, index): """Set the current selected (shown) index. """ self.__browser.setCurrentIndex(index) def setHeading(self, heading): """Set `heading` as the heading string ('<h3>Preview</h3>' by default). """ self.__heading.setText(heading) def heading(self): """Return the heading string. """ def __on_currentIndexChanged(self, index): button = self.__buttons.button(QDialogButtonBox.Open) button.setEnabled(index >= 0) self.currentIndexChanged.emit(index) def __on_activated(self, index): if self.currentIndex() != index: self.setCurrentIndex(index) self.accept()
class OWFile(widget.OWWidget, RecentPathsWComboMixin): name = "File" id = "orange.widgets.data.file" description = "Read data from an input file or network " \ "and send a data table to the output." icon = "icons/File.svg" priority = 10 category = "Data" keywords = ["data", "file", "load", "read"] outputs = [ widget.OutputSignal( "Data", Table, doc="Attribute-valued data set read from the input file.") ] want_main_area = False SEARCH_PATHS = [("sample-datasets", get_sample_datasets_dir())] SIZE_LIMIT = 1e7 LOCAL_FILE, URL = range(2) settingsHandler = PerfectDomainContextHandler() # Overload RecentPathsWidgetMixin.recent_paths to set defaults recent_paths = Setting([ RecentPath("", "sample-datasets", "iris.tab"), RecentPath("", "sample-datasets", "titanic.tab"), RecentPath("", "sample-datasets", "housing.tab"), RecentPath("", "sample-datasets", "heart_disease.tab"), ]) recent_urls = Setting([]) source = Setting(LOCAL_FILE) xls_sheet = ContextSetting("") sheet_names = Setting({}) url = Setting("") variables = ContextSetting([]) dlg_formats = ("All readable files ({});;".format( '*' + ' *'.join(FileFormat.readers.keys())) + ";;".join( "{} (*{})".format(f.DESCRIPTION, ' *'.join(f.EXTENSIONS)) for f in sorted(set(FileFormat.readers.values()), key=list(FileFormat.readers.values()).index))) domain_editor = SettingProvider(DomainEditor) class Warning(widget.OWWidget.Warning): file_too_big = widget.Msg( "The file is too large to load automatically." " Press Reload to load.") class Error(widget.OWWidget.Error): file_not_found = widget.Msg("File not found.") def __init__(self): super().__init__() RecentPathsWComboMixin.__init__(self) self.domain = None self.data = None self.loaded_file = "" self.reader = None layout = QGridLayout() gui.widgetBox(self.controlArea, margin=0, orientation=layout) vbox = gui.radioButtons(None, self, "source", box=True, addSpace=True, callback=self.load_data, addToLayout=False) rb_button = gui.appendRadioButton(vbox, "File:", addToLayout=False) layout.addWidget(rb_button, 0, 0, Qt.AlignVCenter) box = gui.hBox(None, addToLayout=False, margin=0) box.setSizePolicy(Policy.MinimumExpanding, Policy.Fixed) self.file_combo.setSizePolicy(Policy.MinimumExpanding, Policy.Fixed) self.file_combo.activated[int].connect(self.select_file) box.layout().addWidget(self.file_combo) layout.addWidget(box, 0, 1) file_button = gui.button(None, self, '...', callback=self.browse_file, autoDefault=False) file_button.setIcon(self.style().standardIcon(QStyle.SP_DirOpenIcon)) file_button.setSizePolicy(Policy.Maximum, Policy.Fixed) layout.addWidget(file_button, 0, 2) reload_button = gui.button(None, self, "Reload", callback=self.load_data, autoDefault=False) reload_button.setIcon(self.style().standardIcon( QStyle.SP_BrowserReload)) reload_button.setSizePolicy(Policy.Fixed, Policy.Fixed) layout.addWidget(reload_button, 0, 3) self.sheet_box = gui.hBox(None, addToLayout=False, margin=0) self.sheet_combo = gui.comboBox( None, self, "xls_sheet", callback=self.select_sheet, sendSelectedValue=True, ) self.sheet_combo.setSizePolicy(Policy.MinimumExpanding, Policy.Fixed) self.sheet_label = QLabel() self.sheet_label.setText('Sheet') self.sheet_label.setSizePolicy(Policy.MinimumExpanding, Policy.Fixed) self.sheet_box.layout().addWidget(self.sheet_label, Qt.AlignLeft) self.sheet_box.layout().addWidget(self.sheet_combo, Qt.AlignVCenter) layout.addWidget(self.sheet_box, 2, 1) self.sheet_box.hide() rb_button = gui.appendRadioButton(vbox, "URL:", addToLayout=False) layout.addWidget(rb_button, 3, 0, Qt.AlignVCenter) self.url_combo = url_combo = QComboBox() url_model = NamedURLModel(self.sheet_names) url_model.wrap(self.recent_urls) url_combo.setLineEdit(LineEditSelectOnFocus()) url_combo.setModel(url_model) url_combo.setSizePolicy(Policy.MinimumExpanding, Policy.Fixed) url_combo.setEditable(True) url_combo.setInsertPolicy(url_combo.InsertAtTop) url_edit = url_combo.lineEdit() l, t, r, b = url_edit.getTextMargins() url_edit.setTextMargins(l + 5, t, r, b) layout.addWidget(url_combo, 3, 1, 3, 3) url_combo.activated.connect(self._url_set) box = gui.vBox(self.controlArea, "Info") self.info = gui.widgetLabel(box, 'No data loaded.') self.warnings = gui.widgetLabel(box, '') box = gui.widgetBox(self.controlArea, "Columns (Double click to edit)") self.domain_editor = DomainEditor(self) self.editor_model = self.domain_editor.model() box.layout().addWidget(self.domain_editor) box = gui.hBox(self.controlArea) gui.button(box, self, "Browse documentation data sets", callback=lambda: self.browse_file(True), autoDefault=False) gui.rubber(box) box.layout().addWidget(self.report_button) self.report_button.setFixedWidth(170) self.apply_button = gui.button(box, self, "Apply", callback=self.apply_domain_edit) self.apply_button.setEnabled(False) self.apply_button.setFixedWidth(170) self.editor_model.dataChanged.connect( lambda: self.apply_button.setEnabled(True)) self.set_file_list() # Must not call open_file from within __init__. open_file # explicitly re-enters the event loop (by a progress bar) self.setAcceptDrops(True) if self.source == self.LOCAL_FILE: last_path = self.last_path() if last_path and os.path.exists(last_path) and \ os.path.getsize(last_path) > self.SIZE_LIMIT: self.Warning.file_too_big() return QTimer.singleShot(0, self.load_data) def sizeHint(self): return QSize(600, 550) def select_file(self, n): assert n < len(self.recent_paths) super().select_file(n) if self.recent_paths: self.source = self.LOCAL_FILE self.load_data() self.set_file_list() def select_sheet(self): self.recent_paths[0].sheet = self.sheet_combo.currentText() self.load_data() def _url_set(self): self.source = self.URL self.load_data() def browse_file(self, in_demos=False): if in_demos: start_file = get_sample_datasets_dir() if not os.path.exists(start_file): QMessageBox.information( None, "File", "Cannot find the directory with documentation data sets") return else: start_file = self.last_path() or os.path.expanduser("~/") filename, _ = QFileDialog.getOpenFileName(self, 'Open Orange Data File', start_file, self.dlg_formats) if not filename: return self.add_path(filename) self.source = self.LOCAL_FILE self.load_data() # Open a file, create data from it and send it over the data channel def load_data(self): # We need to catch any exception type since anything can happen in # file readers # pylint: disable=broad-except self.closeContext() self.domain_editor.set_domain(None) self.apply_button.setEnabled(False) self.clear_messages() self.set_file_list() if self.last_path() and not os.path.exists(self.last_path()): self.Error.file_not_found() self.send("Data", None) self.info.setText("No data.") return error = None try: self.reader = self._get_reader() if self.reader is None: self.data = None self.send("Data", None) self.info.setText("No data.") self.sheet_box.hide() return except Exception as ex: error = ex if not error: self._update_sheet_combo() with catch_warnings(record=True) as warnings: try: data = self.reader.read() except Exception as ex: log.exception(ex) error = ex self.warning(warnings[-1].message.args[0] if warnings else '') if error: self.data = None self.send("Data", None) self.info.setText("An error occurred:\n{}".format(error)) self.sheet_box.hide() return self.info.setText(self._describe(data)) self.loaded_file = self.last_path() add_origin(data, self.loaded_file) self.data = data self.openContext(data.domain) self.apply_domain_edit() # sends data def _get_reader(self): """ Returns ------- FileFormat """ if self.source == self.LOCAL_FILE: reader = FileFormat.get_reader(self.last_path()) if self.recent_paths and self.recent_paths[0].sheet: reader.select_sheet(self.recent_paths[0].sheet) return reader elif self.source == self.URL: url = self.url_combo.currentText().strip() if url: return UrlReader(url) def _update_sheet_combo(self): if len(self.reader.sheets) < 2: self.sheet_box.hide() self.reader.select_sheet(None) return self.sheet_combo.clear() self.sheet_combo.addItems(self.reader.sheets) self._select_active_sheet() self.sheet_box.show() def _select_active_sheet(self): if self.reader.sheet: try: idx = self.reader.sheets.index(self.reader.sheet) self.sheet_combo.setCurrentIndex(idx) except ValueError: # Requested sheet does not exist in this file self.reader.select_sheet(None) else: self.sheet_combo.setCurrentIndex(0) def _describe(self, table): domain = table.domain text = "" attrs = getattr(table, "attributes", {}) descs = [ attrs[desc] for desc in ("Name", "Description") if desc in attrs ] if len(descs) == 2: descs[0] = "<b>{}</b>".format(descs[0]) if descs: text += "<p>{}</p>".format("<br/>".join(descs)) text += "<p>{} instance(s), {} feature(s), {} meta attribute(s)".\ format(len(table), len(domain.attributes), len(domain.metas)) if domain.has_continuous_class: text += "<br/>Regression; numerical class." elif domain.has_discrete_class: text += "<br/>Classification; discrete class with {} values.".\ format(len(domain.class_var.values)) elif table.domain.class_vars: text += "<br/>Multi-target; {} target variables.".format( len(table.domain.class_vars)) else: text += "<br/>Data has no target variable." text += "</p>" if 'Timestamp' in table.domain: # Google Forms uses this header to timestamp responses text += '<p>First entry: {}<br/>Last entry: {}</p>'.format( table[0, 'Timestamp'], table[-1, 'Timestamp']) return text def storeSpecificSettings(self): self.current_context.modified_variables = self.variables[:] def retrieveSpecificSettings(self): if hasattr(self.current_context, "modified_variables"): self.variables[:] = self.current_context.modified_variables def apply_domain_edit(self): if self.data is not None: domain, cols = self.domain_editor.get_domain( self.data.domain, self.data) X, y, m = cols X = np.array(X).T if len(X) else np.empty((len(self.data), 0)) y = np.array(y).T if len(y) else None dtpe = object if any( isinstance(m, StringVariable) for m in domain.metas) else float m = np.array(m, dtype=dtpe).T if len(m) else None table = Table.from_numpy(domain, X, y, m, self.data.W) table.name = self.data.name table.ids = np.array(self.data.ids) table.attributes = getattr(self.data, 'attributes', {}) else: table = self.data self.send("Data", table) self.apply_button.setEnabled(False) def get_widget_name_extension(self): _, name = os.path.split(self.loaded_file) return os.path.splitext(name)[0] def send_report(self): def get_ext_name(filename): try: return FileFormat.names[os.path.splitext(filename)[1]] except KeyError: return "unknown" if self.data is None: self.report_paragraph("File", "No file.") return if self.source == self.LOCAL_FILE: home = os.path.expanduser("~") if self.loaded_file.startswith(home): # os.path.join does not like ~ name = "~" + os.path.sep + \ self.loaded_file[len(home):].lstrip("/").lstrip("\\") else: name = self.loaded_file if self.sheet_combo.isVisible(): name += " ({})".format(self.sheet_combo.currentText()) self.report_items("File", [("File name", name), ("Format", get_ext_name(name))]) else: self.report_items("Data", [("Resource", self.url), ("Format", get_ext_name(self.url))]) self.report_data("Data", self.data) def dragEnterEvent(self, event): """Accept drops of valid file urls""" urls = event.mimeData().urls() if urls: try: FileFormat.get_reader( OSX_NSURL_toLocalFile(urls[0]) or urls[0].toLocalFile()) event.acceptProposedAction() except IOError: pass def dropEvent(self, event): """Handle file drops""" urls = event.mimeData().urls() if urls: self.add_path( OSX_NSURL_toLocalFile(urls[0]) or urls[0].toLocalFile()) # add first file self.source = self.LOCAL_FILE self.load_data()
class OWTreeGraph(OWTreeViewer2D): """Graphical visualization of tree models""" name = "Tree Viewer" icon = "icons/TreeViewer.svg" priority = 35 inputs = [ widget.InputSignal( "Tree", TreeModel, "ctree", # Had different input names before merging from # Classification/Regression tree variants replaces=["Classification Tree", "Regression Tree"]) ] outputs = [ widget.OutputSignal( "Selected Data", Table, widget.Default, id="selected-data", ), widget.OutputSignal( ANNOTATED_DATA_SIGNAL_NAME, Table, id="annotated-data") ] settingsHandler = ClassValuesContextHandler() target_class_index = ContextSetting(0) regression_colors = Setting(0) replaces = [ "Orange.widgets.classify.owclassificationtreegraph.OWClassificationTreeGraph", "Orange.widgets.classify.owregressiontreegraph.OWRegressionTreeGraph" ] COL_OPTIONS = ["Default", "Number of instances", "Mean value", "Variance"] COL_DEFAULT, COL_INSTANCE, COL_MEAN, COL_VARIANCE = range(4) def __init__(self): super().__init__() self.domain = None self.dataset = None self.clf_dataset = None self.color_label = QLabel("Target class: ") combo = self.color_combo = gui.OrangeComboBox() combo.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed) combo.setSizeAdjustPolicy( QComboBox.AdjustToMinimumContentsLengthWithIcon) combo.setMinimumContentsLength(8) combo.activated[int].connect(self.color_changed) self.display_box.layout().addRow(self.color_label, combo) def set_node_info(self): """Set the content of the node""" for node in self.scene.nodes(): node.set_rect(QRectF()) self.update_node_info(node) w = max([n.rect().width() for n in self.scene.nodes()] + [0]) if w > self.max_node_width: w = self.max_node_width for node in self.scene.nodes(): rect = node.rect() node.set_rect(QRectF(rect.x(), rect.y(), w, rect.height())) self.scene.fix_pos(self.root_node, 10, 10) @staticmethod def _update_node_info_attr_name(node, text): attr = node.node_inst.attr if attr is not None: text += "<hr/>{}".format(attr.name) return text def activate_loaded_settings(self): if not self.model: return super().activate_loaded_settings() if self.domain.class_var.is_discrete: self.color_combo.setCurrentIndex(self.target_class_index) self.toggle_node_color_cls() else: self.color_combo.setCurrentIndex(self.regression_colors) self.toggle_node_color_reg() self.set_node_info() def color_changed(self, i): if self.domain.class_var.is_discrete: self.target_class_index = i self.toggle_node_color_cls() else: self.regression_colors = i self.toggle_node_color_reg() def toggle_node_size(self): self.set_node_info() self.scene.update() self.scene_view.repaint() def toggle_color_cls(self): self.toggle_node_color_cls() self.set_node_info() self.scene.update() def toggle_color_reg(self): self.toggle_node_color_reg() self.set_node_info() self.scene.update() def ctree(self, model=None): """Input signal handler""" self.clear_scene() self.color_combo.clear() self.closeContext() self.model = model if model is None: self.info.setText('No tree.') self.root_node = None self.dataset = None else: self.domain = model.domain self.dataset = model.instances if self.dataset is not None and self.dataset.domain != self.domain: self.clf_dataset = Table.from_table(model.domain, self.dataset) else: self.clf_dataset = self.dataset class_var = self.domain.class_var if class_var.is_discrete: self.scene.colors = [QColor(*col) for col in class_var.colors] self.color_label.setText("Target class: ") self.color_combo.addItem("None") self.color_combo.addItems(self.domain.class_vars[0].values) self.color_combo.setCurrentIndex(self.target_class_index) else: self.scene.colors = \ ContinuousPaletteGenerator(*model.domain.class_var.colors) self.color_label.setText("Color by: ") self.color_combo.addItems(self.COL_OPTIONS) self.color_combo.setCurrentIndex(self.regression_colors) self.openContext(self.domain.class_var) self.root_node = self.walkcreate(model.root, None) self.info.setText('{} nodes, {} leaves'. format(model.node_count(), model.leaf_count())) self.setup_scene() self.send("Selected Data", None) self.send(ANNOTATED_DATA_SIGNAL_NAME, create_annotated_table(self.dataset, [])) def walkcreate(self, node_inst, parent=None): """Create a structure of tree nodes from the given model""" node = TreeNode(self.model, node_inst, parent) self.scene.addItem(node) if parent: edge = GraphicsEdge(node1=parent, node2=node) self.scene.addItem(edge) parent.graph_add_edge(edge) for child_inst in node_inst.children: if child_inst is not None: self.walkcreate(child_inst, node) return node def node_tooltip(self, node): return "<br>".join(to_html(rule) for rule in self.model.rule(node.node_inst)) def update_selection(self): if self.model is None: return nodes = [item.node_inst for item in self.scene.selectedItems() if isinstance(item, TreeNode)] data = self.model.get_instances(nodes) self.send("Selected Data", data) self.send(ANNOTATED_DATA_SIGNAL_NAME, create_annotated_table(self.dataset, self.model.get_indices(nodes))) def send_report(self): if not self.model: return items = [("Tree size", self.info.text()), ("Edge widths", ("Fixed", "Relative to root", "Relative to parent")[ # pylint: disable=invalid-sequence-index self.line_width_method])] if self.domain.class_var.is_discrete: items.append(("Target class", self.color_combo.currentText())) elif self.regression_colors != self.COL_DEFAULT: items.append(("Color by", self.COL_OPTIONS[self.regression_colors])) self.report_items(items) self.report_plot(self.scene) def update_node_info(self, node): if self.domain.class_var.is_discrete: self.update_node_info_cls(node) else: self.update_node_info_reg(node) def update_node_info_cls(self, node): """Update the printed contents of the node for classification trees""" node_inst = node.node_inst distr = node_inst.value total = len(node_inst.subset) distr = distr / np.sum(distr) if self.target_class_index: tabs = distr[self.target_class_index - 1] text = "" else: modus = np.argmax(distr) tabs = distr[modus] text = self.domain.class_vars[0].values[int(modus)] + "<br/>" if tabs > 0.999: text += "100%, {}/{}".format(total, total) else: text += "{:2.1f}%, {}/{}".format(100 * tabs, int(total * tabs), total) text = self._update_node_info_attr_name(node, text) node.setHtml('<p style="line-height: 120%; margin-bottom: 0">' '{}</p>'. format(text)) def update_node_info_reg(self, node): """Update the printed contents of the node for regression trees""" node_inst = node.node_inst mean, var = node_inst.value insts = len(node_inst.subset) text = "{:.1f} ± {:.1f}<br/>".format(mean, var) text += "{} instances".format(insts) text = self._update_node_info_attr_name(node, text) node.setHtml('<p style="line-height: 120%; margin-bottom: 0">{}</p>'. format(text)) def toggle_node_color_cls(self): """Update the node color for classification trees""" colors = self.scene.colors for node in self.scene.nodes(): distr = node.node_inst.value total = sum(distr) if self.target_class_index: p = distr[self.target_class_index - 1] / total color = colors[self.target_class_index - 1].lighter( 200 - 100 * p) else: modus = np.argmax(distr) p = distr[modus] / (total or 1) color = colors[int(modus)].lighter(300 - 200 * p) node.backgroundBrush = QBrush(color) self.scene.update() def toggle_node_color_reg(self): """Update the node color for regression trees""" def_color = QColor(192, 192, 255) if self.regression_colors == self.COL_DEFAULT: brush = QBrush(def_color.lighter(100)) for node in self.scene.nodes(): node.backgroundBrush = brush elif self.regression_colors == self.COL_INSTANCE: max_insts = len(self.model.instances) for node in self.scene.nodes(): node.backgroundBrush = QBrush(def_color.lighter( 120 - 20 * len(node.node_inst.subset) / max_insts)) elif self.regression_colors == self.COL_MEAN: minv = np.nanmin(self.dataset.Y) maxv = np.nanmax(self.dataset.Y) fact = 1 / (maxv - minv) if minv != maxv else 1 colors = self.scene.colors for node in self.scene.nodes(): node.backgroundBrush = QBrush( colors[fact * (node.node_inst.value[0] - minv)]) else: nodes = list(self.scene.nodes()) variances = [node.node_inst.value[1] for node in nodes] max_var = max(variances) for node, var in zip(nodes, variances): node.backgroundBrush = QBrush(def_color.lighter( 120 - 20 * var / max_var)) self.scene.update()
class OWFile(widget.OWWidget, RecentPathsWComboMixin): name = "File" id = "orange.widgets.data.file" description = "Read data from an input file or network " \ "and send a data table to the output." icon = "icons/File.svg" priority = 10 category = "Data" keywords = ["file", "load", "read", "open"] class Outputs: data = Output("Data", Table, doc="Attribute-valued dataset read from the input file.") want_main_area = False SEARCH_PATHS = [("sample-datasets", get_sample_datasets_dir())] SIZE_LIMIT = 1e7 LOCAL_FILE, URL = range(2) settingsHandler = PerfectDomainContextHandler( match_values=PerfectDomainContextHandler.MATCH_VALUES_ALL) # pylint seems to want declarations separated from definitions recent_paths: List[RecentPath] recent_urls: List[str] variables: list # Overload RecentPathsWidgetMixin.recent_paths to set defaults recent_paths = Setting([ RecentPath("", "sample-datasets", "iris.tab"), RecentPath("", "sample-datasets", "titanic.tab"), RecentPath("", "sample-datasets", "housing.tab"), RecentPath("", "sample-datasets", "heart_disease.tab"), RecentPath("", "sample-datasets", "brown-selected.tab"), RecentPath("", "sample-datasets", "zoo.tab"), ]) recent_urls = Setting([]) source = Setting(LOCAL_FILE) xls_sheet = ContextSetting("") sheet_names = Setting({}) url = Setting("") variables = ContextSetting([]) domain_editor = SettingProvider(DomainEditor) class Warning(widget.OWWidget.Warning): file_too_big = widget.Msg( "The file is too large to load automatically." " Press Reload to load.") load_warning = widget.Msg("Read warning:\n{}") class Error(widget.OWWidget.Error): file_not_found = widget.Msg("File not found.") missing_reader = widget.Msg("Missing reader.") sheet_error = widget.Msg("Error listing available sheets.") unknown = widget.Msg("Read error:\n{}") class NoFileSelected: pass UserAdviceMessages = [ widget.Message( "Use CSV File Import widget for advanced options " "for comma-separated files", "use-csv-file-import"), widget.Message( "This widget loads only tabular data. Use other widgets to load " "other data types like models, distance matrices and networks.", "other-data-types") ] def __init__(self): super().__init__() RecentPathsWComboMixin.__init__(self) self.domain = None self.data = None self.loaded_file = "" self.reader = None layout = QGridLayout() gui.widgetBox(self.controlArea, margin=0, orientation=layout) vbox = gui.radioButtons(None, self, "source", box=True, addSpace=True, callback=self.load_data, addToLayout=False) rb_button = gui.appendRadioButton(vbox, "File:", addToLayout=False) layout.addWidget(rb_button, 0, 0, Qt.AlignVCenter) box = gui.hBox(None, addToLayout=False, margin=0) box.setSizePolicy(Policy.MinimumExpanding, Policy.Fixed) self.file_combo.setSizePolicy(Policy.MinimumExpanding, Policy.Fixed) self.file_combo.activated[int].connect(self.select_file) box.layout().addWidget(self.file_combo) layout.addWidget(box, 0, 1) file_button = gui.button(None, self, '...', callback=self.browse_file, autoDefault=False) file_button.setIcon(self.style().standardIcon(QStyle.SP_DirOpenIcon)) file_button.setSizePolicy(Policy.Maximum, Policy.Fixed) layout.addWidget(file_button, 0, 2) reload_button = gui.button(None, self, "Reload", callback=self.load_data, autoDefault=False) reload_button.setIcon(self.style().standardIcon( QStyle.SP_BrowserReload)) reload_button.setSizePolicy(Policy.Fixed, Policy.Fixed) layout.addWidget(reload_button, 0, 3) self.sheet_box = gui.hBox(None, addToLayout=False, margin=0) self.sheet_combo = gui.comboBox( None, self, "xls_sheet", callback=self.select_sheet, sendSelectedValue=True, ) self.sheet_combo.setSizePolicy(Policy.MinimumExpanding, Policy.Fixed) self.sheet_label = QLabel() self.sheet_label.setText('Sheet') self.sheet_label.setSizePolicy(Policy.MinimumExpanding, Policy.Fixed) self.sheet_box.layout().addWidget(self.sheet_label, Qt.AlignLeft) self.sheet_box.layout().addWidget(self.sheet_combo, Qt.AlignVCenter) layout.addWidget(self.sheet_box, 2, 1) self.sheet_box.hide() rb_button = gui.appendRadioButton(vbox, "URL:", addToLayout=False) layout.addWidget(rb_button, 3, 0, Qt.AlignVCenter) self.url_combo = url_combo = QComboBox() url_model = NamedURLModel(self.sheet_names) url_model.wrap(self.recent_urls) url_combo.setLineEdit(LineEditSelectOnFocus()) url_combo.setModel(url_model) url_combo.setSizePolicy(Policy.Ignored, Policy.Fixed) url_combo.setEditable(True) url_combo.setInsertPolicy(url_combo.InsertAtTop) url_edit = url_combo.lineEdit() l, t, r, b = url_edit.getTextMargins() url_edit.setTextMargins(l + 5, t, r, b) layout.addWidget(url_combo, 3, 1, 3, 3) url_combo.activated.connect(self._url_set) # whit completer we set that combo box is case sensitive when # matching the history completer = QCompleter() completer.setCaseSensitivity(Qt.CaseSensitive) url_combo.setCompleter(completer) box = gui.vBox(self.controlArea, "Info") self.infolabel = gui.widgetLabel(box, 'No data loaded.') self.warnings = gui.widgetLabel(box, '') box = gui.widgetBox(self.controlArea, "Columns (Double click to edit)") self.domain_editor = DomainEditor(self) self.editor_model = self.domain_editor.model() box.layout().addWidget(self.domain_editor) box = gui.hBox(self.controlArea) gui.button(box, self, "Browse documentation datasets", callback=lambda: self.browse_file(True), autoDefault=False) gui.rubber(box) gui.button(box, self, "Reset", callback=self.reset_domain_edit) self.apply_button = gui.button(box, self, "Apply", callback=self.apply_domain_edit) self.apply_button.setEnabled(False) self.apply_button.setFixedWidth(170) self.editor_model.dataChanged.connect( lambda: self.apply_button.setEnabled(True)) self.set_file_list() # Must not call open_file from within __init__. open_file # explicitly re-enters the event loop (by a progress bar) self.setAcceptDrops(True) if self.source == self.LOCAL_FILE: last_path = self.last_path() if last_path and os.path.exists(last_path) and \ os.path.getsize(last_path) > self.SIZE_LIMIT: self.Warning.file_too_big() return QTimer.singleShot(0, self.load_data) @staticmethod def sizeHint(): return QSize(600, 550) def select_file(self, n): assert n < len(self.recent_paths) super().select_file(n) if self.recent_paths: self.source = self.LOCAL_FILE self.load_data() self.set_file_list() def select_sheet(self): self.recent_paths[0].sheet = self.sheet_combo.currentText() self.load_data() def _url_set(self): url = self.url_combo.currentText() pos = self.recent_urls.index(url) url = url.strip() if not urlparse(url).scheme: url = 'http://' + url self.url_combo.setItemText(pos, url) self.recent_urls[pos] = url self.source = self.URL self.load_data() def browse_file(self, in_demos=False): if in_demos: start_file = get_sample_datasets_dir() if not os.path.exists(start_file): QMessageBox.information( None, "File", "Cannot find the directory with documentation datasets") return else: start_file = self.last_path() or os.path.expanduser("~/") readers = [ f for f in FileFormat.formats if getattr(f, 'read', None) and getattr(f, "EXTENSIONS", None) ] filename, reader, _ = open_filename_dialog(start_file, None, readers) if not filename: return self.add_path(filename) if reader is not None: self.recent_paths[0].file_format = reader.qualified_name() self.source = self.LOCAL_FILE self.load_data() # Open a file, create data from it and send it over the data channel def load_data(self): # We need to catch any exception type since anything can happen in # file readers self.closeContext() self.domain_editor.set_domain(None) self.apply_button.setEnabled(False) self.clear_messages() self.set_file_list() error = self._try_load() if error: error() self.data = None self.sheet_box.hide() self.Outputs.data.send(None) self.infolabel.setText("No data.") def _try_load(self): # pylint: disable=broad-except if self.last_path() and not os.path.exists(self.last_path()): return self.Error.file_not_found try: self.reader = self._get_reader() assert self.reader is not None except Exception: return self.Error.missing_reader if self.reader is self.NoFileSelected: self.Outputs.data.send(None) return None try: self._update_sheet_combo() except Exception: return self.Error.sheet_error with catch_warnings(record=True) as warnings: try: data = self.reader.read() except Exception as ex: log.exception(ex) return lambda x=ex: self.Error.unknown(str(x)) if warnings: self.Warning.load_warning(warnings[-1].message.args[0]) self.infolabel.setText(self._describe(data)) self.loaded_file = self.last_path() add_origin(data, self.loaded_file) self.data = data self.openContext(data.domain) self.apply_domain_edit() # sends data return None def _get_reader(self) -> FileFormat: if self.source == self.LOCAL_FILE: path = self.last_path() if path is None: return self.NoFileSelected if self.recent_paths and self.recent_paths[0].file_format: qname = self.recent_paths[0].file_format reader_class = class_from_qualified_name(qname) reader = reader_class(path) else: reader = FileFormat.get_reader(path) if self.recent_paths and self.recent_paths[0].sheet: reader.select_sheet(self.recent_paths[0].sheet) return reader else: url = self.url_combo.currentText().strip() if url: return UrlReader(url) else: return self.NoFileSelected def _update_sheet_combo(self): if len(self.reader.sheets) < 2: self.sheet_box.hide() self.reader.select_sheet(None) return self.sheet_combo.clear() self.sheet_combo.addItems(self.reader.sheets) self._select_active_sheet() self.sheet_box.show() def _select_active_sheet(self): if self.reader.sheet: try: idx = self.reader.sheets.index(self.reader.sheet) self.sheet_combo.setCurrentIndex(idx) except ValueError: # Requested sheet does not exist in this file self.reader.select_sheet(None) else: self.sheet_combo.setCurrentIndex(0) @staticmethod def _describe(table): def missing_prop(prop): if prop: return f"({prop * 100:.1f}% missing values)" else: return "(no missing values)" domain = table.domain text = "" attrs = getattr(table, "attributes", {}) descs = [ attrs[desc] for desc in ("Name", "Description") if desc in attrs ] if len(descs) == 2: descs[0] = f"<b>{descs[0]}</b>" if descs: text += f"<p>{'<br/>'.join(descs)}</p>" text += f"<p>{len(table)} instance(s)" missing_in_attr = missing_prop(table.has_missing_attribute() and table.get_nan_frequency_attribute()) missing_in_class = missing_prop(table.has_missing_class() and table.get_nan_frequency_class()) text += f"<br/>{len(domain.attributes)} feature(s) {missing_in_attr}" if domain.has_continuous_class: text += f"<br/>Regression; numerical class {missing_in_class}" elif domain.has_discrete_class: text += "<br/>Classification; categorical class " \ f"with {len(domain.class_var.values)} values {missing_in_class}" elif table.domain.class_vars: text += "<br/>Multi-target; " \ f"{len(table.domain.class_vars)} target variables " \ f"{missing_in_class}" else: text += "<br/>Data has no target variable." text += f"<br/>{len(domain.metas)} meta attribute(s)" text += "</p>" if 'Timestamp' in table.domain: # Google Forms uses this header to timestamp responses text += f"<p>First entry: {table[0, 'Timestamp']}<br/>" \ f"Last entry: {table[-1, 'Timestamp']}</p>" return text def storeSpecificSettings(self): self.current_context.modified_variables = self.variables[:] def retrieveSpecificSettings(self): if hasattr(self.current_context, "modified_variables"): self.variables[:] = self.current_context.modified_variables def reset_domain_edit(self): self.domain_editor.reset_domain() self.apply_domain_edit() def apply_domain_edit(self): if self.data is None: table = None else: domain, cols = self.domain_editor.get_domain( self.data.domain, self.data) if not (domain.variables or domain.metas): table = None else: X, y, m = cols table = Table.from_numpy(domain, X, y, m, self.data.W) table.name = self.data.name table.ids = np.array(self.data.ids) table.attributes = getattr(self.data, 'attributes', {}) self.Outputs.data.send(table) self.apply_button.setEnabled(False) def get_widget_name_extension(self): _, name = os.path.split(self.loaded_file) return os.path.splitext(name)[0] def send_report(self): def get_ext_name(filename): try: return FileFormat.names[os.path.splitext(filename)[1]] except KeyError: return "unknown" if self.data is None: self.report_paragraph("File", "No file.") return if self.source == self.LOCAL_FILE: home = os.path.expanduser("~") if self.loaded_file.startswith(home): # os.path.join does not like ~ name = "~" + os.path.sep + \ self.loaded_file[len(home):].lstrip("/").lstrip("\\") else: name = self.loaded_file if self.sheet_combo.isVisible(): name += f" ({self.sheet_combo.currentText()})" self.report_items("File", [("File name", name), ("Format", get_ext_name(name))]) else: self.report_items("Data", [("Resource", self.url), ("Format", get_ext_name(self.url))]) self.report_data("Data", self.data) @staticmethod def dragEnterEvent(event): """Accept drops of valid file urls""" urls = event.mimeData().urls() if urls: try: FileFormat.get_reader(urls[0].toLocalFile()) event.acceptProposedAction() except IOError: pass def dropEvent(self, event): """Handle file drops""" urls = event.mimeData().urls() if urls: self.add_path(urls[0].toLocalFile()) # add first file self.source = self.LOCAL_FILE self.load_data() def workflowEnvChanged(self, key, value, oldvalue): """ Function called when environment changes (e.g. while saving the scheme) It make sure that all environment connected values are modified (e.g. relative file paths are changed) """ self.update_file_list(key, value, oldvalue)
class OWFiles(Orange.widgets.data.owfile.OWFile, RecentPathsWidgetMixin): name = "Files" id = "orangecontrib.infrared.widgets.files" icon = "icons/files.svg" description = "Read data from input files " \ "and send a data table to the output." file_idx = -1 sheet = Orange.widgets.settings.Setting(None) label = Orange.widgets.settings.Setting("") recent_paths = Orange.widgets.settings.Setting([]) def __init__(self): widget.OWWidget.__init__(self) RecentPathsWidgetMixin.__init__(self) self.domain = None self.data = None self.loaded_file = "" self.sheets = [] self.lb = gui.listBox(self.controlArea, self, "file_idx") layout = QGridLayout() gui.widgetBox(self.controlArea, margin=0, orientation=layout) file_button = gui.button(None, self, ' ...', callback=self.browse_files, autoDefault=False) file_button.setIcon(self.style().standardIcon(QStyle.SP_DirOpenIcon)) file_button.setSizePolicy(Policy.Maximum, Policy.Fixed) layout.addWidget(file_button, 0, 0) remove_button = gui.button(None, self, 'Remove', callback=self.remove_item) clear_button = gui.button(None, self, 'Clear', callback=self.clear) layout.addWidget(remove_button, 0, 1) layout.addWidget(clear_button, 0, 2) reload_button = gui.button(None, self, "Reload", callback=self.load_data, autoDefault=False) reload_button.setIcon(self.style().standardIcon( QStyle.SP_BrowserReload)) reload_button.setSizePolicy(Policy.Fixed, Policy.Fixed) layout.addWidget(reload_button, 0, 7) self.sheet_box = gui.hBox(None, addToLayout=False, margin=0) self.sheet_combo = gui.comboBox(None, self, "xls_sheet", callback=self.select_sheet, sendSelectedValue=True) self.sheet_combo.setSizePolicy(Policy.MinimumExpanding, Policy.Fixed) self.sheet_label = QLabel() self.sheet_label.setText('Sheet') self.sheet_label.setSizePolicy(Policy.MinimumExpanding, Policy.Fixed) self.sheet_box.layout().addWidget(self.sheet_label, Qt.AlignLeft) self.sheet_box.layout().addWidget(self.sheet_combo, Qt.AlignVCenter) layout.addWidget(self.sheet_box, 2, 1) self.sheet_box.hide() layout.addWidget(self.sheet_box, 0, 5) label_box = gui.hBox(None, addToLayout=False, margin=0) label = gui.lineEdit(label_box, self, "label", callback=self.set_label, label="Label", orientation=Qt.Horizontal) layout.addWidget(label_box, 0, 6) layout.setColumnStretch(3, 2) box = gui.widgetBox(self.controlArea, "Columns (Double click to edit)") self.domain_editor = DomainEditor(self) self.editor_model = self.domain_editor.model() box.layout().addWidget(self.domain_editor) for i, rp in enumerate(self.recent_paths): self.lb.addItem(rp.abspath) # TODO unresolved paths just disappear! Modify _relocate_recent_files box = gui.hBox(self.controlArea) gui.rubber(box) box.layout().addWidget(self.report_button) self.report_button.setFixedWidth(170) self.apply_button = gui.button(box, self, "Apply", callback=self.apply_domain_edit) self.apply_button.setEnabled(False) self.apply_button.setFixedWidth(170) self.editor_model.dataChanged.connect( lambda: self.apply_button.setEnabled(True)) self._update_sheet_combo() self.load_data() def set_label(self): self.load_data() def add_path(self, filename): recent = RecentPath.create(filename, self._search_paths()) self.recent_paths.append(recent) def set_file_list(self): # need to define it for RecentPathsWidgetMixin pass def _select_active_sheet(self): if self.sheet: try: sheet_list = [s[0] for s in self.sheets] idx = sheet_list.index(self.sheet) self.sheet_combo.setCurrentIndex(idx) except ValueError: # Requested sheet does not exist in this file self.sheet = None else: self.sheet_combo.setCurrentIndex(0) def _update_sheet_combo(self): sheets = Counter() for fn in self.current_filenames(): try: reader = FileFormat.get_reader(fn) sheets.update(reader.sheets) except: pass sheets = sorted(sheets.items(), key=lambda x: x[0]) self.sheets = [(s, s + " (" + str(n) + ")") for s, n in sheets] if len(sheets) < 2: self.sheet_box.hide() self.sheet = None else: self.sheets.insert(0, (None, "(None)")) self.sheet_combo.clear() self.sheet_combo.addItems([s[1] for s in self.sheets]) self._select_active_sheet() self.sheet_box.show() def select_sheet(self): self.sheet = self.sheets[self.sheet_combo.currentIndex()][0] self.load_data() def remove_item(self): ri = [i.row() for i in self.lb.selectedIndexes()] for i in sorted(ri, reverse=True): self.recent_paths.pop(i) self.lb.takeItem(i) self._update_sheet_combo() self.load_data() def clear(self): self.lb.clear() while self.recent_paths: self.recent_paths.pop() self._update_sheet_combo() self.load_data() def browse_files(self, in_demos=False): if in_demos: start_file = get_sample_datasets_dir() if not os.path.exists(start_file): QMessageBox.information( None, "File", "Cannot find the directory with documentation data sets") return else: start_file = self.last_path() or os.path.expanduser("~/") filenames = QFileDialog.getOpenFileNames(self, 'Open Multiple Data Files', start_file, self.dlg_formats) if isinstance(filenames, tuple): # has a file description filenames = filenames[0] if not filenames: return for f in filenames: self.add_path(f) self.lb.addItem(f) self._update_sheet_combo() self.load_data() def current_filenames(self): return [rp.abspath for rp in self.recent_paths] def load_data(self): self.closeContext() fns = self.current_filenames() data_list = [] fnok_list = [] for fn in fns: reader = FileFormat.get_reader(fn) errors = [] with catch_warnings(record=True) as warnings: try: if self.sheet in reader.sheets: reader.select_sheet(self.sheet) data_list.append(reader.read()) fnok_list.append(fn) except Exception as ex: errors.append("An error occurred:") errors.append(str(ex)) #FIXME show error in the list of data self.warning(warnings[-1].message.args[0] if warnings else '') #code below is from concatenate widget if data_list: tables = data_list domain = reduce(domain_union, (table.domain for table in tables)) tables = [ Orange.data.Table.from_table(domain, table) for table in tables ] data = concat(tables) source_var = Orange.data.StringVariable.make("Filename") source_values = list( chain(*(repeat(fn, len(table)) for fn, table in zip(fnok_list, tables)))) label_var = Orange.data.StringVariable.make("Label") label_values = list( chain(*(repeat(self.label, len(table)) for fn, table in zip(fnok_list, tables)))) data = append_columns( data, **{ "metas": [(source_var, source_values), (label_var, label_values)] }) self.data = data self.openContext(data.domain) else: self.data = None self.domain_editor.set_domain(None) self.apply_domain_edit() # sends data
class WelcomeDialog(QDialog): """ A welcome widget shown at startup presenting a series of buttons (actions) for a beginner to choose from. """ triggered = Signal(QAction) def __init__(self, *args, **kwargs): showAtStartup = kwargs.pop("showAtStartup", True) feedbackUrl = kwargs.pop("feedbackUrl", "") super().__init__(*args, **kwargs) self.__triggeredAction = None self.__showAtStartupCheck = None self.__mainLayout = None self.__feedbackUrl = None self.__feedbackLabel = None self.setupUi() self.setFeedbackUrl(feedbackUrl) self.setShowAtStartup(showAtStartup) def setupUi(self): self.setLayout(QVBoxLayout()) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().setSpacing(0) self.__mainLayout = QVBoxLayout() self.__mainLayout.setContentsMargins(0, 40, 0, 40) self.__mainLayout.setSpacing(65) self.layout().addLayout(self.__mainLayout) self.setStyleSheet(WELCOME_WIDGET_BUTTON_STYLE) bottom_bar = QWidget(objectName="bottom-bar") bottom_bar_layout = QHBoxLayout() bottom_bar_layout.setContentsMargins(20, 10, 20, 10) bottom_bar.setLayout(bottom_bar_layout) bottom_bar.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Maximum) self.__showAtStartupCheck = QCheckBox( self.tr("Show at startup"), bottom_bar, checked=False ) self.__feedbackLabel = QLabel( textInteractionFlags=Qt.TextBrowserInteraction, openExternalLinks=True, visible=False, ) bottom_bar_layout.addWidget( self.__showAtStartupCheck, alignment=Qt.AlignVCenter | Qt.AlignLeft ) bottom_bar_layout.addWidget( self.__feedbackLabel, alignment=Qt.AlignVCenter | Qt.AlignRight ) self.layout().addWidget(bottom_bar, alignment=Qt.AlignBottom, stretch=1) self.setSizeGripEnabled(False) self.setFixedSize(620, 390) def setShowAtStartup(self, show): """ Set the 'Show at startup' check box state. """ if self.__showAtStartupCheck.isChecked() != show: self.__showAtStartupCheck.setChecked(show) def showAtStartup(self): """ Return the 'Show at startup' check box state. """ return self.__showAtStartupCheck.isChecked() def setFeedbackUrl(self, url): # type: (str) -> None """ Set an 'feedback' url. When set a link is displayed in the bottom row. """ self.__feedbackUrl = url if url: text = self.tr("Help us improve!") self.__feedbackLabel.setText( '<a href="{url}">{text}</a>'.format(url=url, text=escape(text)) ) else: self.__feedbackLabel.setText("") self.__feedbackLabel.setVisible(bool(url)) def addRow(self, actions, background="light-orange"): """Add a row with `actions`. """ count = self.__mainLayout.count() self.insertRow(count, actions, background) def insertRow(self, index, actions, background="light-orange"): """Insert a row with `actions` at `index`. """ widget = QWidget(objectName="icon-row") layout = QHBoxLayout() layout.setContentsMargins(40, 0, 40, 0) layout.setSpacing(65) widget.setLayout(layout) self.__mainLayout.insertWidget(index, widget, stretch=10, alignment=Qt.AlignCenter) for i, action in enumerate(actions): self.insertAction(index, i, action, background) def insertAction(self, row, index, action, background="light-orange"): """Insert `action` in `row` in position `index`. """ button = self.createButton(action, background) self.insertButton(row, index, button) def insertButton(self, row, index, button): """Insert `button` in `row` in position `index`. """ item = self.__mainLayout.itemAt(row) layout = item.widget().layout() layout.insertWidget(index, button) button.triggered.connect(self.__on_actionTriggered) def createButton(self, action, background="light-orange"): """Create a tool button for action. """ button = WelcomeActionButton(self) button.setDefaultAction(action) button.setText(action.iconText()) button.setIcon(decorate_welcome_icon(action.icon(), background)) button.setToolTip(action.toolTip()) button.setFixedSize(100, 100) button.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) button.setVisible(action.isVisible()) font = QFont(button.font()) font.setPointSize(13) button.setFont(font) return button def buttonAt(self, i, j): """Return the button at i-t row and j-th column. """ item = self.__mainLayout.itemAt(i) row = item.widget() item = row.layout().itemAt(j) return item.widget() def triggeredAction(self): """Return the action that was triggered by the user. """ return self.__triggeredAction def showEvent(self, event): # Clear the triggered action before show. self.__triggeredAction = None super().showEvent(event) def __on_actionTriggered(self, action): """Called when the button action is triggered. """ self.triggered.emit(action) self.__triggeredAction = action
class OWTreeGraph(OWTreeViewer2D): """Graphical visualization of tree models""" name = "Tree Viewer" icon = "icons/TreeViewer.svg" priority = 35 inputs = [("Tree", TreeModel, "ctree")] outputs = [("Selected Data", Table, widget.Default), (ANNOTATED_DATA_SIGNAL_NAME, Table)] settingsHandler = ClassValuesContextHandler() target_class_index = ContextSetting(0) regression_colors = Setting(0) COL_OPTIONS = ["Default", "Number of instances", "Mean value", "Variance"] COL_DEFAULT, COL_INSTANCE, COL_MEAN, COL_VARIANCE = range(4) def __init__(self): super().__init__() self.domain = None self.dataset = None self.clf_dataset = None self.color_label = QLabel("Target class: ") combo = self.color_combo = gui.OrangeComboBox() combo.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed) combo.setSizeAdjustPolicy( QComboBox.AdjustToMinimumContentsLengthWithIcon) combo.setMinimumContentsLength(8) combo.activated[int].connect(self.color_changed) self.display_box.layout().addRow(self.color_label, combo) def set_node_info(self): """Set the content of the node""" for node in self.scene.nodes(): node.set_rect(QRectF()) self.update_node_info(node) w = max([n.rect().width() for n in self.scene.nodes()] + [0]) if w > self.max_node_width: w = self.max_node_width for node in self.scene.nodes(): rect = node.rect() node.set_rect(QRectF(rect.x(), rect.y(), w, rect.height())) self.scene.fix_pos(self.root_node, 10, 10) @staticmethod def _update_node_info_attr_name(node, text): attr = node.node_inst.attr if attr is not None: text += "<hr/>{}".format(attr.name) return text def activate_loaded_settings(self): if not self.model: return super().activate_loaded_settings() if self.domain.class_var.is_discrete: self.color_combo.setCurrentIndex(self.target_class_index) self.toggle_node_color_cls() else: self.color_combo.setCurrentIndex(self.regression_colors) self.toggle_node_color_reg() self.set_node_info() def color_changed(self, i): if self.domain.class_var.is_discrete: self.target_class_index = i self.toggle_node_color_cls() else: self.regression_colors = i self.toggle_node_color_reg() def toggle_node_size(self): self.set_node_info() self.scene.update() self.scene_view.repaint() def toggle_color_cls(self): self.toggle_node_color_cls() self.set_node_info() self.scene.update() def toggle_color_reg(self): self.toggle_node_color_reg() self.set_node_info() self.scene.update() def ctree(self, model=None): """Input signal handler""" self.clear_scene() self.color_combo.clear() self.closeContext() self.model = model if model is None: self.info.setText('No tree.') self.root_node = None self.dataset = None else: self.domain = model.domain self.dataset = model.instances if self.dataset is not None and self.dataset.domain != self.domain: self.clf_dataset = Table.from_table(model.domain, self.dataset) else: self.clf_dataset = self.dataset class_var = self.domain.class_var if class_var.is_discrete: self.scene.colors = [QColor(*col) for col in class_var.colors] self.color_label.setText("Target class: ") self.color_combo.addItem("None") self.color_combo.addItems(self.domain.class_vars[0].values) self.color_combo.setCurrentIndex(self.target_class_index) else: self.scene.colors = \ ContinuousPaletteGenerator(*model.domain.class_var.colors) self.color_label.setText("Color by: ") self.color_combo.addItems(self.COL_OPTIONS) self.color_combo.setCurrentIndex(self.regression_colors) self.openContext(self.domain.class_var) self.root_node = self.walkcreate(model.root, None) self.info.setText('{} nodes, {} leaves'.format( model.node_count(), model.leaf_count())) self.setup_scene() self.send("Selected Data", None) self.send(ANNOTATED_DATA_SIGNAL_NAME, create_annotated_table(self.dataset, [])) def walkcreate(self, node_inst, parent=None): """Create a structure of tree nodes from the given model""" node = TreeNode(self.model, node_inst, parent) self.scene.addItem(node) if parent: edge = GraphicsEdge(node1=parent, node2=node) self.scene.addItem(edge) parent.graph_add_edge(edge) for child_inst in node_inst.children: if child_inst is not None: self.walkcreate(child_inst, node) return node def node_tooltip(self, node): return "<br>".join( to_html(rule) for rule in self.model.rule(node.node_inst)) def update_selection(self): if self.model is None: return nodes = [ item.node_inst for item in self.scene.selectedItems() if isinstance(item, TreeNode) ] data = self.model.get_instances(nodes) self.send("Selected Data", data) self.send( ANNOTATED_DATA_SIGNAL_NAME, create_annotated_table(self.dataset, self.model.get_indices(nodes))) def send_report(self): if not self.model: return items = [ ("Tree size", self.info.text()), ( "Edge widths", ("Fixed", "Relative to root", "Relative to parent")[ # pylint: disable=invalid-sequence-index self.line_width_method]) ] if self.domain.class_var.is_discrete: items.append(("Target class", self.color_combo.currentText())) elif self.regression_colors != self.COL_DEFAULT: items.append( ("Color by", self.COL_OPTIONS[self.regression_colors])) self.report_items(items) self.report_plot(self.scene) def update_node_info(self, node): if self.domain.class_var.is_discrete: self.update_node_info_cls(node) else: self.update_node_info_reg(node) def update_node_info_cls(self, node): """Update the printed contents of the node for classification trees""" node_inst = node.node_inst distr = node_inst.value total = len(node_inst.subset) distr = distr / np.sum(distr) if self.target_class_index: tabs = distr[self.target_class_index - 1] text = "" else: modus = np.argmax(distr) tabs = distr[modus] text = self.domain.class_vars[0].values[int(modus)] + "<br/>" if tabs > 0.999: text += "100%, {}/{}".format(total, total) else: text += "{:2.1f}%, {}/{}".format(100 * tabs, int(total * tabs), total) text = self._update_node_info_attr_name(node, text) node.setHtml('<p style="line-height: 120%; margin-bottom: 0">' '{}</p>'.format(text)) def update_node_info_reg(self, node): """Update the printed contents of the node for regression trees""" node_inst = node.node_inst mean, var = node_inst.value insts = len(node_inst.subset) text = "{:.1f} ± {:.1f}<br/>".format(mean, var) text += "{} instances".format(insts) text = self._update_node_info_attr_name(node, text) node.setHtml( '<p style="line-height: 120%; margin-bottom: 0">{}</p>'.format( text)) def toggle_node_color_cls(self): """Update the node color for classification trees""" colors = self.scene.colors for node in self.scene.nodes(): distr = node.node_inst.value total = sum(distr) if self.target_class_index: p = distr[self.target_class_index - 1] / total color = colors[self.target_class_index - 1].lighter(200 - 100 * p) else: modus = np.argmax(distr) p = distr[modus] / (total or 1) color = colors[int(modus)].lighter(300 - 200 * p) node.backgroundBrush = QBrush(color) self.scene.update() def toggle_node_color_reg(self): """Update the node color for regression trees""" def_color = QColor(192, 192, 255) if self.regression_colors == self.COL_DEFAULT: brush = QBrush(def_color.lighter(100)) for node in self.scene.nodes(): node.backgroundBrush = brush elif self.regression_colors == self.COL_INSTANCE: max_insts = len(self.model.instances) for node in self.scene.nodes(): node.backgroundBrush = QBrush( def_color.lighter(120 - 20 * len(node.node_inst.subset) / max_insts)) elif self.regression_colors == self.COL_MEAN: minv = np.nanmin(self.dataset.Y) maxv = np.nanmax(self.dataset.Y) fact = 1 / (maxv - minv) if minv != maxv else 1 colors = self.scene.colors for node in self.scene.nodes(): node.backgroundBrush = QBrush( colors[fact * (node.node_inst.value[0] - minv)]) else: nodes = list(self.scene.nodes()) variances = [node.node_inst.value[1] for node in nodes] max_var = max(variances) for node, var in zip(nodes, variances): node.backgroundBrush = QBrush( def_color.lighter(120 - 20 * var / max_var)) self.scene.update()
class PreviewBrowser(QWidget): """A Preview Browser for recent/premade scheme selection. """ # Emitted when the current previewed item changes currentIndexChanged = Signal(int) # Emitted when an item is double clicked in the preview list. activated = Signal(int) def __init__(self, *args): QWidget.__init__(self, *args) self.__model = None self.__currentIndex = -1 self.__template = DESCRIPTION_TEMPLATE self.__setupUi() def __setupUi(self): vlayout = QVBoxLayout() vlayout.setContentsMargins(0, 0, 0, 0) top_layout = QHBoxLayout() top_layout.setContentsMargins(12, 12, 12, 12) # Top row with full text description and a large preview # image. self.__label = QLabel( self, objectName="description-label", wordWrap=True, alignment=Qt.AlignTop | Qt.AlignLeft, ) self.__label.setWordWrap(True) self.__label.setFixedSize(220, PREVIEW_SIZE[1]) self.__image = QSvgWidget(self, objectName="preview-image") self.__image.setFixedSize(*PREVIEW_SIZE) self.__imageFrame = DropShadowFrame(self) self.__imageFrame.setWidget(self.__image) # Path text below the description and image path_layout = QHBoxLayout() path_layout.setContentsMargins(12, 0, 12, 0) path_label = QLabel("<b>{0!s}</b>".format(self.tr("Path:")), self, objectName="path-label") self.__path = TextLabel(self, objectName="path-text") path_layout.addWidget(path_label) path_layout.addWidget(self.__path) self.__selectAction = QAction(self.tr("Select"), self, objectName="select-action") top_layout.addWidget(self.__label, 1, alignment=Qt.AlignTop | Qt.AlignLeft) top_layout.addWidget(self.__image, 1, alignment=Qt.AlignTop | Qt.AlignRight) vlayout.addLayout(top_layout) vlayout.addLayout(path_layout) # An list view with small preview icons. self.__previewList = LinearIconView(objectName="preview-list-view") self.__previewList.doubleClicked.connect(self.__onDoubleClicked) vlayout.addWidget(self.__previewList) self.setLayout(vlayout) def setModel(self, model): """Set the item model for preview. """ if self.__model != model: if self.__model: s_model = self.__previewList.selectionModel() s_model.selectionChanged.disconnect(self.__onSelectionChanged) self.__model.dataChanged.disconnect(self.__onDataChanged) self.__model = model self.__previewList.setModel(model) if model: s_model = self.__previewList.selectionModel() s_model.selectionChanged.connect(self.__onSelectionChanged) self.__model.dataChanged.connect(self.__onDataChanged) if model and model.rowCount(): self.setCurrentIndex(0) def model(self): """Return the item model. """ return self.__model def setPreviewDelegate(self, delegate): """Set the delegate to render the preview images. """ raise NotImplementedError def setDescriptionTemplate(self, template): self.__template = template self.__update() def setCurrentIndex(self, index): """Set the selected preview item index. """ if self.__model is not None and self.__model.rowCount(): index = min(index, self.__model.rowCount() - 1) index = self.__model.index(index, 0) sel_model = self.__previewList.selectionModel() # This emits selectionChanged signal and triggers # __onSelectionChanged, currentIndex is updated there. sel_model.select(index, sel_model.ClearAndSelect) elif self.__currentIndex != -1: self.__currentIndex = -1 self.__update() self.currentIndexChanged.emit(-1) def currentIndex(self): """Return the current selected index. """ return self.__currentIndex def __onSelectionChanged(self, *args): """Selected item in the preview list has changed. Set the new description and large preview image. """ rows = self.__previewList.selectedIndexes() if rows: index = rows[0] self.__currentIndex = index.row() else: index = QModelIndex() self.__currentIndex = -1 self.__update() self.currentIndexChanged.emit(self.__currentIndex) def __onDataChanged(self, topleft, bottomRight): """Data changed, update the preview if current index in the changed range. """ if (self.__currentIndex <= topleft.row() and self.__currentIndex >= bottomRight.row()): self.__update() def __onDoubleClicked(self, index): """Double click on an item in the preview item list. """ self.activated.emit(index.row()) def __update(self): """Update the current description. """ if self.__currentIndex != -1: index = self.model().index(self.__currentIndex, 0) else: index = QModelIndex() if not index.isValid(): description = "" name = "" path = "" svg = NO_PREVIEW_SVG else: description = str(index.data(Qt.WhatsThisRole)) if not description: description = "No description." description = escape(description) description = description.replace("\n", "<br/>") name = str(index.data(Qt.DisplayRole)) if not name: name = "Untitled" name = escape(name) path = str(index.data(Qt.StatusTipRole)) svg = str(index.data(previewmodel.ThumbnailSVGRole)) desc_text = self.__template.format(description=description, name=name) self.__label.setText(desc_text) self.__path.setText(path) if not svg: svg = NO_PREVIEW_SVG if svg: self.__image.load(QByteArray(svg.encode("utf-8")))
class ControlBoundingSlider(ControlBase): """ .. image:: https://raw.githubusercontent.com/UmSenhorQualquer/pyforms/master/tutorials/Controls4Docs/ControlBoundingSlider.png """ def __init__(self, *args, **kwargs): """ :param tupple default: The default value is a list containing in the first element the lower value and in the second element the upper value. Default = [20,40]. :param bool horizontal: Flag indicating if the Bounding slider should be draw horizontally or vertically. Default = True. :param bool show_spinboxes: Show or hide the spinboxes. Default = True :param float minimum: Defines the minimum value that can be selected. :param float maximum: Defines the maximum value that can be selected. :param bool convert_2_int: Flag to define if the control should return floats or integers. """ self._horizontal = kwargs.get('horizontal', True) self._show_spinboxes = kwargs.get('show_spinboxes', True) ControlBase.__init__(self, *args, **kwargs) self.min = kwargs.get('min', kwargs.get('minimum', 0)) self.max = kwargs.get('max', kwargs.get('maximum', 100)) self.value = kwargs.get('default', [10,20]) self.convert_2_int = kwargs.get('convert_2_int', False) self.__update() def init_form(self): self._boundingbox = GaugeWidgetHorizontal() if self._horizontal else GaugeWidgetVertical() self._boundingbox.changed_event = self.__update if self._show_spinboxes: self._form = hwidget = QWidget() if self._horizontal: hlayout = QHBoxLayout() else: hlayout = QVBoxLayout() if _api.USED_API == _api.QT_API_PYQT5: hlayout.setContentsMargins(0,0,0,0) elif _api.USED_API == _api.QT_API_PYQT4: hlayout.setMargin(0) if self._label is not None: self._controllabel = QLabel(self.form) hlayout.addWidget(self._controllabel) self._controllabel.setAccessibleName('ControlBoundingSlider-label') self.label = self._label else: self._controllabel = None hwidget.setLayout(hlayout) self._min_spinbox = QSpinBox() self._min_spinbox.valueChanged.connect(self.__min_spinbox_changed) self._min_spinbox.setMaximumWidth(95) self._max_spinbox = QSpinBox() self._max_spinbox.valueChanged.connect(self.__max_spinbox_changed) self._max_spinbox.setMaximumWidth(95) if self._horizontal: hlayout.addWidget(self._min_spinbox) else: hlayout.addWidget(self._max_spinbox) hlayout.addWidget(self._boundingbox) if self._horizontal: hlayout.addWidget(self._max_spinbox) else: hlayout.addWidget(self._min_spinbox) else: self._form = self._boundingbox super(ControlBoundingSlider, self).init_form() def __max_spinbox_changed(self, value): if value < self._boundingbox._minVal: return if hasattr(self, '_is_updating_spinboxes'): return self.scale = self.__find_scale_factor(value) self._boundingbox._maxVal = value self._boundingbox.repaint() self.changed_event() def __min_spinbox_changed(self, value): if value > self._boundingbox._maxVal: return if hasattr(self, '_is_updating_spinboxes'): return self.scale = self.__find_scale_factor(value) self._boundingbox._minVal = value self._boundingbox.repaint() self.changed_event() def __update(self): l, h = self._boundingbox._minVal, self._boundingbox._maxVal self._is_updating_spinboxes = True if self._show_spinboxes: self._min_spinbox.setValue(l) self._max_spinbox.setValue(h) del self._is_updating_spinboxes self.changed_event() def changed_event(self): pass def __find_scale_factor(self, value): scale = 1.0 new_value = value while abs(new_value) < 0.0: scale *= 10.0 new_value = value * scale return scale def load_form(self, data, path=None): """ Load a value from the dict variable @param data: dictionary with the value of the Control """ self.convert_2_int = data.get('convert-int', self.convert_2_int) self.scale = data.get('scale', self.scale) self.max = data.get('max', self.max) self.min = data.get('min', self.min) self.value = data.get('value', self.value) def save_form(self, data, path=None): """ Save a value to dict variable @param data: dictionary with to where the value of the Control will be added """ data['value'] = self.value data['max'] = self.max data['min'] = self.min data['scale'] = self.scale data['convert-int'] = self.convert_2_int return data ########################################################################## ############ Properties ################################################## ########################################################################## @property def label(self): return self._controllabel.getText() @label.setter def label(self, value): self._controllabel.setText(value) @property def value(self): """ Sets and gets the control value. It should be a list or tuple of 2 values. """ return self._boundingbox._minVal, self._boundingbox._maxVal @value.setter def value(self, value): self.scale = self.__find_scale_factor(value[0]) self._boundingbox._minVal, self._boundingbox._maxVal = value[0], value[1] if hasattr(self, '_min_spinbox'): self._min_spinbox.setValue(value[0]) if hasattr(self, '_max_spinbox'): self._max_spinbox.setValue(value[1]) ControlBase.value.fset(self, value) self._boundingbox.repaint() @property def min(self): """ Sets and gets the minimum value possible. """ return self._boundingbox._lower @min.setter def min(self, value): self._boundingbox._lower = value self._boundingbox.repaint() if hasattr(self, '_min_spinbox'): self._min_spinbox.setMinimum(value) if hasattr(self, '_max_spinbox'): self._max_spinbox.setMinimum(value) @property def max(self): """ Sets and gets the maximum value possible. """ return self._boundingbox._higher @max.setter def max(self, value): self._boundingbox._higher = value self._boundingbox.repaint() if hasattr(self, '_min_spinbox'): self._min_spinbox.setMaximum(value) if hasattr(self, '_max_spinbox'): self._max_spinbox.setMaximum(value) @property def scale(self): """ Sets and gets the scale value. """ return self._boundingbox.scale @scale.setter def scale(self, value): self._boundingbox.scale = value @property def convert_2_int(self): """ Flag to define if the control should return floats or integers. """ return not self._boundingbox._use_float @convert_2_int.setter def convert_2_int(self, value): self._boundingbox._use_float = not value
class ParametersEditor(QGroupBox): remove_clicked = Signal() default_prefix = "Var" parameters = () def __init__(self, parent=None): super().__init__(parent) self.setTitle(self.name) self.setLayout(QVBoxLayout()) hbox = gui.hBox(self) self.add_standard_parameters(hbox) gui.separator(hbox, 20) gui.rubber(hbox) self.add_specific_parameters(hbox) self.error = QLabel() self.error.setHidden(True) self.layout().addWidget(self.error) self.trash_button = trash = QPushButton( self, icon=self.style().standardIcon(QStyle.SP_DockWidgetCloseButton)) trash.setGeometry(0, 20, 15, 15) trash.setFlat(True) trash.setHidden(True) trash.clicked.connect(self.on_trash_clicked) def on_trash_clicked(self): self.remove_clicked.emit() def enterEvent(self, e): super().enterEvent(e) self.trash_button.setHidden(False) def leaveEvent(self, e): super().enterEvent(e) self.trash_button.setHidden(True) def add_standard_parameters(self, parent): form = QFormLayout() parent.layout().addLayout(form) self.number_of_vars = edit = QLineEdit() edit.setValidator(pos_int.validator) edit.setText("10") edit.setAlignment(Qt.AlignRight) edit.setFixedWidth(50) form.addRow("Variables", edit) self.name_prefix = edit = QLineEdit() edit.setPlaceholderText(self.default_prefix) edit.setFixedWidth(50) form.addRow("Name prefix", edit) def fix_standard_parameters(self, number_of_vars, name_prefix): self.number_of_vars.setDisabled(number_of_vars is not None) self.name_prefix.setDisabled(name_prefix is not None) if number_of_vars is not None: self.number_of_vars.setText(str(number_of_vars)) if name_prefix is not None: self.name_prefix.setText(name_prefix) def add_specific_parameters(self, parent): form = QFormLayout() self.edits = {} for parameter in self.parameters: edit = self.edits[parameter.arg_name] = QLineEdit() edit.convert = getattr(parameter.arg_type, "convert", parameter.arg_type) validator = getattr(parameter.arg_type, "validator", None) if validator is not None: edit.setValidator(validator) edit.setText(str(parameter.default)) edit.setAlignment(Qt.AlignRight) edit.setFixedWidth(50) form.addRow(parameter.label, edit) parent.layout().addLayout(form) @property def nvars(self): return int(self.number_of_vars.text()) def get(self, name): edit = self.edits[name] return edit.convert(edit.text()) def get_parameters(self): return {name: self.get(name) for name in self.edits} @staticmethod def check(**_): return None def set_error(self, error): self.error.setText( f"<font color='red'>{error}</font>" if error else "") self.error.setHidden(error is None) def prepare_variables(self, used_names, ndigits): raise NotImplementedError def get_name_prefix(self, used_names): name = self.name_prefix.text() or self.default_prefix start = used_names.get(name, 0) + 1 return name, start def generate_data(self, ninstances): parameters = self.get_parameters() error = self.check(**parameters) # pylint: disable=assignment-from-none data = None if not error: try: data = self.rvs(size=(ninstances, self.nvars), **parameters) except: # can throw anything, pylint: disable=bare-except error = f"Error while sampling. Check distribution parameters." self.set_error(error) return data def pack_settings(self): return dict(number_of_vars=self.number_of_vars.text(), name_prefix=self.name_prefix.text(), **{name: edit.text() for name, edit in self.edits.items()}) def unpack_settings(self, settings): edits = dict(number_of_vars=self.number_of_vars, name_prefix=self.name_prefix, **self.edits) for name, value in settings.items(): edits[name].setText(value)
class OWDataSets(widget.OWWidget): name = "Data Sets" description = "Load a data set from an online repository" icon = "icons/DataSets.svg" priority = 20 replaces = ["orangecontrib.prototypes.widgets.owdatasets.OWDataSets"] # The following constants can be overridden in a subclass # to reuse this widget for a different repository # Take care when refactoring! (used in e.g. single-cell) INDEX_URL = "http://datasets.orange.biolab.si/" DATASET_DIR = "datasets" class Error(widget.OWWidget.Error): no_remote_datasets = Msg("Could not fetch data set list") class Warning(widget.OWWidget.Warning): only_local_datasets = Msg("Could not fetch data sets list, only local " "cached data sets are shown") class Outputs: data = Output("Data", Orange.data.Table) #: Selected data set id selected_id = settings.Setting(None) # type: Optional[str] auto_commit = settings.Setting(False) # type: bool #: main area splitter state splitter_state = settings.Setting(b'') # type: bytes header_state = settings.Setting(b'') # type: bytes def __init__(self): super().__init__() self.local_cache_path = os.path.join(data_dir(), self.DATASET_DIR) self.__awaiting_state = None # type: Optional[_FetchState] box = gui.widgetBox(self.controlArea, "Info") self.infolabel = QLabel(text="Initializing...\n\n") box.layout().addWidget(self.infolabel) gui.widgetLabel(self.mainArea, "Filter") self.filterLineEdit = QLineEdit( textChanged=self.filter ) self.mainArea.layout().addWidget(self.filterLineEdit) self.splitter = QSplitter(orientation=Qt.Vertical) self.view = QTreeView( sortingEnabled=True, selectionMode=QTreeView.SingleSelection, alternatingRowColors=True, rootIsDecorated=False, editTriggers=QTreeView.NoEditTriggers, ) box = gui.widgetBox(self.splitter, "Description", addToLayout=False) self.descriptionlabel = QLabel( wordWrap=True, textFormat=Qt.RichText, ) self.descriptionlabel = QTextBrowser( openExternalLinks=True, textInteractionFlags=(Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse) ) self.descriptionlabel.setFrameStyle(QTextBrowser.NoFrame) # no (white) text background self.descriptionlabel.viewport().setAutoFillBackground(False) box.layout().addWidget(self.descriptionlabel) self.splitter.addWidget(self.view) self.splitter.addWidget(box) self.splitter.setSizes([300, 200]) self.splitter.splitterMoved.connect( lambda: setattr(self, "splitter_state", bytes(self.splitter.saveState())) ) self.mainArea.layout().addWidget(self.splitter) self.controlArea.layout().addStretch(10) gui.auto_commit(self.controlArea, self, "auto_commit", "Send Data") model = QStandardItemModel(self) model.setHorizontalHeaderLabels(HEADER) proxy = QSortFilterProxyModel() proxy.setSourceModel(model) proxy.setFilterKeyColumn(-1) proxy.setFilterCaseSensitivity(False) self.view.setModel(proxy) if self.splitter_state: self.splitter.restoreState(self.splitter_state) self.view.setItemDelegateForColumn( Header.Size, SizeDelegate(self)) self.view.setItemDelegateForColumn( Header.Local, gui.IndicatorItemDelegate(self, role=Qt.DisplayRole)) self.view.setItemDelegateForColumn( Header.Instances, NumericalDelegate(self)) self.view.setItemDelegateForColumn( Header.Variables, NumericalDelegate(self)) self.view.resizeColumnToContents(Header.Local) if self.header_state: self.view.header().restoreState(self.header_state) self.setBlocking(True) self.setStatusMessage("Initializing") self._executor = ThreadPoolExecutor(max_workers=1) f = self._executor.submit(self.list_remote) w = FutureWatcher(f, parent=self) w.done.connect(self.__set_index) @Slot(object) def __set_index(self, f): # type: (Future) -> None # set results from `list_remote` query. assert QThread.currentThread() is self.thread() assert f.done() self.setBlocking(False) self.setStatusMessage("") allinfolocal = self.list_local() try: res = f.result() except Exception: log.exception("Error while fetching updated index") if not allinfolocal: self.Error.no_remote_datasets() else: self.Warning.only_local_datasets() res = {} allinforemote = res # type: Dict[Tuple[str, str], dict] allkeys = set(allinfolocal) if allinforemote is not None: allkeys = allkeys | set(allinforemote) allkeys = sorted(allkeys) def info(file_path): if file_path in allinforemote: info = allinforemote[file_path] else: info = allinfolocal[file_path] islocal = file_path in allinfolocal isremote = file_path in allinforemote outdated = islocal and isremote and ( allinforemote[file_path].get('version', '') != allinfolocal[file_path].get('version', '')) islocal &= not outdated prefix = os.path.join('', *file_path[:-1]) filename = file_path[-1] return namespace( prefix=prefix, filename=filename, title=info.get("title", filename), datetime=info.get("datetime", None), description=info.get("description", None), references=info.get("references", []), seealso=info.get("seealso", []), source=info.get("source", None), year=info.get("year", None), instances=info.get("instances", None), variables=info.get("variables", None), target=info.get("target", None), missing=info.get("missing", None), tags=info.get("tags", []), size=info.get("size", None), islocal=islocal, outdated=outdated ) model = QStandardItemModel(self) model.setHorizontalHeaderLabels(HEADER) current_index = -1 for i, file_path in enumerate(allkeys): datainfo = info(file_path) item1 = QStandardItem() item1.setData(" " if datainfo.islocal else "", Qt.DisplayRole) item1.setData(datainfo, Qt.UserRole) item2 = QStandardItem(datainfo.title) item3 = QStandardItem() item3.setData(datainfo.size, Qt.DisplayRole) item4 = QStandardItem() item4.setData(datainfo.instances, Qt.DisplayRole) item5 = QStandardItem() item5.setData(datainfo.variables, Qt.DisplayRole) item6 = QStandardItem() item6.setData(datainfo.target, Qt.DisplayRole) if datainfo.target: item6.setIcon(variable_icon(datainfo.target)) item7 = QStandardItem() item7.setData(", ".join(datainfo.tags) if datainfo.tags else "", Qt.DisplayRole) row = [item1, item2, item3, item4, item5, item6, item7] model.appendRow(row) if os.path.join(*file_path) == self.selected_id: current_index = i hs = self.view.header().saveState() model_ = self.view.model().sourceModel() self.view.model().setSourceModel(model) self.view.header().restoreState(hs) model_.deleteLater() model_.setParent(None) self.view.selectionModel().selectionChanged.connect( self.__on_selection ) # Update the info text self.infolabel.setText(format_info(model.rowCount(), len(allinfolocal))) if current_index != -1: selmodel = self.view.selectionModel() selmodel.select( self.view.model().mapFromSource(model.index(current_index, 0)), QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows) def __update_cached_state(self): model = self.view.model().sourceModel() localinfo = self.list_local() assert isinstance(model, QStandardItemModel) allinfo = [] for i in range(model.rowCount()): item = model.item(i, 0) info = item.data(Qt.UserRole) info.islocal = (info.prefix, info.filename) in localinfo item.setData(" " if info.islocal else "", Qt.DisplayRole) allinfo.append(info) self.infolabel.setText(format_info( model.rowCount(), sum(info.islocal for info in allinfo))) def selected_dataset(self): """ Return the current selected data set info or None if not selected Returns ------- info : Optional[namespace] """ rows = self.view.selectionModel().selectedRows(0) assert 0 <= len(rows) <= 1 current = rows[0] if rows else None # type: Optional[QModelIndex] if current is not None: info = current.data(Qt.UserRole) assert isinstance(info, namespace) else: info = None return info def filter(self): filter_string = self.filterLineEdit.text().strip() proxyModel = self.view.model() if proxyModel: proxyModel.setFilterFixedString(filter_string) def __on_selection(self): # Main data sets view selection has changed rows = self.view.selectionModel().selectedRows(0) assert 0 <= len(rows) <= 1 current = rows[0] if rows else None # type: Optional[QModelIndex] if current is not None: current = self.view.model().mapToSource(current) di = current.data(Qt.UserRole) text = description_html(di) self.descriptionlabel.setText(text) self.selected_id = os.path.join(di.prefix, di.filename) else: self.descriptionlabel.setText("") self.selected_id = None self.commit() def commit(self): """ Commit a dataset to the output immediately (if available locally) or schedule download background and an eventual send. During the download the widget is in blocking state (OWWidget.isBlocking) """ di = self.selected_dataset() if di is not None: self.Error.clear() if self.__awaiting_state is not None: # disconnect from the __commit_complete self.__awaiting_state.watcher.done.disconnect( self.__commit_complete) # .. and connect to update_cached_state # self.__awaiting_state.watcher.done.connect( # self.__update_cached_state) # TODO: There are possible pending __progress_advance queued self.__awaiting_state.pb.advance.disconnect( self.__progress_advance) self.progressBarFinished(processEvents=None) self.__awaiting_state = None if not di.islocal: pr = progress() callback = lambda pr=pr: pr.advance.emit() pr.advance.connect(self.__progress_advance, Qt.QueuedConnection) self.progressBarInit(processEvents=None) self.setStatusMessage("Fetching...") self.setBlocking(True) f = self._executor.submit( ensure_local, self.INDEX_URL, di.prefix, di.filename, self.local_cache_path, force=di.outdated, progress_advance=callback) w = FutureWatcher(f, parent=self) w.done.connect(self.__commit_complete) self.__awaiting_state = _FetchState(f, w, pr) else: self.setStatusMessage("") self.setBlocking(False) self.commit_cached(di.prefix, di.filename) else: self.Outputs.data.send(None) @Slot(object) def __commit_complete(self, f): # complete the commit operation after the required file has been # downloaded assert QThread.currentThread() is self.thread() assert self.__awaiting_state is not None assert self.__awaiting_state.future is f if self.isBlocking(): self.progressBarFinished(processEvents=None) self.setBlocking(False) self.setStatusMessage("") self.__awaiting_state = None try: path = f.result() except Exception as ex: log.exception("Error:") self.error(format_exception(ex)) path = None self.__update_cached_state() if path is not None: data = Orange.data.Table(path) else: data = None self.Outputs.data.send(data) def commit_cached(self, prefix, filename): path = LocalFiles(self.local_cache_path).localpath(prefix, filename) self.Outputs.data.send(Orange.data.Table(path)) @Slot() def __progress_advance(self): assert QThread.currentThread() is self.thread() self.progressBarAdvance(1, processEvents=None) def onDeleteWidget(self): super().onDeleteWidget() if self.__awaiting_state is not None: self.__awaiting_state.watcher.done.disconnect(self.__commit_complete) self.__awaiting_state.pb.advance.disconnect(self.__progress_advance) self.__awaiting_state = None def sizeHint(self): return QSize(900, 600) def closeEvent(self, event): self.splitter_state = bytes(self.splitter.saveState()) self.header_state = bytes(self.view.header().saveState()) super().closeEvent(event) def list_remote(self): # type: () -> Dict[Tuple[str, str], dict] client = ServerFiles(server=self.INDEX_URL) return client.allinfo() def list_local(self): # type: () -> Dict[Tuple[str, str], dict] return LocalFiles(self.local_cache_path).allinfo()
class FileUploadHelper(QDialog): # settings kegg_domain = 'KEGG' supported_domains = OrderedDict({ 'Gene Ontology': gene_ontology_domain, 'Gene Sets': gene_sets_domain }) supported_organisms = [ common_taxid_to_name(tax_id) for tax_id in common_taxids() ] hierarchies = { 'GO - Biological Process': ('GO', 'biological_process'), 'GO - Molecular Function': ('GO', 'molecular_function'), 'GO - Cellular Component': ('GO', 'cellular_component'), 'KEGG - Pathways': ('KEGG', 'pathways'), 'KEGG - Orthologs': ('KEGG', 'orthologs') } def __init__(self, parent=None): super(FileUploadHelper, self).__init__( parent, Qt.Window | Qt.WindowTitleHint | Qt.CustomizeWindowHint | Qt.WindowCloseButtonHint | Qt.WindowMaximizeButtonHint) self.setAttribute(Qt.WA_DeleteOnClose) self.setWindowTitle('Add new file') self.info_state = INFO_FILE_SCHEMA self.layout = QVBoxLayout(self) # domain selection combobox self.domain_selection = QComboBox() self.domain_selection.addItems(self.supported_domains.keys()) self.domain_selection.currentIndexChanged.connect( self.__on_domain_selection) self.__create_selection_row('Domain: ', self.domain_selection) # domain selection combobox self.hierarchy_selection = QComboBox() self.hierarchy_selection.addItems(self.hierarchies.keys()) self.layout.addWidget(self.hierarchy_selection, alignment=Qt.AlignVCenter) self.__on_domain_selection() # select organism self.organism_selection = QComboBox() self.organism_selection.addItems(self.supported_organisms) self.__create_selection_row('Organism: ', self.organism_selection) # title self.line_edit_title = QLineEdit() self.__create_selection_row('Title: ', self.line_edit_title) # tags self.line_edit_tags = QLineEdit() self.__create_selection_row('Tags (comma-separated): ', self.line_edit_tags) # file selector self.file_info = QLabel() self.file_select_btn = QPushButton('Select File', self) self.file_select_btn.clicked.connect(self.__handle_file_selector) self.__create_selection_row(' ', self.file_select_btn) # add file info section self.layout.addWidget(self.file_info, alignment=Qt.AlignCenter) self.layout.addStretch(1) # Ok and Cancel buttons self.buttons = QDialogButtonBox( QDialogButtonBox.Ok | QDialogButtonBox.Cancel, Qt.Horizontal, self) self.layout.addWidget(self.buttons, alignment=Qt.AlignJustify) self.buttons.accepted.connect(self.__accept) self.buttons.rejected.connect(self.__close) # path to a selected file self.file_path = None def __on_domain_selection(self): selected = self.__get_selected_domain() == gene_sets_domain self.hierarchy_selection.setVisible(selected) def __get_selected_domain(self): domain_label = list(self.supported_domains.keys())[ self.domain_selection.currentIndex()] return self.supported_domains[domain_label] def __get_selected_hier(self): hier_label = list( self.hierarchies.keys())[self.hierarchy_selection.currentIndex()] return self.hierarchies[hier_label] def __create_selection_row(self, label, widget): self.layout.addWidget(QLabel(label), alignment=Qt.AlignLeft) self.layout.addWidget(widget, alignment=Qt.AlignVCenter) def __accept(self): if self.file_path: self.info_state = self.__parse_selection() self.__move_to_serverfiles_folder(self.file_path) self.parent().initialize_files_view() self.close() def __close(self): self.close() def closeEvent(self, event): # clean-up self.parent()._dialog = None def __filename(self, domain, organism): """ Create filename based od domain name and organism. """ if domain in self.supported_domains.values( ) and domain == gene_ontology_domain and organism: return FILENAME_ANNOTATION.format(organism) elif domain in self.supported_domains.values( ) and domain == gene_sets_domain and organism: return filename((self.__get_selected_hier()), organism) def __parse_selection(self): try: domain = self.__get_selected_domain() organism = taxname_to_taxid(self.supported_organisms[ self.organism_selection.currentIndex()]) except KeyError as e: raise e return { 'domain': domain, 'organism': organism, 'filename': self.__filename(domain, organism), 'title': self.line_edit_title.text(), 'tags': self.line_edit_tags.text().split(','), 'source': SOURCE_USER } def __move_to_serverfiles_folder(self, selected_file_path): domain_path = serverfiles.localpath(self.info_state['domain']) file_path = os.path.join(domain_path, self.info_state['filename']) create_folder(domain_path) try: copyfile(selected_file_path, file_path) except IOError as e: # TODO: handle error properly raise e # if copy successful create .info file create_info_file(file_path, **self.info_state) def __handle_file_selector(self): self.file_path = QFileDialog.getOpenFileName(self, 'Open File')[0] self.file_info.setText('Selected File: {}'.format( os.path.basename(self.file_path)))
class MessagesWidget(QWidget): """ An iconified multiple message display area. `MessagesWidget` displays a short message along with an icon. If there are multiple messages they are summarized. The user can click on the widget to display the full message text in a popup view. """ #: Signal emitted when an embedded html link is clicked #: (if `openExternalLinks` is `False`). linkActivated = Signal(str) #: Signal emitted when an embedded html link is hovered. linkHovered = Signal(str) Severity = Severity #: General informative message. Information = Severity.Information #: A warning message severity. Warning = Severity.Warning #: An error message severity. Error = Severity.Error Message = Message def __init__(self, parent=None, openExternalLinks=False, **kwargs): kwargs.setdefault( "sizePolicy", QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)) super().__init__(parent, **kwargs) self.__openExternalLinks = openExternalLinks # type: bool self.__messages = OrderedDict() # type: Dict[Hashable, Message] #: The full (joined all messages text - rendered as html), displayed #: in a tooltip. self.__fulltext = "" #: The full text displayed in a popup. Is empty if the message is #: short self.__popuptext = "" #: Leading icon self.__iconwidget = IconWidget( sizePolicy=QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) #: Inline message text self.__textlabel = QLabel( wordWrap=False, textInteractionFlags=Qt.LinksAccessibleByMouse, openExternalLinks=self.__openExternalLinks, sizePolicy=QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum), ) #: Indicator that extended contents are accessible with a click on the #: widget. self.__popupicon = QLabel( sizePolicy=QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum), text="\N{VERTICAL ELLIPSIS}", visible=False, ) self.__textlabel.linkActivated.connect(self.linkActivated) self.__textlabel.linkHovered.connect(self.linkHovered) self.setLayout(QHBoxLayout()) self.layout().setContentsMargins(2, 1, 2, 1) self.layout().setSpacing(0) self.layout().addWidget(self.__iconwidget) self.layout().addSpacing(4) self.layout().addWidget(self.__textlabel) self.layout().addWidget(self.__popupicon) self.__textlabel.setAttribute(Qt.WA_MacSmallSize) def sizeHint(self): sh = super().sizeHint() h = self.style().pixelMetric(QStyle.PM_SmallIconSize) return sh.expandedTo(QSize(0, h + 2)) def openExternalLinks(self): # type: () -> bool """ If True then linkActivated signal will be emitted when the user clicks on an html link in a message, otherwise links are opened using `QDesktopServices.openUrl` """ return self.__openExternalLinks def setOpenExternalLinks(self, state): # type: (bool) -> None """ """ # TODO: update popup if open self.__openExternalLinks = state self.__textlabel.setOpenExternalLinks(state) def setMessage(self, message_id, message): # type: (Hashable, Message) -> None """ Add a `message` for `message_id` to the current display. Note ---- Set an empty `Message` instance to clear the message display but retain the relative ordering in the display should a message for `message_id` reactivate. """ self.__messages[message_id] = message self.__update() def removeMessage(self, message_id): # type: (Hashable) -> None """ Remove message for `message_id` from the display. Note ---- Setting an empty `Message` instance will also clear the display, however the relative ordering of the messages will be retained, should the `message_id` 'reactivate'. """ del self.__messages[message_id] self.__update() def setMessages(self, messages): # type: (Union[Iterable[Tuple[Hashable, Message]], Dict[Hashable, Message]]) -> None """ Set multiple messages in a single call. """ messages = OrderedDict(messages) self.__messages.update(messages) self.__update() def clear(self): # type: () -> None """ Clear all messages. """ self.__messages.clear() self.__update() def messages(self): # type: () -> List[Message] return list(self.__messages.values()) def summarize(self): # type: () -> Message """ Summarize all the messages into a single message. """ messages = [m for m in self.__messages.values() if not m.isEmpty()] if messages: return summarize(messages) else: return Message() def __update(self): """ Update the current display state. """ self.ensurePolished() summary = self.summarize() icon = message_icon(summary) self.__iconwidget.setIcon(icon) self.__iconwidget.setVisible(not (summary.isEmpty() or icon.isNull())) self.__textlabel.setTextFormat(summary.textFormat) self.__textlabel.setText(summary.text) messages = [m for m in self.__messages.values() if not m.isEmpty()] if messages: messages = sorted(messages, key=attrgetter("severity"), reverse=True) fulltext = "<hr/>".join(m.asHtml() for m in messages) else: fulltext = "" self.__fulltext = fulltext self.setToolTip(fulltext) def is_short(m): return not (m.informativeText or m.detailedText) if not messages or len(messages) == 1 and is_short(messages[0]): self.__popuptext = "" else: self.__popuptext = fulltext self.__popupicon.setVisible(bool(self.__popuptext)) self.layout().activate() def mousePressEvent(self, event): if event.button() == Qt.LeftButton: if self.__popuptext: popup = QMenu(self) label = QLabel( self, textInteractionFlags=Qt.TextBrowserInteraction, openExternalLinks=self.__openExternalLinks, text=self.__popuptext, ) label.linkActivated.connect(self.linkActivated) label.linkHovered.connect(self.linkHovered) action = QWidgetAction(popup) action.setDefaultWidget(label) popup.addAction(action) popup.popup(event.globalPos(), action) event.accept() return else: super().mousePressEvent(event) def enterEvent(self, event): super().enterEvent(event) self.update() def leaveEvent(self, event): super().leaveEvent(event) self.update() def changeEvent(self, event): super().changeEvent(event) self.update() def paintEvent(self, event): opt = QStyleOption() opt.initFrom(self) if not self.__popupicon.isVisible(): return if not (opt.state & QStyle.State_MouseOver or opt.state & QStyle.State_HasFocus): return palette = opt.palette # type: QPalette if opt.state & QStyle.State_HasFocus: pen = QPen(palette.color(QPalette.Highlight)) else: pen = QPen(palette.color(QPalette.Dark)) if (self.__fulltext and opt.state & QStyle.State_MouseOver and opt.state & QStyle.State_Active): g = QLinearGradient() g.setCoordinateMode(QLinearGradient.ObjectBoundingMode) base = palette.color(QPalette.Window) base.setAlpha(90) g.setColorAt(0, base.lighter(200)) g.setColorAt(0.6, base) g.setColorAt(1.0, base.lighter(200)) brush = QBrush(g) else: brush = QBrush(Qt.NoBrush) p = QPainter(self) p.setBrush(brush) p.setPen(pen) p.drawRect(opt.rect.adjusted(0, 0, -1, -1))
class OWDatabasesUpdate(OWWidget): name = "Databases Update" description = "Update local systems biology databases." icon = "../widgets/icons/Databases.svg" priority = 10 inputs = [] outputs = [] want_main_area = False def __init__(self, parent=None, signalManager=None, name="Databases update"): OWWidget.__init__(self, parent, signalManager, name, wantMainArea=False) self.searchString = "" fbox = gui.widgetBox(self.controlArea, "Filter") self.completer = TokenListCompleter( self, caseSensitivity=Qt.CaseInsensitive) self.lineEditFilter = QLineEdit(textChanged=self.SearchUpdate) self.lineEditFilter.setCompleter(self.completer) fbox.layout().addWidget(self.lineEditFilter) box = gui.widgetBox(self.controlArea, "Files") self.filesView = QTreeWidget(self) self.filesView.setHeaderLabels( ["", "Data Source", "Update", "Last Updated", "Size"]) self.filesView.setRootIsDecorated(False) self.filesView.setUniformRowHeights(True) self.filesView.setSelectionMode(QAbstractItemView.NoSelection) self.filesView.setSortingEnabled(True) self.filesView.sortItems(1, Qt.AscendingOrder) self.filesView.setItemDelegateForColumn( 0, UpdateOptionsItemDelegate(self.filesView)) self.filesView.model().layoutChanged.connect(self.SearchUpdate) box.layout().addWidget(self.filesView) box = gui.widgetBox(self.controlArea, orientation="horizontal") self.updateButton = gui.button( box, self, "Update all", callback=self.UpdateAll, tooltip="Update all updatable files", ) self.downloadButton = gui.button( box, self, "Download all", callback=self.DownloadFiltered, tooltip="Download all filtered files shown" ) self.cancelButton = gui.button( box, self, "Cancel", callback=self.Cancel, tooltip="Cancel scheduled downloads/updates." ) self.retryButton = gui.button( box, self, "Reconnect", callback=self.RetrieveFilesList ) self.retryButton.hide() gui.rubber(box) self.warning(0) box = gui.widgetBox(self.controlArea, orientation="horizontal") gui.rubber(box) self.infoLabel = QLabel() self.infoLabel.setAlignment(Qt.AlignCenter) self.controlArea.layout().addWidget(self.infoLabel) self.infoLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.updateItems = [] self.resize(800, 600) self.progress = ProgressState(self, maximum=3) self.progress.valueChanged.connect(self._updateProgress) self.progress.rangeChanged.connect(self._updateProgress) self.executor = ThreadExecutor( threadPool=QThreadPool(maxThreadCount=2) ) task = Task(self, function=self.RetrieveFilesList) task.exceptionReady.connect(self.HandleError) task.start() self._tasks = [] self._haveProgress = False def RetrieveFilesList(self): self.retryButton.hide() self.warning(0) self.progress.setRange(0, 3) task = Task(function=partial(retrieveFilesList, methodinvoke(self.progress, "advance"))) task.resultReady.connect(self.SetFilesList) task.exceptionReady.connect(self.HandleError) self.executor.submit(task) self.setEnabled(False) def SetFilesList(self, serverInfo): """ Set the files to show. """ self.setEnabled(True) localInfo = serverfiles.allinfo() all_tags = set() self.filesView.clear() self.updateItems = [] for item in join_info_dict(localInfo, serverInfo): tree_item = UpdateTreeWidgetItem(item) options_widget = UpdateOptionsWidget(item.state) options_widget.item = item options_widget.installClicked.connect( partial(self.SubmitDownloadTask, item.domain, item.filename) ) options_widget.removeClicked.connect( partial(self.SubmitRemoveTask, item.domain, item.filename) ) self.updateItems.append((item, tree_item, options_widget)) all_tags.update(item.tags) self.filesView.addTopLevelItems( [tree_item for _, tree_item, _ in self.updateItems] ) for item, tree_item, options_widget in self.updateItems: self.filesView.setItemWidget(tree_item, 0, options_widget) # Add an update button if the file is updateable if item.state == OUTDATED: button = QToolButton( None, text="Update", maximumWidth=120, minimumHeight=20, maximumHeight=20 ) if sys.platform == "darwin": button.setAttribute(Qt.WA_MacSmallSize) button.clicked.connect( partial(self.SubmitDownloadTask, item.domain, item.filename) ) self.filesView.setItemWidget(tree_item, 2, button) self.progress.advance() self.filesView.setColumnWidth(0, self.filesView.sizeHintForColumn(0)) for column in range(1, 4): contents_hint = self.filesView.sizeHintForColumn(column) header_hint = self.filesView.header().sectionSizeHint(column) width = max(min(contents_hint, 400), header_hint) self.filesView.setColumnWidth(column, width) hints = [hint for hint in sorted(all_tags) if not hint.startswith("#")] self.completer.setTokenList(hints) self.SearchUpdate() self.UpdateInfoLabel() self.toggleButtons() self.cancelButton.setEnabled(False) self.progress.setRange(0, 0) def buttonCheck(self, selected_items, state, button): for item in selected_items: if item.state != state: button.setEnabled(False) else: button.setEnabled(True) break def toggleButtons(self): selected_items = [item for item, tree_item, _ in self.updateItems if not tree_item.isHidden()] self.buttonCheck(selected_items, OUTDATED, self.updateButton) self.buttonCheck(selected_items, AVAILABLE, self.downloadButton) def HandleError(self, exception): if isinstance(exception, ConnectionError): self.warning(0, "Could not connect to server! Check your connection " "and try to reconnect.") self.SetFilesList({}) self.retryButton.show() else: sys.excepthook(type(exception), exception, None) self.progress.setRange(0, 0) self.setEnabled(True) def UpdateInfoLabel(self): local = [item for item, tree_item, _ in self.updateItems if item.state != AVAILABLE and not tree_item.isHidden()] size = sum(float(item.size) for item in local) onServer = [item for item, tree_item, _ in self.updateItems if not tree_item.isHidden()] sizeOnServer = sum(float(item.size) for item in onServer) text = ("%i items, %s (on server: %i items, %s)" % (len(local), sizeof_fmt(size), len(onServer), sizeof_fmt(sizeOnServer))) self.infoLabel.setText(text) def UpdateAll(self): self.warning(0) for item, tree_item, _ in self.updateItems: if item.state == OUTDATED and not tree_item.isHidden(): self.SubmitDownloadTask(item.domain, item.filename) def DownloadFiltered(self): # TODO: submit items in the order shown. for item, tree_item, _ in self.updateItems: if not tree_item.isHidden() and item.state in \ [AVAILABLE, OUTDATED]: self.SubmitDownloadTask(item.domain, item.filename) def SearchUpdate(self, searchString=None): strings = str(self.lineEditFilter.text()).split() for item, tree_item, _ in self.updateItems: hide = not all(UpdateItem_match(item, string) for string in strings) tree_item.setHidden(hide) self.UpdateInfoLabel() self.toggleButtons() def SubmitDownloadTask(self, domain, filename): """ Submit the (domain, filename) to be downloaded/updated. """ self.cancelButton.setEnabled(True) index = self.updateItemIndex(domain, filename) _, tree_item, opt_widget = self.updateItems[index] sf = LocalFiles(serverfiles.PATH, serverfiles.ServerFiles()) task = DownloadTask(domain, filename, sf) self.progress.adjustRange(0, 100) pb = ItemProgressBar(self.filesView) pb.setRange(0, 100) pb.setTextVisible(False) task.advanced.connect(pb.advance) task.advanced.connect(self.progress.advance) task.finished.connect(pb.hide) task.finished.connect(self.onDownloadFinished, Qt.QueuedConnection) task.exception.connect(self.onDownloadError, Qt.QueuedConnection) self.filesView.setItemWidget(tree_item, 2, pb) # Clear the text so it does not show behind the progress bar. tree_item.setData(2, Qt.DisplayRole, "") pb.show() # Disable the options widget opt_widget.setEnabled(False) self._tasks.append(task) self.executor.submit(task) def EndDownloadTask(self, task): future = task.future() index = self.updateItemIndex(task.domain, task.filename) item, tree_item, opt_widget = self.updateItems[index] self.filesView.removeItemWidget(tree_item, 2) opt_widget.setEnabled(True) if future.cancelled(): # Restore the previous state tree_item.setUpdateItem(item) opt_widget.setState(item.state) elif future.exception(): tree_item.setUpdateItem(item) opt_widget.setState(item.state) # Show the exception string in the size column. self.warning(0, "Error while downloading. Check your connection " "and retry.") # recreate button for download button = QToolButton( None, text="Retry", maximumWidth=120, minimumHeight=20, maximumHeight=20 ) if sys.platform == "darwin": button.setAttribute(Qt.WA_MacSmallSize) button.clicked.connect( partial(self.SubmitDownloadTask, item.domain, item.filename) ) self.filesView.setItemWidget(tree_item, 2, button) else: # get the new updated info dict and replace the the old item self.warning(0) info = serverfiles.info(item.domain, item.filename) new_item = update_item_from_info(item.domain, item.filename, info, info) self.updateItems[index] = (new_item, tree_item, opt_widget) tree_item.setUpdateItem(new_item) opt_widget.setState(new_item.state) self.UpdateInfoLabel() def SubmitRemoveTask(self, domain, filename): serverfiles.LOCALFILES.remove(domain, filename) index = self.updateItemIndex(domain, filename) item, tree_item, opt_widget = self.updateItems[index] if item.info_server: new_item = item._replace(state=AVAILABLE, local=None, info_local=None) else: new_item = item._replace(local=None, info_local=None) # Disable the options widget. No more actions can be performed # for the item. opt_widget.setEnabled(False) tree_item.setUpdateItem(new_item) opt_widget.setState(new_item.state) self.updateItems[index] = (new_item, tree_item, opt_widget) self.UpdateInfoLabel() def Cancel(self): """ Cancel all pending update/download tasks (that have not yet started). """ for task in self._tasks: task.future().cancel() def onDeleteWidget(self): self.Cancel() self.executor.shutdown(wait=False) OWWidget.onDeleteWidget(self) def onDownloadFinished(self): # on download completed/canceled/error assert QThread.currentThread() is self.thread() for task in list(self._tasks): future = task.future() if future.done(): self.EndDownloadTask(task) self._tasks.remove(task) if not self._tasks: # Clear/reset the overall progress self.progress.setRange(0, 0) self.cancelButton.setEnabled(False) def onDownloadError(self, exc_info): sys.excepthook(*exc_info) self.warning(0, "Error while downloading. Check your connection and " "retry.") def updateItemIndex(self, domain, filename): for i, (item, _, _) in enumerate(self.updateItems): if item.domain == domain and item.filename == filename: return i raise ValueError("%r, %r not in update list" % (domain, filename)) def _updateProgress(self, *args): rmin, rmax = self.progress.range() if rmin != rmax: if not self._haveProgress: self._haveProgress = True self.progressBarInit() self.progressBarSet(self.progress.ratioCompleted() * 100, processEvents=None) if rmin == rmax: self._haveProgress = False self.progressBarFinished()
class OWMultifile(Orange.widgets.data.owfile.OWFile, RecentPathsWidgetMixin): name = "Multifile" id = "orangecontrib.spectroscopy.widgets.files" icon = "icons/multifile.svg" description = "Read data from input files " \ "and send a data table to the output." priority = 10000 replaces = ["orangecontrib.infrared.widgets.owfiles.OWFiles", "orangecontrib.infrared.widgets.owmultifile.OWMultifile"] file_idx = [] sheet = Orange.widgets.settings.Setting(None) label = Orange.widgets.settings.Setting("") recent_paths = Orange.widgets.settings.Setting([]) def __init__(self): widget.OWWidget.__init__(self) RecentPathsWidgetMixin.__init__(self) self.domain = None self.data = None self.loaded_file = "" self.sheets = [] self.lb = gui.listBox(self.controlArea, self, "file_idx", selectionMode=QListWidget.MultiSelection) layout = QGridLayout() gui.widgetBox(self.controlArea, margin=0, orientation=layout) file_button = gui.button( None, self, ' ...', callback=self.browse_files, autoDefault=False) file_button.setIcon(self.style().standardIcon( QStyle.SP_DirOpenIcon)) file_button.setSizePolicy(Policy.Maximum, Policy.Fixed) layout.addWidget(file_button, 0, 0) remove_button = gui.button( None, self, 'Remove', callback=self.remove_item) clear_button = gui.button( None, self, 'Clear', callback=self.clear) layout.addWidget(remove_button, 0, 1) layout.addWidget(clear_button, 0, 2) reload_button = gui.button( None, self, "Reload", callback=self.load_data, autoDefault=False) reload_button.setIcon(self.style().standardIcon(QStyle.SP_BrowserReload)) reload_button.setSizePolicy(Policy.Fixed, Policy.Fixed) layout.addWidget(reload_button, 0, 7) self.sheet_box = gui.hBox(None, addToLayout=False, margin=0) self.sheet_combo = gui.comboBox(None, self, "xls_sheet", callback=self.select_sheet, sendSelectedValue=True) self.sheet_combo.setSizePolicy( Policy.MinimumExpanding, Policy.Fixed) self.sheet_label = QLabel() self.sheet_label.setText('Sheet') self.sheet_label.setSizePolicy( Policy.MinimumExpanding, Policy.Fixed) self.sheet_box.layout().addWidget( self.sheet_label, Qt.AlignLeft) self.sheet_box.layout().addWidget( self.sheet_combo, Qt.AlignVCenter) layout.addWidget(self.sheet_box, 2, 1) self.sheet_box.hide() layout.addWidget(self.sheet_box, 0, 5) label_box = gui.hBox(None, addToLayout=False, margin=0) label = gui.lineEdit(label_box, self, "label", callback=self.set_label, label="Label", orientation=Qt.Horizontal) layout.addWidget(label_box, 0, 6) layout.setColumnStretch(3, 2) box = gui.widgetBox(self.controlArea, "Columns (Double click to edit)") self.domain_editor = DomainEditor(self) self.editor_model = self.domain_editor.model() box.layout().addWidget(self.domain_editor) for i, rp in enumerate(self.recent_paths): self.lb.addItem(rp.abspath) # TODO unresolved paths just disappear! Modify _relocate_recent_files box = gui.hBox(self.controlArea) gui.rubber(box) box.layout().addWidget(self.report_button) self.report_button.setFixedWidth(170) self.apply_button = gui.button( box, self, "Apply", callback=self.apply_domain_edit) self.apply_button.setEnabled(False) self.apply_button.setFixedWidth(170) self.editor_model.dataChanged.connect( lambda: self.apply_button.setEnabled(True)) self._update_sheet_combo() self.load_data() def set_label(self): self.load_data() def add_path(self, filename, reader=None): recent = RecentPath.create(filename, self._search_paths()) if reader is not None: recent.file_format = reader.qualified_name() self.recent_paths.append(recent) def set_file_list(self): # need to define it for RecentPathsWidgetMixin pass def _select_active_sheet(self): if self.sheet: try: sheet_list = [s[0] for s in self.sheets] idx = sheet_list.index(self.sheet) self.sheet_combo.setCurrentIndex(idx) except ValueError: # Requested sheet does not exist in this file self.sheet = None else: self.sheet_combo.setCurrentIndex(0) def _update_sheet_combo(self): sheets = Counter() for rp in self.recent_paths: try: reader = _get_reader(rp) sheets.update(reader.sheets) except: pass sheets = sorted(sheets.items(), key=lambda x: x[0]) self.sheets = [(s, s + " (" + str(n) + ")") for s, n in sheets] if len(sheets) < 2: self.sheet_box.hide() self.sheet = None else: self.sheets.insert(0, (None, "(None)")) self.sheet_combo.clear() self.sheet_combo.addItems([s[1] for s in self.sheets]) self._select_active_sheet() self.sheet_box.show() def select_sheet(self): self.sheet = self.sheets[self.sheet_combo.currentIndex()][0] self.load_data() def remove_item(self): ri = [ i.row() for i in self.lb.selectedIndexes() ] for i in sorted(ri, reverse=True): self.recent_paths.pop(i) self.lb.takeItem(i) self._update_sheet_combo() self.load_data() def clear(self): self.lb.clear() while self.recent_paths: self.recent_paths.pop() self._update_sheet_combo() self.load_data() def browse_files(self, in_demos=False): start_file = self.last_path() or os.path.expanduser("~/") readers = [f for f in FileFormat.formats if getattr(f, 'read', None) and getattr(f, "EXTENSIONS", None)] filenames, reader, _ = open_filename_dialog(start_file, None, readers, dialog=QFileDialog.getOpenFileNames) self.load_files(filenames, reader) def load_files(self, filenames, reader): if not filenames: return for f in filenames: self.add_path(f, reader) self.lb.addItem(f) self._update_sheet_combo() self.load_data() def load_data(self): self.closeContext() data_list = [] fnok_list = [] empty_domain = Orange.data.Domain(attributes=[]) for rp in self.recent_paths: fn = rp.abspath reader = _get_reader(rp) errors = [] with catch_warnings(record=True) as warnings: try: if self.sheet in reader.sheets: reader.select_sheet(self.sheet) if isinstance(reader, SpectralFileFormat): xs, vals, additional = reader.read_spectra() if additional is None: additional = Orange.data.Table.from_domain(empty_domain, n_rows=len(vals)) data_list.append((xs, vals, additional)) else: data_list.append(reader.read()) fnok_list.append(fn) except Exception as ex: errors.append("An error occurred:") errors.append(str(ex)) #FIXME show error in the list of data self.warning(warnings[-1].message.args[0] if warnings else '') if data_list: data = concatenate_data(data_list, fnok_list, self.label) self.data = data self.openContext(data.domain) else: self.data = None self.domain_editor.set_domain(None) self.apply_domain_edit() # sends data
class SelectEncodingsWidget(QWidget): """ Popup widget for selecting a set of text encodings for use in other parts of the GUI. """ def __init__(self, *args, headingText="", **kwargs): super().__init__(*args, **kwargs) self.setLayout(QVBoxLayout()) self.__top_label = QLabel(headingText, visible=bool(headingText), objectName="-top-heading-text") self.__top_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) self.__model = model = encodings_model() self.__model.setParent(self) self.__view = view = EncodingsView( self, uniformItemSizes=True, editTriggers=QListView.NoEditTriggers) self.__view.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) view.setModel(model) self.layout().addWidget(self.__top_label) self.layout().addWidget(view) buttons = QDialogButtonBox( standardButtons=QDialogButtonBox.RestoreDefaults) b = buttons.addButton("Select all", QDialogButtonBox.ActionRole) b.clicked.connect(self.selectAll) b = buttons.button(QDialogButtonBox.RestoreDefaults) b.clicked.connect(self.reset) self.layout().addWidget(buttons) self.setAttribute(Qt.WA_MacSmallSize) def headingText(self): # type: () -> str return self.__top_label.text() def setHeadingText(self, text): # type: (str) -> None self.__top_label.setText(text) self.__top_label.setVisible(bool(text)) def setModel(self, model): if self.__model is not None: if self.__model.parent() is self: self.__model.deleteLater() self.__model = None self.__view.setModel(model) self.__model = model def model(self): return self.__model def selectedEncodings(self): # type: () -> List[str] """ Return a list of currently selected (checked) encodings. """ model = self.__model res = [] for i in range(model.rowCount()): data = model.itemData(model.index(i, 0)) if data.get(Qt.CheckStateRole) == Qt.Checked and \ EncodingNameRole in data: res.append(data[EncodingNameRole]) return res @Slot() def selectAll(self): """ Select all encodings. """ model = self.__model for i in range(model.rowCount()): item = model.item(i) item.setCheckState(Qt.Checked) @Slot() def clearAll(self): """ Clear (uncheck) all encodings. """ model = self.__model for i in range(model.rowCount()): item = model.item(i) item.setCheckState(Qt.Checked) @Slot() def reset(self): """ Reset the encodings model to the default selected set. """ model = self.__model for i in range(model.rowCount()): item = model.item(i) co = item.data(CodecInfoRole) if isinstance(co, codecs.CodecInfo): state = co.name in DEFAULT_ENCODINGS item.setCheckState(Qt.Checked if state else Qt.Unchecked)
class OWImportImages(widget.OWWidget): name = "Import Images" description = "Import images from a directory(s)" icon = "icons/ImportImages.svg" priority = 110 outputs = [("Data", Orange.data.Table)] #: list of recent paths recent_paths = settings.Setting([]) # type: List[RecentPath] currentPath = settings.Setting(None) want_main_area = False resizing_enabled = False Modality = Qt.ApplicationModal # Modality = Qt.WindowModal MaxRecentItems = 20 def __init__(self): super().__init__() #: widget's runtime state self.__state = State.NoState self.data = None self._n_image_categories = 0 self._n_image_data = 0 self._n_skipped = 0 self.__invalidated = False self.__pendingTask = None vbox = gui.vBox(self.controlArea) hbox = gui.hBox(vbox) self.recent_cb = QComboBox( sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon, minimumContentsLength=16, acceptDrops=True) self.recent_cb.installEventFilter(self) self.recent_cb.activated[int].connect(self.__onRecentActivated) icons = standard_icons(self) browseaction = QAction( "Open/Load Images", self, iconText="\N{HORIZONTAL ELLIPSIS}", icon=icons.dir_open_icon, toolTip="Select a directory from which to load the images") browseaction.triggered.connect(self.__runOpenDialog) reloadaction = QAction("Reload", self, icon=icons.reload_icon, toolTip="Reload current image set") reloadaction.triggered.connect(self.reload) self.__actions = namespace( browse=browseaction, reload=reloadaction, ) browsebutton = QPushButton(browseaction.iconText(), icon=browseaction.icon(), toolTip=browseaction.toolTip(), clicked=browseaction.trigger) reloadbutton = QPushButton( reloadaction.iconText(), icon=reloadaction.icon(), clicked=reloadaction.trigger, default=True, ) hbox.layout().addWidget(self.recent_cb) hbox.layout().addWidget(browsebutton) hbox.layout().addWidget(reloadbutton) self.addActions([browseaction, reloadaction]) reloadaction.changed.connect( lambda: reloadbutton.setEnabled(reloadaction.isEnabled())) box = gui.vBox(vbox, "Info") self.infostack = QStackedWidget() self.info_area = QLabel(text="No image set selected", wordWrap=True) self.progress_widget = QProgressBar(minimum=0, maximum=0) self.cancel_button = QPushButton( "Cancel", icon=icons.cancel_icon, ) self.cancel_button.clicked.connect(self.cancel) w = QWidget() vlayout = QVBoxLayout() vlayout.setContentsMargins(0, 0, 0, 0) hlayout = QHBoxLayout() hlayout.setContentsMargins(0, 0, 0, 0) hlayout.addWidget(self.progress_widget) hlayout.addWidget(self.cancel_button) vlayout.addLayout(hlayout) self.pathlabel = TextLabel() self.pathlabel.setTextElideMode(Qt.ElideMiddle) self.pathlabel.setAttribute(Qt.WA_MacSmallSize) vlayout.addWidget(self.pathlabel) w.setLayout(vlayout) self.infostack.addWidget(self.info_area) self.infostack.addWidget(w) box.layout().addWidget(self.infostack) self.__initRecentItemsModel() self.__invalidated = True self.__executor = ThreadExecutor(self) QApplication.postEvent(self, QEvent(RuntimeEvent.Init)) def __initRecentItemsModel(self): if self.currentPath is not None and \ not os.path.isdir(self.currentPath): self.currentPath = None recent_paths = [] for item in self.recent_paths: if os.path.isdir(item.abspath): recent_paths.append(item) recent_paths = recent_paths[:OWImportImages.MaxRecentItems] recent_model = self.recent_cb.model() for pathitem in recent_paths: item = RecentPath_asqstandarditem(pathitem) recent_model.appendRow(item) self.recent_paths = recent_paths if self.currentPath is not None and \ os.path.isdir(self.currentPath) and self.recent_paths and \ os.path.samefile(self.currentPath, self.recent_paths[0].abspath): self.recent_cb.setCurrentIndex(0) else: self.currentPath = None self.recent_cb.setCurrentIndex(-1) self.__actions.reload.setEnabled(self.currentPath is not None) def customEvent(self, event): """Reimplemented.""" if event.type() == RuntimeEvent.Init: if self.__invalidated: try: self.start() finally: self.__invalidated = False super().customEvent(event) def __runOpenDialog(self): startdir = os.path.expanduser("~/") if self.recent_paths: startdir = os.path.dirname(self.recent_paths[0].abspath) if OWImportImages.Modality == Qt.WindowModal: dlg = QFileDialog( self, "Select Top Level Directory", startdir, acceptMode=QFileDialog.AcceptOpen, modal=True, ) dlg.setFileMode(QFileDialog.Directory) dlg.setOption(QFileDialog.ShowDirsOnly) dlg.setDirectory(startdir) dlg.setAttribute(Qt.WA_DeleteOnClose) @dlg.accepted.connect def on_accepted(): dirpath = dlg.selectedFiles() if dirpath: self.setCurrentPath(dirpath[0]) self.start() dlg.open() else: dirpath = QFileDialog.getExistingDirectory( self, "Select Top Level Directory", startdir) if dirpath: self.setCurrentPath(dirpath) self.start() def __onRecentActivated(self, index): item = self.recent_cb.itemData(index) if item is None: return assert isinstance(item, RecentPath) self.setCurrentPath(item.abspath) self.start() def __updateInfo(self): if self.__state == State.NoState: text = "No image set selected" elif self.__state == State.Processing: text = "Processing" elif self.__state == State.Done: nvalid = self._n_image_data ncategories = self._n_image_categories n_skipped = self._n_skipped if ncategories < 2: text = "{} image{}".format(nvalid, "s" if nvalid != 1 else "") else: text = "{} images / {} categories".format(nvalid, ncategories) if n_skipped > 0: text = text + ", {} skipped".format(n_skipped) elif self.__state == State.Cancelled: text = "Cancelled" elif self.__state == State.Error: text = "Error state" else: assert False self.info_area.setText(text) if self.__state == State.Processing: self.infostack.setCurrentIndex(1) else: self.infostack.setCurrentIndex(0) def setCurrentPath(self, path): """ Set the current root image path to path If the path does not exists or is not a directory the current path is left unchanged Parameters ---------- path : str New root import path. Returns ------- status : bool True if the current root import path was successfully changed to path. """ if self.currentPath is not None and path is not None and \ os.path.isdir(self.currentPath) and os.path.isdir(path) and \ os.path.samefile(self.currentPath, path): return True success = True error = None if path is not None: if not os.path.exists(path): error = "'{}' does not exist".format(path) path = None success = False elif not os.path.isdir(path): error = "'{}' is not a directory".format(path) path = None success = False if error is not None: self.error(error) warnings.warn(error, UserWarning, stacklevel=3) else: self.error() if path is not None: newindex = self.addRecentPath(path) self.recent_cb.setCurrentIndex(newindex) if newindex >= 0: self.currentPath = path else: self.currentPath = None else: self.currentPath = None self.__actions.reload.setEnabled(self.currentPath is not None) if self.__state == State.Processing: self.cancel() return success def addRecentPath(self, path): """ Prepend a path entry to the list of recent paths If an entry with the same path already exists in the recent path list it is moved to the first place Parameters ---------- path : str """ existing = None for pathitem in self.recent_paths: try: if os.path.samefile(pathitem.abspath, path): existing = pathitem break except FileNotFoundError: # file not found if the `pathitem.abspath` no longer exists pass model = self.recent_cb.model() if existing is not None: selected_index = self.recent_paths.index(existing) assert model.item(selected_index).data(Qt.UserRole) is existing self.recent_paths.remove(existing) row = model.takeRow(selected_index) self.recent_paths.insert(0, existing) model.insertRow(0, row) else: item = RecentPath(path, None, None) self.recent_paths.insert(0, item) model.insertRow(0, RecentPath_asqstandarditem(item)) return 0 def __setRuntimeState(self, state): assert state in State self.setBlocking(state == State.Processing) message = "" if state == State.Processing: assert self.__state in [ State.Done, State.NoState, State.Error, State.Cancelled ] message = "Processing" elif state == State.Done: assert self.__state == State.Processing elif state == State.Cancelled: assert self.__state == State.Processing message = "Cancelled" elif state == State.Error: message = "Error during processing" elif state == State.NoState: message = "" else: assert False self.__state = state if self.__state == State.Processing: self.infostack.setCurrentIndex(1) else: self.infostack.setCurrentIndex(0) self.setStatusMessage(message) self.__updateInfo() def reload(self): """ Restart the image scan task """ if self.__state == State.Processing: self.cancel() self.data = None self.start() def start(self): """ Start/execute the image indexing operation """ self.error() self.__invalidated = False if self.currentPath is None: return if self.__state == State.Processing: assert self.__pendingTask is not None log.info("Starting a new task while one is in progress. " "Cancel the existing task (dir:'{}')".format( self.__pendingTask.startdir)) self.cancel() startdir = self.currentPath self.__setRuntimeState(State.Processing) report_progress = methodinvoke(self, "__onReportProgress", (object, )) task = ImportImages(report_progress=report_progress) # collect the task state in one convenient place self.__pendingTask = taskstate = namespace( task=task, startdir=startdir, future=None, watcher=None, cancelled=False, cancel=None, ) def cancel(): # Cancel the task and disconnect if taskstate.future.cancel(): pass else: taskstate.task.cancelled = True taskstate.cancelled = True try: taskstate.future.result(timeout=3) except UserInterruptError: pass except TimeoutError: log.info("The task did not stop in in a timely manner") taskstate.watcher.finished.disconnect(self.__onRunFinished) taskstate.cancel = cancel def run_image_scan_task_interupt(): try: return task(startdir) except UserInterruptError: # Suppress interrupt errors, so they are not logged return taskstate.future = self.__executor.submit(run_image_scan_task_interupt) taskstate.watcher = FutureWatcher(taskstate.future) taskstate.watcher.finished.connect(self.__onRunFinished) @Slot() def __onRunFinished(self): assert QThread.currentThread() is self.thread() assert self.__state == State.Processing assert self.__pendingTask is not None assert self.sender() is self.__pendingTask.watcher assert self.__pendingTask.future.done() task = self.__pendingTask self.__pendingTask = None try: data, n_skipped = task.future.result() except Exception: sys.excepthook(*sys.exc_info()) state = State.Error data = None n_skipped = 0 self.error(traceback.format_exc()) else: state = State.Done self.error() if data: self._n_image_data = len(data) self._n_image_categories = len(data.domain.class_var.values)\ if data.domain.class_var else 0 self.data = data self._n_skipped = n_skipped self.__setRuntimeState(state) self.commit() def cancel(self): """ Cancel current pending task (if any). """ if self.__state == State.Processing: assert self.__pendingTask is not None self.__pendingTask.cancel() self.__pendingTask = None self.__setRuntimeState(State.Cancelled) @Slot(object) def __onReportProgress(self, arg): # report on scan progress from a worker thread # arg must be a namespace(count: int, lastpath: str) assert QThread.currentThread() is self.thread() if self.__state == State.Processing: self.pathlabel.setText(prettyfypath(arg.lastpath)) def commit(self): """ Commit a Table from the collected image meta data. """ self.send("Data", self.data) def onDeleteWidget(self): self.cancel() self.__executor.shutdown(wait=True) self.__invalidated = False def eventFilter(self, receiver, event): # re-implemented from QWidget # intercept and process drag drop events on the recent directory # selection combo box def dirpath(event): # type: (QDropEvent) -> Optional[str] """Return the directory from a QDropEvent.""" data = event.mimeData() urls = data.urls() if len(urls) == 1: url = urls[0] path = url.toLocalFile() if os.path.isdir(path): return path return None if receiver is self.recent_cb and \ event.type() in {QEvent.DragEnter, QEvent.DragMove, QEvent.Drop}: assert isinstance(event, QDropEvent) path = dirpath(event) if path is not None and event.possibleActions() & Qt.LinkAction: event.setDropAction(Qt.LinkAction) event.accept() if event.type() == QEvent.Drop: self.setCurrentPath(path) self.start() else: event.ignore() return True return super().eventFilter(receiver, event)
class WelcomeDialog(QDialog): """ A welcome widget shown at startup presenting a series of buttons (actions) for a beginner to choose from. """ triggered = Signal(QAction) def __init__(self, *args, **kwargs): showAtStartup = kwargs.pop("showAtStartup", True) feedbackUrl = kwargs.pop("feedbackUrl", "") super().__init__(*args, **kwargs) self.__triggeredAction = None # type: Optional[QAction] self.__showAtStartupCheck = None self.__mainLayout = None self.__feedbackUrl = None self.__feedbackLabel = None self.setupUi() self.setFeedbackUrl(feedbackUrl) self.setShowAtStartup(showAtStartup) def setupUi(self): self.setLayout(QVBoxLayout()) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().setSpacing(0) self.__mainLayout = QVBoxLayout() self.__mainLayout.setContentsMargins(0, 40, 0, 40) self.__mainLayout.setSpacing(65) self.layout().addLayout(self.__mainLayout) self.setStyleSheet(WELCOME_WIDGET_BUTTON_STYLE) dir_path = Path(__file__).resolve() parent_path = dir_path.parent.parent icon_path = f'{str(parent_path)}/icons' guanzhu_box = QVBoxLayout() guanzhu_label = QLabel() guanzhu_pic = QPixmap(icon_path + '/weixin.png') guanzhu_label.setPixmap(guanzhu_pic) guanzhu_text = QLabel('关注我们') guanzhu_box.addWidget(guanzhu_label, alignment=Qt.AlignVCenter | Qt.AlignHCenter) guanzhu_box.addWidget(guanzhu_text, alignment=Qt.AlignVCenter | Qt.AlignHCenter) zan_box = QVBoxLayout() zan_label = QLabel() pixmap = QPixmap(icon_path + '/zan.png') zan_label.setPixmap(pixmap) zan_text = QLabel('赞赏开发者') zan_box.addWidget(zan_label, alignment=Qt.AlignVCenter | Qt.AlignHCenter) zan_box.addWidget(zan_text, alignment=Qt.AlignVCenter | Qt.AlignHCenter) weixinBox = QHBoxLayout() weixinBox.addLayout(guanzhu_box) weixinBox.addLayout(zan_box) self.layout().addLayout(weixinBox) bottom_bar = QWidget(objectName="bottom-bar") bottom_bar_layout = QHBoxLayout() bottom_bar_layout.setContentsMargins(20, 10, 20, 10) bottom_bar.setLayout(bottom_bar_layout) bottom_bar.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Maximum) # self.__showAtStartupCheck = QCheckBox( # self.tr("启动时显示"), bottom_bar, checked=True # ) # self.__showAtStartupCheck = QCheckBox( # self.tr("启动时显示"), bottom_bar, checked=True # ) self.__feedbackLabel = QLabel( textInteractionFlags=Qt.TextBrowserInteraction, openExternalLinks=True, visible=False, ) # bottom_bar_layout.addWidget( # self.__showAtStartupCheck, alignment=Qt.AlignVCenter | Qt.AlignLeft # ) bottom_bar_layout.addWidget(self.__feedbackLabel, alignment=Qt.AlignVCenter | Qt.AlignRight) self.layout().addWidget(bottom_bar, alignment=Qt.AlignBottom, stretch=1) self.setSizeGripEnabled(False) self.setFixedSize(620, 590) def setShowAtStartup(self, show): # type: (bool) -> None """ Set the 'Show at startup' check box state. """ pass # if self.__showAtStartupCheck.isChecked() != show: # # self.__showAtStartupCheck.setChecked(show) def showAtStartup(self): # type: () -> bool """ Return the 'Show at startup' check box state. """ # return self.__showAtStartupCheck.isChecked() return True def setFeedbackUrl(self, url): # type: (str) -> None """ Set an 'feedback' url. When set a link is displayed in the bottom row. """ self.__feedbackUrl = url if url: text = self.tr("帮助我们改进!") self.__feedbackLabel.setText('<a href="{url}">{text}</a>'.format( url=url, text=escape(text))) else: self.__feedbackLabel.setText("") self.__feedbackLabel.setVisible(bool(url)) def addRow(self, actions, background="light-orange"): """Add a row with `actions`. """ count = self.__mainLayout.count() self.insertRow(count, actions, background) def insertRow(self, index, actions, background="light-orange"): # type: (int, Iterable[QAction], Union[QColor, str]) -> None """Insert a row with `actions` at `index`. """ widget = QWidget(objectName="icon-row") layout = QHBoxLayout() layout.setContentsMargins(40, 0, 40, 0) layout.setSpacing(65) widget.setLayout(layout) self.__mainLayout.insertWidget(index, widget, stretch=10, alignment=Qt.AlignCenter) for i, action in enumerate(actions): self.insertAction(index, i, action, background) def insertAction(self, row, index, action, background="light-orange"): """Insert `action` in `row` in position `index`. """ button = self.createButton(action, background) self.insertButton(row, index, button) def insertButton(self, row, index, button): # type: (int, int, QToolButton) -> None """Insert `button` in `row` in position `index`. """ item = self.__mainLayout.itemAt(row) layout = item.widget().layout() layout.insertWidget(index, button) button.triggered.connect(self.__on_actionTriggered) def createButton(self, action, background="light-orange"): # type: (QAction, Union[QColor, str]) -> QToolButton """Create a tool button for action. """ button = WelcomeActionButton(self) button.setDefaultAction(action) button.setText(action.iconText()) button.setIcon(decorate_welcome_icon(action.icon(), background)) button.setToolTip(action.toolTip()) button.setFixedSize(100, 100) button.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) button.setVisible(action.isVisible()) font = QFont(button.font()) font.setPointSize(13) button.setFont(font) return button def buttonAt(self, i, j): # type: (int, int) -> QToolButton """Return the button at i-t row and j-th column. """ item = self.__mainLayout.itemAt(i) row = item.widget() item = row.layout().itemAt(j) return item.widget() def triggeredAction(self): # type: () -> Optional[QAction] """Return the action that was triggered by the user. """ return self.__triggeredAction def showEvent(self, event): # Clear the triggered action before show. self.__triggeredAction = None super().showEvent(event) def __on_actionTriggered(self, action): # type: (QAction) -> None """Called when the button action is triggered. """ self.triggered.emit(action) self.__triggeredAction = action
class OWNNLearner(OWBaseLearner): name = "Neural Network" description = "A multi-layer perceptron (MLP) algorithm with " \ "backpropagation." icon = "icons/NN.svg" priority = 90 keywords = ["mlp"] LEARNER = NNLearner activation = ["identity", "logistic", "tanh", "relu"] act_lbl = ["Identity", "Logistic", "tanh", "ReLu"] solver = ["lbfgs", "sgd", "adam"] solv_lbl = ["L-BFGS-B", "SGD", "Adam"] learner_name = Setting("Neural Network") hidden_layers_input = Setting("100,") activation_index = Setting(3) solver_index = Setting(2) max_iterations = Setting(200) alpha_index = Setting(0) settings_version = 1 alphas = list(chain([x / 10000 for x in range(1, 10)], [x / 1000 for x in range(1, 10)], [x / 100 for x in range(1, 10)], [x / 10 for x in range(1, 10)], range(1, 10), range(10, 100, 5), range(100, 200, 10), range(100, 1001, 50))) def add_main_layout(self): form = QFormLayout() form.setFieldGrowthPolicy(form.AllNonFixedFieldsGrow) form.setVerticalSpacing(25) gui.widgetBox(self.controlArea, True, orientation=form) form.addRow( "Neurons in hidden layers:", gui.lineEdit( None, self, "hidden_layers_input", orientation=Qt.Horizontal, callback=self.settings_changed, tooltip="A list of integers defining neurons. Length of list " "defines the number of layers. E.g. 4, 2, 2, 3.", placeholderText="e.g. 100,")) form.addRow( "Activation:", gui.comboBox( None, self, "activation_index", orientation=Qt.Horizontal, label="Activation:", items=[i for i in self.act_lbl], callback=self.settings_changed)) form.addRow(" ", gui.separator(None, 16)) form.addRow( "Solver:", gui.comboBox( None, self, "solver_index", orientation=Qt.Horizontal, label="Solver:", items=[i for i in self.solv_lbl], callback=self.settings_changed)) self.reg_label = QLabel() slider = gui.hSlider( None, self, "alpha_index", minValue=0, maxValue=len(self.alphas) - 1, callback=lambda: (self.set_alpha(), self.settings_changed()), createLabel=False) form.addRow(self.reg_label, slider) self.set_alpha() form.addRow( "Maximal number of iterations:", gui.spin( None, self, "max_iterations", 10, 10000, step=10, label="Max iterations:", orientation=Qt.Horizontal, alignment=Qt.AlignRight, callback=self.settings_changed)) def set_alpha(self): self.strength_C = self.alphas[self.alpha_index] self.reg_label.setText("Regularization, α={}:".format(self.strength_C)) @property def alpha(self): return self.alphas[self.alpha_index] def setup_layout(self): super().setup_layout() self._task = None # type: Optional[Task] self._executor = ThreadExecutor() # just a test cancel button gui.button(self.apply_button, self, "Cancel", callback=self.cancel) def create_learner(self): return self.LEARNER( hidden_layer_sizes=self.get_hidden_layers(), activation=self.activation[self.activation_index], solver=self.solver[self.solver_index], alpha=self.alpha, max_iter=self.max_iterations, preprocessors=self.preprocessors) def get_learner_parameters(self): return (("Hidden layers", ', '.join(map(str, self.get_hidden_layers()))), ("Activation", self.act_lbl[self.activation_index]), ("Solver", self.solv_lbl[self.solver_index]), ("Alpha", self.alpha), ("Max iterations", self.max_iterations)) def get_hidden_layers(self): layers = tuple(map(int, re.findall(r'\d+', self.hidden_layers_input))) if not layers: layers = (100,) self.hidden_layers_input = "100," return layers def update_model(self): self.show_fitting_failed(None) self.model = None if self.check_data(): self.__update() else: self.Outputs.model.send(self.model) @Slot(float) def setProgressValue(self, value): assert self.thread() is QThread.currentThread() self.progressBarSet(value) def __update(self): if self._task is not None: # First make sure any pending tasks are cancelled. self.cancel() assert self._task is None max_iter = self.learner.kwargs["max_iter"] # Setup the task state task = Task() lastemitted = 0. def callback(iteration): nonlocal task # type: Task nonlocal lastemitted if task.isInterruptionRequested(): raise CancelTaskException() progress = round(iteration / max_iter * 100) if progress != lastemitted: task.emitProgressUpdate(progress) lastemitted = progress # copy to set the callback so that the learner output is not modified # (currently we can not pass callbacks to learners __call__) learner = copy.copy(self.learner) learner.callback = callback def build_model(data, learner): try: return learner(data) except CancelTaskException: return None build_model_func = partial(build_model, self.data, learner) task.setFuture(self._executor.submit(build_model_func)) task.done.connect(self._task_finished) task.progressChanged.connect(self.setProgressValue) self._task = task self.progressBarInit() self.setBlocking(True) @Slot(concurrent.futures.Future) def _task_finished(self, f): """ Parameters ---------- f : Future The future instance holding the built model """ assert self.thread() is QThread.currentThread() assert self._task is not None assert self._task.future is f assert f.done() self._task.deleteLater() self._task = None self.setBlocking(False) self.progressBarFinished() try: self.model = f.result() except Exception as ex: # pylint: disable=broad-except # Log the exception with a traceback log = logging.getLogger() log.exception(__name__, exc_info=True) self.model = None self.show_fitting_failed(ex) else: self.model.name = self.learner_name self.model.instances = self.data self.model.skl_model.orange_callback = None # remove unpicklable callback self.Outputs.model.send(self.model) def cancel(self): """ Cancel the current task (if any). """ if self._task is not None: self._task.cancel() assert self._task.future.done() # disconnect from the task self._task.done.disconnect(self._task_finished) self._task.progressChanged.disconnect(self.setProgressValue) self._task.deleteLater() self._task = None self.progressBarFinished() self.setBlocking(False) def onDeleteWidget(self): self.cancel() super().onDeleteWidget() @classmethod def migrate_settings(cls, settings, version): if not version: alpha = settings.pop("alpha", None) if alpha is not None: settings["alpha_index"] = \ np.argmin(np.abs(np.array(cls.alphas) - alpha))
class OWFile(widget.OWWidget, RecentPathsWComboMixin): name = "File" id = "orange.widgets.data.file" description = "Read data from an input file or network " \ "and send a data table to the output." icon = "icons/File.svg" priority = 10 category = "Data" keywords = ["data", "file", "load", "read"] outputs = [widget.OutputSignal( "Data", Table, doc="Attribute-valued data set read from the input file.")] want_main_area = False SEARCH_PATHS = [("sample-datasets", get_sample_datasets_dir())] SIZE_LIMIT = 1e7 LOCAL_FILE, URL = range(2) settingsHandler = PerfectDomainContextHandler() # Overload RecentPathsWidgetMixin.recent_paths to set defaults recent_paths = Setting([ RecentPath("", "sample-datasets", "iris.tab"), RecentPath("", "sample-datasets", "titanic.tab"), RecentPath("", "sample-datasets", "housing.tab"), RecentPath("", "sample-datasets", "heart_disease.tab"), ]) recent_urls = Setting([]) source = Setting(LOCAL_FILE) xls_sheet = ContextSetting("") sheet_names = Setting({}) url = Setting("") variables = ContextSetting([]) dlg_formats = ( "All readable files ({});;".format( '*' + ' *'.join(FileFormat.readers.keys())) + ";;".join("{} (*{})".format(f.DESCRIPTION, ' *'.join(f.EXTENSIONS)) for f in sorted(set(FileFormat.readers.values()), key=list(FileFormat.readers.values()).index))) class Warning(widget.OWWidget.Warning): file_too_big = widget.Msg("The file is too large to load automatically." " Press Reload to load.") def __init__(self): super().__init__() RecentPathsWComboMixin.__init__(self) self.domain = None self.data = None self.loaded_file = "" self.reader = None layout = QGridLayout() gui.widgetBox(self.controlArea, margin=0, orientation=layout) vbox = gui.radioButtons(None, self, "source", box=True, addSpace=True, callback=self.load_data, addToLayout=False) rb_button = gui.appendRadioButton(vbox, "File:", addToLayout=False) layout.addWidget(rb_button, 0, 0, Qt.AlignVCenter) box = gui.hBox(None, addToLayout=False, margin=0) box.setSizePolicy(Policy.MinimumExpanding, Policy.Fixed) self.file_combo.setSizePolicy(Policy.MinimumExpanding, Policy.Fixed) self.file_combo.activated[int].connect(self.select_file) box.layout().addWidget(self.file_combo) layout.addWidget(box, 0, 1) file_button = gui.button( None, self, '...', callback=self.browse_file, autoDefault=False) file_button.setIcon(self.style().standardIcon(QStyle.SP_DirOpenIcon)) file_button.setSizePolicy(Policy.Maximum, Policy.Fixed) layout.addWidget(file_button, 0, 2) reload_button = gui.button( None, self, "Reload", callback=self.load_data, autoDefault=False) reload_button.setIcon(self.style().standardIcon( QStyle.SP_BrowserReload)) reload_button.setSizePolicy(Policy.Fixed, Policy.Fixed) layout.addWidget(reload_button, 0, 3) self.sheet_box = gui.hBox(None, addToLayout=False, margin=0) self.sheet_combo = gui.comboBox(None, self, "xls_sheet", callback=self.select_sheet, sendSelectedValue=True) self.sheet_combo.setSizePolicy( Policy.MinimumExpanding, Policy.Fixed) self.sheet_label = QLabel() self.sheet_label.setText('Sheet') self.sheet_label.setSizePolicy( Policy.MinimumExpanding, Policy.Fixed) self.sheet_box.layout().addWidget( self.sheet_label, Qt.AlignLeft) self.sheet_box.layout().addWidget( self.sheet_combo, Qt.AlignVCenter) layout.addWidget(self.sheet_box, 2, 1) self.sheet_box.hide() rb_button = gui.appendRadioButton(vbox, "URL:", addToLayout=False) layout.addWidget(rb_button, 3, 0, Qt.AlignVCenter) self.url_combo = url_combo = QComboBox() url_model = NamedURLModel(self.sheet_names) url_model.wrap(self.recent_urls) url_combo.setLineEdit(LineEditSelectOnFocus()) url_combo.setModel(url_model) url_combo.setSizePolicy(Policy.MinimumExpanding, Policy.Fixed) url_combo.setEditable(True) url_combo.setInsertPolicy(url_combo.InsertAtTop) url_edit = url_combo.lineEdit() l, t, r, b = url_edit.getTextMargins() url_edit.setTextMargins(l + 5, t, r, b) layout.addWidget(url_combo, 3, 1, 3, 3) url_combo.activated.connect(self._url_set) box = gui.vBox(self.controlArea, "Info") self.info = gui.widgetLabel(box, 'No data loaded.') self.warnings = gui.widgetLabel(box, '') box = gui.widgetBox(self.controlArea, "Columns (Double click to edit)") domain_editor = DomainEditor(self.variables) self.editor_model = domain_editor.model() box.layout().addWidget(domain_editor) box = gui.hBox(self.controlArea) gui.button( box, self, "Browse documentation data sets", callback=lambda: self.browse_file(True), autoDefault=False) gui.rubber(box) box.layout().addWidget(self.report_button) self.report_button.setFixedWidth(170) self.apply_button = gui.button( box, self, "Apply", callback=self.apply_domain_edit) self.apply_button.setEnabled(False) self.apply_button.setFixedWidth(170) self.editor_model.dataChanged.connect( lambda: self.apply_button.setEnabled(True)) self.set_file_list() # Must not call open_file from within __init__. open_file # explicitly re-enters the event loop (by a progress bar) self.setAcceptDrops(True) if self.source == self.LOCAL_FILE and \ os.path.getsize(self.last_path()) > self.SIZE_LIMIT: self.Warning.file_too_big() return QTimer.singleShot(0, self.load_data) def sizeHint(self): return QSize(600, 550) def select_file(self, n): assert n < len(self.recent_paths) super().select_file(n) if self.recent_paths: self.source = self.LOCAL_FILE self.load_data() self.set_file_list() def select_sheet(self): self.recent_paths[0].sheet = self.sheet_combo.currentText() self.load_data() def _url_set(self): self.source = self.URL self.load_data() def browse_file(self, in_demos=False): if in_demos: start_file = get_sample_datasets_dir() if not os.path.exists(start_file): QMessageBox.information( None, "File", "Cannot find the directory with documentation data sets") return else: start_file = self.last_path() or os.path.expanduser("~/") filename, _ = QFileDialog.getOpenFileName( self, 'Open Orange Data File', start_file, self.dlg_formats) if not filename: return self.loaded_file = filename self.add_path(filename) self.source = self.LOCAL_FILE self.load_data() # Open a file, create data from it and send it over the data channel def load_data(self): # We need to catch any exception type since anything can happen in # file readers # pylint: disable=broad-except self.editor_model.set_domain(None) self.apply_button.setEnabled(False) self.Warning.file_too_big.clear() error = None try: self.reader = self._get_reader() if self.reader is None: self.data = None self.send("Data", None) self.info.setText("No data.") self.sheet_box.hide() return except Exception as ex: error = ex if not error: self._update_sheet_combo() with catch_warnings(record=True) as warnings: try: data = self.reader.read() except Exception as ex: log.exception(ex) error = ex self.warning(warnings[-1].message.args[0] if warnings else '') if error: self.data = None self.send("Data", None) self.info.setText("An error occurred:\n{}".format(error)) self.editor_model.reset() self.sheet_box.hide() return self.info.setText(self._describe(data)) add_origin(data, self.loaded_file or self.last_path()) self.send("Data", data) self.editor_model.set_domain(data.domain) self.data = data def _get_reader(self): """ Returns ------- FileFormat """ if self.source == self.LOCAL_FILE: reader = FileFormat.get_reader(self.last_path()) if self.recent_paths and self.recent_paths[0].sheet: reader.select_sheet(self.recent_paths[0].sheet) return reader elif self.source == self.URL: url = self.url_combo.currentText().strip() if url: return UrlReader(url) def _update_sheet_combo(self): if len(self.reader.sheets) < 2: self.sheet_box.hide() self.reader.select_sheet(None) return self.sheet_combo.clear() self.sheet_combo.addItems(self.reader.sheets) self._select_active_sheet() self.sheet_box.show() def _select_active_sheet(self): if self.reader.sheet: try: idx = self.reader.sheets.index(self.reader.sheet) self.sheet_combo.setCurrentIndex(idx) except ValueError: # Requested sheet does not exist in this file self.reader.select_sheet(None) else: self.sheet_combo.setCurrentIndex(0) def _describe(self, table): domain = table.domain text = "" attrs = getattr(table, "attributes", {}) descs = [attrs[desc] for desc in ("Name", "Description") if desc in attrs] if len(descs) == 2: descs[0] = "<b>{}</b>".format(descs[0]) if descs: text += "<p>{}</p>".format("<br/>".join(descs)) text += "<p>{} instance(s), {} feature(s), {} meta attribute(s)".\ format(len(table), len(domain.attributes), len(domain.metas)) if domain.has_continuous_class: text += "<br/>Regression; numerical class." elif domain.has_discrete_class: text += "<br/>Classification; discrete class with {} values.".\ format(len(domain.class_var.values)) elif table.domain.class_vars: text += "<br/>Multi-target; {} target variables.".format( len(table.domain.class_vars)) else: text += "<br/>Data has no target variable." text += "</p>" if 'Timestamp' in table.domain: # Google Forms uses this header to timestamp responses text += '<p>First entry: {}<br/>Last entry: {}</p>'.format( table[0, 'Timestamp'], table[-1, 'Timestamp']) return text def storeSpecificSettings(self): self.current_context.modified_variables = self.variables[:] def retrieveSpecificSettings(self): if hasattr(self.current_context, "modified_variables"): self.variables[:] = self.current_context.modified_variables def apply_domain_edit(self): attributes = [] class_vars = [] metas = [] places = [attributes, class_vars, metas] X, y, m = [], [], [] cols = [X, y, m] # Xcols, Ycols, Mcols def is_missing(x): return str(x) in ("nan", "") for column, (name, tpe, place, vals, is_con), (orig_var, orig_plc) in \ zip(count(), self.editor_model.variables, chain([(at, 0) for at in self.data.domain.attributes], [(cl, 1) for cl in self.data.domain.class_vars], [(mt, 2) for mt in self.data.domain.metas])): if place == 3: continue if orig_plc == 2: col_data = list(chain(*self.data[:, orig_var].metas)) else: col_data = list(chain(*self.data[:, orig_var])) if name == orig_var.name and tpe == type(orig_var): var = orig_var elif tpe == DiscreteVariable: values = list(str(i) for i in set(col_data) if not is_missing(i)) var = tpe(name, values) col_data = [np.nan if is_missing(x) else values.index(str(x)) for x in col_data] elif tpe == StringVariable and type(orig_var) == DiscreteVariable: var = tpe(name) col_data = [orig_var.repr_val(x) if not np.isnan(x) else "" for x in col_data] else: var = tpe(name) places[place].append(var) cols[place].append(col_data) domain = Domain(attributes, class_vars, metas) X = np.array(X).T if len(X) else np.empty((len(self.data), 0)) y = np.array(y).T if len(y) else None dtpe = object if any(isinstance(m, StringVariable) for m in domain.metas) else float m = np.array(m, dtype=dtpe).T if len(m) else None table = Table.from_numpy(domain, X, y, m, self.data.W) self.send("Data", table) self.apply_button.setEnabled(False) def get_widget_name_extension(self): _, name = os.path.split(self.loaded_file) return os.path.splitext(name)[0] def send_report(self): def get_ext_name(filename): try: return FileFormat.names[os.path.splitext(filename)[1]] except KeyError: return "unknown" if self.data is None: self.report_paragraph("File", "No file.") return if self.source == self.LOCAL_FILE: home = os.path.expanduser("~") if self.loaded_file.startswith(home): # os.path.join does not like ~ name = "~/" + \ self.loaded_file[len(home):].lstrip("/").lstrip("\\") else: name = self.loaded_file if self.sheet_combo.isVisible(): name += " ({})".format(self.sheet_combo.currentText()) self.report_items("File", [("File name", name), ("Format", get_ext_name(name))]) else: self.report_items("Data", [("Resource", self.url), ("Format", get_ext_name(self.url))]) self.report_data("Data", self.data) def dragEnterEvent(self, event): """Accept drops of valid file urls""" urls = event.mimeData().urls() if urls: try: FileFormat.get_reader(OSX_NSURL_toLocalFile(urls[0]) or urls[0].toLocalFile()) event.acceptProposedAction() except IOError: pass def dropEvent(self, event): """Handle file drops""" urls = event.mimeData().urls() if urls: self.add_path(OSX_NSURL_toLocalFile(urls[0]) or urls[0].toLocalFile()) # add first file self.source = self.LOCAL_FILE self.load_data()
class OWResolweDataObject(widget.OWWidget): name = "Resolwe Data Object" description = "Resolwe Data object viewer" icon = "icons/OWResolweDataObject.svg" priority = 20 want_control_area = True want_main_area = False auto_commit = settings.Setting(True) class Inputs: data = widget.Input("Data", resolwe.Data) class Outputs: data = widget.Output("Data", Table) def __init__(self): super().__init__() self.data_table_object = None # type: Optional[resolwe.Data] # threading self._task = None # type: Optional[ResolweTask] self._executor = ThreadExecutor() box = gui.widgetBox(self.controlArea, 'Data Object') self._data_obj = QLabel(box) box.layout().addWidget(self._data_obj) box = gui.widgetBox(self.controlArea, 'Process info') self._proc_info = QLabel(box) box.layout().addWidget(self._proc_info) box = gui.widgetBox(self.controlArea, 'User permissions') self._usr_perm = QLabel(box) box.layout().addWidget(self._usr_perm) self.controlArea.setMinimumWidth(self.controlArea.sizeHint().width()) self.layout().setSizeConstraint(QLayout.SetFixedSize) gui.auto_commit(self.controlArea, self, "auto_commit", "Download data") self.res = ResolweHelper() @staticmethod def pack_table(info): return '<table>\n' + "\n".join( '<tr><td align="right" width="120">%s:</td>\n' '<td width="200">%s</td></tr>\n' % (d, textwrap.shorten(str(v), width=100, placeholder="...")) for d, v in info) + "</table>\n" @Inputs.data def set_data(self, data): # type: (Optional[resolwe.Data]) -> None self.data_table_object = data if self.data_table_object is not None: self.setup() def handleNewSignals(self): self.commit() def __setup_data_object_info(self): info = self.pack_table( (('Id', '{}'.format(self.data_table_object.id)), ('Name', '{}'.format(self.data_table_object.name)))) self._data_obj.setText(info) def __setup_proces_info(self): info = self.pack_table( (('Id', '{}'.format(self.data_table_object.process)), ('Name', '{}'.format(self.data_table_object.process_name)), ('Category', '{}'.format(self.data_table_object.process_type)))) self._proc_info.setText(info) def __setup_usr_permissions(self): if self.data_table_object.current_user_permissions: current_usr_perm = self.data_table_object.current_user_permissions[ 0] perms = current_usr_perm.get('permissions', None) if perms: perms = ','.join(perms) info = self.pack_table( (('Id', '{}'.format(current_usr_perm.get('id', None))), ('Name', '{}'.format(current_usr_perm.get('name', None))), ('Type', '{}'.format(current_usr_perm.get('type', None))), ('Permissions', '{}'.format(perms)))) self._usr_perm.setText(info) def setup(self): self.__setup_data_object_info() self.__setup_proces_info() self.__setup_usr_permissions() def commit(self): if not self.data_table_object: self.Outputs.data.send(None) return self.run_task() def run_task(self): if self._task is not None: self.cancel() assert self._task is None self.progressBarInit() func = partial(self.res.download_data_table, self.data_table_object) self._task = ResolweTask('download') self._task.future = self._executor.submit(func) self._task.watcher = FutureWatcher(self._task.future) self._task.watcher.done.connect(self.task_finished) @Slot(Future, name='Future') def task_finished(self, future): assert threading.current_thread() == threading.main_thread() assert self._task is not None assert self._task.future is future assert future.done() try: future_result = future.result() except Exception as ex: # TODO: raise exceptions raise ex else: if self._task.slug == 'download': self.Outputs.data.send(future_result) finally: self.progressBarFinished() self._task = None
class OWTreeGraph(OWTreeViewer2D): """Graphical visualization of tree models""" name = "Tree Viewer" icon = "icons/TreeViewer.svg" priority = 35 keywords = [] class Inputs: # Had different input names before merging from # Classification/Regression tree variants tree = Input("Tree", TreeModel, replaces=["Classification Tree", "Regression Tree"]) class Outputs: selected_data = Output("Selected Data", Table, default=True, id="selected-data") annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table, id="annotated-data") settingsHandler = ClassValuesContextHandler() target_class_index = ContextSetting(0) regression_colors = Setting(0) replaces = [ "Orange.widgets.classify.owclassificationtreegraph.OWClassificationTreeGraph", "Orange.widgets.classify.owregressiontreegraph.OWRegressionTreeGraph" ] COL_OPTIONS = ["Default", "Number of instances", "Mean value", "Variance"] COL_DEFAULT, COL_INSTANCE, COL_MEAN, COL_VARIANCE = range(4) def __init__(self): super().__init__() self.domain = None self.dataset = None self.clf_dataset = None self.tree_adapter = None self.color_label = QLabel("Target class: ") combo = self.color_combo = ComboBoxSearch() combo.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed) combo.setSizeAdjustPolicy( QComboBox.AdjustToMinimumContentsLengthWithIcon) combo.setMinimumContentsLength(8) combo.activated[int].connect(self.color_changed) self.display_box.layout().addRow(self.color_label, combo) def set_node_info(self): """Set the content of the node""" for node in self.scene.nodes(): node.set_rect(QRectF()) self.update_node_info(node) w = max([n.rect().width() for n in self.scene.nodes()] + [0]) if w > self.max_node_width: w = self.max_node_width for node in self.scene.nodes(): rect = node.rect() node.set_rect(QRectF(rect.x(), rect.y(), w, rect.height())) self.scene.fix_pos(self.root_node, 10, 10) def _update_node_info_attr_name(self, node, text): attr = self.tree_adapter.attribute(node.node_inst) if attr is not None: text += "<hr/>{}".format(attr.name) return text def activate_loaded_settings(self): if not self.model: return super().activate_loaded_settings() if self.domain.class_var.is_discrete: self.color_combo.setCurrentIndex(self.target_class_index) self.toggle_node_color_cls() else: self.color_combo.setCurrentIndex(self.regression_colors) self.toggle_node_color_reg() self.set_node_info() def color_changed(self, i): if self.domain.class_var.is_discrete: self.target_class_index = i self.toggle_node_color_cls() self.set_node_info() else: self.regression_colors = i self.toggle_node_color_reg() def toggle_node_size(self): self.set_node_info() self.scene.update() self.scene_view.repaint() def toggle_color_cls(self): self.toggle_node_color_cls() self.set_node_info() self.scene.update() def toggle_color_reg(self): self.toggle_node_color_reg() self.set_node_info() self.scene.update() @Inputs.tree def ctree(self, model=None): """Input signal handler""" self.clear_scene() self.color_combo.clear() self.closeContext() self.model = model self.target_class_index = 0 if model is None: self.infolabel.setText('No tree.') self.root_node = None self.dataset = None self.tree_adapter = None else: self.tree_adapter = self._get_tree_adapter(model) self.domain = model.domain self.dataset = model.instances if self.dataset is not None and self.dataset.domain != self.domain: self.clf_dataset = self.dataset.transform(model.domain) else: self.clf_dataset = self.dataset class_var = self.domain.class_var self.scene.colors = class_var.palette if class_var.is_discrete: self.color_label.setText("Target class: ") self.color_combo.addItem("None") self.color_combo.addItems(self.domain.class_vars[0].values) self.color_combo.setCurrentIndex(self.target_class_index) else: self.color_label.setText("Color by: ") self.color_combo.addItems(self.COL_OPTIONS) self.color_combo.setCurrentIndex(self.regression_colors) self.openContext(self.domain.class_var) # self.root_node = self.walkcreate(model.root, None) self.root_node = self.walkcreate(self.tree_adapter.root) self.infolabel.setText('{} nodes, {} leaves'.format( self.tree_adapter.num_nodes, len(self.tree_adapter.leaves(self.tree_adapter.root)))) self.setup_scene() self.Outputs.selected_data.send(None) self.Outputs.annotated_data.send(create_annotated_table(self.dataset, [])) def walkcreate(self, node, parent=None): """Create a structure of tree nodes from the given model""" node_obj = TreeNode(self.tree_adapter, node, parent) self.scene.addItem(node_obj) if parent: edge = GraphicsEdge(node1=parent, node2=node_obj) self.scene.addItem(edge) parent.graph_add_edge(edge) for child_inst in self.tree_adapter.children(node): if child_inst is not None: self.walkcreate(child_inst, node_obj) return node_obj def node_tooltip(self, node): return "<br>".join(to_html(str(rule)) for rule in self.tree_adapter.rules(node.node_inst)) def update_selection(self): if self.model is None: return nodes = [item.node_inst for item in self.scene.selectedItems() if isinstance(item, TreeNode)] data = self.tree_adapter.get_instances_in_nodes(nodes) self.Outputs.selected_data.send(data) self.Outputs.annotated_data.send(create_annotated_table( self.dataset, self.tree_adapter.get_indices(nodes))) def send_report(self): if not self.model: return items = [("Tree size", self.infolabel.text()), ("Edge widths", ("Fixed", "Relative to root", "Relative to parent")[ # pylint: disable=invalid-sequence-index self.line_width_method])] if self.domain.class_var.is_discrete: items.append(("Target class", self.color_combo.currentText())) elif self.regression_colors != self.COL_DEFAULT: items.append(("Color by", self.COL_OPTIONS[self.regression_colors])) self.report_items(items) self.report_plot(self.scene) def update_node_info(self, node): if self.domain.class_var.is_discrete: self.update_node_info_cls(node) else: self.update_node_info_reg(node) def update_node_info_cls(self, node): """Update the printed contents of the node for classification trees""" node_inst = node.node_inst distr = self.tree_adapter.get_distribution(node_inst)[0] total = self.tree_adapter.num_samples(node_inst) distr = distr / np.sum(distr) if self.target_class_index: tabs = distr[self.target_class_index - 1] text = "" else: modus = np.argmax(distr) tabs = distr[modus] text = f"<b>{self.domain.class_vars[0].values[int(modus)]}</b><br/>" if tabs > 0.999: text += f"100%, {total}/{total}" else: text += f"{100 * tabs:2.1f}%, {int(total * tabs)}/{total}" text = self._update_node_info_attr_name(node, text) node.setHtml( f'<p style="line-height: 120%; margin-bottom: 0">{text}</p>') def update_node_info_reg(self, node): """Update the printed contents of the node for regression trees""" node_inst = node.node_inst mean, var = self.tree_adapter.get_distribution(node_inst)[0] insts = self.tree_adapter.num_samples(node_inst) text = f"<b>{mean:.1f}</b> ± {var:.1f}<br/>" text += f"{insts} instances" text = self._update_node_info_attr_name(node, text) node.setHtml( f'<p style="line-height: 120%; margin-bottom: 0">{text}</p>') def toggle_node_color_cls(self): """Update the node color for classification trees""" colors = self.scene.colors for node in self.scene.nodes(): distr = node.tree_adapter.get_distribution(node.node_inst)[0] total = sum(distr) if self.target_class_index: p = distr[self.target_class_index - 1] / total color = colors[self.target_class_index - 1].lighter( 200 - 100 * p) else: modus = np.argmax(distr) p = distr[modus] / (total or 1) color = colors.value_to_qcolor(int(modus)) color = color.lighter(300 - 200 * p) node.backgroundBrush = QBrush(color) self.scene.update() def toggle_node_color_reg(self): """Update the node color for regression trees""" def_color = QColor(192, 192, 255) if self.regression_colors == self.COL_DEFAULT: brush = QBrush(def_color.lighter(100)) for node in self.scene.nodes(): node.backgroundBrush = brush elif self.regression_colors == self.COL_INSTANCE: max_insts = len(self.tree_adapter.get_instances_in_nodes( [self.tree_adapter.root])) for node in self.scene.nodes(): node_insts = len(self.tree_adapter.get_instances_in_nodes( [node.node_inst])) node.backgroundBrush = QBrush(def_color.lighter( 120 - 20 * node_insts / max_insts)) elif self.regression_colors == self.COL_MEAN: minv = np.nanmin(self.dataset.Y) maxv = np.nanmax(self.dataset.Y) colors = self.scene.colors for node in self.scene.nodes(): node_mean = self.tree_adapter.get_distribution(node.node_inst)[0][0] color = colors.value_to_qcolor(node_mean, minv, maxv) node.backgroundBrush = QBrush(color) else: nodes = list(self.scene.nodes()) variances = [self.tree_adapter.get_distribution(node.node_inst)[0][1] for node in nodes] max_var = max(variances) for node, var in zip(nodes, variances): node.backgroundBrush = QBrush(def_color.lighter( 120 - 20 * var / max_var)) self.scene.update() def _get_tree_adapter(self, model): if isinstance(model, SklModel): return SklTreeAdapter(model) return TreeAdapter(model)
class OWDatabasesUpdate(OWWidget): name = "Databases Update" description = "Update local systems biology databases." icon = "../widgets/icons/OWDatabasesUpdate.svg" priority = 1 inputs = [] outputs = [] want_main_area = False def __init__(self, parent=None, signalManager=None, name="Databases update"): OWWidget.__init__(self, parent, signalManager, name, wantMainArea=False) self.searchString = "" fbox = gui.widgetBox(self.controlArea, "Filter") self.completer = TokenListCompleter( self, caseSensitivity=Qt.CaseInsensitive) self.lineEditFilter = QLineEdit(textChanged=self.SearchUpdate) self.lineEditFilter.setCompleter(self.completer) fbox.layout().addWidget(self.lineEditFilter) box = gui.widgetBox(self.controlArea, "Files") self.filesView = QTreeWidget(self) self.filesView.setHeaderLabels( ["", "Data Source", "Update", "Last Updated", "Size"]) self.filesView.setRootIsDecorated(False) self.filesView.setUniformRowHeights(True) self.filesView.setSelectionMode(QAbstractItemView.NoSelection) self.filesView.setSortingEnabled(True) self.filesView.sortItems(1, Qt.AscendingOrder) self.filesView.setItemDelegateForColumn( 0, UpdateOptionsItemDelegate(self.filesView)) self.filesView.model().layoutChanged.connect(self.SearchUpdate) box.layout().addWidget(self.filesView) box = gui.widgetBox(self.controlArea, orientation="horizontal") self.updateButton = gui.button( box, self, "Update all", callback=self.UpdateAll, tooltip="Update all updatable files", ) self.downloadButton = gui.button( box, self, "Download all", callback=self.DownloadFiltered, tooltip="Download all filtered files shown" ) self.cancelButton = gui.button( box, self, "Cancel", callback=self.Cancel, tooltip="Cancel scheduled downloads/updates." ) self.retryButton = gui.button( box, self, "Reconnect", callback=self.RetrieveFilesList ) self.retryButton.hide() gui.rubber(box) self.warning(0) box = gui.widgetBox(self.controlArea, orientation="horizontal") gui.rubber(box) self.infoLabel = QLabel() self.infoLabel.setAlignment(Qt.AlignCenter) self.controlArea.layout().addWidget(self.infoLabel) self.infoLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.updateItems = [] self.resize(800, 600) self.progress = ProgressState(self, maximum=3) self.progress.valueChanged.connect(self._updateProgress) self.progress.rangeChanged.connect(self._updateProgress) self.executor = ThreadExecutor( threadPool=QThreadPool(maxThreadCount=2) ) task = Task(self, function=self.RetrieveFilesList) task.exceptionReady.connect(self.HandleError) task.start() self._tasks = [] self._haveProgress = False def RetrieveFilesList(self): self.retryButton.hide() self.warning(0) self.progress.setRange(0, 3) task = Task(function=partial(retrieveFilesList, methodinvoke(self.progress, "advance"))) task.resultReady.connect(self.SetFilesList) task.exceptionReady.connect(self.HandleError) self.executor.submit(task) self.setEnabled(False) def SetFilesList(self, serverInfo): """ Set the files to show. """ self.setEnabled(True) localInfo = serverfiles.allinfo() all_tags = set() self.filesView.clear() self.updateItems = [] for item in join_info_dict(localInfo, serverInfo): tree_item = UpdateTreeWidgetItem(item) options_widget = UpdateOptionsWidget(item.state) options_widget.item = item options_widget.installClicked.connect( partial(self.SubmitDownloadTask, item.domain, item.filename) ) options_widget.removeClicked.connect( partial(self.SubmitRemoveTask, item.domain, item.filename) ) self.updateItems.append((item, tree_item, options_widget)) all_tags.update(item.tags) self.filesView.addTopLevelItems( [tree_item for _, tree_item, _ in self.updateItems] ) for item, tree_item, options_widget in self.updateItems: self.filesView.setItemWidget(tree_item, 0, options_widget) # Add an update button if the file is updateable if item.state == OUTDATED: button = QToolButton( None, text="Update", maximumWidth=120, minimumHeight=20, maximumHeight=20 ) if sys.platform == "darwin": button.setAttribute(Qt.WA_MacSmallSize) button.clicked.connect( partial(self.SubmitDownloadTask, item.domain, item.filename) ) self.filesView.setItemWidget(tree_item, 2, button) self.progress.advance() self.filesView.setColumnWidth(0, self.filesView.sizeHintForColumn(0)) for column in range(1, 4): contents_hint = self.filesView.sizeHintForColumn(column) header_hint = self.filesView.header().sectionSizeHint(column) width = max(min(contents_hint, 400), header_hint) self.filesView.setColumnWidth(column, width) hints = [hint for hint in sorted(all_tags) if not hint.startswith("#")] self.completer.setTokenList(hints) self.SearchUpdate() self.UpdateInfoLabel() self.toggleButtons() self.cancelButton.setEnabled(False) self.progress.setRange(0, 0) def buttonCheck(self, selected_items, state, button): for item in selected_items: if item.state != state: button.setEnabled(False) else: button.setEnabled(True) break def toggleButtons(self): selected_items = [item for item, tree_item, _ in self.updateItems if not tree_item.isHidden()] self.buttonCheck(selected_items, OUTDATED, self.updateButton) self.buttonCheck(selected_items, AVAILABLE, self.downloadButton) def HandleError(self, exception): if isinstance(exception, ConnectionError): self.warning(0, "Could not connect to server! Check your connection " "and try to reconnect.") self.SetFilesList({}) self.retryButton.show() else: sys.excepthook(type(exception), exception, None) self.progress.setRange(0, 0) self.setEnabled(True) def UpdateInfoLabel(self): local = [item for item, tree_item, _ in self.updateItems if item.state != AVAILABLE and not tree_item.isHidden()] size = sum(float(item.size) for item in local) onServer = [item for item, tree_item, _ in self.updateItems if not tree_item.isHidden()] sizeOnServer = sum(float(item.size) for item in onServer) text = ("%i items, %s (on server: %i items, %s)" % (len(local), serverfiles.sizeformat(size), len(onServer), serverfiles.sizeformat(sizeOnServer))) self.infoLabel.setText(text) def UpdateAll(self): self.warning(0) for item, tree_item, _ in self.updateItems: if item.state == OUTDATED and not tree_item.isHidden(): self.SubmitDownloadTask(item.domain, item.filename) def DownloadFiltered(self): # TODO: submit items in the order shown. for item, tree_item, _ in self.updateItems: if not tree_item.isHidden() and item.state in \ [AVAILABLE, OUTDATED]: self.SubmitDownloadTask(item.domain, item.filename) def SearchUpdate(self, searchString=None): strings = str(self.lineEditFilter.text()).split() for item, tree_item, _ in self.updateItems: hide = not all(UpdateItem_match(item, string) for string in strings) tree_item.setHidden(hide) self.UpdateInfoLabel() self.toggleButtons() def SubmitDownloadTask(self, domain, filename): """ Submit the (domain, filename) to be downloaded/updated. """ self.cancelButton.setEnabled(True) index = self.updateItemIndex(domain, filename) _, tree_item, opt_widget = self.updateItems[index] task = DownloadTask(domain, filename, serverfiles.LOCALFILES) self.progress.adjustRange(0, 100) pb = ItemProgressBar(self.filesView) pb.setRange(0, 100) pb.setTextVisible(False) task.advanced.connect(pb.advance) task.advanced.connect(self.progress.advance) task.finished.connect(pb.hide) task.finished.connect(self.onDownloadFinished, Qt.QueuedConnection) task.exception.connect(self.onDownloadError, Qt.QueuedConnection) self.filesView.setItemWidget(tree_item, 2, pb) # Clear the text so it does not show behind the progress bar. tree_item.setData(2, Qt.DisplayRole, "") pb.show() # Disable the options widget opt_widget.setEnabled(False) self._tasks.append(task) self.executor.submit(task) def EndDownloadTask(self, task): future = task.future() index = self.updateItemIndex(task.domain, task.filename) item, tree_item, opt_widget = self.updateItems[index] self.filesView.removeItemWidget(tree_item, 2) opt_widget.setEnabled(True) if future.cancelled(): # Restore the previous state tree_item.setUpdateItem(item) opt_widget.setState(item.state) elif future.exception(): tree_item.setUpdateItem(item) opt_widget.setState(item.state) # Show the exception string in the size column. self.warning(0, "Error while downloading. Check your connection " "and retry.") # recreate button for download button = QToolButton( None, text="Retry", maximumWidth=120, minimumHeight=20, maximumHeight=20 ) if sys.platform == "darwin": button.setAttribute(Qt.WA_MacSmallSize) button.clicked.connect( partial(self.SubmitDownloadTask, item.domain, item.filename) ) self.filesView.setItemWidget(tree_item, 2, button) else: # get the new updated info dict and replace the the old item self.warning(0) info = serverfiles.info(item.domain, item.filename) new_item = update_item_from_info(item.domain, item.filename, info, info) self.updateItems[index] = (new_item, tree_item, opt_widget) tree_item.setUpdateItem(new_item) opt_widget.setState(new_item.state) self.UpdateInfoLabel() def SubmitRemoveTask(self, domain, filename): serverfiles.LOCALFILES.remove(domain, filename) index = self.updateItemIndex(domain, filename) item, tree_item, opt_widget = self.updateItems[index] if item.info_server: new_item = item._replace(state=AVAILABLE, local=None, info_local=None) else: new_item = item._replace(local=None, info_local=None) # Disable the options widget. No more actions can be performed # for the item. opt_widget.setEnabled(False) tree_item.setUpdateItem(new_item) opt_widget.setState(new_item.state) self.updateItems[index] = (new_item, tree_item, opt_widget) self.UpdateInfoLabel() def Cancel(self): """ Cancel all pending update/download tasks (that have not yet started). """ for task in self._tasks: task.future().cancel() def onDeleteWidget(self): self.Cancel() self.executor.shutdown(wait=False) OWWidget.onDeleteWidget(self) def onDownloadFinished(self): # on download completed/canceled/error assert QThread.currentThread() is self.thread() for task in list(self._tasks): future = task.future() if future.done(): self.EndDownloadTask(task) self._tasks.remove(task) if not self._tasks: # Clear/reset the overall progress self.progress.setRange(0, 0) self.cancelButton.setEnabled(False) def onDownloadError(self, exc_info): sys.excepthook(*exc_info) self.warning(0, "Error while downloading. Check your connection and " "retry.") def updateItemIndex(self, domain, filename): for i, (item, _, _) in enumerate(self.updateItems): if item.domain == domain and item.filename == filename: return i raise ValueError("%r, %r not in update list" % (domain, filename)) def _updateProgress(self, *args): rmin, rmax = self.progress.range() if rmin != rmax: if not self._haveProgress: self._haveProgress = True self.progressBarInit() self.progressBarSet(self.progress.ratioCompleted() * 100, processEvents=None) if rmin == rmax: self._haveProgress = False self.progressBarFinished()
class OWFile(widget.OWWidget, RecentPathsWComboMixin): name = "File" id = "orange.widgets.data.file" description = "Read data from an input file or network " \ "and send a data table to the output." icon = "icons/File.svg" priority = 10 category = "Data" keywords = ["file", "load", "read", "open"] class Outputs: data = Output("Data", Table, doc="Attribute-valued dataset read from the input file.") want_main_area = False SEARCH_PATHS = [("sample-datasets", get_sample_datasets_dir())] SIZE_LIMIT = 1e7 LOCAL_FILE, URL = range(2) settingsHandler = PerfectDomainContextHandler( match_values=PerfectDomainContextHandler.MATCH_VALUES_ALL ) # pylint seems to want declarations separated from definitions recent_paths: List[RecentPath] recent_urls: List[str] variables: list # Overload RecentPathsWidgetMixin.recent_paths to set defaults recent_paths = Setting([ RecentPath("", "sample-datasets", "iris.tab"), RecentPath("", "sample-datasets", "titanic.tab"), RecentPath("", "sample-datasets", "housing.tab"), RecentPath("", "sample-datasets", "heart_disease.tab"), ]) recent_urls = Setting([]) source = Setting(LOCAL_FILE) xls_sheet = ContextSetting("") sheet_names = Setting({}) url = Setting("") variables = ContextSetting([]) domain_editor = SettingProvider(DomainEditor) class Warning(widget.OWWidget.Warning): file_too_big = widget.Msg("The file is too large to load automatically." " Press Reload to load.") load_warning = widget.Msg("Read warning:\n{}") class Error(widget.OWWidget.Error): file_not_found = widget.Msg("File not found.") missing_reader = widget.Msg("Missing reader.") sheet_error = widget.Msg("Error listing available sheets.") unknown = widget.Msg("Read error:\n{}") class NoFileSelected: pass def __init__(self): super().__init__() RecentPathsWComboMixin.__init__(self) self.domain = None self.data = None self.loaded_file = "" self.reader = None layout = QGridLayout() gui.widgetBox(self.controlArea, margin=0, orientation=layout) vbox = gui.radioButtons(None, self, "source", box=True, addSpace=True, callback=self.load_data, addToLayout=False) rb_button = gui.appendRadioButton(vbox, "File:", addToLayout=False) layout.addWidget(rb_button, 0, 0, Qt.AlignVCenter) box = gui.hBox(None, addToLayout=False, margin=0) box.setSizePolicy(Policy.MinimumExpanding, Policy.Fixed) self.file_combo.setSizePolicy(Policy.MinimumExpanding, Policy.Fixed) self.file_combo.activated[int].connect(self.select_file) box.layout().addWidget(self.file_combo) layout.addWidget(box, 0, 1) file_button = gui.button( None, self, '...', callback=self.browse_file, autoDefault=False) file_button.setIcon(self.style().standardIcon(QStyle.SP_DirOpenIcon)) file_button.setSizePolicy(Policy.Maximum, Policy.Fixed) layout.addWidget(file_button, 0, 2) reload_button = gui.button( None, self, "Reload", callback=self.load_data, autoDefault=False) reload_button.setIcon(self.style().standardIcon( QStyle.SP_BrowserReload)) reload_button.setSizePolicy(Policy.Fixed, Policy.Fixed) layout.addWidget(reload_button, 0, 3) self.sheet_box = gui.hBox(None, addToLayout=False, margin=0) self.sheet_combo = gui.comboBox(None, self, "xls_sheet", callback=self.select_sheet, sendSelectedValue=True,) self.sheet_combo.setSizePolicy( Policy.MinimumExpanding, Policy.Fixed) self.sheet_label = QLabel() self.sheet_label.setText('Sheet') self.sheet_label.setSizePolicy( Policy.MinimumExpanding, Policy.Fixed) self.sheet_box.layout().addWidget( self.sheet_label, Qt.AlignLeft) self.sheet_box.layout().addWidget( self.sheet_combo, Qt.AlignVCenter) layout.addWidget(self.sheet_box, 2, 1) self.sheet_box.hide() rb_button = gui.appendRadioButton(vbox, "URL:", addToLayout=False) layout.addWidget(rb_button, 3, 0, Qt.AlignVCenter) self.url_combo = url_combo = QComboBox() url_model = NamedURLModel(self.sheet_names) url_model.wrap(self.recent_urls) url_combo.setLineEdit(LineEditSelectOnFocus()) url_combo.setModel(url_model) url_combo.setSizePolicy(Policy.MinimumExpanding, Policy.Fixed) url_combo.setEditable(True) url_combo.setInsertPolicy(url_combo.InsertAtTop) url_edit = url_combo.lineEdit() l, t, r, b = url_edit.getTextMargins() url_edit.setTextMargins(l + 5, t, r, b) layout.addWidget(url_combo, 3, 1, 3, 3) url_combo.activated.connect(self._url_set) box = gui.vBox(self.controlArea, "Info") self.infolabel = gui.widgetLabel(box, 'No data loaded.') self.warnings = gui.widgetLabel(box, '') box = gui.widgetBox(self.controlArea, "Columns (Double click to edit)") self.domain_editor = DomainEditor(self) self.editor_model = self.domain_editor.model() box.layout().addWidget(self.domain_editor) box = gui.hBox(self.controlArea) gui.button( box, self, "Browse documentation datasets", callback=lambda: self.browse_file(True), autoDefault=False) gui.rubber(box) gui.button( box, self, "Reset", callback=self.reset_domain_edit) self.apply_button = gui.button( box, self, "Apply", callback=self.apply_domain_edit) self.apply_button.setEnabled(False) self.apply_button.setFixedWidth(170) self.editor_model.dataChanged.connect( lambda: self.apply_button.setEnabled(True)) self.set_file_list() # Must not call open_file from within __init__. open_file # explicitly re-enters the event loop (by a progress bar) self.setAcceptDrops(True) if self.source == self.LOCAL_FILE: last_path = self.last_path() if last_path and os.path.exists(last_path) and \ os.path.getsize(last_path) > self.SIZE_LIMIT: self.Warning.file_too_big() return QTimer.singleShot(0, self.load_data) @staticmethod def sizeHint(): return QSize(600, 550) def select_file(self, n): assert n < len(self.recent_paths) super().select_file(n) if self.recent_paths: self.source = self.LOCAL_FILE self.load_data() self.set_file_list() def select_sheet(self): self.recent_paths[0].sheet = self.sheet_combo.currentText() self.load_data() def _url_set(self): url = self.url_combo.currentText() pos = self.recent_urls.index(url) url = url.strip() if not urlparse(url).scheme: url = 'http://' + url self.url_combo.setItemText(pos, url) self.recent_urls[pos] = url self.source = self.URL self.load_data() def browse_file(self, in_demos=False): if in_demos: start_file = get_sample_datasets_dir() if not os.path.exists(start_file): QMessageBox.information( None, "File", "Cannot find the directory with documentation datasets") return else: start_file = self.last_path() or os.path.expanduser("~/") readers = [f for f in FileFormat.formats if getattr(f, 'read', None) and getattr(f, "EXTENSIONS", None)] filename, reader, _ = open_filename_dialog(start_file, None, readers) if not filename: return self.add_path(filename) if reader is not None: self.recent_paths[0].file_format = reader.qualified_name() self.source = self.LOCAL_FILE self.load_data() # Open a file, create data from it and send it over the data channel def load_data(self): # We need to catch any exception type since anything can happen in # file readers self.closeContext() self.domain_editor.set_domain(None) self.apply_button.setEnabled(False) self.clear_messages() self.set_file_list() error = self._try_load() if error: error() self.data = None self.sheet_box.hide() self.Outputs.data.send(None) self.infolabel.setText("No data.") def _try_load(self): # pylint: disable=broad-except if self.last_path() and not os.path.exists(self.last_path()): return self.Error.file_not_found try: self.reader = self._get_reader() assert self.reader is not None except Exception: return self.Error.missing_reader if self.reader is self.NoFileSelected: self.Outputs.data.send(None) return None try: self._update_sheet_combo() except Exception: return self.Error.sheet_error with catch_warnings(record=True) as warnings: try: data = self.reader.read() except Exception as ex: log.exception(ex) return lambda x=ex: self.Error.unknown(str(x)) if warnings: self.Warning.load_warning(warnings[-1].message.args[0]) self.infolabel.setText(self._describe(data)) self.loaded_file = self.last_path() add_origin(data, self.loaded_file) self.data = data self.openContext(data.domain) self.apply_domain_edit() # sends data return None def _get_reader(self) -> FileFormat: if self.source == self.LOCAL_FILE: path = self.last_path() if path is None: return self.NoFileSelected if self.recent_paths and self.recent_paths[0].file_format: qname = self.recent_paths[0].file_format reader_class = class_from_qualified_name(qname) reader = reader_class(path) else: reader = FileFormat.get_reader(path) if self.recent_paths and self.recent_paths[0].sheet: reader.select_sheet(self.recent_paths[0].sheet) return reader else: url = self.url_combo.currentText().strip() if url: return UrlReader(url) else: return self.NoFileSelected def _update_sheet_combo(self): if len(self.reader.sheets) < 2: self.sheet_box.hide() self.reader.select_sheet(None) return self.sheet_combo.clear() self.sheet_combo.addItems(self.reader.sheets) self._select_active_sheet() self.sheet_box.show() def _select_active_sheet(self): if self.reader.sheet: try: idx = self.reader.sheets.index(self.reader.sheet) self.sheet_combo.setCurrentIndex(idx) except ValueError: # Requested sheet does not exist in this file self.reader.select_sheet(None) else: self.sheet_combo.setCurrentIndex(0) @staticmethod def _describe(table): def missing_prop(prop): if prop: return f"({prop * 100:.1f}% missing values)" else: return "(no missing values)" domain = table.domain text = "" attrs = getattr(table, "attributes", {}) descs = [attrs[desc] for desc in ("Name", "Description") if desc in attrs] if len(descs) == 2: descs[0] = f"<b>{descs[0]}</b>" if descs: text += f"<p>{'<br/>'.join(descs)}</p>" text += f"<p>{len(table)} instance(s)" missing_in_attr = missing_prop(table.has_missing_attribute() and table.get_nan_frequency_attribute()) missing_in_class = missing_prop(table.has_missing_class() and table.get_nan_frequency_class()) text += f"<br/>{len(domain.attributes)} feature(s) {missing_in_attr}" if domain.has_continuous_class: text += f"<br/>Regression; numerical class {missing_in_class}" elif domain.has_discrete_class: text += "<br/>Classification; categorical class " \ f"with {len(domain.class_var.values)} values {missing_in_class}" elif table.domain.class_vars: text += "<br/>Multi-target; " \ f"{len(table.domain.class_vars)} target variables " \ f"{missing_in_class}" else: text += "<br/>Data has no target variable." text += f"<br/>{len(domain.metas)} meta attribute(s)" text += "</p>" if 'Timestamp' in table.domain: # Google Forms uses this header to timestamp responses text += f"<p>First entry: {table[0, 'Timestamp']}<br/>" \ f"Last entry: {table[-1, 'Timestamp']}</p>" return text def storeSpecificSettings(self): self.current_context.modified_variables = self.variables[:] def retrieveSpecificSettings(self): if hasattr(self.current_context, "modified_variables"): self.variables[:] = self.current_context.modified_variables def reset_domain_edit(self): self.domain_editor.reset_domain() self.apply_domain_edit() def apply_domain_edit(self): if self.data is None: table = None else: domain, cols = self.domain_editor.get_domain(self.data.domain, self.data) if not (domain.variables or domain.metas): table = None else: X, y, m = cols table = Table.from_numpy(domain, X, y, m, self.data.W) table.name = self.data.name table.ids = np.array(self.data.ids) table.attributes = getattr(self.data, 'attributes', {}) self.Outputs.data.send(table) self.apply_button.setEnabled(False) def get_widget_name_extension(self): _, name = os.path.split(self.loaded_file) return os.path.splitext(name)[0] def send_report(self): def get_ext_name(filename): try: return FileFormat.names[os.path.splitext(filename)[1]] except KeyError: return "unknown" if self.data is None: self.report_paragraph("File", "No file.") return if self.source == self.LOCAL_FILE: home = os.path.expanduser("~") if self.loaded_file.startswith(home): # os.path.join does not like ~ name = "~" + os.path.sep + \ self.loaded_file[len(home):].lstrip("/").lstrip("\\") else: name = self.loaded_file if self.sheet_combo.isVisible(): name += f" ({self.sheet_combo.currentText()})" self.report_items("File", [("File name", name), ("Format", get_ext_name(name))]) else: self.report_items("Data", [("Resource", self.url), ("Format", get_ext_name(self.url))]) self.report_data("Data", self.data) @staticmethod def dragEnterEvent(event): """Accept drops of valid file urls""" urls = event.mimeData().urls() if urls: try: FileFormat.get_reader(OSX_NSURL_toLocalFile(urls[0]) or urls[0].toLocalFile()) event.acceptProposedAction() except IOError: pass def dropEvent(self, event): """Handle file drops""" urls = event.mimeData().urls() if urls: self.add_path(OSX_NSURL_toLocalFile(urls[0]) or urls[0].toLocalFile()) # add first file self.source = self.LOCAL_FILE self.load_data() def workflowEnvChanged(self, key, value, oldvalue): """ Function called when environment changes (e.g. while saving the scheme) It make sure that all environment connected values are modified (e.g. relative file paths are changed) """ self.update_file_list(key, value, oldvalue)
class PreviewDialog(QDialog): """A Dialog for selecting an item from a PreviewItem. """ currentIndexChanged = Signal(int) def __init__(self, parent=None, flags=Qt.WindowFlags(0), model=None, **kwargs): QDialog.__init__(self, parent, flags, **kwargs) self.__setupUi() if model is not None: self.setModel(model) def __setupUi(self): layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) self.setContentsMargins(0, 0, 0, 0) heading = self.tr("Preview") heading = u"<h3>{0}</h3>".format(heading) self.__heading = QLabel(heading, self, objectName="heading") self.__heading.setContentsMargins(12, 12, 12, 0) self.__browser = previewbrowser.PreviewBrowser(self) self.__buttons = QDialogButtonBox(QDialogButtonBox.Open | \ QDialogButtonBox.Cancel, Qt.Horizontal,) self.__buttons.button(QDialogButtonBox.Open).setAutoDefault(True) # Set the Open dialog as disabled until the current index changes self.__buttons.button(QDialogButtonBox.Open).setEnabled(False) # The QDialogButtonsWidget messes with the layout if it is # contained directly in the QDialog. So we create an extra # layer of indirection. buttons = QWidget(objectName="button-container") buttons_l = QVBoxLayout() buttons_l.setContentsMargins(12, 0, 12, 12) buttons.setLayout(buttons_l) buttons_l.addWidget(self.__buttons) layout.addWidget(self.__heading) layout.addWidget(self.__browser) layout.addWidget(buttons) self.__buttons.accepted.connect(self.accept) self.__buttons.rejected.connect(self.reject) self.__browser.currentIndexChanged.connect( self.__on_currentIndexChanged) self.__browser.activated.connect(self.__on_activated) layout.setSizeConstraint(QVBoxLayout.SetFixedSize) self.setLayout(layout) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) def setItems(self, items): """Set the items (a list of strings) for preview/selection. """ model = QStringListModel(items) self.setModel(model) def setModel(self, model): """Set the model for preview/selection. """ self.__browser.setModel(model) def model(self): """Return the model. """ return self.__browser.model() def currentIndex(self): return self.__browser.currentIndex() def setCurrentIndex(self, index): """Set the current selected (shown) index. """ self.__browser.setCurrentIndex(index) def setHeading(self, heading): """Set `heading` as the heading string ('<h3>Preview</h3>' by default). """ self.__heading.setText(heading) def heading(self): """Return the heading string. """ def __on_currentIndexChanged(self, index): button = self.__buttons.button(QDialogButtonBox.Open) button.setEnabled(index >= 0) self.currentIndexChanged.emit(index) def __on_activated(self, index): if self.currentIndex() != index: self.setCurrentIndex(index) self.accept()
def generate_panel(self, formset): """ Generate a panel for the module form with all the controls formset format example: [('_video', '_arenas', '_run'), {"Player":['_threshold', "_player", "=", "_results", "_query"], "Background image":[(' ', '_selectBackground', '_paintBackground'), '_image']}, "_progress"] tuple: will display the controls in the same horizontal line list: will display the controls in the same vertical line dict: will display the controls in a tab widget '||': will plit the controls in a horizontal line '=': will plit the controls in a vertical line @param formset: Form configuration @type formset: list """ control = None if '=' in formset or isinstance(formset, hsplitter): control = QSplitter(QtCore.Qt.Vertical) index = list(formset).index('=') first_panel = self.generate_panel(formset[0:index]) second_panel = self.generate_panel(formset[index + 1:]) control.addWidget(first_panel) control.addWidget(second_panel) self._splitters.append(control) return control elif '||' in formset or isinstance(formset, vsplitter): control = QSplitter(QtCore.Qt.Horizontal) index = list(formset).index('||') first_panel = self.generate_panel(formset[0:index]) second_panel = self.generate_panel(formset[index + 1:]) control.addWidget(first_panel) control.addWidget(second_panel) if isinstance(formset, vsplitter): sizes = [formset.left_width, formset.right_width] control.setSizes(sizes) self._splitters.append(control) return control control = QFrame(self) layout = None if type(formset) is tuple: layout = QHBoxLayout() for row in formset: if isinstance(row, (list, tuple, vsplitter, hsplitter)): panel = self.generate_panel(row) layout.addWidget(panel) elif row == " ": spacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) layout.addItem(spacer) elif type(row) is dict: c = self.generate_tabs(row) layout.addWidget(c) self._tabs.append(c) else: param = self.controls.get(row, None) if param is None: label = QLabel() label.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) # layout.addWidget( label ) if row.startswith('info:'): label.setText(row[5:]) font = QFont() font.setPointSize(10) label.setFont(font) label.setAccessibleName('info') elif row.startswith('h1:'): label.setText(row[3:]) font = QFont() font.setPointSize(17) font.setBold(True) label.setFont(font) label.setAccessibleName('h1') elif row.startswith('h2:'): label.setText(row[3:]) font = QFont() font.setPointSize(16) font.setBold(True) label.setFont(font) label.setAccessibleName('h2') elif row.startswith('h3:'): label.setText(row[3:]) font = QFont() font.setPointSize(15) font.setBold(True) label.setFont(font) label.setAccessibleName('h3') elif row.startswith('h4:'): label.setText(row[3:]) font = QFont() font.setPointSize(14) font.setBold(True) label.setFont(font) label.setAccessibleName('h4') elif row.startswith('h5:'): label.setText(row[3:]) font = QFont() font.setPointSize(12) font.setBold(True) label.setFont(font) label.setAccessibleName('h5') else: label.setText(row) font = QFont() font.setPointSize(10) label.setFont(font) label.setAccessibleName('msg') label.setToolTip(label.text()) layout.addWidget(label) else: param.parent = self param.name = row layout.addWidget(param.form) elif type(formset) is list: layout = QVBoxLayout() for row in formset: if isinstance(row, (list, tuple, vsplitter, hsplitter)): panel = self.generate_panel(row) layout.addWidget(panel) elif row == " ": spacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) layout.addItem(spacer) elif type(row) is dict: c = self.generate_tabs(row) layout.addWidget(c) self._tabs.append(c) else: param = self.controls.get(row, None) if param is None: label = QLabel() label.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) label.resize(30, 30) # layout.addWidget( label ) if row.startswith('info:'): label.setText(row[5:]) font = QFont() font.setPointSize(10) label.setFont(font) label.setAccessibleName('info') elif row.startswith('h1:'): label.setText(row[3:]) font = QFont() font.setPointSize(17) font.setBold(True) label.setFont(font) label.setAccessibleName('h1') elif row.startswith('h2:'): label.setText(row[3:]) font = QFont() font.setPointSize(16) font.setBold(True) label.setFont(font) label.setAccessibleName('h2') elif row.startswith('h3:'): label.setText(row[3:]) font = QFont() font.setPointSize(15) font.setBold(True) label.setFont(font) label.setAccessibleName('h3') elif row.startswith('h4:'): label.setText(row[3:]) font = QFont() font.setPointSize(14) font.setBold(True) label.setFont(font) label.setAccessibleName('h4') elif row.startswith('h5:'): label.setText(row[3:]) font = QFont() font.setPointSize(12) font.setBold(True) label.setFont(font) label.setAccessibleName('h5') else: label.setText(row) font = QFont() font.setPointSize(10) label.setFont(font) label.setAccessibleName('msg') label.setToolTip(label.text()) layout.addWidget(label) else: param.parent = self param.name = row layout.addWidget(param.form) if _api.USED_API == _api.QT_API_PYQT5: layout.setContentsMargins(0, 0, 0, 0) elif _api.USED_API == _api.QT_API_PYQT4: layout.setMargin(0) control.setLayout(layout) return control
class OWNNLearner(OWBaseLearner): name = "神经网络(Neural Network)" description = "一种具有反向传播的多层感知器(MLP)算法。" icon = "icons/NN.svg" priority = 90 keywords = ["mlp", 'shenjingwangluo'] category = '模型(Model)' LEARNER = NNLearner activation = ["identity", "logistic", "tanh", "relu"] act_lbl = ["Identity", "Logistic", "tanh", "ReLu"] chinese_act_lbl = ["相等", "Logistic", "tanh", "ReLu"] solver = ["lbfgs", "sgd", "adam"] solv_lbl = ["L-BFGS-B", "SGD", "Adam"] learner_name = Setting("Neural Network") hidden_layers_input = Setting("100,") activation_index = Setting(3) solver_index = Setting(2) max_iterations = Setting(200) alpha_index = Setting(1) replicable = Setting(True) settings_version = 2 alphas = list( chain([0], [x / 10000 for x in range(1, 10)], [x / 1000 for x in range(1, 10)], [x / 100 for x in range(1, 10)], [x / 10 for x in range(1, 10)], range(1, 10), range(10, 100, 5), range(100, 200, 10), range(100, 1001, 50))) class Warning(OWBaseLearner.Warning): no_layers = Msg("ANN without hidden layers is equivalent to logistic " "regression with worse fitting.\nWe recommend using " "logistic regression.") def add_main_layout(self): # this is part of init, pylint: disable=attribute-defined-outside-init form = QFormLayout() form.setFieldGrowthPolicy(form.AllNonFixedFieldsGrow) form.setLabelAlignment(Qt.AlignLeft) gui.widgetBox(self.controlArea, True, orientation=form) form.addRow( "隐藏层中的神经元:", gui.lineEdit(None, self, "hidden_layers_input", orientation=Qt.Horizontal, callback=self.settings_changed, tooltip="定义神经元的整数列表。列表长度定义层数。例如4、2、2、3。", placeholderText="e.g. 10,")) form.addRow( "激活:", gui.comboBox(None, self, "activation_index", orientation=Qt.Horizontal, label="Activation:", items=[i for i in self.chinese_act_lbl], callback=self.settings_changed)) form.addRow( "求解器(Solver):", gui.comboBox(None, self, "solver_index", orientation=Qt.Horizontal, label="Solver:", items=[i for i in self.solv_lbl], callback=self.settings_changed)) self.reg_label = QLabel() slider = gui.hSlider(None, self, "alpha_index", minValue=0, maxValue=len(self.alphas) - 1, callback=lambda: (self.set_alpha(), self.settings_changed()), createLabel=False) form.addRow(self.reg_label, slider) self.set_alpha() form.addRow( "最大迭代次数:", gui.spin(None, self, "max_iterations", 10, 1000000, step=10, label="Max iterations:", orientation=Qt.Horizontal, alignment=Qt.AlignRight, callback=self.settings_changed)) form.addRow( gui.checkBox(None, self, "replicable", label="可重复训练", callback=self.settings_changed, attribute=Qt.WA_LayoutUsesWidgetRect)) def set_alpha(self): # called from init, pylint: disable=attribute-defined-outside-init self.strength_C = self.alphas[self.alpha_index] self.reg_label.setText("正则化, α={}:".format(self.strength_C)) @property def alpha(self): return self.alphas[self.alpha_index] def setup_layout(self): # this is part of init, pylint: disable=attribute-defined-outside-init super().setup_layout() self._task = None # type: Optional[Task] self._executor = ThreadExecutor() # just a test cancel button b = gui.button(self.apply_button, self, "取消", callback=self.cancel, addToLayout=False) self.apply_button.layout().insertStretch(0, 100) self.apply_button.layout().insertWidget(0, b) def create_learner(self): return self.LEARNER(hidden_layer_sizes=self.get_hidden_layers(), activation=self.activation[self.activation_index], solver=self.solver[self.solver_index], alpha=self.alpha, random_state=1 if self.replicable else None, max_iter=self.max_iterations, preprocessors=self.preprocessors) def get_learner_parameters(self): return (("Hidden layers", ', '.join(map(str, self.get_hidden_layers()))), ("Activation", self.act_lbl[self.activation_index]), ("Solver", self.solv_lbl[self.solver_index]), ("Alpha", self.alpha), ("Max iterations", self.max_iterations), ("Replicable training", self.replicable)) def get_hidden_layers(self): self.Warning.no_layers.clear() layers = tuple(map(int, re.findall(r'\d+', self.hidden_layers_input))) if not layers: self.Warning.no_layers() return layers def update_model(self): self.show_fitting_failed(None) self.model = None if self.check_data(): self.__update() else: self.Outputs.model.send(self.model) @Slot(float) def setProgressValue(self, value): assert self.thread() is QThread.currentThread() self.progressBarSet(value) def __update(self): if self._task is not None: # First make sure any pending tasks are cancelled. self.cancel() assert self._task is None max_iter = self.learner.kwargs["max_iter"] # Setup the task state task = Task() lastemitted = 0. def callback(iteration): nonlocal task nonlocal lastemitted if task.isInterruptionRequested(): raise CancelTaskException() progress = round(iteration / max_iter * 100) if progress != lastemitted: task.emitProgressUpdate(progress) lastemitted = progress # copy to set the callback so that the learner output is not modified # (currently we can not pass callbacks to learners __call__) learner = copy.copy(self.learner) learner.callback = callback def build_model(data, learner): try: return learner(data) except CancelTaskException: return None build_model_func = partial(build_model, self.data, learner) task.setFuture(self._executor.submit(build_model_func)) task.done.connect(self._task_finished) task.progressChanged.connect(self.setProgressValue) # set in setup_layout; pylint: disable=attribute-defined-outside-init self._task = task self.progressBarInit() self.setBlocking(True) @Slot(concurrent.futures.Future) def _task_finished(self, f): """ Parameters ---------- f : Future The future instance holding the built model """ assert self.thread() is QThread.currentThread() assert self._task is not None assert self._task.future is f assert f.done() self._task.deleteLater() self._task = None # pylint: disable=attribute-defined-outside-init self.setBlocking(False) self.progressBarFinished() try: self.model = f.result() except Exception as ex: # pylint: disable=broad-except # Log the exception with a traceback log = logging.getLogger() log.exception(__name__, exc_info=True) self.model = None self.show_fitting_failed(ex) else: self.model.name = self.learner_name self.model.instances = self.data self.model.skl_model.orange_callback = None # remove unpicklable callback self.Outputs.model.send(self.model) def cancel(self): """ Cancel the current task (if any). """ if self._task is not None: self._task.cancel() assert self._task.future.done() # disconnect from the task self._task.done.disconnect(self._task_finished) self._task.progressChanged.disconnect(self.setProgressValue) self._task.deleteLater() self._task = None # pylint: disable=attribute-defined-outside-init self.progressBarFinished() self.setBlocking(False) def onDeleteWidget(self): self.cancel() super().onDeleteWidget() @classmethod def migrate_settings(cls, settings, version): if not version: alpha = settings.pop("alpha", None) if alpha is not None: settings["alpha_index"] = \ np.argmin(np.abs(np.array(cls.alphas) - alpha)) elif version < 2: settings["alpha_index"] = settings.get("alpha_index", 0) + 1
class OSK(QWidget): def __init__(self, rWidget=None): super(OSK, self).__init__() self.showFullScreen() self._rWidget = rWidget self.layout = QVBoxLayout(self) self.currentText = QLabel(self) self.currentText.setAlignment(Qt.AlignCenter) if rWidget is not None: self.currentText.setText(rWidget.text()) self.layout.addWidget(self.currentText) keyLayout = [['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-'], ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'], ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'], ['z', 'x', 'c', 'v', 'b', 'n', 'm']] for l in keyLayout: panel = QWidget(self) panel.layout = QHBoxLayout(panel) for key in l: button = OSKKey(key, self, parent=self) panel.layout.addWidget(button) self.layout.addWidget(panel) contolPanel = QWidget(self) contolPanel.layout = QHBoxLayout(contolPanel) self._shift = QPushButton('Shift', self) self._shift.setCheckable(True) self._shift.setFixedWidth(150) contolPanel.layout.addWidget(self._shift) spaceBar = OSKKey('space', self, parent=self) spaceBar.rKey = ' ' spaceBar.setFixedWidth(2 * self.window().geometry().width() / 3) contolPanel.layout.addWidget(spaceBar) bkspc = OSKKey('delete', self, parent=self) bkspc.rKey = '<<' contolPanel.layout.addWidget(bkspc) self.layout.addWidget(contolPanel) self.closeButton = QPushButton("OK", self) self.closeButton.clicked.connect(self.__closeButtonAction) self.layout.addWidget(self.closeButton) def onClick(self, key): if self.rWidget is not None: if key == '<<': self.rWidget.setText(self.rWidget.text()[:-1]) elif self._shift.isChecked(): self.rWidget.setText(self.rWidget.text() + key.upper()) self._shift.setChecked(False) else: self.rWidget.setText(self.rWidget.text() + key) self.currentText.setText(self.rWidget.text()) def __closeButtonAction(self): self.parent().hide() @property def rWidget(self): return self._rWidget @rWidget.setter def rWidget(self, value): if not isinstance(value, QWidget) and value is not None: raise TypeError( "Supplied return Widget is not of type QWidget: %s" % type(value).__name__) else: self._rWidget = value self.currentText.setText(value.text())
class PreviewBrowser(QWidget): """A Preview Browser for recent/premade scheme selection. """ # Emitted when the current previewed item changes currentIndexChanged = Signal(int) # Emitted when an item is double clicked in the preview list. activated = Signal(int) def __init__(self, *args): QWidget.__init__(self, *args) self.__model = None self.__currentIndex = -1 self.__template = DESCRIPTION_TEMPLATE self.__setupUi() def __setupUi(self): vlayout = QVBoxLayout() vlayout.setContentsMargins(0, 0, 0, 0) top_layout = QHBoxLayout() top_layout.setContentsMargins(12, 12, 12, 12) # Top row with full text description and a large preview # image. self.__label = QLabel(self, objectName="description-label", wordWrap=True, alignment=Qt.AlignTop | Qt.AlignLeft) self.__label.setWordWrap(True) self.__label.setFixedSize(220, PREVIEW_SIZE[1]) self.__image = QSvgWidget(self, objectName="preview-image") self.__image.setFixedSize(*PREVIEW_SIZE) self.__imageFrame = DropShadowFrame(self) self.__imageFrame.setWidget(self.__image) # Path text below the description and image path_layout = QHBoxLayout() path_layout.setContentsMargins(12, 0, 12, 0) path_label = QLabel("<b>{0!s}</b>".format(self.tr("Path:")), self, objectName="path-label") self.__path = TextLabel(self, objectName="path-text") path_layout.addWidget(path_label) path_layout.addWidget(self.__path) self.__selectAction = \ QAction(self.tr("Select"), self, objectName="select-action", ) top_layout.addWidget(self.__label, 1, alignment=Qt.AlignTop | Qt.AlignLeft) top_layout.addWidget(self.__image, 1, alignment=Qt.AlignTop | Qt.AlignRight) vlayout.addLayout(top_layout) vlayout.addLayout(path_layout) # An list view with small preview icons. self.__previewList = LinearIconView(objectName="preview-list-view") self.__previewList.doubleClicked.connect(self.__onDoubleClicked) vlayout.addWidget(self.__previewList) self.setLayout(vlayout) def setModel(self, model): """Set the item model for preview. """ if self.__model != model: if self.__model: s_model = self.__previewList.selectionModel() s_model.selectionChanged.disconnect(self.__onSelectionChanged) self.__model.dataChanged.disconnect(self.__onDataChanged) self.__model = model self.__previewList.setModel(model) if model: s_model = self.__previewList.selectionModel() s_model.selectionChanged.connect(self.__onSelectionChanged) self.__model.dataChanged.connect(self.__onDataChanged) if model and model.rowCount(): self.setCurrentIndex(0) def model(self): """Return the item model. """ return self.__model def setPreviewDelegate(self, delegate): """Set the delegate to render the preview images. """ raise NotImplementedError def setDescriptionTemplate(self, template): self.__template = template self.__update() def setCurrentIndex(self, index): """Set the selected preview item index. """ if self.__model is not None and self.__model.rowCount(): index = min(index, self.__model.rowCount() - 1) index = self.__model.index(index, 0) sel_model = self.__previewList.selectionModel() # This emits selectionChanged signal and triggers # __onSelectionChanged, currentIndex is updated there. sel_model.select(index, sel_model.ClearAndSelect) elif self.__currentIndex != -1: self.__currentIndex = -1 self.__update() self.currentIndexChanged.emit(-1) def currentIndex(self): """Return the current selected index. """ return self.__currentIndex def __onSelectionChanged(self, *args): """Selected item in the preview list has changed. Set the new description and large preview image. """ rows = self.__previewList.selectedIndexes() if rows: index = rows[0] self.__currentIndex = index.row() else: index = QModelIndex() self.__currentIndex = -1 self.__update() self.currentIndexChanged.emit(self.__currentIndex) def __onDataChanged(self, topleft, bottomRight): """Data changed, update the preview if current index in the changed range. """ if self.__currentIndex <= topleft.row() and \ self.__currentIndex >= bottomRight.row(): self.__update() def __onDoubleClicked(self, index): """Double click on an item in the preview item list. """ self.activated.emit(index.row()) def __update(self): """Update the current description. """ if self.__currentIndex != -1: index = self.model().index(self.__currentIndex, 0) else: index = QModelIndex() if not index.isValid(): description = "" name = "" path = "" svg = NO_PREVIEW_SVG else: description = str(index.data(Qt.WhatsThisRole)) if not description: description = "No description." description = escape(description) description = description.replace("\n", "<br/>") name = str(index.data(Qt.DisplayRole)) if not name: name = "Untitled" name = escape(name) path = str(index.data(Qt.StatusTipRole)) svg = str(index.data(previewmodel.ThumbnailSVGRole)) desc_text = self.__template.format(description=description, name=name) self.__label.setText(desc_text) self.__path.setText(path) if not svg: svg = NO_PREVIEW_SVG if svg: self.__image.load(QByteArray(svg.encode("utf-8")))