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

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 3933ee9 was 3933ee9, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

standardise monospace font for code editors, QT console

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