source: sasview/src/sas/qtgui/MainWindow/DataExplorer.py @ 515c23df

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 515c23df was 515c23df, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

fixed deleteItem behaviour as per SASVIEW-956 discussion

  • Property mode set to 100644
File size: 42.4 KB
RevLine 
[f721030]1# global
2import sys
3import os
[e540cd2]4import time
[f721030]5import logging
6
[4992ff2]7from PyQt5 import QtCore
8from PyQt5 import QtGui
9from PyQt5 import QtWidgets
[481ff26]10
[f721030]11from twisted.internet import threads
12
[dc5ef15]13# SASCALC
[f721030]14from sas.sascalc.dataloader.loader import Loader
15
[dc5ef15]16# QTGUI
[83eb5208]17import sas.qtgui.Utilities.GuiUtils as GuiUtils
18import sas.qtgui.Plotting.PlotHelper as PlotHelper
[dc5ef15]19
20from sas.qtgui.Plotting.PlotterData import Data1D
21from sas.qtgui.Plotting.PlotterData import Data2D
[83eb5208]22from sas.qtgui.Plotting.Plotter import Plotter
23from sas.qtgui.Plotting.Plotter2D import Plotter2D
24from sas.qtgui.Plotting.MaskEditor import MaskEditor
25
[dc5ef15]26from sas.qtgui.MainWindow.DataManager import DataManager
27from sas.qtgui.MainWindow.DroppableDataLoadWidget import DroppableDataLoadWidget
28
[83eb5208]29import sas.qtgui.Perspectives as Perspectives
[1970780]30
[d4881f6a]31DEFAULT_PERSPECTIVE = "Fitting"
32
[515c23df]33logger = logging.getLogger(__name__)
34
[f82ab8c]35class DataExplorerWindow(DroppableDataLoadWidget):
[f721030]36    # The controller which is responsible for managing signal slots connections
37    # for the gui and providing an interface to the data model.
38
[630155bd]39    def __init__(self, parent=None, guimanager=None, manager=None):
[f82ab8c]40        super(DataExplorerWindow, self).__init__(parent, guimanager)
[f721030]41
42        # Main model for keeping loaded data
43        self.model = QtGui.QStandardItemModel(self)
[f82ab8c]44
45        # Secondary model for keeping frozen data sets
46        self.theory_model = QtGui.QStandardItemModel(self)
[f721030]47
48        # GuiManager is the actual parent, but we needed to also pass the QMainWindow
49        # in order to set the widget parentage properly.
50        self.parent = guimanager
51        self.loader = Loader()
[630155bd]52        self.manager = manager if manager is not None else DataManager()
[4992ff2]53        self.txt_widget = QtWidgets.QTextEdit(None)
[f721030]54
[481ff26]55        # Be careful with twisted threads.
[4992ff2]56        self.mutex = QtCore.QMutex()
[481ff26]57
[8cb6cd6]58        # Active plots
[7d077d1]59        self.active_plots = {}
[8cb6cd6]60
[f721030]61        # Connect the buttons
62        self.cmdLoad.clicked.connect(self.loadFile)
[f82ab8c]63        self.cmdDeleteData.clicked.connect(self.deleteFile)
64        self.cmdDeleteTheory.clicked.connect(self.deleteTheory)
65        self.cmdFreeze.clicked.connect(self.freezeTheory)
[f721030]66        self.cmdSendTo.clicked.connect(self.sendData)
[1042dba]67        self.cmdNew.clicked.connect(self.newPlot)
[0268aed]68        self.cmdNew_2.clicked.connect(self.newPlot)
[8cb6cd6]69        self.cmdAppend.clicked.connect(self.appendPlot)
[481ff26]70        self.cmdHelp.clicked.connect(self.displayHelp)
71        self.cmdHelp_2.clicked.connect(self.displayHelp)
72
[83d6249]73        # Fill in the perspectives combo
74        self.initPerspectives()
75
[e540cd2]76        # Custom context menu
77        self.treeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
78        self.treeView.customContextMenuRequested.connect(self.onCustomContextMenu)
[4b71e91]79        self.contextMenu()
[e540cd2]80
[cbcdd2c]81        # Same menus for the theory view
82        self.freezeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
83        self.freezeView.customContextMenuRequested.connect(self.onCustomContextMenu)
84
[488c49d]85        # Connect the comboboxes
86        self.cbSelect.currentIndexChanged.connect(self.selectData)
87
[f82ab8c]88        #self.closeEvent.connect(self.closeEvent)
[cbcdd2c]89        self.currentChanged.connect(self.onTabSwitch)
[e540cd2]90        self.communicator = self.parent.communicator()
[f82ab8c]91        self.communicator.fileReadSignal.connect(self.loadFromURL)
[7d8bebf]92        self.communicator.activeGraphsSignal.connect(self.updateGraphCount)
[27313b7]93        self.communicator.activeGraphName.connect(self.updatePlotName)
[7d077d1]94        self.communicator.plotUpdateSignal.connect(self.updatePlot)
[e20870bc]95        self.communicator.maskEditorSignal.connect(self.showEditDataMask)
[d5c5d3d]96
[8cb6cd6]97        self.cbgraph.editTextChanged.connect(self.enableGraphCombo)
98        self.cbgraph.currentIndexChanged.connect(self.enableGraphCombo)
[f721030]99
100        # Proxy model for showing a subset of Data1D/Data2D content
[4992ff2]101        self.data_proxy = QtCore.QSortFilterProxyModel(self)
[481ff26]102        self.data_proxy.setSourceModel(self.model)
103
104        # Don't show "empty" rows with data objects
105        self.data_proxy.setFilterRegExp(r"[^()]")
[f721030]106
107        # The Data viewer is QTreeView showing the proxy model
[481ff26]108        self.treeView.setModel(self.data_proxy)
109
110        # Proxy model for showing a subset of Theory content
[4992ff2]111        self.theory_proxy = QtCore.QSortFilterProxyModel(self)
[481ff26]112        self.theory_proxy.setSourceModel(self.theory_model)
113
114        # Don't show "empty" rows with data objects
115        self.theory_proxy.setFilterRegExp(r"[^()]")
[f721030]116
[f82ab8c]117        # Theory model view
[481ff26]118        self.freezeView.setModel(self.theory_proxy)
[f82ab8c]119
[8cb6cd6]120        self.enableGraphCombo(None)
121
[cbcdd2c]122        # Current view on model
123        self.current_view = self.treeView
124
[f82ab8c]125    def closeEvent(self, event):
126        """
127        Overwrite the close event - no close!
128        """
129        event.ignore()
[1042dba]130
[cbcdd2c]131    def onTabSwitch(self, index):
132        """ Callback for tab switching signal """
133        if index == 0:
134            self.current_view = self.treeView
135        else:
136            self.current_view = self.freezeView
137
[481ff26]138    def displayHelp(self):
139        """
140        Show the "Loading data" section of help
141        """
[aed0532]142        tree_location = "/user/qtgui/MainWindow/data_explorer_help.html"
[e90988c]143        self.parent.showHelp(tree_location)
[481ff26]144
[8cb6cd6]145    def enableGraphCombo(self, combo_text):
146        """
147        Enables/disables "Assign Plot" elements
148        """
149        self.cbgraph.setEnabled(len(PlotHelper.currentPlots()) > 0)
150        self.cmdAppend.setEnabled(len(PlotHelper.currentPlots()) > 0)
151
[83d6249]152    def initPerspectives(self):
153        """
154        Populate the Perspective combobox and define callbacks
155        """
[b3e8629]156        available_perspectives = sorted([p for p in list(Perspectives.PERSPECTIVES.keys())])
[1970780]157        if available_perspectives:
158            self.cbFitting.clear()
159            self.cbFitting.addItems(available_perspectives)
[83d6249]160        self.cbFitting.currentIndexChanged.connect(self.updatePerspectiveCombo)
161        # Set the index so we see the default (Fitting)
[d4881f6a]162        self.cbFitting.setCurrentIndex(self.cbFitting.findText(DEFAULT_PERSPECTIVE))
[83d6249]163
[1970780]164    def _perspective(self):
165        """
166        Returns the current perspective
167        """
168        return self.parent.perspective()
169
[f82ab8c]170    def loadFromURL(self, url):
171        """
172        Threaded file load
173        """
174        load_thread = threads.deferToThread(self.readData, url)
175        load_thread.addCallback(self.loadComplete)
[7fb471d]176        load_thread.addErrback(self.loadFailed)
[5032ea68]177
178    def loadFile(self, event=None):
[f721030]179        """
180        Called when the "Load" button pressed.
[481ff26]181        Opens the Qt "Open File..." dialog
[f721030]182        """
183        path_str = self.chooseFiles()
184        if not path_str:
185            return
[f82ab8c]186        self.loadFromURL(path_str)
[f721030]187
[5032ea68]188    def loadFolder(self, event=None):
[f721030]189        """
[5032ea68]190        Called when the "File/Load Folder" menu item chosen.
[481ff26]191        Opens the Qt "Open Folder..." dialog
[f721030]192        """
[7969b9c]193        folder = QtWidgets.QFileDialog.getExistingDirectory(self, "Choose a directory", "",
[4992ff2]194              QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog)
[481ff26]195        if folder is None:
[5032ea68]196            return
197
[481ff26]198        folder = str(folder)
[f721030]199
[481ff26]200        if not os.path.isdir(folder):
[5032ea68]201            return
[f721030]202
[5032ea68]203        # get content of dir into a list
[481ff26]204        path_str = [os.path.join(os.path.abspath(folder), filename)
[e540cd2]205                    for filename in os.listdir(folder)]
[f721030]206
[f82ab8c]207        self.loadFromURL(path_str)
[5032ea68]208
[630155bd]209    def loadProject(self):
210        """
211        Called when the "Open Project" menu item chosen.
212        """
213        kwargs = {
214            'parent'    : self,
215            'caption'   : 'Open Project',
216            'filter'    : 'Project (*.json);;All files (*.*)',
[4992ff2]217            'options'   : QtWidgets.QFileDialog.DontUseNativeDialog
[630155bd]218        }
[fbfc488]219        filename = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
[630155bd]220        if filename:
221            load_thread = threads.deferToThread(self.readProject, filename)
222            load_thread.addCallback(self.readProjectComplete)
[7fb471d]223            load_thread.addErrback(self.readProjectFailed)
224
[4992ff2]225    def loadFailed(self, reason):
226        """
227        """
228        print("file load FAILED: ", reason)
229        pass
230
[7fb471d]231    def readProjectFailed(self, reason):
232        """
233        """
234        print("readProjectFailed FAILED: ", reason)
235        pass
[630155bd]236
237    def readProject(self, filename):
238        self.communicator.statusBarUpdateSignal.emit("Loading Project... %s" % os.path.basename(filename))
239        try:
240            manager = DataManager()
241            with open(filename, 'r') as infile:
242                manager.load_from_readable(infile)
243
244            self.communicator.statusBarUpdateSignal.emit("Loaded Project: %s" % os.path.basename(filename))
245            return manager
246
247        except:
248            self.communicator.statusBarUpdateSignal.emit("Failed: %s" % os.path.basename(filename))
249            raise
250
251    def readProjectComplete(self, manager):
252        self.model.clear()
253
254        self.manager.assign(manager)
[4992ff2]255        self.model.beginResetModel()
[b3e8629]256        for id, item in self.manager.get_all_data().items():
[630155bd]257            self.updateModel(item.data, item.path)
258
[4992ff2]259        self.model.endResetModel()
[630155bd]260
261    def saveProject(self):
262        """
263        Called when the "Save Project" menu item chosen.
264        """
265        kwargs = {
266            'parent'    : self,
267            'caption'   : 'Save Project',
268            'filter'    : 'Project (*.json)',
[4992ff2]269            'options'   : QtWidgets.QFileDialog.DontUseNativeDialog
[630155bd]270        }
[d6b8a1d]271        name_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
272        filename = name_tuple[0]
[630155bd]273        if filename:
[d6b8a1d]274            _, extension = os.path.splitext(filename)
275            if not extension:
276                filename = '.'.join((filename, 'json'))
[630155bd]277            self.communicator.statusBarUpdateSignal.emit("Saving Project... %s\n" % os.path.basename(filename))
278            with open(filename, 'w') as outfile:
279                self.manager.save_to_writable(outfile)
280
[5032ea68]281    def deleteFile(self, event):
282        """
283        Delete selected rows from the model
284        """
285        # Assure this is indeed wanted
286        delete_msg = "This operation will delete the checked data sets and all the dependents." +\
287                     "\nDo you want to continue?"
[53c771e]288        reply = QtWidgets.QMessageBox.question(self,
[e540cd2]289                                           'Warning',
290                                           delete_msg,
[53c771e]291                                           QtWidgets.QMessageBox.Yes,
292                                           QtWidgets.QMessageBox.No)
[5032ea68]293
[53c771e]294        if reply == QtWidgets.QMessageBox.No:
[5032ea68]295            return
296
297        # Figure out which rows are checked
298        ind = -1
299        # Use 'while' so the row count is forced at every iteration
[515c23df]300        deleted_items = []
[1420066]301        deleted_names = []
[5032ea68]302        while ind < self.model.rowCount():
303            ind += 1
304            item = self.model.item(ind)
[f0bb711]305
[5032ea68]306            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
307                # Delete these rows from the model
[1420066]308                deleted_names.append(str(self.model.item(ind).text()))
[515c23df]309                deleted_items.append(item)
[f0bb711]310
[5032ea68]311                self.model.removeRow(ind)
312                # Decrement index since we just deleted it
313                ind -= 1
314
[38eb433]315        # Let others know we deleted data
[515c23df]316        self.communicator.dataDeletedSignal.emit(deleted_items)
[f721030]317
[f0bb711]318        # update stored_data
[1420066]319        self.manager.update_stored_data(deleted_names)
[f0bb711]320
[f82ab8c]321    def deleteTheory(self, event):
322        """
323        Delete selected rows from the theory model
324        """
325        # Assure this is indeed wanted
326        delete_msg = "This operation will delete the checked data sets and all the dependents." +\
327                     "\nDo you want to continue?"
[53c771e]328        reply = QtWidgets.QMessageBox.question(self,
[8cb6cd6]329                                           'Warning',
330                                           delete_msg,
[53c771e]331                                           QtWidgets.QMessageBox.Yes,
332                                           QtWidgets.QMessageBox.No)
[f82ab8c]333
[53c771e]334        if reply == QtWidgets.QMessageBox.No:
[f82ab8c]335            return
336
337        # Figure out which rows are checked
338        ind = -1
339        # Use 'while' so the row count is forced at every iteration
340        while ind < self.theory_model.rowCount():
341            ind += 1
342            item = self.theory_model.item(ind)
343            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
344                # Delete these rows from the model
345                self.theory_model.removeRow(ind)
346                # Decrement index since we just deleted it
347                ind -= 1
348
349        # pass temporarily kept as a breakpoint anchor
350        pass
351
[f721030]352    def sendData(self, event):
353        """
[5032ea68]354        Send selected item data to the current perspective and set the relevant notifiers
[f721030]355        """
[5032ea68]356        # Set the signal handlers
357        self.communicator.updateModelFromPerspectiveSignal.connect(self.updateModelFromPerspective)
[f721030]358
[adf81b8]359        def isItemReady(index):
[5032ea68]360            item = self.model.item(index)
[adf81b8]361            return item.isCheckable() and item.checkState() == QtCore.Qt.Checked
362
363        # Figure out which rows are checked
364        selected_items = [self.model.item(index)
[b3e8629]365                          for index in range(self.model.rowCount())
[adf81b8]366                          if isItemReady(index)]
[f721030]367
[f82ab8c]368        if len(selected_items) < 1:
369            return
370
[f721030]371        # Which perspective has been selected?
[1970780]372        if len(selected_items) > 1 and not self._perspective().allowBatch():
373            msg = self._perspective().title() + " does not allow multiple data."
[53c771e]374            msgbox = QtWidgets.QMessageBox()
375            msgbox.setIcon(QtWidgets.QMessageBox.Critical)
[5032ea68]376            msgbox.setText(msg)
[53c771e]377            msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
[5032ea68]378            retval = msgbox.exec_()
379            return
[a281ab8]380
[f721030]381        # Notify the GuiManager about the send request
[ee18d33]382        self._perspective().setData(data_item=selected_items, is_batch=self.chkBatch.isChecked())
[5032ea68]383
[f82ab8c]384    def freezeTheory(self, event):
385        """
386        Freeze selected theory rows.
387
[ca8b853]388        "Freezing" means taking the plottable data from the Theory item
389        and copying it to a separate top-level item in Data.
[f82ab8c]390        """
[ca8b853]391        # Figure out which rows are checked
[f82ab8c]392        # Use 'while' so the row count is forced at every iteration
393        outer_index = -1
[481ff26]394        theories_copied = 0
[ca8b853]395        while outer_index < self.theory_model.rowCount():
[f82ab8c]396            outer_index += 1
[ca8b853]397            outer_item = self.theory_model.item(outer_index)
[f82ab8c]398            if not outer_item:
399                continue
[ca8b853]400            if outer_item.isCheckable() and \
401                   outer_item.checkState() == QtCore.Qt.Checked:
[7969b9c]402                self.model.beginResetModel()
[ca8b853]403                theories_copied += 1
404                new_item = self.recursivelyCloneItem(outer_item)
405                # Append a "unique" descriptor to the name
406                time_bit = str(time.time())[7:-1].replace('.', '')
407                new_name = new_item.text() + '_@' + time_bit
408                new_item.setText(new_name)
409                self.model.appendRow(new_item)
[7969b9c]410                self.model.endResetModel()
411            #self.model.reset()
[f82ab8c]412
[481ff26]413        freeze_msg = ""
414        if theories_copied == 0:
415            return
416        elif theories_copied == 1:
[ca8b853]417            freeze_msg = "1 theory copied from the Theory tab as a data set"
[481ff26]418        elif theories_copied > 1:
[ca8b853]419            freeze_msg = "%i theories copied from the Theory tab as data sets" % theories_copied
[481ff26]420        else:
421            freeze_msg = "Unexpected number of theories copied: %i" % theories_copied
[b3e8629]422            raise AttributeError(freeze_msg)
[481ff26]423        self.communicator.statusBarUpdateSignal.emit(freeze_msg)
424        # Actively switch tabs
425        self.setCurrentIndex(1)
426
427    def recursivelyCloneItem(self, item):
428        """
429        Clone QStandardItem() object
430        """
431        new_item = item.clone()
432        # clone doesn't do deepcopy :(
[b3e8629]433        for child_index in range(item.rowCount()):
[481ff26]434            child_item = self.recursivelyCloneItem(item.child(child_index))
435            new_item.setChild(child_index, child_item)
436        return new_item
[f82ab8c]437
[27313b7]438    def updatePlotName(self, name_tuple):
439        """
440        Modify the name of the current plot
441        """
442        old_name, current_name = name_tuple
443        ind = self.cbgraph.findText(old_name)
444        self.cbgraph.setCurrentIndex(ind)
445        self.cbgraph.setItemText(ind, current_name)
446
[7d8bebf]447    def updateGraphCount(self, graph_list):
448        """
449        Modify the graph name combo and potentially remove
450        deleted graphs
451        """
452        self.updateGraphCombo(graph_list)
453
454        if not self.active_plots:
455            return
456        new_plots = [PlotHelper.plotById(plot) for plot in graph_list]
[b3e8629]457        active_plots_copy = list(self.active_plots.keys())
[7d8bebf]458        for plot in active_plots_copy:
459            if self.active_plots[plot] in new_plots:
460                continue
461            self.active_plots.pop(plot)
462
[8cb6cd6]463    def updateGraphCombo(self, graph_list):
464        """
465        Modify Graph combo box on graph add/delete
466        """
467        orig_text = self.cbgraph.currentText()
468        self.cbgraph.clear()
[0268aed]469        self.cbgraph.insertItems(0, graph_list)
[8cb6cd6]470        ind = self.cbgraph.findText(orig_text)
471        if ind > 0:
472            self.cbgraph.setCurrentIndex(ind)
473
[83d6249]474    def updatePerspectiveCombo(self, index):
475        """
476        Notify the gui manager about the new perspective chosen.
477        """
[8ac3551]478        self.communicator.perspectiveChangedSignal.emit(self.cbFitting.itemText(index))
[1970780]479        self.chkBatch.setEnabled(self.parent.perspective().allowBatch())
[83d6249]480
[d4dac80]481    def itemFromFilename(self, filename):
482        """
483        Retrieves model item corresponding to the given filename
484        """
485        item = GuiUtils.itemFromFilename(filename, self.model)
486        return item
487
[3b3b40b]488    def displayFile(self, filename=None, is_data=True):
[d48cc19]489        """
490        Forces display of charts for the given filename
491        """
[3b3b40b]492        model = self.model if is_data else self.theory_model
[88e1f57]493        # Now query the model item for available plots
[d48cc19]494        plots = GuiUtils.plotsFromFilename(filename, model)
[88e1f57]495        item = GuiUtils.itemFromFilename(filename, model)
496
497        new_plots = []
[fef38e8]498        for plot in plots:
499            plot_id = plot.id
[b3e8629]500            if plot_id in list(self.active_plots.keys()):
[7d8bebf]501                self.active_plots[plot_id].replacePlot(plot_id, plot)
[fef38e8]502            else:
[88e1f57]503                # 'sophisticated' test to generate standalone plot for residuals
504                if 'esiduals' in plot.title:
505                    self.plotData([(item, plot)])
506                else:
507                    new_plots.append((item, plot))
508
509        if new_plots:
510            self.plotData(new_plots)
[56b22f9]511
[3b3b40b]512    def displayData(self, data_list):
513        """
514        Forces display of charts for the given data set
515        """
516        plot_to_show = data_list[0]
517        # passed plot is used ONLY to figure out its title,
518        # so all the charts related by it can be pulled from
519        # the data explorer indices.
520        filename = plot_to_show.filename
521        self.displayFile(filename=filename, is_data=plot_to_show.is_data)
522
[56b22f9]523    def addDataPlot2D(self, plot_set, item):
[672b8ab]524        """
525        Create a new 2D plot and add it to the workspace
526        """
[56b22f9]527        plot2D = Plotter2D(self)
528        plot2D.item = item
529        plot2D.plot(plot_set)
530        self.addPlot(plot2D)
[88e1f57]531        self.active_plots[plot2D.data.id] = plot2D
[56b22f9]532        #============================================
[672b8ab]533        # Experimental hook for silx charts
534        #============================================
[56b22f9]535        ## Attach silx
536        #from silx.gui import qt
537        #from silx.gui.plot import StackView
538        #sv = StackView()
539        #sv.setColormap("jet", autoscale=True)
540        #sv.setStack(plot_set.data.reshape(1,100,100))
541        ##sv.setLabels(["x: -10 to 10 (200 samples)",
542        ##              "y: -10 to 5 (150 samples)"])
543        #sv.show()
544        #============================================
545
546    def plotData(self, plots):
547        """
548        Takes 1D/2D data and generates a single plot (1D) or multiple plots (2D)
[1042dba]549        """
550        # Call show on requested plots
[31c5b58]551        # All same-type charts in one plot
[3bdbfcc]552        for item, plot_set in plots:
[49e124c]553            if isinstance(plot_set, Data1D):
[7d8bebf]554                if not 'new_plot' in locals():
555                    new_plot = Plotter(self)
[9290b1a]556                new_plot.plot(plot_set)
[88e1f57]557                # active_plots may contain multiple charts
558                self.active_plots[plot_set.id] = new_plot
[49e124c]559            elif isinstance(plot_set, Data2D):
[56b22f9]560                self.addDataPlot2D(plot_set, item)
[49e124c]561            else:
562                msg = "Incorrect data type passed to Plotting"
[b3e8629]563                raise AttributeError(msg)
[49e124c]564
[7d8bebf]565        if 'new_plot' in locals() and \
[b4b8589]566            hasattr(new_plot, 'data') and \
567            isinstance(new_plot.data, Data1D):
[56b22f9]568                self.addPlot(new_plot)
569
570    def newPlot(self):
571        """
572        Select checked data and plot it
573        """
574        # Check which tab is currently active
575        if self.current_view == self.treeView:
576            plots = GuiUtils.plotsFromCheckedItems(self.model)
577        else:
578            plots = GuiUtils.plotsFromCheckedItems(self.theory_model)
579
580        self.plotData(plots)
[1042dba]581
[56b22f9]582    def addPlot(self, new_plot):
[31c5b58]583        """
584        Helper method for plot bookkeeping
585        """
586        # Update the global plot counter
[0268aed]587        title = str(PlotHelper.idOfPlot(new_plot))
[31c5b58]588        new_plot.setWindowTitle(title)
[8cb6cd6]589
[7d8bebf]590        # Set the object name to satisfy the Squish object picker
591        new_plot.setObjectName(title)
592
[31c5b58]593        # Add the plot to the workspace
[7969b9c]594        self.parent.workspace().addSubWindow(new_plot)
[8cb6cd6]595
[31c5b58]596        # Show the plot
597        new_plot.show()
[fbfc488]598        new_plot.canvas.draw()
[f721030]599
[31c5b58]600        # Update the active chart list
[88e1f57]601        #self.active_plots[new_plot.data.id] = new_plot
[8cb6cd6]602
603    def appendPlot(self):
604        """
605        Add data set(s) to the existing matplotlib chart
606        """
607        # new plot data
608        new_plots = GuiUtils.plotsFromCheckedItems(self.model)
609
610        # old plot data
[0268aed]611        plot_id = str(self.cbgraph.currentText())
[8cb6cd6]612
[0268aed]613        assert plot_id in PlotHelper.currentPlots(), "No such plot: %s"%(plot_id)
[8cb6cd6]614
615        old_plot = PlotHelper.plotById(plot_id)
616
[14d9c7b]617        # Add new data to the old plot, if data type is the same.
[3bdbfcc]618        for _, plot_set in new_plots:
[14d9c7b]619            if type(plot_set) is type(old_plot._data):
[31c5b58]620                old_plot.data = plot_set
[14d9c7b]621                old_plot.plot()
[8cb6cd6]622
[7d077d1]623    def updatePlot(self, new_data):
624        """
625        Modify existing plot for immediate response
626        """
627        data = new_data[0]
628        assert type(data).__name__ in ['Data1D', 'Data2D']
629
630        id = data.id
[b3e8629]631        if data.id in list(self.active_plots.keys()):
[7d077d1]632            self.active_plots[id].replacePlot(id, data)
633
[f721030]634    def chooseFiles(self):
635        """
[5032ea68]636        Shows the Open file dialog and returns the chosen path(s)
[f721030]637        """
638        # List of known extensions
639        wlist = self.getWlist()
640
641        # Location is automatically saved - no need to keep track of the last dir
[5032ea68]642        # But only with Qt built-in dialog (non-platform native)
[4992ff2]643        paths = QtWidgets.QFileDialog.getOpenFileNames(self, "Choose a file", "",
[7969b9c]644                wlist, None, QtWidgets.QFileDialog.DontUseNativeDialog)[0]
[fbfc488]645        if not paths:
[f721030]646            return
647
[0cd8612]648        if not isinstance(paths, list):
[f721030]649            paths = [paths]
650
[9e426c1]651        return paths
[f721030]652
653    def readData(self, path):
654        """
[481ff26]655        verbatim copy-paste from
656           sasgui.guiframe.local_perspectives.data_loader.data_loader.py
[f721030]657        slightly modified for clarity
658        """
659        message = ""
660        log_msg = ''
661        output = {}
662        any_error = False
663        data_error = False
664        error_message = ""
[e540cd2]665        number_of_files = len(path)
666        self.communicator.progressBarUpdateSignal.emit(0.0)
667
668        for index, p_file in enumerate(path):
[f721030]669            basename = os.path.basename(p_file)
670            _, extension = os.path.splitext(basename)
[0cd8612]671            if extension.lower() in GuiUtils.EXTENSIONS:
[f721030]672                any_error = True
673                log_msg = "Data Loader cannot "
674                log_msg += "load: %s\n" % str(p_file)
675                log_msg += """Please try to open that file from "open project" """
676                log_msg += """or "open analysis" menu\n"""
677                error_message = log_msg + "\n"
678                logging.info(log_msg)
679                continue
680
681            try:
[5032ea68]682                message = "Loading Data... " + str(basename) + "\n"
[f721030]683
684                # change this to signal notification in GuiManager
[f82ab8c]685                self.communicator.statusBarUpdateSignal.emit(message)
[f721030]686
687                output_objects = self.loader.load(p_file)
688
689                # Some loaders return a list and some just a single Data1D object.
690                # Standardize.
691                if not isinstance(output_objects, list):
692                    output_objects = [output_objects]
693
694                for item in output_objects:
[481ff26]695                    # cast sascalc.dataloader.data_info.Data1D into
696                    # sasgui.guiframe.dataFitting.Data1D
[f721030]697                    # TODO : Fix it
698                    new_data = self.manager.create_gui_data(item, p_file)
699                    output[new_data.id] = new_data
[481ff26]700
701                    # Model update should be protected
702                    self.mutex.lock()
[f721030]703                    self.updateModel(new_data, p_file)
[7969b9c]704                    #self.model.reset()
705                    QtWidgets.QApplication.processEvents()
[481ff26]706                    self.mutex.unlock()
[f721030]707
708                    if hasattr(item, 'errors'):
709                        for error_data in item.errors:
710                            data_error = True
711                            message += "\tError: {0}\n".format(error_data)
712                    else:
[5032ea68]713
[f721030]714                        logging.error("Loader returned an invalid object:\n %s" % str(item))
715                        data_error = True
716
[5032ea68]717            except Exception as ex:
[b3e8629]718                logging.error(sys.exc_info()[1])
[5032ea68]719
[f721030]720                any_error = True
721            if any_error or error_message != "":
722                if error_message == "":
723                    error = "Error: " + str(sys.exc_info()[1]) + "\n"
724                    error += "while loading Data: \n%s\n" % str(basename)
725                    error_message += "The data file you selected could not be loaded.\n"
726                    error_message += "Make sure the content of your file"
727                    error_message += " is properly formatted.\n\n"
728                    error_message += "When contacting the SasView team, mention the"
729                    error_message += " following:\n%s" % str(error)
730                elif data_error:
731                    base_message = "Errors occurred while loading "
732                    base_message += "{0}\n".format(basename)
733                    base_message += "The data file loaded but with errors.\n"
734                    error_message = base_message + error_message
735                else:
736                    error_message += "%s\n" % str(p_file)
[481ff26]737
[e540cd2]738            current_percentage = int(100.0* index/number_of_files)
739            self.communicator.progressBarUpdateSignal.emit(current_percentage)
740
[f721030]741        if any_error or error_message:
[0cd8612]742            logging.error(error_message)
743            status_bar_message = "Errors occurred while loading %s" % format(basename)
744            self.communicator.statusBarUpdateSignal.emit(status_bar_message)
[f721030]745
746        else:
747            message = "Loading Data Complete! "
748        message += log_msg
[0cd8612]749        # Notify the progress bar that the updates are over.
[e540cd2]750        self.communicator.progressBarUpdateSignal.emit(-1)
[454670d]751        self.communicator.statusBarUpdateSignal.emit(message)
[481ff26]752
[a281ab8]753        return output, message
[f721030]754
755    def getWlist(self):
756        """
[f82ab8c]757        Wildcards of files we know the format of.
[f721030]758        """
759        # Display the Qt Load File module
760        cards = self.loader.get_wildcards()
761
762        # get rid of the wx remnant in wildcards
763        # TODO: modify sasview loader get_wildcards method, after merge,
764        # so this kludge can be avoided
765        new_cards = []
766        for item in cards:
767            new_cards.append(item[:item.find("|")])
768        wlist = ';;'.join(new_cards)
769
770        return wlist
[488c49d]771
772    def selectData(self, index):
773        """
774        Callback method for modifying the TreeView on Selection Options change
775        """
776        if not isinstance(index, int):
777            msg = "Incorrect type passed to DataExplorer.selectData()"
[b3e8629]778            raise AttributeError(msg)
[488c49d]779
780        # Respond appropriately
781        if index == 0:
782            # Select All
[5032ea68]783            for index in range(self.model.rowCount()):
784                item = self.model.item(index)
785                if item.isCheckable() and item.checkState() == QtCore.Qt.Unchecked:
[488c49d]786                    item.setCheckState(QtCore.Qt.Checked)
787        elif index == 1:
788            # De-select All
[5032ea68]789            for index in range(self.model.rowCount()):
790                item = self.model.item(index)
791                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
[488c49d]792                    item.setCheckState(QtCore.Qt.Unchecked)
793
794        elif index == 2:
795            # Select All 1-D
[5032ea68]796            for index in range(self.model.rowCount()):
797                item = self.model.item(index)
[488c49d]798                item.setCheckState(QtCore.Qt.Unchecked)
[5032ea68]799
800                try:
[8548d739]801                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
[5032ea68]802                except AttributeError:
803                    msg = "Bad structure of the data model."
[b3e8629]804                    raise RuntimeError(msg)
[5032ea68]805
806                if is1D:
[488c49d]807                    item.setCheckState(QtCore.Qt.Checked)
808
809        elif index == 3:
810            # Unselect All 1-D
[5032ea68]811            for index in range(self.model.rowCount()):
812                item = self.model.item(index)
813
814                try:
[8548d739]815                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
[5032ea68]816                except AttributeError:
817                    msg = "Bad structure of the data model."
[b3e8629]818                    raise RuntimeError(msg)
[5032ea68]819
820                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is1D:
[488c49d]821                    item.setCheckState(QtCore.Qt.Unchecked)
822
823        elif index == 4:
824            # Select All 2-D
[5032ea68]825            for index in range(self.model.rowCount()):
826                item = self.model.item(index)
827                item.setCheckState(QtCore.Qt.Unchecked)
828                try:
[8548d739]829                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
[5032ea68]830                except AttributeError:
831                    msg = "Bad structure of the data model."
[b3e8629]832                    raise RuntimeError(msg)
[5032ea68]833
834                if is2D:
[488c49d]835                    item.setCheckState(QtCore.Qt.Checked)
836
837        elif index == 5:
838            # Unselect All 2-D
[5032ea68]839            for index in range(self.model.rowCount()):
840                item = self.model.item(index)
841
842                try:
[8548d739]843                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
[5032ea68]844                except AttributeError:
845                    msg = "Bad structure of the data model."
[b3e8629]846                    raise RuntimeError(msg)
[5032ea68]847
848                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is2D:
[488c49d]849                    item.setCheckState(QtCore.Qt.Unchecked)
850
851        else:
852            msg = "Incorrect value in the Selection Option"
853            # Change this to a proper logging action
[b3e8629]854            raise Exception(msg)
[488c49d]855
[4b71e91]856    def contextMenu(self):
[e540cd2]857        """
[4b71e91]858        Define actions and layout of the right click context menu
[e540cd2]859        """
[4b71e91]860        # Create a custom menu based on actions defined in the UI file
[4992ff2]861        self.context_menu = QtWidgets.QMenu(self)
[4b71e91]862        self.context_menu.addAction(self.actionDataInfo)
863        self.context_menu.addAction(self.actionSaveAs)
864        self.context_menu.addAction(self.actionQuickPlot)
865        self.context_menu.addSeparator()
866        self.context_menu.addAction(self.actionQuick3DPlot)
867        self.context_menu.addAction(self.actionEditMask)
[c6fb57c]868        self.context_menu.addSeparator()
869        self.context_menu.addAction(self.actionDelete)
870
[4b71e91]871
872        # Define the callbacks
873        self.actionDataInfo.triggered.connect(self.showDataInfo)
874        self.actionSaveAs.triggered.connect(self.saveDataAs)
875        self.actionQuickPlot.triggered.connect(self.quickDataPlot)
876        self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
877        self.actionEditMask.triggered.connect(self.showEditDataMask)
[c6fb57c]878        self.actionDelete.triggered.connect(self.deleteItem)
[e540cd2]879
880    def onCustomContextMenu(self, position):
881        """
[4b71e91]882        Show the right-click context menu in the data treeview
[e540cd2]883        """
[cbcdd2c]884        index = self.current_view.indexAt(position)
885        proxy = self.current_view.model()
886        model = proxy.sourceModel()
887
[e540cd2]888        if index.isValid():
[cbcdd2c]889            model_item = model.itemFromIndex(proxy.mapToSource(index))
[4b71e91]890            # Find the mapped index
891            orig_index = model_item.isCheckable()
892            if orig_index:
893                # Check the data to enable/disable actions
[8548d739]894                is_2D = isinstance(GuiUtils.dataFromItem(model_item), Data2D)
[4b71e91]895                self.actionQuick3DPlot.setEnabled(is_2D)
896                self.actionEditMask.setEnabled(is_2D)
897                # Fire up the menu
[cbcdd2c]898                self.context_menu.exec_(self.current_view.mapToGlobal(position))
[4b71e91]899
900    def showDataInfo(self):
901        """
902        Show a simple read-only text edit with data information.
903        """
[cbcdd2c]904        index = self.current_view.selectedIndexes()[0]
905        proxy = self.current_view.model()
906        model = proxy.sourceModel()
907        model_item = model.itemFromIndex(proxy.mapToSource(index))
908
[8548d739]909        data = GuiUtils.dataFromItem(model_item)
[28a84e9]910        if isinstance(data, Data1D):
[4b71e91]911            text_to_show = GuiUtils.retrieveData1d(data)
[28a84e9]912            # Hardcoded sizes to enable full width rendering with default font
[4b71e91]913            self.txt_widget.resize(420,600)
914        else:
915            text_to_show = GuiUtils.retrieveData2d(data)
[28a84e9]916            # Hardcoded sizes to enable full width rendering with default font
[4b71e91]917            self.txt_widget.resize(700,600)
918
919        self.txt_widget.setReadOnly(True)
920        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
921        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
922        self.txt_widget.setWindowTitle("Data Info: %s" % data.filename)
[d5c5d3d]923        self.txt_widget.clear()
[4b71e91]924        self.txt_widget.insertPlainText(text_to_show)
925
926        self.txt_widget.show()
[28a84e9]927        # Move the slider all the way up, if present
[4b71e91]928        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
[7969b9c]929        vertical_scroll_bar.triggerAction(QtWidgets.QScrollBar.SliderToMinimum)
[4b71e91]930
931    def saveDataAs(self):
932        """
[28a84e9]933        Save the data points as either txt or xml
[4b71e91]934        """
[cbcdd2c]935        index = self.current_view.selectedIndexes()[0]
936        proxy = self.current_view.model()
937        model = proxy.sourceModel()
938        model_item = model.itemFromIndex(proxy.mapToSource(index))
939
[8548d739]940        data = GuiUtils.dataFromItem(model_item)
[28a84e9]941        if isinstance(data, Data1D):
942            GuiUtils.saveData1D(data)
943        else:
944            GuiUtils.saveData2D(data)
[4b71e91]945
946    def quickDataPlot(self):
[1af348e]947        """
948        Frozen plot - display an image of the plot
949        """
[cbcdd2c]950        index = self.current_view.selectedIndexes()[0]
951        proxy = self.current_view.model()
952        model = proxy.sourceModel()
953        model_item = model.itemFromIndex(proxy.mapToSource(index))
954
[8548d739]955        data = GuiUtils.dataFromItem(model_item)
[39551a68]956
[ef01be4]957        method_name = 'Plotter'
958        if isinstance(data, Data2D):
959            method_name='Plotter2D'
[1af348e]960
[ef01be4]961        new_plot = globals()[method_name](self, quickplot=True)
962        new_plot.data = data
[5236449]963        #new_plot.plot(marker='o')
964        new_plot.plot()
[39551a68]965
966        # Update the global plot counter
967        title = "Plot " + data.name
968        new_plot.setWindowTitle(title)
969
970        # Show the plot
971        new_plot.show()
[4b71e91]972
973    def quickData3DPlot(self):
974        """
[55d89f8]975        Slowish 3D plot
[4b71e91]976        """
[cbcdd2c]977        index = self.current_view.selectedIndexes()[0]
978        proxy = self.current_view.model()
979        model = proxy.sourceModel()
980        model_item = model.itemFromIndex(proxy.mapToSource(index))
981
[55d89f8]982        data = GuiUtils.dataFromItem(model_item)
983
984        new_plot = Plotter2D(self, quickplot=True, dimension=3)
985        new_plot.data = data
[965fbd8]986        new_plot.plot()
[55d89f8]987
988        # Update the global plot counter
989        title = "Plot " + data.name
990        new_plot.setWindowTitle(title)
991
992        # Show the plot
993        new_plot.show()
[4b71e91]994
[e20870bc]995    def showEditDataMask(self, data=None):
[4b71e91]996        """
[cad617b]997        Mask Editor for 2D plots
[4b71e91]998        """
[e20870bc]999        if data is None or not isinstance(data, Data2D):
1000            index = self.current_view.selectedIndexes()[0]
1001            proxy = self.current_view.model()
1002            model = proxy.sourceModel()
1003            model_item = model.itemFromIndex(proxy.mapToSource(index))
[cbcdd2c]1004
[e20870bc]1005            data = GuiUtils.dataFromItem(model_item)
[416fa8f]1006
1007        mask_editor = MaskEditor(self, data)
[cad617b]1008        # Modal dialog here.
[416fa8f]1009        mask_editor.exec_()
[f721030]1010
[c6fb57c]1011    def deleteItem(self):
1012        """
1013        Delete the current item
1014        """
1015        # Assure this is indeed wanted
[b1a7a81]1016        delete_msg = "This operation will delete the selected data sets " +\
1017                     "and all the dependents." +\
[c6fb57c]1018                     "\nDo you want to continue?"
1019        reply = QtWidgets.QMessageBox.question(self,
1020                                           'Warning',
1021                                           delete_msg,
1022                                           QtWidgets.QMessageBox.Yes,
1023                                           QtWidgets.QMessageBox.No)
1024
1025        if reply == QtWidgets.QMessageBox.No:
1026            return
1027
1028        proxy = self.current_view.model()
1029        model = proxy.sourceModel()
1030
[515c23df]1031        deleted_items = []
[b1a7a81]1032        deleted_names = []
1033
1034        # Every time a row is removed, the indices change, so we'll just remove
1035        # rows and keep calling selectedIndexes until it returns an empty list.
1036        indices = self.current_view.selectedIndexes()
1037
1038        while len(indices) > 0:
1039            index = indices[0]
[c6fb57c]1040            row_index = proxy.mapToSource(index)
1041            item_to_delete = model.itemFromIndex(row_index)
[cb4d219]1042            if item_to_delete and item_to_delete.isCheckable():
[c6fb57c]1043                row = row_index.row()
[b1a7a81]1044
1045                # store the deleted item details so we can pass them on later
[515c23df]1046                deleted_names.append(item_to_delete.text())
1047                deleted_items.append(item_to_delete)
[b1a7a81]1048
[c6fb57c]1049                if item_to_delete.parent():
1050                    # We have a child item - delete from it
1051                    item_to_delete.parent().removeRow(row)
1052                else:
1053                    # delete directly from model
1054                    model.removeRow(row)
[b1a7a81]1055            indices = self.current_view.selectedIndexes()
1056
1057        # Let others know we deleted data
[515c23df]1058        self.communicator.dataDeletedSignal.emit(deleted_items)
[b1a7a81]1059
1060        # update stored_data
1061        self.manager.update_stored_data(deleted_names)
[c6fb57c]1062
[8ac3551]1063    def onAnalysisUpdate(self, new_perspective=""):
1064        """
1065        Update the perspective combo index based on passed string
1066        """
1067        assert new_perspective in Perspectives.PERSPECTIVES.keys()
1068        self.cbFitting.blockSignals(True)
1069        self.cbFitting.setCurrentIndex(self.cbFitting.findText(new_perspective))
1070        self.cbFitting.blockSignals(False)
1071        pass
1072
[a281ab8]1073    def loadComplete(self, output):
[f721030]1074        """
1075        Post message to status bar and update the data manager
1076        """
[8cb6cd6]1077        assert isinstance(output, tuple)
[e540cd2]1078
[9e426c1]1079        # Reset the model so the view gets updated.
[7969b9c]1080        #self.model.reset()
[e540cd2]1081        self.communicator.progressBarUpdateSignal.emit(-1)
[a281ab8]1082
1083        output_data = output[0]
1084        message = output[1]
[f721030]1085        # Notify the manager of the new data available
[f82ab8c]1086        self.communicator.statusBarUpdateSignal.emit(message)
1087        self.communicator.fileDataReceivedSignal.emit(output_data)
[a281ab8]1088        self.manager.add_data(data_list=output_data)
[f721030]1089
[7969b9c]1090    def loadFailed(self, reason):
[7fb471d]1091        print("File Load Failed with:\n", reason)
1092        pass
1093
[f721030]1094    def updateModel(self, data, p_file):
1095        """
[481ff26]1096        Add data and Info fields to the model item
[f721030]1097        """
1098        # Structure of the model
1099        # checkbox + basename
[481ff26]1100        #     |-------> Data.D object
[f721030]1101        #     |-------> Info
1102        #                 |----> Title:
1103        #                 |----> Run:
1104        #                 |----> Type:
1105        #                 |----> Path:
1106        #                 |----> Process
1107        #                          |-----> process[0].name
[28a84e9]1108        #     |-------> THEORIES
[f721030]1109
1110        # Top-level item: checkbox with label
[6a3e1fe]1111        checkbox_item = GuiUtils.HashableStandardItem()
[f721030]1112        checkbox_item.setCheckable(True)
1113        checkbox_item.setCheckState(QtCore.Qt.Checked)
1114        checkbox_item.setText(os.path.basename(p_file))
1115
1116        # Add the actual Data1D/Data2D object
[6a3e1fe]1117        object_item = GuiUtils.HashableStandardItem()
[b3e8629]1118        object_item.setData(data)
[f721030]1119
[488c49d]1120        checkbox_item.setChild(0, object_item)
1121
[f721030]1122        # Add rows for display in the view
[0cd8612]1123        info_item = GuiUtils.infoFromData(data)
[f721030]1124
[28a84e9]1125        # Set info_item as the first child
[488c49d]1126        checkbox_item.setChild(1, info_item)
[f721030]1127
[28a84e9]1128        # Caption for the theories
1129        checkbox_item.setChild(2, QtGui.QStandardItem("THEORIES"))
1130
[f721030]1131        # New row in the model
[7969b9c]1132        self.model.beginResetModel()
[f721030]1133        self.model.appendRow(checkbox_item)
[7969b9c]1134        self.model.endResetModel()
[481ff26]1135
[5032ea68]1136    def updateModelFromPerspective(self, model_item):
1137        """
[a281ab8]1138        Receive an update model item from a perspective
1139        Make sure it is valid and if so, replace it in the model
[5032ea68]1140        """
[a281ab8]1141        # Assert the correct type
[0cd8612]1142        if not isinstance(model_item, QtGui.QStandardItem):
[5032ea68]1143            msg = "Wrong data type returned from calculations."
[b3e8629]1144            raise AttributeError(msg)
[a281ab8]1145
[1042dba]1146        # TODO: Assert other properties
[a281ab8]1147
[5032ea68]1148        # Reset the view
[7969b9c]1149        ##self.model.reset()
[5032ea68]1150        # Pass acting as a debugger anchor
1151        pass
[481ff26]1152
[5236449]1153    def updateTheoryFromPerspective(self, model_item):
1154        """
1155        Receive an update theory item from a perspective
1156        Make sure it is valid and if so, replace/add in the model
1157        """
1158        # Assert the correct type
1159        if not isinstance(model_item, QtGui.QStandardItem):
1160            msg = "Wrong data type returned from calculations."
[b3e8629]1161            raise AttributeError(msg)
[5236449]1162
1163        # Check if there are any other items for this tab
1164        # If so, delete them
[d6e38661]1165        current_tab_name = model_item.text()
1166        for current_index in range(self.theory_model.rowCount()):
[d6b8a1d]1167            #if current_tab_name in self.theory_model.item(current_index).text():
[d6e38661]1168            if current_tab_name == self.theory_model.item(current_index).text():
1169                return
1170                self.theory_model.removeRow(current_index)
1171                break
[d6b8a1d]1172
[d6e38661]1173        # send in the new item
[5236449]1174        self.theory_model.appendRow(model_item)
1175
[f721030]1176
1177if __name__ == "__main__":
[7969b9c]1178    app = QtWidgets.QApplication([])
[f721030]1179    dlg = DataExplorerWindow()
1180    dlg.show()
[481ff26]1181    sys.exit(app.exec_())
Note: See TracBrowser for help on using the repository browser.