Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions gitfourchette/filelists/dirtyfiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,6 @@ def ignoreSelection(self):
return

NewIgnorePattern.invoke(self, selected[0])

def onItemDoubleClicked(self, index: QModelIndex):
self.stage()
122 changes: 87 additions & 35 deletions gitfourchette/filelists/filelist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

# Determine split based on style
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 '/' in fullText:
slashesInFull = fullText.count('/')
slashesInElided = text.count('/')
if text.startswith(prefix):
firstPortion = prefix
secondPortion = text[len(prefix):]
else:
firstPortion = text
secondPortion = ""

firstColor = QPalette.ColorRole.WindowText
secondColor = QPalette.ColorRole.PlaceholderText

if slashesInFull > slashesInElided:
# A slash was elided - gray everything up to the ellipsis
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)
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():
Expand All @@ -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()


Expand Down Expand Up @@ -575,6 +616,17 @@ def onSpecialMouseClick(self):
""" Override this if you want to react to a middle click. """
pass

def mouseDoubleClickEvent(self, event: QMouseEvent):
index = self.indexAt(event.pos())
if index.isValid():
self.onItemDoubleClicked(index)
else:
super().mouseDoubleClickEvent(event)

def onItemDoubleClicked(self, index: QModelIndex):
""" Override this if you want to react to a double click. """
pass

def selectedDeltas(self) -> Generator[GitDelta, None, None]:
for index in self.selectedIndexes():
yield index.data(FileListModel.Role.Delta)
Expand Down
3 changes: 3 additions & 0 deletions gitfourchette/filelists/stagedfiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,6 @@ def unstageModeChange(self):
def onSpecialMouseClick(self):
if settings.prefs.middleClickToStage:
self.unstage()

def onItemDoubleClicked(self, index: QModelIndex):
self.unstage()
7 changes: 6 additions & 1 deletion gitfourchette/tasks/indextasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,12 @@ def flow(self, deltas: list[GitDelta]):
QApplication.beep()
raise AbortTask()

paths = [delta.new.path for delta in deltas]
paths = []
for delta in deltas:
paths.append(delta.new.path)
if delta.status == "R":
paths.append(delta.old.path)

self.effects |= TaskEffects.Workdir
# Not using 'restore --staged' because it doesn't work in an empty repo
yield from self.flowCallGit("reset", "--", *paths)
Expand Down
1 change: 1 addition & 0 deletions gitfourchette/tasks/jumptasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def loadWorkdir(task: RepoTask, allowWriteIndex: bool):
"""
gitStatus = yield from task.flowCallGit(
*argsIf(not allowWriteIndex, "--no-optional-locks"),
"-c", "status.renames=true",
"status",
"--porcelain=v2",
"-z",
Expand Down
6 changes: 6 additions & 0 deletions gitfourchette/toolbox/pathutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class PathDisplayStyle(enum.IntEnum):
FullPaths = 1
AbbreviateDirs = 2
FileNameOnly = 3
FileNameFirst = 4


def compactPath(path: str) -> str:
Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions gitfourchette/trtables.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
33 changes: 33 additions & 0 deletions test/test_filelist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -660,3 +663,33 @@ def testFileListNaturalSort(tempDir, mainWindow):

rw = mainWindow.openRepo(wd)
assert qlvGetRowData(rw.dirtyFiles) == names


def testUnstageRenamedFile(tempDir, mainWindow):
wd = unpackRepo(tempDir)
writeFile(f"{wd}/a.txt", "content")

with RepoContext(wd) as repo:
repo.index.add("a.txt")
repo.create_commit_on_head("initial", TEST_SIGNATURE, TEST_SIGNATURE)

os.rename(f"{wd}/a.txt", f"{wd}/b.txt")

with RepoContext(wd) as repo:
repo.index.add_all()
repo.index.write()

rw = mainWindow.openRepo(wd)

staged = list(qlvGetRowData(rw.stagedFiles))
assert staged == ["b.txt"]

rw.diffArea.stagedFiles.selectAll()
triggerContextMenuAction(rw.diffArea.stagedFiles.viewport(), "unstage")

rw.refreshRepo()

status = rw.repo.status()

assert status['a.txt'] == FileStatus.WT_DELETED
assert status['b.txt'] == FileStatus.WT_NEW
Loading