1
# -*- coding: utf-8 -*-
3
# Copyright © 2009-2012 Pierre Raybaut
4
# Licensed under the terms of the MIT License
5
# (see spyderlib/__init__.py for details)
8
NumPy Array Editor Dialog based on Qt
11
# pylint: disable=C0103
12
# pylint: disable=R0903
13
# pylint: disable=R0911
14
# pylint: disable=R0201
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,
24
from spyderlib.qt.compat import to_qvariant, from_qvariant
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,
35
# Note: string and unicode data types will be formatted with '%s' (see below)
78
"""Return True if datatype dtype is a float kind"""
79
return ('float' in dtype.name) or dtype.name in ['single', 'double']
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)
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) )
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)
100
self.xlabels = xlabels
101
self.ylabels = ylabels
102
self.readonly = readonly
103
self.test_array = np.array([0], dtype=data.dtype)
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
110
self.color_func = np.real
112
# Backgroundcolor settings
113
huerange = [.66, .99] # Hue
114
self.sat = .7 # Saturation
115
self.val = 1. # Value
116
self.alp = .6 # Alpha-channel
119
self._format = format
122
self.vmin = self.color_func(data).min()
123
self.vmax = self.color_func(data).max()
124
if self.vmax == self.vmin:
126
self.hue0 = huerange[0]
127
self.dhue = huerange[1]-huerange[0]
128
self.bgcolor_enabled = True
134
self.bgcolor_enabled = False
137
def get_format(self):
138
"""Return current format"""
139
# Avoid accessing the private attribute _format from outside
146
def set_format(self, format):
147
"""Change display format"""
148
self._format = format
151
def columnCount(self, qindex=QModelIndex()):
152
"""Array column number"""
153
return self._data.shape[1]
155
def rowCount(self, qindex=QModelIndex()):
156
"""Array row number"""
157
return self._data.shape[0]
159
def bgcolor(self, state):
160
"""Toggle backgroundcolor"""
161
self.bgcolor_enabled = state > 0
164
def get_value(self, index):
167
return self.changes.get((i, j), self._data[i, j])
169
def data(self, index, role=Qt.DisplayRole):
171
if not index.isValid():
173
value = self.get_value(index)
174
if role == Qt.DisplayRole:
175
if value is np.ma.masked:
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:
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'))
193
def setData(self, index, value, role=Qt.EditRole):
194
"""Cell content change"""
195
if not index.isValid() or self.readonly:
199
value = from_qvariant(value, str)
200
if self._data.dtype.name == "bool":
202
val = bool(float(value))
204
val = value.lower() == "true"
205
elif self._data.dtype.name.startswith("string"):
207
elif self._data.dtype.name.startswith("unicode"):
210
if value.lower().startswith('e') or value.lower().endswith('e'):
216
except ValueError, e:
217
QMessageBox.critical(self.dialog, "Error",
218
"Value error: %s" % str(e))
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)
228
# Add change to self.changes
229
self.changes[(i, j)] = val
230
self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"),
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)|
245
def headerData(self, section, orientation, role=Qt.DisplayRole):
246
"""Set header data"""
247
if role != Qt.DisplayRole:
249
labels = self.xlabels if orientation == Qt.Horizontal else self.ylabels
251
return to_qvariant(int(section))
253
return to_qvariant(labels[section])
256
class ArrayDelegate(QItemDelegate):
257
"""Array Editor Item Delegate"""
258
def __init__(self, dtype, parent=None):
259
QItemDelegate.__init__(self, parent)
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":
268
model.setData(index, to_qvariant(value))
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)
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)
286
def setEditorData(self, editor, index):
287
"""Set editor widget's data"""
288
text = from_qvariant(index.model().data(index, Qt.DisplayRole), str)
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)
299
self.setItemDelegate(ArrayDelegate(dtype, self))
301
for k in xrange(shape[1]):
302
total_width += self.columnWidth(k)
303
self.viewport().resize(min(total_width, 1024), self.height())
305
self.menu = self.setup_menu()
307
def resize_to_contents(self):
308
"""Resize cells to contents"""
310
for dim in self.shape:
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:
320
self.resizeColumnsToContents()
321
self.resizeRowsToContents()
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'),
329
context=Qt.WidgetShortcut)
331
add_actions(menu, [self.copy_action, ])
334
def contextMenuEvent(self, event):
335
"""Reimplement Qt method"""
336
self.menu.popup(event.globalPos())
339
def keyPressEvent(self, event):
340
"""Reimplement Qt method"""
341
if event == QKeySequence.Copy:
344
QTableView.keyPressEvent(self, event)
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()
352
_data[row_min:row_max+1, col_min:col_max+1],
354
contents = output.getvalue()
359
"""Copy text to clipboard"""
360
cliptxt = self._sel_to_text( self.selectedIndexes() )
361
clipboard = QApplication.clipboard()
362
clipboard.setText(cliptxt)
365
class ArrayEditorWidget(QWidget):
366
def __init__(self, parent, data, readonly=False,
367
xlabels=None, ylabels=None):
368
QWidget.__init__(self, parent)
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)
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)
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)
399
layout = QVBoxLayout()
400
layout.addWidget(self.view)
401
layout.addLayout(btn_layout)
402
self.setLayout(layout)
404
def accept_changes(self):
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
411
def reject_changes(self):
413
if self.old_data_shape is not None:
414
self.data.shape = self.old_data_shape
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())
426
QMessageBox.critical(self, _("Error"),
427
_("Format (%s) is incorrect") % format)
429
self.model.set_format(format)
432
class ArrayEditor(QDialog):
433
"""Array Editor Dialog"""
434
def __init__(self, parent=None):
435
QDialog.__init__(self, parent)
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)
444
self.arraywidget = None
448
def setup_and_check(self, data, title='', readonly=False,
449
xlabels=None, ylabels=None):
452
return False if data is not supported, True otherwise
455
is_record_array = data.dtype.names is not None
456
is_masked_array = isinstance(data, np.ma.MaskedArray)
458
self.error(_("Array is empty"))
461
self.error(_("Arrays with more than 2 dimensions "
462
"are not supported"))
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"))
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"))
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)
480
self.layout = QGridLayout()
481
self.setLayout(self.layout)
482
self.setWindowIcon(get_icon('arredit.png'))
484
title = unicode(title) # in case title is not a string
486
title = _("Array editor")
488
title += ' (' + _('read only') + ')'
489
self.setWindowTitle(title)
490
self.resize(600, 500)
493
self.stack = QStackedWidget(self)
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,
501
self.stack.addWidget(ArrayEditorWidget(self, data.data, readonly,
503
self.stack.addWidget(ArrayEditorWidget(self, data.mask, readonly,
506
self.stack.addWidget(ArrayEditorWidget(self, data, readonly,
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)
513
# Buttons configuration
514
btn_layout = QHBoxLayout()
515
if is_record_array or is_masked_array:
517
btn_layout.addWidget(QLabel(_("Record array fields:")))
519
for name in data.dtype.names:
520
field = data.dtype.fields[name]
524
if not isinstance(title, basestring):
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)
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)
548
self.setMinimumSize(400, 300)
550
# Make the dialog act as a window
551
self.setWindowFlags(Qt.Window)
555
def current_widget_changed(self, index):
556
self.arraywidget = self.stack.widget(index)
559
"""Reimplement Qt method"""
560
for index in range(self.stack.count()):
561
self.stack.widget(index).accept_changes()
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
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)
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()
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()
597
"""Array editor test"""
598
_app = qapplication()
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])
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")
630
if __name__ == "__main__":
1
# -*- coding: utf-8 -*-
3
# Copyright © 2009-2012 Pierre Raybaut
4
# Licensed under the terms of the MIT License
5
# (see spyderlib/__init__.py for details)
8
NumPy Array Editor Dialog based on Qt
11
# pylint: disable=C0103
12
# pylint: disable=R0903
13
# pylint: disable=R0911
14
# pylint: disable=R0201
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,
24
from spyderlib.qt.compat import to_qvariant, from_qvariant
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,
35
# Note: string and unicode data types will be formatted with '%s' (see below)
78
"""Return True if datatype dtype is a float kind"""
79
return ('float' in dtype.name) or dtype.name in ['single', 'double']
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)
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) )
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)
100
self.xlabels = xlabels
101
self.ylabels = ylabels
102
self.readonly = readonly
103
self.test_array = np.array([0], dtype=data.dtype)
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
110
self.color_func = np.real
112
# Backgroundcolor settings
113
huerange = [.66, .99] # Hue
114
self.sat = .7 # Saturation
115
self.val = 1. # Value
116
self.alp = .6 # Alpha-channel
119
self._format = format
122
self.vmin = self.color_func(data).min()
123
self.vmax = self.color_func(data).max()
124
if self.vmax == self.vmin:
126
self.hue0 = huerange[0]
127
self.dhue = huerange[1]-huerange[0]
128
self.bgcolor_enabled = True
134
self.bgcolor_enabled = False
137
def get_format(self):
138
"""Return current format"""
139
# Avoid accessing the private attribute _format from outside
146
def set_format(self, format):
147
"""Change display format"""
148
self._format = format
151
def columnCount(self, qindex=QModelIndex()):
152
"""Array column number"""
153
return self._data.shape[1]
155
def rowCount(self, qindex=QModelIndex()):
156
"""Array row number"""
157
return self._data.shape[0]
159
def bgcolor(self, state):
160
"""Toggle backgroundcolor"""
161
self.bgcolor_enabled = state > 0
164
def get_value(self, index):
167
return self.changes.get((i, j), self._data[i, j])
169
def data(self, index, role=Qt.DisplayRole):
171
if not index.isValid():
173
value = self.get_value(index)
174
if role == Qt.DisplayRole:
175
if value is np.ma.masked:
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:
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'))
193
def setData(self, index, value, role=Qt.EditRole):
194
"""Cell content change"""
195
if not index.isValid() or self.readonly:
199
value = from_qvariant(value, str)
200
if self._data.dtype.name == "bool":
202
val = bool(float(value))
204
val = value.lower() == "true"
205
elif self._data.dtype.name.startswith("string"):
207
elif self._data.dtype.name.startswith("unicode"):
210
if value.lower().startswith('e') or value.lower().endswith('e'):
216
except ValueError, e:
217
QMessageBox.critical(self.dialog, "Error",
218
"Value error: %s" % str(e))
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)
228
# Add change to self.changes
229
self.changes[(i, j)] = val
230
self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"),
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)|
245
def headerData(self, section, orientation, role=Qt.DisplayRole):
246
"""Set header data"""
247
if role != Qt.DisplayRole:
249
labels = self.xlabels if orientation == Qt.Horizontal else self.ylabels
251
return to_qvariant(int(section))
253
return to_qvariant(labels[section])
256
class ArrayDelegate(QItemDelegate):
257
"""Array Editor Item Delegate"""
258
def __init__(self, dtype, parent=None):
259
QItemDelegate.__init__(self, parent)
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":
268
model.setData(index, to_qvariant(value))
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)
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)
286
def setEditorData(self, editor, index):
287
"""Set editor widget's data"""
288
text = from_qvariant(index.model().data(index, Qt.DisplayRole), str)
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)
299
self.setItemDelegate(ArrayDelegate(dtype, self))
301
for k in xrange(shape[1]):
302
total_width += self.columnWidth(k)
303
self.viewport().resize(min(total_width, 1024), self.height())
305
self.menu = self.setup_menu()
307
def resize_to_contents(self):
308
"""Resize cells to contents"""
310
for dim in self.shape:
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:
320
self.resizeColumnsToContents()
321
self.resizeRowsToContents()
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'),
329
context=Qt.WidgetShortcut)
331
add_actions(menu, [self.copy_action, ])
334
def contextMenuEvent(self, event):
335
"""Reimplement Qt method"""
336
self.menu.popup(event.globalPos())
339
def keyPressEvent(self, event):
340
"""Reimplement Qt method"""
341
if event == QKeySequence.Copy:
344
QTableView.keyPressEvent(self, event)
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()
352
_data[row_min:row_max+1, col_min:col_max+1],
354
contents = output.getvalue()
359
"""Copy text to clipboard"""
360
cliptxt = self._sel_to_text( self.selectedIndexes() )
361
clipboard = QApplication.clipboard()
362
clipboard.setText(cliptxt)
365
class ArrayEditorWidget(QWidget):
366
def __init__(self, parent, data, readonly=False,
367
xlabels=None, ylabels=None):
368
QWidget.__init__(self, parent)
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)
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)
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)
399
layout = QVBoxLayout()
400
layout.addWidget(self.view)
401
layout.addLayout(btn_layout)
402
self.setLayout(layout)
404
def accept_changes(self):
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
411
def reject_changes(self):
413
if self.old_data_shape is not None:
414
self.data.shape = self.old_data_shape
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())
426
QMessageBox.critical(self, _("Error"),
427
_("Format (%s) is incorrect") % format)
429
self.model.set_format(format)
432
class ArrayEditor(QDialog):
433
"""Array Editor Dialog"""
434
def __init__(self, parent=None):
435
QDialog.__init__(self, parent)
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)
444
self.arraywidget = None
448
def setup_and_check(self, data, title='', readonly=False,
449
xlabels=None, ylabels=None):
452
return False if data is not supported, True otherwise
455
is_record_array = data.dtype.names is not None
456
is_masked_array = isinstance(data, np.ma.MaskedArray)
458
self.error(_("Array is empty"))
461
self.error(_("Arrays with more than 2 dimensions "
462
"are not supported"))
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"))
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"))
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)
480
self.layout = QGridLayout()
481
self.setLayout(self.layout)
482
self.setWindowIcon(get_icon('arredit.png'))
484
title = unicode(title) # in case title is not a string
486
title = _("Array editor")
488
title += ' (' + _('read only') + ')'
489
self.setWindowTitle(title)
490
self.resize(600, 500)
493
self.stack = QStackedWidget(self)
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,
501
self.stack.addWidget(ArrayEditorWidget(self, data.data, readonly,
503
self.stack.addWidget(ArrayEditorWidget(self, data.mask, readonly,
506
self.stack.addWidget(ArrayEditorWidget(self, data, readonly,
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)
513
# Buttons configuration
514
btn_layout = QHBoxLayout()
515
if is_record_array or is_masked_array:
517
btn_layout.addWidget(QLabel(_("Record array fields:")))
519
for name in data.dtype.names:
520
field = data.dtype.fields[name]
524
if not isinstance(title, basestring):
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)
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)
548
self.setMinimumSize(400, 300)
550
# Make the dialog act as a window
551
self.setWindowFlags(Qt.Window)
555
def current_widget_changed(self, index):
556
self.arraywidget = self.stack.widget(index)
559
"""Reimplement Qt method"""
560
for index in range(self.stack.count()):
561
self.stack.widget(index).accept_changes()
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
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)
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()
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()
597
"""Array editor test"""
598
_app = qapplication()
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])
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")
630
if __name__ == "__main__":