Example #1
0
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()
Example #2
0
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
Example #3
0
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
Example #4
0
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()
Example #5
0
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)
Example #6
0
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()
Example #7
0
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
            )
Example #8
0
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)