vibefpmgui.quick_open
Go to File (⌘P / Ctrl+P): any file of the project, or of its dependencies, found by typing part of its name, opened in the editor.
The files are those the Project files tree lists (ProjectTree.all_files):
the ones ignored by git and the greyed-out ones only with its Ignored
ticked. What's typed matches the file's name first (its start, then
anywhere, then its letters in order: "dstat" finds demo_stats.f90), then
its path ("src/dem"); the project's own files before its dependencies',
then the shortest paths. Nothing typed: the files open in the editor first.
1"""Go to File (⌘P / Ctrl+P): any file of the project, or of its dependencies, 2found by typing part of its name, opened in the editor. 3 4The files are those the Project files tree lists (`ProjectTree.all_files`): 5the ones ignored by git and the greyed-out ones only with its *Ignored* 6ticked. What's typed matches the file's name first (its start, then 7anywhere, then its letters in order: "dstat" finds demo_stats.f90), then 8its path ("src/dem"); the project's own files before its dependencies', 9then the shortest paths. Nothing typed: the files open in the editor first. 10""" 11 12from __future__ import annotations 13 14from pathlib import Path, PurePosixPath 15 16from qtpy.QtCore import QEvent, QRect, QSize, Qt 17from qtpy.QtGui import QFont, QFontMetrics, QIcon, QPalette 18from qtpy.QtWidgets import ( 19 QApplication, 20 QFrame, 21 QLabel, 22 QLineEdit, 23 QListWidget, 24 QListWidgetItem, 25 QStyle, 26 QStyledItemDelegate, 27 QVBoxLayout, 28 QWidget, 29) 30 31from .project_tree import ProjectFile 32 33FILE_ROLE = Qt.UserRole + 1 34MAX_SHOWN = 300 # files listed at once (more: type more of the name) 35 36 37def _in_order(letters: str, text: str) -> bool: 38 """Whether ``letters`` appear in ``text`` in that order (not together).""" 39 found = 0 40 for ch in text: 41 if found < len(letters) and ch == letters[found]: 42 found += 1 43 return found == len(letters) 44 45 46def score(query: str, item: ProjectFile) -> int | None: 47 """How well a file matches what's typed (None: not at all).""" 48 typed = "".join(query.lower().split()) 49 name, path = PurePosixPath(item.relative).name.lower(), item.relative.lower() 50 if name.startswith(typed): 51 return 1000 52 if typed in name: 53 return 800 54 if _in_order(typed, name): 55 return 600 56 if typed in path: 57 return 400 58 if _in_order(typed, path): 59 return 200 60 return None 61 62 63def ranked(query: str, files: list[ProjectFile], open_paths: list[str] | None = None) -> list[ProjectFile]: 64 """The files matching ``query``, best first (see the module's description).""" 65 open_order = {path: n for n, path in enumerate(open_paths or [])} 66 if not query.strip(): 67 return sorted(files, key=lambda f: (open_order.get(f.path, len(open_order)), f.dependency, f.package, 68 f.relative.lower())) 69 matched = [] 70 for item in files: 71 points = score(query, item) 72 if points is not None: 73 matched.append((-points, item.dependency, open_order.get(item.path, len(open_order)), 74 len(item.relative), item.relative.lower(), item)) 75 matched.sort(key=lambda m: m[:5]) 76 return [m[-1] for m in matched] 77 78 79class _FileDelegate(QStyledItemDelegate): 80 """A file: its icon, name (bold), folder (muted) and, for a dependency's, 81 the package.""" 82 83 def __init__(self, palette: QuickOpen): 84 super().__init__(palette.list) 85 self.palette_ = palette 86 87 def sizeHint(self, option, index): 88 return QSize(option.rect.width(), QFontMetrics(option.font).height() + 10) 89 90 def paint(self, painter, option, index): 91 item: ProjectFile = index.data(FILE_ROLE) 92 style = option.widget.style() if option.widget else QApplication.style() 93 style.drawPrimitive(QStyle.PE_PanelItemViewItem, option, painter, option.widget) 94 selected = bool(option.state & QStyle.State_Selected) 95 pal = option.palette 96 text = pal.color(QPalette.HighlightedText if selected else QPalette.Text) 97 muted = pal.color(QPalette.HighlightedText if selected else QPalette.PlaceholderText) 98 rect = option.rect.adjusted(8, 0, -8, 0) 99 painter.save() 100 icon = self.palette_.icon_for(Path(item.path)) if self.palette_.icon_for else None 101 size = QFontMetrics(option.font).height() 102 x = rect.left() 103 if icon is not None and not icon.isNull(): 104 icon.paint(painter, QRect(x, rect.top() + (rect.height() - size) // 2, size, size)) 105 x += size + 6 106 bold = QFont(option.font) 107 bold.setBold(True) 108 name = PurePosixPath(item.relative).name 109 folder = str(PurePosixPath(item.relative).parent) 110 parts = [(name, bold, text), (f" {folder}" if folder != "." else "", option.font, muted)] 111 if item.dependency: 112 parts.append((f" · {item.package}", option.font, muted)) 113 if item.hidden: 114 parts.append((" · ignored", option.font, muted)) 115 for part, font, color in parts: 116 if not part or x >= rect.right(): 117 continue 118 painter.setFont(font) 119 painter.setPen(color) 120 metrics = QFontMetrics(font) 121 shown = metrics.elidedText(part, Qt.ElideMiddle, rect.right() - x) 122 painter.drawText(QRect(x, rect.top(), rect.right() - x, rect.height()), Qt.AlignVCenter | Qt.AlignLeft, 123 shown) 124 x += metrics.horizontalAdvance(shown) 125 painter.restore() 126 127 128class QuickOpen(QFrame): 129 """The palette: a field to type in, the matching files under it (as the 130 Command Palette). ``opened`` is called with the file chosen.""" 131 132 WIDTH = 660 133 ROWS = 14 134 135 def __init__(self, window: QWidget, opened, icon_for=None): 136 super().__init__(window, Qt.Popup) 137 self.window_ = window 138 self.opened = opened 139 self.icon_for = icon_for 140 self.files: list[ProjectFile] = [] 141 self.open_paths: list[str] = [] 142 self.setFrameShape(QFrame.StyledPanel) 143 self.field = QLineEdit() 144 self.field.setPlaceholderText("Type part of a file's name (letters in order: \"dstat\" finds demo_stats.f90)") 145 self.field.setClearButtonEnabled(True) 146 self.field.textChanged.connect(self._filter) 147 self.field.installEventFilter(self) 148 # Its icon, in front of what's typed (the menu item's: see set_icon). 149 self.icon_action = self.field.addAction(QIcon(), QLineEdit.LeadingPosition) 150 self.list = QListWidget() 151 self.list.setItemDelegate(_FileDelegate(self)) 152 self.list.setUniformItemSizes(True) 153 self.list.setFocusPolicy(Qt.NoFocus) 154 self.list.itemClicked.connect(lambda item: self.choose(item.data(FILE_ROLE))) 155 self.note = QLabel() # (whether ignored files are in the list, and how many more there are) 156 self.note.setStyleSheet("color: gray;") 157 layout = QVBoxLayout(self) 158 layout.setContentsMargins(6, 6, 6, 6) 159 layout.setSpacing(6) 160 layout.addWidget(self.field) 161 layout.addWidget(self.list) 162 layout.addWidget(self.note) 163 self.include_ignored = False 164 165 def set_icon(self, icon: QIcon): 166 """The icon in front of the field (the menu item's, in the theme's colors).""" 167 self.icon_action.setIcon(icon) 168 169 def open(self, files: list[ProjectFile], open_paths: list[str], include_ignored: bool): 170 """Show the palette at the top of the window, for these files.""" 171 self.files, self.open_paths, self.include_ignored = files, open_paths, include_ignored 172 self.field.clear() 173 self._filter("") 174 width = min(self.WIDTH, max(320, self.window_.width() - 40)) 175 row = self.list.sizeHintForRow(0) if self.list.count() else 24 176 self.resize(width, self.field.sizeHint().height() + row * self.ROWS + self.note.sizeHint().height() + 30) 177 top = self.window_.mapToGlobal(self.window_.rect().topLeft()) 178 self.move(top.x() + (self.window_.width() - width) // 2, top.y() + 40) 179 self.show() 180 self.raise_() 181 self.field.setFocus() 182 183 def _filter(self, text: str): 184 self.list.clear() 185 found = ranked(text, self.files, self.open_paths) 186 for item in found[:MAX_SHOWN]: 187 row = QListWidgetItem(item.relative) 188 row.setData(FILE_ROLE, item) 189 row.setToolTip(item.path) 190 self.list.addItem(row) 191 if self.list.count(): 192 self.list.setCurrentRow(0) 193 more = f"{len(found) - MAX_SHOWN} more: type more of the name. " if len(found) > MAX_SHOWN else "" 194 which = ("Files ignored by git are included" if self.include_ignored 195 else "Files ignored by git are left out") 196 self.note.setText(f"{more}{which} (Project files → Ignored). {len(self.files)} files.") 197 198 def choose(self, item: ProjectFile): 199 self.close() 200 self.opened(item.path) 201 202 def eventFilter(self, obj, event): 203 if obj is self.field and event.type() == QEvent.KeyPress: 204 key = event.key() 205 steps = {Qt.Key_Up: -1, Qt.Key_Down: 1, Qt.Key_PageUp: -self.ROWS, Qt.Key_PageDown: self.ROWS} 206 if key in steps and self.list.count(): 207 self.list.setCurrentRow(max(0, min(self.list.count() - 1, self.list.currentRow() + steps[key]))) 208 return True 209 if key in (Qt.Key_Return, Qt.Key_Enter): 210 item = self.list.currentItem() 211 if item is not None: 212 self.choose(item.data(FILE_ROLE)) 213 return True 214 if key == Qt.Key_Escape: 215 self.close() 216 return True 217 return super().eventFilter(obj, event)
47def score(query: str, item: ProjectFile) -> int | None: 48 """How well a file matches what's typed (None: not at all).""" 49 typed = "".join(query.lower().split()) 50 name, path = PurePosixPath(item.relative).name.lower(), item.relative.lower() 51 if name.startswith(typed): 52 return 1000 53 if typed in name: 54 return 800 55 if _in_order(typed, name): 56 return 600 57 if typed in path: 58 return 400 59 if _in_order(typed, path): 60 return 200 61 return None
How well a file matches what's typed (None: not at all).
64def ranked(query: str, files: list[ProjectFile], open_paths: list[str] | None = None) -> list[ProjectFile]: 65 """The files matching ``query``, best first (see the module's description).""" 66 open_order = {path: n for n, path in enumerate(open_paths or [])} 67 if not query.strip(): 68 return sorted(files, key=lambda f: (open_order.get(f.path, len(open_order)), f.dependency, f.package, 69 f.relative.lower())) 70 matched = [] 71 for item in files: 72 points = score(query, item) 73 if points is not None: 74 matched.append((-points, item.dependency, open_order.get(item.path, len(open_order)), 75 len(item.relative), item.relative.lower(), item)) 76 matched.sort(key=lambda m: m[:5]) 77 return [m[-1] for m in matched]
The files matching query, best first (see the module's description).
129class QuickOpen(QFrame): 130 """The palette: a field to type in, the matching files under it (as the 131 Command Palette). ``opened`` is called with the file chosen.""" 132 133 WIDTH = 660 134 ROWS = 14 135 136 def __init__(self, window: QWidget, opened, icon_for=None): 137 super().__init__(window, Qt.Popup) 138 self.window_ = window 139 self.opened = opened 140 self.icon_for = icon_for 141 self.files: list[ProjectFile] = [] 142 self.open_paths: list[str] = [] 143 self.setFrameShape(QFrame.StyledPanel) 144 self.field = QLineEdit() 145 self.field.setPlaceholderText("Type part of a file's name (letters in order: \"dstat\" finds demo_stats.f90)") 146 self.field.setClearButtonEnabled(True) 147 self.field.textChanged.connect(self._filter) 148 self.field.installEventFilter(self) 149 # Its icon, in front of what's typed (the menu item's: see set_icon). 150 self.icon_action = self.field.addAction(QIcon(), QLineEdit.LeadingPosition) 151 self.list = QListWidget() 152 self.list.setItemDelegate(_FileDelegate(self)) 153 self.list.setUniformItemSizes(True) 154 self.list.setFocusPolicy(Qt.NoFocus) 155 self.list.itemClicked.connect(lambda item: self.choose(item.data(FILE_ROLE))) 156 self.note = QLabel() # (whether ignored files are in the list, and how many more there are) 157 self.note.setStyleSheet("color: gray;") 158 layout = QVBoxLayout(self) 159 layout.setContentsMargins(6, 6, 6, 6) 160 layout.setSpacing(6) 161 layout.addWidget(self.field) 162 layout.addWidget(self.list) 163 layout.addWidget(self.note) 164 self.include_ignored = False 165 166 def set_icon(self, icon: QIcon): 167 """The icon in front of the field (the menu item's, in the theme's colors).""" 168 self.icon_action.setIcon(icon) 169 170 def open(self, files: list[ProjectFile], open_paths: list[str], include_ignored: bool): 171 """Show the palette at the top of the window, for these files.""" 172 self.files, self.open_paths, self.include_ignored = files, open_paths, include_ignored 173 self.field.clear() 174 self._filter("") 175 width = min(self.WIDTH, max(320, self.window_.width() - 40)) 176 row = self.list.sizeHintForRow(0) if self.list.count() else 24 177 self.resize(width, self.field.sizeHint().height() + row * self.ROWS + self.note.sizeHint().height() + 30) 178 top = self.window_.mapToGlobal(self.window_.rect().topLeft()) 179 self.move(top.x() + (self.window_.width() - width) // 2, top.y() + 40) 180 self.show() 181 self.raise_() 182 self.field.setFocus() 183 184 def _filter(self, text: str): 185 self.list.clear() 186 found = ranked(text, self.files, self.open_paths) 187 for item in found[:MAX_SHOWN]: 188 row = QListWidgetItem(item.relative) 189 row.setData(FILE_ROLE, item) 190 row.setToolTip(item.path) 191 self.list.addItem(row) 192 if self.list.count(): 193 self.list.setCurrentRow(0) 194 more = f"{len(found) - MAX_SHOWN} more: type more of the name. " if len(found) > MAX_SHOWN else "" 195 which = ("Files ignored by git are included" if self.include_ignored 196 else "Files ignored by git are left out") 197 self.note.setText(f"{more}{which} (Project files → Ignored). {len(self.files)} files.") 198 199 def choose(self, item: ProjectFile): 200 self.close() 201 self.opened(item.path) 202 203 def eventFilter(self, obj, event): 204 if obj is self.field and event.type() == QEvent.KeyPress: 205 key = event.key() 206 steps = {Qt.Key_Up: -1, Qt.Key_Down: 1, Qt.Key_PageUp: -self.ROWS, Qt.Key_PageDown: self.ROWS} 207 if key in steps and self.list.count(): 208 self.list.setCurrentRow(max(0, min(self.list.count() - 1, self.list.currentRow() + steps[key]))) 209 return True 210 if key in (Qt.Key_Return, Qt.Key_Enter): 211 item = self.list.currentItem() 212 if item is not None: 213 self.choose(item.data(FILE_ROLE)) 214 return True 215 if key == Qt.Key_Escape: 216 self.close() 217 return True 218 return super().eventFilter(obj, event)
The palette: a field to type in, the matching files under it (as the
Command Palette). opened is called with the file chosen.
136 def __init__(self, window: QWidget, opened, icon_for=None): 137 super().__init__(window, Qt.Popup) 138 self.window_ = window 139 self.opened = opened 140 self.icon_for = icon_for 141 self.files: list[ProjectFile] = [] 142 self.open_paths: list[str] = [] 143 self.setFrameShape(QFrame.StyledPanel) 144 self.field = QLineEdit() 145 self.field.setPlaceholderText("Type part of a file's name (letters in order: \"dstat\" finds demo_stats.f90)") 146 self.field.setClearButtonEnabled(True) 147 self.field.textChanged.connect(self._filter) 148 self.field.installEventFilter(self) 149 # Its icon, in front of what's typed (the menu item's: see set_icon). 150 self.icon_action = self.field.addAction(QIcon(), QLineEdit.LeadingPosition) 151 self.list = QListWidget() 152 self.list.setItemDelegate(_FileDelegate(self)) 153 self.list.setUniformItemSizes(True) 154 self.list.setFocusPolicy(Qt.NoFocus) 155 self.list.itemClicked.connect(lambda item: self.choose(item.data(FILE_ROLE))) 156 self.note = QLabel() # (whether ignored files are in the list, and how many more there are) 157 self.note.setStyleSheet("color: gray;") 158 layout = QVBoxLayout(self) 159 layout.setContentsMargins(6, 6, 6, 6) 160 layout.setSpacing(6) 161 layout.addWidget(self.field) 162 layout.addWidget(self.list) 163 layout.addWidget(self.note) 164 self.include_ignored = False
__init__(self, /, parent: PySide6.QtWidgets.QWidget | None = None, f: PySide6.QtCore.Qt.WindowType = Default(Qt.WindowFlags), *, frameShape: PySide6.QtWidgets.QFrame.Shape | None = None, frameShadow: PySide6.QtWidgets.QFrame.Shadow | None = None, lineWidth: int | None = None, midLineWidth: int | None = None, frameWidth: int | None = None, frameRect: PySide6.QtCore.QRect | None = None) -> None
Initialize self. See help(type(self)) for accurate signature.
166 def set_icon(self, icon: QIcon): 167 """The icon in front of the field (the menu item's, in the theme's colors).""" 168 self.icon_action.setIcon(icon)
The icon in front of the field (the menu item's, in the theme's colors).
170 def open(self, files: list[ProjectFile], open_paths: list[str], include_ignored: bool): 171 """Show the palette at the top of the window, for these files.""" 172 self.files, self.open_paths, self.include_ignored = files, open_paths, include_ignored 173 self.field.clear() 174 self._filter("") 175 width = min(self.WIDTH, max(320, self.window_.width() - 40)) 176 row = self.list.sizeHintForRow(0) if self.list.count() else 24 177 self.resize(width, self.field.sizeHint().height() + row * self.ROWS + self.note.sizeHint().height() + 30) 178 top = self.window_.mapToGlobal(self.window_.rect().topLeft()) 179 self.move(top.x() + (self.window_.width() - width) // 2, top.y() + 40) 180 self.show() 181 self.raise_() 182 self.field.setFocus()
Show the palette at the top of the window, for these files.
203 def eventFilter(self, obj, event): 204 if obj is self.field and event.type() == QEvent.KeyPress: 205 key = event.key() 206 steps = {Qt.Key_Up: -1, Qt.Key_Down: 1, Qt.Key_PageUp: -self.ROWS, Qt.Key_PageDown: self.ROWS} 207 if key in steps and self.list.count(): 208 self.list.setCurrentRow(max(0, min(self.list.count() - 1, self.list.currentRow() + steps[key]))) 209 return True 210 if key in (Qt.Key_Return, Qt.Key_Enter): 211 item = self.list.currentItem() 212 if item is not None: 213 self.choose(item.data(FILE_ROLE)) 214 return True 215 if key == Qt.Key_Escape: 216 self.close() 217 return True 218 return super().eventFilter(obj, event)
eventFilter(self, watched: PySide6.QtCore.QObject, event: PySide6.QtCore.QEvent, /) -> bool