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

« back to all changes in this revision

Viewing changes to spyderlib/widgets/arrayeditor.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-2012 Pierre Raybaut
4
 
# Licensed under the terms of the MIT License
5
 
# (see spyderlib/__init__.py for details)
6
 
 
7
 
"""
8
 
NumPy Array Editor Dialog based on Qt
9
 
"""
10
 
 
11
 
# pylint: disable=C0103
12
 
# pylint: disable=R0903
13
 
# pylint: disable=R0911
14
 
# pylint: disable=R0201
15
 
 
16
 
from spyderlib.qt.QtGui import (QHBoxLayout, QColor, QTableView, QItemDelegate,
17
 
                                QLineEdit, QCheckBox, QGridLayout,
18
 
                                QDoubleValidator, QDialog, QDialogButtonBox,
19
 
                                QMessageBox, QPushButton, QInputDialog, QMenu,
20
 
                                QApplication, QKeySequence, QLabel, QComboBox,
21
 
                                QStackedWidget, QWidget, QVBoxLayout)
22
 
from spyderlib.qt.QtCore import (Qt, QModelIndex, QAbstractTableModel, SIGNAL,
23
 
                                 SLOT)
24
 
from spyderlib.qt.compat import to_qvariant, from_qvariant
25
 
 
26
 
import numpy as np
27
 
import StringIO
28
 
 
29
 
# Local imports
30
 
from spyderlib.baseconfig import _
31
 
from spyderlib.config import get_icon, get_font
32
 
from spyderlib.utils.qthelpers import (add_actions, create_action, keybinding,
33
 
                                       qapplication)
34
 
 
35
 
# Note: string and unicode data types will be formatted with '%s' (see below)
36
 
SUPPORTED_FORMATS = {
37
 
                     'single': '%.3f',
38
 
                     'double': '%.3f',
39
 
                     'float_': '%.3f',
40
 
                     'longfloat': '%.3f',
41
 
                     'float32': '%.3f',
42
 
                     'float64': '%.3f',
43
 
                     'float96': '%.3f',
44
 
                     'float128': '%.3f',
45
 
                     'csingle': '%r',
46
 
                     'complex_': '%r',
47
 
                     'clongfloat': '%r',
48
 
                     'complex64': '%r',
49
 
                     'complex128': '%r',
50
 
                     'complex192': '%r',
51
 
                     'complex256': '%r',
52
 
                     'byte': '%d',
53
 
                     'short': '%d',
54
 
                     'intc': '%d',
55
 
                     'int_': '%d',
56
 
                     'longlong': '%d',
57
 
                     'intp': '%d',
58
 
                     'int8': '%d',
59
 
                     'int16': '%d',
60
 
                     'int32': '%d',
61
 
                     'int64': '%d',
62
 
                     'ubyte': '%d',
63
 
                     'ushort': '%d',
64
 
                     'uintc': '%d',
65
 
                     'uint': '%d',
66
 
                     'ulonglong': '%d',
67
 
                     'uintp': '%d',
68
 
                     'uint8': '%d',
69
 
                     'uint16': '%d',
70
 
                     'uint32': '%d',
71
 
                     'uint64': '%d',
72
 
                     'bool_': '%r',
73
 
                     'bool8': '%r',
74
 
                     'bool': '%r',
75
 
                     }
76
 
 
77
 
def is_float(dtype):
78
 
    """Return True if datatype dtype is a float kind"""
79
 
    return ('float' in dtype.name) or dtype.name in ['single', 'double']
80
 
 
81
 
def is_number(dtype):
82
 
    """Return True is datatype dtype is a number kind"""
83
 
    return is_float(dtype) or ('int' in dtype.name) or ('long' in dtype.name) \
84
 
           or ('short' in dtype.name)
85
 
 
86
 
def get_idx_rect(index_list):
87
 
    """Extract the boundaries from a list of indexes"""
88
 
    rows, cols = zip(*[(i.row(), i.column()) for i in index_list])
89
 
    return ( min(rows), max(rows), min(cols), max(cols) )
90
 
 
91
 
 
92
 
class ArrayModel(QAbstractTableModel):
93
 
    """Array Editor Table Model"""
94
 
    def __init__(self, data, format="%.3f", xlabels=None, ylabels=None,
95
 
                 readonly=False, parent=None):
96
 
        QAbstractTableModel.__init__(self)
97
 
 
98
 
        self.dialog = parent
99
 
        self.changes = {}
100
 
        self.xlabels = xlabels
101
 
        self.ylabels = ylabels
102
 
        self.readonly = readonly
103
 
        self.test_array = np.array([0], dtype=data.dtype)
104
 
 
105
 
        # for complex numbers, shading will be based on absolute value
106
 
        # but for all other types it will be the real part
107
 
        if data.dtype in (np.complex64, np.complex128):
108
 
            self.color_func = np.abs
109
 
        else:
110
 
            self.color_func = np.real
111
 
        
112
 
        # Backgroundcolor settings
113
 
        huerange = [.66, .99] # Hue
114
 
        self.sat = .7 # Saturation
115
 
        self.val = 1. # Value
116
 
        self.alp = .6 # Alpha-channel
117
 
 
118
 
        self._data = data
119
 
        self._format = format
120
 
        
121
 
        try:
122
 
            self.vmin = self.color_func(data).min()
123
 
            self.vmax = self.color_func(data).max()
124
 
            if self.vmax == self.vmin:
125
 
                self.vmin -= 1
126
 
            self.hue0 = huerange[0]
127
 
            self.dhue = huerange[1]-huerange[0]
128
 
            self.bgcolor_enabled = True
129
 
        except TypeError:
130
 
            self.vmin = None
131
 
            self.vmax = None
132
 
            self.hue0 = None
133
 
            self.dhue = None
134
 
            self.bgcolor_enabled = False
135
 
        
136
 
        
137
 
    def get_format(self):
138
 
        """Return current format"""
139
 
        # Avoid accessing the private attribute _format from outside
140
 
        return self._format
141
 
    
142
 
    def get_data(self):
143
 
        """Return data"""
144
 
        return self._data
145
 
    
146
 
    def set_format(self, format):
147
 
        """Change display format"""
148
 
        self._format = format
149
 
        self.reset()
150
 
 
151
 
    def columnCount(self, qindex=QModelIndex()):
152
 
        """Array column number"""
153
 
        return self._data.shape[1]
154
 
 
155
 
    def rowCount(self, qindex=QModelIndex()):
156
 
        """Array row number"""
157
 
        return self._data.shape[0]
158
 
 
159
 
    def bgcolor(self, state):
160
 
        """Toggle backgroundcolor"""
161
 
        self.bgcolor_enabled = state > 0
162
 
        self.reset()
163
 
 
164
 
    def get_value(self, index):
165
 
        i = index.row()
166
 
        j = index.column()
167
 
        return self.changes.get((i, j), self._data[i, j])
168
 
 
169
 
    def data(self, index, role=Qt.DisplayRole):
170
 
        """Cell content"""
171
 
        if not index.isValid():
172
 
            return to_qvariant()
173
 
        value = self.get_value(index)
174
 
        if role == Qt.DisplayRole:
175
 
            if value is np.ma.masked:
176
 
                return ''
177
 
            else:
178
 
                return to_qvariant(self._format % value)
179
 
        elif role == Qt.TextAlignmentRole:
180
 
            return to_qvariant(int(Qt.AlignCenter|Qt.AlignVCenter))
181
 
        elif role == Qt.BackgroundColorRole and self.bgcolor_enabled\
182
 
             and value is not np.ma.masked:
183
 
            hue = self.hue0+\
184
 
                  self.dhue*(self.vmax-self.color_func(value))\
185
 
                  /(self.vmax-self.vmin)
186
 
            hue = float(np.abs(hue))
187
 
            color = QColor.fromHsvF(hue, self.sat, self.val, self.alp)
188
 
            return to_qvariant(color)
189
 
        elif role == Qt.FontRole:
190
 
            return to_qvariant(get_font('arrayeditor'))
191
 
        return to_qvariant()
192
 
 
193
 
    def setData(self, index, value, role=Qt.EditRole):
194
 
        """Cell content change"""
195
 
        if not index.isValid() or self.readonly:
196
 
            return False
197
 
        i = index.row()
198
 
        j = index.column()
199
 
        value = from_qvariant(value, str)
200
 
        if self._data.dtype.name == "bool":
201
 
            try:
202
 
                val = bool(float(value))
203
 
            except ValueError:
204
 
                val = value.lower() == "true"
205
 
        elif self._data.dtype.name.startswith("string"):
206
 
            val = str(value)
207
 
        elif self._data.dtype.name.startswith("unicode"):
208
 
            val = unicode(value)
209
 
        else:
210
 
            if value.lower().startswith('e') or value.lower().endswith('e'):
211
 
                return False
212
 
            try:
213
 
                val = complex(value)
214
 
                if not val.imag:
215
 
                    val = val.real
216
 
            except ValueError, e:
217
 
                QMessageBox.critical(self.dialog, "Error",
218
 
                                     "Value error: %s" % str(e))
219
 
                return False
220
 
        try:
221
 
            self.test_array[0] = val # will raise an Exception eventually
222
 
        except OverflowError, e:
223
 
            print type(e.message)
224
 
            QMessageBox.critical(self.dialog, "Error",
225
 
                                 "Overflow error: %s" % e.message)
226
 
            return False
227
 
        
228
 
        # Add change to self.changes
229
 
        self.changes[(i, j)] = val
230
 
        self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"),
231
 
                  index, index)
232
 
        if val > self.vmax:
233
 
            self.vmax = val
234
 
        if val < self.vmin:
235
 
            self.vmin = val
236
 
        return True
237
 
    
238
 
    def flags(self, index):
239
 
        """Set editable flag"""
240
 
        if not index.isValid():
241
 
            return Qt.ItemIsEnabled
242
 
        return Qt.ItemFlags(QAbstractTableModel.flags(self, index)|
243
 
                            Qt.ItemIsEditable)
244
 
                
245
 
    def headerData(self, section, orientation, role=Qt.DisplayRole):
246
 
        """Set header data"""
247
 
        if role != Qt.DisplayRole:
248
 
            return to_qvariant()
249
 
        labels = self.xlabels if orientation == Qt.Horizontal else self.ylabels
250
 
        if labels is None:
251
 
            return to_qvariant(int(section))
252
 
        else:
253
 
            return to_qvariant(labels[section])
254
 
 
255
 
 
256
 
class ArrayDelegate(QItemDelegate):
257
 
    """Array Editor Item Delegate"""
258
 
    def __init__(self, dtype, parent=None):
259
 
        QItemDelegate.__init__(self, parent)
260
 
        self.dtype = dtype
261
 
 
262
 
    def createEditor(self, parent, option, index):
263
 
        """Create editor widget"""
264
 
        model = index.model()
265
 
        value = model.get_value(index)
266
 
        if model._data.dtype.name == "bool":
267
 
            value = not value
268
 
            model.setData(index, to_qvariant(value))
269
 
            return
270
 
        elif value is not np.ma.masked:
271
 
            editor = QLineEdit(parent)
272
 
            editor.setFont(get_font('arrayeditor'))
273
 
            editor.setAlignment(Qt.AlignCenter)
274
 
            if is_number(self.dtype):
275
 
                editor.setValidator(QDoubleValidator(editor))
276
 
            self.connect(editor, SIGNAL("returnPressed()"),
277
 
                         self.commitAndCloseEditor)
278
 
            return editor
279
 
 
280
 
    def commitAndCloseEditor(self):
281
 
        """Commit and close editor"""
282
 
        editor = self.sender()
283
 
        self.emit(SIGNAL("commitData(QWidget*)"), editor)
284
 
        self.emit(SIGNAL("closeEditor(QWidget*)"), editor)
285
 
 
286
 
    def setEditorData(self, editor, index):
287
 
        """Set editor widget's data"""
288
 
        text = from_qvariant(index.model().data(index, Qt.DisplayRole), str)
289
 
        editor.setText(text)
290
 
 
291
 
 
292
 
#TODO: Implement "Paste" (from clipboard) feature
293
 
class ArrayView(QTableView):
294
 
    """Array view class"""
295
 
    def __init__(self, parent, model, dtype, shape):
296
 
        QTableView.__init__(self, parent)
297
 
 
298
 
        self.setModel(model)
299
 
        self.setItemDelegate(ArrayDelegate(dtype, self))
300
 
        total_width = 0
301
 
        for k in xrange(shape[1]):
302
 
            total_width += self.columnWidth(k)
303
 
        self.viewport().resize(min(total_width, 1024), self.height())
304
 
        self.shape = shape
305
 
        self.menu = self.setup_menu()
306
 
  
307
 
    def resize_to_contents(self):
308
 
        """Resize cells to contents"""
309
 
        size = 1
310
 
        for dim in self.shape:
311
 
            size *= dim
312
 
        if size > 1e5:
313
 
            answer = QMessageBox.warning(self, _("Array editor"),
314
 
                                         _("Resizing cells of a table of such "
315
 
                                           "size could take a long time.\n"
316
 
                                           "Do you want to continue anyway?"),
317
 
                                         QMessageBox.Yes | QMessageBox.No)
318
 
            if answer == QMessageBox.No:
319
 
                return
320
 
        self.resizeColumnsToContents()
321
 
        self.resizeRowsToContents()
322
 
 
323
 
    def setup_menu(self):
324
 
        """Setup context menu"""
325
 
        self.copy_action = create_action(self, _( "Copy"),
326
 
                                         shortcut=keybinding("Copy"),
327
 
                                         icon=get_icon('editcopy.png'),
328
 
                                         triggered=self.copy,
329
 
                                         context=Qt.WidgetShortcut)
330
 
        menu = QMenu(self)
331
 
        add_actions(menu, [self.copy_action, ])
332
 
        return menu
333
 
 
334
 
    def contextMenuEvent(self, event):
335
 
        """Reimplement Qt method"""
336
 
        self.menu.popup(event.globalPos())
337
 
        event.accept()
338
 
        
339
 
    def keyPressEvent(self, event):
340
 
        """Reimplement Qt method"""
341
 
        if event == QKeySequence.Copy:
342
 
            self.copy()
343
 
        else:
344
 
            QTableView.keyPressEvent(self, event)
345
 
 
346
 
    def _sel_to_text(self, cell_range):
347
 
        """Copy an array portion to a unicode string"""
348
 
        row_min, row_max, col_min, col_max = get_idx_rect(cell_range)
349
 
        _data = self.model().get_data()
350
 
        output = StringIO.StringIO()
351
 
        np.savetxt(output,
352
 
                  _data[row_min:row_max+1, col_min:col_max+1],
353
 
                  delimiter='\t')
354
 
        contents = output.getvalue()
355
 
        output.close()
356
 
        return contents
357
 
    
358
 
    def copy(self):
359
 
        """Copy text to clipboard"""
360
 
        cliptxt = self._sel_to_text( self.selectedIndexes() )
361
 
        clipboard = QApplication.clipboard()
362
 
        clipboard.setText(cliptxt)
363
 
 
364
 
 
365
 
class ArrayEditorWidget(QWidget):
366
 
    def __init__(self, parent, data, readonly=False,
367
 
                 xlabels=None, ylabels=None):
368
 
        QWidget.__init__(self, parent)
369
 
        self.data = data
370
 
        self.old_data_shape = None
371
 
        if len(self.data.shape) == 1:
372
 
            self.old_data_shape = self.data.shape
373
 
            self.data.shape = (self.data.shape[0], 1)
374
 
        elif len(self.data.shape) == 0:
375
 
            self.old_data_shape = self.data.shape
376
 
            self.data.shape = (1, 1)
377
 
 
378
 
        format = SUPPORTED_FORMATS.get(data.dtype.name, '%s')
379
 
        self.model = ArrayModel(self.data, format=format, xlabels=xlabels,
380
 
                                ylabels=ylabels, readonly=readonly, parent=self)
381
 
        self.view = ArrayView(self, self.model, data.dtype, data.shape)
382
 
        
383
 
        btn_layout = QHBoxLayout()
384
 
        btn_layout.setAlignment(Qt.AlignLeft)
385
 
        btn = QPushButton(_( "Format"))
386
 
        # disable format button for int type
387
 
        btn.setEnabled(is_float(data.dtype))
388
 
        btn_layout.addWidget(btn)
389
 
        self.connect(btn, SIGNAL("clicked()"), self.change_format)
390
 
        btn = QPushButton(_( "Resize"))
391
 
        btn_layout.addWidget(btn)
392
 
        self.connect(btn, SIGNAL("clicked()"), self.view.resize_to_contents)
393
 
        bgcolor = QCheckBox(_( 'Background color'))
394
 
        bgcolor.setChecked(self.model.bgcolor_enabled)
395
 
        bgcolor.setEnabled(self.model.bgcolor_enabled)
396
 
        self.connect(bgcolor, SIGNAL("stateChanged(int)"), self.model.bgcolor)
397
 
        btn_layout.addWidget(bgcolor)
398
 
        
399
 
        layout = QVBoxLayout()
400
 
        layout.addWidget(self.view)
401
 
        layout.addLayout(btn_layout)        
402
 
        self.setLayout(layout)
403
 
        
404
 
    def accept_changes(self):
405
 
        """Accept changes"""
406
 
        for (i, j), value in self.model.changes.iteritems():
407
 
            self.data[i, j] = value
408
 
        if self.old_data_shape is not None:
409
 
            self.data.shape = self.old_data_shape
410
 
            
411
 
    def reject_changes(self):
412
 
        """Reject changes"""
413
 
        if self.old_data_shape is not None:
414
 
            self.data.shape = self.old_data_shape
415
 
        
416
 
    def change_format(self):
417
 
        """Change display format"""
418
 
        format, valid = QInputDialog.getText(self, _( 'Format'),
419
 
                                 _( "Float formatting"),
420
 
                                 QLineEdit.Normal, self.model.get_format())
421
 
        if valid:
422
 
            format = str(format)
423
 
            try:
424
 
                format % 1.1
425
 
            except:
426
 
                QMessageBox.critical(self, _("Error"),
427
 
                                     _("Format (%s) is incorrect") % format)
428
 
                return
429
 
            self.model.set_format(format)    
430
 
 
431
 
 
432
 
class ArrayEditor(QDialog):
433
 
    """Array Editor Dialog"""    
434
 
    def __init__(self, parent=None):
435
 
        QDialog.__init__(self, parent)
436
 
        
437
 
        # Destroying the C++ object right after closing the dialog box,
438
 
        # otherwise it may be garbage-collected in another QThread
439
 
        # (e.g. the editor's analysis thread in Spyder), thus leading to
440
 
        # a segmentation fault on UNIX or an application crash on Windows
441
 
        self.setAttribute(Qt.WA_DeleteOnClose)
442
 
        
443
 
        self.data = None
444
 
        self.arraywidget = None
445
 
        self.stack = None
446
 
        self.layout = None
447
 
    
448
 
    def setup_and_check(self, data, title='', readonly=False,
449
 
                        xlabels=None, ylabels=None):
450
 
        """
451
 
        Setup ArrayEditor:
452
 
        return False if data is not supported, True otherwise
453
 
        """
454
 
        self.data = data
455
 
        is_record_array = data.dtype.names is not None
456
 
        is_masked_array = isinstance(data, np.ma.MaskedArray)
457
 
        if data.size == 0:
458
 
            self.error(_("Array is empty"))
459
 
            return False
460
 
        if data.ndim > 2:
461
 
            self.error(_("Arrays with more than 2 dimensions "
462
 
                               "are not supported"))
463
 
            return False
464
 
        if xlabels is not None and len(xlabels) != self.data.shape[1]:
465
 
            self.error(_("The 'xlabels' argument length "
466
 
                                                           "do no match array column number"))
467
 
            return False
468
 
        if ylabels is not None and len(ylabels) != self.data.shape[0]:
469
 
            self.error(_("The 'ylabels' argument length "
470
 
                                                           "do no match array row number"))
471
 
            return False
472
 
        if not is_record_array:
473
 
            dtn = data.dtype.name
474
 
            if dtn not in SUPPORTED_FORMATS and not dtn.startswith('string') \
475
 
               and not dtn.startswith('unicode'):
476
 
                arr = _("%s arrays") % data.dtype.name
477
 
                self.error(_("%s are currently not supported") % arr)
478
 
                return False
479
 
        
480
 
        self.layout = QGridLayout()
481
 
        self.setLayout(self.layout)
482
 
        self.setWindowIcon(get_icon('arredit.png'))
483
 
        if title:
484
 
            title = unicode(title) # in case title is not a string
485
 
        else:
486
 
            title = _("Array editor")
487
 
        if readonly:
488
 
            title += ' (' + _('read only') + ')'
489
 
        self.setWindowTitle(title)
490
 
        self.resize(600, 500)
491
 
        
492
 
        # Stack widget
493
 
        self.stack = QStackedWidget(self)
494
 
        if is_record_array:
495
 
            for name in data.dtype.names:
496
 
                self.stack.addWidget(ArrayEditorWidget(self, data[name],
497
 
                                                   readonly, xlabels, ylabels))
498
 
        elif is_masked_array:
499
 
            self.stack.addWidget(ArrayEditorWidget(self, data, readonly,
500
 
                                                   xlabels, ylabels))
501
 
            self.stack.addWidget(ArrayEditorWidget(self, data.data, readonly,
502
 
                                                   xlabels, ylabels))
503
 
            self.stack.addWidget(ArrayEditorWidget(self, data.mask, readonly,
504
 
                                                   xlabels, ylabels))
505
 
        else:
506
 
            self.stack.addWidget(ArrayEditorWidget(self, data, readonly,
507
 
                                                   xlabels, ylabels))
508
 
        self.arraywidget = self.stack.currentWidget()
509
 
        self.connect(self.stack, SIGNAL('currentChanged(int)'),
510
 
                     self.current_widget_changed)
511
 
        self.layout.addWidget(self.stack, 1, 0)
512
 
 
513
 
        # Buttons configuration
514
 
        btn_layout = QHBoxLayout()
515
 
        if is_record_array or is_masked_array:
516
 
            if is_record_array:
517
 
                btn_layout.addWidget(QLabel(_("Record array fields:")))
518
 
                names = []
519
 
                for name in data.dtype.names:
520
 
                    field = data.dtype.fields[name]
521
 
                    text = name
522
 
                    if len(field) >= 3:
523
 
                        title = field[2]
524
 
                        if not isinstance(title, basestring):
525
 
                            title = repr(title)
526
 
                        text += ' - '+title
527
 
                    names.append(text)
528
 
            else:
529
 
                names = [_('Masked data'), _('Data'), _('Mask')]
530
 
            ra_combo = QComboBox(self)
531
 
            self.connect(ra_combo, SIGNAL('currentIndexChanged(int)'),
532
 
                         self.stack.setCurrentIndex)
533
 
            ra_combo.addItems(names)
534
 
            btn_layout.addWidget(ra_combo)
535
 
            if is_masked_array:
536
 
                label = QLabel(_("<u>Warning</u>: changes are applied separately"))
537
 
                label.setToolTip(_("For performance reasons, changes applied "\
538
 
                                   "to masked array won't be reflected in "\
539
 
                                   "array's data (and vice-versa)."))
540
 
                btn_layout.addWidget(label)
541
 
            btn_layout.addStretch()
542
 
        bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
543
 
        self.connect(bbox, SIGNAL("accepted()"), SLOT("accept()"))
544
 
        self.connect(bbox, SIGNAL("rejected()"), SLOT("reject()"))
545
 
        btn_layout.addWidget(bbox)
546
 
        self.layout.addLayout(btn_layout, 2, 0)
547
 
        
548
 
        self.setMinimumSize(400, 300)
549
 
        
550
 
        # Make the dialog act as a window
551
 
        self.setWindowFlags(Qt.Window)
552
 
        
553
 
        return True
554
 
        
555
 
    def current_widget_changed(self, index):
556
 
        self.arraywidget = self.stack.widget(index)
557
 
        
558
 
    def accept(self):
559
 
        """Reimplement Qt method"""
560
 
        for index in range(self.stack.count()):
561
 
            self.stack.widget(index).accept_changes()
562
 
        QDialog.accept(self)
563
 
        
564
 
    def get_value(self):
565
 
        """Return modified array -- this is *not* a copy"""
566
 
        # It is import to avoid accessing Qt C++ object as it has probably
567
 
        # already been destroyed, due to the Qt.WA_DeleteOnClose attribute
568
 
        return self.data
569
 
 
570
 
    def error(self, message):
571
 
        """An error occured, closing the dialog box"""
572
 
        QMessageBox.critical(self, _("Array editor"), message)
573
 
        self.setAttribute(Qt.WA_DeleteOnClose)
574
 
        self.reject()
575
 
 
576
 
    def reject(self):
577
 
        """Reimplement Qt method"""
578
 
        if self.arraywidget is not None:
579
 
            for index in range(self.stack.count()):
580
 
                self.stack.widget(index).reject_changes()
581
 
        QDialog.reject(self)
582
 
    
583
 
    
584
 
def test_edit(data, title="", xlabels=None, ylabels=None,
585
 
              readonly=False, parent=None):
586
 
    """Test subroutine"""
587
 
    dlg = ArrayEditor(parent)
588
 
    if dlg.setup_and_check(data, title, xlabels=xlabels, ylabels=ylabels,
589
 
                           readonly=readonly) and dlg.exec_():
590
 
        return dlg.get_value()
591
 
    else:
592
 
        import sys
593
 
        sys.exit()
594
 
 
595
 
 
596
 
def test():
597
 
    """Array editor test"""
598
 
    _app = qapplication()
599
 
    
600
 
    arr = np.array(["kjrekrjkejr"])
601
 
    print "out:", test_edit(arr, "string array")
602
 
    arr = np.array([u"kjrekrjkejr"])
603
 
    print "out:", test_edit(arr, "unicode array")
604
 
    arr = np.ma.array([[1, 0], [1, 0]], mask=[[True, False], [False, False]])
605
 
    print "out:", test_edit(arr, "masked array")
606
 
    arr = np.zeros((2,2), {'names': ('red', 'green', 'blue'),
607
 
                           'formats': (np.float32, np.float32, np.float32)})
608
 
    print "out:", test_edit(arr, "record array")
609
 
    arr = np.array([(0, 0.0), (0, 0.0), (0, 0.0)],
610
 
                   dtype=[(('title 1', 'x'), '|i1'),
611
 
                          (('title 2', 'y'), '>f4')])
612
 
    print "out:", test_edit(arr, "record array with titles")
613
 
    arr = np.random.rand(5, 5)
614
 
    print "out:", test_edit(arr, "float array",
615
 
                            xlabels=['a', 'b', 'c', 'd', 'e'])
616
 
    arr = np.round(np.random.rand(5, 5)*10)+\
617
 
                   np.round(np.random.rand(5, 5)*10)*1j
618
 
    print "out:", test_edit(arr, "complex array",
619
 
                            xlabels=np.linspace(-12, 12, 5),
620
 
                            ylabels=np.linspace(-12, 12, 5))
621
 
    arr_in = np.array([True, False, True])
622
 
    print "in:", arr_in
623
 
    arr_out = test_edit(arr_in, "bool array")
624
 
    print "out:", arr_out
625
 
    print arr_in is arr_out
626
 
    arr = np.array([1, 2, 3], dtype="int8")
627
 
    print "out:", test_edit(arr, "int array")
628
 
 
629
 
 
630
 
if __name__ == "__main__":
631
 
    test()
 
1
# -*- coding: utf-8 -*-
 
2
#
 
3
# Copyright © 2009-2012 Pierre Raybaut
 
4
# Licensed under the terms of the MIT License
 
5
# (see spyderlib/__init__.py for details)
 
6
 
 
7
"""
 
8
NumPy Array Editor Dialog based on Qt
 
9
"""
 
10
 
 
11
# pylint: disable=C0103
 
12
# pylint: disable=R0903
 
13
# pylint: disable=R0911
 
14
# pylint: disable=R0201
 
15
 
 
16
from spyderlib.qt.QtGui import (QHBoxLayout, QColor, QTableView, QItemDelegate,
 
17
                                QLineEdit, QCheckBox, QGridLayout,
 
18
                                QDoubleValidator, QDialog, QDialogButtonBox,
 
19
                                QMessageBox, QPushButton, QInputDialog, QMenu,
 
20
                                QApplication, QKeySequence, QLabel, QComboBox,
 
21
                                QStackedWidget, QWidget, QVBoxLayout)
 
22
from spyderlib.qt.QtCore import (Qt, QModelIndex, QAbstractTableModel, SIGNAL,
 
23
                                 SLOT)
 
24
from spyderlib.qt.compat import to_qvariant, from_qvariant
 
25
 
 
26
import numpy as np
 
27
import StringIO
 
28
 
 
29
# Local imports
 
30
from spyderlib.baseconfig import _
 
31
from spyderlib.guiconfig import get_icon, get_font
 
32
from spyderlib.utils.qthelpers import (add_actions, create_action, keybinding,
 
33
                                       qapplication)
 
34
 
 
35
# Note: string and unicode data types will be formatted with '%s' (see below)
 
36
SUPPORTED_FORMATS = {
 
37
                     'single': '%.3f',
 
38
                     'double': '%.3f',
 
39
                     'float_': '%.3f',
 
40
                     'longfloat': '%.3f',
 
41
                     'float32': '%.3f',
 
42
                     'float64': '%.3f',
 
43
                     'float96': '%.3f',
 
44
                     'float128': '%.3f',
 
45
                     'csingle': '%r',
 
46
                     'complex_': '%r',
 
47
                     'clongfloat': '%r',
 
48
                     'complex64': '%r',
 
49
                     'complex128': '%r',
 
50
                     'complex192': '%r',
 
51
                     'complex256': '%r',
 
52
                     'byte': '%d',
 
53
                     'short': '%d',
 
54
                     'intc': '%d',
 
55
                     'int_': '%d',
 
56
                     'longlong': '%d',
 
57
                     'intp': '%d',
 
58
                     'int8': '%d',
 
59
                     'int16': '%d',
 
60
                     'int32': '%d',
 
61
                     'int64': '%d',
 
62
                     'ubyte': '%d',
 
63
                     'ushort': '%d',
 
64
                     'uintc': '%d',
 
65
                     'uint': '%d',
 
66
                     'ulonglong': '%d',
 
67
                     'uintp': '%d',
 
68
                     'uint8': '%d',
 
69
                     'uint16': '%d',
 
70
                     'uint32': '%d',
 
71
                     'uint64': '%d',
 
72
                     'bool_': '%r',
 
73
                     'bool8': '%r',
 
74
                     'bool': '%r',
 
75
                     }
 
76
 
 
77
def is_float(dtype):
 
78
    """Return True if datatype dtype is a float kind"""
 
79
    return ('float' in dtype.name) or dtype.name in ['single', 'double']
 
80
 
 
81
def is_number(dtype):
 
82
    """Return True is datatype dtype is a number kind"""
 
83
    return is_float(dtype) or ('int' in dtype.name) or ('long' in dtype.name) \
 
84
           or ('short' in dtype.name)
 
85
 
 
86
def get_idx_rect(index_list):
 
87
    """Extract the boundaries from a list of indexes"""
 
88
    rows, cols = zip(*[(i.row(), i.column()) for i in index_list])
 
89
    return ( min(rows), max(rows), min(cols), max(cols) )
 
90
 
 
91
 
 
92
class ArrayModel(QAbstractTableModel):
 
93
    """Array Editor Table Model"""
 
94
    def __init__(self, data, format="%.3f", xlabels=None, ylabels=None,
 
95
                 readonly=False, parent=None):
 
96
        QAbstractTableModel.__init__(self)
 
97
 
 
98
        self.dialog = parent
 
99
        self.changes = {}
 
100
        self.xlabels = xlabels
 
101
        self.ylabels = ylabels
 
102
        self.readonly = readonly
 
103
        self.test_array = np.array([0], dtype=data.dtype)
 
104
 
 
105
        # for complex numbers, shading will be based on absolute value
 
106
        # but for all other types it will be the real part
 
107
        if data.dtype in (np.complex64, np.complex128):
 
108
            self.color_func = np.abs
 
109
        else:
 
110
            self.color_func = np.real
 
111
        
 
112
        # Backgroundcolor settings
 
113
        huerange = [.66, .99] # Hue
 
114
        self.sat = .7 # Saturation
 
115
        self.val = 1. # Value
 
116
        self.alp = .6 # Alpha-channel
 
117
 
 
118
        self._data = data
 
119
        self._format = format
 
120
        
 
121
        try:
 
122
            self.vmin = self.color_func(data).min()
 
123
            self.vmax = self.color_func(data).max()
 
124
            if self.vmax == self.vmin:
 
125
                self.vmin -= 1
 
126
            self.hue0 = huerange[0]
 
127
            self.dhue = huerange[1]-huerange[0]
 
128
            self.bgcolor_enabled = True
 
129
        except TypeError:
 
130
            self.vmin = None
 
131
            self.vmax = None
 
132
            self.hue0 = None
 
133
            self.dhue = None
 
134
            self.bgcolor_enabled = False
 
135
        
 
136
        
 
137
    def get_format(self):
 
138
        """Return current format"""
 
139
        # Avoid accessing the private attribute _format from outside
 
140
        return self._format
 
141
    
 
142
    def get_data(self):
 
143
        """Return data"""
 
144
        return self._data
 
145
    
 
146
    def set_format(self, format):
 
147
        """Change display format"""
 
148
        self._format = format
 
149
        self.reset()
 
150
 
 
151
    def columnCount(self, qindex=QModelIndex()):
 
152
        """Array column number"""
 
153
        return self._data.shape[1]
 
154
 
 
155
    def rowCount(self, qindex=QModelIndex()):
 
156
        """Array row number"""
 
157
        return self._data.shape[0]
 
158
 
 
159
    def bgcolor(self, state):
 
160
        """Toggle backgroundcolor"""
 
161
        self.bgcolor_enabled = state > 0
 
162
        self.reset()
 
163
 
 
164
    def get_value(self, index):
 
165
        i = index.row()
 
166
        j = index.column()
 
167
        return self.changes.get((i, j), self._data[i, j])
 
168
 
 
169
    def data(self, index, role=Qt.DisplayRole):
 
170
        """Cell content"""
 
171
        if not index.isValid():
 
172
            return to_qvariant()
 
173
        value = self.get_value(index)
 
174
        if role == Qt.DisplayRole:
 
175
            if value is np.ma.masked:
 
176
                return ''
 
177
            else:
 
178
                return to_qvariant(self._format % value)
 
179
        elif role == Qt.TextAlignmentRole:
 
180
            return to_qvariant(int(Qt.AlignCenter|Qt.AlignVCenter))
 
181
        elif role == Qt.BackgroundColorRole and self.bgcolor_enabled\
 
182
             and value is not np.ma.masked:
 
183
            hue = self.hue0+\
 
184
                  self.dhue*(self.vmax-self.color_func(value))\
 
185
                  /(self.vmax-self.vmin)
 
186
            hue = float(np.abs(hue))
 
187
            color = QColor.fromHsvF(hue, self.sat, self.val, self.alp)
 
188
            return to_qvariant(color)
 
189
        elif role == Qt.FontRole:
 
190
            return to_qvariant(get_font('arrayeditor'))
 
191
        return to_qvariant()
 
192
 
 
193
    def setData(self, index, value, role=Qt.EditRole):
 
194
        """Cell content change"""
 
195
        if not index.isValid() or self.readonly:
 
196
            return False
 
197
        i = index.row()
 
198
        j = index.column()
 
199
        value = from_qvariant(value, str)
 
200
        if self._data.dtype.name == "bool":
 
201
            try:
 
202
                val = bool(float(value))
 
203
            except ValueError:
 
204
                val = value.lower() == "true"
 
205
        elif self._data.dtype.name.startswith("string"):
 
206
            val = str(value)
 
207
        elif self._data.dtype.name.startswith("unicode"):
 
208
            val = unicode(value)
 
209
        else:
 
210
            if value.lower().startswith('e') or value.lower().endswith('e'):
 
211
                return False
 
212
            try:
 
213
                val = complex(value)
 
214
                if not val.imag:
 
215
                    val = val.real
 
216
            except ValueError, e:
 
217
                QMessageBox.critical(self.dialog, "Error",
 
218
                                     "Value error: %s" % str(e))
 
219
                return False
 
220
        try:
 
221
            self.test_array[0] = val # will raise an Exception eventually
 
222
        except OverflowError, e:
 
223
            print type(e.message)
 
224
            QMessageBox.critical(self.dialog, "Error",
 
225
                                 "Overflow error: %s" % e.message)
 
226
            return False
 
227
        
 
228
        # Add change to self.changes
 
229
        self.changes[(i, j)] = val
 
230
        self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"),
 
231
                  index, index)
 
232
        if val > self.vmax:
 
233
            self.vmax = val
 
234
        if val < self.vmin:
 
235
            self.vmin = val
 
236
        return True
 
237
    
 
238
    def flags(self, index):
 
239
        """Set editable flag"""
 
240
        if not index.isValid():
 
241
            return Qt.ItemIsEnabled
 
242
        return Qt.ItemFlags(QAbstractTableModel.flags(self, index)|
 
243
                            Qt.ItemIsEditable)
 
244
                
 
245
    def headerData(self, section, orientation, role=Qt.DisplayRole):
 
246
        """Set header data"""
 
247
        if role != Qt.DisplayRole:
 
248
            return to_qvariant()
 
249
        labels = self.xlabels if orientation == Qt.Horizontal else self.ylabels
 
250
        if labels is None:
 
251
            return to_qvariant(int(section))
 
252
        else:
 
253
            return to_qvariant(labels[section])
 
254
 
 
255
 
 
256
class ArrayDelegate(QItemDelegate):
 
257
    """Array Editor Item Delegate"""
 
258
    def __init__(self, dtype, parent=None):
 
259
        QItemDelegate.__init__(self, parent)
 
260
        self.dtype = dtype
 
261
 
 
262
    def createEditor(self, parent, option, index):
 
263
        """Create editor widget"""
 
264
        model = index.model()
 
265
        value = model.get_value(index)
 
266
        if model._data.dtype.name == "bool":
 
267
            value = not value
 
268
            model.setData(index, to_qvariant(value))
 
269
            return
 
270
        elif value is not np.ma.masked:
 
271
            editor = QLineEdit(parent)
 
272
            editor.setFont(get_font('arrayeditor'))
 
273
            editor.setAlignment(Qt.AlignCenter)
 
274
            if is_number(self.dtype):
 
275
                editor.setValidator(QDoubleValidator(editor))
 
276
            self.connect(editor, SIGNAL("returnPressed()"),
 
277
                         self.commitAndCloseEditor)
 
278
            return editor
 
279
 
 
280
    def commitAndCloseEditor(self):
 
281
        """Commit and close editor"""
 
282
        editor = self.sender()
 
283
        self.emit(SIGNAL("commitData(QWidget*)"), editor)
 
284
        self.emit(SIGNAL("closeEditor(QWidget*)"), editor)
 
285
 
 
286
    def setEditorData(self, editor, index):
 
287
        """Set editor widget's data"""
 
288
        text = from_qvariant(index.model().data(index, Qt.DisplayRole), str)
 
289
        editor.setText(text)
 
290
 
 
291
 
 
292
#TODO: Implement "Paste" (from clipboard) feature
 
293
class ArrayView(QTableView):
 
294
    """Array view class"""
 
295
    def __init__(self, parent, model, dtype, shape):
 
296
        QTableView.__init__(self, parent)
 
297
 
 
298
        self.setModel(model)
 
299
        self.setItemDelegate(ArrayDelegate(dtype, self))
 
300
        total_width = 0
 
301
        for k in xrange(shape[1]):
 
302
            total_width += self.columnWidth(k)
 
303
        self.viewport().resize(min(total_width, 1024), self.height())
 
304
        self.shape = shape
 
305
        self.menu = self.setup_menu()
 
306
  
 
307
    def resize_to_contents(self):
 
308
        """Resize cells to contents"""
 
309
        size = 1
 
310
        for dim in self.shape:
 
311
            size *= dim
 
312
        if size > 1e5:
 
313
            answer = QMessageBox.warning(self, _("Array editor"),
 
314
                                         _("Resizing cells of a table of such "
 
315
                                           "size could take a long time.\n"
 
316
                                           "Do you want to continue anyway?"),
 
317
                                         QMessageBox.Yes | QMessageBox.No)
 
318
            if answer == QMessageBox.No:
 
319
                return
 
320
        self.resizeColumnsToContents()
 
321
        self.resizeRowsToContents()
 
322
 
 
323
    def setup_menu(self):
 
324
        """Setup context menu"""
 
325
        self.copy_action = create_action(self, _( "Copy"),
 
326
                                         shortcut=keybinding("Copy"),
 
327
                                         icon=get_icon('editcopy.png'),
 
328
                                         triggered=self.copy,
 
329
                                         context=Qt.WidgetShortcut)
 
330
        menu = QMenu(self)
 
331
        add_actions(menu, [self.copy_action, ])
 
332
        return menu
 
333
 
 
334
    def contextMenuEvent(self, event):
 
335
        """Reimplement Qt method"""
 
336
        self.menu.popup(event.globalPos())
 
337
        event.accept()
 
338
        
 
339
    def keyPressEvent(self, event):
 
340
        """Reimplement Qt method"""
 
341
        if event == QKeySequence.Copy:
 
342
            self.copy()
 
343
        else:
 
344
            QTableView.keyPressEvent(self, event)
 
345
 
 
346
    def _sel_to_text(self, cell_range):
 
347
        """Copy an array portion to a unicode string"""
 
348
        row_min, row_max, col_min, col_max = get_idx_rect(cell_range)
 
349
        _data = self.model().get_data()
 
350
        output = StringIO.StringIO()
 
351
        np.savetxt(output,
 
352
                  _data[row_min:row_max+1, col_min:col_max+1],
 
353
                  delimiter='\t')
 
354
        contents = output.getvalue()
 
355
        output.close()
 
356
        return contents
 
357
    
 
358
    def copy(self):
 
359
        """Copy text to clipboard"""
 
360
        cliptxt = self._sel_to_text( self.selectedIndexes() )
 
361
        clipboard = QApplication.clipboard()
 
362
        clipboard.setText(cliptxt)
 
363
 
 
364
 
 
365
class ArrayEditorWidget(QWidget):
 
366
    def __init__(self, parent, data, readonly=False,
 
367
                 xlabels=None, ylabels=None):
 
368
        QWidget.__init__(self, parent)
 
369
        self.data = data
 
370
        self.old_data_shape = None
 
371
        if len(self.data.shape) == 1:
 
372
            self.old_data_shape = self.data.shape
 
373
            self.data.shape = (self.data.shape[0], 1)
 
374
        elif len(self.data.shape) == 0:
 
375
            self.old_data_shape = self.data.shape
 
376
            self.data.shape = (1, 1)
 
377
 
 
378
        format = SUPPORTED_FORMATS.get(data.dtype.name, '%s')
 
379
        self.model = ArrayModel(self.data, format=format, xlabels=xlabels,
 
380
                                ylabels=ylabels, readonly=readonly, parent=self)
 
381
        self.view = ArrayView(self, self.model, data.dtype, data.shape)
 
382
        
 
383
        btn_layout = QHBoxLayout()
 
384
        btn_layout.setAlignment(Qt.AlignLeft)
 
385
        btn = QPushButton(_( "Format"))
 
386
        # disable format button for int type
 
387
        btn.setEnabled(is_float(data.dtype))
 
388
        btn_layout.addWidget(btn)
 
389
        self.connect(btn, SIGNAL("clicked()"), self.change_format)
 
390
        btn = QPushButton(_( "Resize"))
 
391
        btn_layout.addWidget(btn)
 
392
        self.connect(btn, SIGNAL("clicked()"), self.view.resize_to_contents)
 
393
        bgcolor = QCheckBox(_( 'Background color'))
 
394
        bgcolor.setChecked(self.model.bgcolor_enabled)
 
395
        bgcolor.setEnabled(self.model.bgcolor_enabled)
 
396
        self.connect(bgcolor, SIGNAL("stateChanged(int)"), self.model.bgcolor)
 
397
        btn_layout.addWidget(bgcolor)
 
398
        
 
399
        layout = QVBoxLayout()
 
400
        layout.addWidget(self.view)
 
401
        layout.addLayout(btn_layout)        
 
402
        self.setLayout(layout)
 
403
        
 
404
    def accept_changes(self):
 
405
        """Accept changes"""
 
406
        for (i, j), value in self.model.changes.iteritems():
 
407
            self.data[i, j] = value
 
408
        if self.old_data_shape is not None:
 
409
            self.data.shape = self.old_data_shape
 
410
            
 
411
    def reject_changes(self):
 
412
        """Reject changes"""
 
413
        if self.old_data_shape is not None:
 
414
            self.data.shape = self.old_data_shape
 
415
        
 
416
    def change_format(self):
 
417
        """Change display format"""
 
418
        format, valid = QInputDialog.getText(self, _( 'Format'),
 
419
                                 _( "Float formatting"),
 
420
                                 QLineEdit.Normal, self.model.get_format())
 
421
        if valid:
 
422
            format = str(format)
 
423
            try:
 
424
                format % 1.1
 
425
            except:
 
426
                QMessageBox.critical(self, _("Error"),
 
427
                                     _("Format (%s) is incorrect") % format)
 
428
                return
 
429
            self.model.set_format(format)    
 
430
 
 
431
 
 
432
class ArrayEditor(QDialog):
 
433
    """Array Editor Dialog"""    
 
434
    def __init__(self, parent=None):
 
435
        QDialog.__init__(self, parent)
 
436
        
 
437
        # Destroying the C++ object right after closing the dialog box,
 
438
        # otherwise it may be garbage-collected in another QThread
 
439
        # (e.g. the editor's analysis thread in Spyder), thus leading to
 
440
        # a segmentation fault on UNIX or an application crash on Windows
 
441
        self.setAttribute(Qt.WA_DeleteOnClose)
 
442
        
 
443
        self.data = None
 
444
        self.arraywidget = None
 
445
        self.stack = None
 
446
        self.layout = None
 
447
    
 
448
    def setup_and_check(self, data, title='', readonly=False,
 
449
                        xlabels=None, ylabels=None):
 
450
        """
 
451
        Setup ArrayEditor:
 
452
        return False if data is not supported, True otherwise
 
453
        """
 
454
        self.data = data
 
455
        is_record_array = data.dtype.names is not None
 
456
        is_masked_array = isinstance(data, np.ma.MaskedArray)
 
457
        if data.size == 0:
 
458
            self.error(_("Array is empty"))
 
459
            return False
 
460
        if data.ndim > 2:
 
461
            self.error(_("Arrays with more than 2 dimensions "
 
462
                               "are not supported"))
 
463
            return False
 
464
        if xlabels is not None and len(xlabels) != self.data.shape[1]:
 
465
            self.error(_("The 'xlabels' argument length "
 
466
                                                           "do no match array column number"))
 
467
            return False
 
468
        if ylabels is not None and len(ylabels) != self.data.shape[0]:
 
469
            self.error(_("The 'ylabels' argument length "
 
470
                                                           "do no match array row number"))
 
471
            return False
 
472
        if not is_record_array:
 
473
            dtn = data.dtype.name
 
474
            if dtn not in SUPPORTED_FORMATS and not dtn.startswith('string') \
 
475
               and not dtn.startswith('unicode'):
 
476
                arr = _("%s arrays") % data.dtype.name
 
477
                self.error(_("%s are currently not supported") % arr)
 
478
                return False
 
479
        
 
480
        self.layout = QGridLayout()
 
481
        self.setLayout(self.layout)
 
482
        self.setWindowIcon(get_icon('arredit.png'))
 
483
        if title:
 
484
            title = unicode(title) # in case title is not a string
 
485
        else:
 
486
            title = _("Array editor")
 
487
        if readonly:
 
488
            title += ' (' + _('read only') + ')'
 
489
        self.setWindowTitle(title)
 
490
        self.resize(600, 500)
 
491
        
 
492
        # Stack widget
 
493
        self.stack = QStackedWidget(self)
 
494
        if is_record_array:
 
495
            for name in data.dtype.names:
 
496
                self.stack.addWidget(ArrayEditorWidget(self, data[name],
 
497
                                                   readonly, xlabels, ylabels))
 
498
        elif is_masked_array:
 
499
            self.stack.addWidget(ArrayEditorWidget(self, data, readonly,
 
500
                                                   xlabels, ylabels))
 
501
            self.stack.addWidget(ArrayEditorWidget(self, data.data, readonly,
 
502
                                                   xlabels, ylabels))
 
503
            self.stack.addWidget(ArrayEditorWidget(self, data.mask, readonly,
 
504
                                                   xlabels, ylabels))
 
505
        else:
 
506
            self.stack.addWidget(ArrayEditorWidget(self, data, readonly,
 
507
                                                   xlabels, ylabels))
 
508
        self.arraywidget = self.stack.currentWidget()
 
509
        self.connect(self.stack, SIGNAL('currentChanged(int)'),
 
510
                     self.current_widget_changed)
 
511
        self.layout.addWidget(self.stack, 1, 0)
 
512
 
 
513
        # Buttons configuration
 
514
        btn_layout = QHBoxLayout()
 
515
        if is_record_array or is_masked_array:
 
516
            if is_record_array:
 
517
                btn_layout.addWidget(QLabel(_("Record array fields:")))
 
518
                names = []
 
519
                for name in data.dtype.names:
 
520
                    field = data.dtype.fields[name]
 
521
                    text = name
 
522
                    if len(field) >= 3:
 
523
                        title = field[2]
 
524
                        if not isinstance(title, basestring):
 
525
                            title = repr(title)
 
526
                        text += ' - '+title
 
527
                    names.append(text)
 
528
            else:
 
529
                names = [_('Masked data'), _('Data'), _('Mask')]
 
530
            ra_combo = QComboBox(self)
 
531
            self.connect(ra_combo, SIGNAL('currentIndexChanged(int)'),
 
532
                         self.stack.setCurrentIndex)
 
533
            ra_combo.addItems(names)
 
534
            btn_layout.addWidget(ra_combo)
 
535
            if is_masked_array:
 
536
                label = QLabel(_("<u>Warning</u>: changes are applied separately"))
 
537
                label.setToolTip(_("For performance reasons, changes applied "\
 
538
                                   "to masked array won't be reflected in "\
 
539
                                   "array's data (and vice-versa)."))
 
540
                btn_layout.addWidget(label)
 
541
            btn_layout.addStretch()
 
542
        bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
 
543
        self.connect(bbox, SIGNAL("accepted()"), SLOT("accept()"))
 
544
        self.connect(bbox, SIGNAL("rejected()"), SLOT("reject()"))
 
545
        btn_layout.addWidget(bbox)
 
546
        self.layout.addLayout(btn_layout, 2, 0)
 
547
        
 
548
        self.setMinimumSize(400, 300)
 
549
        
 
550
        # Make the dialog act as a window
 
551
        self.setWindowFlags(Qt.Window)
 
552
        
 
553
        return True
 
554
        
 
555
    def current_widget_changed(self, index):
 
556
        self.arraywidget = self.stack.widget(index)
 
557
        
 
558
    def accept(self):
 
559
        """Reimplement Qt method"""
 
560
        for index in range(self.stack.count()):
 
561
            self.stack.widget(index).accept_changes()
 
562
        QDialog.accept(self)
 
563
        
 
564
    def get_value(self):
 
565
        """Return modified array -- this is *not* a copy"""
 
566
        # It is import to avoid accessing Qt C++ object as it has probably
 
567
        # already been destroyed, due to the Qt.WA_DeleteOnClose attribute
 
568
        return self.data
 
569
 
 
570
    def error(self, message):
 
571
        """An error occured, closing the dialog box"""
 
572
        QMessageBox.critical(self, _("Array editor"), message)
 
573
        self.setAttribute(Qt.WA_DeleteOnClose)
 
574
        self.reject()
 
575
 
 
576
    def reject(self):
 
577
        """Reimplement Qt method"""
 
578
        if self.arraywidget is not None:
 
579
            for index in range(self.stack.count()):
 
580
                self.stack.widget(index).reject_changes()
 
581
        QDialog.reject(self)
 
582
    
 
583
    
 
584
def test_edit(data, title="", xlabels=None, ylabels=None,
 
585
              readonly=False, parent=None):
 
586
    """Test subroutine"""
 
587
    dlg = ArrayEditor(parent)
 
588
    if dlg.setup_and_check(data, title, xlabels=xlabels, ylabels=ylabels,
 
589
                           readonly=readonly) and dlg.exec_():
 
590
        return dlg.get_value()
 
591
    else:
 
592
        import sys
 
593
        sys.exit()
 
594
 
 
595
 
 
596
def test():
 
597
    """Array editor test"""
 
598
    _app = qapplication()
 
599
    
 
600
    arr = np.array(["kjrekrjkejr"])
 
601
    print "out:", test_edit(arr, "string array")
 
602
    arr = np.array([u"kjrekrjkejr"])
 
603
    print "out:", test_edit(arr, "unicode array")
 
604
    arr = np.ma.array([[1, 0], [1, 0]], mask=[[True, False], [False, False]])
 
605
    print "out:", test_edit(arr, "masked array")
 
606
    arr = np.zeros((2,2), {'names': ('red', 'green', 'blue'),
 
607
                           'formats': (np.float32, np.float32, np.float32)})
 
608
    print "out:", test_edit(arr, "record array")
 
609
    arr = np.array([(0, 0.0), (0, 0.0), (0, 0.0)],
 
610
                   dtype=[(('title 1', 'x'), '|i1'),
 
611
                          (('title 2', 'y'), '>f4')])
 
612
    print "out:", test_edit(arr, "record array with titles")
 
613
    arr = np.random.rand(5, 5)
 
614
    print "out:", test_edit(arr, "float array",
 
615
                            xlabels=['a', 'b', 'c', 'd', 'e'])
 
616
    arr = np.round(np.random.rand(5, 5)*10)+\
 
617
                   np.round(np.random.rand(5, 5)*10)*1j
 
618
    print "out:", test_edit(arr, "complex array",
 
619
                            xlabels=np.linspace(-12, 12, 5),
 
620
                            ylabels=np.linspace(-12, 12, 5))
 
621
    arr_in = np.array([True, False, True])
 
622
    print "in:", arr_in
 
623
    arr_out = test_edit(arr_in, "bool array")
 
624
    print "out:", arr_out
 
625
    print arr_in is arr_out
 
626
    arr = np.array([1, 2, 3], dtype="int8")
 
627
    print "out:", test_edit(arr, "int array")
 
628
 
 
629
 
 
630
if __name__ == "__main__":
 
631
    test()