class LogSParserMain(QMainWindow): """ This is the main class in the application. It's responsible for displaying the log data in a tabular format as well as allowing the user to filter the logs displayed. """ per_column_filter_out_set_list = list() per_column_filter_in_set_list = list() header = list() table_conditional_formatting_config = None def __init__(self): QMainWindow.__init__(self) self.graph_window_dict = {} self.menuFilter = None self.proxy_model = None self.table_data = None self.user_interface = Ui_Siraj() self.user_interface.setupUi(self) self.user_interface.mnuActionOpen.triggered.connect(self.menu_open_file) self.user_interface.mnuActionLoadConfigs.triggered.connect(self.menu_load_configs) self.user_interface.mnuActionExit.triggered.connect(self.menu_exit) self.user_interface.mnuActionAbout.triggered.connect(self.menu_about) self.user_interface.centralwidget.setLayout(self.user_interface.verticalLayout) self.user_interface.dckSourceContents.setLayout(self.user_interface.lytSource) self.user_interface.tblLogData.doubleClicked.connect(self.cell_double_clicked) self.user_interface.tblLogData.clicked.connect(self.cell_left_clicked) self.user_interface.tblLogData.keyPressEvent = self.cell_key_pressed self.user_interface.tblLogData.setContextMenuPolicy(Qt.CustomContextMenu) self.user_interface.tblLogData.customContextMenuRequested.connect(self.cell_right_clicked) self.user_interface.txtSourceFile.setReadOnly(True) self.is_table_visible = True self.is_source_visible = True self.user_interface.tblLogData.resizeColumnsToContents() self.user_interface.tblLogData.resizeRowsToContents() self.setup_context_menu() self.setup_toolbars() self.clipboard = QApplication.clipboard() self.is_filtering_mode_out = True self.matched_row_list = [] self.search_criteria_updated = True self.case_sensitive_search_type = Qt.CaseInsensitive self.is_wrap_search = True self.is_match_whole_word = False self.graph_marker_list = [] self.user_interface.tblLogData.setAcceptDrops(False) self.setAcceptDrops(True) self.load_configuration_file() self.toggle_source_view() def setup_toolbars(self): source_toolbar = self.addToolBar('SourceToolbar') self.user_interface.tbrActionToggleSourceView = QAction('C/C++', self) self.user_interface.tbrActionToggleSourceView.triggered.connect(self.toggle_source_view) self.user_interface.tbrActionToggleSourceView.setToolTip("Toggle source code view") self.user_interface.tbrActionToggleSourceView.setCheckable(True) self.user_interface.tbrActionToggleSourceView.setChecked(True) source_toolbar.addAction(self.user_interface.tbrActionToggleSourceView) search_toolbar = self.addToolBar("SearchToolbar") search_toolbar.setAllowedAreas(Qt.TopToolBarArea | Qt.BottomToolBarArea) self.ledSearchBox = QLineEdit() self.ledSearchBox.textChanged.connect(self.invalidate_search_criteria) self.ledSearchBox.keyPressEvent = self.search_box_key_pressed search_toolbar.addWidget(self.ledSearchBox) tbrActionPrevSearchMatch = QAction('<<', self) tbrActionPrevSearchMatch.triggered.connect(functools.partial(self.select_search_match, False)) tbrActionPrevSearchMatch.setToolTip("Go to previous search match") tbrActionNextSearchMatch = QAction('>>', self) tbrActionNextSearchMatch.triggered.connect(functools.partial(self.select_search_match, True)) tbrActionNextSearchMatch.setToolTip("Go to next search match") tbrActionIgnoreCase = QAction('Ignore Case', self) tbrActionIgnoreCase.setCheckable(True) tbrActionIgnoreCase.setChecked(True) tbrActionIgnoreCase.triggered.connect(self.set_search_case_sensitivity, tbrActionIgnoreCase.isChecked()) tbrActionIgnoreCase.setToolTip("Ignore case") tbrActionWrapSearch = QAction('Wrap Search', self) tbrActionWrapSearch.setCheckable(True) tbrActionWrapSearch.setChecked(True) tbrActionWrapSearch.triggered.connect(self.set_search_wrap, tbrActionWrapSearch.isChecked()) tbrActionWrapSearch.setToolTip("Wrap Search") tbrActionMatchWholeWord = QAction('Match Whole Word', self) tbrActionMatchWholeWord.setCheckable(True) tbrActionMatchWholeWord.setChecked(False) tbrActionMatchWholeWord.triggered.connect(self.set_match_whole_word, tbrActionMatchWholeWord.isChecked()) tbrActionMatchWholeWord.setToolTip("Match Whole Word") search_toolbar.addAction(tbrActionPrevSearchMatch) search_toolbar.addAction(tbrActionNextSearchMatch) search_toolbar.addAction(tbrActionIgnoreCase) search_toolbar.addAction(tbrActionMatchWholeWord) search_toolbar.addAction(tbrActionWrapSearch) def set_search_case_sensitivity(self, ignore_case): self.invalidate_search_criteria() if(ignore_case): self.case_sensitive_search_type = Qt.CaseInsensitive else: self.case_sensitive_search_type = Qt.CaseSensitive def set_search_wrap(self, wrap_search): self.invalidate_search_criteria() self.is_wrap_search = wrap_search def set_match_whole_word(self, match_whole_word): self.invalidate_search_criteria() self.is_match_whole_word = match_whole_word def invalidate_search_criteria(self): self.search_criteria_updated = True; self.matched_row_list.clear() def get_matched_row_list(self, key_column, search_criteria, case_sensitivity): search_proxy = QSortFilterProxyModel() search_proxy.setSourceModel(self.user_interface.tblLogData.model()) search_proxy.setFilterCaseSensitivity(case_sensitivity) search_proxy.setFilterKeyColumn(key_column) if self.is_match_whole_word: search_criteria = r"\b{}\b".format(search_criteria) search_proxy.setFilterRegExp(search_criteria) matched_row_list = [] for proxy_row in range(search_proxy.rowCount()): match_index = search_proxy.mapToSource(search_proxy.index(proxy_row, key_column)) matched_row_list.append(match_index.row()) self.search_criteria_updated = False return matched_row_list def select_search_match(self, is_forward): selected_indexes = self.get_selected_indexes() if(len(selected_indexes) == 0): self.display_message_box( "No selection", "Please select a cell from the column you want to search", QMessageBox.Warning) else: index = self.get_selected_indexes()[0] row = index.row() column = index.column() search_criteria = self.ledSearchBox.text() if(self.search_criteria_updated): self.matched_row_list = self.get_matched_row_list(column, search_criteria, self.case_sensitive_search_type) if(len(self.matched_row_list) > 0): is_match_found = False if(is_forward): matched_row_index = bisect_left(self.matched_row_list, row) if((matched_row_index < len(self.matched_row_list) - 1)): if(self.matched_row_list[matched_row_index] == row): matched_row_index += 1 is_match_found = True elif(self.is_wrap_search): matched_row_index = 0 is_match_found = True else: matched_row_index = bisect_right(self.matched_row_list, row) if(matched_row_index > 0): matched_row_index -= 1 if((matched_row_index > 0)): if((self.matched_row_list[matched_row_index] == row)): matched_row_index -= 1 is_match_found = True elif(self.is_wrap_search): matched_row_index = len(self.matched_row_list) - 1 is_match_found = True if(is_match_found): self.select_cell_by_row_and_column(self.matched_row_list[matched_row_index], column) else: self.display_message_box( "No match found", 'Search pattern "{}" was not found in column "{}"'.format(search_criteria, self.header[column]), QMessageBox.Warning) def reset_per_config_file_data(self): self.graph_window_dict.clear() self.reset_per_log_file_data() self.table_data = None self.table_model = None self.proxy_model = None def load_configuration_file(self, config_file_path="siraj_configs.json"): self.reset_per_config_file_data() self.config = LogSParserConfigs(config_file_path) self.log_file_full_path = self.config.get_config_item("log_file_full_path") self.log_trace_regex_pattern = self.config.get_config_item("log_row_pattern") self.time_stamp_column = self.config.get_config_item("time_stamp_column_number_zero_based") self.user_data_column_zero_based = self.config.get_config_item("user_data_column_zero_based") self.external_editor_configs = self.config.get_config_item("external_editor_configs") cross_reference_configs = self.config.get_config_item("source_cross_reference_configs") self.file_column = cross_reference_configs["file_column_number_zero_based"] self.file_column_pattern = cross_reference_configs["file_column_pattern"] self.line_column = cross_reference_configs["line_column_number_zero_based"] self.line_column_pattern = cross_reference_configs["line_column_pattern"] self.graph_configs = self.config.get_config_item("graph_configs") self.root_source_path_prefix = cross_reference_configs["root_source_path_prefix"] self.syntax_highlighting_style = cross_reference_configs["pygments_syntax_highlighting_style"] self.table_conditional_formatting_config = self.config.get_config_item("table_conditional_formatting_configs") self.load_log_file(self.log_file_full_path) def load_graphs(self, graph_configs, table_data): pg.setConfigOption('background', QColor("white")) pg.setConfigOption('foreground', QColor("black")) pg.setConfigOptions(antialias=True) window_dict = graph_configs["window_dict"] series_list = [] for window_name in window_dict: window_handle = pg.GraphicsWindow(title=window_name) self.graph_window_dict[window_name] = window_handle window_handle.show() plot_dict = window_dict[window_name]["plot_dict"] first_plot_name_in_the_window = "" for plot_name in plot_dict: plot_row = plot_dict[plot_name]["row"] # plot_column = plot_dict[plot_name]["column"] # plot_row_span = plot_dict[plot_name]["row_span"] # plot_column_span = plot_dict[plot_name]["column_span"] plot_handle = window_handle.addPlot( name=plot_name, title=plot_name, row=plot_row, col=1,#plot_column, rowspan=1,#plot_row_span, colspan=1)#plot_column_span) plot_handle.addLegend() if first_plot_name_in_the_window == "": first_plot_name_in_the_window = plot_name plot_handle.setXLink(first_plot_name_in_the_window) marker = pg.InfiniteLine(angle=90, movable=False, pen=pg.mkPen(width=1, color=QColor("red"))) plot_handle.addItem(marker, ignoreBounds=True) self.graph_marker_list.append(marker) plot_handle.scene().sigMouseClicked.connect(functools.partial(self.graph_mouse_clicked, plot_handle)) plot_handle.scene().setClickRadius(50) series_dict = plot_dict[plot_name]["series_dict"] for series_name in series_dict: series_symbol = series_dict[series_name]["symbol"] series_color = series_dict[series_name]["color"] series_pattern = series_dict[series_name]["pattern"] series_list.append((series_name, series_symbol, series_color, series_pattern, [], [], plot_handle)) for row_number, row_data in enumerate(table_data): for (series_name, series_symbol, series_color, series_pattern, x_point_list, y_point_list, plot_handle) in series_list: cell_to_match = row_data[self.user_data_column_zero_based] m = re.search(series_pattern, cell_to_match) if m is not None: x_point_list.append(row_number) y_point_list.append(int(m.group(1))) for (series_name, series_symbol, series_color, series_pattern, x_point_list, y_point_list, plot_handle) in series_list: plot_handle.plot( x_point_list, y_point_list, pen=pg.mkPen(width=1, color=QColor(series_color)), symbol=series_symbol, symbolPen='w', symbolBrush=QColor(series_color), name=series_name) # graphs = list(sorted(graph_configs.keys(), key=lambda k: graph_configs[k]["index"])) # graph_data = [([], [],) for _ in graphs] # # self.graph_marker_list = [] # # for row_number, row_data in enumerate(table_data): # for graph_number, graph_name in enumerate(graphs): # cell_to_match = row_data[graph_configs[graph_name]["column"]] # m = re.search(graph_configs[graph_name]["pattern"], cell_to_match) # if (m is not None): # graph_data[graph_number][0].append(row_number) # X-Axis value # graph_data[graph_number][1].append(int(m.group(1))) # Y-Axis value # # for graph in graphs: # window = None # wnd = graph_configs[graph]["window"] # if (wnd in self.graph_window_dict): # window = self.graph_window_dict[wnd] # window.clear() # # is_new_window = False # first_plot_name = None # for graph_number, graph in enumerate(graphs): # window = None # wnd = graph_configs[graph]["window"] # if (wnd in self.graph_window_dict): # window = self.graph_window_dict[wnd] # is_new_window = False # else: # is_new_window = True # window = pg.GraphicsWindow(title=wnd) # # self.graph_window_dict[wnd] = window # # p = window.addPlot(name=graph, title=graph) # # p.plot(graph_data[graph_number][0], # graph_data[graph_number][1], # pen=pg.mkPen(width=1, color=QColor(graph_configs[graph]["color"])), # symbol=graph_configs[graph]["symbol"], symbolPen='w', # symbolBrush=QColor(graph_configs[graph]["color"]), name=graph) # p.showGrid(x=True, y=True) # if first_plot_name == None: # first_plot_name = graph # p.setXLink(first_plot_name) # marker = pg.InfiniteLine(angle=90, movable=False) # p.addItem(marker, ignoreBounds=True) # self.graph_marker_list.append(marker) # p.scene().sigMouseClicked.connect(functools.partial(self.graph_mouse_clicked, p)) # # window.nextRow() def graph_mouse_clicked(self, plt, evt): point = plt.vb.mapSceneToView(evt.scenePos()) self.select_cell_by_row_and_column(int(round(point.x())), self.user_data_column_zero_based) self.update_graph_markers() def setup_context_menu(self): self.menuFilter = QMenu(self) self.hide_action = QAction('Hide selected values', self) self.show_only_action = QAction('Show only selected values', self) self.clear_all_filters_action = QAction('Clear all filters', self) self.copy_selection_action = QAction('Copy selection', self) self.unhide_menu = QMenu('Unhide item from selected column', self.menuFilter) self.hide_action.triggered.connect(self.hide_rows_based_on_selected_cells) self.show_only_action.triggered.connect(self.show_rows_based_on_selected_cells) self.clear_all_filters_action.triggered.connect(self.clear_all_filters) self.copy_selection_action.triggered.connect(self.prepare_clipboard_text) self.menuFilter.addAction(self.hide_action) self.menuFilter.addMenu(self.unhide_menu) self.menuFilter.addAction(self.show_only_action) self.menuFilter.addAction(self.clear_all_filters_action) self.menuFilter.addSeparator() self.menuFilter.addAction(self.copy_selection_action) self.hide_action.setShortcut('Ctrl+H') self.show_only_action.setShortcut('Ctrl+O') self.clear_all_filters_action.setShortcut('Ctrl+Del') self.copy_selection_action.setShortcut("Ctrl+C") def toggle_source_view(self): self.is_source_visible = not self.is_source_visible self.user_interface.tbrActionToggleSourceView.setChecked(self.is_source_visible) self.user_interface.dckSource.setVisible(self.is_source_visible) logging.info("Source view is now {}".format("Visible" if self.is_source_visible else "Invisible")) def display_message_box(self, title, message, icon): """ Show the about box. """ message_box = QMessageBox(self); message_box.setWindowTitle(title); message_box.setTextFormat(Qt.RichText); message_box.setText(message) message_box.setIcon(icon) message_box.exec_() def menu_about(self): """ Show the about box. """ about_text = """ Copyright 2015 Mohamed Galal El-Din Ebrahim (<a href="mailto:[email protected]">[email protected]</a>) <br> <br> siraj is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License. <br> <br> siraj is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. <br> <br> You should have received a copy of the GNU General Public License along with siraj. If not, see <a href="http://www.gnu.org/licenses">http://www.gnu.org/licenses</a>. """ self.display_message_box("About", about_text, QMessageBox.Information) def menu_exit(self): """ Handles the exit menu clicked event. """ exit(0) def menu_open_file(self): """ Handles the open menu clicked event. """ self.log_file_full_path = QFileDialog.getOpenFileName( self, 'Open Log File', os.getcwd()) if(self.log_file_full_path != ''): self.load_log_file(self.log_file_full_path) def menu_load_configs(self): """ Loads a new configuration file. """ self.config_file_full_path = QFileDialog.getOpenFileName( self, 'Open Config File', os.getcwd()) if(self.config_file_full_path != ''): self.load_configuration_file(self.config_file_full_path) def reset_per_log_file_data(self): self.invalidate_search_criteria() def load_log_file(self, log_file_full_path): """ Loads the given log file into the table. """ self.reset_per_log_file_data() if (log_file_full_path == ""): pass elif (os.path.isfile(log_file_full_path)): with open(log_file_full_path, "r") as log_file_handle: log_file_content_lines = log_file_handle.read().splitlines() pattern = re.compile(self.log_trace_regex_pattern) self.table_data = [] most_recent_valid_table_entry = [] for line in log_file_content_lines: m = pattern.match(line) if(m is not None): most_recent_valid_table_entry = [group.strip() for group in m.groups()] self.table_data.append(list(most_recent_valid_table_entry)) else: if(self.user_data_column_zero_based != -1): temp_list = list(most_recent_valid_table_entry) temp_list[self.user_data_column_zero_based] = line self.table_data.append(temp_list) m = re.search(self.log_trace_regex_pattern, log_file_content_lines[1]) self.header = [group_name for group_name in sorted(m.groupdict().keys(), key=lambda k: m.start(k))] self.table_model = MyTableModel(self.table_data, self.header, self.table_conditional_formatting_config, self) logging.info("Headers: %s", self.header) logging.info("%s has %d lines", self.log_file_full_path, len(self.table_data)) self.proxy_model = MySortFilterProxyModel(self) self.proxy_model.setSourceModel(self.table_model) self.user_interface.tblLogData.setModel(self.proxy_model) if(len(self.per_column_filter_out_set_list) == 0): self.per_column_filter_out_set_list = [set() for column in range(len(self.table_data[0]))] if(len(self.per_column_filter_in_set_list) == 0): self.per_column_filter_in_set_list = [set() for column in range(len(self.table_data[0]))] self.extract_column_dictionaries(self.header, self.table_data) self.load_graphs(self.graph_configs, self.table_data) self.setWindowTitle("Siraj | {}".format(log_file_full_path)) self.select_cell_by_row_and_column(0, self.user_data_column_zero_based) else: self.display_message_box( "File not Found!", "File <b>`{}`</b> was not found. You can either: <br><br>1. Open a log file via the File menu. Or<br>2. Drag a log file from the system and drop it into the application".format(log_file_full_path), QMessageBox.Critical) def extract_column_dictionaries(self, header_vector_list, data_matrix_list): """ This function extracts a dictionary of dictionaries The extracted is a dictionary of columns where key is the column name, and the data is another dictionary. The inner dictionary has a key equal to a specific cell value of the current column, and the value is a list of row number where this value appeared in. This will be used to provide quick navigation through the log. """ column_count = len(header_vector_list) self.columns_dict = {} for column, column_name in enumerate(header_vector_list): self.columns_dict[column] = {} for row, log in enumerate(data_matrix_list): for column, field in enumerate(log): if(log[column] not in self.columns_dict[column]): self.columns_dict[column][log[column]] = [] self.columns_dict[column][log[column]].append(row) def cell_left_clicked(self, index): """ Handles the event of clicking on a table cell. If the clicked column was the the column that contain the source file:line information from the log, the function also populate the the EditView with the source file contents with a marker highlighting the line. This is only done if the source view is visible. """ index = self.proxy_model.mapToSource(index) if(self.is_source_visible): logging.info("cell[%d][%d] = %s", index.row(), index.column(), index.data()) row = index.row() file_matcher = re.search(self.file_column_pattern, self.table_data[row][self.file_column]) line_matcher = re.search(self.line_column_pattern, self.table_data[row][self.line_column]) if((file_matcher is not None) and (line_matcher is not None)): file = file_matcher.group(1) line = line_matcher.group(1) full_path = "{}{}".format(self.root_source_path_prefix, file.strip()) self.load_source_file(full_path, line) self.user_interface.tblLogData.setFocus() self.update_status_bar() self.update_graph_markers() def load_source_file(self, file, line): code = open(file).read() lexer = get_lexer_for_filename(file) formatter = HtmlFormatter( linenos = True, full = True, style = self.syntax_highlighting_style, hl_lines = [line]) result = highlight(code, lexer, formatter) self.user_interface.txtSourceFile.setHtml(result) text_block = self.user_interface.txtSourceFile.document().findBlockByLineNumber(int(line)) text_cursor = self.user_interface.txtSourceFile.textCursor() text_cursor.setPosition(text_block.position()) self.user_interface.txtSourceFile.setTextCursor(text_cursor) self.user_interface.txtSourceFile.ensureCursorVisible() def get_selected_indexes(self): """ Returns a list of the currently selected indexes mapped to the source numbering. mapToSource is needed to retrive the actual row number regardless of whether filtering is applied or not. """ return [self.proxy_model.mapToSource(index) for index in self.user_interface.tblLogData.selectedIndexes()] def update_status_bar(self): """ Updates the status bar with relevant information """ selected_indexes = self.get_selected_indexes() if(len(selected_indexes) == 1): selected_cell_index = selected_indexes[0] number_of_occurances = len(self.columns_dict[selected_cell_index.column()][selected_cell_index.data()]) self.user_interface.statusbar.showMessage( '["{}"] occurred {} time(s) ~ {}%'.format( selected_cell_index.data(), number_of_occurances, number_of_occurances * 100 // len(self.table_data))) elif(len(selected_indexes) == 2): row_1 = selected_indexes[0].row() row_2 = selected_indexes[1].row() time_stamp1 = float(self.table_data[row_1][self.time_stamp_column]) time_stamp2 = float(self.table_data[row_2][self.time_stamp_column]) self.user_interface.statusbar.showMessage("Time difference = {}".format(abs(time_stamp2 - time_stamp1))) else: self.user_interface.statusbar.showMessage("") def cell_right_clicked(self, point): """ Handle the event of right-clicking on a table cell. This function is responsible for showing the context menu for the user to choose from. """ index = self.proxy_model.mapToSource( self.user_interface.tblLogData.indexAt(point)) logging.debug("Cell[%d, %d] was right-clicked. Contents = %s", index.row(), index.column(), index.data()) self.right_clicked_cell_index = index self.populate_unhide_context_menu(index.column()) self.prepare_clipboard_text() self.menuFilter.popup(QCursor.pos()) def populate_unhide_context_menu(self, column): self.unhide_menu.clear() if(self.is_filtering_mode_out): filtered_out_set = self.per_column_filter_out_set_list[column] else: filtered_out_set = set(self.columns_dict[column].keys()) - self.per_column_filter_in_set_list[column] if(len(filtered_out_set) > 0): self.unhide_menu.setEnabled(True) for filtered_string in filtered_out_set: temp_action = QAction(filtered_string, self.unhide_menu) temp_action.triggered.connect(functools.partial(self.unhide_selected_rows_only_based_on_column, self.right_clicked_cell_index.column(), filtered_string)) self.unhide_menu.addAction(temp_action) else: self.unhide_menu.setEnabled(False) def cell_double_clicked(self, index): """ Handles the event of double-clicking on a table cell. If the double clicked cell was at the column of file:line, the function triggers external text editor (currently this is gedit on Linux) and make it point on the corresponding line. """ index = self.proxy_model.mapToSource(index) logging.info("cell[%d][%d] = %s", index.row(), index.column(), index.data()) row = index.row() file_matcher = re.search(self.file_column_pattern, self.table_data[row][self.file_column]) line_matcher = re.search(self.line_column_pattern, self.table_data[row][self.line_column]) if((file_matcher is not None) and (line_matcher is not None)): file = file_matcher.group(1) line = line_matcher.group(1) full_path = "{}{}".format(self.root_source_path_prefix, file.strip()) logging.info("Using external editor (gedit) to open %s at line %s", file, line) editor = self.external_editor_configs["editor"] editor_command_format = self.external_editor_configs["editor_command_format"] editor_command = editor_command_format.format( editor_executable=editor, line_number=line, file_name=full_path) call(editor_command, shell=True) self.user_interface.tblLogData.setFocus() self.update_status_bar() def search_box_key_pressed(self, q_key_event): key = q_key_event.key() if (key in [Qt.Key_Enter, Qt.Key_Return]): if(Qt.ShiftModifier == (int(q_key_event.modifiers()) & (Qt.ShiftModifier))): self.select_search_match(False) else: self.select_search_match(True) else: QLineEdit.keyPressEvent(self.ledSearchBox, q_key_event) def cell_key_pressed(self, q_key_event): """ Handles the event of pressing a keyboard key while on the table. """ logging.warning("A key was pressed!!!") key = q_key_event.key() logging.info("Key = {}".format(key)) if(Qt.ControlModifier == (int(q_key_event.modifiers()) & (Qt.ControlModifier))): if key == Qt.Key_Delete: logging.info("Delete key pressed while in the table. Clear all filters") self.clear_all_filters() elif key == Qt.Key_H: self.hide_rows_based_on_selected_cells() elif key == Qt.Key_O: self.show_rows_based_on_selected_cells() elif key == Qt.Key_Up: # Jump to previous match selected_indexes = self.get_selected_indexes() if(len(selected_indexes) == 1): self.go_to_prev_match(selected_indexes[0]) elif key == Qt.Key_Down: # Jump to next match selected_indexes = self.get_selected_indexes() if(len(selected_indexes) == 1): self.go_to_next_match(selected_indexes[0]) elif key == Qt.Key_PageUp: selected_indexes = self.get_selected_indexes() if(len(selected_indexes) == 1): prev_bookmark_index = self.table_model.getPrevBookmarkIndex(selected_indexes[0]) if(prev_bookmark_index is not None): self.select_cell_by_index(prev_bookmark_index) elif key == Qt.Key_PageDown: selected_indexes = self.get_selected_indexes() if(len(selected_indexes) == 1): next_bookmark_index = self.table_model.getNextBookmarkIndex(selected_indexes[0]) if(next_bookmark_index is not None): self.select_cell_by_index(next_bookmark_index) elif key == Qt.Key_C: selected_indexes = self.get_selected_indexes() self.prepare_clipboard_text() elif key == Qt.Key_B: if(Qt.ShiftModifier == (int(q_key_event.modifiers()) & (Qt.ShiftModifier))): self.table_model.clearAllBookmarks() else: selected_indexes = self.get_selected_indexes() self.table_model.toggleBookmarks(selected_indexes) elif key == Qt.Key_Left: self.select_search_match(is_forward=False) elif key == Qt.Key_Right: self.select_search_match(is_forward=True) elif key == Qt.Key_Home: self.select_cell_by_row_and_column(0, 0); elif key == Qt.Key_End: self.select_cell_by_row_and_column(self.table_model.rowCount(None) - 1, 0); elif key == Qt.Key_F5: self.load_log_file(self.log_file_full_path) else: QTableView.keyPressEvent(self.user_interface.tblLogData, q_key_event) self.update_graph_markers() def update_graph_markers(self): selected_indexes = self.get_selected_indexes() if (len(selected_indexes) == 1): for marker in self.graph_marker_list: marker.setPos(selected_indexes[0].row()) def prepare_clipboard_text(self): """ Copy the cell content to the clipboard if a single cell is selected. Or Copy the whole rows if cells from multiple rows are selected. """ selected_indexes = self.get_selected_indexes() if(len(selected_indexes) == 0): clipboard_text = "" elif(len(selected_indexes) == 1): clipboard_text = self.user_interface.tblLogData.currentIndex().data() else: unique_rows_set = set([index.row() for index in sorted(selected_indexes)]) row_text_list = [str(row) + "," + ",".join([self.proxy_model.index(row, column, QModelIndex()).data() for column in range(self.proxy_model.columnCount())]) for row in sorted(unique_rows_set)] clipboard_text = "\n".join(row_text_list) self.clipboard.setText(clipboard_text) def get_index_by_row_and_column(self, row, column): """ Get the table index value by the given row and column """ index = self.table_model.createIndex(row, column) index = self.proxy_model.mapFromSource(index) return index def select_cell_by_row_and_column(self, row, column): """ Select the cell identified by the given row and column and scroll the table view to make that cell in the middle of the visible part of the table. """ self.user_interface.tblLogData.clearSelection() index = self.get_index_by_row_and_column(row, column) self.user_interface.tblLogData.setCurrentIndex(index) self.user_interface.tblLogData.scrollTo(index, hint = QAbstractItemView.PositionAtCenter) self.user_interface.tblLogData.setFocus() self.update_status_bar() def select_cell_by_index(self, index): """ Select a cell at the given index. """ self.user_interface.tblLogData.clearSelection() index = self.proxy_model.mapFromSource(index) self.user_interface.tblLogData.setCurrentIndex(index) self.user_interface.tblLogData.scrollTo(index, hint = QAbstractItemView.PositionAtCenter) self.user_interface.tblLogData.setFocus() self.update_status_bar() def go_to_prev_match(self, selected_cell): """ Go to the prev cell that matches the currently selected cell in the same column """ matches_list = self.columns_dict[selected_cell.column()][selected_cell.data()] index = matches_list.index(selected_cell.row()) if(index > 0): new_row = matches_list[index - 1] self.select_cell_by_row_and_column(new_row, selected_cell.column()) def go_to_next_match(self, selected_cell): """ Go to the prev cell that matches the currently selected cell in the same column """ matches_list = self.columns_dict[selected_cell.column()][selected_cell.data()] index = matches_list.index(selected_cell.row()) if(index < (len(matches_list) - 1)): new_row = matches_list[index + 1] self.select_cell_by_row_and_column(new_row, selected_cell.column()) def get_top_left_selected_row_index(self): """ This function return the top-left selected index from the selected list. It's used for example to anchor the table view around the top-left selected cell following any change in the visible cells due to filtering """ top_left_index = None selected_indexes = self.get_selected_indexes() if(len(selected_indexes) > 0): selected_indexes = self.get_selected_indexes() top_left_index = selected_indexes[0] row = top_left_index.row() column = top_left_index.column() for index in selected_indexes[1:]: if((index.row() < row) and (index.column() < column)): row = index.row() column = index.column() top_left_index = index return top_left_index def clear_all_filters(self): """ Clears all the current filter and return the table to its initial view. """ top_selected_index = self.get_top_left_selected_row_index() self.per_column_filter_out_set_list = [set() for column in range(len(self.table_data[0]))] self.per_column_filter_in_set_list = [set() for column in range(len(self.table_data[0]))] self.apply_filter(is_filtering_mode_out = True) if(top_selected_index != None): self.select_cell_by_index(top_selected_index) self.update_status_bar() def hide_rows_based_on_selected_cells(self): """ Hides the selected rows and any other rows with matching data. """ selected_indexes = self.get_selected_indexes() for index in selected_indexes: column = index.column() self.per_column_filter_out_set_list[column].add(index.data()) new_selected_row = None min_selected_row = selected_indexes[0].row() max_selected_row = selected_indexes[-1].row() if(min_selected_row != 0): new_selected_row = min_selected_row - 1 elif(max_selected_row != self.table_model.columnCount(None)): new_selected_row = max_selected_row + 1 self.apply_filter(is_filtering_mode_out=True) self.select_cell_by_row_and_column(new_selected_row, selected_indexes[0].column()) self.update_status_bar() def show_rows_based_on_selected_cells(self): """ Shows the selected rows and any other rows with matching data only. """ selected_indexes = self.get_selected_indexes() self.per_column_filter_in_set_list = [set() for column in range(len(self.table_data[0]))] for index in selected_indexes: column = index.column() self.per_column_filter_in_set_list[column].add(index.data()) self.apply_filter(is_filtering_mode_out=False) self.update_status_bar() def unhide_selected_rows_only_based_on_column(self, filter_column, filtered_out_string): """ Unhides the selected rows and any other rows with matching data. The filtering works on one column only. """ top_selected_index = self.get_top_left_selected_row_index() if(self.is_filtering_mode_out): self.per_column_filter_out_set_list[filter_column].remove(filtered_out_string) else: self.per_column_filter_in_set_list[filter_column].add(filtered_out_string) logging.debug("Unhiding: %s", filtered_out_string) self.apply_filter(self.is_filtering_mode_out) if(top_selected_index != None): self.select_cell_by_index(top_selected_index) self.update_status_bar() def apply_filter(self, is_filtering_mode_out): """ Applies the filter based on the given mode. """ self.is_filtering_mode_out = is_filtering_mode_out if(is_filtering_mode_out): self.proxy_model.setFilterOutList(self.per_column_filter_out_set_list) else: self.proxy_model.setFilterInList(self.per_column_filter_in_set_list) # This is just to trigger the proxy model to apply the filter self.proxy_model.setFilterKeyColumn(0) def dragEnterEvent(self, q_drag_enter_event): if(q_drag_enter_event.mimeData().hasFormat("text/uri-list")): q_drag_enter_event.acceptProposedAction(); def dropEvent(self, q_drop_event): url_list = q_drop_event.mimeData().urls() if(len(url_list) == 0): return log_file_list = [url.toLocalFile() for url in url_list] self.log_file_full_path = log_file_list[0] self.load_log_file(self.log_file_full_path) def closeEvent(self, event): app = QApplication([]) # app.closeAllWindows() app.deleteLater() app.closeAllWindows()
class MenuNode(object): _currentMenuContext = None """docstring for MenuNode""" def __init__(self, option, parent, menubar = None): if isinstance(option ,(str,unicode)): blobs = option.split('|') _option={ 'label':blobs[0] } l = len(blobs) if l>1: _option['shortcut'] = blobs[1] if l>2: _option['help'] = blobs[2] option = _option self.qtmenubar = menubar self.qtaction = None self.qtmenu = None # self.qtaction = None self.owner = None self.parent = parent signal = option.get( 'signal', None ) self.setSignal(signal) self.mgr = parent and parent.mgr self.owner = parent and parent.owner self.children = [] self.label = option.get('label', 'UNNAMED') self.name = option.get('name',self.label.replace('&','').replace(' ','_')) self.name = self.name.lower() self.shortcut = option.get( 'shortcut', False ) self.help = option.get( 'help', '' ) self.priority = option.get( 'priority', 0 ) self.itemType = option.get( 'type', False ) self.onClick = option.get( 'on_click', None ) self.cmd = option.get( 'command', None ) self.cmdArgs = option.get( 'command_args', None ) self.link = None self.menuType = self.qtmenubar and 'menubar' or 'item' children = option.get( 'children', None ) link = option.get( 'link', None ) if children or self.itemType == 'menu': if self.menuType != 'menubar': self.menuType = 'menu' self.itemType = False elif link: self.link = link if self.menuType != 'menubar': self.menuType = 'link' elif parent and parent.menuType == 'menubar': self.menuType = 'menu' if self.menuType == 'menu' : self.qtmenu = QMenu(self.label) if not parent or parent.menuType == 'root': return parent.addChildControl(self) if self.itemType == 'check': checked = option.get('checked',False) self.setValue(checked or False) if children: for data in children: self.addChild(data) # self.mgr.addNodeIndex(self) def getFullName(self): if parent: return parent.getFullName()+'/'+self.name return self.name def addChild( self, option, owner = None ): if option=='----': if self.qtmenu: self.qtmenu.addSeparator() elif isinstance(option, list): output=[] for data in option: n = self.addChild(data) if n : output.append(n) if owner: n.owner = owner return output else: node = MenuNode(option, self) if owner: node.owner = owner self.children.append(node) return node def addChildControl(self, child): childType = child.menuType selfType = self.menuType if selfType=='menu': if childType=='menu': child.qtaction = self.qtmenu.addMenu(child.qtmenu) elif child.link: qtmenu = child.link.qtmenu child.qtaction = self.qtmenu.addMenu(qtmenu) else: action = QtGui.QAction(child.label, None, shortcut = child.shortcut, statusTip = child.help, checkable = child.itemType=='check', triggered = child.handleEvent ) self.qtmenu.addAction(action) child.qtaction = action elif selfType=='menubar': if childType=='menu': self.qtmenubar.addMenu(child.qtmenu) child.qtaction = child.qtmenu.menuAction() else: logging.warning('attempt to add menuitem/link to a menubar') return else: logging.warning('menuitem has no child') def setEnabled(self, enabled): #todo: set state of linked item selfType = self.menuType if selfType == 'menubar': self.qtmenubar.setEnable(enabled) return if self.qtmenu: self.qtmenu.setEnabled(enabled) else: self.qtaction.setEnabled(enabled) def remove(self): self.clear() self.parent.children.remove(self) selfType = self.menuType if not self.parent: return if selfType=='menubar': return parentType = self.parent.menuType if parentType == 'menu': self.parent.qtmenu.removeAction( self.qtaction ) elif parentType == 'menubar': self.parent.qtmenubar.removeAction( self.qtaction ) logging.info('remove menunode:' + self.name ) def clear( self ): if self.menuType in [ 'menu', 'menubar' ]: for node in self.children[:]: node.remove() def findChild(self,name): name = name.lower() for c in self.children: if c.name==name: return c return None def getValue(self): if self.itemType in ('check','radio'): return self.qtaction.isChecked() return True def setValue(self, v): if self.itemType in ('check','radio'): self.qtaction.setChecked(v and True or False) def setSignal(self, signal): if isinstance(signal, (str, unicode)): signal = signals.get(signal) self.signal = signal def popUp( self, **option ): if self.qtmenu: context = option.get( 'context', None ) MenuNode._currentMenuContext = context self.qtmenu.exec_(QtGui.QCursor.pos()) def getContext( self ): return MenuNode._currentMenuContext def setOnClick(self, onClick): self.onClick = onClick def handleEvent(self): itemtype = self.itemType value = self.getValue() logging.debug( 'menu event:' + self.name ) if self.owner: if hasattr( self.owner, 'onMenu' ): self.owner.onMenu( self ) if self.signal: self.signal(value) if self.onClick != None: self.onClick(value) if self.cmd: args = self.cmdArgs or {} app.doCommand( self.cmd, **args ) MenuNode._currentMenuContext = None
class ProfileTableViewer( QWidget ): " Profiling results table viewer " escapePressed = pyqtSignal() def __init__( self, scriptName, params, reportTime, dataFile, stats, parent = None ): QWidget.__init__( self, parent ) self.__table = ProfilerTreeWidget( self ) self.__table.escapePressed.connect( self.__onEsc ) self.__script = scriptName self.__stats = stats project = GlobalData().project if project.isLoaded(): self.__projectPrefix = os.path.dirname( project.fileName ) else: self.__projectPrefix = os.path.dirname( scriptName ) if not self.__projectPrefix.endswith( os.path.sep ): self.__projectPrefix += os.path.sep self.__table.setAlternatingRowColors( True ) self.__table.setRootIsDecorated( False ) self.__table.setItemsExpandable( False ) self.__table.setSortingEnabled( True ) self.__table.setItemDelegate( NoOutlineHeightDelegate( 4 ) ) self.__table.setUniformRowHeights( True ) self.__table.setSelectionMode( QAbstractItemView.SingleSelection ) self.__table.setSelectionBehavior( QAbstractItemView.SelectRows ) headerLabels = [ "", "Calls", "Total time", "Per call", "Cum. time", "Per call", "File name:line", "Function", "Callers", "Callees" ] self.__table.setHeaderLabels( headerLabels ) headerItem = self.__table.headerItem() headerItem.setToolTip( 0, "Indication if it is an outside function" ) headerItem.setToolTip( 1, "Actual number of calls/primitive calls " "(not induced via recursion)" ) headerItem.setToolTip( 2, "Total time spent in function " "(excluding time made in calls " "to sub-functions)" ) headerItem.setToolTip( 3, "Total time divided by number " "of actual calls" ) headerItem.setToolTip( 4, "Total time spent in function and all " "subfunctions (from invocation till exit)" ) headerItem.setToolTip( 5, "Cumulative time divided by number " "of primitive calls" ) headerItem.setToolTip( 6, "Function location" ) headerItem.setToolTip( 7, "Function name" ) headerItem.setToolTip( 8, "Function callers" ) headerItem.setToolTip( 9, "Function callees" ) self.__table.itemActivated.connect( self.__activated ) totalCalls = self.__stats.total_calls totalPrimitiveCalls = self.__stats.prim_calls # The calls were not induced via recursion totalTime = self.__stats.total_tt txt = "<b>Script:</b> " + self.__script + " " + params.arguments + "<br>" \ "<b>Run at:</b> " + reportTime + "<br>" + \ str( totalCalls ) + " function calls (" + \ str( totalPrimitiveCalls ) + " primitive calls) in " + \ FLOAT_FORMAT % totalTime + " CPU seconds" summary = QLabel( txt ) summary.setToolTip( txt ) summary.setSizePolicy( QSizePolicy.Ignored, QSizePolicy.Fixed ) summary.setFrameStyle( QFrame.StyledPanel ) summary.setAutoFillBackground( True ) summaryPalette = summary.palette() summaryBackground = summaryPalette.color( QPalette.Background ) summaryBackground.setRgb( min( summaryBackground.red() + 30, 255 ), min( summaryBackground.green() + 30, 255 ), min( summaryBackground.blue() + 30, 255 ) ) summaryPalette.setColor( QPalette.Background, summaryBackground ) summary.setPalette( summaryPalette ) vLayout = QVBoxLayout() vLayout.setContentsMargins( 0, 0, 0, 0 ) vLayout.setSpacing( 0 ) vLayout.addWidget( summary ) vLayout.addWidget( self.__table ) self.setLayout( vLayout ) self.__createContextMenu() self.__populate( totalTime ) return def __onEsc( self ): " Triggered when Esc is pressed " self.escapePressed.emit() return def __createContextMenu( self ): " Creates context menu for the table raws " self.__contextMenu = QMenu( self ) self.__callersMenu = QMenu( "Callers", self ) self.__outsideCallersMenu = QMenu( "Outside callers", self ) self.__calleesMenu = QMenu( "Callees", self ) self.__outsideCalleesMenu = QMenu( "Outside callees", self ) self.__contextMenu.addMenu( self.__callersMenu ) self.__contextMenu.addMenu( self.__outsideCallersMenu ) self.__contextMenu.addSeparator() self.__contextMenu.addMenu( self.__calleesMenu ) self.__contextMenu.addMenu( self.__outsideCalleesMenu ) self.__contextMenu.addSeparator() self.__disasmAct = self.__contextMenu.addAction( PixmapCache().getIcon( 'disasmmenu.png' ), "Disassemble", self.__onDisassemble ) self.__callersMenu.triggered.connect( self.__onCallContextMenu ) self.__outsideCallersMenu.triggered.connect( self.__onCallContextMenu ) self.__calleesMenu.triggered.connect( self.__onCallContextMenu ) self.__outsideCalleesMenu.triggered.connect( self.__onCallContextMenu ) self.__table.setContextMenuPolicy( Qt.CustomContextMenu ) self.__table.customContextMenuRequested.connect( self.__showContextMenu ) return def __showContextMenu( self, point ): " Context menu " self.__callersMenu.clear() self.__outsideCallersMenu.clear() self.__calleesMenu.clear() self.__outsideCalleesMenu.clear() # Detect what the item was clicked item = self.__table.itemAt( point ) funcName = item.getFunctionName() self.__disasmAct.setEnabled( item.getFileName() != "" and \ not funcName.startswith( "<" ) ) # Build the context menu if item.callersCount() == 0: self.__callersMenu.setEnabled( False ) self.__outsideCallersMenu.setEnabled( False ) else: callers = self.__stats.stats[ item.getFuncIDs() ][ 4 ] callersList = callers.keys() callersList.sort() for callerFunc in callersList: menuText = self.__getCallLine( callerFunc, callers[ callerFunc ] ) if self.__isOutsideItem( callerFunc[ 0 ] ): act = self.__outsideCallersMenu.addAction( menuText ) else: act = self.__callersMenu.addAction( menuText ) funcFileName, funcLine, funcName = self.__getLocationAndName( callerFunc ) act.setData( QVariant( funcFileName + ":" + \ str( funcLine ) + ":" + \ funcName ) ) self.__callersMenu.setEnabled( not self.__callersMenu.isEmpty() ) self.__outsideCallersMenu.setEnabled( not self.__outsideCallersMenu.isEmpty() ) if item.calleesCount() == 0: self.__calleesMenu.setEnabled( False ) self.__outsideCalleesMenu.setEnabled( False ) else: callees = self.__stats.all_callees[ item.getFuncIDs() ] calleesList = callees.keys() calleesList.sort() for calleeFunc in calleesList: menuText = self.__getCallLine( calleeFunc, callees[ calleeFunc ] ) if self.__isOutsideItem( calleeFunc[ 0 ] ): act = self.__outsideCalleesMenu.addAction( menuText ) else: act = self.__calleesMenu.addAction( menuText ) funcFileName, funcLine, funcName = self.__getLocationAndName( calleeFunc ) act.setData( QVariant( funcFileName + ":" + \ str( funcLine ) + ":" + \ funcName ) ) self.__calleesMenu.setEnabled( not self.__calleesMenu.isEmpty() ) self.__outsideCalleesMenu.setEnabled( not self.__outsideCalleesMenu.isEmpty() ) self.__contextMenu.popup( QCursor.pos() ) return def __onDisassemble( self ): " On disassemble something " item = self.__table.selectedItems()[ 0 ] GlobalData().mainWindow.showDisassembler( item.getFileName(), item.getFunctionName() ) return def __resize( self ): " Resizes columns to the content " self.__table.header().resizeSections( QHeaderView.ResizeToContents ) self.__table.header().setStretchLastSection( True ) self.__table.header().resizeSection( OUTSIDE_COL_INDEX, 28 ) self.__table.header().setResizeMode( OUTSIDE_COL_INDEX, QHeaderView.Fixed ) return def setFocus( self ): " Set focus to the proper widget " self.__table.setFocus() return def __isOutsideItem( self, fileName ): " Detects if the record should be shown as an outside one " return not fileName.startswith( self.__projectPrefix ) def __activated( self, item, column ): " Triggered when the item is activated " try: line = item.getLineNumber() fileName = item.getFileName() if line == 0 or not os.path.isabs( fileName ): return GlobalData().mainWindow.openFile( fileName, line ) except: logging.error( "Could not jump to function location" ) return @staticmethod def __getFuncShortLocation( funcFileName, funcLine ): " Provides unified shortened function location " if funcFileName == "": return "" return os.path.basename( funcFileName ) + ":" + str( funcLine ) def __getCallLine( self, func, props ): " Provides the formatted call line " funcFileName, funcLine, funcName = self.__getLocationAndName( func ) if isinstance( props, tuple ): actualCalls, primitiveCalls, totalTime, cumulativeTime = props if primitiveCalls != actualCalls: callsString = str( actualCalls ) + "/" + str( primitiveCalls ) else: callsString = str( actualCalls ) return callsString + "\t" + FLOAT_FORMAT % totalTime + "\t" + \ FLOAT_FORMAT % cumulativeTime + "\t" + \ self.__getFuncShortLocation( funcFileName, funcLine ) + \ "(" + funcName + ")" # I've never seen this branch working so it is just in case. # There was something like this in the pstats module return self.__getFuncShortLocation( funcFileName, funcLine ) + \ "(" + funcName + ")" @staticmethod def __getLocationAndName( func ): " Provides standardized function file name, line and its name " if func[ : 2 ] == ( '~', 0 ): # special case for built-in functions name = func[ 2 ] if name.startswith( '<' ) and name.endswith( '>' ): return "", 0, "{%s}" % name[ 1 : -1 ] return "", 0, name return func[ 0 ], func[ 1 ], func[ 2 ] def __createItem( self, func, totalCPUTime, primitiveCalls, actualCalls, totalTime, cumulativeTime, timePerCall, cumulativeTimePerCall, callers ): " Creates an item to display " values = [] values.append( "" ) if primitiveCalls == actualCalls: values.append( str( actualCalls ) ) else: values.append( str( actualCalls ) + "/" + str( primitiveCalls ) ) if totalCPUTime == 0.0: values.append( FLOAT_FORMAT % totalTime ) else: values.append( FLOAT_FORMAT % totalTime + " \t(%3.2f%%)" % (totalTime / totalCPUTime * 100) ) values.append( FLOAT_FORMAT % timePerCall ) values.append( FLOAT_FORMAT % cumulativeTime ) values.append( FLOAT_FORMAT % cumulativeTimePerCall ) # Location and name will be filled in the item constructor values.append( "" ) values.append( "" ) # Callers callersCount = len( callers ) values.append( str( callersCount ) ) # Callees if func in self.__stats.all_callees: calleesCount = len( self.__stats.all_callees[ func ] ) else: calleesCount = 0 values.append( str( calleesCount ) ) item = ProfilingTableItem( values, self.__isOutsideItem( func[ 0 ] ), func ) if callersCount != 0: tooltip = "" callersList = callers.keys() callersList.sort() for callerFunc in callersList[ : MAX_CALLS_IN_TOOLTIP ]: if tooltip != "": tooltip += "\n" tooltip += self.__getCallLine( callerFunc, callers[ callerFunc ] ) if callersCount > MAX_CALLS_IN_TOOLTIP: tooltip += "\n. . ." item.setToolTip( 8, tooltip ) if calleesCount != 0: callees = self.__stats.all_callees[ func ] tooltip = "" calleesList = callees.keys() calleesList.sort() for calleeFunc in calleesList[ : MAX_CALLS_IN_TOOLTIP ]: if tooltip != "": tooltip += "\n" tooltip += self.__getCallLine( calleeFunc, callees[ calleeFunc ] ) if calleesCount > MAX_CALLS_IN_TOOLTIP: tooltip += "\n. . ." item.setToolTip( 9, tooltip ) self.__table.addTopLevelItem( item ) return def __populate( self, totalCPUTime ): " Populates the data " for func, ( primitiveCalls, actualCalls, totalTime, cumulativeTime, callers ) in self.__stats.stats.items(): # Calc time per call if actualCalls == 0: timePerCall = 0.0 else: timePerCall = totalTime / actualCalls # Calc time per cummulative call if primitiveCalls == 0: cumulativeTimePerCall = 0.0 else: cumulativeTimePerCall = cumulativeTime / primitiveCalls self.__createItem( func, totalCPUTime, primitiveCalls, actualCalls, totalTime, cumulativeTime, timePerCall, cumulativeTimePerCall, callers ) self.__resize() self.__table.header().setSortIndicator( 2, Qt.DescendingOrder ) self.__table.sortItems( 2, self.__table.header().sortIndicatorOrder() ) return def togglePath( self, state ): " Switches between showing full paths or file names in locations " for index in xrange( 0, self.__table.topLevelItemCount() ): self.__table.topLevelItem( index ).updateLocation( state ) self.__resize() return def __onCallContextMenu( self, act ): " Triggered when a context menu action is requested " name = str( act.data().toString() ) for index in xrange( 0, self.__table.topLevelItemCount() ): item = self.__table.topLevelItem( index ) if item.match( name ): self.__table.clearSelection() self.__table.scrollToItem( item ) self.__table.setCurrentItem( item ) return def onSaveAs( self, fileName ): " Saves the table to a file in CSV format " try: f = open( fileName, "wt" ) headerItem = self.__table.headerItem() outsideIndex = -1 for index in xrange( 0, headerItem.columnCount() ): title = str( headerItem.text( index ) ) if title == "": outsideIndex = index title = "Outside" if index != 0: f.write( ',' + title ) else: f.write( title ) for index in xrange( 0, self.__table.topLevelItemCount() ): item = self.__table.topLevelItem( index ) f.write( "\n" ) for column in xrange( 0, item.columnCount() ): if column != 0: f.write( ',' ) if column == outsideIndex: if item.isOutside(): f.write( "Y" ) else: f.write( "N" ) else: text = str( item.text( column ) ) f.write( text.replace( '\t', '' ) ) f.close() except Exception, ex: logging.error( ex ) return
class LogSParserMain(QMainWindow): """ This is the main class in the application. It's responsible for displaying the log data in a tabular format as well as allowing the user to filter the logs displayed. """ per_column_filter_out_set_list = list() per_column_filter_in_set_list = list() header = list() table_conditional_formatting_config = None def __init__(self): QMainWindow.__init__(self) self.graph_window_dict = {} self.menuFilter = None self.proxy_model = None self.table_data = None self.user_interface = Ui_Siraj() self.user_interface.setupUi(self) self.user_interface.mnuActionOpen.triggered.connect( self.menu_open_file) self.user_interface.mnuActionLoadConfigs.triggered.connect( self.menu_load_configs) self.user_interface.mnuActionExit.triggered.connect(self.menu_exit) self.user_interface.mnuActionAbout.triggered.connect(self.menu_about) self.user_interface.centralwidget.setLayout( self.user_interface.verticalLayout) self.user_interface.dckSourceContents.setLayout( self.user_interface.lytSource) self.user_interface.tblLogData.doubleClicked.connect( self.cell_double_clicked) self.user_interface.tblLogData.clicked.connect(self.cell_left_clicked) self.user_interface.tblLogData.keyPressEvent = self.cell_key_pressed self.user_interface.tblLogData.setContextMenuPolicy( Qt.CustomContextMenu) self.user_interface.tblLogData.customContextMenuRequested.connect( self.cell_right_clicked) self.user_interface.txtSourceFile.setReadOnly(True) self.is_table_visible = True self.is_source_visible = True self.user_interface.tblLogData.resizeColumnsToContents() self.user_interface.tblLogData.resizeRowsToContents() self.setup_context_menu() self.setup_toolbars() self.clipboard = QApplication.clipboard() self.is_filtering_mode_out = True self.matched_row_list = [] self.search_criteria_updated = True self.case_sensitive_search_type = Qt.CaseInsensitive self.is_wrap_search = True self.is_match_whole_word = False self.graph_marker_list = [] self.user_interface.tblLogData.setAcceptDrops(False) self.setAcceptDrops(True) self.load_configuration_file() self.toggle_source_view() def setup_toolbars(self): source_toolbar = self.addToolBar('SourceToolbar') self.user_interface.tbrActionToggleSourceView = QAction('C/C++', self) self.user_interface.tbrActionToggleSourceView.triggered.connect( self.toggle_source_view) self.user_interface.tbrActionToggleSourceView.setToolTip( "Toggle source code view") self.user_interface.tbrActionToggleSourceView.setCheckable(True) self.user_interface.tbrActionToggleSourceView.setChecked(True) source_toolbar.addAction(self.user_interface.tbrActionToggleSourceView) search_toolbar = self.addToolBar("SearchToolbar") search_toolbar.setAllowedAreas(Qt.TopToolBarArea | Qt.BottomToolBarArea) self.ledSearchBox = QLineEdit() self.ledSearchBox.textChanged.connect(self.invalidate_search_criteria) self.ledSearchBox.keyPressEvent = self.search_box_key_pressed search_toolbar.addWidget(self.ledSearchBox) tbrActionPrevSearchMatch = QAction('<<', self) tbrActionPrevSearchMatch.triggered.connect( functools.partial(self.select_search_match, False)) tbrActionPrevSearchMatch.setToolTip("Go to previous search match") tbrActionNextSearchMatch = QAction('>>', self) tbrActionNextSearchMatch.triggered.connect( functools.partial(self.select_search_match, True)) tbrActionNextSearchMatch.setToolTip("Go to next search match") tbrActionIgnoreCase = QAction('Ignore Case', self) tbrActionIgnoreCase.setCheckable(True) tbrActionIgnoreCase.setChecked(True) tbrActionIgnoreCase.triggered.connect(self.set_search_case_sensitivity, tbrActionIgnoreCase.isChecked()) tbrActionIgnoreCase.setToolTip("Ignore case") tbrActionWrapSearch = QAction('Wrap Search', self) tbrActionWrapSearch.setCheckable(True) tbrActionWrapSearch.setChecked(True) tbrActionWrapSearch.triggered.connect(self.set_search_wrap, tbrActionWrapSearch.isChecked()) tbrActionWrapSearch.setToolTip("Wrap Search") tbrActionMatchWholeWord = QAction('Match Whole Word', self) tbrActionMatchWholeWord.setCheckable(True) tbrActionMatchWholeWord.setChecked(False) tbrActionMatchWholeWord.triggered.connect( self.set_match_whole_word, tbrActionMatchWholeWord.isChecked()) tbrActionMatchWholeWord.setToolTip("Match Whole Word") search_toolbar.addAction(tbrActionPrevSearchMatch) search_toolbar.addAction(tbrActionNextSearchMatch) search_toolbar.addAction(tbrActionIgnoreCase) search_toolbar.addAction(tbrActionMatchWholeWord) search_toolbar.addAction(tbrActionWrapSearch) def set_search_case_sensitivity(self, ignore_case): self.invalidate_search_criteria() if (ignore_case): self.case_sensitive_search_type = Qt.CaseInsensitive else: self.case_sensitive_search_type = Qt.CaseSensitive def set_search_wrap(self, wrap_search): self.invalidate_search_criteria() self.is_wrap_search = wrap_search def set_match_whole_word(self, match_whole_word): self.invalidate_search_criteria() self.is_match_whole_word = match_whole_word def invalidate_search_criteria(self): self.search_criteria_updated = True self.matched_row_list.clear() def get_matched_row_list(self, key_column, search_criteria, case_sensitivity): search_proxy = QSortFilterProxyModel() search_proxy.setSourceModel(self.user_interface.tblLogData.model()) search_proxy.setFilterCaseSensitivity(case_sensitivity) search_proxy.setFilterKeyColumn(key_column) if self.is_match_whole_word: search_criteria = r"\b{}\b".format(search_criteria) search_proxy.setFilterRegExp(search_criteria) matched_row_list = [] for proxy_row in range(search_proxy.rowCount()): match_index = search_proxy.mapToSource( search_proxy.index(proxy_row, key_column)) matched_row_list.append(match_index.row()) self.search_criteria_updated = False return matched_row_list def select_search_match(self, is_forward): selected_indexes = self.get_selected_indexes() if (len(selected_indexes) == 0): self.display_message_box( "No selection", "Please select a cell from the column you want to search", QMessageBox.Warning) else: index = self.get_selected_indexes()[0] row = index.row() column = index.column() search_criteria = self.ledSearchBox.text() if (self.search_criteria_updated): self.matched_row_list = self.get_matched_row_list( column, search_criteria, self.case_sensitive_search_type) if (len(self.matched_row_list) > 0): is_match_found = False if (is_forward): matched_row_index = bisect_left(self.matched_row_list, row) if ((matched_row_index < len(self.matched_row_list) - 1)): if (self.matched_row_list[matched_row_index] == row): matched_row_index += 1 is_match_found = True elif (self.is_wrap_search): matched_row_index = 0 is_match_found = True else: matched_row_index = bisect_right(self.matched_row_list, row) if (matched_row_index > 0): matched_row_index -= 1 if ((matched_row_index > 0)): if ((self.matched_row_list[matched_row_index] == row)): matched_row_index -= 1 is_match_found = True elif (self.is_wrap_search): matched_row_index = len(self.matched_row_list) - 1 is_match_found = True if (is_match_found): self.select_cell_by_row_and_column( self.matched_row_list[matched_row_index], column) else: self.display_message_box( "No match found", 'Search pattern "{}" was not found in column "{}"'.format( search_criteria, self.header[column]), QMessageBox.Warning) def reset_per_config_file_data(self): self.graph_window_dict.clear() self.reset_per_log_file_data() self.table_data = None self.table_model = None self.proxy_model = None def load_configuration_file(self, config_file_path="siraj_configs.json"): self.reset_per_config_file_data() self.config = LogSParserConfigs(config_file_path) self.log_file_full_path = self.config.get_config_item( "log_file_full_path") self.log_trace_regex_pattern = self.config.get_config_item( "log_row_pattern") self.time_stamp_column = self.config.get_config_item( "time_stamp_column_number_zero_based") self.user_data_column_zero_based = self.config.get_config_item( "user_data_column_zero_based") self.external_editor_configs = self.config.get_config_item( "external_editor_configs") cross_reference_configs = self.config.get_config_item( "source_cross_reference_configs") self.file_column = cross_reference_configs[ "file_column_number_zero_based"] self.file_column_pattern = cross_reference_configs[ "file_column_pattern"] self.line_column = cross_reference_configs[ "line_column_number_zero_based"] self.line_column_pattern = cross_reference_configs[ "line_column_pattern"] self.graph_configs = self.config.get_config_item("graph_configs") self.root_source_path_prefix = cross_reference_configs[ "root_source_path_prefix"] self.syntax_highlighting_style = cross_reference_configs[ "pygments_syntax_highlighting_style"] self.table_conditional_formatting_config = self.config.get_config_item( "table_conditional_formatting_configs") self.load_log_file(self.log_file_full_path) def load_graphs(self, graph_configs, table_data): pg.setConfigOption('background', QColor("white")) pg.setConfigOption('foreground', QColor("black")) pg.setConfigOptions(antialias=True) window_dict = graph_configs["window_dict"] series_list = [] for window_name in window_dict: window_handle = pg.GraphicsWindow(title=window_name) self.graph_window_dict[window_name] = window_handle window_handle.show() plot_dict = window_dict[window_name]["plot_dict"] first_plot_name_in_the_window = "" for plot_name in plot_dict: plot_row = plot_dict[plot_name]["row"] # plot_column = plot_dict[plot_name]["column"] # plot_row_span = plot_dict[plot_name]["row_span"] # plot_column_span = plot_dict[plot_name]["column_span"] plot_handle = window_handle.addPlot( name=plot_name, title=plot_name, row=plot_row, col=1, #plot_column, rowspan=1, #plot_row_span, colspan=1) #plot_column_span) plot_handle.addLegend() if first_plot_name_in_the_window == "": first_plot_name_in_the_window = plot_name plot_handle.setXLink(first_plot_name_in_the_window) marker = pg.InfiniteLine(angle=90, movable=False, pen=pg.mkPen(width=1, color=QColor("red"))) plot_handle.addItem(marker, ignoreBounds=True) self.graph_marker_list.append(marker) plot_handle.scene().sigMouseClicked.connect( functools.partial(self.graph_mouse_clicked, plot_handle)) plot_handle.scene().setClickRadius(50) series_dict = plot_dict[plot_name]["series_dict"] for series_name in series_dict: series_symbol = series_dict[series_name]["symbol"] series_color = series_dict[series_name]["color"] series_pattern = series_dict[series_name]["pattern"] series_list.append( (series_name, series_symbol, series_color, series_pattern, [], [], plot_handle)) for row_number, row_data in enumerate(table_data): for (series_name, series_symbol, series_color, series_pattern, x_point_list, y_point_list, plot_handle) in series_list: cell_to_match = row_data[self.user_data_column_zero_based] m = re.search(series_pattern, cell_to_match) if m is not None: x_point_list.append(row_number) y_point_list.append(int(m.group(1))) for (series_name, series_symbol, series_color, series_pattern, x_point_list, y_point_list, plot_handle) in series_list: plot_handle.plot(x_point_list, y_point_list, pen=pg.mkPen(width=1, color=QColor(series_color)), symbol=series_symbol, symbolPen='w', symbolBrush=QColor(series_color), name=series_name) # graphs = list(sorted(graph_configs.keys(), key=lambda k: graph_configs[k]["index"])) # graph_data = [([], [],) for _ in graphs] # # self.graph_marker_list = [] # # for row_number, row_data in enumerate(table_data): # for graph_number, graph_name in enumerate(graphs): # cell_to_match = row_data[graph_configs[graph_name]["column"]] # m = re.search(graph_configs[graph_name]["pattern"], cell_to_match) # if (m is not None): # graph_data[graph_number][0].append(row_number) # X-Axis value # graph_data[graph_number][1].append(int(m.group(1))) # Y-Axis value # # for graph in graphs: # window = None # wnd = graph_configs[graph]["window"] # if (wnd in self.graph_window_dict): # window = self.graph_window_dict[wnd] # window.clear() # # is_new_window = False # first_plot_name = None # for graph_number, graph in enumerate(graphs): # window = None # wnd = graph_configs[graph]["window"] # if (wnd in self.graph_window_dict): # window = self.graph_window_dict[wnd] # is_new_window = False # else: # is_new_window = True # window = pg.GraphicsWindow(title=wnd) # # self.graph_window_dict[wnd] = window # # p = window.addPlot(name=graph, title=graph) # # p.plot(graph_data[graph_number][0], # graph_data[graph_number][1], # pen=pg.mkPen(width=1, color=QColor(graph_configs[graph]["color"])), # symbol=graph_configs[graph]["symbol"], symbolPen='w', # symbolBrush=QColor(graph_configs[graph]["color"]), name=graph) # p.showGrid(x=True, y=True) # if first_plot_name == None: # first_plot_name = graph # p.setXLink(first_plot_name) # marker = pg.InfiniteLine(angle=90, movable=False) # p.addItem(marker, ignoreBounds=True) # self.graph_marker_list.append(marker) # p.scene().sigMouseClicked.connect(functools.partial(self.graph_mouse_clicked, p)) # # window.nextRow() def graph_mouse_clicked(self, plt, evt): point = plt.vb.mapSceneToView(evt.scenePos()) self.select_cell_by_row_and_column(int(round(point.x())), self.user_data_column_zero_based) self.update_graph_markers() def setup_context_menu(self): self.menuFilter = QMenu(self) self.hide_action = QAction('Hide selected values', self) self.show_only_action = QAction('Show only selected values', self) self.clear_all_filters_action = QAction('Clear all filters', self) self.copy_selection_action = QAction('Copy selection', self) self.unhide_menu = QMenu('Unhide item from selected column', self.menuFilter) self.hide_action.triggered.connect( self.hide_rows_based_on_selected_cells) self.show_only_action.triggered.connect( self.show_rows_based_on_selected_cells) self.clear_all_filters_action.triggered.connect(self.clear_all_filters) self.copy_selection_action.triggered.connect( self.prepare_clipboard_text) self.menuFilter.addAction(self.hide_action) self.menuFilter.addMenu(self.unhide_menu) self.menuFilter.addAction(self.show_only_action) self.menuFilter.addAction(self.clear_all_filters_action) self.menuFilter.addSeparator() self.menuFilter.addAction(self.copy_selection_action) self.hide_action.setShortcut('Ctrl+H') self.show_only_action.setShortcut('Ctrl+O') self.clear_all_filters_action.setShortcut('Ctrl+Del') self.copy_selection_action.setShortcut("Ctrl+C") def toggle_source_view(self): self.is_source_visible = not self.is_source_visible self.user_interface.tbrActionToggleSourceView.setChecked( self.is_source_visible) self.user_interface.dckSource.setVisible(self.is_source_visible) logging.info("Source view is now {}".format( "Visible" if self.is_source_visible else "Invisible")) def display_message_box(self, title, message, icon): """ Show the about box. """ message_box = QMessageBox(self) message_box.setWindowTitle(title) message_box.setTextFormat(Qt.RichText) message_box.setText(message) message_box.setIcon(icon) message_box.exec_() def menu_about(self): """ Show the about box. """ about_text = """ Copyright 2015 Mohamed Galal El-Din Ebrahim (<a href="mailto:[email protected]">[email protected]</a>) <br> <br> siraj is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License. <br> <br> siraj is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. <br> <br> You should have received a copy of the GNU General Public License along with siraj. If not, see <a href="http://www.gnu.org/licenses">http://www.gnu.org/licenses</a>. """ self.display_message_box("About", about_text, QMessageBox.Information) def menu_exit(self): """ Handles the exit menu clicked event. """ exit(0) def menu_open_file(self): """ Handles the open menu clicked event. """ self.log_file_full_path = QFileDialog.getOpenFileName( self, 'Open Log File', os.getcwd()) if (self.log_file_full_path != ''): self.load_log_file(self.log_file_full_path) def menu_load_configs(self): """ Loads a new configuration file. """ self.config_file_full_path = QFileDialog.getOpenFileName( self, 'Open Config File', os.getcwd()) if (self.config_file_full_path != ''): self.load_configuration_file(self.config_file_full_path) def reset_per_log_file_data(self): self.invalidate_search_criteria() def load_log_file(self, log_file_full_path): """ Loads the given log file into the table. """ self.reset_per_log_file_data() if (log_file_full_path == ""): pass elif (os.path.isfile(log_file_full_path)): with open(log_file_full_path, "r") as log_file_handle: log_file_content_lines = log_file_handle.read().splitlines() pattern = re.compile(self.log_trace_regex_pattern) self.table_data = [] most_recent_valid_table_entry = [] for line in log_file_content_lines: m = pattern.match(line) if (m is not None): most_recent_valid_table_entry = [ group.strip() for group in m.groups() ] self.table_data.append(list(most_recent_valid_table_entry)) else: if (self.user_data_column_zero_based != -1): temp_list = list(most_recent_valid_table_entry) temp_list[self.user_data_column_zero_based] = line self.table_data.append(temp_list) m = re.search(self.log_trace_regex_pattern, log_file_content_lines[1]) self.header = [ group_name for group_name in sorted(m.groupdict().keys(), key=lambda k: m.start(k)) ] self.table_model = MyTableModel( self.table_data, self.header, self.table_conditional_formatting_config, self) logging.info("Headers: %s", self.header) logging.info("%s has %d lines", self.log_file_full_path, len(self.table_data)) self.proxy_model = MySortFilterProxyModel(self) self.proxy_model.setSourceModel(self.table_model) self.user_interface.tblLogData.setModel(self.proxy_model) if (len(self.per_column_filter_out_set_list) == 0): self.per_column_filter_out_set_list = [ set() for column in range(len(self.table_data[0])) ] if (len(self.per_column_filter_in_set_list) == 0): self.per_column_filter_in_set_list = [ set() for column in range(len(self.table_data[0])) ] self.extract_column_dictionaries(self.header, self.table_data) self.load_graphs(self.graph_configs, self.table_data) self.setWindowTitle("Siraj | {}".format(log_file_full_path)) self.select_cell_by_row_and_column( 0, self.user_data_column_zero_based) else: self.display_message_box( "File not Found!", "File <b>`{}`</b> was not found. You can either: <br><br>1. Open a log file via the File menu. Or<br>2. Drag a log file from the system and drop it into the application" .format(log_file_full_path), QMessageBox.Critical) def extract_column_dictionaries(self, header_vector_list, data_matrix_list): """ This function extracts a dictionary of dictionaries The extracted is a dictionary of columns where key is the column name, and the data is another dictionary. The inner dictionary has a key equal to a specific cell value of the current column, and the value is a list of row number where this value appeared in. This will be used to provide quick navigation through the log. """ column_count = len(header_vector_list) self.columns_dict = {} for column, column_name in enumerate(header_vector_list): self.columns_dict[column] = {} for row, log in enumerate(data_matrix_list): for column, field in enumerate(log): if (log[column] not in self.columns_dict[column]): self.columns_dict[column][log[column]] = [] self.columns_dict[column][log[column]].append(row) def cell_left_clicked(self, index): """ Handles the event of clicking on a table cell. If the clicked column was the the column that contain the source file:line information from the log, the function also populate the the EditView with the source file contents with a marker highlighting the line. This is only done if the source view is visible. """ index = self.proxy_model.mapToSource(index) if (self.is_source_visible): logging.info("cell[%d][%d] = %s", index.row(), index.column(), index.data()) row = index.row() file_matcher = re.search(self.file_column_pattern, self.table_data[row][self.file_column]) line_matcher = re.search(self.line_column_pattern, self.table_data[row][self.line_column]) if ((file_matcher is not None) and (line_matcher is not None)): file = file_matcher.group(1) line = line_matcher.group(1) full_path = "{}{}".format(self.root_source_path_prefix, file.strip()) self.load_source_file(full_path, line) self.user_interface.tblLogData.setFocus() self.update_status_bar() self.update_graph_markers() def load_source_file(self, file, line): code = open(file).read() lexer = get_lexer_for_filename(file) formatter = HtmlFormatter(linenos=True, full=True, style=self.syntax_highlighting_style, hl_lines=[line]) result = highlight(code, lexer, formatter) self.user_interface.txtSourceFile.setHtml(result) text_block = self.user_interface.txtSourceFile.document( ).findBlockByLineNumber(int(line)) text_cursor = self.user_interface.txtSourceFile.textCursor() text_cursor.setPosition(text_block.position()) self.user_interface.txtSourceFile.setTextCursor(text_cursor) self.user_interface.txtSourceFile.ensureCursorVisible() def get_selected_indexes(self): """ Returns a list of the currently selected indexes mapped to the source numbering. mapToSource is needed to retrive the actual row number regardless of whether filtering is applied or not. """ return [ self.proxy_model.mapToSource(index) for index in self.user_interface.tblLogData.selectedIndexes() ] def update_status_bar(self): """ Updates the status bar with relevant information """ selected_indexes = self.get_selected_indexes() if (len(selected_indexes) == 1): selected_cell_index = selected_indexes[0] number_of_occurances = len(self.columns_dict[ selected_cell_index.column()][selected_cell_index.data()]) self.user_interface.statusbar.showMessage( '["{}"] occurred {} time(s) ~ {}%'.format( selected_cell_index.data(), number_of_occurances, number_of_occurances * 100 // len(self.table_data))) elif (len(selected_indexes) == 2): row_1 = selected_indexes[0].row() row_2 = selected_indexes[1].row() time_stamp1 = float(self.table_data[row_1][self.time_stamp_column]) time_stamp2 = float(self.table_data[row_2][self.time_stamp_column]) self.user_interface.statusbar.showMessage( "Time difference = {}".format(abs(time_stamp2 - time_stamp1))) else: self.user_interface.statusbar.showMessage("") def cell_right_clicked(self, point): """ Handle the event of right-clicking on a table cell. This function is responsible for showing the context menu for the user to choose from. """ index = self.proxy_model.mapToSource( self.user_interface.tblLogData.indexAt(point)) logging.debug("Cell[%d, %d] was right-clicked. Contents = %s", index.row(), index.column(), index.data()) self.right_clicked_cell_index = index self.populate_unhide_context_menu(index.column()) self.prepare_clipboard_text() self.menuFilter.popup(QCursor.pos()) def populate_unhide_context_menu(self, column): self.unhide_menu.clear() if (self.is_filtering_mode_out): filtered_out_set = self.per_column_filter_out_set_list[column] else: filtered_out_set = set(self.columns_dict[column].keys( )) - self.per_column_filter_in_set_list[column] if (len(filtered_out_set) > 0): self.unhide_menu.setEnabled(True) for filtered_string in filtered_out_set: temp_action = QAction(filtered_string, self.unhide_menu) temp_action.triggered.connect( functools.partial( self.unhide_selected_rows_only_based_on_column, self.right_clicked_cell_index.column(), filtered_string)) self.unhide_menu.addAction(temp_action) else: self.unhide_menu.setEnabled(False) def cell_double_clicked(self, index): """ Handles the event of double-clicking on a table cell. If the double clicked cell was at the column of file:line, the function triggers external text editor (currently this is gedit on Linux) and make it point on the corresponding line. """ index = self.proxy_model.mapToSource(index) logging.info("cell[%d][%d] = %s", index.row(), index.column(), index.data()) row = index.row() file_matcher = re.search(self.file_column_pattern, self.table_data[row][self.file_column]) line_matcher = re.search(self.line_column_pattern, self.table_data[row][self.line_column]) if ((file_matcher is not None) and (line_matcher is not None)): file = file_matcher.group(1) line = line_matcher.group(1) full_path = "{}{}".format(self.root_source_path_prefix, file.strip()) logging.info("Using external editor (gedit) to open %s at line %s", file, line) editor = self.external_editor_configs["editor"] editor_command_format = self.external_editor_configs[ "editor_command_format"] editor_command = editor_command_format.format( editor_executable=editor, line_number=line, file_name=full_path) call(editor_command, shell=True) self.user_interface.tblLogData.setFocus() self.update_status_bar() def search_box_key_pressed(self, q_key_event): key = q_key_event.key() if (key in [Qt.Key_Enter, Qt.Key_Return]): if (Qt.ShiftModifier == (int(q_key_event.modifiers()) & (Qt.ShiftModifier))): self.select_search_match(False) else: self.select_search_match(True) else: QLineEdit.keyPressEvent(self.ledSearchBox, q_key_event) def cell_key_pressed(self, q_key_event): """ Handles the event of pressing a keyboard key while on the table. """ logging.warning("A key was pressed!!!") key = q_key_event.key() logging.info("Key = {}".format(key)) if (Qt.ControlModifier == (int(q_key_event.modifiers()) & (Qt.ControlModifier))): if key == Qt.Key_Delete: logging.info( "Delete key pressed while in the table. Clear all filters") self.clear_all_filters() elif key == Qt.Key_H: self.hide_rows_based_on_selected_cells() elif key == Qt.Key_O: self.show_rows_based_on_selected_cells() elif key == Qt.Key_Up: # Jump to previous match selected_indexes = self.get_selected_indexes() if (len(selected_indexes) == 1): self.go_to_prev_match(selected_indexes[0]) elif key == Qt.Key_Down: # Jump to next match selected_indexes = self.get_selected_indexes() if (len(selected_indexes) == 1): self.go_to_next_match(selected_indexes[0]) elif key == Qt.Key_PageUp: selected_indexes = self.get_selected_indexes() if (len(selected_indexes) == 1): prev_bookmark_index = self.table_model.getPrevBookmarkIndex( selected_indexes[0]) if (prev_bookmark_index is not None): self.select_cell_by_index(prev_bookmark_index) elif key == Qt.Key_PageDown: selected_indexes = self.get_selected_indexes() if (len(selected_indexes) == 1): next_bookmark_index = self.table_model.getNextBookmarkIndex( selected_indexes[0]) if (next_bookmark_index is not None): self.select_cell_by_index(next_bookmark_index) elif key == Qt.Key_C: selected_indexes = self.get_selected_indexes() self.prepare_clipboard_text() elif key == Qt.Key_B: if (Qt.ShiftModifier == (int(q_key_event.modifiers()) & (Qt.ShiftModifier))): self.table_model.clearAllBookmarks() else: selected_indexes = self.get_selected_indexes() self.table_model.toggleBookmarks(selected_indexes) elif key == Qt.Key_Left: self.select_search_match(is_forward=False) elif key == Qt.Key_Right: self.select_search_match(is_forward=True) elif key == Qt.Key_Home: self.select_cell_by_row_and_column(0, 0) elif key == Qt.Key_End: self.select_cell_by_row_and_column( self.table_model.rowCount(None) - 1, 0) elif key == Qt.Key_F5: self.load_log_file(self.log_file_full_path) else: QTableView.keyPressEvent(self.user_interface.tblLogData, q_key_event) self.update_graph_markers() def update_graph_markers(self): selected_indexes = self.get_selected_indexes() if (len(selected_indexes) == 1): for marker in self.graph_marker_list: marker.setPos(selected_indexes[0].row()) def prepare_clipboard_text(self): """ Copy the cell content to the clipboard if a single cell is selected. Or Copy the whole rows if cells from multiple rows are selected. """ selected_indexes = self.get_selected_indexes() if (len(selected_indexes) == 0): clipboard_text = "" elif (len(selected_indexes) == 1): clipboard_text = self.user_interface.tblLogData.currentIndex( ).data() else: unique_rows_set = set( [index.row() for index in sorted(selected_indexes)]) row_text_list = [ str(row) + "," + ",".join([ self.proxy_model.index(row, column, QModelIndex()).data() for column in range(self.proxy_model.columnCount()) ]) for row in sorted(unique_rows_set) ] clipboard_text = "\n".join(row_text_list) self.clipboard.setText(clipboard_text) def get_index_by_row_and_column(self, row, column): """ Get the table index value by the given row and column """ index = self.table_model.createIndex(row, column) index = self.proxy_model.mapFromSource(index) return index def select_cell_by_row_and_column(self, row, column): """ Select the cell identified by the given row and column and scroll the table view to make that cell in the middle of the visible part of the table. """ self.user_interface.tblLogData.clearSelection() index = self.get_index_by_row_and_column(row, column) self.user_interface.tblLogData.setCurrentIndex(index) self.user_interface.tblLogData.scrollTo( index, hint=QAbstractItemView.PositionAtCenter) self.user_interface.tblLogData.setFocus() self.update_status_bar() def select_cell_by_index(self, index): """ Select a cell at the given index. """ self.user_interface.tblLogData.clearSelection() index = self.proxy_model.mapFromSource(index) self.user_interface.tblLogData.setCurrentIndex(index) self.user_interface.tblLogData.scrollTo( index, hint=QAbstractItemView.PositionAtCenter) self.user_interface.tblLogData.setFocus() self.update_status_bar() def go_to_prev_match(self, selected_cell): """ Go to the prev cell that matches the currently selected cell in the same column """ matches_list = self.columns_dict[selected_cell.column()][ selected_cell.data()] index = matches_list.index(selected_cell.row()) if (index > 0): new_row = matches_list[index - 1] self.select_cell_by_row_and_column(new_row, selected_cell.column()) def go_to_next_match(self, selected_cell): """ Go to the prev cell that matches the currently selected cell in the same column """ matches_list = self.columns_dict[selected_cell.column()][ selected_cell.data()] index = matches_list.index(selected_cell.row()) if (index < (len(matches_list) - 1)): new_row = matches_list[index + 1] self.select_cell_by_row_and_column(new_row, selected_cell.column()) def get_top_left_selected_row_index(self): """ This function return the top-left selected index from the selected list. It's used for example to anchor the table view around the top-left selected cell following any change in the visible cells due to filtering """ top_left_index = None selected_indexes = self.get_selected_indexes() if (len(selected_indexes) > 0): selected_indexes = self.get_selected_indexes() top_left_index = selected_indexes[0] row = top_left_index.row() column = top_left_index.column() for index in selected_indexes[1:]: if ((index.row() < row) and (index.column() < column)): row = index.row() column = index.column() top_left_index = index return top_left_index def clear_all_filters(self): """ Clears all the current filter and return the table to its initial view. """ top_selected_index = self.get_top_left_selected_row_index() self.per_column_filter_out_set_list = [ set() for column in range(len(self.table_data[0])) ] self.per_column_filter_in_set_list = [ set() for column in range(len(self.table_data[0])) ] self.apply_filter(is_filtering_mode_out=True) if (top_selected_index != None): self.select_cell_by_index(top_selected_index) self.update_status_bar() def hide_rows_based_on_selected_cells(self): """ Hides the selected rows and any other rows with matching data. """ selected_indexes = self.get_selected_indexes() for index in selected_indexes: column = index.column() self.per_column_filter_out_set_list[column].add(index.data()) new_selected_row = None min_selected_row = selected_indexes[0].row() max_selected_row = selected_indexes[-1].row() if (min_selected_row != 0): new_selected_row = min_selected_row - 1 elif (max_selected_row != self.table_model.columnCount(None)): new_selected_row = max_selected_row + 1 self.apply_filter(is_filtering_mode_out=True) self.select_cell_by_row_and_column(new_selected_row, selected_indexes[0].column()) self.update_status_bar() def show_rows_based_on_selected_cells(self): """ Shows the selected rows and any other rows with matching data only. """ selected_indexes = self.get_selected_indexes() self.per_column_filter_in_set_list = [ set() for column in range(len(self.table_data[0])) ] for index in selected_indexes: column = index.column() self.per_column_filter_in_set_list[column].add(index.data()) self.apply_filter(is_filtering_mode_out=False) self.update_status_bar() def unhide_selected_rows_only_based_on_column(self, filter_column, filtered_out_string): """ Unhides the selected rows and any other rows with matching data. The filtering works on one column only. """ top_selected_index = self.get_top_left_selected_row_index() if (self.is_filtering_mode_out): self.per_column_filter_out_set_list[filter_column].remove( filtered_out_string) else: self.per_column_filter_in_set_list[filter_column].add( filtered_out_string) logging.debug("Unhiding: %s", filtered_out_string) self.apply_filter(self.is_filtering_mode_out) if (top_selected_index != None): self.select_cell_by_index(top_selected_index) self.update_status_bar() def apply_filter(self, is_filtering_mode_out): """ Applies the filter based on the given mode. """ self.is_filtering_mode_out = is_filtering_mode_out if (is_filtering_mode_out): self.proxy_model.setFilterOutList( self.per_column_filter_out_set_list) else: self.proxy_model.setFilterInList( self.per_column_filter_in_set_list) # This is just to trigger the proxy model to apply the filter self.proxy_model.setFilterKeyColumn(0) def dragEnterEvent(self, q_drag_enter_event): if (q_drag_enter_event.mimeData().hasFormat("text/uri-list")): q_drag_enter_event.acceptProposedAction() def dropEvent(self, q_drop_event): url_list = q_drop_event.mimeData().urls() if (len(url_list) == 0): return log_file_list = [url.toLocalFile() for url in url_list] self.log_file_full_path = log_file_list[0] self.load_log_file(self.log_file_full_path) def closeEvent(self, event): app = QApplication([]) # app.closeAllWindows() app.deleteLater() app.closeAllWindows()
class Historize(QObject): """This class handles the initialization and calls of the menus""" def __init__(self, iface): QObject.__init__(self) self.iface = iface self.dbconn = DBConn(iface) plugin_dir = os.path.dirname(__file__) # initialize locale locale = QSettings().value('locale/userLocale')[0:2] locale_path = os.path.join(plugin_dir, 'i18n', 'Historize_{}.qm'.format(locale)) if os.path.exists(locale_path): translator = QTranslator() translator.load(locale_path) QCoreApplication.installTranslator(translator) if qVersion() > '4.3.3': QCoreApplication.installTranslator(translator) def initGui(self): self.menu = QMenu() self.menu.setTitle("Historize") self.layerMenu = QMenu() self.layerMenu.setTitle("Layer") # Create menu actions self.actionInitDB = QAction(self.tr(u"Initialize Database"), self.iface.mainWindow()) self.actionInitLayer = QAction(self.tr(u"Initialize Layer"), self.iface.mainWindow()) self.actionLayerUpdate = QAction(self.tr(u"Update Layer"), self.iface.mainWindow()) self.actionLayerLoad = QAction(self.tr(u"Load Layer"), self.iface.mainWindow()) self.actionAbout = QAction(self.tr(u"About"), self.iface.mainWindow()) # Connect menu actions self.actionInitDB.triggered.connect(self.initialize_database) self.actionInitLayer.triggered.connect(self.initialize_layer) self.actionLayerLoad.triggered.connect(self.show_load_layer_dialog) self.actionLayerUpdate.triggered.connect(self.show_update_layer_dialog) self.actionAbout.triggered.connect(self.show_about_dialog) self.iface.legendInterface().currentLayerChanged.connect( self.enable_disable_gui) # Add actions to menu self.layerMenu.addActions([ self.actionInitLayer, self.actionLayerLoad, self.actionLayerUpdate ]) self.menu.addAction(self.actionInitDB) self.menu.addMenu(self.layerMenu) self.menu.addAction(self.actionAbout) self.menu.insertSeparator(self.actionAbout) menuBar = self.iface.mainWindow().menuBar() menuBar.addMenu(self.menu) # Disable unusable actions self.actionInitDB.setEnabled(False) self.actionInitLayer.setEnabled(False) self.actionLayerUpdate.setEnabled(False) self.actionLayerLoad.setEnabled(False) def unload(self): self.menu.deleteLater() def initialize_database(self): """Use Database info from layer and run historisation.sql on it.""" selectedLayer = self.iface.activeLayer() provider = selectedLayer.dataProvider() if provider.name() != 'postgres': QMessageBox.warning( self.iface.mainWindow(), self.tr(u"Invalid Layer"), self.tr(u"Layer must be provided by postgres!")) return uri = QgsDataSourceURI(provider.dataSourceUri()) conn = self.dbconn.connect_to_DB(uri) cur = conn.cursor() if conn is False: return result = QMessageBox.warning( self.iface.mainWindow(), self.tr(u"Initialize Historisation"), self.tr(u"Initialize historisation on this layers database?"), QMessageBox.No | QMessageBox.Yes) if result == QMessageBox.Yes: sqlPath = os.path.dirname( os.path.realpath(__file__)) + '/sql/historisierung.sql' try: # Ignore first three characters # which invalidate the SQL command cur.execute(open(sqlPath, "r").read()) conn.commit() QMessageBox.warning( self.iface.mainWindow(), self.tr(u"Success"), self.tr(u"Database initialized successfully!")) except psycopg2.Error as e: conn.rollback() QMessageBox.warning( self.iface.mainWindow(), self.tr(u"Error"), self.tr(u"Couldn't initialize Database.\n" + e.message)) conn.close() self.enable_disable_gui(selectedLayer) else: return def initialize_layer(self): """Use Layer info and run init() .sql query""" selectedLayer = self.iface.activeLayer() provider = selectedLayer.dataProvider() uri = QgsDataSourceURI(provider.dataSourceUri()) conn = self.dbconn.connect_to_DB(uri) if conn is False: return result = QMessageBox.warning( self.iface.mainWindow(), self.tr(u"Initialize Layer"), self.tr(u"Are you sure you wish to proceed?"), QMessageBox.No | QMessageBox.Yes) if result == QMessageBox.Yes: # Get SQL vars hasGeometry = selectedLayer.hasGeometryType() schema = uri.schema() table = uri.table() execute = SQLExecute(self.iface, self.iface.mainWindow(), uri) success, msg = execute.Init_hist_tabs(hasGeometry, schema, table) if success: QMessageBox.warning( self.iface.mainWindow(), self.tr(u"Success"), self.tr(u"Layer successfully initialized!")) else: QMessageBox.warning(self.iface.mainWindow(), self.tr(u"Error"), self.tr(u"Initialization failed!\n" + msg)) self.enable_disable_gui(selectedLayer) else: return def show_update_layer_dialog(self): """Open ImportUpdate dialog""" self.updateDialog = ImportUpdateDialog(self.iface) self.updateDialog.show() def show_load_layer_dialog(self): """Open selectDate dialog""" self.dateDialog = SelectDateDialog(self.iface) self.dateDialog.show() def show_about_dialog(self): """Show About dialog""" self.aboutDialog = AboutDialog() self.aboutDialog.show() def enable_disable_gui(self, layer): """Enable/Disable menu options based on selected layer""" self.actionInitDB.setEnabled(False) self.layerMenu.setEnabled(False) self.actionInitLayer.setEnabled(False) self.actionLayerUpdate.setEnabled(False) self.actionLayerLoad.setEnabled(False) selectedLayer = self.iface.activeLayer() if selectedLayer: provider = layer.dataProvider() if provider.name() == "postgres": self.actionInitDB.setEnabled(True) uri = QgsDataSourceURI(provider.dataSourceUri()) execute = SQLExecute(self.iface, self.iface.mainWindow(), uri) historised = execute.check_if_historised( uri.schema(), self.iface.activeLayer().name()) db_initialized = execute.db_initialize_check(uri.schema()) if db_initialized: self.actionInitDB.setEnabled(False) self.layerMenu.setEnabled(True) else: self.layerMenu.setEnabled(False) if historised: self.actionLayerUpdate.setEnabled(True) self.actionLayerLoad.setEnabled(True) else: self.actionInitLayer.setEnabled(True)
class GuiMenu(QMenuBar): """ Class GuiMenu contains the menu details incl. action functions that were moved out of main_menu module. @author: ssimons """ HELP_WEBSITE_LINK = "http://www.s-simons.de/tree_editor_help.html" MAIN_TREE = True SECOND_TREE = False def __init__(self, tree_main, tree_second, configuration, right_window, main_window, signal_wrapper): """ Creates menu elements. @param tree_main: Tree object of main file / tree (left side) @param tree_second: Tree object of second file / tree (right side) @param text_output_instance: QTextEdit object which should be used / bind with the tree wiget. @param configuration: Current Configuration object. @param right_window: QWidget object of the second (right) file/tree @param main_window: QMainWindow object to change the title @param signal_wrapper: SignalWrapper object which wraps signals """ QMenuBar.__init__(self, main_window.centralWidget()) logging.info("menu foo") self.tree_main = tree_main self.tree_second = tree_second self._conf = configuration self.widget_right_window = right_window self.main_window = main_window self.signal_wrapper = signal_wrapper self.two_windows_action = QAction("Two Windows", self) self._importer = TextImporter(self._conf) self._init_gui_menu_view() self._init_gui_menu_main_file() self.menu_second_file = QMenu("SecondFile", self.main_window.centralWidget()) self._init_gui_menu_second_file() gui_helper.change_window_title("", self.main_window) def _init_gui_menu_view(self): """Initialises the view menu ( with configuration, exit etc.) """ menu1 = QMenu("View", self.main_window.centralWidget()) self.two_windows_action.setCheckable(True) QObject.connect(self.two_windows_action, SIGNAL('triggered()'), self._show_or_hide_second_window_file) menu1.addAction(self.two_windows_action) configuration_gui_action = QAction("Configuration", self) QObject.connect(configuration_gui_action, SIGNAL('triggered()'), self._gui_open_configuration) menu1.addAction(configuration_gui_action) info_license_action = QAction("Info/License", self) QObject.connect(info_license_action, SIGNAL('triggered()'), lambda: InfoLicenseWindow(self.main_window)) menu1.addAction(info_license_action) help_gui_action = QAction("Help", self) QObject.connect(help_gui_action, SIGNAL('triggered()'), lambda: webbrowser.open(self.HELP_WEBSITE_LINK, 1)) menu1.addAction(help_gui_action) menu_exit_action = QAction("Exit", self) QObject.connect(menu_exit_action, SIGNAL('triggered()'), self._menu_exit) menu1.addSeparator() menu1.addAction(menu_exit_action) self.addMenu(menu1) logging.info("menu1" + str(menu1 is None)) def _init_gui_menu_main_file(self): """Initialises the first window / file """ menu_main_file = QMenu("MainFile", self.main_window.centralWidget()) self.addMenu(menu_main_file) menu_open_main_file_action = QAction("Open file", self) QObject.connect(menu_open_main_file_action, SIGNAL('triggered()'), self._menu_main_window_open_file) menu_main_file.addAction(menu_open_main_file_action) menu_save_main_file_action = QAction("Save file", self) QObject.connect(menu_save_main_file_action, SIGNAL('triggered()'), self._default_file_save) menu_main_file.addAction(menu_save_main_file_action) remove_elem_in_main_file_action = QAction("Remove element", self) QObject.connect(remove_elem_in_main_file_action, SIGNAL('triggered()'), self._menu_delete_elem_main_file) menu_main_file.addAction(remove_elem_in_main_file_action) copy_main_to_second_file_action = QAction("Copy to second File", self) QObject.connect(copy_main_to_second_file_action, SIGNAL('triggered()'), self._menu_copy_main_to_second_file) menu_main_file.addAction(copy_main_to_second_file_action) expand_all_action = QAction("Expand all", self) QObject.connect(expand_all_action, SIGNAL('triggered()'), lambda: self.signal_wrapper. \ signal_treeview1_expand_all.emit()) menu_main_file.addAction(expand_all_action) collapse_all_action = QAction("Collapse all", self) QObject.connect(collapse_all_action, SIGNAL('triggered()'), lambda: self.signal_wrapper. \ signal_treeview1_collapse_all.emit()) menu_main_file.addAction(collapse_all_action) menu_main_file.addSeparator() self._initialize_tree_specific_menu_entries(self.tree_main, menu_main_file, self.MAIN_TREE) def _init_gui_menu_second_file(self): """Initialises the second window / file """ self.menu_second_file.setEnabled(False) self.addMenu(self.menu_second_file) menu_open_second_file_action = QAction("Open file", self) QObject.connect(menu_open_second_file_action, SIGNAL('triggered()'), self._menu_second_window_open_file) self.menu_second_file.addAction(menu_open_second_file_action) remove_elem_in_second_file_action = QAction("Remove element", self) QObject.connect(remove_elem_in_second_file_action, SIGNAL('triggered()'), self._menu_delete_elem_second_file) self.menu_second_file.addAction(remove_elem_in_second_file_action) copy_second_to_main_file_action = QAction("Copy to main File", self) QObject.connect(copy_second_to_main_file_action, SIGNAL('triggered()'), self._menu_copy_second_to_main_file) self.menu_second_file.addAction(copy_second_to_main_file_action) expand_all_action = QAction("Expand all", self) QObject.connect(expand_all_action, SIGNAL('triggered()'), lambda: self.signal_wrapper. \ signal_treeview2_expand_all.emit()) self.menu_second_file.addAction(expand_all_action) collapse_all_action = QAction("Collapse all", self) QObject.connect(collapse_all_action, SIGNAL('triggered()'), lambda: self.signal_wrapper. \ signal_treeview2_collapse_all.emit()) self.menu_second_file.addAction(collapse_all_action) self.menu_second_file.addSeparator() self._initialize_tree_specific_menu_entries(self.tree_second, self.menu_second_file, self.SECOND_TREE) def _initialize_tree_specific_menu_entries(self, tree_instance, menu_reference, is_main_tree): """ Creates standard menu entries (that are used for both trees) for the given tree_instance @param tree_instance: Tree object that should be used. @param menu_reference: QMenu object where to add the menu entries @param is_main_tree: to differ between main and second tree. """ exchange_action = QAction("Exchange", self) QObject.connect( exchange_action, SIGNAL('triggered()'), lambda: self._menu_exchange_tree_elements(tree_instance)) menu_reference.addAction(exchange_action) data_up_action = QAction("DataUp", self) QObject.connect( data_up_action, SIGNAL('triggered()'), #call data up move lambda: QMessageBox.information(self.main_window, "Info", '''The following were susscessully moved up: ''' + "".join(tree_operations.data_up_move(tree_instance)))) menu_reference.addAction(data_up_action) data_down_action = QAction("DataDown", self) QObject.connect( data_down_action, SIGNAL('triggered()'), #call data down move lambda: QMessageBox.information(self.main_window, "Info", '''The following were successfully moved down: ''' + "".join(tree_operations.data_down_move(tree_instance) ))) menu_reference.addAction(data_down_action) data_replace_action = QAction("Replace", self) QObject.connect( data_replace_action, SIGNAL('triggered()'), lambda: self._menu_change_label_of_selected_elements(tree_instance, is_main_tree)) menu_reference.addAction(data_replace_action) data_search_and_replace_action = QAction("Search and replace", self) QObject.connect( data_search_and_replace_action, SIGNAL('triggered()'), lambda: tree_operations.data_search_and_replace(self.main_window, tree_instance)) menu_reference.addAction(data_search_and_replace_action) data_search_and_replace_action.setEnabled(False) # ------------------------------------------------ # ------------- MENU ACTIONS -------------------- # ------------------------------------------------ def _default_file_save(self): """ Action function to save to file. """ file_name = QFileDialog.getSaveFileName(self, "Save file", QDir.home().dirName(), "All files (*.*)") if not file_name.isNull(): _exporter = TextExporter(self._conf) _exporter.write_file_from_data(self.tree_main, file_name) QMessageBox.information(self.main_window, "Info", '''Please ensure the correctness of the output file by comparing (diff) the output file to the original one. See help for further information.''') def _show_or_hide_second_window_file(self): """ Action function to show/hide the second window (which means the second pair of tree and text editor). """ if self.two_windows_action.isChecked(): self.widget_right_window.show() self.main_window.resize(self.main_window.width() * 2, self.main_window.height()) self.menu_second_file.setEnabled(True) else: self.widget_right_window.hide() self.main_window.resize(self.main_window.width() / 2, self.main_window.height()) self.menu_second_file.setEnabled(False) def _gui_open_configuration(self): """ Action to open the configuration window. """ configuration_gui = ConfigurationGUI(self, self._conf) configuration_gui.show() def _menu_change_label_of_selected_elements(self, tree_instance, is_main_tree): text, ok_response = QInputDialog.getText(self.main_window, "Replace", "Replace checked to:", QLineEdit.Normal, QDir.home().dirName()) logging.debug("Replace / Change label with:" + text) if ok_response is False: return result_list = tree_operations.tree_element_change_label(tree_instance, text) #emit the signal to trigger a click event. That is needed to refresh #the texteditor, that it contains the replaced data. if is_main_tree == self.SECOND_TREE: self.signal_wrapper.signal_treeview2_clicked.emit() else: self.signal_wrapper.signal_treeview1_clicked.emit() QMessageBox.information(self.main_window, "Info", '''The labels of the following tree elements were successfully renamned/changed : ''' + "".join(result_list)) def _menu_exchange_tree_elements(self, tree_instance): """ Action to exchange (data) tree elements. """ try: tree_operations.exchange_elements(tree_instance) except NotEnoughTreeElementsCheckedException as netc: QMessageBox.warning(self.main_window, "Warning", netc.args[0]) return except NoProperTreeElementException as npte: QMessageBox.warning(self.main_window, "Warning", npte.args[0]) return except TreeElementsNotSameLevelException as tnl: QMessageBox.information(self.main_window, "Info", tnl.args[0]) return def _menu_exit(self): """ Action to exit the program. """ try: ConfigurationFileWriter.write_config(self._conf) except IOError, exc: logging.exception(exc) logging.shutdown() sys.exit()
class BrowserView(QWidget): """Luma LDAP Browser plugin """ # Custom signals used reloadSignal = QtCore.pyqtSignal(QtCore.QModelIndex) clearSignal = QtCore.pyqtSignal(QtCore.QModelIndex) __logger = logging.getLogger(__name__) def __init__(self, parent=None, configPrefix=None): """ :param configPrefix: defines the location of serverlist. :type configPrefix: string """ super(BrowserView, self).__init__(parent) self.__logger = logging.getLogger(__name__) self.setObjectName("PLUGIN_BROWSER") self.templateList = TemplateList() # The serverlist used self.serverList = ServerList(configPrefix) self.serversChangedMessage = QtGui.QErrorMessage() self.mainLayout = QtGui.QHBoxLayout(self) self.splitter = QtGui.QSplitter(self) # Create the model self.ldaptreemodel = LDAPTreeItemModel(self.serverList, self) self.ldaptreemodel.workingSignal.connect(self.setBusy) # Set up the entrylist (uses the model) self.__setupEntryList() # The editor for entries self.tabWidget = QtGui.QTabWidget(self) #self.tabWidget.setDocumentMode(True) self.tabWidget.setMovable(True) self.setMinimumWidth(200) self.tabWidget.setTabsClosable(True) self.tabWidget.tabCloseRequested.connect(self.tabCloseClicked) self.tabWidget.setUsesScrollButtons(True) sizePolicy = self.tabWidget.sizePolicy() sizePolicy.setHorizontalStretch(1) self.tabWidget.setSizePolicy(sizePolicy) # Remember and looks up open tabs self.openTabs = {} self.splitter.addWidget(self.entryList) self.splitter.addWidget(self.tabWidget) self.mainLayout.addWidget(self.splitter) # Used to signal the ldaptreemodel with a index # which needs processing (reloading, clearing) self.reloadSignal.connect(self.ldaptreemodel.reloadItem) self.clearSignal.connect(self.ldaptreemodel.clearItem) eventFilter = BrowserPluginEventFilter(self) self.installEventFilter(eventFilter) self.__createContextMenu() self.retranslateUi() self.progress = QMessageBox( 1, self.str_PLEASE_WAIT, self.str_PLEASE_WAIT_MSG, QMessageBox.Ignore, parent=self ) # For testing ONLY # AND ONLY ON SMALL LDAP-SERVERS SINCE IT LOADS BASICALLY ALL ENTIRES #import modeltest #self.modeltest = modeltest.ModelTest(self.ldaptreemodel, self); def setBusy(self, status): """ Helper-method. """ if status == True: self.progress.show() qApp.setOverrideCursor(Qt.WaitCursor) else: if not self.progress.isHidden(): self.progress.hide() qApp.restoreOverrideCursor() def __setupEntryList(self): # The view for server-content self.entryList = QtGui.QTreeView(self) self.entryList.setMinimumWidth(200) #self.entryList.setMaximumWidth(400) #self.entryList.setAlternatingRowColors(True) # Somewhat cool, but should be removed if deemed too taxing self.entryList.setAnimated(True) self.entryList.setUniformRowHeights(True) # MAJOR optimalization #self.entryList.setExpandsOnDoubleClick(False) self.entryList.setModel(self.ldaptreemodel) self.entryList.setMouseTracking(True) self.entryList.viewport().setMouseTracking(True) # For right-clicking in the tree self.entryList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.entryList.customContextMenuRequested.connect(self.rightClick) # When something is activated (doubleclick, <enter> etc.) self.entryList.activated.connect(self.viewItem) self.delegate = LoadingDelegate(self.entryList) self.entryList.setItemDelegate(self.delegate) self.entryList.setSelectionMode( QtGui.QAbstractItemView.ExtendedSelection) def __createContextMenu(self): """Creates the context menu for the tree view. """ self.contextMenu = QMenu() self.contextMenuServerSettings = QAction(self) self.contextMenu.addAction(self.contextMenuServerSettings) self.contextMenu.addSeparator() self.contextMenuOpen = QAction(self) self.contextMenu.addAction(self.contextMenuOpen) self.contextMenuReload = QAction(self) self.contextMenu.addAction(self.contextMenuReload) self.contextMenuClear = QAction(self) self.contextMenu.addAction(self.contextMenuClear) self.contextMenuFilter = QAction(self) self.contextMenu.addAction(self.contextMenuFilter) self.contextMenuLimit = QAction(self) self.contextMenu.addAction(self.contextMenuLimit) self.contextMenu.addSeparator() self.contextMenuAdd = QMenu() self.contextMenu.addMenu(self.contextMenuAdd) self.contextMenuDelete = QMenu() self.contextMenu.addMenu(self.contextMenuDelete) self.contextMenuExport = QMenu() self.contextMenu.addMenu(self.contextMenuExport) # Connect the context menu actions to the correct slots self.contextMenuServerSettings.triggered.connect( self.editServerSettings) self.contextMenuOpen.triggered.connect(self.openChoosen) self.contextMenuReload.triggered.connect(self.reloadChoosen) self.contextMenuClear.triggered.connect(self.clearChoosen) self.contextMenuFilter.triggered.connect(self.filterChoosen) self.contextMenuLimit.triggered.connect(self.limitChoosen) def rightClick(self, point): """ Called when the view is right-clicked. Displays a context menu with possible actions. :param point: contains the global screen coordinates for the right-click that generated this call. :type potin: QPoint """ # This is a list of QModelIndex objects, which will be used by # the various context menu slots. # We therfore store it as a class member self.selection = self.entryList.selectedIndexes() openSupport = True reloadSupport = True clearSupport = True filterSupport = True limitSupport = True addSupport = True deleteSupport = True exportSupport = True editServerSupport = True # The number of selected items is used for naming of the actions # added to the submenues numselected = len(self.selection) # View disabled menu if nothing selected self.contextMenu.setEnabled(True) # Remember to enable if a selection if not numselected > 0: # If nothing is selected self.contextMenu.setEnabled(False) # Disable self.contextMenu.exec_(self.entryList.mapToGlobal(point)) # Show return # Iterate through the list of selected indexes, and # validate what operations are supported. That is, # if one of the selected indexes do not support an # operation, we cannot allow to apply that operation # on the whole selection for index in self.selection: item = index.internalPointer() operations = item.getSupportedOperations() if not AbstractLDAPTreeItem.SUPPORT_OPEN & operations: openSupport = False if not AbstractLDAPTreeItem.SUPPORT_RELOAD & operations: reloadSupport = False if not AbstractLDAPTreeItem.SUPPORT_CLEAR & operations: clearSupport = False if not AbstractLDAPTreeItem.SUPPORT_FILTER & operations: filterSupport = False if not AbstractLDAPTreeItem.SUPPORT_LIMIT & operations: limitSupport = False if not AbstractLDAPTreeItem.SUPPORT_ADD & operations: addSupport = False if not AbstractLDAPTreeItem.SUPPORT_DELETE & operations: deleteSupport = False if not AbstractLDAPTreeItem.SUPPORT_EXPORT & operations: exportSupport = False if index.internalPointer().getParentServerItem() == None: editServerSupport = False # Now we just use the *Support variables to enable|disable # the context menu actions. self.contextMenuOpen.setEnabled(openSupport) self.contextMenuReload.setEnabled(reloadSupport) self.contextMenuClear.setEnabled(clearSupport) self.contextMenuFilter.setEnabled(filterSupport) self.contextMenuLimit.setEnabled(limitSupport) self.contextMenuServerSettings.setEnabled(editServerSupport) # For the submenues in the context menu, we add appropriate # actions, based on single|multi selection, or disable the menu # altogether if there is no support for the operation. if (limitSupport or filterSupport or openSupport) \ and not numselected == 1: self.contextMenuLimit.setEnabled(False) self.contextMenuFilter.setEnabled(False) self.contextMenuOpen.setEnabled(False) if addSupport and numselected == 1: self.contextMenuAdd.setEnabled(True) # template templateMenu = QMenu(self.str_TEMPLATE) self.contextMenuAdd.addMenu(templateMenu) index = self.selection[0] for template in self.templateList.getTable(): sO = index.internalPointer().smartObject() if template.server == sO.serverMeta.name: method = lambda name = template.templateName, i = index : self.addTemplateChoosen(name, i) templateMenu.addAction(template.templateName, method) else: self.contextMenuAdd.setEnabled(False) if numselected != 1: self.contextMenuServerSettings.setEnabled(False) if deleteSupport: self.contextMenuDelete.setEnabled(True) if numselected == 1: self.contextMenuDelete.addAction( self.str_ITEM, self.deleteSelection ) self.contextMenuDelete.addAction( self.str_SUBTREE_ONLY, self.deleteSubtree ) #self.contextMenuDelete.addAction( # self.str_SUBTREE_PARENTS, self.deleteSelection #) else: self.contextMenuDelete.addAction( self.str_ITEMS, self.deleteSelection ) self.contextMenuDelete.addAction( self.str_SUBTREES, self.deleteSubtree ) #self.contextMenuDelete.addAction( # self.str_SUBTREES_PARENTS, self.deleteSelection #) else: self.contextMenuDelete.setEnabled(False) if exportSupport: self.contextMenuExport.setEnabled(True) if numselected == 1: self.contextMenuExport.addAction( self.str_ITEM, self.exportItems ) self.contextMenuExport.addAction( self.str_SUBTREE, self.exportSubtrees ) self.contextMenuExport.addAction( self.str_SUBTREE_PARENTS, self.exportSubtreeWithParents ) else: self.contextMenuExport.addAction( self.str_ITEMS, self.exportItems ) self.contextMenuExport.addAction( self.str_SUBTREES, self.exportSubtrees ) self.contextMenuExport.addAction( self.str_SUBTREES_PARENTS, self.exportSubtreeWithParents ) else: self.contextMenuExport.setEnabled(False) # Finally we execute the context menu self.contextMenu.exec_(self.entryList.mapToGlobal(point)) # We need to clear all the submenues after each right click # selection, if not; the submenu actions will be added and # thus duplicated for every selection the user makes. # FIXME: Find a better way of handling this issue. self.contextMenuAdd.clear() self.contextMenuDelete.clear() self.contextMenuExport.clear() """ Following methods are called from a context-menu. """ def openChoosen(self): if len(self.selection) == 1: self.viewItem(self.selection[0]) def reloadChoosen(self): for index in self.selection: self.reloadSignal.emit(index) def clearChoosen(self): for index in self.selection: self.clearSignal.emit(index) def limitChoosen(self): # Have the item set the limit for us, the reload for index in self.selection: ok = index.internalPointer().setLimit() if ok: self.reloadSignal.emit(index) def filterChoosen(self): # Have the item set the filter, then reload for index in self.selection: ok = index.internalPointer().setFilter() if ok: self.reloadSignal.emit(index) def addTemplateChoosen(self, templateName, index): serverMeta = index.internalPointer().smartObject().serverMeta baseDN = index.internalPointer().smartObject().getDN() template = self.templateList.getTemplateObject(templateName) smartO = template.getDataObject(serverMeta, baseDN) self.addNewEntry(index, smartO, template) def addNewEntry(self, parentIndex, defaultSmartObject=None, template=None): tmp = NewEntryDialog(parentIndex, defaultSmartObject, entryTemplate=template) if tmp.exec_(): ret = QMessageBox.question(self, QtCore.QCoreApplication.translate("BrowserView","Add"), QtCore.QCoreApplication.translate("BrowserView", "Do you want to reload to show the changes?"), QMessageBox.Yes|QMessageBox.No) if ret == QMessageBox.Yes: self.ldaptreemodel.reloadItem(self.selection[0]) """ Utility-methods """ def isOpen(self, smartObject): rep = self.getRepForSmartObject(smartObject) # The {}.has_key() method will be removed in the future version # of Python. Use the 'in' operation instead. [PEP8] #if self.openTabs.has_key(str(rep)): if str(rep) in self.openTabs: return True else: return False def getRepForSmartObject(self, smartObject): serverName = smartObject.getServerAlias() dn = smartObject.getDN() return (serverName, dn) def viewItem(self, index): """Opens items for viewing. """ item = index.internalPointer() supports = item.getSupportedOperations() # If we can't open this item, then don't if not supports & AbstractLDAPTreeItem.SUPPORT_OPEN: self.__logger.debug("Item didn't support open.") return smartObject = index.internalPointer().smartObject() rep = self.getRepForSmartObject(smartObject) # If the smartobject is already open, switch to it if self.isOpen(smartObject): x = self.openTabs[str(rep)] self.tabWidget.setCurrentWidget(x) return # Saves a representation of the opened entry to avoid opening duplicates # and open it x = AdvancedObjectWidget(QtCore.QPersistentModelIndex(index)) x.initModel(smartObject) self.openTabs[str(rep)] = x self.tabWidget.addTab(x, smartObject.getPrettyRDN()) self.tabWidget.setCurrentWidget(x) def deleteIndex(self, index): # Remember the smartObject for later sO = index.internalPointer().smartObject() # Try to delete (success, message) = self.ldaptreemodel.deleteItem(index) if success: # Close open edit-windows if any self.__closeTabIfOpen(sO) # Notify success return (True, message) else: # Notify fail return (False, message) def __closeTabIfOpen(self, sO): if self.isOpen(sO): rep = self.getRepForSmartObject(sO) x = self.openTabs.pop(str(rep)) i = self.tabWidget.indexOf(x) if i != -1: self.tabWidget.removeTab(i) def deleteSelection(self, subTree=False): """Slot for the context menu. Opens the DeleteDialog with the selected entries, giving the user the option to validate the selection before deleting. This is for deleting the item + possibly it's subtree. See deleteOnlySubtreeOfSelection() for only subtree. """ # Only a single item if len(self.selection) == 1 and not subTree: # Confirmation-message ok = QMessageBox.question( self, self.str_DELETE, self.str_REALLY_DELETE, QMessageBox.Yes | QMessageBox.No ) if ok == QMessageBox.No: return index = self.selection[0] (status, message) = self.deleteIndex(index) if not status: QMessageBox.critical( self, self.str_ERROR, self.str_ERROR_MSG.format( index.data().toPyObject(), message ) ) return # Make persistent indexes and list of smartObjects to be deleted persistenSelection = [] sOList = [] for x in self.selection: persistenSelection.append(QPersistentModelIndex(x)) sOList.append(x.internalPointer().smartObject()) # Create gui self.setBusy(True) deleteDialog = DeleteDialog(sOList, subTree) self.setBusy(False) status = deleteDialog.exec_() if status: # the dialog was not canceled if subTree: # Reload the items whos subtree was deleted for x in self.selection: self.ldaptreemodel.reloadItem(x) return # If all rows were removed successfully, just call # removeRows on all selected items (reloading all items of # the parent can be expensive) if deleteDialog.passedItemsWasDeleted: for x in persistenSelection: if x.isValid: i = x.sibling(x.row(), 0) # QModelIndex self.__closeTabIfOpen( i.internalPointer().smartObject()) self.ldaptreemodel.removeRow(x.row(), x.parent()) return # If not, call reload on the parent of all the items? else: tmp = QMessageBox.question( self, self.str_DELETION, self.str_DELETION_MSG, buttons=QMessageBox.Yes | QMessageBox.No, defaultButton=QMessageBox.Yes ) if tmp == QMessageBox.Yes: for x in persistenSelection: # index might not be valid if the parent was # reloaded by a previous item if x.isValid(): self.ldaptreemodel.reloadItem(x.parent()) return # Was cancelled so do nothing else: pass def deleteSubtree(self): self.deleteSelection(subTree=True) def exportItems(self): """Slot for the context menu. """ self.__exportSelection(scope=0) def exportSubtrees(self): """Slot for the context menu. """ self.__exportSelection(scope=1) def exportSubtreeWithParents(self): """Slot for the context menu. """ self.__exportSelection(scope=2) def __exportSelection(self, scope=0): """Slot for the context menu. Opens the ExportDialog with the selected entries, giving the user the option to validate the selection before exporting. :param scope: The scope selection. 0 = SCOPE_BASE -> Item(s), 1 = SCOPE_ONELEVEL -> Subtree(s); 2 = SCOPE_SUBTREE -> Subtree(s) with parent :type scope: int """ exportObjects = [] msg = '' self.setBusy(True) for index in self.selection: smartObject = index.internalPointer().smartObject() serverName = smartObject.getServerAlias() dn = smartObject.getDN() serverObject = self.serverList.getServerObject(serverName) con = LumaConnectionWrapper(serverObject, self) # For both subtree and subtree with parent, we fetch the # whole subtree including the parent, with a basic sync # search operation. Then, if only the subtree is to be # exported, we remove the smartObject(s) selected. if scope > 0: pass # Do a search on the whole subtree # 2 = ldap.SCOPE_SUBTREE #elif scope == 2: success, e = con.bindSync() if not success: self.__logger.error(str(e)) continue success, result, e = con.searchSync(base=dn, scope=2) if success: exportObjects.extend(result) else: self.__logger.error(str(e)) # If only the subtree is to be selected, we remove # the parent, which happens to be the smartObject(s) # initialy selected. if scope == 1: exportObjects.remove(smartObject) # For scope == 0 we need not do any LDAP search operation # because we already got what we need else: exportObjects.append(smartObject) # Initialize the export dialog # and give it the items for export dialog = ExportDialog(msg) dialog.setExportItems(exportObjects) self.setBusy(False) dialog.exec_() def editServerSettings(self): """Slot for the context menu. Opens the ServerDialog with the selected server. """ try: items = self.selection serverItem = items[0].internalPointer().getParentServerItem() serverName = serverItem.serverMeta.name serverDialog = ServerDialog(serverName) r = serverDialog.exec_() if r: self.serversChangedMessage.showMessage( self.str_SERVER_CHANGED_MSG ) except Exception, e: self.__logger.error(str(e)) QMessageBox.information( self, self.str_ERROR, self.str_SEE_LOG_DETAILS )
class BrowserView(QWidget): """Luma LDAP Browser plugin """ # Custom signals used reloadSignal = QtCore.pyqtSignal(QtCore.QModelIndex) clearSignal = QtCore.pyqtSignal(QtCore.QModelIndex) __logger = logging.getLogger(__name__) def __init__(self, parent=None, configPrefix=None): """ :param configPrefix: defines the location of serverlist. :type configPrefix: string """ super(BrowserView, self).__init__(parent) self.__logger = logging.getLogger(__name__) self.setObjectName("PLUGIN_BROWSER") self.templateList = TemplateList() # The serverlist used self.serverList = ServerList(configPrefix) self.serversChangedMessage = QtGui.QErrorMessage() self.mainLayout = QtGui.QHBoxLayout(self) self.splitter = QtGui.QSplitter(self) # Create the model self.ldaptreemodel = LDAPTreeItemModel(self.serverList, self) self.ldaptreemodel.workingSignal.connect(self.setBusy) # Set up the entrylist (uses the model) self.__setupEntryList() # The editor for entries self.tabWidget = QtGui.QTabWidget(self) #self.tabWidget.setDocumentMode(True) self.tabWidget.setMovable(True) self.setMinimumWidth(200) self.tabWidget.setTabsClosable(True) self.tabWidget.tabCloseRequested.connect(self.tabCloseClicked) self.tabWidget.setUsesScrollButtons(True) sizePolicy = self.tabWidget.sizePolicy() sizePolicy.setHorizontalStretch(1) self.tabWidget.setSizePolicy(sizePolicy) # Remember and looks up open tabs self.openTabs = {} self.splitter.addWidget(self.entryList) self.splitter.addWidget(self.tabWidget) self.mainLayout.addWidget(self.splitter) # Used to signal the ldaptreemodel with a index # which needs processing (reloading, clearing) self.reloadSignal.connect(self.ldaptreemodel.reloadItem) self.clearSignal.connect(self.ldaptreemodel.clearItem) eventFilter = BrowserPluginEventFilter(self) self.installEventFilter(eventFilter) self.__createContextMenu() self.retranslateUi() self.progress = QMessageBox(1, self.str_PLEASE_WAIT, self.str_PLEASE_WAIT_MSG, QMessageBox.Ignore, parent=self) # For testing ONLY # AND ONLY ON SMALL LDAP-SERVERS SINCE IT LOADS BASICALLY ALL ENTIRES #import modeltest #self.modeltest = modeltest.ModelTest(self.ldaptreemodel, self); def setBusy(self, status): """ Helper-method. """ if status == True: self.progress.show() qApp.setOverrideCursor(Qt.WaitCursor) else: if not self.progress.isHidden(): self.progress.hide() qApp.restoreOverrideCursor() def __setupEntryList(self): # The view for server-content self.entryList = QtGui.QTreeView(self) self.entryList.setMinimumWidth(200) #self.entryList.setMaximumWidth(400) #self.entryList.setAlternatingRowColors(True) # Somewhat cool, but should be removed if deemed too taxing self.entryList.setAnimated(True) self.entryList.setUniformRowHeights(True) # MAJOR optimalization #self.entryList.setExpandsOnDoubleClick(False) self.entryList.setModel(self.ldaptreemodel) self.entryList.setMouseTracking(True) self.entryList.viewport().setMouseTracking(True) # For right-clicking in the tree self.entryList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.entryList.customContextMenuRequested.connect(self.rightClick) # When something is activated (doubleclick, <enter> etc.) self.entryList.activated.connect(self.viewItem) self.delegate = LoadingDelegate(self.entryList) self.entryList.setItemDelegate(self.delegate) self.entryList.setSelectionMode( QtGui.QAbstractItemView.ExtendedSelection) def __createContextMenu(self): """Creates the context menu for the tree view. """ self.contextMenu = QMenu() self.contextMenuServerSettings = QAction(self) self.contextMenu.addAction(self.contextMenuServerSettings) self.contextMenu.addSeparator() self.contextMenuOpen = QAction(self) self.contextMenu.addAction(self.contextMenuOpen) self.contextMenuReload = QAction(self) self.contextMenu.addAction(self.contextMenuReload) self.contextMenuClear = QAction(self) self.contextMenu.addAction(self.contextMenuClear) self.contextMenuFilter = QAction(self) self.contextMenu.addAction(self.contextMenuFilter) self.contextMenuLimit = QAction(self) self.contextMenu.addAction(self.contextMenuLimit) self.contextMenu.addSeparator() self.contextMenuAdd = QMenu() self.contextMenu.addMenu(self.contextMenuAdd) self.contextMenuDelete = QMenu() self.contextMenu.addMenu(self.contextMenuDelete) self.contextMenuExport = QMenu() self.contextMenu.addMenu(self.contextMenuExport) # Connect the context menu actions to the correct slots self.contextMenuServerSettings.triggered.connect( self.editServerSettings) self.contextMenuOpen.triggered.connect(self.openChoosen) self.contextMenuReload.triggered.connect(self.reloadChoosen) self.contextMenuClear.triggered.connect(self.clearChoosen) self.contextMenuFilter.triggered.connect(self.filterChoosen) self.contextMenuLimit.triggered.connect(self.limitChoosen) def rightClick(self, point): """ Called when the view is right-clicked. Displays a context menu with possible actions. :param point: contains the global screen coordinates for the right-click that generated this call. :type potin: QPoint """ # This is a list of QModelIndex objects, which will be used by # the various context menu slots. # We therfore store it as a class member self.selection = self.entryList.selectedIndexes() openSupport = True reloadSupport = True clearSupport = True filterSupport = True limitSupport = True addSupport = True deleteSupport = True exportSupport = True editServerSupport = True # The number of selected items is used for naming of the actions # added to the submenues numselected = len(self.selection) # View disabled menu if nothing selected self.contextMenu.setEnabled(True) # Remember to enable if a selection if not numselected > 0: # If nothing is selected self.contextMenu.setEnabled(False) # Disable self.contextMenu.exec_(self.entryList.mapToGlobal(point)) # Show return # Iterate through the list of selected indexes, and # validate what operations are supported. That is, # if one of the selected indexes do not support an # operation, we cannot allow to apply that operation # on the whole selection for index in self.selection: item = index.internalPointer() operations = item.getSupportedOperations() if not AbstractLDAPTreeItem.SUPPORT_OPEN & operations: openSupport = False if not AbstractLDAPTreeItem.SUPPORT_RELOAD & operations: reloadSupport = False if not AbstractLDAPTreeItem.SUPPORT_CLEAR & operations: clearSupport = False if not AbstractLDAPTreeItem.SUPPORT_FILTER & operations: filterSupport = False if not AbstractLDAPTreeItem.SUPPORT_LIMIT & operations: limitSupport = False if not AbstractLDAPTreeItem.SUPPORT_ADD & operations: addSupport = False if not AbstractLDAPTreeItem.SUPPORT_DELETE & operations: deleteSupport = False if not AbstractLDAPTreeItem.SUPPORT_EXPORT & operations: exportSupport = False if index.internalPointer().getParentServerItem() == None: editServerSupport = False # Now we just use the *Support variables to enable|disable # the context menu actions. self.contextMenuOpen.setEnabled(openSupport) self.contextMenuReload.setEnabled(reloadSupport) self.contextMenuClear.setEnabled(clearSupport) self.contextMenuFilter.setEnabled(filterSupport) self.contextMenuLimit.setEnabled(limitSupport) self.contextMenuServerSettings.setEnabled(editServerSupport) # For the submenues in the context menu, we add appropriate # actions, based on single|multi selection, or disable the menu # altogether if there is no support for the operation. if (limitSupport or filterSupport or openSupport) \ and not numselected == 1: self.contextMenuLimit.setEnabled(False) self.contextMenuFilter.setEnabled(False) self.contextMenuOpen.setEnabled(False) if addSupport and numselected == 1: self.contextMenuAdd.setEnabled(True) # template templateMenu = QMenu(self.str_TEMPLATE) self.contextMenuAdd.addMenu(templateMenu) index = self.selection[0] for template in self.templateList.getTable(): sO = index.internalPointer().smartObject() if template.server == sO.serverMeta.name: method = lambda name=template.templateName, i=index: self.addTemplateChoosen( name, i) templateMenu.addAction(template.templateName, method) else: self.contextMenuAdd.setEnabled(False) if numselected != 1: self.contextMenuServerSettings.setEnabled(False) if deleteSupport: self.contextMenuDelete.setEnabled(True) if numselected == 1: self.contextMenuDelete.addAction(self.str_ITEM, self.deleteSelection) self.contextMenuDelete.addAction(self.str_SUBTREE_ONLY, self.deleteSubtree) #self.contextMenuDelete.addAction( # self.str_SUBTREE_PARENTS, self.deleteSelection #) else: self.contextMenuDelete.addAction(self.str_ITEMS, self.deleteSelection) self.contextMenuDelete.addAction(self.str_SUBTREES, self.deleteSubtree) #self.contextMenuDelete.addAction( # self.str_SUBTREES_PARENTS, self.deleteSelection #) else: self.contextMenuDelete.setEnabled(False) if exportSupport: self.contextMenuExport.setEnabled(True) if numselected == 1: self.contextMenuExport.addAction(self.str_ITEM, self.exportItems) self.contextMenuExport.addAction(self.str_SUBTREE, self.exportSubtrees) self.contextMenuExport.addAction(self.str_SUBTREE_PARENTS, self.exportSubtreeWithParents) else: self.contextMenuExport.addAction(self.str_ITEMS, self.exportItems) self.contextMenuExport.addAction(self.str_SUBTREES, self.exportSubtrees) self.contextMenuExport.addAction(self.str_SUBTREES_PARENTS, self.exportSubtreeWithParents) else: self.contextMenuExport.setEnabled(False) # Finally we execute the context menu self.contextMenu.exec_(self.entryList.mapToGlobal(point)) # We need to clear all the submenues after each right click # selection, if not; the submenu actions will be added and # thus duplicated for every selection the user makes. # FIXME: Find a better way of handling this issue. self.contextMenuAdd.clear() self.contextMenuDelete.clear() self.contextMenuExport.clear() """ Following methods are called from a context-menu. """ def openChoosen(self): if len(self.selection) == 1: self.viewItem(self.selection[0]) def reloadChoosen(self): for index in self.selection: self.reloadSignal.emit(index) def clearChoosen(self): for index in self.selection: self.clearSignal.emit(index) def limitChoosen(self): # Have the item set the limit for us, the reload for index in self.selection: ok = index.internalPointer().setLimit() if ok: self.reloadSignal.emit(index) def filterChoosen(self): # Have the item set the filter, then reload for index in self.selection: ok = index.internalPointer().setFilter() if ok: self.reloadSignal.emit(index) def addTemplateChoosen(self, templateName, index): serverMeta = index.internalPointer().smartObject().serverMeta baseDN = index.internalPointer().smartObject().getDN() template = self.templateList.getTemplateObject(templateName) smartO = template.getDataObject(serverMeta, baseDN) self.addNewEntry(index, smartO, template) def addNewEntry(self, parentIndex, defaultSmartObject=None, template=None): tmp = NewEntryDialog(parentIndex, defaultSmartObject, entryTemplate=template) if tmp.exec_(): ret = QMessageBox.question( self, QtCore.QCoreApplication.translate("BrowserView", "Add"), QtCore.QCoreApplication.translate( "BrowserView", "Do you want to reload to show the changes?"), QMessageBox.Yes | QMessageBox.No) if ret == QMessageBox.Yes: self.ldaptreemodel.reloadItem(self.selection[0]) """ Utility-methods """ def isOpen(self, smartObject): rep = self.getRepForSmartObject(smartObject) # The {}.has_key() method will be removed in the future version # of Python. Use the 'in' operation instead. [PEP8] #if self.openTabs.has_key(str(rep)): if str(rep) in self.openTabs: return True else: return False def getRepForSmartObject(self, smartObject): serverName = smartObject.getServerAlias() dn = smartObject.getDN() return (serverName, dn) def viewItem(self, index): """Opens items for viewing. """ item = index.internalPointer() supports = item.getSupportedOperations() # If we can't open this item, then don't if not supports & AbstractLDAPTreeItem.SUPPORT_OPEN: self.__logger.debug("Item didn't support open.") return smartObject = index.internalPointer().smartObject() rep = self.getRepForSmartObject(smartObject) # If the smartobject is already open, switch to it if self.isOpen(smartObject): x = self.openTabs[str(rep)] self.tabWidget.setCurrentWidget(x) return # Saves a representation of the opened entry to avoid opening duplicates # and open it x = AdvancedObjectWidget(QtCore.QPersistentModelIndex(index)) x.initModel(smartObject) self.openTabs[str(rep)] = x self.tabWidget.addTab(x, smartObject.getPrettyRDN()) self.tabWidget.setCurrentWidget(x) def deleteIndex(self, index): # Remember the smartObject for later sO = index.internalPointer().smartObject() # Try to delete (success, message) = self.ldaptreemodel.deleteItem(index) if success: # Close open edit-windows if any self.__closeTabIfOpen(sO) # Notify success return (True, message) else: # Notify fail return (False, message) def __closeTabIfOpen(self, sO): if self.isOpen(sO): rep = self.getRepForSmartObject(sO) x = self.openTabs.pop(str(rep)) i = self.tabWidget.indexOf(x) if i != -1: self.tabWidget.removeTab(i) def deleteSelection(self, subTree=False): """Slot for the context menu. Opens the DeleteDialog with the selected entries, giving the user the option to validate the selection before deleting. This is for deleting the item + possibly it's subtree. See deleteOnlySubtreeOfSelection() for only subtree. """ # Only a single item if len(self.selection) == 1 and not subTree: # Confirmation-message ok = QMessageBox.question(self, self.str_DELETE, self.str_REALLY_DELETE, QMessageBox.Yes | QMessageBox.No) if ok == QMessageBox.No: return index = self.selection[0] (status, message) = self.deleteIndex(index) if not status: QMessageBox.critical( self, self.str_ERROR, self.str_ERROR_MSG.format(index.data().toPyObject(), message)) return # Make persistent indexes and list of smartObjects to be deleted persistenSelection = [] sOList = [] for x in self.selection: persistenSelection.append(QPersistentModelIndex(x)) sOList.append(x.internalPointer().smartObject()) # Create gui self.setBusy(True) deleteDialog = DeleteDialog(sOList, subTree) self.setBusy(False) status = deleteDialog.exec_() if status: # the dialog was not canceled if subTree: # Reload the items whos subtree was deleted for x in self.selection: self.ldaptreemodel.reloadItem(x) return # If all rows were removed successfully, just call # removeRows on all selected items (reloading all items of # the parent can be expensive) if deleteDialog.passedItemsWasDeleted: for x in persistenSelection: if x.isValid: i = x.sibling(x.row(), 0) # QModelIndex self.__closeTabIfOpen( i.internalPointer().smartObject()) self.ldaptreemodel.removeRow(x.row(), x.parent()) return # If not, call reload on the parent of all the items? else: tmp = QMessageBox.question(self, self.str_DELETION, self.str_DELETION_MSG, buttons=QMessageBox.Yes | QMessageBox.No, defaultButton=QMessageBox.Yes) if tmp == QMessageBox.Yes: for x in persistenSelection: # index might not be valid if the parent was # reloaded by a previous item if x.isValid(): self.ldaptreemodel.reloadItem(x.parent()) return # Was cancelled so do nothing else: pass def deleteSubtree(self): self.deleteSelection(subTree=True) def exportItems(self): """Slot for the context menu. """ self.__exportSelection(scope=0) def exportSubtrees(self): """Slot for the context menu. """ self.__exportSelection(scope=1) def exportSubtreeWithParents(self): """Slot for the context menu. """ self.__exportSelection(scope=2) def __exportSelection(self, scope=0): """Slot for the context menu. Opens the ExportDialog with the selected entries, giving the user the option to validate the selection before exporting. :param scope: The scope selection. 0 = SCOPE_BASE -> Item(s), 1 = SCOPE_ONELEVEL -> Subtree(s); 2 = SCOPE_SUBTREE -> Subtree(s) with parent :type scope: int """ exportObjects = [] msg = '' self.setBusy(True) for index in self.selection: smartObject = index.internalPointer().smartObject() serverName = smartObject.getServerAlias() dn = smartObject.getDN() serverObject = self.serverList.getServerObject(serverName) con = LumaConnectionWrapper(serverObject, self) # For both subtree and subtree with parent, we fetch the # whole subtree including the parent, with a basic sync # search operation. Then, if only the subtree is to be # exported, we remove the smartObject(s) selected. if scope > 0: pass # Do a search on the whole subtree # 2 = ldap.SCOPE_SUBTREE #elif scope == 2: success, e = con.bindSync() if not success: self.__logger.error(str(e)) continue success, result, e = con.searchSync(base=dn, scope=2) if success: exportObjects.extend(result) else: self.__logger.error(str(e)) # If only the subtree is to be selected, we remove # the parent, which happens to be the smartObject(s) # initialy selected. if scope == 1: exportObjects.remove(smartObject) # For scope == 0 we need not do any LDAP search operation # because we already got what we need else: exportObjects.append(smartObject) # Initialize the export dialog # and give it the items for export dialog = ExportDialog(msg) dialog.setExportItems(exportObjects) self.setBusy(False) dialog.exec_() def editServerSettings(self): """Slot for the context menu. Opens the ServerDialog with the selected server. """ try: items = self.selection serverItem = items[0].internalPointer().getParentServerItem() serverName = serverItem.serverMeta.name serverDialog = ServerDialog(serverName) r = serverDialog.exec_() if r: self.serversChangedMessage.showMessage( self.str_SERVER_CHANGED_MSG) except Exception, e: self.__logger.error(str(e)) QMessageBox.information(self, self.str_ERROR, self.str_SEE_LOG_DETAILS)