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