# -*- coding: utf-8 -*- # import native python modules import time import math import sys import platform import io import os.path import traceback import tables import xml.parsers.expat import threading import webbrowser import xdg.BaseDirectory import tempfile import gi gi.require_version('Gtk', '3.0') gi.require_version('GtkSource', '3.0') gi.require_version('GdkPixbuf', '2.0') # import 3rd party modules # gui graphics from gi.repository import GObject as gobject import sqlite3 import uuid from gi.repository import GLib as glib from gi.repository import Gtk as gtk gtk_version_missmatch = gtk.check_version( 3, 0, 0 ) if gtk_version_missmatch: raise Exception( "insufficient gtk version: " + gtk_version_missmatch ) from gi.repository import Gdk as gdk from gi.repository import GdkPixbuf from gi.repository import GtkSource as gtksourceview # referencing the class registers the GtkSourceView GType, which GtkBuilder # needs to resolve the type when loading damaris3.glade _ = gtksourceview.View from gi.repository import Pango as pango # array math import numpy # math graphics import matplotlib import cairo from matplotlib import cbook from matplotlib.transforms import Bbox # force noninteractive matplotlib.rcParams[ "interactive" ] = "False" matplotlib.rcParams[ "text.usetex" ] = "False" matplotlib.rcParams[ "axes.formatter.limits" ] = "-3,3" from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas #from matplotlib.backends.backend_gtk3cairo import FigureCanvasGTK3Cairo as FigureCanvas max_points_to_display = 0 def apply_font_css(widget, font_string): """ Non-deprecated replacement for the deprecated widget.modify_font(): converts a pango font description string (family, optional size in pt, optional weight) into a CSS rule applied to the widget's style context. Examples: >>> apply_font_css(textview, "Monospace 10") >>> apply_font_css(label, "sans bold 16") """ fd = pango.FontDescription(font_string) decl = "font-family: %s;" % fd.get_family() if fd.get_size() > 0 and not fd.get_size_is_absolute(): decl += " font-size: %.6gpt;" % (fd.get_size() / float(pango.SCALE)) if fd.get_weight() != pango.Weight.NORMAL: decl += " font-weight: %d;" % fd.get_weight() provider = gtk.CssProvider() provider.load_from_data(("%s { %s }" % (widget.get_css_name(), decl)).encode("utf-8")) context = widget.get_style_context() context.add_provider(provider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) # keep a reference so the provider is not garbage collected setattr(widget, "_damaris_font_css_provider", provider) import matplotlib.axes import matplotlib.figure # import our own stuff from damaris.gui import ExperimentWriter, ExperimentHandling from damaris.gui import ResultReader, ResultHandling from damaris.gui import BackendDriver #from damaris.gui.gtkcodebuffer import CodeBuffer, SyntaxLoader #from damaris.data import Drawable # this is a base class, it should be used... from damaris.data import DataPool, Accumulation, ADC_Result, MeasurementResult, TemperatureResult, FitResult # TemperatureMonitor imported lazily to avoid circular import issues # default, can be set to true from start script debug = False # version info from damaris import __version__ # Unified logging import logging logger = logging.getLogger("damaris.gui") class LockFile: def __init__(self): self.index = 0 self.file = os.path.expanduser("~/.damaris.lockdb") self.conn = sqlite3.connect(self.file) self.cursor = self.conn.cursor() self.id = None logger.debug("Connected to db..") def add_experiment(self): logger.debug("Add experiment..") self.id = str(uuid.uuid4()) self.cursor.execute("INSERT INTO damaris VALUES (?, ?)",(self.id, "started")) self.conn.commit() return self.id def del_experiment(self, id): logger.debug("Delete experiment..") self.cursor.execute('DELETE FROM damaris WHERE uuid=?', (id,)) self.conn.commit() return True def am_i_next(self): first = self.cursor.execute("SELECT * FROM damaris ORDER BY ROWID ASC LIMIT 1") current_running = first.fetchone()[0] #print current_running,self.id if current_running == self.id: return True else: return False class DamarisGUI: ExpScript_Display = 0 ResScript_Display = 1 Monitor_Display = 2 Log_Display = 3 Config_Display = 4 Edit_State = 0 Run_State = 1 Pause_State = 2 Stop_State = 3 Quit_State = 4 def __init__(self, exp_script_filename=None, res_script_filename=None, start_immediately=False): # state: edit, run, stop, quit # state transitions: # edit -> run|quit # run -> pause|stop # pause -> run|stop # stop -> edit self.state = DamarisGUI.Edit_State # script execution engines and backend driver self.si = None # produced and displayed data self.data = None self.glade_layout_init( ) # my notebook self.main_notebook = self.xml_gui.get_object( "main_notebook" ) self.log = LogWindow( self.xml_gui , self) # Initialize unified logging with GUI output from damaris.gui import setup_logging self._logger = setup_logging( level=logging.DEBUG if debug else logging.INFO, gui_callback=self.log, ) self.sw = ScriptWidgets( self.xml_gui, self ) self.toolbar_init( ) self.documentation_init( ) self.monitor = MonitorWidgets( self.xml_gui ) self.config = ConfigTab( self.xml_gui ) # lock file to prevent other DAMARIS to start immediatly self.lockfile = LockFile() # = os.path.expanduser("~/.damaris.lock") self.id = None #to stop queued experiments self.stop_experiment_flag = threading.Event() # Temperature monitoring self.temp_monitor = None self.temp_controller = None exp_script = "" if exp_script_filename is not None and exp_script_filename != "": self.sw.exp_script_filename = exp_script_filename[ : ] if os.path.isfile( exp_script_filename ) and os.access( exp_script_filename, os.R_OK ): exp_script = self.sw.load_file_as_unicode( exp_script_filename ) res_script = "" if res_script_filename is not None and res_script_filename != "": self.sw.res_script_filename = res_script_filename[ : ] if os.path.isfile( res_script_filename ) and os.access( res_script_filename, os.R_OK ): res_script = self.sw.load_file_as_unicode( res_script_filename ) self.sw.set_scripts( exp_script, res_script ) if start_immediately: if exp_script and res_script: self.start_immediately = start_immediately else: raise RuntimeError("start_immediately requires both experiment and result script") else: self.start_immediately = False self.statusbar_init( ) #connect event handlers eventhandlers = {"on_toolbar_run_button_clicked": self.start_experiment, "on_toolbar_estimate_button_clicked": self.estimate_duration, "on_toolbar_pause_button_toggled": self.pause_experiment, "on_toolbar_stop_button_clicked": self.stop_experiment, "on_doc_menu_activate": self.show_doc_menu, "on_toolbar_manual_button_clicked": self.show_manual, "on_data_handling_column_textfield_value_changed": self.sw.column_line_widgets_changed_event, "on_data_handling_line_textfield_value_changed": self.sw.column_line_widgets_changed_event, "on_experiment_script_line_textfield_value_changed": self.sw.column_line_widgets_changed_event, "on_experiment_script_column_textfield_value_changed": self.sw.column_line_widgets_changed_event, "on_toolbar_open_file_button_clicked": self.sw.open_file, "on_toolbar_open_hdf5_button_clicked": self.sw.open_hdf5, "on_toolbar_new_button_clicked": self.sw.new_file, "on_toolbar_save_as_button_clicked": self.sw.save_file_as, "on_toolbar_save_file_button_clicked": self.sw.save_file, "on_toolbar_save_all_button_clicked": self.sw.save_all_files, "on_toolbar_check_scripts_button_clicked": self.sw.check_script, "on_toolbar_undo_button_clicked": self.sw.undo, "on_toolbar_redo_button_clicked": self.sw.redo, "on_toolbar_search_button_clicked": self.sw.search, "on_config_save_button_clicked": self.config.save_config_handler, "on_config_load_button_clicked": self.config.load_config_handler, "on_backend_executable_browse_button_clicked": self.config.browse_backend_executable_dialog, "on_fontbutton_font_set": self.config.set_script_font_handler, "on_display_autoscaling_checkbutton_toggled": self.monitor.display_autoscaling_toggled, "on_display_statistics_checkbutton_toggled": self.monitor.display_statistics_toggled, "on_display_x_scaling_combobox_changed": self.monitor.display_scaling_changed, "on_display_y_scaling_combobox_changed": self.monitor.display_scaling_changed, "on_display_save_data_as_text_button_clicked": self.monitor.save_display_data_as_text, "on_display_copy_data_to_clipboard_button_clicked": self.monitor.copy_display_data_to_clipboard, "on_experiment_script_textview_event": self.sw.textviews_modified, "on_main_notebook_switch_page": self.nopevent, "main_window_close": self.nopevent } self.xml_gui.connect_signals(eventhandlers) self.main_window.maximize() self.main_window.show_all( ) self.main_window.present( ) def glade_layout_init( self ): glade_file = os.path.join( os.path.dirname( __file__ ), "damaris3.glade" ) self.xml_gui = gtk.Builder() self.xml_gui.add_from_file(glade_file) self.main_window = self.xml_gui.get_object( "main_window" ) self.main_window.connect( "delete-event", self.quit_event ) for icon_path in ["/usr/share/pixmaps/DAMARIS3.png", os.path.join( os.path.dirname( __file__ ), "DAMARIS3.png" )]: if os.path.exists(icon_path): self.main_window.set_icon_from_file(icon_path) break self.main_window.set_title( "DAMARIS-%s" % __version__ ) def statusbar_init( self ): """ experiment and result thread status, backend state """ self.experiment_script_statusbar_label = self.xml_gui.get_object( "statusbar_experiment_script_label" ) self.data_handling_statusbar_label = self.xml_gui.get_object( "statusbar_data_handling_label" ) self.backend_statusbar_label = self.xml_gui.get_object( "statusbar_core_label" ) def toolbar_init( self ): """ buttons like save and run... """ self.toolbar_stop_button = self.xml_gui.get_object( "toolbar_stop_button" ) self.toolbar_run_button = self.xml_gui.get_object( "toolbar_run_button" ) self.toolbar_pause_button = self.xml_gui.get_object( "toolbar_pause_button" ) # prepare for edit state self.toolbar_run_button.set_sensitive( True ) self.toolbar_stop_button.set_sensitive( False ) self.toolbar_pause_button.set_sensitive( False ) def run( self ): # prolong lifetime of clipboard till the very end (avoid error message) self.main_clipboard = self.sw.main_clipboard if self.start_immediately: self.toolbar_run_button.emit("clicked") gtk.main( ) self.si = None self.sw = None self.config = None self.xml_gui = None def observe_data_pool( self, data_pool ): """ Delegate to monitor widgets """ self.data = data_pool if hasattr(self, "monitor"): self.monitor.observe_data_pool(data_pool) # event handling: the real acitons in gui programming # first global events def nopevent(self, widget, data=None, otherargument=None): #print(widget) #print(data) #print(otherargument) pass def quit_event( self, widget, data=None ): """ expecting quit event for main application """ if self.state in [ DamarisGUI.Edit_State, DamarisGUI.Quit_State ]: self.state = DamarisGUI.Quit_State # do a cleanup... if self.sw.experiment_script_textbuffer.get_modified( ): question = gtk.MessageDialog(parent=self.xml_gui.get_object( "main_window"), type=gtk.MessageType.WARNING, flags=gtk.DialogFlags.MODAL, buttons=("_Cancel", gtk.ResponseType.CANCEL, "_No", gtk.ResponseType.NO, "_Yes", gtk.ResponseType.YES)) question.set_markup("The Experiment Script has been changed. Save changes?") response = question.run() question.destroy() if response == gtk.ResponseType.CANCEL: return True elif response == gtk.ResponseType.YES: current_page = self.main_notebook.get_current_page( ) self.main_notebook.set_current_page(0) if self.sw.exp_script_filename is None: self.sw.save_file_as() else: self.sw.save_file() self.main_notebook.set_current_page(current_page) if self.sw.data_handling_textbuffer.get_modified( ): question = gtk.MessageDialog(parent=self.xml_gui.get_object( "main_window"), type=gtk.MessageType.WARNING, flags=gtk.DialogFlags.MODAL, buttons=("_Cancel", gtk.ResponseType.CANCEL, "_No", gtk.ResponseType.NO, "_Yes", gtk.ResponseType.YES)) question.set_markup("The Result Script has been changed. Save changes?") response = question.run() question.destroy() if response == gtk.ResponseType.CANCEL: return True elif response == gtk.ResponseType.YES: current_page = self.main_notebook.get_current_page( ) self.main_notebook.set_current_page(1) if self.sw.res_script_filename is None: self.sw.save_file_as() else: self.sw.save_file() self.main_notebook.set_current_page(current_page) self.config = None self.sw = None self.monitor = None self.log = None # and quit gtk.main_quit( ) return True else: question = gtk.MessageDialog(parent=self.xml_gui.get_object( "main_window"), type=gtk.MessageType.ERROR, flags=gtk.DialogFlags.MODAL, buttons=("_Understood!", gtk.ResponseType.OK)) question.set_markup("Experiment still running, can not close DAMARIS!") response = question.run() question.destroy() return True # toolbar related events: def estimate_duration( self, widget, data=None ): """ Estimate the total experiment duration and show a dialog. """ from datetime import datetime, timedelta from damaris.experiments.DurationEstimator import estimate_duration exp_script, _ = self.sw.get_scripts() # validate syntax first try: compile(exp_script, "Experiment Script", "exec") except SyntaxError as e: ln = e.lineno if isinstance(e.lineno, int) else 0 lo = e.offset if isinstance(e.offset, int) else 0 msg = "Experiment Script: %s at line %d, col %d" % (e.__class__.__name__, ln, lo) dialog = gtk.MessageDialog( parent=self.main_window, flags=gtk.DialogFlags.MODAL, type=gtk.MessageType.ERROR, buttons=gtk.ButtonsType.OK, message_format=msg ) dialog.run() dialog.destroy() return # run estimator in a background thread to not freeze the GUI dialog = gtk.MessageDialog( parent=self.main_window, flags=gtk.DialogFlags.MODAL, type=gtk.MessageType.INFO, buttons=gtk.ButtonsType.CLOSE, message_format="Estimating duration..." ) dialog.set_resizable(False) dialog.show() result = [None] error = [None] done = threading.Event() def run_estimate(): try: result[0] = estimate_duration(exp_script) except Exception as e: error[0] = str(e) done.set() t = threading.Thread(target=run_estimate) t.daemon = True t.start() while not done.is_set(): while gtk.events_pending(): gtk.main_iteration_do(False) if done.wait(0.1): break dialog.hide() dialog.destroy() if error[0] is not None: dialog = gtk.MessageDialog( parent=self.main_window, flags=gtk.DialogFlags.MODAL, type=gtk.MessageType.ERROR, buttons=gtk.ButtonsType.OK, message_format="Estimation failed: " + error[0] ) dialog.run() dialog.destroy() return r = result[0] total = r["total_time"] exp_time = r["experiment_time"] sleep_t = r["sleep_time"] jobs = r["job_count"] # format duration as HH:MM:SS def fmt(seconds): h = int(seconds) // 3600 m = (int(seconds) % 3600) // 60 s = int(seconds) % 60 return "%02d:%02d:%02d" % (h, m, s) now = datetime.now() end = now + timedelta(seconds=total) end_str = end.strftime("%H:%M:%S") text = ( "Jobs: %d\n" "Pulse program: %s (%.2f s)\n" "Sleeps: %s (%.2f s)\n" "\n" "Total: %s\n\n" "Estimated end: %s" ) % (jobs, fmt(exp_time), exp_time, fmt(sleep_t), sleep_t, fmt(total), end_str) dialog = gtk.MessageDialog( parent=self.main_window, flags=gtk.DialogFlags.MODAL, type=gtk.MessageType.INFO, buttons=gtk.ButtonsType.OK, message_format=text ) dialog.run() dialog.destroy() def start_experiment( self, widget, data=None ): import time overall_start = time.time() logger.debug("start_experiment called at %s", overall_start) # something running? if self.si is not None or self.state != DamarisGUI.Edit_State: logger.error("Experiment is still running or cleaning up!") return # get config values: config_start = time.time() actual_config = self.config.get( ) logger.debug("Config load took %.3fs", time.time() - config_start) # get scripts and start script interface gui_start = time.time() self.sw.disable_editing( ) exp_script, res_script = self.sw.get_scripts( ) logger.debug("GUI operations took %.3fs", time.time() - gui_start) if not actual_config[ "start_result_script" ]: res_script = "" if not actual_config[ "start_experiment_script" ]: exp_script = "" backend = actual_config[ "backend_executable" ] if not actual_config[ "start_backend" ]: backend = "" if (backend == "" and exp_script == "" and res_script == ""): logger.info("Nothing to do...so doing nothing!") self.sw.enable_editing( ) return # check whether scripts are syntactically valid # should be merged with check script function compile_start = time.time() exp_code = None if exp_script != "": try: exp_code = compile( exp_script, "Experiment Script", "exec" ) except SyntaxError as e: ln = e.lineno lo = e.offset if type( ln ) is not int: ln = 0 if type( lo ) is not int: lo = 0 logger.error("Experiment Script: %s at line %d, col %d", e.__class__.__name__, ln, lo) if e.text != "": logger.error(e.text.rstrip()) logger.error(" "*(e.offset-1)+"^") logger.error("%s", e) res_code = None if res_script != "": # Continue with result script compilation try: res_code = compile( res_script, "Result Script", "exec" ) except SyntaxError as e: ln = e.lineno lo = e.offset if type( ln ) is not int: ln = 0 if type( lo ) is not int: lo = 0 logger.error("Result script: %s at line %d, col %d", e.__class__.__name__, ln, lo) if e.text != "": logger.error("\"%s\"", e.text) # logger.error(" "*(e.offset+1)+"^" # nice idea, but needs monospaced fonts pass logger.error("%s", e) logger.debug("Script compilation took %.3fs", time.time() - compile_start) # detect error if (exp_script != "" and exp_code is None) or \ (res_script != "" and res_code is None): self.main_notebook.set_current_page( DamarisGUI.Log_Display ) self.sw.enable_editing( ) return # prepare to run self.state = DamarisGUI.Run_State self.toolbar_run_button.set_sensitive( False ) self.toolbar_stop_button.set_sensitive( True ) self.toolbar_pause_button.set_sensitive( True ) self.toolbar_pause_button.set_active( False ) # delete old data - use efficient cleanup data_cleanup_start = time.time() self.data = None # Clean up monitor widgets and data pool if hasattr(self, 'monitor') and self.monitor is not None: listener_count = 0 data_items = 0 if hasattr(self.monitor, 'data_pool') and self.monitor.data_pool is not None: try: with self.monitor.data_pool._dictlock: listener_count = len(self.monitor.data_pool._registered_listeners) if hasattr(self.monitor.data_pool, '_registered_listeners') else 0 data_items = len(self.monitor.data_pool) except: pass # Use observe_data_pool(None) which is now optimized with fast TreeStore replacement logger.debug("Data pool cleanup (%d listeners, %d items)", listener_count, data_items) self.monitor.observe_data_pool( None ) logger.debug("Data cleanup took %.3fs", time.time() - data_cleanup_start) # set the text mark for hdf logging with self.log.lock: if self.log.textbuffer.get_mark( "lastdumped" ) is None: self.log.textbuffer.create_mark( "lastdumped", self.log.textbuffer.get_end_iter( ), left_gravity=True ) loop_run=0 #user shorter interval because this blocks user events from the GUI interval = 0.1 self.stop_experiment_flag.clear() start_time = time.time() logger.debug("Starting experiment at %s", start_time) self.id = self.lockfile.add_experiment() logger.debug("Lock file operation took %.3fs", time.time() - start_time) # Add more timing points lock_wait_start = time.time() while not self.lockfile.am_i_next(): while gtk.events_pending(): gtk.main_iteration_do(False) if self.stop_experiment_flag.wait(interval): break if loop_run > 1: self.experiment_script_statusbar_label.set_text("Waiting for other experiment to finish") loop_run = 0 loop_run += interval logger.debug("Lock wait took %.3fs", time.time() - lock_wait_start) if self.stop_experiment_flag.is_set(): #Experiment has been stopped, clean up and leave self.experiment_script_statusbar_label.set_text("Experiment stopped") self.state = DamarisGUI.Edit_State self.sw.enable_editing( ) self.toolbar_run_button.set_sensitive( True ) self.toolbar_stop_button.set_sensitive( False ) self.toolbar_pause_button.set_sensitive( False ) self.toolbar_pause_button.set_active( False ) #delete experiment from queue so that next experiment may start self.lockfile.del_experiment(self.id) return # start experiment try: self.spool_dir = os.path.abspath( actual_config[ "spool_dir" ] ) # setup script engines script_interface_start = time.time() self.si = ScriptInterface( exp_code, res_code, backend, self.spool_dir, log_callback=self.log, clear_jobs=actual_config[ "del_jobs_after_execution" ], clear_results=actual_config[ "del_results_after_processing" ] ) logger.debug("ScriptInterface creation took %.3fs", time.time() - script_interface_start) self.data = self.si.data # run frontend and script engines self.monitor.observe_data_pool( self.data ) # Start temperature monitoring if configured (before scripts start) temp_start = time.time() self.start_temperature_monitoring(actual_config) logger.debug("Temperature monitoring startup took %.3fs", time.time() - temp_start) scripts_start = time.time() self.si.runScripts( ) logger.debug("Script execution startup took %.3fs", time.time() - scripts_start) except Exception as e: #logger.debug("ToDo evaluate exception %s at %s", str(e), traceback.extract_tb(sys.exc_info()[2])[-1][1:3]) #logger.debug("Full traceback:") traceback_file = io.StringIO( ) traceback.print_tb( sys.exc_info( )[ 2 ], None, traceback_file ) self.main_notebook.set_current_page( DamarisGUI.Log_Display ) logger.error("Error while executing scripts: %s", str( e ) + traceback_file.getvalue( )) traceback_file = None self.data = None if self.si is not None: still_running = [_f for _f in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver ] if _f] for r in still_running: r.quit_flag.set( ) logger.info("Waiting for threads to stop...") still_running = [x for x in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver ] if x is not None and x.is_alive( )] for t in still_running: while t.is_alive(): while gtk.events_pending(): gtk.main_iteration_do(False) t.join( 0.05 ) logger.info("Threads stopped") # cleanup self.si = None self.state = DamarisGUI.Edit_State self.sw.enable_editing( ) self.toolbar_run_button.set_sensitive( True ) self.toolbar_stop_button.set_sensitive( False ) self.toolbar_pause_button.set_sensitive( False ) self.toolbar_pause_button.set_active( False ) #delete Experiment from queue self.lockfile.del_experiment(self.id) return # switch to grapics self.main_notebook.set_current_page( DamarisGUI.Monitor_Display ) # set running if self.si.exp_handling is not None: self.experiment_script_statusbar_label.set_text( "Experiment Script Running (0)" ) else: self.experiment_script_statusbar_label.set_text( "Experiment Script Idle" ) if self.si.res_handling is not None: self.data_handling_statusbar_label.set_text( "Result Script Running (0)" ) else: self.data_handling_statusbar_label.set_text( "Result Script Idle" ) if self.si.back_driver is not None: self.backend_statusbar_label.set_text( "Backend Running" ) else: self.backend_statusbar_label.set_text( "Backend Idle" ) # start data dump self.dump_thread = None self.save_thread = None self.dump_filename = "" if actual_config[ "data_pool_name" ] != "": self.dump_states( init=True, logtext=self.log.snapshot_since_last_dump( ) ) glib.timeout_add( 200, self.observe_running_experiment ) def observe_running_experiment( self ): """ periodically look at running threads """ if self.si is None: return False # look at components and update them # test whether backend and scripts are done r = self.si.data.get( "__recentresult", -1 ) + 1 b = self.si.data.get( "__resultsinadvance", -1 ) + 1 e = self.si.data.get( "__recentexperiment", -1 ) + 1 experimentstarttime = self.si.data.get( "__experimentstarted", 0 ) expectedexperimentruntime = self.si.data.get( "__totalexperimentlength", 0 ) experimentsfinishedtime = 0.0 for i in range(0, b): experimentsfinishedtime += self.si.data.get("__experimentlengths", {}).get(i, 0.0) experimentruntime = time.time() - experimentstarttime experimenttimetext = "" if experimentstarttime > 0 and expectedexperimentruntime > 0: runtimemin, runtimesec = divmod(math.floor(experimentruntime), 60) runtimehours, runtimemin = divmod(runtimemin, 60) experimenttimetext += " ({:02d}:{:02d}:{:02d} / ".format(int(runtimehours), int(runtimemin), int(runtimesec)) expectedmin, expectedsec = divmod(math.ceil(expectedexperimentruntime), 60) expectedhours, expectedmin = divmod(expectedmin, 60) experimenttimetext += "{:02d}:{:02d}:{:02d})".format(int(expectedhours), int(expectedmin), int(expectedsec)) backendtimetext = "" if experimentsfinishedtime > 0: finexperimmin, finexperimsec = divmod(round(experimentsfinishedtime), 60) finexperimhours, finexperimmin = divmod(finexperimmin, 60) backendtimetext = " ({:02d}:{:02d}:{:02d})".format(int(finexperimhours), int(finexperimmin), int(finexperimsec)) e_text = None r_text = None b_text = None if self.si.exp_handling is not None: if not self.si.exp_handling.is_alive( ): self.si.exp_handling.join( ) if self.si.exp_handling.raised_exception: logger.error("Experiment script failed at line %d (function %s): %s", self.si.exp_handling.location[ 0 ], self.si.exp_handling.location[ 1 ], self.si.exp_handling.raised_exception) logger.error("Full traceback: %s", self.si.exp_handling.traceback) e_text = "Experiment Script Failed (%d)" % e else: e_text = "Experiment Script Finished (%d)" % e logger.info("Experiment script finished") self.si.exp_handling = None else: # Backend messages are now handled by FileTailerHandler e_text = "Experiment Script Running (%d)" % e e_text += experimenttimetext if self.si.res_handling is not None: if not self.si.res_handling.is_alive( ): self.si.res_handling.join( ) if self.si.res_handling.raised_exception: logger.error("Result script failed at line %d (function %s): %s", self.si.res_handling.location[ 0 ], self.si.res_handling.location[ 1 ], self.si.res_handling.raised_exception) logger.error("Full traceback: %s", self.si.res_handling.traceback) r_text = "Result Script Failed (%d)" % r else: r_text = "Result Script Finished (%d)" % r self.si.res_handling = None else: r_text = "Result Script Running (%d)" % r if self.si.back_driver is not None: if not self.si.back_driver.is_alive( ): if self.si.back_driver.raised_exception: b_text = "Backend Failed" else: b_text = "Backend Finished" self.si.back_driver.join( ) self.si.back_driver = None else: b_text = "Backend Running" if b != 0: b_text += " (%d)" % b b_text += backendtimetext if self.dump_thread is not None: if self.dump_thread.is_alive( ): sys.stderr.write( "." ) self.dump_dots += 1 if self.dump_dots > 80: sys.stderr.write("\n") self.dump_dots = 0 else: self.dump_thread.join( ) self.dump_thread = None dump_size = os.stat( self.dump_filename ).st_size / 1e6 logger.info("Done (%.1f s, %.1f MB)", time.time( ) - self.dump_start_time, dump_size) if e_text: self.experiment_script_statusbar_label.set_text( e_text ) if r_text: self.data_handling_statusbar_label.set_text( r_text ) if b_text: self.backend_statusbar_label.set_text( b_text ) still_running = [_f for _f in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver, self.dump_thread ] if _f] if len( still_running ) == 0: # Stop temperature monitoring when experiment finishes self.stop_temperature_monitoring() if self.save_thread is None and self.dump_filename != "": logger.info("All subprocesses ended, saving data pool") # Disable run button during data saving to prevent UI confusion self.toolbar_run_button.set_sensitive(False) # thread to save data... snapshot log on the main thread first self.save_thread = threading.Thread( target=self.dump_states, args=(False, self.log.snapshot_since_last_dump( )), name="dump states" ) self.save_thread.start( ) self.dump_start_time = time.time( ) self.dump_dots = 0 self.state = DamarisGUI.Stop_State if self.state == DamarisGUI.Stop_State: self.toolbar_pause_button.set_sensitive( False ) self.toolbar_stop_button.set_sensitive( False ) if len( still_running ) != 0: logger.warning("Subprocess(es) still running: %s", ', '.join( [s.name for s in still_running] )) return True else: if self.save_thread is not None: if self.save_thread.is_alive( ): sys.stderr.write( "." ) self.dump_dots += 1 if self.dump_dots > 80: sys.stderr.write("\n") self.dump_dots = 0 return True self.save_thread.join( ) self.save_thread = None dump_size = os.stat( self.dump_filename ).st_size / 1e6 logger.info("Done (%.1f s, %.1f MB)", time.time( ) - self.dump_start_time, dump_size) # close hdf5 files that the scripts left open (e.g. after a # script failure), otherwise pytables keeps them registered # as open and they block the next experiment run self.close_left_open_hdf5_files( ) # keep data to display but throw away everything else self.si = None # delete lock file so that other experiment can start self.lockfile.del_experiment(self.id) # now everything is stopped self.state = DamarisGUI.Edit_State self.sw.enable_editing( ) self.toolbar_run_button.set_sensitive( True ) self.toolbar_stop_button.set_sensitive( False ) self.toolbar_pause_button.set_sensitive( False ) return False # dump states? dump_interval = self.dump_timeinterval if self.dump_timeinterval > 0 else 2.0 if self.dump_thread is None and self.dump_filename != "" and \ self.last_dumped + dump_interval < time.time( ): logger.info("Dumping data pool") self.dump_start_time = time.time( ) self.dump_dots = 0 # thread to save data... snapshot log on the main thread first self.dump_thread = threading.Thread( target=self.dump_states, args=(False, self.log.snapshot_since_last_dump( )), name="dump states" ) self.dump_thread.start( ) # or look at them again return True def dump_states( self, init=False, logtext=None ): """ init: constructs basic structure of this file logtext: pending log window text, captured on the GTK main thread compress: optional argument for zlib compression 0-9 """ if init: # calculate new settings for hdf files actual_config = self.config.get( ) # provide name tags for dump file name extensions_dict = { "date": time.strftime( "%Y-%m-%d" ), "datetime": time.strftime( "%Y-%m-%d_%H%M" ) } if self.sw.exp_script_filename is None: extensions_dict[ "exp" ] = "unnamed" else: extensions_dict[ "exp" ] = os.path.splitext( os.path.basename( self.sw.exp_script_filename ) )[ 0 ] if self.sw.res_script_filename is None: extensions_dict[ "res" ] = "unnamed" else: extensions_dict[ "res" ] = os.path.splitext( os.path.basename( self.sw.res_script_filename ) )[ 0 ] # create new dump file name try: self.dump_filename = actual_config.get( "data_pool_name" ) % extensions_dict except ValueError: logger.error("Invalid formatting character in '%s'", actual_config.get( "data_pool_name" )) self.dump_filename = "DAMARIS_data_pool.h5" logger.info("Resetting dumpfile to %s", self.dump_filename) del extensions_dict if os.path.split( self.dump_filename )[ 1 ] == "" or os.path.isdir( self.dump_filename ): logger.warning("Dump filename is a directory, using filename 'DAMARIS_data_pool.h5'") self.dump_filename += os.sep + "DAMARIS_data_pool.h5" # compression self.dump_complib = actual_config.get( "data_pool_complib", None ) if self.dump_complib == "None": self.dump_complib = None if self.dump_complib is not None: self.dump_complevel = int( actual_config.get( "data_pool_comprate", 0 ) ) else: self.dump_complevel = 0 # time interval self.dump_timeinterval = 60 * 60 * 10 try: self.dump_timeinterval = int( 60 * float( actual_config[ "data_pool_write_interval" ] ) ) except ValueError as e: logger.warning("Configuration provides non-number for dump interval: %s", str( e )) # if existent, move away old files if os.path.isfile( self.dump_filename ): # create bakup name pattern dump_filename_pattern = None (filename, ext) = os.path.splitext( self.dump_filename ) if ext in [ ".h5", ".hdf", ".hdf5" ]: dump_filename_pattern = filename.replace( "%", "%%" ) + "_%d" + ext else: dump_filename_pattern = self.dump_filename.replace( "%", "%%" ) + "_%d" last_backup = 0 cummulated_size = os.stat( self.dump_filename ).st_size while os.path.isfile( dump_filename_pattern % last_backup ): cummulated_size += os.stat( dump_filename_pattern % last_backup ).st_size last_backup += 1 while last_backup > 0: os.rename( dump_filename_pattern % (last_backup - 1), dump_filename_pattern % last_backup ) last_backup -= 1 os.rename( self.dump_filename, dump_filename_pattern % 0 ) if cummulated_size > (1 << 30): logger.warning("The cumulated backups size of '%s' is %d MByte", self.dump_filename, cummulated_size / (1 << 20)) # init is finnished now # now it's time to create the hdf file dump_file = None try: if not os.path.isfile( self.dump_filename ): if not init: logger.warning("Dump file \"%s\" vanished unexpectedly, creating new one", self.dump_filename) # have a look to the path and create necessary directories dir_stack = [ ] dir_trunk = os.path.dirname( os.path.abspath( self.dump_filename ) ) while dir_trunk != "" and not os.path.isdir( dir_trunk ): dir_stack.append( os.path.basename( dir_trunk ) ) dir_trunk = os.path.dirname( dir_trunk ) try: while len( dir_stack ): dir_trunk = os.path.join( dir_trunk, dir_stack.pop( ) ) if os.path.isdir( dir_trunk ): continue os.mkdir( dir_trunk ) except OSError as e: logger.error("Could not create dump directory '%s', so hdf5 dumps disabled: %s", dir_trunk, e) self.dump_filename = "" self.dump_timeinterval = 0 return True # create new dump file dump_file = tables.open_file( self.dump_filename, mode="w", title="DAMARIS experiment data" ) # write scripts and other useful information scriptgroup = dump_file.create_group( "/", "scripts", "Used Scripts" ) exp_text, res_text = self.sw.get_scripts( ) if self.si.exp_script: dump_file.create_array( scriptgroup, "experiment_script", exp_text.encode('utf-8') ) if self.si.res_script: dump_file.create_array( scriptgroup, "result_script", res_text.encode('utf-8') ) if self.si.backend_executable: dump_file.create_array( scriptgroup, "backend_executable", self.si.backend_executable.encode('utf-8') ) if self.spool_dir: dump_file.create_array( scriptgroup, "spool_directory", self.spool_dir.encode('utf-8') ) timeline_tablecols = numpy.recarray( 0, dtype=([ ("time", "S17"), ("experiments", "int64"), ("results", "int64") ]) ) timeline_table = dump_file.create_table( "/", "timeline", timeline_tablecols, title="Timeline of Experiment" ) if tables.__version__[ 0 ] == "1": logarray = dump_file.create_vlarray( where=dump_file.root, name="log", atom=tables.StringAtom( length=120 ), title="log messages", filters=tables.Filters( complevel=9, complib='zlib' ) ) else: logarray = dump_file.create_earray( where=dump_file.root, name="log", atom=tables.StringAtom( itemsize=120 ), shape=(0,), title="log messages", filters=tables.Filters( complevel=9, complib='zlib' ) ) if dump_file is None and os.path.isfile( self.dump_filename ) and tables.is_pytables_file( self.dump_filename ): # prepare for update dump_file = tables.open_file( self.dump_filename, mode="r+" ) if dump_file is None: # exit! logger.error("Could not create dump directory '%s', so hdf5 dumps disabled", dir_trunk) self.dump_filename = "" self.dump_timeinterval = 0 return True # no undo please! if dump_file.is_undo_enabled( ): dump_file.disable_undo( ) # save the data! if self.data is not None: self.data.write_hdf5( dump_file, where="/", name="data_pool", complib=self.dump_complib, complevel=self.dump_complevel, incremental=not init, write_interval=self.dump_timeinterval ) # Re-enable run button if this was called from save thread # NO: this is dangerous as it allows starting a new experiment before cleanup is finished. # it will be re-enabled in observe_running_experiment after joining this thread. # if not init and threading.current_thread().name == "dump states": # gdk.threads_enter() # self.toolbar_run_button.set_sensitive(True) # gdk.threads_leave() # now save additional information e = 0 r = 0 if self.si is not None: e = self.si.data.get( "__recentexperiment", -1 ) + 1 r = self.si.data.get( "__recentresult", -1 ) + 1 timeline_table = dump_file.root.timeline timeline_row = timeline_table.row timeline_row[ "time" ] = time.strftime( "%Y%m%d %H:%M:%S" ) timeline_row[ "experiments" ] = e timeline_row[ "results" ] = r timeline_row.append( ) timeline_table.flush( ) # save log window information: # also save backend's logfile information? # logtext was snapshotted on the GTK main thread before spawning if logtext is not None: for l in logtext.splitlines( ): dump_file.root.log.append( numpy.array( [ l ], dtype="S120" ) ) dump_file.flush( ) finally: if dump_file is not None: dump_file.close( ) self.last_dumped = time.time( ) return True def close_left_open_hdf5_files( self ): """ close hdf5 files that experiment or result scripts left open, e.g. after a script failure; pytables keeps such files registered as open, so they cannot be reopened in the next experiment run """ open_files = getattr( tables.file, "_open_files", None ) if open_files is None: return if hasattr( open_files, "handlers" ): filehandles = list( open_files.handlers ) else: filehandles = list( open_files.values( ) ) for filehandle in filehandles: try: logger.warning( "Closing hdf5 file left open by script: %s", filehandle.filename ) filehandle.close( ) except Exception: pass def pause_experiment( self, widget, data=None ): """ pause experiment execution (that means delay backend and let others run) """ if self.si is None: return False pause_state = self.toolbar_pause_button.get_active( ) if pause_state: if self.state != DamarisGUI.Run_State: return False if self.spool_dir is None: return False no = self.si.data.get( "__recentresult", -1 ) + 1 result_pattern = os.path.join( self.spool_dir, "job.%09d.result" ) job_pattern = os.path.join( self.spool_dir, "job.%09d" ) while os.path.isfile( result_pattern % no ): no += 1 i = 0 self.pause_files = [ ] while i < 3 and os.path.isfile( job_pattern % (no + i) ): pause_file = (job_pattern % (no + i)) + ".pause" os.rename( job_pattern % (no + i), pause_file ) self.pause_files.append( pause_file ) i += 1 self.state = DamarisGUI.Pause_State self.backend_statusbar_label.set_text( "Backend Paused" ) else: if self.state != DamarisGUI.Pause_State: return False self.state = DamarisGUI.Run_State for f in self.pause_files: os.rename( f, f[ :-6 ] ) self.pause_files = None self.backend_statusbar_label.set_text( "Backend Running" ) def stop_experiment( self, widget, data=None ): if self.state in [ DamarisGUI.Run_State, DamarisGUI.Pause_State ]: self.stop_experiment_flag.set() if self.si is None: return still_running = [_f for _f in [ self.si.exp_handling, self.si.res_handling, self.si.back_driver ] if _f] for r in still_running: r.quit_flag.set( ) # Stop temperature monitoring self.stop_temperature_monitoring() self.state = DamarisGUI.Stop_State def start_temperature_monitoring(self, config): """ Start temperature monitoring thread if configured. Args: config: Configuration dictionary from self.config.get() """ # Check if temperature monitoring is enabled if not config.get("enable_temperature_monitoring", False): # Stop any existing monitor if temperature monitoring is disabled self.stop_temperature_monitoring() return # Stop any existing monitor before starting a new one if self.temp_monitor is not None: self.stop_temperature_monitoring() controller_type = config.get("temperature_controller_type", "eurotherm") temp_interval = config.get("temperature_interval", 2.0) try: # Import the factory and TemperatureMonitor (lazy import to avoid circular issues) from damaris.tools.temperature import create_controller from damaris.gui.TemperatureMonitor import TemperatureMonitor # Create controller based on type if controller_type == "eurotherm": temp_device = config.get("temperature_device", "") temp_baudrate = config.get("temperature_baudrate", 19200) if not temp_device: logger.warning("Temperature monitoring: No device configured for Eurotherm") return self.temp_controller = create_controller( "eurotherm", temp_device, baudrate=temp_baudrate ) logger.info("Temperature monitoring started with Eurotherm on %s (interval: %ss)", temp_device, temp_interval) elif controller_type == "simulated": # Simulated controller doesn't need device configuration # Use default parameters: 290K base, 10K amplitude, 60s period, 5% noise, 10% missing self.temp_controller = create_controller("simulated") logger.info("Temperature monitoring started with simulated controller (interval: %ss)", temp_interval) else: logger.warning("Temperature monitoring: Unknown controller type: %s", controller_type) return # Create and start monitor thread self.temp_monitor = TemperatureMonitor( controller=self.temp_controller, interval=temp_interval, data_pool=self.data, data_key="temperature" ) self.temp_monitor.start() except Exception as e: logger.error("Failed to start temperature monitoring: %s", e) import traceback traceback.print_exc() self.temp_controller = None self.temp_monitor = None def stop_temperature_monitoring(self): """ Stop the temperature monitoring thread if running. """ if self.temp_monitor is not None: try: self.temp_monitor.stop() logger.info("Temperature monitoring stopped") except Exception as e: logger.error("Error stopping temperature monitoring: %s", e) finally: self.temp_monitor = None if self.temp_controller is not None: try: self.temp_controller.close() except Exception as e: logger.error("Error closing temperature controller: %s", e) finally: self.temp_controller = None def documentation_init( self ): doc_index_candidates = ( "/usr/share/doc/python3-damaris/html/index.html", os.path.join( os.path.dirname( os.path.dirname( os.path.dirname( os.path.dirname( os.path.abspath( __file__ ) ) ) ) ), "doc", "_build", "html", "index.html" ), ) self.doc_urls = { } for doc_index in doc_index_candidates: if os.path.isfile( doc_index ): self.doc_urls[ "DAMARIS Manual" ] = "file://" + doc_index break if os.path.isdir( "/usr/share/doc/python%d.%d-doc/html" % (sys.version_info[ :2 ]) ): self.doc_urls[ "Python Docs" ] = "file:///usr/share/doc/python%d.%d-doc/html/index.html" % ( sys.version_info[ :2 ]) doc_menu = self.xml_gui.get_object( "documentation_menu" ) for doc_title in self.doc_urls: doc_menu_item = gtk.MenuItem( label=doc_title ) doc_menu_item.connect( "activate", self.show_doc_menu ) doc_menu.append( doc_menu_item ) doc_menu_item.show( ) self.xml_gui.get_object( "documentation" ).set_menu( doc_menu ) self.doc_browser = None def show_doc_menu( self, widget, data=None ): """ offer a wide variety of docs, prefer local installations """ if type( widget ) is gtk.MenuItem: requested_doc = widget.get_child( ).get_text( ) else: requested_doc = "DAMARIS Manual" if requested_doc in self.doc_urls and self.doc_urls[ requested_doc ] is not None: if self.doc_browser is not None: if not self.doc_browser.is_alive( ): self.doc_browser.join( ) if self.doc_browser.my_webbrowser is not None: logger.debug("New browser tab") self.doc_browser.my_webbrowser.open_new_tab( self.doc_urls[ requested_doc ] ) else: del self.doc_browser self.doc_browser = start_browser( self.doc_urls[ requested_doc ] ) self.doc_browser.start( ) else: self.doc_browser = start_browser( self.doc_urls[ requested_doc ] ) self.doc_browser.start( ) else: logger.warning("Missing docs for '%s'", requested_doc) show_manual = show_doc_menu class start_browser( threading.Thread ): def __init__( self, url ): threading.Thread.__init__( self, name="manual browser" ) self.my_webbrowser = None self.my_webbrowser_process = None self.start_url = url def run( self ): """ start a webbrowser """ if sys.hexversion >= 0x02050000: if sys.platform == "linux2" and self.my_webbrowser is None: # try for debian linux self.my_webbrowser = webbrowser.get( "x-www-browser" ) if self.my_webbrowser is None: # this is what it should be everywhere! self.my_webbrowser = webbrowser.get( ) if self.my_webbrowser is not None: logger.info("Starting web browser (module webbrowser)") self.my_webbrowser.open( self.start_url ) return True # last resort logger.info("Starting web browser (webbrowser.py)") self.my_webbrowser_process = os.spawnl( os.P_NOWAIT, sys.executable, os.path.basename( sys.executable ), "-c", "import webbrowser\nwebbrowser.open('%s')" % self.start_url ) return True class LogWindow: """Manages the application log window. Provides a timestamped, scrollable text view for application messages. Messages are dispatched via ``glib.idle_add`` to keep the GTK main loop responsive. Args: xml_gui: The :class:`gtk.Builder` instance containing the GUI widgets. damaris_gui: Reference to the main :class:`DamarisGUI` application. Attributes: textview (:class:`gtk.TextView`): The monospaced text view widget. textbuffer (:class:`gtk.TextBuffer`): The buffer backing the text view. Examples: >>> log = LogWindow(xml_gui, damaris_gui) >>> log("Application started") """ def __init__( self, xml_gui, damaris_gui ): self.xml_gui = xml_gui self.damaris_gui = damaris_gui self.textview = self.xml_gui.get_object( "messages_textview" ) self.textview.connect( "key-press-event", self.textview_keypress) apply_font_css(self.textview, "Monospace 10") self.textbuffer = self.textview.get_buffer( ) self.lock = threading.RLock( ) self.last_timetag = None self( "Started in directory %s\n" % os.getcwd( ) ) def __call__( self, message ): timetag = time.time( ) glib.idle_add( self.add_message_callback, timetag, message, priority=glib.PRIORITY_LOW ) def add_message_callback( self, timetag, message ): date_tag = "" if self.last_timetag is None or (message != "\n" and self.last_timetag + 60 < timetag): self.last_timetag = timetag timetag_int = math.floor( timetag ) timetag_ms = int( (timetag - timetag_int) * 1000 ) date_tag = time.strftime( "%Y%m%d %H:%M:%S.%%03d:\n", time.localtime( timetag_int ) ) % timetag_ms with self.lock: self.textbuffer.place_cursor( self.textbuffer.get_end_iter( ) ) self.textbuffer.insert_at_cursor( date_tag + str( message ) ) self.textview.scroll_to_mark( self.textbuffer.get_insert( ), 0.1 , False, 0.5, 0.5) def snapshot_since_last_dump( self ): """Return log text appended since the last dump and advance the mark. Must be called from the GTK main thread. The result is plain Python data that can safely be passed to the dump thread. Examples: >>> logtext = self.log.snapshot_since_last_dump( ) """ with self.lock: tb = self.textbuffer mark = tb.get_mark( "lastdumped" ) if mark is None: mark = tb.create_mark( "lastdumped", tb.get_start_iter( ), left_gravity=True ) start = tb.get_iter_at_mark( mark ) end = tb.get_end_iter( ) text = tb.get_text( start, end, True ) tb.move_mark( mark, end ) return text def textview_keypress( self, widget, event, data=None ): if event.state & gdk.ModifierType.CONTROL_MASK != 0: if event.keyval == gdk.keyval_from_name("f"): self.damaris_gui.sw.search(None, None) return True return False def __del__( self ): pass class ScriptWidgets: """Manages experiment and result script editing widgets. Provides two syntax-highlighted, auto-completing text views for editing DAMARIS experiment and result scripts. Handles file open/save dialogs, toolbar state management, line/column indicators, and search functionality. Args: xml_gui: The :class:`gtk.Builder` instance containing the GUI widgets. outer_space: Reference to the parent application (typically :class:`DamarisGUI`) that owns this widget set. Attributes: editing_state (bool): ``True`` if scripts are editable, ``False`` during experiment execution. exp_script_filename (str or None): Path to the currently loaded experiment script file. res_script_filename (str or None): Path to the currently loaded result script file. experiment_script_textview (:class:`gtksourceview.View`): Text view for the experiment script. data_handling_textview (:class:`gtksourceview.View`): Text view for the result script. Public Methods: set_scripts: Replace script buffer contents. get_scripts: Retrieve current script text from both buffers. enable_editing: Re-enable script editing. disable_editing: Lock scripts during experiment execution. open_file: Open a script file via file chooser dialog. open_hdf5: Load data and scripts from an HDF5 file. save_file: Save the current script to its associated file. save_file_as: Save the current script with a new filename. save_all_files: Save both experiment and result scripts. new_file: Clear the current script buffer. check_script: Compile-check the current script for syntax errors. undo / redo: Undo/redo text edits. search: Open the search-and-replace dialog. """ def __init__( self, xml_gui, outer_space ): """Initialize text widgets with syntax highlighting and completion. Sets up two :class:`gtksourceview.View` instances with Python syntax highlighting, keyword auto-completion, line numbers, and smart indentation. Connects text buffer signals to toolbar and status bar update handlers. Args: xml_gui: The :class:`gtk.Builder` instance containing the GUI widgets. outer_space: Reference to the parent application. """ self.xml_gui = xml_gui self.outer_space = outer_space # my states # editing enabled/disabled self.editing_state = True # keep in mind which filename was used for experiment script self.exp_script_filename = None # keep in mind which filename was used for result script self.res_script_filename = None # load syntax description for syntax highlighting (defined in python.xml) #sl = SyntaxLoader( "python" ) # script buffers: self.experiment_script_textview = self.xml_gui.get_object( "experiment_script_textview" ) self.data_handling_textview = self.xml_gui.get_object( "data_handling_textview" ) # create and set syntax-highlighting text-buffers as textview backends self.experiment_script_textbuffer = gtksourceview.Buffer() self.data_handling_textbuffer = gtksourceview.Buffer() lm = gtksourceview.LanguageManager() langpython = lm.get_language("python") self.experiment_script_textbuffer.set_language(langpython) self.experiment_script_textbuffer.set_highlight_syntax(True) self.data_handling_textbuffer.set_language(langpython) self.data_handling_textbuffer.set_highlight_syntax(True) self.experiment_script_textview.set_buffer( self.experiment_script_textbuffer ) self.experiment_script_textview.set_show_line_numbers(True) self.experiment_script_textview.set_auto_indent(True) self.experiment_script_textview.set_insert_spaces_instead_of_tabs(True) self.experiment_script_textview.set_tab_width(4) self.experiment_script_textview.set_smart_home_end(True) apply_font_css(self.experiment_script_textview, "monospace") self.data_handling_textview.set_buffer( self.data_handling_textbuffer ) self.data_handling_textview.set_show_line_numbers(True) self.data_handling_textview.set_auto_indent(True) self.data_handling_textview.set_insert_spaces_instead_of_tabs(True) self.data_handling_textview.set_tab_width(4) self.data_handling_textview.set_smart_home_end(True) apply_font_css(self.data_handling_textview, "monospace") keywords = """for if else elif in print try finally except global lambda not or pass def class import from as return yield while continue break assert None True False AccumulatedValue Accumulation MeasurementResult ADC_Result Experiment synchronize sleep result data issubclass min max abs pow range xrange log_range lin_range staggered_range file combine_ranges interleaved_range get_sampling_rate uses_statistics write_to_csv write_to_hdf lowpass get_job_id get_description set_description get_xdata get_ydata set_xdata set_ydata ttl_pulse ttls rf_pulse state_start state_end wait record loop_start loop_end set_frequency set_pfg set_dac set_phase set_pts_local get_length set_title get_title add_lineplotdata add_errorplotdata get_number_of_channels get_style set_style get_xlabel set_xlabel get_ylabel set_ylabel""" self.keywordsbuffer = gtksourceview.Buffer() self.keywordsbuffer.set_text(keywords) compexp = self.experiment_script_textview.get_completion() compres = self.data_handling_textview.get_completion() compexp.set_property("show-headers", True) compres.set_property("show-headers", True) self.compwgen = gtksourceview.CompletionWords.new("Keywords", None) self.compwgen.register(self.keywordsbuffer) self.compwgen.set_property("minimum-word-size", 2) self.compwexp = gtksourceview.CompletionWords.new("Words", None) self.compwexp.register(self.experiment_script_textbuffer) self.compwexp.set_property("minimum-word-size", 2) self.compwres = gtksourceview.CompletionWords.new("Words", None) self.compwres.register(self.data_handling_textbuffer) self.compwres.set_property("minimum-word-size", 2) compexp.add_provider(self.compwgen) compres.add_provider(self.compwgen) compexp.add_provider(self.compwexp) compres.add_provider(self.compwres) # clipboard self.main_clipboard = gtk.Clipboard( ) # statusbar self.experiment_script_statusbar_label = self.xml_gui.get_object( "statusbar_experiment_script_label" ) self.data_handling_statusbar_label = self.xml_gui.get_object( "statusbar_data_handling_label" ) # footer with line and col indicators self.experiment_script_line_indicator = self.xml_gui.get_object( "experiment_script_line_textfield" ) self.experiment_script_column_indicator = self.xml_gui.get_object( "experiment_script_column_textfield" ) self.data_handling_line_indicator = self.xml_gui.get_object( "data_handling_line_textfield" ) self.data_handling_column_indicator = self.xml_gui.get_object( "data_handling_column_textfield" ) # some event handlers self.experiment_script_textbuffer.connect( "modified-changed", self.textviews_modified ) self.experiment_script_textview.connect_after( "move-cursor", self.textviews_moved ) self.experiment_script_textview.connect( "key-press-event", self.textviews_keypress ) self.experiment_script_textview.connect( "button-release-event", self.textviews_clicked ) self.data_handling_textbuffer.connect( "modified-changed", self.textviews_modified ) self.data_handling_textview.connect_after( "move-cursor", self.textviews_moved ) self.data_handling_textview.connect( "key-press-event", self.textviews_keypress ) self.data_handling_textview.connect( "button-release-event", self.textviews_clicked ) # and the editing toolbar buttons self.toolbar_new_button = self.xml_gui.get_object( "toolbar_new_button" ) self.toolbar_open_button = self.xml_gui.get_object( "toolbar_open_file_button" ) self.toolbar_save_button = self.xml_gui.get_object( "toolbar_save_file_button" ) self.toolbar_save_as_button = self.xml_gui.get_object( "toolbar_save_as_button" ) self.toolbar_save_all_button = self.xml_gui.get_object( "toolbar_save_all_button" ) self.toolbar_check_scripts_button = self.xml_gui.get_object( "toolbar_check_scripts_button" ) self.toolbar_undo_button = self.xml_gui.get_object( "toolbar_undo_button" ) self.toolbar_redo_button = self.xml_gui.get_object( "toolbar_redo_button" ) self.toolbar_search_button = self.xml_gui.get_object("toolbar_search_button") # main notebook self.main_notebook = self.xml_gui.get_object( "main_notebook" ) # config toolbar self.main_notebook.connect_after( "switch_page", self.notebook_page_switched ) # start with empty scripts self.set_scripts( "", "" ) self.enable_editing( ) # public methods def set_scripts( self, exp_script=None, res_script=None ): # load buffers and set cursor to front if exp_script is not None: self.experiment_script_textbuffer.begin_not_undoable_action() self.experiment_script_textbuffer.set_text( str( exp_script ) ) self.experiment_script_textbuffer.place_cursor( self.experiment_script_textbuffer.get_start_iter( ) ) self.experiment_script_textbuffer.set_modified( False ) self.experiment_script_textbuffer.end_not_undoable_action() self.textviews_moved( self.experiment_script_textview ) if res_script is not None: self.data_handling_textbuffer.begin_not_undoable_action() self.data_handling_textbuffer.set_text( str( res_script ) ) self.data_handling_textbuffer.place_cursor( self.data_handling_textbuffer.get_start_iter( ) ) self.data_handling_textbuffer.set_modified( False ) self.data_handling_textbuffer.end_not_undoable_action() self.textviews_moved( self.data_handling_textview ) self.set_toolbuttons_status( ) def get_scripts( self ): """ returns scripts """ exp_script = self.experiment_script_textbuffer.get_text( self.experiment_script_textbuffer.get_start_iter( ), self.experiment_script_textbuffer.get_end_iter( ) , True).rstrip( ) res_script = self.data_handling_textbuffer.get_text( self.data_handling_textbuffer.get_start_iter( ), self.data_handling_textbuffer.get_end_iter( ) , True).rstrip( ) return (exp_script, res_script) def disable_editing( self ): """ disable editing (for running experiments) """ # disable buffers self.editing_state = False self.experiment_script_textview.set_sensitive( False ) self.data_handling_textview.set_sensitive( False ) self.set_toolbuttons_status( ) def enable_editing( self ): """ returns to editable state """ self.editing_state = True self.experiment_script_textview.set_sensitive( True ) self.data_handling_textview.set_sensitive( True ) self.set_toolbuttons_status( ) # methods to update status and appearance def set_toolbuttons_status( self ): """ ToDo: care about associated file names """ current_page = self.main_notebook.get_current_page( ) if self.editing_state and current_page in [ 0, 1 ]: self.toolbar_new_button.set_sensitive( True ) self.toolbar_open_button.set_sensitive( True ) # find visible tab exp_modified = self.experiment_script_textbuffer.get_modified( ) res_modified = self.data_handling_textbuffer.get_modified( ) enable_save = True canundo = False canredo = False if current_page == 0: enable_save = exp_modified and self.exp_script_filename is not None canundo = self.experiment_script_textbuffer.can_undo() canredo = self.experiment_script_textbuffer.can_redo() elif current_page == 1: enable_save = res_modified and self.res_script_filename is not None canundo = self.data_handling_textbuffer.can_undo() canredo = self.data_handling_textbuffer.can_redo() self.toolbar_save_button.set_sensitive( enable_save ) self.toolbar_save_as_button.set_sensitive( True ) self.toolbar_save_all_button.set_sensitive( exp_modified or res_modified ) self.toolbar_check_scripts_button.set_sensitive( True ) self.toolbar_undo_button.set_sensitive(canundo) self.toolbar_redo_button.set_sensitive(canredo) self.toolbar_search_button.set_sensitive(True) else: # disable toolbar self.toolbar_new_button.set_sensitive( False ) self.toolbar_open_button.set_sensitive( False ) self.toolbar_save_button.set_sensitive( False ) self.toolbar_save_as_button.set_sensitive( False ) self.toolbar_save_all_button.set_sensitive( False ) self.toolbar_check_scripts_button.set_sensitive( False ) self.toolbar_undo_button.set_sensitive(False) self.toolbar_redo_button.set_sensitive(False) self.toolbar_search_button.set_sensitive(current_page == 3) if self.exp_script_filename: exp_titlename = str( os.path.splitext( os.path.basename( self.exp_script_filename ) )[ 0 ] ) else: exp_titlename = "unnamed" if self.res_script_filename: res_titlename = str( os.path.splitext( os.path.basename( self.res_script_filename ) )[ 0 ] ) else: res_titlename = "unnamed" window_title = "DAMARIS-%s %s,%s" % (__version__, exp_titlename, res_titlename) self.xml_gui.get_object( "main_window" ).set_title( window_title ) # text widget related events def check_script( self, widget, data=None ): if not self.editing_state: return 0 current_page = self.main_notebook.get_current_page( ) if current_page not in [ 0, 1 ]: return 0 script = self.get_scripts( )[ current_page ] try: compile( script, ("Experiment Script", "Result Script")[ current_page ], "exec" ) except SyntaxError as se: if current_page == 0: tb = self.experiment_script_textbuffer tv = self.experiment_script_textview elif current_page == 1: tb = self.data_handling_script_textbuffer tv = self.data_handling_script_textview ln = se.lineno lo = se.offset # reguard http://bugs.python.org/issue1778 if type( ln ) is not int: ln = 0 if type( lo ) is not int: lo = 0 logger.error("Syntax Error:\n%s in %s at line %d, offset %d", str( se ), se.filename, ln, lo) if ln > 0 and ln <= tb.get_line_count( ): new_place = tb.get_iter_at_line_offset( ln - 1, 0 ) if lo > 0 and lo <= new_place.get_chars_in_line( ): new_place.set_line_offset( lo ) tb.place_cursor( new_place ) tv.scroll_to_iter( new_place, 0.2, False, 0, 0 ) except Exception as e: logger.error("Compilation Error:\n%s", str( e )) def notebook_page_switched( self, notebook, page, pagenumber ): self.set_toolbuttons_status( ) def column_line_widgets_changed_event( self, data=None ): page = self.main_notebook.get_current_page( ) if page == 0: textview = self.experiment_script_textview column_indicator = self.experiment_script_column_indicator newline = self.experiment_script_line_indicator.get_value_as_int( ) - 1 newcol = column_indicator.get_value_as_int( ) - 1 elif page == 1: textview = self.data_handling_textview column_indicator = self.data_handling_column_indicator newline = self.data_handling_line_indicator.get_value_as_int( ) - 1 newcol = column_indicator.get_value_as_int( ) - 1 else: logger.error("Unknown line/column selector") return False textbuffer = textview.get_buffer( ) new_place = textbuffer.get_iter_at_line( newline ) if not newcol > new_place.get_chars_in_line( ): new_place.set_line_offset( newcol ) else: column_indicator.set_value( 1 ) if len( textbuffer.get_selection_bounds( ) ) != 0: textbuffer.move_mark_by_name( "insert", new_place ) else: textbuffer.place_cursor( new_place ) textview.scroll_mark_onscreen( textbuffer.get_insert( ) ) textview.grab_focus( ) return True def textviews_modified( self, widget = None, event=None ): # mix into toolbar affairs if type(widget) is gtksourceview.View: return self.textviews_moved( widget ) else: return False def textviews_clicked( self, widget, event ): return self.textviews_moved( widget ) def textviews_moved( self, widget, text=None, count=None, ext_selection=None, data=None ): textbuffer = widget.get_buffer( ) cursor_mark = textbuffer.get_insert( ) cursor_iter = textbuffer.get_iter_at_mark( cursor_mark ) if textbuffer is self.experiment_script_textbuffer: line_indicator = self.experiment_script_line_indicator column_indicator = self.experiment_script_column_indicator if textbuffer is self.data_handling_textbuffer: line_indicator = self.data_handling_line_indicator column_indicator = self.data_handling_column_indicator # do only necessary updates! li_range_new = textbuffer.get_end_iter( ).get_line( ) + 1 if line_indicator.get_range( )[ 1 ] != li_range_new: line_indicator.set_range( 1, li_range_new ) ci_range_new = cursor_iter.get_chars_in_line( ) + 1 if column_indicator.get_range( )[ 1 ] != ci_range_new: column_indicator.set_range( 1, ci_range_new ) cursor_line = cursor_iter.get_line( ) + 1 cursor_lineoffset = cursor_iter.get_line_offset( ) + 1 if line_indicator.get_value( ) != cursor_line: line_indicator.set_value( cursor_line ) if column_indicator.get_value( ) != cursor_lineoffset: column_indicator.set_value( cursor_lineoffset ) self.set_toolbuttons_status( ) return False def textviews_keypress( self, widget, event, data=None ): """ helpful tab and return key functions """ if event.state & gdk.ModifierType.CONTROL_MASK != 0: if event.keyval == gdk.keyval_from_name( "s" ): # save buffer page = self.main_notebook.get_current_page( ) if (self.exp_script_filename, self.res_script_filename)[ page ] is None: self.save_file_as( ) else: self.save_file( ) return True elif event.keyval == gdk.keyval_from_name( "S" ): # save both buffers self.save_all_files( None, None ) return True elif event.keyval == gdk.keyval_from_name("f"): self.search(None, None) return True return False #Handle Backspace (delete more than one space on line beginning) if event.keyval == 0xFF08: textbuffer = widget.get_buffer( ) if textbuffer.get_has_selection(): return False cursor_mark = textbuffer.get_insert( ) cursor_iter = textbuffer.get_iter_at_mark( cursor_mark ) linestart_iter = cursor_iter.copy( ) linestart_iter.set_line_offset( 0 ) linebegin = textbuffer.get_text( linestart_iter, cursor_iter, True ).expandtabs(4) if linebegin.isspace() and len(linebegin) > 0: linebegin = ' ' * int((len( linebegin ) - 1) / 4) * 4 textbuffer.delete( linestart_iter, cursor_iter ) textbuffer.insert( linestart_iter, linebegin ) return True elif event.keyval == 0xFF0D: textbuffer = widget.get_buffer( ) if textbuffer.get_has_selection(): return False cursor_mark = textbuffer.get_insert( ) cursor_iter = textbuffer.get_iter_at_mark( cursor_mark ) lastchar_iter = cursor_iter.copy() lastchar = lastchar_iter.backward_char( ) if not lastchar_iter.get_char( ) == ":": return False linestart_iter = cursor_iter.copy( ) linestart_iter.set_line_offset( 0 ) spaceend_iter = linestart_iter.copy( ) while (not spaceend_iter.ends_line( ) and not spaceend_iter.is_end( ) and spaceend_iter.get_char( ).isspace( )): spaceend_iter.forward_char( ) linebegin = textbuffer.get_text( linestart_iter, spaceend_iter, True ).expandtabs(4) indent_length = int((int(len(linebegin)/4)+1)*4) intext = "\n" + (" " * indent_length) textbuffer.insert(cursor_iter, intext) widget.scroll_to_mark( cursor_mark, 0.0, False, 0.5, 0.5 ) return True #self.textviews_moved(widget) return False def load_file_as_unicode( self, script_filename ): script_file = open( script_filename, "r", encoding="iso-8859-15" ) script_string = "" for line in script_file: script_string += line # str( line, encoding="iso-8859-15", errors="replace" ) script_file.close( ) return script_string def open_file( self, widget, Data=None ): """ do the open file dialog, if necessary ask for save """ # ignore if not self.editing_state: return 0 # Determining the tab which is currently open current_page = self.main_notebook.get_current_page( ) if current_page == 0: open_dialog_title = "Open Experiment Script..." modified = self.experiment_script_textbuffer.get_modified( ) elif current_page == 1: open_dialog_title = "Open Result Script..." modified = self.data_handling_textbuffer.get_modified( ) else: return 0 if modified: question = gtk.MessageDialog(parent=self.xml_gui.get_object( "main_window"), type=gtk.MessageType.WARNING, flags=gtk.DialogFlags.MODAL, buttons=("_Yes", gtk.ResponseType.YES, "_No", gtk.ResponseType.NO, "_Cancel", gtk.ResponseType.CANCEL)) question.set_markup("The file has been changed. Save changes?") response = question.run() question.destroy() if response == gtk.ResponseType.YES: self.save_file() elif response == gtk.ResponseType.CANCEL: return 0 def response( dialog, response_id, script_widget ): if response_id == gtk.ResponseType.OK: file_name = dialog.get_filename( ) if file_name is None: return script_filename = os.path.abspath( file_name ) if not os.access( script_filename, os.R_OK ): error_dialog = gtk.MessageDialog(parent=script_widget.xml_gui.get_object( "main_window"), type=gtk.MessageType.ERROR, flags=gtk.DialogFlags.MODAL, buttons=("_OK", gtk.ResponseType.OK)) error_dialog.set_markup("Cannot read from file %s" % script_filename) error_dialog.run() error_dialog.destroy() return True script_string = script_widget.load_file_as_unicode( script_filename ) if script_widget.main_notebook.get_current_page( ) == 0: script_widget.exp_script_filename = script_filename script_widget.set_scripts( script_string, None ) elif script_widget.main_notebook.get_current_page( ) == 1: script_widget.res_script_filename = script_filename script_widget.set_scripts( None, script_string ) return True parent_window = self.xml_gui.get_object( "main_window" ) dialog = gtk.FileChooserDialog( title=open_dialog_title, parent=parent_window, buttons=("_Open", gtk.ResponseType.OK, "_Cancel", gtk.ResponseType.CANCEL) ) dialog.set_default_response( gtk.ResponseType.OK ) dialog.set_select_multiple( False ) # Event-Handler for responce-signal (when one of the button is pressed) dialog.connect( "response", response, self ) dialog.run( ) dialog.destroy( ) # update title and so on... return True def open_hdf5( self, widget, Data=None ): """ Open and load an HDF5 file written by DAMARIS. """ # ignore if not self.editing_state: return 0 parent_window = self.xml_gui.get_object( "main_window" ) dialog = gtk.FileChooserDialog( title="Open HDF5 File...", parent=parent_window, buttons=("_Open", gtk.ResponseType.OK, "_Cancel", gtk.ResponseType.CANCEL) ) dialog.set_default_response( gtk.ResponseType.OK ) dialog.set_select_multiple( False ) # Add filter for HDF5 files filter_hdf5 = gtk.FileFilter() filter_hdf5.set_name("HDF5 files") filter_hdf5.add_pattern("*.h5") filter_hdf5.add_pattern("*.hdf5") filter_hdf5.add_pattern("*.HDF5") filter_hdf5.add_pattern("*.H5") dialog.add_filter(filter_hdf5) # Also allow all files filter_all = gtk.FileFilter() filter_all.set_name("All files") filter_all.add_pattern("*") dialog.add_filter(filter_all) def response( dialog, response_id, script_widget ): if response_id == gtk.ResponseType.OK: file_name = dialog.get_filename( ) if file_name is None: return file_name = os.path.abspath( file_name ) if not os.access( file_name, os.R_OK ): error_dialog = gtk.MessageDialog(parent=script_widget.xml_gui.get_object( "main_window"), type=gtk.MessageType.ERROR, flags=gtk.DialogFlags.MODAL, buttons=("_OK", gtk.ResponseType.OK)) error_dialog.set_markup("Cannot read from file %s" % file_name) error_dialog.run() error_dialog.destroy() return True try: # Load DataPool and metadata from HDF5 data_pool, metadata = DataPool.load_hdf5(file_name) # Set the loaded data pool in the outer space (main application) if hasattr(script_widget.outer_space, 'observe_data_pool'): script_widget.outer_space.observe_data_pool(data_pool) elif hasattr(script_widget.outer_space, 'monitor'): # Fallback if delegate not present script_widget.outer_space.data = data_pool script_widget.outer_space.monitor.observe_data_pool(data_pool) # If there are scripts in metadata, load them if 'scripts' in metadata: scripts = metadata['scripts'] exp_script = scripts.get('experiment_script', None) res_script = scripts.get('result_script', None) if exp_script or res_script: script_widget.set_scripts(exp_script, res_script) except Exception as e: error_dialog = gtk.MessageDialog(parent=script_widget.xml_gui.get_object( "main_window"), type=gtk.MessageType.ERROR, flags=gtk.DialogFlags.MODAL, buttons=("_OK", gtk.ResponseType.OK)) error_dialog.set_markup("Failed to load HDF5 file: %s" % str(e)) error_dialog.run() error_dialog.destroy() return True # Event-Handler for response-signal (when one of the button is pressed) dialog.connect( "response", response, self ) dialog.run( ) dialog.destroy( ) return True def save_file( self, widget=None, Data=None ): """ save file to associated filename """ # ignore if not self.editing_state: return 0 # Determining the tab which is currently open current_page = self.main_notebook.get_current_page( ) if current_page == 0: filename = self.exp_script_filename elif current_page == 1: filename = self.res_script_filename else: return 0 if filename is None: return 0 # save file if current_page == 0: script = self.get_scripts( )[ 0 ] elif current_page == 1: script = self.get_scripts( )[ 1 ] else: return 0 # encode from unicode to iso-8859-15 filecontents = script openfile = open( filename, "w", encoding = "iso-8859-15" ) openfile.write( filecontents ) openfile.close() if current_page == 0: self.experiment_script_textbuffer.set_modified( False ) elif current_page == 1: self.data_handling_textbuffer.set_modified( False ) self.set_toolbuttons_status( ) def save_file_as( self, widget=None, Data=None ): def response( self, response_id, script_widget ): if response_id == gtk.ResponseType.OK: file_name = dialog.get_filename( ) if file_name is None: return True absfilename = os.path.abspath( file_name ) if os.access( file_name, os.F_OK ): question = gtk.MessageDialog(parent=script_widget.xml_gui.get_object( "main_window"), type=gtk.MessageType.WARNING, flags=gtk.DialogFlags.MODAL, buttons=("_Yes", gtk.ResponseType.YES, "_No", gtk.ResponseType.NO)) question.set_markup("The file already exists. Do you want to overwrite the existing file and delete its contents?") response = question.run() question.destroy() if response != gtk.ResponseType.YES: return True current_page = script_widget.main_notebook.get_current_page( ) if current_page == 0: script_widget.exp_script_filename = absfilename elif current_page == 1: script_widget.res_script_filename = absfilename script_widget.save_file( ) return True # Determining the tab which is currently open current_page = self.main_notebook.get_current_page( ) if current_page == 0: dialog_title = "Save Experiment Script As..." elif current_page == 1: dialog_title = "Save Result Script As..." else: return parent_window = self.xml_gui.get_object( "main_window" ) dialog = gtk.FileChooserDialog( title=dialog_title, parent=parent_window, buttons=("_Save", gtk.ResponseType.OK, "_Cancel", gtk.ResponseType.CANCEL) ) dialog.set_property("action", gtk.FileChooserAction.SAVE) dialog.set_default_response( gtk.ResponseType.OK ) dialog.set_select_multiple( False ) # Event-Handler for responce-signal (when one of the button is pressed) dialog.connect( "response", response, self ) dialog.run( ) dialog.destroy( ) return True def save_all_files( self, widget, Data=None ): current_page = self.main_notebook.get_current_page( ) # change page and call save dialog self.main_notebook.set_current_page( 0 ) if self.exp_script_filename is None: self.save_file_as( ) else: self.save_file( ) self.main_notebook.set_current_page( 1 ) if self.res_script_filename is None: self.save_file_as( ) else: self.save_file( ) self.main_notebook.set_current_page( current_page ) def undo(self, widget=None, Data=None ): if not self.editing_state: return 0 current_page = self.main_notebook.get_current_page( ) if current_page == 0: self.experiment_script_textview.do_undo(self.experiment_script_textview) elif current_page == 1: self.data_handling_textview.do_undo(self.data_handling_textview) self.set_toolbuttons_status() def redo(self, widget=None, Data=None ): if not self.editing_state: return 0 current_page = self.main_notebook.get_current_page( ) if current_page == 0: self.experiment_script_textview.do_redo(self.experiment_script_textview) elif current_page == 1: self.data_handling_textview.do_redo(self.data_handling_textview) self.set_toolbuttons_status() def search(self, widget=None, Data=None): def response(script_widget, response_id, damsw, tb ): if response_id != -1 and response_id != 1: script_widget.destroy() return True current_page = damsw.main_notebook.get_current_page( ) startiter = None enditer = None logview = None logbuffer = None if current_page == 0: if damsw.experiment_script_textbuffer.get_has_selection(): startiter, enditer = damsw.experiment_script_textbuffer.get_selection_bounds() else: mark = damsw.experiment_script_textbuffer.get_insert() startiter = damsw.experiment_script_textbuffer.get_iter_at_mark(mark) enditer = startiter elif current_page == 1: if damsw.data_handling_textbuffer.get_has_selection(): startiter, enditer = damsw.data_handling_textbuffer.get_selection_bounds() else: mark = damsw.data_handling_textbuffer.get_insert() startiter = damsw.data_handling_textbuffer.get_iter_at_mark(mark) enditer = startiter elif current_page == 3: logview = damsw.xml_gui.get_object( "messages_textview" ) logbuffer = logview.get_buffer() if logbuffer.get_has_selection(): startiter, enditer = logbuffer.get_selection_bounds() else: mark = logbuffer.get_insert() startiter = logbuffer.get_iter_at_mark(mark) enditer = startiter else: script_widget.destroy() return True searchstring = tb.get_text() if len(searchstring) > 0: match_start = None match_end = None try: if response_id == -1: match_start, match_end = startiter.backward_search(searchstring, gtk.TextSearchFlags.TEXT_ONLY) elif response_id == 1: match_start, match_end = enditer.forward_search(searchstring, gtk.TextSearchFlags.TEXT_ONLY) except: pass if match_start is not None and match_end is not None: if current_page == 0: damsw.experiment_script_textbuffer.select_range(match_start, match_end) damsw.experiment_script_textview.scroll_to_iter(match_start, 0.2) damsw.textviews_moved(damsw.experiment_script_textview) elif current_page == 1: damsw.data_handling_textbuffer.select_range(match_start, match_end) damsw.data_handling_textview.scroll_to_iter(match_start, 0.2) damsw.textviews_moved(damsw.data_handling_textview) elif current_page == 3: logbuffer.select_range(match_start, match_end) logview.scroll_to_iter(match_start, 0.2) dialog = gtk.Dialog(title="Search", parent=self.xml_gui.get_object("main_window"), flags=0, buttons=("_Close", gtk.ResponseType.CLOSE, "Find last", -1, "Find next", 1)) cont = dialog.get_content_area() markedstring = "" current_page = self.main_notebook.get_current_page( ) startit = None endit = None logbuffer = None if current_page == 0: if self.experiment_script_textbuffer.get_has_selection(): startit, endit = self.experiment_script_textbuffer.get_selection_bounds() markedstring = self.experiment_script_textbuffer.get_text(startit, endit, True) elif current_page == 1: if self.data_handling_textbuffer.get_has_selection(): startit, endit = self.data_handling_textbuffer.get_selection_bounds() markedstring = self.data_handling_textbuffer.get_text(startit, endit, True) elif current_page == 3: logview = self.xml_gui.get_object( "messages_textview" ) logbuffer = logview.get_buffer() if logbuffer.get_has_selection(): startit, endit = logbuffer.get_selection_bounds() markedstring = logbuffer.get_text(startit, endit, True) markedstring = markedstring.strip() textbox = gtk.Entry() textbox.set_text(markedstring) cont.add(textbox) dialog.set_default_response(1) dialog.connect("response", response, self, textbox) dialog.set_resizable(False) dialog.show_all() if startit is not None and endit is not None: if current_page == 0: self.experiment_script_textbuffer.select_range(startit, endit) elif current_page == 1: self.data_handling_textbuffer.select_range(startit, endit) elif current_page == 3: logbuffer.select_range(startit, endit) def new_file( self, widget, Data=None ): if not self.editing_state: return 0 current_page = self.main_notebook.get_current_page( ) if current_page == 0: if self.experiment_script_textbuffer.get_modified( ): question = gtk.MessageDialog(parent=self.xml_gui.get_object( "main_window"), type=gtk.MessageType.WARNING, flags=gtk.DialogFlags.MODAL, buttons=("_Yes", gtk.ResponseType.YES, "_No", gtk.ResponseType.NO, "_Cancel", gtk.ResponseType.CANCEL)) question.set_markup("The file has been changed. Save changes?") response = question.run() question.destroy() if response == gtk.ResponseType.YES: self.save_file() elif response == gtk.ResponseType.CANCEL: return 0 self.set_scripts( "", None ) self.exp_script_filename = None elif current_page == 1: if self.data_handling_textbuffer.get_modified( ): question = gtk.MessageDialog(parent=self.xml_gui.get_object( "main_window"), type=gtk.MessageType.WARNING, flags=gtk.DialogFlags.MODAL, buttons=("_Yes", gtk.ResponseType.YES, "_No", gtk.ResponseType.NO, "_Cancel", gtk.ResponseType.CANCEL)) question.set_markup("The file has been changed. Save changes?") response = question.run() question.destroy() if response == gtk.ResponseType.YES: self.save_file() elif response == gtk.ResponseType.CANCEL: return 0 self.set_scripts( None, "" ) self.res_script_filename = None self.set_toolbuttons_status( ) class ConfigTab: """ by now all values are saved in the GUI widgets """ def __init__( self, xml_gui ): self.xml_gui = xml_gui self.configname = "damaris/python-damaris.xml" self.system_default_filename = None self.system_backend_folder = "/usr/lib/damaris/backends/" if sys.platform[ :5 ] == "linux" or "darwin": xdg_dirs = xdg.BaseDirectory.xdg_config_dirs xdg_dirs.remove( xdg.BaseDirectory.xdg_config_home ) self.user_default_filename = os.path.join( xdg.BaseDirectory.xdg_config_home, self.configname ) self.system_default_filenames = [ os.path.join( syscfg, self.configname ) for syscfg in xdg_dirs ] self.config_start_backend_checkbutton = self.xml_gui.get_object( "start_backend_checkbutton" ) self.config_backend_executable_entry = self.xml_gui.get_object( "backend_executable_entry" ) self.config_spool_dir_entry = self.xml_gui.get_object( "spool_dir_entry" ) self.config_start_experiment_script_checkbutton = self.xml_gui.get_object( "start_experiment_script_checkbutton" ) self.config_start_result_script_checkbutton = self.xml_gui.get_object( "start_result_script_checkbutton" ) self.config_del_results_after_processing_checkbutton = self.xml_gui.get_object( "del_results_after_processing_checkbutton" ) self.config_del_jobs_after_execution_checkbutton = self.xml_gui.get_object( "del_jobs_after_execution_checkbutton" ) self.config_data_pool_name_entry = self.xml_gui.get_object( "data_pool_name_entry" ) self.config_data_pool_name_entry.set_tooltip_text("Use %(date)s for ISO date or %(datetime)s for ISO date+time") self.config_data_pool_write_interval_entry = self.xml_gui.get_object( "data_pool_write_interval_entry" ) self.config_data_pool_complib = self.xml_gui.get_object( "CompLibs" ) self.config_data_pool_complib.set_tooltip_text("Use zlib for best compatibility with other tools") self.config_data_pool_comprate = self.xml_gui.get_object( "CompRatio" ) self.config_data_pool_comprate.set_tooltip_text("Use 1 for best compression, 9 for best speed; 3 is very usable") self.config_info_textview = self.xml_gui.get_object( "info_textview" ) self.config_script_font_button = self.xml_gui.get_object( "script_fontbutton" ) # Connect keyboard event handlers for Ctrl+S support self.config_info_textview.connect("key-press-event", self.config_keypress) self.config_backend_executable_entry.connect("key-press-event", self.config_keypress) self.config_spool_dir_entry.connect("key-press-event", self.config_keypress) self.config_data_pool_name_entry.connect("key-press-event", self.config_keypress) self.config_data_pool_write_interval_entry.connect("key-press-event", self.config_keypress) # ADC bit depth setting self.config_adc_bit_depth_hbox = gtk.Box(orientation=gtk.Orientation.HORIZONTAL, spacing=6) self.config_adc_bit_depth_label = gtk.Label(label="ADC bit depth:") self.config_adc_bit_depth_spinbutton = gtk.SpinButton.new_with_range(8, 32, 1) self.config_adc_bit_depth_spinbutton.set_value(ADC_Result.default_bit_depth) self.config_adc_bit_depth_spinbutton.set_tooltip_text("Set the default bit depth for clipping detection (e.g., 14 for 14-bit ADCs).") self.config_adc_bit_depth_spinbutton.connect("value-changed", self.on_adc_bit_depth_changed) self.config_adc_bit_depth_spinbutton.connect("key-press-event", self.config_keypress) self.config_adc_bit_depth_hbox.pack_start(self.config_adc_bit_depth_label, False, False, 0) self.config_adc_bit_depth_hbox.pack_start(self.config_adc_bit_depth_spinbutton, False, False, 0) backend_vbox = self.xml_gui.get_object("vbox4") if backend_vbox: backend_vbox.pack_start(self.config_adc_bit_depth_hbox, False, False, 0) self.config_adc_bit_depth_hbox.show_all() # Temperature monitoring settings - single line layout self.config_temp_hbox = gtk.Box(orientation=gtk.Orientation.HORIZONTAL, spacing=6) # Controller type selector self.config_temp_controller_type_label = gtk.Label(label="Controller:") self.config_temp_controller_type_combobox = gtk.ComboBoxText() self.config_temp_controller_type_combobox.append_text("eurotherm") self.config_temp_controller_type_combobox.append_text("simulated") self.config_temp_controller_type_combobox.set_active(0) # Default to eurotherm self.config_temp_controller_type_combobox.set_tooltip_text("Select temperature controller type") self.config_temp_controller_type_combobox.connect("changed", self.on_temp_controller_type_changed) self.config_temp_controller_type_hbox = gtk.Box(orientation=gtk.Orientation.HORIZONTAL, spacing=0) self.config_temp_controller_type_hbox.pack_start(self.config_temp_controller_type_label, False, False, 0) self.config_temp_controller_type_hbox.pack_start(self.config_temp_controller_type_combobox, False, False, 0) self.config_temp_hbox.pack_start(self.config_temp_controller_type_hbox, False, False, 0) # Enable checkbox self.config_temp_monitoring_checkbutton = gtk.CheckButton(label="Temp:") self.config_temp_monitoring_checkbutton.set_tooltip_text("Enable periodic temperature reading from controller") self.config_temp_hbox.pack_start(self.config_temp_monitoring_checkbutton, False, False, 0) # Device file chooser with entry (shown for eurotherm, hidden for simulated) self.config_temp_device_entry = gtk.Entry() self.config_temp_device_entry.set_tooltip_text("Serial port device (e.g., /dev/serial/by-id/...)") self.config_temp_device_entry.set_width_chars(30) self.config_temp_device_entry.connect("key-press-event", self.config_keypress) self.config_temp_device_button = gtk.Button(label="...") self.config_temp_device_button.set_tooltip_text("Browse for temperature controller device") self.config_temp_device_button.connect("clicked", self.on_temp_device_browse_clicked) self.config_temp_device_hbox = gtk.Box(orientation=gtk.Orientation.HORIZONTAL, spacing=0) self.config_temp_device_hbox.pack_start(self.config_temp_device_entry, True, True, 0) self.config_temp_device_hbox.pack_start(self.config_temp_device_button, False, False, 0) self.config_temp_hbox.pack_start(self.config_temp_device_hbox, True, True, 0) # Interval spinbutton self.config_temp_interval_label = gtk.Label(label="Interval (s):") self.config_temp_interval_spinbutton = gtk.SpinButton.new_with_range(0.1, 60, 0.1) self.config_temp_interval_spinbutton.set_value(2.0) # Default to 2 seconds self.config_temp_interval_spinbutton.set_tooltip_text("Time between temperature readings in seconds") self.config_temp_interval_spinbutton.connect("key-press-event", self.config_keypress) self.config_temp_interval_hbox = gtk.Box(orientation=gtk.Orientation.HORIZONTAL, spacing=6) self.config_temp_interval_hbox.pack_start(self.config_temp_interval_label, False, False, 0) self.config_temp_interval_hbox.pack_start(self.config_temp_interval_spinbutton, False, False, 0) self.config_temp_hbox.pack_start(self.config_temp_interval_hbox, False, False, 0) # Baud rate (for eurotherm) self.config_temp_baudrate_combobox = gtk.ComboBoxText() self.config_temp_baudrate_combobox.append_text("19200") self.config_temp_baudrate_combobox.append_text("9600") self.config_temp_baudrate_combobox.append_text("38400") self.config_temp_baudrate_combobox.append_text("57600") self.config_temp_baudrate_combobox.append_text("115200") self.config_temp_baudrate_combobox.set_active(0) self.config_temp_baudrate_combobox.set_tooltip_text("Serial baud rate for temperature controller") self.config_temp_baudrate_hbox = gtk.Box(orientation=gtk.Orientation.HORIZONTAL, spacing=6) self.config_temp_baudrate_hbox.pack_start(self.config_temp_baudrate_combobox, False, False, 0) self.config_temp_hbox.pack_start(self.config_temp_baudrate_hbox, False, False, 0) # Note: baud rate is shown for eurotherm, hidden for simulated # Set initial visibility based on controller type self._update_temp_widgets_visibility() if backend_vbox: backend_vbox.pack_start(self.config_temp_hbox, False, False, 0) self.config_temp_hbox.show_all() # insert version informations components_text = """ operating system %(os)s gtk version %(gtk)s glib version %(glib)s python version %(python)s matplotlib version %(matplotlib)s, %(matplotlib_backend)s numpy version %(numpy)s pytables version %(pytables)s, using: %(pytables_libs)s pygobject version %(pygobject)s """ if hasattr( gobject, "glib_version" ): glib_version = "%d.%d.%d" % gobject.glib_version else: glib_version = "? (no pygobject module)" if hasattr( gobject, "pygobject_version" ): pygobject_version = "%d.%d.%d" % gobject.pygobject_version else: pygobject_version = "? (no gobject version number)" numpy_version = "none" try: import numpy except ImportError: pass else: numpy_version = numpy.__version__ components_versions = { "os": platform.platform( ), "gtk": "{}.{}.{}".format(gtk.MAJOR_VERSION, gtk.MINOR_VERSION, gtk.MICRO_VERSION), "glib": glib_version, "python": sys.version, "matplotlib": matplotlib.__version__, "matplotlib_backend": FigureCanvas.__name__[ 12: ], "numpy": numpy_version, "pytables": tables.__version__, "pytables_libs": "", "pygobject": pygobject_version } # pytables modules: # find compression extensions for combo box and write version numbers # list is taken from ValueError output of tables.which_lib_version("") model = self.config_data_pool_complib.get_model( ) for libname in ('hdf5', 'zlib', 'lzo', 'blosc', 'bzip2', 'szip'): version_info = None try: version_info = tables.which_lib_version( libname ) except ValueError: continue if version_info: components_versions[ "pytables_libs" ] += "\n * %s: %s" % (libname, str( version_info[1] )) if libname != "hdf5": # a compression library, add it to combo box if isinstance( model, gtk.ListStore ): model.append( [ libname ] ) elif isinstance( model, gtk.TreeStore ): model.append( None, [ libname ] ) else: logger.warning("Cannot append compression lib name to %s", model.__class__.__name__) # debug message if debug: logger.debug("DAMARIS %s", __version__) logger.debug(components_text % components_versions) # set no compression as default... self.config_data_pool_complib.set_active( 0 ) info_textbuffer = self.config_info_textview.get_buffer( ) info_text = info_textbuffer.get_text( info_textbuffer.get_start_iter( ), info_textbuffer.get_end_iter( ), True ) info_text %= { "moduleversions": components_text % components_versions, "damarisversion": __version__ } info_textbuffer.set_text( info_text ) del info_textbuffer, info_text, components_text, components_versions for self.system_default_filename in self.system_default_filenames: if self.system_default_filename: if os.access( self.system_default_filename, os.R_OK ): self.load_config( self.system_default_filename ) else: logger.warning("Can not read system defaults from %s, ask your instrument responsible if required", self.system_default_filename) self.config_from_system = self.get( ) self.load_config( ) def get( self ): """ returns a dictionary of actual config values """ complib_iter = self.config_data_pool_complib.get_active_iter( ) complib = self.config_data_pool_complib.get_model( ).get_value( complib_iter, 0 ) actual_config = { "start_backend": self.config_start_backend_checkbutton.get_active( ), "start_result_script": self.config_start_result_script_checkbutton.get_active( ), "start_experiment_script": self.config_start_experiment_script_checkbutton.get_active( ), "spool_dir": self.config_spool_dir_entry.get_text( ), "backend_executable": self.config_backend_executable_entry.get_text( ), "data_pool_name": self.config_data_pool_name_entry.get_text( ), "del_results_after_processing": self.config_del_results_after_processing_checkbutton.get_active( ), "del_jobs_after_execution": self.config_del_jobs_after_execution_checkbutton.get_active( ), "data_pool_write_interval": self.config_data_pool_write_interval_entry.get_text( ), "data_pool_complib": complib, "data_pool_comprate": self.config_data_pool_comprate.get_value_as_int( ), "script_font": self.config_script_font_button.get_font_name( ), "adc_bit_depth": self.config_adc_bit_depth_spinbutton.get_value_as_int(), # Temperature monitoring configuration (only if widgets exist) "enable_temperature_monitoring": self.config_temp_monitoring_checkbutton.get_active() if hasattr(self, 'config_temp_monitoring_checkbutton') else False, "temperature_controller_type": self.config_temp_controller_type_combobox.get_active_text() if hasattr(self, 'config_temp_controller_type_combobox') else "eurotherm", "temperature_device": self.config_temp_device_entry.get_text() if hasattr(self, 'config_temp_device_entry') else "", "temperature_baudrate": self._get_temp_baudrate() if hasattr(self, 'config_temp_baudrate_combobox') else 19200, "temperature_interval": self.config_temp_interval_spinbutton.get_value() if hasattr(self, 'config_temp_interval_spinbutton') else 2.0 } ADC_Result.default_bit_depth = actual_config["adc_bit_depth"] return actual_config def _get_temp_baudrate(self): """Get selected baud rate from combobox.""" if not hasattr(self, 'config_temp_baudrate_combobox'): return 19200 active_text = self.config_temp_baudrate_combobox.get_active_text() if active_text: try: return int(active_text) except ValueError: pass return 19200 def set( self, config ): if "start_backend" in config: self.config_start_backend_checkbutton.set_active( config[ "start_backend" ] ) if "start_experiment_script" in config: self.config_start_experiment_script_checkbutton.set_active( config[ "start_experiment_script" ] ) if "start_result_script" in config: self.config_start_result_script_checkbutton.set_active( config[ "start_result_script" ] ) if "spool_dir" in config: self.config_spool_dir_entry.set_text( config[ "spool_dir" ] ) if "backend_executable" in config: self.config_backend_executable_entry.set_text( config[ "backend_executable" ] ) if "del_results_after_processing" in config: self.config_del_results_after_processing_checkbutton.set_active( config[ "del_results_after_processing" ] ) if "del_jobs_after_execution" in config: self.config_del_jobs_after_execution_checkbutton.set_active( config[ "del_jobs_after_execution" ] ) if "data_pool_write_interval" in config: self.config_data_pool_write_interval_entry.set_text( config[ "data_pool_write_interval" ] ) if "data_pool_name" in config: self.config_data_pool_name_entry.set_text( config[ "data_pool_name" ] ) if "data_pool_comprate" in config: self.config_data_pool_comprate.set_value( float( config[ "data_pool_comprate" ] ) ) if "script_font" in config: self.config_script_font_button.set_font_name( config[ "script_font" ] ) self.set_script_font_handler( None ) if "adc_bit_depth" in config: try: self.config_adc_bit_depth_spinbutton.set_value(float(config["adc_bit_depth"])) ADC_Result.default_bit_depth = int(config["adc_bit_depth"]) except (ValueError, TypeError): pass # Temperature monitoring configuration (only if widgets exist) if hasattr(self, 'config_temp_monitoring_checkbutton') and "enable_temperature_monitoring" in config: try: self.config_temp_monitoring_checkbutton.set_active(bool(config["enable_temperature_monitoring"])) except (TypeError, ValueError): pass if hasattr(self, 'config_temp_controller_type_combobox') and "temperature_controller_type" in config: try: controller_type = config["temperature_controller_type"] if controller_type in ["eurotherm", "simulated"]: self._set_combo_active_text(self.config_temp_controller_type_combobox, controller_type) # Update widget visibility based on type self._update_temp_widgets_visibility() except (TypeError, ValueError): pass if hasattr(self, 'config_temp_device_entry') and "temperature_device" in config: try: self.config_temp_device_entry.set_text(str(config["temperature_device"])) except (TypeError, ValueError): pass if hasattr(self, 'config_temp_baudrate_combobox') and "temperature_baudrate" in config: try: baudrate = str(config["temperature_baudrate"]) if baudrate in ["9600", "19200", "38400", "57600", "115200"]: self._set_combo_active_text(self.config_temp_baudrate_combobox, baudrate) else: self.config_temp_baudrate_combobox.set_active(0) # Default to 19200 except (TypeError, ValueError): pass if hasattr(self, 'config_temp_interval_spinbutton') and "temperature_interval" in config: try: self.config_temp_interval_spinbutton.set_value(float(config["temperature_interval"])) except (ValueError, TypeError): self.config_temp_interval_spinbutton.set_value(2.0) if "data_pool_complib" in config: # find combo-box entry and make it active... model = self.config_data_pool_complib.get_model( ) treeiter = model.get_iter_first( ) while treeiter is not None: if model.get( treeiter, 0 )[ 0 ] == config[ "data_pool_complib" ]: self.config_data_pool_complib.set_active_iter( treeiter ) break treeiter = model.iter_next( treeiter ) # if this compression method is not supported, warn and do nothing if treeiter is None: logger.warning("Compression method %s is not supported", config[ "data_pool_complib" ]) def on_adc_bit_depth_changed(self, widget): ADC_Result.default_bit_depth = widget.get_value_as_int() def on_temp_device_browse_clicked(self, widget): """ Open file chooser dialog to select temperature controller device. Defaults to /dev/serial/by-id/ which is typically used for USB serial converters. """ dialog = gtk.FileChooserDialog( title="Select Temperature Controller Device", parent=None, action=gtk.FileChooserAction.OPEN, ) dialog.add_button("Cancel", gtk.ResponseType.CANCEL) dialog.add_button("Open", gtk.ResponseType.OK) # Set default folder to /dev/serial/by-id/ if it exists default_folder = "/dev/serial/by-id/" if os.path.isdir(default_folder): dialog.set_current_folder(default_folder) else: # Fallback to /dev dialog.set_current_folder("/dev/") # Only show regular files (device files) dialog.set_select_multiple(False) response = dialog.run() if response == gtk.ResponseType.OK: selected_file = dialog.get_filename() self.config_temp_device_entry.set_text(selected_file) dialog.destroy() def on_temp_controller_type_changed(self, widget): """Update widget visibility when controller type changes.""" self._update_temp_widgets_visibility() def _update_temp_widgets_visibility(self): """Show/hide widgets based on selected controller type.""" # Defensive: check if combobox exists if not hasattr(self, 'config_temp_controller_type_combobox'): return active_type = self.config_temp_controller_type_combobox.get_active_text() if active_type is None: active_type = "eurotherm" # Defensive: check if widgets exist before showing/hiding show_device = active_type == "eurotherm" if hasattr(self, 'config_temp_device_hbox'): if show_device: self.config_temp_device_hbox.show() else: self.config_temp_device_hbox.hide() if hasattr(self, 'config_temp_baudrate_hbox'): if show_device: self.config_temp_baudrate_hbox.show() else: self.config_temp_baudrate_hbox.hide() def _set_combo_active_text(self, combobox, text): """ Set a ComboBoxText's active entry by text. Gtk.ComboBoxText.set_active_text() does not exist in GTK 3.24.x, so we iterate the model to find the matching entry. """ if not text: return model = combobox.get_model() if model is None: return for i in range(model.iter_n_children(None)): treeiter = model.iter_nth_child(None, i) if treeiter and model.get(treeiter, 0)[0] == text: combobox.set_active(i) return def load_config_handler( self, widget ): if self.system_default_filename: self.load_config( self.system_default_filename ) self.load_config( ) def save_config_handler( self, widget ): self.save_config( ) def config_keypress( self, widget, event ): """ Handle keyboard shortcuts in configuration window """ if event.state & gdk.ModifierType.CONTROL_MASK != 0: if event.keyval == gdk.keyval_from_name( "s" ): # Save configuration with Ctrl+S self.save_config( ) return True return False def set_script_font_handler( self, widget ): """ handles changes in font name also sets the fonts to the text views (fast implementation: breaking encapsulation) """ font = self.config_script_font_button.get_font_name( ) experiment_script_textview = self.xml_gui.get_object( "experiment_script_textview" ) if experiment_script_textview: apply_font_css(experiment_script_textview, font) data_handling_textview = self.xml_gui.get_object( "data_handling_textview" ) if data_handling_textview: apply_font_css(data_handling_textview, font) def browse_backend_executable_dialog( self, widget ): """ do the open file dialog """ backend_filename_dialog_title = "find backend" def response( self, response_id, script_widget ): if response_id == gtk.ResponseType.OK: file_name = self.get_filename( ) if file_name is None: return script_widget.config_backend_executable_entry.set_text( file_name ) return True parent_window = self.xml_gui.get_object( "main_window" ) dialog = gtk.FileChooserDialog( title=backend_filename_dialog_title, parent=parent_window, action=gtk.FileChooserAction.OPEN, buttons=("_Open", gtk.ResponseType.OK, "_Cancel", gtk.ResponseType.CANCEL) ) dialog.set_default_response( gtk.ResponseType.OK ) dialog.set_select_multiple( False ) dialog.set_filename( os.path.abspath( self.config_backend_executable_entry.get_text( ) ) ) if os.path.isdir( self.system_backend_folder ) \ and os.access( self.system_backend_folder, os.R_OK ): dialog.add_shortcut_folder( self.system_backend_folder ) # Event-Handler for responce-signal (when one of the button is pressed) dialog.connect( "response", response, self ) #f = gtk.FileFilter( ) #f.add_custom( gtk.FileFilterFlags.FILENAME, lambda x: os.access( x[ 0 ], os.X_OK ) ) #dialog.set_filter( f ) dialog.run( ) dialog.destroy( ) return True def load_config( self, filename=None ): """ set config from an xml file """ if filename is None: filename = self.user_default_filename try: readfile = open( filename, "rb" ) except Exception as e: if debug: logger.debug("Could not open %s: %s", filename, str( e )) return # parser functions def start_element( name, attrs, config ): if name == "config" and "key" in attrs: config[ "__this_key__" ] = attrs[ "key" ] if "type" in attrs: config[ "__this_type__" ] = attrs[ "type" ] config[ attrs[ "key" ] ] = "" def end_element( name, config ): if "__this_type__" in config and "__this_key__" in config: if config[ "__this_type__" ] == "Boolean": if config[ config[ "__this_key__" ] ] == "True": config[ config[ "__this_key__" ] ] = True else: config[ config[ "__this_key__" ] ] = False elif config[ "__this_type__" ] == "Integer": config[ config[ "__this_key__" ] ] = int( config[ config[ "__this_key__" ] ] ) if "__this_type__" in config: del config[ "__this_type__" ] if "__this_key__" in config: del config[ "__this_key__" ] def char_data( data, config ): if "__this_key__" in config: config[ config[ "__this_key__" ] ] += data # parse file contents to dictionary config = { } p = xml.parsers.expat.ParserCreate( ) p.StartElementHandler = lambda n, a: start_element( n, a, config ) p.EndElementHandler = lambda n: end_element( n, config ) p.CharacterDataHandler = lambda d: char_data( d, config ) p.ParseFile( readfile ) self.set( config ) def save_config( self, filename=None ): """ write config as an xml file """ config = self.get( ) if filename is None: filename = self.user_default_filename dirs = os.path.dirname( filename ) if not os.path.isdir( dirs ): os.makedirs( dirs ) logger.info("Save config to: %s", filename) configfile = open( filename, "w" ) configfile.write( "\n" ) configfile.write( "\n" ) for k, v in config.items( ): if k in self.config_from_system \ and self.config_from_system[ k ] == v: if debug: logger.debug("Ignoring for write, because system value for %r is %r equal to %r", k, self.config_from_system[ k ], v) continue logger.debug("Config key: %s = %r (%s)", k, v, type(v).__name__) val = "" typename = "" if type( v ) is bool: typename = "Boolean" if v: val = "True" else: val = "False" elif type( v ) is bytes: typename = "Bytes" val = v elif type( v ) is str: typename = "String" val = v elif type( v ) is int: typename = "Integer" val = str( v ) configfile.write( " %s\n" % (k, typename, val) ) configfile.write( "\n" ) # crosshair icon for the display toolbar, drawn from inline XPM data # so it does not depend on an installed icon theme crosshair_xpm = ( "24 24 2 1", " c None", ". c #cc0000", " .. ", " .. ", " .. ", " .. ", " .. ", " ", " ", " ...... ", " . . ", " . . ", " . .... . ", "..... . .... . .....", "..... . .... . .....", " . .... . ", " . . ", " . . ", " ...... ", " ", " ", " .. ", " .. ", " .. ", " .. ", " .. ", ) class BlitSafeCanvas( FigureCanvas ): """ GTK3Agg canvas that paints blitted regions immediately. The stock blit appends the region to an internal bbox queue and the next paint then redraws only that region; a full redraw (data update, zoom) happening meanwhile is clipped to it, wiping the axis labels from the screen. Painting the region right away keeps every regular paint a complete one and stays a partial update. """ def blit( self, bbox=None ): if bbox is None: bbox = self.figure.bbox window = self.get_window( ) if window is None: return scale = self.device_pixel_ratio allocation = self.get_allocation( ) x = int( bbox.x0 / scale ) y = allocation.height - int( bbox.y1 / scale ) width = ( int( bbox.x1 ) - int( bbox.x0 ) ) // scale height = ( int( bbox.y1 ) - int( bbox.y0 ) ) // scale region = cairo.Region( cairo.RectangleInt( x, y, width, height ) ) context = window.begin_draw_frame( region ) try: buffer = cbook._unmultiplied_rgba8888_to_premultiplied_argb32( numpy.asarray( self.copy_from_bbox( bbox ) ) ) image = cairo.ImageSurface.create_for_data( buffer.ravel( ).data, cairo.FORMAT_ARGB32, int( bbox.x1 ) - int( bbox.x0 ), int( bbox.y1 ) - int( bbox.y0 ) ) image.set_device_scale( scale, scale ) cr = context.get_cairo_context( ) cr.set_source_surface( image, x, y ) cr.paint( ) finally: window.end_draw_frame( context ) class CrosshairCursor( matplotlib.widgets.Cursor ): """ Crosshair cursor with blitting that coalesces motion events to a fixed frame rate. GTK delivers motion-notify events at the mouse's full reporting rate (up to 1000 Hz); restoring the plot background for every single event makes the GUI sluggish, in particular on remote X11 connections. A frame rate limit is visually indistinguishable. Per frame only the thin strips around the old and the new line position are restored, redrawn and blitted. Blitting the full axes bbox instead requires a pixel format conversion of several megapixels per frame, which is far too slow on HiDPI screens. """ def __init__( self, ax, frame_rate=60, **lineprops ): matplotlib.widgets.Cursor.__init__( self, ax, useblit=True, **lineprops ) self.frame_interval = 1.0 / frame_rate self.last_frame_time = 0.0 self._last_x = None self._last_y = None def clear( self, event ): """Refresh the saved background after a full render.""" if self.ignore( event ): return if self.useblit: # save the full figure so the saved array stays aligned with # the canvas buffer and strips can be restored by direct # array copy (restore_region with a bbox argument addresses # a different coordinate frame and restores the wrong area) self.background = self.canvas.copy_from_bbox( self.canvas.figure.bbox ) # the render removed the cursor lines from the buffer, but # the screen still shows them until the repaint arrives; # motion events processed in between would draw the cursor # at its new position next to the stale one, so wipe the old # line positions right away for strip in ( self._strip_bbox( self._last_x, None, True ), self._strip_bbox( self._last_y, None, False ) ): if strip is not None: self.canvas.blit( strip ) self.linev.set_visible( False ) self.lineh.set_visible( False ) self._last_x = None self._last_y = None def _strip_bbox( self, prev, cur, vertical ): """Bbox covering the old and new position of one cursor line. The strips are padded by a pixel on each side so that integer truncation downstream never clips a line pixel at the edges. """ bbox = self.ax.bbox positions = [p for p in ( prev, cur ) if p is not None] if not positions: return None if vertical: lo = max( min( positions ) - 4, bbox.x0 ) hi = min( max( positions ) + 5, bbox.x1 ) if hi <= lo: return None return Bbox( [[lo, bbox.y0 - 1], [hi, bbox.y1 + 1]] ) lo = max( min( positions ) - 4, bbox.y0 ) hi = min( max( positions ) + 5, bbox.y1 ) if hi <= lo: return None return Bbox( [[bbox.x0 - 1, lo], [bbox.x1 + 1, hi]] ) def onmove( self, event ): if self.ignore( event ) or not self.canvas.widgetlock.available( self ): return now = time.monotonic( ) if now - self.last_frame_time < self.frame_interval: return self.last_frame_time = now if self.background is None: return if event.inaxes is self.ax and event.xdata is not None and event.ydata is not None: self.linev.set_xdata( ( event.xdata, event.xdata ) ) self.lineh.set_ydata( ( event.ydata, event.ydata ) ) self.linev.set_visible( self.visible and self.vertOn ) self.lineh.set_visible( self.visible and self.horizOn ) new_x, new_y = event.x, event.y else: # pointer left the plot: hide the lines by restoring the plain # background, no full render needed self.linev.set_visible( False ) self.lineh.set_visible( False ) new_x = new_y = None bbox = self.ax.bbox vstrip = self._strip_bbox( self._last_x, new_x, True ) hstrip = self._strip_bbox( self._last_y, new_y, False ) self._last_x, self._last_y = new_x, new_y # erase the previous lines by copying the strips back from the # saved background background = numpy.asarray( self.background ) width, height = self.canvas.get_width_height( physical=True ) buffer = numpy.frombuffer( self.canvas.buffer_rgba( ), dtype=numpy.uint8 ).reshape( height, width, 4 ) for strip in ( vstrip, hstrip ): if strip is not None: rows = slice( height - int( strip.y1 ), height - int( strip.y0 ) + 1 ) cols = slice( int( strip.x0 ), int( strip.x1 ) + 1 ) buffer[rows, cols] = background[rows, cols] if self.linev.get_visible( ) or self.lineh.get_visible( ): self.ax.draw_artist( self.linev ) self.ax.draw_artist( self.lineh ) for strip in ( vstrip, hstrip ): if strip is not None: self.canvas.blit( strip ) class MonitorWidgets: """Manages the matplotlib-based data visualization panel. Provides an interactive plot canvas with zoom, pan, scaling controls, a data source selector, and real-time updates from the DAMARIS data pool. Supports :class:`Accumulation`, :class:`ADC_Result`, and :class:`MeasurementResult` data types with automatic axis rescaling, error bars, and statistics overlays. Args: xml_gui: The :class:`gtk.Builder` instance containing the GUI widgets. Attributes: matplot_figure (:class:`matplotlib.figure.Figure`): The underlying matplotlib figure. matplot_axes (:class:`matplotlib.axes.Axes`): The plot axes. matplot_canvas (:class:`matplotlib.backends.backend_gtk3.FigureCanvas`): The GTK canvas embedding the figure. matplot_toolbar (:class:`matplotlib.backends.backend_gtk3.NavigationToolbar2GTK3`): The navigation toolbar with pan/zoom/cursor controls. data_pool (:class:`damaris.data.DataPool` or None): The data pool being observed for real-time updates. displayed_data (list): ``[data_name, data_object]`` of the currently selected data, or ``[None, None]``. Public Methods: observe_data_pool: Register a listener and begin observing a data pool. source_list_reset: Reset the data source selector tree. source_list_add: Add a data source entry to the selector tree. source_list_remove: Remove a data source entry from the selector tree. source_list_current: Get the currently selected data source name. clear_display: Clear all plot elements and reset axes. update_display: Redraw the plot with current data. renew_display: Clear and redraw the plot with current data. save_display_data_as_text: Export displayed data to a CSV file. toggle_cursor: Toggle the matplotlib crosshair cursor. copy_canvas_as_png: Copy the plot as a PNG image to the clipboard. copy_canvas_as_svg: Copy the plot as SVG text to the clipboard. """ def __init__( self, xml_gui ): """Initialize matplotlib widgets, canvas, toolbar, and source selector. Creates a :class:`matplotlib.figure.Figure` with linear x/y axes, embeds it in a GTK canvas, attaches a navigation toolbar with a custom crosshair toggle button, and builds a hierarchical source selector :class:`gtk.TreeStore`. Args: xml_gui: The :class:`gtk.Builder` instance containing the GUI widgets. """ self.xml_gui = xml_gui self.main_window = self.xml_gui.get_object( "main_window" ) self.display_settings_frame = self.xml_gui.get_object( "display_settings_frame" ) # Display footer: self.display_x_scaling_combobox = self.xml_gui.get_object( "display_x_scaling_combobox" ) self.display_x_scaling_combobox.set_sensitive( False ) self.display_y_scaling_combobox = self.xml_gui.get_object( "display_y_scaling_combobox" ) self.display_y_scaling_combobox.set_sensitive( False ) self.display_autoscaling_checkbutton = self.xml_gui.get_object( "display_autoscaling_checkbutton" ) self.display_statistics_checkbutton = self.xml_gui.get_object( "display_statistics_checkbutton" ) self.display_descriptions_checkbutton = gtk.CheckButton(label="Show Desc.") self.display_descriptions_checkbutton.set_active(True) self.display_descriptions_checkbutton.show() self.display_settings_frame.set_property("n-columns", 5) self.display_settings_frame.attach(self.display_descriptions_checkbutton, 4, 5, 0, 1, gtk.AttachOptions.FILL, 0, 0, 0) self.display_descriptions_checkbutton.connect("toggled", self.display_descriptions_toggled) # Matplot (Display_Table, 1st Row) -------------------------------------------------------- # create new plot self.matplot_figure = matplotlib.figure.Figure( ) # the plot area surrounded by axes self.matplot_axes = self.matplot_figure.add_axes( [ 0.1, 0.15, 0.8, 0.7 ] ) self.matplot_axes.grid( True ) self.matplot_axes.set_ylim( [ 0.0, 1.0 ] ) self.matplot_axes.set_xlim( [ 0.0, 1.0 ] ) self.matplot_axes.set_autoscale_on( self.display_autoscaling_checkbutton.get_active( ) ) # linear y/x scaling self.matplot_axes.set_yscale( "linear" ) self.matplot_axes.set_xscale( "linear" ) # add figure to canvas self.matplot_canvas = BlitSafeCanvas( self.matplot_figure ) # add ctrl + right-click context menu to canvas self.matplot_canvas.connect("button-press-event", self.on_canvas_button_press) self.display_table = self.xml_gui.get_object( "display_table" ) self.display_table.attach( self.matplot_canvas, 0, 6, 0, 1, gtk.AttachOptions.EXPAND | gtk.AttachOptions.FILL, gtk.AttachOptions.EXPAND | gtk.AttachOptions.FILL, 0, 0 ) self.matplot_canvas.show( ) # add matplotlib toolbar (display_table, 2nd row) self.matplot_toolbar = matplotlib.backends.backend_gtk3.NavigationToolbar2GTK3( self.matplot_canvas ) # remove subplots: #self.matplot_toolbar.DeleteToolByPos(1) #self.matplot_toolbar.remove(self.matplot_toolbar.get_nth_item(3)) self.matplot_toolbar.remove(self.matplot_toolbar.get_nth_item(6)) # now let's add a button to the toolbar self.matplot_cursor_button = gtk.ToggleButton(label='') cursor_icon = GdkPixbuf.Pixbuf.new_from_xpm_data( crosshair_xpm ) self.matplot_cursor_button.set_image( gtk.Image.new_from_pixbuf( cursor_icon ) ) # gtk-button-images is False on many desktops, which would hide the icon self.matplot_cursor_button.set_always_show_image( True ) self.matplot_cursor_button.set_tooltip_text('Enable Crosshairs') self.matplot_cursor_button.show() self.matplot_cursor_button.connect('toggled', self.toggle_cursor) toolitem = gtk.ToolItem() toolitem.add(self.matplot_cursor_button) self.matplot_toolbar.insert(toolitem, 5) # Make cursor position text bigger and bold - access message label directly try: # The coordinate label is stored in the toolbar's message attribute if hasattr(self.matplot_toolbar, 'message'): apply_font_css(self.matplot_toolbar.message, "sans bold 16") except: pass self.display_table.attach( self.matplot_toolbar, 0, 1, 1, 2, gtk.AttachOptions.FILL | gtk.AttachOptions.EXPAND, 0, 0, 0 ) self.matplot_toolbar.show( ) # select display source self.display_source_combobox = self.xml_gui.get_object( "display_source_combobox" ) self.display_source_treestore = gtk.TreeStore( gobject.TYPE_STRING ) self.display_source_combobox.set_model( self.display_source_treestore ) display_source_cell = gtk.CellRendererText( ) self.display_source_combobox.pack_start( display_source_cell, True ) self.display_source_combobox.add_attribute( display_source_cell, 'text', 0 ) # GTK3 fix: Prevent blank space from child tree nodes in popup self.display_source_combobox.set_wrap_width(1) self.source_list_reset( ) self.display_source_path_label = self.xml_gui.get_object( "display_source_path_label" ) # enable display scaling self.display_x_scaling_combobox.set_active( 0 ) self.display_y_scaling_combobox.set_active( 0 ) for i in ("lin", "log10"): self.display_x_scaling_combobox.append_text(i) self.display_y_scaling_combobox.append_text(i) self.display_x_scaling_combobox.append_text("ppm") self.display_x_scaling_combobox.set_active(0) self.display_y_scaling_combobox.set_active(0) self.display_x_scaling_combobox.set_sensitive( True ) self.display_y_scaling_combobox.set_sensitive( True ) # and events... self.display_source_combobox.connect( "changed", self.display_source_changed_event ) # data to observe self.data_pool = None # name of displayed data and reference to data self.displayed_data = [ None, None ] self.__rescale = True self.update_counter = 0 self.update_counter_lock = threading.Lock( ) self.description_text = None self.fitresultlines = [ ] self.fit_source_keys = set( ) self.clipping_marker = None self.source_list_cache = {} def source_list_reset( self ): self.display_source_combobox.set_model(None) try: # OPTIMIZATION: Replacing the TreeStore is much faster than clear() for large trees self.display_source_treestore = gtk.TreeStore( gobject.TYPE_STRING ) self.source_list_cache = {} self.source_list_add( 'None' ) finally: self.display_source_combobox.set_model(self.display_source_treestore) none_iter = self.source_list_find( [ 'None' ] ) if none_iter is not None: self.display_source_combobox.set_active_iter( none_iter ) def source_list_find_one( self, model, treeiter, what ): """find node in subcategory""" while treeiter is not None: if str( model.get( treeiter, 0 )[ 0 ] ) == what: return treeiter treeiter = model.iter_next( treeiter ) return treeiter def source_list_find( self, namelist ): """ namelist sequence of names, e.g. ["a", "b", "c" ] for "a/b/c" returns the deepest found iter. namelist is modified (items found are popped). """ # Try full path from cache first full_path = "/".join(namelist) if full_path in self.source_list_cache: found_iter = self.source_list_cache[full_path] # Verify it's still valid by checking it belongs to our model if self.display_source_treestore.iter_is_valid(found_iter): del namelist[:] return found_iter else: del self.source_list_cache[full_path] # Try partial paths from cache for i in range(len(namelist) - 1, 0, -1): partial_path = "/".join(namelist[:i]) if partial_path in self.source_list_cache: found_iter = self.source_list_cache[partial_path] if self.display_source_treestore.iter_is_valid(found_iter): del namelist[:i] # Continue search from here retval = found_iter treeiter = self.display_source_treestore.iter_children( retval ) while treeiter is not None and len( namelist ) > 0: name = namelist[ 0 ] treeiter = self.source_list_find_one( self.display_source_treestore, treeiter, name ) if treeiter is not None: retval = treeiter namelist.pop( 0 ) # Update cache partial_path += "/" + name self.source_list_cache[partial_path] = retval treeiter = self.display_source_treestore.iter_children( retval ) return retval else: del self.source_list_cache[partial_path] model = self.display_source_treestore retval = None #treeiter = model.get_iter_root( ) treeiter = model.get_iter_first( ) current_path = [] while treeiter is not None and len( namelist ) > 0: name = namelist[ 0 ] treeiter = self.source_list_find_one( model, treeiter, name ) if treeiter is not None: retval = treeiter namelist.pop( 0 ) current_path.append(name) self.source_list_cache["/".join(current_path)] = retval treeiter = model.iter_children( retval ) return retval def source_list_add( self, source_name, parent=None ): namelist = source_name.split( "/" ) found = self.source_list_find( namelist ) if parent is None: parent = found current_path = source_name.split("/") if found is not None: # We found part of the path, namelist now contains the rest # Adjust current_path to reflect the part we already have already_found_len = len(current_path) - len(namelist) current_path = current_path[:already_found_len] else: current_path = [] for rest_name in namelist: # append() returns iter for the new row parent = self.display_source_treestore.append( parent, [ rest_name ] ) current_path.append(rest_name) self.source_list_cache["/".join(current_path)] = parent def source_list_remove( self, source_name ): namelist = source_name.split( "/" ) pwd = namelist[ : ] treeiter = self.source_list_find( namelist ) if treeiter is None or len( namelist ) > 0: logger.warning("source_list_remove: Not found") return model = self.display_source_treestore if model.iter_has_child( treeiter ): logger.warning("source_list_remove: Request to delete a tree") return while True: parent = model.iter_parent( treeiter ) model.remove( treeiter ) pwd.pop( ) # We now test, if we want to remove parent too if parent is None: break if model.iter_has_child( parent ): # The parent has other children break if "/".join( pwd ) in self.data_pool: # The parent has data connected to it break treeiter = parent def source_list_current( self ): ai = self.display_source_combobox.get_active_iter( ) namelist = [ ] while ai is not None: namelist.insert( 0, str( self.display_source_treestore.get( ai, 0 )[ 0 ] ) ) ai = self.display_source_treestore.iter_parent( ai ) cur_source_name = "/".join( namelist ) return cur_source_name def observe_data_pool( self, data_pool ): """ register a listener and save reference to data assume to be in gtk/gdk lock """ if self.data_pool is not None: # maybe some extra cleanup needed if self.displayed_data[ 1 ] is not None and hasattr( self.displayed_data[ 1 ], "unregister_listener" ): self.displayed_data[ 1 ].unregister_listener( self.datastructures_listener ) self.displayed_data[ 1 ] = None self.displayed_data = [ None, None ] self.data_pool.unregister_listener( self.datapool_listener ) self.data_pool = None self.update_counter_lock.acquire( ) self.update_counter = 0 self.update_counter_lock.release( ) # display states self.source_list_reset( ) self.__rescale = True self.displayed_data = [ None, None ] self.fit_source_keys = set( ) self.display_source_path_label.set_label( "" ) self.clear_display( ) if data_pool is not None: # keep track of data self.data_pool = data_pool self.data_pool.register_listener( self.datapool_listener ) # Populate source list with existing data since the listener # was registered after the data was loaded keys = sorted([k for k in data_pool if k != "__builtins__"]) for key in keys: self.source_list_add(key) # Select first data item if available keys = [k for k in data_pool if not k.startswith("__")] if keys: keys.sort() first_key = keys[0] first_iter = self.source_list_find(first_key.split("/")) if first_iter: self.display_source_combobox.set_active_iter(first_iter) #################### observing data structures and produce idle events def datapool_listener( self, event ): """ sort data as fast as possible and get rid of non-interesting data """ if event.subject.startswith( "__" ): return if debug and self.update_counter < 0: logger.debug("Negative event count: %d", self.update_counter) # Throttling removed as it blocks data production thread. # GUI updates are already decoupled via idle_add. if event.what == DataPool.Event.updated_value: if self.displayed_data[ 0 ] is None or event.subject != self.displayed_data[ 0 ]: # do nothing, forget it return displayed_object = self.displayed_data[ 1 ] object_to_display = self.data_pool.get( event.subject ) if displayed_object is None or object_to_display is None: self.update_counter_lock.acquire( ) self.update_counter += 1 self.update_counter_lock.release( ) glib.idle_add(self.datapool_idle_listener, event, priority=glib.PRIORITY_DEFAULT_IDLE ) else: if event.what == DataPool.Event.updated_value and \ ( displayed_object is object_to_display or displayed_object.__class__ is object_to_display.__class__): # oh, another category self.update_counter_lock.acquire() self.update_counter += 1 self.update_counter_lock.release() glib.idle_add(self.update_display_idle_event, self.displayed_data[ 0 ][ : ], priority=glib.PRIORITY_DEFAULT_IDLE ) else: self.update_counter_lock.acquire( ) self.update_counter += 1 self.update_counter_lock.release( ) glib.idle_add(self.datapool_idle_listener, event, priority=glib.PRIORITY_DEFAULT_IDLE ) if event.what in [ DataPool.Event.updated_value, DataPool.Event.new_key ]: #print "Update hdf5 file ..." # TODO: incremental hdf5 update pass del displayed_object del object_to_display def datastructures_listener( self, event ): """ do fast work selecting important events """ if debug and self.update_counter < 0: logger.debug("Negative event count: %d", self.update_counter) if self.update_counter > 5: if debug: logger.debug("Sleeping to find time for graphics updates") threading.Event( ).wait( 0.05 ) while self.update_counter > 15: threading.Event( ).wait( 0.05 ) if event.origin is not self.displayed_data[ 1 ]: return self.update_counter_lock.acquire( ) self.update_counter += 1 self.update_counter_lock.release( ) glib.idle_add(self.update_display_idle_event, self.displayed_data[ 0 ][ : ], priority=glib.PRIORITY_DEFAULT_IDLE ) ################### consume idle events def datapool_idle_listener( self, event ): """ here dictionary changes are done """ self.update_counter_lock.acquire( ) self.update_counter -= 1 self.update_counter_lock.release( ) if event.what == DataPool.Event.updated_value: if (self.displayed_data[ 0 ] is not None and self.displayed_data[ 0 ] == event.subject): new_data_struct = self.data_pool[ self.displayed_data[ 0 ] ] if self.displayed_data[ 1 ] is new_data_struct: # update display only if self.update_counter > 10: logger.warning("Update queue too long (%d>10): skipping one update", self.update_counter) else: self.update_display( ) else: # unregister old one if self.displayed_data[ 1 ] is not None and hasattr( self.displayed_data[ 1 ], "unregister_listener" ): self.displayed_data[ 1 ].unregister_listener( self.datastructures_listener ) self.displayed_data[ 1 ] = None # register new one if hasattr( new_data_struct, "register_listener" ): new_data_struct.register_listener( self.datastructures_listener ) self.displayed_data[ 1 ] = new_data_struct if self.update_counter > 10: logger.warning("Update queue too long (%d>10): skipping one update", self.update_counter) else: self.renew_display( ) new_data_struct = None elif event.what == DataPool.Event.new_key: if isinstance( self.data_pool.get( event.subject ), FitResult ): # fits are shown as overlay on their measurement only, # they do not get their own entry in the display tree self.fit_source_keys.add( event.subject ) else: # update combo-box by inserting and rely on consistent information self.source_list_add( event.subject ) elif event.what == DataPool.Event.deleted_key: if event.subject in self.fit_source_keys: # fit entries are not in the display tree self.fit_source_keys.discard( event.subject ) return # update combo-box by removing and rely on consistent information if (self.displayed_data[ 0 ] is not None and self.displayed_data[ 0 ] == event.subject): self.displayed_data = [ None, None ] none_iter = self.source_list_find( [ 'None' ] ) if none_iter is not None: self.display_source_combobox.set_active_iter( none_iter ) # not necessary, because event will be submitted #self.clear_display() self.source_list_remove( event.subject ) elif event.what == DataPool.Event.destroy: self.source_list_reset( ) self.displayed_data = [ None, None ] self.clear_display( ) return def update_display_idle_event( self, subject=None ): self.update_counter_lock.acquire( ) self.update_counter -= 1 self.update_counter_lock.release( ) # print "update display", self.update_counter if self.update_counter > 10: logger.warning("Update queue too long (%d>10): skipping one update", self.update_counter) return if self.displayed_data[ 0 ] is None or subject != self.displayed_data[ 0 ]: return self.update_display( ) ######################## events from buttons def display_source_changed_event( self, widget, data=None ): new_data_name = self.source_list_current( ) if (self.displayed_data[ 0 ] is None and new_data_name == "None"): return if (self.displayed_data[ 0 ] == new_data_name): return if self.displayed_data[ 1 ] is not None and hasattr( self.displayed_data[ 1 ], "unregister_listener" ): self.displayed_data[ 1 ].unregister_listener( self.datastructures_listener ) self.displayed_data[ 1 ] = None # register new one if new_data_name == "None": self.display_source_path_label.set_label( "" ) self.displayed_data = [ None, None ] self.clear_display( ) elif self.data_pool is None or new_data_name not in self.data_pool: none_iter = self.source_list_find( [ 'None' ] ) if none_iter is not None: self.display_source_combobox.set_active_iter( none_iter ) self.display_source_path_label.set_label( "" ) else: new_data_struct = self.data_pool[ new_data_name ] if hasattr( new_data_struct, "register_listener" ): new_data_struct.register_listener( self.datastructures_listener ) self.displayed_data = [ new_data_name, new_data_struct ] dirpart = new_data_name.rfind( "/" ) if dirpart >= 0: self.display_source_path_label.set_label( "in " + new_data_name[ :dirpart ] ) else: self.display_source_path_label.set_label( "" ) self.clear_display( ) # renew display via idle event self.update_counter_lock.acquire( ) self.update_counter += 1 self.update_counter_lock.release( ) event = DataPool.Event( DataPool.Event.updated_value, new_data_name ) glib.idle_add(self.datapool_idle_listener, event, priority=glib.PRIORITY_DEFAULT_IDLE ) def display_autoscaling_toggled( self, widget, data=None ): self.matplot_axes.set_autoscale_on( self.display_autoscaling_checkbutton.get_active( ) ) if self.displayed_data[ 0 ] is not None: self.update_display( self.displayed_data[ 0 ][ : ] ) def display_scaling_changed( self, widget, data=None ): self.__rescale = True if self.displayed_data[ 0 ] is not None: self.update_display( self.displayed_data[ 0 ][ : ] ) def display_statistics_toggled( self, widget, data=None ): if self.displayed_data[ 0 ] is not None: self.update_display( self.displayed_data[ 0 ][ : ] ) def display_descriptions_toggled( self, widget, data=None ): if self.displayed_data[ 0 ] is not None: self.update_display( self.displayed_data[ 0 ][ : ] ) def save_display_data_as_text( self, widget, data=None ): """ copy data to tmp file and show save dialog """ data_to_save = self.displayed_data[ : ] if self.displayed_data[ 1 ] is None: # nothing to save return if not hasattr( data_to_save[ 1 ], "write_to_csv" ): log( "do not know how to save %s of class/type %s" % (data_to_save[ 0 ], type( data_to_save[ 1 ] )) ) return # save them to a temporary file (in memory) tmpdata = tempfile.TemporaryFile(mode="w+" ) tmpdata.write("# saved from monitor as %s\n" % data_to_save[ 0 ] ) data_to_save[ 1 ].write_to_csv( tmpdata ) parentfordialog = self.main_window # show save dialog def response( self, response_id, tmpfile ): if response_id == gtk.ResponseType.OK: file_name = dialog.get_filename( ) if file_name is None: return True absfilename = os.path.abspath( file_name ) if os.access( file_name, os.F_OK ): question = gtk.MessageDialog(parent=parentfordialog, type=gtk.MessageType.WARNING, flags=gtk.DialogFlags.MODAL, buttons=("_Yes", gtk.ResponseType.YES, "_No", gtk.ResponseType.NO)) question.set_markup("The file already exists. Do you want to overwrite the existing file and delete its contents?") response = question.run() question.destroy() if response != gtk.ResponseType.YES: return True textfile = open( absfilename, "w" ) tmpfile.seek( 0 ) for l in tmpfile: textfile.write( l ) textfile.close( ) textfile = None tmpfile = None return True # Determining the tab which is currently open dialog_title = "Save %s in file" % data_to_save[ 0 ] dialog = gtk.FileChooserDialog( title=dialog_title, parent=self.main_window, action=gtk.FileChooserAction.SAVE, buttons=("_Save", gtk.ResponseType.OK, "_Cancel", gtk.ResponseType.CANCEL) ) dialog.set_default_response( gtk.ResponseType.OK ) dialog.set_current_name( data_to_save[ 0 ] ) dialog.set_select_multiple( False ) # Event-Handler for responce-signal (when one of the button is pressed) dialog.connect( "response", response, tmpdata ) del tmpdata, data_to_save dialog.run( ) dialog.destroy( ) return True def toggle_cursor(self, widget): """Toggle matplotlib crosshair cursor""" if widget.get_active(): # Activate cursor if not hasattr(self, 'matplot_cursor') or self.matplot_cursor is None: self.matplot_cursor = CrosshairCursor(self.matplot_axes, color='red', linewidth=1) self.matplot_canvas.draw() # TODO: Deactivate the pane button else: # Deactivate cursor if hasattr(self, 'matplot_cursor') and self.matplot_cursor is not None: self.matplot_cursor.disconnect_events() # Remove the lines from the plot if hasattr(self.matplot_cursor, 'lineh'): self.matplot_cursor.lineh.remove() if hasattr(self.matplot_cursor, 'linev'): self.matplot_cursor.linev.remove() self.matplot_cursor = None self.matplot_canvas.draw() def on_canvas_button_press(self, widget, event): """Handle button press events on matplotlib canvas""" if event.button == 2: # middle click self.show_canvas_context_menu(event) return True return False def show_canvas_context_menu(self, event): """Show context menu for matplotlib canvas""" menu = gtk.Menu() # toggle cursor toggle_cursor = gtk.CheckMenuItem(label="Crosshair") # Set the current state based on whether cursor exists if hasattr(self, 'matplot_cursor') and self.matplot_cursor is not None: toggle_cursor.set_active(True) else: toggle_cursor.set_active(False) toggle_cursor.connect("toggled", lambda w: self.matplot_cursor_button.set_active(w.get_active())) menu.append(toggle_cursor) # Copy as PNG item_copy_png = gtk.MenuItem(label="Copy as PNG") item_copy_png.connect("activate", self.copy_canvas_as_png) menu.append(item_copy_png) # Copy as SVG item_copy_svg = gtk.MenuItem(label="Copy as SVG") item_copy_svg.connect("activate", self.copy_canvas_as_svg) menu.append(item_copy_svg) menu.show_all() menu.popup_at_pointer(event) def copy_canvas_as_png(self, widget): """Copy canvas to clipboard as PNG""" self.copy_display_data_to_clipboard(widget) def copy_canvas_as_svg(self, widget): """Copy canvas as SVG to clipboard""" if self.matplot_figure is None: return try: # Save the original figure size original_size = self.matplot_figure.get_size_inches() # Save figure to a buffer as SVG import io as iomodule buf = iomodule.StringIO() self.matplot_figure.set_size_inches(6, 4) self.matplot_figure.savefig(buf, format='svg', bbox_inches='tight') buf.seek(0) # Copy SVG text to clipboard clipboard = gtk.Clipboard.get(gdk.SELECTION_CLIPBOARD) clipboard.set_text(buf.read(), -1) clipboard.store() self.matplot_figure.set_size_inches(original_size) log("Figure copied to clipboard as SVG") except Exception as e: log(f"Error copying figure as SVG: {e}") def copy_display_data_to_clipboard( self, widget, data=None ): """ Copy matplotlib figure as PNG image to clipboard Also keep text data copy functionality as fallback """ # First, try to copy the matplotlib figure as image # Save the original figure size original_size = self.matplot_figure.get_size_inches() if self.matplot_figure is not None and self.matplot_canvas is not None: try: # Save figure to a temporary buffer as PNG import io as iomodule buf = iomodule.BytesIO() self.matplot_figure.set_size_inches(6, 4) self.matplot_figure.savefig(buf, format='png', dpi=300, bbox_inches='tight') buf.seek(0) # Load the PNG data into a GdkPixbuf from gi.repository import GdkPixbuf loader = GdkPixbuf.PixbufLoader.new_with_type('png') loader.write(buf.read()) loader.close() pixbuf = loader.get_pixbuf() # Copy to clipboard clipboard = gtk.Clipboard.get(gdk.SELECTION_CLIPBOARD) clipboard.set_image(pixbuf) clipboard.store() # Restore original figure size self.matplot_figure.set_size_inches(original_size) # Redraw the canvas to ensure it stays visible self.matplot_canvas.draw() log("Figure copied to clipboard as PNG image") return except Exception as e: log("Error copying figure to clipboard: %s" % str(e)) ##################### functions to feed display def clear_display( self ): """ unconditionally throw away everything we are inside gtk/gdk lock """ self.display_x_scaling_combobox.set_sensitive( True ) self.display_y_scaling_combobox.set_sensitive( True ) x_scale = self.display_x_scaling_combobox.get_active_text() y_scale = self.display_y_scaling_combobox.get_active_text() if x_scale == "lin": self.matplot_axes.set_xscale("linear") elif x_scale == "log10": self.matplot_axes.set_xscale("log", base=10, nonpositive="mask") elif x_scale == "ppm": self.matplot_axes.set_xscale("linear") self.matplot_axes.invert_xaxis() if y_scale == "lin": self.matplot_axes.set_yscale("linear") elif y_scale == "log10": self.matplot_axes.set_yscale("log", base=10, nonpositive="mask") if not hasattr( self, "__rescale" ): self.__rescale = True if not hasattr( self, "measurementresultline" ): self.measurementresultline = None elif self.measurementresultline is not None: # clear line plot self.matplot_axes.lines.remove( self.measurementresultline[ 0 ] ) self.measurementresultline = None if hasattr(self, 'matplot_cursor') and self.matplot_cursor is not None: self.matplot_cursor.disconnect_events() self.matplot_cursor = None if not hasattr( self, "measurementresultgraph" ): self.measurementresultgraph = None elif self.measurementresultgraph is not None: # clear errorbars self.measurementresultgraph[0].remove() for line in self.measurementresultgraph[ 1 ]: line.remove() for line in self.measurementresultgraph[ 2 ]: line.remove() self.measurementresultgraph = None self.matplot_axes.clear( ) self.matplot_axes.grid( True ) if not hasattr( self, "fitresultlines" ): self.fitresultlines = [ ] elif self.fitresultlines: for line in self.fitresultlines: try: line.remove( ) except: pass self.fitresultlines = [ ] if not hasattr( self, "graphen" ): self.graphen = [ ] elif self.graphen: for line in self.graphen: line.remove() self.graphen = [ ] self.matplot_axes.clear( ) self.matplot_axes.grid( True ) if self.description_text is not None: try: self.description_text.remove() except: pass self.description_text = None if self.clipping_marker is not None: try: self.clipping_marker.remove() except: pass self.clipping_marker = None # the crosshair line artists were removed from the axes above, # recreate the crosshair if it is enabled if self.matplot_cursor_button.get_active( ): self.matplot_cursor = CrosshairCursor(self.matplot_axes, color='red', linewidth=1) self.matplot_canvas.draw_idle( ) def update_display( self, subject=None ): """ try to recycle labels, data, lines.... assume, object is not changed we are inside gtk/gdk lock """ in_result = self.data_pool.get( self.displayed_data[ 0 ] ) if in_result is None: self.clear_display( ) return if isinstance( in_result, Accumulation ) or isinstance( in_result, ADC_Result ) or isinstance( in_result, TemperatureResult ): xmin = in_result.get_xmin( ) xmax = in_result.get_xmax( ) ymin = in_result.get_ymin( ) ymax = in_result.get_ymax( ) # Initial rescaling needed? if self.__rescale: x_scale = self.display_x_scaling_combobox.get_active_text( ) y_scale = self.display_y_scaling_combobox.get_active_text( ) # x-axis if x_scale == "lin": self.matplot_axes.set_xscale( "linear" ) self.matplot_axes.set_xlim(xmin, xmax) elif x_scale == "log10": xminpos = in_result.get_xminpos( ) self.matplot_axes.set_xscale("log", base=10, nonpositive="mask") self.matplot_axes.set_xlim(xminpos, xmax) elif x_scale == "ppm": self.matplot_axes.set_xscale("linear") self.matplot_axes.set_xlabel("PPM") #self.display_autoscaling_checkbutton.set_active(True) #partial fix T141 self.matplot_axes.set_xlim(xmax, xmin) #self.matplot_axes.invert_xaxis() # y-axis if y_scale == "lin": self.matplot_axes.set_yscale( "linear" ) self.matplot_axes.set_ylim(ymin, ymax) elif y_scale == "log10": yminpos = in_result.get_yminpos() self.matplot_axes.set_yscale("log", base=10, nonpositive="mask") self.matplot_axes.set_ylim( yminpos, ymax ) self.__rescale = False # Autoscaling activated? elif self.display_autoscaling_checkbutton.get_active(): xlim_min, xlim_max = self.matplot_axes.get_xlim() if xlim_min != xmin or xlim_max != xmax: self.matplot_axes.set_xlim(xmin, xmax) ylim_min, ylim_max = self.matplot_axes.get_ylim() # Check if y-axis is log scale yscale = self.matplot_axes.get_yscale() if yscale == 'log' or yscale == 'symlog': # For log scales, work with log values to avoid issues with # small numbers (undetected rescale) # Use log-space calculations log_ymin, log_ymax = numpy.log10(ymin), numpy.log10(ymax) log_diff = log_ymax - log_ymin # Add a small margin in log space (about 0.08 in log10 units ~20% margin) new_ymin = 10**(log_ymin - 0.08 * max(log_diff, 1)) new_ymax = 10**(log_ymax + 0.08 * max(log_diff, 1)) # Only update if values would change significantly if (ylim_max < ymax or ylim_min > ymin or ylim_max > new_ymax * 1.2 or ylim_min < new_ymin / 1.2): self.matplot_axes.set_ylim(new_ymin, new_ymax) else: # Original linear scale logic ydiff = ymax - ymin if (ylim_max < ymax or ylim_min > ymin or ylim_max > ymax + 0.2 * ydiff or ylim_min < ymin - 0.2 * ydiff): self.matplot_axes.set_ylim(ymin, ymax) xdata = in_result.get_xdata( ) chans = in_result.get_number_of_channels( ) data = [ ] colors = [ (0, 0, 0.8, 1), (0.7, 0, 0, 1), (0, 0.7, 0, 1), (0.7, 0.5, 0, 1), (0, 0, 0, 1) ] # rgba tuples: blue, red, green, yellow for i in range( chans ): data.append( in_result.get_ydata( i ) ) moving_average = data_slice = None if max_points_to_display > 0 and len( xdata ) > max_points_to_display: logger.warning("Decimating data to %d points by moving average (prevent crash of matplotlib)", max_points_to_display) n = numpy.ceil( len( xdata ) / max_points_to_display ) moving_average = numpy.ones( n, dtype="float" ) / n data_slice = numpy.array( numpy.floor( numpy.arange( max_points_to_display, dtype="float" ) \ / max_points_to_display * len( xdata ) ), dtype="int" ) xdata = xdata.take( data_slice ) # no average !? for i in range( chans ): data[ i ] = numpy.convolve( data[ i ], moving_average, "same" ).take( data_slice ) if len( self.graphen ) == 0: for i in range( chans ): self.graphen.extend( self.matplot_axes.plot( xdata, data[ i ], linestyle="-", color=colors[ i ], linewidth=2 ) ) for i in range( chans ): # initialize error bars self.graphen.extend( self.matplot_axes.plot( [ 0.0 ], [ 0.0 ], linestyle="-", color=colors[ i ], linewidth=0.5 ) ) self.graphen.extend( self.matplot_axes.plot( [ 0.0 ], [ 0.0 ], linestyle="-", color=colors[ i ], linewidth=0.5 ) ) else: for i in range( chans ): self.graphen[ i ].set_data( xdata, data[ i ] ) # Statistics activated? if (self.display_statistics_checkbutton.get_active( ) and in_result.uses_statistics( ) and in_result.ready_for_drawing_error( )): for i in range( chans ): err = in_result.get_yerr( i ) if moving_average is not None: err = numpy.convolve( err, moving_average, "same" ).take( data_slice ) self.graphen[ chans + 2 * i ].set_data( xdata, data[ i ] + err ) self.graphen[ chans + 2 * i + 1 ].set_data( xdata, data[ i ] - err ) else: for i in range( chans ): self.graphen[ chans + 2 * i ].set_data( [ 0.0 ], [ 0.0 ] ) self.graphen[ chans + 2 * i + 1 ].set_data( [ 0.0 ], [ 0.0 ] ) moving_average = data_mask = None data = None # Any title to be set? in_result_title = in_result.get_title( ) if in_result_title is not None: col = 101 while len( in_result_title ) - col > 10: in_result_title = in_result_title[ :col ] + "\n" + in_result_title[ col: ] col += 101 self.matplot_axes.set_title( in_result_title ) else: self.matplot_axes.set_title( "" ) # Any labels to be set? if in_result.get_xlabel( ) is not None: self.matplot_axes.set_xlabel( in_result.get_xlabel( ) ) else: self.matplot_axes.set_xlabel( "" ) if in_result.get_ylabel( ) is not None: self.matplot_axes.set_ylabel( in_result.get_ylabel( ) ) else: self.matplot_axes.set_ylabel( "" ) # Any variables to be drawn on plot description_lines = [] if self.display_descriptions_checkbutton.get_active(): if isinstance(in_result, Accumulation): description_lines.append("n = %d" % in_result.n) if in_result.common_descriptions is not None: description_lines.extend(["%s = %s" % (key, in_result.common_descriptions[key]) for key in in_result.common_descriptions.keys()]) elif isinstance(in_result, ADC_Result): if hasattr(in_result, "job_id") and in_result.job_id is not None: description_lines.append("job_id = %s" % str(in_result.job_id)) if hasattr(in_result, "description") and in_result.description is not None: description_lines.extend(["%s = %s" % (key, in_result.description[key]) for key in in_result.description.keys()]) if description_lines: description_string = "\n".join(description_lines) if self.description_text is None: self.description_text = self.matplot_axes.text(0.98, 0.95, description_string, size=10, transform=self.matplot_axes.transAxes, va='top', ha='right', ma='left', bbox=dict(facecolor='lightgray', alpha=0.5, boxstyle='round,pad=0.2')) else: self.description_text.set_text(description_string) elif self.description_text is not None: try: self.description_text.remove() except: pass self.description_text = None if getattr(in_result, "is_clipped", False): if self.clipping_marker is None: self.clipping_marker = self.matplot_axes.text(0.5, 0.95, "ADC OVERFLOW", transform=self.matplot_axes.transAxes, color="red", fontsize=20, fontweight="bold", ha="center", va="top", bbox=dict(facecolor='white', alpha=0.7, edgecolor='red')) elif self.clipping_marker is not None: try: self.clipping_marker.remove() except: pass self.clipping_marker = None # Draw it! self.matplot_canvas.draw_idle( ) del in_result elif isinstance( in_result, MeasurementResult ): # remove lines and error bars if self.measurementresultgraph is not None: self.matplot_axes.lines.remove( self.measurementresultgraph[ 0 ] ) # remove caps for l in self.measurementresultgraph[ 1 ]: self.matplot_axes.lines.remove( l ) # and columns for l in self.measurementresultgraph[ 2 ]: self.matplot_axes.collections.remove( l ) self.measurementresultgraph = None if self.measurementresultline is not None: self.matplot_axes.lines.remove( self.measurementresultline[ 0 ] ) self.measurementresultline = None # remove fit overlay lines for line in self.fitresultlines: try: line.remove( ) except: pass self.fitresultlines = [ ] # collect fits belonging to this measurement: # either by source attribute or by nested data pool key displayed_key = self.displayed_data[ 0 ] fit_results = [ ] for key in self.data_pool.keys( ): if key == displayed_key: continue fit = self.data_pool.get( key ) if isinstance( fit, FitResult ) and \ (fit.source == displayed_key or key.startswith( displayed_key + "/" )): fit_results.append( (key, fit) ) fit_results.sort( key=lambda key_fit: key_fit[ 0 ] ) fit_xydata = [ fit.get_lineplotdata( ) for key, fit in fit_results ] [ k, v, e ] = in_result.get_errorplotdata( ) if k.shape[ 0 ] != 0: xmin = k.min( ) ymin = (v - e).min( ) for fx, fy in fit_xydata: if fx.shape[ 0 ] != 0: xmin = min( xmin, fx.min( ) ) ymin = min( ymin, fy.min( ) ) if xmin > 0: self.display_x_scaling_combobox.set_sensitive( True ) else: # force switch to lin scale self.display_x_scaling_combobox.set_sensitive( False ) if self.display_x_scaling_combobox.get_active_text( ) != "lin": self.__rescale = True # and reset to linear self.display_x_scaling_combobox.set_active( 0 ) if ymin > 0: self.display_y_scaling_combobox.set_sensitive( True ) else: # force switch to lin scale self.display_y_scaling_combobox.set_sensitive( False ) if self.display_y_scaling_combobox.get_active_text( ) != "lin": self.__rescale = True # and reset to linear self.display_y_scaling_combobox.set_active( 0 ) x_scale = self.display_x_scaling_combobox.get_active_text( ) y_scale = self.display_y_scaling_combobox.get_active_text( ) # Initial rescaling needed? if self.__rescale: # or self.display_autoscaling_checkbutton.get_active(): xmax = k.max( ) ymax = (v + e).max( ) for fx, fy in fit_xydata: if fx.shape[ 0 ] != 0: xmax = max( xmax, fx.max( ) ) ymax = max( ymax, fy.max( ) ) # is there a range problem? if xmin == xmax or ymin == ymax: # fix range and scaling problems if xmin == xmax: if xmin != 0: (xmin, xmax) = (xmin - abs( xmin / 10.0 ), xmin + abs( xmin / 10.0 )) else: (xmin, xmax) = (-1, 1) if ymin == ymax: if ymin == 0: (ymin, ymax) = (ymin - abs( ymin / 10.0 ), ymin + abs( ymin / 10.0 )) else: (ymin, ymax) = (-1, 1) if xmin <= 0 or x_scale == "lin": self.matplot_axes.set_xscale( "linear" ) if ymin <= 0 or y_scale == "lin": self.matplot_axes.set_yscale( "linear" ) self.matplot_axes.set_xlim( xmin, xmax ) self.matplot_axes.set_ylim( ymin, ymax ) # finally decide about x log plot if x_scale == "log10" and xmin > 0: self.matplot_axes.set_xscale( "log", basex=10.0 ) self.matplot_axes.fmt_xdata = lambda x: "%g" % x #elif x_scale=="log" and xmin>0: # e scaling implementation not really useful # self.matplot_axes.set_xscale("log", basex=numpy.e) if y_scale == "log10" and ymin > 0: self.matplot_axes.set_yscale( "log", basex=10.0 ) self.matplot_axes.fmt_ydata = lambda x: "%g" % x self.__rescale = False self.measurementresultgraph = self.matplot_axes.errorbar( x=k, y=v, yerr=e, fmt="bx" ) [ k, v ] = in_result.get_lineplotdata( ) if k.shape[ 0 ] != 0 and v.shape == k.shape: self.measurementresultline = self.matplot_axes.plot( k, v, 'r-' ) # draw the fits as overlay lines fit_colors = [ "green", "purple", "darkorange" ] for i, (fx, fy) in enumerate( fit_xydata ): if fx.shape[ 0 ] != 0 and fy.shape == fx.shape: self.fitresultlines.extend( self.matplot_axes.plot( fx, fy, "-", color=fit_colors[ i % len( fit_colors ) ], linewidth=1.5 ) ) # show fit parameters description_lines = [ ] for fit_key, fit in fit_results: if fit.params: if len( fit_results ) > 1: description_lines.append( str( fit.get_title( ) or fit_key ) ) for param_key in fit.params: param_value = fit.params[ param_key ] if isinstance( param_value, float ): description_lines.append( "%s = %.4g" % (param_key, param_value) ) else: description_lines.append( "%s = %s" % (param_key, param_value) ) if description_lines: description_string = "\n".join( description_lines ) if self.description_text is None: self.description_text = self.matplot_axes.text(0.98, 0.95, description_string, size=10, transform=self.matplot_axes.transAxes, va='top', ha='right', ma='left', bbox=dict(facecolor='lightgray', alpha=0.5, boxstyle='round,pad=0.2')) else: self.description_text.set_text( description_string ) elif self.description_text is not None: try: self.description_text.remove() except: pass self.description_text = None # Any title to be set? title = in_result.get_title( ) + "" if title is not None: self.matplot_axes.set_title( title ) else: self.matplot_axes.set_title( "" ) self.matplot_canvas.draw_idle( ) del k, v, e del in_result def renew_display( self ): """ set all properties of display we are inside gtk/gdk lock """ self.clear_display( ) to_draw = self.data_pool[ self.displayed_data[ 0 ] ] if to_draw is None: return self.update_display( ) class ScriptInterface: """Executes experiment and result scripts, coordinating with the DAMARIS backend. Manages the lifecycle of experiment and result script execution. Depending on whether a backend executable is provided, it either uses :class:`BackendDriver.BackendDriver` to spawn an external process or falls back to in-process :class:`ExperimentWriter` and :class:`ResultReader` components. A shared :class:`DataPool` is used for communication between scripts and the GUI. Args: exp_script (str or None): Python source code for the experiment script. res_script (str or None): Python source code for the result script. backend_executable (str or None): Path to the DAMARIS backend executable. If ``None``, in-process execution is used. spool_dir (str): Directory for inter-process communication spool files. Defaults to ``"spool"``. clear_jobs (bool): Clear pending jobs on startup. Defaults to ``True``. clear_results (bool): Clear pending results on startup. Defaults to ``True``. log_callback (callable or None): Optional callback for log messages. Attributes: data (:class:`damaris.data.DataPool`): Shared data pool for experiment/result communication. exp_handling (:class:`ExperimentHandling.ExperimentHandling` or None): The running experiment handler thread. res_handling (:class:`ResultHandling.ResultHandling` or None): The running result handler thread. back_driver (:class:`BackendDriver.BackendDriver` or None): The backend process driver, if a backend executable was provided. Public Methods: runScripts: Start experiment, result, and backend handlers. """ def __init__( self, exp_script=None, res_script=None, backend_executable=None, spool_dir="spool", clear_jobs=True, clear_results=True, log_callback=None ): """Initialize the script execution environment. Sets up the data pool, experiment writer, result reader, and optional backend driver based on the provided configuration. Args: exp_script (str or None): Python source code for the experiment script. res_script (str or None): Python source code for the result script. backend_executable (str or None): Path to the DAMARIS backend executable. spool_dir (str): Directory for inter-process communication spool files. clear_jobs (bool): Clear pending jobs on startup. clear_results (bool): Clear pending results on startup. log_callback (callable or None): Optional callback for log messages. """ self.exp_script = exp_script self.res_script = res_script self.backend_executable = str( backend_executable ) self.spool_dir = os.path.abspath( spool_dir ) self.clear_jobs = clear_jobs self.clear_results = clear_results self.exp_handling = self.res_handling = None self._log_callback = log_callback self.exp_writer = self.res_reader = self.back_driver = None if self.backend_executable is not None and self.backend_executable != "": self.back_driver = BackendDriver.BackendDriver( self.backend_executable, spool_dir, clear_jobs, clear_results ) if self.exp_script: self.exp_writer = self.back_driver.get_exp_writer( ) if self.res_script: self.res_reader = self.back_driver.get_res_reader( ) elif self.exp_script and self.res_script: self.back_driver = None self.res_reader = ResultReader.BlockingResultReader( spool_dir, clear_jobs=self.clear_jobs, clear_results=self.clear_results ) self.exp_writer = ExperimentWriter.ExperimentWriter( spool_dir, inform_last_job=self.res_reader ) else: self.back_driver = None if self.exp_script: self.exp_writer = ExperimentWriter.ExperimentWriter( spool_dir ) if self.res_script: self.res_reader = ResultReader.ResultReader( spool_dir, clear_jobs=self.clear_jobs, clear_results=self.clear_results ) self.data = DataPool( ) def runScripts( self ): """Start experiment, result, and backend handler threads. Creates and starts :class:`ExperimentHandling` and :class:`ResultHandling` threads, and optionally starts the backend driver process. Waits for the backend to report a valid PID before proceeding. """ try: # get script engines self.exp_handling = self.res_handling = None if self.exp_script and self.exp_writer: self.exp_handling = ExperimentHandling.ExperimentHandling( self.exp_script, self.exp_writer, self.data, self._log_callback ) if self.res_script and self.res_reader: self.res_handling = ResultHandling.ResultHandling( self.res_script, self.res_reader, self.data, self._log_callback ) # start them if self.back_driver is not None: self.back_driver.start( ) while (not self.back_driver.quit_flag.is_set( ) and \ (self.back_driver.core_pid is None or self.back_driver.core_pid <= 0)): while gtk.events_pending(): gtk.main_iteration_do(False) self.back_driver.quit_flag.wait( 0.05 ) if self.exp_handling: self.exp_handling.start( ) if self.res_handling: self.res_handling.start( ) finally: self.exp_writer = self.res_reader = None def __del__( self ): self.exp_writer = None self.res_reader = None self.back_driver = None self.data = None self.exp_handling = None self.res_handling = None