source: sasview/src/sas/qtgui/Utilities/GuiUtils.py @ 5a731c63

ESS_GUIESS_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 5a731c63 was d4dac80, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

Merge branch 'ESS_GUI' into ESS_GUI_better_batch

  • Property mode set to 100644
File size: 31.7 KB
Line 
1"""
2Global defaults and various utility functions usable by the general GUI
3"""
4
5import os
6import re
7import sys
8import imp
9import warnings
10import webbrowser
11import urllib.parse
12
13import numpy as np
14
15warnings.simplefilter("ignore")
16import logging
17
18from PyQt5 import QtCore
19from PyQt5 import QtGui
20from PyQt5 import QtWidgets
21
22from periodictable import formula as Formula
23from sas.qtgui.Plotting import DataTransform
24from sas.qtgui.Plotting.ConvertUnits import convertUnit
25from sas.qtgui.Plotting.PlotterData import Data1D
26from sas.qtgui.Plotting.PlotterData import Data2D
27from sas.sascalc.dataloader.loader import Loader
28from sas.qtgui.Utilities import CustomDir
29
30## TODO: CHANGE FOR SHIPPED PATH IN RELEASE
31if os.path.splitext(sys.argv[0])[1].lower() != ".py":
32        HELP_DIRECTORY_LOCATION = "doc"
33else:
34        HELP_DIRECTORY_LOCATION = "docs/sphinx-docs/build/html"
35IMAGES_DIRECTORY_LOCATION = HELP_DIRECTORY_LOCATION + "/_images"
36
37def get_app_dir():
38    """
39        The application directory is the one where the default custom_config.py
40        file resides.
41
42        :returns: app_path - the path to the applicatin directory
43    """
44    # First, try the directory of the executable we are running
45    app_path = sys.path[0]
46    if os.path.isfile(app_path):
47        app_path = os.path.dirname(app_path)
48    if os.path.isfile(os.path.join(app_path, "custom_config.py")):
49        app_path = os.path.abspath(app_path)
50        #logging.info("Using application path: %s", app_path)
51        return app_path
52
53    # Next, try the current working directory
54    if os.path.isfile(os.path.join(os.getcwd(), "custom_config.py")):
55        #logging.info("Using application path: %s", os.getcwd())
56        return os.path.abspath(os.getcwd())
57
58    # Finally, try the directory of the sasview module
59    # TODO: gui_manager will have to know about sasview until we
60    # clean all these module variables and put them into a config class
61    # that can be passed by sasview.py.
62    # logging.info(sys.executable)
63    # logging.info(str(sys.argv))
64    from sas import sasview as sasview
65    app_path = os.path.dirname(sasview.__file__)
66    # logging.info("Using application path: %s", app_path)
67    return app_path
68
69def get_user_directory():
70    """
71        Returns the user's home directory
72    """
73    userdir = os.path.join(os.path.expanduser("~"), ".sasview")
74    if not os.path.isdir(userdir):
75        os.makedirs(userdir)
76    return userdir
77
78def _find_local_config(confg_file, path):
79    """
80        Find configuration file for the current application
81    """
82    config_module = None
83    fObj = None
84    try:
85        fObj, path_config, descr = imp.find_module(confg_file, [path])
86        config_module = imp.load_module(confg_file, fObj, path_config, descr)
87    except ImportError:
88        pass
89        #logging.error("Error loading %s/%s: %s" % (path, confg_file, sys.exc_value))
90    except ValueError:
91        print("Value error")
92        pass
93    finally:
94        if fObj is not None:
95            fObj.close()
96    #logging.info("GuiManager loaded %s/%s" % (path, confg_file))
97    return config_module
98
99
100# Get APP folder
101PATH_APP = get_app_dir()
102DATAPATH = PATH_APP
103
104# Read in the local config, which can either be with the main
105# application or in the installation directory
106config = _find_local_config('local_config', PATH_APP)
107
108if config is None:
109    config = _find_local_config('local_config', os.getcwd())
110else:
111    pass
112
113c_conf_dir = CustomDir.setup_conf_dir(PATH_APP)
114
115custom_config = _find_local_config('custom_config', c_conf_dir)
116if custom_config is None:
117    custom_config = _find_local_config('custom_config', os.getcwd())
118    if custom_config is None:
119        msgConfig = "Custom_config file was not imported"
120
121#read some constants from config
122APPLICATION_STATE_EXTENSION = config.APPLICATION_STATE_EXTENSION
123APPLICATION_NAME = config.__appname__
124SPLASH_SCREEN_PATH = config.SPLASH_SCREEN_PATH
125WELCOME_PANEL_ON = config.WELCOME_PANEL_ON
126SPLASH_SCREEN_WIDTH = config.SPLASH_SCREEN_WIDTH
127SPLASH_SCREEN_HEIGHT = config.SPLASH_SCREEN_HEIGHT
128SS_MAX_DISPLAY_TIME = config.SS_MAX_DISPLAY_TIME
129if not WELCOME_PANEL_ON:
130    WELCOME_PANEL_SHOW = False
131else:
132    WELCOME_PANEL_SHOW = True
133try:
134    DATALOADER_SHOW = custom_config.DATALOADER_SHOW
135    TOOLBAR_SHOW = custom_config.TOOLBAR_SHOW
136    FIXED_PANEL = custom_config.FIXED_PANEL
137    if WELCOME_PANEL_ON:
138        WELCOME_PANEL_SHOW = custom_config.WELCOME_PANEL_SHOW
139    PLOPANEL_WIDTH = custom_config.PLOPANEL_WIDTH
140    DATAPANEL_WIDTH = custom_config.DATAPANEL_WIDTH
141    GUIFRAME_WIDTH = custom_config.GUIFRAME_WIDTH
142    GUIFRAME_HEIGHT = custom_config.GUIFRAME_HEIGHT
143    CONTROL_WIDTH = custom_config.CONTROL_WIDTH
144    CONTROL_HEIGHT = custom_config.CONTROL_HEIGHT
145    DEFAULT_PERSPECTIVE = custom_config.DEFAULT_PERSPECTIVE
146    CLEANUP_PLOT = custom_config.CLEANUP_PLOT
147    # custom open_path
148    open_folder = custom_config.DEFAULT_OPEN_FOLDER
149    if open_folder is not None and os.path.isdir(open_folder):
150        DEFAULT_OPEN_FOLDER = os.path.abspath(open_folder)
151    else:
152        DEFAULT_OPEN_FOLDER = PATH_APP
153except AttributeError:
154    DATALOADER_SHOW = True
155    TOOLBAR_SHOW = True
156    FIXED_PANEL = True
157    WELCOME_PANEL_SHOW = False
158    PLOPANEL_WIDTH = config.PLOPANEL_WIDTH
159    DATAPANEL_WIDTH = config.DATAPANEL_WIDTH
160    GUIFRAME_WIDTH = config.GUIFRAME_WIDTH
161    GUIFRAME_HEIGHT = config.GUIFRAME_HEIGHT
162    CONTROL_WIDTH = -1
163    CONTROL_HEIGHT = -1
164    DEFAULT_PERSPECTIVE = None
165    CLEANUP_PLOT = False
166    DEFAULT_OPEN_FOLDER = PATH_APP
167
168#DEFAULT_STYLE = config.DEFAULT_STYLE
169
170PLUGIN_STATE_EXTENSIONS = config.PLUGIN_STATE_EXTENSIONS
171OPEN_SAVE_MENU = config.OPEN_SAVE_PROJECT_MENU
172VIEW_MENU = config.VIEW_MENU
173EDIT_MENU = config.EDIT_MENU
174extension_list = []
175if APPLICATION_STATE_EXTENSION is not None:
176    extension_list.append(APPLICATION_STATE_EXTENSION)
177EXTENSIONS = PLUGIN_STATE_EXTENSIONS + extension_list
178try:
179    PLUGINS_WLIST = '|'.join(config.PLUGINS_WLIST)
180except AttributeError:
181    PLUGINS_WLIST = ''
182APPLICATION_WLIST = config.APPLICATION_WLIST
183IS_WIN = True
184IS_LINUX = False
185CLOSE_SHOW = True
186TIME_FACTOR = 2
187NOT_SO_GRAPH_LIST = ["BoxSum"]
188
189
190class Communicate(QtCore.QObject):
191    """
192    Utility class for tracking of the Qt signals
193    """
194    # File got successfully read
195    fileReadSignal = QtCore.pyqtSignal(list)
196
197    # Open File returns "list" of paths
198    fileDataReceivedSignal = QtCore.pyqtSignal(dict)
199
200    # Update Main window status bar with "str"
201    # Old "StatusEvent"
202    statusBarUpdateSignal = QtCore.pyqtSignal(str)
203
204    # Send data to the current perspective
205    updatePerspectiveWithDataSignal = QtCore.pyqtSignal(list)
206
207    # New data in current perspective
208    updateModelFromPerspectiveSignal = QtCore.pyqtSignal(QtGui.QStandardItem)
209
210    # New theory data in current perspective
211    updateTheoryFromPerspectiveSignal = QtCore.pyqtSignal(QtGui.QStandardItem)
212
213    # New plot requested from the GUI manager
214    # Old "NewPlotEvent"
215    plotRequestedSignal = QtCore.pyqtSignal(list)
216
217    # Plot from file names
218    plotFromFilenameSignal = QtCore.pyqtSignal(str)
219
220    # Plot update requested from a perspective
221    plotUpdateSignal = QtCore.pyqtSignal(list)
222
223    # Progress bar update value
224    progressBarUpdateSignal = QtCore.pyqtSignal(int)
225
226    # Workspace charts added/removed
227    activeGraphsSignal = QtCore.pyqtSignal(list)
228
229    # Current workspace chart's name changed
230    activeGraphName = QtCore.pyqtSignal(tuple)
231
232    # Current perspective changed
233    perspectiveChangedSignal = QtCore.pyqtSignal(str)
234
235    # File/dataset got deleted
236    dataDeletedSignal = QtCore.pyqtSignal(list)
237
238    # Send data to Data Operation Utility panel
239    sendDataToPanelSignal = QtCore.pyqtSignal(dict)
240
241    # Send result of Data Operation Utility panel to Data Explorer
242    updateModelFromDataOperationPanelSignal = QtCore.pyqtSignal(QtGui.QStandardItem, dict)
243
244    # Notify about a new custom plugin being written/deleted/modified
245    customModelDirectoryChanged = QtCore.pyqtSignal()
246
247    # Notify the gui manager about new data to be added to the grid view
248    sendDataToGridSignal = QtCore.pyqtSignal(list)
249
250
251def updateModelItemWithPlot(item, update_data, name=""):
252    """
253    Adds a checkboxed row named "name" to QStandardItem
254    Adds 'update_data' to that row.
255    """
256    assert isinstance(item, QtGui.QStandardItem)
257
258    # Check if data with the same ID is already present
259    for index in range(item.rowCount()):
260        plot_item = item.child(index)
261        if plot_item.isCheckable():
262            plot_data = plot_item.child(0).data()
263            if plot_data.id is not None and \
264                   (plot_data.name == update_data.name or plot_data.id == update_data.id):
265            # if plot_data.id is not None and plot_data.id == update_data.id:
266                # replace data section in item
267                plot_item.child(0).setData(update_data)
268                plot_item.setText(name)
269                # Plot title if any
270                if plot_item.child(1).hasChildren():
271                    plot_item.child(1).child(0).setText("Title: %s"%name)
272                # Force redisplay
273                return
274
275    # Create the new item
276    checkbox_item = createModelItemWithPlot(update_data, name)
277
278    # Append the new row to the main item
279    item.appendRow(checkbox_item)
280
281class HashableStandardItem(QtGui.QStandardItem):
282    """
283    Subclassed standard item with reimplemented __hash__
284    to allow for use as an index.
285    """
286    def __init__(self, parent=None):
287        super(HashableStandardItem, self).__init__()
288
289    def __hash__(self):
290        ''' just a random hash value '''
291        #return hash(self.__init__)
292        return 0
293
294    def clone(self):
295        ''' Assure __hash__ is cloned as well'''
296        clone = super(HashableStandardItem, self).clone()
297        clone.__hash__ = self.__hash__
298        return clone
299
300
301def createModelItemWithPlot(update_data, name=""):
302    """
303    Creates a checkboxed QStandardItem named "name"
304    Adds 'update_data' to that row.
305    """
306    py_update_data = update_data
307
308    checkbox_item = HashableStandardItem()
309    checkbox_item.setCheckable(True)
310    checkbox_item.setCheckState(QtCore.Qt.Checked)
311    checkbox_item.setText(name)
312
313    # Add "Info" item
314    if isinstance(py_update_data, (Data1D, Data2D)):
315        # If Data1/2D added - extract Info from it
316        info_item = infoFromData(py_update_data)
317    else:
318        # otherwise just add a naked item
319        info_item = QtGui.QStandardItem("Info")
320
321    # Add the actual Data1D/Data2D object
322    object_item = QtGui.QStandardItem()
323    object_item.setData(update_data)
324
325    # Set the data object as the first child
326    checkbox_item.setChild(0, object_item)
327
328    # Set info_item as the second child
329    checkbox_item.setChild(1, info_item)
330
331    # And return the newly created item
332    return checkbox_item
333
334def updateModelItem(item, update_data, name=""):
335    """
336    Adds a simple named child to QStandardItem
337    """
338    assert isinstance(item, QtGui.QStandardItem)
339
340    # Add the actual Data1D/Data2D object
341    object_item = QtGui.QStandardItem()
342    object_item.setText(name)
343    object_item.setData(update_data)
344
345    # Append the new row to the main item
346    item.appendRow(object_item)
347
348def updateModelItemStatus(model_item, filename="", name="", status=2):
349    """
350    Update status of checkbox related to high- and low-Q extrapolation
351    choice in Invariant Panel
352    """
353    assert isinstance(model_item, QtGui.QStandardItemModel)
354
355    # Iterate over model looking for items with checkboxes
356    for index in range(model_item.rowCount()):
357        item = model_item.item(index)
358        if item.text() == filename and item.isCheckable() \
359                and item.checkState() == QtCore.Qt.Checked:
360            # Going 1 level deeper only
361            for index_2 in range(item.rowCount()):
362                item_2 = item.child(index_2)
363                if item_2 and item_2.isCheckable() and item_2.text() == name:
364                    item_2.setCheckState(status)
365
366    return
367
368def itemFromFilename(filename, model_item):
369    """
370    Returns the model item text=filename in the model
371    """
372    assert isinstance(model_item, QtGui.QStandardItemModel)
373    assert isinstance(filename, str)
374
375    # Iterate over model looking for named items
376    item = list([i for i in [model_item.item(index)
377                             for index in range(model_item.rowCount())]
378                 if str(i.text()) == filename])
379    return item[0] if len(item)>0 else None
380
381def plotsFromFilename(filename, model_item):
382    """
383    Returns the list of plots for the item with text=filename in the model
384    """
385    assert isinstance(model_item, QtGui.QStandardItemModel)
386    assert isinstance(filename, str)
387
388    plot_data = []
389    # Iterate over model looking for named items
390    for index in range(model_item.rowCount()):
391        item = model_item.item(index)
392        if str(item.text()) == filename:
393            # TODO: assure item type is correct (either data1/2D or Plotter)
394            plot_data.append(item.child(0).data())
395            # Going 1 level deeper only
396            for index_2 in range(item.rowCount()):
397                item_2 = item.child(index_2)
398                if item_2 and item_2.isCheckable():
399                    # TODO: assure item type is correct (either data1/2D or Plotter)
400                    plot_data.append(item_2.child(0).data())
401
402    return plot_data
403
404def plotsFromCheckedItems(model_item):
405    """
406    Returns the list of plots for items in the model which are checked
407    """
408    assert isinstance(model_item, QtGui.QStandardItemModel)
409
410    plot_data = []
411    # Iterate over model looking for items with checkboxes
412    for index in range(model_item.rowCount()):
413        item = model_item.item(index)
414
415        # Going 1 level deeper only
416        for index_2 in range(item.rowCount()):
417            item_2 = item.child(index_2)
418            if item_2 and item_2.isCheckable() and item_2.checkState() == QtCore.Qt.Checked:
419                # TODO: assure item type is correct (either data1/2D or Plotter)
420                plot_data.append((item_2, item_2.child(0).data()))
421
422        if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
423            # TODO: assure item type is correct (either data1/2D or Plotter)
424            plot_data.append((item, item.child(0).data()))
425
426    return plot_data
427
428def infoFromData(data):
429    """
430    Given Data1D/Data2D object, extract relevant Info elements
431    and add them to a model item
432    """
433    assert isinstance(data, (Data1D, Data2D))
434
435    info_item = QtGui.QStandardItem("Info")
436
437    title_item = QtGui.QStandardItem("Title: " + data.title)
438    info_item.appendRow(title_item)
439    run_item = QtGui.QStandardItem("Run: " + str(data.run))
440    info_item.appendRow(run_item)
441    type_item = QtGui.QStandardItem("Type: " + str(data.__class__.__name__))
442    info_item.appendRow(type_item)
443
444    if data.path:
445        path_item = QtGui.QStandardItem("Path: " + data.path)
446        info_item.appendRow(path_item)
447
448    if data.instrument:
449        instr_item = QtGui.QStandardItem("Instrument: " + data.instrument)
450        info_item.appendRow(instr_item)
451
452    process_item = QtGui.QStandardItem("Process")
453    if isinstance(data.process, list) and data.process:
454        for process in data.process:
455            process_date = process.date
456            process_date_item = QtGui.QStandardItem("Date: " + process_date)
457            process_item.appendRow(process_date_item)
458
459            process_descr = process.description
460            process_descr_item = QtGui.QStandardItem("Description: " + process_descr)
461            process_item.appendRow(process_descr_item)
462
463            process_name = process.name
464            process_name_item = QtGui.QStandardItem("Name: " + process_name)
465            process_item.appendRow(process_name_item)
466
467    info_item.appendRow(process_item)
468
469    return info_item
470
471def dataFromItem(item):
472    """
473    Retrieve Data1D/2D component from QStandardItem.
474    The assumption - data stored in SasView standard, in child 0
475    """
476    return item.child(0).data()
477
478def openLink(url):
479    """
480    Open a URL in an external browser.
481    Check the URL first, though.
482    """
483    parsed_url = urllib.parse.urlparse(url)
484    if parsed_url.scheme:
485        webbrowser.open(url)
486    else:
487        msg = "Attempt at opening an invalid URL"
488        raise AttributeError(msg)
489
490def retrieveData1d(data):
491    """
492    Retrieve 1D data from file and construct its text
493    representation
494    """
495    if not isinstance(data, Data1D):
496        msg = "Incorrect type passed to retrieveData1d"
497        raise AttributeError(msg)
498    try:
499        xmin = min(data.x)
500        ymin = min(data.y)
501    except:
502        msg = "Unable to find min/max of \n data named %s" % \
503                    data.filename
504        #logging.error(msg)
505        raise ValueError(msg)
506
507    text = data.__str__()
508    text += 'Data Min Max:\n'
509    text += 'X_min = %s:  X_max = %s\n' % (xmin, max(data.x))
510    text += 'Y_min = %s:  Y_max = %s\n' % (ymin, max(data.y))
511    if data.dy is not None:
512        text += 'dY_min = %s:  dY_max = %s\n' % (min(data.dy), max(data.dy))
513    text += '\nData Points:\n'
514    x_st = "X"
515    for index in range(len(data.x)):
516        if data.dy is not None and len(data.dy) > index:
517            dy_val = data.dy[index]
518        else:
519            dy_val = 0.0
520        if data.dx is not None and len(data.dx) > index:
521            dx_val = data.dx[index]
522        else:
523            dx_val = 0.0
524        if data.dxl is not None and len(data.dxl) > index:
525            if index == 0:
526                x_st = "Xl"
527            dx_val = data.dxl[index]
528        elif data.dxw is not None and len(data.dxw) > index:
529            if index == 0:
530                x_st = "Xw"
531            dx_val = data.dxw[index]
532
533        if index == 0:
534            text += "<index> \t<X> \t<Y> \t<dY> \t<d%s>\n" % x_st
535        text += "%s \t%s \t%s \t%s \t%s\n" % (index,
536                                                data.x[index],
537                                                data.y[index],
538                                                dy_val,
539                                                dx_val)
540    return text
541
542def retrieveData2d(data):
543    """
544    Retrieve 2D data from file and construct its text
545    representation
546    """
547    if not isinstance(data, Data2D):
548        msg = "Incorrect type passed to retrieveData2d"
549        raise AttributeError(msg)
550
551    text = data.__str__()
552    text += 'Data Min Max:\n'
553    text += 'I_min = %s\n' % min(data.data)
554    text += 'I_max = %s\n\n' % max(data.data)
555    text += 'Data (First 2501) Points:\n'
556    text += 'Data columns include err(I).\n'
557    text += 'ASCII data starts here.\n'
558    text += "<index> \t<Qx> \t<Qy> \t<I> \t<dI> \t<dQparal> \t<dQperp>\n"
559    di_val = 0.0
560    dx_val = 0.0
561    dy_val = 0.0
562    len_data = len(data.qx_data)
563    for index in range(0, len_data):
564        x_val = data.qx_data[index]
565        y_val = data.qy_data[index]
566        i_val = data.data[index]
567        if data.err_data is not None:
568            di_val = data.err_data[index]
569        if data.dqx_data is not None:
570            dx_val = data.dqx_data[index]
571        if data.dqy_data is not None:
572            dy_val = data.dqy_data[index]
573
574        text += "%s \t%s \t%s \t%s \t%s \t%s \t%s\n" % (index,
575                                                        x_val,
576                                                        y_val,
577                                                        i_val,
578                                                        di_val,
579                                                        dx_val,
580                                                        dy_val)
581        # Takes too long time for typical data2d: Break here
582        if index >= 2500:
583            text += ".............\n"
584            break
585
586    return text
587
588def onTXTSave(data, path):
589    """
590    Save file as formatted txt
591    """
592    with open(path,'w') as out:
593        has_errors = True
594        if data.dy is None or not data.dy.any():
595            has_errors = False
596        # Sanity check
597        if has_errors:
598            try:
599                if len(data.y) != len(data.dy):
600                    has_errors = False
601            except:
602                has_errors = False
603        if has_errors:
604            if data.dx is not None and data.dx.any():
605                out.write("<X>   <Y>   <dY>   <dX>\n")
606            else:
607                out.write("<X>   <Y>   <dY>\n")
608        else:
609            out.write("<X>   <Y>\n")
610
611        for i in range(len(data.x)):
612            if has_errors:
613                if data.dx is not None and data.dx.any():
614                    if  data.dx[i] is not None:
615                        out.write("%g  %g  %g  %g\n" % (data.x[i],
616                                                        data.y[i],
617                                                        data.dy[i],
618                                                        data.dx[i]))
619                    else:
620                        out.write("%g  %g  %g\n" % (data.x[i],
621                                                    data.y[i],
622                                                    data.dy[i]))
623                else:
624                    out.write("%g  %g  %g\n" % (data.x[i],
625                                                data.y[i],
626                                                data.dy[i]))
627            else:
628                out.write("%g  %g\n" % (data.x[i],
629                                        data.y[i]))
630
631def saveData1D(data):
632    """
633    Save 1D data points
634    """
635    default_name = os.path.basename(data.filename)
636    default_name, extension = os.path.splitext(default_name)
637    if not extension:
638        extension = ".txt"
639    default_name += "_out" + extension
640
641    wildcard = "Text files (*.txt);;"\
642                "CanSAS 1D files(*.xml)"
643    kwargs = {
644        'caption'   : 'Save As',
645        'directory' : default_name,
646        'filter'    : wildcard,
647        'parent'    : None,
648    }
649    # Query user for filename.
650    filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
651    filename = filename_tuple[0]
652
653    # User cancelled.
654    if not filename:
655        return
656
657    #Instantiate a loader
658    loader = Loader()
659    if os.path.splitext(filename)[1].lower() == ".txt":
660        onTXTSave(data, filename)
661    if os.path.splitext(filename)[1].lower() == ".xml":
662        loader.save(filename, data, ".xml")
663
664def saveData2D(data):
665    """
666    Save data2d dialog
667    """
668    default_name = os.path.basename(data.filename)
669    default_name, _ = os.path.splitext(default_name)
670    ext_format = ".dat"
671    default_name += "_out" + ext_format
672
673    wildcard = "IGOR/DAT 2D file in Q_map (*.dat)"
674    kwargs = {
675        'caption'   : 'Save As',
676        'directory' : default_name,
677        'filter'    : wildcard,
678        'parent'    : None,
679    }
680    # Query user for filename.
681    filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
682    filename = filename_tuple[0]
683
684    # User cancelled.
685    if not filename:
686        return
687
688    #Instantiate a loader
689    loader = Loader()
690
691    if os.path.splitext(filename)[1].lower() == ext_format:
692        loader.save(filename, data, ext_format)
693
694class FormulaValidator(QtGui.QValidator):
695    def __init__(self, parent=None):
696        super(FormulaValidator, self).__init__(parent)
697 
698    def validate(self, input, pos):
699
700        self._setStyleSheet("")
701        return QtGui.QValidator.Acceptable, pos
702
703        #try:
704        #    Formula(str(input))
705        #    self._setStyleSheet("")
706        #    return QtGui.QValidator.Acceptable, pos
707
708        #except Exception as e:
709        #    self._setStyleSheet("background-color:pink;")
710        #    return QtGui.QValidator.Intermediate, pos
711
712    def _setStyleSheet(self, value):
713        try:
714            if self.parent():
715                self.parent().setStyleSheet(value)
716        except:
717            pass
718
719def xyTransform(data, xLabel="", yLabel=""):
720    """
721    Transforms x and y in View and set the scale
722    """
723    # Changing the scale might be incompatible with
724    # currently displayed data (for instance, going
725    # from ln to log when all plotted values have
726    # negative natural logs).
727    # Go linear and only change the scale at the end.
728    xscale = 'linear'
729    yscale = 'linear'
730    # Local data is either 1D or 2D
731    if data.id == 'fit':
732        return
733
734    # make sure we have some function to operate on
735    if xLabel is None:
736        xLabel = 'log10(x)'
737    if yLabel is None:
738        yLabel = 'log10(y)'
739
740    # control axis labels from the panel itself
741    yname, yunits = data.get_yaxis()
742    xname, xunits = data.get_xaxis()
743
744    # Goes through all possible scales
745    # self.x_label is already wrapped with Latex "$", so using the argument
746
747    # X
748    if xLabel == "x":
749        data.transformX(DataTransform.toX, DataTransform.errToX)
750        xLabel = "%s(%s)" % (xname, xunits)
751    if xLabel == "x^(2)":
752        data.transformX(DataTransform.toX2, DataTransform.errToX2)
753        xunits = convertUnit(2, xunits)
754        xLabel = "%s^{2}(%s)" % (xname, xunits)
755    if xLabel == "x^(4)":
756        data.transformX(DataTransform.toX4, DataTransform.errToX4)
757        xunits = convertUnit(4, xunits)
758        xLabel = "%s^{4}(%s)" % (xname, xunits)
759    if xLabel == "ln(x)":
760        data.transformX(DataTransform.toLogX, DataTransform.errToLogX)
761        xLabel = "\ln{(%s)}(%s)" % (xname, xunits)
762    if xLabel == "log10(x)":
763        data.transformX(DataTransform.toX_pos, DataTransform.errToX_pos)
764        xscale = 'log'
765        xLabel = "%s(%s)" % (xname, xunits)
766    if xLabel == "log10(x^(4))":
767        data.transformX(DataTransform.toX4, DataTransform.errToX4)
768        xunits = convertUnit(4, xunits)
769        xLabel = "%s^{4}(%s)" % (xname, xunits)
770        xscale = 'log'
771
772    # Y
773    if yLabel == "ln(y)":
774        data.transformY(DataTransform.toLogX, DataTransform.errToLogX)
775        yLabel = "\ln{(%s)}(%s)" % (yname, yunits)
776    if yLabel == "y":
777        data.transformY(DataTransform.toX, DataTransform.errToX)
778        yLabel = "%s(%s)" % (yname, yunits)
779    if yLabel == "log10(y)":
780        data.transformY(DataTransform.toX_pos, DataTransform.errToX_pos)
781        yscale = 'log'
782        yLabel = "%s(%s)" % (yname, yunits)
783    if yLabel == "y^(2)":
784        data.transformY(DataTransform.toX2, DataTransform.errToX2)
785        yunits = convertUnit(2, yunits)
786        yLabel = "%s^{2}(%s)" % (yname, yunits)
787    if yLabel == "1/y":
788        data.transformY(DataTransform.toOneOverX, DataTransform.errOneOverX)
789        yunits = convertUnit(-1, yunits)
790        yLabel = "1/%s(%s)" % (yname, yunits)
791    if yLabel == "y*x^(2)":
792        data.transformY(DataTransform.toYX2, DataTransform.errToYX2)
793        xunits = convertUnit(2, xunits)
794        yLabel = "%s \ \ %s^{2}(%s%s)" % (yname, xname, yunits, xunits)
795    if yLabel == "y*x^(4)":
796        data.transformY(DataTransform.toYX4, DataTransform.errToYX4)
797        xunits = convertUnit(4, xunits)
798        yLabel = "%s \ \ %s^{4}(%s%s)" % (yname, xname, yunits, xunits)
799    if yLabel == "1/sqrt(y)":
800        data.transformY(DataTransform.toOneOverSqrtX, DataTransform.errOneOverSqrtX)
801        yunits = convertUnit(-0.5, yunits)
802        yLabel = "1/\sqrt{%s}(%s)" % (yname, yunits)
803    if yLabel == "ln(y*x)":
804        data.transformY(DataTransform.toLogXY, DataTransform.errToLogXY)
805        yLabel = "\ln{(%s \ \ %s)}(%s%s)" % (yname, xname, yunits, xunits)
806    if yLabel == "ln(y*x^(2))":
807        data.transformY(DataTransform.toLogYX2, DataTransform.errToLogYX2)
808        xunits = convertUnit(2, xunits)
809        yLabel = "\ln (%s \ \ %s^{2})(%s%s)" % (yname, xname, yunits, xunits)
810    if yLabel == "ln(y*x^(4))":
811        data.transformY(DataTransform.toLogYX4, DataTransform.errToLogYX4)
812        xunits = convertUnit(4, xunits)
813        yLabel = "\ln (%s \ \ %s^{4})(%s%s)" % (yname, xname, yunits, xunits)
814    if yLabel == "log10(y*x^(4))":
815        data.transformY(DataTransform.toYX4, DataTransform.errToYX4)
816        xunits = convertUnit(4, xunits)
817        yscale = 'log'
818        yLabel = "%s \ \ %s^{4}(%s%s)" % (yname, xname, yunits, xunits)
819
820    # Perform the transformation of data in data1d->View
821    data.transformView()
822
823    return (xLabel, yLabel, xscale, yscale)
824
825def formatNumber(value, high=False):
826    """
827    Return a float in a standardized, human-readable formatted string.
828    This is used to output readable (e.g. x.xxxe-y) values to the panel.
829    """
830    try:
831        value = float(value)
832    except:
833        output = "NaN"
834        return output.lstrip().rstrip()
835
836    if high:
837        output = "%-7.5g" % value
838
839    else:
840        output = "%-5.3g" % value
841    return output.lstrip().rstrip()
842
843def convertUnitToHTML(unit):
844    """
845    Convert ASCII unit display into well rendering HTML
846    """
847    if unit == "1/A":
848        return "&#x212B;<sup>-1</sup>"
849    elif unit == "1/cm":
850        return "cm<sup>-1</sup>"
851    elif unit == "Ang":
852        return "&#x212B;"
853    elif unit == "1e-6/Ang^2":
854        return "10<sup>-6</sup>/&#x212B;<sup>2</sup>"
855    elif unit == "inf":
856        return "&#x221e;"
857    elif unit == "-inf":
858        return "-&#x221e;"
859    else:
860        return unit
861
862def parseName(name, expression):
863    """
864    remove "_" in front of a name
865    """
866    if re.match(expression, name) is not None:
867        word = re.split(expression, name, 1)
868        for item in word:           
869            if item.lstrip().rstrip() != '':
870                return item
871    else:
872        return name
873
874def toDouble(value_string):
875    """
876    toFloat conversion which cares deeply about user's locale
877    """
878    # Holy shit this escalated quickly in Qt5.
879    # No more float() cast on general locales.
880    value = QtCore.QLocale().toFloat(value_string)
881    if value[1]:
882        return value[0]
883
884    # Try generic locale
885    value = QtCore.QLocale(QtCore.QLocale('en_US')).toFloat(value_string)
886    if value[1]:
887        return value[0]
888    else:
889        raise TypeError
890
891def findNextFilename(filename, directory):
892    """
893    Finds the next available (non-existing) name for 'filename' in 'directory'.
894    plugin.py -> plugin (n).py  - for first 'n' for which the file doesn't exist
895    """
896    basename, ext = os.path.splitext(filename)
897    # limit the number of copies
898    MAX_FILENAMES = 1000
899    # Start with (1)
900    number_ext = 1
901    proposed_filename = ""
902    found_filename = False
903    # Find the next available filename or exit if too many copies
904    while not found_filename or number_ext > MAX_FILENAMES:
905        proposed_filename = basename + " ("+str(number_ext)+")" + ext
906        if os.path.exists(os.path.join(directory, proposed_filename)):
907            number_ext += 1
908        else:
909            found_filename = True
910
911    return proposed_filename
912
913
914class DoubleValidator(QtGui.QDoubleValidator):
915    """
916    Allow only dots as decimal separator
917    """
918    def validate(self, input, pos):
919        """
920        Return invalid for commas
921        """
922        if input is not None and ',' in input:
923            return (QtGui.QValidator.Invalid, input, pos)
924        return super(DoubleValidator, self).validate(input, pos)
925
926    def fixup(self, input):
927        """
928        Correct (remove) potential preexisting content
929        """
930        super(DoubleValidator, self).fixup(input)
931        input = input.replace(",", "")
932
933def checkModel(path):
934    """
935    Check that the model save in file 'path' can run.
936    """
937    # The following return needs to be removed once
938    # the unittest related changes in Sasmodels are commited
939    return True
940    # try running the model
941    from sasmodels.sasview_model import load_custom_model
942    Model = load_custom_model(path)
943    model = Model()
944    q =  np.array([0.01, 0.1])
945    _ = model.evalDistribution(q)
946    qx, qy =  np.array([0.01, 0.01]), np.array([0.1, 0.1])
947    _ = model.evalDistribution([qx, qy])
948
949    # check the model's unit tests run
950    from sasmodels.model_test import run_one
951    # TestSuite module in Qt5 now deletes tests in the suite after running,
952    # so suite[0] in run_one() in sasmodels/model_test.py will contain [None] and
953    # test.info.tests will raise.
954    # Not sure how to change the behaviour here, most likely sasmodels will have to
955    # be modified
956    result = run_one(path)
957
958    return result
959
960
961def enum(*sequential, **named):
962    """Create an enumeration object from a list of strings"""
963    enums = dict(zip(sequential, range(len(sequential))), **named)
964    return type('Enum', (), enums)
Note: See TracBrowser for help on using the repository browser.