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

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

Prototype DE↔perspective api based on QStandardItem.

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