git-cola v4.3.1
[git-cola.git] / cola / widgets / startup.py
blob546aff10a15e8f7bb16988134ac1039bd9c78d7b
1 """The startup dialog is presented when no repositories can be found at startup"""
2 from __future__ import absolute_import, division, print_function, unicode_literals
4 from qtpy.QtCore import Qt
5 from qtpy import QtCore
6 from qtpy import QtGui
7 from qtpy import QtWidgets
9 from ..i18n import N_
10 from ..models import prefs
11 from .. import cmds
12 from .. import core
13 from .. import display
14 from .. import guicmds
15 from .. import hotkeys
16 from .. import icons
17 from .. import qtutils
18 from .. import utils
19 from .. import version
20 from . import clone
21 from . import defs
22 from . import standard
25 ICON_MODE = 0
26 LIST_MODE = 1
29 class StartupDialog(standard.Dialog):
30 """Provides a GUI to Open or Clone a git repository."""
32 def __init__(self, context, parent=None):
33 standard.Dialog.__init__(self, parent)
34 self.context = context
35 self.setWindowTitle(N_('git-cola'))
37 # Top-most large icon
38 logo_pixmap = icons.cola().pixmap(defs.huge_icon, defs.huge_icon)
40 self.logo_label = QtWidgets.QLabel()
41 self.logo_label.setPixmap(logo_pixmap)
42 self.logo_label.setAlignment(Qt.AlignCenter)
44 self.logo_text_label = qtutils.label(text=version.cola_version())
45 self.logo_text_label.setAlignment(Qt.AlignCenter)
47 self.repodir = None
48 if context.runtask:
49 self.runtask = context.runtask
50 else:
51 self.runtask = context.runtask = qtutils.RunTask(parent=self)
53 self.new_button = qtutils.create_button(text=N_('New...'), icon=icons.new())
54 self.open_button = qtutils.create_button(
55 text=N_('Open Git Repository'), icon=icons.folder()
57 self.clone_button = qtutils.create_button(
58 text=N_('Clone...'), icon=icons.cola()
60 self.close_button = qtutils.close_button()
62 self.bookmarks_model = bookmarks_model = QtGui.QStandardItemModel()
63 self.items = items = []
65 item = QtGui.QStandardItem(N_('Browse...'))
66 item.setEditable(False)
67 item.setIcon(icons.open_directory())
68 bookmarks_model.appendRow(item)
70 # The tab bar allows choosing between Folder and List mode
71 self.tab_bar = QtWidgets.QTabBar()
72 self.tab_bar.setMovable(False)
73 self.tab_bar.addTab(icons.directory(), N_('Folder'))
74 self.tab_bar.addTab(icons.three_bars(), N_('List'))
76 # Bookmarks/"Favorites" and Recent are lists of {name,path: str}
77 normalize = display.normalize_path
78 settings = context.settings
79 all_repos = get_all_repos(self.context, settings)
81 added = set()
82 builder = BuildItem(self.context)
83 default_view_mode = ICON_MODE
84 for repo, is_bookmark in all_repos:
85 path = normalize(repo['path'])
86 name = normalize(repo['name'])
87 if path in added:
88 continue
89 added.add(path)
91 item = builder.get(path, name, default_view_mode, is_bookmark)
92 bookmarks_model.appendRow(item)
93 items.append(item)
95 self.bookmarks = BookmarksListView(
96 context,
97 bookmarks_model,
98 self.open_selected_bookmark,
99 self.set_model,
102 self.tab_layout = qtutils.vbox(
103 defs.no_margin, defs.no_spacing, self.tab_bar, self.bookmarks
106 self.logo_layout = qtutils.vbox(
107 defs.no_margin,
108 defs.spacing,
109 self.logo_label,
110 self.logo_text_label,
111 defs.button_spacing,
112 qtutils.STRETCH,
115 self.button_layout = qtutils.hbox(
116 defs.no_margin,
117 defs.spacing,
118 self.open_button,
119 self.clone_button,
120 self.new_button,
121 qtutils.STRETCH,
122 self.close_button,
125 self.main_layout = qtutils.grid(defs.margin, defs.spacing)
126 self.main_layout.addItem(self.logo_layout, 1, 1)
127 self.main_layout.addItem(self.tab_layout, 1, 2)
128 self.main_layout.addItem(self.button_layout, 2, 1, columnSpan=2)
129 self.setLayout(self.main_layout)
131 qtutils.connect_button(self.open_button, self.open_repo)
132 qtutils.connect_button(self.clone_button, self.clone_repo)
133 qtutils.connect_button(self.new_button, self.new_repo)
134 qtutils.connect_button(self.close_button, self.reject)
136 # pylint: disable=no-member
137 self.tab_bar.currentChanged.connect(self.tab_changed)
139 self.init_state(settings, self.resize_widget)
140 self.setFocusProxy(self.bookmarks)
141 self.bookmarks.setFocus()
143 # Update the list mode
144 list_mode = context.cfg.get('cola.startupmode', default='folder')
145 self.list_mode = list_mode
146 if list_mode == 'list':
147 self.tab_bar.setCurrentIndex(1)
149 def tab_changed(self, idx):
150 bookmarks = self.bookmarks
151 if idx == ICON_MODE:
152 mode = QtWidgets.QListView.IconMode
153 icon_size = make_size(defs.medium_icon)
154 grid_size = make_size(defs.large_icon)
155 list_mode = 'folder'
156 view_mode = ICON_MODE
157 rename_enabled = True
158 else:
159 mode = QtWidgets.QListView.ListMode
160 icon_size = make_size(defs.default_icon)
161 grid_size = QtCore.QSize()
162 list_mode = 'list'
163 view_mode = LIST_MODE
164 rename_enabled = False
166 self.bookmarks.set_rename_enabled(rename_enabled)
167 self.bookmarks.set_view_mode(view_mode)
169 bookmarks.setViewMode(mode)
170 bookmarks.setIconSize(icon_size)
171 bookmarks.setGridSize(grid_size)
173 new_items = []
174 builder = BuildItem(self.context)
175 for item in self.items:
176 if isinstance(item, PromptWidgetItem):
177 item = builder.get(item.path, item.name, view_mode, item.is_bookmark)
178 new_items.append(item)
180 self.set_model(new_items)
182 if list_mode != self.list_mode:
183 self.list_mode = list_mode
184 self.context.cfg.set_user('cola.startupmode', list_mode)
186 def resize_widget(self):
187 width, height = qtutils.desktop_size()
188 self.setGeometry(
189 width // 4,
190 height // 4,
191 width // 2,
192 height // 2,
195 def find_git_repo(self):
197 Return a path to a git repository
199 This is the entry point for external callers.
200 This method finds a git repository by allowing the
201 user to browse to one on the filesystem or by creating
202 a new one with git-clone.
205 self.show()
206 self.raise_()
207 if self.exec_() == QtWidgets.QDialog.Accepted:
208 return self.repodir
209 return None
211 def open_repo(self):
212 self.repodir = self.get_selected_bookmark()
213 if not self.repodir:
214 self.repodir = qtutils.opendir_dialog(
215 N_('Open Git Repository'), core.getcwd()
217 if self.repodir:
218 self.accept()
220 def clone_repo(self):
221 context = self.context
222 progress = standard.progress('', '', self)
223 clone.clone_repo(context, True, progress, self.clone_repo_done, False)
225 def clone_repo_done(self, task):
226 if task.cmd and task.cmd.status == 0:
227 self.repodir = task.destdir
228 self.accept()
229 else:
230 clone.task_finished(task)
232 def new_repo(self):
233 context = self.context
234 repodir = guicmds.new_repo(context)
235 if repodir:
236 self.repodir = repodir
237 self.accept()
239 def open_selected_bookmark(self):
240 selected = self.bookmarks.selectedIndexes()
241 if selected:
242 self.open_bookmark(selected[0])
244 def open_bookmark(self, index):
245 if index.row() == 0:
246 self.open_repo()
247 else:
248 self.repodir = self.bookmarks_model.data(index, Qt.UserRole)
249 if not self.repodir:
250 return
251 if not core.exists(self.repodir):
252 self.handle_broken_repo(index)
253 return
254 self.accept()
256 def handle_broken_repo(self, index):
257 settings = self.context.settings
258 all_repos = get_all_repos(self.context, settings)
260 repodir = self.bookmarks_model.data(index, Qt.UserRole)
261 repo = next(repo for repo, is_bookmark in all_repos if repo['path'] == repodir)
262 title = N_('Repository Not Found')
263 text = N_('%s could not be opened. Remove from bookmarks?') % repo['path']
264 logo = icons.from_style(QtWidgets.QStyle.SP_MessageBoxWarning)
265 if standard.question(title, text, N_('Remove'), logo=logo):
266 self.context.settings.remove_bookmark(repo['path'], repo['name'])
267 self.context.settings.remove_recent(repo['path'])
268 self.context.settings.save()
270 item = self.bookmarks_model.item(index.row())
271 self.items.remove(item)
272 self.bookmarks_model.removeRow(index.row())
274 def get_selected_bookmark(self):
275 selected = self.bookmarks.selectedIndexes()
276 if selected and selected[0].row() != 0:
277 return self.bookmarks_model.data(selected[0], Qt.UserRole)
278 return None
280 def set_model(self, items):
281 bookmarks_model = self.bookmarks_model
282 self.items = new_items = []
283 bookmarks_model.clear()
285 item = QtGui.QStandardItem(N_('Browse...'))
286 item.setEditable(False)
287 item.setIcon(icons.open_directory())
288 bookmarks_model.appendRow(item)
290 for item in items:
291 bookmarks_model.appendRow(item)
292 new_items.append(item)
295 def get_all_repos(context, settings):
296 """Return a sorted list of bookmarks and recent repositorties"""
297 bookmarks = settings.bookmarks
298 recent = settings.recent
299 all_repos = [(repo, True) for repo in bookmarks] + [
300 (repo, False) for repo in recent
302 if prefs.sort_bookmarks(context):
303 all_repos.sort(key=lambda details: details[0]['path'].lower())
304 return all_repos
307 class BookmarksListView(QtWidgets.QListView):
309 List view class implementation of QWidgets.QListView for bookmarks and recent repos.
310 Almost methods is comes from `cola/widgets/bookmarks.py`.
313 def __init__(self, context, model, open_selected_repo, set_model, parent=None):
314 super(BookmarksListView, self).__init__(parent)
316 self.current_mode = ICON_MODE
317 self.context = context
318 self.open_selected_repo = open_selected_repo
319 self.set_model = set_model
321 self.setEditTriggers(self.SelectedClicked)
323 self.activated.connect(self.open_selected_repo)
325 self.setModel(model)
326 self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
327 self.setViewMode(QtWidgets.QListView.IconMode)
328 self.setResizeMode(QtWidgets.QListView.Adjust)
329 self.setGridSize(make_size(defs.large_icon))
330 self.setIconSize(make_size(defs.medium_icon))
331 self.setDragEnabled(False)
332 self.setWordWrap(True)
334 # Context Menu
335 self.open_action = qtutils.add_action(
336 self, N_('Open'), self.open_selected_repo, hotkeys.OPEN
339 self.accept_action = qtutils.add_action(
340 self, N_('Accept'), self.accept_repo, *hotkeys.ACCEPT
343 self.open_new_action = qtutils.add_action(
344 self, N_('Open in New Window'), self.open_new_repo, hotkeys.NEW
347 self.set_default_repo_action = qtutils.add_action(
348 self, N_('Set Default Repository'), self.set_default_repo
351 self.clear_default_repo_action = qtutils.add_action(
352 self, N_('Clear Default Repository'), self.clear_default_repo
355 self.rename_repo_action = qtutils.add_action(
356 self, N_('Rename Repository'), self.rename_repo
359 self.open_default_action = qtutils.add_action(
360 self, cmds.OpenDefaultApp.name(), self.open_default, hotkeys.PRIMARY_ACTION
363 self.launch_editor_action = qtutils.add_action(
364 self, cmds.Edit.name(), self.launch_editor, hotkeys.EDIT
367 self.launch_terminal_action = qtutils.add_action(
368 self, cmds.LaunchTerminal.name(), self.launch_terminal, hotkeys.TERMINAL
371 self.copy_action = qtutils.add_action(self, N_('Copy'), self.copy, hotkeys.COPY)
373 self.delete_action = qtutils.add_action(self, N_('Delete'), self.delete_item)
375 self.remove_missing_action = qtutils.add_action(
376 self, N_('Prune Missing Entries'), self.remove_missing
378 self.remove_missing_action.setToolTip(
379 N_('Remove stale entries for repositories that no longer exist')
382 # pylint: disable=no-member
383 self.model().itemChanged.connect(self.item_changed)
385 self.action_group = utils.Group(
386 self.open_action,
387 self.open_new_action,
388 self.copy_action,
389 self.launch_editor_action,
390 self.launch_terminal_action,
391 self.open_default_action,
392 self.rename_repo_action,
393 self.delete_action,
395 self.action_group.setEnabled(True)
396 self.set_default_repo_action.setEnabled(True)
397 self.clear_default_repo_action.setEnabled(True)
399 def set_rename_enabled(self, is_enabled):
400 self.rename_repo_action.setEnabled(is_enabled)
402 def set_view_mode(self, view_mode):
403 self.current_mode = view_mode
405 def selected_item(self):
406 index = self.currentIndex()
407 return self.model().itemFromIndex(index)
409 def refresh(self):
410 self.model().layoutChanged.emit()
411 context = self.context
412 settings = context.settings
413 builder = BuildItem(context)
414 normalize = display.normalize_path
415 items = []
416 added = set()
418 all_repos = get_all_repos(self.context, settings)
419 for repo, is_bookmark in all_repos:
420 path = normalize(repo['path'])
421 name = normalize(repo['name'])
422 if path in added:
423 continue
424 added.add(path)
426 item = builder.get(path, name, self.current_mode, is_bookmark)
427 items.append(item)
429 self.set_model(items)
431 def contextMenuEvent(self, event):
432 """Configures prompt's context menu."""
433 item = self.selected_item()
435 if isinstance(item, PromptWidgetItem):
436 menu = qtutils.create_menu(N_('Actions'), self)
437 menu.addAction(self.open_action)
438 menu.addAction(self.open_new_action)
439 menu.addAction(self.open_default_action)
440 menu.addSeparator()
441 menu.addAction(self.copy_action)
442 menu.addAction(self.launch_editor_action)
443 menu.addAction(self.launch_terminal_action)
444 menu.addSeparator()
445 if item and item.is_default:
446 menu.addAction(self.clear_default_repo_action)
447 else:
448 menu.addAction(self.set_default_repo_action)
449 menu.addAction(self.rename_repo_action)
450 menu.addSeparator()
451 menu.addAction(self.delete_action)
452 menu.addAction(self.remove_missing_action)
453 menu.exec_(self.mapToGlobal(event.pos()))
455 def item_changed(self, item):
456 self.rename_entry(item, item.text())
458 def rename_entry(self, item, new_name):
459 settings = self.context.settings
460 if item.is_bookmark:
461 rename = settings.rename_bookmark
462 else:
463 rename = settings.rename_recent
465 if rename(item.path, item.name, new_name):
466 settings.save()
467 item.name = new_name
468 else:
469 item.setText(item.name)
471 def apply_func(self, func, *args, **kwargs):
472 item = self.selected_item()
473 if item:
474 func(item, *args, **kwargs)
476 def copy(self):
477 self.apply_func(lambda item: qtutils.set_clipboard(item.path))
479 def open_default(self):
480 context = self.context
481 self.apply_func(lambda item: cmds.do(cmds.OpenDefaultApp, context, [item.path]))
483 def set_default_repo(self):
484 self.apply_func(self.set_default_item)
486 def set_default_item(self, item):
487 context = self.context
488 cmds.do(cmds.SetDefaultRepo, context, item.path)
489 self.refresh()
491 def clear_default_repo(self):
492 self.apply_func(self.clear_default_item)
494 def clear_default_item(self, _item):
495 context = self.context
496 cmds.do(cmds.SetDefaultRepo, context, None)
497 self.refresh()
499 def rename_repo(self):
500 index = self.currentIndex()
501 self.edit(index)
503 def accept_repo(self):
504 self.apply_func(self.accept_item)
506 def accept_item(self, _item):
507 if self.state() & self.EditingState:
508 current_index = self.currentIndex()
509 widget = self.indexWidget(current_index)
510 if widget:
511 self.commitData(widget)
512 self.closePersistentEditor(current_index)
513 self.refresh()
514 else:
515 self.open_selected_repo()
517 def open_new_repo(self):
518 context = self.context
519 self.apply_func(lambda item: cmds.do(cmds.OpenNewRepo, context, item.path))
521 def launch_editor(self):
522 context = self.context
523 self.apply_func(lambda item: cmds.do(cmds.Edit, context, [item.path]))
525 def launch_terminal(self):
526 context = self.context
527 self.apply_func(lambda item: cmds.do(cmds.LaunchTerminal, context, item.path))
529 def delete_item(self):
530 """Remove the selected repo item
532 If the item comes from bookmarks (item.is_bookmark) then delete the item
533 from the Bookmarks list, otherwise delete it from the Recents list.
535 item = self.selected_item()
536 if not item:
537 return
539 if item.is_bookmark:
540 cmd = cmds.RemoveBookmark
541 else:
542 cmd = cmds.RemoveRecent
543 context = self.context
544 ok, _, _, _ = cmds.do(cmd, context, item.path, item.name, icon=icons.discard())
545 if ok:
546 self.refresh()
548 def remove_missing(self):
549 """Remove missing entries from the favorites/recent file list"""
550 settings = self.context.settings
551 settings.remove_missing_bookmarks()
552 settings.remove_missing_recent()
553 self.refresh()
556 class BuildItem(object):
557 def __init__(self, context):
558 self.star_icon = icons.star()
559 self.folder_icon = icons.folder()
560 cfg = context.cfg
561 self.default_repo = cfg.get('cola.defaultrepo')
563 def get(self, path, name, mode, is_bookmark):
564 is_default = self.default_repo == path
565 if is_default:
566 icon = self.star_icon
567 else:
568 icon = self.folder_icon
569 return PromptWidgetItem(path, name, mode, icon, is_default, is_bookmark)
572 class PromptWidgetItem(QtGui.QStandardItem):
573 def __init__(self, path, name, mode, icon, is_default, is_bookmark):
574 QtGui.QStandardItem.__init__(self, icon, name)
575 self.path = path
576 self.name = name
577 self.mode = mode
578 self.is_default = is_default
579 self.is_bookmark = is_bookmark
580 editable = mode == ICON_MODE
582 if self.mode == ICON_MODE:
583 item_text = self.name
584 else:
585 item_text = self.path
587 user_role = Qt.UserRole
588 self.setEditable(editable)
589 self.setData(path, user_role)
590 self.setIcon(icon)
591 self.setText(item_text)
592 self.setToolTip(path)
595 def make_size(size):
596 """Construct a QSize from a single value"""
597 return QtCore.QSize(size, size)