Compare commits

...
64 Commits
Author SHA1 Message Date
David Aguilar 712ffe9461 git-cola 1.7.5
Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-24 11:53:16 -08:00
David Aguilar c0f637696b dag: Properly handle right-clicking in an empty area
update_menu_actions() assumed that there was always a clicked item
and thus we were trying to use None as a commit.

Add safety checks to avoid this problem and make it so that the 'copy'
action can be performed even when the right-click action happens in
an empty area.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-24 11:48:22 -08:00
David Aguilar 8c057f23c4 doc: Add 1.7.5 release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-19 02:51:13 -08:00
David Aguilar 2eb5c7c169 pull: Remember the 'rebase' checkbox state between invocations
Save the value of the 'rebase' checkbox so that `git pull --rebase`
users do not need to remember to click on the checkbox each time.

The infrastructure is fully in place to save/restore options in every
dialog so it's finally time to close this issue.

Closes #4

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-19 02:51:13 -08:00
David Aguilar cd5575ceeb dag.view: Remove unnecessary try/except block
The base class does the right thing.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-19 02:51:13 -08:00
David Aguilar 53d3678cad main.view: Load GUI state in the main thread
We have to update in the main thread anyways so there's really little
benefit loading the GUI state in the background.

There's no perceptible difference so simplify the code by loading
the GUI state immediately during startup.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-19 02:49:21 -08:00
David Aguilar bfd3ffe12d widgets.createbranch: Fetch remote in a background thread
Finish the implementation for 'reset' and 'fetch remote branch'.
Do updates in a background thread; anything less would be uncivilized.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-17 00:44:46 -08:00
David Aguilar 5b8a48531d dag: Make 'parent' the last argument to the update thread
Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-17 00:27:24 -08:00
David Aguilar e2fe2c8220 ssh-askpass: Pickup fixes from upstream
Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-15 20:01:07 -08:00
David Aguilar 1a36ed3608 widgets.cfgactions: Sort user-configured GUI tools
Actions are easier to find when they are listed alphabetically.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-15 17:47:22 -08:00
David Aguilar c51ebdf897 main: Focus the status widget by default
Makes the navigation hotkeys active at startup by pre-focusing
the status widget.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-15 17:11:17 -08:00
David Aguilar 98c97ae89b dag: Focus the graph view on startup
Make the navigation shortcuts available by focusing the graph
widget after the history is loaded.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-15 16:47:24 -08:00
David Aguilar 10454e65d3 widgets.createtag: Add hints and tab completion
Add tab completion to the revision field.
Add hints to the message and name fields.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-15 16:46:42 -08:00
David Aguilar 406e9111e8 widgets.completion: Close the popup when the parent widget goes away
The completion popup can sometimes stick around when a dialog is
closed using the escape key.  Close the popup window whenever the
parent widget is hidden.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-15 16:44:37 -08:00
David Aguilar 188e72390b widgets.createbranch: Default to the current branch
Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-15 16:41:55 -08:00
David Aguilar 0c1cc38303 guicmds: Provide a default value for diff actions
Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-15 16:38:23 -08:00
David Aguilar 2c3cf4d4d6 widgets: Use 'Qt' instead of 'QtCore.Qt' consistently
...and other small style fixups.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-15 16:37:11 -08:00
David Aguilar 1adc310e0a dag: Use QColor.darker() instead of hard-coding values
Tweak the colors to add slightly more contrast.
Rename constants to make things easier to read.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-14 15:38:19 -08:00
David Aguilar a92773e902 widgets.createbranch: Tab-complete the revision field
Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-11 23:20:34 -08:00
David Aguilar 0e6995fce5 widgets: Factor completion widgets out into a standalone module
Create a standalone widgets.completion module for GitRefLineEdit
and the GitLogLineEdit widgets.  These widgets provide tab completion
for git refs and filenames.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-11 23:19:56 -08:00
David Aguilar d7987e6fc3 qt: Use 'Qt' instead 'QtCore.Qt'
Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-11 23:17:00 -08:00
David Aguilar 98ee623ffe dag: Make selected nodes bright orange
This makes it very easy to see which nodes are selected.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-11 15:08:52 -08:00
David Aguilar 9b75fd9e69 dag: Misc. UI improvements
Make the selection color subtler and more pleasing.
Swap the J / shift-J hotkeys so that going "down"
follows the oldest ancestor.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-11 03:06:56 -08:00
David Aguilar d0392ad426 remote: Fix the 'push' action
It was running 'fetch' instead.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-10 14:43:41 -08:00
David Aguilar e44710ab00 dag: Elide the 'author' text
Allow the text in the 'author' label to elide itself when resized.
The email hyperlinks learned to hide when the widget gets too thin.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-09 20:43:42 -08:00
David Aguilar 5d97efb930 git-cola 1.7.4.1
Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-07 20:06:54 -08:00
David Aguilar a2fef119c7 doc: Add 1.7.4.1 release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-07 20:06:53 -08:00
David Aguilar f81b6c932a dag: Show more in the initial view
Make the default view show more commits by default.
This also tightens up the "fit to view" function so that it
uses less padding around selected commits.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-07 20:06:53 -08:00
David Aguilar 7f3470a3bf dag: Make the reader thread self-managed
Move the stop()/start()/etc. methods into the thread class

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-07 20:06:53 -08:00
David Aguilar 84c5849dd8 dag: Add missing 'self' argument to method
Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-06 15:07:04 -08:00
David Aguilar dae657e7aa dag: Handle creating a patch for a single commit
sort_by_generation() assumed that the list of commits was always >= 2.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-06 15:05:09 -08:00
David Aguilar 092566c334 app: Simplify homebrew setup
The homebrew python path does not need to be at the front
of sys.path.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-06 03:31:29 -08:00
David Aguilar 9674055ba5 app: Make homebrew work by default
Homebrew currently requires users to set PYTHONPATH before they
can use git-cola.  Detect homebrew and use it by default.

Fixes: mxcl/homebrew/7462

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-06 03:04:00 -08:00
David Aguilar 2f015d3e53 README: typofix
Fix a mistake and make it simpler by saying less.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-02 20:59:14 -08:00
David Aguilar 1b1aeb289e git-cola 1.7.4
Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-01 22:19:47 -08:00
David Aguilar dc9bdddb3c widgets.remote: Fix remote branch labels
Two variables were renamed to the same thing.  Refactoring gone wild!

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-01 22:15:51 -08:00
David Aguilar 93164bf942 dag: Tighten fit-view-to-selection
Remove excess margin around the scene and tighten the
margins in the fit-to-view function as well.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-01 22:09:54 -08:00
David Aguilar e9ec0b6eda doc: Add 1.7.4 release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-01 21:25:46 -08:00
David Aguilar d7a7e677ad dag: Add gravatar icons and an elegant new design
Remove the author and summary fields from the display and
use dedicated QLabels to display them.  The diff displays
the extended description only.

Later work would be adding links to parents and children.
A corner widget menu might be appropriate.

Gravatar image lookups are done asynchronously and
use an internal pixmap when an image does not exist.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-01 18:47:44 -08:00
David Aguilar 2f6c2cd72c dag.view: Factor common methods into a helper class
ViewerMixin handles most of the common callbacks between the
list view and the graph view.  The "create patch code" is still
different but this hits all of the duplicates.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-01 17:06:56 -08:00
David Aguilar 6b395315be i18n: Allow hinted widgets to be translated
Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-01 16:50:40 -08:00
David Aguilar e7eb21e7f5 compat: Add hashlib.md5()
Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-01 16:49:13 -08:00
David Aguilar 7269d006f7 dag: Teach the model about the author email field
This will allow the DAG view to do something interesting
with the email address, e.g. use gravatar to display
email addresses.

Signed-off-by: David Aguilar <davvid@gmail.com>
2012-01-01 04:44:23 -08:00
David Aguilar 4070e6a742 widgets.commitmsg: Rename signal to avoid overloading Qt's signal
This is functionally equivalent and less likely to confuse someone
(me) in the future.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-12-31 21:33:27 -08:00
David Aguilar fc1e292125 widgets.commitmsg: Refine the keyboard navigation
Allow shift+up/down in the description field to select text
when on the first and last lines, respectively.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-12-31 20:28:20 -08:00
David Aguilar b90d34fcf0 widgets.commitmsg: Factor out the 'hint' text behavior
Create standalone widgets to display the 'Commit summary' and
'Extended description...' text hints.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-12-31 20:27:24 -08:00
David Aguilar 3d9819f68b main.view: Support 'cola.classicdockable' for backwards compatibility
The "dockable browser tool" feature was added long before the
'Classic' tool was rebranded as 'Browser'.  Although we don't
advertise it, support 'cola.classicdockable' so that existing
users are not affected by the rebranding.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-12-29 23:14:21 -08:00
David Aguilar 39ad9c8ee8 main.view: Avoid shortcut overloads when the 'Browser' tool is dockable
The "dockable browser tool" enabled by 'git config cola.browserdockable true'
is not a common feature but we support it so avoid introducing overloaded
keyboard shortcuts when configured this way.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-12-29 23:10:45 -08:00
David Aguilar 69f90dae5a browser: Rebrand the 'Classic' tool as 'Browser'
The 'Classic' tool was slightly inspired by classical "file-based"
browsers such as tkcvs and provides an entire view of the repo which
is not available in the focused 'Status' widget.  Likewise, it was
kind of funny to say `git cola classic`.

Inside jokes aside, it's better to rebrand this tool as the 'Browser'
since browsing is what it does.  The next thing it really needs is
a vim-like "/" search function to filter (or possibly just find) paths.
Being able to "chdir" into a subdirectory and see its contents only
would be a nice as would dedicated list, item, and tree modes.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-12-29 23:07:50 -08:00
David Aguilar c147b36506 classic: Make the title match 'cola' and 'dag'
Make the titles consistent for now.  These are different tools,
though, so we might want to be able to differentiate them one day.

We might want to make the titles say this eventually:

	cola: <project>: <branch>
	dag: <project>: <branch>
	browser: <project>: <branch>

..but for now we omit the "tool: " prefix to save panel real estate.

Having different logos might be a better way in the future instead
of changing the titles as well.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-12-29 22:39:20 -08:00
David Aguilar 8489f6419b classic: Use a QWidget for proper window management
The classic view stays on top of other windows because it is a QDialog.
It really should be a QWidget since we want other windows to be able to
float over it.

This makes it so that external difftools appear on top when launched.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-12-29 22:35:11 -08:00
David Aguilar 000b272486 widgets.commitmsg: Jump to the next line only when at EOL
Hitting 'Down' in the summary field is still useful for jumping
to the end of the summary.

Make the 'Down' key work by first jumping to the end of the line
(which is the default behavior we get for free from Qt) and then
a second down (when at the end of the line) jumps to the
description field.  This makes the summary field act just like
the description field which is another usability plus.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-12-29 22:26:22 -08:00
David Aguilar f0c3bbeede difftool: Add vim navigation to the file list
Eliminate custom options and a keyPressEvent() by using the
common tree implementation.  One benefit of doing this is that
we gain a keyPressEvent() which provides vim-style keyboard
navigation.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-12-29 22:19:48 -08:00
David Aguilar 99e67fd27a browse: Use the standard TreeView as a base class
Eliminate custom options and a keyPressEvent() by using the
common implementation.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-12-29 22:17:56 -08:00
David Aguilar 8195d3e211 classic: Use the standard TreeView as a base class
Eliminate custom options and a keyPressEvent() by using the
common implementation.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-12-29 22:16:50 -08:00
David Aguilar 1a61315114 widgets.standard: Add TreeView and TreeWidget classes
Add widgets to consolidate the various tree widget options and
enhancements used in cola.  The standard ones set common options
and override keyPressEvent() to provide vim-style navigation.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-12-29 22:13:13 -08:00
Uri Okrent b1de8addfa classic: look at path history on the current branch
classic was looking at global path history when displaying path details
and when showing the "Diff Against Predecessor" dialog, instead of
limiting the path's history to the current branch.

Signed-off-by: Uri Okrent <uokrent@gmail.com>
2011-12-29 11:18:17 +02:00
David Aguilar fa8ed6b9c3 widgets: Use the brushed metal style in more dialogs
Start enabling this in more places since it looks nicer.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-12-28 20:47:32 -08:00
David Aguilar e849844421 widgets.commitmsg: Navigate summary and description with arrow keys
Up and down arrows can now jump between the summary and description fields.
This makes them feel like a single field which improves usability.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-12-28 20:35:35 -08:00
David Aguilar a1e9785974 widgets.commitmsg: Make Up/Down arrows more useful
Pressing 'Up' while on the first line and 'Down' while on the last line
should move the cursor to the beginning/end of the line.

The summary works this way so the description editor should too.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-12-28 20:15:57 -08:00
David Aguilar dc2b920794 main.view: Use the brushed metal style on OS X
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-12-28 18:41:58 -08:00
David Aguilar 48fc0e41e5 dag: Drastically speedup rendering performance
Use an OpenGL viewport to speedup rendering.

This makes the DAG very fast even on very large graphs such as git.git.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-12-28 16:31:58 -08:00
David Aguilar 0fb081a33f dag: Use a slightly thinner edge thickness
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-12-28 16:30:52 -08:00
David Aguilar e7dcc99112 about: Improve the about dialog
Make the text easier to read and resurrect the git-cola logo.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-12-28 11:57:17 -08:00
37 changed files with 1728 additions and 1341 deletions
+1 -1
View File
@@ -66,7 +66,7 @@ You don't need to install cola to run it, though.
bin/git-cola
bin/git-dag
## MAC OS X INSTALLTION INSTRUCTIONS
## MAC OS X
Whether you install cola yourself with `make install` or
use the `git-cola.app` bundle, you will need to install
+10 -2
View File
@@ -4,9 +4,16 @@
import glob
import optparse
import os
import platform
import signal
import sys
# Make homebrew work by default
if platform.system() == 'Darwin':
homebrew_mods = '/usr/local/lib/python'
if os.path.isdir(homebrew_mods):
sys.path.append(homebrew_mods)
try:
from PyQt4 import QtGui
from PyQt4 import QtCore
@@ -131,6 +138,7 @@ class ColaApplication(object):
def parse_args(context):
args = sys.argv[1:]
builtins = set(('branch',
'browse',
'classic',
'dag',
'fetch',
@@ -240,10 +248,10 @@ def main(context):
# Show the GUI
if context == 'branch':
view = create_new_branch()
elif context == 'git-dag' or context == 'dag':
elif context in ('git-dag', 'dag'):
ctl = git_dag(model, opts=opts, args=args)
view = ctl.view
elif context == 'classic':
elif context in ('classic', 'browse'):
view = cola_classic(update=False)
# TODO: the calls to update_status() can be done asynchronously
# by hooking into the message_updated notification.
+2 -1
View File
@@ -4,6 +4,7 @@ import sys
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import Qt
from PyQt4.QtCore import SIGNAL
from cola import core
@@ -28,7 +29,7 @@ class BookmarksDialog(standard.Dialog):
self.resize(494, 238)
self.setWindowTitle(self.tr('Bookmarks'))
self.setWindowModality(QtCore.Qt.WindowModal)
self.setWindowModality(Qt.WindowModal)
self.layt = QtGui.QVBoxLayout(self)
self.layt.setMargin(defs.margin)
self.layt.setSpacing(defs.spacing)
+5 -6
View File
@@ -7,10 +7,9 @@ import cola.utils
import cola.difftool
from cola import gitcmds
from cola import qtutils
from cola import signals
from cola.widgets.selectcommits import select_commits
from cola.classic.view import RepoDialog
from cola.classic.view import Browser
from cola.classic.model import GitRepoModel
from cola.classic.model import GitRepoEntryManager
from cola.compat import set
@@ -18,15 +17,15 @@ from cola.compat import set
def widget(parent, update=True):
"""Return a widget for immediate use."""
view = RepoDialog(parent, update=update)
view = Browser(parent, update=update)
view.tree.setModel(GitRepoModel(view.tree))
ctl = ClassicController(view.tree)
view.ctl = ClassicController(view.tree)
return view
def cola_classic(update=True):
"""Launch a new cola classic session."""
view = widget(qtutils.active_window(), update=update)
view = widget(None, update=update)
view.show()
return view
@@ -65,7 +64,7 @@ class ClassicController(QtCore.QObject):
def difftool_predecessor(self, paths):
"""Prompt for an older commit and launch difftool against it."""
args = ['--'] + paths
revs, summaries = gitcmds.log_helper(all=True, extra_args=args)
revs, summaries = gitcmds.log_helper(all=False, extra_args=args)
commits = select_commits('Select Previous Version',
revs, summaries, multiselect=False)
if not commits:
+1 -1
View File
@@ -290,7 +290,7 @@ class GitRepoInfoTask(QRunnable):
if not self._data:
log_line = cola.model().git.log('-1', '--', self.path,
M=True,
all=True,
all=False,
no_color=True,
pretty='format:%ar%x01%s%x01%an')
if log_line:
+9 -91
View File
@@ -1,7 +1,6 @@
import os
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import Qt
from PyQt4.QtCore import SIGNAL
@@ -15,10 +14,9 @@ from cola.widgets import standard
from cola.classic.model import GitRepoNameItem
class RepoDialog(standard.Dialog):
class Browser(standard.Widget):
def __init__(self, parent, update=True):
standard.Dialog.__init__(self, parent)
self.setObjectName('classic')
super(Browser, self).__init__(parent)
self.tree = RepoTreeView(self)
self.mainlayout = QtGui.QHBoxLayout()
self.setLayout(self.mainlayout)
@@ -48,22 +46,18 @@ class RepoDialog(standard.Dialog):
self.setToolTip(msg)
title = '%s [%s]' % (self.model.project, branch)
title = '%s: %s' % (self.model.project, branch)
if self.mode == self.model.mode_amend:
title += ' ** amending **'
self.setWindowTitle(title)
class RepoTreeView(QtGui.QTreeView):
class RepoTreeView(standard.TreeView):
"""Provides a filesystem-like view of a git repository."""
def __init__(self, parent):
QtGui.QTreeView.__init__(self, parent)
super(RepoTreeView, self).__init__(parent)
self.setSortingEnabled(False)
self.setAllColumnsShowFocus(True)
self.setAlternatingRowColors(True)
self.setUniformRowHeights(True)
self.setAnimated(True)
self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
# Observe model updates
@@ -73,6 +67,10 @@ class RepoTreeView(QtGui.QTreeView):
# The non-Qt cola application model
self.connect(self, SIGNAL('expanded(QModelIndex)'), self.size_columns)
self.connect(self, SIGNAL('collapsed(QModelIndex)'), self.size_columns)
# Sync selection before the key press event changes the model index
self.connect(self, SIGNAL('indexAboutToChange()'), self.sync_selection)
self.action_history =\
self._create_action('View History...',
'View history for selected path(s).',
@@ -145,86 +143,6 @@ class RepoTreeView(QtGui.QTreeView):
menu.addAction(self.action_revert)
menu.exec_(self.mapToGlobal(event.pos()))
def keyPressEvent(self, event):
"""
Override keyPressEvent to allow LeftArrow to work on non-directories.
When LeftArrow is pressed on a file entry or an unexpanded directory,
then move the current index to the parent directory.
This simplifies navigation using the keyboard.
For power-users, we support Vim keybindings ;-P
"""
# Check whether the item is expanded before calling the base class
# keyPressEvent otherwise we end up collapsing and changing the
# current index in one shot, which we don't want to do.
index = self.currentIndex()
was_expanded = self.isExpanded(index)
was_collapsed = not was_expanded
# Vim keybindings...
# Rewrite the event before marshalling to QTreeView.event()
key = event.key()
# Remap 'H' to 'Left'
if key == QtCore.Qt.Key_H:
event = QtGui.QKeyEvent(event.type(),
QtCore.Qt.Key_Left,
event.modifiers())
# Remap 'J' to 'Down'
elif key == QtCore.Qt.Key_J:
event = QtGui.QKeyEvent(event.type(),
QtCore.Qt.Key_Down,
event.modifiers())
# Remap 'K' to 'Up'
elif key == QtCore.Qt.Key_K:
event = QtGui.QKeyEvent(event.type(),
QtCore.Qt.Key_Up,
event.modifiers())
# Remap 'L' to 'Right'
elif key == QtCore.Qt.Key_L:
event = QtGui.QKeyEvent(event.type(),
QtCore.Qt.Key_Right,
event.modifiers())
# Re-read the event key to take the remappings into account
key = event.key()
# Process the keyPressEvent before changing the current index
# otherwise the event will affect the new index set here
# instead of the original index.
result = QtGui.QTreeView.keyPressEvent(self, event)
# Sync the selection model
self.sync_selection()
# Try to select the first item if the model index is invalid
if not index.isValid():
index = self.model().index(0, 0, QtCore.QModelIndex())
if index.isValid():
self.setCurrentIndex(index)
return result
# Automatically select the first entry when expanding a directory
if (key == QtCore.Qt.Key_Right and was_collapsed and
self.isExpanded(index)):
index = self.moveCursor(self.MoveDown, event.modifiers())
self.setCurrentIndex(index)
# Process non-root entries with valid parents only.
if key == QtCore.Qt.Key_Left and index.parent().isValid():
# File entries have rowCount() == 0
if self.item_from_index(index).rowCount() == 0:
self.setCurrentIndex(index.parent())
# Otherwise, do this for collapsed directories only
elif was_collapsed:
self.setCurrentIndex(index.parent())
return result
def mousePressEvent(self, event):
"""Synchronize the selection on mouse-press."""
result = QtGui.QTreeView.mousePressEvent(self, event)
+6
View File
@@ -12,3 +12,9 @@ except ImportError:
@staticmethod
def new(*args):
return md5.new()
@classmethod
def md5(cls, value=''):
obj = md5.new()
obj.update(value)
return obj
+1 -16
View File
@@ -8,7 +8,6 @@ if __name__ == '__main__':
src = os.path.join(os.path.dirname(__file__), '..', '..')
sys.path.insert(1, os.path.abspath(src))
from cola import qtutils
from cola.dag.view import DAGView
from cola.dag.model import DAG
from cola.dag.controller import DAGController
@@ -22,20 +21,6 @@ def git_dag(model, opts=None, args=None):
view = DAGView(model, dag, None)
ctl = DAGController(dag, view)
view.show()
view.raise_()
if dag.ref:
view.thread.start(QtCore.QThread.LowPriority)
view.display()
return ctl
if __name__ == "__main__":
import cola
from cola import app
model = cola.model()
model.set_worktree(os.getcwd())
model.update_status()
app = app.ColaApplication(sys.argv)
ctl = git_dag(model)
sys.exit(app.exec_())
+14 -7
View File
@@ -3,16 +3,16 @@ import subprocess
import os
import cola
from cola import core
from cola import git
from cola.git import git
from cola import signals
from cola import utils
from cola.cmds import BaseCommand
from cola.observable import Observable
# put summary at the end b/c it can contain
# any number of funky characters
logfmt = 'format:%H%x01%P%x01%d%x01%an%x01%aD%x01%s'
git = git.instance()
# any number of funky characters, including the separator
logfmt = 'format:%H%x01%P%x01%d%x01%an%x01%ar%x01%ae%x01%s'
logsep = chr(0x01)
archive = 'archive'
@@ -101,6 +101,7 @@ class Commit(object):
'tags',
'author',
'authdate',
'email',
'generation',
'parsed')
def __init__(self, sha1=None, log_entry=None):
@@ -109,6 +110,7 @@ class Commit(object):
self.parents = []
self.children = []
self.tags = set()
self.email = None
self.author = None
self.authdate = None
self.parsed = False
@@ -116,10 +118,10 @@ class Commit(object):
if log_entry:
self.parse(log_entry)
def parse(self, log_entry):
def parse(self, log_entry, sep=logsep):
self.sha1 = log_entry[:40]
(parents, tags, author, authdate, summary) = \
log_entry[41:].split(chr(0x01), 5)
(parents, tags, author, authdate, email, summary) = \
log_entry[41:].split(sep, 6)
if summary:
self.summary = core.decode(summary)
@@ -150,6 +152,8 @@ class Commit(object):
self.author = core.decode(author)
if authdate:
self.authdate = authdate
if email:
self.email = core.decode(email)
self.parsed = True
return self
@@ -214,17 +218,20 @@ class RepoReader(object):
except IndexError:
self._idx = -1
raise StopIteration
if self._proc is None:
ref_args = utils.shell_split(self.dag.ref)
cmd = self._cmd + ['-%d' % self.dag.count] + ref_args
self._proc = utils.start_command(cmd)
self._topo_list = []
log_entry = self._proc.stdout.readline().rstrip()
if not log_entry:
del self._proc
self._cached = True
self._proc = None
raise StopIteration
sha1 = log_entry[:40]
try:
return self._objects[sha1]
+505 -315
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -1,11 +1,13 @@
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import Qt
from PyQt4.QtCore import SIGNAL
from cola import utils
from cola import qtutils
from cola import gitcmds
from cola.widgets import defs
from cola.widgets import standard
def launch(args):
@@ -31,7 +33,8 @@ def diff_expression(parent, expr):
class FileDiffDialog(QtGui.QDialog):
def __init__(self, parent, a=None, b=None, expr=None):
QtGui.QDialog.__init__(self, parent)
super(FileDiffDialog, self).__init__(parent)
self.setAttribute(Qt.WA_MacMetalStyle)
self.a = a
self.b = b
self.expr = expr
@@ -39,12 +42,9 @@ class FileDiffDialog(QtGui.QDialog):
self.setWindowTitle('Select File(s)')
self.setWindowModality(QtCore.Qt.WindowModal)
self._tree = QtGui.QTreeWidget(self)
self._tree.setAlternatingRowColors(True)
self._tree = standard.TreeWidget(self)
self._tree.setRootIsDecorated(False)
self._tree.setSelectionMode(self._tree.ExtendedSelection)
self._tree.setUniformRowHeights(True)
self._tree.setAllColumnsShowFocus(True)
self._tree.setHeaderHidden(True)
self._diff_btn = QtGui.QPushButton('Compare')
+4 -5
View File
@@ -224,9 +224,8 @@ class GitConfig(observable.Observable):
return opts
def get_guitool_names(self):
cmds = []
guitools = self.find('guitool.*.cmd')
for name, cmd in guitools.items():
name = name[len('guitool.'):-len('.cmd')]
cmds.append(name)
return cmds
prefix = len('guitool.')
suffix = len('.cmd')
return sorted([name[prefix:-suffix]
for (name, cmd) in guitools.items()])
+6 -16
View File
@@ -215,22 +215,12 @@ def sha1_diff(sha1, git=git):
return core.decode(git.diff(sha1 + '^!', **_common_diff_opts()))
def diff_info(sha1, git=git, merge=True):
if merge:
mergefmt = 'Merge: %P%n'
else:
mergefmt = ''
log = git.log('-1',
'--pretty=format:'
'commit %H%n' +
mergefmt +
'Author: %aN <%aE>%n'
'Date: %aD%n'
'%n%s%n%n%b',
sha1)
return core.decode(log) + '\n\n' + sha1_diff(sha1)
def diff_info(sha1, git=git):
log = git.log('-1', '--pretty=format:%b', sha1)
decoded = core.decode(log).strip()
if decoded:
decoded += '\n\n'
return decoded + sha1_diff(sha1)
def diff_helper(commit=None,
+12 -7
View File
@@ -56,7 +56,8 @@ def branch_delete():
def diff_revision():
"""Diff an arbitrary revision against the worktree"""
ref = choose_ref('Select Revision to Diff', 'Diff', pre=['HEAD^'])
ref = choose_ref('Select Revision to Diff', 'Diff',
default='HEAD^')
if not ref:
return
difftool.diff_commits(qtutils.active_window(), ref, None)
@@ -167,7 +168,14 @@ def export_patches():
def diff_expression():
"""Diff using an arbitrary expression."""
ref = choose_ref('Enter Diff Expression', 'Diff')
tracked = gitcmds.tracked_branch()
current = gitcmds.current_branch()
if tracked and current:
default = tracked + '..' + current
else:
default = 'origin/master..'
ref = choose_ref('Enter Diff Expression', 'Diff',
default=default)
if not ref:
return
difftool.diff_expression(qtutils.active_window(), ref)
@@ -217,12 +225,9 @@ def rebase():
qtutils.log(status, output)
def choose_ref(title, button_text, pre=None):
provider = None
if pre:
provider = qt.GitRefProvider(pre=pre)
def choose_ref(title, button_text, default=None):
parent = qtutils.active_window()
return qt.GitRefDialog.ref(title, button_text, parent, provider=provider)
return qt.GitRefDialog.ref(title, button_text, parent, default=default)
def review_branch():
+4 -3
View File
@@ -353,17 +353,18 @@ class MainModel(Observable):
return self.run_remote_action(self.git.fetch, remote, **opts)
def push(self, remote, **opts):
return self.run_remote_action(self.git.fetch, remote, push=True, **opts)
return self.run_remote_action(self.git.push, remote, push=True, **opts)
def pull(self, remote, **opts):
return self.run_remote_action(self.git.pull, remote, push=True, **opts)
def create_branch(self, name, base, track=False):
def create_branch(self, name, base, track=False, force=False):
"""Create a branch named 'name' from revision 'base'
Pass track=True to create a local tracking branch.
"""
return self.git.branch(name, base, track=track,
return self.git.branch(name, base,
track=track, force=force,
with_stderr=True,
with_status=True)
+22 -34
View File
@@ -59,7 +59,7 @@ from cola.widgets.search import search
class MainView(standard.MainWindow):
def __init__(self, model, parent):
standard.MainWindow.__init__(self, parent)
super(MainView, self).__init__(parent)
# Default size; this is thrown out when save/restore is used
self.resize(987, 610)
self.model = model
@@ -73,14 +73,16 @@ class MainView(standard.MainWindow):
self.merge_message_hash = ''
self.setAcceptDrops(True)
self.setAttribute(Qt.WA_MacMetalStyle)
# Dockwidget options
qtcompat.set_common_dock_options(self)
self.classic_dockable = gitcfg.instance().get('cola.classicdockable')
cfg = gitcfg.instance()
self.classic_dockable = (cfg.get('cola.browserdockable') or
cfg.get('cola.classicdockable'))
if self.classic_dockable:
self.classicdockwidget = create_dock('Classic', self)
self.classicdockwidget = create_dock('Browser', self)
self.classicwidget = classic_widget(self)
self.classicdockwidget.setWidget(self.classicwidget)
@@ -118,7 +120,7 @@ class MainView(standard.MainWindow):
SIGNAL(signals.load_previous_message))
self.commitdockwidget.setWidget(self.commitmsgeditor)
# "Command Output" widget
# "Console" widget
logwidget = qtutils.logger()
logwidget.setFont(diff_font())
self.logdockwidget = create_dock('Console', self)
@@ -253,7 +255,7 @@ class MainView(standard.MainWindow):
'Review...', guicmds.review_branch)
self.menu_classic = add_action(self,
'Cola Classic...', cola_classic)
'Browser...', cola_classic)
self.menu_classic.setIcon(qtutils.git_icon())
self.menu_dag = add_action(self,
@@ -261,11 +263,15 @@ class MainView(standard.MainWindow):
self.menu_dag.setIcon(qtutils.git_icon())
# Relayed actions
status_tree = self.statusdockwidget.widget().tree
self.addAction(status_tree.up)
self.addAction(status_tree.down)
self.addAction(status_tree.process_selection)
self.addAction(status_tree.launch_difftool)
if not self.classic_dockable:
# These shortcuts conflict with those from the
# 'Browser' widget so don't register them when
# the browser is a dockable tool.
status_tree = self.statusdockwidget.widget().tree
self.addAction(status_tree.up)
self.addAction(status_tree.down)
self.addAction(status_tree.process_selection)
self.addAction(status_tree.launch_difftool)
# Create the application menu
self.menubar = QtGui.QMenuBar(self)
@@ -408,7 +414,6 @@ class MainView(standard.MainWindow):
self.connect(self.commitmsgeditor, SIGNAL('cursorPosition(int,int)'),
self.show_cursor_position)
self.connect(self, SIGNAL('update'), self._update_callback)
self.connect(self, SIGNAL('apply_state'), self.apply_state)
self.connect(self, SIGNAL('install_config_actions'),
self._install_config_actions)
@@ -417,8 +422,9 @@ class MainView(standard.MainWindow):
self.install_config_actions()
# Restore saved settings
self._gui_state_task = None
self._load_gui_state()
qtutils.apply_state(self)
self.statusdockwidget.widget().setFocus()
log(0, version.git_version_str() + '\ncola version ' + version.version())
@@ -503,32 +509,14 @@ class MainView(standard.MainWindow):
def apply_state(self, state):
"""Imports data for save/restore"""
# 1 is the widget version; change when widgets are added/removed
standard.MainWindow.apply_state(self, state)
super(MainView, self).apply_state(state)
qtutils.apply_window_state(self, state, 1)
def export_state(self):
"""Exports data for save/restore"""
state = standard.MainWindow.export_state(self)
state = super(MainView, self).export_state()
return qtutils.export_window_state(self, state, self.widget_version)
def _load_gui_state(self):
"""Restores the gui from the preferences file."""
self._gui_state_task = self._start_gui_state_loading_thread()
def _start_gui_state_loading_thread(self):
"""Do expensive file reading and json decoding in the background"""
class LoadGUIStateTask(QtCore.QRunnable):
def __init__(self, sender):
QtCore.QRunnable.__init__(self)
self._sender = sender
def run(self):
state = settings.Settings().get_gui_state(self._sender)
self._sender.emit(SIGNAL('apply_state'), state)
task = LoadGUIStateTask(self)
QtCore.QThreadPool.globalInstance().start(task)
return task
def setup_dockwidget_tools_menu(self):
# Hotkeys for toggling the dock widgets
dockwidgets = (
+2 -2
View File
@@ -3,8 +3,8 @@ from PyQt4.QtCore import Qt
from PyQt4.QtCore import SIGNAL
from cola import qtutils
from cola.qt import GitRefLineEdit
from cola.qtutils import tr
from cola.widgets import completion
from cola.widgets import defs
from cola.merge.model import merge, visualize_revision
@@ -22,7 +22,7 @@ class MergeView(QtGui.QDialog):
self.revision_label = QtGui.QLabel()
self.revision_label.setText(tr('Revision To Merge'))
self.revision = GitRefLineEdit()
self.revision = completion.GitRefLineEdit()
self.radio_local = QtGui.QRadioButton()
self.radio_local.setText(tr('Local Branch'))
+14 -399
View File
@@ -1,5 +1,4 @@
import re
import subprocess
from PyQt4 import QtGui
from PyQt4 import QtCore
@@ -10,11 +9,10 @@ from PyQt4.QtGui import QSyntaxHighlighter
from PyQt4.QtGui import QTextCharFormat
from PyQt4.QtGui import QColor
import cola
from cola import utils
from cola import qtutils
from cola.compat import set
from cola.qtutils import tr
from cola.widgets import completion
from cola.widgets import defs
@@ -161,7 +159,7 @@ class ExpandableGroupBox(QtGui.QGroupBox):
self.emit(SIGNAL('expanded(bool)'), expanded)
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
if event.button() == Qt.LeftButton:
option = QtGui.QStyleOptionGroupBox()
self.initStyleOption(option)
icon_size = self.arrow_icon_size
@@ -174,7 +172,7 @@ class ExpandableGroupBox(QtGui.QGroupBox):
QtGui.QGroupBox.mousePressEvent(self, event)
def mouseReleaseEvent(self, event):
if (event.button() == QtCore.Qt.LeftButton and
if (event.button() == Qt.LeftButton and
self.click_pos == event.pos()):
self.set_expanded(not self.expanded)
QtGui.QGroupBox.mouseReleaseEvent(self, event)
@@ -185,7 +183,7 @@ class ExpandableGroupBox(QtGui.QGroupBox):
self.initStyleOption(option)
painter.save()
painter.translate(self.arrow_icon_size + defs.spacing, 0)
painter.drawText(option.rect, QtCore.Qt.AlignLeft, self.title())
painter.drawText(option.rect, Qt.AlignLeft, self.title())
painter.restore()
style = QtGui.QStyle
@@ -198,55 +196,15 @@ class ExpandableGroupBox(QtGui.QGroupBox):
painter.drawPrimitive(style.PE_IndicatorArrowRight, option)
class GitRefCompleter(QtGui.QCompleter):
"""Provides completion for branches and tags"""
def __init__(self, parent, provider=None):
QtGui.QCompleter.__init__(self, parent)
self._model = GitRefModel(parent, provider=provider)
self.setModel(self._model)
self.setCompletionMode(self.UnfilteredPopupCompletion)
self.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
def __del__(self):
self.dispose()
def dispose(self):
self._model.dispose()
class GitRefLineEdit(QtGui.QLineEdit):
def __init__(self, parent=None, provider=None):
QtGui.QLineEdit.__init__(self, parent)
self.refcompleter = GitRefCompleter(self, provider=provider)
self.setCompleter(self.refcompleter)
self.refcompleter.popup().installEventFilter(self)
def eventFilter(self, obj, event):
"""Fix an annoyance on OS X
The completer popup steals focus. Work around it.
This affects dialogs without QtCore.Qt.WindowModal modality.
"""
if obj == self.refcompleter.popup():
if event.type() == QtCore.QEvent.FocusIn:
return True
return False
def mouseReleaseEvent(self, event):
super(GitRefLineEdit, self).mouseReleaseEvent(event)
self.refcompleter.complete()
class GitRefDialog(QtGui.QDialog):
def __init__(self, title, button_text, parent, provider=None):
def __init__(self, title, button_text, parent):
super(GitRefDialog, self).__init__(parent)
self.setWindowTitle(title)
self.label = QtGui.QLabel()
self.label.setText(title)
self.lineedit = GitRefLineEdit(self, provider=provider)
self.lineedit = completion.GitRefLineEdit(self)
self.setFocusProxy(self.lineedit)
self.ok_button = QtGui.QPushButton()
@@ -278,7 +236,7 @@ class GitRefDialog(QtGui.QDialog):
self.connect(self.lineedit, SIGNAL('textChanged(QString)'),
self.text_changed)
self.setWindowModality(QtCore.Qt.WindowModal)
self.setWindowModality(Qt.WindowModal)
self.ok_button.setEnabled(False)
def text(self):
@@ -287,9 +245,14 @@ class GitRefDialog(QtGui.QDialog):
def text_changed(self, txt):
self.ok_button.setEnabled(bool(self.text()))
def set_text(self, ref):
self.lineedit.setText(ref)
@staticmethod
def ref(title, button_text, parent, provider=None):
dlg = GitRefDialog(title, button_text, parent, provider=provider)
def ref(title, button_text, parent, default=None):
dlg = GitRefDialog(title, button_text, parent)
if default:
dlg.set_text(default)
dlg.show()
dlg.raise_()
dlg.setFocus()
@@ -298,354 +261,6 @@ class GitRefDialog(QtGui.QDialog):
else:
return None
class GitRefProvider(QtCore.QObject):
def __init__(self, pre=None):
super(GitRefProvider, self).__init__()
if pre:
self.pre = pre
else:
self.pre = []
self.model = model = cola.model()
msg = model.message_updated
model.add_observer(msg, self.emit_updated)
def emit_updated(self):
self.emit(SIGNAL('updated()'))
def matches(self):
model = self.model
return self.pre + model.local_branches + model.remote_branches + model.tags
def dispose(self):
self.model.remove_observer(self.emit_updated)
class GitRefModel(QtGui.QStandardItemModel):
def __init__(self, parent, provider=None):
QtGui.QStandardItemModel.__init__(self, parent)
if provider is None:
provider = GitRefProvider()
self.provider = provider
self.update_matches()
self.connect(self.provider, SIGNAL('updated()'),
self.update_matches)
def dispose(self):
self.provider.dispose()
def update_matches(self):
QStandardItem = QtGui.QStandardItem
self.clear()
for match in self.provider.matches():
item = QStandardItem()
item.setIcon(qtutils.git_icon())
item.setText(match)
self.appendRow(item)
class UpdateGitLogCompletionModelThread(QtCore.QThread):
def __init__(self, model):
QtCore.QThread.__init__(self)
self.model = model
self.case_insensitive = False
def run(self):
text = None
# Loop when the matched text changes between the start and end time.
# This happens when gather_matches() takes too long and the
# model's matched_text changes in-between.
while text != self.model.matched_text:
text = self.model.matched_text
items = self.model.gather_matches(self.case_insensitive)
self.emit(SIGNAL('items_gathered'), *items)
class GitLogCompletionModel(QtGui.QStandardItemModel):
def __init__(self, parent):
self.matched_text = None
QtGui.QStandardItemModel.__init__(self, parent)
self.cmodel = cola.model()
self.update_thread = UpdateGitLogCompletionModelThread(self)
self.connect(self.update_thread, SIGNAL('items_gathered'),
self.apply_matches)
def lower_cmp(self, a, b):
return cmp(a.replace('.','').lower(), b.replace('.','').lower())
def update_matches(self, case_insensitive):
self.update_thread.case_insensitive = case_insensitive
if not self.update_thread.isRunning():
self.update_thread.start()
def gather_matches(self, case_sensitive):
file_list = self.cmodel.everything()
files = set(file_list)
files_and_dirs = utils.add_parents(set(files))
dirs = files_and_dirs.difference(files)
model = self.cmodel
refs = model.local_branches + model.remote_branches + model.tags
matched_text = self.matched_text
if matched_text:
if case_sensitive:
matched_refs = [r for r in refs if matched_text in r]
else:
matched_refs = [r for r in refs
if matched_text.lower() in r.lower()]
else:
matched_refs = refs
matched_refs.sort(cmp=self.lower_cmp)
if matched_text:
if case_sensitive:
matched_paths = [f for f in files_and_dirs
if matched_text in f]
else:
matched_paths = [f for f in files_and_dirs
if matched_text.lower() in f.lower()]
else:
matched_paths = list(files_and_dirs)
matched_paths.sort(cmp=self.lower_cmp)
return (matched_refs, matched_paths, dirs)
def apply_matches(self, matched_refs, matched_paths, dirs):
QStandardItem = QtGui.QStandardItem
file_icon = qtutils.file_icon()
dir_icon = qtutils.dir_icon()
git_icon = qtutils.git_icon()
matched_text = self.matched_text
items = []
for ref in matched_refs:
item = QStandardItem()
item.setText(ref)
item.setIcon(git_icon)
items.append(item)
if matched_paths and (not matched_text or matched_text in '--'):
item = QStandardItem()
item.setText('--')
item.setIcon(file_icon)
items.append(item)
for match in matched_paths:
item = QStandardItem()
item.setText(match)
if match in dirs:
item.setIcon(dir_icon)
else:
item.setIcon(file_icon)
items.append(item)
self.clear()
self.invisibleRootItem().appendRows(items)
def set_match_text(self, text, case_sensitive):
self.matched_text = text
self.update_matches(case_sensitive)
class GitLogLineEdit(QtGui.QLineEdit):
def __init__(self, parent=None):
QtGui.QLineEdit.__init__(self, parent)
# used to hide the completion popup after a drag-select
self._drag = 0
self._model = GitLogCompletionModel(self)
self._delegate = HighlightCompletionDelegate(self)
self._completer = QtGui.QCompleter(self)
self._completer.setWidget(self)
self._completer.setModel(self._model)
self._completer.setCompletionMode(
QtGui.QCompleter.UnfilteredPopupCompletion)
self._completer.popup().setItemDelegate(self._delegate)
self.connect(self._completer, SIGNAL('activated(QString)'),
self._complete)
self.connect(self, SIGNAL('textChanged(QString)'), self._text_changed)
self._keys_to_ignore = set([QtCore.Qt.Key_Enter,
QtCore.Qt.Key_Return,
QtCore.Qt.Key_Escape])
def is_case_sensitive(self, text):
return bool([char for char in text if char.isupper()])
def _text_changed(self, text):
text = self.last_word()
case_sensitive = self.is_case_sensitive(text)
if case_sensitive:
self._completer.setCaseSensitivity(QtCore.Qt.CaseSensitive)
else:
self._completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self._delegate.set_highlight_text(text, case_sensitive)
self._model.set_match_text(text, case_sensitive)
def update_matches(self):
text = self.last_word()
case_sensitive = self.is_case_sensitive(text)
self._model.update_matches(case_sensitive)
def _complete(self, completion):
"""
This is the event handler for the QCompleter.activated(QString) signal,
it is called when the user selects an item in the completer popup.
"""
if not completion:
return
words = self.words()
if words:
words.pop()
words.append(unicode(completion))
self.setText(subprocess.list2cmdline(words))
self.emit(SIGNAL('ref_changed'))
def words(self):
return utils.shell_usplit(unicode(self.text()))
def last_word(self):
words = self.words()
if not words:
return unicode(self.text())
if not words[-1]:
return u''
return words[-1]
def event(self, event):
if event.type() == QtCore.QEvent.KeyPress:
if (event.key() == QtCore.Qt.Key_Tab and
self._completer.popup().isVisible()):
event.ignore()
return True
return QtGui.QLineEdit.event(self, event)
def do_completion(self):
self._completer.popup().setCurrentIndex(
self._completer.completionModel().index(0,0))
self._completer.complete()
def keyPressEvent(self, event):
if self._completer.popup().isVisible():
if event.key() in self._keys_to_ignore:
event.ignore()
self._complete(self.last_word())
return
elif (event.key() == QtCore.Qt.Key_Down and
self._completer.completionCount() > 0):
event.accept()
self.do_completion()
return
QtGui.QLineEdit.keyPressEvent(self, event)
prefix = self.last_word()
if prefix != unicode(self._completer.completionPrefix()):
self._update_popup_items(prefix)
if len(event.text()) > 0 and len(prefix) > 0:
self._completer.complete()
if len(prefix) == 0:
self._completer.popup().hide()
#: _drag: 0 - unclicked, 1 - clicked, 2 - dragged
def mousePressEvent(self, event):
self._drag = 1
return QtGui.QLineEdit.mousePressEvent(self, event)
def mouseMoveEvent(self, event):
if self._drag == 1:
self._drag = 2
return QtGui.QLineEdit.mouseMoveEvent(self, event)
def mouseReleaseEvent(self, event):
if self._drag != 2 and event.buttons() != QtCore.Qt.RightButton:
self.do_completion()
self._drag = 0
return QtGui.QLineEdit.mouseReleaseEvent(self, event)
def close_popup(self):
self._completer.popup().close()
def _update_popup_items(self, prefix):
"""
Filters the completer's popup items to only show items
with the given prefix.
"""
self._completer.setCompletionPrefix(prefix)
self._completer.popup().setCurrentIndex(
self._completer.completionModel().index(0,0))
class HighlightCompletionDelegate(QtGui.QStyledItemDelegate):
"""A delegate used for auto-completion to give formatted completion"""
def __init__(self, parent=None): # model, parent=None):
QtGui.QStyledItemDelegate.__init__(self, parent)
self.highlight_text = ''
self.case_sensitive = False
self.doc = QtGui.QTextDocument()
self.doc.setDocumentMargin(0)
def set_highlight_text(self, text, case_sensitive):
"""Sets the text that will be made bold in the term name when displayed"""
self.highlight_text = text
self.case_sensitive = case_sensitive
def paint(self, painter, option, index):
"""Overloaded Qt method for custom painting of a model index"""
if not self.highlight_text:
return QtGui.QStyledItemDelegate.paint(self, painter, option, index)
text = unicode(index.data().toPyObject())
if self.case_sensitive:
html = text.replace(self.highlight_text,
'<strong>%s</strong>' % self.highlight_text)
else:
match = re.match('(.*)(' + self.highlight_text + ')(.*)',
text, re.IGNORECASE)
if match:
start = match.group(1) or ''
middle = match.group(2) or ''
end = match.group(3) or ''
html = (start + ('<strong>%s</strong>' % middle) + end)
else:
html = text
self.doc.setHtml(html)
# Painting item without text, Text Document will paint the text
optionV4 = QtGui.QStyleOptionViewItemV4(option)
self.initStyleOption(optionV4, index)
optionV4.text = QtCore.QString()
style = QtGui.QApplication.style()
style.drawControl(QtGui.QStyle.CE_ItemViewItem, optionV4, painter)
ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
# Highlighting text if item is selected
if (optionV4.state & QtGui.QStyle.State_Selected):
ctx.palette.setColor(QtGui.QPalette.Text, optionV4.palette.color(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText))
# translate the painter to where the text is drawn
textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, optionV4)
painter.save()
start = textRect.topLeft() + QtCore.QPoint(3, 0)
painter.translate(start)
painter.setClipRect(textRect.translated(-start))
# tell the text document to draw the html for us
self.doc.documentLayout().draw(painter, ctx)
painter.restore()
# Syntax highlighting
def TERMINAL(pattern):
+3 -1
View File
@@ -2,6 +2,7 @@
from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4.QtCore import Qt
from PyQt4.QtCore import SIGNAL
from cola import qt
@@ -15,7 +16,8 @@ from cola.widgets.diff import DiffTextEdit
class StashView(standard.Dialog):
def __init__(self, model, parent=None):
standard.Dialog.__init__(self, parent=parent)
super(StashView, self).__init__(parent=parent)
self.setAttribute(Qt.WA_MacMetalStyle)
self.model = model
self.stashes = []
self.revids = []
+1 -1
View File
@@ -2,7 +2,7 @@
"""Provide cola's version number"""
# The current git-cola version
_default_version = '1.7.3'
_default_version = '1.7.5'
import os
+44 -34
View File
@@ -1,26 +1,23 @@
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import SIGNAL
from PyQt4.QtCore import Qt
from cola import resources
from cola import qtutils
from cola import utils
from cola import version
from cola.prefs import diff_font
from cola.widgets import defs
def launch_about_dialog():
"""Launches the Help -> About dialog"""
view = AboutView(qtutils.active_window())
view.show()
view.set_version(version.version())
view.show()
COPYRIGHT = """git-cola: The highly caffeinated git GUI v$VERSION
git-cola is a sweet, carbonated git GUI known for its
sugary flavor and caffeine-inspired features.
Copyright (C) 2007-2012, David Aguilar and contributors
This program is free software: you can redistribute it and/or
@@ -47,39 +44,52 @@ class AboutView(QtGui.QDialog):
QtGui.QDialog.__init__(self, parent)
self.setWindowTitle('About git-cola')
self.setStyleSheet('QWidget { background-color: white; color: #666; }')
self.setWindowModality(Qt.WindowModal)
self.setMinimumSize(QtCore.QSize(280, 420))
self.setMaximumSize(QtCore.QSize(280, 420))
self.label = QtGui.QLabel()
self.pixmap = QtGui.QPixmap('icons:logo-top.png')
#self.label.setStyleSheet('QWidget {background: #000; }')
self.label.setPixmap(self.pixmap)
self.label.setAlignment(Qt.AlignRight | Qt.AlignTop)
self.ham = QtGui.QLabel(self)
self.ham.setGeometry(QtCore.QRect(0, -10, 291, 121))
self.ham.setPixmap(QtGui.QPixmap('images:logo-cola.png'))
palette = self.label.palette()
palette.setColor(QtGui.QPalette.Window, Qt.black)
self.label.setAutoFillBackground(True)
self.label.setPalette(palette)
self.spam = QtGui.QLabel(self)
self.spam.setGeometry(QtCore.QRect(10, 110, 261, 261))
self.text = QtGui.QTextEdit()
self.text.setReadOnly(True)
self.text.setLineWrapMode(QtGui.QTextEdit.NoWrap)
self.text.setAcceptRichText(False)
self.text.setTextInteractionFlags(Qt.TextSelectableByKeyboard |
Qt.TextSelectableByMouse)
self.text.setFont(diff_font())
self.text.setPlainText(COPYRIGHT)
font = QtGui.QFont()
font.setFamily('Sans Serif')
font.setPointSize(5)
self.spam.setFont(font)
self.close_button = QtGui.QPushButton()
self.close_button.setText(self.tr('Close'))
self.close_button.setDefault(True)
self.spam.setTextFormat(QtCore.Qt.LogText)
self.spam.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
self.spam.setWordWrap(True)
self.spam.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
self.spam.setText(COPYRIGHT)
self.button_layout = QtGui.QHBoxLayout()
self.button_layout.addStretch()
self.button_layout.addWidget(self.close_button)
self.kthx = QtGui.QPushButton(self)
self.kthx.setGeometry(QtCore.QRect(60, 380, 160, 24))
self.kthx.setText('kthx bye')
self.kthx.setDefault(True)
self.main_layout = QtGui.QVBoxLayout()
self.main_layout.setMargin(0)
self.main_layout.setSpacing(defs.spacing)
self.connect(self.kthx, SIGNAL('clicked()'), self.accept)
self.main_layout.addWidget(self.label)
self.main_layout.addWidget(self.text)
self.main_layout.addLayout(self.button_layout)
self.setLayout(self.main_layout)
self.resize(666, 420)
qtutils.connect_button(self.close_button, self.accept)
def set_version(self, version):
"""Sets the version field in the 'about' dialog"""
self.spam.setText(self.spam.text().replace('$VERSION', version))
self.text.setPlainText(self.text.toPlainText().replace('$VERSION', version))
def show_shortcuts():
@@ -103,7 +113,7 @@ def show_shortcuts():
except AttributeError:
parent = qtutils.active_window()
widget = show_shortcuts.widget = QtGui.QDialog(parent)
widget.setWindowModality(QtCore.Qt.WindowModal)
widget.setWindowModality(Qt.WindowModal)
web = QtWebKit.QWebView(parent)
web.setHtml(html)
@@ -117,8 +127,8 @@ def show_shortcuts():
widget.resize(800, min(parent.height(), 600))
qtutils.add_action(widget, 'Close', widget.accept,
QtCore.Qt.Key_Question,
QtCore.Qt.Key_Enter,
QtCore.Qt.Key_Return)
Qt.Key_Question,
Qt.Key_Enter,
Qt.Key_Return)
widget.show()
return widget
+7 -83
View File
@@ -1,5 +1,6 @@
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import Qt
from PyQt4.QtCore import SIGNAL
import cola
@@ -12,6 +13,7 @@ from cola.cmds import BaseCommand
from cola.ctrl import Controller
from cola.git import git
from cola.widgets import defs
from cola.widgets import standard
save_blob = 'save_blob'
@@ -111,8 +113,9 @@ class BrowseDialog(QtGui.QDialog):
return model.filename
def __init__(self, model, select_file=False, parent=None):
QtGui.QDialog.__init__(self, parent)
self.setWindowModality(QtCore.Qt.WindowModal)
super(BrowseDialog, self).__init__(parent)
self.setAttribute(Qt.WA_MacMetalStyle)
self.setWindowModality(Qt.WindowModal)
# updated for use by commands
self.model = model
@@ -188,13 +191,10 @@ class BrowseDialog(QtGui.QDialog):
self.save.setEnabled(bool(filenames))
class GitTreeWidget(QtGui.QTreeView):
class GitTreeWidget(standard.TreeView):
def __init__(self, ref, parent=None):
QtGui.QTreeView.__init__(self, parent)
super(GitTreeWidget, self).__init__(parent)
self.setHeaderHidden(True)
self.setAlternatingRowColors(True)
self.setAllColumnsShowFocus(True)
self.setUniformRowHeights(True)
self.connect(self, SIGNAL('doubleClicked(const QModelIndex &)'),
self.double_clicked)
@@ -215,82 +215,6 @@ class GitTreeWidget(QtGui.QTreeView):
QtGui.QTreeView.selectionChanged(self, old_selection, new_selection)
self.emit(SIGNAL('selectionChanged()'))
def keyPressEvent(self, event):
"""
Override keyPressEvent to allow LeftArrow to work on non-directories.
When LeftArrow is pressed on a file entry or an unexpanded directory,
then move the current index to the parent directory.
This simplifies navigation using the keyboard.
For power-users, we support Vim keybindings ;-P
"""
# Check whether the item is expanded before calling the base class
# keyPressEvent otherwise we end up collapsing and changing the
# current index in one shot, which we don't want to do.
index = self.currentIndex()
was_expanded = self.isExpanded(index)
was_collapsed = not was_expanded
# Vim keybindings...
# Rewrite the event before marshalling to QTreeView.event()
key = event.key()
# Remap 'H' to 'Left'
if key == QtCore.Qt.Key_H:
event = QtGui.QKeyEvent(event.type(),
QtCore.Qt.Key_Left,
event.modifiers())
# Remap 'J' to 'Down'
elif key == QtCore.Qt.Key_J:
event = QtGui.QKeyEvent(event.type(),
QtCore.Qt.Key_Down,
event.modifiers())
# Remap 'K' to 'Up'
elif key == QtCore.Qt.Key_K:
event = QtGui.QKeyEvent(event.type(),
QtCore.Qt.Key_Up,
event.modifiers())
# Remap 'L' to 'Right'
elif key == QtCore.Qt.Key_L:
event = QtGui.QKeyEvent(event.type(),
QtCore.Qt.Key_Right,
event.modifiers())
# Re-read the event key to take the remappings into account
key = event.key()
# Process the keyPressEvent before changing the current index
# otherwise the event will affect the new index set here
# instead of the original index.
result = QtGui.QTreeView.keyPressEvent(self, event)
# Try to select the first item if the model index is invalid
if not index.isValid():
index = self.model().index(0, 0, QtCore.QModelIndex())
if index.isValid():
self.setCurrentIndex(index)
# Automatically select the first entry when expanding a directory
elif (key == QtCore.Qt.Key_Right and was_collapsed and
self.isExpanded(index)):
index = self.moveCursor(self.MoveDown, event.modifiers())
self.setCurrentIndex(index)
# Process non-root entries with valid parents only.
elif key == QtCore.Qt.Key_Left and index.parent().isValid():
# File entries have rowCount() == 0
if self.model().itemFromIndex(index).rowCount() == 0:
self.setCurrentIndex(index.parent())
# Otherwise, do this for collapsed directories only
elif was_collapsed:
self.setCurrentIndex(index.parent())
return result
def select_first_file(self):
"""Select the first filename in the tree"""
model = self.model()
+11 -9
View File
@@ -1,6 +1,7 @@
import os
from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4.QtCore import Qt
from PyQt4.QtCore import SIGNAL
import cola
@@ -10,8 +11,8 @@ from cola import gitcmds
from cola import qt
from cola import qtutils
from cola import signals
from cola.qt import GitRefLineEdit
from cola.widgets import defs
from cola.widgets import completion
from cola.widgets import standard
@@ -22,16 +23,13 @@ def install_command_wrapper():
def get_config_actions():
cfg = gitcfg.instance()
names = cfg.get_guitool_names()
return names or []
return cfg.get_guitool_names()
def run_command(title, command):
"""Show a command widget"""
view = GitCommandWidget(qtutils.active_window())
view.setWindowModality(QtCore.Qt.ApplicationModal)
view = GitCommandWidget(title, qtutils.active_window())
view.set_command(command)
view.setWindowTitle(title)
view.show()
view.raise_()
view.run()
@@ -42,10 +40,13 @@ def run_command(title, command):
class GitCommandWidget(standard.Dialog):
"""Nice TextView that reads the output of a command syncronously"""
# Keep us in scope otherwise PyQt kills the widget
def __init__(self, parent=None):
def __init__(self, title, parent=None):
standard.Dialog.__init__(self, parent)
self.resize(720, 420)
self.setWindowTitle(title)
self.setWindowModality(Qt.ApplicationModal)
# Construct the process
self.proc = QtCore.QProcess(self)
self.exitstatus = 0
@@ -180,10 +181,11 @@ class ActionCommandWrapper(object):
class ActionDialog(standard.Dialog):
def __init__(self, parent, name, opts):
standard.Dialog.__init__(self, parent)
self.setWindowModality(QtCore.Qt.ApplicationModal)
self.name = name
self.opts = opts
self.setWindowModality(Qt.ApplicationModal)
self.layt = QtGui.QVBoxLayout()
self.layt.setMargin(defs.margin)
self.layt.setSpacing(defs.spacing)
@@ -276,7 +278,7 @@ class RevisionSelector(QtGui.QWidget):
self._rev_label = QtGui.QLabel()
self._rev_layt.addWidget(self._rev_label)
self._revision = GitRefLineEdit()
self._revision = completion.GitRefLineEdit()
self._rev_layt.addWidget(self._revision)
self._layt.addLayout(self._rev_layt)
+96 -134
View File
@@ -1,5 +1,6 @@
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import Qt
from PyQt4.QtCore import SIGNAL
import cola
@@ -22,6 +23,8 @@ from cola.prefs import diff_font
from cola.dag.model import DAG
from cola.dag.model import RepoReader
from cola.widgets.selectcommits import select_commits
from cola.widgets.text import HintedLineEdit
from cola.widgets.text import HintedTextEdit
class CommitMessageEditor(QtGui.QWidget):
@@ -30,20 +33,8 @@ class CommitMessageEditor(QtGui.QWidget):
self.model = model
self.notifying = False
self.summary_placeholder = u'Commit summary'
self.description_placeholder = u'Extended description...'
# Palette for normal text
self.default_palette = QtGui.QPalette(self.palette())
# Palette used for the placeholder text
self.placeholder_palette = pal = QtGui.QPalette(self.palette())
color = pal.text().color()
color.setAlpha(128)
pal.setColor(QtGui.QPalette.Active, QtGui.QPalette.Text, color)
pal.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Text, color)
self.summary = QtGui.QLineEdit()
self.summary = CommitSummaryLineEdit()
self.description = CommitMessageTextEdit()
self.commit_button = create_toolbutton(text='Commit@@verb',
@@ -104,10 +95,17 @@ class CommitMessageEditor(QtGui.QWidget):
self.set_commit_message)
self.connect(self.summary, SIGNAL('returnPressed()'),
self.summary_return_pressed)
self.focus_description)
self.connect(self.summary, SIGNAL('cursorPositionChanged(int,int)'),
lambda x, y: self.emit_summary_position())
self.connect(self.summary, SIGNAL('downPressed()'),
self.focus_description)
self.connect(self.summary, SIGNAL('cursorPosition(int,int)'),
self.emit_position)
self.connect(self.description, SIGNAL('cursorPosition(int,int)'),
# description starts at line 2
lambda row, col: self.emit_position(row + 2, col))
# Keep model informed of changes
self.connect(self.summary, SIGNAL('textChanged(QString)'),
@@ -116,19 +114,13 @@ class CommitMessageEditor(QtGui.QWidget):
self.connect(self.description, SIGNAL('textChanged()'),
self.commit_message_changed)
self.connect(self.description, SIGNAL('cursorPositionChanged()'),
self.emit_cursor_position)
self.connect(self.description, SIGNAL('shiftTab()'),
self.focus_summary)
self.setFont(diff_font())
self.summary.installEventFilter(self)
self.description.installEventFilter(self)
self.enable_placeholder_summary(True)
self.enable_placeholder_description(True)
self.summary.enable_hint(True)
self.description.enable_hint(True)
self.commit_button.setEnabled(False)
self.commit_action.setEnabled(False)
@@ -138,86 +130,13 @@ class CommitMessageEditor(QtGui.QWidget):
def focus_summary(self):
self.summary.setFocus(True)
def is_summary_placeholder(self):
summary = unicode(self.summary.text()).strip()
return summary == self.summary_placeholder
def is_description_placeholder(self):
description = unicode(self.description.toPlainText()).strip()
return description == self.description_placeholder
def eventFilter(self, obj, event):
if obj == self.summary:
if event.type() == QtCore.QEvent.FocusIn:
self.emit_summary_position()
if self.is_summary_placeholder():
self.enable_placeholder_summary(False)
elif event.type() == QtCore.QEvent.FocusOut:
if not bool(self.commit_summary()):
self.enable_placeholder_summary(True)
elif obj == self.description:
if event.type() == QtCore.QEvent.FocusIn:
self.emit_cursor_position()
if self.is_description_placeholder():
self.enable_placeholder_description(False)
elif event.type() == QtCore.QEvent.FocusOut:
if not bool(self.commit_description()):
self.enable_placeholder_description(True)
return False
def enable_placeholder_summary(self, placeholder):
blocksignals = self.summary.blockSignals(True)
if placeholder:
self.summary.setText(self.summary_placeholder)
else:
self.summary.clear()
self.summary.setCursorPosition(0)
self.summary.blockSignals(blocksignals)
self.set_placeholder_palette(self.summary, placeholder)
def enable_placeholder_description(self, placeholder):
blocksignals = self.description.blockSignals(True)
if placeholder:
self.description.setPlainText(self.description_placeholder)
else:
self.description.clear()
self.description.blockSignals(blocksignals)
self.set_placeholder_palette(self.description, placeholder)
def set_placeholder_palette(self, widget, placeholder):
if placeholder:
widget.setPalette(self.placeholder_palette)
else:
widget.setPalette(self.default_palette)
def summary_return_pressed(self):
if bool(self.commit_summary()):
self.description.setFocus(True)
def commit_summary(self):
"""Return the commit summary as unicode"""
summary = unicode(self.summary.text()).strip()
if summary != self.summary_placeholder:
return summary
else:
return u''
def commit_description(self):
"""Return the commit description as unicode"""
description = unicode(self.description.toPlainText()).strip()
if description != self.description_placeholder:
return description
else:
return u''
def focus_description(self):
self.description.setFocus(True)
def commit_message(self):
"""Return the commit message as a unicode string"""
summary = self.commit_summary()
description = self.commit_description()
summary = self.summary.value()
description = self.description.value()
if summary and description:
return summary + u'\n\n' + description
elif summary:
@@ -232,21 +151,19 @@ class CommitMessageEditor(QtGui.QWidget):
self.notifying = True
message = self.commit_message()
self.model.set_commitmsg(message)
self.update_placeholder_state()
self.refresh_palettes()
self.notifying = False
self.update_actions()
def update_actions(self):
commit_enabled = bool(self.commit_summary())
commit_enabled = bool(self.summary.value())
self.commit_button.setEnabled(commit_enabled)
self.commit_action.setEnabled(commit_enabled)
def update_placeholder_state(self):
"""Update the color palette for the placeholder text"""
self.set_placeholder_palette(self.summary,
self.is_summary_placeholder())
self.set_placeholder_palette(self.description,
self.is_description_placeholder())
def refresh_palettes(self):
"""Update the color palette for the hint text"""
self.summary.refresh_palette()
self.description.refresh_palette()
def set_commit_message(self, message):
"""Set the commit message to match the observed model"""
@@ -291,7 +208,7 @@ class CommitMessageEditor(QtGui.QWidget):
# Update summary
if not summary and not self.summary.hasFocus():
summary = self.summary_placeholder
summary = self.summary.hint()
blocksignals = self.summary.blockSignals(True)
self.summary.setText(summary)
@@ -300,24 +217,24 @@ class CommitMessageEditor(QtGui.QWidget):
# Update description
if not description and not self.description.hasFocus():
description = self.description_placeholder
description = self.description.hint()
blocksignals = self.description.blockSignals(True)
self.description.setPlainText(description)
self.description.blockSignals(blocksignals)
# Update text color
self.update_placeholder_state()
self.refresh_palettes()
# Focus the empty summary or description
if focus_summary:
self.summary.setFocus(True)
self.emit_summary_position()
self.summary.emit_position()
elif focus_description:
self.description.setFocus(True)
self.emit_cursor_position()
self.description.emit_position()
else:
self.emit_summary_position()
self.summary.emit_position()
self.update_actions()
@@ -332,22 +249,12 @@ class CommitMessageEditor(QtGui.QWidget):
self.amend_action.setChecked(checked)
self.amend_action.blockSignals(blocksignals)
def emit_summary_position(self):
cols = self.summary.cursorPosition()
self.emit(SIGNAL('cursorPosition(int,int)'), 1, cols)
def emit_cursor_position(self):
"""Update the UI with the current row and column."""
cursor = self.description.textCursor()
position = cursor.position()
txt = unicode(self.description.toPlainText())
rows = txt[:position].count('\n') + 1 + 2 # description starts at 2
cols = cursor.columnNumber()
self.emit(SIGNAL('cursorPosition(int,int)'), rows, cols)
def emit_position(self, row, col):
self.emit(SIGNAL('cursorPosition(int,int)'), row, col)
def commit(self):
"""Attempt to create a commit from the index and commit message."""
if not bool(self.commit_summary()):
if not bool(self.summary.value()):
# Describe a good commit message
error_msg = tr(''
'Please supply a commit message.\n\n'
@@ -431,15 +338,30 @@ class CommitMessageEditor(QtGui.QWidget):
self.emit(SIGNAL(signals.load_previous_message), sha1)
class CommitMessageTextEdit(QtGui.QTextEdit):
class CommitSummaryLineEdit(HintedLineEdit):
def __init__(self, parent=None):
QtGui.QTextEdit.__init__(self, parent)
self.setLineWrapMode(QtGui.QTextEdit.NoWrap)
self.setAcceptRichText(False)
hint = u'Commit summary'
super(CommitSummaryLineEdit, self).__init__(hint, parent)
def keyPressEvent(self, event):
if event.key() == Qt.Key_Down:
position = self.cursorPosition()
curtext = unicode(self.text())
if position == len(curtext):
self.emit(SIGNAL('downPressed()'))
event.ignore()
return
super(CommitSummaryLineEdit, self).keyPressEvent(event)
class CommitMessageTextEdit(HintedTextEdit):
def __init__(self, parent=None):
hint = u'Extended description...'
super(CommitMessageTextEdit, self).__init__(hint, parent)
self.setMinimumSize(QtCore.QSize(1, 1))
self.action_emit_shift_tab = add_action(self,
'Shift Tab', self.shift_tab, 'Shift+tab')
'Shift Tab', self.emit_shift_tab, 'Shift+tab')
self.installEventFilter(self)
@@ -454,5 +376,45 @@ class CommitMessageTextEdit(QtGui.QTextEdit):
return False
def shift_tab(self):
def keyPressEvent(self, event):
if event.key() == Qt.Key_Up:
cursor = self.textCursor()
position = cursor.position()
if position == 0:
if cursor.hasSelection():
cursor.setPosition(0)
self.setTextCursor(cursor)
else:
self.emit_shift_tab()
event.ignore()
return
text_before = unicode(self.toPlainText())[:position]
lines_before = text_before.count('\n')
if lines_before == 0:
if event.modifiers() & Qt.ShiftModifier:
mode = QtGui.QTextCursor.KeepAnchor
else:
mode = QtGui.QTextCursor.MoveAnchor
cursor.setPosition(0, mode)
self.setTextCursor(cursor)
event.ignore()
return
elif event.key() == Qt.Key_Down:
cursor = self.textCursor()
position = cursor.position()
all_text = unicode(self.toPlainText())
text_after = all_text[position:]
lines_after = text_after.count('\n')
if lines_after == 0:
if event.modifiers() & Qt.ShiftModifier:
mode = QtGui.QTextCursor.KeepAnchor
else:
mode = QtGui.QTextCursor.MoveAnchor
cursor.setPosition(len(all_text), mode)
self.setTextCursor(cursor)
event.ignore()
return
super(CommitMessageTextEdit, self).keyPressEvent(event)
def emit_shift_tab(self):
self.emit(SIGNAL('shiftTab()'))
+422
View File
@@ -0,0 +1,422 @@
import re
import subprocess
from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4.QtCore import Qt
from PyQt4.QtCore import SIGNAL
import cola
from cola import qtutils
from cola import utils
from cola.compat import set
class CompletionLineEdit(QtGui.QLineEdit):
def __init__(self, parent=None):
super(CompletionLineEdit, self).__init__(parent)
# used to hide the completion popup after a drag-select
self._drag = 0
self._completer = None
self._delegate = HighlightDelegate(self)
self.connect(self, SIGNAL('textChanged(QString)'), self._text_changed)
self._keys_to_ignore = set([Qt.Key_Enter, Qt.Key_Return,
Qt.Key_Escape])
def value(self):
return unicode(self.text())
def setCompleter(self, completer):
self._completer = completer
completer.setWidget(self)
completer.popup().setItemDelegate(self._delegate)
self.connect(self._completer, SIGNAL('activated(QString)'),
self._complete)
def _is_case_sensitive(self, text):
return bool([char for char in text if char.isupper()])
def _text_changed(self, text):
text = self._last_word()
case_sensitive = self._is_case_sensitive(text)
if case_sensitive:
self._completer.setCaseSensitivity(Qt.CaseSensitive)
else:
self._completer.setCaseSensitivity(Qt.CaseInsensitive)
self._delegate.set_highlight_text(text, case_sensitive)
self._completer.set_match_text(text, case_sensitive)
def update_matches(self):
text = self._last_word()
case_sensitive = self._is_case_sensitive(text)
self._completer.model().update_matches(case_sensitive)
def _complete(self, completion):
"""
This is the event handler for the QCompleter.activated(QString) signal,
it is called when the user selects an item in the completer popup.
"""
if not completion:
return
words = self._words()
if words and not self._ends_with_whitespace():
words.pop()
words.append(unicode(completion))
self.setText(subprocess.list2cmdline(words))
self.emit(SIGNAL('ref_changed'))
def _words(self):
return utils.shell_usplit(unicode(self.text()))
def _ends_with_whitespace(self):
text = unicode(self.text())
return text.rstrip() != text
def _last_word(self):
if self._ends_with_whitespace():
return u''
words = self._words()
if not words:
return unicode(self.text())
if not words[-1]:
return u''
return words[-1]
def event(self, event):
if event.type() == QtCore.QEvent.KeyPress:
if (event.key() == Qt.Key_Tab and
self._completer.popup().isVisible()):
event.ignore()
return True
if event.type() == QtCore.QEvent.Hide:
self.close_popup()
return super(CompletionLineEdit, self).event(event)
def do_completion(self):
self._completer.popup().setCurrentIndex(
self._completer.model().index(0,0))
self._completer.complete()
def keyPressEvent(self, event):
if self._completer.popup().isVisible():
if event.key() in self._keys_to_ignore:
event.ignore()
self._complete(self._last_word())
return
elif (event.key() == Qt.Key_Down and
self._completer.completionCount() > 0):
event.accept()
self.do_completion()
return
super(CompletionLineEdit, self).keyPressEvent(event)
prefix = self._last_word()
if prefix != unicode(self._completer.completionPrefix()):
self._update_popup_items(prefix)
if len(event.text()) > 0 and len(prefix) > 0:
self._completer.complete()
#if len(prefix) == 0:
# self._completer.popup().hide()
#: _drag: 0 - unclicked, 1 - clicked, 2 - dragged
def mousePressEvent(self, event):
self._drag = 1
return super(CompletionLineEdit, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
if self._drag == 1:
self._drag = 2
return super(CompletionLineEdit, self).mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if self._drag != 2 and event.buttons() != Qt.RightButton:
self.do_completion()
self._drag = 0
return super(CompletionLineEdit, self).mouseReleaseEvent(event)
def close_popup(self):
if self._completer.popup().isVisible():
self._completer.popup().close()
def _update_popup_items(self, prefix):
"""
Filters the completer's popup items to only show items
with the given prefix.
"""
self._completer.setCompletionPrefix(prefix)
self._completer.popup().setCurrentIndex(
self._completer.model().index(0,0))
def __del__(self):
self.dispose()
def dispose(self):
self._completer.dispose()
class GatherCompletionsThread(QtCore.QThread):
def __init__(self, model):
QtCore.QThread.__init__(self)
self.model = model
self.case_sensitive = False
def run(self):
text = None
# Loop when the matched text changes between the start and end time.
# This happens when gather_matches() takes too long and the
# model's matched_text changes in-between.
while text != self.model.matched_text:
text = self.model.matched_text
items = self.model.gather_matches(self.case_sensitive)
if text is not None:
self.emit(SIGNAL('items_gathered'), items)
class HighlightDelegate(QtGui.QStyledItemDelegate):
"""A delegate used for auto-completion to give formatted completion"""
def __init__(self, parent=None): # model, parent=None):
super(HighlightDelegate, self).__init__(parent)
self.highlight_text = ''
self.case_sensitive = False
self.doc = QtGui.QTextDocument()
self.doc.setDocumentMargin(0)
def set_highlight_text(self, text, case_sensitive):
"""Sets the text that will be made bold in the term name when displayed"""
self.highlight_text = text
self.case_sensitive = case_sensitive
def paint(self, painter, option, index):
"""Overloaded Qt method for custom painting of a model index"""
if not self.highlight_text:
return super(HighlightDelegate, self).paint(painter, option, index)
text = unicode(index.data().toPyObject())
if self.case_sensitive:
html = text.replace(self.highlight_text,
'<strong>%s</strong>' % self.highlight_text)
else:
match = re.match('(.*)(' + self.highlight_text + ')(.*)',
text, re.IGNORECASE)
if match:
start = match.group(1) or ''
middle = match.group(2) or ''
end = match.group(3) or ''
html = (start + ('<strong>%s</strong>' % middle) + end)
else:
html = text
self.doc.setHtml(html)
# Painting item without text, Text Document will paint the text
optionV4 = QtGui.QStyleOptionViewItemV4(option)
self.initStyleOption(optionV4, index)
optionV4.text = QtCore.QString()
style = QtGui.QApplication.style()
style.drawControl(QtGui.QStyle.CE_ItemViewItem, optionV4, painter)
ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
# Highlighting text if item is selected
if (optionV4.state & QtGui.QStyle.State_Selected):
ctx.palette.setColor(QtGui.QPalette.Text, optionV4.palette.color(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText))
# translate the painter to where the text is drawn
textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, optionV4)
painter.save()
start = textRect.topLeft() + QtCore.QPoint(3, 0)
painter.translate(start)
painter.setClipRect(textRect.translated(-start))
# tell the text document to draw the html for us
self.doc.documentLayout().draw(painter, ctx)
painter.restore()
class CompletionModel(QtGui.QStandardItemModel):
def __init__(self, parent):
super(CompletionModel, self).__init__(parent)
self.matched_text = ''
self.case_sensitive = False
self.update_thread = GatherCompletionsThread(self)
self.connect(self.update_thread, SIGNAL('items_gathered'),
self.apply_matches)
def lower_completion_cmp(self, a, b):
return cmp(a.replace('.','').lower(), a.replace('.','').lower())
def completion_cmp(self, a, b):
return cmp(a.replace('.',''), a.replace('.',''))
def update(self):
case_sensitive = self.update_thread.case_sensitive
self.update_matches(case_sensitive)
def set_match_text(self, matched_text, case_sensitive):
self.matched_text = matched_text
self.update_matches(case_sensitive)
def update_matches(self, case_sensitive):
self.case_sensitive = case_sensitive
self.update_thread.case_sensitive = case_sensitive
if not self.update_thread.isRunning():
self.update_thread.start()
def gather_matches(self, case_sensitive):
return ((), (), set())
def apply_matches(self, match_tuple):
matched_refs, matched_paths, dirs = match_tuple
QStandardItem = QtGui.QStandardItem
file_icon = qtutils.file_icon()
dir_icon = qtutils.dir_icon()
git_icon = qtutils.git_icon()
matched_text = self.matched_text
items = []
for ref in matched_refs:
item = QStandardItem()
item.setText(ref)
item.setIcon(git_icon)
items.append(item)
if matched_paths and (not matched_text or matched_text in '--'):
item = QStandardItem()
item.setText('--')
item.setIcon(file_icon)
items.append(item)
for match in matched_paths:
item = QStandardItem()
item.setText(match)
if match in dirs:
item.setIcon(dir_icon)
else:
item.setIcon(file_icon)
items.append(item)
self.clear()
self.invisibleRootItem().appendRows(items)
class Completer(QtGui.QCompleter):
def __init__(self, parent):
super(Completer, self).__init__(parent)
self._model = None
self.setWidget(parent)
self.setCompletionMode(QtGui.QCompleter.UnfilteredPopupCompletion)
self.setCaseSensitivity(Qt.CaseInsensitive)
def setModel(self, model):
super(Completer, self).setModel(model)
self.connect(model, SIGNAL('updated()'), self.updated)
self._model = model
def model(self):
return self._model
def updated(self):
self.model().update()
def dispose(self):
self.model().dispose()
def set_match_text(self, matched_text, case_sensitive):
self.model().set_match_text(matched_text, case_sensitive)
class GitRefCompletionModel(CompletionModel):
def __init__(self, parent):
super(GitRefCompletionModel, self).__init__(parent)
self.cola_model = model = cola.model()
msg = model.message_updated
model.add_observer(msg, self.emit_updated)
def emit_updated(self):
self.emit(SIGNAL('updated()'))
def matches(self):
model = self.cola_model
return model.local_branches + model.remote_branches + model.tags
def dispose(self):
self.cola_model.remove_observer(self.emit_updated)
def gather_matches(self, case_sensitive):
refs = self.matches()
matched_text = self.matched_text
if matched_text:
if not case_sensitive:
matched_refs = [r for r in refs if matched_text in r]
else:
matched_refs = [r for r in refs
if matched_text.lower() in r.lower()]
else:
matched_refs = refs
if self.case_sensitive:
matched_refs.sort(cmp=self.completion_cmp)
else:
matched_refs.sort(cmp=self.lower_completion_cmp)
return (matched_refs, (), set())
class GitLogCompletionModel(GitRefCompletionModel):
def __init__(self, parent):
super(GitLogCompletionModel, self).__init__(parent)
def gather_matches(self, case_sensitive):
(matched_refs, dummy_paths, dummy_dirs) =\
super(GitLogCompletionModel, self).gather_matches(case_sensitive)
file_list = self.cola_model.everything()
files = set(file_list)
files_and_dirs = utils.add_parents(set(files))
dirs = files_and_dirs.difference(files)
matched_text = self.matched_text
if matched_text:
if case_sensitive:
matched_paths = [f for f in files_and_dirs
if matched_text in f]
else:
matched_paths = [f for f in files_and_dirs
if matched_text.lower() in f.lower()]
else:
matched_paths = list(files_and_dirs)
if self.case_sensitive:
matched_paths.sort(cmp=self.completion_cmp)
else:
matched_paths.sort(cmp=self.lower_completion_cmp)
return (matched_refs, matched_paths, dirs)
class GitLogCompleter(Completer):
def __init__(self, parent):
super(GitLogCompleter, self).__init__(parent)
self.setModel(GitLogCompletionModel(self))
class GitRefCompleter(Completer):
def __init__(self, parent):
super(GitRefCompleter, self).__init__(parent)
self.setModel(GitRefCompletionModel(self))
class GitLogLineEdit(CompletionLineEdit):
def __init__(self, parent=None):
super(GitLogLineEdit, self).__init__(parent)
self.setCompleter(GitLogCompleter(self))
class GitRefLineEdit(CompletionLineEdit):
def __init__(self, parent=None):
super(GitRefLineEdit, self).__init__(parent)
self.setCompleter(GitRefCompleter(self))
+132 -50
View File
@@ -1,4 +1,6 @@
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import Qt
from PyQt4.QtCore import SIGNAL
from cola import gitcmds
@@ -6,6 +8,7 @@ from cola import qt
from cola import qtutils
from cola import utils
from cola.widgets import defs
from cola.widgets import completion
from cola.widgets import standard
from cola.main.model import MainModel
@@ -21,24 +24,85 @@ def create_new_branch(revision=''):
return view
class CreateOpts(object):
def __init__(self, model):
self.model = model
self.reset = False
self.track = False
self.fetch = True
self.checkout = True
self.revision = 'HEAD'
self.branch = ''
class CreateThread(QtCore.QThread):
def __init__(self, opts, parent):
super(CreateThread, self).__init__(parent)
self.opts = opts
def run(self):
branch = self.opts.branch
revision = self.opts.revision
reset = self.opts.reset
checkout = self.opts.checkout
track = self.opts.track
model = self.opts.model
results = []
if track and '/' in revision:
remote = revision.split('/', 1)[0]
status, out = model.git.fetch(remote,
with_status=True,
with_stderr=True)
self.emit(SIGNAL('command'), status, out)
results.append(('fetch', status, out))
if status == 0:
status, out = model.create_branch(branch, revision,
force=reset,
track=track)
self.emit(SIGNAL('command'), status, out)
results.append(('branch', status, out))
if status == 0 and checkout:
status, out = model.git.checkout(branch,
with_status=True,
with_stderr=True)
self.emit(SIGNAL('command'), status, out)
results.append(('checkout', status, out))
self.emit(SIGNAL('done'), results)
class CreateBranchDialog(standard.Dialog):
"""A dialog for creating branches."""
def __init__(self, model, parent=None):
standard.Dialog.__init__(self, parent=parent)
self.model = model
super(CreateBranchDialog, self).__init__(parent=parent)
self.setWindowModality(Qt.WindowModal)
self.setAttribute(Qt.WA_MacMetalStyle)
self.setWindowTitle(self.tr('Create Branch'))
self.model = model
self.opts = CreateOpts(model)
self.thread = CreateThread(self.opts, self)
self.progress = QtGui.QProgressDialog(self)
self.progress.setRange(0, 0)
self.progress.setCancelButton(None)
self.progress.setWindowTitle(self.tr('Create Branch'))
self.progress.setWindowModality(Qt.WindowModal)
self.branch_name_label = QtGui.QLabel()
self.branch_name_label.setText(self.tr('Branch Name'))
self.local_branch = QtGui.QLineEdit()
self.branch_name = QtGui.QLineEdit()
self.rev_label = QtGui.QLabel()
self.rev_label.setText(self.tr('Revision Expression:'))
self.rev_label.setText(self.tr('Starting Revision'))
self.revision = QtGui.QLineEdit()
self.revision = completion.GitRefLineEdit()
current = gitcmds.current_branch()
if current:
self.revision.setText(current)
self.local_radio = QtGui.QRadioButton()
self.local_radio.setText(self.tr('Local Branch'))
@@ -85,11 +149,11 @@ class CreateBranchDialog(standard.Dialog):
icon=qtutils.git_icon())
self.create_button.setDefault(True)
self.cancel_button = qt.create_button(text='Cancel')
self.close_button = qt.create_button(text='Close')
self.branch_name_layout = QtGui.QHBoxLayout()
self.branch_name_layout.addWidget(self.branch_name_label)
self.branch_name_layout.addWidget(self.local_branch)
self.branch_name_layout.addWidget(self.branch_name)
self.rev_start_radiobtn_layout = QtGui.QHBoxLayout()
self.rev_start_radiobtn_layout.addWidget(self.local_radio)
@@ -132,7 +196,7 @@ class CreateBranchDialog(standard.Dialog):
self.buttons_layout.setMargin(defs.margin)
self.buttons_layout.setSpacing(defs.spacing)
self.buttons_layout.addWidget(self.create_button)
self.buttons_layout.addWidget(self.cancel_button)
self.buttons_layout.addWidget(self.close_button)
self.options_section_layout = QtGui.QHBoxLayout()
self.options_section_layout.setMargin(defs.margin)
@@ -148,7 +212,7 @@ class CreateBranchDialog(standard.Dialog):
self.main_layout.addLayout(self.options_section_layout)
self.setLayout(self.main_layout)
qtutils.connect_button(self.cancel_button, self.reject)
qtutils.connect_button(self.close_button, self.reject)
qtutils.connect_button(self.create_button, self.create_branch)
qtutils.connect_button(self.local_radio, self.display_model)
qtutils.connect_button(self.remote_radio, self.display_model)
@@ -157,32 +221,40 @@ class CreateBranchDialog(standard.Dialog):
self.connect(self.branch_list, SIGNAL('itemSelectionChanged()'),
self.branch_item_changed)
if self.parent():
self.resize(self.parent().width(), self.parent().height())
else:
self.resize(555, 333)
self.connect(self.thread, SIGNAL('command'), self.thread_command)
self.connect(self.thread, SIGNAL('done'), self.thread_done)
self.resize(555, 333)
self.display_model()
def set_revision(self, revision):
self.revision.setText(revision)
def getopts(self):
self.opts.revision = self.revision.value()
self.opts.branch = unicode(self.branch_name.text())
self.opts.checkout = self.checkout_checkbox.isChecked()
self.opts.reset = self.reset_radio.isChecked()
self.opts.fetch = self.fetch_checkbox.isChecked()
self.opts.track = self.remote_radio.isChecked()
def create_branch(self):
"""Creates a branch; called by the "Create Branch" button"""
revision = unicode(self.revision.text())
branch = unicode(self.local_branch.text())
existing_branches = self.model.local_branches
self.getopts()
revision = self.opts.revision
branch = self.opts.branch
no_update = self.no_update_radio.isChecked()
ffwd_only = self.ffwd_only_radio.isChecked()
existing_branches = gitcmds.branch_list()
check_branch = False
if not branch or not revision:
qtutils.critical('Missing Data',
'Please provide both a branch '
'name and revision expression.')
return
check_branch = False
if branch in existing_branches:
if self.no_update_radio.isChecked():
if no_update:
msg = self.tr("Branch '%s' already exists.")
msg = unicode(msg) % branch
qtutils.critical('Branch Exists', msg)
@@ -192,10 +264,12 @@ class CreateBranchDialog(standard.Dialog):
check_branch = bool(commits)
if check_branch:
msg = self.tr("Resetting '%s' to '%s' will "
"lose the following commits:")
lines = [ unicode(msg) % (branch, revision) ]
qmsg = self.tr("Resetting '%s' to '%s' will lose commits.")
msg = unicode(qmsg) % (branch, revision)
if ffwd_only:
qtutils.critical('Branch Exists', msg)
return
lines = [msg]
for idx, commit in enumerate(commits):
subject = commit[1][0:min(len(commit[1]),16)]
if len(subject) < len(commit[1]):
@@ -206,11 +280,8 @@ class CreateBranchDialog(standard.Dialog):
skip = len(commits) - 5
lines.append('\t(%d skipped)' % skip)
break
lines.extend([
unicode(self.tr('Recovering lost commits may not be easy.')),
])
line = unicode(self.tr('Recovering lost commits may not be easy.'))
lines.append(line)
if not qtutils.confirm('Reset Branch?',
'\n'.join(lines),
'Reset "%s" to "%s"?' % (branch, revision),
@@ -218,49 +289,60 @@ class CreateBranchDialog(standard.Dialog):
default=False,
icon=qtutils.icon('undo.svg')):
return
self.setEnabled(False)
self.progress.setEnabled(True)
QtGui.QApplication.setOverrideCursor(Qt.WaitCursor)
# TODO handle fetch
track = self.remote_radio.isChecked()
fetch = self.fetch_checkbox.isChecked()
ffwd = self.ffwd_only_radio.isChecked()
reset = self.reset_radio.isChecked()
chkout = self.checkout_checkbox.isChecked()
# Show a nice progress bar
self.progress.setLabelText('Updating...')
self.progress.show()
self.thread.start()
status, output = self.model.create_branch(branch, revision, track=track)
def thread_command(self, status, output):
qtutils.log(status, output)
if status == 0 and chkout:
status, output = self.model.git.checkout(branch,
with_status=True,
with_stderr=True)
qtutils.log(status, output)
def thread_done(self, results):
self.setEnabled(True)
self.progress.close()
QtGui.QApplication.restoreOverrideCursor()
detail_lines = []
for (cmd, status, out) in results:
if status != 0:
qtutils.critical('Create Branch Error',
'"git %s" returned exit status "%d"' %
(cmd, status))
return
line = '"git %s" returned exit status %d' % (cmd, status)
detail_lines.append(line)
detail_lines.append(out)
detail_lines.append('')
details = '\n'.join(detail_lines)
qtutils.information('Create Branch', 'Branch created',
details=details)
self.accept()
def branch_item_changed(self, *rest):
"""This callback is called when the branch selection changes"""
# When the branch selection changes then we should update
# the "Revision Expression" accordingly.
qlist = self.branch_list
(row, selected) = qtutils.selected_row(qlist)
if not selected:
return
# Update the model with the selection
sources = self.branch_sources()
rev = sources[row]
# Update the model with the selection
self.revision.setText(rev)
# Only set the branch name field if we're
# branching from a remote branch.
# Set the branch field if we're branching from a remote branch.
if not self.remote_radio.isChecked():
return
branch = utils.basename(rev)
if branch == 'HEAD':
return
# Signal that we've clicked on a remote branch
self.local_branch.setText(branch)
self.branch_name.setText(branch)
def display_model(self):
"""Sets the branch list to the available branches
+20 -21
View File
@@ -1,5 +1,6 @@
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import Qt
import cola
from cola import qt
@@ -9,14 +10,14 @@ from cola.prefs import diff_font
from cola.qtutils import connect_button
from cola.qtutils import critical
from cola.qtutils import information
from cola.widgets import completion
from cola.widgets import standard
from cola.widgets import text
def create_tag(revision=''):
"""Entry point for external callers."""
opts = TagOptions()
if revision:
opts.revisions.insert(0, revision)
opts = TagOptions(revision)
view = CreateTag(opts, qtutils.active_window())
view.show()
return view
@@ -26,18 +27,19 @@ def create_tag(revision=''):
class TagOptions(object):
"""Simple data container for the CreateTag dialog."""
def __init__(self):
self.revisions = ['HEAD'] + cola.model().all_branches()
def __init__(self, revision):
self.revision = revision or 'HEAD'
class CreateTag(standard.Dialog):
def __init__(self, opts, parent):
standard.Dialog.__init__(self, parent=parent)
super(CreateTag, self).__init__(parent=parent)
self.setWindowModality(QtCore.Qt.WindowModal)
self.setAttribute(Qt.WA_MacMetalStyle)
self.setWindowTitle(self.tr('Create Tag'))
self.opts = opts
self.setWindowModality(QtCore.Qt.WindowModal)
self.setWindowTitle(self.tr('Create Tag'))
self.resize(506, 295)
self.main_layt = QtGui.QVBoxLayout(self)
self.main_layt.setContentsMargins(6, 12, 6, 6)
@@ -51,7 +53,7 @@ class CreateTag(standard.Dialog):
self.input_form_layt.setWidget(0, QtGui.QFormLayout.LabelRole,
self.tag_name_label)
self.tag_name = QtGui.QLineEdit(self)
self.tag_name = text.HintedLineEdit('vX.Y.Z', self)
self.tag_name.setToolTip(self.tr('Specifies the tag name'))
self.input_form_layt.setWidget(0, QtGui.QFormLayout.FieldRole,
self.tag_name)
@@ -75,10 +77,10 @@ class CreateTag(standard.Dialog):
self.input_form_layt.setWidget(2, QtGui.QFormLayout.LabelRole,
self.tag_msg_label)
self.tag_msg = QtGui.QTextEdit(self)
self.tag_msg.setAcceptRichText(False)
self.tag_msg = text.HintedTextEdit('Tag message...', self)
self.tag_msg.setToolTip(self.tr('Specifies the tag message'))
self.tag_msg.setFont(diff_font())
self.tag_msg.enable_hint(True)
self.input_form_layt.setWidget(2, QtGui.QFormLayout.FieldRole,
self.tag_msg)
# Revision
@@ -87,10 +89,8 @@ class CreateTag(standard.Dialog):
self.input_form_layt.setWidget(3, QtGui.QFormLayout.LabelRole,
self.rev_label)
self.revision = QtGui.QComboBox()
self.revision.addItems(self.opts.revisions)
self.revision.setCurrentIndex(0)
self.revision.setEditable(True)
self.revision = completion.GitRefLineEdit()
self.revision.setText(self.opts.revision)
self.revision.setToolTip(self.tr('Specifies the SHA-1 to tag'))
self.input_form_layt.setWidget(3, QtGui.QFormLayout.FieldRole,
self.revision)
@@ -110,15 +110,14 @@ class CreateTag(standard.Dialog):
connect_button(self.close_button, self.accept)
connect_button(self.create_button, self.create_tag)
def sync_options(self):
pass
self.resize(506, 295)
def create_tag(self):
"""Verifies inputs and emits a notifier tag message."""
revision = unicode(self.revision.currentText())
tag_name = unicode(self.tag_name.text())
tag_msg = unicode(self.tag_msg.toPlainText())
revision = self.revision.value()
tag_name = self.tag_name.value()
tag_msg = self.tag_msg.value()
sign_tag = self.sign_tag.isChecked()
if not revision:
+30 -9
View File
@@ -1,8 +1,8 @@
import fnmatch
from PyQt4.QtCore import Qt
from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4.QtCore import Qt
from PyQt4.QtCore import SIGNAL
import cola
@@ -65,13 +65,14 @@ class RemoteActionDialog(standard.Dialog):
def __init__(self, model, action, parent):
"""Customizes the dialog based on the remote action
"""
standard.Dialog.__init__(self, parent=parent)
super(RemoteActionDialog, self).__init__(parent=parent)
self.model = model
self.action = action
self.tasks = []
self.setAttribute(Qt.WA_MacMetalStyle)
self.setWindowModality(Qt.WindowModal)
self.setWindowTitle(self.tr(action))
self.setWindowModality(QtCore.Qt.WindowModal)
self.progress = QtGui.QProgressDialog(self)
self.progress.setRange(0, 0)
@@ -80,7 +81,7 @@ class RemoteActionDialog(standard.Dialog):
self.progress.setWindowModality(Qt.WindowModal)
self.local_label = QtGui.QLabel()
self.local_label.setText(self.tr('Local Branches'))
self.local_label.setText(self.tr('Local Branch'))
self.local_branch = QtGui.QLineEdit()
self.local_branches = QtGui.QListWidget()
@@ -93,8 +94,8 @@ class RemoteActionDialog(standard.Dialog):
self.remotes = QtGui.QListWidget()
self.remotes.addItems(self.model.remotes)
self.remote_label = QtGui.QLabel()
self.remote_label.setText(self.tr('Remote Branch'))
self.remote_branch_label = QtGui.QLabel()
self.remote_branch_label.setText(self.tr('Remote Branch'))
self.remote_branch = QtGui.QLineEdit()
self.remote_branches = QtGui.QListWidget()
@@ -127,7 +128,7 @@ class RemoteActionDialog(standard.Dialog):
self.remote_branch_layout.addWidget(self.remote_name)
self.remote_branches_layout = QtGui.QHBoxLayout()
self.remote_branches_layout.addWidget(self.remote_label)
self.remote_branches_layout.addWidget(self.remote_branch_label)
self.remote_branches_layout.addWidget(self.remote_branch)
self.options_layout = QtGui.QHBoxLayout()
@@ -194,9 +195,12 @@ class RemoteActionDialog(standard.Dialog):
self.remote_branch.setFocus()
else:
self.rebase_checkbox.hide()
if not qtutils.apply_state(self):
self.resize(666, 420)
self.remote_name.setFocus()
self.resize(666, 420)
def set_field_defaults(self):
# Default to "git fetch origin master"
@@ -350,7 +354,6 @@ class RemoteActionDialog(standard.Dialog):
qtutils.log(1, errmsg)
return
remote, kwargs = self.common_args()
action = self.action
# Check if we're about to create a new branch and warn.
remote_branch = unicode(self.remote_branch.text())
@@ -449,3 +452,21 @@ class Push(RemoteActionDialog):
class Pull(RemoteActionDialog):
def __init__(self, model, parent):
super(Pull, self).__init__(model, PULL, parent)
def apply_state(self, state):
super(Pull, self).apply_state(state)
try:
rebase = state['rebase']
except KeyError:
pass
else:
self.rebase_checkbox.setChecked(rebase)
def export_state(self):
state = super(Pull, self).export_state()
state['rebase'] = self.rebase_checkbox.isChecked()
return state
def done(self, exit_code):
qtutils.save_state(self)
return super(Pull, self).done(exit_code)
-1
View File
@@ -6,7 +6,6 @@ from PyQt4.QtCore import SIGNAL
from cola import gitcmds
from cola import qtutils
from cola.qt import DiffSyntaxHighlighter
from cola.qt import GitRefLineEdit
from cola.prefs import diff_font
from cola.widgets import defs
+138 -45
View File
@@ -1,57 +1,150 @@
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import Qt
from PyQt4.QtCore import SIGNAL
def create_standard_widget(qtclass):
"""Create a standard widget derived from a qt class.
"""
class StandardWidget(qtclass):
# Mix-in for standard view operations
def __init__(self, parent=None):
self._qtclass = qtclass
self._qtclass.__init__(self, parent)
def create_widget_class(Base):
class Widget(Base):
# Mix-in for standard view operations
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
def show(self):
"""Automatically centers dialogs"""
if self.parent():
left = self.parent().x()
width = self.parent().width()
center_x = left + width/2
def show(self):
"""Automatically centers dialogs"""
if self.parent():
left = self.parent().x()
width = self.parent().width()
center_x = left + width/2
x = center_x - self.width()/2
y = self.parent().y()
x = center_x - self.width()/2
y = self.parent().y()
self.move(x, y)
# Call the base Qt show()
self._qtclass.show(self)
self.move(x, y)
# Call the base Qt show()
super(Widget, self).show()
def name(self):
"""Returns the name of the view class"""
return self.__class__.__name__.lower()
def name(self):
"""Returns the name of the view class"""
return self.__class__.__name__.lower()
def apply_state(self, state):
"""Imports data for view save/restore"""
try:
self.resize(state['width'], state['height'])
except:
pass
try:
self.move(state['x'], state['y'])
except:
pass
def apply_state(self, state):
"""Imports data for view save/restore"""
try:
self.resize(state['width'], state['height'])
except:
pass
try:
self.move(state['x'], state['y'])
except:
pass
def export_state(self):
"""Exports data for view save/restore"""
return {
'x': self.x(),
'y': self.y(),
'width': self.width(),
'height': self.height(),
}
def export_state(self):
"""Exports data for view save/restore"""
return {
'x': self.x(),
'y': self.y(),
'width': self.width(),
'height': self.height(),
}
return StandardWidget
return Widget
# The base class for all cola QDialogs.
Widget = create_standard_widget(QtGui.QWidget)
Dialog = create_standard_widget(QtGui.QDialog)
MainWindow = create_standard_widget(QtGui.QMainWindow)
def create_tree_class(Base):
class Tree(Base):
def __init__(self, parent=None):
super(Tree, self).__init__(parent)
self.setAlternatingRowColors(True)
self.setUniformRowHeights(True)
self.setAllColumnsShowFocus(True)
self.setAnimated(True)
def key_pressed(self):
pass
def keyPressEvent(self, event):
"""
Override keyPressEvent to allow LeftArrow to work on non-directories.
When LeftArrow is pressed on a file entry or an unexpanded directory,
then move the current index to the parent directory.
This simplifies navigation using the keyboard.
For power-users, we support Vim keybindings ;-P
"""
# Check whether the item is expanded before calling the base class
# keyPressEvent otherwise we end up collapsing and changing the
# current index in one shot, which we don't want to do.
index = self.currentIndex()
was_expanded = self.isExpanded(index)
was_collapsed = not was_expanded
# Vim keybindings...
# Rewrite the event before marshalling to QTreeView.event()
key = event.key()
# Remap 'H' to 'Left'
if key == Qt.Key_H:
event = QtGui.QKeyEvent(event.type(),
Qt.Key_Left,
event.modifiers())
# Remap 'J' to 'Down'
elif key == Qt.Key_J:
event = QtGui.QKeyEvent(event.type(),
Qt.Key_Down,
event.modifiers())
# Remap 'K' to 'Up'
elif key == Qt.Key_K:
event = QtGui.QKeyEvent(event.type(),
Qt.Key_Up,
event.modifiers())
# Remap 'L' to 'Right'
elif key == Qt.Key_L:
event = QtGui.QKeyEvent(event.type(),
Qt.Key_Right,
event.modifiers())
# Re-read the event key to take the remappings into account
key = event.key()
result = super(Tree, self).keyPressEvent(event)
# Let others hook in here before we change the indexes
self.emit(SIGNAL('indexAboutToChange()'))
# Try to select the first item if the model index is invalid
if not index.isValid():
index = self.model().index(0, 0, QtCore.QModelIndex())
if index.isValid():
self.setCurrentIndex(index)
# Automatically select the first entry when expanding a directory
elif (key == Qt.Key_Right and was_collapsed and
self.isExpanded(index)):
index = self.moveCursor(self.MoveDown, event.modifiers())
self.setCurrentIndex(index)
# Process non-root entries with valid parents only.
elif key == Qt.Key_Left and index.parent().isValid():
# File entries have rowCount() == 0
if self.model().itemFromIndex(index).rowCount() == 0:
self.setCurrentIndex(index.parent())
# Otherwise, do this for collapsed directories only
elif was_collapsed:
self.setCurrentIndex(index.parent())
return result
# Tree is a closure over "Base"
return Tree
Widget = create_widget_class(QtGui.QWidget)
Dialog = create_widget_class(QtGui.QDialog)
MainWindow = create_widget_class(QtGui.QMainWindow)
TreeView = create_tree_class(QtGui.QTreeView)
TreeWidget = create_tree_class(QtGui.QTreeWidget)
+128
View File
@@ -0,0 +1,128 @@
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import SIGNAL
class HintedTextWidgetEventFilter(QtCore.QObject):
def __init__(self, parent):
super(HintedTextWidgetEventFilter, self).__init__(parent)
self.widget = parent
def eventFilter(self, obj, event):
if event.type() == QtCore.QEvent.FocusIn:
self.widget.emit_position()
if self.widget.is_hint():
self.widget.enable_hint(False)
elif event.type() == QtCore.QEvent.FocusOut:
if not bool(self.widget.value()):
self.widget.enable_hint(True)
return False
class HintedTextWidgetMixin(object):
def __init__(self, hint):
self._hint = unicode(self.tr(hint))
self._event_filter = HintedTextWidgetEventFilter(self)
self.installEventFilter(self._event_filter)
# Palette for normal text
self.default_palette = QtGui.QPalette(self.palette())
# Palette used for the placeholder text
self.hint_palette = pal = QtGui.QPalette(self.palette())
color = self.hint_palette.text().color()
color.setAlpha(128)
pal.setColor(QtGui.QPalette.Active, QtGui.QPalette.Text, color)
pal.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Text, color)
def emit_position(self):
pass
def reset_cursor(self):
pass
def hint(self):
return self._hint
def is_hint(self):
return self.strip() == self._hint
def value(self):
text = self.strip()
if text == self._hint:
return u''
else:
return text
def strip(self):
return self.as_unicode().strip()
def enable_hint(self, hint):
blocksignals = self.blockSignals(True)
if hint:
self.set_value(self.hint())
else:
self.clear()
self.reset_cursor()
self.blockSignals(blocksignals)
self.enable_hint_palette(hint)
def enable_hint_palette(self, hint):
if hint:
self.setPalette(self.hint_palette)
else:
self.setPalette(self.default_palette)
def refresh_palette(self):
self.enable_hint_palette(self.is_hint())
class HintedTextEdit(QtGui.QTextEdit, HintedTextWidgetMixin):
def __init__(self, hint, parent=None):
QtGui.QTextEdit.__init__(self, parent)
HintedTextWidgetMixin.__init__(self, hint)
self.setLineWrapMode(QtGui.QTextEdit.NoWrap)
self.setAcceptRichText(False)
self.connect(self, SIGNAL('cursorPositionChanged()'),
self.emit_position)
def as_unicode(self):
return unicode(self.toPlainText())
def set_value(self, value):
self.setPlainText(value)
def emit_position(self):
cursor = self.textCursor()
position = cursor.position()
txt = self.as_unicode()
rows = txt[:position].count('\n') + 1
cols = cursor.columnNumber()
self.emit(SIGNAL('cursorPosition(int,int)'), rows, cols)
class HintedLineEdit(QtGui.QLineEdit, HintedTextWidgetMixin):
def __init__(self, hint, parent=None):
QtGui.QLineEdit.__init__(self, parent)
HintedTextWidgetMixin.__init__(self, hint)
self.connect(self,
SIGNAL('cursorPositionChanged(int,int)'),
lambda x, y: self.emit_position())
def emit_position(self):
cols = self.cursorPosition()
self.emit(SIGNAL('cursorPosition(int,int)'), 1, cols)
def set_value(self, value):
self.setText(value)
def as_unicode(self):
return unicode(self.text())
def reset_cursor(self):
self.setCursorPosition(0)
+5 -5
View File
@@ -37,8 +37,8 @@ SUB-COMMANDS
branch::
Launch the 'branch' tool.
classic::
Launch the 'classic' tool.
browse::
Launch the 'browser' tool.
fetch::
Launch the 'fetch' tool.
@@ -67,8 +67,8 @@ cola.savewindowsettings::
cola.fontdiff::
Specifies the font to use for 'git-cola''s diff display.
cola.classicdockable::
Whether to create a dock widget with the 'Cola Classic' tool.
cola.browserdockable::
Whether to create a dock widget with the 'Browser' tool.
Defaults to 'false' to speedup startup time.
gui.diffcontext::
@@ -110,7 +110,7 @@ GIT_COLA_TRACE::
When defined, 'git-cola' logs 'git' commands to stdout.
When set to 'full', 'git-cola' also logs the exit status and output.
When set to 'trace', 'git-cola' logs to the 'Command Output' widget.
When set to 'trace', 'git-cola' logs to the 'Console' widget.
SOURCE
------
+2 -2
View File
@@ -154,12 +154,12 @@ span.title {
</tr>
</table>
<!-- Classic actions -->
<!-- Browser actions -->
<table class="center">
<tr>
<td width="35%">&nbsp;</td>
<td>&nbsp;</td>
<td class="title">Classic actions</td>
<td class="title">Browser actions</td>
</tr>
<tr>
<td class="shortcut">Ctrl + e</td>
+50 -23
View File
@@ -1,4 +1,47 @@
.. |beta(TM)| unicode:: beta U+2122
git-cola v1.7.5
===============
Usability, bells and whistles
-----------------------------
* Auto-completion was added to more tools.
* `git-dag` is easier to use on smaller displays -- the author
field elides its text which allows for a more compact display.
* Selected commits in `git-dag` were made more prominent and
easier to see.
* 'Create Branch' learned to fetch remote branches and uses a
background thread to do so.
* User-configured GUI tools are listed alphabetically in the 'Actions' menu.
* The 'Pull' dialog remembers the value of the 'Rebase' checkbox
between invocations.
git-cola v1.7.4.1
=================
Fixes
-----
* Detect Homebrew so that OS X users do not need to set PYTHONPATH.
* `git-dag` can export patches again.
git-cola v1.7.4
===============
Usability, bells and whistles
-----------------------------
* The 'Classic' tool was renamed to 'Browser' and learned to
limit history to the current branch.
* `git-dag` learned about gravatar and uses it to show images
for commit authors.
* `git-dag` learned to use OpenGL for rendering resulting in
much faster rendering.
* More dialogs learned vim-style keyboard shortcuts.
* The commit message editor learned better arrow key navigation.
git-cola v1.7.3
===============
@@ -55,7 +98,7 @@ Usability, bells and whistles
git cola pull
git cola tag
* `git-dag` is more responsive when gathering tab-completions.
* `git-dag` is more responsive when gathering auto-completions.
* Keyboard shortcuts are displayed when the '?' key is pressed.
@@ -158,8 +201,6 @@ git-cola v1.7.0
===============
Usability, bells and whistles
-----------------------------
* `git-dag` is no longer |beta(TM)|
* Export a patch series from `git-dag` into a `patches/` directory.
* `git-dag` learned to diff commits, slice history along paths, etc.
@@ -337,7 +378,7 @@ git-cola v1.4.3
===============
Usability, bells and whistles
-----------------------------
* `git-dag` |beta(TM)| now has a separate display area
* `git-dag` now has a separate display area
for displaying commit metadata. This area will soon
grow additional functionality such as cherry-picking,
branching, etc.
@@ -376,7 +417,7 @@ Usability, bells and whistles
* Clicking on paths in the status widget copies them into the
copy/paste buffer for easy middle-clicking into terminals.
* Ctrl+C in diff viewer copies the selected diff to the clipboard.
* `Ctrl+C` in diff viewer copies the selected diff to the clipboard.
Fixes
-----
@@ -435,7 +476,7 @@ git-cola v1.4.2.2
=================
Usability, bells and whistles
-----------------------------
* `git-dag` |beta(TM)| interaction was made faster.
* `git-dag` interaction was made faster.
Fixes
-----
@@ -455,7 +496,7 @@ Usability, bells and whistles
http://github.com/git-cola/git-cola/issues/closed#issue/48
* `git-dag` |beta(TM)| reads history in a background thread.
* `git-dag` reads history in a background thread.
Portability
-----------
@@ -469,7 +510,6 @@ Fixes
git-cola v1.4.2
===============
Usability, bells and whistles
-----------------------------
* Added support for the configurable ``guitool.<tool>.*``
@@ -552,7 +592,6 @@ Packaging
git-cola v1.4.1.2
=================
Usability, bells and whistles
-----------------------------
* It is now possible to checkout from the index as well
@@ -563,7 +602,7 @@ Usability, bells and whistles
* Bookmarks can be selected when `git-cola` is run outside of a git repository.
* Added more user documentation. We now include many links to
external git resources.
* Added `git-dag Beta` to the available tools.
* Added `git-dag` to the available tools.
`git-dag` is a node-based DAG history browser.
It doesn't do much yet, but it's been merged so that we can start
building and improving upon it.
@@ -600,7 +639,6 @@ Packaging
git-cola v1.4.1.1
=================
Usability, bells and whistles
-----------------------------
* We now use patience diff by default when it is available via
@@ -619,7 +657,6 @@ Packaging
git-cola v1.4.1
===============
This feature release adds two new features directly from
`git-cola`'s github issues backlog. On the developer
front, further work was done towards modularizing the code base.
@@ -656,7 +693,6 @@ Developer
git-cola v1.4.0.5
=================
Fixes
-----
* Fix launching external applications on Windows
@@ -666,7 +702,6 @@ Fixes
git-cola v1.4.0.4
=================
Packaging
---------
* Fix Lintian warnings
@@ -674,7 +709,6 @@ Packaging
git-cola v1.4.0.3
=================
Fixes
-----
* Fix X11 warnings on application startup
@@ -682,7 +716,6 @@ Fixes
git-cola v1.4.0.2
=================
Fixes
-----
* Added missing 'Exit Diff Mode' button for 'Diff Expression' mode
@@ -696,7 +729,6 @@ Fixes
git-cola v1.4.0.1
=================
Fixes
-----
* Keep entries in sorted order in the `cola classic` tool
@@ -715,7 +747,6 @@ Fixes
git-cola v1.4.0
===============
This release focuses on a redesign of the git-cola user interface,
a tags interface, and better integration of the `cola classic` tool.
A flexible interface based on configurable docks is used to manage the
@@ -753,7 +784,6 @@ Developer
git-cola v1.3.9
===============
Usability, bells and whistles
-----------------------------
* Added a `cola classic` tool for browsing the entire repository
@@ -780,7 +810,6 @@ Fixes
git-cola v1.3.8
===============
Usability, bells and whistles
-----------------------------
* Fresh and tasty SVG logos
@@ -805,7 +834,6 @@ Packaging
git-cola v1.3.7
===============
Subsystems
----------
* `git-difftool` became an official git command in `git-1.6.3`.
@@ -837,7 +865,6 @@ Packaging
git-cola v1.3.6
===============
Subsystems
----------
* Added support for Kompare in `git-difftool`
+4 -4
View File
@@ -18,13 +18,13 @@ Main Interface
* :command:`ctrl-p` -- Cherry-pick
* :command:`shift-alt-s` -- Stash dialog
Diff Viewer
===========
Diff
====
* :command:`h` -- Stage/unstage section under text cursor
* :command:`s` -- Stage/unstage selection
Cola Classic
============
Browser
=======
* :command:`h` -- Move to parent/collapse
* :command:`j` -- Move down
* :command:`k` -- Move up
+12 -8
View File
@@ -5,6 +5,8 @@ exec wish "$0" -- "$@"
# This is a trivial implementation of an SSH_ASKPASS handler.
# Git-gui uses this script if none are already configured.
package require Tk
set answer {}
set yesno 0
set rc 255
@@ -30,18 +32,19 @@ if {!$yesno} {
frame .b
button .b.ok -text OK -command finish
button .b.cancel -text Cancel -command {destroy .}
button .b.cancel -text Cancel -command cancel
pack .b.ok -side left -expand 1
pack .b.cancel -side right -expand 1
pack .b -side bottom -fill x -padx 10 -pady 10
bind . <Visibility> {focus -force .e}
bind . <Key-Return> finish
bind . <Key-Escape> {destroy .}
bind . <Destroy> {
bind . <Destroy> {}
exit $::rc
bind . <Key-Return> [list .b.ok invoke]
bind . <Key-Escape> [list .b.cancel invoke]
bind . <Destroy> {set rc $rc}
proc cancel {} {
set ::rc 255
}
proc finish {} {
@@ -53,10 +56,11 @@ proc finish {} {
}
}
set ::rc 0
puts $::answer
destroy .
set ::rc 0
}
wm title . "OpenSSH"
tk::PlaceWindow .
vwait rc
exit $rc
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB