From b5cf50c80f598b0d1e5f4b95ef58a041207e69fb Mon Sep 17 00:00:00 2001 From: Zach Rammell Date: Tue, 3 Feb 2026 11:40:04 -0800 Subject: [PATCH 1/2] Add "Filename first" path display style --- gitfourchette/filelists/filelist.py | 109 +++++++++++++++++++--------- gitfourchette/toolbox/pathutils.py | 6 ++ gitfourchette/trtables.py | 1 + test/test_filelist.py | 3 + 4 files changed, 85 insertions(+), 34 deletions(-) diff --git a/gitfourchette/filelists/filelist.py b/gitfourchette/filelists/filelist.py index 9c4f2e71..d63d761f 100644 --- a/gitfourchette/filelists/filelist.py +++ b/gitfourchette/filelists/filelist.py @@ -72,46 +72,84 @@ def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIn if font: painter.setFont(font) fullText = index.data(Qt.ItemDataRole.DisplayRole) - text = painter.fontMetrics().elidedText(fullText, option.textElideMode, textRect.width()) + elideMode = Qt.TextElideMode.ElideRight if settings.prefs.pathDisplayStyle == PathDisplayStyle.FileNameFirst else option.textElideMode + text = painter.fontMetrics().elidedText(fullText, elideMode, textRect.width()) # Split path into directory and filename for better readability - dirPortion = None - filePortion = None + firstPortion = None + secondPortion = None + firstColor = QPalette.ColorRole.PlaceholderText + secondColor = QPalette.ColorRole.WindowText - if '/' in fullText: - slashesInFull = fullText.count('/') - slashesInElided = text.count('/') + # Determine split based on style + isFileNameFirst = settings.prefs.pathDisplayStyle == PathDisplayStyle.FileNameFirst - if slashesInFull > slashesInElided: - # A slash was elided - gray everything up to the ellipsis + if isFileNameFirst: + # Rely on the raw path to identify the filename, as filenames can contain spaces + fullPath = index.data(FileListModel.Role.FilePath) + fName = os.path.basename(fullPath) + + prefix = fName + " " + if not text.startswith(prefix) and '\u2026' in text: ellipsisPos = text.find('\u2026') - dirPortion = text[:ellipsisPos + 1] - filePortion = text[ellipsisPos + 1:] - elif slashesInElided > 0: - # No slash elided - gray up to the last slash - lastSlash = text.rfind('/') - dirPortion = text[:lastSlash + 1] - filePortion = text[lastSlash + 1:] - - if dirPortion is not None: - textColor = QPalette.ColorRole.WindowText if not isSelected else QPalette.ColorRole.HighlightedText - dirColor = QPalette.ColorRole.PlaceholderText if not isSelected else textColor - - # Draw directory with muted color - mutedColor = option.palette.color(colorGroup, dirColor) - if isSelected: - mutedColor.setAlphaF(.7) - painter.setPen(mutedColor) - painter.drawText(textRect, option.displayAlignment, dirPortion) - - # Draw filename with normal color - painter.setPen(option.palette.color(colorGroup, textColor)) - dirWidth = painter.fontMetrics().horizontalAdvance(dirPortion) - fileRect = QRect(textRect) - fileRect.setLeft(textRect.left() + dirWidth) - painter.drawText(fileRect, option.displayAlignment, filePortion) + if fName.startswith(text[:ellipsisPos]): + prefix = text[:ellipsisPos + 1] + + if text.startswith(prefix): + firstPortion = prefix + secondPortion = text[len(prefix):] + else: + firstPortion = text + secondPortion = "" + + firstColor = QPalette.ColorRole.WindowText + secondColor = QPalette.ColorRole.PlaceholderText + else: - painter.drawText(textRect, option.displayAlignment, text) + if '/' in fullText: + slashesInFull = fullText.count('/') + slashesInElided = text.count('/') + + if slashesInFull > slashesInElided: + # A slash was elided - gray everything up to the ellipsis + ellipsisPos = text.find('\u2026') + firstPortion = text[:ellipsisPos + 1] + secondPortion = text[ellipsisPos + 1:] + elif slashesInElided > 0: + # No slash elided - gray up to the last slash + lastSlash = text.rfind('/') + firstPortion = text[:lastSlash + 1] + secondPortion = text[lastSlash + 1:] + + if firstPortion is None: + firstPortion = "" + secondPortion = text + + firstColor = QPalette.ColorRole.PlaceholderText + secondColor = QPalette.ColorRole.WindowText + + # Draw the parts + if firstPortion: + fg1 = option.palette.color(colorGroup, firstColor if not isSelected else QPalette.ColorRole.HighlightedText) + if firstColor == QPalette.ColorRole.PlaceholderText and isSelected: + fg1 = QColor(option.palette.color(colorGroup, QPalette.ColorRole.HighlightedText)) + fg1.setAlphaF(0.7) + + painter.setPen(fg1) + painter.drawText(textRect, option.displayAlignment, firstPortion) + + # Prepare rect for second part + part1Width = painter.fontMetrics().horizontalAdvance(firstPortion) + textRect.setLeft(textRect.left() + part1Width) + + if secondPortion: + fg2 = option.palette.color(colorGroup, secondColor if not isSelected else QPalette.ColorRole.HighlightedText) + if secondColor == QPalette.ColorRole.PlaceholderText and isSelected: + fg2 = QColor(option.palette.color(colorGroup, QPalette.ColorRole.HighlightedText)) + fg2.setAlphaF(0.7) + + painter.setPen(fg2) + painter.drawText(textRect, option.displayAlignment, secondPortion) # Highlight search term if searchTerm and searchTerm in fullText.lower(): @@ -122,8 +160,11 @@ def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIn else: needleLen = len(searchTerm) + # Use original textRect (before we advanced it for the second part) + textRect.setLeft(textRect.left() - part1Width if firstPortion else 0) SearchBar.highlightNeedle(painter, textRect, text, needlePos, needleLen) + painter.restore() diff --git a/gitfourchette/toolbox/pathutils.py b/gitfourchette/toolbox/pathutils.py index 12b0d1d4..d83b6114 100644 --- a/gitfourchette/toolbox/pathutils.py +++ b/gitfourchette/toolbox/pathutils.py @@ -14,6 +14,7 @@ class PathDisplayStyle(enum.IntEnum): FullPaths = 1 AbbreviateDirs = 2 FileNameOnly = 3 + FileNameFirst = 4 def compactPath(path: str) -> str: @@ -33,6 +34,11 @@ def abbreviatePath(path: str, style: PathDisplayStyle = PathDisplayStyle.FullPat else: splitLong[i] = splitLong[i][0] return '/'.join(splitLong) + elif style == PathDisplayStyle.FileNameFirst: + split = path.rsplit('/', 1) + if len(split) == 1: + return path + return split[-1] + ' ' + split[0] elif style == PathDisplayStyle.FileNameOnly: return path.rsplit('/', 1)[-1] else: diff --git a/gitfourchette/trtables.py b/gitfourchette/trtables.py index fc6eb016..c0cbe0c7 100644 --- a/gitfourchette/trtables.py +++ b/gitfourchette/trtables.py @@ -195,6 +195,7 @@ def _init_enums(): PathDisplayStyle.FullPaths : _("Full paths"), PathDisplayStyle.AbbreviateDirs : _("Abbreviate directories"), PathDisplayStyle.FileNameOnly : _("Show filename only"), + PathDisplayStyle.FileNameFirst : _("Filename first"), }, AuthorDisplayStyle: { diff --git a/test/test_filelist.py b/test/test_filelist.py index 79bd6e2d..cd904b37 100644 --- a/test/test_filelist.py +++ b/test/test_filelist.py @@ -498,6 +498,9 @@ def testFileListChangePathDisplayStyle(tempDir, mainWindow): triggerContextMenuAction(rw.committedFiles.viewport(), "path display style/full") assert ["c/c2-2.txt"] == qlvGetRowData(rw.committedFiles) + triggerContextMenuAction(rw.committedFiles.viewport(), "path display style/name first") + assert ["c2-2.txt c"] == qlvGetRowData(rw.committedFiles) + def testFileListShowInFolder(tempDir, mainWindow): wd = unpackRepo(tempDir) From 32a41f546bac4d874928b9e9dd677e5050585135 Mon Sep 17 00:00:00 2001 From: Zach Rammell Date: Mon, 16 Feb 2026 11:56:37 -0800 Subject: [PATCH 2/2] Simplify "Filename first" implementation --- gitfourchette/filelists/filelist.py | 25 +++++++------------------ gitfourchette/toolbox/pathutils.py | 2 +- test/test_filelist.py | 2 +- 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/gitfourchette/filelists/filelist.py b/gitfourchette/filelists/filelist.py index d63d761f..227a2e1a 100644 --- a/gitfourchette/filelists/filelist.py +++ b/gitfourchette/filelists/filelist.py @@ -72,8 +72,7 @@ def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIn if font: painter.setFont(font) fullText = index.data(Qt.ItemDataRole.DisplayRole) - elideMode = Qt.TextElideMode.ElideRight if settings.prefs.pathDisplayStyle == PathDisplayStyle.FileNameFirst else option.textElideMode - text = painter.fontMetrics().elidedText(fullText, elideMode, textRect.width()) + text = painter.fontMetrics().elidedText(fullText, option.textElideMode, textRect.width()) # Split path into directory and filename for better readability firstPortion = None @@ -85,22 +84,10 @@ def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIn isFileNameFirst = settings.prefs.pathDisplayStyle == PathDisplayStyle.FileNameFirst if isFileNameFirst: - # Rely on the raw path to identify the filename, as filenames can contain spaces - fullPath = index.data(FileListModel.Role.FilePath) - fName = os.path.basename(fullPath) - - prefix = fName + " " - if not text.startswith(prefix) and '\u2026' in text: - ellipsisPos = text.find('\u2026') - if fName.startswith(text[:ellipsisPos]): - prefix = text[:ellipsisPos + 1] - - if text.startswith(prefix): - firstPortion = prefix - secondPortion = text[len(prefix):] - else: - firstPortion = text - secondPortion = "" + try: + firstPortion, secondPortion = text.split('\0') + except ValueError: + firstPortion, secondPortion = text, "" firstColor = QPalette.ColorRole.WindowText secondColor = QPalette.ColorRole.PlaceholderText @@ -236,6 +223,8 @@ def __init__(self, repoModel: RepoModel, parent: QWidget, navContext: NavContext def refreshPrefs(self): self.setVerticalScrollMode(settings.prefs.listViewScrollMode) + nameFirst = settings.prefs.pathDisplayStyle == PathDisplayStyle.FileNameFirst + self.setTextElideMode(Qt.TextElideMode.ElideRight if nameFirst else Qt.TextElideMode.ElideMiddle) @property def repo(self) -> Repo: diff --git a/gitfourchette/toolbox/pathutils.py b/gitfourchette/toolbox/pathutils.py index d83b6114..e06f268c 100644 --- a/gitfourchette/toolbox/pathutils.py +++ b/gitfourchette/toolbox/pathutils.py @@ -38,7 +38,7 @@ def abbreviatePath(path: str, style: PathDisplayStyle = PathDisplayStyle.FullPat split = path.rsplit('/', 1) if len(split) == 1: return path - return split[-1] + ' ' + split[0] + return split[-1] + ' \0' + split[0] elif style == PathDisplayStyle.FileNameOnly: return path.rsplit('/', 1)[-1] else: diff --git a/test/test_filelist.py b/test/test_filelist.py index cd904b37..16883c9a 100644 --- a/test/test_filelist.py +++ b/test/test_filelist.py @@ -499,7 +499,7 @@ def testFileListChangePathDisplayStyle(tempDir, mainWindow): assert ["c/c2-2.txt"] == qlvGetRowData(rw.committedFiles) triggerContextMenuAction(rw.committedFiles.viewport(), "path display style/name first") - assert ["c2-2.txt c"] == qlvGetRowData(rw.committedFiles) + assert ["c2-2.txt \0c"] == qlvGetRowData(rw.committedFiles) def testFileListShowInFolder(tempDir, mainWindow):