1# -*- coding: utf-8 -*-
2#
3# Copyright © Spyder Project Contributors
4# Licensed under the terms of the MIT License
5# (see spyder/__init__.py for details)
6
7"""
8Collections (i.e. dictionary, list and tuple) editor widget and dialog
9"""
10
11#TODO: Multiple selection: open as many editors (array/dict/...) as necessary,
12#      at the same time
13
14# pylint: disable=C0103
15# pylint: disable=R0903
16# pylint: disable=R0911
17# pylint: disable=R0201
18
19# Standard library imports
20from __future__ import print_function
21import datetime
22import gc
23import sys
24import warnings
25
26# Third party imports
27from qtpy.compat import getsavefilename, to_qvariant
28from qtpy.QtCore import (QAbstractTableModel, QDateTime, QModelIndex, Qt,
29                         Signal, Slot)
30from qtpy.QtGui import QColor, QKeySequence
31from qtpy.QtWidgets import (QAbstractItemDelegate, QApplication, QDateEdit,
32                            QDateTimeEdit, QDialog, QDialogButtonBox,
33                            QInputDialog, QItemDelegate, QLineEdit, QMenu,
34                            QMessageBox, QTableView, QVBoxLayout, QWidget)
35
36# Local import
37from spyder.config.base import _
38from spyder.config.fonts import DEFAULT_SMALL_DELTA
39from spyder.config.gui import get_font
40from spyder.py3compat import (io, is_binary_string, is_text_string,
41                              PY3, to_text_string)
42from spyder.utils import icon_manager as ima
43from spyder.utils.misc import fix_reference_name, getcwd_or_home
44from spyder.utils.qthelpers import (add_actions, create_action,
45                                    mimedata2url)
46from spyder.widgets.variableexplorer.importwizard import ImportWizard
47from spyder.widgets.variableexplorer.texteditor import TextEditor
48from spyder.widgets.variableexplorer.utils import (
49    array, DataFrame, DatetimeIndex, display_to_value, FakeObject,
50    get_color_name, get_human_readable_type, get_size, Image, is_editable_type,
51    is_known_type, MaskedArray, ndarray, np_savetxt, Series, sort_against,
52    try_to_eval, unsorted_unique, value_to_display, get_object_attrs,
53    get_type_string)
54
55if ndarray is not FakeObject:
56    from spyder.widgets.variableexplorer.arrayeditor import ArrayEditor
57
58if DataFrame is not FakeObject:
59    from spyder.widgets.variableexplorer.dataframeeditor import DataFrameEditor
60
61
62LARGE_NROWS = 100
63ROWS_TO_LOAD = 50
64
65class ProxyObject(object):
66    """Dictionary proxy to an unknown object."""
67
68    def __init__(self, obj):
69        """Constructor."""
70        self.__obj__ = obj
71
72    def __len__(self):
73        """Get len according to detected attributes."""
74        return len(get_object_attrs(self.__obj__))
75
76    def __getitem__(self, key):
77        """Get attribute corresponding to key."""
78        return getattr(self.__obj__, key)
79
80    def __setitem__(self, key, value):
81        """Set attribute corresponding to key with value."""
82        try:
83            setattr(self.__obj__, key, value)
84        except TypeError:
85            pass
86
87
88class ReadOnlyCollectionsModel(QAbstractTableModel):
89    """CollectionsEditor Read-Only Table Model"""
90
91    def __init__(self, parent, data, title="", names=False,
92                 minmax=False, dataframe_format=None, remote=False):
93        QAbstractTableModel.__init__(self, parent)
94        if data is None:
95            data = {}
96        self.names = names
97        self.minmax = minmax
98        self.dataframe_format = dataframe_format
99        self.remote = remote
100        self.header0 = None
101        self._data = None
102        self.total_rows = None
103        self.showndata = None
104        self.keys = None
105        self.title = to_text_string(title) # in case title is not a string
106        if self.title:
107            self.title = self.title + ' - '
108        self.sizes = []
109        self.types = []
110        self.set_data(data)
111
112    def get_data(self):
113        """Return model data"""
114        return self._data
115
116    def set_data(self, data, coll_filter=None):
117        """Set model data"""
118        self._data = data
119        data_type = get_type_string(data)
120
121        if coll_filter is not None and not self.remote and \
122          isinstance(data, (tuple, list, dict)):
123            data = coll_filter(data)
124        self.showndata = data
125
126        self.header0 = _("Index")
127        if self.names:
128            self.header0 = _("Name")
129        if isinstance(data, tuple):
130            self.keys = list(range(len(data)))
131            self.title += _("Tuple")
132        elif isinstance(data, list):
133            self.keys = list(range(len(data)))
134            self.title += _("List")
135        elif isinstance(data, dict):
136            self.keys = list(data.keys())
137            self.title += _("Dictionary")
138            if not self.names:
139                self.header0 = _("Key")
140        else:
141            self.keys = get_object_attrs(data)
142            self._data = data = self.showndata = ProxyObject(data)
143            if not self.names:
144                self.header0 = _("Attribute")
145
146        if not isinstance(self._data, ProxyObject):
147            self.title += (' (' + str(len(self.keys)) + ' ' +
148                          _("elements") + ')')
149        else:
150            self.title += data_type
151
152        self.total_rows = len(self.keys)
153        if self.total_rows > LARGE_NROWS:
154            self.rows_loaded = ROWS_TO_LOAD
155        else:
156            self.rows_loaded = self.total_rows
157
158        self.set_size_and_type()
159        self.reset()
160
161    def set_size_and_type(self, start=None, stop=None):
162        data = self._data
163
164        if start is None and stop is None:
165            start = 0
166            stop = self.rows_loaded
167            fetch_more = False
168        else:
169            fetch_more = True
170
171        if self.remote:
172            sizes = [ data[self.keys[index]]['size']
173                      for index in range(start, stop) ]
174            types = [ data[self.keys[index]]['type']
175                      for index in range(start, stop) ]
176        else:
177            sizes = [ get_size(data[self.keys[index]])
178                      for index in range(start, stop) ]
179            types = [ get_human_readable_type(data[self.keys[index]])
180                      for index in range(start, stop) ]
181
182        if fetch_more:
183            self.sizes = self.sizes + sizes
184            self.types = self.types + types
185        else:
186            self.sizes = sizes
187            self.types = types
188
189    def sort(self, column, order=Qt.AscendingOrder):
190        """Overriding sort method"""
191        reverse = (order==Qt.DescendingOrder)
192        if column == 0:
193            self.sizes = sort_against(self.sizes, self.keys, reverse)
194            self.types = sort_against(self.types, self.keys, reverse)
195            try:
196                self.keys.sort(reverse=reverse)
197            except:
198                pass
199        elif column == 1:
200            self.keys[:self.rows_loaded] = sort_against(self.keys, self.types,
201                                                        reverse)
202            self.sizes = sort_against(self.sizes, self.types, reverse)
203            try:
204                self.types.sort(reverse=reverse)
205            except:
206                pass
207        elif column == 2:
208            self.keys[:self.rows_loaded] = sort_against(self.keys, self.sizes,
209                                                        reverse)
210            self.types = sort_against(self.types, self.sizes, reverse)
211            try:
212                self.sizes.sort(reverse=reverse)
213            except:
214                pass
215        elif column == 3:
216            values = [self._data[key] for key in self.keys]
217            self.keys = sort_against(self.keys, values, reverse)
218            self.sizes = sort_against(self.sizes, values, reverse)
219            self.types = sort_against(self.types, values, reverse)
220        self.beginResetModel()
221        self.endResetModel()
222
223    def columnCount(self, qindex=QModelIndex()):
224        """Array column number"""
225        return 4
226
227    def rowCount(self, index=QModelIndex()):
228        """Array row number"""
229        if self.total_rows <= self.rows_loaded:
230            return self.total_rows
231        else:
232            return self.rows_loaded
233
234    def canFetchMore(self, index=QModelIndex()):
235        if self.total_rows > self.rows_loaded:
236            return True
237        else:
238            return False
239
240    def fetchMore(self, index=QModelIndex()):
241        reminder = self.total_rows - self.rows_loaded
242        items_to_fetch = min(reminder, ROWS_TO_LOAD)
243        self.set_size_and_type(self.rows_loaded,
244                               self.rows_loaded + items_to_fetch)
245        self.beginInsertRows(QModelIndex(), self.rows_loaded,
246                             self.rows_loaded + items_to_fetch - 1)
247        self.rows_loaded += items_to_fetch
248        self.endInsertRows()
249
250    def get_index_from_key(self, key):
251        try:
252            return self.createIndex(self.keys.index(key), 0)
253        except (RuntimeError, ValueError):
254            return QModelIndex()
255
256    def get_key(self, index):
257        """Return current key"""
258        return self.keys[index.row()]
259
260    def get_value(self, index):
261        """Return current value"""
262        if index.column() == 0:
263            return self.keys[ index.row() ]
264        elif index.column() == 1:
265            return self.types[ index.row() ]
266        elif index.column() == 2:
267            return self.sizes[ index.row() ]
268        else:
269            return self._data[ self.keys[index.row()] ]
270
271    def get_bgcolor(self, index):
272        """Background color depending on value"""
273        if index.column() == 0:
274            color = QColor(Qt.lightGray)
275            color.setAlphaF(.05)
276        elif index.column() < 3:
277            color = QColor(Qt.lightGray)
278            color.setAlphaF(.2)
279        else:
280            color = QColor(Qt.lightGray)
281            color.setAlphaF(.3)
282        return color
283
284    def data(self, index, role=Qt.DisplayRole):
285        """Cell content"""
286        if not index.isValid():
287            return to_qvariant()
288        value = self.get_value(index)
289        if index.column() == 3 and self.remote:
290            value = value['view']
291        if index.column() == 3:
292            display = value_to_display(value, minmax=self.minmax)
293        else:
294             display = to_text_string(value)
295        if role == Qt.DisplayRole:
296            return to_qvariant(display)
297        elif role == Qt.EditRole:
298            return to_qvariant(value_to_display(value))
299        elif role == Qt.TextAlignmentRole:
300            if index.column() == 3:
301                if len(display.splitlines()) < 3:
302                    return to_qvariant(int(Qt.AlignLeft|Qt.AlignVCenter))
303                else:
304                    return to_qvariant(int(Qt.AlignLeft|Qt.AlignTop))
305            else:
306                return to_qvariant(int(Qt.AlignLeft|Qt.AlignVCenter))
307        elif role == Qt.BackgroundColorRole:
308            return to_qvariant( self.get_bgcolor(index) )
309        elif role == Qt.FontRole:
310            return to_qvariant(get_font(font_size_delta=DEFAULT_SMALL_DELTA))
311        return to_qvariant()
312
313    def headerData(self, section, orientation, role=Qt.DisplayRole):
314        """Overriding method headerData"""
315        if role != Qt.DisplayRole:
316            return to_qvariant()
317        i_column = int(section)
318        if orientation == Qt.Horizontal:
319            headers = (self.header0, _("Type"), _("Size"), _("Value"))
320            return to_qvariant( headers[i_column] )
321        else:
322            return to_qvariant()
323
324    def flags(self, index):
325        """Overriding method flags"""
326        # This method was implemented in CollectionsModel only, but to enable
327        # tuple exploration (even without editing), this method was moved here
328        if not index.isValid():
329            return Qt.ItemIsEnabled
330        return Qt.ItemFlags(QAbstractTableModel.flags(self, index)|
331                            Qt.ItemIsEditable)
332    def reset(self):
333        self.beginResetModel()
334        self.endResetModel()
335
336
337class CollectionsModel(ReadOnlyCollectionsModel):
338    """Collections Table Model"""
339
340    def set_value(self, index, value):
341        """Set value"""
342        self._data[ self.keys[index.row()] ] = value
343        self.showndata[ self.keys[index.row()] ] = value
344        self.sizes[index.row()] = get_size(value)
345        self.types[index.row()] = get_human_readable_type(value)
346
347    def get_bgcolor(self, index):
348        """Background color depending on value"""
349        value = self.get_value(index)
350        if index.column() < 3:
351            color = ReadOnlyCollectionsModel.get_bgcolor(self, index)
352        else:
353            if self.remote:
354                color_name = value['color']
355            else:
356                color_name = get_color_name(value)
357            color = QColor(color_name)
358            color.setAlphaF(.2)
359        return color
360
361    def setData(self, index, value, role=Qt.EditRole):
362        """Cell content change"""
363        if not index.isValid():
364            return False
365        if index.column() < 3:
366            return False
367        value = display_to_value(value, self.get_value(index),
368                                 ignore_errors=True)
369        self.set_value(index, value)
370        self.dataChanged.emit(index, index)
371        return True
372
373
374class CollectionsDelegate(QItemDelegate):
375    """CollectionsEditor Item Delegate"""
376
377    def __init__(self, parent=None):
378        QItemDelegate.__init__(self, parent)
379        self._editors = {} # keep references on opened editors
380
381    def get_value(self, index):
382        if index.isValid():
383            return index.model().get_value(index)
384
385    def set_value(self, index, value):
386        if index.isValid():
387            index.model().set_value(index, value)
388
389    def show_warning(self, index):
390        """
391        Decide if showing a warning when the user is trying to view
392        a big variable associated to a Tablemodel index
393
394        This avoids getting the variables' value to know its
395        size and type, using instead those already computed by
396        the TableModel.
397
398        The problem is when a variable is too big, it can take a
399        lot of time just to get its value
400        """
401        try:
402            val_size = index.model().sizes[index.row()]
403            val_type = index.model().types[index.row()]
404        except:
405            return False
406        if val_type in ['list', 'tuple', 'dict'] and int(val_size) > 1e5:
407            return True
408        else:
409            return False
410
411    def createEditor(self, parent, option, index):
412        """Overriding method createEditor"""
413        if index.column() < 3:
414            return None
415        if self.show_warning(index):
416            answer = QMessageBox.warning(self.parent(), _("Warning"),
417                                      _("Opening this variable can be slow\n\n"
418                                        "Do you want to continue anyway?"),
419                                      QMessageBox.Yes | QMessageBox.No)
420            if answer == QMessageBox.No:
421                return None
422        try:
423            value = self.get_value(index)
424            if value is None:
425                return None
426        except Exception as msg:
427            QMessageBox.critical(self.parent(), _("Error"),
428                                 _("Spyder was unable to retrieve the value of "
429                                   "this variable from the console.<br><br>"
430                                   "The error mesage was:<br>"
431                                   "<i>%s</i>"
432                                   ) % to_text_string(msg))
433            return
434        key = index.model().get_key(index)
435        readonly = isinstance(value, tuple) or self.parent().readonly \
436                   or not is_known_type(value)
437        #---editor = CollectionsEditor
438        if isinstance(value, (list, tuple, dict)):
439            editor = CollectionsEditor()
440            editor.setup(value, key, icon=self.parent().windowIcon(),
441                         readonly=readonly)
442            self.create_dialog(editor, dict(model=index.model(), editor=editor,
443                                            key=key, readonly=readonly))
444            return None
445        #---editor = ArrayEditor
446        elif isinstance(value, (ndarray, MaskedArray)) \
447          and ndarray is not FakeObject:
448            editor = ArrayEditor(parent)
449            if not editor.setup_and_check(value, title=key, readonly=readonly):
450                return
451            self.create_dialog(editor, dict(model=index.model(), editor=editor,
452                                            key=key, readonly=readonly))
453            return None
454        #---showing image
455        elif isinstance(value, Image) and ndarray is not FakeObject \
456          and Image is not FakeObject:
457            arr = array(value)
458            editor = ArrayEditor(parent)
459            if not editor.setup_and_check(arr, title=key, readonly=readonly):
460                return
461            conv_func = lambda arr: Image.fromarray(arr, mode=value.mode)
462            self.create_dialog(editor, dict(model=index.model(), editor=editor,
463                                            key=key, readonly=readonly,
464                                            conv=conv_func))
465            return None
466        #--editor = DataFrameEditor
467        elif isinstance(value, (DataFrame, DatetimeIndex, Series)) \
468          and DataFrame is not FakeObject:
469            editor = DataFrameEditor()
470            if not editor.setup_and_check(value, title=key):
471                return
472            editor.dataModel.set_format(index.model().dataframe_format)
473            editor.sig_option_changed.connect(self.change_option)
474            self.create_dialog(editor, dict(model=index.model(), editor=editor,
475                                            key=key, readonly=readonly))
476            return None
477        #---editor = QDateEdit or QDateTimeEdit
478        elif isinstance(value, datetime.date):
479            if readonly:
480                return None
481            else:
482                if isinstance(value, datetime.datetime):
483                    editor = QDateTimeEdit(value, parent)
484                else:
485                    editor = QDateEdit(value, parent)
486                editor.setCalendarPopup(True)
487                editor.setFont(get_font(font_size_delta=DEFAULT_SMALL_DELTA))
488                return editor
489        #---editor = TextEditor
490        elif is_text_string(value) and len(value) > 40:
491            te = TextEditor(None)
492            if te.setup_and_check(value):
493                editor = TextEditor(value, key, readonly=readonly)
494                self.create_dialog(editor, dict(model=index.model(),
495                                                editor=editor, key=key,
496                                                readonly=readonly))
497            return None
498        #---editor = QLineEdit
499        elif is_editable_type(value):
500            if readonly:
501                return None
502            else:
503                editor = QLineEdit(parent)
504                editor.setFont(get_font(font_size_delta=DEFAULT_SMALL_DELTA))
505                editor.setAlignment(Qt.AlignLeft)
506                # This is making Spyder crash because the QLineEdit that it's
507                # been modified is removed and a new one is created after
508                # evaluation. So the object on which this method is trying to
509                # act doesn't exist anymore.
510                # editor.returnPressed.connect(self.commitAndCloseEditor)
511                return editor
512        #---editor = CollectionsEditor for an arbitrary object
513        else:
514            editor = CollectionsEditor()
515            editor.setup(value, key, icon=self.parent().windowIcon(),
516                         readonly=readonly)
517            self.create_dialog(editor, dict(model=index.model(), editor=editor,
518                                            key=key, readonly=readonly))
519            return None
520
521    def create_dialog(self, editor, data):
522        self._editors[id(editor)] = data
523        editor.accepted.connect(
524                     lambda eid=id(editor): self.editor_accepted(eid))
525        editor.rejected.connect(
526                     lambda eid=id(editor): self.editor_rejected(eid))
527        editor.show()
528
529    @Slot(str, object)
530    def change_option(self, option_name, new_value):
531        """
532        Change configuration option.
533
534        This function is called when a `sig_option_changed` signal is received.
535        At the moment, this signal can only come from a DataFrameEditor.
536        """
537        if option_name == 'dataframe_format':
538            self.parent().set_dataframe_format(new_value)
539
540    def editor_accepted(self, editor_id):
541        data = self._editors[editor_id]
542        if not data['readonly']:
543            index = data['model'].get_index_from_key(data['key'])
544            value = data['editor'].get_value()
545            conv_func = data.get('conv', lambda v: v)
546            self.set_value(index, conv_func(value))
547        self._editors.pop(editor_id)
548        self.free_memory()
549
550    def editor_rejected(self, editor_id):
551        self._editors.pop(editor_id)
552        self.free_memory()
553
554    def free_memory(self):
555        """Free memory after closing an editor."""
556        gc.collect()
557
558    def commitAndCloseEditor(self):
559        """Overriding method commitAndCloseEditor"""
560        editor = self.sender()
561        # Avoid a segfault with PyQt5. Variable value won't be changed
562        # but at least Spyder won't crash. It seems generated by a
563        # bug in sip. See
564        # http://comments.gmane.org/gmane.comp.python.pyqt-pykde/26544
565        try:
566            self.commitData.emit(editor)
567        except AttributeError:
568            pass
569        self.closeEditor.emit(editor, QAbstractItemDelegate.NoHint)
570
571    def setEditorData(self, editor, index):
572        """
573        Overriding method setEditorData
574        Model --> Editor
575        """
576        value = self.get_value(index)
577        if isinstance(editor, QLineEdit):
578            if is_binary_string(value):
579                try:
580                    value = to_text_string(value, 'utf8')
581                except:
582                    pass
583            if not is_text_string(value):
584                value = repr(value)
585            editor.setText(value)
586        elif isinstance(editor, QDateEdit):
587            editor.setDate(value)
588        elif isinstance(editor, QDateTimeEdit):
589            editor.setDateTime(QDateTime(value.date(), value.time()))
590
591    def setModelData(self, editor, model, index):
592        """
593        Overriding method setModelData
594        Editor --> Model
595        """
596        if not hasattr(model, "set_value"):
597            # Read-only mode
598            return
599
600        if isinstance(editor, QLineEdit):
601            value = editor.text()
602            try:
603                value = display_to_value(to_qvariant(value),
604                                         self.get_value(index),
605                                         ignore_errors=False)
606            except Exception as msg:
607                raise
608                QMessageBox.critical(editor, _("Edit item"),
609                                     _("<b>Unable to assign data to item.</b>"
610                                       "<br><br>Error message:<br>%s"
611                                       ) % str(msg))
612                return
613        elif isinstance(editor, QDateEdit):
614            qdate = editor.date()
615            value = datetime.date( qdate.year(), qdate.month(), qdate.day() )
616        elif isinstance(editor, QDateTimeEdit):
617            qdatetime = editor.dateTime()
618            qdate = qdatetime.date()
619            qtime = qdatetime.time()
620            value = datetime.datetime( qdate.year(), qdate.month(),
621                                       qdate.day(), qtime.hour(),
622                                       qtime.minute(), qtime.second() )
623        else:
624            # Should not happen...
625            raise RuntimeError("Unsupported editor widget")
626        self.set_value(index, value)
627
628
629class BaseTableView(QTableView):
630    """Base collection editor table view"""
631    sig_option_changed = Signal(str, object)
632    sig_files_dropped = Signal(list)
633    redirect_stdio = Signal(bool)
634
635    def __init__(self, parent):
636        QTableView.__init__(self, parent)
637        self.array_filename = None
638        self.menu = None
639        self.empty_ws_menu = None
640        self.paste_action = None
641        self.copy_action = None
642        self.edit_action = None
643        self.plot_action = None
644        self.hist_action = None
645        self.imshow_action = None
646        self.save_array_action = None
647        self.insert_action = None
648        self.remove_action = None
649        self.minmax_action = None
650        self.rename_action = None
651        self.duplicate_action = None
652        self.delegate = None
653        self.setAcceptDrops(True)
654
655    def setup_table(self):
656        """Setup table"""
657        self.horizontalHeader().setStretchLastSection(True)
658        self.adjust_columns()
659        # Sorting columns
660        self.setSortingEnabled(True)
661        self.sortByColumn(0, Qt.AscendingOrder)
662
663    def setup_menu(self, minmax):
664        """Setup context menu"""
665        if self.minmax_action is not None:
666            self.minmax_action.setChecked(minmax)
667            return
668
669        resize_action = create_action(self, _("Resize rows to contents"),
670                                      triggered=self.resizeRowsToContents)
671        self.paste_action = create_action(self, _("Paste"),
672                                          icon=ima.icon('editpaste'),
673                                          triggered=self.paste)
674        self.copy_action = create_action(self, _("Copy"),
675                                         icon=ima.icon('editcopy'),
676                                         triggered=self.copy)
677        self.edit_action = create_action(self, _("Edit"),
678                                         icon=ima.icon('edit'),
679                                         triggered=self.edit_item)
680        self.plot_action = create_action(self, _("Plot"),
681                                    icon=ima.icon('plot'),
682                                    triggered=lambda: self.plot_item('plot'))
683        self.plot_action.setVisible(False)
684        self.hist_action = create_action(self, _("Histogram"),
685                                    icon=ima.icon('hist'),
686                                    triggered=lambda: self.plot_item('hist'))
687        self.hist_action.setVisible(False)
688        self.imshow_action = create_action(self, _("Show image"),
689                                           icon=ima.icon('imshow'),
690                                           triggered=self.imshow_item)
691        self.imshow_action.setVisible(False)
692        self.save_array_action = create_action(self, _("Save array"),
693                                               icon=ima.icon('filesave'),
694                                               triggered=self.save_array)
695        self.save_array_action.setVisible(False)
696        self.insert_action = create_action(self, _("Insert"),
697                                           icon=ima.icon('insert'),
698                                           triggered=self.insert_item)
699        self.remove_action = create_action(self, _("Remove"),
700                                           icon=ima.icon('editdelete'),
701                                           triggered=self.remove_item)
702        self.minmax_action = create_action(self, _("Show arrays min/max"),
703                                           toggled=self.toggle_minmax)
704        self.minmax_action.setChecked(minmax)
705        self.toggle_minmax(minmax)
706        self.rename_action = create_action(self, _("Rename"),
707                                           icon=ima.icon('rename'),
708                                           triggered=self.rename_item)
709        self.duplicate_action = create_action(self, _("Duplicate"),
710                                              icon=ima.icon('edit_add'),
711                                              triggered=self.duplicate_item)
712        menu = QMenu(self)
713        menu_actions = [self.edit_action, self.plot_action, self.hist_action,
714                        self.imshow_action, self.save_array_action,
715                        self.insert_action, self.remove_action,
716                        self.copy_action, self.paste_action,
717                        None, self.rename_action, self.duplicate_action,
718                        None, resize_action]
719        if ndarray is not FakeObject:
720            menu_actions.append(self.minmax_action)
721        add_actions(menu, menu_actions)
722        self.empty_ws_menu = QMenu(self)
723        add_actions(self.empty_ws_menu,
724                    [self.insert_action, self.paste_action,
725                     None, resize_action])
726        return menu
727
728    #------ Remote/local API ---------------------------------------------------
729    def remove_values(self, keys):
730        """Remove values from data"""
731        raise NotImplementedError
732
733    def copy_value(self, orig_key, new_key):
734        """Copy value"""
735        raise NotImplementedError
736
737    def new_value(self, key, value):
738        """Create new value in data"""
739        raise NotImplementedError
740
741    def is_list(self, key):
742        """Return True if variable is a list or a tuple"""
743        raise NotImplementedError
744
745    def get_len(self, key):
746        """Return sequence length"""
747        raise NotImplementedError
748
749    def is_array(self, key):
750        """Return True if variable is a numpy array"""
751        raise NotImplementedError
752
753    def is_image(self, key):
754        """Return True if variable is a PIL.Image image"""
755        raise NotImplementedError
756
757    def is_dict(self, key):
758        """Return True if variable is a dictionary"""
759        raise NotImplementedError
760
761    def get_array_shape(self, key):
762        """Return array's shape"""
763        raise NotImplementedError
764
765    def get_array_ndim(self, key):
766        """Return array's ndim"""
767        raise NotImplementedError
768
769    def oedit(self, key):
770        """Edit item"""
771        raise NotImplementedError
772
773    def plot(self, key, funcname):
774        """Plot item"""
775        raise NotImplementedError
776
777    def imshow(self, key):
778        """Show item's image"""
779        raise NotImplementedError
780
781    def show_image(self, key):
782        """Show image (item is a PIL image)"""
783        raise NotImplementedError
784    #---------------------------------------------------------------------------
785
786    def refresh_menu(self):
787        """Refresh context menu"""
788        index = self.currentIndex()
789        condition = index.isValid()
790        self.edit_action.setEnabled( condition )
791        self.remove_action.setEnabled( condition )
792        self.refresh_plot_entries(index)
793
794    def refresh_plot_entries(self, index):
795        if index.isValid():
796            key = self.model.get_key(index)
797            is_list = self.is_list(key)
798            is_array = self.is_array(key) and self.get_len(key) != 0
799            condition_plot = (is_array and len(self.get_array_shape(key)) <= 2)
800            condition_hist = (is_array and self.get_array_ndim(key) == 1)
801            condition_imshow = condition_plot and self.get_array_ndim(key) == 2
802            condition_imshow = condition_imshow or self.is_image(key)
803        else:
804            is_array = condition_plot = condition_imshow = is_list \
805                     = condition_hist = False
806        self.plot_action.setVisible(condition_plot or is_list)
807        self.hist_action.setVisible(condition_hist or is_list)
808        self.imshow_action.setVisible(condition_imshow)
809        self.save_array_action.setVisible(is_array)
810
811    def adjust_columns(self):
812        """Resize two first columns to contents"""
813        for col in range(3):
814            self.resizeColumnToContents(col)
815
816    def set_data(self, data):
817        """Set table data"""
818        if data is not None:
819            self.model.set_data(data, self.dictfilter)
820            self.sortByColumn(0, Qt.AscendingOrder)
821
822    def mousePressEvent(self, event):
823        """Reimplement Qt method"""
824        if event.button() != Qt.LeftButton:
825            QTableView.mousePressEvent(self, event)
826            return
827        index_clicked = self.indexAt(event.pos())
828        if index_clicked.isValid():
829            if index_clicked == self.currentIndex() \
830               and index_clicked in self.selectedIndexes():
831                self.clearSelection()
832            else:
833                QTableView.mousePressEvent(self, event)
834        else:
835            self.clearSelection()
836            event.accept()
837
838    def mouseDoubleClickEvent(self, event):
839        """Reimplement Qt method"""
840        index_clicked = self.indexAt(event.pos())
841        if index_clicked.isValid():
842            row = index_clicked.row()
843            # TODO: Remove hard coded "Value" column number (3 here)
844            index_clicked = index_clicked.child(row, 3)
845            self.edit(index_clicked)
846        else:
847            event.accept()
848
849    def keyPressEvent(self, event):
850        """Reimplement Qt methods"""
851        if event.key() == Qt.Key_Delete:
852            self.remove_item()
853        elif event.key() == Qt.Key_F2:
854            self.rename_item()
855        elif event == QKeySequence.Copy:
856            self.copy()
857        elif event == QKeySequence.Paste:
858            self.paste()
859        else:
860            QTableView.keyPressEvent(self, event)
861
862    def contextMenuEvent(self, event):
863        """Reimplement Qt method"""
864        if self.model.showndata:
865            self.refresh_menu()
866            self.menu.popup(event.globalPos())
867            event.accept()
868        else:
869            self.empty_ws_menu.popup(event.globalPos())
870            event.accept()
871
872    def dragEnterEvent(self, event):
873        """Allow user to drag files"""
874        if mimedata2url(event.mimeData()):
875            event.accept()
876        else:
877            event.ignore()
878
879    def dragMoveEvent(self, event):
880        """Allow user to move files"""
881        if mimedata2url(event.mimeData()):
882            event.setDropAction(Qt.CopyAction)
883            event.accept()
884        else:
885            event.ignore()
886
887    def dropEvent(self, event):
888        """Allow user to drop supported files"""
889        urls = mimedata2url(event.mimeData())
890        if urls:
891            event.setDropAction(Qt.CopyAction)
892            event.accept()
893            self.sig_files_dropped.emit(urls)
894        else:
895            event.ignore()
896
897    @Slot(bool)
898    def toggle_minmax(self, state):
899        """Toggle min/max display for numpy arrays"""
900        self.sig_option_changed.emit('minmax', state)
901        self.model.minmax = state
902
903    @Slot(str)
904    def set_dataframe_format(self, new_format):
905        """
906        Set format to use in DataframeEditor.
907
908        Args:
909            new_format (string): e.g. "%.3f"
910        """
911        self.sig_option_changed.emit('dataframe_format', new_format)
912        self.model.dataframe_format = new_format
913
914    @Slot()
915    def edit_item(self):
916        """Edit item"""
917        index = self.currentIndex()
918        if not index.isValid():
919            return
920        # TODO: Remove hard coded "Value" column number (3 here)
921        self.edit(index.child(index.row(), 3))
922
923    @Slot()
924    def remove_item(self):
925        """Remove item"""
926        indexes = self.selectedIndexes()
927        if not indexes:
928            return
929        for index in indexes:
930            if not index.isValid():
931                return
932        one = _("Do you want to remove the selected item?")
933        more = _("Do you want to remove all selected items?")
934        answer = QMessageBox.question(self, _( "Remove"),
935                                      one if len(indexes) == 1 else more,
936                                      QMessageBox.Yes | QMessageBox.No)
937        if answer == QMessageBox.Yes:
938            idx_rows = unsorted_unique([idx.row() for idx in indexes])
939            keys = [ self.model.keys[idx_row] for idx_row in idx_rows ]
940            self.remove_values(keys)
941
942    def copy_item(self, erase_original=False):
943        """Copy item"""
944        indexes = self.selectedIndexes()
945        if not indexes:
946            return
947        idx_rows = unsorted_unique([idx.row() for idx in indexes])
948        if len(idx_rows) > 1 or not indexes[0].isValid():
949            return
950        orig_key = self.model.keys[idx_rows[0]]
951        if erase_original:
952            title = _('Rename')
953            field_text = _('New variable name:')
954        else:
955            title = _('Duplicate')
956            field_text = _('Variable name:')
957        data = self.model.get_data()
958        if isinstance(data, (list, set)):
959            new_key, valid = len(data), True
960        else:
961            new_key, valid = QInputDialog.getText(self, title, field_text,
962                                                  QLineEdit.Normal, orig_key)
963        if valid and to_text_string(new_key):
964            new_key = try_to_eval(to_text_string(new_key))
965            if new_key == orig_key:
966                return
967            self.copy_value(orig_key, new_key)
968            if erase_original:
969                self.remove_values([orig_key])
970
971    @Slot()
972    def duplicate_item(self):
973        """Duplicate item"""
974        self.copy_item()
975
976    @Slot()
977    def rename_item(self):
978        """Rename item"""
979        self.copy_item(True)
980
981    @Slot()
982    def insert_item(self):
983        """Insert item"""
984        index = self.currentIndex()
985        if not index.isValid():
986            row = self.model.rowCount()
987        else:
988            row = index.row()
989        data = self.model.get_data()
990        if isinstance(data, list):
991            key = row
992            data.insert(row, '')
993        elif isinstance(data, dict):
994            key, valid = QInputDialog.getText(self, _( 'Insert'), _( 'Key:'),
995                                              QLineEdit.Normal)
996            if valid and to_text_string(key):
997                key = try_to_eval(to_text_string(key))
998            else:
999                return
1000        else:
1001            return
1002        value, valid = QInputDialog.getText(self, _('Insert'), _('Value:'),
1003                                            QLineEdit.Normal)
1004        if valid and to_text_string(value):
1005            self.new_value(key, try_to_eval(to_text_string(value)))
1006
1007    def __prepare_plot(self):
1008        try:
1009            import guiqwt.pyplot   #analysis:ignore
1010            return True
1011        except:
1012            try:
1013                if 'matplotlib' not in sys.modules:
1014                    import matplotlib
1015                    matplotlib.use("Qt4Agg")
1016                return True
1017            except:
1018                QMessageBox.warning(self, _("Import error"),
1019                                    _("Please install <b>matplotlib</b>"
1020                                      " or <b>guiqwt</b>."))
1021
1022    def plot_item(self, funcname):
1023        """Plot item"""
1024        index = self.currentIndex()
1025        if self.__prepare_plot():
1026            key = self.model.get_key(index)
1027            try:
1028                self.plot(key, funcname)
1029            except (ValueError, TypeError) as error:
1030                QMessageBox.critical(self, _( "Plot"),
1031                                     _("<b>Unable to plot data.</b>"
1032                                       "<br><br>Error message:<br>%s"
1033                                       ) % str(error))
1034
1035    @Slot()
1036    def imshow_item(self):
1037        """Imshow item"""
1038        index = self.currentIndex()
1039        if self.__prepare_plot():
1040            key = self.model.get_key(index)
1041            try:
1042                if self.is_image(key):
1043                    self.show_image(key)
1044                else:
1045                    self.imshow(key)
1046            except (ValueError, TypeError) as error:
1047                QMessageBox.critical(self, _( "Plot"),
1048                                     _("<b>Unable to show image.</b>"
1049                                       "<br><br>Error message:<br>%s"
1050                                       ) % str(error))
1051
1052    @Slot()
1053    def save_array(self):
1054        """Save array"""
1055        title = _( "Save array")
1056        if self.array_filename is None:
1057            self.array_filename = getcwd_or_home()
1058        self.redirect_stdio.emit(False)
1059        filename, _selfilter = getsavefilename(self, title,
1060                                               self.array_filename,
1061                                               _("NumPy arrays")+" (*.npy)")
1062        self.redirect_stdio.emit(True)
1063        if filename:
1064            self.array_filename = filename
1065            data = self.delegate.get_value( self.currentIndex() )
1066            try:
1067                import numpy as np
1068                np.save(self.array_filename, data)
1069            except Exception as error:
1070                QMessageBox.critical(self, title,
1071                                     _("<b>Unable to save array</b>"
1072                                       "<br><br>Error message:<br>%s"
1073                                       ) % str(error))
1074
1075    @Slot()
1076    def copy(self):
1077        """Copy text to clipboard"""
1078        clipboard = QApplication.clipboard()
1079        clipl = []
1080        for idx in self.selectedIndexes():
1081            if not idx.isValid():
1082                continue
1083            obj = self.delegate.get_value(idx)
1084            # Check if we are trying to copy a numpy array, and if so make sure
1085            # to copy the whole thing in a tab separated format
1086            if isinstance(obj, (ndarray, MaskedArray)) \
1087              and ndarray is not FakeObject:
1088                if PY3:
1089                    output = io.BytesIO()
1090                else:
1091                    output = io.StringIO()
1092                try:
1093                    np_savetxt(output, obj, delimiter='\t')
1094                except:
1095                    QMessageBox.warning(self, _("Warning"),
1096                                        _("It was not possible to copy "
1097                                          "this array"))
1098                    return
1099                obj = output.getvalue().decode('utf-8')
1100                output.close()
1101            elif isinstance(obj, (DataFrame, Series)) \
1102              and DataFrame is not FakeObject:
1103                output = io.StringIO()
1104                obj.to_csv(output, sep='\t', index=True, header=True)
1105                if PY3:
1106                    obj = output.getvalue()
1107                else:
1108                    obj = output.getvalue().decode('utf-8')
1109                output.close()
1110            elif is_binary_string(obj):
1111                obj = to_text_string(obj, 'utf8')
1112            else:
1113                obj = to_text_string(obj)
1114            clipl.append(obj)
1115        clipboard.setText('\n'.join(clipl))
1116
1117    def import_from_string(self, text, title=None):
1118        """Import data from string"""
1119        data = self.model.get_data()
1120        editor = ImportWizard(self, text, title=title,
1121                              contents_title=_("Clipboard contents"),
1122                              varname=fix_reference_name("data",
1123                                                         blacklist=list(data.keys())))
1124        if editor.exec_():
1125            var_name, clip_data = editor.get_data()
1126            self.new_value(var_name, clip_data)
1127
1128    @Slot()
1129    def paste(self):
1130        """Import text/data/code from clipboard"""
1131        clipboard = QApplication.clipboard()
1132        cliptext = ''
1133        if clipboard.mimeData().hasText():
1134            cliptext = to_text_string(clipboard.text())
1135        if cliptext.strip():
1136            self.import_from_string(cliptext, title=_("Import from clipboard"))
1137        else:
1138            QMessageBox.warning(self, _( "Empty clipboard"),
1139                                _("Nothing to be imported from clipboard."))
1140
1141
1142class CollectionsEditorTableView(BaseTableView):
1143    """CollectionsEditor table view"""
1144    def __init__(self, parent, data, readonly=False, title="",
1145                 names=False, minmax=False):
1146        BaseTableView.__init__(self, parent)
1147        self.dictfilter = None
1148        self.readonly = readonly or isinstance(data, tuple)
1149        CollectionsModelClass = ReadOnlyCollectionsModel if self.readonly \
1150                                else CollectionsModel
1151        self.model = CollectionsModelClass(self, data, title, names=names,
1152                                           minmax=minmax)
1153        self.setModel(self.model)
1154        self.delegate = CollectionsDelegate(self)
1155        self.setItemDelegate(self.delegate)
1156
1157        self.setup_table()
1158        self.menu = self.setup_menu(minmax)
1159
1160    #------ Remote/local API ---------------------------------------------------
1161    def remove_values(self, keys):
1162        """Remove values from data"""
1163        data = self.model.get_data()
1164        for key in sorted(keys, reverse=True):
1165            data.pop(key)
1166            self.set_data(data)
1167
1168    def copy_value(self, orig_key, new_key):
1169        """Copy value"""
1170        data = self.model.get_data()
1171        if isinstance(data, list):
1172            data.append(data[orig_key])
1173        if isinstance(data, set):
1174            data.add(data[orig_key])
1175        else:
1176            data[new_key] = data[orig_key]
1177        self.set_data(data)
1178
1179    def new_value(self, key, value):
1180        """Create new value in data"""
1181        data = self.model.get_data()
1182        data[key] = value
1183        self.set_data(data)
1184
1185    def is_list(self, key):
1186        """Return True if variable is a list or a tuple"""
1187        data = self.model.get_data()
1188        return isinstance(data[key], (tuple, list))
1189
1190    def get_len(self, key):
1191        """Return sequence length"""
1192        data = self.model.get_data()
1193        return len(data[key])
1194
1195    def is_array(self, key):
1196        """Return True if variable is a numpy array"""
1197        data = self.model.get_data()
1198        return isinstance(data[key], (ndarray, MaskedArray))
1199
1200    def is_image(self, key):
1201        """Return True if variable is a PIL.Image image"""
1202        data = self.model.get_data()
1203        return isinstance(data[key], Image)
1204
1205    def is_dict(self, key):
1206        """Return True if variable is a dictionary"""
1207        data = self.model.get_data()
1208        return isinstance(data[key], dict)
1209
1210    def get_array_shape(self, key):
1211        """Return array's shape"""
1212        data = self.model.get_data()
1213        return data[key].shape
1214
1215    def get_array_ndim(self, key):
1216        """Return array's ndim"""
1217        data = self.model.get_data()
1218        return data[key].ndim
1219
1220    def oedit(self, key):
1221        """Edit item"""
1222        data = self.model.get_data()
1223        from spyder.widgets.variableexplorer.objecteditor import oedit
1224        oedit(data[key])
1225
1226    def plot(self, key, funcname):
1227        """Plot item"""
1228        data = self.model.get_data()
1229        import spyder.pyplot as plt
1230        plt.figure()
1231        getattr(plt, funcname)(data[key])
1232        plt.show()
1233
1234    def imshow(self, key):
1235        """Show item's image"""
1236        data = self.model.get_data()
1237        import spyder.pyplot as plt
1238        plt.figure()
1239        plt.imshow(data[key])
1240        plt.show()
1241
1242    def show_image(self, key):
1243        """Show image (item is a PIL image)"""
1244        data = self.model.get_data()
1245        data[key].show()
1246    #---------------------------------------------------------------------------
1247
1248    def refresh_menu(self):
1249        """Refresh context menu"""
1250        data = self.model.get_data()
1251        index = self.currentIndex()
1252        condition = (not isinstance(data, tuple)) and index.isValid() \
1253                    and not self.readonly
1254        self.edit_action.setEnabled( condition )
1255        self.remove_action.setEnabled( condition )
1256        self.insert_action.setEnabled( not self.readonly )
1257        self.duplicate_action.setEnabled(condition)
1258        condition_rename = not isinstance(data, (tuple, list, set))
1259        self.rename_action.setEnabled(condition_rename)
1260        self.refresh_plot_entries(index)
1261
1262    def set_filter(self, dictfilter=None):
1263        """Set table dict filter"""
1264        self.dictfilter = dictfilter
1265
1266
1267class CollectionsEditorWidget(QWidget):
1268    """Dictionary Editor Widget"""
1269    def __init__(self, parent, data, readonly=False, title="", remote=False):
1270        QWidget.__init__(self, parent)
1271        if remote:
1272            self.editor = RemoteCollectionsEditorTableView(self, data, readonly)
1273        else:
1274            self.editor = CollectionsEditorTableView(self, data, readonly,
1275                                                     title)
1276        layout = QVBoxLayout()
1277        layout.addWidget(self.editor)
1278        self.setLayout(layout)
1279
1280    def set_data(self, data):
1281        """Set DictEditor data"""
1282        self.editor.set_data(data)
1283
1284    def get_title(self):
1285        """Get model title"""
1286        return self.editor.model.title
1287
1288
1289class CollectionsEditor(QDialog):
1290    """Collections Editor Dialog"""
1291    def __init__(self, parent=None):
1292        QDialog.__init__(self, parent)
1293
1294        # Destroying the C++ object right after closing the dialog box,
1295        # otherwise it may be garbage-collected in another QThread
1296        # (e.g. the editor's analysis thread in Spyder), thus leading to
1297        # a segmentation fault on UNIX or an application crash on Windows
1298        self.setAttribute(Qt.WA_DeleteOnClose)
1299
1300        self.data_copy = None
1301        self.widget = None
1302
1303    def setup(self, data, title='', readonly=False, width=650, remote=False,
1304              icon=None, parent=None):
1305        """Setup editor."""
1306        if isinstance(data, dict):
1307            # dictionnary
1308            self.data_copy = data.copy()
1309            datalen = len(data)
1310        elif isinstance(data, (tuple, list)):
1311            # list, tuple
1312            self.data_copy = data[:]
1313            datalen = len(data)
1314        else:
1315            # unknown object
1316            import copy
1317            try:
1318                self.data_copy = copy.deepcopy(data)
1319            except NotImplementedError:
1320                self.data_copy = copy.copy(data)
1321            except (TypeError, AttributeError):
1322                readonly = True
1323                self.data_copy = data
1324            datalen = len(get_object_attrs(data))
1325        self.widget = CollectionsEditorWidget(self, self.data_copy,
1326                                              title=title, readonly=readonly,
1327                                              remote=remote)
1328
1329        layout = QVBoxLayout()
1330        layout.addWidget(self.widget)
1331        self.setLayout(layout)
1332
1333        # Buttons configuration
1334        buttons = QDialogButtonBox.Ok
1335        if not readonly:
1336            buttons = buttons | QDialogButtonBox.Cancel
1337        self.bbox = QDialogButtonBox(buttons)
1338        self.bbox.accepted.connect(self.accept)
1339        if not readonly:
1340            self.bbox.rejected.connect(self.reject)
1341        layout.addWidget(self.bbox)
1342
1343        constant = 121
1344        row_height = 30
1345        error_margin = 10
1346        height = constant + row_height * min([10, datalen]) + error_margin
1347        self.resize(width, height)
1348
1349        self.setWindowTitle(self.widget.get_title())
1350        if icon is None:
1351            self.setWindowIcon(ima.icon('dictedit'))
1352        # Make the dialog act as a window
1353        self.setWindowFlags(Qt.Window)
1354
1355    def get_value(self):
1356        """Return modified copy of dictionary or list"""
1357        # It is import to avoid accessing Qt C++ object as it has probably
1358        # already been destroyed, due to the Qt.WA_DeleteOnClose attribute
1359        return self.data_copy
1360
1361
1362class DictEditor(CollectionsEditor):
1363    def __init__(self, parent=None):
1364        warnings.warn("`DictEditor` has been renamed to `CollectionsEditor` in "
1365                      "Spyder 3. Please use `CollectionsEditor` instead",
1366                      RuntimeWarning)
1367        CollectionsEditor.__init__(self, parent)
1368
1369
1370#----Remote versions of CollectionsDelegate and CollectionsEditorTableView
1371class RemoteCollectionsDelegate(CollectionsDelegate):
1372    """CollectionsEditor Item Delegate"""
1373    def __init__(self, parent=None, get_value_func=None, set_value_func=None):
1374        CollectionsDelegate.__init__(self, parent)
1375        self.get_value_func = get_value_func
1376        self.set_value_func = set_value_func
1377
1378    def get_value(self, index):
1379        if index.isValid():
1380            name = index.model().keys[index.row()]
1381            return self.get_value_func(name)
1382
1383    def set_value(self, index, value):
1384        if index.isValid():
1385            name = index.model().keys[index.row()]
1386            self.set_value_func(name, value)
1387
1388
1389class RemoteCollectionsEditorTableView(BaseTableView):
1390    """DictEditor table view"""
1391    def __init__(self, parent, data, minmax=False,
1392                 dataframe_format=None,
1393                 get_value_func=None, set_value_func=None,
1394                 new_value_func=None, remove_values_func=None,
1395                 copy_value_func=None, is_list_func=None, get_len_func=None,
1396                 is_array_func=None, is_image_func=None, is_dict_func=None,
1397                 get_array_shape_func=None, get_array_ndim_func=None,
1398                 plot_func=None, imshow_func=None,
1399                 is_data_frame_func=None, is_series_func=None,
1400                 show_image_func=None):
1401        BaseTableView.__init__(self, parent)
1402
1403        self.remove_values = remove_values_func
1404        self.copy_value = copy_value_func
1405        self.new_value = new_value_func
1406
1407        self.is_data_frame = is_data_frame_func
1408        self.is_series = is_series_func
1409        self.is_list = is_list_func
1410        self.get_len = get_len_func
1411        self.is_array = is_array_func
1412        self.is_image = is_image_func
1413        self.is_dict = is_dict_func
1414        self.get_array_shape = get_array_shape_func
1415        self.get_array_ndim = get_array_ndim_func
1416        self.plot = plot_func
1417        self.imshow = imshow_func
1418        self.show_image = show_image_func
1419
1420        self.dictfilter = None
1421        self.model = None
1422        self.delegate = None
1423        self.readonly = False
1424        self.model = CollectionsModel(self, data, names=True,
1425                                      minmax=minmax,
1426                                      dataframe_format=dataframe_format,
1427                                      remote=True)
1428        self.setModel(self.model)
1429        self.delegate = RemoteCollectionsDelegate(self, get_value_func,
1430                                                  set_value_func)
1431        self.setItemDelegate(self.delegate)
1432
1433        self.setup_table()
1434        self.menu = self.setup_menu(minmax)
1435
1436    def setup_menu(self, minmax):
1437        """Setup context menu"""
1438        menu = BaseTableView.setup_menu(self, minmax)
1439        return menu
1440
1441
1442# =============================================================================
1443# Tests
1444# =============================================================================
1445def get_test_data():
1446    """Create test data."""
1447    import numpy as np
1448    from spyder.pil_patch import Image
1449    image = Image.fromarray(np.random.random_integers(255, size=(100, 100)),
1450                            mode='P')
1451    testdict = {'d': 1, 'a': np.random.rand(10, 10), 'b': [1, 2]}
1452    testdate = datetime.date(1945, 5, 8)
1453
1454    class Foobar(object):
1455
1456        def __init__(self):
1457            self.text = "toto"
1458            self.testdict = testdict
1459            self.testdate = testdate
1460
1461    foobar = Foobar()
1462    return {'object': foobar,
1463            'str': 'kjkj kj k j j kj k jkj',
1464            'unicode': to_text_string('éù', 'utf-8'),
1465            'list': [1, 3, [sorted, 5, 6], 'kjkj', None],
1466            'tuple': ([1, testdate, testdict], 'kjkj', None),
1467            'dict': testdict,
1468            'float': 1.2233,
1469            'int': 223,
1470            'bool': True,
1471            'array': np.random.rand(10, 10),
1472            'masked_array': np.ma.array([[1, 0], [1, 0]],
1473                                        mask=[[True, False], [False, False]]),
1474            '1D-array': np.linspace(-10, 10),
1475            'empty_array': np.array([]),
1476            'image': image,
1477            'date': testdate,
1478            'datetime': datetime.datetime(1945, 5, 8),
1479            'complex': 2+1j,
1480            'complex64': np.complex64(2+1j),
1481            'int8_scalar': np.int8(8),
1482            'int16_scalar': np.int16(16),
1483            'int32_scalar': np.int32(32),
1484            'bool_scalar': np.bool(8),
1485            'unsupported1': np.arccos,
1486            'unsupported2': np.cast,
1487            # Test for Issue #3518
1488            'big_struct_array': np.zeros(1000, dtype=[('ID', 'f8'),
1489                                                      ('param1', 'f8', 5000)]),
1490            }
1491
1492
1493def editor_test():
1494    """Collections editor test"""
1495    from spyder.utils.qthelpers import qapplication
1496
1497    app = qapplication()             #analysis:ignore
1498    dialog = CollectionsEditor()
1499    dialog.setup(get_test_data())
1500    dialog.show()
1501    app.exec_()
1502
1503
1504def remote_editor_test():
1505    """Remote collections editor test"""
1506    from spyder.utils.qthelpers import qapplication
1507    app = qapplication()
1508
1509    from spyder.plugins.variableexplorer import VariableExplorer
1510    from spyder.widgets.variableexplorer.utils import make_remote_view
1511
1512    remote = make_remote_view(get_test_data(),
1513                              VariableExplorer(None).get_settings())
1514    dialog = CollectionsEditor()
1515    dialog.setup(remote, remote=True)
1516    dialog.show()
1517    app.exec_()
1518
1519
1520if __name__ == "__main__":
1521    editor_test()
1522    remote_editor_test()
1523