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

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

Copy/paste fitting parameters SASVIEW-933

  • Property mode set to 100644
File size: 34.0 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
257    # Fitting parameter copy to clipboard
258    copyFitParamsSignal = QtCore.pyqtSignal(str)
259
260    # Fitting parameter paste from clipboard
261    pasteFitParamsSignal = QtCore.pyqtSignal()
262
263def updateModelItemWithPlot(item, update_data, name=""):
264    """
265    Adds a checkboxed row named "name" to QStandardItem
266    Adds 'update_data' to that row.
267    """
268    assert isinstance(item, QtGui.QStandardItem)
269
270    # Check if data with the same ID is already present
271    for index in range(item.rowCount()):
272        plot_item = item.child(index)
273        if plot_item.isCheckable():
274            plot_data = plot_item.child(0).data()
275            if plot_data.id is not None and \
276                   (plot_data.name == update_data.name or plot_data.id == update_data.id):
277            # if plot_data.id is not None and plot_data.id == update_data.id:
278                # replace data section in item
279                plot_item.child(0).setData(update_data)
280                plot_item.setText(name)
281                # Plot title if any
282                if plot_item.child(1).hasChildren():
283                    plot_item.child(1).child(0).setText("Title: %s"%name)
284                # Force redisplay
285                return
286
287    # Create the new item
288    checkbox_item = createModelItemWithPlot(update_data, name)
289
290    # Append the new row to the main item
291    item.appendRow(checkbox_item)
292
293class HashableStandardItem(QtGui.QStandardItem):
294    """
295    Subclassed standard item with reimplemented __hash__
296    to allow for use as an index.
297    """
298    def __init__(self, parent=None):
299        super(HashableStandardItem, self).__init__()
300
301    def __hash__(self):
302        ''' just a random hash value '''
303        #return hash(self.__init__)
304        return 0
305
306    def clone(self):
307        ''' Assure __hash__ is cloned as well'''
308        clone = super(HashableStandardItem, self).clone()
309        clone.__hash__ = self.__hash__
310        return clone
311
312
313def createModelItemWithPlot(update_data, name=""):
314    """
315    Creates a checkboxed QStandardItem named "name"
316    Adds 'update_data' to that row.
317    """
318    py_update_data = update_data
319
320    checkbox_item = HashableStandardItem()
321    checkbox_item.setCheckable(True)
322    checkbox_item.setCheckState(QtCore.Qt.Checked)
323    checkbox_item.setText(name)
324
325    # Add "Info" item
326    if isinstance(py_update_data, (Data1D, Data2D)):
327        # If Data1/2D added - extract Info from it
328        info_item = infoFromData(py_update_data)
329    else:
330        # otherwise just add a naked item
331        info_item = QtGui.QStandardItem("Info")
332
333    # Add the actual Data1D/Data2D object
334    object_item = QtGui.QStandardItem()
335    object_item.setData(update_data)
336
337    # Set the data object as the first child
338    checkbox_item.setChild(0, object_item)
339
340    # Set info_item as the second child
341    checkbox_item.setChild(1, info_item)
342
343    # And return the newly created item
344    return checkbox_item
345
346def updateModelItem(item, update_data, name=""):
347    """
348    Adds a simple named child to QStandardItem
349    """
350    assert isinstance(item, QtGui.QStandardItem)
351
352    # Add the actual Data1D/Data2D object
353    object_item = QtGui.QStandardItem()
354    object_item.setText(name)
355    object_item.setData(update_data)
356
357    # Append the new row to the main item
358    item.appendRow(object_item)
359
360def updateModelItemStatus(model_item, filename="", name="", status=2):
361    """
362    Update status of checkbox related to high- and low-Q extrapolation
363    choice in Invariant Panel
364    """
365    assert isinstance(model_item, QtGui.QStandardItemModel)
366
367    # Iterate over model looking for items with checkboxes
368    for index in range(model_item.rowCount()):
369        item = model_item.item(index)
370        if item.text() == filename and item.isCheckable() \
371                and item.checkState() == QtCore.Qt.Checked:
372            # Going 1 level deeper only
373            for index_2 in range(item.rowCount()):
374                item_2 = item.child(index_2)
375                if item_2 and item_2.isCheckable() and item_2.text() == name:
376                    item_2.setCheckState(status)
377
378    return
379
380def itemFromFilename(filename, model_item):
381    """
382    Returns the model item text=filename in the model
383    """
384    assert isinstance(model_item, QtGui.QStandardItemModel)
385    assert isinstance(filename, str)
386
387    # Iterate over model looking for named items
388    item = list([i for i in [model_item.item(index)
389                             for index in range(model_item.rowCount())]
390                 if str(i.text()) == filename])
391    return item[0] if len(item)>0 else None
392
393def plotsFromModel(model_name, model_item):
394    """
395    Returns the list of plots for the item with model name in the model
396    """
397    assert isinstance(model_item, QtGui.QStandardItem)
398    assert isinstance(model_name, str)
399
400    plot_data = []
401    # Iterate over model looking for named items
402    for index in range(model_item.rowCount()):
403        item = model_item.child(index)
404        if isinstance(item.data(), (Data1D, Data2D)):
405            plot_data.append(item.data())
406        if model_name in str(item.text()):
407            #plot_data.append(item.child(0).data())
408            # Going 1 level deeper only
409            for index_2 in range(item.rowCount()):
410                item_2 = item.child(index_2)
411                if item_2 and isinstance(item_2.data(), (Data1D, Data2D)):
412                    plot_data.append(item_2.data())
413
414    return plot_data
415
416def plotsFromFilename(filename, model_item):
417    """
418    Returns the list of plots for the item with text=filename in the model
419    """
420    assert isinstance(model_item, QtGui.QStandardItemModel)
421    assert isinstance(filename, str)
422
423    plot_data = {}
424    # Iterate over model looking for named items
425    for index in range(model_item.rowCount()):
426        item = model_item.item(index)
427        if str(item.text()) == filename:
428            # TODO: assure item type is correct (either data1/2D or Plotter)
429            plot_data[item] = item.child(0).data()
430            # Going 1 level deeper only
431            for index_2 in range(item.rowCount()):
432                item_2 = item.child(index_2)
433                if item_2 and item_2.isCheckable():
434                    # TODO: assure item type is correct (either data1/2D or Plotter)
435                    plot_data[item_2] = item_2.child(0).data()
436
437    return plot_data
438
439def plotsFromCheckedItems(model_item):
440    """
441    Returns the list of plots for items in the model which are checked
442    """
443    assert isinstance(model_item, QtGui.QStandardItemModel)
444
445    plot_data = []
446    # Iterate over model looking for items with checkboxes
447    for index in range(model_item.rowCount()):
448        item = model_item.item(index)
449
450        # Going 1 level deeper only
451        for index_2 in range(item.rowCount()):
452            item_2 = item.child(index_2)
453            if item_2 and item_2.isCheckable() and item_2.checkState() == QtCore.Qt.Checked:
454                # TODO: assure item type is correct (either data1/2D or Plotter)
455                plot_data.append((item_2, item_2.child(0).data()))
456
457        if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
458            # TODO: assure item type is correct (either data1/2D or Plotter)
459            plot_data.append((item, item.child(0).data()))
460
461    return plot_data
462
463def infoFromData(data):
464    """
465    Given Data1D/Data2D object, extract relevant Info elements
466    and add them to a model item
467    """
468    assert isinstance(data, (Data1D, Data2D))
469
470    info_item = QtGui.QStandardItem("Info")
471
472    title_item = QtGui.QStandardItem("Title: " + data.title)
473    info_item.appendRow(title_item)
474    run_item = QtGui.QStandardItem("Run: " + str(data.run))
475    info_item.appendRow(run_item)
476    type_item = QtGui.QStandardItem("Type: " + str(data.__class__.__name__))
477    info_item.appendRow(type_item)
478
479    if data.path:
480        path_item = QtGui.QStandardItem("Path: " + data.path)
481        info_item.appendRow(path_item)
482
483    if data.instrument:
484        instr_item = QtGui.QStandardItem("Instrument: " + data.instrument)
485        info_item.appendRow(instr_item)
486
487    process_item = QtGui.QStandardItem("Process")
488    if isinstance(data.process, list) and data.process:
489        for process in data.process:
490            process_date = process.date
491            process_date_item = QtGui.QStandardItem("Date: " + process_date)
492            process_item.appendRow(process_date_item)
493
494            process_descr = process.description
495            process_descr_item = QtGui.QStandardItem("Description: " + process_descr)
496            process_item.appendRow(process_descr_item)
497
498            process_name = process.name
499            process_name_item = QtGui.QStandardItem("Name: " + process_name)
500            process_item.appendRow(process_name_item)
501
502    info_item.appendRow(process_item)
503
504    return info_item
505
506def dataFromItem(item):
507    """
508    Retrieve Data1D/2D component from QStandardItem.
509    The assumption - data stored in SasView standard, in child 0
510    """
511    return item.child(0).data()
512
513def openLink(url):
514    """
515    Open a URL in an external browser.
516    Check the URL first, though.
517    """
518    parsed_url = urllib.parse.urlparse(url)
519    if parsed_url.scheme:
520        webbrowser.open(url)
521    else:
522        msg = "Attempt at opening an invalid URL"
523        raise AttributeError(msg)
524
525def retrieveData1d(data):
526    """
527    Retrieve 1D data from file and construct its text
528    representation
529    """
530    if not isinstance(data, Data1D):
531        msg = "Incorrect type passed to retrieveData1d"
532        raise AttributeError(msg)
533    try:
534        xmin = min(data.x)
535        ymin = min(data.y)
536    except:
537        msg = "Unable to find min/max of \n data named %s" % \
538                    data.filename
539        #logging.error(msg)
540        raise ValueError(msg)
541
542    text = data.__str__()
543    text += 'Data Min Max:\n'
544    text += 'X_min = %s:  X_max = %s\n' % (xmin, max(data.x))
545    text += 'Y_min = %s:  Y_max = %s\n' % (ymin, max(data.y))
546    if data.dy is not None:
547        text += 'dY_min = %s:  dY_max = %s\n' % (min(data.dy), max(data.dy))
548    text += '\nData Points:\n'
549    x_st = "X"
550    for index in range(len(data.x)):
551        if data.dy is not None and len(data.dy) > index:
552            dy_val = data.dy[index]
553        else:
554            dy_val = 0.0
555        if data.dx is not None and len(data.dx) > index:
556            dx_val = data.dx[index]
557        else:
558            dx_val = 0.0
559        if data.dxl is not None and len(data.dxl) > index:
560            if index == 0:
561                x_st = "Xl"
562            dx_val = data.dxl[index]
563        elif data.dxw is not None and len(data.dxw) > index:
564            if index == 0:
565                x_st = "Xw"
566            dx_val = data.dxw[index]
567
568        if index == 0:
569            text += "<index> \t<X> \t<Y> \t<dY> \t<d%s>\n" % x_st
570        text += "%s \t%s \t%s \t%s \t%s\n" % (index,
571                                                data.x[index],
572                                                data.y[index],
573                                                dy_val,
574                                                dx_val)
575    return text
576
577def retrieveData2d(data):
578    """
579    Retrieve 2D data from file and construct its text
580    representation
581    """
582    if not isinstance(data, Data2D):
583        msg = "Incorrect type passed to retrieveData2d"
584        raise AttributeError(msg)
585
586    text = data.__str__()
587    text += 'Data Min Max:\n'
588    text += 'I_min = %s\n' % min(data.data)
589    text += 'I_max = %s\n\n' % max(data.data)
590    text += 'Data (First 2501) Points:\n'
591    text += 'Data columns include err(I).\n'
592    text += 'ASCII data starts here.\n'
593    text += "<index> \t<Qx> \t<Qy> \t<I> \t<dI> \t<dQparal> \t<dQperp>\n"
594    di_val = 0.0
595    dx_val = 0.0
596    dy_val = 0.0
597    len_data = len(data.qx_data)
598    for index in range(0, len_data):
599        x_val = data.qx_data[index]
600        y_val = data.qy_data[index]
601        i_val = data.data[index]
602        if data.err_data is not None:
603            di_val = data.err_data[index]
604        if data.dqx_data is not None:
605            dx_val = data.dqx_data[index]
606        if data.dqy_data is not None:
607            dy_val = data.dqy_data[index]
608
609        text += "%s \t%s \t%s \t%s \t%s \t%s \t%s\n" % (index,
610                                                        x_val,
611                                                        y_val,
612                                                        i_val,
613                                                        di_val,
614                                                        dx_val,
615                                                        dy_val)
616        # Takes too long time for typical data2d: Break here
617        if index >= 2500:
618            text += ".............\n"
619            break
620
621    return text
622
623def onTXTSave(data, path):
624    """
625    Save file as formatted txt
626    """
627    with open(path,'w') as out:
628        has_errors = True
629        if data.dy is None or not data.dy.any():
630            has_errors = False
631        # Sanity check
632        if has_errors:
633            try:
634                if len(data.y) != len(data.dy):
635                    has_errors = False
636            except:
637                has_errors = False
638        if has_errors:
639            if data.dx is not None and data.dx.any():
640                out.write("<X>   <Y>   <dY>   <dX>\n")
641            else:
642                out.write("<X>   <Y>   <dY>\n")
643        else:
644            out.write("<X>   <Y>\n")
645
646        for i in range(len(data.x)):
647            if has_errors:
648                if data.dx is not None and data.dx.any():
649                    if  data.dx[i] is not None:
650                        out.write("%g  %g  %g  %g\n" % (data.x[i],
651                                                        data.y[i],
652                                                        data.dy[i],
653                                                        data.dx[i]))
654                    else:
655                        out.write("%g  %g  %g\n" % (data.x[i],
656                                                    data.y[i],
657                                                    data.dy[i]))
658                else:
659                    out.write("%g  %g  %g\n" % (data.x[i],
660                                                data.y[i],
661                                                data.dy[i]))
662            else:
663                out.write("%g  %g\n" % (data.x[i],
664                                        data.y[i]))
665
666def saveData1D(data):
667    """
668    Save 1D data points
669    """
670    default_name = os.path.basename(data.filename)
671    default_name, extension = os.path.splitext(default_name)
672    if not extension:
673        extension = ".txt"
674    default_name += "_out" + extension
675
676    wildcard = "Text files (*.txt);;"\
677                "CanSAS 1D files(*.xml)"
678    kwargs = {
679        'caption'   : 'Save As',
680        'directory' : default_name,
681        'filter'    : wildcard,
682        'parent'    : None,
683    }
684    # Query user for filename.
685    filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
686    filename = filename_tuple[0]
687
688    # User cancelled.
689    if not filename:
690        return
691
692    #Instantiate a loader
693    loader = Loader()
694    if os.path.splitext(filename)[1].lower() == ".txt":
695        onTXTSave(data, filename)
696    if os.path.splitext(filename)[1].lower() == ".xml":
697        loader.save(filename, data, ".xml")
698
699def saveData2D(data):
700    """
701    Save data2d dialog
702    """
703    default_name = os.path.basename(data.filename)
704    default_name, _ = os.path.splitext(default_name)
705    ext_format = ".dat"
706    default_name += "_out" + ext_format
707
708    wildcard = "IGOR/DAT 2D file in Q_map (*.dat)"
709    kwargs = {
710        'caption'   : 'Save As',
711        'directory' : default_name,
712        'filter'    : wildcard,
713        'parent'    : None,
714    }
715    # Query user for filename.
716    filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
717    filename = filename_tuple[0]
718
719    # User cancelled.
720    if not filename:
721        return
722
723    #Instantiate a loader
724    loader = Loader()
725
726    if os.path.splitext(filename)[1].lower() == ext_format:
727        loader.save(filename, data, ext_format)
728
729class FormulaValidator(QtGui.QValidator):
730    def __init__(self, parent=None):
731        super(FormulaValidator, self).__init__(parent)
732 
733    def validate(self, input, pos):
734
735        self._setStyleSheet("")
736        return QtGui.QValidator.Acceptable, pos
737
738        #try:
739        #    Formula(str(input))
740        #    self._setStyleSheet("")
741        #    return QtGui.QValidator.Acceptable, pos
742
743        #except Exception as e:
744        #    self._setStyleSheet("background-color:pink;")
745        #    return QtGui.QValidator.Intermediate, pos
746
747    def _setStyleSheet(self, value):
748        try:
749            if self.parent():
750                self.parent().setStyleSheet(value)
751        except:
752            pass
753
754def xyTransform(data, xLabel="", yLabel=""):
755    """
756    Transforms x and y in View and set the scale
757    """
758    # Changing the scale might be incompatible with
759    # currently displayed data (for instance, going
760    # from ln to log when all plotted values have
761    # negative natural logs).
762    # Go linear and only change the scale at the end.
763    xscale = 'linear'
764    yscale = 'linear'
765    # Local data is either 1D or 2D
766    if data.id == 'fit':
767        return
768
769    # make sure we have some function to operate on
770    if xLabel is None:
771        xLabel = 'log10(x)'
772    if yLabel is None:
773        yLabel = 'log10(y)'
774
775    # control axis labels from the panel itself
776    yname, yunits = data.get_yaxis()
777    xname, xunits = data.get_xaxis()
778
779    # Goes through all possible scales
780    # self.x_label is already wrapped with Latex "$", so using the argument
781
782    # X
783    if xLabel == "x":
784        data.transformX(DataTransform.toX, DataTransform.errToX)
785        xLabel = "%s(%s)" % (xname, xunits)
786    if xLabel == "x^(2)":
787        data.transformX(DataTransform.toX2, DataTransform.errToX2)
788        xunits = convertUnit(2, xunits)
789        xLabel = "%s^{2}(%s)" % (xname, xunits)
790    if xLabel == "x^(4)":
791        data.transformX(DataTransform.toX4, DataTransform.errToX4)
792        xunits = convertUnit(4, xunits)
793        xLabel = "%s^{4}(%s)" % (xname, xunits)
794    if xLabel == "ln(x)":
795        data.transformX(DataTransform.toLogX, DataTransform.errToLogX)
796        xLabel = "\ln{(%s)}(%s)" % (xname, xunits)
797    if xLabel == "log10(x)":
798        data.transformX(DataTransform.toX_pos, DataTransform.errToX_pos)
799        xscale = 'log'
800        xLabel = "%s(%s)" % (xname, xunits)
801    if xLabel == "log10(x^(4))":
802        data.transformX(DataTransform.toX4, DataTransform.errToX4)
803        xunits = convertUnit(4, xunits)
804        xLabel = "%s^{4}(%s)" % (xname, xunits)
805        xscale = 'log'
806
807    # Y
808    if yLabel == "ln(y)":
809        data.transformY(DataTransform.toLogX, DataTransform.errToLogX)
810        yLabel = "\ln{(%s)}(%s)" % (yname, yunits)
811    if yLabel == "y":
812        data.transformY(DataTransform.toX, DataTransform.errToX)
813        yLabel = "%s(%s)" % (yname, yunits)
814    if yLabel == "log10(y)":
815        data.transformY(DataTransform.toX_pos, DataTransform.errToX_pos)
816        yscale = 'log'
817        yLabel = "%s(%s)" % (yname, yunits)
818    if yLabel == "y^(2)":
819        data.transformY(DataTransform.toX2, DataTransform.errToX2)
820        yunits = convertUnit(2, yunits)
821        yLabel = "%s^{2}(%s)" % (yname, yunits)
822    if yLabel == "1/y":
823        data.transformY(DataTransform.toOneOverX, DataTransform.errOneOverX)
824        yunits = convertUnit(-1, yunits)
825        yLabel = "1/%s(%s)" % (yname, yunits)
826    if yLabel == "y*x^(2)":
827        data.transformY(DataTransform.toYX2, DataTransform.errToYX2)
828        xunits = convertUnit(2, xunits)
829        yLabel = "%s \ \ %s^{2}(%s%s)" % (yname, xname, yunits, xunits)
830    if yLabel == "y*x^(4)":
831        data.transformY(DataTransform.toYX4, DataTransform.errToYX4)
832        xunits = convertUnit(4, xunits)
833        yLabel = "%s \ \ %s^{4}(%s%s)" % (yname, xname, yunits, xunits)
834    if yLabel == "1/sqrt(y)":
835        data.transformY(DataTransform.toOneOverSqrtX, DataTransform.errOneOverSqrtX)
836        yunits = convertUnit(-0.5, yunits)
837        yLabel = "1/\sqrt{%s}(%s)" % (yname, yunits)
838    if yLabel == "ln(y*x)":
839        data.transformY(DataTransform.toLogXY, DataTransform.errToLogXY)
840        yLabel = "\ln{(%s \ \ %s)}(%s%s)" % (yname, xname, yunits, xunits)
841    if yLabel == "ln(y*x^(2))":
842        data.transformY(DataTransform.toLogYX2, DataTransform.errToLogYX2)
843        xunits = convertUnit(2, xunits)
844        yLabel = "\ln (%s \ \ %s^{2})(%s%s)" % (yname, xname, yunits, xunits)
845    if yLabel == "ln(y*x^(4))":
846        data.transformY(DataTransform.toLogYX4, DataTransform.errToLogYX4)
847        xunits = convertUnit(4, xunits)
848        yLabel = "\ln (%s \ \ %s^{4})(%s%s)" % (yname, xname, yunits, xunits)
849    if yLabel == "log10(y*x^(4))":
850        data.transformY(DataTransform.toYX4, DataTransform.errToYX4)
851        xunits = convertUnit(4, xunits)
852        yscale = 'log'
853        yLabel = "%s \ \ %s^{4}(%s%s)" % (yname, xname, yunits, xunits)
854
855    # Perform the transformation of data in data1d->View
856    data.transformView()
857
858    return (xLabel, yLabel, xscale, yscale)
859
860def formatNumber(value, high=False):
861    """
862    Return a float in a standardized, human-readable formatted string.
863    This is used to output readable (e.g. x.xxxe-y) values to the panel.
864    """
865    try:
866        value = float(value)
867    except:
868        output = "NaN"
869        return output.lstrip().rstrip()
870
871    if high:
872        output = "%-7.5g" % value
873
874    else:
875        output = "%-5.3g" % value
876    return output.lstrip().rstrip()
877
878def replaceHTMLwithUTF8(html):
879    """
880    Replace some important HTML-encoded characters
881    with their UTF-8 equivalents
882    """
883    # Angstrom
884    html_out = html.replace("&#x212B;", "Å")
885    # infinity
886    html_out = html_out.replace("&#x221e;", "∞")
887    # +/-
888    html_out = html_out.replace("&#177;", "±")
889
890    return html_out
891
892def replaceHTMLwithASCII(html):
893    """
894    Replace some important HTML-encoded characters
895    with their ASCII equivalents
896    """
897    # Angstrom
898    html_out = html.replace("&#x212B;", "Ang")
899    # infinity
900    html_out = html_out.replace("&#x221e;", "inf")
901    # +/-
902    html_out = html_out.replace("&#177;", "+/-")
903
904    return html_out
905
906def convertUnitToUTF8(unit):
907    """
908    Convert ASCII unit display into UTF-8 symbol
909    """
910    if unit == "1/A":
911        return "Å<sup>-1</sup>"
912    elif unit == "1/cm":
913        return "cm<sup>-1</sup>"
914    elif unit == "Ang":
915        return "Å"
916    elif unit == "1e-6/Ang^2":
917        return "10<sup>-6</sup>/Å<sup>2</sup>"
918    elif unit == "inf":
919        return "∞"
920    elif unit == "-inf":
921        return "-∞"
922    else:
923        return unit
924
925def convertUnitToHTML(unit):
926    """
927    Convert ASCII unit display into well rendering HTML
928    """
929    if unit == "1/A":
930        return "&#x212B;<sup>-1</sup>"
931    elif unit == "1/cm":
932        return "cm<sup>-1</sup>"
933    elif unit == "Ang":
934        return "&#x212B;"
935    elif unit == "1e-6/Ang^2":
936        return "10<sup>-6</sup>/&#x212B;<sup>2</sup>"
937    elif unit == "inf":
938        return "&#x221e;"
939    elif unit == "-inf":
940        return "-&#x221e;"
941    else:
942        return unit
943
944def parseName(name, expression):
945    """
946    remove "_" in front of a name
947    """
948    if re.match(expression, name) is not None:
949        word = re.split(expression, name, 1)
950        for item in word:           
951            if item.lstrip().rstrip() != '':
952                return item
953    else:
954        return name
955
956def toDouble(value_string):
957    """
958    toFloat conversion which cares deeply about user's locale
959    """
960    # Holy shit this escalated quickly in Qt5.
961    # No more float() cast on general locales.
962    value = QtCore.QLocale().toFloat(value_string)
963    if value[1]:
964        return value[0]
965
966    # Try generic locale
967    value = QtCore.QLocale(QtCore.QLocale('en_US')).toFloat(value_string)
968    if value[1]:
969        return value[0]
970    else:
971        raise TypeError
972
973def findNextFilename(filename, directory):
974    """
975    Finds the next available (non-existing) name for 'filename' in 'directory'.
976    plugin.py -> plugin (n).py  - for first 'n' for which the file doesn't exist
977    """
978    basename, ext = os.path.splitext(filename)
979    # limit the number of copies
980    MAX_FILENAMES = 1000
981    # Start with (1)
982    number_ext = 1
983    proposed_filename = ""
984    found_filename = False
985    # Find the next available filename or exit if too many copies
986    while not found_filename or number_ext > MAX_FILENAMES:
987        proposed_filename = basename + " ("+str(number_ext)+")" + ext
988        if os.path.exists(os.path.join(directory, proposed_filename)):
989            number_ext += 1
990        else:
991            found_filename = True
992
993    return proposed_filename
994
995
996class DoubleValidator(QtGui.QDoubleValidator):
997    """
998    Allow only dots as decimal separator
999    """
1000    def validate(self, input, pos):
1001        """
1002        Return invalid for commas
1003        """
1004        if input is not None and ',' in input:
1005            return (QtGui.QValidator.Invalid, input, pos)
1006        return super(DoubleValidator, self).validate(input, pos)
1007
1008    def fixup(self, input):
1009        """
1010        Correct (remove) potential preexisting content
1011        """
1012        super(DoubleValidator, self).fixup(input)
1013        input = input.replace(",", "")
1014
1015def checkModel(path):
1016    """
1017    Check that the model save in file 'path' can run.
1018    """
1019    # The following return needs to be removed once
1020    # the unittest related changes in Sasmodels are commited
1021    # return True
1022    # try running the model
1023    from sasmodels.sasview_model import load_custom_model
1024    Model = load_custom_model(path)
1025    model = Model()
1026    q =  np.array([0.01, 0.1])
1027    _ = model.evalDistribution(q)
1028    qx, qy =  np.array([0.01, 0.01]), np.array([0.1, 0.1])
1029    _ = model.evalDistribution([qx, qy])
1030
1031    # check the model's unit tests run
1032    from sasmodels.model_test import run_one
1033    # TestSuite module in Qt5 now deletes tests in the suite after running,
1034    # so suite[0] in run_one() in sasmodels/model_test.py will contain [None] and
1035    # test.info.tests will raise.
1036    # Not sure how to change the behaviour here, most likely sasmodels will have to
1037    # be modified
1038    result = run_one(path)
1039
1040    return result
1041
1042
1043def enum(*sequential, **named):
1044    """Create an enumeration object from a list of strings"""
1045    enums = dict(zip(sequential, range(len(sequential))), **named)
1046    return type('Enum', (), enums)
Note: See TracBrowser for help on using the repository browser.