1"""Provides widgets related to bookmarks""" 2from __future__ import division, absolute_import, unicode_literals 3import os 4 5from qtpy import QtCore 6from qtpy import QtWidgets 7from qtpy.QtCore import Qt 8from qtpy.QtCore import Signal 9 10from .. import cmds 11from .. import core 12from .. import git 13from .. import hotkeys 14from .. import icons 15from .. import qtutils 16from .. import utils 17from ..i18n import N_ 18from ..interaction import Interaction 19from ..models import prefs 20from ..widgets import defs 21from ..widgets import standard 22 23 24BOOKMARKS = 0 25RECENT_REPOS = 1 26 27 28def bookmark(context, parent): 29 return BookmarksWidget(context, BOOKMARKS, parent=parent) 30 31 32def recent(context, parent): 33 return BookmarksWidget(context, RECENT_REPOS, parent=parent) 34 35 36class BookmarksWidget(QtWidgets.QFrame): 37 def __init__(self, context, style=BOOKMARKS, parent=None): 38 QtWidgets.QFrame.__init__(self, parent) 39 40 self.context = context 41 self.style = style 42 self.tree = BookmarksTreeWidget(context, style, parent=self) 43 44 self.add_button = qtutils.create_action_button( 45 tooltip=N_('Add'), icon=icons.add() 46 ) 47 48 self.delete_button = qtutils.create_action_button( 49 tooltip=N_('Delete'), icon=icons.remove() 50 ) 51 52 self.open_button = qtutils.create_action_button( 53 tooltip=N_('Open'), icon=icons.repo() 54 ) 55 56 self.button_group = utils.Group(self.delete_button, self.open_button) 57 self.button_group.setEnabled(False) 58 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() 65 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, 72 ) 73 74 self.main_layout = qtutils.vbox(defs.no_margin, defs.spacing, self.tree) 75 self.setLayout(self.main_layout) 76 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) 81 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) 85 86 item_selection_changed = self.tree_item_selection_changed 87 # pylint: disable=no-member 88 self.tree.itemSelectionChanged.connect(item_selection_changed) 89 90 QtCore.QTimer.singleShot(0, self.reload_bookmarks) 91 92 def reload_bookmarks(self): 93 # Called once after the GUI is initialized 94 self.tree.refresh() 95 96 def tree_item_selection_changed(self): 97 enabled = bool(self.tree.selected_item()) 98 self.button_group.setEnabled(enabled) 99 100 def connect_to(self, other): 101 self.tree.default_changed.connect(other.tree.refresh) 102 other.tree.default_changed.connect(self.tree.refresh) 103 104 105def disable_rename(_path, _name, _new_name): 106 return False 107 108 109# pylint: disable=too-many-ancestors 110class BookmarksTreeWidget(standard.TreeWidget): 111 default_changed = Signal() 112 worktree_changed = Signal() 113 114 def __init__(self, context, style, parent=None): 115 standard.TreeWidget.__init__(self, parent=parent) 116 self.context = context 117 self.style = style 118 119 self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) 120 self.setHeaderHidden(True) 121 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) 125 126 self.open_action = qtutils.add_action( 127 self, N_('Open'), self.open_repo, hotkeys.OPEN 128 ) 129 130 self.accept_action = qtutils.add_action( 131 self, N_('Accept'), self.accept_repo, *hotkeys.ACCEPT 132 ) 133 134 self.open_new_action = qtutils.add_action( 135 self, N_('Open in New Window'), self.open_new_repo, hotkeys.NEW 136 ) 137 138 self.set_default_repo_action = qtutils.add_action( 139 self, N_('Set Default Repository'), self.set_default_repo 140 ) 141 142 self.clear_default_repo_action = qtutils.add_action( 143 self, N_('Clear Default Repository'), self.clear_default_repo 144 ) 145 146 self.rename_repo_action = qtutils.add_action( 147 self, N_('Rename Repository'), self.rename_repo 148 ) 149 150 self.open_default_action = qtutils.add_action( 151 self, cmds.OpenDefaultApp.name(), self.open_default, hotkeys.PRIMARY_ACTION 152 ) 153 154 self.launch_editor_action = qtutils.add_action( 155 self, cmds.Edit.name(), self.launch_editor, hotkeys.EDIT 156 ) 157 158 self.launch_terminal_action = qtutils.add_action( 159 self, cmds.LaunchTerminal.name(), self.launch_terminal, hotkeys.TERMINAL 160 ) 161 162 self.copy_action = qtutils.add_action(self, N_('Copy'), self.copy, hotkeys.COPY) 163 164 self.delete_action = qtutils.add_action( 165 self, N_('Delete'), self.delete_bookmark 166 ) 167 168 self.remove_missing_action = qtutils.add_action( 169 self, N_('Prune Missing Entries'), self.remove_missing 170 ) 171 self.remove_missing_action.setToolTip( 172 N_('Remove stale entries for repositories that no longer exist') 173 ) 174 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) 179 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, 189 ) 190 self.action_group.setEnabled(False) 191 self.set_default_repo_action.setEnabled(False) 192 self.clear_default_repo_action.setEnabled(False) 193 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 199 ) 200 201 def refresh(self): 202 context = self.context 203 settings = context.settings 204 builder = BuildItem(context) 205 206 # bookmarks 207 if self.style == BOOKMARKS: 208 entries = settings.bookmarks 209 # recent items 210 elif self.style == RECENT_REPOS: 211 entries = settings.recent 212 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) 216 217 self.clear() 218 self.addTopLevelItems(items) 219 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())) 241 242 def item_changed(self, item, _index): 243 self.rename_entry(item, item.text(0)) 244 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) 258 259 def apply_fn(self, fn, *args, **kwargs): 260 item = self.selected_item() 261 if item: 262 fn(item, *args, **kwargs) 263 264 def copy(self): 265 self.apply_fn(lambda item: qtutils.set_clipboard(item.path)) 266 267 def open_default(self): 268 context = self.context 269 self.apply_fn(lambda item: cmds.do(cmds.OpenDefaultApp, context, [item.path])) 270 271 def set_default_repo(self): 272 self.apply_fn(self.set_default_item) 273 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() 279 280 def clear_default_repo(self): 281 self.apply_fn(self.clear_default_item) 282 self.default_changed.emit() 283 284 def clear_default_item(self, _item): 285 context = self.context 286 cmds.do(cmds.SetDefaultRepo, context, None) 287 self.refresh() 288 289 def rename_repo(self): 290 self.apply_fn(lambda item: self.editItem(item, 0)) 291 292 def accept_repo(self): 293 self.apply_fn(self.accept_item) 294 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() 303 304 def open_repo(self): 305 context = self.context 306 self.apply_fn(lambda item: cmds.do(cmds.OpenRepo, context, item.path)) 307 308 def open_new_repo(self): 309 context = self.context 310 self.apply_fn(lambda item: cmds.do(cmds.OpenNewRepo, context, item.path)) 311 312 def launch_editor(self): 313 context = self.context 314 self.apply_fn(lambda item: cmds.do(cmds.Edit, context, [item.path])) 315 316 def launch_terminal(self): 317 context = self.context 318 self.apply_fn(lambda item: cmds.do(cmds.LaunchTerminal, context, item.path)) 319 320 def item_selection_changed(self): 321 item = self.selected_item() 322 enabled = bool(item) 323 self.action_group.setEnabled(enabled) 324 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) 328 329 def tree_double_clicked(self, item, _column): 330 context = self.context 331 cmds.do(cmds.OpenRepo, context, item.path) 332 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()), 339 ) 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) 353 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() 369 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() 378 379 380class 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') 386 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) 394 395 396class 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 402 403 self.setIcon(0, icon) 404 self.setText(0, name) 405 self.setToolTip(0, path) 406 self.setFlags(self.flags() | Qt.ItemIsEditable) 407