class WavemeterArtiqUpdater: """ Simple loop to push new values from the wavemeter server to ARTIQ datasets. Subscribes to a list of channels and appends their name to a common prefix to determine the target dataset. :param channels: list of channels to handle entries can be of the form <n>, 'ch<n>', 'T', or 'p' :param host_artiq: host running the ARTIQ master :param port_artiq: port of the ARTIQ master's RPC server :param host_wavemeter_pub: host of the wavemeter publisher :param port_wavemeter_pub: port of the wavemeter publisher :param dataset_name_prefix: prefix for the target dataset names :param event_loop: asyncio event loop for the subscribers (defaults to asyncio.get_event_loop()) """ def __init__(self, channels: List[Any], host_artiq: str = "::1", port_artiq: int = 3251, host_wavemeter_pub: str = "::1", port_wavemeter_pub: int = 3281, dataset_name_prefix: str = "wavemeter.", event_loop: Any = None): self._rpc_client = None self._loop = asyncio.get_event_loop() if event_loop is None else event_loop self.channels = [] for ch in channels: try: # accept integer (or string lacking the "ch" prefix) as channel argument self.channels.append("ch{}".format(int(ch))) except ValueError: self.channels.append(ch) self.channels = list(set(self.channels)) # remove duplicates self.host_artiq = host_artiq self.port_artiq = port_artiq self.host_wavemeter_pub = host_wavemeter_pub self.port_wavemeter_pub = port_wavemeter_pub self._wavemeter_clients = [] self.dataset_name_prefix = dataset_name_prefix def run(self): self._rpc_client = Client(self.host_artiq, self.port_artiq, "master_dataset_db") def callback_factory(client, dataset): def callback(): self._rpc_client.set(dataset, client.value) return callback for channel in self.channels: client = WavemeterClient(channel=channel, host=self.host_wavemeter_pub, port=self.port_wavemeter_pub, event_loop=self._loop) client._new_value_callback = callback_factory(client, self.dataset_name_prefix + channel) self._wavemeter_clients.append(client) try: self._loop.run_forever() finally: self._rpc_client.close_rpc() for cl in self._wavemeter_clients: cl.close_subscriber()
class PMTControlDock(QtWidgets.QDockWidget): def __init__(self, acxn): QtWidgets.QDockWidget.__init__(self, "Manual Controls") self.setObjectName("pmt_control") self.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable | QtWidgets.QDockWidget.DockWidgetFloatable) self.pv = None self.pm = None self.bb = None self.acxn = acxn self.setup_listeners() self.dset_ctl = Client("::1", 3251, "master_dataset_db") self.scheduler = Client("::1", 3251, "master_schedule") self.dataset_db = Client("::1", 3251, "master_dataset_db") self.rid = None self.pulsed = False self.expid_continuous = { "arguments": {}, "class_name": "pmt_collect_continuously", "file": "run_continuously/run_pmt_continuously.py", "log_level": 30, "repo_rev": None, "priority": 0 } self.expid_pulsed = { "arguments": {}, "class_name": "pmt_collect_pulsed", "file": "run_continuously/run_pmt_pulsed.py", "log_level": 30, "repo_rev": None, "priority": 0 } self.expid_ttl = { "class_name": "change_ttl", "file": "misc/manual_ttl_control.py", "log_level": 30, "repo_rev": None, "priority": 1 } self.expid_dds = { "arguments": {}, "class_name": "change_cw", "file": "misc/manual_dds_control.py", "log_level": 30, "repo_rev": None, "priority": 1 } self.expid_dc = { "arguments": {}, "class_name": "set_dopplercooling_and_statereadout", "file": "misc/set_dopplercooling_and_statereadout.py", "log_level": 30, "repo_rev": None, "priority": 2 } frame = QtWidgets.QFrame() layout = QtWidgets.QVBoxLayout() pmt_frame = self.create_pmt_frame() linetrigger_frame = self.create_linetrigger_frame() dds_frame = self.create_dds_frame() picomotor_frame = self.create_picomotor_frame() layout.addWidget(pmt_frame) layout.addWidget(dds_frame) layout.addWidget(linetrigger_frame) layout.addWidget(picomotor_frame) layout.setSpacing(50) layout.setContentsMargins(0, 50, 0, 50) frame.setLayout(layout) scroll = QtWidgets.QScrollArea() scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) scroll.setWidgetResizable(False) scroll.setWidget(frame) scroll.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) scroll.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) self.setWidget(scroll) self.connect_servers() def create_pmt_frame(self): pmtLabel = boldLabel("PMT") self.onButton = QtWidgets.QPushButton("On") self.onButton.setCheckable(True) self.onButton.clicked[bool].connect(self.set_state) self.setDCButton = QtWidgets.QPushButton("set") self.setDCButton.clicked.connect(self.set_dc_and_state_readout) self.clearPMTPlotButton = QtWidgets.QPushButton("clear") self.clearPMTPlotButton.clicked.connect(self.clear_pmt_plot) self.autoLoadButton = QtWidgets.QPushButton("On") self.autoLoadButton.setCheckable(True) self.autoLoadButton.clicked[bool].connect(self.toggle_autoload) self.autoLoadSpin = customIntSpinBox(0, (0, 1000000)) self.autoLoadCurrentSpin = customSpinBox(0, (0, 10), " A") self.countDisplay = QtWidgets.QLCDNumber() self.countDisplay.setSegmentStyle(2) self.countDisplay.display(0) self.timer = QtCore.QTimer() self.timer.timeout.connect(self.updateLCD) self.timer.start(250) self.countDisplay.setStyleSheet("background-color: lightGray;" "color: green;") self.unitsLabel = QtWidgets.QLabel("kcounts / s") self.durationLabel = QtWidgets.QLabel("Duration (ms): ") self.duration = QtWidgets.QLineEdit("100") try: with labrad.connection() as cxn: p = cxn.parametervault p.set_parameter(["PmtReadout", "duration", U(100, "ms")]) except: logger.error("Failed to initially connect to labrad.") self.duration.setDisabled(True) validator = QtGui.QDoubleValidator() self.duration.setValidator(validator) self.duration.returnPressed.connect(self.duration_changed) self.duration.setStyleSheet("QLineEdit { background-color: #c4df9b}" ) self.modeLabel = QtWidgets.QLabel("Mode: ") self.setMode = customComboBox(["continuous", "pulsed"]) self.setMode.currentIndexChanged.connect(self.set_mode) layout = QtWidgets.QGridLayout() frame = QtWidgets.QFrame() frame.setFrameStyle(QtWidgets.QFrame.Panel | QtWidgets.QFrame.Sunken) frame.setLineWidth(2) frame.setMidLineWidth(3) layout.addWidget(pmtLabel, 0, 0, 1, 3) layout.addWidget(self.onButton, 1, 0) layout.addWidget(self.countDisplay, 1, 1) layout.addWidget(self.unitsLabel, 1, 2) layout.addWidget(self.durationLabel, 2, 0) layout.addWidget(self.duration, 2, 1, 1, 2) layout.addWidget(self.modeLabel, 3, 0) layout.addWidget(self.setMode, 3, 1, 1, 2) layout.addWidget(QtWidgets.QLabel("Autoload: "), 4, 0) layout.addWidget(self.autoLoadButton, 4, 1) layout.addWidget(self.autoLoadSpin, 4, 2) layout.addWidget(QtWidgets.QLabel("Current: "), 5, 0) layout.addWidget(self.autoLoadCurrentSpin, 5, 1) dcLabel = QtWidgets.QLabel("Set Doppler cooling and state readout: ") layout.addWidget(dcLabel, 6, 0, 1, 2) layout.addWidget(self.setDCButton, 6, 2) # clearLabel = QtWidgets.QLabel("Reset PMT plot: ") # layout.addWidget(clearLabel, 7, 0) # layout.addWidget(self.clearPMTPlotButton, 7, 1, 1, 2) frame.setLayout(layout) return frame def create_linetrigger_frame(self): linetriggerLabel = boldLabel("LINETRIGGER") self.linetriggerButton = QtWidgets.QPushButton("Off") self.linetriggerButton.setCheckable(True) self.linetriggerButton.setChecked(True) self.linetriggerButton.clicked[bool].connect(self.toggle_linetrigger) self.linetriggerLineEdit = QtWidgets.QLineEdit("0") self.linetriggerLineEdit.returnPressed.connect(self.linetrigger_duration_changed) layout = QtWidgets.QGridLayout() frame = QtWidgets.QFrame() frame.setFrameStyle(QtWidgets.QFrame.Panel | QtWidgets.QFrame.Sunken) frame.setLineWidth(2) frame.setMidLineWidth(3) layout.addWidget(linetriggerLabel, 0, 0, 1, 3) layout.addWidget(self.linetriggerButton, 1, 0, 1, 3) layout.addWidget(QtWidgets.QLabel("Offset duration (us): "), 2, 0) layout.addWidget(self.linetriggerLineEdit, 2, 1, 1, 2) frame.setLayout(layout) return frame def create_picomotor_frame(self): layout = QtWidgets.QGridLayout() frame = QtWidgets.QFrame() frame.setFrameStyle(QtWidgets.QFrame.Panel | QtWidgets.QFrame.Sunken) frame.setLineWidth(2) frame.setMidLineWidth(3) piezoLabel = boldLabel("PICOMOTOR") ctls = ["Local X", "Local Y", "Global X", "Global Y"] self.piezoStepSize = dict() self.piezoCurrentPos = dict() self.piezoLastPos = dict() layout.addWidget(piezoLabel, 0, 0, 1, 3) for i, ctl in enumerate(ctls): layout.addWidget(QtWidgets.QLabel(ctl + ": "), i + 1, 0) self.piezoStepSize[ctl] = customIntSpinBox(0, (0, 300)) self.piezoStepSize[ctl].setToolTip("Set step size.") self.piezoStepSize[ctl].setObjectName(ctl) self.piezoStepSize[ctl].setKeyboardTracking(False) self.piezoStepSize[ctl].valueChanged.connect(self.piezo_step_size_changed) self.piezoStepSize[ctl].setRange(0, 300) layout.addWidget(self.piezoStepSize[ctl], i + 1, 1) self.piezoCurrentPos[ctl] = QtWidgets.QSpinBox() self.piezoCurrentPos[ctl].setSingleStep(0) self.piezoCurrentPos[ctl].setToolTip("Current position.") self.piezoCurrentPos[ctl].setRange(-100000, 100000) self.piezoCurrentPos[ctl].setObjectName(str(i + 1)) self.piezoLastPos[i + 1] = 0 self.piezoCurrentPos[ctl].setKeyboardTracking(False) self.piezoCurrentPos[ctl].valueChanged.connect(self.piezo_changed) layout.addWidget(self.piezoCurrentPos[ctl], i + 1, 2) frame.setLayout(layout) return frame def create_dds_frame(self): layout = QtWidgets.QGridLayout() frame = QtWidgets.QFrame() frame.setFrameStyle(QtWidgets.QFrame.Panel | QtWidgets.QFrame.Sunken) frame.setLineWidth(2) frame.setMidLineWidth(3) ddsLabel = boldLabel("DDS Control") layout.addWidget(ddsLabel, 0, 0, 1, 2) home_dir = os.path.expanduser("~") dir_ = os.path.join(home_dir, "artiq-master/HardwareConfiguration.py") settings = run_path(dir_) dds_dict = settings["dds_config"] self.all_dds_specs = dict() self.dds_widgets = dict() try: cxn = labrad.connect() p = cxn.parametervault for (name, specs) in dds_dict.items(): try: params = p.get_parameter(["dds_cw_parameters", name]) freq, amp, state, att = params[1] except: freq = str(specs.default_freq) att = str(specs.default_att) amp = str(1.) state = str(0) p.new_parameter("dds_cw_parameters", name, ("cw_settings", [freq, amp, state, att])) self.all_dds_specs[name] = {"cpld": int(specs.urukul), "frequency": float(freq) * 1e6, "att": float(att), "state": bool(int(state)), "amplitude": float(amp)} for i, (name, specs) in enumerate(sorted(dds_dict.items())): widget = ddsControlWidget(name, specs, self.scheduler, self) layout.addWidget(widget, i // 2 + 1 , i % 2) self.dds_widgets[name] = widget frame.setLayout(layout) cxn.disconnect() except: logger.error("Failed to initially connect to labrad.") return frame def set_state(self, override=False): if override: flag = True else: flag = self.onButton.isChecked() if flag: if self.rid is None: if self.setMode.currentText() == "continuous": self.rid = self.scheduler.submit("main", self.expid_continuous, 0) else: self.rid = self.scheduler.submit("main", self.expid_pulsed, 0) self.onButton.setText("Off") else: if self.rid is None: return # Shouldn't happen else: self.scheduler.request_termination(self.rid) self.rid = None self.onButton.setText("On") def set_dc_and_state_readout(self): self.scheduler.submit("main", self.expid_dc, 2) def clear_pmt_plot(self): self.dataset_db.set("clear_pmt_plot", True) @inlineCallbacks def duration_changed(self, *args, **kwargs): # connect to parametervault here if self.pv is None: self.pv = yield self.acxn.get_server("ParameterVault") sender = self.sender() validator = sender.validator() state = validator.validate(sender.text(), 0)[0] if state == QtGui.QValidator.Acceptable: color = "#c4df9b" # green elif state == QtGui.QValidator.Intermediate: color = "#fff79a" # yellow else: color = "#f6989d" # red sender.setStyleSheet("QLineEdit { background-color: %s }" %color) try: min = 1e-3 # 1 us raw_duration = float(sender.text()) duration = raw_duration if raw_duration >= min else min yield self.pv.set_parameter(["PmtReadout", "duration", U(duration, "ms")]) a = yield self.pv.get_parameter(["PmtReadout", "duration"]) if self.rid is None: return else: self.scheduler.request_termination(self.rid) self.rid = None self.set_state(True) except ValueError: # Shouldn't happen yield print("") logger.warning("Problem trying to update duration", exc_info=True) def set_mode(self): txt = str(self.setMode.currentText()) if self.rid is None: self.pulsed = txt == "pulsed" elif txt == "continuous": if not self.pulsed: return else: self.pulsed = False self.scheduler.request_termination(self.rid) self.rid = self.scheduler.submit("main", self.expid_continuous, 0) else: if self.pulsed: return else: self.pulsed = True self.scheduler.request_termination(self.rid) self.rid = self.scheduler.submit("main", self.expid_pulsed, 0) def updateLCD(self): if not self.onButton.isChecked(): self.countDisplay.display(0) return try: raw_val = self.dset_ctl.get("pmt_counts")[-1] try: # duration in mseconds duration = float(self.duration.text()) except ValueError: # picked up a backspace or something logger.warning("Failed to update LCD", exc_info=True) return val = raw_val / duration # kcounts / second self.countDisplay.display(val) except KeyError: # dataset doesn't exist logger.info("dataset doesn't exist yet") self.countDisplay.display(0) except IndexError: # timer too fast pass @inlineCallbacks def toggle_linetrigger(self, *args): sender = self.sender() flag = sender.isChecked() if flag: sender.setText("Off") yield self.pv.set_parameter(["line_trigger_settings", "enabled", True]) else: sender.setText("On") yield self.pv.set_parameter(["line_trigger_settings", "enabled", False]) @inlineCallbacks def linetrigger_duration_changed(self, *args): value = float(self.sender().text()) yield self.pv.set_parameter(["line_trigger_settings", "offset_duration", value]) def piezo_step_size_changed(self): sender = self.sender() step = int(sender.value()) ctl = sender.objectName() self.piezoCurrentPos[ctl].setSingleStep(step) @inlineCallbacks def piezo_changed(self, *args): if self.pm is None: yield print("not connected to picomotor") sender = self.sender() piezo = int(sender.objectName()) current_pos = int(sender.value()) last_pos = self.piezoLastPos[piezo] self.piezoLastPos[piezo] = current_pos move = current_pos - last_pos yield self.pm.relative_move(piezo, move) @inlineCallbacks def toggle_autoload(self, *args): sender = self.autoLoadButton flag = sender.isChecked() if flag: try: self.check_pmt_data_length = len(self.dataset_db.get("pmt_counts")) except KeyError: sender.setChecked(False) return sender.setText("Off") self.expid_ttl.update({"arguments": {"device": "blue_PIs", "state": True}}) if not hasattr(self, "check_pmt_timer"): self.check_pmt_timer = QtCore.QTimer() self.check_pmt_timer.timeout.connect(self.check_pmt_counts) self.check_pmt_timer.start(100) yield self.bb.connect() yield self.bb.set_current(self.autoLoadCurrentSpin.value()) yield self.bb.on() else: sender.setText("On") if not hasattr(self, "check_pmt_timer"): return self.check_pmt_timer.stop() self.expid_ttl.update({"arguments": {"device": "blue_PIs", "state": False}}) yield self.bb.off() self.scheduler.submit("main", self.expid_ttl, priority=1) def check_pmt_counts(self): try: counts = self.dataset_db.get("pmt_counts")[self.check_pmt_data_length:] except KeyError: return if len(counts) == 0: return if max(counts) > int(self.autoLoadSpin.value()): self.autoLoadButton.setChecked(False) self.autoLoadButton.clicked.emit() def save_state(self): return {"ctl": {ctl: self.piezoStepSize[ctl].value() for ctl in self.piezoStepSize.keys()}, "offset": self.linetriggerLineEdit.text(), "autoload": self.autoLoadSpin.value(), "mode": self.setMode.currentText(), "ltrigger": self.linetriggerButton.isChecked(), "current": self.autoLoadCurrentSpin.value()} def restore_state(self, state): for ctl, value in state["ctl"].items(): self.piezoStepSize[ctl].setValue(value) self.piezoCurrentPos[ctl].setSingleStep(int(value)) self.linetriggerLineEdit.setText(state["offset"]) self.autoLoadSpin.setValue(int(state["autoload"])) self.setMode.setCurrentText(state["mode"]) self.linetriggerButton.setChecked(state["ltrigger"]) d = {False: "On", True: "Off"} self.linetriggerButton.setText(d[state["ltrigger"]]) self.autoLoadCurrentSpin.setValue(state["current"]) def setup_listeners(self): self.acxn.add_on_connect("ParameterVault", self.parameter_vault_connect) self.acxn.add_on_disconnect("ParameterVault", self.parameter_vault_disconnect) self.acxn.add_on_connect("picomotorserver", self.picomotor_connect) self.acxn.add_on_disconnect("picomotorserver", self.picomotor_disconnect) self.acxn.add_on_connect("barebonese3663a", self.barebones_connect) self.acxn.add_on_disconnect("barebonese3663a", self.barebones_disconnect) def parameter_vault_connect(self): self.duration.setDisabled(False) def parameter_vault_disconnect(self): self.duration.setDisabled(True) def picomotor_connect(self): for spinbox in self.piezoStepSize.values(): spinbox.setDisabled(False) for spinbox in self.piezoCurrentPos.values(): spinbox.setDisabled(False) def picomotor_disconnect(self): for spinbox in self.piezoStepSize.values(): spinbox.setDisabled(True) for spinbox in self.piezoCurrentPos.values(): spinbox.setDisabled(True) def barebones_connect(self): self.autoLoadCurrentSpin.setDisabled(False) def barebones_disconnect(self): self.autoLoadCurrentSpin.setDisabled(True) @inlineCallbacks def connect_servers(self): if self.pv is None: try: self.pv = yield self.acxn.get_server("ParameterVault") except: self.parameter_vault_disconnect() if self.pm is None: try: self.pm = yield self.acxn.get_server("picomotorserver") except: self.picomotor_disconnect() if self.bb is None: try: self.bb = yield self.acxn.get_server("barebonese3663a") except: self.barebones_disconnect()
class PMTPlot(pyqtgraph.PlotWidget): def __init__(self, args): pyqtgraph.PlotWidget.__init__(self) self.args = args self.curves = {} self.current_curve_x_start = {} self.current_curve_point_count = {} self.showGrid(x=True, y=True, alpha=0.75) self.setYRange(0, 1000) self.setXRange(0, 500) self.pens = { "with_866_on": pyqtgraph.mkPen((255, 0, 0), width=2), "with_866_off": pyqtgraph.mkPen((0, 0, 255), width=2), "diff_counts": pyqtgraph.mkPen((0, 255, 0), width=2) } self.z_values = { "with_866_on": 30, "with_866_off": 20, "diff_counts": 10 } legend = self.addLegend() legend.addItem(pyqtgraph.PlotDataItem(pen=self.pens["with_866_on"]), " 866 ON") legend.addItem(pyqtgraph.PlotDataItem(pen=self.pens["with_866_off"]), " 866 OFF") legend.addItem(pyqtgraph.PlotDataItem(pen=self.pens["diff_counts"]), " Diff") self.autoscroll = True self.setLimits(yMin=0, xMin=0) self.disableAutoRange() self.scene().sigMouseClicked.connect(self.mouse_clicked) self.getPlotItem().setClipToView(True) self.data_mgr = Client("::1", 3251, "master_dataset_db") def data_changed(self, data, mods, title): try: clear_pmt_plot = data[self.args.clear_pmt_plot][1] if clear_pmt_plot: self.clear() self.curves = dict() self.current_curve_x_start = dict() self.current_curve_point_count = dict() self.data_mgr.set("clear_pmt_plot", False) self.data_mgr.set("pmt_counts", []) self.data_mgr.set("pmt_counts_866_off", []) self.data_mgr.set("diff_counts", []) self.setYRange(0, 1000) self.setXRange(0, 500) return # don't want to plot twice except Exception as e: print(Exception) self.disableAutoRange() raw_data = {} try: raw_data["with_866_on"] = data[self.args.with_866_on][1][1:] raw_data["with_866_off"] = data[self.args.with_866_off][1][1:] raw_data["diff_counts"] = [] max_with_866_off = max(raw_data["with_866_off"], default=0) for i in range( min(len(raw_data["with_866_on"]), len(raw_data["with_866_off"]))): if max_with_866_off > 0: raw_data["diff_counts"].append(raw_data["with_866_on"][i] - raw_data["with_866_off"][i]) else: raw_data["diff_counts"].append(-1) pulsed = data[self.args.pulsed][1][0] except KeyError: return for curve_name in ["with_866_on", "diff_counts", "with_866_off"]: if not curve_name in raw_data.keys(): continue data_to_plot = raw_data[curve_name] num_points = len(data_to_plot) if not curve_name in self.curves.keys(): self.curves[curve_name] = [] if not curve_name in self.current_curve_point_count.keys(): self.current_curve_point_count[curve_name] = 0 if not curve_name in self.current_curve_x_start.keys(): self.current_curve_x_start[curve_name] = 0 if num_points == self.current_curve_point_count[curve_name]: # nothing new to plot for this curve continue if num_points < self.current_curve_point_count[curve_name]: # if this dataset has fewer points than the current curve, the dataset # must have been restarted, so we will simply begin a new curve. # update the x_start value for the new curve self.current_curve_x_start[ curve_name] += self.current_curve_point_count[curve_name] else: # we have more points, so the current curve needs to be updated. # remove the current curve so that we will recreate it. num_curves_now = len(self.curves[curve_name]) if num_curves_now > 0: latest_curve = self.curves[curve_name][-1] self.removeItem(latest_curve) self.curves[curve_name].pop(-1) num_curves_now -= 1 self.current_curve_point_count[curve_name] = num_points # for curves with all negative values, don't use any pen to plot them. # this will happen, e.g., if we are running the PMT in continuous mode # rather than in pulsed mode -- the 866_off and diff curves will not # contain valid data. pen = self.pens[curve_name] if num_points > 0 and max(data_to_plot, default=0) < 0: pen = pyqtgraph.mkPen(None) x_start = self.current_curve_x_start[curve_name] x_end = x_start + num_points x = np.arange(x_start, x_end) curve = self.plot(x, data_to_plot, pen=pen, fillLevel=0) curve.setZValue(self.z_values[curve_name]) self.curves[curve_name].append(curve) if self.autoscroll: (xmin_cur, xmax_cur), _ = self.viewRange() max_x = 0 for curve_name in self.curves.keys(): for curve in self.curves[curve_name]: localxmax = curve.dataBounds(0)[-1] try: if localxmax > max_x: max_x = localxmax except TypeError: continue window_width = xmax_cur - xmin_cur if max_x > xmin_cur + window_width: shift = (xmax_cur - xmin_cur) / 2 xmin = xmin_cur + shift xmax = xmax_cur + shift limits = [xmin, xmax] self.setXRange(*limits) self.setTitle(title) def mouse_clicked(self, ev): if ev.double(): self.autoscroll = not self.autoscroll