source: sasview/src/sas/qtgui/DataExplorer.py @ 481ff26

ESS_GUIESS_GUI_DocsESS_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 481ff26 was 481ff26, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 8 years ago

Modified Data Explorer slightly

  • Property mode set to 100755
File size: 20.7 KB
Line 
1# global
2import sys
3import os
4import logging
5
6from PyQt4 import QtCore
7from PyQt4 import QtGui
8from PyQt4 import QtWebKit
9from PyQt4.Qt import QMutex
10
11from twisted.internet import threads
12
13# SAS
14from GuiUtils import *
15from Plotter import Plotter
16from sas.sascalc.dataloader.loader import Loader
17from sas.sasgui.guiframe.data_manager import DataManager
18
19from DroppableDataLoadWidget import DroppableDataLoadWidget
20
21# This is how to get data1/2D from the model item
22# data = [selected_items[0].child(0).data().toPyObject()]
23
24class DataExplorerWindow(DroppableDataLoadWidget):
25    # The controller which is responsible for managing signal slots connections
26    # for the gui and providing an interface to the data model.
27
28    def __init__(self, parent=None, guimanager=None):
29        super(DataExplorerWindow, self).__init__(parent, guimanager)
30
31        # Main model for keeping loaded data
32        self.model = QtGui.QStandardItemModel(self)
33
34        # Secondary model for keeping frozen data sets
35        self.theory_model = QtGui.QStandardItemModel(self)
36
37        # GuiManager is the actual parent, but we needed to also pass the QMainWindow
38        # in order to set the widget parentage properly.
39        self.parent = guimanager
40        self.loader = Loader()
41        self.manager = DataManager()
42
43        # Be careful with twisted threads.
44        self.mutex = QMutex()
45
46        # Connect the buttons
47        self.cmdLoad.clicked.connect(self.loadFile)
48        self.cmdDeleteData.clicked.connect(self.deleteFile)
49        self.cmdDeleteTheory.clicked.connect(self.deleteTheory)
50        self.cmdFreeze.clicked.connect(self.freezeTheory)
51        self.cmdSendTo.clicked.connect(self.sendData)
52        self.cmdNew.clicked.connect(self.newPlot)
53        self.cmdHelp.clicked.connect(self.displayHelp)
54        self.cmdHelp_2.clicked.connect(self.displayHelp)
55
56        # Display HTML content
57        self._helpView = QtWebKit.QWebView()
58
59        # Connect the comboboxes
60        self.cbSelect.currentIndexChanged.connect(self.selectData)
61
62        #self.closeEvent.connect(self.closeEvent)
63        # self.aboutToQuit.connect(self.closeEvent)
64
65        self.communicator.fileReadSignal.connect(self.loadFromURL)
66
67        # Proxy model for showing a subset of Data1D/Data2D content
68        self.data_proxy = QtGui.QSortFilterProxyModel(self)
69        self.data_proxy.setSourceModel(self.model)
70
71        # Don't show "empty" rows with data objects
72        self.data_proxy.setFilterRegExp(r"[^()]")
73
74        # The Data viewer is QTreeView showing the proxy model
75        self.treeView.setModel(self.data_proxy)
76
77        # Proxy model for showing a subset of Theory content
78        self.theory_proxy = QtGui.QSortFilterProxyModel(self)
79        self.theory_proxy.setSourceModel(self.theory_model)
80
81        # Don't show "empty" rows with data objects
82        self.theory_proxy.setFilterRegExp(r"[^()]")
83
84        # Theory model view
85        self.freezeView.setModel(self.theory_proxy)
86
87    def closeEvent(self, event):
88        """
89        Overwrite the close event - no close!
90        """
91        event.ignore()
92
93    def displayHelp(self):
94        """
95        Show the "Loading data" section of help
96        """
97        _TreeLocation = "html/user/sasgui/guiframe/data_explorer_help.html"
98        self._helpView.load(QtCore.QUrl(_TreeLocation))
99        self._helpView.show()
100
101    def loadFromURL(self, url):
102        """
103        Threaded file load
104        """
105        load_thread = threads.deferToThread(self.readData, url)
106        load_thread.addCallback(self.loadComplete)
107
108    def loadFile(self, event=None):
109        """
110        Called when the "Load" button pressed.
111        Opens the Qt "Open File..." dialog
112        """
113        path_str = self.chooseFiles()
114        if not path_str:
115            return
116        self.loadFromURL(path_str)
117
118    def loadFolder(self, event=None):
119        """
120        Called when the "File/Load Folder" menu item chosen.
121        Opens the Qt "Open Folder..." dialog
122        """
123        folder = QtGui.QFileDialog.getExistingDirectory(self, "Choose a directory", "",
124              QtGui.QFileDialog.ShowDirsOnly | QtGui.QFileDialog.DontUseNativeDialog)
125        if folder is None:
126            return
127
128        folder = str(folder)
129
130        if not os.path.isdir(folder):
131            return
132
133        # get content of dir into a list
134        path_str = [os.path.join(os.path.abspath(folder), filename)
135                        for filename in os.listdir(folder)]
136
137        self.loadFromURL(path_str)
138
139    def deleteFile(self, event):
140        """
141        Delete selected rows from the model
142        """
143        # Assure this is indeed wanted
144        delete_msg = "This operation will delete the checked data sets and all the dependents." +\
145                     "\nDo you want to continue?"
146        reply = QtGui.QMessageBox.question(self, 'Warning', delete_msg,
147                QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
148
149        if reply == QtGui.QMessageBox.No:
150            return
151
152        # Figure out which rows are checked
153        ind = -1
154        # Use 'while' so the row count is forced at every iteration
155        while ind < self.model.rowCount():
156            ind += 1
157            item = self.model.item(ind)
158            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
159                # Delete these rows from the model
160                self.model.removeRow(ind)
161                # Decrement index since we just deleted it
162                ind -= 1
163
164        # pass temporarily kept as a breakpoint anchor
165        pass
166
167    def deleteTheory(self, event):
168        """
169        Delete selected rows from the theory model
170        """
171        # Assure this is indeed wanted
172        delete_msg = "This operation will delete the checked data sets and all the dependents." +\
173                     "\nDo you want to continue?"
174        reply = QtGui.QMessageBox.question(self, 'Warning', delete_msg,
175                QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
176
177        if reply == QtGui.QMessageBox.No:
178            return
179
180        # Figure out which rows are checked
181        ind = -1
182        # Use 'while' so the row count is forced at every iteration
183        while ind < self.theory_model.rowCount():
184            ind += 1
185            item = self.theory_model.item(ind)
186            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
187                # Delete these rows from the model
188                self.theory_model.removeRow(ind)
189                # Decrement index since we just deleted it
190                ind -= 1
191
192        # pass temporarily kept as a breakpoint anchor
193        pass
194
195    def sendData(self, event):
196        """
197        Send selected item data to the current perspective and set the relevant notifiers
198        """
199        # should this reside on GuiManager or here?
200        self._perspective = self.parent.perspective()
201
202        # Set the signal handlers
203        self.communicator = self._perspective.communicator()
204        self.communicator.updateModelFromPerspectiveSignal.connect(self.updateModelFromPerspective)
205
206        # Figure out which rows are checked
207        selected_items = []
208        for index in range(self.model.rowCount()):
209            item = self.model.item(index)
210            if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
211                selected_items.append(item)
212
213        if len(selected_items) < 1:
214            return
215
216        # Which perspective has been selected?
217        if len(selected_items) > 1 and not self._perspective.allowBatch():
218            msg = self._perspective.title() + " does not allow multiple data."
219            msgbox = QtGui.QMessageBox()
220            msgbox.setIcon(QtGui.QMessageBox.Critical)
221            msgbox.setText(msg)
222            msgbox.setStandardButtons(QtGui.QMessageBox.Ok)
223            retval = msgbox.exec_()
224            return
225
226        # Dig up the item
227        data = selected_items
228
229        # TODO
230        # New plot or appended?
231
232        # Notify the GuiManager about the send request
233        self._perspective.setData(data_item=data)
234
235    def freezeTheory(self, event):
236        """
237        Freeze selected theory rows.
238
239        "Freezing" means taking the plottable data from the filename item
240        and copying it to a separate top-level item.
241        """
242        # Figure out which _inner_ rows are checked
243        # Use 'while' so the row count is forced at every iteration
244        outer_index = -1
245        theories_copied = 0
246        while outer_index < self.model.rowCount():
247            outer_index += 1
248            outer_item = self.model.item(outer_index)
249            if not outer_item:
250                continue
251            for inner_index in xrange(outer_item.rowCount()): # Should be just two rows: data and Info
252                subitem = outer_item.child(inner_index)
253                if subitem and subitem.isCheckable() and subitem.checkState() == QtCore.Qt.Checked:
254                    theories_copied += 1
255                    new_item = self.recursivelyCloneItem(subitem)
256                    self.theory_model.appendRow(new_item)
257            self.theory_model.reset()
258
259        freeze_msg = ""
260        if theories_copied == 0:
261            return
262        elif theories_copied == 1:
263            freeze_msg = "1 theory sent to Theory tab"
264        elif theories_copied > 1:
265            freeze_msg = "%i theories sent to Theory tab" % theories_copied
266        else:
267            freeze_msg = "Unexpected number of theories copied: %i" % theories_copied
268            raise AttributeError, freeze_msg
269        self.communicator.statusBarUpdateSignal.emit(freeze_msg)
270        # Actively switch tabs
271        self.setCurrentIndex(1)
272
273    def recursivelyCloneItem(self, item):
274        """
275        Clone QStandardItem() object
276        """
277        new_item = item.clone()
278        # clone doesn't do deepcopy :(
279        for child_index in xrange(item.rowCount()):
280            child_item = self.recursivelyCloneItem(item.child(child_index))
281            new_item.setChild(child_index, child_item)
282        return new_item
283
284    def newPlot(self):
285        """
286        Create a new matplotlib chart from selected data
287
288        TODO: Add 2D-functionality
289        """
290
291        plots = plotsFromCheckedItems(self.model)
292
293        # Call show on requested plots
294        new_plot = Plotter()
295        for plot_set in plots:
296            new_plot.data(plot_set)
297            new_plot.plot()
298
299        new_plot.show()
300
301    def chooseFiles(self):
302        """
303        Shows the Open file dialog and returns the chosen path(s)
304        """
305        # List of known extensions
306        wlist = self.getWlist()
307
308        # Location is automatically saved - no need to keep track of the last dir
309        # But only with Qt built-in dialog (non-platform native)
310        paths = QtGui.QFileDialog.getOpenFileNames(self, "Choose a file", "",
311                wlist, None, QtGui.QFileDialog.DontUseNativeDialog)
312        if paths is None:
313            return
314
315        #if type(paths) == QtCore.QStringList:
316        if isinstance(paths, QtCore.QStringList):
317            paths = [str(f) for f in paths]
318
319        if paths.__class__.__name__ != "list":
320            paths = [paths]
321
322        return paths
323
324    def readData(self, path):
325        """
326        verbatim copy-paste from
327           sasgui.guiframe.local_perspectives.data_loader.data_loader.py
328        slightly modified for clarity
329        """
330        message = ""
331        log_msg = ''
332        output = {}
333        any_error = False
334        data_error = False
335        error_message = ""
336
337        for p_file in path:
338            basename = os.path.basename(p_file)
339            _, extension = os.path.splitext(basename)
340            if extension.lower() in EXTENSIONS:
341                any_error = True
342                log_msg = "Data Loader cannot "
343                log_msg += "load: %s\n" % str(p_file)
344                log_msg += """Please try to open that file from "open project" """
345                log_msg += """or "open analysis" menu\n"""
346                error_message = log_msg + "\n"
347                logging.info(log_msg)
348                continue
349
350            try:
351                message = "Loading Data... " + str(basename) + "\n"
352
353                # change this to signal notification in GuiManager
354                self.communicator.statusBarUpdateSignal.emit(message)
355
356                output_objects = self.loader.load(p_file)
357
358                # Some loaders return a list and some just a single Data1D object.
359                # Standardize.
360                if not isinstance(output_objects, list):
361                    output_objects = [output_objects]
362
363                for item in output_objects:
364                    # cast sascalc.dataloader.data_info.Data1D into
365                    # sasgui.guiframe.dataFitting.Data1D
366                    # TODO : Fix it
367                    new_data = self.manager.create_gui_data(item, p_file)
368                    output[new_data.id] = new_data
369
370                    # Model update should be protected
371                    self.mutex.lock()
372                    self.updateModel(new_data, p_file)
373                    self.model.reset()
374                    QtGui.qApp.processEvents()
375                    self.mutex.unlock()
376
377                    if hasattr(item, 'errors'):
378                        for error_data in item.errors:
379                            data_error = True
380                            message += "\tError: {0}\n".format(error_data)
381                    else:
382
383                        logging.error("Loader returned an invalid object:\n %s" % str(item))
384                        data_error = True
385
386            except Exception as ex:
387                logging.error(sys.exc_value)
388
389                any_error = True
390            if any_error or error_message != "":
391                if error_message == "":
392                    error = "Error: " + str(sys.exc_info()[1]) + "\n"
393                    error += "while loading Data: \n%s\n" % str(basename)
394                    error_message += "The data file you selected could not be loaded.\n"
395                    error_message += "Make sure the content of your file"
396                    error_message += " is properly formatted.\n\n"
397                    error_message += "When contacting the SasView team, mention the"
398                    error_message += " following:\n%s" % str(error)
399                elif data_error:
400                    base_message = "Errors occurred while loading "
401                    base_message += "{0}\n".format(basename)
402                    base_message += "The data file loaded but with errors.\n"
403                    error_message = base_message + error_message
404                else:
405                    error_message += "%s\n" % str(p_file)
406                info = "error"
407
408        if any_error or error_message:
409            self.communicator.statusBarUpdateSignal.emit(error_message)
410
411        else:
412            message = "Loading Data Complete! "
413        message += log_msg
414
415        return output, message
416
417    def getWlist(self):
418        """
419        Wildcards of files we know the format of.
420        """
421        # Display the Qt Load File module
422        cards = self.loader.get_wildcards()
423
424        # get rid of the wx remnant in wildcards
425        # TODO: modify sasview loader get_wildcards method, after merge,
426        # so this kludge can be avoided
427        new_cards = []
428        for item in cards:
429            new_cards.append(item[:item.find("|")])
430        wlist = ';;'.join(new_cards)
431
432        return wlist
433
434    def selectData(self, index):
435        """
436        Callback method for modifying the TreeView on Selection Options change
437        """
438        if not isinstance(index, int):
439            msg = "Incorrect type passed to DataExplorer.selectData()"
440            raise AttributeError, msg
441
442        # Respond appropriately
443        if index == 0:
444            # Select All
445            for index in range(self.model.rowCount()):
446                item = self.model.item(index)
447                if item.isCheckable() and item.checkState() == QtCore.Qt.Unchecked:
448                    item.setCheckState(QtCore.Qt.Checked)
449        elif index == 1:
450            # De-select All
451            for index in range(self.model.rowCount()):
452                item = self.model.item(index)
453                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
454                    item.setCheckState(QtCore.Qt.Unchecked)
455
456        elif index == 2:
457            # Select All 1-D
458            for index in range(self.model.rowCount()):
459                item = self.model.item(index)
460                item.setCheckState(QtCore.Qt.Unchecked)
461
462                try:
463                    is1D = item.child(0).data().toPyObject().__class__.__name__ == 'Data1D'
464                except AttributeError:
465                    msg = "Bad structure of the data model."
466                    raise RuntimeError, msg
467
468                if is1D:
469                    item.setCheckState(QtCore.Qt.Checked)
470
471        elif index == 3:
472            # Unselect All 1-D
473            for index in range(self.model.rowCount()):
474                item = self.model.item(index)
475
476                try:
477                    is1D = item.child(0).data().toPyObject().__class__.__name__ == 'Data1D'
478                except AttributeError:
479                    msg = "Bad structure of the data model."
480                    raise RuntimeError, msg
481
482                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is1D:
483                    item.setCheckState(QtCore.Qt.Unchecked)
484
485        elif index == 4:
486            # Select All 2-D
487            for index in range(self.model.rowCount()):
488                item = self.model.item(index)
489                item.setCheckState(QtCore.Qt.Unchecked)
490                try:
491                    is2D = item.child(0).data().toPyObject().__class__.__name__ == 'Data2D'
492                except AttributeError:
493                    msg = "Bad structure of the data model."
494                    raise RuntimeError, msg
495
496                if is2D:
497                    item.setCheckState(QtCore.Qt.Checked)
498
499        elif index == 5:
500            # Unselect All 2-D
501            for index in range(self.model.rowCount()):
502                item = self.model.item(index)
503
504                try:
505                    is2D = item.child(0).data().toPyObject().__class__.__name__ == 'Data2D'
506                except AttributeError:
507                    msg = "Bad structure of the data model."
508                    raise RuntimeError, msg
509
510                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is2D:
511                    item.setCheckState(QtCore.Qt.Unchecked)
512
513        else:
514            msg = "Incorrect value in the Selection Option"
515            # Change this to a proper logging action
516            raise Exception, msg
517
518
519    def loadComplete(self, output):
520        """
521        Post message to status bar and update the data manager
522        """
523        # Reset the model so the view gets updated.
524        self.model.reset()
525        assert type(output) == tuple
526
527        output_data = output[0]
528        message = output[1]
529        # Notify the manager of the new data available
530        self.communicator.statusBarUpdateSignal.emit(message)
531        self.communicator.fileDataReceivedSignal.emit(output_data)
532        self.manager.add_data(data_list=output_data)
533
534    def updateModel(self, data, p_file):
535        """
536        Add data and Info fields to the model item
537        """
538        # Structure of the model
539        # checkbox + basename
540        #     |-------> Data.D object
541        #     |-------> Info
542        #                 |----> Title:
543        #                 |----> Run:
544        #                 |----> Type:
545        #                 |----> Path:
546        #                 |----> Process
547        #                          |-----> process[0].name
548        #
549
550        # Top-level item: checkbox with label
551        checkbox_item = QtGui.QStandardItem(True)
552        checkbox_item.setCheckable(True)
553        checkbox_item.setCheckState(QtCore.Qt.Checked)
554        checkbox_item.setText(os.path.basename(p_file))
555
556        # Add the actual Data1D/Data2D object
557        object_item = QtGui.QStandardItem()
558        object_item.setData(QtCore.QVariant(data))
559
560        checkbox_item.setChild(0, object_item)
561
562        # Add rows for display in the view
563        info_item = infoFromData(data)
564
565        # Set info_item as the only child
566        checkbox_item.setChild(1, info_item)
567
568        # New row in the model
569        self.model.appendRow(checkbox_item)
570
571    def updateModelFromPerspective(self, model_item):
572        """
573        Receive an update model item from a perspective
574        Make sure it is valid and if so, replace it in the model
575        """
576        # Assert the correct type
577        if type(model_item) != QtGui.QStandardItem:
578            msg = "Wrong data type returned from calculations."
579            raise AttributeError, msg
580
581        # TODO: Assert other properties
582
583        # Reset the view
584        self.model.reset()
585
586        # Pass acting as a debugger anchor
587        pass
588
589
590if __name__ == "__main__":
591    app = QtGui.QApplication([])
592    dlg = DataExplorerWindow()
593    dlg.show()
594    sys.exit(app.exec_())
Note: See TracBrowser for help on using the repository browser.