def _get_new_filename(self): conf = get_acqui_conf() return os.path.join( conf.last_path, # u"%s%s" % (time.strftime("%Y%m%d-%H%M%S"), conf.last_extension) u"%s.tiff" % (time.strftime("%Y%m%d-%H%M%S"),) )
def __init__(self, tab_data, main_frame, settings_controller, roa, vas): """ tab_data (MicroscopyGUIData): the representation of the microscope GUI main_frame: (wx.Frame): the frame which contains the 4 viewports settings_controller (SettingsController) roa (VA): VA of the ROA vas (list of VAs): all the VAs which might affect acquisition (time) """ self._tab_data_model = tab_data self._main_data_model = tab_data.main self._main_frame = main_frame self._roa = roa self._vas = vas # For file selection self.conf = conf.get_acqui_conf() # for saving/restoring the settings self._settings_controller = settings_controller self._orig_settings = {} # Entry -> value to restore # TODO: this should be the date at which the user presses the acquire # button (or when the last settings were changed)! # At least, we must ensure it's a new date after the acquisition # is done. # Filename to save the acquisition self.filename = model.StringVA(self._get_default_filename()) self.filename.subscribe(self._onFilename, init=True) # For acquisition # a ProgressiveFuture if the acquisition is going on self.btn_acquire = self._main_frame.btn_sparc_acquire self.btn_change_file = self._main_frame.btn_sparc_change_file self.btn_cancel = self._main_frame.btn_sparc_cancel self.acq_future = None self.gauge_acq = self._main_frame.gauge_sparc_acq self.lbl_acqestimate = self._main_frame.lbl_sparc_acq_estimate self._acq_future_connector = None # TODO: share an executor with the whole GUI. self._executor = futures.ThreadPoolExecutor(max_workers=2) # Link buttons self.btn_acquire.Bind(wx.EVT_BUTTON, self.on_acquisition) self.btn_change_file.Bind(wx.EVT_BUTTON, self.on_change_file) self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_cancel) self.gauge_acq.Hide() self._main_frame.Layout() # TODO: we need to be informed if the user closes suddenly the window # self.Bind(wx.EVT_CLOSE, self.on_close) # Event binding pub.subscribe(self.on_setting_change, 'setting.changed') # TODO We should also listen to repetition, in case it's modified after we've # received the new ROI. Or maybe always compute acquisition time a bit delayed? for va in vas: va.subscribe(self.onAnyVA) roa.subscribe(self.onROA, init=True)
def _get_new_filename(self): conf = get_acqui_conf() # Use TIFF by default, as it's a little bit more user-friendly for simple # coloured images. return os.path.join( conf.last_path, u"%s%s" % (time.strftime("%Y%m%d-%H%M%S"), ".tiff") )
def __init__(self, tab_data, tab_panel, streambar_controller): """ tab_data (MicroscopyGUIData): the representation of the microscope GUI tab_panel: (wx.Frame): the frame which contains the 4 viewports stream_ctrl (StreamBarController): controller to pause/resume the streams """ self._tab_data_model = tab_data self._main_data_model = tab_data.main self._tab_panel = tab_panel self._streambar_controller = streambar_controller # For file selection self.conf = conf.get_acqui_conf() # TODO: this should be the date at which the user presses the acquire # button (or when the last settings were changed)! # At least, we must ensure it's a new date after the acquisition # is done. # Filename to save the acquisition self.filename = model.StringVA(self._get_default_filename()) self.filename.subscribe(self._onFilename, init=True) # For acquisition # a ProgressiveFuture if the acquisition is going on self.btn_acquire = self._tab_panel.btn_sparc_acquire self.btn_change_file = self._tab_panel.btn_sparc_change_file self.btn_cancel = self._tab_panel.btn_sparc_cancel self.acq_future = None self.gauge_acq = self._tab_panel.gauge_sparc_acq self.lbl_acqestimate = self._tab_panel.lbl_sparc_acq_estimate self._acq_future_connector = None self._stream_paused = () # TODO: share an executor with the whole GUI. self._executor = futures.ThreadPoolExecutor(max_workers=2) # Link buttons self.btn_acquire.Bind(wx.EVT_BUTTON, self.on_acquisition) self.btn_change_file.Bind(wx.EVT_BUTTON, self.on_change_file) self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_cancel) self.gauge_acq.Hide() self._tab_panel.Parent.Layout() # TODO: we need to be informed if the user closes suddenly the window # self.Bind(wx.EVT_CLOSE, self.on_close) # Listen to change of streams to update the acquisition time self._prev_streams = set() # set of streams already listened to tab_data.streams.subscribe(self._onStreams, init=True) # also listen to .semStream, which is not in .streams for va in self._get_settings_vas(tab_data.semStream): va.subscribe(self._onAnyVA) self._roa = tab_data.semStream.roi self._roa.subscribe(self._onROA, init=True)
def _get_snapshot_info(self, dialog=False): config = conf.get_acqui_conf() tab, filepath, exporter = self._main_data_model.tab.value, None, None if dialog: format_info = get_available_formats() wildcards, formats = formats_to_wildcards(format_info) # The default file name should be empty because otherwise the # dialog will add an extension that won't change when the user # selects a different file type in the dialog. dlg = wx.FileDialog(self._main_frame, "Save Snapshot", config.last_path, "", wildcard=wildcards, style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) # Select the last format used try: idx = formats.index(config.last_format) except ValueError: idx = 0 dlg.SetFilterIndex(idx) if dlg.ShowModal() == wx.ID_OK: path = dlg.GetPath() fmt = formats[dlg.GetFilterIndex()] extension = format_info[fmt][0] # Prevent double extensions when an old file is selected filepath, _ = os.path.splitext(path) filepath = filepath + extension config.last_path = os.path.dirname(path) config.last_format = fmt config.last_extension = extension config.write() exporter = dataio.get_exporter(config.last_format) dlg.Destroy() else: extension = config.last_extension dirname = get_picture_folder() basename = time.strftime("%Y%m%d-%H%M%S", time.localtime()) filepath = os.path.join(dirname, basename + extension) exporter = dataio.get_exporter(config.last_format) if os.path.exists(filepath): msg = "File '%s' already exists, cancelling snapshot" logging.warning(msg, filepath) tab, filepath, exporter = None, None, None return tab, filepath, exporter
def add_file_btn(self, label, value=None, tooltip=None, clear=None, style=wx.FD_OPEN): config = guiconf.get_acqui_conf() lbl_ctrl, value_ctrl = self.panel.add_file_button(label, value or config.last_path, clear, style) if tooltip is not None: lbl_ctrl.SetToolTipString(tooltip) value_ctrl.SetToolTipString(tooltip) # Add the corresponding setting entry ne = SettingEntry(name=label, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl) self.entries.append(ne) return ne
def _on_filename(self, fn): # Make the name "fn" -> "fn-XXXXXX.ext" bn, ext = os.path.splitext(fn) self._fntmpl = bn + "-%06d" + ext if not ext.endswith(".tiff"): logging.warning("Only TIFF format is recommended to use") # Store the directory so that next filename is in the same place conf = get_acqui_conf() p, bn = os.path.split(fn) if p: conf.last_path = p
def open_image(self, dlg): tab = self.main_app.main_data.getTabByName("analysis") tab_data = tab.tab_data_model fi = tab_data.acq_fileinfo.value if fi and fi.file_name: path, _ = os.path.split(fi.file_name) else: config = get_acqui_conf() path = config.last_path # Find the available formats (and corresponding extensions) formats_to_ext = dataio.get_available_formats(os.O_RDONLY) wildcards, formats = guiutil.formats_to_wildcards(formats_to_ext, include_all=True) dialog = wx.FileDialog(dlg, message="Choose a file to load", defaultDir=path, defaultFile="", style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST, wildcard=wildcards) # Show the dialog and check whether is was accepted or cancelled if dialog.ShowModal() != wx.ID_OK: return None # Detect the format to use filename = dialog.GetPath() data = udataio.open_acquisition(filename)[0] try: data = self._ensureGrayscale(data) except ValueError as ex: box = wx.MessageDialog(dlg, str(ex), "Failed to open image", wx.OK | wx.ICON_STOP) box.ShowModal() box.Destroy() return None self.crop_top.range = (0, data.shape[0] // 2) self.crop_bottom.range = (0, data.shape[0] // 2) self.crop_left.range = (0, data.shape[1] // 2) self.crop_right.range = (0, data.shape[1] // 2) data.metadata[model.MD_POS] = (0, 0) data.metadata[model.MD_PIXEL_SIZE] = (1e-9, 1e-9) basename = os.path.splitext(os.path.split(filename)[1])[0] return stream.StaticSEMStream(basename, data)
def _get_snapshot_info(self, dialog=False): config = conf.get_acqui_conf() tab, filepath, exporter = self._main_data_model.tab.value, None, None extension = config.last_extension basename = time.strftime("%Y%m%d-%H%M%S", time.localtime()) if dialog: filepath = os.path.join(config.last_path, basename + extension) # filepath will be None if cancelled by user filepath = ShowAcquisitionFileDialog(self._main_frame, filepath) else: dirname = get_picture_folder() filepath = os.path.join(dirname, basename + extension) if os.path.exists(filepath): msg = "File '%s' already exists, cancelling snapshot" logging.warning(msg, filepath) tab, filepath = None, None exporter = dataio.get_converter(config.last_format) return tab, filepath, exporter
def __init__(self, tab_data, main_frame, tab_panel, viewports): """ tab_data: MicroscopyGUIData -- the representation of the microscope GUI main_frame: (wx.Frame): the whole GUI frame """ self._data_model = tab_data self._main_data_model = tab_data.main self._main_frame = main_frame self._tab_panel = tab_panel self._conf = get_acqui_conf() # Listen to "acquire image" button self._tab_panel.btn_secom_export.Bind(wx.EVT_BUTTON, self.on_export) self._viewports = viewports.keys() self._main_frame.Bind(wx.EVT_MENU, self.on_export, id=self._main_frame.menu_item_export_as.GetId()) self._main_frame.menu_item_export_as.Enable(False) # subscribe to get notified about tab changes self._prev_streams = None # To unsubscribe afterwards self._main_data_model.tab.subscribe(self.on_tab_change, init=True)
def __init__(self, microscope, main_app): super(QuickCLPlugin, self).__init__(microscope, main_app) # Can only be used with a SPARC with CL detector (or monochromator) if not microscope: return main_data = self.main_app.main_data if not main_data.ebeam or not (main_data.cld or main_data.monochromator): return self.conf = get_acqui_conf() self.filename = model.StringVA("") self.filename.subscribe(self._on_filename) self.expectedDuration = model.VigilantAttribute(1, unit="s", readonly=True) self.hasDatabar = model.BooleanVA(False) # Only put the VAs that do directly define the image as local, everything # else should be global. The advantage is double: the global VAs will # set the hardware even if another stream (also using the e-beam) is # currently playing, and if the VAs are changed externally, the settings # will be displayed correctly (and not reset the values on next play). emtvas = set() hwemtvas = set() for vaname in get_local_vas(main_data.ebeam, main_data.hw_settings_config): if vaname in ("resolution", "dwellTime", "scale"): emtvas.add(vaname) else: hwemtvas.add(vaname) self._sem_stream = stream.SEMStream( "Secondary electrons", main_data.sed, main_data.sed.data, main_data.ebeam, focuser=main_data.ebeam_focus, hwemtvas=hwemtvas, hwdetvas=None, emtvas=emtvas, detvas=get_local_vas(main_data.sed, main_data.hw_settings_config), ) # This stream is used both for rendering and acquisition. # LiveCLStream is more or less like a SEMStream, but ensures the icon in # the merge slider is correct, and provide a few extra. if main_data.cld: self._cl_stream = LiveCLStream( "CL intensity", main_data.cld, main_data.cld.data, main_data.ebeam, focuser=main_data.ebeam_focus, emtvas=emtvas, detvas=get_local_vas(main_data.cld, main_data.hw_settings_config), opm=main_data.opm, ) # TODO: allow to type in the resolution of the CL? # TODO: add the cl-filter axis (or reset it to pass-through?) self.logScale = self._cl_stream.logScale if hasattr(self._cl_stream, "detGain"): self._cl_stream.detGain.subscribe(self._on_cl_gain) # Update the acquisition time when it might change (ie, the scan settings # change) self._cl_stream.emtDwellTime.subscribe(self._update_exp_dur) self._cl_stream.emtResolution.subscribe(self._update_exp_dur) # Note: for now we don't really support SPARC with BOTH CL-detector and # monochromator. if main_data.monochromator: self._mn_stream = LiveCLStream( "Monochromator", main_data.monochromator, main_data.monochromator.data, main_data.ebeam, focuser=main_data.ebeam_focus, emtvas=emtvas, detvas=get_local_vas(main_data.monochromator, main_data.hw_settings_config), opm=main_data.opm, ) self._mn_stream.emtDwellTime.subscribe(self._update_exp_dur) self._mn_stream.emtResolution.subscribe(self._update_exp_dur) # spg = self._getAffectingSpectrograph(main_data.spectrometer) # TODO: show axes self._dlg = None self.addMenu("Acquisition/Quick CL...\tF2", self.start)
def create_setting_entry(container, name, va, hw_comp, conf=None, change_callback=None): """ Determine what type on control to use for a setting and have the container create it Args: container (SettingsController or StreamController): Controller in charge of the settings name (str): Name of the setting va (VigilantAttribute): The va containing the value of the setting hw_comp (Component): The hardware component to which the setting belongs conf ({} or None): The optional configuration options for the control change_callback (callable): Callable to bind to the control's change event Returns: SettingEntry """ value_ctrl = lbl_ctrl = setting_entry = None # If no conf provided, set it to an empty dictionary conf = conf or {} # Get the range and choices min_val, max_val, choices, unit = process_setting_metadata(hw_comp, va, conf) # Format the provided choices choices_formatted, choices_si_prefix = format_choices(choices) # Determine the control type to use, either from config or some 'smart' default control_type = determine_control_type(hw_comp, va, choices_formatted, conf) # Special case, early stop if control_type == odemis.gui.CONTROL_NONE: # No value, not even a label, just an empty entry, so that the settings are saved # during acquisition return SettingEntry(name=name, va=va, hw_comp=hw_comp) # Format label label_text = conf.get('label', label_to_human(name)) tooltip = conf.get('tooltip', "") logging.debug("Adding VA %s", label_text) # Create the needed wxPython controls if control_type == odemis.gui.CONTROL_READONLY: val = va.value accuracy = conf.get('accuracy', 3) val_str = value_to_str(val, unit, accuracy, pretty_time=True) lbl_ctrl, value_ctrl = container.add_readonly_field(label_text, val_str) def set_ctrl(value, ctrl=value_ctrl, u=unit, acc=accuracy): ctrl.SetValue(value_to_str(value, u, acc, pretty_time=True)) setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, va_2_ctrl=set_ctrl) elif control_type == odemis.gui.CONTROL_TEXT: val = va.value accuracy = conf.get('accuracy', 3) val_str = value_to_str(val, unit, accuracy) # No pretty_time, as we don't support reading it back lbl_ctrl, value_ctrl = container.add_text_field(label_text, val_str) # To set the value on the control (when the VA is updated) def set_ctrl(value, ctrl=value_ctrl, u=unit, acc=accuracy): ctrl.SetValue(value_to_str(value, u, acc)) # To retrieve the actual value, which may contain prefix and unit def text_get(ctrl=value_ctrl, va=va): ctrl_value = ctrl.GetValue() va_val = va.value try: new_val = str_to_value(ctrl_value, va) except (ValueError, TypeError): logging.warning("Value %s couldn't be understood", ctrl_value, exc_info=True) new_val = va_val # To force going back to last value # if it ends up being the same value as before the VA will # not update, so force reformatting it if va_val == new_val: set_ctrl(va_val) return new_val setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, va_2_ctrl=set_ctrl, ctrl_2_va=text_get, events=wx.EVT_TEXT_ENTER) if change_callback: value_ctrl.Bind(wx.EVT_TEXT_ENTER, change_callback) elif control_type in (odemis.gui.CONTROL_SAVE_FILE, odemis.gui.CONTROL_OPEN_FILE): val = va.value if not val: config = guiconf.get_acqui_conf() val = config.last_path if control_type == odemis.gui.CONTROL_SAVE_FILE: dialog_style = wx.FD_SAVE else: # odemis.gui.CONTROL_OPEN_FILE dialog_style = wx.FD_OPEN clearlabel = conf.get('clearlabel') # Text to show when no filename (+ allow to clear the filename) wildcard = conf.get('wildcard', "*.*") # File extension wildcard string lbl_ctrl, value_ctrl = container.add_file_button(label_text, val, clearlabel, wildcard=wildcard, dialog_style=dialog_style) # Add the corresponding setting entry setting_entry = SettingEntry(name=label_text, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, events=EVT_FILE_SELECT) elif control_type == odemis.gui.CONTROL_SLIDER: # The slider is accompanied by an extra number text field if "type" in conf: if conf["type"] == "integer": # add_integer_slider factory = container.add_integer_slider elif conf["type"] == "slider": factory = container.add_slider else: factory = container.add_float_slider else: # guess from value(s) known_values = [va.value, min_val, max_val] if choices is not None: known_values.extend(list(choices)) if any(isinstance(v, float) for v in known_values): factory = container.add_float_slider else: factory = container.add_integer_slider # The event configuration determines what event will signal that the # setting entry has changed value. update_event = conf.get("event", wx.EVT_SLIDER) if update_event.typeId not in (wx.EVT_SCROLL_CHANGED.typeId, wx.EVT_SLIDER.typeId): raise ValueError("Illegal event type %d for Slider setting entry!" % (update_event.typeId,)) ctrl_conf = { 'min_val': min_val, 'max_val': max_val, 'scale': conf.get('scale', None), 'unit': unit, 'accuracy': conf.get('accuracy', 4), } lbl_ctrl, value_ctrl = factory(label_text, va.value, ctrl_conf) setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, events=update_event) if change_callback: if not isinstance(update_event, collections.Iterable): update_event = (update_event,) for event in update_event: value_ctrl.Bind(event, change_callback) elif control_type == odemis.gui.CONTROL_INT: if unit == "": # don't display unit prefix if no unit unit = None ctrl_conf = { 'min_val': min_val, 'max_val': max_val, 'unit': unit, 'choices': choices, } if 'key_step' in conf: ctrl_conf['key_step'] = conf['key_step'] if 'key_step_min' in conf: ctrl_conf['key_step_min'] = conf['key_step_min'] lbl_ctrl, value_ctrl = container.add_int_field(label_text, conf=ctrl_conf) setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, events=wx.EVT_COMMAND_ENTER) if change_callback: value_ctrl.Bind(wx.EVT_COMMAND_ENTER, change_callback) elif control_type == odemis.gui.CONTROL_FLT: if unit == "": # don't display unit prefix if no unit unit = None ctrl_conf = { 'min_val': min_val, 'max_val': max_val, 'unit': unit, 'choices': choices, 'accuracy': conf.get('accuracy', 5), } if 'key_step' in conf: ctrl_conf['key_step'] = conf['key_step'] if 'key_step_min' in conf: ctrl_conf['key_step_min'] = conf['key_step_min'] lbl_ctrl, value_ctrl = container.add_float_field(label_text, conf=ctrl_conf) setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, events=wx.EVT_COMMAND_ENTER) if change_callback: value_ctrl.Bind(wx.EVT_COMMAND_ENTER, change_callback) elif control_type == odemis.gui.CONTROL_CHECK: # Only supports boolean VAs lbl_ctrl, value_ctrl = container.add_checkbox_control(label_text, value=va.value) setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, events=wx.EVT_CHECKBOX) if change_callback: value_ctrl.Bind(wx.EVT_CHECKBOX, change_callback) elif control_type == odemis.gui.CONTROL_RADIO: unit_fmt = (choices_si_prefix or "") + (unit or "") ctrl_conf = { 'size': (-1, 16), 'units': unit_fmt, 'choices': [v for v, _ in choices_formatted], 'labels': [l for _, l in choices_formatted], } lbl_ctrl, value_ctrl = container.add_radio_control(label_text, conf=ctrl_conf) setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, events=wx.EVT_BUTTON) if change_callback: value_ctrl.Bind(wx.EVT_BUTTON, change_callback) elif control_type == odemis.gui.CONTROL_COMBO: accuracy = conf.get('accuracy', 3) # TODO: Might need size=(100, 16)!! cbconf = {} if hasattr(va, "choices") and isinstance(va.choices, collections.Iterable): # Enumerated VA => don't allow entering other values cbconf["style"] = wx.CB_READONLY lbl_ctrl, value_ctrl = container.add_combobox_control(label_text, conf=cbconf) # Set choices if choices_si_prefix: for choice, formatted in choices_formatted: value_ctrl.Append(u"%s %s" % (formatted, choices_si_prefix + unit), choice) else: for choice, formatted in choices_formatted: value_ctrl.Append(u"%s%s" % (formatted, unit), choice) # A small wrapper function makes sure that the value can # be set by passing the actual value (As opposed to the text label) def cb_set(value, va=va, ctrl=value_ctrl, u=unit, acc=accuracy): # Re-read the value from the VA because it'll be called via # CallAfter(), and if the value is changed multiple times, it might # not be in chronological order. value = va.value for i in range(ctrl.GetCount()): d = ctrl.GetClientData(i) if (d == value or (all(isinstance(v, float) for v in (value, d)) and util.almost_equal(d, value)) ): logging.debug("Setting combobox value to %s", ctrl.Items[i]) ctrl.SetSelection(i) break else: logging.debug("No existing label found for value %s in combobox ctrl %d", value, id(ctrl)) # entering value as free text txt = value_to_str(value, u, acc) ctrl.SetValue(txt) # equivalent wrapper function to retrieve the actual value def cb_get(ctrl=value_ctrl, va=va, u=unit): ctrl_value = ctrl.GetValue() # Try to use the predefined value if it's available i = ctrl.GetSelection() # Warning: if the text contains an unknown value, GetSelection will # not return wx.NOT_FOUND (as expected), but the last selection value if i != wx.NOT_FOUND and ctrl.Items[i] == ctrl_value: logging.debug("Getting item value %s from combobox control", ctrl.GetClientData(i)) return ctrl.GetClientData(i) else: va_val = va.value try: new_val = str_to_value(ctrl_value, va) except (ValueError, TypeError): logging.warning("Value %s couldn't be understood", ctrl_value, exc_info=True) new_val = va_val # To force going back to last value # if it ends up being the same value as before the combobox will # not update, so force it now if va_val == new_val: cb_set(va_val) return new_val setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, va_2_ctrl=cb_set, ctrl_2_va=cb_get, events=(wx.EVT_COMBOBOX, wx.EVT_TEXT_ENTER)) if change_callback: value_ctrl.Bind(wx.EVT_COMBOBOX, change_callback) value_ctrl.Bind(wx.EVT_TEXT_ENTER, change_callback) else: logging.error("Unknown control type %s", control_type) value_ctrl.SetToolTip(tooltip) lbl_ctrl.SetToolTip(tooltip) return setting_entry
def _get_new_filename(self): conf = get_acqui_conf() return os.path.join( conf.last_path, u"%s%s" % (time.strftime("sr-%Y%m%d-%H%M%S"), ".tiff"))
def __init__(self, parent, orig_tab_data): xrcfr_acq.__init__(self, parent) self.conf = get_acqui_conf() for n in presets: self.cmb_presets.Append(n) # TODO: record and reuse the preset used? self.cmb_presets.Select(0) self.filename = model.StringVA(create_filename(self.conf.last_path, self.conf.fn_ptn, self.conf.last_extension, self.conf.fn_count)) self.filename.subscribe(self._onFilename, init=True) # The name of the last file that got written to disk (used for auto viewing on close) self.last_saved_file = None # True when acquisition occurs self.acquiring = False # a ProgressiveFuture if the acquisition is going on self.acq_future = None self._acq_future_connector = None self._main_data_model = orig_tab_data.main # duplicate the interface, but with only one view self._tab_data_model = self.duplicate_tab_data_model(orig_tab_data) # Create a new settings controller for the acquisition dialog self._settings_controller = SecomSettingsController( self, self._tab_data_model, highlight_change=True # also adds a "Reset" context menu ) orig_view = orig_tab_data.focussedView.value self._view = self._tab_data_model.focussedView.value self._hidden_view = StreamView("Plugin View Hidden") self.streambar_controller = StreamBarController(self._tab_data_model, self.pnl_secom_streams, static=True, ignore_view=True) # The streams currently displayed are the one visible self.add_all_streams() # The list of streams ready for acquisition (just used as a cache) self._acq_streams = {} # FIXME: pass the fold_panels # Compute the preset values for each preset self._preset_values = {} # dict string -> dict (SettingEntries -> value) self._orig_entries = get_global_settings_entries(self._settings_controller) for sc in self.streambar_controller.stream_controllers: self._orig_entries += get_local_settings_entries(sc) self._orig_settings = preset_as_is(self._orig_entries) # to detect changes for n, preset in presets.items(): self._preset_values[n] = preset(self._orig_entries) # Presets which have been confirmed on the hardware self._presets_confirmed = set() # (string) self.start_listening_to_va() # If it could be possible to do fine alignment, allow the user to choose if self._can_fine_align(self._tab_data_model.streams.value): self.chkbox_fine_align.Show() # Set to True to make it the default, but will be automatically # disabled later if the current visible streams don't allow it. self.chkbox_fine_align.Value = True for s in self._tab_data_model.streams.value: if isinstance(s, EMStream): em_det = s.detector em_emt = s.emitter elif isinstance(s, OpticalStream) and not isinstance(s, ScannedFluoStream): opt_det = s.detector self._ovrl_stream = stream.OverlayStream("Fine alignment", opt_det, em_emt, em_det, opm=self._main_data_model.opm) self._ovrl_stream.dwellTime.value = self._main_data_model.fineAlignDwellTime.value else: self.chkbox_fine_align.Show(False) self.chkbox_fine_align.Value = False self._prev_fine_align = self.chkbox_fine_align.Value # make sure the view displays the same thing as the one we are # duplicating self._view.view_pos.value = orig_view.view_pos.value self._view.mpp.value = orig_view.mpp.value self._view.merge_ratio.value = orig_view.merge_ratio.value # attach the view to the viewport self.pnl_view_acq.canvas.fit_view_to_next_image = False self.pnl_view_acq.setView(self._view, self._tab_data_model) # The TOOL_ROA is not present because we don't allow the user to change # the ROA), so we need to explicitly request the canvas to show the ROA. if hasattr(self._tab_data_model, "roa") and self._tab_data_model.roa is not None: cnvs = self.pnl_view_acq.canvas self.roa_overlay = RepetitionSelectOverlay(cnvs, self._tab_data_model.roa, self._tab_data_model.fovComp) cnvs.add_world_overlay(self.roa_overlay) self.Bind(wx.EVT_CHAR_HOOK, self.on_key) self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close) self.btn_change_file.Bind(wx.EVT_BUTTON, self.on_change_file) self.btn_secom_acquire.Bind(wx.EVT_BUTTON, self.on_acquire) self.cmb_presets.Bind(wx.EVT_COMBOBOX, self.on_preset) self.Bind(wx.EVT_CLOSE, self.on_close) # on_streams_changed is compatible because it doesn't use the args self.chkbox_fine_align.Bind(wx.EVT_CHECKBOX, self.on_streams_changed) self.on_preset(None) # will force setting the current preset # To update the estimated time when streams are removed/added self._view.stream_tree.flat.subscribe(self.on_streams_changed) self._hidden_view.stream_tree.flat.subscribe(self.on_streams_changed)
def _get_new_filename(self): conf = get_acqui_conf() return os.path.join( conf.last_path, u"%s%s" % (time.strftime("%Y%m%d-%H%M%S"), conf.last_extension))
def __init__(self, main_data, tab, overview_canvas, m_view, stream_bar): self.main_data = main_data self._tab = tab self._data_model = tab.tab_data_model self.canvas = overview_canvas self.m_view = m_view self._stream_bar = stream_bar self.conf = get_acqui_conf() self.curr_s = None # Timer to detect when the stage ends moving self._timer_pos = wx.PyTimer(self.add_pos_to_history) if hasattr(m_view, "merge_ratio"): m_view.merge_ratio.subscribe(self._on_merge_ratio_change) # Global overview image (Delphi) if main_data.overview_ccd: # Overview camera can be RGB => in that case len(shape) == 4 if len(main_data.overview_ccd.shape) == 4: overview_stream = acqstream.RGBCameraStream("Overview", main_data.overview_ccd, main_data.overview_ccd.data, None, acq_type=MD_AT_OVV_FULL) else: overview_stream = acqstream.BrightfieldStream("Overview", main_data.overview_ccd, main_data.overview_ccd.data, None, acq_type=MD_AT_OVV_FULL) self.m_view.addStream(overview_stream) # TODO: add it to self.tab_data_model.streams? else: # black image to display history overlay separately from built-up ovv image # controlled by merge slider da, _ = self._initialize_ovv_im(OVV_SHAPE) history_stream = acqstream.RGBUpdatableStream("History Stream", da, acq_type=MD_AT_HISTORY) self.m_view.addStream(history_stream) # Built-up overview image self.ovv_im, self.m_view.mpp.value = self._initialize_ovv_im(OVV_SHAPE) logging.debug("Overview image FoV: %s", getBoundingBox(self.ovv_im)) # Initialize individual ovv images for optical and sem stream self.im_opt = copy.deepcopy(self.ovv_im) self.im_sem = copy.deepcopy(self.ovv_im) # Extra images to be used for complete overviews, shown behind the build-up images self._bkg_opt = copy.deepcopy(self.ovv_im) self._bkg_sem = copy.deepcopy(self.ovv_im) # Add stream to view self.upd_stream = acqstream.RGBUpdatableStream("Overview Stream", self.ovv_im, acq_type=MD_AT_OVV_TILES) self.m_view.addStream(self.upd_stream) self._data_model.focussedView.subscribe(self._on_focused_view) if main_data.stage: # Update the image when the stage move main_data.stage.position.subscribe(self.on_stage_pos_change, init=True) main_data.chamberState.subscribe(self._on_chamber_state) self._data_model.streams.subscribe(self._on_current_stream) # Add a "acquire overview" button. self._stream_bar.btn_add_overview.Bind(wx.EVT_BUTTON, self._on_overview_acquire) self._acquisition_controller = acqcont.OverviewStreamAcquiController(self._data_model, tab) self._bkg_ovv_subs = {} # Just used temporarily when background overview is projected
def __init__(self, parent, orig_tab_data): xrcfr_acq.__init__(self, parent) self.conf = get_acqui_conf() for n in presets: self.cmb_presets.Append(n) # TODO: record and reuse the preset used? self.cmb_presets.Select(0) self.filename = model.StringVA( create_filename(self.conf.last_path, self.conf.fn_ptn, self.conf.last_extension, self.conf.fn_count)) self.filename.subscribe(self._onFilename, init=True) # The name of the last file that got written to disk (used for auto viewing on close) self.last_saved_file = None # True when acquisition occurs self.acquiring = False # a ProgressiveFuture if the acquisition is going on self.acq_future = None self._acq_future_connector = None self._main_data_model = orig_tab_data.main # duplicate the interface, but with only one view self._tab_data_model = self.duplicate_tab_data_model(orig_tab_data) # Create a new settings controller for the acquisition dialog self._settings_controller = SecomSettingsController( self, self._tab_data_model, highlight_change=True # also adds a "Reset" context menu ) orig_view = orig_tab_data.focussedView.value self._view = self._tab_data_model.focussedView.value self._hidden_view = StreamView("Plugin View Hidden") self.streambar_controller = StreamBarController(self._tab_data_model, self.pnl_secom_streams, static=True, ignore_view=True) # The streams currently displayed are the one visible self.add_all_streams() # The list of streams ready for acquisition (just used as a cache) self._acq_streams = {} # FIXME: pass the fold_panels # Compute the preset values for each preset self._preset_values = { } # dict string -> dict (SettingEntries -> value) self._orig_entries = get_global_settings_entries( self._settings_controller) for sc in self.streambar_controller.stream_controllers: self._orig_entries += get_local_settings_entries(sc) self._orig_settings = preset_as_is( self._orig_entries) # to detect changes for n, preset in presets.items(): self._preset_values[n] = preset(self._orig_entries) # Presets which have been confirmed on the hardware self._presets_confirmed = set() # (string) self.start_listening_to_va() # If it could be possible to do fine alignment, allow the user to choose if self._can_fine_align(self._tab_data_model.streams.value): self.chkbox_fine_align.Show() # Set to True to make it the default, but will be automatically # disabled later if the current visible streams don't allow it. self.chkbox_fine_align.Value = True for s in self._tab_data_model.streams.value: if isinstance(s, EMStream): em_det = s.detector em_emt = s.emitter elif isinstance(s, OpticalStream) and not isinstance( s, ScannedFluoStream): opt_det = s.detector self._ovrl_stream = stream.OverlayStream( "Fine alignment", opt_det, em_emt, em_det, opm=self._main_data_model.opm) self._ovrl_stream.dwellTime.value = self._main_data_model.fineAlignDwellTime.value else: self.chkbox_fine_align.Show(False) self.chkbox_fine_align.Value = False self._prev_fine_align = self.chkbox_fine_align.Value # make sure the view displays the same thing as the one we are # duplicating self._view.view_pos.value = orig_view.view_pos.value self._view.mpp.value = orig_view.mpp.value self._view.merge_ratio.value = orig_view.merge_ratio.value # attach the view to the viewport self.pnl_view_acq.canvas.fit_view_to_next_image = False self.pnl_view_acq.setView(self._view, self._tab_data_model) # The TOOL_ROA is not present because we don't allow the user to change # the ROA), so we need to explicitly request the canvas to show the ROA. if hasattr(self._tab_data_model, "roa") and self._tab_data_model.roa is not None: cnvs = self.pnl_view_acq.canvas self.roa_overlay = RepetitionSelectOverlay( cnvs, self._tab_data_model.roa, self._tab_data_model.fovComp) cnvs.add_world_overlay(self.roa_overlay) self.Bind(wx.EVT_CHAR_HOOK, self.on_key) self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close) self.btn_change_file.Bind(wx.EVT_BUTTON, self.on_change_file) self.btn_secom_acquire.Bind(wx.EVT_BUTTON, self.on_acquire) self.cmb_presets.Bind(wx.EVT_COMBOBOX, self.on_preset) self.Bind(wx.EVT_CLOSE, self.on_close) # on_streams_changed is compatible because it doesn't use the args self.chkbox_fine_align.Bind(wx.EVT_CHECKBOX, self.on_streams_changed) self.on_preset(None) # will force setting the current preset # To update the estimated time when streams are removed/added self._view.stream_tree.flat.subscribe(self.on_streams_changed) self._hidden_view.stream_tree.flat.subscribe(self.on_streams_changed)
def __init__(self, tab_data, tab_panel, streambar_controller): """ tab_data (MicroscopyGUIData): the representation of the microscope GUI tab_panel: (wx.Frame): the frame which contains the 4 viewports stream_ctrl (StreamBarController): controller to pause/resume the streams """ self._tab_data_model = tab_data self._main_data_model = tab_data.main self._tab_panel = tab_panel self._streambar_controller = streambar_controller # For file selection self.conf = conf.get_acqui_conf() # TODO: this should be the date at which the user presses the acquire # button (or when the last settings were changed)! # At least, we must ensure it's a new date after the acquisition # is done. # Filename to save the acquisition self.filename = model.StringVA(create_filename(self.conf.last_path, self.conf.fn_ptn, self.conf.last_extension, self.conf.fn_count)) self.filename.subscribe(self._onFilename, init=True) # For acquisition # a ProgressiveFuture if the acquisition is going on self.btn_acquire = self._tab_panel.btn_sparc_acquire self.btn_change_file = self._tab_panel.btn_sparc_change_file self.btn_cancel = self._tab_panel.btn_sparc_cancel self.acq_future = None self.gauge_acq = self._tab_panel.gauge_sparc_acq self.lbl_acqestimate = self._tab_panel.lbl_sparc_acq_estimate self.bmp_acq_status_warn = self._tab_panel.bmp_acq_status_warn self.bmp_acq_status_info = self._tab_panel.bmp_acq_status_info self._acq_future_connector = None # TODO: share an executor with the whole GUI. self._executor = futures.ThreadPoolExecutor(max_workers=2) # Link buttons self.btn_acquire.Bind(wx.EVT_BUTTON, self.on_acquisition) self.btn_change_file.Bind(wx.EVT_BUTTON, self.on_change_file) self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_cancel) self.gauge_acq.Hide() self._tab_panel.Parent.Layout() # Animator for messages containing ellipsis character self._ellipsis_animator = None # TODO: we need to be informed if the user closes suddenly the window # self.Bind(wx.EVT_CLOSE, self.on_close) self._roa = tab_data.semStream.roi # Listen to change of streams to update the acquisition time self._prev_streams = set() # set of streams already listened to tab_data.streams.subscribe(self._onStreams, init=True) # also listen to .semStream, which is not in .streams for va in self._get_settings_vas(tab_data.semStream): va.subscribe(self._onAnyVA) # Extra options affecting the acquisitions globally tab_data.pcdActive.subscribe(self._onAnyVA) # TODO: should also listen to the VAs of the leeches on semStream tab_data.useScanStage.subscribe(self._onAnyVA) self._roa.subscribe(self._onROA, init=True) # Listen to preparation state self._main_data_model.is_preparing.subscribe(self.on_preparation)
def __init__(self, parent, orig_tab_data): xrcfr_acq.__init__(self, parent) self.conf = get_acqui_conf() for n in presets: self.cmb_presets.Append(n) # TODO: record and reuse the preset used? self.cmb_presets.Select(0) self.filename = model.StringVA(self._get_default_filename()) self.filename.subscribe(self._onFilename, init=True) # The name of the last file that got written to disk (used for auto viewing on close) self.last_saved_file = None # a ProgressiveFuture if the acquisition is going on self.acq_future = None self._acq_future_connector = None self._main_data_model = orig_tab_data.main # duplicate the interface, but with only one view self._tab_data_model = self.duplicate_tab_data_model(orig_tab_data) # Create a new settings controller for the acquisition dialog self._settings_controller = SecomSettingsController( self, self._tab_data_model, highlight_change=True # also adds a "Reset" context menu ) # To turn on/off the fan self._orig_fan_speed = None orig_view = orig_tab_data.focussedView.value self._view = self._tab_data_model.focussedView.value self.streambar_controller = StreamBarController(self._tab_data_model, self.pnl_secom_streams) # The streams currently displayed are the one visible self.add_all_streams() # FIXME: pass the fold_panels # Compute the preset values for each preset self._preset_values = {} # dict string -> dict (SettingEntries -> value) orig_entries = get_global_settings_entries(self._settings_controller) for sc in self.streambar_controller.stream_controllers: orig_entries += get_local_settings_entries(sc) self._orig_settings = preset_as_is(orig_entries) # to detect changes for n, preset in presets.items(): self._preset_values[n] = preset(orig_entries) # Presets which have been confirmed on the hardware self._presets_confirmed = set() # (string) # If it could be possible to do fine alignment, allow the user to choose if self._can_fine_align(self._tab_data_model.streams.value): self.chkbox_fine_align.Show() # Set to True to make it the default, but will be automatically # disabled later if the current visible streams don't allow it. self.chkbox_fine_align.Value = True for s in self._tab_data_model.streams.value: if isinstance(s, EMStream): em_det = s.detector em_emt = s.emitter elif isinstance(s, OpticalStream): opt_det = s.detector self._ovrl_stream = stream.OverlayStream("Fine alignment", opt_det, em_emt, em_det) if self._main_data_model.role == "delphi": self._main_data_model.fineAlignDwellTime.value = 0.5 self._ovrl_stream.dwellTime.value = self._main_data_model.fineAlignDwellTime.value else: self.chkbox_fine_align.Show(False) self.chkbox_fine_align.Value = False self._prev_fine_align = self.chkbox_fine_align.Value # make sure the view displays the same thing as the one we are # duplicating self._view.view_pos.value = orig_view.view_pos.value self._view.mpp.value = orig_view.mpp.value self._view.merge_ratio.value = orig_view.merge_ratio.value # attach the view to the viewport self.pnl_view_acq.canvas.fit_view_to_next_image = False self.pnl_view_acq.setView(self._view, self._tab_data_model) self.Bind(wx.EVT_CHAR_HOOK, self.on_key) self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close) self.btn_change_file.Bind(wx.EVT_BUTTON, self.on_change_file) self.btn_secom_acquire.Bind(wx.EVT_BUTTON, self.on_acquire) self.cmb_presets.Bind(wx.EVT_COMBOBOX, self.on_preset) self.Bind(wx.EVT_CLOSE, self.on_close) # on_streams_changed is compatible because it doesn't use the args self.chkbox_fine_align.Bind(wx.EVT_CHECKBOX, self.on_streams_changed) self.on_preset(None) # will force setting the current preset # TODO: use the presets VAs and subscribe to each of them, instead of # using pub/sub messages pub.subscribe(self.on_setting_change, 'setting.changed') # TODO: we should actually listen to the stream tree, but it's not # currently possible. => listen to .flat once it's there # Currently just use view.lastUpdate which should be "similar" # (but doesn't work if the stream contains no image) self._view.lastUpdate.subscribe(self.on_streams_changed)
def _get_new_filename(self): conf = get_acqui_conf() return os.path.join( conf.last_path, u"%s%s" % (time.strftime("sr-%Y%m%d-%H%M%S"), ".tiff") )
def create_setting_entry(container, name, va, hw_comp, conf=None, change_callback=None): """ Determine what type on control to use for a setting and have the container create it Args: container (SettingsController or StreamController): Controller in charge of the settings name (str): Name of the setting va (VigilantAttribute): The va containing the value of the setting hw_comp (Component): The hardware component to which the setting belongs conf ({} or None): The optional configuration options for the control change_callback (callable): Callable to bind to the control's change event Returns: SettingEntry """ value_ctrl = lbl_ctrl = setting_entry = None # If no conf provided, set it to an empty dictionary conf = conf or {} # Get the range and choices min_val, max_val, choices, unit = process_setting_metadata(hw_comp, va, conf) # Format the provided choices si = conf.get('si', None) si_unif = conf.get('si_unif', True) choices_formatted, choices_si_prefix = format_choices(choices, uniformat=si_unif, si=si) # Determine the control type to use, either from config or some 'smart' default control_type = determine_control_type(hw_comp, va, choices_formatted, conf) # Special case, early stop if control_type == odemis.gui.CONTROL_NONE: # No value, not even a label, just an empty entry, so that the settings are saved # during acquisition return SettingEntry(name=name, va=va, hw_comp=hw_comp) # Format label label_text = conf.get('label', label_to_human(name)) tooltip = conf.get('tooltip', "") logging.debug("Adding VA %s", label_text) # Create the needed wxPython controls if control_type == odemis.gui.CONTROL_READONLY: val = va.value # only format if it's a number accuracy = conf.get('accuracy', 3) lbl_ctrl, value_ctrl = container.add_readonly_field(label_text, val) value_formatter = create_formatted_setter(value_ctrl, val, unit, accuracy) setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, va_2_ctrl=value_formatter) elif control_type == odemis.gui.CONTROL_TEXT: val = va.value # only format if it's a number accuracy = conf.get('accuracy', 3) lbl_ctrl, value_ctrl = container.add_text_field(label_text, val) value_formatter = create_formatted_setter(value_ctrl, val, unit, accuracy) setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, va_2_ctrl=value_formatter, events=wx.EVT_TEXT_ENTER) elif control_type in (odemis.gui.CONTROL_SAVE_FILE, odemis.gui.CONTROL_OPEN_FILE): val = va.value if not val: config = guiconf.get_acqui_conf() val = config.last_path if control_type == odemis.gui.CONTROL_SAVE_FILE: dialog_style = wx.FD_SAVE else: # odemis.gui.CONTROL_OPEN_FILE dialog_style = wx.FD_OPEN clearlabel = conf.get('clearlabel') # Text to show when no filename (+ allow to clear the filename) lbl_ctrl, value_ctrl = container.add_file_button(label_text, val, clearlabel, dialog_style=dialog_style) # TODO: allow to change the wildcard via a conf key? # value_ctrl.SetWildcard(wildcards) # Add the corresponding setting entry setting_entry = SettingEntry(name=label_text, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, events=EVT_FILE_SELECT) elif control_type == odemis.gui.CONTROL_SLIDER: # The slider is accompanied by an extra number text field if "type" in conf: if conf["type"] == "integer": # add_integer_slider factory = container.add_integer_slider elif conf["type"] == "slider": factory = container.add_slider else: factory = container.add_float_slider else: # guess from value(s) known_values = [va.value, min_val, max_val] if choices is not None: known_values.extend(list(choices)) if any(isinstance(v, float) for v in known_values): factory = container.add_float_slider else: factory = container.add_integer_slider # The event configuration determines what event will signal that the # setting entry has changed value. update_event = conf.get("event", wx.EVT_SLIDER) if update_event not in (wx.EVT_SCROLL_CHANGED, wx.EVT_SLIDER): raise ValueError("Illegal event type %d for Slider setting entry!" % (update_event,)) ctrl_conf = { 'min_val': min_val, 'max_val': max_val, 'scale': conf.get('scale', None), 'unit': unit, 'accuracy': conf.get('accuracy', 4), } lbl_ctrl, value_ctrl = factory(label_text, va.value, ctrl_conf) setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, events=update_event) if change_callback: value_ctrl.Bind(wx.EVT_SLIDER, change_callback) elif control_type == odemis.gui.CONTROL_INT: if unit == "": # don't display unit prefix if no unit unit = None ctrl_conf = { 'min_val': min_val, 'max_val': max_val, 'unit': unit, 'choices': choices, } lbl_ctrl, value_ctrl = container.add_int_field(label_text, conf=ctrl_conf) setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, events=wx.EVT_COMMAND_ENTER) if change_callback: value_ctrl.Bind(wx.EVT_COMMAND_ENTER, change_callback) elif control_type == odemis.gui.CONTROL_FLT: if unit == "": # don't display unit prefix if no unit unit = None ctrl_conf = { 'min_val': min_val, 'max_val': max_val, 'unit': unit, 'choices': choices, 'accuracy': conf.get('accuracy', 5), } lbl_ctrl, value_ctrl = container.add_float_field(label_text, conf=ctrl_conf) setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, events=wx.EVT_COMMAND_ENTER) if change_callback: value_ctrl.Bind(wx.EVT_COMMAND_ENTER, change_callback) elif control_type == odemis.gui.CONTROL_CHECK: # Only supports boolean VAs lbl_ctrl, value_ctrl = container.add_checkbox_control(label_text, value=va.value) setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, events=wx.EVT_CHECKBOX) if change_callback: value_ctrl.Bind(wx.EVT_CHECKBOX, change_callback) elif control_type == odemis.gui.CONTROL_RADIO: unit_fmt = (choices_si_prefix or "") + (unit or "") ctrl_conf = { 'size': (-1, 16), 'units': unit_fmt, 'choices': [v for v, _ in choices_formatted], 'labels': [l for _, l in choices_formatted], } lbl_ctrl, value_ctrl = container.add_radio_control(label_text, conf=ctrl_conf) setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, events=wx.EVT_BUTTON) if change_callback: value_ctrl.Bind(wx.EVT_BUTTON, change_callback) elif control_type == odemis.gui.CONTROL_COMBO: accuracy = conf.get('accuracy', 3) # TODO: Might need size=(100, 16)!! lbl_ctrl, value_ctrl = container.add_combobox_control(label_text) # Set choices if choices_si_prefix: for choice, formatted in choices_formatted: value_ctrl.Append(u"%s %s" % (formatted, choices_si_prefix + unit), choice) else: for choice, formatted in choices_formatted: value_ctrl.Append(u"%s%s" % (formatted, unit), choice) # A small wrapper function makes sure that the value can # be set by passing the actual value (As opposed to the text label) def cb_set(value, ctrl=value_ctrl, u=unit): for i in range(ctrl.Count): if ctrl.GetClientData(i) == value: logging.debug("Setting combobox value to %s", ctrl.Items[i]) ctrl.SetSelection(i) break else: logging.debug("No existing label found for value %s in combobox ctrl %d", value, id(ctrl)) # entering value as free text txt = readable_str(value, u, sig=accuracy) return ctrl.SetValue(txt) # equivalent wrapper function to retrieve the actual value def cb_get(ctrl=value_ctrl, va=va, u=unit): ctrl_value = ctrl.GetValue() # Try to use the predefined value if it's available i = ctrl.GetSelection() # Warning: if the text contains an unknown value, GetSelection will # not return wx.NOT_FOUND (as expected), but the last selection value if i != wx.NOT_FOUND and ctrl.Items[i] == ctrl_value: logging.debug("Getting item value %s from combobox control", ctrl.GetClientData(i)) return ctrl.GetClientData(i) else: logging.debug("Parsing combobox free text value %s", ctrl_value) va_val = va.value # Try to find a good corresponding value inside the string # Try and find an SI prefix str_val, str_si, _ = decompose_si_prefix(ctrl_value, unit=u) new_val = reproduce_typed_value(va_val, str_val) if isinstance(new_val, collections.Iterable): # be less picky, by shortening the number of values if it's too many new_val = new_val[:len(va_val)] # If an SI prefix was found, scale the new value new_val = si_scale_val(new_val, str_si) # if it ends up being the same value as before the combobox will not update, so # force it now if va_val == new_val: cb_set(va_val) return new_val setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, va_2_ctrl=cb_set, ctrl_2_va=cb_get, events=(wx.EVT_COMBOBOX, wx.EVT_TEXT_ENTER)) if change_callback: value_ctrl.Bind(wx.EVT_COMBOBOX, change_callback) value_ctrl.Bind(wx.EVT_TEXT_ENTER, change_callback) else: logging.error("Unknown control type %s", control_type) value_ctrl.SetToolTipString(tooltip) lbl_ctrl.SetToolTipString(tooltip) return setting_entry
def __init__(self, parent, orig_tab_data): xrcfr_acq.__init__(self, parent) self.conf = get_acqui_conf() for n in presets: self.cmb_presets.Append(n) # TODO: record and reuse the preset used? self.cmb_presets.Select(0) self.filename = model.StringVA(self._get_default_filename()) self.filename.subscribe(self._onFilename, init=True) # a ProgressiveFuture if the acquisition is going on self.acq_future = None self._acq_future_connector = None # duplicate the interface, but with only one view self._tab_data_model = self.duplicate_tab_data_model(orig_tab_data) # Create a new settings controller for the acquisition dialog self._settings_controller = SecomSettingsController(self, self._tab_data_model, highlight_change=True) # FIXME: pass the fold_panels # Compute the preset values for each preset self._preset_values = {} # dict string -> dict (SettingEntries -> value) orig_entries = self._settings_controller.entries self._orig_settings = preset_as_is(orig_entries) # to detect changes for n, preset in presets.items(): self._preset_values[n] = preset(orig_entries) # Presets which have been confirmed on the hardware self._presets_confirmed = set() # (string) orig_view = orig_tab_data.focussedView.value view = self._tab_data_model.focussedView.value self.stream_controller = StreamController(self._tab_data_model, self.pnl_secom_streams) # The streams currently displayed are the one visible self.add_all_streams(orig_view.getStreams()) # If it could be possible to do fine alignment, allow the user to choose if self._can_fine_align(self._tab_data_model.streams.value): self.chkbox_fine_align.Show() # Set to True to make it the default, but will be automatically # disabled later if the current visible streams don't allow it. self.chkbox_fine_align.Value = True main_data = self._tab_data_model.main self._ovrl_stream = stream.OverlayStream("fine alignment", main_data.ccd, main_data.ebeam, main_data.sed) self._ovrl_stream.dwellTime.value = main_data.fineAlignDwellTime.value else: self.chkbox_fine_align.Show(False) self.chkbox_fine_align.Value = False self._prev_fine_align = self.chkbox_fine_align.Value # make sure the view displays the same thing as the one we are # duplicating view.view_pos.value = orig_view.view_pos.value view.mpp.value = orig_view.mpp.value view.merge_ratio.value = orig_view.merge_ratio.value # attach the view to the viewport self.pnl_view_acq.setView(view, self._tab_data_model) self.Bind(wx.EVT_CHAR_HOOK, self.on_key) self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close) self.btn_change_file.Bind(wx.EVT_BUTTON, self.on_change_file) self.btn_secom_acquire.Bind(wx.EVT_BUTTON, self.on_acquire) self.cmb_presets.Bind(wx.EVT_COMBOBOX, self.on_preset) self.Bind(wx.EVT_CLOSE, self.on_close) # on_streams_changed is compatible because it doesn't use the args self.chkbox_fine_align.Bind(wx.EVT_CHECKBOX, self.on_streams_changed) self.on_preset(None) # will force setting the current preset pub.subscribe(self.on_setting_change, 'setting.changed') # TODO: we should actually listen to the stream tree, but it's not # currently possible. # Currently just use view.last_update which should be "similar" view.lastUpdate.subscribe(self.on_streams_changed)
def create_setting_entry(container, name, va, hw_comp, conf=None, change_callback=None): """ Determine what type on control to use for a setting and have the container create it Args: container (SettingsController or StreamController): Controller in charge of the settings name (str): Name of the setting va (VigilantAttribute): The va containing the value of the setting hw_comp (Component): The hardware component to which the setting belongs conf ({} or None): The optional configuration options for the control change_callback (callable): Callable to bind to the control's change event Returns: SettingEntry or None (if CONTROL_NONE) """ value_ctrl = lbl_ctrl = setting_entry = None # If no conf provided, set it to an empty dictionary conf = conf or {} # Get the range and choices min_val, max_val, choices, unit = process_setting_metadata(hw_comp, va, conf) # Format the provided choices choices_formatted, choices_si_prefix = format_choices(choices) # Determine the control type to use, either from config or some 'smart' default control_type = determine_control_type(hw_comp, va, choices_formatted, conf) # Special case, early stop if control_type == odemis.gui.CONTROL_NONE: return None # Format label label_text = conf.get('label', label_to_human(name)) tooltip = conf.get('tooltip', "") logging.debug("Adding VA %s", label_text) # Create the needed wxPython controls if control_type == odemis.gui.CONTROL_READONLY: val = va.value accuracy = conf.get('accuracy', 3) val_str = value_to_str(val, unit, accuracy, pretty_time=True) lbl_ctrl, value_ctrl = container.add_readonly_field(label_text, val_str) def set_ctrl(value, ctrl=value_ctrl, u=unit, acc=accuracy): ctrl.SetValue(value_to_str(value, u, acc, pretty_time=True)) setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, va_2_ctrl=set_ctrl) elif control_type == odemis.gui.CONTROL_TEXT: val = va.value accuracy = conf.get('accuracy', 3) val_str = value_to_str(val, unit, accuracy) # No pretty_time, as we don't support reading it back lbl_ctrl, value_ctrl = container.add_text_field(label_text, val_str) # To set the value on the control (when the VA is updated) def set_ctrl(value, ctrl=value_ctrl, u=unit, acc=accuracy): ctrl.SetValue(value_to_str(value, u, acc)) # To retrieve the actual value, which may contain prefix and unit def text_get(ctrl=value_ctrl, va=va): ctrl_value = ctrl.GetValue() va_val = va.value try: new_val = str_to_value(ctrl_value, va) except (ValueError, TypeError): logging.warning("Value %s couldn't be understood", ctrl_value, exc_info=True) new_val = va_val # To force going back to last value # if it ends up being the same value as before the VA will # not update, so force reformatting it if va_val == new_val: set_ctrl(va_val) return new_val setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, va_2_ctrl=set_ctrl, ctrl_2_va=text_get, events=wx.EVT_TEXT_ENTER) if change_callback: value_ctrl.Bind(wx.EVT_TEXT_ENTER, change_callback) elif control_type in (odemis.gui.CONTROL_SAVE_FILE, odemis.gui.CONTROL_OPEN_FILE): val = va.value if not val: config = guiconf.get_acqui_conf() val = config.last_path if control_type == odemis.gui.CONTROL_SAVE_FILE: dialog_style = wx.FD_SAVE else: # odemis.gui.CONTROL_OPEN_FILE dialog_style = wx.FD_OPEN clearlabel = conf.get('clearlabel') # Text to show when no filename (+ allow to clear the filename) wildcard = conf.get('wildcard', "*.*") # File extension wildcard string lbl_ctrl, value_ctrl = container.add_file_button(label_text, val, clearlabel, wildcard=wildcard, dialog_style=dialog_style) # Add the corresponding setting entry setting_entry = SettingEntry(name=label_text, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, events=EVT_FILE_SELECT) elif control_type == odemis.gui.CONTROL_SLIDER: # The slider is accompanied by an extra number text field if "type" in conf: if conf["type"] == "integer": # add_integer_slider factory = container.add_integer_slider elif conf["type"] == "slider": factory = container.add_slider else: factory = container.add_float_slider else: # guess from value(s) known_values = [va.value, min_val, max_val] if choices is not None: known_values.extend(list(choices)) if any(isinstance(v, float) for v in known_values): factory = container.add_float_slider else: factory = container.add_integer_slider # The event configuration determines what event will signal that the # setting entry has changed value. update_event = conf.get("event", wx.EVT_SLIDER) if update_event.typeId not in (wx.EVT_SCROLL_CHANGED.typeId, wx.EVT_SLIDER.typeId): raise ValueError("Illegal event type %d for Slider setting entry!" % (update_event.typeId,)) ctrl_conf = { 'min_val': min_val, 'max_val': max_val, 'scale': conf.get('scale', None), 'unit': unit, 'accuracy': conf.get('accuracy', 4), } lbl_ctrl, value_ctrl = factory(label_text, va.value, ctrl_conf) setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, events=update_event) if change_callback: if not isinstance(update_event, collections.Iterable): update_event = (update_event,) for event in update_event: value_ctrl.Bind(event, change_callback) elif control_type == odemis.gui.CONTROL_INT: if unit == "": # don't display unit prefix if no unit unit = None ctrl_conf = { 'min_val': min_val, 'max_val': max_val, 'unit': unit, 'choices': choices, } if 'key_step' in conf: ctrl_conf['key_step'] = conf['key_step'] if 'key_step_min' in conf: ctrl_conf['key_step_min'] = conf['key_step_min'] lbl_ctrl, value_ctrl = container.add_int_field(label_text, conf=ctrl_conf) setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, events=wx.EVT_COMMAND_ENTER) if change_callback: value_ctrl.Bind(wx.EVT_COMMAND_ENTER, change_callback) elif control_type == odemis.gui.CONTROL_FLT: if unit == "": # don't display unit prefix if no unit unit = None ctrl_conf = { 'min_val': min_val, 'max_val': max_val, 'unit': unit, 'choices': choices, 'accuracy': conf.get('accuracy', 5), } if 'key_step' in conf: ctrl_conf['key_step'] = conf['key_step'] if 'key_step_min' in conf: ctrl_conf['key_step_min'] = conf['key_step_min'] lbl_ctrl, value_ctrl = container.add_float_field(label_text, conf=ctrl_conf) setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, events=wx.EVT_COMMAND_ENTER) if change_callback: value_ctrl.Bind(wx.EVT_COMMAND_ENTER, change_callback) elif control_type == odemis.gui.CONTROL_CHECK: # Only supports boolean VAs lbl_ctrl, value_ctrl = container.add_checkbox_control(label_text, value=va.value) setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, events=wx.EVT_CHECKBOX) if change_callback: value_ctrl.Bind(wx.EVT_CHECKBOX, change_callback) elif control_type == odemis.gui.CONTROL_RADIO: unit_fmt = (choices_si_prefix or "") + (unit or "") ctrl_conf = { 'size': (-1, 16), 'units': unit_fmt, 'choices': [v for v, _ in choices_formatted], 'labels': [l for _, l in choices_formatted], } lbl_ctrl, value_ctrl = container.add_radio_control(label_text, conf=ctrl_conf) setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, events=wx.EVT_BUTTON) if change_callback: value_ctrl.Bind(wx.EVT_BUTTON, change_callback) elif control_type == odemis.gui.CONTROL_COMBO: accuracy = conf.get('accuracy', 3) # TODO: Might need size=(100, 16)!! cbconf = {} if hasattr(va, "choices") and isinstance(va.choices, collections.Iterable): # Enumerated VA => don't allow entering other values cbconf["style"] = wx.CB_READONLY lbl_ctrl, value_ctrl = container.add_combobox_control(label_text, conf=cbconf) # Set choices if choices_si_prefix: for choice, formatted in choices_formatted: value_ctrl.Append(u"%s %s" % (formatted, choices_si_prefix + unit), choice) else: for choice, formatted in choices_formatted: value_ctrl.Append(u"%s%s" % (formatted, unit), choice) # A small wrapper function makes sure that the value can # be set by passing the actual value (As opposed to the text label) def cb_set(value, va=va, ctrl=value_ctrl, u=unit, acc=accuracy): # Re-read the value from the VA because it'll be called via # CallAfter(), and if the value is changed multiple times, it might # not be in chronological order. value = va.value for i in range(ctrl.GetCount()): d = ctrl.GetClientData(i) if (d == value or (all(isinstance(v, float) for v in (value, d)) and util.almost_equal(d, value)) ): logging.debug("Setting combobox value to %s", ctrl.Items[i]) ctrl.SetSelection(i) break else: logging.debug("No existing label found for value %s in combobox ctrl %d", value, id(ctrl)) # entering value as free text txt = value_to_str(value, u, acc) ctrl.SetValue(txt) # equivalent wrapper function to retrieve the actual value def cb_get(ctrl=value_ctrl, va=va, u=unit): ctrl_value = ctrl.GetValue() # Try to use the predefined value if it's available i = ctrl.GetSelection() # Warning: if the text contains an unknown value, GetSelection will # not return wx.NOT_FOUND (as expected), but the last selection value if i != wx.NOT_FOUND and ctrl.Items[i] == ctrl_value: logging.debug("Getting item value %s from combobox control", ctrl.GetClientData(i)) return ctrl.GetClientData(i) else: va_val = va.value try: new_val = str_to_value(ctrl_value, va) except (ValueError, TypeError): logging.warning("Value %s couldn't be understood", ctrl_value, exc_info=True) new_val = va_val # To force going back to last value # if it ends up being the same value as before the combobox will # not update, so force it now if va_val == new_val: cb_set(va_val) return new_val setting_entry = SettingEntry(name=name, va=va, hw_comp=hw_comp, lbl_ctrl=lbl_ctrl, value_ctrl=value_ctrl, va_2_ctrl=cb_set, ctrl_2_va=cb_get, events=(wx.EVT_COMBOBOX, wx.EVT_TEXT_ENTER)) if change_callback: value_ctrl.Bind(wx.EVT_COMBOBOX, change_callback) value_ctrl.Bind(wx.EVT_TEXT_ENTER, change_callback) else: logging.error("Unknown control type %s", control_type) value_ctrl.SetToolTip(tooltip) lbl_ctrl.SetToolTip(tooltip) return setting_entry
def __init__(self, parent, orig_tab_data): xrcfr_overview_acq.__init__(self, parent) self.conf = get_acqui_conf() # True when acquisition occurs self.acquiring = False self.data = None # a ProgressiveFuture if the acquisition is going on self.acq_future = None self._acq_future_connector = None self._main_data_model = orig_tab_data.main # duplicate the interface, but with only one view self._tab_data_model = self.duplicate_tab_data_model(orig_tab_data) # The pattern to use for storing each tile file individually # None disables storing them self.filename_tiles = create_filename(self.conf.pj_last_path, "{datelng}-{timelng}-overview", ".ome.tiff") # Create a new settings controller for the acquisition dialog self._settings_controller = LocalizationSettingsController( self, self._tab_data_model, highlight_change=True # also adds a "Reset" context menu ) self.zsteps = model.IntContinuous(1, range=(1, 51)) self.tiles_nx = model.IntContinuous(5, range=(1, 1000)) self.tiles_ny = model.IntContinuous(5, range=(1, 1000)) self._zsteps_vac = VigilantAttributeConnector(self.zsteps, self.zstack_steps, events=wx.EVT_SLIDER) self._tiles_n_vacx = VigilantAttributeConnector( self.tiles_nx, self.tiles_number_x, events=wx.EVT_COMMAND_ENTER) self._tiles_n_vacy = VigilantAttributeConnector( self.tiles_ny, self.tiles_number_y, events=wx.EVT_COMMAND_ENTER) orig_view = orig_tab_data.focussedView.value self._view = self._tab_data_model.focussedView.value self.streambar_controller = StreamBarController(self._tab_data_model, self.pnl_secom_streams, static=True, ignore_view=True) # The streams currently displayed are the one visible self.add_streams() # The list of streams ready for acquisition (just used as a cache) self._acq_streams = {} # Compute the preset values for each preset self._orig_entries = get_global_settings_entries( self._settings_controller) self._orig_settings = preset_as_is(self._orig_entries) for sc in self.streambar_controller.stream_controllers: self._orig_entries += get_local_settings_entries(sc) self.start_listening_to_va() # make sure the view displays the same thing as the one we are # duplicating self._view.view_pos.value = orig_view.view_pos.value self._view.mpp.value = orig_view.mpp.value self._view.merge_ratio.value = orig_view.merge_ratio.value # attach the view to the viewport self.pnl_view_acq.canvas.fit_view_to_next_image = False self.pnl_view_acq.setView(self._view, self._tab_data_model) self.Bind(wx.EVT_CHAR_HOOK, self.on_key) self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close) self.btn_secom_acquire.Bind(wx.EVT_BUTTON, self.on_acquire) self.Bind(wx.EVT_CLOSE, self.on_close) # on_streams_changed is compatible because it doesn't use the args # Set parameters for tiled acq self.overlap = 0.2 try: # Use the stage range, which can be overridden by the MD_POS_ACTIVE_RANGE. # Note: this last one might be temporary, until we have a RoA tool provided in the GUI. self._tiling_rng = { "x": self._main_data_model.stage.axes["x"].range, "y": self._main_data_model.stage.axes["y"].range } stage_md = self._main_data_model.stage.getMetadata() if model.MD_POS_ACTIVE_RANGE in stage_md: self._tiling_rng.update(stage_md[model.MD_POS_ACTIVE_RANGE]) except (KeyError, IndexError): raise ValueError( "Failed to find stage.MD_POS_ACTIVE_RANGE with x and y range") # Note: It should never be possible to reach here with no streams streams = self.get_acq_streams() for s in streams: self._view.addStream(s) # To update the estimated time when streams are removed/added self._view.stream_tree.flat.subscribe(self.on_streams_changed, init=True) zstep = util.readable_str(ZSTEP, unit="m", sig=3) self.zstack_slider_step.SetLabel(zstep)
def ShowAcquisitionFileDialog(parent, filename): """ parent (wxFrame): parent window filename (string): full filename to propose by default Note: updates the acquisition configuration if the user did pick a new file return (string or None): the new filename (or the None if the user cancelled) """ conf = get_acqui_conf() # Find the available formats (and corresponding extensions) formats_to_ext = dataio.get_available_formats() # current filename path, base = os.path.split(filename) # Note: When setting 'defaultFile' when creating the file dialog, the # first filter will automatically be added to the name. Since it # cannot be changed by selecting a different file type, this is big # nono. Also, extensions with multiple periods ('.') are not correctly # handled. The solution is to use the SetFilename method instead. wildcards, formats = formats_to_wildcards(formats_to_ext) dialog = wx.FileDialog(parent, message="Choose a filename and destination", defaultDir=path, defaultFile="", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, wildcard=wildcards) # Select the last format used prev_fmt = conf.last_format try: idx = formats.index(conf.last_format) except ValueError: idx = 0 dialog.SetFilterIndex(idx) # Strip the extension, so that if the user changes the file format, # it will not have 2 extensions in a row. if base.endswith(conf.last_extension): base = base[:-len(conf.last_extension)] dialog.SetFilename(base) # Show the dialog and check whether is was accepted or cancelled if dialog.ShowModal() != wx.ID_OK: return None # New location and name have been selected... # Store the path path = dialog.GetDirectory() conf.last_path = path # Store the format fmt = formats[dialog.GetFilterIndex()] conf.last_format = fmt # Check the filename has a good extension, or add the default one fn = dialog.GetFilename() ext = None for extension in formats_to_ext[fmt]: if fn.endswith(extension) and len(extension) > len(ext or ""): ext = extension if ext is None: if fmt == prev_fmt and conf.last_extension in formats_to_ext[fmt]: # if the format is the same (and extension is compatible): keep # the extension. This avoid changing the extension if it's not # the default one. ext = conf.last_extension else: ext = formats_to_ext[fmt][0] # default extension fn += ext conf.last_extension = ext return os.path.join(path, fn)
def ShowAcquisitionFileDialog(parent, filename): """ parent (wxFrame): parent window filename (string): full filename to propose by default Note: updates the acquisition configuration if the user did pick a new file return (string): the new filename (or the old one if the user cancelled) """ conf = get_acqui_conf() # Find the available formats (and corresponding extensions) formats_to_ext = dataio.get_available_formats() # current filename path, base = os.path.split(filename) # Note: When setting 'defaultFile' when creating the file dialog, the # first filter will automatically be added to the name. Since it # cannot be changed by selecting a different file type, this is big # nono. Also, extensions with multiple periods ('.') are not correctly # handled. The solution is to use the SetFilename method instead. wildcards, formats = formats_to_wildcards(formats_to_ext) dialog = wx.FileDialog(parent, message="Choose a filename and destination", defaultDir=path, defaultFile="", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, wildcard=wildcards) # Select the last format used prev_fmt = conf.last_format try: idx = formats.index(conf.last_format) except ValueError: idx = 0 dialog.SetFilterIndex(idx) # Strip the extension, so that if the user changes the file format, # it will not have 2 extensions in a row. if base.endswith(conf.last_extension): base = base[:-len(conf.last_extension)] dialog.SetFilename(base) # Show the dialog and check whether is was accepted or cancelled if dialog.ShowModal() != wx.ID_OK: return filename # New location and name have been selected... # Store the path path = dialog.GetDirectory() conf.last_path = path # Store the format fmt = formats[dialog.GetFilterIndex()] conf.last_format = fmt # Check the filename has a good extension, or add the default one fn = dialog.GetFilename() ext = None for extension in formats_to_ext[fmt]: if fn.endswith(extension) and len(extension) > len(ext or ""): ext = extension if ext is None: if fmt == prev_fmt and conf.last_extension in formats_to_ext[fmt]: # if the format is the same (and extension is compatible): keep # the extension. This avoid changing the extension if it's not # the default one. ext = conf.last_extension else: ext = formats_to_ext[fmt][0] # default extension fn += ext conf.last_extension = ext conf.write() return os.path.join(path, fn)