source: sasview/src/sas/qtgui/DataExplorer.py @ a95260d

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 a95260d was 0cd8612, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 8 years ago

output console + logging

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