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

Last change on this file since e0da307 was 6da3e3d, checked in by Laura Forster <Awork@…>, 6 years ago

Merge branch 'ESS_GUI' of https://github.com/SasView/sasview into ESS_GUI

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