source: sasview/src/sas/qtgui/DataExplorer.py @ 5032ea68

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

threaded file load, data object related fixes, more unit tests.

  • Property mode set to 100755
File size: 17.1 KB
Line 
1# global
2import sys
3import os
4import logging
5
6from PyQt4 import QtCore
7from PyQt4 import QtGui
8from PyQt4 import QtWebKit
9from twisted.internet import threads
10
11# SAS
12from GuiUtils import *
13from sas.sascalc.dataloader.loader import Loader
14from sas.sasgui.guiframe.data_manager import DataManager
15
16# UI
17from UI.TabbedFileLoadUI import DataLoadWidget
18
19class DataExplorerWindow(DataLoadWidget):
20    # The controller which is responsible for managing signal slots connections
21    # for the gui and providing an interface to the data model.
22
23    def __init__(self, parent=None, guimanager=None):
24        super(DataExplorerWindow, self).__init__(parent)
25
26        # Main model for keeping loaded data
27        self.model = QtGui.QStandardItemModel(self)
28        self._default_save_location = None
29
30        # GuiManager is the actual parent, but we needed to also pass the QMainWindow
31        # in order to set the widget parentage properly.
32        self.parent = guimanager
33        self.loader = Loader()
34        self.manager = DataManager()
35
36        # Connect the buttons
37        self.cmdLoad.clicked.connect(self.loadFile)
38        self.cmdDelete.clicked.connect(self.deleteFile)
39        self.cmdSendTo.clicked.connect(self.sendData)
40
41        # Connect the comboboxes
42        self.cbSelect.currentIndexChanged.connect(self.selectData)
43
44        # Communicator for signal definitions
45        self.communicate = self.parent.communicator()
46
47        # Proxy model for showing a subset of Data1D/Data2D content
48        self.proxy = QtGui.QSortFilterProxyModel(self)
49        self.proxy.setSourceModel(self.model)
50
51        # The Data viewer is QTreeView showing the proxy model
52        self.treeView.setModel(self.proxy)
53
54
55    def loadFile(self, event=None):
56        """
57        Called when the "Load" button pressed.
58        Opens the Qt "Open File..." dialog
59        """
60        path_str = self.chooseFiles()
61        if not path_str:
62            return
63
64        # Notify the manager of the new data available
65        self.communicate.fileReadSignal.emit(path_str)
66
67        # threaded file load
68        load_thread = threads.deferToThread(self.readData, path_str)
69        load_thread.addCallback(self.loadComplete)
70
71        return
72
73    def loadFolder(self, event=None):
74        """
75        Called when the "File/Load Folder" menu item chosen.
76        Opens the Qt "Open Folder..." dialog
77        """
78        dir = QtGui.QFileDialog.getExistingDirectory(self, "Choose a directory", "",
79              QtGui.QFileDialog.ShowDirsOnly)
80        if dir is None:
81            return
82
83        dir = str(dir)
84
85        if not os.path.isdir(dir):
86            return
87
88        # get content of dir into a list
89        path_str = [os.path.join(os.path.abspath(dir), filename) for filename in os.listdir(dir)]
90
91        # threaded file load
92        load_thread = threads.deferToThread(self.readData, path_str)
93        load_thread.addCallback(self.loadComplete)
94       
95        return
96
97    def deleteFile(self, event):
98        """
99        Delete selected rows from the model
100        """
101        # Assure this is indeed wanted
102        delete_msg = "This operation will delete the checked data sets and all the dependents." +\
103                     "\nDo you want to continue?"
104        reply = QtGui.QMessageBox.question(self, 'Warning', delete_msg,
105                QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
106
107        if reply == QtGui.QMessageBox.No:
108            return
109
110        # Figure out which rows are checked
111        ind = -1
112        # Use 'while' so the row count is forced at every iteration
113        while ind < self.model.rowCount():
114            ind += 1
115            item = self.model.item(ind)
116            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
117                # Delete these rows from the model
118                self.model.removeRow(ind)
119                # Decrement index since we just deleted it
120                ind -= 1
121
122        # pass temporarily kept as a breakpoint anchor
123        pass
124
125    def sendData(self, event):
126        """
127        Send selected item data to the current perspective and set the relevant notifiers
128        """
129        # should this reside on GuiManager or here?
130        self._perspective = self.parent.perspective()
131
132        # Set the signal handlers
133        self.communicator = self._perspective.communicator()
134        self.communicator.updateModelFromPerspectiveSignal.connect(self.updateModelFromPerspective)
135
136        # Figure out which rows are checked
137        selected_items = []
138        for index in range(self.model.rowCount()):
139            item = self.model.item(index)
140            if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
141                selected_items.append(item)
142
143        # Which perspective has been selected?
144        if len(selected_items) > 1 and not self._perspective.allowBatch():
145            msg = self._perspective.title() + " does not allow multiple data."
146            msgbox = QtGui.QMessageBox()
147            msgbox.setIcon(QtGui.QMessageBox.Critical)
148            msgbox.setText(msg)
149            msgbox.setStandardButtons(QtGui.QMessageBox.Ok)
150            retval = msgbox.exec_()
151            return
152        # Dig up data from model
153        data = [selected_items[0].child(0).data().toPyObject()]
154
155        # TODO
156        # New plot or appended?
157
158        # Notify the GuiManager about the send request
159        self._perspective.setData(data_list=data)
160
161
162    def chooseFiles(self):
163        """
164        Shows the Open file dialog and returns the chosen path(s)
165        """
166        # List of known extensions
167        wlist = self.getWlist()
168
169        # Location is automatically saved - no need to keep track of the last dir
170        # But only with Qt built-in dialog (non-platform native)
171        paths = QtGui.QFileDialog.getOpenFileName(self, "Choose a file", "",
172                wlist, None, QtGui.QFileDialog.DontUseNativeDialog)
173        if paths is None:
174            return
175
176        if paths.__class__.__name__ != "list":
177            paths = [paths]
178
179        path_str=[]
180        for path in paths:
181            if str(path):
182                path_str.append(str(path))
183
184        return path_str
185
186    def readData(self, path):
187        """
188        verbatim copy/paste from
189            sasgui\guiframe\local_perspectives\data_loader\data_loader.py
190        slightly modified for clarity
191        """
192        message = ""
193        log_msg = ''
194        output = {}
195        any_error = False
196        data_error = False
197        error_message = ""
198       
199        for p_file in path:
200            info = "info"
201            basename = os.path.basename(p_file)
202            _, extension = os.path.splitext(basename)
203            if extension.lower() in EXTENSIONS:
204                any_error = True
205                log_msg = "Data Loader cannot "
206                log_msg += "load: %s\n" % str(p_file)
207                log_msg += """Please try to open that file from "open project" """
208                log_msg += """or "open analysis" menu\n"""
209                error_message = log_msg + "\n"
210                logging.info(log_msg)
211                continue
212
213            try:
214                message = "Loading Data... " + str(basename) + "\n"
215
216                # change this to signal notification in GuiManager
217                self.communicate.statusBarUpdateSignal.emit(message)
218
219                output_objects = self.loader.load(p_file)
220
221                # Some loaders return a list and some just a single Data1D object.
222                # Standardize.
223                if not isinstance(output_objects, list):
224                    output_objects = [output_objects]
225
226                for item in output_objects:
227                    # cast sascalc.dataloader.data_info.Data1D into sasgui.guiframe.dataFitting.Data1D
228                    # TODO : Fix it
229                    new_data = self.manager.create_gui_data(item, p_file)
230                    output[new_data.id] = new_data
231                    self.updateModel(new_data, p_file)
232                    self.model.reset()
233
234                    QtGui.qApp.processEvents()
235
236                    if hasattr(item, 'errors'):
237                        for error_data in item.errors:
238                            data_error = True
239                            message += "\tError: {0}\n".format(error_data)
240                    else:
241
242                        logging.error("Loader returned an invalid object:\n %s" % str(item))
243                        data_error = True
244
245            except Exception as ex:
246                logging.error(sys.exc_value)
247
248                any_error = True
249            if any_error or error_message != "":
250                if error_message == "":
251                    error = "Error: " + str(sys.exc_info()[1]) + "\n"
252                    error += "while loading Data: \n%s\n" % str(basename)
253                    error_message += "The data file you selected could not be loaded.\n"
254                    error_message += "Make sure the content of your file"
255                    error_message += " is properly formatted.\n\n"
256                    error_message += "When contacting the SasView team, mention the"
257                    error_message += " following:\n%s" % str(error)
258                elif data_error:
259                    base_message = "Errors occurred while loading "
260                    base_message += "{0}\n".format(basename)
261                    base_message += "The data file loaded but with errors.\n"
262                    error_message = base_message + error_message
263                else:
264                    error_message += "%s\n" % str(p_file)
265                info = "error"
266       
267        if any_error or error_message:
268            # self.loadUpdate(output=output, message=error_message, info=info)
269            self.communicate.statusBarUpdateSignal.emit(error_message)
270
271        else:
272            message = "Loading Data Complete! "
273        message += log_msg
274        return (output, message)
275
276    def getWlist(self):
277        """
278        """
279        # Display the Qt Load File module
280        cards = self.loader.get_wildcards()
281
282        # get rid of the wx remnant in wildcards
283        # TODO: modify sasview loader get_wildcards method, after merge,
284        # so this kludge can be avoided
285        new_cards = []
286        for item in cards:
287            new_cards.append(item[:item.find("|")])
288        wlist = ';;'.join(new_cards)
289
290        return wlist
291
292    def selectData(self, index):
293        """
294        Callback method for modifying the TreeView on Selection Options change
295        """
296        if not isinstance(index, int):
297            msg = "Incorrect type passed to DataExplorer.selectData()"
298            raise AttributeError, msg
299
300        # Respond appropriately
301        if index == 0:
302            # Select All
303            for index in range(self.model.rowCount()):
304                item = self.model.item(index)
305                if item.isCheckable() and item.checkState() == QtCore.Qt.Unchecked:
306                    item.setCheckState(QtCore.Qt.Checked)
307        elif index == 1:
308            # De-select All
309            for index in range(self.model.rowCount()):
310                item = self.model.item(index)
311                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
312                    item.setCheckState(QtCore.Qt.Unchecked)
313
314        elif index == 2:
315            # Select All 1-D
316            for index in range(self.model.rowCount()):
317                item = self.model.item(index)
318                item.setCheckState(QtCore.Qt.Unchecked)
319
320                try:
321                    is1D = item.child(0).data().toPyObject().__class__.__name__ == 'Data1D'
322                except AttributeError:
323                    msg = "Bad structure of the data model."
324                    raise RuntimeError, msg
325
326                if is1D:
327                    item.setCheckState(QtCore.Qt.Checked)
328
329        elif index == 3:
330            # Unselect All 1-D
331            for index in range(self.model.rowCount()):
332                item = self.model.item(index)
333
334                try:
335                    is1D = item.child(0).data().toPyObject().__class__.__name__ == 'Data1D'
336                except AttributeError:
337                    msg = "Bad structure of the data model."
338                    raise RuntimeError, msg
339
340                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is1D:
341                    item.setCheckState(QtCore.Qt.Unchecked)
342
343        elif index == 4:
344            # Select All 2-D
345            for index in range(self.model.rowCount()):
346                item = self.model.item(index)
347                item.setCheckState(QtCore.Qt.Unchecked)
348                try:
349                    is2D = item.child(0).data().toPyObject().__class__.__name__ == 'Data2D'
350                except AttributeError:
351                    msg = "Bad structure of the data model."
352                    raise RuntimeError, msg
353
354                if is2D:
355                    item.setCheckState(QtCore.Qt.Checked)
356
357        elif index == 5:
358            # Unselect All 2-D
359            for index in range(self.model.rowCount()):
360                item = self.model.item(index)
361
362                try:
363                    is2D = item.child(0).data().toPyObject().__class__.__name__ == 'Data2D'
364                except AttributeError:
365                    msg = "Bad structure of the data model."
366                    raise RuntimeError, msg
367
368                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is2D:
369                    item.setCheckState(QtCore.Qt.Unchecked)
370
371        else:
372            msg = "Incorrect value in the Selection Option"
373            # Change this to a proper logging action
374            raise Exception, msg
375
376
377    def loadComplete(self, output, message=""):
378        """
379        Post message to status bar and update the data manager
380        """
381        self.model.reset()
382        # Notify the manager of the new data available
383        self.communicate.statusBarUpdateSignal.emit(message)
384        self.communicate.fileDataReceivedSignal.emit(output)
385        self.manager.add_data(data_list=output)
386
387    def updateModel(self, data, p_file):
388        """
389        """
390        # Structure of the model
391        # checkbox + basename
392        #     |-------> Info
393        #                 |----> Data.D object
394        #                 |----> Title:
395        #                 |----> Run:
396        #                 |----> Type:
397        #                 |----> Path:
398        #                 |----> Process
399        #                          |-----> process[0].name
400        #
401
402        # Top-level item: checkbox with label
403        checkbox_item = QtGui.QStandardItem(True)
404        checkbox_item.setCheckable(True)
405        checkbox_item.setCheckState(QtCore.Qt.Checked)
406        checkbox_item.setText(os.path.basename(p_file))
407
408        # Add "Info" item
409        info_item = QtGui.QStandardItem("Info")
410
411        # Add the actual Data1D/Data2D object
412        object_item = QtGui.QStandardItem()
413        object_item.setData(QtCore.QVariant(data))
414
415        checkbox_item.setChild(0, object_item)
416
417        # Add rows for display in the view
418        self.addExtraRows(info_item, data)
419
420        # Set info_item as the only child
421        checkbox_item.setChild(1, info_item)
422
423        # New row in the model
424        self.model.appendRow(checkbox_item)
425       
426        # Don't show "empty" rows with data objects
427        self.proxy.setFilterRegExp(r"[^()]")
428
429    def updateModelFromPerspective(self, model_item):
430        """
431        """
432        # Overwrite the index with what we got from the perspective
433        if type(model_item) != QtGui.QStandardItem:
434            msg = "Wrong data type returned from calculations."
435            raise AttributeError, msg
436        # self.model.insertRow(model_item)
437        # Reset the view
438        self.model.reset()
439        # Pass acting as a debugger anchor
440        pass
441
442    def addExtraRows(self, info_item, data):
443        """
444        Extract relevant data to include in the Info ModelItem
445        """
446        title_item   = QtGui.QStandardItem("Title: "      + data.title)
447        run_item     = QtGui.QStandardItem("Run: "        + str(data.run))
448        type_item    = QtGui.QStandardItem("Type: "       + str(data.__class__.__name__))
449        path_item    = QtGui.QStandardItem("Path: "       + data.path)
450        instr_item   = QtGui.QStandardItem("Instrument: " + data.instrument)
451        process_item = QtGui.QStandardItem("Process")
452        if isinstance(data.process, list) and data.process:
453            for process in data.process:
454                process_date = process.date
455                process_date_item = QtGui.QStandardItem("Date: " + process_date)
456                process_item.appendRow(process_date_item)
457
458                process_descr = process.description
459                process_descr_item = QtGui.QStandardItem("Description: " + process_descr)
460                process_item.appendRow(process_descr_item)
461
462                process_name = process.name
463                process_name_item = QtGui.QStandardItem("Name: " + process_name)
464                process_item.appendRow(process_name_item)
465
466        info_item.appendRow(title_item)
467        info_item.appendRow(run_item)
468        info_item.appendRow(type_item)
469        info_item.appendRow(path_item)
470        info_item.appendRow(instr_item)
471        info_item.appendRow(process_item)
472       
473
474if __name__ == "__main__":
475    app = QtGui.QApplication([])
476    dlg = DataExplorerWindow()
477    dlg.show()
478    sys.exit(app.exec_())
Note: See TracBrowser for help on using the repository browser.