def __init__(self, parent: None, title: str, animation_duration: int, set_expanded_height: int = 0): super().__init__(parent=parent) self.parent = parent self.animation_duration = animation_duration self.title = title self.set_expanded_height = set_expanded_height self.toggle_button = QtWidgets.QToolButton() self.header_line = QtWidgets.QFrame() self.toggle_animation = QtCore.QParallelAnimationGroup() self.content_area = QtWidgets.QScrollArea() self.main_layout = QtWidgets.QGridLayout() self.toggle_button.setStyleSheet("QToolButton { border: none; }") self.toggle_button.setToolButtonStyle( QtCore.Qt.ToolButtonTextBesideIcon) self.toggle_button.setArrowType(QtCore.Qt.RightArrow) self.toggle_button.setText(str(title)) self.toggle_button.setCheckable(True) self.toggle_button.setChecked(False) self.header_line.setFrameShape(QtWidgets.QFrame.HLine) self.header_line.setFrameShadow(QtWidgets.QFrame.Sunken) self.header_line.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Maximum) self.content_area.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) self.content_area.setMaximumHeight(0) self.content_area.setMinimumHeight(0) self.toggle_animation.addAnimation( QtCore.QPropertyAnimation(self, b'minimumHeight')) self.toggle_animation.addAnimation( QtCore.QPropertyAnimation(self, b'maximumHeight')) self.toggle_animation.addAnimation( QtCore.QPropertyAnimation(self.content_area, b'maximumHeight')) self.main_layout.setVerticalSpacing(0) self.main_layout.setContentsMargins(0, 0, 0, 0) self.main_layout.addWidget(self.toggle_button, 0, 0, 1, 1, QtCore.Qt.AlignLeft) self.main_layout.addWidget(self.header_line, 0, 2, 1, 1) self.main_layout.addWidget(self.content_area, 1, 0, 1, 3) self.setLayout(self.main_layout) self.toggle_button.toggled.connect(self.toggle)
def paintEvent(self, event: QtGui.QPaintEvent): painter = QtGui.QPainter(self) # Draw rule self.opt.initFrom(self) self.opt.rect = self.rect() self.opt.sliderPosition = 0 self.opt.subControls = QtWidgets.QStyle.SC_SliderGroove | QtWidgets.QStyle.SC_SliderTickmarks # Draw GROOVE self.style().drawComplexControl(QtWidgets.QStyle.CC_Slider, self.opt, painter) # Draw INTERVAL color = self.palette().color(QtGui.QPalette.Highlight) color.setAlpha(160) painter.setBrush(QtGui.QBrush(color)) painter.setPen(QtCore.Qt.NoPen) self.opt.sliderPosition = self.first_position x_left_handle = (self.style().subControlRect( QtWidgets.QStyle.CC_Slider, self.opt, QtWidgets.QStyle.SC_SliderHandle).right()) self.opt.sliderPosition = self.second_position x_right_handle = (self.style().subControlRect( QtWidgets.QStyle.CC_Slider, self.opt, QtWidgets.QStyle.SC_SliderHandle).left()) groove_rect = self.style().subControlRect( QtWidgets.QStyle.CC_Slider, self.opt, QtWidgets.QStyle.SC_SliderGroove) selection = QtCore.QRect( x_left_handle, groove_rect.y(), x_right_handle - x_left_handle, groove_rect.height(), ).adjusted(-1, 1, 1, -1) painter.drawRect(selection) # Draw first handle self.opt.subControls = QtWidgets.QStyle.SC_SliderHandle self.opt.sliderPosition = self.first_position self.style().drawComplexControl(QtWidgets.QStyle.CC_Slider, self.opt, painter) # Draw second handle self.opt.sliderPosition = self.second_position self.style().drawComplexControl(QtWidgets.QStyle.CC_Slider, self.opt, painter)
def trim_time_toggled(self, state): """ Triggered if the 'trim by time' checkbox is checked. Automatically turns off the 'trim by line' checkbox and populates the trim time controls Parameters ---------- state if True, the 'trim by time' checkbox has been checked """ if state: self.trim_line_check.setChecked(False) starttme = self.slider_mintime endtme = self.slider_maxtime self.trim_time_start.setText(str(starttme)) self.trim_time_end.setText(str(endtme)) self.trim_time_datetime_start.setDateTime( QtCore.QDateTime.fromSecsSinceEpoch(int(starttme), QtCore.QTimeZone(0))) self.trim_time_datetime_end.setDateTime( QtCore.QDateTime.fromSecsSinceEpoch(int(endtme), QtCore.QTimeZone(0)))
def sizeHint(self): """ override """ SliderLength = 84 TickSpace = 5 w = SliderLength h = self.style().pixelMetric(QtWidgets.QStyle.PM_SliderThickness, self.opt, self) if (self.opt.tickPosition & QtWidgets.QSlider.TicksAbove or self.opt.tickPosition & QtWidgets.QSlider.TicksBelow): h += TickSpace return (self.style().sizeFromContents( QtWidgets.QStyle.CT_Slider, self.opt, QtCore.QSize(w, h), self).expandedTo(QtWidgets.QApplication.globalStrut()))
def settings_object(self): if self.external_settings: return self.external_settings else: return QtCore.QSettings("NOAA", "Kluster")
def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle('Overwrite Navigation') layout = QtWidgets.QVBoxLayout() self.input_msg = QtWidgets.QLabel('Apply to the following:') self.hlayout_zero = QtWidgets.QHBoxLayout() self.input_fqpr = BrowseListWidget(self) self.input_fqpr.sizeHint() self.input_fqpr.setup(mode='directory', registry_key='kluster', app_name='klusterbrowse', filebrowse_title='Select input processed folder') self.hlayout_zero.addWidget(self.input_fqpr) self.posmv_msg = QtWidgets.QLabel('POSMV Files') self.hlayout_two = QtWidgets.QHBoxLayout() self.posmvfiles = BrowseListWidget(self) self.posmvfiles.setup(registry_key='kluster', app_name='klusterbrowse', multiselect=True, filebrowse_title='Select POS MV files') self.hlayout_two.addWidget(self.posmvfiles) self.override_check = QtWidgets.QGroupBox('Manually set metadata') self.override_check.setCheckable(False) self.override_check.setChecked(True) self.overrideopts = QtWidgets.QVBoxLayout() self.hlayout_four_one = QtWidgets.QHBoxLayout() self.caltext = QtWidgets.QLabel('Date of POS MV File') self.hlayout_four_one.addWidget(self.caltext) self.calendar_widget = QtWidgets.QDateEdit() self.calendar_widget.setMaximumWidth(100) self.calendar_widget.setCalendarPopup(True) currdate = datetime.now() self.calendar_widget.setDate( QtCore.QDate(currdate.year, currdate.month, currdate.day)) self.hlayout_four_one.addWidget(self.calendar_widget) self.hlayout_four_two = QtWidgets.QHBoxLayout() self.overrideopts.addLayout(self.hlayout_four_one) self.overrideopts.addLayout(self.hlayout_four_two) self.override_check.setLayout(self.overrideopts) self.status_msg = QtWidgets.QLabel('') self.status_msg.setStyleSheet("QLabel { " + kluster_variables.error_color + "; }") self.hlayout_five = QtWidgets.QHBoxLayout() self.hlayout_five.addStretch(1) self.ok_button = QtWidgets.QPushButton('OK', self) self.hlayout_five.addWidget(self.ok_button) self.hlayout_five.addStretch(1) self.cancel_button = QtWidgets.QPushButton('Cancel', self) self.hlayout_five.addWidget(self.cancel_button) self.hlayout_five.addStretch(1) layout.addWidget(self.input_msg) layout.addLayout(self.hlayout_zero) layout.addWidget(self.posmv_msg) layout.addLayout(self.hlayout_two) layout.addWidget(self.override_check) layout.addWidget(self.status_msg) layout.addLayout(self.hlayout_five) self.setLayout(layout) self.pos_files = [] self.fqpr_inst = [] self.canceled = False self.input_fqpr.files_updated.connect( self._event_update_fqpr_instances) self.posmvfiles.files_updated.connect(self.update_posmv_files) self.ok_button.clicked.connect(self.start_processing) self.cancel_button.clicked.connect(self.cancel_processing) self.resize(600, 500)
def settings_object(self): if self.external_settings: return self.external_settings else: return QtCore.QSettings("NOAA", self.appname)
class IntelViewer(QtWidgets.QTableWidget): """ QTableWidget to display the data from an fqpr_intelligence IntelModule. """ file_added = QtCore.Signal(str) remove_by_uniqueid = QtCore.Signal(int) show_in_explorer = QtCore.Signal(int) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.headr = [] # self.setStyleSheet('font: 10.5pt "Consolas";') self.setDragEnabled(True) # enable support for dragging table items self.setAcceptDrops(True) # enable drop events self.viewport().setAcceptDrops( True ) # viewport is the total rendered area, this is recommended from my reading self.setDragDropOverwriteMode( False) # False makes sure we don't overwrite rows on dragging self.setDropIndicatorShown(True) self.setSortingEnabled(True) # ExtendedSelection - allows multiselection with shift/ctrl self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) # makes it so no editing is possible with the table self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) self.right_click_menu = None self.setup_menu() self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.show_right_click_menu) def setup_menu(self): """ Build the right click menu for added lines """ self.right_click_menu = QtWidgets.QMenu('menu', self) close_dat = QtWidgets.QAction('Close', self) close_dat.triggered.connect(self.right_click_close_row) show_explorer = QtWidgets.QAction('Open Explorer', self) show_explorer.triggered.connect(self.show_file_in_explorer) self.right_click_menu.addAction(close_dat) self.right_click_menu.addAction(show_explorer) def show_right_click_menu(self): """ Generate a close option when you right click as well as an open in explorer option """ if self.selectedItems(): # a row is selected self.right_click_menu.exec_(QtGui.QCursor.pos()) def right_click_close_row(self, event: QtCore.QEvent): """ On right clicking an added file and selecting 'Close', remove the file from the table and from the attached intelligence module Parameters ---------- event triggered event """ itms = self.selectedItems() if self.headr[-1] != 'unique_id': print( 'Error: context menu requires "unique_id" as the last element') return idx = len(self.headr) - 1 while idx <= len(itms): uid = itms[idx].text() self.remove_by_uniqueid.emit(int(uid)) idx += len(self.headr) def show_file_in_explorer(self, event: QtCore.QEvent): """ On right clicking an added file and selecting 'Open Explorer', open the containing file in explorer Parameters ---------- event triggered event """ itms = self.selectedItems() if self.headr[-1] != 'unique_id': print( 'Error: context menu requires "unique_id" as the last element') return idx = len(self.headr) - 1 while idx <= len(itms): uid = itms[idx].text() self.show_in_explorer.emit(int(uid)) idx += len(self.headr) def dragEnterEvent(self, event: QtCore.QEvent): """ Catch mouse drag enter events to block things not move/read related Parameters ---------- event QEvent which is sent to a widget when a drag and drop action enters it """ event.accept() def dragMoveEvent(self, event: QtCore.QEvent): """ Catch mouse drag enter events to block things not move/read related Parameters ---------- event QEvent which is sent while a drag and drop action is in progress """ event.accept() def dropEvent(self, event: QtCore.QEvent): """ On drag and drop, handle either reordering of rows or incoming new data from file For incoming new file, trigger a file_added event when dragging and dropping a file in the IntelViewer Parameters ---------- event QEvent which is sent when a drag and drop action is completed """ if not event.isAccepted() and event.source() == self: event.setDropAction(QtCore.Qt.MoveAction) drop_row = self._drop_row_index(event) self.custom_move_row(drop_row) elif event.mimeData().hasUrls(): for url in event.mimeData().urls(): self.file_added.emit(url.toLocalFile()) else: print('Unrecognized input: {}'.format(event.source())) def _drop_row_index(self, event: QtCore.QEvent): """ Returns the integer row index of the insertion point on drag and drop Parameters ---------- event QEvent which is sent when a drag and drop action is completed Returns ------- int row index """ index = self.indexAt(event.pos()) if not index.isValid(): return self.rowCount() return index.row() + 1 if self._is_below(event.pos(), index) else index.row() def _is_below(self, pos: QtCore.QPoint, index: int): """ Using the event position and the row rect shape, figure out if the new row should go above the index row or below. Parameters ---------- pos position of the cursor at the event time index row index at the cursor Returns ------- bool True if new row should go below, False otherwise """ rect = self.visualRect(index) margin = 2 if pos.y() - rect.top() < margin: return False elif rect.bottom() - pos.y() < margin: return True return rect.contains(pos, True) and pos.y() >= rect.center().y() def custom_move_row(self, drop_row: int): """ Something I stole from someone online. Will get the row indices of the selected rows and insert those rows at the drag-n-drop mouse cursor location. Will even account for relative cursor position to the center of the row, see _is_below. Parameters ---------- drop_row row index of the insertion point for the drag and drop """ rows = sorted(set( item.row() for item in self.selectedItems())) # pull all the selected rows rows_to_move = [[ QtWidgets.QTableWidgetItem(self.item(row_index, column_index)) for column_index in range(self.columnCount()) ] for row_index in rows] # get the data for the rows for row_index in reversed(rows): self.removeRow(row_index) if row_index < drop_row: drop_row -= 1 for row_index, data in enumerate(rows_to_move): row_index += drop_row self.insertRow(row_index) for column_index, column_data in enumerate(data): self.setItem(row_index, column_index, column_data) for row_index in range(len(rows_to_move)): for i in range(int(len(self.headr))): itm = self.item(drop_row + row_index, i) if itm is not None: itm.setSelected(True) def update_from_dict(self, dict_attributes: OrderedDict): """ Add a new row to the table, where the column values are the matching keys between dict_attributes and the self.headr. Parameters ---------- dict_attributes new row to be added """ if dict_attributes and self.headr: # headr is only populated when extending this class next_row = self.rowCount() self.insertRow(next_row) for col_index, ky in enumerate(self.headr): data = dict_attributes[ky] if isinstance(data, datetime): data = data.strftime('%D %H:%M:%S') elif isinstance(data, list): for cnt, d in enumerate(data): if isinstance(d, datetime): data[cnt] = d.strftime('%D %H:%M:%S') data_item = QtWidgets.QTableWidgetItem(str(data)) self.setItem(next_row, col_index, data_item) def remove_row(self, unique_id: int): """ Remove a row based on the provided unique_id Parameters ---------- unique_id unique id for the row to be removed """ uid_column_index = self.headr.index('unique_id') total_rows = self.rowCount() remove_these_rows = [] for i in range(total_rows): if self.item(i, uid_column_index).text() == str(unique_id): remove_these_rows.append(i) for i in sorted( remove_these_rows, reverse=True): # remove from bottom up to not mess up index self.removeRow(i)
def minimumSizeHint(self): return QtCore.QSize(600, 400)
def sizeHint(self): return QtCore.QSize(1200, 600)
def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle('Import Post Processed Navigation') layout = QtWidgets.QVBoxLayout() self.input_msg = QtWidgets.QLabel('Apply to the following:') self.hlayout_zero = QtWidgets.QHBoxLayout() self.input_fqpr = BrowseListWidget(self) self.input_fqpr.sizeHint() self.input_fqpr.setup(mode='directory', registry_key='kluster', app_name='klusterbrowse', filebrowse_title='Select input processed folder') self.hlayout_zero.addWidget(self.input_fqpr) self.sbet_msg = QtWidgets.QLabel('POSPac SBET Files') self.hlayout_two = QtWidgets.QHBoxLayout() self.sbetfiles = BrowseListWidget(self) self.sbetfiles.setup(registry_key='kluster', app_name='klusterbrowse', supported_file_extension=['.out', '.sbet'], multiselect=True, filebrowse_title='Select SBET files', filebrowse_filter='POSPac SBET files (*.out;*.sbet)') self.hlayout_two.addWidget(self.sbetfiles) self.smrmsg_msg = QtWidgets.QLabel('POSPac SMRMSG Files') self.hlayout_three = QtWidgets.QHBoxLayout() self.smrmsgfiles = BrowseListWidget(self) self.smrmsgfiles.setup(registry_key='kluster', app_name='klusterbrowse', supported_file_extension=['.out', '.smrmsg'], multiselect=True, filebrowse_title='Select SMRMSG files', filebrowse_filter='POSPac SMRMSG files (*.out;*.smrmsg)') self.hlayout_three.addWidget(self.smrmsgfiles) self.log_check = QtWidgets.QGroupBox('Load from POSPac export log') self.log_check.setCheckable(True) self.log_check.setChecked(True) self.logopts = QtWidgets.QVBoxLayout() self.hlayout_one = QtWidgets.QHBoxLayout() self.log_file = QtWidgets.QLineEdit('', self) self.log_file.setMinimumWidth(400) self.log_file.setReadOnly(True) self.hlayout_one.addWidget(self.log_file) self.browse_button = QtWidgets.QPushButton("Browse", self) self.hlayout_one.addWidget(self.browse_button) self.logopts.addLayout(self.hlayout_one) self.log_check.setLayout(self.logopts) self.override_check = QtWidgets.QGroupBox('Manually set metadata') self.override_check.setCheckable(True) self.override_check.setChecked(False) self.overrideopts = QtWidgets.QVBoxLayout() self.hlayout_four_one = QtWidgets.QHBoxLayout() self.caltext = QtWidgets.QLabel('Date of SBET') self.hlayout_four_one.addWidget(self.caltext) self.calendar_widget = QtWidgets.QDateEdit() self.calendar_widget.setMaximumWidth(100) self.calendar_widget.setCalendarPopup(True) currdate = datetime.now() self.calendar_widget.setDate(QtCore.QDate(currdate.year, currdate.month, currdate.day)) self.hlayout_four_one.addWidget(self.calendar_widget) self.hlayout_four_two = QtWidgets.QHBoxLayout() self.datumtext = QtWidgets.QLabel('Coordinate System') self.hlayout_four_two.addWidget(self.datumtext) self.datum_val = QtWidgets.QComboBox() self.datum_val.addItems(['NAD83', 'WGS84']) self.datum_val.setMaximumWidth(100) self.hlayout_four_two.addWidget(self.datum_val) self.overrideopts.addLayout(self.hlayout_four_one) self.overrideopts.addLayout(self.hlayout_four_two) self.override_check.setLayout(self.overrideopts) self.status_msg = QtWidgets.QLabel('') self.status_msg.setStyleSheet("QLabel { " + kluster_variables.error_color + "; }") self.hlayout_five = QtWidgets.QHBoxLayout() self.hlayout_five.addStretch(1) self.ok_button = QtWidgets.QPushButton('OK', self) self.hlayout_five.addWidget(self.ok_button) self.hlayout_five.addStretch(1) self.cancel_button = QtWidgets.QPushButton('Cancel', self) self.hlayout_five.addWidget(self.cancel_button) self.hlayout_five.addStretch(1) layout.addWidget(self.input_msg) layout.addLayout(self.hlayout_zero) layout.addWidget(self.sbet_msg) layout.addLayout(self.hlayout_two) layout.addWidget(self.smrmsg_msg) layout.addLayout(self.hlayout_three) layout.addWidget(self.log_check) layout.addWidget(self.override_check) layout.addWidget(self.status_msg) layout.addLayout(self.hlayout_five) self.setLayout(layout) self.log_file_path = '' self.sbet_files = [] self.smrmsg_files = [] self.fqpr_inst = [] self.canceled = False self.browse_button.clicked.connect(self.file_browse) self.log_check.clicked.connect(self.log_override_checked) self.override_check.clicked.connect(self.log_override_checked) self.input_fqpr.files_updated.connect(self._event_update_fqpr_instances) self.sbetfiles.files_updated.connect(self.update_sbet_files) self.smrmsgfiles.files_updated.connect(self.update_smrmsg_files) self.ok_button.clicked.connect(self.start_processing) self.cancel_button.clicked.connect(self.cancel_processing) self.resize(600, 500)
class KlusterProjectView(QtWidgets.QWidget): """ QTableWidget to display the data from an fqpr_intelligence IntelModule. """ file_added = QtCore.Signal(str) def __init__(self, parent=None, *args, **kwargs): super().__init__(*args, **kwargs) self.parent = parent self.project_file = None self.project = None self.loaded_fqpr_views = [] self.loaded_collapsible = [] self.mainlayout = QtWidgets.QVBoxLayout() self.hlayout = QtWidgets.QHBoxLayout() self.fil_text = QtWidgets.QLineEdit('') self.fil_text.setMinimumWidth(400) self.fil_text.setReadOnly(True) self.hlayout.addWidget(self.fil_text) self.newproj_button = QtWidgets.QPushButton("New Project") self.hlayout.addWidget(self.newproj_button) self.openproj_button = QtWidgets.QPushButton("Open Project") self.hlayout.addWidget(self.openproj_button) self.mainlayout.addLayout(self.hlayout) scroll = QtWidgets.QScrollArea() scroll.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) scroll_content = QtWidgets.QWidget() scroll_layout = QtWidgets.QVBoxLayout(scroll_content) scroll_content.setLayout(scroll_layout) self.datalayout = QtWidgets.QVBoxLayout() scroll_layout.addLayout(self.datalayout) scroll.setWidget(scroll_content) scroll.setWidgetResizable(True) self.mainlayout.addWidget(scroll) self.setLayout(self.mainlayout) self.setMinimumSize(1000, 600) self.newproj_button.clicked.connect(self.new_project) self.openproj_button.clicked.connect(self.open_project) def _load_from_project(self): """ Build out the gui from the loaded project. Each fqpr instance gets it's own collapsible section """ for fqpr_name, fqpr_inst in self.project.fqpr_instances.items(): fqprview = KlusterFqprView(self, fqpr_inst) new_expand = CollapsibleWidget(self, fqpr_name, 100, set_expanded_height=800) new_layout = QtWidgets.QVBoxLayout() new_layout.addWidget(fqprview) new_expand.setContentLayout(new_layout) self.datalayout.addWidget(new_expand) self.loaded_fqpr_views.append(fqprview) self.loaded_collapsible.append(new_expand) self.datalayout.addStretch() self.datalayout.layout() def new_project(self): """ Get the file path to a new project """ # dirpath will be None or a string msg, pth = RegistryHelpers.GetFilenameFromUserQT( self, RegistryKey='klusterintel', Title='Create a new Kluster project', AppName='klusterintel', fFilter="*.json", bSave=True, DefaultFile='kluster_project.json') if pth: # the project name is mandatory, just so that we can find it later, I ask for a file path for the project # file and override the filename, kind of messy but works for now if os.path.exists(pth): os.remove(pth) directory, filename = os.path.split(pth) project_file = os.path.join(directory, 'kluster_project.json') self.fil_text.setText(project_file) self.project_file = project_file self.project = FqprProject(is_gui=True) self.project.new_project_from_directory(directory) if self.parent: self.parent.set_project(self.project) self._load_from_project() def open_project(self): """ Get the file path to a new project """ # dirpath will be None or a string msg, pth = RegistryHelpers.GetFilenameFromUserQT( self, RegistryKey='klusterintel', Title='Open an existing Kluster project', AppName='klusterintel', fFilter="*.json", bSave=False) if pth: self.fil_text.setText(pth) self.build_from_project(pth) def close_project(self): self.fil_text.setText('') self.project = None if self.parent: self.parent.set_project(self.project) def build_from_project(self, project_path: str): """ Load from a new project file, will close the active project and repopulate the gui Parameters ---------- project_path path to a kluster project, kluster_project.json file """ if os.path.exists(project_path): self.clear_project() self.project_file = project_path self.project = FqprProject(is_gui=True) self.project.open_project(self.project_file, skip_dask=True) if self.parent: self.parent.set_project(self.project) self._load_from_project() print('Loaded {}'.format(project_path)) else: print('Unable to load from file, does not exist: {}'.format( project_path)) def clear_project(self): """ Clear the datalayout layout widget """ clear_layout(self.datalayout)