class _PeaksWorkspaceTableView(TableWorkspaceDisplayView): """Specialization of a table view to display peaks Designed specifically to be used by PeaksViewerView """ def __init__(self, *args, **kwargs): self._key_handler = kwargs.pop('key_handler') TableWorkspaceDisplayView.__init__(self, *args, **kwargs) self.source_model = self.model() self.proxy_model = None def keyPressEvent(self, event): """ Override base to call handler as part of event """ # bypass immediate base class to get standard table arrow key behaviour QTableView.keyPressEvent(self, event) self._key_handler._row_selected() def enable_sorting(self): """ Turn on column sorting """ self.setSortingEnabled(True) self.proxy_model = QSortFilterProxyModel() self.proxy_model.setSourceModel(self.source_model) self.setModel(self.proxy_model)
class DeviceView(QtWidgets.QTableView): def __init__(self, device, parent=None): super().__init__(parent=parent) self.proxy_model = QSortFilterProxyModel() self.proxy_model.setFilterKeyColumn(-1) self.proxy_model.setDynamicSortFilter(True) self.setModel(self.proxy_model) self.models = {} self._device = None # Set the property last self.device = device def clear(self): for model in self.models.values(): model.stop() self.models.clear() self._device = None @property def device(self): return self._device @device.setter def device(self, device): if device is self._device: return if self._device is not None: self.models[device].stop() self._device = device if device: try: model = self.models[device] except KeyError: model = PolledDeviceModel(device=device) self.models[device] = model model.start() self.proxy_model.setSourceModel(model)
class RedisTree(QMainWindow): addKey = Signal(object) currentChanged = Signal(object) def __init__(self, redis, parent=None): super(RedisTree, self).__init__(parent) self.load_ui() ui = self.ui self.redis = redis self.source_model = RedisKeyModel(redis) self.sort_filter_model = QSortFilterProxyModel() self.sort_filter_model.setFilterRole(KeyNameRole) self.sort_filter_model.setSourceModel(self.source_model) ui.tree.setModel(self.sort_filter_model) selection = ui.tree.selectionModel() selection.currentChanged.connect(self._on_current_changed) selection.selectionChanged.connect(self._on_selection_changed) # TODO: fix bug search of type "bl04:" still gives no result ui.filter_container.setVisible(False) add_menu = QMenu("Add") ui.add_string_action = add_menu.addAction("string") ui.add_list_action = add_menu.addAction("list") ui.add_set_action = add_menu.addAction("set") ui.add_hash_action = add_menu.addAction("hash") add_button = QToolButton() add_button.setMenu(add_menu) add_button.setPopupMode(QToolButton.InstantPopup) add_button.setIcon(QIcon.fromTheme("list-add")) ui.add_key_action = ui.db_toolbar.insertWidget(ui.remove_key_action, add_button) ui.add_string_action.triggered.connect( functools.partial(self._on_add_key, "string")) ui.add_list_action.triggered.connect( functools.partial(self._on_add_key, "list")) ui.add_set_action.triggered.connect( functools.partial(self._on_add_key, "set")) ui.add_hash_action.triggered.connect( functools.partial(self._on_add_key, "hash")) ui.update_db_action.triggered.connect(self._on_update_db) ui.flush_db_action.triggered.connect(self._on_flush_db) ui.remove_key_action.triggered.connect(self._on_remove_key) ui.touch_key_action.triggered.connect(self._on_touch_key) ui.persist_key_action.triggered.connect(self._on_persist_key) ui.copy_key_action.triggered.connect(self._on_copy_key) ui.filter_edit.textChanged.connect(self._on_filter_changed) def contextMenuEvent(self, event): pass def _get_selected_keys(self): selection = self.ui.tree.selectionModel() indexes = (self.sort_filter_model.mapToSource(i) for i in selection.selectedIndexes()) nodes = (self.source_model.data(i, NodeRole) for i in indexes) keys = tuple(node.key for node in nodes if node is not None and node.is_key()) return keys def _on_filter_changed(self, text): if not text.endswith("*"): text += "*" self.sort_filter_model.setFilterWildcard(text) def _on_current_changed(self, current, previous): current = self.sort_filter_model.mapToSource(current) node = self.source_model.data(current, Qt.UserRole) self.currentChanged.emit(node) def _on_selection_changed(self, selected, deselected): indexes = (self.sort_filter_model.mapToSource(i) for i in selected.indexes()) nodes = (self.source_model.data(i, NodeRole) for i in indexes) nodes = [node for node in nodes if node is not None] nodes_selected = bool(nodes) ui = self.ui ui.remove_key_action.setEnabled(nodes_selected) ui.touch_key_action.setEnabled(nodes_selected) ui.persist_key_action.setEnabled(nodes_selected) ui.copy_key_action.setEnabled(len(nodes) == 1) def _on_flush_db(self): result = QMessageBox.question( self, "Danger!", "This action will delete all data from the current database.\n" \ "Are you absolutely sure?") if result == QMessageBox.Yes: self.redis.flushdb() self.source_model.refresh() def _on_update_db(self): self.source_model.refresh() def _on_touch_key(self): keys = self._get_selected_keys() if keys: self.redis.touch(*keys) def _on_persist_key(self): keys = self._get_selected_keys() for key in keys: self.redis.persist(key) def _on_add_key(self, dtype): value = None if dtype == "string": value = "" elif dtype == "list": value = [] elif dtype == "set": value = set() elif dtype == "hash": value = {} item = Item(self.redis, "", dtype, -1, value) self.addKey.emit(item) def _on_remove_key(self): keys = self._get_selected_keys() if keys: self.redis.delete(*keys) self.ui.tree.clearSelection() def _on_copy_key(self): keys = self._get_selected_keys() assert len(keys) == 1 src = keys[0] dst, ok = QInputDialog.getText(self, f"Copy {src!r} to...", "New key") if ok: # redis.copy() only >= 6.2 #self.redis.copy(src, dst) self.redis[dst] = self.redis[src].value self.source_model.refresh()
class IMCWidget(QWidget): NAME = 'Imaging Mass Cytometry' FULL_NAME = f'napari-imc: {NAME}' AREA = 'right' ALLOWED_AREAS = ['left', 'right'] def __init__(self, napari_viewer: Viewer, parent: Optional[QWidget] = None): super(IMCWidget, self).__init__(parent) self._controller = IMCController(napari_viewer, self) self._imc_file_tree_model = IMCFileTreeModel(self._controller, parent=self) self._imc_file_tree_view = IMCFileTreeView(parent=self) self._imc_file_tree_view.setModel(self._imc_file_tree_model) self._channel_table_model = ChannelTableModel(self._controller, parent=self) self._channel_table_proxy_model = QSortFilterProxyModel(self) self._channel_table_proxy_model.setSourceModel( self._channel_table_model) self._channel_table_proxy_model.sort( self._channel_table_model.LABEL_COLUMN) self._channel_table_view = ChannelTableView(parent=self) self._channel_table_view.setModel(self._channel_table_proxy_model) self._channel_controls_widget = ChannelControlsWidget(self._controller, parent=self) self._channel_controls_container = QStackedWidget(self) self._channel_controls_container.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) self._channel_controls_container.addWidget(QWidget(self)) self._channel_controls_container.addWidget( self._channel_controls_widget) layout = QVBoxLayout(self) splitter = QSplitter(Qt.Vertical, self) splitter.addWidget(self._imc_file_tree_view) channel_panel = QWidget(self) channel_panel_layout = QVBoxLayout() channel_panel_layout.addWidget(self._channel_table_view) channel_panel_layout.addWidget(self._channel_controls_container) channel_panel.setLayout(channel_panel_layout) splitter.addWidget(channel_panel) layout.addWidget(splitter) self.setLayout(layout) # noinspection PyUnusedLocal # noinspection PyUnresolvedReferences @self._imc_file_tree_model.dataChanged.connect def on_imc_file_tree_model_data_changed(top_left: QModelIndex, bottom_right: QModelIndex, roles: Optional[int] = None): item = top_left.internalPointer() if isinstance(item, IMCFilePanoramaModel): if item.is_shown: self._controller.hide_imc_file_panorama(item) else: self._controller.show_imc_file_panorama(item) elif isinstance(item, IMCFileAcquisitionModel): if item.is_loaded: self._controller.unload_imc_file_acquisition(item) else: self._controller.load_imc_file_acquisition(item) # noinspection PyUnresolvedReferences @self._imc_file_tree_view.events.imc_file_closed.connect def on_imc_file_tree_view_imc_file_closed(imc_file: IMCFileModel): self._controller.close_imc_file(imc_file) # noinspection PyUnusedLocal # noinspection PyUnresolvedReferences @self._channel_table_model.dataChanged.connect def on_channel_table_model_data_changed(top_left: QModelIndex, bottom_right: QModelIndex, roles: Optional[int] = None): channel = self._controller.channels[top_left.row()] if channel.is_shown: self._controller.hide_channel(channel) else: self._controller.show_channel(channel) channel_table_selection_model: QItemSelectionModel = self._channel_table_view.selectionModel( ) # noinspection PyUnusedLocal # noinspection PyUnresolvedReferences @channel_table_selection_model.selectionChanged.connect def on_channel_table_view_selection_changed( selected: QItemSelection, deselected: QItemSelection): selected_channels = [] for index in self._channel_table_view.selectedIndexes(): index = self._channel_table_proxy_model.mapToSource(index) channel = self._controller.channels[index.row()] selected_channels.append(channel) self._controller.selected_channels = selected_channels def select_channel(self, channel_index: int): top_left = self._channel_table_model.index(channel_index, 0) top_left = self._channel_table_proxy_model.mapFromSource(top_left) bottom_right = self._channel_table_model.index( channel_index, self._channel_table_model.columnCount() - 1) bottom_right = self._channel_table_proxy_model.mapFromSource( bottom_right) selection_model: QItemSelectionModel = self._channel_table_view.selectionModel( ) selection_model.clearSelection( ) # first clear the selection, to make sure the channel controls refresh selection_model.select(QItemSelection(top_left, bottom_right), QItemSelectionModel.Select) def refresh_channel_controls_widget(self): self._channel_controls_widget.refresh() if len(self._controller.selected_channels) > 0: self._channel_controls_container.setCurrentIndex(1) else: self._channel_controls_container.setCurrentIndex(0) @property def controller(self): return self._controller @property def imc_file_tree_model(self) -> IMCFileTreeModel: return self._imc_file_tree_model @property def channel_table_model(self) -> ChannelTableModel: return self._channel_table_model
class ToolTable(QTableView): toolSelected = Signal(int) def __init__(self, parent=None): super(ToolTable, self).__init__(parent) self.clicked.connect(self.onClick) self.tool_model = ToolModel(self) self.item_delegate = ItemDelegate(columns=self.tool_model._columns) self.setItemDelegate(self.item_delegate) self.proxy_model = QSortFilterProxyModel() self.proxy_model.setFilterKeyColumn(0) self.proxy_model.setSourceModel(self.tool_model) self.setModel(self.proxy_model) # Properties self._columns = self.tool_model._columns self._confirm_actions = False self._current_tool_color = QColor('sage') self._current_tool_bg = None # Appearance/Behaviour settings self.setSortingEnabled(True) self.verticalHeader().hide() self.setAlternatingRowColors(True) self.setSelectionBehavior(QTableView.SelectRows) self.setSelectionMode(QTableView.SingleSelection) self.horizontalHeader().setStretchLastSection(True) self.horizontalHeader().setSortIndicator(0, Qt.AscendingOrder) @Slot() def saveToolTable(self): if not self.confirmAction("Do you want to save changes and\n" "load tool table into LinuxCNC?"): return self.tool_model.saveToolTable() @Slot() def loadToolTable(self): if not self.confirmAction("Do you want to re-load the tool table?\n" "All unsaved changes will be lost."): return self.tool_model.loadToolTable() @Slot() def deleteSelectedTool(self): """Delete the currently selected item""" current_row = self.selectedRow() if current_row == -1: # no row selected return tdata = self.tool_model.toolDataFromRow(current_row) tnum = tdata['T'] # should not delete tool if currently loaded in spindle. Warn user if tnum == self.tool_model.stat.tool_in_spindle: box = QMessageBox( QMessageBox.Warning, "Can't delete current tool!", "Tool #{} is currently loaded in the spindle.\n" "Please remove tool from spindle and try again.".format(tnum), QMessageBox.Ok, parent=self) box.show() return False if not self.confirmAction( 'Are you sure you want to delete T{tdata[T]}?\n' '"{tdata[R]}"'.format(tdata=tdata)): return self.tool_model.removeTool(current_row) @Slot() def selectPrevious(self): """Select the previous item in the view.""" self.selectRow(self.selectedRow() - 1) return True @Slot() def selectNext(self): """Select the next item in the view.""" self.selectRow(self.selectedRow() + 1) return True @Slot() def clearToolTable(self, confirm=True): """Remove all items from the model""" if confirm: if not self.confirmAction( "Do you want to delete the whole tool table?"): return self.tool_model.clearToolTable() @Slot() def addTool(self): """Appends a new item to the model""" self.tool_model.addTool() self.selectRow(self.tool_model.rowCount() - 1) @Slot() def loadSelectedTool(self): """Loads the currently selected tool""" # see: https://forum.linuxcnc.org/41-guis/36042?start=50#151820 current_row = self.selectedRow() if current_row == -1: # no row selected return tnum = self.tool_model.toolDataFromRow(current_row)['T'] issue_mdi("T%s M6" % tnum) def selectedRow(self): """Returns the row number of the currently selected row, or 0""" tool_no = self.selectionModel().currentIndex().row() return tool_no def onClick(self, index): row = index.row() tnum = self.tool_model.toolDataFromRow(row)['T'] self.toolSelected.emit(tnum) def confirmAction(self, message): if not self._confirm_actions: return True box = QMessageBox.question(self, 'Confirm Action', message, QMessageBox.Yes, QMessageBox.No) if box == QMessageBox.Yes: return True else: return False @Property(bool) def confirmActions(self): return self._confirm_actions @confirmActions.setter def confirmActions(self, confirm): self._confirm_actions = confirm @Property(QColor) def currentToolColor(self): return self.tool_model.current_tool_color @currentToolColor.setter def currentToolColor(self, color): self.tool_model.current_tool_color = color @Property(QColor) def currentToolBackground(self): return self.tool_model.current_tool_bg or QColor() @currentToolBackground.setter def currentToolBackground(self, color): self.tool_model.current_tool_bg = color def insertToolAbove(self): # it does not make sense to insert tools, since the numbering # of all the other tools would have to change. self.addTool() raise DeprecationWarning("insertToolAbove() will be removed in " "the future, use addTool() instead") def insertToolBelow(self): self.addTool() raise DeprecationWarning("insertToolBelow() will be removed in " "the future, use addTool() instead")
class DetectorView(QtWidgets.QTableView): def __init__(self, prefix, parent=None): super().__init__(parent=parent) self._prefix = None self._pvlist = None self._pvlist_key = None self.models = {} self.proxy_model = QSortFilterProxyModel() self.proxy_model.setDynamicSortFilter(True) self.proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) self.setModel(self.proxy_model) self.setSortingEnabled(True) # Set the property last self.prefix = prefix @property def model(self): if self._pvlist_key: return self.models.get(self._pvlist_key, None) return self.models.get(self._prefix, None) @property def prefix(self): return self._prefix @prefix.setter def prefix(self, prefix): if prefix == self._prefix: return logger.info('New prefix: %s', prefix) self._prefix = prefix self._pvlist = None self._pvlist_key = None if prefix: try: model = self.models[prefix] except KeyError: model = DetectorFromPrefixModel(prefix=prefix) self.models[prefix] = model self.proxy_model.setSourceModel(model) header = self.horizontalHeader() for col in range(3): header.setSectionResizeMode( col, QtWidgets.QHeaderView.ResizeToContents) @property def pvlist(self): return self.model.pvlist @pvlist.setter def pvlist(self, pvlist): pvlist = list(sorted(set(pvlist))) unique_id = '/'.join(pvlist) pvlist_key = f'pvlist_{hash(unique_id)}' if pvlist_key == self._pvlist_key: return self._pvlist = pvlist self._pvlist_key = pvlist_key try: model = self.models[self._pvlist_key] except KeyError: model = DetectorFromPVListModel(pvlist=self._pvlist) self.models[self._pvlist_key] = model logger.info( 'New PV list (%d PVs) - searching for %d cam PVs, %d plugin PVs', len(pvlist), len(model.cams.pvs), len(model.plugins.pvs)) self._prefix = model.prefix self.proxy_model.setSourceModel(model) header = self.horizontalHeader() for col in range(3): header.setSectionResizeMode(col, QtWidgets.QHeaderView.ResizeToContents)
class NotificationsWidget(QWidget, VCPWidget): notificationsCleared = Signal(object) def __init__(self, parent=None): super(NotificationsWidget, self).__init__(parent) self.notification_channel = getPlugin("notifications") self.main_layout = QVBoxLayout() self.button_layout = QHBoxLayout() self.all_button = QPushButton() self.info_button = QPushButton() self.warn_button = QPushButton() self.error_button = QPushButton() self.debug_button = QPushButton() self.clear_button = QPushButton() self.all_button.setText("all") self.info_button.setText("info") self.warn_button.setText("warn") self.error_button.setText("error") self.debug_button.setText("debug") self.clear_button.setText("clear") self.all_button.setCheckable(True) self.info_button.setCheckable(True) self.warn_button.setCheckable(True) self.error_button.setCheckable(True) self.debug_button.setCheckable(True) self.all_button.setChecked(True) self.info_button.setChecked(False) self.warn_button.setChecked(False) self.error_button.setChecked(False) self.debug_button.setChecked(False) self.clear_button.clicked.connect(self.clear_all_notifications) self.button_layout.addWidget(self.all_button) self.button_layout.addWidget(self.info_button) self.button_layout.addWidget(self.warn_button) self.button_layout.addWidget(self.error_button) self.button_layout.addWidget(self.debug_button) self.button_layout.addWidget(self.clear_button) self.notification_name = QLabel() self.notification_name.setAlignment(Qt.AlignCenter) self.notification_name.setText("All Notifications") self.all_notification_view = QListView() self.all_notification_model = QStandardItemModel( self.all_notification_view) self.all_notification_model_proxy = QSortFilterProxyModel( self.all_notification_view) self.all_notification_model_proxy.setSourceModel( self.all_notification_model) # self.all_notification_view.setModel(self.all_notification_model) self.all_notification_view.setModel(self.all_notification_model_proxy) self.all_notifications = list() self.main_layout.addWidget(self.notification_name) self.main_layout.addWidget(self.all_notification_view) self.main_layout.addLayout(self.button_layout) self.setLayout(self.main_layout) self.notification_channel.info_message.notify(self.on_info_message) self.notification_channel.warn_message.notify(self.on_warn_message) self.notification_channel.error_message.notify(self.on_error_message) self.notification_channel.debug_message.notify(self.on_debug_message) self.all_button.clicked.connect(self.show_all_notifications) self.info_button.clicked.connect(self.show_info_notifications) self.warn_button.clicked.connect(self.show_warn_notifications) self.error_button.clicked.connect(self.show_error_notifications) self.debug_button.clicked.connect(self.show_debug_notifications) @staticmethod def _get_time(): timestamp = time() dt_object = datetime.fromtimestamp(timestamp) return dt_object.strftime("%d / %b / %Y - %H:%M:%S") @staticmethod def _strip_new_line(message): return message.replace('\n', ' ').replace('\r', '').replace( ' ', ' ').replace(' ', ' ').replace(' ', ' ') def on_info_message(self, message): msg = 'INFO - occurred at: [ {} ]\n{}'.format( NotificationsWidget._get_time(), NotificationsWidget._strip_new_line(message)) notification_item = QStandardItem() notification_item.setText(msg) notification_item.setIcon(QIcon.fromTheme('dialog-information')) notification_item.setEditable(False) self.all_notification_model.appendRow(notification_item) def on_warn_message(self, message): msg = 'WARNING - occurred at: [ {} ]\n{}'.format( NotificationsWidget._get_time(), NotificationsWidget._strip_new_line(message)) notification_item = QStandardItem() notification_item.setText(msg) notification_item.setIcon(QIcon.fromTheme('dialog-warning')) notification_item.setEditable(False) self.all_notification_model.appendRow(notification_item) def on_error_message(self, message): msg = 'ERROR - occurred at: [ {} ]\n{}'.format( NotificationsWidget._get_time(), NotificationsWidget._strip_new_line(message)) LOG.debug('-----on_error_message called: {}'.format(message)) notification_item = QStandardItem() notification_item.setText(msg) notification_item.setIcon(QIcon.fromTheme('dialog-error')) notification_item.setEditable(False) self.all_notification_model.appendRow(notification_item) def on_debug_message(self, message): msg = 'DEBUG - occurred at: [ {} ]\n{}'.format( NotificationsWidget._get_time(), NotificationsWidget._strip_new_line(message)) notification_item = QStandardItem() notification_item.setText(msg) notification_item.setIcon(QIcon.fromTheme('dialog-question')) notification_item.setEditable(False) self.all_notification_model.appendRow(notification_item) def show_all_notifications(self): self.all_button.setChecked(True) self.info_button.setChecked(False) self.warn_button.setChecked(False) self.error_button.setChecked(False) self.debug_button.setChecked(False) self.notification_name.setText("All Notifications") self.all_notification_model_proxy.setFilterRegExp(None) def show_info_notifications(self): self.all_button.setChecked(False) self.info_button.setChecked(True) self.warn_button.setChecked(False) self.error_button.setChecked(False) self.debug_button.setChecked(False) self.notification_name.setText("Information Notifications") self.all_notification_model_proxy.setFilterRegExp( QRegExp("INFO", Qt.CaseSensitive, QRegExp.FixedString)) def show_warn_notifications(self): self.all_button.setChecked(False) self.info_button.setChecked(False) self.warn_button.setChecked(True) self.error_button.setChecked(False) self.debug_button.setChecked(False) self.notification_name.setText("Warning Notifications") self.all_notification_model_proxy.setFilterRegExp( QRegExp("WANRNING", Qt.CaseSensitive, QRegExp.FixedString)) def show_error_notifications(self): self.all_button.setChecked(False) self.info_button.setChecked(False) self.warn_button.setChecked(False) self.error_button.setChecked(True) self.debug_button.setChecked(False) self.notification_name.setText("Error Notifications") self.all_notification_model_proxy.setFilterRegExp( QRegExp("ERROR", Qt.CaseInsensitive, QRegExp.FixedString)) def show_debug_notifications(self): self.all_button.setChecked(False) self.info_button.setChecked(False) self.warn_button.setChecked(False) self.error_button.setChecked(False) self.debug_button.setChecked(True) self.notification_name.setText("Debug Notifications") self.all_notification_model_proxy.setFilterRegExp( QRegExp("DEBUG", Qt.CaseSensitive, QRegExp.FixedString)) def clear_all_notifications(self): self.all_notification_model.clear() self.notificationsCleared.emit(self)
def __init__(self, parent=None, *args): QTableView.__init__(self, parent, *args) self.setMinimumSize(800, 600) # set the table model tm = MyTableModel(self) # set the proxy model pm = QSortFilterProxyModel(self) pm.setSourceModel(tm) self.setModel(pm) self.frozenTableView = QTableView(self) self.frozenTableView.setModel(pm) self.frozenTableView.verticalHeader().hide() self.frozenTableView.setFocusPolicy(Qt.NoFocus) # self.frozenTableView.horizontalHeader().setSectionResizeMode(QHeaderView.Fixed) self.frozenTableView.setStyleSheet( '''border: none; background-color: #CCC''') self.frozenTableView.setSelectionModel( QAbstractItemView.selectionModel(self)) self.frozenTableView.setHorizontalScrollBarPolicy( Qt.ScrollBarAlwaysOff) self.frozenTableView.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.viewport().stackUnder(self.frozenTableView) self.setEditTriggers(QAbstractItemView.SelectedClicked) # hide grid self.setShowGrid(False) self.setStyleSheet('font: 10pt "Courier New"') hh = self.horizontalHeader() hh.setDefaultAlignment(Qt.AlignCenter) hh.setStretchLastSection(True) # self.resizeColumnsToContents() ncol = tm.columnCount(self) for col in range(ncol): if col == 0: self.horizontalHeader().resizeSection(col, 60) # self.horizontalHeader().setSectionResizeMode(col, QHeaderView.Fixed) self.frozenTableView.setColumnWidth(col, self.columnWidth(col)) elif col == 1: self.horizontalHeader().resizeSection(col, 150) # self.horizontalHeader().setSectionResizeMode(col, QHeaderView.Fixed) self.frozenTableView.setColumnWidth(col, self.columnWidth(col)) else: self.horizontalHeader().resizeSection(col, 100) self.frozenTableView.setColumnHidden(col, True) self.frozenTableView.setSortingEnabled(True) self.frozenTableView.sortByColumn(0, Qt.AscendingOrder) self.setAlternatingRowColors(True) vh = self.verticalHeader() vh.setDefaultSectionSize(25) vh.setDefaultAlignment(Qt.AlignCenter) vh.setVisible(True) self.frozenTableView.verticalHeader().setDefaultSectionSize( vh.defaultSectionSize()) # nrows = tm.rowCount(self) # for row in range(nrows): # self.setRowHeight(row, 25) self.frozenTableView.show() self.updateFrozenTableGeometry() self.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) self.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) self.frozenTableView.setVerticalScrollMode( QAbstractItemView.ScrollPerPixel) # connect the headers and scrollbars of both tableviews together self.horizontalHeader().sectionResized.connect(self.updateSectionWidth) self.verticalHeader().sectionResized.connect(self.updateSectionHeight) self.frozenTableView.verticalScrollBar().valueChanged.connect( self.verticalScrollBar().setValue) self.verticalScrollBar().valueChanged.connect( self.frozenTableView.verticalScrollBar().setValue)
class LicenseManagerDialog(DialogBase): """License Manager main dialog.""" CONTACT_LINK = 'https://support.continuum.io/' # TODO: Centralize this? # Url, Sender sig_url_clicked = Signal(object, object) def __init__(self, parent=None): """License Manager main dialog.""" super(LicenseManagerDialog, self).__init__(parent=parent) self.api = AnacondaAPI() # Widgets self.message_box = None # For testing self.button_add = ButtonPrimary('Add license') self.button_ok = ButtonNormal('Close') self.button_remove = ButtonNormal('Remove license') self.button_contact = ButtonLink('Please contact us.') self.label_info = LabelBase('Manage your Continuum Analytics ' 'license keys.') self.label_contact = LabelBase('Got a problem with your license? ') self.proxy_model = QSortFilterProxyModel(parent=self) self.model = LicenseModel(parent=self) self.table = LicenseTableView(parent=self) self.delegate = BackgroundDelegate(self.table) # Widget setup self.proxy_model.setSourceModel(self.model) self.table.setItemDelegate(self.delegate) self.table.setModel(self.proxy_model) self.setWindowTitle('License Manager') # Layouts layout_buttons = QHBoxLayout() layout_buttons.addWidget(self.label_info) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addStretch() layout_buttons.addWidget(self.button_add) layout_buttons.addWidget(SpacerHorizontal()) layout_buttons.addWidget(self.button_remove) layout_buttons_bottom = QHBoxLayout() layout_buttons_bottom.addWidget(self.label_contact) layout_buttons_bottom.addWidget(self.button_contact) layout_buttons_bottom.addStretch() layout_buttons_bottom.addWidget(self.button_ok) layout = QVBoxLayout() layout.addLayout(layout_buttons) layout.addWidget(SpacerVertical()) layout.addWidget(self.table) layout.addWidget(SpacerVertical()) layout.addWidget(SpacerVertical()) layout.addLayout(layout_buttons_bottom) self.setLayout(layout) # Signals self.button_add.clicked.connect(lambda: self.add_license()) self.button_remove.clicked.connect(self.remove_license) self.button_ok.clicked.connect(self.accept) self.button_contact.clicked.connect( lambda v=None: self.sig_url_clicked.emit(self.CONTACT_LINK, 'License Manager')) self.table.sig_dropped.connect(self.handle_drop) # Setup self.button_add.setFocus() self.load_licenses() def handle_drop(self, links): """Handle a drag and drop event.""" self.api.add_license(links) self.load_licenses() def _hide_columns(self): """Hide columns.""" for key, val in COL_MAP.items(): if val in HIDDEN_COLUMNS: self.table.setColumnHidden(key, True) def add_license(self, v=None, path=None): """Add license file.""" if path is None: filename, selected_filter = getopenfilename( self, 'Select license file', filters='License files (*.txt)', basedir=get_home_dir(), ) if filename: paths = [filename] else: paths = [] else: paths = [path] valid_licenses, invalid_licenses = self.api.add_license(paths) for path in invalid_licenses: text = ('File: <b>"{0}"</b>' '<br>is not a valid license file.').format(path) self.message_box = MessageBoxInformation( text=text, title="Invalid license file") self.message_box.exec_() if valid_licenses: self.load_licenses() def remove_license(self, row=None): """Remove license from file.""" if row is None: index = self.table.currentIndex() else: index = self.proxy_model.index(row, 0) model_index = self.proxy_model.mapToSource(index) row_data = self.model.row(model_index.row()) if row_data: text = ('Do you want to remove license for product:<br><br>' '<b>{product}</b> ({issued} - {end_date})') text = text.format(product=row_data.get('product'), end_date=row_data.get('end_date'), issued=row_data.get('issued')) self.message_box = MessageBoxRemove(title='Remove license', text=text) if self.message_box.exec_(): self.api.remove_license(row_data) self.load_licenses() def load_licenses(self): """Load license files.""" res = self.api.load_licenses() self.model.load_licenses(res) self.proxy_model.setSourceModel(self.model) self.table.resizeColumnsToContents() self._hide_columns() self.update_status() def count(self): """Return the number of items in the table.""" return self.table.model().rowCount() def update_status(self): """Update visible and enabled status for widgets based on actions.""" self.button_remove.setEnabled(bool(self.count()))
class NotificationWidget(QWidget, VCPWidget): def __init__(self, parent=None): super(NotificationWidget, self).__init__(parent) self.notification_channel = getPlugin("notifications") self.main_layout = QVBoxLayout() self.button_layout = QHBoxLayout() self.all_button = QPushButton() self.info_button = QPushButton() self.warn_button = QPushButton() self.error_button = QPushButton() self.debug_button = QPushButton() self.clear_button = QPushButton() self.all_button.setText("all") self.info_button.setText("info") self.warn_button.setText("warn") self.error_button.setText("error") self.debug_button.setText("debug") self.clear_button.setText("clear") self.all_button.setCheckable(True) self.info_button.setCheckable(True) self.warn_button.setCheckable(True) self.error_button.setCheckable(True) self.debug_button.setCheckable(True) self.all_button.setChecked(True) self.info_button.setChecked(False) self.warn_button.setChecked(False) self.error_button.setChecked(False) self.debug_button.setChecked(False) self.clear_button.clicked.connect(self.clear_all_notifications) self.button_layout.addWidget(self.all_button) self.button_layout.addWidget(self.info_button) self.button_layout.addWidget(self.warn_button) self.button_layout.addWidget(self.error_button) self.button_layout.addWidget(self.debug_button) self.button_layout.addWidget(self.clear_button) self.notification_name = QLabel() self.notification_name.setAlignment(Qt.AlignCenter) self.notification_name.setText("All Notifications") self.all_notification_view = QListView() self.all_notification_model = QStandardItemModel( self.all_notification_view) self.all_notification_model_proxy = QSortFilterProxyModel( self.all_notification_view) self.all_notification_model_proxy.setSourceModel( self.all_notification_model) # self.all_notification_view.setModel(self.all_notification_model) self.all_notification_view.setModel(self.all_notification_model_proxy) self.all_notifications = list() self.main_layout.addWidget(self.notification_name) self.main_layout.addWidget(self.all_notification_view) self.main_layout.addLayout(self.button_layout) self.setLayout(self.main_layout) self.notification_channel.info_message.notify(self.on_info_message) self.notification_channel.warn_message.notify(self.on_warn_message) self.notification_channel.error_message.notify(self.on_error_message) self.notification_channel.debug_message.notify(self.on_debug_message) self.all_button.clicked.connect(self.show_all_notifications) self.info_button.clicked.connect(self.show_info_notifications) self.warn_button.clicked.connect(self.show_warn_notifications) self.error_button.clicked.connect(self.show_error_notifications) self.debug_button.clicked.connect(self.show_debug_notifications) def on_info_message(self, message): timestamp = time() dt_object = datetime.fromtimestamp(timestamp) current_time = str(dt_object) msg = 'INFO:\nTIME {}\n {}'.format(current_time, message) notification_item = QStandardItem() notification_item.setText(msg) notification_item.setIcon(QIcon.fromTheme('dialog-information')) notification_item.setEditable(False) self.all_notification_model.appendRow(notification_item) def on_warn_message(self, message): timestamp = time() dt_object = datetime.fromtimestamp(timestamp) current_time = str(dt_object) msg = 'WARNING:\nTIME {}\n {}'.format(current_time, message) notification_item = QStandardItem() notification_item.setText(msg) notification_item.setIcon(QIcon.fromTheme('dialog-warning')) notification_item.setEditable(False) self.all_notification_model.appendRow(notification_item) def on_error_message(self, message): timestamp = time() dt_object = datetime.fromtimestamp(timestamp) current_time = str(dt_object) msg = 'ERROR:\nTIME {}\n {}'.format(current_time, message) notification_item = QStandardItem() notification_item.setText(msg) notification_item.setIcon(QIcon.fromTheme('dialog-error')) notification_item.setEditable(False) self.all_notification_model.appendRow(notification_item) def on_debug_message(self, message): timestamp = time() dt_object = datetime.fromtimestamp(timestamp) current_time = str(dt_object) msg = 'DEBUG\nTIME {}\n {}'.format(current_time, message) notification_item = QStandardItem() notification_item.setText(msg) notification_item.setIcon(QIcon.fromTheme('dialog-question')) notification_item.setEditable(False) self.all_notification_model.appendRow(notification_item) def show_all_notifications(self): self.all_button.setChecked(True) self.info_button.setChecked(False) self.warn_button.setChecked(False) self.error_button.setChecked(False) self.debug_button.setChecked(False) self.notification_name.setText("All Notifications") self.all_notification_model_proxy.setFilterRegExp(None) def show_info_notifications(self): self.all_button.setChecked(False) self.info_button.setChecked(True) self.warn_button.setChecked(False) self.error_button.setChecked(False) self.debug_button.setChecked(False) self.notification_name.setText("Information Notifications") self.all_notification_model_proxy.setFilterRegExp( QRegExp("INFO", Qt.CaseSensitive, QRegExp.FixedString)) def show_warn_notifications(self): self.all_button.setChecked(False) self.info_button.setChecked(False) self.warn_button.setChecked(True) self.error_button.setChecked(False) self.debug_button.setChecked(False) self.notification_name.setText("Warning Notifications") self.all_notification_model_proxy.setFilterRegExp( QRegExp("WANRNING", Qt.CaseSensitive, QRegExp.FixedString)) def show_error_notifications(self): self.all_button.setChecked(False) self.info_button.setChecked(False) self.warn_button.setChecked(False) self.error_button.setChecked(True) self.debug_button.setChecked(False) self.notification_name.setText("Error Notifications") self.all_notification_model_proxy.setFilterRegExp( QRegExp("ERROR", Qt.CaseInsensitive, QRegExp.FixedString)) def show_debug_notifications(self): self.all_button.setChecked(False) self.info_button.setChecked(False) self.warn_button.setChecked(False) self.error_button.setChecked(False) self.debug_button.setChecked(True) self.notification_name.setText("Debug Notifications") self.all_notification_model_proxy.setFilterRegExp( QRegExp("DEBUG", Qt.CaseSensitive, QRegExp.FixedString)) def clear_all_notifications(self): self.all_notification_model.clear()
class OffsetTable(QTableView): def __init__(self, parent=None): super(OffsetTable, self).__init__(parent) self.setEnabled(False) self.offset_model = OffsetModel(self) # Properties self._columns = self.offset_model._columns self._confirm_actions = False self._current_row_color = QColor('sage') self.proxy_model = QSortFilterProxyModel() self.proxy_model.setFilterKeyColumn(0) self.proxy_model.setSourceModel(self.offset_model) self.item_delegate = ItemDelegate(columns=self._columns) self.setItemDelegate(self.item_delegate) self.setModel(self.proxy_model) # Appearance/Behaviour settings self.setSortingEnabled(False) self.setAlternatingRowColors(True) self.setSelectionBehavior(QTableView.SelectRows) self.setSelectionMode(QTableView.SingleSelection) self.horizontalHeader().setStretchLastSection(False) self.horizontalHeader().setSortIndicator(0, Qt.AscendingOrder) STATUS.all_axes_homed.notify(self.handle_home_signal) def handle_home_signal(self, all_axes): if all_axes: self.setEnabled(True) else: self.setEnabled(False) @Slot() def saveOffsetTable(self): if self.isEnabled(): if not self.confirmAction("Do you want to save changes and\n" "load offset table into LinuxCNC?"): return self.offset_model.saveOffsetTable() @Slot() def loadOffsetTable(self): if not self.confirmAction("Do you want to re-load the offset table?\n" "All unsaved changes will be lost."): return self.offset_model.loadOffsetTable() @Slot() def deleteSelectedOffset(self): """Delete the currently selected item""" current_row = self.selectedRow() if current_row == -1: # no row selected return if not self.confirmAction("Are you sure you want to delete offset {}?".format(current_row)): return self.offset_model.clearRow(current_row) # @Slot() # def selectPrevious(self): # """Select the previous item in the view.""" # self.selectRow(self.selectedRow() - 1) # return True # @Slot() # def selectNext(self): # """Select the next item in the view.""" # self.selectRow(self.selectedRow() + 1) # return True @Slot() def clearOffsetTable(self, confirm=True): """Remove all items from the model""" if confirm: if not self.confirmAction("Do you want to delete the whole offsets table?"): return self.offset_model.clearRows() def selectedRow(self): """Returns the row number of the currently selected row, or 0""" return self.selectionModel().currentIndex().row() def confirmAction(self, message): if not self._confirm_actions: return True box = QMessageBox.question(self, 'Confirm Action', message, QMessageBox.Yes, QMessageBox.No) if box == QMessageBox.Yes: return True else: return False @Property(str) def displayColumns(self): return "".join(self._columns) @displayColumns.setter def displayColumns(self, columns): self._columns = [col for col in columns.upper() if col in 'XYZABCUVWR'] self.offset_model.setColumns(self._columns) self.itemDelegate().setColumns(self._columns) @Property(bool) def confirmActions(self): return self._confirm_actions @confirmActions.setter def confirmActions(self, confirm): self._confirm_actions = confirm @Property(QColor) def currentRowColor(self): return self.offset_model.current_row_color @currentRowColor.setter def currentRowColor(self, color): self.offset_model.current_row_color = color
class ToolTable(QTableView): def __init__(self, parent=None): super(ToolTable, self).__init__(parent) self.tool_model = ToolModel(self) self.item_delegate = ItemDelegate(columns=self.tool_model._columns) self.setItemDelegate(self.item_delegate) self.proxy_model = QSortFilterProxyModel() self.proxy_model.setFilterKeyColumn(0) self.proxy_model.setSourceModel(self.tool_model) self.setModel(self.proxy_model) # Properties self._columns = self.tool_model._columns self._confirm_actions = False self._current_tool_color = QColor('sage') # Appearance/Behaviour settings self.setSortingEnabled(True) self.verticalHeader().hide() self.setAlternatingRowColors(True) self.setSelectionBehavior(QTableView.SelectRows) self.setSelectionMode(QTableView.SingleSelection) self.horizontalHeader().setStretchLastSection(True) self.horizontalHeader().setSortIndicator(0, Qt.AscendingOrder) @Slot() def saveToolTable(self): if not self.confirmAction("Do you want to save changes and\n" "load tool table into LinuxCNC?"): return self.tool_model.saveToolTable() @Slot() def loadToolTable(self): if not self.confirmAction("Do you want to re-load the tool table?\n" "All unsaved changes will be lost."): return self.tool_model.loadToolTable() @Slot() def deleteSelectedTool(self): """Delete the currently selected item""" current_row = self.selectedRow() if current_row == -1: # no row selected return tdata = self.tool_model.toolDataFromRow(current_row) if not self.confirmAction( "Are you sure you want to delete T{tdata[T]}?\n" "{tdata[R]}".format(tdata=tdata)): return self.tool_model.removeTool(current_row) @Slot() def selectPrevious(self): """Select the previous item in the view.""" self.selectRow(self.selectedRow() - 1) return True @Slot() def selectNext(self): """Select the next item in the view.""" self.selectRow(self.selectedRow() + 1) return True @Slot() def clearToolTable(self, confirm=True): """Remove all items from the model""" if confirm: if not self.confirmAction( "Do you want to delete the whole tool table?"): return self.tool_model.clearToolTable() @Slot() def addTool(self): """Appends a new item to the model""" self.tool_model.addTool() self.selectRow(self.tool_model.rowCount() - 1) def selectedRow(self): """Returns the row number of the currently selected row, or 0""" return self.selectionModel().currentIndex().row() def confirmAction(self, message): if not self._confirm_actions: return True box = QMessageBox.question(self, 'Confirm Action', message, QMessageBox.Yes, QMessageBox.No) if box == QMessageBox.Yes: return True else: return False @Property(str) def displayColumns(self): return "".join(self._columns) @displayColumns.setter def displayColumns(self, columns): self._columns = [ col for col in columns.upper() if col in 'TPXYZABCUVWDIJQR' ] self.tool_model.setColumns(self._columns) self.itemDelegate().setColumns(self._columns) @Property(bool) def confirmActions(self): return self._confirm_actions @confirmActions.setter def confirmActions(self, confirm): self._confirm_actions = confirm @Property(QColor) def currentToolColor(self): return self.tool_model.current_tool_color @currentToolColor.setter def currentToolColor(self, color): self.tool_model.current_tool_color = color def insertToolAbove(self): # it does not make sense to insert tools, since the numbering # of all the other tools would have to change. self.addTool() raise DeprecationWarning("insertToolAbove() will be removed in " "the future, use addTool() instead") def insertToolBelow(self): self.addTool() raise DeprecationWarning("insertToolBelow() will be removed in " "the future, use addTool() instead")
class AbstractListInputWidget(AbstractStringInputWidget): """ TODO: * as soon as you type it breaks? Signals: QLineEdit QCompleter | -- QSortFilterProxyModel --> QAbstractListModel (Custom Model) | -- Popup | -- QListView (CompleterPopup) Attributes: dynamic_update (bool): Determines if the model should be repopulated every time the user requests the popup to be displayed filter_results (bool): Determines if the returned results in the popup should be filtered based off of the current input. display_item_colors (bool): Determines if the item colors should be displayed or not. By default this is set to off. Item colors will need to be put as the second index for each item in the model provided to the list. item_list (list): string list of all of the items in the list. Updating this will auto update the default settings for blank setups previous_text (str): the previous items text. This is stored for cancel events and allowing the user to return to the previous item after cancelling. cleanItems (virtual function): returns a list of items to populate the model with. Not sure if I still need this... """ TYPE = 'list' def __init__(self, parent=None, item_list=[]): super(AbstractListInputWidget, self).__init__(parent) # setup default attrs self._item_list = [] self._previous_text = '' self._dynamic_update = False self._filter_results = True self._display_item_colors = False # setup custom completer self.setupCustomModelCompleter(item_list) # setup style self.updateStyleSheet() # setup signals self.textChanged.connect(self.filterCompletionResults) def populate(self, item_list): """ Creates all of the items from the item list and appends them to the model. item_list (list): list of strings to be displayed to the user this should be set with the setCleanItemsFunction. """ self.setItemList(item_list) self.setupCustomModelCompleter(item_list) def __getCleanItems(self): return [] def setCleanItemsFunction(self, function): """ Sets the function to get the list of strings to populate the model function (function): function to return a list of strings to be shown to the user """ self.__getCleanItems = function def getCleanItems(self): """ Returns a list of strings based off of the function set with setCleanItemsFunction """ return self.__getCleanItems() """ Style Sheet""" def updateStyleSheet(self): style_sheet_args = iColor.style_sheet_args style_sheet_args.update({'type': type(self).__name__}) style_sheet = """ {type}{{ border:None; background-color: rgba{rgba_gray_0}; selection-background-color: rgba{rgba_selected}; color: rgba{rgba_text} }} """.format(**style_sheet_args) self.setStyleSheet(style_sheet) """ COMPLETER """ def _updateModel(self, item_list=None): # get item list if not item_list: item_list = self.getCleanItems() # completer = CustomModelCompleter() # self.setCompleter(completer) # update model items self._model = CustomModel(item_list=item_list) self._model.display_item_colors = self.display_item_colors self.proxy_model = QSortFilterProxyModel() self.proxy_model.setSourceModel(self._model) # set models self.completer().setModel(self.proxy_model) # set item for popup # this makes it so that the stylesheet can be attached... # https://forum.qt.io/topic/26703/solved-stylize-using-css-and-editable-qcombobox-s-completions-list-view/7 delegate = QStyledItemDelegate() self.completer().popup().setItemDelegate(delegate) def setupCustomModelCompleter(self, item_list): """ Creates a new completely custom completer Args: item_list (list): of strings to be the list of things that is displayed to the user """ # create completer/models completer = CustomModelCompleter() self.setCompleter(completer) self.proxy_model = QSortFilterProxyModel() self._updateModel(item_list) def filterCompletionResults(self): """ Filter the current proxy model based off of the text in the input string """ # preflight if not self.filter_results: return # filter results if self.text() != '': self.completer().setCaseSensitivity(False) self.completer().setCompletionMode(QCompleter.PopupCompletion) else: self.completer().setCompletionMode( QCompleter.UnfilteredPopupCompletion) """ EVENTS """ def mouseReleaseEvent(self, event, *args, **kwargs): # update model (if enabled) if self.dynamic_update: self._updateModel() # update the current proxy model based off the current text self.filterCompletionResults() # show completer self.completer().complete() return QLineEdit.mouseReleaseEvent(self, event, *args, **kwargs) """ UTILS """ def next_completion(self): row = self.completer().currentRow() # if does not exist reset if not self.completer().setCurrentRow(row + 1): self.completer().setCurrentRow(0) # if initializing if self.completer().popup().currentIndex().row() == -1: self.completer().setCurrentRow(0) index = self.completer().currentIndex() self.completer().popup().setCurrentIndex(index) def previous_completion(self): row = self.completer().currentRow() numRows = self.completer().completionCount() # if wrapping if not self.completer().setCurrentRow(row - 1): self.completer().setCurrentRow(numRows - 1) # if initializing if self.completer().popup().currentIndex().row() == -1: self.completer().setCurrentRow(numRows - 1) index = self.completer().currentIndex() self.completer().popup().setCurrentIndex(index) def event(self, event, *args, **kwargs): if (event.type() == QEvent.KeyPress) and (event.key() == Qt.Key_Tab): if self.text() == '': self.completer().complete() else: self.next_completion() return True if (event.type() == QEvent.KeyPress) and (event.key() == 16777218): self.previous_completion() return True return AbstractStringInputWidget.event(self, event, *args, **kwargs) """ PROPERTIES """ @property def dynamic_update(self): return self._dynamic_update @dynamic_update.setter def dynamic_update(self, dynamic_update): self._dynamic_update = dynamic_update @property def filter_results(self): return self._filter_results @filter_results.setter def filter_results(self, filter_results): self._filter_results = filter_results @property def display_item_colors(self): return self._display_item_colors @display_item_colors.setter def display_item_colors(self, display_item_colors): self._display_item_colors = display_item_colors self.model().display_item_colors = display_item_colors def getItemList(self): return self._item_list def setItemList(self, item_list): # if self.previous_text == '': # item_list.insert(0, '') self._item_list = item_list @property def previous_text(self): return self._previous_text @previous_text.setter def previous_text(self, previous_text): self._previous_text = previous_text def model(self): return self._model def setModel(self, _model): self._model = _model
class AbstractComboListInputWidget(QComboBox): """ A custom QComboBox with a completer / model. This is designed to be an abstract class that will be inherited by the the GSV and Node ComboBoxes Attributes: exists (bool) flag used to determine whether or not the popup menu for the menu change should register or not (specific to copy/paste of a node. In plain english... this flag is toggled to hide the Warning PopUp Box from displaying to the user in some events. item_list (list): string list of all of the items in the list. Updating this will auto update the default settings for blank setups previous_text (str): the previous items text. This is stored for cancel events and allowing the user to return to the previous item after cancelling. """ TYPE = 'list' def __init__(self, parent=None): super(AbstractComboListInputWidget, self).__init__(parent) self.main_widget = self.parent() self.previous_text = '' self.setExistsFlag(True) # setup line edit #self.line_edit = QLineEdit("Select & Focus", self) self.line_edit = QLineEdit(self) self.line_edit.editingFinished.connect(self.userFinishedEditing) self.setLineEdit(self.line_edit) self.setEditable(True) # setup completer self.completer = QCompleter(self) self.completer.setCompletionMode(QCompleter.PopupCompletion) self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.completer.setPopup(self.view()) self.setCompleter(self.completer) self.pFilterModel = QSortFilterProxyModel(self) # set size policy ( this will ignore the weird resizing effects) size_policy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred) self.setSizePolicy(size_policy) # initialize model... model = QStandardItemModel() self.setModel(model) self.setModelColumn(0) def populate(self, item_list): """ Creates all of the items from the item list and appends them to the model. item_list (list): list of strings to be displayed to the user this should be set with the setCleanItemsFunction. """ self.setItemList(item_list) for i, item_name in enumerate(self.getItemList()): item = QStandardItem(item_name) self.model().setItem(i, 0, item) self.setExistsFlag(False) def update(self): """ Updates the model items with all of the graph state variables. This is very similar to the populate call, except that with this, it will remove all of the items except for the current one. Which will ensure that an indexChanged event is not registered thus updating the UI. """ self.setExistsFlag(False) self.model().clear() self.populate(self.getCleanItems()) self.setCurrentIndexToText(self.previous_text) self.setExistsFlag(True) def setModel(self, model): # somehow this super makes the node type not resize... super(AbstractListInputWidget, self).setModel(model) self.pFilterModel.setSourceModel(model) self.completer.setModel(self.pFilterModel) def setModelColumn(self, column): self.completer.setCompletionColumn(column) self.pFilterModel.setFilterKeyColumn(column) super(AbstractListInputWidget, self).setModelColumn(column) def view(self): return self.completer.popup() """ UTILS """ def next_completion(self): row = self.completer.currentRow() # if does not exist reset if not self.completer.setCurrentRow(row + 1): self.completer.setCurrentRow(0) # if initializing if self.completer.popup().currentIndex().row() == -1: self.completer.setCurrentRow(0) index = self.completer.currentIndex() self.completer.popup().setCurrentIndex(index) def previous_completion(self): row = self.completer.currentRow() numRows = self.completer.completionCount() # if wrapping if not self.completer.setCurrentRow(row - 1): self.completer.setCurrentRow(numRows - 1) # if initializing if self.completer.popup().currentIndex().row() == -1: self.completer.setCurrentRow(numRows - 1) index = self.completer.currentIndex() self.completer.popup().setCurrentIndex(index) def setCurrentIndexToText(self, text): """ Sets the current index to the text provided. If no match can be found, this will default back to the first index. If no first index... create '' ? """ self.setExistsFlag(False) # get all matches items = self.model().findItems(text, Qt.MatchExactly) # set to index of match if len(items) > 0: index = self.model().indexFromItem(items[0]).row() self.setCurrentIndex(index) else: self.setCurrentIndex(0) self.previous_text = self.currentText() self.setExistsFlag(True) def isUserInputValid(self): """ Determines if the new user input is currently in the model. Returns True if this is an existing item, Returns false if it is not. """ items = self.model().findItems(self.currentText(), Qt.MatchExactly) if len(items) > 0: return True else: return False def setupStyleSheet(self): width = self.width() dropdown_width = int(width * 0.35) style_sheet_args = iColor.style_sheet_args style_sheet_args['width'] = dropdown_width # QComboBox {{ # border: None; # background-color: rgba{rgba_gray_0} # }} style_sheet = """ QComboBox{{ border: None; background-color: rgba{rgba_gray_0}; color: rgba{rgba_text}; }} QComboBox::drop-down {{ width: {width}px; }} QLineEdit{{ border: None; background-color: rgba{rgba_gray_0}; color: rgba{rgba_text}; }} QListView{{ border: None; background-color: rgba{rgba_gray_0}; color: rgba{rgba_text}; }} QListView::item:hover{{ background-color: rgba(255,0,0,255); }} """.format(**style_sheet_args) self.completer.popup().setStyleSheet(""" QListView{{ border: None; background-color: rgba{rgba_gray_0}; color: rgba{rgba_text}; }} QListView::item:hover{{ background-color: rgba(255,0,0,255); }} """.format(**style_sheet_args)) self.setStyleSheet(style_sheet) """ API """ def __selectionChangedEmit(self): pass def setSelectionChangedEmitEvent(self, method): """ sets the method for the selection changed emit call this will be called everytime the user hits enter/return inside of the line edits as a way of sending an emit signal from the current text changed (finalized) before input changed event... """ self.__selectionChangedEmit = method def __getCleanItems(self): return [] def setCleanItemsFunction(self, function): """ Sets the function to get the list of strings to populate the model function (function): function to return a list of strings to be shown to the user """ self.__getCleanItems = function def getCleanItems(self): """ Returns a list of strings based off of the function set with setCleanItemsFunction """ return self.__getCleanItems() """ EVENTS """ def userFinishedEditing(self): is_input_valid = self.isUserInputValid() if is_input_valid: self.__selectionChangedEmit() self.previous_text = self.currentText() #self.userFinishedEditingEvent(self.currentText()) else: self.setCurrentIndexToText(self.previous_text) def resizeEvent(self, event): self.setupStyleSheet() return QComboBox.resizeEvent(self, event) def event(self, event, *args, **kwargs): """ Registering key presses in here as for some reason they don't work in the keyPressEvent method... """ if event.type() == QEvent.KeyPress: # tab if event.key() == Qt.Key_Tab: self.next_completion() return True # shift tab elif event.key() == Qt.Key_Tab + 1: self.previous_completion() return True # enter pressed elif event.key() in [Qt.Key_Return, Qt.Key_Enter, Qt.Key_Down]: self.__selectionChangedEmit() elif event.type() == QEvent.MouseButtonRelease: self.completer.setPopup(self.view()) return QComboBox.event(self, event, *args, **kwargs) """ PROPERTIES """ def getExistsFlag(self): return self._exists def setExistsFlag(self, exists): self._exists = exists def getItemList(self): return self._item_list def setItemList(self, item_list): if self.previous_text == '': item_list.insert(0, '') self._item_list = item_list @property def previous_text(self): return self._previous_text @previous_text.setter def previous_text(self, previous_text): self._previous_text = previous_text