maint: format code using black
[git-cola.git] / cola / widgets / bookmarks.py
blobf3338656a8a6565de8954acdd688cbeef5025511
1 """Provides widgets related to bookmarks"""
2 from __future__ import division, absolute_import, unicode_literals
3 import os
5 from qtpy import QtCore
6 from qtpy import QtWidgets
7 from qtpy.QtCore import Qt
8 from qtpy.QtCore import Signal
10 from .. import cmds
11 from .. import core
12 from .. import git
13 from .. import hotkeys
14 from .. import icons
15 from .. import qtutils
16 from .. import utils
17 from ..i18n import N_
18 from ..interaction import Interaction
19 from ..models import prefs
20 from ..widgets import defs
21 from ..widgets import standard
24 BOOKMARKS = 0
25 RECENT_REPOS = 1
28 def bookmark(context, parent):
29 return BookmarksWidget(context, BOOKMARKS, parent=parent)
32 def recent(context, parent):
33 return BookmarksWidget(context, RECENT_REPOS, parent=parent)
36 class BookmarksWidget(QtWidgets.QFrame):
37 def __init__(self, context, style=BOOKMARKS, parent=None):
38 QtWidgets.QFrame.__init__(self, parent)
40 self.context = context
41 self.style = style
42 self.tree = BookmarksTreeWidget(context, style, parent=self)
44 self.add_button = qtutils.create_action_button(
45 tooltip=N_('Add'), icon=icons.add()
48 self.delete_button = qtutils.create_action_button(
49 tooltip=N_('Delete'), icon=icons.remove()
52 self.open_button = qtutils.create_action_button(
53 tooltip=N_('Open'), icon=icons.repo()
56 self.button_group = utils.Group(self.delete_button, self.open_button)
57 self.button_group.setEnabled(False)
59 self.setFocusProxy(self.tree)
60 if style == BOOKMARKS:
61 self.setToolTip(N_('Favorite repositories'))
62 elif style == RECENT_REPOS:
63 self.setToolTip(N_('Recent repositories'))
64 self.add_button.hide()
66 self.button_layout = qtutils.hbox(
67 defs.no_margin,
68 defs.spacing,
69 self.open_button,
70 self.add_button,
71 self.delete_button,
74 self.main_layout = qtutils.vbox(defs.no_margin, defs.spacing, self.tree)
75 self.setLayout(self.main_layout)
77 self.corner_widget = QtWidgets.QWidget(self)
78 self.corner_widget.setLayout(self.button_layout)
79 titlebar = parent.titleBarWidget()
80 titlebar.add_corner_widget(self.corner_widget)
82 qtutils.connect_button(self.add_button, self.tree.add_bookmark)
83 qtutils.connect_button(self.delete_button, self.tree.delete_bookmark)
84 qtutils.connect_button(self.open_button, self.tree.open_repo)
86 item_selection_changed = self.tree_item_selection_changed
87 # pylint: disable=no-member
88 self.tree.itemSelectionChanged.connect(item_selection_changed)
90 QtCore.QTimer.singleShot(0, self.reload_bookmarks)
92 def reload_bookmarks(self):
93 # Called once after the GUI is initialized
94 self.tree.refresh()
96 def tree_item_selection_changed(self):
97 enabled = bool(self.tree.selected_item())
98 self.button_group.setEnabled(enabled)
100 def connect_to(self, other):
101 self.tree.default_changed.connect(other.tree.refresh)
102 other.tree.default_changed.connect(self.tree.refresh)
105 def disable_rename(_path, _name, _new_name):
106 return False
109 # pylint: disable=too-many-ancestors
110 class BookmarksTreeWidget(standard.TreeWidget):
111 default_changed = Signal()
112 worktree_changed = Signal()
114 def __init__(self, context, style, parent=None):
115 standard.TreeWidget.__init__(self, parent=parent)
116 self.context = context
117 self.style = style
119 self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
120 self.setHeaderHidden(True)
122 # We make the items editable, but we don't want the double-click
123 # behavior to trigger editing. Make it behave like Mac OS X's Finder.
124 self.setEditTriggers(self.SelectedClicked)
126 self.open_action = qtutils.add_action(
127 self, N_('Open'), self.open_repo, hotkeys.OPEN
130 self.accept_action = qtutils.add_action(
131 self, N_('Accept'), self.accept_repo, *hotkeys.ACCEPT
134 self.open_new_action = qtutils.add_action(
135 self, N_('Open in New Window'), self.open_new_repo, hotkeys.NEW
138 self.set_default_repo_action = qtutils.add_action(
139 self, N_('Set Default Repository'), self.set_default_repo
142 self.clear_default_repo_action = qtutils.add_action(
143 self, N_('Clear Default Repository'), self.clear_default_repo
146 self.rename_repo_action = qtutils.add_action(
147 self, N_('Rename Repository'), self.rename_repo
150 self.open_default_action = qtutils.add_action(
151 self, cmds.OpenDefaultApp.name(), self.open_default, hotkeys.PRIMARY_ACTION
154 self.launch_editor_action = qtutils.add_action(
155 self, cmds.Edit.name(), self.launch_editor, hotkeys.EDIT
158 self.launch_terminal_action = qtutils.add_action(
159 self, cmds.LaunchTerminal.name(), self.launch_terminal, hotkeys.TERMINAL
162 self.copy_action = qtutils.add_action(self, N_('Copy'), self.copy, hotkeys.COPY)
164 self.delete_action = qtutils.add_action(
165 self, N_('Delete'), self.delete_bookmark
168 self.remove_missing_action = qtutils.add_action(
169 self, N_('Prune Missing Entries'), self.remove_missing
171 self.remove_missing_action.setToolTip(
172 N_('Remove stale entries for repositories that no longer exist')
175 # pylint: disable=no-member
176 self.itemChanged.connect(self.item_changed)
177 self.itemSelectionChanged.connect(self.item_selection_changed)
178 self.itemDoubleClicked.connect(self.tree_double_clicked)
180 self.action_group = utils.Group(
181 self.open_action,
182 self.open_new_action,
183 self.copy_action,
184 self.launch_editor_action,
185 self.launch_terminal_action,
186 self.open_default_action,
187 self.rename_repo_action,
188 self.delete_action,
190 self.action_group.setEnabled(False)
191 self.set_default_repo_action.setEnabled(False)
192 self.clear_default_repo_action.setEnabled(False)
194 # Connections
195 if style == RECENT_REPOS:
196 self.worktree_changed.connect(self.refresh, type=Qt.QueuedConnection)
197 context.model.add_observer(
198 context.model.message_worktree_changed, self.worktree_changed.emit
201 def refresh(self):
202 context = self.context
203 settings = context.settings
204 builder = BuildItem(context)
206 # bookmarks
207 if self.style == BOOKMARKS:
208 entries = settings.bookmarks
209 # recent items
210 elif self.style == RECENT_REPOS:
211 entries = settings.recent
213 items = [builder.get(entry['path'], entry['name']) for entry in entries]
214 if self.style == BOOKMARKS and prefs.sort_bookmarks(context):
215 items.sort(key=lambda x: x.name)
217 self.clear()
218 self.addTopLevelItems(items)
220 def contextMenuEvent(self, event):
221 menu = qtutils.create_menu(N_('Actions'), self)
222 menu.addAction(self.open_action)
223 menu.addAction(self.open_new_action)
224 menu.addAction(self.open_default_action)
225 menu.addSeparator()
226 menu.addAction(self.copy_action)
227 menu.addAction(self.launch_editor_action)
228 menu.addAction(self.launch_terminal_action)
229 menu.addSeparator()
230 item = self.selected_item()
231 is_default = bool(item and item.is_default)
232 if is_default:
233 menu.addAction(self.clear_default_repo_action)
234 else:
235 menu.addAction(self.set_default_repo_action)
236 menu.addAction(self.rename_repo_action)
237 menu.addSeparator()
238 menu.addAction(self.delete_action)
239 menu.addAction(self.remove_missing_action)
240 menu.exec_(self.mapToGlobal(event.pos()))
242 def item_changed(self, item, _index):
243 self.rename_entry(item, item.text(0))
245 def rename_entry(self, item, new_name):
246 settings = self.context.settings
247 if self.style == BOOKMARKS:
248 rename = settings.rename_bookmark
249 elif self.style == RECENT_REPOS:
250 rename = settings.rename_recent
251 else:
252 rename = disable_rename
253 if rename(item.path, item.name, new_name):
254 settings.save()
255 item.name = new_name
256 else:
257 item.setText(0, item.name)
259 def apply_fn(self, fn, *args, **kwargs):
260 item = self.selected_item()
261 if item:
262 fn(item, *args, **kwargs)
264 def copy(self):
265 self.apply_fn(lambda item: qtutils.set_clipboard(item.path))
267 def open_default(self):
268 context = self.context
269 self.apply_fn(lambda item: cmds.do(cmds.OpenDefaultApp, context, [item.path]))
271 def set_default_repo(self):
272 self.apply_fn(self.set_default_item)
274 def set_default_item(self, item):
275 context = self.context
276 cmds.do(cmds.SetDefaultRepo, context, item.path)
277 self.refresh()
278 self.default_changed.emit()
280 def clear_default_repo(self):
281 self.apply_fn(self.clear_default_item)
282 self.default_changed.emit()
284 def clear_default_item(self, _item):
285 context = self.context
286 cmds.do(cmds.SetDefaultRepo, context, None)
287 self.refresh()
289 def rename_repo(self):
290 self.apply_fn(lambda item: self.editItem(item, 0))
292 def accept_repo(self):
293 self.apply_fn(self.accept_item)
295 def accept_item(self, item):
296 if self.state() & self.EditingState:
297 widget = self.itemWidget(item, 0)
298 if widget:
299 self.commitData(widget)
300 self.closePersistentEditor(item, 0)
301 else:
302 self.open_repo()
304 def open_repo(self):
305 context = self.context
306 self.apply_fn(lambda item: cmds.do(cmds.OpenRepo, context, item.path))
308 def open_new_repo(self):
309 context = self.context
310 self.apply_fn(lambda item: cmds.do(cmds.OpenNewRepo, context, item.path))
312 def launch_editor(self):
313 context = self.context
314 self.apply_fn(lambda item: cmds.do(cmds.Edit, context, [item.path]))
316 def launch_terminal(self):
317 context = self.context
318 self.apply_fn(lambda item: cmds.do(cmds.LaunchTerminal, context, item.path))
320 def item_selection_changed(self):
321 item = self.selected_item()
322 enabled = bool(item)
323 self.action_group.setEnabled(enabled)
325 is_default = bool(item and item.is_default)
326 self.set_default_repo_action.setEnabled(not is_default)
327 self.clear_default_repo_action.setEnabled(is_default)
329 def tree_double_clicked(self, item, _column):
330 context = self.context
331 cmds.do(cmds.OpenRepo, context, item.path)
333 def add_bookmark(self):
334 normpath = utils.expandpath(core.getcwd())
335 name = os.path.basename(normpath)
336 prompt = (
337 (N_('Name'), name),
338 (N_('Path'), core.getcwd()),
340 ok, values = qtutils.prompt_n(N_('Add Favorite'), prompt)
341 if not ok:
342 return
343 name, path = values
344 normpath = utils.expandpath(path)
345 if git.is_git_worktree(normpath):
346 settings = self.context.settings
347 settings.load()
348 settings.add_bookmark(normpath, name)
349 settings.save()
350 self.refresh()
351 else:
352 Interaction.critical(N_('Error'), N_('%s is not a Git repository.') % path)
354 def delete_bookmark(self):
355 """Removes a bookmark from the bookmarks list"""
356 item = self.selected_item()
357 context = self.context
358 if not item:
359 return
360 if self.style == BOOKMARKS:
361 cmd = cmds.RemoveBookmark
362 elif self.style == RECENT_REPOS:
363 cmd = cmds.RemoveRecent
364 else:
365 return
366 ok, _, _, _ = cmds.do(cmd, context, item.path, item.name, icon=icons.discard())
367 if ok:
368 self.refresh()
370 def remove_missing(self):
371 """Remove missing entries from the favorites/recent file list"""
372 settings = self.context.settings
373 if self.style == BOOKMARKS:
374 settings.remove_missing_bookmarks()
375 elif self.style == RECENT_REPOS:
376 settings.remove_missing_recent()
377 self.refresh()
380 class BuildItem(object):
381 def __init__(self, context):
382 self.star_icon = icons.star()
383 self.folder_icon = icons.folder()
384 cfg = context.cfg
385 self.default_repo = cfg.get('cola.defaultrepo')
387 def get(self, path, name):
388 is_default = self.default_repo == path
389 if is_default:
390 icon = self.star_icon
391 else:
392 icon = self.folder_icon
393 return BookmarksTreeWidgetItem(path, name, icon, is_default)
396 class BookmarksTreeWidgetItem(QtWidgets.QTreeWidgetItem):
397 def __init__(self, path, name, icon, is_default):
398 QtWidgets.QTreeWidgetItem.__init__(self)
399 self.path = path
400 self.name = name
401 self.is_default = is_default
403 self.setIcon(0, icon)
404 self.setText(0, name)
405 self.setToolTip(0, path)
406 self.setFlags(self.flags() | Qt.ItemIsEditable)