git-cola v4.3.1
[git-cola.git] / cola / widgets / bookmarks.py
blob5990c460fe126895efa203434690b4d662482667
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 QtGui
7 from qtpy import QtWidgets
8 from qtpy.QtCore import Qt
9 from qtpy.QtCore import Signal
11 from .. import cmds
12 from .. import core
13 from .. import git
14 from .. import hotkeys
15 from .. import icons
16 from .. import qtutils
17 from .. import utils
18 from ..i18n import N_
19 from ..interaction import Interaction
20 from ..models import prefs
21 from ..widgets import defs
22 from ..widgets import standard
23 from ..widgets import switcher
26 BOOKMARKS = 0
27 RECENT_REPOS = 1
30 def bookmark(context, parent):
31 return BookmarksWidget(context, BOOKMARKS, parent=parent)
34 def recent(context, parent):
35 return BookmarksWidget(context, RECENT_REPOS, parent=parent)
38 class BookmarksWidget(QtWidgets.QFrame):
39 def __init__(self, context, style=BOOKMARKS, parent=None):
40 QtWidgets.QFrame.__init__(self, parent)
42 self.context = context
43 self.style = style
45 self.items = items = []
46 self.model = model = QtGui.QStandardItemModel()
48 settings = context.settings
49 builder = BuildItem(context)
50 # bookmarks
51 if self.style == BOOKMARKS:
52 entries = settings.bookmarks
53 # recent items
54 elif self.style == RECENT_REPOS:
55 entries = settings.recent
57 for entry in entries:
58 item = builder.get(entry['path'], entry['name'])
59 items.append(item)
60 model.appendRow(item)
62 place_holder = N_('Search repositories by name...')
63 self.quick_switcher = switcher.switcher_outer_view(
64 context, model, place_holder=place_holder
66 self.tree = BookmarksTreeView(
67 context, style, self.set_items_to_models, parent=self
70 self.add_button = qtutils.create_action_button(
71 tooltip=N_('Add'), icon=icons.add()
74 self.delete_button = qtutils.create_action_button(
75 tooltip=N_('Delete'), icon=icons.remove()
78 self.open_button = qtutils.create_action_button(
79 tooltip=N_('Open'), icon=icons.repo()
82 self.search_button = qtutils.create_action_button(
83 tooltip=N_('Search'), icon=icons.search()
86 self.button_group = utils.Group(self.delete_button, self.open_button)
87 self.button_group.setEnabled(False)
89 self.setFocusProxy(self.tree)
90 if style == BOOKMARKS:
91 self.setToolTip(N_('Favorite repositories'))
92 elif style == RECENT_REPOS:
93 self.setToolTip(N_('Recent repositories'))
94 self.add_button.hide()
96 self.button_layout = qtutils.hbox(
97 defs.no_margin,
98 defs.spacing,
99 self.search_button,
100 self.open_button,
101 self.add_button,
102 self.delete_button,
105 self.main_layout = qtutils.vbox(
106 defs.no_margin, defs.spacing, self.quick_switcher, self.tree
108 self.setLayout(self.main_layout)
110 self.corner_widget = QtWidgets.QWidget(self)
111 self.corner_widget.setLayout(self.button_layout)
112 titlebar = parent.titleBarWidget()
113 titlebar.add_corner_widget(self.corner_widget)
115 qtutils.connect_button(self.add_button, self.tree.add_bookmark)
116 qtutils.connect_button(self.delete_button, self.tree.delete_bookmark)
117 qtutils.connect_button(self.open_button, self.tree.open_repo)
118 qtutils.connect_button(self.search_button, self.toggle_switcher_input_field)
120 QtCore.QTimer.singleShot(0, self.reload_bookmarks)
122 self.tree.toggle_switcher.connect(self.enable_switcher_input_field)
123 # moving key has pressed while focusing on input field
124 self.quick_switcher.filter_input.switcher_selection_move.connect(
125 self.tree.keyPressEvent
127 # escape key has pressed while focusing on input field
128 self.quick_switcher.filter_input.switcher_escape.connect(
129 self.close_switcher_input_field
131 # some key except moving key has pressed while focusing on list view
132 self.tree.switcher_text.connect(self.switcher_text_inputted)
134 def reload_bookmarks(self):
135 # Called once after the GUI is initialized
136 tree = self.tree
137 tree.refresh()
139 model = tree.model()
141 model.dataChanged.connect(tree.item_changed)
142 selection = tree.selectionModel()
143 selection.selectionChanged.connect(tree.item_selection_changed)
144 tree.doubleClicked.connect(tree.tree_double_clicked)
146 def tree_item_selection_changed(self):
147 enabled = bool(self.tree.selected_item())
148 self.button_group.setEnabled(enabled)
150 def connect_to(self, other):
151 self.tree.default_changed.connect(other.tree.refresh)
152 other.tree.default_changed.connect(self.tree.refresh)
154 def set_items_to_models(self, items):
155 model = self.model
156 self.items.clear()
157 model.clear()
159 for item in items:
160 self.items.append(item)
161 model.appendRow(item)
163 self.quick_switcher.proxy_model.setSourceModel(model)
164 self.tree.setModel(self.quick_switcher.proxy_model)
166 def toggle_switcher_input_field(self):
167 visible = self.quick_switcher.filter_input.isVisible()
168 self.enable_switcher_input_field(not visible)
170 def close_switcher_input_field(self):
171 self.enable_switcher_input_field(False)
173 def enable_switcher_input_field(self, visible):
174 filter_input = self.quick_switcher.filter_input
176 filter_input.setVisible(visible)
177 if not visible:
178 filter_input.clear()
180 def switcher_text_inputted(self, event):
181 # default selection for first index
182 first_proxy_idx = self.quick_switcher.proxy_model.index(0, 0)
183 self.tree.setCurrentIndex(first_proxy_idx)
185 self.quick_switcher.filter_input.keyPressEvent(event)
188 def disable_rename(_path, _name, _new_name):
189 return False
192 # pylint: disable=too-many-ancestors
193 class BookmarksTreeView(standard.TreeView):
194 default_changed = Signal()
195 toggle_switcher = Signal(bool)
196 # this signal will be emitted when some key pressed while focusing on tree view
197 switcher_text = Signal(QtGui.QKeyEvent)
199 def __init__(self, context, style, set_model, parent=None):
200 standard.TreeView.__init__(self, parent=parent)
201 self.context = context
202 self.style = style
203 self.set_model = set_model
205 self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
206 self.setHeaderHidden(True)
208 # We make the items editable, but we don't want the double-click
209 # behavior to trigger editing. Make it behave like Mac OS X's Finder.
210 self.setEditTriggers(self.SelectedClicked)
212 self.open_action = qtutils.add_action(
213 self, N_('Open'), self.open_repo, hotkeys.OPEN
216 self.accept_action = qtutils.add_action(
217 self, N_('Accept'), self.accept_repo, *hotkeys.ACCEPT
220 self.open_new_action = qtutils.add_action(
221 self, N_('Open in New Window'), self.open_new_repo, hotkeys.NEW
224 self.set_default_repo_action = qtutils.add_action(
225 self, N_('Set Default Repository'), self.set_default_repo
228 self.clear_default_repo_action = qtutils.add_action(
229 self, N_('Clear Default Repository'), self.clear_default_repo
232 self.rename_repo_action = qtutils.add_action(
233 self, N_('Rename Repository'), self.rename_repo
236 self.open_default_action = qtutils.add_action(
237 self, cmds.OpenDefaultApp.name(), self.open_default, hotkeys.PRIMARY_ACTION
240 self.launch_editor_action = qtutils.add_action(
241 self, cmds.Edit.name(), self.launch_editor, hotkeys.EDIT
244 self.launch_terminal_action = qtutils.add_action(
245 self, cmds.LaunchTerminal.name(), self.launch_terminal, hotkeys.TERMINAL
248 self.copy_action = qtutils.add_action(self, N_('Copy'), self.copy, hotkeys.COPY)
250 self.delete_action = qtutils.add_action(
251 self, N_('Delete'), self.delete_bookmark
254 self.remove_missing_action = qtutils.add_action(
255 self, N_('Prune Missing Entries'), self.remove_missing
257 self.remove_missing_action.setToolTip(
258 N_('Remove stale entries for repositories that no longer exist')
261 self.action_group = utils.Group(
262 self.open_action,
263 self.open_new_action,
264 self.copy_action,
265 self.launch_editor_action,
266 self.launch_terminal_action,
267 self.open_default_action,
268 self.rename_repo_action,
269 self.delete_action,
271 self.action_group.setEnabled(False)
272 self.set_default_repo_action.setEnabled(False)
273 self.clear_default_repo_action.setEnabled(False)
275 # Connections
276 if style == RECENT_REPOS:
277 context.model.worktree_changed.connect(
278 self.refresh, type=Qt.QueuedConnection
281 def keyPressEvent(self, event):
283 This will be hooked while focusing on this list view.
284 Set input field invisible when escape key pressed.
285 Move selection when move key like tab, UP etc pressed.
286 Or open input field and simply act like text input to it. This is when
287 some character key pressed while focusing on tree view, NOT input field.
289 selection_moving_keys = switcher.moving_keys()
290 pressed_key = event.key()
292 if pressed_key == Qt.Key_Escape:
293 self.toggle_switcher.emit(False)
294 elif pressed_key in hotkeys.ACCEPT:
295 self.accept_repo()
296 elif pressed_key in selection_moving_keys:
297 super().keyPressEvent(event)
298 else:
299 self.toggle_switcher.emit(True)
300 self.switcher_text.emit(event)
302 def refresh(self):
303 context = self.context
304 settings = context.settings
305 builder = BuildItem(context)
307 # bookmarks
308 if self.style == BOOKMARKS:
309 entries = settings.bookmarks
310 # recent items
311 elif self.style == RECENT_REPOS:
312 entries = settings.recent
314 items = [builder.get(entry['path'], entry['name']) for entry in entries]
315 if self.style == BOOKMARKS and prefs.sort_bookmarks(context):
316 items.sort(key=lambda x: x.name.lower())
318 self.set_model(items)
320 def contextMenuEvent(self, event):
321 menu = qtutils.create_menu(N_('Actions'), self)
322 menu.addAction(self.open_action)
323 menu.addAction(self.open_new_action)
324 menu.addAction(self.open_default_action)
325 menu.addSeparator()
326 menu.addAction(self.copy_action)
327 menu.addAction(self.launch_editor_action)
328 menu.addAction(self.launch_terminal_action)
329 menu.addSeparator()
330 item = self.selected_item()
331 is_default = bool(item and item.is_default)
332 if is_default:
333 menu.addAction(self.clear_default_repo_action)
334 else:
335 menu.addAction(self.set_default_repo_action)
336 menu.addAction(self.rename_repo_action)
337 menu.addSeparator()
338 menu.addAction(self.delete_action)
339 menu.addAction(self.remove_missing_action)
340 menu.exec_(self.mapToGlobal(event.pos()))
342 def item_selection_changed(self, selected, _deselected):
343 item_idx = selected.indexes()
344 if item_idx:
345 item = self.model().itemFromIndex(item_idx[0])
346 enabled = bool(item)
347 self.action_group.setEnabled(enabled)
349 is_default = bool(item and item.is_default)
350 self.set_default_repo_action.setEnabled(not is_default)
351 self.clear_default_repo_action.setEnabled(is_default)
353 def tree_double_clicked(self, _index):
354 context = self.context
355 item = self.selected_item()
356 cmds.do(cmds.OpenRepo, context, item.path)
357 self.toggle_switcher.emit(False)
359 def selected_item(self):
360 index = self.currentIndex()
361 return self.model().itemFromIndex(index)
363 def item_changed(self, _top_left, _bottom_right, _roles):
364 item = self.selected_item()
365 self.rename_entry(item, item.text())
367 def rename_entry(self, item, new_name):
368 settings = self.context.settings
369 if self.style == BOOKMARKS:
370 rename = settings.rename_bookmark
371 elif self.style == RECENT_REPOS:
372 rename = settings.rename_recent
373 else:
374 rename = disable_rename
375 if rename(item.path, item.name, new_name):
376 settings.save()
377 item.name = new_name
378 else:
379 item.setText(item.name)
380 self.toggle_switcher.emit(False)
382 def apply_func(self, func, *args, **kwargs):
383 item = self.selected_item()
384 if item:
385 func(item, *args, **kwargs)
387 def copy(self):
388 self.apply_func(lambda item: qtutils.set_clipboard(item.path))
389 self.toggle_switcher.emit(False)
391 def open_default(self):
392 context = self.context
393 self.apply_func(lambda item: cmds.do(cmds.OpenDefaultApp, context, [item.path]))
394 self.toggle_switcher.emit(False)
396 def set_default_repo(self):
397 self.apply_func(self.set_default_item)
398 self.toggle_switcher.emit(False)
400 def set_default_item(self, item):
401 context = self.context
402 cmds.do(cmds.SetDefaultRepo, context, item.path)
403 self.refresh()
404 self.default_changed.emit()
405 self.toggle_switcher.emit(False)
407 def clear_default_repo(self):
408 self.apply_func(self.clear_default_item)
409 self.default_changed.emit()
410 self.toggle_switcher.emit(False)
412 def clear_default_item(self, _item):
413 context = self.context
414 cmds.do(cmds.SetDefaultRepo, context, None)
415 self.refresh()
416 self.toggle_switcher.emit(False)
418 def rename_repo(self):
419 index = self.currentIndex()
420 self.edit(index)
421 self.toggle_switcher.emit(False)
423 def accept_repo(self):
424 self.apply_func(self.accept_item)
425 self.toggle_switcher.emit(False)
427 def accept_item(self, _item):
428 if self.state() & self.EditingState:
429 current_index = self.currentIndex()
430 widget = self.indexWidget(current_index)
431 if widget:
432 self.commitData(widget)
433 self.closePersistentEditor(current_index)
434 self.refresh()
435 else:
436 self.open_selected_repo()
438 def open_repo(self):
439 context = self.context
440 self.apply_func(lambda item: cmds.do(cmds.OpenRepo, context, item.path))
442 def open_selected_repo(self):
443 item = self.selected_item()
444 context = self.context
445 cmds.do(cmds.OpenRepo, context, item.path)
446 self.toggle_switcher.emit(False)
448 def open_new_repo(self):
449 context = self.context
450 self.apply_func(lambda item: cmds.do(cmds.OpenNewRepo, context, item.path))
451 self.toggle_switcher.emit(False)
453 def launch_editor(self):
454 context = self.context
455 self.apply_func(lambda item: cmds.do(cmds.Edit, context, [item.path]))
456 self.toggle_switcher.emit(False)
458 def launch_terminal(self):
459 context = self.context
460 self.apply_func(lambda item: cmds.do(cmds.LaunchTerminal, context, item.path))
461 self.toggle_switcher.emit(False)
463 def add_bookmark(self):
464 normpath = utils.expandpath(core.getcwd())
465 name = os.path.basename(normpath)
466 prompt = (
467 (N_('Name'), name),
468 (N_('Path'), core.getcwd()),
470 ok, values = qtutils.prompt_n(N_('Add Favorite'), prompt)
471 if not ok:
472 return
473 name, path = values
474 normpath = utils.expandpath(path)
475 if git.is_git_worktree(normpath):
476 settings = self.context.settings
477 settings.load()
478 settings.add_bookmark(normpath, name)
479 settings.save()
480 self.refresh()
481 else:
482 Interaction.critical(N_('Error'), N_('%s is not a Git repository.') % path)
484 def delete_bookmark(self):
485 """Removes a bookmark from the bookmarks list"""
486 item = self.selected_item()
487 context = self.context
488 if not item:
489 return
490 if self.style == BOOKMARKS:
491 cmd = cmds.RemoveBookmark
492 elif self.style == RECENT_REPOS:
493 cmd = cmds.RemoveRecent
494 else:
495 return
496 ok, _, _, _ = cmds.do(cmd, context, item.path, item.name, icon=icons.discard())
497 if ok:
498 self.refresh()
499 self.toggle_switcher.emit(False)
501 def remove_missing(self):
502 """Remove missing entries from the favorites/recent file list"""
503 settings = self.context.settings
504 if self.style == BOOKMARKS:
505 settings.remove_missing_bookmarks()
506 elif self.style == RECENT_REPOS:
507 settings.remove_missing_recent()
508 self.refresh()
511 class BuildItem(object):
512 def __init__(self, context):
513 self.star_icon = icons.star()
514 self.folder_icon = icons.folder()
515 cfg = context.cfg
516 self.default_repo = cfg.get('cola.defaultrepo')
518 def get(self, path, name):
519 is_default = self.default_repo == path
520 if is_default:
521 icon = self.star_icon
522 else:
523 icon = self.folder_icon
524 return BookmarksTreeItem(path, name, icon, is_default)
527 class BookmarksTreeItem(switcher.SwitcherListItem):
528 def __init__(self, path, name, icon, is_default):
529 switcher.SwitcherListItem.__init__(self, name, icon=icon, name=name)
531 self.path = path
532 self.name = name
533 self.is_default = is_default
535 self.setIcon(icon)
536 self.setText(name)
537 self.setToolTip(path)
538 self.setFlags(self.flags() | Qt.ItemIsEditable)