Last active
October 27, 2023 22:01
-
-
Save mwganson/3464e2d54e859ee94ec8d7ce20c75660 to your computer and use it in GitHub Desktop.
Easily manage custom macro toolbars in FreeCAD
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 "application-python" 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