472 lines
22 KiB
Python
472 lines
22 KiB
Python
import time
|
|
from pathlib import Path
|
|
|
|
from inkex.gui import Gtk, GLib
|
|
from gi.repository import GdkPixbuf
|
|
from plotters import plotters
|
|
|
|
|
|
class KMPlotGUI:
|
|
def build_window(self):
|
|
self.window = Gtk.Window(title="KM Plot")
|
|
self.window.set_border_width(10)
|
|
self.window.set_default_size(500, 500)
|
|
|
|
root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
|
|
|
self.status_label = Gtk.Label(label="No supported plotter detected.")
|
|
self.status_label.set_xalign(0.5)
|
|
|
|
self.device_label = Gtk.Label(label="Device: not connected")
|
|
self.device_label.set_xalign(0.5)
|
|
|
|
self.device_image = Gtk.Image()
|
|
self.device_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG)
|
|
self.device_image.set_pixel_size(150)
|
|
self.device_image.set_halign(Gtk.Align.CENTER)
|
|
self.device_image.set_valign(Gtk.Align.CENTER)
|
|
|
|
self.port_info_label = Gtk.Label(label="")
|
|
self.port_info_label.set_xalign(0.5)
|
|
self.port_info_label.set_halign(Gtk.Align.CENTER)
|
|
self.port_info_label.set_justify(Gtk.Justification.CENTER)
|
|
self.port_info_label.set_line_wrap(True)
|
|
self.port_info_label.set_margin_top(14)
|
|
self.port_info_label.set_margin_bottom(14)
|
|
self.port_info_label.set_margin_start(12)
|
|
self.port_info_label.set_margin_end(12)
|
|
self.port_info_frame = Gtk.Frame(label="Driver")
|
|
self.port_info_frame.set_shadow_type(Gtk.ShadowType.IN)
|
|
self.port_info_frame.set_halign(Gtk.Align.CENTER)
|
|
self.port_info_frame.set_margin_top(12)
|
|
self.port_info_frame.set_margin_bottom(12)
|
|
self.port_info_frame.set_margin_start(12)
|
|
self.port_info_frame.set_margin_end(12)
|
|
self.port_info_frame.set_size_request(250, -1)
|
|
self.port_info_frame.add(self.port_info_label)
|
|
|
|
self.port_store = Gtk.ListStore(str, str, str) # device, vidpid, display
|
|
self.port_combo = Gtk.ComboBox.new_with_model(self.port_store)
|
|
port_renderer = Gtk.CellRendererText()
|
|
self.port_combo.pack_start(port_renderer, True)
|
|
self.port_combo.add_attribute(port_renderer, "text", 2)
|
|
self.port_combo.set_sensitive(False)
|
|
self.port_combo.connect("changed", self.on_port_changed)
|
|
self.port_combo.set_halign(Gtk.Align.CENTER)
|
|
|
|
self.cut_button = Gtk.Button(label="Send to plotter")
|
|
self.cut_button.set_sensitive(False)
|
|
self.cut_button.connect("clicked", self.on_cut_clicked)
|
|
|
|
self.status_bar = Gtk.Label(label="Searching for devices...")
|
|
self.status_bar.set_xalign(0)
|
|
|
|
notebook = Gtk.Notebook()
|
|
notebook.set_tab_pos(Gtk.PositionType.TOP)
|
|
|
|
# Main tab
|
|
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
|
main_box.set_hexpand(True)
|
|
main_box.set_vexpand(True)
|
|
main_box.set_valign(Gtk.Align.CENTER)
|
|
main_box.set_halign(Gtk.Align.CENTER)
|
|
port_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
|
port_row.set_halign(Gtk.Align.CENTER)
|
|
port_row.pack_start(self.port_combo, False, False, 0)
|
|
main_box.pack_start(port_row, False, False, 0)
|
|
main_box.pack_start(self.port_info_frame, False, False, 10)
|
|
main_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 12)
|
|
main_box.pack_start(self.device_image, False, False, 6)
|
|
|
|
notebook.append_page(main_box, Gtk.Label(label="Device"))
|
|
|
|
# Connection settings tab
|
|
conn_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
|
conn_box.set_hexpand(True)
|
|
conn_box.set_vexpand(True)
|
|
conn_box.set_halign(Gtk.Align.CENTER)
|
|
conn_box.set_margin_top(12)
|
|
conn_box.set_margin_bottom(12)
|
|
conn_grid = Gtk.Grid(column_spacing=8, row_spacing=6)
|
|
conn_grid.set_column_homogeneous(False)
|
|
conn_grid.set_halign(Gtk.Align.CENTER)
|
|
|
|
# Plot settings tab
|
|
plot_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
|
plot_box.set_hexpand(True)
|
|
plot_box.set_vexpand(True)
|
|
plot_box.set_halign(Gtk.Align.CENTER)
|
|
plot_box.set_margin_top(12)
|
|
plot_box.set_margin_bottom(12)
|
|
plot_grid = Gtk.Grid(column_spacing=8, row_spacing=6)
|
|
plot_grid.set_column_homogeneous(False)
|
|
plot_grid.set_halign(Gtk.Align.CENTER)
|
|
|
|
self.adv_controls = {}
|
|
|
|
conn_row = 0
|
|
plot_row = 0
|
|
|
|
def add_conn_row(label_text, widget):
|
|
nonlocal conn_row
|
|
label = Gtk.Label(label=label_text)
|
|
label.set_xalign(0)
|
|
conn_grid.attach(label, 0, conn_row, 1, 1)
|
|
conn_grid.attach(widget, 1, conn_row, 1, 1)
|
|
conn_row += 1
|
|
|
|
def add_plot_row(label_text, widget):
|
|
nonlocal plot_row
|
|
label = Gtk.Label(label=label_text)
|
|
label.set_xalign(0)
|
|
plot_grid.attach(label, 0, plot_row, 1, 1)
|
|
plot_grid.attach(widget, 1, plot_row, 1, 1)
|
|
plot_row += 1
|
|
|
|
# Dropdown helpers
|
|
def combo(options, active_value):
|
|
store = Gtk.ListStore(str, str)
|
|
for key, text in options:
|
|
store.append([key, text])
|
|
combo_box = Gtk.ComboBox.new_with_model(store)
|
|
renderer_text = Gtk.CellRendererText()
|
|
combo_box.pack_start(renderer_text, True)
|
|
combo_box.add_attribute(renderer_text, "text", 1)
|
|
# set active
|
|
for i, row_data in enumerate(store):
|
|
if row_data[0] == active_value:
|
|
combo_box.set_active(i)
|
|
break
|
|
return combo_box
|
|
|
|
# Spin helpers
|
|
def spin_int(value, min_val, max_val, step):
|
|
adj = Gtk.Adjustment(value=value, lower=min_val, upper=max_val, step_increment=step)
|
|
return Gtk.SpinButton(adjustment=adj, climb_rate=1, digits=0)
|
|
|
|
def spin_float(value, min_val, max_val, step, digits=1):
|
|
adj = Gtk.Adjustment(value=value, lower=min_val, upper=max_val, step_increment=step)
|
|
return Gtk.SpinButton(adjustment=adj, climb_rate=1, digits=digits)
|
|
|
|
# Build widgets using current options/defaults
|
|
baud_spin = spin_int(int(getattr(self.options, "serialBaudRate", 9600)), 1200, 115200, 100)
|
|
bytesize_combo = combo(
|
|
[("five", "5"), ("six", "6"), ("seven", "7"), ("eight", "8")],
|
|
str(getattr(self.options, "serialByteSize", "eight")).lower(),
|
|
)
|
|
stopbits_combo = combo(
|
|
[("one", "1"), ("onepointfive", "1.5"), ("two", "2")],
|
|
str(getattr(self.options, "serialStopBits", "one")).lower(),
|
|
)
|
|
parity_combo = combo(
|
|
[("none", "None"), ("even", "Even"), ("odd", "Odd"), ("mark", "Mark"), ("space", "Space")],
|
|
str(getattr(self.options, "serialParity", "none")).lower(),
|
|
)
|
|
flow_combo = combo(
|
|
[("xonxoff", "XON/XOFF"), ("rtscts", "RTS/CTS"), ("dsrdtrrtscts", "DSR/DTR+RTS/CTS"), ("none", "None")],
|
|
str(getattr(self.options, "serialFlowControl", "xonxoff")).lower(),
|
|
)
|
|
resx_spin = spin_float(float(getattr(self.options, "resolutionX", 1016.0)), 10, 5000, 10, digits=1)
|
|
resy_spin = spin_float(float(getattr(self.options, "resolutionY", 1016.0)), 10, 5000, 10, digits=1)
|
|
pen_spin = spin_int(int(getattr(self.options, "pen", 1)), 0, 10, 1)
|
|
force_spin = spin_int(int(getattr(self.options, "force", 0)), 0, 1000, 1)
|
|
speed_spin = spin_int(int(getattr(self.options, "speed", 0)), 0, 1000, 1)
|
|
orientation_combo = combo(
|
|
[("0", "0°"), ("90", "90°"), ("180", "180°"), ("270", "270°")],
|
|
str(getattr(self.options, "orientation", "0")),
|
|
)
|
|
mirrorx_check = Gtk.CheckButton(label="Mirror X")
|
|
mirrorx_check.set_active(bool(getattr(self.options, "mirrorX", False)))
|
|
mirrory_check = Gtk.CheckButton(label="Mirror Y")
|
|
mirrory_check.set_active(bool(getattr(self.options, "mirrorY", False)))
|
|
center_check = Gtk.CheckButton(label="Center zero point")
|
|
center_check.set_active(bool(getattr(self.options, "center", False)))
|
|
precut_check = Gtk.CheckButton(label="Use precut")
|
|
precut_check.set_active(bool(getattr(self.options, "precut", True)))
|
|
autoalign_check = Gtk.CheckButton(label="Auto align")
|
|
autoalign_check.set_active(bool(getattr(self.options, "autoAlign", True)))
|
|
overcut_spin = spin_float(float(getattr(self.options, "overcut", 1.0)), 0, 10, 0.1, digits=2)
|
|
flat_spin = spin_float(float(getattr(self.options, "flat", 1.2)), 0.1, 10, 0.1, digits=2)
|
|
tool_spin = spin_float(float(getattr(self.options, "toolOffset", 0.25)), 0, 10, 0.05, digits=2)
|
|
|
|
def set_tip(widget, text):
|
|
try:
|
|
widget.set_tooltip_text(text)
|
|
except Exception:
|
|
pass
|
|
|
|
# Tooltips for plot settings to guide users.
|
|
set_tip(resx_spin, "Horizontal resolution in dots per inch (higher is finer).")
|
|
set_tip(resy_spin, "Vertical resolution in dots per inch (higher is finer).")
|
|
set_tip(pen_spin, "Pen number to use when cutting/drawing.")
|
|
set_tip(force_spin, "Downward force in grams; higher presses harder.")
|
|
set_tip(speed_spin, "Movement speed in cm/s; higher is faster.")
|
|
set_tip(orientation_combo, "Rotate the plot by the selected angle.")
|
|
set_tip(mirrorx_check, "Flip the design horizontally.")
|
|
set_tip(mirrory_check, "Flip the design vertically.")
|
|
set_tip(center_check, "Center the zero point on the page.")
|
|
set_tip(precut_check, "Use a small precut to help corners release cleanly.")
|
|
set_tip(autoalign_check, "Attempt to auto-align the plot to the material.")
|
|
set_tip(overcut_spin, "Extend cuts past corners to ensure complete separation.")
|
|
set_tip(flat_spin, "Flatness compensation factor.")
|
|
set_tip(tool_spin, "Offset distance for the tool tip (in mm).")
|
|
|
|
add_conn_row("Baud rate", baud_spin); self.adv_controls["serialBaudRate"] = baud_spin
|
|
add_conn_row("Byte size", bytesize_combo); self.adv_controls["serialByteSize"] = bytesize_combo
|
|
add_conn_row("Stop bits", stopbits_combo); self.adv_controls["serialStopBits"] = stopbits_combo
|
|
add_conn_row("Parity", parity_combo); self.adv_controls["serialParity"] = parity_combo
|
|
add_conn_row("Flow control", flow_combo); self.adv_controls["serialFlowControl"] = flow_combo
|
|
add_plot_row("Resolution X (dpi)", resx_spin); self.adv_controls["resolutionX"] = resx_spin
|
|
add_plot_row("Resolution Y (dpi)", resy_spin); self.adv_controls["resolutionY"] = resy_spin
|
|
add_plot_row("Pen number", pen_spin); self.adv_controls["pen"] = pen_spin
|
|
add_plot_row("Force (g)", force_spin); self.adv_controls["force"] = force_spin
|
|
add_plot_row("Speed (cm/s)", speed_spin); self.adv_controls["speed"] = speed_spin
|
|
add_plot_row("Orientation", orientation_combo); self.adv_controls["orientation"] = orientation_combo
|
|
add_plot_row("Mirror X", mirrorx_check); self.adv_controls["mirrorX"] = mirrorx_check
|
|
add_plot_row("Mirror Y", mirrory_check); self.adv_controls["mirrorY"] = mirrory_check
|
|
add_plot_row("Center", center_check); self.adv_controls["center"] = center_check
|
|
add_plot_row("Precut", precut_check); self.adv_controls["precut"] = precut_check
|
|
add_plot_row("Auto align", autoalign_check); self.adv_controls["autoAlign"] = autoalign_check
|
|
add_plot_row("Overcut (mm)", overcut_spin); self.adv_controls["overcut"] = overcut_spin
|
|
add_plot_row("Flatness", flat_spin); self.adv_controls["flat"] = flat_spin
|
|
add_plot_row("Tool offset (mm)", tool_spin); self.adv_controls["toolOffset"] = tool_spin
|
|
|
|
# Connect change handlers to keep self.options in sync.
|
|
baud_spin.connect("value-changed", lambda w: self.update_option("serialBaudRate", int(w.get_value())))
|
|
resx_spin.connect("value-changed", lambda w: self.update_option("resolutionX", float(w.get_value())))
|
|
resy_spin.connect("value-changed", lambda w: self.update_option("resolutionY", float(w.get_value())))
|
|
pen_spin.connect("value-changed", lambda w: self.update_option("pen", int(w.get_value())))
|
|
force_spin.connect("value-changed", lambda w: self.update_option("force", int(w.get_value())))
|
|
speed_spin.connect("value-changed", lambda w: self.update_option("speed", int(w.get_value())))
|
|
overcut_spin.connect("value-changed", lambda w: self.update_option("overcut", float(w.get_value())))
|
|
flat_spin.connect("value-changed", lambda w: self.update_option("flat", float(w.get_value())))
|
|
tool_spin.connect("value-changed", lambda w: self.update_option("toolOffset", float(w.get_value())))
|
|
|
|
bytesize_combo.connect(
|
|
"changed",
|
|
lambda w: self.update_option_from_combo("serialByteSize", w, default="eight"),
|
|
)
|
|
stopbits_combo.connect(
|
|
"changed",
|
|
lambda w: self.update_option_from_combo("serialStopBits", w, default="one"),
|
|
)
|
|
parity_combo.connect(
|
|
"changed", lambda w: self.update_option_from_combo("serialParity", w, default="none")
|
|
)
|
|
flow_combo.connect(
|
|
"changed",
|
|
lambda w: self.update_option_from_combo("serialFlowControl", w, default="xonxoff"),
|
|
)
|
|
orientation_combo.connect(
|
|
"changed", lambda w: self.update_option_from_combo("orientation", w, default="0")
|
|
)
|
|
|
|
mirrorx_check.connect("toggled", lambda w: self.update_option("mirrorX", w.get_active()))
|
|
mirrory_check.connect("toggled", lambda w: self.update_option("mirrorY", w.get_active()))
|
|
center_check.connect("toggled", lambda w: self.update_option("center", w.get_active()))
|
|
precut_check.connect("toggled", lambda w: self.update_option("precut", w.get_active()))
|
|
autoalign_check.connect("toggled", lambda w: self.update_option("autoAlign", w.get_active()))
|
|
|
|
conn_box.pack_start(conn_grid, False, False, 0)
|
|
conn_scroller = Gtk.ScrolledWindow()
|
|
conn_scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
|
conn_scroller.set_hexpand(True)
|
|
conn_scroller.set_vexpand(True)
|
|
conn_scroller.add(conn_box)
|
|
|
|
plot_box.pack_start(plot_grid, False, False, 0)
|
|
plot_scroller = Gtk.ScrolledWindow()
|
|
plot_scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
|
plot_scroller.set_hexpand(True)
|
|
plot_scroller.set_vexpand(True)
|
|
plot_scroller.add(plot_box)
|
|
|
|
notebook.append_page(conn_scroller, Gtk.Label(label="Connection Settings"))
|
|
notebook.append_page(plot_scroller, Gtk.Label(label="Plot Settings"))
|
|
|
|
# Footer with status and send button at the bottom.
|
|
footer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
|
footer.pack_start(self.status_bar, True, True, 0)
|
|
footer.pack_end(self.cut_button, False, False, 0)
|
|
|
|
root.pack_start(notebook, True, True, 0)
|
|
root.pack_end(footer, False, False, 0)
|
|
|
|
self.window.add(root)
|
|
self.window.connect("destroy", self.on_window_close)
|
|
self.window.show_all()
|
|
|
|
def poll_devices(self):
|
|
if getattr(self, "sending", False):
|
|
return True
|
|
self.debug("Polling for devices...")
|
|
self.update_status_bar("Searching for devices...")
|
|
devices = list(self.enumerate_with_serial())
|
|
self.port_entries = []
|
|
for device_entry in devices:
|
|
device_path = device_entry["device"]
|
|
vidpid = device_entry["vidpid"]
|
|
info_text = device_entry["info"]
|
|
plotter_info = plotters.get(vidpid)
|
|
if isinstance(plotter_info, dict):
|
|
name = plotter_info.get("name", vidpid)
|
|
icon_key = plotter_info.get("icon")
|
|
elif plotter_info:
|
|
name = str(plotter_info)
|
|
icon_key = None
|
|
else:
|
|
name = "Unknown device"
|
|
icon_key = None
|
|
display = f"{device_path} ({vidpid})" if vidpid else device_path
|
|
self.port_entries.append(
|
|
{
|
|
"device": device_path,
|
|
"vidpid": vidpid,
|
|
"name": name,
|
|
"display": display,
|
|
"info": info_text,
|
|
"icon": icon_key,
|
|
"supported": plotter_info is not None,
|
|
}
|
|
)
|
|
|
|
if self.port_store:
|
|
self.port_store.clear()
|
|
for entry in self.port_entries:
|
|
self.port_store.append([entry["device"], entry["vidpid"] or "", entry["display"]])
|
|
|
|
if not self.port_entries:
|
|
self.debug("No ports detected this cycle.")
|
|
if self.port_combo:
|
|
self.port_combo.set_sensitive(False)
|
|
self.port_combo.hide()
|
|
self.current_device = None
|
|
self.current_vidpid = None
|
|
if self.port_info_label:
|
|
self.port_info_label.set_text("No Serial Devices Found")
|
|
self.port_info_frame.set_visible(True)
|
|
self.set_device_icon(None, fallback="noport")
|
|
self.cut_button.set_sensitive(False)
|
|
self.update_status_bar("Waiting for a supported plotter...")
|
|
return True
|
|
|
|
if self.port_combo:
|
|
self.port_combo.set_sensitive(True)
|
|
self.port_combo.show()
|
|
if self.port_info_frame:
|
|
self.port_info_frame.set_visible(True)
|
|
|
|
active_idx = 0
|
|
if self.current_device:
|
|
for idx, entry in enumerate(self.port_entries):
|
|
if entry["device"] == self.current_device:
|
|
active_idx = idx
|
|
break
|
|
|
|
if self.port_combo:
|
|
self.port_combo.handler_block_by_func(self.on_port_changed)
|
|
self.port_combo.set_active(active_idx)
|
|
self.port_combo.handler_unblock_by_func(self.on_port_changed)
|
|
|
|
self.apply_port_entry(self.port_entries[active_idx], update_status=not getattr(self, "sending", False))
|
|
return True
|
|
|
|
def apply_port_entry(self, entry, update_status=False):
|
|
self.current_device = entry["device"]
|
|
self.current_vidpid = entry["vidpid"]
|
|
detail = entry["vidpid"] or "unknown VID/PID"
|
|
if self.port_info_label:
|
|
info_text = entry.get("info") or ""
|
|
self.port_info_label.set_text(info_text)
|
|
self.set_device_icon(entry.get("icon"))
|
|
self.cut_button.set_sensitive(True)
|
|
if update_status:
|
|
self.update_status_bar("Ready")
|
|
|
|
def on_port_changed(self, _combo):
|
|
idx = self.port_combo.get_active() if self.port_combo else -1
|
|
if idx is None or idx < 0 or idx >= len(self.port_entries):
|
|
return
|
|
self.apply_port_entry(self.port_entries[idx], update_status=True)
|
|
|
|
def on_window_close(self, *_args):
|
|
if self.poll_id:
|
|
GLib.source_remove(self.poll_id)
|
|
Gtk.main_quit()
|
|
|
|
def on_cut_clicked(self, _button):
|
|
if not self.current_device:
|
|
self.show_dialog("No plotter detected yet.", Gtk.MessageType.ERROR)
|
|
self.update_status_bar("No plotter detected.", error=True)
|
|
return
|
|
try:
|
|
self.sending = True
|
|
self.update_status_bar("Sending to plotter...")
|
|
self.flush_gui()
|
|
time.sleep(0.05)
|
|
self.perform_cut(self.current_device)
|
|
self.show_dialog(
|
|
f"Sent the document to {self.current_device}.", Gtk.MessageType.INFO
|
|
)
|
|
self.update_status_bar("Sent")
|
|
except Exception as exc: # pylint: disable=broad-except
|
|
self.show_dialog(f"Cut failed: {exc}", Gtk.MessageType.ERROR)
|
|
self.update_status_bar(f"Cut failed: {exc}", error=True)
|
|
finally:
|
|
self.sending = False
|
|
|
|
def show_dialog(self, message, level):
|
|
dialog = Gtk.MessageDialog(
|
|
transient_for=self.window,
|
|
flags=0,
|
|
message_type=level,
|
|
buttons=Gtk.ButtonsType.OK,
|
|
text=message,
|
|
)
|
|
dialog.run()
|
|
dialog.destroy()
|
|
|
|
def update_status_bar(self, message, error=False):
|
|
if not self.status_bar:
|
|
return
|
|
prefix = "Error: " if error else ""
|
|
self.status_bar.set_text(f"{prefix}{message}")
|
|
|
|
def set_device_icon(self, icon_key, fallback=None):
|
|
if not self.device_image:
|
|
return
|
|
target_px = 150
|
|
base_dir = getattr(self, "icons_dir", None)
|
|
if base_dir is None:
|
|
base_dir = Path(__file__).resolve().parent / "icons"
|
|
|
|
def load_icon(name):
|
|
icon_path = base_dir / f"{name}.png"
|
|
if icon_path.exists():
|
|
try:
|
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
|
|
str(icon_path), width=target_px, height=target_px, preserve_aspect_ratio=True
|
|
)
|
|
self.device_image.set_from_pixbuf(pixbuf)
|
|
return True
|
|
except Exception as exc:
|
|
try:
|
|
self.debug(f"Failed to load icon {icon_path}: {exc}")
|
|
except Exception:
|
|
pass
|
|
return False
|
|
|
|
if icon_key and load_icon(icon_key):
|
|
return
|
|
if fallback and load_icon(fallback):
|
|
return
|
|
if load_icon("unknown"):
|
|
return
|
|
self.device_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG)
|
|
self.device_image.set_pixel_size(target_px)
|
|
|
|
def flush_gui(self):
|
|
"""Process pending GTK events so label updates are shown before blocking work."""
|
|
while Gtk.events_pending():
|
|
Gtk.main_iteration_do(False)
|