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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since ee7e423 was 6ff103a, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

CR: Deleting a single dataitem child will close the corresponding plot (SASVIEW-958)

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