replace gtk2/PyGTK and Python 2 idioms in DamarisGUI.py

- raise "RuntimeError" -> raise RuntimeError(...)
- gobject.timeout_add/idle_add/PRIORITY_* -> GLib equivalents
  (PyGIDeprecationWarning)
- remove no-op gobject.type_register(GtkSource.View)
- gtk.HBox -> gtk.Box(orientation=HORIZONTAL) (deprecated since 3.2)
- menu.popup(6-arg) -> menu.popup_at_pointer (deprecated since 3.22)
- Thread.getName() -> Thread.name (deprecated since 3.10)
- rename gtksourceview2 alias to gtksourceview
- rename bare iter locals/params to treeiter to avoid shadowing
  the builtin

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
markusro committed 2026-10-18 16:24:59 +02:00
1 parent 6d46e8ca1a
commit f18bbdf48c
1 file changed
+65 -65
+65 -65
View File
@@ -24,6 +24,7 @@ from gi.repository import GObject as gobject
import sqlite3 import sqlite3
import uuid import uuid
from gi.repository import GLib as glib
from gi.repository import Gtk as gtk from gi.repository import Gtk as gtk
gtk_version_missmatch = gtk.check_version( 3, 0, 0 ) gtk_version_missmatch = gtk.check_version( 3, 0, 0 )
@@ -32,8 +33,7 @@ if gtk_version_missmatch:
from gi.repository import Gdk as gdk from gi.repository import Gdk as gdk
from gi.repository import GtkSource as gtksourceview2 from gi.repository import GtkSource as gtksourceview
gobject.type_register(gtksourceview2.View)
from gi.repository import Pango as pango from gi.repository import Pango as pango
@@ -208,7 +208,7 @@ class DamarisGUI:
if exp_script and res_script: if exp_script and res_script:
self.start_immediately = start_immediately self.start_immediately = start_immediately
else: else:
raise "RuntimeError" raise RuntimeError("start_immediately requires both experiment and result script")
else: else:
self.start_immediately = False self.start_immediately = False
self.statusbar_init( ) self.statusbar_init( )
@@ -740,7 +740,7 @@ class DamarisGUI:
if actual_config[ "data_pool_name" ] != "": if actual_config[ "data_pool_name" ] != "":
self.dump_states( init=True, logtext=self.log.snapshot_since_last_dump( ) ) self.dump_states( init=True, logtext=self.log.snapshot_since_last_dump( ) )
gobject.timeout_add( 200, self.observe_running_experiment ) glib.timeout_add( 200, self.observe_running_experiment )
def observe_running_experiment( self ): def observe_running_experiment( self ):
""" """
@@ -875,7 +875,7 @@ class DamarisGUI:
self.toolbar_pause_button.set_sensitive( False ) self.toolbar_pause_button.set_sensitive( False )
self.toolbar_stop_button.set_sensitive( False ) self.toolbar_stop_button.set_sensitive( False )
if len( still_running ) != 0: if len( still_running ) != 0:
logger.warning("Subprocess(es) still running: %s", ', '.join( [s.getName( ) for s in still_running] )) logger.warning("Subprocess(es) still running: %s", ', '.join( [s.name for s in still_running] ))
return True return True
else: else:
if self.save_thread is not None: if self.save_thread is not None:
@@ -1323,7 +1323,7 @@ class LogWindow:
"""Manages the application log window. """Manages the application log window.
Provides a timestamped, scrollable text view for application messages. Provides a timestamped, scrollable text view for application messages.
Messages are dispatched via ``gobject.idle_add`` to keep the GTK main Messages are dispatched via ``glib.idle_add`` to keep the GTK main
loop responsive. loop responsive.
Args: Args:
@@ -1353,7 +1353,7 @@ class LogWindow:
def __call__( self, message ): def __call__( self, message ):
timetag = time.time( ) timetag = time.time( )
gobject.idle_add( self.add_message_callback, timetag, message, priority=gobject.PRIORITY_LOW ) glib.idle_add( self.add_message_callback, timetag, message, priority=glib.PRIORITY_LOW )
def add_message_callback( self, timetag, message ): def add_message_callback( self, timetag, message ):
date_tag = "" date_tag = ""
@@ -1418,9 +1418,9 @@ class ScriptWidgets:
experiment script file. experiment script file.
res_script_filename (str or None): Path to the currently loaded res_script_filename (str or None): Path to the currently loaded
result script file. result script file.
experiment_script_textview (:class:`gtksourceview2.View`): Text view experiment_script_textview (:class:`gtksourceview.View`): Text view
for the experiment script. for the experiment script.
data_handling_textview (:class:`gtksourceview2.View`): Text view for data_handling_textview (:class:`gtksourceview.View`): Text view for
the result script. the result script.
Public Methods: Public Methods:
@@ -1441,7 +1441,7 @@ class ScriptWidgets:
def __init__( self, xml_gui, outer_space ): def __init__( self, xml_gui, outer_space ):
"""Initialize text widgets with syntax highlighting and completion. """Initialize text widgets with syntax highlighting and completion.
Sets up two :class:`gtksourceview2.View` instances with Python Sets up two :class:`gtksourceview.View` instances with Python
syntax highlighting, keyword auto-completion, line numbers, and syntax highlighting, keyword auto-completion, line numbers, and
smart indentation. Connects text buffer signals to toolbar and smart indentation. Connects text buffer signals to toolbar and
status bar update handlers. status bar update handlers.
@@ -1468,10 +1468,10 @@ class ScriptWidgets:
self.data_handling_textview = self.xml_gui.get_object( "data_handling_textview" ) self.data_handling_textview = self.xml_gui.get_object( "data_handling_textview" )
# create and set syntax-highlighting text-buffers as textview backends # create and set syntax-highlighting text-buffers as textview backends
self.experiment_script_textbuffer = gtksourceview2.Buffer() self.experiment_script_textbuffer = gtksourceview.Buffer()
self.data_handling_textbuffer = gtksourceview2.Buffer() self.data_handling_textbuffer = gtksourceview.Buffer()
lm = gtksourceview2.LanguageManager() lm = gtksourceview.LanguageManager()
langpython = lm.get_language("python") langpython = lm.get_language("python")
self.experiment_script_textbuffer.set_language(langpython) self.experiment_script_textbuffer.set_language(langpython)
@@ -1505,7 +1505,7 @@ set_pfg set_dac set_phase set_pts_local get_length set_title get_title add_linep
add_errorplotdata get_number_of_channels get_style set_style get_xlabel set_xlabel add_errorplotdata get_number_of_channels get_style set_style get_xlabel set_xlabel
get_ylabel set_ylabel""" get_ylabel set_ylabel"""
self.keywordsbuffer = gtksourceview2.Buffer() self.keywordsbuffer = gtksourceview.Buffer()
self.keywordsbuffer.set_text(keywords) self.keywordsbuffer.set_text(keywords)
compexp = self.experiment_script_textview.get_completion() compexp = self.experiment_script_textview.get_completion()
@@ -1514,15 +1514,15 @@ get_ylabel set_ylabel"""
compexp.set_property("show-headers", True) compexp.set_property("show-headers", True)
compres.set_property("show-headers", True) compres.set_property("show-headers", True)
self.compwgen = gtksourceview2.CompletionWords.new("Keywords", None) self.compwgen = gtksourceview.CompletionWords.new("Keywords", None)
self.compwgen.register(self.keywordsbuffer) self.compwgen.register(self.keywordsbuffer)
self.compwgen.set_property("minimum-word-size", 2) self.compwgen.set_property("minimum-word-size", 2)
self.compwexp = gtksourceview2.CompletionWords.new("Words", None) self.compwexp = gtksourceview.CompletionWords.new("Words", None)
self.compwexp.register(self.experiment_script_textbuffer) self.compwexp.register(self.experiment_script_textbuffer)
self.compwexp.set_property("minimum-word-size", 2) self.compwexp.set_property("minimum-word-size", 2)
self.compwres = gtksourceview2.CompletionWords.new("Words", None) self.compwres = gtksourceview.CompletionWords.new("Words", None)
self.compwres.register(self.data_handling_textbuffer) self.compwres.register(self.data_handling_textbuffer)
self.compwres.set_property("minimum-word-size", 2) self.compwres.set_property("minimum-word-size", 2)
@@ -1754,7 +1754,7 @@ get_ylabel set_ylabel"""
def textviews_modified( self, widget = None, event=None ): def textviews_modified( self, widget = None, event=None ):
# mix into toolbar affairs # mix into toolbar affairs
if type(widget) is gtksourceview2.View: if type(widget) is gtksourceview.View:
return self.textviews_moved( widget ) return self.textviews_moved( widget )
else: else:
return False return False
@@ -2395,7 +2395,7 @@ class ConfigTab:
self.config_data_pool_write_interval_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 # ADC bit depth setting
self.config_adc_bit_depth_hbox = gtk.HBox(homogeneous=False, spacing=6) self.config_adc_bit_depth_hbox = gtk.Box(orientation=gtk.Orientation.HORIZONTAL, spacing=6)
self.config_adc_bit_depth_label = gtk.Label(label="ADC bit depth:") self.config_adc_bit_depth_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 = 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_value(ADC_Result.default_bit_depth)
@@ -2410,7 +2410,7 @@ class ConfigTab:
self.config_adc_bit_depth_hbox.show_all() self.config_adc_bit_depth_hbox.show_all()
# Temperature monitoring settings - single line layout # Temperature monitoring settings - single line layout
self.config_temp_hbox = gtk.HBox(homogeneous=False, spacing=6) self.config_temp_hbox = gtk.Box(orientation=gtk.Orientation.HORIZONTAL, spacing=6)
# Controller type selector # Controller type selector
self.config_temp_controller_type_label = gtk.Label(label="Controller:") self.config_temp_controller_type_label = gtk.Label(label="Controller:")
@@ -2421,7 +2421,7 @@ class ConfigTab:
self.config_temp_controller_type_combobox.set_tooltip_text("Select temperature controller type") 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_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 = gtk.Box(orientation=gtk.Orientation.HORIZONTAL, spacing=0)
self.config_temp_controller_type_hbox.pack_start(self.config_temp_controller_type_label, False, False, 0) self.config_temp_controller_type_hbox.pack_start(self.config_temp_controller_type_label, False, False, 0)
self.config_temp_controller_type_hbox.pack_start(self.config_temp_controller_type_combobox, 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) self.config_temp_hbox.pack_start(self.config_temp_controller_type_hbox, False, False, 0)
@@ -2440,7 +2440,7 @@ class ConfigTab:
self.config_temp_device_button.set_tooltip_text("Browse for temperature controller device") 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_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 = gtk.Box(orientation=gtk.Orientation.HORIZONTAL, spacing=0)
self.config_temp_device_hbox.pack_start(self.config_temp_device_entry, True, True, 0) self.config_temp_device_hbox.pack_start(self.config_temp_device_entry, True, True, 0)
self.config_temp_device_hbox.pack_start(self.config_temp_device_button, False, False, 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) self.config_temp_hbox.pack_start(self.config_temp_device_hbox, True, True, 0)
@@ -2452,7 +2452,7 @@ class ConfigTab:
self.config_temp_interval_spinbutton.set_tooltip_text("Time between temperature readings in 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_spinbutton.connect("key-press-event", self.config_keypress)
self.config_temp_interval_hbox = gtk.HBox(homogeneous=False, spacing=6) self.config_temp_interval_hbox = gtk.Box(orientation=gtk.Orientation.HORIZONTAL, spacing=6)
self.config_temp_interval_hbox.pack_start(self.config_temp_interval_label, False, False, 0) self.config_temp_interval_hbox.pack_start(self.config_temp_interval_label, False, False, 0)
self.config_temp_interval_hbox.pack_start(self.config_temp_interval_spinbutton, 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) self.config_temp_hbox.pack_start(self.config_temp_interval_hbox, False, False, 0)
@@ -2466,7 +2466,7 @@ class ConfigTab:
self.config_temp_baudrate_combobox.append_text("115200") self.config_temp_baudrate_combobox.append_text("115200")
self.config_temp_baudrate_combobox.set_active(0) 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_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 = gtk.Box(orientation=gtk.Orientation.HORIZONTAL, spacing=6)
self.config_temp_baudrate_hbox.pack_start(self.config_temp_baudrate_combobox, False, False, 0) self.config_temp_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) self.config_temp_hbox.pack_start(self.config_temp_baudrate_hbox, False, False, 0)
# Note: baud rate is shown for eurotherm, hidden for simulated # Note: baud rate is shown for eurotherm, hidden for simulated
@@ -2675,14 +2675,14 @@ pygobject version %(pygobject)s
if "data_pool_complib" in config: if "data_pool_complib" in config:
# find combo-box entry and make it active... # find combo-box entry and make it active...
model = self.config_data_pool_complib.get_model( ) model = self.config_data_pool_complib.get_model( )
iter = model.get_iter_first( ) treeiter = model.get_iter_first( )
while iter is not None: while treeiter is not None:
if model.get( iter, 0 )[ 0 ] == config[ "data_pool_complib" ]: if model.get( treeiter, 0 )[ 0 ] == config[ "data_pool_complib" ]:
self.config_data_pool_complib.set_active_iter( iter ) self.config_data_pool_complib.set_active_iter( treeiter )
break break
iter = model.iter_next( iter ) treeiter = model.iter_next( treeiter )
# if this compression method is not supported, warn and do nothing # if this compression method is not supported, warn and do nothing
if iter is None: if treeiter is None:
logger.warning("Compression method %s is not supported", config[ "data_pool_complib" ]) logger.warning("Compression method %s is not supported", config[ "data_pool_complib" ])
def on_adc_bit_depth_changed(self, widget): def on_adc_bit_depth_changed(self, widget):
@@ -2759,8 +2759,8 @@ pygobject version %(pygobject)s
if model is None: if model is None:
return return
for i in range(model.iter_n_children(None)): for i in range(model.iter_n_children(None)):
iter = model.iter_nth_child(None, i) treeiter = model.iter_nth_child(None, i)
if iter and model.get(iter, 0)[0] == text: if treeiter and model.get(treeiter, 0)[0] == text:
combobox.set_active(i) combobox.set_active(i)
return return
@@ -3113,13 +3113,13 @@ class MonitorWidgets:
if none_iter is not None: if none_iter is not None:
self.display_source_combobox.set_active_iter( none_iter ) self.display_source_combobox.set_active_iter( none_iter )
def source_list_find_one( self, model, iter, what ): def source_list_find_one( self, model, treeiter, what ):
"""find node in subcategory""" """find node in subcategory"""
while iter is not None: while treeiter is not None:
if str( model.get( iter, 0 )[ 0 ] ) == what: if str( model.get( treeiter, 0 )[ 0 ] ) == what:
return iter return treeiter
iter = model.iter_next( iter ) treeiter = model.iter_next( treeiter )
return iter return treeiter
def source_list_find( self, namelist ): def source_list_find( self, namelist ):
""" """
@@ -3146,35 +3146,35 @@ class MonitorWidgets:
del namelist[:i] del namelist[:i]
# Continue search from here # Continue search from here
retval = found_iter retval = found_iter
iter = self.display_source_treestore.iter_children( retval ) treeiter = self.display_source_treestore.iter_children( retval )
while iter is not None and len( namelist ) > 0: while treeiter is not None and len( namelist ) > 0:
name = namelist[ 0 ] name = namelist[ 0 ]
iter = self.source_list_find_one( self.display_source_treestore, iter, name ) treeiter = self.source_list_find_one( self.display_source_treestore, treeiter, name )
if iter is not None: if treeiter is not None:
retval = iter retval = treeiter
namelist.pop( 0 ) namelist.pop( 0 )
# Update cache # Update cache
partial_path += "/" + name partial_path += "/" + name
self.source_list_cache[partial_path] = retval self.source_list_cache[partial_path] = retval
iter = self.display_source_treestore.iter_children( retval ) treeiter = self.display_source_treestore.iter_children( retval )
return retval return retval
else: else:
del self.source_list_cache[partial_path] del self.source_list_cache[partial_path]
model = self.display_source_treestore model = self.display_source_treestore
retval = None retval = None
#iter = model.get_iter_root( ) #treeiter = model.get_iter_root( )
iter = model.get_iter_first( ) treeiter = model.get_iter_first( )
current_path = [] current_path = []
while iter is not None and len( namelist ) > 0: while treeiter is not None and len( namelist ) > 0:
name = namelist[ 0 ] name = namelist[ 0 ]
iter = self.source_list_find_one( model, iter, name ) treeiter = self.source_list_find_one( model, treeiter, name )
if iter is not None: if treeiter is not None:
retval = iter retval = treeiter
namelist.pop( 0 ) namelist.pop( 0 )
current_path.append(name) current_path.append(name)
self.source_list_cache["/".join(current_path)] = retval self.source_list_cache["/".join(current_path)] = retval
iter = model.iter_children( retval ) treeiter = model.iter_children( retval )
return retval return retval
def source_list_add( self, source_name, parent=None ): def source_list_add( self, source_name, parent=None ):
@@ -3201,17 +3201,17 @@ class MonitorWidgets:
def source_list_remove( self, source_name ): def source_list_remove( self, source_name ):
namelist = source_name.split( "/" ) namelist = source_name.split( "/" )
pwd = namelist[ : ] pwd = namelist[ : ]
iter = self.source_list_find( namelist ) treeiter = self.source_list_find( namelist )
if iter is None or len( namelist ) > 0: if treeiter is None or len( namelist ) > 0:
logger.warning("source_list_remove: Not found") logger.warning("source_list_remove: Not found")
return return
model = self.display_source_treestore model = self.display_source_treestore
if model.iter_has_child( iter ): if model.iter_has_child( treeiter ):
logger.warning("source_list_remove: Request to delete a tree") logger.warning("source_list_remove: Request to delete a tree")
return return
while True: while True:
parent = model.iter_parent( iter ) parent = model.iter_parent( treeiter )
model.remove( iter ) model.remove( treeiter )
pwd.pop( ) pwd.pop( )
# We now test, if we want to remove parent too # We now test, if we want to remove parent too
if parent is None: if parent is None:
@@ -3222,7 +3222,7 @@ class MonitorWidgets:
if "/".join( pwd ) in self.data_pool: if "/".join( pwd ) in self.data_pool:
# The parent has data connected to it # The parent has data connected to it
break break
iter = parent treeiter = parent
def source_list_current( self ): def source_list_current( self ):
ai = self.display_source_combobox.get_active_iter( ) ai = self.display_source_combobox.get_active_iter( )
@@ -3304,7 +3304,7 @@ class MonitorWidgets:
self.update_counter_lock.acquire( ) self.update_counter_lock.acquire( )
self.update_counter += 1 self.update_counter += 1
self.update_counter_lock.release( ) self.update_counter_lock.release( )
gobject.idle_add( self.datapool_idle_listener, event, priority=gobject.PRIORITY_DEFAULT_IDLE ) glib.idle_add(self.datapool_idle_listener, event, priority=glib.PRIORITY_DEFAULT_IDLE )
else: else:
if event.what == DataPool.Event.updated_value and \ if event.what == DataPool.Event.updated_value and \
( (
@@ -3313,13 +3313,13 @@ class MonitorWidgets:
self.update_counter_lock.acquire() self.update_counter_lock.acquire()
self.update_counter += 1 self.update_counter += 1
self.update_counter_lock.release() self.update_counter_lock.release()
gobject.idle_add( self.update_display_idle_event, self.displayed_data[ 0 ][ : ], glib.idle_add(self.update_display_idle_event, self.displayed_data[ 0 ][ : ],
priority=gobject.PRIORITY_DEFAULT_IDLE ) priority=glib.PRIORITY_DEFAULT_IDLE )
else: else:
self.update_counter_lock.acquire( ) self.update_counter_lock.acquire( )
self.update_counter += 1 self.update_counter += 1
self.update_counter_lock.release( ) self.update_counter_lock.release( )
gobject.idle_add( self.datapool_idle_listener, event, priority=gobject.PRIORITY_DEFAULT_IDLE ) glib.idle_add(self.datapool_idle_listener, event, priority=glib.PRIORITY_DEFAULT_IDLE )
if event.what in [ DataPool.Event.updated_value, DataPool.Event.new_key ]: if event.what in [ DataPool.Event.updated_value, DataPool.Event.new_key ]:
#print "Update hdf5 file ..." #print "Update hdf5 file ..."
@@ -3346,8 +3346,8 @@ class MonitorWidgets:
self.update_counter_lock.acquire( ) self.update_counter_lock.acquire( )
self.update_counter += 1 self.update_counter += 1
self.update_counter_lock.release( ) self.update_counter_lock.release( )
gobject.idle_add( self.update_display_idle_event, self.displayed_data[ 0 ][ : ], glib.idle_add(self.update_display_idle_event, self.displayed_data[ 0 ][ : ],
priority=gobject.PRIORITY_DEFAULT_IDLE ) priority=glib.PRIORITY_DEFAULT_IDLE )
################### consume idle events ################### consume idle events
@@ -3457,7 +3457,7 @@ class MonitorWidgets:
self.update_counter += 1 self.update_counter += 1
self.update_counter_lock.release( ) self.update_counter_lock.release( )
event = DataPool.Event( DataPool.Event.updated_value, new_data_name ) event = DataPool.Event( DataPool.Event.updated_value, new_data_name )
gobject.idle_add( self.datapool_idle_listener, event, priority=gobject.PRIORITY_DEFAULT_IDLE ) glib.idle_add(self.datapool_idle_listener, event, priority=glib.PRIORITY_DEFAULT_IDLE )
def display_autoscaling_toggled( self, widget, data=None ): def display_autoscaling_toggled( self, widget, data=None ):
self.matplot_axes.set_autoscale_on( self.display_autoscaling_checkbutton.get_active( ) ) self.matplot_axes.set_autoscale_on( self.display_autoscaling_checkbutton.get_active( ) )
@@ -3600,7 +3600,7 @@ class MonitorWidgets:
menu.append(item_copy_svg) menu.append(item_copy_svg)
menu.show_all() menu.show_all()
menu.popup(None, None, None, None, event.button, event.time) menu.popup_at_pointer(event)
def copy_canvas_as_png(self, widget): def copy_canvas_as_png(self, widget):
"""Copy canvas to clipboard as PNG""" """Copy canvas to clipboard as PNG"""