~ubuntu-branches/debian/experimental/spyder/experimental

« back to all changes in this revision

Viewing changes to spyderlib/plugins/configdialog.py

  • Committer: Package Import Robot
  • Author(s): Picca Frédéric-Emmanuel
  • Date: 2013-01-20 12:19:54 UTC
  • mfrom: (1.1.16)
  • Revision ID: package-import@ubuntu.com-20130120121954-1jt1xa924bshhvh0
Tags: 2.2.0~beta1+dfsg-2
fix typo ipython-qtconsol -> ipython-qtconsole

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
#
3
 
# Copyright © 2009-2010 Pierre Raybaut
4
 
# Licensed under the terms of the MIT License
5
 
# (see spyderlib/__init__.py for details)
6
 
 
7
 
"""Configuration dialog / Preferences"""
8
 
 
9
 
import os
10
 
import os.path as osp
11
 
 
12
 
from spyderlib.baseconfig import _
13
 
from spyderlib.config import (get_icon, CONF, CUSTOM_COLOR_SCHEME_NAME,
14
 
                              set_default_color_scheme, COLOR_SCHEME_NAMES)
15
 
from spyderlib.utils.qthelpers import get_std_icon
16
 
from spyderlib.userconfig import NoDefault
17
 
from spyderlib.widgets.colors import ColorLayout
18
 
 
19
 
from spyderlib.qt.QtGui import (QWidget, QDialog, QListWidget, QListWidgetItem,
20
 
                                QVBoxLayout, QStackedWidget, QListView,
21
 
                                QHBoxLayout, QDialogButtonBox, QCheckBox,
22
 
                                QMessageBox, QLabel, QLineEdit, QSpinBox,
23
 
                                QPushButton, QFontComboBox, QGroupBox,
24
 
                                QComboBox, QColor, QGridLayout, QTabWidget,
25
 
                                QRadioButton, QButtonGroup, QSplitter,
26
 
                                QStyleFactory, QScrollArea)
27
 
from spyderlib.qt.QtCore import Qt, QSize, SIGNAL, SLOT, Slot
28
 
from spyderlib.qt.compat import (to_qvariant, from_qvariant,
29
 
                                 getexistingdirectory, getopenfilename)
30
 
 
31
 
 
32
 
class ConfigPage(QWidget):
33
 
    """Configuration page base class"""
34
 
    def __init__(self, parent, apply_callback=None):
35
 
        QWidget.__init__(self, parent)
36
 
        self.apply_callback = apply_callback
37
 
        self.is_modified = False
38
 
        
39
 
    def initialize(self):
40
 
        """
41
 
        Initialize configuration page:
42
 
            * setup GUI widgets
43
 
            * load settings and change widgets accordingly
44
 
        """
45
 
        self.setup_page()
46
 
        self.load_from_conf()
47
 
        
48
 
    def get_name(self):
49
 
        """Return page name"""
50
 
        raise NotImplementedError
51
 
    
52
 
    def get_icon(self):
53
 
        """Return page icon"""
54
 
        raise NotImplementedError
55
 
    
56
 
    def setup_page(self):
57
 
        """Setup configuration page widget"""
58
 
        raise NotImplementedError
59
 
        
60
 
    def set_modified(self, state):
61
 
        self.is_modified = state
62
 
        self.emit(SIGNAL("apply_button_enabled(bool)"), state)
63
 
        
64
 
    def is_valid(self):
65
 
        """Return True if all widget contents are valid"""
66
 
        raise NotImplementedError
67
 
    
68
 
    def apply_changes(self):
69
 
        """Apply changes callback"""
70
 
        if self.is_modified:
71
 
            self.save_to_conf()
72
 
            if self.apply_callback is not None:
73
 
                self.apply_callback()
74
 
            self.set_modified(False)
75
 
    
76
 
    def load_from_conf(self):
77
 
        """Load settings from configuration file"""
78
 
        raise NotImplementedError
79
 
    
80
 
    def save_to_conf(self):
81
 
        """Save settings to configuration file"""
82
 
        raise NotImplementedError
83
 
 
84
 
 
85
 
class ConfigDialog(QDialog):
86
 
    """Spyder configuration ('Preferences') dialog box"""
87
 
    def __init__(self, parent=None):
88
 
        QDialog.__init__(self, parent)
89
 
        
90
 
        # Destroying the C++ object right after closing the dialog box,
91
 
        # otherwise it may be garbage-collected in another QThread
92
 
        # (e.g. the editor's analysis thread in Spyder), thus leading to
93
 
        # a segmentation fault on UNIX or an application crash on Windows
94
 
        self.setAttribute(Qt.WA_DeleteOnClose)
95
 
 
96
 
        self.contents_widget = QListWidget()
97
 
        self.contents_widget.setMovement(QListView.Static)
98
 
        self.contents_widget.setSpacing(1)
99
 
 
100
 
        bbox = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Apply
101
 
                                |QDialogButtonBox.Cancel)
102
 
        self.apply_btn = bbox.button(QDialogButtonBox.Apply)
103
 
        self.connect(bbox, SIGNAL("accepted()"), SLOT("accept()"))
104
 
        self.connect(bbox, SIGNAL("rejected()"), SLOT("reject()"))
105
 
        self.connect(bbox, SIGNAL("clicked(QAbstractButton*)"),
106
 
                     self.button_clicked)
107
 
 
108
 
        self.pages_widget = QStackedWidget()
109
 
        self.connect(self.pages_widget, SIGNAL("currentChanged(int)"),
110
 
                     self.current_page_changed)
111
 
 
112
 
        self.connect(self.contents_widget, SIGNAL("currentRowChanged(int)"),
113
 
                     self.pages_widget.setCurrentIndex)
114
 
        self.contents_widget.setCurrentRow(0)
115
 
 
116
 
        hsplitter = QSplitter()
117
 
        hsplitter.addWidget(self.contents_widget)
118
 
        hsplitter.addWidget(self.pages_widget)
119
 
 
120
 
        btnlayout = QHBoxLayout()
121
 
        btnlayout.addStretch(1)
122
 
        btnlayout.addWidget(bbox)
123
 
 
124
 
        vlayout = QVBoxLayout()
125
 
        vlayout.addWidget(hsplitter)
126
 
        vlayout.addLayout(btnlayout)
127
 
 
128
 
        self.setLayout(vlayout)
129
 
 
130
 
        self.setWindowTitle(_("Preferences"))
131
 
        self.setWindowIcon(get_icon("configure.png"))
132
 
        
133
 
    def get_current_index(self):
134
 
        """Return current page index"""
135
 
        return self.contents_widget.currentRow()
136
 
        
137
 
    def set_current_index(self, index):
138
 
        """Set current page index"""
139
 
        self.contents_widget.setCurrentRow(index)
140
 
        
141
 
    def get_page(self, index=None):
142
 
        """Return page widget"""
143
 
        if index is None:
144
 
            widget = self.pages_widget.currentWidget()
145
 
        else:
146
 
            widget = self.pages_widget.widget(index)
147
 
        return widget.widget()
148
 
        
149
 
    def accept(self):
150
 
        """Reimplement Qt method"""
151
 
        for index in range(self.pages_widget.count()):
152
 
            configpage = self.get_page(index)
153
 
            if not configpage.is_valid():
154
 
                return
155
 
            configpage.apply_changes()
156
 
        QDialog.accept(self)
157
 
        
158
 
    def button_clicked(self, button):
159
 
        if button is self.apply_btn:
160
 
            # Apply button was clicked
161
 
            configpage = self.get_page()
162
 
            if not configpage.is_valid():
163
 
                return
164
 
            configpage.apply_changes()
165
 
            
166
 
    def current_page_changed(self, index):
167
 
        widget = self.get_page(index)
168
 
        self.apply_btn.setVisible(widget.apply_callback is not None)
169
 
        self.apply_btn.setEnabled(widget.is_modified)
170
 
        
171
 
    def add_page(self, widget):
172
 
        self.connect(self, SIGNAL('check_settings()'), widget.check_settings)
173
 
        self.connect(widget, SIGNAL('show_this_page()'),
174
 
                     lambda row=self.contents_widget.count():
175
 
                     self.contents_widget.setCurrentRow(row))
176
 
        self.connect(widget, SIGNAL("apply_button_enabled(bool)"),
177
 
                     self.apply_btn.setEnabled)
178
 
        scrollarea = QScrollArea(self)
179
 
        scrollarea.setWidgetResizable(True)
180
 
        scrollarea.setWidget(widget)
181
 
        self.pages_widget.addWidget(scrollarea)
182
 
        item = QListWidgetItem(self.contents_widget)
183
 
        item.setIcon(widget.get_icon())
184
 
        item.setText(widget.get_name())
185
 
        item.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
186
 
        item.setSizeHint(QSize(0, 25))
187
 
        
188
 
    def check_all_settings(self):
189
 
        """This method is called to check all configuration page settings
190
 
        after configuration dialog has been shown"""
191
 
        self.emit(SIGNAL('check_settings()'))
192
 
 
193
 
 
194
 
class SpyderConfigPage(ConfigPage):
195
 
    """Plugin configuration dialog box page widget"""
196
 
    def __init__(self, parent):
197
 
        ConfigPage.__init__(self, parent,
198
 
                            apply_callback=lambda:
199
 
                            self.apply_settings(self.changed_options))
200
 
        self.checkboxes = {}
201
 
        self.radiobuttons = {}
202
 
        self.lineedits = {}
203
 
        self.validate_data = {}
204
 
        self.spinboxes = {}
205
 
        self.comboboxes = {}
206
 
        self.fontboxes = {}
207
 
        self.coloredits = {}
208
 
        self.scedits = {}
209
 
        self.changed_options = set()
210
 
        self.default_button_group = None
211
 
        
212
 
    def apply_settings(self, options):
213
 
        raise NotImplementedError
214
 
    
215
 
    def check_settings(self):
216
 
        """This method is called to check settings after configuration 
217
 
        dialog has been shown"""
218
 
        pass
219
 
        
220
 
    def set_modified(self, state):
221
 
        ConfigPage.set_modified(self, state)
222
 
        if not state:
223
 
            self.changed_options = set()
224
 
        
225
 
    def is_valid(self):
226
 
        """Return True if all widget contents are valid"""
227
 
        for lineedit in self.lineedits:
228
 
            if lineedit in self.validate_data and lineedit.isEnabled():
229
 
                validator, invalid_msg = self.validate_data[lineedit]
230
 
                text = unicode(lineedit.text())
231
 
                if not validator(text):
232
 
                    QMessageBox.critical(self, self.get_name(),
233
 
                                     "%s:<br><b>%s</b>" % (invalid_msg, text),
234
 
                                     QMessageBox.Ok)
235
 
                    return False
236
 
        return True
237
 
        
238
 
    def load_from_conf(self):
239
 
        """Load settings from configuration file"""
240
 
        for checkbox, (option, default) in self.checkboxes.items():
241
 
            checkbox.setChecked(self.get_option(option, default))
242
 
            self.connect(checkbox, SIGNAL("clicked(bool)"),
243
 
                         lambda _foo, opt=option: self.has_been_modified(opt))
244
 
        for radiobutton, (option, default) in self.radiobuttons.items():
245
 
            radiobutton.setChecked(self.get_option(option, default))
246
 
            self.connect(radiobutton, SIGNAL("toggled(bool)"),
247
 
                         lambda _foo, opt=option: self.has_been_modified(opt))
248
 
        for lineedit, (option, default) in self.lineedits.items():
249
 
            lineedit.setText(self.get_option(option, default))
250
 
            self.connect(lineedit, SIGNAL("textChanged(QString)"),
251
 
                         lambda _foo, opt=option: self.has_been_modified(opt))
252
 
        for spinbox, (option, default) in self.spinboxes.items():
253
 
            spinbox.setValue(self.get_option(option, default))
254
 
            self.connect(spinbox, SIGNAL('valueChanged(int)'),
255
 
                         lambda _foo, opt=option: self.has_been_modified(opt))
256
 
        for combobox, (option, default) in self.comboboxes.items():
257
 
            value = self.get_option(option, default)
258
 
            for index in range(combobox.count()):
259
 
                data = from_qvariant(combobox.itemData(index), unicode)
260
 
                # For PyQt API v2, it is necessary to convert `data` to 
261
 
                # unicode in case the original type was not a string, like an 
262
 
                # integer for example (see spyderlib.qt.compat.from_qvariant):
263
 
                if unicode(data) == unicode(value):
264
 
                    break
265
 
            combobox.setCurrentIndex(index)
266
 
            self.connect(combobox, SIGNAL('currentIndexChanged(int)'),
267
 
                         lambda _foo, opt=option: self.has_been_modified(opt))
268
 
        for (fontbox, sizebox), option in self.fontboxes.items():
269
 
            font = self.get_font(option)
270
 
            fontbox.setCurrentFont(font)
271
 
            sizebox.setValue(font.pointSize())
272
 
            if option is None:
273
 
                property = 'plugin_font'
274
 
            else:
275
 
                property = option
276
 
            self.connect(fontbox, SIGNAL('currentIndexChanged(int)'),
277
 
                         lambda _foo, opt=property: self.has_been_modified(opt))
278
 
            self.connect(sizebox, SIGNAL('valueChanged(int)'),
279
 
                         lambda _foo, opt=property: self.has_been_modified(opt))
280
 
        for clayout, (option, default) in self.coloredits.items():
281
 
            property = to_qvariant(option)
282
 
            edit = clayout.lineedit
283
 
            btn = clayout.colorbtn
284
 
            edit.setText(self.get_option(option, default))
285
 
            self.connect(btn, SIGNAL('clicked()'),
286
 
                         lambda opt=option: self.has_been_modified(opt))
287
 
            self.connect(edit, SIGNAL("textChanged(QString)"),
288
 
                         lambda _foo, opt=option: self.has_been_modified(opt))
289
 
        for (clayout, cb_bold, cb_italic
290
 
             ), (option, default) in self.scedits.items():
291
 
            edit = clayout.lineedit
292
 
            btn = clayout.colorbtn
293
 
            color, bold, italic = self.get_option(option, default)
294
 
            edit.setText(color)
295
 
            cb_bold.setChecked(bold)
296
 
            cb_italic.setChecked(italic)
297
 
            self.connect(btn, SIGNAL('clicked()'),
298
 
                         lambda opt=option: self.has_been_modified(opt))
299
 
            self.connect(edit, SIGNAL("textChanged(QString)"),
300
 
                         lambda _foo, opt=option: self.has_been_modified(opt))
301
 
            self.connect(cb_bold, SIGNAL("clicked(bool)"),
302
 
                         lambda _foo, opt=option: self.has_been_modified(opt))
303
 
            self.connect(cb_italic, SIGNAL("clicked(bool)"),
304
 
                         lambda _foo, opt=option: self.has_been_modified(opt))
305
 
    
306
 
    def save_to_conf(self):
307
 
        """Save settings to configuration file"""
308
 
        for checkbox, (option, _default) in self.checkboxes.items():
309
 
            self.set_option(option, checkbox.isChecked())
310
 
        for radiobutton, (option, _default) in self.radiobuttons.items():
311
 
            self.set_option(option, radiobutton.isChecked())
312
 
        for lineedit, (option, _default) in self.lineedits.items():
313
 
            self.set_option(option, unicode(lineedit.text()))
314
 
        for spinbox, (option, _default) in self.spinboxes.items():
315
 
            self.set_option(option, spinbox.value())
316
 
        for combobox, (option, _default) in self.comboboxes.items():
317
 
            data = combobox.itemData(combobox.currentIndex())
318
 
            self.set_option(option, from_qvariant(data, unicode))
319
 
        for (fontbox, sizebox), option in self.fontboxes.items():
320
 
            font = fontbox.currentFont()
321
 
            font.setPointSize(sizebox.value())
322
 
            self.set_font(font, option)
323
 
        for clayout, (option, _default) in self.coloredits.items():
324
 
            self.set_option(option, unicode(clayout.lineedit.text()))
325
 
        for (clayout, cb_bold, cb_italic), (option, _default) in self.scedits.items():
326
 
            color = unicode(clayout.lineedit.text())
327
 
            bold = cb_bold.isChecked()
328
 
            italic = cb_italic.isChecked()
329
 
            self.set_option(option, (color, bold, italic))
330
 
    
331
 
    @Slot(str)
332
 
    def has_been_modified(self, option):
333
 
        self.set_modified(True)
334
 
        self.changed_options.add(option)
335
 
    
336
 
    def create_checkbox(self, text, option, default=NoDefault,
337
 
                        tip=None, msg_warning=None, msg_info=None,
338
 
                        msg_if_enabled=False):
339
 
        checkbox = QCheckBox(text)
340
 
        if tip is not None:
341
 
            checkbox.setToolTip(tip)
342
 
        self.checkboxes[checkbox] = (option, default)
343
 
        if msg_warning is not None or msg_info is not None:
344
 
            def show_message(is_checked):
345
 
                if is_checked or not msg_if_enabled:
346
 
                    if msg_warning is not None:
347
 
                        QMessageBox.warning(self, self.get_name(),
348
 
                                            msg_warning, QMessageBox.Ok)
349
 
                    if msg_info is not None:
350
 
                        QMessageBox.information(self, self.get_name(),
351
 
                                                msg_info, QMessageBox.Ok)
352
 
            self.connect(checkbox, SIGNAL("clicked(bool)"), show_message)
353
 
        return checkbox
354
 
    
355
 
    def create_radiobutton(self, text, option, default=NoDefault,
356
 
                           tip=None, msg_warning=None, msg_info=None,
357
 
                           msg_if_enabled=False, button_group=None):
358
 
        radiobutton = QRadioButton(text)
359
 
        if button_group is None:
360
 
            if self.default_button_group is None:
361
 
                self.default_button_group = QButtonGroup(self)
362
 
            button_group = self.default_button_group
363
 
        button_group.addButton(radiobutton)
364
 
        if tip is not None:
365
 
            radiobutton.setToolTip(tip)
366
 
        self.radiobuttons[radiobutton] = (option, default)
367
 
        if msg_warning is not None or msg_info is not None:
368
 
            def show_message(is_checked):
369
 
                if is_checked or not msg_if_enabled:
370
 
                    if msg_warning is not None:
371
 
                        QMessageBox.warning(self, self.get_name(),
372
 
                                            msg_warning, QMessageBox.Ok)
373
 
                    if msg_info is not None:
374
 
                        QMessageBox.information(self, self.get_name(),
375
 
                                                msg_info, QMessageBox.Ok)
376
 
            self.connect(radiobutton, SIGNAL("toggled(bool)"), show_message)
377
 
        return radiobutton
378
 
    
379
 
    def create_lineedit(self, text, option, default=NoDefault,
380
 
                        tip=None, alignment=Qt.Vertical):
381
 
        label = QLabel(text)
382
 
        label.setWordWrap(True)
383
 
        edit = QLineEdit()
384
 
        layout = QVBoxLayout() if alignment == Qt.Vertical else QHBoxLayout()
385
 
        layout.addWidget(label)
386
 
        layout.addWidget(edit)
387
 
        layout.setContentsMargins(0, 0, 0, 0)
388
 
        if tip:
389
 
            edit.setToolTip(tip)
390
 
        self.lineedits[edit] = (option, default)
391
 
        widget = QWidget(self)
392
 
        widget.setLayout(layout)
393
 
        return widget
394
 
    
395
 
    def create_browsedir(self, text, option, default=NoDefault, tip=None):
396
 
        widget = self.create_lineedit(text, option, default,
397
 
                                      alignment=Qt.Horizontal)
398
 
        for edit in self.lineedits:
399
 
            if widget.isAncestorOf(edit):
400
 
                break
401
 
        msg = _("Invalid directory path")
402
 
        self.validate_data[edit] = (osp.isdir, msg)
403
 
        browse_btn = QPushButton(get_std_icon('DirOpenIcon'), "", self)
404
 
        browse_btn.setToolTip(_("Select directory"))
405
 
        self.connect(browse_btn, SIGNAL("clicked()"),
406
 
                     lambda: self.select_directory(edit))
407
 
        layout = QHBoxLayout()
408
 
        layout.addWidget(widget)
409
 
        layout.addWidget(browse_btn)
410
 
        layout.setContentsMargins(0, 0, 0, 0)
411
 
        browsedir = QWidget(self)
412
 
        browsedir.setLayout(layout)
413
 
        return browsedir
414
 
 
415
 
    def select_directory(self, edit):
416
 
        """Select directory"""
417
 
        basedir = unicode(edit.text())
418
 
        if not osp.isdir(basedir):
419
 
            basedir = os.getcwdu()
420
 
        title = _("Select directory")
421
 
        directory = getexistingdirectory(self, title, basedir)
422
 
        if directory:
423
 
            edit.setText(directory)
424
 
    
425
 
    def create_browsefile(self, text, option, default=NoDefault, tip=None,
426
 
                          filters=None):
427
 
        widget = self.create_lineedit(text, option, default,
428
 
                                      alignment=Qt.Horizontal)
429
 
        for edit in self.lineedits:
430
 
            if widget.isAncestorOf(edit):
431
 
                break
432
 
        msg = _("Invalid file path")
433
 
        self.validate_data[edit] = (osp.isfile, msg)
434
 
        browse_btn = QPushButton(get_std_icon('FileIcon'), "", self)
435
 
        browse_btn.setToolTip(_("Select file"))
436
 
        self.connect(browse_btn, SIGNAL("clicked()"),
437
 
                     lambda: self.select_file(edit, filters))
438
 
        layout = QHBoxLayout()
439
 
        layout.addWidget(widget)
440
 
        layout.addWidget(browse_btn)
441
 
        layout.setContentsMargins(0, 0, 0, 0)
442
 
        browsedir = QWidget(self)
443
 
        browsedir.setLayout(layout)
444
 
        return browsedir
445
 
 
446
 
    def select_file(self, edit, filters=None):
447
 
        """Select File"""
448
 
        basedir = osp.dirname(unicode(edit.text()))
449
 
        if not osp.isdir(basedir):
450
 
            basedir = os.getcwdu()
451
 
        if filters is None:
452
 
            filters = _("All files (*)")
453
 
        title = _("Select file")
454
 
        filename, _selfilter = getopenfilename(self, title, basedir, filters)
455
 
        if filename:
456
 
            edit.setText(filename)
457
 
    
458
 
    def create_spinbox(self, prefix, suffix, option, default=NoDefault,
459
 
                       min_=None, max_=None, step=None, tip=None):
460
 
        if prefix:
461
 
            plabel = QLabel(prefix)
462
 
        else:
463
 
            plabel = None
464
 
        if suffix:
465
 
            slabel = QLabel(suffix)
466
 
        else:
467
 
            slabel = None
468
 
        spinbox = QSpinBox()
469
 
        if min_ is not None:
470
 
            spinbox.setMinimum(min_)
471
 
        if max_ is not None:
472
 
            spinbox.setMaximum(max_)
473
 
        if step is not None:
474
 
            spinbox.setSingleStep(step)
475
 
        if tip is not None:
476
 
            spinbox.setToolTip(tip)
477
 
        self.spinboxes[spinbox] = (option, default)
478
 
        layout = QHBoxLayout()
479
 
        for subwidget in (plabel, spinbox, slabel):
480
 
            if subwidget is not None:
481
 
                layout.addWidget(subwidget)
482
 
        layout.addStretch(1)
483
 
        layout.setContentsMargins(0, 0, 0, 0)
484
 
        widget = QWidget(self)
485
 
        widget.setLayout(layout)
486
 
        return widget
487
 
    
488
 
    def create_coloredit(self, text, option, default=NoDefault, tip=None,
489
 
                         without_layout=False):
490
 
        label = QLabel(text)
491
 
        clayout = ColorLayout(QColor(Qt.black), self)
492
 
        clayout.lineedit.setMaximumWidth(80)
493
 
        if tip is not None:
494
 
            clayout.setToolTip(tip)
495
 
        self.coloredits[clayout] = (option, default)
496
 
        if without_layout:
497
 
            return label, clayout
498
 
        layout = QHBoxLayout()
499
 
        layout.addWidget(label)
500
 
        layout.addLayout(clayout)
501
 
        layout.addStretch(1)
502
 
        layout.setContentsMargins(0, 0, 0, 0)
503
 
        widget = QWidget(self)
504
 
        widget.setLayout(layout)
505
 
        return widget
506
 
    
507
 
    def create_scedit(self, text, option, default=NoDefault, tip=None,
508
 
                      without_layout=False):
509
 
        label = QLabel(text)
510
 
        clayout = ColorLayout(QColor(Qt.black), self)
511
 
        clayout.lineedit.setMaximumWidth(80)
512
 
        if tip is not None:
513
 
            clayout.setToolTip(tip)
514
 
        cb_bold = QCheckBox()
515
 
        cb_bold.setIcon(get_icon("bold.png"))
516
 
        cb_bold.setToolTip(_("Bold"))
517
 
        cb_italic = QCheckBox()
518
 
        cb_italic.setIcon(get_icon("italic.png"))
519
 
        cb_italic.setToolTip(_("Italic"))
520
 
        self.scedits[(clayout, cb_bold, cb_italic)] = (option, default)
521
 
        if without_layout:
522
 
            return label, clayout, cb_bold, cb_italic
523
 
        layout = QHBoxLayout()
524
 
        layout.addWidget(label)
525
 
        layout.addLayout(clayout)
526
 
        layout.addSpacing(10)
527
 
        layout.addWidget(cb_bold)
528
 
        layout.addWidget(cb_italic)
529
 
        layout.addStretch(1)
530
 
        layout.setContentsMargins(0, 0, 0, 0)
531
 
        widget = QWidget(self)
532
 
        widget.setLayout(layout)
533
 
        return widget
534
 
    
535
 
    def create_combobox(self, text, choices, option, default=NoDefault,
536
 
                        tip=None):
537
 
        """choices: couples (name, key)"""
538
 
        label = QLabel(text)
539
 
        combobox = QComboBox()
540
 
        if tip is not None:
541
 
            combobox.setToolTip(tip)
542
 
        for name, key in choices:
543
 
            combobox.addItem(name, to_qvariant(key))
544
 
        self.comboboxes[combobox] = (option, default)
545
 
        layout = QHBoxLayout()
546
 
        for subwidget in (label, combobox):
547
 
            layout.addWidget(subwidget)
548
 
        layout.addStretch(1)
549
 
        layout.setContentsMargins(0, 0, 0, 0)
550
 
        widget = QWidget(self)
551
 
        widget.setLayout(layout)
552
 
        return widget
553
 
    
554
 
    def create_fontgroup(self, option=None, text=None,
555
 
                         tip=None, fontfilters=None):
556
 
        """Option=None -> setting plugin font"""
557
 
        fontlabel = QLabel(_("Font: "))
558
 
        fontbox = QFontComboBox()
559
 
        if fontfilters is not None:
560
 
            fontbox.setFontFilters(fontfilters)
561
 
        sizelabel = QLabel("  "+_("Size: "))
562
 
        sizebox = QSpinBox()
563
 
        sizebox.setRange(7, 100)
564
 
        self.fontboxes[(fontbox, sizebox)] = option
565
 
        layout = QHBoxLayout()
566
 
        for subwidget in (fontlabel, fontbox, sizelabel, sizebox):
567
 
            layout.addWidget(subwidget)
568
 
        layout.addStretch(1)
569
 
        if text is None:
570
 
            text = _("Font style")
571
 
        group = QGroupBox(text)
572
 
        group.setLayout(layout)
573
 
        if tip is not None:
574
 
            group.setToolTip(tip)
575
 
        return group
576
 
    
577
 
    def create_button(self, text, callback):
578
 
        btn = QPushButton(text)
579
 
        self.connect(btn, SIGNAL('clicked()'), callback)
580
 
        self.connect(btn, SIGNAL('clicked()'),
581
 
                     lambda opt='': self.has_been_modified(opt))
582
 
        return btn
583
 
    
584
 
    def create_tab(self, *widgets):
585
 
        """Create simple tab widget page: widgets added in a vertical layout"""
586
 
        widget = QWidget()
587
 
        layout = QVBoxLayout()
588
 
        for widg in widgets:
589
 
            layout.addWidget(widg)
590
 
        layout.addStretch(1)
591
 
        widget.setLayout(layout)
592
 
        return widget
593
 
 
594
 
 
595
 
class GeneralConfigPage(SpyderConfigPage):
596
 
    CONF_SECTION = None
597
 
    def __init__(self, parent, main):
598
 
        SpyderConfigPage.__init__(self, parent)
599
 
        self.main = main
600
 
 
601
 
    def set_option(self, option, value):
602
 
        CONF.set(self.CONF_SECTION, option, value)
603
 
 
604
 
    def get_option(self, option, default=NoDefault):
605
 
        return CONF.get(self.CONF_SECTION, option, default)
606
 
            
607
 
    def apply_settings(self, options):
608
 
        raise NotImplementedError
609
 
 
610
 
 
611
 
class MainConfigPage(GeneralConfigPage):
612
 
    CONF_SECTION = "main"
613
 
    def get_name(self):
614
 
        return _("General")
615
 
    
616
 
    def get_icon(self):
617
 
        return get_icon("genprefs.png")
618
 
    
619
 
    def setup_page(self):
620
 
        interface_group = QGroupBox(_("Interface"))
621
 
        styles = [str(txt) for txt in QStyleFactory.keys()]
622
 
        choices = zip(styles, [style.lower() for style in styles])
623
 
        style_combo = self.create_combobox(_('Qt windows style'), choices,
624
 
                                           'windows_style',
625
 
                                           default=self.main.default_style)
626
 
        newcb = self.create_checkbox
627
 
        vertdock_box = newcb(_("Vertical dockwidget title bars"),
628
 
                             'vertical_dockwidget_titlebars')
629
 
        verttabs_box = newcb(_("Vertical dockwidget tabs"),
630
 
                             'vertical_tabs')
631
 
        animated_box = newcb(_("Animated toolbars and dockwidgets"),
632
 
                             'animated_docks')
633
 
        margin_box = newcb(_("Custom dockwidget margin:"),
634
 
                           'use_custom_margin')
635
 
        margin_spin = self.create_spinbox("", "pixels", 'custom_margin',
636
 
                                          0, 0, 30)
637
 
        self.connect(margin_box, SIGNAL("toggled(bool)"),
638
 
                     margin_spin.setEnabled)
639
 
        margin_spin.setEnabled(self.get_option('use_custom_margin'))
640
 
        margins_layout = QHBoxLayout()
641
 
        margins_layout.addWidget(margin_box)
642
 
        margins_layout.addWidget(margin_spin)
643
 
        
644
 
        interface_layout = QVBoxLayout()
645
 
        interface_layout.addWidget(style_combo)
646
 
        interface_layout.addWidget(vertdock_box)
647
 
        interface_layout.addWidget(verttabs_box)
648
 
        interface_layout.addWidget(animated_box)
649
 
        interface_layout.addLayout(margins_layout)
650
 
        interface_group.setLayout(interface_layout)
651
 
        
652
 
        vlayout = QVBoxLayout()
653
 
        vlayout.addWidget(interface_group)
654
 
        vlayout.addStretch(1)
655
 
        self.setLayout(vlayout)
656
 
        
657
 
    def apply_settings(self, options):
658
 
        self.main.apply_settings()
659
 
 
660
 
 
661
 
class ColorSchemeConfigPage(GeneralConfigPage):
662
 
    CONF_SECTION = "color_schemes"
663
 
    def get_name(self):
664
 
        return _("Syntax coloring")
665
 
    
666
 
    def get_icon(self):
667
 
        return get_icon("genprefs.png")
668
 
    
669
 
    def setup_page(self):
670
 
        tabs = QTabWidget()
671
 
        names = self.get_option("names")
672
 
        names.pop(names.index(CUSTOM_COLOR_SCHEME_NAME))
673
 
        names.insert(0, CUSTOM_COLOR_SCHEME_NAME)
674
 
        fieldnames = {
675
 
                      "background":     _("Background:"),
676
 
                      "currentline":    _("Current line:"),
677
 
                      "occurence":      _("Occurence:"),
678
 
                      "ctrlclick":      _("Link:"),
679
 
                      "sideareas":      _("Side areas:"),
680
 
                      "matched_p":      _("Matched parentheses:"),
681
 
                      "unmatched_p":    _("Unmatched parentheses:"),
682
 
                      "normal":         _("Normal text:"),
683
 
                      "keyword":        _("Keyword:"),
684
 
                      "builtin":        _("Builtin:"),
685
 
                      "definition":     _("Definition:"),
686
 
                      "comment":        _("Comment:"),
687
 
                      "string":         _("String:"),
688
 
                      "number":         _("Number:"),
689
 
                      "instance":       _("Instance:"),
690
 
                      }
691
 
        from spyderlib.widgets.sourcecode import syntaxhighlighters
692
 
        assert all([key in fieldnames
693
 
                    for key in syntaxhighlighters.COLOR_SCHEME_KEYS])
694
 
        for tabname in names:
695
 
            cs_group = QGroupBox(_("Color scheme"))
696
 
            cs_layout = QGridLayout()
697
 
            for row, key in enumerate(syntaxhighlighters.COLOR_SCHEME_KEYS):
698
 
                option = "%s/%s" % (tabname, key)
699
 
                value = self.get_option(option)
700
 
                name = fieldnames[key]
701
 
                if isinstance(value, basestring):
702
 
                    label, clayout = self.create_coloredit(name, option,
703
 
                                                           without_layout=True)
704
 
                    label.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
705
 
                    cs_layout.addWidget(label, row+1, 0)
706
 
                    cs_layout.addLayout(clayout, row+1, 1)
707
 
                else:
708
 
                    label, clayout, cb_bold, cb_italic = self.create_scedit(
709
 
                                            name, option, without_layout=True)
710
 
                    label.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
711
 
                    cs_layout.addWidget(label, row+1, 0)
712
 
                    cs_layout.addLayout(clayout, row+1, 1)
713
 
                    cs_layout.addWidget(cb_bold, row+1, 2)
714
 
                    cs_layout.addWidget(cb_italic, row+1, 3)
715
 
            cs_group.setLayout(cs_layout)
716
 
            if tabname in COLOR_SCHEME_NAMES:
717
 
                def_btn = self.create_button(_("Reset to default values"),
718
 
                                         lambda: self.reset_to_default(tabname))
719
 
                tabs.addTab(self.create_tab(cs_group, def_btn), tabname)
720
 
            else:
721
 
                tabs.addTab(self.create_tab(cs_group), tabname)
722
 
        
723
 
        vlayout = QVBoxLayout()
724
 
        vlayout.addWidget(tabs)
725
 
        self.setLayout(vlayout)
726
 
        
727
 
    @Slot(str)
728
 
    def reset_to_default(self, name):
729
 
        set_default_color_scheme(name, replace=True)
730
 
        self.load_from_conf()
731
 
            
732
 
    def apply_settings(self, options):
733
 
        self.main.editor.apply_plugin_settings(['color_scheme_name'])
734
 
        if self.main.historylog is not None:
735
 
            self.main.historylog.apply_plugin_settings(['color_scheme_name'])
736
 
        if self.main.inspector is not None:
737
 
            self.main.inspector.apply_plugin_settings(['color_scheme_name'])
 
1
# -*- coding: utf-8 -*-
 
2
#
 
3
# Copyright © 2009-2010 Pierre Raybaut
 
4
# Licensed under the terms of the MIT License
 
5
# (see spyderlib/__init__.py for details)
 
6
 
 
7
"""Configuration dialog / Preferences"""
 
8
 
 
9
import os
 
10
import os.path as osp
 
11
 
 
12
from spyderlib.baseconfig import _
 
13
from spyderlib.config import CONF
 
14
from spyderlib.guiconfig import (get_icon, CUSTOM_COLOR_SCHEME_NAME,
 
15
                                 set_default_color_scheme, COLOR_SCHEME_NAMES)
 
16
from spyderlib.utils.qthelpers import get_std_icon
 
17
from spyderlib.userconfig import NoDefault
 
18
from spyderlib.widgets.colors import ColorLayout
 
19
 
 
20
from spyderlib.qt.QtGui import (QWidget, QDialog, QListWidget, QListWidgetItem,
 
21
                                QVBoxLayout, QStackedWidget, QListView,
 
22
                                QHBoxLayout, QDialogButtonBox, QCheckBox,
 
23
                                QMessageBox, QLabel, QLineEdit, QSpinBox,
 
24
                                QPushButton, QFontComboBox, QGroupBox,
 
25
                                QComboBox, QColor, QGridLayout, QTabWidget,
 
26
                                QRadioButton, QButtonGroup, QSplitter,
 
27
                                QStyleFactory, QScrollArea)
 
28
from spyderlib.qt.QtCore import Qt, QSize, SIGNAL, SLOT, Slot
 
29
from spyderlib.qt.compat import (to_qvariant, from_qvariant,
 
30
                                 getexistingdirectory, getopenfilename)
 
31
 
 
32
 
 
33
class SizeMixin(object):
 
34
    """Mixin to keep widget size accessible
 
35
    even when C++ object has been deleted"""
 
36
    def resizeEvent(self, event):
 
37
        """Reimplement Qt method"""
 
38
        QDialog.resizeEvent(self, event)
 
39
        self.emit(SIGNAL("size_change(QSize)"), self.size())
 
40
 
 
41
 
 
42
class ConfigPage(QWidget):
 
43
    """Configuration page base class"""
 
44
    def __init__(self, parent, apply_callback=None):
 
45
        QWidget.__init__(self, parent)
 
46
        self.apply_callback = apply_callback
 
47
        self.is_modified = False
 
48
        
 
49
    def initialize(self):
 
50
        """
 
51
        Initialize configuration page:
 
52
            * setup GUI widgets
 
53
            * load settings and change widgets accordingly
 
54
        """
 
55
        self.setup_page()
 
56
        self.load_from_conf()
 
57
        
 
58
    def get_name(self):
 
59
        """Return page name"""
 
60
        raise NotImplementedError
 
61
    
 
62
    def get_icon(self):
 
63
        """Return page icon"""
 
64
        raise NotImplementedError
 
65
    
 
66
    def setup_page(self):
 
67
        """Setup configuration page widget"""
 
68
        raise NotImplementedError
 
69
        
 
70
    def set_modified(self, state):
 
71
        self.is_modified = state
 
72
        self.emit(SIGNAL("apply_button_enabled(bool)"), state)
 
73
        
 
74
    def is_valid(self):
 
75
        """Return True if all widget contents are valid"""
 
76
        raise NotImplementedError
 
77
    
 
78
    def apply_changes(self):
 
79
        """Apply changes callback"""
 
80
        if self.is_modified:
 
81
            self.save_to_conf()
 
82
            if self.apply_callback is not None:
 
83
                self.apply_callback()
 
84
            self.set_modified(False)
 
85
    
 
86
    def load_from_conf(self):
 
87
        """Load settings from configuration file"""
 
88
        raise NotImplementedError
 
89
    
 
90
    def save_to_conf(self):
 
91
        """Save settings to configuration file"""
 
92
        raise NotImplementedError
 
93
 
 
94
 
 
95
class ConfigDialog(QDialog, SizeMixin):
 
96
    """Spyder configuration ('Preferences') dialog box"""
 
97
    def __init__(self, parent=None):
 
98
        QDialog.__init__(self, parent)
 
99
        SizeMixin.__init__(self)
 
100
        
 
101
        # Destroying the C++ object right after closing the dialog box,
 
102
        # otherwise it may be garbage-collected in another QThread
 
103
        # (e.g. the editor's analysis thread in Spyder), thus leading to
 
104
        # a segmentation fault on UNIX or an application crash on Windows
 
105
        self.setAttribute(Qt.WA_DeleteOnClose)
 
106
 
 
107
        self.contents_widget = QListWidget()
 
108
        self.contents_widget.setMovement(QListView.Static)
 
109
        self.contents_widget.setSpacing(1)
 
110
 
 
111
        bbox = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Apply
 
112
                                |QDialogButtonBox.Cancel)
 
113
        self.apply_btn = bbox.button(QDialogButtonBox.Apply)
 
114
        self.connect(bbox, SIGNAL("accepted()"), SLOT("accept()"))
 
115
        self.connect(bbox, SIGNAL("rejected()"), SLOT("reject()"))
 
116
        self.connect(bbox, SIGNAL("clicked(QAbstractButton*)"),
 
117
                     self.button_clicked)
 
118
 
 
119
        self.pages_widget = QStackedWidget()
 
120
        self.connect(self.pages_widget, SIGNAL("currentChanged(int)"),
 
121
                     self.current_page_changed)
 
122
 
 
123
        self.connect(self.contents_widget, SIGNAL("currentRowChanged(int)"),
 
124
                     self.pages_widget.setCurrentIndex)
 
125
        self.contents_widget.setCurrentRow(0)
 
126
 
 
127
        hsplitter = QSplitter()
 
128
        hsplitter.addWidget(self.contents_widget)
 
129
        hsplitter.addWidget(self.pages_widget)
 
130
 
 
131
        btnlayout = QHBoxLayout()
 
132
        btnlayout.addStretch(1)
 
133
        btnlayout.addWidget(bbox)
 
134
 
 
135
        vlayout = QVBoxLayout()
 
136
        vlayout.addWidget(hsplitter)
 
137
        vlayout.addLayout(btnlayout)
 
138
 
 
139
        self.setLayout(vlayout)
 
140
 
 
141
        self.setWindowTitle(_("Preferences"))
 
142
        self.setWindowIcon(get_icon("configure.png"))
 
143
        
 
144
    def get_current_index(self):
 
145
        """Return current page index"""
 
146
        return self.contents_widget.currentRow()
 
147
        
 
148
    def set_current_index(self, index):
 
149
        """Set current page index"""
 
150
        self.contents_widget.setCurrentRow(index)
 
151
        
 
152
    def get_page(self, index=None):
 
153
        """Return page widget"""
 
154
        if index is None:
 
155
            widget = self.pages_widget.currentWidget()
 
156
        else:
 
157
            widget = self.pages_widget.widget(index)
 
158
        return widget.widget()
 
159
        
 
160
    def accept(self):
 
161
        """Reimplement Qt method"""
 
162
        for index in range(self.pages_widget.count()):
 
163
            configpage = self.get_page(index)
 
164
            if not configpage.is_valid():
 
165
                return
 
166
            configpage.apply_changes()
 
167
        QDialog.accept(self)
 
168
        
 
169
    def button_clicked(self, button):
 
170
        if button is self.apply_btn:
 
171
            # Apply button was clicked
 
172
            configpage = self.get_page()
 
173
            if not configpage.is_valid():
 
174
                return
 
175
            configpage.apply_changes()
 
176
            
 
177
    def current_page_changed(self, index):
 
178
        widget = self.get_page(index)
 
179
        self.apply_btn.setVisible(widget.apply_callback is not None)
 
180
        self.apply_btn.setEnabled(widget.is_modified)
 
181
        
 
182
    def add_page(self, widget):
 
183
        self.connect(self, SIGNAL('check_settings()'), widget.check_settings)
 
184
        self.connect(widget, SIGNAL('show_this_page()'),
 
185
                     lambda row=self.contents_widget.count():
 
186
                     self.contents_widget.setCurrentRow(row))
 
187
        self.connect(widget, SIGNAL("apply_button_enabled(bool)"),
 
188
                     self.apply_btn.setEnabled)
 
189
        scrollarea = QScrollArea(self)
 
190
        scrollarea.setWidgetResizable(True)
 
191
        scrollarea.setWidget(widget)
 
192
        self.pages_widget.addWidget(scrollarea)
 
193
        item = QListWidgetItem(self.contents_widget)
 
194
        item.setIcon(widget.get_icon())
 
195
        item.setText(widget.get_name())
 
196
        item.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
 
197
        item.setSizeHint(QSize(0, 25))
 
198
        
 
199
    def check_all_settings(self):
 
200
        """This method is called to check all configuration page settings
 
201
        after configuration dialog has been shown"""
 
202
        self.emit(SIGNAL('check_settings()'))
 
203
 
 
204
 
 
205
class SpyderConfigPage(ConfigPage):
 
206
    """Plugin configuration dialog box page widget"""
 
207
    def __init__(self, parent):
 
208
        ConfigPage.__init__(self, parent,
 
209
                            apply_callback=lambda:
 
210
                            self.apply_settings(self.changed_options))
 
211
        self.checkboxes = {}
 
212
        self.radiobuttons = {}
 
213
        self.lineedits = {}
 
214
        self.validate_data = {}
 
215
        self.spinboxes = {}
 
216
        self.comboboxes = {}
 
217
        self.fontboxes = {}
 
218
        self.coloredits = {}
 
219
        self.scedits = {}
 
220
        self.changed_options = set()
 
221
        self.default_button_group = None
 
222
        
 
223
    def apply_settings(self, options):
 
224
        raise NotImplementedError
 
225
    
 
226
    def check_settings(self):
 
227
        """This method is called to check settings after configuration 
 
228
        dialog has been shown"""
 
229
        pass
 
230
        
 
231
    def set_modified(self, state):
 
232
        ConfigPage.set_modified(self, state)
 
233
        if not state:
 
234
            self.changed_options = set()
 
235
        
 
236
    def is_valid(self):
 
237
        """Return True if all widget contents are valid"""
 
238
        for lineedit in self.lineedits:
 
239
            if lineedit in self.validate_data and lineedit.isEnabled():
 
240
                validator, invalid_msg = self.validate_data[lineedit]
 
241
                text = unicode(lineedit.text())
 
242
                if not validator(text):
 
243
                    QMessageBox.critical(self, self.get_name(),
 
244
                                     "%s:<br><b>%s</b>" % (invalid_msg, text),
 
245
                                     QMessageBox.Ok)
 
246
                    return False
 
247
        return True
 
248
        
 
249
    def load_from_conf(self):
 
250
        """Load settings from configuration file"""
 
251
        for checkbox, (option, default) in self.checkboxes.items():
 
252
            checkbox.setChecked(self.get_option(option, default))
 
253
            self.connect(checkbox, SIGNAL("clicked(bool)"),
 
254
                         lambda _foo, opt=option: self.has_been_modified(opt))
 
255
        for radiobutton, (option, default) in self.radiobuttons.items():
 
256
            radiobutton.setChecked(self.get_option(option, default))
 
257
            self.connect(radiobutton, SIGNAL("toggled(bool)"),
 
258
                         lambda _foo, opt=option: self.has_been_modified(opt))
 
259
        for lineedit, (option, default) in self.lineedits.items():
 
260
            lineedit.setText(self.get_option(option, default))
 
261
            self.connect(lineedit, SIGNAL("textChanged(QString)"),
 
262
                         lambda _foo, opt=option: self.has_been_modified(opt))
 
263
        for spinbox, (option, default) in self.spinboxes.items():
 
264
            spinbox.setValue(self.get_option(option, default))
 
265
            self.connect(spinbox, SIGNAL('valueChanged(int)'),
 
266
                         lambda _foo, opt=option: self.has_been_modified(opt))
 
267
        for combobox, (option, default) in self.comboboxes.items():
 
268
            value = self.get_option(option, default)
 
269
            for index in range(combobox.count()):
 
270
                data = from_qvariant(combobox.itemData(index), unicode)
 
271
                # For PyQt API v2, it is necessary to convert `data` to 
 
272
                # unicode in case the original type was not a string, like an 
 
273
                # integer for example (see spyderlib.qt.compat.from_qvariant):
 
274
                if unicode(data) == unicode(value):
 
275
                    break
 
276
            combobox.setCurrentIndex(index)
 
277
            self.connect(combobox, SIGNAL('currentIndexChanged(int)'),
 
278
                         lambda _foo, opt=option: self.has_been_modified(opt))
 
279
        for (fontbox, sizebox), option in self.fontboxes.items():
 
280
            font = self.get_font(option)
 
281
            fontbox.setCurrentFont(font)
 
282
            sizebox.setValue(font.pointSize())
 
283
            if option is None:
 
284
                property = 'plugin_font'
 
285
            else:
 
286
                property = option
 
287
            self.connect(fontbox, SIGNAL('currentIndexChanged(int)'),
 
288
                         lambda _foo, opt=property: self.has_been_modified(opt))
 
289
            self.connect(sizebox, SIGNAL('valueChanged(int)'),
 
290
                         lambda _foo, opt=property: self.has_been_modified(opt))
 
291
        for clayout, (option, default) in self.coloredits.items():
 
292
            property = to_qvariant(option)
 
293
            edit = clayout.lineedit
 
294
            btn = clayout.colorbtn
 
295
            edit.setText(self.get_option(option, default))
 
296
            self.connect(btn, SIGNAL('clicked()'),
 
297
                         lambda opt=option: self.has_been_modified(opt))
 
298
            self.connect(edit, SIGNAL("textChanged(QString)"),
 
299
                         lambda _foo, opt=option: self.has_been_modified(opt))
 
300
        for (clayout, cb_bold, cb_italic
 
301
             ), (option, default) in self.scedits.items():
 
302
            edit = clayout.lineedit
 
303
            btn = clayout.colorbtn
 
304
            color, bold, italic = self.get_option(option, default)
 
305
            edit.setText(color)
 
306
            cb_bold.setChecked(bold)
 
307
            cb_italic.setChecked(italic)
 
308
            self.connect(btn, SIGNAL('clicked()'),
 
309
                         lambda opt=option: self.has_been_modified(opt))
 
310
            self.connect(edit, SIGNAL("textChanged(QString)"),
 
311
                         lambda _foo, opt=option: self.has_been_modified(opt))
 
312
            self.connect(cb_bold, SIGNAL("clicked(bool)"),
 
313
                         lambda _foo, opt=option: self.has_been_modified(opt))
 
314
            self.connect(cb_italic, SIGNAL("clicked(bool)"),
 
315
                         lambda _foo, opt=option: self.has_been_modified(opt))
 
316
    
 
317
    def save_to_conf(self):
 
318
        """Save settings to configuration file"""
 
319
        for checkbox, (option, _default) in self.checkboxes.items():
 
320
            self.set_option(option, checkbox.isChecked())
 
321
        for radiobutton, (option, _default) in self.radiobuttons.items():
 
322
            self.set_option(option, radiobutton.isChecked())
 
323
        for lineedit, (option, _default) in self.lineedits.items():
 
324
            self.set_option(option, unicode(lineedit.text()))
 
325
        for spinbox, (option, _default) in self.spinboxes.items():
 
326
            self.set_option(option, spinbox.value())
 
327
        for combobox, (option, _default) in self.comboboxes.items():
 
328
            data = combobox.itemData(combobox.currentIndex())
 
329
            self.set_option(option, from_qvariant(data, unicode))
 
330
        for (fontbox, sizebox), option in self.fontboxes.items():
 
331
            font = fontbox.currentFont()
 
332
            font.setPointSize(sizebox.value())
 
333
            self.set_font(font, option)
 
334
        for clayout, (option, _default) in self.coloredits.items():
 
335
            self.set_option(option, unicode(clayout.lineedit.text()))
 
336
        for (clayout, cb_bold, cb_italic), (option, _default) in self.scedits.items():
 
337
            color = unicode(clayout.lineedit.text())
 
338
            bold = cb_bold.isChecked()
 
339
            italic = cb_italic.isChecked()
 
340
            self.set_option(option, (color, bold, italic))
 
341
    
 
342
    @Slot(str)
 
343
    def has_been_modified(self, option):
 
344
        self.set_modified(True)
 
345
        self.changed_options.add(option)
 
346
    
 
347
    def create_checkbox(self, text, option, default=NoDefault,
 
348
                        tip=None, msg_warning=None, msg_info=None,
 
349
                        msg_if_enabled=False):
 
350
        checkbox = QCheckBox(text)
 
351
        if tip is not None:
 
352
            checkbox.setToolTip(tip)
 
353
        self.checkboxes[checkbox] = (option, default)
 
354
        if msg_warning is not None or msg_info is not None:
 
355
            def show_message(is_checked):
 
356
                if is_checked or not msg_if_enabled:
 
357
                    if msg_warning is not None:
 
358
                        QMessageBox.warning(self, self.get_name(),
 
359
                                            msg_warning, QMessageBox.Ok)
 
360
                    if msg_info is not None:
 
361
                        QMessageBox.information(self, self.get_name(),
 
362
                                                msg_info, QMessageBox.Ok)
 
363
            self.connect(checkbox, SIGNAL("clicked(bool)"), show_message)
 
364
        return checkbox
 
365
    
 
366
    def create_radiobutton(self, text, option, default=NoDefault,
 
367
                           tip=None, msg_warning=None, msg_info=None,
 
368
                           msg_if_enabled=False, button_group=None):
 
369
        radiobutton = QRadioButton(text)
 
370
        if button_group is None:
 
371
            if self.default_button_group is None:
 
372
                self.default_button_group = QButtonGroup(self)
 
373
            button_group = self.default_button_group
 
374
        button_group.addButton(radiobutton)
 
375
        if tip is not None:
 
376
            radiobutton.setToolTip(tip)
 
377
        self.radiobuttons[radiobutton] = (option, default)
 
378
        if msg_warning is not None or msg_info is not None:
 
379
            def show_message(is_checked):
 
380
                if is_checked or not msg_if_enabled:
 
381
                    if msg_warning is not None:
 
382
                        QMessageBox.warning(self, self.get_name(),
 
383
                                            msg_warning, QMessageBox.Ok)
 
384
                    if msg_info is not None:
 
385
                        QMessageBox.information(self, self.get_name(),
 
386
                                                msg_info, QMessageBox.Ok)
 
387
            self.connect(radiobutton, SIGNAL("toggled(bool)"), show_message)
 
388
        return radiobutton
 
389
    
 
390
    def create_lineedit(self, text, option, default=NoDefault,
 
391
                        tip=None, alignment=Qt.Vertical):
 
392
        label = QLabel(text)
 
393
        label.setWordWrap(True)
 
394
        edit = QLineEdit()
 
395
        layout = QVBoxLayout() if alignment == Qt.Vertical else QHBoxLayout()
 
396
        layout.addWidget(label)
 
397
        layout.addWidget(edit)
 
398
        layout.setContentsMargins(0, 0, 0, 0)
 
399
        if tip:
 
400
            edit.setToolTip(tip)
 
401
        self.lineedits[edit] = (option, default)
 
402
        widget = QWidget(self)
 
403
        widget.setLayout(layout)
 
404
        return widget
 
405
    
 
406
    def create_browsedir(self, text, option, default=NoDefault, tip=None):
 
407
        widget = self.create_lineedit(text, option, default,
 
408
                                      alignment=Qt.Horizontal)
 
409
        for edit in self.lineedits:
 
410
            if widget.isAncestorOf(edit):
 
411
                break
 
412
        msg = _("Invalid directory path")
 
413
        self.validate_data[edit] = (osp.isdir, msg)
 
414
        browse_btn = QPushButton(get_std_icon('DirOpenIcon'), "", self)
 
415
        browse_btn.setToolTip(_("Select directory"))
 
416
        self.connect(browse_btn, SIGNAL("clicked()"),
 
417
                     lambda: self.select_directory(edit))
 
418
        layout = QHBoxLayout()
 
419
        layout.addWidget(widget)
 
420
        layout.addWidget(browse_btn)
 
421
        layout.setContentsMargins(0, 0, 0, 0)
 
422
        browsedir = QWidget(self)
 
423
        browsedir.setLayout(layout)
 
424
        return browsedir
 
425
 
 
426
    def select_directory(self, edit):
 
427
        """Select directory"""
 
428
        basedir = unicode(edit.text())
 
429
        if not osp.isdir(basedir):
 
430
            basedir = os.getcwdu()
 
431
        title = _("Select directory")
 
432
        directory = getexistingdirectory(self, title, basedir)
 
433
        if directory:
 
434
            edit.setText(directory)
 
435
    
 
436
    def create_browsefile(self, text, option, default=NoDefault, tip=None,
 
437
                          filters=None):
 
438
        widget = self.create_lineedit(text, option, default,
 
439
                                      alignment=Qt.Horizontal)
 
440
        for edit in self.lineedits:
 
441
            if widget.isAncestorOf(edit):
 
442
                break
 
443
        msg = _("Invalid file path")
 
444
        self.validate_data[edit] = (osp.isfile, msg)
 
445
        browse_btn = QPushButton(get_std_icon('FileIcon'), "", self)
 
446
        browse_btn.setToolTip(_("Select file"))
 
447
        self.connect(browse_btn, SIGNAL("clicked()"),
 
448
                     lambda: self.select_file(edit, filters))
 
449
        layout = QHBoxLayout()
 
450
        layout.addWidget(widget)
 
451
        layout.addWidget(browse_btn)
 
452
        layout.setContentsMargins(0, 0, 0, 0)
 
453
        browsedir = QWidget(self)
 
454
        browsedir.setLayout(layout)
 
455
        return browsedir
 
456
 
 
457
    def select_file(self, edit, filters=None):
 
458
        """Select File"""
 
459
        basedir = osp.dirname(unicode(edit.text()))
 
460
        if not osp.isdir(basedir):
 
461
            basedir = os.getcwdu()
 
462
        if filters is None:
 
463
            filters = _("All files (*)")
 
464
        title = _("Select file")
 
465
        filename, _selfilter = getopenfilename(self, title, basedir, filters)
 
466
        if filename:
 
467
            edit.setText(filename)
 
468
    
 
469
    def create_spinbox(self, prefix, suffix, option, default=NoDefault,
 
470
                       min_=None, max_=None, step=None, tip=None):
 
471
        if prefix:
 
472
            plabel = QLabel(prefix)
 
473
        else:
 
474
            plabel = None
 
475
        if suffix:
 
476
            slabel = QLabel(suffix)
 
477
        else:
 
478
            slabel = None
 
479
        spinbox = QSpinBox()
 
480
        if min_ is not None:
 
481
            spinbox.setMinimum(min_)
 
482
        if max_ is not None:
 
483
            spinbox.setMaximum(max_)
 
484
        if step is not None:
 
485
            spinbox.setSingleStep(step)
 
486
        if tip is not None:
 
487
            spinbox.setToolTip(tip)
 
488
        self.spinboxes[spinbox] = (option, default)
 
489
        layout = QHBoxLayout()
 
490
        for subwidget in (plabel, spinbox, slabel):
 
491
            if subwidget is not None:
 
492
                layout.addWidget(subwidget)
 
493
        layout.addStretch(1)
 
494
        layout.setContentsMargins(0, 0, 0, 0)
 
495
        widget = QWidget(self)
 
496
        widget.setLayout(layout)
 
497
        return widget
 
498
    
 
499
    def create_coloredit(self, text, option, default=NoDefault, tip=None,
 
500
                         without_layout=False):
 
501
        label = QLabel(text)
 
502
        clayout = ColorLayout(QColor(Qt.black), self)
 
503
        clayout.lineedit.setMaximumWidth(80)
 
504
        if tip is not None:
 
505
            clayout.setToolTip(tip)
 
506
        self.coloredits[clayout] = (option, default)
 
507
        if without_layout:
 
508
            return label, clayout
 
509
        layout = QHBoxLayout()
 
510
        layout.addWidget(label)
 
511
        layout.addLayout(clayout)
 
512
        layout.addStretch(1)
 
513
        layout.setContentsMargins(0, 0, 0, 0)
 
514
        widget = QWidget(self)
 
515
        widget.setLayout(layout)
 
516
        return widget
 
517
    
 
518
    def create_scedit(self, text, option, default=NoDefault, tip=None,
 
519
                      without_layout=False):
 
520
        label = QLabel(text)
 
521
        clayout = ColorLayout(QColor(Qt.black), self)
 
522
        clayout.lineedit.setMaximumWidth(80)
 
523
        if tip is not None:
 
524
            clayout.setToolTip(tip)
 
525
        cb_bold = QCheckBox()
 
526
        cb_bold.setIcon(get_icon("bold.png"))
 
527
        cb_bold.setToolTip(_("Bold"))
 
528
        cb_italic = QCheckBox()
 
529
        cb_italic.setIcon(get_icon("italic.png"))
 
530
        cb_italic.setToolTip(_("Italic"))
 
531
        self.scedits[(clayout, cb_bold, cb_italic)] = (option, default)
 
532
        if without_layout:
 
533
            return label, clayout, cb_bold, cb_italic
 
534
        layout = QHBoxLayout()
 
535
        layout.addWidget(label)
 
536
        layout.addLayout(clayout)
 
537
        layout.addSpacing(10)
 
538
        layout.addWidget(cb_bold)
 
539
        layout.addWidget(cb_italic)
 
540
        layout.addStretch(1)
 
541
        layout.setContentsMargins(0, 0, 0, 0)
 
542
        widget = QWidget(self)
 
543
        widget.setLayout(layout)
 
544
        return widget
 
545
    
 
546
    def create_combobox(self, text, choices, option, default=NoDefault,
 
547
                        tip=None):
 
548
        """choices: couples (name, key)"""
 
549
        label = QLabel(text)
 
550
        combobox = QComboBox()
 
551
        if tip is not None:
 
552
            combobox.setToolTip(tip)
 
553
        for name, key in choices:
 
554
            combobox.addItem(name, to_qvariant(key))
 
555
        self.comboboxes[combobox] = (option, default)
 
556
        layout = QHBoxLayout()
 
557
        for subwidget in (label, combobox):
 
558
            layout.addWidget(subwidget)
 
559
        layout.addStretch(1)
 
560
        layout.setContentsMargins(0, 0, 0, 0)
 
561
        widget = QWidget(self)
 
562
        widget.setLayout(layout)
 
563
        return widget
 
564
    
 
565
    def create_fontgroup(self, option=None, text=None,
 
566
                         tip=None, fontfilters=None):
 
567
        """Option=None -> setting plugin font"""
 
568
        fontlabel = QLabel(_("Font: "))
 
569
        fontbox = QFontComboBox()
 
570
        if fontfilters is not None:
 
571
            fontbox.setFontFilters(fontfilters)
 
572
        sizelabel = QLabel("  "+_("Size: "))
 
573
        sizebox = QSpinBox()
 
574
        sizebox.setRange(7, 100)
 
575
        self.fontboxes[(fontbox, sizebox)] = option
 
576
        layout = QHBoxLayout()
 
577
        for subwidget in (fontlabel, fontbox, sizelabel, sizebox):
 
578
            layout.addWidget(subwidget)
 
579
        layout.addStretch(1)
 
580
        if text is None:
 
581
            text = _("Font style")
 
582
        group = QGroupBox(text)
 
583
        group.setLayout(layout)
 
584
        if tip is not None:
 
585
            group.setToolTip(tip)
 
586
        return group
 
587
    
 
588
    def create_button(self, text, callback):
 
589
        btn = QPushButton(text)
 
590
        self.connect(btn, SIGNAL('clicked()'), callback)
 
591
        self.connect(btn, SIGNAL('clicked()'),
 
592
                     lambda opt='': self.has_been_modified(opt))
 
593
        return btn
 
594
    
 
595
    def create_tab(self, *widgets):
 
596
        """Create simple tab widget page: widgets added in a vertical layout"""
 
597
        widget = QWidget()
 
598
        layout = QVBoxLayout()
 
599
        for widg in widgets:
 
600
            layout.addWidget(widg)
 
601
        layout.addStretch(1)
 
602
        widget.setLayout(layout)
 
603
        return widget
 
604
 
 
605
 
 
606
class GeneralConfigPage(SpyderConfigPage):
 
607
    CONF_SECTION = None
 
608
    def __init__(self, parent, main):
 
609
        SpyderConfigPage.__init__(self, parent)
 
610
        self.main = main
 
611
 
 
612
    def set_option(self, option, value):
 
613
        CONF.set(self.CONF_SECTION, option, value)
 
614
 
 
615
    def get_option(self, option, default=NoDefault):
 
616
        return CONF.get(self.CONF_SECTION, option, default)
 
617
            
 
618
    def apply_settings(self, options):
 
619
        raise NotImplementedError
 
620
 
 
621
 
 
622
class MainConfigPage(GeneralConfigPage):
 
623
    CONF_SECTION = "main"
 
624
    def get_name(self):
 
625
        return _("General")
 
626
    
 
627
    def get_icon(self):
 
628
        return get_icon("genprefs.png")
 
629
    
 
630
    def setup_page(self):
 
631
        interface_group = QGroupBox(_("Interface"))
 
632
        styles = [str(txt) for txt in QStyleFactory.keys()]
 
633
        choices = zip(styles, [style.lower() for style in styles])
 
634
        style_combo = self.create_combobox(_('Qt windows style'), choices,
 
635
                                           'windows_style',
 
636
                                           default=self.main.default_style)
 
637
        newcb = self.create_checkbox
 
638
        vertdock_box = newcb(_("Vertical dockwidget title bars"),
 
639
                             'vertical_dockwidget_titlebars')
 
640
        verttabs_box = newcb(_("Vertical dockwidget tabs"),
 
641
                             'vertical_tabs')
 
642
        animated_box = newcb(_("Animated toolbars and dockwidgets"),
 
643
                             'animated_docks')
 
644
        margin_box = newcb(_("Custom dockwidget margin:"),
 
645
                           'use_custom_margin')
 
646
        margin_spin = self.create_spinbox("", "pixels", 'custom_margin',
 
647
                                          0, 0, 30)
 
648
        self.connect(margin_box, SIGNAL("toggled(bool)"),
 
649
                     margin_spin.setEnabled)
 
650
        margin_spin.setEnabled(self.get_option('use_custom_margin'))
 
651
        margins_layout = QHBoxLayout()
 
652
        margins_layout.addWidget(margin_box)
 
653
        margins_layout.addWidget(margin_spin)
 
654
        
 
655
        single_instance_box = newcb(_("Use a single instance"),
 
656
                                    'single_instance',
 
657
                                    tip=_("Set this to open external<br> "
 
658
                                          "python, .spy and .mat files in an "
 
659
                                          "already running instance (Requires "
 
660
                                          "a restart)"))
 
661
        
 
662
        interface_layout = QVBoxLayout()
 
663
        interface_layout.addWidget(style_combo)
 
664
        interface_layout.addWidget(single_instance_box)
 
665
        interface_layout.addWidget(vertdock_box)
 
666
        interface_layout.addWidget(verttabs_box)
 
667
        interface_layout.addWidget(animated_box)
 
668
        interface_layout.addLayout(margins_layout)
 
669
        interface_group.setLayout(interface_layout)
 
670
 
 
671
        sbar_group = QGroupBox(_("Status bar"))
 
672
        memory_box = newcb(_("Show memory usage every"), 'memory_usage/enable',
 
673
                           tip=self.main.mem_status.toolTip())
 
674
        memory_spin = self.create_spinbox("", " ms", 'memory_usage/timeout',
 
675
                                          min_=100, max_=1000000, step=100)
 
676
        self.connect(memory_box, SIGNAL("toggled(bool)"),
 
677
                     memory_spin.setEnabled)
 
678
        memory_spin.setEnabled(self.get_option('memory_usage/enable'))
 
679
        memory_layout = QHBoxLayout()
 
680
        memory_layout.addWidget(memory_box)
 
681
        memory_layout.addWidget(memory_spin)
 
682
        memory_layout.setEnabled(self.main.mem_status.is_supported())
 
683
        cpu_box = newcb(_("Show CPU usage every"), 'cpu_usage/enable',
 
684
                        tip=self.main.cpu_status.toolTip())
 
685
        cpu_spin = self.create_spinbox("", " ms", 'cpu_usage/timeout',
 
686
                                       min_=100, max_=1000000, step=100)
 
687
        self.connect(cpu_box, SIGNAL("toggled(bool)"), cpu_spin.setEnabled)
 
688
        cpu_spin.setEnabled(self.get_option('cpu_usage/enable'))
 
689
        cpu_layout = QHBoxLayout()
 
690
        cpu_layout.addWidget(cpu_box)
 
691
        cpu_layout.addWidget(cpu_spin)
 
692
        cpu_layout.setEnabled(self.main.cpu_status.is_supported())
 
693
        
 
694
        sbar_layout = QVBoxLayout()
 
695
        sbar_layout.addLayout(memory_layout)
 
696
        sbar_layout.addLayout(cpu_layout)
 
697
        sbar_group.setLayout(sbar_layout)
 
698
        
 
699
        debug_group = QGroupBox(_("Debugging"))
 
700
        popup_console_box = newcb(_("Pop up internal console when errors "
 
701
                                    "were intercepted"),
 
702
                                  'show_internal_console_if_traceback',
 
703
                                  default=False)
 
704
        
 
705
        debug_layout = QVBoxLayout()
 
706
        debug_layout.addWidget(popup_console_box)
 
707
        debug_group.setLayout(debug_layout)
 
708
        
 
709
        vlayout = QVBoxLayout()
 
710
        vlayout.addWidget(interface_group)
 
711
        vlayout.addWidget(sbar_group)
 
712
        vlayout.addWidget(debug_group)
 
713
        vlayout.addStretch(1)
 
714
        self.setLayout(vlayout)
 
715
        
 
716
    def apply_settings(self, options):
 
717
        self.main.apply_settings()
 
718
 
 
719
 
 
720
class ColorSchemeConfigPage(GeneralConfigPage):
 
721
    CONF_SECTION = "color_schemes"
 
722
    def get_name(self):
 
723
        return _("Syntax coloring")
 
724
    
 
725
    def get_icon(self):
 
726
        return get_icon("genprefs.png")
 
727
    
 
728
    def setup_page(self):
 
729
        tabs = QTabWidget()
 
730
        names = self.get_option("names")
 
731
        names.pop(names.index(CUSTOM_COLOR_SCHEME_NAME))
 
732
        names.insert(0, CUSTOM_COLOR_SCHEME_NAME)
 
733
        fieldnames = {
 
734
                      "background":     _("Background:"),
 
735
                      "currentline":    _("Current line:"),
 
736
                      "occurence":      _("Occurence:"),
 
737
                      "ctrlclick":      _("Link:"),
 
738
                      "sideareas":      _("Side areas:"),
 
739
                      "matched_p":      _("Matched parentheses:"),
 
740
                      "unmatched_p":    _("Unmatched parentheses:"),
 
741
                      "normal":         _("Normal text:"),
 
742
                      "keyword":        _("Keyword:"),
 
743
                      "builtin":        _("Builtin:"),
 
744
                      "definition":     _("Definition:"),
 
745
                      "comment":        _("Comment:"),
 
746
                      "string":         _("String:"),
 
747
                      "number":         _("Number:"),
 
748
                      "instance":       _("Instance:"),
 
749
                      }
 
750
        from spyderlib.widgets.sourcecode import syntaxhighlighters
 
751
        assert all([key in fieldnames
 
752
                    for key in syntaxhighlighters.COLOR_SCHEME_KEYS])
 
753
        for tabname in names:
 
754
            cs_group = QGroupBox(_("Color scheme"))
 
755
            cs_layout = QGridLayout()
 
756
            for row, key in enumerate(syntaxhighlighters.COLOR_SCHEME_KEYS):
 
757
                option = "%s/%s" % (tabname, key)
 
758
                value = self.get_option(option)
 
759
                name = fieldnames[key]
 
760
                if isinstance(value, basestring):
 
761
                    label, clayout = self.create_coloredit(name, option,
 
762
                                                           without_layout=True)
 
763
                    label.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
 
764
                    cs_layout.addWidget(label, row+1, 0)
 
765
                    cs_layout.addLayout(clayout, row+1, 1)
 
766
                else:
 
767
                    label, clayout, cb_bold, cb_italic = self.create_scedit(
 
768
                                            name, option, without_layout=True)
 
769
                    label.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
 
770
                    cs_layout.addWidget(label, row+1, 0)
 
771
                    cs_layout.addLayout(clayout, row+1, 1)
 
772
                    cs_layout.addWidget(cb_bold, row+1, 2)
 
773
                    cs_layout.addWidget(cb_italic, row+1, 3)
 
774
            cs_group.setLayout(cs_layout)
 
775
            if tabname in COLOR_SCHEME_NAMES:
 
776
                def_btn = self.create_button(_("Reset to default values"),
 
777
                                         lambda: self.reset_to_default(tabname))
 
778
                tabs.addTab(self.create_tab(cs_group, def_btn), tabname)
 
779
            else:
 
780
                tabs.addTab(self.create_tab(cs_group), tabname)
 
781
        
 
782
        vlayout = QVBoxLayout()
 
783
        vlayout.addWidget(tabs)
 
784
        self.setLayout(vlayout)
 
785
        
 
786
    @Slot(str)
 
787
    def reset_to_default(self, name):
 
788
        set_default_color_scheme(name, replace=True)
 
789
        self.load_from_conf()
 
790
            
 
791
    def apply_settings(self, options):
 
792
        self.main.editor.apply_plugin_settings(['color_scheme_name'])
 
793
        if self.main.historylog is not None:
 
794
            self.main.historylog.apply_plugin_settings(['color_scheme_name'])
 
795
        if self.main.inspector is not None:
 
796
            self.main.inspector.apply_plugin_settings(['color_scheme_name'])