1# Copyright (C) 2002-2009 Stephen Kennedy <stevek@gnome.org> 2# Copyright (C) 2010-2013 Kai Willadsen <kai.willadsen@gmail.com> 3# 4# This program is free software: you can redistribute it and/or modify 5# it under the terms of the GNU General Public License as published by 6# the Free Software Foundation, either version 2 of the License, or (at 7# your option) any later version. 8# 9# This program is distributed in the hope that it will be useful, but 10# WITHOUT ANY WARRANTY; without even the implied warranty of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12# General Public License for more details. 13# 14# You should have received a copy of the GNU General Public License 15# along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17from gi.repository import Gio 18from gi.repository import GLib 19from gi.repository import GObject 20from gi.repository import Gtk 21from gi.repository import GtkSource 22 23from meld.conf import _ 24from meld.filters import FilterEntry 25from meld.settings import settings 26from meld.ui.gnomeglade import Component 27from meld.ui.listwidget import ListWidget 28 29 30class FilterList(ListWidget): 31 32 def __init__(self, key, filter_type): 33 default_entry = [_("label"), False, _("pattern"), True] 34 super().__init__( 35 "EditableList.ui", "list_vbox", ["EditableListStore"], 36 "EditableList", default_entry) 37 self.key = key 38 self.filter_type = filter_type 39 40 self.pattern_column.set_cell_data_func( 41 self.validity_renderer, self.valid_icon_celldata) 42 43 for filter_params in settings.get_value(self.key): 44 filt = FilterEntry.new_from_gsetting(filter_params, filter_type) 45 if filt is None: 46 continue 47 valid = filt.filter is not None 48 self.model.append( 49 [filt.label, filt.active, filt.filter_string, valid]) 50 51 for signal in ('row-changed', 'row-deleted', 'row-inserted', 52 'rows-reordered'): 53 self.model.connect(signal, self._update_filter_string) 54 55 self._update_sensitivity() 56 57 def valid_icon_celldata(self, col, cell, model, it, user_data=None): 58 is_valid = model.get_value(it, 3) 59 icon_name = "gtk-dialog-warning" if not is_valid else None 60 cell.set_property("stock-id", icon_name) 61 62 def on_name_edited(self, ren, path, text): 63 self.model[path][0] = text 64 65 def on_cellrenderertoggle_toggled(self, ren, path): 66 self.model[path][1] = not ren.get_active() 67 68 def on_pattern_edited(self, ren, path, text): 69 valid = FilterEntry.check_filter(text, self.filter_type) 70 self.model[path][2] = text 71 self.model[path][3] = valid 72 73 def _update_filter_string(self, *args): 74 value = [(row[0], row[1], row[2]) for row in self.model] 75 settings.set_value(self.key, GLib.Variant('a(sbs)', value)) 76 77 78class ColumnList(ListWidget): 79 80 available_columns = { 81 "size": _("Size"), 82 "modification time": _("Modification time"), 83 "permissions": _("Permissions"), 84 } 85 86 def __init__(self, key): 87 super().__init__( 88 "EditableList.ui", "columns_ta", ["ColumnsListStore"], 89 "columns_treeview") 90 self.key = key 91 92 # Unwrap the variant 93 prefs_columns = [(k, v) for k, v in settings.get_value(self.key)] 94 column_vis = {} 95 column_order = {} 96 for sort_key, (column_name, visibility) in enumerate(prefs_columns): 97 column_vis[column_name] = bool(int(visibility)) 98 column_order[column_name] = sort_key 99 100 columns = [ 101 (column_vis.get(name, True), name, label) 102 for name, label in self.available_columns.items() 103 ] 104 columns = sorted(columns, key=lambda c: column_order.get(c[1], 0)) 105 106 for visibility, name, label in columns: 107 self.model.append([visibility, name, label]) 108 109 for signal in ('row-changed', 'row-deleted', 'row-inserted', 110 'rows-reordered'): 111 self.model.connect(signal, self._update_columns) 112 113 self._update_sensitivity() 114 115 def on_cellrenderertoggle_toggled(self, ren, path): 116 self.model[path][0] = not ren.get_active() 117 118 def _update_columns(self, *args): 119 value = [(c[1].lower(), c[0]) for c in self.model] 120 settings.set_value(self.key, GLib.Variant('a(sb)', value)) 121 122 123class GSettingsComboBox(Gtk.ComboBox): 124 125 def __init__(self): 126 super().__init__() 127 self.connect('notify::gsettings-value', self._setting_changed) 128 self.connect('notify::active', self._active_changed) 129 130 def bind_to(self, key): 131 settings.bind( 132 key, self, 'gsettings-value', Gio.SettingsBindFlags.DEFAULT) 133 134 def _setting_changed(self, obj, val): 135 column = self.get_property('gsettings-column') 136 value = self.get_property('gsettings-value') 137 138 for row in self.get_model(): 139 if value == row[column]: 140 idx = row.path[0] 141 break 142 else: 143 idx = 0 144 145 if self.get_property('active') != idx: 146 self.set_property('active', idx) 147 148 def _active_changed(self, obj, val): 149 active_iter = self.get_active_iter() 150 if active_iter is None: 151 return 152 column = self.get_property('gsettings-column') 153 value = self.get_model()[active_iter][column] 154 self.set_property('gsettings-value', value) 155 156 157class GSettingsIntComboBox(GSettingsComboBox): 158 159 __gtype_name__ = "GSettingsIntComboBox" 160 161 gsettings_column = GObject.Property(type=int, default=0) 162 gsettings_value = GObject.Property(type=int) 163 164 165class GSettingsBoolComboBox(GSettingsComboBox): 166 167 __gtype_name__ = "GSettingsBoolComboBox" 168 169 gsettings_column = GObject.Property(type=int, default=0) 170 gsettings_value = GObject.Property(type=bool, default=False) 171 172 173class GSettingsStringComboBox(GSettingsComboBox): 174 175 __gtype_name__ = "GSettingsStringComboBox" 176 177 gsettings_column = GObject.Property(type=int, default=0) 178 gsettings_value = GObject.Property(type=str, default="") 179 180 181class PreferencesDialog(Component): 182 183 def __init__(self, parent): 184 super().__init__( 185 "preferences.ui", "preferencesdialog", [ 186 "adjustment1", "adjustment2", "fileorderstore", 187 "sizegroup_editor", "timestampstore", "mergeorderstore", 188 "sizegroup_file_order_labels", "sizegroup_file_order_combos", 189 "syntaxschemestore" 190 ]) 191 self.widget.set_transient_for(parent) 192 193 bindings = [ 194 ('use-system-font', self.checkbutton_default_font, 'active'), 195 ('custom-font', self.fontpicker, 'font'), 196 ('indent-width', self.spinbutton_tabsize, 'value'), 197 ('insert-spaces-instead-of-tabs', self.checkbutton_spaces_instead_of_tabs, 'active'), # noqa: E501 198 ('highlight-current-line', self.checkbutton_highlight_current_line, 'active'), # noqa: E501 199 ('show-line-numbers', self.checkbutton_show_line_numbers, 'active'), # noqa: E501 200 ('highlight-syntax', self.checkbutton_use_syntax_highlighting, 'active'), # noqa: E501 201 ('use-system-editor', self.system_editor_checkbutton, 'active'), 202 ('custom-editor-command', self.custom_edit_command_entry, 'text'), 203 ('folder-shallow-comparison', self.checkbutton_shallow_compare, 'active'), # noqa: E501 204 ('folder-filter-text', self.checkbutton_folder_filter_text, 'active'), # noqa: E501 205 ('folder-ignore-symlinks', self.checkbutton_ignore_symlinks, 'active'), # noqa: E501 206 ('vc-show-commit-margin', self.checkbutton_show_commit_margin, 'active'), # noqa: E501 207 ('vc-commit-margin', self.spinbutton_commit_margin, 'value'), 208 ('vc-break-commit-message', self.checkbutton_break_commit_lines, 'active'), # noqa: E501 209 ('ignore-blank-lines', self.checkbutton_ignore_blank_lines, 'active'), # noqa: E501 210 # Sensitivity bindings must come after value bindings, or the key 211 # writability in gsettings overrides manual sensitivity setting. 212 ('vc-show-commit-margin', self.spinbutton_commit_margin, 'sensitive'), # noqa: E501 213 ('vc-show-commit-margin', self.checkbutton_break_commit_lines, 'sensitive'), # noqa: E501 214 ] 215 for key, obj, attribute in bindings: 216 settings.bind(key, obj, attribute, Gio.SettingsBindFlags.DEFAULT) 217 218 invert_bindings = [ 219 ('use-system-editor', self.custom_edit_command_entry, 'sensitive'), 220 ('use-system-font', self.fontpicker, 'sensitive'), 221 ('folder-shallow-comparison', self.checkbutton_folder_filter_text, 'sensitive'), # noqa: E501 222 ] 223 for key, obj, attribute in invert_bindings: 224 settings.bind( 225 key, obj, attribute, Gio.SettingsBindFlags.DEFAULT | 226 Gio.SettingsBindFlags.INVERT_BOOLEAN) 227 228 self.checkbutton_wrap_text.bind_property( 229 'active', self.checkbutton_wrap_word, 'sensitive', 230 GObject.BindingFlags.DEFAULT) 231 232 # TODO: Fix once bind_with_mapping is available 233 self.checkbutton_show_whitespace.set_active( 234 bool(settings.get_flags('draw-spaces'))) 235 236 wrap_mode = settings.get_enum('wrap-mode') 237 self.checkbutton_wrap_text.set_active(wrap_mode != Gtk.WrapMode.NONE) 238 self.checkbutton_wrap_word.set_active(wrap_mode == Gtk.WrapMode.WORD) 239 240 filefilter = FilterList("filename-filters", FilterEntry.SHELL) 241 self.file_filters_vbox.pack_start(filefilter.widget, True, True, 0) 242 243 textfilter = FilterList("text-filters", FilterEntry.REGEX) 244 self.text_filters_vbox.pack_start(textfilter.widget, True, True, 0) 245 246 columnlist = ColumnList("folder-columns") 247 self.column_list_vbox.pack_start(columnlist.widget, True, True, 0) 248 249 self.combo_timestamp.bind_to('folder-time-resolution') 250 self.combo_file_order.bind_to('vc-left-is-local') 251 self.combo_merge_order.bind_to('vc-merge-file-order') 252 253 # Fill color schemes 254 manager = GtkSource.StyleSchemeManager.get_default() 255 for scheme_id in manager.get_scheme_ids(): 256 scheme = manager.get_scheme(scheme_id) 257 self.syntaxschemestore.append([scheme_id, scheme.get_name()]) 258 self.combobox_style_scheme.bind_to('style-scheme') 259 260 self.widget.show() 261 262 def on_checkbutton_wrap_text_toggled(self, button): 263 if not self.checkbutton_wrap_text.get_active(): 264 wrap_mode = Gtk.WrapMode.NONE 265 elif self.checkbutton_wrap_word.get_active(): 266 wrap_mode = Gtk.WrapMode.WORD 267 else: 268 wrap_mode = Gtk.WrapMode.CHAR 269 settings.set_enum('wrap-mode', wrap_mode) 270 271 def on_checkbutton_show_whitespace_toggled(self, widget): 272 value = GtkSource.DrawSpacesFlags.ALL if widget.get_active() else 0 273 settings.set_flags('draw-spaces', value) 274 275 def on_response(self, dialog, response_id): 276 self.widget.destroy() 277