#CustomToolbarManager.FCMacro #2023, by , LGPL 2.1 or later #based on some of the code found in AddonManager # #Goal is to make it simpler and easier to manage custom #macros on custom toolbars __version__ = "0.2026.07.20b" LOAD_STRING = True #load UI from a string defined in this file if True, #else from a separate ui file during development from PySide import QtGui,QtCore,QtSvg import os import tempfile import importlib.util import base64 import requests import re import PIL import time import numpy as np import math import json import Mesh,MeshPart __dir__ = FreeCAD.getUserMacroDir(True) __Requires__ = 'opencv-python' uiPath = os.path.join( __dir__,"toolbar") class CustomToolbarEditor(QtGui.QWidget): """Easily setup and manage custom toolbar macros""" def __init__(self, macroName="", \ menuText="", \ tooltip = "", \ statusText = "", \ whatsThis = "", \ shortcut = "", \ pixmap = "",\ ): super().__init__() self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) self.setAttribute(QtCore.Qt.WA_WindowPropagation,True) self.iconMakerTabWidget = None self.iconMakerDlg = None self.currentIcon = None if not LOAD_STRING: self.form = FreeCADGui.PySideUic.loadUi(uiPath + "/custom_toolbar.ui") else: with tempfile.NamedTemporaryFile(suffix=".ui", delete=False) as temp_file: temp_ui_file = temp_file.name temp_file.write(UI_FILE.encode('utf-8')) self.form = FreeCADGui.PySideUic.loadUi(temp_ui_file) temp_file.close() os.remove(temp_ui_file) self.form.setWindowTitle(f"Macro Toolbar Manager v.{__version__}") self.icon = self.QIconFromXPMString(__icon__) self.form.setWindowIcon(self.icon) self.messages = ["","","",""] #we keep only the last 3 self.lastPrintedMsg = "" #avoid repeating messages self.macroName = macroName self.nonMacroName = "" self.menuText = menuText self.tooltip = tooltip self.statusText = statusText self.whatsThis = whatsThis self.shortcut = shortcut self.pixmap = pixmap self.XIcon = None self.getXIcon() #initializes self.XIcon #prevent multiple calls to line edit handlers while user is still typing self.LineEditPixmapTimer = QtCore.QTimer(self) self.LineEditPixmapTimer.setSingleShot(True) self.LineEditPixmapTimer.timeout.connect(self.handlePixmap) self.LineEditFilterTimer = QtCore.QTimer(self) self.LineEditFilterTimer.setSingleShot(True) self.LineEditFilterTimer.timeout.connect(self.handleFilter) self.makeConnections() self.setupWorkbenchBox() self.setupUi() self.updateUi() self.form.show() winMinSize = self.form.minimumSizeHint() self.resize(winMinSize) def QIconFromXPMString(self, xpm_string): if "/*pixels*/" in xpm_string: xpm = xpm_string.replace("\"","").replace(',','').splitlines()[4:] else: xpm = xpm_string.replace("\"","").replace(',','').splitlines()[3:] for line in reversed(xpm): if line.startswith("/*") and line.endswith("*/"): xpm.pop(xpm.index(line)) pixmap = QtGui.QPixmap(xpm) icon = QtGui.QIcon(pixmap) return icon def makeConnections(self): self.form.statusLabel.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) self.form.statusLabel.customContextMenuRequested.connect(self.showContextMenu) self.form.menuButton.clicked.connect(self.showMenu) self.form.selectIconFileButton.clicked.connect(self.selectIconFileButtonClicked) self.form.fromMacroButton.clicked.connect(self.fromMacroButtonClicked) self.form.renameToolbarButton.clicked.connect(self.renameToolbarButtonClicked) self.form.deleteToolbarButton.clicked.connect(self.deleteToolbarButtonClicked) self.form.newToolbarButton.clicked.connect(self.newToolbarButtonClicked) self.form.removeMacroFromToolbarButton.clicked.connect(self.removeMacroFromToolbarButtonClicked) self.form.addMacroToToolbarButton.clicked.connect(self.addMacroToToolbarButtonClicked) self.form.addNonMacroButton.clicked.connect(self.addNonMacroButtonClicked) self.form.removeNonMacroButton.clicked.connect(self.removeNonMacroButtonClicked) self.form.ComboBoxMacroName.currentIndexChanged.connect(self.onComboBoxMacroNameCurrentIndexChanged) self.form.ComboBoxNonMacros.currentIndexChanged.connect(self.onComboBoxNonMacrosCurrentIndexChanged) self.form.openHyperlinkButton.clicked.connect(self.openHyperlinkButtonClicked) self.form.saveExtractedButton.clicked.connect(self.saveExtractedButtonClicked) self.form.useAsPixmapButton.clicked.connect(self.useAsPixmapButtonClicked) self.form.ComboBoxWorkbench.currentIndexChanged.connect(self.onComboBoxWorkbenchCurrentIndexChanged) self.form.activeGlobalButton.clicked.connect(self.onActiveGlobalButtonClicked) self.form.ComboBoxCustomToolbar.currentIndexChanged.connect(self.onComboBoxCustomToolbarCurrentIndexChanged) self.form.ComboBoxInstalled.currentIndexChanged.connect(self.onComboBoxInstalledCurrentIndexChanged) self.form.selectInstalledButton.clicked.connect(self.onSelectInstalledButtonClicked) self.form.selectNonMacroButton.clicked.connect(self.onSelectNonMacroButtonClicked) self.form.selectMacroButton.clicked.connect(self.onSelectMacroButtonClicked) self.form.activeCheckBox.clicked.connect(self.onActiveCheckBoxClicked) self.form.LineEditPixmap.textChanged.connect(self.startPixmapTimer) self.form.systemIconButton.clicked.connect(self.onSystemIconButtonClicked) self.form.systemIconButton.installEventFilter(self) self.form.LineEditFilter.textChanged.connect(self.startFilterTimer) #self.form.LineEditShortcut.textChanged.connect(self.onLineEditShortcutTextChanged) self.form.LineEditShortcut.keySequenceChanged.connect(self.onLineEditShortcutTextChanged) self.form.makeIconButton.clicked.connect(self.makeSimpleIcon) self.form.extractXPMButton.clicked.connect(self.extractXPMButtonClicked) def onLineEditShortcutTextChanged(self): short = self.form.LineEditShortcut.keySequence().toString() if not short: return ourCmd = FreeCADGui.Command.findCustomCommand(self.macroName) ourNonMacroCmd = FreeCADGui.Command.get(self.nonMacroName) info = ourNonMacroCmd.getInfo() if ourNonMacroCmd else "" ourNonMacroCmdName = info["name"] if info else "" matches = [sh for sh in FreeCADGui.Command.listByShortcut(short) if bool(sh != ourCmd and sh != ourNonMacroCmdName)] if matches: self.showMsg(f"There are potential conflicting shortcuts for {short}: {matches}","warning") else: self.showMsg(f"No conflicting shortcuts found, but note the search for conflicts only includes loaded workbenches") def onSystemIconButtonClicked(self): parseMod = self.getBool("ParseModFolder",False) w = SystemIconSelector(parseModFolder = parseMod, cte=self) w.setModal(True) w.exec() self.form.LineEditPixmap.setText(w.icon_text) w.deleteLater() def startPixmapTimer(self): """resets the timer to prevent it timing out and firing the handler while the user is still tying in the line edit""" self.LineEditPixmapTimer.start(750) def startFilterTimer(self): """resets the timer to prevent it timeout and firing the handler while the the user is still typing in the line edit""" self.LineEditFilterTimer.start(500) def handleFilter(self): """fired when the filter line edit changes. We filter the files displayed in the Macro name combo box""" self.updateMacroNames() self.updateNonMacroNames() def updateVariables(self): """update the self.VARS from the dialog values in case the user has changed some""" self.macroName = self.form.ComboBoxMacroName.currentText() self.menuText = self.form.LineEditMenuText.text() self.tooltip = self.form.LineEditToolTip.text() self.statusText = self.form.LineEditStatusText.text() self.whatsThis = self.form.LineEditWhatsThis.text() self.shortcut = self.form.LineEditShortcut.keySequence().toString() self.pixmap = self.form.LineEditPixmap.text() def deleteMacro(self): if not self.macroName: self.showMsg("No macro to delete","error") return tm = self.tm() if tm and self.macroName in tm.getInstalledMacros(): self.showMsg("Macro is installed in the toolbar. Remove from toolbar first!", "error") return fullpath = os.path.join(FreeCAD.getUserMacroDir(True),self.macroName) msg = QtGui.QMessageBox() msg.setIcon(QtGui.QMessageBox.Warning) msg.setText(f"Do you want to delete the file:\n{fullpath}?") msg.setWindowTitle("Delete Macro File Confirmation") msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) msg.setDefaultButton(QtGui.QMessageBox.No) result = msg.exec() if result == QtGui.QMessageBox.Yes: try: os.remove(fullpath) self.showMsg(f"The file {fullpath} has been deleted.") self.updateMacroNames() except Exception as e: QtGui.QMessageBox.critical(self, "Error", f"An error occurred while deleting the file:\n{str(e)}") else: self.showMsg("User canceled. The file has not been deleted.") def setBool(self, name, value): pg = FreeCAD.ParamGet("User parameter:Plugins/MacroToolbarManager") pg.SetBool(name, value) self.showMsg(f"Setting {name} to {value}") def getBool(self, name, default): pg = FreeCAD.ParamGet("User parameter:Plugins/MacroToolbarManager") return pg.GetBool(name, default) def eventFilter(self, obj, event): if event.type() == event.Type.ContextMenu and obj == self.form.systemIconButton: # Show the context menu at the cursor position systemIconsMenu = self.makeSystemIconsMenu() systemIconsMenu.exec(event.globalPos()) return True return super().eventFilter(obj, event) def makeSimpleIcon(self): """make a simple single character icon""" class ColorDlg (QtGui.QColorDialog): def __init__(self,title,flags,textColor,borderColor): QtGui.QColorDialog.__init__(self) self.title = title self.flags = flags self.textColor = textColor self.borderColor = borderColor def getClr(self, currentColor): current = QtGui.QColor(currentColor[0],currentColor[1],currentColor[2],0) self.setCustomColor(0,QtGui.QColor.fromRgb(self.textColor[0],self.textColor[1],self.textColor[2],0)) self.setCustomColor(1,QtGui.QColor.fromRgb(self.borderColor[0],self.borderColor[1],self.borderColor[2],0)) clr = self.getColor(current,None,self.title, self.flags) return clr class EditableListWidget(QtGui.QWidget): def __init__(self, iconEditor, pointsColor): super().__init__() self.iconEditor = iconEditor self.pointsColor = pointsColor self.initUI() self.signalsBlocked = False self.flashingBlocked = False self.showPointsAllowed = True self.backupPoints = [] self.undoTransformationPoints = [] self.isFlashing = False def initUI(self): self.layout = QtGui.QGridLayout() self.list_widget = QtGui.QListWidget(self) self.list_widget.setMaximumWidth(150) self.layout.addWidget(self.list_widget,0,0,3,1) self.list_widget.currentItemChanged.connect(self.handleItemChanged) #self.list_widget.itemDoubleClicked.connect(self.editItem) self.showPointsCheckBox = QtGui.QCheckBox("Show points") self.showPointsCheckBox.setChecked(True) self.layout.addWidget(self.showPointsCheckBox,0,1,1,1) self.showPointsCheckBox.toggled.connect(self.showPointsToggled) self.colorButton = QtGui.QPushButton("Points") self.layout.addWidget(self.colorButton,0,2,1,1) self.setColor(self.pointsColor) self.colorButton.clicked.connect(self.onColorButtonClicked) self.colorButton.setToolTip("Color for the construction mode points") self.xSpinBox = QtGui.QSpinBox() self.xSpinBox.setToolTip("Alt + Click up or down = Etch-a-sketch (TM) mode") self.xSpinBox.valueChanged.connect(self.onValueChanged) self.layout.addWidget(self.xSpinBox,1,1,1,1) self.ySpinBox = QtGui.QSpinBox() self.ySpinBox.setToolTip("Alt + Click up or down = Etch-a-sketch (TM) mode") self.ySpinBox.valueChanged.connect(self.onValueChanged) self.layout.addWidget(self.ySpinBox,1,2,1,1) self.clearButton = QtGui.QPushButton("Clear") self.clearButton.clicked.connect(self.onClearButtonClicked) self.layout.addWidget(self.clearButton, 2,2,1,1) self.removePointButton = QtGui.QPushButton("Remove Pt") self.removePointButton.clicked.connect(self.onRemovePointButtonClicked) self.layout.addWidget(self.removePointButton, 2,1,1,1) self.setLayout(self.layout) def onColorButtonClicked(self): colorDlg = ColorDlg("Simple Icon Maker", QtGui.QColorDialog.DontUseNativeDialog, self.getColor(), self.getColor()) color = colorDlg.getClr(self.getColor()) colorString = f'rgb({color.red()}, {color.green()}, {color.blue()})' self.colorButton.setStyleSheet(f'background-color: {colorString};') self.pointsColor = (color.red(), color.green(), color.blue()) self.showPoints() self.iconEditor.pointsColor = self.pointsColor def getColor(self): """gets the current color of the color button, returns as (r,g,b,a) each a value from 0 to 255""" palette = self.colorButton.palette() color = palette.color(palette.Background) #print(f"color = {color.red(), color.green(), color.blue()}") return (color.red(), color.green(), color.blue(), 0) #0 not used, but 4 values are expected def setColor(self, colorTuple): self.pointsColor = colorTuple if len(colorTuple) == 4: r,g,b,a = colorTuple else: r,g,b = colorTuple a = 0 color = QtGui.QColor(r,g,b,a) colorString = f'rgb({color.red()}, {color.green()}, {color.blue()})' self.colorButton.setStyleSheet(f'background-color: {colorString};') def onClearButtonClicked(self): self.xSpinBox.setValue(0) self.ySpinBox.setValue(0) self.list_widget.clear() self.showPoints() def transformPointsTriggered(self): """apply the xAdjust, yAdjust, scale, and angle spinbox values to the points angle comes from the Angle spin box, the center of rotation is the selected point in the list widget. 0 degrees is the 3 o'clock position, and moves clockwise from there. The rotation is applied first, then we move the points, and finally the scaling is done. """ def rotate_point(point, center, angle_degrees): x, y = point cx, cy = center angle_radians = math.radians(angle_degrees) new_x = cx + round((x - cx) * math.cos(angle_radians) - (y - cy) * math.sin(angle_radians)) new_y = cy + round((x - cx) * math.sin(angle_radians) + (y - cy) * math.cos(angle_radians)) return (new_x, new_y) def rotate_points(point_list, center, angle_degrees): return [rotate_point(point, center, angle_degrees) for point in point_list] def scale_around_center(points, scale_factor): center_x, center_y = 32, 32 # New center for scaling # Step 1: Translate points to make (32, 32) the origin translated_points = [(x - center_x, y - center_y) for x, y in points] # Step 2: Perform the scaling transformation scaled_points = [(x * scale_factor, y * scale_factor) for x, y in translated_points] # Step 3: Translate points back to their original positions final_points = [(x + center_x, y + center_y) for x, y in scaled_points] return final_points self.iconEditor.transformPointsButton.setEnabled(False) modifiers = QtGui.QApplication.keyboardModifiers() retain = False if not modifiers == QtCore.Qt.AltModifier else True if modifiers == QtCore.Qt.ShiftModifier: undo = self.undoTransformationPoints.pop() if self.undoTransformationPoints else [] if undo: self.addPoints(undo) self.iconEditor.cte.showMsg("Points transformation undone.") self.showPoints() else: self.iconEditor.cte.showMsg("Undo queue is empty","warning") self.iconEditor.transformPointsButton.setEnabled(True) return else: self.undoTransformationPoints.append(self.getPoints()) self.iconEditor.cte.showMsg("Points backed up in memory prior to transformation, use Shift + command to undo") row = self.list_widget.currentRow() if row == -1: self.iconEditor.transformPointsButton.setEnabled(True) return center = self.getPoints()[row] angle = self.iconEditor.angleAdjustSpinBox.value() if angle: rotatedPoints = rotate_points(self.getPoints(), center, angle) else: rotatedPoints = self.getPoints() movedPoints = [(p[0]+self.iconEditor.xAdjustSpinBox.value(),p[1]+self.iconEditor.yAdjustSpinBox.value()) for p in rotatedPoints] scale = self.iconEditor.scaleSpinBox.value() if scale != 1.0: scaledPoints = scale_around_center(movedPoints, scale) scaledPoints = [(int(round(p[0])),int(round(p[1]))) for p in scaledPoints] self.addAndRetainPoints(scaledPoints, retain) else: self.addAndRetainPoints(movedPoints, retain) self.select(row) self.showPoints() self.iconEditor.transformPointsButton.setEnabled(True) def addAndRetainPoints(self, pts, bRetain): """add pts to current points if bRetain else replace existing""" def fold(list1, list2): """we fold them in so if polyline is used the lines will not be crossed, hopefully""" result = [] #lengths should be the same, but just in case they're not... max_len = max(len(list1), len(list2)) #fold them together #example: list1 = [0,2,4], list2 = [1,3,5], fold(list1,list2) -> [0,1,2,3,4,5] for i in range(max_len): if i < len(list1): result.append(list1[i]) if i < len(list2): result.append(list2[i]) return result if not bRetain: self.addPoints(pts) else: currentPoints = self.getPoints() newPoints = fold(currentPoints,pts) newPoints = self.remove_consecutive_duplicates(newPoints) self.addPoints(newPoints) def importPointsFromFacesTriggered(self): """discretize the faces, in a manner of speaking. We make them into meshes and then get those points.""" selx = FreeCADGui.Selection.getCompleteSelection() if not selx: self.iconEditor.statusBox.setText("Select one or more faces to discretize.") return points = [] for sel in selx: if not sel.Object.isDerivedFrom("Part::Feature"): self.iconEditor.statusBox.setText(f"Must be a part feature, found: {sel.Object.TypeId}.") continue if sel.SubElementNames[0]: face = sel.SubObjects[0] if hasattr(face, "Faces") and face.Faces and face.Area == face.Face1.Area: mesh = MeshPart.meshFromShape(face, Fineness = 0, MaxLength = 1, AllowQuad = 1) pts = [(int(round(p.Vector[0])),int(round(p.Vector[1]))) for p in mesh.Points] points.extend(pts) else: #whole object selected for face in sel.Object.Shape.Faces: mesh = MeshPart.meshFromShape(face, Fineness = 0, MaxLength = 1, AllowQuad = 1) pts = [(int(round(p.Vector[0])),int(round(p.Vector[1]))) for p in mesh.Points] points.extend(pts) points = self.mirror_in_y_direction(points) #points = self.remove_consecutive_duplicates(points) points = list(set(points)) #remove all duplicates self.addPoints(points) self.showPoints() def importVerticesTriggered(self): """Import vertices selected vertices""" selx = FreeCADGui.Selection.getCompleteSelection() if not selx: self.iconEditor.statusBox.setText("Select some edges to discretize and import.\n") return points = [] for sel in selx: if sel.SubElementNames[0]: for v in sel.SubObjects: if hasattr(v,"X"): p = (int(round(v.X)),int(round(v.Y))) points.append(p) else: pts = [v.Point for v in sel.Object.Shape.Vertexes if hasattr(sel.Object,"Shape")] pts = [(int(round(p[0])),int(round(p[1]))) for p in pts] points.extend(pts) points = self.mirror_in_y_direction(points) points = self.remove_consecutive_duplicates(points) self.addPoints(points) self.showPoints() def loadPointsTriggered(self): options = QtGui.QFileDialog.Options() options |= QtGui.QFileDialog.ReadOnly file_dialog = QtGui.QFileDialog() file_dialog.setDefaultSuffix("json") filename, ok = file_dialog.getOpenFileName(None, "Open Points", "", "JSON Files (*.json);;All Files (*)", options=options) if filename: with open(filename, 'r') as file: points = json.load(file) self.addPoints(points) self.showPoints() def savePointsTriggered(self): points = self.getPoints() file_dialog = QtGui.QFileDialog() file_dialog.setDefaultSuffix("json") filename, ok = file_dialog.getSaveFileName(None, "Save Points", "", "JSON Files (*.json);;All Files (*)") if filename: with open(filename, 'w') as file: json.dump(points, file) def copyPointsTriggered(self): clip = QtGui.QClipboard() points = self.getPoints() clip.setText(json.dumps(points)) def remove_consecutive_duplicates(self, input_list): if not input_list: return input_list result = [input_list[0]] for item in input_list[1:]: if item != result[-1]: result.append(item) return result def importDiscretizedEdgesTriggered(self): """import points by discretizing selected edges or objects""" selx = FreeCADGui.Selection.getCompleteSelection() if not selx: self.iconEditor.statusBox.setText("Select some edges to discretize and import.\n") return points = [] for sel in selx: if sel.SubElementNames[0]: for sub in sel.SubObjects: if hasattr(sub,"Length"): length = sub.Length print(f"sub.Length = {sub.Length}") if hasattr(sub,"discretize"): pts = sub.discretize(int(length)) print(f"len(pts): {len(pts)}") pts = [(int(round(v[0])),int(round(v[1]))) for v in pts] if points: if points[-1] != pts[0]: pts = reversed(pts) pts = self.mirror_in_y_direction(pts) pts = self.remove_consecutive_duplicates(pts) points.extend(pts) else: edges = sel.Object.Shape.Edges for edge in edges: length = edge.Length pts = edge.discretize(int(length)) pts = [(int(round(v[0])),int(round(v[1]))) for v in pts] if points: if points[-1] != pts[0]: pts = reversed(pts) pts = self.mirror_in_y_direction(pts) pts = self.remove_consecutive_duplicates(pts) points.extend(pts) self.addPoints(points) self.showPoints() def pastePointsTriggered(self): clip = QtGui.QClipboard() text = clip.text() points = json.loads(text) self.addPoints(points) self.showPoints() def mirror_in_y_direction(self, points): """translates and mirrors points from imported objects because the coordinate system here has 0,0 at the top left corner""" if not points: return points tx = 32 ty = 32 # Apply the translation to all points and mirror in the Y direction mirrored_points = [(x + tx, 64 - (y + ty)) for x, y in points] return mirrored_points def geometryFromSketchTriggered(self): """get the geometry from a sketch and pass it as a string to makeImage()""" sel = FreeCADGui.Selection.getSelection() obj = sel[0] if sel else None if not obj or not obj.isDerivedFrom("Sketcher::SketchObject"): msgBox = QtGui.QMessageBox() msgBox.setWindowTitle("Sketch required") msgBox.setIcon(QtGui.QMessageBox.Information) msgBox.setText(\ "A sketch with geometry to import is required for this feature.\n" +\ "Please select a sketch and run the command again, or if you do \n" +\ "not yet have a sketch we can create one for you with a construction \n" +\ "mode rectangle showing you where to place your sketch elements.\n\n" +\ "Would you like to create a sketch now? Yes or no?\n") msgBox.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) result = msgBox.exec() if result == msgBox.Yes: import Sketcher, Part doc = FreeCAD.ActiveDocument if FreeCAD.ActiveDocument else FreeCAD.newDocument() sketch = doc.addObject("Sketcher::SketchObject", "Icon_Sketch") points =[((-32,32,0),(32,32,0)),((32,32,0),(32,-32,0)),((32,-32,0),(-32,-32,0)),((-32,-32,0),(-32,32,0))] for ii in range(4): sketch.addGeometry(Part.LineSegment(FreeCAD.Vector(points[ii][0]),FreeCAD.Vector(points[ii][1]))) sketch.toggleConstruction(ii) #sketch.addGeometry(geoList,False) conList = [] conList.append(Sketcher.Constraint('Coincident',0,2,1,1)) conList.append(Sketcher.Constraint('Coincident',1,2,2,1)) conList.append(Sketcher.Constraint('Coincident',2,2,3,1)) conList.append(Sketcher.Constraint('Coincident',3,2,0,1)) conList.append(Sketcher.Constraint('Horizontal',0)) conList.append(Sketcher.Constraint('Horizontal',2)) conList.append(Sketcher.Constraint('Vertical',1)) conList.append(Sketcher.Constraint('Vertical',3)) sketch.addConstraint(conList) sketch.addConstraint(Sketcher.Constraint('Equal',1,0)) sketch.addConstraint(Sketcher.Constraint('DistanceX',0,1,0,2,64)) sketch.setDatum(9,App.Units.Quantity('64 mm')) sketch.addConstraint(Sketcher.Constraint('Symmetric',1,2,0,1,-1,1)) doc.recompute() FreeCADGui.Selection.clearSelection() FreeCADGui.Selection.addSelection(sketch) FreeCADGui.ActiveDocument.setEdit(sketch) return else: return geometry = obj.Geometry strings = ["SketchImported:"] for idx,geom in enumerate(geometry): if obj.getConstruction(idx): continue if geom.TypeId == "Part::GeomLineSegment": start = geom.StartPoint[:-1] end = geom.EndPoint[:-1] p1,p2 = self.mirror_in_y_direction([start,end]) p1 = (int(round(p1[0])),int(round(p1[1]))) p2 = (int(round(p2[0])),int(round(p2[1]))) strings.append(f"line({p1[0]},{p1[1]},{p2[0]},{p2[1]})") elif geom.TypeId == "Part::GeomArcOfCircle" \ or geom.TypeId == "Part::GeomBSplineCurve" \ or geom.TypeId == "Part::GeomEllipse" \ or geom.TypeId == "Part::GeomArcOfEllipse" \ or geom.TypeId == "Part::GeomArcOfHyperbola": length = int(round(geom.length())) pts = geom.discretize(length) pts = [(p[0],p[1]) for p in pts] p1 = pts[0] for p2 in pts[1:]: p3 = (int(round(p1[0])),int(round(p1[1]))) p4 = (int(round(p2[0])),int(round(p2[1]))) p5,p6 = self.mirror_in_y_direction([p3,p4]) strings.append(f"line({p5[0]},{p5[1]},{p6[0]},{p6[1]})") p1 = p2 elif geom.TypeId == "Part::GeomCircle": radius = int(round(geom.Radius)) center = geom.Center cX,cY = (int(round(center[0])),int(round(center[1]))) c = self.mirror_in_y_direction([(cX,cY)]) cX,cY = c[0] strings.append(f"circle({cX},{cY},{radius})") elif geom.TypeId == "Part::GeomPoint": x,y = int(round(geom.X)),int(round(geom.Y)) p1 = self.mirror_in_y_direction([(x,y)]) x,y = p1[0] strings.append(f"line({x},{y},{x},{y})") else: try: length = int(round(geom.length())) pts = geom.discretize(length) pts = [(p[0],p[1]) for p in pts] p1 = pts[0] for p2 in pts[1:]: p3 = (int(round(p1[0])),int(round(p1[1]))) p4 = (int(round(p2[0])),int(round(p2[1]))) p5,p6 = self.mirror_in_y_direction([p3,p4]) strings.append(f"line({p5[0]},{p5[1]},{p6[0]},{p6[1]})") p1 = p2 except: FreeCAD.Console.PrintError(f"Unsupported geometry type: {geom.TypeId}. \n") outString = "\n".join(strings) self.iconEditor.textEdit.setText(outString) def backupPointsTriggered(self): self.backupPoints.append(self.getPoints()) def restorePointsTriggered(self): if not self.backupPoints: self.iconEditor.statusBox.setText("No backup points to restore. (Use clipboard to transfer \n"+\ "points between layers.)") return self.addPoints(self.backupPoints.pop()) self.showPoints() def showPointsToggled(self, checked): self.showPointsAllowed = checked self.showPointsCheckBox.blockSignals(True) self.showPointsCheckBox.setChecked(self.showPointsAllowed) self.showPointsCheckBox.blockSignals(False) action = self.iconEditor.pointsMenu.findChild(QtGui.QAction, "showPointsAction") if action: action.blockSignals(True) action.setChecked(self.showPointsAllowed) action.blockSignals(False) if not checked: self.iconEditor.showPoints([],self.pointsColor) #clear points display else: self.showPoints() def showPoints(self): if not self.showPointsAllowed: return self.iconEditor.showPoints(self.getPoints(), self.pointsColor) def duplicatePointTriggered(self): """duplicate selected point, adds to top of list""" row = self.list_widget.currentRow() pts = self.getPoints() pt = [pts[row]] self.addNewItem(pt) self.showPoints() def flashPoint(self, row): if self.flashingBlocked or self.isFlashing: return pts = self.getPoints() pts_without_row = pts[:row] + pts[row+1:] flashes = 3 delay = .05 for ii in range(flashes): self.iconEditor.showPoints(pts_without_row, self.pointsColor) time.sleep(delay) FreeCADGui.updateGui() self.iconEditor.showPoints(pts, self.pointsColor) time.sleep(delay) FreeCADGui.updateGui() self.isFlashing = False def onRemovePointButtonClicked(self): row = self.list_widget.currentRow() self.flashingBlocked = True self.list_widget.takeItem(row) self.relabelList(row) self.flashingBlocked = False self.showPoints() def select(self,row): self.signalsBlocked = True self.list_widget.setCurrentRow(row) self.list_widget.scrollToItem(self.list_widget.item(row)) self.signalsBlocked = False def onValueChanged(self): if self.signalsBlocked: return row = self.list_widget.currentRow() if row == -1: return pts = self.getPoints() if not pts: return self.flashingBlocked = True x = self.xSpinBox.value() y = self.ySpinBox.value() if not QtGui.QApplication.keyboardModifiers() == QtCore.Qt.AltModifier: self.list_widget.takeItem(row) self.insertNewItem((x,y),row) self.showPoints() self.flashingBlocked = False def handleItemChanged(self, current, previous): item = self.list_widget.currentItem() if item: self.editItem(item) def editItem(self, item): pt = self.parseItem(item) self.signalsBlocked = True self.xSpinBox.setValue(pt[0]) self.ySpinBox.setValue(pt[1]) self.flashPoint(self.list_widget.currentRow()) self.signalsBlocked = False def addPoints(self, pts): pb = None if len(pts) > 500: pb = QtGui.QProgressDialog(f"Adding {len(pts)} points", "Cancel", 0, len(pts)) pb.setWindowModality(QtCore.Qt.WindowModal) pb.show() self.list_widget.clear() self.flashingBlocked = True self.list_widget.blockSignals(True) for ii,pt in enumerate(pts): self.addNewItem(pt) if pb and ii % 10 == 0: pb.setValue(ii) FreeCADGui.updateGui() if pb.wasCanceled(): break self.flashingBlocked = False self.list_widget.blockSignals(False) if pb: pb.close() def parseItem(self, item): #txt = "1: (42,56)" # Example string if not item: return None txt = item.text() parts = txt.split(":") if len(parts) == 2: # Extract the part within parentheses coord_str = parts[1].strip() # Remove the parentheses and split the coordinates x_str, y_str = coord_str[1:-1].split(",") # Convert "x" and "y" to integers x = int(x_str) y = int(y_str) return (x,y) else: raise StandardException("invalid string format") def getPoints(self): point_list = [] for ii in range(self.list_widget.count()): pt = self.parseItem(self.list_widget.item(ii)) point_list.append(pt) return point_list def relabelList(self, from_row): pts = self.getPoints() for rr in range(from_row, self.list_widget.count()): item = self.list_widget.item(rr) new_text = f"{rr}: ({pts[rr][0]},{pts[rr][1]})" item.setText(new_text) def insertNewItem(self, pt, row): txt = f"{row}: ({pt[0]},{pt[1]})" item = QtGui.QListWidgetItem(txt) self.list_widget.insertItem(row,item) self.relabelList(row) self.select(row) def addNewItem(self, pt): txt = f"{self.list_widget.count()}: ({pt[0]},{pt[1]})" item = QtGui.QListWidgetItem(txt) self.list_widget.addItem(item) self.select(0) class SimpleIconMaker(QtGui.QDialog): def __init__(self, cte, basePixmap = None, layer=1, parentDlg = None, tabWidget = None): super(SimpleIconMaker, self).__init__() self.layer = layer self.parentDlg = parentDlg self.tabWidget = tabWidget self.childDlg = None #if we spawn a new layer self.closingAll = False self.setWindowTitle(f"Simple 64x64 Icon Maker Layer({self.layer})") self.seed_x = 0 #seeds used for floodfill() algorithm self.seed_y = 0 #we track this to flash that seed point so the user can find it self.zoomFactor = 4 #how big the image in the image label is (4*64, 4*64) = (256,256) #self.lastClicks= [] #track recent clicks on the image in the image label self.cte = cte #cte = Custom Toolbar Editor instance self.pointsColor = (128,128,128) self.topLayout = QtGui.QVBoxLayout() self.layout = QtGui.QGridLayout() self.toolbar = QtGui.QToolBar() self.topLayout.addWidget(self.toolbar) self.topLayout.addLayout(self.layout) self.setLayout(self.topLayout) self.fileMenu = QtGui.QMenu() self.fileMenuButton = QtGui.QToolButton() self.fileMenuButton.setArrowType(QtCore.Qt.NoArrow) self.fileMenuButton.setText("File") #menuIcon = self.cte.QIconFromXPMString(self.cte.getMenuIcon()) #self.fileMenuButton.setIcon(menuIcon) #self.fileMenuButton.setPopupMode(QtGui.QToolButton.MenuButtonPopup) self.fileMenuButton.clicked.connect(self.fileMenuButton.showMenu) self.fileMenuButton.setMenu(self.fileMenu) self.toolbar.addWidget(self.fileMenuButton) self.layerMenu = QtGui.QMenu() self.layerMenuButton = QtGui.QToolButton() self.layerMenuButton.setText("Layer") #self.layerMenuButton.setPopupMode(QtGui.QToolButton.MenuButtonPopup) self.layerMenuButton.clicked.connect(self.layerMenuButton.showMenu) self.layerMenuButton.setMenu(self.layerMenu) self.toolbar.addWidget(self.layerMenuButton) self.pointsMenu = QtGui.QMenu() self.pointsMenuButton = QtGui.QToolButton() self.pointsMenuButton.setText("Points") #self.pointsMenuButton.setPopupMode(QtGui.QToolButton.MenuButtonPopup) self.pointsMenuButton.clicked.connect(self.pointsMenuButton.showMenu) self.pointsMenuButton.setMenu(self.pointsMenu) self.toolbar.addWidget(self.pointsMenuButton) self.settingsMenu = QtGui.QMenu() self.settingsMenuButton = QtGui.QToolButton() self.settingsMenuButton.setText("Settings") self.settingsMenuButton.clicked.connect(self.settingsMenuButton.showMenu) self.settingsMenuButton.setMenu(self.settingsMenu) self.toolbar.addWidget(self.settingsMenuButton) self.importSketchButton = QtGui.QToolButton() self.importSketchButton.setToolTip("Import geometry from a selected sketch.\n" +\ "All elements in sketch should be *inside* a construction mode 64x64 \n" +\ "mm rectangle that is constrained with its center at the origin.\n") importSketchIcon = self.cte.QIconFromXPMString(import_sketch_icon) self.importSketchButton.setIcon(importSketchIcon) self.toolbar.addWidget(self.importSketchButton) self.transformPointsButton = QtGui.QToolButton() self.transformPointsButton.setToolTip( "Transform the construction points using the scale, \n" +\ "angle, and x and y adjustment widgets.\n" +\ "Shift+Click = undo last transformation\n" +\ "Alt+Click = add transformed points to existing points") transformIcon = self.cte.QIconFromXPMString(transformPointsIcon) self.transformPointsButton.setIcon(transformIcon) self.toolbar.addWidget(self.transformPointsButton) self.recomputeButton = QtGui.QToolButton() self.recomputeButton.setToolTip("Recompute this layer") recomputeIcon = self.cte.QIconFromXPMString(refreshButtonIcon) self.recomputeButton.setIcon(recomputeIcon) self.toolbar.addWidget(self.recomputeButton) self.textLabel = QtGui.QLabel("Element:") self.layout.addWidget(self.textLabel, 2, 0, 1, 1) self.textEdit = QtGui.QLineEdit() self.textEdit.setPlaceholderText("Enter text here or an element function") self.textEdit.textChanged.connect(self.updateImage) self.layout.addWidget(self.textEdit, 2, 1, 1, 3) #font setting moved to menu, but we retain this for the functionality #even though it's not shown self.fontLabel = QtGui.QLabel("Font:") #self.layout.addWidget(self.fontLabel, 1, 0, 1, 1) self.fontBox = QtGui.QComboBox() self.fontBox.setMinimumWidth(300) self.fonts = { "HERSHEY_COMPLEX":cv2.FONT_HERSHEY_COMPLEX,\ "HERSHEY_COMPLEX_SMALL":cv2.FONT_HERSHEY_COMPLEX_SMALL,\ "HERSHEY_DUPLEX":cv2.FONT_HERSHEY_DUPLEX,\ "HERSHEY_SCRIPT_COMPLEX":cv2.FONT_HERSHEY_SCRIPT_COMPLEX,\ "HERSHEY_SCRIPT_SIMPLEX":cv2.FONT_HERSHEY_SCRIPT_SIMPLEX,\ "HERSHEY_SIMPLEX":cv2.FONT_HERSHEY_SIMPLEX,\ "HERSHEY_TRIPLEX":cv2.FONT_HERSHEY_TRIPLEX,\ "ITALIC":cv2.FONT_ITALIC} self.fontBox.currentIndexChanged.connect(self.updateImage) self.fontBox.addItems(self.fonts.keys()) #self.layout.addWidget(self.fontBox, 1, 1, 1, 3) self.lineTypeBox = QtGui.QComboBox() self.lineTypes = { "cv2.LINE_4":cv2.LINE_4,\ "cv2.LINE_8":cv2.LINE_8,\ "cv2.LINE_AA":cv2.LINE_AA} self.lineTypeBox.currentIndexChanged.connect(self.updateImage) self.lineTypeBox.addItems(self.lineTypes.keys()) self.colorLabel = QtGui.QLabel("Color:") self.layout.addWidget(self.colorLabel, 3, 0, 1, 1) self.colorButton = QtGui.QPushButton("Elements") self.colorButton.setStyleSheet("background-color:red;") self.colorButton.clicked.connect(self.onColorButtonClicked) self.layout.addWidget(self.colorButton, 3, 1, 1, 1) self.borderColorLabel = QtGui.QLabel("Border:") self.layout.addWidget(self.borderColorLabel, 3, 2, 1, 1) self.borderColorButton = QtGui.QPushButton("Border") self.borderColorButton.setStyleSheet("background-color:darkred;") self.borderColorButton.clicked.connect(self.onBorderColorButtonClicked) self.layout.addWidget(self.borderColorButton, 3, 3, 1, 1) self.scaleLabel = QtGui.QLabel("Scale:") self.layout.addWidget(self.scaleLabel, 5, 0, 1, 1) self.scaleSpinBox = QtGui.QDoubleSpinBox() self.scaleSpinBox.valueChanged.connect(self.updateImage) self.scaleSpinBox.setToolTip("Scaling only applicable to elements with a radius or text elements or construction points") self.scaleSpinBox.setRange(0.01,100) self.scaleSpinBox.setSingleStep(0.1) self.scaleSpinBox.setValue(1) self.layout.addWidget(self.scaleSpinBox, 5, 1, 1, 1) self.angleAdjustLabel = QtGui.QLabel("Angle adjust:") self.layout.addWidget(self.angleAdjustLabel, 5, 2, 1, 1) self.angleAdjustSpinBox = QtGui.QSpinBox() self.angleAdjustSpinBox.setToolTip("only works with ellipses and polygons") self.angleAdjustSpinBox.valueChanged.connect(self.updateImage) self.angleAdjustSpinBox.setRange(-720,720) self.angleAdjustSpinBox.setSingleStep(1) self.angleAdjustSpinBox.setValue(0) self.layout.addWidget(self.angleAdjustSpinBox, 5, 3, 1, 1) self.thicknessLabel = QtGui.QLabel("Thickness:") self.layout.addWidget(self.thicknessLabel, 4, 0, 1, 1) self.thicknessSpinBox = QtGui.QSpinBox() self.thicknessSpinBox.setToolTip("-1 to fill closed shapes like circles, else 0 or 1 will be interpreted as 1 for text") self.thicknessSpinBox.valueChanged.connect(self.updateImage) self.thicknessSpinBox.setRange(-1,100) self.thicknessSpinBox.setSingleStep(1) self.thicknessSpinBox.setValue(2) self.layout.addWidget(self.thicknessSpinBox, 4, 1, 1, 1) self.borderThicknessLabel = QtGui.QLabel("Border:") self.layout.addWidget(self.borderThicknessLabel, 4, 2, 1, 1) self.borderThicknessSpinBox = QtGui.QSpinBox() self.borderThicknessSpinBox.setToolTip("0 for no border") self.borderThicknessSpinBox.valueChanged.connect(self.updateImage) self.borderThicknessSpinBox.setRange(0,100) self.borderThicknessSpinBox.setSingleStep(1) self.borderThicknessSpinBox.setValue(2) self.layout.addWidget(self.borderThicknessSpinBox, 4, 3, 1, 1) #for dragging in the image label self.dragging = False self.start_pos = None self.xAdjustLabel = QtGui.QLabel("X adjust:") self.layout.addWidget(self.xAdjustLabel, 6, 0, 1, 1) self.xAdjustSpinBox = QtGui.QSpinBox() self.xAdjustSpinBox.valueChanged.connect(self.updateImage) self.xAdjustSpinBox.setRange(-100,100) self.xAdjustSpinBox.setSingleStep(1) self.xAdjustSpinBox.setValue(0) self.layout.addWidget(self.xAdjustSpinBox, 6, 1, 1, 1) self.yAdjustLabel = QtGui.QLabel("Y adjust:") self.layout.addWidget(self.yAdjustLabel, 6, 2, 1, 1) self.yAdjustSpinBox = QtGui.QSpinBox() self.yAdjustSpinBox.valueChanged.connect(self.updateImage) self.yAdjustSpinBox.setRange(-100,100) self.yAdjustSpinBox.setSingleStep(1) self.yAdjustSpinBox.setValue(0) self.layout.addWidget(self.yAdjustSpinBox, 6, 3, 1, 1) self.statusBox = QtGui.QLabel() self.statusBox.setStyleSheet("color:blue;") self.layout.addWidget(self.statusBox, 0,0,1,4) self.addElementLabel = QtGui.QLabel("Add element:") self.layout.addWidget(self.addElementLabel, 1, 0, 1, 1) self.addElementComboBox = QtGui.QComboBox() self.addElementComboBox.currentIndexChanged.connect(self.onAddElementComboBoxCurrentIndexChanged) #self.addElementComboBox.setEnabled(False) self.elements = {"Add an element (one per layer)":("","Items with () make use of points, right-click to add some -->\n" +\ "One element per layer, add a new layer to add more elements."), \ "clear element box":("","Clears current element"), \ "putpoints()":("putpoints()", "puts the construction points into the image as regular points"), \ "text":("A","Just enter the text you want displayed"), \ "circle":("circle(32,32,10)","circle(center_x, center_y, radius)"),\ "circle3points()":("circle3points()","circle3points() takes last 3 construction points and uses \n" +\ "them to find the circle that all 3 points lie on."), \ "ellipse":("ellipse(32,32,10,15,45)","ellipse(center_x, center_y, major_radius, minor_radius, angle)"), \ "ellipse5points()":("ellipse5points()","Draws the ellipse that the last 5 construction points lie on"), \ "rectangle()":("rectangle()","rectangle(top_left_x, top_left_y, bottom_right_x, bottom_right_y)\n" +\ "Last 2 points can be used to define top left and bottom right"), \ "rectangles()":("rectangles()","Uses the points in pairs to make multiple rectangles.\n" +\ "Tip: set color to black or white (thickness = -1 to fill) for erasers"),\ "polygon":("polygon(32,32,5,10,45)","polygon(center_x, center_y, number_sides, circumradius, angle)"), \ "polyline()":("polyline()","polyline(x1,y1, x2,y2, ... end with x1,y1 to close, thickness = -1 to fill)\n"+ "Can use construction points to define the start and end points of the lines."),\ "line()":("line()","line(x1,y1, x2,y2) or line() to use last 2 clicks"), \ "arc": ("ellipse(20,30,10,10,0,90,180)","ellipse(center_x,center_y,major,minor,angle,startAngle,endAngle)"), \ "arc3points()":("arc3points()","arc3points(), arc that passes through last 3 construction points"), \ "arc3points2()":("arc3points2()","complementary arc to arc3points()"), \ "floodfill()":("floodfill()","Right-click on the image, then enter floodfill() to seed the clicked point"), \ "floodfillall()":("floodfillall(20)","Tip right-click on all the seed points, and then run this command")} self.addElementComboBox.addItems(sorted(self.elements.keys())) self.layout.addWidget(self.addElementComboBox, 1, 1, 1, 3) self.basePixmap = basePixmap self.basePixmapMethod = None #options are None, ("system",system_name), ("style", style_name), ("file", file_path) self.loadBaseButton = QtGui.QPushButton("Load base") self.loadBaseButton.setToolTip("Load an image or select an icon to serve as the base for your icon") self.loadBaseButton.clicked.connect(self.loadBaseButtonClicked) #self.layout.addWidget(self.loadBaseButton, 9, 5 , 1, 1) self.addLayerButton = QtGui.QPushButton("Add layer") self.addLayerButton.setToolTip("Add another layer. Each layer builds on top of the lower layers, one feature per layer") self.addLayerButton.clicked.connect(self.onAddLayerButtonClicked) #self.layout.addWidget(self.addLayerButton, 9, 4, 1, 1) # moved to menu for now, but save this in case we decide to put it back # self.saveButton = QtGui.QPushButton("Export XPM") # self.saveButton.clicked.connect(self.saveImage) # self.saveButton.setToolTip("Puts the XPM string for this icon into the plain text area in the main dialog.") # self.layout.addWidget(self.saveButton, 10, 4, 1, 1) self.closeTabButton = QtGui.QPushButton("Close tab") self.closeTabButton.setToolTip("Closes this layer. (only top layer can be closed)") self.closeTabButton.clicked.connect(self.onCloseTabButtonClicked) # self.layout.addWidget(self.closeTabButton, 10, 6, 1, 1) # moved to menu for now # self.closeAllButton = QtGui.QPushButton("Close all layers") # self.closeAllButton.clicked.connect(self.closeAll) # self.closeAllButton.setToolTip("Closes this and all other layers") # self.layout.addWidget(self.closeAllButton, 10, 5, 1, 1) self.flashButton = QtGui.QPushButton("Flash layer") self.flashButton.setToolTip("Flashes the element in the active layer") self.flashButton.clicked.connect(self.onFlashButtonClicked) self.layout.addWidget(self.flashButton, 10, 6, 1, 1) self.pointsBox = EditableListWidget(self, self.pointsColor) self.layout.addWidget(self.pointsBox, 7, 0, 3, 4) self.iconLabel = QtGui.QLabel() self.iconLabel.installEventFilter(self) self.iconLabel.setObjectName("iconLabel") self.iconLabel.setStyleSheet("background-color: lightgray;") self.iconLabel.setContentsMargins(2,2,2,2) self.iconLabel.setAlignment(QtCore.Qt.AlignCenter) self.iconLabel.setToolTip("Right-click to add construction point, Shift + Right-click to select an existing point.") self.iconLabel.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding) self.layout.addWidget(self.iconLabel, 0, 4, 10, 3) self.pixmap = None self.updateImage() self.whiteToTransparentCheckBox = QtGui.QCheckBox() self.whiteToTransparentCheckBox.setText("White -> Transparent") self.whiteToTransparentCheckBox.setToolTip("Applied when the icon is exported") self.blackToTransparentCheckBox = QtGui.QCheckBox() self.blackToTransparentCheckBox.setText("Black -> Transparent") self.blackToTransparentCheckBox.setToolTip("Applied when the icon is exported") self.whiteToTransparentCheckBox.setChecked(True) self.blackToTransparentCheckBox.setChecked(True) self.layout.addWidget(self.whiteToTransparentCheckBox, 10, 2, 1, 2) self.layout.addWidget(self.blackToTransparentCheckBox, 10, 0, 1, 2) self.pointsBox.showPoints() self.setupMenu() # moved to menu # self.loadProjectButton = QtGui.QPushButton("Load project") # self.loadProjectButton.clicked.connect(self.loadProjectButtonClicked) # self.layout.addWidget(self.loadProjectButton, 10,2,1,1) # # self.saveProjectButton = QtGui.QPushButton("Save project") # self.saveProjectButton.clicked.connect(self.saveProjectButtonClicked) # self.layout.addWidget(self.saveProjectButton, 10,3,1,1) def onTabWidgetCurrentChanged(self, idx): if idx == 0: count = self.tabWidget.count() iconMaker = self.tabWidget.widget(count-1) iconMaker.onAddLayerButtonClicked() def setupMenu(self): """make the menus""" #File loadProjectAction = QtGui.QAction("Load project", self.fileMenu) loadProjectAction.triggered.connect(self.loadProjectButtonClicked) self.fileMenu.addAction(loadProjectAction) saveProjectAction = QtGui.QAction("Save project", self.fileMenu) saveProjectAction.triggered.connect(self.saveProjectButtonClicked) self.fileMenu.addAction(saveProjectAction) loadBaseAction = QtGui.QAction("Load base pixmap", self.fileMenu) loadBaseAction.triggered.connect(self.loadBaseButtonClicked) self.fileMenu.addAction(loadBaseAction) exportXPMAction = QtGui.QAction("Export XPM", self.fileMenu) exportXPMAction.triggered.connect(self.saveImage) self.fileMenu.addAction(exportXPMAction) exportPNGAction = QtGui.QAction("Export PNG", self.fileMenu) exportPNGAction.triggered.connect(self.exportPNG) self.fileMenu.addAction(exportPNGAction) exportSVGAction = QtGui.QAction("Export SVG", self.fileMenu) exportSVGAction.triggered.connect(self.exportSVG) self.fileMenu.addAction(exportSVGAction) self.fileMenu.addSeparator() closeAllAction = QtGui.QAction("Quit", self.fileMenu) closeAllAction.triggered.connect(self.closeAll) self.fileMenu.addAction(closeAllAction) #Layers closeTabAction = QtGui.QAction("Close top layer", self.layerMenu) closeTabAction.triggered.connect(self.onCloseTabButtonClicked) self.layerMenu.addAction(closeTabAction) addLayerAction = QtGui.QAction("Add layer", self.layerMenu) addLayerAction.triggered.connect(self.onAddLayerButtonClicked) self.layerMenu.addAction(addLayerAction) refreshAction = QtGui.QAction("Recompute layer", self.layerMenu) refreshAction.triggered.connect(self.updateImage) self.layerMenu.addAction(refreshAction) self.recomputeButton.clicked.connect(self.updateImage) #Points clearPointsAction = QtGui.QAction("Clear points", self.pointsMenu) clearPointsAction.triggered.connect(self.pointsBox.onClearButtonClicked) self.pointsMenu.addAction(clearPointsAction) removePointAction = QtGui.QAction("Remove selected point", self.pointsMenu) removePointAction.triggered.connect(self.pointsBox.onRemovePointButtonClicked) self.pointsMenu.addAction(removePointAction) duplicatePointAction = QtGui.QAction("Duplicated selected point", self.pointsMenu) duplicatePointAction.triggered.connect(self.pointsBox.duplicatePointTriggered) self.pointsMenu.addAction(duplicatePointAction) showPointsAction = QtGui.QAction("Show points (boolean)", self.pointsMenu) showPointsAction.setCheckable(True) showPointsAction.setChecked(True) showPointsAction.setObjectName("showPointsAction") showPointsAction.toggled.connect(self.pointsBox.showPointsToggled) self.pointsMenu.addAction(showPointsAction) transformPointsAction = QtGui.QAction("Apply transformation to points", self.pointsMenu) transformPointsAction.triggered.connect(self.pointsBox.transformPointsTriggered) self.pointsMenu.addAction(transformPointsAction) self.transformPointsButton.clicked.connect(self.pointsBox.transformPointsTriggered) backupPointsAction = QtGui.QAction("Backup points in memory", self.pointsMenu) backupPointsAction.triggered.connect(self.pointsBox.backupPointsTriggered) self.pointsMenu.addAction(backupPointsAction) restorePointsAction = QtGui.QAction("Restore points from memory", self.pointsMenu) restorePointsAction.triggered.connect(self.pointsBox.restorePointsTriggered) self.pointsMenu.addAction(restorePointsAction) copyPointsAction = QtGui.QAction("Copy points to clipboard", self.pointsMenu) copyPointsAction.triggered.connect(self.pointsBox.copyPointsTriggered) self.pointsMenu.addAction(copyPointsAction) pastePointsAction = QtGui.QAction("Paste points from clipboard", self.pointsMenu) pastePointsAction.triggered.connect(self.pointsBox.pastePointsTriggered) self.pointsMenu.addAction(pastePointsAction) importDiscretizedEdgesAction = QtGui.QAction("Import discretized edges of object", self.pointsMenu) importDiscretizedEdgesAction.triggered.connect(self.pointsBox.importDiscretizedEdgesTriggered) self.pointsMenu.addAction(importDiscretizedEdgesAction) importVerticesAction = QtGui.QAction("Import selected vertices", self.pointsMenu) importVerticesAction.triggered.connect(self.pointsBox.importVerticesTriggered) self.pointsMenu.addAction(importVerticesAction) importDiscretizedFacesAction = QtGui.QAction("Import points from faces", self.pointsMenu) importDiscretizedFacesAction.triggered.connect(self.pointsBox.importPointsFromFacesTriggered) self.pointsMenu.addAction(importDiscretizedFacesAction) geometryFromSketchAction = QtGui.QAction("Geometry from sketch", self.pointsMenu) geometryFromSketchAction.triggered.connect(self.pointsBox.geometryFromSketchTriggered) #self.pointsMenu.addAction(geometryFromSketchAction) self.importSketchButton.clicked.connect(self.pointsBox.geometryFromSketchTriggered) savePointsAction = QtGui.QAction("Save points", self.pointsMenu) savePointsAction.triggered.connect(self.pointsBox.savePointsTriggered) self.pointsMenu.addAction(savePointsAction) loadPointsAction = QtGui.QAction("Load points", self.pointsMenu) loadPointsAction.triggered.connect(self.pointsBox.loadPointsTriggered) self.pointsMenu.addAction(loadPointsAction) #settings fontMenu = QtGui.QMenu("Font") self.settingsMenu.addMenu(fontMenu) fontActionGroup = QtGui.QActionGroup(self) fontActionGroup.triggered.connect(self.onFontMenuAction) for k,v in self.fonts.items(): option = QtGui.QAction(k, self) option.setCheckable(True) fontActionGroup.addAction(option) fontMenu.addAction(option) if k == "HERSHEY_SIMPLEX": option.setChecked(True) lineTypeMenu = QtGui.QMenu("Line type") self.settingsMenu.addMenu(lineTypeMenu) lineTypeActionGroup = QtGui.QActionGroup(self) lineTypeActionGroup.triggered.connect(self.onLineTypeMenuAction) for k,v in self.lineTypes.items(): option = QtGui.QAction(k, self) option.setCheckable(True) lineTypeActionGroup.addAction(option) lineTypeMenu.addAction(option) if k == "cv2.LINE_AA": option.setChecked(True) def onLineTypeMenuAction(self, action): self.lineTypeBox.setCurrentText(action.text()) def onFontMenuAction(self,action): self.fontBox.setCurrentText(action.text()) def loadProject(self): data = self.load_dict_from_json() if not data: return #first close all tabs except this one children = [] child = self.childDlg while child: children.append(child) child = child.childDlg for child in children: child.onCloseTabButtonClicked() if self.childDlg: self.childDlg.onCloseTabButtonClicked() self.putSerializingData(data["1"]) print(f"data.keys(): {data.keys()}") if len(data)>1: self.onAddLayerButtonClicked() child = self.childDlg for ii in range(len(data)-2): child.onAddLayerButtonClicked() child = child.childDlg children = [] child = self.childDlg while child: children.append(child) child = child.childDlg for child in children: key = str(child.layer) if key in data.keys(): child.putSerializingData(data[key]) child.updateImage() def saveProject(self): data = {} children = [self] child = self.childDlg while child: children.append(child) child = child.childDlg for child in children: data[child.layer] = child.getSerializingData() self.save_dict_to_json(data) def load_dict_from_json(self): options = QtGui.QFileDialog.Options() file_name, ok = QtGui.QFileDialog.getOpenFileName(None, "Open JSON File",\ "", "JSON Files (*.json);;All Files (*)", options=options) if file_name: try: with open(file_name, 'r') as file: dict_data = json.load(file) print(f"Dictionary loaded from {file_name}") return dict_data except Exception as e: print(f"Error loading JSON file: {str(e)}") else: return None def save_dict_to_json(self, dict_data): options = QtGui.QFileDialog.Options() file_name, ok = QtGui.QFileDialog.getSaveFileName(None, "Save JSON File", "", \ "JSON Files (*.json);;All Files (*)", options=options) if file_name: try: with open(file_name, 'w') as file: json.dump(dict_data, file, indent=4) print(f"Dictionary saved to {file_name}") except Exception as e: print(f"Error saving JSON file: {str(e)}") def loadProjectButtonClicked(self): print(f"this is layer{self.layer}, but we are calling layer 1 dialog loadProject() function") self.cte.iconMakerDlg.loadProject() def saveProjectButtonClicked(self): self.cte.iconMakerDlg.saveProject() def getSerializingData(self): """return a dictionary of information needed to restore this dialog""" data = {} data["layer"] = self.layer data["seed_x"] = self.seed_x data["seed_y"] = self.seed_y data["zoomFactor"] = self.zoomFactor data["textEdit"] = self.textEdit.text() data["fontBox"] = self.fontBox.currentText() data["lineTypeBox"] = self.lineTypeBox.currentText() data["color"] = self.getColor() data["borderColor"] = self.getBorderColor() data["scaleSpinBox"] = self.scaleSpinBox.value() data["angleAdjust"] = self.angleAdjustSpinBox.value() data["thickness"] = self.thicknessSpinBox.value() data["borderThickness"] = self.borderThicknessSpinBox.value() data["xAdjust"] = self.xAdjustSpinBox.value() data["yAdjust"] = self.yAdjustSpinBox.value() data["basePixmapMethod"] = self.basePixmapMethod data["pointsBox"] = self.pointsBox.getPoints() data["pointsColor"] = self.pointsColor data["pointsBoxBackupPoints"] = self.pointsBox.backupPoints data["whiteToTransparent"] = self.whiteToTransparentCheckBox.isChecked() data["blackToTransparent"] = self.blackToTransparentCheckBox.isChecked() return data def putSerializingData(self, data): self.layer = data["layer"] self.seed_x = data["seed_x"] self.seed_y = data["seed_y"] self.zoomFactor = data["zoomFactor"] self.textEdit.setText(data["textEdit"]) self.fontBox.setCurrentText(data["fontBox"]) self.lineTypeBox.setCurrentText(data["lineTypeBox"]) #print(f"data['color']: {data['color']}") self.setColor(data["color"]) self.setBorderColor(data["borderColor"]) self.scaleSpinBox.setValue(data["scaleSpinBox"]) self.angleAdjustSpinBox.setValue(data["angleAdjust"]) self.thicknessSpinBox.setValue(data["thickness"]) self.borderThicknessSpinBox.setValue(data["borderThickness"]) self.xAdjustSpinBox.setValue(data["xAdjust"]) self.yAdjustSpinBox.setValue(data["yAdjust"]) self.basePixmapMethod = data["basePixmapMethod"] if self.basePixmapMethod: method,value = self.basePixmapMethod if method == "file": self.basePixmap = QtGui.QPixmap(value) elif method == "style": icon = self.style().standardIcon(value) self.basePixmap = icon.pixmap(64,64) elif method == "system": icon = QtGui.QIcon(FreeCADGui.getIcon(value)) self.basePixmap = icon.pixmap(64,64) self.updateImage() self.pointsBox.addPoints(data["pointsBox"]) self.pointsBox.setColor(data["pointsColor"]) self.pointsBox.backupPoints = data["pointsBoxBackupPoints"] self.whiteToTransparentCheckBox.setChecked(data["whiteToTransparent"]) self.blackToTransparentCheckBox.setChecked(data["blackToTransparent"]) def showPoints(self, pts, color): """put the points in a temporary pixmap, a copy of the current layer's pixmap, and display the temporary pixmap""" base_image = self.pixmap.toImage() # Convert the QImage to a numpy array, because cv2 needs it in that format width, height = base_image.width(), base_image.height() format = base_image.format() if format == 3: # RGB format (3 bytes per pixel) image_array = np.frombuffer(base_image.constBits(), dtype=np.uint8).reshape((height, width, 3)) elif format == 4 or format == QtGui.QImage.Format.Format_ARGB32_Premultiplied: # RGBA format (4 bytes per pixel) image_array = np.frombuffer(base_image.constBits(), dtype=np.uint8).reshape((height, width, 4)) if image_array is not None: if format == 4 or format == QtGui.QImage.Format.Format_ARGB32_Premultiplied: # ARGB or ARGB_Premultiplied to BGR temp_image = cv2.cvtColor(image_array, cv2.COLOR_RGBA2BGR) elif format == 3: # RGB to BGR temp_image = cv2.cvtColor(image_array, cv2.COLOR_RGB2BGR) temp_image = cv2.resize(temp_image, (64,64)) #to fit label for pt in pts: #pt = (self.seed_x,self.seed_y) cv2.polylines(temp_image, [np.array([pt,pt])], isClosed = False, thickness = 1, color=color) h, w, ch = temp_image.shape bytes_per_line = ch * w q_image = QtGui.QImage(temp_image.data, w, h, bytes_per_line, QtGui.QImage.Format_RGB888) temp_pixmap = QtGui.QPixmap.fromImage(q_image) scaledPixmap = temp_pixmap.scaled(64*self.zoomFactor, 64*self.zoomFactor,\ QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) self.iconLabel.setPixmap(scaledPixmap) def flashFloodfillPosition(self): """called from makeImage() when it's doing a floodfill()""" flashing_color = [(255, 0, 0), (0, 255, 0), (0, 0, 255)] flashes = 1 sleepTime = .125 for ii in range(flashes): # Flash the seed point for color in flashing_color: self.showPoints([(self.seed_x,self.seed_y)], color) time.sleep(sleepTime) FreeCADGui.updateGui() scaledPixmap = self.pixmap.scaled(64*self.zoomFactor, 64*self.zoomFactor,\ QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) self.iconLabel.setPixmap(scaledPixmap) #back to original image def onFlashButtonClicked(self): """flash the current layer's element off and on a few times""" backup = self.textEdit.text() if "floodfill(" in backup: self.flashFloodfillPosition() return flashes = 3 sleepTime = .125 for ii in range(flashes): self.textEdit.setText("") time.sleep(sleepTime) FreeCADGui.updateGui() self.textEdit.setText(backup) time.sleep(sleepTime) FreeCADGui.updateGui() def onAddElementComboBoxCurrentIndexChanged(self): currentValue = self.addElementComboBox.currentText() elementToAdd = self.elements[currentValue] if self.addElementComboBox.currentIndex() == 0: self.statusBox.setText(elementToAdd[1]) return currentValue = self.addElementComboBox.currentText() elementToAdd = self.elements[currentValue] self.textEdit.setText(elementToAdd[0]) self.textEdit.setToolTip(elementToAdd[1]) self.statusBox.setText(elementToAdd[1]) self.addElementComboBox.blockSignals(True) self.addElementComboBox.setCurrentIndex(0) self.addElementComboBox.blockSignals(False) self.updateImage() def updateImage(self): if not bool(hasattr(self, "textEdit") and hasattr(self,"scaleSpinBox") and \ hasattr(self, "thicknessSpinBox") and hasattr(self, "iconLabel") and \ hasattr(self, "fontBox") and hasattr(self, "borderThicknessSpinBox") and \ hasattr(self, "xAdjustSpinBox") and hasattr(self, "yAdjustSpinBox") and \ hasattr(self, "addElementComboBox") and hasattr(self, "angleAdjustSpinBox")): return self.pixmap = self.makeImage(self.textEdit.text() ,self.scaleSpinBox.value(), \ self.thicknessSpinBox.value(), self.getColor(), (255,255,255), \ self.getBorderColor(), self.borderThicknessSpinBox.value()) scaledPixmap = self.pixmap.scaled(64*self.zoomFactor, 64*self.zoomFactor, \ QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) self.iconLabel.setPixmap(scaledPixmap) self.pointsBox.showPoints() #we have a child dialog if there is another layer following this one #inform next layer it needs to update, ensure this layer is that layer's base image if self.childDlg: self.childDlg.basePixmap = self.pixmap self.childDlg.updateImage() def setColor(self, colorTuple): r,g,b,a = colorTuple color = QtGui.QColor(r,g,b,a) colorString = f'rgb({color.red()}, {color.green()}, {color.blue()})' self.colorButton.setStyleSheet(f'background-color: {colorString};') def setBorderColor(self, colorTuple): r,g,b,a = colorTuple color = QtGui.QColor(r,g,b,a) colorString = f'rgb({color.red()}, {color.green()}, {color.blue()})' self.borderColorButton.setStyleSheet(f'background-color: {colorString};') def getColor(self): """gets the current color of the color button, returns as (r,g,b,a) each a value from 0 to 255""" palette = self.colorButton.palette() color = palette.color(palette.Background) #print(f"color = {color.red(), color.green(), color.blue()}") return (color.red(), color.green(), color.blue(), 0) #0 not used, but 4 values are expected def getBorderColor(self): """gets the current color of the border color button, returns as (r,g,b,a) each from 0 to 255""" palette = self.borderColorButton.palette() color = palette.color(palette.Background) #print(f"color = {color.red(), color.green(), color.blue()}") return (color.red(), color.green(), color.blue(), 0) #0 not used, but 4 values are expected def onAddLayerButtonClicked(self): """Opens a new tab with the current image as a base for the new image""" self.childDlg = SimpleIconMaker(self.cte, self.pixmap, layer=self.layer+1, parentDlg = self, tabWidget = self.tabWidget) self.childDlg.setColor(self.getColor()) self.childDlg.setBorderColor(self.getBorderColor()) self.childDlg.pointsBox.setColor(self.pointsColor) self.childDlg.zoomFactor = self.zoomFactor self.childDlg.updateImage() self.tabWidget.addTab(self.childDlg, f"Layer{self.layer+1}") self.tabWidget.setCurrentWidget(self.childDlg) self.childDlg.show() return def enableAction(self, menu, actionText, bEnable): """enable or disable an action in the menu""" actions = menu.actions() for action in actions: if action.text() == actionText: action.setEnabled(bEnable) break def onCloseTabButtonClicked(self): count = self.tabWidget.count() lastTab = self.tabWidget.widget(count-1) if count >2: lastTab.close() self.tabWidget.removeTab(count-1) #top layer's parent will be None since there is no layer below it if hasattr(lastTab.parentDlg,"addLayerButton"): #we're closing this top layer, so re-enable the next lower level's add layer button #lastTab.parentDlg.addLayerButton.setEnabled(True) #lastTab.parentDlg.enableAction(layerMenu,"Add layer tab", True) pass else: self.closeAll() #closing layer1, so close everything. #lastTab.close() def closeAll(self): """closes and deletes all layers and the tab widget""" self.closingAll = True widgets = [] p = self.parentDlg while p: widgets.append(p) p = p.parentDlg child = self.childDlg while child: widgets.append(child) child = child.childDlg for widget in widgets: widget.closingAll = True #print(f"Deleting layer: {widget.layer}") widget.close() widget.deleteLater self.tabWidget.close() self.tabWidget.deleteLater() self.close() self.deleteLater() def closeEvent(self, event): """handle close event""" #some of this is unnecessary, but it works, so we leave it in just #in case we want to allow closing interior layers in the future #(currently, only the top layer can be closed) #this logic removes the tab being closed from the image chain #by setting it's own parent's childDlg property to point to its #own childDlg and it's child's parentDlg to its own parentDlg if not self.closingAll: if self.parentDlg: # self.parentDlg.closeTabButton.setEnabled(True) # self.parentDlg.enableAction("Close layer tab", True) pass if self.childDlg: self.parentDlg.childDlg = self.childDlg self.childDlg.parentDlg = self.parentDlg self.parentDlg.updateImage() self.childDlg.updateImage() if self.layer != 1: # self.parentDlg.closeTabButton.setEnabled(True) # self.parentDlg.enableAction("Close layer tab",True) pass # self.parentDlg.addLayerButton.setEnabled(True) # self.parentDlg.enableAction("Add layer tab", True) #print(f"Deleting layer: {self.layer}") self.deleteLater() #since not all tabs are closing we ignore the #event as we have already deleted the layer event.ignore() else: #everything is closing, so propage the event up the chain super().closeEvent(event) @property def lastClicks(self): return self.pointsBox.getPoints() @lastClicks.setter def lastClicks(self, pts): self.pointsBox.addPoints(pts) self.pointsBox.showPoints() def addAClick(self, pt): self.pointsBox.insertNewItem(pt,0) self.pointsBox.showPoints() def exportSVG(self): """first we make it ionto the xpm with transparent background, then make a pixmap back out of the xpm and use that to export to svg""" image = self.pixmap.toImage() buffer = QtCore.QBuffer() buffer.open(QtCore.QBuffer.ReadWrite) image.save(buffer, "XPM") xpm_data_bytes = buffer.data() # Convert the bytes to a string so we can replace white and black with transparency xpm_string = xpm_data_bytes.data().decode('utf-8') if self.whiteToTransparentCheckBox.isChecked(): xpm_string = xpm_string.replace("#ffffff", "None") if self.blackToTransparentCheckBox.isChecked(): xpm_string = xpm_string.replace("#000000", "None") #then back to a buffer for loading into the QImage buffer2 = QtCore.QBuffer() buffer2.open(QtCore.QBuffer.ReadWrite) buffer2.write(QtCore.QByteArray(xpm_string.encode())) xpm_data_bytes = buffer2.data() image = QtGui.QImage() image.loadFromData(xpm_data_bytes) iconFolders = self.cte.getIconFolders() if iconFolders: default_folder = iconFolders[0] else: default_folder = FreeCAD.getUserMacroDir(True) file_dialog = QtGui.QFileDialog() file_dialog.setDefaultSuffix("svg") filename, ok = file_dialog.getSaveFileName(None, \ "Export Image, must be saved to a file to get the text into the extracted box",\ default_folder, "SVG Files (*.svg);;All Files (*)") if filename: svg_generator = QtSvg.QSvgGenerator() svg_generator.setFileName(filename) svg_generator.setSize(image.size()) svg_generator.setViewBox(image.rect()) painter = QtGui.QPainter(svg_generator) painter.drawImage(0, 0, image) # Draw the QImage onto the SVG painter.end() with open(filename, 'r') as file: svg_string = file.read() self.cte.form.PlainTextEditExtracted.setPlainText(svg_string) self.cte.form.saveExtractedButton.setEnabled(True) def exportPNG(self): """export current layer as PNG file""" # Convert the pixmap to a 32-bit image with an alpha channel image = QtGui.QImage(self.pixmap.size(), QtGui.QImage.Format_ARGB32) image.fill(QtGui.QColor(0, 0, 0, 0)) # Fill the image with transparent replaceWhite = self.whiteToTransparentCheckBox.isChecked() replaceBlack = self.blackToTransparentCheckBox.isChecked() # Loop through the pixmap and replace white and black pixels with transparent pixels for x in range(self.pixmap.width()): for y in range(self.pixmap.height()): color = self.pixmap.toImage().pixel(x, y) if QtGui.QColor(color) == QtGui.QColor(255, 255, 255) and replaceWhite: # Replace white image.setPixel(x, y, QtGui.QColor(0, 0, 0, 0).rgba()) elif QtGui.QColor(color) == QtGui.QColor(0, 0, 0) and replaceBlack: # Replace black image.setPixel(x, y, QtGui.QColor(0, 0, 0, 0).rgba()) else: image.setPixel(x, y, color) file_dialog = QtGui.QFileDialog() file_dialog.setDefaultSuffix("png") filename, ok = file_dialog.getSaveFileName(None, "Export Image", "", "PNG Files (*.png);;All Files (*)") if filename: image.save(filename, "PNG") def loadBaseButtonClicked(self): """Now Load base button, loads an image file to use as base icon""" firstTab = self.tabWidget.widget(1) parseMod = self.cte.getBool("ParseModFolder",False) w = SystemIconSelector(parseModFolder = parseMod, cte=self.cte, bShowBrowseButton = True, bShowStyleIcons = True) w.setModal(True) w.exec() result = w.icon_text bUseStyle = w.bUseStyle bLoadFromDisk = w.bLoadFromDisk w.deleteLater() if result and bUseStyle: pixmapi = getattr(QtGui.QStyle, result) icon = self.style().standardIcon(pixmapi) firstTab.basePixmap = icon.pixmap(64,64) firstTab.updateImage() firstTab.basePixmapMethod = ("style", pixmapi) return elif result and not bUseStyle: icon = QtGui.QIcon(FreeCADGui.getIcon(result)) firstTab.basePixmap = icon.pixmap(64,64) firstTab.updateImage() firstTab.basePixmapMethod = ("system", result) elif bLoadFromDisk: iconFolders = self.cte.getIconFolders() if iconFolders: default_folder = iconFolders[0] else: default_folder = FreeCAD.getUserMacroDir(True) dlg = QtGui.QFileDialog(self, "Open image file, or cancel to clear out base icon", default_folder, \ "Image Files (*.png *.jpg *.jpeg *.bmp *.gif *.svg *.webp *.xpm *.XPM);;All Files (*)") dlg.setFileMode(QtGui.QFileDialog.ExistingFile) if dlg.exec(): filename = dlg.selectedFiles()[0] firstTab.basePixmap = QtGui.QPixmap(filename) firstTab.updateImage() firstTab.basePixmapMethod = ("file", filename) else: firstTab.basePixmap = None firstTab.updateImage() firstTab.basePixmapMethod = None return else: firstTab.basePixmap = None firstTab.updateImage() firstTab.basePixmapMethod = None return def onBorderColorButtonClicked(self): """handle case where user wants to change the border color""" #the 2 color arguments at the end are to tell the dialog to add as custom colors, the current color and border color #then we call getClr(current color), getClr() makes use of those last 2 arguments to add them as custom colors colorDlg = ColorDlg("Simple Icon Maker", QtGui.QColorDialog.DontUseNativeDialog, self.getColor(), self.getBorderColor()) color = colorDlg.getClr(self.getBorderColor()) colorString = f'rgb({color.red()}, {color.green()}, {color.blue()})' self.borderColorButton.setStyleSheet(f'background-color: {colorString};') self.updateImage() def onColorButtonClicked(self): colorDlg = ColorDlg("Simple Icon Maker", QtGui.QColorDialog.DontUseNativeDialog, self.getColor(), self.getBorderColor()) color = colorDlg.getClr(self.getColor()) colorString = f'rgb({color.red()}, {color.green()}, {color.blue()})' self.colorButton.setStyleSheet(f'background-color: {colorString};') self.updateImage() def saveImage(self): """converts image to an xpm string, puts into the extracted plain text edit""" image = self.pixmap.toImage() buffer = QtCore.QBuffer() buffer.open(QtCore.QBuffer.ReadWrite) image.save(buffer, "XPM") # Get the XPM data as bytes xpm_data_bytes = buffer.data() # Convert the bytes to a string xpm_data_string = xpm_data_bytes.data().decode('utf-8') if self.whiteToTransparentCheckBox.isChecked(): xpm_data_string = xpm_data_string.replace("#ffffff","None").replace("#FFFFFF","None") if self.blackToTransparentCheckBox.isChecked(): xpm_data_string = xpm_data_string.replace("#000000","None") self.cte.form.PlainTextEditExtracted.setPlainText(xpm_data_string) self.cte.form.saveExtractedButton.setEnabled(True) #FreeCAD.Console.PrintMessage(f"xpm = {xpm_data_string}\n") def eventFilter(self, obj, event): """installed to image label and to tab widget for the image label we use this for scaling and dragging the curent layer. For the tab widget we detect when it is being closed and delete all the layer dialogs in that case """ if hasattr(obj, "iconLabel") and obj == self.iconLabel and event.type() == event.Wheel: modifiers = QtGui.QApplication.keyboardModifiers() if modifiers == QtCore.Qt.ControlModifier: zoom = True #change zoom else: zoom = False #change scale # Adjust the scale or zoom factor based on the mouse wheel angle = event.angleDelta().y() scale_factor = 1.1 if angle > 0 else 1 / 1.1 if not zoom: scale = self.scaleSpinBox.value() scale *= scale_factor self.scaleSpinBox.setValue(scale) else: if scale_factor > 1: self.zoomFactor += 1 else: self.zoomFactor -= 1 if self.zoomFactor <= 1: self.zoomFactor = 1 self.updateImage() self.statusBox.setText(f"Zoom: {self.zoomFactor} (default = 4)") return super().eventFilter(obj, event) if obj == self.iconLabel: if event.type() == event.MouseButtonPress: if event.button() == QtCore.Qt.LeftButton: self.dragging = True self.start_pos = event.pos() self.iconLabel.setCursor(QtGui.QCursor(QtCore.Qt.ClosedHandCursor)) #return True # Consume the event return super().eventFilter(obj, event) elif event.button() == QtCore.Qt.RightButton: modifiers = QtGui.QApplication.keyboardModifiers() # Get the size of the image image_width = self.pixmap.width() #always 64,64 image_height = self.pixmap.height() # Get the size of the QLabel label_width = self.iconLabel.width() // self.zoomFactor label_height = self.iconLabel.height() //self.zoomFactor # Calculate the margin x_margin = (label_width - image_width) // 2 y_margin = (label_height - image_height) // 2 # Calculate the mouse coordinates in the image's coordinate system posX = event.pos().x() //self.zoomFactor - x_margin posY = event.pos().y() //self.zoomFactor - y_margin #self.lastClicks = [(posX,posY)] + self.lastClicks #[0] is most recent, [1] was previous click if not modifiers == QtCore.Qt.ShiftModifier: self.addAClick((posX,posY)) self.statusBox.setText(f"Added new point: ({posX},{posY}). Shift + Right-click to select\n"+\ "an existing point.") self.pointsBox.showPoints() else: pts = self.lastClicks pt = (posX,posY) print(f"pt = {pt}") if pt in pts: self.pointsBox.select(pts.index(pt)) self.statusBox.setText(f"({posX},{posY}) selected at row: {pts.index(pt)}") else: self.statusBox.setText(f"({posX},{posY}) not found in points list.") self.iconLabel.setToolTip("") #gets annoying and in the way at times return super().eventFilter(obj, event) elif event.type() == event.MouseMove: if self.dragging and self.start_pos is not None: delta = event.pos() - self.start_pos self.xAdjustSpinBox.setValue(self.xAdjustSpinBox.value() + delta.x()) self.yAdjustSpinBox.setValue(self.yAdjustSpinBox.value() + delta.y()) self.start_pos = event.pos() elif event.type() == event.MouseButtonRelease: if event.button() == QtCore.Qt.LeftButton: self.dragging = False self.start_pos = None self.iconLabel.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) return super().eventFilter(obj, event) # Consume the event if obj == self.tabWidget and event.type() == QtCore.QEvent.Type.Close: if not self.parentDlg: #this is layer 1 self.closeAll() return True # elif obj == self.tabWidget and event.type() == QtCore.QEvent.Type.LayoutRequest: # #print(f"event.Type = {event.type()}") # #user changed tabs # self.onFlashButtonClicked() # return True return super().eventFilter(obj, event) def polygon_to_polyline(self, polygon_str): """ these are regular polygons polygon_str should be of the form: polygon(centerX, centerY, numer of sides, circumradius, angle = 0) the angle is option, if left out we do 0 degrees Here, we check if this is a valid polygon string, and if so we convert it to a polyline string and return this new string, since we already had good working code to handle polyline input, else return None """ # Extract polygon parameters from the input string polygon_str = polygon_str.replace(" ", "").lower() match = re.search(r"polygon\((\d+),(\d+),(\d+),(\d+)(?:,(\d+))?\)", polygon_str) if match: center_x = int(match.group(1)) center_y = int(match.group(2)) num_sides = int(match.group(3)) circumradius = int(round(float(match.group(4)) * self.scaleSpinBox.value())) #angle is optional argument, defaulting to 0 if left out angle = int(match.group(5)) if match.group(5) is not None else 0 angleAdj = self.angleAdjustSpinBox.value() # Calculate the vertices of the polygon vertices = [] for i in range(num_sides): theta = 2 * np.pi * i / num_sides + np.radians(angle + angleAdj) x = center_x + circumradius * np.cos(theta) y = center_y + circumradius * np.sin(theta) vertices.append((x, y)) # Convert vertices into a polyline string polyline_str = "polyline(" for vertex in vertices: x, y = int(round(vertex[0])), int(round(vertex[1])) polyline_str += f"{x},{y}," if vertices[0] != vertices[-1]: # If not closed, add the first vertex again to close the polyline x, y = int(round(vertices[0][0])), int(round(vertices[0][1])) polyline_str += f"{x},{y})" else: polyline_str = polyline_str[:-1] + ")" return polyline_str else: return None def floodfillAuto_to_floodfill(self, text): """convenience function for user to use the last click position to determine where to seed the floodfill() function""" floodfillAutoPattern = r"floodfill\((\d*)\)" floodfillAutoMatch = re.search(floodfillAutoPattern, text.lower().replace(" ","")) if floodfillAutoMatch: if floodfillAutoMatch.group(1): tol = int(floodfillAutoMatch.group(1)) else: tol = 20 x, y = self.lastClicks[0] if self.lastClicks else (32,32) return f"floodfill({x},{y},{tol})" return None def lineAuto_to_line(self,text): """convenience function for user to use the last click positions to determine how to fill out the line() function""" if text.lower().replace(" ","").startswith("line()") and len(self.lastClicks) >= 2: x1,y1 = self.lastClicks[0] x2,y2 = self.lastClicks[1] return f"line({x2},{y2},{x1},{y1})" else: return None return None def find_circle_from_points(self, point1, point2, point3): """find the circle from 3 given points on the perimeter""" def dist(p1,p2): x1,y1 = p1 x2,y2 = p2 return math.sqrt((x2-x1)**2 + (y2-y1)**2) def midpoint(A, B): """ midpoint(A, B) A,B are vectors, return midpoint""" mid = FreeCAD.Base.Vector() mid.x = (A.x + B.x)/2.0 mid.y = (A.y + B.y)/2.0 mid.z = (A.z + B.z)/2.0 return mid def outerradius(a, b, c): """ helper for circumcenter()""" return (a*b*c) / (4*sss_area(a,b,c)) def sss_area(a,b,c): #semiperimeter """ helper for circumcenter()""" sp = (a+b+c)*0.5; return math.sqrt(sp*(sp-a)*(sp-b)*(sp-c)) def circumcenter(A,B,C): """ circumcenter(A, B, C) return the circumcenter of triangle A,B,C the circumcenter is the circle that passes through all 3 of the triangle's vertices expects 3D coordinates """ #convert 2d to 3d A = FreeCAD.Vector(A[0],A[1],0) B = FreeCAD.Vector(B[0],B[1],0) C = FreeCAD.Vector(C[0],C[1],0) z = C.sub(B).cross(A.sub(B)) a = A.sub(B).Length b = B.sub(C).Length c = C.sub(A).Length r = ((b*b + c*c - a*a)/(2*b*c)) * outerradius(a,b,c) ret = A.sub(B).cross(z).normalize().multiply(r).add(midpoint(A,B)) #return as 2D return (ret[0],ret[1]) x1, y1 = point1 x2, y2 = point2 x3, y3 = point3 center = circumcenter(point1,point2,point3) radius = dist(center,point1) return center[0], center[1], radius def find_arc_from_points(self, point1, point2, point3): x1, y1 = point1 x2, y2 = point2 x3, y3 = point3 circle = self.find_circle_from_points(point1, point2, point3) if circle is None: return None center_x, center_y, radius = circle # Calculate the angles relative to the center angle1 = math.atan2(y1 - center_y, x1 - center_x) angle2 = math.atan2(y2 - center_y, x2 - center_x) angle3 = math.atan2(y3 - center_y, x3 - center_x) # Ensure angles are between 0 and 2*pi def normalize_angle(angle): while angle < 0: angle += 2 * math.pi while angle >= 2 * math.pi: angle -= 2 * math.pi return angle angle1 = normalize_angle(angle1) angle2 = normalize_angle(angle2) angle3 = normalize_angle(angle3) # Sort the angles to determine start and end angles angles = [angle1, angle2, angle3] #angles.sort() start_angle = math.degrees(angles[0]) end_angle = math.degrees(angles[2]) return center_x, center_y, radius, start_angle, end_angle def makeImage(self, text, scale, thickness, color, bgColor, borderColor, borderThickness): """text is text to be used, scale is a float, thickness is an integer for how thick in pixels the font will be, color is foreground color, bgColor is background color. Both colors are tuples in the form of (0-255, 0-255, 0-255) RGB. Returns a QPixmap or None""" #basePixmap is either the layer beneath this one (and all previous layers) #or if this is the first layer, then it is an image the user wants to use #as a starting point for the icon, if None, then we create a blank image with white background if self.basePixmap: imageBase = self.basePixmap.toImage() # Convert the QImage to a numpy array, because cv2 needs it in that format width, height = imageBase.width(), imageBase.height() format = imageBase.format() if format == 3: # RGB format (3 bytes per pixel) image_array = np.frombuffer(imageBase.constBits(), dtype=np.uint8).reshape((height, width, 3)) elif format == 4 or format == QtGui.QImage.Format.Format_ARGB32_Premultiplied: # RGBA format (4 bytes per pixel) image_array = np.frombuffer(imageBase.constBits(), dtype=np.uint8).reshape((height, width, 4)) else: #unknown format, so ignore this base image and start with a new blank image instead self.cte.showMsg(f"Unsupported bits per pixel in base icon: {format}, ignoring base icon","error") image_array = None image = np.zeros((64, 64, 3), dtype=np.uint8) image[:, :] = bgColor # Set the entire image to background color if image_array is not None: if format == 4 or format == QtGui.QImage.Format.Format_ARGB32_Premultiplied: # ARGB or ARGB_Premultiplied to BGR image = cv2.cvtColor(image_array, cv2.COLOR_RGBA2BGR) elif format == 3: # RGB to BGR image = cv2.cvtColor(image_array, cv2.COLOR_RGB2BGR) image = cv2.resize(image, (64, 64)) else: #no base icon, so start with new, blank image. #Could put this in another function since there is some code #duplication, but it's only 2 lines... image = np.zeros((64, 64, 3), dtype=np.uint8) image[:, :] = bgColor # Set the entire image to background color #check if text is a valid polygon() function call first #if so, we translate it into a polyine() call and just use #polyline() to handle it from there polygonString = self.polygon_to_polyline(text) if polygonString: text = polygonString #we do the polygon with existing polyline code #print(f"polygonString = {polygonString}") #this allows the user to click on the image, and then enter simply "floodfill()" #to have a new string generated in the form of "floodfill(clickX, clickY, defaultTolerance)" floodfillString = self.floodfillAuto_to_floodfill(text) if floodfillString: self.textEdit.setText(floodfillString) text = floodfillString if self.xAdjustSpinBox.value() or self.yAdjustSpinBox.value(): self.cte.showMsg("Set X adjust and Y adjust to 0 or else the seed point might not be where you expect it to be","warning") lineString = self.lineAuto_to_line(text) if lineString: text = lineString self.textEdit.setText(lineString) if self.xAdjustSpinBox.value() or self.yAdjustSpinBox.value(): self.cte.showMsg("Set X adjust and Y adjust to 0 or else the line might not be where you expect it to be","warning") if text.startswith("rectangle()"): #allow to use rectangle() to get the last 2 clicks used in rectangle function if len(self.lastClicks) >= 2: (x1,y1) = self.lastClicks[0] (x2,y2) = self.lastClicks[1] rectStr = f"rectangle({x1},{y1}, {x2},{y2})" text = rectStr.replace(" ","") self.textEdit.setText(rectStr) else: self.cte.showMsg("Add 2 construction mode points first, and then make this rectangle.") if text.startswith("circle3points()"): if len(self.lastClicks) >= 3: p1 = self.lastClicks[0] p2 = self.lastClicks[1] p3 = self.lastClicks[2] centerX,centerY,radius = self.find_circle_from_points(p1,p2,p3) x = int(round(centerX)) y = int(round(centerY)) r = int(round(radius)) text = f"circle({x},{y},{r})" self.textEdit.setText(text) #line(x1, y1, x2, y2) linePattern = r"^line\((\d+),(\d+),(\d+),(\d+)\)" lineMatch = re.search(linePattern, text.lower().replace(" ","")) #rectangle(top left x, top left y, bottom right x, bottom right y) rectanglePattern = r"^rectangle\((\d+),(\d+),(\d+),(\d+)\)" rectangleMatch = re.search(rectanglePattern, text.lower().replace(" ","")) #circle(x,y,radius) circlePattern = r"^circle\((\d+),(\d+),(\d+)\)" circleMatch = re.search(circlePattern, text.lower().replace(" ","")) #polyline(x1,y1,x2,y2,x3,y3,...) #end with x1,y1 if you want a closed polygon polylinePattern = r"^polyline\(" polylineMatch = re.search(polylinePattern, text.lower().replace(" ","")) #ellipse(x, y, major radius,minor radius,angle=0) ellipsePattern = r"^ellipse\((\d+),(\d+),(\d+),(\d+)(?:,(\d+))?\)" ellipsePattern7 = r"^ellipse\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)" ellipseMatch = re.search(ellipsePattern, text.lower().replace(" ","")) ellipseMatch7 = re.search(ellipsePattern7, text.lower().replace(" ","")) floodfillPattern = r"^floodfill\((\d+),(\d+),(\d+)\)" floodfillMatch = re.search(floodfillPattern, text.lower().replace(" ","")) floodfillAllPattern = r"^floodfillall\((\d+)\)" floodfillAllMatch = re.search(floodfillAllPattern, text.lower().replace(" ","")) #everything can be dragged xAdj = self.xAdjustSpinBox.value() yAdj = self.yAdjustSpinBox.value() #only text and things with a radius property are scaled scaleAdj = self.scaleSpinBox.value() #only objects with angle property get adjusted with this angleAdj = self.angleAdjustSpinBox.value() lineType = self.lineTypes[self.lineTypeBox.currentText()] if lineMatch: x1 = int(lineMatch.group(1)) y1 = int(lineMatch.group(2)) x2 = int(lineMatch.group(3)) y2 = int(lineMatch.group(4)) if borderThickness != 0: cv2.line(image, (x1 + xAdj ,y1 + yAdj), (x2 + xAdj,y2 + yAdj), borderColor, \ thickness + borderThickness, lineType = lineType) cv2.line(image, (x1 + xAdj ,y1 + yAdj), (x2 + xAdj,y2 + yAdj), color, thickness, lineType = lineType) elif rectangleMatch: x1 = int(rectangleMatch.group(1)) y1 = int(rectangleMatch.group(2)) x2 = int(rectangleMatch.group(3)) y2 = int(rectangleMatch.group(4)) if borderThickness != 0: cv2.rectangle(image, (x1 + xAdj ,y1 + yAdj), (x2 + xAdj,y2 + yAdj), borderColor, \ thickness + borderThickness, lineType = lineType) cv2.rectangle(image, (x1 + xAdj ,y1 + yAdj), (x2 + xAdj,y2 + yAdj), color, thickness, lineType = lineType) elif text.lower().replace(" ","").startswith("rectangles()"): pairs = [self.lastClicks[i:i+2] for i in range(0, len(self.lastClicks), 2)] if len(pairs) > 1 and len(pairs[-1]) == 1: # If the last pair has only one element, convert it to a list. pairs[-2].extend(pairs[-1]) pairs.pop() for isBorder in [True,False]: if isBorder and borderThickness == 0: continue for pair in pairs: if len(pair) == 2: x1,y1 = pair[0] x2,y2 = pair[1] else: continue #ignore any extra last single points if isBorder: cv2.rectangle(image, (x1 + xAdj ,y1 + yAdj), (x2 + xAdj,y2 + yAdj), borderColor, \ thickness + borderThickness, lineType = lineType) else: cv2.rectangle(image, (x1 + xAdj ,y1 + yAdj), (x2 + xAdj,y2 + yAdj), color, thickness, lineType = lineType) elif circleMatch: x = int(circleMatch.group(1)) y = int(circleMatch.group(2)) r = int(round(float(circleMatch.group(3)) * scaleAdj)) if borderThickness != 0: cv2.circle(image, (x + xAdj ,y + yAdj), r, borderColor, thickness + borderThickness, lineType = lineType) cv2.circle(image, (x + xAdj ,y + yAdj), r, color, thickness, lineType = lineType) elif text.startswith("arc3points()") or text.startswith("arc3points2()"): if len(self.lastClicks) >= 3: p1 = self.lastClicks[0] p2 = self.lastClicks[1] p3 = self.lastClicks[2] centerX,centerY,radius,startAngle,endAngle = self.find_arc_from_points(p1,p2,p3) center_x = int(round(centerX)) center_y = int(round(centerY)) r = int(round(radius)) startAngle = int(round(startAngle)) endAngle = int(round(endAngle)) angle = int(0) if startAngle > endAngle: tmp = startAngle startAngle = endAngle endAngle = tmp arc_points1 = [] arc_points2 = [] angles1 = [] angles2 = [] for alpha in range(startAngle, endAngle+1): x = int(centerX + (r * scaleAdj) * math.cos(math.radians(alpha)) + xAdj) y = int(centerY + (r * scaleAdj) * math.sin(math.radians(alpha)) + yAdj) arc_points1.append((x, y)) angles1.append(alpha) for alpha in range(endAngle, 361): x = int(centerX + (r * scaleAdj) * math.cos(math.radians(alpha)) + xAdj) y = int(centerY + (r * scaleAdj) * math.sin(math.radians(alpha)) + yAdj) arc_points2.append((x, y)) angles2.append(alpha) for alpha in range(0, startAngle): x = int(centerX + (r * scaleAdj) * math.cos(math.radians(alpha)) + xAdj) y = int(centerY + (r * scaleAdj) * math.sin(math.radians(alpha)) + yAdj) arc_points2.append((x, y)) angles2.append(alpha) if "arc3points2" in text: arc_points = arc_points2 else: arc_points = arc_points1 if borderThickness != 0: x1,y1 = arc_points[0] for x2,y2 in arc_points[1:]: cv2.line(image, (x1 + xAdj ,y1 + yAdj), (x2 + xAdj,y2 + yAdj), borderColor, \ thickness + borderThickness, lineType = lineType) x1,y1 = x2,y2 x1,y1 = arc_points[0] for x2,y2 in arc_points[1:]: cv2.line(image, (x1 + xAdj ,y1 + yAdj), (x2 + xAdj,y2 + yAdj), color, \ thickness, lineType = lineType) x1,y1 = x2,y2 elif text.startswith("putpoints()"): if self.lastClicks: if borderThickness != 0: for x,y in self.lastClicks: cv2.line(image, (x + xAdj ,y + yAdj), (x + xAdj,y + yAdj), borderColor, \ thickness + borderThickness, lineType = lineType) for x,y in self.lastClicks: cv2.line(image, (x + xAdj ,y + yAdj), (x + xAdj,y + yAdj), color, thickness, lineType = lineType) elif floodfillMatch: x = int(floodfillMatch.group(1)) y = int(floodfillMatch.group(2)) tol = int(floodfillMatch.group(3)) self.seed_x = x + xAdj self.seed_y = y + yAdj if self.seed_x < 0: self.seed_x = 0 elif self.seed_x > 63: self.seed_x = 63 if self.seed_y < 0: self.seed_y = 0 elif self.seed_y > 63: self.seed_y = 63 self.showPoints([(self.seed_x,self.seed_y)],(128,128,128)) cv2.floodFill(image, None, (self.seed_x, self.seed_y), color, loDiff=(tol, tol, tol), upDiff = (tol, tol, tol)) elif floodfillAllMatch: tol = int(floodfillAllMatch.group(1)) pts = self.lastClicks for pt in pts: cv2.floodFill(image, None, pt , color, loDiff=(tol, tol, tol), upDiff = (tol, tol, tol)) elif text.startswith("SketchImported:"): if thickness <= 0: thickness = 1 self.statusBox.setText("Thickness must be at least 1 for sketch imports") modes = ["border", "foreground"] for mode in modes: if mode == "border" and borderThickness == 0: continue strings = text.split("\n") for txt in strings[1:]: if txt.startswith("line("): pattern = r"^line\((-?\d+),(-?\d+),(-?\d+),(-?\d+)\)" match = re.search(pattern,txt) if match: x1 = int(match.group(1)) y1 = int(match.group(2)) x2 = int(match.group(3)) y2 = int(match.group(4)) if mode == "border": cv2.line(image, (x1 + xAdj ,y1 + yAdj), (x2 + xAdj,y2 + yAdj), borderColor, \ thickness + borderThickness, lineType = lineType) else: cv2.line(image, (x1 + xAdj ,y1 + yAdj), (x2 + xAdj,y2 + yAdj), color, \ thickness, lineType = lineType) else: raise Exception(f"Unable to match line for {txt}") elif txt.startswith("circle("): pattern = r"^circle\((-?\d+),(-?\d+),(\d+)\)" match = re.search(pattern,txt) if match: x = int(match.group(1)) y = int(match.group(2)) r = int(match.group(3)) if mode == "border": cv2.circle(image, (x + xAdj ,y + yAdj), r, borderColor, \ thickness + borderThickness, lineType = lineType) else: cv2.circle(image, (x + xAdj ,y + yAdj), r, color, thickness, lineType = lineType) else: raise StandardException(f"invalid line: {txt}") elif text.lower().startswith("polyline()"): #use the last NNN clicks to do the polyline if self.lastClicks: #print(f"points = {points}") points = [(x + xAdj, y + yAdj) for x,y in self.lastClicks] is_closed = points[-1] == points[0] if is_closed: if borderThickness > 0: #do the border first, strip last point since we're using isClosed = True cv2.polylines(image, [np.array(points[:-1])], isClosed=True, color=borderColor, \ thickness = thickness + borderThickness, lineType = lineType) if thickness == -1: #fill the polygon if thickness = -1 cv2.fillPoly(image, [np.array(points)], color) elif thickness > 0: cv2.polylines(image, [np.array(points[:-1])], isClosed=True, color=color, \ thickness=thickness, lineType = lineType) else: #not closed, so no fill option and we don't strip the last point from points list if borderThickness > 0: #do the border first cv2.polylines(image, [np.array(points)], isClosed=False, color=borderColor, \ thickness = thickness + borderThickness, lineType = lineType) if thickness > 0: cv2.polylines(image, [np.array(points)], isClosed=False, color=color, \ thickness=thickness, lineType = lineType) elif polylineMatch: #this was a bit tricky, hence the reason for using it for polygon(), too #the unknown argument count nature of the beast made coming up with a #working regular expression non-trivial (for me, anyway), so simple text #parsing is used instead points = [] input_string = text.lower().replace(" ","") start_index = input_string.find("polyline(") # Remove the "polyline(" part of the string polyline_data = input_string[start_index + len("polyline("):] # Check if the string ends with a closing ')' character if polyline_data.endswith(")"): # Remove the closing ')' character polyline_data = polyline_data[:-1] coordinates = polyline_data.split(',') # Ensure there are at least two coordinates to form pairs if len(coordinates) >= 2: #cv2 needs int arguments, it seems try: points = [(int(coordinates[i]), int(coordinates[i + 1])) for i in range(0, len(coordinates), 2)] except Exception as e: print(f"Exception converting {coordinates} to ints: {e}") if points: #print(f"points = {points}") points = [(x + xAdj, y + yAdj) for x,y in points] is_closed = points[-1] == points[0] if is_closed: if borderThickness > 0: #do the border first, strip last point since we're using isClosed = True cv2.polylines(image, [np.array(points[:-1])], isClosed=True, color=borderColor, \ thickness = thickness + borderThickness, lineType = lineType) if thickness == -1: #fill the polygon if thickness = -1 cv2.fillPoly(image, [np.array(points)], color) elif thickness > 0: cv2.polylines(image, [np.array(points[:-1])], isClosed=True, color=color, \ thickness=thickness, lineType = lineType) else: #not closed, so no fill option and we don't strip the last point from points list if borderThickness > 0: #do the border first cv2.polylines(image, [np.array(points)], isClosed=False, color=borderColor, \ thickness = thickness + borderThickness, lineType = lineType) if thickness > 0: cv2.polylines(image, [np.array(points)], isClosed=False, color=color, \ thickness=thickness, lineType = lineType) elif ellipseMatch or ellipseMatch7: if ellipseMatch: center_x = int(ellipseMatch.group(1)) center_y = int(ellipseMatch.group(2)) major_axis = int(round(float(ellipseMatch.group(3)) * scaleAdj)) minor_axis = int(round(float(ellipseMatch.group(4)) * scaleAdj)) #angle is optional argument for ellipseMatch, but not for ellipseMatch7, defaulting to 0 if left out if ellipseMatch.group(5): angle = int(ellipseMatch.group(5)) if ellipseMatch.group(5) is not None else 0 startAngle = 0 endAngle = 360 else: #ellipseMatch7 center_x = int(ellipseMatch7.group(1)) center_y = int(ellipseMatch7.group(2)) major_axis = int(round(float(ellipseMatch7.group(3)) * scaleAdj)) minor_axis = int(round(float(ellipseMatch7.group(4)) * scaleAdj)) #angle is optional argument for ellipseMatch, but not for ellipseMatch7, defaulting to 0 if left ou angle = int(ellipseMatch7.group(5)) startAngle = int(ellipseMatch7.group(6)) endAngle = int(ellipseMatch7.group(7)) cv2.ellipse(image, (center_x + xAdj, center_y + yAdj), (major_axis, minor_axis), angle + angleAdj, \ color = borderColor, thickness = borderThickness + thickness, startAngle = startAngle, \ endAngle = endAngle, lineType = lineType) cv2.ellipse(image, (center_x + xAdj, center_y + yAdj), (major_axis, minor_axis), angle + angleAdj, \ color=color, thickness=thickness, startAngle = startAngle, endAngle = endAngle, \ lineType = lineType) elif text.startswith("ellipse5points()"): if len(self.lastClicks) >= 5: points = np.array([self.lastClicks[:5]]) points = points + np.array([xAdj,yAdj]) ellipse =cv2.fitEllipse(points) print(f"ellipse = {ellipse}") cv2.ellipse(image, ellipse, color=borderColor, thickness=thickness+borderThickness, lineType=lineType) cv2.ellipse(image, ellipse, color=color, thickness=thickness, lineType=lineType) else: #interpret as text font = self.fonts[self.fontBox.currentText()] font_scale = scale font_color = color font_thickness = thickness if thickness >= 1 else 1 # Get the size of the text text_size = cv2.getTextSize(text, font, font_scale, font_thickness)[0] # Calculate the position to center the text x = (image.shape[1] - text_size[0]) // 2 + self.xAdjustSpinBox.value() y = (image.shape[0] + text_size[1]) // 2 + self.yAdjustSpinBox.value() if not "\\n" in text: if borderThickness: cv2.putText(image, text, (x, y), font, font_scale, borderColor, \ font_thickness + borderThickness, lineType=lineType) cv2.putText(image, text, (x, y), font, font_scale, font_color, font_thickness) else: idx = text.find("\\n") text1 = text[:idx] text2 = text[idx+2:] #print(f"text1,text2: {text1},{text2}") if borderThickness: cv2.putText(image, text1, (x, y), font, font_scale, borderColor, \ font_thickness + borderThickness, lineType=lineType) cv2.putText(image, text2, (x, y + int(text_size[1] * 1.25)), font, font_scale, borderColor, \ font_thickness + borderThickness, lineType=lineType) cv2.putText(image, text1, (x, y), font, font_scale, font_color, font_thickness) cv2.putText(image, text2, (x, y + int(text_size[1] *1.25)), font, font_scale, font_color, font_thickness) if text.count("\\n") > 1: self.cte.showMsg("Only 2 lines of text supported at this time, 3rd and later lines ignored.", "warning") if image is not None: #image is a numpy object we need to convert to actual pixmap h, w, ch = image.shape bytes_per_line = ch * w q_image = QtGui.QImage(image.data, w, h, bytes_per_line, QtGui.QImage.Format_RGB888) pixmap = QtGui.QPixmap.fromImage(q_image) return pixmap else: return None def iconize(self,button,icon_theme_name="",system_name=""): """iconize widget, which could be button or checkbox from QStyle theme or system""" if not system_name: pixmapi = getattr(QtGui.QStyle, icon_theme_name) button.setIcon(self.style().standardIcon(pixmapi)) else: icon = FreeCADGui.getIcon(system_name) button.setIcon(icon) # end SimpleIconMaker ############# #try the import here instead of at the top so users who lack this package #can still use the macro, just not the icon maker feature try: import cv2 except: self.showMsg("This feature requires openCV package. Install with pip install opencv-python", "error") return self.iconMakerTabWidget = QtGui.QTabWidget() self.iconMakerTabWidget.setObjectName("IconMakerTabWidget") self.iconMakerTabWidget.setWindowTitle("Simple 64x64 Icon Maker") self.iconMakerDlg = SimpleIconMaker(self, layer=1, tabWidget = self.iconMakerTabWidget) widget = QtGui.QWidget() btn = QtGui.QPushButton() #to get the icon for the tab self.iconize(btn,system_name="list-add") icon = btn.icon() self.iconMakerTabWidget.addTab(widget,"") self.iconMakerTabWidget.setTabIcon(0,icon) self.iconMakerTabWidget.currentChanged.connect(self.iconMakerDlg.onTabWidgetCurrentChanged) self.iconMakerTabWidget.addTab(self.iconMakerDlg,"Layer1") self.iconMakerTabWidget.installEventFilter(self.iconMakerDlg) self.iconMakerTabWidget.show() self.iconMakerTabWidget.setCurrentWidget(self.iconMakerDlg) self.iconMakerDlg.textEdit.selectAll() self.iconMakerDlg.textEdit.setFocus() def removeMacroAction(self): """removes the current macro's action from the system. Does not delete macro file""" tm = self.tm() if not tm: self.showMsg(f"Unable to aquire ToolbarManager object for {self.tbName}:{self.wbName}", "error") return commandName = tm.getCommandName(self.macroName) if tm.removeMacroAction(self.macroName): self.showMsg(f"Successfully removed {self.macroName} ({commandName}) action") else: self.showMsg(f"Failed to remove {self.macroName} ({commandName}) action","error") def makeSystemIconsMenu(self): """make the sytem icons menu, will be used for both the context menu and the main menu""" systemIconsMenu = QtGui.QMenu("System icons") parseModFolderAction = QtGui.QAction("Parse Mod folders", systemIconsMenu, checkable=True) parseModFolder = self.getBool("ParseModFolder",False) parseModFolderAction.setChecked(parseModFolder) toggler = self.makeToggler("ParseModFolder", not parseModFolderAction.isChecked()) parseModFolderAction.toggled.connect(toggler) systemIconsMenu.addAction(parseModFolderAction) manageIconFoldersAction = QtGui.QAction("Manage icon folders", systemIconsMenu) manageIconFoldersAction.triggered.connect(self.manageIconFolders) systemIconsMenu.addAction(manageIconFoldersAction) return systemIconsMenu def autoLoadWorkbench(self): pg = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/General") autoLoads = pg.GetString("BackgroundAutoloadModules","") if self.wbName not in autoLoads: autoLoads += f",{self.wbName}" pg.SetString("BackgroundAutoloadModules",autoLoads) self.showMsg(f"Workbench: {self.wbName} set to automatically load on each restart.") self.showMsg(f"Use Edit menu -> preferences -> workbenches -> uncheck {self.wbName} Autoload checkbox") def createMacroForAction(self): """create a macro for the current non-macro action""" cmd = self.nonMacroName macroText = "" if not cmd.startswith("Std_"): workbenches = [self.form.ComboBoxWorkbench.itemText(i) for i in range(1,self.form.ComboBoxWorkbench.count())] workbenches = ["None - this is a core function"] + workbenches item,ok = QtGui.QInputDialog.getItem(self, "Select workbench", \ "Select the workbench this command is from, or None if it is a core function:",\ workbenches, 0, editable=False) if not item: return currentWB = FreeCADGui.activeWorkbench().name() if item != workbenches[0]: macroText += \ f"currentWB = Gui.activeWorkbench().name()\n" +\ f"Gui.activateWorkbench('{item}')\n" +\ f"Gui.activateWorkbench(currentWB)\n" macroText += \ f"cmd = Gui.Command.get('{cmd}')\n" +\ "cmd.run(0)\n" macroDir = FreeCAD.getUserMacroDir(True) fullpath = os.path.join(macroDir, f"{cmd}.FCMacro") if os.path.exists(fullpath): self.showMsg(f"A file named {fullpath} already exists.", "error") self.form.ComboBoxMacroName.setCurrentText(f"{cmd}.FCMacro") return with open(fullpath,"w") as file: file.write(macroText) self.showMsg(f"A file named {fullpath} has been created in your macros directory.") self.updateMacroNames() self.form.ComboBoxMacroName.setCurrentText(f"{cmd}.FCMacro") self.updateLineEditsFromNonMacroCommand(cmd) self.enableNonMacroButtons(False) def makeToggler(self,name,value): return lambda: self.setBool(name,value) def showMenu(self): menu = QtGui.QMenu("Main menu") # ########## begin settings menu settingsMenu = QtGui.QMenu("Settings") self.iconize(settingsMenu, system_name="preferences-system") menu.addMenu(settingsMenu) # # system icons start systemIconsMenu = self.makeSystemIconsMenu() settingsMenu.addMenu(systemIconsMenu) # # system icons end # ########## end settings menu loadWorkbenchesAction = QtGui.QAction("Load workbenches", self) loadWorkbenchesAction.triggered.connect(self.loadWorkbenches) self.iconize(loadWorkbenchesAction, system_name="AddonManager") loadWorkbenchesAction.setToolTip("By loading all workbenches you can ensure "+\ "any shortcuts you have set will not conflict with any shortcuts in any workbench") menu.addAction(loadWorkbenchesAction) autoLoadWorkbenchAction = QtGui.QAction("Autoload this workbench", menu) pg = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/General") autoLoads = pg.GetString("BackgroundAutoloadModules","") if self.wbName == "Global" or self.wbName in autoLoads: autoLoadWorkbenchAction.setEnabled(False) autoLoadWorkbenchAction.triggered.connect(self.autoLoadWorkbench) menu.addAction(autoLoadWorkbenchAction) removeOrphansAction = QtGui.QAction("Remove orphans", menu) tm = self.tm() if not tm: removeOrphansAction.setEnabled(False) else: removeOrphansAction.setEnabled(tm.getOrphans() != []) removeOrphansAction.triggered.connect(self.removeOrphans) menu.addAction(removeOrphansAction) createMacroForAction = QtGui.QAction("Create macro for this non-macro action", menu) createMacroForAction.triggered.connect(self.createMacroForAction) menu.addAction(createMacroForAction) deleteMacroAction = QtGui.QAction("Delete macro", menu) if not self.macroName: deleteMacroAction.setEnabled(False) deleteMacroAction.triggered.connect(self.deleteMacro) menu.addAction(deleteMacroAction) removeMacroActionAction = QtGui.QAction("Remove macro action", menu) tm = self.tm() #must not be installed and must have action if tm and not tm.isInstalled(self.macroName) and tm.getCommandName(self.macroName): removeMacroActionAction.setEnabled(True) else: removeMacroActionAction.setEnabled(False) removeMacroActionAction.triggered.connect(self.removeMacroAction) menu.addAction(removeMacroActionAction) quitAction = QtGui.QAction("Quit", menu) quitAction.triggered.connect(self.reject) menu.addAction(quitAction) menu.exec(self.form.menuButton.mapToGlobal(QtCore.QPoint())) def manageIconFolders(self): """Manage the folders we use to find additional icons in when using the System icons button""" class IconFolderManager(QtGui.QDialog): def __init__(self): super(IconFolderManager, self).__init__() self.setWindowTitle("Icon Folder Manager") self.layout = QtGui.QGridLayout() self.setLayout(self.layout) self.customPathNames = [] #e.g. CustomPath0, CustomPath1, etc. self.iconFolders = [] self.lineEdits = [] self.minusButtons = [] self.plusButton = None self.bitmapsGroup = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Bitmaps") self.setupUi() def iconize(self,button,icon_theme_name="",system_name=""): """iconize widget, which could be button or checkbox from QStyle theme or system""" if not system_name: pixmapi = getattr(QtGui.QStyle, icon_theme_name) button.setIcon(self.style().standardIcon(pixmapi)) else: icon = FreeCADGui.getIcon(system_name) button.setIcon(icon) def makeWidgets(self): """make the line edit and button widgets""" def makeTrigger(cpn): return lambda : self.onMinusButtonClicked(cpn) self.plusButton = QtGui.QPushButton() self.plusButton.setMaximumWidth(20) self.iconize(self.plusButton,system_name="list-add") self.plusButton.clicked.connect(self.onPlusButtonClicked) for row, folder in enumerate(self.iconFolders): lineEdit = QtGui.QLineEdit() lineEdit.setReadOnly(True) self.lineEdits.append(lineEdit) minusButton = QtGui.QPushButton() self.iconize(minusButton,system_name="list-remove") minusButton.clicked.connect(makeTrigger(self.customPathNames[row])) self.minusButtons.append(minusButton) self.layout.addWidget(lineEdit, row, 0, 1, 5) self.layout.addWidget(minusButton, row, 5) lineEdit.setText(folder) lineEdit.setMinimumWidth(400) minusButton.setContentsMargins(10,0,10,0) lineEdit.setContentsMargins(10,0,10,0) minusButton.setMaximumWidth(20) self.layout.addWidget(self.plusButton, len(self.iconFolders), 5) def clearLayout(self): """clear the layout in preparation for rebuilding it""" excluded = ["qwidget","scrollArea"] while self.layout.count(): item = self.layout.takeAt(0) widget = item.widget() if widget and widget.objectName() not in excluded: widget.deleteLater() self.lineEdits = [] self.minusButtons = [] self.plusButton = None def setupUi(self): """setup the ui, only called once from __init__()""" self.getIconFolders() self.makeWidgets() self.adjustContentMinimumWidth() def onMinusButtonClicked(self, customPathName): """remove this custom path from parameters""" self.removeCustomPathName(customPathName) self.clearLayout() self.getIconFolders() self.makeWidgets() def onPlusButtonClicked(self): """add a new custom path folder to parameters""" newFolder = self.selectFolder() if not newFolder: return self.addCustomPathName(newFolder) self.clearLayout() self.getIconFolders() self.makeWidgets() def selectFolder(self): folderPath = QtGui.QFileDialog.getExistingDirectory(self, "Select an icon folder") if folderPath: return folderPath else: return None def adjustContentMinimumWidth(self): """Adjust the minimum width of the content widget to accommodate line edits""" if not self.lineEdits: return maxLineEditWidth = max(lineEdit.sizeHint().width() for lineEdit in self.lineEdits) content_widget = self.layout.parentWidget() # Get the content widget content_widget.setMinimumWidth(maxLineEditWidth + 100) # Adjust as needed self.layout.setColumnMinimumWidth(0, maxLineEditWidth + 10) def getIconFolders(self): """Gets the current icon folders in parameters""" self.customPathNames = self.bitmapsGroup.GetStrings() self.iconFolders = [self.bitmapsGroup.GetString(cpn) for cpn in self.customPathNames] #print(f"self.iconFolders = {self.iconFolders}") def removeCustomPathName(self, cpn): """remove the custom path name from parameters""" self.bitmapsGroup.RemString(cpn) #print(f"removed string {cpn}") def addCustomPathName(self, folder): """make a new custom path name and set folder as the value""" existing = self.bitmapsGroup.GetStrings() basename = "CustomPath" ii = 0 while f"{basename}{ii}" in existing: ii += 1 if ii>= 1000: raise Exception("too many Custom Paths") newname = f"{basename}{ii}" self.bitmapsGroup.SetString(newname,folder) dlg = IconFolderManager() dlg.exec() dlg.deleteLater() def loadWorkbenches(self): self.showMsg("loading workbenches, might take a minute") workbenches = [self.form.ComboBoxWorkbench.itemText(i) for i in range(1,self.form.ComboBoxWorkbench.count())] curBench = FreeCADGui.activeWorkbench().name() for wkbench in workbenches: FreeCADGui.activateWorkbench(wkbench) self.showMsg(f"Activating {wkbench}...") FreeCADGui.updateGui() FreeCADGui.activateWorkbench(curBench) self.showMsg("Done activating workbenches.") def showContextMenu(self, event): menu = QtGui.QMenu(self) clearAction = QtGui.QAction("Clear messages", self) clearAction.triggered.connect(lambda:self.form.statusLabel.setText("")) menu.addAction(clearAction) menu.exec(self.form.statusLabel.mapToGlobal(event)) #the following are convenience properties for accessing widgets' text @property def macroName(self): return self.form.ComboBoxMacroName.currentText() @macroName.setter def macroName(self,txt): self.form.ComboBoxMacroName.setCurrentText(txt) @property def nonMacroName(self): return self.form.ComboBoxNonMacros.currentText() @nonMacroName.setter def nonMacroName(self,txt): self.form.ComboBoxNonMacros.setCurrentText(txt) @property def wbName(self): return self.form.ComboBoxWorkbench.currentText() @wbName.setter def wbName(self,txt): self.form.ComboBoxWorkbench.setCurrentText(txt) @property def tbName(self): return self.form.ComboBoxCustomToolbar.currentText() @tbName.setter def tbName(self,txt): self.form.ComboBoxCustomToolbar.setCurrentText(txt) def getXIcon(self): b64_icon_string = "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAIAAABvFaqvAAAACXBIWXMAABJ0AAASdAHeZh94AAAAmklEQVQ4jbWVQQ7AIAgEgf//2R6a0EqXhRjqycoyQoGoay2ZWDZC2UCqqqp9z6A3Pw2bkhI2ILWSBQX4HxFWMHmtLHxzFrngiahkfSlvF8sMwZlTRESbSXFKjCgTdQS4aoSVmdIRgQ7kghR0Xv6mQ2b6bURgv3RaxLjZEXXf3woYbSeQbWibFI4ea0iQWvM5uF1cDIb2bI29Ihe+pFo7/ev0twAAAABJRU5ErkJggg==" binary_data = base64.b64decode(b64_icon_string) byte_array = QtCore.QByteArray(binary_data) pixmap = QtGui.QPixmap() pixmap.loadFromData(byte_array) self.XIcon = QtGui.QIcon(pixmap) def isInvalidIcon(self,pixmap2): """compare the pixmap to a known invalid (X) icon""" #this is the base64 text representation of the (X) icon #you get when FreeCAD cannot find an icon file #We do it this way of generating an invalid icon in order #to avoid warning messages pixmap1 = self.XIcon.pixmap(64,64) #if brute force isn't working it just means you're not using enough force for x in range(pixmap1.width()): for y in range(pixmap1.height()): pixel1 = pixmap1.toImage().pixel(x, y) pixel2 = pixmap2.toImage().pixel(x, y) if pixel1 != pixel2: return False return True def handlePixmap(self): """handles changes to the Pixmap field in the dialog""" pixmap = self.form.LineEditPixmap.text() icon = QtGui.QIcon(pixmap) icon2 = QtGui.QIcon(FreeCADGui.getIcon(pixmap)) self.currentIcon = None if not icon.isNull() and not icon.pixmap(64,64).isNull(): #self.showMsg(f"valid icon from: {pixmap}") self.form.iconLabel.setPixmap(icon.pixmap(64,64)) self.currentIcon = icon elif not icon2.isNull() and not icon2.pixmap(64,64).isNull(): self.form.iconLabel.setPixmap(icon2.pixmap(64,64)) self.currentIcon = icon2 if self.isInvalidIcon(icon2.pixmap(64,64)): self.showMsg(f"icon from '{pixmap}' is invalid","warning") else: pass #self.showMsg(f"valid icon from theme: '{pixmap}'") else: self.form.LineEditPixmap.setPlaceholderText("(no icon)") #self.form.iconLabel.setPixmap(self.icon.pixmap(64,64)) self.form.iconLabel.setPixmap(self.XIcon.pixmap(64,64)) if pixmap: self.showMsg(f"Unable to make valid icon from {pixmap}","warning") def onActiveGlobalButtonClicked(self): """toggles workbench combo box between Global workbench and currently active workbench""" cb = self.form.ComboBoxWorkbench cbCurrent = cb.currentText() activeWB = FreeCADGui.activeWorkbench().name() if cbCurrent != activeWB and cbCurrent != "Global": cb.setCurrentText("Global") self.updateUi() self.showMsg(f"Switched to {activeWB}, click again to switch to Global","normal") return elif cbCurrent == "Global": cb.setCurrentText(activeWB) self.updateUi() self.showMsg(f"Switched to {activeWB}, click again to switch to Global","normal") return elif cbCurrent == activeWB: cb.setCurrentText("Global") self.updateUi() self.showMsg(f"Switched to Global workbench, click again to switch to {activeWB}","normal") return def setupWorkbenchBox(self): """adds 'Global' and all installed workbenches to workbench combo box""" wbs = FreeCADGui.listWorkbenches() wbNames = sorted([k for k,v in wbs.items() if not k=="NoneWorkbench"]) wbNames[0] = "Global" self.form.ComboBoxWorkbench.addItems(wbNames) self.form.ComboBoxWorkbench.setCurrentText("Global") def onComboBoxWorkbenchCurrentIndexChanged(self): """main thing to do here is to update the toolbar combo box, the rest will update based on this update""" if self.wbName: txt = "Toolbar will appear in all workbenches" if self.wbName == "Global"\ else f"Toolbar will only appear in {self.wbName}" self.form.ComboBoxWorkbench.setToolTip(txt) self.updateCustomToolbarComboBox() def onComboBoxInstalledCurrentIndexChanged(self): """The user has changed the current text of the installed macros combo box so we select that macro in the macro names combo box, by calling the select button handler. We can also get here when the Installed box is getting filled. If it's not empty we enable the Select installed button, else it gets disabled.""" cb = self.form.ComboBoxInstalled if cb.currentText(): self.form.selectInstalledButton.setEnabled(True) self.onSelectInstalledButtonClicked() else: self.form.selectInstalledButton.setEnabled(False) def onSelectMacroButtonClicked(self): current = self.form.ComboBoxMacroName.currentIndex() self.form.ComboBoxMacroName.setCurrentIndex(current - 1) self.form.ComboBoxMacroName.setCurrentIndex(current) def onSelectNonMacroButtonClicked(self): current = self.form.ComboBoxNonMacros.currentIndex() self.form.ComboBoxNonMacros.setCurrentIndex(current - 1) self.form.ComboBoxNonMacros.setCurrentIndex(current) def onSelectInstalledButtonClicked(self): self.updateVariables() cbMacroName = self.form.ComboBoxMacroName cbNonMacros = self.form.ComboBoxNonMacros installedName = self.form.ComboBoxInstalled.currentText() if installedName: idx = cbMacroName.findText(installedName) if idx == -1 and cbMacroName.count() != 0: idx = cbNonMacros.findText(installedName) if idx == -1 and cbNonMacros.count() != 0: if self.form.LineEditFilter.text(): self.showMsg(f"{installedName} is filtered out","error") return else: #non-macro cbNonMacros.setCurrentText(installedName) self.enableNonMacroButtons(True) else: #macro so disable nonmacrobuttons and enable macro buttons self.enableNonMacroButtons(False) cbMacroName.setCurrentText(installedName) def enableNonMacroButtons(self, bEnable): """Either we enable the macro buttons or the non-macro buttons""" self.form.addNonMacroButton.setEnabled(bEnable) self.form.removeNonMacroButton.setEnabled(bEnable) self.form.addMacroToToolbarButton.setEnabled(not bEnable) self.form.removeMacroFromToolbarButton.setEnabled(not bEnable) self.form.LineEditMenuText.setEnabled(not bEnable) self.form.LineEditToolTip.setEnabled(not bEnable) self.form.LineEditStatusText.setEnabled(not bEnable) self.form.LineEditWhatsThis.setEnabled(not bEnable) #self.form.LineEditShortcut.setEnabled(not bEnable) self.form.LineEditPixmap.setEnabled(not bEnable) self.form.systemIconButton.setEnabled(not bEnable) self.form.selectIconFileButton.setEnabled(not bEnable) self.form.fromMacroButton.setEnabled(not bEnable) #self.form.makeIconButton.setEnabled(not bEnable) def updateLineEditsFromNonMacroCommand(self,cmdName): """fills the line edits with the info for the non-macro command""" cmd = FreeCADGui.Command.get(cmdName) if not cmd: return info = cmd.getInfo() self.form.LineEditMenuText.setText(info["menuText"]) self.form.LineEditToolTip.setText(info["toolTip"]) self.form.LineEditWhatsThis.setText(info["whatsThis"]) self.form.LineEditStatusText.setText(info["statusTip"]) self.form.LineEditShortcut.setKeySequence(QtGui.QKeySequence(info["shortcut"])) self.form.LineEditPixmap.setText(info["pixmap"]) def removeOrphans(self): tm = self.tm() if tm: tm.removeOrphans() self.updateInstalled() def updateInstalled(self): """update the installed macros combo box""" tm = self.tm() im = [] orphans = [] if tm: im = tm.getInstalledMacros() + tm.getInstalledNonMacros() orphans = tm.getOrphans() cbInstalled = self.form.ComboBoxInstalled cbInstalled.clear() self.form.ComboBoxInstalled.addItems(im) if cbInstalled.currentText(): self.form.selectInstalledButton.setEnabled(True) else: self.form.selectInstalledButton.setEnabled(False) if orphans: self.showMsg(f"Orphans found: {orphans}. You can remove them from the menu button.", "warning") def onComboBoxCustomToolbarCurrentIndexChanged(self): tm = self.tm() if tm: self.form.ComboBoxCustomToolbar.setToolTip(f"{self.tbName} real name: {tm.RealName}") self.updateInstalled() self.updateUi() def onActiveCheckBoxClicked(self): tm = self.tm() if tm: qt = tm.QToolbar if not qt: self.showMsg(f"Can't get QToolbar for toolbar {self.tbName}: workbench {self.wbName}","warning") return cb = self.form.activeCheckBox qt.setVisible(cb.isChecked()) tm.Active = cb.isChecked() icon_name = "Invisible" if not cb.isChecked() else "dagViewVisible" self.iconize(self.form.activeCheckBox, system_name=icon_name) def updateCustomToolbarComboBox(self): """called by setupUi() and any time a toolbar is added or removed or renamed""" customToolbars = ToolbarManager.getCustomToolbarNames(self.wbName) self.form.ComboBoxCustomToolbar.clear() self.form.ComboBoxCustomToolbar.addItems(customToolbars) self.updateInstalled() def updateMacroNames(self): """updates the macro names in the macro name combo box""" macroDir = FreeCAD.getUserMacroDir(True) dirEntries = os.scandir(macroDir) filenames = [de.name for de in dirEntries if de.is_file() and \ bool(".py" in de.name or ".FCMacro" in de.name \ or ".fcmacro" in de.name)] #filter the filenames based on the content of the filter line edit filterText = self.form.LineEditFilter.text() filteredNames = self.filterFileNames(filenames, filterText) self.form.ComboBoxMacroName.clear() self.form.ComboBoxMacroName.addItems(filteredNames) self.form.ComboBoxMacroName.setCurrentText(self.macroName) def updateNonMacroNames(self): commands = FreeCADGui.Command.listAll() nonMacroCommands = [cmd for cmd in commands if not "Std_Macro" in cmd] filterText = self.form.LineEditFilter.text() filteredNames = self.filterFileNames(nonMacroCommands, filterText) self.form.ComboBoxNonMacros.clear() self.form.ComboBoxNonMacros.addItems(filteredNames) self.form.ComboBoxNonMacros.setCurrentText(self.nonMacroName) def filterFileNames(self, filenames, filter): filteredFilenames = [] try: # Try to compile the filter_text as a regular expression regex = re.compile(filter, re.IGNORECASE) for filename in filenames: if regex.search(filename): filteredFilenames.append(filename) except re.error: # If the regular expression is not valid, use a simple contains test for filename in filenames: if filter.lower() in filename.lower(): filteredFilenames.append(filename) return filteredFilenames def setupUi(self): """initial ui setup""" self.updateCustomToolbarComboBox() #self.form.gridLayout.addWidget(self.form.LineEditShortcut,11,2) self.updateMacroNames() self.updateNonMacroNames() self.iconize(self.form.selectIconFileButton, "SP_DialogOpenButton") self.iconize(self.form.openHyperlinkButton, "SP_DriveNetIcon") self.iconize(self.form.saveExtractedButton, "SP_DialogSaveButton") self.iconize(self.form.fromMacroButton, "SP_FileDialogContentsView") self.iconize(self.form.deleteToolbarButton, system_name="delete") self.iconize(self.form.addMacroToToolbarButton, system_name="list-add") self.iconize(self.form.addNonMacroButton, system_name = "list-add") self.iconize(self.form.removeMacroFromToolbarButton, system_name="list-remove") self.iconize(self.form.removeNonMacroButton, system_name = "list-remove") self.iconize(self.form.renameToolbarButton, system_name="edit-edit") self.iconize(self.form.makeIconButton, system_name="colors") self.iconize(self.form.newToolbarButton, system_name="window-new") self.iconize(self.form.selectInstalledButton, system_name="view-select") self.iconize(self.form.selectMacroButton, system_name="view-select") self.iconize(self.form.selectNonMacroButton, system_name="view-select") self.iconize(self.form.systemIconButton, icon_theme_name="SP_ComputerIcon") # self.iconize(self.form.preferencesButton, system_name="preferences-system") menuIcon = self.QIconFromXPMString(self.getMenuIcon()) self.form.menuButton.setIcon(menuIcon) icon_name = "Invisible" if not self.form.activeCheckBox.isChecked() else "dagViewVisible" self.iconize(self.form.activeCheckBox, system_name=icon_name) self.form.LineEditFilter.setFocus() def iconize(self,button,icon_theme_name="",system_name=""): """iconize widget, which could be button or checkbox from QStyle theme or system""" if not system_name: pixmapi = getattr(QtGui.QStyle, icon_theme_name) button.setIcon(self.style().standardIcon(pixmapi)) else: icon = FreeCADGui.getIcon(system_name) button.setIcon(icon) def updateUi(self): """this gets called when we need to update the widgets""" tm = self.tm() if tm: qtoolbar = tm.QToolbar if qtoolbar: self.form.activeCheckBox.setChecked(qtoolbar.isVisible()) if self.tbName: self.form.deleteToolbarButton.setEnabled(True) self.form.renameToolbarButton.setEnabled(True) # self.form.addMacroToToolbarButton.setEnabled(True) else: self.form.deleteToolbarButton.setEnabled(False) self.form.renameToolbarButton.setEnabled(False) # self.form.addMacroToToolbarButton.setEnabled(False) #sometimes a macro might have icon information embedded in the code #we try to extract this information, which might be the path to #the icon file, a url to download it, or XPM data stored as a string content = self.form.PlainTextEditExtracted.toPlainText() if not content: self.form.openHyperlinkButton.setEnabled(False) self.form.saveExtractedButton.setEnabled(False) self.form.useAsPixmapButton.setEnabled(False) if not self.menuText: if self.macroName: self.menuText = os.path.splitext(self.macroName)[0] self.form.LineEditMenuText.setText(self.menuText) self.form.LineEditToolTip.setText(self.tooltip) self.form.LineEditStatusText.setText(self.statusText) self.form.LineEditWhatsThis.setText(self.whatsThis) self.form.LineEditShortcut.setKeySequence(QtGui.QKeySequence(self.shortcut)) self.form.LineEditPixmap.setText(self.pixmap) def showMsg(self, msg, typeId="normal"): """Put message in the message label below the Close button""" formattedMsg = "" if typeId == "normal": formattedMsg = f'{msg}
' elif typeId == "warning": formattedMsg = f'{msg}
' elif typeId == "error": formattedMsg = f'{msg}
' # Check if the message is not the same as the last printed message if formattedMsg != self.lastPrintedMsg: # Print the message to the console using the appropriate print command if typeId == "normal": FreeCAD.Console.PrintMessage(msg + "\n") elif typeId == "warning": FreeCAD.Console.PrintWarning(msg + "\n") elif typeId == "error": FreeCAD.Console.PrintError(msg + "\n") # Prepend the new message to the list self.messages.insert(0, formattedMsg) # Keep only the last 3 messages if len(self.messages) > 3: self.messages.pop() # Update the QLabel with the last 3 messages self.form.statusLabel.setText("".join(self.messages)) # Update the last printed message self.lastPrintedMsg = formattedMsg def download_file(self, url, default_folder, default_filename): response = requests.get(url) if response.status_code == 200: file_name,ok = QtGui.QFileDialog.getSaveFileName(None, "Save File As", f"{default_folder}\{default_filename}") if file_name: with open(file_name, 'wb') as file: file.write(response.content) return file_name return None def openHyperlinkButtonClicked(self): url = self.form.PlainTextEditExtracted.toPlainText() macroBaseName = os.path.splitext(self.form.ComboBoxMacroName.currentText())[0] fn = os.path.basename(url) ext = os.path.splitext(fn)[1] iconFolders = self.getIconFolders() if iconFolders: default_folder = iconFolders[0] else: default_folder = FreeCAD.getUserMacroDir(True) default_filename = f"{macroBaseName}_icon{ext}" file_name = self.download_file(url,default_folder,default_filename) if file_name: self.form.LineEditPixmap.setText(file_name) self.handlePixmap() def extractXPMButtonClicked(self): """extract the XPM data from the current pixmap icon and put it in the extracted plain text edit for saving""" image = self.currentIcon.pixmap(64,64).toImage() buffer = QtCore.QBuffer() buffer.open(QtCore.QBuffer.ReadWrite) image.save(buffer, "XPM") # Get the XPM data as bytes xpm_data_bytes = buffer.data() # Convert the bytes to a string xpm_data_string = xpm_data_bytes.data().decode('utf-8') self.form.PlainTextEditExtracted.setPlainText(xpm_data_string) self.form.saveExtractedButton.setEnabled(True) #FreeCAD.Console.PrintMessage(f"xpm = {xpm_data_string}\n") def saveXPMFile(self, default_folder, default_filename, content): file_name, ok = QtGui.QFileDialog.getSaveFileName(None, "Save File As", f"{default_folder}/{default_filename}", "Images (*.xpm *.svg);;All Files (*)") if file_name: with open(file_name, 'w') as file: file.write(content) return file_name return None def saveExtractedButtonClicked(self): #Save XPM button macroBaseName = os.path.splitext(self.form.ComboBoxMacroName.currentText())[0] content = self.form.PlainTextEditExtracted.toPlainText() ext = ".xpm" if "xpm" in content.lower() else ".svg" if "svg" in content.lower() else ".txt" iconFolders = self.getIconFolders() if iconFolders: default_folder = iconFolders[0] else: default_folder = FreeCAD.getUserMacroDir(True) default_filename = f"{macroBaseName}_icon{ext}" content = self.form.PlainTextEditExtracted.toPlainText() file_name = self.saveXPMFile(default_folder,default_filename,content) if file_name: self.form.LineEditPixmap.setText(file_name) self.handlePixmap() def useAsPixmapButtonClicked(self): content = self.form.PlainTextEditExtracted.toPlainText() if len(content) > 255: self.showMsg("Content is too big for Pixmap field. "+ "We don't want to enlarge too much our configuration files. Save as XPM instead if it's an XPM definition.","error") return self.form.LineEditPixmap.setText(content) self.handlePixmap() def showExtracted(self, value): """show content extracted from macro file in a QPlainTextEdit""" #first clear the plain text edit of any existing content edit = self.form.PlainTextEditExtracted edit.setPlainText("") if value: edit.setPlainText(value) self.form.openHyperlinkButton.setEnabled(True) self.form.saveExtractedButton.setEnabled(True) self.form.useAsPixmapButton.setEnabled(True) def addNonMacroButtonClicked(self): """add a non macro to the toolbar""" self.updateVariables() if not self.tbName: self.showMsg("No custom toolbar. Create one first.","error") return tm = self.tm() if not tm: self.showMsg("Unable to get toolbar manager instance for {self.tbName}:{self.wbName}","error") return exists = tm.isInstalled(self.nonMacroName) if exists: tm.uninstallNonMacroFromToolbar(self.nonMacroName) self.showMsg(f"Removing and re-installing {self.nonMacroName} to {self.tbName} for workbench: {self.wbName}","warning") #for non-macro types all we can really edit is the shortcut because we are simply #re-using an existing action cmd = FreeCADGui.Command.get(self.nonMacroName) if cmd: cmd.setShortcut(self.shortcut if self.shortcut else "") nonMacroObject = Macro(self.nonMacroName, \ self.menuText, \ self.tooltip, \ self.statusText, \ self.whatsThis, \ self.shortcut, \ self.pixmap) command_name = tm.addMacroToToolbar(nonMacroObject, bAddAction = False) #works for both types self.updateInstalled() self.form.ComboBoxInstalled.setCurrentText(nonMacroObject.macroName) if command_name: self.showMsg(f"Added {self.nonMacroName} to {self.tbName} -> workbench:{tm.Workbench}, command name: {command_name}","normal") self.showMsg(f"If this is a workbench action the icon will not appear unless the workbench has been loaded.", "normal") def addMacroToToolbarButtonClicked(self): self.updateVariables() if not self.menuText: self.showMsg("Menu text field cannot be empty.","error") return if not self.tbName: self.showMsg("No custom toolbar. Create one first.","error") return tm = self.tm() if not tm: self.showMsg("Unable to get toolbar manager instance for {self.tbName}:{self.wbName}","error") return exists = tm.isInstalled(self.macroName) if exists: tm.uninstallMacroFromToolbar(self.macroName) self.showMsg(f"Removing and re-installing {self.macroName} to {self.tbName} for workbench: {self.wbName}","warning") macroObject = Macro(self.macroName, \ self.menuText, \ self.tooltip, \ self.statusText, \ self.whatsThis, \ self.shortcut, \ self.pixmap) command_name = tm.addMacroToToolbar(macroObject) self.updateInstalled() self.form.ComboBoxInstalled.setCurrentText(macroObject.macroName) if command_name: self.showMsg(f"Added {self.macroName} to {self.tbName} -> workbench:{tm.Workbench}, command name: {command_name}","normal") def removeNonMacroButtonClicked(self): """remove a non-macro from the toolbar""" tm = self.tm() if not tm: self.showMsg(f"Error getting instance of toolbar manager for toolbar {self.tbName}: workbench {self.wbName}","error") return if tm.uninstallNonMacroFromToolbar(self.nonMacroName): self.showMsg(f"{self.nonMacroName} removed from {self.tbName} -> workbench:{self.wbName}","normal") else: self.showMsg(f"Error removing {self.nonMacroName} from {self.tbName} --> workbench:{self.wbName}","error") self.updateInstalled() self.updateUi() self.form.ComboBoxNonMacros.setCurrentText(self.nonMacroName) def removeMacroFromToolbarButtonClicked(self): """We remove the macro from the toolbar, but we don't delete the macro action""" tm = self.tm() if not tm: self.showMsg(f"Error getting instance of toolbar manager for toolbar {self.tbName}: workbench {self.wbName}","error") return if tm.uninstallMacroFromToolbar(self.macroName): self.showMsg(f"{self.macroName} removed from {self.tbName} -> workbench:{self.wbName}","normal") else: self.showMsg(f"Error removing {self.macroName} from {self.tbName} --> workbench:{self.wbName}","error") macroName = self.macroName self.updateInstalled() self.form.ComboBoxMacroName.setCurrentText(macroName) self.updateUi() def tm(self): """we get the ToolbarManager instance if we can (if both toolbar name and workbench name are defined) but if both are not defined we don't call the constructor, but instead return None. Callers must always test the return value against None before attempting to use the object.""" if self.wbName and self.tbName: return ToolbarManager(self.tbName, self.wbName) else: return None def onComboBoxNonMacrosCurrentIndexChanged(self, index): """user has selected a different non-macro or possibly it has been updated from code during setup or if a filter has caused it to get rebuilt""" tm = self.tm() # if tm: # self.form.removeNonMacroButton.setEnabled(tm.isInstalled(self.nonMacroName)) self.enableNonMacroButtons(True) self.updateLineEditsFromNonMacroCommand(self.nonMacroName) self.handlePixmap() self.updateVariables() self.updateUi() def onComboBoxMacroNameCurrentIndexChanged(self, index): """user has selected a different macro name or possibly it has been updated from code during setup or if a filter has caused it to get rebuilt""" tm = self.tm() # if tm: # self.form.removeMacroFromToolbarButton.setEnabled(tm.isInstalled(self.macroName)) self.enableNonMacroButtons(False) command = FreeCADGui.Command.findCustomCommand(self.macroName) if command: descriptor = FreeCADGui.CommandAction(command) cmdObj = descriptor.getCommand() commandDict = cmdObj.getInfo() self.form.LineEditMenuText.setText(commandDict['menuText']) self.form.LineEditToolTip.setText(commandDict['toolTip']) self.form.LineEditStatusText.setText(commandDict['statusTip']) self.form.LineEditWhatsThis.setText(commandDict['whatsThis']) self.form.LineEditShortcut.setText(commandDict['shortcut']) self.form.LineEditPixmap.setText(commandDict['pixmap']) else: self.form.LineEditMenuText.setText(os.path.splitext(self.macroName)[0]) self.form.LineEditToolTip.setText('') self.form.LineEditStatusText.setText('') self.form.LineEditWhatsThis.setText('') self.form.LineEditShortcut.setKeySequence(QtGui.QKeySequence('')) self.form.LineEditPixmap.setText('') self.handlePixmap() self.updateVariables() self.updateUi() def getToolbarName(self,default="MyToolbar"): """get new name for custom toolbar, used when making new toolbar or renaming another""" val,ok = QtGui.QInputDialog.getText(self.form,"Macro Toolbar Manager Input","Enter name for toolbar:",text=default) if ok: return val else: return None def getIconFolders(self): bitmapsGroup = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Bitmaps") customPathNames = bitmapsGroup.GetStrings() iconFolders = [bitmapsGroup.GetString(cpn) for cpn in customPathNames] return iconFolders def selectIconFileButtonClicked(self): """first icon folder is the default, else we use the user macro directory""" iconFolders = self.getIconFolders() if iconFolders: default = iconFolders[0] else: default = FreeCAD.getUserMacroDir(True) fileName, ok = QtGui.QFileDialog.getOpenFileName(self, "Select icon image file", default, "Images (*.xpm *.jpg *.svg *.jpeg *.bmp *.png *.ico);;All Files (*)") if fileName: self.form.LineEditPixmap.setText(os.path.normpath(fileName)) self.handlePixmap() def fromMacroButtonClicked(self): """extract icon information embedded in some macro files as __xpm__ or __icon__ string variables""" fullpath = os.path.join(FreeCAD.getUserMacroDir(True), \ self.form.ComboBoxMacroName.currentText()) delimiters = ["3dq", "3sq", "sq", "dq"] #3dq = triple double quotes, etc. varnames = ["__xpm__", "__icon__"] value = None bFound = False for delimiter in delimiters: for varname in varnames: #print(f"calling extractIconValues with:{varname},{delimiter}") value = self.extractIconValues(fullpath, varname, delimiter) if value is not None and len(value) > 5: bFound = True break if bFound and len(value) > 5: break if not bFound: self.showMsg(f"Unable to extract __xpm__ or __icon__ information from " +\ f"{self.form.ComboBoxMacroName.currentText()}\n","normal") return self.showExtracted(value) #puts in QPlainTextEdit def extractIconValues(self, file_path, varname="__icon__", delimiter_type = "3sq"): """we search for __icon__ and __xpm__ defines in the macro text using this function""" DEBUG_PRINT = False try: with open(file_path, 'r') as file: content = file.read() # Convert varname and content to lowercase for case-insensitive search to match __XPM__ or __ICON__, too varname = varname.lower() content_lower = content.lower() # Define the variable search pattern based on the delimiter type # It seemed simpler to code this way, so it can be called multiple # times to look for the various potential string delimiters if delimiter_type == "3sq": delimiter = "'''" elif delimiter_type == "3dq": delimiter = '"""' elif delimiter_type == "sq": delimiter = "'" elif delimiter_type == "dq": delimiter = '"' else: raise ValueError("Invalid delimiter_type. Use '3sq', '3dq', 'sq', or 'dq'.") # Search for the variable definition with varname at the beginning of a line # varname might be __icon__ or __xpm__, depending on what we called this function using # the test is, first we look for __icon__ or __xpm__ (case insensitive) at the start of a line # then it must be followed by an = sign and by the sought after delimiter # there must be no other characters other than space or \n between varname and = sign # then there must be no other characters other than space or \n between the = sign and the delimiter # then we search for the next occurrence of the delimiter and accept all characters between the delimiters # as the variable definition var_start = content_lower.find(f"\n{varname}") if var_start != -1: eq_index = content.find("=", var_start) if eq_index != -1: varname_to_eq = content[var_start + len(varname) + 1: eq_index] # Add 1 to skip the newline # Ensure only whitespaces (spaces or carriage returns) between varname and = if all(char in " \n" for char in varname_to_eq): delimiter_start = content.find(delimiter, eq_index) if delimiter_start != -1: delimiter_end = content.find(delimiter, delimiter_start + len(delimiter)) if delimiter_end != -1: # Ensure only whitespaces (spaces or carriage returns) between = and delimiter between_eq_and_delimiter = content[eq_index + 1: delimiter_start] if all(char in " \n" for char in between_eq_and_delimiter): value = content[delimiter_start+len(delimiter): delimiter_end] if value and value[0] == "\n": #XPM is invalid if first line is empty value = value[1:] return value elif DEBUG_PRINT: print("Invalid characters found between = and delimiter:",\ between_eq_and_delimiter) elif DEBUG_PRINT: print(f"Delimiter '{delimiter}' not found after '='.") elif DEBUG_PRINT: print(f"Delimiter '{delimiter}' not found after '='.") elif DEBUG_PRINT: print("Invalid characters found between varname and '=':", varname_to_eq) elif DEBUG_PRINT: print("= not found after varname.") elif DEBUG_PRINT: print(f"{varname} not found in the file.") return None except FileNotFoundError: print(f"File not found: {file_path}") return None def newToolbarButtonClicked(self): """user is creating a new toolbar, note we must call constructor with bCreate = True to create the toolbar manager. We also cannot use the self.tm() convenience method because we need to set bCreate to True""" newName = self.getToolbarName() if newName and self.wbName: tm = ToolbarManager(newName, self.wbName, bCreate = True) self.showMsg(f"New toolbar created: {newName}") self.updateCustomToolbarComboBox() def renameToolbarButtonClicked(self): if not self.tbName: self.showMsg("No toolbar to rename. Create one first.", "error") return tm = self.tm() if not tm: self.showMsg(f"Error getting toolbar manager for toolbar {tbName}: workbench {wbName}","error") newName = self.getToolbarName(self.tbName) if newName: tm.renameToolbar(newName) self.updateCustomToolbarComboBox() self.tbName = newName def deleteToolbarButtonClicked(self): self.updateVariables() msg = QtGui.QMessageBox() msg.setIcon(QtGui.QMessageBox.Warning) msg.setText(f"Do you want to delete the toolbar:\n{self.tbName}?") msg.setWindowTitle("Delete Toolbar Confirmation") msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) msg.setDefaultButton(QtGui.QMessageBox.No) result = msg.exec() if result == QtGui.QMessageBox.Yes: tm = self.tm() if tm: tm.removeToolbar() self.showMsg(f"custom toolbar: {self.tbName} removed -> workbench:{self.wbName}","normal") else: self.showMsg(f"Error deleting toolbar for toolbar:{self.tbName} for workbench:{self.wbName}.", "error") self.updateCustomToolbarComboBox() self.updateUi() else: self.showMsg(f"User canceled. The toolbar {self.tbName} has not been deleted.") def reject(self): self.form.close() def getMenuIcon(self): width, height = 64, 64 icon = ["\n/* XPM */"] icon.append(f"static char *icon_xpm[] = {{") icon.append(f"/* cols rows colors chars-per-color */") icon.append(f'"{width} {height} 2 1",') icon.append(f'" c None",') icon.append(f'". c Black",') icon.append('/*pixels*/') emptyLine = '"'+' ' * width + '",' stripe = '"'+'.' * width + '",' linesWithStripes = [11,12,13,14, 31,32,33,34, 51,52,53,54] for y in range(height): line = emptyLine if not y in linesWithStripes else stripe icon.append(line) icon.append("};") ret = "\n".join(icon) return ret class SystemIconSelector(QtGui.QDialog): """creates a dialog of icon buttons for the user to select from among available system icons These basenames are filenames from the 0.22 source code in src/Gui/Icons""" def __init__(self, parseModFolder, cte, bShowBrowseButton=False, bShowStyleIcons=False): self.cte = cte #to use with showMsg() self.bUseStyle = False #to use text data to generate a style icon self.bShowBrowseButton = bShowBrowseButton self.bShowStyleIcons = bShowStyleIcons self.bLoadFromDisk = False self.basenames = ['AddonManager', 'Document', 'DrawStyleAsIs', 'DrawStyleFlatLines', 'DrawStyleHiddenLine', \ 'DrawStyleNoShading', 'DrawStylePoints','DrawStyleShaded', 'DrawStyleWireFrame', 'Feature', 'Geofeaturegroup',\ 'Group', 'Invisible', 'Link', 'LinkArray', 'LinkArrayOverlay', 'LinkElement', 'LinkGroup', 'LinkImport',\ 'LinkImportAll', 'LinkOverlay', 'LinkReplace', 'LinkSelect', 'LinkSelectAll', 'LinkSelectFinal', \ 'LinkSub', 'LinkSubElement', 'LinkSubOverlay', 'MacroEditor', 'Param_Bool', 'Param_Float',\ 'Param_Int', 'Param_Text', 'Param_UInt', 'Part_Measure_Clear_All', 'Part_Measure_Toggle_All', 'PolygonPick', \ 'Python', 'SpNav-PanLR', 'SpNav-PanUD', 'SpNav-Roll', 'SpNav-Spin', 'SpNav-Tilt', 'SpNav-Zoom', 'Std_Axis', \ 'Std_CoordinateSystem', 'Std_CoordinateSystem_alt', 'Std_Placement', 'Std_Plane', 'Std_Tool1', 'Std_Tool10', \ 'Std_Tool11', 'Std_Tool12', 'Std_Tool2', 'Std_Tool3', 'Std_Tool4', 'Std_Tool5', 'Std_Tool6', 'Std_Tool7', \ 'Std_Tool8', 'Std_Tool9', 'Std_ViewScreenShot', 'Std_WindowCascade', 'Std_WindowNext', 'Std_WindowPrev', \ 'Std_WindowTileVer', 'TextDocument', 'Tree_Annotation', 'Tree_Dimension', 'Tree_Python', 'Unlink', 'WhatsThis', \ 'accessories-calculator', 'accessories-text-editor', 'application-exit', 'applications-accessories', 'applications-python',\ 'background', 'bound-expression', 'bound-expression-unset', 'breakpoint', 'bulb', 'button_add_all', 'button_down', \ 'button_invalid', 'button_left', 'button_right', 'button_sort', 'button_up', 'button_valid', 'camera-photo', 'colors', \ 'dagViewFail', 'dagViewPass', 'dagViewPending', 'dagViewVisible', 'debug-marker', 'debug-start', 'debug-stop', 'delete', \ 'document-new', 'document-open', 'document-package', 'document-print', 'document-print-preview', 'document-properties', \ 'document-python', 'document-save', 'document-save-as', 'edit-cleartext', 'edit-copy', 'edit-cut', 'edit-delete', \ 'edit-edit', 'edit-element-select-box', 'edit-paste', 'edit-redo', 'edit-select-all', 'edit-select-box', 'edit-undo', \ 'edit_Cancel', 'edit_OK', 'folder', 'freecad', 'freecad-doc', 'freecadsplash', 'help-browser',\ 'internet-web-browser', 'list-add', 'list-remove', \ 'media-playback-start', 'media-playback-stop', 'media-record', 'mouse_pointer', 'preferences-display', \ 'preferences-general', 'preferences-import-export', 'preferences-system', 'process-stop', 'px', \ 'sel-back', 'sel-bbox', 'sel-forward', 'sel-instance', 'spaceball_button', 'tree-doc-collapse', 'tree-doc-multi', \ 'tree-doc-single', 'tree-goto-sel', 'tree-item-drag', 'tree-pre-sel', 'tree-rec-sel', 'tree-sync-pla', 'tree-sync-sel', \ 'tree-sync-view', 'user', 'utilities-terminal', 'view-axonometric', 'view-bottom', 'view-front', 'view-fullscreen', \ 'view-isometric', 'view-left', 'view-measurement', 'view-perspective', 'view-rear', 'view-refresh', 'view-right', \ 'view-rotate-left', 'view-rotate-right', 'view-select', 'view-top', 'view-unselectable', 'window-new', 'zoom-all', \ 'zoom-border', 'zoom-fit-best', 'zoom-in', 'zoom-out', 'zoom-selection'] self.updateBaseNames() self.styles = ['SP_ArrowBack', 'SP_ArrowDown', 'SP_ArrowForward', 'SP_ArrowLeft', 'SP_ArrowRight', 'SP_ArrowUp',\ 'SP_BrowserReload', 'SP_BrowserStop', 'SP_CommandLink', 'SP_ComputerIcon', 'SP_CustomBase', 'SP_DesktopIcon', \ 'SP_DialogAbortButton', 'SP_DialogApplyButton', 'SP_DialogCancelButton', 'SP_DialogCloseButton', 'SP_DialogDiscardButton',\ 'SP_DialogHelpButton', 'SP_DialogIgnoreButton', 'SP_DialogNoButton', 'SP_DialogNoToAllButton', 'SP_DialogOkButton',\ 'SP_DialogOpenButton', 'SP_DialogResetButton', 'SP_DialogRetryButton', 'SP_DialogSaveAllButton', 'SP_DialogSaveButton',\ 'SP_DialogYesButton', 'SP_DialogYesToAllButton', 'SP_DirClosedIcon', 'SP_DirHomeIcon', 'SP_DirIcon', 'SP_DirLinkIcon',\ 'SP_DirLinkOpenIcon', 'SP_DirOpenIcon', 'SP_DockWidgetCloseButton', 'SP_DriveCDIcon', 'SP_DriveDVDIcon', 'SP_DriveFDIcon',\ 'SP_DriveHDIcon', 'SP_DriveNetIcon', 'SP_FileDialogBack', 'SP_FileDialogContentsView', 'SP_FileDialogDetailedView',\ 'SP_FileDialogEnd', 'SP_FileDialogInfoView', 'SP_FileDialogListView', 'SP_FileDialogNewFolder', 'SP_FileDialogStart',\ 'SP_FileDialogToParent', 'SP_FileIcon', 'SP_FileLinkIcon', 'SP_LineEditClearButton', 'SP_MediaPause', 'SP_MediaPlay',\ 'SP_MediaSeekBackward', 'SP_MediaSeekForward', 'SP_MediaSkipBackward', 'SP_MediaSkipForward', 'SP_MediaStop', 'SP_MediaVolume',\ 'SP_MediaVolumeMuted', 'SP_MessageBoxCritical', 'SP_MessageBoxInformation', 'SP_MessageBoxQuestion', 'SP_MessageBoxWarning',\ 'SP_RestoreDefaultsButton', 'SP_TitleBarCloseButton', 'SP_TitleBarContextHelpButton', 'SP_TitleBarMaxButton',\ 'SP_TitleBarMenuButton', 'SP_TitleBarMinButton', 'SP_TitleBarNormalButton', 'SP_TitleBarShadeButton', 'SP_TitleBarUnshadeButton',\ 'SP_ToolBarHorizontalExtensionButton', 'SP_ToolBarVerticalExtensionButton', 'SP_TrashIcon', 'SP_VistaShield'] super(SystemIconSelector, self).__init__() self.setWindowTitle("System Icon Selector") self.parseModFolder = parseModFolder #whether to parse Mod folder for more icons layout = QtGui.QGridLayout() scrollArea = QtGui.QScrollArea(self) scrollArea.setWidgetResizable(True) qwidget = QtGui.QWidget() qwidget.setLayout(layout) scrollArea.setWidget(qwidget) self.icon_text = "" def trigger(val): return lambda : self.setIconText(val) def styleTrigger(val): return lambda : self.setIconStyleText(val) #we make a grid of clickable icon buttons the user can choose from for a toolbar icon iconCount = 0 for n, name in enumerate(self.basenames): try: btn = QtGui.QPushButton() btn.installEventFilter(self) btn.setToolTipDuration(2) icon = FreeCADGui.getIcon(name) if icon: btn.setIcon(icon) btn.setToolTip(name) btn.clicked.connect(trigger(name)) layout.addWidget(btn, n // 8, n % 8) # Use integer division (//) for row iconCount += 1 except Exception as e: pass imgFiles = self.findMoreIcons() for n, filename in enumerate(imgFiles): try: btn = QtGui.QPushButton() btn.installEventFilter(self) btn.setToolTipDuration(2) icon = QtGui.QIcon(filename) if icon: btn.setIcon(icon) btn.setToolTip(filename) btn.clicked.connect(trigger(filename)) layout.addWidget(btn, iconCount // 8, iconCount % 8) iconCount += 1 except Exception as e: pass if self.bShowStyleIcons: for n, style in enumerate(self.styles): try: btn = QtGui.QPushButton() btn.installEventFilter(self) btn.setToolTipDuration(2) pixmapi = getattr(QtGui.QStyle, style) icon = self.style().standardIcon(pixmapi) if icon: btn.setIcon(icon) btn.setToolTip(style) btn.clicked.connect(styleTrigger(style)) layout.addWidget(btn, iconCount // 8, iconCount % 8) iconCount += 1 except Exception as e: pass # Set the layout of the main window # this dialog called from 2 places: main toolbar manager editor # and within the simple icon maker dialog # when called from main we don't show the browse icon because there is a # separate button for that, and we don't show the style icons because # they are not directly supported in the pixmap field in FreeCAD # but we can make them readily enough for our purposes, so we do show # the style icons when the icon maker opens this dialog main_layout = QtGui.QVBoxLayout() if self.bShowBrowseButton: loadFromDiskButton = QtGui.QPushButton("Load from disk") main_layout.addWidget(loadFromDiskButton) loadFromDiskButton.clicked.connect(self.loadFromDiskButtonClicked) main_layout.addWidget(scrollArea) self.setLayout(main_layout) def updateBaseNames(self): actions = FreeCADGui.Command.listAll() infos = [FreeCADGui.Command.get(action).getInfo() for action in actions if not "Std_Macro" in action] pixmaps = [info["pixmap"] for info in infos] self.basenames = sorted(list(set(pixmaps+self.basenames))) def show_custom_tooltip(self, widget, pixmap, txt): self.tooltip_widget = QtGui.QWidget(self) self.tooltip_widget.setWindowFlags(QtCore.Qt.ToolTip) self.tooltip_widget.setGeometry(widget.mapToGlobal(widget.rect().topLeft()).x() + 40, widget.mapToGlobal(widget.rect().topLeft()).y() + 40, pixmap.width() + 10, pixmap.height() + 30) # Adjust size for text layout = QtGui.QVBoxLayout(self.tooltip_widget) label = QtGui.QLabel(self.tooltip_widget) label.setPixmap(pixmap) label.setAlignment(QtCore.Qt.AlignCenter) text_label = QtGui.QLabel(txt, self.tooltip_widget) text_label.setAlignment(QtCore.Qt.AlignCenter) layout.addWidget(label) layout.addWidget(text_label) self.tooltip_widget.setLayout(layout) self.tooltip_widget.show() timer = QtCore.QTimer(self) timer.setSingleShot(True) timer.start(1500) timer.timeout.connect(self.tooltip_widget.close) def eventFilter(self, obj, event): if event.type() == event.ToolTip and type(obj) == QtGui.QPushButton: # Create a custom tooltip with an enlarged icon using a QLabel icon = obj.icon() pixmap = icon.pixmap(100, 100) # Set the desired enlarged size # Show the custom tooltip when the mouse enters the button txt = obj.toolTip() self.show_custom_tooltip(obj, pixmap, txt) return True return super().eventFilter(obj, event) def parseFolder(self, folder): """parse folder for image files""" self.cte.showMsg(f"\nParsing {folder}...") FreeCADGui.updateGui() time.sleep(0.01) dirEntries = os.scandir(folder) filepaths = [os.path.join(folder, de.name) for de in dirEntries if de.is_file() and bool(".svg" in de.name \ or ".xpm" in de.name \ or ".jpg" in de.name \ or ".jpeg" in de.name \ or ".png" in de.name \ or ".bmp" in de.name \ or ".ico" in de.name)] return filepaths def findMoreIcons(self): """look through user-defined icon folders""" bitmapsGroup = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Bitmaps") strings = bitmapsGroup.GetStrings() folders = [bitmapsGroup.GetString(string) for string in strings] filenames = [] for folder in folders: files = self.parseFolder(folder) if files: filenames.extend(files) #now search workbenches installed in Mod folder if self.parseModFolder: userFolder = FreeCAD.getUserConfigDir() if hasattr(FreeCAD,"getUserConfigDir") else FreeCAD.getUserAppDataDir() modFolder = os.path.join(userFolder,"Mod") filenames.extend(self.parseWorkbenchImages(modFolder)) return filenames def isSmallImage(self, file_path, max_width=64, max_height=64): try: with PIL.Image.open(file_path) as img: width, height = img.size return width <= max_width and height <= max_height except: if file_path.endswith(".svg"): return True def parseWorkbenchImages(self, modFolder): image_extensions = (".svg", ".xpm", ".jpg", ".jpeg", ".png", ".bmp", ".ico") image_files = [] for root, _, files in os.walk(modFolder): for file in files: if file.endswith(image_extensions): file_path = os.path.join(root, file) self.cte.showMsg(f"Parsing {file_path}...") FreeCADGui.updateGui() time.sleep(0.01) if self.isSmallImage(file_path, max_width=128, max_height=128): image_files.append(file_path) return image_files def setIconText(self,value): self.icon_text = value self.close() def setIconStyleText(self, value): self.icon_text = value self.bUseStyle = True self.close() def loadFromDiskButtonClicked(self): self.bLoadFromDisk = True self.close() class Macro: """holds macro name, tooltip, etc.""" def __init__(self, macroName="", \ menuText="", \ tooltip = "", \ statusText = "", \ whatsThis = "", \ shortcut = "", \ pixmap = "",\ ): self.macroName = macroName self.menuText = menuText self.tooltip = tooltip self.statusText = statusText self.whatsThis = whatsThis self.shortcut = shortcut self.pixmap = pixmap class ToolbarManager: """a helper class for manipulating custom toolbars Credit to the developers of AddonManager as much of this code was borrowed from there and modified, for better or worse. static methods: ToolbarManager.refreshToolbar() #reloads the currently active workbench tm = ToolbarManager(toolbarName, workbenchName, bCreate = False) If bCreate = True, then if a toolbar named toolbarName does not exist for workbenchName, it will be created autmatically. Default constructor ToolbarManager() is not supported. tm.QToolbar -- returns the QToolBar object associated with this toolbar, or None tm.Name -- the name of the toolbar, e.g. MyToolbar, can also be used for renaming it tm.removeToolbar() -- removes the toolbar and deletes it tm.Active -- whether the toolbar is active or not, is also a setter for this property. tm.Active = False will hide the toolbar and make it inactive, meaning it will remain hidden after restarting. tm.Active = True will make it active and visible. tm.Workbench -- returns the workbench name or sets it to a new name. tm.RealName -- returns the real name of the toolbar, e.g. Custom1 ToolbarManager.getCustomToolbarNames(wbName) --static method, returns names of all custom toolbars for the given workbench name. If wbName is "Global", then it returns all the global custom toolbars ToolbarManager.exists(tbName, wbName) -- static method, boolean return True if toolbar exists tm.CustomToolbarNames -- returns all custom toolbar names for the workbench this object was created for tm.TopGroup -- ParameterGrp object: BaseApp/Workbench/{wbName}/Toolbar (parent of this toolbar object) tm.Groups -- list of names of toolbars for this workbench, e.g. ["Custom1","Custom2"] tm.isInstalled(macroName) -- boolean, whether macro is installed on this toolbar tm.Group -- ParameterGrp object for this toolbar, e.g. BaseApp/Workbench/Global/Toolbar/Custom2 tm.getOtherGroup(gpName) -- used to get another sibling ParameterGrp object ToolbarManager.refreshToolbar() -- static method, refreshes toolbar by reloading active workbench tm.renameToolbar(newName) -- renames the toolbar tm.newToolbar(tbName) -- creates new toolbar for the workbench already associated with this object Alternatively, use tm = ToolbarManager(tbName, wbName, bCreate=True) ToolbarManager.getAllMacroCommandsWithActions() -- static method, returns list of all macro commands with actions in the form of ["Std_Macro_1","Std_Macro_3", etc.] Note: not all macros with actions are installed to toolbars. When removing a macro from a toolbar, we don't delete the action because it might be on another toolbar. tm.removeMacroAction(macroName) -- deletes the macro action. If the macro is on a toolbar it will disappear, left as an orphan in parameters. You should remove from the toolbar(s) first, then remove the action. tm.getMacroName(commandName) -- returns the macro name, e.g. "MyMacro.FCMacro" from its commandName, e.g. "Std_Macro_23" tm.getInstalledMacros() -- returns a list of macro names that are installed on this toolbar tm.MacrosGroup -- returns ParameterGrp object BaseApp/Macro/Macros tm.addMacroAction(macroObject) -- creates the action, but doesn't add to any toolbar. Argument is a Macro class object, see Macro class. tm.addMacroToToolbar(macroObject) -- creates the action, and adds macro to the toolbar. If the action already exists it gets removed and a new one created. tm.getCommandName(macroName) -- translates macro name into command name, e.g. Mymacro.FCMacro -> Std_Macro_0 tm.uninstallMacroFromToolbar(macroName, bRemoveAction = False) -- removes macro from toolbar, deletes action if bRemoveAction = True """ def __init__(self,name,workbench,bCreate = False): """name is the name the user gives to the toolbar, e.g. MyMacroToolbar""" if not name and workbench: raise Exception("unsupported creation method, must provide both name and workbench name") self.name = name self.workbenchName = workbench if bCreate and not ToolbarManager.exists(self.name, self.workbenchName): FreeCAD.Console.PrintMessage(f"Creating new toolbar: {name} for workbench: {workbench}\n") self.newToolbar(self.name) @property def QToolbar(self): #FreeCAD.Console.PrintMessage(f"Getting QToolBar for {self.Name}\n") self.qtoolbar = FreeCADGui.getMainWindow().findChild(QtGui.QToolBar,self.Name) return self.qtoolbar @property def Name(self): return self.name @Name.setter def Name(self,name): """renames toolbar""" self.renameToolbar(name) def removeToolbar(self): """remove this custom toolbar""" self.TopGroup.RemGroup(self.RealName) #RealName is e.g. Custom1, not user-friendly label ToolbarManager.refreshToolbar() @property def Active(self): """returns boolean whether this toolbar is active""" return self.Group.GetBool("Active") @Active.setter def Active(self, bActive): """sets Active based on bActive argument""" self.Group.SetBool("Active",bActive) @property def Workbench(self): return self.workbenchName @Workbench.setter def Workbench(self, newBench): self.__init__(self.name, newBench) @property def RealName(self): """RealName is the name in the parameters for this group, e.g. Custom_1""" groups = self.Groups names = [] for group in groups: toolbar = self.TopGroup.GetGroup(group) name = toolbar.GetString("Name","") if name == self.Name: return group if self.Name: FreeCAD.Console.PrintError(f"Unable to find custom toolbar real name for {self.Name}\n") return None @staticmethod def getCustomToolbarNames(wbName): """return the names of all the custom toolbars for this workbench""" topGroup = FreeCAD.ParamGet(f"User parameter:BaseApp/Workbench/{wbName}/Toolbar") groups = topGroup.GetGroups() names = [] for group in groups: toolbar = topGroup.GetGroup(group) name = toolbar.GetString("Name","") if name: names.append(name) return names @staticmethod def exists(tbName, wbName): """check of there is a toolbar name of tbName in wbName""" topGroup = FreeCAD.ParamGet(f"User parameter:BaseApp/Workbench/{wbName}/Toolbar") groups = topGroup.GetGroups() for group in groups: toolbar = topGroup.GetGroup(group) name = toolbar.GetString("Name","") if name == tbName: return True return False @property def CustomToolbarNames(self): """get a list of the names of the current global custom toolbars""" # parameter group hierarchy is as follows: # BaseApp/Workbench/Global/Toolbar/(here we have the toolbar groups, e.g. Custom1) # Custom1 will have these parameters: # Active - boolean # Name - name of the toolbar, e.g. MyMacrosToolbar # Std_Macro_NNN where NNN can be anything from 0 to 99 or whatever - strings - "FreeCAD" groups = self.Groups names = [] for group in groups: toolbar = self.TopGroup.GetGroup(group) name = toolbar.GetString("Name","") if name: names.append(name) return names @property def TopGroup(self): return FreeCAD.ParamGet(f"User parameter:BaseApp/Workbench/{self.workbenchName}/Toolbar") @property def Groups(self): """string list of group names, e.g. Custom1, Custom2""" return self.TopGroup.GetGroups() def isInstalled(self,macroName) -> bool: """Returns True if a macro already in toolbar, or False if not.""" installed = self.getInstalledMacros() + self.getInstalledNonMacros() #FreeCAD.Console.PrintMessage(f"installed = {installed}, {macroName} in installed = {macroName in installed}\n") if installed: return macroName in installed else: return False @property def Group(self): """ParameterGrp object, this toolbar""" custom_toolbars = self.Groups for toolbar in custom_toolbars: group = FreeCAD.ParamGet(f"User parameter:BaseApp/Workbench/{self.workbenchName}/Toolbar/" + toolbar) group_name = group.GetString("Name", "") if group_name == self.name: return group return None def getOtherGroup(self, name): """Try to find a toolbar group with a given name, e.g. MyMacroToolbar. Returns the preference group for the toolbar if found, or None if it does not exist.""" #top_group = FreeCAD.ParamGet(f"User parameter:BaseApp/Workbench/{self.workbenchName}/Toolbar") #custom_toolbars = top_group.GetGroups() for toolbar in self.Groups:#custom_toolbars: group = FreeCAD.ParamGet(f"User parameter:BaseApp/Workbench/{self.workbenchName}/Toolbar/" + toolbar) group_name = group.GetString("Name", "") if group_name == name: return group return None @staticmethod def refreshToolbar(): wb = FreeCADGui.activeWorkbench() wb.reloadActive() def renameToolbar(self, newName): """rename the toolbar to newName""" bSuccess = False #FreeCAD.Console.PrintMessage(f"renameToolbar({newName}) custom_toolbars = {custom_toolbars}\n") for toolbar in self.Groups: group_name = self.TopGroup.GetGroup(toolbar).GetString("Name", "") #FreeCAD.Console.PrintMessage(f"group_name = {group_name}, self.Name = {self.Name}\n") if group_name == self.Name: FreeCAD.Console.PrintMessage(f"renamed toolbar from {self.Name} to {newName}\n") self.TopGroup.GetGroup(toolbar).SetString("Name",newName) ToolbarManager.refreshToolbar() bSuccess = True if not bSuccess: FreeCAD.Console.PrintError(f"Something went wrong renaming {self.Name} to {newName}\n") def newToolbar(self, toolbarName) -> object: """Create a new custom toolbar and returns its preference group.""" # We need two names: the name of the auto-created toolbar, as it will be displayed to the # user in various menus, and the underlying name of the toolbar group. Both must be # unique. # First, the displayed name if not toolbarName: raise Exception("Must specify name for new toolbar") name_taken = self.getOtherGroup(toolbarName) if name_taken: i = 2 # Don't use (1), start at (2) while True: test_name = toolbarName + f" ({i})" if not self.getOtherGroup(test_name): toolbarName = test_name i = i + 1 # Second, the toolbar preference group name i = 1 while True: new_group_name = "Custom_" + str(i) if new_group_name not in self.Groups: break i = i + 1 custom_toolbar = FreeCAD.ParamGet( f"User parameter:BaseApp/Workbench/{self.workbenchName}/Toolbar/" + new_group_name ) custom_toolbar.SetString("Name", toolbarName) custom_toolbar.SetBool("Active", True) return custom_toolbar @staticmethod def getAllMacroCommandsWithActions(): """returns ["Std_Macro_1",etc] for all macros that have created actions. These are not necessarily all installed in toolbars.""" commands = FreeCADGui.Command.listAll() macros = [com for com in commands if "Std_Macro_" in com] return macros def removeMacroAction(self, macroName): """remove macro from Macros actions, will no longer have a Std_Macro_NNN associated with it return True on success or False if nothing was removed """ commandName = self.getCommandName(macroName) bSuccess = FreeCADGui.Command.removeCustomCommand(commandName) self.MacrosGroup.RemGroup(commandName) return bSuccess def getMacroName(self, commandName): """returns macro file name from commandName, e.g. Std_Macro_1 -> mymacro.py""" grp = self.MacrosGroup.GetGroup(commandName) return grp.GetString("Script", "") def removeOrphans(self): """removes the orphans from the current toolbar""" if not self.Group: FreeCAD.Console.PrintError(f"Invalid Group error for {self.tbName}:{self.wbName}\n") return orphans = self.getOrphansByCommandName() for orphan in orphans: self.Group.RemString(orphan) def getOrphansByCommandName(self): """orphans are macros that are still in the toolbar, but whose actions have been removed""" FreeCADGui.Command.update() commands = FreeCADGui.Command.listAll() macros = [com for com in commands if "Std_Macro_" in com] #macros is now a list in the form of ["Std_Macro_0", "Std_Macro_1", etc.] if self.Group: strings = self.Group.GetStrings() installed = [string for string in strings if string != "Name"] else: return [] scripts = [] orphans = [] for inst in installed: grp = self.MacrosGroup.GetGroup(inst) script = grp.GetString("Script","") if script: scripts.append(script) bOrphan = FreeCADGui.Command.get(inst) == None if bOrphan: orphans.append(inst) return orphans def getOrphans(self): """orphans are macros that are still in the toolbar, but whose actions have been removed""" FreeCADGui.Command.update() commands = FreeCADGui.Command.listAll() macros = [com for com in commands if "Std_Macro_" in com] #macros is now a list in the form of ["Std_Macro_0", "Std_Macro_1", etc.] if self.Group: strings = self.Group.GetStrings() installed = [string for string in strings if string != "Name"] else: return [] scripts = [] orphans = [] for inst in installed: grp = self.MacrosGroup.GetGroup(inst) script = grp.GetString("Script","") if script: scripts.append(script) bOrphan = FreeCADGui.Command.get(inst) == None if bOrphan: orphans.append(script) return orphans def getInstalledNonMacros(self): """returns all installed actions, not just macros, by name of action""" commands = FreeCADGui.Command.listAll() nonMacros = [com for com in commands if not "Std_Macro_" in com] if self.Group: strings = self.Group.GetStrings() installed = [string for string in strings if string != "Name" and string in nonMacros] #print(f"installed = {installed}") return installed def getInstalledMacros(self): """returns the installed macros on this toolbar by filename in a list of strings""" FreeCADGui.Command.update() commands = FreeCADGui.Command.listAll() macros = [com for com in commands if "Std_Macro_" in com] #macros is now a list in the form of ["Std_Macro_0", "Std_Macro_1", etc.] if self.Group: strings = self.Group.GetStrings() installed = [string for string in strings if string != "Name"] #FreeCAD.Console.PrintMessage(f"(getInstalledMacros()): installed = {installed}\n") else: return [] scripts = [] for inst in installed: grp = self.MacrosGroup.GetGroup(inst) script = grp.GetString("Script","") if script: scripts.append(script) #FreeCAD.Console.PrintMessage(f"scripts = {scripts}\n") return scripts @property def MacrosGroup(self): return FreeCAD.ParamGet("User parameter:BaseApp/Macro/Macros") def addMacroAction(self, macroObject): """creates a macro action and adds to Macros actions, but doesn't put on a toolbar returns the new command name for this action, e.g. Std_Macro_12""" command_name = FreeCADGui.Command.createCustomCommand( macroObject.macroName, macroObject.menuText, macroObject.tooltip, macroObject.whatsThis, macroObject.statusText, macroObject.pixmap, macroObject.shortcut ) #let's add it to the macros group, too, rather than require a restart group = self.MacrosGroup.GetGroup(command_name) group.SetString("Accel",macroObject.shortcut) group.SetString("Menu",macroObject.menuText) group.SetString("Pixmap",macroObject.pixmap) group.SetString("Script",macroObject.macroName) group.SetString("Statustip",macroObject.statusText) group.SetString("Tooltip",macroObject.tooltip) group.SetString("WhatsThis",macroObject.whatsThis) group.SetBool("System",False) return command_name def addMacroToToolbar(self, macroObject, bAddAction=True) -> str: """Creates macro action, adds macro to the toolbar, returns command name, e.g. Std_Macro1""" commands = ToolbarManager.getAllMacroCommandsWithActions() macroName = macroObject.macroName for command in commands: comName = self.getMacroName(command) if comName == macroName: self.removeMacroAction(macroName) #remove before adding so not to have duplicates command_name = self.addMacroAction(macroObject) if bAddAction else macroObject.macroName self.Group.SetString(command_name, macroObject.menuText if macroObject.menuText else macroObject.macroName if macroObject.macroName else "FreeCAD") ToolbarManager.refreshToolbar() return command_name def getCommandName(self,macroName): """command name is in form of Std_Macro_9, returns None if macro is not already in Macros Action""" return FreeCADGui.Command.findCustomCommand(macroName) def uninstallMacroFromToolbar(self, macroName, bRemoveAction = False) -> bool: """Removes macro from toolbar. Also removes action if bRemoveAction = True, which also removes the macro from any other toolbar it might also be installed on""" bSuccess = True command = FreeCADGui.Command.findCustomCommand(macroName) if not command: FreeCAD.Console.PrintError(f"Command for {macroName} not found\n") bSuccess = False else: FreeCAD.Console.PrintMessage(f"Command for {macroName} found: {command}\n") self.Group.RemString(command) #even if no action we can still remove from toolbar, but still flag the error if bRemoveAction: FreeCADGui.Command.removeCustomCommand(command) ToolbarManager.refreshToolbar() return bSuccess def uninstallNonMacroFromToolbar(self, nonMacroName) -> bool: """Removes non-macro from toolbar.""" bSuccess = True command = FreeCADGui.Command.get(nonMacroName) if not command: FreeCAD.Console.PrintError(f"Command for {nonMacroName} not found\n") bSuccess = False else: FreeCAD.Console.PrintMessage(f"Command for {nonMacroName} found: {command}\n") self.Group.RemString(nonMacroName) ToolbarManager.refreshToolbar() return bSuccess refreshButtonIcon = """ /* XPM */ static char *dummy[]={ "64 64 253 2", ".i c #257070", ".Z c #257474", "a6 c #267070", ".j c #267171", ".9 c #277171", "br c #277272", "ay c #277878", "aa c #277c7c", "aF c #287171", "#q c #287272", "#l c #287373", "#X c #297373", ".u c #297e7e", "aV c #298080", "aY c #298484", ".k c #2a7373", ".r c #2a7474", ".x c #2a7f7f", "#u c #2a8383", "bv c #2a8686", "bt c #2a8787", "bW c #2b7373", "al c #2b8181", "am c #2b8282", "bJ c #2b8585", "bI c #2b8686", ".0 c #2b8888", ".Y c #2b8989", "bV c #2b8a8a", ".t c #2c7474", "b1 c #2c7575", ".E c #2d7575", ".q c #2d7676", ".n c #2d7777", ".h c #2d7878", ".v c #2d8c8c", "bU c #2e7575", "b3 c #2e7878", ".I c #2e8b8b", ".J c #2e8d8d", "#B c #2e9797", "#D c #2e9898", ".w c #2f8c8c", "#O c #2f8e8e", "#R c #2f9191", "bZ c #2f9393", ".6 c #2f9595", ".5 c #2f9696", "bD c #2f9797", "a7 c #307676", "aq c #307777", "bk c #308f8f", "aj c #309090", "aw c #309292", ".R c #309393", "ak c #309494", "#C c #309797", ".S c #309898", "aI c #317878", ".D c #319898", "aU c #327979", ".z c #329797", "bw c #329898", "#T c #339999", "#U c #339d9d", ".T c #33a6a6", "bh c #349c9c", "bi c #349f9f", ".Q c #34a6a6", "#N c #357b7b", "ag c #367b7b", "ao c #367c7c", "bH c #397e7e", "#a c #3a7e7e", "#d c #3a7f7f", "as c #3aadad", "#5 c #3bb4b4", "a2 c #3c7f7f", "aG c #3c8080", "aE c #3d8080", "#7 c #3db6b6", "#Y c #3dbaba", "bb c #3dbbbb", "#G c #3e8181", "#6 c #3ebebe", "bf c #3ebfbf", "a9 c #3fbebe", "#1 c #3fc0c0", "a. c #3fc2c2", "ai c #40bfbf", "a8 c #40c1c1", "b. c #41c1c1", "ah c #41c2c2", "an c #41c3c3", "#0 c #42c4c4", "at c #42c7c7", "#9 c #42c9c9", "#c c #42cbcb", "bc c #43c5c5", "#3 c #43c6c6", "#V c #43c7c7", "ad c #43c9c9", "ac c #43caca", "bq c #43cbcb", "aS c #448686", "aR c #448787", "#f c #44caca", "bo c #44cbcb", "#i c #44cdcd", "#Z c #44cece", "bF c #44cfcf", "#b c #44d0d0", "#H c #44d1d1", "#I c #45cbcb", "bn c #45cdcd", "#h c #45cece", "#g c #45d0d0", ".V c #468888", "#P c #46d0d0", "#n c #46d7d7", "bQ c #478686", "bO c #478787", ".P c #478989", "#o c #47d1d1", "bj c #47d3d3", "bm c #47d4d4", "aL c #488787", ".O c #488888", "bl c #48dbdb", "bR c #498888", "#Q c #49dfdf", "aK c #4a8989", ".L c #4a8a8a", "aQ c #4adfdf", "ax c #4ae0e0", "aJ c #4b8989", "a1 c #4d8989", "bM c #4f8b8b", "b# c #4feaea", "ab c #4febeb", "ae c #4fecec", "bP c #4feded", ".A c #4feeee", ".C c #4fefef", ".M c #4ff0f0", ".N c #4ff1f1", ".X c #4ff2f2", ".4 c #4ff8f8", ".7 c #4ff9f9", ".2 c #508b8b", "bN c #508c8c", ".W c #508d8d", "bG c #50efef", "bB c #50f0f0", ".U c #50f1f1", ".1 c #50f2f2", "bp c #50f8f8", "#t c #50f9f9", "aB c #51f4f4", "#s c #51f9f9", "#v c #51fafa", "#S c #51fbfb", "#. c #52f8f8", "bu c #52f9f9", "aZ c #538f8f", "aX c #53f9f9", "#m c #53fdfd", "aO c #548f8f", "bx c #54fbfb", "#p c #54fcfc", "#E c #54fefe", "a0 c #559090", ".B c #55ffff", "aN c #568f8f", "bS c #5f9696", ".K c #619696", ".H c #629797", "b6 c #669999", ".l c #679a9a", "b5 c #679b9b", "#2 c #6a9e9e", "be c #6b9d9d", ".g c #6b9e9e", "#4 c #6fa3a3", "aP c #72a2a2", ".b c #75a5a5", "bA c #7caaaa", "bC c #7cabab", "#r c #7eacac", "#w c #80abab", "a# c #82abab", ".e c #82adad", "#W c #82afaf", ".c c #83adad", "bs c #84aeae", "bE c #84b0b0", "bg c #85b0b0", "ba c #86aeae", "#e c #87aeae", "#J c #88afaf", "#j c #88b2b2", "bK c #8cb4b4", ".f c #8eb3b3", "bL c #8eb4b4", ".a c #8eb6b6", ".3 c #90b6b6", "av c #95bbbb", "az c #96baba", "aT c #9abdbd", "bz c #9abebe", "au c #9ec1c1", "#x c #a0c0c0", "a5 c #a3c4c4", ".d c #a4c3c3", ".s c #a5c5c5", "b0 c #a6c3c3", "ar c #a8c7c7", "aC c #a9c7c7", "bY c #a9c8c8", "a4 c #aac8c8", ".p c #abc6c6", "#8 c #acc8c8", "#M c #adcaca", "aA c #adcbcb", "bd c #aec9c9", ".o c #afcccc", "b4 c #b0cbcb", "by c #b4cdcd", "b2 c #b4d0d0", ".m c #b5cfcf", "#A c #b9d0d0", "#y c #b9d2d2", "bX c #bcd2d2", ".F c #bdd5d5", "aW c #bfd5d5", "bT c #c1d8d8", ".y c #c2d6d6", "aH c #c8dbdb", "a3 c #c9dddd", "#F c #cedfdf", "aD c #cee0e0", "af c #d0e0e0", "## c #d0e2e2", ".8 c #d1dfdf", "#z c #d2e1e1", "#K c #d3e2e2", ".# c #deeaea", "#k c #dfe9e9", "ap c #dfebeb", ".G c #eef4f4", "#L c #eff3f3", "aM c #f9fbfb", "Qt c None", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.#.a.b.c.d.e.b.f.#QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.#.g.h.i.i.i.i.i.i.i.i.j.k.l.#QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.m.h.i.i.i.i.i.i.i.i.i.i.i.i.i.i.j.n.oQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.p.q.i.i.i.i.i.i.i.i.i.i.i.i.i.i.i.i.i.i.i.r.sQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.p.t.i.i.i.i.i.i.i.i.u.v.v.w.v.v.x.i.i.i.i.i.i.i.i.r.sQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.y.t.i.i.i.i.i.i.i.u.z.A.B.B.B.B.B.B.B.C.D.x.i.i.i.i.i.i.i.E.FQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.G.H.i.i.i.i.i.i.i.I.A.B.B.B.B.B.B.B.B.B.B.B.B.B.C.J.i.i.i.i.i.i.i.K.GQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQt.G.L.i.i.i.i.i.i.I.M.B.B.B.B.B.B.B.B.B.B.B.B.B.B.B.B.B.N.J.i.i.i.i.i.i.O.GQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQt.G.P.i.i.i.i.i.I.M.B.B.B.B.B.B.B.C.Q.R.S.R.T.U.B.B.B.B.B.B.B.N.J.i.i.i.i.i.V.GQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQt.G.W.i.i.i.i.x.A.B.B.B.B.B.B.X.Q.Y.Z.i.i.i.i.i.Z.0.T.1.B.B.B.B.B.B.M.x.i.i.i.i.2.GQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQt.3.i.i.i.i.x.4.B.B.B.B.B.N.5.Z.i.i.i.i.i.i.i.i.i.i.i.Z.6.1.B.B.B.B.B.7.x.i.i.i.i.3QtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQt.8.9.i.i.i.x#..B.B.B.B.N.5.Z.i.i.i.i.i.i.i.i.i.i.i.i.i.i.i.Z.6.1.B.B.B.B.7.x.i.i.i.j##QtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQt.G#a.i.i.i.x.4.B.B.B.B#b.Z.i.i.i.i.i.i.u.v.v.v.v.v.x.i.i.i.i.i.i.Z#c.B.B.B.B.4.x.i.i.i#d.GQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQt#e.i.i.i.i#f.B.B.B.B#g.Z.i.i.i.i.i.I.A.B.B.B.B.B.B.B.C.J.i.i.i.i.i.Z#h.B.B.B.B#i.i.i.i.i#jQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQt#k#l.i.i.i.J#m.B.B.B#n.Z.i.i.i.i.I.M.B.B.B.B.B.B.B.B.B.B.B.N.J.i.i.i.i.Z#o.B.B.B#p.J.i.i.i#q.#QtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQt#r.i.i.i.x#s.B.B.B#t#u.i.i.i.x.A.B.B.B.B.B.B.B.B.B.B.B.B.B.B.B.M.x.i.i.i#u#v.B.B.B.7.x.i.i.i#wQtQtQtQtQtQtQtQt", "QtQtQtQt.##x#y#z#A#q.i.i.i#f.B.B.B#p#B.i.i.i.x.4.B.B.B.B.B.N.Q#C#C#C.T.1.B.B.B.B.B.7.x.i.i.i#D#E.B.B.B#i.i.i.i#q.#QtQtQtQtQtQtQt", "QtQt#F#G#q.i.i.i.i.i.i.i.x.7.B.B.B#H.i.i.i.x#..B.B.B.B.N.5.Z.i.i.i.i.i.Z.6.1.B.B.B.B.7.x.i.i.i#I.B.B.B.7.x.i.i.i#JQtQtQtQtQtQtQt", "Qt#K#q.i.i.i.i.i.i.i.i.i#f.B.B.B#t#u.i.i.x.4.B.B.B.B#b.Z.i.i.i.i.i.i.i.i.i.Z#c.B.B.B.B.4.x.i.i#u#t.B.B.B#i.i.i.i.q#LQtQtQtQtQtQt", "#M.j.i.i.i.i.i.i.i.i.i.x.7.B.B.B#H.i.i.i#f.B.B.B.B#g.Z.i.i.i.i.i.i.i.i.i.i.i.Z#h.B.B.B.B#i.i.i.i#I.B.B.B.7.x.i.i.i.8QtQtQtQtQtQt", "#N.i.i.i.i#O.w#O#O#O#O#P.B.B.B#t#u.i.i.x.7.B.B.B#Q#R#O#O#O#O.i.i.i.i.j.i.i.i.i.Z#o.B.B.B#S#T#O#O#U#S.B.B.B#V.i.i.i#WQtQtQtQtQtQt", "#X.i.i.i#Y.B.B.B.B.B.B.B.B.B.B#Z.i.i.i#0.B.B.B.B.B.B.B.B.B.B#1.i.i.i#X#2.i.i.i.i#u#t.B.B.B.B.B.B.B.B.B.B.B#3.i.i.i#4QtQtQtQtQtQt", ".i.i.i#5.B.B.B.B.B.B.B.B.B.B.B#1.i.i.i#6.B.B.B.B.B.B.B.B.B.B.B#7.i.i.i#8#A.i.i.i.i#9.B.B.B.B.B.B.B.B.B.B.Ba..i.i.ia#QtQtQtQtQtQt", "#X.i.iaaab.B.B.B.B.B.B.B.B.Bac.Z.i.i.i.Zad.B.B.B.B.B.B.B.B.Baeaa.i.i#X.#Qt.l.i.i.i.Zad.B.B.B.B.B.B.B.B.Bac.Z.i.i.iafQtQtQtQtQtQt", "ag.i.i.i.Zah.B.B.B.Baiajakal.Z.i.i.i.i.i.Zamakajai.B.B.B.Ban.Z.i.i.iao#LQtap#q.i.i.i.Zamakajajajajajakal.Z.i.i.iaq#LQtQtQtQtQtQt", "ar.i.i.i.i.ias.B.B.B.Bat.i.i.i.i.i.i.i.i.i.i.iat.B.B.B.Bas.i.i.i.i.i#8QtQtQtau.i.i.i.i.i.i.i.i.i.i.i.i.i.i.i.i.i#8QtQtQtQtQtQtQt", "Qtav.i.i.i.i.iaw.B.B.B.Baxay.i.i.i.i.i.i.iayax.B.B.B.Baw.i.i.i.i.iazQtQtQtQtQt#K.9.i.i.i.i.i.i.i.i.i.i.i.i.i.9#KQtQtQtQtQtQtQtQt", "QtQtaA.i.i.i.i.iamaB.B.B.BaBam.i.i.i.i.iamaB.B.B.BaBam.i.i.i.i.iaCQtQtQtQtQtQtQtaDaEaF.i.i.i.i.i.i.i.i.iaFaGaHQtQtQtQtQtQtQtQtQt", "QtQtQt.#aI.i.i.i.iayax.B.B.B.Baw.i.i.iaw.B.B.B.Baxay.i.i.i.iaI.#QtQtQtQtQtQtQtQtQtQt.#aJ.i.i.i.i.i.i.iaK.#QtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQt.GaL.i.i.i.i.iat.B.B.B.Bas.ias.B.B.B.Bat.i.i.i.i.iaL.GQtQtQtQtQtQtQtQtQtQtaMaN.i.i.i.i.i.i.i.i.iaOaMQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtaMaP.i.i.i.i.ias.B.B.B.BaQ.B.B.B.Bas.i.i.i.i.iaPaMQtQtQtQtQtQtQtQtQtQt.GaR.i.i.i.i.iaj.i.i.i.i.iaS.GQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtaT.i.i.i.i.iaw.B.B.B.B.B.B.Baw.i.i.i.i.iaTQtQtQtQtQtQtQtQtQtQtQt.#aU.i.i.i.iaV.M.B.NaV.i.i.i.iaU.#QtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtaW.i.i.i.i.iam#s.B.B.BaXam.i.i.i.i.iaWQtQtQtQtQtQtQtQtQtQtQtaW.i.i.i.i.iam#s.B.B.BaXam.i.i.i.i.iaWQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQt.#aU.i.i.i.iaY.U.B.1aY.i.i.i.iaU.#QtQtQtQtQtQtQtQtQtQtQtaT.i.i.i.i.iaw.B.B.B.B.B.B.Baw.i.i.i.i.iaTQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQt.GaR.i.i.i.i.Zaj.Z.i.i.i.iaS.GQtQtQtQtQtQtQtQtQtQtaMaP.i.i.i.i.ias.B.B.B.BaQ.B.B.B.Bas.i.i.i.i.iaPaMQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtaMaZ.i.i.i.i.i.i.i.i.ia0aMQtQtQtQtQtQtQtQtQtQt.GaL.i.i.i.i.iat.B.B.B.Bas.ias.B.B.B.Bat.i.i.i.i.iaL.GQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQt.#a1.i.i.i.i.i.i.ia1.#QtQtQtQtQtQtQtQtQtQt.#aI.i.i.i.iayax.B.B.B.Baw.i.i.iaw.B.B.B.Baxay.i.i.i.iaI.#QtQt", "QtQtQtQtQtQtQtQtQtQt#F#GaF.i.i.i.i.i.i.i.i.iaFa2a3QtQtQtQtQtQtQt#M.i.i.i.i.iamaB.B.B.BaBam.i.i.i.i.iamaB.B.B.BaBam.i.i.i.i.ia4Qt", "QtQtQtQtQtQtQtQtQt#KaF.i.i.i.i.i.i.i.i.i.i.i.i.i.j#KQtQtQtQtQtaz.i.i.i.i.iaw.B.B.B.Baxay.i.i.i.i.i.i.iayax.B.B.B.Baw.i.i.i.i.iav", "QtQtQtQtQtQtQtQt#M.j.i.i.i.i.i.i.i.i.i.i.i.i.i.i.i.ia5QtQtQt#Ma6.i.i.i.ias.B.B.B.Bat.i.i.i.i.i.i.i.i.i.i.iat.B.B.B.Bas.i.i.i.i.j", "QtQtQtQtQtQtQt#La7.i.i.i.i.u.w#O#O#O#O#O#O.x.i.i.i.i#qapQt#L#N.i.i.i.ia8.B.B.B.Ba9#O#O.x.i.i.i.i.i.i.i.u.w#Oa9.B.B.B.Ba8.i.i.i.i", "QtQtQtQtQtQtQtaf.i.i.i.ib..B.B.B.B.B.B.B.B.B#3.i.i.i.i.lQt.##X.i.iayb#.B.B.B.B.B.B.B.B.B#3.i.i.i.i.ib..B.B.B.B.B.B.B.B.Baeay.i.i", "QtQtQtQtQtQtQtba.i.i.ibb.B.B.B.B.B.B.B.B.B.B.Bbc.i.i.i.i#Abd.i.i.i#5.B.B.B.B.B.B.B.B.B.B.Ba9.i.i.ibb.B.B.B.B.B.B.B.B.B.B.B#7.i.i", "QtQtQtQtQtQtQt.b.i.i.ian.B.B.B.B.B.B.B.B.B.B.B.7.x.i.i.ia6be#X.i.i.Zbf.B.B.B.B.B.B.B.B.B.B#V.i.i.iad.B.B.B.B.B.B.B.B.B.Ba8.Z.i.i", "QtQtQtQtQtQtQtbg.i.i.iat.B.B.B#Sbhajajbi#S.B.B.Bbj.i.i.i.i.i.j.i.i.i.Zbkakajajajbl.B.B.B#t#u.i.i.x.7.B.B.Bbmajajajajakaj.Z.i.i.i", "QtQtQtQtQtQtQt.8.i.i.i#u#t.B.B.Bbn.i.i.i#Z.B.B.B.Bbo.i.i.i.i.i.i.i.i.i.i.i.i.i#V.B.B.B.Bbn.i.i.i#f.B.B.B#t#u.i.i.i.i.i.i.i.i.i.i", "QtQtQtQtQtQtQt#L.n.i.i.i#Z.B.B.B.7.x.i.i#ubp.B.B.B.Bbq.i.i.i.i.i.i.i.i.i.i.i#9.B.B.B.B#t#u.i.i.x.7.B.B.Bbn.i.i.i.i.i.i.i.i.ibr#K", "QtQtQtQtQtQtQtQtbs.i.i.i#u#t.B.B.Bbn.i.i.ibt#t.B.B.B.B.N.J.i.i.i.i.i.i.i.v.C.B.B.B.Bbubt.i.i.i#f.B.B.B#t#u.i.i.i.i.i.i.i#qaGaHQt", "QtQtQtQtQtQtQtQt.##q.i.i.i#Z.B.B.B#m.J.i.i.ibv#t.B.B.B.B.B.Nbw.I.I.I.z.C.B.B.B.B.B#tbt.i.i.i.Jbx.B.B.Bbn.i.i.i#qby#F.mbz.#QtQtQt", "QtQtQtQtQtQtQtQtQtbA.i.i.i#u#t.B.B.B#s.x.i.i.ibtbB.B.B.B.B.B.B.B.B.B.B.B.B.B.B.B.Mbt.i.i.i.x.7.B.B.B#v#u.i.i.ibCQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQt.##q.i.i.i#B#p.B.B.Bbj.i.i.i.i.ZbD.U.B.B.B.B.B.B.B.B.B.B.B.X.5.Z.i.i.i.i#b.B.B.B#m#D.i.i.i#X#kQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtbE.i.i.i.ibF.B.B.B.Bbo.i.i.i.i.i.ZbDbG.B.B.B.B.B.B.B.M.5.Z.i.i.i.i.i#V.B.B.B.Bbn.i.i.i.ibaQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQt.GbH.i.i.i#ubp.B.B.B.Bbq.i.i.i.i.i.i.ZbIbDbDbDbDbDbJ.Z.i.i.i.i.i.i#9.B.B.B.B#t#u.i.i.i#a.GQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQt##.j.i.i.ibt#t.B.B.B.B.N.J.i.i.i.i.i.i.i.i.i.i.i.i.i.i.i.i.i.v.C.B.B.B.Bbubt.i.i.iaF.8QtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtbK.i.i.i.ibv#t.B.B.B.B.B.N.J.i.i.i.i.i.i.i.i.i.i.i.i.i.v.C.B.B.B.B.B#tbt.i.i.i.ibLQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQt.GbM.i.i.i.ibtbB.B.B.B.B.B.B.Nbw.x.i.i.i.i.i.i.i.u.z.C.B.B.B.B.B.B.Mbt.i.i.i.ibN.GQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQt.GbO.i.i.i.i.ZbD.U.B.B.B.B.B.B.B.Mbw.I.w.I.zbP.B.B.B.B.B.B.B.X.5.Z.i.i.i.ibQ.GQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQt.GbR.i.i.i.i.i.ZbD.U.B.B.B.B.B.B.B.B.B.B.B.B.B.B.B.B.B.X.5.Z.i.i.i.i.i.O.GQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.G.H.i.i.i.i.i.i.ZbDbG.B.B.B.B.B.B.B.B.B.B.B.B.B.M.5.Z.i.i.i.i.i.ibS.GQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtbTbU.i.i.i.i.i.i.ZbV.TbB.B.B.B.B.B.B.B.M.T.Y.Z.i.i.i.i.i.ibWbXQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtbY.r.i.i.i.i.i.i.i.ZbIbDbZ.SbZbDbJ.Z.i.i.i.i.i.i.ibWb0QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtbY.r.i.i.i.i.i.i.i.i.i.i.i.i.i.i.i.i.i.i.ib1b0QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtb2b3.j.i.i.i.i.i.i.i.i.i.i.i.i.i.i.nb4QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.#b5.r.j.i.i.i.i.i.i.i.i.hb6.#QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt"}; """ __icon__ = """ /* XPM */ static char *dummy[]={ "64 64 3 1", ". c None", "# c #0000ff", "a c #ff0000", "................................................................", "................................................................", "................................................................", "...................#............................................", "..................###...........................................", ".................#####..........................................", ".................#####..........................................", ".................#####..........................................", "................#####...........................................", "................#####...........................................", "................#####...........................................", "...............#####............................................", "...............#####............................................", "...............#####............................................", "..............#####...aaaaaaaa........................aaaaaaaaa.", "..............#####..aaaaaaaaaaa....................aaaaaaaaaaaa", "..............#####.aaaaaaaaaaaaa...................aaaaaaaaaaaa", ".............#####...aaaaaaaaaaaa...................aaaaaaaaaaaa", ".............#####....aaaaaaaaaaa..................aaaaaaaaaaaa.", ".............#####........aaaaaaaa.................aaaaaaaa.....", "............#####.........aaaaaaaa.................aaaaaaaa.....", "............#####.........aaaaaaaa................aaaaaaaaa.....", ".....#################....aaaaaaaaa...............aaaaaaaaa.....", "....###################...aaaaaaaaa...............aaaaaaaaa.....", "...#####################..aaaaaaaaa..............aaaaaaaaaa.....", "....###################...aaaaaaaaaa.............aaaaaaaaaa.....", ".....#################....aaaaaaaaaa.............aaaaaaaaaa.....", ".........######...........aaaaaaaaaa............aaaaaaaaaaa.....", ".........######...........aaaaaaaaaaa...........aaaaaaaaaaa.....", "........######............aaaaaaaaaaa...........aaaaaaaaaaa.....", "........######............aaaaaaaaaaa..........aaaaaaaaaaaa.....", ".......#######............aaaaaaaaaaaa.........aaaaaaaaaaaa.....", ".......######.............aaaaaaaaaaaa.........aaaaaaaaaaaa.....", "......#######.............aaaaaaaaaaaa........aaaaaaaaaaaaa.....", "......#######.............aaaaaaaaaaaaa.......aaaaaaaaaaaaa.....", ".....#######..............aaaaa.aaaaaaa.......aaaaa.aaaaaaa.....", ".....#######..............aaaaa.aaaaaaa......aaaaaa.aaaaaaa.....", "....########..............aaaaa.aaaaaaaa.....aaaaaa.aaaaaaa.....", "...#########..............aaaaa..aaaaaaa.....aaaaa..aaaaaaa.....", "...########...............aaaaa..aaaaaaa....aaaaaa..aaaaaaa.....", "..#########.............#.aaaaa..aaaaaaaa...aaaaaa..aaaaaaa.....", ".##########...........####aaaaa...aaaaaaa...aaaaa...aaaaaaa.....", ".#########...........#####aaaaa...aaaaaaa..aaaaaa...aaaaaaa.....", "..########...........#####aaaaa...aaaaaaaa.aaaaaa...aaaaaaa.....", "...#######..........######aaaaa....aaaaaaa.aaaaa....aaaaaaa.....", "....#####..........#######aaaaa....aaaaaaaaaaaaa....aaaaaaa.....", "...######..........######.aaaaa....aaaaaaaaaaaaa....aaaaaaa.....", "...######.........######..aaaaa.....aaaaaaaaaaa.....aaaaaaa.....", "...#####..........######..aaaaa.....aaaaaaaaaaa.....aaaaaaa.....", "...#####.........######...aaaaa.....aaaaaaaaaaa.....aaaaaaa.....", "...#####.......#######....aaaaa......aaaaaaaaa......aaaaaaa.....", "...######....#########....aaaaa......aaaaaaaaa......aaaaaaa.....", "...##################.....aaaaa......aaaaaaaaa......aaaaaaa.....", "....################......aaaaa.......aaaaaaa.......aaaaaaa.....", ".....#############....aaaaaaaaaaaa....aaaaaaa....aaaaaaaaaaaaaa.", "......##########.....aaaaaaaaaaaaaa...aaaaaaa...aaaaaaaaaaaaaaaa", "........######......aaaaaaaaaaaaaaaa...aaaaa...aaaaaaaaaaaaaaaaa", ".....................aaaaaaaaaaaaaa....aaaaa....aaaaaaaaaaaaaaaa", "......................aaaaaaaaaaaa.......a.......aaaaaaaaaaaaaa.", "................................................................", "................................................................", "................................................................", "................................................................", "................................................................"}; """ import_sketch_icon = """ /* XPM */ static char *dummy[]={ "64 64 392 2", "## c #008686", "dv c #008787", "c9 c #008888", "dj c #008989", "dw c #008a8a", "#D c #008b8b", "#Q c #008c8c", "au c #008d8d", "aM c #008e8e", "bb c #008f8f", "bj c #009090", "bx c #009191", "bH c #009292", "ds c #009393", "bX c #009494", "b5 c #009595", "dX c #009696", "ce c #009797", "cn c #009898", "co c #009a9a", "cf c #009c9c", "b6 c #009e9e", "bO c #00a0a0", "bI c #00a2a2", "by c #00a4a4", "dT c #00a5a5", "bk c #00a6a6", "dJ c #00a7a7", "dZ c #00a8a8", "a3 c #00a9a9", "aV c #00aaaa", "dQ c #00acac", "#a c #00adad", "#r c #00aeae", "#q c #00afaf", "ak c #00b0b0", "#. c #00b1b1", "ab c #00b2b2", "#2 c #00b4b4", "#R c #00b5b5", "dG c #00b6b6", "#E c #00b7b7", "#C c #00b9b9", "#P c #00baba", "#1 c #00bbbb", "c4 c #00bcbc", "aa c #00bdbd", "at c #00bebe", "dc c #00bfbf", "aL c #00c0c0", "ba c #00c2c2", "bG c #00c3c3", "bW c #00c4c4", "dz c #00c5c5", ".M c #00c6c6", "dB c #01c6c6", "dR c #02c7c7", "#B c #03c3c3", "dO c #06c7c7", "du c #0bc8c8", "dM c #0cc9c9", "#O c #0dbbbb", "di c #10b4b4", "dK c #11caca", "do c #12caca", "#0 c #17b4b4", "a# c #19b3b3", "c7 c #1ba8a8", "as c #1bb1b1", "dI c #1bcccc", "aB c #1dafaf", "dt c #1ecece", "c3 c #1faeae", "aK c #20adad", "#b c #21acac", "aU c #22acac", "dC c #23cece", ".9 c #25cccc", "b# c #26a9a9", "dm c #26cece", "d. c #27a6a6", "df c #28cece", "dp c #28cfcf", "db c #28d0d0", "bi c #29a7a7", "bw c #2ea3a3", "c2 c #319f9f", "cC c #339292", "bF c #339f9f", "cy c #34a5a5", "cx c #368787", "bV c #379b9b", "b4 c #3e9696", "b7 c #3f7b7b", "dA c #3fd4d4", "cd c #439292", "ct c #468c8c", "cB c #48b5b5", "dY c #48d7d7", "cu c #49d5d5", "cm c #4a8c8c", "bz c #4d6868", "cg c #50c5c5", "dF c #51d9d9", "cp c #52d8d8", "c5 c #55d9d9", "dd c #5bd9d9", "dx c #5cdbdb", "dy c #5ddada", "dW c #5ddbdb", "cH c #5fd7d7", "d2 c #63dcdc", "bP c #687575", "dH c #68dcdc", "cM c #68dede", "d1 c #6adfdf", "cU c #6b7474", "dl c #6edede", "bJ c #706f6f", "c1 c #70dfdf", "#F c #727c7c", "bl c #75c8c8", "d0 c #78e0e0", "c6 c #78e1e1", "#N c #7a3131", "dk c #815959", "de c #81e3e3", "#s c #853e3e", "cP c #892a2a", "a4 c #89e5e5", "cD c #8e1212", "#m c #8f0606", "cs c #901414", "#e c #910505", "#t c #910d0d", ".j c #920404", "b9 c #920505", "#w c #920e0e", "aW c #92e7e7", ".o c #930303", "aE c #930404", "an c #930707", "az c #930c0c", "aG c #940c0c", "#X c #941010", "cw c #960e0e", ".k c #970000", ".i c #971717", ".p c #971818", "eb c #980000", "#l c #981e1e", "ec c #990000", ".B c #991b1b", "#f c #991d1d", ".G c #992020", ".N c #993e3e", "c8 c #9a0c0c", ".t c #9a1b1b", ".U c #9a4d4d", "aN c #9ae8e8", ".T c #9be9e9", "bM c #9c1919", "aO c #9d1919", "d3 c #9de9e9", ".l c #9f0000", ".n c #a00000", "cO c #a00a0a", "#A c #a0d0d0", "aC c #a0eaea", "bs c #a20000", ".u c #a20202", "bt c #a30000", "cb c #a31010", "#9 c #a31414", "cj c #a31515", "ae c #a33636", ".m c #a40000", "b1 c #a50000", "ag c #a51414", "bT c #a60000", "aI c #a60404", "ax c #a63d3d", "dh c #a65252", "b2 c #a70000", "cA c #a70101", "#L c #a70202", "aP c #a70404", "#Z c #a72222", "aX c #a83838", "ap c #a83d3d", "al c #a8ebeb", ".Z c #a90000", "cY c #a9ecec", ".Q c #aa0505", ".H c #ac0000", ".J c #ad0000", ".0 c #ae0000", ".h c #ae4a4a", ".q c #ae4c4c", ".Y c #af0000", "ai c #af0101", "cV c #af3e3e", "d# c #b02222", "bY c #b05151", "ac c #b0eded", "cT c #b23c3c", "#I c #b25454", "dD c #b2eeee", ".L c #b4eeee", "#U c #b50202", "#p c #b5eeee", "da c #b60000", "cE c #b65b5b", "#g c #b65d5d", "#k c #b65e5e", "cr c #b76060", "#3 c #b7efef", "b8 c #b80000", "#J c #b80101", "ck c #b86060", "#Y c #b86565", "dV c #b9f0f0", "b0 c #ba0000", "a7 c #ba0101", "#7 c #ba6565", "bc c #ba6767", "a2 c #bb0202", "bR c #bb6666", "bd c #bb6868", "a9 c #bc0000", "#S c #bcecec", "cW c #bd0000", "aS c #bd0303", "bp c #be0202", "aZ c #be0303", "av c #be6a6a", "e# c #bf6464", "e. c #bf6666", "ed c #bf6c6c", "ea c #bf6e6e", "bA c #bf7171", "cX c #bf7474", "#d c #c00000", "d8 c #c07272", "#u c #c07575", "bL c #c07676", "#x c #c10000", "cF c #c10101", "bn c #c17171", "a5 c #c17474", "bC c #c17777", "d7 c #c17979", "#y c #c20101", "bN c #c27a7a", "bg c #c37676", "br c #c37b7b", ".6 c #c46b6b", "dq c #c4caca", "aw c #c50000", "cR c #c56d6d", "bZ c #c60000", ".5 c #c60101", "cQ c #c60a0a", "#o c #c66f6f", "cL c #c67070", "cl c #c70707", "a6 c #c80000", "bo c #c90000", "#6 c #c90303", "d5 c #c9f3f3", "bu c #ca0000", "cq c #ca0202", ".s c #ca8888", ".C c #ca8b8b", ".E c #caf3f3", "bQ c #cb0000", "bD c #cd0000", "aR c #cd9090", "a0 c #cd9191", "cN c #ce0202", "bq c #ce9494", "dU c #cef4f4", "#n c #cf0000", ".R c #cf9494", "#4 c #cf9595", "#z c #cf9696", ".1 c #d00000", "cG c #d09595", "c. c #d09898", "a1 c #d09999", "bK c #d10000", "a8 c #d19898", ".X c #d20000", "#c c #d22323", "ch c #d29c9c", ".v c #d30000", ".A c #d40000", "a. c #d41d1d", "d6 c #d4a0a0", ".8 c #d4f5f5", "d4 c #d5f6f6", "dS c #d7f6f6", "#h c #d8a3a3", "#T c #d8a7a7", "dr c #d8f7f7", "#j c #d9a3a3", "dg c #d9c7c7", "aq c #da0000", "aj c #da1d1d", "cK c #daa5a5", "cJ c #daa6a6", "ao c #daa8a8", "ah c #daaaaa", "ca c #daacac", "ci c #db0000", ".F c #dbcaca", "af c #dc0000", ".P c #de0000", "ar c #de1919", "#i c #dfbcbc", "dn c #dff7f7", "ef c #e1bebe", "aA c #e21717", "aY c #e40000", "aH c #e4c2c2", "bS c #e4c5c5", "bB c #e50000", "c# c #e5c5c5", "dE c #e5caca", "aJ c #e61414", "dP c #e6f9f9", "cI c #e70606", ".g c #e7c6c6", ".r c #e7c7c7", ".K c #e7c8c8", "bf c #e7caca", ".d c #e8cccc", "aT c #ea1111", "#M c #ead4d4", ".c c #ebd4d4", "cZ c #ebfbfb", ".e c #ecd4d4", "b. c #ed0e0e", ".2 c #ee0000", ".7 c #eed9d9", "cz c #eee6e6", ".W c #ef0000", "bh c #ef0c0c", "am c #efdddd", "dN c #effbfb", "#W c #f00000", "cc c #f10404", "bv c #f10a0a", "#8 c #f1dede", ".b c #f1e2e2", ".D c #f1fbfb", "ee c #f2e0e0", ".w c #f30000", "bE c #f30a0a", "aD c #f3e3e3", "bm c #f3e4e4", "#G c #f3e5e5", "be c #f3e6e6", "dL c #f3fdfd", ".I c #f40000", "#H c #f4e6e6", ".z c #f50000", "bU c #f50808", "#V c #f60000", "#K c #f70000", "b3 c #f70606", "#v c #f7ebeb", ".a c #f7efef", ".4 c #f80000", "aF c #f90000", ".S c #f9f1f1", "cv c #f9fdfd", ".3 c #fa0000", ".V c #fb0000", "ay c #fbf5f5", "d9 c #fbf7f7", "#5 c #fc0000", ".x c #fd0000", "cS c #fd0202", "ad c #fdf7f7", ".f c #fdf9f9", ".# c #fdfbfb", "c0 c #fdffff", ".O c #fe0000", ".y c #ff0000", "aQ c #fffdfd", "Qt c None", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.#.a.b.c.d.e.b.a.#QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.f.g.h.i.j.k.l.m.n.k.o.p.q.r.fQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.s.t.u.v.w.x.y.y.y.y.y.x.z.A.u.B.CQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.D.E.F.G.H.I.y.y.y.y.y.y.y.y.y.y.y.y.y.z.J.G.KQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.L.M.M.M.N.O.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.O.P.Q.R.SQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.T.M.M.M.M.M.U.y.y.y.V.W.X.Y.Z.0.1.2.3.y.y.y.y.y.4.5.6.7QtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.8.9.M#.###a.M#b#c.W#d#e#f#g#h#i#j#k#l#m#d.W.y.y.y.y.V#n#o.SQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt#p.M#q#######r.M#s#t#u#vQtQtQtQtQtQtQt#v#u#w#x.y.y.y.y.V#y#zQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt#A#B#C#D#####E.M#F#GQtQtQtQtQtQtQtQtQtQtQt#H#I#J#K.y.y.y.3#L#MQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.f#N#O#P#Q#####R.M#SQtQtQtQtQtQtQtQtQtQtQtQtQt.f#T#U#V.y.y.y#W#X.fQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt#Y#Z#0#1#Q#####2.M#3QtQtQtQtQtQtQtQtQtQtQtQtQtQtQt#4#x#5.y.y.O#6#7QtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt#8#9a.a#aa#Q####ab.MacQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtadaeaf.y.y.y#Kag#8QtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.#ahaiaja#aa#Q####ak.MalQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtaman.4.y.y.yaiao.#QtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtapaqarasatau####ak.MalQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtavaw.O.y.yaqaxQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtayaz.VaAaBatau#####r.MaCQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtaDaE#5.y.yaFaGayQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtaHaI.xaJaKaLaM#####a.MaNQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.SaO.W.y.y.xaPaHQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtaQaRaS#5aTaUaLaM####aV.MaWQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtayaXaY.y.y#5aZa0aQQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQta1a2.xaTaUaLaM####a3.Ma4QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQta5a6.y.y.Oa7a8QtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.Ra9.yb.b#babb####a3.Ma4QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtbc#n.y.y.y#nbdQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtbe.dbfbfbfbfbfbfbfbfbfbfbfbga2.Obhbibabj####bk.MblbfbfbfbfbfbfbfbfbfbfbfbfbfbfbmQtQtQtbnbo.y.y.xbpbqQtQtQtQtQtQt", "QtQtQtQtQtQtQtbr.kbsbtbtbtbtbtbtbtbtbtbtbtbtbu#5bvbwbabx####by.Mbzbtbtbtbtbtbtbtbtbtbtbtbtbtbt.kbAQtayaXbB.y.y#5aSaRaQQtQtQtQtQt", "QtQtQtQtQtQtbCbD.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.ybEbFbGbH####bI.MbJ.y.y.y.y.y.y.y.y.y.y.y.y.y.y.ybKbL.SbM.W.y.y.xaIaHQtQtQtQtQtQt", "QtQtQtQtQtbNa6.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.ybEbFbGbH####bO.MbP.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.ybQbR.o#5.y.y.VaGayQtQtQtQtQtQt", "QtQtQtQtQtbSbT.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.ybUbVbWbX####bO.MbP.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.ZbYbZ.O.y.yaqapQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb0b1b1b1b1b1b1b1b1b1b1b1b2#xb3b4bWb5####b6.Mb7b1b1b1b1b1b1b1b1b1b1b1b1b8.y.y.yb1b9aF.y.y.yaiah.#QtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c.c#c#c#c#c#c#c#c#c#c#cacbcccdbWce####cf.Mcgc#c#c#c#c#c#c#c#c#c#c#chbt.y.y.yb1ci.O.y.y#Kcj#8QtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtckclcmbWcn####co.McpQtQtQtQtQtQtQtQtQtQtQtbfbt.y.y.ybK#5.y.y.OcqcrQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQt.fcsctbWcn####cnbWcucvQtQtQtQtQtQtQtQtQtQtbfbt.y.y.y#K.y.y.y#Wcw.fQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQt#Mcx.Mco####cnbWcyczQtQtQtQtQtQtQtQtQtQtbfbt.y.y.y.y.y.y.VcA.eQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQtcB.Mcf####cebWcCcD#u#vQtQtQtQtQtQtQt#vcEbt.y.y.y.y.y.VcFcGQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQtcH.Mb6####b5bWb4cI#d#m#l#kcJ#icK#g#f#e#d.w.y.y.y.y.V#ncL.SQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQtcM.MbO####bXbWbVbU.y.3.2cN.HcOcPcQ.W.V.y.y.y.y.y.3.5cR.7QtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQt.D.E.DQtQtQtQtQtcM.MbO####bHbGbFbE.y.ycScTcU.M.M.McV.y.y.y.y.y.ycWcX.SQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQt.L.M.M.McYcZc0QtQtc1.MbI####bHbGc2bE.y.y.yc3.MaLc4.M.M.U.y.y.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQt.T.M.M.M.M.Mc5QtQtQtc6.Mby####bxbac7c8.A.zc3.Matc9###2.Md.d#da.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQt.8db.M#2##bXdc.MddQtQtde.Mbk####bjbadfdgdhdi.Mbadj#######a.Mdkbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQt.E.M#r######bX.M.MdlQta4.Ma3####bbbadmdndo.MaLc9######c9dcdpdqbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtdrdpdcaM######ds.M.Mdla4.Ma3####aMaLdtdu.Matdv######dwat.Mdxbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtdy.Mdzds######ds.M.MdA.MaV####aMaLdB.M#1########c9ba.MdCdDdEbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtc0cZdF.M.Mds######ds.M.M.M#a####aMaL.MdG########dvaL.MdCQtc0bfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtdH.M.Mds######ds.M.M#r####auatab##########at.MdIQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtdl.M.Mds######ds.Mak####audJ###########1.MdKdLQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtdl.M.Mds######dsak####dj##########dG.MdMdNQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtdl.M.Mds######bb##############ab.MdOdPQtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtdl.M.Mds##################dQ.MdRdSQtQtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtdl.M.Mds##############dT.MdBdUQtQtQtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtdl.M.Mds##########bO.MdBdVQtQtQtQtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtdW.Mdc#Q######dX.M.MaCQtQtQtQtQtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQtdY.MdzdZ##bHdz.Md0QtQtQtQtQtQtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtc0cZd1.Mdz.Mdc.Md2QtQtQtQtQtQtQtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtcY.M.M.Md3d4QtQtQtQtQtQtQtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt.Dd5.DQtc0QtQtQtQtQtQtQtQtQtQtQtbfbt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb1chbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfd6bt.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.yb8btbtbtbtbtbtbtbtbtbtbtbtbtbtbtbtbtbtbtbtbtbtbtbtbtbtbtbtbtbtbtbtbtbtda.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtbfbt.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.yb1c#QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtd7bo.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.ybud8QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtd9e..1.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.y.ybKe#d9QtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQteaeb.Zb1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1.ZecedQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtd9eeefc#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#c#ef#8d9QtQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt", "QtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQtQt"}; """ transformPointsIcon = """ /* XPM */ static char *dummy[]={ "64 64 38 1", "c c #8b0000", "h c #8e0606", "u c #b45c5c", "v c #b55757", "f c #b55e5e", "e c #b65e5e", "i c #c04f00", "n c #c25000", "r c #c25400", "d c #c27878", "x c #c35706", "o c #c45200", "s c #c45600", "y c #c45a05", "A c #c45c06", "H c #c47c78", "G c #c47d79", "b c #c47e7e", "k c #c55500", "D c #c65d05", "B c #c65d06", "E c #c75e05", "J c #d39c9c", "p c #d39e9e", "m c #d3a0a0", "a c #d5a0a0", "I c #d7a7a7", "g c #dbafaf", "l c #dbb1b1", "t c #ddb6b6", "q c #dfb7b7", "# c #f3e4e4", "F c #fbe9e3", "C c #fbebe7", "z c #fbf3f3", "j c #ffaa00", "w c #fffbfb", ". c None", "................................................................", "................................................................", "................................................................", "................................................................", "................................................................", "................................................................", "................................................................", ".................................#a#............................", "................................bcccd...........................", "...............................ecccccf..........................", "..............................ghcijkchl.........................", "..............................mcnjjjocp.........................", "..............................qhrjjjsht.........................", "...............................urjjjsv..........................", "...............................wxjjjyz..........................", "...............................wAjjjBz..........................", "...............................wAjjjBz........#a#...............", ".............#a#...............wCDjEFw.......bcccd..............", "............bcccd................GcH........ecccccf.............", "...........ecccccf.........................ghcijkchl............", "..........ghcijkchl........................mcnjjjocp............", "..........mcnjjjocp........................qhrjjjsht............", "..........qhrjjjsht.........................urjjjsv.............", "...........urjjjsv..........................wxjjjyz.............", "...........wxjjjyz.........#a#..............wAjjjBz.............", "...........wAjjjBz........bcccd.............wAjjjBz.............", "...........wAjjjBz.......ecccccf............wCDjEFw.............", "...........wCDjEFw......ghcijkchl.............GcH...............", ".............GcH........mcnjjjocp...............................", "........................qhrjjjsht...............................", ".........................urjjjsv................................", ".........................wxjjjyz................................", ".........................wAjjjBz................................", ".........................wAjjjBz...#a#..........................", ".........................wCDjEFw..bcccd.........................", "...........................GcH...ecccccf........................", "................................ghcijkchl.......................", "................................mcnjjjocp.......................", "................................qhrjjjsht.......................", ".................................urjjjsv..#a#...................", ".................................wxjjjyz.bcccd..................", ".......................#a#.......wAjjjBzecccccf.................", "......................bcccd......wAjjjBIhcijkchl................", ".....................ecccccf.....wCDjEFJcnjjjocp................", "....................ghcijkchl......GcH.qhrjjjsht................", "....................mcnjjjocp...........urjjjsv.................", "....................qhrjjjsht...........wxjjjyz.................", ".....................urjjjsv............wAjjjBz.................", ".....................wxjjjyz............wAjjjBz.................", ".....................wAjjjBz............wAjjjBz.................", ".....................wAjjjBz............wAjjjBz.................", ".....................wCDjEFw............wCDjEFw.................", ".......................GcH................GcH...................", "................................................................", "................................................................", "................................................................", "................................................................", "................................................................", "................................................................", "................................................................", "................................................................", "................................................................", "................................................................", "................................................................", "................................................................"}; """ #UI_FILE created with QtCreator #rather than deal with distributing a separate ui file #I just copy/paste into here, save it to a temp file #load it in from the temp file, and then delete the temp file UI_FILE = ''' Dialog 0 0 834 718 0 0 0 0 99999 99999 1.000000000000000 false 10 0 811 711 If the text is a direct path to a local file or the name of a system icon, such as "application-python" you can use this button to set it up as the Pixmap text. Use as pixmap You must create a custom toolbar for macros because you cannot edit the standard toolbars. Custom toolbar: Text will appear in the status bar when the mouse pointer is over the toolbar icon or menu text Status text: These are all the macro files in the user's macro directory. Macro name: Make your own icon with the icon maker tool. Make icon 0 0 0 0 65 28 Menu Qt::RightToLeft Selects the currently installed macro for editing or removing Delete the current toolbar Delete Toolbar will appear in all workbenches Browse local file system for an icon file. Many image formats are supported. SVG is scalable, and so is recommended. Qt::LeftToRight Browse If the text is an XPM definition, then you can use this button to save it as a local XPM file and set it up as the Pixmap text. Save XPM 1 0 0 0 64 64 Icon in Pixmap will be displayed here, or if null you get this macro's icon. QFrame::Box icon Qt::AlignCenter keyboard accelerator, e.g. Ctrl + D Shortcut: This text (required) will be shown on the toolbar in lieu of a toolbar icon if one isn't selected. Menu text: Non-Macros: Remove the macro from the toolbar. Does not delete the macro action. Macro If the text is a hyperlink to an online image file, you can use this button to download and store it to your local hard drive. Download Selects active workbench or the Global workbench in the combo box. Note: custom toolbars added to the Global workbench are available to all workbenches, but toolbars added to specific workbenches are only available when those workbenches are active. Active / Global Rename the current toolbar Rename Adds or updates macro to toolbar if there is an active toolbar. Requirements: Menu text must not be empty Must be toolbar / workbench combination selected in the dialog. Macro These are the macros currently installed in the current toolbar, if any. Only macros appear here, not other types of commands that the toolbar might have. Currently installed: Choose a workbench your toolbar will appear in. If you choose Global it will appear in all of them. Workbench: Creates a new custom toolbar New Non-macro Text displayed when the user clicks the ? help button and then clicks the toolbar icon or menu text What's this: This text you're reading now. It appears when the mouse pointer hovers over the toolbar icon or menu text. Tool tip: Extract XPM from the current icon and put it into the Extraction text edit below, where it can then be saved using the Save XPM button. Extract XPM Filter: Non-macro text for displaying the icon. It is in some cases a name of a system icon, and in other cases the path to an icon file. Pixmap: Browse system icons. This button has a right-click context menu System Toggles visibility of toolbar. You can disable the toolbar from here without deleting it. Qt::RightToLeft Active Attempt to extract icon information embedded in macro file as __icon__ or __xpm__ variable. From macro 0 0 Gui::AccelLineEdit widget, select and press your desired shortcut key modifier key combination 16777215 100 If this is a direct hyperlink to a file, you can download and save it with this button, which also sets it up as the Pixmap. Various assorted status messages go here. Black is normal message, Orange is warning, and Red indicates some kind of error. Right-click context menu available for clearing messages. QFrame::Box Messages go here. Gui::AccelLineEdit QWidget
gui::accellineedit.h
''' dlg = CustomToolbarEditor()