Merge pull request #1387 from davvid/remote-dialog
[git-cola.git] / cola / widgets / prefs.py
blob993c374ba3d592c01f563bdf0a05779a26d3ae7e
1 from qtpy import QtCore
2 from qtpy import QtWidgets
4 from . import defs
5 from . import standard
6 from .. import cmds
7 from .. import hidpi
8 from .. import icons
9 from .. import qtutils
10 from .. import themes
11 from ..compat import ustr
12 from ..i18n import N_
13 from ..models import prefs
14 from ..models.prefs import Defaults
15 from ..models.prefs import fallback_editor
18 def preferences(context, model=None, parent=None):
19 if model is None:
20 model = prefs.PreferencesModel(context)
21 view = PreferencesView(context, model, parent=parent)
22 view.show()
23 view.raise_()
24 return view
27 class FormWidget(QtWidgets.QWidget):
28 def __init__(self, context, model, parent, source='global'):
29 QtWidgets.QWidget.__init__(self, parent)
30 self.context = context
31 self.cfg = context.cfg
32 self.model = model
33 self.config_to_widget = {}
34 self.widget_to_config = {}
35 self.source = source
36 self.defaults = {}
37 self.setLayout(QtWidgets.QFormLayout())
39 def add_row(self, label, widget):
40 self.layout().addRow(label, widget)
42 def set_config(self, config_dict):
43 self.config_to_widget.update(config_dict)
44 for config, (widget, default) in config_dict.items():
45 self.widget_to_config[config] = widget
46 self.defaults[config] = default
47 self.connect_widget_to_config(widget, config)
49 def connect_widget_to_config(self, widget, config):
50 if isinstance(widget, QtWidgets.QSpinBox):
51 widget.valueChanged.connect(self._int_config_changed(config))
53 elif isinstance(widget, QtWidgets.QCheckBox):
54 widget.toggled.connect(self._bool_config_changed(config))
56 elif isinstance(widget, QtWidgets.QLineEdit):
57 widget.editingFinished.connect(self._text_config_changed(config, widget))
58 widget.returnPressed.connect(self._text_config_changed(config, widget))
60 elif isinstance(widget, qtutils.ComboBox):
61 widget.currentIndexChanged.connect(
62 self._item_config_changed(config, widget)
65 def _int_config_changed(self, config):
66 def runner(value):
67 cmds.do(prefs.SetConfig, self.model, self.source, config, value)
69 return runner
71 def _bool_config_changed(self, config):
72 def runner(value):
73 cmds.do(prefs.SetConfig, self.model, self.source, config, value)
75 return runner
77 def _text_config_changed(self, config, widget):
78 def runner():
79 value = widget.text()
80 cmds.do(prefs.SetConfig, self.model, self.source, config, value)
82 return runner
84 def _item_config_changed(self, config, widget):
85 def runner():
86 value = widget.current_data()
87 cmds.do(prefs.SetConfig, self.model, self.source, config, value)
89 return runner
91 def update_from_config(self):
92 if self.source == 'global':
93 getter = self.cfg.get_user_or_system
94 else:
95 getter = self.cfg.get
97 for config, widget in self.widget_to_config.items():
98 value = getter(config)
99 if value is None:
100 value = self.defaults[config]
101 set_widget_value(widget, value)
104 def set_widget_value(widget, value):
105 """Set a value on a widget without emitting notifications"""
106 with qtutils.BlockSignals(widget):
107 if isinstance(widget, QtWidgets.QSpinBox):
108 widget.setValue(value)
109 elif isinstance(widget, QtWidgets.QLineEdit):
110 widget.setText(value)
111 elif isinstance(widget, QtWidgets.QCheckBox):
112 widget.setChecked(value)
113 elif hasattr(widget, 'set_value'):
114 widget.set_value(value)
117 class RepoFormWidget(FormWidget):
118 def __init__(self, context, model, parent, source):
119 FormWidget.__init__(self, context, model, parent, source=source)
120 self.name = QtWidgets.QLineEdit()
121 self.email = QtWidgets.QLineEdit()
123 tooltip = N_(
124 'Default directory when exporting patches.\n'
125 'Relative paths are relative to the current repository.\n'
126 'Absolute path are used as-is.'
128 patches_directory = prefs.patches_directory(context)
129 self.patches_directory = standard.DirectoryPathLineEdit(patches_directory, self)
130 self.patches_directory.setToolTip(tooltip)
132 tooltip = N_(
134 This option determines how the supplied commit message should be
135 cleaned up before committing.
137 The <mode> can be strip, whitespace, verbatim, scissors or default.
139 strip
140 Strip leading and trailing empty lines, trailing whitespace,
141 commentary and collapse consecutive empty lines.
143 whitespace
144 Same as strip except #commentary is not removed.
146 verbatim
147 Do not change the message at all.
149 scissors
150 Same as whitespace except that everything from (and including) the line
151 found below is truncated, if the message is to be edited.
152 "#" can be customized with core.commentChar.
154 # ------------------------ >8 ------------------------"""
156 self.commit_cleanup = qtutils.combo(
157 prefs.commit_cleanup_modes(), tooltip=tooltip
159 self.diff_context = standard.SpinBox(value=5, mini=2, maxi=9995)
160 self.merge_verbosity = standard.SpinBox(value=5, maxi=5)
161 self.merge_summary = qtutils.checkbox(checked=True)
162 self.autotemplate = qtutils.checkbox(checked=False)
163 self.merge_diffstat = qtutils.checkbox(checked=True)
164 self.display_untracked = qtutils.checkbox(checked=True)
165 self.show_path = qtutils.checkbox(checked=True)
167 self.refresh_on_focus = qtutils.checkbox(checked=False)
168 tooltip = N_(
169 'Refresh repository state whenever the window is focused or un-minimized'
171 self.refresh_on_focus.setToolTip(tooltip)
173 tooltip = N_(
174 'Enable file system change monitoring using '
175 'inotify on Linux and win32event on Windows'
177 self.inotify = qtutils.checkbox(checked=True)
178 self.inotify.setToolTip(tooltip)
180 self.logdate = qtutils.combo(prefs.date_formats())
181 tooltip = N_(
182 'The date-time format used when displaying dates in Git DAG.\n'
183 'This value is passed to git log --date=<format>'
185 self.logdate.setToolTip(tooltip)
187 tooltip = N_('Detect conflict markers in unmerged files')
188 self.check_conflicts = qtutils.checkbox(checked=True, tooltip=tooltip)
190 tooltip = N_('Use gravatar.com to lookup icons for author emails')
191 self.enable_gravatar = qtutils.checkbox(checked=True, tooltip=tooltip)
193 tooltip = N_('Prevent "Stage" from staging all files when nothing is selected')
194 self.safe_mode = qtutils.checkbox(checked=False, tooltip=tooltip)
196 tooltip = N_('Enable path autocompletion in tools')
197 self.autocomplete_paths = qtutils.checkbox(checked=True, tooltip=tooltip)
199 tooltip = N_('Check whether a commit has been published when amending')
200 self.check_published_commits = qtutils.checkbox(checked=True, tooltip=tooltip)
202 self.add_row(N_('User Name'), self.name)
203 self.add_row(N_('Email Address'), self.email)
204 self.add_row(N_('Patches Directory'), self.patches_directory)
205 self.add_row(N_('Log Date Format'), self.logdate)
206 self.add_row(N_('Commit Message Cleanup'), self.commit_cleanup)
207 self.add_row(N_('Merge Verbosity'), self.merge_verbosity)
208 self.add_row(N_('Number of Diff Context Lines'), self.diff_context)
209 self.add_row(N_('Refresh on Focus'), self.refresh_on_focus)
210 self.add_row(N_('Enable File System Change Monitoring'), self.inotify)
211 self.add_row(N_('Summarize Merge Commits'), self.merge_summary)
212 self.add_row(
213 N_('Automatically Load Commit Message Template'), self.autotemplate
215 self.add_row(N_('Show Full Paths in the Window Title'), self.show_path)
216 self.add_row(N_('Show Diffstat After Merge'), self.merge_diffstat)
217 self.add_row(N_('Display Untracked Files'), self.display_untracked)
218 self.add_row(N_('Detect Conflict Markers'), self.check_conflicts)
219 self.add_row(N_('Enable Gravatar Icons'), self.enable_gravatar)
220 self.add_row(N_('Safe Mode'), self.safe_mode)
221 self.add_row(N_('Autocomplete Paths'), self.autocomplete_paths)
222 self.add_row(
223 N_('Check Published Commits when Amending'), self.check_published_commits
226 self.set_config({
227 prefs.AUTOTEMPLATE: (self.autotemplate, Defaults.autotemplate),
228 prefs.AUTOCOMPLETE_PATHS: (
229 self.autocomplete_paths,
230 Defaults.autocomplete_paths,
232 prefs.CHECK_CONFLICTS: (self.check_conflicts, Defaults.check_conflicts),
233 prefs.CHECK_PUBLISHED_COMMITS: (
234 self.check_published_commits,
235 Defaults.check_published_commits,
237 prefs.COMMIT_CLEANUP: (self.commit_cleanup, Defaults.commit_cleanup),
238 prefs.DIFFCONTEXT: (self.diff_context, Defaults.diff_context),
239 prefs.DISPLAY_UNTRACKED: (
240 self.display_untracked,
241 Defaults.display_untracked,
243 prefs.ENABLE_GRAVATAR: (self.enable_gravatar, Defaults.enable_gravatar),
244 prefs.INOTIFY: (self.inotify, Defaults.inotify),
245 prefs.LOGDATE: (self.logdate, Defaults.logdate),
246 prefs.MERGE_DIFFSTAT: (self.merge_diffstat, Defaults.merge_diffstat),
247 prefs.MERGE_SUMMARY: (self.merge_summary, Defaults.merge_summary),
248 prefs.MERGE_VERBOSITY: (self.merge_verbosity, Defaults.merge_verbosity),
249 prefs.PATCHES_DIRECTORY: (
250 self.patches_directory,
251 Defaults.patches_directory,
253 prefs.REFRESH_ON_FOCUS: (self.refresh_on_focus, Defaults.refresh_on_focus),
254 prefs.SAFE_MODE: (self.safe_mode, Defaults.safe_mode),
255 prefs.SHOW_PATH: (self.show_path, Defaults.show_path),
256 prefs.USER_NAME: (self.name, ''),
257 prefs.USER_EMAIL: (self.email, ''),
261 class SettingsFormWidget(FormWidget):
262 def __init__(self, context, model, parent):
263 FormWidget.__init__(self, context, model, parent)
265 self.fixed_font = QtWidgets.QFontComboBox()
266 self.font_size = standard.SpinBox(value=12, mini=6, maxi=192)
268 self.maxrecent = standard.SpinBox(maxi=99)
269 self.tabwidth = standard.SpinBox(maxi=42)
270 self.textwidth = standard.SpinBox(maxi=150)
272 self.editor = QtWidgets.QLineEdit()
273 self.historybrowser = QtWidgets.QLineEdit()
274 self.blameviewer = QtWidgets.QLineEdit()
275 self.difftool = QtWidgets.QLineEdit()
276 self.mergetool = QtWidgets.QLineEdit()
278 self.linebreak = qtutils.checkbox()
279 self.mouse_zoom = qtutils.checkbox()
280 self.keep_merge_backups = qtutils.checkbox()
281 self.sort_bookmarks = qtutils.checkbox()
282 self.save_window_settings = qtutils.checkbox()
283 self.check_spelling = qtutils.checkbox()
284 self.expandtab = qtutils.checkbox()
285 self.resize_browser_columns = qtutils.checkbox(checked=False)
287 self.add_row(N_('Fixed-Width Font'), self.fixed_font)
288 self.add_row(N_('Font Size'), self.font_size)
289 self.add_row(N_('Text Width'), self.textwidth)
290 self.add_row(N_('Tab Width'), self.tabwidth)
291 self.add_row(N_('Editor'), self.editor)
292 self.add_row(N_('History Browser'), self.historybrowser)
293 self.add_row(N_('Blame Viewer'), self.blameviewer)
294 self.add_row(N_('Diff Tool'), self.difftool)
295 self.add_row(N_('Merge Tool'), self.mergetool)
296 self.add_row(N_('Recent repository count'), self.maxrecent)
297 self.add_row(N_('Auto-Wrap Lines'), self.linebreak)
298 self.add_row(N_('Insert spaces instead of tabs'), self.expandtab)
299 self.add_row(N_('Sort bookmarks alphabetically'), self.sort_bookmarks)
300 self.add_row(N_('Keep *.orig Merge Backups'), self.keep_merge_backups)
301 self.add_row(N_('Save GUI Settings'), self.save_window_settings)
302 self.add_row(N_('Resize File Browser columns'), self.resize_browser_columns)
303 self.add_row(N_('Check spelling'), self.check_spelling)
304 self.add_row(N_('Ctrl+MouseWheel to Zoom'), self.mouse_zoom)
306 self.set_config({
307 prefs.SAVEWINDOWSETTINGS: (
308 self.save_window_settings,
309 Defaults.save_window_settings,
311 prefs.TABWIDTH: (self.tabwidth, Defaults.tabwidth),
312 prefs.EXPANDTAB: (self.expandtab, Defaults.expandtab),
313 prefs.TEXTWIDTH: (self.textwidth, Defaults.textwidth),
314 prefs.LINEBREAK: (self.linebreak, Defaults.linebreak),
315 prefs.MAXRECENT: (self.maxrecent, Defaults.maxrecent),
316 prefs.SORT_BOOKMARKS: (self.sort_bookmarks, Defaults.sort_bookmarks),
317 prefs.DIFFTOOL: (self.difftool, Defaults.difftool),
318 prefs.EDITOR: (self.editor, fallback_editor()),
319 prefs.HISTORY_BROWSER: (
320 self.historybrowser,
321 prefs.default_history_browser(),
323 prefs.BLAME_VIEWER: (self.blameviewer, Defaults.blame_viewer),
324 prefs.MERGE_KEEPBACKUP: (
325 self.keep_merge_backups,
326 Defaults.merge_keep_backup,
328 prefs.MERGETOOL: (self.mergetool, Defaults.mergetool),
329 prefs.RESIZE_BROWSER_COLUMNS: (
330 self.resize_browser_columns,
331 Defaults.resize_browser_columns,
333 prefs.SPELL_CHECK: (self.check_spelling, Defaults.spellcheck),
334 prefs.MOUSE_ZOOM: (self.mouse_zoom, Defaults.mouse_zoom),
337 self.fixed_font.currentFontChanged.connect(self.current_font_changed)
338 self.font_size.valueChanged.connect(self.font_size_changed)
340 def update_from_config(self):
341 """Update widgets to the current config values"""
342 FormWidget.update_from_config(self)
343 context = self.context
345 with qtutils.BlockSignals(self.fixed_font):
346 font = qtutils.diff_font(context)
347 self.fixed_font.setCurrentFont(font)
349 with qtutils.BlockSignals(self.font_size):
350 font_size = font.pointSize()
351 self.font_size.setValue(font_size)
353 def font_size_changed(self, size):
354 font = self.fixed_font.currentFont()
355 font.setPointSize(size)
356 cmds.do(prefs.SetConfig, self.model, 'global', prefs.FONTDIFF, font.toString())
358 def current_font_changed(self, font):
359 cmds.do(prefs.SetConfig, self.model, 'global', prefs.FONTDIFF, font.toString())
362 class AppearanceFormWidget(FormWidget):
363 def __init__(self, context, model, parent):
364 FormWidget.__init__(self, context, model, parent)
365 # Theme selectors
366 self.themes = themes.get_all_themes()
367 self.theme = qtutils.combo_mapped(themes.options(themes=self.themes))
368 self.icon_theme = qtutils.combo_mapped(icons.icon_themes())
370 # The transform to ustr is needed because the config reader will convert
371 # "0", "1", and "2" into integers. The "1.5" value, though, is
372 # parsed as a string, so the transform is effectively a no-op.
373 self.high_dpi = qtutils.combo_mapped(hidpi.options(), transform=ustr)
374 self.high_dpi.setEnabled(hidpi.is_supported())
375 self.bold_headers = qtutils.checkbox()
376 self.status_show_totals = qtutils.checkbox()
377 self.status_indent = qtutils.checkbox()
378 self.block_cursor = qtutils.checkbox(checked=True)
380 self.add_row(N_('GUI theme'), self.theme)
381 self.add_row(N_('Icon theme'), self.icon_theme)
382 self.add_row(N_('High DPI'), self.high_dpi)
383 self.add_row(N_('Bold on dark headers instead of italic'), self.bold_headers)
384 self.add_row(N_('Show file counts in Status titles'), self.status_show_totals)
385 self.add_row(N_('Indent Status paths'), self.status_indent)
386 self.add_row(N_('Use a block cursor in diff editors'), self.block_cursor)
388 self.set_config({
389 prefs.BOLD_HEADERS: (self.bold_headers, Defaults.bold_headers),
390 prefs.HIDPI: (self.high_dpi, Defaults.hidpi),
391 prefs.STATUS_SHOW_TOTALS: (
392 self.status_show_totals,
393 Defaults.status_show_totals,
395 prefs.STATUS_INDENT: (self.status_indent, Defaults.status_indent),
396 prefs.THEME: (self.theme, Defaults.theme),
397 prefs.ICON_THEME: (self.icon_theme, Defaults.icon_theme),
398 prefs.BLOCK_CURSOR: (self.block_cursor, Defaults.block_cursor),
401 self.theme.currentIndexChanged.connect(self._theme_changed)
403 def _theme_changed(self, theme_idx):
404 """Set the icon theme to dark/light when the main theme changes"""
405 # Set the icon theme to a theme that corresponds to the main settings.
406 try:
407 theme = self.themes[theme_idx]
408 except IndexError:
409 return
410 icon_theme = self.icon_theme.current_data()
411 if theme.name == 'default':
412 if icon_theme in ('light', 'dark'):
413 self.icon_theme.set_value('default')
414 elif theme.is_dark:
415 if icon_theme in ('default', 'light'):
416 self.icon_theme.set_value('dark')
417 elif not theme.is_dark:
418 if icon_theme in ('default', 'dark'):
419 self.icon_theme.set_value('light')
422 class AppearanceWidget(QtWidgets.QWidget):
423 def __init__(self, form, parent):
424 QtWidgets.QWidget.__init__(self, parent)
425 self.form = form
426 self.label = QtWidgets.QLabel(
427 '<center><b>'
428 + N_('Restart the application after changing appearance settings.')
429 + '</b></center>'
431 layout = qtutils.vbox(
432 defs.margin,
433 defs.spacing,
434 self.form,
435 defs.spacing * 4,
436 self.label,
437 qtutils.STRETCH,
439 self.setLayout(layout)
441 def update_from_config(self):
442 self.form.update_from_config()
445 class PreferencesView(standard.Dialog):
446 def __init__(self, context, model, parent=None):
447 standard.Dialog.__init__(self, parent=parent)
448 self.context = context
449 self.setWindowTitle(N_('Preferences'))
450 if parent is not None:
451 self.setWindowModality(QtCore.Qt.WindowModal)
453 self.resize(600, 360)
455 self.tab_bar = QtWidgets.QTabBar()
456 self.tab_bar.setDrawBase(False)
457 self.tab_bar.addTab(N_('All Repositories'))
458 self.tab_bar.addTab(N_('Current Repository'))
459 self.tab_bar.addTab(N_('Settings'))
460 self.tab_bar.addTab(N_('Appearance'))
462 self.user_form = RepoFormWidget(context, model, self, source='global')
463 self.repo_form = RepoFormWidget(context, model, self, source='local')
464 self.options_form = SettingsFormWidget(context, model, self)
465 self.appearance_form = AppearanceFormWidget(context, model, self)
466 self.appearance = AppearanceWidget(self.appearance_form, self)
468 self.stack_widget = QtWidgets.QStackedWidget()
469 self.stack_widget.addWidget(self.user_form)
470 self.stack_widget.addWidget(self.repo_form)
471 self.stack_widget.addWidget(self.options_form)
472 self.stack_widget.addWidget(self.appearance)
474 self.close_button = qtutils.close_button()
476 self.button_layout = qtutils.hbox(
477 defs.no_margin, defs.spacing, qtutils.STRETCH, self.close_button
480 self.main_layout = qtutils.vbox(
481 defs.margin,
482 defs.spacing,
483 self.tab_bar,
484 self.stack_widget,
485 self.button_layout,
487 self.setLayout(self.main_layout)
489 self.tab_bar.currentChanged.connect(self.stack_widget.setCurrentIndex)
490 self.stack_widget.currentChanged.connect(self.update_widget)
492 qtutils.connect_button(self.close_button, self.accept)
493 qtutils.add_close_action(self)
495 self.update_widget(0)
497 def update_widget(self, idx):
498 widget = self.stack_widget.widget(idx)
499 widget.update_from_config()