class SnifferFilter(QDialog): ## The constructor. # Initialize lists to be filled with filtered payloads and # create the dialog. def __init__(self, parent): super(SnifferFilter, self).__init__() self.parent = parent self.setWindowTitle('SnifferFilter') self.filteredPayloadList = [] self.filteredIdList = [] self.layoutingComplete = False self.calledBy = 'NONE' # We store here which tab the filter belongs to, if there is no parent.(Like when the filter is global!) self.objectTabDict = {} self.filteredObjectDict = {} self.resize(670, 700) ## Filters the global payloadList by ID by iterating through it and # appending to a new list by filter-criteria # @param filterList a list of IDs that are to be transferred to the new list def filterPayloadsByID(self, filterList): for payload in Globals.payloadList: if hasattr(payload, 'payloadHead'): if Globals.tspDict[ payload.payloadHead.informationID][0] in filterList: self.filteredPayloadList.append(payload) else: #print('informationID is in filteredIdList, skipping packet') pass ## Filters the global payloadList by Message by iterating through it and # appending to a new list by filter-criteria # @param filterDict a dictionary of Key: ObjectType and Value: ObjectsToKeep as a template # of which items are to be kept in the filteredList def filterPayloadsByMessage(self, filterDict): localFilteredList = [] for payload in self.filteredPayloadList: print('payloadID:' + str(payload.payloadHead.informationID)) # If the ID has nothing to do with the object, we can safely add it. if payload.payloadHead.informationID is 23: x = 0 pass if isRelatedToObject(payload): for objType, messageID in filterDict.items(): print(Globals.objectTypeDict[getObjectType(payload)]) if Globals.objectTypeDict[getObjectType( payload )] == objType: # If the objectType matches the one in the dictionary if objType == 0: x = 0 pass if getDataType(payload) == 2: if payload.data2 not in messageID: # and the message does not match the dictionary print( 'Passing data with msgid: ' + str(payload.data2) ) # don't append, but print that we skipped this one else: localFilteredList.append( payload ) # the message does match the dictionary -> we want to keep it, so we add it to the list elif getDataType(payload) == 1: if payload.data1 not in messageID: print('Passing data with msgid: ' + str(payload.data1)) else: localFilteredList.append(payload) else: localFilteredList.append(payload) else: # If the ID has nothing to do with the object, we can safely add it. # Also, is the object is not even in the filterDict, we can add it too (this should not happen, but # it's there for safety purposes if getDataType(payload) == 0 or Globals.objectTypeDict[ getObjectType(payload)] not in filterDict: localFilteredList.append(payload) else: localFilteredList.append(payload) # In every other case, append it to the list, since we only want to filter out specific objects self.filteredPayloadList = list(localFilteredList) ## Create the visible UI # like the different tables, the searchbar etc. def setSnifferFilterUi(self): self.filterTabs = QTabWidget() self.H1layout = QHBoxLayout() self.Vlayout = QVBoxLayout() self.searchInputField = QLineEdit() self.searchInputField.setPlaceholderText( 'Enter search term, then click search') self.searchButt = QPushButton('Search Table') self.saveFilterButt = QPushButton('Save Filter') self.filterTable = QTableWidget() self.saveFilterButt.clicked.connect(self.saveFilterList) self.searchButt.clicked.connect( lambda: self.searchInTable(self.searchInputField.text(), 0)) # Create Table self.filterTableIndex = 0 self.filterTable = QTableWidget() self.filterTableItem = QTableWidgetItem() self.filterTable.setRowCount(0) self.filterTable.setColumnCount(2) self.filterTable.setHorizontalHeaderLabels( 'informationID;Enable'.split(';')) self.filterTable.resizeColumnsToContents() self.filterTableHeader = self.filterTable.horizontalHeader() self.filterTableHeader.setSectionResizeMode(0, QHeaderView.Stretch) self.filterTableHeader.setSectionResizeMode( 1, QHeaderView.ResizeToContents) font = self.getFont() self.checkBoxAllIds = self.createCheckBox() self.filterTable.itemChanged.connect(self.filterAllIDs) self.checkBoxAllMessages = self.createCheckBox() # -- Add first Row for all -- # self.filterTable.insertRow(self.filterTableIndex) idFilterItem = QTableWidgetItem('FILTER ALL IDs') idFilterItem.setFont(font) messageFilterItem = QTableWidgetItem('FILTER ALL Messages') messageFilterItem.setFont(font) self.filterTable.setItem(self.filterTableIndex, 0, idFilterItem) self.filterTable.setItem(self.filterTableIndex, 1, self.checkBoxAllIds) self.filterTableIndex = self.filterTableIndex + 1 # -- Add informationID filter rows -- # for keys, values in Globals.tspDict.items(): if values[0].startswith('ID'): checkBoxFilter = self.createCheckBox() self.filterTable.insertRow(self.filterTableIndex) self.filterTable.setItem(self.filterTableIndex, 0, QTableWidgetItem(values[0])) self.filterTable.setItem(self.filterTableIndex, 1, checkBoxFilter) self.filterTableIndex = self.filterTableIndex + 1 self.filterTabs.addTab(self.filterTable, 'Information ID') self.H1layout.addWidget(self.searchInputField) self.H1layout.addWidget(self.searchButt) self.Vlayout.addLayout(self.H1layout) self.Vlayout.addWidget(self.filterTabs) self.Vlayout.addWidget(self.saveFilterButt) #------------------------------------ self.setLayout(self.Vlayout) self.layoutingComplete = True ## Updates the visible filter UI to the new objectList # This function is called, when either a new measurement was executed or an old measurement was loaded. # Since the objects shown in the Filter need to be updated. def updateObjectFilter(self): # -- Add message filter rows -- # font = self.getFont() self.filterTabs.clear() self.filterTabs.addTab(self.filterTable, 'Information ID') # For each object in objectList, create a new Table and add it to the tabs. for keys, values in Globals.objectDict.items(): objectFilterTable = QTableWidget() objectFilterTable.setRowCount(0) objectFilterTable.setColumnCount(2) strSplit = keys + ';Enable' objectFilterTable.setHorizontalHeaderLabels(strSplit.split(';')) objectFilterTable.resizeColumnsToContents() filterTableHeader = objectFilterTable.horizontalHeader() filterTableHeader.setSectionResizeMode(0, QHeaderView.Stretch) filterTableHeader.setSectionResizeMode( 1, QHeaderView.ResizeToContents) checkBoxFilter = self.createCheckBox() objectFilterTable.itemChanged.connect( lambda *a, table=objectFilterTable: self.filterAllObjectIDs( *a, table=table)) objectTableIndex = 0 objectFilterTable.insertRow(objectTableIndex) objCat = QTableWidgetItem('FILTER ALL') objCat.setFont(font) objectFilterTable.setItem(objectTableIndex, 0, objCat) objectFilterTable.setItem(objectTableIndex, 1, checkBoxFilter) objectTableIndex = objectTableIndex + 1 for keys2, values2 in values.items(): objectFilterTable.insertRow(objectTableIndex) checkBoxFilter = self.createCheckBox() objectFilterTable.setItem( objectTableIndex, 0, QTableWidgetItem('ID: ' + str(keys2) + ' Name: ' + str(values2))) objectFilterTable.setItem(objectTableIndex, 1, checkBoxFilter) objectTableIndex = objectTableIndex + 1 # Add the newly create table to the tabs. self.filterTabs.addTab(objectFilterTable, keys) self.objectTabDict[keys] = objectFilterTable ## Searches the table for a string and scrolls to the found item. # @param textToSearch the string that needs to be found in the table # @param column the column where the search needs to take place def searchInTable(self, textToSearch, column): # Create a model of the table, so we can match a text tableModel = self.filterTable.model() start = tableModel.index(0, column) matches = tableModel.match(start, Qt.DisplayRole, textToSearch, 1, Qt.MatchContains) # If there are multiple matches, we take the first one, select it and scroll to it if matches: index = matches[0] self.filterTable.selectionModel().select( index, QItemSelectionModel.Select) self.filterTable.scrollToItem( self.filterTable.itemFromIndex(index)) ## CB: SaveFilterButton // Call the filterFunctions -> Filter the unfiltered list by ID and Object and call the update # function of the executing tab in order to update their UI. def saveFilterList(self): self.filteredPayloadList.clear() #--Save by ID--# rowCnt = self.filterTable.rowCount() self.filteredIdList.clear() for rows in range(0, rowCnt): if self.filterTable.item(rows, 1).checkState() == Qt.Checked: #print(self.filterTable.item(rows,0).text()) self.filteredIdList.append( self.filterTable.item(rows, 0).text()) self.filterPayloadsByID(self.filteredIdList) print(len(self.filteredPayloadList)) print(len(Globals.payloadList)) #--Save By Objects--# self.filteredObjectDict.clear() for objType, objectTable in self.objectTabDict.items(): rowCnt = objectTable.rowCount() objectsToFilter = [] for rows in range(0, rowCnt): if objectTable.item(rows, 1).checkState() == Qt.Checked: #print(objectTable.item(rows,0).text()) try: objectsToFilter.append( int( re.search('ID: (.*) Name:.*', objectTable.item( rows, 0).text()).group(1))) self.filteredObjectDict[objType] = objectsToFilter #print('Found Regex: '+re.search('ID: (.*) Name:.*',objectTable.item(rows,0).text()).group(1)) except: print('Error when parsing TableRegex...this is okay') self.filterPayloadsByMessage(self.filteredObjectDict) # We filtered the list, now we hide the windows and call the update-function # If the maintainer of the tab did not follow the implementation guide, there is no update-function to call. # We still save the filtered list, so we print a message to show where to find it. self.hide() try: self.parent.filterUpdated(self.filteredIdList, self.filteredPayloadList) except AttributeError: print( 'No corresponding callable function filterUpdated was found, you can access the most recent filteredList via self.snifferFilter.filteredIdList' ) try: Globals.dockDict[self.calledBy].filterUpdated( self.filteredIdList, self.filteredPayloadList) except: print('not global!') ## Check whether the first checkbox was checked and then update the entire ID table to either checked or unchecked state # @param checkBox a checkBox we perform the query on def filterAllIDs(self, checkBox): if self.layoutingComplete == True: if checkBox.column() == 1 and checkBox.row( ) == 0: # We clicked the toggle ID checkbox if checkBox.checkState() == Qt.Checked: rowCnt = self.filterTable.rowCount() for rows in range(0, rowCnt): try: self.filterTable.item(rows, 1).setCheckState(Qt.Checked) except AttributeError: print( 'no items after' + str(rows) + 'found...Maybe this column has less items than' + str(rowCnt) + '?') elif checkBox.checkState() == Qt.Unchecked: rowCnt = self.filterTable.rowCount() for rows in range(0, rowCnt): try: self.filterTable.item(rows, 1).setCheckState( Qt.Unchecked) except AttributeError: print( 'no items after' + str(rows) + 'found...Maybe this column has less items than' + str(rowCnt) + '?') else: print( 'neither checked nor unchecked...should never be here..' ) ## Check whether the first checkbox was checked and then update the entire ObjectIDtable to either checked or unchecked state # @param checkBox a checkBox we perform the query on # @param table the table that is to be updated def filterAllObjectIDs(self, checkBox, table): if (self.objectTabDict): if checkBox.column() == 1 and checkBox.row( ) == 0: # We clicked the toggle ID checkbox if checkBox.checkState() == Qt.Checked: rowCnt = table.rowCount() for rows in range(0, rowCnt): try: table.item(rows, 1).setCheckState(Qt.Checked) except AttributeError: print( 'no items after' + str(rows) + 'found...Maybe this column has less items than' + str(rowCnt) + '?') elif checkBox.checkState() == Qt.Unchecked: rowCnt = table.rowCount() for rows in range(0, rowCnt): try: table.item(rows, 1).setCheckState(Qt.Unchecked) except AttributeError: print( 'no items after' + str(rows) + 'found...Maybe this column has less items than' + str(rowCnt) + '?') else: print( 'neither checked nor unchecked...should never be here..' ) # --- HELPER FUNCTIONS --- # ## Create a defined checkbox within a tableWidgetItem to facilitate filling the table # @return the created checkbox def createCheckBox(self): myCheck = QTableWidgetItem() myCheck.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) myCheck.setCheckState(Qt.Checked) return myCheck ## Create a defined font (bold,underlined) to facilitate filling the table # @return the created font def getFont(self): font = QFont() font.setBold(True) font.setUnderline(True) return font
class ModelGroupsTable(QWidget): EditableAttrs = [ attr for attr in PlotStyles.StyleAttributes if attr in PlotStyles.StyleAttributeOptions ] ColList = 3 ColPlot = 4 ColApply = 5 AttrByCol = dict([(i + 6, attr) for i, attr in enumerate(EditableAttrs)]) def __init__(self, parent, *args): QWidget.__init__(self, parent, *args) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) lo = QVBoxLayout(self) lo.setContentsMargins(0, 0, 0, 0) lo1 = QHBoxLayout() lo.addLayout(lo1) lo1.setContentsMargins(0, 0, 0, 0) lbl = QLabel(QString("<nobr><b>Source groupings:</b></nobr>"), self) lo1.addWidget(lbl, 0) lo1.addStretch(1) # add show/hide button self._showattrbtn = QPushButton(self) self._showattrbtn.setMinimumWidth(256) lo1.addWidget(self._showattrbtn, 0) lo1.addStretch() self._showattrbtn.clicked.connect(self._togglePlotControlsVisibility) # add table self.table = QTableWidget(self) lo.addWidget(self.table) self.table.cellChanged[int, int].connect(self._valueChanged) self.table.setSelectionMode(QTableWidget.NoSelection) # setup basic columns self.table.setColumnCount(6 + len(self.EditableAttrs)) for i, label in enumerate( ("grouping", "total", "selection", "list", "plot", "style")): self.table.setHorizontalHeaderItem(i, QTableWidgetItem(label)) self.table.horizontalHeader().setSectionHidden(self.ColApply, True) # setup columns for editable grouping attributes for i, attr in self.AttrByCol.items(): self.table.setHorizontalHeaderItem( i, QTableWidgetItem(PlotStyles.StyleAttributeLabels[attr])) self.table.horizontalHeader().setSectionHidden(i, True) self.table.verticalHeader().hide() # other internal init self._attrs_shown = False self._togglePlotControlsVisibility() self.model = None self._setting_model = False self._currier = PersistentCurrier() # row of 'selected' grouping self._irow_selgroup = 0 def clear(self): self.table.setRowCount(0) self.model = None # setup mappings from the group.show_plot attribute to check state ShowAttrToCheckState = { PlotStyles.ShowNot: Qt.Unchecked, PlotStyles.ShowDefault: Qt.PartiallyChecked, PlotStyles.ShowAlways: Qt.Checked } CheckStateToShowAttr = dict([(val, key) for key, val in ShowAttrToCheckState.items()]) def _makeCheckItem(self, name, group, attr): item = QTableWidgetItem(name) if group is self.model.defgroup: item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable) item.setCheckState( Qt.Checked if getattr(group.style, attr) else Qt.Unchecked) else: item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable | Qt.ItemIsTristate) item.setCheckState(self.ShowAttrToCheckState[getattr( group.style, attr)]) return item def _updateModel(self, what=SkyModel.UpdateAll, origin=None): if origin is self or not what & (SkyModel.UpdateTags | SkyModel.UpdateGroupStyle): return model = self.model self._setting_model = True # to ignore cellChanged() signals (in valueChanged()) # _item_cb is a dict (with row,col keys) containing the widgets (CheckBoxes ComboBoxes) per each cell self._item_cb = {} # lists of "list" and "plot" checkboxes per each grouping (excepting the default grouping); each entry is an (row,col,item) tuple. # used as argument to self._showControls() self._list_controls = [] self._plot_controls = [] # list of selection callbacks (to which signals are connected) self._callbacks = [] # set requisite number of rows,and start filling self.table.setRowCount(len(model.groupings)) for irow, group in enumerate(model.groupings): self.table.setItem(irow, 0, QTableWidgetItem(group.name)) if group is model.selgroup: self._irow_selgroup = irow # total # source in group: skip for "current" if group is not model.curgroup: self.table.setItem(irow, 1, QTableWidgetItem(str(group.total))) # selection controls: skip for current and selection if group not in (model.curgroup, model.selgroup): btns = QWidget() lo = QHBoxLayout(btns) lo.setContentsMargins(0, 0, 0, 0) lo.setSpacing(0) # make selector buttons (depending on which group we're in) if group is model.defgroup: Buttons = (("+", lambda src, grp=group: True, "select all sources"), ("-", lambda src, grp=group: False, "unselect all sources")) else: Buttons = ( ("=", lambda src, grp=group: grp.func(src), "select only this grouping"), ("+", lambda src, grp=group: src.selected or grp.func(src), "add grouping to selection"), ("-", lambda src, grp=group: src.selected and not grp. func(src), "remove grouping from selection"), ("&&", lambda src, grp=group: src.selected and grp.func(src), "intersect selection with grouping")) lo.addStretch(1) for label, predicate, tooltip in Buttons: btn = QToolButton(btns) btn.setText(label) btn.setMinimumWidth(24) btn.setMaximumWidth(24) btn.setToolTip(tooltip) lo.addWidget(btn) # add callback btn.clicked.connect( self._currier.curry(self.selectSources, predicate)) lo.addStretch(1) self.table.setCellWidget(irow, 2, btns) # "list" checkbox (not for current and selected groupings: these are always listed) if group not in (model.curgroup, model.selgroup): item = self._makeCheckItem("", group, "show_list") self.table.setItem(irow, self.ColList, item) item.setToolTip( """<P>If checked, sources in this grouping will be listed in the source table. If un-checked, sources will be excluded from the table. If partially checked, then the default list/no list setting of "all sources" will be in effect. </P>""") # "plot" checkbox (not for the current grouping, since that's always plotted) if group is not model.curgroup: item = self._makeCheckItem("", group, "show_plot") self.table.setItem(irow, self.ColPlot, item) item.setToolTip( """<P>If checked, sources in this grouping will be included in the plot. If un-checked, sources will be excluded from the plot. If partially checked, then the default plot/no plot setting of "all sources" will be in effect. </P>""") # custom style control # for default, current and selected, this is just a text label if group is model.defgroup: item = QTableWidgetItem("default:") item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) item.setToolTip( """<P>This is the default plot style used for all sources for which a custom grouping style is not selected.</P>""" ) self.table.setItem(irow, self.ColApply, item) elif group is model.curgroup: item = QTableWidgetItem("") item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) item.setToolTip( """<P>This is the plot style used for the highlighted source, if any.</P>""" ) self.table.setItem(irow, self.ColApply, item) elif group is model.selgroup: item = QTableWidgetItem("") item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) item.setToolTip( """<P>This is the plot style used for the currently selected sources.</P>""" ) self.table.setItem(irow, self.ColApply, item) # for the rest, a combobox with custom priorities else: cb = QComboBox() cb.addItems(["default"] + ["custom %d" % p for p in range(1, 10)]) index = max(0, min(group.style.apply, 9)) # dprint(0,group.name,"apply",index) cb.setCurrentIndex(index) cb.activated[int].connect( self._currier.xcurry(self._valueChanged, (irow, self.ColApply))) self.table.setCellWidget(irow, self.ColApply, cb) cb.setToolTip( """<P>This controls whether sources within this group are plotted with a customized plot style. Customized styles have numeric priority; if a source belongs to multiple groups, then the style with the lowest priority takes precedence.<P>""") # attribute comboboxes for icol, attr in self.AttrByCol.items(): # get list of options for this style attribute. If dealing with first grouping (i==0), which is # the "all sources" grouping, then remove the "default" option (which is always first in the list) options = PlotStyles.StyleAttributeOptions[attr] if irow == 0: options = options[1:] # make combobox cb = QComboBox() cb.addItems(list(map(str, options))) # the "label" option is also editable if attr == "label": cb.setEditable(True) try: index = options.index(getattr(group.style, attr)) cb.setCurrentIndex(index) except ValueError: cb.setEditText(str(getattr(group.style, attr))) slot = self._currier.xcurry(self._valueChanged, (irow, icol)) cb.activated[int].connect(slot) cb.editTextChanged['QString'].connect(slot) cb.setEnabled(group is model.defgroup or group.style.apply) self.table.setCellWidget(irow, icol, cb) label = attr if irow: cb.setToolTip( """<P>This is the %s used to plot sources in this group, when a "custom" style for the group is enabled via the style control.<P>""" % label) else: cb.setToolTip( "<P>This is the default %s used for all sources for which a custom style is not specified below.<P>" % label) self.table.resizeColumnsToContents() # re-enable processing of cellChanged() signals self._setting_model = False def setModel(self, model): self.model = model self.model.connect("updated", self._updateModel) self.model.connect("selected", self.updateModelSelection) self._updateModel(SkyModel.UpdateAll) def _valueChanged(self, row, col): """Called when a cell has been edited""" if self._setting_model: return group = self.model.groupings[row] item = self.table.item(row, col) if col == self.ColList: if group is not self.model.defgroup: # tri-state items go from unchecked to checked when user clicks them. Make them partially checked instead. if group.style.show_list == PlotStyles.ShowNot and item.checkState( ) == Qt.Checked: item.setCheckState(Qt.PartiallyChecked) group.style.show_list = self.CheckStateToShowAttr[ item.checkState()] self.model.emitChangeGroupingVisibility(group, origin=self) return elif col == self.ColPlot: if group is not self.model.defgroup: # tri-state items go from unchecked to checked by default. Make them partially checked instead. if group.style.show_plot == PlotStyles.ShowNot and item.checkState( ) == Qt.Checked: item.setCheckState(Qt.PartiallyChecked) group.style.show_plot = self.CheckStateToShowAttr[ item.checkState()] elif col == self.ColApply: group.style.apply = self.table.cellWidget(row, col).currentIndex() # enable/disable editable cells for j in list(self.AttrByCol.keys()): item1 = self.table.item(row, j) if item1: fl = item1.flags() & ~Qt.ItemIsEnabled if group.style.apply: fl |= Qt.ItemIsEnabled item1.setFlags(fl) cw = self.table.cellWidget(row, j) cw and cw.setEnabled(group.style.apply) elif col in self.AttrByCol: cb = self.table.cellWidget(row, col) txt = str(cb.currentText()) attr = self.AttrByCol[col] if txt == "default": setattr(group.style, attr, PlotStyles.DefaultValue) else: setattr(group.style, attr, PlotStyles.StyleAttributeTypes.get(attr, str)(txt)) # all other columns: return so we don't emit a signal else: return # in all cases emit a signal self.model.emitChangeGroupingStyle(group, origin=self) def selectSources(self, predicate, curry=False): """Selects sources according to predicate(src)""" busy = BusyIndicator() for src in self.model.sources: src.selected = predicate(src) self.model.emitSelection(origin=self) busy.reset_cursor() def updateModelSelection(self, nsel, origin=None): """This is called when some other widget changes the set of selected model sources""" self.table.clearSelection() if self.model: self.table.item(self._irow_selgroup, 1).setText(str(nsel)) def _togglePlotControlsVisibility(self): if self._attrs_shown: self._attrs_shown = False self.table.hideColumn(self.ColApply) for col in self.AttrByCol.keys(): self.table.hideColumn(col) self._showattrbtn.setText("Show plot styles >>") else: self._attrs_shown = True self.table.showColumn(self.ColApply) for col in self.AttrByCol.keys(): self.table.showColumn(col) self._showattrbtn.setText("<< Hide plot styles")
class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(WINDOW_TITLE) from pathlib import Path file_name = str(Path(__file__).resolve().parent / 'favicon.ico') icon = QIcon(file_name) self.setWindowIcon(icon) self.tray = QSystemTrayIcon(icon) self.tray.setToolTip(self.windowTitle()) self.tray.activated.connect(self._on_tray_activated) self.tray.show() self.logged_dict = dict() self.pb_refresh = QPushButton('REFRESH') self.pb_refresh.clicked.connect(self.refresh) self.cb_show_log = QCheckBox() self.cb_show_log.setChecked(True) self.log = QPlainTextEdit() self.log.setReadOnly(True) self.log.setWordWrapMode(QTextOption.NoWrap) log_font = self.log.font() log_font.setFamily('Courier New') self.log.setFont(log_font) self.cb_show_log.clicked.connect(self.log.setVisible) self.log.setVisible(self.cb_show_log.isChecked()) header_labels = ['DATE', 'TOTAL LOGGED TIME'] self.table_logged = QTableWidget() self.table_logged.setEditTriggers(QTableWidget.NoEditTriggers) self.table_logged.setSelectionBehavior(QTableWidget.SelectRows) self.table_logged.setSelectionMode(QTableWidget.SingleSelection) self.table_logged.setColumnCount(len(header_labels)) self.table_logged.setHorizontalHeaderLabels(header_labels) self.table_logged.horizontalHeader().setStretchLastSection(True) self.table_logged.itemClicked.connect( self._on_table_logged_item_clicked) header_labels = ['TIME', 'LOGGED', 'JIRA'] self.table_logged_info = QTableWidget() self.table_logged_info.setEditTriggers(QTableWidget.NoEditTriggers) self.table_logged_info.setSelectionBehavior(QTableWidget.SelectRows) self.table_logged_info.setSelectionMode(QTableWidget.SingleSelection) self.table_logged_info.setColumnCount(len(header_labels)) self.table_logged_info.setHorizontalHeaderLabels(header_labels) self.table_logged_info.horizontalHeader().setSectionResizeMode( 1, QHeaderView.ResizeToContents) self.table_logged_info.horizontalHeader().setStretchLastSection(True) self.table_logged_info.itemDoubleClicked.connect( self._on_table_logged_info_item_double_clicked) main_layout = QVBoxLayout() central_widget = QWidget() central_widget.setLayout(main_layout) self.setCentralWidget(central_widget) self.pb_refresh.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)) h_layout = QHBoxLayout() h_layout.addWidget(self.pb_refresh) h_layout.addWidget(self.cb_show_log) layout_table_widget = QVBoxLayout() layout_table_widget.setContentsMargins(0, 0, 0, 0) layout_table_widget.addWidget(self.table_logged) layout_table_widget.addWidget(self.table_logged_info) table_widget = QWidget() table_widget.setLayout(layout_table_widget) splitter = QSplitter(Qt.Horizontal) splitter.addWidget(table_widget) splitter.addWidget(self.log) main_layout.addLayout(h_layout) main_layout.addWidget(splitter) def _fill_tables(self, xml_data: bytes): import io buffer_io = io.StringIO() from contextlib import redirect_stdout try: with redirect_stdout(buffer_io): print(len(xml_data), repr(xml_data[:50])) # Структура документа -- xml self.logged_dict = parse_logged_dict(xml_data) print(self.logged_dict) if not self.logged_dict: return import json print( json.dumps(self.logged_dict, indent=4, ensure_ascii=False)) print() logged_list = get_logged_list_by_now_utc_date(self.logged_dict) logged_total_seconds = get_logged_total_seconds(logged_list) logged_total_seconds_str = seconds_to_str(logged_total_seconds) print('entry_logged_list:', logged_list) print('today seconds:', logged_total_seconds) print('today time:', logged_total_seconds_str) print() # Для красоты выводим результат в табличном виде lines = [] # Удаление строк таблицы while self.table_logged.rowCount(): self.table_logged.removeRow(0) for i, (date_str, logged_list) in enumerate( get_sorted_logged(self.logged_dict)): total_seconds = get_logged_total_seconds(logged_list) total_seconds_str = seconds_to_str(total_seconds) row = date_str, total_seconds_str, total_seconds lines.append(row) self.table_logged.setRowCount( self.table_logged.rowCount() + 1) self.table_logged.setItem(i, 0, QTableWidgetItem(date_str)) item = QTableWidgetItem(total_seconds_str) item.setToolTip('Total seconds: {}'.format(total_seconds)) self.table_logged.setItem(i, 1, item) self.table_logged.setCurrentCell(0, 0) self.table_logged.setFocus() self._on_table_logged_item_clicked( self.table_logged.currentItem()) # Список строк станет списком столбцов, у каждого столбца подсчитается максимальная длина max_len_columns = [ max(map(len, map(str, col))) for col in zip(*lines) ] # Создание строки форматирования: [30, 14, 5] -> "{:<30} | {:<14} | {:<5}" my_table_format = ' | '.join('{:<%s}' % max_len for max_len in max_len_columns) for line in lines: print(my_table_format.format(*line)) finally: text = buffer_io.getvalue() self.log.setPlainText(text) print(text) def refresh(self): progress_dialog = QProgressDialog(self) thread = RunFuncThread(func=get_rss_jira_log) thread.run_finished.connect(self._fill_tables) thread.run_finished.connect(progress_dialog.close) thread.start() progress_dialog.setWindowTitle('Please wait...') progress_dialog.setLabelText(progress_dialog.windowTitle()) progress_dialog.setRange(0, 0) progress_dialog.exec() from datetime import datetime self.setWindowTitle(WINDOW_TITLE + ". Last refresh date: " + datetime.now().strftime('%d/%m/%Y %H:%M:%S')) def _on_table_logged_item_clicked(self, item: QTableWidgetItem): # Удаление строк таблицы while self.table_logged_info.rowCount(): self.table_logged_info.removeRow(0) row = item.row() date_str = self.table_logged.item(row, 0).text() logged_list = self.logged_dict[date_str] logged_list = reversed(logged_list) for i, logged in enumerate(logged_list): self.table_logged_info.setRowCount( self.table_logged_info.rowCount() + 1) self.table_logged_info.setItem(i, 0, QTableWidgetItem(logged['time'])) self.table_logged_info.setItem( i, 1, QTableWidgetItem(logged['logged_human_time'])) item = QTableWidgetItem(logged['jira_id']) item.setToolTip(logged['jira_title']) self.table_logged_info.setItem(i, 2, item) def _on_table_logged_info_item_double_clicked(self, item: QTableWidgetItem): row = item.row() jira_id = self.table_logged_info.item(row, 2).text() url = 'https://jira.compassplus.ru/browse/' + jira_id import webbrowser webbrowser.open(url) def _on_tray_activated(self, reason): self.setVisible(not self.isVisible()) if self.isVisible(): self.showNormal() self.activateWindow() def changeEvent(self, event: QEvent): if event.type() == QEvent.WindowStateChange: # Если окно свернули if self.isMinimized(): # Прячем окно с панели задач QTimer.singleShot(0, self.hide)