preferences.py 17.9 KB
Newer Older
1
### Copyright (C) 2002-2009 Stephen Kennedy <stevek@gnome.org>
2
### Copyright (C) 2010-2011 Kai Willadsen <kai.willadsen@gmail.com>
3 4 5 6 7 8 9 10 11 12 13 14 15

### This program is free software; you can redistribute it and/or modify
### it under the terms of the GNU General Public License as published by
### the Free Software Foundation; either version 2 of the License, or
### (at your option) any later version.

### This program is distributed in the hope that it will be useful,
### but WITHOUT ANY WARRANTY; without even the implied warranty of
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
### GNU General Public License for more details.

### You should have received a copy of the GNU General Public License
### along with this program; if not, write to the Free Software
16 17
### Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
### USA.
18

19 20 21
import logging
import shlex
import string
22 23 24 25 26

from gettext import gettext as _

import gtk

27 28 29 30 31 32 33 34 35
from . import filters
from . import misc
from . import paths
from . import vc
from .ui import gnomeglade
from .ui import listwidget
from .util import prefs

from .util.sourceviewer import srcviewer
36 37


38 39 40 41 42
TIMESTAMP_RESOLUTION_PRESETS = [('1ns (ext4)', 1),
                                ('100ns (NTFS)', 100),
                                ('1s (ext2/ext3)', 1000000000),
                                ('2s (VFAT)', 2000000000)]

43 44
log = logging.getLogger(__name__)

45

46 47
class FilterList(listwidget.ListWidget):

48
    def __init__(self, prefs, key, filter_type):
49
        default_entry = [_("label"), False, _("pattern"), True]
50 51 52
        listwidget.ListWidget.__init__(self, "EditableList.ui",
                                       "list_alignment", ["EditableListStore"],
                                       "EditableList", default_entry)
53 54
        self.prefs = prefs
        self.key = key
55 56 57 58
        self.filter_type = filter_type

        self.pattern_column.set_cell_data_func(self.validity_renderer,
                                               self.valid_icon_celldata)
59 60

        for filtstring in getattr(self.prefs, self.key).split("\n"):
61
            filt = filters.FilterEntry.parse(filtstring, filter_type)
62 63
            if filt is None:
                continue
64 65 66
            valid = filt.filter is not None
            self.model.append([filt.label, filt.active,
                               filt.filter_string, valid])
67 68 69 70 71

        for signal in ('row-changed', 'row-deleted', 'row-inserted',
                       'rows-reordered'):
            self.model.connect(signal, self._update_filter_string)

72
        self._update_sensitivity()
73

74 75 76 77 78
    def valid_icon_celldata(self, col, cell, model, it, user_data=None):
        is_valid = model.get_value(it, 3)
        icon_name = "gtk-dialog-warning" if not is_valid else None
        cell.set_property("stock-id", icon_name)

79 80 81 82 83 84 85
    def on_name_edited(self, ren, path, text):
        self.model[path][0] = text

    def on_cellrenderertoggle_toggled(self, ren, path):
        self.model[path][1] = not ren.get_active()

    def on_pattern_edited(self, ren, path, text):
86
        filt = filters.FilterEntry.compile_filter(text, self.filter_type)
87
        valid = filt is not None
88
        self.model[path][2] = text
89
        self.model[path][3] = valid
90 91

    def _update_filter_string(self, *args):
92
        pref = []
93
        for row in self.model:
94 95 96 97 98
            pattern = row[2]
            if pattern:
                pattern = pattern.replace('\r', '')
                pattern = pattern.replace('\n', '')
            pref.append("%s\t%s\t%s" % (row[0], 1 if row[1] else 0, pattern))
99 100
        setattr(self.prefs, self.key, "\n".join(pref))

101

102 103
class ColumnList(listwidget.ListWidget):

104 105 106
    available_columns = set((
        "size",
        "modification time",
107
        "permissions",
108 109
    ))

110 111 112 113 114 115 116
    def __init__(self, prefs, key):
        listwidget.ListWidget.__init__(self, "EditableList.ui",
                               "columns_ta", ["ColumnsListStore"],
                               "columns_treeview")
        self.prefs = prefs
        self.key = key

117
        prefs_columns = []
118 119 120
        for column in getattr(self.prefs, self.key):
            column_name, visibility = column.rsplit(" ", 1)
            visibility = bool(int(visibility))
121 122 123 124 125
            prefs_columns.append((column_name, visibility))

        missing = self.available_columns - set([c[0] for c in prefs_columns])
        prefs_columns.extend([(m, False) for m in missing])
        for column_name, visibility in prefs_columns:
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
            self.model.append([visibility, _(column_name.capitalize())])

        for signal in ('row-changed', 'row-deleted', 'row-inserted',
                       'rows-reordered'):
            self.model.connect(signal, self._update_columns)

        self._update_sensitivity()

    def on_cellrenderertoggle_toggled(self, ren, path):
        self.model[path][0] = not ren.get_active()

    def _update_columns(self, *args):
        columns = ["%s %d" % (c[1].lower(), int(c[0])) for c in self.model]
        setattr(self.prefs, self.key, columns)


142 143
class PreferencesDialog(gnomeglade.Component):

144
    def __init__(self, parent, prefs):
145 146
        gnomeglade.Component.__init__(self, paths.ui_dir("preferences.ui"),
                                      "preferencesdialog", ["adjustment1"])
147 148
        self.widget.set_transient_for(parent)
        self.prefs = prefs
149 150 151
        if not self.prefs.use_custom_font:
            self.checkbutton_default_font.set_active(True)
            self.fontpicker.set_sensitive(False)
152
        else:
153 154 155
            self.checkbutton_default_font.set_active(False)
            self.fontpicker.set_sensitive(True)
            self.fontpicker.set_font_name(self.prefs.custom_font)
156 157
        self.fontpicker.set_font_name( self.prefs.custom_font )
        self.spinbutton_tabsize.set_value( self.prefs.tab_size )
158
        if srcviewer.gsv is not None:
159 160
            self.checkbutton_spaces_instead_of_tabs.set_active( self.prefs.spaces_instead_of_tabs )
            self.checkbutton_show_line_numbers.set_active( self.prefs.show_line_numbers )
161
            self.checkbutton_show_whitespace.set_active(self.prefs.show_whitespace)
162 163
            self.checkbutton_use_syntax_highlighting.set_active( self.prefs.use_syntax_highlighting )
        else:
164 165 166 167
            no_sourceview_text = \
                _("Only available if you have gnome-python-desktop installed")
            for w in (self.checkbutton_spaces_instead_of_tabs,
                      self.checkbutton_show_line_numbers,
168 169
                      self.checkbutton_use_syntax_highlighting,
                      self.checkbutton_show_whitespace):
170 171
                w.set_sensitive(False)
                w.set_tooltip_text(no_sourceview_text)
172 173 174 175 176 177
        # TODO: This doesn't restore the state of character wrapping when word
        # wrapping is disabled, but this is hard with our existing gconf keys
        if self.prefs.edit_wrap_lines != gtk.WRAP_NONE:
            if self.prefs.edit_wrap_lines == gtk.WRAP_CHAR:
                self.checkbutton_split_words.set_active(False)
            self.checkbutton_wrap_text.set_active(True)
178 179 180 181 182

        size_group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
        size_group.add_widget(self.label1)
        size_group.add_widget(self.label2)
        size_group.add_widget(self.label16)
183 184 185 186
        use_default = self.prefs.edit_command_type == "internal" or \
                      self.prefs.edit_command_type == "gnome"
        self.system_editor_checkbutton.set_active(use_default)
        self.custom_edit_command_entry.set_sensitive(not use_default)
187
        self.custom_edit_command_entry.set_text(self.prefs.edit_command_custom)
188

189
        # file filters
190
        self.filefilter = FilterList(self.prefs, "filters",
191
                                     filters.FilterEntry.SHELL)
192 193
        self.file_filters_tab.pack_start(self.filefilter.widget)
        self.checkbutton_ignore_symlinks.set_active( self.prefs.ignore_symlinks)
194

195
        # text filters
196
        self.textfilter = FilterList(self.prefs, "regexes",
197
                                     filters.FilterEntry.REGEX)
198 199 200 201
        self.text_filters_tab.pack_start(self.textfilter.widget)
        self.checkbutton_ignore_blank_lines.set_active( self.prefs.ignore_blank_lines )
        # encoding
        self.entry_text_codecs.set_text( self.prefs.text_codecs )
202

203 204
        columnlist = ColumnList(self.prefs, "dirdiff_columns")
        self.column_list_vbox.pack_start(columnlist.widget)
205

206 207
        self.checkbutton_shallow_compare.set_active(
                self.prefs.dirdiff_shallow_comparison)
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222

        self.combo_timestamp.lock = True
        model = gtk.ListStore(str, int)
        active_idx = 0
        for i, entry in enumerate(TIMESTAMP_RESOLUTION_PRESETS):
            model.append(entry)
            if entry[1] == self.prefs.dirdiff_time_resolution_ns:
                active_idx = i
        self.combo_timestamp.set_model(model)
        cell = gtk.CellRendererText()
        self.combo_timestamp.pack_start(cell, False)
        self.combo_timestamp.add_attribute(cell, 'text', 0)
        self.combo_timestamp.set_active(active_idx)
        self.combo_timestamp.lock = False

223
        self.widget.show()
224

225 226
    def on_fontpicker_font_set(self, picker):
        self.prefs.custom_font = picker.get_font_name()
227 228 229 230 231 232

    def on_checkbutton_default_font_toggled(self, button):
        use_custom = not button.get_active()
        self.fontpicker.set_sensitive(use_custom)
        self.prefs.use_custom_font = use_custom

233 234 235 236
    def on_spinbutton_tabsize_changed(self, spin):
        self.prefs.tab_size = int(spin.get_value())
    def on_checkbutton_spaces_instead_of_tabs_toggled(self, check):
        self.prefs.spaces_instead_of_tabs = check.get_active()
237 238 239 240 241 242 243 244 245 246 247 248

    def on_checkbutton_wrap_text_toggled(self, button):
        if not self.checkbutton_wrap_text.get_active():
            self.prefs.edit_wrap_lines = 0
            self.checkbutton_split_words.set_sensitive(False)
        else:
            self.checkbutton_split_words.set_sensitive(True)
            if self.checkbutton_split_words.get_active():
                self.prefs.edit_wrap_lines = 2
            else:
                self.prefs.edit_wrap_lines = 1

249 250
    def on_checkbutton_show_line_numbers_toggled(self, check):
        self.prefs.show_line_numbers = check.get_active()
251 252
    def on_checkbutton_show_whitespace_toggled(self, check):
        self.prefs.show_whitespace = check.get_active()
253 254
    def on_checkbutton_use_syntax_highlighting_toggled(self, check):
        self.prefs.use_syntax_highlighting = check.get_active()
255 256 257 258 259 260 261 262 263

    def on_system_editor_checkbutton_toggled(self, check):
        use_default = check.get_active()
        self.custom_edit_command_entry.set_sensitive(not use_default)
        if use_default:
            self.prefs.edit_command_type = "gnome"
        else:
            self.prefs.edit_command_type = "custom"

264 265 266 267
    def on_custom_edit_command_entry_activate(self, entry, *args):
        # Called on "activate" and "focus-out-event"
        self.prefs.edit_command_custom = entry.props.text

268 269 270 271 272 273 274 275
    #
    # filters
    #
    def on_checkbutton_ignore_symlinks_toggled(self, check):
        self.prefs.ignore_symlinks = check.get_active()
    def on_checkbutton_ignore_blank_lines_toggled(self, check):
        self.prefs.ignore_blank_lines = check.get_active()

276 277 278 279
    def on_entry_text_codecs_activate(self, entry, *args):
        # Called on "activate" and "focus-out-event"
        self.prefs.text_codecs = entry.props.text

280 281
    def on_checkbutton_shallow_compare_toggled(self, check):
        self.prefs.dirdiff_shallow_comparison = check.get_active()
282

283 284
    def on_combo_timestamp_changed(self, combo):
        if not combo.lock:
285 286 287
            resolution = combo.get_model()[combo.get_active_iter()][1]
            self.prefs.dirdiff_time_resolution_ns = resolution

288
    def on_response(self, dialog, response_id):
289 290 291 292 293 294 295 296 297 298 299 300
        self.widget.destroy()


class MeldPreferences(prefs.Preferences):
    defaults = {
        "window_size_x": prefs.Value(prefs.INT, 600),
        "window_size_y": prefs.Value(prefs.INT, 600),
        "use_custom_font": prefs.Value(prefs.BOOL,0),
        "custom_font": prefs.Value(prefs.STRING,"monospace, 14"),
        "tab_size": prefs.Value(prefs.INT, 4),
        "spaces_instead_of_tabs": prefs.Value(prefs.BOOL, False),
        "show_line_numbers": prefs.Value(prefs.BOOL, 0),
301
        "show_whitespace": prefs.Value(prefs.BOOL, False),
302 303
        "use_syntax_highlighting": prefs.Value(prefs.BOOL, 0),
        "edit_wrap_lines" : prefs.Value(prefs.INT, 0),
304
        "edit_command_type" : prefs.Value(prefs.STRING, "gnome"), #gnome, custom
305 306 307 308 309 310 311 312
        "edit_command_custom" : prefs.Value(prefs.STRING, "gedit"),
        "text_codecs": prefs.Value(prefs.STRING, "utf8 latin1"),
        "ignore_symlinks": prefs.Value(prefs.BOOL,0),
        "vc_console_visible": prefs.Value(prefs.BOOL, 0),
        "filters" : prefs.Value(prefs.STRING,
            #TRANSLATORS: translate this string ONLY to the first "\t", leave it and the following parts intact
            _("Backups\t1\t#*# .#* ~* *~ *.{orig,bak,swp}\n") + \
            #TRANSLATORS: translate this string ONLY to the first "\t", leave it and the following parts intact
313 314
            _("OS-specific metadata\t0\t.DS_Store ._* .Spotlight-V100 .Trashes Thumbs.db Desktop.ini\n") + \
            #TRANSLATORS: translate this string ONLY to the first "\t", leave it and the following parts intact
315 316
            _("Version Control\t1\t%s\n") % misc.shell_escape(' '.join(vc.get_plugins_metadata())) + \
            #TRANSLATORS: translate this string ONLY to the first "\t", leave it and the following parts intact
317
            _("Binaries\t1\t*.{pyc,a,obj,o,so,la,lib,dll,exe}\n") + \
318
            #TRANSLATORS: translate this string ONLY to the first "\t", leave it and the following parts intact
319
            _("Media\t0\t*.{jpg,gif,png,bmp,wav,mp3,ogg,flac,avi,mpg,xcf,xpm}")),
320 321 322 323 324 325 326 327 328 329 330 331 332 333
            #TRANSLATORS: translate this string ONLY to the first "\t", leave it and the following parts intact
        "regexes" : prefs.Value(prefs.STRING, _("CVS keywords\t0\t\$\\w+(:[^\\n$]+)?\$\n") + \
            #TRANSLATORS: translate this string ONLY to the first "\t", leave it and the following parts intact
            _("C++ comment\t0\t//.*\n") + \
            #TRANSLATORS: translate this string ONLY to the first "\t", leave it and the following parts intact
            _("C comment\t0\t/\*.*?\*/\n") + \
            #TRANSLATORS: translate this string ONLY to the first "\t", leave it and the following parts intact
            _("All whitespace\t0\t[ \\t\\r\\f\\v]*\n") + \
            #TRANSLATORS: translate this string ONLY to the first "\t", leave it and the following parts intact
            _("Leading whitespace\t0\t^[ \\t\\r\\f\\v]*\n") + \
            #TRANSLATORS: translate this string ONLY to the first "\t", leave it and the following parts intact
            _("Script comment\t0\t#.*")),
        "ignore_blank_lines" : prefs.Value(prefs.BOOL, False),
        "toolbar_visible" : prefs.Value(prefs.BOOL, True),
334 335 336
        "statusbar_visible" : prefs.Value(prefs.BOOL, True),
        "dir_status_filters": prefs.Value(prefs.LIST,
                                          ['normal', 'modified', 'new']),
337 338
        "vc_status_filters": prefs.Value(prefs.LIST,
                                         ['flatten', 'modified']),
339 340 341 342
        # Currently, we're using a quite simple format to store the columns:
        # each line contains a column name followed by a 1 or a 0
        # depending on whether the column is visible or not.
        "dirdiff_columns": prefs.Value(prefs.LIST,
343 344
                                         ["size 1", "modification time 1",
                                          "permissions 0"]),
345 346
        "dirdiff_shallow_comparison" : prefs.Value(prefs.BOOL, False),
        "dirdiff_time_resolution_ns" : prefs.Value(prefs.INT, 100),
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
    }

    def __init__(self):
        super(MeldPreferences, self).__init__("/apps/meld", self.defaults)

    def get_current_font(self):
        if self.use_custom_font:
            return self.custom_font
        else:
            if not hasattr(self, "_gconf"):
                return "Monospace 10"
            return self._gconf.get_string('/desktop/gnome/interface/monospace_font_name') or "Monospace 10"

    def get_toolbar_style(self):
        if not hasattr(self, "_gconf"):
362 363 364 365 366 367 368 369 370 371 372
            style = "both-horiz"
        else:
            style = self._gconf.get_string(
                      '/desktop/gnome/interface/toolbar_style') or "both-horiz"
        toolbar_styles = {
            "both": gtk.TOOLBAR_BOTH, "text": gtk.TOOLBAR_TEXT,
            "icon": gtk.TOOLBAR_ICONS, "icons": gtk.TOOLBAR_ICONS,
            "both_horiz": gtk.TOOLBAR_BOTH_HORIZ,
            "both-horiz": gtk.TOOLBAR_BOTH_HORIZ
        }
        return toolbar_styles[style]
373

374
    def get_editor_command(self, path, line=0):
375
        if self.edit_command_type == "custom":
376
            custom_command = self.edit_command_custom
377 378 379 380 381 382 383 384 385 386 387
            fmt = string.Formatter()
            replacements = [tok[1] for tok in fmt.parse(custom_command)]

            if not any(replacements):
                cmd = " ".join([custom_command, path])
            elif not all(r in (None, 'file', 'line') for r in replacements):
                cmd = " ".join([custom_command, path])
                log.error("Unsupported fields found", )
            else:
                cmd = custom_command.format(file=path, line=line)
            return shlex.split(cmd)
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
        else:
            if not hasattr(self, "_gconf"):
                return []

            editor_path = "/desktop/gnome/applications/editor/"
            terminal_path = "/desktop/gnome/applications/terminal/"
            editor = self._gconf.get_string(editor_path + "exec") or "gedit"
            if self._gconf.get_bool(editor_path + "needs_term"):
                argv = []
                texec = self._gconf.get_string(terminal_path + "exec")
                if texec:
                    argv.append(texec)
                    targ = self._gconf.get_string(terminal_path + "exec_arg")
                    if targ:
                        argv.append(targ)
403 404
                escaped_path = path.replace(" ", "\\ ")
                argv.append("%s %s" % (editor, escaped_path))
405 406
                return argv
            else:
407
                return [editor, path]