updated pronterface.py losing my own changes

Wed, 20 Jan 2021 10:17:01 +0100

author
mdd
date
Wed, 20 Jan 2021 10:17:01 +0100
changeset 47
dcc64b767b64
parent 46
cce0af6351f0
child 48
3c27b4ee6fec

updated pronterface.py losing my own changes

printrun-src/printrun/pronterface.py file | annotate | diff | comparison | revisions
--- a/printrun-src/printrun/pronterface.py	Wed Jan 20 10:15:13 2021 +0100
+++ b/printrun-src/printrun/pronterface.py	Wed Jan 20 10:17:01 2021 +0100
@@ -1,17 +1,3 @@
-#!/usr/bin/env python
-
-# FILE MODIFIED BY NEOSOFT - MALTE DI DONATO
-# Embed Lasercut functions from laser.py 
-from . import laser
-try:
-    from . import module_watcher
-    mw = module_watcher.ModuleWatcher()
-    mw.watch_module('laser')
-    mw.start_watching()
-except Exception, e:
-    print e
-    print "ModuleWatcher not loaded, skipping autoreloading of changed modules"
-
 # This file is part of the Printrun suite.
 #
 # Printrun is free software: you can redistribute it and/or modify
@@ -28,32 +14,39 @@
 # along with Printrun.  If not, see <http://www.gnu.org/licenses/>.
 
 import os
-import Queue
+import platform
+import queue
 import sys
 import time
 import threading
 import traceback
-import cStringIO as StringIO
+import io as StringIO
 import subprocess
 import glob
 import logging
+import re
 
 try: import simplejson as json
 except ImportError: import json
 
 from . import pronsole
 from . import printcore
+from printrun.spoolmanager import spoolmanager_gui
 
 from .utils import install_locale, setup_logging, dosify, \
     iconfile, configfile, format_time, format_duration, \
     hexcolor_to_float, parse_temperature_report, \
-    prepare_command, check_rgb_color, check_rgba_color
+    prepare_command, check_rgb_color, check_rgba_color, compile_file, \
+    write_history_to, read_history_from
 install_locale('pronterface')
 
 try:
     import wx
+    import wx.adv
+    if wx.VERSION < (4,):
+        raise ImportError()
 except:
-    logging.error(_("WX is not installed. This program requires WX to run."))
+    logging.error(_("WX >= 4 is not installed. This program requires WX >= 4 to run."))
     raise
 
 from .gui.widgets import SpecialButton, MacroEditor, PronterOptions, ButtonEdit
@@ -70,11 +63,23 @@
 
 from .gui import MainWindow
 from .settings import wxSetting, HiddenSetting, StringSetting, SpinSetting, \
-    FloatSpinSetting, BooleanSetting, StaticTextSetting
+    FloatSpinSetting, BooleanSetting, StaticTextSetting, ColorSetting, ComboSetting
 from printrun import gcoder
 from .pronsole import REPORT_NONE, REPORT_POS, REPORT_TEMP, REPORT_MANUAL
 
-class ConsoleOutputHandler(object):
+def format_length(mm, fractional=2):
+    if mm <= 10:
+        units = mm
+        suffix = 'mm'
+    elif mm < 1000:
+        units = mm / 10
+        suffix = 'cm'
+    else:
+        units = mm / 1000
+        suffix = 'm'
+    return '%%.%df' % fractional % units + suffix
+
+class ConsoleOutputHandler:
     """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.
     We also redirect stdout and stderr to ourself to catch print messages and al."""
 
@@ -83,14 +88,12 @@
         self.stderr = sys.stderr
         sys.stdout = self
         sys.stderr = self
+        self.print_on_stdout = not log_path
         if log_path:
-            self.print_on_stdout = False
             setup_logging(self, log_path, reset_handlers = True)
-            self.target = target
         else:
-            self.print_on_stdout = True
-            setup_logging(sys.stdout)
-            self.target = target
+            setup_logging(sys.stdout, reset_handlers = True)
+        self.target = target
 
     def __del__(self):
         sys.stdout = self.stdout
@@ -102,30 +105,16 @@
         except:
             pass
         if self.print_on_stdout:
-            try:
-                data = data.encode("utf-8")
-            except:
-                pass
             self.stdout.write(data)
 
     def flush(self):
         if self.stdout:
             self.stdout.flush()
 
-class ComboSetting(wxSetting):
-
-    def __init__(self, name, default, choices, label = None, help = None, group = None):
-        super(ComboSetting, self).__init__(name, default, label, help, group)
-        self.choices = choices
-
-    def get_specific_widget(self, parent):
-        import wx
-        self.widget = wx.ComboBox(parent, -1, str(self.value), choices = self.choices, style = wx.CB_DROPDOWN)
-        return self.widget
-
 class PronterWindow(MainWindow, pronsole.pronsole):
 
     _fgcode = None
+    printer_progress_time = time.time()
 
     def _get_fgcode(self):
         return self._fgcode
@@ -153,12 +142,6 @@
         self.ui_ready = False
         self._add_settings(size)
 
-        for field in dir(self.settings):
-            if field.startswith("_gcview_color_"):
-                cleanname = field[1:]
-                color = hexcolor_to_float(getattr(self.settings, cleanname), 4)
-                setattr(self, cleanname, list(color))
-
         self.pauseScript = None #"pause.gcode"
         self.endScript = None #"end.gcode"
 
@@ -172,7 +155,7 @@
         self.current_pos = [0, 0, 0]
         self.paused = False
         self.uploading = False
-        self.sentglines = Queue.Queue(0)
+        self.sentglines = queue.Queue(0)
         self.cpbuttons = {
             "motorsoff": SpecialButton(_("Motors off"), ("M84"), (250, 250, 250), _("Switch all motors off")),
             "extrude": SpecialButton(_("Extrude"), ("pront_extrude"), (225, 200, 200), _("Advance extruder by set length")),
@@ -182,7 +165,14 @@
         self.btndict = {}
         self.filehistory = None
         self.autoconnect = False
+        self.autoscrolldisable = False
+
         self.parse_cmdline(sys.argv[1:])
+        for field in dir(self.settings):
+            if field.startswith("_gcview_color_"):
+                cleanname = field[1:]
+                color = hexcolor_to_float(getattr(self.settings, cleanname), 4)
+                setattr(self, cleanname, list(color))
 
         # FIXME: We need to initialize the main window after loading the
         # configs to restore the size, but this might have some unforeseen
@@ -197,7 +187,8 @@
         self.Bind(wx.EVT_SIZE, self.on_resize)
         self.Bind(wx.EVT_MAXIMIZE, self.on_maximize)
         self.window_ready = True
-
+        self.Bind(wx.EVT_CLOSE, self.closewin)
+        self.Bind(wx.EVT_CHAR_HOOK, self.on_key)
         # set feedrates in printcore for pause/resume
         self.p.xy_feedrate = self.settings.xy_feedrate
         self.p.z_feedrate = self.settings.z_feedrate
@@ -205,18 +196,18 @@
         self.panel.SetBackgroundColour(self.bgcolor)
         customdict = {}
         try:
-            execfile(configfile("custombtn.txt"), customdict)
+            exec(compile_file(configfile("custombtn.txt")), customdict)
             if len(customdict["btns"]):
                 if not len(self.custombuttons):
                     try:
                         self.custombuttons = customdict["btns"]
-                        for n in xrange(len(self.custombuttons)):
+                        for n in range(len(self.custombuttons)):
                             self.cbutton_save(n, self.custombuttons[n])
                         os.rename("custombtn.txt", "custombtn.old")
                         rco = open("custombtn.txt", "w")
                         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"))
                         rco.close()
-                    except IOError, x:
+                    except IOError as x:
                         logging.error(str(x))
                 else:
                     logging.warning(_("Note!!! You have specified custom buttons in both custombtn.txt and .pronsolerc"))
@@ -224,9 +215,7 @@
 
         except:
             pass
-        self.create_menu()
-        self.update_recent_files("recentfiles", self.settings.recentfiles)
-
+        self.menustrip = wx.MenuBar()
         self.reload_ui()
         # disable all printer controls until we connect to a printer
         self.gui_set_disconnected()
@@ -257,91 +246,6 @@
         if self.settings.monitor:
             self.update_monitor()
 
-        self.lc_printing = False
-        self.pass_current = 1
-
-    #  --------------------------------------------------------------
-    #  Lasercutter methods
-    #  --------------------------------------------------------------
-
-    def on_lc_printfile(self, event):
-        # lc print button
-        self.log("Priming Z axis to initial focus")
-        line = self.precmd("G1 Z%.2f" % (self.settings.lc_z_focus + self.lc_material_thickness.GetValue()))
-        wx.CallAfter(self.onecmd, line)
-        self.lc_printing = True
-        wx.CallAfter(self.printfile, None)
-
-    def endcb_lasercut(self):
-        # LASERCUT: Now check if we should do another print pass?
-        self.log("event: endcb_lasercut")
-        if self.lc_printing:
-            self.log(" -> checking if something to do...")
-            pass_count = self.lc_pass_count.GetValue()
-            if pass_count > 1:
-                time.sleep(0.5)
-                if self.pass_current < pass_count:
-                    self.pass_current += 1
-                    self.log("Starting lasercut pass # %i of %i" % (self.pass_current, pass_count))
-                    if self.lc_pass_zdiff.GetValue() != 0:
-                        # move Z focus
-                        new_z = self.settings.lc_z_focus + self.lc_material_thickness.GetValue() + (
-                            self.lc_pass_zdiff.GetValue() * (self.pass_current - 1))
-                        self.log("Re-Positioning laser focus by %.1f mm to %.1f" % (self.lc_pass_zdiff.GetValue(), new_z))
-                        line = self.precmd("G1 Z%.2f" % (new_z))
-                        self.onecmd(line)
-                        time.sleep(0.5)
-
-                    # "click" print button again
-                    tmp = self.pass_current
-                    self.printfile(None)
-                    self.pass_current = tmp
-                else:
-                    self.lc_printing = False
-                    wx.CallAfter(self.lc_printbtn.Enable)
-                    wx.CallAfter(self.lc_printbtn.SetLabel, _("Start cutting"))
-
-                    self.log("Resetting Z axis to initial focus")
-                    line = self.precmd("G1 Z%.2f" % (self.settings.lc_z_focus + self.lc_material_thickness.GetValue()))
-                    self.onecmd(line)
-            else:
-                self.lc_printing = False
-                wx.CallAfter(self.lc_printbtn.Enable)
-                wx.CallAfter(self.lc_printbtn.SetLabel, _("Start cutting"))
-                
-
-    def update_lc_settings(self, key, value):
-        return True
-
-    def _lc_add_settings(self, size):
-        # first add the lasercutter options
-        self.settings._add(StaticTextSetting("separator_lc_general", "General laser settings", "", group = "Laser"))
-        self.settings._add(BooleanSetting("lc_melzi_hack", False, "Use Melzi M571 Hack instead M3/M5", "no description :)", "Laser"), self.update_lc_settings)
-        self.settings._add(SpinSetting("lc_travel_speed", 120, 1, 300, "Travel speed in mm/s", "", "Laser"), self.update_lc_settings)
-        self.settings._add(SpinSetting("lc_engrave_speed", 10, 1, 300, "Engrave speed in mm/s", "", "Laser"), self.update_lc_settings)
-        self.settings._add(SpinSetting("lc_z_focus", 16, -80, 80, "Laser Z focus position", "", "Laser"), self.update_lc_settings)
-        self.settings._add(SpinSetting("lc_pass_count", 1, 0, 20, "Default Number of cutting passes", "", "Laser"), self.reload_ui)
-        self.settings._add(FloatSpinSetting("lc_pass_zdiff", -0.25, -2.0, 2.0, "Default Z movement after each cut", "", "Laser"), self.reload_ui)
-        self.settings._add(FloatSpinSetting("lc_material_thickness", 4.0, 0.0, 80.0, "Default Material Thickness", "", "Laser"), self.reload_ui)
-
-        self.settings._add(StaticTextSetting("separator_lc_bitmap", "PNG Bitmap processing", "", group = "Laser"))
-        self.settings._add(FloatSpinSetting("lc_bitmap_speed_factor", 1.0, 0.1, 2.0, "Engrave speed factor", "", "Laser"), self.update_lc_settings)
-        self.settings._add(SpinSetting("lc_dpi", 300, 25, 600, "Image DPI", "Image resolution for scaling", "Laser"), self.update_lc_settings)
-        self.settings._add(SpinSetting("lc_grey_threshold", 0, 0, 255, "Grey threshold value for RGB", "", "Laser"), self.update_lc_settings)
-        self.settings._add(BooleanSetting("lc_invert_cut", True, "PNG: Invert grey threshold", "Invert laser on/off logic", "Laser"), self.update_lc_settings)
-        self.settings._add(BooleanSetting("lc_change_dir", True, "PNG: Change direction", "Engrave in both directions on Y Axis", "Laser"), self.update_lc_settings)
-
-        self.settings._add(StaticTextSetting("separator_lc_hpgl", "HPGL processing", "", group = "Laser"))
-        self.settings._add(FloatSpinSetting("lc_hpgl_speed_factor", 1.0, 0.1, 2.0, "Engrave speed factor", "", "Laser"), self.update_lc_settings)
-
-        self.settings._add(StaticTextSetting("separator_lc_svg", "SVG processing", "", group = "Laser"))
-        self.settings._add(FloatSpinSetting("lc_svg_speed_factor", 1.0, 0.1, 2.0, "Engrave speed factor", "", "Laser"), self.update_lc_settings)
-        self.settings._add(FloatSpinSetting("lc_svg_smoothness", 0.2, 0.1, 10.0, "Smoothness", "Smoothness of curves (smaller value = smoother curve)", "Laser"), self.update_lc_settings)
-        self.settings._add(SpinSetting("lc_svg_width", 50, 1, 9999, "Width (mm)", "Image width", "Laser"), self.update_lc_settings)
-        self.settings._add(SpinSetting("lc_svg_height", 50, 1, 9999, "Height (mm)", "Image height", "Laser"), self.update_lc_settings)
-        self.settings._add(ComboSetting("lc_svg_scalemode", "original", ["original", "scale", "stretch"], "Scaling mode", "scale/stretch to above dimensions", "Laser"), self.update_lc_settings)
-        self.settings._add(BooleanSetting("lc_svg_offset", True, "Calculate offset to X=0, Y=0", "If enabled, move image to origin position", "Laser"), self.update_lc_settings)
-
     #  --------------------------------------------------------------
     #  Main interface handling
     #  --------------------------------------------------------------
@@ -352,12 +256,22 @@
 
     def reload_ui(self, *args):
         if not self.window_ready: return
+        temp_monitor = self.settings.monitor
+        self.settings.monitor = False
+        self.update_monitor()
         self.Freeze()
 
         # If UI is being recreated, delete current one
         if self.ui_ready:
             # Store log console content
             logcontent = self.logbox.GetValue()
+            self.menustrip.SetMenus([])
+            if len(self.commandbox.history):
+                #save current command box history
+                if not os.path.exists(self.history_file):
+                    if not os.path.exists(self.cache_dir):
+                        os.makedirs(self.cache_dir)
+                write_history_to(self.history_file, self.commandbox.history)
             # Create a temporary panel to reparent widgets with state we want
             # to retain across UI changes
             temppanel = wx.Panel(self)
@@ -370,13 +284,16 @@
             self.reset_ui()
 
         # Create UI
+        self.create_menu()
+        self.update_recent_files("recentfiles", self.settings.recentfiles)
+        self.splitterwindow = None
         if self.settings.uimode in (_("Tabbed"), _("Tabbed with platers")):
             self.createTabbedGui()
         else:
             self.createGui(self.settings.uimode == _("Compact"),
                            self.settings.controlsmode == "Mini")
 
-        if hasattr(self, "splitterwindow"):
+        if self.splitterwindow:
             self.splitterwindow.SetSashPosition(self.settings.last_sash_position)
 
             def splitter_resize(event):
@@ -392,7 +309,7 @@
         self.update_gcview_params()
 
         # Finalize
-        if self.online:
+        if self.p.online:
             self.gui_set_connected()
         if self.ui_ready:
             self.logbox.SetValue(logcontent)
@@ -400,10 +317,13 @@
             self.panel.Layout()
             if self.fgcode:
                 self.start_viz_thread()
-            if self.settings.monitor:
-                self.update_monitor()
         self.ui_ready = True
+        self.settings.monitor = temp_monitor
+        self.commandbox.history = read_history_from(self.history_file)
+        self.commandbox.histindex = len(self.commandbox.history)
         self.Thaw()
+        if self.settings.monitor:
+            self.update_monitor()
 
     def on_resize(self, event):
         wx.CallAfter(self.on_resize_real)
@@ -424,7 +344,57 @@
     def on_exit(self, event):
         self.Close()
 
-    def kill(self, e):
+    def on_settings_change(self, changed_settings):
+        if self.gviz:
+            self.gviz.on_settings_change(changed_settings)
+
+    def on_key(self, event):
+        if not isinstance(event.EventObject, (wx.TextCtrl, wx.ComboBox)) \
+            or event.HasModifiers():
+            ch = chr(event.KeyCode)
+            keys = {'B': self.btemp, 'H': self.htemp, 'J': self.xyb, 'S': self.commandbox,
+                'V': self.gviz}
+            widget = keys.get(ch)
+            #ignore Alt+(S, H), so it can open Settings, Help menu
+            if widget and (ch not in 'SH' or not event.AltDown()) \
+                and not (event.ControlDown() and ch == 'V'
+                        and event.EventObject is self.commandbox):
+                widget.SetFocus()
+                return
+            # On MSWindows button mnemonics are processed only if the
+            # focus is in the parent panel
+            if event.AltDown() and ch < 'Z':
+                in_toolbar = self.toolbarsizer.GetItem(event.EventObject)
+                candidates = (self.connectbtn, self.connectbtn_cb_var), \
+                            (self.pausebtn, self.pause), \
+                            (self.printbtn, self.printfile)
+                for ctl, cb in candidates:
+                    match = ('&' + ch) in ctl.Label.upper()
+                    handled = in_toolbar and match
+                    if handled:
+                        break
+                    # react to 'P' even for 'Restart', 'Resume'
+                    # print('match', match, 'handled', handled, ctl.Label, ctl.Enabled)
+                    if (match or ch == 'P' and ctl != self.connectbtn) and ctl.Enabled:
+                        # print('call', ch, cb)
+                        cb()
+                        # react to only 1 of 'P' buttons, prefer Resume
+                        return
+
+        event.Skip()
+
+    def closewin(self, e):
+        e.StopPropagation()
+        self.do_exit("force")
+
+    def kill(self, e=None):
+        if len(self.commandbox.history):
+                #save current command box history
+                history = (self.history_file)
+                if not os.path.exists(history):
+                    if not os.path.exists(self.cache_dir):
+                        os.makedirs(self.cache_dir)
+                write_history_to(history,self.commandbox.history)
         if self.p.printing or self.p.paused:
             dlg = wx.MessageDialog(self, _("Print in progress ! Are you really sure you want to quit ?"), _("Exit"), wx.YES_NO | wx.ICON_WARNING)
             if dlg.ShowModal() == wx.ID_NO:
@@ -445,12 +415,11 @@
         wx.CallAfter(self.gwindow.Destroy)
         wx.CallAfter(self.Destroy)
 
-    def _get_bgcolor(self):
-        if self.settings.bgcolor != "auto":
-            return self.settings.bgcolor
-        else:
-            return wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWFRAME)
-    bgcolor = property(_get_bgcolor)
+    @property
+    def bgcolor(self):
+        return (wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWFRAME)
+                if self.settings.bgcolor == 'auto'
+                else self.settings.bgcolor)
 
     #  --------------------------------------------------------------
     #  Main interface actions
@@ -473,16 +442,22 @@
             self.log(_("Done monitoring."))
 
     def do_pront_extrude(self, l = ""):
+        if self.p.printing and not self.paused:
+            self.log(_("Please pause or stop print before extruding."))
+            return
         feed = self.settings.e_feedrate
         self.do_extrude_final(self.edist.GetValue(), feed)
 
     def do_pront_reverse(self, l = ""):
+        if self.p.printing and not self.paused:
+            self.log(_("Please pause or stop print before reversing."))
+            return
         feed = self.settings.e_feedrate
         self.do_extrude_final(- self.edist.GetValue(), feed)
 
     def do_settemp(self, l = ""):
         try:
-            if l.__class__ not in (str, unicode) or not len(l):
+            if not isinstance(l, str) or not len(l):
                 l = str(self.htemp.GetValue().split()[0])
             l = l.lower().replace(", ", ".")
             for i in self.temps.keys():
@@ -491,18 +466,18 @@
             if f >= 0:
                 if self.p.online:
                     self.p.send_now("M104 S" + l)
-                    self.log(_("Setting hotend temperature to %f degrees Celsius.") % f)
+                    self.log(_("Setting hotend temperature to %g degrees Celsius.") % f)
                     self.sethotendgui(f)
                 else:
                     self.logError(_("Printer is not online."))
             else:
                 self.logError(_("You cannot set negative temperatures. To turn the hotend off entirely, set its temperature to 0."))
-        except Exception, x:
+        except Exception as x:
             self.logError(_("You must enter a temperature. (%s)") % (repr(x),))
 
     def do_bedtemp(self, l = ""):
         try:
-            if l.__class__ not in (str, unicode) or not len(l):
+            if not isinstance(l, str) or not len(l):
                 l = str(self.btemp.GetValue().split()[0])
             l = l.lower().replace(", ", ".")
             for i in self.bedtemps.keys():
@@ -511,18 +486,18 @@
             if f >= 0:
                 if self.p.online:
                     self.p.send_now("M140 S" + l)
-                    self.log(_("Setting bed temperature to %f degrees Celsius.") % f)
+                    self.log(_("Setting bed temperature to %g degrees Celsius.") % f)
                     self.setbedgui(f)
                 else:
                     self.logError(_("Printer is not online."))
             else:
                 self.logError(_("You cannot set negative temperatures. To turn the bed off entirely, set its temperature to 0."))
-        except Exception, x:
+        except Exception as x:
             self.logError(_("You must enter a temperature. (%s)") % (repr(x),))
 
     def do_setspeed(self, l = ""):
         try:
-            if l.__class__ not in (str, unicode) or not len(l):
+            if not isinstance(l, str) or not len(l):
                 l = str(self.speed_slider.GetValue())
             else:
                 l = l.lower()
@@ -532,12 +507,12 @@
                 self.log(_("Setting print speed factor to %d%%.") % speed)
             else:
                 self.logError(_("Printer is not online."))
-        except Exception, x:
+        except Exception as x:
             self.logError(_("You must enter a speed. (%s)") % (repr(x),))
 
     def do_setflow(self, l = ""):
         try:
-            if l.__class__ not in (str, unicode) or not len(l):
+            if not isinstance(l, str) or not len(l):
                 l = str(self.flow_slider.GetValue())
             else:
                 l = l.lower()
@@ -547,7 +522,7 @@
                 self.log(_("Setting print flow factor to %d%%.") % flow)
             else:
                 self.logError(_("Printer is not online."))
-        except Exception, x:
+        except Exception as x:
             self.logError(_("You must enter a flow. (%s)") % (repr(x),))
 
     def setbedgui(self, f):
@@ -602,21 +577,28 @@
         elif portslist:
             self.serialport.SetValue(portslist[0])
 
+    def appendCommandHistory(self):
+        cmd = self.commandbox.Value
+        hist = self.commandbox.history
+        append = cmd and (not hist or hist[-1] != cmd)
+        if append:
+            self.commandbox.history.append(cmd)
+        return append
+
     def cbkey(self, e):
-        if e.GetKeyCode() == wx.WXK_UP:
+        dir = {wx.WXK_UP: -1, wx.WXK_DOWN: 1}.get(e.KeyCode)
+        if dir:
             if self.commandbox.histindex == len(self.commandbox.history):
-                self.commandbox.history.append(self.commandbox.GetValue())  # save current command
-            if len(self.commandbox.history):
-                self.commandbox.histindex = (self.commandbox.histindex - 1) % len(self.commandbox.history)
-                self.commandbox.SetValue(self.commandbox.history[self.commandbox.histindex])
-                self.commandbox.SetSelection(0, len(self.commandbox.history[self.commandbox.histindex]))
-        elif e.GetKeyCode() == wx.WXK_DOWN:
-            if self.commandbox.histindex == len(self.commandbox.history):
-                self.commandbox.history.append(self.commandbox.GetValue())  # save current command
-            if len(self.commandbox.history):
-                self.commandbox.histindex = (self.commandbox.histindex + 1) % len(self.commandbox.history)
-                self.commandbox.SetValue(self.commandbox.history[self.commandbox.histindex])
-                self.commandbox.SetSelection(0, len(self.commandbox.history[self.commandbox.histindex]))
+                if dir == 1:
+                    # do not cycle top => bottom
+                    return
+                #save unsent command before going back
+                self.appendCommandHistory()
+            self.commandbox.histindex = max(0, min(self.commandbox.histindex + dir, len(self.commandbox.history)))
+            self.commandbox.Value = (self.commandbox.history[self.commandbox.histindex]
+                if self.commandbox.histindex < len(self.commandbox.history)
+                else '')
+            self.commandbox.SetInsertionPointEnd()
         else:
             e.Skip()
 
@@ -775,11 +757,24 @@
 
     def addtexttolog(self, text):
         try:
-            self.logbox.AppendText(text)
             max_length = 20000
             current_length = self.logbox.GetLastPosition()
             if current_length > max_length:
                 self.logbox.Remove(0, current_length / 10)
+            currentCaretPosition = self.logbox.GetInsertionPoint()
+            currentLengthOfText = self.logbox.GetLastPosition()
+            if self.autoscrolldisable:
+                self.logbox.Freeze()
+                currentSelectionStart, currentSelectionEnd = self.logbox.GetSelection()
+                self.logbox.SetInsertionPointEnd()
+                self.logbox.AppendText(text)
+                self.logbox.SetInsertionPoint(currentCaretPosition)
+                self.logbox.SetSelection(currentSelectionStart, currentSelectionEnd)
+                self.logbox.Thaw()
+            else:
+                self.logbox.SetInsertionPointEnd()
+                self.logbox.AppendText(text)
+
         except:
             self.log(_("Attempted to write invalid text to console, which could be due to an invalid baudrate"))
 
@@ -789,16 +784,19 @@
     def set_verbose_communications(self, e):
         self.p.loud = e.IsChecked()
 
+    def set_autoscrolldisable(self,e):
+        self.autoscrolldisable = e.IsChecked()
+
     def sendline(self, e):
-        command = self.commandbox.GetValue()
+        command = self.commandbox.Value
         if not len(command):
             return
-        wx.CallAfter(self.addtexttolog, ">>> " + command + "\n")
+        logging.info(">>> " + command)
         line = self.precmd(str(command))
         self.onecmd(line)
-        self.commandbox.SetSelection(0, len(command))
-        self.commandbox.history.append(command)
+        self.appendCommandHistory()
         self.commandbox.histindex = len(self.commandbox.history)
+        self.commandbox.Value = ''
 
     #  --------------------------------------------------------------
     #  Main menu handling & actions
@@ -806,10 +804,10 @@
 
     def create_menu(self):
         """Create main menu"""
-        self.menustrip = wx.MenuBar()
+
         # File menu
         m = wx.Menu()
-        self.Bind(wx.EVT_MENU, self.loadfile, m.Append(-1, _("&Open..."), _(" Open file")))
+        self.Bind(wx.EVT_MENU, self.loadfile, m.Append(-1, _("&Open...\tCtrl+O"), _(" Open file")))
         self.savebtn = m.Append(-1, _("&Save..."), _(" Save file"))
         self.savebtn.Enable(False)
         self.Bind(wx.EVT_MENU, self.savefile, self.savebtn)
@@ -819,19 +817,25 @@
         self.filehistory.UseMenu(recent)
         self.Bind(wx.EVT_MENU_RANGE, self.load_recent_file,
                   id = wx.ID_FILE1, id2 = wx.ID_FILE9)
-        m.AppendMenu(wx.ID_ANY, _("&Recent Files"), recent)
-        self.Bind(wx.EVT_MENU, self.clear_log, m.Append(-1, _("Clear console"), _(" Clear output console")))
+        m.Append(wx.ID_ANY, _("&Recent Files"), recent)
+        self.Bind(wx.EVT_MENU, self.clear_log, m.Append(-1, _("Clear console\tCtrl+L"), _(" Clear output console")))
         self.Bind(wx.EVT_MENU, self.on_exit, m.Append(wx.ID_EXIT, _("E&xit"), _(" Closes the Window")))
         self.menustrip.Append(m, _("&File"))
 
+        # Tools Menu
         m = wx.Menu()
         self.Bind(wx.EVT_MENU, self.do_editgcode, m.Append(-1, _("&Edit..."), _(" Edit open file")))
         self.Bind(wx.EVT_MENU, self.plate, m.Append(-1, _("Plater"), _(" Compose 3D models into a single plate")))
         self.Bind(wx.EVT_MENU, self.plate_gcode, m.Append(-1, _("G-Code Plater"), _(" Compose G-Codes into a single plate")))
         self.Bind(wx.EVT_MENU, self.exclude, m.Append(-1, _("Excluder"), _(" Exclude parts of the bed from being printed")))
         self.Bind(wx.EVT_MENU, self.project, m.Append(-1, _("Projector"), _(" Project slices")))
+        self.Bind(wx.EVT_MENU,
+                  self.show_spool_manager,
+                  m.Append(-1, _("Spool Manager"),
+                           _(" Manage different spools of filament")))
         self.menustrip.Append(m, _("&Tools"))
 
+        # Advanced Menu
         m = wx.Menu()
         self.recoverbtn = m.Append(-1, _("Recover"), _(" Recover previous print after a disconnect (homes X, Y, restores Z and E status)"))
         self.recoverbtn.Disable = lambda *a: self.recoverbtn.Enable(False)
@@ -870,6 +874,11 @@
         m.Check(mItem.GetId(), self.p.loud)
         self.Bind(wx.EVT_MENU, self.set_verbose_communications, mItem)
 
+        mItem = m.AppendCheckItem(-1, _("Don't autoscroll"),
+                                  _("Disables automatic scrolling of the console when new text is added"))
+        m.Check(mItem.GetId(), self.autoscrolldisable)
+        self.Bind(wx.EVT_MENU, self.set_autoscrolldisable, mItem)
+
         self.menustrip.Append(m, _("&Settings"))
         self.update_macros_menu()
         self.SetMenuBar(self.menustrip)
@@ -895,10 +904,14 @@
         self.excluder.pop_window(self.fgcode, bgcolor = self.bgcolor,
                                  build_dimensions = self.build_dimensions_list)
 
+    def show_spool_manager(self, event):
+        """Show Spool Manager Window"""
+        spoolmanager_gui.SpoolManagerMainWindow(self, self.spool_manager).Show()
+
     def about(self, event):
         """Show about dialog"""
 
-        info = wx.AboutDialogInfo()
+        info = wx.adv.AboutDialogInfo()
 
         info.SetIcon(wx.Icon(iconfile("pronterface.png"), wx.BITMAP_TYPE_PNG))
         info.SetName('Printrun')
@@ -912,7 +925,7 @@
                        % self.settings.total_filament_used
 
         info.SetDescription(description)
-        info.SetCopyright('(C) 2011 - 2015')
+        info.SetCopyright('(C) 2011 - 2020')
         info.SetWebSite('https://github.com/kliment/Printrun')
 
         licence = """\
@@ -931,26 +944,28 @@
         info.SetLicence(licence)
         info.AddDeveloper('Kliment Yanev')
         info.AddDeveloper('Guillaume Seguin')
-        info.AddDeveloper('Malte Bayer')
 
-        wx.AboutBox(info)
+        wx.adv.AboutBox(info)
 
     #  --------------------------------------------------------------
     #  Settings & command line handling (including update callbacks)
     #  --------------------------------------------------------------
+
     def _add_settings(self, size):
-        self._lc_add_settings(size)
-
         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)
         self.settings._add(StringSetting("simarrange_path", "", _("Simarrange command"), _("Path to the simarrange binary to use in the STL plater"), "External"))
         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)
         self.settings._add(SpinSetting("extruders", 0, 1, 5, _("Extruders count"), _("Number of extruders"), "Printer"))
         self.settings._add(BooleanSetting("clamp_jogging", False, _("Clamp manual moves"), _("Prevent manual moves from leaving the specified build dimensions"), "Printer"))
-        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\nTabbed mode is a two-pages mode, where the first page shows controls and the second one shows visualization and log.\nTabbed with platers mode is the same as Tabbed, but with two extra pages for the STL and G-Code platers."), "UI"), self.reload_ui)
-        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)
+        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"))
+        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"))
+        self.settings._add(BooleanSetting("cutting_as_extrusion", True, _("Display cutting moves"), _("Show moves where spindle is active as printing moves"), "Printer"))
+        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)
+        #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)
+        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)
         self.settings._add(BooleanSetting("slic3rintegration", False, _("Enable Slic3r integration"), _("Add a menu to select Slic3r profiles directly from Pronterface"), "UI"), self.reload_ui)
         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"))
-        self.settings._add(ComboSetting("mainviz", "3D", ["2D", "3D", "None"], _("Main visualization"), _("Select visualization for main window."), "Viewer"), self.reload_ui)
+        self.settings._add(ComboSetting("mainviz", "3D", ("2D", "3D", "None"), _("Main visualization"), _("Select visualization for main window."), "Viewer"), self.reload_ui)
         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)
         self.settings._add(StaticTextSetting("separator_3d_viewer", _("3D viewer options"), "", group = "Viewer"))
         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)
@@ -968,29 +983,37 @@
         self.settings._add(HiddenSetting("last_window_maximized", False))
         self.settings._add(HiddenSetting("last_sash_position", -1))
         self.settings._add(HiddenSetting("last_bed_temperature", 0.0))
-        self.settings._add(HiddenSetting("last_file_path", u""))
+        self.settings._add(HiddenSetting("last_file_path", ""))
         self.settings._add(HiddenSetting("last_file_filter", 0))
         self.settings._add(HiddenSetting("last_temperature", 0.0))
         self.settings._add(StaticTextSetting("separator_2d_viewer", _("2D viewer options"), "", group = "Viewer"))
         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)
         self.settings._add(SpinSetting("preview_grid_step1", 10., 0, 200, _("Fine grid spacing"), _("Fine Grid Spacing"), "Viewer"), self.update_gviz_params)
         self.settings._add(SpinSetting("preview_grid_step2", 50., 0, 200, _("Coarse grid spacing"), _("Coarse Grid Spacing"), "Viewer"), self.update_gviz_params)
-        self.settings._add(StringSetting("bgcolor", "#FFFFFF", _("Background color"), _("Pronterface background color"), "Colors"), self.reload_ui, validate = check_rgb_color)
-        self.settings._add(StringSetting("gcview_color_background", "#FAFAC7FF", _("3D view background color"), _("Color of the 3D view background"), "Colors"), self.update_gcview_colors, validate = check_rgba_color)
-        self.settings._add(StringSetting("gcview_color_travel", "#99999999", _("3D view travel moves color"), _("Color of travel moves in 3D view"), "Colors"), self.update_gcview_colors, validate = check_rgba_color)
-        self.settings._add(StringSetting("gcview_color_tool0", "#FF000099", _("3D view print moves color"), _("Color of print moves with tool 0 in 3D view"), "Colors"), self.update_gcview_colors, validate = check_rgba_color)
-        self.settings._add(StringSetting("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, validate = check_rgba_color)
-        self.settings._add(StringSetting("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, validate = check_rgba_color)
-        self.settings._add(StringSetting("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, validate = check_rgba_color)
-        self.settings._add(StringSetting("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, validate = check_rgba_color)
-        self.settings._add(StringSetting("gcview_color_printed", "#33BF0099", _("3D view printed moves color"), _("Color of printed moves in 3D view"), "Colors"), self.update_gcview_colors, validate = check_rgba_color)
-        self.settings._add(StringSetting("gcview_color_current", "#00E5FFCC", _("3D view current layer moves color"), _("Color of moves in current layer in 3D view"), "Colors"), self.update_gcview_colors, validate = check_rgba_color)
-        self.settings._add(StringSetting("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, validate = check_rgba_color)
+        self.settings._add(ColorSetting("bgcolor", self._preferred_bgcolour_hex(), _("Background color"), _("Pronterface background color"), "Colors", isRGBA=False), self.reload_ui)
+        self.settings._add(ColorSetting("graph_color_background", "#FAFAC7", _("Graph background color"), _("Color of the temperature graph background"), "Colors", isRGBA=False), self.reload_ui)
+        self.settings._add(ColorSetting("gcview_color_background", "#FAFAC7FF", _("3D view background color"), _("Color of the 3D view background"), "Colors"), self.update_gcview_colors)
+        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)
+        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)
+        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)
+        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)
+        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)
+        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)
+        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)
+        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)
+        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)
         self.settings._add(StaticTextSetting("note1", _("Note:"), _("Changing some of these settings might require a restart to get effect"), group = "UI"))
         recentfilessetting = StringSetting("recentfiles", "[]")
         recentfilessetting.hidden = True
         self.settings._add(recentfilessetting, self.update_recent_files)
 
+    def _preferred_bgcolour_hex(self):
+        id = wx.SYS_COLOUR_WINDOW \
+            if platform.system() == 'Windows' \
+            else wx.SYS_COLOUR_BACKGROUND
+        sys_bgcolour = wx.SystemSettings.GetColour(id)
+        return sys_bgcolour.GetAsString(flags=wx.C2S_HTML_SYNTAX)
+
     def add_cmdline_arguments(self, parser):
         pronsole.pronsole.add_cmdline_arguments(self, parser)
         parser.add_argument('-a', '--autoconnect', help = _("automatically try to connect to printer on startup"), action = "store_true")
@@ -1050,6 +1073,8 @@
         wx.CallAfter(widget.Refresh)
 
     def update_gcview_colors(self, param, value):
+        if not self.window_ready:
+            return
         color = hexcolor_to_float(value, 4)
         # This is sort of a hack: we copy the color values into the preexisting
         # color tuple so that we don't need to update the tuple used by gcview
@@ -1065,9 +1090,11 @@
     def update_bed_viz(self, *args):
         """Update bed visualization when size/type changed"""
         if hasattr(self, "gviz") and hasattr(self.gviz, "recreate_platform"):
-            self.gviz.recreate_platform(self.build_dimensions_list, self.settings.circular_bed)
+            self.gviz.recreate_platform(self.build_dimensions_list, self.settings.circular_bed,
+                grid = (self.settings.preview_grid_step1, self.settings.preview_grid_step2))
         if hasattr(self, "gwindow") and hasattr(self.gwindow, "recreate_platform"):
-            self.gwindow.recreate_platform(self.build_dimensions_list, self.settings.circular_bed)
+            self.gwindow.recreate_platform(self.build_dimensions_list, self.settings.circular_bed,
+                grid = (self.settings.preview_grid_step1, self.settings.preview_grid_step2))
 
     def update_gcview_params(self, *args):
         need_reload = False
@@ -1106,6 +1133,14 @@
                 status_string += _(" Est: %s of %s remaining | ") % (format_duration(secondsremain),
                                                                      format_duration(secondsestimate))
                 status_string += _(" Z: %.3f mm") % self.curlayer
+                if self.settings.display_progress_on_printer and time.time() - self.printer_progress_time >= self.settings.printer_progress_update_interval:
+                    self.printer_progress_time = time.time()
+                    printer_progress_string = "M117 " + str(round(100 * float(self.p.queueindex) / len(self.p.mainqueue), 2)) + "% Est " + format_duration(secondsremain)
+                    #":" seems to be some kind of seperator for G-CODE"
+                    self.p.send_now(printer_progress_string.replace(":", "."))
+                    if len(printer_progress_string) > 25:
+                        logging.info("Warning: The print progress message might be too long to be displayed properly")
+                    #13 chars for up to 99h est.
         elif self.loading_gcode:
             status_string = self.loading_gcode_message
         wx.CallAfter(self.statusbar.SetStatusText, status_string)
@@ -1118,7 +1153,7 @@
                 gc = self.sentglines.get_nowait()
                 wx.CallAfter(self.gviz.addgcodehighlight, gc)
                 self.sentglines.task_done()
-        except Queue.Empty:
+        except queue.Empty:
             pass
 
     def statuschecker(self):
@@ -1145,6 +1180,12 @@
     #  Printer connection handling
     #  --------------------------------------------------------------
 
+    def connectbtn_cb(self, event):
+        # Implement toggle behavior with a single Bind
+        # and switched variable, so we have reference to
+        # the actual callback to use in on_key
+        self.connectbtn_cb_var()
+
     def connect(self, event = None):
         self.log(_("Connecting..."))
         port = None
@@ -1163,8 +1204,8 @@
         if self.paused:
             self.p.paused = 0
             self.p.printing = 0
-            wx.CallAfter(self.pausebtn.SetLabel, _("Pause"))
-            wx.CallAfter(self.printbtn.SetLabel, _("Print"))
+            wx.CallAfter(self.pausebtn.SetLabel, _("&Pause"))
+            wx.CallAfter(self.printbtn.SetLabel, _("&Print"))
             wx.CallAfter(self.toolbarsizer.Layout)
             self.paused = 0
             if self.sdprinting:
@@ -1193,17 +1234,18 @@
             self.status_thread.join()
             self.status_thread = None
 
-        wx.CallAfter(self.connectbtn.SetLabel, _("Connect"))
-        wx.CallAfter(self.connectbtn.SetToolTip, wx.ToolTip(_("Connect to the printer")))
-        wx.CallAfter(self.connectbtn.Bind, wx.EVT_BUTTON, self.connect)
-
-        wx.CallAfter(self.gui_set_disconnected)
+        def toggle():
+            self.connectbtn.SetLabel(_("&Connect"))
+            self.connectbtn.SetToolTip(wx.ToolTip(_("Connect to the printer")))
+            self.connectbtn_cb_var = self.connect
+            self.gui_set_disconnected()
+        wx.CallAfter(toggle)
 
         if self.paused:
             self.p.paused = 0
             self.p.printing = 0
-            wx.CallAfter(self.pausebtn.SetLabel, _("Pause"))
-            wx.CallAfter(self.printbtn.SetLabel, _("Print"))
+            wx.CallAfter(self.pausebtn.SetLabel, _("&Pause"))
+            wx.CallAfter(self.printbtn.SetLabel, _("&Print"))
             self.paused = 0
             if self.sdprinting:
                 self.p.send_now("M26 S0")
@@ -1219,10 +1261,10 @@
             self.sethotendgui(0)
             self.setbedgui(0)
             self.p.printing = 0
-            wx.CallAfter(self.printbtn.SetLabel, _("Print"))
+            wx.CallAfter(self.printbtn.SetLabel, _("&Print"))
             if self.paused:
                 self.p.paused = 0
-                wx.CallAfter(self.pausebtn.SetLabel, _("Pause"))
+                wx.CallAfter(self.pausebtn.SetLabel, _("&Pause"))
                 self.paused = 0
             wx.CallAfter(self.toolbarsizer.Layout)
         dlg.Destroy()
@@ -1232,15 +1274,12 @@
     #  --------------------------------------------------------------
 
     def on_startprint(self):
-        wx.CallAfter(self.pausebtn.SetLabel, _("Pause"))
+        wx.CallAfter(self.pausebtn.SetLabel, _("&Pause"))
         wx.CallAfter(self.pausebtn.Enable)
         wx.CallAfter(self.printbtn.SetLabel, _("Restart"))
         wx.CallAfter(self.toolbarsizer.Layout)
 
-        wx.CallAfter(self.lc_printbtn.Disable)
-        wx.CallAfter(self.lc_printbtn.SetLabel, _("Cut in progress"))
-
-    def printfile(self, event):
+    def printfile(self, event=None):
         self.extra_print_time = 0
         if self.paused:
             self.p.paused = 0
@@ -1260,7 +1299,6 @@
         self.sdprinting = False
         self.on_startprint()
         self.p.startprint(self.fgcode)
-        self.pass_current = 1
 
     def sdprintfile(self, event):
         self.extra_print_time = 0
@@ -1298,6 +1336,9 @@
     def pause(self, event = None):
         if not self.paused:
             self.log(_("Print paused at: %s") % format_time(time.time()))
+            if self.settings.display_progress_on_printer:
+                printer_progress_string = "M117 PausedInPronterface"
+                self.p.send_now(printer_progress_string)
             if self.sdprinting:
                 self.p.send_now("M25")
             else:
@@ -1312,12 +1353,15 @@
             wx.CallAfter(self.toolbarsizer.Layout)
         else:
             self.log(_("Resuming."))
+            if self.settings.display_progress_on_printer:
+                printer_progress_string = "M117 Resuming"
+                self.p.send_now(printer_progress_string)
             self.paused = False
             if self.sdprinting:
                 self.p.send_now("M24")
             else:
                 self.p.resume()
-            wx.CallAfter(self.pausebtn.SetLabel, _("Pause"))
+            wx.CallAfter(self.pausebtn.SetLabel, _("&Pause"))
             wx.CallAfter(self.toolbarsizer.Layout)
 
     def recover(self, event):
@@ -1365,7 +1409,7 @@
     def slice_func(self):
         try:
             output_filename = self.model_to_gcode_filename(self.filename)
-            pararray = prepare_command(self.settings.slicecommand,
+            pararray = prepare_command(self.settings.slicecommandpath+self.settings.slicecommand,
                                        {"$s": self.filename, "$o": output_filename})
             if self.settings.slic3rintegration:
                 for cat, config in self.slic3r_configs.items():
@@ -1373,7 +1417,7 @@
                         fpath = os.path.join(self.slic3r_configpath, cat, config)
                         pararray += ["--load", fpath]
             self.log(_("Running ") + " ".join(pararray))
-            self.slicep = subprocess.Popen(pararray, stderr = subprocess.STDOUT, stdout = subprocess.PIPE)
+            self.slicep = subprocess.Popen(pararray, stdin=subprocess.DEVNULL, stderr = subprocess.STDOUT, stdout = subprocess.PIPE, universal_newlines = True)
             while True:
                 o = self.slicep.stdout.read(1)
                 if o == '' and self.slicep.poll() is not None: break
@@ -1399,6 +1443,7 @@
             self.filename = fn
         self.slicing = False
         self.slicep = None
+        self.loadbtn.SetLabel, _("Load file")
 
     def slice(self, filename):
         wx.CallAfter(self.loadbtn.SetLabel, _("Cancel"))
@@ -1441,8 +1486,7 @@
         dlg = None
         if filename is None:
             dlg = wx.FileDialog(self, _("Open file to print"), basedir, style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
-            # add image files to GCODE file list
-            dlg.SetWildcard(_("GCODE and Image files|*.gcode;*.gco;*.g;*.png;*.svg;*.hpgl;*.plt|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 (*.*)|*.*"))
+            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 (*.*)|*.*"))
             try:
               dlg.SetFilterIndex(self.settings.last_file_filter)
             except:
@@ -1477,27 +1521,8 @@
             except:
                 self.logError(_("Could not update recent files list:") +
                               "\n" + traceback.format_exc())
-
-            # reload the library local so we dont have to restart the whole app when making code changes
-            reload(laser)
-
             if name.lower().endswith(".stl") or name.lower().endswith(".obj"):
                 self.slice(name)
-            elif name.lower().endswith(".png") or name.lower().endswith(".jpg") or name.lower().endswith(".gif"):
-                # Generate GCODE from IMAGE
-                lc = laser.Lasercutter(pronterwindow = self)
-                lc.image2gcode(name)  
-                wx.CallAfter(self.endcb_lasercut)
-            elif name.lower().endswith(".svg"):
-                # Generate GCODE from SVG
-                lc = laser.Lasercutter(pronterwindow = self)
-                lc.svg2gcode(name)  
-                wx.CallAfter(self.endcb_lasercut)
-            elif name.lower().endswith(".hpgl") or name.lower().endswith(".plt"):
-                # Generate GCODE from HPGL
-                lc = laser.Lasercutter(pronterwindow = self)
-                lc.hpgl2gcode(name)  
-                wx.CallAfter(self.endcb_lasercut)
             else:
                 self.load_gcode_async(name)
         else:
@@ -1516,6 +1541,10 @@
                             gcode = gcode)
         except PronterfaceQuitException:
             return
+        except Exception as e:
+            self.log(str(e))
+            wx.CallAfter(self.post_gcode_load,False,True)
+            return
         wx.CallAfter(self.post_gcode_load)
 
     def layer_ready_cb(self, gcode, layer):
@@ -1540,38 +1569,71 @@
         if self.settings.mainviz == "None":
             gcode = gcoder.LightGCode(deferred = True)
         else:
-            gcode = gcoder.GCode(deferred = True)
+            gcode = gcoder.GCode(deferred = True, cutting_as_extrusion = self.settings.cutting_as_extrusion)
         self.viz_last_yield = 0
         self.viz_last_layer = -1
         self.start_viz_thread(gcode)
         return gcode
 
-    def post_gcode_load(self, print_stats = True):
+    def post_gcode_load(self, print_stats = True, failed=False):
         # Must be called in wx.CallAfter for safety
         self.loading_gcode = False
-        self.SetTitle(_(u"Pronterface - %s") % self.filename)
-        message = _("Loaded %s, %d lines") % (self.filename, len(self.fgcode),)
-        self.log(message)
-        self.statusbar.SetStatusText(message)
-        self.savebtn.Enable(True)
+        if not failed:
+            self.SetTitle(_("Pronterface - %s") % self.filename)
+            message = _("Loaded %s, %d lines") % (self.filename, len(self.fgcode),)
+            self.log(message)
+            self.statusbar.SetStatusText(message)
+            self.savebtn.Enable(True)
         self.loadbtn.SetLabel(_("Load File"))
-        self.printbtn.SetLabel(_("Print"))
-        self.pausebtn.SetLabel(_("Pause"))
+        self.printbtn.SetLabel(_("&Print"))
+        self.pausebtn.SetLabel(_("&Pause"))
         self.pausebtn.Disable()
         self.recoverbtn.Disable()
-        if self.p.online:
+        if not failed and self.p.online:
             self.printbtn.Enable()
         self.toolbarsizer.Layout()
         self.viz_last_layer = None
         if print_stats:
             self.output_gcode_stats()
 
+    def calculate_remaining_filament(self, length, extruder = 0):
+        """
+        float calculate_remaining_filament( float length, int extruder )
+
+        Calculate the remaining length of filament for the given extruder if
+        the given length were to be extruded.
+        """
+
+        remainder = self.spool_manager.getRemainingFilament(extruder) - length
+        minimum_warning_length = 1000.0
+        if remainder < minimum_warning_length:
+            self.log(_("\nWARNING: Currently loaded spool for extruder " +
+            "%d will likely run out of filament during the print.\n" %
+            extruder))
+        return remainder
+
     def output_gcode_stats(self):
         gcode = self.fgcode
-        self.log(_("%.2fmm of filament used in this print") % gcode.filament_length)
-        if(len(gcode.filament_length_multi)>1):
+        self.spool_manager.refresh()
+
+        self.log(_("%s of filament used in this print") % format_length(gcode.filament_length))
+
+        if len(gcode.filament_length_multi) > 1:
             for i in enumerate(gcode.filament_length_multi):
-                print "Extruder %d: %0.02fmm" % (i[0],i[1])
+                if self.spool_manager.getSpoolName(i[0]) == None:
+                    logging.info("- Extruder %d: %0.02fmm" % (i[0], i[1]))
+                else:
+                    logging.info(("- Extruder %d: %0.02fmm" % (i[0], i[1]) +
+                        " from spool '%s' (%.2fmm will remain)" %
+                        (self.spool_manager.getSpoolName(i[0]),
+                        self.calculate_remaining_filament(i[1], i[0]))))
+        elif self.spool_manager.getSpoolName(0) != None:
+                self.log(
+                    _("Using spool '%s' (%s of filament will remain)") %
+                    (self.spool_manager.getSpoolName(0),
+                    format_length(self.calculate_remaining_filament(
+                        gcode.filament_length, 0))))
+
         self.log(_("The print goes:"))
         self.log(_("- from %.2f mm to %.2f mm in X and is %.2f mm wide") % (gcode.xmin, gcode.xmax, gcode.width))
         self.log(_("- from %.2f mm to %.2f mm in Y and is %.2f mm deep") % (gcode.ymin, gcode.ymax, gcode.depth))
@@ -1579,45 +1641,52 @@
         self.log(_("Estimated duration: %d layers, %s") % gcode.estimate_duration())
 
     def loadviz(self, gcode = None):
-        self.gviz.clear()
-        self.gwindow.p.clear()
-        if gcode is not None:
-            generator = self.gviz.addfile_perlayer(gcode, True)
-            next_layer = 0
-            # Progressive loading of visualization
-            # We load layers up to the last one which has been processed in GCoder
-            # (self.viz_last_layer)
-            # Once the GCode has been entirely loaded, this variable becomes None,
-            # indicating that we can do the last generator call to finish the
-            # loading of the visualization, which will itself return None.
-            # During preloading we verify that the layer we added is the one we
-            # expected through the assert call.
-            while True:
-                global pronterface_quitting
-                if pronterface_quitting:
-                    return
-                max_layer = self.viz_last_layer
-                if max_layer is None:
-                    break
-                while next_layer <= max_layer:
-                    assert(generator.next() == next_layer)
+        try:
+            self.gviz.clear()
+            self.gwindow.p.clear()
+            if gcode is not None:
+                generator = self.gviz.addfile_perlayer(gcode, True)
+                next_layer = 0
+                # Progressive loading of visualization
+                # We load layers up to the last one which has been processed in GCoder
+                # (self.viz_last_layer)
+                # Once the GCode has been entirely loaded, this variable becomes None,
+                # indicating that we can do the last generator call to finish the
+                # loading of the visualization, which will itself return None.
+                # During preloading we verify that the layer we added is the one we
+                # expected through the assert call.
+                while True:
+                    global pronterface_quitting
+                    if pronterface_quitting:
+                        return
+                    max_layer = self.viz_last_layer
+                    if max_layer is None:
+                        break
+                    start_layer = next_layer
+                    while next_layer <= max_layer:
+                        assert next(generator) == next_layer
+                        next_layer += 1
+                    if next_layer != start_layer:
+                        wx.CallAfter(self.gviz.Refresh)
+                    time.sleep(0.1)
+                generator_output = next(generator)
+                while generator_output is not None:
+                    assert generator_output == next_layer
                     next_layer += 1
-                time.sleep(0.1)
-            generator_output = generator.next()
-            while generator_output is not None:
-                assert(generator_output in (None, next_layer))
-                next_layer += 1
-                generator_output = generator.next()
-        else:
-            # If GCode is not being loaded asynchroneously, it is already
-            # loaded, so let's make visualization sequentially
-            gcode = self.fgcode
-            self.gviz.addfile(gcode)
-        wx.CallAfter(self.gviz.Refresh)
-        # Load external window sequentially now that everything is ready.
-        # We can't really do any better as the 3D viewer might clone the
-        # finalized model from the main visualization
-        self.gwindow.p.addfile(gcode)
+                    generator_output = next(generator)
+            else:
+                # If GCode is not being loaded asynchroneously, it is already
+                # loaded, so let's make visualization sequentially
+                gcode = self.fgcode
+                self.gviz.addfile(gcode)
+            wx.CallAfter(self.gviz.Refresh)
+            # Load external window sequentially now that everything is ready.
+            # We can't really do any better as the 3D viewer might clone the
+            # finalized model from the main visualization
+            self.gwindow.p.addfile(gcode)
+        except:
+            logging.error(traceback.format_exc())
+            wx.CallAfter(self.gviz.Refresh)
 
     #  --------------------------------------------------------------
     #  File saving handling
@@ -1662,10 +1731,12 @@
         pronsole.pronsole.endcb(self)
         if self.p.queueindex == 0:
             self.p.runSmallScript(self.endScript)
+            if self.settings.display_progress_on_printer:
+                printer_progress_string = "M117 Finished Print"
+                self.p.send_now(printer_progress_string)
             wx.CallAfter(self.pausebtn.Disable)
-            wx.CallAfter(self.printbtn.SetLabel, _("Print"))
+            wx.CallAfter(self.printbtn.SetLabel, _("&Print"))
             wx.CallAfter(self.toolbarsizer.Layout)
-            wx.CallAfter(self.endcb_lasercut)
 
     def online(self):
         """Callback when printer goes online"""
@@ -1674,9 +1745,9 @@
 
     def online_gui(self):
         """Callback when printer goes online (graphical bits)"""
-        self.connectbtn.SetLabel(_("Disconnect"))
+        self.connectbtn.SetLabel(_("Dis&connect"))
         self.connectbtn.SetToolTip(wx.ToolTip("Disconnect from the printer"))
-        self.connectbtn.Bind(wx.EVT_BUTTON, self.disconnect)
+        self.connectbtn_cb_var = self.disconnect
 
         if hasattr(self, "extrudersel"):
             self.do_tool(self.extrudersel.GetValue())
@@ -1841,7 +1912,7 @@
                 wx.CallAfter(self.pause)
             msg = l.split(" ", 1)
             if len(msg) > 1 and not self.p.loud:
-                wx.CallAfter(self.addtexttolog, msg[1] + "\n")
+                self.log(msg[1] + "\n")
             return True
         elif l.startswith("//"):
             command = l.split(" ", 1)
@@ -1874,8 +1945,8 @@
             elif report_type & REPORT_TEMP:
                 wx.CallAfter(self.tempdisp.SetLabel, self.tempreadings.strip().replace("ok ", ""))
                 self.update_tempdisplay()
-            if not self.p.loud and (l not in ["ok", "wait"] and (not isreport or report_type & REPORT_MANUAL)):
-                wx.CallAfter(self.addtexttolog, l + "\n")
+            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)):
+                self.log(l)
         for listener in self.recvlisteners:
             listener(l)
 
@@ -1887,7 +1958,7 @@
             self.recvlisteners.remove(self.listfiles)
             wx.CallAfter(self.filesloaded)
         elif self.sdlisting:
-            self.sdfiles.append(line.strip().lower())
+            self.sdfiles.append(re.sub(" \d+$","",line.strip().lower()))
 
     def waitforsdresponse(self, l):
         if "file.open failed" in l:
@@ -1931,7 +2002,7 @@
         for i, btndef in enumerate(custombuttons):
             if btndef is None:
                 if i == len(custombuttons) - 1:
-                    self.newbuttonbutton = b = wx.Button(self.centerpanel, -1, "+", size = (19, 18), style = wx.BU_EXACTFIT)
+                    self.newbuttonbutton = b = wx.Button(self.centerpanel, -1, "+", size = (35, 18), style = wx.BU_EXACTFIT)
                     b.SetForegroundColour("#4444ff")
                     b.SetToolTip(wx.ToolTip(_("click to add new custom button")))
                     b.Bind(wx.EVT_BUTTON, self.cbutton_edit)
@@ -1942,7 +2013,7 @@
                 b.SetToolTip(wx.ToolTip(_("Execute command: ") + btndef.command))
                 if btndef.background:
                     b.SetBackgroundColour(btndef.background)
-                    rr, gg, bb = b.GetBackgroundColour().Get()
+                    rr, gg, bb, aa = b.GetBackgroundColour().Get() #last item is alpha
                     if 0.3 * rr + 0.59 * gg + 0.11 * bb < 60:
                         b.SetForegroundColour("#ffffff")
                 b.custombutton = i
@@ -1951,7 +2022,7 @@
                 b.Bind(wx.EVT_BUTTON, self.process_button)
                 b.Bind(wx.EVT_MOUSE_EVENTS, self.editbutton)
             self.custombuttons_widgets.append(b)
-            if type(self.cbuttonssizer) == wx.GridBagSizer:
+            if isinstance(self.cbuttonssizer, wx.GridBagSizer):
                 self.cbuttonssizer.Add(b, pos = (i // 4, i % 4), flag = wx.EXPAND)
             else:
                 self.cbuttonssizer.Add(b, flag = wx.EXPAND)
@@ -1997,9 +2068,9 @@
             self.save_in_rc(("button %d" % n), '')
         elif bdef.background:
             colour = bdef.background
-            if type(colour) not in (str, unicode):
-                if type(colour) == tuple and tuple(map(type, colour)) == (int, int, int):
-                    colour = map(lambda x: x % 256, colour)
+            if not isinstance(colour, str):
+                if isinstance(colour, tuple) and tuple(map(type, colour)) == (int, int, int):
+                    colour = (x % 256 for x in colour)
                     colour = wx.Colour(*colour).GetAsString(wx.C2S_NAME | wx.C2S_HTML_SYNTAX)
                 else:
                     colour = wx.Colour(colour).GetAsString(wx.C2S_NAME | wx.C2S_HTML_SYNTAX)
@@ -2015,9 +2086,9 @@
             bedit.command.SetValue(button.properties.command)
             if button.properties.background:
                 colour = button.properties.background
-                if type(colour) not in (str, unicode):
-                    if type(colour) == tuple and tuple(map(type, colour)) == (int, int, int):
-                        colour = map(lambda x: x % 256, colour)
+                if not isinstance(colour, str):
+                    if isinstance(colour, tuple) and tuple(map(type, colour)) == (int, int, int):
+                        colour = (x % 256 for x in colour)
                         colour = wx.Colour(*colour).GetAsString(wx.C2S_NAME | wx.C2S_HTML_SYNTAX)
                     else:
                         colour = wx.Colour(colour).GetAsString(wx.C2S_NAME | wx.C2S_HTML_SYNTAX)
@@ -2080,7 +2151,7 @@
                 item = popupmenu.Append(-1, _("Add custom button"))
                 self.Bind(wx.EVT_MENU, self.cbutton_edit, item)
             self.panel.PopupMenu(popupmenu, pos)
-        elif e.Dragging() and e.ButtonIsDown(wx.MOUSE_BTN_LEFT):
+        elif e.Dragging() and e.LeftIsDown():
             obj = e.GetEventObject()
             scrpos = obj.ClientToScreen(e.GetPosition())
             if not hasattr(self, "dragpos"):
@@ -2089,14 +2160,14 @@
                 return
             else:
                 dx, dy = self.dragpos[0] - scrpos[0], self.dragpos[1] - scrpos[1]
-                if dx * dx + dy * dy < 5 * 5:  # threshold to detect dragging for jittery mice
+                if dx * dx + dy * dy < 30 * 30:  # threshold to detect dragging for jittery mice
                     e.Skip()
                     return
             if not hasattr(self, "dragging"):
                 # init dragging of the custom button
-                if hasattr(obj, "custombutton") and obj.properties is not None:
+                if hasattr(obj, "custombutton") and (not hasattr(obj,"properties") or obj.properties is not None):
                     for b in self.custombuttons_widgets:
-                        if b.properties is None:
+                        if not hasattr(b,"properties") or b.properties is None:
                             b.Enable()
                             b.SetLabel("")
                             b.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL))
@@ -2147,7 +2218,7 @@
                         src.SetForegroundColour(drg.fgc)
                         src.SetLabel(drg.label)
                     self.last_drag_dest = dst
-        elif hasattr(self, "dragging") and not e.ButtonIsDown(wx.MOUSE_BTN_LEFT):
+        elif hasattr(self, "dragging") and not e.LeftIsDown():
             # dragging finished
             obj = e.GetEventObject()
             scrpos = obj.ClientToScreen(e.GetPosition())
@@ -2158,7 +2229,7 @@
                 if b.GetScreenRect().Contains(scrpos):
                     dst = b
                     break
-            if dst is not None:
+            if dst is not None and hasattr(dst,"custombutton"):
                 src_i = src.custombutton
                 dst_i = dst.custombutton
                 self.custombuttons[src_i], self.custombuttons[dst_i] = self.custombuttons[dst_i], self.custombuttons[src_i]
@@ -2220,23 +2291,39 @@
         self.update_macros_menu()
 
     def new_macro(self, e = None):
-        dialog = wx.Dialog(self, -1, _("Enter macro name"), size = (260, 85))
-        panel = wx.Panel(dialog, -1)
-        vbox = wx.BoxSizer(wx.VERTICAL)
-        wx.StaticText(panel, -1, _("Macro name:"), (8, 14))
-        dialog.namectrl = wx.TextCtrl(panel, -1, '', (110, 8), size = (130, 24), style = wx.TE_PROCESS_ENTER)
-        hbox = wx.BoxSizer(wx.HORIZONTAL)
-        okb = wx.Button(dialog, wx.ID_OK, _("Ok"), size = (60, 24))
-        dialog.Bind(wx.EVT_TEXT_ENTER, lambda e: dialog.EndModal(wx.ID_OK), dialog.namectrl)
-        hbox.Add(okb)
-        hbox.Add(wx.Button(dialog, wx.ID_CANCEL, _("Cancel"), size = (60, 24)))
-        vbox.Add(panel)
-        vbox.Add(hbox, 1, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 10)
-        dialog.SetSizer(vbox)
+        dialog = wx.Dialog(self, -1, _("Enter macro name"))
+        text = wx.StaticText(dialog, -1, _("Macro name:"))
+        namectrl = wx.TextCtrl(dialog, -1, style = wx.TE_PROCESS_ENTER)
+        okb = wx.Button(dialog, wx.ID_OK, _("Ok"))
+        dialog.Bind(wx.EVT_TEXT_ENTER,
+            lambda e: dialog.EndModal(wx.ID_OK), namectrl)
+        cancel_button = wx.Button(dialog, wx.ID_CANCEL, _("Cancel"))
+
+        # Layout
+        ## Group the buttons horizontally
+        buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        buttons_sizer.Add(okb, 0)
+        buttons_sizer.Add(cancel_button, 0)
+        ## Set a minimum size for the name control box
+        min_size = namectrl.GetTextExtent('Default Long Macro Name')
+        namectrl.SetMinSize(wx.Size(min_size.width, -1))
+        ## Group the text and the name control box horizontally
+        name_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        name_sizer.Add(text, 0, flag = wx.ALIGN_CENTER)
+        name_sizer.AddSpacer(10)
+        name_sizer.Add(namectrl, 1, wx.EXPAND)
+        ## Group everything vertically
+        dialog_sizer = wx.BoxSizer(wx.VERTICAL)
+        dialog_sizer.Add(name_sizer, 0, border = 10,
+            flag = wx.LEFT | wx.TOP | wx.RIGHT)
+        dialog_sizer.Add(buttons_sizer, 0, border = 10,
+            flag = wx.ALIGN_CENTER | wx.ALL)
+        dialog.SetSizerAndFit(dialog_sizer)
         dialog.Centre()
+
         macro = ""
         if dialog.ShowModal() == wx.ID_OK:
-            macro = dialog.namectrl.GetValue()
+            macro = namectrl.GetValue()
             if macro != "":
                 wx.CallAfter(self.edit_macro, macro)
         dialog.Destroy()
@@ -2246,7 +2333,7 @@
         if macro == "": return self.new_macro()
         if macro in self.macros:
             old_def = self.macros[macro]
-        elif len([c for c in macro.encode("ascii", "replace") if not c.isalnum() and c != "_"]):
+        elif len([chr(c) for c in macro.encode("ascii", "replace") if not chr(c).isalnum() and chr(c) != "_"]):
             self.log(_("Macro name may contain only ASCII alphanumeric symbols and underscores"))
             return
         elif hasattr(self.__class__, "do_" + macro):
@@ -2264,7 +2351,7 @@
             while True:
                 item = self.macros_menu.FindItemByPosition(1)
                 if item is None: break
-                self.macros_menu.DeleteItem(item)
+                self.macros_menu.DestroyItem(item)
         except:
             pass
         for macro in self.macros.keys():
@@ -2280,10 +2367,19 @@
         orig_appname = self.app.GetAppName()
         self.app.SetAppName("Slic3r")
         configpath = wx.StandardPaths.Get().GetUserDataDir()
-        self.app.SetAppName(orig_appname)
         self.slic3r_configpath = configpath
         configfile = os.path.join(configpath, "slic3r.ini")
+        if not os.path.exists(configfile):
+            self.app.SetAppName("Slic3rPE")
+            configpath = wx.StandardPaths.Get().GetUserDataDir()
+            self.slic3r_configpath = configpath
+            configfile = os.path.join(configpath, "slic3r.ini")
+        if not os.path.exists(configfile):
+            self.settings.slic3rintegration=False;
+            return
+        self.app.SetAppName(orig_appname)
         config = self.read_slic3r_config(configfile)
+        version = config.get("dummy", "version") # Slic3r version
         self.slic3r_configs = {}
         for cat in menus:
             menu = menus[cat]
@@ -2291,6 +2387,8 @@
             files = sorted(glob.glob(pattern))
             try:
                 preset = config.get("presets", cat)
+                # Starting from Slic3r 1.3.0, preset names have no extension
+                if version.split(".") >= ["1","3","0"]: preset += ".ini"
                 self.slic3r_configs[cat] = preset
             except:
                 preset = None
@@ -2305,10 +2403,10 @@
 
     def read_slic3r_config(self, configfile, parser = None):
         """Helper to read a Slic3r configuration file"""
-        import ConfigParser
-        parser = ConfigParser.RawConfigParser()
+        import configparser
+        parser = configparser.RawConfigParser()
 
-        class add_header(object):
+        class add_header:
             def __init__(self, f):
                 self.f = f
                 self.header = '[dummy]'
@@ -2319,6 +2417,11 @@
                     finally: self.header = None
                 else:
                     return self.f.readline()
+
+            def __iter__(self):
+                import itertools
+                return itertools.chain([self.header], iter(self.f))
+
         parser.readfp(add_header(open(configfile)), configfile)
         return parser
 
@@ -2327,7 +2430,12 @@
         self.slic3r_configs[cat] = file
         if self.settings.slic3rupdate:
             config = self.read_slic3r_config(configfile)
-            config.set("presets", cat, os.path.basename(file))
+            version = config.get("dummy", "version") # Slic3r version
+            preset = os.path.basename(file)
+            # Starting from Slic3r 1.3.0, preset names have no extension
+            if version.split(".") >= ["1","3","0"]:
+                preset = os.path.splitext(preset)[0]
+            config.set("presets", cat, preset)
             f = StringIO.StringIO()
             config.write(f)
             data = f.getvalue()
@@ -2343,5 +2451,6 @@
     def __init__(self, *args, **kwargs):
         super(PronterApp, self).__init__(*args, **kwargs)
         self.SetAppName("Pronterface")
+        self.locale = wx.Locale(wx.Locale.GetSystemLanguage())
         self.mainwindow = PronterWindow(self)
         self.mainwindow.Show()

mercurial