fetch: add support for the traditional FETCH_HEAD behavior
[git-cola.git] / cola / widgets / editremotes.py
blob1ff7d90008f229319f48f5c7eda3c453462f86ef
1 import operator
3 from qtpy import QtCore
4 from qtpy import QtWidgets
5 from qtpy.QtCore import Qt
6 from qtpy.QtCore import Signal
8 from ..git import STDOUT
9 from ..i18n import N_
10 from ..qtutils import get
11 from .. import cmds
12 from .. import core
13 from .. import gitcmds
14 from .. import icons
15 from .. import qtutils
16 from .. import utils
17 from . import completion
18 from . import defs
19 from . import standard
20 from . import text
23 def editor(context, run=True):
24 view = RemoteEditor(context, parent=qtutils.active_window())
25 if run:
26 view.show()
27 view.exec_()
28 return view
31 class RemoteEditor(standard.Dialog):
32 def __init__(self, context, parent=None):
33 standard.Dialog.__init__(self, parent)
34 self.setWindowTitle(N_('Edit Remotes'))
35 if parent is not None:
36 self.setWindowModality(Qt.WindowModal)
38 self.context = context
39 self.current_name = ''
40 self.current_url = ''
42 hint = N_('Edit remotes by selecting them from the list')
43 self.default_hint = hint
45 self.remote_list = []
46 self.remotes = QtWidgets.QListWidget()
47 self.remotes.setToolTip(N_('Remote git repositories - double-click to rename'))
49 self.editor = RemoteWidget(context, self)
51 self.save_button = qtutils.create_button(
52 text=N_('Save'), icon=icons.save(), default=True
55 self.reset_button = qtutils.create_button(
56 text=N_('Reset'), icon=icons.discard()
59 tooltip = N_(
60 'Add and remove remote repositories using the \n'
61 'Add(+) and Delete(-) buttons on the left-hand side.\n'
62 '\n'
63 'Remotes can be renamed by selecting one from the list\n'
64 'and pressing "enter", or by double-clicking.'
66 hint = self.default_hint
67 self.info = text.VimHintedPlainTextEdit(context, hint, parent=self)
68 self.info.setToolTip(tooltip)
70 text_width, text_height = qtutils.text_size(self.info.font(), 'M')
71 width = text_width * 42
72 height = text_height * 13
73 self.info.setMinimumWidth(width)
74 self.info.setMinimumHeight(height)
75 self.info_thread = RemoteInfoThread(context, self)
77 icon = icons.add()
78 tooltip = N_('Add new remote git repository')
79 self.add_button = qtutils.create_toolbutton(icon=icon, tooltip=tooltip)
81 self.refresh_button = qtutils.create_toolbutton(
82 icon=icons.sync(), tooltip=N_('Refresh')
84 self.delete_button = qtutils.create_toolbutton(
85 icon=icons.remove(), tooltip=N_('Delete remote')
87 self.close_button = qtutils.close_button()
89 self._edit_button_layout = qtutils.vbox(
90 defs.no_margin, defs.spacing, self.save_button, self.reset_button
93 self._edit_layout = qtutils.hbox(
94 defs.no_margin, defs.spacing, self.editor, self._edit_button_layout
97 self._display_layout = qtutils.vbox(
98 defs.no_margin, defs.spacing, self._edit_layout, self.info
100 self._display_widget = QtWidgets.QWidget(self)
101 self._display_widget.setLayout(self._display_layout)
103 self._top_layout = qtutils.splitter(
104 Qt.Horizontal, self.remotes, self._display_widget
106 width = self._top_layout.width()
107 self._top_layout.setSizes([width // 4, width * 3 // 4])
109 self._button_layout = qtutils.hbox(
110 defs.margin,
111 defs.spacing,
112 self.add_button,
113 self.delete_button,
114 self.refresh_button,
115 qtutils.STRETCH,
116 self.close_button,
119 self._layout = qtutils.vbox(
120 defs.margin, defs.spacing, self._top_layout, self._button_layout
122 self.setLayout(self._layout)
124 qtutils.connect_button(self.add_button, self.add)
125 qtutils.connect_button(self.delete_button, self.delete)
126 qtutils.connect_button(self.refresh_button, self.refresh)
127 qtutils.connect_button(self.close_button, self.accept)
128 qtutils.connect_button(self.save_button, self.save)
129 qtutils.connect_button(self.reset_button, self.reset)
130 qtutils.add_close_action(self)
132 thread = self.info_thread
133 thread.result.connect(self.set_info, type=Qt.QueuedConnection)
135 self.editor.remote_name.returnPressed.connect(self.save)
136 self.editor.remote_url.returnPressed.connect(self.save)
137 self.editor.valid.connect(self.editor_valid)
139 self.remotes.itemChanged.connect(self.remote_item_renamed)
140 self.remotes.itemSelectionChanged.connect(self.selection_changed)
142 self.disable_editor()
143 self.init_state(None, self.resize_widget, parent)
144 self.remotes.setFocus(Qt.OtherFocusReason)
145 self.refresh()
147 def reset(self):
148 focus = self.focusWidget()
149 if self.current_name:
150 self.activate_remote(self.current_name, gather_info=False)
151 restore_focus(focus)
153 @property
154 def changed(self):
155 url = self.editor.url
156 name = self.editor.name
157 return url != self.current_url or name != self.current_name
159 def save(self):
160 if not self.changed:
161 return
162 context = self.context
163 name = self.editor.name
164 url = self.editor.url
165 old_url = self.current_url
166 old_name = self.current_name
168 name_changed = name and name != old_name
169 url_changed = url and url != old_url
170 focus = self.focusWidget()
172 name_ok = url_ok = False
174 # Run the corresponding command
175 if name_changed and url_changed:
176 name_ok, url_ok = cmds.do(cmds.RemoteEdit, context, old_name, name, url)
177 elif name_changed:
178 result = cmds.do(cmds.RemoteRename, context, old_name, name)
179 name_ok = result[0]
180 elif url_changed:
181 result = cmds.do(cmds.RemoteSetURL, context, name, url)
182 url_ok = result[0]
184 # Update state if the change succeeded
185 gather = False
186 if url_changed and url_ok:
187 self.current_url = url
188 gather = True
190 # A name change requires a refresh
191 if name_changed and name_ok:
192 self.current_name = name
194 self.refresh(select=False)
195 remotes = utils.Sequence(self.remote_list)
196 idx = remotes.index(name)
197 self.select_remote(idx)
198 gather = False # already done by select_remote()
200 restore_focus(focus)
201 if gather:
202 self.gather_info()
204 def editor_valid(self, valid):
205 changed = self.changed
206 self.reset_button.setEnabled(changed)
207 self.save_button.setEnabled(changed and valid)
209 def disable_editor(self):
210 self.save_button.setEnabled(False)
211 self.reset_button.setEnabled(False)
212 self.editor.setEnabled(False)
213 self.editor.name = ''
214 self.editor.url = ''
215 self.info.hint.set_value(self.default_hint)
216 self.info.set_value('')
218 def resize_widget(self, parent):
219 """Set the initial size of the widget"""
220 width, height = qtutils.default_size(parent, 720, 445)
221 self.resize(width, height)
223 def set_info(self, info):
224 self.info.hint.set_value(self.default_hint)
225 self.info.set_value(info)
227 def select_remote(self, index):
228 if index >= 0:
229 item = self.remotes.item(index)
230 if item:
231 item.setSelected(True)
233 def refresh(self, select=True):
234 git = self.context.git
235 remotes = git.remote()[STDOUT].splitlines()
236 # Ignore notifications from self.remotes while mutating.
237 with qtutils.BlockSignals(self.remotes):
238 self.remotes.clear()
239 self.remotes.addItems(remotes)
240 self.remote_list = remotes
242 for idx in range(len(remotes)):
243 item = self.remotes.item(idx)
244 item.setFlags(item.flags() | Qt.ItemIsEditable)
246 if select:
247 if not self.current_name and remotes:
248 # Nothing is selected; select the first item
249 self.select_remote(0)
250 elif self.current_name and remotes:
251 # Reselect the previously selected item
252 remote_seq = utils.Sequence(remotes)
253 idx = remote_seq.index(self.current_name)
254 if idx >= 0:
255 item = self.remotes.item(idx)
256 if item:
257 item.setSelected(True)
259 def add(self):
260 if add_remote(self.context, self):
261 self.refresh(select=False)
262 # Newly added remote will be last; select it
263 self.select_remote(len(self.remote_list) - 1)
265 def delete(self):
266 remote = qtutils.selected_item(self.remotes, self.remote_list)
267 if not remote:
268 return
269 cmds.do(cmds.RemoteRemove, self.context, remote)
270 self.refresh(select=False)
272 def remote_item_renamed(self, item):
273 idx = self.remotes.row(item)
274 if idx < 0:
275 return
276 if idx >= len(self.remote_list):
277 return
278 old_name = self.remote_list[idx]
279 new_name = item.text()
280 if new_name == old_name:
281 return
282 if not new_name:
283 item.setText(old_name)
284 return
285 context = self.context
286 ok, status, _, _ = cmds.do(cmds.RemoteRename, context, old_name, new_name)
287 if ok and status == 0:
288 self.remote_list[idx] = new_name
289 self.activate_remote(new_name)
290 else:
291 item.setText(old_name)
293 def selection_changed(self):
294 remote = qtutils.selected_item(self.remotes, self.remote_list)
295 if not remote:
296 self.disable_editor()
297 return
298 self.activate_remote(remote)
300 def activate_remote(self, name, gather_info=True):
301 url = gitcmds.remote_url(self.context, name)
302 self.current_name = name
303 self.current_url = url
305 self.editor.setEnabled(True)
306 self.editor.name = name
307 self.editor.url = url
309 if gather_info:
310 self.gather_info()
312 def gather_info(self):
313 name = self.current_name
314 self.info.hint.set_value(N_('Gathering info for "%s"...') % name)
315 self.info.set_value('')
317 self.info_thread.remote = name
318 self.info_thread.start()
321 def add_remote(context, parent, name='', url='', readonly_url=False):
322 """Bring up the "Add Remote" dialog"""
323 widget = AddRemoteDialog(context, parent, readonly_url=readonly_url)
324 if name:
325 widget.name = name
326 if url:
327 widget.url = url
328 if widget.run():
329 cmds.do(cmds.RemoteAdd, context, widget.name, widget.url)
330 result = True
331 else:
332 result = False
333 return result
336 def restore_focus(focus):
337 if focus:
338 focus.setFocus(Qt.OtherFocusReason)
339 if hasattr(focus, 'selectAll'):
340 focus.selectAll()
343 class RemoteInfoThread(QtCore.QThread):
344 result = Signal(object)
346 def __init__(self, context, parent):
347 QtCore.QThread.__init__(self, parent)
348 self.context = context
349 self.remote = None
351 def run(self):
352 remote = self.remote
353 if remote is None:
354 return
355 git = self.context.git
356 _, out, err = git.remote('show', '-n', remote)
357 self.result.emit(out + err)
360 class AddRemoteDialog(QtWidgets.QDialog):
361 def __init__(self, context, parent, readonly_url=False):
362 super().__init__(parent)
363 self.context = context
364 if parent:
365 self.setWindowModality(Qt.WindowModal)
367 self.context = context
368 self.widget = RemoteWidget(context, self, readonly_url=readonly_url)
369 self.add_button = qtutils.create_button(
370 text=N_('Add Remote'), icon=icons.ok(), enabled=False
372 self.close_button = qtutils.close_button()
374 self._button_layout = qtutils.hbox(
375 defs.no_margin,
376 defs.button_spacing,
377 qtutils.STRETCH,
378 self.close_button,
379 self.add_button,
382 self._layout = qtutils.vbox(
383 defs.margin, defs.spacing, self.widget, self._button_layout
385 self.setLayout(self._layout)
387 self.widget.valid.connect(self.add_button.setEnabled)
388 qtutils.connect_button(self.add_button, self.accept)
389 qtutils.connect_button(self.close_button, self.reject)
391 def set_name(self, value):
392 self.widget.name = value
394 def set_url(self, value):
395 self.widget.url = value
397 name = property(operator.attrgetter('widget.name'), set_name)
398 url = property(operator.attrgetter('widget.url'), set_url)
400 def run(self):
401 self.show()
402 self.raise_()
403 return self.exec_() == QtWidgets.QDialog.Accepted
406 def lineedit(context, hint):
407 """Create a HintedLineEdit with a preset minimum width"""
408 widget = text.HintedLineEdit(context, hint)
409 width = qtutils.text_width(widget.font(), 'M')
410 widget.setMinimumWidth(width * 32)
411 return widget
414 class RemoteWidget(QtWidgets.QWidget):
415 name = property(
416 lambda self: get(self.remote_name),
417 lambda self, value: self.remote_name.set_value(value),
419 url = property(
420 lambda self: get(self.remote_url),
421 lambda self, value: self.remote_url.set_value(value),
423 valid = Signal(bool)
425 def __init__(self, context, parent, readonly_url=False):
426 super().__init__(parent)
427 self.setWindowModality(Qt.WindowModal)
428 self.context = context
429 self.setWindowTitle(N_('Add remote'))
430 self.remote_name = lineedit(context, N_('Name for the new remote'))
431 self.remote_url = lineedit(context, 'git://git.example.com/repo.git')
432 self.open_button = qtutils.create_button(
433 text=N_('Browse...'), icon=icons.folder(), tooltip=N_('Select repository')
436 self.url_layout = qtutils.hbox(
437 defs.no_margin, defs.spacing, self.remote_url, self.open_button
440 validate_remote = completion.RemoteValidator(self.remote_name)
441 self.remote_name.setValidator(validate_remote)
443 self._form = qtutils.form(
444 defs.margin,
445 defs.spacing,
446 (N_('Name'), self.remote_name),
447 (N_('URL'), self.url_layout),
450 self._layout = qtutils.vbox(defs.margin, defs.spacing, self._form)
451 self.setLayout(self._layout)
453 self.remote_name.textChanged.connect(self.validate)
454 self.remote_url.textChanged.connect(self.validate)
455 qtutils.connect_button(self.open_button, self.open_repo)
457 if readonly_url:
458 self.remote_url.setReadOnly(True)
459 self.open_button.setEnabled(False)
461 def validate(self, _text):
462 name = self.name
463 url = self.url
464 self.valid.emit(bool(name) and bool(url))
466 def open_repo(self):
467 git = self.context.git
468 repo = qtutils.opendir_dialog(N_('Open Git Repository'), core.getcwd())
469 if repo and git.is_git_repository(repo):
470 self.url = repo