Wed, 20 Jan 2021 10:17:01 +0100
updated pronterface.py losing my own changes
15 | 1 | # This file is part of the Printrun suite. |
2 | # | |
3 | # Printrun is free software: you can redistribute it and/or modify | |
4 | # it under the terms of the GNU General Public License as published by | |
5 | # the Free Software Foundation, either version 3 of the License, or | |
6 | # (at your option) any later version. | |
7 | # | |
8 | # Printrun is distributed in the hope that it will be useful, | |
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
11 | # GNU General Public License for more details. | |
12 | # | |
13 | # You should have received a copy of the GNU General Public License | |
14 | # along with Printrun. If not, see <http://www.gnu.org/licenses/>. | |
15 | ||
16 | import os | |
47 | 17 | import platform |
18 | import queue | |
15 | 19 | import sys |
20 | import time | |
21 | import threading | |
22 | import traceback | |
47 | 23 | import io as StringIO |
15 | 24 | import subprocess |
25 | import glob | |
26 | import logging | |
47 | 27 | import re |
15 | 28 | |
29 | try: import simplejson as json | |
30 | except ImportError: import json | |
31 | ||
32 | from . import pronsole | |
33 | from . import printcore | |
47 | 34 | from printrun.spoolmanager import spoolmanager_gui |
15 | 35 | |
36 | from .utils import install_locale, setup_logging, dosify, \ | |
37 | iconfile, configfile, format_time, format_duration, \ | |
38 | hexcolor_to_float, parse_temperature_report, \ | |
47 | 39 | prepare_command, check_rgb_color, check_rgba_color, compile_file, \ |
40 | write_history_to, read_history_from | |
15 | 41 | install_locale('pronterface') |
42 | ||
43 | try: | |
44 | import wx | |
47 | 45 | import wx.adv |
46 | if wx.VERSION < (4,): | |
47 | raise ImportError() | |
15 | 48 | except: |
47 | 49 | logging.error(_("WX >= 4 is not installed. This program requires WX >= 4 to run.")) |
15 | 50 | raise |
51 | ||
52 | from .gui.widgets import SpecialButton, MacroEditor, PronterOptions, ButtonEdit | |
53 | ||
54 | winsize = (800, 500) | |
55 | layerindex = 0 | |
56 | if os.name == "nt": | |
57 | winsize = (800, 530) | |
58 | ||
59 | pronterface_quitting = False | |
60 | ||
61 | class PronterfaceQuitException(Exception): | |
62 | pass | |
63 | ||
64 | from .gui import MainWindow | |
65 | from .settings import wxSetting, HiddenSetting, StringSetting, SpinSetting, \ | |
47 | 66 | FloatSpinSetting, BooleanSetting, StaticTextSetting, ColorSetting, ComboSetting |
15 | 67 | from printrun import gcoder |
68 | from .pronsole import REPORT_NONE, REPORT_POS, REPORT_TEMP, REPORT_MANUAL | |
69 | ||
47 | 70 | def format_length(mm, fractional=2): |
71 | if mm <= 10: | |
72 | units = mm | |
73 | suffix = 'mm' | |
74 | elif mm < 1000: | |
75 | units = mm / 10 | |
76 | suffix = 'cm' | |
77 | else: | |
78 | units = mm / 1000 | |
79 | suffix = 'm' | |
80 | return '%%.%df' % fractional % units + suffix | |
81 | ||
82 | class ConsoleOutputHandler: | |
15 | 83 | """Handle console output. All messages go through the logging submodule. We setup a logging handler to get logged messages and write them to both stdout (unless a log file path is specified, in which case we add another logging handler to write to this file) and the log panel. |
84 | We also redirect stdout and stderr to ourself to catch print messages and al.""" | |
85 | ||
86 | def __init__(self, target, log_path): | |
87 | self.stdout = sys.stdout | |
88 | self.stderr = sys.stderr | |
89 | sys.stdout = self | |
90 | sys.stderr = self | |
47 | 91 | self.print_on_stdout = not log_path |
15 | 92 | if log_path: |
93 | setup_logging(self, log_path, reset_handlers = True) | |
94 | else: | |
47 | 95 | setup_logging(sys.stdout, reset_handlers = True) |
96 | self.target = target | |
15 | 97 | |
98 | def __del__(self): | |
99 | sys.stdout = self.stdout | |
100 | sys.stderr = self.stderr | |
101 | ||
102 | def write(self, data): | |
103 | try: | |
104 | self.target(data) | |
105 | except: | |
106 | pass | |
107 | if self.print_on_stdout: | |
108 | self.stdout.write(data) | |
109 | ||
110 | def flush(self): | |
111 | if self.stdout: | |
112 | self.stdout.flush() | |
113 | ||
114 | class PronterWindow(MainWindow, pronsole.pronsole): | |
115 | ||
116 | _fgcode = None | |
47 | 117 | printer_progress_time = time.time() |
15 | 118 | |
119 | def _get_fgcode(self): | |
120 | return self._fgcode | |
121 | ||
122 | def _set_fgcode(self, value): | |
123 | self._fgcode = value | |
124 | self.excluder = None | |
125 | self.excluder_e = None | |
126 | self.excluder_z_abs = None | |
127 | self.excluder_z_rel = None | |
128 | fgcode = property(_get_fgcode, _set_fgcode) | |
129 | ||
130 | def _get_display_graph(self): | |
131 | return self.settings.tempgraph | |
132 | display_graph = property(_get_display_graph) | |
133 | ||
134 | def _get_display_gauges(self): | |
135 | return self.settings.tempgauges | |
136 | display_gauges = property(_get_display_gauges) | |
137 | ||
138 | def __init__(self, app, filename = None, size = winsize): | |
139 | pronsole.pronsole.__init__(self) | |
140 | self.app = app | |
141 | self.window_ready = False | |
142 | self.ui_ready = False | |
143 | self._add_settings(size) | |
144 | ||
145 | self.pauseScript = None #"pause.gcode" | |
146 | self.endScript = None #"end.gcode" | |
147 | ||
148 | self.filename = filename | |
149 | ||
150 | self.capture_skip = {} | |
151 | self.capture_skip_newline = False | |
152 | self.fgcode = None | |
153 | self.excluder = None | |
154 | self.slicep = None | |
155 | self.current_pos = [0, 0, 0] | |
156 | self.paused = False | |
157 | self.uploading = False | |
47 | 158 | self.sentglines = queue.Queue(0) |
15 | 159 | self.cpbuttons = { |
160 | "motorsoff": SpecialButton(_("Motors off"), ("M84"), (250, 250, 250), _("Switch all motors off")), | |
161 | "extrude": SpecialButton(_("Extrude"), ("pront_extrude"), (225, 200, 200), _("Advance extruder by set length")), | |
162 | "reverse": SpecialButton(_("Reverse"), ("pront_reverse"), (225, 200, 200), _("Reverse extruder by set length")), | |
163 | } | |
164 | self.custombuttons = [] | |
165 | self.btndict = {} | |
166 | self.filehistory = None | |
167 | self.autoconnect = False | |
47 | 168 | self.autoscrolldisable = False |
169 | ||
15 | 170 | self.parse_cmdline(sys.argv[1:]) |
47 | 171 | for field in dir(self.settings): |
172 | if field.startswith("_gcview_color_"): | |
173 | cleanname = field[1:] | |
174 | color = hexcolor_to_float(getattr(self.settings, cleanname), 4) | |
175 | setattr(self, cleanname, list(color)) | |
15 | 176 | |
177 | # FIXME: We need to initialize the main window after loading the | |
178 | # configs to restore the size, but this might have some unforeseen | |
179 | # consequences. | |
180 | # -- Okai, it seems it breaks things like update_gviz_params >< | |
181 | os.putenv("UBUNTU_MENUPROXY", "0") | |
182 | size = (self.settings.last_window_width, self.settings.last_window_height) | |
183 | MainWindow.__init__(self, None, title = _("Pronterface"), size = size) | |
184 | if self.settings.last_window_maximized: | |
185 | self.Maximize() | |
186 | self.SetIcon(wx.Icon(iconfile("pronterface.png"), wx.BITMAP_TYPE_PNG)) | |
187 | self.Bind(wx.EVT_SIZE, self.on_resize) | |
188 | self.Bind(wx.EVT_MAXIMIZE, self.on_maximize) | |
189 | self.window_ready = True | |
47 | 190 | self.Bind(wx.EVT_CLOSE, self.closewin) |
191 | self.Bind(wx.EVT_CHAR_HOOK, self.on_key) | |
15 | 192 | # set feedrates in printcore for pause/resume |
193 | self.p.xy_feedrate = self.settings.xy_feedrate | |
194 | self.p.z_feedrate = self.settings.z_feedrate | |
195 | ||
196 | self.panel.SetBackgroundColour(self.bgcolor) | |
197 | customdict = {} | |
198 | try: | |
47 | 199 | exec(compile_file(configfile("custombtn.txt")), customdict) |
15 | 200 | if len(customdict["btns"]): |
201 | if not len(self.custombuttons): | |
202 | try: | |
203 | self.custombuttons = customdict["btns"] | |
47 | 204 | for n in range(len(self.custombuttons)): |
15 | 205 | self.cbutton_save(n, self.custombuttons[n]) |
206 | os.rename("custombtn.txt", "custombtn.old") | |
207 | rco = open("custombtn.txt", "w") | |
208 | rco.write(_("# I moved all your custom buttons into .pronsolerc.\n# Please don't add them here any more.\n# Backup of your old buttons is in custombtn.old\n")) | |
209 | rco.close() | |
47 | 210 | except IOError as x: |
15 | 211 | logging.error(str(x)) |
212 | else: | |
213 | logging.warning(_("Note!!! You have specified custom buttons in both custombtn.txt and .pronsolerc")) | |
214 | logging.warning(_("Ignoring custombtn.txt. Remove all current buttons to revert to custombtn.txt")) | |
215 | ||
216 | except: | |
217 | pass | |
47 | 218 | self.menustrip = wx.MenuBar() |
15 | 219 | self.reload_ui() |
220 | # disable all printer controls until we connect to a printer | |
221 | self.gui_set_disconnected() | |
222 | self.statusbar = self.CreateStatusBar() | |
223 | self.statusbar.SetStatusText(_("Not connected to printer.")) | |
224 | ||
225 | self.t = ConsoleOutputHandler(self.catchprint, self.settings.log_path) | |
226 | self.stdout = sys.stdout | |
227 | self.slicing = False | |
228 | self.loading_gcode = False | |
229 | self.loading_gcode_message = "" | |
230 | self.mini = False | |
231 | self.p.sendcb = self.sentcb | |
232 | self.p.preprintsendcb = self.preprintsendcb | |
233 | self.p.printsendcb = self.printsentcb | |
234 | self.p.startcb = self.startcb | |
235 | self.p.endcb = self.endcb | |
236 | self.cur_button = None | |
237 | self.predisconnect_mainqueue = None | |
238 | self.predisconnect_queueindex = None | |
239 | self.predisconnect_layer = None | |
240 | self.hsetpoint = 0.0 | |
241 | self.bsetpoint = 0.0 | |
242 | if self.autoconnect: | |
243 | self.connect() | |
244 | if self.filename is not None: | |
245 | self.do_load(self.filename) | |
246 | if self.settings.monitor: | |
247 | self.update_monitor() | |
248 | ||
249 | # -------------------------------------------------------------- | |
250 | # Main interface handling | |
251 | # -------------------------------------------------------------- | |
252 | ||
253 | def reset_ui(self): | |
254 | MainWindow.reset_ui(self) | |
255 | self.custombuttons_widgets = [] | |
256 | ||
257 | def reload_ui(self, *args): | |
258 | if not self.window_ready: return | |
47 | 259 | temp_monitor = self.settings.monitor |
260 | self.settings.monitor = False | |
261 | self.update_monitor() | |
15 | 262 | self.Freeze() |
263 | ||
264 | # If UI is being recreated, delete current one | |
265 | if self.ui_ready: | |
266 | # Store log console content | |
267 | logcontent = self.logbox.GetValue() | |
47 | 268 | self.menustrip.SetMenus([]) |
269 | if len(self.commandbox.history): | |
270 | #save current command box history | |
271 | if not os.path.exists(self.history_file): | |
272 | if not os.path.exists(self.cache_dir): | |
273 | os.makedirs(self.cache_dir) | |
274 | write_history_to(self.history_file, self.commandbox.history) | |
15 | 275 | # Create a temporary panel to reparent widgets with state we want |
276 | # to retain across UI changes | |
277 | temppanel = wx.Panel(self) | |
278 | # TODO: add viz widgets to statefulControls | |
279 | for control in self.statefulControls: | |
280 | control.GetContainingSizer().Detach(control) | |
281 | control.Reparent(temppanel) | |
282 | self.panel.DestroyChildren() | |
283 | self.gwindow.Destroy() | |
284 | self.reset_ui() | |
285 | ||
286 | # Create UI | |
47 | 287 | self.create_menu() |
288 | self.update_recent_files("recentfiles", self.settings.recentfiles) | |
289 | self.splitterwindow = None | |
15 | 290 | if self.settings.uimode in (_("Tabbed"), _("Tabbed with platers")): |
291 | self.createTabbedGui() | |
292 | else: | |
293 | self.createGui(self.settings.uimode == _("Compact"), | |
294 | self.settings.controlsmode == "Mini") | |
295 | ||
47 | 296 | if self.splitterwindow: |
15 | 297 | self.splitterwindow.SetSashPosition(self.settings.last_sash_position) |
298 | ||
299 | def splitter_resize(event): | |
300 | self.splitterwindow.UpdateSize() | |
301 | self.splitterwindow.Bind(wx.EVT_SIZE, splitter_resize) | |
302 | ||
303 | def sash_position_changed(event): | |
304 | self.set("last_sash_position", self.splitterwindow.GetSashPosition()) | |
305 | self.splitterwindow.Bind(wx.EVT_SPLITTER_SASH_POS_CHANGED, sash_position_changed) | |
306 | ||
307 | # Set gcview parameters here as they don't get set when viewers are | |
308 | # created | |
309 | self.update_gcview_params() | |
310 | ||
311 | # Finalize | |
47 | 312 | if self.p.online: |
15 | 313 | self.gui_set_connected() |
314 | if self.ui_ready: | |
315 | self.logbox.SetValue(logcontent) | |
316 | temppanel.Destroy() | |
317 | self.panel.Layout() | |
318 | if self.fgcode: | |
319 | self.start_viz_thread() | |
320 | self.ui_ready = True | |
47 | 321 | self.settings.monitor = temp_monitor |
322 | self.commandbox.history = read_history_from(self.history_file) | |
323 | self.commandbox.histindex = len(self.commandbox.history) | |
15 | 324 | self.Thaw() |
47 | 325 | if self.settings.monitor: |
326 | self.update_monitor() | |
15 | 327 | |
328 | def on_resize(self, event): | |
329 | wx.CallAfter(self.on_resize_real) | |
330 | event.Skip() | |
331 | ||
332 | def on_resize_real(self): | |
333 | maximized = self.IsMaximized() | |
334 | self.set("last_window_maximized", maximized) | |
335 | if not maximized and not self.IsIconized(): | |
336 | size = self.GetSize() | |
337 | self.set("last_window_width", size[0]) | |
338 | self.set("last_window_height", size[1]) | |
339 | ||
340 | def on_maximize(self, event): | |
341 | self.set("last_window_maximized", self.IsMaximized()) | |
342 | event.Skip() | |
343 | ||
344 | def on_exit(self, event): | |
345 | self.Close() | |
346 | ||
47 | 347 | def on_settings_change(self, changed_settings): |
348 | if self.gviz: | |
349 | self.gviz.on_settings_change(changed_settings) | |
350 | ||
351 | def on_key(self, event): | |
352 | if not isinstance(event.EventObject, (wx.TextCtrl, wx.ComboBox)) \ | |
353 | or event.HasModifiers(): | |
354 | ch = chr(event.KeyCode) | |
355 | keys = {'B': self.btemp, 'H': self.htemp, 'J': self.xyb, 'S': self.commandbox, | |
356 | 'V': self.gviz} | |
357 | widget = keys.get(ch) | |
358 | #ignore Alt+(S, H), so it can open Settings, Help menu | |
359 | if widget and (ch not in 'SH' or not event.AltDown()) \ | |
360 | and not (event.ControlDown() and ch == 'V' | |
361 | and event.EventObject is self.commandbox): | |
362 | widget.SetFocus() | |
363 | return | |
364 | # On MSWindows button mnemonics are processed only if the | |
365 | # focus is in the parent panel | |
366 | if event.AltDown() and ch < 'Z': | |
367 | in_toolbar = self.toolbarsizer.GetItem(event.EventObject) | |
368 | candidates = (self.connectbtn, self.connectbtn_cb_var), \ | |
369 | (self.pausebtn, self.pause), \ | |
370 | (self.printbtn, self.printfile) | |
371 | for ctl, cb in candidates: | |
372 | match = ('&' + ch) in ctl.Label.upper() | |
373 | handled = in_toolbar and match | |
374 | if handled: | |
375 | break | |
376 | # react to 'P' even for 'Restart', 'Resume' | |
377 | # print('match', match, 'handled', handled, ctl.Label, ctl.Enabled) | |
378 | if (match or ch == 'P' and ctl != self.connectbtn) and ctl.Enabled: | |
379 | # print('call', ch, cb) | |
380 | cb() | |
381 | # react to only 1 of 'P' buttons, prefer Resume | |
382 | return | |
383 | ||
384 | event.Skip() | |
385 | ||
386 | def closewin(self, e): | |
387 | e.StopPropagation() | |
388 | self.do_exit("force") | |
389 | ||
390 | def kill(self, e=None): | |
391 | if len(self.commandbox.history): | |
392 | #save current command box history | |
393 | history = (self.history_file) | |
394 | if not os.path.exists(history): | |
395 | if not os.path.exists(self.cache_dir): | |
396 | os.makedirs(self.cache_dir) | |
397 | write_history_to(history,self.commandbox.history) | |
15 | 398 | if self.p.printing or self.p.paused: |
399 | dlg = wx.MessageDialog(self, _("Print in progress ! Are you really sure you want to quit ?"), _("Exit"), wx.YES_NO | wx.ICON_WARNING) | |
400 | if dlg.ShowModal() == wx.ID_NO: | |
401 | return | |
402 | pronsole.pronsole.kill(self) | |
403 | global pronterface_quitting | |
404 | pronterface_quitting = True | |
405 | self.p.recvcb = None | |
406 | self.p.disconnect() | |
407 | if hasattr(self, "feedrates_changed"): | |
408 | self.save_in_rc("set xy_feedrate", "set xy_feedrate %d" % self.settings.xy_feedrate) | |
409 | self.save_in_rc("set z_feedrate", "set z_feedrate %d" % self.settings.z_feedrate) | |
410 | self.save_in_rc("set e_feedrate", "set e_feedrate %d" % self.settings.e_feedrate) | |
411 | if self.settings.last_extrusion != self.settings.default_extrusion: | |
412 | self.save_in_rc("set last_extrusion", "set last_extrusion %d" % self.settings.last_extrusion) | |
413 | if self.excluder: | |
414 | self.excluder.close_window() | |
415 | wx.CallAfter(self.gwindow.Destroy) | |
416 | wx.CallAfter(self.Destroy) | |
417 | ||
47 | 418 | @property |
419 | def bgcolor(self): | |
420 | return (wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWFRAME) | |
421 | if self.settings.bgcolor == 'auto' | |
422 | else self.settings.bgcolor) | |
15 | 423 | |
424 | # -------------------------------------------------------------- | |
425 | # Main interface actions | |
426 | # -------------------------------------------------------------- | |
427 | ||
428 | def do_monitor(self, l = ""): | |
429 | if l.strip() == "": | |
430 | self.set("monitor", not self.settings.monitor) | |
431 | elif l.strip() == "off": | |
432 | self.set("monitor", False) | |
433 | else: | |
434 | try: | |
435 | self.monitor_interval = float(l) | |
436 | self.set("monitor", self.monitor_interval > 0) | |
437 | except: | |
438 | self.log(_("Invalid period given.")) | |
439 | if self.settings.monitor: | |
440 | self.log(_("Monitoring printer.")) | |
441 | else: | |
442 | self.log(_("Done monitoring.")) | |
443 | ||
444 | def do_pront_extrude(self, l = ""): | |
47 | 445 | if self.p.printing and not self.paused: |
446 | self.log(_("Please pause or stop print before extruding.")) | |
447 | return | |
15 | 448 | feed = self.settings.e_feedrate |
449 | self.do_extrude_final(self.edist.GetValue(), feed) | |
450 | ||
451 | def do_pront_reverse(self, l = ""): | |
47 | 452 | if self.p.printing and not self.paused: |
453 | self.log(_("Please pause or stop print before reversing.")) | |
454 | return | |
15 | 455 | feed = self.settings.e_feedrate |
456 | self.do_extrude_final(- self.edist.GetValue(), feed) | |
457 | ||
458 | def do_settemp(self, l = ""): | |
459 | try: | |
47 | 460 | if not isinstance(l, str) or not len(l): |
15 | 461 | l = str(self.htemp.GetValue().split()[0]) |
462 | l = l.lower().replace(", ", ".") | |
463 | for i in self.temps.keys(): | |
464 | l = l.replace(i, self.temps[i]) | |
465 | f = float(l) | |
466 | if f >= 0: | |
467 | if self.p.online: | |
468 | self.p.send_now("M104 S" + l) | |
47 | 469 | self.log(_("Setting hotend temperature to %g degrees Celsius.") % f) |
15 | 470 | self.sethotendgui(f) |
471 | else: | |
472 | self.logError(_("Printer is not online.")) | |
473 | else: | |
474 | self.logError(_("You cannot set negative temperatures. To turn the hotend off entirely, set its temperature to 0.")) | |
47 | 475 | except Exception as x: |
15 | 476 | self.logError(_("You must enter a temperature. (%s)") % (repr(x),)) |
477 | ||
478 | def do_bedtemp(self, l = ""): | |
479 | try: | |
47 | 480 | if not isinstance(l, str) or not len(l): |
15 | 481 | l = str(self.btemp.GetValue().split()[0]) |
482 | l = l.lower().replace(", ", ".") | |
483 | for i in self.bedtemps.keys(): | |
484 | l = l.replace(i, self.bedtemps[i]) | |
485 | f = float(l) | |
486 | if f >= 0: | |
487 | if self.p.online: | |
488 | self.p.send_now("M140 S" + l) | |
47 | 489 | self.log(_("Setting bed temperature to %g degrees Celsius.") % f) |
15 | 490 | self.setbedgui(f) |
491 | else: | |
492 | self.logError(_("Printer is not online.")) | |
493 | else: | |
494 | self.logError(_("You cannot set negative temperatures. To turn the bed off entirely, set its temperature to 0.")) | |
47 | 495 | except Exception as x: |
15 | 496 | self.logError(_("You must enter a temperature. (%s)") % (repr(x),)) |
497 | ||
498 | def do_setspeed(self, l = ""): | |
499 | try: | |
47 | 500 | if not isinstance(l, str) or not len(l): |
15 | 501 | l = str(self.speed_slider.GetValue()) |
502 | else: | |
503 | l = l.lower() | |
504 | speed = int(l) | |
505 | if self.p.online: | |
506 | self.p.send_now("M220 S" + l) | |
507 | self.log(_("Setting print speed factor to %d%%.") % speed) | |
508 | else: | |
509 | self.logError(_("Printer is not online.")) | |
47 | 510 | except Exception as x: |
15 | 511 | self.logError(_("You must enter a speed. (%s)") % (repr(x),)) |
512 | ||
513 | def do_setflow(self, l = ""): | |
514 | try: | |
47 | 515 | if not isinstance(l, str) or not len(l): |
15 | 516 | l = str(self.flow_slider.GetValue()) |
517 | else: | |
518 | l = l.lower() | |
519 | flow = int(l) | |
520 | if self.p.online: | |
521 | self.p.send_now("M221 S" + l) | |
522 | self.log(_("Setting print flow factor to %d%%.") % flow) | |
523 | else: | |
524 | self.logError(_("Printer is not online.")) | |
47 | 525 | except Exception as x: |
15 | 526 | self.logError(_("You must enter a flow. (%s)") % (repr(x),)) |
527 | ||
528 | def setbedgui(self, f): | |
529 | self.bsetpoint = f | |
530 | if self.display_gauges: self.bedtgauge.SetTarget(int(f)) | |
531 | if self.display_graph: wx.CallAfter(self.graph.SetBedTargetTemperature, int(f)) | |
532 | if f > 0: | |
533 | wx.CallAfter(self.btemp.SetValue, str(f)) | |
534 | self.set("last_bed_temperature", str(f)) | |
535 | wx.CallAfter(self.setboff.SetBackgroundColour, None) | |
536 | wx.CallAfter(self.setboff.SetForegroundColour, None) | |
537 | wx.CallAfter(self.setbbtn.SetBackgroundColour, "#FFAA66") | |
538 | wx.CallAfter(self.setbbtn.SetForegroundColour, "#660000") | |
539 | wx.CallAfter(self.btemp.SetBackgroundColour, "#FFDABB") | |
540 | else: | |
541 | wx.CallAfter(self.setboff.SetBackgroundColour, "#0044CC") | |
542 | wx.CallAfter(self.setboff.SetForegroundColour, "white") | |
543 | wx.CallAfter(self.setbbtn.SetBackgroundColour, None) | |
544 | wx.CallAfter(self.setbbtn.SetForegroundColour, None) | |
545 | wx.CallAfter(self.btemp.SetBackgroundColour, "white") | |
546 | wx.CallAfter(self.btemp.Refresh) | |
547 | ||
548 | def sethotendgui(self, f): | |
549 | self.hsetpoint = f | |
550 | if self.display_gauges: self.hottgauge.SetTarget(int(f)) | |
551 | if self.display_graph: wx.CallAfter(self.graph.SetExtruder0TargetTemperature, int(f)) | |
552 | if f > 0: | |
553 | wx.CallAfter(self.htemp.SetValue, str(f)) | |
554 | self.set("last_temperature", str(f)) | |
555 | wx.CallAfter(self.settoff.SetBackgroundColour, None) | |
556 | wx.CallAfter(self.settoff.SetForegroundColour, None) | |
557 | wx.CallAfter(self.settbtn.SetBackgroundColour, "#FFAA66") | |
558 | wx.CallAfter(self.settbtn.SetForegroundColour, "#660000") | |
559 | wx.CallAfter(self.htemp.SetBackgroundColour, "#FFDABB") | |
560 | else: | |
561 | wx.CallAfter(self.settoff.SetBackgroundColour, "#0044CC") | |
562 | wx.CallAfter(self.settoff.SetForegroundColour, "white") | |
563 | wx.CallAfter(self.settbtn.SetBackgroundColour, None) | |
564 | wx.CallAfter(self.settbtn.SetForegroundColour, None) | |
565 | wx.CallAfter(self.htemp.SetBackgroundColour, "white") | |
566 | wx.CallAfter(self.htemp.Refresh) | |
567 | ||
568 | def rescanports(self, event = None): | |
569 | scanned = self.scanserial() | |
570 | portslist = list(scanned) | |
571 | if self.settings.port != "" and self.settings.port not in portslist: | |
572 | portslist.append(self.settings.port) | |
573 | self.serialport.Clear() | |
574 | self.serialport.AppendItems(portslist) | |
575 | if os.path.exists(self.settings.port) or self.settings.port in scanned: | |
576 | self.serialport.SetValue(self.settings.port) | |
577 | elif portslist: | |
578 | self.serialport.SetValue(portslist[0]) | |
579 | ||
47 | 580 | def appendCommandHistory(self): |
581 | cmd = self.commandbox.Value | |
582 | hist = self.commandbox.history | |
583 | append = cmd and (not hist or hist[-1] != cmd) | |
584 | if append: | |
585 | self.commandbox.history.append(cmd) | |
586 | return append | |
587 | ||
15 | 588 | def cbkey(self, e): |
47 | 589 | dir = {wx.WXK_UP: -1, wx.WXK_DOWN: 1}.get(e.KeyCode) |
590 | if dir: | |
15 | 591 | if self.commandbox.histindex == len(self.commandbox.history): |
47 | 592 | if dir == 1: |
593 | # do not cycle top => bottom | |
594 | return | |
595 | #save unsent command before going back | |
596 | self.appendCommandHistory() | |
597 | self.commandbox.histindex = max(0, min(self.commandbox.histindex + dir, len(self.commandbox.history))) | |
598 | self.commandbox.Value = (self.commandbox.history[self.commandbox.histindex] | |
599 | if self.commandbox.histindex < len(self.commandbox.history) | |
600 | else '') | |
601 | self.commandbox.SetInsertionPointEnd() | |
15 | 602 | else: |
603 | e.Skip() | |
604 | ||
605 | def plate(self, e): | |
606 | from . import stlplater as plater | |
607 | self.log(_("Plate function activated")) | |
608 | plater.StlPlater(size = (800, 580), callback = self.platecb, | |
609 | parent = self, | |
610 | build_dimensions = self.build_dimensions_list, | |
611 | circular_platform = self.settings.circular_bed, | |
612 | simarrange_path = self.settings.simarrange_path, | |
613 | antialias_samples = int(self.settings.antialias3dsamples)).Show() | |
614 | ||
615 | def plate_gcode(self, e): | |
616 | from . import gcodeplater as plater | |
617 | self.log(_("G-Code plate function activated")) | |
618 | plater.GcodePlater(size = (800, 580), callback = self.platecb, | |
619 | parent = self, | |
620 | build_dimensions = self.build_dimensions_list, | |
621 | circular_platform = self.settings.circular_bed, | |
622 | antialias_samples = int(self.settings.antialias3dsamples)).Show() | |
623 | ||
624 | def platecb(self, name): | |
625 | self.log(_("Plated %s") % name) | |
626 | self.loadfile(None, name) | |
627 | if self.settings.uimode in (_("Tabbed"), _("Tabbed with platers")): | |
628 | # Switch to page 1 (Status tab) | |
629 | self.notebook.SetSelection(1) | |
630 | ||
631 | def do_editgcode(self, e = None): | |
632 | if self.filename is not None: | |
633 | MacroEditor(self.filename, [line.raw for line in self.fgcode], self.doneediting, True) | |
634 | ||
635 | def doneediting(self, gcode): | |
636 | open(self.filename, "w").write("\n".join(gcode)) | |
637 | wx.CallAfter(self.loadfile, None, self.filename) | |
638 | ||
639 | def sdmenu(self, e): | |
640 | obj = e.GetEventObject() | |
641 | popupmenu = wx.Menu() | |
642 | item = popupmenu.Append(-1, _("SD Upload")) | |
643 | if not self.fgcode: | |
644 | item.Enable(False) | |
645 | self.Bind(wx.EVT_MENU, self.upload, id = item.GetId()) | |
646 | item = popupmenu.Append(-1, _("SD Print")) | |
647 | self.Bind(wx.EVT_MENU, self.sdprintfile, id = item.GetId()) | |
648 | self.panel.PopupMenu(popupmenu, obj.GetPosition()) | |
649 | ||
650 | def htemp_change(self, event): | |
651 | if self.hsetpoint > 0: | |
652 | self.do_settemp("") | |
653 | wx.CallAfter(self.htemp.SetInsertionPoint, 0) | |
654 | ||
655 | def btemp_change(self, event): | |
656 | if self.bsetpoint > 0: | |
657 | self.do_bedtemp("") | |
658 | wx.CallAfter(self.btemp.SetInsertionPoint, 0) | |
659 | ||
660 | def tool_change(self, event): | |
661 | self.do_tool(self.extrudersel.GetValue()) | |
662 | ||
663 | def show_viz_window(self, event): | |
664 | if self.fgcode: | |
665 | self.gwindow.Show(True) | |
666 | self.gwindow.SetToolTip(wx.ToolTip("Mousewheel zooms the display\nShift / Mousewheel scrolls layers")) | |
667 | self.gwindow.Raise() | |
668 | ||
669 | def setfeeds(self, e): | |
670 | self.feedrates_changed = True | |
671 | try: | |
672 | if self.efeedc is not None: | |
673 | self.settings._set("e_feedrate", self.efeedc.GetValue()) | |
674 | except: | |
675 | pass | |
676 | try: | |
677 | self.settings._set("z_feedrate", self.zfeedc.GetValue()) | |
678 | except: | |
679 | pass | |
680 | try: | |
681 | self.settings._set("xy_feedrate", self.xyfeedc.GetValue()) | |
682 | except: | |
683 | pass | |
684 | try: | |
685 | self.settings._set("last_extrusion", self.edist.GetValue()) | |
686 | except: | |
687 | pass | |
688 | ||
689 | def homeButtonClicked(self, axis): | |
690 | # When user clicks on the XY control, the Z control no longer gets spacebar/repeat signals | |
691 | self.zb.clearRepeat() | |
692 | if axis == "x": | |
693 | self.onecmd('home X') | |
694 | elif axis == "y": # upper-right | |
695 | self.onecmd('home Y') | |
696 | elif axis == "z": | |
697 | self.onecmd('home Z') | |
698 | elif axis == "all": | |
699 | self.onecmd('home') | |
700 | elif axis == "center": | |
701 | center_x = self.build_dimensions_list[0] / 2 + self.build_dimensions_list[3] | |
702 | center_y = self.build_dimensions_list[1] / 2 + self.build_dimensions_list[4] | |
703 | feed = self.settings.xy_feedrate | |
704 | self.onecmd('G0 X%s Y%s F%s' % (center_x, center_y, feed)) | |
705 | else: | |
706 | return | |
707 | self.p.send_now('M114') | |
708 | ||
709 | def clamped_move_message(self): | |
710 | self.log(_("Manual move outside of the build volume prevented (see the \"Clamp manual moves\" option).")) | |
711 | ||
712 | def moveXY(self, x, y): | |
713 | # When user clicks on the XY control, the Z control no longer gets spacebar/repeat signals | |
714 | self.zb.clearRepeat() | |
715 | if x != 0: | |
716 | if self.settings.clamp_jogging: | |
717 | new_x = self.current_pos[0] + x | |
718 | if new_x < self.build_dimensions_list[3] or new_x > self.build_dimensions_list[0] + self.build_dimensions_list[3]: | |
719 | self.clamped_move_message() | |
720 | return | |
721 | self.onecmd('move X %s' % x) | |
722 | elif y != 0: | |
723 | if self.settings.clamp_jogging: | |
724 | new_y = self.current_pos[1] + y | |
725 | if new_y < self.build_dimensions_list[4] or new_y > self.build_dimensions_list[1] + self.build_dimensions_list[4]: | |
726 | self.clamped_move_message() | |
727 | return | |
728 | self.onecmd('move Y %s' % y) | |
729 | else: | |
730 | return | |
731 | self.p.send_now('M114') | |
732 | ||
733 | def moveZ(self, z): | |
734 | if z != 0: | |
735 | if self.settings.clamp_jogging: | |
736 | new_z = self.current_pos[2] + z | |
737 | if new_z < self.build_dimensions_list[5] or new_z > self.build_dimensions_list[2] + self.build_dimensions_list[5]: | |
738 | self.clamped_move_message() | |
739 | return | |
740 | self.onecmd('move Z %s' % z) | |
741 | self.p.send_now('M114') | |
742 | # When user clicks on the Z control, the XY control no longer gets spacebar/repeat signals | |
743 | self.xyb.clearRepeat() | |
744 | ||
745 | def spacebarAction(self): | |
746 | self.zb.repeatLast() | |
747 | self.xyb.repeatLast() | |
748 | ||
749 | # -------------------------------------------------------------- | |
750 | # Console handling | |
751 | # -------------------------------------------------------------- | |
752 | ||
753 | def catchprint(self, l): | |
754 | """Called by the Tee operator to write to the log box""" | |
755 | if not self.IsFrozen(): | |
756 | wx.CallAfter(self.addtexttolog, l) | |
757 | ||
758 | def addtexttolog(self, text): | |
759 | try: | |
760 | max_length = 20000 | |
761 | current_length = self.logbox.GetLastPosition() | |
762 | if current_length > max_length: | |
763 | self.logbox.Remove(0, current_length / 10) | |
47 | 764 | currentCaretPosition = self.logbox.GetInsertionPoint() |
765 | currentLengthOfText = self.logbox.GetLastPosition() | |
766 | if self.autoscrolldisable: | |
767 | self.logbox.Freeze() | |
768 | currentSelectionStart, currentSelectionEnd = self.logbox.GetSelection() | |
769 | self.logbox.SetInsertionPointEnd() | |
770 | self.logbox.AppendText(text) | |
771 | self.logbox.SetInsertionPoint(currentCaretPosition) | |
772 | self.logbox.SetSelection(currentSelectionStart, currentSelectionEnd) | |
773 | self.logbox.Thaw() | |
774 | else: | |
775 | self.logbox.SetInsertionPointEnd() | |
776 | self.logbox.AppendText(text) | |
777 | ||
15 | 778 | except: |
779 | self.log(_("Attempted to write invalid text to console, which could be due to an invalid baudrate")) | |
780 | ||
781 | def clear_log(self, e): | |
782 | self.logbox.Clear() | |
783 | ||
784 | def set_verbose_communications(self, e): | |
785 | self.p.loud = e.IsChecked() | |
786 | ||
47 | 787 | def set_autoscrolldisable(self,e): |
788 | self.autoscrolldisable = e.IsChecked() | |
789 | ||
15 | 790 | def sendline(self, e): |
47 | 791 | command = self.commandbox.Value |
15 | 792 | if not len(command): |
793 | return | |
47 | 794 | logging.info(">>> " + command) |
15 | 795 | line = self.precmd(str(command)) |
796 | self.onecmd(line) | |
47 | 797 | self.appendCommandHistory() |
15 | 798 | self.commandbox.histindex = len(self.commandbox.history) |
47 | 799 | self.commandbox.Value = '' |
15 | 800 | |
801 | # -------------------------------------------------------------- | |
802 | # Main menu handling & actions | |
803 | # -------------------------------------------------------------- | |
804 | ||
805 | def create_menu(self): | |
806 | """Create main menu""" | |
47 | 807 | |
15 | 808 | # File menu |
809 | m = wx.Menu() | |
47 | 810 | self.Bind(wx.EVT_MENU, self.loadfile, m.Append(-1, _("&Open...\tCtrl+O"), _(" Open file"))) |
15 | 811 | self.savebtn = m.Append(-1, _("&Save..."), _(" Save file")) |
812 | self.savebtn.Enable(False) | |
813 | self.Bind(wx.EVT_MENU, self.savefile, self.savebtn) | |
814 | ||
815 | self.filehistory = wx.FileHistory(maxFiles = 8, idBase = wx.ID_FILE1) | |
816 | recent = wx.Menu() | |
817 | self.filehistory.UseMenu(recent) | |
818 | self.Bind(wx.EVT_MENU_RANGE, self.load_recent_file, | |
819 | id = wx.ID_FILE1, id2 = wx.ID_FILE9) | |
47 | 820 | m.Append(wx.ID_ANY, _("&Recent Files"), recent) |
821 | self.Bind(wx.EVT_MENU, self.clear_log, m.Append(-1, _("Clear console\tCtrl+L"), _(" Clear output console"))) | |
15 | 822 | self.Bind(wx.EVT_MENU, self.on_exit, m.Append(wx.ID_EXIT, _("E&xit"), _(" Closes the Window"))) |
823 | self.menustrip.Append(m, _("&File")) | |
824 | ||
47 | 825 | # Tools Menu |
15 | 826 | m = wx.Menu() |
827 | self.Bind(wx.EVT_MENU, self.do_editgcode, m.Append(-1, _("&Edit..."), _(" Edit open file"))) | |
828 | self.Bind(wx.EVT_MENU, self.plate, m.Append(-1, _("Plater"), _(" Compose 3D models into a single plate"))) | |
829 | self.Bind(wx.EVT_MENU, self.plate_gcode, m.Append(-1, _("G-Code Plater"), _(" Compose G-Codes into a single plate"))) | |
830 | self.Bind(wx.EVT_MENU, self.exclude, m.Append(-1, _("Excluder"), _(" Exclude parts of the bed from being printed"))) | |
831 | self.Bind(wx.EVT_MENU, self.project, m.Append(-1, _("Projector"), _(" Project slices"))) | |
47 | 832 | self.Bind(wx.EVT_MENU, |
833 | self.show_spool_manager, | |
834 | m.Append(-1, _("Spool Manager"), | |
835 | _(" Manage different spools of filament"))) | |
15 | 836 | self.menustrip.Append(m, _("&Tools")) |
837 | ||
47 | 838 | # Advanced Menu |
15 | 839 | m = wx.Menu() |
840 | self.recoverbtn = m.Append(-1, _("Recover"), _(" Recover previous print after a disconnect (homes X, Y, restores Z and E status)")) | |
841 | self.recoverbtn.Disable = lambda *a: self.recoverbtn.Enable(False) | |
842 | self.Bind(wx.EVT_MENU, self.recover, self.recoverbtn) | |
843 | self.menustrip.Append(m, _("&Advanced")) | |
844 | ||
845 | if self.settings.slic3rintegration: | |
846 | m = wx.Menu() | |
847 | print_menu = wx.Menu() | |
848 | filament_menu = wx.Menu() | |
849 | printer_menu = wx.Menu() | |
850 | m.AppendSubMenu(print_menu, _("Print &settings")) | |
851 | m.AppendSubMenu(filament_menu, _("&Filament")) | |
852 | m.AppendSubMenu(printer_menu, _("&Printer")) | |
853 | menus = {"print": print_menu, | |
854 | "filament": filament_menu, | |
855 | "printer": printer_menu} | |
856 | try: | |
857 | self.load_slic3r_configs(menus) | |
858 | self.menustrip.Append(m, _("&Slic3r")) | |
859 | except IOError: | |
860 | self.logError(_("Failed to load Slic3r configuration:") + | |
861 | "\n" + traceback.format_exc()) | |
862 | ||
863 | # Settings menu | |
864 | m = wx.Menu() | |
865 | self.macros_menu = wx.Menu() | |
866 | m.AppendSubMenu(self.macros_menu, _("&Macros")) | |
867 | self.Bind(wx.EVT_MENU, self.new_macro, self.macros_menu.Append(-1, _("<&New...>"))) | |
868 | self.Bind(wx.EVT_MENU, lambda *e: PronterOptions(self), m.Append(-1, _("&Options"), _(" Options dialog"))) | |
869 | ||
870 | self.Bind(wx.EVT_MENU, lambda x: threading.Thread(target = lambda: self.do_slice("set")).start(), m.Append(-1, _("Slicing settings"), _(" Adjust slicing settings"))) | |
871 | ||
872 | mItem = m.AppendCheckItem(-1, _("Debug communications"), | |
873 | _("Print all G-code sent to and received from the printer.")) | |
874 | m.Check(mItem.GetId(), self.p.loud) | |
875 | self.Bind(wx.EVT_MENU, self.set_verbose_communications, mItem) | |
876 | ||
47 | 877 | mItem = m.AppendCheckItem(-1, _("Don't autoscroll"), |
878 | _("Disables automatic scrolling of the console when new text is added")) | |
879 | m.Check(mItem.GetId(), self.autoscrolldisable) | |
880 | self.Bind(wx.EVT_MENU, self.set_autoscrolldisable, mItem) | |
881 | ||
15 | 882 | self.menustrip.Append(m, _("&Settings")) |
883 | self.update_macros_menu() | |
884 | self.SetMenuBar(self.menustrip) | |
885 | ||
886 | m = wx.Menu() | |
887 | self.Bind(wx.EVT_MENU, self.about, | |
888 | m.Append(-1, _("&About Printrun"), _("Show about dialog"))) | |
889 | self.menustrip.Append(m, _("&Help")) | |
890 | ||
891 | def project(self, event): | |
892 | """Start Projector tool""" | |
893 | from printrun import projectlayer | |
894 | projectlayer.SettingsFrame(self, self.p).Show() | |
895 | ||
896 | def exclude(self, event): | |
897 | """Start part excluder tool""" | |
898 | if not self.fgcode: | |
899 | wx.CallAfter(self.statusbar.SetStatusText, _("No file loaded. Please use load first.")) | |
900 | return | |
901 | if not self.excluder: | |
902 | from .excluder import Excluder | |
903 | self.excluder = Excluder() | |
904 | self.excluder.pop_window(self.fgcode, bgcolor = self.bgcolor, | |
905 | build_dimensions = self.build_dimensions_list) | |
906 | ||
47 | 907 | def show_spool_manager(self, event): |
908 | """Show Spool Manager Window""" | |
909 | spoolmanager_gui.SpoolManagerMainWindow(self, self.spool_manager).Show() | |
910 | ||
15 | 911 | def about(self, event): |
912 | """Show about dialog""" | |
913 | ||
47 | 914 | info = wx.adv.AboutDialogInfo() |
15 | 915 | |
916 | info.SetIcon(wx.Icon(iconfile("pronterface.png"), wx.BITMAP_TYPE_PNG)) | |
917 | info.SetName('Printrun') | |
918 | info.SetVersion(printcore.__version__) | |
919 | ||
920 | description = _("Printrun is a pure Python 3D printing" | |
921 | " (and other types of CNC) host software.") | |
922 | ||
923 | description += "\n\n" + \ | |
924 | _("%.02fmm of filament have been extruded during prints") \ | |
925 | % self.settings.total_filament_used | |
926 | ||
927 | info.SetDescription(description) | |
47 | 928 | info.SetCopyright('(C) 2011 - 2020') |
15 | 929 | info.SetWebSite('https://github.com/kliment/Printrun') |
930 | ||
931 | licence = """\ | |
932 | Printrun is free software: you can redistribute it and/or modify it under the | |
933 | terms of the GNU General Public License as published by the Free Software | |
934 | Foundation, either version 3 of the License, or (at your option) any later | |
935 | version. | |
936 | ||
937 | Printrun is distributed in the hope that it will be useful, but WITHOUT ANY | |
938 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A | |
939 | PARTICULAR PURPOSE. See the GNU General Public License for more details. | |
940 | ||
941 | You should have received a copy of the GNU General Public License along with | |
942 | Printrun. If not, see <http://www.gnu.org/licenses/>.""" | |
943 | ||
944 | info.SetLicence(licence) | |
945 | info.AddDeveloper('Kliment Yanev') | |
946 | info.AddDeveloper('Guillaume Seguin') | |
947 | ||
47 | 948 | wx.adv.AboutBox(info) |
15 | 949 | |
950 | # -------------------------------------------------------------- | |
951 | # Settings & command line handling (including update callbacks) | |
952 | # -------------------------------------------------------------- | |
47 | 953 | |
15 | 954 | def _add_settings(self, size): |
955 | self.settings._add(BooleanSetting("monitor", True, _("Monitor printer status"), _("Regularly monitor printer temperatures (required to have functional temperature graph or gauges)"), "Printer"), self.update_monitor) | |
956 | self.settings._add(StringSetting("simarrange_path", "", _("Simarrange command"), _("Path to the simarrange binary to use in the STL plater"), "External")) | |
957 | self.settings._add(BooleanSetting("circular_bed", False, _("Circular build platform"), _("Draw a circular (or oval) build platform instead of a rectangular one"), "Printer"), self.update_bed_viz) | |
958 | self.settings._add(SpinSetting("extruders", 0, 1, 5, _("Extruders count"), _("Number of extruders"), "Printer")) | |
959 | self.settings._add(BooleanSetting("clamp_jogging", False, _("Clamp manual moves"), _("Prevent manual moves from leaving the specified build dimensions"), "Printer")) | |
47 | 960 | self.settings._add(BooleanSetting("display_progress_on_printer", False, _("Display progress on printer"), _("Show progress on printers display (sent via M117, might not be supported by all printers)"), "Printer")) |
961 | self.settings._add(SpinSetting("printer_progress_update_interval", 10., 0, 120, _("Printer progress update interval"), _("Interval in which pronterface sends the progress to the printer if enabled, in seconds"), "Printer")) | |
962 | self.settings._add(BooleanSetting("cutting_as_extrusion", True, _("Display cutting moves"), _("Show moves where spindle is active as printing moves"), "Printer")) | |
963 | self.settings._add(ComboSetting("uimode", _("Standard"), [_("Standard"), _("Compact"), ], _("Interface mode"), _("Standard interface is a one-page, three columns layout with controls/visualization/log\nCompact mode is a one-page, two columns layout with controls + log/visualization"), "UI"), self.reload_ui) | |
964 | #self.settings._add(ComboSetting("uimode", _("Standard"), [_("Standard"), _("Compact"), _("Tabbed"), _("Tabbed with platers")], _("Interface mode"), _("Standard interface is a one-page, three columns layout with controls/visualization/log\nCompact mode is a one-page, two columns layout with controls + log/visualization"), "UI"), self.reload_ui) | |
965 | self.settings._add(ComboSetting("controlsmode", "Standard", ("Standard", "Mini"), _("Controls mode"), _("Standard controls include all controls needed for printer setup and calibration, while Mini controls are limited to the ones needed for daily printing"), "UI"), self.reload_ui) | |
15 | 966 | self.settings._add(BooleanSetting("slic3rintegration", False, _("Enable Slic3r integration"), _("Add a menu to select Slic3r profiles directly from Pronterface"), "UI"), self.reload_ui) |
967 | self.settings._add(BooleanSetting("slic3rupdate", False, _("Update Slic3r default presets"), _("When selecting a profile in Slic3r integration menu, also save it as the default Slic3r preset"), "UI")) | |
47 | 968 | self.settings._add(ComboSetting("mainviz", "3D", ("2D", "3D", "None"), _("Main visualization"), _("Select visualization for main window."), "Viewer"), self.reload_ui) |
15 | 969 | self.settings._add(BooleanSetting("viz3d", False, _("Use 3D in GCode viewer window"), _("Use 3D mode instead of 2D layered mode in the visualization window"), "Viewer"), self.reload_ui) |
970 | self.settings._add(StaticTextSetting("separator_3d_viewer", _("3D viewer options"), "", group = "Viewer")) | |
971 | self.settings._add(BooleanSetting("light3d", False, _("Use a lighter 3D visualization"), _("Use a lighter visualization with simple lines instead of extruded paths for 3D viewer"), "Viewer"), self.reload_ui) | |
972 | self.settings._add(ComboSetting("antialias3dsamples", "0", ["0", "2", "4", "8"], _("Number of anti-aliasing samples"), _("Amount of anti-aliasing samples used in the 3D viewer"), "Viewer"), self.reload_ui) | |
973 | self.settings._add(BooleanSetting("trackcurrentlayer3d", False, _("Track current layer in main 3D view"), _("Track the currently printing layer in the main 3D visualization"), "Viewer")) | |
974 | self.settings._add(FloatSpinSetting("gcview_path_width", 0.4, 0.01, 2, _("Extrusion width for 3D viewer"), _("Width of printed path in 3D viewer"), "Viewer", increment = 0.05), self.update_gcview_params) | |
975 | self.settings._add(FloatSpinSetting("gcview_path_height", 0.3, 0.01, 2, _("Layer height for 3D viewer"), _("Height of printed path in 3D viewer"), "Viewer", increment = 0.05), self.update_gcview_params) | |
976 | self.settings._add(BooleanSetting("tempgraph", True, _("Display temperature graph"), _("Display time-lapse temperature graph"), "UI"), self.reload_ui) | |
977 | self.settings._add(BooleanSetting("tempgauges", False, _("Display temperature gauges"), _("Display graphical gauges for temperatures visualization"), "UI"), self.reload_ui) | |
978 | self.settings._add(BooleanSetting("lockbox", False, _("Display interface lock checkbox"), _("Display a checkbox that, when check, locks most of Pronterface"), "UI"), self.reload_ui) | |
979 | self.settings._add(BooleanSetting("lockonstart", False, _("Lock interface upon print start"), _("If lock checkbox is enabled, lock the interface when starting a print"), "UI")) | |
980 | self.settings._add(BooleanSetting("refreshwhenloading", True, _("Update UI during G-Code load"), _("Regularly update visualization during the load of a G-Code file"), "UI")) | |
981 | self.settings._add(HiddenSetting("last_window_width", size[0])) | |
982 | self.settings._add(HiddenSetting("last_window_height", size[1])) | |
983 | self.settings._add(HiddenSetting("last_window_maximized", False)) | |
984 | self.settings._add(HiddenSetting("last_sash_position", -1)) | |
985 | self.settings._add(HiddenSetting("last_bed_temperature", 0.0)) | |
47 | 986 | self.settings._add(HiddenSetting("last_file_path", "")) |
15 | 987 | self.settings._add(HiddenSetting("last_file_filter", 0)) |
988 | self.settings._add(HiddenSetting("last_temperature", 0.0)) | |
989 | self.settings._add(StaticTextSetting("separator_2d_viewer", _("2D viewer options"), "", group = "Viewer")) | |
990 | self.settings._add(FloatSpinSetting("preview_extrusion_width", 0.5, 0, 10, _("Preview extrusion width"), _("Width of Extrusion in Preview"), "Viewer", increment = 0.1), self.update_gviz_params) | |
991 | self.settings._add(SpinSetting("preview_grid_step1", 10., 0, 200, _("Fine grid spacing"), _("Fine Grid Spacing"), "Viewer"), self.update_gviz_params) | |
992 | self.settings._add(SpinSetting("preview_grid_step2", 50., 0, 200, _("Coarse grid spacing"), _("Coarse Grid Spacing"), "Viewer"), self.update_gviz_params) | |
47 | 993 | self.settings._add(ColorSetting("bgcolor", self._preferred_bgcolour_hex(), _("Background color"), _("Pronterface background color"), "Colors", isRGBA=False), self.reload_ui) |
994 | self.settings._add(ColorSetting("graph_color_background", "#FAFAC7", _("Graph background color"), _("Color of the temperature graph background"), "Colors", isRGBA=False), self.reload_ui) | |
995 | self.settings._add(ColorSetting("gcview_color_background", "#FAFAC7FF", _("3D view background color"), _("Color of the 3D view background"), "Colors"), self.update_gcview_colors) | |
996 | self.settings._add(ColorSetting("gcview_color_travel", "#99999999", _("3D view travel moves color"), _("Color of travel moves in 3D view"), "Colors"), self.update_gcview_colors) | |
997 | self.settings._add(ColorSetting("gcview_color_tool0", "#FF000099", _("3D view print moves color"), _("Color of print moves with tool 0 in 3D view"), "Colors"), self.update_gcview_colors) | |
998 | self.settings._add(ColorSetting("gcview_color_tool1", "#AC0DFF99", _("3D view tool 1 moves color"), _("Color of print moves with tool 1 in 3D view"), "Colors"), self.update_gcview_colors) | |
999 | self.settings._add(ColorSetting("gcview_color_tool2", "#FFCE0099", _("3D view tool 2 moves color"), _("Color of print moves with tool 2 in 3D view"), "Colors"), self.update_gcview_colors) | |
1000 | self.settings._add(ColorSetting("gcview_color_tool3", "#FF009F99", _("3D view tool 3 moves color"), _("Color of print moves with tool 3 in 3D view"), "Colors"), self.update_gcview_colors) | |
1001 | self.settings._add(ColorSetting("gcview_color_tool4", "#00FF8F99", _("3D view tool 4 moves color"), _("Color of print moves with tool 4 in 3D view"), "Colors"), self.update_gcview_colors) | |
1002 | self.settings._add(ColorSetting("gcview_color_printed", "#33BF0099", _("3D view printed moves color"), _("Color of printed moves in 3D view"), "Colors"), self.update_gcview_colors) | |
1003 | self.settings._add(ColorSetting("gcview_color_current", "#00E5FFCC", _("3D view current layer moves color"), _("Color of moves in current layer in 3D view"), "Colors"), self.update_gcview_colors) | |
1004 | self.settings._add(ColorSetting("gcview_color_current_printed", "#196600CC", _("3D view printed current layer moves color"), _("Color of already printed moves from current layer in 3D view"), "Colors"), self.update_gcview_colors) | |
15 | 1005 | self.settings._add(StaticTextSetting("note1", _("Note:"), _("Changing some of these settings might require a restart to get effect"), group = "UI")) |
1006 | recentfilessetting = StringSetting("recentfiles", "[]") | |
1007 | recentfilessetting.hidden = True | |
1008 | self.settings._add(recentfilessetting, self.update_recent_files) | |
1009 | ||
47 | 1010 | def _preferred_bgcolour_hex(self): |
1011 | id = wx.SYS_COLOUR_WINDOW \ | |
1012 | if platform.system() == 'Windows' \ | |
1013 | else wx.SYS_COLOUR_BACKGROUND | |
1014 | sys_bgcolour = wx.SystemSettings.GetColour(id) | |
1015 | return sys_bgcolour.GetAsString(flags=wx.C2S_HTML_SYNTAX) | |
1016 | ||
15 | 1017 | def add_cmdline_arguments(self, parser): |
1018 | pronsole.pronsole.add_cmdline_arguments(self, parser) | |
1019 | parser.add_argument('-a', '--autoconnect', help = _("automatically try to connect to printer on startup"), action = "store_true") | |
1020 | ||
1021 | def process_cmdline_arguments(self, args): | |
1022 | pronsole.pronsole.process_cmdline_arguments(self, args) | |
1023 | self.autoconnect = args.autoconnect | |
1024 | ||
1025 | def update_recent_files(self, param, value): | |
1026 | if self.filehistory is None: | |
1027 | return | |
1028 | recent_files = [] | |
1029 | try: | |
1030 | recent_files = json.loads(value) | |
1031 | except: | |
1032 | self.logError(_("Failed to load recent files list:") + | |
1033 | "\n" + traceback.format_exc()) | |
1034 | # Clear history | |
1035 | while self.filehistory.GetCount(): | |
1036 | self.filehistory.RemoveFileFromHistory(0) | |
1037 | recent_files.reverse() | |
1038 | for f in recent_files: | |
1039 | self.filehistory.AddFileToHistory(f) | |
1040 | ||
1041 | def update_gviz_params(self, param, value): | |
1042 | params_map = {"preview_extrusion_width": "extrusion_width", | |
1043 | "preview_grid_step1": "grid", | |
1044 | "preview_grid_step2": "grid"} | |
1045 | if param not in params_map: | |
1046 | return | |
1047 | if not hasattr(self, "gviz"): | |
1048 | # GUI hasn't been loaded yet, ignore this setting | |
1049 | return | |
1050 | trueparam = params_map[param] | |
1051 | if hasattr(self.gviz, trueparam): | |
1052 | gviz = self.gviz | |
1053 | elif hasattr(self.gwindow, "p") and hasattr(self.gwindow.p, trueparam): | |
1054 | gviz = self.gwindow.p | |
1055 | else: | |
1056 | return | |
1057 | if trueparam == "grid": | |
1058 | try: | |
1059 | item = int(param[-1]) # extract list item position | |
1060 | grid = list(gviz.grid) | |
1061 | grid[item - 1] = value | |
1062 | value = tuple(grid) | |
1063 | except: | |
1064 | self.logError(traceback.format_exc()) | |
1065 | if hasattr(self.gviz, trueparam): | |
1066 | self.apply_gviz_params(self.gviz, trueparam, value) | |
1067 | if hasattr(self.gwindow, "p") and hasattr(self.gwindow.p, trueparam): | |
1068 | self.apply_gviz_params(self.gwindow.p, trueparam, value) | |
1069 | ||
1070 | def apply_gviz_params(self, widget, param, value): | |
1071 | setattr(widget, param, value) | |
1072 | widget.dirty = 1 | |
1073 | wx.CallAfter(widget.Refresh) | |
1074 | ||
1075 | def update_gcview_colors(self, param, value): | |
47 | 1076 | if not self.window_ready: |
1077 | return | |
15 | 1078 | color = hexcolor_to_float(value, 4) |
1079 | # This is sort of a hack: we copy the color values into the preexisting | |
1080 | # color tuple so that we don't need to update the tuple used by gcview | |
1081 | target_color = getattr(self, param) | |
1082 | for i, v in enumerate(color): | |
1083 | target_color[i] = v | |
1084 | wx.CallAfter(self.Refresh) | |
1085 | ||
1086 | def update_build_dimensions(self, param, value): | |
1087 | pronsole.pronsole.update_build_dimensions(self, param, value) | |
1088 | self.update_bed_viz() | |
1089 | ||
1090 | def update_bed_viz(self, *args): | |
1091 | """Update bed visualization when size/type changed""" | |
1092 | if hasattr(self, "gviz") and hasattr(self.gviz, "recreate_platform"): | |
47 | 1093 | self.gviz.recreate_platform(self.build_dimensions_list, self.settings.circular_bed, |
1094 | grid = (self.settings.preview_grid_step1, self.settings.preview_grid_step2)) | |
15 | 1095 | if hasattr(self, "gwindow") and hasattr(self.gwindow, "recreate_platform"): |
47 | 1096 | self.gwindow.recreate_platform(self.build_dimensions_list, self.settings.circular_bed, |
1097 | grid = (self.settings.preview_grid_step1, self.settings.preview_grid_step2)) | |
15 | 1098 | |
1099 | def update_gcview_params(self, *args): | |
1100 | need_reload = False | |
1101 | if hasattr(self, "gviz") and hasattr(self.gviz, "set_gcview_params"): | |
1102 | need_reload |= self.gviz.set_gcview_params(self.settings.gcview_path_width, self.settings.gcview_path_height) | |
1103 | if hasattr(self, "gwindow") and hasattr(self.gwindow, "set_gcview_params"): | |
1104 | need_reload |= self.gwindow.set_gcview_params(self.settings.gcview_path_width, self.settings.gcview_path_height) | |
1105 | if need_reload: | |
1106 | self.start_viz_thread() | |
1107 | ||
1108 | def update_monitor(self, *args): | |
1109 | if hasattr(self, "graph") and self.display_graph: | |
1110 | if self.settings.monitor: | |
1111 | wx.CallAfter(self.graph.StartPlotting, 1000) | |
1112 | else: | |
1113 | wx.CallAfter(self.graph.StopPlotting) | |
1114 | ||
1115 | # -------------------------------------------------------------- | |
1116 | # Statusbar handling | |
1117 | # -------------------------------------------------------------- | |
1118 | ||
1119 | def statuschecker_inner(self): | |
1120 | status_string = "" | |
1121 | if self.sdprinting or self.uploading or self.p.printing: | |
1122 | secondsremain, secondsestimate, progress = self.get_eta() | |
1123 | if self.sdprinting or self.uploading: | |
1124 | if self.uploading: | |
1125 | status_string += _("SD upload: %04.2f%% |") % (100 * progress,) | |
1126 | status_string += _(" Line# %d of %d lines |") % (self.p.queueindex, len(self.p.mainqueue)) | |
1127 | else: | |
1128 | status_string += _("SD printing: %04.2f%% |") % (self.percentdone,) | |
1129 | elif self.p.printing: | |
1130 | status_string += _("Printing: %04.2f%% |") % (100 * float(self.p.queueindex) / len(self.p.mainqueue),) | |
1131 | status_string += _(" Line# %d of %d lines |") % (self.p.queueindex, len(self.p.mainqueue)) | |
1132 | if progress > 0: | |
1133 | status_string += _(" Est: %s of %s remaining | ") % (format_duration(secondsremain), | |
1134 | format_duration(secondsestimate)) | |
1135 | status_string += _(" Z: %.3f mm") % self.curlayer | |
47 | 1136 | if self.settings.display_progress_on_printer and time.time() - self.printer_progress_time >= self.settings.printer_progress_update_interval: |
1137 | self.printer_progress_time = time.time() | |
1138 | printer_progress_string = "M117 " + str(round(100 * float(self.p.queueindex) / len(self.p.mainqueue), 2)) + "% Est " + format_duration(secondsremain) | |
1139 | #":" seems to be some kind of seperator for G-CODE" | |
1140 | self.p.send_now(printer_progress_string.replace(":", ".")) | |
1141 | if len(printer_progress_string) > 25: | |
1142 | logging.info("Warning: The print progress message might be too long to be displayed properly") | |
1143 | #13 chars for up to 99h est. | |
15 | 1144 | elif self.loading_gcode: |
1145 | status_string = self.loading_gcode_message | |
1146 | wx.CallAfter(self.statusbar.SetStatusText, status_string) | |
1147 | wx.CallAfter(self.gviz.Refresh) | |
1148 | # Call pronsole's statuschecker inner loop function to handle | |
1149 | # temperature monitoring and status loop sleep | |
1150 | pronsole.pronsole.statuschecker_inner(self, self.settings.monitor) | |
1151 | try: | |
1152 | while not self.sentglines.empty(): | |
1153 | gc = self.sentglines.get_nowait() | |
1154 | wx.CallAfter(self.gviz.addgcodehighlight, gc) | |
1155 | self.sentglines.task_done() | |
47 | 1156 | except queue.Empty: |
15 | 1157 | pass |
1158 | ||
1159 | def statuschecker(self): | |
1160 | pronsole.pronsole.statuschecker(self) | |
1161 | wx.CallAfter(self.statusbar.SetStatusText, _("Not connected to printer.")) | |
1162 | ||
1163 | # -------------------------------------------------------------- | |
1164 | # Interface lock handling | |
1165 | # -------------------------------------------------------------- | |
1166 | ||
1167 | def lock(self, event = None, force = None): | |
1168 | if force is not None: | |
1169 | self.locker.SetValue(force) | |
1170 | if self.locker.GetValue(): | |
1171 | self.log(_("Locking interface.")) | |
1172 | for panel in self.panels: | |
1173 | panel.Disable() | |
1174 | else: | |
1175 | self.log(_("Unlocking interface.")) | |
1176 | for panel in self.panels: | |
1177 | panel.Enable() | |
1178 | ||
1179 | # -------------------------------------------------------------- | |
1180 | # Printer connection handling | |
1181 | # -------------------------------------------------------------- | |
1182 | ||
47 | 1183 | def connectbtn_cb(self, event): |
1184 | # Implement toggle behavior with a single Bind | |
1185 | # and switched variable, so we have reference to | |
1186 | # the actual callback to use in on_key | |
1187 | self.connectbtn_cb_var() | |
1188 | ||
15 | 1189 | def connect(self, event = None): |
1190 | self.log(_("Connecting...")) | |
1191 | port = None | |
1192 | if self.serialport.GetValue(): | |
1193 | port = str(self.serialport.GetValue()) | |
1194 | else: | |
1195 | scanned = self.scanserial() | |
1196 | if scanned: | |
1197 | port = scanned[0] | |
1198 | baud = 115200 | |
1199 | try: | |
1200 | baud = int(self.baud.GetValue()) | |
1201 | except: | |
1202 | self.logError(_("Could not parse baud rate: ") | |
1203 | + "\n" + traceback.format_exc()) | |
1204 | if self.paused: | |
1205 | self.p.paused = 0 | |
1206 | self.p.printing = 0 | |
47 | 1207 | wx.CallAfter(self.pausebtn.SetLabel, _("&Pause")) |
1208 | wx.CallAfter(self.printbtn.SetLabel, _("&Print")) | |
15 | 1209 | wx.CallAfter(self.toolbarsizer.Layout) |
1210 | self.paused = 0 | |
1211 | if self.sdprinting: | |
1212 | self.p.send_now("M26 S0") | |
1213 | if not self.connect_to_printer(port, baud, self.settings.dtr): | |
1214 | return | |
1215 | if port != self.settings.port: | |
1216 | self.set("port", port) | |
1217 | if baud != self.settings.baudrate: | |
1218 | self.set("baudrate", str(baud)) | |
1219 | if self.predisconnect_mainqueue: | |
1220 | self.recoverbtn.Enable() | |
1221 | ||
1222 | def store_predisconnect_state(self): | |
1223 | self.predisconnect_mainqueue = self.p.mainqueue | |
1224 | self.predisconnect_queueindex = self.p.queueindex | |
1225 | self.predisconnect_layer = self.curlayer | |
1226 | ||
1227 | def disconnect(self, event = None): | |
1228 | self.log(_("Disconnected.")) | |
1229 | if self.p.printing or self.p.paused or self.paused: | |
1230 | self.store_predisconnect_state() | |
1231 | self.p.disconnect() | |
1232 | self.statuscheck = False | |
1233 | if self.status_thread: | |
1234 | self.status_thread.join() | |
1235 | self.status_thread = None | |
1236 | ||
47 | 1237 | def toggle(): |
1238 | self.connectbtn.SetLabel(_("&Connect")) | |
1239 | self.connectbtn.SetToolTip(wx.ToolTip(_("Connect to the printer"))) | |
1240 | self.connectbtn_cb_var = self.connect | |
1241 | self.gui_set_disconnected() | |
1242 | wx.CallAfter(toggle) | |
15 | 1243 | |
1244 | if self.paused: | |
1245 | self.p.paused = 0 | |
1246 | self.p.printing = 0 | |
47 | 1247 | wx.CallAfter(self.pausebtn.SetLabel, _("&Pause")) |
1248 | wx.CallAfter(self.printbtn.SetLabel, _("&Print")) | |
15 | 1249 | self.paused = 0 |
1250 | if self.sdprinting: | |
1251 | self.p.send_now("M26 S0") | |
1252 | ||
1253 | # Relayout the toolbar to handle new buttons size | |
1254 | wx.CallAfter(self.toolbarsizer.Layout) | |
1255 | ||
1256 | def reset(self, event): | |
1257 | self.log(_("Reset.")) | |
1258 | dlg = wx.MessageDialog(self, _("Are you sure you want to reset the printer?"), _("Reset?"), wx.YES | wx.NO) | |
1259 | if dlg.ShowModal() == wx.ID_YES: | |
1260 | self.p.reset() | |
1261 | self.sethotendgui(0) | |
1262 | self.setbedgui(0) | |
1263 | self.p.printing = 0 | |
47 | 1264 | wx.CallAfter(self.printbtn.SetLabel, _("&Print")) |
15 | 1265 | if self.paused: |
1266 | self.p.paused = 0 | |
47 | 1267 | wx.CallAfter(self.pausebtn.SetLabel, _("&Pause")) |
15 | 1268 | self.paused = 0 |
1269 | wx.CallAfter(self.toolbarsizer.Layout) | |
1270 | dlg.Destroy() | |
1271 | ||
1272 | # -------------------------------------------------------------- | |
1273 | # Print/upload handling | |
1274 | # -------------------------------------------------------------- | |
1275 | ||
1276 | def on_startprint(self): | |
47 | 1277 | wx.CallAfter(self.pausebtn.SetLabel, _("&Pause")) |
15 | 1278 | wx.CallAfter(self.pausebtn.Enable) |
1279 | wx.CallAfter(self.printbtn.SetLabel, _("Restart")) | |
1280 | wx.CallAfter(self.toolbarsizer.Layout) | |
1281 | ||
47 | 1282 | def printfile(self, event=None): |
15 | 1283 | self.extra_print_time = 0 |
1284 | if self.paused: | |
1285 | self.p.paused = 0 | |
1286 | self.paused = 0 | |
1287 | if self.sdprinting: | |
1288 | self.on_startprint() | |
1289 | self.p.send_now("M26 S0") | |
1290 | self.p.send_now("M24") | |
1291 | return | |
1292 | ||
1293 | if not self.fgcode: | |
1294 | wx.CallAfter(self.statusbar.SetStatusText, _("No file loaded. Please use load first.")) | |
1295 | return | |
1296 | if not self.p.online: | |
1297 | wx.CallAfter(self.statusbar.SetStatusText, _("Not connected to printer.")) | |
1298 | return | |
1299 | self.sdprinting = False | |
1300 | self.on_startprint() | |
1301 | self.p.startprint(self.fgcode) | |
1302 | ||
1303 | def sdprintfile(self, event): | |
1304 | self.extra_print_time = 0 | |
1305 | self.on_startprint() | |
1306 | threading.Thread(target = self.getfiles).start() | |
1307 | ||
1308 | def upload(self, event): | |
1309 | if not self.fgcode: | |
1310 | return | |
1311 | if not self.p.online: | |
1312 | return | |
1313 | dlg = wx.TextEntryDialog(self, ("Enter a target filename in 8.3 format:"), _("Pick SD filename"), dosify(self.filename)) | |
1314 | if dlg.ShowModal() == wx.ID_OK: | |
1315 | self.p.send_now("M21") | |
1316 | self.p.send_now("M28 " + str(dlg.GetValue())) | |
1317 | self.recvlisteners.append(self.uploadtrigger) | |
1318 | dlg.Destroy() | |
1319 | ||
1320 | def uploadtrigger(self, l): | |
1321 | if "Writing to file" in l: | |
1322 | self.uploading = True | |
1323 | self.p.startprint(self.fgcode) | |
1324 | self.p.endcb = self.endupload | |
1325 | self.recvlisteners.remove(self.uploadtrigger) | |
1326 | elif "open failed, File" in l: | |
1327 | self.recvlisteners.remove(self.uploadtrigger) | |
1328 | ||
1329 | def endupload(self): | |
1330 | self.p.send_now("M29 ") | |
1331 | wx.CallAfter(self.statusbar.SetStatusText, _("File upload complete")) | |
1332 | time.sleep(0.5) | |
1333 | self.p.clear = True | |
1334 | self.uploading = False | |
1335 | ||
1336 | def pause(self, event = None): | |
1337 | if not self.paused: | |
1338 | self.log(_("Print paused at: %s") % format_time(time.time())) | |
47 | 1339 | if self.settings.display_progress_on_printer: |
1340 | printer_progress_string = "M117 PausedInPronterface" | |
1341 | self.p.send_now(printer_progress_string) | |
15 | 1342 | if self.sdprinting: |
1343 | self.p.send_now("M25") | |
1344 | else: | |
1345 | if not self.p.printing: | |
1346 | return | |
1347 | self.p.pause() | |
1348 | self.p.runSmallScript(self.pauseScript) | |
1349 | self.paused = True | |
1350 | # self.p.runSmallScript(self.pauseScript) | |
1351 | self.extra_print_time += int(time.time() - self.starttime) | |
1352 | wx.CallAfter(self.pausebtn.SetLabel, _("Resume")) | |
1353 | wx.CallAfter(self.toolbarsizer.Layout) | |
1354 | else: | |
1355 | self.log(_("Resuming.")) | |
47 | 1356 | if self.settings.display_progress_on_printer: |
1357 | printer_progress_string = "M117 Resuming" | |
1358 | self.p.send_now(printer_progress_string) | |
15 | 1359 | self.paused = False |
1360 | if self.sdprinting: | |
1361 | self.p.send_now("M24") | |
1362 | else: | |
1363 | self.p.resume() | |
47 | 1364 | wx.CallAfter(self.pausebtn.SetLabel, _("&Pause")) |
15 | 1365 | wx.CallAfter(self.toolbarsizer.Layout) |
1366 | ||
1367 | def recover(self, event): | |
1368 | self.extra_print_time = 0 | |
1369 | if not self.p.online: | |
1370 | wx.CallAfter(self.statusbar.SetStatusText, _("Not connected to printer.")) | |
1371 | return | |
1372 | # Reset Z | |
1373 | self.p.send_now("G92 Z%f" % self.predisconnect_layer) | |
1374 | # Home X and Y | |
1375 | self.p.send_now("G28 X Y") | |
1376 | self.on_startprint() | |
1377 | self.p.startprint(self.predisconnect_mainqueue, self.p.queueindex) | |
1378 | ||
1379 | # -------------------------------------------------------------- | |
1380 | # File loading handling | |
1381 | # -------------------------------------------------------------- | |
1382 | ||
1383 | def filesloaded(self): | |
1384 | dlg = wx.SingleChoiceDialog(self, _("Select the file to print"), _("Pick SD file"), self.sdfiles) | |
1385 | if dlg.ShowModal() == wx.ID_OK: | |
1386 | target = dlg.GetStringSelection() | |
1387 | if len(target): | |
1388 | self.recvlisteners.append(self.waitforsdresponse) | |
1389 | self.p.send_now("M23 " + target.lower()) | |
1390 | dlg.Destroy() | |
1391 | ||
1392 | def getfiles(self): | |
1393 | if not self.p.online: | |
1394 | self.sdfiles = [] | |
1395 | return | |
1396 | self.sdlisting = 0 | |
1397 | self.sdfiles = [] | |
1398 | self.recvlisteners.append(self.listfiles) | |
1399 | self.p.send_now("M21") | |
1400 | self.p.send_now("M20") | |
1401 | ||
1402 | def model_to_gcode_filename(self, filename): | |
1403 | suffix = "_export.gcode" | |
1404 | for ext in [".stl", ".obj"]: | |
1405 | filename = filename.replace(ext, suffix) | |
1406 | filename = filename.replace(ext.upper(), suffix) | |
1407 | return filename | |
1408 | ||
1409 | def slice_func(self): | |
1410 | try: | |
1411 | output_filename = self.model_to_gcode_filename(self.filename) | |
47 | 1412 | pararray = prepare_command(self.settings.slicecommandpath+self.settings.slicecommand, |
15 | 1413 | {"$s": self.filename, "$o": output_filename}) |
1414 | if self.settings.slic3rintegration: | |
1415 | for cat, config in self.slic3r_configs.items(): | |
1416 | if config: | |
1417 | fpath = os.path.join(self.slic3r_configpath, cat, config) | |
1418 | pararray += ["--load", fpath] | |
1419 | self.log(_("Running ") + " ".join(pararray)) | |
47 | 1420 | self.slicep = subprocess.Popen(pararray, stdin=subprocess.DEVNULL, stderr = subprocess.STDOUT, stdout = subprocess.PIPE, universal_newlines = True) |
15 | 1421 | while True: |
1422 | o = self.slicep.stdout.read(1) | |
1423 | if o == '' and self.slicep.poll() is not None: break | |
1424 | sys.stdout.write(o) | |
1425 | self.slicep.wait() | |
1426 | self.stopsf = 1 | |
1427 | except: | |
1428 | self.logError(_("Failed to execute slicing software: ") | |
1429 | + "\n" + traceback.format_exc()) | |
1430 | self.stopsf = 1 | |
1431 | ||
1432 | def slice_monitor(self): | |
1433 | while not self.stopsf: | |
1434 | try: | |
1435 | wx.CallAfter(self.statusbar.SetStatusText, _("Slicing...")) # +self.cout.getvalue().split("\n")[-1]) | |
1436 | except: | |
1437 | pass | |
1438 | time.sleep(0.1) | |
1439 | fn = self.filename | |
1440 | try: | |
1441 | self.load_gcode_async(self.model_to_gcode_filename(self.filename)) | |
1442 | except: | |
1443 | self.filename = fn | |
1444 | self.slicing = False | |
1445 | self.slicep = None | |
47 | 1446 | self.loadbtn.SetLabel, _("Load file") |
15 | 1447 | |
1448 | def slice(self, filename): | |
1449 | wx.CallAfter(self.loadbtn.SetLabel, _("Cancel")) | |
1450 | wx.CallAfter(self.toolbarsizer.Layout) | |
1451 | self.log(_("Slicing ") + filename) | |
1452 | self.cout = StringIO.StringIO() | |
1453 | self.filename = filename | |
1454 | self.stopsf = 0 | |
1455 | self.slicing = True | |
1456 | threading.Thread(target = self.slice_func).start() | |
1457 | threading.Thread(target = self.slice_monitor).start() | |
1458 | ||
1459 | def cmdline_filename_callback(self, filename): | |
1460 | # Do nothing when processing a filename from command line, as we'll | |
1461 | # handle it when everything has been prepared | |
1462 | self.filename = filename | |
1463 | ||
1464 | def do_load(self, l): | |
1465 | if hasattr(self, 'slicing'): | |
1466 | self.loadfile(None, l) | |
1467 | else: | |
1468 | self._do_load(l) | |
1469 | ||
1470 | def load_recent_file(self, event): | |
1471 | fileid = event.GetId() - wx.ID_FILE1 | |
1472 | path = self.filehistory.GetHistoryFile(fileid) | |
1473 | self.loadfile(None, filename = path) | |
1474 | ||
1475 | def loadfile(self, event, filename = None): | |
1476 | if self.slicing and self.slicep is not None: | |
1477 | self.slicep.terminate() | |
1478 | return | |
1479 | basedir = self.settings.last_file_path | |
1480 | if not os.path.exists(basedir): | |
1481 | basedir = "." | |
1482 | try: | |
1483 | basedir = os.path.split(self.filename)[0] | |
1484 | except: | |
1485 | pass | |
1486 | dlg = None | |
1487 | if filename is None: | |
1488 | dlg = wx.FileDialog(self, _("Open file to print"), basedir, style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) | |
47 | 1489 | dlg.SetWildcard(_("OBJ, STL, and GCODE files (*.gcode;*.gco;*.g;*.stl;*.STL;*.obj;*.OBJ)|*.gcode;*.gco;*.g;*.stl;*.STL;*.obj;*.OBJ|GCODE files (*.gcode;*.gco;*.g)|*.gcode;*.gco;*.g|OBJ, STL files (*.stl;*.STL;*.obj;*.OBJ)|*.stl;*.STL;*.obj;*.OBJ|All Files (*.*)|*.*")) |
15 | 1490 | try: |
1491 | dlg.SetFilterIndex(self.settings.last_file_filter) | |
1492 | except: | |
1493 | pass | |
1494 | if filename or dlg.ShowModal() == wx.ID_OK: | |
1495 | if filename: | |
1496 | name = filename | |
1497 | else: | |
1498 | name = dlg.GetPath() | |
1499 | self.set("last_file_filter", dlg.GetFilterIndex()) | |
1500 | dlg.Destroy() | |
1501 | if not os.path.exists(name): | |
1502 | self.statusbar.SetStatusText(_("File not found!")) | |
1503 | return | |
1504 | path = os.path.split(name)[0] | |
1505 | if path != self.settings.last_file_path: | |
1506 | self.set("last_file_path", path) | |
1507 | try: | |
1508 | abspath = os.path.abspath(name) | |
1509 | recent_files = [] | |
1510 | try: | |
1511 | recent_files = json.loads(self.settings.recentfiles) | |
1512 | except: | |
1513 | self.logError(_("Failed to load recent files list:") + | |
1514 | "\n" + traceback.format_exc()) | |
1515 | if abspath in recent_files: | |
1516 | recent_files.remove(abspath) | |
1517 | recent_files.insert(0, abspath) | |
1518 | if len(recent_files) > 5: | |
1519 | recent_files = recent_files[:5] | |
1520 | self.set("recentfiles", json.dumps(recent_files)) | |
1521 | except: | |
1522 | self.logError(_("Could not update recent files list:") + | |
1523 | "\n" + traceback.format_exc()) | |
1524 | if name.lower().endswith(".stl") or name.lower().endswith(".obj"): | |
1525 | self.slice(name) | |
1526 | else: | |
1527 | self.load_gcode_async(name) | |
1528 | else: | |
1529 | dlg.Destroy() | |
1530 | ||
1531 | def load_gcode_async(self, filename): | |
1532 | self.filename = filename | |
1533 | gcode = self.pre_gcode_load() | |
1534 | self.log(_("Loading file: %s") % filename) | |
1535 | threading.Thread(target = self.load_gcode_async_thread, args = (gcode,)).start() | |
1536 | ||
1537 | def load_gcode_async_thread(self, gcode): | |
1538 | try: | |
1539 | self.load_gcode(self.filename, | |
1540 | layer_callback = self.layer_ready_cb, | |
1541 | gcode = gcode) | |
1542 | except PronterfaceQuitException: | |
1543 | return | |
47 | 1544 | except Exception as e: |
1545 | self.log(str(e)) | |
1546 | wx.CallAfter(self.post_gcode_load,False,True) | |
1547 | return | |
15 | 1548 | wx.CallAfter(self.post_gcode_load) |
1549 | ||
1550 | def layer_ready_cb(self, gcode, layer): | |
1551 | global pronterface_quitting | |
1552 | if pronterface_quitting: | |
1553 | raise PronterfaceQuitException | |
1554 | if not self.settings.refreshwhenloading: | |
1555 | return | |
1556 | self.viz_last_layer = layer | |
1557 | if time.time() - self.viz_last_yield > 1.0: | |
1558 | time.sleep(0.2) | |
1559 | self.loading_gcode_message = _("Loading %s: %d layers loaded (%d lines)") % (self.filename, layer + 1, len(gcode)) | |
1560 | self.viz_last_yield = time.time() | |
1561 | wx.CallAfter(self.statusbar.SetStatusText, self.loading_gcode_message) | |
1562 | ||
1563 | def start_viz_thread(self, gcode = None): | |
1564 | threading.Thread(target = self.loadviz, args = (gcode,)).start() | |
1565 | ||
1566 | def pre_gcode_load(self): | |
1567 | self.loading_gcode = True | |
1568 | self.loading_gcode_message = _("Loading %s...") % self.filename | |
1569 | if self.settings.mainviz == "None": | |
1570 | gcode = gcoder.LightGCode(deferred = True) | |
1571 | else: | |
47 | 1572 | gcode = gcoder.GCode(deferred = True, cutting_as_extrusion = self.settings.cutting_as_extrusion) |
15 | 1573 | self.viz_last_yield = 0 |
1574 | self.viz_last_layer = -1 | |
1575 | self.start_viz_thread(gcode) | |
1576 | return gcode | |
1577 | ||
47 | 1578 | def post_gcode_load(self, print_stats = True, failed=False): |
15 | 1579 | # Must be called in wx.CallAfter for safety |
1580 | self.loading_gcode = False | |
47 | 1581 | if not failed: |
1582 | self.SetTitle(_("Pronterface - %s") % self.filename) | |
1583 | message = _("Loaded %s, %d lines") % (self.filename, len(self.fgcode),) | |
1584 | self.log(message) | |
1585 | self.statusbar.SetStatusText(message) | |
1586 | self.savebtn.Enable(True) | |
15 | 1587 | self.loadbtn.SetLabel(_("Load File")) |
47 | 1588 | self.printbtn.SetLabel(_("&Print")) |
1589 | self.pausebtn.SetLabel(_("&Pause")) | |
15 | 1590 | self.pausebtn.Disable() |
1591 | self.recoverbtn.Disable() | |
47 | 1592 | if not failed and self.p.online: |
15 | 1593 | self.printbtn.Enable() |
1594 | self.toolbarsizer.Layout() | |
1595 | self.viz_last_layer = None | |
1596 | if print_stats: | |
1597 | self.output_gcode_stats() | |
1598 | ||
47 | 1599 | def calculate_remaining_filament(self, length, extruder = 0): |
1600 | """ | |
1601 | float calculate_remaining_filament( float length, int extruder ) | |
1602 | ||
1603 | Calculate the remaining length of filament for the given extruder if | |
1604 | the given length were to be extruded. | |
1605 | """ | |
1606 | ||
1607 | remainder = self.spool_manager.getRemainingFilament(extruder) - length | |
1608 | minimum_warning_length = 1000.0 | |
1609 | if remainder < minimum_warning_length: | |
1610 | self.log(_("\nWARNING: Currently loaded spool for extruder " + | |
1611 | "%d will likely run out of filament during the print.\n" % | |
1612 | extruder)) | |
1613 | return remainder | |
1614 | ||
15 | 1615 | def output_gcode_stats(self): |
1616 | gcode = self.fgcode | |
47 | 1617 | self.spool_manager.refresh() |
1618 | ||
1619 | self.log(_("%s of filament used in this print") % format_length(gcode.filament_length)) | |
1620 | ||
1621 | if len(gcode.filament_length_multi) > 1: | |
15 | 1622 | for i in enumerate(gcode.filament_length_multi): |
47 | 1623 | if self.spool_manager.getSpoolName(i[0]) == None: |
1624 | logging.info("- Extruder %d: %0.02fmm" % (i[0], i[1])) | |
1625 | else: | |
1626 | logging.info(("- Extruder %d: %0.02fmm" % (i[0], i[1]) + | |
1627 | " from spool '%s' (%.2fmm will remain)" % | |
1628 | (self.spool_manager.getSpoolName(i[0]), | |
1629 | self.calculate_remaining_filament(i[1], i[0])))) | |
1630 | elif self.spool_manager.getSpoolName(0) != None: | |
1631 | self.log( | |
1632 | _("Using spool '%s' (%s of filament will remain)") % | |
1633 | (self.spool_manager.getSpoolName(0), | |
1634 | format_length(self.calculate_remaining_filament( | |
1635 | gcode.filament_length, 0)))) | |
1636 | ||
15 | 1637 | self.log(_("The print goes:")) |
1638 | self.log(_("- from %.2f mm to %.2f mm in X and is %.2f mm wide") % (gcode.xmin, gcode.xmax, gcode.width)) | |
1639 | self.log(_("- from %.2f mm to %.2f mm in Y and is %.2f mm deep") % (gcode.ymin, gcode.ymax, gcode.depth)) | |
1640 | self.log(_("- from %.2f mm to %.2f mm in Z and is %.2f mm high") % (gcode.zmin, gcode.zmax, gcode.height)) | |
1641 | self.log(_("Estimated duration: %d layers, %s") % gcode.estimate_duration()) | |
1642 | ||
1643 | def loadviz(self, gcode = None): | |
47 | 1644 | try: |
1645 | self.gviz.clear() | |
1646 | self.gwindow.p.clear() | |
1647 | if gcode is not None: | |
1648 | generator = self.gviz.addfile_perlayer(gcode, True) | |
1649 | next_layer = 0 | |
1650 | # Progressive loading of visualization | |
1651 | # We load layers up to the last one which has been processed in GCoder | |
1652 | # (self.viz_last_layer) | |
1653 | # Once the GCode has been entirely loaded, this variable becomes None, | |
1654 | # indicating that we can do the last generator call to finish the | |
1655 | # loading of the visualization, which will itself return None. | |
1656 | # During preloading we verify that the layer we added is the one we | |
1657 | # expected through the assert call. | |
1658 | while True: | |
1659 | global pronterface_quitting | |
1660 | if pronterface_quitting: | |
1661 | return | |
1662 | max_layer = self.viz_last_layer | |
1663 | if max_layer is None: | |
1664 | break | |
1665 | start_layer = next_layer | |
1666 | while next_layer <= max_layer: | |
1667 | assert next(generator) == next_layer | |
1668 | next_layer += 1 | |
1669 | if next_layer != start_layer: | |
1670 | wx.CallAfter(self.gviz.Refresh) | |
1671 | time.sleep(0.1) | |
1672 | generator_output = next(generator) | |
1673 | while generator_output is not None: | |
1674 | assert generator_output == next_layer | |
15 | 1675 | next_layer += 1 |
47 | 1676 | generator_output = next(generator) |
1677 | else: | |
1678 | # If GCode is not being loaded asynchroneously, it is already | |
1679 | # loaded, so let's make visualization sequentially | |
1680 | gcode = self.fgcode | |
1681 | self.gviz.addfile(gcode) | |
1682 | wx.CallAfter(self.gviz.Refresh) | |
1683 | # Load external window sequentially now that everything is ready. | |
1684 | # We can't really do any better as the 3D viewer might clone the | |
1685 | # finalized model from the main visualization | |
1686 | self.gwindow.p.addfile(gcode) | |
1687 | except: | |
1688 | logging.error(traceback.format_exc()) | |
1689 | wx.CallAfter(self.gviz.Refresh) | |
15 | 1690 | |
1691 | # -------------------------------------------------------------- | |
1692 | # File saving handling | |
1693 | # -------------------------------------------------------------- | |
1694 | ||
1695 | def savefile(self, event): | |
1696 | basedir = self.settings.last_file_path | |
1697 | if not os.path.exists(basedir): | |
1698 | basedir = "." | |
1699 | try: | |
1700 | basedir = os.path.split(self.filename)[0] | |
1701 | except: | |
1702 | pass | |
1703 | dlg = wx.FileDialog(self, _("Save as"), basedir, style = wx.FD_SAVE) | |
1704 | dlg.SetWildcard(_("GCODE files (*.gcode;*.gco;*.g)|*.gcode;*.gco;*.g|All Files (*.*)|*.*")) | |
1705 | if dlg.ShowModal() == wx.ID_OK: | |
1706 | name = dlg.GetPath() | |
1707 | open(name, "w").write("\n".join((line.raw for line in self.fgcode))) | |
1708 | self.log(_("G-Code succesfully saved to %s") % name) | |
1709 | dlg.Destroy() | |
1710 | ||
1711 | # -------------------------------------------------------------- | |
1712 | # Printcore callbacks | |
1713 | # -------------------------------------------------------------- | |
1714 | ||
1715 | def process_host_command(self, command): | |
1716 | """Override host command handling""" | |
1717 | command = command.lstrip() | |
1718 | if command.startswith(";@pause"): | |
1719 | self.pause(None) | |
1720 | else: | |
1721 | pronsole.pronsole.process_host_command(self, command) | |
1722 | ||
1723 | def startcb(self, resuming = False): | |
1724 | """Callback on print start""" | |
1725 | pronsole.pronsole.startcb(self, resuming) | |
1726 | if self.settings.lockbox and self.settings.lockonstart: | |
1727 | wx.CallAfter(self.lock, force = True) | |
1728 | ||
1729 | def endcb(self): | |
1730 | """Callback on print end/pause""" | |
1731 | pronsole.pronsole.endcb(self) | |
1732 | if self.p.queueindex == 0: | |
1733 | self.p.runSmallScript(self.endScript) | |
47 | 1734 | if self.settings.display_progress_on_printer: |
1735 | printer_progress_string = "M117 Finished Print" | |
1736 | self.p.send_now(printer_progress_string) | |
15 | 1737 | wx.CallAfter(self.pausebtn.Disable) |
47 | 1738 | wx.CallAfter(self.printbtn.SetLabel, _("&Print")) |
15 | 1739 | wx.CallAfter(self.toolbarsizer.Layout) |
33
eee51ca7cbe7
Added support for multiple cutting passes with automatic Z refocusing
mdd
parents:
31
diff
changeset
|
1740 | |
15 | 1741 | def online(self): |
1742 | """Callback when printer goes online""" | |
1743 | self.log(_("Printer is now online.")) | |
1744 | wx.CallAfter(self.online_gui) | |
1745 | ||
1746 | def online_gui(self): | |
1747 | """Callback when printer goes online (graphical bits)""" | |
47 | 1748 | self.connectbtn.SetLabel(_("Dis&connect")) |
15 | 1749 | self.connectbtn.SetToolTip(wx.ToolTip("Disconnect from the printer")) |
47 | 1750 | self.connectbtn_cb_var = self.disconnect |
15 | 1751 | |
1752 | if hasattr(self, "extrudersel"): | |
1753 | self.do_tool(self.extrudersel.GetValue()) | |
1754 | ||
1755 | self.gui_set_connected() | |
1756 | ||
1757 | if self.filename: | |
1758 | self.printbtn.Enable() | |
1759 | ||
1760 | wx.CallAfter(self.toolbarsizer.Layout) | |
1761 | ||
1762 | def sentcb(self, line, gline): | |
1763 | """Callback when a printer gcode has been sent""" | |
1764 | if not gline: | |
1765 | pass | |
1766 | elif gline.command in ["M104", "M109"]: | |
1767 | gline_s = gcoder.S(gline) | |
1768 | if gline_s is not None: | |
1769 | temp = gline_s | |
1770 | if self.display_gauges: wx.CallAfter(self.hottgauge.SetTarget, temp) | |
1771 | if self.display_graph: wx.CallAfter(self.graph.SetExtruder0TargetTemperature, temp) | |
1772 | elif gline.command in ["M140", "M190"]: | |
1773 | gline_s = gcoder.S(gline) | |
1774 | if gline_s is not None: | |
1775 | temp = gline_s | |
1776 | if self.display_gauges: wx.CallAfter(self.bedtgauge.SetTarget, temp) | |
1777 | if self.display_graph: wx.CallAfter(self.graph.SetBedTargetTemperature, temp) | |
1778 | elif gline.command in ["M106"]: | |
1779 | gline_s=gcoder.S(gline) | |
1780 | fanpow=255 | |
1781 | if gline_s is not None: | |
1782 | fanpow=gline_s | |
1783 | if self.display_graph: wx.CallAfter(self.graph.SetFanPower, fanpow) | |
1784 | elif gline.command in ["M107"]: | |
1785 | if self.display_graph: wx.CallAfter(self.graph.SetFanPower, 0) | |
1786 | elif gline.command.startswith("T"): | |
1787 | tool = gline.command[1:] | |
1788 | if hasattr(self, "extrudersel"): wx.CallAfter(self.extrudersel.SetValue, tool) | |
1789 | if gline.is_move: | |
1790 | self.sentglines.put_nowait(gline) | |
1791 | ||
1792 | def is_excluded_move(self, gline): | |
1793 | """Check whether the given moves ends at a position specified as | |
1794 | excluded in the part excluder""" | |
1795 | if not gline.is_move or not self.excluder or not self.excluder.rectangles: | |
1796 | return False | |
1797 | for (x0, y0, x1, y1) in self.excluder.rectangles: | |
1798 | if x0 <= gline.current_x <= x1 and y0 <= gline.current_y <= y1: | |
1799 | return True | |
1800 | return False | |
1801 | ||
1802 | def preprintsendcb(self, gline, next_gline): | |
1803 | """Callback when a printer gcode is about to be sent. We use it to | |
1804 | exclude moves defined by the part excluder tool""" | |
1805 | if not self.is_excluded_move(gline): | |
1806 | return gline | |
1807 | else: | |
1808 | if gline.z is not None: | |
1809 | if gline.relative: | |
1810 | if self.excluder_z_abs is not None: | |
1811 | self.excluder_z_abs += gline.z | |
1812 | elif self.excluder_z_rel is not None: | |
1813 | self.excluder_z_rel += gline.z | |
1814 | else: | |
1815 | self.excluder_z_rel = gline.z | |
1816 | else: | |
1817 | self.excluder_z_rel = None | |
1818 | self.excluder_z_abs = gline.z | |
1819 | if gline.e is not None and not gline.relative_e: | |
1820 | self.excluder_e = gline.e | |
1821 | # If next move won't be excluded, push the changes we have to do | |
1822 | if next_gline is not None and not self.is_excluded_move(next_gline): | |
1823 | if self.excluder_e is not None: | |
1824 | self.p.send_now("G92 E%.5f" % self.excluder_e) | |
1825 | self.excluder_e = None | |
1826 | if self.excluder_z_abs is not None: | |
1827 | if gline.relative: | |
1828 | self.p.send_now("G90") | |
1829 | self.p.send_now("G1 Z%.5f" % self.excluder_z_abs) | |
1830 | self.excluder_z_abs = None | |
1831 | if gline.relative: | |
1832 | self.p.send_now("G91") | |
1833 | if self.excluder_z_rel is not None: | |
1834 | if not gline.relative: | |
1835 | self.p.send_now("G91") | |
1836 | self.p.send_now("G1 Z%.5f" % self.excluder_z_rel) | |
1837 | self.excluder_z_rel = None | |
1838 | if not gline.relative: | |
1839 | self.p.send_now("G90") | |
1840 | return None | |
1841 | ||
1842 | def printsentcb(self, gline): | |
1843 | """Callback when a print gcode has been sent""" | |
1844 | if gline.is_move: | |
1845 | if hasattr(self.gwindow, "set_current_gline"): | |
1846 | wx.CallAfter(self.gwindow.set_current_gline, gline) | |
1847 | if hasattr(self.gviz, "set_current_gline"): | |
1848 | wx.CallAfter(self.gviz.set_current_gline, gline) | |
1849 | ||
1850 | def layer_change_cb(self, newlayer): | |
1851 | """Callback when the printed layer changed""" | |
1852 | pronsole.pronsole.layer_change_cb(self, newlayer) | |
1853 | if self.settings.mainviz != "3D" or self.settings.trackcurrentlayer3d: | |
1854 | wx.CallAfter(self.gviz.setlayer, newlayer) | |
1855 | ||
1856 | def update_tempdisplay(self): | |
1857 | try: | |
1858 | temps = parse_temperature_report(self.tempreadings) | |
1859 | if "T0" in temps and temps["T0"][0]: | |
1860 | hotend_temp = float(temps["T0"][0]) | |
1861 | elif "T" in temps and temps["T"][0]: | |
1862 | hotend_temp = float(temps["T"][0]) | |
1863 | else: | |
1864 | hotend_temp = None | |
1865 | if hotend_temp is not None: | |
1866 | if self.display_graph: wx.CallAfter(self.graph.SetExtruder0Temperature, hotend_temp) | |
1867 | if self.display_gauges: wx.CallAfter(self.hottgauge.SetValue, hotend_temp) | |
1868 | setpoint = None | |
1869 | if "T0" in temps and temps["T0"][1]: setpoint = float(temps["T0"][1]) | |
1870 | elif temps["T"][1]: setpoint = float(temps["T"][1]) | |
1871 | if setpoint is not None: | |
1872 | if self.display_graph: wx.CallAfter(self.graph.SetExtruder0TargetTemperature, setpoint) | |
1873 | if self.display_gauges: wx.CallAfter(self.hottgauge.SetTarget, setpoint) | |
1874 | if "T1" in temps: | |
1875 | hotend_temp = float(temps["T1"][0]) | |
1876 | if self.display_graph: wx.CallAfter(self.graph.SetExtruder1Temperature, hotend_temp) | |
1877 | setpoint = temps["T1"][1] | |
1878 | if setpoint and self.display_graph: | |
1879 | wx.CallAfter(self.graph.SetExtruder1TargetTemperature, float(setpoint)) | |
1880 | bed_temp = float(temps["B"][0]) if "B" in temps and temps["B"][0] else None | |
1881 | if bed_temp is not None: | |
1882 | if self.display_graph: wx.CallAfter(self.graph.SetBedTemperature, bed_temp) | |
1883 | if self.display_gauges: wx.CallAfter(self.bedtgauge.SetValue, bed_temp) | |
1884 | setpoint = temps["B"][1] | |
1885 | if setpoint: | |
1886 | setpoint = float(setpoint) | |
1887 | if self.display_graph: wx.CallAfter(self.graph.SetBedTargetTemperature, setpoint) | |
1888 | if self.display_gauges: wx.CallAfter(self.bedtgauge.SetTarget, setpoint) | |
1889 | except: | |
1890 | self.logError(traceback.format_exc()) | |
1891 | ||
1892 | def update_pos(self): | |
1893 | bits = gcoder.m114_exp.findall(self.posreport) | |
1894 | x = None | |
1895 | y = None | |
1896 | z = None | |
1897 | for bit in bits: | |
1898 | if not bit[0]: continue | |
1899 | if x is None and bit[0] == "X": | |
1900 | x = float(bit[1]) | |
1901 | elif y is None and bit[0] == "Y": | |
1902 | y = float(bit[1]) | |
1903 | elif z is None and bit[0] == "Z": | |
1904 | z = float(bit[1]) | |
1905 | if x is not None: self.current_pos[0] = x | |
1906 | if y is not None: self.current_pos[1] = y | |
1907 | if z is not None: self.current_pos[2] = z | |
1908 | ||
1909 | def recvcb_actions(self, l): | |
1910 | if l.startswith("!!"): | |
1911 | if not self.paused: | |
1912 | wx.CallAfter(self.pause) | |
1913 | msg = l.split(" ", 1) | |
1914 | if len(msg) > 1 and not self.p.loud: | |
47 | 1915 | self.log(msg[1] + "\n") |
15 | 1916 | return True |
1917 | elif l.startswith("//"): | |
1918 | command = l.split(" ", 1) | |
1919 | if len(command) > 1: | |
1920 | command = command[1] | |
1921 | command = command.split(":") | |
1922 | if len(command) == 2 and command[0] == "action": | |
1923 | command = command[1] | |
1924 | self.log(_("Received command %s") % command) | |
1925 | if command == "pause": | |
1926 | if not self.paused: | |
1927 | wx.CallAfter(self.pause) | |
1928 | return True | |
1929 | elif command == "resume": | |
1930 | if self.paused: | |
1931 | wx.CallAfter(self.pause) | |
1932 | return True | |
1933 | elif command == "disconnect": | |
1934 | wx.CallAfter(self.disconnect) | |
1935 | return True | |
1936 | return False | |
1937 | ||
1938 | def recvcb(self, l): | |
1939 | l = l.rstrip() | |
1940 | if not self.recvcb_actions(l): | |
1941 | report_type = self.recvcb_report(l) | |
1942 | isreport = report_type != REPORT_NONE | |
1943 | if report_type & REPORT_POS: | |
1944 | self.update_pos() | |
1945 | elif report_type & REPORT_TEMP: | |
1946 | wx.CallAfter(self.tempdisp.SetLabel, self.tempreadings.strip().replace("ok ", "")) | |
1947 | self.update_tempdisplay() | |
47 | 1948 | if not self.lineignorepattern.match(l) and not self.p.loud and (l not in ["ok", "wait"] and (not isreport or report_type & REPORT_MANUAL)): |
1949 | self.log(l) | |
15 | 1950 | for listener in self.recvlisteners: |
1951 | listener(l) | |
1952 | ||
1953 | def listfiles(self, line, ignored = False): | |
1954 | if "Begin file list" in line: | |
1955 | self.sdlisting = True | |
1956 | elif "End file list" in line: | |
1957 | self.sdlisting = False | |
1958 | self.recvlisteners.remove(self.listfiles) | |
1959 | wx.CallAfter(self.filesloaded) | |
1960 | elif self.sdlisting: | |
47 | 1961 | self.sdfiles.append(re.sub(" \d+$","",line.strip().lower())) |
15 | 1962 | |
1963 | def waitforsdresponse(self, l): | |
1964 | if "file.open failed" in l: | |
1965 | wx.CallAfter(self.statusbar.SetStatusText, _("Opening file failed.")) | |
1966 | self.recvlisteners.remove(self.waitforsdresponse) | |
1967 | return | |
1968 | if "File opened" in l: | |
1969 | wx.CallAfter(self.statusbar.SetStatusText, l) | |
1970 | if "File selected" in l: | |
1971 | wx.CallAfter(self.statusbar.SetStatusText, _("Starting print")) | |
1972 | self.sdprinting = True | |
1973 | self.p.send_now("M24") | |
1974 | self.startcb() | |
1975 | return | |
1976 | if "Done printing file" in l: | |
1977 | wx.CallAfter(self.statusbar.SetStatusText, l) | |
1978 | self.sdprinting = False | |
1979 | self.recvlisteners.remove(self.waitforsdresponse) | |
1980 | self.endcb() | |
1981 | return | |
1982 | if "SD printing byte" in l: | |
1983 | # M27 handler | |
1984 | try: | |
1985 | resp = l.split() | |
1986 | vals = resp[-1].split("/") | |
1987 | self.percentdone = 100.0 * int(vals[0]) / int(vals[1]) | |
1988 | except: | |
1989 | pass | |
1990 | ||
1991 | # -------------------------------------------------------------- | |
1992 | # Custom buttons handling | |
1993 | # -------------------------------------------------------------- | |
1994 | ||
1995 | def cbuttons_reload(self): | |
1996 | allcbs = getattr(self, "custombuttons_widgets", []) | |
1997 | for button in allcbs: | |
1998 | self.cbuttonssizer.Detach(button) | |
1999 | button.Destroy() | |
2000 | self.custombuttons_widgets = [] | |
2001 | custombuttons = self.custombuttons[:] + [None] | |
2002 | for i, btndef in enumerate(custombuttons): | |
2003 | if btndef is None: | |
2004 | if i == len(custombuttons) - 1: | |
47 | 2005 | self.newbuttonbutton = b = wx.Button(self.centerpanel, -1, "+", size = (35, 18), style = wx.BU_EXACTFIT) |
15 | 2006 | b.SetForegroundColour("#4444ff") |
2007 | b.SetToolTip(wx.ToolTip(_("click to add new custom button"))) | |
2008 | b.Bind(wx.EVT_BUTTON, self.cbutton_edit) | |
2009 | else: | |
2010 | b = wx.StaticText(self.panel, -1, "") | |
2011 | else: | |
2012 | b = wx.Button(self.centerpanel, -1, btndef.label, style = wx.BU_EXACTFIT) | |
2013 | b.SetToolTip(wx.ToolTip(_("Execute command: ") + btndef.command)) | |
2014 | if btndef.background: | |
2015 | b.SetBackgroundColour(btndef.background) | |
47 | 2016 | rr, gg, bb, aa = b.GetBackgroundColour().Get() #last item is alpha |
15 | 2017 | if 0.3 * rr + 0.59 * gg + 0.11 * bb < 60: |
2018 | b.SetForegroundColour("#ffffff") | |
2019 | b.custombutton = i | |
2020 | b.properties = btndef | |
2021 | if btndef is not None: | |
2022 | b.Bind(wx.EVT_BUTTON, self.process_button) | |
2023 | b.Bind(wx.EVT_MOUSE_EVENTS, self.editbutton) | |
2024 | self.custombuttons_widgets.append(b) | |
47 | 2025 | if isinstance(self.cbuttonssizer, wx.GridBagSizer): |
15 | 2026 | self.cbuttonssizer.Add(b, pos = (i // 4, i % 4), flag = wx.EXPAND) |
2027 | else: | |
2028 | self.cbuttonssizer.Add(b, flag = wx.EXPAND) | |
2029 | self.centerpanel.Layout() | |
2030 | self.centerpanel.GetContainingSizer().Layout() | |
2031 | ||
2032 | def help_button(self): | |
2033 | self.log(_('Defines custom button. Usage: button <num> "title" [/c "colour"] command')) | |
2034 | ||
2035 | def do_button(self, argstr): | |
2036 | def nextarg(rest): | |
2037 | rest = rest.lstrip() | |
2038 | if rest.startswith('"'): | |
2039 | return rest[1:].split('"', 1) | |
2040 | else: | |
2041 | return rest.split(None, 1) | |
2042 | # try: | |
2043 | num, argstr = nextarg(argstr) | |
2044 | num = int(num) | |
2045 | title, argstr = nextarg(argstr) | |
2046 | colour = None | |
2047 | try: | |
2048 | c1, c2 = nextarg(argstr) | |
2049 | if c1 == "/c": | |
2050 | colour, argstr = nextarg(c2) | |
2051 | except: | |
2052 | pass | |
2053 | command = argstr.strip() | |
2054 | if num < 0 or num >= 64: | |
2055 | self.log(_("Custom button number should be between 0 and 63")) | |
2056 | return | |
2057 | while num >= len(self.custombuttons): | |
2058 | self.custombuttons.append(None) | |
2059 | self.custombuttons[num] = SpecialButton(title, command) | |
2060 | if colour is not None: | |
2061 | self.custombuttons[num].background = colour | |
2062 | if not self.processing_rc: | |
2063 | self.cbuttons_reload() | |
2064 | ||
2065 | def cbutton_save(self, n, bdef, new_n = None): | |
2066 | if new_n is None: new_n = n | |
2067 | if bdef is None or bdef == "": | |
2068 | self.save_in_rc(("button %d" % n), '') | |
2069 | elif bdef.background: | |
2070 | colour = bdef.background | |
47 | 2071 | if not isinstance(colour, str): |
2072 | if isinstance(colour, tuple) and tuple(map(type, colour)) == (int, int, int): | |
2073 | colour = (x % 256 for x in colour) | |
15 | 2074 | colour = wx.Colour(*colour).GetAsString(wx.C2S_NAME | wx.C2S_HTML_SYNTAX) |
2075 | else: | |
2076 | colour = wx.Colour(colour).GetAsString(wx.C2S_NAME | wx.C2S_HTML_SYNTAX) | |
2077 | self.save_in_rc(("button %d" % n), 'button %d "%s" /c "%s" %s' % (new_n, bdef.label, colour, bdef.command)) | |
2078 | else: | |
2079 | self.save_in_rc(("button %d" % n), 'button %d "%s" %s' % (new_n, bdef.label, bdef.command)) | |
2080 | ||
2081 | def cbutton_edit(self, e, button = None): | |
2082 | bedit = ButtonEdit(self) | |
2083 | if button is not None: | |
2084 | n = button.custombutton | |
2085 | bedit.name.SetValue(button.properties.label) | |
2086 | bedit.command.SetValue(button.properties.command) | |
2087 | if button.properties.background: | |
2088 | colour = button.properties.background | |
47 | 2089 | if not isinstance(colour, str): |
2090 | if isinstance(colour, tuple) and tuple(map(type, colour)) == (int, int, int): | |
2091 | colour = (x % 256 for x in colour) | |
15 | 2092 | colour = wx.Colour(*colour).GetAsString(wx.C2S_NAME | wx.C2S_HTML_SYNTAX) |
2093 | else: | |
2094 | colour = wx.Colour(colour).GetAsString(wx.C2S_NAME | wx.C2S_HTML_SYNTAX) | |
2095 | bedit.color.SetValue(colour) | |
2096 | else: | |
2097 | n = len(self.custombuttons) | |
2098 | while n > 0 and self.custombuttons[n - 1] is None: | |
2099 | n -= 1 | |
2100 | if bedit.ShowModal() == wx.ID_OK: | |
2101 | if n == len(self.custombuttons): | |
2102 | self.custombuttons.append(None) | |
2103 | self.custombuttons[n] = SpecialButton(bedit.name.GetValue().strip(), bedit.command.GetValue().strip(), custom = True) | |
2104 | if bedit.color.GetValue().strip() != "": | |
2105 | self.custombuttons[n].background = bedit.color.GetValue() | |
2106 | self.cbutton_save(n, self.custombuttons[n]) | |
2107 | wx.CallAfter(bedit.Destroy) | |
2108 | wx.CallAfter(self.cbuttons_reload) | |
2109 | ||
2110 | def cbutton_remove(self, e, button): | |
2111 | n = button.custombutton | |
2112 | self.cbutton_save(n, None) | |
2113 | del self.custombuttons[n] | |
2114 | for i in range(n, len(self.custombuttons)): | |
2115 | self.cbutton_save(i, self.custombuttons[i]) | |
2116 | wx.CallAfter(self.cbuttons_reload) | |
2117 | ||
2118 | def cbutton_order(self, e, button, dir): | |
2119 | n = button.custombutton | |
2120 | if dir < 0: | |
2121 | n = n - 1 | |
2122 | if n + 1 >= len(self.custombuttons): | |
2123 | self.custombuttons.append(None) # pad | |
2124 | # swap | |
2125 | self.custombuttons[n], self.custombuttons[n + 1] = self.custombuttons[n + 1], self.custombuttons[n] | |
2126 | self.cbutton_save(n, self.custombuttons[n]) | |
2127 | self.cbutton_save(n + 1, self.custombuttons[n + 1]) | |
2128 | wx.CallAfter(self.cbuttons_reload) | |
2129 | ||
2130 | def editbutton(self, e): | |
2131 | if e.IsCommandEvent() or e.ButtonUp(wx.MOUSE_BTN_RIGHT): | |
2132 | if e.IsCommandEvent(): | |
2133 | pos = (0, 0) | |
2134 | else: | |
2135 | pos = e.GetPosition() | |
2136 | popupmenu = wx.Menu() | |
2137 | obj = e.GetEventObject() | |
2138 | if hasattr(obj, "custombutton"): | |
2139 | item = popupmenu.Append(-1, _("Edit custom button '%s'") % e.GetEventObject().GetLabelText()) | |
2140 | self.Bind(wx.EVT_MENU, lambda e, button = e.GetEventObject(): self.cbutton_edit(e, button), item) | |
2141 | item = popupmenu.Append(-1, _("Move left <<")) | |
2142 | self.Bind(wx.EVT_MENU, lambda e, button = e.GetEventObject(): self.cbutton_order(e, button, -1), item) | |
2143 | if obj.custombutton == 0: item.Enable(False) | |
2144 | item = popupmenu.Append(-1, _("Move right >>")) | |
2145 | self.Bind(wx.EVT_MENU, lambda e, button = e.GetEventObject(): self.cbutton_order(e, button, 1), item) | |
2146 | if obj.custombutton == 63: item.Enable(False) | |
2147 | pos = self.panel.ScreenToClient(e.GetEventObject().ClientToScreen(pos)) | |
2148 | item = popupmenu.Append(-1, _("Remove custom button '%s'") % e.GetEventObject().GetLabelText()) | |
2149 | self.Bind(wx.EVT_MENU, lambda e, button = e.GetEventObject(): self.cbutton_remove(e, button), item) | |
2150 | else: | |
2151 | item = popupmenu.Append(-1, _("Add custom button")) | |
2152 | self.Bind(wx.EVT_MENU, self.cbutton_edit, item) | |
2153 | self.panel.PopupMenu(popupmenu, pos) | |
47 | 2154 | elif e.Dragging() and e.LeftIsDown(): |
15 | 2155 | obj = e.GetEventObject() |
2156 | scrpos = obj.ClientToScreen(e.GetPosition()) | |
2157 | if not hasattr(self, "dragpos"): | |
2158 | self.dragpos = scrpos | |
2159 | e.Skip() | |
2160 | return | |
2161 | else: | |
2162 | dx, dy = self.dragpos[0] - scrpos[0], self.dragpos[1] - scrpos[1] | |
47 | 2163 | if dx * dx + dy * dy < 30 * 30: # threshold to detect dragging for jittery mice |
15 | 2164 | e.Skip() |
2165 | return | |
2166 | if not hasattr(self, "dragging"): | |
2167 | # init dragging of the custom button | |
47 | 2168 | if hasattr(obj, "custombutton") and (not hasattr(obj,"properties") or obj.properties is not None): |
15 | 2169 | for b in self.custombuttons_widgets: |
47 | 2170 | if not hasattr(b,"properties") or b.properties is None: |
15 | 2171 | b.Enable() |
2172 | b.SetLabel("") | |
2173 | b.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)) | |
2174 | b.SetForegroundColour("black") | |
2175 | b.SetSize(obj.GetSize()) | |
2176 | if self.toolbarsizer.GetItem(b) is not None: | |
2177 | self.toolbarsizer.SetItemMinSize(b, obj.GetSize()) | |
2178 | self.mainsizer.Layout() | |
2179 | self.dragging = wx.Button(self.panel, -1, obj.GetLabel(), style = wx.BU_EXACTFIT) | |
2180 | self.dragging.SetBackgroundColour(obj.GetBackgroundColour()) | |
2181 | self.dragging.SetForegroundColour(obj.GetForegroundColour()) | |
2182 | self.dragging.sourcebutton = obj | |
2183 | self.dragging.Raise() | |
2184 | self.dragging.Disable() | |
2185 | self.dragging.SetPosition(self.panel.ScreenToClient(scrpos)) | |
2186 | self.last_drag_dest = obj | |
2187 | self.dragging.label = obj.s_label = obj.GetLabel() | |
2188 | self.dragging.bgc = obj.s_bgc = obj.GetBackgroundColour() | |
2189 | self.dragging.fgc = obj.s_fgc = obj.GetForegroundColour() | |
2190 | else: | |
2191 | # dragging in progress | |
2192 | self.dragging.SetPosition(self.panel.ScreenToClient(scrpos)) | |
2193 | wx.CallAfter(self.dragging.Refresh) | |
2194 | dst = None | |
2195 | src = self.dragging.sourcebutton | |
2196 | drg = self.dragging | |
2197 | for b in self.custombuttons_widgets: | |
2198 | if b.GetScreenRect().Contains(scrpos): | |
2199 | dst = b | |
2200 | break | |
2201 | if dst is not self.last_drag_dest: | |
2202 | if self.last_drag_dest is not None: | |
2203 | self.last_drag_dest.SetBackgroundColour(self.last_drag_dest.s_bgc) | |
2204 | self.last_drag_dest.SetForegroundColour(self.last_drag_dest.s_fgc) | |
2205 | self.last_drag_dest.SetLabel(self.last_drag_dest.s_label) | |
2206 | if dst is not None and dst is not src: | |
2207 | dst.s_bgc = dst.GetBackgroundColour() | |
2208 | dst.s_fgc = dst.GetForegroundColour() | |
2209 | dst.s_label = dst.GetLabel() | |
2210 | src.SetBackgroundColour(dst.GetBackgroundColour()) | |
2211 | src.SetForegroundColour(dst.GetForegroundColour()) | |
2212 | src.SetLabel(dst.GetLabel()) | |
2213 | dst.SetBackgroundColour(drg.bgc) | |
2214 | dst.SetForegroundColour(drg.fgc) | |
2215 | dst.SetLabel(drg.label) | |
2216 | else: | |
2217 | src.SetBackgroundColour(drg.bgc) | |
2218 | src.SetForegroundColour(drg.fgc) | |
2219 | src.SetLabel(drg.label) | |
2220 | self.last_drag_dest = dst | |
47 | 2221 | elif hasattr(self, "dragging") and not e.LeftIsDown(): |
15 | 2222 | # dragging finished |
2223 | obj = e.GetEventObject() | |
2224 | scrpos = obj.ClientToScreen(e.GetPosition()) | |
2225 | dst = None | |
2226 | src = self.dragging.sourcebutton | |
2227 | drg = self.dragging | |
2228 | for b in self.custombuttons_widgets: | |
2229 | if b.GetScreenRect().Contains(scrpos): | |
2230 | dst = b | |
2231 | break | |
47 | 2232 | if dst is not None and hasattr(dst,"custombutton"): |
15 | 2233 | src_i = src.custombutton |
2234 | dst_i = dst.custombutton | |
2235 | self.custombuttons[src_i], self.custombuttons[dst_i] = self.custombuttons[dst_i], self.custombuttons[src_i] | |
2236 | self.cbutton_save(src_i, self.custombuttons[src_i]) | |
2237 | self.cbutton_save(dst_i, self.custombuttons[dst_i]) | |
2238 | while self.custombuttons[-1] is None: | |
2239 | del self.custombuttons[-1] | |
2240 | wx.CallAfter(self.dragging.Destroy) | |
2241 | del self.dragging | |
2242 | wx.CallAfter(self.cbuttons_reload) | |
2243 | del self.last_drag_dest | |
2244 | del self.dragpos | |
2245 | else: | |
2246 | e.Skip() | |
2247 | ||
2248 | def process_button(self, e): | |
2249 | try: | |
2250 | if hasattr(e.GetEventObject(), "custombutton"): | |
2251 | if wx.GetKeyState(wx.WXK_CONTROL) or wx.GetKeyState(wx.WXK_ALT): | |
2252 | return self.editbutton(e) | |
2253 | self.cur_button = e.GetEventObject().custombutton | |
2254 | command = e.GetEventObject().properties.command | |
2255 | command = self.precmd(command) | |
2256 | self.onecmd(command) | |
2257 | self.cur_button = None | |
2258 | except: | |
2259 | self.log(_("Failed to handle button")) | |
2260 | self.cur_button = None | |
2261 | raise | |
2262 | ||
2263 | # -------------------------------------------------------------- | |
2264 | # Macros handling | |
2265 | # -------------------------------------------------------------- | |
2266 | ||
2267 | def start_macro(self, macro_name, old_macro_definition = ""): | |
2268 | if not self.processing_rc: | |
2269 | def cb(definition): | |
2270 | if len(definition.strip()) == 0: | |
2271 | if old_macro_definition != "": | |
2272 | dialog = wx.MessageDialog(self, _("Do you want to erase the macro?"), style = wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION) | |
2273 | if dialog.ShowModal() == wx.ID_YES: | |
2274 | self.delete_macro(macro_name) | |
2275 | return | |
2276 | self.log(_("Cancelled.")) | |
2277 | return | |
2278 | self.cur_macro_name = macro_name | |
2279 | self.cur_macro_def = definition | |
2280 | self.end_macro() | |
2281 | MacroEditor(macro_name, old_macro_definition, cb) | |
2282 | else: | |
2283 | pronsole.pronsole.start_macro(self, macro_name, old_macro_definition) | |
2284 | ||
2285 | def end_macro(self): | |
2286 | pronsole.pronsole.end_macro(self) | |
2287 | self.update_macros_menu() | |
2288 | ||
2289 | def delete_macro(self, macro_name): | |
2290 | pronsole.pronsole.delete_macro(self, macro_name) | |
2291 | self.update_macros_menu() | |
2292 | ||
2293 | def new_macro(self, e = None): | |
47 | 2294 | dialog = wx.Dialog(self, -1, _("Enter macro name")) |
2295 | text = wx.StaticText(dialog, -1, _("Macro name:")) | |
2296 | namectrl = wx.TextCtrl(dialog, -1, style = wx.TE_PROCESS_ENTER) | |
2297 | okb = wx.Button(dialog, wx.ID_OK, _("Ok")) | |
2298 | dialog.Bind(wx.EVT_TEXT_ENTER, | |
2299 | lambda e: dialog.EndModal(wx.ID_OK), namectrl) | |
2300 | cancel_button = wx.Button(dialog, wx.ID_CANCEL, _("Cancel")) | |
2301 | ||
2302 | # Layout | |
2303 | ## Group the buttons horizontally | |
2304 | buttons_sizer = wx.BoxSizer(wx.HORIZONTAL) | |
2305 | buttons_sizer.Add(okb, 0) | |
2306 | buttons_sizer.Add(cancel_button, 0) | |
2307 | ## Set a minimum size for the name control box | |
2308 | min_size = namectrl.GetTextExtent('Default Long Macro Name') | |
2309 | namectrl.SetMinSize(wx.Size(min_size.width, -1)) | |
2310 | ## Group the text and the name control box horizontally | |
2311 | name_sizer = wx.BoxSizer(wx.HORIZONTAL) | |
2312 | name_sizer.Add(text, 0, flag = wx.ALIGN_CENTER) | |
2313 | name_sizer.AddSpacer(10) | |
2314 | name_sizer.Add(namectrl, 1, wx.EXPAND) | |
2315 | ## Group everything vertically | |
2316 | dialog_sizer = wx.BoxSizer(wx.VERTICAL) | |
2317 | dialog_sizer.Add(name_sizer, 0, border = 10, | |
2318 | flag = wx.LEFT | wx.TOP | wx.RIGHT) | |
2319 | dialog_sizer.Add(buttons_sizer, 0, border = 10, | |
2320 | flag = wx.ALIGN_CENTER | wx.ALL) | |
2321 | dialog.SetSizerAndFit(dialog_sizer) | |
15 | 2322 | dialog.Centre() |
47 | 2323 | |
15 | 2324 | macro = "" |
2325 | if dialog.ShowModal() == wx.ID_OK: | |
47 | 2326 | macro = namectrl.GetValue() |
15 | 2327 | if macro != "": |
2328 | wx.CallAfter(self.edit_macro, macro) | |
2329 | dialog.Destroy() | |
2330 | return macro | |
2331 | ||
2332 | def edit_macro(self, macro): | |
2333 | if macro == "": return self.new_macro() | |
2334 | if macro in self.macros: | |
2335 | old_def = self.macros[macro] | |
47 | 2336 | elif len([chr(c) for c in macro.encode("ascii", "replace") if not chr(c).isalnum() and chr(c) != "_"]): |
15 | 2337 | self.log(_("Macro name may contain only ASCII alphanumeric symbols and underscores")) |
2338 | return | |
2339 | elif hasattr(self.__class__, "do_" + macro): | |
2340 | self.log(_("Name '%s' is being used by built-in command") % macro) | |
2341 | return | |
2342 | else: | |
2343 | old_def = "" | |
2344 | self.start_macro(macro, old_def) | |
2345 | return macro | |
2346 | ||
2347 | def update_macros_menu(self): | |
2348 | if not hasattr(self, "macros_menu"): | |
2349 | return # too early, menu not yet built | |
2350 | try: | |
2351 | while True: | |
2352 | item = self.macros_menu.FindItemByPosition(1) | |
2353 | if item is None: break | |
47 | 2354 | self.macros_menu.DestroyItem(item) |
15 | 2355 | except: |
2356 | pass | |
2357 | for macro in self.macros.keys(): | |
2358 | self.Bind(wx.EVT_MENU, lambda x, m = macro: self.start_macro(m, self.macros[m]), self.macros_menu.Append(-1, macro)) | |
2359 | ||
2360 | # -------------------------------------------------------------- | |
2361 | # Slic3r integration | |
2362 | # -------------------------------------------------------------- | |
2363 | ||
2364 | def load_slic3r_configs(self, menus): | |
2365 | """List Slic3r configurations and create menu""" | |
2366 | # Hack to get correct path for Slic3r config | |
2367 | orig_appname = self.app.GetAppName() | |
2368 | self.app.SetAppName("Slic3r") | |
2369 | configpath = wx.StandardPaths.Get().GetUserDataDir() | |
2370 | self.slic3r_configpath = configpath | |
2371 | configfile = os.path.join(configpath, "slic3r.ini") | |
47 | 2372 | if not os.path.exists(configfile): |
2373 | self.app.SetAppName("Slic3rPE") | |
2374 | configpath = wx.StandardPaths.Get().GetUserDataDir() | |
2375 | self.slic3r_configpath = configpath | |
2376 | configfile = os.path.join(configpath, "slic3r.ini") | |
2377 | if not os.path.exists(configfile): | |
2378 | self.settings.slic3rintegration=False; | |
2379 | return | |
2380 | self.app.SetAppName(orig_appname) | |
15 | 2381 | config = self.read_slic3r_config(configfile) |
47 | 2382 | version = config.get("dummy", "version") # Slic3r version |
15 | 2383 | self.slic3r_configs = {} |
2384 | for cat in menus: | |
2385 | menu = menus[cat] | |
2386 | pattern = os.path.join(configpath, cat, "*.ini") | |
2387 | files = sorted(glob.glob(pattern)) | |
2388 | try: | |
2389 | preset = config.get("presets", cat) | |
47 | 2390 | # Starting from Slic3r 1.3.0, preset names have no extension |
2391 | if version.split(".") >= ["1","3","0"]: preset += ".ini" | |
15 | 2392 | self.slic3r_configs[cat] = preset |
2393 | except: | |
2394 | preset = None | |
2395 | self.slic3r_configs[cat] = None | |
2396 | for f in files: | |
2397 | name = os.path.splitext(os.path.basename(f))[0] | |
2398 | item = menu.Append(-1, name, f, wx.ITEM_RADIO) | |
2399 | item.Check(os.path.basename(f) == preset) | |
2400 | self.Bind(wx.EVT_MENU, | |
2401 | lambda event, cat = cat, f = f: | |
2402 | self.set_slic3r_config(configfile, cat, f), item) | |
2403 | ||
2404 | def read_slic3r_config(self, configfile, parser = None): | |
2405 | """Helper to read a Slic3r configuration file""" | |
47 | 2406 | import configparser |
2407 | parser = configparser.RawConfigParser() | |
15 | 2408 | |
47 | 2409 | class add_header: |
15 | 2410 | def __init__(self, f): |
2411 | self.f = f | |
2412 | self.header = '[dummy]' | |
2413 | ||
2414 | def readline(self): | |
2415 | if self.header: | |
2416 | try: return self.header | |
2417 | finally: self.header = None | |
2418 | else: | |
2419 | return self.f.readline() | |
47 | 2420 | |
2421 | def __iter__(self): | |
2422 | import itertools | |
2423 | return itertools.chain([self.header], iter(self.f)) | |
2424 | ||
15 | 2425 | parser.readfp(add_header(open(configfile)), configfile) |
2426 | return parser | |
2427 | ||
2428 | def set_slic3r_config(self, configfile, cat, file): | |
2429 | """Set new preset for a given category""" | |
2430 | self.slic3r_configs[cat] = file | |
2431 | if self.settings.slic3rupdate: | |
2432 | config = self.read_slic3r_config(configfile) | |
47 | 2433 | version = config.get("dummy", "version") # Slic3r version |
2434 | preset = os.path.basename(file) | |
2435 | # Starting from Slic3r 1.3.0, preset names have no extension | |
2436 | if version.split(".") >= ["1","3","0"]: | |
2437 | preset = os.path.splitext(preset)[0] | |
2438 | config.set("presets", cat, preset) | |
15 | 2439 | f = StringIO.StringIO() |
2440 | config.write(f) | |
2441 | data = f.getvalue() | |
2442 | f.close() | |
2443 | data = data.replace("[dummy]\n", "") | |
2444 | with open(configfile, "w") as f: | |
2445 | f.write(data) | |
2446 | ||
2447 | class PronterApp(wx.App): | |
2448 | ||
2449 | mainwindow = None | |
2450 | ||
2451 | def __init__(self, *args, **kwargs): | |
2452 | super(PronterApp, self).__init__(*args, **kwargs) | |
2453 | self.SetAppName("Pronterface") | |
47 | 2454 | self.locale = wx.Locale(wx.Locale.GetSystemLanguage()) |
15 | 2455 | self.mainwindow = PronterWindow(self) |
2456 | self.mainwindow.Show() |