diff: use a block cursor for better usability
[git-cola.git] / cola / widgets / prefs.py
blob8491dbe0b180530f99a44b0106760ce7b467a0a8
1 from __future__ import absolute_import, division, print_function, unicode_literals
3 from qtpy import QtCore
4 from qtpy import QtWidgets
6 from . import defs
7 from . import standard
8 from .. import cmds
9 from .. import hidpi
10 from .. import icons
11 from .. import qtutils
12 from .. import themes
13 from ..compat import ustr
14 from ..i18n import N_
15 from ..models import prefs
16 from ..models.prefs import Defaults
17 from ..models.prefs import fallback_editor
20 def preferences(context, model=None, parent=None):
21 if model is None:
22 model = prefs.PreferencesModel(context)
23 view = PreferencesView(context, model, parent=parent)
24 view.show()
25 view.raise_()
26 return view
29 class FormWidget(QtWidgets.QWidget):
30 def __init__(self, context, model, parent, source='global'):
31 QtWidgets.QWidget.__init__(self, parent)
32 self.context = context
33 self.cfg = context.cfg
34 self.model = model
35 self.config_to_widget = {}
36 self.widget_to_config = {}
37 self.source = source
38 self.defaults = {}
39 self.setLayout(QtWidgets.QFormLayout())
41 def add_row(self, label, widget):
42 self.layout().addRow(label, widget)
44 def set_config(self, config_dict):
45 self.config_to_widget.update(config_dict)
46 for config, (widget, default) in config_dict.items():
47 self.widget_to_config[config] = widget
48 self.defaults[config] = default
49 self.connect_widget_to_config(widget, config)
51 def connect_widget_to_config(self, widget, config):
52 if isinstance(widget, QtWidgets.QSpinBox):
53 widget.valueChanged.connect(self._int_config_changed(config))
55 elif isinstance(widget, QtWidgets.QCheckBox):
56 widget.toggled.connect(self._bool_config_changed(config))
58 elif isinstance(widget, QtWidgets.QLineEdit):
59 widget.editingFinished.connect(self._text_config_changed(config, widget))
60 widget.returnPressed.connect(self._text_config_changed(config, widget))
62 elif isinstance(widget, qtutils.ComboBox):
63 widget.currentIndexChanged.connect(
64 self._item_config_changed(config, widget)
67 def _int_config_changed(self, config):
68 def runner(value):
69 cmds.do(prefs.SetConfig, self.model, self.source, config, value)
71 return runner
73 def _bool_config_changed(self, config):
74 def runner(value):
75 cmds.do(prefs.SetConfig, self.model, self.source, config, value)
77 return runner
79 def _text_config_changed(self, config, widget):
80 def runner():
81 value = widget.text()
82 cmds.do(prefs.SetConfig, self.model, self.source, config, value)
84 return runner
86 def _item_config_changed(self, config, widget):
87 def runner():
88 value = widget.current_data()
89 cmds.do(prefs.SetConfig, self.model, self.source, config, value)
91 return runner
93 def update_from_config(self):
94 if self.source == 'global':
95 getter = self.cfg.get_user_or_system
96 else:
97 getter = self.cfg.get
99 for config, widget in self.widget_to_config.items():
100 value = getter(config)
101 if value is None:
102 value = self.defaults[config]
103 set_widget_value(widget, value)
106 def set_widget_value(widget, value):
107 """Set a value on a widget without emitting notifications"""
108 with qtutils.BlockSignals(widget):
109 if isinstance(widget, QtWidgets.QSpinBox):
110 widget.setValue(value)
111 elif isinstance(widget, QtWidgets.QLineEdit):
112 widget.setText(value)
113 elif isinstance(widget, QtWidgets.QCheckBox):
114 widget.setChecked(value)
115 elif isinstance(widget, qtutils.ComboBox):
116 widget.set_value(value)
119 class RepoFormWidget(FormWidget):
120 def __init__(self, context, model, parent, source):
121 FormWidget.__init__(self, context, model, parent, source=source)
122 self.name = QtWidgets.QLineEdit()
123 self.email = QtWidgets.QLineEdit()
125 self.diff_context = standard.SpinBox(value=5, mini=2, maxi=9995)
126 self.merge_verbosity = standard.SpinBox(value=5, maxi=5)
127 self.merge_summary = qtutils.checkbox(checked=True)
128 self.autotemplate = qtutils.checkbox(checked=False)
129 self.merge_diffstat = qtutils.checkbox(checked=True)
130 self.display_untracked = qtutils.checkbox(checked=True)
131 self.show_path = qtutils.checkbox(checked=True)
132 self.tabwidth = standard.SpinBox(value=8, maxi=42)
133 self.textwidth = standard.SpinBox(value=72, maxi=150)
135 tooltip = N_('Detect conflict markers in unmerged files')
136 self.check_conflicts = qtutils.checkbox(checked=True, tooltip=tooltip)
138 tooltip = N_('Use gravatar.com to lookup icons for author emails')
139 self.enable_gravatar = qtutils.checkbox(checked=True, tooltip=tooltip)
141 tooltip = N_('Prevent "Stage" from staging all files when nothing is selected')
142 self.safe_mode = qtutils.checkbox(checked=False, tooltip=tooltip)
144 tooltip = N_('Enable path autocompletion in tools')
145 self.autocomplete_paths = qtutils.checkbox(checked=True, tooltip=tooltip)
147 tooltip = N_('Check whether a commit has been published when amending')
148 self.check_published_commits = qtutils.checkbox(checked=True, tooltip=tooltip)
150 self.add_row(N_('User Name'), self.name)
151 self.add_row(N_('Email Address'), self.email)
152 self.add_row(N_('Tab Width'), self.tabwidth)
153 self.add_row(N_('Text Width'), self.textwidth)
154 self.add_row(N_('Merge Verbosity'), self.merge_verbosity)
155 self.add_row(N_('Number of Diff Context Lines'), self.diff_context)
156 self.add_row(N_('Summarize Merge Commits'), self.merge_summary)
157 self.add_row(
158 N_('Automatically Load Commit Message Template'), self.autotemplate
160 self.add_row(N_('Show Full Paths in the Window Title'), self.show_path)
161 self.add_row(N_('Show Diffstat After Merge'), self.merge_diffstat)
162 self.add_row(N_('Display Untracked Files'), self.display_untracked)
163 self.add_row(N_('Detect Conflict Markers'), self.check_conflicts)
164 self.add_row(N_('Enable Gravatar Icons'), self.enable_gravatar)
165 self.add_row(N_('Safe Mode'), self.safe_mode)
166 self.add_row(N_('Autocomplete Paths'), self.autocomplete_paths)
167 self.add_row(
168 N_('Check Published Commits when Amending'), self.check_published_commits
171 self.set_config(
173 prefs.AUTOTEMPLATE: (self.autotemplate, Defaults.autotemplate),
174 prefs.CHECK_CONFLICTS: (self.check_conflicts, Defaults.check_conflicts),
175 prefs.ENABLE_GRAVATAR: (self.enable_gravatar, Defaults.enable_gravatar),
176 prefs.CHECK_PUBLISHED_COMMITS: (
177 self.check_published_commits,
178 Defaults.check_published_commits,
180 prefs.DIFFCONTEXT: (self.diff_context, Defaults.diff_context),
181 prefs.DISPLAY_UNTRACKED: (
182 self.display_untracked,
183 Defaults.display_untracked,
185 prefs.USER_NAME: (self.name, ''),
186 prefs.USER_EMAIL: (self.email, ''),
187 prefs.MERGE_DIFFSTAT: (self.merge_diffstat, Defaults.merge_diffstat),
188 prefs.MERGE_SUMMARY: (self.merge_summary, Defaults.merge_summary),
189 prefs.MERGE_VERBOSITY: (self.merge_verbosity, Defaults.merge_verbosity),
190 prefs.SAFE_MODE: (self.safe_mode, Defaults.safe_mode),
191 prefs.AUTOCOMPLETE_PATHS: (
192 self.autocomplete_paths,
193 Defaults.autocomplete_paths,
195 prefs.SHOW_PATH: (self.show_path, Defaults.show_path),
196 prefs.TABWIDTH: (self.tabwidth, Defaults.tabwidth),
197 prefs.TEXTWIDTH: (self.textwidth, Defaults.textwidth),
202 class SettingsFormWidget(FormWidget):
203 def __init__(self, context, model, parent):
204 FormWidget.__init__(self, context, model, parent)
206 self.fixed_font = QtWidgets.QFontComboBox()
207 self.font_size = standard.SpinBox(value=12, mini=8, maxi=192)
209 self.maxrecent = standard.SpinBox(maxi=99)
210 self.tabwidth = standard.SpinBox(maxi=42)
211 self.textwidth = standard.SpinBox(maxi=150)
213 self.editor = QtWidgets.QLineEdit()
214 self.historybrowser = QtWidgets.QLineEdit()
215 self.blameviewer = QtWidgets.QLineEdit()
216 self.difftool = QtWidgets.QLineEdit()
217 self.mergetool = QtWidgets.QLineEdit()
219 self.linebreak = qtutils.checkbox()
220 self.mouse_zoom = qtutils.checkbox()
221 self.keep_merge_backups = qtutils.checkbox()
222 self.sort_bookmarks = qtutils.checkbox()
223 self.save_window_settings = qtutils.checkbox()
224 self.check_spelling = qtutils.checkbox()
225 self.expandtab = qtutils.checkbox()
226 self.resize_browser_columns = qtutils.checkbox(checked=False)
228 self.add_row(N_('Fixed-Width Font'), self.fixed_font)
229 self.add_row(N_('Font Size'), self.font_size)
230 self.add_row(N_('Editor'), self.editor)
231 self.add_row(N_('History Browser'), self.historybrowser)
232 self.add_row(N_('Blame Viewer'), self.blameviewer)
233 self.add_row(N_('Diff Tool'), self.difftool)
234 self.add_row(N_('Merge Tool'), self.mergetool)
235 self.add_row(N_('Recent repository count'), self.maxrecent)
236 self.add_row(N_('Auto-Wrap Lines'), self.linebreak)
237 self.add_row(N_('Insert spaces instead of tabs'), self.expandtab)
238 self.add_row(N_('Sort bookmarks alphabetically'), self.sort_bookmarks)
239 self.add_row(N_('Keep *.orig Merge Backups'), self.keep_merge_backups)
240 self.add_row(N_('Save GUI Settings'), self.save_window_settings)
241 self.add_row(N_('Resize File Browser columns'), self.resize_browser_columns)
242 self.add_row(N_('Check spelling'), self.check_spelling)
243 self.add_row(N_('Ctrl+MouseWheel to Zoom'), self.mouse_zoom)
245 self.set_config(
247 prefs.SAVEWINDOWSETTINGS: (
248 self.save_window_settings,
249 Defaults.save_window_settings,
251 prefs.TABWIDTH: (self.tabwidth, Defaults.tabwidth),
252 prefs.EXPANDTAB: (self.expandtab, Defaults.expandtab),
253 prefs.TEXTWIDTH: (self.textwidth, Defaults.textwidth),
254 prefs.LINEBREAK: (self.linebreak, Defaults.linebreak),
255 prefs.MAXRECENT: (self.maxrecent, Defaults.maxrecent),
256 prefs.SORT_BOOKMARKS: (self.sort_bookmarks, Defaults.sort_bookmarks),
257 prefs.DIFFTOOL: (self.difftool, Defaults.difftool),
258 prefs.EDITOR: (self.editor, fallback_editor()),
259 prefs.HISTORY_BROWSER: (
260 self.historybrowser,
261 prefs.default_history_browser(),
263 prefs.BLAME_VIEWER: (self.blameviewer, Defaults.blame_viewer),
264 prefs.MERGE_KEEPBACKUP: (
265 self.keep_merge_backups,
266 Defaults.merge_keep_backup,
268 prefs.MERGETOOL: (self.mergetool, Defaults.mergetool),
269 prefs.RESIZE_BROWSER_COLUMNS: (
270 self.resize_browser_columns,
271 Defaults.resize_browser_columns,
273 prefs.SPELL_CHECK: (self.check_spelling, Defaults.spellcheck),
274 prefs.MOUSE_ZOOM: (self.mouse_zoom, Defaults.mouse_zoom),
278 # pylint: disable=no-member
279 self.fixed_font.currentFontChanged.connect(self.current_font_changed)
280 self.font_size.valueChanged.connect(self.font_size_changed)
282 def update_from_config(self):
283 """Update widgets to the current config values"""
284 FormWidget.update_from_config(self)
285 context = self.context
287 with qtutils.BlockSignals(self.fixed_font):
288 font = qtutils.diff_font(context)
289 self.fixed_font.setCurrentFont(font)
291 with qtutils.BlockSignals(self.font_size):
292 font_size = font.pointSize()
293 self.font_size.setValue(font_size)
295 def font_size_changed(self, size):
296 font = self.fixed_font.currentFont()
297 font.setPointSize(size)
298 cmds.do(prefs.SetConfig, self.model, 'global', prefs.FONTDIFF, font.toString())
300 def current_font_changed(self, font):
301 cmds.do(prefs.SetConfig, self.model, 'global', prefs.FONTDIFF, font.toString())
304 class AppearanceFormWidget(FormWidget):
305 def __init__(self, context, model, parent):
306 FormWidget.__init__(self, context, model, parent)
307 # Theme selectors
308 self.theme = qtutils.combo_mapped(themes.options())
309 self.icon_theme = qtutils.combo_mapped(icons.icon_themes())
311 # The transform to ustr is needed because the config reader will convert
312 # "0", "1", and "2" into integers. The "1.5" value, though, is
313 # parsed as a string, so the transform is effectively a no-op.
314 self.high_dpi = qtutils.combo_mapped(hidpi.options(), transform=ustr)
315 self.high_dpi.setEnabled(hidpi.is_supported())
316 self.bold_headers = qtutils.checkbox()
317 self.status_show_totals = qtutils.checkbox()
318 self.status_indent = qtutils.checkbox()
319 self.block_cursor = qtutils.checkbox(checked=True)
321 self.add_row(N_('GUI theme'), self.theme)
322 self.add_row(N_('Icon theme'), self.icon_theme)
323 self.add_row(N_('High DPI'), self.high_dpi)
324 self.add_row(N_('Bold on dark headers instead of italic'), self.bold_headers)
325 self.add_row(N_('Show file counts in Status titles'), self.status_show_totals)
326 self.add_row(N_('Indent Status paths'), self.status_indent)
327 self.add_row(N_('Use a block cursor in diff editors'), self.block_cursor)
329 self.set_config(
331 prefs.BOLD_HEADERS: (self.bold_headers, Defaults.bold_headers),
332 prefs.HIDPI: (self.high_dpi, Defaults.hidpi),
333 prefs.STATUS_SHOW_TOTALS: (
334 self.status_show_totals,
335 Defaults.status_show_totals,
337 prefs.STATUS_INDENT: (self.status_indent, Defaults.status_indent),
338 prefs.THEME: (self.theme, Defaults.theme),
339 prefs.ICON_THEME: (self.icon_theme, Defaults.icon_theme),
340 prefs.BLOCK_CURSOR: (self.block_cursor, Defaults.block_cursor),
345 class AppearanceWidget(QtWidgets.QWidget):
346 def __init__(self, form, parent):
347 QtWidgets.QWidget.__init__(self, parent)
348 self.form = form
349 self.label = QtWidgets.QLabel(
350 '<center><b>'
351 + N_('Restart the application after changing appearance settings.')
352 + '</b></center>'
354 layout = qtutils.vbox(
355 defs.margin,
356 defs.spacing,
357 self.form,
358 defs.spacing * 4,
359 self.label,
360 qtutils.STRETCH,
362 self.setLayout(layout)
364 def update_from_config(self):
365 self.form.update_from_config()
368 class PreferencesView(standard.Dialog):
369 def __init__(self, context, model, parent=None):
370 standard.Dialog.__init__(self, parent=parent)
371 self.context = context
372 self.setWindowTitle(N_('Preferences'))
373 if parent is not None:
374 self.setWindowModality(QtCore.Qt.WindowModal)
376 self.resize(600, 360)
378 self.tab_bar = QtWidgets.QTabBar()
379 self.tab_bar.setDrawBase(False)
380 self.tab_bar.addTab(N_('All Repositories'))
381 self.tab_bar.addTab(N_('Current Repository'))
382 self.tab_bar.addTab(N_('Settings'))
383 self.tab_bar.addTab(N_('Appearance'))
385 self.user_form = RepoFormWidget(context, model, self, source='global')
386 self.repo_form = RepoFormWidget(context, model, self, source='local')
387 self.options_form = SettingsFormWidget(context, model, self)
388 self.appearance_form = AppearanceFormWidget(context, model, self)
389 self.appearance = AppearanceWidget(self.appearance_form, self)
391 self.stack_widget = QtWidgets.QStackedWidget()
392 self.stack_widget.addWidget(self.user_form)
393 self.stack_widget.addWidget(self.repo_form)
394 self.stack_widget.addWidget(self.options_form)
395 self.stack_widget.addWidget(self.appearance)
397 self.close_button = qtutils.close_button()
399 self.button_layout = qtutils.hbox(
400 defs.no_margin, defs.spacing, qtutils.STRETCH, self.close_button
403 self.main_layout = qtutils.vbox(
404 defs.margin,
405 defs.spacing,
406 self.tab_bar,
407 self.stack_widget,
408 self.button_layout,
410 self.setLayout(self.main_layout)
412 # pylint: disable=no-member
413 self.tab_bar.currentChanged.connect(self.stack_widget.setCurrentIndex)
414 self.stack_widget.currentChanged.connect(self.update_widget)
416 qtutils.connect_button(self.close_button, self.accept)
417 qtutils.add_close_action(self)
419 self.update_widget(0)
421 def update_widget(self, idx):
422 widget = self.stack_widget.widget(idx)
423 widget.update_from_config()