diff --git a/printrun/excluder.py b/printrun/excluder.py index ad07dad39..656b4cf8e 100644 --- a/printrun/excluder.py +++ b/printrun/excluder.py @@ -26,8 +26,12 @@ def __init__(self, excluder, *args, **kwargs): self.SetTitle(_("Print Excluder")) self.parent = excluder - self.toolbar.ClearTools() - self.build_toolbar(excluder = True) + tool_pos = self.toolbar.GetToolPos(6) + self.toolbar.InsertTool(tool_pos, 8, _('Reset Selection'), + wx.Image(imagefile('reset.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), + shortHelp = _("Reset Selection")) + self.toolbar.DeleteTool(6) + self.toolbar.DeleteTool(7) self.toolbar.Realize() minsize = self.toolbar.GetEffectiveMinSize().width self.SetMinClientSize((minsize, minsize)) diff --git a/printrun/gcview.py b/printrun/gcview.py index b5bb29f7a..0c763845a 100755 --- a/printrun/gcview.py +++ b/printrun/gcview.py @@ -30,6 +30,7 @@ from .gviz import GvizBaseFrame +from .gui.widgets import get_space from .utils import imagefile, install_locale, get_home_pos install_locale('pronterface') @@ -154,7 +155,7 @@ def draw_objects(self): if not obj.model \ or not obj.model.loaded: continue - # Skip (comment out) initialized check, which safely causes empty + # Skip (comment out) initialized check, which safely causes empty # model during progressive load. This can cause exceptions/garbage # render, but seems fine for now # May need to lock init() and draw_objects() together @@ -238,7 +239,7 @@ def layerdown(self): def handle_wheel(self, event): if self.wheelTimestamp == event.Timestamp: # filter duplicate event delivery in Ubuntu, Debian issue #1110 - return + return self.wheelTimestamp = event.Timestamp @@ -251,8 +252,10 @@ def handle_wheel(self, event): return count = 1 if not event.ControlDown() else 10 for i in range(count): - if delta > 0: self.layerup() - else: self.layerdown() + if delta > 0: + self.layerup() + else: + self.layerdown() return x, y = event.GetPosition() * self.GetContentScaleFactor() x, y, _ = self.mouse_to_3d(x, y) @@ -426,7 +429,7 @@ def __init__(self, parent, ID, title, build_dimensions, objects = None, pos, size, style) self.root = root - panel, vbox = self.create_base_ui() + panel, h_sizer = self.create_base_ui() self.refresh_timer = wx.CallLater(100, self.Refresh) self.p = self # Hack for backwards compatibility with gviz API @@ -436,24 +439,35 @@ def __init__(self, parent, ID, title, build_dimensions, objects = None, self.objects = [GCObject(self.platform), GCObject(None)] fit_image = wx.Image(imagefile('fit.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap() - self.toolbar.InsertTool(6, 8, " " + _("Fit to plate"), fit_image, - shortHelp = _("Fit to plate [F]"), - longHelp = '') + tool_pos = self.toolbar.GetToolPos(3) + 1 + self.toolbar.InsertTool(tool_pos, 10, " " + _("Fit to plate"), fit_image, + shortHelp = _("Fit to plate [F]"), longHelp = '') self.toolbar.Realize() self.glpanel = GcodeViewPanel(panel, build_dimensions = build_dimensions, realparent = self, antialias_samples = antialias_samples, perspective=perspective) - vbox.Add(self.glpanel, 1, flag = wx.EXPAND) - self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.zoom_to_center(1.2), id = 1) - self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.zoom_to_center(1 / 1.2), id = 2) - self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.layerup(), id = 3) + if self.root and hasattr(self.root, "gcview_color_background"): + self.glpanel.color_background = self.root.gcview_color_background + + h_sizer.Add(self.glpanel, 1, wx.EXPAND) + h_sizer.Add(self.layerslider, 0, wx.EXPAND | wx.ALL, get_space('minor')) + self.glpanel.SetToolTip("Left-click to pan, right-click to move the view " + "and shift + scroll to change the layer") + + minsize = self.toolbar.GetEffectiveMinSize().width + self.SetMinClientSize((minsize, minsize)) + + self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.zoom_to_center(1 / 1.2), id = 1) + self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.zoom_to_center(1.2), id = 2) + self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.resetview(), id = 3) self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.layerdown(), id = 4) - self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.resetview(), id = 5) - self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.fit(), id = 8) + self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.layerup(), id = 5) + self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.fit(), id = 10) self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.inject(), id = 6) self.Bind(wx.EVT_TOOL, lambda x: self.glpanel.editlayer(), id = 7) + self.Bind(wx.EVT_TOOL, lambda x: self.Close(), id = 9) def setlayercb(self, layer): self.layerslider.SetValue(layer) @@ -465,9 +479,9 @@ def update_status(self, extra): if filtered: true_layer = filtered[0] z = self.model.gcode.all_layers[true_layer].z - message = _("Layer %d -%s Z = %.03f mm") % (layer, extra, z) + message = _("Layer %d: %s Z = %.03f mm") % (layer, extra, z) else: - message = _("Entire object") + message = _("Last layer: Object complete") wx.CallAfter(self.SetStatusText, message, 0) def process_slider(self, event): @@ -479,7 +493,7 @@ def process_slider(self, event): self.update_status("") wx.CallAfter(self.Refresh) else: - logging.info(_("G-Code view, can't process slider. Please wait until model is loaded completely.")) + logging.info(_("G-Code Viewer: Can't process slider. Please wait until model is loaded completely.")) def set_current_gline(self, gline): if gline.is_move and gline.gcview_end_vertex is not None \ @@ -499,7 +513,7 @@ def addfile(self, gcode = None): GcodeViewLoader.addfile(self, gcode) self.layerslider.SetRange(1, self.model.max_layers + 1) self.layerslider.SetValue(self.model.max_layers + 1) - wx.CallAfter(self.SetStatusText, _("Entire object"), 0) + wx.CallAfter(self.SetStatusText, _("Last layer: Object complete"), 0) wx.CallAfter(self.Refresh) def clear(self): @@ -511,7 +525,7 @@ def clear(self): import sys app = wx.App(redirect = False) build_dimensions = [200, 200, 100, 0, 0, 0] - title = 'Gcode view, shift to move view, mousewheel to set layer' + title = 'G-Code Viewer' frame = GcodeViewFrame(None, wx.ID_ANY, title, size = (400, 400), build_dimensions = build_dimensions) gcode = gcoder.GCode(open(sys.argv[1]), get_home_pos(build_dimensions)) diff --git a/printrun/gui/viz.py b/printrun/gui/viz.py index eb4c4ebaa..bab926b93 100644 --- a/printrun/gui/viz.py +++ b/printrun/gui/viz.py @@ -98,7 +98,7 @@ def __init__(self, root, parentpanel = None): objects = None if isinstance(root.gviz, printrun.gcview.GcodeViewMainWrapper): objects = root.gviz.objects - root.gwindow = printrun.gcview.GcodeViewFrame(None, wx.ID_ANY, 'Gcode view, shift to move view, mousewheel to set layer', + root.gwindow = printrun.gcview.GcodeViewFrame(None, wx.ID_ANY, 'G-Code Viewer', size = (600, 600), build_dimensions = root.build_dimensions_list, objects = objects, diff --git a/printrun/gui/widgets.py b/printrun/gui/widgets.py index dc5291ace..b91339fc2 100644 --- a/printrun/gui/widgets.py +++ b/printrun/gui/widgets.py @@ -13,80 +13,76 @@ # You should have received a copy of the GNU General Public License # along with Printrun. If not, see . -import wx import re -import platform +import string # For determining whitespaces and punctuation marks +import platform # Used by get_space() for platform specific spacing +import logging +import wx -def get_space(a): +def get_space(key: str) -> int: ''' Takes key (str), returns spacing value (int). Provides correct spacing in pixel for borders and sizers. ''' - match a: - case 'major': # e.g. outer border of dialog boxes - return 12 - case 'minor': # e.g. border of inner elements - return 8 - case 'mini': - return 4 - case 'stddlg': - # Differentiation is necessary because wxPython behaves slightly differently on different systems. - platformname = platform.system() - if platformname == 'Windows': - return 8 - if platformname == 'Darwin': - return 4 - return 4 # Linux systems - case 'stddlg-frame': - # Border for std dialog buttons when used with frames. - platformname = platform.system() - if platformname == 'Windows': - return 8 - if platformname == 'Darwin': - return 12 - return 8 # Linux systems - case 'staticbox': - # Border between StaticBoxSizers and the elements inside. - platformname = platform.system() - if platformname == 'Windows': - return 4 - if platformname == 'Darwin': - return 0 - return 0 # Linux systems - case 'none': - return 0 + + spacing_values = { + 'major': 12, # e.g. outer border of dialog boxes + 'minor': 8, # e.g. border of inner elements + 'mini': 4, + 'stddlg': 4, # Border around std dialog buttons. + 'stddlg-frame': 8, # Border around std dialog buttons when used with frames. + 'staticbox': 0, # Border between StaticBoxSizers and the elements inside. + 'settings': 16, # How wide setting elements can be (multiples of this) + 'none': 0 + } + + # Platform specific overrides, Windows + if platform.system() == 'Windows': + spacing_values['stddlg'] = 8 + spacing_values['staticbox'] = 4 + + # Platform specific overrides, macOS + if platform.system() == 'Darwin': + spacing_values['stddlg-frame'] = 12 + + try: + return spacing_values[key] + except KeyError: + logging.warning("get_space() cannot return spacing value, " + "will return 0 instead. No entry: %s" % key) + return 0 + class MacroEditor(wx.Dialog): """Really simple editor to edit macro definitions""" def __init__(self, macro_name, definition, callback, gcode = False): self.indent_chars = " " - title = " Macro %s" - if gcode: - title = " %s" + title = "%s" if gcode else "Macro %s" self.gcode = gcode + self.fr_settings = (False, False, True, '') wx.Dialog.__init__(self, None, title = title % macro_name, style = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER) self.callback = callback panel = wx.Panel(self) panelsizer = wx.BoxSizer(wx.VERTICAL) titlesizer = wx.BoxSizer(wx.HORIZONTAL) - self.titletext = wx.StaticText(panel, -1, " _") # title%macro_name) - titlesizer.Add(self.titletext, 1, wx.ALIGN_CENTER_VERTICAL) - self.findbtn = wx.Button(panel, -1, _(" Find "), style = wx.BU_EXACTFIT) # New button for "Find" (Jezmy) + self.status_field = wx.StaticText(panel, -1, "") + titlesizer.Add(self.status_field, 1, wx.ALIGN_CENTER_VERTICAL) + self.findbtn = wx.Button(panel, -1, _("Find...")) # New button for "Find" (Jezmy) self.findbtn.Bind(wx.EVT_BUTTON, self.on_find) - self.Bind(wx.EVT_FIND, self.on_find_find) - self.Bind(wx.EVT_FIND_NEXT, self.on_find_find) - self.Bind(wx.EVT_FIND_CLOSE, self.on_find_cancel) - self.Bind(wx.EVT_CLOSE, self.close) + self.Bind(wx.EVT_CLOSE, self.on_close) + titlesizer.Add(self.findbtn, 0, wx.ALIGN_CENTER_VERTICAL) panelsizer.Add(titlesizer, 0, wx.EXPAND | wx.ALL, get_space('minor')) - self.e = wx.TextCtrl(panel, style = wx.HSCROLL | wx.TE_MULTILINE | wx.TE_RICH2, size = (400, 400)) + self.text_box = wx.TextCtrl(panel, + style = wx.HSCROLL | wx.TE_MULTILINE | wx.TE_RICH2 | wx.TE_NOHIDESEL, + size = (400, 400)) if not self.gcode: - self.e.SetValue(self.unindent(definition)) + self.text_box.SetValue(self.unindent(definition)) else: - self.e.SetValue("\n".join(definition)) - panelsizer.Add(self.e, 1, wx.EXPAND) + self.text_box.SetValue("\n".join(definition)) + panelsizer.Add(self.text_box, 1, wx.EXPAND) panel.SetSizer(panelsizer) topsizer = wx.BoxSizer(wx.VERTICAL) topsizer.Add(panel, 1, wx.EXPAND | wx.ALL, get_space('none')) @@ -94,9 +90,9 @@ def __init__(self, macro_name, definition, callback, gcode = False): btnsizer = wx.StdDialogButtonSizer() self.savebtn = wx.Button(self, wx.ID_SAVE) self.savebtn.SetDefault() - self.savebtn.Bind(wx.EVT_BUTTON, self.save) + self.savebtn.Bind(wx.EVT_BUTTON, self.on_save) self.cancelbtn = wx.Button(self, wx.ID_CANCEL) - self.cancelbtn.Bind(wx.EVT_BUTTON, self.close) + self.cancelbtn.Bind(wx.EVT_BUTTON, self.on_close) btnsizer.AddButton(self.savebtn) btnsizer.AddButton(self.cancelbtn) btnsizer.Realize() @@ -106,78 +102,35 @@ def __init__(self, macro_name, definition, callback, gcode = False): topsizer.Fit(self) self.CentreOnParent() self.Show() - self.e.SetFocus() - - def on_find(self, ev): - # Ask user what to look for, find it and point at it ... (Jezmy) - self.findbtn.Disable() - self.finddata = wx.FindReplaceData(wx.FR_DOWN) # initializes and holds search parameters - selection = self.e.GetStringSelection() - if selection: - self.finddata.SetFindString(selection) - self.finddialog = wx.FindReplaceDialog(self.e, self.finddata, _("Find..."), wx.FR_NOWHOLEWORD) # wx.FR_REPLACEDIALOG - # TODO: Setup ReplaceDialog, Setup WholeWord Search, deactivated for now... - self.finddialog.Show() - - def on_find_cancel(self, ev): - self.findbtn.Enable() - self.titletext.SetLabel(" _") - self.finddialog.Destroy() - - def on_find_find(self, ev): - findstring = self.finddata.GetFindString() - macrocode = self.e.GetValue() - - if self.e.GetStringSelection().lower() == findstring.lower(): - # If the desired string is already selected, change the position to jump to the next one. - if self.finddata.GetFlags() % 2 == 1: - self.e.SetInsertionPoint(self.e.GetInsertionPoint() + len(findstring)) - else: - self.e.SetInsertionPoint(self.e.GetInsertionPoint() - len(findstring)) + self.text_box.SetFocus() - if int(self.finddata.GetFlags() / 4) != 1: - # When search is not case-sensitve, convert the whole string to lowercase - findstring = findstring.casefold() - macrocode = macrocode.casefold() + def on_find(self, event): + for window in self.GetChildren(): + if isinstance(window, wx.FindReplaceDialog): + window.Show() + window.Raise() + return + FindAndReplace(self.text_box, self.status_field, self.fr_settings, self.fr_callback) - # The user can choose to search upwards or downwards - if self.finddata.GetFlags() % 2 == 1: - stringpos = macrocode.find(findstring, self.e.GetInsertionPoint()) - else: - stringpos = macrocode.rfind(findstring, 0, self.e.GetInsertionPoint()) - - if stringpos == -1 and self.finddata.GetFlags() % 2 == 1: - self.titletext.SetLabel(_("End of macro, jumped to first line")) - stringpos = 0 # jump to the beginning - self.e.SetInsertionPoint(stringpos) - self.e.ShowPosition(stringpos) - elif stringpos == -1 and self.finddata.GetFlags() % 2 == 0: - self.titletext.SetLabel(_("Begin of macro, jumped to last line")) - stringpos = self.e.GetLastPosition() # jump to the end - self.e.SetInsertionPoint(stringpos) - self.e.ShowPosition(stringpos) - else: - # TODO: Implement a Not Found state when no single match was found - self.titletext.SetLabel(_("Found!")) - self.e.SetSelection(stringpos, stringpos + len(findstring)) - self.e.ShowPosition(stringpos) + def fr_callback(self, val1, val2, val3, val4): + self.fr_settings = (val1, val2, val3, val4) - def ShowMessage(self, ev, message): - dlg = wx.MessageDialog(self, message, - "Info!", wx.OK | wx.ICON_INFORMATION) - dlg.ShowModal() - dlg.Destroy() - - def save(self, ev): + def on_save(self, event): self.Destroy() if not self.gcode: - self.callback(self.reindent(self.e.GetValue())) + self.callback(self.reindent(self.text_box.GetValue())) else: - self.callback(self.e.GetValue().split("\n")) + self.callback(self.text_box.GetValue().split("\n")) - def close(self, ev): + def on_close(self, event): self.Destroy() + def ShowMessage(self, event, message): + dlg = wx.MessageDialog(self, message, + "Info!", wx.OK | wx.ICON_INFORMATION) + dlg.ShowModal() + dlg.Destroy() + def unindent(self, text): self.indent_chars = text[:len(text) - len(text.lstrip())] if len(self.indent_chars) == 0: @@ -203,17 +156,294 @@ def reindent(self, text): reindented += self.indent_chars + line + "\n" return reindented - -SETTINGS_GROUPS = {"Printer": _("Printer settings"), - "UI": _("User interface"), +class FindAndReplace(): + '''A dialogue that provides full functionality for finding + and replacing strings in a given target string. + ''' + def __init__(self, text_cntrl: wx.TextCtrl, + statusbar: wx.StaticText, + settings: tuple = (False, False, True, ''), + settings_cb = None): + + self.matchcase = settings[0] # wx.FR_MATCHCASE + self.wholeword = settings[1] # wx.FR_WHOLEWORD + self.down = settings[2] # wx.FR_DOWN + self.callback = settings_cb + + self.statusbar = statusbar + self.text_cntrl = text_cntrl + self.find_str = settings[3] + self.replace_str = "" + self.target = "" + self.all_matches = 0 + self.current_match = 0 + + if self.text_cntrl.IsEmpty(): + self.statusbar.SetLabel(_("No content to search.")) + return + + # Initialise and hold search parameters in fr_data + self.fr_data = wx.FindReplaceData(self.bools_to_flags(settings)) + selection = text_cntrl.GetStringSelection() + if selection and not len(selection) > 40 and selection not in ('\n', '\r'): + self.find_str = selection + self.fr_data.SetFindString(self.find_str) + self.fr_dialog = wx.FindReplaceDialog(self.text_cntrl, + self.fr_data, _("Find and Replace..."), + wx.FR_REPLACEDIALOG) + + # Bind all button events + self.fr_dialog.Bind(wx.EVT_FIND, self.on_find) + self.fr_dialog.Bind(wx.EVT_FIND_NEXT, self.on_find_next) + self.fr_dialog.Bind(wx.EVT_FIND_REPLACE, self.on_replace) + self.fr_dialog.Bind(wx.EVT_FIND_REPLACE_ALL, self.on_replace_all) + self.fr_dialog.Bind(wx.EVT_FIND_CLOSE, self.on_cancel) + + # Move the dialogue to the side of the editor where there is more space + display_size = wx.Display(self.fr_dialog).GetClientArea() + ed_x, ed_y, ed_width, ed_height = self.fr_dialog.GetParent().GetRect() + fr_x, fr_y, fr_width, fr_height = self.fr_dialog.GetRect() + if display_size[2] - ed_x - ed_width < fr_width: + fr_x = ed_x - fr_width + else: + fr_x = ed_x + ed_width - 16 + self.fr_dialog.SetRect((fr_x, fr_y, fr_width, fr_height)) + self.fr_dialog.Show() + + def update_data(self): + '''Reads the current settings of the FindReplaceDialog and updates + all relevant strings of the search feature. + ''' + # Update flags + flags_binary = bin(self.fr_data.GetFlags())[2:].zfill(3) + self.down = bool(int(flags_binary[2])) + self.wholeword = bool(int(flags_binary[1])) + self.matchcase = bool(int(flags_binary[0])) + + # Update search data + self.find_str = self.fr_data.GetFindString() + self.replace_str = self.fr_data.GetReplaceString() + self.target = self.text_cntrl.GetRange(0, self.text_cntrl.GetLastPosition()) + if not self.find_str: + self.statusbar.SetLabel("") + + if not self.matchcase: + # When search is not case-sensitve, convert the whole string to lowercase + self.find_str = self.find_str.casefold() + self.target = self.target.casefold() + + def find_next(self): + self.update_data() + if not self.update_all_matches(): + return + + # If the search string is already selected, move + # the InsertionPoint and then select the next match + idx = self.text_cntrl.GetInsertionPoint() + selection = self.get_selected_str() + if selection == self.find_str: + sel_from, sel_to = self.text_cntrl.GetSelection() + self.text_cntrl.SelectNone() + if self.down: + self.text_cntrl.SetInsertionPoint(sel_to) + idx = sel_to + else: + self.text_cntrl.SetInsertionPoint(sel_from) + idx = sel_from + + self.select_next_match(idx) + + def replace_next(self): + '''Replaces one time the next instance of the search string + in the defined direction. + ''' + self.update_data() + if not self.update_all_matches(): + return + + # If the search string is already selected, replace it. + # Otherwise find the next match an replace that one. + # The while loop helps us with the wholeword checks + if self.get_selected_str() == self.find_str: + sel_from, sel_to = self.text_cntrl.GetSelection() + else: + sel_from = self.get_next_idx(self.text_cntrl.GetInsertionPoint()) + sel_to = sel_from + len(self.find_str) + self.text_cntrl.SelectNone() + self.text_cntrl.Replace(sel_from, sel_to, self.replace_str) + + # The text_cntrl object is changed directly so + # we need to update the copy in self.target + self.update_data() + + self.all_matches -= 1 + if not self.all_matches: + self.statusbar.SetLabel(_('No matches')) + return + self.select_next_match(sel_from) + + def replace_all(self): + '''Goes through the whole file and replaces + every instance of the search string. + ''' + position = self.text_cntrl.GetInsertionPoint() + self.update_data() + if not self.update_all_matches(): + return + + self.text_cntrl.SelectNone() + seek_idx = 0 + for match in range(self.all_matches): + sel_from = self.get_next_idx(seek_idx) + sel_to = sel_from + len(self.find_str) + self.text_cntrl.Replace(sel_from, sel_to, self.replace_str) + seek_idx = sel_from + self.update_data() + + self.statusbar.SetLabel(_('Replaced {} matches').format(self.all_matches)) + self.all_matches = 0 + self.text_cntrl.SetInsertionPoint(position) + self.text_cntrl.ShowPosition(position) + + def bools_to_flags(self, bools: tuple | list) -> int: + '''Converts a tuple of bool settings into an integer + that is readable for wx.FindReplaceData''' + matchcase = wx.FR_MATCHCASE if bools[0] else 0 + wholeword = wx.FR_WHOLEWORD if bools[1] else 0 + down = wx.FR_DOWN if bools[2] else 0 + return matchcase | wholeword | down + + def get_selected_str(self) -> str: + '''Returns the currently selected string, accounting for matchcase.''' + selection = self.text_cntrl.GetStringSelection() + if not self.matchcase: + selection = selection.casefold() + return selection + + def get_next_idx(self, position: int) -> int: + '''Searches for the next instance of the search string + in the defined direction. + Takes wholeword setting into account. + Returns index of the first character. + ''' + while True: + if self.down: + next_idx = self.target.find(self.find_str, position) + if next_idx == -1: + next_idx = self.target.find(self.find_str, 0, position) + if not self.wholeword or (self.wholeword and self.is_wholeword(next_idx)): + break + position = next_idx + len(self.find_str) + else: + next_idx = self.target.rfind(self.find_str, 0, position) + if next_idx == -1: + next_idx = self.target.rfind(self.find_str, position) + if not self.wholeword or (self.wholeword and self.is_wholeword(next_idx)): + break + position = next_idx + return next_idx + + def update_all_matches(self) -> bool: + '''Updates self.all_matches with the amount of search + string instances in the target string. + ''' + self.all_matches = 0 + if self.wholeword: + selection = self.text_cntrl.GetSelection() + self.text_cntrl.SetInsertionPoint(0) + seek_idx = 0 + found_idx = 0 + while found_idx != -1: + found_idx = self.target.find(self.find_str, seek_idx) + if found_idx == -1: + break + if self.is_wholeword(found_idx): + self.all_matches += 1 + seek_idx = found_idx + len(self.find_str) + self.text_cntrl.SetSelection(selection[0], selection[1]) + else: + self.all_matches = self.target.count(self.find_str) + + if not self.all_matches: + self.statusbar.SetLabel(_('No matches')) + return False + return True + + def select_next_match(self, position: int): + '''Selects the next match in the defined direction.''' + idx = self.get_next_idx(position) + + self.text_cntrl.SetSelection(idx, idx + len(self.find_str)) + self.text_cntrl.ShowPosition(idx) + self.update_current_match() + + def update_current_match(self): + '''Updates the current match index.''' + self.current_match = 0 + position = self.text_cntrl.GetInsertionPoint() + if self.wholeword: + selection = self.text_cntrl.GetSelection() + seek_idx = position + found_idx = 0 + while found_idx != -1: + found_idx = self.target.rfind(self.find_str, 0, seek_idx) + if found_idx == -1: + break + if self.is_wholeword(found_idx): + self.current_match += 1 + seek_idx = found_idx + self.current_match += 1 # We counted all matches before the current, therefore +1 + self.text_cntrl.SetSelection(selection[0], selection[1]) + else: + self.current_match = self.target.count(self.find_str, 0, position) + 1 + + self.statusbar.SetLabel(_('Match {} out of {}').format(self.current_match, self.all_matches)) + + def is_wholeword(self, index: int) -> bool: + '''Returns True if the search string is a whole word. + That is, if it is enclosed in spaces, line breaks, or + the very start or end of the target string. + ''' + start_idx = index + delimiter = string.whitespace + string.punctuation + if start_idx != 0 and self.target[start_idx - 1] not in delimiter: + return False + end_idx = start_idx + len(self.find_str) + if not end_idx > len(self.target) and self.target[end_idx] not in delimiter: + return False + return True + + def on_find_next(self, event): + self.find_next() + + def on_find(self, event): + self.find_next() + + def on_replace(self, event): + self.replace_next() + + def on_replace_all(self, event): + self.replace_all() + + def on_cancel(self, event): + self.statusbar.SetLabel("") + if self.callback: + self.update_data() + self.callback(self.matchcase, self.wholeword, + self.down, self.find_str) + self.fr_dialog.Destroy() + + +SETTINGS_GROUPS = {"Printer": _("Printer Settings"), + "UI": _("User Interface"), "Viewer": _("Viewer"), "Colors": _("Colors"), - "External": _("External commands")} + "External": _("External Commands")} class PronterOptionsDialog(wx.Dialog): """Options editor""" def __init__(self, pronterface): - wx.Dialog.__init__(self, parent = None, title = _("Edit settings"), + wx.Dialog.__init__(self, parent = None, title = _("Edit Settings"), size = wx.DefaultSize, style = wx.DEFAULT_DIALOG_STYLE) self.notebook = notebook = wx.Notebook(self) all_settings = pronterface.settings._all_settings() @@ -294,29 +524,29 @@ def PronterOptions(pronterface): class ButtonEdit(wx.Dialog): """Custom button edit dialog""" def __init__(self, pronterface): - wx.Dialog.__init__(self, None, title = _("Custom button"), + wx.Dialog.__init__(self, None, title = _("Custom Button"), style = wx.DEFAULT_DIALOG_STYLE) self.pronterface = pronterface panel = wx.Panel(self) grid = wx.FlexGridSizer(rows = 0, cols = 2, hgap = get_space('minor'), vgap = get_space('minor')) grid.AddGrowableCol(1, 1) - ## Title of the button - grid.Add(wx.StaticText(panel, -1, _("Button title:")), 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + # Title of the button + grid.Add(wx.StaticText(panel, -1, _("Button Title:")), 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) self.name = wx.TextCtrl(panel, -1, "") dlg_size = 260 self.name.SetMinSize(wx.Size(dlg_size, -1)) grid.Add(self.name, 1, wx.EXPAND) - ## Colour of the button + # Colour of the button grid.Add(wx.StaticText(panel, -1, _("Colour:")), 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) coloursizer = wx.BoxSizer(wx.HORIZONTAL) self.use_colour = wx.CheckBox(panel, -1) self.color = wx.ColourPickerCtrl(panel, colour=(255, 255, 255), style=wx.CLRP_USE_TEXTCTRL) self.color.Disable() - self.use_colour.Bind(wx.EVT_CHECKBOX, self.onColourCheckbox) + self.use_colour.Bind(wx.EVT_CHECKBOX, self.toggle_colour) coloursizer.Add(self.use_colour, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, get_space('minor')) coloursizer.Add(self.color, 1, wx.EXPAND) grid.Add(coloursizer, 1, wx.EXPAND) - ## Enter commands or choose a macro + # Enter commands or choose a macro macrotooltip = _("Type short commands directly, enter a name for a new macro or select an existing macro from the list.") commandfield = wx.StaticText(panel, -1, _("Command:")) commandfield.SetToolTip(wx.ToolTip(macrotooltip)) @@ -344,7 +574,7 @@ def __init__(self, pronterface): self.CentreOnParent() self.name.SetFocus() - def macrob_enabler(self, e): + def macrob_enabler(self, event): macro = self.command.GetValue() valid = False try: @@ -369,19 +599,16 @@ def macrob_enabler(self, e): valid = True self.macrobtn.Enable(valid) - def macrob_handler(self, e): + def macrob_handler(self, event): macro = self.command.GetValue() macro = self.pronterface.edit_macro(macro) self.command.SetValue(macro) if self.name.GetValue() == "": self.name.SetValue(macro) - def onColourCheckbox(self, e): - status = self.use_colour.GetValue() - if status: - self.color.Enable() - else: - self.color.Disable() + def toggle_colour(self, event): + self.color.Enable(self.use_colour.GetValue()) + class TempGauge(wx.Panel): @@ -427,7 +654,7 @@ def interpolatedColour(self, val, vmin, vmid, vmax, cmin, cmid, cmax): rgb = (int(x * 0.8) for x in rgb) return wx.Colour(*rgb) - def paint(self, ev): + def paint(self, event): self.width, self.height = self.GetClientSize() self.recalc() x0, y0, x1, y1, xE, yE = 1, 1, self.ypt + 1, 1, self.width + 1 - 2, 20 diff --git a/printrun/gviz.py b/printrun/gviz.py index 5563ebd9c..793ff7515 100644 --- a/printrun/gviz.py +++ b/printrun/gviz.py @@ -37,7 +37,7 @@ def create_base_ui(self): h_sizer = wx.BoxSizer(wx.HORIZONTAL) self.toolbar = wx.ToolBar(panel, -1, style = wx.TB_HORIZONTAL | wx.NO_BORDER | wx.TB_HORZ_TEXT) - self.build_toolbar(excluder = False) + self.build_toolbar() self.toolbar.Realize() v_sizer.Add(self.toolbar, 0, wx.EXPAND) v_sizer.Add(h_sizer, 1, wx.EXPAND) @@ -48,26 +48,25 @@ def create_base_ui(self): return panel, h_sizer - def build_toolbar(self, excluder): + def build_toolbar(self): self.toolbar.SetMargins(get_space('minor'), get_space('mini')) self.toolbar.SetToolPacking(get_space('minor')) - self.toolbar.AddTool(1, '', wx.Image(imagefile('zoom_in.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), _("Zoom In [+]"),) - self.toolbar.AddTool(2, '', wx.Image(imagefile('zoom_out.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), _("Zoom Out [-]")) - self.toolbar.AddTool(3, _("Reset View"), wx.Image(imagefile('fit.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), shortHelp = _("Reset View")) + self.toolbar.AddTool(1, '', wx.Image(imagefile('zoom_out.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), _("Zoom Out [-]")) + self.toolbar.AddTool(2, '', wx.Image(imagefile('zoom_in.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), _("Zoom In [+]"),) + self.toolbar.AddTool(3, _("Reset View"), wx.Image(imagefile('fit.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), + shortHelp = _("Reset View")) self.toolbar.AddSeparator() - self.toolbar.AddTool(4, '', wx.Image(imagefile('arrow_up.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), _("Move Up a Layer [U]")) - self.toolbar.AddTool(5, '', wx.Image(imagefile('arrow_down.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), _("Move Down a Layer [D]")) + self.toolbar.AddTool(4, '', wx.Image(imagefile('arrow_down.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), _("Move Down a Layer [D]")) + self.toolbar.AddTool(5, '', wx.Image(imagefile('arrow_up.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), _("Move Up a Layer [U]")) self.toolbar.AddSeparator() - if not excluder: # This is added to the 'GCode Viewer' Toolbar - self.toolbar.AddTool(6, 'Inject', wx.Image(imagefile('inject.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), - wx.NullBitmap, shortHelp = _("Inject G-Code"), longHelp = _("Insert code at the beginning of this layer")) - self.toolbar.AddTool(7, 'Edit', wx.Image(imagefile('edit.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), - wx.NullBitmap, shortHelp = _("Edit Layer"), longHelp = _("Edit the G-Code of this layer")) - else: # This is added to the 'Excluder' Toolbar - self.toolbar.AddTool(8, _('Reset Selection'), wx.Image(imagefile('reset.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), shortHelp = _("Reset Selection")) + self.toolbar.AddTool(6, 'Inject', wx.Image(imagefile('inject.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), + wx.NullBitmap, shortHelp = _("Inject G-Code"), longHelp = _("Insert code at the beginning of this layer")) + self.toolbar.AddTool(7, 'Edit', wx.Image(imagefile('edit.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), + wx.NullBitmap, shortHelp = _("Edit Layer"), longHelp = _("Edit the G-Code of this layer")) self.toolbar.AddStretchableSpace() self.toolbar.AddSeparator() - self.toolbar.AddTool(9, _('Close'), wx.Image(imagefile('fit.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), shortHelp = _("Close Window")) + self.toolbar.AddTool(9, _('Close'), wx.Image(imagefile('fit.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap(), + shortHelp = _("Close Window")) def setlayercb(self, layer): self.layerslider.SetValue(layer) @@ -80,14 +79,14 @@ def process_slider(self, event): ID_EXIT = 110 class GvizWindow(GvizBaseFrame): def __init__(self, f = None, size = (550, 550), build_dimensions = [200, 200, 100, 0, 0, 0], grid = (10, 50), extrusion_width = 0.5, bgcolor = "#000000"): - super().__init__(None, title = _("GCode Viewer"), size = size, style = wx.DEFAULT_FRAME_STYLE) + super().__init__(None, title = _("G-Code Viewer"), size = size, style = wx.DEFAULT_FRAME_STYLE) panel, h_sizer = self.create_base_ui() self.p = Gviz(panel, size = size, build_dimensions = build_dimensions, grid = grid, extrusion_width = extrusion_width, bgcolor = bgcolor, realparent = self) h_sizer.Add(self.p, 1, wx.EXPAND) h_sizer.Add(self.layerslider, 0, wx.EXPAND | wx.ALL, get_space('minor')) - self.p.SetToolTip("Shift + Click to move view, scroll to change layer") + self.p.SetToolTip("Left-click to move the view, scroll to zoom and shift + scroll to change the layer.") self.Layout() size = (size[0] + self.layerslider.GetEffectiveMinSize().width, @@ -96,11 +95,11 @@ def __init__(self, f = None, size = (550, 550), build_dimensions = [200, 200, 10 self.SetMinClientSize((minsize, minsize)) self.SetClientSize(size) - self.Bind(wx.EVT_TOOL, lambda x: self.p.zoom(-1, -1, 1.2), id = 1) - self.Bind(wx.EVT_TOOL, lambda x: self.p.zoom(-1, -1, 1 / 1.2), id = 2) + self.Bind(wx.EVT_TOOL, lambda x: self.p.zoom(-1, -1, 1 / 1.2), id = 1) + self.Bind(wx.EVT_TOOL, lambda x: self.p.zoom(-1, -1, 1.2), id = 2) self.Bind(wx.EVT_TOOL, self.reset_view, id = 3) - self.Bind(wx.EVT_TOOL, lambda x: self.p.layerup(), id = 4) - self.Bind(wx.EVT_TOOL, lambda x: self.p.layerdown(), id = 5) + self.Bind(wx.EVT_TOOL, lambda x: self.p.layerdown(), id = 4) + self.Bind(wx.EVT_TOOL, lambda x: self.p.layerup(), id = 5) self.Bind(wx.EVT_TOOL, lambda x: self.p.inject(), id = 6) self.Bind(wx.EVT_TOOL, lambda x: self.p.editlayer(), id = 7) self.Bind(wx.EVT_TOOL, self.reset_selection, id = 8) diff --git a/printrun/pronsole.py b/printrun/pronsole.py index 616b88087..7650b30cf 100644 --- a/printrun/pronsole.py +++ b/printrun/pronsole.py @@ -166,8 +166,8 @@ def __init__(self): self.paused = False self.sdprinting = 0 self.uploading = 0 # Unused, just for pronterface generalization - self.temps = {"pla": "185", "abs": "230", "off": "0"} - self.bedtemps = {"pla": "60", "abs": "110", "off": "0"} + self.temps = {"PLA": "185", "ABS": "230", "Off": "0"} + self.bedtemps = {"PLA": "60", "ABS": "110", "Off": "0"} self.percentdone = 0 self.posreport = "" self.tempreadings = "" @@ -179,7 +179,7 @@ def __init__(self): self.processing_rc = False self.processing_args = False self.settings = Settings(self) - self.settings._add(BuildDimensionsSetting("build_dimensions", "200x200x100+0+0+0+0+0+0", _("Build dimensions"), _("Dimensions of Build Platform\n & optional offset of origin\n & optional switch position\n\nExamples:\n XXXxYYY\n XXX,YYY,ZZZ\n XXXxYYYxZZZ+OffX+OffY+OffZ\nXXXxYYYxZZZ+OffX+OffY+OffZ+HomeX+HomeY+HomeZ"), "Printer"), self.update_build_dimensions) + self.settings._add(BuildDimensionsSetting("build_dimensions", "200x200x100+0+0+0+0+0+0", _("Build Dimensions:"), _("Dimensions of Build Platform\n & optional offset of origin\n & optional switch position\n\nExamples:\n XXXxYYY\n XXX,YYY,ZZZ\n XXXxYYYxZZZ+OffX+OffY+OffZ\nXXXxYYYxZZZ+OffX+OffY+OffZ+HomeX+HomeY+HomeZ"), "Printer"), self.update_build_dimensions) self.settings._port_list = self.scanserial self.update_build_dimensions(None, self.settings.build_dimensions) self.update_tcp_streaming_mode(None, self.settings.tcp_streaming_mode) diff --git a/printrun/pronterface.py b/printrun/pronterface.py index e779d5eaf..d637260a0 100644 --- a/printrun/pronterface.py +++ b/printrun/pronterface.py @@ -1090,12 +1090,12 @@ def _add_settings(self, size): 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", 4*get_space('settings')), 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) self.settings._add(BooleanSetting("perspective", False, _("Use a perspective view instead of orthographic"), _("A perspective view looks more realistic, but is a bit more confusing to navigate"), "Viewer"), self.reload_ui) - 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) + 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", 4*get_space('settings')), self.reload_ui) self.settings._add(BooleanSetting("trackcurrentlayer3d", False, _("Track current layer in main 3D view"), _("Track the currently printing layer in the main 3D visualization"), "Viewer")) 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) 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) diff --git a/printrun/settings.py b/printrun/settings.py index 5de86fb54..2adf6c4f8 100644 --- a/printrun/settings.py +++ b/printrun/settings.py @@ -20,8 +20,10 @@ from functools import wraps -from .utils import parse_build_dimensions +import wx from pathlib import Path +from .utils import parse_build_dimensions, check_rgb_color, check_rgba_color +from .gui.widgets import get_space def setting_add_tooltip(func): @wraps(func) @@ -69,7 +71,6 @@ def _set_value(self, value): @setting_add_tooltip def get_label(self, parent): - import wx widget = wx.StaticText(parent, -1, self.label or self.name) widget.set_default = self.set_default return widget @@ -84,7 +85,8 @@ def get_specific_widget(self, parent): def update(self): raise NotImplementedError - def validate(self, value): pass + def validate(self, value): + pass def __str__(self): return self.name @@ -122,7 +124,6 @@ def set_default(self, e): class StringSetting(wxSetting): def get_specific_widget(self, parent): - import wx self.widget = wx.TextCtrl(parent, -1, str(self.value)) return self.widget @@ -132,56 +133,103 @@ def wxColorToStr(color, withAlpha = True): + ('{0.alpha:02X}' if withAlpha else '') return format.format(color) +class DirSetting(wxSetting): + '''Adds a setting type that works similar to the StringSetting but with + an additional 'Browse' button that opens an directory chooser dialog.''' + + def get_widget(self, parent): + # Create the text control + self.text_ctrl = wx.TextCtrl(parent, -1, str(self.value)) + + # Create the browse-button control + button = wx.Button(parent, -1, "Browse") + button.Bind(wx.EVT_BUTTON, self.on_browse) + + self.widget = wx.BoxSizer(wx.HORIZONTAL) + self.widget.Add(self.text_ctrl, 1) + self.widget.Add(button, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, get_space('mini')) + + return self.widget + + def on_browse(self, event = None): + # Going to browse for file... + directory = self.text_ctrl.GetValue() + if not os.path.isdir(directory): + directory = '.' + + message = _("Choose Directory...") + dlg = wx.DirDialog(None, message, directory, + wx.DD_DEFAULT_STYLE | wx.DD_DIR_MUST_EXIST) + dlg.SetMessage(message) + + if dlg.ShowModal() == wx.ID_OK: + self.text_ctrl.SetValue(dlg.GetPath()) + dlg.Destroy() + + def _set_value(self, value): + self._value = value + if self.text_ctrl: + self.text_ctrl.SetValue(value) + + value = property(wxSetting._get_value, _set_value) + + def update(self): + self.value = self.text_ctrl.GetValue() + class ColorSetting(wxSetting): + def __init__(self, name, default, label = None, help = None, group = None, isRGBA=True): super().__init__(name, default, label, help, group) self.isRGBA = isRGBA def validate(self, value): - from .utils import check_rgb_color, check_rgba_color validate = check_rgba_color if self.isRGBA else check_rgb_color validate(value) def get_specific_widget(self, parent): - import wx - self.widget = wx.ColourPickerCtrl(parent, colour=wx.Colour(self.value), style=wx.CLRP_USE_TEXTCTRL) + self.widget = wx.ColourPickerCtrl(parent, colour=wx.Colour(self.value), + style=wx.CLRP_USE_TEXTCTRL) self.widget.SetValue = self.widget.SetColour self.widget.LayoutDirection = wx.Layout_RightToLeft return self.widget + def update(self): self._value = wxColorToStr(self.widget.Colour, self.isRGBA) class ComboSetting(wxSetting): - def __init__(self, name, default, choices, label = None, help = None, group = None): - super(ComboSetting, self).__init__(name, default, label, help, group) + def __init__(self, name, default, choices, label = None, help = None, + group = None, size = 7 * get_space('settings')): + # size: Default length is set here, can be overwritten on creation. + super().__init__(name, default, label, help, group) self.choices = choices + self.size = size def get_specific_widget(self, parent): - import wx readonly = isinstance(self.choices, tuple) if readonly: # wx.Choice drops its list on click, no need to click down arrow - # which is far to the right because of wx.EXPAND - self.widget = wx.Choice(parent, -1, choices = self.choices) + self.widget = wx.Choice(parent, -1, choices = self.choices, size = (self.size, -1)) self.widget.GetValue = lambda: self.choices[self.widget.Selection] self.widget.SetValue = lambda v: self.widget.SetSelection(self.choices.index(v)) self.widget.SetValue(self.value) else: - self.widget = wx.ComboBox(parent, -1, str(self.value), choices = self.choices, style = wx.CB_DROPDOWN) + self.widget = wx.ComboBox(parent, -1, str(self.value), choices = self.choices, + style = wx.CB_DROPDOWN, size = (self.size, -1)) return self.widget class SpinSetting(wxSetting): - def __init__(self, name, default, min, max, label = None, help = None, group = None, increment = 0.1): + def __init__(self, name, default, min, max, label = None, + help = None, group = None, increment = 0.1): super().__init__(name, default, label, help, group) self.min = min self.max = max self.increment = increment def get_specific_widget(self, parent): - import wx - self.widget = wx.SpinCtrlDouble(parent, -1, min = self.min, max = self.max) + self.widget = wx.SpinCtrlDouble(parent, -1, min = self.min, max = self.max, + size = (4 * get_space('settings'), -1)) self.widget.SetDigits(0) self.widget.SetValue(self.value) orig = self.widget.GetValue @@ -195,13 +243,13 @@ def MySpin(parent, digits, *args, **kw): # If native wx.SpinCtrlDouble has problems in different platforms # try agw # from wx.lib.agw.floatspin import FloatSpin - import wx sp = wx.SpinCtrlDouble(parent, *args, **kw) # sp = FloatSpin(parent) sp.SetDigits(digits) # sp.SetValue(kw['initial']) + def fitValue(ev): - text = '%%.%df'% digits % sp.Max + text = '%%.%df' % digits % sp.Max # native wx.SpinCtrlDouble does not return good size # in GTK 3.0 tex = sp.GetTextExtent(text) @@ -218,7 +266,9 @@ def fitValue(ev): class FloatSpinSetting(SpinSetting): def get_specific_widget(self, parent): - self.widget = MySpin(parent, 2, initial = self.value, min = self.min, max = self.max, inc = self.increment) + self.widget = MySpin(parent, 2, initial = self.value, min = self.min, + max = self.max, inc = self.increment, + size = (4 * get_space('settings'), -1)) return self.widget class BooleanSetting(wxSetting): @@ -234,7 +284,6 @@ def _set_value(self, value): value = property(_get_value, _set_value) def get_specific_widget(self, parent): - import wx self.widget = wx.CheckBox(parent, -1) self.widget.SetValue(bool(self.value)) return self.widget @@ -242,7 +291,7 @@ def get_specific_widget(self, parent): class StaticTextSetting(wxSetting): def __init__(self, name, label = " ", text = "", help = None, group = None): - super(StaticTextSetting, self).__init__(name, "", label, help, group) + super().__init__(name, "", label, help, group) self.text = text def update(self): @@ -255,7 +304,6 @@ def _set_value(self, value): pass def get_specific_widget(self, parent): - import wx self.widget = wx.StaticText(parent, -1, self.text) return self.widget @@ -271,46 +319,52 @@ def _set_value(self, value): def _set_widgets_values(self, value): build_dimensions_list = parse_build_dimensions(value) - for i in range(len(self.widgets)): - self.widgets[i].SetValue(build_dimensions_list[i]) + for i, widget in enumerate(self.widgets): + widget.SetValue(build_dimensions_list[i]) def get_widget(self, parent): - from wx.lib.agw.floatspin import FloatSpin - import wx build_dimensions = parse_build_dimensions(self.value) self.widgets = [] + def w(val, m, M): - self.widgets.append(MySpin(parent, 2, initial = val, min = m, max = M)) + self.widgets.append(MySpin(parent, 2, initial = val, min = m, + max = M, size = (5 * get_space('settings'), -1))) + def addlabel(name, pos): - self.widget.Add(wx.StaticText(parent, -1, name), pos = pos, flag = wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border = 5) + self.widget.Add(wx.StaticText(parent, -1, name), pos = pos, + flag = wx.ALIGN_CENTER_VERTICAL | wx.RIGHT | wx.ALIGN_RIGHT, + border = get_space('mini')) + def addwidget(*pos): - self.widget.Add(self.widgets[-1], pos = pos, flag = wx.RIGHT | wx.EXPAND, border = 5) - self.widget = wx.GridBagSizer() - addlabel(_("Width"), (0, 0)) + self.widget.Add(self.widgets[-1], pos = pos, + flag = wx.RIGHT | wx.EXPAND, border = get_space('mini')) + + self.widget = wx.GridBagSizer(vgap = get_space('mini'), hgap = get_space('mini')) + addlabel(_("Width:"), (0, 0)) w(build_dimensions[0], 0, 2000) addwidget(0, 1) - addlabel(_("Depth"), (0, 2)) + addlabel(_("Depth:"), (0, 2)) w(build_dimensions[1], 0, 2000) addwidget(0, 3) - addlabel(_("Height"), (0, 4)) + addlabel(_("Height:"), (0, 4)) w(build_dimensions[2], 0, 2000) addwidget(0, 5) - addlabel(_("X offset"), (1, 0)) + addlabel(_("X Offset:"), (1, 0)) w(build_dimensions[3], -2000, 2000) addwidget(1, 1) - addlabel(_("Y offset"), (1, 2)) + addlabel(_("Y Offset:"), (1, 2)) w(build_dimensions[4], -2000, 2000) addwidget(1, 3) - addlabel(_("Z offset"), (1, 4)) + addlabel(_("Z Offset:"), (1, 4)) w(build_dimensions[5], -2000, 2000) addwidget(1, 5) - addlabel(_("X home pos."), (2, 0)) + addlabel(_("X Home Pos.:"), (2, 0)) w(build_dimensions[6], -2000, 2000) addwidget(2, 1) - addlabel(_("Y home pos."), (2, 2)) + addlabel(_("Y Home Pos.:"), (2, 2)) w(build_dimensions[7], -2000, 2000) addwidget(2, 3) - addlabel(_("Z home pos."), (2, 4)) + addlabel(_("Z Home Pos.:"), (2, 4)) w(build_dimensions[8], -2000, 2000) addwidget(2, 5) return self.widget @@ -320,39 +374,42 @@ def update(self): self.value = "%.02fx%.02fx%.02f%+.02f%+.02f%+.02f%+.02f%+.02f%+.02f" % tuple(values) class Settings: - def __baudrate_list(self): return ["2400", "9600", "19200", "38400", "57600", "115200", "250000"] + def __baudrate_list(self): + return ["2400", "9600", "19200", "38400", "57600", "115200", "250000"] def __init__(self, root): # defaults here. # the initial value determines the type - self._add(StringSetting("port", "", _("Serial port"), _("Port used to communicate with printer"))) - self._add(ComboSetting("baudrate", 115200, self.__baudrate_list(), _("Baud rate"), _("Communications Speed"))) - self._add(BooleanSetting("tcp_streaming_mode", False, _("TCP streaming mode"), _("When using a TCP connection to the printer, the streaming mode will not wait for acks from the printer to send new commands. This will break things such as ETA prediction, but can result in smoother prints.")), root.update_tcp_streaming_mode) - self._add(BooleanSetting("rpc_server", True, _("RPC server"), _("Enable RPC server to allow remotely querying print status")), root.update_rpc_server) - self._add(BooleanSetting("dtr", True, _("DTR"), _("Disabling DTR would prevent Arduino (RAMPS) from resetting upon connection"), "Printer")) + self._add(StringSetting("port", "", _("Serial Port:"), _("Port used to communicate with printer"))) + self._add(ComboSetting("baudrate", 115200, self.__baudrate_list(), _("Baud Rate:"), _("Communications Speed"))) + self._add(BooleanSetting("tcp_streaming_mode", False, _("TCP Streaming Mode:"), + _("When using a TCP connection to the printer, the streaming mode will not wait for acks from the printer to send new commands." + "This will break things such as ETA prediction, but can result in smoother prints.")), root.update_tcp_streaming_mode) + self._add(BooleanSetting("rpc_server", True, _("RPC Server:"), _("Enable RPC server to allow remotely querying print status")), root.update_rpc_server) + self._add(BooleanSetting("dtr", True, _("DTR:"), _("Disabling DTR would prevent Arduino (RAMPS) from resetting upon connection"), "Printer")) if sys.platform != "win32": - self._add(StringSetting("devicepath", "", _("Device name pattern"), _("Custom device pattern: for example /dev/3DP_* "), "Printer")) - self._add(SpinSetting("bedtemp_abs", 110, 0, 400, _("Bed temperature for ABS"), _("Heated Build Platform temp for ABS (deg C)"), "Printer"), root.set_temp_preset) - self._add(SpinSetting("bedtemp_pla", 60, 0, 400, _("Bed temperature for PLA"), _("Heated Build Platform temp for PLA (deg C)"), "Printer"), root.set_temp_preset) - self._add(SpinSetting("temperature_abs", 230, 0, 400, _("Extruder temperature for ABS"), _("Extruder temp for ABS (deg C)"), "Printer"), root.set_temp_preset) - self._add(SpinSetting("temperature_pla", 185, 0, 400, _("Extruder temperature for PLA"), _("Extruder temp for PLA (deg C)"), "Printer"), root.set_temp_preset) - self._add(SpinSetting("xy_feedrate", 3000, 0, 50000, _("X && Y manual feedrate"), _("Feedrate for Control Panel Moves in X and Y (mm/min)"), "Printer")) - self._add(SpinSetting("z_feedrate", 100, 0, 50000, _("Z manual feedrate"), _("Feedrate for Control Panel Moves in Z (mm/min)"), "Printer")) - self._add(SpinSetting("e_feedrate", 100, 0, 1000, _("E manual feedrate"), _("Feedrate for Control Panel Moves in Extrusions (mm/min)"), "Printer")) + self._add(StringSetting("devicepath", "", _("Device Name Pattern:"), _("Custom device pattern: for example /dev/3DP_* "), "Printer")) + self._add(SpinSetting("bedtemp_abs", 110, 0, 400, _("Bed Temperature for ABS:"), _("Heated Build Platform temp for ABS (deg C)"), "Printer"), root.set_temp_preset) + self._add(SpinSetting("bedtemp_pla", 60, 0, 400, _("Bed Temperature for PLA:"), _("Heated Build Platform temp for PLA (deg C)"), "Printer"), root.set_temp_preset) + self._add(SpinSetting("temperature_abs", 230, 0, 400, _("Extruder Temperature for ABS:"), _("Extruder temp for ABS (deg C)"), "Printer"), root.set_temp_preset) + self._add(SpinSetting("temperature_pla", 185, 0, 400, _("Extruder Temperature for PLA:"), _("Extruder temp for PLA (deg C)"), "Printer"), root.set_temp_preset) + self._add(SpinSetting("xy_feedrate", 3000, 0, 50000, _("X && Y Manual Feedrate:"), _("Feedrate for Control Panel Moves in X and Y (mm/min)"), "Printer")) + self._add(SpinSetting("z_feedrate", 100, 0, 50000, _("Z Manual Feedrate:"), _("Feedrate for Control Panel Moves in Z (mm/min)"), "Printer")) + self._add(SpinSetting("e_feedrate", 100, 0, 1000, _("E Manual Feedrate:"), _("Feedrate for Control Panel Moves in Extrusions (mm/min)"), "Printer")) defaultslicerpath = "" if getattr(sys, 'frozen', False): if sys.platform == "darwin": defaultslicerpath = "/Applications/Slic3r.app/Contents/MacOS/" elif sys.platform == "win32": defaultslicerpath = ".\\slic3r\\" - self._add(StringSetting("slicecommandpath", defaultslicerpath, _("Path to slicer"), _("Path to slicer"), "External")) + self._add(DirSetting("slicecommandpath", defaultslicerpath, _("Path to Slicer:"), _("Path to slicer"), "External")) slicer = 'slic3r-console' if sys.platform == 'win32' else 'slic3r' - self._add(StringSetting("slicecommand", slicer + ' $s --output $o', _("Slice command"), _("Slice command"), "External")) - self._add(StringSetting("sliceoptscommand", "slic3r", _("Slicer options command"), _("Slice settings command"), "External")) - self._add(StringSetting("start_command", "", _("Start command"), _("Executable to run when the print is started"), "External")) - self._add(StringSetting("final_command", "", _("Final command"), _("Executable to run when the print is finished"), "External")) - self._add(StringSetting("error_command", "", _("Error command"), _("Executable to run when an error occurs"), "External")) - self._add(StringSetting("log_path", str(Path.home()), _("Log path"), _("Path to the log file. An empty path will log to the console."), "UI")) + self._add(StringSetting("slicecommand", slicer + ' $s --output $o', _("Slice Command:"), _("Slice command"), "External")) + self._add(StringSetting("sliceoptscommand", "slic3r", _("Slicer options Command:"), _("Slice settings command"), "External")) + self._add(StringSetting("start_command", "", _("Start Command:"), _("Executable to run when the print is started"), "External")) + self._add(StringSetting("final_command", "", _("Final Command:"), _("Executable to run when the print is finished"), "External")) + self._add(StringSetting("error_command", "", _("Error Command:"), _("Executable to run when an error occurs"), "External")) + self._add(DirSetting("log_path", str(Path.home()), _("Log Path:"), _("Path to the log file. An empty path will log to the console."), "UI")) self._add(HiddenSetting("project_offset_x", 0.0)) self._add(HiddenSetting("project_offset_y", 0.0)) @@ -362,7 +419,7 @@ def __init__(self, root): self._add(HiddenSetting("project_x", 1024)) self._add(HiddenSetting("project_y", 768)) self._add(HiddenSetting("project_projected_x", 150.0)) - self._add(HiddenSetting("project_direction", "Top Down")) + self._add(HiddenSetting("project_direction", 0)) # 0: Top Down self._add(HiddenSetting("project_overshoot", 3.0)) self._add(HiddenSetting("project_z_axis_rate", 200)) self._add(HiddenSetting("project_layer", 0.1)) @@ -387,6 +444,7 @@ def __setattr__(self, name, value): getattr(self, "_" + name).value = value else: setattr(self, name, StringSetting(name = name, default = value)) + return None def __getattr__(self, name): if name.startswith("_"): @@ -410,7 +468,7 @@ def _set(self, key, value): pass except AttributeError: pass - setting = getattr(self, '_'+key) + setting = getattr(self, '_' + key) setting.validate(value) t = type(getattr(self, key)) if t == bool and value == "False": @@ -421,7 +479,7 @@ def _set(self, key, value): if cb is not None: cb(key, value) except: - logging.warning((_("Failed to run callback after setting \"%s\":") % key) + + logging.warning((_("Failed to run callback after setting '%s':") % key) + "\n" + traceback.format_exc()) return value diff --git a/printrun/utils.py b/printrun/utils.py index 2ca0977ba..b45113470 100644 --- a/printrun/utils.py +++ b/printrun/utils.py @@ -46,20 +46,22 @@ def install_locale(domain): if osPlatform == "Darwin": # improvised workaround for macOS crash with gettext.translation, see issue #1154 - if os.path.exists(shared_locale_dir): - gettext.install(domain, shared_locale_dir) - else: - gettext.install(domain, './locale') + if os.path.exists(shared_locale_dir): + gettext.install(domain, shared_locale_dir) + else: + gettext.install(domain, './locale') else: if os.path.exists('./locale'): - translation = gettext.translation(domain, './locale', languages=[lang[0]], fallback= True) + translation = gettext.translation(domain, './locale', + languages=[lang[0]], fallback= True) else: - translation = gettext.translation(domain, shared_locale_dir, languages=[lang[0]], fallback= True) + translation = gettext.translation(domain, shared_locale_dir, + languages=[lang[0]], fallback= True) translation.install() class LogFormatter(logging.Formatter): def __init__(self, format_default, format_info): - super(LogFormatter, self).__init__(format_info) + super().__init__(format_info) self.format_default = format_default self.format_info = format_info @@ -68,7 +70,7 @@ def format(self, record): self._fmt = self.format_info else: self._fmt = self.format_default - return super(LogFormatter, self).format(record) + return super().format(record) def setup_logging(out, filepath = None, reset_handlers = False): logger = logging.getLogger() @@ -100,8 +102,7 @@ def iconfile(filename): ''' if hasattr(sys, "frozen") and sys.frozen == "windows_exe": return sys.executable - else: - return pixmapfile(filename) + return pixmapfile(filename) def imagefile(filename): ''' @@ -138,18 +139,19 @@ def lookup_file(filename, prefixes): constructor and filename isn't found, the C++ part of wx will raise an exception (wx._core.wxAssertionError): "invalid image". - + Sequential arguments: filename -- a filename without the path. prefixes -- a list of paths. - + Returns: The full path if found, or filename if not found. ''' local_candidate = os.path.join(os.path.dirname(sys.argv[0]), filename) if os.path.exists(local_candidate): return local_candidate - if getattr(sys,"frozen",False): prefixes+=[getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))),] + if getattr(sys, "frozen", False): + prefixes += [getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))),] for prefix in prefixes: candidate = os.path.join(prefix, filename) if os.path.exists(candidate): @@ -190,7 +192,7 @@ def configfile(filename): def decode_utf8(s): try: s = s.decode("utf-8") - except: + except ValueError: pass return s @@ -208,12 +210,14 @@ def prepare_command(command, replaces = None): command = [bit.replace(pattern, rep) for bit in command] return command -def run_command(command, replaces = None, stdout = subprocess.STDOUT, stderr = subprocess.STDOUT, blocking = False, universal_newlines = False): +def run_command(command, replaces = None, stdout = subprocess.STDOUT, + stderr = subprocess.STDOUT, blocking = False, + universal_newlines = False): command = prepare_command(command, replaces) if blocking: return subprocess.call(command, universal_newlines = universal_newlines) - else: - return subprocess.Popen(command, stderr = stderr, stdout = stdout, universal_newlines = universal_newlines) + return subprocess.Popen(command, stderr = stderr, stdout = stdout, + universal_newlines = universal_newlines) def get_command_output(command, replaces): p = run_command(command, replaces, @@ -235,6 +239,8 @@ def __init__(self, gcode): self.current_layer_estimate = 0 self.current_layer_lines = 0 self.gcode = gcode + self.last_idx = -1 + self.last_estimate = None self.remaining_layers_estimate = sum(layer.duration for layer in gcode.all_layers) if len(gcode) > 0: self.update_layer(0, 0) @@ -273,14 +279,15 @@ def parse_build_dimensions(bdim): # "XXXxYYY+xxx-yyy" # "XXX,YYY,ZZZ+xxx+yyy-zzz" # etc - bdl = re.findall("([-+]?[0-9]*\.?[0-9]*)", bdim) + bdl = re.findall(r"([-+]?[0-9]*\.?[0-9]*)", bdim) defaults = [200, 200, 100, 0, 0, 0, 0, 0, 0] bdl = [b for b in bdl if b] bdl_float = [float(value) if value else defaults[i] for i, value in enumerate(bdl)] if len(bdl_float) < len(defaults): bdl_float += [defaults[i] for i in range(len(bdl_float), len(defaults))] for i in range(3): # Check for nonpositive dimensions for build volume - if bdl_float[i] <= 0: bdl_float[i] = 1 + if bdl_float[i] <= 0: + bdl_float[i] = 1 return bdl_float def get_home_pos(build_dimensions): @@ -306,25 +313,25 @@ def check_rgba_color(color): ex.from_validator = True raise ex -tempreport_exp = re.compile("([TB]\d*):([-+]?\d*\.?\d*)(?: ?\/)?([-+]?\d*\.?\d*)") + +tempreport_exp = re.compile(r"([TB]\d*):([-+]?\d*\.?\d*)(?: ?\/)?([-+]?\d*\.?\d*)") def parse_temperature_report(report): matches = tempreport_exp.findall(report) return dict((m[0], (m[1], m[2])) for m in matches) def compile_file(filename): - with open(filename) as f: + with open(filename, 'r', encoding='utf-8') as f: return compile(f.read(), filename, 'exec') def read_history_from(filename): - history=[] + history = [] if os.path.exists(filename): - _hf=open(filename,encoding="utf-8") - for i in _hf: - history.append(i.rstrip()) + with open(filename, 'r', encoding='utf-8') as _hf: + for i in _hf: + history.append(i.rstrip()) return history def write_history_to(filename, hist): - _hf=open(filename,"w",encoding="utf-8") - for i in hist: - _hf.write(i+"\n") - _hf.close() + with open(filename, 'w', encoding='utf-8') as _hf: + for i in hist: + _hf.write(i + '\n')