class Channel(QtCore.QObject): finished = Signal(object) result = Signal(object)
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, '')
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)
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]))
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)
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()
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))
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()))
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()
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]
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)))
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)
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])
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)
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()
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)
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
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))
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()
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
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('-', '‑') # 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
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()
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))
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 {})
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)
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()
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
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
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)
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