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

ESS_GUIESS_GUI_DocsESS_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 441a03f was 3b3b40b, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

Merging feature branches

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