Exemplo n.º 1
0
class Channel(QtCore.QObject):
    finished = Signal(object)
    result = Signal(object)
Exemplo n.º 2
0
class LineProfilerDataTree(QTreeWidget):
    """
    Convenience tree widget (with built-in model)
    to store and view line profiler data.
    """
    edit_goto = Signal(str, int, str)

    def __init__(self, parent=None):
        QTreeWidget.__init__(self, parent)
        self.header_list = [
            _('Line #'),
            _('Hits'),
            _('Time (ms)'),
            _('Per hit (ms)'),
            _('% Time'),
            _('Line contents')
        ]
        self.stats = None  # To be filled by self.load_data()
        self.max_time = 0  # To be filled by self.load_data()
        self.header().setDefaultAlignment(Qt.AlignCenter)
        self.setColumnCount(len(self.header_list))
        self.setHeaderLabels(self.header_list)
        self.clear()
        self.itemActivated.connect(self.item_activated)

    def show_tree(self):
        """Populate the tree with line profiler data and display it."""
        self.clear()  # Clear before re-populating
        self.setItemsExpandable(True)
        self.setSortingEnabled(False)
        self.populate_tree()
        self.expandAll()
        for col in range(self.columnCount() - 1):
            self.resizeColumnToContents(col)
        if self.topLevelItemCount() > 1:
            self.collapseAll()
        self.setSortingEnabled(True)
        self.sortItems(COL_POS, Qt.AscendingOrder)

    def load_data(self, profdatafile):
        """Load line profiler data saved by kernprof module"""
        # lstats has the following layout :
        # lstats.timings =
        #     {(filename1, line_no1, function_name1):
        #         [(line_no1, hits1, total_time1),
        #          (line_no2, hits2, total_time2)],
        #      (filename2, line_no2, function_name2):
        #         [(line_no1, hits1, total_time1),
        #          (line_no2, hits2, total_time2),
        #          (line_no3, hits3, total_time3)]}
        # lstats.unit = time_factor
        with open(profdatafile, 'rb') as fid:
            lstats = pickle.load(fid)

        # First pass to group by filename
        self.stats = dict()
        linecache.checkcache()
        for func_info, stats in lstats.timings.items():
            # func_info is a tuple containing (filename, line, function anme)
            filename, start_line_no = func_info[:2]

            # Read code
            start_line_no -= 1  # include the @profile decorator
            all_lines = linecache.getlines(filename)
            block_lines = inspect.getblock(all_lines[start_line_no:])

            # Loop on each line of code
            func_stats = []
            func_total_time = 0.0
            next_stat_line = 0
            for line_no, code_line in enumerate(block_lines):
                line_no += start_line_no + 1  # Lines start at 1
                code_line = code_line.rstrip('\n')
                if (next_stat_line >= len(stats)
                        or line_no != stats[next_stat_line][0]):
                    # Line didn't run
                    hits, line_total_time, time_per_hit = None, None, None
                else:
                    # Compute line times
                    hits, line_total_time = stats[next_stat_line][1:]
                    line_total_time *= lstats.unit
                    time_per_hit = line_total_time / hits
                    func_total_time += line_total_time
                    next_stat_line += 1
                func_stats.append(
                    [line_no, code_line, line_total_time, time_per_hit, hits])

            # Compute percent time
            for line in func_stats:
                line_total_time = line[2]
                if line_total_time is None:
                    line.append(None)
                else:
                    line.append(line_total_time / func_total_time)

            # Fill dict
            self.stats[func_info] = [func_stats, func_total_time]

    def fill_item(self, item, filename, line_no, code, time, percent, perhit,
                  hits):
        item.setData(COL_POS, Qt.UserRole, (osp.normpath(filename), line_no))

        item.setData(COL_NO, Qt.DisplayRole, line_no)

        item.setData(COL_LINE, Qt.DisplayRole, code)

        if percent is None:
            percent = ''
        else:
            percent = '%.1f' % (100 * percent)
        item.setData(COL_PERCENT, Qt.DisplayRole, percent)
        item.setTextAlignment(COL_PERCENT, Qt.AlignCenter)

        if time is None:
            time = ''
        else:
            time = '%.3f' % (time * 1e3)
        item.setData(COL_TIME, Qt.DisplayRole, time)
        item.setTextAlignment(COL_TIME, Qt.AlignCenter)

        if perhit is None:
            perhit = ''
        else:
            perhit = '%.3f' % (perhit * 1e3)
        item.setData(COL_PERHIT, Qt.DisplayRole, perhit)
        item.setTextAlignment(COL_PERHIT, Qt.AlignCenter)

        if hits is None:
            hits = ''
        else:
            hits = '%d' % hits
        item.setData(COL_HITS, Qt.DisplayRole, hits)
        item.setTextAlignment(COL_HITS, Qt.AlignCenter)

    def populate_tree(self):
        """Create each item (and associated data) in the tree"""
        if not self.stats:
            warn_item = TreeWidgetItem(self)
            warn_item.setData(
                0, Qt.DisplayRole,
                _('No timings to display. '
                  'Did you forget to add @profile decorators ?').format(
                      url=WEBSITE_URL))
            warn_item.setFirstColumnSpanned(True)
            warn_item.setTextAlignment(0, Qt.AlignCenter)
            font = warn_item.font(0)
            font.setStyle(QFont.StyleItalic)
            warn_item.setFont(0, font)
            return

        try:
            monospace_font = self.window().editor.get_plugin_font()
        except AttributeError:  # If run standalone for testing
            monospace_font = QFont("Courier New")
            monospace_font.setPointSize(10)

        for func_info, func_data in self.stats.items():
            # Function name and position
            filename, start_line_no, func_name = func_info
            func_stats, func_total_time = func_data
            func_item = TreeWidgetItem(self)
            func_item.setData(
                0, Qt.DisplayRole,
                _('{func_name} ({time_ms:.3f}ms) in file "{filename}", '
                  'line {line_no}').format(filename=filename,
                                           line_no=start_line_no,
                                           func_name=func_name,
                                           time_ms=func_total_time * 1e3))
            func_item.setFirstColumnSpanned(True)
            func_item.setData(COL_POS, Qt.UserRole,
                              (osp.normpath(filename), start_line_no))

            # For sorting by time
            func_item.setData(COL_TIME, Qt.DisplayRole, func_total_time * 1e3)
            func_item.setData(COL_PERCENT, Qt.DisplayRole,
                              func_total_time * 1e3)

            if self.parent().use_colors:
                # Choose deteministic unique color for the function
                md5 = hashlib.md5(
                    (filename + func_name).encode("utf8")).hexdigest()
                hue = (int(md5[:2], 16) - 68) % 360  # avoid blue (unreadable)
                func_color = QColor.fromHsv(hue, 200, 255)
            else:
                # Red color only
                func_color = QColor.fromRgb(255, 0, 0)

            # Lines of code
            for line_info in func_stats:
                line_item = TreeWidgetItem(func_item)
                (line_no, code_line, line_total_time, time_per_hit, hits,
                 percent) = line_info
                self.fill_item(line_item, filename, line_no, code_line,
                               line_total_time, percent, time_per_hit, hits)

                # Color background
                if line_total_time is not None:
                    alpha = percent
                    color = QColor(func_color)
                    color.setAlphaF(alpha)  # Returns None
                    color = QBrush(color)
                    for col in range(self.columnCount()):
                        line_item.setBackground(col, color)
                else:

                    for col in range(self.columnCount()):
                        line_item.setForeground(col, CODE_NOT_RUN_COLOR)

                # Monospace font for code
                line_item.setFont(COL_LINE, monospace_font)

    def item_activated(self, item):
        filename, line_no = item.data(COL_POS, Qt.UserRole)
        self.edit_goto.emit(filename, line_no, '')
Exemplo n.º 3
0
class PMGFilesTreeview(QTreeView):
    """
        文件树
    """
    open_signal = Signal(str)
    open_folder_signal = Signal(str)
    new_file_signal = Signal(str)
    new_folder_signal = Signal(str)
    delete_file_signal = Signal(str)
    rename_file_signal = Signal(str, str)

    signal_ext_filter_adapt = Signal(bool)
    signal_ext_filter_changed = Signal(dict)

    def __init__(self, initial_dir: str = '', parent=None):
        super().__init__(parent)
        self.initial_dir = initial_dir
        self.setup_ui()
        self.bind_events()

        self.filter_exts = True
        self.exts_to_filter = {
            'Program Scripts': {
                '.pyx': True,
                '.py': True,
                '.c': True,
                '.pyi': True,
                '.dll': True,
                '.h': True,
                '.cpp': True,
                '.ipynb': True,
                '.sh': True,
                '.cmd': True,
                '.bat': True
            },
            'Documents': {
                '.txt': True,
                '.md': True,
                '.doc': True,
                '.docx': True,
                '.ppt': True,
                '.pptx': True,
                '.html': True
            },
            'Data Files': {
                '.csv': True,
                '.xls': True,
                '.xlsx': True,
                '.tab': True,
                '.dat': True,
                '.tsv': True,
                '.sav': True,
                '.zsav': True,
                '.sas7bdat': True,
                '.pkl': True,
                '.json': True,
                '.mat': True,
                '.pmjson': True,
                '.pmd': True
            },
            'Medias': {
                '.mp3': False,
                '.mp4': False,
                '.avi': False,
                '.wma': False,
                '.png': True,
                '.jpg': True,
                '.svg': True
            },
            'Resources': {
                '.qm': True,
                '.ts': True
            }
        }

    def setup_ui(self):
        """
        界面初始化
        :return:
        """

        self.translator = create_translator(path=os.path.join(
            os.path.dirname(__file__), 'translations',
            'qt_{0}.qm'.format(QLocale.system().name())))  # translator

        self.setTabKeyNavigation(True)
        self.setDragEnabled(True)
        self.setDragDropOverwriteMode(True)
        self.setAlternatingRowColors(False)
        self.setUniformRowHeights(True)
        self.setSortingEnabled(True)
        self.setAnimated(True)
        self.setAllColumnsShowFocus(False)
        self.setWordWrap(False)
        self.setHeaderHidden(False)
        self.setObjectName("treeView_files")
        self.header().setSortIndicatorShown(True)

        self.model = PMFileSystemModel()
        self.model.setRootPath(self.initial_dir)

        self.setModel(self.model)
        self.setRootIndex(self.model.index(self.initial_dir))
        self.setAnimated(False)
        self.setSortingEnabled(True)  # 启用排序
        self.header().setSortIndicatorShown(True)  # 启用标题排序
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.show_context_menu)
        self.init_context_menu()

    def bind_events(self):
        """
        回调、事件与信号初始化
        :return:
        """
        self.doubleClicked.connect(lambda index: self.on_open())

        self.openAction.triggered.connect(self.on_open)
        self.importAction.triggered.connect(self.on_import)
        self.renameAction.triggered.connect(self.on_rename)
        self.deleteAction.triggered.connect(self.on_delete)

        self.copyAction.triggered.connect(self.on_copy)
        self.pasteAction.triggered.connect(self.on_paste)
        self.filterAction.triggered.connect(
            self.show_ext_filter_selection_dialog)

        self.copyPathAction.triggered.connect(self.copy_path)
        self.new_file_action.triggered.connect(lambda: self.on_new_file(''))
        self.new_python_file_action.triggered.connect(
            lambda: self.on_new_file('py'))
        self.new_folder_action.triggered.connect(self.on_new_folder)
        self.open_file_manager_action.triggered.connect(
            self.on_open_file_manager)

        self.rename_shortcut.activated.connect(self.on_rename)
        self.paste_shortcut.activated.connect(self.on_paste)
        self.copy_shortcut.activated.connect(self.on_copy)
        self.open_shortcut.activated.connect(self.on_open)
        self.delete_shortcut.activated.connect(self.on_delete)
        self.goto_parent_path_shortcut.activated.connect(
            self.slot_goto_parent_path)

        self.customContextMenuRequested.connect(self.show_context_menu)

    def init_context_menu(self):
        """
        初始化右键菜单
        :return:
        """
        self.contextMenu = QMenu(self)
        self.openAction = self.contextMenu.addAction(self.tr('Open'))

        self.importAction = self.contextMenu.addAction(self.tr('Import'))
        self.importAction.setEnabled(False)

        self.new_file_or_folder_menu = QMenu(self.tr('New..'))
        self.contextMenu.addMenu(self.new_file_or_folder_menu)
        self.new_file_action = self.new_file_or_folder_menu.addAction(
            self.tr('File..'))
        self.new_python_file_action = self.new_file_or_folder_menu.addAction(
            self.tr('Python File'))
        self.new_folder_action = self.new_file_or_folder_menu.addAction(
            self.tr('Folder'))
        self.new_file_or_folder_menu.addSeparator()

        self.copyAction = self.contextMenu.addAction(self.tr("Copy"))
        self.pasteAction = self.contextMenu.addAction(self.tr("Paste"))
        self.pasteAction.setEnabled(False)

        self.renameAction = self.contextMenu.addAction(self.tr('Rename'))
        self.deleteAction = self.contextMenu.addAction(self.tr('Delete'))

        self.filterAction = self.contextMenu.addAction(self.tr('Filter'))
        self.copyPathAction = self.contextMenu.addAction(self.tr('Copy Path'))

        self.open_file_manager_action = self.contextMenu.addAction(
            self.tr('Open Explorer'))

        self.renameAction.setShortcut(QKeySequence('F2'))
        self.copyAction.setShortcut(QKeySequence('Ctrl+C'))
        self.pasteAction.setShortcut(QKeySequence('Ctrl+V'))
        self.deleteAction.setShortcut(QKeySequence('Delete'))

        self.rename_shortcut = QShortcut(QKeySequence('F2'),
                                         self,
                                         context=Qt.WidgetShortcut)
        self.copy_shortcut = QShortcut(QKeySequence.Copy,
                                       self,
                                       context=Qt.WidgetShortcut)
        self.paste_shortcut = QShortcut(QKeySequence.Paste,
                                        self,
                                        context=Qt.WidgetShortcut)
        self.delete_shortcut = QShortcut(QKeySequence('Delete'),
                                         self,
                                         context=Qt.WidgetShortcut)
        self.open_shortcut = QShortcut(QKeySequence('Return'),
                                       self,
                                       context=Qt.WidgetShortcut)
        self.goto_parent_path_shortcut = QShortcut(QKeySequence('Backspace'),
                                                   self,
                                                   context=Qt.WidgetShortcut)

    def show_context_menu(self):
        """
        显示上下文右键菜单
        :return:
        """
        self.contextMenu.popup(QCursor.pos())
        self.contextMenu.show()

    def get_current_file_path(self):
        """
        获取当前选中文件的路径。
        如果当前没有选中的文件,就返回根路径。
        :return:
        """
        if len(self.selectedIndexes()) > 0:
            index = self.currentIndex()
            file_info = self.model.fileInfo(index)
            return file_info.absoluteFilePath()
        else:
            return self.get_root_path()

    def get_root_path(self):
        """
        获取根路径
        :return:
        """
        return self.model.rootPath()

    def set_item_focus(self, file_path: str):
        """
        set item focus in TreeView
        :param file_path: File or Dir
        :return:
        """
        self.setCurrentIndex(self.model.index(file_path))

    def on_open_file_manager(self):
        path = self.get_current_file_path()
        print(path)
        if os.path.isdir(path):
            open_file_manager(path)
        else:
            open_file_manager(os.path.dirname(path))

        # if os.path.exists(new_folder_path):
        #     self.set_item_focus(new_folder_path)  # 设置focus liugang 200923
        #     QMessageBox.critical(self, self.tr('Error'),
        #                          self.tr('Folder %s already exists!' % name))
        #     return
        # else:
        #     os.mkdir(new_folder_path)
        #     self.new_folder_signal[str].emit(new_folder_path)
        #     self.set_item_focus(new_folder_path)  # 设置focus liugang 200923

    def on_new_folder(self):
        """
        新建文件夹时出发的回调
        :return:
        """
        path = self.get_current_file_path()
        name, stat = QInputDialog.getText(self,
                                          self.tr('Please Input folder name'),
                                          '', QLineEdit.Normal, '')
        if name.find('.') != -1:
            QMessageBox.critical(self, self.tr('Error'),
                                 self.tr('Folder name %s is illeagal!' % name))
            return
        if stat:
            if os.path.isdir(path):
                new_folder_path = os.path.join(path, name)
            else:
                new_folder_path = os.path.join(os.path.dirname(path), name)

            if os.path.exists(new_folder_path):
                self.set_item_focus(new_folder_path)  # 设置focus liugang 200923
                QMessageBox.critical(
                    self, self.tr('Error'),
                    self.tr('Folder %s already exists!' % name))
                return
            else:
                os.mkdir(new_folder_path)
                self.new_folder_signal[str].emit(new_folder_path)
                self.set_item_focus(new_folder_path)  # 设置focus liugang 200923

    def on_new_file(self, ext: str = ''):
        """
        新建文件时触发的回调
        :return:
        """
        path = self.get_current_file_path()
        dlg = InputFilenameDialog(parent=self,
                                  title=self.tr('Please input file name'),
                                  ext=ext)

        dlg.exec_()
        name = dlg.name_input.text()
        stat = dlg.status
        if stat:
            if os.path.isdir(path):
                new_file_path = os.path.join(path, name)
            else:
                new_file_path = os.path.join(os.path.dirname(path), name)

            if os.path.exists(new_file_path):
                self.set_item_focus(new_file_path)  # 设置focus  liugang 200923
                QMessageBox.critical(self, self.tr('Error'),
                                     self.tr('File %s already exists!' % name))
                return
            with open(new_file_path, 'wb') as f:
                f.close()
                self.new_file_signal[str].emit(new_file_path)

            self.set_item_focus(new_file_path)
            self.on_open()  # 创建文件后打开  liugang 200923

    def on_open(self):
        """
        点击‘open’时候触发的回调, 等效的方式还有双击以及按下回车键。
        :return:
        """
        path = self.get_current_file_path()
        if os.path.isdir(path):
            self.open_folder_signal.emit(path)
        else:
            self.open_signal[str].emit(path)

    def on_import(self):
        """

        :return:
        """
        pass

    def on_rename(self):
        """
        点击’重命名‘时候的回调。
        :return:
        """
        from pmgwidgets import rename_file
        path = self.get_current_file_path()
        basename = os.path.basename(path)
        dir_name = os.path.dirname(path)
        name, stat = QInputDialog.getText(self,
                                          self.tr('Please Input file name'),
                                          '', QLineEdit.Normal, basename)
        if stat:
            new_absolute_path = os.path.join(dir_name, name)
            rename_result = rename_file(path, new_absolute_path)
            if not rename_result:
                QMessageBox.critical(self, self.tr('Error'),
                                     self.tr('Unable to Rename this file.'))
            else:
                self.rename_file_signal[str, str].emit(path, new_absolute_path)

    def on_delete(self):
        """
        点击’删除‘时的回调
        :return:
        """
        from pmgwidgets import move_to_trash
        path = self.get_current_file_path()

        moved_successful = move_to_trash(path)
        if not moved_successful:
            QMessageBox.critical(
                self, self.tr('Error'),
                self.tr('Unable to Move this file to recycle bin.'))
        else:
            self.delete_file_signal[str].emit(path)

    def on_copy(self):
        """
        copy file or dir , save path in pasteAction data.
        :return:
        """
        path = self.get_current_file_path()
        self.pasteAction.setEnabled(True)
        self.pasteAction.setData(path)

        data = QMimeData()
        data.setUrls([QUrl.fromLocalFile(path)])  # 复制到系统剪贴板

        clip = QApplication.clipboard()
        clip.setMimeData(data)

    def on_paste(self):
        """
        Paste file or dir in pasteAction data
        :return:
        """
        from pmgwidgets import copy_paste
        path = self.get_current_file_path()
        target_dir_name = path if os.path.isdir(path) else os.path.dirname(
            path)
        url: QUrl = None

        mimedata = QApplication.clipboard().mimeData(mode=QClipboard.Clipboard)
        print(mimedata)
        urls: List[QUrl] = mimedata.urls()
        for url in urls:
            source_path = url.toLocalFile()  # self.pasteAction.data()
            # File
            if os.path.isfile(source_path):
                source_file_name = os.path.basename(source_path)
                # if exist ,rename to copy_xxx
                if os.path.isfile(
                        os.path.join(target_dir_name, source_file_name)):
                    target_file_name = "copy_{0}".format(source_file_name)
                else:
                    target_file_name = source_file_name
                target_path = os.path.join(target_dir_name, target_file_name)
            # Directory
            else:
                last_dir_name = os.path.split(source_path)[-1]
                # if exist , rename dir copy_xxxx
                if os.path.isdir(os.path.join(target_dir_name, last_dir_name)):
                    target_name = "copy_{0}".format(last_dir_name)
                else:
                    target_name = last_dir_name
                target_path = os.path.join(target_dir_name, target_name)

            copy_succ = copy_paste(source_path, target_path)
            if not copy_succ:
                QMessageBox.critical(self, self.tr('Error'),
                                     self.tr('Copy File or Directory Error.'))
            else:
                self.set_item_focus(target_path)

    def show_ext_filter_selection_dialog(self):

        self.dlg = QDialog(self)
        self.dlg.setWindowTitle(self.tr('Extension Name To Show'))
        self.dlg.setLayout(QVBoxLayout())
        self.dlg.layout().addWidget(QLabel('过滤文件名'))
        check_box = QCheckBox()
        self.dlg.check_box = check_box
        check_box.setChecked(self.filter_exts)
        self.dlg.layout().addWidget(check_box)
        check_box.stateChanged.connect(
            lambda stat: self.signal_ext_filter_adapt.emit(stat))
        check_widget = PMCheckTree(data=self.exts_to_filter)
        self.dlg.check_widget = check_widget
        self.dlg.layout().addWidget(check_widget)
        buttonBox = QDialogButtonBox(QDialogButtonBox.Ok
                                     | QDialogButtonBox.Cancel)

        buttonBox.accepted.connect(self.on_ext_filter_changed)
        buttonBox.rejected.connect(self.dlg.deleteLater)
        # 清除选择功能不完善目前禁用
        # button_clear = buttonBox.addButton(self.tr('Clear Filter'), QDialogButtonBox.ApplyRole)
        # button_clear.clicked.connect(self.clear_ext_filter)
        self.dlg.layout().addWidget(buttonBox)
        self.dlg.exec_()

    def on_ext_filter_changed(self):
        """
        当扩展名过滤改变的时候。
        :return:
        """
        self.exts_to_filter = self.dlg.check_widget.get_data()
        self.filter_exts = self.dlg.check_box.isChecked()
        self.update_ext_filter()
        self.dlg.deleteLater()
        self.signal_ext_filter_changed.emit(self.exts_to_filter)

    def clear_ext_filter(self):
        self.set_ext_filter(None)
        self.dlg.deleteLater()

    def update_ext_filter(self):
        """
        刷新扩展名过滤。
        :return:
        """
        ext_list = []
        for key in self.exts_to_filter.keys():
            for name in self.exts_to_filter[key].keys():
                if self.exts_to_filter[key][name]:
                    ext_list.append('*' + name)
        self.set_ext_filter(ext_list)

    def set_ext_filter(self, ext_names: List[str]):
        """
        文件名过滤
        例如要过滤出.py和.pyx文件,就是ext_names=['*.py','*.pyx']
        discard功能不太完善,目前先禁用。
        :param ext_names:
        :return:
        """
        if ext_names is not None and self.filter_exts:
            self.model.setNameFilterDisables(False)
            self.model.setNameFilters(ext_names)
        else:
            self.model.setNameFilterDisables(True)
            self.model.setNameFilters(["*"])

    def slot_goto_parent_path(self):
        """

        Returns:

        """
        root = self.get_root_path()
        parent = os.path.dirname(root)
        if os.path.exists(parent):
            pass
        self.open_folder_signal.emit(parent)

    def copy_path(self):
        """
        复制当前文件的路径的回调
        Returns:

        """
        path = self.get_current_file_path()
        # data = QMimeData()
        clipboard = QApplication.clipboard()
        clipboard.setText(path)
Exemplo n.º 4
0
class DAQ_Viewer_base(QObject):
    """
        ===================== ===================================
        **Attributes**          **Type**
        *hardware_averaging*    boolean
        *data_grabed_signal*    instance of Signal
        *params*                list
        *settings*              instance of pyqtgraph Parameter
        *parent*                ???
        *status*                dictionnary
        ===================== ===================================

        See Also
        --------
        send_param_status
    """
    hardware_averaging = False
    live_mode_available = False
    data_grabed_signal = Signal(list)
    data_grabed_signal_temp = Signal(list)

    params = []

    def __init__(self, parent=None, params_state=None):
        QObject.__init__(self)
        self.parent_parameters_path = [
        ]  # this is to be added in the send_param_status to take into account when the current class instance parameter list is a child of some other class
        self.settings = Parameter.create(name='Settings',
                                         type='group',
                                         children=self.params)
        if params_state is not None:
            if isinstance(params_state, dict):
                self.settings.restoreState(params_state)
            elif isinstance(params_state, Parameter):
                self.settings.restoreState(params_state.saveState())

        if '0D' in str(self.__class__):
            self.plugin_type = '0D'
        elif '1D' in str(self.__class__):
            self.plugin_type = '1D'
        else:
            self.plugin_type = '2D'

        self.settings.sigTreeStateChanged.connect(self.send_param_status)

        self.parent = parent
        self.status = edict(info="", controller=None, initialized=False)
        self.scan_parameters = None

        self.x_axis = None
        self.y_axis = None

    def ini_detector(self, controller=None):
        """
        Mandatory
        To be reimplemented in subclass
        """
        raise NotImplemented

    def close(self):
        """
        Mandatory
        To be reimplemented in subclass
        """
        raise NotImplemented

    def grab_data(self, Naverage=1, **kwargs):
        """
        Mandatory
        To be reimplemented in subclass
        """
        raise NotImplemented

    def stop(self):
        """
        Mandatory
        To be reimplemented in subclass
        """
        raise NotImplemented

    def commit_settings(self, param):
        """
        To be reimplemented in subclass
        """
        pass

    def update_com(self):
        """
        If some communications settings have to be re init
        To be reimplemented in subclass
        -------

        """
        pass

    def get_axis(self):
        if self.plugin_type == '1D' or self.plugin_type == '2D':
            self.emit_x_axis()

        if self.plugin_type == '2D':
            self.emit_y_axis()

    def emit_status(self, status):
        """
            Emit the status signal from the given status.

            =============== ============ =====================================
            **Parameters**    **Type**     **Description**
            *status*                       the status information to transmit
            =============== ============ =====================================
        """
        if self.parent is not None:
            self.parent.status_sig.emit(status)
            QtWidgets.QApplication.processEvents()
        else:
            print(*status)

    @Slot(ScanParameters)
    def update_scanner(self, scan_parameters):
        self.scan_parameters = scan_parameters

    @Slot(edict)
    def update_settings(self, settings_parameter_dict):
        """
            Update the settings tree from settings_parameter_dict.
            Finally do a commit to activate changes.

            ========================== ============= =====================================================
            **Parameters**              **Type**      **Description**
            *settings_parameter_dict*   dictionnnary  a dictionnary listing path and associated parameter
            ========================== ============= =====================================================

            See Also
            --------
            send_param_status, commit_settings
        """
        # settings_parameter_dict=edict(path=path,param=param)
        try:
            path = settings_parameter_dict['path']
            param = settings_parameter_dict['param']
            change = settings_parameter_dict['change']
            try:
                self.settings.sigTreeStateChanged.disconnect(
                    self.send_param_status)
            except Exception:
                pass
            if change == 'value':
                self.settings.child(*path[1:]).setValue(
                    param.value())  # blocks signal back to main UI
            elif change == 'childAdded':
                child = Parameter.create(name='tmp')
                child.restoreState(param)
                self.settings.child(*path[1:]).addChild(
                    child)  # blocks signal back to main UI
                param = child

            elif change == 'parent':
                children = get_param_from_name(self.settings, param.name())

                if children is not None:
                    path = get_param_path(children)
                    self.settings.child(*path[1:-1]).removeChild(children)

            self.settings.sigTreeStateChanged.connect(self.send_param_status)

            self.commit_settings(param)
        except Exception as e:
            self.emit_status(ThreadCommand("Update_Status", [str(e), 'log']))

    def send_param_status(self, param, changes):
        """
            Check for changes in the given (parameter,change,information) tuple list.
            In case of value changed, send the 'update_settings' ThreadCommand with concerned path,data and change as attributes.

            =============== ============================================ ============================
            **Parameters**    **Type**                                    **Description**
            *param*           instance of pyqtgraph parameter             The parameter to check
            *changes*         (parameter,change,information) tuple list   The changes list to course
            =============== ============================================ ============================

            See Also
            --------
            daq_utils.ThreadCommand
        """
        for param, change, data in changes:
            path = self.settings.childPath(param)
            if change == 'childAdded':
                # first create a "copy" of the actual parameter and send this "copy", to be restored in the main UI
                self.emit_status(
                    ThreadCommand('update_settings', [
                        self.parent_parameters_path + path,
                        [data[0].saveState(), data[1]], change
                    ])
                )  # send parameters values/limits back to the GUI. Send kind of a copy back the GUI otherwise the child reference will be the same in both th eUI and the plugin so one of them will be removed

            elif change == 'value' or change == 'limits' or change == 'options':
                self.emit_status(
                    ThreadCommand(
                        'update_settings',
                        [self.parent_parameters_path + path, data, change
                         ]))  # send parameters values/limits back to the GUI
            elif change == 'parent':
                pass

            pass

    def emit_x_axis(self, x_axis=None):
        """
            Convenience function
            Emit the thread command "x_axis" with x_axis as an attribute.

            See Also
            --------
            daq_utils.ThreadCommand
        """
        if x_axis is None:
            x_axis = self.x_axis
        self.emit_status(ThreadCommand("x_axis", [x_axis]))

    def emit_y_axis(self):
        """
            Emit the thread command "y_axis" with y_axis as an attribute.

            See Also
            --------
            daq_utils.ThreadCommand
        """
        self.emit_status(ThreadCommand("y_axis", [self.y_axis]))
Exemplo n.º 5
0
class HistoryWidget(PluginMainWidget):
    """
    History plugin main widget.
    """

    DEFAULT_OPTIONS = {
        'color_scheme_name': 'spyder/dark',
        'go_to_eof': True,
        'line_numbers': True,
        'wrap': True,
    }

    # Signals
    sig_focus_changed = Signal()
    """
    This signal is emitted when the focus of the code editor storing history
    changes.
    """
    def __init__(self, name, plugin, parent, options=DEFAULT_OPTIONS):
        super().__init__(name, plugin, parent, options)

        # Attributes
        self.editors = []
        self.filenames = []
        self.tabwidget = None
        self.dockviewer = None
        self.wrap_action = None
        self.linenumbers_action = None
        self.editors = []
        self.filenames = []
        self.font = None

        # Widgets
        self.tabwidget = Tabs(self)
        self.find_widget = FindReplace(self)

        # Setup
        self.find_widget.hide()

        # Layout
        layout = QVBoxLayout()

        # TODO: Move this to the tab container directly
        if sys.platform == 'darwin':
            tab_container = QWidget(self)
            tab_container.setObjectName('tab-container')
            tab_layout = QVBoxLayout(tab_container)
            tab_layout.setContentsMargins(0, 0, 0, 0)
            tab_layout.addWidget(self.tabwidget)
            layout.addWidget(tab_container)
        else:
            layout.addWidget(self.tabwidget)

        layout.addWidget(self.find_widget)
        self.setLayout(layout)

        # Signals
        self.tabwidget.currentChanged.connect(self.refresh)
        self.tabwidget.move_data.connect(self.move_tab)

    # --- PluginMainWidget API
    # ------------------------------------------------------------------------
    def get_title(self):
        return _('History')

    def get_focus_widget(self):
        return self.tabwidget.currentWidget()

    def setup(self, options):
        # Actions
        self.wrap_action = self.create_action(
            HistoryWidgetActions.ToggleWrap,
            text=_("Wrap lines"),
            toggled=lambda value: self.set_option('wrap', value),
            initial=self.get_option('wrap'),
        )
        self.linenumbers_action = self.create_action(
            HistoryWidgetActions.ToggleLineNumbers,
            text=_("Show line numbers"),
            toggled=lambda value: self.set_option('line_numbers', value),
            initial=self.get_option('line_numbers'),
        )

        # Menu
        menu = self.get_options_menu()
        for item in [self.wrap_action, self.linenumbers_action]:
            self.add_item_to_menu(
                item,
                menu=menu,
                section=HistoryWidgetOptionsMenuSections.Main,
            )

    def update_actions(self):
        pass

    def on_option_update(self, option, value):
        if self.tabwidget is not None:
            if option == 'wrap':
                for editor in self.editors:
                    editor.toggle_wrap_mode(value)
            elif option == 'line_numbers':
                for editor in self.editors:
                    editor.toggle_line_numbers(value)
            elif option == 'color_scheme_name':
                for editor in self.editors:
                    editor.set_font(self.font)

    # --- Public API
    # ------------------------------------------------------------------------
    def update_font(self, font, color_scheme):
        """
        Update font of the code editor.

        Parameters
        ----------
        font: QFont
            Font object.
        color_scheme: str
            Name of the color scheme to use.
        """
        self.color_scheme = color_scheme
        self.font = font

        for editor in self.editors:
            editor.set_font(font)
            editor.set_color_scheme(color_scheme)

    def move_tab(self, index_from, index_to):
        """
        Move tab.

        Parameters
        ----------
        index_from: int
            Move tab from this index.
        index_to: int
            Move tab to this index.

        Notes
        -----
        Tabs themselves have already been moved by the history.tabwidget.
        """
        filename = self.filenames.pop(index_from)
        editor = self.editors.pop(index_from)

        self.filenames.insert(index_to, filename)
        self.editors.insert(index_to, editor)

    def get_filename_text(self, filename):
        """
        Read and return content from filename.

        Parameters
        ----------
        filename: str
            The file path to read.

        Returns
        -------
        str
            Content of the filename.
        """
        # Avoid a possible error when reading the history file
        try:
            text, _ = encoding.read(filename)
        except (IOError, OSError):
            text = "# Previous history could not be read from disk, sorry\n\n"

        text = normalize_eols(text)
        linebreaks = [m.start() for m in re.finditer('\n', text)]

        if len(linebreaks) > MAX_LINES:
            text = text[linebreaks[-MAX_LINES - 1] + 1:]
            # Avoid an error when trying to write the trimmed text to disk.
            # See spyder-ide/spyder#9093.
            try:
                encoding.write(text, filename)
            except (IOError, OSError):
                pass

        return text

    def add_history(self, filename):
        """
        Create a history tab for `filename`.

        Parameters
        ----------
        filename: str
            History filename.
        """

        filename = encoding.to_unicode_from_fs(filename)
        if filename in self.filenames:
            return

        # Widgets
        editor = SimpleCodeEditor(self)

        # Setup
        language = 'py' if osp.splitext(filename)[1] == '.py' else 'bat'
        editor.setup_editor(
            linenumbers=self.get_option('line_numbers'),
            language=language,
            color_scheme=self.get_option('color_scheme_name'),
            font=self.font,
            wrap=self.get_option('wrap'),
        )
        editor.setReadOnly(True)
        editor.set_text(self.get_filename_text(filename))
        editor.set_cursor_position('eof')
        self.find_widget.set_editor(editor)

        index = self.tabwidget.addTab(editor, osp.basename(filename))
        self.filenames.append(filename)
        self.editors.append(editor)
        self.tabwidget.setCurrentIndex(index)
        self.tabwidget.setTabToolTip(index, filename)

        # Signals
        editor.sig_focus_changed.connect(lambda: self.sig_focus_changed.emit())

    @Slot(str, str)
    def append_to_history(self, filename, command):
        """
        Append command to history tab.

        Parameters
        ----------
        filename: str
            History file.
        command: str
            Command to append to history file.
        """
        if not is_text_string(filename):  # filename is a QString
            filename = to_text_string(filename.toUtf8(), 'utf-8')

        index = self.filenames.index(filename)
        command = to_text_string(command)
        self.editors[index].append(command)

        if self.get_option('go_to_eof'):
            self.editors[index].set_cursor_position('eof')

        self.tabwidget.setCurrentIndex(index)

    def refresh(self):
        """Refresh widget and update find widget on current editor."""
        if self.tabwidget.count():
            editor = self.tabwidget.currentWidget()
        else:
            editor = None

        self.find_widget.set_editor(editor)
Exemplo n.º 6
0
class PythonShellWidget(TracebackLinksMixin, ShellBaseWidget, GetHelpMixin):
    """Python shell widget"""
    QT_CLASS = ShellBaseWidget
    INITHISTORY = [
        '# -*- coding: utf-8 -*-',
        '# *** Spyder Python Console History Log ***',
    ]
    SEPARATOR = '%s##---(%s)---' % (os.linesep * 2, time.ctime())
    go_to_error = Signal(str)

    def __init__(self,
                 parent,
                 history_filename,
                 profile=False,
                 initial_message=None):
        ShellBaseWidget.__init__(self,
                                 parent,
                                 history_filename,
                                 profile=profile,
                                 initial_message=initial_message)
        TracebackLinksMixin.__init__(self)
        GetHelpMixin.__init__(self)

        # Local shortcuts
        self.shortcuts = self.create_shortcuts()

    def create_shortcuts(self):
        array_inline = config_shortcut(lambda: self.enter_array_inline(),
                                       context='array_builder',
                                       name='enter array inline',
                                       parent=self)
        array_table = config_shortcut(lambda: self.enter_array_table(),
                                      context='array_builder',
                                      name='enter array table',
                                      parent=self)
        inspectsc = config_shortcut(self.inspect_current_object,
                                    context='Console',
                                    name='Inspect current object',
                                    parent=self)
        return [inspectsc, array_inline, array_table]

    def get_shortcut_data(self):
        """
        Returns shortcut data, a list of tuples (shortcut, text, default)
        shortcut (QShortcut or QAction instance)
        text (string): action/shortcut description
        default (string): default key sequence
        """
        return [sc.data for sc in self.shortcuts]

    #------ Context menu
    def setup_context_menu(self):
        """Reimplements ShellBaseWidget method"""
        ShellBaseWidget.setup_context_menu(self)
        self.copy_without_prompts_action = create_action(
            self,
            _("Copy without prompts"),
            icon=ima.icon('copywop'),
            triggered=self.copy_without_prompts)
        clear_line_action = create_action(
            self,
            _("Clear line"),
            QKeySequence(get_shortcut('console', 'Clear line')),
            icon=ima.icon('editdelete'),
            tip=_("Clear line"),
            triggered=self.clear_line)
        clear_action = create_action(self,
                                     _("Clear shell"),
                                     QKeySequence(
                                         get_shortcut('console',
                                                      'Clear shell')),
                                     icon=ima.icon('editclear'),
                                     tip=_("Clear shell contents "
                                           "('cls' command)"),
                                     triggered=self.clear_terminal)
        add_actions(self.menu, (self.copy_without_prompts_action,
                                clear_line_action, clear_action))

    def contextMenuEvent(self, event):
        """Reimplements ShellBaseWidget method"""
        state = self.has_selected_text()
        self.copy_without_prompts_action.setEnabled(state)
        ShellBaseWidget.contextMenuEvent(self, event)

    @Slot()
    def copy_without_prompts(self):
        """Copy text to clipboard without prompts"""
        text = self.get_selected_text()
        lines = text.split(os.linesep)
        for index, line in enumerate(lines):
            if line.startswith('>>> ') or line.startswith('... '):
                lines[index] = line[4:]
        text = os.linesep.join(lines)
        QApplication.clipboard().setText(text)

    #------ Key handlers
    def postprocess_keyevent(self, event):
        """Process keypress event"""
        ShellBaseWidget.postprocess_keyevent(self, event)
        if QToolTip.isVisible():
            _event, _text, key, _ctrl, _shift = restore_keyevent(event)
            self.hide_tooltip_if_necessary(key)

    def _key_other(self, text):
        """1 character key"""
        if self.is_completion_widget_visible():
            self.completion_text += text

    def _key_backspace(self, cursor_position):
        """Action for Backspace key"""
        if self.has_selected_text():
            self.check_selection()
            self.remove_selected_text()
        elif self.current_prompt_pos == cursor_position:
            # Avoid deleting prompt
            return
        elif self.is_cursor_on_last_line():
            self.stdkey_backspace()
            if self.is_completion_widget_visible():
                # Removing only last character because if there was a selection
                # the completion widget would have been canceled
                self.completion_text = self.completion_text[:-1]

    def _key_tab(self):
        """Action for TAB key"""
        if self.is_cursor_on_last_line():
            empty_line = not self.get_current_line_to_cursor().strip()
            if empty_line:
                self.stdkey_tab()
            else:
                self.show_code_completion(automatic=False)

    def _key_ctrl_space(self):
        """Action for Ctrl+Space"""
        if not self.is_completion_widget_visible():
            self.show_code_completion(automatic=False)

    def _key_pageup(self):
        """Action for PageUp key"""
        pass

    def _key_pagedown(self):
        """Action for PageDown key"""
        pass

    def _key_escape(self):
        """Action for ESCAPE key"""
        if self.is_completion_widget_visible():
            self.hide_completion_widget()

    def _key_question(self, text):
        """Action for '?'"""
        if self.get_current_line_to_cursor():
            last_obj = self.get_last_obj()
            if last_obj and not last_obj.isdigit():
                self.show_object_info(last_obj)
        self.insert_text(text)
        # In case calltip and completion are shown at the same time:
        if self.is_completion_widget_visible():
            self.completion_text += '?'

    def _key_parenleft(self, text):
        """Action for '('"""
        self.hide_completion_widget()
        if self.get_current_line_to_cursor():
            last_obj = self.get_last_obj()
            if last_obj and not last_obj.isdigit():
                self.insert_text(text)
                self.show_object_info(last_obj, call=True)
                return
        self.insert_text(text)

    def _key_period(self, text):
        """Action for '.'"""
        self.insert_text(text)
        if self.codecompletion_auto:
            # Enable auto-completion only if last token isn't a float
            last_obj = self.get_last_obj()
            if last_obj and not last_obj.isdigit():
                self.show_code_completion(automatic=True)

    #------ Paste
    def paste(self):
        """Reimplemented slot to handle multiline paste action"""
        text = to_text_string(QApplication.clipboard().text())
        if len(text.splitlines()) > 1:
            # Multiline paste
            if self.new_input_line:
                self.on_new_line()
            self.remove_selected_text()  # Remove selection, eventually
            end = self.get_current_line_from_cursor()
            lines = self.get_current_line_to_cursor() + text + end
            self.clear_line()
            self.execute_lines(lines)
            self.move_cursor(-len(end))
        else:
            # Standard paste
            ShellBaseWidget.paste(self)

    #------ Code Completion / Calltips
    # Methods implemented in child class:
    # (e.g. InternalShell)
    def get_dir(self, objtxt):
        """Return dir(object)"""
        raise NotImplementedError

    def get_module_completion(self, objtxt):
        """Return module completion list associated to object name"""
        pass

    def get_globals_keys(self):
        """Return shell globals() keys"""
        raise NotImplementedError

    def get_cdlistdir(self):
        """Return shell current directory list dir"""
        raise NotImplementedError

    def iscallable(self, objtxt):
        """Is object callable?"""
        raise NotImplementedError

    def get_arglist(self, objtxt):
        """Get func/method argument list"""
        raise NotImplementedError

    def get__doc__(self, objtxt):
        """Get object __doc__"""
        raise NotImplementedError

    def get_doc(self, objtxt):
        """Get object documentation dictionary"""
        raise NotImplementedError

    def get_source(self, objtxt):
        """Get object source"""
        raise NotImplementedError

    def is_defined(self, objtxt, force_import=False):
        """Return True if object is defined"""
        raise NotImplementedError

    def show_code_completion(self, automatic):
        """Display a completion list based on the current line"""
        # Note: unicode conversion is needed only for ExternalShellBase
        text = to_text_string(self.get_current_line_to_cursor())
        last_obj = self.get_last_obj()

        if not text:
            return

        if text.startswith('import '):
            obj_list = self.get_module_completion(text)
            words = text.split(' ')
            if ',' in words[-1]:
                words = words[-1].split(',')
            self.show_completion_list(obj_list,
                                      completion_text=words[-1],
                                      automatic=automatic)
            return

        elif text.startswith('from '):
            obj_list = self.get_module_completion(text)
            if obj_list is None:
                return
            words = text.split(' ')
            if '(' in words[-1]:
                words = words[:-2] + words[-1].split('(')
            if ',' in words[-1]:
                words = words[:-2] + words[-1].split(',')
            self.show_completion_list(obj_list,
                                      completion_text=words[-1],
                                      automatic=automatic)
            return

        obj_dir = self.get_dir(last_obj)
        if last_obj and obj_dir and text.endswith('.'):
            self.show_completion_list(obj_dir, automatic=automatic)
            return

        # Builtins and globals
        if not text.endswith('.') and last_obj \
           and re.match(r'[a-zA-Z_0-9]*$', last_obj):
            b_k_g = dir(builtins) + self.get_globals_keys() + keyword.kwlist
            for objname in b_k_g:
                if objname.startswith(last_obj) and objname != last_obj:
                    self.show_completion_list(b_k_g,
                                              completion_text=last_obj,
                                              automatic=automatic)
                    return
            else:
                return

        # Looking for an incomplete completion
        if last_obj is None:
            last_obj = text
        dot_pos = last_obj.rfind('.')
        if dot_pos != -1:
            if dot_pos == len(last_obj) - 1:
                completion_text = ""
            else:
                completion_text = last_obj[dot_pos + 1:]
                last_obj = last_obj[:dot_pos]
            completions = self.get_dir(last_obj)
            if completions is not None:
                self.show_completion_list(completions,
                                          completion_text=completion_text,
                                          automatic=automatic)
                return

        # Looking for ' or ": filename completion
        q_pos = max([text.rfind("'"), text.rfind('"')])
        if q_pos != -1:
            completions = self.get_cdlistdir()
            if completions:
                self.show_completion_list(completions,
                                          completion_text=text[q_pos + 1:],
                                          automatic=automatic)
            return

    #------ Drag'n Drop
    def drop_pathlist(self, pathlist):
        """Drop path list"""
        if pathlist:
            files = ["r'%s'" % path for path in pathlist]
            if len(files) == 1:
                text = files[0]
            else:
                text = "[" + ", ".join(files) + "]"
            if self.new_input_line:
                self.on_new_line()
            self.insert_text(text)
            self.setFocus()
Exemplo n.º 7
0
class LogConnection(QThread):

    new_record = Signal(LogRecord)
    connection_finished = Signal(object)
    internal_prefix = b"!!cutelog!!"

    def __init__(self, parent, socketDescriptor, conn_id, log):
        super().__init__(parent)
        self.log = log.getChild(conn_id)
        self.socketDescriptor = socketDescriptor
        self.conn_id = conn_id
        self.tab_closed = False  # used to stop the connection from a "parent" logger
        self.setup_serializers()

    def __repr__(self):
        return "{}(id={})".format(self.__class__.__name__, self.conn_id)

    def setup_serializers(self):
        self.serializers = {'pickle': pickle.loads, 'json': json.loads}
        if MSGPACK_SUPPORT:
            import msgpack
            from functools import partial
            self.serializers['msgpack'] = partial(msgpack.loads, raw=False)
        if CBOR_SUPPORT:
            import cbor
            self.serializers['cbor'] = cbor.loads
        self.deserialize = self.serializers[CONFIG['default_serialization_format']]

    def run(self):
        self.log.debug('Connection id={} is starting'.format(self.conn_id))

        def wait_and_read(n_bytes):
            """
            Convenience function that simplifies reading and checking for stop events, etc.
            Returns a byte string of length n_bytes or None if socket needs to be closed.

            """
            data = b""
            while len(data) < n_bytes:
                if sock.bytesAvailable() == 0:
                    new_data = sock.waitForReadyRead(100)  # wait for 100ms between read attempts
                    if not new_data:
                        if sock.state() != sock.ConnectedState or self.need_to_stop():
                            return None
                        else:
                            continue
                if self.need_to_stop():
                    return None
                new_data = sock.read(n_bytes - len(data))
                if type(new_data) != bytes:
                    new_data = new_data.data()
                data += new_data
            return data

        sock = QTcpSocket(None)
        sock.setSocketDescriptor(self.socketDescriptor)
        sock.waitForConnected()

        while True:
            read_len = wait_and_read(4)
            if not read_len:
                break
            read_len = struct.unpack(">L", read_len)[0]

            data = wait_and_read(read_len)
            if not data:
                break

            if data.startswith(self.internal_prefix):
                self.handle_internal_command(data)
                continue

            try:
                logDict = self.deserialize(data)
                record = LogRecord(logDict)
            except Exception:
                self.log.error('Creating log record failed', exc_info=True)
                continue
            self.new_record.emit(record)

        self.log.debug('Connection id={} is stopping'.format(self.conn_id))
        sock.disconnectFromHost()
        sock.close()
        self.connection_finished.emit(self)
        self.log.debug('Connection id={} has stopped'.format(self.conn_id))

    def need_to_stop(self):
        return any([self.tab_closed, self.isInterruptionRequested()])

    def handle_internal_command(self, data):
        """
        Used for managing listener options from non-Python clients.
        Command data must start with a special prefix (see self.internal_prefix),
        followed by a command in a key=value format.

        Supported commands:
            format - changes the serialization format to one specified in
                     self.serializers[value]. pickle and json are supported out of the box
                     Example: format=json
        """
        try:
            data = data[len(self.internal_prefix):].decode('utf-8')
            cmd, value = data.split("=", 1)
        except Exception:
            self.log.error('Internal request decoding failed', exc_info=True)
            return
        self.log.debug('Handling internal cmd="{}", value="{}"'.format(cmd, value))
        if cmd == 'format':
            if value in self.serializers:
                self.log.debug('Changing serialization format to "{}"'.format(value))
                self.deserialize = self.serializers[value]
            else:
                self.log.error('Serialization format "{}" is not supported'.format(value))
Exemplo n.º 8
0
class ThumbnailScrollBar(QFrame):
    """
    A widget that manages the display of the FigureThumbnails that are
    created when a figure is sent to the IPython console by the kernel and
    that controls what is displayed in the FigureViewer.
    """
    redirect_stdio = Signal(bool)
    _min_scrollbar_width = 100

    def __init__(self, figure_viewer, parent=None, background_color=None):
        super(ThumbnailScrollBar, self).__init__(parent)
        self._thumbnails = []

        self.background_color = background_color
        self.current_thumbnail = None
        self.set_figureviewer(figure_viewer)
        self.setup_gui()

        # Because the range of Qt scrollareas is not updated immediately
        # after a new item is added to it, setting the scrollbar's value
        # to its maximum value after adding a new item will scroll down to
        # the penultimate item instead of the last.
        # So to scroll programmatically to the latest item after it
        # is added to the scrollarea, we need to do it instead in a slot
        # connected to the scrollbar's rangeChanged signal.
        # See spyder-ide/#10914 for more details.
        self._new_thumbnail_added = False
        self.scrollarea.verticalScrollBar().rangeChanged.connect(
            self._scroll_to_newest_item)

    def setup_gui(self):
        """Setup the main layout of the widget."""
        scrollarea = self.setup_scrollarea()
        up_btn, down_btn = self.setup_arrow_buttons()

        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        layout.addWidget(up_btn)
        layout.addWidget(scrollarea)
        layout.addWidget(down_btn)

    def setup_scrollarea(self):
        """Setup the scrollarea that will contain the FigureThumbnails."""
        self.view = QWidget()

        self.scene = QGridLayout(self.view)
        self.scene.setContentsMargins(0, 0, 0, 0)
        self.scene.setSpacing(3)

        self.scrollarea = QScrollArea()
        self.scrollarea.setWidget(self.view)
        self.scrollarea.setWidgetResizable(True)
        self.scrollarea.setFrameStyle(0)
        self.scrollarea.setViewportMargins(2, 2, 2, 2)
        self.scrollarea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.scrollarea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.scrollarea.setMinimumWidth(self._min_scrollbar_width)

        # Set the vertical scrollbar explicitely.
        # This is required to avoid a "RuntimeError: no access to protected
        # functions or signals for objects not created from Python" in Linux.
        self.scrollarea.setVerticalScrollBar(QScrollBar())

        # Install an event filter on the scrollbar.
        self.scrollarea.installEventFilter(self)

        return self.scrollarea

    def setup_arrow_buttons(self):
        """
        Setup the up and down arrow buttons that are placed at the top and
        bottom of the scrollarea.
        """
        # Get the size hint height of the horizontal scrollbar.
        height = self.scrollarea.horizontalScrollBar().sizeHint().height()

        # Setup the up and down arrow button.
        up_btn = up_btn = QPushButton(icon=ima.icon('last_edit_location'))
        up_btn.setFlat(True)
        up_btn.setFixedHeight(height)
        up_btn.clicked.connect(self.go_up)

        down_btn = QPushButton(icon=ima.icon('folding.arrow_down_on'))
        down_btn.setFlat(True)
        down_btn.setFixedHeight(height)
        down_btn.clicked.connect(self.go_down)

        return up_btn, down_btn

    def set_figureviewer(self, figure_viewer):
        """Set the bamespace for the FigureViewer."""
        self.figure_viewer = figure_viewer

    def eventFilter(self, widget, event):
        """
        An event filter to trigger an update of the thumbnails size so that
        their width fit that of the scrollarea.
        """
        if event.type() == QEvent.Resize:
            self._update_thumbnail_size()
        return super(ThumbnailScrollBar, self).eventFilter(widget, event)

    # ---- Save Figure
    def save_all_figures_as(self):
        """Save all the figures to a file."""
        self.redirect_stdio.emit(False)
        dirname = getexistingdirectory(self,
                                       caption='Save all figures',
                                       basedir=getcwd_or_home())
        self.redirect_stdio.emit(True)
        if dirname:
            return self.save_all_figures_todir(dirname)

    def save_all_figures_todir(self, dirname):
        """Save all figure in dirname."""
        fignames = []
        for thumbnail in self._thumbnails:
            fig = thumbnail.canvas.fig
            fmt = thumbnail.canvas.fmt
            fext = {
                'image/png': '.png',
                'image/jpeg': '.jpg',
                'image/svg+xml': '.svg'
            }[fmt]

            figname = get_unique_figname(dirname, 'Figure', fext)
            save_figure_tofile(fig, fmt, figname)
            fignames.append(figname)
        return fignames

    def save_current_figure_as(self):
        """Save the currently selected figure."""
        if self.current_thumbnail is not None:
            self.save_figure_as(self.current_thumbnail.canvas.fig,
                                self.current_thumbnail.canvas.fmt)

    def save_figure_as(self, fig, fmt):
        """Save the figure to a file."""
        fext, ffilt = {
            'image/png': ('.png', 'PNG (*.png)'),
            'image/jpeg': ('.jpg', 'JPEG (*.jpg;*.jpeg;*.jpe;*.jfif)'),
            'image/svg+xml': ('.svg', 'SVG (*.svg);;PNG (*.png)')
        }[fmt]

        figname = get_unique_figname(getcwd_or_home(), 'Figure', fext)

        self.redirect_stdio.emit(False)
        fname, fext = getsavefilename(parent=self.parent(),
                                      caption='Save Figure',
                                      basedir=figname,
                                      filters=ffilt,
                                      selectedfilter='',
                                      options=None)
        self.redirect_stdio.emit(True)

        if fname:
            save_figure_tofile(fig, fmt, fname)

    # ---- Thumbails Handlers
    def _calculate_figure_canvas_width(self, thumbnail):
        """
        Calculate the witdh the thumbnail's figure canvas need to have for the
        thumbnail to fit the scrollarea.
        """
        extra_padding = 10 if sys.platform == 'darwin' else 0
        figure_canvas_width = (self.scrollarea.width() - 2 * self.lineWidth() -
                               self.scrollarea.viewportMargins().left() -
                               self.scrollarea.viewportMargins().right() -
                               thumbnail.savefig_btn.width() -
                               thumbnail.layout().spacing() - extra_padding)
        if is_dark_interface():
            # This is required to take into account some hard-coded padding
            # and margin in qdarkstyle.
            figure_canvas_width = figure_canvas_width - 6
        return figure_canvas_width

    def _setup_thumbnail_size(self, thumbnail):
        """
        Scale the thumbnail's canvas size so that it fits the thumbnail
        scrollbar's width.
        """
        max_canvas_size = self._calculate_figure_canvas_width(thumbnail)
        thumbnail.scale_canvas_size(max_canvas_size)

    def _update_thumbnail_size(self):
        """
        Update the thumbnails size so that their width fit that of
        the scrollarea.
        """
        # NOTE: We hide temporarily the thumbnails to prevent a repaint of
        # each thumbnail as soon as their size is updated in the loop, which
        # causes some flickering of the thumbnail scrollbar resizing animation.
        # Once the size of all the thumbnails has been updated, we show them
        # back so that they are repainted all at once instead of one after the
        # other. This is just a trick to make the resizing animation of the
        # thumbnail scrollbar look smoother.
        self.view.hide()
        for thumbnail in self._thumbnails:
            self._setup_thumbnail_size(thumbnail)
        self.view.show()

    def add_thumbnail(self, fig, fmt):
        """
        Add a new thumbnail to that thumbnail scrollbar.
        """
        thumbnail = FigureThumbnail(parent=self,
                                    background_color=self.background_color)
        thumbnail.canvas.load_figure(fig, fmt)
        thumbnail.sig_canvas_clicked.connect(self.set_current_thumbnail)
        thumbnail.sig_remove_figure.connect(self.remove_thumbnail)
        thumbnail.sig_save_figure.connect(self.save_figure_as)
        self._thumbnails.append(thumbnail)
        self._new_thumbnail_added = True

        self.scene.setRowStretch(self.scene.rowCount() - 1, 0)
        self.scene.addWidget(thumbnail, self.scene.rowCount() - 1, 0)
        self.scene.setRowStretch(self.scene.rowCount(), 100)
        self.set_current_thumbnail(thumbnail)

        thumbnail.show()
        self._setup_thumbnail_size(thumbnail)

    def remove_current_thumbnail(self):
        """Remove the currently selected thumbnail."""
        if self.current_thumbnail is not None:
            self.remove_thumbnail(self.current_thumbnail)

    def remove_all_thumbnails(self):
        """Remove all thumbnails."""
        for thumbnail in self._thumbnails:
            self.layout().removeWidget(thumbnail)
            thumbnail.sig_canvas_clicked.disconnect()
            thumbnail.sig_remove_figure.disconnect()
            thumbnail.sig_save_figure.disconnect()
            thumbnail.setParent(None)
        self._thumbnails = []
        self.current_thumbnail = None
        self.figure_viewer.figcanvas.clear_canvas()

    def remove_thumbnail(self, thumbnail):
        """Remove thumbnail."""
        if thumbnail in self._thumbnails:
            index = self._thumbnails.index(thumbnail)
            self._thumbnails.remove(thumbnail)
        self.layout().removeWidget(thumbnail)
        thumbnail.setParent(None)
        thumbnail.sig_canvas_clicked.disconnect()
        thumbnail.sig_remove_figure.disconnect()
        thumbnail.sig_save_figure.disconnect()

        # Select a new thumbnail if any :
        if thumbnail == self.current_thumbnail:
            if len(self._thumbnails) > 0:
                self.set_current_index(min(index, len(self._thumbnails) - 1))
            else:
                self.current_thumbnail = None
                self.figure_viewer.figcanvas.clear_canvas()

    def set_current_index(self, index):
        """Set the currently selected thumbnail by its index."""
        self.set_current_thumbnail(self._thumbnails[index])

    def get_current_index(self):
        """Return the index of the currently selected thumbnail."""
        try:
            return self._thumbnails.index(self.current_thumbnail)
        except ValueError:
            return -1

    def set_current_thumbnail(self, thumbnail):
        """Set the currently selected thumbnail."""
        self.current_thumbnail = thumbnail
        self.figure_viewer.load_figure(thumbnail.canvas.fig,
                                       thumbnail.canvas.fmt)
        for thumbnail in self._thumbnails:
            thumbnail.highlight_canvas(thumbnail == self.current_thumbnail)

    def go_previous_thumbnail(self):
        """Select the thumbnail previous to the currently selected one."""
        if self.current_thumbnail is not None:
            index = self._thumbnails.index(self.current_thumbnail) - 1
            index = index if index >= 0 else len(self._thumbnails) - 1
            self.set_current_index(index)
            self.scroll_to_item(index)

    def go_next_thumbnail(self):
        """Select thumbnail next to the currently selected one."""
        if self.current_thumbnail is not None:
            index = self._thumbnails.index(self.current_thumbnail) + 1
            index = 0 if index >= len(self._thumbnails) else index
            self.set_current_index(index)
            self.scroll_to_item(index)

    def scroll_to_item(self, index):
        """Scroll to the selected item of ThumbnailScrollBar."""
        spacing_between_items = self.scene.verticalSpacing()
        height_view = self.scrollarea.viewport().height()
        height_item = self.scene.itemAt(index).sizeHint().height()
        height_view_excluding_item = max(0, height_view - height_item)

        height_of_top_items = spacing_between_items
        for i in range(index):
            item = self.scene.itemAt(i)
            height_of_top_items += item.sizeHint().height()
            height_of_top_items += spacing_between_items

        pos_scroll = height_of_top_items - height_view_excluding_item // 2

        vsb = self.scrollarea.verticalScrollBar()
        vsb.setValue(pos_scroll)

    def _scroll_to_newest_item(self, vsb_min, vsb_max):
        """
        Scroll to the newest item added to the thumbnail scrollbar.

        Note that this method is called each time the rangeChanged signal
        is emitted by the scrollbar.
        """
        if self._new_thumbnail_added:
            self._new_thumbnail_added = False
            self.scrollarea.verticalScrollBar().setValue(vsb_max)

    # ---- ScrollBar Handlers
    def go_up(self):
        """Scroll the scrollbar of the scrollarea up by a single step."""
        vsb = self.scrollarea.verticalScrollBar()
        vsb.setValue(int(vsb.value() - vsb.singleStep()))

    def go_down(self):
        """Scroll the scrollbar of the scrollarea down by a single step."""
        vsb = self.scrollarea.verticalScrollBar()
        vsb.setValue(int(vsb.value() + vsb.singleStep()))
Exemplo n.º 9
0
class FigureBrowser(QWidget):
    """
    Widget to browse the figures that were sent by the kernel to the IPython
    console to be plotted inline.
    """
    sig_option_changed = Signal(str, object)
    sig_collapse = Signal()

    def __init__(self,
                 parent=None,
                 options_button=None,
                 plugin_actions=[],
                 background_color=None):
        super(FigureBrowser, self).__init__(parent)

        self.shellwidget = None
        self.is_visible = True
        self.figviewer = None
        self.setup_in_progress = False
        self.background_color = background_color

        # Options :
        self.mute_inline_plotting = None
        self.show_plot_outline = None
        self.auto_fit_plotting = None

        # Option actions :
        self.mute_inline_action = None
        self.show_plot_outline_action = None
        self.auto_fit_action = None

        self.options_button = options_button
        self.plugin_actions = plugin_actions
        self.shortcuts = self.create_shortcuts()

    def setup(self,
              mute_inline_plotting=None,
              show_plot_outline=None,
              auto_fit_plotting=None):
        """Setup the figure browser with provided settings."""
        assert self.shellwidget is not None

        self.mute_inline_plotting = mute_inline_plotting
        self.show_plot_outline = show_plot_outline
        self.auto_fit_plotting = auto_fit_plotting

        if self.figviewer is not None:
            self.mute_inline_action.setChecked(mute_inline_plotting)
            self.show_plot_outline_action.setChecked(show_plot_outline)
            self.auto_fit_action.setChecked(auto_fit_plotting)
            return

        self.figviewer = FigureViewer(background_color=self.background_color)
        self.figviewer.setStyleSheet("FigureViewer{"
                                     "border: 1px solid lightgrey;"
                                     "border-top-width: 0px;"
                                     "border-bottom-width: 0px;"
                                     "border-left-width: 0px;"
                                     "}")
        self.thumbnails_sb = ThumbnailScrollBar(
            self.figviewer, background_color=self.background_color)

        toolbar = self.setup_toolbar()
        self.setup_option_actions(mute_inline_plotting, show_plot_outline,
                                  auto_fit_plotting)

        # Create the layout.
        main_widget = QSplitter()
        main_widget.addWidget(self.figviewer)
        main_widget.addWidget(self.thumbnails_sb)
        main_widget.setFrameStyle(QScrollArea().frameStyle())

        self.tools_layout = QHBoxLayout()
        for widget in toolbar:
            self.tools_layout.addWidget(widget)
        self.tools_layout.addStretch()
        self.setup_options_button()

        layout = create_plugin_layout(self.tools_layout, main_widget)
        self.setLayout(layout)

    def setup_toolbar(self):
        """Setup the toolbar"""
        savefig_btn = create_toolbutton(self,
                                        icon=ima.icon('filesave'),
                                        tip=_("Save Image As..."),
                                        triggered=self.save_figure)

        saveall_btn = create_toolbutton(self,
                                        icon=ima.icon('save_all'),
                                        tip=_("Save All Images..."),
                                        triggered=self.save_all_figures)

        copyfig_btn = create_toolbutton(
            self,
            icon=ima.icon('editcopy'),
            tip=(_("Copy plot to clipboard as image (%s)") %
                 CONF.get_shortcut('plots', 'copy')),
            triggered=self.copy_figure)

        closefig_btn = create_toolbutton(self,
                                         icon=ima.icon('editclear'),
                                         tip=_("Remove image"),
                                         triggered=self.close_figure)

        closeall_btn = create_toolbutton(
            self,
            icon=ima.icon('filecloseall'),
            tip=_("Remove all images from the explorer"),
            triggered=self.close_all_figures)

        vsep1 = QFrame()
        vsep1.setFrameStyle(53)

        goback_btn = create_toolbutton(self,
                                       icon=ima.icon('ArrowBack'),
                                       tip=_("Previous figure ({})").format(
                                           CONF.get_shortcut(
                                               'plots', 'previous figure')),
                                       triggered=self.go_previous_thumbnail)

        gonext_btn = create_toolbutton(self,
                                       icon=ima.icon('ArrowForward'),
                                       tip=_("Next figure ({})").format(
                                           CONF.get_shortcut(
                                               'plots', 'next figure')),
                                       triggered=self.go_next_thumbnail)

        vsep2 = QFrame()
        vsep2.setFrameStyle(53)

        self.zoom_out_btn = create_toolbutton(
            self,
            icon=ima.icon('zoom_out'),
            tip=_("Zoom out (Ctrl + mouse-wheel-down)"),
            triggered=self.zoom_out)

        self.zoom_in_btn = create_toolbutton(
            self,
            icon=ima.icon('zoom_in'),
            tip=_("Zoom in (Ctrl + mouse-wheel-up)"),
            triggered=self.zoom_in)

        self.zoom_disp = QSpinBox()
        self.zoom_disp.setAlignment(Qt.AlignCenter)
        self.zoom_disp.setButtonSymbols(QSpinBox.NoButtons)
        self.zoom_disp.setReadOnly(True)
        self.zoom_disp.setSuffix(' %')
        self.zoom_disp.setRange(0, 9999)
        self.zoom_disp.setValue(100)
        self.figviewer.sig_zoom_changed.connect(self.zoom_disp.setValue)

        zoom_pan = QWidget()
        layout = QHBoxLayout(zoom_pan)
        layout.setSpacing(0)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.zoom_out_btn)
        layout.addWidget(self.zoom_in_btn)
        layout.addWidget(self.zoom_disp)

        return [
            savefig_btn, saveall_btn, copyfig_btn, closefig_btn, closeall_btn,
            vsep1, goback_btn, gonext_btn, vsep2, zoom_pan
        ]

    def setup_option_actions(self, mute_inline_plotting, show_plot_outline,
                             auto_fit_plotting):
        """Setup the actions to show in the cog menu."""
        self.setup_in_progress = True
        self.mute_inline_action = create_action(
            self,
            _("Mute inline plotting"),
            tip=_("Mute inline plotting in the ipython console."),
            toggled=lambda state: self.option_changed('mute_inline_plotting',
                                                      state))
        self.mute_inline_action.setChecked(mute_inline_plotting)

        self.show_plot_outline_action = create_action(
            self,
            _("Show plot outline"),
            tip=_("Show the plot outline."),
            toggled=self.show_fig_outline_in_viewer)
        self.show_plot_outline_action.setChecked(show_plot_outline)

        self.auto_fit_action = create_action(
            self,
            _("Fit plots to window"),
            tip=_("Automatically fit plots to Plot pane size."),
            toggled=self.change_auto_fit_plotting)
        self.auto_fit_action.setChecked(auto_fit_plotting)

        self.actions = [
            self.mute_inline_action, self.show_plot_outline_action,
            self.auto_fit_action
        ]

        self.setup_in_progress = False

    def setup_options_button(self):
        """Add the cog menu button to the toolbar."""
        if not self.options_button:
            # When the FigureBowser widget is instatiated outside of the
            # plugin (for testing purpose for instance), we need to create
            # the options_button and set its menu.
            self.options_button = create_toolbutton(
                self, text=_('Options'), icon=ima.icon('tooloptions'))

            actions = self.actions + [MENU_SEPARATOR] + self.plugin_actions
            self.options_menu = QMenu(self)
            add_actions(self.options_menu, actions)
            self.options_button.setMenu(self.options_menu)

        if self.tools_layout.itemAt(self.tools_layout.count() - 1) is None:
            self.tools_layout.insertWidget(self.tools_layout.count() - 1,
                                           self.options_button)
        else:
            self.tools_layout.addWidget(self.options_button)

    def create_shortcuts(self):
        """Create shortcuts for this widget."""
        # Configurable
        copyfig = CONF.config_shortcut(self.copy_figure,
                                       context='plots',
                                       name='copy',
                                       parent=self)

        prevfig = CONF.config_shortcut(self.go_previous_thumbnail,
                                       context='plots',
                                       name='previous figure',
                                       parent=self)

        nextfig = CONF.config_shortcut(self.go_next_thumbnail,
                                       context='plots',
                                       name='next figure',
                                       parent=self)

        return [copyfig, prevfig, nextfig]

    def get_shortcut_data(self):
        """
        Return shortcut data, a list of tuples (shortcut, text, default).

        shortcut (QShortcut or QAction instance)
        text (string): action/shortcut description
        default (string): default key sequence
        """
        return [sc.data for sc in self.shortcuts]

    def option_changed(self, option, value):
        """Handle when the value of an option has changed"""
        setattr(self, to_text_string(option), value)
        self.shellwidget.set_namespace_view_settings()
        if self.setup_in_progress is False:
            self.sig_option_changed.emit(option, value)

    def show_fig_outline_in_viewer(self, state):
        """Draw a frame around the figure viewer if state is True."""
        if state is True:
            self.figviewer.figcanvas.setStyleSheet(
                "FigureCanvas{border: 1px solid lightgrey;}")
        else:
            self.figviewer.figcanvas.setStyleSheet("FigureCanvas{}")
        self.option_changed('show_plot_outline', state)

    def change_auto_fit_plotting(self, state):
        """Change the auto_fit_plotting option and scale images."""
        self.option_changed('auto_fit_plotting', state)
        self.figviewer.auto_fit_plotting = state
        self.zoom_out_btn.setEnabled(not state)
        self.zoom_in_btn.setEnabled(not state)

    def set_shellwidget(self, shellwidget):
        """Bind the shellwidget instance to the figure browser"""
        self.shellwidget = shellwidget
        shellwidget.set_figurebrowser(self)
        shellwidget.sig_new_inline_figure.connect(self._handle_new_figure)

    def get_actions(self):
        """Get the actions of the widget."""
        return self.actions

    def _handle_new_figure(self, fig, fmt):
        """
        Handle when a new figure is sent to the IPython console by the
        kernel.
        """
        self.thumbnails_sb.add_thumbnail(fig, fmt)

    # ---- Toolbar Handlers
    def zoom_in(self):
        """Zoom the figure in by a single step in the figure viewer."""
        self.figviewer.zoom_in()

    def zoom_out(self):
        """Zoom the figure out by a single step in the figure viewer."""
        self.figviewer.zoom_out()

    def go_previous_thumbnail(self):
        """
        Select the thumbnail previous to the currently selected one in the
        thumbnail scrollbar.
        """
        self.thumbnails_sb.go_previous_thumbnail()

    def go_next_thumbnail(self):
        """
        Select the thumbnail next to the currently selected one in the
        thumbnail scrollbar.
        """
        self.thumbnails_sb.go_next_thumbnail()

    def save_figure(self):
        """Save the currently selected figure in the thumbnail scrollbar."""
        self.thumbnails_sb.save_current_figure_as()

    def save_all_figures(self):
        """Save all the figures in a selected directory."""
        return self.thumbnails_sb.save_all_figures_as()

    def close_figure(self):
        """Close the currently selected figure in the thumbnail scrollbar."""
        self.thumbnails_sb.remove_current_thumbnail()

    def close_all_figures(self):
        """Close all the figures in the thumbnail scrollbar."""
        self.thumbnails_sb.remove_all_thumbnails()

    def copy_figure(self):
        """Copy figure from figviewer to clipboard."""
        if self.figviewer and self.figviewer.figcanvas.fig:
            self.figviewer.figcanvas.copy_figure()
Exemplo n.º 10
0
class Projects(SpyderPluginWidget):
    """Projects plugin."""

    CONF_SECTION = 'project_explorer'
    pythonpath_changed = Signal()
    sig_project_created = Signal(object, object, object)
    sig_project_loaded = Signal(object)
    sig_project_closed = Signal(object)

    def __init__(self, parent=None):
        """Initialization."""
        SpyderPluginWidget.__init__(self, parent)

        self.explorer = ProjectExplorerWidget(
            self,
            name_filters=self.get_option('name_filters'),
            show_all=self.get_option('show_all'),
            show_hscrollbar=self.get_option('show_hscrollbar'))

        layout = QVBoxLayout()
        layout.addWidget(self.explorer)
        self.setLayout(layout)

        self.recent_projects = self.get_option('recent_projects', default=[])
        self.current_active_project = None
        self.latest_project = None

        self.editor = None
        self.workingdirectory = None

        # Initialize plugin
        self.initialize_plugin()
        self.explorer.setup_project(self.get_active_project_path())

    #------ SpyderPluginWidget API ---------------------------------------------
    def get_plugin_title(self):
        """Return widget title"""
        return _("Project explorer")

    def get_focus_widget(self):
        """
        Return the widget to give focus to when
        this plugin's dockwidget is raised on top-level
        """
        return self.explorer.treewidget

    def get_plugin_actions(self):
        """Return a list of actions related to plugin"""
        self.new_project_action = create_action(
            self, _("New Project..."), triggered=self.create_new_project)
        self.open_project_action = create_action(
            self,
            _("Open Project..."),
            triggered=lambda v: self.open_project())
        self.close_project_action = create_action(self,
                                                  _("Close Project"),
                                                  triggered=self.close_project)
        self.delete_project_action = create_action(
            self, _("Delete Project"), triggered=self.explorer.delete_project)
        self.clear_recent_projects_action =\
            create_action(self, _("Clear this list"),
                          triggered=self.clear_recent_projects)
        self.edit_project_preferences_action =\
            create_action(self, _("Project Preferences"),
                          triggered=self.edit_project_preferences)
        self.recent_project_menu = QMenu(_("Recent Projects"), self)

        self.main.projects_menu_actions += [
            self.new_project_action, MENU_SEPARATOR, self.open_project_action,
            self.close_project_action, self.delete_project_action,
            MENU_SEPARATOR, self.recent_project_menu, self.toggle_view_action
        ]

        self.setup_menu_actions()
        return []

    def register_plugin(self):
        """Register plugin in Spyder's main window"""
        self.editor = self.main.editor
        self.workingdirectory = self.main.workingdirectory
        extconsole = self.main.extconsole
        treewidget = self.explorer.treewidget

        self.main.add_dockwidget(self)
        self.explorer.sig_open_file.connect(self.main.open_file)

        treewidget.sig_edit.connect(self.editor.load)
        treewidget.sig_removed.connect(self.editor.removed)
        treewidget.sig_removed_tree.connect(self.editor.removed_tree)
        treewidget.sig_renamed.connect(self.editor.renamed)
        treewidget.sig_create_module.connect(self.editor.new)
        treewidget.sig_new_file.connect(lambda t: self.main.editor.new(text=t))
        treewidget.sig_open_terminal.connect(extconsole.open_terminal)
        treewidget.sig_open_interpreter.connect(extconsole.open_interpreter)
        treewidget.redirect_stdio.connect(
            self.main.redirect_internalshell_stdio)
        treewidget.sig_run.connect(
            lambda fname: self.main.open_external_console(
                to_text_string(fname), osp.dirname(to_text_string(fname)), '',
                False, False, True, '', False))

        # New project connections. Order matters!
        self.sig_project_loaded.connect(
            lambda v: self.workingdirectory.chdir(v))
        self.sig_project_loaded.connect(
            lambda v: self.main.update_window_title())
        self.sig_project_loaded.connect(
            lambda v: self.editor.setup_open_files())
        self.sig_project_loaded.connect(self.update_explorer)
        self.sig_project_closed[object].connect(
            lambda v: self.workingdirectory.chdir(self.get_last_working_dir()))
        self.sig_project_closed.connect(
            lambda v: self.main.update_window_title())
        self.sig_project_closed.connect(
            lambda v: self.editor.setup_open_files())
        self.recent_project_menu.aboutToShow.connect(self.setup_menu_actions)

        self.main.pythonpath_changed()
        self.main.restore_scrollbar_position.connect(
            self.restore_scrollbar_position)
        self.pythonpath_changed.connect(self.main.pythonpath_changed)
        self.editor.set_projects(self)

    def refresh_plugin(self):
        """Refresh project explorer widget"""
        pass

    def closing_plugin(self, cancelable=False):
        """Perform actions before parent main window is closed"""
        self.save_config()
        self.explorer.closing_widget()
        return True

    #------ Public API ---------------------------------------------------------
    def setup_menu_actions(self):
        """Setup and update the menu actions."""
        self.recent_project_menu.clear()
        self.recent_projects_actions = []
        if self.recent_projects:
            for project in self.recent_projects:
                if self.is_valid_project(project):
                    name = project.replace(get_home_dir(), '~')
                    action = create_action(self,
                                           name,
                                           icon=ima.icon('project'),
                                           triggered=lambda v, path=project:
                                           self.open_project(path=path))
                    self.recent_projects_actions.append(action)
                else:
                    self.recent_projects.remove(project)
            self.recent_projects_actions += [
                None, self.clear_recent_projects_action
            ]
        else:
            self.recent_projects_actions = [self.clear_recent_projects_action]
        add_actions(self.recent_project_menu, self.recent_projects_actions)
        self.update_project_actions()

    def update_project_actions(self):
        """Update actions of the Projects menu"""
        if self.recent_projects:
            self.clear_recent_projects_action.setEnabled(True)
        else:
            self.clear_recent_projects_action.setEnabled(False)

        active = bool(self.get_active_project_path())
        self.close_project_action.setEnabled(active)
        self.delete_project_action.setEnabled(active)
        self.edit_project_preferences_action.setEnabled(active)

    def edit_project_preferences(self):
        """Edit Spyder active project preferences"""
        from spyder.widgets.projects.configdialog import ProjectPreferences
        if self.project_active:
            active_project = self.project_list[0]
            dlg = ProjectPreferences(self, active_project)
            #            dlg.size_change.connect(self.set_project_prefs_size)
            #            if self.projects_prefs_dialog_size is not None:
            #                dlg.resize(self.projects_prefs_dialog_size)
            dlg.show()
            #        dlg.check_all_settings()
            #        dlg.pages_widget.currentChanged.connect(self.__preference_page_changed)
            dlg.exec_()

    @Slot()
    def create_new_project(self):
        """Create new project"""
        active_project = self.current_active_project
        dlg = ProjectDialog(self)
        dlg.sig_project_creation_requested.connect(self._create_project)
        dlg.sig_project_creation_requested.connect(self.sig_project_created)
        if dlg.exec_():
            pass
            if active_project is None:
                self.show_explorer()
            self.pythonpath_changed.emit()
            self.restart_consoles()

    def _create_project(self, path):
        """Create a new project."""
        self.open_project(path=path)
        self.setup_menu_actions()
        self.add_to_recent(path)

    def open_project(self,
                     path=None,
                     restart_consoles=True,
                     save_previous_files=True):
        """Open the project located in `path`"""
        if path is None:
            basedir = get_home_dir()
            path = getexistingdirectory(parent=self,
                                        caption=_("Open project"),
                                        basedir=basedir)
            if not self.is_valid_project(path):
                if path:
                    QMessageBox.critical(
                        self, _('Error'),
                        _("<b>%s</b> is not a Spyder project!") % path)
                return
            else:
                self.add_to_recent(path)

        # A project was not open before
        if self.current_active_project is None:
            if save_previous_files:
                self.editor.save_open_files()
            self.editor.set_option('last_working_dir', getcwd())
            self.show_explorer()
        else:  # we are switching projects
            self.set_project_filenames(self.editor.get_open_filenames())

        self.current_active_project = EmptyProject(path)
        self.latest_project = EmptyProject(path)
        self.set_option('current_project_path', self.get_active_project_path())
        self.setup_menu_actions()
        self.sig_project_loaded.emit(path)
        self.pythonpath_changed.emit()
        if restart_consoles:
            self.restart_consoles()

    def close_project(self):
        """
        Close current project and return to a window without an active
        project
        """
        if self.current_active_project:
            path = self.current_active_project.root_path
            self.set_project_filenames(self.editor.get_open_filenames())
            self.current_active_project = None
            self.set_option('current_project_path', None)
            self.setup_menu_actions()
            self.sig_project_closed.emit(path)
            self.pythonpath_changed.emit()
            self.dockwidget.close()
            self.explorer.clear()
            self.restart_consoles()

    def clear_recent_projects(self):
        """Clear the list of recent projects"""
        self.recent_projects = []
        self.setup_menu_actions()

    def get_active_project(self):
        """Get the active project"""
        return self.current_active_project

    def reopen_last_project(self):
        """
        Reopen the active project when Spyder was closed last time, if any
        """
        current_project_path = self.get_option('current_project_path',
                                               default=None)

        # Needs a safer test of project existence!
        if current_project_path and \
          self.is_valid_project(current_project_path):
            self.open_project(path=current_project_path,
                              restart_consoles=False,
                              save_previous_files=False)
            self.load_config()

    def get_project_filenames(self):
        """Get the list of recent filenames of a project"""
        recent_files = []
        if self.current_active_project:
            recent_files = self.current_active_project.get_recent_files()
        elif self.latest_project:
            recent_files = self.latest_project.get_recent_files()
        return recent_files

    def set_project_filenames(self, recent_files):
        """Set the list of open file names in a project"""
        if self.current_active_project:
            self.current_active_project.set_recent_files(recent_files)

    def get_active_project_path(self):
        """Get path of the active project"""
        active_project_path = None
        if self.current_active_project:
            active_project_path = self.current_active_project.root_path
        return active_project_path

    def get_pythonpath(self, at_start=False):
        """Get project path as a list to be added to PYTHONPATH"""
        if at_start:
            current_path = self.get_option('current_project_path',
                                           default=None)
        else:
            current_path = self.get_active_project_path()
        if current_path is None:
            return []
        else:
            return [current_path]

    def get_last_working_dir(self):
        """Get the path of the last working directory"""
        return self.editor.get_option('last_working_dir', default=getcwd())

    def save_config(self):
        """Save configuration: opened projects & tree widget state"""
        self.set_option('recent_projects', self.recent_projects)
        self.set_option('expanded_state',
                        self.explorer.treewidget.get_expanded_state())
        self.set_option('scrollbar_position',
                        self.explorer.treewidget.get_scrollbar_position())

    def load_config(self):
        """Load configuration: opened projects & tree widget state"""
        expanded_state = self.get_option('expanded_state', None)
        # Sometimes the expanded state option may be truncated in .ini file
        # (for an unknown reason), in this case it would be converted to a
        # string by 'userconfig':
        if is_text_string(expanded_state):
            expanded_state = None
        if expanded_state is not None:
            self.explorer.treewidget.set_expanded_state(expanded_state)

    def restore_scrollbar_position(self):
        """Restoring scrollbar position after main window is visible"""
        scrollbar_pos = self.get_option('scrollbar_position', None)
        if scrollbar_pos is not None:
            self.explorer.treewidget.set_scrollbar_position(scrollbar_pos)

    def update_explorer(self):
        """Update explorer tree"""
        self.explorer.setup_project(self.get_active_project_path())

    def show_explorer(self):
        """Show the explorer"""
        if self.dockwidget.isHidden():
            self.dockwidget.show()
        self.dockwidget.raise_()
        self.dockwidget.update()

    def restart_consoles(self):
        """Restart consoles when closing, opening and switching projects"""
        self.main.extconsole.restart()
        if self.main.ipyconsole:
            self.main.ipyconsole.restart()

    def is_valid_project(self, path):
        """Check if a directory is a valid Spyder project"""
        spy_project_dir = osp.join(path, '.spyproject')
        if osp.isdir(path) and osp.isdir(spy_project_dir):
            return True
        else:
            return False

    def add_to_recent(self, project):
        """
        Add an entry to recent projetcs

        We only maintain the list of the 10 most recent projects
        """
        if project not in self.recent_projects:
            self.recent_projects.insert(0, project)
            self.recent_projects = self.recent_projects[:10]
Exemplo n.º 11
0
class FigureViewer(QScrollArea):
    """
    A scrollarea that displays a single FigureCanvas with zooming and panning
    capability with CTRL + Mouse_wheel and Left-press mouse button event.
    """

    sig_zoom_changed = Signal(int)

    def __init__(self, parent=None, background_color=None):
        super(FigureViewer, self).__init__(parent)
        self.setAlignment(Qt.AlignCenter)
        self.viewport().setObjectName("figviewport")
        self.viewport().setStyleSheet("#figviewport {background-color:" +
                                      str(background_color) + "}")
        self.setFrameStyle(0)

        self.background_color = background_color
        self._scalefactor = 0
        self._scalestep = 1.2
        self._sfmax = 10
        self._sfmin = -10

        self.setup_figcanvas()
        self.auto_fit_plotting = False

        # An internal flag that tracks when the figure is being panned.
        self._ispanning = False

    @property
    def auto_fit_plotting(self):
        """
        Return whether to automatically fit the plot to the scroll area size.
        """
        return self._auto_fit_plotting

    @auto_fit_plotting.setter
    def auto_fit_plotting(self, value):
        """
        Set whether to automatically fit the plot to the scroll area size.
        """
        self._auto_fit_plotting = value
        if value:
            self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
            self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        else:
            self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
            self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.scale_image()

    def setup_figcanvas(self):
        """Setup the FigureCanvas."""
        self.figcanvas = FigureCanvas(background_color=self.background_color)
        self.figcanvas.installEventFilter(self)
        self.setWidget(self.figcanvas)

    def load_figure(self, fig, fmt):
        """Set a new figure in the figure canvas."""
        self.figcanvas.load_figure(fig, fmt)
        self.scale_image()
        self.figcanvas.repaint()

    def eventFilter(self, widget, event):
        """A filter to control the zooming and panning of the figure canvas."""

        # ---- Zooming
        if event.type() == QEvent.Wheel and not self.auto_fit_plotting:
            modifiers = QApplication.keyboardModifiers()
            if modifiers == Qt.ControlModifier:
                if event.angleDelta().y() > 0:
                    self.zoom_in()
                else:
                    self.zoom_out()
                return True
            else:
                return False

        # ---- Scaling
        elif event.type() == QEvent.Paint and self.auto_fit_plotting:
            self.scale_image()

        # ---- Panning
        # Set ClosedHandCursor:
        elif event.type() == QEvent.MouseButtonPress:
            if event.button() == Qt.LeftButton:
                QApplication.setOverrideCursor(Qt.ClosedHandCursor)
                self._ispanning = True
                self.xclick = event.globalX()
                self.yclick = event.globalY()

        # Reset Cursor:
        elif event.type() == QEvent.MouseButtonRelease:
            QApplication.restoreOverrideCursor()
            self._ispanning = False

        # Move  ScrollBar:
        elif event.type() == QEvent.MouseMove:
            if self._ispanning:
                dx = self.xclick - event.globalX()
                self.xclick = event.globalX()

                dy = self.yclick - event.globalY()
                self.yclick = event.globalY()

                scrollBarH = self.horizontalScrollBar()
                scrollBarH.setValue(scrollBarH.value() + dx)

                scrollBarV = self.verticalScrollBar()
                scrollBarV.setValue(scrollBarV.value() + dy)

        return QWidget.eventFilter(self, widget, event)

    # ---- Figure Scaling Handlers
    def zoom_in(self):
        """Scale the image up by one scale step."""
        if self._scalefactor <= self._sfmax:
            self._scalefactor += 1
            self.scale_image()
            self._adjust_scrollbar(self._scalestep)

    def zoom_out(self):
        """Scale the image down by one scale step."""
        if self._scalefactor >= self._sfmin:
            self._scalefactor -= 1
            self.scale_image()
            self._adjust_scrollbar(1 / self._scalestep)

    def scale_image(self):
        """Scale the image size."""
        fwidth = self.figcanvas.fwidth
        fheight = self.figcanvas.fheight

        # Don't auto fit plotting
        if not self.auto_fit_plotting:
            new_width = int(fwidth * self._scalestep**self._scalefactor)
            new_height = int(fheight * self._scalestep**self._scalefactor)

        # Auto fit plotting
        # Scale the image to fit the figviewer size while respecting the ratio.
        else:
            size = self.size()
            style = self.style()
            width = (size.width() -
                     style.pixelMetric(QStyle.PM_LayoutLeftMargin) -
                     style.pixelMetric(QStyle.PM_LayoutRightMargin))
            height = (size.height() -
                      style.pixelMetric(QStyle.PM_LayoutTopMargin) -
                      style.pixelMetric(QStyle.PM_LayoutBottomMargin))
            if (fwidth / fheight) > (width / height):
                new_width = int(width)
                new_height = int(width / fwidth * fheight)
            else:
                new_height = int(height)
                new_width = int(height / fheight * fwidth)

        if self.figcanvas.size() != QSize(new_width, new_height):
            self.figcanvas.setFixedSize(new_width, new_height)
            self.sig_zoom_changed.emit(self.get_scaling())

    def get_scaling(self):
        """Get the current scaling of the figure in percent."""
        return round(self.figcanvas.width() / self.figcanvas.fwidth * 100)

    def reset_original_image(self):
        """Reset the image to its original size."""
        self._scalefactor = 0
        self.scale_image()

    def _adjust_scrollbar(self, f):
        """
        Adjust the scrollbar position to take into account the zooming of
        the figure.
        """
        # Adjust horizontal scrollbar :
        hb = self.horizontalScrollBar()
        hb.setValue(int(f * hb.value() + ((f - 1) * hb.pageStep() / 2)))

        # Adjust the vertical scrollbar :
        vb = self.verticalScrollBar()
        vb.setValue(int(f * vb.value() + ((f - 1) * vb.pageStep() / 2)))
Exemplo n.º 12
0
class ShellWidget(NamepaceBrowserWidget, HelpWidget, DebuggingWidget,
                  FigureBrowserWidget):
    """
    Shell widget for the IPython Console

    This is the widget in charge of executing code
    """
    # NOTE: Signals can't be assigned separately to each widget
    #       That's why we define all needed signals here.

    # For NamepaceBrowserWidget
    sig_show_syspath = Signal(object)
    sig_show_env = Signal(object)

    # For FigureBrowserWidget
    sig_new_inline_figure = Signal(object, str)

    # For DebuggingWidget
    sig_pdb_step = Signal(str, int)

    # For ShellWidget
    focus_changed = Signal()
    new_client = Signal()
    sig_is_spykernel = Signal(object)
    sig_kernel_restarted = Signal(str)
    sig_prompt_ready = Signal()

    # For global working directory
    sig_change_cwd = Signal(str)

    # For printing internal errors
    sig_exception_occurred = Signal(str, bool)

    def __init__(self, ipyclient, additional_options, interpreter_versions,
                 external_kernel, *args, **kw):
        # To override the Qt widget used by RichJupyterWidget
        self.custom_control = ControlWidget
        self.custom_page_control = PageControlWidget
        self.custom_edit = True
        super(ShellWidget, self).__init__(*args, **kw)

        self.ipyclient = ipyclient
        self.additional_options = additional_options
        self.interpreter_versions = interpreter_versions
        self.external_kernel = external_kernel
        self._cwd = ''

        # Keyboard shortcuts
        self.shortcuts = self.create_shortcuts()

        # Set the color of the matched parentheses here since the qtconsole
        # uses a hard-coded value that is not modified when the color scheme is
        # set in the qtconsole constructor. See spyder-ide/spyder#4806.
        self.set_bracket_matcher_color_scheme(self.syntax_style)

        self.spyder_kernel_comm = KernelComm(
            interrupt_callback=self._pdb_update)
        self.spyder_kernel_comm.sig_exception_occurred.connect(
            self.sig_exception_occurred)
        self.kernel_manager = None
        self.kernel_client = None
        handlers = {
            'pdb_state': self.set_pdb_state,
            'pdb_continue': self.pdb_continue,
            'get_breakpoints': self.get_spyder_breakpoints,
            'save_files': self.handle_save_files,
            'run_cell': self.handle_run_cell,
            'cell_count': self.handle_cell_count,
            'current_filename': self.handle_current_filename,
            'set_debug_state': self._handle_debug_state
        }
        for request_id in handlers:
            self.spyder_kernel_comm.register_call_handler(
                request_id, handlers[request_id])

    def call_kernel(self, interrupt=False, blocking=False, callback=None):
        """Send message to spyder."""
        return self.spyder_kernel_comm.remote_call(interrupt=interrupt,
                                                   blocking=blocking,
                                                   callback=callback)

    def set_kernel_client_and_manager(self, kernel_client, kernel_manager):
        """Set the kernel client and manager"""
        self.kernel_manager = kernel_manager
        self.kernel_client = kernel_client
        self.spyder_kernel_comm.open_comm(kernel_client)

    #---- Public API ----------------------------------------------------------
    def set_exit_callback(self):
        """Set exit callback for this shell."""
        self.exit_requested.connect(self.ipyclient.exit_callback)

    def is_running(self):
        if self.kernel_client is not None and \
          self.kernel_client.channels_running:
            return True
        else:
            return False

    def is_spyder_kernel(self):
        """Determine if the kernel is from Spyder."""
        code = u"getattr(get_ipython().kernel, 'set_value', False)"
        if self._reading:
            return
        else:
            self.silent_exec_method(code)

    def set_cwd(self, dirname):
        """Set shell current working directory."""
        # Replace single for double backslashes on Windows
        if os.name == 'nt':
            dirname = dirname.replace(u"\\", u"\\\\")

        if self.ipyclient.hostname is None:
            self.call_kernel(interrupt=True).set_cwd(dirname)
            self._cwd = dirname

    def update_cwd(self):
        """Update current working directory.

        Retrieve the cwd and emit a signal connected to the working directory
        widget. (see: handle_exec_method())
        """
        if self.kernel_client is None:
            return
        self.call_kernel(callback=self.remote_set_cwd).get_cwd()

    def remote_set_cwd(self, cwd):
        """Get current working directory from kernel."""
        self._cwd = cwd
        self.sig_change_cwd.emit(self._cwd)

    def set_bracket_matcher_color_scheme(self, color_scheme):
        """Set color scheme for matched parentheses."""
        bsh = sh.BaseSH(parent=self, color_scheme=color_scheme)
        mpcolor = bsh.get_matched_p_color()
        self._bracket_matcher.format.setBackground(mpcolor)

    def set_color_scheme(self, color_scheme, reset=True):
        """Set color scheme of the shell."""
        self.set_bracket_matcher_color_scheme(color_scheme)
        self.style_sheet, dark_color = create_qss_style(color_scheme)
        self.syntax_style = color_scheme
        self._style_sheet_changed()
        self._syntax_style_changed()
        if reset:
            self.reset(clear=True)
        if not dark_color:
            # Needed to change the colors of tracebacks
            self.silent_execute("%colors linux")
            self.call_kernel(
                interrupt=True,
                blocking=False).set_sympy_forecolor(background_color='dark')
        else:
            self.silent_execute("%colors lightbg")
            self.call_kernel(
                interrupt=True,
                blocking=False).set_sympy_forecolor(background_color='light')

    def request_syspath(self):
        """Ask the kernel for sys.path contents."""
        self.call_kernel(interrupt=True,
                         callback=self.sig_show_syspath.emit).get_syspath()

    def request_env(self):
        """Ask the kernel for environment variables."""
        self.call_kernel(interrupt=True,
                         callback=self.sig_show_env.emit).get_env()

    # --- To handle the banner
    def long_banner(self):
        """Banner for IPython widgets with pylab message"""
        # Default banner
        try:
            from IPython.core.usage import quick_guide
        except Exception:
            quick_guide = ''
        banner_parts = [
            'Python %s\n' % self.interpreter_versions['python_version'],
            'Type "copyright", "credits" or "license" for more information.\n\n',
            'IPython %s -- An enhanced Interactive Python.\n' % \
            self.interpreter_versions['ipython_version'],
            quick_guide
        ]
        banner = ''.join(banner_parts)

        # Pylab additions
        pylab_o = self.additional_options['pylab']
        autoload_pylab_o = self.additional_options['autoload_pylab']
        mpl_installed = programs.is_module_installed('matplotlib')
        if mpl_installed and (pylab_o and autoload_pylab_o):
            pylab_message = ("\nPopulating the interactive namespace from "
                             "numpy and matplotlib\n")
            banner = banner + pylab_message

        # Sympy additions
        sympy_o = self.additional_options['sympy']
        if sympy_o:
            lines = """
These commands were executed:
>>> from __future__ import division
>>> from sympy import *
>>> x, y, z, t = symbols('x y z t')
>>> k, m, n = symbols('k m n', integer=True)
>>> f, g, h = symbols('f g h', cls=Function)
"""
            banner = banner + lines
        if (pylab_o and sympy_o):
            lines = """
Warning: pylab (numpy and matplotlib) and symbolic math (sympy) are both
enabled at the same time. Some pylab functions are going to be overrided by
the sympy module (e.g. plot)
"""
            banner = banner + lines
        return banner

    def short_banner(self):
        """Short banner with Python and QtConsole versions"""
        banner = 'Python %s -- IPython %s' % (
            self.interpreter_versions['python_version'],
            self.interpreter_versions['ipython_version'])
        return banner

    # --- To define additional shortcuts
    def clear_console(self):
        if self.is_waiting_pdb_input():
            self.dbg_exec_magic('clear')
        else:
            self.execute("%clear")
        # Stop reading as any input has been removed.
        self._reading = False

    def _reset_namespace(self):
        warning = CONF.get('ipython_console', 'show_reset_namespace_warning')
        self.reset_namespace(warning=warning)

    def reset_namespace(self, warning=False, message=False):
        """Reset the namespace by removing all names defined by the user."""
        reset_str = _("Remove all variables")
        warn_str = _("All user-defined variables will be removed. "
                     "Are you sure you want to proceed?")
        # This is necessary to make resetting variables work in external
        # kernels.
        # See spyder-ide/spyder#9505.
        try:
            kernel_env = self.kernel_manager._kernel_spec.env
        except AttributeError:
            kernel_env = {}

        if warning:
            box = MessageCheckBox(icon=QMessageBox.Warning, parent=self)
            box.setWindowTitle(reset_str)
            box.set_checkbox_text(_("Don't show again."))
            box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
            box.setDefaultButton(QMessageBox.Yes)

            box.set_checked(False)
            box.set_check_visible(True)
            box.setText(warn_str)

            answer = box.exec_()

            # Update checkbox based on user interaction
            CONF.set('ipython_console', 'show_reset_namespace_warning',
                     not box.is_checked())
            self.ipyclient.reset_warning = not box.is_checked()

            if answer != QMessageBox.Yes:
                return

        try:
            if self.is_waiting_pdb_input():
                self.dbg_exec_magic('reset', '-f')
            else:
                if message:
                    self.reset()
                    self._append_html(_("<br><br>Removing all variables..."
                                        "\n<hr>"),
                                      before_prompt=False)
                self.silent_execute("%reset -f")
                if kernel_env.get('SPY_AUTOLOAD_PYLAB_O') == 'True':
                    self.silent_execute("from pylab import *")
                if kernel_env.get('SPY_SYMPY_O') == 'True':
                    sympy_init = """
                        from __future__ import division
                        from sympy import *
                        x, y, z, t = symbols('x y z t')
                        k, m, n = symbols('k m n', integer=True)
                        f, g, h = symbols('f g h', cls=Function)
                        init_printing()"""
                    self.silent_execute(dedent(sympy_init))
                if kernel_env.get('SPY_RUN_CYTHON') == 'True':
                    self.silent_execute("%reload_ext Cython")
                self.refresh_namespacebrowser()

                if not self.external_kernel:
                    self.call_kernel().close_all_mpl_figures()
        except AttributeError:
            pass

    def create_shortcuts(self):
        """Create shortcuts for ipyconsole."""
        inspect = config_shortcut(self._control.inspect_current_object,
                                  context='Console',
                                  name='Inspect current object',
                                  parent=self)
        clear_console = config_shortcut(self.clear_console,
                                        context='Console',
                                        name='Clear shell',
                                        parent=self)
        restart_kernel = config_shortcut(self.ipyclient.restart_kernel,
                                         context='ipython_console',
                                         name='Restart kernel',
                                         parent=self)
        new_tab = config_shortcut(lambda: self.new_client.emit(),
                                  context='ipython_console',
                                  name='new tab',
                                  parent=self)
        reset_namespace = config_shortcut(lambda: self._reset_namespace(),
                                          context='ipython_console',
                                          name='reset namespace',
                                          parent=self)
        array_inline = config_shortcut(self._control.enter_array_inline,
                                       context='array_builder',
                                       name='enter array inline',
                                       parent=self)
        array_table = config_shortcut(self._control.enter_array_table,
                                      context='array_builder',
                                      name='enter array table',
                                      parent=self)
        clear_line = config_shortcut(self.ipyclient.clear_line,
                                     context='console',
                                     name='clear line',
                                     parent=self)

        return [
            inspect, clear_console, restart_kernel, new_tab, reset_namespace,
            array_inline, array_table, clear_line
        ]

    # --- To communicate with the kernel
    def silent_execute(self, code):
        """Execute code in the kernel without increasing the prompt"""
        try:
            self.kernel_client.execute(to_text_string(code), silent=True)
        except AttributeError:
            pass

    def silent_exec_method(self, code):
        """Silently execute a kernel method and save its reply

        The methods passed here **don't** involve getting the value
        of a variable but instead replies that can be handled by
        ast.literal_eval.

        To get a value see `get_value`

        Parameters
        ----------
        code : string
            Code that contains the kernel method as part of its
            string

        See Also
        --------
        handle_exec_method : Method that deals with the reply

        Note
        ----
        This is based on the _silent_exec_callback method of
        RichJupyterWidget. Therefore this is licensed BSD
        """
        # Generate uuid, which would be used as an indication of whether or
        # not the unique request originated from here
        local_uuid = to_text_string(uuid.uuid1())
        code = to_text_string(code)
        if self.kernel_client is None:
            return

        msg_id = self.kernel_client.execute(
            '', silent=True, user_expressions={local_uuid: code})
        self._kernel_methods[local_uuid] = code
        self._request_info['execute'][msg_id] = self._ExecutionRequest(
            msg_id, 'silent_exec_method')

    def handle_exec_method(self, msg):
        """
        Handle data returned by silent executions of kernel methods

        This is based on the _handle_exec_callback of RichJupyterWidget.
        Therefore this is licensed BSD.
        """
        user_exp = msg['content'].get('user_expressions')
        if not user_exp:
            return
        for expression in user_exp:
            if expression in self._kernel_methods:
                # Process kernel reply
                method = self._kernel_methods[expression]
                reply = user_exp[expression]
                data = reply.get('data')
                if 'getattr' in method:
                    if data is not None and 'text/plain' in data:
                        is_spyder_kernel = data['text/plain']
                        if 'SpyderKernel' in is_spyder_kernel:
                            self.sig_is_spykernel.emit(self)

                # Remove method after being processed
                self._kernel_methods.pop(expression)

    def set_backend_for_mayavi(self, command):
        """
        Mayavi plots require the Qt backend, so we try to detect if one is
        generated to change backends
        """
        calling_mayavi = False
        lines = command.splitlines()
        for l in lines:
            if not l.startswith('#'):
                if 'import mayavi' in l or 'from mayavi' in l:
                    calling_mayavi = True
                    break
        if calling_mayavi:
            message = _("Changing backend to Qt for Mayavi")
            self._append_plain_text(message + '\n')
            self.silent_execute("%gui inline\n%gui qt")

    def change_mpl_backend(self, command):
        """
        If the user is trying to change Matplotlib backends with
        %matplotlib, send the same command again to the kernel to
        correctly change it.

        Fixes spyder-ide/spyder#4002.
        """
        if command.startswith('%matplotlib') and \
          len(command.splitlines()) == 1:
            if not 'inline' in command:
                self.silent_execute(command)

    # ---- Spyder-kernels methods -------------------------------------------
    def get_editor(self, filename):
        """Get editor for filename and set it as the current editor."""
        editorstack = self.get_editorstack()
        if editorstack is None:
            return None

        if not filename:
            return None

        index = editorstack.has_filename(filename)
        if index is None:
            return None

        editor = editorstack.data[index].editor
        editorstack.set_stack_index(index)
        return editor

    def get_editorstack(self):
        """Get the current editorstack."""
        plugin = self.ipyclient.plugin
        if plugin.main.editor is not None:
            editor = plugin.main.editor
            return editor.get_current_editorstack()
        raise RuntimeError('No editorstack found.')

    def handle_save_files(self):
        """Save the open files."""
        self.get_editorstack().save()

    def handle_run_cell(self, cell_name, filename):
        """
        Get cell code from cell name and file name.
        """
        editorstack = self.get_editorstack()
        editorstack.save()
        editor = self.get_editor(filename)

        if editor is None:
            raise RuntimeError(
                "File {} not open in the editor".format(filename))

        editorstack.last_cell_call = (filename, cell_name)

        # The file is open, load code from editor
        return editor.get_cell_code(cell_name)

    def handle_cell_count(self, filename):
        """Get number of cells in file to loop."""
        editorstack = self.get_editorstack()
        editorstack.save()
        editor = self.get_editor(filename)

        if editor is None:
            raise RuntimeError(
                "File {} not open in the editor".format(filename))

        # The file is open, get cell count from editor
        return editor.get_cell_count()

    def handle_current_filename(self):
        """Get the current filename."""
        return self.get_editorstack().get_current_finfo().filename

    # ---- Private methods (overrode by us) ---------------------------------

    def _handle_error(self, msg):
        """
        Reimplemented to reset the prompt if the error comes after the reply
        """
        self._process_execute_error(msg)
        self._show_interpreter_prompt()

    def _context_menu_make(self, pos):
        """Reimplement the IPython context menu"""
        menu = super(ShellWidget, self)._context_menu_make(pos)
        return self.ipyclient.add_actions_to_context_menu(menu)

    def _banner_default(self):
        """
        Reimplement banner creation to let the user decide if he wants a
        banner or not
        """
        # Don't change banner for external kernels
        if self.external_kernel:
            return ''
        show_banner_o = self.additional_options['show_banner']
        if show_banner_o:
            return self.long_banner()
        else:
            return self.short_banner()

    def _kernel_restarted_message(self, died=True):
        msg = _("Kernel died, restarting") if died else _("Kernel restarting")
        self.sig_kernel_restarted.emit(msg)

    def _syntax_style_changed(self):
        """Refresh the highlighting with the current syntax style by class."""
        if self._highlighter is None:
            # ignore premature calls
            return
        if self.syntax_style:
            self._highlighter._style = create_style_class(self.syntax_style)
            self._highlighter._clear_caches()
        else:
            self._highlighter.set_style_sheet(self.style_sheet)

    def _prompt_started_hook(self):
        """Emit a signal when the prompt is ready."""
        if not self._reading:
            self._highlighter.highlighting_on = True
            self.sig_prompt_ready.emit()

    #---- Qt methods ----------------------------------------------------------
    def focusInEvent(self, event):
        """Reimplement Qt method to send focus change notification"""
        self.focus_changed.emit()
        return super(ShellWidget, self).focusInEvent(event)

    def focusOutEvent(self, event):
        """Reimplement Qt method to send focus change notification"""
        self.focus_changed.emit()
        return super(ShellWidget, self).focusOutEvent(event)
Exemplo n.º 13
0
class ConfigurationPanel(QWidget):

    reloadApplication = Signal(str)

    def __init__(self, config_file, help_tool):
        QWidget.__init__(self)

        layout = QVBoxLayout()

        toolbar = QToolBar("toolbar")

        save_action = toolbar.addAction(resourceIcon("ide/disk"), "Save")
        save_action.triggered.connect(self.save)

        save_as_action = toolbar.addAction(resourceIcon("ide/save_as"),
                                           "Save As")
        save_as_action.triggered.connect(self.saveAs)

        # reload_icon = toolbar.style().standardIcon(QStyle.SP_BrowserReload)
        # reload_action = toolbar.addAction(reload_icon, "Reload")
        # reload_action.triggered.connect(self.reload)

        toolbar.addSeparator()

        toolbar.addAction(help_tool.getAction())

        stretchy_separator = QWidget()
        stretchy_separator.setSizePolicy(QSizePolicy.Expanding,
                                         QSizePolicy.Expanding)
        toolbar.addWidget(stretchy_separator)

        search = SearchBox()
        search.setMaximumWidth(200)
        search.setContentsMargins(5, 2, 5, 2)

        toolbar.addWidget(search)

        layout.addWidget(toolbar)

        self.ide_panel = IdePanel()
        layout.addWidget(self.ide_panel, 1)

        self.config_file = config_file

        with open(config_file) as f:
            config_file_text = f.read()

        self.highlighter = KeywordHighlighter(self.ide_panel.document())

        search.filterChanged.connect(self.highlighter.setSearchString)

        self.parseDefines(config_file_text)
        self.ide_panel.document().setPlainText(config_file_text)

        cursor = self.ide_panel.textCursor()
        cursor.setPosition(0)
        self.ide_panel.setTextCursor(cursor)
        self.ide_panel.setFocus()

        self.setLayout(layout)

    def getName(self):
        return "Configuration"

    def save(self):
        backup_path = "%s.backup" % self.config_file
        shutil.copyfile(self.config_file, backup_path)

        with open(self.config_file, "w") as f:
            f.write(self.ide_panel.getText())

        message = "To make your changes current, a reload of the configuration file is required. Would you like to reload now?"
        result = QMessageBox.information(self, "Reload required!", message,
                                         QMessageBox.Yes | QMessageBox.No)

        if result == QMessageBox.Yes:
            self.reload(self.config_file)

    def saveAs(self):
        config_file = QFileDialog.getSaveFileName(
            self, "Save Configuration File As")

        config_file = str(config_file)

        if len(config_file) > 0:
            with open(config_file, "w") as f:
                f.write(self.ide_panel.getText())

            message = "The current configuration file has been saved to a new file. Do you want to restart Ert using the new configuration file?"
            result = QMessageBox.information(self, "Restart Ert?", message,
                                             QMessageBox.Yes | QMessageBox.No)

            if result == QMessageBox.Yes:
                self.reload(config_file)

    def reload(self, path):
        self.reloadApplication.emit(path)

    def start(self):
        print("Start!")

    def parseDefines(self, text):
        pattern = re.compile(r"[ \t]*DEFINE[ \t]*(\S+)[ \t]*(\S+)")

        match = re.findall(pattern, text)

        for m in match:
            PathArgument.addDefine(m[0], m[1])
Exemplo n.º 14
0
class DiffEditor(DiffTextEdit):

    up = Signal()
    down = Signal()
    options_changed = Signal()
    updated = Signal()
    diff_text_changed = Signal(object)

    def __init__(self, parent, titlebar):
        DiffTextEdit.__init__(self, parent, numbers=True)
        self.model = model = main.model()

        # "Diff Options" tool menu
        self.diff_ignore_space_at_eol_action = add_action(
            self, N_('Ignore changes in whitespace at EOL'),
            self._update_diff_opts)
        self.diff_ignore_space_at_eol_action.setCheckable(True)

        self.diff_ignore_space_change_action = add_action(
            self, N_('Ignore changes in amount of whitespace'),
            self._update_diff_opts)
        self.diff_ignore_space_change_action.setCheckable(True)

        self.diff_ignore_all_space_action = add_action(
            self, N_('Ignore all whitespace'), self._update_diff_opts)
        self.diff_ignore_all_space_action.setCheckable(True)

        self.diff_function_context_action = add_action(
            self, N_('Show whole surrounding functions of changes'),
            self._update_diff_opts)
        self.diff_function_context_action.setCheckable(True)

        self.diff_show_line_numbers = add_action(self,
                                                 N_('Show lines numbers'),
                                                 self._update_diff_opts)
        self.diff_show_line_numbers.setCheckable(True)

        self.diffopts_button = create_action_button(tooltip=N_('Diff Options'),
                                                    icon=icons.configure())
        self.diffopts_menu = create_menu(N_('Diff Options'),
                                         self.diffopts_button)

        self.diffopts_menu.addAction(self.diff_ignore_space_at_eol_action)
        self.diffopts_menu.addAction(self.diff_ignore_space_change_action)
        self.diffopts_menu.addAction(self.diff_ignore_all_space_action)
        self.diffopts_menu.addAction(self.diff_show_line_numbers)
        self.diffopts_menu.addAction(self.diff_function_context_action)
        self.diffopts_button.setMenu(self.diffopts_menu)
        qtutils.hide_button_menu_indicator(self.diffopts_button)

        titlebar.add_corner_widget(self.diffopts_button)

        self.action_apply_selection = qtutils.add_action(
            self, 'Apply', self.apply_selection, hotkeys.STAGE_DIFF)

        self.action_revert_selection = qtutils.add_action(
            self, 'Revert', self.revert_selection, hotkeys.REVERT)
        self.action_revert_selection.setIcon(icons.undo())

        self.launch_editor = actions.launch_editor(self, *hotkeys.ACCEPT)
        self.launch_difftool = actions.launch_difftool(self)
        self.stage_or_unstage = actions.stage_or_unstage(self)

        # Emit up/down signals so that they can be routed by the main widget
        self.move_up = actions.move_up(self)
        self.move_down = actions.move_down(self)

        diff_text_changed = model.message_diff_text_changed
        model.add_observer(diff_text_changed, self.diff_text_changed.emit)

        self.selection_model = selection_model = selection.selection_model()
        selection_model.add_observer(selection_model.message_selection_changed,
                                     self.updated.emit)
        self.updated.connect(self.refresh, type=Qt.QueuedConnection)

        self.diff_text_changed.connect(self.set_diff)

    def refresh(self):
        enabled = False
        s = self.selection_model.selection()
        if s.modified and self.model.stageable():
            if s.modified[0] in self.model.submodules:
                pass
            elif s.modified[0] not in main.model().unstaged_deleted:
                enabled = True
        self.action_revert_selection.setEnabled(enabled)

    def enable_line_numbers(self, enabled):
        """Enable/disable the diff line number display"""
        self.numbers.setVisible(enabled)
        self.diff_show_line_numbers.setChecked(enabled)

    def show_line_numbers(self):
        """Return True if we should show line numbers"""
        return self.diff_show_line_numbers.isChecked()

    def _update_diff_opts(self):
        space_at_eol = self.diff_ignore_space_at_eol_action.isChecked()
        space_change = self.diff_ignore_space_change_action.isChecked()
        all_space = self.diff_ignore_all_space_action.isChecked()
        function_context = self.diff_function_context_action.isChecked()
        self.numbers.setVisible(self.show_line_numbers())

        gitcmds.update_diff_overrides(space_at_eol, space_change, all_space,
                                      function_context)
        self.options_changed.emit()

    # Qt overrides
    def contextMenuEvent(self, event):
        """Create the context menu for the diff display."""
        menu = qtutils.create_menu(N_('Actions'), self)
        s = selection.selection()
        filename = selection.filename()

        if self.model.stageable() or self.model.unstageable():
            if self.model.stageable():
                self.stage_or_unstage.setText(N_('Stage'))
            else:
                self.stage_or_unstage.setText(N_('Unstage'))
            menu.addAction(self.stage_or_unstage)

        if s.modified and self.model.stageable():
            if s.modified[0] in main.model().submodules:
                action = menu.addAction(icons.add(), cmds.Stage.name(),
                                        cmds.run(cmds.Stage, s.modified))
                action.setShortcut(hotkeys.STAGE_SELECTION)
                menu.addAction(
                    icons.cola(), N_('Launch git-cola'),
                    cmds.run(cmds.OpenRepo, core.abspath(s.modified[0])))
            elif s.modified[0] not in main.model().unstaged_deleted:
                if self.has_selection():
                    apply_text = N_('Stage Selected Lines')
                    revert_text = N_('Revert Selected Lines...')
                else:
                    apply_text = N_('Stage Diff Hunk')
                    revert_text = N_('Revert Diff Hunk...')

                self.action_apply_selection.setText(apply_text)
                self.action_apply_selection.setIcon(icons.add())

                self.action_revert_selection.setText(revert_text)

                menu.addAction(self.action_apply_selection)
                menu.addAction(self.action_revert_selection)

        if s.staged and self.model.unstageable():
            if s.staged[0] in main.model().submodules:
                action = menu.addAction(icons.remove(), cmds.Unstage.name(),
                                        cmds.do(cmds.Unstage, s.staged))
                action.setShortcut(hotkeys.STAGE_SELECTION)
                menu.addAction(
                    icons.cola(), N_('Launch git-cola'),
                    cmds.do(cmds.OpenRepo, core.abspath(s.staged[0])))
            elif s.staged[0] not in main.model().staged_deleted:
                if self.has_selection():
                    apply_text = N_('Unstage Selected Lines')
                else:
                    apply_text = N_('Unstage Diff Hunk')

                self.action_apply_selection.setText(apply_text)
                self.action_apply_selection.setIcon(icons.remove())
                menu.addAction(self.action_apply_selection)

        if self.model.stageable() or self.model.unstageable():
            # Do not show the "edit" action when the file does not exist.
            # Untracked files exist by definition.
            if filename and core.exists(filename):
                menu.addSeparator()
                menu.addAction(self.launch_editor)

            # Removed files can still be diffed.
            menu.addAction(self.launch_difftool)

        # Add the Previous/Next File actions, which improves discoverability
        # of their associated shortcuts
        menu.addSeparator()
        menu.addAction(self.move_up)
        menu.addAction(self.move_down)

        menu.addSeparator()
        action = menu.addAction(icons.copy(), N_('Copy'), self.copy)
        action.setShortcut(QtGui.QKeySequence.Copy)

        action = menu.addAction(icons.select_all(), N_('Select All'),
                                self.selectAll)
        action.setShortcut(QtGui.QKeySequence.SelectAll)
        menu.exec_(self.mapToGlobal(event.pos()))

    def mousePressEvent(self, event):
        if event.button() == Qt.RightButton:
            # Intercept right-click to move the cursor to the current position.
            # setTextCursor() clears the selection so this is only done when
            # nothing is selected.
            if not self.has_selection():
                cursor = self.cursorForPosition(event.pos())
                self.setTextCursor(cursor)

        return DiffTextEdit.mousePressEvent(self, event)

    def setPlainText(self, text):
        """setPlainText(str) while retaining scrollbar positions"""
        mode = self.model.mode
        highlight = (mode != self.model.mode_none
                     and mode != self.model.mode_untracked)
        self.highlighter.set_enabled(highlight)

        scrollbar = self.verticalScrollBar()
        if scrollbar:
            scrollvalue = scrollbar.value()
        else:
            scrollvalue = None

        if text is None:
            return

        DiffTextEdit.setPlainText(self, text)

        if scrollbar and scrollvalue is not None:
            scrollbar.setValue(scrollvalue)

    def selected_lines(self):
        cursor = self.textCursor()
        selection_start = cursor.selectionStart()
        selection_end = max(selection_start, cursor.selectionEnd() - 1)

        first_line_idx = -1
        last_line_idx = -1
        line_idx = 0
        line_start = 0

        for line_idx, line in enumerate(self.value().splitlines()):
            line_end = line_start + len(line)
            if line_start <= selection_start <= line_end:
                first_line_idx = line_idx
            if line_start <= selection_end <= line_end:
                last_line_idx = line_idx
                break
            line_start = line_end + 1

        if first_line_idx == -1:
            first_line_idx = line_idx

        if last_line_idx == -1:
            last_line_idx = line_idx

        return first_line_idx, last_line_idx

    def apply_selection(self):
        s = selection.single_selection()
        if self.model.stageable() and s.modified:
            self.process_diff_selection()
        elif self.model.unstageable():
            self.process_diff_selection(reverse=True)

    def revert_selection(self):
        """Destructively revert selected lines or hunk from a worktree file."""

        if self.has_selection():
            title = N_('Revert Selected Lines?')
            ok_text = N_('Revert Selected Lines')
        else:
            title = N_('Revert Diff Hunk?')
            ok_text = N_('Revert Diff Hunk')

        if not qtutils.confirm(title,
                               N_('This operation drops uncommitted changes.\n'
                                  'These changes cannot be recovered.'),
                               N_('Revert the uncommitted changes?'),
                               ok_text,
                               default=True,
                               icon=icons.undo()):
            return
        self.process_diff_selection(reverse=True, apply_to_worktree=True)

    def process_diff_selection(self, reverse=False, apply_to_worktree=False):
        """Implement un/staging of the selected line(s) or hunk."""
        if selection.selection_model().is_empty():
            return
        first_line_idx, last_line_idx = self.selected_lines()
        cmds.do(cmds.ApplyDiffSelection, first_line_idx, last_line_idx,
                self.has_selection(), reverse, apply_to_worktree)
Exemplo n.º 15
0
class DiffEditor(DiffTextEdit):

    up = Signal()
    down = Signal()
    options_changed = Signal()

    def __init__(self, context, options, parent):
        DiffTextEdit.__init__(self, context, parent, numbers=True)
        self.context = context
        self.model = model = context.model
        self.selection_model = selection_model = context.selection

        # "Diff Options" tool menu
        self.options = options

        self.action_apply_selection = qtutils.add_action(
            self,
            'Apply',
            self.apply_selection,
            hotkeys.STAGE_DIFF,
            hotkeys.STAGE_DIFF_ALT,
        )

        self.action_revert_selection = qtutils.add_action(
            self, 'Revert', self.revert_selection, hotkeys.REVERT
        )
        self.action_revert_selection.setIcon(icons.undo())

        self.launch_editor = actions.launch_editor_at_line(
            context, self, hotkeys.EDIT_SHORT, *hotkeys.ACCEPT
        )
        self.launch_difftool = actions.launch_difftool(context, self)
        self.stage_or_unstage = actions.stage_or_unstage(context, self)

        # Emit up/down signals so that they can be routed by the main widget
        self.move_up = actions.move_up(self)
        self.move_down = actions.move_down(self)

        model.diff_text_updated.connect(self.set_diff, type=Qt.QueuedConnection)

        selection_model.selection_changed.connect(
            self.refresh, type=Qt.QueuedConnection
        )
        # Update the selection model when the cursor changes
        self.cursorPositionChanged.connect(self._update_line_number)

        qtutils.connect_button(options.toggle_image_diff, self.toggle_diff_type)

    def toggle_diff_type(self):
        cmds.do(cmds.ToggleDiffType, self.context)

    def refresh(self):
        enabled = False
        s = self.selection_model.selection()
        model = self.model
        if model.stageable():
            item = s.modified[0] if s.modified else None
            if item in model.submodules:
                pass
            elif item not in model.unstaged_deleted:
                enabled = True
        self.action_revert_selection.setEnabled(enabled)

    def set_line_numbers(self, enabled, update=False):
        """Enable/disable the diff line number display"""
        self.numbers.setVisible(enabled)
        if update:
            with qtutils.BlockSignals(self.options.show_line_numbers):
                self.options.show_line_numbers.setChecked(enabled)
        # Refresh the display. Not doing this results in the display not
        # correctly displaying the line numbers widget until the text scrolls.
        self.set_value(self.value())

    def set_word_wrapping(self, enabled, update=False):
        """Enable/disable word wrapping"""
        if update:
            with qtutils.BlockSignals(self.options.enable_word_wrapping):
                self.options.enable_word_wrapping.setChecked(enabled)
        if enabled:
            self.setWordWrapMode(QtGui.QTextOption.WordWrap)
            self.setLineWrapMode(QtWidgets.QPlainTextEdit.WidgetWidth)
        else:
            self.setWordWrapMode(QtGui.QTextOption.NoWrap)
            self.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap)

    def set_options(self):
        self.options_changed.emit()

    # Qt overrides
    def contextMenuEvent(self, event):
        """Create the context menu for the diff display."""
        menu = qtutils.create_menu(N_('Actions'), self)
        context = self.context
        model = self.model
        s = self.selection_model.selection()
        filename = self.selection_model.filename()

        if model.stageable() or model.unstageable():
            if model.stageable():
                self.stage_or_unstage.setText(N_('Stage'))
            else:
                self.stage_or_unstage.setText(N_('Unstage'))
            menu.addAction(self.stage_or_unstage)

        if model.stageable():
            item = s.modified[0] if s.modified else None
            if item in model.submodules:
                path = core.abspath(item)
                action = menu.addAction(
                    icons.add(),
                    cmds.Stage.name(),
                    cmds.run(cmds.Stage, context, s.modified),
                )
                action.setShortcut(hotkeys.STAGE_SELECTION)
                menu.addAction(
                    icons.cola(),
                    N_('Launch git-cola'),
                    cmds.run(cmds.OpenRepo, context, path),
                )
            elif item not in model.unstaged_deleted:
                if self.has_selection():
                    apply_text = N_('Stage Selected Lines')
                    revert_text = N_('Revert Selected Lines...')
                else:
                    apply_text = N_('Stage Diff Hunk')
                    revert_text = N_('Revert Diff Hunk...')

                self.action_apply_selection.setText(apply_text)
                self.action_apply_selection.setIcon(icons.add())

                self.action_revert_selection.setText(revert_text)

                menu.addAction(self.action_apply_selection)
                menu.addAction(self.action_revert_selection)

        if s.staged and model.unstageable():
            item = s.staged[0]
            if item in model.submodules:
                path = core.abspath(item)
                action = menu.addAction(
                    icons.remove(),
                    cmds.Unstage.name(),
                    cmds.run(cmds.Unstage, context, s.staged),
                )
                action.setShortcut(hotkeys.STAGE_SELECTION)
                menu.addAction(
                    icons.cola(),
                    N_('Launch git-cola'),
                    cmds.run(cmds.OpenRepo, context, path),
                )
            elif item not in model.staged_deleted:
                if self.has_selection():
                    apply_text = N_('Unstage Selected Lines')
                else:
                    apply_text = N_('Unstage Diff Hunk')

                self.action_apply_selection.setText(apply_text)
                self.action_apply_selection.setIcon(icons.remove())
                menu.addAction(self.action_apply_selection)

        if model.stageable() or model.unstageable():
            # Do not show the "edit" action when the file does not exist.
            # Untracked files exist by definition.
            if filename and core.exists(filename):
                menu.addSeparator()
                menu.addAction(self.launch_editor)

            # Removed files can still be diffed.
            menu.addAction(self.launch_difftool)

        # Add the Previous/Next File actions, which improves discoverability
        # of their associated shortcuts
        menu.addSeparator()
        menu.addAction(self.move_up)
        menu.addAction(self.move_down)

        menu.addSeparator()
        action = menu.addAction(icons.copy(), N_('Copy'), self.copy)
        action.setShortcut(QtGui.QKeySequence.Copy)

        action = menu.addAction(icons.select_all(), N_('Select All'), self.selectAll)
        action.setShortcut(QtGui.QKeySequence.SelectAll)
        menu.exec_(self.mapToGlobal(event.pos()))

    def mousePressEvent(self, event):
        if event.button() == Qt.RightButton:
            # Intercept right-click to move the cursor to the current position.
            # setTextCursor() clears the selection so this is only done when
            # nothing is selected.
            if not self.has_selection():
                cursor = self.cursorForPosition(event.pos())
                self.setTextCursor(cursor)

        return super(DiffEditor, self).mousePressEvent(event)

    def setPlainText(self, text):
        """setPlainText(str) while retaining scrollbar positions"""
        model = self.model
        mode = model.mode
        highlight = mode not in (model.mode_none, model.mode_untracked)
        self.highlighter.set_enabled(highlight)

        scrollbar = self.verticalScrollBar()
        if scrollbar:
            scrollvalue = get(scrollbar)
        else:
            scrollvalue = None

        if text is None:
            return

        DiffTextEdit.setPlainText(self, text)

        if scrollbar and scrollvalue is not None:
            scrollbar.setValue(scrollvalue)

    def selected_lines(self):
        cursor = self.textCursor()
        selection_start = cursor.selectionStart()
        selection_end = max(selection_start, cursor.selectionEnd() - 1)

        first_line_idx = -1
        last_line_idx = -1
        line_idx = 0
        line_start = 0

        for line_idx, line in enumerate(get(self).splitlines()):
            line_end = line_start + len(line)
            if line_start <= selection_start <= line_end:
                first_line_idx = line_idx
            if line_start <= selection_end <= line_end:
                last_line_idx = line_idx
                break
            line_start = line_end + 1

        if first_line_idx == -1:
            first_line_idx = line_idx

        if last_line_idx == -1:
            last_line_idx = line_idx

        return first_line_idx, last_line_idx

    def apply_selection(self):
        model = self.model
        s = self.selection_model.single_selection()
        if model.stageable() and (s.modified or s.untracked):
            self.process_diff_selection()
        elif model.unstageable():
            self.process_diff_selection(reverse=True)

    def revert_selection(self):
        """Destructively revert selected lines or hunk from a worktree file."""

        if self.has_selection():
            title = N_('Revert Selected Lines?')
            ok_text = N_('Revert Selected Lines')
        else:
            title = N_('Revert Diff Hunk?')
            ok_text = N_('Revert Diff Hunk')

        if not Interaction.confirm(
            title,
            N_(
                'This operation drops uncommitted changes.\n'
                'These changes cannot be recovered.'
            ),
            N_('Revert the uncommitted changes?'),
            ok_text,
            default=True,
            icon=icons.undo(),
        ):
            return
        self.process_diff_selection(reverse=True, apply_to_worktree=True)

    def process_diff_selection(self, reverse=False, apply_to_worktree=False):
        """Implement un/staging of the selected line(s) or hunk."""
        if self.selection_model.is_empty():
            return
        context = self.context
        first_line_idx, last_line_idx = self.selected_lines()
        cmds.do(
            cmds.ApplyDiffSelection,
            context,
            first_line_idx,
            last_line_idx,
            self.has_selection(),
            reverse,
            apply_to_worktree,
        )

    def _update_line_number(self):
        """Update the selection model when the cursor changes"""
        self.selection_model.line_number = self.numbers.current_line()
Exemplo n.º 16
0
class FigureThumbnail(QWidget):
    """
    A widget that consists of a FigureCanvas, a side toolbar, and a context
    menu that is used to show preview of figures in the ThumbnailScrollBar.
    """
    sig_canvas_clicked = Signal(object)
    sig_remove_figure = Signal(object)
    sig_save_figure = Signal(object, str)

    def __init__(self, parent=None, background_color=None):
        super(FigureThumbnail, self).__init__(parent)
        self.canvas = FigureCanvas(self, background_color=background_color)
        self.canvas.installEventFilter(self)
        self.setup_gui()

    def setup_gui(self):
        """Setup the main layout of the widget."""
        layout = QGridLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.canvas, 0, 0, Qt.AlignCenter)
        layout.addLayout(self.setup_toolbar(), 0, 1, 2, 1)
        layout.setRowStretch(1, 100)
        layout.setSizeConstraint(layout.SetFixedSize)

    def setup_toolbar(self):
        """Setup the toolbar."""
        self.savefig_btn = create_toolbutton(self,
                                             icon=ima.icon('filesave'),
                                             tip=_("Save Image As..."),
                                             triggered=self.emit_save_figure)
        self.delfig_btn = create_toolbutton(self,
                                            icon=ima.icon('editclear'),
                                            tip=_("Delete image"),
                                            triggered=self.emit_remove_figure)

        toolbar = QVBoxLayout()
        toolbar.setContentsMargins(0, 0, 0, 0)
        toolbar.setSpacing(1)
        toolbar.addWidget(self.savefig_btn)
        toolbar.addWidget(self.delfig_btn)
        toolbar.addStretch(2)

        return toolbar

    def highlight_canvas(self, highlight):
        """
        Set a colored frame around the FigureCanvas if highlight is True.
        """
        colorname = self.canvas.palette().highlight().color().name()
        if highlight:
            # Highlighted figure is not clear in dark mode with blue color.
            # See spyder-ide/spyder#10255.
            if is_dark_interface():
                self.canvas.setStyleSheet(
                    "FigureCanvas{border: 2px solid %s;}" % "#148CD2")
            else:
                self.canvas.setStyleSheet(
                    "FigureCanvas{border: 2px solid %s;}" % colorname)
        else:
            self.canvas.setStyleSheet("FigureCanvas{}")

    def scale_canvas_size(self, max_canvas_size):
        """
        Scale this thumbnail canvas size, while respecting its associated
        figure dimension ratio.
        """
        fwidth = self.canvas.fwidth
        fheight = self.canvas.fheight
        if fwidth / fheight > 1:
            canvas_width = max_canvas_size
            canvas_height = canvas_width / fwidth * fheight
        else:
            canvas_height = max_canvas_size
            canvas_width = canvas_height / fheight * fwidth
        self.canvas.setFixedSize(int(canvas_width), int(canvas_height))
        self.layout().setColumnMinimumWidth(0, max_canvas_size)

    def eventFilter(self, widget, event):
        """
        A filter that is used to send a signal when the figure canvas is
        clicked.
        """
        if event.type() == QEvent.MouseButtonPress:
            if event.button() == Qt.LeftButton:
                self.sig_canvas_clicked.emit(self)
        return super(FigureThumbnail, self).eventFilter(widget, event)

    def emit_save_figure(self):
        """
        Emit a signal when the toolbutton to save the figure is clicked.
        """
        self.sig_save_figure.emit(self.canvas.fig, self.canvas.fmt)

    def emit_remove_figure(self):
        """
        Emit a signal when the toolbutton to close the figure is clicked.
        """
        self.sig_remove_figure.emit(self)
Exemplo n.º 17
0
class ShellBaseWidget(ConsoleBaseWidget, SaveHistoryMixin, BrowseHistoryMixin):
    """
    Shell base widget
    """

    redirect_stdio = Signal(bool)
    sig_keyboard_interrupt = Signal()
    execute = Signal(str)
    append_to_history = Signal(str, str)

    def __init__(self,
                 parent,
                 history_filename,
                 profile=False,
                 initial_message=None,
                 default_foreground_color=None,
                 error_foreground_color=None,
                 traceback_foreground_color=None,
                 prompt_foreground_color=None,
                 background_color=None):
        """
        parent : specifies the parent widget
        """
        ConsoleBaseWidget.__init__(self, parent)
        SaveHistoryMixin.__init__(self, history_filename)
        BrowseHistoryMixin.__init__(self)

        # Prompt position: tuple (line, index)
        self.current_prompt_pos = None
        self.new_input_line = True

        # History
        assert is_text_string(history_filename)
        self.history = self.load_history()

        # Session
        self.historylog_filename = CONF.get('main', 'historylog_filename',
                                            get_conf_path('history.log'))

        # Context menu
        self.menu = None
        self.setup_context_menu()

        # Simple profiling test
        self.profile = profile

        # Buffer to increase performance of write/flush operations
        self.__buffer = []
        if initial_message:
            self.__buffer.append(initial_message)

        self.__timestamp = 0.0
        self.__flushtimer = QTimer(self)
        self.__flushtimer.setSingleShot(True)
        self.__flushtimer.timeout.connect(self.flush)

        # Give focus to widget
        self.setFocus()

        # Cursor width
        self.setCursorWidth(CONF.get('main', 'cursor/width'))

    def toggle_wrap_mode(self, enable):
        """Enable/disable wrap mode"""
        self.set_wrap_mode('character' if enable else None)

    def set_font(self, font):
        """Set shell styles font"""
        self.setFont(font)
        self.set_pythonshell_font(font)
        cursor = self.textCursor()
        cursor.select(QTextCursor.Document)
        charformat = QTextCharFormat()
        charformat.setFontFamily(font.family())
        charformat.setFontPointSize(font.pointSize())
        cursor.mergeCharFormat(charformat)

    #------ Context menu
    def setup_context_menu(self):
        """Setup shell context menu"""
        self.menu = QMenu(self)
        self.cut_action = create_action(self,
                                        _("Cut"),
                                        shortcut=keybinding('Cut'),
                                        icon=ima.icon('editcut'),
                                        triggered=self.cut)
        self.copy_action = create_action(self,
                                         _("Copy"),
                                         shortcut=keybinding('Copy'),
                                         icon=ima.icon('editcopy'),
                                         triggered=self.copy)
        paste_action = create_action(self,
                                     _("Paste"),
                                     shortcut=keybinding('Paste'),
                                     icon=ima.icon('editpaste'),
                                     triggered=self.paste)
        save_action = create_action(self,
                                    _("Save history log..."),
                                    icon=ima.icon('filesave'),
                                    tip=_(
                                        "Save current history log (i.e. all "
                                        "inputs and outputs) in a text file"),
                                    triggered=self.save_historylog)
        self.delete_action = create_action(self,
                                           _("Delete"),
                                           shortcut=keybinding('Delete'),
                                           icon=ima.icon('editdelete'),
                                           triggered=self.delete)
        selectall_action = create_action(self,
                                         _("Select All"),
                                         shortcut=keybinding('SelectAll'),
                                         icon=ima.icon('selectall'),
                                         triggered=self.selectAll)
        add_actions(
            self.menu,
            (self.cut_action, self.copy_action, paste_action,
             self.delete_action, None, selectall_action, None, save_action))

    def contextMenuEvent(self, event):
        """Reimplement Qt method"""
        state = self.has_selected_text()
        self.copy_action.setEnabled(state)
        self.cut_action.setEnabled(state)
        self.delete_action.setEnabled(state)
        self.menu.popup(event.globalPos())
        event.accept()

    #------ Input buffer
    def get_current_line_from_cursor(self):
        return self.get_text('cursor', 'eof')

    def _select_input(self):
        """Select current line (without selecting console prompt)"""
        line, index = self.get_position('eof')
        if self.current_prompt_pos is None:
            pline, pindex = line, index
        else:
            pline, pindex = self.current_prompt_pos
        self.setSelection(pline, pindex, line, index)

    @Slot()
    def clear_terminal(self):
        """
        Clear terminal window
        Child classes reimplement this method to write prompt
        """
        self.clear()

    # The buffer being edited
    def _set_input_buffer(self, text):
        """Set input buffer"""
        if self.current_prompt_pos is not None:
            self.replace_text(self.current_prompt_pos, 'eol', text)
        else:
            self.insert(text)
        self.set_cursor_position('eof')

    def _get_input_buffer(self):
        """Return input buffer"""
        input_buffer = ''
        if self.current_prompt_pos is not None:
            input_buffer = self.get_text(self.current_prompt_pos, 'eol')
            input_buffer = input_buffer.replace(os.linesep, '\n')
        return input_buffer

    input_buffer = Property("QString", _get_input_buffer, _set_input_buffer)

    #------ Prompt
    def new_prompt(self, prompt):
        """
        Print a new prompt and save its (line, index) position
        """
        if self.get_cursor_line_column()[1] != 0:
            self.write('\n')
        self.write(prompt, prompt=True)
        # now we update our cursor giving end of prompt
        self.current_prompt_pos = self.get_position('cursor')
        self.ensureCursorVisible()
        self.new_input_line = False

    def check_selection(self):
        """
        Check if selected text is r/w,
        otherwise remove read-only parts of selection
        """
        if self.current_prompt_pos is None:
            self.set_cursor_position('eof')
        else:
            self.truncate_selection(self.current_prompt_pos)

    #------ Copy / Keyboard interrupt
    @Slot()
    def copy(self):
        """Copy text to clipboard... or keyboard interrupt"""
        if self.has_selected_text():
            ConsoleBaseWidget.copy(self)
        elif not sys.platform == 'darwin':
            self.interrupt()

    def interrupt(self):
        """Keyboard interrupt"""
        self.sig_keyboard_interrupt.emit()

    @Slot()
    def cut(self):
        """Cut text"""
        self.check_selection()
        if self.has_selected_text():
            ConsoleBaseWidget.cut(self)

    @Slot()
    def delete(self):
        """Remove selected text"""
        self.check_selection()
        if self.has_selected_text():
            ConsoleBaseWidget.remove_selected_text(self)

    @Slot()
    def save_historylog(self):
        """Save current history log (all text in console)"""
        title = _("Save history log")
        self.redirect_stdio.emit(False)
        filename, _selfilter = getsavefilename(
            self, title, self.historylog_filename,
            "%s (*.log)" % _("History logs"))
        self.redirect_stdio.emit(True)
        if filename:
            filename = osp.normpath(filename)
            try:
                encoding.write(to_text_string(self.get_text_with_eol()),
                               filename)
                self.historylog_filename = filename
                CONF.set('main', 'historylog_filename', filename)
            except EnvironmentError:
                pass

    #------ Basic keypress event handler
    def on_enter(self, command):
        """on_enter"""
        self.execute_command(command)

    def execute_command(self, command):
        self.execute.emit(command)
        self.add_to_history(command)
        self.new_input_line = True

    def on_new_line(self):
        """On new input line"""
        self.set_cursor_position('eof')
        self.current_prompt_pos = self.get_position('cursor')
        self.new_input_line = False

    @Slot()
    def paste(self):
        """Reimplemented slot to handle multiline paste action"""
        if self.new_input_line:
            self.on_new_line()
        ConsoleBaseWidget.paste(self)

    def keyPressEvent(self, event):
        """
        Reimplement Qt Method
        Basic keypress event handler
        (reimplemented in InternalShell to add more sophisticated features)
        """
        if self.preprocess_keyevent(event):
            # Event was accepted in self.preprocess_keyevent
            return
        self.postprocess_keyevent(event)

    def preprocess_keyevent(self, event):
        """Pre-process keypress event:
        return True if event is accepted, false otherwise"""
        # Copy must be done first to be able to copy read-only text parts
        # (otherwise, right below, we would remove selection
        #  if not on current line)
        ctrl = event.modifiers() & Qt.ControlModifier
        meta = event.modifiers() & Qt.MetaModifier  # meta=ctrl in OSX
        if event.key() == Qt.Key_C and \
          ((Qt.MetaModifier | Qt.ControlModifier) & event.modifiers()):
            if meta and sys.platform == 'darwin':
                self.interrupt()
            elif ctrl:
                self.copy()
            event.accept()
            return True

        if self.new_input_line and ( len(event.text()) or event.key() in \
           (Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right) ):
            self.on_new_line()

        return False

    def postprocess_keyevent(self, event):
        """Post-process keypress event:
        in InternalShell, this is method is called when shell is ready"""
        event, text, key, ctrl, shift = restore_keyevent(event)

        # Is cursor on the last line? and after prompt?
        if len(text):
            #XXX: Shouldn't it be: `if len(unicode(text).strip(os.linesep))` ?
            if self.has_selected_text():
                self.check_selection()
            self.restrict_cursor_position(self.current_prompt_pos, 'eof')

        cursor_position = self.get_position('cursor')

        if key in (Qt.Key_Return, Qt.Key_Enter):
            if self.is_cursor_on_last_line():
                self._key_enter()
            # add and run selection
            else:
                self.insert_text(self.get_selected_text(), at_end=True)

        elif key == Qt.Key_Insert and not shift and not ctrl:
            self.setOverwriteMode(not self.overwriteMode())

        elif key == Qt.Key_Delete:
            if self.has_selected_text():
                self.check_selection()
                self.remove_selected_text()
            elif self.is_cursor_on_last_line():
                self.stdkey_clear()

        elif key == Qt.Key_Backspace:
            self._key_backspace(cursor_position)

        elif key == Qt.Key_Tab:
            self._key_tab()

        elif key == Qt.Key_Space and ctrl:
            self._key_ctrl_space()

        elif key == Qt.Key_Left:
            if self.current_prompt_pos == cursor_position:
                # Avoid moving cursor on prompt
                return
            method = self.extend_selection_to_next if shift \
                     else self.move_cursor_to_next
            method('word' if ctrl else 'character', direction='left')

        elif key == Qt.Key_Right:
            if self.is_cursor_at_end():
                return
            method = self.extend_selection_to_next if shift \
                     else self.move_cursor_to_next
            method('word' if ctrl else 'character', direction='right')

        elif (key == Qt.Key_Home) or ((key == Qt.Key_Up) and ctrl):
            self._key_home(shift, ctrl)

        elif (key == Qt.Key_End) or ((key == Qt.Key_Down) and ctrl):
            self._key_end(shift, ctrl)

        elif key == Qt.Key_Up:
            if not self.is_cursor_on_last_line():
                self.set_cursor_position('eof')
            y_cursor = self.get_coordinates(cursor_position)[1]
            y_prompt = self.get_coordinates(self.current_prompt_pos)[1]
            if y_cursor > y_prompt:
                self.stdkey_up(shift)
            else:
                self.browse_history(backward=True)

        elif key == Qt.Key_Down:
            if not self.is_cursor_on_last_line():
                self.set_cursor_position('eof')
            y_cursor = self.get_coordinates(cursor_position)[1]
            y_end = self.get_coordinates('eol')[1]
            if y_cursor < y_end:
                self.stdkey_down(shift)
            else:
                self.browse_history(backward=False)

        elif key in (Qt.Key_PageUp, Qt.Key_PageDown):
            #XXX: Find a way to do this programmatically instead of calling
            # widget keyhandler (this won't work if the *event* is coming from
            # the event queue - i.e. if the busy buffer is ever implemented)
            ConsoleBaseWidget.keyPressEvent(self, event)

        elif key == Qt.Key_Escape and shift:
            self.clear_line()

        elif key == Qt.Key_Escape:
            self._key_escape()

        elif key == Qt.Key_L and ctrl:
            self.clear_terminal()

        elif key == Qt.Key_V and ctrl:
            self.paste()

        elif key == Qt.Key_X and ctrl:
            self.cut()

        elif key == Qt.Key_Z and ctrl:
            self.undo()

        elif key == Qt.Key_Y and ctrl:
            self.redo()

        elif key == Qt.Key_A and ctrl:
            self.selectAll()

        elif key == Qt.Key_Question and not self.has_selected_text():
            self._key_question(text)

        elif key == Qt.Key_ParenLeft and not self.has_selected_text():
            self._key_parenleft(text)

        elif key == Qt.Key_Period and not self.has_selected_text():
            self._key_period(text)

        elif len(text) and not self.isReadOnly():
            self.hist_wholeline = False
            self.insert_text(text)
            self._key_other(text)

        else:
            # Let the parent widget handle the key press event
            ConsoleBaseWidget.keyPressEvent(self, event)

    #------ Key handlers
    def _key_enter(self):
        command = self.input_buffer
        self.insert_text('\n', at_end=True)
        self.on_enter(command)
        self.flush()

    def _key_other(self, text):
        raise NotImplementedError

    def _key_backspace(self, cursor_position):
        raise NotImplementedError

    def _key_tab(self):
        raise NotImplementedError

    def _key_ctrl_space(self):
        raise NotImplementedError

    def _key_home(self, shift, ctrl):
        if self.is_cursor_on_last_line():
            self.stdkey_home(shift, ctrl, self.current_prompt_pos)

    def _key_end(self, shift, ctrl):
        if self.is_cursor_on_last_line():
            self.stdkey_end(shift, ctrl)

    def _key_pageup(self):
        raise NotImplementedError

    def _key_pagedown(self):
        raise NotImplementedError

    def _key_escape(self):
        raise NotImplementedError

    def _key_question(self, text):
        raise NotImplementedError

    def _key_parenleft(self, text):
        raise NotImplementedError

    def _key_period(self, text):
        raise NotImplementedError

    #------ History Management
    def load_history(self):
        """Load history from a .py file in user home directory"""
        if osp.isfile(self.history_filename):
            rawhistory, _ = encoding.readlines(self.history_filename)
            rawhistory = [line.replace('\n', '') for line in rawhistory]
            if rawhistory[1] != self.INITHISTORY[1]:
                rawhistory[1] = self.INITHISTORY[1]
        else:
            rawhistory = self.INITHISTORY
        history = [line for line in rawhistory \
                   if line and not line.startswith('#')]

        # Truncating history to X entries:
        while len(history) >= CONF.get('historylog', 'max_entries'):
            del history[0]
            while rawhistory[0].startswith('#'):
                del rawhistory[0]
            del rawhistory[0]

        # Saving truncated history:
        try:
            encoding.writelines(rawhistory, self.history_filename)
        except EnvironmentError:
            pass

        return history

    #------ Simulation standards input/output
    def write_error(self, text):
        """Simulate stderr"""
        self.flush()
        self.write(text, flush=True, error=True)
        if get_debug_level():
            STDERR.write(text)

    def write(self, text, flush=False, error=False, prompt=False):
        """Simulate stdout and stderr"""
        if prompt:
            self.flush()
        if not is_string(text):
            # This test is useful to discriminate QStrings from decoded str
            text = to_text_string(text)
        self.__buffer.append(text)
        ts = time.time()
        if flush or prompt:
            self.flush(error=error, prompt=prompt)
        elif ts - self.__timestamp > 0.05:
            self.flush(error=error)
            self.__timestamp = ts
            # Timer to flush strings cached by last write() operation in series
            self.__flushtimer.start(50)

    def flush(self, error=False, prompt=False):
        """Flush buffer, write text to console"""
        # Fix for Issue 2452
        if PY3:
            try:
                text = "".join(self.__buffer)
            except TypeError:
                text = b"".join(self.__buffer)
                try:
                    text = text.decode(locale.getdefaultlocale()[1])
                except:
                    pass
        else:
            text = "".join(self.__buffer)

        self.__buffer = []
        self.insert_text(text, at_end=True, error=error, prompt=prompt)
        QCoreApplication.processEvents()
        self.repaint()
        # Clear input buffer:
        self.new_input_line = True

    #------ Text Insertion
    def insert_text(self, text, at_end=False, error=False, prompt=False):
        """
        Insert text at the current cursor position
        or at the end of the command line
        """
        if at_end:
            # Insert text at the end of the command line
            self.append_text_to_shell(text, error, prompt)
        else:
            # Insert text at current cursor position
            ConsoleBaseWidget.insert_text(self, text)

    #------ Re-implemented Qt Methods
    def focusNextPrevChild(self, next):
        """
        Reimplemented to stop Tab moving to the next window
        """
        if next:
            return False
        return ConsoleBaseWidget.focusNextPrevChild(self, next)

    #------ Drag and drop
    def dragEnterEvent(self, event):
        """Drag and Drop - Enter event"""
        event.setAccepted(event.mimeData().hasFormat("text/plain"))

    def dragMoveEvent(self, event):
        """Drag and Drop - Move event"""
        if (event.mimeData().hasFormat("text/plain")):
            event.setDropAction(Qt.MoveAction)
            event.accept()
        else:
            event.ignore()

    def dropEvent(self, event):
        """Drag and Drop - Drop event"""
        if (event.mimeData().hasFormat("text/plain")):
            text = to_text_string(event.mimeData().text())
            if self.new_input_line:
                self.on_new_line()
            self.insert_text(text, at_end=True)
            self.setFocus()
            event.setDropAction(Qt.MoveAction)
            event.accept()
        else:
            event.ignore()

    def drop_pathlist(self, pathlist):
        """Drop path list"""
        raise NotImplementedError
Exemplo n.º 18
0
class BaseGenerator(QObject):
    build_progress = Signal(str)
    build_error = Signal(str)
    build_finished = Signal(int)
    build_started = Signal()
    build_update = Signal(str)

    tag = ""
    dockerfile = ""
    build_suffix = ""

    def __init__(self, command, keep_build=False):
        super(BaseGenerator, self).__init__(parent=None)
        self.logger = logging.getLogger(self.__class__.__name__)
        self.__build_process = None
        self.__run_process = None
        self.__finished = False
        self.__cancleded = False
        self.__tmpdir = None
        self.__client = None
        self.__run_command = command
        self.__keep_build = keep_build

    def setFinished(self, value):
        self.__finished = value

    def isFinished(self):
        return self.__finished

    @Slot()
    def start(self):
        self.build_started.emit()
        try:
            self.__client = docker.from_env()
        except Exception as e:
            self.build_error.emit("Failed to create Docker client: {}".format(e))
            self.build_finished.emit(100)
        self.docker_build()

    @Slot()
    def stop(self):
        self.__cancleded = True
        if self.__build_process:
            self.__build_process.terminate()
        if self.__run_process:
            self.__run_process.terminate()

    @Slot()
    def docker_build(self):
        self.build_update.emit("Starting Docker image build.")
        try:
            resp = self.__client.api.build(path="./core/Docker", dockerfile=self.dockerfile,
                                           tag=self.tag, quiet=False, rm=True)
            if isinstance(resp, str):
                return self.docker_run(self.__client.images.get(resp))
            last_event = None
            image_id = None
            result_stream, internal_stream = itertools.tee(json_stream(resp))
            for chunk in internal_stream:
                if 'error' in chunk:
                    self.build_error.emit(chunk['error'])
                    raise BuildError(chunk['error'], result_stream)
                if 'stream' in chunk:
                    self.build_progress.emit(chunk['stream'])
                    match = re.search(
                        r'(^Successfully built |sha256:)([0-9a-f]+)$',
                        chunk['stream']
                    )
                    if match:
                        image_id = match.group(2)
                last_event = chunk
            if image_id:
                return self.docker_run(image_id)
            raise BuildError(last_event or 'Unknown', result_stream)
        except Exception as e:
            self.build_error.emit("An exception occurred while starting Docker image building process: {}".format(e))
            self.build_finished.emit(1)
            return

    @Slot(str)
    def docker_run(self, image_id: str):
        if self.__cancleded:
            self.build_update.emit("Skipping Docker run")
            self.cleanup()
            self.build_finished.emit(0)
            return

        try:
            # create temporary directory and copy client source code
            self.__tmpdir = tempfile.TemporaryDirectory(dir="./build/")
            tmp_dir_path = os.path.abspath(self.__tmpdir.name)
            self.build_update.emit("Created temporary directory: {}".format(tmp_dir_path))

            copy_tree("./client/", self.__tmpdir.name)
            self.build_update.emit("Temporary directory contents: {}".format(os.listdir(tmp_dir_path)))
        except Exception as e:
            self.build_error.emit("Failed to create temporary directory: {}".format(e))
            self.build_finished.emit(1)
            return

        try:
            # create and run container
            self.build_update.emit("Creating Docker container from image: {}".format(image_id))
            container = self.__client.api.create_container(image_id, self.__run_command,
                                                           volumes=['/src'],
                                                           host_config=self.__client.api.create_host_config(binds={
                                                               tmp_dir_path: {
                                                                   'bind': '/src',
                                                                   'mode': 'rw',
                                                               }
                                                           }))

            container = self.__client.containers.get(container['Id'])
            self.build_update.emit("Starting Docker container")
            container.start()
            self.build_update.emit("Started building process")

            # send container logs
            logs = container.attach(stdout=True, stderr=True, stream=True, logs=True)
            for log in logs:
                self.build_progress.emit(str(log, encoding="utf-8"))

            exit_status = container.wait()['StatusCode']
            if exit_status == 0:
                self.build_update.emit("Build process completed successfully")
                if self.__keep_build:
                    # copy all build files
                    output_path = os.path.realpath(os.path.join(DIST_PATH, self.dockerfile))
                    copy_tree(os.path.realpath(tmp_dir_path), output_path)
                else:
                    # copy executable file
                    output_path = os.path.realpath(DIST_PATH)
                    copy_tree(os.path.realpath(os.path.join(tmp_dir_path, "./dist/")), output_path)
                self.build_update.emit("Executable path: {}".format(output_path))
            else:
                self.build_error.emit(str(container.logs(stdout=False, stderr=True), encoding="utf-8"))
        except Exception as e:
            exit_status = 1
            self.build_error.emit("An exception occurred while starting Docker container: {}".format(e))

        self.cleanup()
        self.setFinished(True)
        self.build_update.emit("{} completed with exit code {}".format(self.dockerfile, exit_status))
        self.build_finished.emit(exit_status)

    @Slot()
    def cleanup(self):
        try:
            if self.__tmpdir:
                self.__tmpdir.cleanup()
            self.build_update.emit("Cleanup successfull.")
        except Exception as e:
            self.build_error.emit("Error while cleaning after build: {}".format(e))
Exemplo n.º 19
0
class TerminalWidget(ShellBaseWidget):
    """
    Terminal widget
    """
    COM = 'rem' if os.name == 'nt' else '#'
    INITHISTORY = [
        '%s *** Spyder Terminal History Log ***' % COM,
        COM,
    ]
    SEPARATOR = '%s%s ---(%s)---' % (os.linesep * 2, COM, time.ctime())
    go_to_error = Signal(str)

    def __init__(self, parent, history_filename, profile=False):
        ShellBaseWidget.__init__(self, parent, history_filename, profile)

    #------ Key handlers
    def _key_other(self, text):
        """1 character key"""
        pass

    def _key_backspace(self, cursor_position):
        """Action for Backspace key"""
        if self.has_selected_text():
            self.check_selection()
            self.remove_selected_text()
        elif self.current_prompt_pos == cursor_position:
            # Avoid deleting prompt
            return
        elif self.is_cursor_on_last_line():
            self.stdkey_backspace()

    def _key_tab(self):
        """Action for TAB key"""
        if self.is_cursor_on_last_line():
            self.stdkey_tab()

    def _key_ctrl_space(self):
        """Action for Ctrl+Space"""
        pass

    def _key_escape(self):
        """Action for ESCAPE key"""
        self.clear_line()

    def _key_question(self, text):
        """Action for '?'"""
        self.insert_text(text)

    def _key_parenleft(self, text):
        """Action for '('"""
        self.insert_text(text)

    def _key_period(self, text):
        """Action for '.'"""
        self.insert_text(text)

    #------ Drag'n Drop
    def drop_pathlist(self, pathlist):
        """Drop path list"""
        if pathlist:
            files = ['"%s"' % path for path in pathlist]
            if len(files) == 1:
                text = files[0]
            else:
                text = " ".join(files)
            if self.new_input_line:
                self.on_new_line()
            self.insert_text(text)
            self.setFocus()
Exemplo n.º 20
0
class FindReplace(QWidget):
    """Find widget"""
    STYLE = {False: "background-color:rgb(255, 175, 90);", True: ""}
    visibility_changed = Signal(bool)

    def __init__(self, parent, enable_replace=False):
        QWidget.__init__(self, parent)
        self.enable_replace = enable_replace
        self.editor = None
        self.is_code_editor = None

        glayout = QGridLayout()
        glayout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(glayout)

        self.close_button = create_toolbutton(
            self, triggered=self.hide, icon=ima.icon('DialogCloseButton'))
        glayout.addWidget(self.close_button, 0, 0)

        # Find layout
        self.search_text = PatternComboBox(self,
                                           tip=_("Search string"),
                                           adjust_to_minimum=False)
        self.search_text.valid.connect(lambda state: self.find(
            changed=False, forward=True, rehighlight=False))
        self.search_text.lineEdit().textEdited.connect(
            self.text_has_been_edited)

        self.previous_button = create_toolbutton(self,
                                                 triggered=self.find_previous,
                                                 icon=ima.icon('ArrowUp'))
        self.next_button = create_toolbutton(self,
                                             triggered=self.find_next,
                                             icon=ima.icon('ArrowDown'))
        self.next_button.clicked.connect(self.update_search_combo)
        self.previous_button.clicked.connect(self.update_search_combo)

        self.re_button = create_toolbutton(self,
                                           icon=ima.icon('advanced'),
                                           tip=_("Regular expression"))
        self.re_button.setCheckable(True)
        self.re_button.toggled.connect(lambda state: self.find())

        self.case_button = create_toolbutton(self,
                                             icon=get_icon("upper_lower.png"),
                                             tip=_("Case Sensitive"))
        self.case_button.setCheckable(True)
        self.case_button.toggled.connect(lambda state: self.find())

        self.words_button = create_toolbutton(self,
                                              icon=get_icon("whole_words.png"),
                                              tip=_("Whole words"))
        self.words_button.setCheckable(True)
        self.words_button.toggled.connect(lambda state: self.find())

        self.highlight_button = create_toolbutton(
            self, icon=get_icon("highlight.png"), tip=_("Highlight matches"))
        self.highlight_button.setCheckable(True)
        self.highlight_button.toggled.connect(self.toggle_highlighting)

        hlayout = QHBoxLayout()
        self.widgets = [
            self.close_button, self.search_text, self.previous_button,
            self.next_button, self.re_button, self.case_button,
            self.words_button, self.highlight_button
        ]
        for widget in self.widgets[1:]:
            hlayout.addWidget(widget)
        glayout.addLayout(hlayout, 0, 1)

        # Replace layout
        replace_with = QLabel(_("Replace with:"))
        self.replace_text = PatternComboBox(self,
                                            adjust_to_minimum=False,
                                            tip=_('Replace string'))

        self.replace_button = create_toolbutton(
            self,
            text=_('Replace/find'),
            icon=ima.icon('DialogApplyButton'),
            triggered=self.replace_find,
            text_beside_icon=True)
        self.replace_button.clicked.connect(self.update_replace_combo)
        self.replace_button.clicked.connect(self.update_search_combo)

        self.all_check = QCheckBox(_("Replace all"))

        self.replace_layout = QHBoxLayout()
        widgets = [
            replace_with, self.replace_text, self.replace_button,
            self.all_check
        ]
        for widget in widgets:
            self.replace_layout.addWidget(widget)
        glayout.addLayout(self.replace_layout, 1, 1)
        self.widgets.extend(widgets)
        self.replace_widgets = widgets
        self.hide_replace()

        self.search_text.setTabOrder(self.search_text, self.replace_text)

        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)

        self.shortcuts = self.create_shortcuts(parent)

        self.highlight_timer = QTimer(self)
        self.highlight_timer.setSingleShot(True)
        self.highlight_timer.setInterval(1000)
        self.highlight_timer.timeout.connect(self.highlight_matches)

    def create_shortcuts(self, parent):
        """Create shortcuts for this widget"""
        # Configurable
        findnext = config_shortcut(self.find_next,
                                   context='_',
                                   name='Find next',
                                   parent=parent)
        findprev = config_shortcut(self.find_previous,
                                   context='_',
                                   name='Find previous',
                                   parent=parent)
        togglefind = config_shortcut(self.show,
                                     context='_',
                                     name='Find text',
                                     parent=parent)
        togglereplace = config_shortcut(self.toggle_replace_widgets,
                                        context='_',
                                        name='Replace text',
                                        parent=parent)
        # Fixed
        fixed_shortcut("Escape", self, self.hide)

        return [findnext, findprev, togglefind, togglereplace]

    def get_shortcut_data(self):
        """
        Returns shortcut data, a list of tuples (shortcut, text, default)
        shortcut (QShortcut or QAction instance)
        text (string): action/shortcut description
        default (string): default key sequence
        """
        return [sc.data for sc in self.shortcuts]

    def update_search_combo(self):
        self.search_text.lineEdit().returnPressed.emit()

    def update_replace_combo(self):
        self.replace_text.lineEdit().returnPressed.emit()

    def toggle_replace_widgets(self):
        if self.enable_replace:
            # Toggle replace widgets
            if self.replace_widgets[0].isVisible():
                self.hide_replace()
                self.hide()
            else:
                self.show_replace()
                self.replace_text.setFocus()

    @Slot(bool)
    def toggle_highlighting(self, state):
        """Toggle the 'highlight all results' feature"""
        if self.editor is not None:
            if state:
                self.highlight_matches()
            else:
                self.clear_matches()

    def show(self):
        """Overrides Qt Method"""
        QWidget.show(self)
        self.visibility_changed.emit(True)
        if self.editor is not None:
            text = self.editor.get_selected_text()

            # If no text is highlighted for search, use whatever word is under the cursor
            if not text:
                cursor = self.editor.textCursor()
                cursor.select(QTextCursor.WordUnderCursor)
                text = to_text_string(cursor.selectedText())

            # Now that text value is sorted out, use it for the search
            if text:
                self.search_text.setEditText(text)
                self.search_text.lineEdit().selectAll()
                self.refresh()
            else:
                self.search_text.lineEdit().selectAll()
            self.search_text.setFocus()

    @Slot()
    def hide(self):
        """Overrides Qt Method"""
        for widget in self.replace_widgets:
            widget.hide()
        QWidget.hide(self)
        self.visibility_changed.emit(False)
        if self.editor is not None:
            self.editor.setFocus()
            self.clear_matches()

    def show_replace(self):
        """Show replace widgets"""
        self.show()
        for widget in self.replace_widgets:
            widget.show()

    def hide_replace(self):
        """Hide replace widgets"""
        for widget in self.replace_widgets:
            widget.hide()

    def refresh(self):
        """Refresh widget"""
        if self.isHidden():
            if self.editor is not None:
                self.clear_matches()
            return
        state = self.editor is not None
        for widget in self.widgets:
            widget.setEnabled(state)
        if state:
            self.find()

    def set_editor(self, editor, refresh=True):
        """
        Set associated editor/web page:
            codeeditor.base.TextEditBaseWidget
            browser.WebView
        """
        self.editor = editor
        # Note: This is necessary to test widgets/editor.py
        # in Qt builds that don't have web widgets
        try:
            from qtpy.QtWebEngineWidgets import QWebEngineView
        except ImportError:
            QWebEngineView = type(None)
        self.words_button.setVisible(not isinstance(editor, QWebEngineView))
        self.re_button.setVisible(not isinstance(editor, QWebEngineView))
        from spyderlib.widgets.sourcecode.codeeditor import CodeEditor
        self.is_code_editor = isinstance(editor, CodeEditor)
        self.highlight_button.setVisible(self.is_code_editor)
        if refresh:
            self.refresh()
        if self.isHidden() and editor is not None:
            self.clear_matches()

    @Slot()
    def find_next(self):
        """Find next occurrence"""
        state = self.find(changed=False, forward=True, rehighlight=False)
        self.editor.setFocus()
        self.search_text.add_current_text()
        return state

    @Slot()
    def find_previous(self):
        """Find previous occurrence"""
        state = self.find(changed=False, forward=False, rehighlight=False)
        self.editor.setFocus()
        return state

    def text_has_been_edited(self, text):
        """Find text has been edited (this slot won't be triggered when 
        setting the search pattern combo box text programmatically"""
        self.find(changed=True, forward=True, start_highlight_timer=True)

    def highlight_matches(self):
        """Highlight found results"""
        if self.is_code_editor and self.highlight_button.isChecked():
            text = self.search_text.currentText()
            words = self.words_button.isChecked()
            regexp = self.re_button.isChecked()
            self.editor.highlight_found_results(text,
                                                words=words,
                                                regexp=regexp)

    def clear_matches(self):
        """Clear all highlighted matches"""
        if self.is_code_editor:
            self.editor.clear_found_results()

    def find(self,
             changed=True,
             forward=True,
             rehighlight=True,
             start_highlight_timer=False):
        """Call the find function"""
        text = self.search_text.currentText()
        if len(text) == 0:
            self.search_text.lineEdit().setStyleSheet("")
            return None
        else:
            case = self.case_button.isChecked()
            words = self.words_button.isChecked()
            regexp = self.re_button.isChecked()
            found = self.editor.find_text(text,
                                          changed,
                                          forward,
                                          case=case,
                                          words=words,
                                          regexp=regexp)
            self.search_text.lineEdit().setStyleSheet(self.STYLE[found])
            if self.is_code_editor and found:
                if rehighlight or not self.editor.found_results:
                    self.highlight_timer.stop()
                    if start_highlight_timer:
                        self.highlight_timer.start()
                    else:
                        self.highlight_matches()
            else:
                self.clear_matches()
            return found
Exemplo n.º 21
0
class ClientWidget(QWidget, SaveHistoryMixin):
    """
    Client widget for the IPython Console

    This is a widget composed of a shell widget and a WebView info widget
    to print different messages there.
    """

    SEPARATOR = '%s##---(%s)---' % (os.linesep * 2, time.ctime())
    append_to_history = Signal(str, str)

    def __init__(self,
                 plugin,
                 name,
                 history_filename,
                 config_options,
                 additional_options,
                 interpreter_versions,
                 connection_file=None,
                 hostname=None,
                 menu_actions=None,
                 slave=False,
                 external_kernel=False):
        super(ClientWidget, self).__init__(plugin)
        SaveHistoryMixin.__init__(self)

        # --- Init attrs
        self.name = name
        self.history_filename = get_conf_path(history_filename)
        self.connection_file = connection_file
        self.hostname = hostname
        self.menu_actions = menu_actions
        self.slave = slave

        # --- Other attrs
        self.options_button = None
        self.stop_button = None
        self.stop_icon = ima.icon('stop')
        self.history = []

        # --- Widgets
        self.shellwidget = ShellWidget(
            config=config_options,
            ipyclient=self,
            additional_options=additional_options,
            interpreter_versions=interpreter_versions,
            external_kernel=external_kernel,
            local_kernel=True)
        self.infowidget = WebView(self)
        self.set_infowidget_font()
        self.loading_page = self._create_loading_page()
        self._show_loading_page()

        # --- Layout
        vlayout = QVBoxLayout()
        toolbar_buttons = self.get_toolbar_buttons()
        hlayout = QHBoxLayout()
        for button in toolbar_buttons:
            hlayout.addWidget(button)
        vlayout.addLayout(hlayout)
        vlayout.setContentsMargins(0, 0, 0, 0)
        vlayout.addWidget(self.shellwidget)
        vlayout.addWidget(self.infowidget)
        self.setLayout(vlayout)

        # --- Exit function
        self.exit_callback = lambda: plugin.close_client(client=self)

        # --- Signals
        # As soon as some content is printed in the console, stop
        # our loading animation
        document = self.get_control().document()
        document.contentsChange.connect(self._hide_loading_page)

    #------ Public API --------------------------------------------------------
    @property
    def stderr_file(self):
        """Filename to save kernel stderr output."""
        json_file = osp.basename(self.connection_file)
        stderr_file = json_file.split('json')[0] + 'stderr'
        stderr_file = osp.join(TEMPDIR, stderr_file)
        return stderr_file

    def configure_shellwidget(self, give_focus=True):
        """Configure shellwidget after kernel is started"""
        if give_focus:
            self.get_control().setFocus()

        # Set exit callback
        self.shellwidget.set_exit_callback()

        # To save history
        self.shellwidget.executing.connect(self.add_to_history)

        # For Mayavi to run correctly
        self.shellwidget.executing.connect(
            self.shellwidget.set_backend_for_mayavi)

        # To update history after execution
        self.shellwidget.executed.connect(self.update_history)

        # To update the Variable Explorer after execution
        self.shellwidget.executed.connect(
            self.shellwidget.refresh_namespacebrowser)

        # To enable the stop button when executing a process
        self.shellwidget.executing.connect(self.enable_stop_button)

        # To disable the stop button after execution stopped
        self.shellwidget.executed.connect(self.disable_stop_button)

        # To show kernel restarted/died messages
        self.shellwidget.sig_kernel_restarted.connect(
            self.kernel_restarted_message)

        # To restart the kernel when errors happened while debugging
        # See issue 4003
        self.shellwidget.sig_dbg_kernel_restart.connect(self.restart_kernel)

        # To correctly change Matplotlib backend interactively
        self.shellwidget.executing.connect(self.shellwidget.change_mpl_backend)

    def enable_stop_button(self):
        self.stop_button.setEnabled(True)

    def disable_stop_button(self):
        self.stop_button.setDisabled(True)

    @Slot()
    def stop_button_click_handler(self):
        """Method to handle what to do when the stop button is pressed"""
        self.stop_button.setDisabled(True)
        # Interrupt computations or stop debugging
        if not self.shellwidget._reading:
            self.interrupt_kernel()
        else:
            self.shellwidget.write_to_stdin('exit')

    def show_kernel_error(self, error):
        """Show kernel initialization errors in infowidget."""
        # Replace end of line chars with <br>
        eol = sourcecode.get_eol_chars(error)
        if eol:
            error = error.replace(eol, '<br>')

        # Don't break lines in hyphens
        # From http://stackoverflow.com/q/7691569/438386
        error = error.replace('-', '&#8209')

        # Create error page
        message = _("An error ocurred while starting the kernel")
        kernel_error_template = Template(KERNEL_ERROR)
        page = kernel_error_template.substitute(css_path=CSS_PATH,
                                                message=message,
                                                error=error)

        # Show error
        self.infowidget.setHtml(page)
        self.shellwidget.hide()
        self.infowidget.show()

    def get_name(self):
        """Return client name"""
        return ((_("Console") if self.hostname is None else self.hostname) +
                " " + self.name)

    def get_control(self):
        """Return the text widget (or similar) to give focus to"""
        # page_control is the widget used for paging
        page_control = self.shellwidget._page_control
        if page_control and page_control.isVisible():
            return page_control
        else:
            return self.shellwidget._control

    def get_kernel(self):
        """Get kernel associated with this client"""
        return self.shellwidget.kernel_manager

    def get_options_menu(self):
        """Return options menu"""
        return self.menu_actions

    def get_toolbar_buttons(self):
        """Return toolbar buttons list."""
        buttons = []
        # Code to add the stop button
        if self.stop_button is None:
            self.stop_button = create_toolbutton(
                self,
                text=_("Stop"),
                icon=self.stop_icon,
                tip=_("Stop the current command"))
            self.disable_stop_button()
            # set click event handler
            self.stop_button.clicked.connect(self.stop_button_click_handler)
        if self.stop_button is not None:
            buttons.append(self.stop_button)

        if self.options_button is None:
            options = self.get_options_menu()
            if options:
                self.options_button = create_toolbutton(
                    self, text=_('Options'), icon=ima.icon('tooloptions'))
                self.options_button.setPopupMode(QToolButton.InstantPopup)
                menu = QMenu(self)
                add_actions(menu, options)
                self.options_button.setMenu(menu)
        if self.options_button is not None:
            buttons.append(self.options_button)

        return buttons

    def add_actions_to_context_menu(self, menu):
        """Add actions to IPython widget context menu"""
        inspect_action = create_action(
            self,
            _("Inspect current object"),
            QKeySequence(get_shortcut('console', 'inspect current object')),
            icon=ima.icon('MessageBoxInformation'),
            triggered=self.inspect_object)
        clear_line_action = create_action(self,
                                          _("Clear line or block"),
                                          QKeySequence("Shift+Escape"),
                                          icon=ima.icon('editdelete'),
                                          triggered=self.clear_line)
        reset_namespace_action = create_action(self,
                                               _("Reset namespace"),
                                               QKeySequence("Ctrl+Alt+R"),
                                               triggered=self.reset_namespace)
        clear_console_action = create_action(
            self,
            _("Clear console"),
            QKeySequence(get_shortcut('console', 'clear shell')),
            icon=ima.icon('editclear'),
            triggered=self.clear_console)
        quit_action = create_action(self,
                                    _("&Quit"),
                                    icon=ima.icon('exit'),
                                    triggered=self.exit_callback)
        add_actions(
            menu,
            (None, inspect_action, clear_line_action, clear_console_action,
             reset_namespace_action, None, quit_action))
        return menu

    def set_font(self, font):
        """Set IPython widget's font"""
        self.shellwidget._control.setFont(font)
        self.shellwidget.font = font

    def set_infowidget_font(self):
        """Set font for infowidget"""
        font = get_font(option='rich_font')
        self.infowidget.set_font(font)

    def shutdown(self):
        """Shutdown kernel"""
        if self.get_kernel() is not None and not self.slave:
            self.shellwidget.kernel_manager.shutdown_kernel()
        if self.shellwidget.kernel_client is not None:
            background(self.shellwidget.kernel_client.stop_channels)

    def interrupt_kernel(self):
        """Interrupt the associanted TRex kernel if it's running"""
        self.shellwidget.request_interrupt_kernel()

    @Slot()
    def restart_kernel(self):
        """
        Restart the associanted kernel

        Took this code from the qtconsole project
        Licensed under the BSD license
        """
        sw = self.shellwidget

        # This is needed to restart the kernel without a prompt
        # when an error in stdout corrupts the debugging process.
        # See issue 4003
        if not sw._input_reply_failed:
            message = _('Are you sure you want to restart the kernel?')
            buttons = QMessageBox.Yes | QMessageBox.No
            result = QMessageBox.question(self, _('Restart kernel?'), message,
                                          buttons)
        else:
            result = None

        if result == QMessageBox.Yes or sw._input_reply_failed:
            if sw.kernel_manager:
                if self.infowidget.isVisible():
                    self.infowidget.hide()
                    sw.show()
                try:
                    sw.kernel_manager.restart_kernel()
                except RuntimeError as e:
                    sw._append_plain_text(_('Error restarting kernel: %s\n') %
                                          e,
                                          before_prompt=True)
                else:
                    sw.reset(clear=not sw._input_reply_failed)
                    if sw._input_reply_failed:
                        sw._append_html(_("<br>Restarting kernel because "
                                          "an error occurred while "
                                          "debugging\n<hr><br>"),
                                        before_prompt=False)
                        sw._input_reply_failed = False
                    else:
                        sw._append_html(
                            _("<br>Restarting kernel...\n<hr><br>"),
                            before_prompt=False)
            else:
                sw._append_plain_text(
                    _('Cannot restart a kernel not started by TRex\n'),
                    before_prompt=True)

    @Slot(str)
    def kernel_restarted_message(self, msg):
        """Show kernel restarted/died messages."""
        try:
            stderr = codecs.open(self.stderr_file, 'r',
                                 encoding='utf-8').read()
        except UnicodeDecodeError:
            # This is needed since the stderr file could be encoded
            # in something different to utf-8.
            # See issue 4191
            try:
                stderr = self._read_stderr()
            except:
                stderr = None

        if stderr:
            self.show_kernel_error('<tt>%s</tt>' % stderr)
        else:
            self.shellwidget._append_html("<br>%s<hr><br>" % msg,
                                          before_prompt=False)

    @Slot()
    def inspect_object(self):
        """Show how to inspect an object with our Help plugin"""
        self.shellwidget._control.inspect_current_object()

    @Slot()
    def clear_line(self):
        """Clear a console line"""
        self.shellwidget._keyboard_quit()

    @Slot()
    def clear_console(self):
        """Clear the whole console"""
        self.shellwidget.clear_console()

    @Slot()
    def reset_namespace(self):
        """Resets the namespace by removing all names defined by the user"""
        self.shellwidget.reset_namespace()

    def update_history(self):
        self.history = self.shellwidget._history

    #------ Private API -------------------------------------------------------
    def _create_loading_page(self):
        """Create html page to show while the kernel is starting"""
        loading_template = Template(LOADING)
        loading_img = get_image_path('loading_sprites.png')
        if os.name == 'nt':
            loading_img = loading_img.replace('\\', '/')
        message = _("Connecting to kernel...")
        page = loading_template.substitute(css_path=CSS_PATH,
                                           loading_img=loading_img,
                                           message=message)
        return page

    def _show_loading_page(self):
        """Show animation while the kernel is loading."""
        self.shellwidget.hide()
        self.infowidget.show()
        self.infowidget.setHtml(self.loading_page,
                                QUrl.fromLocalFile(CSS_PATH))

    def _hide_loading_page(self):
        """Hide animation shown while the kernel is loading."""
        self.infowidget.hide()
        self.shellwidget.show()
        self.infowidget.setHtml(BLANK)

        document = self.get_control().document()
        document.contentsChange.disconnect(self._hide_loading_page)

    def _read_stderr(self):
        """Read the stderr file of the kernel."""
        stderr_text = open(self.stderr_file, 'rb').read()
        encoding = get_coding(stderr_text)
        stderr = to_text_string(stderr_text, encoding)
        return stderr
Exemplo n.º 22
0
class BasePlot(QwtPlot):
    """
    An enhanced QwtPlot class that provides
    methods for handling plotitems and axes better
    
    It distinguishes activatable items from basic QwtPlotItems.
    
    Activatable items must support IBasePlotItem interface and should
    be added to the plot using add_item methods.
    """

    Y_LEFT, Y_RIGHT, X_BOTTOM, X_TOP = (
        QwtPlot.yLeft,
        QwtPlot.yRight,
        QwtPlot.xBottom,
        QwtPlot.xTop,
    )
    #    # To be replaced by (in the near future):
    #    Y_LEFT, Y_RIGHT, X_BOTTOM, X_TOP = range(4)
    AXIS_IDS = (Y_LEFT, Y_RIGHT, X_BOTTOM, X_TOP)
    AXIS_NAMES = {
        "left": Y_LEFT,
        "right": Y_RIGHT,
        "bottom": X_BOTTOM,
        "top": X_TOP
    }
    AXIS_TYPES = {"lin": QwtLinearScaleEngine, "log": QwtLogScaleEngine}
    AXIS_CONF_OPTIONS = ("axis", "axis", "axis", "axis")
    DEFAULT_ACTIVE_XAXIS = X_BOTTOM
    DEFAULT_ACTIVE_YAXIS = Y_LEFT

    #: Signal emitted by plot when an IBasePlotItem object was moved (args: x0, y0, x1, y1)
    SIG_ITEM_MOVED = Signal(object, float, float, float, float)

    #: Signal emitted by plot when a shapes.Marker position changes
    SIG_MARKER_CHANGED = Signal(object)

    #: Signal emitted by plot when a shapes.Axes position (or the angle) changes
    SIG_AXES_CHANGED = Signal(object)

    #: Signal emitted by plot when an annotation.AnnotatedShape position changes
    SIG_ANNOTATION_CHANGED = Signal(object)

    #: Signal emitted by plot when the a shapes.XRangeSelection range changes
    SIG_RANGE_CHANGED = Signal(object, float, float)

    #: Signal emitted by plot when item list has changed (item removed, added, ...)
    SIG_ITEMS_CHANGED = Signal(object)

    #: Signal emitted by plot when selected item has changed
    SIG_ACTIVE_ITEM_CHANGED = Signal(object)

    #: Signal emitted by plot when an item was deleted from the item list or using the
    #: delete item tool
    SIG_ITEM_REMOVED = Signal(object)

    #: Signal emitted by plot when an item is selected
    SIG_ITEM_SELECTION_CHANGED = Signal(object)

    #: Signal emitted by plot when plot's title or any axis label has changed
    SIG_PLOT_LABELS_CHANGED = Signal(object)

    #: Signal emitted by plot when any plot axis direction has changed
    SIG_AXIS_DIRECTION_CHANGED = Signal(object, object)

    #: Signal emitted by plot when LUT has been changed by the user
    SIG_LUT_CHANGED = Signal(object)

    #: Signal emitted by plot when image mask has changed
    SIG_MASK_CHANGED = Signal(object)

    #: Signal emitted by cross section plot when cross section curve data has changed
    SIG_CS_CURVE_CHANGED = Signal(object)

    def __init__(self, parent=None, section="plot"):
        super(BasePlot, self).__init__(parent)
        self._start_autoscaled = True
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.manager = None
        self.plot_id = None  # id assigned by it's manager
        self.filter = StatefulEventFilter(self)
        self.items = []
        self.active_item = None
        self.last_selected = {
        }  # a mapping from item type to last selected item
        self.axes_styles = [
            AxeStyleParam(_("Left")),
            AxeStyleParam(_("Right")),
            AxeStyleParam(_("Bottom")),
            AxeStyleParam(_("Top")),
        ]
        self._active_xaxis = self.DEFAULT_ACTIVE_XAXIS
        self._active_yaxis = self.DEFAULT_ACTIVE_YAXIS
        self.read_axes_styles(section, self.AXIS_CONF_OPTIONS)
        self.font_title = get_font(CONF, section, "title")
        canvas = self.canvas()
        canvas.setFocusPolicy(Qt.StrongFocus)
        canvas.setFocusIndicator(QwtPlotCanvas.ItemFocusIndicator)
        self.SIG_ITEM_MOVED.connect(self._move_selected_items_together)
        self.legendDataChanged.connect(
            lambda item, _legdata: item.update_item_parameters())

    # ---- QWidget API ---------------------------------------------------------
    def mouseDoubleClickEvent(self, event):
        """Reimplement QWidget method"""
        for axis_id in self.AXIS_IDS:
            widget = self.axisWidget(axis_id)
            if widget.geometry().contains(event.pos()):
                self.edit_axis_parameters(axis_id)
                break
        else:
            QwtPlot.mouseDoubleClickEvent(self, event)

    # ---- QwtPlot API ---------------------------------------------------------
    def showEvent(self, event):
        """Reimplement Qwt method"""
        QwtPlot.showEvent(self, event)
        if self._start_autoscaled:
            self.do_autoscale()

    # ---- Public API ----------------------------------------------------------
    def _move_selected_items_together(self, item, x0, y0, x1, y1):
        """Selected items move together"""
        for selitem in self.get_selected_items():
            if selitem is not item and selitem.can_move():
                selitem.move_with_selection(x1 - x0, y1 - y0)

    def set_manager(self, manager, plot_id):
        """Set the associated :py:class:`guiqwt.plot.PlotManager` instance"""
        self.manager = manager
        self.plot_id = plot_id

    def sizeHint(self):
        """Preferred size"""
        return QSize(400, 300)

    def get_title(self):
        """Get plot title"""
        return to_text_string(self.title().text())

    def set_title(self, title):
        """Set plot title"""
        text = QwtText(title)
        text.setFont(self.font_title)
        self.setTitle(text)
        self.SIG_PLOT_LABELS_CHANGED.emit(self)

    def get_axis_id(self, axis_name):
        """Return axis ID from axis name
        If axis ID is passed directly, check the ID"""
        assert axis_name in self.AXIS_NAMES or axis_name in self.AXIS_IDS
        return self.AXIS_NAMES.get(axis_name, axis_name)

    def read_axes_styles(self, section, options):
        """
        Read axes styles from section and options (one option
        for each axis in the order left, right, bottom, top)
        
        Skip axis if option is None
        """
        for prm, option in zip(self.axes_styles, options):
            if option is None:
                continue
            prm.read_config(CONF, section, option)
        self.update_all_axes_styles()

    def get_axis_title(self, axis_id):
        """Get axis title"""
        axis_id = self.get_axis_id(axis_id)
        return self.axes_styles[axis_id].title

    def set_axis_title(self, axis_id, text):
        """Set axis title"""
        axis_id = self.get_axis_id(axis_id)
        self.axes_styles[axis_id].title = text
        self.update_axis_style(axis_id)

    def get_axis_unit(self, axis_id):
        """Get axis unit"""
        axis_id = self.get_axis_id(axis_id)
        return self.axes_styles[axis_id].unit

    def set_axis_unit(self, axis_id, text):
        """Set axis unit"""
        axis_id = self.get_axis_id(axis_id)
        self.axes_styles[axis_id].unit = text
        self.update_axis_style(axis_id)

    def get_axis_font(self, axis_id):
        """Get axis font"""
        axis_id = self.get_axis_id(axis_id)
        return self.axes_styles[axis_id].title_font.build_font()

    def set_axis_font(self, axis_id, font):
        """Set axis font"""
        axis_id = self.get_axis_id(axis_id)
        self.axes_styles[axis_id].title_font.update_param(font)
        self.axes_styles[axis_id].ticks_font.update_param(font)
        self.update_axis_style(axis_id)

    def get_axis_color(self, axis_id):
        """Get axis color (color name, i.e. string)"""
        axis_id = self.get_axis_id(axis_id)
        return self.axes_styles[axis_id].color

    def set_axis_color(self, axis_id, color):
        """
        Set axis color
        color: color name (string) or QColor instance
        """
        axis_id = self.get_axis_id(axis_id)
        if is_text_string(color):
            color = QColor(color)
        self.axes_styles[axis_id].color = str(color.name())
        self.update_axis_style(axis_id)

    def update_axis_style(self, axis_id):
        """Update axis style"""
        axis_id = self.get_axis_id(axis_id)
        style = self.axes_styles[axis_id]

        title_font = style.title_font.build_font()
        ticks_font = style.ticks_font.build_font()
        self.setAxisFont(axis_id, ticks_font)

        if style.title and style.unit:
            title = "%s (%s)" % (style.title, style.unit)
        elif style.title:
            title = style.title
        else:
            title = style.unit
        axis_text = self.axisTitle(axis_id)
        axis_text.setFont(title_font)
        axis_text.setText(title)
        axis_text.setColor(QColor(style.color))
        self.setAxisTitle(axis_id, axis_text)
        self.SIG_PLOT_LABELS_CHANGED.emit(self)

    def update_all_axes_styles(self):
        """Update all axes styles"""
        for axis_id in self.AXIS_IDS:
            self.update_axis_style(axis_id)

    def get_axis_limits(self, axis_id):
        """Return axis limits (minimum and maximum values)"""
        axis_id = self.get_axis_id(axis_id)
        sdiv = self.axisScaleDiv(axis_id)
        return sdiv.lowerBound(), sdiv.upperBound()

    def set_axis_limits(self, axis_id, vmin, vmax, stepsize=0):
        """Set axis limits (minimum and maximum values) and optional
        step size"""
        axis_id = self.get_axis_id(axis_id)
        self.setAxisScale(axis_id, vmin, vmax, stepsize)
        self._start_autoscaled = False

    def set_axis_ticks(self, axis_id, nmajor=None, nminor=None):
        """Set axis maximum number of major ticks
        and maximum of minor ticks"""
        axis_id = self.get_axis_id(axis_id)
        if nmajor is not None:
            self.setAxisMaxMajor(axis_id, nmajor)
        if nminor is not None:
            self.setAxisMaxMinor(axis_id, nminor)

    def get_axis_scale(self, axis_id):
        """Return the name ('lin' or 'log') of the scale used by axis"""
        axis_id = self.get_axis_id(axis_id)
        engine = self.axisScaleEngine(axis_id)
        for axis_label, axis_type in list(self.AXIS_TYPES.items()):
            if isinstance(engine, axis_type):
                return axis_label
        return "lin"  # unknown default to linear

    def set_axis_scale(self, axis_id, scale, autoscale=True):
        """Set axis scale
        Example: self.set_axis_scale(curve.yAxis(), 'lin')"""
        axis_id = self.get_axis_id(axis_id)
        self.setAxisScaleEngine(axis_id, self.AXIS_TYPES[scale]())
        if autoscale:
            self.do_autoscale(replot=False)

    def get_scales(self):
        """Return active curve scales"""
        ax, ay = self.get_active_axes()
        return self.get_axis_scale(ax), self.get_axis_scale(ay)

    def set_scales(self, xscale, yscale):
        """Set active curve scales
        Example: self.set_scales('lin', 'lin')"""
        ax, ay = self.get_active_axes()
        self.set_axis_scale(ax, xscale)
        self.set_axis_scale(ay, yscale)
        self.replot()

    def enable_used_axes(self):
        """
        Enable only used axes
        For now, this is needed only by the pyplot interface
        """
        for axis in self.AXIS_IDS:
            self.enableAxis(axis, True)
        self.disable_unused_axes()

    def disable_unused_axes(self):
        """Disable unused axes"""
        used_axes = set()
        for item in self.get_items():
            used_axes.add(item.xAxis())
            used_axes.add(item.yAxis())
        unused_axes = set(self.AXIS_IDS) - set(used_axes)
        for axis in unused_axes:
            self.enableAxis(axis, False)

    def get_items(self, z_sorted=False, item_type=None):
        """Return widget's item list
        (items are based on IBasePlotItem's interface)"""
        if z_sorted:
            items = sorted(self.items, reverse=True, key=lambda x: x.z())
        else:
            items = self.items
        if item_type is None:
            return items
        else:
            assert issubclass(item_type, IItemType)
            return [item for item in items if item_type in item.types()]

    def get_public_items(self, z_sorted=False, item_type=None):
        """Return widget's public item list
        (items are based on IBasePlotItem's interface)"""
        return [
            item
            for item in self.get_items(z_sorted=z_sorted, item_type=item_type)
            if not item.is_private()
        ]

    def get_private_items(self, z_sorted=False, item_type=None):
        """Return widget's private item list
        (items are based on IBasePlotItem's interface)"""
        return [
            item
            for item in self.get_items(z_sorted=z_sorted, item_type=item_type)
            if item.is_private()
        ]

    def copy_to_clipboard(self):
        """Copy widget's window to clipboard"""
        clipboard = QApplication.clipboard()
        if PYQT5:
            pixmap = self.grab()
        else:
            pixmap = QPixmap.grabWidget(self)
        clipboard.setPixmap(pixmap)

    def save_widget(self, fname):
        """Grab widget's window and save it to filename (\*.png, \*.pdf)"""
        fname = to_text_string(fname)
        if fname.lower().endswith(".pdf"):
            printer = QPrinter()
            printer.setOutputFormat(QPrinter.PdfFormat)
            printer.setOrientation(QPrinter.Landscape)
            printer.setOutputFileName(fname)
            printer.setCreator("guidata")
            self.print_(printer)
        elif fname.lower().endswith(".png"):
            if PYQT5:
                pixmap = self.grab()
            else:
                pixmap = QPixmap.grabWidget(self)
            pixmap.save(fname, "PNG")
        else:
            raise RuntimeError(_("Unknown file extension"))

    def get_selected_items(self, z_sorted=False, item_type=None):
        """Return selected items"""
        return [
            item
            for item in self.get_items(item_type=item_type, z_sorted=z_sorted)
            if item.selected
        ]

    def get_max_z(self):
        """
        Return maximum z-order for all items registered in plot
        If there is no item, return 0
        """
        if self.items:
            return max([_it.z() for _it in self.items])
        else:
            return 0

    def add_item(self, item, z=None):
        """
        Add a *plot item* instance to this *plot widget*
        
        item: :py:data:`qwt.QwtPlotItem` object implementing
              the IBasePlotItem interface (guiqwt.interfaces)
        """
        assert hasattr(item, "__implements__")
        assert IBasePlotItem in item.__implements__
        item.attach(self)
        if z is not None:
            item.setZ(z)
        else:
            item.setZ(self.get_max_z() + 1)
        if item in self.items:
            print("Warning: item %r is already attached to plot" % item,
                  file=sys.stderr)
        else:
            self.items.append(item)
        self.SIG_ITEMS_CHANGED.emit(self)

    def add_item_with_z_offset(self, item, zoffset):
        """
        Add a plot *item* instance within a specified z range, over *zmin*
        """
        zlist = sorted([_it.z() for _it in self.items if _it.z() >= zoffset] +
                       [zoffset - 1])
        dzlist = np.argwhere(np.diff(zlist) > 1)
        if len(dzlist) == 0:
            z = max(zlist) + 1
        else:
            z = zlist[dzlist[0]] + 1
        self.add_item(item, z=z)

    def __clean_item_references(self, item):
        """Remove all reference to this item (active,
        last_selected"""
        if item is self.active_item:
            self.active_item = None
            self._active_xaxis = self.DEFAULT_ACTIVE_XAXIS
            self._active_yaxis = self.DEFAULT_ACTIVE_YAXIS
        for key, it in list(self.last_selected.items()):
            if item is it:
                del self.last_selected[key]

    def del_items(self, items):
        """Remove item from widget"""
        items = items[:]  # copy the list to avoid side effects when we empty it
        active_item = self.get_active_item()
        while items:
            item = items.pop()
            item.detach()
            # raises ValueError if item not in list
            self.items.remove(item)
            self.__clean_item_references(item)
            self.SIG_ITEM_REMOVED.emit(item)
        self.SIG_ITEMS_CHANGED.emit(self)
        if active_item is not self.get_active_item():
            self.SIG_ACTIVE_ITEM_CHANGED.emit(self)

    def del_item(self, item):
        """
        Remove item from widget
        Convenience function (see 'del_items')
        """
        try:
            self.del_items([item])
        except ValueError:
            raise ValueError("item not in plot")

    def set_item_visible(self, item, state, notify=True, replot=True):
        """Show/hide *item* and emit a SIG_ITEMS_CHANGED signal"""
        item.setVisible(state)
        if item is self.active_item and not state:
            self.set_active_item(None)  # Notify the item list (see baseplot)
        if notify:
            self.SIG_ITEMS_CHANGED.emit(self)
        if replot:
            self.replot()

    def __set_items_visible(self, state, items=None, item_type=None):
        """Show/hide items (if *items* is None, show/hide all items)"""
        if items is None:
            items = self.get_items(item_type=item_type)
        for item in items:
            self.set_item_visible(item, state, notify=False, replot=False)
        self.SIG_ITEMS_CHANGED.emit(self)
        self.replot()

    def show_items(self, items=None, item_type=None):
        """Show items (if *items* is None, show all items)"""
        self.__set_items_visible(True, items, item_type=item_type)

    def hide_items(self, items=None, item_type=None):
        """Hide items (if *items* is None, hide all items)"""
        self.__set_items_visible(False, items, item_type=item_type)

    def save_items(self, iofile, selected=False):
        """
        Save (serializable) items to file using the :py:mod:`pickle` protocol
            * iofile: file object or filename
            * selected=False: if True, will save only selected items
            
        See also :py:meth:`guiqwt.baseplot.BasePlot.restore_items`
        """
        if selected:
            items = self.get_selected_items()
        else:
            items = self.items[:]
        items = [item for item in items if ISerializableType in item.types()]
        import pickle

        pickle.dump(items, iofile)

    def restore_items(self, iofile):
        """
        Restore items from file using the :py:mod:`pickle` protocol
            * iofile: file object or filename
            
        See also :py:meth:`guiqwt.baseplot.BasePlot.save_items`
        """
        import pickle

        items = pickle.load(iofile)
        for item in items:
            self.add_item(item)

    def serialize(self, writer, selected=False):
        """
        Save (serializable) items to HDF5 file:
            * writer: :py:class:`guidata.hdf5io.HDF5Writer` object
            * selected=False: if True, will save only selected items
            
        See also :py:meth:`guiqwt.baseplot.BasePlot.restore_items_from_hdf5`
        """
        if selected:
            items = self.get_selected_items()
        else:
            items = self.items[:]
        items = [item for item in items if ISerializableType in item.types()]
        io.save_items(writer, items)

    def deserialize(self, reader):
        """
        Restore items from HDF5 file:
            * reader: :py:class:`guidata.hdf5io.HDF5Reader` object
            
        See also :py:meth:`guiqwt.baseplot.BasePlot.save_items_to_hdf5`
        """
        for item in io.load_items(reader):
            self.add_item(item)

    def set_items(self, *args):
        """Utility function used to quickly setup a plot
        with a set of items"""
        self.del_all_items()
        for item in args:
            self.add_item(item)

    def del_all_items(self):
        """Remove (detach) all attached items"""
        self.del_items(self.items)

    def __swap_items_z(self, item1, item2):
        old_item1_z, old_item2_z = item1.z(), item2.z()
        item1.setZ(max([_it.z() for _it in self.items]) + 1)
        item2.setZ(old_item1_z)
        item1.setZ(old_item2_z)

    def move_up(self, item_list):
        """Move item(s) up, i.e. to the foreground
        (swap item with the next item in z-order)
        
        item: plot item *or* list of plot items
        
        Return True if items have been moved effectively"""
        objects = self.get_items(z_sorted=True)
        items = sorted(list(item_list),
                       reverse=True,
                       key=lambda x: objects.index(x))
        changed = False
        if objects.index(items[-1]) > 0:
            for item in items:
                index = objects.index(item)
                self.__swap_items_z(item, objects[index - 1])
                changed = True
        if changed:
            self.SIG_ITEMS_CHANGED.emit(self)
        return changed

    def move_down(self, item_list):
        """Move item(s) down, i.e. to the background
        (swap item with the previous item in z-order)
        
        item: plot item *or* list of plot items
        
        Return True if items have been moved effectively"""
        objects = self.get_items(z_sorted=True)
        items = sorted(list(item_list),
                       reverse=False,
                       key=lambda x: objects.index(x))
        changed = False
        if objects.index(items[-1]) < len(objects) - 1:
            for item in items:
                index = objects.index(item)
                self.__swap_items_z(item, objects[index + 1])
                changed = True
        if changed:
            self.SIG_ITEMS_CHANGED.emit(self)
        return changed

    def set_items_readonly(self, state):
        """Set all items readonly state to *state*
        Default item's readonly state: False (items may be deleted)"""
        for item in self.get_items():
            item.set_readonly(state)
        self.SIG_ITEMS_CHANGED.emit(self)

    def select_item(self, item):
        """Select item"""
        item.select()
        for itype in item.types():
            self.last_selected[itype] = item
        self.SIG_ITEM_SELECTION_CHANGED.emit(self)

    def unselect_item(self, item):
        """Unselect item"""
        item.unselect()
        self.SIG_ITEM_SELECTION_CHANGED.emit(self)

    def get_last_active_item(self, item_type):
        """Return last active item corresponding to passed `item_type`"""
        assert issubclass(item_type, IItemType)
        return self.last_selected.get(item_type)

    def select_all(self):
        """Select all selectable items"""
        last_item = None
        block = self.blockSignals(True)
        for item in self.items:
            if item.can_select():
                self.select_item(item)
                last_item = item
        self.blockSignals(block)
        self.SIG_ITEM_SELECTION_CHANGED.emit(self)
        self.set_active_item(last_item)

    def unselect_all(self):
        """Unselect all selected items"""
        for item in self.items:
            if item.can_select():
                item.unselect()
        self.set_active_item(None)
        self.SIG_ITEM_SELECTION_CHANGED.emit(self)

    def select_some_items(self, items):
        """Select items"""
        active = self.active_item
        block = self.blockSignals(True)
        self.unselect_all()
        if items:
            new_active_item = items[-1]
        else:
            new_active_item = None
        for item in items:
            self.select_item(item)
            if active is item:
                new_active_item = item
        self.set_active_item(new_active_item)
        self.blockSignals(block)
        if new_active_item is not active:
            # if the new selection doesn't include the
            # previously active item
            self.SIG_ACTIVE_ITEM_CHANGED.emit(self)
        self.SIG_ITEM_SELECTION_CHANGED.emit(self)

    def set_active_item(self, item):
        """Set active item, and unselect the old active item"""
        self.active_item = item
        if self.active_item is not None:
            if not item.selected:
                self.select_item(self.active_item)
            self._active_xaxis = item.xAxis()
            self._active_yaxis = item.yAxis()
        self.SIG_ACTIVE_ITEM_CHANGED.emit(self)

    def get_active_axes(self):
        """Return active axes"""
        item = self.active_item
        if item is not None:
            self._active_xaxis = item.xAxis()
            self._active_yaxis = item.yAxis()
        return self._active_xaxis, self._active_yaxis

    def get_active_item(self, force=False):
        """
        Return active item
        Force item activation if there is no active item
        """
        if force and not self.active_item:
            for item in self.get_items():
                if item.can_select():
                    self.set_active_item(item)
                    break
        return self.active_item

    def get_nearest_object(self, pos, close_dist=0):
        """
        Return nearest item from position 'pos'

        If close_dist > 0:
            
            Return the first found item (higher z) which distance to 'pos' is 
            less than close_dist

        else:
            
            Return the closest item
        """
        selobj, distance, inside, handle = None, maxsize, None, None
        for obj in self.get_items(z_sorted=True):
            if not obj.isVisible() or not obj.can_select():
                continue
            d, _handle, _inside, other = obj.hit_test(pos)
            if d < distance:
                selobj, distance, handle, inside = obj, d, _handle, _inside
                if d < close_dist:
                    break
            if other is not None:
                # e.g. LegendBoxItem: selecting a curve ('other') instead of
                #                     legend box ('obj')
                return other, 0, None, True
        return selobj, distance, handle, inside

    def get_nearest_object_in_z(self, pos):
        """
        Return nearest item for which position 'pos' is inside of it
        (iterate over items with respect to their 'z' coordinate)
        """
        selobj, distance, inside, handle = None, maxsize, None, None
        for obj in self.get_items(z_sorted=True):
            if not obj.isVisible() or not obj.can_select():
                continue
            d, _handle, _inside, _other = obj.hit_test(pos)
            if _inside:
                selobj, distance, handle, inside = obj, d, _handle, _inside
                break
        return selobj, distance, handle, inside

    def get_context_menu(self):
        """Return widget context menu"""
        return self.manager.get_context_menu(self)

    def get_plot_parameters_status(self, key):
        if key == "item":
            return self.get_active_item() is not None
        else:
            return True

    def get_selected_item_parameters(self, itemparams):
        for item in self.get_selected_items():
            item.get_item_parameters(itemparams)
        # Retrieving active_item's parameters after every other item:
        # this way, the common datasets will be based on its parameters
        active_item = self.get_active_item()
        active_item.get_item_parameters(itemparams)

    def get_axesparam_class(self, item):
        """Return AxesParam dataset class associated to item's type"""
        return AxesParam

    def get_plot_parameters(self, key, itemparams):
        """
        Return a list of DataSets for a given parameter key
        the datasets will be edited and passed back to set_plot_parameters
        
        this is a generic interface to help building context menus
        using the BasePlotMenuTool
        """
        if key == "axes":
            for i, axeparam in enumerate(self.axes_styles):
                itemparams.add("AxeStyleParam%d" % i, self, axeparam)
        elif key == "item":
            active_item = self.get_active_item()
            if not active_item:
                return
            self.get_selected_item_parameters(itemparams)
            Param = self.get_axesparam_class(active_item)
            axesparam = Param(
                title=_("Axes"),
                icon="lin_lin.png",
                comment=_("Axes associated to selected item"),
            )
            axesparam.update_param(active_item)
            itemparams.add("AxesParam", self, axesparam)

    def set_item_parameters(self, itemparams):
        """Set item (plot, here) parameters"""
        # Axe styles
        datasets = [itemparams.get("AxeStyleParam%d" % i) for i in range(4)]
        if datasets[0] is not None:
            self.axes_styles = datasets
            self.update_all_axes_styles()
        # Changing active item's associated axes
        dataset = itemparams.get("AxesParam")
        if dataset is not None:
            active_item = self.get_active_item()
            dataset.update_axes(active_item)

    def edit_plot_parameters(self, key):
        """
        Edit plot parameters
        """
        multiselection = len(self.get_selected_items()) > 1
        itemparams = ItemParameters(multiselection=multiselection)
        self.get_plot_parameters(key, itemparams)
        title, icon = PARAMETERS_TITLE_ICON[key]
        itemparams.edit(self, title, icon)

    def edit_axis_parameters(self, axis_id):
        """Edit axis parameters"""
        if axis_id in (self.Y_LEFT, self.Y_RIGHT):
            title = _("Y Axis")
        else:
            title = _("X Axis")
        param = AxisParam(title=title)
        param.update_param(self, axis_id)
        if param.edit(parent=self):
            param.update_axis(self, axis_id)
            self.replot()

    def do_autoscale(self, replot=True, axis_id=None):
        """Do autoscale on all axes"""
        for axis_id in self.AXIS_IDS if axis_id is None else [axis_id]:
            self.setAxisAutoScale(axis_id)
        if replot:
            self.replot()

    def disable_autoscale(self):
        """Re-apply the axis scales so as to disable autoscaling
        without changing the view"""
        for axis_id in self.AXIS_IDS:
            vmin, vmax = self.get_axis_limits(axis_id)
            self.set_axis_limits(axis_id, vmin, vmax)

    def invalidate(self):
        """Invalidate paint cache and schedule redraw
        use instead of replot when only the content
        of the canvas needs redrawing (axes, shouldn't change)
        """
        self.canvas().replot()
        self.update()
Exemplo n.º 23
0
class DAQ_Viewer_TCP_server(DAQ_Viewer_base, TCPServer):
    """
        ================= ==============================
        **Attributes**      **Type**
        *command_server*    instance of Signal
        *x_axis*            1D numpy array
        *y_axis*            1D numpy array
        *data*              double precision float array
        ================= ==============================

        See Also
        --------
        utility_classes.DAQ_TCP_server
    """
    params_GRABBER = []  # parameters of a client grabber
    command_server = Signal(list)

    message_list = [
        "Quit", "Send Data 0D", "Send Data 1D", "Send Data 2D", "Send Data ND",
        "Status", "Done", "Server Closed", "Info", "Infos", "Info_xml",
        'x_axis', 'y_axis'
    ]
    socket_types = ["GRABBER"]
    params = comon_parameters + tcp_parameters

    def __init__(self, parent=None, params_state=None, grabber_type='2D'):
        """

        Parameters
        ----------
        parent
        params_state
        grabber_type: (str) either '0D', '1D' or '2D'
        """
        self.client_type = "GRABBER"
        DAQ_Viewer_base.__init__(
            self, parent, params_state
        )  # initialize base class with commom attributes and methods
        TCPServer.__init__(self, self.client_type)

        self.x_axis = None
        self.y_axis = None
        self.data = None
        self.grabber_type = grabber_type
        self.ind_data = 0
        self.data_mock = None

    def command_to_from_client(self, command):
        sock = self.find_socket_within_connected_clients(self.client_type)
        if sock is not None:  # if client self.client_type is connected then send it the command

            if command == 'x_axis':
                x_axis = dict(data=sock.get_array())
                x_axis['label'] = sock.get_string()
                x_axis['units'] = sock.get_string()
                self.x_axis = x_axis.copy()
                self.emit_x_axis()
            elif command == 'y_axis':
                y_axis = dict(data=sock.get_array())
                y_axis['label'] = sock.get_string()
                y_axis['units'] = sock.get_string()
                self.y_axis = y_axis.copy()
                self.emit_y_axis()

            else:
                self.send_command(sock, command)

        else:  # else simulate mock data
            if command == "Send Data 0D":
                self.set_1D_Mock_data()
                self.data_mock = np.array([self.data_mock[0]])
            elif command == "Send Data 1D":
                self.set_1D_Mock_data()
                data = self.data_mock
            elif command == "Send Data 2D":
                self.set_2D_Mock_data()
                data = self.data_mock
            self.process_cmds('Done')

    def send_data(self, sock, data):
        """
            To match digital and labview, send again a command.

            =============== ============================== ====================
            **Parameters**   **Type**                       **Description**
            *sock*                                          the socket receipt
            *data*           double precision float array   the data to be sent
            =============== ============================== ====================

            See Also
            --------
            send_command, check_send_data
        """
        self.send_command(sock, 'Done')

        sock.send_array(data)
        # if len(data.shape) == 0:
        #     Nrow = 1
        #     Ncol = 0
        # elif len(data.shape) == 1:
        #     Nrow = data.shape[0]
        #     Ncol = 0
        # elif len(data.shape) == 2:
        #     Nrow = data.shape[0]
        #     Ncol = data.shape[1]
        # data_bytes = data.tobytes()
        # check_sended(sock, np.array([len(data_bytes)],
        #                             dtype='>i4').tobytes())  # first send length of data after reshaping as 1D bytes array
        # check_sended(sock, np.array([Nrow], dtype='>i4').tobytes())  # then send dimension of lines
        # check_sended(sock, np.array([Ncol], dtype='>i4').tobytes())  # then send dimensions of columns
        #
        # check_sended(sock, data_bytes)  # then send data

    def read_data(self, sock):
        """
            Read the unsigned 32bits int data contained in the given socket in five steps :
                * get back the message
                * get the list length
                * get the data length
                * get the number of row
                * get the number of column
                * get data

            =============== ===================== =========================
            **Parameters**    **Type**             **Description**
            *sock*              ???                the socket to be readed
            *dtype*           numpy unint 32bits   ???
            =============== ===================== =========================

            See Also
            --------
            check_received_length
        """

        data_list = sock.get_list()

        return data_list

    def data_ready(self, data):
        """
            Send the grabed data signal. to be written in the detailed plugin using this base class

        for instance:
        self.data_grabed_signal.emit([OrderedDict(name=self.client_type,data=[data], type='Data2D')])  #to be overloaded
        """
        pass

    def command_done(self, command_sock):
        try:
            sock = self.find_socket_within_connected_clients(self.client_type)
            if sock is not None:  # if client self.client_type is connected then send it the command
                data = self.read_data(sock)
            else:
                data = self.data_mock

            if command_sock is None:
                # self.data_grabed_signal.emit([OrderedDict(data=[data],name='TCP GRABBER', type='Data2D')]) #to be directly send to a viewer
                self.data_ready(data)
                # print(data)
            else:
                self.send_data(command_sock, data)  # to be send to a client

        except Exception as e:
            self.emit_status(ThreadCommand("Update_Status", [str(e), 'log']))

    def commit_settings(self, param):

        if param.name() in iter_children(
                self.settings.child(('settings_client')), []):
            grabber_socket = \
                [client['socket'] for client in self.connected_clients if client['type'] == self.client_type][0]
            grabber_socket.send_string('set_info')

            path = get_param_path(
                param
            )[2:]  # get the path of this param as a list starting at parent 'infos'
            grabber_socket.send_list(path)

            # send value
            data = ioxml.parameter_to_xml_string(param)
            grabber_socket.send_string(data)

    def ini_detector(self, controller=None):
        """
            | Initialisation procedure of the detector updating the status dictionnary.
            |
            | Init axes from image , here returns only None values (to tricky to di it with the server and not really
             necessary for images anyway)

            See Also
            --------
            utility_classes.DAQ_TCP_server.init_server, get_xaxis, get_yaxis
        """
        self.status.update(
            edict(initialized=False,
                  info="",
                  x_axis=None,
                  y_axis=None,
                  controller=None))
        try:
            self.settings.child(('infos')).addChildren(self.params_GRABBER)

            self.init_server()

            # %%%%%%% init axes from image , here returns only None values (to tricky to di it with the server and not really necessary for images anyway)
            self.x_axis = self.get_xaxis()
            self.y_axis = self.get_yaxis()
            self.status.x_axis = self.x_axis
            self.status.y_axis = self.y_axis
            self.status.initialized = True
            self.status.controller = self.serversocket
            return self.status

        except Exception as e:
            self.status.info = getLineInfo() + str(e)
            self.status.initialized = False
            return self.status

    def close(self):
        """
            Should be used to uninitialize hardware.

            See Also
            --------
            utility_classes.DAQ_TCP_server.close_server
        """
        self.listening = False
        self.close_server()

    def get_xaxis(self):
        """
            Obtain the horizontal axis of the image.

            Returns
            -------
            1D numpy array
                Contains a vector of integer corresponding to the horizontal camera pixels.
        """
        pass
        return self.x_axis

    def get_yaxis(self):
        """
            Obtain the vertical axis of the image.

            Returns
            -------
            1D numpy array
                Contains a vector of integer corresponding to the vertical camera pixels.
        """
        pass
        return self.y_axis

    def grab_data(self, Naverage=1, **kwargs):
        """
            Start new acquisition.
            Grabbed indice is used to keep track of the current image in the average.

            ============== ========== ==============================
            **Parameters**   **Type**  **Description**

            *Naverage*        int       Number of images to average
            ============== ========== ==============================

            See Also
            --------
            utility_classes.DAQ_TCP_server.process_cmds
        """
        try:
            self.ind_grabbed = 0  # to keep track of the current image in the average
            self.Naverage = Naverage
            self.process_cmds("Send Data {:s}".format(self.grabber_type))
            # self.command_server.emit(["process_cmds","Send Data 2D"])

        except Exception as e:
            self.emit_status(
                ThreadCommand('Update_Status',
                              [getLineInfo() + str(e), "log"]))

    def stop(self):
        """
            not implemented.
        """
        pass
        return ""

    def set_1D_Mock_data(self):
        self.data_mock
        x = np.linspace(0, 99, 100)
        data_tmp = 10 * gauss1D(x, 50, 10, 1) + 1 * np.random.rand((100))
        self.ind_data += 1
        self.data_mock = np.roll(data_tmp, self.ind_data)

    def set_2D_Mock_data(self):
        self.x_axis = np.linspace(0, 50, 50, endpoint=False)
        self.y_axis = np.linspace(0, 30, 30, endpoint=False)
        self.data_mock = 10 * gauss2D(self.x_axis, 20, 10, self.y_axis, 15, 7,
                                      1) + 2 * np.random.rand(
                                          len(self.y_axis), len(self.x_axis))
Exemplo n.º 24
0
class KiteClient(QObject, KiteMethodProviderMixIn):
    sig_response_ready = Signal(int, dict)
    sig_client_started = Signal(list)
    sig_client_not_responding = Signal()
    sig_perform_request = Signal(int, str, object)
    sig_perform_status_request = Signal(str)
    sig_status_response_ready = Signal((str, ), (dict, ))
    sig_perform_onboarding_request = Signal()
    sig_onboarding_response_ready = Signal(str)

    def __init__(self, parent, enable_code_snippets=True):
        QObject.__init__(self, parent)
        self.endpoint = None
        self.requests = {}
        self.languages = []
        self.mutex = QMutex()
        self.opened_files = {}
        self.opened_files_status = {}
        self.thread_started = False
        self.enable_code_snippets = enable_code_snippets
        self.thread = QThread()
        self.moveToThread(self.thread)
        self.thread.started.connect(self.started)
        self.sig_perform_request.connect(self.perform_request)
        self.sig_perform_status_request.connect(self.get_status)
        self.sig_perform_onboarding_request.connect(self.get_onboarding_file)

    def start(self):
        if not self.thread_started:
            self.thread.start()
        logger.debug('Starting Kite HTTP session...')
        self.endpoint = requests.Session()
        self.languages = self.get_languages()
        self.sig_client_started.emit(self.languages)

    def started(self):
        self.thread_started = True

    def stop(self):
        if self.thread_started:
            logger.debug('Closing Kite HTTP session...')
            self.endpoint.close()
            self.thread.quit()

    def get_languages(self):
        verb, url = KITE_ENDPOINTS.LANGUAGES_ENDPOINT
        success, response = self.perform_http_request(verb, url)
        if response is None or isinstance(response, TEXT_TYPES):
            response = ['python']
        return response

    def _get_onboarding_file(self):
        """Perform a request to get kite's onboarding file."""
        verb, url = KITE_ENDPOINTS.ONBOARDING_ENDPOINT
        success, response = self.perform_http_request(verb, url)
        return response

    def get_onboarding_file(self):
        """Get onboarding file."""
        onboarding_file = self._get_onboarding_file()
        self.sig_onboarding_response_ready.emit(onboarding_file)

    def _get_status(self, filename):
        """Perform a request to get kite status for a file."""
        verb, url = KITE_ENDPOINTS.STATUS_ENDPOINT
        if filename:
            url_params = {'filename': filename}
        else:
            url_params = {'filetype': 'python'}
        success, response = self.perform_http_request(verb,
                                                      url,
                                                      url_params=url_params)
        return success, response

    def get_status(self, filename):
        """Get kite status for a given filename."""
        success_status, kite_status = self._get_status(filename)
        if not filename or kite_status is None:
            kite_status = status()
            self.sig_status_response_ready[str].emit(kite_status)
        elif isinstance(kite_status, TEXT_TYPES):
            status_str = status(extra_status=' with errors')
            long_str = _(
                "<code>{error}</code><br><br>"
                "Note: If you are using a VPN, "
                "please don't route requests to "
                "localhost/127.0.0.1 with it").format(error=kite_status)
            kite_status_dict = {
                'status': status_str,
                'short': status_str,
                'long': long_str
            }
            self.sig_status_response_ready[dict].emit(kite_status_dict)
        else:
            self.sig_status_response_ready[dict].emit(kite_status)

    def perform_http_request(self, verb, url, url_params=None, params=None):
        response = None
        http_method = getattr(self.endpoint, verb)
        try:
            http_response = http_method(url, params=url_params, json=params)
        except Exception as error:
            return False, None
        success = http_response.status_code == 200
        if success:
            try:
                response = http_response.json()
            except Exception:
                response = http_response.text
                response = None if response == '' else response
        return success, response

    def send(self, method, params, url_params):
        response = None
        if self.endpoint is not None and method in KITE_REQUEST_MAPPING:
            http_verb, path = KITE_REQUEST_MAPPING[method]
            encoded_url_params = {
                key: quote(value) if isinstance(value, TEXT_TYPES) else value
                for (key, value) in url_params.items()
            }
            path = path.format(**encoded_url_params)
            try:
                success, response = self.perform_http_request(http_verb,
                                                              path,
                                                              params=params)
            except (ConnectionRefusedError, ConnectionError):
                return response
        return response

    def perform_request(self, req_id, method, params):
        response = None
        if method in self.sender_registry:
            logger.debug('Perform {0} request with id {1}'.format(
                method, req_id))
            handler_name = self.sender_registry[method]
            handler = getattr(self, handler_name)
            response = handler(params)
            if method in self.handler_registry:
                converter_name = self.handler_registry[method]
                converter = getattr(self, converter_name)
                if response is not None:
                    response = converter(response)
        self.sig_response_ready.emit(req_id, response or {})
Exemplo n.º 25
0
class LineProfilerWidget(QWidget):
    """
    Line profiler widget.
    """
    DATAPATH = get_conf_path('lineprofiler.results')
    VERSION = '0.0.1'
    redirect_stdio = Signal(bool)
    sig_finished = Signal()

    def __init__(self, parent):
        QWidget.__init__(self, parent)
        # Need running QApplication before importing runconfig
        from spyder.preferences import runconfig
        self.runconfig = runconfig
        self.spyder_pythonpath = None

        self.setWindowTitle("Line profiler")

        self.output = None
        self.error_output = None

        self.use_colors = True

        self._last_wdir = None
        self._last_args = None
        self._last_pythonpath = None

        self.filecombo = PythonModulesComboBox(self)

        self.start_button = create_toolbutton(
            self,
            icon=get_icon('run.png'),
            text=_("Profile by line"),
            tip=_("Run line profiler"),
            triggered=(lambda checked=False: self.analyze()),
            text_beside_icon=True)
        self.stop_button = create_toolbutton(self,
                                             icon=get_icon('terminate.png'),
                                             text=_("Stop"),
                                             tip=_("Stop current profiling"),
                                             text_beside_icon=True)
        self.filecombo.valid.connect(self.start_button.setEnabled)
        #self.filecombo.valid.connect(self.show_data)
        # FIXME: The combobox emits this signal on almost any event
        #        triggering show_data() too early, too often.

        browse_button = create_toolbutton(self,
                                          icon=get_icon('fileopen.png'),
                                          tip=_('Select Python script'),
                                          triggered=self.select_file)

        self.datelabel = QLabel()

        self.log_button = create_toolbutton(self,
                                            icon=get_icon('log.png'),
                                            text=_("Output"),
                                            text_beside_icon=True,
                                            tip=_("Show program's output"),
                                            triggered=self.show_log)

        self.datatree = LineProfilerDataTree(self)

        self.collapse_button = create_toolbutton(
            self,
            icon=get_icon('collapse.png'),
            triggered=lambda dD=-1: self.datatree.collapseAll(),
            tip=_('Collapse all'))
        self.expand_button = create_toolbutton(
            self,
            icon=get_icon('expand.png'),
            triggered=lambda dD=1: self.datatree.expandAll(),
            tip=_('Expand all'))

        hlayout1 = QHBoxLayout()
        hlayout1.addWidget(self.filecombo)
        hlayout1.addWidget(browse_button)
        hlayout1.addWidget(self.start_button)
        hlayout1.addWidget(self.stop_button)

        hlayout2 = QHBoxLayout()
        hlayout2.addWidget(self.collapse_button)
        hlayout2.addWidget(self.expand_button)
        hlayout2.addStretch()
        hlayout2.addWidget(self.datelabel)
        hlayout2.addStretch()
        hlayout2.addWidget(self.log_button)

        layout = QVBoxLayout()
        layout.addLayout(hlayout1)
        layout.addLayout(hlayout2)
        layout.addWidget(self.datatree)
        self.setLayout(layout)

        self.process = None
        self.set_running_state(False)
        self.start_button.setEnabled(False)

        if not is_lineprofiler_installed():
            for widget in (self.datatree, self.filecombo, self.log_button,
                           self.start_button, self.stop_button, browse_button,
                           self.collapse_button, self.expand_button):
                widget.setDisabled(True)
            text = _(
                '<b>Please install the <a href="%s">line_profiler module</a></b>'
            ) % WEBSITE_URL
            self.datelabel.setText(text)
            self.datelabel.setOpenExternalLinks(True)
        else:
            pass  # self.show_data()

    def analyze(self,
                filename=None,
                wdir=None,
                args=None,
                pythonpath=None,
                use_colors=True):
        self.use_colors = use_colors
        if not is_lineprofiler_installed():
            return
        self.kill_if_running()
        #index, _data = self.get_data(filename) # FIXME: storing data is not implemented yet
        if filename is not None:
            filename = osp.abspath(to_text_string(filename))
            index = self.filecombo.findText(filename)
            if index == -1:
                self.filecombo.addItem(filename)
                self.filecombo.setCurrentIndex(self.filecombo.count() - 1)
            else:
                self.filecombo.setCurrentIndex(index)
            self.filecombo.selected()
        if self.filecombo.is_valid():
            filename = to_text_string(self.filecombo.currentText())
            runconf = self.runconfig.get_run_configuration(filename)
            if runconf is not None:
                if wdir is None:
                    if runconf.wdir_enabled:
                        wdir = runconf.wdir
                    elif runconf.cw_dir:
                        wdir = os.getcwd()
                    elif runconf.file_dir:
                        wdir = osp.dirname(filename)
                    elif runconf.fixed_dir:
                        wdir = runconf.dir
                if args is None:
                    if runconf.args_enabled:
                        args = runconf.args
            if wdir is None:
                wdir = osp.dirname(filename)
            if pythonpath is None:
                pythonpath = self.spyder_pythonpath
            self.start(wdir, args, pythonpath)

    def select_file(self):
        self.redirect_stdio.emit(False)
        filename, _selfilter = getopenfilename(
            self, _("Select Python script"), getcwd(),
            _("Python scripts") + " (*.py ; *.pyw)")
        self.redirect_stdio.emit(False)
        if filename:
            self.analyze(filename)

    def show_log(self):
        if self.output:
            editor = TextEditor(self.output,
                                title=_("Line profiler output"),
                                readonly=True)
            # Call .show() to dynamically resize editor;
            # see spyder-ide/spyder#12202
            editor.show()
            editor.exec_()

    def show_errorlog(self):
        if self.error_output:
            editor = TextEditor(self.error_output,
                                title=_("Line profiler output"),
                                readonly=True)
            # Call .show() to dynamically resize editor;
            # see spyder-ide/spyder#12202
            editor.show()
            editor.exec_()

    def start(self, wdir=None, args=None, pythonpath=None):
        filename = to_text_string(self.filecombo.currentText())
        if wdir is None:
            wdir = self._last_wdir
            if wdir is None:
                wdir = osp.basename(filename)
        if args is None:
            args = self._last_args
            if args is None:
                args = []
        if pythonpath is None:
            pythonpath = self._last_pythonpath
        self._last_wdir = wdir
        self._last_args = args
        self._last_pythonpath = pythonpath

        self.datelabel.setText(_('Profiling, please wait...'))

        self.process = QProcess(self)
        self.process.setProcessChannelMode(QProcess.SeparateChannels)
        self.process.setWorkingDirectory(wdir)
        self.process.readyReadStandardOutput.connect(self.read_output)
        self.process.readyReadStandardError.connect(
            lambda: self.read_output(error=True))
        self.process.finished.connect(self.finished)
        self.stop_button.clicked.connect(self.process.kill)

        if pythonpath is not None:
            env = [
                to_text_string(_pth)
                for _pth in self.process.systemEnvironment()
            ]
            add_pathlist_to_PYTHONPATH(env, pythonpath)
            processEnvironment = QProcessEnvironment()
            for envItem in env:
                envName, separator, envValue = envItem.partition('=')
                processEnvironment.insert(envName, envValue)
            self.process.setProcessEnvironment(processEnvironment)

        self.output = ''
        self.error_output = ''

        if os.name == 'nt':
            # On Windows, one has to replace backslashes by slashes to avoid
            # confusion with escape characters (otherwise, for example, '\t'
            # will be interpreted as a tabulation):
            filename = osp.normpath(filename).replace(os.sep, '/')
            p_args = [
                '-lvb', '-o', '"' + self.DATAPATH + '"', '"' + filename + '"'
            ]
            if args:
                p_args.extend(programs.shell_split(args))
            executable = '"' + programs.find_program('kernprof') + '"'
            executable += ' ' + ' '.join(p_args)
            executable = executable.replace(os.sep, '/')
            self.process.start(executable)
        else:
            p_args = ['-lvb', '-o', self.DATAPATH, filename]
            if args:
                p_args.extend(programs.shell_split(args))
            executable = 'kernprof'
            self.process.start(executable, p_args)

        running = self.process.waitForStarted()
        self.set_running_state(running)
        if not running:
            QMessageBox.critical(self, _("Error"),
                                 _("Process failed to start"))

    def set_running_state(self, state=True):
        self.start_button.setEnabled(not state)
        self.stop_button.setEnabled(state)

    def read_output(self, error=False):
        if error:
            self.process.setReadChannel(QProcess.StandardError)
        else:
            self.process.setReadChannel(QProcess.StandardOutput)
        qba = QByteArray()
        while self.process.bytesAvailable():
            if error:
                qba += self.process.readAllStandardError()
            else:
                qba += self.process.readAllStandardOutput()
        text = to_text_string(locale_codec.toUnicode(qba.data()))
        if error:
            self.error_output += text
        else:
            self.output += text

    def finished(self):
        self.set_running_state(False)
        self.show_errorlog()  # If errors occurred, show them.
        self.output = self.error_output + self.output
        # FIXME: figure out if show_data should be called here or
        #        as a signal from the combobox
        self.show_data(justanalyzed=True)
        self.sig_finished.emit()

    def kill_if_running(self):
        if self.process is not None:
            if self.process.state() == QProcess.Running:
                self.process.kill()
                self.process.waitForFinished()

    def show_data(self, justanalyzed=False):
        if not justanalyzed:
            self.output = None
        self.log_button.setEnabled(self.output is not None
                                   and len(self.output) > 0)
        self.kill_if_running()
        filename = to_text_string(self.filecombo.currentText())
        if not filename:
            return

        self.datatree.load_data(self.DATAPATH)
        self.datelabel.setText(_('Sorting data, please wait...'))
        QApplication.processEvents()
        self.datatree.show_tree()

        text_style = "<span style=\'color: #444444\'><b>%s </b></span>"
        date_text = text_style % time.strftime("%d %b %Y %H:%M",
                                               time.localtime())
        self.datelabel.setText(date_text)
Exemplo n.º 26
0
class AsyncClient(QObject):
    """
    A class which handles a connection to a client through a QProcess.
    """

    # Emitted when the client has initialized.
    initialized = Signal()

    # Emitted when the client errors.
    errored = Signal()

    # Emitted when a request response is received.
    received = Signal(object)

    def __init__(self,
                 target,
                 executable=None,
                 name=None,
                 extra_args=None,
                 libs=None,
                 cwd=None,
                 env=None,
                 extra_path=None):
        super(AsyncClient, self).__init__()
        self.executable = executable or sys.executable
        self.extra_args = extra_args
        self.target = target
        self.name = name or self
        self.libs = libs
        self.cwd = cwd
        self.env = env
        self.extra_path = extra_path
        self.is_initialized = False
        self.closing = False
        self.notifier = None
        self.process = None
        self.context = zmq.Context()
        QApplication.instance().aboutToQuit.connect(self.close)

        # Set up the heartbeat timer.
        self.timer = QTimer(self)
        self.timer.timeout.connect(self._heartbeat)

    def run(self):
        """Handle the connection with the server.
        """
        # Set up the zmq port.
        self.socket = self.context.socket(zmq.PAIR)
        self.port = self.socket.bind_to_random_port('tcp://*')

        # Set up the process.
        self.process = QProcess(self)
        if self.cwd:
            self.process.setWorkingDirectory(self.cwd)
        p_args = ['-u', self.target, str(self.port)]
        if self.extra_args is not None:
            p_args += self.extra_args

        # Set up environment variables.
        processEnvironment = QProcessEnvironment()
        env = self.process.systemEnvironment()
        if (self.env and 'PYTHONPATH' not in self.env) or self.env is None:
            python_path = osp.dirname(get_module_path('trex'))
            # Add the libs to the python path.
            for lib in self.libs:
                try:
                    path = osp.dirname(imp.find_module(lib)[1])
                    python_path = osp.pathsep.join([python_path, path])
                except ImportError:
                    pass
            if self.extra_path:
                try:
                    python_path = osp.pathsep.join([python_path] +
                                                   self.extra_path)
                except Exception as e:
                    debug_print("Error when adding extra_path to plugin env")
                    debug_print(e)
            env.append("PYTHONPATH=%s" % python_path)
        if self.env:
            env.update(self.env)
        for envItem in env:
            envName, separator, envValue = envItem.partition('=')
            processEnvironment.insert(envName, envValue)
        self.process.setProcessEnvironment(processEnvironment)

        # Start the process and wait for started.
        self.process.start(self.executable, p_args)
        self.process.finished.connect(self._on_finished)
        running = self.process.waitForStarted()
        if not running:
            raise IOError('Could not start %s' % self)

        # Set up the socket notifer.
        fid = self.socket.getsockopt(zmq.FD)
        self.notifier = QSocketNotifier(fid, QSocketNotifier.Read, self)
        self.notifier.activated.connect(self._on_msg_received)

    def request(self, func_name, *args, **kwargs):
        """Send a request to the server.

        The response will be a dictionary the 'request_id' and the
        'func_name' as well as a 'result' field with the object returned by
        the function call or or an 'error' field with a traceback.
        """
        if not self.is_initialized:
            return
        request_id = uuid.uuid4().hex
        request = dict(func_name=func_name,
                       args=args,
                       kwargs=kwargs,
                       request_id=request_id)
        self._send(request)
        return request_id

    def close(self):
        """Cleanly close the connection to the server.
        """
        self.closing = True
        self.is_initialized = False
        self.timer.stop()

        if self.notifier is not None:
            self.notifier.activated.disconnect(self._on_msg_received)
            self.notifier.setEnabled(False)
            self.notifier = None

        self.request('server_quit')

        if self.process is not None:
            self.process.waitForFinished(1000)
            self.process.close()
        self.context.destroy()

    def _on_finished(self):
        """Handle a finished signal from the process.
        """
        if self.closing:
            return
        if self.is_initialized:
            debug_print('Restarting %s' % self.name)
            debug_print(self.process.readAllStandardOutput())
            debug_print(self.process.readAllStandardError())
            self.is_initialized = False
            self.notifier.setEnabled(False)
            self.run()
        else:
            debug_print('Errored %s' % self.name)
            debug_print(self.process.readAllStandardOutput())
            debug_print(self.process.readAllStandardError())
            self.errored.emit()

    def _on_msg_received(self):
        """Handle a message trigger from the socket.
        """
        self.notifier.setEnabled(False)
        while 1:
            try:
                resp = self.socket.recv_pyobj(flags=zmq.NOBLOCK)
            except zmq.ZMQError:
                self.notifier.setEnabled(True)
                return
            if not self.is_initialized:
                self.is_initialized = True
                debug_print('Initialized %s' % self.name)
                self.initialized.emit()
                self.timer.start(HEARTBEAT)
                continue
            resp['name'] = self.name
            self.received.emit(resp)

    def _heartbeat(self):
        """Send a heartbeat to keep the server alive.
        """
        self._send(dict(func_name='server_heartbeat'))

    def _send(self, obj):
        """Send an object to the server.
        """
        try:
            self.socket.send_pyobj(obj, zmq.NOBLOCK)
        except Exception as e:
            debug_print(e)
            self.is_initialized = False
            self._on_finished()
Exemplo n.º 27
0
class TextEditBaseWidget(QPlainTextEdit, BaseEditMixin):
    """Text edit base widget"""
    BRACE_MATCHING_SCOPE = ('sof', 'eof')
    has_cell_separators = False
    focus_in = Signal()
    zoom_in = Signal()
    zoom_out = Signal()
    zoom_reset = Signal()
    focus_changed = Signal()
    sig_insert_completion = Signal(str)
    sig_eol_chars_changed = Signal(str)

    def __init__(self, parent=None):
        QPlainTextEdit.__init__(self, parent)
        BaseEditMixin.__init__(self)
        self.setAttribute(Qt.WA_DeleteOnClose)

        self.extra_selections_dict = {}
        self._restore_selection_pos = None

        # Trailing newlines/spaces trimming
        self.remove_trailing_spaces = False
        self.remove_trailing_newlines = False

        # Add a new line when saving
        self.add_newline = False

        # Code snippets
        self.code_snippets = True

        self.textChanged.connect(self.changed)
        self.cursorPositionChanged.connect(self.cursor_position_changed)

        self.indent_chars = " " * 4
        self.tab_stop_width_spaces = 4

        # Code completion / calltips
        if parent is not None:
            mainwin = parent
            while not isinstance(mainwin, QMainWindow):
                mainwin = mainwin.parent()
                if mainwin is None:
                    break
            if mainwin is not None:
                parent = mainwin

        self.completion_widget = CompletionWidget(self, parent)
        self.codecompletion_auto = False
        self.setup_completion()

        self.calltip_widget = CallTipWidget(self, hide_timer_on=False)
        self.calltip_position = None
        self.tooltip_widget = ToolTipWidget(self, as_tooltip=True)

        self.highlight_current_cell_enabled = False

        # The color values may be overridden by the syntax highlighter
        # Highlight current line color
        self.currentline_color = QColor(Qt.red).lighter(190)
        self.currentcell_color = QColor(Qt.red).lighter(194)

        # Brace matching
        self.bracepos = None
        self.matched_p_color = QColor(Qt.green)
        self.unmatched_p_color = QColor(Qt.red)

        self.decorations = TextDecorationsManager(self)

        # Save current cell. This is invalidated as soon as the text changes.
        # Useful to avoid recomputing while scrolling.
        self.current_cell = None

        def reset_current_cell():
            self.current_cell = None

        self.textChanged.connect(reset_current_cell)

    def setup_completion(self):
        size = CONF.get('main', 'completion/size')
        font = get_font()
        self.completion_widget.setup_appearance(size, font)

    def set_indent_chars(self, indent_chars):
        self.indent_chars = indent_chars

    def set_tab_stop_width_spaces(self, tab_stop_width_spaces):
        self.tab_stop_width_spaces = tab_stop_width_spaces
        self.update_tab_stop_width_spaces()

    def set_remove_trailing_spaces(self, flag):
        self.remove_trailing_spaces = flag

    def set_add_newline(self, add_newline):
        self.add_newline = add_newline

    def set_remove_trailing_newlines(self, flag):
        self.remove_trailing_newlines = flag

    def update_tab_stop_width_spaces(self):
        self.setTabStopWidth(self.fontMetrics().width(
            ' ' * self.tab_stop_width_spaces))

    def set_palette(self, background, foreground):
        """
        Set text editor palette colors:
        background color and caret (text cursor) color
        """
        # Because QtStylsheet overrides QPalette and because some style do not
        # use the palette for all drawing (e.g. macOS styles), the background
        # and foreground color of each TextEditBaseWidget instance must be set
        # with a stylesheet extended with an ID Selector.
        # Fixes spyder-ide/spyder#2028, spyder-ide/spyder#8069 and
        # spyder-ide/spyder#9248.
        if not self.objectName():
            self.setObjectName(self.__class__.__name__ + str(id(self)))
        style = "QPlainTextEdit#%s {background: %s; color: %s;}" % \
                (self.objectName(), background.name(), foreground.name())
        self.setStyleSheet(style)

    # ---- Extra selections
    def get_extra_selections(self, key):
        """Return editor extra selections.

        Args:
            key (str) name of the extra selections group

        Returns:
            list of sourcecode.api.TextDecoration.
        """
        return self.extra_selections_dict.get(key, [])

    def set_extra_selections(self, key, extra_selections):
        """Set extra selections for a key.

        Also assign draw orders to leave current_cell and current_line
        in the backgrund (and avoid them to cover other decorations)

        NOTE: This will remove previous decorations added to the same key.

        Args:
            key (str) name of the extra selections group.
            extra_selections (list of sourcecode.api.TextDecoration).
        """
        # use draw orders to highlight current_cell and current_line first
        draw_order = DRAW_ORDERS.get(key)
        if draw_order is None:
            draw_order = DRAW_ORDERS.get('on_top')

        for selection in extra_selections:
            selection.draw_order = draw_order
            selection.kind = key

        self.clear_extra_selections(key)
        self.extra_selections_dict[key] = extra_selections

    def update_extra_selections(self):
        """Add extra selections to DecorationsManager.

        TODO: This method could be remove it and decorations could be
        added/removed in set_extra_selections/clear_extra_selections.
        """
        extra_selections = []

        for key, extra in list(self.extra_selections_dict.items()):
            extra_selections.extend(extra)
        self.decorations.add(extra_selections)

    def clear_extra_selections(self, key):
        """Remove decorations added through set_extra_selections.

        Args:
            key (str) name of the extra selections group.
        """
        for decoration in self.extra_selections_dict.get(key, []):
            self.decorations.remove(decoration)
        self.extra_selections_dict[key] = []
        self.update()

    def changed(self):
        """Emit changed signal"""
        self.modificationChanged.emit(self.document().isModified())

    def get_visible_block_numbers(self):
        """Get the first and last visible block numbers."""
        first = self.firstVisibleBlock().blockNumber()
        bottom_right = QPoint(self.viewport().width() - 1,
                              self.viewport().height() - 1)
        last = self.cursorForPosition(bottom_right).blockNumber()
        return (first, last)

    def get_buffer_block_numbers(self):
        """
        Get the first and last block numbers of a region that covers
        the visible one plus a buffer of half that region above and
        below to make more fluid certain operations.
        """
        first_visible, last_visible = self.get_visible_block_numbers()
        buffer_height = round((last_visible - first_visible) / 2)

        first = first_visible - buffer_height
        first = 0 if first < 0 else first

        last = last_visible + buffer_height
        last = self.blockCount() if last > self.blockCount() else last

        return (first, last)

    #------Highlight current line
    def highlight_current_line(self):
        """Highlight current line"""
        selection = TextDecoration(self.textCursor())
        selection.format.setProperty(QTextFormat.FullWidthSelection,
                                     to_qvariant(True))
        selection.format.setBackground(self.currentline_color)
        selection.cursor.clearSelection()
        self.set_extra_selections('current_line', [selection])
        self.update_extra_selections()

    def unhighlight_current_line(self):
        """Unhighlight current line"""
        self.clear_extra_selections('current_line')

    #------Highlight current cell
    def highlight_current_cell(self):
        """Highlight current cell"""
        if (not self.has_cell_separators
                or not self.highlight_current_cell_enabled):
            return
        cursor, whole_file_selected = self.select_current_cell()
        selection = TextDecoration(cursor)
        selection.format.setProperty(QTextFormat.FullWidthSelection,
                                     to_qvariant(True))
        selection.format.setBackground(self.currentcell_color)

        if whole_file_selected:
            self.clear_extra_selections('current_cell')
        else:
            self.set_extra_selections('current_cell', [selection])
            self.update_extra_selections()

    def unhighlight_current_cell(self):
        """Unhighlight current cell"""
        self.clear_extra_selections('current_cell')

    #------Brace matching
    def find_brace_match(self, position, brace, forward):
        start_pos, end_pos = self.BRACE_MATCHING_SCOPE
        if forward:
            bracemap = {'(': ')', '[': ']', '{': '}'}
            text = self.get_text(position, end_pos)
            i_start_open = 1
            i_start_close = 1
        else:
            bracemap = {')': '(', ']': '[', '}': '{'}
            text = self.get_text(start_pos, position)
            i_start_open = len(text) - 1
            i_start_close = len(text) - 1

        while True:
            if forward:
                i_close = text.find(bracemap[brace], i_start_close)
            else:
                i_close = text.rfind(bracemap[brace], 0, i_start_close + 1)
            if i_close > -1:
                if forward:
                    i_start_close = i_close + 1
                    i_open = text.find(brace, i_start_open, i_close)
                else:
                    i_start_close = i_close - 1
                    i_open = text.rfind(brace, i_close, i_start_open + 1)
                if i_open > -1:
                    if forward:
                        i_start_open = i_open + 1
                    else:
                        i_start_open = i_open - 1
                else:
                    # found matching brace
                    if forward:
                        return position + i_close
                    else:
                        return position - (len(text) - i_close)
            else:
                # no matching brace
                return

    def __highlight(self, positions, color=None, cancel=False):
        if cancel:
            self.clear_extra_selections('brace_matching')
            return
        extra_selections = []
        for position in positions:
            if position > self.get_position('eof'):
                return
            selection = TextDecoration(self.textCursor())
            selection.format.setBackground(color)
            selection.cursor.clearSelection()
            selection.cursor.setPosition(position)
            selection.cursor.movePosition(QTextCursor.NextCharacter,
                                          QTextCursor.KeepAnchor)
            extra_selections.append(selection)
        self.set_extra_selections('brace_matching', extra_selections)
        self.update_extra_selections()

    def cursor_position_changed(self):
        """Brace matching"""
        if self.bracepos is not None:
            self.__highlight(self.bracepos, cancel=True)
            self.bracepos = None
        cursor = self.textCursor()
        if cursor.position() == 0:
            return
        cursor.movePosition(QTextCursor.PreviousCharacter,
                            QTextCursor.KeepAnchor)
        text = to_text_string(cursor.selectedText())
        pos1 = cursor.position()
        if text in (')', ']', '}'):
            pos2 = self.find_brace_match(pos1, text, forward=False)
        elif text in ('(', '[', '{'):
            pos2 = self.find_brace_match(pos1, text, forward=True)
        else:
            return
        if pos2 is not None:
            self.bracepos = (pos1, pos2)
            self.__highlight(self.bracepos, color=self.matched_p_color)
        else:
            self.bracepos = (pos1, )
            self.__highlight(self.bracepos, color=self.unmatched_p_color)

    #-----Widget setup and options
    def set_codecompletion_auto(self, state):
        """Set code completion state"""
        self.codecompletion_auto = state

    def set_wrap_mode(self, mode=None):
        """
        Set wrap mode
        Valid *mode* values: None, 'word', 'character'
        """
        if mode == 'word':
            wrap_mode = QTextOption.WrapAtWordBoundaryOrAnywhere
        elif mode == 'character':
            wrap_mode = QTextOption.WrapAnywhere
        else:
            wrap_mode = QTextOption.NoWrap
        self.setWordWrapMode(wrap_mode)

    #------Reimplementing Qt methods
    @Slot()
    def copy(self):
        """
        Reimplement Qt method
        Copy text to clipboard with correct EOL chars
        """
        if self.get_selected_text():
            QApplication.clipboard().setText(self.get_selected_text())

    def toPlainText(self):
        """
        Reimplement Qt method
        Fix PyQt4 bug on Windows and Python 3
        """
        # Fix what appears to be a PyQt4 bug when getting file
        # contents under Windows and PY3. This bug leads to
        # corruptions when saving files with certain combinations
        # of unicode chars on them (like the one attached on
        # spyder-ide/spyder#1546).
        if os.name == 'nt' and PY3:
            text = self.get_text('sof', 'eof')
            return text.replace('\u2028', '\n').replace('\u2029', '\n')\
                       .replace('\u0085', '\n')
        else:
            return super(TextEditBaseWidget, self).toPlainText()

    def keyPressEvent(self, event):
        text, key = event.text(), event.key()
        ctrl = event.modifiers() & Qt.ControlModifier
        meta = event.modifiers() & Qt.MetaModifier
        # Use our own copy method for {Ctrl,Cmd}+C to avoid Qt
        # copying text in HTML. See spyder-ide/spyder#2285.
        if (ctrl or meta) and key == Qt.Key_C:
            self.copy()
        else:
            super(TextEditBaseWidget, self).keyPressEvent(event)

    #------Text: get, set, ...
    def get_cell_list(self):
        """Get all cells."""
        # Reimplemented in childrens
        return []

    def get_selection_as_executable_code(self, cursor=None):
        """Return selected text as a processed text,
        to be executable in a Python/IPython interpreter"""
        ls = self.get_line_separator()

        _indent = lambda line: len(line) - len(line.lstrip())

        line_from, line_to = self.get_selection_bounds(cursor)
        text = self.get_selected_text(cursor)
        if not text:
            return

        lines = text.split(ls)
        if len(lines) > 1:
            # Multiline selection -> eventually fixing indentation
            original_indent = _indent(self.get_text_line(line_from))
            text = (" " * (original_indent - _indent(lines[0]))) + text

        # If there is a common indent to all lines, find it.
        # Moving from bottom line to top line ensures that blank
        # lines inherit the indent of the line *below* it,
        # which is the desired behavior.
        min_indent = 999
        current_indent = 0
        lines = text.split(ls)
        for i in range(len(lines) - 1, -1, -1):
            line = lines[i]
            if line.strip():
                current_indent = _indent(line)
                min_indent = min(current_indent, min_indent)
            else:
                lines[i] = ' ' * current_indent
        if min_indent:
            lines = [line[min_indent:] for line in lines]

        # Remove any leading whitespace or comment lines
        # since they confuse the reserved word detector that follows below
        lines_removed = 0
        while lines:
            first_line = lines[0].lstrip()
            if first_line == '' or first_line[0] == '#':
                lines_removed += 1
                lines.pop(0)
            else:
                break

        # Add an EOL character after the last line of code so that it gets
        # evaluated automatically by the console and any quote characters
        # are separated from the triple quotes of runcell
        lines.append(ls)

        # Add removed lines back to have correct traceback line numbers
        leading_lines_str = ls * lines_removed

        return leading_lines_str + ls.join(lines)

    def get_cell_as_executable_code(self, cursor=None):
        """Return cell contents as executable code."""
        if cursor is None:
            cursor = self.textCursor()
        ls = self.get_line_separator()
        cursor, whole_file_selected = self.select_current_cell(cursor)
        line_from, line_to = self.get_selection_bounds(cursor)
        # Get the block for the first cell line
        start = cursor.selectionStart()
        block = self.document().findBlock(start)
        if not is_cell_header(block) and start > 0:
            block = self.document().findBlock(start - 1)
        # Get text
        text = self.get_selection_as_executable_code(cursor)
        if text is not None:
            text = ls * line_from + text
        return text, block

    def select_current_cell(self, cursor=None):
        """
        Select cell under cursor in the visible portion of the file
        cell = group of lines separated by CELL_SEPARATORS
        returns
         -the textCursor
         -a boolean indicating if the entire file is selected
        """
        if cursor is None:
            cursor = self.textCursor()

        if self.current_cell:
            current_cell, cell_full_file = self.current_cell
            cell_start_pos = current_cell.selectionStart()
            cell_end_position = current_cell.selectionEnd()
            # Check if the saved current cell is still valid
            if cell_start_pos <= cursor.position() < cell_end_position:
                return current_cell, cell_full_file
            else:
                self.current_cell = None

        block = cursor.block()
        try:
            if is_cell_header(block):
                header = block.userData().oedata
            else:
                header = next(
                    document_cells(block,
                                   forward=False,
                                   cell_list=self.get_cell_list()))
            cell_start_pos = header.block.position()
            cell_at_file_start = False
            cursor.setPosition(cell_start_pos)
        except StopIteration:
            # This cell has no header, so it is the first cell.
            cell_at_file_start = True
            cursor.movePosition(QTextCursor.Start)

        try:
            footer = next(
                document_cells(block,
                               forward=True,
                               cell_list=self.get_cell_list()))
            cell_end_position = footer.block.position()
            cell_at_file_end = False
            cursor.setPosition(cell_end_position, QTextCursor.KeepAnchor)
        except StopIteration:
            # This cell has no next header, so it is the last cell.
            cell_at_file_end = True
            cursor.movePosition(QTextCursor.End, QTextCursor.KeepAnchor)

        cell_full_file = cell_at_file_start and cell_at_file_end
        self.current_cell = (cursor, cell_full_file)

        return cursor, cell_full_file

    def go_to_next_cell(self):
        """Go to the next cell of lines"""
        cursor = self.textCursor()
        block = cursor.block()
        try:
            footer = next(
                document_cells(block,
                               forward=True,
                               cell_list=self.get_cell_list()))
            cursor.setPosition(footer.block.position())
        except StopIteration:
            return
        self.setTextCursor(cursor)

    def go_to_previous_cell(self):
        """Go to the previous cell of lines"""
        cursor = self.textCursor()
        block = cursor.block()
        if is_cell_header(block):
            block = block.previous()
        try:
            header = next(
                document_cells(block,
                               forward=False,
                               cell_list=self.get_cell_list()))
            cursor.setPosition(header.block.position())
        except StopIteration:
            return
        self.setTextCursor(cursor)

    def get_line_count(self):
        """Return document total line number"""
        return self.blockCount()

    def paintEvent(self, e):
        """
        Override Qt method to restore text selection after text gets inserted
        at the current position of the cursor.

        See spyder-ide/spyder#11089 for more info.
        """
        if self._restore_selection_pos is not None:
            self.__restore_selection(*self._restore_selection_pos)
            self._restore_selection_pos = None
        super(TextEditBaseWidget, self).paintEvent(e)

    def __save_selection(self):
        """Save current cursor selection and return position bounds"""
        cursor = self.textCursor()
        return cursor.selectionStart(), cursor.selectionEnd()

    def __restore_selection(self, start_pos, end_pos):
        """Restore cursor selection from position bounds"""
        cursor = self.textCursor()
        cursor.setPosition(start_pos)
        cursor.setPosition(end_pos, QTextCursor.KeepAnchor)
        self.setTextCursor(cursor)

    def __duplicate_line_or_selection(self, after_current_line=True):
        """Duplicate current line or selected text"""
        cursor = self.textCursor()
        cursor.beginEditBlock()
        cur_pos = cursor.position()
        start_pos, end_pos = self.__save_selection()
        end_pos_orig = end_pos
        if to_text_string(cursor.selectedText()):
            cursor.setPosition(end_pos)
            # Check if end_pos is at the start of a block: if so, starting
            # changes from the previous block
            cursor.movePosition(QTextCursor.StartOfBlock,
                                QTextCursor.KeepAnchor)
            if not to_text_string(cursor.selectedText()):
                cursor.movePosition(QTextCursor.PreviousBlock)
                end_pos = cursor.position()

        cursor.setPosition(start_pos)
        cursor.movePosition(QTextCursor.StartOfBlock)
        while cursor.position() <= end_pos:
            cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
            if cursor.atEnd():
                cursor_temp = QTextCursor(cursor)
                cursor_temp.clearSelection()
                cursor_temp.insertText(self.get_line_separator())
                break
            cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor)
        text = cursor.selectedText()
        cursor.clearSelection()

        if not after_current_line:
            # Moving cursor before current line/selected text
            cursor.setPosition(start_pos)
            cursor.movePosition(QTextCursor.StartOfBlock)
            start_pos += len(text)
            end_pos_orig += len(text)
            cur_pos += len(text)

        # We save the end and start position of the selection, so that it
        # can be restored within the paint event that is triggered by the
        # text insertion. This is done to prevent a graphical glitch that
        # occurs when text gets inserted at the current position of the cursor.
        # See spyder-ide/spyder#11089 for more info.
        if cur_pos == start_pos:
            self._restore_selection_pos = (end_pos_orig, start_pos)
        else:
            self._restore_selection_pos = (start_pos, end_pos_orig)
        cursor.insertText(text)
        cursor.endEditBlock()

        self.document_did_change()

    def duplicate_line_down(self):
        """
        Copy current line or selected text and paste the duplicated text
        *after* the current line or selected text.
        """
        self.__duplicate_line_or_selection(after_current_line=False)

    def duplicate_line_up(self):
        """
        Copy current line or selected text and paste the duplicated text
        *before* the current line or selected text.
        """
        self.__duplicate_line_or_selection(after_current_line=True)

    def __move_line_or_selection(self, after_current_line=True):
        """Move current line or selected text"""
        cursor = self.textCursor()
        cursor.beginEditBlock()
        start_pos, end_pos = self.__save_selection()
        last_line = False

        # ------ Select text
        # Get selection start location
        cursor.setPosition(start_pos)
        cursor.movePosition(QTextCursor.StartOfBlock)
        start_pos = cursor.position()

        # Get selection end location
        cursor.setPosition(end_pos)
        if not cursor.atBlockStart() or end_pos == start_pos:
            cursor.movePosition(QTextCursor.EndOfBlock)
            cursor.movePosition(QTextCursor.NextBlock)
        end_pos = cursor.position()

        # Check if selection ends on the last line of the document
        if cursor.atEnd():
            if not cursor.atBlockStart() or end_pos == start_pos:
                last_line = True

        # ------ Stop if at document boundary
        cursor.setPosition(start_pos)
        if cursor.atStart() and not after_current_line:
            # Stop if selection is already at top of the file while moving up
            cursor.endEditBlock()
            self.setTextCursor(cursor)
            self.__restore_selection(start_pos, end_pos)
            return

        cursor.setPosition(end_pos, QTextCursor.KeepAnchor)
        if last_line and after_current_line:
            # Stop if selection is already at end of the file while moving down
            cursor.endEditBlock()
            self.setTextCursor(cursor)
            self.__restore_selection(start_pos, end_pos)
            return

        # ------ Move text
        sel_text = to_text_string(cursor.selectedText())
        cursor.removeSelectedText()

        if after_current_line:
            # Shift selection down
            text = to_text_string(cursor.block().text())
            sel_text = os.linesep + sel_text[0:-1]  # Move linesep at the start
            cursor.movePosition(QTextCursor.EndOfBlock)
            start_pos += len(text) + 1
            end_pos += len(text)
            if not cursor.atEnd():
                end_pos += 1
        else:
            # Shift selection up
            if last_line:
                # Remove the last linesep and add it to the selected text
                cursor.deletePreviousChar()
                sel_text = sel_text + os.linesep
                cursor.movePosition(QTextCursor.StartOfBlock)
                end_pos += 1
            else:
                cursor.movePosition(QTextCursor.PreviousBlock)
            text = to_text_string(cursor.block().text())
            start_pos -= len(text) + 1
            end_pos -= len(text) + 1

        cursor.insertText(sel_text)

        cursor.endEditBlock()
        self.setTextCursor(cursor)
        self.__restore_selection(start_pos, end_pos)

        self.document_did_change()

    def move_line_up(self):
        """Move up current line or selected text"""
        self.__move_line_or_selection(after_current_line=False)

    def move_line_down(self):
        """Move down current line or selected text"""
        self.__move_line_or_selection(after_current_line=True)

    def go_to_new_line(self):
        """Go to the end of the current line and create a new line"""
        self.stdkey_end(False, False)
        self.insert_text(self.get_line_separator())

    def extend_selection_to_complete_lines(self):
        """Extend current selection to complete lines"""
        cursor = self.textCursor()
        start_pos, end_pos = cursor.selectionStart(), cursor.selectionEnd()
        cursor.setPosition(start_pos)
        cursor.setPosition(end_pos, QTextCursor.KeepAnchor)
        if cursor.atBlockStart():
            cursor.movePosition(QTextCursor.PreviousBlock,
                                QTextCursor.KeepAnchor)
            cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
        self.setTextCursor(cursor)

    def delete_line(self, cursor=None):
        """Delete current line."""
        if cursor is None:
            cursor = self.textCursor()
        if self.has_selected_text():
            self.extend_selection_to_complete_lines()
            start_pos, end_pos = cursor.selectionStart(), cursor.selectionEnd()
            cursor.setPosition(start_pos)
        else:
            start_pos = end_pos = cursor.position()
        cursor.beginEditBlock()
        cursor.setPosition(start_pos)
        cursor.movePosition(QTextCursor.StartOfBlock)
        while cursor.position() <= end_pos:
            cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
            if cursor.atEnd():
                break
            cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor)
        cursor.removeSelectedText()
        cursor.endEditBlock()
        self.ensureCursorVisible()
        self.document_did_change()

    def set_selection(self, start, end):
        cursor = self.textCursor()
        cursor.setPosition(start)
        cursor.setPosition(end, QTextCursor.KeepAnchor)
        self.setTextCursor(cursor)

    def truncate_selection(self, position_from):
        """Unselect read-only parts in shell, like prompt"""
        position_from = self.get_position(position_from)
        cursor = self.textCursor()
        start, end = cursor.selectionStart(), cursor.selectionEnd()
        if start < end:
            start = max([position_from, start])
        else:
            end = max([position_from, end])
        self.set_selection(start, end)

    def restrict_cursor_position(self, position_from, position_to):
        """In shell, avoid editing text except between prompt and EOF"""
        position_from = self.get_position(position_from)
        position_to = self.get_position(position_to)
        cursor = self.textCursor()
        cursor_position = cursor.position()
        if cursor_position < position_from or cursor_position > position_to:
            self.set_cursor_position(position_to)

    #------Code completion / Calltips
    def hide_tooltip_if_necessary(self, key):
        """Hide calltip when necessary"""
        try:
            calltip_char = self.get_character(self.calltip_position)
            before = self.is_cursor_before(self.calltip_position,
                                           char_offset=1)
            other = key in (Qt.Key_ParenRight, Qt.Key_Period, Qt.Key_Tab)
            if calltip_char not in ('?', '(') or before or other:
                QToolTip.hideText()
        except (IndexError, TypeError):
            QToolTip.hideText()

    def select_completion_list(self):
        """Completion list is active, Enter was just pressed"""
        self.completion_widget.item_selected()

    def insert_completion(self, completion, completion_position):
        """Insert a completion into the editor.

        completion_position is where the completion was generated.

        The replacement range is computed using the (LSP) completion's
        textEdit field if it exists. Otherwise, we replace from the
        start of the word under the cursor.
        """
        if not completion:
            return

        cursor = self.textCursor()

        has_selected_text = self.has_selected_text()
        selection_start, selection_end = self.get_selection_start_end()

        if isinstance(completion, dict) and 'textEdit' in completion:
            completion_range = completion['textEdit']['range']
            start = completion_range['start']
            end = completion_range['end']
            if isinstance(completion_range['start'], dict):
                start_line, start_col = start['line'], start['character']
                start = self.get_position_line_number(start_line, start_col)
            if isinstance(completion_range['start'], dict):
                end_line, end_col = end['line'], end['character']
                end = self.get_position_line_number(end_line, end_col)
            cursor.setPosition(start)
            cursor.setPosition(end, QTextCursor.KeepAnchor)
            text = to_text_string(completion['textEdit']['newText'])
        else:
            text = completion
            if isinstance(completion, dict):
                text = completion['insertText']
            text = to_text_string(text)

            # Get word on the left of the cursor.
            result = self.get_current_word_and_position(completion=True)
            if result is not None:
                current_text, start_position = result
                end_position = start_position + len(current_text)
                # Check if the completion position is in the expected range
                if not start_position <= completion_position <= end_position:
                    return
                cursor.setPosition(start_position)
                # Remove the word under the cursor
                cursor.setPosition(end_position, QTextCursor.KeepAnchor)
            else:
                # Check if we are in the correct position
                if cursor.position() != completion_position:
                    return

        if has_selected_text:
            self.sig_will_remove_selection.emit(selection_start, selection_end)

        cursor.removeSelectedText()
        self.setTextCursor(cursor)

        # Add text
        if self.objectName() == 'console':
            # Handle completions for the internal console
            self.insert_text(text)
        else:
            if self.code_snippets:
                self.sig_insert_completion.emit(text)
            else:
                self.insert_text(text)
            self.document_did_change()

    def is_completion_widget_visible(self):
        """Return True is completion list widget is visible"""
        return self.completion_widget.isVisible()

    def hide_completion_widget(self, focus_to_parent=True):
        """Hide completion widget and tooltip."""
        self.completion_widget.hide(focus_to_parent=focus_to_parent)
        QToolTip.hideText()

    #------Standard keys
    def stdkey_clear(self):
        if not self.has_selected_text():
            self.moveCursor(QTextCursor.NextCharacter, QTextCursor.KeepAnchor)
        self.remove_selected_text()

    def stdkey_backspace(self):
        if not self.has_selected_text():
            self.moveCursor(QTextCursor.PreviousCharacter,
                            QTextCursor.KeepAnchor)
        self.remove_selected_text()

    def __get_move_mode(self, shift):
        return QTextCursor.KeepAnchor if shift else QTextCursor.MoveAnchor

    def stdkey_up(self, shift):
        self.moveCursor(QTextCursor.Up, self.__get_move_mode(shift))

    def stdkey_down(self, shift):
        self.moveCursor(QTextCursor.Down, self.__get_move_mode(shift))

    def stdkey_tab(self):
        self.insert_text(self.indent_chars)

    def stdkey_home(self, shift, ctrl, prompt_pos=None):
        """Smart HOME feature: cursor is first moved at
        indentation position, then at the start of the line"""
        move_mode = self.__get_move_mode(shift)
        if ctrl:
            self.moveCursor(QTextCursor.Start, move_mode)
        else:
            cursor = self.textCursor()
            if prompt_pos is None:
                start_position = self.get_position('sol')
            else:
                start_position = self.get_position(prompt_pos)
            text = self.get_text(start_position, 'eol')
            indent_pos = start_position + len(text) - len(text.lstrip())
            if cursor.position() != indent_pos:
                cursor.setPosition(indent_pos, move_mode)
            else:
                cursor.setPosition(start_position, move_mode)
            self.setTextCursor(cursor)

    def stdkey_end(self, shift, ctrl):
        move_mode = self.__get_move_mode(shift)
        if ctrl:
            self.moveCursor(QTextCursor.End, move_mode)
        else:
            self.moveCursor(QTextCursor.EndOfBlock, move_mode)

    def stdkey_pageup(self):
        pass

    def stdkey_pagedown(self):
        pass

    def stdkey_escape(self):
        pass

    #----Qt Events
    def mousePressEvent(self, event):
        """Reimplement Qt method"""
        if sys.platform.startswith('linux') and event.button() == Qt.MidButton:
            self.calltip_widget.hide()
            self.setFocus()
            event = QMouseEvent(QEvent.MouseButtonPress, event.pos(),
                                Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
            QPlainTextEdit.mousePressEvent(self, event)
            QPlainTextEdit.mouseReleaseEvent(self, event)
            # Send selection text to clipboard to be able to use
            # the paste method and avoid the strange spyder-ide/spyder#1445.
            # NOTE: This issue seems a focusing problem but it
            # seems really hard to track
            mode_clip = QClipboard.Clipboard
            mode_sel = QClipboard.Selection
            text_clip = QApplication.clipboard().text(mode=mode_clip)
            text_sel = QApplication.clipboard().text(mode=mode_sel)
            QApplication.clipboard().setText(text_sel, mode=mode_clip)
            self.paste()
            QApplication.clipboard().setText(text_clip, mode=mode_clip)
        else:
            self.calltip_widget.hide()
            QPlainTextEdit.mousePressEvent(self, event)

    def focusInEvent(self, event):
        """Reimplemented to handle focus"""
        self.focus_changed.emit()
        self.focus_in.emit()
        QPlainTextEdit.focusInEvent(self, event)

    def focusOutEvent(self, event):
        """Reimplemented to handle focus"""
        self.focus_changed.emit()
        QPlainTextEdit.focusOutEvent(self, event)

    def wheelEvent(self, event):
        """Reimplemented to emit zoom in/out signals when Ctrl is pressed"""
        # This feature is disabled on MacOS, see spyder-ide/spyder#1510.
        if sys.platform != 'darwin':
            if event.modifiers() & Qt.ControlModifier:
                if hasattr(event, 'angleDelta'):
                    if event.angleDelta().y() < 0:
                        self.zoom_out.emit()
                    elif event.angleDelta().y() > 0:
                        self.zoom_in.emit()
                elif hasattr(event, 'delta'):
                    if event.delta() < 0:
                        self.zoom_out.emit()
                    elif event.delta() > 0:
                        self.zoom_in.emit()
                return

        QPlainTextEdit.wheelEvent(self, event)

        # Needed to prevent stealing focus when scrolling.
        # If the current widget with focus is the CompletionWidget, it means
        # it's being displayed in the editor, so we need to hide it and give
        # focus back to the editor. If not, we need to leave the focus in
        # the widget that currently has it.
        # See spyder-ide/spyder#11502
        current_widget = QApplication.focusWidget()
        if isinstance(current_widget, CompletionWidget):
            self.hide_completion_widget(focus_to_parent=True)
        else:
            self.hide_completion_widget(focus_to_parent=False)

    def position_widget_at_cursor(self, widget):
        # Retrieve current screen height
        desktop = QApplication.desktop()
        srect = desktop.availableGeometry(desktop.screenNumber(widget))

        left, top, right, bottom = (srect.left(), srect.top(), srect.right(),
                                    srect.bottom())
        ancestor = widget.parent()
        if ancestor:
            left = max(left, ancestor.x())
            top = max(top, ancestor.y())
            right = min(right, ancestor.x() + ancestor.width())
            bottom = min(bottom, ancestor.y() + ancestor.height())

        point = self.cursorRect().bottomRight()
        point = self.calculate_real_position(point)
        point = self.mapToGlobal(point)
        # Move to left of cursor if not enough space on right
        widget_right = point.x() + widget.width()
        if widget_right > right:
            point.setX(point.x() - widget.width())
        # Push to right if not enough space on left
        if point.x() < left:
            point.setX(left)

        # Moving widget above if there is not enough space below
        widget_bottom = point.y() + widget.height()
        x_position = point.x()
        if widget_bottom > bottom:
            point = self.cursorRect().topRight()
            point = self.mapToGlobal(point)
            point.setX(x_position)
            point.setY(point.y() - widget.height())

        if ancestor is not None:
            # Useful only if we set parent to 'ancestor' in __init__
            point = ancestor.mapFromGlobal(point)

        widget.move(point)

    def calculate_real_position(self, point):
        return point
Exemplo n.º 28
0
class Help(SpyderPluginWidget):
    """
    Docstrings viewer widget
    """
    CONF_SECTION = 'help'
    CONFIGWIDGET_CLASS = HelpConfigPage
    CONF_FILE = False
    LOG_PATH = get_conf_path(CONF_SECTION)
    FONT_SIZE_DELTA = DEFAULT_SMALL_DELTA

    # Signals
    focus_changed = Signal()

    def __init__(self, parent=None, css_path=CSS_PATH):
        SpyderPluginWidget.__init__(self, parent)

        self.internal_shell = None
        self.console = None
        self.css_path = css_path

        self.no_doc_string = _("No documentation available")

        self._last_console_cb = None
        self._last_editor_cb = None

        self.plain_text = PlainText(self)
        self.rich_text = RichText(self)

        color_scheme = self.get_color_scheme()
        self.set_plain_text_font(self.get_font(), color_scheme)
        self.plain_text.editor.toggle_wrap_mode(self.get_option('wrap'))

        # Add entries to read-only editor context-menu
        self.wrap_action = create_action(self,
                                         _("Wrap lines"),
                                         toggled=self.toggle_wrap_mode)
        self.wrap_action.setChecked(self.get_option('wrap'))
        self.plain_text.editor.readonly_menu.addSeparator()
        add_actions(self.plain_text.editor.readonly_menu, (self.wrap_action, ))

        self.set_rich_text_font(self.get_font(rich_text=True))

        self.shell = None

        # locked = disable link with Console
        self.locked = False
        self._last_texts = [None, None]
        self._last_editor_doc = None

        # Object name
        layout_edit = QHBoxLayout()
        layout_edit.setContentsMargins(0, 0, 0, 0)
        txt = _("Source")
        if sys.platform == 'darwin':
            source_label = QLabel("  " + txt)
        else:
            source_label = QLabel(txt)
        layout_edit.addWidget(source_label)
        self.source_combo = QComboBox(self)
        self.source_combo.addItems([_("Console"), _("Editor")])
        self.source_combo.currentIndexChanged.connect(self.source_changed)
        if (not programs.is_module_installed('rope')
                and not programs.is_module_installed('jedi', '>=0.11.0')):
            self.source_combo.hide()
            source_label.hide()
        layout_edit.addWidget(self.source_combo)
        layout_edit.addSpacing(10)
        layout_edit.addWidget(QLabel(_("Object")))
        self.combo = ObjectComboBox(self)
        layout_edit.addWidget(self.combo)
        self.object_edit = QLineEdit(self)
        self.object_edit.setReadOnly(True)
        layout_edit.addWidget(self.object_edit)
        self.combo.setMaxCount(self.get_option('max_history_entries'))
        self.combo.addItems(self.load_history())
        self.combo.setItemText(0, '')
        self.combo.valid.connect(self.force_refresh)

        # Plain text docstring option
        self.docstring = True
        self.rich_help = self.get_option('rich_mode', True)
        self.plain_text_action = create_action(self,
                                               _("Plain Text"),
                                               toggled=self.toggle_plain_text)

        # Source code option
        self.show_source_action = create_action(
            self, _("Show Source"), toggled=self.toggle_show_source)

        # Rich text option
        self.rich_text_action = create_action(self,
                                              _("Rich Text"),
                                              toggled=self.toggle_rich_text)

        # Add the help actions to an exclusive QActionGroup
        help_actions = QActionGroup(self)
        help_actions.setExclusive(True)
        help_actions.addAction(self.plain_text_action)
        help_actions.addAction(self.rich_text_action)

        # Automatic import option
        self.auto_import_action = create_action(
            self, _("Automatic import"), toggled=self.toggle_auto_import)
        auto_import_state = self.get_option('automatic_import')
        self.auto_import_action.setChecked(auto_import_state)

        # Lock checkbox
        self.locked_button = create_toolbutton(self,
                                               triggered=self.toggle_locked)
        layout_edit.addWidget(self.locked_button)
        self._update_lock_icon()

        # Option menu
        layout_edit.addWidget(self.options_button)

        if self.rich_help:
            self.switch_to_rich_text()
        else:
            self.switch_to_plain_text()
        self.plain_text_action.setChecked(not self.rich_help)
        self.rich_text_action.setChecked(self.rich_help)
        self.source_changed()

        # Main layout
        layout = create_plugin_layout(layout_edit)
        # we have two main widgets, but only one of them is shown at a time
        layout.addWidget(self.plain_text)
        layout.addWidget(self.rich_text)
        self.setLayout(layout)

        # Add worker thread for handling rich text rendering
        self._sphinx_thread = SphinxThread(html_text_no_doc=warning(
            self.no_doc_string, css_path=self.css_path),
                                           css_path=self.css_path)
        self._sphinx_thread.html_ready.connect(
            self._on_sphinx_thread_html_ready)
        self._sphinx_thread.error_msg.connect(self._on_sphinx_thread_error_msg)

        # Handle internal and external links
        view = self.rich_text.webview
        if not WEBENGINE:
            view.page().setLinkDelegationPolicy(
                QWebEnginePage.DelegateAllLinks)
        view.linkClicked.connect(self.handle_link_clicks)

        self._starting_up = True

    #------ SpyderPluginWidget API ---------------------------------------------
    def on_first_registration(self):
        """Action to be performed on first plugin registration"""
        self.tabify(self.main.variableexplorer)

    def get_plugin_title(self):
        """Return widget title"""
        return _('Help')

    def get_plugin_icon(self):
        """Return widget icon"""
        return ima.icon('help')

    def get_focus_widget(self):
        """
        Return the widget to give focus to when
        this plugin's dockwidget is raised on top-level
        """
        self.combo.lineEdit().selectAll()
        return self.combo

    def get_plugin_actions(self):
        """Return a list of actions related to plugin"""
        return [
            self.rich_text_action, self.plain_text_action,
            self.show_source_action, MENU_SEPARATOR, self.auto_import_action
        ]

    def register_plugin(self):
        """Register plugin in Spyder's main window"""
        self.focus_changed.connect(self.main.plugin_focus_changed)
        self.add_dockwidget()
        self.main.console.set_help(self)

        self.internal_shell = self.main.console.shell
        self.console = self.main.console

    def refresh_plugin(self):
        """Refresh widget"""
        if self._starting_up:
            self._starting_up = False
            self.switch_to_rich_text()
            self.show_intro_message()

    def update_font(self):
        """Update font from Preferences"""
        color_scheme = self.get_color_scheme()
        font = self.get_font()
        rich_font = self.get_font(rich_text=True)

        self.set_plain_text_font(font, color_scheme=color_scheme)
        self.set_rich_text_font(rich_font)

    def apply_plugin_settings(self, options):
        """Apply configuration file's plugin settings"""
        color_scheme_n = 'color_scheme_name'
        color_scheme_o = self.get_color_scheme()
        connect_n = 'connect_to_oi'
        wrap_n = 'wrap'
        wrap_o = self.get_option(wrap_n)
        self.wrap_action.setChecked(wrap_o)
        math_n = 'math'
        math_o = self.get_option(math_n)

        if color_scheme_n in options:
            self.set_plain_text_color_scheme(color_scheme_o)
        if wrap_n in options:
            self.toggle_wrap_mode(wrap_o)
        if math_n in options:
            self.toggle_math_mode(math_o)

        # To make auto-connection changes take place instantly
        self.main.editor.apply_plugin_settings(options=[connect_n])
        self.main.ipyconsole.apply_plugin_settings(options=[connect_n])

    #------ Public API (related to Help's source) -------------------------
    def source_is_console(self):
        """Return True if source is Console"""
        return self.source_combo.currentIndex() == 0

    def switch_to_editor_source(self):
        self.source_combo.setCurrentIndex(1)

    def switch_to_console_source(self):
        self.source_combo.setCurrentIndex(0)

    def source_changed(self, index=None):
        if self.source_is_console():
            # Console
            self.combo.show()
            self.object_edit.hide()
            self.show_source_action.setEnabled(True)
            self.auto_import_action.setEnabled(True)
        else:
            # Editor
            self.combo.hide()
            self.object_edit.show()
            self.show_source_action.setDisabled(True)
            self.auto_import_action.setDisabled(True)
        self.restore_text()

    def save_text(self, callback):
        if self.source_is_console():
            self._last_console_cb = callback
        else:
            self._last_editor_cb = callback

    def restore_text(self):
        if self.source_is_console():
            cb = self._last_console_cb
        else:
            cb = self._last_editor_cb
        if cb is None:
            if self.is_plain_text_mode():
                self.plain_text.clear()
            else:
                self.rich_text.clear()
        else:
            func = cb[0]
            args = cb[1:]
            func(*args)
            if get_meth_class_inst(func) is self.rich_text:
                self.switch_to_rich_text()
            else:
                self.switch_to_plain_text()

    #------ Public API (related to rich/plain text widgets) --------------------
    @property
    def find_widget(self):
        if self.plain_text.isVisible():
            return self.plain_text.find_widget
        else:
            return self.rich_text.find_widget

    def set_rich_text_font(self, font):
        """Set rich text mode font"""
        self.rich_text.set_font(font, fixed_font=self.get_font())

    def set_plain_text_font(self, font, color_scheme=None):
        """Set plain text mode font"""
        self.plain_text.set_font(font, color_scheme=color_scheme)

    def set_plain_text_color_scheme(self, color_scheme):
        """Set plain text mode color scheme"""
        self.plain_text.set_color_scheme(color_scheme)

    @Slot(bool)
    def toggle_wrap_mode(self, checked):
        """Toggle wrap mode"""
        self.plain_text.editor.toggle_wrap_mode(checked)
        self.set_option('wrap', checked)

    def toggle_math_mode(self, checked):
        """Toggle math mode"""
        self.set_option('math', checked)

    def is_plain_text_mode(self):
        """Return True if plain text mode is active"""
        return self.plain_text.isVisible()

    def is_rich_text_mode(self):
        """Return True if rich text mode is active"""
        return self.rich_text.isVisible()

    def switch_to_plain_text(self):
        """Switch to plain text mode"""
        self.rich_help = False
        self.plain_text.show()
        self.rich_text.hide()
        self.plain_text_action.setChecked(True)

    def switch_to_rich_text(self):
        """Switch to rich text mode"""
        self.rich_help = True
        self.plain_text.hide()
        self.rich_text.show()
        self.rich_text_action.setChecked(True)
        self.show_source_action.setChecked(False)

    def set_plain_text(self, text, is_code):
        """Set plain text docs"""

        # text is coming from utils.dochelpers.getdoc
        if type(text) is dict:
            name = text['name']
            if name:
                rst_title = ''.join([
                    '=' * len(name), '\n', name, '\n', '=' * len(name), '\n\n'
                ])
            else:
                rst_title = ''

            if text['argspec']:
                definition = ''.join(
                    ['Definition: ', name, text['argspec'], '\n'])
            else:
                definition = ''

            if text['note']:
                note = ''.join(['Type: ', text['note'], '\n\n----\n\n'])
            else:
                note = ''

            full_text = ''.join(
                [rst_title, definition, note, text['docstring']])
        else:
            full_text = text

        self.plain_text.set_text(full_text, is_code)
        self.save_text([self.plain_text.set_text, full_text, is_code])

    def set_rich_text_html(self, html_text, base_url):
        """Set rich text"""
        self.rich_text.set_html(html_text, base_url)
        self.save_text([self.rich_text.set_html, html_text, base_url])

    def show_loading_message(self):
        """Create html page to show while the documentation is generated."""
        loading_message = _("Retrieving documentation")
        loading_img = get_image_path('loading_sprites.png')
        if os.name == 'nt':
            loading_img = loading_img.replace('\\', '/')

        self.set_rich_text_html(
            loading(loading_message, loading_img, css_path=self.css_path),
            QUrl.fromLocalFile(self.css_path))

    def show_intro_message(self):
        intro_message_eq = _("Here you can get help of any object by pressing "
                             "%s in front of it, either on the Editor or the "
                             "Console.%s")
        intro_message_dif = _(
            "Here you can get help of any object by pressing "
            "%s in front of it on the Editor, or %s in front "
            "of it on the Console.%s")
        intro_message_common = _(
            "Help can also be shown automatically after writing "
            "a left parenthesis next to an object. You can "
            "activate this behavior in %s.")
        prefs = _("Preferences > Help")
        shortcut_editor = self.get_option('editor/inspect current object',
                                          section='shortcuts')
        shortcut_console = self.get_option('console/inspect current object',
                                           section='shortcuts')

        if sys.platform == 'darwin':
            shortcut_editor = shortcut_editor.replace('Ctrl', 'Cmd')
            shortcut_console = shortcut_console.replace('Ctrl', 'Cmd')

        if self.is_rich_text_mode():
            title = _("Usage")
            tutorial_message = _("New to Spyder? Read our")
            tutorial = _("tutorial")
            if shortcut_editor == shortcut_console:
                intro_message = (intro_message_eq + intro_message_common) % (
                    "<b>" + shortcut_editor + "</b>", "<br><br>",
                    "<i>" + prefs + "</i>")
            else:
                intro_message = (intro_message_dif + intro_message_common) % (
                    "<b>" + shortcut_editor + "</b>", "<b>" + shortcut_console
                    + "</b>", "<br><br>", "<i>" + prefs + "</i>")

            self.set_rich_text_html(
                usage(title,
                      intro_message,
                      tutorial_message,
                      tutorial,
                      css_path=self.css_path),
                QUrl.fromLocalFile(self.css_path))
        else:
            install_sphinx = "\n\n%s" % _("Please consider installing Sphinx "
                                          "to get documentation rendered in "
                                          "rich text.")
            if shortcut_editor == shortcut_console:
                intro_message = (intro_message_eq + intro_message_common) % (
                    shortcut_editor, "\n\n", prefs)
            else:
                intro_message = (intro_message_dif + intro_message_common) % (
                    shortcut_editor, shortcut_console, "\n\n", prefs)

            intro_message += install_sphinx
            self.set_plain_text(intro_message, is_code=False)

    def show_rich_text(self, text, collapse=False, img_path=''):
        """Show text in rich mode"""
        self.switch_to_plugin()
        self.switch_to_rich_text()
        context = generate_context(collapse=collapse,
                                   img_path=img_path,
                                   css_path=self.css_path)
        self.render_sphinx_doc(text, context)

    def show_plain_text(self, text):
        """Show text in plain mode"""
        self.switch_to_plugin()
        self.switch_to_plain_text()
        self.set_plain_text(text, is_code=False)

    @Slot()
    def show_tutorial(self):
        """Show the Spyder tutorial in the Help plugin, opening it if needed"""
        self.switch_to_plugin()
        tutorial_path = get_module_source_path('spyder.plugins.help.utils')
        tutorial = osp.join(tutorial_path, 'tutorial.rst')
        text = open(tutorial).read()
        self.show_rich_text(text, collapse=True)

    def handle_link_clicks(self, url):
        url = to_text_string(url.toString())
        if url == "spy://tutorial":
            self.show_tutorial()
        elif url.startswith('http'):
            programs.start_file(url)
        else:
            self.rich_text.webview.load(QUrl(url))

    # ------ Public API -------------------------------------------------------
    @Slot()
    @Slot(bool)
    @Slot(bool, bool)
    def force_refresh(self, valid=True, editing=True):
        if valid:
            if self.source_is_console():
                self.set_object_text(None, force_refresh=True)
            elif self._last_editor_doc is not None:
                self.set_editor_doc(self._last_editor_doc, force_refresh=True)

    def set_object_text(self, text, force_refresh=False, ignore_unknown=False):
        """Set object analyzed by Help"""
        if (self.locked and not force_refresh):
            return
        self.switch_to_console_source()

        add_to_combo = True
        if text is None:
            text = to_text_string(self.combo.currentText())
            add_to_combo = False

        found = self.show_help(text, ignore_unknown=ignore_unknown)
        if ignore_unknown and not found:
            return

        if add_to_combo:
            self.combo.add_text(text)
        if found:
            self.save_history()

        if self.dockwidget is not None:
            self.dockwidget.blockSignals(True)
        self.__eventually_raise_help(text, force=force_refresh)
        if self.dockwidget is not None:
            self.dockwidget.blockSignals(False)

    def set_editor_doc(self, doc, force_refresh=False):
        """
        Use the help plugin to show docstring dictionary computed
        with introspection plugin from the Editor plugin
        """
        if (self.locked and not force_refresh):
            return
        self.switch_to_editor_source()
        self._last_editor_doc = doc
        self.object_edit.setText(doc['obj_text'])

        if self.rich_help:
            self.render_sphinx_doc(doc)
        else:
            self.set_plain_text(doc, is_code=False)

        if self.dockwidget is not None:
            self.dockwidget.blockSignals(True)
        self.__eventually_raise_help(doc['docstring'], force=force_refresh)
        if self.dockwidget is not None:
            self.dockwidget.blockSignals(False)

    def __eventually_raise_help(self, text, force=False):
        index = self.source_combo.currentIndex()
        if hasattr(self.main, 'tabifiedDockWidgets'):
            # 'QMainWindow.tabifiedDockWidgets' was introduced in PyQt 4.5
            if (self.dockwidget and (force or self.dockwidget.isVisible())
                    and not self._ismaximized
                    and (force or text != self._last_texts[index])):
                dockwidgets = self.main.tabifiedDockWidgets(self.dockwidget)
                if (self.console.dockwidget not in dockwidgets
                        and self.main.ipyconsole is not None and
                        self.main.ipyconsole.dockwidget not in dockwidgets):
                    self.switch_to_plugin()
        self._last_texts[index] = text

    def load_history(self, obj=None):
        """Load history from a text file in user home directory"""
        if osp.isfile(self.LOG_PATH):
            history = [
                line.replace('\n', '')
                for line in open(self.LOG_PATH, 'r').readlines()
            ]
        else:
            history = []
        return history

    def save_history(self):
        """Save history to a text file in user home directory"""
        # Don't fail when saving search history to disk
        # See issues 8878 and 6864
        try:
            search_history = [
                to_text_string(self.combo.itemText(index))
                for index in range(self.combo.count())
            ]
            search_history = '\n'.join(search_history)
            open(self.LOG_PATH, 'w').write(search_history)
        except (UnicodeEncodeError, UnicodeDecodeError, EnvironmentError):
            pass

    @Slot(bool)
    def toggle_plain_text(self, checked):
        """Toggle plain text docstring"""
        if checked:
            self.docstring = checked
            self.switch_to_plain_text()
            self.force_refresh()
        self.set_option('rich_mode', not checked)

    @Slot(bool)
    def toggle_show_source(self, checked):
        """Toggle show source code"""
        if checked:
            self.switch_to_plain_text()
        self.docstring = not checked
        self.force_refresh()
        self.set_option('rich_mode', not checked)

    @Slot(bool)
    def toggle_rich_text(self, checked):
        """Toggle between sphinxified docstrings or plain ones"""
        if checked:
            self.docstring = not checked
            self.switch_to_rich_text()
        self.set_option('rich_mode', checked)

    @Slot(bool)
    def toggle_auto_import(self, checked):
        """Toggle automatic import feature"""
        self.combo.validate_current_text()
        self.set_option('automatic_import', checked)
        self.force_refresh()

    @Slot()
    def toggle_locked(self):
        """
        Toggle locked state
        locked = disable link with Console
        """
        self.locked = not self.locked
        self._update_lock_icon()

    def _update_lock_icon(self):
        """Update locked state icon"""
        icon = ima.icon('lock') if self.locked else ima.icon('lock_open')
        self.locked_button.setIcon(icon)
        tip = _("Unlock") if self.locked else _("Lock")
        self.locked_button.setToolTip(tip)

    def set_shell(self, shell):
        """Bind to shell"""
        self.shell = shell

    def get_shell(self):
        """
        Return shell which is currently bound to Help,
        or another running shell if it has been terminated
        """
        if (not hasattr(self.shell, 'get_doc')
                or (hasattr(self.shell, 'is_running')
                    and not self.shell.is_running())):
            self.shell = None
            if self.main.ipyconsole is not None:
                shell = self.main.ipyconsole.get_current_shellwidget()
                if shell is not None and shell.kernel_client is not None:
                    self.shell = shell
            if self.shell is None:
                self.shell = self.internal_shell
        return self.shell

    def render_sphinx_doc(self, doc, context=None, css_path=CSS_PATH):
        """Transform doc string dictionary to HTML and show it"""
        # Math rendering option could have changed
        if self.main.editor is not None:
            fname = self.main.editor.get_current_filename()
            dname = osp.dirname(fname)
        else:
            dname = ''
        self._sphinx_thread.render(doc,
                                   context,
                                   self.get_option('math'),
                                   dname,
                                   css_path=self.css_path)
        self.show_loading_message()

    def _on_sphinx_thread_html_ready(self, html_text):
        """Set our sphinx documentation based on thread result"""
        self._sphinx_thread.wait()
        self.set_rich_text_html(html_text, QUrl.fromLocalFile(self.css_path))

    def _on_sphinx_thread_error_msg(self, error_msg):
        """ Display error message on Sphinx rich text failure"""
        self._sphinx_thread.wait()
        self.plain_text_action.setChecked(True)
        sphinx_ver = programs.get_module_version('sphinx')
        QMessageBox.critical(
            self, _('Help'),
            _("The following error occured when calling "
              "<b>Sphinx %s</b>. <br>Incompatible Sphinx "
              "version or doc string decoding failed."
              "<br><br>Error message:<br>%s") % (sphinx_ver, error_msg))

    def show_help(self, obj_text, ignore_unknown=False):
        """Show help"""
        shell = self.get_shell()
        if shell is None:
            return
        obj_text = to_text_string(obj_text)

        if not shell.is_defined(obj_text):
            if self.get_option('automatic_import') and \
               self.internal_shell.is_defined(obj_text, force_import=True):
                shell = self.internal_shell
            else:
                shell = None
                doc = None
                source_text = None

        if shell is not None:
            doc = shell.get_doc(obj_text)
            source_text = shell.get_source(obj_text)

        is_code = False

        if self.rich_help:
            self.render_sphinx_doc(doc, css_path=self.css_path)
            return doc is not None
        elif self.docstring:
            hlp_text = doc
            if hlp_text is None:
                hlp_text = source_text
                if hlp_text is None:
                    hlp_text = self.no_doc_string
                    if ignore_unknown:
                        return False
        else:
            hlp_text = source_text
            if hlp_text is None:
                hlp_text = doc
                if hlp_text is None:
                    hlp_text = _("No source code available.")
                    if ignore_unknown:
                        return False
            else:
                is_code = True
        self.set_plain_text(hlp_text, is_code=is_code)
        return True
Exemplo n.º 29
0
class GitRepoModel(QtGui.QStandardItemModel):
    """Provides an interface into a git repository for browsing purposes."""

    model_updated = Signal()
    restore = Signal()

    def __init__(self, context, parent):
        QtGui.QStandardItemModel.__init__(self, parent)
        self.setColumnCount(len(Columns.ALL))

        self.context = context
        self.model = model = context.model
        self.entries = {}
        cfg = context.cfg
        self.turbo = cfg.get('cola.turbo', False)
        self.default_author = cfg.get('user.name', N_('Author'))
        self._parent = parent
        self._interesting_paths = set()
        self._interesting_files = set()
        self._runtask = qtutils.RunTask(parent=parent)

        self.model_updated.connect(self.refresh, type=Qt.QueuedConnection)

        model = context.model
        model.add_observer(model.message_updated, self._model_updated)

        self.file_icon = icons.file_text()
        self.dir_icon = icons.directory()

    def mimeData(self, indexes):
        context = self.context
        paths = qtutils.paths_from_indexes(self,
                                           indexes,
                                           item_type=GitRepoNameItem.TYPE)
        return qtutils.mimedata_from_paths(context, paths)

    def mimeTypes(self):
        return qtutils.path_mimetypes()

    def clear(self):
        self.entries.clear()
        super(GitRepoModel, self).clear()

    def hasChildren(self, index):
        if index.isValid():
            item = self.itemFromIndex(index)
            result = item.hasChildren()
        else:
            result = True
        return result

    def get(self, path, default=None):
        if not path:
            item = self.invisibleRootItem()
        else:
            item = self.entries.get(path, default)
        return item

    def create_row(self, path, create=True, is_dir=False):
        try:
            row = self.entries[path]
        except KeyError:
            if create:
                column = self.create_column
                row = self.entries[path] = [
                    column(c, path, is_dir) for c in Columns.ALL
                ]
            else:
                row = None
        return row

    def create_column(self, col, path, is_dir):
        """Creates a StandardItem for use in a treeview cell."""
        # GitRepoNameItem is the only one that returns a custom type()
        # and is used to infer selections.
        if col == Columns.NAME:
            item = GitRepoNameItem(path, is_dir)
        else:
            item = GitRepoItem(path)
        return item

    def populate(self, item):
        self.populate_dir(item, item.path + '/')

    def add_directory(self, parent, path):
        """Add a directory entry to the model."""
        # First, try returning an existing item
        current_item = self.get(path)
        if current_item is not None:
            return current_item[0]

        # Create model items
        row_items = self.create_row(path, is_dir=True)

        # Use a standard directory icon
        name_item = row_items[0]
        name_item.setIcon(self.dir_icon)
        parent.appendRow(row_items)

        return name_item

    def add_file(self, parent, path):
        """Add a file entry to the model."""

        file_entry = self.get(path)
        if file_entry is not None:
            return file_entry

        # Create model items
        row_items = self.create_row(path)
        name_item = row_items[0]

        # Use a standard file icon for the name field
        name_item.setIcon(self.file_icon)

        # Add file paths at the end of the list
        parent.appendRow(row_items)

        return name_item

    def populate_dir(self, parent, path):
        """Populate a subtree"""
        context = self.context
        dirs, paths = gitcmds.listdir(context, path)

        # Insert directories before file paths
        for dirname in dirs:
            dir_parent = parent
            if '/' in dirname:
                dir_parent = self.add_parent_directories(parent, dirname)
            self.add_directory(dir_parent, dirname)
            self.update_entry(dirname)

        for filename in paths:
            file_parent = parent
            if '/' in filename:
                file_parent = self.add_parent_directories(parent, filename)
            self.add_file(file_parent, filename)
            self.update_entry(filename)

    def add_parent_directories(self, parent, dirname):
        """Ensure that all parent directory entries exist"""
        sub_parent = parent
        parent_dir = utils.dirname(dirname)
        for dirname in utils.pathset(parent_dir):
            sub_parent = self.add_directory(sub_parent, dirname)
        return sub_parent

    def path_is_interesting(self, path):
        """Return True if path has a status."""
        return path in self._interesting_paths

    def get_paths(self, files=None):
        """Return paths of interest; e.g. paths with a status."""
        if files is None:
            files = self.get_files()
        return utils.add_parents(files)

    def get_files(self):
        model = self.model
        return set(model.staged + model.unstaged)

    def _model_updated(self):
        """Observes model changes and updates paths accordingly."""
        self.model_updated.emit()

    def refresh(self):
        old_files = self._interesting_files
        old_paths = self._interesting_paths
        new_files = self.get_files()
        new_paths = self.get_paths(files=new_files)

        if new_files != old_files or not old_paths:
            selected = self._parent.selected_paths()
            self.clear()
            self._initialize()
            self.restore.emit()

        # Existing items
        for path in sorted(new_paths.union(old_paths)):
            self.update_entry(path)

        self._interesting_files = new_files
        self._interesting_paths = new_paths

    def _initialize(self):
        self.setHorizontalHeaderLabels(Columns.text_values())
        self.entries = {}
        self._interesting_files = files = self.get_files()
        self._interesting_paths = self.get_paths(files=files)

        root = self.invisibleRootItem()
        self.populate_dir(root, './')

    def update_entry(self, path):
        if self.turbo or path not in self.entries:
            return  # entry doesn't currently exist
        context = self.context
        task = GitRepoInfoTask(context, self._parent, path,
                               self.default_author)
        self._runtask.start(task)
Exemplo n.º 30
0
class FitInteractiveTool(QObject):
    """
    Peak editing tool. Peaks can be added by clicking on the plot. Peak parameters can be edited with the mouse.
    """

    fit_range_changed = Signal(list)
    peak_added = Signal(int, float, float, float)
    peak_moved = Signal(int, float, float)
    peak_fwhm_changed = Signal(int, float)
    peak_type_changed = Signal(str)
    add_background_requested = Signal(str)
    add_other_requested = Signal(str)

    default_background = 'LinearBackground'

    def __init__(self, canvas, toolbar_manager, current_peak_type):
        """
        Create an instance of FitInteractiveTool.
        :param canvas: A MPL canvas to draw on.
        :param toolbar_manager: A helper object that checks and manipulates
            the state of the plot toolbar. It is necessary to disable this
            tool's editing when zoom/pan is enabled by the user.
        :param current_peak_type: A name of a peak fit function to create by default.
        """
        super(FitInteractiveTool, self).__init__()
        self.canvas = canvas
        self.toolbar_manager = toolbar_manager
        ax = canvas.figure.get_axes()[0]
        self.ax = ax
        xlim = ax.get_xlim()
        dx = (xlim[1] - xlim[0]) / 20.
        # The fitting range: [StartX, EndX]
        start_x = xlim[0] + dx
        end_x = xlim[1] - dx
        # The interactive range marker drawn on the canvas as vertical lines that represent the fitting range.
        self.fit_range = RangeMarker(canvas, 'green', start_x, end_x,
                                     'XMinMax', '--')
        self.fit_range.range_changed.connect(self.fit_range_changed)

        # A list of interactive peak markers
        self.peak_markers = []
        # A reference to the currently selected peak marker
        self.selected_peak = None
        # A width to set to newly created peaks
        self.fwhm = dx
        # The name of the currently selected peak
        self.current_peak_type = current_peak_type
        # A cache for peak function names to use in the add function dialog
        self.peak_names = []
        # A cache for background function names to use in the add function dialog
        self.background_names = []
        # A cache for names of function that are neither peaks or backgrounds to use in the add function dialog
        self.other_names = []

        # Connect MPL events to callbacks and store connection ids in a cache
        self._cids = []
        self._cids.append(canvas.mpl_connect('draw_event', self.draw_callback))
        self._cids.append(
            canvas.mpl_connect('motion_notify_event',
                               self.motion_notify_callback))
        self._cids.append(
            canvas.mpl_connect('button_press_event',
                               self.button_press_callback))
        self._cids.append(
            canvas.mpl_connect('button_release_event',
                               self.button_release_callback))

        # The mouse state machine that handles responses to the mouse events.
        self.mouse_state = StateMachine(self)

    def disconnect(self):
        """
        Disconnect the tool from everything
        """
        QObject.disconnect(self)
        for cid in self._cids:
            self.canvas.mpl_disconnect(cid)
        self.fit_range.remove()

    def draw_callback(self, event):
        """
        This is called at every canvas draw. Redraw the markers.
        :param event: Unused
        """
        self.fit_range.redraw()
        for pm in self.peak_markers:
            pm.redraw()

    def motion_notify_callback(self, event):
        """
        This is called when the mouse moves across the canvas
        :param event: An event object with information on the current mouse position
        """
        self.mouse_state.motion_notify_callback(event)

    def button_press_callback(self, event):
        """
        This is called when a mouse button is pressed inside the canvas
        :param event: An event object with information on the current mouse position
        """
        self.mouse_state.button_press_callback(event)

    def button_release_callback(self, event):
        """
        This is called when a mouse button is released inside the canvas
        :param event: An event object with information on the current mouse position
        """
        self.mouse_state.button_release_callback(event)

    def move_markers(self, event):
        """
        Move markers that need moving.
        :param event: A MPL mouse event.
        """
        x, y = event.xdata, event.ydata
        if x is None or y is None:
            return

        should_redraw = self.fit_range.mouse_move(x, y)
        for pm in self.peak_markers:
            should_redraw = pm.mouse_move(x, y) or should_redraw
        if should_redraw:
            self.canvas.draw()

    def start_move_markers(self, event):
        """
        Start moving markers under the mouse.
        :param event: A MPL mouse event.
        """
        x = event.xdata
        y = event.ydata
        if x is None or y is None:
            return
        self.fit_range.mouse_move_start(x, y)
        selected_peak = None
        for pm in self.peak_markers:
            pm.mouse_move_start(x, y)
            if pm.is_moving:
                selected_peak = pm
        if selected_peak is not None:
            self.select_peak(selected_peak)
            self.canvas.draw()

    def stop_move_markers(self, event):
        """
        Stop moving all markers.
        """
        self.fit_range.mouse_move_stop()
        for pm in self.peak_markers:
            pm.mouse_move_stop()

    def set_fit_range(self, start_x, end_x):
        """
        Change the fit range when it has been changed in the FitPropertyBrowser.
        :param start_x: New value of StartX
        :param end_x: New value of EndX
        """
        if start_x is not None and end_x is not None:
            self.fit_range.set_range(start_x, end_x)
            self.canvas.draw()

    def _make_peak_id(self):
        """
        Generate a new peak id. Ids of deleted markers can be reused.
        :return: An integer id that is unique among self.peak_markers.
        """
        ids = set([pm.peak_id for pm in self.peak_markers])
        n = 0
        for i in range(len(ids)):
            if i in ids:
                if i > n:
                    n = i
            else:
                return i
        return n + 1

    def add_default_peak(self):
        """
        A QAction callback. Start adding a new peak. The tool will expect the user to click on the canvas to
        where the peak should be placed.
        """
        self.mouse_state.transition_to('add_peak')

    def add_peak_dialog(self):
        """
        A QAction callback. Start a dialog to choose a peak function name. After that the tool will expect the user
        to click on the canvas to where the peak should be placed.
        """
        selected_name = QInputDialog.getItem(
            self.canvas, 'Fit', 'Select peak function', self.peak_names,
            self.peak_names.index(self.current_peak_type), False)
        if selected_name[1]:
            self.peak_type_changed.emit(selected_name[0])
            self.mouse_state.transition_to('add_peak')

    def add_background_dialog(self):
        """
        A QAction callback. Start a dialog to choose a background function name. The new function is added to the
        browser.
        """
        current_index = self.background_names.index(self.default_background)
        if current_index < 0:
            current_index = 0
        selected_name = QInputDialog.getItem(self.canvas, 'Fit',
                                             'Select background function',
                                             self.background_names,
                                             current_index, False)
        if selected_name[1]:
            self.add_background_requested.emit(selected_name[0])

    def add_other_dialog(self):
        """
        A QAction callback. Start a dialog to choose a name of a function except a peak or a background. The new
        function is added to the browser.
        """
        selected_name = QInputDialog.getItem(self.canvas, 'Fit',
                                             'Select function',
                                             self.other_names, 0, False)
        if selected_name[1]:
            self.add_other_requested.emit(selected_name[0])

    def add_peak_marker(self, x, y_top, y_bottom=0.0, fwhm=None):
        """
        Add a new peak marker. No signal is sent to the fit browser.
        :param x: The peak centre.
        :param y_top: The y coordinate of the top of the peak.
        :param y_bottom: The y coordinate of the bottom of the peak (background level).
        :param fwhm: A full width at half maximum. If None use the value of the FWHM of the last edited peak.
        :return: An instance of PeakMarker.
        """
        if fwhm is None:
            fwhm = self.fwhm
        peak_id = self._make_peak_id()
        peak = PeakMarker(self.canvas, peak_id, x, y_top, y_bottom, fwhm=fwhm)
        peak.peak_moved.connect(self.peak_moved)
        peak.fwhm_changed.connect(self.peak_fwhm_changed_slot)
        self.peak_markers.append(peak)
        return peak

    def add_peak(self, x, y_top, y_bottom=0.0):
        """
        Add a new peak marker and send a signal to the fit browser to add a new peak function.
        :param x: The peak centre.
        :param y_top: The y coordinate of the top of the peak.
        :param y_bottom: The y coordinate of the bottom of the peak (background level).
        """
        peak = self.add_peak_marker(x, y_top, y_bottom)
        self.select_peak(peak)
        self.canvas.draw()
        self.peak_added.emit(peak.peak_id, x, peak.height(), peak.fwhm())

    def update_peak(self, peak_id, centre, height, fwhm):
        """
        Update a peak marker.
        :param peak_id: An id of the marker to update.
        :param centre: A new peak centre.
        :param height: A new peak height.
        :param fwhm: A new peak width.
        """
        for pm in self.peak_markers:
            if pm.peak_id == peak_id:
                pm.update_peak(centre, height, fwhm)
        self.canvas.draw()

    def select_peak(self, peak):
        """
        Make a peak marker selected. Deselect all others.
        :param peak: An instance of PeakMarker to select.
        """
        self.selected_peak = None
        for pm in self.peak_markers:
            if peak == pm:
                pm.select()
                self.selected_peak = peak
            else:
                pm.deselect()

    def _get_default_height(self):
        """
        Calculate the value of the default peak height to set to peaks added by the user to the fit property browser
        directly.
        """
        ylim = self.ax.get_ylim()
        return (ylim[0] + ylim[1]) / 2

    def get_peak_list(self):
        """
        get a list of peak parameters as tuples of (id, centre, height, fwhm).
        """
        plist = []
        for pm in self.peak_markers:
            plist.append((pm.peak_id, pm.centre(), pm.height(), pm.fwhm()))
        return plist

    def update_peak_markers(self, peaks_to_keep, peaks_to_add):
        """
        Update the peak marker list.
        :param peaks_to_keep: A list of ids of the peaks that should be kept. Markers with ids not found in this list
            will be removed.
        :param peaks_to_add: Parameters of peaks to add as a list of tuples (prefix, centre, height, fwhm).
        :return: A tuple of: first item: {map of peak id -> prefix},
                             second item: a list of (prefix, centre, height, fwhm) for those added peaks that had
                                          their parameters changed and need to be updated in the fit browser.
                                          Parameters are changed if the added peak has zero height or width.
        """
        peaks_to_remove = []
        for i, pm in enumerate(self.peak_markers):
            if pm.peak_id not in peaks_to_keep:
                peaks_to_remove.append(i)
        peaks_to_remove.sort(reverse=True)
        for i in peaks_to_remove:
            self.peak_markers[i].remove()
            del self.peak_markers[i]
        peak_ids = {}
        peak_updates = []
        for prefix, c, h, w in peaks_to_add:
            do_updates = False
            if h == 0.0:
                h = self._get_default_height()
                do_updates = True
            if w <= 0:
                w = self.fwhm
                do_updates = True
            pm = self.add_peak_marker(c, h, fwhm=w)
            peak_ids[pm.peak_id] = prefix
            if do_updates:
                peak_updates.append((prefix, c, h, w))
        self.canvas.draw()
        return peak_ids, peak_updates

    @Slot(int, float)
    def peak_fwhm_changed_slot(self, peak_id, fwhm):
        """
        Respond to a peak marker changing its width.
        :param peak_id: Marker's peak id.
        :param fwhm: A new fwhm value.
        """
        self.fwhm = fwhm
        self.peak_fwhm_changed.emit(peak_id, fwhm)

    def get_transform(self):
        """
        Get the MPL transform object used to draw the markers. Used by the unit tests.
        """
        return self.fit_range.patch.get_transform()

    def add_to_menu(self, menu, peak_names, current_peak_type,
                    background_names, other_names):
        """
        Adds the fit tool menu actions to the given menu and returns the menu

        :param menu: A reference to a menu that will accept the actions
        :param peak_names: A list of registered fit function peak names to be offered to choose from by the "Add a peak"
            dialog.
        :param current_peak_type:
        :param background_names: A list of registered background fit functions to be offered to choose from by the
            "Add a background" dialog.
        :param other_names:  A list of other registered fit functions to be offered to choose from by the
            "Add other function" dialog.
        :returns: The menu reference passed in
        """
        self.peak_names = peak_names
        self.current_peak_type = current_peak_type
        self.background_names = background_names
        self.other_names = other_names
        if not self.toolbar_manager.is_tool_active():
            menu.addAction("Add peak", self.add_default_peak)
            menu.addAction("Select peak type", self.add_peak_dialog)
            menu.addAction("Add background", self.add_background_dialog)
            menu.addAction("Add other function", self.add_other_dialog)

        return menu