source: sasview/src/sas/qtgui/DataExplorer.py @ 9e426c1

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

More main window items, system close, update checker, doc viewer etc.

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