tree-wide: spelling corrections
[git-cola.git] / cola / widgets / startup.py
blob83db61fe8bd3a85608a9dc7fad1c4c2135794844
1 """The startup dialog is presented when no repositories can be found at startup"""
3 from qtpy.QtCore import Qt
4 from qtpy import QtCore
5 from qtpy import QtGui
6 from qtpy import QtWidgets
8 from ..i18n import N_
9 from ..models import prefs
10 from .. import cmds
11 from .. import core
12 from .. import display
13 from .. import guicmds
14 from .. import hotkeys
15 from .. import icons
16 from .. import qtutils
17 from .. import utils
18 from .. import version
19 from . import clone
20 from . import defs
21 from . import standard
24 ICON_MODE = 0
25 LIST_MODE = 1
28 class StartupDialog(standard.Dialog):
29 """Provides a GUI to Open or Clone a git repository."""
31 def __init__(self, context, parent=None):
32 standard.Dialog.__init__(self, parent)
33 self.context = context
34 self.setWindowTitle(N_('git-cola'))
36 # Top-most large icon
37 logo_pixmap = icons.cola().pixmap(defs.huge_icon, defs.huge_icon)
39 self.logo_label = QtWidgets.QLabel()
40 self.logo_label.setPixmap(logo_pixmap)
41 self.logo_label.setAlignment(Qt.AlignCenter)
43 self.logo_text_label = qtutils.label(text=version.cola_version())
44 self.logo_text_label.setAlignment(Qt.AlignCenter)
46 self.repodir = None
47 if context.runtask:
48 self.runtask = context.runtask
49 else:
50 self.runtask = context.runtask = qtutils.RunTask(parent=self)
52 self.new_button = qtutils.create_button(text=N_('New...'), icon=icons.new())
53 self.open_button = qtutils.create_button(
54 text=N_('Open Git Repository'), icon=icons.folder()
56 self.clone_button = qtutils.create_button(
57 text=N_('Clone...'), icon=icons.cola()
59 self.close_button = qtutils.close_button()
61 self.bookmarks_model = bookmarks_model = QtGui.QStandardItemModel()
62 self.items = items = []
64 item = QtGui.QStandardItem(N_('Browse...'))
65 item.setEditable(False)
66 item.setIcon(icons.open_directory())
67 bookmarks_model.appendRow(item)
69 # The tab bar allows choosing between Folder and List mode
70 self.tab_bar = QtWidgets.QTabBar()
71 self.tab_bar.setMovable(False)
72 self.tab_bar.addTab(icons.directory(), N_('Folder'))
73 self.tab_bar.addTab(icons.three_bars(), N_('List'))
75 # Bookmarks/"Favorites" and Recent are lists of {name,path: str}
76 normalize = display.normalize_path
77 settings = context.settings
78 all_repos = get_all_repos(self.context, settings)
80 added = set()
81 builder = BuildItem(self.context)
82 default_view_mode = ICON_MODE
83 for repo, is_bookmark in all_repos:
84 path = normalize(repo['path'])
85 name = normalize(repo['name'])
86 if path in added:
87 continue
88 added.add(path)
90 item = builder.get(path, name, default_view_mode, is_bookmark)
91 bookmarks_model.appendRow(item)
92 items.append(item)
94 self.bookmarks = BookmarksListView(
95 context,
96 bookmarks_model,
97 self.open_selected_bookmark,
98 self.set_model,
101 self.tab_layout = qtutils.vbox(
102 defs.no_margin, defs.no_spacing, self.tab_bar, self.bookmarks
105 self.logo_layout = qtutils.vbox(
106 defs.no_margin,
107 defs.spacing,
108 self.logo_label,
109 self.logo_text_label,
110 defs.button_spacing,
111 qtutils.STRETCH,
114 self.button_layout = qtutils.hbox(
115 defs.no_margin,
116 defs.spacing,
117 self.open_button,
118 self.clone_button,
119 self.new_button,
120 qtutils.STRETCH,
121 self.close_button,
124 self.main_layout = qtutils.grid(defs.margin, defs.spacing)
125 self.main_layout.addItem(self.logo_layout, 1, 1)
126 self.main_layout.addItem(self.tab_layout, 1, 2)
127 self.main_layout.addItem(self.button_layout, 2, 1, columnSpan=2)
128 self.setLayout(self.main_layout)
130 qtutils.connect_button(self.open_button, self.open_repo)
131 qtutils.connect_button(self.clone_button, self.clone_repo)
132 qtutils.connect_button(self.new_button, self.new_repo)
133 qtutils.connect_button(self.close_button, self.reject)
135 # pylint: disable=no-member
136 self.tab_bar.currentChanged.connect(self.tab_changed)
138 self.init_state(settings, self.resize_widget)
139 self.setFocusProxy(self.bookmarks)
140 self.bookmarks.setFocus()
142 # Update the list mode
143 list_mode = context.cfg.get('cola.startupmode', default='folder')
144 self.list_mode = list_mode
145 if list_mode == 'list':
146 self.tab_bar.setCurrentIndex(1)
148 def tab_changed(self, idx):
149 bookmarks = self.bookmarks
150 if idx == ICON_MODE:
151 mode = QtWidgets.QListView.IconMode
152 icon_size = make_size(defs.medium_icon)
153 grid_size = make_size(defs.large_icon)
154 list_mode = 'folder'
155 view_mode = ICON_MODE
156 rename_enabled = True
157 else:
158 mode = QtWidgets.QListView.ListMode
159 icon_size = make_size(defs.default_icon)
160 grid_size = QtCore.QSize()
161 list_mode = 'list'
162 view_mode = LIST_MODE
163 rename_enabled = False
165 self.bookmarks.set_rename_enabled(rename_enabled)
166 self.bookmarks.set_view_mode(view_mode)
168 bookmarks.setViewMode(mode)
169 bookmarks.setIconSize(icon_size)
170 bookmarks.setGridSize(grid_size)
172 new_items = []
173 builder = BuildItem(self.context)
174 for item in self.items:
175 if isinstance(item, PromptWidgetItem):
176 item = builder.get(item.path, item.name, view_mode, item.is_bookmark)
177 new_items.append(item)
179 self.set_model(new_items)
181 if list_mode != self.list_mode:
182 self.list_mode = list_mode
183 self.context.cfg.set_user('cola.startupmode', list_mode)
185 def resize_widget(self):
186 width, height = qtutils.desktop_size()
187 self.setGeometry(
188 width // 4,
189 height // 4,
190 width // 2,
191 height // 2,
194 def find_git_repo(self):
196 Return a path to a git repository
198 This is the entry point for external callers.
199 This method finds a git repository by allowing the
200 user to browse to one on the filesystem or by creating
201 a new one with git-clone.
204 self.show()
205 self.raise_()
206 if self.exec_() == QtWidgets.QDialog.Accepted:
207 return self.repodir
208 return None
210 def open_repo(self):
211 self.repodir = self.get_selected_bookmark()
212 if not self.repodir:
213 self.repodir = qtutils.opendir_dialog(
214 N_('Open Git Repository'), core.getcwd()
216 if self.repodir:
217 self.accept()
219 def clone_repo(self):
220 context = self.context
221 progress = standard.progress('', '', self)
222 clone.clone_repo(context, True, progress, self.clone_repo_done, False)
224 def clone_repo_done(self, task):
225 if task.cmd and task.cmd.status == 0:
226 self.repodir = task.destdir
227 self.accept()
228 else:
229 clone.task_finished(task)
231 def new_repo(self):
232 context = self.context
233 repodir = guicmds.new_repo(context)
234 if repodir:
235 self.repodir = repodir
236 self.accept()
238 def open_selected_bookmark(self):
239 selected = self.bookmarks.selectedIndexes()
240 if selected:
241 self.open_bookmark(selected[0])
243 def open_bookmark(self, index):
244 if index.row() == 0:
245 self.open_repo()
246 else:
247 self.repodir = self.bookmarks_model.data(index, Qt.UserRole)
248 if not self.repodir:
249 return
250 if not core.exists(self.repodir):
251 self.handle_broken_repo(index)
252 return
253 self.accept()
255 def handle_broken_repo(self, index):
256 settings = self.context.settings
257 all_repos = get_all_repos(self.context, settings)
259 repodir = self.bookmarks_model.data(index, Qt.UserRole)
260 repo = next(repo for repo, is_bookmark in all_repos if repo['path'] == repodir)
261 title = N_('Repository Not Found')
262 text = N_('%s could not be opened. Remove from bookmarks?') % repo['path']
263 logo = icons.from_style(QtWidgets.QStyle.SP_MessageBoxWarning)
264 if standard.question(title, text, N_('Remove'), logo=logo):
265 self.context.settings.remove_bookmark(repo['path'], repo['name'])
266 self.context.settings.remove_recent(repo['path'])
267 self.context.settings.save()
269 item = self.bookmarks_model.item(index.row())
270 self.items.remove(item)
271 self.bookmarks_model.removeRow(index.row())
273 def get_selected_bookmark(self):
274 selected = self.bookmarks.selectedIndexes()
275 if selected and selected[0].row() != 0:
276 return self.bookmarks_model.data(selected[0], Qt.UserRole)
277 return None
279 def set_model(self, items):
280 bookmarks_model = self.bookmarks_model
281 self.items = new_items = []
282 bookmarks_model.clear()
284 item = QtGui.QStandardItem(N_('Browse...'))
285 item.setEditable(False)
286 item.setIcon(icons.open_directory())
287 bookmarks_model.appendRow(item)
289 for item in items:
290 bookmarks_model.appendRow(item)
291 new_items.append(item)
294 def get_all_repos(context, settings):
295 """Return a sorted list of bookmarks and recent repositories"""
296 bookmarks = settings.bookmarks
297 recent = settings.recent
298 all_repos = [(repo, True) for repo in bookmarks] + [
299 (repo, False) for repo in recent
301 if prefs.sort_bookmarks(context):
302 all_repos.sort(key=lambda details: details[0]['path'].lower())
303 return all_repos
306 class BookmarksListView(QtWidgets.QListView):
308 List view class implementation of QWidgets.QListView for bookmarks and recent repos.
309 Almost methods is comes from `cola/widgets/bookmarks.py`.
312 def __init__(self, context, model, open_selected_repo, set_model, parent=None):
313 super().__init__(parent)
315 self.current_mode = ICON_MODE
316 self.context = context
317 self.open_selected_repo = open_selected_repo
318 self.set_model = set_model
320 self.setEditTriggers(self.SelectedClicked)
322 self.activated.connect(self.open_selected_repo)
324 self.setModel(model)
325 self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
326 self.setViewMode(QtWidgets.QListView.IconMode)
327 self.setResizeMode(QtWidgets.QListView.Adjust)
328 self.setGridSize(make_size(defs.large_icon))
329 self.setIconSize(make_size(defs.medium_icon))
330 self.setDragEnabled(False)
331 self.setWordWrap(True)
333 # Context Menu
334 self.open_action = qtutils.add_action(
335 self, N_('Open'), self.open_selected_repo, hotkeys.OPEN
338 self.accept_action = qtutils.add_action(
339 self, N_('Accept'), self.accept_repo, *hotkeys.ACCEPT
342 self.open_new_action = qtutils.add_action(
343 self, N_('Open in New Window'), self.open_new_repo, hotkeys.NEW
346 self.set_default_repo_action = qtutils.add_action(
347 self, N_('Set Default Repository'), self.set_default_repo
350 self.clear_default_repo_action = qtutils.add_action(
351 self, N_('Clear Default Repository'), self.clear_default_repo
354 self.rename_repo_action = qtutils.add_action(
355 self, N_('Rename Repository'), self.rename_repo
358 self.open_default_action = qtutils.add_action(
359 self, cmds.OpenDefaultApp.name(), self.open_default, hotkeys.PRIMARY_ACTION
362 self.launch_editor_action = qtutils.add_action(
363 self, cmds.Edit.name(), self.launch_editor, hotkeys.EDIT
366 self.launch_terminal_action = qtutils.add_action(
367 self, cmds.LaunchTerminal.name(), self.launch_terminal, hotkeys.TERMINAL
370 self.copy_action = qtutils.add_action(self, N_('Copy'), self.copy, hotkeys.COPY)
372 self.delete_action = qtutils.add_action(self, N_('Delete'), self.delete_item)
374 self.remove_missing_action = qtutils.add_action(
375 self, N_('Prune Missing Entries'), self.remove_missing
377 self.remove_missing_action.setToolTip(
378 N_('Remove stale entries for repositories that no longer exist')
381 # pylint: disable=no-member
382 self.model().itemChanged.connect(self.item_changed)
384 self.action_group = utils.Group(
385 self.open_action,
386 self.open_new_action,
387 self.copy_action,
388 self.launch_editor_action,
389 self.launch_terminal_action,
390 self.open_default_action,
391 self.rename_repo_action,
392 self.delete_action,
394 self.action_group.setEnabled(True)
395 self.set_default_repo_action.setEnabled(True)
396 self.clear_default_repo_action.setEnabled(True)
398 def set_rename_enabled(self, is_enabled):
399 self.rename_repo_action.setEnabled(is_enabled)
401 def set_view_mode(self, view_mode):
402 self.current_mode = view_mode
404 def selected_item(self):
405 index = self.currentIndex()
406 return self.model().itemFromIndex(index)
408 def refresh(self):
409 self.model().layoutChanged.emit()
410 context = self.context
411 settings = context.settings
412 builder = BuildItem(context)
413 normalize = display.normalize_path
414 items = []
415 added = set()
417 all_repos = get_all_repos(self.context, settings)
418 for repo, is_bookmark in all_repos:
419 path = normalize(repo['path'])
420 name = normalize(repo['name'])
421 if path in added:
422 continue
423 added.add(path)
425 item = builder.get(path, name, self.current_mode, is_bookmark)
426 items.append(item)
428 self.set_model(items)
430 def contextMenuEvent(self, event):
431 """Configures prompt's context menu."""
432 item = self.selected_item()
434 if isinstance(item, PromptWidgetItem):
435 menu = qtutils.create_menu(N_('Actions'), self)
436 menu.addAction(self.open_action)
437 menu.addAction(self.open_new_action)
438 menu.addAction(self.open_default_action)
439 menu.addSeparator()
440 menu.addAction(self.copy_action)
441 menu.addAction(self.launch_editor_action)
442 menu.addAction(self.launch_terminal_action)
443 menu.addSeparator()
444 if item and item.is_default:
445 menu.addAction(self.clear_default_repo_action)
446 else:
447 menu.addAction(self.set_default_repo_action)
448 menu.addAction(self.rename_repo_action)
449 menu.addSeparator()
450 menu.addAction(self.delete_action)
451 menu.addAction(self.remove_missing_action)
452 menu.exec_(self.mapToGlobal(event.pos()))
454 def item_changed(self, item):
455 self.rename_entry(item, item.text())
457 def rename_entry(self, item, new_name):
458 settings = self.context.settings
459 if item.is_bookmark:
460 rename = settings.rename_bookmark
461 else:
462 rename = settings.rename_recent
464 if rename(item.path, item.name, new_name):
465 settings.save()
466 item.name = new_name
467 else:
468 item.setText(item.name)
470 def apply_func(self, func, *args, **kwargs):
471 item = self.selected_item()
472 if item:
473 func(item, *args, **kwargs)
475 def copy(self):
476 self.apply_func(lambda item: qtutils.set_clipboard(item.path))
478 def open_default(self):
479 context = self.context
480 self.apply_func(lambda item: cmds.do(cmds.OpenDefaultApp, context, [item.path]))
482 def set_default_repo(self):
483 self.apply_func(self.set_default_item)
485 def set_default_item(self, item):
486 context = self.context
487 cmds.do(cmds.SetDefaultRepo, context, item.path)
488 self.refresh()
490 def clear_default_repo(self):
491 self.apply_func(self.clear_default_item)
493 def clear_default_item(self, _item):
494 context = self.context
495 cmds.do(cmds.SetDefaultRepo, context, None)
496 self.refresh()
498 def rename_repo(self):
499 index = self.currentIndex()
500 self.edit(index)
502 def accept_repo(self):
503 self.apply_func(self.accept_item)
505 def accept_item(self, _item):
506 if self.state() & self.EditingState:
507 current_index = self.currentIndex()
508 widget = self.indexWidget(current_index)
509 if widget:
510 self.commitData(widget)
511 self.closePersistentEditor(current_index)
512 self.refresh()
513 else:
514 self.open_selected_repo()
516 def open_new_repo(self):
517 context = self.context
518 self.apply_func(lambda item: cmds.do(cmds.OpenNewRepo, context, item.path))
520 def launch_editor(self):
521 context = self.context
522 self.apply_func(lambda item: cmds.do(cmds.Edit, context, [item.path]))
524 def launch_terminal(self):
525 context = self.context
526 self.apply_func(lambda item: cmds.do(cmds.LaunchTerminal, context, item.path))
528 def delete_item(self):
529 """Remove the selected repo item
531 If the item comes from bookmarks (item.is_bookmark) then delete the item
532 from the Bookmarks list, otherwise delete it from the Recents list.
534 item = self.selected_item()
535 if not item:
536 return
538 if item.is_bookmark:
539 cmd = cmds.RemoveBookmark
540 else:
541 cmd = cmds.RemoveRecent
542 context = self.context
543 ok, _, _, _ = cmds.do(cmd, context, item.path, item.name, icon=icons.discard())
544 if ok:
545 self.refresh()
547 def remove_missing(self):
548 """Remove missing entries from the favorites/recent file list"""
549 settings = self.context.settings
550 settings.remove_missing_bookmarks()
551 settings.remove_missing_recent()
552 self.refresh()
555 class BuildItem:
556 def __init__(self, context):
557 self.star_icon = icons.star()
558 self.folder_icon = icons.folder()
559 cfg = context.cfg
560 self.default_repo = cfg.get('cola.defaultrepo')
562 def get(self, path, name, mode, is_bookmark):
563 is_default = self.default_repo == path
564 if is_default:
565 icon = self.star_icon
566 else:
567 icon = self.folder_icon
568 return PromptWidgetItem(path, name, mode, icon, is_default, is_bookmark)
571 class PromptWidgetItem(QtGui.QStandardItem):
572 def __init__(self, path, name, mode, icon, is_default, is_bookmark):
573 QtGui.QStandardItem.__init__(self, icon, name)
574 self.path = path
575 self.name = name
576 self.mode = mode
577 self.is_default = is_default
578 self.is_bookmark = is_bookmark
579 editable = mode == ICON_MODE
581 if self.mode == ICON_MODE:
582 item_text = self.name
583 else:
584 item_text = self.path
586 user_role = Qt.UserRole
587 self.setEditable(editable)
588 self.setData(path, user_role)
589 self.setIcon(icon)
590 self.setText(item_text)
591 self.setToolTip(path)
594 def make_size(size):
595 """Construct a QSize from a single value"""
596 return QtCore.QSize(size, size)