core: make getcwd() fail-safe
[git-cola.git] / cola / widgets / bookmarks.py
blobbd63162dd5f9c742e57d6ec48bfd9b66395fadea
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 ..settings import Settings
21 from ..widgets import defs
22 from ..widgets import standard
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):
39 def __init__(self, context, style=BOOKMARKS, parent=None):
40 QtWidgets.QFrame.__init__(self, parent)
42 self.style = style
43 self.settings = Settings()
44 self.tree = BookmarksTreeWidget(
45 context, style, self.settings, parent=self)
47 self.add_button = qtutils.create_action_button(
48 tooltip=N_('Add'), icon=icons.add())
50 self.delete_button = qtutils.create_action_button(
51 tooltip=N_('Delete'), icon=icons.remove())
53 self.open_button = qtutils.create_action_button(
54 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(defs.no_margin, defs.spacing,
67 self.open_button, self.add_button,
68 self.delete_button)
70 self.main_layout = qtutils.vbox(defs.no_margin, defs.spacing, self.tree)
71 self.setLayout(self.main_layout)
73 self.corner_widget = QtWidgets.QWidget(self)
74 self.corner_widget.setLayout(self.button_layout)
75 titlebar = parent.titleBarWidget()
76 titlebar.add_corner_widget(self.corner_widget)
78 qtutils.connect_button(self.add_button, self.tree.add_bookmark)
79 qtutils.connect_button(self.delete_button, self.tree.delete_bookmark)
80 qtutils.connect_button(self.open_button, self.tree.open_repo)
82 item_selection_changed = self.tree_item_selection_changed
83 self.tree.itemSelectionChanged.connect(item_selection_changed)
85 QtCore.QTimer.singleShot(0, self.reload_bookmarks)
87 def reload_bookmarks(self):
88 # Called once after the GUI is initialized
89 self.settings.load()
90 self.tree.refresh()
92 def tree_item_selection_changed(self):
93 enabled = bool(self.tree.selected_item())
94 self.button_group.setEnabled(enabled)
96 def connect_to(self, other):
97 self.tree.default_changed.connect(other.tree.refresh)
98 other.tree.default_changed.connect(self.tree.refresh)
101 def disable_rename(_path, _name, _new_name):
102 return False
105 class BookmarksTreeWidget(standard.TreeWidget):
106 default_changed = Signal()
108 def __init__(self, context, style, settings, parent=None):
109 standard.TreeWidget.__init__(self, parent=parent)
110 self.context = context
111 self.style = style
112 self.settings = settings
114 self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
115 self.setHeaderHidden(True)
117 # We make the items editable, but we don't want the double-click
118 # behavior to trigger editing. Make it behave like Mac OS X's Finder.
119 self.setEditTriggers(self.SelectedClicked)
121 self.open_action = qtutils.add_action(
122 self, N_('Open'), self.open_repo, hotkeys.OPEN)
124 self.accept_action = qtutils.add_action(
125 self, N_('Accept'), self.accept_repo, *hotkeys.ACCEPT)
127 self.open_new_action = qtutils.add_action(
128 self, N_('Open in New Window'), self.open_new_repo, hotkeys.NEW)
130 self.set_default_repo_action = qtutils.add_action(
131 self, N_('Set Default Repository'), self.set_default_repo)
133 self.clear_default_repo_action = qtutils.add_action(
134 self, N_('Clear Default Repository'), self.clear_default_repo)
136 self.rename_repo_action = qtutils.add_action(
137 self, N_('Rename Repository'), self.rename_repo)
139 self.open_default_action = qtutils.add_action(
140 self, cmds.OpenDefaultApp.name(), self.open_default,
141 hotkeys.PRIMARY_ACTION)
143 self.launch_editor_action = qtutils.add_action(
144 self, cmds.Edit.name(), self.launch_editor, hotkeys.EDIT)
146 self.launch_terminal_action = qtutils.add_action(
147 self, cmds.LaunchTerminal.name(), self.launch_terminal,
148 hotkeys.TERMINAL)
150 self.copy_action = qtutils.add_action(
151 self, N_('Copy'), self.copy, hotkeys.COPY)
153 self.itemChanged.connect(self.item_changed)
154 self.itemSelectionChanged.connect(self.item_selection_changed)
155 self.itemDoubleClicked.connect(self.tree_double_clicked)
157 self.action_group = utils.Group(self.open_action,
158 self.open_new_action,
159 self.copy_action,
160 self.launch_editor_action,
161 self.launch_terminal_action,
162 self.open_default_action,
163 self.rename_repo_action)
164 self.action_group.setEnabled(False)
165 self.set_default_repo_action.setEnabled(False)
166 self.clear_default_repo_action.setEnabled(False)
168 def refresh(self):
169 context = self.context
170 settings = self.settings
171 builder = BuildItem(context)
173 # bookmarks
174 if self.style == BOOKMARKS:
175 entries = settings.bookmarks
176 # recent items
177 elif self.style == RECENT_REPOS:
178 entries = settings.recent
180 items = [builder.get(entry['path'], entry['name']) for entry in entries]
181 if self.style == BOOKMARKS and prefs.sort_bookmarks(context):
182 items.sort(key=lambda x: x.name)
184 self.clear()
185 self.addTopLevelItems(items)
187 def contextMenuEvent(self, event):
188 menu = qtutils.create_menu(N_('Actions'), self)
189 menu.addAction(self.open_action)
190 menu.addAction(self.open_new_action)
191 menu.addAction(self.open_default_action)
192 menu.addSeparator()
193 menu.addAction(self.copy_action)
194 menu.addAction(self.launch_editor_action)
195 menu.addAction(self.launch_terminal_action)
196 menu.addSeparator()
197 item = self.selected_item()
198 is_default = bool(item and item.is_default)
199 if is_default:
200 menu.addAction(self.clear_default_repo_action)
201 else:
202 menu.addAction(self.set_default_repo_action)
203 menu.addAction(self.rename_repo_action)
204 menu.exec_(self.mapToGlobal(event.pos()))
206 def item_changed(self, item, _index):
207 self.rename_entry(item, item.text(0))
209 def rename_entry(self, item, new_name):
210 if self.style == BOOKMARKS:
211 rename = self.settings.rename_bookmark
212 elif self.style == RECENT_REPOS:
213 rename = self.settings.rename_recent
214 else:
215 rename = disable_rename
216 if rename(item.path, item.name, new_name):
217 self.settings.save()
218 item.name = new_name
219 else:
220 item.setText(0, item.name)
222 def apply_fn(self, fn, *args, **kwargs):
223 item = self.selected_item()
224 if item:
225 fn(item, *args, **kwargs)
227 def copy(self):
228 self.apply_fn(lambda item: qtutils.set_clipboard(item.path))
230 def open_default(self):
231 context = self.context
232 self.apply_fn(
233 lambda item: cmds.do(cmds.OpenDefaultApp, context, [item.path]))
235 def set_default_repo(self):
236 self.apply_fn(self.set_default_item)
238 def set_default_item(self, item):
239 context = self.context
240 cmds.do(cmds.SetDefaultRepo, context, item.path)
241 self.refresh()
242 self.default_changed.emit()
244 def clear_default_repo(self):
245 self.apply_fn(self.clear_default_item)
246 self.default_changed.emit()
248 def clear_default_item(self, _item):
249 context = self.context
250 cmds.do(cmds.SetDefaultRepo, context, None)
251 self.refresh()
253 def rename_repo(self):
254 self.apply_fn(lambda item: self.editItem(item, 0))
256 def accept_repo(self):
257 self.apply_fn(self.accept_item)
259 def accept_item(self, item):
260 if self.state() & self.EditingState:
261 widget = self.itemWidget(item, 0)
262 if widget:
263 self.commitData(widget)
264 self.closePersistentEditor(item, 0)
265 else:
266 self.open_repo()
268 def open_repo(self):
269 context = self.context
270 self.apply_fn(lambda item: cmds.do(cmds.OpenRepo, context, item.path))
272 def open_new_repo(self):
273 context = self.context
274 self.apply_fn(
275 lambda item: cmds.do(cmds.OpenNewRepo, context, item.path))
277 def launch_editor(self):
278 context = self.context
279 self.apply_fn(lambda item: cmds.do(cmds.Edit, context, [item.path]))
281 def launch_terminal(self):
282 context = self.context
283 self.apply_fn(
284 lambda item: cmds.do(cmds.LaunchTerminal, context, item.path))
286 def item_selection_changed(self):
287 item = self.selected_item()
288 enabled = bool(item)
289 self.action_group.setEnabled(enabled)
291 is_default = bool(item and item.is_default)
292 self.set_default_repo_action.setEnabled(not is_default)
293 self.clear_default_repo_action.setEnabled(is_default)
295 def tree_double_clicked(self, item, _column):
296 context = self.context
297 cmds.do(cmds.OpenRepo, context, item.path)
299 def add_bookmark(self):
300 normpath = utils.expandpath(core.getcwd())
301 name = os.path.basename(normpath)
302 prompt = (
303 (N_('Name'), name),
304 (N_('Path'), core.getcwd()),
306 ok, values = qtutils.prompt_n(N_('Add Favorite'), prompt)
307 if not ok:
308 return
309 name, path = values
310 normpath = utils.expandpath(path)
311 if git.is_git_worktree(normpath):
312 self.settings.add_bookmark(normpath, name)
313 self.settings.save()
314 self.refresh()
315 else:
316 Interaction.critical(
317 N_('Error'), N_('%s is not a Git repository.') % path)
319 def delete_bookmark(self):
320 """Removes a bookmark from the bookmarks list"""
321 item = self.selected_item()
322 context = self.context
323 if not item:
324 return
325 if self.style == BOOKMARKS:
326 cmd = cmds.RemoveBookmark
327 elif self.style == RECENT_REPOS:
328 cmd = cmds.RemoveRecent
329 else:
330 return
331 ok, _, _, _ = cmds.do(
332 cmd, context, self.settings, item.path, item.name,
333 icon=icons.discard())
334 if ok:
335 self.refresh()
338 class BuildItem(object):
340 def __init__(self, context):
341 self.star_icon = icons.star()
342 self.folder_icon = icons.folder()
343 cfg = context.cfg
344 self.default_repo = cfg.get('cola.defaultrepo')
346 def get(self, path, name):
347 is_default = self.default_repo == path
348 if is_default:
349 icon = self.star_icon
350 else:
351 icon = self.folder_icon
352 return BookmarksTreeWidgetItem(path, name, icon, is_default)
355 class BookmarksTreeWidgetItem(QtWidgets.QTreeWidgetItem):
357 def __init__(self, path, name, icon, is_default):
358 QtWidgets.QTreeWidgetItem.__init__(self)
359 self.path = path
360 self.name = name
361 self.is_default = is_default
363 self.setIcon(0, icon)
364 self.setText(0, name)
365 self.setToolTip(0, path)
366 self.setFlags(self.flags() | Qt.ItemIsEditable)