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

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

Minor modifications in response to requests during demo session

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