Merge pull request #1405 from github/pre-commit-ci-update-config
[git-cola.git] / cola / widgets / prefs.py
blobab726ab4d3671b519e6666e5e05d0aad3f26ebe7
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 tooltip = N_(
168 'Enable file system change monitoring using '
169 'inotify on Linux and win32event on Windows'
171 self.inotify = qtutils.checkbox(checked=True)
172 self.inotify.setToolTip(tooltip)
174 self.logdate = qtutils.combo(prefs.date_formats())
175 tooltip = N_(
176 'The date-time format used when displaying dates in Git DAG.\n'
177 'This value is passed to git log --date=<format>'
179 self.logdate.setToolTip(tooltip)
181 tooltip = N_('Use gravatar.com to lookup icons for author emails')
182 self.enable_gravatar = qtutils.checkbox(checked=True, tooltip=tooltip)
184 tooltip = N_('Enable path autocompletion in tools')
185 self.autocomplete_paths = qtutils.checkbox(checked=True, tooltip=tooltip)
187 self.add_row(N_('User Name'), self.name)
188 self.add_row(N_('Email Address'), self.email)
189 self.add_row(N_('Patches Directory'), self.patches_directory)
190 self.add_row(N_('Log Date Format'), self.logdate)
191 self.add_row(N_('Commit Message Cleanup'), self.commit_cleanup)
192 self.add_row(N_('Merge Verbosity'), self.merge_verbosity)
193 self.add_row(N_('Number of Diff Context Lines'), self.diff_context)
194 self.add_row(N_('Summarize Merge Commits'), self.merge_summary)
195 self.add_row(
196 N_('Automatically Load Commit Message Template'), self.autotemplate
198 self.add_row(N_('Show Full Paths in the Window Title'), self.show_path)
199 self.add_row(N_('Show Diffstat After Merge'), self.merge_diffstat)
200 self.add_row(N_('Display Untracked Files'), self.display_untracked)
201 self.add_row(N_('Enable Gravatar Icons'), self.enable_gravatar)
202 self.add_row(N_('Autocomplete Paths'), self.autocomplete_paths)
204 self.set_config({
205 prefs.AUTOTEMPLATE: (self.autotemplate, Defaults.autotemplate),
206 prefs.AUTOCOMPLETE_PATHS: (
207 self.autocomplete_paths,
208 Defaults.autocomplete_paths,
210 prefs.COMMIT_CLEANUP: (self.commit_cleanup, Defaults.commit_cleanup),
211 prefs.DIFFCONTEXT: (self.diff_context, Defaults.diff_context),
212 prefs.DISPLAY_UNTRACKED: (
213 self.display_untracked,
214 Defaults.display_untracked,
216 prefs.ENABLE_GRAVATAR: (self.enable_gravatar, Defaults.enable_gravatar),
217 prefs.INOTIFY: (self.inotify, Defaults.inotify),
218 prefs.LOGDATE: (self.logdate, Defaults.logdate),
219 prefs.MERGE_DIFFSTAT: (self.merge_diffstat, Defaults.merge_diffstat),
220 prefs.MERGE_SUMMARY: (self.merge_summary, Defaults.merge_summary),
221 prefs.MERGE_VERBOSITY: (self.merge_verbosity, Defaults.merge_verbosity),
222 prefs.PATCHES_DIRECTORY: (
223 self.patches_directory,
224 Defaults.patches_directory,
226 prefs.SHOW_PATH: (self.show_path, Defaults.show_path),
227 prefs.USER_NAME: (self.name, ''),
228 prefs.USER_EMAIL: (self.email, ''),
232 class SettingsFormWidget(FormWidget):
233 def __init__(self, context, model, parent):
234 FormWidget.__init__(self, context, model, parent)
236 self.fixed_font = QtWidgets.QFontComboBox()
237 self.font_size = standard.SpinBox(value=12, mini=6, maxi=192)
238 self.maxrecent = standard.SpinBox(maxi=99)
239 self.tabwidth = standard.SpinBox(maxi=42)
240 self.textwidth = standard.SpinBox(maxi=150)
242 self.editor = QtWidgets.QLineEdit()
243 self.historybrowser = QtWidgets.QLineEdit()
244 self.blameviewer = QtWidgets.QLineEdit()
245 self.difftool = QtWidgets.QLineEdit()
246 self.mergetool = QtWidgets.QLineEdit()
248 self.linebreak = qtutils.checkbox()
249 self.mouse_zoom = qtutils.checkbox()
250 self.keep_merge_backups = qtutils.checkbox()
251 self.sort_bookmarks = qtutils.checkbox()
252 self.save_window_settings = qtutils.checkbox()
253 self.check_spelling = qtutils.checkbox()
254 tooltip = N_('Detect conflict markers in unmerged files')
255 self.check_conflicts = qtutils.checkbox(checked=True, tooltip=tooltip)
256 self.expandtab = qtutils.checkbox(tooltip=N_('Insert tabs instead of spaces'))
257 tooltip = N_('Prevent "Stage" from staging all files when nothing is selected')
258 self.safe_mode = qtutils.checkbox(checked=False, tooltip=tooltip)
259 tooltip = N_('Check whether a commit has been published when amending')
260 self.check_published_commits = qtutils.checkbox(checked=True, tooltip=tooltip)
261 tooltip = N_(
262 'Refresh repository state whenever the window is focused or un-minimized'
264 self.refresh_on_focus = qtutils.checkbox(checked=False, tooltip=tooltip)
265 self.resize_browser_columns = qtutils.checkbox(checked=False)
266 tooltip = N_('Emit notifications when commits are pushed.')
267 self.notifyonpush = qtutils.checkbox(checked=False, tooltip=tooltip)
269 self.add_row(N_('Fixed-Width Font'), self.fixed_font)
270 self.add_row(N_('Font Size'), self.font_size)
271 self.add_row(N_('Text Width'), self.textwidth)
272 self.add_row(N_('Tab Width'), self.tabwidth)
273 self.add_row(N_('Editor'), self.editor)
274 self.add_row(N_('History Browser'), self.historybrowser)
275 self.add_row(N_('Blame Viewer'), self.blameviewer)
276 self.add_row(N_('Diff Tool'), self.difftool)
277 self.add_row(N_('Merge Tool'), self.mergetool)
278 self.add_row(N_('Recent repository count'), self.maxrecent)
279 self.add_row(N_('Auto-Wrap Lines'), self.linebreak)
280 self.add_row(N_('Insert spaces instead of tabs'), self.expandtab)
281 self.add_row(
282 N_('Check Published Commits when Amending'), self.check_published_commits
284 self.add_row(N_('Sort bookmarks alphabetically'), self.sort_bookmarks)
285 self.add_row(N_('Safe Mode'), self.safe_mode)
286 self.add_row(N_('Detect Conflict Markers'), self.check_conflicts)
287 self.add_row(N_('Keep *.orig Merge Backups'), self.keep_merge_backups)
288 self.add_row(N_('Save GUI Settings'), self.save_window_settings)
289 self.add_row(N_('Refresh on Focus'), self.refresh_on_focus)
290 self.add_row(N_('Resize File Browser columns'), self.resize_browser_columns)
291 self.add_row(N_('Check spelling'), self.check_spelling)
292 self.add_row(N_('Ctrl+MouseWheel to Zoom'), self.mouse_zoom)
293 self.add_row(N_('Notify on Push'), self.notifyonpush)
295 self.set_config({
296 prefs.SAVEWINDOWSETTINGS: (
297 self.save_window_settings,
298 Defaults.save_window_settings,
300 prefs.TABWIDTH: (self.tabwidth, Defaults.tabwidth),
301 prefs.EXPANDTAB: (self.expandtab, Defaults.expandtab),
302 prefs.TEXTWIDTH: (self.textwidth, Defaults.textwidth),
303 prefs.LINEBREAK: (self.linebreak, Defaults.linebreak),
304 prefs.MAXRECENT: (self.maxrecent, Defaults.maxrecent),
305 prefs.SORT_BOOKMARKS: (self.sort_bookmarks, Defaults.sort_bookmarks),
306 prefs.DIFFTOOL: (self.difftool, Defaults.difftool),
307 prefs.EDITOR: (self.editor, fallback_editor()),
308 prefs.HISTORY_BROWSER: (
309 self.historybrowser,
310 prefs.default_history_browser(),
312 prefs.BLAME_VIEWER: (self.blameviewer, Defaults.blame_viewer),
313 prefs.CHECK_CONFLICTS: (self.check_conflicts, Defaults.check_conflicts),
314 prefs.CHECK_PUBLISHED_COMMITS: (
315 self.check_published_commits,
316 Defaults.check_published_commits,
318 prefs.MERGE_KEEPBACKUP: (
319 self.keep_merge_backups,
320 Defaults.merge_keep_backup,
322 prefs.MERGETOOL: (self.mergetool, Defaults.mergetool),
323 prefs.REFRESH_ON_FOCUS: (self.refresh_on_focus, Defaults.refresh_on_focus),
324 prefs.RESIZE_BROWSER_COLUMNS: (
325 self.resize_browser_columns,
326 Defaults.resize_browser_columns,
328 prefs.SAFE_MODE: (self.safe_mode, Defaults.safe_mode),
329 prefs.SPELL_CHECK: (self.check_spelling, Defaults.spellcheck),
330 prefs.MOUSE_ZOOM: (self.mouse_zoom, Defaults.mouse_zoom),
331 prefs.NOTIFY_ON_PUSH: (self.notifyonpush, Defaults.notifyonpush),
334 self.fixed_font.currentFontChanged.connect(self.current_font_changed)
335 self.font_size.valueChanged.connect(self.font_size_changed)
337 def update_from_config(self):
338 """Update widgets to the current config values"""
339 FormWidget.update_from_config(self)
340 context = self.context
342 with qtutils.BlockSignals(self.fixed_font):
343 font = qtutils.diff_font(context)
344 self.fixed_font.setCurrentFont(font)
346 with qtutils.BlockSignals(self.font_size):
347 font_size = font.pointSize()
348 self.font_size.setValue(font_size)
350 def font_size_changed(self, size):
351 font = self.fixed_font.currentFont()
352 font.setPointSize(size)
353 cmds.do(prefs.SetConfig, self.model, 'global', prefs.FONTDIFF, font.toString())
355 def current_font_changed(self, font):
356 cmds.do(prefs.SetConfig, self.model, 'global', prefs.FONTDIFF, font.toString())
359 class AppearanceFormWidget(FormWidget):
360 def __init__(self, context, model, parent):
361 FormWidget.__init__(self, context, model, parent)
362 # Theme selectors
363 self.themes = themes.get_all_themes()
364 self.theme = qtutils.combo_mapped(themes.options(themes=self.themes))
365 self.icon_theme = qtutils.combo_mapped(icons.icon_themes())
367 # The transform to ustr is needed because the config reader will convert
368 # "0", "1", and "2" into integers. The "1.5" value, though, is
369 # parsed as a string, so the transform is effectively a no-op.
370 self.high_dpi = qtutils.combo_mapped(hidpi.options(), transform=ustr)
371 self.high_dpi.setEnabled(hidpi.is_supported())
372 self.bold_headers = qtutils.checkbox()
373 self.status_show_totals = qtutils.checkbox()
374 self.status_indent = qtutils.checkbox()
375 self.block_cursor = qtutils.checkbox(checked=True)
377 self.add_row(N_('GUI theme'), self.theme)
378 self.add_row(N_('Icon theme'), self.icon_theme)
379 self.add_row(N_('High DPI'), self.high_dpi)
380 self.add_row(N_('Bold on dark headers instead of italic'), self.bold_headers)
381 self.add_row(N_('Show file counts in Status titles'), self.status_show_totals)
382 self.add_row(N_('Indent Status paths'), self.status_indent)
383 self.add_row(N_('Use a block cursor in diff editors'), self.block_cursor)
385 self.set_config({
386 prefs.BOLD_HEADERS: (self.bold_headers, Defaults.bold_headers),
387 prefs.HIDPI: (self.high_dpi, Defaults.hidpi),
388 prefs.STATUS_SHOW_TOTALS: (
389 self.status_show_totals,
390 Defaults.status_show_totals,
392 prefs.STATUS_INDENT: (self.status_indent, Defaults.status_indent),
393 prefs.THEME: (self.theme, Defaults.theme),
394 prefs.ICON_THEME: (self.icon_theme, Defaults.icon_theme),
395 prefs.BLOCK_CURSOR: (self.block_cursor, Defaults.block_cursor),
398 self.theme.currentIndexChanged.connect(self._theme_changed)
400 def _theme_changed(self, theme_idx):
401 """Set the icon theme to dark/light when the main theme changes"""
402 # Set the icon theme to a theme that corresponds to the main settings.
403 try:
404 theme = self.themes[theme_idx]
405 except IndexError:
406 return
407 icon_theme = self.icon_theme.current_data()
408 if theme.name == 'default':
409 if icon_theme in ('light', 'dark'):
410 self.icon_theme.set_value('default')
411 elif theme.is_dark:
412 if icon_theme in ('default', 'light'):
413 self.icon_theme.set_value('dark')
414 elif not theme.is_dark:
415 if icon_theme in ('default', 'dark'):
416 self.icon_theme.set_value('light')
419 class AppearanceWidget(QtWidgets.QWidget):
420 def __init__(self, form, parent):
421 QtWidgets.QWidget.__init__(self, parent)
422 self.form = form
423 self.label = QtWidgets.QLabel(
424 '<center><b>'
425 + N_('Restart the application after changing appearance settings.')
426 + '</b></center>'
428 layout = qtutils.vbox(
429 defs.margin,
430 defs.spacing,
431 self.form,
432 defs.spacing * 4,
433 self.label,
434 qtutils.STRETCH,
436 self.setLayout(layout)
438 def update_from_config(self):
439 self.form.update_from_config()
442 class PreferencesView(standard.Dialog):
443 def __init__(self, context, model, parent=None):
444 standard.Dialog.__init__(self, parent=parent)
445 self.context = context
446 self.setWindowTitle(N_('Preferences'))
447 if parent is not None:
448 self.setWindowModality(QtCore.Qt.WindowModal)
450 self.resize(600, 360)
452 self.tab_bar = QtWidgets.QTabBar()
453 self.tab_bar.setDrawBase(False)
454 self.tab_bar.addTab(N_('All Repositories'))
455 self.tab_bar.addTab(N_('Current Repository'))
456 self.tab_bar.addTab(N_('Settings'))
457 self.tab_bar.addTab(N_('Appearance'))
459 self.user_form = RepoFormWidget(context, model, self, source='global')
460 self.repo_form = RepoFormWidget(context, model, self, source='local')
461 self.options_form = SettingsFormWidget(context, model, self)
462 self.appearance_form = AppearanceFormWidget(context, model, self)
463 self.appearance = AppearanceWidget(self.appearance_form, self)
465 self.stack_widget = QtWidgets.QStackedWidget()
466 self.stack_widget.addWidget(self.user_form)
467 self.stack_widget.addWidget(self.repo_form)
468 self.stack_widget.addWidget(self.options_form)
469 self.stack_widget.addWidget(self.appearance)
471 self.close_button = qtutils.close_button()
473 self.button_layout = qtutils.hbox(
474 defs.no_margin, defs.spacing, qtutils.STRETCH, self.close_button
477 self.main_layout = qtutils.vbox(
478 defs.margin,
479 defs.spacing,
480 self.tab_bar,
481 self.stack_widget,
482 self.button_layout,
484 self.setLayout(self.main_layout)
486 self.tab_bar.currentChanged.connect(self.stack_widget.setCurrentIndex)
487 self.stack_widget.currentChanged.connect(self.update_widget)
489 qtutils.connect_button(self.close_button, self.accept)
490 qtutils.add_close_action(self)
492 self.update_widget(0)
494 def update_widget(self, idx):
495 widget = self.stack_widget.widget(idx)
496 widget.update_from_config()