Skip to content

Instantly share code, notes, and snippets.

@mwganson
Last active October 27, 2023 22:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mwganson/3464e2d54e859ee94ec8d7ce20c75660 to your computer and use it in GitHub Desktop.
Save mwganson/3464e2d54e859ee94ec8d7ce20c75660 to your computer and use it in GitHub Desktop.
Easily manage custom macro toolbars in FreeCAD
#CustomToolbarManager.FCMacro
#2023, by <TheMarkster>, 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.2023.10.27"
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.makeIconButton.clicked.connect(self.makeSimpleIcon)
self.form.extractXPMButton.clicked.connect(self.extractXPMButtonClicked)
def onLineEditShortcutTextChanged(self):
short = self.form.LineEditShortcut.text()
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.text()
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.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.setText(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.setText(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'<span style="color: black;">{msg}</span><br/>'
elif typeId == "warning":
formattedMsg = f'<span style="color: orange;">{msg}</span><br/>'
elif typeId == "error":
formattedMsg = f'<span style="color: red;">{msg}</span><br/>'
# 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.setText('')
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 = '''<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>834</width>
<height>718</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>99999</width>
<height>99999</height>
</size>
</property>
<property name="windowTitle">
<string/>
</property>
<property name="windowOpacity">
<double>1.000000000000000</double>
</property>
<property name="sizeGripEnabled">
<bool>false</bool>
</property>
<widget class="QWidget" name="gridLayoutWidget">
<property name="geometry">
<rect>
<x>10</x>
<y>0</y>
<width>811</width>
<height>711</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="11" column="5">
<widget class="QPushButton" name="useAsPixmapButton">
<property name="toolTip">
<string>If the text is a direct path to a local file or the name of a system icon, such as &quot;application-python&quot; you can use this button to set it up as the Pixmap text.</string>
</property>
<property name="text">
<string>Use as pixmap</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="customToolbarLabel">
<property name="statusTip">
<string>You must create a custom toolbar for macros because you cannot edit the standard toolbars.</string>
</property>
<property name="text">
<string>Custom toolbar:</string>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="statusTextLabel">
<property name="toolTip">
<string>Text will appear in the status bar when the mouse pointer is over the toolbar icon or menu text</string>
</property>
<property name="whatsThis">
<string/>
</property>
<property name="text">
<string>Status text:</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="macroNameLabel">
<property name="toolTip">
<string>These are all the macro files in the user's macro directory.</string>
</property>
<property name="text">
<string>Macro name:</string>
</property>
</widget>
</item>
<item row="11" column="4">
<widget class="QPushButton" name="makeIconButton">
<property name="toolTip">
<string>Make your own icon with the icon maker tool.</string>
</property>
<property name="text">
<string>Make icon</string>
</property>
</widget>
</item>
<item row="12" column="2">
<widget class="QLineEdit" name="LineEditPixmap"/>
</item>
<item row="5" column="2">
<widget class="QComboBox" name="ComboBoxNonMacros"/>
</item>
<item row="0" column="7">
<widget class="QPushButton" name="menuButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>65</width>
<height>28</height>
</size>
</property>
<property name="toolTip">
<string>Menu</string>
</property>
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="3" column="4">
<widget class="QPushButton" name="selectInstalledButton">
<property name="toolTip">
<string>Selects the currently installed macro for editing or removing</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="4" column="7">
<widget class="QPushButton" name="deleteToolbarButton">
<property name="toolTip">
<string>Delete the current toolbar</string>
</property>
<property name="text">
<string>Delete</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QComboBox" name="ComboBoxWorkbench">
<property name="toolTip">
<string>Toolbar will appear in all workbenches</string>
</property>
</widget>
</item>
<item row="12" column="5">
<widget class="QPushButton" name="selectIconFileButton">
<property name="toolTip">
<string>Browse local file system for an icon file. Many image formats are supported. SVG is scalable, and so is recommended.</string>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="10" column="7">
<widget class="QPushButton" name="saveExtractedButton">
<property name="toolTip">
<string>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.</string>
</property>
<property name="text">
<string>Save XPM</string>
</property>
</widget>
</item>
<item row="10" column="2">
<widget class="QLineEdit" name="LineEditWhatsThis"/>
</item>
<item row="2" column="2">
<widget class="QComboBox" name="ComboBoxCustomToolbar">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item row="12" column="4">
<widget class="QLabel" name="iconLabel">
<property name="minimumSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="toolTip">
<string>Icon in Pixmap will be displayed here, or if null you get this macro's icon.</string>
</property>
<property name="frameShape">
<enum>QFrame::Box</enum>
</property>
<property name="text">
<string>icon</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="shortcutLabel">
<property name="toolTip">
<string>keyboard accelerator, e.g. Ctrl + D</string>
</property>
<property name="text">
<string>Shortcut:</string>
</property>
</widget>
</item>
<item row="9" column="2">
<widget class="QLineEdit" name="LineEditStatusText"/>
</item>
<item row="3" column="2">
<widget class="QComboBox" name="ComboBoxInstalled"/>
</item>
<item row="7" column="0">
<widget class="QLabel" name="menuTextLabel">
<property name="toolTip">
<string>This text (required) will be shown on the toolbar in lieu of a toolbar icon if one isn't selected.</string>
</property>
<property name="text">
<string>Menu text:</string>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QLineEdit" name="LineEditFilter"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="nonMacrosLabel">
<property name="text">
<string>Non-Macros:</string>
</property>
</widget>
</item>
<item row="6" column="7">
<widget class="QPushButton" name="removeMacroFromToolbarButton">
<property name="toolTip">
<string>Remove the macro from the toolbar. Does not delete the macro action.</string>
</property>
<property name="text">
<string>Macro</string>
</property>
</widget>
</item>
<item row="11" column="7">
<widget class="QPushButton" name="openHyperlinkButton">
<property name="toolTip">
<string>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.</string>
</property>
<property name="text">
<string>Download</string>
</property>
</widget>
</item>
<item row="0" column="5">
<widget class="QPushButton" name="activeGlobalButton">
<property name="toolTip">
<string>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.</string>
</property>
<property name="text">
<string>Active / Global</string>
</property>
</widget>
</item>
<item row="3" column="7">
<widget class="QPushButton" name="renameToolbarButton">
<property name="toolTip">
<string>Rename the current toolbar</string>
</property>
<property name="text">
<string>Rename</string>
</property>
</widget>
</item>
<item row="6" column="5">
<widget class="QPushButton" name="addMacroToToolbarButton">
<property name="toolTip">
<string>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.</string>
</property>
<property name="text">
<string>Macro</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="installedLabel">
<property name="toolTip">
<string>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.</string>
</property>
<property name="text">
<string>Currently installed:</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="workbenchLabel">
<property name="toolTip">
<string>Choose a workbench your toolbar will appear in. If you choose Global it will appear in all of them.</string>
</property>
<property name="whatsThis">
<string/>
</property>
<property name="text">
<string>Workbench:</string>
</property>
</widget>
</item>
<item row="8" column="2">
<widget class="QLineEdit" name="LineEditToolTip"/>
</item>
<item row="2" column="7">
<widget class="QPushButton" name="newToolbarButton">
<property name="toolTip">
<string>Creates a new custom toolbar</string>
</property>
<property name="text">
<string>New</string>
</property>
</widget>
</item>
<item row="5" column="7">
<widget class="QPushButton" name="removeNonMacroButton">
<property name="text">
<string>Non-macro</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="whatsThisLabel">
<property name="toolTip">
<string>Text displayed when the user clicks the ? help button and then clicks the toolbar icon or menu text</string>
</property>
<property name="whatsThis">
<string/>
</property>
<property name="text">
<string>What's this:</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="tooltipLabel">
<property name="statusTip">
<string>This text you're reading now. It appears when the mouse pointer hovers over the toolbar icon or menu text.</string>
</property>
<property name="text">
<string>Tool tip:</string>
</property>
</widget>
</item>
<item row="6" column="4">
<widget class="QPushButton" name="selectMacroButton">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="10" column="4">
<widget class="QPushButton" name="extractXPMButton">
<property name="toolTip">
<string>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.</string>
</property>
<property name="text">
<string>Extract XPM</string>
</property>
</widget>
</item>
<item row="7" column="2">
<widget class="QLineEdit" name="LineEditMenuText"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="filterLabel">
<property name="text">
<string>Filter:</string>
</property>
</widget>
</item>
<item row="5" column="5">
<widget class="QPushButton" name="addNonMacroButton">
<property name="text">
<string>Non-macro</string>
</property>
</widget>
</item>
<item row="12" column="0">
<widget class="QLabel" name="pixmapLabel">
<property name="toolTip">
<string>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.</string>
</property>
<property name="text">
<string>Pixmap:</string>
</property>
</widget>
</item>
<item row="10" column="5">
<widget class="QPushButton" name="systemIconButton">
<property name="toolTip">
<string>Browse system icons. This button has a right-click context menu</string>
</property>
<property name="text">
<string>System</string>
</property>
</widget>
</item>
<item row="6" column="2">
<widget class="QComboBox" name="ComboBoxMacroName"/>
</item>
<item row="5" column="4">
<widget class="QPushButton" name="selectNonMacroButton">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QCheckBox" name="activeCheckBox">
<property name="toolTip">
<string>Toggles visibility of toolbar. You can disable the toolbar from here without deleting it.</string>
</property>
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string>Active</string>
</property>
</widget>
</item>
<item row="12" column="7">
<widget class="QPushButton" name="fromMacroButton">
<property name="toolTip">
<string>Attempt to extract icon information embedded in macro file as __icon__ or __xpm__ variable.</string>
</property>
<property name="text">
<string>From macro</string>
</property>
</widget>
</item>
<item row="11" column="2">
<widget class="Gui::AccelLineEdit" name="LineEditShortcut" native="true">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Gui::AccelLineEdit widget, select and press your desired shortcut key modifier key combination</string>
</property>
</widget>
</item>
<item row="22" column="0" colspan="8">
<widget class="QPlainTextEdit" name="PlainTextEditExtracted">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>100</height>
</size>
</property>
<property name="toolTip">
<string>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.</string>
</property>
</widget>
</item>
<item row="23" column="0" colspan="8">
<widget class="QLabel" name="statusLabel">
<property name="toolTip">
<string>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.</string>
</property>
<property name="frameShape">
<enum>QFrame::Box</enum>
</property>
<property name="text">
<string>Messages go here.</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>Gui::AccelLineEdit</class>
<extends>QWidget</extends>
<header>gui::accellineedit.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
'''
dlg = CustomToolbarEditor()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment