git-cola v4.0.0
[git-cola.git] / cola / widgets / bookmarks.py
blobbd76ba810bb2170aec7f3d23d3ddc8808def3ba0
1 """Provides widgets related to bookmarks"""
2 from __future__ import absolute_import, division, print_function, 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()
113 def __init__(self, context, style, parent=None):
114 standard.TreeWidget.__init__(self, parent=parent)
115 self.context = context
116 self.style = style
118 self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
119 self.setHeaderHidden(True)
121 # We make the items editable, but we don't want the double-click
122 # behavior to trigger editing. Make it behave like Mac OS X's Finder.
123 self.setEditTriggers(self.SelectedClicked)
125 self.open_action = qtutils.add_action(
126 self, N_('Open'), self.open_repo, hotkeys.OPEN
129 self.accept_action = qtutils.add_action(
130 self, N_('Accept'), self.accept_repo, *hotkeys.ACCEPT
133 self.open_new_action = qtutils.add_action(
134 self, N_('Open in New Window'), self.open_new_repo, hotkeys.NEW
137 self.set_default_repo_action = qtutils.add_action(
138 self, N_('Set Default Repository'), self.set_default_repo
141 self.clear_default_repo_action = qtutils.add_action(
142 self, N_('Clear Default Repository'), self.clear_default_repo
145 self.rename_repo_action = qtutils.add_action(
146 self, N_('Rename Repository'), self.rename_repo
149 self.open_default_action = qtutils.add_action(
150 self, cmds.OpenDefaultApp.name(), self.open_default, hotkeys.PRIMARY_ACTION
153 self.launch_editor_action = qtutils.add_action(
154 self, cmds.Edit.name(), self.launch_editor, hotkeys.EDIT
157 self.launch_terminal_action = qtutils.add_action(
158 self, cmds.LaunchTerminal.name(), self.launch_terminal, hotkeys.TERMINAL
161 self.copy_action = qtutils.add_action(self, N_('Copy'), self.copy, hotkeys.COPY)
163 self.delete_action = qtutils.add_action(
164 self, N_('Delete'), self.delete_bookmark
167 self.remove_missing_action = qtutils.add_action(
168 self, N_('Prune Missing Entries'), self.remove_missing
170 self.remove_missing_action.setToolTip(
171 N_('Remove stale entries for repositories that no longer exist')
174 # pylint: disable=no-member
175 self.itemChanged.connect(self.item_changed)
176 self.itemSelectionChanged.connect(self.item_selection_changed)
177 self.itemDoubleClicked.connect(self.tree_double_clicked)
179 self.action_group = utils.Group(
180 self.open_action,
181 self.open_new_action,
182 self.copy_action,
183 self.launch_editor_action,
184 self.launch_terminal_action,
185 self.open_default_action,
186 self.rename_repo_action,
187 self.delete_action,
189 self.action_group.setEnabled(False)
190 self.set_default_repo_action.setEnabled(False)
191 self.clear_default_repo_action.setEnabled(False)
193 # Connections
194 if style == RECENT_REPOS:
195 context.model.worktree_changed.connect(
196 self.refresh, type=Qt.QueuedConnection
199 def refresh(self):
200 context = self.context
201 settings = context.settings
202 builder = BuildItem(context)
204 # bookmarks
205 if self.style == BOOKMARKS:
206 entries = settings.bookmarks
207 # recent items
208 elif self.style == RECENT_REPOS:
209 entries = settings.recent
211 items = [builder.get(entry['path'], entry['name']) for entry in entries]
212 if self.style == BOOKMARKS and prefs.sort_bookmarks(context):
213 items.sort(key=lambda x: x.name)
215 self.clear()
216 self.addTopLevelItems(items)
218 def contextMenuEvent(self, event):
219 menu = qtutils.create_menu(N_('Actions'), self)
220 menu.addAction(self.open_action)
221 menu.addAction(self.open_new_action)
222 menu.addAction(self.open_default_action)
223 menu.addSeparator()
224 menu.addAction(self.copy_action)
225 menu.addAction(self.launch_editor_action)
226 menu.addAction(self.launch_terminal_action)
227 menu.addSeparator()
228 item = self.selected_item()
229 is_default = bool(item and item.is_default)
230 if is_default:
231 menu.addAction(self.clear_default_repo_action)
232 else:
233 menu.addAction(self.set_default_repo_action)
234 menu.addAction(self.rename_repo_action)
235 menu.addSeparator()
236 menu.addAction(self.delete_action)
237 menu.addAction(self.remove_missing_action)
238 menu.exec_(self.mapToGlobal(event.pos()))
240 def item_changed(self, item, _index):
241 self.rename_entry(item, item.text(0))
243 def rename_entry(self, item, new_name):
244 settings = self.context.settings
245 if self.style == BOOKMARKS:
246 rename = settings.rename_bookmark
247 elif self.style == RECENT_REPOS:
248 rename = settings.rename_recent
249 else:
250 rename = disable_rename
251 if rename(item.path, item.name, new_name):
252 settings.save()
253 item.name = new_name
254 else:
255 item.setText(0, item.name)
257 def apply_fn(self, fn, *args, **kwargs):
258 item = self.selected_item()
259 if item:
260 fn(item, *args, **kwargs)
262 def copy(self):
263 self.apply_fn(lambda item: qtutils.set_clipboard(item.path))
265 def open_default(self):
266 context = self.context
267 self.apply_fn(lambda item: cmds.do(cmds.OpenDefaultApp, context, [item.path]))
269 def set_default_repo(self):
270 self.apply_fn(self.set_default_item)
272 def set_default_item(self, item):
273 context = self.context
274 cmds.do(cmds.SetDefaultRepo, context, item.path)
275 self.refresh()
276 self.default_changed.emit()
278 def clear_default_repo(self):
279 self.apply_fn(self.clear_default_item)
280 self.default_changed.emit()
282 def clear_default_item(self, _item):
283 context = self.context
284 cmds.do(cmds.SetDefaultRepo, context, None)
285 self.refresh()
287 def rename_repo(self):
288 self.apply_fn(lambda item: self.editItem(item, 0))
290 def accept_repo(self):
291 self.apply_fn(self.accept_item)
293 def accept_item(self, item):
294 if self.state() & self.EditingState:
295 widget = self.itemWidget(item, 0)
296 if widget:
297 self.commitData(widget)
298 self.closePersistentEditor(item, 0)
299 else:
300 self.open_repo()
302 def open_repo(self):
303 context = self.context
304 self.apply_fn(lambda item: cmds.do(cmds.OpenRepo, context, item.path))
306 def open_new_repo(self):
307 context = self.context
308 self.apply_fn(lambda item: cmds.do(cmds.OpenNewRepo, context, item.path))
310 def launch_editor(self):
311 context = self.context
312 self.apply_fn(lambda item: cmds.do(cmds.Edit, context, [item.path]))
314 def launch_terminal(self):
315 context = self.context
316 self.apply_fn(lambda item: cmds.do(cmds.LaunchTerminal, context, item.path))
318 def item_selection_changed(self):
319 item = self.selected_item()
320 enabled = bool(item)
321 self.action_group.setEnabled(enabled)
323 is_default = bool(item and item.is_default)
324 self.set_default_repo_action.setEnabled(not is_default)
325 self.clear_default_repo_action.setEnabled(is_default)
327 def tree_double_clicked(self, item, _column):
328 context = self.context
329 cmds.do(cmds.OpenRepo, context, item.path)
331 def add_bookmark(self):
332 normpath = utils.expandpath(core.getcwd())
333 name = os.path.basename(normpath)
334 prompt = (
335 (N_('Name'), name),
336 (N_('Path'), core.getcwd()),
338 ok, values = qtutils.prompt_n(N_('Add Favorite'), prompt)
339 if not ok:
340 return
341 name, path = values
342 normpath = utils.expandpath(path)
343 if git.is_git_worktree(normpath):
344 settings = self.context.settings
345 settings.load()
346 settings.add_bookmark(normpath, name)
347 settings.save()
348 self.refresh()
349 else:
350 Interaction.critical(N_('Error'), N_('%s is not a Git repository.') % path)
352 def delete_bookmark(self):
353 """Removes a bookmark from the bookmarks list"""
354 item = self.selected_item()
355 context = self.context
356 if not item:
357 return
358 if self.style == BOOKMARKS:
359 cmd = cmds.RemoveBookmark
360 elif self.style == RECENT_REPOS:
361 cmd = cmds.RemoveRecent
362 else:
363 return
364 ok, _, _, _ = cmds.do(cmd, context, item.path, item.name, icon=icons.discard())
365 if ok:
366 self.refresh()
368 def remove_missing(self):
369 """Remove missing entries from the favorites/recent file list"""
370 settings = self.context.settings
371 if self.style == BOOKMARKS:
372 settings.remove_missing_bookmarks()
373 elif self.style == RECENT_REPOS:
374 settings.remove_missing_recent()
375 self.refresh()
378 class BuildItem(object):
379 def __init__(self, context):
380 self.star_icon = icons.star()
381 self.folder_icon = icons.folder()
382 cfg = context.cfg
383 self.default_repo = cfg.get('cola.defaultrepo')
385 def get(self, path, name):
386 is_default = self.default_repo == path
387 if is_default:
388 icon = self.star_icon
389 else:
390 icon = self.folder_icon
391 return BookmarksTreeWidgetItem(path, name, icon, is_default)
394 class BookmarksTreeWidgetItem(QtWidgets.QTreeWidgetItem):
395 def __init__(self, path, name, icon, is_default):
396 QtWidgets.QTreeWidgetItem.__init__(self)
397 self.path = path
398 self.name = name
399 self.is_default = is_default
401 self.setIcon(0, icon)
402 self.setText(0, name)
403 self.setToolTip(0, path)
404 self.setFlags(self.flags() | Qt.ItemIsEditable)