about: update authors list
[git-cola.git] / cola / widgets / startup.py
blob1427c0c2a92cf80d551601b7cc9c3d8bab946600
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)
134 self.tab_bar.currentChanged.connect(self.tab_changed)
136 self.init_state(settings, self.resize_widget)
137 self.setFocusProxy(self.bookmarks)
138 self.bookmarks.setFocus()
140 # Update the list mode
141 list_mode = context.cfg.get('cola.startupmode', default='folder')
142 self.list_mode = list_mode
143 if list_mode == 'list':
144 self.tab_bar.setCurrentIndex(1)
146 def tab_changed(self, idx):
147 bookmarks = self.bookmarks
148 if idx == ICON_MODE:
149 mode = QtWidgets.QListView.IconMode
150 icon_size = make_size(defs.medium_icon)
151 grid_size = make_size(defs.large_icon)
152 list_mode = 'folder'
153 view_mode = ICON_MODE
154 rename_enabled = True
155 else:
156 mode = QtWidgets.QListView.ListMode
157 icon_size = make_size(defs.default_icon)
158 grid_size = QtCore.QSize()
159 list_mode = 'list'
160 view_mode = LIST_MODE
161 rename_enabled = False
163 self.bookmarks.set_rename_enabled(rename_enabled)
164 self.bookmarks.set_view_mode(view_mode)
166 bookmarks.setViewMode(mode)
167 bookmarks.setIconSize(icon_size)
168 bookmarks.setGridSize(grid_size)
170 new_items = []
171 builder = BuildItem(self.context)
172 for item in self.items:
173 if isinstance(item, PromptWidgetItem):
174 item = builder.get(item.path, item.name, view_mode, item.is_bookmark)
175 new_items.append(item)
177 self.set_model(new_items)
179 if list_mode != self.list_mode:
180 self.list_mode = list_mode
181 self.context.cfg.set_user('cola.startupmode', list_mode)
183 def resize_widget(self):
184 width, height = qtutils.desktop_size()
185 self.setGeometry(
186 width // 4,
187 height // 4,
188 width // 2,
189 height // 2,
192 def find_git_repo(self):
194 Return a path to a git repository
196 This is the entry point for external callers.
197 This method finds a git repository by allowing the
198 user to browse to one on the filesystem or by creating
199 a new one with git-clone.
202 self.show()
203 self.raise_()
204 if self.exec_() == QtWidgets.QDialog.Accepted:
205 return self.repodir
206 return None
208 def open_repo(self):
209 self.repodir = self.get_selected_bookmark()
210 if not self.repodir:
211 self.repodir = qtutils.opendir_dialog(
212 N_('Open Git Repository'), core.getcwd()
214 if self.repodir:
215 self.accept()
217 def clone_repo(self):
218 context = self.context
219 progress = standard.progress('', '', self)
220 clone.clone_repo(context, True, progress, self.clone_repo_done, False)
222 def clone_repo_done(self, task):
223 if task.cmd and task.cmd.status == 0:
224 self.repodir = task.destdir
225 self.accept()
226 else:
227 clone.task_finished(task)
229 def new_repo(self):
230 context = self.context
231 repodir = guicmds.new_repo(context)
232 if repodir:
233 self.repodir = repodir
234 self.accept()
236 def open_selected_bookmark(self):
237 selected = self.bookmarks.selectedIndexes()
238 if selected:
239 self.open_bookmark(selected[0])
241 def open_bookmark(self, index):
242 if index.row() == 0:
243 self.open_repo()
244 else:
245 self.repodir = self.bookmarks_model.data(index, Qt.UserRole)
246 if not self.repodir:
247 return
248 if not core.exists(self.repodir):
249 self.handle_broken_repo(index)
250 return
251 self.accept()
253 def handle_broken_repo(self, index):
254 settings = self.context.settings
255 all_repos = get_all_repos(self.context, settings)
257 repodir = self.bookmarks_model.data(index, Qt.UserRole)
258 repo = next(repo for repo, is_bookmark in all_repos if repo['path'] == repodir)
259 title = N_('Repository Not Found')
260 text = N_('%s could not be opened. Remove from bookmarks?') % repo['path']
261 logo = icons.from_style(QtWidgets.QStyle.SP_MessageBoxWarning)
262 if standard.question(title, text, N_('Remove'), logo=logo):
263 self.context.settings.remove_bookmark(repo['path'], repo['name'])
264 self.context.settings.remove_recent(repo['path'])
265 self.context.settings.save()
267 item = self.bookmarks_model.item(index.row())
268 self.items.remove(item)
269 self.bookmarks_model.removeRow(index.row())
271 def get_selected_bookmark(self):
272 selected = self.bookmarks.selectedIndexes()
273 if selected and selected[0].row() != 0:
274 return self.bookmarks_model.data(selected[0], Qt.UserRole)
275 return None
277 def set_model(self, items):
278 bookmarks_model = self.bookmarks_model
279 self.items = new_items = []
280 bookmarks_model.clear()
282 item = QtGui.QStandardItem(N_('Browse...'))
283 item.setEditable(False)
284 item.setIcon(icons.open_directory())
285 bookmarks_model.appendRow(item)
287 for item in items:
288 bookmarks_model.appendRow(item)
289 new_items.append(item)
292 def get_all_repos(context, settings):
293 """Return a sorted list of bookmarks and recent repositories"""
294 bookmarks = settings.bookmarks
295 recent = settings.recent
296 all_repos = [(repo, True) for repo in bookmarks] + [
297 (repo, False) for repo in recent
299 if prefs.sort_bookmarks(context):
300 all_repos.sort(key=lambda details: details[0]['path'].lower())
301 return all_repos
304 class BookmarksListView(QtWidgets.QListView):
306 List view class implementation of QWidgets.QListView for bookmarks and recent repos.
307 Almost methods is comes from `cola/widgets/bookmarks.py`.
310 def __init__(self, context, model, open_selected_repo, set_model, parent=None):
311 super().__init__(parent)
313 self.current_mode = ICON_MODE
314 self.context = context
315 self.open_selected_repo = open_selected_repo
316 self.set_model = set_model
318 self.setEditTriggers(self.SelectedClicked)
320 self.activated.connect(self.open_selected_repo)
322 self.setModel(model)
323 self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
324 self.setViewMode(QtWidgets.QListView.IconMode)
325 self.setResizeMode(QtWidgets.QListView.Adjust)
326 self.setGridSize(make_size(defs.large_icon))
327 self.setIconSize(make_size(defs.medium_icon))
328 self.setDragEnabled(False)
329 self.setWordWrap(True)
331 # Context Menu
332 self.open_action = qtutils.add_action(
333 self, N_('Open'), self.open_selected_repo, hotkeys.OPEN
336 self.accept_action = qtutils.add_action(
337 self, N_('Accept'), self.accept_repo, *hotkeys.ACCEPT
340 self.open_new_action = qtutils.add_action(
341 self, N_('Open in New Window'), self.open_new_repo, hotkeys.NEW
344 self.set_default_repo_action = qtutils.add_action(
345 self, N_('Set Default Repository'), self.set_default_repo
348 self.clear_default_repo_action = qtutils.add_action(
349 self, N_('Clear Default Repository'), self.clear_default_repo
352 self.rename_repo_action = qtutils.add_action(
353 self, N_('Rename Repository'), self.rename_repo
356 self.open_default_action = qtutils.add_action(
357 self, cmds.OpenDefaultApp.name(), self.open_default, hotkeys.PRIMARY_ACTION
360 self.launch_editor_action = qtutils.add_action(
361 self, cmds.Edit.name(), self.launch_editor, hotkeys.EDIT
364 self.launch_terminal_action = qtutils.add_action(
365 self, cmds.LaunchTerminal.name(), self.launch_terminal, hotkeys.TERMINAL
368 self.copy_action = qtutils.add_action(self, N_('Copy'), self.copy, hotkeys.COPY)
370 self.delete_action = qtutils.add_action(self, N_('Delete'), self.delete_item)
372 self.remove_missing_action = qtutils.add_action(
373 self, N_('Prune Missing Entries'), self.remove_missing
375 self.remove_missing_action.setToolTip(
376 N_('Remove stale entries for repositories that no longer exist')
379 self.model().itemChanged.connect(self.item_changed)
381 self.action_group = utils.Group(
382 self.open_action,
383 self.open_new_action,
384 self.copy_action,
385 self.launch_editor_action,
386 self.launch_terminal_action,
387 self.open_default_action,
388 self.rename_repo_action,
389 self.delete_action,
391 self.action_group.setEnabled(True)
392 self.set_default_repo_action.setEnabled(True)
393 self.clear_default_repo_action.setEnabled(True)
395 def set_rename_enabled(self, is_enabled):
396 self.rename_repo_action.setEnabled(is_enabled)
398 def set_view_mode(self, view_mode):
399 self.current_mode = view_mode
401 def selected_item(self):
402 index = self.currentIndex()
403 return self.model().itemFromIndex(index)
405 def refresh(self):
406 self.model().layoutChanged.emit()
407 context = self.context
408 settings = context.settings
409 builder = BuildItem(context)
410 normalize = display.normalize_path
411 items = []
412 added = set()
414 all_repos = get_all_repos(self.context, settings)
415 for repo, is_bookmark in all_repos:
416 path = normalize(repo['path'])
417 name = normalize(repo['name'])
418 if path in added:
419 continue
420 added.add(path)
422 item = builder.get(path, name, self.current_mode, is_bookmark)
423 items.append(item)
425 self.set_model(items)
427 def contextMenuEvent(self, event):
428 """Configures prompt's context menu."""
429 item = self.selected_item()
431 if isinstance(item, PromptWidgetItem):
432 menu = qtutils.create_menu(N_('Actions'), self)
433 menu.addAction(self.open_action)
434 menu.addAction(self.open_new_action)
435 menu.addAction(self.open_default_action)
436 menu.addSeparator()
437 menu.addAction(self.copy_action)
438 menu.addAction(self.launch_editor_action)
439 menu.addAction(self.launch_terminal_action)
440 menu.addSeparator()
441 if item and item.is_default:
442 menu.addAction(self.clear_default_repo_action)
443 else:
444 menu.addAction(self.set_default_repo_action)
445 menu.addAction(self.rename_repo_action)
446 menu.addSeparator()
447 menu.addAction(self.delete_action)
448 menu.addAction(self.remove_missing_action)
449 menu.exec_(self.mapToGlobal(event.pos()))
451 def item_changed(self, item):
452 self.rename_entry(item, item.text())
454 def rename_entry(self, item, new_name):
455 settings = self.context.settings
456 if item.is_bookmark:
457 rename = settings.rename_bookmark
458 else:
459 rename = settings.rename_recent
461 if rename(item.path, item.name, new_name):
462 settings.save()
463 item.name = new_name
464 else:
465 item.setText(item.name)
467 def apply_func(self, func, *args, **kwargs):
468 item = self.selected_item()
469 if item:
470 func(item, *args, **kwargs)
472 def copy(self):
473 self.apply_func(lambda item: qtutils.set_clipboard(item.path))
475 def open_default(self):
476 context = self.context
477 self.apply_func(lambda item: cmds.do(cmds.OpenDefaultApp, context, [item.path]))
479 def set_default_repo(self):
480 self.apply_func(self.set_default_item)
482 def set_default_item(self, item):
483 context = self.context
484 cmds.do(cmds.SetDefaultRepo, context, item.path)
485 self.refresh()
487 def clear_default_repo(self):
488 self.apply_func(self.clear_default_item)
490 def clear_default_item(self, _item):
491 context = self.context
492 cmds.do(cmds.SetDefaultRepo, context, None)
493 self.refresh()
495 def rename_repo(self):
496 index = self.currentIndex()
497 self.edit(index)
499 def accept_repo(self):
500 self.apply_func(self.accept_item)
502 def accept_item(self, _item):
503 if self.state() & self.EditingState:
504 current_index = self.currentIndex()
505 widget = self.indexWidget(current_index)
506 if widget:
507 self.commitData(widget)
508 self.closePersistentEditor(current_index)
509 self.refresh()
510 else:
511 self.open_selected_repo()
513 def open_new_repo(self):
514 context = self.context
515 self.apply_func(lambda item: cmds.do(cmds.OpenNewRepo, context, item.path))
517 def launch_editor(self):
518 context = self.context
519 self.apply_func(lambda item: cmds.do(cmds.Edit, context, [item.path]))
521 def launch_terminal(self):
522 context = self.context
523 self.apply_func(lambda item: cmds.do(cmds.LaunchTerminal, context, item.path))
525 def delete_item(self):
526 """Remove the selected repo item
528 If the item comes from bookmarks (item.is_bookmark) then delete the item
529 from the Bookmarks list, otherwise delete it from the Recents list.
531 item = self.selected_item()
532 if not item:
533 return
535 if item.is_bookmark:
536 cmd = cmds.RemoveBookmark
537 else:
538 cmd = cmds.RemoveRecent
539 context = self.context
540 ok, _, _, _ = cmds.do(cmd, context, item.path, item.name, icon=icons.discard())
541 if ok:
542 self.refresh()
544 def remove_missing(self):
545 """Remove missing entries from the favorites/recent file list"""
546 settings = self.context.settings
547 settings.remove_missing_bookmarks()
548 settings.remove_missing_recent()
549 self.refresh()
552 class BuildItem:
553 def __init__(self, context):
554 self.star_icon = icons.star()
555 self.folder_icon = icons.folder()
556 cfg = context.cfg
557 self.default_repo = cfg.get('cola.defaultrepo')
559 def get(self, path, name, mode, is_bookmark):
560 is_default = self.default_repo == path
561 if is_default:
562 icon = self.star_icon
563 else:
564 icon = self.folder_icon
565 return PromptWidgetItem(path, name, mode, icon, is_default, is_bookmark)
568 class PromptWidgetItem(QtGui.QStandardItem):
569 def __init__(self, path, name, mode, icon, is_default, is_bookmark):
570 QtGui.QStandardItem.__init__(self, icon, name)
571 self.path = path
572 self.name = name
573 self.mode = mode
574 self.is_default = is_default
575 self.is_bookmark = is_bookmark
576 editable = mode == ICON_MODE
578 if self.mode == ICON_MODE:
579 item_text = self.name
580 else:
581 item_text = self.path
583 user_role = Qt.UserRole
584 self.setEditable(editable)
585 self.setData(path, user_role)
586 self.setIcon(icon)
587 self.setText(item_text)
588 self.setToolTip(path)
591 def make_size(size):
592 """Construct a QSize from a single value"""
593 return QtCore.QSize(size, size)