# -*- coding: utf-8 -*- # # Copyright © 2009-2010 Pierre Raybaut # Licensed under the terms of the MIT License # (see spyderlib/__init__.py for details) """ Dictionary Editor Widget and Dialog based on Qt """ #TODO: Multiple selection: open as many editors (array/dict/...) as necessary, # at the same time # pylint: disable=C0103 # pylint: disable=R0903 # pylint: disable=R0911 # pylint: disable=R0201 from __future__ import print_function from spyderlib.qt.QtGui import (QMessageBox, QTableView, QItemDelegate, QLineEdit, QVBoxLayout, QWidget, QColor, QDialog, QDateEdit, QDialogButtonBox, QMenu, QInputDialog, QDateTimeEdit, QApplication, QKeySequence) from spyderlib.qt.QtCore import (Qt, QModelIndex, QAbstractTableModel, SIGNAL, SLOT, QDateTime, Signal) from spyderlib.qt.compat import to_qvariant, from_qvariant, getsavefilename from spyderlib.utils.qthelpers import mimedata2url import sys import datetime # Local import from spyderlib.baseconfig import _ from spyderlib.guiconfig import get_font from spyderlib.utils.misc import fix_reference_name from spyderlib.utils.qthelpers import (get_icon, add_actions, create_action, qapplication) from spyderlib.widgets.dicteditorutils import (sort_against, get_size, get_human_readable_type, value_to_display, get_color_name, is_known_type, FakeObject, Image, ndarray, array, MaskedArray, unsorted_unique, try_to_eval, datestr_to_datetime, get_numpy_dtype, is_editable_type, DataFrame, Series) if ndarray is not FakeObject: from spyderlib.widgets.arrayeditor import ArrayEditor if DataFrame is not FakeObject: from spyderlib.widgets.dataframeeditor import DataFrameEditor from spyderlib.widgets.texteditor import TextEditor from spyderlib.widgets.importwizard import ImportWizard from spyderlib.py3compat import (to_text_string, to_binary_string, is_text_string, is_binary_string, getcwd, u) LARGE_NROWS = 100 def display_to_value(value, default_value, ignore_errors=True): """Convert back to value""" value = from_qvariant(value, to_text_string) try: np_dtype = get_numpy_dtype(default_value) if isinstance(default_value, bool): # We must test for boolean before NumPy data types # because `bool` class derives from `int` class try: value = bool(float(value)) except ValueError: value = value.lower() == "true" elif np_dtype is not None: if 'complex' in str(type(default_value)): value = np_dtype(complex(value)) else: value = np_dtype(value) elif is_binary_string(default_value): value = to_binary_string(value, 'utf8') elif is_text_string(default_value): value = to_text_string(value) elif isinstance(default_value, complex): value = complex(value) elif isinstance(default_value, float): value = float(value) elif isinstance(default_value, int): try: value = int(value) except ValueError: value = float(value) elif isinstance(default_value, datetime.datetime): value = datestr_to_datetime(value) elif isinstance(default_value, datetime.date): value = datestr_to_datetime(value).date() elif ignore_errors: value = try_to_eval(value) else: value = eval(value) except (ValueError, SyntaxError): if ignore_errors: value = try_to_eval(value) else: return default_value return value class ProxyObject(object): """Dictionary proxy to an unknown object""" def __init__(self, obj): self.__obj__ = obj def __len__(self): return len(dir(self.__obj__)) def __getitem__(self, key): return getattr(self.__obj__, key) def __setitem__(self, key, value): setattr(self.__obj__, key, value) class ReadOnlyDictModel(QAbstractTableModel): """DictEditor Read-Only Table Model""" ROWS_TO_LOAD = 50 def __init__(self, parent, data, title="", names=False, truncate=True, minmax=False, remote=False): QAbstractTableModel.__init__(self, parent) if data is None: data = {} self.names = names self.truncate = truncate self.minmax = minmax self.remote = remote self.header0 = None self._data = None self.total_rows = None self.showndata = None self.keys = None self.title = to_text_string(title) # in case title is not a string if self.title: self.title = self.title + ' - ' self.sizes = [] self.types = [] self.set_data(data) def get_data(self): """Return model data""" return self._data def set_data(self, data, dictfilter=None): """Set model data""" self._data = data if dictfilter is not None and not self.remote and \ isinstance(data, (tuple, list, dict)): data = dictfilter(data) self.showndata = data self.header0 = _("Index") if self.names: self.header0 = _("Name") if isinstance(data, tuple): self.keys = list(range(len(data))) self.title += _("Tuple") elif isinstance(data, list): self.keys = list(range(len(data))) self.title += _("List") elif isinstance(data, dict): self.keys = list(data.keys()) self.title += _("Dictionary") if not self.names: self.header0 = _("Key") else: self.keys = dir(data) self._data = data = self.showndata = ProxyObject(data) self.title += _("Object") if not self.names: self.header0 = _("Attribute") self.title += ' ('+str(len(self.keys))+' '+ _("elements")+')' self.total_rows = len(self.keys) if self.total_rows > LARGE_NROWS: self.rows_loaded = self.ROWS_TO_LOAD else: self.rows_loaded = self.total_rows self.set_size_and_type() self.reset() def set_size_and_type(self, start=None, stop=None): data = self._data if start is None and stop is None: start = 0 stop = self.rows_loaded fetch_more = False else: fetch_more = True if self.remote: sizes = [ data[self.keys[index]]['size'] for index in range(start, stop) ] types = [ data[self.keys[index]]['type'] for index in range(start, stop) ] else: sizes = [ get_size(data[self.keys[index]]) for index in range(start, stop) ] types = [ get_human_readable_type(data[self.keys[index]]) for index in range(start, stop) ] if fetch_more: self.sizes = self.sizes + sizes self.types = self.types + types else: self.sizes = sizes self.types = types def sort(self, column, order=Qt.AscendingOrder): """Overriding sort method""" reverse = (order==Qt.DescendingOrder) if column == 0: self.sizes = sort_against(self.sizes, self.keys, reverse) self.types = sort_against(self.types, self.keys, reverse) try: self.keys.sort(reverse=reverse) except: pass elif column == 1: self.keys = sort_against(self.keys, self.types, reverse) self.sizes = sort_against(self.sizes, self.types, reverse) try: self.types.sort(reverse=reverse) except: pass elif column == 2: self.keys = sort_against(self.keys, self.sizes, reverse) self.types = sort_against(self.types, self.sizes, reverse) try: self.sizes.sort(reverse=reverse) except: pass elif column == 3: self.keys = sort_against(self.keys, self.sizes, reverse) self.types = sort_against(self.types, self.sizes, reverse) try: self.sizes.sort(reverse=reverse) except: pass elif column == 4: values = [self._data[key] for key in self.keys] self.keys = sort_against(self.keys, values, reverse) self.sizes = sort_against(self.sizes, values, reverse) self.types = sort_against(self.types, values, reverse) self.reset() def columnCount(self, qindex=QModelIndex()): """Array column number""" return 4 def rowCount(self, index=QModelIndex()): """Array row number""" if self.total_rows <= self.rows_loaded: return self.total_rows else: return self.rows_loaded def canFetchMore(self, index=QModelIndex()): if self.total_rows > self.rows_loaded: return True else: return False def fetchMore(self, index=QModelIndex()): reminder = self.total_rows - self.rows_loaded items_to_fetch = min(reminder, self.ROWS_TO_LOAD) self.set_size_and_type(self.rows_loaded, self.rows_loaded + items_to_fetch) self.beginInsertRows(QModelIndex(), self.rows_loaded, self.rows_loaded + items_to_fetch - 1) self.rows_loaded += items_to_fetch self.endInsertRows() def get_index_from_key(self, key): try: return self.createIndex(self.keys.index(key), 0) except ValueError: return QModelIndex() def get_key(self, index): """Return current key""" return self.keys[index.row()] def get_value(self, index): """Return current value""" if index.column() == 0: return self.keys[ index.row() ] elif index.column() == 1: return self.types[ index.row() ] elif index.column() == 2: return self.sizes[ index.row() ] else: return self._data[ self.keys[index.row()] ] def get_bgcolor(self, index): """Background color depending on value""" if index.column() == 0: color = QColor(Qt.lightGray) color.setAlphaF(.05) elif index.column() < 3: color = QColor(Qt.lightGray) color.setAlphaF(.2) else: color = QColor(Qt.lightGray) color.setAlphaF(.3) return color def data(self, index, role=Qt.DisplayRole): """Cell content""" if not index.isValid(): return to_qvariant() value = self.get_value(index) if index.column() == 3 and self.remote: value = value['view'] display = value_to_display(value, truncate=index.column() == 3 and self.truncate, minmax=self.minmax) if role == Qt.DisplayRole: return to_qvariant(display) elif role == Qt.EditRole: return to_qvariant(value_to_display(value)) elif role == Qt.TextAlignmentRole: if index.column() == 3: if len(display.splitlines()) < 3: return to_qvariant(int(Qt.AlignLeft|Qt.AlignVCenter)) else: return to_qvariant(int(Qt.AlignLeft|Qt.AlignTop)) else: return to_qvariant(int(Qt.AlignLeft|Qt.AlignVCenter)) elif role == Qt.BackgroundColorRole: return to_qvariant( self.get_bgcolor(index) ) elif role == Qt.FontRole: if index.column() < 3: return to_qvariant(get_font('dicteditor_header')) else: return to_qvariant(get_font('dicteditor')) return to_qvariant() def headerData(self, section, orientation, role=Qt.DisplayRole): """Overriding method headerData""" if role != Qt.DisplayRole: if role == Qt.FontRole: return to_qvariant(get_font('dicteditor_header')) else: return to_qvariant() i_column = int(section) if orientation == Qt.Horizontal: headers = (self.header0, _("Type"), _("Size"), _("Value")) return to_qvariant( headers[i_column] ) else: return to_qvariant() def flags(self, index): """Overriding method flags""" # This method was implemented in DictModel only, but to enable tuple # exploration (even without editing), this method was moved here if not index.isValid(): return Qt.ItemIsEnabled return Qt.ItemFlags(QAbstractTableModel.flags(self, index)| Qt.ItemIsEditable) class DictModel(ReadOnlyDictModel): """DictEditor Table Model""" def set_value(self, index, value): """Set value""" self._data[ self.keys[index.row()] ] = value self.showndata[ self.keys[index.row()] ] = value self.sizes[index.row()] = get_size(value) self.types[index.row()] = get_human_readable_type(value) def get_bgcolor(self, index): """Background color depending on value""" value = self.get_value(index) if index.column() < 3: color = ReadOnlyDictModel.get_bgcolor(self, index) else: if self.remote: color_name = value['color'] else: color_name = get_color_name(value) color = QColor(color_name) color.setAlphaF(.2) return color def setData(self, index, value, role=Qt.EditRole): """Cell content change""" if not index.isValid(): return False if index.column() < 3: return False value = display_to_value(value, self.get_value(index), ignore_errors=True) self.set_value(index, value) self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), index, index) return True class DictDelegate(QItemDelegate): """DictEditor Item Delegate""" def __init__(self, parent=None): QItemDelegate.__init__(self, parent) self._editors = {} # keep references on opened editors def get_value(self, index): if index.isValid(): return index.model().get_value(index) def set_value(self, index, value): if index.isValid(): index.model().set_value(index, value) def show_warning(self, index): """ Decide if showing a warning when the user is trying to view a big variable associated to a Tablemodel index This avoids getting the variables' value to know its size and type, using instead those already computed by the TableModel. The problem is when a variable is too big, it can take a lot of time just to get its value """ try: val_size = index.model().sizes[index.row()] val_type = index.model().types[index.row()] except: return False if val_type in ['list', 'tuple', 'dict'] and int(val_size) > 1e5: return True else: return False def createEditor(self, parent, option, index): """Overriding method createEditor""" if index.column() < 3: return None if self.show_warning(index): answer = QMessageBox.warning(self.parent(), _("Warning"), _("Opening this variable can be slow\n\n" "Do you want to continue anyway?"), QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.No: return None try: value = self.get_value(index) except Exception as msg: QMessageBox.critical(self.parent(), _("Edit item"), _("Unable to retrieve data." "

Error message:
%s" ) % to_text_string(msg)) return key = index.model().get_key(index) readonly = isinstance(value, tuple) or self.parent().readonly \ or not is_known_type(value) #---editor = DictEditor if isinstance(value, (list, tuple, dict)): editor = DictEditor() editor.setup(value, key, icon=self.parent().windowIcon(), readonly=readonly) self.create_dialog(editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly)) return None #---editor = ArrayEditor elif isinstance(value, (ndarray, MaskedArray)) \ and ndarray is not FakeObject: if value.size == 0: return None editor = ArrayEditor(parent) if not editor.setup_and_check(value, title=key, readonly=readonly): return self.create_dialog(editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly)) return None #---showing image elif isinstance(value, Image) and ndarray is not FakeObject \ and Image is not FakeObject: arr = array(value) if arr.size == 0: return None editor = ArrayEditor(parent) if not editor.setup_and_check(arr, title=key, readonly=readonly): return conv_func = lambda arr: Image.fromarray(arr, mode=value.mode) self.create_dialog(editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly, conv=conv_func)) return None #--editor = DataFrameEditor elif isinstance(value, (DataFrame, Series)) \ and DataFrame is not FakeObject: editor = DataFrameEditor() if not editor.setup_and_check(value, title=key): return self.create_dialog(editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly)) return None #---editor = QDateTimeEdit elif isinstance(value, datetime.datetime): editor = QDateTimeEdit(value, parent) editor.setCalendarPopup(True) editor.setFont(get_font('dicteditor')) self.connect(editor, SIGNAL("returnPressed()"), self.commitAndCloseEditor) return editor #---editor = QDateEdit elif isinstance(value, datetime.date): editor = QDateEdit(value, parent) editor.setCalendarPopup(True) editor.setFont(get_font('dicteditor')) self.connect(editor, SIGNAL("returnPressed()"), self.commitAndCloseEditor) return editor #---editor = QTextEdit elif is_text_string(value) and len(value)>40: editor = TextEditor(value, key) self.create_dialog(editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly)) return None #---editor = QLineEdit elif is_editable_type(value): editor = QLineEdit(parent) editor.setFont(get_font('dicteditor')) editor.setAlignment(Qt.AlignLeft) self.connect(editor, SIGNAL("returnPressed()"), self.commitAndCloseEditor) return editor #---editor = DictEditor for an arbitrary object else: editor = DictEditor() editor.setup(value, key, icon=self.parent().windowIcon(), readonly=readonly) self.create_dialog(editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly)) return None def create_dialog(self, editor, data): self._editors[id(editor)] = data self.connect(editor, SIGNAL('accepted()'), lambda eid=id(editor): self.editor_accepted(eid)) self.connect(editor, SIGNAL('rejected()'), lambda eid=id(editor): self.editor_rejected(eid)) editor.show() def editor_accepted(self, editor_id): data = self._editors[editor_id] if not data['readonly']: index = data['model'].get_index_from_key(data['key']) value = data['editor'].get_value() conv_func = data.get('conv', lambda v: v) self.set_value(index, conv_func(value)) self._editors.pop(editor_id) def editor_rejected(self, editor_id): self._editors.pop(editor_id) def commitAndCloseEditor(self): """Overriding method commitAndCloseEditor""" editor = self.sender() self.emit(SIGNAL("commitData(QWidget*)"), editor) self.emit(SIGNAL("closeEditor(QWidget*)"), editor) def setEditorData(self, editor, index): """Overriding method setEditorData Model --> Editor""" value = self.get_value(index) if isinstance(editor, QLineEdit): if is_binary_string(value): try: value = to_text_string(value, 'utf8') except: pass if not is_text_string(value): value = repr(value) editor.setText(value) elif isinstance(editor, QDateEdit): editor.setDate(value) elif isinstance(editor, QDateTimeEdit): editor.setDateTime(QDateTime(value.date(), value.time())) def setModelData(self, editor, model, index): """Overriding method setModelData Editor --> Model""" if not hasattr(model, "set_value"): # Read-only mode return if isinstance(editor, QLineEdit): value = editor.text() try: value = display_to_value(to_qvariant(value), self.get_value(index), ignore_errors=False) except Exception as msg: raise QMessageBox.critical(editor, _("Edit item"), _("Unable to assign data to item." "

Error message:
%s" ) % str(msg)) return elif isinstance(editor, QDateEdit): qdate = editor.date() value = datetime.date( qdate.year(), qdate.month(), qdate.day() ) elif isinstance(editor, QDateTimeEdit): qdatetime = editor.dateTime() qdate = qdatetime.date() qtime = qdatetime.time() value = datetime.datetime( qdate.year(), qdate.month(), qdate.day(), qtime.hour(), qtime.minute(), qtime.second() ) else: # Should not happen... raise RuntimeError("Unsupported editor widget") self.set_value(index, value) class BaseTableView(QTableView): """Base dictionary editor table view""" sig_option_changed = Signal(str, object) sig_files_dropped = Signal(list) def __init__(self, parent): QTableView.__init__(self, parent) self.array_filename = None self.menu = None self.empty_ws_menu = None self.paste_action = None self.copy_action = None self.edit_action = None self.plot_action = None self.hist_action = None self.imshow_action = None self.save_array_action = None self.insert_action = None self.remove_action = None self.truncate_action = None self.minmax_action = None self.rename_action = None self.duplicate_action = None self.delegate = None self.setAcceptDrops(True) def setup_table(self): """Setup table""" self.horizontalHeader().setStretchLastSection(True) self.adjust_columns() # Sorting columns self.setSortingEnabled(True) self.sortByColumn(0, Qt.AscendingOrder) def setup_menu(self, truncate, minmax): """Setup context menu""" if self.truncate_action is not None: self.truncate_action.setChecked(truncate) self.minmax_action.setChecked(minmax) return resize_action = create_action(self, _("Resize rows to contents"), triggered=self.resizeRowsToContents) self.paste_action = create_action(self, _("Paste"), icon=get_icon('editpaste.png'), triggered=self.paste) self.copy_action = create_action(self, _("Copy"), icon=get_icon('editcopy.png'), triggered=self.copy) self.edit_action = create_action(self, _("Edit"), icon=get_icon('edit.png'), triggered=self.edit_item) self.plot_action = create_action(self, _("Plot"), icon=get_icon('plot.png'), triggered=lambda: self.plot_item('plot')) self.plot_action.setVisible(False) self.hist_action = create_action(self, _("Histogram"), icon=get_icon('hist.png'), triggered=lambda: self.plot_item('hist')) self.hist_action.setVisible(False) self.imshow_action = create_action(self, _("Show image"), icon=get_icon('imshow.png'), triggered=self.imshow_item) self.imshow_action.setVisible(False) self.save_array_action = create_action(self, _("Save array"), icon=get_icon('filesave.png'), triggered=self.save_array) self.save_array_action.setVisible(False) self.insert_action = create_action(self, _("Insert"), icon=get_icon('insert.png'), triggered=self.insert_item) self.remove_action = create_action(self, _("Remove"), icon=get_icon('editdelete.png'), triggered=self.remove_item) self.truncate_action = create_action(self, _("Truncate values"), toggled=self.toggle_truncate) self.truncate_action.setChecked(truncate) self.toggle_truncate(truncate) self.minmax_action = create_action(self, _("Show arrays min/max"), toggled=self.toggle_minmax) self.minmax_action.setChecked(minmax) self.toggle_minmax(minmax) self.rename_action = create_action(self, _( "Rename"), icon=get_icon('rename.png'), triggered=self.rename_item) self.duplicate_action = create_action(self, _( "Duplicate"), icon=get_icon('edit_add.png'), triggered=self.duplicate_item) menu = QMenu(self) menu_actions = [self.edit_action, self.plot_action, self.hist_action, self.imshow_action, self.save_array_action, self.insert_action, self.remove_action, self.copy_action, self.paste_action, None, self.rename_action, self.duplicate_action, None, resize_action, None, self.truncate_action] if ndarray is not FakeObject: menu_actions.append(self.minmax_action) add_actions(menu, menu_actions) self.empty_ws_menu = QMenu(self) add_actions(self.empty_ws_menu, [self.insert_action, self.paste_action, None, resize_action]) return menu #------ Remote/local API --------------------------------------------------- def remove_values(self, keys): """Remove values from data""" raise NotImplementedError def copy_value(self, orig_key, new_key): """Copy value""" raise NotImplementedError def new_value(self, key, value): """Create new value in data""" raise NotImplementedError def is_list(self, key): """Return True if variable is a list or a tuple""" raise NotImplementedError def get_len(self, key): """Return sequence length""" raise NotImplementedError def is_array(self, key): """Return True if variable is a numpy array""" raise NotImplementedError def is_image(self, key): """Return True if variable is a PIL.Image image""" raise NotImplementedError def is_dict(self, key): """Return True if variable is a dictionary""" raise NotImplementedError def get_array_shape(self, key): """Return array's shape""" raise NotImplementedError def get_array_ndim(self, key): """Return array's ndim""" raise NotImplementedError def oedit(self, key): """Edit item""" raise NotImplementedError def plot(self, key, funcname): """Plot item""" raise NotImplementedError def imshow(self, key): """Show item's image""" raise NotImplementedError def show_image(self, key): """Show image (item is a PIL image)""" raise NotImplementedError #--------------------------------------------------------------------------- def refresh_menu(self): """Refresh context menu""" index = self.currentIndex() condition = index.isValid() self.edit_action.setEnabled( condition ) self.remove_action.setEnabled( condition ) self.refresh_plot_entries(index) def refresh_plot_entries(self, index): if index.isValid(): key = self.model.get_key(index) is_list = self.is_list(key) is_array = self.is_array(key) and self.get_len(key) != 0 condition_plot = (is_array and len(self.get_array_shape(key)) <= 2) condition_hist = (is_array and self.get_array_ndim(key) == 1) condition_imshow = condition_plot and self.get_array_ndim(key) == 2 condition_imshow = condition_imshow or self.is_image(key) else: is_array = condition_plot = condition_imshow = is_list \ = condition_hist = False self.plot_action.setVisible(condition_plot or is_list) self.hist_action.setVisible(condition_hist or is_list) self.imshow_action.setVisible(condition_imshow) self.save_array_action.setVisible(is_array) def adjust_columns(self): """Resize two first columns to contents""" for col in range(3): self.resizeColumnToContents(col) def set_data(self, data): """Set table data""" if data is not None: self.model.set_data(data, self.dictfilter) self.sortByColumn(0, Qt.AscendingOrder) def mousePressEvent(self, event): """Reimplement Qt method""" if event.button() != Qt.LeftButton: QTableView.mousePressEvent(self, event) return index_clicked = self.indexAt(event.pos()) if index_clicked.isValid(): if index_clicked == self.currentIndex() \ and index_clicked in self.selectedIndexes(): self.clearSelection() else: QTableView.mousePressEvent(self, event) else: self.clearSelection() event.accept() def mouseDoubleClickEvent(self, event): """Reimplement Qt method""" index_clicked = self.indexAt(event.pos()) if index_clicked.isValid(): row = index_clicked.row() # TODO: Remove hard coded "Value" column number (3 here) index_clicked = index_clicked.child(row, 3) self.edit(index_clicked) else: event.accept() def keyPressEvent(self, event): """Reimplement Qt methods""" if event.key() == Qt.Key_Delete: self.remove_item() elif event.key() == Qt.Key_F2: self.rename_item() elif event == QKeySequence.Copy: self.copy() elif event == QKeySequence.Paste: self.paste() else: QTableView.keyPressEvent(self, event) def contextMenuEvent(self, event): """Reimplement Qt method""" if self.model.showndata: self.refresh_menu() self.menu.popup(event.globalPos()) event.accept() else: self.empty_ws_menu.popup(event.globalPos()) event.accept() def dragEnterEvent(self, event): """Allow user to drag files""" if mimedata2url(event.mimeData()): event.accept() else: event.ignore() def dragMoveEvent(self, event): """Allow user to move files""" if mimedata2url(event.mimeData()): event.setDropAction(Qt.CopyAction) event.accept() else: event.ignore() def dropEvent(self, event): """Allow user to drop supported files""" urls = mimedata2url(event.mimeData()) if urls: event.setDropAction(Qt.CopyAction) event.accept() self.sig_files_dropped.emit(urls) else: event.ignore() def toggle_truncate(self, state): """Toggle display truncating option""" self.sig_option_changed.emit('truncate', state) self.model.truncate = state def toggle_minmax(self, state): """Toggle min/max display for numpy arrays""" self.sig_option_changed.emit('minmax', state) self.model.minmax = state def edit_item(self): """Edit item""" index = self.currentIndex() if not index.isValid(): return # TODO: Remove hard coded "Value" column number (3 here) self.edit(index.child(index.row(), 3)) def remove_item(self): """Remove item""" indexes = self.selectedIndexes() if not indexes: return for index in indexes: if not index.isValid(): return one = _("Do you want to remove selected item?") more = _("Do you want to remove all selected items?") answer = QMessageBox.question(self, _( "Remove"), one if len(indexes) == 1 else more, QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.Yes: idx_rows = unsorted_unique([idx.row() for idx in indexes]) keys = [ self.model.keys[idx_row] for idx_row in idx_rows ] self.remove_values(keys) def copy_item(self, erase_original=False): """Copy item""" indexes = self.selectedIndexes() if not indexes: return idx_rows = unsorted_unique([idx.row() for idx in indexes]) if len(idx_rows) > 1 or not indexes[0].isValid(): return orig_key = self.model.keys[idx_rows[0]] new_key, valid = QInputDialog.getText(self, _( 'Rename'), _( 'Key:'), QLineEdit.Normal, orig_key) if valid and to_text_string(new_key): new_key = try_to_eval(to_text_string(new_key)) if new_key == orig_key: return self.copy_value(orig_key, new_key) if erase_original: self.remove_values([orig_key]) def duplicate_item(self): """Duplicate item""" self.copy_item() def rename_item(self): """Rename item""" self.copy_item(True) def insert_item(self): """Insert item""" index = self.currentIndex() if not index.isValid(): row = self.model.rowCount() else: row = index.row() data = self.model.get_data() if isinstance(data, list): key = row data.insert(row, '') elif isinstance(data, dict): key, valid = QInputDialog.getText(self, _( 'Insert'), _( 'Key:'), QLineEdit.Normal) if valid and to_text_string(key): key = try_to_eval(to_text_string(key)) else: return else: return value, valid = QInputDialog.getText(self, _('Insert'), _('Value:'), QLineEdit.Normal) if valid and to_text_string(value): self.new_value(key, try_to_eval(to_text_string(value))) def __prepare_plot(self): try: import guiqwt.pyplot #analysis:ignore return True except ImportError: try: if 'matplotlib' not in sys.modules: import matplotlib matplotlib.use("Qt4Agg") return True except ImportError: QMessageBox.warning(self, _("Import error"), _("Please install matplotlib" " or guiqwt.")) def plot_item(self, funcname): """Plot item""" index = self.currentIndex() if self.__prepare_plot(): key = self.model.get_key(index) try: self.plot(key, funcname) except (ValueError, TypeError) as error: QMessageBox.critical(self, _( "Plot"), _("Unable to plot data." "

Error message:
%s" ) % str(error)) def imshow_item(self): """Imshow item""" index = self.currentIndex() if self.__prepare_plot(): key = self.model.get_key(index) try: if self.is_image(key): self.show_image(key) else: self.imshow(key) except (ValueError, TypeError) as error: QMessageBox.critical(self, _( "Plot"), _("Unable to show image." "

Error message:
%s" ) % str(error)) def save_array(self): """Save array""" title = _( "Save array") if self.array_filename is None: self.array_filename = getcwd() self.emit(SIGNAL('redirect_stdio(bool)'), False) filename, _selfilter = getsavefilename(self, title, self.array_filename, _("NumPy arrays")+" (*.npy)") self.emit(SIGNAL('redirect_stdio(bool)'), True) if filename: self.array_filename = filename data = self.delegate.get_value( self.currentIndex() ) try: import numpy as np np.save(self.array_filename, data) except Exception as error: QMessageBox.critical(self, title, _("Unable to save array" "

Error message:
%s" ) % str(error)) def copy(self): """Copy text to clipboard""" clipboard = QApplication.clipboard() clipl = [] for idx in self.selectedIndexes(): if not idx.isValid(): continue clipl.append(to_text_string(self.delegate.get_value(idx))) clipboard.setText(u('\n').join(clipl)) def import_from_string(self, text, title=None): """Import data from string""" data = self.model.get_data() editor = ImportWizard(self, text, title=title, contents_title=_("Clipboard contents"), varname=fix_reference_name("data", blacklist=list(data.keys()))) if editor.exec_(): var_name, clip_data = editor.get_data() self.new_value(var_name, clip_data) def paste(self): """Import text/data/code from clipboard""" clipboard = QApplication.clipboard() cliptext = '' if clipboard.mimeData().hasText(): cliptext = to_text_string(clipboard.text()) if cliptext.strip(): self.import_from_string(cliptext, title=_("Import from clipboard")) else: QMessageBox.warning(self, _( "Empty clipboard"), _("Nothing to be imported from clipboard.")) class DictEditorTableView(BaseTableView): """DictEditor table view""" def __init__(self, parent, data, readonly=False, title="", names=False, truncate=True, minmax=False): BaseTableView.__init__(self, parent) self.dictfilter = None self.readonly = readonly or isinstance(data, tuple) DictModelClass = ReadOnlyDictModel if self.readonly else DictModel self.model = DictModelClass(self, data, title, names=names, truncate=truncate, minmax=minmax) self.setModel(self.model) self.delegate = DictDelegate(self) self.setItemDelegate(self.delegate) self.setup_table() self.menu = self.setup_menu(truncate, minmax) #------ Remote/local API --------------------------------------------------- def remove_values(self, keys): """Remove values from data""" data = self.model.get_data() for key in sorted(keys, reverse=True): data.pop(key) self.set_data(data) def copy_value(self, orig_key, new_key): """Copy value""" data = self.model.get_data() data[new_key] = data[orig_key] self.set_data(data) def new_value(self, key, value): """Create new value in data""" data = self.model.get_data() data[key] = value self.set_data(data) def is_list(self, key): """Return True if variable is a list or a tuple""" data = self.model.get_data() return isinstance(data[key], (tuple, list)) def get_len(self, key): """Return sequence length""" data = self.model.get_data() return len(data[key]) def is_array(self, key): """Return True if variable is a numpy array""" data = self.model.get_data() return isinstance(data[key], (ndarray, MaskedArray)) def is_image(self, key): """Return True if variable is a PIL.Image image""" data = self.model.get_data() return isinstance(data[key], Image) def is_dict(self, key): """Return True if variable is a dictionary""" data = self.model.get_data() return isinstance(data[key], dict) def get_array_shape(self, key): """Return array's shape""" data = self.model.get_data() return data[key].shape def get_array_ndim(self, key): """Return array's ndim""" data = self.model.get_data() return data[key].ndim def oedit(self, key): """Edit item""" data = self.model.get_data() from spyderlib.widgets.objecteditor import oedit oedit(data[key]) def plot(self, key, funcname): """Plot item""" data = self.model.get_data() import spyderlib.pyplot as plt plt.figure() getattr(plt, funcname)(data[key]) plt.show() def imshow(self, key): """Show item's image""" data = self.model.get_data() import spyderlib.pyplot as plt plt.figure() plt.imshow(data[key]) plt.show() def show_image(self, key): """Show image (item is a PIL image)""" data = self.model.get_data() data[key].show() #--------------------------------------------------------------------------- def refresh_menu(self): """Refresh context menu""" data = self.model.get_data() index = self.currentIndex() condition = (not isinstance(data, tuple)) and index.isValid() \ and not self.readonly self.edit_action.setEnabled( condition ) self.remove_action.setEnabled( condition ) self.insert_action.setEnabled( not self.readonly ) self.refresh_plot_entries(index) def set_filter(self, dictfilter=None): """Set table dict filter""" self.dictfilter = dictfilter class DictEditorWidget(QWidget): """Dictionary Editor Dialog""" def __init__(self, parent, data, readonly=False, title="", remote=False): QWidget.__init__(self, parent) if remote: self.editor = RemoteDictEditorTableView(self, data, readonly) else: self.editor = DictEditorTableView(self, data, readonly, title) layout = QVBoxLayout() layout.addWidget(self.editor) self.setLayout(layout) def set_data(self, data): """Set DictEditor data""" self.editor.set_data(data) def get_title(self): """Get model title""" return self.editor.model.title class DictEditor(QDialog): """Dictionary/List Editor Dialog""" def __init__(self, parent=None): QDialog.__init__(self, parent) # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) self.data_copy = None self.widget = None def setup(self, data, title='', readonly=False, width=500, icon='dictedit.png', remote=False, parent=None): if isinstance(data, dict): # dictionnary self.data_copy = data.copy() datalen = len(data) elif isinstance(data, (tuple, list)): # list, tuple self.data_copy = data[:] datalen = len(data) else: # unknown object import copy self.data_copy = copy.deepcopy(data) datalen = len(dir(data)) self.widget = DictEditorWidget(self, self.data_copy, title=title, readonly=readonly, remote=remote) layout = QVBoxLayout() layout.addWidget(self.widget) self.setLayout(layout) # Buttons configuration buttons = QDialogButtonBox.Ok if not readonly: buttons = buttons | QDialogButtonBox.Cancel bbox = QDialogButtonBox(buttons) self.connect(bbox, SIGNAL("accepted()"), SLOT("accept()")) if not readonly: self.connect(bbox, SIGNAL("rejected()"), SLOT("reject()")) layout.addWidget(bbox) constant = 121 row_height = 30 error_margin = 20 height = constant + row_height*min([15, datalen]) + error_margin self.resize(width, height) self.setWindowTitle(self.widget.get_title()) if is_text_string(icon): icon = get_icon(icon) self.setWindowIcon(icon) # Make the dialog act as a window self.setWindowFlags(Qt.Window) def get_value(self): """Return modified copy of dictionary or list""" # It is import to avoid accessing Qt C++ object as it has probably # already been destroyed, due to the Qt.WA_DeleteOnClose attribute return self.data_copy #----Remote versions of DictDelegate and DictEditorTableView class RemoteDictDelegate(DictDelegate): """DictEditor Item Delegate""" def __init__(self, parent=None, get_value_func=None, set_value_func=None): DictDelegate.__init__(self, parent) self.get_value_func = get_value_func self.set_value_func = set_value_func def get_value(self, index): if index.isValid(): name = index.model().keys[index.row()] return self.get_value_func(name) def set_value(self, index, value): if index.isValid(): name = index.model().keys[index.row()] self.set_value_func(name, value) class RemoteDictEditorTableView(BaseTableView): """DictEditor table view""" def __init__(self, parent, data, truncate=True, minmax=False, get_value_func=None, set_value_func=None, new_value_func=None, remove_values_func=None, copy_value_func=None, is_list_func=None, get_len_func=None, is_array_func=None, is_image_func=None, is_dict_func=None, get_array_shape_func=None, get_array_ndim_func=None, oedit_func=None, plot_func=None, imshow_func=None, is_data_frame_func=None, is_series_func=None, show_image_func=None, remote_editing=False): BaseTableView.__init__(self, parent) self.remote_editing_enabled = None self.remove_values = remove_values_func self.copy_value = copy_value_func self.new_value = new_value_func self.is_data_frame = is_data_frame_func self.is_series = is_series_func self.is_list = is_list_func self.get_len = get_len_func self.is_array = is_array_func self.is_image = is_image_func self.is_dict = is_dict_func self.get_array_shape = get_array_shape_func self.get_array_ndim = get_array_ndim_func self.oedit = oedit_func self.plot = plot_func self.imshow = imshow_func self.show_image = show_image_func self.dictfilter = None self.model = None self.delegate = None self.readonly = False self.model = DictModel(self, data, names=True, truncate=truncate, minmax=minmax, remote=True) self.setModel(self.model) self.delegate = RemoteDictDelegate(self, get_value_func, set_value_func) self.setItemDelegate(self.delegate) self.setup_table() self.menu = self.setup_menu(truncate, minmax) def setup_menu(self, truncate, minmax): """Setup context menu""" menu = BaseTableView.setup_menu(self, truncate, minmax) return menu def oedit_possible(self, key): if (self.is_list(key) or self.is_dict(key) or self.is_array(key) or self.is_image(key) or self.is_data_frame(key) or self.is_series(key)): # If this is a remote dict editor, the following avoid # transfering large amount of data through the socket return True def edit_item(self): """ Reimplement BaseTableView's method to edit item Some supported data types are directly edited in the remote process, thus avoiding to transfer large amount of data through the socket from the remote process to Spyder """ if self.remote_editing_enabled: index = self.currentIndex() if not index.isValid(): return key = self.model.get_key(index) if self.oedit_possible(key): # If this is a remote dict editor, the following avoid # transfering large amount of data through the socket self.oedit(key) else: BaseTableView.edit_item(self) else: BaseTableView.edit_item(self) def get_test_data(): """Create test data""" import numpy as np from spyderlib.pil_patch import Image image = Image.fromarray(np.random.random_integers(255, size=(100, 100)), mode='P') testdict = {'d': 1, 'a': np.random.rand(10, 10), 'b': [1, 2]} testdate = datetime.date(1945, 5, 8) class Foobar(object): def __init__(self): self.text = "toto" self.testdict = testdict self.testdate = testdate foobar = Foobar() return {'object': foobar, 'str': 'kjkj kj k j j kj k jkj', 'unicode': to_text_string('éù', 'utf-8'), 'list': [1, 3, [sorted, 5, 6], 'kjkj', None], 'tuple': ([1, testdate, testdict], 'kjkj', None), 'dict': testdict, 'float': 1.2233, 'int': 223, 'bool': True, 'array': np.random.rand(10, 10), 'masked_array': np.ma.array([[1, 0], [1, 0]], mask=[[True, False], [False, False]]), '1D-array': np.linspace(-10, 10), 'empty_array': np.array([]), 'image': image, 'date': testdate, 'datetime': datetime.datetime(1945, 5, 8), 'complex': 2+1j, 'complex64': np.complex64(2+1j), 'int8_scalar': np.int8(8), 'int16_scalar': np.int16(16), 'int32_scalar': np.int32(32), 'bool_scalar': np.bool(8), 'unsupported1': np.arccos, 'unsupported2': np.cast, #1: (1, 2, 3), -5: ("a", "b", "c"), 2.5: np.array((4.0, 6.0, 8.0)), } def test(): """Dictionary editor test""" app = qapplication() #analysis:ignore dialog = DictEditor() dialog.setup(get_test_data()) dialog.show() app.exec_() print("out:", dialog.get_value()) def remote_editor_test(): """Remote dictionary editor test""" from spyderlib.plugins.variableexplorer import VariableExplorer from spyderlib.widgets.externalshell.monitor import make_remote_view remote = make_remote_view(get_test_data(), VariableExplorer.get_settings()) from pprint import pprint pprint(remote) app = qapplication() dialog = DictEditor() dialog.setup(remote, remote=True) dialog.show() app.exec_() if dialog.result(): print(dialog.get_value()) if __name__ == "__main__": remote_editor_test()