class ProjectExplorer(QtCore.QObject): """ Displays project content in a treeview, manage the list of open projects and provides the list of project files to other plugins. This is a central plugin that all workspace should include! """ project_only = True preferred_position = 0 def __init__(self, window): super().__init__() self.main_window = window self.parser_plugins = [] self._indexors = [] self._locator = LocatorWidget(self.main_window) self._locator.activated.connect(self._on_locator_activated) self._locator.cancelled.connect(self._on_locator_cancelled) self._widget = QtWidgets.QWidget(self.main_window) self._job_runner = DelayJobRunner() self._cached_files = [] self._running_tasks = False self._widget.installEventFilter(self) self._setup_filesystem_treeview() self._setup_prj_selector_widget(self.main_window) self._setup_dock_widget() self._setup_tab_bar_context_menu(self.main_window) self._setup_locator() self._setup_project_menu() self._load_parser_plugins() api.signals.connect_slot(api.signals.CURRENT_EDITOR_CHANGED, self._on_current_editor_changed) def activate(self): for p in api.project.get_projects(): self._indexors.append(ProjectIndexor(p, self.main_window)) def _load_parser_plugins(self): _logger().debug('loading symbol parser plugins') for entrypoint in pkg_resources.iter_entry_points( api.plugins.SymbolParserPlugin.ENTRYPOINT): _logger().debug(' - loading plugin: %s', entrypoint) try: plugin = entrypoint.load()() except ImportError: _logger().exception('failed to load plugin') else: self.parser_plugins.append(plugin) _logger().debug(' - plugin loaded: %s', entrypoint) _logger().debug('indexor plugins: %r', self.parser_plugins) @staticmethod def get_ignored_patterns(): patterns = utils.get_ignored_patterns() prj_path = api.project.get_root_project() # project specific ignore patterns usd = api.project.load_user_config(prj_path) try: patterns += usd['ignored_patterns'] except KeyError: _logger().debug('no ignored patterns found in user config') return patterns def _run_update_projects_model_thread(self): if not self._running_tasks: self._running_tasks += 1 for project_dir in api.project.get_projects(): db_path = os.path.join(project_dir, '.hackedit', 'project.db') api.tasks.start( _('Indexing project files (%r)') % os.path.split( project_dir)[1], index_project_files, self._on_file_list_available, args=(db_path, project_dir, self.get_ignored_patterns(), self.parser_plugins), cancellable=True) def close(self): # save active project in first open project paths = api.project.get_projects() data = load_user_config(paths[0]) data['active_project'] = self._combo_projects.currentData() try: save_user_config(paths[0], data) except PermissionError: _logger().warn('failed to save user config to %r, ' 'permission error', paths[0]) self.main_window = None self._locator.window = None self._locator = None def apply_preferences(self): new_patterns = ';'.join(utils.get_ignored_patterns()) if self._last_ignore_patterns != new_patterns: self._last_ignore_patterns = new_patterns self.view.clear_ignore_patterns() self.view.add_ignore_patterns(self.get_ignored_patterns()) self.view.set_root_path(api.project.get_current_project()) self.view.context_menu.update_show_in_explorer_action() self._tab_bar_action_show_in_explorer.setText( _('Show in %s') % FileSystemContextMenu.get_file_explorer_name()) self._update_workspaces_menu() self.action_goto_anything.setShortcut(shortcuts.get( 'Goto anything', _('Goto anything'), 'Ctrl+P')) self.action_goto_symbol.setShortcut(shortcuts.get( 'Goto symbol', _('Goto symbol'), 'Ctrl+R')) self.action_goto_symbol_in_project.setShortcut(shortcuts.get( 'Goto symbol in project', _('Goto symbol in project'), 'Ctrl+Shift+R')) self.action_goto_line.setShortcut(shortcuts.get( 'Goto line', _('Goto line'), 'Ctrl+G')) self._update_templates_menu() menu = api.window.get_menu(_('&Goto')) for action in menu.actions(): action.setEnabled(settings.indexing_enabled()) tab = api.editor.get_current_editor() self.action_goto_line.setEnabled(tab is not None) if self.main_window.app.flg_force_indexing: self._reindex_all_projects() if not settings.indexing_enabled(): for indexor in self._indexors: indexor.cancel() def _on_file_list_available(self, status): self._running_tasks = False if status is False: # too much file indexed, display a warning to let the user know # he should ignore some unwanted directories. event = api.events.Event( _('Project directory contains too much files for indexing...'), _('You might want to mark the directories that contains ' 'non-project files as ignored (<i>Project View -> Right ' 'click on a directory -> Mark as ignored</i>)'), level=api.events.WARNING) api.events.post(event) self.main_window.project_files_available.emit() _logger().debug('project model updated') def _setup_tab_bar_context_menu(self, window): text = _('Show in %s') % FileSystemContextMenu.get_file_explorer_name() action = QtWidgets.QAction(text, window) action.setToolTip(text) action.setIcon(QtGui.QIcon.fromTheme('system-file-manager')) action.triggered.connect(self._on_show_in_explorer_triggered) api.window.add_tab_widget_context_menu_action(action) self._tab_bar_action_show_in_explorer = action def _setup_dock_widget(self): layout = QtWidgets.QVBoxLayout() layout.addWidget(self._widget_active_projects) layout.addWidget(self.view) layout.setContentsMargins(3, 3, 3, 3) self._widget.setLayout(layout) dock = api.window.add_dock_widget( self._widget, _('Project'), QtGui.QIcon.fromTheme('folder'), QtCore.Qt.LeftDockWidgetArea) dock.show() def _setup_project_menu(self): menu = api.window.get_menu(_('&View')) self.workspaces_menu = menu.addMenu(_('Workspaces')) menu.addSeparator() def _update_workspaces_menu(self): if QtGui.QIcon.hasThemeIcon('preferences-system-windows'): icon = QtGui.QIcon.fromTheme('preferences-system-windows') else: icon = QtGui.QIcon.fromTheme('applications-interfacedesign') self.workspaces_menu.clear() ag = QtWidgets.QActionGroup(self.workspaces_menu) for w in WorkspaceManager().get_names(): a = QtWidgets.QAction(w, self.workspaces_menu) a.setCheckable(True) a.setChecked(w == self.main_window.workspace['name']) a.setIcon(icon) ag.addAction(a) self.workspaces_menu.addAction(a) ag.triggered.connect(self._on_workspace_action_clicked) @QtCore.pyqtSlot(QtWidgets.QAction) def _on_workspace_action_clicked(self, action): prj = api.project.get_root_project() open_path = self.main_window.app.open_path api.project.save_workspace(prj, action.text().replace('&', '')) self.main_window.save_state(*self.main_window.get_session_info()) window = self.main_window open_path(prj, force=True) QtCore.QTimer.singleShot(1, window.close) def _setup_locator(self): menu = api.window.get_menu(_('&Goto')) self.action_goto_anything = menu.addAction(_('Goto anything...')) self.action_goto_anything.setShortcut(shortcuts.get( 'Goto anything', _('Goto anything'), 'Ctrl+P')) self.main_window.addAction(self.action_goto_anything) self.action_goto_anything.triggered.connect(self._goto_anything) menu.addSeparator() self.action_goto_symbol = menu.addAction(_('Goto symbol...')) self.action_goto_symbol.setShortcut(shortcuts.get( 'Goto symbol', _('Goto symbol'), 'Ctrl+R')) self.main_window.addAction(self.action_goto_symbol) self.action_goto_symbol.triggered.connect(self._goto_symbol) self.action_goto_symbol_in_project = menu.addAction( _('Goto symbol in project...')) self.action_goto_symbol_in_project.setShortcut(shortcuts.get( 'Goto symbol in project', _('Goto symbol in project'), 'Ctrl+Shift+R')) self.main_window.addAction(self.action_goto_symbol_in_project) self.action_goto_symbol_in_project.triggered.connect( self._goto_symbol_in_project) self.action_goto_line = menu.addAction(_('Goto line')) self.action_goto_line.setShortcut(shortcuts.get( 'Goto line', _('Goto line'), 'Ctrl+G')) self.main_window.addAction(self.action_goto_line) self.action_goto_line.triggered.connect(self._goto_line) menu.addSeparator() indexing_menu = menu.addMenu('Indexing') action = indexing_menu.addAction('Update project(s) index') action.setToolTip('Update project index database...') action.triggered.connect(self._reindex_all_projects) action = indexing_menu.addAction('Force full project(s) indexation') action.setToolTip('Invalidate project index and force a full ' 'reindexation...') action.triggered.connect(self._force_reindex_all_projects) def _goto_anything(self): self._locator.mode = self._locator.MODE_GOTO_ANYTHING self._show_locator() def _goto_symbol(self): try: self._cached_cursor_position = TextHelper( api.editor.get_current_editor()).cursor_position() except (TypeError, AttributeError): pass # no current editor or not a code edit else: self._locator.mode = self._locator.MODE_GOTO_SYMBOL self._show_locator() def _goto_symbol_in_project(self): self._locator.mode = self._locator.MODE_GOTO_SYMBOL_IN_PROJECT self._show_locator() def _goto_line(self): self._cached_cursor_position = TextHelper( api.editor.get_current_editor()).cursor_position() self._locator.mode = self._locator.MODE_GOTO_LINE self._show_locator() def _show_locator(self): widget = api.window.get_main_window_ui().stackedWidget parent_pos = widget.pos() parent_size = widget.size() w = parent_size.width() * 0.8 self._locator.setMinimumWidth(parent_size.width() * 0.8) x, y = parent_pos.x(), parent_pos.y() pw = parent_size.width() x += pw / 2 - w / 2 self._locator.move(widget.mapToGlobal(QtCore.QPoint(x, y))) self._locator.show() def _setup_prj_selector_widget(self, window): self._widget_active_projects = QtWidgets.QWidget(window) layout = QtWidgets.QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) self._combo_projects = QtWidgets.QComboBox() layout.addWidget(self._combo_projects) self.load_project_combo() api.signals.connect_slot(api.signals.PROJECT_ADDED, self._on_path_added) api.signals.connect_slot(api.signals.CURRENT_PROJECT_CHANGED, self._on_current_proj_changed) self._set_active_project() self._combo_projects.currentIndexChanged.connect( self._on_current_index_changed) bt_refresh = QtWidgets.QToolButton() bt_refresh.setIcon(QtGui.QIcon.fromTheme('view-refresh')) bt_refresh.setToolTip( _('Refresh project tree view...')) bt_refresh.clicked.connect(self._refresh) layout.addWidget(bt_refresh) bt_remove_project = QtWidgets.QToolButton() bt_remove_project.setIcon(QtGui.QIcon.fromTheme('list-remove')) bt_remove_project.setToolTip( _('Remove project from view')) bt_remove_project.clicked.connect(self._remove_current_project) layout.addWidget(bt_remove_project) self._widget_active_projects.setLayout(layout) self.bt_rm_proj = bt_remove_project if self._combo_projects.count() == 1: self.bt_rm_proj.hide() def load_project_combo(self, current_index=None): if current_index is None: current_index = self._combo_projects.currentIndex() project_paths = api.project.get_projects() with api.utils.block_signals(self._combo_projects): self._combo_projects.clear() for pth in project_paths: index = self._combo_projects.count() if os.path.ismount(pth): if api.system.WINDOWS and not pth.endswith('\\'): pth += '\\' name = pth else: name = os.path.split(pth)[1] self._combo_projects.addItem(name) self._combo_projects.setItemIcon( index, FileIconProvider().icon(QtCore.QFileInfo(pth))) self._combo_projects.setItemData(index, pth) self._combo_projects.setCurrentIndex(current_index) def _setup_filesystem_treeview(self): self.view = FileSystemTreeView(self._widget) self.view.setMinimumWidth(200) self._last_ignore_patterns = ';'.join(utils.get_ignored_patterns()) self.view.add_ignore_patterns(self.get_ignored_patterns()) self.view.file_created.connect(self._on_file_created) self.view.files_deleted.connect(self._on_files_deleted) self.view.files_renamed.connect(self._on_files_renamed) self.main_window.document_saved.connect(self._on_file_saved) self.view.set_icon_provider(FileIconProvider()) context_menu = FileSystemContextMenu() self.view.set_context_menu(context_menu) self.templates_menu = context_menu.menu_new.addSeparator() self.templates_menu = context_menu.menu_new.addMenu( _('Templates')) self.templates_menu.menuAction().setIcon(QtGui.QIcon.fromTheme( 'folder-templates')) self._update_templates_menu() self.view.activated.connect(self._on_file_activated) self.action_mark_as_ignored = QtWidgets.QAction( _('Mark as ignored'), self.view) self.action_mark_as_ignored.triggered.connect( self._on_mark_as_ignored) self.action_mark_as_ignored.setIcon(QtGui.QIcon.fromTheme( 'emblem-unreadable')) self.view.context_menu.insertAction( self.view.context_menu.action_show_in_explorer, self.action_mark_as_ignored) self.view.context_menu.insertSeparator( self.view.context_menu.action_show_in_explorer) self.action_show_in_terminal = QtWidgets.QAction( QtGui.QIcon.fromTheme('utilities-terminal'), _('Open in terminal'), self.view) self.action_show_in_terminal.triggered.connect( self._on_show_in_terminal_triggered) self.view.context_menu.addAction(self.action_show_in_terminal) self.action_open_in_browser = QtWidgets.QAction( QtGui.QIcon.fromTheme('applications-internet'), _('Open in web browser'), self.view) self.action_open_in_browser.triggered.connect( self._on_action_open_in_browser_triggered) self.view.context_menu.addAction(self.action_open_in_browser) insert_pt = self.view.context_menu.menu_new.menuAction() action = QtWidgets.QAction(_('Execute file'), self.main_window) action.setToolTip(_('Run executable')) action.setIcon(api.special_icons.run_icon()) action.triggered.connect(self._execute_file) self.action_exec_file = action self.view.context_menu.insertAction(insert_pt, action) action = QtWidgets.QAction(self.main_window) action.setSeparator(True) self.view.context_menu.insertAction(insert_pt, action) self.view.about_to_show_context_menu.connect( self._on_about_to_show_context_menu) def _update_templates_menu(self): self.templates_menu.clear() sources = {} for src in templates.get_sources(): lbl = src['label'] icon = QtGui.QIcon.fromTheme('folder-templates') menu = self.templates_menu.addMenu(icon, lbl) sources[lbl] = menu for template in templates.get_templates(category='File'): icon = template['icon'] lbl = template['source']['label'] if icon.startswith(':') or os.path.exists(icon): icon = QtGui.QIcon(icon) elif icon.startswith('file.'): icon = FileIconProvider().icon(icon) else: icon = QtGui.QIcon.fromTheme(icon) a = sources[lbl].addAction(icon, template['name']) a.setData((lbl, template)) a.triggered.connect(self._add_file_from_template) for menu in sources.values(): menu.menuAction().setVisible(len(menu.actions()) > 0) def _on_mark_as_ignored(self): path = FileSystemHelper(self.view).get_current_path() prj = api.project.get_root_project() usd = api.project.load_user_config(prj) try: patterns = usd['ignored_patterns'] except KeyError: patterns = [] finally: name = os.path.split(path)[1] pattern = DlgIgnore.get_ignore_pattern(self.main_window, name) if pattern: patterns.append(pattern) usd['ignored_patterns'] = patterns api.project.save_user_config(prj, usd) self.view.clear_ignore_patterns() self.view.add_ignore_patterns(self.get_ignored_patterns()) # force reload self.view.set_root_path(api.project.get_current_project()) self._reindex_all_projects() def _on_about_to_show_context_menu(self, path): try: is_html = 'text/html' in mimetypes.guess_type(path)[0] except TypeError: is_html = False # mimetype is None, not iterable self.action_open_in_browser.setVisible(is_html) if api.system.WINDOWS: is_executable = path.endswith('.exe') or path.endswith('.bat') else: is_executable = os.access(path, os.X_OK) and os.path.isfile(path) self.action_exec_file.setVisible(is_executable) def _on_action_open_in_browser_triggered(self): path = FileSystemHelper(self.view).get_current_path() if settings.use_default_browser(): webbrowser.open_new_tab(path) else: cmd = utils.get_custom_browser_command() % path try: subprocess.Popen(cmd) except (OSError, subprocess.CalledProcessError): _logger().exception('failed to open file in browser (cmd=%s)', cmd) def _execute_file(self): path = FileSystemHelper(self.view).get_current_path() run_widget = api.window.get_run_widget() assert isinstance(run_widget, api.widgets.RunWidget) run_widget.run_program(path, cwd=os.path.dirname(path)) def _on_show_in_terminal_triggered(self): path = FileSystemHelper(self.view).get_current_path() if os.path.isfile(path): path = os.path.dirname(path) cmd = utils.get_cmd_open_folder_in_terminal() % path try: if api.system.WINDOWS: subprocess.Popen( cmd, creationflags=subprocess.CREATE_NEW_CONSOLE) else: subprocess.Popen(shlex.split(cmd, posix=False)) except (OSError, subprocess.CalledProcessError): _logger().exception('failed to open directory in terminal') def _add_file_from_template(self): source, template = self.sender().data() path = FileSystemHelper(self.view).get_current_path() if os.path.isfile(path): path = os.path.dirname(path) path = common.create_new_from_template( source, template, path, True, self.main_window, self.main_window.app) self._on_file_created(path) @staticmethod def _on_show_in_explorer_triggered(): path = api.window.get_tab_under_context_menu().file.path FileSystemContextMenu.show_in_explorer( path, api.window.get_main_window()) def _on_file_activated(self, index): path = self.view.filePath(index) if path and os.path.isfile(path): _logger().debug('showing editor for %r', path) api.editor.open_file(path) def _on_current_editor_changed(self, tab): if tab is not None: self.view.select_path(tab.file.path) self.action_goto_line.setEnabled(tab is not None) self.action_goto_symbol.setEnabled( tab is not None and settings.indexing_enabled()) def _on_current_proj_changed(self, new_path): if api.project.get_current_project() != new_path: self.main_window.current_project = new_path self.view.set_root_path(new_path) def _set_active_project(self): paths = api.project.get_projects() try: data = load_user_config(paths[0]) except IndexError: return try: active_path = data['active_project'] if active_path is None or not os.path.exists(active_path): raise KeyError('active_project') except KeyError: active_path = paths[0] for i in range(self._combo_projects.count()): if self._combo_projects.itemData(i) == active_path: self._combo_projects.setCurrentIndex(i) self.view.set_root_path(active_path) break else: self.view.set_root_path(active_path) self.main_window.current_project = active_path def _on_path_added(self, path): if os.path.isfile(path): return index = self._combo_projects.count() self.load_project_combo(current_index=index) self._on_current_index_changed(index) self.bt_rm_proj.setVisible(self._combo_projects.count() > 1) data = load_user_config(api.project.get_projects()[0]) data['active_project'] = path self._indexors.append(ProjectIndexor(path, self.main_window)) def _on_current_index_changed(self, index): new_path = self._combo_projects.itemData(index) if new_path: self.main_window.current_project_changed.emit(new_path) def _refresh(self): self.view.set_root_path('/') self.view.set_root_path(api.project.get_current_project()) def _force_reindex_all_projects(self): for indexor in self._indexors: indexor.cancel() # wait a bit before removing the index database (to make sure # pending indexation tasks are actually canceled). QtCore.QTimer.singleShot(100, self._perform_full_reindexation) def _perform_full_reindexation(self): for project in api.project.get_projects(): index.remove_project(project) self._reindex_all_projects() def _reindex_all_projects(self): for indexor in self._indexors: indexor.cancel() QtCore.QTimer.singleShot(100, self._perform_reindexation) def _perform_reindexation(self): for indexor in self._indexors: indexor.perform_indexing() def _remove_current_project(self): path = self._combo_projects.itemData( self._combo_projects.currentIndex()) self._combo_projects.removeItem(self._combo_projects.currentIndex()) self.main_window.remove_folder(path) initial_path = api.project.get_projects()[0] data = load_user_config(initial_path) try: linked_paths = data['linked_paths'] except KeyError: linked_paths = [] try: linked_paths.remove(path) except ValueError: _logger().warn('failed to remove linked path %r, path not in list', path) finally: data['linked_paths'] = linked_paths save_user_config(initial_path, data) for indexor in self._indexors: if indexor.project == path: self._indexors.remove(indexor) def _on_file_created(self, path): api.editor.open_file(path) project_path = self._combo_projects.itemData(self._combo_projects.currentIndex()) with DbHelper() as dbh: try: dbh.create_file(path, project_id=index.get_project_ids([project_path])[0]) except (TypeError, IndexError): return else: self._update_document_index(path) def _on_file_saved(self, path): self._update_document_index(path) def _update_document_index(self, path): file = index.get_file(path) if not file: return project = None for p in index.get_all_projects(): if p.id == file.project_id: project = p break if not project: return test_path = 'file' + os.path.splitext(path)[1] plugin = get_symbol_parser(test_path, tuple(self.parser_plugins)) if not plugin: return args = (path, file.id, project.path, project.id, plugin) api.tasks.start('Update file index', update_file, None, args=args) def _on_files_renamed(self, renamed_files): for old_path, new_path in renamed_files: old_path = os.path.normpath(os.path.normcase(old_path)) new_path = os.path.normpath(os.path.normcase(new_path)) self.main_window.tab_widget.rename_document(old_path, new_path) self.main_window.file_renamed.emit(old_path, new_path) api.tasks.start('Update renamed files index', rename_files, None, args=(renamed_files,)) def _on_files_deleted(self, deleted_files): for path in deleted_files: path = os.path.normpath(os.path.normcase(path)) self.main_window.tab_widget.close_document(path) self.main_window.file_deleted.emit(path) api.tasks.start('Update renamed files index', delete_files, None, args=(deleted_files,)) @staticmethod def _on_locator_activated(path, line): if line == -1: line = None else: line -= 1 # 0 based api.editor.open_file(path, line=line) def _on_locator_cancelled(self): if self._locator.mode in [ self._locator.MODE_GOTO_LINE, self._locator.MODE_GOTO_SYMBOL]: line, col = self._cached_cursor_position TextHelper(api.editor.get_current_editor()).goto_line(line, col)
class ProjectExplorer(QtCore.QObject): """ Displays project content in a treeview, manage the list of open projects and provides the list of project files to other plugins. This is a central plugin that all workspace should include! """ project_only = True preferred_position = 0 def __init__(self, window): super().__init__() self.main_window = window self.parser_plugins = [] self._indexors = [] self._locator = LocatorWidget(self.main_window) self._locator.activated.connect(self._on_locator_activated) self._locator.cancelled.connect(self._on_locator_cancelled) self._widget = QtWidgets.QWidget(self.main_window) self._job_runner = DelayJobRunner() self._cached_files = [] self._running_tasks = False self._widget.installEventFilter(self) self._setup_filesystem_treeview() self._setup_prj_selector_widget(self.main_window) self._setup_dock_widget() self._setup_tab_bar_context_menu(self.main_window) self._setup_locator() self._setup_project_menu() self._load_parser_plugins() api.signals.connect_slot(api.signals.CURRENT_EDITOR_CHANGED, self._on_current_editor_changed) def activate(self): for p in api.project.get_projects(): self._indexors.append(ProjectIndexor(p, self.main_window)) def _load_parser_plugins(self): _logger().debug('loading symbol parser plugins') for entrypoint in pkg_resources.iter_entry_points( api.plugins.SymbolParserPlugin.ENTRYPOINT): _logger().debug(' - loading plugin: %s', entrypoint) try: plugin = entrypoint.load()() except ImportError: _logger().exception('failed to load plugin') else: self.parser_plugins.append(plugin) _logger().debug(' - plugin loaded: %s', entrypoint) _logger().debug('indexor plugins: %r', self.parser_plugins) @staticmethod def get_ignored_patterns(): patterns = utils.get_ignored_patterns() prj_path = api.project.get_root_project() # project specific ignore patterns usd = api.project.load_user_config(prj_path) try: patterns += usd['ignored_patterns'] except KeyError: _logger().debug('no ignored patterns found in user config') return patterns def _run_update_projects_model_thread(self): if not self._running_tasks: self._running_tasks += 1 for project_dir in api.project.get_projects(): db_path = os.path.join(project_dir, '.hackedit', 'project.db') api.tasks.start(_('Indexing project files (%r)') % os.path.split(project_dir)[1], index_project_files, self._on_file_list_available, args=(db_path, project_dir, self.get_ignored_patterns(), self.parser_plugins), cancellable=True) def close(self): # save active project in first open project paths = api.project.get_projects() data = load_user_config(paths[0]) data['active_project'] = self._combo_projects.currentData() try: save_user_config(paths[0], data) except PermissionError: _logger().warn( 'failed to save user config to %r, ' 'permission error', paths[0]) self.main_window = None self._locator.window = None self._locator = None def apply_preferences(self): new_patterns = ';'.join(utils.get_ignored_patterns()) if self._last_ignore_patterns != new_patterns: self._last_ignore_patterns = new_patterns self.view.clear_ignore_patterns() self.view.add_ignore_patterns(self.get_ignored_patterns()) self.view.set_root_path(api.project.get_current_project()) self.view.context_menu.update_show_in_explorer_action() self._tab_bar_action_show_in_explorer.setText( _('Show in %s') % FileSystemContextMenu.get_file_explorer_name()) self._update_workspaces_menu() self.action_goto_file.setShortcut( shortcuts.get('Goto file', _('Goto file'), 'Ctrl+T')) self.action_goto_symbol.setShortcut( shortcuts.get('Goto symbol', _('Goto symbol'), 'Ctrl+R')) self.action_goto_symbol_in_project.setShortcut( shortcuts.get('Goto symbol in project', _('Goto symbol in project'), 'Ctrl+Shift+R')) self.action_goto_line.setShortcut( shortcuts.get('Goto line', _('Goto line'), 'Ctrl+G')) self._update_templates_menu() menu = api.window.get_menu(_('&Goto')) for action in menu.actions(): action.setEnabled(settings.indexing_enabled()) tab = api.editor.get_current_editor() self.action_goto_line.setEnabled(tab is not None) if self.main_window.app.flg_force_indexing: self._reindex_all_projects() if not settings.indexing_enabled(): for indexor in self._indexors: indexor.cancel() def _on_file_list_available(self, status): self._running_tasks = False if status is False: # too much file indexed, display a warning to let the user know # he should ignore some unwanted directories. event = api.events.Event( _('Project directory contains too much files for indexing...'), _('You might want to mark the directories that contains ' 'non-project files as ignored (<i>Project View -> Right ' 'click on a directory -> Mark as ignored</i>)'), level=api.events.WARNING) api.events.post(event) self.main_window.project_files_available.emit() _logger().debug('project model updated') def _setup_tab_bar_context_menu(self, window): text = _('Show in %s') % FileSystemContextMenu.get_file_explorer_name() action = QtWidgets.QAction(text, window) action.setToolTip(text) action.setIcon(QtGui.QIcon.fromTheme('system-file-manager')) action.triggered.connect(self._on_show_in_explorer_triggered) api.window.add_tab_widget_context_menu_action(action) self._tab_bar_action_show_in_explorer = action def _setup_dock_widget(self): layout = QtWidgets.QVBoxLayout() layout.addWidget(self._widget_active_projects) layout.addWidget(self.view) layout.setContentsMargins(3, 3, 3, 3) self._widget.setLayout(layout) dock = api.window.add_dock_widget(self._widget, _('Project'), QtGui.QIcon.fromTheme('folder'), QtCore.Qt.LeftDockWidgetArea) dock.show() def _setup_project_menu(self): menu = api.window.get_menu(_('&View')) self.workspaces_menu = menu.addMenu(_('Workspaces')) menu.addSeparator() def _update_workspaces_menu(self): if QtGui.QIcon.hasThemeIcon('preferences-system-windows'): icon = QtGui.QIcon.fromTheme('preferences-system-windows') else: icon = QtGui.QIcon.fromTheme('applications-interfacedesign') self.workspaces_menu.clear() ag = QtWidgets.QActionGroup(self.workspaces_menu) for w in WorkspaceManager().get_names(): a = QtWidgets.QAction(w, self.workspaces_menu) a.setCheckable(True) a.setChecked(w == self.main_window.workspace['name']) a.setIcon(icon) ag.addAction(a) self.workspaces_menu.addAction(a) ag.triggered.connect(self._on_workspace_action_clicked) def _on_workspace_action_clicked(self, action): prj = api.project.get_root_project() open_path = self.main_window.app.open_path api.project.save_workspace(prj, action.text().replace('&', '')) self.main_window.save_state(*self.main_window.get_session_info()) window = self.main_window open_path(prj, force=True) QtCore.QTimer.singleShot(1, window.close) def _setup_locator(self): menu = api.window.get_menu(_('&Goto')) self.action_goto_file = menu.addAction(_('Goto file...')) self.action_goto_file.setShortcut( shortcuts.get('Goto file', _('Goto file'), 'Ctrl+T')) self.main_window.addAction(self.action_goto_file) self.action_goto_file.triggered.connect(self._goto_anything) menu.addSeparator() self.action_goto_symbol = menu.addAction(_('Goto symbol...')) self.action_goto_symbol.setShortcut( shortcuts.get('Goto symbol', _('Goto symbol'), 'Ctrl+R')) self.main_window.addAction(self.action_goto_symbol) self.action_goto_symbol.triggered.connect(self._goto_symbol) self.action_goto_symbol_in_project = menu.addAction( _('Goto symbol in project...')) self.action_goto_symbol_in_project.setShortcut( shortcuts.get('Goto symbol in project', _('Goto symbol in project'), 'Ctrl+Shift+R')) self.main_window.addAction(self.action_goto_symbol_in_project) self.action_goto_symbol_in_project.triggered.connect( self._goto_symbol_in_project) self.action_goto_line = menu.addAction(_('Goto line')) self.action_goto_line.setShortcut( shortcuts.get('Goto line', _('Goto line'), 'Ctrl+G')) self.main_window.addAction(self.action_goto_line) self.action_goto_line.triggered.connect(self._goto_line) menu.addSeparator() indexing_menu = menu.addMenu('Indexing') action = indexing_menu.addAction('Update project(s) index') action.setToolTip('Update project index database...') action.triggered.connect(self._reindex_all_projects) action = indexing_menu.addAction('Force full project(s) indexation') action.setToolTip('Invalidate project index and force a full ' 'reindexation...') action.triggered.connect(self._force_reindex_all_projects) def _goto_anything(self): self._locator.mode = self._locator.MODE_GOTO_FILE self._show_locator() def _goto_symbol(self): try: self._cached_cursor_position = TextHelper( api.editor.get_current_editor()).cursor_position() except (TypeError, AttributeError): pass # no current editor or not a code edit else: self._locator.mode = self._locator.MODE_GOTO_SYMBOL self._show_locator() def _goto_symbol_in_project(self): self._locator.mode = self._locator.MODE_GOTO_SYMBOL_IN_PROJECT self._show_locator() def _goto_line(self): self._cached_cursor_position = TextHelper( api.editor.get_current_editor()).cursor_position() self._locator.mode = self._locator.MODE_GOTO_LINE self._show_locator() def _show_locator(self): widget = api.window.get_main_window_ui().stackedWidget parent_pos = widget.pos() parent_size = widget.size() w = parent_size.width() * 0.8 self._locator.setMinimumWidth(parent_size.width() * 0.8) x, y = parent_pos.x(), parent_pos.y() pw = parent_size.width() x += pw / 2 - w / 2 self._locator.move(widget.mapToGlobal(QtCore.QPoint(x, y))) self._locator.show() def _setup_prj_selector_widget(self, window): self._widget_active_projects = QtWidgets.QWidget(window) layout = QtWidgets.QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) self._combo_projects = QtWidgets.QComboBox() layout.addWidget(self._combo_projects) self.load_project_combo() api.signals.connect_slot(api.signals.PROJECT_ADDED, self._on_path_added) api.signals.connect_slot(api.signals.CURRENT_PROJECT_CHANGED, self._on_current_proj_changed) self._set_active_project() self._combo_projects.currentIndexChanged.connect( self._on_current_index_changed) bt_refresh = QtWidgets.QToolButton() bt_refresh.setIcon(QtGui.QIcon.fromTheme('view-refresh')) bt_refresh.setToolTip(_('Refresh project tree view...')) bt_refresh.clicked.connect(self._refresh) layout.addWidget(bt_refresh) bt_remove_project = QtWidgets.QToolButton() bt_remove_project.setIcon(QtGui.QIcon.fromTheme('list-remove')) bt_remove_project.setToolTip(_('Remove project from view')) bt_remove_project.clicked.connect(self._remove_current_project) layout.addWidget(bt_remove_project) self._widget_active_projects.setLayout(layout) self.bt_rm_proj = bt_remove_project if self._combo_projects.count() == 1: self.bt_rm_proj.hide() def load_project_combo(self, current_index=None): if current_index is None: current_index = self._combo_projects.currentIndex() project_paths = api.project.get_projects() with api.utils.block_signals(self._combo_projects): self._combo_projects.clear() for pth in project_paths: index = self._combo_projects.count() if os.path.ismount(pth): if api.system.WINDOWS and not pth.endswith('\\'): pth += '\\' name = pth else: name = os.path.split(pth)[1] self._combo_projects.addItem(name) self._combo_projects.setItemIcon( index, FileIconProvider().icon(QtCore.QFileInfo(pth))) self._combo_projects.setItemData(index, pth) self._combo_projects.setCurrentIndex(current_index) def _setup_filesystem_treeview(self): self.view = FileSystemTreeView(self._widget) self.view.setMinimumWidth(200) self._last_ignore_patterns = ';'.join(utils.get_ignored_patterns()) self.view.add_ignore_patterns(self.get_ignored_patterns()) self.view.file_created.connect(self._on_file_created) self.view.files_deleted.connect(self._on_files_deleted) self.view.files_renamed.connect(self._on_files_renamed) self.main_window.document_saved.connect(self._on_file_saved) self.view.set_icon_provider(FileIconProvider()) context_menu = FileSystemContextMenu() self.view.set_context_menu(context_menu) self.templates_menu = context_menu.menu_new.addSeparator() self.templates_menu = context_menu.menu_new.addMenu(_('Templates')) self.templates_menu.menuAction().setIcon( QtGui.QIcon.fromTheme('folder-templates')) self._update_templates_menu() self.view.activated.connect(self._on_file_activated) self.action_mark_as_ignored = QtWidgets.QAction( _('Mark as ignored'), self.view) self.action_mark_as_ignored.triggered.connect(self._on_mark_as_ignored) self.action_mark_as_ignored.setIcon( QtGui.QIcon.fromTheme('emblem-unreadable')) self.view.context_menu.insertAction( self.view.context_menu.action_show_in_explorer, self.action_mark_as_ignored) self.view.context_menu.insertSeparator( self.view.context_menu.action_show_in_explorer) self.action_show_in_terminal = QtWidgets.QAction( QtGui.QIcon.fromTheme('utilities-terminal'), _('Open in terminal'), self.view) self.action_show_in_terminal.triggered.connect( self._on_show_in_terminal_triggered) self.view.context_menu.addAction(self.action_show_in_terminal) self.action_open_in_browser = QtWidgets.QAction( QtGui.QIcon.fromTheme('applications-internet'), _('Open in web browser'), self.view) self.action_open_in_browser.triggered.connect( self._on_action_open_in_browser_triggered) self.view.context_menu.addAction(self.action_open_in_browser) insert_pt = self.view.context_menu.menu_new.menuAction() action = QtWidgets.QAction(_('Execute file'), self.main_window) action.setToolTip(_('Run executable')) action.setIcon(api.special_icons.run_icon()) action.triggered.connect(self._execute_file) self.action_exec_file = action self.view.context_menu.insertAction(insert_pt, action) action = QtWidgets.QAction(self.main_window) action.setSeparator(True) self.view.context_menu.insertAction(insert_pt, action) self.view.about_to_show_context_menu.connect( self._on_about_to_show_context_menu) def _update_templates_menu(self): self.templates_menu.clear() sources = {} for src in templates.get_sources(): lbl = src['label'] icon = QtGui.QIcon.fromTheme('folder-templates') menu = self.templates_menu.addMenu(icon, lbl) sources[lbl] = menu for template in templates.get_templates(category='File'): icon = template['icon'] lbl = template['source']['label'] if icon.startswith(':') or os.path.exists(icon): icon = QtGui.QIcon(icon) elif icon.startswith('file.'): icon = FileIconProvider().icon(icon) else: icon = QtGui.QIcon.fromTheme(icon) a = sources[lbl].addAction(icon, template['name']) a.setData((lbl, template)) a.triggered.connect(self._add_file_from_template) for menu in sources.values(): menu.menuAction().setVisible(len(menu.actions()) > 0) def _on_mark_as_ignored(self): path = FileSystemHelper(self.view).get_current_path() prj = api.project.get_root_project() usd = api.project.load_user_config(prj) try: patterns = usd['ignored_patterns'] except KeyError: patterns = [] finally: name = os.path.split(path)[1] pattern = DlgIgnore.get_ignore_pattern(self.main_window, name) if pattern: patterns.append(pattern) usd['ignored_patterns'] = patterns api.project.save_user_config(prj, usd) self.view.clear_ignore_patterns() self.view.add_ignore_patterns(self.get_ignored_patterns()) # force reload self.view.set_root_path(api.project.get_current_project()) self._reindex_all_projects() def _on_about_to_show_context_menu(self, path): try: is_html = 'text/html' in mimetypes.guess_type(path)[0] except TypeError: is_html = False # mimetype is None, not iterable self.action_open_in_browser.setVisible(is_html) if api.system.WINDOWS: is_executable = path.endswith('.exe') or path.endswith('.bat') else: is_executable = os.access(path, os.X_OK) and os.path.isfile(path) self.action_exec_file.setVisible(is_executable) def _on_action_open_in_browser_triggered(self): path = FileSystemHelper(self.view).get_current_path() if settings.use_default_browser(): webbrowser.open_new_tab(path) else: cmd = utils.get_custom_browser_command() % path try: subprocess.Popen(cmd) except (OSError, subprocess.CalledProcessError): _logger().exception('failed to open file in browser (cmd=%s)', cmd) def _execute_file(self): path = FileSystemHelper(self.view).get_current_path() run_widget = api.window.get_run_widget() assert isinstance(run_widget, api.widgets.RunWidget) run_widget.run_program(path, cwd=os.path.dirname(path)) def _on_show_in_terminal_triggered(self): path = FileSystemHelper(self.view).get_current_path() if os.path.isfile(path): path = os.path.dirname(path) cmd = utils.get_cmd_open_folder_in_terminal() % path try: if api.system.WINDOWS: subprocess.Popen(cmd, creationflags=subprocess.CREATE_NEW_CONSOLE) else: subprocess.Popen(shlex.split(cmd, posix=False)) except (OSError, subprocess.CalledProcessError): _logger().exception('failed to open directory in terminal') def _add_file_from_template(self): source, template = self.sender().data() path = FileSystemHelper(self.view).get_current_path() if os.path.isfile(path): path = os.path.dirname(path) path = common.create_new_from_template(source, template, path, True, self.main_window, self.main_window.app) self._on_file_created(path) @staticmethod def _on_show_in_explorer_triggered(): path = api.window.get_tab_under_context_menu().file.path FileSystemContextMenu.show_in_explorer(path, api.window.get_main_window()) def _on_file_activated(self, index): path = self.view.filePath(index) if path and os.path.isfile(path): _logger().debug('showing editor for %r', path) api.editor.open_file(path) def _on_current_editor_changed(self, tab): if tab is not None: self.view.select_path(tab.file.path) self.action_goto_line.setEnabled(tab is not None) self.action_goto_symbol.setEnabled(tab is not None and settings.indexing_enabled()) def _on_current_proj_changed(self, new_path): if api.project.get_current_project() != new_path: self.main_window.current_project = new_path self.view.set_root_path(new_path) def _set_active_project(self): paths = api.project.get_projects() try: data = load_user_config(paths[0]) except IndexError: return try: active_path = data['active_project'] if active_path is None or not os.path.exists(active_path): raise KeyError('active_project') except KeyError: active_path = paths[0] for i in range(self._combo_projects.count()): if self._combo_projects.itemData(i) == active_path: self._combo_projects.setCurrentIndex(i) self.view.set_root_path(active_path) break else: self.view.set_root_path(active_path) self.main_window.current_project = active_path def _on_path_added(self, path): if os.path.isfile(path): return index = self._combo_projects.count() self.load_project_combo(current_index=index) self._on_current_index_changed(index) self.bt_rm_proj.setVisible(self._combo_projects.count() > 1) data = load_user_config(api.project.get_projects()[0]) data['active_project'] = path self._indexors.append(ProjectIndexor(path, self.main_window)) def _on_current_index_changed(self, index): new_path = self._combo_projects.itemData(index) if new_path: self.main_window.current_project_changed.emit(new_path) def _refresh(self): self.view.set_root_path('/') self.view.set_root_path(api.project.get_current_project()) def _force_reindex_all_projects(self): for indexor in self._indexors: indexor.cancel() # wait a bit before removing the index database (to make sure # pending indexation tasks are actually canceled). QtCore.QTimer.singleShot(100, self._perform_full_reindexation) def _perform_full_reindexation(self): for project in api.project.get_projects(): index.remove_project(project) self._reindex_all_projects() def _reindex_all_projects(self): for indexor in self._indexors: indexor.cancel() QtCore.QTimer.singleShot(100, self._perform_reindexation) def _perform_reindexation(self): for indexor in self._indexors: indexor.perform_indexing() def _remove_current_project(self): path = self._combo_projects.itemData( self._combo_projects.currentIndex()) self._combo_projects.removeItem(self._combo_projects.currentIndex()) self.main_window.remove_folder(path) initial_path = api.project.get_projects()[0] data = load_user_config(initial_path) try: linked_paths = data['linked_paths'] except KeyError: linked_paths = [] try: linked_paths.remove(path) except ValueError: _logger().warn('failed to remove linked path %r, path not in list', path) finally: data['linked_paths'] = linked_paths save_user_config(initial_path, data) for indexor in self._indexors: if indexor.project == path: self._indexors.remove(indexor) def _on_file_created(self, path): api.editor.open_file(path) project_path = self._combo_projects.itemData( self._combo_projects.currentIndex()) with DbHelper() as dbh: try: dbh.create_file(path, project_id=index.get_project_ids( [project_path])[0]) except (TypeError, IndexError): return else: self._update_document_index(path) def _on_file_saved(self, path): self._update_document_index(path) def _update_document_index(self, path): file = index.get_file(path) if not file: return project = None for p in index.get_all_projects(): if p.id == file.project_id: project = p break if not project: return test_path = 'file' + os.path.splitext(path)[1] plugin = get_symbol_parser(test_path, tuple(self.parser_plugins)) if not plugin: return args = (path, file.id, project.path, project.id, plugin) api.tasks.start('Update file index', update_file, None, args=args) def _on_files_renamed(self, renamed_files): for old_path, new_path in renamed_files: old_path = os.path.normpath(os.path.normcase(old_path)) new_path = os.path.normpath(os.path.normcase(new_path)) self.main_window.tab_widget.rename_document(old_path, new_path) self.main_window.file_renamed.emit(old_path, new_path) api.tasks.start('Update renamed files index', rename_files, None, args=(renamed_files, )) def _on_files_deleted(self, deleted_files): for path in deleted_files: path = os.path.normpath(os.path.normcase(path)) self.main_window.tab_widget.close_document(path) self.main_window.file_deleted.emit(path) api.tasks.start('Update renamed files index', delete_files, None, args=(deleted_files, )) @staticmethod def _on_locator_activated(path, line): if line == -1: line = None else: line -= 1 # 0 based api.editor.open_file(path, line=line) def _on_locator_cancelled(self): if self._locator.mode in [ self._locator.MODE_GOTO_LINE, self._locator.MODE_GOTO_SYMBOL ]: line, col = self._cached_cursor_position TextHelper(api.editor.get_current_editor()).goto_line(line, col)