diff --git a/nplab/instrument/__init__.py b/nplab/instrument/__init__.py index 906bbe01..5364632a 100644 --- a/nplab/instrument/__init__.py +++ b/nplab/instrument/__init__.py @@ -12,34 +12,46 @@ """ from builtins import str -from nplab.utils.thread_utils import locked_action_decorator, background_action_decorator +from nplab.utils.thread_utils import locked_action_decorator import nplab from weakref import WeakSet import nplab.utils.log from nplab.utils.array_with_attrs import ArrayWithAttrs from nplab.utils.show_gui_mixin import ShowGUIMixin +from nplab.ui.widgets.msgbox import prompt_box import logging from nplab.utils.log import create_logger import inspect import os import h5py import datetime +# <<<<<<< HEAD +import json +import numpy as np +import tempfile + + +# ======= from contextlib import contextmanager +# >>>>>>> 642d2633a1fc31a24d017cb97b8427f0125f9387 LOGGER = create_logger('Instrument') LOGGER.setLevel('INFO') + class Instrument(ShowGUIMixin): """Base class for all instrument-control classes. This class takes care of management of instruments, saving data, etc. """ __instances = None - metadata_property_names = () #"Tuple of names of properties that should be automatically saved as HDF5 metadata + metadata_property_names = () # tuple of names of properties that should be automatically saved as HDF5 metadata + config_property_names = () # tuple of names of properties that are saved and loaded for default configuration + _CONFIG_EXTENSION = '.json' def __init__(self): """Create an instrument object.""" super(Instrument, self).__init__() - Instrument.instances_set().add(self) #keep track of instances (should this be in __new__?) + Instrument.instances_set().add(self) # keep track of instances (should this be in __new__?) self._logger = logging.getLogger('Instrument.' + str(type(self)).split('.')[-1].split('\'')[0]) @classmethod @@ -60,7 +72,7 @@ def get_instance(cls, create=True, exceptions=True, *args, **kwargs): Usually returns the first available instance. """ instances = cls.get_instances() - if len(instances)>0: + if len(instances) > 0: return instances[0] else: if create: @@ -99,6 +111,7 @@ def create_dataset(cls, name, flush=True, *args, **kwargs): :param name: should be a noun describing what the reading is (image, spectrum, etc.) + :param flush: bool Other arguments are passed to `nplab.datafile.Group.create_dataset`. """ @@ -107,17 +120,17 @@ def create_dataset(cls, name, flush=True, *args, **kwargs): df = cls.get_root_data_folder() dset = df.create_dataset(name, *args, **kwargs) if 'data' in kwargs and flush: - dset.file.flush() #make sure it's in the file if we wrote data + dset.file.flush() # make sure it's in the file if we wrote data return dset - def log(self, message,level = 'info'): + def log(self, message, level='info'): """Save a log message to the current datafile. This is the preferred way to output debug/informational messages. They will be saved in the current HDF5 file and optionally shown in the nplab console. """ - nplab.utils.log.log(message, from_object=self,level = level) + nplab.utils.log.log(message, from_object=self, level=level) def get_metadata(self, property_names=[], @@ -177,31 +190,190 @@ def bundle_metadata(self, data, enable=True, **kwargs): else: return data - def open_config_file(self): - """Open the config file for the current spectrometer and return it, creating if it's not there""" - if not hasattr(self,'_config_file'): + def get_config(self, mode='named'): + """Returns the configuration dictionary + + :param mode: str. Either 'named' or 'all' + If 'named' it only iterates over self.config_property_names. + If 'all' it iterates over the whole __dir__, ignoring hidden attributes/methods, and attempts to get the + values. Currently only returns values if they are one of the following: + bool, dict, float, int, list, str, tuple, np.array + :return: + """ + configuration = dict() + if mode == 'named': + for name in self.config_property_names: + try: + configuration[name] = getattr(self, name) + except Exception as e: + self._logger.debug('Failed getting configuration for key: %s' % name) + elif mode == 'all': + for name in dir(self): + if not name.startswith('_'): # ignores hidden attributes/methods + try: + value = getattr(self, name) + if type(value) in [bool, dict, float, int, list, str, tuple, np.array]: + try: + # check whether value can be saved to JSON. Check is done here rather than in + # self.save_config because self.save_config dumps the whole config at once + with tempfile.TemporaryFile('w') as f: + json.dump(value, f) + configuration[name] = value + except Exception as e: + self._logger.info('Configuration value for key: %s cannot be saved to json' % name) + except Exception as e: + self._logger.debug('Failed getting configuration for key: %s' % name) + else: + raise ValueError("Unrecognised configuration mode: %s. Needs to be 'named' or 'all'" % mode) + return configuration + + def set_config(self, configuration): + """Sets attributes according to configuration + :param configuration: dict + :return: + """ + for key, value in configuration.items(): + try: + setattr(self, key, value) + except Exception as e: + self._logger.info('Configuration could not be set for: %s = %s' % (key, value)) + + config = property(get_config, set_config) + + def _config_filename(self, name=None, extension=None): + """Returns valid file path + + :param name: str. Can be just the filename, a filename with/out an extension, or a relative/absolute path + :param extension: str + :return: str. Absolute path + """ + if name is None: + name = 'config' # default name + if extension is None: + extension = self._CONFIG_EXTENSION # default extension. Can be changed in subclasses + # Ensure name has expected extension or adds it if not there + root, ext = os.path.splitext(name) + if not ext: + ext = extension + else: + assert ext == extension + filename = root + ext + + # Default location for configuration file is wherever the instance's Python definition is + if not os.path.isabs(filename): f = inspect.getfile(self.__class__) d = os.path.dirname(f) - self._config_file = nplab.datafile.DataFile(h5py.File(os.path.join(d, 'config.h5')), mode='a') - self._config_file.attrs['date'] = datetime.datetime.now().strftime("%H:%M %d/%m/%y") - return self._config_file - - config_file = property(open_config_file) - - def update_config(self, name, data, attrs= None): - """Update the configuration file for this spectrometer. - - A file is created in the nplab directory that holds configuration - data for the spectrometer, including reference/background. This - function allows values to be stored in that file.""" - f = self.config_file - if name in f: - try: del f[name] - except: - f[name][...] = data - f.flush() +# <<<<<<< HEAD + filename = os.path.join(d, filename) + return filename + + def save_config(self, config=None, filename=None, mode='named'): + """Saves instrument configuration to file + :param config: dict or None + :param filename: str. Passed to self._config_filename + :param mode: str. If config dictionary not given, passed to self.get_config + """ + # Get filename + filename = self._config_filename(filename) + if config is None: + config = self.get_config(mode) + + # If the file exists, checks whether the user wants to overwrite it + if os.path.exists(filename): + reply = prompt_box(text='That configuration file exists. Do you want to overwrite it?', default=filename) + if not reply: + return + filename = reply + + _, ext = os.path.splitext(filename) + if ext == '.json': + # Dumps the configuration dictionary to a JSON + with open(filename, 'w') as config_file: + json.dump(config, config_file, indent=4) + elif ext == '.h5': + with h5py.File(filename, 'w') as dfile: + dfile.attrs['date'] = datetime.datetime.now().strftime("%H:%M %d/%m/%y") + for name, value in config.items(): + try: + dfile.create_dataset(name, data=value) + except Exception as e: + self._logger.info('Configuration value for key: %s cannot be saved to HDF5' % name) + else: + raise ValueError('Unrecognised extension: %s' % ext) + + def load_config(self, filename=None): + """Loads configuration from file + :param filename: str. Passed to self._config_filename + :return: dict + """ + # Get filename + filename = self._config_filename(filename) + _, ext = os.path.splitext(filename) + + # Loads and sets the configuration + if ext == '.json': + with open(filename, 'r') as config_file: + config = json.load(config_file) + elif ext == '.h5': + with h5py.File(filename, 'r') as dfile: + config = dict() + for key, value in dfile.items(): + config[key] = value + else: + raise ValueError('Unrecognised extension: %s' % ext) + return config + + config_file = property(load_config, save_config) + + def update_config(self, name, data, filename=None): + """Edits configuration file + :param name: str + :param data: anything that can be parsed by JSON or directly saved to HDF5 + :param filename: str. Passed to self._config_filename + """ + # Get filename + filename = self._config_filename(filename) + + _, ext = os.path.splitext(filename) + if ext == '.json': + # Need to read the whole JSON first, modify it, and then re-write the file + with open(filename, 'a') as config_file: + try: + config = json.load(config_file) + except: # would fail if config_file is empty + config = dict() + config[name] = data + config_file.seek(0) + json.dump(config, config_file) + config_file.truncate() + elif ext == '.h5': + with h5py.File(filename, 'a') as f: + if name in f: + del f[name] + f.create_dataset(name, data=data) else: - f.create_dataset(name, data=data ,attrs = attrs) + raise ValueError('Unrecognised extension: %s' % ext) +# ======= +# self._config_file = nplab.datafile.DataFile(h5py.File(os.path.join(d, 'config.h5')), mode='a') +# self._config_file.attrs['date'] = datetime.datetime.now().strftime("%H:%M %d/%m/%y") +# return self._config_file +# +# config_file = property(open_config_file) +# +# def update_config(self, name, data, attrs= None): +# """Update the configuration file for this spectrometer. +# +# A file is created in the nplab directory that holds configuration +# data for the spectrometer, including reference/background. This +# function allows values to be stored in that file.""" +# f = self.config_file +# if name in f: +# try: del f[name] +# except: +# f[name][...] = data +# f.flush() +# else: +# f.create_dataset(name, data=data ,attrs = attrs) @contextmanager def temporarily_set(self, **kwargs): @@ -223,3 +395,4 @@ def temporarily_set(self, **kwargs): finally: for key, value in original_settings.items(): setattr(self, key, value) +# >>>>>>> 642d2633a1fc31a24d017cb97b8427f0125f9387 diff --git a/nplab/instrument/camera/Andor/__init__.py b/nplab/instrument/camera/Andor/__init__.py index 101172c0..30c81997 100644 --- a/nplab/instrument/camera/Andor/__init__.py +++ b/nplab/instrument/camera/Andor/__init__.py @@ -12,12 +12,16 @@ from nplab.ui.ui_tools import UiTools from weakref import WeakSet import time +import inspect class Andor(CameraRoiScale, AndorBase): metadata_property_names = ('Exposure', 'x_axis', 'CurrentTemperature',) + config_property_names = CameraRoiScale.config_property_names + ('ReadMode', 'AcquisitionMode', 'TriggerMode', + 'Exposure', 'Shutter', 'SetTemperature', + 'CoolerMode', 'FanMode', 'cooler') - def __init__(self, settings_filepath=None, camera_index=None, **kwargs): + def __init__(self, settings_filepath=None, camera_index=None, config_file=None, **kwargs): super(Andor, self).__init__() self.start(camera_index) @@ -30,6 +34,15 @@ def __init__(self, settings_filepath=None, camera_index=None, **kwargs): self.isAborted = False self.detector_shape = self.DetectorShape # passing the Andor parameter to the CameraRoiScale class + if config_file is not None: + self.config = self.load_config(config_file) + else: + filename = 'default_config' + if not os.path.isabs(filename): + f = inspect.getfile(Andor) + d = os.path.dirname(f) + filename = os.path.join(d, filename) + self.config = self.load_config(filename) def __del__(self): # Need to explicitly call this method so that the shutdown procedure is followed correctly diff --git a/nplab/instrument/camera/Andor/andor_sdk.py b/nplab/instrument/camera/Andor/andor_sdk.py index 4d8a73ff..ee8755fa 100644 --- a/nplab/instrument/camera/Andor/andor_sdk.py +++ b/nplab/instrument/camera/Andor/andor_sdk.py @@ -461,22 +461,32 @@ def initialize(self): if self.CurrentCamera not in self._initialized_cameras: self._initialized_cameras += [self.CurrentCamera] self._dll_wrapper('Initialize', outputs=(c_char(),)) - self.channel = 0 - self.set_andor_parameter('ReadMode', 4) - self.set_andor_parameter('AcquisitionMode', 1) - self.set_andor_parameter('TriggerMode', 0) - self.set_andor_parameter('Exposure', 1) +# <<<<<<< HEAD detector_shape = self.get_andor_parameter('DetectorShape') self.set_andor_parameter('Image', 1, 1, 1, detector_shape[0], 1, detector_shape[1]) - self.set_andor_parameter('Shutter', 1, 0, 0, 0) - self.set_andor_parameter('SetTemperature', -90) - self.set_andor_parameter('CoolerMode', 0) - self.set_andor_parameter('FanMode', 0) - try: - self.set_andor_parameter('OutAmp', 1) # This means EMCCD off - this is the default mode - except AndorWarning: + if self.get_andor_parameter('NumAmp') > 1: + self.set_andor_parameter('OutAmp', 1) # This means EMCCD off - which is default just for safety + else: +# ======= +# self.channel = 0 +# self.set_andor_parameter('ReadMode', 4) +# self.set_andor_parameter('AcquisitionMode', 1) +# self.set_andor_parameter('TriggerMode', 0) +# self.set_andor_parameter('Exposure', 1) +# detector_shape = self.get_andor_parameter('DetectorShape') +# self.set_andor_parameter('Image', 1, 1, 1, detector_shape[0], 1, detector_shape[1]) +# self.set_andor_parameter('Shutter', 1, 0, 0, 0) +# self.set_andor_parameter('SetTemperature', -90) +# self.set_andor_parameter('CoolerMode', 0) +# self.set_andor_parameter('FanMode', 0) +# try: +# self.set_andor_parameter('OutAmp', 1) # This means EMCCD off - this is the default mode +# except AndorWarning: +# >>>>>>> 642d2633a1fc31a24d017cb97b8427f0125f9387 self.set_andor_parameter('OutAmp', 0) - self.cooler = 1 + if self.get_andor_parameter('NumADChannels') > 1: + self._logger.info('Andor has more than one ADC channel. Default settings is 0') + self.set_andor_parameter('ADChannel', 0) @locked_action def capture(self): @@ -668,8 +678,7 @@ def load_params_from_file(self, filepath=None): AvailableCameras=dict(Get=dict(cmdName='GetAvailableCameras', Outputs=(c_uint,)), value=None), CurrentCamera=dict(Get=dict(cmdName='GetCurrentCamera', Outputs=(c_uint,)), Set=dict(cmdName='SetCurrentCamera', Inputs=(c_uint,))), - channel=dict(value=0), - CameraHandle=dict(Get=dict(cmdName='GetCameraHandle', Outputs=(c_uint,), Input_params=(('camera_index', c_int), ))), + CameraHandle=dict(Get=dict(cmdName='GetCameraHandle', Outputs=(c_uint,), Input_params=(('camera_index', c_int),))), PixelSize=dict(Get=dict(cmdName='GetPixelSize', Outputs=(c_float, c_float))), SoftwareWaitBetweenCaptures=dict(value=0), SoftwareVersion=dict(Get=dict(cmdName='GetSoftwareVersion', Outputs=(c_int, c_int, c_int, c_int, c_int, c_int))), @@ -714,20 +723,21 @@ def load_params_from_file(self, filepath=None): Spool=dict(Set=dict(cmdName='SetSpool', Inputs=(c_int, c_int, c_char, c_int)), value=None), NumVSSpeed=dict(Get=dict(cmdName='GetNumberVSSpeeds', Outputs=(c_int,)), value=None), NumHSSpeed=dict(Get=dict(cmdName='GetNumberHSSpeeds', Outputs=(c_int,), - Input_params=(('channel', c_int), ('OutAmp', c_int))), value=None), + Input_params=(('ADChannel', c_int), ('OutAmp', c_int))), value=None), VSSpeed=dict(Set=dict(cmdName='SetVSSpeed', Inputs=(c_int,)), Get_from_prop='VSSpeeds'), VSSpeeds=dict(Get=dict(cmdName='GetVSSpeed', Inputs=(c_int,), Outputs=(c_float,), Iterator='NumVSSpeed')), # why no work? HSSpeed=dict(Set=dict(cmdName='SetHSSpeed', Inputs=(c_int,), Input_params=(('OutAmp', c_int),)), Get_from_prop='HSSpeeds'), HSSpeeds=dict(Get=dict(cmdName='GetHSSpeed', Inputs=(c_int,) * 2, Iterator='NumHSSpeed', Outputs=(c_float,), - Input_params=(('channel', c_int), ('OutAmp', c_int),))), + Input_params=(('ADChannel', c_int), ('OutAmp', c_int),))), NumPreAmp=dict(Get=dict(cmdName='GetNumberPreAmpGains', Outputs=(c_int,))), PreAmpGains=dict(Get=dict(cmdName='GetPreAmpGain', Inputs=(c_int,), Outputs=(c_float,), Iterator='NumPreAmp')), PreAmpGain=dict(Set=dict(cmdName='SetPreAmpGain', Inputs=(c_int,)), Get_from_prop='PreAmpGains'), NumADChannels=dict(Get=dict(cmdName='GetNumberADChannels', Outputs=(c_int,))), ADChannel=dict(Set=dict(cmdName='SetADChannel', Inputs=(c_int,))), - BitDepth=dict(Get=dict(cmdName='GetBitDepth', Inputs=(c_int,), Outputs=(c_int,), Iterator='NumADChannels')) + BitDepth=dict(Get=dict(cmdName='GetBitDepth', Inputs=(c_int,), Outputs=(c_int,), Iterator='NumADChannels')), + NumAmp=dict(Get=dict(cmdName='GetNumberAmp', Outputs=(c_int,))) ) for param_name in parameters: if param_name != 'Image': diff --git a/nplab/instrument/camera/Andor/default_config.json b/nplab/instrument/camera/Andor/default_config.json new file mode 100644 index 00000000..afbf5550 --- /dev/null +++ b/nplab/instrument/camera/Andor/default_config.json @@ -0,0 +1 @@ +{"ReadMode": 4, "AcquisitionMode": 1, "TriggerMode": 0, "Exposure": 0.01, "Shutter": [1, 0, 1, 1], "SetTemperature": -90, "CoolerMode": 0, "FanMode": 0, "cooler": 0} \ No newline at end of file diff --git a/nplab/instrument/ccd/__init__.py b/nplab/instrument/ccd/__init__.py index e146dbbd..12627e47 100644 --- a/nplab/instrument/ccd/__init__.py +++ b/nplab/instrument/ccd/__init__.py @@ -12,45 +12,18 @@ class CCD(Instrument): + _CONFIG_EXTENSION = '.h5' + def __init__(self): super(CCD, self).__init__() self._wavelengths = None self.reference = None self.background = None - self._config_file = None self.latest_image = None - def __del__(self): - try: - self._config_file.close() - except AttributeError: - pass # if it's not present, we get an exception - which doesn't matter. - def read_image(self): raise NotImplementedError - @property - def config_file(self): - """ - Open the config file for the current spectrometer and return it, creating if it's not - there. - """ - if self._config_file is None: - f = inspect.getfile(self.__class__) - d = os.path.dirname(f) - self._config_file = h5py.File(os.path.join(d, 'config.h5')) - self._config_file.attrs['date'] = datetime.datetime.now().strftime("%H:%M %d/%m/%y") - return self._config_file - - def update_config(self, name, data): - f = self.config_file - if name not in f: - f.create_dataset(name, data=data) - else: - dset = f[name] - dset[:] = data - f.flush() - def read_background(self): """Acquire a new spectrum and use it as a background measurement.""" self.background = self.read_image() diff --git a/nplab/instrument/ccd/pixis.py b/nplab/instrument/ccd/pixis.py index 59ae54b0..d9d61964 100755 --- a/nplab/instrument/ccd/pixis.py +++ b/nplab/instrument/ccd/pixis.py @@ -29,6 +29,8 @@ def __init__(self, msg): class Pixis256E(CCD,Camera): + _CONFIG_EXTENSION = '.h5' + def __init__(self): super(Pixis256E, self).__init__() try: diff --git a/nplab/instrument/spectrometer/__init__.py b/nplab/instrument/spectrometer/__init__.py index 34bac528..d686ec72 100644 --- a/nplab/instrument/spectrometer/__init__.py +++ b/nplab/instrument/spectrometer/__init__.py @@ -42,7 +42,9 @@ class Spectrometer(Instrument): variable_int_enabled = DumbNotifiedProperty(False) filename = DumbNotifiedProperty("spectrum") + _CONFIG_EXTENSION = '.h5' dark = False + def __init__(self): super(Spectrometer, self).__init__() self._model_name = None @@ -60,7 +62,6 @@ def __init__(self): self.averaging_enabled = False self.spectra_deque = deque(maxlen = 1) self.absorption_enabled = False - self._config_file = None self.stored_references = {} self.stored_backgrounds = {} @@ -72,38 +73,6 @@ def __init__(self): self.delay = 0 self.time_series_name = 'time_series_%d' - - def __del__(self): - try: - self._config_file.close() - except AttributeError: - pass #if it's not present, we get an exception - which doesn't matter. - - def open_config_file(self): - """Open the config file for the current spectrometer and return it, creating if it's not there""" - if self._config_file is None: - f = inspect.getfile(self.__class__) - d = os.path.dirname(f) - self._config_file = DataFile(h5py.File(os.path.join(d, 'config.h5'))) - self._config_file.attrs['date'] = datetime.datetime.now().strftime("%H:%M %d/%m/%y") - return self._config_file - - config_file = property(open_config_file) - - def update_config(self, name, data, attrs= None): - """Update the configuration file for this spectrometer. - - A file is created in the nplab directory that holds configuration - data for the spectrometer, including reference/background. This - function allows values to be stored in that file.""" - f = self.config_file - if name not in f: - f.create_dataset(name, data=data ,attrs = attrs) - else: - dset = f[name] - dset[...] = data - f.flush() - def get_model_name(self): """The model name of the spectrometer.""" if self._model_name is None: diff --git a/nplab/instrument/spectrometer/seabreeze.py b/nplab/instrument/spectrometer/seabreeze.py index a31facac..09abf40f 100644 --- a/nplab/instrument/spectrometer/seabreeze.py +++ b/nplab/instrument/spectrometer/seabreeze.py @@ -203,15 +203,23 @@ def _close(self, force=False): check_error(e) self._isOpen = False - def open_config_file(self): - if self._config_file is None: - f = inspect.getfile(self.__class__) - d = os.path.dirname(f) - self._config_file = DataFile(h5py.File(os.path.join(d, self.model_name+'_'+self.serial_number+'_config.h5'), 'a')) - self._config_file.attrs['date'] = datetime.datetime.now().strftime("%H:%M %d/%m/%y") - return self._config_file - - config_file = property(open_config_file) +# <<<<<<< HEAD + def _config_filename(self, name=None, extension=None): + if name is None: + name = self.model_name+'_'+self.serial_number+'_config' + return super(OceanOpticsSpectrometer, self)._config_filename(name, extension) + +# ======= +# def open_config_file(self): +# if self._config_file is None: +# f = inspect.getfile(self.__class__) +# d = os.path.dirname(f) +# self._config_file = DataFile(h5py.File(os.path.join(d, self.model_name+'_'+self.serial_number+'_config.h5'), 'a')) +# self._config_file.attrs['date'] = datetime.datetime.now().strftime("%H:%M %d/%m/%y") +# return self._config_file +# +# config_file = property(open_config_file) +# >>>>>>> 642d2633a1fc31a24d017cb97b8427f0125f9387 def get_API_version(self): N = 32 # make a buffer for the DLL to return a string into s = ctypes.create_string_buffer(N) diff --git a/nplab/ui/widgets/msgbox.py b/nplab/ui/widgets/msgbox.py new file mode 100644 index 00000000..3e36a03b --- /dev/null +++ b/nplab/ui/widgets/msgbox.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +from nplab.utils.gui import QtWidgets, get_qt_app +from functools import reduce +import numpy as np + + +def button_box(text="Message box pop up window", title="NpLab button_box", buttons=('Ok', 'Cancel')): + app = get_qt_app() + msgBox = QtWidgets.QMessageBox() + msgBox.setIcon(QtWidgets.QMessageBox.Information) + msgBox.setText(text) + msgBox.setWindowTitle(title) + bttns = [getattr(QtWidgets.QMessageBox, b) for b in buttons] + msgBox.setStandardButtons(reduce(lambda x, y: x | y, bttns)) + + return_button = msgBox.exec() + button_index = np.argwhere(return_button == np.array(bttns))[0][0] + return button_index + + +def prompt_box(text='Enter text:', title="NpLab prompt_box", default='', widget=None, ): + app = get_qt_app() + if widget is None: + widget = QtWidgets.QInputDialog() + widget.setInputMode(QtWidgets.QInputDialog.TextInput) + widget.setWindowTitle(title) + widget.setLabelText(text) + widget.setTextValue(default) + if widget.exec_() == QtWidgets.QDialog.Accepted: + returnValue = widget.textValue() + else: + returnValue = False + widget.deleteLater() + return returnValue + else: + text, ok = QtWidgets.QInputDialog().getText(widget, title, text, text=default) + if ok: + return text + else: + return ok