# -*- 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')

# import 3rd party modules
# gui graphics
from gi.repository import GObject as gobject
import sqlite3
import uuid

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 GtkSource as gtksourceview2
gobject.type_register(gtksourceview2.View)

from gi.repository import Pango as pango

# array math
import numpy

# math graphics
import matplotlib
# 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

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
# 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__


class logstream:
    gui_log = None
    text_log = sys.__stdout__

    def write( self, message ):
        # default for stdout and stderr
        if self.gui_log is not None:
            self.gui_log( message )
        if debug or self.gui_log is None:
            self.text_log.write( message )
            self.text_log.flush( )
    def __call__( self, message ):
        self.write( message )
    def flush(self):
        pass
    def __del__( self ):
        pass

global log
log = logstream( )
sys.stdout = log
sys.stderr = log

ExperimentHandling.log = log
ResultHandling.log = log
ResultReader.log = log
ExperimentWriter.log = log
BackendDriver.log = log
DataPool.log = log


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
        print("Connected to db..")

    def add_experiment(self):
        print("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):
        print("Delete experiment..")
        self.cursor.execute('DELETE FROM damaris WHERE uuid=?', (self.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)

        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"
        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
        gdk.threads_enter( )

        if self.start_immediately:
            self.toolbar_run_button.emit("clicked")
        gtk.main( )
        gdk.threads_leave( )

        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"
            "Start now:\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 ):

        # something running?
        if self.si is not None:
            print("Last Experiment is not clearly stopped!")
        self.si = None

        # get config values:
        actual_config = self.config.get( )

        # get scripts and start script interface
        self.sw.disable_editing( )
        exp_script, res_script = self.sw.get_scripts( )
        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 == ""):
            print("nothing to do...so doing nothing!")
            self.sw.enable_editing( )
            return

        # check whether scripts are syntactically valid
        # should be merged with check script function
        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
                print("Experiment Script: %s at line %d, col %d:" % (e.__class__.__name__, ln, lo))
                if e.text != "":
                    print(e.text.rstrip())
                    print(" "*(e.offset-1)+"^")
                print(e)

        res_code = None
        if res_script != "":
            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
                print("Result script: %s at line %d, col %d:" % (e.__class__.__name__, ln, lo))
                if e.text != "":
                    print("\"%s\"" % e.text)
                    # print " "*(e.offset+1)+"^" # nice idea, but needs monospaced fonts
                    pass
                print(e)

        # 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
        self.data = None
        self.monitor.observe_data_pool( self.data )

        # set the text mark for hdf logging
        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()

        self.id = self.lockfile.add_experiment()
        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

            if self.stop_experiment_flag.isSet():
                #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
            self.si = ScriptInterface( exp_code,
                                       res_code,
                                       backend,
                                       self.spool_dir,
                                       clear_jobs=actual_config[ "del_jobs_after_execution" ],
                                       clear_results=actual_config[ "del_results_after_processing" ] )

            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)
            self.start_temperature_monitoring(actual_config)
            self.si.runScripts( )
        except Exception as e:
            #print "ToDo evaluate exception",str(e), "at",traceback.extract_tb(sys.exc_info()[2])[-1][1:3]
            #print "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 )
            print("Error while executing scripts: %s\n" % 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( )
                print("waiting for threads stoping...", end=' ')
                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 )
                print("done")

            # 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 )
        
        gobject.timeout_add( 200, self.observe_running_experiment )

    def observe_running_experiment( self ):
        """
        periodically look at running threads
        """
        # 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:
                    print("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))
                    print("Full traceback", self.si.exp_handling.traceback)
                    e_text = "Experiment Script Failed (%d)" % e
                else:
                    e_text = "Experiment Script Finished (%d)" % e
                    print("experiment script finished")
                self.si.exp_handling = None
            else:
                #print self.si.back_driver.get_messages()
                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:
                    print("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))
                    print("Full traceback", 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.stdout.write( "." )
                self.dump_dots += 1
                if self.dump_dots > 80:
                    print()
                    self.dump_dots = 0
            else:
                self.dump_thread.join( )
                self.dump_thread = None
                dump_size = os.stat( self.dump_filename ).st_size / 1e6
                print("done (%.1f s, %.1f MB)" % (time.time( ) - self.dump_start_time, dump_size))

        gdk.threads_enter( )
        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 )
        gdk.threads_leave( )

        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 != "":
                print("all subprocesses ended, saving data pool")
                # thread to save data...
                self.save_thread = threading.Thread( target=self.dump_states, 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:
            gdk.threads_enter( )
            self.toolbar_pause_button.set_sensitive( False )
            self.toolbar_stop_button.set_sensitive( False )
            gdk.threads_leave( )
            if len( still_running ) != 0:
                print("subprocess(es) still running: " + ', '.join( [s.getName( ) for s in still_running] ))
                return True
            else:
                if self.save_thread is not None:
                    if self.save_thread.is_alive( ):
                        sys.stdout.write( "." )
                        self.dump_dots += 1
                        if self.dump_dots > 80:
                            print()
                            self.dump_dots = 0
                        return True
                    self.save_thread.join( )
                    self.save_thread = None
                    dump_size = os.stat( self.dump_filename ).st_size / 1e6
                    print("done (%.1f s, %.1f MB)" % (time.time( ) - self.dump_start_time, dump_size))

            # now everything is stopped
            self.state = DamarisGUI.Edit_State
            gdk.threads_enter( )
            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 )
            gdk.threads_leave( )

            # 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)
            return False

        # dump states?
        if self.dump_thread is None and self.dump_filename != "" and \
                        self.dump_timeinterval != 0 and self.last_dumped + self.dump_timeinterval < time.time( ):
            print("dumping data pool")
            self.dump_start_time = time.time( )
            self.dump_dots = 0
            # thread to save data...
            self.dump_thread = threading.Thread( target=self.dump_states, name="dump states" )
            self.dump_thread.start( )

        # or look at them again
        return True

    def dump_states( self, init=False ):
        """
        init: constructs basic structure of this file
        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:
                print("invalid formating character in '" + actual_config.get( "data_pool_name" ) + "'")
                self.dump_filename = "DAMARIS_data_pool.h5"
                print("reseting dumpfile to " + self.dump_filename)
            del extensions_dict

            if os.path.split( self.dump_filename )[ 1 ] == "" or os.path.isdir( self.dump_filename ):
                print("The 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:
                print("configuration provides non-number for dump interval: " + 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):
                    print("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
        if not os.path.isfile( self.dump_filename ):
            if not init:
                print("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:
                    print(e)
                    print("could not create dump directory '%s', so hdf5 dumps disabled" % dir_trunk)
                    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!
            print("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!
        self.data.write_hdf5( dump_file, where="/", name="data_pool",
                              complib=self.dump_complib, complevel=self.dump_complevel,
                              incremental=not init )

        # now save additional information
        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?
        logtextbuffer = self.log.textbuffer
        last_end = logtextbuffer.get_mark( "lastdumped" )
        if last_end is None:
            last_end = logtextbuffer.create_mark( "lastdumped", logtextbuffer.get_start_iter( ), left_gravity=True )
        #logtext_start = logtextbuffer.get_iter_at_mark( last_end )
        logtext_end = logtextbuffer.get_end_iter( )
        logtextbuffer.move_mark( last_end, logtext_end )
        # Create new iterators after the mark move
        logtext_start = logtextbuffer.get_iter_at_mark( last_end )
        logtext_end = logtextbuffer.get_end_iter( )
        # recode from unicode
        logtext = logtextbuffer.get_text( logtext_start, logtext_end, True )
        # avoid circular references (seems to be necessary with gtk-2.12)
        # del logtextbuffer, logtext_start, logtext_end, last_end
        for l in logtext.splitlines( ):
            dump_file.root.log.append( numpy.array( [ l ], dtype="S120" ) )

        dump_file.flush( )
        dump_file.close( )
        self.last_dumped = time.time( )
        del dump_file

        return True

    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:
                    print("Temperature monitoring: No device configured for Eurotherm")
                    return
                
                self.temp_controller = create_controller(
                    "eurotherm", temp_device, baudrate=temp_baudrate
                )
                print(f"Temperature monitoring started with Eurotherm on {temp_device} (interval: {temp_interval}s)")
                
            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")
                print(f"Temperature monitoring started with simulated controller (interval: {temp_interval}s)")
                
            else:
                print(f"Temperature monitoring: Unknown controller type: {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:
            print(f"Failed to start temperature monitoring: {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()
                print("Temperature monitoring stopped")
            except Exception as e:
                print(f"Error stopping temperature monitoring: {e}")
            finally:
                self.temp_monitor = None
        
        if self.temp_controller is not None:
            try:
                self.temp_controller.close()
            except Exception as e:
                print(f"Error closing temperature controller: {e}")
            finally:
                self.temp_controller = None

    def documentation_init( self ):

        self.doc_urls = {
            "DAMARIS Manual": "file:///usr/share/doc/python3-damaris/doc/_build/html/index.html",
        }

        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_index_url = None
        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 = "Python DAMARIS"

        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:
                    print("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:
            print("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:
                print("starting web browser (module webbrowser)")
                self.my_webbrowser.open( self.start_url )
                return True
        # last resort
        print("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:
    """
    writes messages to the log window
    """

    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)
        font_desc = pango.FontDescription("Monospace 10")
        self.textview.modify_font(font_desc)
        self.textbuffer = self.textview.get_buffer( )
        self.logstream = log
        self.logstream.gui_log = self
        self.last_timetag = None
        self( "Started in directory %s\n" % os.getcwd( ) )

    def __call__( self, message ):
        timetag = time.time( )
        gobject.idle_add( self.add_message_callback, timetag, message, priority=gobject.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
        gdk.threads_enter( )
        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)
        gdk.threads_leave( )

    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 ):
        self.logstream.gui_log = None
        self.logstream = None


class ScriptWidgets:
    def __init__( self, xml_gui, outer_space ):
        """
        initialize text widgets with text
        """
        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 = gtksourceview2.Buffer()
        self.data_handling_textbuffer = gtksourceview2.Buffer()

        lm = gtksourceview2.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)

        fontdesc = pango.FontDescription("monospace")

        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)
        self.experiment_script_textview.modify_font(fontdesc)
        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)
        self.data_handling_textview.modify_font(fontdesc)

        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 = gtksourceview2.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 = gtksourceview2.CompletionWords.new("Keywords", None)
        self.compwgen.register(self.keywordsbuffer)
        self.compwgen.set_property("minimum-word-size", 2)

        self.compwexp = gtksourceview2.CompletionWords.new("Words", None)
        self.compwexp.register(self.experiment_script_textbuffer)
        self.compwexp.set_property("minimum-word-size", 2)

        self.compwres = gtksourceview2.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
            print("Syntax Error:\n%s in %s at line %d, offset %d" % (
            str( se ), se.filename, ln, lo) + "\n(ToDo: Dialog)")
            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:
            print("Compilation Error:\n" + str( e ) + "\n(ToDo: Dialog)")

    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:
            print("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 gtksourceview2.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,
                                        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 )
        # 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,
                                        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 )

        # 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,
                                        action=gtk.FileChooserAction.SAVE,
                                        buttons=("_Save", 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( )

        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.HBox(homogeneous=False, 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.HBox(homogeneous=False, 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.HBox(homogeneous=False, 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.HBox(homogeneous=False, 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.HBox(homogeneous=False, 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.HBox(homogeneous=False, 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:
                        print("cannot append compression lib name to %s" % model.__class__.__name__)


        # debug message
        if debug:
            print("DAMARIS", __version__)
            print(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:
                    print("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( )
            iter = model.get_iter_first( )
            while iter is not None:
                if model.get( iter, 0 )[ 0 ] == config[ "data_pool_complib" ]:
                    self.config_data_pool_complib.set_active_iter( iter )
                    break
                iter = model.iter_next( iter )
            # if this compression method is not supported, warn and do nothing
            if iter is None:
                print("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,
            buttons=(
                gtk.STOCK_CANCEL, gtk.ResponseType.CANCEL,
                gtk.STOCK_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)):
            iter = model.iter_nth_child(None, i)
            if iter and model.get(iter, 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:
            experiment_script_textview.modify_font( pango.FontDescription( font ) )
        data_handling_textview = self.xml_gui.get_object( "data_handling_textview" )
        if data_handling_textview:
            data_handling_textview.modify_font( pango.FontDescription( 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:
                print("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 )

        print("save config to: "+filename)

        configfile = open( filename, "w" )
        configfile.write( "<?xml version='1.0'?>\n" )
        configfile.write( "<damaris>\n" )
        for k, v in config.items( ):
            if k in self.config_from_system \
                    and self.config_from_system[ k ] == v:
                if debug:
                    print("Ignoring for write, because system value for %r is %r equal to %r" % \
                          (k, self.config_from_system[ k ], v))
                continue
            print(k,v, type(v))
            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( "  <config key='%s' type='%s'>%s</config>\n" % (k, typename, val) )
        configfile.write( "</damaris>\n" )


class MonitorWidgets:
    def __init__( self, xml_gui ):
        """
        initialize matplotlib widgets and stuff around
        """

        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 = FigureCanvas( 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='')
        try:
            icon_theme = gtk.IconTheme.get_default()
            icon = icon_theme.load_icon("crosshairs", 24, 0)
            image = gtk.Image.new_from_pixbuf(icon)
            self.matplot_cursor_button.set_image(image)
        except:
            pass
        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'):
                self.matplot_toolbar.message.modify_font(pango.FontDescription("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.clipping_marker = None

    def source_list_reset( self ):
        self.display_source_treestore.clear( )
        self.source_list_add( 'None' )
        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, iter, what ):
        """find node in subcategory"""
        while iter is not None:
            if str( model.get( iter, 0 )[ 0 ] ) == what:
                return iter
            iter = model.iter_next( iter )
        return iter

    def source_list_find( self, namelist ):
        """
        namelist sequence of names, e.g. ["a", "b", "c" ] for "a/b/c"
        """
        model = self.display_source_treestore
        retval = None
        #iter = model.get_iter_root( )
        iter = model.get_iter_first( )
        while iter is not None and len( namelist ) > 0:
            name = namelist[ 0 ]
            iter = self.source_list_find_one( model, iter, name )
            if iter is not None:
                retval = iter
                namelist.pop( 0 )
                iter = 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
        for rest_name in namelist:
            # append() returns iter for the new row
            parent = self.display_source_treestore.append( parent, [ rest_name ] )

    def source_list_remove( self, source_name ):
        namelist = source_name.split( "/" )
        pwd = namelist[ : ]
        iter = self.source_list_find( namelist )
        if iter is None or len( namelist ) > 0:
            print("source_list_remove: WARNING: Not found")
            return
        model = self.display_source_treestore
        if model.iter_has_child( iter ):
            print("source_list_remove: WARNING: Request to delete a tree")
            return
        while True:
            parent = model.iter_parent( iter )
            model.remove( iter )
            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
            iter = 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
        """
        gdk.threads_enter( )
        try:
            if self.data_pool is not None:
                # maybe some extra cleanup needed
                print("ToDo: cleanup widgets")
                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.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
                for key in sorted(data_pool.keys()):
                    if key != "__builtins__":  # Skip internal keys
                        self.source_list_add(key)
                
                # Select first data item if available
                keys = [k for k in data_pool.keys() 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)
        finally:
            gdk.threads_leave( )

    #################### 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:
            print("negative event count!", self.update_counter)
        if self.update_counter > 5:
            if debug:
                print("sleeping to find time for grapics updates")
            threading.Event( ).wait( 0.05 )
            while self.update_counter > 15:
                threading.Event( ).wait( 0.05 )
        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( )
            gobject.idle_add( self.datapool_idle_listener, event, priority=gobject.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()
                gobject.idle_add( self.update_display_idle_event, self.displayed_data[ 0 ][ : ],
                                  priority=gobject.PRIORITY_DEFAULT_IDLE )
            else:
                self.update_counter_lock.acquire( )
                self.update_counter += 1
                self.update_counter_lock.release( )
                gobject.idle_add( self.datapool_idle_listener, event, priority=gobject.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:
            print("negative event count!", self.update_counter)
        if self.update_counter > 5:
            if debug:
                print("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( )
        gobject.idle_add( self.update_display_idle_event, self.displayed_data[ 0 ][ : ],
                          priority=gobject.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:
                        print("update queue too long (%d>10): skipping one update" % self.update_counter)
                    else:
                        gdk.threads_enter( )
                        try:
                            self.update_display( )
                        finally:
                            gdk.threads_leave( )
                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:
                        print("update queue too long (%d>10): skipping one update" % self.update_counter)
                    else:
                        gdk.threads_enter( )
                        try:
                            self.renew_display( )
                        finally:
                            gdk.threads_leave( )
                new_data_struct = None
        elif event.what == DataPool.Event.new_key:
            # update combo-box by inserting and rely on consistent information
            gdk.threads_enter( )
            self.source_list_add( event.subject )
            gdk.threads_leave( )
        elif event.what == DataPool.Event.deleted_key:
            # update combo-box by removing and rely on consistent information
            gdk.threads_enter( )
            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 )
            gdk.threads_leave( )
        elif event.what == DataPool.Event.destroy:
            gdk.threads_enter( )
            self.source_list_reset( )
            self.displayed_data = [ None, None ]
            self.clear_display( )
            gdk.threads_leave( )
        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:
            print("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
        gdk.threads_enter( )
        try:
            self.update_display( )
        finally:
            gdk.threads_leave( )


    ######################## 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 )
            gobject.idle_add( self.datapool_idle_listener, event, priority=gobject.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 = matplotlib.widgets.Cursor(self.matplot_axes,
                                                                useblit=False, 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(None, None, None, None, event.button, event.time)

    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, "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
        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:
                print("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

            [ k, v, e ] = in_result.get_errorplotdata( )
            if k.shape[ 0 ] != 0:
                xmin = k.min( )
                ymin = (v - e).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( )
                    # 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-' )


            # 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:
    """
    texts or code objects are executed as experiment and result script the backend is started with sufficient arguments
    """

    def __init__( self, exp_script=None, res_script=None, backend_executable=None, spool_dir="spool", clear_jobs=True,
                  clear_results=True ):
        """
        run experiment scripts and result scripts
        """

        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.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 ):
        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 )
            if self.res_script and self.res_reader:
                self.res_handling = ResultHandling.ResultHandling( self.res_script, self.res_reader, self.data )

            # start them
            if self.back_driver is not None:
                self.back_driver.start( )
                while (not self.back_driver.quit_flag.isSet( ) 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.1 )
            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

