Merge pull request #1387 from davvid/remote-dialog
[git-cola.git] / cola / widgets / bookmarks.py
blobba4019f0512d295d96f7412c345ced6ee6b6faac
1 """Provides widgets related to bookmarks"""
2 import os
4 from qtpy import QtCore
5 from qtpy import QtGui
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
22 from ..widgets import switcher
25 BOOKMARKS = 0
26 RECENT_REPOS = 1
29 def bookmark(context, parent):
30 return BookmarksWidget(context, BOOKMARKS, parent=parent)
33 def recent(context, parent):
34 return BookmarksWidget(context, RECENT_REPOS, parent=parent)
37 class BookmarksWidget(QtWidgets.QFrame):
38 def __init__(self, context, style=BOOKMARKS, parent=None):
39 QtWidgets.QFrame.__init__(self, parent)
41 self.context = context
42 self.style = style
44 self.items = items = []
45 self.model = model = QtGui.QStandardItemModel()
47 settings = context.settings
48 builder = BuildItem(context)
49 # bookmarks
50 if self.style == BOOKMARKS:
51 entries = settings.bookmarks
52 # recent items
53 elif self.style == RECENT_REPOS:
54 entries = settings.recent
56 for entry in entries:
57 item = builder.get(entry['path'], entry['name'])
58 items.append(item)
59 model.appendRow(item)
61 place_holder = N_('Search repositories by name...')
62 self.quick_switcher = switcher.switcher_outer_view(
63 context, model, place_holder=place_holder
65 self.tree = BookmarksTreeView(
66 context, style, self.set_items_to_models, parent=self
69 self.add_button = qtutils.create_action_button(
70 tooltip=N_('Add'), icon=icons.add()
73 self.delete_button = qtutils.create_action_button(
74 tooltip=N_('Delete'), icon=icons.remove()
77 self.open_button = qtutils.create_action_button(
78 tooltip=N_('Open'), icon=icons.repo()
81 self.search_button = qtutils.create_action_button(
82 tooltip=N_('Search'), icon=icons.search()
85 self.button_group = utils.Group(self.delete_button, self.open_button)
86 self.button_group.setEnabled(False)
88 self.setFocusProxy(self.tree)
89 if style == BOOKMARKS:
90 self.setToolTip(N_('Favorite repositories'))
91 elif style == RECENT_REPOS:
92 self.setToolTip(N_('Recent repositories'))
93 self.add_button.hide()
95 self.button_layout = qtutils.hbox(
96 defs.no_margin,
97 defs.spacing,
98 self.search_button,
99 self.open_button,
100 self.add_button,
101 self.delete_button,
104 self.main_layout = qtutils.vbox(
105 defs.no_margin, defs.spacing, self.quick_switcher, self.tree
107 self.setLayout(self.main_layout)
109 self.corner_widget = QtWidgets.QWidget(self)
110 self.corner_widget.setLayout(self.button_layout)
111 titlebar = parent.titleBarWidget()
112 titlebar.add_corner_widget(self.corner_widget)
114 qtutils.connect_button(self.add_button, self.tree.add_bookmark)
115 qtutils.connect_button(self.delete_button, self.tree.delete_bookmark)
116 qtutils.connect_button(self.open_button, self.tree.open_repo)
117 qtutils.connect_button(self.search_button, self.toggle_switcher_input_field)
119 QtCore.QTimer.singleShot(0, self.reload_bookmarks)
121 self.tree.toggle_switcher.connect(self.enable_switcher_input_field)
122 # moving key has pressed while focusing on input field
123 self.quick_switcher.filter_input.switcher_selection_move.connect(
124 self.tree.keyPressEvent
126 # escape key has pressed while focusing on input field
127 self.quick_switcher.filter_input.switcher_escape.connect(
128 self.close_switcher_input_field
130 # some key except moving key has pressed while focusing on list view
131 self.tree.switcher_text.connect(self.switcher_text_inputted)
133 def reload_bookmarks(self):
134 # Called once after the GUI is initialized
135 tree = self.tree
136 tree.refresh()
138 model = tree.model()
140 model.dataChanged.connect(tree.item_changed)
141 selection = tree.selectionModel()
142 selection.selectionChanged.connect(tree.item_selection_changed)
143 tree.doubleClicked.connect(tree.tree_double_clicked)
145 def tree_item_selection_changed(self):
146 enabled = bool(self.tree.selected_item())
147 self.button_group.setEnabled(enabled)
149 def connect_to(self, other):
150 self.tree.default_changed.connect(other.tree.refresh)
151 other.tree.default_changed.connect(self.tree.refresh)
153 def set_items_to_models(self, items):
154 model = self.model
155 self.items.clear()
156 model.clear()
158 for item in items:
159 self.items.append(item)
160 model.appendRow(item)
162 self.quick_switcher.proxy_model.setSourceModel(model)
163 self.tree.setModel(self.quick_switcher.proxy_model)
165 def toggle_switcher_input_field(self):
166 visible = self.quick_switcher.filter_input.isVisible()
167 self.enable_switcher_input_field(not visible)
169 def close_switcher_input_field(self):
170 self.enable_switcher_input_field(False)
172 def enable_switcher_input_field(self, visible):
173 filter_input = self.quick_switcher.filter_input
175 filter_input.setVisible(visible)
176 if not visible:
177 filter_input.clear()
179 def switcher_text_inputted(self, event):
180 # default selection for first index
181 first_proxy_idx = self.quick_switcher.proxy_model.index(0, 0)
182 self.tree.setCurrentIndex(first_proxy_idx)
184 self.quick_switcher.filter_input.keyPressEvent(event)
187 def disable_rename(_path, _name, _new_name):
188 return False
191 class BookmarksTreeView(standard.TreeView):
192 default_changed = Signal()
193 toggle_switcher = Signal(bool)
194 # this signal will be emitted when some key pressed while focusing on tree view
195 switcher_text = Signal(QtGui.QKeyEvent)
197 def __init__(self, context, style, set_model, parent=None):
198 standard.TreeView.__init__(self, parent=parent)
199 self.context = context
200 self.style = style
201 self.set_model = set_model
203 self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
204 self.setHeaderHidden(True)
206 # We make the items editable, but we don't want the double-click
207 # behavior to trigger editing. Make it behave like Mac OS X's Finder.
208 self.setEditTriggers(self.SelectedClicked)
210 self.open_action = qtutils.add_action(
211 self, N_('Open'), self.open_repo, hotkeys.OPEN
214 self.accept_action = qtutils.add_action(
215 self, N_('Accept'), self.accept_repo, *hotkeys.ACCEPT
218 self.open_new_action = qtutils.add_action(
219 self, N_('Open in New Window'), self.open_new_repo, hotkeys.NEW
222 self.set_default_repo_action = qtutils.add_action(
223 self, N_('Set Default Repository'), self.set_default_repo
226 self.clear_default_repo_action = qtutils.add_action(
227 self, N_('Clear Default Repository'), self.clear_default_repo
230 self.rename_repo_action = qtutils.add_action(
231 self, N_('Rename Repository'), self.rename_repo
234 self.open_default_action = qtutils.add_action(
235 self, cmds.OpenDefaultApp.name(), self.open_default, hotkeys.PRIMARY_ACTION
238 self.launch_editor_action = qtutils.add_action(
239 self, cmds.Edit.name(), self.launch_editor, hotkeys.EDIT
242 self.launch_terminal_action = qtutils.add_action(
243 self, cmds.LaunchTerminal.name(), self.launch_terminal, hotkeys.TERMINAL
246 self.copy_action = qtutils.add_action(self, N_('Copy'), self.copy, hotkeys.COPY)
248 self.delete_action = qtutils.add_action(
249 self, N_('Delete'), self.delete_bookmark
252 self.remove_missing_action = qtutils.add_action(
253 self, N_('Prune Missing Entries'), self.remove_missing
255 self.remove_missing_action.setToolTip(
256 N_('Remove stale entries for repositories that no longer exist')
259 self.action_group = utils.Group(
260 self.open_action,
261 self.open_new_action,
262 self.copy_action,
263 self.launch_editor_action,
264 self.launch_terminal_action,
265 self.open_default_action,
266 self.rename_repo_action,
267 self.delete_action,
269 self.action_group.setEnabled(False)
270 self.set_default_repo_action.setEnabled(False)
271 self.clear_default_repo_action.setEnabled(False)
273 # Connections
274 if style == RECENT_REPOS:
275 context.model.worktree_changed.connect(
276 self.refresh, type=Qt.QueuedConnection
279 def keyPressEvent(self, event):
281 This will be hooked while focusing on this list view.
282 Set input field invisible when escape key pressed.
283 Move selection when move key like tab, UP etc pressed.
284 Or open input field and simply act like text input to it. This is when
285 some character key pressed while focusing on tree view, NOT input field.
287 selection_moving_keys = switcher.moving_keys()
288 pressed_key = event.key()
290 if pressed_key == Qt.Key_Escape:
291 self.toggle_switcher.emit(False)
292 elif pressed_key in hotkeys.ACCEPT:
293 self.accept_repo()
294 elif pressed_key in selection_moving_keys:
295 super().keyPressEvent(event)
296 else:
297 self.toggle_switcher.emit(True)
298 self.switcher_text.emit(event)
300 def refresh(self):
301 context = self.context
302 settings = context.settings
303 builder = BuildItem(context)
305 # bookmarks
306 if self.style == BOOKMARKS:
307 entries = settings.bookmarks
308 # recent items
309 elif self.style == RECENT_REPOS:
310 entries = settings.recent
312 items = [builder.get(entry['path'], entry['name']) for entry in entries]
313 if self.style == BOOKMARKS and prefs.sort_bookmarks(context):
314 items.sort(key=lambda x: x.name.lower())
316 self.set_model(items)
318 def contextMenuEvent(self, event):
319 menu = qtutils.create_menu(N_('Actions'), self)
320 menu.addAction(self.open_action)
321 menu.addAction(self.open_new_action)
322 menu.addAction(self.open_default_action)
323 menu.addSeparator()
324 menu.addAction(self.copy_action)
325 menu.addAction(self.launch_editor_action)
326 menu.addAction(self.launch_terminal_action)
327 menu.addSeparator()
328 item = self.selected_item()
329 is_default = bool(item and item.is_default)
330 if is_default:
331 menu.addAction(self.clear_default_repo_action)
332 else:
333 menu.addAction(self.set_default_repo_action)
334 menu.addAction(self.rename_repo_action)
335 menu.addSeparator()
336 menu.addAction(self.delete_action)
337 menu.addAction(self.remove_missing_action)
338 menu.exec_(self.mapToGlobal(event.pos()))
340 def item_selection_changed(self, selected, _deselected):
341 item_idx = selected.indexes()
342 if item_idx:
343 item = self.model().itemFromIndex(item_idx[0])
344 enabled = bool(item)
345 self.action_group.setEnabled(enabled)
347 is_default = bool(item and item.is_default)
348 self.set_default_repo_action.setEnabled(not is_default)
349 self.clear_default_repo_action.setEnabled(is_default)
351 def tree_double_clicked(self, _index):
352 context = self.context
353 item = self.selected_item()
354 cmds.do(cmds.OpenRepo, context, item.path)
355 self.toggle_switcher.emit(False)
357 def selected_item(self):
358 index = self.currentIndex()
359 return self.model().itemFromIndex(index)
361 def item_changed(self, _top_left, _bottom_right, _roles):
362 item = self.selected_item()
363 self.rename_entry(item, item.text())
365 def rename_entry(self, item, new_name):
366 settings = self.context.settings
367 if self.style == BOOKMARKS:
368 rename = settings.rename_bookmark
369 elif self.style == RECENT_REPOS:
370 rename = settings.rename_recent
371 else:
372 rename = disable_rename
373 if rename(item.path, item.name, new_name):
374 settings.save()
375 item.name = new_name
376 else:
377 item.setText(item.name)
378 self.toggle_switcher.emit(False)
380 def apply_func(self, func, *args, **kwargs):
381 item = self.selected_item()
382 if item:
383 func(item, *args, **kwargs)
385 def copy(self):
386 self.apply_func(lambda item: qtutils.set_clipboard(item.path))
387 self.toggle_switcher.emit(False)
389 def open_default(self):
390 context = self.context
391 self.apply_func(lambda item: cmds.do(cmds.OpenDefaultApp, context, [item.path]))
392 self.toggle_switcher.emit(False)
394 def set_default_repo(self):
395 self.apply_func(self.set_default_item)
396 self.toggle_switcher.emit(False)
398 def set_default_item(self, item):
399 context = self.context
400 cmds.do(cmds.SetDefaultRepo, context, item.path)
401 self.refresh()
402 self.default_changed.emit()
403 self.toggle_switcher.emit(False)
405 def clear_default_repo(self):
406 self.apply_func(self.clear_default_item)
407 self.default_changed.emit()
408 self.toggle_switcher.emit(False)
410 def clear_default_item(self, _item):
411 context = self.context
412 cmds.do(cmds.SetDefaultRepo, context, None)
413 self.refresh()
414 self.toggle_switcher.emit(False)
416 def rename_repo(self):
417 index = self.currentIndex()
418 self.edit(index)
419 self.toggle_switcher.emit(False)
421 def accept_repo(self):
422 self.apply_func(self.accept_item)
423 self.toggle_switcher.emit(False)
425 def accept_item(self, _item):
426 if self.state() & self.EditingState:
427 current_index = self.currentIndex()
428 widget = self.indexWidget(current_index)
429 if widget:
430 self.commitData(widget)
431 self.closePersistentEditor(current_index)
432 self.refresh()
433 else:
434 self.open_selected_repo()
436 def open_repo(self):
437 context = self.context
438 self.apply_func(lambda item: cmds.do(cmds.OpenRepo, context, item.path))
440 def open_selected_repo(self):
441 item = self.selected_item()
442 context = self.context
443 cmds.do(cmds.OpenRepo, context, item.path)
444 self.toggle_switcher.emit(False)
446 def open_new_repo(self):
447 context = self.context
448 self.apply_func(lambda item: cmds.do(cmds.OpenNewRepo, context, item.path))
449 self.toggle_switcher.emit(False)
451 def launch_editor(self):
452 context = self.context
453 self.apply_func(lambda item: cmds.do(cmds.Edit, context, [item.path]))
454 self.toggle_switcher.emit(False)
456 def launch_terminal(self):
457 context = self.context
458 self.apply_func(lambda item: cmds.do(cmds.LaunchTerminal, context, item.path))
459 self.toggle_switcher.emit(False)
461 def add_bookmark(self):
462 normpath = utils.expandpath(core.getcwd())
463 name = os.path.basename(normpath)
464 prompt = (
465 (N_('Name'), name),
466 (N_('Path'), core.getcwd()),
468 ok, values = qtutils.prompt_n(N_('Add Favorite'), prompt)
469 if not ok:
470 return
471 name, path = values
472 normpath = utils.expandpath(path)
473 if git.is_git_worktree(normpath):
474 settings = self.context.settings
475 settings.load()
476 settings.add_bookmark(normpath, name)
477 settings.save()
478 self.refresh()
479 else:
480 Interaction.critical(N_('Error'), N_('%s is not a Git repository.') % path)
482 def delete_bookmark(self):
483 """Removes a bookmark from the bookmarks list"""
484 item = self.selected_item()
485 context = self.context
486 if not item:
487 return
488 if self.style == BOOKMARKS:
489 cmd = cmds.RemoveBookmark
490 elif self.style == RECENT_REPOS:
491 cmd = cmds.RemoveRecent
492 else:
493 return
494 ok, _, _, _ = cmds.do(cmd, context, item.path, item.name, icon=icons.discard())
495 if ok:
496 self.refresh()
497 self.toggle_switcher.emit(False)
499 def remove_missing(self):
500 """Remove missing entries from the favorites/recent file list"""
501 settings = self.context.settings
502 if self.style == BOOKMARKS:
503 settings.remove_missing_bookmarks()
504 elif self.style == RECENT_REPOS:
505 settings.remove_missing_recent()
506 self.refresh()
509 class BuildItem:
510 def __init__(self, context):
511 self.star_icon = icons.star()
512 self.folder_icon = icons.folder()
513 cfg = context.cfg
514 self.default_repo = cfg.get('cola.defaultrepo')
516 def get(self, path, name):
517 is_default = self.default_repo == path
518 if is_default:
519 icon = self.star_icon
520 else:
521 icon = self.folder_icon
522 return BookmarksTreeItem(path, name, icon, is_default)
525 class BookmarksTreeItem(switcher.SwitcherListItem):
526 def __init__(self, path, name, icon, is_default):
527 switcher.SwitcherListItem.__init__(self, name, icon=icon, name=name)
529 self.path = path
530 self.name = name
531 self.is_default = is_default
533 self.setIcon(icon)
534 self.setText(name)
535 self.setToolTip(path)
536 self.setFlags(self.flags() | Qt.ItemIsEditable)