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

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

New category manager design

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